/* words — phone first, because a 5-wide grid and a 10-wide keyboard are only
 * hard on a phone.
 *
 * TWO THEMES, AND LIGHT IS THE BASE, matching budget, pattern and mines. The
 * palette below is the light one on bare :root; dark is a token swap in two
 * guarded blocks — the media query for "system", the attribute for a choice the
 * user made. Nothing outside those blocks names a literal colour, so a rule
 * written once is right in both. The duplication between the two dark blocks is
 * unavoidable: a media query cannot be OR-ed with an attribute selector, and
 * defining a colour in only one of them is how a theme ends up half-applied.
 *
 * THE THREE MARK COLOURS ARE THE ONLY PART OF THE PALETTE THAT CANNOT MOVE.
 * They are the entire information content of the screen, they must survive both
 * themes, and green-against-yellow is the single worst pair for the commonest
 * colour blindness there is — so they are separated by LIGHTNESS as well as
 * hue, so they stay apart in greyscale too. See --mark-* below.
 *
 * Insets go on PADDING, never on layout, and every one carries a 0px fallback —
 * `calc(6px + env(...))` with no fallback is an invalid length and drops the
 * whole declaration on every desktop browser.
 */

:root {
  --bg:       #f2f2f7;
  --panel:    #ffffff;
  --panel-2:  #f6f6f9;
  --line:     rgba(60, 60, 67, .16);
  --line-2:   rgba(60, 60, 67, .08);
  --ink:      #000000;
  --ink-2:    #3c3c43;
  --dim:      #8a8a8e;
  --dim-2:    #b0b0b6;
  --pill:     #efeff0;
  --good:     #1a9e4b;
  --warn:     #b8730a;
  --bad:      #e5322b;
  --link:     #007aff;
  --shadow:   0 1px 2px rgba(0,0,0,.04), 0 6px 20px rgba(0,0,0,.05);
  --heart:    #e0596e;   /* the tip jar's one colour; the same in both themes, in every app */

  /* THE MARKS.
     Green and amber, not green and yellow: a mid yellow on white is nearly
     invisible and, next to green, is the pair deuteranopes cannot separate at
     all. These two differ by about 18 points of lightness as well as hue, so
     they stay distinguishable in greyscale — which is also what a screenshot
     pasted into a chat becomes for a lot of people. */
  --mark-g:   #2e8b46;
  --mark-y:   #c99000;
  --mark-x:   #9a9aa0;
  --mark-ink: #ffffff;

  --tile:       #ffffff;
  --tile-edge:  rgba(60,60,67,.20);
  --tile-fill:  rgba(60,60,67,.42);   /* a typed but unsubmitted letter */

  --key:        #d7d9de;
  --key-ink:    #000000;
  --key-wide:   #b9bcc4;
  /* A LETTER THE WORD DOES NOT HAVE, on the keyboard. NOT --mark-x: the tile's
     grey has to be one of three marks beside green and amber; the key's grey has
     to be told from an UNUSED key at a glance, which is a different job with a
     different neighbour. In light this is --mark-x at the 75% the rule used to
     apply, baked in, so nothing moved; in dark the two tokens part company —
     see the dark blocks. */
  --key-x:      #b0b0b6;
  --key-x-ink:  #ffffff;

  --r-card: 20px;
  --r-mid:  13px;
  --r-tile: 6px;

  --font: -apple-system, BlinkMacSystemFont, system-ui, "SF Pro Text",
          "Helvetica Neue", sans-serif;
  --font-num: ui-rounded, -apple-system, "SF Pro Rounded", system-ui, sans-serif;
  --font-mono: ui-monospace, SFMono-Regular, "SF Mono", Menlo, monospace;
}

@media (prefers-color-scheme: dark) {
  :root:not([data-theme="light"]) {
    --bg:       #000000;
    --panel:    #1c1c1e;
    --panel-2:  #2c2c2e;
    --line:     rgba(84, 84, 88, .5);
    --line-2:   rgba(84, 84, 88, .28);
    --ink:      #ffffff;
    --ink-2:    #ebebf5;
    --dim:      #8e8e93;
    --dim-2:    #636366;
    --pill:     #2c2c2e;
    --good:     #30d158;
    --warn:     #ffd60a;
    --bad:      #ff453a;
    --link:     #0a84ff;
    --shadow:   0 1px 2px rgba(0,0,0,.5), 0 6px 20px rgba(0,0,0,.4);

    --mark-g:   #479e5c;
    --mark-y:   #c8a02e;
    --mark-x:   #4a4a50;
    --mark-ink: #ffffff;

    --tile:       #000000;
    --tile-edge:  rgba(255,255,255,.22);
    --tile-fill:  rgba(255,255,255,.45);

    /* THE DARK KEYBOARD, REDRAWN 2026-09-12. An unused key was #3a3a3f and a
       spent one was --mark-x #4a4a50 at 75% over black — about #38383c. The
       same shade, which on the phone meant the letters that had been ruled OUT
       could not be told from the ones still in play. The fix is the one every
       dark keyboard uses: the unused keys come UP to a mid grey and the spent
       ones go DOWN to nearly the background, with the letter dimmed but legible
       — so the gap between the two is the whole of the available range rather
       than a sixteenth of it. Light did not have the problem and is unchanged. */
    --key:        #5a5c63;
    --key-ink:    #ffffff;
    --key-wide:   #6f727a;
    --key-x:      #1f1f22;
    --key-x-ink:  #8e8e93;
  }
}
/* NATIVE CONTROLS FOLLOW THE APP'S CHOICE, NOT ONLY THE PHONE'S (2026-09-26,
   ~/claude/IOS.md §11). Switches, scrollbars and the page's own canvas read
   `color-scheme`; without it, Dark chosen in Settings on a light phone drew
   light switches on a black page. theme.js repaints the status bar to match. */
:root { color-scheme: light dark; }
:root[data-theme="light"] { color-scheme: light; }
:root[data-theme="dark"]  { color-scheme: dark; }

:root[data-theme="dark"] {
  --bg:       #000000;
  --panel:    #1c1c1e;
  --panel-2:  #2c2c2e;
  --line:     rgba(84, 84, 88, .5);
  --line-2:   rgba(84, 84, 88, .28);
  --ink:      #ffffff;
  --ink-2:    #ebebf5;
  --dim:      #8e8e93;
  --dim-2:    #636366;
  --pill:     #2c2c2e;
  --good:     #30d158;
  --warn:     #ffd60a;
  --bad:      #ff453a;
  --link:     #0a84ff;
  --shadow:   0 1px 2px rgba(0,0,0,.5), 0 6px 20px rgba(0,0,0,.4);

  --mark-g:   #479e5c;
  --mark-y:   #c8a02e;
  --mark-x:   #4a4a50;
  --mark-ink: #ffffff;

  --tile:       #000000;
  --tile-edge:  rgba(255,255,255,.22);
  --tile-fill:  rgba(255,255,255,.45);

  /* Same values as the media block above; see the note there. */
  --key:        #5a5c63;
  --key-ink:    #ffffff;
  --key-wide:   #6f727a;
  --key-x:      #1f1f22;
  --key-x-ink:  #8e8e93;
}

* { box-sizing: border-box; }

/* `[hidden]` MUST WIN, AND BY DEFAULT IT DOES NOT. The browser's own
 * `[hidden] { display: none }` lives in the UA stylesheet, so any author rule
 * setting `display` on the same element beats it outright — no `!important`,
 * no specificity war, just author-over-UA. In `mines` both
 * `.controls { display: flex }` and `.btn-wide { display: block }` did, so
 * `el.hidden = true` did nothing at all and the element sat there in plain
 * sight while the PROPERTY read true — which is why the check that asserted on
 * `el.hidden` agreed with the code and disagreed with the screen. The
 * `.something[hidden]` patches further down this file are each one day
 * somebody hit it without naming it. */
[hidden] { display: none !important; }

/* THE DOCUMENT SCROLLS; THE PLAY SCREEN HOLDS STILL (2026-09-26,
   ~/claude/IOS.md §15, letters' shape). Until words-v16 the document was
   exactly one viewport tall and locked (`height: 100%; overflow: hidden`),
   with every view scrolling inside itself. On an installed iPhone a locked
   100%-tall root is laid out a status bar SHORTER than the screen, so the
   tab bar and the keyboard sat about 60pt above the bottom edge with the
   page's colour in the band — bubbles spent seven versions finding that.
   Only in-flow content of a document that scrolls itself reaches the
   bottom. So Leaderboard and Settings are in-flow content and the page is
   their one scroller, and Play, which is laid out to fit and must never be
   dragged (a keyboard you can drag eats keystrokes), locks the page while
   it shows (`html.in-game`, set by showTab()).

   The 2026-09-13 problem the old shape solved — Play a document 50px taller
   than the phone, so the header slid under the glass on a drag — is solved
   by the lock instead: in play nothing scrolls at all. */
html, body {
  margin: 0;
  min-height: 100%;
  font-family: var(--font);
  color: var(--ink);
  background-color: var(--bg);
  -webkit-font-smoothing: antialiased;
}
html.in-game, html.in-game body { height: 100%; min-height: 0; overflow: hidden; overscroll-behavior: none; }

/* No text selection and no iOS long-press callout anywhere in the app: a
   long-press on a key pops the magnifier and eats the keystroke. Inputs opt
   back in. */
body {
  -webkit-user-select: none;
  user-select: none;
  -webkit-touch-callout: none;
  -webkit-tap-highlight-color: transparent;
  touch-action: manipulation;
}
input, textarea { -webkit-user-select: text; user-select: text; }

/* ── the glass at the top of the screen ────────────────────────────────────

   iOS 26 blurs the top edge of a full-screen web app against the status bar:
   content does not stop under the bar, it goes SOFT beneath it, and anything in
   that band is unreadable rather than hidden.

   `env(safe-area-inset-top)` does not describe it. That inset is the status
   bar's own height — it is what keeps content out from UNDER the bar, and it is
   already applied everywhere below. The blur reaches past it, so the two are
   different measurements and both are needed. Adding them is the same fix
   `budget` made, and 20px is the same number: about one line of small text,
   which is about what the effect eats.

   It costs a browser tab nothing — 20px of background at the top of a scrolling
   page reads as margin — so it is not gated on display-mode. A media query that
   silently fails to match is worse than a margin nobody notices, and this house
   has been bitten by exactly that shape of thing before. */
:root { --glass-t: 20px; }

/* ── shell ─────────────────────────────────────────────────────────────── */

/* The tab bar is fixed to the foot, so a page that scrolls pads for it once,
   here, from the one number. */
:root { --tabs-h: 56px; }
main { padding-bottom: calc(var(--tabs-h) + env(safe-area-inset-bottom, 0px)); }
main[hidden] { display: none; }
html.in-game main { padding-bottom: 0; }

.view {
  width: 100%; max-width: 560px;
  margin: 0 auto;
  padding: calc(8px + var(--glass-t) + env(safe-area-inset-top, 0px)) 16px 24px;
}
.view[hidden] { display: none; }

/* THE PLAY SCREEN IS A COLUMN THAT FITS, so it never scrolls. Header and
   keyboard at their natural height, `.board-area` takes whatever is left and
   game.js sizes the tiles into it. `overflow: hidden` rather than `auto` on
   purpose: a keyboard you can drag is a keyboard that eats keystrokes, and if
   the column ever did not fit the answer is a smaller tile, which fit() already
   does, not a scrollbar. The bottom padding is what keeps the ENTER row off the
   tab bar's edge; there is no other spacer under the keyboard. */
/* 100dvh, not 100vh: in Safari `vh` is the LARGE viewport, the one with the
   address bar collapsed, so a 100vh column in a browser tab is taller than the
   screen by exactly the bar. Installed, the two are the same number. The
   bottom padding clears the fixed tab bar and keeps the ENTER row off its
   edge. */
#view-play {
  height: 100dvh;
  display: flex; flex-direction: column;
  overflow: hidden;
  padding-bottom: calc(10px + var(--tabs-h) + env(safe-area-inset-bottom, 0px));
}
#view-play > * { flex: none; }
#view-play > .board-area {
  flex: 1 1 0; min-height: 0;
  position: relative;
  display: grid; place-items: center;
}

.view-head { padding: 12px 4px 18px; }
.view-head h1 { margin: 0; font-size: 34px; font-weight: 700; letter-spacing: -.6px; }
.view-sub { margin: 4px 0 0; color: var(--dim); font-size: 15px; }

h2.sec {
  margin: 28px 4px 8px;
  font-size: 13px; font-weight: 600; text-transform: uppercase;
  letter-spacing: .5px; color: var(--dim);
}

.card {
  background: var(--panel);
  border-radius: var(--r-card);
  box-shadow: var(--shadow);
  overflow: hidden;
}

.fineprint { margin: 14px 6px 0; color: var(--dim); font-size: 12.5px; line-height: 1.55; }
.fineprint strong { color: var(--ink-2); font-weight: 600; }

/* ── buttons ───────────────────────────────────────────────────────────── */

.btn {
  font: inherit; font-size: 16px; font-weight: 600;
  padding: 13px 18px;
  border: 0; border-radius: var(--r-mid);
  background: var(--pill); color: var(--ink);
  cursor: pointer;
}
.btn:active { opacity: .7; }
.btn:disabled { opacity: .45; cursor: default; }
.btn-primary { background: var(--link); color: #fff; }
.btn-quiet   { background: var(--pill); color: var(--ink-2); font-weight: 500; }
.btn-wide    { display: block; width: 100%; margin-top: 18px; }
.btn-small   { font-size: 13.5px; padding: 8px 14px; border-radius: 10px; }

.icon-btn {
  font: inherit; font-size: 22px; line-height: 1;
  width: 40px; height: 40px;
  display: grid; place-items: center;
  border: 0; border-radius: 12px;
  background: transparent; color: var(--link);
  cursor: pointer;
}
.icon-btn:active { background: var(--pill); }

/* ── the gate ──────────────────────────────────────────────────────────── */

/* `min-height`, and the document scrolls it (IOS.md §15): a gate taller than
   a landscape phone keeps its button reachable. */
.gate { min-height: 100dvh; display: grid; place-items: center; padding: 24px; }
.gate[hidden] { display: none; }
.gate-card { max-width: 340px; text-align: center; }
.gate-mark { display: flex; gap: 5px; justify-content: center; margin-bottom: 14px; }
.gate-mark span {
  width: 34px; height: 34px; border-radius: var(--r-tile);
  display: grid; place-items: center;
  font-weight: 700; font-size: 19px; color: var(--mark-ink);
}
.gate-card h1 { margin: 8px 0 6px; font-size: 32px; letter-spacing: -.5px; }
.gate-sub { margin: 0 0 24px; color: var(--dim); font-size: 15px; line-height: 1.5; }
.gate-note { margin-top: 16px; color: var(--dim); font-size: 12.5px; line-height: 1.5; }

/* ── the board of tiles ────────────────────────────────────────────────── */

/* NO SAFE-AREA INSET AND NO GLASS OFFSET HERE — both are on `.view`, which
   wraps this. They were on both, so a notched phone was pushed down by the
   status bar TWICE: 8 + inset from the section and 4 + inset again from this
   header, which on an iPhone is about 120px of nothing above the title. Adding
   the glass offset to both would have doubled that too, which is how the
   duplicate got noticed at all. One screen, one measurement, in the outer
   element that every screen shares. */
.play-head {
  display: flex; align-items: center; gap: 10px;
  padding: 4px 4px 6px;
}
.play-title { font-size: 19px; font-weight: 700; letter-spacing: -.3px; }
.play-sub { font-size: 11.5px; color: var(--dim); margin-top: 1px; }
.play-head .spacer { margin-left: auto; }

/* ── the sound toggle in Play's header (CLAUDE.md 5e; words-v29, letters'
   .snd-g): filled body, stroked waves, currentColor in the header's ink. Off
   is the same speaker crossed out and a shade dimmer. */
.icon-btn {
  font: inherit; line-height: 1;
  width: 40px; height: 40px; flex: none;
  display: grid; place-items: center;
  border: 0; border-radius: 12px; margin: -4px -4px -4px 0;
  background: transparent; color: var(--ink);
  cursor: pointer;
}
.icon-btn:active { background: var(--pill); }
.snd-g {
  width: 23px; height: 23px; display: block;
  fill: currentColor; stroke: currentColor;
  stroke-width: 1.7; stroke-linecap: round; stroke-linejoin: round;
  paint-order: stroke fill;
}
.snd-g .ol { fill: none; }
.sound-btn[aria-pressed="true"] .snd-off,
.sound-btn[aria-pressed="false"] .snd-on { display: none; }
.sound-btn[aria-pressed="false"] { color: var(--dim); }
.sound-btn[hidden] { display: none; }

.tiles {
  display: grid;
  grid-template-columns: repeat(5, var(--tile-size, 62px));
  grid-auto-rows: var(--tile-size, 62px);
  gap: 5px;
  justify-content: center;
  margin: 0 auto;
}

.tile {
  border-radius: var(--r-tile);
  display: grid; place-items: center;
  background: var(--tile);
  box-shadow: inset 0 0 0 2px var(--tile-edge);
  font-family: var(--font-num);
  font-weight: 700;
  font-size: calc(var(--tile-size, 62px) * .52);
  line-height: 1;
  text-transform: uppercase;
  color: var(--ink);
}
/* A letter that has been typed but not submitted: the border firms up so the
   row reads as "in progress" without claiming anything about the letter. */
.tile.filled { box-shadow: inset 0 0 0 2px var(--tile-fill); animation: pop .1s ease-out; }
@keyframes pop { from { transform: scale(.9); } to { transform: scale(1); } }

.tile.g { background: var(--mark-g); box-shadow: none; color: var(--mark-ink); }
.tile.y { background: var(--mark-y); box-shadow: none; color: var(--mark-ink); }
.tile.x { background: var(--mark-x); box-shadow: none; color: var(--mark-ink); }

/* THE FLIP. Staggered per column by an inline --i, so a row reveals left to
   right the way it does in every version of this game — the delay is what makes
   the last tile of a winning row land like a full stop. The mark class is
   applied at the halfway point by game.js, not by the animation, so a browser
   that refuses the animation still ends up with the right colour. */
.tile.flip { animation: flip .5s ease forwards; animation-delay: calc(var(--i, 0) * 260ms); }
@keyframes flip {
  0%   { transform: rotateX(0deg); }
  50%  { transform: rotateX(90deg); }
  100% { transform: rotateX(0deg); }
}

.tiles.shake { animation: shake .5s; }
@keyframes shake {
  10%, 90% { transform: translateX(-2px); }
  20%, 80% { transform: translateX(4px); }
  30%, 50%, 70% { transform: translateX(-7px); }
  40%, 60% { transform: translateX(7px); }
}
.row-win .tile { animation: bounce .55s ease; animation-delay: calc(var(--i, 0) * 90ms); }
@keyframes bounce {
  0%, 100% { transform: translateY(0); }
  40%      { transform: translateY(-16px); }
  60%      { transform: translateY(3px); }
}

/* ── the message line ──────────────────────────────────────────────────── */

/* Floats over the top of the board area, reserving nothing — see the note in
   index.html. `pointer-events: none` so a tile under it still gets the tap;
   `z-index` so it draws over a row that is mid-flip (a transform makes a
   stacking context, and without this the pill went BEHIND the tile animating
   under it). */
.say {
  position: absolute; top: 6px; left: 0; right: 0;
  margin: 0;
  display: flex; align-items: center; justify-content: center;
  pointer-events: none;
  z-index: 2;
}
.say span {
  padding: 7px 14px; border-radius: 999px;
  background: var(--ink); color: var(--bg);
  font-size: 13.5px; font-weight: 600;
  opacity: 0; transform: translateY(-4px);
  transition: opacity .15s ease, transform .15s ease;
}
.say.on span { opacity: 1; transform: none; }

/* ── the keyboard ──────────────────────────────────────────────────────── */

/* A 10-key row on a 390pt phone puts a key at about 30pt — well under Apple's
   44pt minimum, exactly as mines' 12-wide grid was. The answer is the same one:
   keyboard.js resolves a touch to the NEAREST KEY CENTRE rather than to what is
   under the finger, so the 6px gutters belong to somebody, and it commits on
   LIFT so a finger that lands wrong can slide to the right key and watch itself
   do it. The pressed key follows the finger. */
.kb {
  margin: 12px auto 0;
  width: 100%; max-width: 500px;
  display: flex; flex-direction: column; gap: 7px;
  touch-action: none;         /* the keyboard owns every gesture inside it */
}
/* `display: flex` above beats the [hidden] attribute's default `display: none`,
   so app.js setting `el.kb.hidden = true` at the end of a game would do exactly
   nothing without this line. Same trap `.view[hidden]` covers above; it is the
   commonest way a hidden element stays on screen. */
.kb[hidden] { display: none; }
.kb-row { display: flex; gap: 6px; justify-content: center; }
.kb-row.pad { padding: 0 calc(var(--key-w, 32px) / 2 + 3px); }

.key {
  flex: 1 1 0;
  min-width: 0;
  height: 54px;
  padding: 0;                 /* a button's default is ~6px a side — a quarter of a 46pt key */
  border: 0; border-radius: 6px;
  background: var(--key); color: var(--key-ink);
  font: inherit; font-size: 15px; font-weight: 600;
  text-transform: uppercase;
  display: grid; place-items: center;
  cursor: pointer;
  transition: background .12s ease, transform .08s ease;
}
/* ENTER is five letters on a key about 46pt wide on the phone this is for. At
   12px with tracking it ran edge to edge and read as cramped; 11px with none
   leaves a margin either side. DEL is a glyph and gets its own size. */
.key.wide { flex: 1.6 1 0; font-size: 11px; letter-spacing: 0; background: var(--key-wide); }
.key[data-key="DEL"] { font-size: 18px; }
.key.g { background: var(--mark-g); color: var(--mark-ink); }
.key.y { background: var(--mark-y); color: var(--mark-ink); }
.key.x { background: var(--key-x); color: var(--key-x-ink); }
/* The press preview, which is the whole reason a 30pt key is usable: you can
   see which key you are about to commit to while there is still time to move. */
.key.press { transform: scale(.94); filter: brightness(.86); }
.kb.busy { opacity: .55; pointer-events: none; }

/* ── stats ─────────────────────────────────────────────────────────────── */

.stat-row { display: flex; justify-content: space-around; padding: 16px 8px; }
.stat { display: flex; flex-direction: column; align-items: center; gap: 2px; }
.stat .v { font-family: var(--font-num); font-size: 26px; font-weight: 700; font-variant-numeric: tabular-nums; }
.stat .k { font-size: 10.5px; color: var(--dim); text-transform: uppercase; letter-spacing: .4px; text-align: center; }

.dist { padding: 4px 16px 18px; }
.dist-row { display: flex; align-items: center; gap: 8px; margin-top: 5px; font-size: 13px; }
.dist-n { width: 10px; color: var(--dim); font-family: var(--font-num); font-weight: 600; }
.dist-bar {
  background: var(--mark-x); color: var(--mark-ink);
  border-radius: 4px; padding: 2px 7px;
  font-family: var(--font-num); font-weight: 700; font-size: 12px;
  min-width: 26px; text-align: right;
  transition: width .3s ease;
}
.dist-bar.here { background: var(--mark-g); }
.dist-lost { margin-top: 12px; padding-top: 10px; border-top: 1px solid var(--line-2); font-size: 12.5px; color: var(--dim); }

.countdown {
  display: flex; align-items: baseline; justify-content: center; gap: 8px;
  padding: 16px; font-size: 13px; color: var(--dim);
}
.countdown b { font-family: var(--font-num); font-size: 22px; font-variant-numeric: tabular-nums; color: var(--ink); }

.share-grid { display: flex; flex-direction: column; gap: 3px; align-items: center; padding: 4px 0 14px; }
.share-grid .board-mini i { width: 15px; height: 15px; border-radius: 3px; }
.share-grid .board-mini { gap: 3px; }

/* ── segmented control ─────────────────────────────────────────────────── */

.seg { display: flex; gap: 2px; padding: 2px; background: var(--pill); border-radius: 10px; }
.seg button {
  flex: 1; font: inherit; font-size: 14px; font-weight: 500;
  padding: 7px 6px; border: 0; border-radius: 8px;
  background: transparent; color: var(--ink); cursor: pointer;
}
.seg button[aria-selected="true"], .seg button.is-on {
  background: var(--panel); font-weight: 600;
  box-shadow: 0 1px 3px rgba(0,0,0,.12);
}
.seg-sm { max-width: 230px; }

/* ── the boards ────────────────────────────────────────────────────────── */

.board-list { margin-top: 12px; }
.board-row {
  display: flex; align-items: center; gap: 12px;
  padding: 12px 16px;
  border-bottom: 1px solid var(--line-2);
}
.board-row:last-child { border-bottom: 0; }
.board-row.you { background: color-mix(in srgb, var(--link) 9%, var(--panel)); }
.board-rank {
  width: 26px; flex: none; text-align: right;
  font-family: var(--font-num); font-weight: 700; font-size: 15px;
  font-variant-numeric: tabular-nums; color: var(--dim);
}
.board-row.you .board-rank { color: var(--link); }
.board-row.you .board-name { font-weight: 700; }
.board-who { flex: 1; min-width: 0; }
.board-name { font-size: 15px; font-weight: 600; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }
.board-meta { font-size: 11.5px; color: var(--dim); margin-top: 1px; }
.board-score {
  font-family: var(--font-num); font-size: 18px; font-weight: 600;
  font-variant-numeric: tabular-nums; text-align: right;
}
.board-score small { display: block; font-size: 11px; font-weight: 500; color: var(--dim); }
.board-score.lost { color: var(--dim); }
.board-empty { padding: 30px 20px; text-align: center; color: var(--dim); font-size: 14px; line-height: 1.5; }

/* A player's own row on the day's board shows their mark grid, small — it is
   the one thing on the board that is fun to look at, and it is only ever shown
   for a game that is OVER. */
.board-mini { display: flex; gap: 2px; margin-top: 4px; }
.board-mini i { width: 7px; height: 7px; border-radius: 2px; display: block; }
.board-mini i.g { background: var(--mark-g); }
.board-mini i.y { background: var(--mark-y); }
.board-mini i.x { background: var(--mark-x); }

/* The day's leaderboard: the count, then the game's colours, tiny, to its
   right — always (words-v26; until then a tap swapped one for the other). The
   grid takes a fixed slot so the counts line up down the list. */
.board-score.with-grid { display: flex; align-items: center; gap: 12px; }
.board-count { min-width: 46px; }
.board-grid { display: flex; flex-direction: column; gap: 1.5px; width: 33px; }
.board-grid .board-mini { margin-top: 0; gap: 1.5px; }
.board-grid .board-mini i { width: 5px; height: 5px; border-radius: 1px; }

/* ── settings rows ─────────────────────────────────────────────────────── */

.rows > .row {
  padding: 14px 16px;
  border-bottom: 1px solid var(--line-2);
  display: flex; align-items: center; justify-content: space-between; gap: 14px;
}
.rows > .row:last-child { border-bottom: 0; }
/* Matched at the same depth as the rule above, or its `align-items: center`
   wins and every stacked row shrinks to its content instead of spanning the
   card. This exact bug is written up in mines' stylesheet; it is here because
   the rule was copied from there and the fix has to come with it. */
.rows > .row.row-stack { flex-direction: column; align-items: stretch; gap: 8px; }
.row-head { display: flex; align-items: center; justify-content: space-between; }
.row-k { font-size: 15px; font-weight: 500; }
.row-v { font-family: var(--font-num); font-weight: 600; font-variant-numeric: tabular-nums; }
.row-note { margin: 0; font-size: 12.5px; line-height: 1.5; color: var(--dim); }
/* A ROW YOU TAP (2026-09-20): the tip jar at the foot of Settings. A <button>
   so haptics.js lays its switch on it like every other control; the chevron
   says it opens. The card holds only the row, so the card's own padding is
   none and the row's 14px 16px is the whole of it. */
.rows > button.row-tap {
  appearance: none; -webkit-appearance: none; width: 100%; background: none; border: 0;
  font: inherit; color: inherit; text-align: left; cursor: pointer;
}
.rows > button.row-tap:active { background: var(--pill); }
.row-tap .chev { color: var(--dim); font-size: 22px; line-height: 1; margin-top: -2px; }
#tipjar-card { margin-top: 14px; }
#tipjar .tip-glyph { color: var(--heart); font-size: 18px; line-height: 1; margin-right: 2px; }
#tipjar .row-k { flex: 1; }
/* MYJP.CA GAMES (index.html, the foot): the lockup from myjp.ca/games —
   the wordmark in the rounded face, "games" tucked under it in the games'
   gold (games/index.php) — then "More games" and the chevron. */
:root { --games-gold: #c48a12; }
@media (prefers-color-scheme: dark) { :root:not([data-theme="light"]) { --games-gold: #f2b93a; } }
:root[data-theme="dark"] { --games-gold: #f2b93a; }
#games-card { margin-top: 14px; }
#games-row { padding: 12px 16px; }
.games-lockup { display: inline-flex; flex-direction: column; align-items: flex-end; line-height: 1; font-family: "SF Pro Rounded", ui-rounded, var(--font); font-weight: 700; }
.games-lockup .gl-mark { font-size: 22px; letter-spacing: -0.01em; color: var(--ink); }
.games-lockup .gl-ca { color: var(--dim); }
.games-lockup .gl-sub { font-size: 10px; letter-spacing: .02em; color: var(--games-gold); margin-top: -1px; margin-right: 1px; }
#games-row .games-more { flex: 1; text-align: right; color: var(--dim); font-weight: 400; }
/* ACCOUNT: who, one line of fine print, and Sign out as a row in the danger
   colour (the colour .btn-danger had). */
#account-card { padding: 14px 16px 0; }
#account-card .row-note { margin: 0 0 12px; }
#account-card .row-note + .row-note { margin-top: -6px; }
#account-card .rows { margin: 0 -16px; }
#account-card .rows > button.row-tap { padding: 14px 16px; border-bottom: 0; border-top: 1px solid var(--line-2); }
.rows > button.row-tap.danger { color: var(--bad); }
/* TECHNICAL (index.html): a folded card — the summary is a row with a caret
   that turns; inside, a key/value list in the mono face and one note.
   list-style none + the -webkit-details-marker rule hide the browser's own
   triangle. */
details.technical { margin-top: 14px; }
details.technical > summary { list-style: none; cursor: pointer; display: flex; align-items: center; justify-content: space-between; gap: 14px; padding: 14px 16px; }
details.technical > summary::-webkit-details-marker { display: none; }
details.technical > summary .chev { color: var(--dim); font-size: 22px; line-height: 1; margin-top: -2px; transition: transform .15s; }
details.technical[open] > summary .chev { transform: rotate(90deg); }
details.technical > summary:active { background: var(--pill); }
details.technical .kv { margin: 0; padding: 4px 16px 12px; border-top: 1px solid var(--line-2); }
details.technical .kv > div { display: flex; gap: 12px; padding: 8px 0; border-bottom: 1px solid var(--line-2); }
details.technical .kv > div:last-child { border-bottom: 0; }
details.technical .kv dt { flex: 0 0 76px; font-size: 13px; color: var(--dim); }
details.technical .kv dd { margin: 0; flex: 1; min-width: 0; font-size: 12.5px; font-family: var(--font-mono); color: var(--ink-2); line-height: 1.5; overflow-wrap: anywhere; }
details.technical > .row-note { padding: 0 16px 14px; }

.switch { position: relative; display: inline-block; width: 51px; height: 31px; flex: none; }
.switch input { position: absolute; opacity: 0; width: 100%; height: 100%; margin: 0; cursor: pointer; z-index: 2; }
.switch span { position: absolute; inset: 0; border-radius: 999px; background: var(--pill); transition: background .2s ease; }
.switch span::after {
  content: ""; position: absolute; top: 2px; left: 2px;
  width: 27px; height: 27px; border-radius: 50%;
  background: #fff; box-shadow: 0 1px 3px rgba(0,0,0,.25);
  transition: transform .2s ease;
}
.switch input:checked + span { background: var(--good); }
.switch input:checked + span::after { transform: translateX(20px); }
.switch input:disabled + span { opacity: .4; }


/* ── the end-of-game panel ─────────────────────────────────────────────── */

.done { margin-top: 12px; }
.done-head { padding: 18px 16px 6px; text-align: center; }
.done-head h2 { margin: 0 0 4px; font-size: 24px; letter-spacing: -.4px; }
.done-word {
  font-family: var(--font-num); font-size: 30px; font-weight: 700;
  letter-spacing: 3px; text-transform: uppercase;
}
.done-line { margin: 6px 0 0; color: var(--dim); font-size: 13.5px; line-height: 1.5; }
.done-btns { display: flex; gap: 10px; padding: 4px 16px 16px; }
.done-btns .btn { flex: 1; min-width: 0; padding: 13px 8px; font-size: 15px; white-space: nowrap; }
.done-btns .btn[hidden] { display: none; }

/* ── tab bar ───────────────────────────────────────────────────────────── */

/* FIXED TO THE FOOT (words-v16, IOS.md §15; letters' and mathtime's bar). It
   was the last row of a locked 100dvh column, which on an installed iPhone is
   a status bar short of the screen. Every place that pads for it reads the
   one number, --tabs-h, so the 50px disagreement that once made the play
   screen scroll cannot come back (bin/check_boot.js measures the bar against
   it). */
.tabs {
  position: fixed; left: 0; right: 0; bottom: 0;
  display: flex;
  padding-bottom: env(safe-area-inset-bottom, 0px);
  background: color-mix(in srgb, var(--panel) 86%, transparent);
  backdrop-filter: saturate(180%) blur(20px);
  -webkit-backdrop-filter: saturate(180%) blur(20px);
  border-top: 1px solid var(--line-2);
  z-index: 10;
}
.tab {
  flex: 1; font: inherit; font-size: 10.5px; font-weight: 500;
  display: flex; flex-direction: column; align-items: center; gap: 3px;
  padding: 9px 0 7px;
  border: 0; background: transparent; color: var(--dim); cursor: pointer;
}
/* One drawing each (index.html). Solids carry the form with no stroke — a
   stroke on a filled tile would fatten it into its neighbour at 23px — and
   the empty tiles and the switch's track are outlines, marked with a class
   rather than picked out by their coordinates (letters' lesson: nudge a path
   and a coordinate selector silently stops matching). */
.tab-g { width: 23px; height: 23px; display: block; fill: currentColor; stroke: none; }
.tab-g .ol { fill: none; stroke: currentColor; stroke-width: 1.5; }
.tab.is-on { color: var(--link); }

/* ── the banner ────────────────────────────────────────────────────────── */

.banner {
  position: fixed; left: 12px; right: 12px;
  bottom: calc(var(--tabs-h) + 18px + env(safe-area-inset-bottom, 0px));
  display: flex; align-items: center; gap: 10px;
  padding: 12px 14px;
  background: var(--ink); color: var(--bg);
  border-radius: var(--r-mid);
  font-size: 13.5px; line-height: 1.45;
  box-shadow: var(--shadow);
  z-index: 20;
}
.banner[hidden] { display: none; }
.banner button {
  margin-left: auto; flex: none;
  font: inherit; font-size: 20px; line-height: 1;
  border: 0; background: transparent; color: inherit; cursor: pointer;
}

/* The banner's Retry. It exists because the old copy told people to pull down
   on a screen that had no pull-to-refresh, in an app with no address bar. */
.banner .banner-retry {
  margin-left: auto;
  padding: 6px 13px;
  font-size: 12.5px; font-weight: 600; line-height: 1.2;
  border-radius: 999px;
  background: var(--bg); color: var(--ink);
}
.banner .banner-retry[hidden] { display: none; }
.banner .banner-retry:disabled { opacity: .45; }
.banner .banner-retry + button { margin-left: 4px; }

/* Why the sign-in screen is showing, when there is a reason worth giving. */
.gate-msg {
  margin: 0 0 18px;
  padding: 11px 13px;
  border-radius: var(--r-mid);
  background: color-mix(in srgb, var(--warn) 14%, transparent);
  color: var(--warn);
  font-size: 13px; line-height: 1.45; text-align: left;
}
.gate-msg[hidden] { display: none; }

/* The one line in the app that is asking for something to be done, so not dim
   like the fineprint it sits under. */
.update-note { color: var(--warn); }
