/* Chrome shared by every page: the settings popout, the sign-in card, and the onboarding
   controls. It reads the palette from style.css, so it themes for free. The landing page loads it
   too, that page has its own poster palette, so the few places where the two would clash are
   handled at the bottom of this file rather than by forking the component. */

/* --- Settings ------------------------------------------------------------------------------- */

/* Fixed rather than part of a header bar: the trainer and the analysis board are centred layouts
   sized against the viewport, and threading a header through them would move the board. */
.settings-root {
  position: fixed;
  top: 14px;
  right: 16px;
  z-index: 200;
  font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, Helvetica, Arial, sans-serif;
}

.settings-btn {
  display: flex;
  align-items: center;
  justify-content: center;
  width: 34px;
  height: 34px;
  padding: 0;
  border: 1px solid var(--border);
  border-radius: 8px;
  background: var(--surface);
  color: var(--text-dim);
  cursor: pointer;
  transition: color 120ms ease, border-color 120ms ease;
}

.settings-btn:hover { color: var(--text); border-color: var(--gold); }
.settings-btn svg { width: 18px; height: 18px; display: block; }

.settings-btn:focus-visible,
.settings-panel button:focus-visible,
.settings-panel a:focus-visible {
  outline: 2px solid var(--gold);
  outline-offset: 2px;
}

/* The same panel inside a dashboard card rather than hanging off the gear: no positioning, no
   ceiling on its width, and the card around it already carries the border and the ground. */
/* `.settings-panel.settings-inline`, not `.settings-inline`: the two carry the same class-level
   specificity and this block sits above the popover's, so on a plain class the popover's
   `position: absolute` and `right: 0` won and the panel rendered clipped against the card's right
   edge. Naming both classes settles it by weight rather than by order. */
.settings-panel.settings-inline {
  position: static;
  /* Across, not down. Three stacked sections in a card this wide is a column of controls with a
     card's worth of empty beside it; side by side each one gets a readable measure and the card
     stops being taller than everything around it. They wrap to one column when there is not room
     for two, which is what `auto-fit` and the 240px floor are for. */
  display: grid;
  grid-template-columns: repeat(auto-fit, minmax(240px, 1fr));
  gap: 12px 28px;
  align-items: start;
  width: 100%;
  padding: 0;
  border: none;
  border-radius: 0;
  background: none;
  box-shadow: none;
}

.settings-panel {
  position: absolute;
  top: 42px;
  right: 0;
  /* Wide enough that "Show legal moves" fits on one line beside its toggle. At 232px it wrapped,
     and a two-line label beside a one-line control is the thing that made this panel look untidy
     before any of its contents did. */
  width: 254px;
  padding: 14px;
  border: 1px solid var(--border);
  border-radius: 10px;
  background: var(--surface);
  box-shadow: var(--shadow-pop);
  color: var(--text);
  font-size: 13px;
}

.settings-panel[hidden] { display: none; }

.settings-section + .settings-section {
  margin-top: 14px;
  padding-top: 12px;
  border-top: 1px solid var(--border);
}

/* Side by side, the rule between them is a rule between *columns*, which is not what a top border
   draws. The grid's own gap does the separating instead. */
.settings-inline .settings-section + .settings-section {
  margin-top: 0;
  padding-top: 0;
  border-top: none;
}

.settings-label {
  display: block;
  margin-bottom: 7px;
  font-size: 11px;
  font-weight: 700;
  letter-spacing: 0.08em;
  text-transform: uppercase;
  color: var(--text-faint);
}

/* Mutually exclusive choices in the width of a popout: a segmented control says "pick one" without
   spending a row per option. Columns are derived rather than fixed at three, so the same control
   serves the theme (three) and the on/off toggles (two). */
.segmented {
  display: grid;
  grid-auto-flow: column;
  grid-auto-columns: 1fr;
  gap: 3px;
  padding: 3px;
  border-radius: 7px;
  background: var(--sunken);
}

.segmented button {
  padding: 6px 4px;
  border: none;
  border-radius: 5px;
  background: transparent;
  color: var(--text-dim);
  font: inherit;
  font-size: 12px;
  font-weight: 600;
  cursor: pointer;
}

.segmented button:hover { color: var(--text); }

.segmented button[aria-pressed="true"] {
  background: var(--gold);
  color: var(--on-gold);
}

/* A named setting beside its control. The control is given a floor so a two-option toggle does not
   shrink to the width of the word "Off" when the label beside it is long. */
.settings-row {
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: 10px;
}

.settings-row .segmented { flex: 0 0 96px; }

/* Rows sat flush against each other, which was survivable while each was a label and a control on
   one line. It stopped being so once two of them stacked their control underneath the label: the
   swatch strip and the *next* row's label ended up nearer to each other than that label was to its
   own swatches, so "Pieces" read as a caption on the board colours. A stacked row gets the larger
   gap for that reason. It is the one whose top edge has to be unambiguous. */
.settings-row + .settings-row { margin-top: 9px; }
.settings-row + .settings-row.is-stacked { margin-top: 14px; }

.settings-row-label {
  font-size: 12.5px;
  color: var(--text);
}

.settings-hint {
  margin: 7px 0 0;
  font-size: 11.5px;
  line-height: 1.4;
  color: var(--text-faint);
}

.settings-links { display: flex; flex-direction: column; gap: 2px; }

/* One shape for every row, whether it is a link or a button. They used to be bare text of differing
   lengths, one of which, "Dashboard, accounts, tactic settings, primers", was a table of
   contents wedged into a menu item and wrapped onto three lines in a 232px popout. Icon, one word
   or two, done. */
.settings-item {
  display: flex;
  align-items: center;
  gap: 9px;
  width: 100%;
  padding: 7px 8px;
  border: none;
  border-radius: 7px;
  background: transparent;
  color: var(--text);
  font: inherit;
  font-size: 12.5px;
  text-align: left;
  text-decoration: none;
  cursor: pointer;
}

.settings-item:hover { background: var(--raised); }
.settings-item:disabled { opacity: 0.55; cursor: default; }

.settings-icon {
  flex: none;
  width: 15px;
  height: 15px;
  color: var(--text-faint);
}

.settings-item:hover .settings-icon { color: var(--gold-ink); }

/* Who you are, as a row rather than a sentence. The initial is not decoration: at a glance it is
   the fastest confirmation that the app thinks you are the account you meant to be. */
.settings-account {
  display: flex;
  align-items: center;
  gap: 9px;
  margin-bottom: 9px;
  padding: 7px 8px;
  border-radius: 8px;
  background: var(--surface-2);
}

.settings-avatar {
  flex: none;
  display: grid;
  place-items: center;
  width: 28px;
  height: 28px;
  border-radius: 8px;
  background: color-mix(in srgb, var(--gold) 16%, var(--surface));
  box-shadow: inset 0 0 0 1px color-mix(in srgb, var(--gold) 28%, transparent);
  color: var(--gold-ink);
  font-size: 13px;
  font-weight: 700;
}

.settings-account-text {
  display: flex;
  flex-direction: column;
  gap: 1px;
  min-width: 0;
}

.settings-account-name {
  font-size: 12.5px;
  font-weight: 600;
  color: var(--text);
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
}

.settings-account-sub { font-size: 10.5px; color: var(--text-faint); }

/* --- Account pages -------------------------------------------------------------------------- */

/* `width: 100%` is not decoration, and removing it makes the card mysteriously narrow.
   style.css puts `display: flex` on `<body>` for the trainer's board layout, so this shell is a
   flex item, and a flex item is shrink-to-fit, not full width. Without an explicit width the shell
   collapses to the widest thing inside it, and the card's own `min(460px, 100%)` then resolves its
   `100%` against *that* rather than against the viewport. It was a stray line of small print at the
   bottom setting the width of every page in the funnel, holding the card 87px under its own
   declared size. The line has since gone; the reason to pin the width has not. */
.auth-shell {
  width: 100%;
  min-height: 100vh;
  display: flex;
  flex-direction: column;
  align-items: center;
  justify-content: center;
  gap: 18px;
  padding: 48px 20px 72px;
}

.auth-card {
  width: min(460px, 100%);
  padding: 34px 32px;
  border: 1px solid var(--border);
  border-radius: 14px;
  background: var(--surface);
  box-shadow: var(--shadow-pop);
}

.auth-card.wide { width: min(620px, 100%); }

.auth-brand {
  display: flex;
  flex-direction: column;
  align-items: center;
  gap: 10px;
  margin-bottom: 22px;
  text-decoration: none;
  color: inherit;
}

/* Sized by height, not by a square box. The mark is tall and narrow, so constraining the width
   would leave it a fraction of the height it was given and the strokes would go to mush.

   86px is why the auth cards get their own file. `logo-mark-sm.png` is only 160px tall, which is
   1.9x this and visibly soft on any retina screen, so these three pages ship `logo-mark-md.png`
   (144x258, 15KB): 3x the render, and still a fifteenth of the 240KB master they used to load.
   Both derived files are Lanczos downscales of `logo-mark.png` quantised to 256 colours, which
   costs about 2/255 rms on colour and 4/255 on alpha, invisible at these sizes. `logo-mark.png`
   stays the master and stays unquantised; regenerate from it, never from these. */
.auth-brand img { height: 86px; width: auto; display: block; }

/* The mark was drawn to glow on navy, and with the navy keyed out its paler end nearly vanishes
   against a white card. Deepening it in the light theme is the same move the palette already makes
   for `--gold`, which drops from #f0a92b to #e09a1c for exactly this reason. */
:root[data-theme="light"] .auth-brand img {
  filter: brightness(0.86) saturate(1.18);
}

.auth-brand span {
  font-size: 22px;
  font-weight: 700;
  letter-spacing: 0.14em;
  text-transform: uppercase;
  color: var(--text);
}

.auth-title {
  margin: 0 0 5px;
  font-size: 23px;
  font-weight: 700;
  text-align: center;
  color: var(--text);
}

.auth-sub {
  margin: 0 0 22px;
  font-size: 14.5px;
  line-height: 1.5;
  text-align: center;
  color: var(--text-dim);
}

.field { display: flex; flex-direction: column; gap: 7px; margin-bottom: 16px; }

.field label {
  font-size: 13px;
  font-weight: 600;
  color: var(--text-dim);
}

.field input {
  padding: 12px 14px;
  border: 1px solid var(--border);
  border-radius: 7px;
  background: var(--sunken);
  color: var(--text);
  font-size: 15.5px;
}

.field input::placeholder { color: var(--text-faint); }

.field input:focus-visible {
  outline: 2px solid var(--gold);
  outline-offset: 1px;
}

.field-hint { font-size: 11.5px; color: var(--text-faint); }

/* A choice, not a field. `.field` stacks a label above an input and that is exactly wrong here:
   the box and the sentence it belongs to have to sit on one line, or the tick reads as belonging
   to the control above it. `align-items: flex-start` keeps the box against the first line of a
   sentence that wraps to three. */
.field-check {
  display: flex;
  align-items: flex-start;
  gap: 10px;
  margin: 0 0 18px;
  font-size: 12.5px;
  line-height: 1.5;
  color: var(--text-dim);
  cursor: pointer;
}

/* 2px down, because a checkbox's own box is centred in its line box and a 12.5px line is taller
   than the control. Without it the tick floats above the first word. */
.field-check input {
  flex: 0 0 auto;
  width: 15px;
  height: 15px;
  margin: 2px 0 0;
  accent-color: var(--gold);
  cursor: pointer;
}

.field-check:hover span { color: var(--text); }

.auth-error,
.auth-ok {
  margin: 0 0 14px;
  padding: 9px 11px;
  border-radius: 7px;
  font-size: 12.5px;
  line-height: 1.45;
}

.auth-error { background: rgba(232, 97, 92, 0.14); color: var(--bad); }
.auth-ok { background: rgba(75, 191, 114, 0.14); color: var(--good); }
.auth-error[hidden], .auth-ok[hidden] { display: none; }

.auth-submit { width: 100%; padding: 14px 16px; font-size: 15px; }

.auth-alt {
  margin: 18px 0 0;
  font-size: 13.5px;
  text-align: center;
  color: var(--text-dim);
}

.auth-alt a { color: var(--gold-ink); }

/* The line under the sign-up form saying what creating an account agrees to. Quieter and smaller
   than `.auth-alt` above it, because "log in instead" is an offer and this is a disclosure: it has
   to be legible and findable, and it must not compete with the button. */
.auth-legal {
  margin: 14px 0 0;
  font-size: 12px;
  line-height: 1.5;
  text-align: center;
  color: var(--text-faint);
}

.auth-legal a { color: var(--text-dim); }
.auth-legal a:hover { color: var(--gold-ink); }

/* --- Linking a chess account ---------------------------------------------------------------- */

.link-row {
  display: flex;
  align-items: flex-end;
  gap: 8px;
  margin-bottom: 6px;
}

.link-row .field { flex: 1; margin-bottom: 0; }
.link-row .btn { flex: none; padding: 10px 14px; }

.link-state {
  margin: 0 0 18px;
  font-size: 12px;
  line-height: 1.45;
  min-height: 16px;
  color: var(--text-faint);
}

.link-state.linked { color: var(--good); }
.link-state.failed { color: var(--bad); }

.platform-head {
  display: flex;
  align-items: baseline;
  gap: 8px;
  margin-bottom: 8px;
}

.platform-head h3 {
  margin: 0;
  font-size: 14px;
  font-weight: 700;
  color: var(--text);
}

.platform-head span { font-size: 11.5px; color: var(--text-faint); }

/* --- Signing in through someone else --------------------------------------------------------- */

.provider-buttons {
  display: flex;
  flex-direction: column;
  align-items: stretch;
  gap: 10px;
  margin-bottom: 4px;
}

/* Google renders its own button into a div it owns, and centres it inside whatever width it is
   given, so the row is centred rather than the button stretched. */
.provider-buttons > div { display: flex; justify-content: center; }

/* Sized to Google's button rather than to the card.
 *
 * Google will not be styled, it draws its own button and only takes a width, so the only way the
 * two read as a pair is for this one to copy it. `renderButton` in ui.js asks for `width: 320` and
 * `size: 'large'`, which comes out 320x40 with a 4px corner and a 14px label; those are the numbers
 * below. Left to stretch, this one filled the card at 394px and stood a head taller than the button
 * under it, which made the second option look like the afterthought.
 *
 * The mark is pinned to the left edge and the label centred across the whole button, which is how
 * Google arranges its own. Centring the pair together instead leaves the two buttons' text starting
 * at different places, and the eye reads that before it reads either word. */
/* Styled as Google's button, not as one of ours.
 *
 * These two are a single row offering the same kind of thing, and they only read that way if they
 * are the same kind of object. Ours was a filled navy panel beside Google's white outline card,
 * matched in size and still obviously two different buttons that happened to be stacked. So the
 * colours here are Google's own: #fff on #dadce0, #3c4043 text at 500, and their faint blue hover.
 *
 * They stay light in both themes, because Google's does. `theme: 'outline'` renders white whatever
 * the page around it is doing, and a navy card with one white button and one dark one would be
 * worse than a navy card with two white ones. */
.btn-provider {
  position: relative;
  display: flex;
  align-items: center;
  justify-content: center;
  width: 320px;
  max-width: 100%;
  height: 40px;
  margin: 0 auto;
  /* Asymmetric, and that is what puts the two labels on the same axis. Google does not centre its
     text across the button, it centres it in what is left *after* the icon, a box running from
     39px to 307px, so the label lands at 173px rather than at the button's own midpoint of 160.
     Centring ours across the full width put it 13px to the left of Google's, which is plainly
     visible with one stacked above the other. Matching the padding matches the axis. */
  padding: 0 13px 0 39px;
  border: 1px solid #dadce0;
  border-radius: 4px;
  background: #fff;
  color: #3c4043;
  /* Google's own stack, copied exactly rather than approximated. Ours was the app's system stack,
     which resolves to Segoe UI on Windows while Google's falls through to Arial, two different
     typefaces at the same size, side by side. Listing the identical fonts in the identical order is
     the only way the two resolve to the same face on a machine we cannot see. The 0.25px of
     tracking is theirs too. */
  font-family: "Google Sans", arial, sans-serif;
  font-size: 14px;
  font-weight: 500;
  letter-spacing: 0.25px;
  text-align: center;
  text-decoration: none;
}

.btn-provider:hover {
  border-color: #d2e3fc;
  background: #f7faff;
  color: #3c4043;
}

.btn-provider:focus-visible {
  outline: 2px solid var(--gold);
  outline-offset: 2px;
}

/* The same 18px box and the same 13px inset as Google's mark, which is only correct now that ours
   is a bare glyph too. While it was a filled tile it had to be shrunk to 16px to stop it reading as
   the heavier of the two; with the plate gone, an open knight and an open G at the same size weigh
   the same, and matching Google exactly is the simpler rule. */
.btn-provider .provider-icon {
  position: absolute;
  left: 13px;
  display: block;
  width: 18px;
  height: 18px;
}

/* A rule with the word "or" sitting in the gap. Two pseudo-element lines rather than a border on
   the text, so the lines meet the card's edges however long the label is. */
.auth-divider {
  display: flex;
  align-items: center;
  gap: 10px;
  margin: 18px 0;
  color: var(--text-faint);
  font-size: 11.5px;
}

.auth-divider::before,
.auth-divider::after {
  content: "";
  flex: 1;
  height: 1px;
  background: var(--border);
}

.auth-divider[hidden] { display: none; }

/* The optional step under each platform. Quieter than the row above it on purpose: taking it is
   worth doing and not taking it must not read as an unfinished form. */
.upgrade-note {
  margin: -8px 0 18px;
  font-size: 11.5px;
  line-height: 1.5;
  color: var(--text-faint);
}

.upgrade-note a {
  color: var(--gold-ink);
  text-decoration: underline;
  text-underline-offset: 2px;
}

/* --- Proving a chess.com account is yours ----------------------------------------------------- */

.verify-steps {
  margin: 12px 0 10px;
  padding: 14px;
  border: 1px solid var(--border);
  border-radius: 10px;
  background: var(--sunken);
}

.verify-steps[hidden] { display: none; }

.verify-steps ol {
  margin: 0 0 12px;
  padding-left: 18px;
  font-size: 12.5px;
  line-height: 1.7;
  color: var(--text-dim);
}

.verify-steps strong { color: var(--text); }

/* Without this the link inherits the browser's default blue, which on the dark card is very nearly
   the same value as the card itself and effectively invisible. */
.verify-steps a {
  color: var(--gold-ink);
  text-decoration: underline;
  text-underline-offset: 2px;
}

.verify-code {
  display: inline-block;
  padding: 2px 7px;
  border-radius: 5px;
  background: var(--raised);
  color: var(--gold-ink);
  font-family: ui-monospace, SFMono-Regular, Menlo, Consolas, monospace;
  font-size: 12.5px;
  user-select: all; /* one click selects the whole code, which is the only thing anyone wants here */
}

/* --- Onboarding preferences ----------------------------------------------------------------- */

.tier-readout {
  display: flex;
  align-items: baseline;
  justify-content: space-between;
  gap: 10px;
  margin-bottom: 10px;
}

.tier-name {
  font-size: 22px;
  font-weight: 700;
  color: var(--gold-ink);
}

.tier-cp {
  font-size: 13px;
  font-variant-numeric: tabular-nums;
  color: var(--text-dim);
}

/* A native range input, restyled. It is keyboard-operable and screen-reader-labelled for free,
   which a div-with-a-drag-handler is not. */
.tier-slider {
  width: 100%;
  margin: 0;
  appearance: none;
  -webkit-appearance: none;
  height: 22px;
  background: transparent;
  cursor: pointer;
}

/* Gold deepens toward the hard end, so the track itself says which way is harder, and everything
   past the thumb is dimmed, so it also says how far along you are. `--tier-fill` is set by setup.js
   on every frame of the drag; the hard stop is what keeps the boundary crisp rather than smearing a
   second gradient over the first. */
.tier-slider::-webkit-slider-runnable-track {
  height: 6px;
  border-radius: 3px;
  background:
    linear-gradient(90deg,
      var(--surface-2) 0%, var(--gold) var(--tier-fill, 50%),
      var(--surface-2) var(--tier-fill, 50%), var(--surface-2) 100%);
}

.tier-slider::-moz-range-track {
  height: 6px;
  border-radius: 3px;
  background:
    linear-gradient(90deg,
      var(--surface-2) 0%, var(--gold) var(--tier-fill, 50%),
      var(--surface-2) var(--tier-fill, 50%), var(--surface-2) 100%);
}

.tier-slider::-webkit-slider-thumb {
  -webkit-appearance: none;
  width: 18px;
  height: 18px;
  margin-top: -6px;
  border: 2px solid var(--on-gold);
  border-radius: 50%;
  background: var(--gold-bright);
}

.tier-slider::-moz-range-thumb {
  width: 18px;
  height: 18px;
  border: 2px solid var(--on-gold);
  border-radius: 50%;
  background: var(--gold-bright);
}

.tier-slider:focus-visible { outline: 2px solid var(--gold); outline-offset: 4px; }

.tier-ends {
  display: flex;
  justify-content: space-between;
  margin-top: 6px;
  font-size: 11px;
  letter-spacing: 0.06em;
  text-transform: uppercase;
  color: var(--text-faint);
}

.tier-blurb {
  margin: 12px 0 0;
  font-size: 12.5px;
  line-height: 1.55;
  color: var(--text-dim);
}

.tc-grid {
  display: grid;
  grid-template-columns: repeat(2, 1fr);
  gap: 8px;
}

/* The whole tile is the hit target, and the checkbox inside it stays real so the label, the
   keyboard and the accessibility tree all keep working. */
.tc-option {
  display: flex;
  align-items: center;
  gap: 9px;
  padding: 11px 12px;
  border: 1px solid var(--border);
  border-radius: 8px;
  background: var(--sunken);
  cursor: pointer;
  user-select: none;
}

.tc-option:hover { border-color: var(--gold-dim); }

.tc-option:has(input:checked) {
  border-color: var(--gold);
  background: var(--surface-2);
}

.tc-option:has(input:focus-visible) {
  outline: 2px solid var(--gold);
  outline-offset: 2px;
}

.tc-option input { accent-color: var(--gold); width: 15px; height: 15px; flex: none; }

.tc-text { display: flex; flex-direction: column; gap: 1px; line-height: 1.25; }
.tc-name { font-size: 13.5px; font-weight: 600; color: var(--text); }
.tc-note { font-size: 10.5px; color: var(--text-faint); }

.setup-section + .setup-section {
  margin-top: 24px;
  padding-top: 22px;
  border-top: 1px solid var(--border);
}

.setup-heading {
  margin: 0 0 4px;
  font-size: 15px;
  font-weight: 700;
  color: var(--text);
}

.setup-lede {
  margin: 0 0 16px;
  font-size: 12.5px;
  line-height: 1.5;
  color: var(--text-dim);
}

/* --- Landing page overrides ------------------------------------------------------------------ */

/* landing.css defines its own poster palette and does not load style.css, so the settings popout
   would have no variables to read there. These are the poster's values, hard-coded once. */
body.landing-page {
  --border: rgba(216, 172, 83, 0.3);
  --surface: rgba(12, 9, 7, 0.94);
  --sunken: rgba(0, 0, 0, 0.5);
  --raised: rgba(216, 172, 83, 0.14);
  --text: #efe3c6;
  --text-dim: #b7a684;
  --text-faint: #7d7059;
  --on-gold: #201505;
  --shadow-pop: 4px 4px 0 rgba(0, 0, 0, 0.55);
}

/* --- Home: the hub between signing in and solving -------------------------------------------- */
/*
   Deliberately a wide, calm page rather than another 420px card. The auth cards are narrow because
   they ask one question at a time; this one is a place to look around, and everything on it used to
   be crammed into a popover anchored to the top-right corner.
*/
.dash-page {
  display: block;
  background: var(--bg);
}

/* One gap, set once, rather than a margin per block. The blocks down this page had picked up four
   different spacings, 20px under the heading, 18px under the hero, none at all between the
   statistics card and the grid below it, 22px above the footer, against a 14px gap *inside* the
   card grid. A column with a single gap cannot drift like that, and it is the same 14px the cards
   use, so the vertical and horizontal rhythm match. */
/* Sized and spaced to land the whole page on one screen at 1280x800, which is what it is for: the
   dashboard is a place you glance at on the way to the trainer, and anything below the fold on it
   was, in practice, not being looked at.

   Wider than it was (920 was a reading measure, and none of this is prose), with the padding and
   the gaps pulled in and the top band split into two columns. Nothing was removed to get there. */
/* One number for every gap on this page.
 *
 * The three that matter, the column's own gap, the top band's and the card grid's, were 8px, 9px
 * and 8px: three hand-set values for one rhythm, which is how they drifted a pixel apart in the
 * first place. Naming it means the page's density is one edit rather than three, and the next
 * person to think it is still too tight has somewhere obvious to go. */
.dash { --dash-gap: 16px; }

/* The layout every `.dash` page had, and the one that still applies to any of them without a rail
   in it. `openings.html`, `history.html`, `trophies.html` and `account.html` all use this class and
   none of them has a rail yet; the grid below took them over and squeezed each page's header into a
   phantom 352px first column with the rest of the page sprawling edge to edge beside it. */
.dash {
  width: min(1240px, 100%);
  margin: 0 auto;
  /* Air at the top, 28px, the same as the dashboard's column. It was 48, which was clearance for a
     fixed brand mark in the corner; the mark was removed on 2026-09-11. */
  padding: 28px 18px 24px;
  display: flex;
  flex-direction: column;
  gap: var(--dash-gap);
}

/* Two columns, on the *body*, because the rail is the app's navigation now rather than one page's
   and it ships on seven pages whose main elements have nothing in common. `with-rail` in the markup
   rather than `:has(.dash-rail)`, so the column is reserved from the moment the document is parsed
   and nothing moves when `rail.js` fills it: what appears is the rail's contents, not the space for
   them.

   `.dash-bar` and `.dash-sheet` are both `position: fixed`, so they are out of flow and
   never become grid items; everything else on the page lands in the second column. */
body.with-rail {
  display: grid;
  grid-template-columns: 232px minmax(0, 1fr);
  align-items: start;
  justify-content: stretch;
  min-height: 100vh;
}

body.with-rail > .dash-rail { grid-column: 1; }

/* The three boards size themselves against `100vw`, and the rail takes 232px of that before they
   see it. Measured at 960x1200, where the width budget wins over the height one: a 608px board in
   728px of column, 197px of the page hanging off the right edge.

   `min-width: 901px` because below that the rail is a bottom bar and takes no width at all, and the
   stacked budgets in `style.css` are already right; without the query these would win anyway, since
   this file is linked second, and would shrink every phone board by a rail that is not there. */
@media (min-width: 901px) {
  /* The trainer and the analysis board are content-width layouts that were centred by the body
     until the body became a grid; in a stretched column they sat against its left edge with the
     slack piled up on the right. Centred within the column they are given. */
  body.with-rail > .page { justify-self: center; }

  /* Even air above and below the board. Measured at 1440x900: 139px over it and 45 under, so it sat
     hard against the bottom of the window with a band of empty page over the top.

     Most of that 139 is content, the prompt row and the primer track, and those have to be above the
     board. What was uneven is the *whitespace* around them: 45px of page margin plus 24 of padding
     over the top against 24 underneath. This makes it 24 and 24 against 48, which is the same 48px
     of air at each end.

     **It costs no board.** The margin comes off the top and goes on the bottom, so the furniture the
     size budget is measured against is within 3px of what it was; the trainer's board is still 716px
     at this size. The margin could go because the fixed brand mark had moved over the rail, which was
     the only reason it was 48 in the first place; the mark has since been removed altogether. */
  .with-rail .page:not(.analysis-page) {
    margin-top: 24px;
    padding-bottom: 48px;
  }

  .with-rail .page { --board-size: max(260px, min(calc(100vw - 584px), calc(100svh - 184px))); }
  /* The analysis board's vertical budget, re-measured at 1916x990 on 2026-09-11 after it was reported
     stopping short of the bottom of the window. The 296 it had still allowed for a row over the board
     that the turn chip used to fill; the chip has moved into the panel, so the board plus its FEN and
     PGN boxes ended 98px above the window's foot.

     What is really spent, with the trainer's 24px margin rather than `.page`'s 48 (for the reason given
     above): 24 of margin and 24 of padding over
     the board, and under it the source line (8, plus 16 once the board was opened from a game), the
     FEN and PGN boxes (8 + 109.5), and 24 of padding. 213.5px at most, against a budget of 220.
     Measured after: a 770px board at 1916x990 (694 before), its boxes ending 46.5px above the
     window's foot opened cold and 30.5 opened from a tactic, and no scrollbar either way. */
  .with-rail .analysis-page {
    margin-top: 24px;
    --board-size: max(260px, min(calc(100vw - 708px), calc(100svh - 220px)));
  }
  .with-rail .rush { --board-size: max(240px, min(calc(100vw - 434px), calc(100dvh - 264px))); }
}
body.with-rail > :not(.dash-rail) { grid-column: 2; min-width: 0; }

/* The dashboard is the one railed page that does its own padding, in `.dash-main`. Everywhere else
   the centred column that `.dash` has always been is exactly right inside the second column, so it
   is left alone. Told apart by what it holds rather than by a page name. */
.dash:has(> .dash-main) {
  width: auto;
  max-width: none;
  margin: 0;
  padding: 0;
  display: block;
}

/* What to do next, and how it has been going. Two columns, hero narrower: it is a number and a
   button, where the statistics panel is three groups of figures and wants the room. */
.dash-top {
  display: grid;
  /* One narrow rail and then everything left over. The hero is a two-digit number and a button: it
     does not get better with more width, it just gets emptier, so the remainder goes to the
     statistics, which do.
     There were two rails here, the second of them the motif of the day. That card is in the grid
     now, where it has room for the position, and this band is the better for it: the daily was the
     only panel up here that wanted to be taller than a number and a button, and every panel in a
     grid row pays for the tallest one. */
  grid-template-columns: minmax(132px, 206px) minmax(0, 1fr);
  gap: var(--dash-gap);
  align-items: stretch;
}

@media (max-width: 700px) {
  .dash-top { grid-template-columns: 1fr; }
}

.dash-head { margin: 0; }

.dash-title {
  margin: 0 0 2px;
  font-size: 20px;
  font-weight: 800;
  letter-spacing: -0.01em;
}

.dash-sub {
  margin: 0;
  font-size: 12.5px;
  color: var(--text-dim);
}

/* The one action the page is for, given the room to say so. */
/* Stacked, not side by side: the count sits above the button it explains. Across, the figure and
   the button each claimed half a panel and the divider between them made a narrow rail narrower
   still. */
.dash-hero {
  display: flex;
  flex-direction: column;
  align-items: center;
  justify-content: center;
  text-align: center;
  gap: 16px;
  padding: 22px 18px;
  border: 1px solid var(--gold-dim);
  border-radius: 16px;
  background:
    radial-gradient(120% 140% at 0% 0%, color-mix(in srgb, var(--gold) 12%, transparent), transparent 60%),
    var(--surface);
  box-shadow: var(--shadow-pop);
}

.hero-figure {
  display: flex;
  flex-direction: column;
  align-items: center;
  flex: none;
}

.hero-count {
  font-size: 40px;
  font-weight: 800;
  line-height: 1;
  color: var(--gold-ink);
  font-variant-numeric: tabular-nums;
}

.hero-label {
  margin-top: 4px;
  font-size: 11.5px;
  text-transform: uppercase;
  letter-spacing: 0.08em;
  color: var(--text-dim);
}

/* A flex row of its own, so the button inside it is centred rather than left in the corner. It is
   a plain block otherwise, and `.btn` is inline-flex, so the anchor sized to its own text and sat
   against the left edge while the count above it was centred. */
.hero-action {
  flex: none;
  display: flex;
  /* A column since the allowance meter joined the button underneath it. With one child, which is
     what the classic shell has, a column and a row are the same picture. */
  flex-direction: column;
  align-items: center;
  gap: 12px;
  width: 100%;
  min-width: 0;
}

/* Only the extra padding is its own: `.btn` handles being an anchor, inline-flex, centred, no
   underline, for every link styled as a button, not just this one. */
/* Centred under the count it acts on, and sized to its own label. */
.btn-hero {
  justify-content: center;
  padding: 10px 18px;
}

/* Cards flow into as many columns as the window allows, so the page reads the same on a laptop and
   a phone without a stack of breakpoints. */
/* Three across on a desktop, explicitly. `auto-fit` at the old measure gave two wide columns and
   a third row nobody could see; six cards in two rows of three is what fits. */
.dash-grid {
  display: grid;
  grid-template-columns: repeat(3, minmax(0, 1fr));
  gap: var(--dash-gap);
}

@media (max-width: 1080px) {
  .dash-grid { grid-template-columns: repeat(auto-fit, minmax(260px, 1fr)); }
}

/* `/dashboard-classic` keeps the layout it was built for, and now gets it the same way every other
   railless page does: it has no `.dash-rail`, so the base rule above is already the right one and
   the only thing this still has to say is that it should not be a full-height column. */
.dash-classic .dash { min-height: 0; }

/* The motif of the day, as a board rather than a picture of one. `.board` is `height: 100%`, so the
   wrapper is what gives it a size; square, and as wide as the card lets it be. */
.daily-live {
  position: relative;
  width: 100%;
  aspect-ratio: 1;
}

.daily-verdict {
  margin: 8px 0 0;
  font-size: 13px;
  color: var(--text-dim);
  min-height: 1.4em;
}

.daily-verdict.correct { color: var(--good-ink); font-weight: 600; }
.daily-verdict.incorrect { color: var(--bad); font-weight: 600; }

/* --- The Statistics tab's own cards ----------------------------------------------------------- */

.stat-line { display: flex; flex-direction: column; gap: 5px; }
.stat-line + .stat-line { margin-top: 14px; }

.stat-line-head {
  display: flex;
  align-items: baseline;
  justify-content: space-between;
  gap: 12px;
}

.stat-line-label { font-size: 13.5px; color: var(--text); }
.stat-line-head b { font-size: 15px; font-weight: 700; font-variant-numeric: tabular-nums; }

.stat-line-bar {
  height: 6px;
  border-radius: 999px;
  background: var(--track);
  overflow: hidden;
}

.stat-line-bar i { display: block; height: 100%; border-radius: 999px; background: var(--gold); }
.stat-line-note { font-size: 12px; color: var(--text-faint); }

/* One column per day you did something, not per calendar day: a month of empty columns with three
   bars in it is a picture of the calendar rather than of the work. */
.stat-days {
  display: flex;
  align-items: flex-end;
  gap: 8px;
  height: 132px;
  margin: 6px 0 10px;
  overflow-x: auto;
  scrollbar-width: thin;
}

.stat-day {
  flex: 1 0 34px;
  min-width: 34px;
  display: flex;
  flex-direction: column;
  align-items: center;
  gap: 6px;
  height: 100%;
  justify-content: flex-end;
}

.stat-day-bar {
  position: relative;
  width: 100%;
  min-height: 3px;
  border-radius: 4px 4px 0 0;
  background: var(--surface-2);
  display: flex;
  align-items: flex-end;
}

/* The solved share, drawn from the bottom of the day it belongs to. */
.stat-day-bar i { display: block; width: 100%; border-radius: 0 0 4px 4px; background: var(--good); }
.stat-day span { font-size: 10.5px; color: var(--text-faint); font-variant-numeric: tabular-nums; }

/* --- The rail, and the tabs behind it ---------------------------------------------------------

   The dashboard is a rail and one panel now. `dash-tabs.js` sets `data-dash-tab` on the root and
   moves sections between the Train tab's slots, the panel every other tab renders into, and a hidden
   store; everything below is layout for those three places and nothing here knows which tab is open.

   `.dash` was a centred column with a max-width. It is a two-column grid here, and the *content*
   keeps the max-width instead, so the rail sits against the window edge the way a rail should while
   a table of five columns still stops before it becomes unreadable on a wide monitor. */
.dash-rail {
  position: sticky;
  top: 0;
  align-self: start;
  /* The viewport's height, not the content's. Shrink-wrapped it was 665px of dark column in a 993px
     page, so the rail stopped two thirds of the way down and the page background carried on beneath
     it, which reads as the layout having run out rather than as a rail. Sticky at `top: 0` and
     exactly one screen tall, it fills the window at every scroll position and at every page length.
     `overflow-y` below is what happens when the rail itself is taller than the screen. */
  height: 100vh;
  overflow-y: auto;
  scrollbar-width: thin;
  display: flex;
  flex-direction: column;
  gap: 22px;
  width: 232px;
  /* 20px of air above the mark. It was 74 while the mark was fixed in the page's corner and painted
     over the top of the rail, and the mark is inside the rail now. */
  padding: 20px 14px 20px;
  background: var(--sunken);
  border-right: 1px solid var(--border);
}

/* The mark, at the top of the rail rather than fixed in the page's corner, which is where it sat
   until 2026-09-11: there it floated over whatever each page put beneath it, and four separate
   rules existed to keep content clear of it. In the rail it is one more thing in the column, it
   scrolls with it, and it is absent exactly where the rail is: on a phone, where the bar takes
   over, and on the pages that carry no rail at all. */
.rail-brand {
  display: flex;
  align-items: center;
  gap: 9px;
  /* The rail's own gap is 22px, which is the space between one group of links and the next. The
     mark is a heading rather than a group, so it sits closer to the first of them. */
  margin-bottom: -8px;
  padding: 0 12px;
  color: var(--text-dim);
  text-decoration: none;
  transition: color 120ms ease;
}

.rail-brand:hover { color: var(--text); }

.rail-brand:focus-visible { outline: 2px solid var(--gold); outline-offset: 3px; border-radius: 8px; }

/* Sized by height, not by a square box: the mark is tall and narrow, so constraining the width
   would leave it a fraction of the height it was given. 34px against `logo-mark-sm.png`'s 160 is
   over four times the render, so it holds up on a 3x screen. */
.rail-brand img { height: 34px; width: auto; display: block; }

/* The mark was drawn to glow on navy and its paler end nearly disappears on a light background,
   the same correction the sign-in card makes. */
:root[data-theme="light"] .rail-brand img { filter: brightness(0.86) saturate(1.18); }

.rail-brand span {
  font-size: 15px;
  font-weight: 700;
  letter-spacing: 0.14em;
  text-transform: uppercase;
}

.rail-group { display: flex; flex-direction: column; }

.rail-label {
  margin: 0 0 6px;
  padding: 0 12px;
  font-size: 10.5px;
  font-weight: 700;
  text-transform: uppercase;
  letter-spacing: 0.12em;
  color: var(--text-faint);
}

.rail-items { display: flex; flex-direction: column; gap: 3px; }

.rail-foot {
  margin-top: auto;
  padding-top: 12px;
  border-top: 1px solid var(--border);
}

.rail-item {
  /* Relative for the bottom bar's pip, which sits on the icon's corner rather than pushing off the
     label, and `z-index` so the item paints over `.rail-pill`: the pill is appended last, so in
     document order it is on top, and without this it covered the label of whichever item it was
     currently sitting on. */
  position: relative;
  z-index: 1;
  display: flex;
  align-items: center;
  gap: 12px;
  height: 42px;
  padding: 0 12px;
  border-radius: 6px;
  font-size: 14px;
  font-weight: 600;
  color: var(--text-dim);
  text-decoration: none;
}

.rail-item svg { width: 20px; height: 20px; flex: none; }

/* `.rail-item` sets a display, so the `hidden` attribute cannot turn one off on its own. The owner's
   Dev entry ships hidden on every page and is revealed by `setRailOwner`. */
.rail-item[hidden] { display: none; }
.rail-item:hover { background: var(--raised-hover); color: var(--text); }
/* No background of its own: `.rail-pill` draws it, so that it can travel between items rather than
   switch off here and on there. The colour stays, because that is about the text. */
.rail-item.on { background: none; color: var(--gold-ink); }

/* The travelling background. Behind the items, which are `position: relative` already, so their
   labels and icons sit over it without either needing a z-index. */
.rail-pill {
  position: absolute;
  left: 14px;
  right: 14px;
  top: 0;
  z-index: 0;
  border-radius: 6px;
  background: var(--raised);
  opacity: 0;
  pointer-events: none;
  transition: transform 260ms cubic-bezier(0.22, 0.61, 0.36, 1),
              height 260ms cubic-bezier(0.22, 0.61, 0.36, 1),
              opacity 140ms ease;
}

.rail-pill.is-on { opacity: 1; }

/* --- What stays put across a navigation ----------------------------------------------------------
 *
 * See the note on `@view-transition` in style.css. A name tells the browser that this element on the
 * page you left and that element on the page you arrived at are one thing, so it is held and moved
 * rather than faded out and faded in. Each of these is unique on a page, which the API requires: two
 * elements sharing a name in one document turns the transition off entirely.
 *
 * The pill is named as well as the rail, which is what lifts it out of the rail's snapshot and gives
 * it a group of its own to travel in. Without it the rail holds still and the pill jumps inside it,
 * which is the same result as no transition at all. */
.dash-rail { view-transition-name: rail; }
.rail-pill { view-transition-name: rail-pill; }
.dash-bar { view-transition-name: bar; }

/* Held to the same duration the pill uses within a page, so a move between tabs and a move between
   pages are the same gesture at the same speed rather than two different ones. */
::view-transition-group(rail-pill) { animation-duration: 260ms; }

@media (prefers-reduced-motion: reduce) {
  ::view-transition-group(*),
  ::view-transition-old(*),
  ::view-transition-new(*) { animation: none !important; }
}

/* The first placement of a session, and every resize: those are corrections rather than moves. */
.rail-pill.no-anim { transition: none; }

@media (prefers-reduced-motion: reduce) { .rail-pill { transition: none; } }
.rail-item:focus-visible { outline: 2px solid var(--gold); outline-offset: 2px; }

/* Hover is a state a finger cannot leave: tapping applies `:hover` and holds it until something
   else is tapped, so on a touch screen the last tab pressed stays lit as though the cursor were
   still on it. The same exception `.btn` carries, for the same reason. */
@media (hover: none) {
  .rail-item:hover { background: none; color: var(--text-dim); }
  .rail-item.on:hover { background: var(--raised); color: var(--gold-ink); }
}

/* The two marks the rail is allowed to carry: a pip for an unsolved motif of the day, a count for
   trophies earned since the last look. Both push right off the label rather than sitting in a
   reserved column, so an item without one is not paying for it. */
.rail-pip {
  margin-left: auto;
  width: 7px;
  height: 7px;
  flex: none;
  border-radius: 50%;
  background: var(--gold);
}

.rail-badge {
  margin-left: auto;
  min-width: 20px;
  height: 20px;
  padding: 0 6px;
  flex: none;
  display: flex;
  align-items: center;
  justify-content: center;
  border-radius: 999px;
  background: var(--gold);
  color: var(--on-gold);
  font-size: 11px;
  font-weight: 800;
  font-variant-numeric: tabular-nums;
}

.dash-main {
  min-width: 0;
  display: flex;
  flex-direction: column;
  gap: 22px;
  /* More on the right than the left. The left is a gap between the content and the rail, which
     is a boundary and wants to read as one; the right is the page edge, and the cards were
     running up against it. */
  padding: 28px 52px 0 34px;
}

/* The content uses the window. It used to stop at 1180px, which is the width the Train grid was
   drawn at, and on a 2560px screen that left 1114px of empty page to the right of everything: the
   rail against one edge, the cards bunched against it, and half the monitor showing the background.

   Capping it was defended as readability, and that argument only ever applied to one thing on the
   page, the record list, which is a five-column table. That one keeps a reading width of its own
   below. Everything else here is cards, and cards are content that scales. */
.dash-main > * { width: 100%; }

/* The exception. A list of positions, motifs, openings and dates is read left to right along each
   row, and past about 1400px the eye loses the line between the game and its date. */
#panel-tab > .record-card { max-width: 1400px; }

.tab-head {
  display: flex;
  align-items: baseline;
  justify-content: space-between;
  gap: 16px;
}

.dash-panel { display: flex; flex-direction: column; gap: 22px; }

/* The Statistics tab is four cards, and two of them are a pair: *Tactics and primers* and *How it
   goes* are both a short list of figures about the same set of attempts, so they read side by side
   and stack into one tall column of half-empty cards when they do not. The card the Home page also
   shows, and the activity chart, keep the full width: one is the summary the others qualify and the
   other is a chart that wants every pixel of its axis.

   Keyed on `data-dash-tab`, which `dash-tabs.js` puts on the root, so this is the one tab that gets
   it and no other tab has to opt out. */
html[data-dash-tab="stats"] #panel-tab {
  display: grid;
  grid-template-columns: repeat(2, minmax(0, 1fr));
  gap: 22px;
  align-items: stretch;
}

html[data-dash-tab="stats"] #panel-tab > [data-panel="stats"],
html[data-dash-tab="stats"] #panel-tab > [data-panel="activity"] { grid-column: 1 / -1; }

/* --- The Settings tab ------------------------------------------------------------------------- */

/* One column of four sections: Appearance, Chess accounts, Tactics, Account.

   It was three cards side by side, 577, 242 and 229px tall at 1916x990: the first was the settings
   popover dropped in whole, navigation links and all, and the other two ended less than halfway down
   it over empty page. Chosen on 2026-09-11 from two mock-ups (this, and two balanced columns of
   cards), because a settings page is read down, a row at a time, and one column cannot go ragged.

   Every row is the same shape, the setting's name with a line on what it does, then its control, so
   the controls line up down the page whatever section they are in. 820px at most: past that a label
   and its control drift apart, which is what the three cards were first brought in to fix. A size
   container, so the rows stack by the column's own width rather than the window's. */
html[data-dash-tab="settings"] #panel-tab {
  container: settings / inline-size;
  display: flex;
  flex-direction: column;
  gap: 18px;
  max-width: 820px;
}

/* Appearance: its rows, and the preview board beside them. */
.set-appearance {
  display: grid;
  grid-template-columns: minmax(0, 1fr) 150px;
  gap: 24px;
  align-items: center;
}

.set-rows { display: flex; flex-direction: column; min-width: 0; }

/* The row every section uses. `.set-row` is built by ui.js for Appearance. The Tactics section's
   `.pref-block` is built by `renderPrefs` for both dashboards and is put on the same grid only on
   this tab, so `/dashboard-classic`, which renders the same block, keeps its card as it was. */
.set-row,
html[data-dash-tab="settings"] #prefs-facts .pref-block {
  display: grid;
  grid-template-columns: 190px minmax(0, 1fr);
  column-gap: 20px;
  row-gap: 6px;
  align-items: center;
  padding: 13px 0;
  border-top: 1px solid color-mix(in srgb, var(--border) 70%, transparent);
}

.set-row:first-child,
html[data-dash-tab="settings"] #prefs-facts .pref-block:first-child { border-top: 0; padding-top: 4px; }

.set-text { display: flex; flex-direction: column; gap: 2px; min-width: 0; }
.set-name { font-size: 13.5px; font-weight: 600; color: var(--text); }
.set-help { font-size: 12px; line-height: 1.4; color: var(--text-faint); }
.set-help:empty { display: none; }
.set-control { min-width: 0; }

/* Sized to what it holds rather than to the card, which is what made the theme switch 476px wide two
   rows above a 96px one. */
.set-control .segmented { width: 170px; }
.set-control .segmented button { padding: 7px 4px; font-size: 12.5px; }

/* Board colours and piece sets. */
.choice-group { display: flex; flex-wrap: wrap; gap: 10px 12px; }

.choice {
  display: flex;
  flex-direction: column;
  align-items: center;
  gap: 5px;
  min-width: 44px;
  padding: 0;
  border: 0;
  background: none;
  color: var(--text-faint);
  font: inherit;
  font-size: 11px;
  cursor: pointer;
}

.choice-tile {
  width: 38px;
  height: 38px;
  border-radius: 9px;
  box-shadow: inset 0 0 0 1px var(--border);
  transition: box-shadow 120ms ease;
}

.choice-tile.is-piece {
  background-color: var(--surface-2);
  background-repeat: no-repeat;
  background-position: center;
  background-size: 80%;
}

/* A ring *outside* the tile rather than a thicker border, so choosing one does not resize it and
   shuffle the row, the same reason the old swatches gave. */
.choice:hover .choice-tile {
  box-shadow: inset 0 0 0 1px var(--border), 0 0 0 2px var(--surface), 0 0 0 4px var(--border);
}

.choice[aria-pressed="true"] { color: var(--text); font-weight: 700; }

.choice[aria-pressed="true"] .choice-tile {
  box-shadow: inset 0 0 0 1px var(--border), 0 0 0 2px var(--surface), 0 0 0 4px var(--gold);
}

.choice:focus-visible { outline: 2px solid var(--gold); outline-offset: 3px; border-radius: 6px; }

.set-preview {
  margin: 0;
  display: flex;
  flex-direction: column;
  align-items: center;
  gap: 7px;
}

/* Two squares by two, the knight on the light one in the top corner. 120px, so each square is 60:
   big enough to read a piece set by, small enough to sit beside the rows as a sample rather than as
   a second board on the page. */
/* A fixed height and two equal rows, not `aspect-ratio` and auto rows. With auto rows the knight's
   `height: 86%` had no definite track to be a share of, so the image took its own height and the
   top row grew to 86px against 34px for the bottom one, measured, and the squares were not square. */
.set-preview-board {
  display: grid;
  grid-template-columns: repeat(2, minmax(0, 1fr));
  grid-template-rows: repeat(2, minmax(0, 1fr));
  width: 120px;
  height: 120px;
  border-radius: 7px;
  overflow: hidden;
  box-shadow: 0 2px 6px rgb(0 0 0 / 0.3);
}

.set-preview-board span {
  display: grid;
  place-items: center;
  min-width: 0;
  min-height: 0;
  background: var(--sq-light, #f0d9b5);
}

.set-preview-board span.d { background: var(--sq-dark, #b58863); }
.set-preview-board img { width: 86%; height: 86%; object-fit: contain; }
.set-preview figcaption { font-size: 11.5px; line-height: 1.35; color: var(--text-faint); text-align: center; }

/* Tactics, on the same grid. `renderPrefs` writes a label, a help line and a value into one
   `.pref-line`; `display: contents` lets the three take their own cells, the label and its help on
   the left, and everything else, the tier's name, the slider, the note, the time controls and a Plus
   link, down the right in the order it was written. `.pref-help` is hidden everywhere else, so the
   classic dashboard's card does not grow a line. */
.pref-help { display: none; }

html[data-dash-tab="settings"] #prefs-facts { gap: 0; }
html[data-dash-tab="settings"] #prefs-facts .pref-block { align-items: start; }
html[data-dash-tab="settings"] #prefs-facts .pref-line { display: contents; }

html[data-dash-tab="settings"] #prefs-facts .pref-label {
  grid-column: 1;
  grid-row: 1;
  font-size: 13.5px;
  font-weight: 600;
  letter-spacing: 0;
  text-transform: none;
  color: var(--text);
}

html[data-dash-tab="settings"] #prefs-facts .pref-help {
  grid-column: 1;
  grid-row: 2 / span 2;
  display: block;
  font-size: 12px;
  line-height: 1.4;
  color: var(--text-faint);
}

html[data-dash-tab="settings"] #prefs-facts .pref-block > :not(.pref-line),
html[data-dash-tab="settings"] #prefs-facts .pref-value,
html[data-dash-tab="settings"] #prefs-facts .pref-line .plus-link {
  grid-column: 2;
  max-width: 440px;
}

html[data-dash-tab="settings"] #prefs-facts .pref-line .plus-link { margin-left: 0; justify-self: start; }
html[data-dash-tab="settings"] #prefs-facts > .pref-note:empty { display: none; }

/* Account: who is signed in, the plan, and the two ways out that are about the account. */
.account-card-body {
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  justify-content: space-between;
  gap: 12px 16px;
}

.account-card-body .settings-account { margin: 0; padding: 0; background: none; min-width: 0; }
.account-card-body .settings-avatar { width: 34px; height: 34px; font-size: 15px; }
.account-card-body .settings-account-name { font-size: 14px; }
.account-card-body .settings-account-sub { font-size: 12px; }

.account-card-plan {
  margin-left: 6px;
  padding: 1px 7px;
  border-radius: 999px;
  font-size: 10.5px;
  font-weight: 700;
  vertical-align: 1px;
  color: var(--gold-ink);
  background: color-mix(in srgb, var(--gold) 14%, transparent);
}

.account-card-plan.is-free { color: var(--text-dim); background: var(--surface-2); }

.account-card-actions { display: flex; flex-wrap: wrap; gap: 8px; }

.account-card-logout {
  background: transparent;
  color: var(--text-dim);
  box-shadow: inset 0 0 0 1px var(--border);
}

.account-card-logout:hover { background: var(--surface-2); color: var(--text); }

/* A phone, or any column too narrow for a label beside its control: the name and its line above the
   control, and the preview above the rows it previews rather than squeezed beside them. */
@container settings (max-width: 620px) {
  .set-appearance { grid-template-columns: minmax(0, 1fr); gap: 8px; }
  .set-preview { order: -1; }

  .set-row,
  html[data-dash-tab="settings"] #prefs-facts .pref-block {
    grid-template-columns: minmax(0, 1fr);
    row-gap: 8px;
  }

  html[data-dash-tab="settings"] #prefs-facts .pref-help { grid-row: 2; }

  html[data-dash-tab="settings"] #prefs-facts .pref-block > :not(.pref-line),
  html[data-dash-tab="settings"] #prefs-facts .pref-value,
  html[data-dash-tab="settings"] #prefs-facts .pref-line .plus-link {
    grid-column: 1;
    max-width: none;
  }

  .account-card-body { flex-direction: column; align-items: stretch; }
  .account-card-actions .btn { flex: 1 1 0; }
}

/* Fingers. The same 44px the site's buttons get on a touch screen, extended to the controls this tab
   draws itself, which are smaller than a button on a mouse and too small under a thumb. */
@media (pointer: coarse) {
  .set-control .segmented { width: 200px; }
  .set-control .segmented button { min-height: 44px; }
  .choice-tile { width: 44px; height: 44px; }
  html[data-dash-tab="settings"] .tc-grid .tc-chip { min-height: 44px; }
  /* The slider's box, not its track: the track keeps its own height from the pseudo-elements, and
     the box is what a thumb has to land in. It was 16px, measured on a 375px phone. */
  html[data-dash-tab="settings"] #prefs-facts .pref-slider { height: 36px; }
}

@media (prefers-reduced-motion: reduce) {
  .choice-tile { transition: none; }
}

@media (max-width: 900px) {
  html[data-dash-tab="stats"] #panel-tab { grid-template-columns: minmax(0, 1fr); }
}

/* The Plus panel and the invite strip beneath it. A plain block until now, so the two sections sat
   flush against one another while everything else on the page was 22px apart, which read as one
   tall panel with a seam rather than as two things. */
#train-extras { display: flex; flex-direction: column; gap: 22px; }
#train-extras:empty { display: none; }
/* An id beats a class outright, so `.hidden` could never turn this off however the files are
   ordered, and the Plus panel sat under every tab rather than only under Home. */
#train-extras.hidden { display: none; }

/* `style.css` is linked before this file, so its `.hidden { display: none }` and a `display` set on
   a class here are the same specificity and the later rule wins. Every class in this block that
   sets a display has to stand its own `.hidden` down, or hiding it does nothing at all: the first
   symptom was the whole Train tab, hero included, still painted above whichever tab you opened. */
.dash-panel.hidden,
.tab-head.hidden,
.dash-head.hidden,
.dash-sheet.hidden,
.rail-pip.hidden,
.rail-badge.hidden,
.hero-allow.hidden { display: none; }

/* Home: the hero and two cards in one column, the motif of the day in a narrower one beside it.

   `stretch`, not `start`. The two columns are one band and they should end on the same line: the
   motif card came out 582px against the hero and its two cards at 621, so the band finished 39px
   ragged with a strip of page showing under one half of it. Stretching makes whichever column is
   taller set the height, so this holds whichever way round the two end up. */
.train-grid {
  display: grid;
  /* 380 rather than 316. The motif of the day is a board you play a move on now, not a picture, and
     at 316 it came out 274px square, which is small for something you are meant to aim a piece at.
     The 64px comes off the pair beside it, where it costs a card that is mostly a heading and two
     figures far less than it costs a board. */
  grid-template-columns: minmax(0, 1fr) 380px;
  gap: 24px;
  align-items: stretch;
}

.train-col { min-width: 0; display: flex; flex-direction: column; gap: 20px; }

/* The row of two under the hero takes whatever height the stretched column has left, so its cards
   finish on the same line as the motif card beside them. Without it the column stretched and the
   cards inside did not, which put the motif card's bottom edge below theirs by however much taller
   the row was, and that gap grew the moment the Plus panel appeared underneath and made the whole
   band taller. */
.train-col > .train-under { flex: 1 1 auto; min-height: 0; }
.train-side { min-width: 0; display: flex; flex-direction: column; gap: 16px; }

/* The card fills the column it was given, and the slack goes to the *body* rather than to the foot
   of the card: `auto 1fr` puts it under the board, where `.daily-side` already pins its button to
   the bottom of its own column, so a taller card reads as the button sitting at the card's foot
   rather than as a gap someone forgot to close. */
.train-side > .dash-card { height: 100%; }
.train-side .daily-body { flex: 1 1 auto; min-height: 0; grid-template-rows: auto 1fr; }

/* The two cards under the hero. One card stretched across this column was a large box with a
   sentence in it; two of them answer different questions and fill the width they are given. */
.train-under {
  display: grid;
  grid-template-columns: repeat(2, minmax(0, 1fr));
  gap: 20px;
  align-items: stretch;
}

.train-under > .dash-card { height: 100%; }

/* One under the other once two will not fit, which is most of the way down to the phone layout and
   was not handled at all: `.train-grid` collapses at 900px and this collapsed with it, so between
   901 and 1360 the pair went on splitting a column that is `100vw - 737px` wide. Measured on the
   home tab, the statistics card came out 134px at 1024 and 212px at 1180, and inside it the counts
   column was 54px holding 118px of content: `1/14` was painted into a 4px box. Every iPad in
   landscape is in that band, and so is a 1280 laptop, which is why it survived, the desk it was
   built on is wider than the bug.

   1360 rather than 1325, which is where today's figures stop clipping. The threshold is not a
   property of the layout, it is a property of the *numbers*: measured, the card needs 284px with
   `1/14` in it and 396px with `487/1203`, so fitting the breakpoint to the data on this account
   would put it back the first time somebody solved a few hundred tactics. The container query
   below is what actually holds the card together; this only stops asking it to do the impossible. */
@media (max-width: 1360px) {
  .train-under { grid-template-columns: minmax(0, 1fr); }
}

/* And the band itself stops one step before that. The motif card is a fixed 380px, so the column
   beside it is `100vw - 737px` and goes on shrinking after the pair inside it has already stacked:
   at 901px, the last width before the phone layout, it is 164px, which is narrower than any card
   on this page can be drawn. 1000 is where it passes 260, which is the width the statistics card
   needs for its own narrowest step, so this is the same measurement the container queries are made
   of rather than a second opinion about devices.

   Deliberately not 1024: an iPad in landscape keeps the two columns, where the left one is 287px
   and every card in it is legible. Losing the motif of the day below the fold to buy width nobody
   needed would be the wrong trade on the one screen this was reported from. */
@media (max-width: 1000px) {
  .train-grid { grid-template-columns: minmax(0, 1fr); }
}

/* The footer at the foot of the window, rather than 22px under the last card.

   On a desktop the Home band ends about two thirds of the way down, so a footer that followed the
   cards sat mid-screen with a third of the window blank beneath it. The column is now at least one
   window tall and the footer takes whatever is left above it. `margin-top: auto`, not `position:
   fixed`: on a page longer than the window it is simply last, and it never covers a card. Every tab
   shares this column, so every tab gets it.

   Above 900px only. Below that the rail is a bottom bar that `.dash-main`'s own padding clears, and
   a phone page is longer than its window anyway. The bottom padding is new: the column had none,
   which did not show while the footer had empty page under it and does once it sits on the edge. */
@media (min-width: 901px) {
  .dash-main { min-height: 100vh; padding-bottom: 20px; }
  .dash-main > .dash-foot { margin-top: auto; }
}

/* --- Your latest tactics: the full-width row under the band ----------------------------------- */

/* The slot holds two sections on Home, your latest tactics and then the invite strip, so it is a
   column with the page's own 22px between them rather than a block that stacks them flush. */
.home-wide { display: flex; flex-direction: column; gap: 22px; }

/* Out of the column's gap until one of its cards is showing. Both are `hidden` until they have
   something to say (`renderRecent` a tactic, `renderInviteStrip` a solved one), and an empty slot is
   still a flex item: 22px of gap with nothing in it. */
.home-wide:not(:has(> :not([hidden]))) { display: none; }

/* The same fix for the wrapper round the Plus panel. It is a flex item whatever it holds, so with the
   panel hidden, which is every Plus account, it cost a 22px gap above it and another below: 44px
   under the band where every other gap on the page is 22. Measured before this rule, 975px to 1019px,
   when the wrapper also held the invite strip. */
#train-extras:not(:has(> :not([hidden]))) { display: none; }

.recent-card { container: recent / inline-size; }

/* A title and no lede, so the title is centred on the icon rather than hanging from its top edge. */
.recent-card .card-head { align-items: center; }
.recent-card .dash-card-title { margin: 0; }

.recent-all {
  flex: none;
  margin-left: auto;
  align-self: center;
  font-size: 12.5px;
  color: var(--text-dim);
  white-space: nowrap;
}

/* As many tiles as fit on one line, and never a second line. A tile is 236px at least, so `auto-fill`
   and the container queries under it agree on the count: n tiles and their 14px gaps need `250n - 14`
   pixels, which is where each step is. Tiles past the line are `display: none` rather than clipped by
   a zero-height row, because a clipped tile is still a link the keyboard stops on.

   Below the first step the row is one column and shows three, one under another: a phone page scrolls
   anyway, and one position is too few to be a row of anything. */
.recent-list {
  display: grid;
  grid-template-columns: repeat(auto-fill, minmax(236px, 1fr));
  gap: 14px;
}

.recent-tac:nth-child(n + 4) { display: none; }
@container recent (min-width: 486px) { .recent-tac:nth-child(n + 3) { display: none; } }
@container recent (min-width: 736px) { .recent-tac:nth-child(3) { display: grid; } }
@container recent (min-width: 986px) { .recent-tac:nth-child(4) { display: grid; } }
@container recent (min-width: 1236px) { .recent-tac:nth-child(5) { display: grid; } }
@container recent (min-width: 1486px) { .recent-tac:nth-child(6) { display: grid; } }
@container recent (min-width: 1736px) { .recent-tac:nth-child(7) { display: grid; } }
@container recent (min-width: 1986px) { .recent-tac:nth-child(8) { display: grid; } }

/* The board and a column beside it, the same shape as the motif of the day card, so a position reads
   the same way wherever the dashboard puts one. A tint rather than a border: these are items inside
   a card, not cards, and the border arrives on hover to say the whole tile is the link. */
.recent-tac {
  display: grid;
  grid-template-columns: 116px minmax(0, 1fr);
  gap: 11px;
  padding: 8px;
  border: 1px solid transparent;
  border-radius: 10px;
  background: color-mix(in srgb, var(--surface-2) 55%, var(--surface));
  color: inherit;
  text-decoration: none;
  transition: border-color 120ms ease, background-color 120ms ease;
}

.recent-tac:hover {
  border-color: color-mix(in srgb, var(--gold) 55%, transparent);
  background: var(--surface-2);
}

.recent-tac:focus-visible { outline: 2px solid var(--gold); outline-offset: 2px; }

/* The exception `.btn` and `.rail-item` carry: a tap leaves `:hover` behind on a touch screen. */
@media (hover: none) {
  .recent-tac:hover {
    border-color: transparent;
    background: color-mix(in srgb, var(--surface-2) 55%, var(--surface));
  }
}

@media (prefers-reduced-motion: reduce) {
  .recent-tac { transition: none; }
}

/* `.mini-board` sizes itself off the daily card's width. Here the tile's first column already has. */
.recent-tac .mini-board { width: 100%; max-width: none; border-radius: 5px; }

.recent-side { display: flex; flex-direction: column; gap: 3px; min-width: 0; }

/* Green and red for the reason the openings pips give: found and missed are semantic state, and that
   is what `style.css` keeps them for. "Shown" is neither, so it is neutral. */
.recent-verdict {
  align-self: flex-start;
  padding: 2px 6px;
  border-radius: 4px;
  font-size: 9.5px;
  font-weight: 800;
  letter-spacing: 0.07em;
  text-transform: uppercase;
}

.recent-verdict.is-found { color: var(--good); background: color-mix(in srgb, var(--good) 17%, transparent); }
.recent-verdict.is-missed { color: var(--bad); background: color-mix(in srgb, var(--bad) 17%, transparent); }
.recent-verdict.is-shown { color: var(--text-dim); background: var(--raised); }

/* Two lines rather than one with an ellipsis. The text column beside a 116px board is about 100px at
   a 1916px window, so "Discovered attack" came out as "Discovered at…", and the column had room under
   it: the board, not the text, sets the tile's height. */
.recent-motif {
  display: -webkit-box;
  -webkit-box-orient: vertical;
  -webkit-line-clamp: 2;
  overflow: hidden;
  font-size: 13.5px;
  font-weight: 700;
  line-height: 1.25;
}

/* Two lines, then an ellipsis: a full opening name such as "Sicilian Defense: Najdorf Variation" is
   what an older puzzle carries when its family was never stored, and it would push the foot out of
   the tile. */
.recent-where {
  display: -webkit-box;
  -webkit-box-orient: vertical;
  -webkit-line-clamp: 2;
  overflow: hidden;
  font-size: 11.5px;
  line-height: 1.35;
  color: var(--text-dim);
}

.recent-foot {
  margin-top: auto;
  display: flex;
  flex-wrap: wrap;
  justify-content: space-between;
  gap: 2px 8px;
  font-size: 11px;
  color: var(--text-faint);
}

.recent-swing { font-weight: 600; color: var(--text-dim); font-variant-numeric: tabular-nums; }

/* The statistics card used to be told what to do from here, and from a `max-width: 780px` media
   query, both of which describe the *window* when the only thing that decides this card's layout is
   the width of the slot it is in. The note left here said as much and settled for a descendant rule
   "because it is the one slot where the answer is known in advance". It stopped being the one slot,
   and the two rules then disagreed: a container query added later lost a specificity fight with
   this one and silently did nothing. Both are gone, and the card answers for itself in
   `@container` beside `.stats-body`. */

/* --- What a free day has left ---------------------------------------------------------------- */

.hero-allow {
  display: flex;
  flex-direction: column;
  gap: 6px;
  /* Not the hero's full width: `.allow-line` is space-between, and across 785px that is a number at
     one edge of the panel and a link at the other with nothing between them. */
  width: min(100%, 380px);
}

.allow-bar {
  height: 6px;
  border-radius: 999px;
  background: var(--track);
  overflow: hidden;
}

.allow-fill {
  height: 100%;
  border-radius: 999px;
  background: var(--gold);
  transition: width 320ms ease;
}

.hero-allow.spent .allow-fill { background: var(--bad); }

.allow-line {
  display: flex;
  align-items: baseline;
  justify-content: space-between;
  gap: 10px;
  font-size: 11.5px;
  color: var(--text-faint);
}

.allow-line a { color: var(--gold-ink); text-decoration: none; }
.allow-line a:hover { text-decoration: underline; }

@media (prefers-reduced-motion: reduce) { .allow-fill { transition: none; } }

/* --- Narrow ------------------------------------------------------------------------------------

   One breakpoint, not three. At 900px the rail becomes a bar across the top and the two columns
   become one; there is deliberately no in-between state where the rail collapses to icons, because
   a 316px side column in a 700px window is neither a rail nor a row and two breakpoints is a lot of
   layout to keep honest for the window sizes between them. */
/* The bottom bar and the sheet behind More. Both are the narrow layout's navigation and neither is
   drawn on a wide screen, where the rail carries all eleven. */
.dash-bar, .dash-sheet { display: none; }

/* --- Narrow ------------------------------------------------------------------------------------

   One breakpoint, not three. At 900px the rail is replaced by a bar of four plus More, and the two
   columns become one. There is deliberately no in-between state where the rail collapses to icons:
   a 316px side column in a 700px window is neither a rail nor a row, and two breakpoints is a lot of
   layout to keep honest for the window sizes between them.

   The rail is *replaced* rather than turned on its side. A row of eleven items in 390px is a strip
   that scrolls sideways, which hides more than half of itself behind a gesture nobody is told
   about. Four that fit, and a sheet for the rest, is the same navigation without the guessing. */
@media (max-width: 900px) {
  body.with-rail { grid-template-columns: minmax(0, 1fr); }

  /* And nothing is pinned to a column that no longer exists. `grid-column: 2` above has no media
     query of its own, so below 900 every page went on asking for the *second* column of a
     one-column grid, which creates an implicit second column and leaves the first one to take up
     whatever slack is going. Measured on the trainer at 820px, an iPad held upright: the body
     resolved to `110px 710px`, the page sat in the 710 and the 110 was empty, so the whole layout
     was 110px right of where it should be with nothing on the other side of it.

     Invisible on the dashboard, which is why it lasted: `.dash-main` fills its column either way,
     so the phantom column collapsed to nothing there and only the content-width layouts, the
     trainer, the analysis board and rush, were pushed across. */
  body.with-rail > :not(.dash-rail) { grid-column: 1; }

  /* Content-width layouts centre in the column rather than sitting against its left edge, which is
     the same thing `justify-self: center` does for them above 900. It was inside the `min-width:
     901px` block because above 900 it was the only place the page could be off-centre; below it,
     the page was off-centre for the other reason and this half was missing too. */
  body.with-rail > .page { justify-self: center; }

  .dash-rail { display: none; }

  /* Every railed page has to clear the bar, not only the dashboard. */
  body.with-rail { padding-bottom: calc(74px + env(safe-area-inset-bottom, 0px)); }

  /* 24px at the top, which is air rather than clearance: it was 74, to clear a fixed brand mark that
     was removed on 2026-09-11. The bottom is the bar, plus the home indicator on a phone that has one. */
  .dash-main { padding: 24px 16px calc(74px + env(safe-area-inset-bottom, 0px)); }

  .train-grid { grid-template-columns: minmax(0, 1fr); }
  .train-under { grid-template-columns: minmax(0, 1fr); }

  .dash-bar {
    position: fixed;
    left: 0;
    right: 0;
    bottom: 0;
    z-index: 120;
    display: grid;
    grid-template-columns: repeat(5, minmax(0, 1fr));
    padding-bottom: env(safe-area-inset-bottom, 0px);
    background: var(--sunken);
    border-top: 1px solid var(--border);
  }

  /* The rail's row shape does not survive a 78px column, so the bar restates it rather than
     inheriting it: icon over label, centred, and 62px of height, which clears the 44px hit target
     with the label inside it rather than beside it. */
  .dash-bar .rail-item {
    height: 62px;
    flex-direction: column;
    justify-content: center;
    gap: 4px;
    padding: 0 4px;
    border-radius: 0;
    font-size: 10.5px;
    background: none;
    border: none;
    color: var(--text-faint);
    font-family: inherit;
    cursor: pointer;
  }

  .dash-bar .rail-item svg { width: 22px; height: 22px; }
  .dash-bar .rail-item.on { background: none; color: var(--gold-ink); }
  .dash-bar .rail-item:hover { background: none; }

  /* The label is centred under the icon, so a mark cannot push off it the way it does in the rail.
     It goes on the icon's corner instead. */
  .dash-bar .rail-pip {
    position: absolute;
    margin: 0;
    transform: translate(11px, -12px);
  }

  .dash-sheet {
    position: fixed;
    inset: 0;
    z-index: 130;
    display: flex;
    align-items: flex-end;
    background: rgba(4, 3, 16, 0.62);
  }

  .sheet-panel {
    width: 100%;
    max-height: 76vh;
    overflow-y: auto;
    display: flex;
    flex-direction: column;
    gap: 3px;
    padding: 12px 12px calc(74px + env(safe-area-inset-bottom, 0px));
    background: var(--surface);
    border-top: 1px solid var(--border);
    border-radius: 16px 16px 0 0;
  }

  .sheet-panel .rail-item { height: 48px; }
}

.dash-card {
  position: relative;
  display: flex;
  flex-direction: column;
  gap: 9px;
  padding: 18px 20px 20px;
  border: 1px solid var(--border);
  border-radius: 16px;
  background: var(--surface);
  overflow: hidden;
}

/* A hairline of gold along the top edge, brightest at the left and fading out. Enough to give the
   card a top and a bottom, a rectangle of uniform grey has neither, without spending the accent
   colour, which belongs to the two buttons that actually start something. */
.dash-card::before {
  content: '';
  position: absolute;
  inset: 0 0 auto;
  height: 1px;
  background: linear-gradient(90deg,
    color-mix(in srgb, var(--gold) 55%, transparent) 0%,
    color-mix(in srgb, var(--gold) 12%, transparent) 45%,
    transparent 80%);
}

/* The card spanning the full grid, for the one whose content is a row of figures rather than a
   paragraph. */
.dash-card-wide { grid-column: 1 / -1; }

/* Rush and openings are cells in the grid, not strips under it. They were strips, laid out as a
   row of copy and one button, because the six cards then in the grid filled two rows exactly and a
   seventh would have sat alone in a third. They are two of the six now, the leaderboard and the
   accounts card having moved to the foot, so they stack like every other card and want nothing
   from this file that the other four do not.

   `flex: 1 1 auto` on `.card-head` was the last thing left over from the strip, and it was not
   harmless: `.dash-card` is a flex *column*, so a head told to grow absorbs the card's spare
   height. It measured 48px against the motif card's 39 with identical content, which put two
   boards side by side nine pixels out of line for no reason visible in the markup. */

/* --- What fills the two cards you go and do something in ------------------------------------- */

/* Both cards are built the same way: a big gold figure with a caption beside it, a small chart
   under it, and a note. Two cards in one row built alike read as a row; built differently they read
   as one of them being wrong. */
.card-figure {
  display: flex;
  flex-direction: column;
  gap: 11px;
  margin-top: 6px;
}

.figure-hero { display: flex; align-items: center; gap: 11px; }

/* The locked card puts "3·5·10" where a number goes, which is three numbers and two dots. At 38px
   that is a headline; at this size it is a list. */
.figure-hero-value.is-words { font-size: 22px; letter-spacing: 0.02em; }

.figure-hero-value {
  font-size: 38px;
  font-weight: 700;
  line-height: 1;
  font-variant-numeric: tabular-nums;
  letter-spacing: -0.015em;
  color: var(--gold-ink);
}

/* The caption is two lines and they carry different weights on purpose: what the number *is*, then
   what it is *of*. Run together on one line it read as a sentence somebody had to parse. */
.figure-hero-meta { display: flex; flex-direction: column; gap: 1px; min-width: 0; }

.figure-hero-label {
  font-size: 12.5px;
  font-weight: 600;
  line-height: 1.25;
  color: var(--text);
}

.figure-hero-detail {
  font-size: 11.5px;
  line-height: 1.3;
  color: var(--text-dim);
}

.figure-note { margin: 0; font-size: 11.5px; line-height: 1.4; color: var(--text-dim); }

/* --- The form guide -------------------------------------------------------------------------- */

.form-guide { display: flex; flex-direction: column; }

.form-row { display: flex; gap: 3px; }

/* Green and red, which the palette otherwise reserves for semantic state. A game won or lost is
   semantic state, which is the whole of the exception: these are not brand colours being borrowed,
   they are the same right/wrong the board uses. The four `--pip-*` tokens exist because the letter
   has to clear AA against the fill and the two themes need that the opposite way round; see the
   note in style.css. */
.form-pip {
  flex: 1;
  height: 24px;
  display: grid;
  place-items: center;
  border-radius: 3px;
  font-size: 9.5px;
  font-weight: 700;
  color: var(--pip-ink);
}

.form-pip.is-w { background: var(--pip-win); }
.form-pip.is-d { background: var(--pip-draw); }
.form-pip.is-l { background: var(--pip-loss); }

/* The run you are on, ruled under the newest end so it does not have to be counted. Gold by
   default, because a run is a run whichever way it is going and gold is the app's "look here";
   green when it is a winning one, since that is the one case where the colour is information the
   pips have already established. */
.form-streak {
  height: 2px;
  margin: 4px 0 0 auto;
  border-radius: 2px;
  background: var(--gold);
}

.form-streak.is-w { background: var(--pip-win); }

.form-ends {
  display: flex;
  justify-content: space-between;
  gap: 8px;
  margin-top: 5px;
  font-size: 9.5px;
  font-weight: 700;
  letter-spacing: 0.1em;
  text-transform: uppercase;
  color: var(--text-faint);
}

/* --- Where it goes wrong: a move-number histogram --------------------------------------------
   `loadOpeningQueueSummary`'s own section, under the card's usual figure. Five bars over the
   opening's whole move range rather than one per move number, which on the small samples one
   opening actually has would read as noise rather than a shape. */
.move-hist {
  display: flex;
  align-items: flex-end;
  gap: 4px;
  height: 48px;
  margin: 6px 0 6px;
}

.move-hist-bar {
  flex: 1 1 0;
  min-width: 0;
  background: linear-gradient(180deg, var(--gold-bright), var(--gold));
  border-radius: 3px 3px 1px 1px;
  transition: height 600ms cubic-bezier(0.16, 0.9, 0.26, 1.02);
}

.move-hist-labels { display: flex; gap: 4px; margin-bottom: 8px; }

.move-hist-labels span {
  flex: 1 1 0;
  text-align: center;
  font-size: 9px;
  color: var(--text-faint);
  font-variant-numeric: tabular-nums;
}

.move-hist-summary { margin: 0; }

/* --- The run trend ---------------------------------------------------------------------------- */

.rush-trend { display: flex; flex-direction: column; }
.trend-svg { display: block; width: 100%; height: 44px; }
.trend-line { fill: none; stroke: var(--gold); stroke-width: 2; stroke-linejoin: round;
              stroke-linecap: round; }
/* The latest run, which is the point the reader is looking for. Ringed in the card's own surface so
   it reads as sitting on the line rather than as a hole in it. */
.trend-dot { fill: var(--gold-bright); stroke: var(--surface); stroke-width: 2; }

/* The floor the chart sits on, drawn whether or not there is anything above it. It is what lets an
   account with no runs get an empty chart rather than a gap: the card holds one shape from the
   first run to the fiftieth, and the rule says where the line will be. Dashed, because a solid one
   would read as a value of zero rather than as an axis. */
.trend-guide {
  stroke: var(--border);
  stroke-width: 1;
  stroke-dasharray: 3 4;
  /* The SVG is stretched by `preserveAspectRatio: none`, which would stretch the dashes with it.
     Taking the stroke out of that scaling is what keeps them square at any card width. */
  vector-effect: non-scaling-stroke;
}

/* --- A trophy medal --------------------------------------------------------------------------- */

/* Here rather than in `trophies.css` because two pages draw one: the case itself, and the dashboard
   card that is a window into it. The state classes are on the badge rather than on whatever contains
   it, so a medal is a complete object anywhere it is put down; the trophies page also puts them on
   its row, for the name and the date, which is a different job. `ui.js` builds it. */
.trophy-badge {
  flex: none;
  display: flex;
  align-items: center;
  justify-content: center;
  width: 30px;
  height: 30px;
  border-radius: 50%;
  border: 1px solid var(--border);
  color: var(--text-faint);
  background: var(--surface);
}

.trophy-badge svg { width: 17px; height: 17px; }

/* The dashboard's strip, where six of them share a card with a number and a bar. */
.trophy-badge.is-small { width: 25px; height: 25px; }
.trophy-badge.is-small svg { width: 14px; height: 14px; }

.trophy-badge.is-earned {
  border-color: color-mix(in srgb, var(--gold) 45%, var(--border));
  background: color-mix(in srgb, var(--gold) 12%, var(--surface));
  color: var(--gold-ink);
}

/* Three tiers, three weights of the same accent rather than three different hues. Bronze, silver
   and gold as literal metal colours would put two colours on screen that appear nowhere else in the
   product, and the palette has one accent on purpose. What the tier changes is how *bright* the
   medal is, which is the part anybody actually reads off a case at a glance. */
.trophy-badge.is-earned.is-silver {
  border-color: color-mix(in srgb, var(--gold) 62%, var(--border));
  background: color-mix(in srgb, var(--gold) 20%, var(--surface));
}

.trophy-badge.is-earned.is-gold {
  border-color: var(--gold);
  background: color-mix(in srgb, var(--gold) 30%, var(--surface));
  color: var(--gold-bright);
  /* The one glow, and it is spent here: gold is the end of a ladder, one per group, and a case
     where every medal glowed would be a case where none of them did. */
  box-shadow: 0 0 0 3px color-mix(in srgb, var(--gold) 14%, transparent);
}

/* Locked. Drained rather than hidden: a case showing only what somebody has is a receipt, and the
   whole argument for drawing the locked ones is that the next rung is the interesting part. */
.trophy-badge.is-locked { opacity: 0.5; }

/* --- The invite link -------------------------------------------------------------------------- */

/* Monospace, because this is a string somebody reads character by character to check they have
   copied the right one, and a proportional font makes that harder for no gain.
   Here rather than in `trophies.css` because the dashboard's invite strip shows the same link in
   the same box and does not load that file. */
.invite-url {
  flex: 1 1 auto;
  min-width: 0;
  padding: 8px 11px;
  border: 1px solid var(--border);
  border-radius: 8px;
  background: var(--sunken);
  color: var(--text);
  font-family: ui-monospace, SFMono-Regular, Menlo, Consolas, monospace;
  font-size: 12.5px;
}

.invite-url:focus {
  outline: 2px solid var(--gold);
  outline-offset: 1px;
}

/* --- The dashboard's invite strip --------------------------------------------------------------- */

/* A strip, not a card in the grid. The grid is six cards in two rows of three and a seventh would
   sit alone in a third; more to the point this is not a thing to *look at* on the way past, it is
   one action with one control, and the control is a link that wants a full-width box to be readable
   in. On Home it is the last card, after your latest tactics and below the Plus panel; on
   `/dashboard-classic` it still sits above the upsell.

   `.dash-card` gives it the surface, the border and the gold hairline; this only lays it out. */
.invite-strip {
  flex-direction: row;
  align-items: center;
  gap: 12px;
  padding: 11px 13px;
}

.invite-strip[hidden] { display: none; }

.invite-strip-copy { flex: 1 1 auto; min-width: 0; }

.invite-strip-title {
  margin: 0;
  font-size: 13.5px;
  font-weight: 700;
  line-height: 1.25;
  color: var(--text);
}

.invite-strip-lede {
  margin: 1px 0 0;
  font-size: 11.5px;
  line-height: 1.35;
  color: var(--text-dim);
}

/* The link and its button, sized so the URL is readable without taking the whole strip. It is the
   half of this that does something, so it gets a fixed share rather than whatever the copy leaves. */
.invite-strip-action {
  flex: 0 1 380px;
  display: flex;
  gap: 8px;
  min-width: 0;
}

/* There was a dismiss button here, `.invite-strip-hide`. It and its `inviteDismissed` pref were
   removed on 2026-09-11 at the owner's call, so the strip has no way to be hidden once it appears. */

/* Under about 720px the link box and the copy stop fitting on one line long before the page's own
   breakpoint, so the strip becomes what it is on a phone: a heading, a line, and the control. */
@media (max-width: 760px) {
  .invite-strip {
    flex-direction: column;
    align-items: stretch;
    gap: 9px;
  }
  .invite-strip-action { flex: 1 1 auto; }
}

/* --- The trophy card's medals and its "next one" ----------------------------------------------- */

/* The strip on the dashboard card: what is already in the case, then the next one to come, dim and
   set apart. It is the only picture on a card that is otherwise a number and a bar, and it is what
   makes the card legible as *trophies* from across the page rather than as another statistic.

   `flex-wrap: nowrap` with `overflow: hidden` rather than wrapping: the row is capped in script, and
   a second line of medals appearing at some card widths and not others would change the height of
   the whole foot band. */
.trophy-medals {
  display: flex;
  align-items: center;
  gap: 5px;
  flex-wrap: nowrap;
  overflow: hidden;
}

/* The gap that says the rest of the row is not the same thing as the start of it: everything to the
   left is earned, and what follows is what is next. A rule rather than a wider gap, because a gap
   alone reads as a rendering accident at this size. */
.trophy-medals .is-next {
  margin-left: 3px;
  padding-left: 8px;
  border-left: 1px dashed var(--border);
}

/* How many more there are than fit. Not a medal, so it is not drawn as one: a circle with a number
   in it beside five circles with pictures in them reads as a sixth trophy called "+4". */
.trophy-more {
  flex: none;
  font-size: 11px;
  font-weight: 700;
  font-variant-numeric: tabular-nums;
  color: var(--text-faint);
}


/* Label, figure, bar. A three-cell grid with the bar spanning both columns underneath, which is the
   same shape `.trophy` on the trophies page uses, so the two agree about what a locked trophy looks
   like without sharing a stylesheet: this file is loaded by every page and `trophies.css` by one.

   It sits where the rush card's trend chart sits and is the same height, so the three cards in the
   foot band do not stretch each other. */
.trophy-next {
  display: grid;
  grid-template-columns: minmax(0, 1fr) auto;
  align-items: baseline;
  gap: 3px 8px;
  padding: 8px 10px;
  border-radius: 9px;
  background: var(--sunken);
}

.trophy-next-label {
  font-size: 11.5px;
  font-weight: 600;
  color: var(--text-dim);
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
}

.trophy-next-figure {
  font-size: 11.5px;
  font-weight: 700;
  font-variant-numeric: tabular-nums;
  color: var(--gold-ink);
}

.trophy-next-bar {
  grid-column: 1 / -1;
  height: 4px;
  margin-top: 3px;
  border-radius: 2px;
  background: var(--track);
  overflow: hidden;
}

/* Filled on the frame after paint by `animateIn`, the same way the motif bars in the statistics
   panel are. */
.trophy-next-fill {
  height: 100%;
  border-radius: inherit;
  background: linear-gradient(90deg, var(--gold-dim), var(--gold));
  transition: width 620ms cubic-bezier(0.22, 1, 0.36, 1);
}

@media (prefers-reduced-motion: reduce) {
  .trophy-next-fill { transition: none; }
}

/* --- Starting a run --------------------------------------------------------------------------- */

/* Four buttons on one row: the primary, then the three clocks. Each clock starts a run at that
   length, so picking one is a single press instead of a page and a choice. "Start a run" stays and
   leads, because it uses whichever length you last played, which is the right answer for anybody
   who does not want to think about it. */
.rush-starts { display: flex; flex-wrap: wrap; align-items: center; gap: 6px; }

.btn-rush-go {
  border-color: color-mix(in srgb, var(--gold) 55%, var(--border));
  background: color-mix(in srgb, var(--gold) 18%, var(--surface));
  font-weight: 600;
}

.btn-rush-go:hover { background: color-mix(in srgb, var(--gold) 30%, var(--surface)); }

/* Quieter than the primary and narrower than a word: three of them are a set of sizes, not three
   separate invitations. */
.btn-rush-len {
  padding: 6px 10px;
  font-size: 11.5px;
  font-variant-numeric: tabular-nums;
  color: var(--text-dim);
}

.btn-rush-len:hover { background: var(--raised-hover); color: var(--text); }

/* The foot: the leaderboard, the trophy case and the accounts card, under the six.
   Three columns, matching `.dash-grid` above rather than differing from it. It was two, in a band
   of halves, and the note here said three would leave a row two-thirds full, which was true of the
   two cards that were then in it. There are three now, so the row is full and the band lines up
   with the grid above it instead of straddling its columns. It inherits the gap and the card sizing
   from `.dash-grid`, and now the space above it too.

   It used to add `margin-top: 8px` on top of the page's own 16px, and that was the only seam on
   the dashboard that was not 16: every other panel boundary, and every row inside both grids,
   came from `--dash-gap`. The 8 was there to mark the band as a band. It is not needed for that,
   because three cards under six already read as a separate group, and it cost the page the one
   thing an even rhythm is for, which is that nothing looks like a mistake.

   The class stays on the div with no declaration of its own. Grep will land here, and this is
   where the reasoning for the band lives; the alternative is a bare `.dash-grid` in the markup
   and nowhere to write any of it down.

   The order is deliberate: two scoreboards and then the settings. The leaderboard is about everyone
   else, the trophy case is about you alone, and putting them side by side is what makes the second
   one worth having for the many people who are nowhere near the top of the first. */

/* No breakpoint of its own any more. The band carries `.dash-grid` as well, and now that it wants
   that grid's three columns rather than two of its own, it wants that grid's collapse too: a
   duplicate rule at the same measure and the same value is a rule that can only ever drift. */

/* Icon, title and lede as one block, so the text starts at a consistent place whether the icon is
   one line tall or three. */
.card-head {
  display: flex;
  align-items: flex-start;
  gap: 9px;
  margin-bottom: 0;
}

.card-head > div { min-width: 0; }

/* The icon is what makes five cards findable without reading five headings. Tinted rather than
   gold: at full strength six of them would compete with the buttons, and the job here is
   recognition, not attention. */
.card-icon {
  flex: none;
  display: grid;
  place-items: center;
  width: 33px;
  height: 33px;
  border-radius: 11px;
  background: color-mix(in srgb, var(--gold) 13%, var(--surface-2));
  color: var(--gold-ink);
  box-shadow: inset 0 0 0 1px color-mix(in srgb, var(--gold) 22%, transparent);
}

.card-icon svg { width: 16px; height: 16px; }

.dash-card-title {
  margin: 0 0 4px;
  font-size: 16.5px;
  font-weight: 700;
  letter-spacing: 0.02em;
}

.dash-card-lede {
  margin: 0;
  font-size: 13.5px;
  line-height: 1.5;
  color: var(--text-dim);
}

.dash-card .username-input { margin: 0; }
.dash-card .threshold-label { margin: 2px 0 0; }

/* `.scan-alt` used to live here: a number box and a "Rescan the last N games" button. It is gone.
   Its only real use was after loosening the difficulty, when games already analysed could yield
   tactics they had not before, and that is now handled where it belongs, by `/api/preferences`
   forgetting those games so the next ordinary scan re-reads them. A control that asks somebody to
   notice that connection and then pick a number is a control that should not exist. */

.dash-note {
  margin: 0;
  font-size: 12.5px;
  line-height: 1.45;
  color: var(--text-dim);
  min-height: 1em;
}

.dash-link {
  margin-top: auto;
  padding-top: 4px;
  font-size: 12.5px;
  color: var(--gold-ink);
}

.dash-actions { margin-top: auto; padding-top: 4px; }

/* Four ways onto the analysis board, on a 2x2 grid so they are one control rather than four loose
   ones. Buttons rather than an inline field: a FEN box only served one of the four, and it asked
   for a paste before saying what would happen to it.

   Equal cells, and every label centred, a wrapped row sized each button to its own text, so
   "Insert FEN" and "Set up board" sat at different widths and the group looked scattered. */
.pos-buttons {
  display: grid;
  grid-template-columns: 1fr 1fr;
  /* Equal rows as well as equal columns. One of the four is an `<a>` and three are `<button>`s, and
     the two size differently by a couple of pixels, enough that the top row sat proud of the
     bottom one, which is the same "arranged by accident" look the row of mismatched widths had. */
  grid-auto-rows: 1fr;
  gap: 6px;
  margin-top: auto;
  padding-top: 4px;
}

.pos-buttons .btn { justify-content: center; }

/* One accent between the four, on the one that needs nothing from you. Tinted rather than the solid
   gold of a hero button: this is the default of a set, not the point of the page. */
.pos-btn-lead {
  border-color: color-mix(in srgb, var(--gold) 45%, var(--border));
  background: color-mix(in srgb, var(--gold) 14%, transparent);
  color: var(--gold-ink);
}

.pos-btn-lead:hover {
  border-color: var(--gold);
  background: color-mix(in srgb, var(--gold) 22%, transparent);
}

/*
 * Light mode needs the opposite treatment, and the first attempt at it made the button look broken.
 *
 * Measured on the white card: the tint came out at 1.18 contrast against the card behind it, where
 * the three plain buttons beside it sit at 1.32, so the *lead* control was the faintest of the
 * four. Its label was a muddy brown (`gold` pulled 38% toward the ink) while every other button on
 * the page uses near-black. Pale plus brown is precisely how a disabled control is drawn, which is
 * what made it read as unclickable.
 *
 * So: the same ink as its neighbours, a tint with enough body to sit on a white card, and a gold
 * edge that is unmistakably an edge. The accent now comes from the border and the fill rather than
 * from washing out the text.
 */
:root[data-theme="light"] .pos-btn-lead {
  border-color: color-mix(in srgb, var(--gold) 75%, var(--border));
  background: color-mix(in srgb, var(--gold) 42%, #fff);
  color: var(--text);
  font-weight: 700;
}

:root[data-theme="light"] .pos-btn-lead:hover {
  border-color: var(--gold);
  background: color-mix(in srgb, var(--gold) 58%, #fff);
}

/* --- Getting a position onto the board ------------------------------------------------------ */

.pos-dialog { max-width: 420px; }

.pos-text {
  font-family: "Cascadia Mono", Consolas, monospace;
  font-size: 11.5px;
  line-height: 1.5;
}

.editor-layout {
  display: flex;
  gap: 10px;
  align-items: flex-start;
}

/* Squares as a plain grid of buttons. Not the real board: that one owns a position and enforces
   the rules, and an editor's whole job is to let you build a position the rules would not reach. */
.editor-board {
  display: grid;
  grid-template-columns: repeat(8, 1fr);
  width: 264px;
  flex: none;
  border-radius: 5px;
  overflow: hidden;
  box-shadow: 0 0 0 1px var(--border);
}

.editor-square {
  aspect-ratio: 1;
  border: none;
  padding: 0;
  cursor: pointer;
  background-size: 88%;
  background-position: center;
  background-repeat: no-repeat;
}

/* The board's own colours, so a position looks the same here as it will there. */
.editor-square.light { background-color: var(--sq-light, #f0d9b5); }
.editor-square.dark { background-color: var(--sq-dark, #b58863); }
.editor-square:hover { box-shadow: inset 0 0 0 2px var(--gold); }
.editor-square:focus-visible { outline: 2px solid var(--gold); outline-offset: -2px; }

.editor-palette {
  display: grid;
  grid-template-columns: repeat(2, 1fr);
  gap: 4px;
  flex: 1 1 auto;
  align-content: start;
}

.palette-piece {
  aspect-ratio: 1;
  border: 1px solid var(--border);
  border-radius: 6px;
  background: var(--sunken);
  background-size: 82%;
  background-position: center;
  background-repeat: no-repeat;
  cursor: pointer;
  font-size: 15px;
  color: var(--text-dim);
}

.palette-piece:hover { border-color: var(--gold-dim); }

.palette-piece.on {
  border-color: var(--gold);
  background-color: color-mix(in srgb, var(--gold) 16%, var(--sunken));
}

.editor-opts {
  display: flex;
  flex-direction: column;
  gap: 9px;
  margin-top: 12px;
}

.editor-opt {
  display: flex;
  flex-direction: column;
  gap: 5px;
}

.editor-actions { display: flex; gap: 6px; }

/* A right the position cannot have is shown but not offered, which says more than hiding it
   would: it explains, on hover, what would have to be true for it to be available. */
.tc-chip:disabled {
  opacity: 0.4;
  cursor: not-allowed;
}

.tc-chip:disabled:hover {
  border-color: var(--border);
  color: var(--text-faint);
}

/* Same fade-versus-draw problem as `.btn:disabled`, see the note in style.css. At 0.4 over a white
   page these chips were all but gone: "Bullet · Blitz · Rapid · Classical" is the readout of a
   setting, so a Free account could not see what its games were being filtered by, only that
   something was greyed out. */
:root[data-theme="light"] .tc-chip:disabled,
:root[data-theme="light"] .tc-chip:disabled:hover {
  opacity: 1;
  background: var(--surface-2);
  border-color: var(--border);
  color: var(--text-dim);
}

/* `.locked` adds `grayscale(0.6)` on top of the fade, which on a light page takes the last of the
   colour difference out of an already-washed control.
   No `background` here, and that is the point. The class is worn by the difficulty *slider*, whose
   own background is deliberately transparent so that only its 5px track paints, giving it a fill
   drew a 16px grey slab across the width of the card behind the track. A rule that is right for a
   button and wrong for a range input has no business being written for both, so this only touches
   the two properties that were the actual problem. */
:root[data-theme="light"] .locked {
  opacity: 0.8;
  filter: none;
}

:root[data-theme="light"] .sub-reset:disabled,
:root[data-theme="light"] .plan-switch:disabled,
:root[data-theme="light"] .settings-item:disabled {
  opacity: 1;
  color: var(--text-faint);
}

/* `.dash-facts` used to be here: two-column term/value rows, for cards whose content was a short
   table of numbers. Nothing renders one any more, the last of them was the primer tally, which is
   now a bar and a fraction inside the record card, and a layout with no callers is a thing the next
   person has to read before finding out it does nothing. */

/* One row per platform, stacked. This rule was at one point folded into the selector list of
   `.primer-list` below, which made it a wrapping flex row of dots, so each account's mark, name
   and button became three loose items and the two platforms ran into one another. They are two
   different lists and they want two different layouts. */
.account-list {
  display: flex;
  flex-direction: column;
  gap: 6px;
  margin: 2px 0 0;
  padding: 0;
  list-style: none;
}

/* Recent primers as coloured dots, newest last, so the row reads in the order they happened,
   the same left-to-right the rail above the board uses. They were pills of text: six of them
   spelling "solved" and "missed" over and over, which is a wall of the same two strings where a
   colour says it instantly. */
.primer-list {
  display: flex;
  flex-wrap: wrap;
  gap: 5px;
  margin: 0;
  padding: 0;
  list-style: none;
}

/* No primers yet is a row of nothing, and a row of nothing between the bar and the note reads as a
   rendering fault rather than an empty list. */
.primer-list:empty { display: none; }

.primer-dot {
  display: block;
  width: 13px;
  height: 13px;
  border-radius: 50%;
  border: 1px solid var(--border);
  background: var(--surface-2);
  transition: transform 120ms ease, box-shadow 120ms ease;
}

.primer-dot.solved {
  border-color: transparent;
  background: var(--good);
}

.primer-dot.missed {
  border-color: color-mix(in srgb, var(--bad) 50%, transparent);
  background: color-mix(in srgb, var(--bad) 22%, var(--surface-2));
}

.primer-dot:hover {
  transform: scale(1.18);
  box-shadow: 0 0 0 2px color-mix(in srgb, var(--gold) 45%, transparent);
}

/* Bar and fraction on one row: the fraction is what the bar means, and stacking them spent two
   lines saying it twice. `1fr auto` so the count never wraps and the bar takes the slack. */
.primer-summary {
  display: grid;
  grid-template-columns: minmax(0, 1fr) auto;
  align-items: center;
  gap: 9px;
}

.primer-count {
  font-size: 12.5px;
  font-weight: 700;
  font-variant-numeric: tabular-nums;
  color: var(--text);
}

.primer-of { color: var(--text-faint); font-weight: 600; }

.primer-bar {
  height: 6px;
  border-radius: 999px;
  background: var(--track);
  overflow: hidden;
}

.primer-bar-fill {
  height: 100%;
  width: 0;
  border-radius: inherit;
  background: var(--good);
  transition: width 600ms cubic-bezier(0.16, 0.9, 0.26, 1.02);
}

/* --- Dashboard: which chess accounts are attached --------------------------------------------- */

/* Mark, then username over platform, then the one thing you can do about the row. The button is
   pushed to the far edge by the text block taking the slack rather than by a margin, so both rows
   finish their control at the same x whatever length the username is. */
.account-row {
  display: flex;
  align-items: center;
  gap: 9px;
  padding: 5px 8px;
  border: 1px solid var(--border);
  border-radius: 9px;
  background: var(--sunken);
}

/* An unconnected platform is a prompt, not an account in a bad state, so the row reads as an empty
   slot: no fill, and a dashed edge that says something belongs here. */
.account-row.empty {
  border-style: dashed;
  background: transparent;
}

.account-mark {
  flex: none;
  width: 22px;
  height: 22px;
  border-radius: 5px;
}

/* Only ever seen if the artwork will not load, which is a supported state, not a failure: the
   chess.com mark is one `rm` away from being removed for trademark reasons (see the note in that
   file), and the panel has to keep working when it is. */
.account-mark-fallback {
  display: grid;
  place-items: center;
  font-size: 12px;
  font-weight: 700;
  color: var(--text-dim);
  background: var(--surface-2);
}

/* Takes the slack, and gives it up first: a long username ellipses rather than pushing the button
   out of the card. */
.account-text {
  display: flex;
  flex: 1 1 auto;
  min-width: 0;
  flex-direction: column;
  gap: 1px;
  line-height: 1.3;
}

.account-name {
  overflow: hidden;
  white-space: nowrap;
  text-overflow: ellipsis;
  font-size: 13px;
  font-weight: 600;
  color: var(--text);
}

.account-sub {
  font-size: 12px;
  color: var(--text-faint);
}

/* A state, never a control, every row that has something you can do about it ends in a real
   button instead. Muted deliberately: this is the one thing in the list you cannot press, and the
   whole reason the old "Claimed" pill was removed was that it looked like you could. */
.account-pill {
  flex: none;
  padding: 3px 9px;
  border-radius: 999px;
  font-size: 10.5px;
  font-weight: 600;
  letter-spacing: 0.04em;
  text-transform: uppercase;
}

.account-pill.verified {
  color: var(--good);
  background: color-mix(in srgb, var(--good) 14%, transparent);
  box-shadow: inset 0 0 0 1px color-mix(in srgb, var(--good) 32%, transparent);
}

/* An account kept over a lapsed Plus and not read on Free. Grey, because nothing is wrong with it. */
.account-pill.paused {
  color: var(--text-faint);
  background: var(--surface-2);
}

.account-row.paused .account-name { color: var(--text-dim); }

/* The state or the Verify button, then the remove cross. Its own box so the pair stays together at
   the row's right edge, and `flex: none` so a long username gives way before they do. */
.account-row-actions {
  display: flex;
  flex: none;
  align-items: center;
  gap: 4px;
}

/* A cross, quiet until it is wanted: removing an account is the rare thing to do to a row, and six
   red buttons down a list would make it the loudest. Armed, it becomes the word, in red, so the
   second press is a decision read rather than a place clicked twice. */
.account-remove {
  display: inline-grid;
  place-items: center;
  flex: none;
  min-width: 24px;
  height: 24px;
  padding: 0;
  border: 0;
  border-radius: 999px;
  background: transparent;
  color: var(--text-faint);
  font: inherit;
  font-size: 11.5px;
  font-weight: 600;
  cursor: pointer;
  transition: background 120ms ease, color 120ms ease;
}

.account-remove svg {
  width: 14px;
  height: 14px;
  fill: none;
  stroke: currentColor;
  stroke-width: 2;
  stroke-linecap: round;
}

.account-remove:hover,
.account-remove:focus-visible {
  color: var(--text);
  background: var(--surface-2);
}

.account-remove.armed {
  padding: 0 10px;
  color: var(--bad);
  background: color-mix(in srgb, var(--bad) 12%, transparent);
}

.account-remove:disabled { opacity: 0.5; cursor: default; }

@media (pointer: coarse) {
  .account-remove { min-width: 44px; height: 44px; }
}

/* Only there when something went wrong with the list, so it takes no room otherwise. */
.accounts-note:empty { display: none; }
.accounts-note.warn { color: var(--bad); }

/* --- Dashboard: what counts as a tactic worth showing you ------------------------------------- */
/*
   The same two settings the onboarding page carries (`.tier-*` and `.tc-option` above), at card
   scale. Deliberately not the same rules: onboarding is a full page with one job and can afford a
   22px readout and two-column tiles, while this has to sit in a 272px card next to four others
   without becoming the loudest thing on the dashboard.
*/
.pref-body {
  display: flex;
  flex-direction: column;
  gap: 9px;
}

.pref-block {
  display: flex;
  flex-direction: column;
  gap: 6px;
}

.pref-line {
  display: flex;
  align-items: baseline;
  justify-content: space-between;
  gap: 10px;
}

.pref-label {
  font-size: 12px;
  font-weight: 600;
  letter-spacing: 0.06em;
  text-transform: uppercase;
  color: var(--text-faint);
}

.pref-value {
  font-size: 13px;
  font-weight: 700;
  color: var(--gold-ink);
}

.pref-note {
  margin: 0;
  font-size: 12.5px;
  line-height: 1.5;
  color: var(--text-dim);
}

.pref-slider {
  width: 100%;
  height: 16px;
  margin: 0;
  appearance: none;
  -webkit-appearance: none;
  background: transparent;
  cursor: pointer;
}

/* `--pref-fill` is written by dashboard.js on every input event, so the filled part of the track
   follows the thumb. A hard stop rather than the onboarding slider's blend: there the gradient is
   saying "this end is harder", here the track is reporting a value, and a fade makes the place it
   stops impossible to read. */
.pref-slider::-webkit-slider-runnable-track {
  height: 5px;
  border-radius: 999px;
  /* Two layers, and the order is the whole trick. Underneath, a gold-to-ember gradient across the
     *full* track: warmer to the right, which is the direction the threshold gets more demanding,
     the same thing the onboarding slider says with its own gradient. On top, a hard-edged cover
     that is transparent up to the thumb and opaque `--surface-2` after it, so what shows through is
     the gradient up to the value and grey beyond.
     Sizing the gradient to the filled part instead would have been one layer, and wrong: the
     gradient would then be squeezed and stretched as the thumb moved, so a given colour would not
     mean a given setting and the warmth would say nothing. */
  background:
    linear-gradient(90deg,
      transparent 0 var(--pref-fill, 0%), var(--track) var(--pref-fill, 0%) 100%),
    linear-gradient(90deg, var(--gold) 0%, var(--ember) 100%);
}

.pref-slider::-moz-range-track {
  height: 5px;
  border-radius: 999px;
  /* Two layers, and the order is the whole trick. Underneath, a gold-to-ember gradient across the
     *full* track: warmer to the right, which is the direction the threshold gets more demanding,
     the same thing the onboarding slider says with its own gradient. On top, a hard-edged cover
     that is transparent up to the thumb and opaque `--surface-2` after it, so what shows through is
     the gradient up to the value and grey beyond.
     Sizing the gradient to the filled part instead would have been one layer, and wrong: the
     gradient would then be squeezed and stretched as the thumb moved, so a given colour would not
     mean a given setting and the warmth would say nothing. */
  background:
    linear-gradient(90deg,
      transparent 0 var(--pref-fill, 0%), var(--track) var(--pref-fill, 0%) 100%),
    linear-gradient(90deg, var(--gold) 0%, var(--ember) 100%);
}

.pref-slider::-webkit-slider-thumb {
  -webkit-appearance: none;
  width: 14px;
  height: 14px;
  margin-top: -4.5px;
  border: 2px solid var(--on-gold);
  border-radius: 50%;
  background: var(--gold-bright);
}

.pref-slider::-moz-range-thumb {
  width: 14px;
  height: 14px;
  border: 2px solid var(--on-gold);
  border-radius: 50%;
  background: var(--gold-bright);
}

.pref-slider:focus-visible { outline: 2px solid var(--gold); outline-offset: 3px; }

.tc-chips {
  display: flex;
  flex-wrap: wrap;
  gap: 5px;
}

/* The time controls as a 2x2 of equal cells, matching the analysis card's buttons directly below
   them. As a wrapping row of four pills sized to their own words they were the same "arranged by
   accident" look: a ragged second row, and four different widths saying nothing about the four
   choices being equivalent. */
.tc-grid {
  display: grid;
  grid-template-columns: repeat(4, minmax(0, 1fr));
  grid-auto-rows: 1fr;
  gap: 5px;
}

.tc-grid .tc-chip {
  display: flex;
  align-items: center;
  justify-content: center;
  /* Tight, because four of these share a card's width and the icon has to fit beside the word.
     `min-width: 0` so a long label shrinks the chip rather than overflowing the grid cell. */
  gap: 4px;
  padding: 6px 3px;
  min-width: 0;
  border-radius: 8px;
  font-size: 11px;
}

/* The icon used to be dropped at four across for want of room, which left the desktop layout
   looking bare next to the mobile one that kept it. The room was found instead: the chip's own
   padding and gap were the widest part of it, not the glyph. */
.tc-grid .tc-icon { display: block; }

@media (max-width: 1080px) {
  .tc-grid { grid-template-columns: 1fr 1fr; }
  .tc-grid .tc-chip { justify-content: flex-start; padding: 6px 9px; }
}

.tc-icon {
  flex: none;
  width: 13px;
  height: 13px;
  opacity: 0.85;
}

/* Toggles, so each one has to say both that it can be pressed and whether it currently is. Off is
   outlined and dim, on is filled with the accent at low strength and gets its colour back, the
   difference has to survive being glanced at rather than read. */
.tc-chip {
  padding: 4px 10px;
  border: 1px solid var(--border);
  border-radius: 999px;
  background: transparent;
  font: inherit;
  font-size: 11.5px;
  color: var(--text-faint);
  cursor: pointer;
  /* `color` is deliberately *not* transitioned, and taking it out is what makes the per-chip
     colours below survive the theme toggle.
     With it in the list, a chip whose `color` is `var(--tc-ink)` kept painting the previous theme's
     ink after a switch: the custom property had already updated, `getComputedStyle` reported the
     new value, and the screen showed the old one, so blitz stayed dark yellow on a white card.
     Measured over four consecutive toggles, stale every time with `color` listed and correct every
     time without it; `border-color` and `background-color` were re-measured the same way and track
     the theme fine, so only the one property comes out.
     Nothing is lost visually. The fade that reads on these is the fill and the outline arriving,
     which is still here; a word changing colour over 120ms is not something anyone sees. */
  transition: border-color 120ms ease, background-color 120ms ease;
}

/* The hover hint follows the chip's own colour, so pressing one previews what it becomes rather
   than flashing gold and then turning green. `--tc-accent` falls back to `--gold` on any chip that
   is not one of the four, which is what the default in the block below is for. */
.tc-chip:hover {
  border-color: color-mix(in srgb, var(--tc-accent) 45%, transparent);
  color: var(--text-dim);
}

/* One colour per time control, so the row reads as four kinds of game rather than four identical
   switches. Bullet orange, blitz yellow, rapid green, classical blue. The values live in
   `style.css` with the rest of the palette, per theme; this only says which chip takes which.

   The per-theme values stay on `:root`, next to `--gold` and `--gold-ink`, rather than being
   swapped by a `:root[data-theme="light"] .tc-chip[data-tc="…"]` rule here. That was the first
   version and it is worth knowing it was *not* the cause of the theme-toggle bug described on the
   `transition` above: rewriting it this way left the bug exactly where it was, and taking `color`
   out of the transition fixed it with either structure. This is kept because it is where the rest
   of the palette lives, so the light theme stays a second set of tokens rather than a second set
   of rules, not because it fixes anything.

   Only the `on` state is coloured. Off stays neutral and outlined, because that difference *is* the
   setting and it is the one thing this panel exists to report: four permanently coloured chips
   would say which time controls exist, not which ones are being read.

   `--gold` is the fallback so the class's other users, the position editor's side-to-move and
   castling pills, which are not time controls and carry no `data-tc`, stay as they were. */
.tc-chip {
  --tc-accent: var(--gold);
  --tc-ink: var(--gold-ink);
}

.tc-chip[data-tc="bullet"]    { --tc-accent: var(--tc-bullet);    --tc-ink: var(--tc-bullet-ink); }
.tc-chip[data-tc="blitz"]     { --tc-accent: var(--tc-blitz);     --tc-ink: var(--tc-blitz-ink); }
.tc-chip[data-tc="rapid"]     { --tc-accent: var(--tc-rapid);     --tc-ink: var(--tc-rapid-ink); }
.tc-chip[data-tc="classical"] { --tc-accent: var(--tc-classical); --tc-ink: var(--tc-classical-ink); }

.tc-chip.on {
  border-color: color-mix(in srgb, var(--tc-accent) 55%, transparent);
  background: color-mix(in srgb, var(--tc-accent) 14%, transparent);
  color: var(--tc-ink);
}

.tc-chip:focus-visible { outline: 2px solid var(--tc-accent); outline-offset: 2px; }

/* --- Dashboard: how you are doing ------------------------------------------------------------ */
/*
   Three things, left to right: the one number that answers "am I any good at this", the counts
   behind it, and the motifs sorted by how you do on them. The ring is first and largest because
   accuracy is the only figure here that means something on its own, 340 puzzles solved is not
   good or bad until you know how many were attempted.
*/
.stats-body {
  display: grid;
  grid-template-columns: auto minmax(140px, 1fr) minmax(210px, 1.4fr);
  align-items: start;
  gap: 16px;
  margin-top: 8px;
}

/* An arc rather than a bar: a proportion of a whole reads as a proportion when it is drawn as one,
   and this is the figure the card exists for. The value sits in the middle, where the eye lands. */
.stat-ring {
  position: relative;
  width: 100px;
  height: 100px;
  flex: none;
}

.stat-ring svg { width: 100%; height: 100%; transform: rotate(-90deg); }

.stat-ring-track { stroke: var(--track); }

/* The arc sweeps from the deep end of the gold into the bright end as it goes round, so a long arc
   is visibly hotter than a short one and the ring reads before the number in the middle of it does.
   Defined here rather than in the SVG so it follows the theme; dashboard.js points the gradient's
   stops at these. */
.stat-ring {
  --ring-from: var(--ember);
  --ring-to: var(--gold-bright);
}

/* The arc grows by its dash *offset*, never its dash array. Both draw the same arc; only one of them
   is a single number. Transitioning the array meant interpolating a list of two lengths, which
   re-derives the dash pattern and re-rasterises the stroke on every frame, and it ran during the
   worst 700ms on the page, with six fetches landing, the sparkline and three lists being built and
   the day's board decoding its pieces. That is what made it stutter. */
.stat-ring-fill {
  stroke-linecap: round;
  transition: stroke-dashoffset 700ms cubic-bezier(0.16, 0.9, 0.26, 1.02);
}

/* The ring's version of the light that travels along the primer segment you are on. Same idea, same
   pacing: a short bright dash making a circuit on a loop, kept to the filled arc so it never
   suggests progress that is not there.

   It moves by rotating, not by walking `stroke-dashoffset` along the path. This is the one animation
   on the page that never stops, so what it costs per frame is what it costs for as long as the tab
   is open, and a dash offset is a geometry property, meaning every frame re-derives the pattern and
   re-rasterises a 9px round-capped stroke on the main thread. Rotating the same static dash puts it
   in exactly the same places for a transform, which does not touch the geometry at all.

   `--ring-travel-deg` is how far it may turn before its leading edge reaches the end of the arc.
   dashboard.js works it out; the short version is the angle subtended by the arc less the dash's own
   length. `transform-box: view-box` is what makes `transform-origin` the centre of the 120x120
   viewBox rather than the circle's own bounding box, which is not the same point.

   The delay holds the glint back until the arc has finished growing under it. Without it the dash
   sets off immediately and spends the first few hundred milliseconds ahead of the arc's own leading
   edge, out on the bare track, which is the one place it must never be.

   **It fades out before the loop restarts, and that is not decoration.** A travelling highlight has
   to get back to the beginning somehow, and there is no way round the circle that is not either a
   jump or a journey. It used to reach the end of the arc, sit there, and then be somewhere else the
   next frame, a bright dot teleporting from the end to the start, which is the one thing the eye
   is guaranteed to catch. So the opacity track is timed against the travel: lit shortly after it
   sets off, dimmed as it arrives, and dark for the whole of the pause in which it is put back. The
   two animations share a duration and a delay for exactly this reason, if they ever drift apart,
   the jump becomes visible again. */
.stat-ring-glint {
  stroke: color-mix(in srgb, var(--gold-bright) 70%, transparent);
  stroke-linecap: round;
  opacity: 0;
  transform-box: view-box;
  transform-origin: 60px 60px;
  animation:
    ring-glint-travel 2.6s ease-in-out 700ms infinite,
    ring-glint-fade 2.6s linear 700ms infinite;
}

/* Travels for the first 62%, then holds at the end, unseen, because the fade below has already
   taken it to nothing by then. The hold is the pause between passes. */
@keyframes ring-glint-travel {
  0% { transform: rotate(0deg); }
  62%, 100% { transform: rotate(calc(var(--ring-travel-deg, 0) * 1deg)); }
}

/* Dark at both ends of the loop, so the reset happens where there is nothing to see. */
@keyframes ring-glint-fade {
  0%, 100% { opacity: 0; }
  14%, 50% { opacity: 1; }
  62% { opacity: 0; }
}

/* A perpetual loop is the first thing to go for anyone who has asked for less movement, and unlike
   the segment sweep this one has no state to communicate, so it simply stops. Both halves of it: the
   fade is what keeps the travel from being seen resetting, so stopping one without the other would
   leave a lit dash frozen wherever it happened to be. */
@media (prefers-reduced-motion: reduce) {
  .stat-ring-glint { animation: none; opacity: 0; }
}

.stat-ring-centre {
  position: absolute;
  inset: 0;
  display: grid;
  place-content: center;
  text-align: center;
  gap: 1px;
}

.stat-ring-value {
  font-size: 27px;
  font-weight: 700;
  line-height: 1;
  font-variant-numeric: tabular-nums;
}

.stat-ring-label {
  font-size: 11.5px;
  letter-spacing: 0.07em;
  text-transform: uppercase;
  color: var(--text-faint);
}

/* A size container, so the card can answer the only question that decides its layout: how wide is
   the slot I am in?
   
   Every fallback it had was keyed on the window, which is a proxy that is wrong in both directions:
   the same 780px window holds this card at full width on `/dashboard-classic` and at half a column
   on the home tab, and the home tab's own slot goes from 486px to 134px without the window
   crossing a breakpoint at all. Three steps down, each one measured off the widths in the rules
   above rather than off a device.
   
   `inline-size` only, so the card's height still comes from its content and nothing here can clip
   vertically. */
.stats-card { container-type: inline-size; }

/* Two across where two fit, one where they do not, decided by the row rather than by a number
   somebody chose. It was `repeat(2, minmax(0, 1fr))`, and `minmax(0, …)` is an instruction to
   shrink to nothing: at 940px on the home tab each tile was 45px holding 51px, and `1/14` was
   painted into a 25px box.

   `50% - 3px` is half the row less half the gap, so two tracks fill it exactly and three can never
   fit. `auto-fit` on the floor alone put three across a wide row and left the fourth tile alone on
   a second, which is a worse answer than the two-by-two it replaced. This is two-or-one and nothing
   else, which is the only choice this row has ever wanted to make.

   76px is the floor under that, and it is bounded from both sides. `1/14` is 41px at 19px bold and
   the tile adds 20px of padding, so 61 is what a track has to hold. The narrowest column this card
   lives in is 166px, at 760px on the dashboard, and two tracks plus their gap have to fit inside
   it, so the floor cannot exceed 80. 76 sits between the two.

   Deliberately not sized for the widest figure the card could ever show. Measured, `487/1203` wants
   105px a track, and a floor that high does fit inside 166 for one track but not two: it kept the
   big numbers legible and cost the phone its two-by-two, four rows where there were two and 130px
   of a screen that has none to spare. If the counts ever do run to four digits, the figure is the
   thing that should give way, not the layout. */
.stat-tiles {
  display: grid;
  grid-template-columns: repeat(auto-fit, minmax(max(76px, calc(50% - 3px)), 1fr));
  gap: 6px;
}

/* Three columns into two. The ring is 100, the counts ask for 140 and the motif list for 210, plus
   two 16px gaps and the card's own 28px of padding: 510. Below that the ring's labels were being
   painted over the motif list, which is what the 780px media query was put here for. The motif
   list, the one thing here that is genuinely full width, spans both underneath, and the tiles give
   up two pixels each to the ring's column against filling the card and 123px off its height. */
@container (max-width: 520px) {
  .stats-body {
    grid-template-columns: auto minmax(0, 1fr);
    justify-items: stretch;
  }

  .stat-motifs { grid-column: 1 / -1; }
}

/* And the ring comes off the row entirely once even that is not enough. It is a fixed 100px with
   `flex: none`, so it never gives way, and below this it was leaving the counts beside it 45px to
   hold a sparkline whose own minimum is 118. Stacked, the card is correct at any width down to
   about 150px, which is narrower than any slot on the site. */
@container (max-width: 260px) {
  .stats-body { grid-template-columns: minmax(0, 1fr); }

  .stat-ring { justify-self: center; }
}

.stat-tile {
  padding: 7px 10px;
  border-radius: 9px;
  background: var(--surface-2);
}

.stat-tile-value {
  display: block;
  font-size: 19px;
  font-weight: 700;
  line-height: 1.15;
  font-variant-numeric: tabular-nums;
}

.stat-tile-label {
  display: block;
  margin-top: 1px;
  font-size: 11px;
  line-height: 1.35;
  color: var(--text-dim);
}

/* The streak is the one tile worth colouring: it is the only number that can be lost, so it is the
   only one anybody watches. */
.stat-tile.streak .stat-tile-value { color: var(--gold-ink); }

.stat-section-title {
  margin: 0 0 6px;
  font-size: 12px;
  font-weight: 700;
  letter-spacing: 0.07em;
  text-transform: uppercase;
  color: var(--text-faint);
}

/* The heading and the sort control on one line. `baseline` rather than `center`, because the label
   is 11px tracked caps and the buttons are 10.5px: centred on the box they read as sitting slightly
   low against the word beside them. */
.stat-section-head {
  display: flex;
  align-items: baseline;
  justify-content: space-between;
  gap: 10px;
}

.stat-section-head .stat-section-title { margin-bottom: 6px; }

/* Two halves of one control rather than two buttons, so it reads as a switch with a side selected
   rather than as a pair of things to press. The unselected half is quiet: this is a preference, not
   an action, and it should not compete with the rows underneath it. */
.motif-sort {
  display: inline-flex;
  flex: none;
  padding: 2px;
  border-radius: 999px;
  background: var(--sunken);
  box-shadow: inset 0 0 0 1px var(--border);
}

.motif-sort-btn {
  padding: 2px 9px;
  border: 0;
  border-radius: 999px;
  background: none;
  color: var(--text-faint);
  font: inherit;
  font-size: 10.5px;
  font-weight: 600;
  letter-spacing: 0.02em;
  line-height: 1.5;
  cursor: pointer;
  transition: background-color 140ms ease, color 140ms ease;
}

.motif-sort-btn:hover { color: var(--text); }

.motif-sort-btn[aria-pressed="true"] {
  background: var(--surface);
  color: var(--text-bright);
  box-shadow: 0 1px 2px rgba(0, 0, 0, 0.18);
  cursor: default;
}

.motif-sort-btn:focus-visible { outline: 2px solid var(--gold); outline-offset: 1px; }

/* One row per motif: the name, a bar as long as your accuracy, and the raw fraction. The bar is
   what makes the list scannable, sorted worst-first, a ragged left edge is the answer to "what
   should I work on" without reading a single percentage. */
.motif-list {
  display: grid;
  gap: 5px;
  margin: 0;
  padding: 0;
  list-style: none;
}

.motif-row {
  display: grid;
  grid-template-columns: minmax(84px, auto) 1fr auto;
  align-items: center;
  gap: 10px;
  font-size: 12px;
}

.motif-name {
  color: var(--text);
  white-space: nowrap;
  overflow: hidden;
  text-overflow: ellipsis;
}

.motif-bar {
  height: 6px;
  border-radius: 999px;
  background: var(--surface-2);
  overflow: hidden;
}

/* The fill colour is set per row by `motifColour` in dashboard.js, red at 0, gold at half, green
   at 1, because it is computed from the number rather than chosen from a set. The `.weak`,
   `.middling` and `.strong` classes are still on the row for anything that wants the coarse
   answer, and `.thin` still fades a row with too few attempts to judge, which is a job colour
   should not be asked to do as well. */
.motif-bar-fill {
  height: 100%;
  border-radius: inherit;
  background: var(--good);
  transition: width 600ms cubic-bezier(0.16, 0.9, 0.26, 1.02);
}

.motif-score {
  font-size: 11.5px;
  font-variant-numeric: tabular-nums;
  color: var(--text-dim);
  white-space: nowrap;
}

/* Too few attempts to mean anything. Shown rather than hidden, it is still something you have met,
   but greyed, so it cannot be mistaken for a verdict. */
.motif-row.thin { opacity: 0.55; }

/* --- In your queue, and how big the misses are ------------------------------------------------
   `renderQueueSummary`'s two sections, as a second row of `.stats-body`'s own grid: the queue under
   the ring and the counts, the misses under "By motif".

   Not more rows in the "By motif" column, which is where they went first. That column is already
   the longest in the card, so lengthening it made the card taller, and because the row stretches
   every card to the tallest, all three cards in it got a new empty block: the problem this was
   supposed to fix, moved and tripled. What the row can hold is what the motif of the day card
   leaves, measured at 144px at 1912px wide, so both sections are sized to fit under that.

   The bar is gold at every length rather than coloured by accuracy like `.motif-list`, because
   nothing in the queue has been attempted yet to be right or wrong about. */
.queue-motifs { grid-column: 1 / 3; }
.queue-severity { grid-column: 3 / 4; }

.queue-total { font-size: 11px; color: var(--text-faint); font-variant-numeric: tabular-nums; }

.queue-bar-list { display: grid; gap: 5px; }

.queue-bar-row {
  display: grid;
  grid-template-columns: minmax(84px, auto) 1fr auto;
  align-items: center;
  gap: 10px;
  font-size: 12px;
}

.queue-bar-name { color: var(--text); white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }

.queue-bar-track { height: 6px; border-radius: 999px; background: var(--surface-2); overflow: hidden; }

.queue-bar-fill {
  height: 100%;
  border-radius: inherit;
  background: linear-gradient(90deg, var(--gold-dim), var(--gold));
  transition: width 600ms cubic-bezier(0.16, 0.9, 0.26, 1.02);
}

.queue-bar-count {
  font-size: 11.5px;
  font-variant-numeric: tabular-nums;
  color: var(--text-dim);
  white-space: nowrap;
}

/* One bar split three ways, because the three counts are shares of one whole. */
.severity-bar {
  display: flex;
  gap: 2px;
  height: 8px;
  margin: 2px 0 9px;
  border-radius: 999px;
  overflow: hidden;
  background: var(--surface-2);
}

.severity-seg { flex: 1 1 0; min-width: 3px; }

.severity-legend { display: grid; gap: 4px; }

.severity-row {
  display: grid;
  grid-template-columns: 8px minmax(0, 1fr) auto;
  align-items: center;
  gap: 8px;
  font-size: 12px;
}

.severity-swatch { width: 8px; height: 8px; border-radius: 2px; }

.severity-name { color: var(--text); white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }

.severity-count { font-size: 11.5px; font-variant-numeric: tabular-nums; color: var(--text-dim); }

/* Deeper as the miss gets bigger: faint gold, gold, then the red the board uses for a wrong move. */
.severity-seg.is-small,
.severity-swatch.is-small { background: color-mix(in srgb, var(--gold) 35%, var(--surface-2)); }
.severity-seg.is-medium,
.severity-swatch.is-medium { background: var(--gold); }
.severity-seg.is-large,
.severity-swatch.is-large { background: var(--bad); }

/* Where `.stats-body` drops to two tracks, and then to one, both sections go full width the way
   "By motif" does, rather than asking for a third track that is no longer there. */
@container (max-width: 520px) {
  .queue-motifs,
  .queue-severity { grid-column: 1 / -1; }
}

/* On the Home tab they only appear where the card has room for them, which is a narrow set of
   places, because this card does not set its own height. The motif of the day card beside the
   column does: it leaves the row about 445px, which is also what the openings card needs.
   Measured on an account with a full "By motif" list:

     * Wider than 1360px the two cards sit side by side. Where this card is wide enough for three
       tracks (1912px) it is 302px without these and 419px with them, so they fit. Where it is
       narrow (1440 to 1760px) it is already 440 to 490px without them, as tall as the row, and
       their 228px stacked stretched the openings and motif of the day cards by 228px of nothing.
     * From 1001 to 1360px the pair stacks, but the motif of the day card is still beside the
       column and stretched to its full height, so every pixel added here is one more blank pixel
       in that card.
     * At 1000px and under the page is one column, nothing shares a height, and they stay.

   So they are hidden in the two-column layout and brought back only where the pair is side by
   side and this card has its three tracks. Scoped to `.train-under`, the Home tab's own slot, so
   anywhere else this card is drawn keeps them. */
@media (min-width: 1001px) {
  .train-under .queue-motifs,
  .train-under .queue-severity { display: none; }
}

@media (min-width: 1361px) {
  @container (width > 520px) {
    .train-under .queue-motifs,
    .train-under .queue-severity { display: block; }
  }
}

/* Thirty days, one column per day, height by how many puzzles were finished. A sparkline rather
   than a chart: the question it answers is "have I been turning up", which needs no axis. */
.activity-strip {
  display: flex;
  align-items: flex-end;
  gap: 2px;
  height: 26px;
  margin-top: 4px;
}

.activity-day {
  flex: 1 1 0;
  min-width: 2px;
  min-height: 2px;
  border-radius: 2px 2px 1px 1px;
  background: color-mix(in srgb, var(--gold) 40%, var(--surface-2));
}

.activity-day.none { background: var(--track); }

/* Two columns rather than one, and stretched rather than shrink-wrapped.
 *
 * `1fr` with `justify-items: start` is what left the panel looking half empty on a phone, and it
 * did it twice over. The ring is 100px wide, so a row of its own is 100px of ring and 229px of
 * nothing; and `start` sizes every *other* row to its contents too, which is why the four tiles
 * were 218px of a 329px card with a hole beside them. Measured on a 393px screen.
 *
 * The ring belongs beside the counts it is the summary of, so it goes there, and the motif list,
 * the one thing here that is genuinely full width, spans both underneath. The tiles give up two
 * pixels each to the ring's column, which is nothing against what they gain: they fill the card,
 * and the card comes out 123px shorter. */
/* This was the window-keyed half of the same rule and has gone the same way: at 780px wide the card
   is narrow on the dashboard and full width on `/dashboard-classic`, so the query was right about
   one of them at a time. See the `@container` rules beside `.stats-body`. */

.dash-foot {
  margin: 0;
  display: flex;
  align-items: center;
  justify-content: space-between;
  flex-wrap: wrap;
  gap: 10px 18px;
  font-size: 12.5px;
  color: var(--text-faint);
}

/* `.dash-foot` is in this list for `history.html`, which put it on a bare <p> holding one "Back to the
   dashboard" link. That link and its copies on the openings and trophies pages went on 2026-09-11,
   the rail having made them redundant, so only the dashboards use `.dash-foot` now. The selector
   stays because this rule has been setting the dashboard footer's 10px gap ever since, over the
   `10px 18px` above, and taking it out would move the footer's links. */
.dash-foot,
.foot-links {
  margin: 0;
  display: flex;
  align-items: center;
  gap: 10px;
  /* The row breaks *between* links, and this rule and the one under it are one fix in two halves.
   *
   * `.foot-links` was a flex row that could not wrap, so on a phone it did the only other thing a
   * flex row can do when it runs out of width: squeeze every item until the text inside it wrapped
   * instead. The result was a footer reading "Log out" over two lines, "How it works" over three,
   * and the owner's tier switch, which is the longest item and cannot shrink to nothing, hanging
   * off the right edge of the screen entirely.
   *
   * The wrap gives it somewhere to break. The `nowrap` below is what stops it breaking anywhere
   * else, and without both, adding `flex-wrap` alone still leaves a squeezed first line. */
  flex-wrap: wrap;
}

/* Each link is one unbroken thing. The separators are `·` and mean nothing on their own, so a line
   that begins with one reads as a typo. */
.foot-links > * { white-space: nowrap; }

/* Marks on the left, the account links on the right. Ordered here rather than by moving the markup,
   because the reading order is the one the markup already has: "log out / help / report a bug" is
   what somebody comes to a footer looking for, and a screen reader should meet it first whichever
   end of the row it is painted at. */
.foot-social { order: 1; }
.foot-links { order: 2; }

.dash-foot a,
.dash-foot .linkish { color: var(--text-dim); }

/* The two marks at the other end of the footer. Sized by the box rather than by the glyph so the
   hit target is a comfortable 30px square while the icon inside stays small enough to read as a
   footnote, a 30px Instagram logo down here would outweigh everything above it. */
.foot-social {
  display: flex;
  align-items: center;
  gap: 4px;
}

.social-link {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  width: 30px;
  height: 30px;
  border-radius: 8px;
  color: var(--text-faint);
  transition: color 120ms ease, background-color 120ms ease;
}

.social-link svg { width: 17px; height: 17px; }

.social-link:hover,
.social-link:focus-visible {
  color: var(--gold-ink);
  background: var(--surface-2);
}

/* A button that has to read as a link, because it sits in a row of them. */
.linkish {
  padding: 0;
  border: none;
  background: none;
  font: inherit;
  text-decoration: underline;
  cursor: pointer;
}

/* --- Engine comparison (test page) ------------------------------------------------------------ */

.cmp-controls { display: flex; align-items: center; gap: 10px; flex-wrap: wrap; }
.cmp-controls .threshold-label { margin: 0; }
.cmp-controls .username-input { margin: 0; width: 84px; }

.cmp-summary { margin-bottom: 14px; }
.cmp-rows { display: grid; gap: 8px; }

.cmp-row {
  padding: 9px 11px;
  border-radius: 8px;
  background: var(--surface-2);
  font-size: 12px;
}

.cmp-row.bad { box-shadow: inset 0 0 0 1px color-mix(in srgb, var(--bad) 55%, transparent); }

.cmp-row-head { display: flex; gap: 9px; align-items: baseline; margin-bottom: 4px; }
.cmp-index { font-weight: 700; color: var(--gold-ink); }

.cmp-fen {
  font-family: ui-monospace, SFMono-Regular, Menlo, Consolas, monospace;
  font-size: 10.5px;
  color: var(--text-faint);
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
}

.cmp-row-body { display: flex; flex-wrap: wrap; gap: 4px 18px; color: var(--text-dim); }
.cmp-side b { color: var(--text); font-weight: 600; }
.cmp-problem { margin: 5px 0 0; color: var(--bad); }
.cmp-note { margin: 4px 0 0; color: var(--text-faint); }

/* --- Reporting a bug -------------------------------------------------------------------------- */

.bug-dialog {
  width: min(520px, calc(100vw - 32px));
  padding: 22px;
  border: 1px solid var(--border);
  border-radius: 14px;
  background: var(--surface);
  color: var(--text);
  box-shadow: var(--shadow-pop);
}

.bug-dialog::backdrop { background: rgba(4, 3, 20, 0.55); }

.bug-title { margin: 0 0 6px; font-size: 16px; font-weight: 700; }

.bug-lede {
  margin: 0 0 14px;
  font-size: 12.5px;
  line-height: 1.55;
  color: var(--text-dim);
}

.bug-lede a { color: var(--gold-ink); }

.bug-label {
  display: block;
  margin: 0 0 5px;
  font-size: 11px;
  font-weight: 700;
  letter-spacing: 0.07em;
  text-transform: uppercase;
  color: var(--text-faint);
}

.bug-what {
  width: 100%;
  margin-bottom: 14px;
  padding: 9px 11px;
  border: 1px solid var(--border);
  border-radius: 8px;
  background: var(--sunken);
  color: var(--text);
  font: inherit;
  font-size: 13px;
  resize: vertical;
}

.bug-what:focus-visible { outline: 2px solid var(--gold); outline-offset: 1px; }

/* Shown rather than hidden: somebody about to send their browser string and account name to a
   stranger is entitled to see exactly what that is first. */
.bug-details {
  margin: 0 0 14px;
  padding: 10px 11px;
  max-height: 132px;
  overflow: auto;
  border-radius: 8px;
  background: var(--sunken);
  color: var(--text-dim);
  font-family: ui-monospace, SFMono-Regular, Menlo, Consolas, monospace;
  font-size: 11px;
  line-height: 1.55;
  white-space: pre-wrap;
  word-break: break-word;
}

/* --- Board colour and piece set, in the settings popout ---------------------------------------- */

/* Label above, swatches on their own full-width line below.
   The other settings rows put the control beside its label, and this one cannot: the popout's row is
   224px, the label eats 99 of it, and six 20px swatches with gaps need 145, so beside the label
   they were squeezed to 143.75 and the sixth wrapped onto a line by itself, reading as a different
   kind of choice from the other five. Shrinking the swatches instead would have made the hit target
   smaller than a fingertip to save a line in a menu. */
.settings-row.is-stacked {
  flex-direction: column;
  align-items: stretch;
  gap: 6px;
}

.swatches {
  display: flex;
  flex-wrap: wrap;
  gap: 5px;
}

.swatch {
  width: 20px;
  height: 20px;
  padding: 0;
  border: 1px solid var(--border);
  border-radius: 5px;
  cursor: pointer;
  background-size: cover;
  transition: transform 110ms ease, border-color 110ms ease;
}

.swatch:hover { transform: scale(1.12); }

/* The chosen one gets a gold ring *outside* its border rather than a thicker border, so picking a
   swatch does not change its size and shuffle the row. */
.swatch[aria-pressed="true"] {
  border-color: var(--gold);
  box-shadow: 0 0 0 2px color-mix(in srgb, var(--gold) 55%, transparent);
}

/* A knight from the set, on a neutral tile, the piece is the choice, so the tile must not be one
   of the board colours or the two controls would look like they set the same thing. */
.swatch-piece {
  background-color: var(--surface-2);
  background-repeat: no-repeat;
  background-position: center;
  background-size: 84%;
}

/* --- The leaderboard, and the countdown to launch ---------------------------------------------- */

/* The body is the containing block for the cover, so the cover stops at the heading rather than
   swallowing it. `min-height` is what stops the countdown being squeezed into a two-line strip
   before launch, when the only thing under it is one note and an empty list. */
/* `flex: 1` so the body, and therefore the cover over it, runs to the bottom of the card rather
   than stopping wherever the list happens to end. `.dash-card` is already a flex column, so this
   costs one declaration and makes the countdown fill the panel it is standing in for. */
.board-body {
  position: relative;
  flex: 1 1 auto;
  min-height: 132px;
}

/* The board's precondition, standing where the board will be.
   In normal flow rather than absolutely positioned like `.launch` above, and that is the difference
   between the two: the countdown was a cover over a board that existed underneath it, where this is
   what the card *has*, because the server sends no rows until the field is there. So it should size
   the card like any other content, and `.board-body`'s `min-height` is what stops the card jumping
   on the day it is replaced by ten rows. */
.board-gate {
  display: flex;
  flex-direction: column;
  gap: 8px;
  padding: 4px 0 2px;
}

.board-gate.hidden { display: none; }

/* The number first and at size, because it is the one thing on this card that changes. Tabular so a
   9 becoming a 10 does not shift the words beside it. */
.board-gate-count {
  margin: 0;
  font-size: 13px;
  color: var(--text-dim);
  font-variant-numeric: tabular-nums;
}

.board-gate-count strong {
  font-size: 26px;
  font-weight: 700;
  color: var(--text);
  margin-right: 4px;
}

/* Deliberately the same 6px track and the same gradient as `.scan-bar`: both are "how far through
   something are we", the dashboard already teaches that shape once, and a second bar drawn its own
   way would read as a different kind of measurement. */
.board-gate-bar {
  position: relative;
  height: 6px;
  border-radius: 3px;
  background: var(--track);
  overflow: hidden;
}

.board-gate-fill {
  height: 100%;
  width: 0;
  border-radius: 3px;
  background: linear-gradient(90deg, var(--good), var(--gold));
  transition: width 300ms ease-out;
}

.board-gate-note {
  margin: 0;
  font-size: 11.5px;
  line-height: 1.45;
  color: var(--text-dim);
}

/* While the countdown is standing in for the board, the board is not measured.
   `.launch` is `position: absolute`, so it takes the panel out of sight and leaves every pixel of
   its height behind: eight players on the live leaderboard sized this card to 282px from behind an
   opaque cover, and the two cards beside it stretched to match, which is a row of three cards each
   with 60px of nothing at the bottom and no visible cause. `min-height` above is then what the
   countdown actually stands in. Set by `tickLaunch`, which is the one place that knows whether the
   cover is up. */
.board-body.is-covered > :not(.launch) { display: none; }

/* The countdown, over the board and not over the title. */
.launch {
  position: absolute;
  inset: 0;
  z-index: 2;
  display: flex;
  flex-direction: column;
  align-items: center;
  justify-content: center;
  gap: 6px;
  border-radius: 10px;
  border: 1px solid color-mix(in srgb, var(--gold) 42%, var(--border));
  background:
    radial-gradient(120% 90% at 50% 0%, color-mix(in srgb, var(--gold) 13%, transparent), transparent 70%),
    var(--surface);
}

.launch[hidden] { display: none; }

.launch-label {
  font-size: 10px;
  font-weight: 700;
  letter-spacing: 0.18em;
  text-transform: uppercase;
  color: var(--gold-ink);
}

/* Tabular figures, so the clock does not jitter as the digits change width. This is the one number
   on the page that is repainted while somebody is looking straight at it. `clamp` because the card
   is a grid cell whose width changes with the breakpoint, and a fixed size that fits at 1600px
   wraps at 900. */
.launch-clock {
  font-variant-numeric: tabular-nums;
  font-size: clamp(26px, 4.2vw, 42px);
  font-weight: 700;
  line-height: 1;
  letter-spacing: 0.01em;
  color: var(--text-bright);
}

.launch-when {
  font-size: 11.5px;
  color: var(--text-dim);
}

.board-list {
  margin: 0;
  padding: 0;
  list-style: none;
  display: grid;
  gap: 1px;
}

/* Rank, name, score. A grid rather than a flex row so every rank column is the same width and the
   names line up, with `flex` a two-digit rank shunts its whole row right and the column reads as
   ragged. */
.board-row {
  display: grid;
  grid-template-columns: 22px minmax(0, 1fr) auto;
  align-items: center;
  gap: 8px;
  padding: 4px 7px;
  border-radius: 7px;
  font-size: 12.5px;
  color: var(--text-dim);
}

.board-row:nth-child(odd) { background: var(--sunken); }

.board-rank {
  font-variant-numeric: tabular-nums;
  font-size: 11px;
  color: var(--text-faint);
}

.board-name {
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
  color: var(--text);
}

.board-score {
  font-variant-numeric: tabular-nums;
  font-weight: 700;
  color: var(--gold-ink);
}

/* You. Filled rather than merely bolded, because the whole reason to look at a leaderboard is to
   find your own row and a weight change is not enough to catch at a glance. */
.board-row.is-you {
  background: color-mix(in srgb, var(--gold) 14%, var(--surface));
  color: var(--text);
}

.board-row.is-you .board-name { font-weight: 700; }

/* Set apart, because this row is not the one after the row above it. The gap and the rule are the
   whole message: without them a rank of 27 under a rank of 10 reads as a rendering bug. */
.board-row.is-adrift {
  margin-top: 7px;
  border-top: 1px dashed var(--border);
  border-radius: 0 0 7px 7px;
  padding-top: 8px;
}

/* The scan, folded into the accounts card. Separated by a rule rather than by a heading: it is a
   footnote on the accounts above it, and a second heading inside one card reads as two cards that
   failed to divide. */
/* No rule, no padding, no margin. This block is empty the overwhelming majority of the time, a
   scan is running for a minute or two after you arrive and never again, so any chrome it carries is
   a divider under the last account row separating it from nothing at all. The progress bar brings
   its own spacing when it appears. */
.account-scan {
  display: grid;
  gap: 6px;
  justify-items: start;
}

.account-scan .dash-note { margin: 0; }
.account-scan .scan-progress { width: 100%; }

/* `.dash-note` carries a min-height so a status line appearing does not shove the card, which is
   right where one is expected and wrong here: these two are empty except during a scan, and empty
   they were still 12px each plus the grid gap, 29px of nothing under the last account row, which
   is the space this block was supposed to stop wasting. Collapsed while they have no text, and back
   to their normal height the moment they get some. */
.account-scan .dash-note:empty { display: none; }

/* --- Redeeming a code ------------------------------------------------------------------------- */

/* The way in, on `/pricing`. That page used to wear `landing.css`'s clothes rather than the app's,
   so this took the poster's gold, and the `#d8ac53` fallback here was that gold spelled out. The
   page is on `style.css` now, where `--gold` is the ember orange reserved for things you press, and
   as *text* on the light theme's near-white ground that measured 3.04:1, under AA. `--gold-ink` is
   the token that exists for exactly this, gold said in ink rather than as a fill, and it is what
   `.bug-fallback .linkish` already uses a few hundred lines down. */
.code-aside { margin-top: 6px; }

.code-open {
  color: var(--gold-ink);
  font: inherit;
}

/* Monospaced and tracked out, because this is six characters being copied from a video one at a
   time and the reader needs to see that they have typed six. Upper-cased on the way in as well,
   `fold` accepts lower case, but a box that shows MOTIFY-KJ7X2P while you type is a box that lets
   you check your work against what is on screen. */
.code-input {
  font-family: ui-monospace, SFMono-Regular, Menlo, Consolas, monospace;
  font-size: 15px;
  letter-spacing: 0.12em;
  text-transform: uppercase;
}

.code-input::placeholder { letter-spacing: 0.08em; text-transform: none; }

/* --- The queue explainer ---------------------------------------------------------------------- */

.queue-para {
  margin: 0 0 12px;
  font-size: 13px;
  line-height: 1.6;
  color: var(--text-dim);
}

/* Numbered, because the whole point is that they happen in order. The marker is inside so a lap
   that wraps to two lines stays inside the block rather than hanging into the dialog's padding. */
.queue-laps {
  margin: 0 0 14px;
  padding: 12px 14px 12px 30px;
  border-radius: 10px;
  background: var(--sunken);
  font-size: 13px;
  line-height: 1.55;
  color: var(--text-dim);
}

.queue-laps li + li { margin-top: 8px; }
.queue-laps strong { color: var(--text); font-weight: 600; }

/* The diagnostics, foldable. The marker is the only affordance saying there is something under
   here, so it keeps the pointer and the label styling rather than being a bare <summary>. */
.bug-diag { margin-bottom: 4px; }
/* `display: list-item`, not the `block` `.bug-label` would otherwise give it: a <summary> that is
   not a list item loses its disclosure triangle, and the triangle is the only thing on screen
   saying the block folds. */
.bug-diag > summary { display: list-item; cursor: pointer; }
.bug-diag > summary::marker { color: var(--text-faint); }
.bug-diag[open] > summary { margin-bottom: 6px; }

.bug-actions { display: flex; flex-wrap: wrap; gap: 8px; }

.bug-note {
  margin: 10px 0 0;
  min-height: 1em;
  font-size: 12px;
  color: var(--good);
}

.bug-note.bad { color: var(--bad); }

.bug-optional {
  margin-left: 5px;
  font-weight: 400;
  letter-spacing: 0;
  text-transform: none;
  color: var(--text-faint);
}

.bug-dialog .username-input { margin: 0 0 14px; }

.bug-fallback {
  margin: 8px 0 0;
  font-size: 12px;
  line-height: 1.5;
  color: var(--text-dim);
}

.bug-fallback a, .bug-fallback .linkish { color: var(--gold-ink); }

/* Chess.com's one-time verification code. Monospaced and big enough to read off the screen while
   typing it into another tab, which is exactly what it is for. */
.cc-code-row { display: flex; align-items: center; gap: 8px; margin-bottom: 10px; }

.cc-code {
  flex: 1 1 auto;
  padding: 9px 11px;
  border-radius: 8px;
  background: var(--sunken);
  color: var(--gold-ink);
  font-family: ui-monospace, SFMono-Regular, Menlo, Consolas, monospace;
  font-size: 15px;
  font-weight: 700;
  letter-spacing: 0.08em;
  user-select: all;
}

.cc-steps {
  margin: 0 0 10px;
  font-size: 12.5px;
  line-height: 1.55;
  color: var(--text-dim);
}

.cc-steps strong { color: var(--text); }

/* A button small enough to sit at the end of a list row without being the loudest thing in it. */
.btn-tiny {
  flex: none;
  padding: 5px 11px;
  font-size: 11.5px;
  border-radius: 999px;
}

/* --- Upgrading ------------------------------------------------------------------------------ */
/*
   The Plus panel, at the foot of the dashboard.

   It sits last on purpose. The dashboard's job is the tactics above it, and a paid tier that
   interrupts that is an advert wearing the app's clothes. This was a link in the footer for
   exactly that reason, which turned out to be the other failure: nobody reads a footer.

   It is also a *band* rather than a card, and that is a height decision as much as a visual one.
   The rest of this page is sized to land on one screen, and an upgrade panel that pushes the last
   row of cards below the fold has taken something from the app to sell something. So: two columns,
   the points running in three short columns rather than a stacked list, and no more vertical space
   than the price and the button actually need.
*/
.upsell {
  display: grid;
  grid-template-columns: minmax(0, 1fr) minmax(190px, 230px);
  gap: 18px;
  padding: 12px 14px;
  /* Warmer than an ordinary card, and the only gold-edged thing on the page. One panel allowed to
     look different is a panel you notice; two would be a page that shouts. */
  border-color: color-mix(in srgb, var(--gold) 38%, var(--border));
  background:
    linear-gradient(135deg, color-mix(in srgb, var(--gold) 7%, transparent), transparent 62%),
    var(--surface);
}

.upsell[hidden] { display: none; }

/* Eyebrow and headline on one line: they are one sentence's worth of information and stacking them
   cost two line-heights for no gain. */
.upsell-copy { display: flex; flex-direction: column; gap: 5px; }

.upsell-eyebrow {
  margin: 0;
  font-size: 10px;
  font-weight: 700;
  letter-spacing: 0.18em;
  text-transform: uppercase;
  color: var(--gold-ink);
}

.upsell-title {
  margin: 0;
  font-size: 15px;
  line-height: 1.2;
  color: var(--text-bright);
}

.upsell-lede {
  margin: 0;
  max-width: 76ch;
  font-size: 12px;
  line-height: 1.45;
  color: var(--text-dim);
}

/* Three columns of two. Six one-line points stacked is six line-heights of panel; in threes it is
   two, and the list is still read down each column the way a list should be.

   The count is set outright rather than left to `auto-fit`, because six points only ever divide
   evenly by three, two and one, and `auto-fit` picks whatever the track floor allows, which at
   200px was four across a full-width dashboard: a row of four over a ragged row of two, in columns
   narrow enough that every point wrapped as well. Raising the floor only moved the problem, since
   the floor that forbids a fourth column at 1280 also forbids a second at 900. */
.upsell-points {
  margin: 1px 0 0;
  padding: 0;
  list-style: none;
  display: grid;
  grid-template-columns: repeat(3, minmax(0, 1fr));
  gap: 3px 18px;
  /* Take whatever height the copy column has left over, and share it out between the two rows
     rather than leaving it in a slab underneath.
     The buy column, price, small print, button, hook, is the taller of the two, and the panel's
     grid stretches both to match. Measured at 1600px wide: the points finished 35px above the
     bottom of their own column, so on a full-screen dashboard the list read as having stopped
     early rather than as filling the panel. `space-evenly` spreads that above, between and below,
     which comes out around 13px each; on a narrow screen there is no slack and nothing moves. */
  flex: 1 1 auto;
  align-content: space-evenly;
  font-size: 11.5px;
  line-height: 1.4;
  color: var(--text);
}

.upsell-points li {
  /* A hanging indent rather than a grid: the points are plain inline text and grid items would
     break any of them that ever gains an emphasis. */
  padding-left: 16px;
  text-indent: -16px;
}

.upsell-tick {
  display: inline-block;
  width: 16px;
  text-indent: 0;
  color: var(--gold-ink);
  font-weight: 700;
}

/* The ask, boxed off from the case for it. */
.upsell-buy {
  display: flex;
  flex-direction: column;
  align-items: stretch;
  justify-content: center;
  gap: 2px;
  padding: 10px 13px;
  border: 1px solid var(--border);
  border-radius: 10px;
  background: var(--surface-2);
  text-align: center;
}

.upsell-price {
  margin: 0;
  display: flex;
  align-items: baseline;
  justify-content: center;
  gap: 5px;
}

.upsell-amount {
  font-size: 26px;
  font-weight: 700;
  line-height: 1;
  color: var(--text-bright);
}

.upsell-per {
  font-size: 10.5px;
  letter-spacing: 0.06em;
  text-transform: uppercase;
  color: var(--text-faint);
}

/* The list price an offer is off. Pinned line thickness for the same reason as the pricing page's:
   the default is derived from the font size, and at 15px it draws a hairline that reads as a
   rendering fault rather than as a strikethrough. */
.upsell-was {
  font-size: 15px;
  line-height: 1;
  color: var(--text-faint);
  text-decoration-thickness: 2px;
}

.upsell-sale {
  margin: 3px 0 0;
  font-size: 10px;
  font-weight: 700;
  letter-spacing: 0.1em;
  text-transform: uppercase;
  color: var(--gold-ink);
}

.upsell-note { margin: 0 0 8px; font-size: 11px; color: var(--text-dim); }

.upsell-buy .btn { width: 100%; text-align: center; }

/* The one number that argues for this account rather than for the product. Empty until dashboard.js
   knows it, and the margin is on the filled state so an empty line costs nothing. */
.upsell-hook { margin: 0; font-size: 10.5px; line-height: 1.35; color: var(--text-faint); }
.upsell-hook:empty { display: none; }
.upsell-hook:not(:empty) { margin-top: 6px; }

/* Threes into twos while the price box is still taking its 230px out of the row, and into ones only
   once a column would be narrower than the longest point. Both are two rows of three or three rows
   of two, even either way, which is the whole reason the count is stated rather than fitted. */
@media (max-width: 1080px) {
  .upsell-points { grid-template-columns: repeat(2, minmax(0, 1fr)); }
}

@media (max-width: 780px) {
  .upsell { grid-template-columns: 1fr; }
}

@media (max-width: 560px) {
  .upsell-points { grid-template-columns: 1fr; }

  /* Centred once it is wrapping. `space-between` is right for one row with the marks at one end and
     the links at the other; across three ragged rows it reads as three things that failed to line
     up rather than as a footer. */
  .dash-foot,
  .dash-foot .foot-links,
  .dash-foot .foot-social { justify-content: center; }
  .dash-foot { text-align: center; }

  /* And the separators go, because a wrapped row puts one at the end of a line.
     Each `·` is its own flex item, so it breaks independently of the link it belongs to: with the
     row wrapping after "Terms", the dot that was introducing "Privacy" stayed behind on the line
     above, where it reads as a typo. There is no way to glue the two together without pairing them
     in the markup, and two of these links are added by dashboard.js rather than written there.
     A wider gap says the same thing and cannot land in the wrong place. */
  .dash-foot .foot-links { gap: 16px; }
  .dash-foot .foot-links > [aria-hidden="true"] { display: none; }
}

/* --- Locked on Free --------------------------------------------------------------------------- */
/*
   A control a free account can see but not use. Dimmed and not-allowed rather than removed: a
   feature you have never seen is one you cannot want, and a control that is present but locked says
   both what exists and what it would take to have it.

   Everything wearing this class also has a refusal behind it on the server, so unlocking one in dev
   tools gets you a 402 rather than the feature. */
.locked {
  opacity: 0.45;
  cursor: not-allowed;
  filter: grayscale(0.6);
}

.locked:hover { background: inherit; border-color: var(--border); }

/* The way out, under whatever is locked. Gold link, because it is the one link on the page that
   leads somewhere the app is asking you to go. Set small and tight: there are two of these in the
   settings card and they are an explanation, not content, on a page sized to one screen they have
   to cost about a line each. */
.plus-note {
  margin: 0;
  font-size: 11px;
  line-height: 1.35;
  color: var(--text-faint);
}
.plus-note a { color: var(--gold-ink); white-space: nowrap; }

/* The ring and its scope label as one cell of the statistics grid. */
.stat-figure { display: flex; flex-direction: column; align-items: center; }

/* "Last 30 days" under the ring, so a windowed figure is never read as a lifetime one. */
.stat-scope {
  margin-top: 6px;
  text-align: center;
  font-size: 10.5px;
  letter-spacing: 0.06em;
  text-transform: uppercase;
  color: var(--text-faint);
}

/* The owner's tier switch, in the footer with the other utilities. Scaffolding, it goes when
   billing lands, so it is styled as one of the footer's links and not as a control. */
/* The tier switch, which is a `<button>` wearing `.rail-item` so it sits in the rail's foot above
   Dev. Those three lines are what a button does not inherit from an anchor. */
.plan-switch {
  width: 100%;
  border: none;
  background: none;
  font-family: inherit;
  text-align: left;
  cursor: pointer;
  color: var(--gold-ink);
}

.plan-switch:disabled { opacity: 0.6; cursor: default; }

/* And the live count next to it, same treatment for the same reason. Kept as its own selector
   rather than added to the line above so that either can be picked out later without touching the
   other; they are two readouts that happen to look alike, not one thing in two places. */
.live-count { color: var(--text-dim); }

/* The way out of a locked control, sitting inside a line that already exists rather than adding
   one of its own. */
.plus-link {
  color: var(--gold-ink);
  white-space: nowrap;
  text-decoration: none;
  border-bottom: 1px solid color-mix(in srgb, var(--gold) 45%, transparent);
}

.plus-link:hover { border-bottom-color: var(--gold); }

/* In a `.pref-line`, which is a space-between row, the link goes hard right against the label. */
.pref-line .plus-link { margin-left: auto; font-size: 11px; }

/* The daily allowance in the Plus panel, once it has actually run out. Gold rather than red: it is
   a limit reached, not an error, and the day resets on its own. */
.upsell-hook.spent { color: var(--gold-ink); font-weight: 700; }

/* --- The record card -------------------------------------------------------------------------- */
/*
   Just the count and the way in. It briefly listed the last three solves, and that was a better card
   inside a worse row: the grid gives every cell the height of the tallest, so one card with a list
   in it stretched the two beside it to match. The list lives on `/history`, which is what the button
   is for.
*/
.record-body { display: flex; flex-direction: column; gap: 6px; }

/* Nothing renders into it at the moment, and an empty flex box inside a card that lays its children
   out with a gap is still a gap, a stray 5px between the lede and the primer panel. */
.record-body:empty { display: none; }

/* The primer tally, folded into the record card. Two cards became one: they read the same attempts,
   so the primers were a heading competing with the thing they are part of. Sunken and quiet,
   because it is the answer to a second question rather than the reason you looked at the card. */
.record-sub {
  display: grid;
  gap: 6px;
  margin-top: 8px;
  padding: 8px 9px 9px;
  border: 1px solid var(--border);
  border-radius: 9px;
  background: var(--sunken);
}

.sub-head {
  display: flex;
  align-items: baseline;
  justify-content: space-between;
  gap: 10px;
}

/* A reset nobody presses twice a year, sized accordingly. As a full-width button in the actions row
   it had the same weight as the way into the record, which made a destructive control look like the
   card's main offer. */
.sub-reset {
  font-size: 11px;
  color: var(--text-faint);
  text-decoration-color: color-mix(in srgb, var(--text-faint) 45%, transparent);
}

.sub-reset:hover:not(:disabled) { color: var(--gold-ink); text-decoration-color: var(--gold); }
.sub-reset:disabled { opacity: 0.45; cursor: default; text-decoration: none; }

.primer-facts { display: grid; gap: 6px; }

.sub-note {
  margin: 0;
  font-size: 11px;
  line-height: 1.4;
  color: var(--text-faint);
}

/* --- The hero count changing ------------------------------------------------------------------ */
/*
   The number re-filters live now, moving the difficulty slider or unticking a time control can take
   "82 tactics waiting" to 64 while you are looking at it. dashboard.js counts it across; this is the
   colour that says which way it went, for the moment it is going.

   A tint and a small lift rather than anything louder: it is feedback on a control you just touched,
   not an event. `tabular-nums` is already on `.hero-count`, so the digits do not jitter as they run.
*/
@keyframes count-rise {
  0%   { transform: translateY(3px); color: var(--good); }
  60%  { transform: translateY(-1px); color: var(--good); }
  100% { transform: none; color: var(--gold-ink); }
}

@keyframes count-fall {
  0%   { transform: translateY(-3px); color: var(--bad); }
  60%  { transform: translateY(1px); color: var(--bad); }
  100% { transform: none; color: var(--gold-ink); }
}

.hero-count.rising { animation: count-rise 620ms cubic-bezier(0.2, 0.8, 0.3, 1); }
.hero-count.falling { animation: count-fall 620ms cubic-bezier(0.2, 0.8, 0.3, 1); }

/* dashboard.js already skips the tween under reduced motion; this covers the tint, which is a pure
   CSS animation and would otherwise still run. */
@media (prefers-reduced-motion: reduce) {
  .hero-count.rising,
  .hero-count.falling { animation: none; }
}

/* --- The motif of the day --------------------------------------------------------------------- */
/*
   The position, and beside it whose move it is. It was a single line of text in a rail at the top of
   the page, which is the least a card can be: "White to play and win" is true of every tactic in the
   app, so the card said nothing that distinguished today from yesterday. A cell in the grid buys the
   board, and the board is the puzzle, not the answer, which stays unnamed until it is solved.
*/
/* Sized against the card, not the window. The card is a third of the grid on a desktop, a half at
   1080 and the whole width on a phone, and only the first of those follows from the viewport, a
   `@media` rule here would shrink the board at the exact widths where it had just been given more
   room. */
.daily-card { container-type: inline-size; }

/* Board on the left, everything else in a column on its right. `align-items: stretch` rather than
   `center` is what makes the column start level with the top of the board instead of floating in
   the middle of it, and it is also what lets the button reach the bottom edge, since a stretched
   column is as tall as the board and `margin-top: auto` inside it has somewhere to push to. */
.daily-body {
  display: grid;
  grid-template-columns: auto minmax(0, 1fr);
  align-items: stretch;
  gap: 12px;
  margin-top: 4px;
}

/* In the dashboard's 316px side column the board is the card, so it takes the whole width and the
   game and the button read underneath it. Two columns is right where the card is wide, which is
   every other place this card is drawn; here it left a 206px board with a column of text squeezed
   beside it, and the board is now the thing you are meant to use rather than look at. */
.train-side .daily-body { grid-template-columns: minmax(0, 1fr); }

/* The button finishes level with the bottom of the **board**, not the bottom of the card.
   This used to be `flex: 1 1 auto` on the row above, so the grid stretched to whatever height the
   card had and `margin-top: auto` pushed the button down with it. In a grid row whose height is set
   by a taller neighbour that put the button well below the board, floating under it with a gap in
   between, which is what it looked like: a picture with a button dropped underneath rather than one
   block. Pinning it to the board's own height costs the alignment between the three cards' buttons,
   and reads better, because the thing a button should line up with is the thing it is about. */
.daily-side { min-height: 9.5em; }

/* The board sits at the top of its own cell; without this the stretch above would pull the grid
   wrapper to the column's height and take the board's `aspect-ratio` with it. */
.daily-board { align-self: start; }

/* Read top to bottom: who played it, where and when, whose move, how the run is shaped, and the way
   in. The button is last and pinned to the foot of the column so it finishes level with the bottom
   of the board, left to flow, it stopped wherever the text happened to end and left a hole under
   itself. */
.daily-side {
  display: flex;
  flex-direction: column;
  gap: 5px;
  min-width: 0;
}

.daily-side .dash-actions {
  margin-top: auto;
  padding-top: 6px;
}

/* Who was playing, and where and when. Both halves come from the same two fields the trainer reveals
   after a solve, and they keep that reveal's two stacked lines here: the column beside the board is
   a fraction of a third of the grid, and the two set inline on one line wrap through the middle of
   whichever half is longer. Stacked, each gets a line of its own to be too long in.

   The sizes below all went up a point when the board was sized off the card. They had been set a
   notch under the trainer's own reward panel because this column was the cramped half of a card
   that was mostly empty underneath; it is not cramped, and type held small to fit a space that was
   never short was the other half of why the card looked unfilled. */
.daily-source {
  margin: 0;
  font-size: 13px;
  line-height: 1.35;
}

/* Empty until the fetch lands, and an empty paragraph in a flex column with a gap is still a gap. */
.daily-source:empty { display: none; }
.daily-meta:empty { display: none; }

/* style.css already stacks these two and colours them, so only the sizes are set here, a caption at
   the top of a card, a notch down from the same names as the reward after a solve. */
.daily-source .daily-game { font-size: 13.5px; line-height: 1.3; }
.daily-source .daily-event { font-size: 12.5px; line-height: 1.3; }

/* Sized as a **fraction of the card**, which is what `container-type: inline-size` on `.daily-card`
   was declared for. Eight columns and `aspect-ratio` do the rest, the squares are always square,
   whatever width the cell ends up at.

   It was a flat `9.5em` with a container query knocking it down to `7.6em` in a narrow card, and the
   two together left the card visibly unfilled: the row's height is set by the openings and rush
   cards beside it, and this card's content needed 191px of the 249px it was given, so there were
   58px of nothing under the board at *every* width. A board sized off the card takes that space
   instead of leaving it, and it takes it smoothly rather than in one step at 300px, so the step
   rule below is gone with it.

   The bounds are what stop it going wrong at the two ends. `49cqi` leaves slightly over half the
   card for the text, which is the balance that keeps "Botvinnik vs Capablanca" on two lines rather
   than eight characters a line; the floor is the old narrow-card size, because below that the board
   stops being a position and becomes a texture; and the ceiling stops a wide card handing the board
   so much height that this card becomes the tallest in the row and makes the whole page longer. */
.mini-board {
  width: clamp(7.6em, 49cqi, 12.5em);
  max-width: 42vw;
  aspect-ratio: 1;
  display: grid;
  grid-template-columns: repeat(8, 1fr);
  border-radius: 7px;
  overflow: hidden;
  box-shadow: 0 1px 3px rgb(0 0 0 / 0.28), inset 0 0 0 1px color-mix(in srgb, var(--gold) 22%, transparent);
}

/* The same two colours the board editor uses, so a position looks the same everywhere in the app. */
.mini-sq {
  background-size: cover;
  background-repeat: no-repeat;
  background-position: center;
}

.mini-sq.light { background-color: var(--sq-light, #f0d9b5); }
.mini-sq.dark { background-color: var(--sq-dark, #b58863); }

/* The middle block of the right-hand column: whose move, how the run is shaped, and whether it is
   already done. Its own flex column inside `.daily-side` because dashboard.js rebuilds exactly this
   much on each render and the source line and the button above and below it are not its business. */
.daily-meta {
  display: flex;
  flex-direction: column;
  gap: 5px;
  min-width: 0;
}

.daily-turn {
  display: flex;
  align-items: center;
  gap: 7px;
  margin: 0;
  font-size: 13.5px;
  font-weight: 600;
  line-height: 1.35;
  color: var(--text);
}

/* Whose move, as the colour of the piece that has it. Next to a board this is read before any of
   the words are. */
.daily-disc {
  flex: none;
  width: 12px;
  height: 12px;
  border-radius: 50%;
  border: 1px solid color-mix(in srgb, var(--text-faint) 70%, transparent);
}

.daily-disc.white { background: #f2efe6; }
.daily-disc.black { background: #23201b; }

/* The motif-of-the-day card's supporting line. It says how the run is shaped and nothing about which
   game it is, naming that here would answer the puzzle from the dashboard. */
.daily-line { margin: 0; font-size: 12.5px; line-height: 1.45; color: var(--text-dim); }

/* Done, not gone: the position stays open all day. A tick and two words, in the good colour, rather
   than the card switching itself off. */
.daily-done {
  display: flex;
  align-items: center;
  gap: 5px;
  margin: 2px 0 0;
  font-size: 12.5px;
  font-weight: 700;
  letter-spacing: 0.02em;
  color: var(--good);
}

/* In a narrower card the text next to the board drops towards eight characters a line, which is a
   column of broken words beside a picture. The board gives up the width first: it is legible a long
   way down, and the sentence is not.

   The width itself is no longer set here. `.mini-board` is a fraction of the card now, so it has
   already given the width up by the time this rule matches, and a step down on top of that took the
   board to 7.6em in a card that had 58px of unused height under it. What is left is the gap, which
   does not scale with anything and is worth closing by hand. */
@container (max-width: 300px) {
  .daily-body { gap: 10px; }
}

/* Narrower still and there is no side-by-side left to have, so they stack and the board takes the
   room it is given. The button loses its pin to the foot of the column at the same time: with the
   column below the board rather than beside it there is no bottom edge to line up with, and
   `margin-top: auto` in a stack that is only as tall as its content does nothing anyway. */
@container (max-width: 250px) {
  .daily-body { grid-template-columns: 1fr; justify-items: center; }
  .daily-board { align-self: auto; }
  .mini-board { width: 11em; max-width: 100%; }
  .daily-side { align-items: center; text-align: center; }
  .daily-side .dash-actions { margin-top: 4px; }
  .daily-meta { align-items: center; text-align: center; }
}

/* --- The account page --------------------------------------------------------------------------
 *
 * Its own small block rather than reuse of the dashboard's grid: the dashboard is a grid of cards
 * you scan, and this is a column of statements you read. The cards themselves are the dashboard's,
 * so the two pages still look like one product.
 */

/* Narrow, and centred by `.dash`'s own margin. Long measure is the enemy on a page that is mostly
   sentences, and this page has no wide content at all, no board, no table, no chart. */
.dash-narrow { width: min(640px, 100%); }

/* Label above value, not beside it. A two-column definition list has to pick a label width, and the
   longest label here ("Chess accounts") would set a gutter that makes every short one look lost. */
.account-facts {
  margin: 8px 0 0;
  display: grid;
  gap: 9px;
}

.account-facts dt {
  font-size: 11px;
  font-weight: 700;
  letter-spacing: 0.06em;
  text-transform: uppercase;
  color: var(--text-faint);
}

.account-facts dd {
  margin: 2px 0 0;
  font-size: 13.5px;
  line-height: 1.45;
  /* Long, unbroken and not chosen by us: an email address from a provider can be longer than this
     card is wide, and the alternative to breaking it is a horizontal scrollbar on the page. */
  overflow-wrap: anywhere;
}

.account-fact-hint {
  display: block;
  margin-top: 2px;
  font-size: 11.5px;
  color: var(--text-faint);
}

.account-actions {
  display: flex;
  flex-wrap: wrap;
  gap: 8px;
  margin-top: 10px;
}

.account-note {
  margin: 8px 0 0;
  font-size: 11.5px;
  line-height: 1.45;
  color: var(--text-dim);
}

.account-error {
  margin: 10px 0 0;
  padding: 8px 10px;
  border-radius: 6px;
  font-size: 12.5px;
  line-height: 1.45;
  background: color-mix(in srgb, var(--bad) 14%, transparent);
  color: var(--bad);
}

/* The one card on the site that offers to destroy something. It is marked, but quietly: a card
   shouting in red from the moment the page loads makes the page feel dangerous to visit, when the
   danger is entirely in one button two clicks away. The tint carries the meaning; the button
   carries the weight. */
.danger-card { border-color: color-mix(in srgb, var(--bad) 32%, var(--border)); }

.danger-card::before {
  background: linear-gradient(90deg,
    color-mix(in srgb, var(--bad) 45%, transparent) 0%,
    color-mix(in srgb, var(--bad) 10%, transparent) 45%,
    transparent 80%);
}

.card-icon-danger {
  background: color-mix(in srgb, var(--bad) 13%, var(--surface-2));
  color: var(--bad);
  box-shadow: inset 0 0 0 1px color-mix(in srgb, var(--bad) 22%, transparent);
}

/* Not gold. Gold is spent on the buttons that start something, and this one ends something, a
   destructive action wearing the same colour as "Start training" is a misprint. */
.btn-danger {
  background: var(--bad);
  color: #fff;
}

.btn-danger:hover { background: color-mix(in srgb, var(--bad) 82%, #000); }

/* Disabled means the typed name does not match yet. It has to *look* unavailable, or the button
   reads as broken rather than as waiting for something. */
.btn-danger:disabled {
  background: color-mix(in srgb, var(--bad) 30%, var(--surface-2));
  color: var(--text-faint);
  cursor: not-allowed;
}

.delete-confirm {
  margin-top: 10px;
  padding-top: 10px;
  border-top: 1px solid color-mix(in srgb, var(--bad) 22%, var(--border));
}

.delete-confirm .field { margin-bottom: 0; }

/* The password card. The fields are `.field` from the auth forms, so this only has to stop them
   stretching to the full width of a card that is much wider than a login box. */
.password-form { margin-top: 10px; }

.password-form .field { max-width: 340px; }

.password-form .field:last-of-type { margin-bottom: 0; }

/* The counterpart to .account-error, and deliberately the same shape: a confirmation that looks
   nothing like the failure it replaces makes the form feel like two different forms. */
.account-ok {
  margin: 10px 0 0;
  padding: 8px 10px;
  border-radius: 6px;
  font-size: 12.5px;
  line-height: 1.45;
  background: color-mix(in srgb, var(--good) 14%, transparent);
  color: var(--good);
}

/* --- Touch --------------------------------------------------------------------------------------
 *
 * `ui.css` is on all eleven pages, which is why this lives here and not in each of them.
 *
 * **`touch-action: manipulation` is the fix for "the buttons are not very responsive".** Safari on
 * iOS still waits about 350ms after a tap before it dispatches `click`, because until that timer
 * expires the tap might turn out to be the first half of a double-tap-to-zoom. The page is
 * zoomable, `width=device-width, initial-scale=1.0` with no `user-scalable=no`, and it should stay
 * zoomable, so the delay cannot be removed by making the page refuse to zoom. Declaring
 * `manipulation` on the controls themselves opts each of them out of double-tap zoom and nothing
 * else: panning and pinch still work, on the control and on the page around it.
 *
 * That delay is also the likeliest half of "sometimes I need to press Next primer twice". A button
 * that does nothing for a third of a second gets pressed again, and the second press lands inside
 * the double-tap window of the first.
 *
 * Deliberately on the controls and not on `body`. A blanket `touch-action: manipulation` would take
 * double-tap zoom off the whole page, which is how some people read it. */
a,
button,
label,
summary,
select,
input,
textarea,
[role="button"],
.btn,
.linkish {
  touch-action: manipulation;
}

/* A 44px minimum, on touch only.
 *
 * `.btn` is `padding: 10px 14px` on a 14px font, which computes to about 37px tall, and `.btn-small`
 * to about 32px. Both are comfortable under a cursor and both are under Apple's 44pt minimum for a
 * finger. Guarded by `pointer: coarse` so the desktop layout is untouched: these buttons sit in
 * rows where 44px everywhere would be loud.
 *
 * `.btn-provider` is the one exception, and it has to be. It is drawn to match Google's button,
 * Google's button is 40px at `size: 'large'` and there is no taller size to ask for, so a 44px
 * minimum here made ours four pixels taller than the one under it, on a phone only. Two buttons
 * offering the same thing, stacked, at different heights: which is exactly the thing the rest of
 * `.btn-provider` exists to avoid. 40px full-width is a comfortable target regardless. */
@media (pointer: coarse) {
  .btn:not(.btn-provider),
  .btn-small {
    min-height: 44px;
  }
}

