/* ============================================================
   WORD QUARRY — GAME STYLES
   Ported from the prototype's felt-table aesthetic (streak/timer
   styles removed per D-003/D-018; overlay, banner, finish added).
   ============================================================ */

/* The game's two faces, BUNDLED (D-212) — Latin only, which is all the
   game draws. They were @imported from Google Fonts, which made every
   player's device ask a third party for them; the webview now asks no one.
   SIL Open Font License, texts beside the files in fonts/. */
@font-face { font-family: 'IBM Plex Mono'; font-style: normal; font-weight: 400; font-display: swap; src: url('fonts/ibm-plex-mono-400.woff2') format('woff2'); unicode-range: U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6, U+02DA, U+02DC, U+0304, U+0308, U+0329, U+2000-206F, U+20AC, U+2122, U+2191, U+2193, U+2212, U+2215, U+FEFF, U+FFFD; }
@font-face { font-family: 'IBM Plex Mono'; font-style: normal; font-weight: 600; font-display: swap; src: url('fonts/ibm-plex-mono-600.woff2') format('woff2'); unicode-range: U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6, U+02DA, U+02DC, U+0304, U+0308, U+0329, U+2000-206F, U+20AC, U+2122, U+2191, U+2193, U+2212, U+2215, U+FEFF, U+FFFD; }
@font-face { font-family: 'IBM Plex Mono'; font-style: normal; font-weight: 700; font-display: swap; src: url('fonts/ibm-plex-mono-700.woff2') format('woff2'); unicode-range: U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6, U+02DA, U+02DC, U+0304, U+0308, U+0329, U+2000-206F, U+20AC, U+2122, U+2191, U+2193, U+2212, U+2215, U+FEFF, U+FFFD; }
@font-face { font-family: 'Zilla Slab'; font-style: normal; font-weight: 500; font-display: swap; src: url('fonts/zilla-slab-500.woff2') format('woff2'); unicode-range: U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6, U+02DA, U+02DC, U+0304, U+0308, U+0329, U+2000-206F, U+20AC, U+2122, U+2191, U+2193, U+2212, U+2215, U+FEFF, U+FFFD; }
@font-face { font-family: 'Zilla Slab'; font-style: normal; font-weight: 600; font-display: swap; src: url('fonts/zilla-slab-600.woff2') format('woff2'); unicode-range: U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6, U+02DA, U+02DC, U+0304, U+0308, U+0329, U+2000-206F, U+20AC, U+2122, U+2191, U+2193, U+2212, U+2215, U+FEFF, U+FFFD; }
@font-face { font-family: 'Zilla Slab'; font-style: normal; font-weight: 700; font-display: swap; src: url('fonts/zilla-slab-700.woff2') format('woff2'); unicode-range: U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6, U+02DA, U+02DC, U+0304, U+0308, U+0329, U+2000-206F, U+20AC, U+2122, U+2191, U+2193, U+2212, U+2215, U+FEFF, U+FFFD; }

/* Not colours, so not the palette's: the body face's fallback stack, the
   corner radius, and the measure every base screen centres its contents at
   (S8-9). They lived in Strata's block, which was :root; with Survey the one
   palette left (D-274), they sit on their own. */
:root {
  --font-mono:    'IBM Plex Mono', Consolas, monospace;
  --radius: 4px;
  --measure: 420px;
}

/* ── Survey (D-044): quarry-as-worksite — stamped concrete blocks on a light
   field, one loud hi-vis orange, brutalist black outlines. NYT-daily adjacent;
   the bold, light alternative. ── */
:root {
  --bg:           #e9e3d7;
  --felt:         #e6e0d3;
  --felt-2:       #f1ece2;
  --panel:        #f6f2ea;
  --border:       #c3bba8;
  --tile-bg-free: #ffffff;   /* clean block, hard black outline */
  /* Pushed from #c4bdac (D-099 contrast pass): free-vs-blocked measured
     1.87:1 there, against 4.13:1 in the dark counterpart. Telling a tappable
     tile from dead stone is the read the whole game rests on, so a light
     palette does not get to be half as clear about it. Now 2.70:1. */
  --tile-bg-blk:#8a8474;   /* recessed grey block */
  --tile-bdr:     #16120b;
  --tile-shadow:  #16120b;   /* brutalist hard offset, no blur */
  --tile-text:    #16120b;
  --tile-pts-clr:#4a4438;
  --bar-tile-bg:  #ffe7d6;
  --bar-tile-bdr:var(--accent);
  --gold:#9e3000;   /* hi-vis safety orange */
  --gold-lt:#c23c00;
  --text:         #16120b;
  --text-dim:     #6a6354;
  --font-title:   'Zilla Slab', Georgia, serif; /* D-158: Strata's slab; the condensed poster face was unreadable at caption size (D-124) */
  --scrim:rgba(20,17,12,.58);
  --accent:       #ff5a1f;
  --accent-bdr:#c24000;
  --accent-ink:var(--tile-text);
  --surface-2:    #ded7c9;
  --rare-glow:var(--accent);   /* hi-vis block for the rares */
  --live-ink:var(--tile-text);
  /* Two the light theme cannot inherit. --gold-lt is #ff7a44 here, which is
     ~2:1 on the pale panel; this is the same hue, darkened to read. And the
     free tile is WHITE in survey, so a "kept" streak cell would be invisible —
     it becomes the ink instead. Both are divergences with a reason, which is
     the only kind a token should carry. */
  --ledger-credit:var(--gold-lt);
  --streak-kept:   #16120b;
  /* ── the same roles, answered by material ── */
  /* the price area (D-081), answered by MATERIAL — this palette's accent, gold
     and ore are one orange, which is exactly the case the roles exist for. */
  --lane-pay-bg:   var(--tile-bg-free);
  --lane-pay-ink:  var(--gold-ink);
  --lane-pay-edge: var(--tile-text);
  --lane-pay-rim:  var(--tile-text);
  --lane-free-bg:  var(--bar-tile-bg);
  --lane-free-ink: var(--gold-ink);
  --lane-free-edge: var(--tile-text);
  --lane-free-rim: var(--gold);
  --lane-live-bg:  var(--tile-text);
  --lane-live-ink: var(--gold-lt);
  --lane-live-edge: var(--gold);
  --short-rim:     color-mix(in srgb,var(--tile-text) 45%,transparent);

  /* flash, holdings pill, accent-on-tint */
  --flash-bg:      var(--tile-text);
  --flash-ink:     var(--gold-lt);
  --pill-bg:       var(--tile-text);
  --pill-ink:      var(--tile-bg-free);
  --tint-bg:color-mix(in srgb,var(--gold) 12%,transparent);
  --tint-ink:var(--gold);
  /* the ticket's own colour (D-302): a fuchsia-purple, never the live lime */
  --tkt:           #c626d7;
  --tkt-on:        #ffffff;
  --tkt-bg:color-mix(in srgb,var(--tkt) 13%,transparent);
  --tkt-ink:       #8a1b96;
  --tkt-rim:color-mix(in srgb,var(--tkt) 55%,transparent);
  --tkt-wash:color-mix(in srgb,var(--tkt) 15%,var(--tile-bg-free));
  --tkt-wash-ink:  #65136e;
  /* dim calendar days — an ink, never an alpha */
  --day-off-ink:   #7d766a;
  --day-none-ink:  #8c857a;

  /* the medal (D-101), a token since D-099's light palettes */
  --medal-gold:var(--gold);
  --medal-pip:var(--gold);
  /* ── Completing the set (D-099). Once inherited from Strata's :root, and
     wrong in light mode; declared here since. With one palette (D-274) the
     dark block below must still name every token this one does, which
     tests/theme.test.ts checks. ── */
  --gold-ink:      var(--tile-text);
  --live:#a8e000;
  --ledger-debit:  var(--text-dim);
  --streak-missed: color-mix(in srgb,var(--text-dim) 55%,transparent);
  --streak-today:  var(--live);
  --green:         #1f7a3d;
  --red:           #b3261e;
  --blue:          #1c5fa8;
  --tile-bg-blk-or: #b8b1a0;
  /* The key is a drawn object on a panel, so it has the medal's problem and
     takes the medal's answer: a metal chosen for THIS ground. */
  --key-metal:     #c23c00;
  --hint:            color-mix(in srgb, var(--blue) 30%, var(--tile-bg-free));   /* Design's tint (D-273): --blue into the free tile, the one hue with no other meaning on the board */
  --news-berry:      #9d1f5a;
  --news-berry-ink:  #fff0f6;
  --news-accent:     #b83500;
  --news-accent-ink: #fff1ea;
  --key-edge:      #3a1000;
  --coach-bg:      color-mix(in srgb,var(--key-metal) 16%,var(--panel));
  /* The welcome card (D-209, Design): the ring on today-still-to-dig takes
     --live-text, because Survey light's lime is a fill and not an edge
     (1.5:1 on the panel); the name-tile's inner rule and its streak tab. */
  --streak-due:    var(--live-text);
  --nt-frame:      color-mix(in srgb,var(--tile-bdr) 30%,var(--tile-bg-free));
  --nt-tab-bg:     var(--tile-text);
  --nt-tab-ink:    var(--tile-bg-free);
  --gnd-ink:       #524838;
  --gnd-grain:     0.16;
  --gnd-dust:      0.1;

  /* ── Added in the 2026-09-19 revision ── */
  /* the letter on a COVERED tile. It used to be --tile-text dimmed by
     opacity, which pinned the stone to a narrow mid band (the ink had to
     survive on it) and is why free-vs-blocked could not reach 3:1 in either
     light palette. Its own ink unpins the stone: 7.8:1 dark, 4.6:1 daylight. */
  --tile-text-blk: var(--tile-text);
  /* LIVE as INK, which is not the same colour as live as a GROUND — the
     same split --gold and --gold-ink have. Survey's liveness is a hi-vis
     lime: perfect as a badge fill, unreadable as text on a panel. */
  --live-text:     #5a6b00;
  /* the rare tile's corner flags. One shade down from --rare-glow and
     derived from it, so flags and halo are the same colour by construction. */
  --rare-mark:     var(--accent);
  /* the day number on a locked calendar cell. An ink, never an alpha. */
  --day-buried-ink: var(--tile-text);
}

*, *::before, *::after { box-sizing: border-box; margin: 0; padding: 0; }
html, body { height: 100%; }

body {
  font-family: var(--font-mono);
  background: var(--bg);
  color: var(--text);
  height: 100dvh;
  overflow: hidden;
}

/* ── Survey DARK (D-099) — the hi-vis worksite at night. The orange is the
   identity and does not move; what moves is the stone. Blocked tiles are
   DESATURATED rather than darkened (#4a4436 put black ink at 1.9:1), which is
   the same answer the light palettes give, arrived at from the other side. ── */
[data-mode="dark"] {
  --bg:            #14110c;
  --felt:          #171309;
  --felt-2:        #221c11;
  --panel:         #1c1810;
  --border:        #3a3324;
  --scrim:         rgba(8,6,3,.85);
  --surface-2:     #272115;
  --tile-bg-free:  #ffffff;
  --tile-bg-blk-or: #2a251a;
  --tile-bg-blk:   #857c68;
  --tile-bdr:var(--accent);
  --tile-shadow:   #a8330a;
  --tile-text:     #16120b;
  --tile-pts-clr:#4a4438;
  --bar-tile-bg:   #ffe7d6;
  --bar-tile-bdr:var(--accent);
  --text:          #ece4d6;
  --text-dim:      #948a76;
  --font-title:    'Zilla Slab',Georgia,serif;
  --gold:#ffc08a;
  --gold-lt:#ffd6ad;
  --gold-ink:      var(--tile-text);
  --accent:        #ff5a1f;
  --accent-bdr:    #ff7a44;
  --accent-ink:    var(--tile-text);
  --green:         #52c472;
  --red:           #e05050;
  --blue:          #5090d8;
  --rare-glow:var(--accent);
  --live:#a8e000;
  --live-ink:      var(--tile-text);
  --ledger-credit: var(--gold-lt);
  --ledger-debit:  var(--text-dim);
  --streak-kept:   var(--tile-bg-free);
  --streak-missed: color-mix(in srgb,var(--text-dim) 55%,transparent);
  --streak-today:  var(--live);
  --lane-pay-bg:   var(--tile-bg-free);
  --lane-pay-ink:  var(--gold-ink);
  --lane-pay-edge: var(--gold);
  --lane-pay-rim:  var(--gold);
  --lane-free-bg:  var(--bar-tile-bg);
  --lane-free-ink: var(--gold-ink);
  --lane-free-edge: var(--gold);
  --lane-free-rim: color-mix(in srgb,var(--gold) 60%,transparent);
  --lane-live-bg:  var(--tile-text);
  --lane-live-ink: var(--gold-lt);
  --lane-live-edge: var(--gold);
  --flash-bg:      var(--tile-text);
  --flash-ink:     var(--gold-lt);
  --pill-bg:       var(--tile-text);
  --pill-ink:      var(--tile-bg-free);
  --day-off-ink:   #5e5646;
  --day-none-ink:  #403a31;
  --tint-bg:color-mix(in srgb,var(--gold) 14%,transparent);
  --tint-ink:      var(--gold);
  --tkt:           #ee48ff;
  --tkt-on:        #1c0a24;
  --tkt-bg:color-mix(in srgb,var(--tkt) 16%,transparent);
  --tkt-ink:       #e678f2;
  --tkt-rim:color-mix(in srgb,var(--tkt) 60%,transparent);
  --tkt-wash:color-mix(in srgb,var(--tkt) 30%,var(--tile-bg-free));
  --tkt-wash-ink:  #4e0856;
  --short-rim:     color-mix(in srgb,var(--tile-text) 45%,transparent);
  --medal-gold:    #ff7a44;
  --medal-pip:     #b34500;
  /* The key is a drawn object on a panel, so it has the medal's problem and
     takes the medal's answer: a metal chosen for THIS ground. */
  --key-metal:     #ffc08a;
  --hint:            color-mix(in srgb, var(--blue) 42%, var(--tile-bg-free));   /* Design's tint (D-273); 42% in Survey dark */
  --news-berry:      #ff6fa4;
  --news-berry-ink:  #2a0710;
  --news-accent:     #ff5a1f;
  --news-accent-ink: #2a0710;
  --key-edge:      #8a2c06;
  --coach-bg:      color-mix(in srgb,var(--key-metal) 12%,var(--panel));
  /* The welcome card (D-209, Design): the ring on today-still-to-dig takes
     --live-text, because Survey light's lime is a fill and not an edge
     (1.5:1 on the panel); the name-tile's inner rule and its streak tab. */
  --streak-due:    var(--live-text);
  --nt-frame:      color-mix(in srgb,var(--tile-bdr) 30%,var(--tile-bg-free));
  --nt-tab-bg:     var(--tile-text);
  --nt-tab-ink:    var(--tile-bg-free);
  --gnd-ink:       #91846f;
  --gnd-grain:     0.14;
  --gnd-dust:      0.05;

  /* ── Added in the 2026-09-19 revision ── */
  /* the letter on a COVERED tile. It used to be --tile-text dimmed by
     opacity, which pinned the stone to a narrow mid band (the ink had to
     survive on it) and is why free-vs-blocked could not reach 3:1 in either
     light palette. Its own ink unpins the stone: 7.8:1 dark, 4.6:1 daylight. */
  --tile-text-blk: var(--tile-text);
  /* LIVE as INK, which is not the same colour as live as a GROUND — the
     same split --gold and --gold-ink have. Survey's liveness is a hi-vis
     lime: perfect as a badge fill, unreadable as text on a panel. */
  --live-text:     var(--live);
  /* the rare tile's corner flags. One shade down from --rare-glow and
     derived from it, so flags and halo are the same colour by construction. */
  --rare-mark:     var(--accent);
  /* the day number on a locked calendar cell. An ink, never an alpha. */
  --day-buried-ink: #c9c0aa;
}

#app {
  display: flex; flex-direction: column; height: 100dvh; overflow: hidden;
  /* One token drives every "leave room for the nav" rule (D-070). The nav is
     hidden while digging, so the same token collapses to 0 there. */
  --nav-h: calc(54px + env(safe-area-inset-bottom, 0px));
}
#app.digging { --nav-h: 0px; }

/* ── Header ── */
/* Stacks ABOVE the nav and every sheet, so the key pill and ⚙ stay reachable
   on every screen (D-070 band table below). */
#game-header {
  display: flex;
  align-items: center;
  justify-content: space-between;
  padding: 6px 12px;
  background: var(--panel);
  border-bottom: 1px solid var(--border);
  flex-shrink: 0;
  gap: 10px;
  position: relative;
  z-index: 31000;
}

.hd-left { display: flex; align-items: baseline; gap: 8px; min-width: 0; }

#game-header h1 {
  font-family: var(--font-title);
  font-size: 1rem;
  color: var(--gold);
  letter-spacing: 0.05em;
  white-space: nowrap;
}

/* Logo: the game's own letter tile — W, 4 points — built from the live theme
   tokens, so it reskins with the board (replaces the borrowed 🀄 glyph). */
.logo-tile {
  position: relative;
  display: inline-flex;
  align-items: center;
  justify-content: center;
  width: 22px;
  height: 22px;
  margin-right: 7px;
  vertical-align: -5px;
  border-radius: 3px;
  background: var(--tile-bg-free);
  border: 1px solid var(--tile-bdr);
  box-shadow: 1px 2px 0 var(--tile-shadow);
  color: var(--tile-text);
  font-family: var(--font-mono);
  font-weight: 700;
  font-size: 14px;
  letter-spacing: 0;
}
.logo-tile i {
  position: absolute;
  right: 2px;
  bottom: 1px;
  font-style: normal;
  font-size: 8px;
  font-weight: 600;
  line-height: 1;
  color: var(--tile-pts-clr);
}

/* Date sits quietly under the board, in the felt padding area. */
.board-date {
  position: absolute;
  bottom: 4px;
  left: 0;
  right: 0;
  text-align: center;
  font-size: 0.62rem;
  color: var(--text-dim);
  letter-spacing: 0.08em;
  pointer-events: none;
}

.hd-center { display: flex; gap: 14px; align-items: center; }
.hd-right { display: flex; flex-direction: row; align-items: center; gap: 6px; }

/* The key pill (S8-7/D-078; keys since D-091). It rides the header on every
   screen but the dig — one holding, one place to look. */
.key-pill {
  display: inline-flex; align-items: center; gap: 5px;
  min-height: 30px; padding: 3px 10px 3px 8px; border-radius: 20px;
  font-family: var(--font-mono); font-weight: 700; font-size: 0.78rem;
  color: var(--gold);
  background: color-mix(in srgb, var(--gold) 12%, transparent);
  border: 1px solid color-mix(in srgb, var(--gold) 45%, transparent);
  cursor: pointer;
}
.key-pill:hover { background: color-mix(in srgb, var(--gold) 20%, transparent); }
/* "You are here" — quieter than the nav's tab state, because these are not
   tabs: you stepped aside into a page and you are coming back. */
.key-pill.on { background: color-mix(in srgb, var(--gold) 26%, transparent); border-color: var(--gold); }
/* A FULL PURSE (D-176, Design's first-days delivery): solid metal and a bunch
   of keys — a state of the pill, never a mark, never moving. The thin tint
   fills to solid --gold and inks in --panel; the key re-points its metal the
   way the pay lane does, and two more keys stand behind it as drop-shadows of
   its own outline. Turned 45° clockwise (Andres 2026-09-24): lying flat, the
   three overlapped along their own length and read as one blurred key; on the
   diagonal they read as three. A filter runs BEFORE the transform, so the
   shadow offsets are written in the key's own turned frame — (±6px, ∓6px)
   there is ±8.5px straight across on screen. Adapted to this pill: the bunch
   is wider than the plain pill's padding, so BOTH states share a min-width
   and the header does not move on the day the purse fills. These rules must
   stay AFTER :hover and .on above — they tie on specificity. */
.key-pill { min-width: 61px; justify-content: center; }
.key-pill.is-full,
.key-pill.is-full.on {
  background: var(--gold); border-color: var(--gold); color: var(--panel);
  padding-left: 12px; padding-right: 8px; gap: 11px;
}
.key-pill.is-full .key {
  --key-metal: var(--panel); --key-edge: var(--gold);
  transform: rotate(45deg);
  filter:
    drop-shadow(-6px 6px 0 color-mix(in srgb, var(--panel) 55%, var(--gold)))
    drop-shadow(6px -6px 0 color-mix(in srgb, var(--panel) 55%, var(--gold)));
}
/* Full with the Keys page open: an inner ring, so "you are here" survives. */
.key-pill.is-full.on { box-shadow: inset 0 0 0 2px var(--panel); }
#profile-btn.on { border-color: var(--gold); color: var(--gold); }
/* A head, not a letter tile (Andres 2026-09-08). The tile read as a game
   piece; the head is the one profile glyph a player never has to learn. The
   letter tile survives INSIDE the profile as the avatar, where it is identity
   rather than a signpost. */
.icon-btn { display: inline-flex; align-items: center; justify-content: center; }
.icon-btn svg { width: 17px; height: 17px; display: block; }
.stat { font-size: 0.75rem; color: var(--text-dim); white-space: nowrap; }
.stat strong { color: var(--text); font-weight: 700; }

/* Wider screens have header room to spare — let Score/Tiles/Time breathe
   (Andres 2026-07-16); the 560px mobile wrap keeps the compact size. */
@media (min-width: 561px) {
  .stat { font-size: 0.95rem; }
}

/* Tight screens: header wraps to two rows — name + buttons up top, the
   score/tiles stats on their own full row. The groups themselves stay
   horizontal (Andres 2026-06-12). */
@media (max-width: 560px) {
  #game-header { flex-wrap: wrap; row-gap: 2px; }
  .hd-left { order: 1; }
  .hd-right { order: 2; margin-left: auto; }
  .hd-center { order: 3; width: 100%; }
}

.btn-big { font-size: 0.95rem; padding: 14px 26px; min-height: 48px; }

.hidden { display: none !important; }

/* ── Board ── */
#board-container {
  flex: 1;
  overflow: auto;
  padding: 12px;
  background:
    radial-gradient(ellipse at 50% 40%, var(--felt-2) 0%, var(--felt) 70%),
    repeating-linear-gradient(0deg, transparent, transparent 22px,
      rgba(255,255,255,0.015) 22px, rgba(255,255,255,0.015) 23px);
  display: flex;
  justify-content: center;
  align-items: flex-start;
  position: relative;
}

/* z-index: 0 makes #board its own stacking context: the (z,y,x)-packed tile
   z-indexes (up to ~500k) stay LOCAL and can never rise above overlays or the
   header — they only order tiles against each other. */
#board { position: relative; flex-shrink: 0; z-index: 0; }

.loading {
  position: absolute;
  inset: 0;
  display: flex;
  align-items: center;
  justify-content: center;
  color: var(--text-dim);
  font-size: 0.8rem;
  letter-spacing: 0.05em;
}

/* ── Tiles ── */
.tile {
  position: absolute;
  display: flex;
  flex-direction: column;
  align-items: center;
  justify-content: center;
  cursor: not-allowed;
  border-radius: var(--radius);
  user-select: none;
  -webkit-user-select: none;
  transition: opacity 0.18s, transform 0.15s, background 0.12s;
  background: var(--tile-bg-blk);
  border: 1px solid color-mix(in srgb, var(--tile-bg-blk) 72%, var(--tile-text));
  box-shadow: 2px 2px 0 color-mix(in srgb, var(--tile-bg-blk) 45%, var(--tile-text));
}

.tile .tl {
  font-family: var(--font-mono);
  font-weight: 700;
  color: var(--tile-text);
  line-height: 1;
  pointer-events: none;
}

.tile .tp {
  font-family: var(--font-mono);
  font-weight: 600;
  color: var(--tile-pts-clr);
  position: absolute;
  bottom: 7%;
  right: 9%;
  pointer-events: none;
}

/* The letter on a COVERED tile (2026-09-19 revision). It used to be
   --tile-text on whatever the blocked stone happened to be, which pinned the
   stone to a narrow mid band: it had to stay light enough for a dark letter,
   and that is why free-vs-blocked could not reach 3:1 in either light palette.
   With the ink a token of its own the stone is free to go dark again — 7.8:1
   against the free tile in Strata dark, 4.6:1 in daylight, and the letter
   still reads at 5.6 and 5.2. */
.tile:not(.free) .tl { color: var(--tile-text-blk); }

.tile.free {
  cursor: pointer;
  background: var(--tile-bg-free);
  border-color: var(--tile-bdr);
  box-shadow: 2px 3px 0 var(--tile-shadow);
}

.tile.free:hover {
  background: var(--bar-tile-bg);
  /* LIFT ONLY, no scale (Andres 2026-09-19): scaling the tile scales the
     letter with it, so the type changed size as the thumb moved across the
     board. A tile on the board is one size, always. */
  transform: translateY(-3px);
  box-shadow: 2px 6px 8px rgba(0,0,0,0.55);
  /* Must beat the (z,y,x)-packed tile z-indexes (max ~500k). 20000 was above
     the OLD z*1000+y scale but UNDER the new one — a hovered/tapped tile sank
     beneath its neighbours, vanished, and ate the click (bug 2026-06-13). */
  z-index: 1000000 !important;
}

/* Press reads as the tile going back down onto the mound, not as it shrinking —
   same reason as the hover above. */
.tile.free:active { transform: translateY(0); }

/* ── RARE (redesigned 2026-09-19, at Andres's request) ────────────────────
   Was an outer glow, which is a halo drawn OUTSIDE the tile: on a crowded
   mound it bled onto the neighbour, and in a light palette a soft glow on a
   pale ground is nearly nothing at all.
   Now: THREE corner flags plus a ring with a 1px gap in the tile's own colour,
   so the ring reads as a ring rather than as a thick border. Three, not four —
   the bottom-right corner belongs to the point value, and a numeral on the ore
   measures 1.37:1.
   The flags are --rare-mark (one shade down from --rare-glow, derived from it,
   so flag and ring are the same colour by construction). The GROUND never
   moves: a rare tile is still a tile you can read, which is what rules out
   tinting or inverting it. Our picked tile leaves the board for the word bar,
   so there is no picked state to mark here (Andres 2026-09-19) — the
   designer's --pick-mark has no consumer in this client and is not shipped. */
/* The point value on a rare tile is a POINT VALUE (Andres 2026-09-19): same
   colour as every other tile's, and no text-shadow — it was the only number on
   the board wearing one, which read as a different kind of thing rather than
   the same thing on a special tile. The ore is spent on the edges, and the
   edges are enough. */
.tile.rare::before, .bar-tile.rare::before {
  content: '';
  position: absolute;
  inset: 0;
  z-index: 1;
  border-radius: inherit;
  background-repeat: no-repeat;
  background-image:
    linear-gradient(to bottom right, var(--rare-mark) 49.5%, transparent 50%),
    linear-gradient(to bottom left, var(--rare-mark) 49.5%, transparent 50%),
    linear-gradient(to top right, var(--rare-mark) 49.5%, transparent 50%);
  background-position: top left, top right, bottom left;
  background-size: 12px 12px;
}
/* The letter and the points sit above the flags.
   `position: relative` ONLY on the ones that are statically positioned: .tp is
   already absolute (bottom/right), and relative-ing it dropped it back into the
   flex flow and centred it on the tile (bug, Andres 2026-09-19). An absolutely
   positioned box takes a z-index as it is. */
.tile.rare .tl, .bar-tile.rare .bl { position: relative; z-index: 2; }
.tile.rare .tp, .bar-tile.rare .bpts, .bar-tile.rare .bplus, .bar-tile.rare .bmult { z-index: 2; }

.tile.free.rare {
  border-color: var(--rare-mark);
  box-shadow: 2px 3px 0 var(--tile-shadow);
}
.tile.rare:not(.free) { border-color: color-mix(in srgb, var(--rare-mark) 45%, var(--tile-bdr)); }
.tile.free.rare:hover { box-shadow: 2px 6px 8px rgba(0,0,0,0.55); }
.tile.free.rare:active { box-shadow: 2px 3px 0 var(--tile-shadow); }
.bar-tile.rare {
  border-color: var(--rare-mark);
  box-shadow: 1px 2px 4px rgba(0,0,0,0.4);
}
.bar-tile.rare::before { background-size: 8px 8px; }
/* A rare tile that is ALSO carrying a speed streak keeps the streak's glow. */
.bar-tile.rare.streaked { box-shadow: 1px 2px 4px rgba(0,0,0,0.4), 0 0 6px 2px color-mix(in srgb, var(--gold) 50%, transparent); }
/* Flags, and no ring (Andres 2026-09-19). Survey has one orange: its tile
   edge, its accent and its rare mark are the same hue, so a second orange ring
   around an orange-edged tile reads as a halo, not a mark. Strata's teal ore
   could carry a ring; with Survey the one palette (D-274), no rare tile does. */
/* The point value keeps the tile's own ink here, as it does on the board.
   (This comment used to justify that by a dark text-shadow on the board's own
   number — D-108 removed that shadow and left the reasoning behind, which is
   the same staleness the poster's stylesheet kept showing. There is no
   text-shadow on a point value anywhere; the flags say the letter is rare.) */

.tile.picked { opacity: 0; transform: scale(0.6); pointer-events: none; }

.tile-ghost {
  background: var(--tile-bg-free) !important;
  border: 1px solid var(--tile-bdr) !important;
  box-shadow: 2px 3px 0 var(--tile-shadow) !important;
  border-radius: var(--radius);
  display: flex;
  flex-direction: column;
  align-items: center;
  justify-content: center;
}

.tile-ghost .tl { font-family: var(--font-mono); font-weight: 700; color: var(--tile-text); line-height: 1; }
.tile-ghost .tp { font-family: var(--font-mono); font-weight: 600; color: var(--tile-pts-clr); position: absolute; bottom: 7%; right: 9%; }

/* ── Word area ── */
#word-area {
  position: relative; /* the positioning host for the floating message */
  background: var(--panel);
  border-top: 1px solid var(--border);
  padding: 8px 10px 6px;
  flex-shrink: 0;
}

#word-row { display: flex; align-items: center; gap: 10px; min-width: 0; }

/* The bar's host (S15-5): the bar fills it, and Clear floats over its right
   end, so the row gains no height and the bar keeps its reservation. */
#word-bar-wrap { position: relative; display: flex; flex: 1; min-width: 0; }
#clear-btn {
  position: absolute;
  right: 0;
  top: 50%;
  transform: translateY(-50%);
  width: 44px;  /* CLEAR_W in game.ts; a real tap target (G6) */
  height: 44px;
  display: flex;
  align-items: center;
  justify-content: center;
  padding: 0;
  border: none;
  background: none;
  color: var(--text-dim);
  cursor: pointer;
}
#clear-btn svg { width: 18px; height: 18px; }

/* The pocket (S15-6) as Design's lip (D-273, variant A). Two 44px tap targets
   hang from the word area's top edge over the board's lower-left corner. Each
   holds a 34x40 tile TUCKED under the row: the slot clips it, so only its top
   22px shows, and that part carries the letter and its points (top-right), so
   a tucked tile still names itself and its value. A slot mouth inked on the
   row's edge says where the tiles went. The word row keeps its whole width
   (Andres: "leaving the lower area fully for the word forming").
   The tile slides up under a pointer that can hover, and an armed slot slides
   up everywhere, which is how a touchscreen sees it (Andres: "For
   touchscreens the pocket slide up would be when empty pocket is selected").
   The board's fit keeps this corner clear (lip.ts). */
#word-area.has-pocket::before {
  content: "";
  position: absolute;
  top: -2px;
  left: 6px;
  width: 92px;
  height: 3px;
  border-radius: 2px;
  background: var(--text);
  z-index: 4;
}
.pocket {
  position: absolute;
  left: 6px;
  bottom: 100%;
  z-index: 3;
  display: flex;
  align-items: flex-end;
  gap: 4px;
}
.pocket-slot {
  position: relative;
  width: 44px;
  height: 44px;
  overflow: hidden; /* the tuck: the tile's lower part is under the row */
  padding: 0;
  border: none;
  background: none;
  color: inherit;
  font: inherit;
  cursor: pointer;
}
.pocket-slot > i {
  position: absolute;
  left: 5px;
  top: 22px; /* 22 of its 40px show */
  width: 34px;
  height: 40px;
  box-sizing: border-box;
  display: flex;
  align-items: flex-start; /* the letter at the top, where it shows */
  justify-content: center;
  padding-top: 3px;
  border: 1.5px dashed var(--text-dim);
  border-radius: 4px;
  background: var(--surface-2);
  transition: transform 0.15s ease-out;
}
.pocket-slot.armed > i { transform: translateY(-18px); }
@media (hover: hover) {
  .pocket-slot:hover > i { transform: translateY(-18px); }
}
/* Reduced motion: the tile still rises, at once. */
@media (prefers-reduced-motion: reduce) {
  .pocket-slot > i { transition: none; }
}
.pocket-slot > i:empty::before {
  content: "";
  width: 9px;
  height: 9px;
  margin-top: 3px;
  background:
    linear-gradient(var(--text-dim), var(--text-dim)) center / 100% 1.5px no-repeat,
    linear-gradient(var(--text-dim), var(--text-dim)) center / 1.5px 100% no-repeat;
}
.pocket-slot.armed > i { border: 2px solid var(--text); }
.pocket-slot.armed > i:empty::before {
  background:
    linear-gradient(var(--text), var(--text)) center / 100% 2px no-repeat,
    linear-gradient(var(--text), var(--text)) center / 2px 100% no-repeat;
}
/* A pocket tile wears the word bar's paint, flat (Design). */
.pocket-slot > i.pocket-tile {
  border: 1.5px solid var(--bar-tile-bdr);
  background: var(--bar-tile-bg);
  color: var(--tile-text);
  font: 700 1rem/1 var(--font-mono);
}
.pocket-tile .pp {
  position: absolute;
  top: 2px;
  right: 2px;
  font-size: 0.5rem;
  font-weight: 600;
  color: var(--tile-pts-clr);
}
/* A rare letter in the pocket keeps its coloured border and its top-left flag
   (Andres: "the border stays colored, the top left corner stays colored").
   The top-right flag goes: the points take that corner, and a numeral on the
   ore measures 1.37:1, which is why the board's tile has no flag in the corner
   its points use either. The bottom flags would be under the row. */
.pocket-slot > i.pocket-tile.rare { border-color: var(--rare-mark); }
.pocket-tile.rare::before {
  content: "";
  position: absolute;
  top: 0;
  left: 0;
  width: 9px;
  height: 9px;
  border-top-left-radius: 2px;
  background: linear-gradient(to bottom right, var(--rare-mark) 49.5%, transparent 50%);
}
/* A dim name while both slots are empty, so a first-timer has a word for it;
   "Choose a tile" while one is armed (Design). */
.pocket-lbl {
  margin: 0 0 3px 2px;
  font: 700 0.68rem/1 var(--font-mono);
  letter-spacing: 0.1em;
  text-transform: uppercase;
  color: var(--text-dim);
  white-space: nowrap;
  pointer-events: none;
}
.pocket-lbl.on {
  color: var(--text);
  letter-spacing: 0.02em;
  text-transform: none;
  /* A chip of its own (D-275): the line sits on the board's corner, where the
     date or a tile can be under it. The date also steps aside while armed. */
  padding: 2px 6px;
  border-radius: 4px;
  background: var(--panel);
  box-shadow: 0 0 0 1px var(--border);
}
/* The name sits ABOVE the slots, in their empty upper half, where a rising
   tile covers it (D-275, Andres: "Move the name of the pocket above the
   pockets (the tiles can cover it when the will pop up). It is currently too
   close to the date of the board."). */
.pocket-lbl:not(.on) { position: absolute; left: 7px; bottom: 26px; margin: 0; z-index: 0; }
.pocket-slot { z-index: 1; }
#board-container.is-arming .board-date { visibility: hidden; }
/* Its letter is in the word: the tile slides down out of sight, as a picked
   tile leaves the board, and comes back up if the letter is taken back. Never
   dimmed: no tile is ever drawn see-through (Andres 2026-10-02, D-274). */
.pocket-slot.used { cursor: default; }
.pocket-slot.used > i,
.pocket-slot.used:hover > i { transform: translateY(22px); }
/* Armed: the chosen tile is marked in ink, not red, since nothing is spent.
   The board itself does not change: a free tile is already the white one
   (Andres 2026-10-02: "The white tiles are already visible, so no need to
   emphasize them separately for pocketing"). */
.tile.pocket-choice { outline: 3px dashed var(--text); outline-offset: 2px; }
.pocket-card {
  position: absolute;
  left: 10px;
  right: 10px;
  bottom: 54px; /* clear of the lip's 44px corner (D-273) */
  z-index: 2; /* over #board, which is its own stacking context at 0 */
  padding: 10px 12px;
  background: var(--panel);
  border: 1px solid var(--border);
  border-radius: 10px;
  box-shadow: 0 4px 14px rgba(0, 0, 0, 0.35);
  font-size: 0.8rem;
  line-height: 1.35;
}
.pocket-card.at-top { top: 10px; bottom: auto; }
.pocket-card p { margin: 0; padding-right: 30px; }
/* A note laid over the board always closes (D-275): the pocket card and the
   suggest tip. 32px of glyph room inside a 44px reach, at the note's corner. */
.note-x {
  position: absolute;
  top: 2px;
  right: 2px;
  width: 36px;
  height: 36px;
  padding: 0;
  display: flex;
  align-items: center;
  justify-content: center;
  border: none;
  background: none;
  color: var(--text-dim);
  cursor: pointer;
}
.note-x svg { width: 16px; height: 16px; }
.note-x:hover { color: var(--text); }
.pocket-card .btn-row { margin-top: 8px; justify-content: flex-end; }

/* The hint (S15-7, D-272): the hinted word's tiles PAINTED in a colour of
   their own, each with its pick-order number pulsing in the tile's upper-left
   corner, on the tile and not on its border. The rest of the board is left as
   it is (D-270). An ink ring was too quiet once the board stopped dimming
   (Andres: "Hint border is too unnoticeable without transparent tiles").
   The fill is the GROUND only, so a rare tile keeps its corner flags and its
   ring. A covered tile of the word is ringed INSIDE in the colour instead, and
   painted once it comes free: mixing the colour into dark stone took its light
   letter down to about 2:1 in Strata dark. `--hint` is a hue of its own in each palette, never
   the live colour: LIVE means a ranked board (D-077). Its value is Design's
   tint (D-273): blue, the one hue with no other meaning on the board. */
.tile.hint-mark { box-shadow: inset 0 0 0 3px var(--hint), 2px 2px 0 color-mix(in srgb, var(--tile-bg-blk) 45%, var(--tile-text)); }
.tile.free.hint-mark,
.tile.free.hint-mark:hover { background: var(--hint); }
.pocket-slot.hint-mark > i.pocket-tile { background: var(--hint); }
.hint-o {
  position: absolute;
  top: 2px;
  left: 2px;
  z-index: 3; /* above a rare tile's flags (z 1) and its letter (z 2) */
  min-width: 15px;
  height: 15px;
  padding: 0 3px;
  border-radius: 8px;
  display: flex;
  align-items: center;
  justify-content: center;
  background: var(--tile-text);
  color: var(--tile-bg-free);
  font: 700 0.68rem var(--font-mono);
  pointer-events: none;
  /* Scale only, on badgeBeat's 1.6s: a transform changes no layout (D-137). */
  animation: hintBeat 1.6s ease-in-out infinite;
}
@keyframes hintBeat {
  0%, 100% { transform: scale(1); }
  50% { transform: scale(1.25); }
}
@media (prefers-reduced-motion: reduce) {
  .hint-o { animation: none; }
}
/* Still idle after "I spy…": the Hint reminds itself, playfully, once
   (D-261 item 5). A transform changes no layout. */
@keyframes hintWiggle {
  0%, 100% { transform: rotate(0); }
  20% { transform: rotate(-8deg); }
  40% { transform: rotate(7deg); }
  60% { transform: rotate(-5deg); }
  80% { transform: rotate(3deg); }
}
#hint-btn.wiggle { animation: hintWiggle 0.7s ease-in-out 2; }
@media (prefers-reduced-motion: reduce) {
  #hint-btn.wiggle { animation: none; }
}

/* The day card (S15-17, Design's round 2 §4): on the day sheet, above the
   action. The picture is a fixed 124px band, so a tall layout shrinks into it
   rather than pushing the action down ("every inch of vertical space is
   sacred", D-261 item 1). */
.day-card {
  margin: 6px 0 4px;
  border: 1px solid var(--border);
  border-radius: var(--radius);
  background: var(--surface-2);
  overflow: hidden;
  text-align: left;
}
.dc-thumb {
  position: relative;
  height: 124px;
  background: radial-gradient(ellipse at 50% 45%, var(--felt-2, var(--felt)), var(--felt) 70%);
  border-bottom: 1px solid var(--border);
  overflow: hidden;
}
.dc-t {
  position: absolute;
  display: flex;
  align-items: center;
  justify-content: center;
  border-radius: 2px;
  background: var(--tile-bg-free);
  box-shadow: 1px 1px 0 var(--tile-shadow);
  font-style: normal;
  font-family: var(--font-mono);
  font-weight: 700;
  color: var(--tile-text);
  line-height: 1;
}
.dc-t.blk { background: var(--tile-bg-blk); color: var(--tile-text-blk); box-shadow: none; }
.dc-row {
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: 8px;
  padding: 6px 10px 2px;
  font-size: 0.78rem;
}
.dc-diff { display: inline-flex; align-items: center; gap: 6px; font-weight: 700; }
.dc-pips { display: inline-flex; gap: 2px; }
.dc-pips i { width: 6px; height: 10px; border-radius: 1px; background: var(--border); }
.dc-pips i.on { background: var(--text); }
.dc-free { display: inline-flex; align-items: center; gap: 4px; color: var(--text-dim); }
.dc-free b { color: var(--text); }
/* The free-tile symbol: a small bright tile, the board's own sign of "free". */
.dc-free-ico {
  width: 11px;
  height: 11px;
  border-radius: 2px;
  background: var(--tile-bg-free);
  box-shadow: 1px 1px 0 var(--tile-shadow);
}
.dc-bag { padding: 4px 10px 7px; }
.dc-bb { position: relative; display: flex; height: 7px; border-radius: 4px; overflow: hidden; background: var(--border); }
.dc-bb i { display: block; height: 100%; }
.dc-bb i.v { background: var(--tile-bg-free); }
.dc-bb i.c { background: var(--tile-bg-blk); }
/* Rare letters wear the rare accent: this IS the rare letters (D-077). */
.dc-bb i.r { background: var(--rare-glow); }
.dc-bb u { position: absolute; top: -1px; bottom: -1px; width: 2px; background: var(--text); }
.dc-bl { display: flex; flex-wrap: wrap; gap: 2px 10px; margin-top: 3px; font-size: 0.68rem; color: var(--text-dim); }
.dc-bag-name { margin-left: auto; color: var(--text); }

/* "Words in reach" (S15-16), in the empty word bar: Design's round 2. It
   steps aside while a message holds the same band (#message-box is absolute
   over it, D-077). */
.bar-reach {
  flex: 1;
  text-align: center;
  font-size: 0.68rem;
  font-weight: 600;
  line-height: 1.2;
  color: var(--text);
}
.bar-reach.dim { color: var(--text-dim); font-weight: 500; }
/* I spy (D-270) takes the same place, in the voice of a game, not a label. */
.bar-reach.spy { font-style: italic; }
#word-bar.msg-up .bar-reach { visibility: hidden; }
.hint-n { font-style: normal; margin-left: 4px; opacity: 0.75; }

/* Never scrolls (nobody scrolls mid-game — Andres 2026-06-12): when the word
   outgrows the row, tiles shrink and then overlap like cards (fitWordBar). */
#word-bar {
  display: flex;
  gap: 5px;
  flex-wrap: nowrap;
  overflow: hidden;
  min-width: 0;
  padding: 4px 2px 6px;
  /* Reserve the FULL occupied height (50px tile + 10px padding) — anything
     less and the first pick grows the bar, hopping the whole layout (S6 bug:
     "assume there is always a tile"). */
  min-height: 60px;
  flex: 1;
  align-items: center;
}

.bar-tile {
  position: relative;
  width: 40px;
  height: 50px;
  flex-shrink: 0;
  background: var(--bar-tile-bg);
  border: 1px solid var(--bar-tile-bdr);
  border-radius: var(--radius);
  display: flex;
  flex-direction: column;
  align-items: center;
  justify-content: center;
  box-shadow: 1px 2px 4px rgba(0,0,0,0.4);
}

.bar-tile.sm { width: 32px; height: 40px; }
.bar-tile.sm .bl { font-size: 0.85rem; }
.bar-tile.sm .bpts { display: none; }

.bar-tile .bl { font-family: var(--font-mono); font-size: 1.1rem; font-weight: 700; color: var(--tile-text); line-height: 1; }
.bar-tile .bpts { font-family: var(--font-mono); font-size: 0.5rem; font-weight: 600; color: var(--tile-pts-clr); position: absolute; bottom: 3px; right: 4px; }

/* Speed streak (R4, ported from the prototype): tiles picked at ×2+ glow
   while the window is alive; the gate is re-derived server-side at submit. */
.bar-tile.streaked {
  background: color-mix(in srgb, var(--gold) 30%, var(--bar-tile-bg));
  border-color: var(--gold);
  box-shadow: 0 0 6px color-mix(in srgb, var(--gold) 50%, transparent);
}
/* What the multipliers ADD to this letter (Andres 2026-09-08). Top-LEFT: the
   streak's ×N owns top-right and the base value owns bottom-right, so this is
   the only corner left — and the three numbers then read as gain, reason, base.
   Bar tiles are a fixed warm cream in every theme, so these literals sit beside
   .bpts's own; they are not tokens that got away. */
.bar-tile .bplus {
  font-family: var(--font-mono);
  font-size: 0.5rem;
  font-weight: 700;
  color: var(--tile-pts-clr);
  position: absolute;
  top: 2px;
  left: 4px;
}
/* The narrow layout already drops the base value; a gain without its base is
   worse than neither. */
.bar-tile.sm .bplus { display: none; }
.bar-tile .bmult {
  font-family: var(--font-mono);
  font-size: 0.5rem;
  font-weight: 700;
  color: var(--accent);
  position: absolute;
  top: 2px;
  right: 4px;
}
.wi-bonus { font-size: 0.72rem; color: var(--gold); font-weight: 600; }

/* min-width 0 + shrink: a long word + 🔥 streak preview must squeeze (wrap/
   clip) inside the controls column, never widen it past the viewport —
   the buttons alone set the width (they don't shrink). */
#word-controls {
  display: flex;
  flex-direction: column;
  align-items: stretch;
  gap: 5px;
  flex-shrink: 1;
  min-width: 0;
}
#word-controls .btn-row { flex-shrink: 0; }

/* Strictly ONE line (nowrap): a wrapped 🔥-bonus line used to grow the word
   area and steal the board's vertical budget (mobile Y-overflow, 2026-06-13).
   The WORD ellipsizes under pressure; points + bonus are pinned (flex-shrink:0)
   so they never wrap or clip.
   It hangs from the word area's top-right edge, mirroring the pocket at the
   top-left (D-277, Andres: "The point calculation does not fit next to the
   buttons anymore"). With the Hint in the row it had been squeezed to a
   sliver. Absolute, so it costs the layout nothing; never wider than the room
   right of the pocket's two slots (6 + 2 x 44 + 4, and a 10px gap), nor than
   INFO_W in lip.ts, which keeps the board's fit clear of it. */
#word-info {
  position: absolute;
  right: 10px;
  bottom: calc(100% + 2px);
  z-index: 3;
  max-width: min(calc(100% - 118px), 250px);
  display: flex;
  justify-content: flex-end;
  gap: 8px;
  align-items: baseline;
  flex-wrap: nowrap;
  height: 18px;
  min-width: 0;
  overflow: hidden;
  pointer-events: none;
}
/* The date steps aside while a word is in hand, as it does for an armed slot,
   and the line steps aside for "Choose a tile", which runs along the same
   edge from the left. */
#board-container.has-word .board-date { visibility: hidden; }
#board-container.is-arming + #word-area #word-info { visibility: hidden; }
.wi-word {
  font-family: var(--font-mono);
  font-size: 0.9rem;
  font-weight: 700;
  letter-spacing: 0.1em;
  min-width: 0;
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
}
/* The spelled word is the always-readable truth (tiles may card-overlap) —
   long words step the font down before the ellipsis bites. */
.wi-word.sm { font-size: 0.72rem; letter-spacing: 0.06em; }
.wi-word.xs { font-size: 0.68rem; letter-spacing: 0.03em; }
.wi-pts { font-size: 0.75rem; color: var(--gold); font-weight: 600; flex-shrink: 0; white-space: nowrap; }
.wi-bonus { flex-shrink: 0; white-space: nowrap; }
/* The run-count toggle used two characters where it has one meaning. It is a
   chevron that TURNS, which is also why the transition can exist at all. */
.run-caret { display: inline-flex; transition: transform 0.15s ease; transform: rotate(-90deg); }
.run-caret.up { transform: rotate(90deg); }
.run-caret [data-icon], .run-caret svg { width: 0.9em; height: 0.9em; }

/* Wrap, always (Andres 2026-09-07): .btn has white-space: nowrap, so an
   unwrapped row can only shrink to min-content and then overflow on X. The
   <560px column rule below never covered 561-700px, which is exactly where
   the finish sheet's three buttons broke out. Shared by the language and
   tutorial overlays, which get the same fix for free. */
.btn-row { display: flex; flex-wrap: wrap; gap: 6px; }
.btn-row.center { justify-content: center; margin-top: 12px; }
/* The first finish card's way on (D-169): first in the row, whatever the DOM
   order, because it is the one the tour wants taken. */
.btn-row .end-continue { order: -1; }

/* The message FLOATS over the word row (Andres 2026-09-07: it "creates a
   movement appearing and disappearing"). It used to be a flow sibling reserving
   21px, and a two-line message grew that box and shoved the whole board up
   (#word-area is flex-shrink: 0, so #board-container's flex: 1 pays for it).
   Absolute means the layout can never move; the 21px goes back to the board.
   The rules MUST live on the id, not a class: flash() does
   `messageEl.className = ...` on every message and would wipe a class. */
#message-box {
  position: absolute;
  left: 10px;
  right: 10px;
  /* Over the TILE band, not the button row: #word-bar reserves 60px at the top
     of the area in both layouts (row on desktop, column on mobile), so anchoring
     here keeps the message off Undo/Submit. Cleared on the next pick, so it is
     never competing with tiles for the same space. */
  top: 8px;
  height: 30px;
  display: flex;
  align-items: center;
  justify-content: center;
  margin: 0;
  min-height: 0;
  font-size: 0.72rem;
  letter-spacing: 0.03em;
  text-align: center;
  pointer-events: none;
  text-shadow: 0 1px 3px var(--panel), 0 0 6px var(--panel);
}

/* Narrow screens: stack the word area — tiles get the full row, buttons move
   below (mobile feedback 2026-06-12: controls were covering the tile bar). */
@media (max-width: 560px) {
  #word-row { flex-direction: column; align-items: stretch; gap: 2px; }
  /* Same full-height reservation as the desktop rule — no first-pick hop. */
  #word-bar-wrap { width: 100%; }
  #word-bar { width: 100%; min-height: 60px; }
  #word-controls {
    flex-direction: row;
    align-items: center;
    justify-content: space-between;
    gap: 8px;
  }
  .btn-row { flex-shrink: 0; }
  .btn { padding: 8px 10px; }
  /* The buttons share the whole row, evenly (D-277, Andres: "On mobile the
     buttons at the bottom could be spaced evenly to cover whole bottom
     area"). Equal shares from a zero basis; a label never shrinks below its
     own width, so at 320px Finish keeps its icon and the rest give way. */
  #word-controls .btn-row { flex: 1; }
  #word-controls .btn-row .btn { flex: 1 1 0; padding-left: 6px; padding-right: 6px; }
}
.msg-ok   { color: var(--green); }
.msg-err  { color: var(--red); }
.msg-warn { color: var(--gold); }
.msg-info { color: var(--blue); }

/* ── Buttons ── */
.btn {
  padding: 8px 12px;
  border: 1px solid transparent;
  border-radius: var(--radius);
  font-family: var(--font-mono);
  font-size: 0.75rem;
  font-weight: 600;
  cursor: pointer;
  letter-spacing: 0.04em;
  transition: background 0.12s, border-color 0.12s, transform 0.1s;
  white-space: nowrap;
  min-height: 36px;
}

.btn:active { transform: scale(0.96); }
.btn:disabled { opacity: 0.35; cursor: not-allowed; transform: none; }

.btn-primary { background: var(--accent); border-color: var(--accent-bdr); color: var(--accent-ink); }
/* The lit Submit's other state (S15-5): the letters spell no word yet. Dim
   but NOT disabled, because a dim tap still answers why, for free. */
.btn-primary.is-dim { background: var(--surface-2); border-color: var(--border); color: var(--text-dim); }
.btn-primary:not(:disabled):hover { filter: brightness(1.12); }
/* The rim is DERIVED, not hand-picked (D-145). `--border` is a surface
   separator — tuned to divide two panels without drawing attention, which in
   the two dark palettes puts it a few points off `--surface-2` — and a button
   that inherits that brief has no visible edge. Pulling it a quarter of the
   way toward the palette's own ink lightens it in a dark theme and darkens it
   in a light one, so a palette added later gets a rim by construction rather
   than by someone noticing a sixth hard-coded colour. */
.btn-secondary {
  background: var(--surface-2);
  border-color: color-mix(in srgb, var(--text) 28%, var(--border));
  color: var(--text);
}
.btn-secondary:not(:disabled):hover { filter: brightness(1.15); }
.btn-warn {
  background: color-mix(in srgb, var(--gold) 18%, transparent);
  border-color: color-mix(in srgb, var(--gold) 45%, transparent);
  color: var(--tint-ink);
}
.btn-warn:not(:disabled):hover { background: color-mix(in srgb, var(--gold) 28%, transparent); }
.btn-ghost { background: transparent; border-color: var(--border); color: var(--text-dim); }
/* A button that SPENDS must not look like one that does not (Andres
   2026-09-09). Provisional treatment — the price's own gold, so the cost reads
   before the label does. The full convention (always free / free for now /
   priced / watch-an-ad) is out for design: docs/acquisition-buttons-brief.md. */
.btn.btn-cost { border-color: var(--gold); box-shadow: inset 0 0 0 1px color-mix(in srgb, var(--gold) 35%, transparent); }
.btn-ghost:hover { color: var(--text); }

/* ── Score pop ── */
.score-pop {
  position: absolute;
  left: 50%;
  top: -4px;
  transform: translateX(-50%);
  font-family: var(--font-mono);
  font-size: 1rem;
  font-weight: 700;
  color: var(--gold-lt);
  animation: scoreFly 1.1s ease-out forwards;
  pointer-events: none;
  white-space: nowrap;
  text-shadow: 0 0 8px rgba(230,190,60,0.6);
  z-index: 100; /* pops anchor inside buttons (D-035) — fly above neighbours */
}

/* Penalty pops (R1 rules: wrong-word / undo costs) read as losses. */
.score-pop.neg {
  color: var(--red);
  text-shadow: 0 0 8px rgba(224,106,90,0.6);
}

/* ── Overlay bands ── */
/* z-bands (D-073, retiring D-067/D-070's panel band). THIS TABLE IS THE
   CONTRACT — a new overlay picks a band, or the discipline erodes:
         0  BASE, in normal flow, exactly one visible, owned by `screen`:
            #home-screen (the Quarry Wall) · #board-container + #word-area
            (the dig) · #heap-screen (the day's results) · the .page-screen
            pages (keys, profile, words, shop). A base is a PLACE: it has its
            own top bar, body and footer, and you leave it through the nav.
            Overlays never close a base.
     25000  SHEET — #day-sheet, #finish-sheet, #stuck-overlay. Bottom-anchored,
            at most one visible. A sheet is a DECISION ABOUT THE THING BENEATH
            (start this? bank this?), which is why it shows its context through
            a scrim. Anything you browse, tab through or scroll is a base.
     30500  #nav-bar — deliberately ABOVE the sheets: the nav is the one thing
            that must never become unreachable. Hidden while digging.
     31000  #game-header — above the nav so the key pill and ⚙ stay clickable.
     32000  UTILITY — #faq-overlay, #feedback-overlay, #lang-overlay,
            #tutlvl-overlay, #more-menu: stack over ANYTHING incl. the header;
            their own Close (or the menu's scrim) is the only exit.
            De-facto single-occupancy: every ⋯ row closes the menu before it
            opens its card, and the lang note only appears mid-dig.
     32001  #build-tag (dev aid, pointer-events: none). */
.end-overlay {
  position: fixed;
  inset: 0;
  background: var(--scrim);
  display: flex;
  align-items: center;
  justify-content: center;
  z-index: 30000;
  /* leave the (raised) header usable above the card */
  padding: 64px 16px 16px;
}

/* Tight screens: card actions stack to a column (Andres 2026-06-12 — the
   side-by-side trio overflowed). Word-area .btn-row is unaffected. */
@media (max-width: 560px) {
  .end-card .btn-row { flex-direction: column; align-items: stretch; }
  .end-card .btn-row .btn { width: 100%; }
}

.end-card {
  background: var(--panel);
  border: 1px solid var(--gold);
  border-radius: 8px;
  padding: 20px 22px;
  max-width: var(--measure);
  width: 100%;
  max-height: 86dvh;
  /* Y only, and never chained to the page behind the scrim. */
  overflow-y: auto;
  overflow-x: hidden;
  overscroll-behavior: contain;
  scrollbar-gutter: stable;
  text-align: center;
  box-shadow: 0 8px 40px rgba(0,0,0,0.6);
}

/* Themed scrollbars (Andres 2026-09-07 — the card scrolled with the host's
   default bar over a sandstone panel). One treatment for all three
   scrollers so a scroll looks the same wherever it happens. */
.end-card, .heap-body, .page-body, #home-screen {
  scrollbar-width: thin;
  scrollbar-color: color-mix(in srgb, var(--gold) 55%, var(--border)) transparent;
}
.end-card::-webkit-scrollbar,
.heap-body::-webkit-scrollbar,
.page-body::-webkit-scrollbar,
#home-screen::-webkit-scrollbar { width: 8px; }
.end-card::-webkit-scrollbar-track,
.heap-body::-webkit-scrollbar-track,
.page-body::-webkit-scrollbar-track,
#home-screen::-webkit-scrollbar-track { background: transparent; }
.end-card::-webkit-scrollbar-thumb,
.heap-body::-webkit-scrollbar-thumb,
.page-body::-webkit-scrollbar-thumb,
#home-screen::-webkit-scrollbar-thumb {
  background: color-mix(in srgb, var(--gold) 45%, var(--border));
  border-radius: var(--radius);
}
.end-card::-webkit-scrollbar-thumb:hover,
.heap-body::-webkit-scrollbar-thumb:hover,
.page-body::-webkit-scrollbar-thumb:hover,
#home-screen::-webkit-scrollbar-thumb:hover { background: var(--gold); }

.end-card h2 { font-family: var(--font-title); color: var(--gold); font-size: 1.2rem; letter-spacing: 0.05em; }

/* The one CTA worth the rare accent (D-077): on a LIVE board another run still
   moves your standing. Archive gets the common palette and a plain sentence —
   the colour is the message, so it is spent only where engagement is real. */
.btn.cta-live {
  border-color: var(--rare-glow);
  box-shadow: 0 0 10px color-mix(in srgb, var(--rare-glow) 45%, transparent);
}

/* ── 🎓 Interactive tutorial level (D-052) — a fixed 10-tile board played
   before the first real dig. Reuses the .tile look; its own small board box
   and letter strip. A utility: it stacks over the wall and its sheet. ── */
/* Utility band (D-067): over panels AND the header. */
#faq-overlay, #feedback-overlay, #lang-overlay, #names-overlay, #join-overlay, #tutlvl-overlay, #more-menu { z-index: 32000; }
/* The names window (D-182). The word the player tried, in the word bar's own
   tiles, struck through — the one thing on it that is theirs — then the rule,
   then the best-known names of every kind, the tried kind first. The strike
   is the --red a refusal already wears; the chips are ordinary stone. */
.names-card { max-width: 400px; }
.names-word { display: flex; gap: 3px; width: fit-content; margin: 2px auto 10px; position: relative; }
.names-word .bar-tile { width: 28px; height: 34px; }
.names-word .bar-tile .bl { font-size: 0.95rem; }
.names-word::after {
  content: ""; position: absolute; left: 50%; top: 50%;
  width: calc(100% + 12px); height: 2px; border-radius: 1px;
  transform: translate(-50%, -50%) rotate(-8deg); background: var(--red);
}
.names-list { display: grid; gap: 7px; margin: 10px 0 4px; text-align: left; }
.names-row { display: grid; grid-template-columns: 6.6em 1fr; gap: 8px; align-items: baseline; }
.names-grp {
  font-size: 0.68rem; letter-spacing: 0.06em; text-transform: uppercase; color: var(--text-dim);
}
.names-row.is-yours .names-grp { color: var(--text); font-weight: 700; }
.names-chips { display: flex; flex-wrap: wrap; gap: 4px; }
.names-chip {
  font-family: var(--font-mono); font-weight: 700; font-size: 0.68rem; letter-spacing: 0.04em;
  padding: 2px 6px; border-radius: 4px; background: var(--surface-2); color: var(--text-dim);
  border: 1px solid var(--border);
  text-decoration: line-through; text-decoration-thickness: 1.5px; text-decoration-color: var(--red);
}
.names-row.is-yours .names-chip { color: var(--text); }
.names-chip.is-tried { border-color: var(--red); }
/* Words that don't count (D-197): the names window, with a tab per kind. The
   strip is the Words page's own (.heap-tabs.word-tabs), so five labels scroll
   on a phone exactly as they do there. A group with no heading is chips
   alone; a wrong form wears its right one beside it, un-struck. */
/* Longhands: .heap-tabs centres itself under the shared measure with auto
   side margins, which a shorthand would reset. */
.nw-tabs.heap-tabs { margin-top: 2px; margin-bottom: 12px; }
.nw-row { text-align: left; }
.nw-row.is-yours .names-chip { color: var(--text); }
.nw-pair { display: inline-flex; align-items: baseline; gap: 4px; margin-right: 6px; }
.nw-right {
  font-family: var(--font-mono); font-weight: 700; font-size: 0.68rem; letter-spacing: 0.04em; color: var(--text);
}
.nw-right::before { content: "→ "; font-weight: 400; color: var(--text-dim); }
/* The FAQ's way in, under the rule it expands. */
.faq-notwords { display: block; margin: 10px auto 0; }
/* The Join card (D-188). Two numbered steps: the one the game can do (Join, a
   real button) and the one it cannot (the bell), drawn as Reddit shows it —
   beside a "Joined" pill — so the player knows it again on the community page. */
.join-card { max-width: 400px; }
.join-steps { list-style: none; margin: 12px 0 6px; padding: 0; display: grid; gap: 14px; text-align: left; }
.join-step { display: grid; grid-template-columns: 26px 1fr; gap: 10px; align-items: start; }
.join-n {
  width: 26px; height: 26px; border-radius: 50%;
  display: inline-flex; align-items: center; justify-content: center;
  font-weight: 700; font-size: 0.8rem; color: var(--text);
  background: var(--surface-2); border: 1px solid var(--border);
}
.join-body { display: grid; gap: 6px; justify-items: start; }
.join-line { margin: 0; font-size: 0.82rem; line-height: 1.4; color: var(--text-dim); }
.join-line b { color: var(--text); }
.join-err { color: var(--red); }
.join-done {
  display: none; align-items: center; gap: 6px; min-height: 44px;
  font-weight: 700; color: var(--gold);
}
.join-done [data-icon] { width: 1.1em; height: 1.1em; }
.join-card.is-joined .join-btn { display: none; }
.join-card.is-joined .join-done { display: inline-flex; }
/* Reddit's two controls as a picture to recognise, never a control to press:
   an outlined "Joined" pill and the bell in a round button, ringed in gold
   where the tap goes. */
.join-pic { display: inline-flex; align-items: center; gap: 8px; margin: 2px 0; pointer-events: none; }
.join-pill {
  padding: 5px 14px; border-radius: 999px; border: 1.5px solid var(--text-dim);
  font-size: 0.8rem; font-weight: 700; color: var(--text);
}
.join-bell {
  position: relative; width: 34px; height: 34px; border-radius: 50%;
  display: inline-flex; align-items: center; justify-content: center;
  border: 1.5px solid var(--text-dim); color: var(--text);
}
.join-bell [data-icon] { width: 18px; height: 18px; }
.join-bell::after { content: ""; position: absolute; inset: -5px; border-radius: 50%; border: 2px solid var(--gold); }
.join-fine { margin: 8px 0 0; font-size: 0.72rem; color: var(--text-dim); }
/* The Dictionary tab's verdict for a name wears the same chips, inline. */
.dict-verdict.names .names-chips { margin-top: 6px; }
/* An explained miss opens its tab of the window (D-197): its own line. */
.dict-verdict .dict-why { display: flex; margin-top: 6px; }
/* What a place-lookalike means (D-186) — "a herb, not the city". Its own line
   in the verdict; beside the word in a list row, quieter and never bold. */
.dict-verdict .dict-gloss { display: block; margin-top: 3px; font-size: 0.72rem; color: var(--text-dim); }
.dict-row .dict-gloss {
  margin-left: 8px; font-weight: 400; letter-spacing: 0; font-size: 0.68rem; color: var(--text-dim);
}
.tutlvl-card { position: relative; max-width: 460px; }
.tutlvl-skip { position: absolute; top: 6px; right: 6px; }
#tutlvl-board {
  position: relative;
  /* Its own stacking context, like #board and the poster's #mound: the tiles
     carry the board's z-index scale (shared/paint-order.ts), and without this
     they would stack against the tutorial card's own text and buttons. */
  z-index: 0;
  margin: 6px auto 4px;
  /* size set inline by tutorial-level.ts from the tile geometry */
}
.tutlvl-bar {
  display: flex;
  justify-content: center;
  gap: 4px;
  min-height: 40px;
  margin: 8px 0;
}
.tutlvl-bar .bar-tile { width: 34px; height: 42px; }
.tutlvl-bar .bar-tile .bl { font-size: 1rem; }
#tutlvl-hint { min-height: 2.4em; }
#tutlvl-hint b { color: var(--gold-lt); letter-spacing: 0.12em; }

/* Idle guidance (D-052, a finger since D-168): after a short idle the next
   thing to tap gets a ring and a finger whose tip rests on it — a tile while
   the word is being built, Submit once it is complete, Undo off the path. */
.tile.tut-target {
  outline: 2px solid var(--gold-lt);
  outline-offset: 1px;
  box-shadow: 2px 3px 0 var(--tile-shadow), 0 0 8px 2px var(--gold-lt);
}
#tutlvl-submit,
#tutlvl-undo { position: relative; } /* a host for the finger */
.btn.tut-target-btn {
  outline: 2px solid var(--gold-lt);
  outline-offset: 2px;
}

/* ── The tutorial's finish (D-177, Design's first-days delivery) ───────────
   The two words rise back out of the ground as the player's own tiles, WORD
   over GAMES in the order they were dug, settle on a ledge, and the ledge
   throws a little stone dust. About a second, one shot, then at rest; only
   transform and opacity animate. Adapted: the stage is laid OVER the play
   area (board, word bar, Undo/Submit), which keeps its height with its own
   contents hidden, so the card does not re-lay out and the button never moves;
   and the dust hangs off the ledge itself rather than off the stage's foot,
   because this stage is the whole play area, taller than Design's. Tiles are
   the real .tile, sized inline by the client — nothing here touches width,
   height or font-size. */
.tutlvl-area { position: relative; }
.tutlvl-area.is-done > :not(.tfin-stage) { visibility: hidden; }
.tfin-stage {
  position: absolute; inset: 0;
  display: flex; flex-direction: column; align-items: center; justify-content: center;
  gap: 7px; pointer-events: none;
}
.tfin-row { display: flex; justify-content: center; gap: 5px; }
.tfin-stage .tile {
  position: relative; left: auto; top: auto; flex-shrink: 0;
  animation: tfin-rise 0.34s cubic-bezier(0.2, 0.8, 0.3, 1) both;
}
.tfin-row:nth-child(1) .tile:nth-child(2) { animation-delay: 0.05s; }
.tfin-row:nth-child(1) .tile:nth-child(3) { animation-delay: 0.1s; }
.tfin-row:nth-child(1) .tile:nth-child(4) { animation-delay: 0.15s; }
.tfin-row:nth-child(2) .tile:nth-child(1) { animation-delay: 0.33s; }
.tfin-row:nth-child(2) .tile:nth-child(2) { animation-delay: 0.38s; }
.tfin-row:nth-child(2) .tile:nth-child(3) { animation-delay: 0.43s; }
.tfin-row:nth-child(2) .tile:nth-child(4) { animation-delay: 0.48s; }
.tfin-row:nth-child(2) .tile:nth-child(5) { animation-delay: 0.53s; }
@keyframes tfin-rise {
  0% { opacity: 0; transform: translateY(22px) scale(0.9); }
  62% { opacity: 1; transform: translateY(-5px) scale(1.03); }
  100% { opacity: 1; transform: none; }
}
/* the ledge: the quarry floor the words now stand on */
.tfin-ledge {
  width: 78%; height: 2px; border-radius: 1px; margin-top: 2px;
  background: var(--gold); opacity: 0.55;
  transform-origin: center; animation: tfin-ledge 0.3s ease-out 0.3s both;
}
@keyframes tfin-ledge { from { transform: scaleX(0); } to { transform: scaleX(1); } }
/* the dust: eight chips of stone and metal off the ledge, once. A zero-size
   point just under the ledge (the negative margin cancels the stage's gap). */
.tfin-dust { position: relative; width: 0; height: 0; margin-top: -7px; }
.tfin-chip {
  position: absolute; left: -3px; top: -3px; width: 6px; height: 6px; border-radius: 1px; opacity: 0;
  background: var(--tile-bg-blk);
  animation: tfin-chip 0.46s cubic-bezier(0.15, 0.7, 0.35, 1) 0.56s both;
}
.tfin-chip:nth-child(even) { background: var(--gold); width: 4px; height: 4px; }
.tfin-chip:nth-child(1) { --dx: -118px; --dy: -26px; --r: -140deg; }
.tfin-chip:nth-child(2) { --dx: -84px; --dy: -44px; --r: 90deg; }
.tfin-chip:nth-child(3) { --dx: -52px; --dy: -16px; --r: -60deg; }
.tfin-chip:nth-child(4) { --dx: -24px; --dy: -38px; --r: 160deg; }
.tfin-chip:nth-child(5) { --dx: 26px; --dy: -34px; --r: -120deg; }
.tfin-chip:nth-child(6) { --dx: 56px; --dy: -18px; --r: 70deg; }
.tfin-chip:nth-child(7) { --dx: 88px; --dy: -42px; --r: -80deg; }
.tfin-chip:nth-child(8) { --dx: 120px; --dy: -22px; --r: 130deg; }
@keyframes tfin-chip {
  0% { opacity: 1; transform: translate(0, 0) rotate(0); }
  70% { opacity: 1; }
  100% { opacity: 0; transform: translate(var(--dx), var(--dy)) rotate(var(--r)); }
}
@media (prefers-reduced-motion: reduce) {
  .tfin-stage .tile, .tfin-ledge { animation: none; }
  .tfin-chip { display: none; }
}
.tut-point {
  position: absolute;
  /* The glyph's fingertip is drawn near (11, 2) of 24: these place that point
     on the centre of whatever hosts it. */
  top: 50%;
  left: 50%;
  /* DRAWN, not an emoji (S10d): it is the one glyph a first-time player is
     asked to follow, so a tofu box here breaks the onboarding rather than
     looking untidy. Sized in em so it tracks the tile it points at. */
  width: 1.25em;
  height: 1.25em;
  display: flex;
  pointer-events: none; /* clicks pass through to the tile beneath */
  color: var(--text);
  filter: drop-shadow(0 0 1.5px var(--panel)) drop-shadow(0 2px 2px rgba(0, 0, 0, 0.45));
  transform: translate(-46%, -9%);
  animation: tutPointTap 0.9s ease-in-out infinite;
  z-index: 1000001;
}
.btn .tut-point { width: 2.2em; height: 2.2em; }
.tut-point svg { width: 100%; height: 100%; }
/* A tap, not a wobble: the finger presses in and lifts. */
@keyframes tutPointTap {
  0%, 100% { transform: translate(-46%, -9%); }
  45% { transform: translate(-46%, -9%) translateY(5px) scale(0.94); }
}
@media (prefers-reduced-motion: reduce) {
  .tut-point { animation: none; }
}

/* ── ❓ FAQ + feedback modal (D-046) ── */
.faq-card { text-align: left; }
.faq-card h2 { text-align: center; margin-bottom: 8px; }
.faq-body { font-size: 0.8rem; line-height: 1.45; }
.faq-body p { margin: 6px 0; }
.faq-body ul { margin: 6px 0 6px 18px; }
.faq-body li { margin: 4px 0; }
.faq-body b { color: var(--gold-lt); }
.faq-credit { color: var(--text-dim); font-size: 0.72rem; margin-top: 10px; }
/* On its own card since D-095, the feedback box needs no rule above it. */
.faq-fb { margin-top: 4px; }
.faq-fb-head { display: flex; align-items: center; justify-content: flex-end; font-size: 0.82rem; margin-bottom: 6px; }
#faq-text {
  width: 100%;
  resize: vertical;
  background: var(--surface-2);
  color: var(--text);
  border: 1px solid var(--border);
  border-radius: var(--radius);
  font-family: var(--font-mono);
  font-size: 0.78rem;
  padding: 7px 9px;
  margin-bottom: 8px;
}
#faq-text:focus { outline: none; border-color: var(--accent-bdr); }
/* Beside Send while there is room, its own line when not; a long status
   (the thanks names where a reply goes, D-191) wraps inside its box. */
.faq-status { flex: 1 1 12em; min-width: 0; font-size: 0.72rem; color: var(--text-dim); align-self: center; }
.faq-status.ok { color: var(--green); }
.faq-status.err { color: var(--red); }
.end-stats { font-size: 0.78rem; color: var(--text-dim); margin-bottom: 10px; }

/* The Words page's board tabs (D-119). A strip, not a nav: these switch what
   the page shows, where the nav switches which page you are on.

   `.heap-tabs` gives it the shared geometry; what remains here is the
   centring, kept from D-121: the labels overflow a 320px phone, and plain
   `center` would push the first tab past the scrollable start where it cannot
   be reached. `safe center` falls back to start exactly then, and a browser
   that does not know it keeps the plain one. Both, in that order, or the
   fallback is the broken one. */
.word-tabs {
  display: flex; gap: 4px; overflow-x: auto; scrollbar-width: none;
  justify-content: center;
  justify-content: safe center;
}
.word-tabs::-webkit-scrollbar { display: none; }
/* The Words page wears the RESULTS page's tab (D-123). Andres: the two were
   "too different" — and they were, a pill strip on one page and a bordered
   segmented row on the other, which reads as two apps rather than two pages.
   `.heap-tabs`/`.heap-tab` now do the work on both; the only local change is
   the room five tabs need where Results has four.

   D-123 shipped the CLASSES and not the computed style: `.word-tabs button`
   and `.word-tabs button.on` survived the change, and being element selectors
   they out-specified `.heap-tab` — so the round pill, the transparent border
   and (through a `font: inherit`) the loss of the mono face all stayed. They
   are deleted (D-124). A second tab style left in the sheet is a tab style
   that comes back; the guard below is now on the RULE, not the markup. */
.word-tabs.heap-tabs { margin-top: 6px; }
.word-tabs .heap-tab {
  /* A fifth tab, and "Dictionary" is the longest label in either page. The
     type floor (D-077) is 0.68rem and stays — the room comes out of the
     letter-spacing and the side padding, not the letters. */
  padding: 5px 7px;
  font-size: 0.68rem;
  letter-spacing: 0.03em;
  /* Results stretches four equal tabs; five labels do not divide a 320px row
     evenly, and "Dictionary" is the longest on either page. `1 0 auto` grows
     each tab from its OWN width (Andres): the labels stay whole, the strip
     still fills the header edge to edge, and the tabs end up deliberately
     asymmetric rather than one of them clipped to "Diction…" (G6). The basis
     is `auto` and not `0` — that is the whole difference. */
  flex: 1 0 auto;
}
/* The ‹ › of a strip too wide for its box (D-233): a finger swipes it, a
   mouse had no way to. The wrapper takes the strip's measure and margins, so
   an arrow's top and bottom are the strip's own; it lies over the strip's end
   on a fade of the panel under both, and the strip's scroll-padding keeps a
   tapped tab out from under it. src/client/tab-scroll.ts shows each arrow
   only while tabs are hidden past its end. */
.tab-scroll {
  position: relative;
  max-width: var(--measure);
  margin-top: 6px;
  margin-left: auto;
  margin-right: auto;
}
.tab-scroll.nw-scroll { margin-top: 2px; margin-bottom: 12px; }
.tab-scroll > .heap-tabs.word-tabs { margin-top: 0; margin-bottom: 0; scroll-padding-inline: 30px; }
.tab-arrow {
  position: absolute;
  top: 0;
  bottom: 0;
  z-index: 1;
  width: 30px;
  padding: 0 3px;
  border: 0;
  cursor: pointer;
  font-family: var(--font-mono);
  font-size: 1.2rem;
  font-weight: 700;
  line-height: 1;
  color: var(--text);
}
.tab-arrow.is-back { left: 0; text-align: left; background: linear-gradient(to right, var(--panel) 60%, transparent); }
.tab-arrow.is-fwd { right: 0; text-align: right; background: linear-gradient(to left, var(--panel) 60%, transparent); }

/* The caller's own row, welded under the list the way the results page pins a
   standing (D-073) — so "where am I" never needs a scroll. */
.lb-row.pinned { margin-top: 6px; border-top: 1px solid var(--line, var(--surface-2)); }

/* The notification dot (D-119). It marks NEWS, and opening the page clears
   it; a dot that visiting cannot resolve is a nag, and a nag teaches people to
   ignore every later one. Sized to read at a glance without crowding a 40px
   tap target (G6). */
.nav-i { position: relative; }
/* ══ D-122 · The badge family: news · count · claimable ════════════════════
   The old dot was gold on the nav — house furniture wearing the house colour,
   which said "something" and nothing more.

   COLOUR. A badge is neither rare nor live, so it cannot wear the ore (D-077),
   and it is not furniture, so it should not wear the metal. It gets its own
   hue: a berry that appears nowhere else in any of the five palettes. Andres
   was unsure it is the right one — it is deliberately the cheapest thing here
   to change, because the FAMILY is stepped by form, not colour.

   FORM IS THE STEP. A dot says something happened, a pill says how many, a
   metal-rimmed tag says it is yours to take. Colour alone would have made
   "new" and "claimable" a saturation argument — invisible to a good share of
   players, and unreadable at 10px to everyone else. The metal rim is the tell
   on the claim tag because the only things you claim are keys, tickets and
   awards: the things the economy already draws in metal.

   REACHABLE TODAY: news only. Count and claim are drawn and typed but nothing
   mints them yet — the D-081 discipline, where a state you cannot reach is
   still worth drawing once so the family is designed whole rather than grown
   by accident. */
:root { --badge-halo: var(--panel, var(--surface-1, var(--surface-2))); }

/* WHICH HUE THE FAMILY WEARS (D-147), live config. Andres asked to try
   survey's orange here and to keep the berry as the way back, so both modes
   name BOTH pairs and only this binds `--news`.

   The binding lives out here, after the palette, so `[data-news="accent"]`
   wins by order: both rules are (0,1,0) on the same <html>. (Before D-274 the
   survey dark block was (0,2,0), and an override inside the palettes would
   have lost exactly there.)

   In survey `--rare-glow` IS `--accent`, so the orange already means "rare
   letter" and "dig this": the dot is at 0° from both, where the berry sat
   38-49° from its nearest neighbour (D-122). */
:root { --news: var(--news-berry); --news-ink: var(--news-berry-ink); }
[data-news="accent"] { --news: var(--news-accent); --news-ink: var(--news-accent-ink); }

.wq-flag, .nav-dot {
  display: inline-flex; align-items: center; justify-content: center; flex-shrink: 0;
  box-sizing: border-box; width: 10px; height: 10px; border-radius: 50%;
  background: var(--news); color: var(--news-ink);
  font-weight: 700; font-size: 0.5rem; line-height: 1; padding: 0; border: none;
  box-shadow: 0 0 0 2px var(--badge-halo); pointer-events: none;
}
/* On the nav it hangs off the ICON, absolutely, so it can never reflow the bar
   or shift a label — the nav sits at band 30500 and must not move when one
   arrives. */
.nav-dot { position: absolute; top: -4px; right: -7px; }
.nav-i { overflow: visible; }

/* COUNT — the same object with a number in it. Grows sideways only. */
.wq-flag.flag-count, .nav-dot.count {
  width: auto; min-width: 16px; height: 16px; border-radius: 9px; padding: 0 4px;
  font-size: 0.52rem; font-variant-numeric: tabular-nums;
}
.nav-dot.count { top: -6px; right: -10px; }

/* CLAIMABLE — squarer, one size up, wearing the economy's metal as a rim. A
   rim and not a fill, so the family stays one colour and the step from "new"
   to "yours" is material rather than louder. */
.wq-flag.flag-claim, .nav-dot.claim {
  width: 19px; height: 19px; border-radius: 7px; border: 1.5px solid var(--key-metal);
}
.nav-dot.claim { top: -7px; right: -10px; }
.wq-flag.flag-claim svg, .nav-dot.claim svg {
  width: 11px; height: 11px; display: block; stroke: var(--news-ink);
  stroke-width: 2.2; stroke-linecap: round; stroke-linejoin: round; fill: none;
}
.wq-flag.flag-claim.has-n, .nav-dot.claim.has-n { width: auto; min-width: 19px; padding: 0 5px; gap: 3px; }

/* Inline use: a tab, a section head, a list row.
   CENTRED ON THE TEXT, not on the baseline (D-137). The dot is an EMPTY
   inline-flex box, so it has no baseline of its own and the browser
   synthesises one from its bottom margin edge — which is why the old
   `vertical-align: -1px` put a 10px dot's centre 2.5px above the baseline
   against a cap-height centre of ~3.9px, and why the 7px word-tab dot then
   inherited a number measured for a different size. `middle` aligns the
   MARGIN box, so the correction below is derived from the label's cap height
   and is independent of how big the dot itself is. */
.wq-flag.flag-inline { vertical-align: middle; margin-bottom: 2px; box-shadow: none; }
/* On a Words tab (D-132) the label is already at D-077's 0.68rem floor and the
   tab clips its own overflow, so the dot rides IN the text flow at 7px with a
   gap of its own. Absolute placement is not an option here: `.heap-tab` is
   `overflow: hidden` and `.word-tabs` is `overflow-x: auto`, so a dot hung over
   the corner would be clipped twice. */
.word-tabs .wq-flag.flag-inline { width: 7px; height: 7px; margin-left: 5px; }

/* ARRIVAL — one shot, on the frame it appears — and then a RESTING BEAT
   (D-137, amending D-119/D-122's "no resting pulse").

   Andres 2026-09-22: a dot that never moves reads as a hardcoded design
   element rather than as news, which is the failure the dot rule was written
   to prevent, arriving by the opposite route. What D-119 actually forbids is a
   badge that keeps DEMANDING to be tapped; slow, small and scale-only says
   temporary without asking for anything, and the dot is still cleared by
   visiting.

   TRANSFORM ONLY, deliberately: a scale changes no layout, so the beat is safe
   inside `.heap-tab { overflow: hidden }` and on the absolutely-placed nav dot,
   and it can never shunt a label or move the nav bar. It sits between the
   file's two other infinite animations in pace (badgeBeat 1.6s, calNag 3.2s). */
@keyframes wq-flag-in {
  0% { transform: scale(0.2); opacity: 0; }
  58% { transform: scale(1.16); opacity: 1; }
  100% { transform: scale(1); opacity: 1; }
}
/* ×1.4, not ×1.18 (D-143). Andres reported the dot "was not pulsating" and it
   was — invisibly: a 7px dot at ×1.18 grows by 0.6px a side, which is below
   the size at which anything reads as motion. The fix is amplitude, not a new
   mechanism, and a scale still changes no layout. */
@keyframes wq-flag-beat {
  0%, 100% { transform: scale(1); }
  50% { transform: scale(1.4); }
}
.wq-flag, .nav-dot { animation: wq-flag-beat 2.4s ease-in-out infinite; }
/* The arrival plays first and the beat takes over where it lands — one
   shorthand, so the two can never both claim the transform. */
.wq-flag.arrive, .nav-dot.arrive {
  animation:
    wq-flag-in 0.42s cubic-bezier(0.2, 1.25, 0.4, 1) both,
    wq-flag-beat 2.4s ease-in-out 0.42s infinite;
}
@media (prefers-reduced-motion: reduce) {
  .wq-flag, .nav-dot,
  .wq-flag.arrive, .nav-dot.arrive { animation: none; }
}


.end-note { font-size: 0.7rem; color: var(--text-dim); margin-top: 6px; }
.end-consent { font-size: 0.62rem; color: var(--text-dim); margin-top: 6px; }
/* "How was today's heap?" (S14-2, D-250): its own sheet, before the finish
   card. The three answers are equal columns, a scale from hard to easy, and
   none is primary. `white-space: normal` lets "About right" take two lines at
   320px rather than push the row wider than the card (the base .btn is
   nowrap). 44px keeps the tap target G6 asks for, which the base .btn's 36px
   would miss; Skip is quieter and still 40px. */
.rate-btns { display: grid; grid-template-columns: repeat(3, 1fr); gap: 8px; margin-top: 12px; }
.rate-btns .btn { min-height: 44px; white-space: normal; line-height: 1.15; padding: 6px 8px; }
.rate-why { margin: 12px 0 0; font-size: 0.82rem; color: var(--text-dim); }
.rate-skip { min-height: 40px; margin-top: 6px; }

/* ── The finish card (S15-9, D-278; Design's round 2 §2) ──────────────────
   Left-aligned rows in one column, the gap doing the separating: the badge
   and the par; the rung and the score; the ladder; what the run added; the
   goal; the rank line; the action; Share; the foot. Scoped to .fin-card,
   because .end-card is every other sheet's card too. */
.end-card.fin-card { display: flex; flex-direction: column; gap: 9px; padding: 12px 16px 8px; text-align: left; }
.fc-top { display: flex; justify-content: space-between; align-items: center; gap: 8px; }
.fc-par { font-size: 0.68rem; color: var(--text-dim); }
.fc-verdict { display: flex; justify-content: space-between; align-items: flex-end; gap: 10px; }
.fc-head { min-width: 0; }
/* The news over the rung: a first heap, a new best, a crossing (D-126). */
.fc-kicker {
  margin: 0 0 5px; font: 600 0.68rem/1 var(--font-mono); letter-spacing: 0.12em;
  text-transform: uppercase; color: var(--gold);
}
/* The rung as the heading: its mark and name in the medal's colours (D-276 f). */
.fin-card .end-medal { margin: 0; font: 700 1.6rem/1.05 var(--font-title); letter-spacing: 0; }
.fin-card .end-medal.first { color: var(--text); }
.fc-score { flex-shrink: 0; text-align: right; font: 700 1.85rem/1 var(--font-mono); font-variant-numeric: tabular-nums; color: var(--text); }
.fc-score small { display: block; margin-top: 4px; font-size: 0.68rem; font-weight: 500; color: var(--text-dim); }
/* The five steps, each at its points, this run's ringed (Design's fix 1: the
   points are ceil-safe, from boardLadder). */
.fc-ladder { display: grid; grid-template-columns: repeat(5, 1fr); gap: 4px; align-items: end; }
.fc-step { display: flex; flex-direction: column; align-items: center; gap: 4px; }
/* The bar by its class, never `.fc-step i`: a star is an <i> too (ico), and
   Design's bare-`i` rule, safe on its bare <svg> stars, drew each of ours as a
   bar (D-284). A star step carries its stars in the bar, gold in an open bar
   and cut out of a filled one, so the label is the points alone. */
.fc-bar {
  display: flex; align-items: center; justify-content: center; gap: 1px;
  width: 100%; box-sizing: border-box; border-radius: 3px;
  border: 1px solid var(--medal-gold); color: var(--medal-gold);
}
.fc-step.got .fc-bar { background: var(--medal-gold); color: var(--panel); }
.fc-step.here .fc-bar { box-shadow: 0 0 0 2px var(--panel), 0 0 0 4px var(--text); }
.fc-bar [data-icon] { width: 11px; height: 11px; vertical-align: 0; }
.fc-step span { font-size: 0.68rem; color: var(--text-dim); font-variant-numeric: tabular-nums; }
.fc-step.here span { color: var(--text); font-weight: 700; }
/* What the run added: tiles mined, the key when one landed, the new words. */
/* The row wraps (Andres, web, 2026-10-07): when the tiles, the keys and the
   door do not fit side by side (a phone, with "dig + discover · today's
   three"), the door drops to a full-width line of its own. Each cell draws its
   divider as a 1px shadow to its left and top, which the row clips at its own
   edge, so a wrapped line is divided cleanly and an edge never doubles. */
.fc-add { display: flex; flex-wrap: wrap; min-height: 48px; border: 1px solid var(--border); border-radius: 10px; overflow: hidden; }
.fc-add > * { display: flex; flex-direction: column; justify-content: center; gap: 1px; padding: 5px 9px; box-shadow: -1px -1px 0 var(--border); }
.fc-add b { font-size: 0.92rem; line-height: 1.15; font-variant-numeric: tabular-nums; color: var(--text); }
.fc-add b i { font-style: normal; font-size: 0.7rem; font-weight: 500; }
.fc-add small { font-size: 0.68rem; line-height: 1.2; color: var(--text-dim); }
/* The keys cell and the door each start from a width that holds their text
   on two lines, and share what is left. The keys cell used to keep its reason
   on one line at full width while the door started at nothing, so the best
   word broke into "BRE / AKI / NG" (Andres, web, 2026-10-07). */
.fc-add > .fc-keys { flex: 1 1 10rem; min-width: 0; flex-direction: row; align-items: center; gap: 6px; }
.fc-keys > span:last-child { display: flex; flex-direction: column; min-width: 0; }
.fc-keys b { font-family: var(--font-title); color: var(--key-metal); }
.fc-kk { display: flex; align-items: center; gap: 2px; flex-shrink: 0; }
/* The door to Words (D-138, fix 7): a real 44px target. A long best word
   wraps rather than being cut (Design's build fix 2). */
.fc-door { border: 0; }
.fc-add > .fc-door {
  flex: 1 1 9rem; flex-direction: row; align-items: center; justify-content: space-between; gap: 6px;
  min-width: 0; min-height: 44px; background: none; color: var(--text); font: inherit; text-align: left; cursor: pointer;
}
.fc-door > span { display: flex; flex-direction: column; min-width: 0; }
.fc-door small { overflow-wrap: anywhere; }
.fc-door b.dim { color: var(--text-dim); }
.fc-door svg { width: 16px; height: 16px; flex-shrink: 0; color: var(--gold); }
.fc-add > .fc-door:hover { background: var(--surface-2); }
/* The goal line, the door to Progress (D-287): a 44px target, the arrow at the
   far edge as on the door to Words. */
.fc-goal {
  display: flex; align-items: center; gap: 10px; width: 100%; min-height: 44px; margin: 0; padding: 0;
  border: 0; background: none; font: inherit; font-size: 0.78rem; line-height: 1.35; color: var(--text);
  text-align: left; cursor: pointer;
}
.fc-goal > span { flex: 1; min-width: 0; }
.fc-goal svg { width: 18px; height: 18px; flex-shrink: 0; color: var(--gold); }
.fc-goal > [data-icon]:last-child svg { width: 16px; height: 16px; }
.fc-goal b { font-weight: 700; }
/* The one line of place, with Results as a 44px tap (Design's fix 18). */
.fc-rank {
  display: flex; justify-content: space-between; align-items: center; gap: 8px;
  min-height: 44px; border-top: 1px solid var(--border); font-size: 0.74rem; color: var(--text);
}
.fc-link {
  display: inline-flex; align-items: center; gap: 4px; flex-shrink: 0; min-height: 44px; padding: 0 0 0 12px;
  border: 0; background: none; color: var(--gold); font: 700 0.72rem var(--font-mono); white-space: nowrap; cursor: pointer;
}
.fc-link svg { width: 13px; height: 13px; }
/* The level line (S15-10, Design's round 2 kd-lv): the disc, the XP this run
   paid beside what is left, and the bar under both. */
.fc-level { display: grid; grid-template-columns: auto 1fr; gap: 4px 10px; align-items: center; }
.fc-level .lv-disc { grid-row: span 2; }
.fc-lvt { display: flex; justify-content: space-between; gap: 6px; font-size: 0.72rem; }
.fc-lvt b { font-weight: 700; color: var(--text); }
.fc-lvt span { color: var(--text-dim); }
.fin-card .end-note { margin: 0; text-align: center; }
.fin-card .end-note:empty { display: none; }
/* One action, full width; the tour's Continue leads with Play again under it. */
.fin-card .fc-actions { flex-direction: column; align-items: stretch; margin: 0; }
.fin-card .fc-actions .btn { width: 100%; min-height: 48px; }
.fin-card #share-row { display: grid; grid-template-columns: 1fr 1fr; gap: 8px; margin: 0; }
.fin-card #share-row .btn { width: 100%; min-height: 44px; }
.fin-card .end-consent { margin: -4px 0 0; text-align: center; font-size: 0.68rem; }
.fc-foot { display: flex; align-items: center; gap: 8px; flex-wrap: wrap; font-size: 0.68rem; color: var(--text-dim); }
.fc-foot .fc-link { padding-left: 0; }
.fc-count { margin-left: auto; }

/* ── Leaderboard ── */
.btn-icon { min-height: 30px; padding: 4px 8px; font-size: 0.9rem; }

.lb-list {
  text-align: left;
  margin: 10px 0;
  max-height: 46dvh;
  overflow-y: auto;
  display: flex;
  flex-direction: column;
  gap: 2px;
}
/* Inside a card that already scrolls, a second scroller is a trap: the finish
   sheet's #end-neighbours would catch the drag and the card behind it would
   never move. One scroller per surface — the card owns it. */
.end-card .lb-list { max-height: none; overflow: visible; }

/* ══ D-122 · Design's board row: one row, two numbers ═══════════════════════
   Delivered as a REDEFINITION of .lb-row, which six render sites share. It ran
   behind a live switch (`data-rows`: every board, or the word boards only)
   while it was played; Andres kept it on every board (D-243), so it is the
   row now, unconditionally, and the switch, its narrow flex row and the stamp
   are gone.

   The pair was a COLUMN in Design's sheet, on the argument that two numbers
   side by side read as two competing scores. Andres played it: *"two lines are
   pretty but eat up too much of the space"* (D-123). He is right — a 46px row
   is nine rows on a phone where eleven fit, and on a LEADERBOARD the thing you
   are buying with that height is a footnote.

   So the pair is one line, and Design's concern is answered by TYPOGRAPHY
   instead of by geometry: the primary keeps the display face at full size and
   full weight, the secondary is two-thirds the size, dimmed, and set hard
   against it. The risk was never that the numbers were adjacent — it was that
   they might look equal. */
.lb-row {
  display: grid;
  grid-template-columns: 2.4em minmax(0, 1fr) auto;
  align-items: center;
  gap: 10px;
  font-size: 0.78rem;
  padding: 6px 10px;
  border-bottom: 1px solid var(--line, var(--surface-2));
}
.lb-row:last-child { border-bottom: none; }

/* `me`: the gold tint carries it, not a box. AFTER :last-child on purpose —
   your own row keeps its rule even when it is the list's last. */
.lb-row.me {
  border: none;
  border-bottom: 1px solid var(--line, var(--surface-2));
  background: color-mix(in srgb, var(--gold) 12%, transparent);
}
.lb-row.me .lb-name { color: var(--tint-ink); font-weight: 700; }
.lb-row.me .lb-rank, .lb-row.me .lb-score { color: var(--tint-ink); }
.lb-rank { color: var(--text-dim); min-width: 2.4em; text-align: right; font-variant-numeric: tabular-nums; }
.lb-row.top1 .lb-rank { color: var(--gold-lt); }
.lb-name { flex: 1; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
/* A name with the First try mark (S15-12): the name gives way, the mark never
   does, and both stay in the one cell (D-124). */
.lb-name.has-mark { display: flex; align-items: center; gap: 6px; min-width: 0; }
.lb-nm { min-width: 0; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
/* Ink on an outline, square-cornered (Design's round 2 §5): not a medal's gold,
   not the live lime, not the news colour, and no glyph (build fix 4). */
.ft-mark {
  flex-shrink: 0; padding: 2px 4px; border: 1px solid var(--text-dim); border-radius: 3px;
  font: 600 0.68rem/1 var(--font-mono); color: var(--text); white-space: nowrap;
}
@media (max-width: 360px) { .ft-long { display: none; } }
/* The score CELL, not the score: since D-124 it can hold a pair — a primary
   number and one annotation (the run time on timed days, R3; the companion
   quantity on the word boards). min-width holds the widest pair (+1,203 over
   12,847), so five digits never push a name into an ellipsis it did not need
   at 320px. */
.lb-score {
  display: flex; flex-direction: row; align-items: baseline; justify-content: flex-end; gap: 5px;
  min-width: 58px; text-align: right;
  font-weight: 700; font-size: 0.92rem; color: var(--text); font-variant-numeric: tabular-nums;
}
/* The word boards' score keeps the text colour on your own row too, as it has
   since D-122: an id selector there outranked `.lb-row.me .lb-score`. Kept so
   deleting the switch (D-243) changes nothing on screen. */
#word-board .lb-score { color: var(--text); }
/* The pair is EVERY list's now, not the word boards' (D-124): the day
   leaderboard's run time is the same species of annotation as a companion
   count, and scoping the treatment to #word-board was what left the time as a
   fourth grid child wrapping onto a second line. */
.lb-pri { font-variant-numeric: tabular-nums; letter-spacing: 0.01em; }
/* Three things at once — scale, weight and colour — so neither number has to
   shout to be the ranked one. Two-thirds the size is what keeps a pair on one
   line from reading as a pair of scores. */
.lb-sec {
  font-family: var(--font-mono, inherit); font-size: 0.62rem; font-weight: 500;
  color: var(--text-dim); font-variant-numeric: tabular-nums;
}

/* AFTER `.lb-row.me`: the pinned row is always your own. */
.lb-row.pinned { border-bottom: none; }
#word-board .lb-row.pinned .lb-rank, #word-board .lb-row.pinned .lb-pri { color: var(--gold); }
#word-board .lb-row.pinned .lb-sec { color: color-mix(in srgb, var(--gold) 78%, var(--text)); }

/* An absence is not a zero (D-119). No primary number at all, a dashed rank
   well where the number would be, and one clause of prose. */
#word-board .lb-row.absent { background: var(--surface-2); }
#word-board .lb-row.absent .lb-rank {
  color: transparent; border: 1px dashed var(--line, var(--surface-2));
  border-radius: var(--radius); height: 20px;
}
#word-board .lb-row.absent .lb-name { color: var(--text-dim); font-weight: 500; }
.lb-none { font-size: 0.6rem; color: var(--text-dim); white-space: nowrap; }

/* The column heading: says which number is which ONCE, so fifty rows do not
   have to caption themselves. It changes with the tab. */
.lb-key {
  display: grid; grid-template-columns: 2.4em minmax(0, 1fr) auto; gap: 10px;
  padding: 0 10px 6px; font-size: 0.5rem; letter-spacing: 0.13em;
  text-transform: uppercase; color: var(--text-dim);
}
.lb-key span:last-child { text-align: right; }
.lb-key b { color: var(--text); font-weight: 700; }

.lb-me { font-size: 0.75rem; color: var(--text); margin-top: 4px; }
.lb-empty { color: var(--text-dim); font-size: 0.78rem; text-align: center; padding: 12px 0; }

/* ── Yesterday's reveal ── */

.yd-section { text-align: left; margin-top: 10px; }
.yd-title { font-size: 0.7rem; color: var(--text-dim); text-transform: uppercase; letter-spacing: 0.06em; margin-bottom: 4px; }

.yd-word {
  display: flex;
  align-items: baseline;
  gap: 8px;
  font-size: 0.78rem;
  padding: 3px 8px;
  border-radius: var(--radius);
}
.yd-w { color: var(--gold); font-weight: 700; letter-spacing: 0.04em; }
.yd-f { flex: 1; text-align: right; color: var(--text-dim); overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
/* The points column (D-090): what the word is worth on that day's board under
   that day's rules. Fixed width and tabular figures so the column lines up
   down the list, whichever list it is. */
.yd-p {
  flex-shrink: 0;
  min-width: 4.2em;
  text-align: right;
  color: var(--text);
  font-weight: 700;
  font-variant-numeric: tabular-nums;
}

.yd-bar-row { display: flex; align-items: center; gap: 6px; font-size: 0.7rem; padding: 1px 8px; }
.yd-bar-label { min-width: 4.6em; text-align: right; color: var(--text-dim); font-variant-numeric: tabular-nums; }
.yd-bar { flex: 1; height: 10px; border-radius: 5px; background: var(--surface-2); overflow: hidden; }
.yd-bar-fill { display: block; height: 100%; border-radius: 5px; background: var(--gold); }
.yd-bar-count { min-width: 2em; color: var(--text); font-variant-numeric: tabular-nums; }

/* Your own finds on the word lists (D-200): the leaderboard's own "me" row,
   so a player already knows what it means. */
.yd-word.me { background: var(--tint-bg); box-shadow: inset 0 0 0 1px var(--gold); }
.yd-word.me .yd-w, .yd-word.me .yd-f { color: var(--tint-ink); }

/* The name choice (D-200): first thing under the day's line, one choice for
   all three lists, a real tap target (G6). */
.yd-optin {
  display: flex;
  align-items: center;
  justify-content: center;
  gap: 8px;
  min-height: 44px;
  margin-bottom: 6px;
  font-size: 0.72rem;
  color: var(--text);
  cursor: pointer;
}
.yd-optin input { accent-color: var(--gold); width: 16px; height: 16px; }

/* The Feedback card's foot (D-190): the build and the screen, which go with
   the message. The faintest readable text — a record, not an instruction. */
.fb-build {
  margin: 10px 0 0; font-size: 0.68rem; color: var(--text-dim);
  text-align: center; overflow-wrap: anywhere;
}

/* ── Build tag (temporary placement — bottom right) ── */
#build-tag {
  position: fixed;
  right: 4px;
  bottom: 2px;
  font-size: 9px;
  /* The one literal colour left on this element was white at 25%, which is
     invisible on every light palette (Andres, 2026-09-21). It is a dev aid, so
     it wants to be the faintest readable text and nothing louder: the dim ink
     of whichever palette and mode is live, thinned the way --streak-missed
     already thins it. Both halves move with the switch, so it stays legible in
     light and stays out of the way in dark. */
  color: color-mix(in srgb, var(--text-dim) 62%, transparent);
  pointer-events: none;
  z-index: 32001; /* dev aid — visible above overlays incl. utilities */
}

/* ── Animations ── */
@keyframes popIn {
  0%   { transform: scale(0.3) translateY(-8px); opacity: 0; }
  65%  { transform: scale(1.1); opacity: 1; }
  100% { transform: scale(1) translateY(0); opacity: 1; }
}

@keyframes scoreFly {
  0%   { opacity: 1; transform: translateX(-50%) translateY(0); }
  100% { opacity: 0; transform: translateX(-50%) translateY(-38px); }
}

/* ── Score-vs-par medal on the results card (S7-5 / D-064) ── */
.end-medal {
  display: block; margin: 4px auto 6px; font-weight: 800; letter-spacing: 0.04em;
  font-size: 1.05rem; color: var(--medal-gold);
}
.end-medal.tier0 { color: var(--text-dim); }
/* A rung below par (S15-8, D-276): its bars and name, in the medal's gold. */
.end-medal.rung { color: var(--medal-gold); }
/* The rung mark (Design's round 2, fix 15): one bar for the first rung, two
   rising bars for the second. Sized in em and painted in currentColor, so it
   sits in every medal surface's own size and gold. */
.rung-mark { display: inline-flex; align-items: flex-end; gap: 0.12em; height: 0.85em; vertical-align: -0.05em; }
.rung-mark i { width: 0.3em; border-radius: 1px; background: currentColor; }
.rung-mark i:nth-child(1) { height: 0.45em; }
.rung-mark i:nth-child(2) { height: 0.85em; }

/* ── The ladder strip under the dig header (S15-8; Design's round 2 §1) ──
   Its own 14px row, so the caption keeps its full length at 320px (fix 17). */
#game-header.playing { flex-wrap: wrap; row-gap: 4px; }
.rung-strip { order: 4; flex: 0 0 100%; display: flex; align-items: center; gap: 8px; height: 14px; }
.rung-track {
  position: relative; flex: 1; min-width: 40px; height: 6px;
  border-radius: 3px; background: var(--surface-2); border: 1px solid var(--border);
}
.rung-track > i { position: absolute; left: 0; top: 0; bottom: 0; border-radius: 2px; background: var(--medal-gold); }
.rung-strip.tiles .rung-track > i { background: var(--text); }
/* Each step's mark where its tick was, and the dig key on Topsoil's (D-314;
   Design's docs/wq-rung.css, prefix rgm-). Every mark is 12px tall, centred
   on the 6px track, so the row stays 14px. A --panel halo, the header's own
   ground, cuts each mark out of the track and the fill. A step ahead is open,
   a step reached is struck: both --medal-gold, the calendar's star. */
.rgm-track { margin-right: 18px; } /* room for ★★★'s mark, just past the end */
.rgm-mk {
  position: absolute; top: 50%; left: var(--at); height: 12px; transform: translate(-50%, -50%);
  display: flex; align-items: center; color: var(--medal-gold); pointer-events: none;
}
.rgm-mk svg { display: block; height: 12px; overflow: visible; }
.rgm-mk.is-end { left: auto; right: 0; transform: translate(calc(100% + 4px), -50%); }
.rgm-h { fill: var(--panel); stroke: var(--panel); stroke-width: 2.6px; stroke-linejoin: round; vector-effect: non-scaling-stroke; }
.rgm-s { fill: var(--panel); stroke: currentColor; stroke-width: 1.15px; stroke-linejoin: round; vector-effect: non-scaling-stroke; }
.rgm-mk.is-at .rgm-s { fill: currentColor; }
/* The dig key hangs LEFT of Topsoil's bar, so the bar stays on its tick and
   Deep dig keeps its room at 320px, on its own --panel backing so it never
   sits on the gold fill. It stays once Topsoil is passed: Finish pays it. */
.rgm-key {
  position: absolute; right: 100%; top: 50%; transform: translateY(-50%); margin-right: 2px;
  display: flex; align-items: center; justify-content: center; height: 12px; padding: 0 1.5px;
  border-radius: 3px; background: var(--panel);
}
.rgm-key .key, .rgm-plus .key { margin: 0; }
/* The caption's key: "12 to Topsoil · +1 [key]". The number is the server's. */
.rgm-plus { display: inline-flex; align-items: center; gap: 3px; }
.rgm-dot { color: var(--text-dim); }
/* Only the fill moves, by transform: one property, one element. */
.rung-track > i.rgm-fill {
  width: 100%; transform-origin: 0 50%; transform: scaleX(var(--f, 0));
  transition: transform 0.24s cubic-bezier(0.2, 0.7, 0.3, 1);
}
@media (prefers-reduced-motion: reduce) { .rung-track > i.rgm-fill { transition: none; } }
.rung-cap {
  font-size: 0.68rem; line-height: 14px; color: var(--text-dim); white-space: nowrap;
  display: flex; align-items: center; gap: 3px;
}
.rung-cap b { color: var(--text); font-weight: 700; font-variant-numeric: tabular-nums; }
.rung-cap [data-icon] { width: 9px; height: 9px; color: var(--medal-gold); }
/* Medal gold was a FIXED literal in every theme (D-077) on the grounds that a
   medal you earned should look the same wherever you see it. D-099 retired
   that: the literal measured 1.65:1 on a cream ground, so on a light palette
   the player's only personal progress signal read as a smudge. It is now
   --medal-gold, per combination, still recognisably gold in all four. Not part of
   the D-077 token sweep — this one is deliberate, not an oversight. */
.end-medal.tier3 { text-shadow: 0 0 12px rgba(240, 168, 60, 0.6); }

/* ── Quarry Wall calendar (S7-4 / D-060 / D-064) — theme-aware via the same
      tile tokens the board uses; medal gold is fixed for recognisability. ── */
.cal-top { display: flex; flex-direction: column; gap: 10px; margin-bottom: 10px; }
/* The heading is a `.page-title` like every other page's since D-124 — it was
   the one base whose name wore the body font, at a size the month above the
   grid outranked. */
.cal-brand { display: flex; justify-content: space-between; align-items: center; gap: 8px; min-height: 40px; }
/* The key is DRAWN, never a glyph (D-091), for the reason the coin it replaced
   was: an emoji is at the mercy of the Devvit webview's font stack, and U+1FA99
   rendered there as tofu (D-077). CSS reaches every environment identically.

   ONE class for every key in the app — size is a modifier, never a second
   drawing. Every length is in EM, so a modifier only sets font-size and the
   bow, blade and teeth keep their proportions at any size. The metal is a
   FIXED literal in all three themes, exactly like the medal and the coin
   before it: a currency mark that changed colour with the theme would stop
   being one mark. The one sanctioned override is the gold price lane, where
   metal on metal would dissolve (see the price area). */
.key {
  /* Declared per palette (see the token blocks): a fixed metal was right while
     every palette was dark, and wrong the moment one was not — exactly what
     happened to the medal. The gold lane still re-inks it, and only it. */
  position: relative;
  display: inline-block; flex-shrink: 0;
  font-size: 12px;
  width: 1.36em; height: 0.8em;
  margin: 0 2px;
  vertical-align: -0.05em;
  /* One edge for the whole drawing: a drop-shadow follows both pseudo
     elements, where a border would outline the bow and leave the blade bare. */
  filter: drop-shadow(0 0 0.05em var(--key-edge));
}
/* The bow — a ring, open through the middle, so it reads as a key's handle and
   not as a dot on a stick. */
.key::before {
  content: "";
  position: absolute; left: 0; top: 0;
  width: 0.8em; height: 0.8em; box-sizing: border-box;
  border-radius: 50%;
  border: 0.22em solid var(--key-metal);
}
/* The blade — a shaft with two teeth hanging from it: one at the tip, a
   shorter one partway along. */
.key::after {
  content: "";
  position: absolute; left: 0.72em; top: 0.3em;
  width: 0.64em; height: 0.46em;
  background:
    linear-gradient(var(--key-metal), var(--key-metal)) 0 0 / 100% 0.2em no-repeat,
    linear-gradient(var(--key-metal), var(--key-metal)) 100% 0 / 0.14em 100% no-repeat,
    linear-gradient(var(--key-metal), var(--key-metal)) 0.3em 0 / 0.13em 75% no-repeat;
}
/* Sizes: font-size only — the drawing scales with it. */
.key.key-sm { font-size: 9px; vertical-align: 0; }
.key.key-lg { font-size: 17px; vertical-align: -0.1em; }
.key.key-xl {
  font-size: 40px; vertical-align: middle;
  filter: drop-shadow(0 0 0.04em var(--key-edge)) drop-shadow(0 2px 6px rgba(0, 0, 0, 0.35));
}
.cal-nav { display: flex; justify-content: space-between; align-items: center; }
.cal-month {
  font-family: var(--font-title); font-weight: 700; font-size: 1.5rem; color: var(--text);
}
.cal-dow { display: grid; grid-template-columns: repeat(7, 1fr); gap: 4px; margin-bottom: 5px; }
.cal-dow div {
  text-align: center; font-size: 0.72rem; letter-spacing: 0.06em;
  color: var(--text-dim); text-transform: uppercase;
}
.cal-grid { display: grid; grid-template-columns: repeat(7, 1fr); gap: 4px; }
/* Type scales WITH the cell (Andres 2026-09-07). The cell is a 1fr grid column
   that lands near 42px on a 320px phone, so the requested sizes are the ceiling
   of a clamp, not a fixed value — a fixed 0.92rem "READY" clips against the
   cell's own overflow: hidden. */
/* THREE ROWS, not three overlapping absolutes (Andres 2026-09-07: the pips sat
   on top of the date). The pip strip is RESERVED on every cell, medal or not,
   so the day numbers keep one baseline across the whole month — a row that
   collapsed when empty would make dug days sit lower than undug ones. Nothing
   can collide at any font size, which a nudged offset could not promise. */
.cal-cell {
  position: relative; aspect-ratio: 1 / 1.1; border-radius: 6px; overflow: hidden;
  display: grid; grid-template-rows: 0.82rem 1fr auto; justify-items: center; align-items: center;
  padding: 2px 2px 3px;
  font-weight: 700; font-size: clamp(0.78rem, 3vw, 1rem); border: 1px solid transparent; cursor: pointer;
}
.cal-cell .cal-d { grid-row: 2; line-height: 1; }
.cal-cell .cal-sc { grid-row: 3; font-size: clamp(0.62rem, 2.6vw, 0.92rem); font-weight: 700; opacity: 0.9; }
.cal-cell .cal-md {
  grid-row: 1; justify-self: end; align-self: center;
  font-size: 0.68rem; line-height: 1;
  color: var(--medal-gold);
  display: flex; align-items: center; gap: 1px;
  /* The pips sit over cells of every shade — sandstone when done, lightened
     shale when unlocked-and-undug. Gold on a light cell needs a dark floor,
     and a DRAWN star needs it as a filter rather than a text-shadow. */
  filter: drop-shadow(0 1px 1px rgba(0, 0, 0, 0.85));
}
/* Each pip is its own square. Tighter than the default slot because a ladder
   of three has to fit the corner of a calendar cell that lands near 42px on a
   320px phone (D-077). */
.cal-cell .cal-md [data-icon] { width: 0.78em; height: 0.78em; vertical-align: 0; }
.cal-cell.cal-blank { visibility: hidden; cursor: default; }
.cal-cell.cal-nogame { background: transparent; border: none; color: var(--text-dim); opacity: 0.4; cursor: default; }
.cal-cell.cal-done {
  background: var(--tile-bg-free); color: var(--tile-text); border-color: var(--accent-bdr);
  box-shadow: 2px 2px 0 rgba(0, 0, 0, 0.25); text-shadow: 0 1px 0 rgba(255, 255, 255, 0.35);
}
.cal-cell.cal-done .cal-sc { color: var(--tile-pts-clr); opacity: 1; }
.cal-cell.cal-done.cal-t3 { box-shadow: 2px 2px 0 rgba(0, 0, 0, 0.25), 0 0 10px color-mix(in srgb, var(--gold) 55%, transparent); }
/* Today is the one LIVE board, so it wears the LIVE colour (D-093) — the same
   --live that fills the LIVE badge and outlines every "Go live" door, so the
   door and the day it leads to read as one thing. The ring is an inset shadow
   on top of the border, not a thicker border, so the cell keeps the month's
   grid; the ground stays --accent (in survey --live and --accent are one hex,
   and a live-coloured ground there would swallow the ring). */
.cal-cell.cal-today {
  background: var(--accent); color: var(--accent-ink); border-color: var(--live);
  box-shadow: inset 0 0 0 1px var(--live), 0 0 12px color-mix(in srgb, var(--live) 45%, transparent);
}
.cal-cell.cal-ready { background: var(--tile-bg-blk); color: var(--text); border-color: var(--accent-bdr); }
.cal-cell.cal-ready .cal-sc { color: var(--gold-lt); opacity: 1; }
.cal-cell.cal-locked { background: var(--tile-bg-blk-or); color: var(--day-buried-ink); border-color: rgba(0, 0, 0, 0.4); }
.cal-cell.cal-future { background: transparent; color: var(--text-dim); border: 1px dashed var(--text-dim); opacity: 0.5; cursor: default; }
.cal-note { text-align: center; font-size: 0.82rem; color: var(--text-dim); margin: 10px 0; }

/* ── Flow v3 (D-070): base / sheet / panel / nav ────────────────────────── */

/* BASE — the Quarry Wall. A real screen in normal flow, not an overlay: it is
   what the app boots into and what everything else layers over. */
#home-screen {
  flex: 1;
  overflow-y: auto;
  padding: 12px 14px 14px;
  background:
    radial-gradient(ellipse at 50% 30%, var(--felt-2) 0%, var(--felt) 75%);
}
/* --measure is the one number every base agrees on (S8-9). It was three
   copies of 420px on the wall alone; the results page and the two new pages
   now read the same token, so a change lands everywhere at once. */
#home-screen .cal-top { max-width: var(--measure); margin: 0 auto 10px; }
#home-screen .cal-dow,
#home-screen .cal-grid { max-width: var(--measure); margin-left: auto; margin-right: auto; }
#home-screen .cal-note { max-width: var(--measure); margin: 12px auto 0; }

/* SHEET band — bottom-anchored, thumb-reachable, clears the nav. */
.wq-sheet {
  position: fixed;
  inset: 0;
  z-index: 25000;
  display: flex;
  flex-direction: column;
  justify-content: flex-end;
  background: var(--scrim);
  /* Clear of the header, which stacks above the sheets and wraps to two rows
     and a rung strip on a phone (D-284): --hdr-h is its measured bottom edge
     (header-inset.ts), and 56px the inset before it is measured. */
  padding: max(56px, calc(var(--hdr-h, 0px) + 6px)) 0 var(--nav-h, 0px);
}
.wq-sheet > .end-card {
  position: relative;
  z-index: 1;
  margin: 0 auto;
  max-width: 460px;
  max-height: 100%;
  border-radius: 12px 12px 0 0;
  border-bottom: none;
}
/* Full-bleed click target behind the card: tapping the dim dismisses the sheet.
   A discovery moment (you find the wall behind it), never a dead end. */
.wq-scrim { position: absolute; inset: 0; z-index: 0; }

.sheet-card { position: relative; }
.sheet-grab {
  width: 36px; height: 4px; border-radius: 3px;
  background: var(--border); margin: 0 auto 12px;
}
.sheet-dismiss { position: absolute; top: 8px; right: 8px; }
/* Today's rules (D-275): an icon in the corner opposite the close, and the
   rules as a column when it is pressed, not a sentence always on the card. */
.sheet-rules-btn { position: absolute; top: 8px; left: 8px; }
.sheet-rules-btn[aria-expanded="true"] { color: var(--gold); }
.rules-list {
  margin: 0 0 10px;
  padding: 8px 12px 8px 28px;
  border: 1px solid var(--border);
  border-radius: var(--radius);
  background: var(--surface-2);
  font-size: 0.78rem;
  line-height: 1.35;
  text-align: left;
}
.rules-list li + li { margin-top: 3px; }
/* 3-up facts (bot par / top score / closes in) — same row on every face. */
.sheet-stats { display: flex; gap: 6px; margin: 0 0 10px; }
.sheet-stats .s {
  flex: 1; min-width: 0; text-align: center;
  border: 1px solid var(--border); border-radius: var(--radius);
  background: var(--surface-2); padding: 6px 4px;
}
.sheet-stats .s span {
  display: block; font-size: 0.68rem; letter-spacing: 0.1em;
  text-transform: uppercase; color: var(--text-dim);
}
.sheet-stats .s b { font-family: var(--font-title); font-size: 0.95rem; color: var(--text); }
/* "Emphasised" is not "rare" (D-077) — this stat is merely the interesting one. */
.sheet-stats .s.o b { color: var(--gold); }
/* The second number a closed day can hold (D-068): the frozen ranked result
   above, the archive best here — kept for you, never ranked. Common palette:
   archive is ordinary furniture, not treasure (D-077). */
.sheet-archive { font-size: 0.72rem; color: var(--gold); margin: 0 0 8px; }

/* NAV band — in normal flow so the safe-area inset and the board's height math
   need no fixed-position guesswork; z-raised so sheets/panels never bury it. */
#nav-bar {
  position: relative;
  z-index: 30500;
  display: flex;
  flex-shrink: 0;
  gap: 4px;
  padding: 0 8px env(safe-area-inset-bottom, 0px);
  height: var(--nav-h);
  background: var(--panel);
  border-top: 1px solid var(--border);
}
.nav-btn {
  flex: 1;
  min-width: 0;
  min-height: 44px; /* G6 tap target */
  display: flex;
  flex-direction: column;
  align-items: center;
  justify-content: center;
  gap: 2px;
  background: transparent;
  border: none;
  border-radius: var(--radius);
  color: var(--text-dim);
  font-family: var(--font-mono);
  cursor: pointer;
}
/* ── icon slots (S10d) ──────────────────────────────────────────────────
   Every drawn glyph arrives through `<i data-icon="...">`, filled once at boot
   from src/client/icons.ts. The box is sized in EM so an icon tracks the type
   it sits beside — the same reason the key is drawn in em units — and the
   baseline nudge is what stops a 1em square riding high next to a cap-height
   word. `currentColor` is the whole mechanism: the icon takes the colour of
   whatever it is inside, so one glyph serves gold on a key row and ore on a
   live badge without a second copy.
   ──────────────────────────────────────────────────────────────────────── */
[data-icon] {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  width: 1.05em;
  height: 1.05em;
  vertical-align: -0.16em;
  flex-shrink: 0;
}
[data-icon] svg { width: 100%; height: 100%; display: block; }
/* A slot inside a button is part of the label, never a separate target. */
.btn [data-icon] { pointer-events: none; }

.nav-btn .nav-i { font-size: 1.05rem; line-height: 1.1; display: inline-flex; align-items: center; justify-content: center; height: 1.2em; }
.nav-btn .nav-i svg { width: 1.15em; height: 1.15em; }
/* Six tabs since D-095: a label may shrink below its text and the ellipsis is
   the backstop — a tab must never push the bar wider than the phone. */
.nav-btn .nav-l {
  font-size: 0.7rem; letter-spacing: 0.08em; text-transform: uppercase;
  max-width: 100%; overflow: hidden; text-overflow: ellipsis; white-space: nowrap;
}
.nav-btn.on { color: var(--gold); background: color-mix(in srgb, var(--gold) 10%, transparent); }
/* A 320px phone gives each of six tabs ~50px; "CALENDAR" in the mono face
   needs ~60. Below 440px the labels drop to the title face in sentence case
   (proportional, and the condensed faces fit comfortably) — the type floor
   (D-076) holds at 0.68rem. The designer's flow rewrite owns the final look. */
@media (max-width: 440px) {
  #nav-bar { gap: 0; padding-left: 2px; padding-right: 2px; }
  .nav-btn .nav-l { font-family: var(--font-title); font-size: 0.72rem; letter-spacing: 0.01em; text-transform: none; }
}

/* ── ⋯ More (D-095): the switches and the help, anchored above the nav ── */
#more-menu { position: fixed; inset: 0; }
.more-scrim { position: absolute; inset: 0; background: color-mix(in srgb, var(--scrim) 60%, transparent); }
.more-card {
  position: absolute;
  right: 8px;
  bottom: calc(var(--nav-h) + 6px);
  width: min(240px, calc(100vw - 16px));
  padding: 4px;
  background: var(--panel);
  border: 1px solid var(--gold);
  border-radius: 8px;
  box-shadow: 0 8px 32px rgba(0, 0, 0, 0.55);
}
.more-row {
  display: flex; align-items: center; gap: 10px;
  width: 100%;
  min-height: 44px; /* G6 */
  padding: 6px 10px;
  background: transparent;
  border: none;
  border-radius: var(--radius);
  color: var(--text);
  font-family: var(--font-mono);
  font-size: 0.82rem;
  text-align: left;
  cursor: pointer;
}
.more-row + .more-row { border-top: 1px solid var(--border); border-radius: 0; }
.more-row:hover { background: color-mix(in srgb, var(--gold) 10%, transparent); }
/* The row's icon box. The glyph inside is a [data-icon] slot like every other,
   which is what stops a row painted from the markup and a row painted by
   setRowIcon() ending up different sizes. */
.more-i { width: 1.4em; display: inline-flex; align-items: center; justify-content: center; font-size: 1rem; }
.more-i [data-icon] { width: 1.15em; height: 1.15em; vertical-align: 0; }
.more-l { flex: 1; }
.more-v {
  font-size: 0.68rem; letter-spacing: 0.1em; text-transform: uppercase;
  padding: 2px 8px; border-radius: 20px;
  /* One width for every state a row can show (On/Off, Auto/Light/Dark), so
     cycling a switch does not resize its own pill under the thumb
     (Andres 2026-09-19). Centred, because the text inside it changes length. */
  min-width: 4.6em; text-align: center;
  border: 1px solid var(--gold); color: var(--gold);
}
.more-row.off .more-v { border-color: var(--border); color: var(--text-dim); }

/* ── Board status badge (D-071): liveness is a property of the BOARD ────── */
.bwrap { display: flex; justify-content: center; margin: 0 0 8px; }
.wq-badge {
  display: inline-flex;
  align-items: center;
  gap: 5px;
  /* 0.66rem, up from 0.52 (D-144). At a 16px root that was 8.3px of tracked
     uppercase — footer type for the one line that says whether the board you
     are about to dig is ranked. */
  font-size: 0.66rem;
  font-weight: 700;
  letter-spacing: 0.16em;
  text-transform: uppercase;
  padding: 3px 9px;
  border-radius: 20px;
  white-space: nowrap;
}
/* Liveness is blue, archive is the common gold-on-brown (D-077). Both are
   token-only: the old rules hard-coded Strata's teal and a near-black ink, so
   in classic/survey the archive badge wore gold text on a teal wash and the two
   badges collapsed to one colour — the D-071 distinction only worked in Strata. */
.wq-badge.badge-live {
  background: var(--live);
  color: var(--live-ink);
  animation: badgeBeat 1.6s ease-in-out infinite;
}
.wq-badge.badge-archive {
  border: 1px solid var(--gold);
  color: var(--gold);
  background: color-mix(in srgb, var(--surface-2) 88%, transparent);
}
.wq-badge.badge-locked { border: 1px solid var(--border); color: var(--text-dim); background: var(--surface-2); }

/* ── One corner pill, two accents (S8-8 / D-078) ──────────────────────────
   LIVE and its door differed in FAMILY, not just accent: 0.52rem default-font
   solid fill against 0.75rem mono ghost outline. D-077 gave them a shared box,
   but it is the TEXT size that reads as a height mismatch, so the box alone
   was not enough. One typography, one geometry; the only difference is filled
   ore against ore outline.

   The badge is reached by descendant selector, not by a class of its own:
   paintBadge() rewrites className wholesale, so a `pill` class on the element
   would not survive the next repaint. Scoped to .heap-brand on purpose — the
   day sheet, the finish sheet and #board-status keep the small badge. */
.pill {
  min-height: 30px;
  display: inline-flex; align-items: center; justify-content: center; gap: 5px;
  padding: 3px 12px;
  border: 1px solid transparent;
  border-radius: 20px;
  font-family: var(--font-mono);
  font-size: 0.75rem;
  font-weight: 700;
  letter-spacing: 0.1em;
  text-transform: uppercase;
  white-space: nowrap;
}
/* The door is the same accent as the badge it swaps with — one control in two
   states, not two controls.

   THE ACCENT IS A GROUND, NOT ONLY A RIM (D-139). Andres 2026-09-22: the two
   "look too different - althou they are of same family". They were, and the
   reason is worth keeping: the pair was only ever meant to differ as filled
   against hollow, which holds where `--live-text` and `--live` are the same
   colour — and in survey they are not. There the badge is a filled hi-vis lime
   and the door was a transparent rim with OLIVE ink, because lime is
   unreadable as a label on a panel and `--live-text` exists for that measured
   reason. So the door carries a resting wash of the accent and a heavier rim:
   the ink keeps the token it needs, and the block is what makes the family
   legible. Filled and half-filled, rather than filled and empty. */
.btn.pill-door {
  background: color-mix(in srgb, var(--live) 18%, transparent);
  border-color: var(--live);
  border-width: 1.5px;
  /* Ink, not ground — see --live-text. A lime that reads as a badge fill does
     not read as a label on a panel. */
  color: var(--live-text);
}
.btn.pill-door:not(:disabled):hover { background: color-mix(in srgb, var(--live) 34%, transparent); }
/* And it BREATHES (D-143), on `badgeBeat`'s own 1.6s so the badge and the door
   are one rhythm as well as one colour. Andres asked whether a background
   pulse is expensive: it is not — it repaints ONE 30px pill per frame and
   never touches layout. What would be expensive is animating width, height or
   box-shadow across many elements, or anything that forces a reflow.
   The two never co-exist, which is what makes two pulses safe: on the Results
   page they swap in one corner, and on the day sheet the door shows only on a
   non-live face, where the badge is archive or buried. */
@keyframes doorBeat {
  0%, 100% { background-color: color-mix(in srgb, var(--live) 18%, transparent); }
  50% { background-color: color-mix(in srgb, var(--live) 38%, transparent); }
}
.btn.pill-door { animation: doorBeat 1.6s ease-in-out infinite; }
.btn.pill-door:not(:disabled):hover { animation: none; }
@media (prefers-reduced-motion: reduce) {
  .btn.pill-door { animation: none; }
}
@keyframes badgeBeat { 0%, 100% { opacity: 1; } 50% { opacity: 0.62; } }
/* In the header the badge rides beside the score — smaller, never pulsing
   (a heartbeat next to a live timer is noise while you are concentrating). */
/* The header badge shares a row with Score, Tiles and (on a timed day) Time at
   320px, so it is the one place with a width budget (D-144). It pays for the
   size out of TRACKING rather than out of letters: uppercase at 0.16em spends
   about a fifth of its line on the gaps. Text is untouched — `boardStatus()`
   is the one writer of it (D-077) and a shorter header string would fork the
   vocabulary D-076 unified. */
#board-status { animation: none; font-size: 0.58rem; letter-spacing: 0.1em; padding: 2px 7px; }

/* ── Heap results panel (D-070) — day-scoped: tabs over one day's results, a
      pinned standing panel that never scrolls away. ───────────────────────── */
/* A page: fixed top, scrolling middle, fixed foot — the same anatomy the
   design review gave it, and the reason it reads as a place. */
#heap-screen,
.page-screen {
  flex: 1;
  min-height: 0;
  display: flex;
  flex-direction: column;
  background: radial-gradient(ellipse at 50% 20%, var(--felt-2) 0%, var(--felt) 75%);
}

/* ── The ground (D-180, Design's "Board Grounds") ───────────────────────────
   The space behind the tiles and the lists read as an empty fill, worst on a
   light palette. Every base that paints the felt carries .gnd, and the ground
   is PAPER, always: Design drew four, all four shipped behind a live switch
   while paper was tested, and paper was kept (D-181) — the other three, the
   plain-felt `none` and the switch are gone. Two overlays sit UNDER the base's
   children — ::before the tooth, ::after the flecks and mottle — each a mask
   painted in --gnd-ink, so it follows the palette, needs no image file and
   never moves.
   Adapted from Design's CSS in one way: the overlays are position: FIXED and
   the base clips them (clip-path: inset(0)). Two bases scroll themselves (the
   calendar and the board), and an absolute overlay inside a scroller scrolls
   away with the content and leaves a bare band; a fixed one stays put under
   everything, like paper under a page. The SVGs spell w3.org as w3%2Eorg so
   the class-name audit does not read ".w3" and ".org" as classes, and every
   mask lives on .gnd itself — a custom property declared under a [data-…]
   selector would read as a palette block to the theme tests. */
.gnd {
  position: relative; isolation: isolate; clip-path: inset(0);
  --gnd-tooth: url("data:image/svg+xml,%3Csvg xmlns='http://www%2Ew3%2Eorg/2000/svg' width='180' height='180'%3E%3Cfilter id='n'%3E%3CfeTurbulence type='fractalNoise' baseFrequency='.85' numOctaves='2' stitchTiles='stitch'/%3E%3CfeColorMatrix values='0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 1.4 -.45'/%3E%3C/filter%3E%3Crect width='100%25' height='100%25' filter='url(%23n)'/%3E%3C/svg%3E");
  --gnd-paper: url("data:image/svg+xml,%3Csvg xmlns='http://www%2Ew3%2Eorg/2000/svg' width='307' height='307'%3E%3Cfilter id='s'%3E%3CfeTurbulence type='fractalNoise' baseFrequency='.55' seed='3' stitchTiles='stitch'/%3E%3CfeColorMatrix values='0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 16 -11.3'/%3E%3C/filter%3E%3Crect width='100%25' height='100%25' filter='url(%23s)'/%3E%3C/svg%3E"),
    url("data:image/svg+xml,%3Csvg xmlns='http://www%2Ew3%2Eorg/2000/svg' width='431' height='431'%3E%3Cfilter id='f'%3E%3CfeTurbulence type='fractalNoise' baseFrequency='.09' numOctaves='2' seed='11' stitchTiles='stitch'/%3E%3CfeColorMatrix values='0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 16 -12.2'/%3E%3C/filter%3E%3Crect width='100%25' height='100%25' filter='url(%23f)'/%3E%3C/svg%3E"),
    url("data:image/svg+xml,%3Csvg xmlns='http://www%2Ew3%2Eorg/2000/svg' width='613' height='613'%3E%3Cfilter id='m'%3E%3CfeTurbulence type='fractalNoise' baseFrequency='.006' numOctaves='3' seed='7' stitchTiles='stitch'/%3E%3CfeColorMatrix values='0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 1 -.52'/%3E%3C/filter%3E%3Crect width='100%25' height='100%25' filter='url(%23m)'/%3E%3C/svg%3E");
}
.gnd::before,
.gnd::after {
  content: ""; position: fixed; inset: 0; z-index: -1; pointer-events: none;
  background: var(--gnd-ink);
  -webkit-mask-repeat: repeat; mask-repeat: repeat;
}
.gnd::before { opacity: var(--gnd-grain); -webkit-mask-image: var(--gnd-tooth); mask-image: var(--gnd-tooth); }
/* paper: flecks at two sizes and a slow mottle (307/431/613 share no factor,
   so the repeat never lines up) */
.gnd::after { opacity: var(--gnd-dust); -webkit-mask-image: var(--gnd-paper); mask-image: var(--gnd-paper); }
.heap-top,
.page-top {
  flex-shrink: 0;
  /* Tightened in D-124: the page's vertical budget is the list, and the
     chrome above it was spending more than it earned. */
  padding: 8px 14px 6px;
  border-bottom: 1px solid var(--border);
  background: var(--panel);
}
/* One measure for every base (S8-9). The wall already centres per-child at
   420px; the results page had no constraint, so on a desktop a leaderboard row
   spanned the whole window. The CONTENTS are constrained, never the bars —
   constraining .heap-top/.heap-foot themselves would turn their hairlines into
   short floating lines with felt either side. */
.heap-brand,
.heap-nav,
.heap-tabs,
.heap-body > *,
.heap-standing,
.heap-foot > .btn,
.page-bar,
.page-body > *,
.page-foot > .btn { max-width: var(--measure); margin-left: auto; margin-right: auto; }
/* The badge and the "Go live" door swap in the same corner, so the row is
   given ONE height and both children fill it (Andres 2026-09-07: they were
   different heights, which shunted the page every time you changed day). The
   button keeps a real tap target (G6) — the fix is the shared box, not a
   shrunken control. The badge is scoped here so its size elsewhere is untouched. */
.heap-brand { display: flex; align-items: center; justify-content: space-between; gap: 8px; min-height: 40px; }
.heap-nav { display: flex; align-items: center; justify-content: space-between; gap: 8px; margin-top: 6px; }
.heap-day {
  flex: 1; text-align: center;
  font-family: var(--font-title); font-weight: 700; font-size: 1.5rem; color: var(--text);
}
.heap-tabs { display: flex; gap: 5px; margin-top: 6px; }
.heap-tab {
  flex: 1;
  /* Four tabs since D-090. min-width: 0 lets a flex item shrink below its
     label, and the ellipsis is the backstop — a tab must never push the row
     wider than a 320px phone. */
  min-width: 0;
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
  min-height: 32px;
  padding: 5px 4px;
  border-radius: var(--radius);
  border: 1px solid var(--border);
  background: var(--surface-2);
  color: var(--text-dim);
  font-family: var(--font-mono);
  font-size: 0.72rem;
  letter-spacing: 0.08em;
  text-transform: uppercase;
  cursor: pointer;
}
.heap-tab.on { border-color: var(--gold); color: var(--gold); background: color-mix(in srgb, var(--gold) 10%, transparent); }
.heap-body,
.page-body {
  flex: 1;
  min-height: 0;
  overflow-y: auto;
  padding: 8px 14px 10px;
  text-align: left;
}
/* The foot is part of the page, not a floating bar — it sits directly on the
   nav, so the standing and the way in are always both on screen. */
.heap-foot,
.page-foot {
  flex-shrink: 0;
  padding: 6px 14px 8px;
  border-top: 1px solid var(--border);
  background: var(--panel);
}
.heap-foot .btn,
.page-foot .btn { width: 100%; display: block; }
.heap-standing {
  flex-shrink: 0;
  margin-bottom: 6px;
  border: 1px solid var(--gold);
  border-radius: var(--radius);
  background: color-mix(in srgb, var(--gold) 8%, transparent);
  padding: 3px 2px;
}
/* The standing's own row (D-124). It borrowed .lb-row until the day it held
   four children in a three-column grid and wrapped — and the borrowing was
   always a little wrong anyway: a leaderboard row answers "who is on this
   board?", the standing answers "what am I on?". Same three columns as the
   lists so the two rows of a both-scores day share one score baseline. */
.stand-row {
  display: grid; grid-template-columns: 2.4em minmax(0, 1fr) auto;
  align-items: center; gap: 8px; padding: 4px 8px;
  font-size: 0.78rem; color: var(--tint-ink);
}
.stand-row + .stand-row { border-top: 1px dashed color-mix(in srgb, var(--gold) 30%, transparent); }
.stand-pos { color: var(--tint-ink); text-align: right; font-variant-numeric: tabular-nums; }
.stand-who {
  display: flex; align-items: center; gap: 6px; min-width: 0;
  overflow: hidden; text-overflow: ellipsis; white-space: nowrap; font-weight: 700;
}
.stand-score { color: var(--tint-ink); font-weight: 700; font-size: 0.92rem; font-variant-numeric: tabular-nums; }
/* An indicative ≈ position is visibly a different SPECIES from a rank (D-071):
   tilde-prefixed, tagged, and dimmer than a real rank. It never comes from the
   ranked zset. It does NOT need a rarity colour to say so (D-077) — the tilde
   and the tag carry the meaning; gold keeps it in the common palette. */
.stand-row.arch .stand-pos, .stand-row.arch .stand-score { color: var(--gold); }
.lb-tag {
  font-size: 0.46rem; letter-spacing: 0.1em; text-transform: uppercase;
  color: var(--text-dim); border: 1px solid var(--border);
  padding: 1px 5px; border-radius: 3px; flex-shrink: 0;
}
.stand-row.arch .lb-tag { color: var(--gold); border-color: var(--gold); }
/* The estimated position (S15-11, D-261 item 2): Andres asked for a number
   drawn weaker than a real rank, with its asterisk explained under the name.
   Opacity, not a new colour, so it stays the archive's gold in every theme. */
.stand-pos.est, .est-pos { opacity: 0.6; }
.stand-who { flex-direction: column; align-items: flex-start; gap: 0; }
.stand-name { display: flex; align-items: center; gap: 6px; min-width: 0; }
.stand-note { font-size: 0.68rem; font-weight: 400; color: var(--text-dim); }
/* The thin strip over the standing: whether the leaderboard still moves. */
.stand-strip {
  padding: 2px 8px;
  font-size: 0.68rem;
  letter-spacing: 0.08em;
  text-transform: uppercase;
  color: var(--text-dim);
  border-bottom: 1px solid color-mix(in srgb, var(--gold) 30%, transparent);
}
.stand-strip.live { background: var(--live); color: var(--live-ink); border-bottom-color: transparent; }
/* G3, taught at the moment it bites. */
.heap-veil {
  border: 1px dashed var(--border);
  border-radius: var(--radius);
  padding: 20px 16px;
  text-align: center;
  background: var(--surface-2);
  margin-top: 8px;
}
.heap-veil b { display: block; font-family: var(--font-title); color: var(--text); margin-bottom: 5px; }
.heap-veil span { font-size: 0.7rem; color: var(--text-dim); }

/* Calendar cells whose entitlement data hasn't landed yet (D-070). Without
   this a boot flashes a whole month of padlocks: calCellState falls through to
   "locked" while walletState is null. Neutral, inert, no lock glyph. */
.cal-cell.cal-pending {
  background: var(--surface-2); color: var(--text-dim);
  border-color: var(--border); cursor: default; opacity: 0.55;
}
/* Unlocked-but-undug days breathe (D-070) — an ambient itch, not a shouting
   badge. Respects reduced-motion. */
@keyframes calNag {
  0%, 100% { box-shadow: 0 0 0 transparent; }
  50% { box-shadow: 0 0 9px color-mix(in srgb, var(--gold) 42%, transparent); }
}
.cal-cell.cal-ready { animation: calNag 3.2s ease-in-out infinite; }
@media (prefers-reduced-motion: reduce) {
  .cal-cell.cal-ready { animation: none; }
  .wq-badge.badge-live { animation: none; }
}

/* The break between the top of the board and your own neighbourhood (D-072). */
.lb-gap {
  text-align: center;
  color: var(--text-dim);
  letter-spacing: 0.3em;
  font-size: 0.72rem;
  padding: 2px 0;
}

/* Score/Tiles are the DIG's instruments (D-073) — on the wall and the results
   page they are stale numbers that make a place look like a game with a card
   over it. #time-stat keeps its own separate hidden logic on top of this. */
#game-header:not(.playing) .stat { display: none; }

/* ══════════════════════════════════════════════════════════════════════════
   PLAYER SHELL — the Keys page and the profile page (S8-15/S8-16, D-078)
   Ported from docs/design-coins-and-profile-v2.html into the house names:
   the mock's compound spellings (.wq-btn-primary, .wq-badge-live, .wq-coin,
   .wq-tabbar) map onto what already ships, and only the genuinely new
   components — wal-* and prof-* — arrive under their own names. The page was
   the coins page until D-091; it kept its components when the coin went.
   ══════════════════════════════════════════════════════════════════════════ */

/* The mock's .wq-appbar overlapped what .heap-top already does, so there is ONE
   page anatomy and both wear it: fixed top, scrolling middle, fixed foot.
   Shipping two components for one job is how vocabularies fork. */
/* Back, then title, both LEFT (D-124). The bar used to centre the title
   between two 72px spacer elements; Andres asked for the left edge, which is
   also where Results has always put its own, so the two page anatomies finally
   agree and the spacers are gone rather than left inert. */
.page-bar { display: flex; align-items: center; justify-content: flex-start; gap: 8px; min-height: 40px; }
/* The live door (D-205): the same button in the same corner of every main
   page — the right end of the title row, where the Results page's door always
   was. Below 400px a 1.7rem title and the whole "● Live · ranked ›" do not fit
   one row, so the pill keeps its meaning and drops its qualifier: every board
   that is live is ranked, and the day sheet it opens says both. */
.live-go { margin-left: auto; flex-shrink: 0; }
@media (max-width: 400px) {
  .live-go-tail { display: none; }
}
/* ONE title component (D-124). `.heap-wm` was a copy of this rule differing
   only in letter-spacing — the fork the note above warns about, shipped. The
   Results page and the calendar now wear this one; 1.7rem because at 0.9 the
   page's own name was smaller than the month and the day beneath it. */
.page-title {
  font-family: var(--font-title); font-weight: 700; font-size: 1.7rem;
  letter-spacing: 0.06em; text-transform: uppercase; color: var(--text);
}
.page-back { min-height: 30px; padding: 4px 12px 4px 8px; font-size: 0.78rem; letter-spacing: 0.06em; }

/* Section heads, shared by both pages. */
.sec-head { display: flex; justify-content: space-between; align-items: baseline; padding: 16px 2px 7px; }
.sec-head:first-child { padding-top: 0; }
.sec-head b { font-family: var(--font-title); font-size: 0.9rem; color: var(--text); }
.sec-head span { font-size: 0.62rem; letter-spacing: 0.1em; text-transform: uppercase; color: var(--text-dim); }
.sec-list { border: 1px solid var(--border); border-radius: 8px; overflow: hidden; background: var(--panel); }
.sec-note { text-align: center; font-size: 0.68rem; color: var(--text-dim); margin-top: 9px; line-height: 1.6; }

/* ── Keys (wal-; the prefix is the page's, not the currency's) ──────────── */
.wal-hero {
  border: 1px solid color-mix(in srgb, var(--gold) 45%, transparent);
  border-radius: 12px;
  background: color-mix(in srgb, var(--gold) 9%, var(--panel));
  padding: 14px; text-align: center;
}
.wal-hero-label { font-size: 0.62rem; letter-spacing: 0.18em; text-transform: uppercase; color: var(--gold); }
.wal-hero-bal { display: flex; align-items: center; justify-content: center; gap: 9px; margin: 5px 0 3px; }
.wal-hero-num { font-family: var(--font-title); font-weight: 700; font-size: 2.6rem; line-height: 1; color: var(--gold-lt); }
/* "3 of 5" — the cap is the number a player plans against, quieter than the
   holding it qualifies. Absent above the cap, where "7 of 5" would read wrong. */
.wal-hero-of { align-self: flex-end; margin: 0 0 4px -3px; font-family: var(--font-title); font-size: 0.95rem; color: var(--text-dim); }
.wal-hero-sub { font-size: 0.7rem; color: var(--text-dim); }
.wal-hero-low { color: var(--gold); font-weight: 600; }
.wal-meters { display: flex; gap: 7px; margin-top: 11px; }
.wal-meter {
  flex: 1; border: 1px solid var(--border); border-radius: 8px;
  padding: 8px 6px; text-align: center; background: var(--surface-2);
}
.wal-meter span { display: block; font-size: 0.6rem; letter-spacing: 0.1em; text-transform: uppercase; color: var(--text-dim); }
.wal-meter b { font-family: var(--font-title); font-size: 1rem; color: var(--text); }
.wal-meter-credit b { color: var(--ledger-credit); }
.wal-filters { display: flex; gap: 6px; margin-bottom: 8px; }
.wal-filters button {
  flex: 1; min-height: 32px; padding: 6px; border-radius: 8px;
  border: 1px solid var(--border); background: var(--surface-2); color: var(--text-dim);
  font-family: var(--font-mono); font-size: 0.68rem; letter-spacing: 0.1em;
  text-transform: uppercase; cursor: pointer;
}
.wal-filters button.on { border-color: var(--gold); color: var(--gold); background: color-mix(in srgb, var(--gold) 9%, transparent); }
.wal-month { font-size: 0.62rem; letter-spacing: 0.16em; text-transform: uppercase; color: var(--text-dim); padding: 12px 3px 6px; }
.wal-entry {
  display: flex; align-items: center; gap: 10px; width: 100%;
  padding: 9px 11px; border: none; border-bottom: 1px solid var(--border);
  background: transparent; text-align: left; cursor: pointer; font-family: var(--font-mono);
}
.wal-entry:last-child { border-bottom: none; }
.wal-entry:disabled { cursor: default; }
.wal-entry-icon {
  width: 26px; height: 26px; border-radius: 7px; flex-shrink: 0;
  display: flex; align-items: center; justify-content: center;
  border: 1px solid var(--border); background: var(--surface-2); color: var(--text-dim);
  font-size: 0.85rem;
}
.wal-entry-icon [data-icon] { width: 15px; height: 15px; vertical-align: 0; }
.wal-entry-icon.credit { border-color: color-mix(in srgb, var(--ledger-credit) 55%, transparent); color: var(--ledger-credit); }
.wal-entry-body { flex: 1; min-width: 0; }
.wal-entry-body b {
  display: block; font-family: var(--font-title); font-size: 0.82rem; color: var(--text);
  font-weight: 600; line-height: 1.25; white-space: nowrap; overflow: hidden; text-overflow: ellipsis;
}
.wal-entry-body span { font-size: 0.66rem; color: var(--text-dim); }
.wal-entry-amt {
  font-family: var(--font-title); font-weight: 700; font-size: 0.92rem;
  text-align: right; flex-shrink: 0; color: var(--ledger-debit);
}
.wal-entry-amt.credit { color: var(--ledger-credit); }
.wal-empty { padding: 22px 16px; text-align: center; }
.wal-empty b { display: block; font-family: var(--font-title); font-size: 0.95rem; color: var(--text); margin-bottom: 4px; }
.wal-empty span { font-size: 0.7rem; color: var(--text-dim); }
.wal-earn {
  display: flex; align-items: center; gap: 11px; padding: 11px;
  border-radius: 9px; border: 1px dashed var(--border); background: transparent; margin-bottom: 7px;
}
.wal-earn-icon {
  width: 32px; height: 32px; border-radius: 8px; flex-shrink: 0;
  display: flex; align-items: center; justify-content: center;
  border: 1px solid var(--border); color: var(--text-dim); font-size: 1rem;
}
.wal-earn-icon [data-icon] { width: 17px; height: 17px; vertical-align: 0; }
.wal-earn-body { flex: 1; min-width: 0; }
.wal-earn-body b { display: block; font-family: var(--font-title); font-size: 0.85rem; color: var(--text); }
.wal-earn-body span { font-size: 0.68rem; color: var(--text-dim); }
.wal-earn-gain { font-family: var(--font-title); font-weight: 700; font-size: 0.9rem; color: var(--ledger-credit); flex-shrink: 0; }

/* -- Levels (S15-10, D-281): the disc, the XP bar and the level-up -------- */
/* The level as a square disc, ink on an outline: it is the player's own
   number, so it wears none of the medal gold, the live lime or the news
   orange (D-077). The level-up's big disc takes the news colour: it is news,
   seen once. */
.lv-disc {
  display: inline-flex; flex-direction: column; align-items: center; justify-content: center; flex-shrink: 0;
  width: 38px; height: 38px; box-sizing: border-box; border: 2px solid var(--text); border-radius: 8px;
  line-height: 1; color: var(--text);
}
.lv-disc b { font: 700 1.1rem var(--font-mono); font-variant-numeric: tabular-nums; }
.lv-disc small { font-size: 0.68rem; font-weight: 700; letter-spacing: 0.1em; text-transform: uppercase; margin-bottom: 2px; }
.lv-disc.lv-big { width: 96px; height: 96px; border-width: 3px; border-radius: 18px; border-color: var(--news-accent); }
.lv-disc.lv-big b { font-size: 2.8rem; }
/* The bar: what the level held, then this run's gain in a paler ink. */
.xp-bar {
  position: relative; display: block; height: 10px; box-sizing: border-box; border-radius: 5px;
  border: 1px solid var(--border); background: var(--surface-2); overflow: hidden;
}
.xp-bar i { position: absolute; top: 0; bottom: 0; left: 0; background: var(--text); }
.xp-bar i.xp-gain { background: color-mix(in srgb, var(--text) 45%, transparent); }
.lvup-card { display: flex; flex-direction: column; align-items: center; gap: 10px; padding: 28px 20px 18px; text-align: center; }
.lvup-card h2 { margin: 0; font: 700 1.9rem var(--font-title); }
.lvup-card p { margin: 0; font-size: 0.8rem; line-height: 1.45; max-width: 26ch; color: var(--text); }
.lvup-card .lvup-why, .lvup-card .lvup-for { margin-top: -6px; font-size: 0.74rem; color: var(--text-dim); }
.lvup-card .btn { min-width: 200px; min-height: 48px; margin-top: 4px; }
.lvup-tag {
  padding: 4px 10px; border-radius: 20px; background: var(--news-accent); color: var(--news-accent-ink);
  font: 700 0.68rem var(--font-mono); letter-spacing: 0.1em; text-transform: uppercase;
}
/* One pop, and none under reduced motion (Design's round 2). */
@media (prefers-reduced-motion: no-preference) {
  .lvup-card.play .lv-disc { animation: lv-pop 0.6s cubic-bezier(0.2, 1.4, 0.4, 1) both; }
  .lvup-card.play h2, .lvup-card.play p, .lvup-card.play .lvup-tag { animation: lv-fade 0.4s 0.35s both; }
}
@keyframes lv-pop { from { transform: scale(0.4); opacity: 0; } to { transform: none; opacity: 1; } }
@keyframes lv-fade { from { opacity: 0; transform: translateY(6px); } to { opacity: 1; transform: none; } }
/* Progress's head: the level, its title, the bar and when the next title comes. */
.pg-hero {
  display: flex; align-items: center; gap: 12px; padding: 12px; margin-bottom: 4px;
  border: 1px solid var(--border); border-radius: 12px; background: var(--panel);
}
.pg-hero .lv-disc { width: 58px; height: 58px; border-radius: 12px; }
.pg-hero .lv-disc b { font-size: 1.7rem; }
.pg-hero-b { flex: 1; min-width: 0; display: flex; flex-direction: column; gap: 5px; }
.pg-hero-b > b { font: 700 1.25rem/1.05 var(--font-title); color: var(--text); }
.pg-hero-b small { font-size: 0.68rem; line-height: 1.35; color: var(--text-dim); }
/* margin-top and padding, never the margin shorthand: a direct child of
   .page-body is centred in the page's column by its auto side margins, and a
   shorthand here, later in the file, put the note at the left edge of a wide
   screen (Andres, 2026-10-04). */
.pg-xp-note { margin-top: 4px; padding: 0 2px; font-size: 0.68rem; line-height: 1.4; color: var(--text-dim); }
.pg-xp { color: var(--text); font-weight: 700; white-space: nowrap; }
.prof-lv { font-size: 0.74rem; font-weight: 700; color: var(--text); }

/* -- Progress (pg-) : the ladders, D-154 -------------------------------- */
/* One row per family: icon, body, holding. FLEX and not a grid on purpose --
   `.lb-row` is a three-column grid and a fourth child silently lands on a
   second line (D-124), which is a trap a row with an optional badge would walk
   straight into. */
.pg-row {
  display: flex; align-items: center; gap: 11px; padding: 11px;
  border-radius: 9px; border: 1px solid var(--border); background: var(--panel); margin-bottom: 7px;
}
.pg-icon {
  width: 32px; height: 32px; border-radius: 8px; flex-shrink: 0;
  display: flex; align-items: center; justify-content: center;
  border: 1px solid var(--border); color: var(--text-dim);
}
.pg-icon [data-icon] { width: 17px; height: 17px; vertical-align: 0; }
.pg-body { flex: 1; min-width: 0; }
.pg-body b { display: block; font-family: var(--font-title); font-size: 0.85rem; color: var(--text); }
/* What a collection row counts (D-287): "6 letters or more", a line of its own
   under the name, since beside it the hint wrapped a word onto a line alone at 320px. */
.pg-what { display: block; font-size: 0.68rem; color: var(--text-dim); }
.pg-next { display: block; font-size: 0.68rem; color: var(--text-dim); }
.pg-next b { display: inline; font-family: inherit; font-size: inherit; color: var(--text-dim); }
.pg-topped { color: var(--text-dim); }
.pg-topped [data-icon] { width: 12px; height: 12px; vertical-align: -1px; }
/* The distance from the rung below to the rung above. A bar, not a percentage:
   the number that matters is already written beside it. */
.pg-bar {
  display: block; height: 3px; margin-top: 6px; border-radius: 2px;
  background: color-mix(in srgb, var(--text) 12%, transparent); overflow: hidden;
}
.pg-bar i { display: block; height: 100%; background: var(--gold); border-radius: 2px; }
.pg-now { font-family: var(--font-title); font-weight: 700; font-size: 1.05rem; color: var(--text); flex-shrink: 0; }

/* The streak, on the screen a player lands on (S8-141). One line above the
   wall's own note -- the grid below it is the evidence. */
.cal-streak {
  text-align: center; font-size: 0.8rem; color: var(--text); margin: 10px 0 0;
  display: flex; align-items: center; justify-content: center; gap: 6px;
}
.cal-streak b { font-family: var(--font-title); }
.cal-streak [data-icon] { width: 15px; height: 15px; vertical-align: 0; color: var(--gold); }

/* ── Profile (prof-) ────────────────────────────────────────────────────── */
.prof-id { display: flex; align-items: center; gap: 12px; }
.prof-tile { flex-shrink: 0; }
/* The player's name-tile (D-208, drawn by Design as D-209). A board tile's
   material with three differences, so it never reads as a letter you could
   play, none of them a colour: rounder corners (16% of the side, a board tile
   is ~11%), an inner rule like a stamped plate, and the streak as a TAB that
   breaks the tile's edge where a board tile's points sit inside it. The tab
   is the tile's own ink turned inside out: not gold (a top result) and not the
   live colour (today). Everything scales from --nt, set by the host's rule,
   except the numeral, which never drops under the 0.68rem floor. */
.name-tile {
  --nt: 52px;
  position: relative; flex-shrink: 0;
  width: var(--nt); height: var(--nt);
  border-radius: calc(var(--nt) * 0.16);
  background: var(--tile-bg-free); border: 1.5px solid var(--tile-bdr);
  box-shadow: 2px 2px 0 var(--tile-shadow),
    inset 0 0 0 calc(var(--nt) * 0.055) var(--tile-bg-free),
    inset 0 0 0 calc(var(--nt) * 0.055 + 1px) var(--nt-frame);
  display: flex; align-items: center; justify-content: center;
}
.prof-tile .name-tile { --nt: 54px; }
.recap-me .name-tile { --nt: 52px; }
.nt-l {
  font-family: var(--font-title); font-weight: 700; font-size: calc(var(--nt) * 0.5); line-height: 1;
  color: var(--tile-text); text-transform: uppercase; transform: translateY(-3%);
}
/* The tab: both numerals stacked in one grid cell, so a roll never reflows. */
.nt-tab {
  position: absolute; right: calc(var(--nt) * -0.1); bottom: calc(var(--nt) * -0.1);
  display: grid; place-items: center; overflow: hidden;
  min-width: max(17px, calc(var(--nt) * 0.4)); height: max(16px, calc(var(--nt) * 0.34));
  padding: 0 max(4px, calc(var(--nt) * 0.07)); border-radius: calc(var(--nt) * 0.17);
  background: var(--nt-tab-bg); color: var(--nt-tab-ink); box-shadow: 0 0 0 1.5px var(--panel);
  font-family: var(--font-mono); font-weight: 700; font-size: max(0.68rem, calc(var(--nt) * 0.21));
  line-height: 1; font-variant-numeric: tabular-nums; letter-spacing: -0.02em;
}
.nt-tab > b { grid-area: 1 / 1; font-weight: 700; }
/* The count-up, welcome card only: the old numeral rolls up and out at 0.9s,
   the new one rolls in, and the tab pops once — transform and opacity, one
   shot. From no streak to 1 there is nothing to roll: the tab is born. */
.nt-up .nt-was { animation: ntOut 0.26s cubic-bezier(0.5, 0, 0.8, 0.3) 0.9s both; }
.nt-up .nt-now { animation: ntIn 0.34s cubic-bezier(0.2, 0.8, 0.3, 1.25) 1s both; }
.nt-up .nt-tab { animation: ntPop 0.4s ease-out 1s both; }
.nt-up.nt-born .nt-tab { animation: ntBorn 0.42s cubic-bezier(0.2, 0.8, 0.3, 1.3) 0.9s both; }
@keyframes ntOut { to { transform: translateY(-120%); opacity: 0; } }
@keyframes ntIn { from { transform: translateY(120%); opacity: 0; } }
@keyframes ntPop { 0%, 100% { transform: scale(1); } 40% { transform: scale(1.24); } }
@keyframes ntBorn { from { transform: scale(0); opacity: 0; } }
/* Reduced motion: the tile is simply drawn at the real streak. */
@media (prefers-reduced-motion: reduce) {
  .nt-up .nt-was { display: none; }
  .nt-up .nt-tab, .nt-up .nt-now { animation: none; }
}
}
.prof-name { flex: 1; min-width: 0; }
.prof-name b { display: block; font-family: var(--font-title); font-size: 1.2rem; color: var(--text); line-height: 1.2; }
.prof-name > span { font-size: 0.68rem; color: var(--text-dim); }
/* Private is the whole point (D-078): no visitor view, no public handle. It is
   also what lets History show archive scores beside ranked ones honestly. */
.prof-private {
  display: inline-flex; align-items: center; gap: 5px; margin-top: 5px;
  font-size: 0.6rem; letter-spacing: 0.12em; text-transform: uppercase; color: var(--text-dim);
  border: 1px solid var(--border); background: var(--surface-2);
  padding: 2px 7px; border-radius: 20px;
}
.prof-counts { display: grid; grid-template-columns: 1fr 1fr; gap: 7px; margin-top: 13px; }
.prof-count { border: 1px solid var(--border); border-radius: 8px; padding: 9px 10px; background: var(--surface-2); }
.prof-count span { display: block; font-size: 0.6rem; letter-spacing: 0.11em; text-transform: uppercase; color: var(--text-dim); }
.prof-count b { font-family: var(--font-title); font-size: 1.26rem; color: var(--text); line-height: 1.2; }
.prof-count i { font-style: normal; font-size: 0.62rem; color: var(--text-dim); margin-left: 4px; }
.prof-streak {
  display: flex; align-items: center; gap: 3px; margin-top: 9px; padding: 9px 10px;
  border: 1px solid var(--border); border-radius: 8px; background: var(--surface-2);
}
.prof-streak-label { font-size: 0.6rem; letter-spacing: 0.11em; text-transform: uppercase; color: var(--text-dim); margin-right: 6px; white-space: nowrap; }
/* A day you did not visit is an EMPTY SLOT, so it is an outline; a day you
   kept is filled. It used to be filled in --streak-missed, which every palette
   set to --surface-2 — the exact colour of the strip behind it, so eleven of
   the last fourteen days were invisible by construction rather than by
   accident (Andres 2026-09-19). The border stays on the filled states too, or
   the boxes would change size as the streak grows. */
.prof-streak-day {
  flex: 1; height: 15px; border-radius: 3px; display: block;
  background: transparent;
  border: 1.5px solid var(--streak-missed);
}
.prof-streak-day.kept { background: var(--streak-kept); border-color: var(--streak-kept); }
/* Today, dug: the live fill, edged in --streak-due so it holds on a pale
   panel (D-209). */
.prof-streak-day.today { background: var(--streak-today); border-color: var(--streak-due); }
/* Today, still to dig (D-208, D-209): a dark-edged ring. The outer 2px in
   --streak-due carries the contrast, the inner 2px in --streak-today the
   meaning. Cells are border-box, so the box keeps its size. */
.prof-streak-day.due { border: 2px solid var(--streak-due); box-shadow: inset 0 0 0 2px var(--streak-today); }
.prof-tabs { display: flex; gap: 6px; margin-top: 13px; }
.prof-tabs button {
  flex: 1; min-height: 32px; padding: 6px; border-radius: 8px;
  border: 1px solid var(--border); background: var(--surface-2); color: var(--text-dim);
  font-family: var(--font-mono); font-size: 0.68rem; letter-spacing: 0.1em;
  text-transform: uppercase; cursor: pointer;
}
.prof-tabs button.on { border-color: var(--gold); color: var(--gold); background: color-mix(in srgb, var(--gold) 9%, transparent); }
/* One board = one item: its best-run row, and (D-094) the runs folded under
   it. The hairline lives on the ITEM, so an open run list sits inside its
   board's rule rather than after it. */
.prof-day-item { border-bottom: 1px solid var(--border); }
.prof-day-item:last-child { border-bottom: none; }
.prof-day-row { display: flex; align-items: stretch; }
.prof-day {
  display: flex; align-items: center; gap: 9px; flex: 1; min-width: 0;
  padding: 9px 11px; border: none;
  background: transparent; text-align: left; cursor: pointer; font-family: var(--font-mono);
}
/* The run count is its own button BESIDE the row, never inside it (a button
   in a button is invalid): the row still opens the day, the count opens the
   list. 48px wide and the row's full height, so it clears G6's tap target. */
.prof-runs-btn {
  flex-shrink: 0; width: 48px; min-height: 44px;
  display: flex; flex-direction: column; align-items: center; justify-content: center; gap: 1px;
  border: none; border-left: 1px solid var(--border); background: transparent;
  color: var(--text-dim); font-family: var(--font-mono); cursor: pointer;
}
.prof-runs-btn b { font-family: var(--font-title); font-size: 0.9rem; line-height: 1; color: var(--text); }
.prof-runs-btn em { font-style: normal; font-size: 0.55rem; letter-spacing: 0.1em; text-transform: uppercase; }
.prof-runs-btn i { font-style: normal; font-size: 0.6rem; line-height: 1; }
.prof-runs-btn.on { color: var(--gold); background: color-mix(in srgb, var(--gold) 9%, transparent); }
.prof-runs { background: var(--surface-2); border-top: 1px dashed var(--border); padding: 3px 0; }
.prof-run { display: flex; align-items: center; gap: 8px; padding: 6px 11px 6px 16px; font-family: var(--font-mono); }
.prof-run-pos { width: 30px; flex-shrink: 0; font-size: 0.6rem; letter-spacing: 0.06em; text-transform: uppercase; color: var(--text-dim); }
.prof-run-pos.best { color: var(--gold); font-weight: 700; }
/* The D-071 pair, in D-077's colours: LIVE ranked on an open leaderboard,
   ARCHIVE is yours only. Outlined, not filled — these are labels in a list,
   not the board's status badge. */
.prof-run-kind {
  flex-shrink: 0; font-size: 0.56rem; letter-spacing: 0.1em; text-transform: uppercase;
  padding: 1px 6px; border-radius: 20px; border: 1px solid;
}
.prof-run-kind.live { color: var(--live-text); border-color: color-mix(in srgb, var(--live) 60%, transparent); }
.prof-run-kind.archive { color: var(--gold); border-color: color-mix(in srgb, var(--gold) 55%, transparent); }
.prof-run-meta { flex: 1; min-width: 0; font-size: 0.6rem; line-height: 1.3; color: var(--text-dim); white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }
.prof-run-meta b { display: block; font-weight: 400; font-size: 0.64rem; color: var(--text); overflow: hidden; text-overflow: ellipsis; }
.prof-run-score { width: 46px; flex-shrink: 0; text-align: right; font-family: var(--font-title); font-weight: 700; font-size: 0.85rem; color: var(--text); }
.prof-day-date { width: 36px; flex-shrink: 0; font-family: var(--font-title); font-weight: 700; font-size: 0.86rem; color: var(--text); line-height: 1.05; }
.prof-day-date em { display: block; font-style: normal; font-family: var(--font-mono); font-size: 0.58rem; letter-spacing: 0.1em; color: var(--text-dim); }
.prof-day-body { flex: 1; min-width: 0; }
.prof-day-body b { display: block; font-family: var(--font-title); font-size: 0.82rem; color: var(--text); }
.prof-day-body span { font-size: 0.62rem; color: var(--text-dim); white-space: nowrap; overflow: hidden; text-overflow: ellipsis; display: block; }
.prof-medals { flex-shrink: 0; color: var(--medal-gold); font-size: 0.62rem; display: inline-flex; gap: 1px; }
.prof-medals [data-icon] { width: 0.95em; height: 0.95em; vertical-align: 0; }
.prof-day-score { width: 46px; text-align: right; flex-shrink: 0; font-family: var(--font-title); font-weight: 700; font-size: 0.9rem; color: var(--text); }
/* D-071's pair, in D-077's colours — the same two it means on the badges and
   on the run tags beneath (D-094): a ranked #N in --live (counted on an open
   leaderboard), archive in gold — yours only, never ranked. It was the other
   way round until the run tags sat under it and contradicted it. */
.prof-rank { display: block; font-style: normal; font-family: var(--font-mono); font-size: 0.6rem; letter-spacing: 0.06em; font-weight: 400; color: var(--live-text); }
.prof-rank.archive { color: var(--gold); }
.prof-word { display: flex; align-items: center; gap: 9px; padding: 9px 11px; border-bottom: 1px solid var(--border); }
.prof-word:last-child { border-bottom: none; }
.prof-word-text { font-family: var(--font-title); font-weight: 700; font-size: 0.9rem; color: var(--text); letter-spacing: 0.04em; }
.prof-word-meta { flex: 1; font-size: 0.62rem; letter-spacing: 0.04em; text-transform: uppercase; color: var(--text-dim); }
.prof-word-rare {
  font-size: 0.6rem; letter-spacing: 0.1em; text-transform: uppercase; color: var(--rare-glow);
  border: 1px solid color-mix(in srgb, var(--rare-glow) 55%, transparent); padding: 2px 6px; border-radius: 20px;
}
.prof-stat { display: flex; align-items: center; justify-content: space-between; gap: 9px; padding: 9px 11px; border-bottom: 1px solid var(--border); }
.prof-stat:last-child { border-bottom: none; }
.prof-stat-key { font-family: var(--font-title); font-size: 0.82rem; color: var(--text); }
.prof-stat-val { font-family: var(--font-title); font-weight: 700; font-size: 0.9rem; color: var(--gold-lt); }
.prof-stub { padding: 28px 16px; text-align: center; }
.prof-stub b { display: block; font-family: var(--font-title); font-size: 1rem; color: var(--text); margin-bottom: 8px; }
.prof-stub span { font-size: 0.72rem; color: var(--text-dim); line-height: 1.7; }

/* ══════════════════════════════════════════════════════════════════════════
   THE PRICE AREA (D-081)
   A SERVICE button is one that offers a board. It has one body and a fixed
   lane on its leading edge that states the price — free, priced, or priced
   and briefly waived. The lane is the only part that repaints: paintPrice()
   owns it, so a window closing or a holding going short leaves the action's
   label, size, position and handler untouched.

   Three grounds, three meanings. SOLID GOLD = this is a transaction (the
   lane's CONTENTS say which currency: a key means keys, the test card means
   attention — and a ticket, in S9c, will mean tickets). GOLD TINT behind a
   gold rim = free, and worth something. ORE = a window that closes. The key
   never appears in a free lane, so scanning for it stays a reliable test.

   In survey --rare-glow, --gold and --accent are near or the same hex, so the
   grounds cannot differ by hue; they differ by material, through the role
   tokens below (D-099, S10b-2b).
   ══════════════════════════════════════════════════════════════════════════ */

/* Deliberately NOT .btn-big: the service button is its own geometry (56px,
   10px radius, uppercase). It must also out-order `.heap-foot .btn { display:
   block }`, which ties on specificity — hence its place at the end of the
   file. */
.btn.btn-svc {
  display: flex;
  align-items: stretch;
  justify-content: flex-start;
  width: 100%;
  min-height: 56px;
  padding: 0;
  overflow: hidden;
  border-radius: 10px;
  font-size: 0.72rem;
  letter-spacing: 0.12em;
  text-transform: uppercase;
  background: var(--accent);
  border: 1px solid var(--accent-bdr);
  color: var(--accent-ink);
}
.btn.btn-svc:not(:disabled):hover { filter: brightness(1.1); }

/* The lane. A real width, not a minimum, so every lane in a stack aligns
   whatever its caption says — the price is never the part that clips. */
.btn-price {
  position: relative;
  flex-shrink: 0;
  align-self: stretch;
  display: flex;
  flex-direction: column;
  align-items: center;
  justify-content: center;
  gap: 2px;
  width: 92px;
  min-width: 92px;
  padding: 6px 8px;
  margin: -1px 0 -1px -1px;
  border-radius: 9px 0 0 9px;
  border-right: 1px solid var(--accent-bdr);
  text-align: center;
}
.price-now {
  display: flex; align-items: center; gap: 5px;
  font-family: var(--font-title); font-size: 1.12rem; font-weight: 700;
  line-height: 1.05; letter-spacing: 0.01em;
}
.price-was {
  display: flex; align-items: center; gap: 4px;
  font-size: 0.67rem; font-weight: 600; line-height: 1; white-space: nowrap;
  text-decoration: line-through; text-decoration-thickness: 1.5px;
}
.price-cap {
  font-size: 0.63rem; letter-spacing: 0.07em; font-weight: 700;
  line-height: 1.2; white-space: nowrap; text-transform: uppercase;
}
.price-free {
  font-family: var(--font-title); font-size: 0.98rem; font-weight: 700;
  letter-spacing: 0.04em; line-height: 1.05;
}

/* The body. `.btn` is white-space: nowrap for good reasons elsewhere; the
   label is the one part of a service button that may wrap. */
.btn-lab {
  flex: 1; min-width: 0;
  display: flex; flex-direction: column;
  align-items: center; justify-content: center;
  gap: 2px; padding: 10px 14px;
  text-align: center; white-space: normal;
}
.btn-sub {
  font-size: 0.69rem; letter-spacing: 0.07em; font-weight: 600;
  text-transform: uppercase; color: var(--accent-ink);
}

/* THE LANES READ FROM ROLES, NOT FROM --gold (D-099, S10b-2b). Each ground used
   to name the colour it wanted, which is exactly why every lane collapsed into
   one orange rectangle outside Strata: in Classic and Survey --gold and
   --rare-glow are the same hex, and in Survey --accent is too. A palette that
   cannot separate them by HUE can still separate them by MATERIAL — but only if
   it gets to answer the question, which is what these role tokens are. This is
   what retires the "price lanes are excluded from the theme sweep" note. */

/* 1 · ALWAYS FREE — the lane looks like it holds a price and then says there
   is none. The most flattering reading of free: this has value, and it is
   yours. One rung below a solid price on purpose; if the tint ever creeps
   above ~25% the ladder collapses into it. */
.btn-free .btn-price {
  background: var(--lane-free-bg);
  color: var(--lane-free-ink);
  border-right-color: var(--lane-free-edge);
  box-shadow: inset 0 0 0 1px var(--lane-free-rim);
}
.btn-free .price-free { font-size: 1.04rem; letter-spacing: 0.06em; }
.btn-free .price-cap { color: var(--lane-free-ink); }

/* 2 · FREE WHILE A WINDOW IS OPEN — the ore, because a window is what expires.
   NOT used for today's board: what lapses there is the RANKING, not the price
   (D-056/D-081). CSS only until a campaign concept exists. */
.btn-timed .btn-price {
  background: var(--lane-live-bg); color: var(--lane-live-ink); border-right-color: var(--lane-live-edge);
}
.btn-timed .price-was,
.btn-timed .price-cap { color: var(--lane-live-ink); }

/* 3/4/5 · EVERY TRANSACTION — solid gold, the loudest thing the palette will
   do without touching the ore. Keys or attention; the contents say which. */
.btn-sale .btn-price,
.btn-cost .btn-price,
.btn-ad .btn-price {
  background: var(--lane-pay-bg);
  color: var(--lane-pay-ink);
  border-right-color: var(--lane-pay-edge);
  box-shadow: inset 0 0 0 1px var(--lane-pay-rim);
  overflow: hidden;
}
.btn-sale .price-was, .btn-sale .price-cap,
.btn-cost .price-was, .btn-cost .price-cap,
.btn-ad .price-cap { color: var(--lane-pay-ink); }
/* On a gold ground a gold key is metal on metal and dissolves, so here — and
   only here — it is struck in the lane's own ink, like a mark engraved in the
   plate. The one sanctioned break from the fixed metal (see .key). */
.btn-sale .key,
.btn-cost .key {
  --key-metal: var(--lane-pay-ink);
  --key-edge: transparent;
}

/* The saving is a flash across the foot of the lane — ore, because a cut price
   is a window that shuts. CSS only: no discount concept exists (D-081). */
.btn-sale .btn-price { padding-bottom: 18px; }
.btn-sale .price-cap {
  position: absolute; left: 0; right: 0; bottom: 0;
  margin: 0; padding: 3px 0;
  background: var(--lane-live-bg); color: var(--lane-live-ink); letter-spacing: 0.09em;
}

/* 6 · PRICED, AND SHORT — never disabled. Disabling hides the price, which is
   the one thing that must stay visible. The price stays fully lit, the edge
   breaks to a dash, the lane says when more keys come, and the tap opens the
   Keys page. */
.btn-cost.btn-short-funds .btn-price {
  border-right-style: dashed;
  box-shadow: inset 0 0 0 1px var(--short-rim);
}
.btn-cost.btn-short-funds .price-cap {
  background: var(--pill-bg);
  color: var(--pill-ink); border-radius: 20px; padding: 1px 8px;
}

/* 7 · WATCH AN AD — off-platform only (D-061), so CSS only here. Gold lane,
   because attention is still a transaction; no key, because no key moves. */
.btn-ad .btn-price { gap: 3px; }
.btn-ad .btn-price svg { width: 27px; height: 23px; display: block; }
.ad-tv rect[fill^="var"] { opacity: 0.92; }

/* Off-dig toast (D-082). #message-box is absolutely positioned INSIDE
   #word-area so the board never shifts (D-077) — and #word-area is hidden on
   every screen but the dig, which is why unlock confirmations and errors were
   written and thrown away. This is the host flash() uses everywhere else:
   fixed, clear of the nav, utility band, and never part of any layout. */
/* ══════════════════════════════════════════════════════════════════════════
   WORDS — the dictionary and the word of the day (D-096), and the SHOP
   placeholder (D-095). Built from the house page anatomy and tokens; the
   designer's flow rewrite owns the final look (docs/design-brief-nav-words.md).
   ══════════════════════════════════════════════════════════════════════════ */
.dict-search-row { max-width: var(--measure); margin: 8px auto 0; }
#dict-search {
  width: 100%;
  min-height: 40px; /* G6 */
  padding: 8px 12px;
  background: var(--surface-2);
  color: var(--text);
  border: 1px solid var(--border);
  border-radius: var(--radius);
  font-family: var(--font-mono);
  font-size: 1rem; /* 16px: a smaller input makes iOS zoom the page on focus */
  letter-spacing: 0.08em;
  text-transform: uppercase;
}
#dict-search::placeholder { text-transform: none; letter-spacing: 0; color: var(--text-dim); }
#dict-search:focus { outline: none; border-color: var(--gold); }
.dict-verdict {
  margin: 2px 0 4px;
  padding: 10px 12px;
  border-radius: 8px;
  border: 1px solid var(--border);
  background: var(--panel);
  font-size: 0.85rem;
  color: var(--text-dim);
}
.dict-verdict b { font-family: var(--font-title); font-size: 1.05rem; letter-spacing: 0.06em; color: var(--text); }
.dict-verdict.ok { border-color: var(--green); }
.dict-verdict.ok b { color: var(--green); }
.dict-verdict.miss { border-style: dashed; }
.dict-row {
  display: flex; align-items: center; gap: 8px;
  padding: 7px 12px;
  font-size: 0.82rem;
}
.dict-row + .dict-row { border-top: 1px solid var(--border); }
.dict-row.me { background: color-mix(in srgb, var(--gold) 10%, transparent); }
.dict-w { flex: 1; min-width: 0; font-weight: 600; letter-spacing: 0.06em; color: var(--text); overflow: hidden; text-overflow: ellipsis; }
.dict-new {
  font-size: 0.62rem; letter-spacing: 0.1em; text-transform: uppercase;
  padding: 1px 6px; border-radius: 3px;
  border: 1px solid var(--gold); color: var(--gold);
}
.dict-pts { font-family: var(--font-title); font-weight: 700; color: var(--gold-lt); min-width: 2ch; text-align: right; }

.wotd-hero {
  text-align: center;
  padding: 16px 12px 12px;
  border: 1px solid var(--gold);
  border-radius: 8px;
  background: color-mix(in srgb, var(--gold) 8%, var(--panel));
}
.wotd-label { font-size: 0.68rem; letter-spacing: 0.14em; text-transform: uppercase; color: var(--gold); }
/* The word as ONE row of letter tiles: each tile shrinks (never below its
   letter) rather than wrapping, so QUARTZES does not strand its S on a
   second line at 320px. The ratio keeps a shrunk tile a tile. */
.wotd-word { display: flex; justify-content: center; gap: 4px; margin: 12px 0 10px; }
.wotd-tile {
  position: relative;
  flex: 0 1 34px; min-width: 0;
  aspect-ratio: 34 / 40;
  display: inline-flex; align-items: center; justify-content: center;
  background: var(--tile-bg-free);
  border: 1px solid var(--tile-bdr);
  color: var(--tile-text);
  border-radius: 5px;
  box-shadow: 2px 3px 0 var(--tile-shadow);
  font-family: var(--font-mono); font-weight: 700; font-size: clamp(0.8rem, 4.6vw, 1.15rem);
}
.wotd-tile i { position: absolute; bottom: 1px; right: 3px; font-style: normal; font-size: 0.55rem; font-weight: 600; color: var(--tile-pts-clr); }
.wotd-meta { font-size: 0.74rem; color: var(--text-dim); }
.wotd-open { margin-top: 8px; min-height: 40px; }
.wotd-row {
  display: flex; align-items: center; gap: 10px;
  width: 100%;
  min-height: 44px; /* G6 */
  padding: 6px 12px;
  background: transparent;
  border: none;
  color: var(--text);
  font-family: var(--font-mono);
  text-align: left;
  cursor: pointer;
}
.wotd-row + .wotd-row { border-top: 1px solid var(--border); }
.wotd-row:hover { background: color-mix(in srgb, var(--gold) 8%, transparent); }
.wotd-row-day { display: flex; flex-direction: column; width: 64px; flex-shrink: 0; font-size: 0.74rem; color: var(--text); }
.wotd-row-day em { font-style: normal; font-size: 0.62rem; color: var(--text-dim); }
.wotd-row-word { flex: 1; min-width: 0; font-weight: 700; letter-spacing: 0.08em; overflow: hidden; text-overflow: ellipsis; }
.wotd-row-pts { font-family: var(--font-title); font-weight: 700; color: var(--gold-lt); }
.wotd-row-pts em { font-style: normal; font-size: 0.62rem; color: var(--text-dim); margin-left: 3px; }
.wotd-more { display: block; width: 100%; margin-top: 8px; min-height: 40px; }

.shop-card {
  padding: 12px 14px;
  border: 1px solid var(--border);
  border-radius: 8px;
  background: var(--panel);
}
.shop-card-head { display: flex; align-items: center; gap: 12px; }
.shop-card-title { flex: 1; min-width: 0; }
.shop-card-title b { display: block; font-family: var(--font-title); font-size: 0.95rem; color: var(--text); }
.shop-card-title span { font-size: 0.7rem; color: var(--text-dim); }
.shop-ticket { display: inline-flex; line-height: 1; color: var(--tkt-ink); }
.shop-ticket svg { width: 22px; height: 22px; }
.shop-soon { font-size: 0.74rem; color: var(--text-dim); line-height: 1.55; margin: 10px 0 0; }
.shop-soon b { color: var(--gold-lt); }
.shop-link { margin-top: 10px; min-height: 40px; }

#toast {
  position: fixed;
  left: 50%;
  transform: translateX(-50%);
  bottom: calc(64px + env(safe-area-inset-bottom, 0px));
  z-index: 32000;
  max-width: min(92vw, var(--measure));
  padding: 0 14px;
  font-size: 0.78rem;
  line-height: 1.5;
  text-align: center;
  pointer-events: none;
  text-wrap: pretty;
}
#toast:empty { display: none; }
#toast.msg-ok, #toast.msg-err, #toast.msg-warn, #toast.msg-info {
  padding: 8px 14px;
  border: 1px solid var(--border);
  border-radius: var(--radius);
  background: var(--panel);
  box-shadow: 0 4px 16px rgba(0, 0, 0, 0.4);
}

/* ── Selection (D-083) ──────────────────────────────────────────────────────
   Andres 2026-09-09: "i see a cursor blinking at various game elements when I
   click with mouse on the gameboard."

   Only `.tile` was ever user-select: none, so every other element was ordinary
   selectable text and a click landed a text insertion point in it. Whether the
   host paints that as a blinking caret is its business (caret browsing, focus
   models differ) — but a node that cannot be selected cannot hold one, so this
   removes the cause either way.

   The caret is the visible symptom; the reasons that matter are on a phone. A
   long-press on a tile or a button raises the selection callout and magnifier,
   and fast tapping (Submit, Undo, tiles in sequence) trips double-click
   selection and highlights labels mid-run. Both are real interruptions in a
   game where every interaction is a tap.

   Scoped to #app, NOT body: admin.html links this same stylesheet and its root
   is #admin. A blanket rule would break copying level JSON, post ids and
   feedback text out of the console.

   Turning selection off everywhere and stopping there is the version of this
   that annoys people, so the exceptions below are not optional.

   #build-tag sits OUTSIDE #app (it is a body child), and it is
   pointer-events: none — so a click near it falls through to body, which is
   selectable, and the browser parks the insertion point in the nearest text it
   can find: the build tag. Andres 2026-09-10 saw exactly that. It is named here
   rather than widening the rule to body, for the admin reason above (D-089). */
#app,
#build-tag {
  user-select: none;
  -webkit-user-select: none;
  /* Kills the iOS long-press callout on tiles and buttons. */
  -webkit-touch-callout: none;
}

/* Text a player may legitimately want to take with them: the how-to-play copy
   and its ENABLE credit, and their own feedback while they are writing it.

   Leaderboard names USED to be here (D-083) and were taken out (D-089): a
   results row is tapped far more often than it is copied, every tap parked a
   caret in the name, and a Reddit username is one tap away on Reddit itself. */
#app .faq-body,
#app input,
#app textarea {
  user-select: text;
  -webkit-user-select: text;
  -webkit-touch-callout: default;
}

/* Stubborn-repeat tip (D-088). The SAME word refused twice in one run is the
   strongest signal we get that a player believes a word is real, so it earns
   one tap — but a timed run (R3) must not be interrupted, so this is never a
   modal and never moves anything. `bottom: 100%` hangs it ABOVE #word-area
   (the positioning host, which does not clip), so it floats over the board's
   bottom edge and the layout is untouched — the same discipline #message-box
   follows for the same reason (D-077). 44px keeps a real tap target (G6). */
#suggest-tip {
  position: absolute;
  bottom: 100%;
  left: 10px;
  right: 10px;
  margin-bottom: 8px;
  min-height: 44px;
  display: flex;
  align-items: stretch;
  border: 1px dashed var(--accent-bdr);
  border-radius: 10px;
  background: var(--surface-2);
  color: var(--text);
  font-size: 0.78rem;
  line-height: 1.25;
}
/* The tap that suggests is the whole note but its close corner (D-275). */
#suggest-go {
  flex: 1;
  min-height: 44px;
  padding: 6px 40px 6px 14px;
  border: none;
  background: none;
  color: inherit;
  font: inherit;
  text-align: center;
  cursor: pointer;
}
#suggest-go:active { transform: translateY(1px); }
#suggest-tip .note-x { top: 4px; }
#suggest-tip.tip-sent {
  border-style: solid;
  border-color: var(--gold);
}
#suggest-tip.tip-sent #suggest-go { cursor: default; }

/* ══ D-125/D-127 · The key moments ════════════════════════════════════════
   The three daily keys are a LADDER — arrive, dig, discover — and this is the
   one component that says so, in two densities: compact on the day sheet and
   the Keys page, open on the welcome present where the whole schedule is
   stated once.

   NO NEW COLOUR. A key moment is a present, so it may not borrow the ore
   (--rare-glow), the live colour or the badge berry (D-077). The key already
   has a material of its own — --key-metal / --key-edge — and that metal paints
   every earned state here. The only accent is --gold on the spend prompt,
   where it already means "this costs a key".

   Design's delivery set these labels at 0.55-0.63rem; every one is lifted to
   the 0.68rem floor (D-077), so the card is a little taller than the mock and
   a good deal more readable (D-127's first adaptation). */

/* A key that did NOT move, because the purse was full: the same drawing with
   the metal drained out of it, so it reads as the same object un-minted rather
   than as a different glyph. Ours is two pseudo-elements, not an SVG, so the
   bow keeps its ring and both parts lose the metal (D-127's second
   adaptation — Design drew this on an SVG we do not have). */
.key.key-void { filter: none; opacity: 0.75; }
.key.key-void::before { border-color: var(--text-dim); }
.key.key-void::after {
  background:
    linear-gradient(var(--text-dim), var(--text-dim)) 0 0 / 100% 0.12em no-repeat,
    linear-gradient(var(--text-dim), var(--text-dim)) 100% 0 / 0.1em 100% no-repeat,
    linear-gradient(var(--text-dim), var(--text-dim)) 0.3em 0 / 0.1em 75% no-repeat;
}

/* The slot ladder. The rail is the point: progress reads leftward because the
   segment BEHIND a finished slot goes metal, which is the "2 of 3" nobody has
   to read. */
.tdy-card {
  border: 1px solid var(--border);
  border-radius: 11px;
  background: var(--surface-2);
  padding: 9px 10px 0;
  display: flex;
  flex-direction: column;
  flex-shrink: 0;
}
.tdy-rail { display: flex; align-items: flex-start; }
.tdy-slot {
  flex: 1;
  min-width: 0;
  position: relative;
  display: flex;
  flex-direction: column;
  align-items: center;
  gap: 4px;
  text-align: center;
}
.tdy-mk {
  width: 22px;
  height: 22px;
  flex-shrink: 0;
  border-radius: 50%;
  display: flex;
  align-items: center;
  justify-content: center;
  border: 2px solid var(--border);
  background: var(--panel);
  position: relative;
  z-index: 1;
}
.tdy-mk svg { width: 13px; height: 13px; display: block; }
.tdy-tx { min-width: 0; display: flex; flex-direction: column; gap: 2px; }
.tdy-nm {
  font-family: var(--font-mono);
  font-size: 0.68rem;
  font-weight: 700;
  letter-spacing: 0.06em;
  text-transform: uppercase;
  color: var(--text-dim);
}
.tdy-cond, .tdy-rw { display: none; }
/* The connector belongs to the slot on its RIGHT and is painted by the state
   of the slot on its LEFT — which is what makes progress read leftward. */
.tdy-slot + .tdy-slot::before {
  content: "";
  position: absolute;
  top: 9px;
  height: 2px;
  left: calc(-50% + 15px);
  right: calc(50% + 15px);
  background: var(--border);
}
.tdy-slot.is-done + .tdy-slot::before,
.tdy-slot.is-full + .tdy-slot::before { background: var(--key-metal); }

/* done — the key is paid and already in the pill. Metal disc, punched tick. */
.tdy-slot.is-done .tdy-mk { background: var(--key-metal); border-color: var(--key-metal); color: var(--key-edge); }
.tdy-slot.is-done .tdy-nm { color: var(--text); }
/* done, full — done, but nothing moved. The rim is metal (it IS complete);
   the disc stays empty, because the key is the thing that did not arrive. */
.tdy-slot.is-full .tdy-mk { border-color: var(--key-metal); color: var(--key-metal); background: var(--panel); }
.tdy-slot.is-full .tdy-nm { color: var(--text); }
/* pending — the ring is the only hollow thing on the card, and the slot you
   are next to earn carries its name at full ink. */
.tdy-slot.is-next .tdy-mk { border-color: var(--text-dim); }
.tdy-slot.is-next .tdy-nm { color: var(--text); }
/* shut — the return key did not come today: no dig the day before (D-203).
   Not a miss (no metal) and not the next thing to earn (no ring to fill): a
   dashed rim, a day that simply did not offer it. */
.tdy-slot.is-off .tdy-mk { border-style: dashed; }
.tdy-slot.is-off .tdy-nm { opacity: 0.7; }

/* The line beneath: what to do next, or what happened. Its own row, so the
   ladder above never re-lays out as the day fills in. */
.tdy-next {
  display: flex;
  align-items: center;
  gap: 7px;
  width: 100%;
  min-height: 44px;
  padding: 0 1px;
  margin-top: 7px;
  border: none;
  border-top: 1px solid var(--border);
  background: none;
  text-align: left;
  font-family: var(--font-mono);
  font-size: 0.68rem;
  line-height: 1.4;
  color: var(--text-dim);
}
.tdy-next b { font-family: var(--font-mono); font-weight: 700; color: var(--text); letter-spacing: 0.02em; }
.tdy-next .tdy-kb { display: flex; align-items: center; gap: 3px; flex-shrink: 0; font-weight: 700; color: var(--text); }
.tdy-next .tdy-sp { flex: 1; min-width: 0; }
.tdy-next svg { width: 12px; height: 12px; flex-shrink: 0; }
/* A full purse is a SPEND PROMPT, not an error (the map's P5) — so the line
   becomes a link, in the metal that already means "this costs a key", and it
   is the whole 44px row rather than a word inside one. */
button.tdy-next { cursor: pointer; }
.tdy-next.is-full, .tdy-next.is-full b { color: var(--gold); }
.tdy-next.is-shut { color: var(--text-dim); }

/* Open density: the welcome's three rows. */
.tdy-open { padding: 4px 11px 5px; background: var(--panel); }
.tdy-open .tdy-rail { flex-direction: column; }
.tdy-open .tdy-slot {
  flex: none;
  width: 100%;
  flex-direction: row;
  align-items: center;
  gap: 11px;
  min-height: 46px;
  text-align: left;
}
.tdy-open .tdy-tx { flex: 1; }
.tdy-open .tdy-nm { letter-spacing: 0.08em; }
.tdy-open .tdy-cond { display: block; font-style: normal; font-size: 0.68rem; color: var(--text-dim); line-height: 1.35; }
.tdy-open .tdy-rw {
  display: flex;
  align-items: center;
  gap: 4px;
  flex-shrink: 0;
  font-family: var(--font-mono);
  font-size: 0.68rem;
  font-weight: 700;
  color: var(--text-dim);
}
.tdy-open .tdy-slot.is-done .tdy-rw { color: var(--key-metal); }
/* The rail turns vertical: the segment sits above the mark it leads into. */
.tdy-open .tdy-slot + .tdy-slot::before {
  top: -6px;
  bottom: auto;
  left: 10px;
  right: auto;
  width: 2px;
  height: 18px;
}

/* The welcome present. An .end-card under a scrim, per the house rule for a
   decision about the thing beneath. Only three pieces are its own: the title,
   the fanned pair of keys, the one sentence. */
/* ── The recap (D-204), as Design drew it (D-209): two weights, not four ────
   a GREETING (who) and a RESULT (how it ended), with the finds, the key and
   the streak as the result's small print. The worst case (three finds, a key,
   a streak, the brag) is ~440px, which fits a 320×568 phone between the
   header and the nav. The card keeps the house ✕ and its scroll, for phones
   shorter than that. */
.end-card.recap-card { display: flex; flex-direction: column; gap: 10px; padding: 12px; text-align: left; }

/* 1 · the greeting */
.recap-head { display: flex; align-items: center; gap: 15px; min-height: 52px; padding: 0 40px 0 2px; }
.recap-me { flex-shrink: 0; }
.wb-hi { display: flex; flex-direction: column; gap: 1px; min-width: 0; }
.wb-kick { font-size: 0.68rem; letter-spacing: 0.16em; text-transform: uppercase; font-weight: 700; color: var(--text-dim); }
.wb-name {
  font-family: var(--font-title); font-weight: 700; font-size: 1.4rem; line-height: 1.1; color: var(--text);
  overflow: hidden; text-overflow: ellipsis; white-space: nowrap;
}
.wb-name.is-long { font-size: 1.08rem; } /* 15–20 characters: Reddit's longest fit one line */

/* 2 · the result */
.recap-hero {
  display: grid; grid-template-columns: auto minmax(0, 1fr); align-items: end; gap: 3px 12px;
  padding-top: 10px; border-top: 1px solid var(--border);
}
.wb-day {
  grid-column: 1 / -1; font-size: 0.68rem; letter-spacing: 0.08em; text-transform: uppercase; font-weight: 600;
  color: var(--text-dim); white-space: nowrap; overflow: hidden; text-overflow: ellipsis;
}
.wb-score {
  display: flex; align-items: baseline; gap: 5px; font-family: var(--font-title); font-weight: 700; font-size: 2.4rem;
  line-height: 0.92; color: var(--gold); font-variant-numeric: tabular-nums;
}
.wb-score small {
  font-family: var(--font-mono); font-size: 0.68rem; font-weight: 600; letter-spacing: 0.08em;
  text-transform: uppercase; color: var(--text-dim);
}
.wb-stand { display: flex; flex-direction: column; align-items: flex-end; gap: 1px; min-width: 0; padding-bottom: 1px; }
.wb-sr { display: flex; align-items: center; gap: 8px; }
.wb-medal { display: flex; gap: 2px; color: var(--medal-gold); }
.wb-medal svg { width: 15px; height: 15px; }
.wb-place {
  display: flex; align-items: center; gap: 5px; font-family: var(--font-title); font-weight: 700;
  font-size: 1.05rem; color: var(--text); line-height: 1.1;
}
.wb-place svg { width: 16px; height: 16px; color: var(--gold); }
.wb-pct { font-size: 0.68rem; color: var(--text-dim); letter-spacing: 0.02em; white-space: nowrap; }

/* 3 · the finds: a small ledger, one line each */
.recap-finds { display: flex; flex-direction: column; border: 1px solid var(--border); border-radius: 10px; overflow: hidden; }
.wb-find { display: grid; grid-template-columns: 92px minmax(0, 1fr) auto; align-items: center; gap: 8px; min-height: 30px; padding: 0 10px; }
.wb-find + .wb-find { border-top: 1px solid color-mix(in srgb, var(--border) 60%, transparent); }
.wb-fl { font-size: 0.68rem; letter-spacing: 0.04em; text-transform: uppercase; color: var(--text-dim); white-space: nowrap; }
.wb-fw {
  font-family: var(--font-title); font-weight: 700; font-size: 0.98rem; letter-spacing: 0.05em; color: var(--text);
  overflow: hidden; text-overflow: ellipsis; white-space: nowrap;
}
.wb-fd { display: flex; align-items: center; gap: 4px; font-size: 0.68rem; color: var(--text-dim); white-space: nowrap; }
.wb-fd svg { width: 14px; height: 14px; }
/* A list the player TOPPED: gold tint, gold detail, the trophy — the same
   gold the leaderboard's own row wears (D-200). */
.wb-find.is-top { background: var(--tint-bg); }
.wb-find.is-top .wb-fl, .wb-find.is-top .wb-fd { color: var(--tint-ink); font-weight: 700; }

/* 4 · the key and the streak: one band, two halves */
.recap-keys { display: grid; grid-template-columns: minmax(0, 1fr) minmax(0, 1.15fr); border: 1px solid var(--border); border-radius: 10px; }
.recap-keys.no-key { grid-template-columns: 1fr; }
.recap-keys.no-key .wb-sc { border-left: none; }
.wb-kc, .wb-sc { display: flex; align-items: center; gap: 9px; padding: 8px 10px; min-width: 0; }
.wb-sc { flex-direction: column; align-items: flex-start; justify-content: center; gap: 6px; border-left: 1px solid var(--border); }
/* The key dims when it leaves for the pill (D-149's wait, then the flight). */
.wb-kc .key { transition: opacity 0.3s; }
.wb-kc.is-sent .key { opacity: 0.3; }
.wb-kt, .wb-st { display: flex; flex-direction: column; min-width: 0; line-height: 1.2; }
.wb-kt b { font-family: var(--font-title); font-weight: 700; font-size: 1.02rem; color: var(--text); }
.wb-kt span, .wb-st { font-size: 0.68rem; color: var(--text-dim); white-space: nowrap; }
.wb-st { display: block; }
.wb-st b { font-weight: 700; color: var(--text); }
/* The strip here is bare (no label, no box of its own) and square. */
.recap-keys .prof-streak { --sd: 16px; margin: 0; padding: 0; border: none; background: none; gap: 3px; }
.recap-keys .prof-streak-day { flex: 0 0 auto; width: var(--sd); height: var(--sd); }
/* No streak alive: today's ring alone and one inviting line, never a row of
   empty days (D-125's M4). */
.wb-sc.is-fresh { flex-direction: row; align-items: center; gap: 10px; }
.wb-sc.is-fresh .prof-streak { --sd: 18px; }
.wb-sc.is-fresh .wb-st { display: flex; flex-direction: column; white-space: normal; }
.wb-sc.is-fresh .wb-st b { font-family: var(--font-title); font-size: 1.02rem; }

/* 5 · the brag: the one primary, when there is one; Copy beside it. The line
   under the label says where the comment goes, before the tap (G7). */
.wb-brag { display: grid; grid-template-columns: minmax(0, 1fr) 54px; gap: 8px; }
.wb-brag .btn-primary {
  display: flex; flex-direction: column; align-items: center; justify-content: center;
  gap: 2px; min-height: 54px; padding: 6px 12px; min-width: 0;
}
.wb-brag .btn-icon { display: flex; align-items: center; justify-content: center; width: 54px; min-height: 54px; }
.wb-brag .btn-icon svg { width: 18px; height: 18px; }
.wb-say {
  font-size: 0.68rem; font-weight: 500; letter-spacing: 0; text-transform: none; line-height: 1.3;
  white-space: nowrap; overflow: hidden; text-overflow: ellipsis; max-width: 100%;
}
.recap-card .end-consent { margin: 0; }

/* 6 · the ways out. Continue leads unless there is a brag, then it steps down
   to secondary — one primary a view. Full results is always the quiet one. */
.wb-acts { display: grid; grid-template-columns: 1fr 1.25fr; gap: 8px; }
.wb-acts .btn { min-height: 44px; padding: 0 10px; }

.wel-h { font-family: var(--font-title); font-size: 1.28rem; line-height: 1.15; color: var(--text); text-align: center; margin-top: 2px; }
.wel-keys { display: flex; align-items: flex-end; justify-content: center; gap: 6px; padding: 4px 0 2px; }
.wel-keys .key { transform: rotate(-11deg); }
.wel-keys .key + .key { transform: rotate(11deg); }
.wel-say { font-size: 0.72rem; line-height: 1.5; color: var(--text); text-align: center; text-wrap: pretty; margin: 0 auto; max-width: 31ch; }

/* The flight (D-127). A fixed-position clone on band 31500: ABOVE the header
   pill it lands in and therefore above the nav (30500) it would otherwise
   pass behind. One shot, and under prefers-reduced-motion it does not run at
   all — every count is painted from the server's number before any key moves,
   so the flight is decoration by construction. */
.key-fly { position: fixed; z-index: 31500; pointer-events: none; will-change: transform, opacity; }
.key-land { animation: key-land 0.16s ease-out; }
@keyframes key-land {
  0% { transform: scale(1); }
  42% { transform: scale(1.22); }
  100% { transform: scale(1); }
}
@media (prefers-reduced-motion: reduce) {
  .key-fly { display: none; }
  .key-land { animation: none; }
}

/* The notice host (D-126). One visible at a time, never while digging.
   Utility band beside #toast — the same position and chrome, a different job:
   #toast is a flash, this is a queued notice that waits its turn. */
#notice {
  position: fixed;
  left: 50%;
  transform: translateX(-50%);
  bottom: calc(64px + env(safe-area-inset-bottom, 0px));
  z-index: 32000;
  max-width: min(92vw, var(--measure));
  padding: 0 14px;
  font-size: 0.78rem;
  line-height: 1.5;
  text-align: center;
  pointer-events: none;
  text-wrap: pretty;
}
#notice:empty { display: none; }
#notice .wq-notice {
  display: block;
  padding: 8px 14px;
  border: 1px solid var(--border);
  border-radius: var(--radius);
  background: var(--panel);
  box-shadow: 0 4px 16px rgba(0, 0, 0, 0.4);
}
#notice .wq-notice b { color: var(--text); }

/* The AWARD form (D-136): the same host, a card instead of a strip, because a
   present is not news. It WAITS to be dismissed rather than expiring in five
   seconds, and it is anchored to NOTHING — it shows over the calendar, over
   the Words page and over a three-week-old post's day sheet alike, so no
   screen and no entry point has to declare a host for it. That portability is
   the decision: the ladder on the live sheet face is exactly such a host, and
   it is why the key looked missing everywhere else. */
#notice.is-award { pointer-events: auto; }
#notice .wq-award {
  position: relative;
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  gap: 9px;
  padding: 12px 32px 12px 14px;
  border: 1px solid var(--gold);
  border-radius: var(--radius);
  background: var(--panel);
  box-shadow: 0 6px 22px rgba(0, 0, 0, 0.45);
  text-align: left;
  cursor: pointer;
}
#notice .wq-aw-kk { display: inline-flex; gap: 3px; flex-shrink: 0; }
#notice .wq-aw-tx { display: flex; flex-direction: column; min-width: 0; }
#notice .wq-aw-tx b { color: var(--text); }
#notice .wq-aw-why { font-size: 0.7rem; color: var(--text-dim); }
/* The ladder's own next-line comes in from the same generator as the day
   sheet's card and keeps its own rule; the full-width basis is what makes the
   prize and the promise read as two lines rather than one long sentence. */
#notice .tdy-next { flex: 1 0 100%; margin: 0; }
/* The mark is the affordance; the CARD is the tap target (G6). */
#notice .wq-award-x {
  position: absolute;
  top: 7px;
  right: 9px;
  font-size: 0.72rem;
  line-height: 1;
  color: var(--text-dim);
}

/* The coach mark (D-126). One line, anchored to the thing it is about,
   dismissed by any tap. It is a TIP, not a badge: it fires on the first
   occurrence of its situation, once per player, and the server remembers that
   it was dismissed — so it cannot become the nag D-119 forbids. */
#coach {
  position: fixed;
  z-index: 32000;
  max-width: min(78vw, 260px);
  padding: 9px 12px;
  border: 1px solid var(--key-metal);
  border-radius: 10px;
  background: var(--coach-bg);
  color: var(--text);
  font-size: 0.72rem;
  line-height: 1.4;
  text-align: left;
  text-wrap: pretty;
  box-shadow: 0 6px 20px rgba(0, 0, 0, 0.45);
  cursor: pointer;
}
#coach::after {
  content: "";
  position: absolute;
  left: var(--coach-tail, 50%);
  bottom: -6px;
  width: 10px;
  height: 10px;
  margin-left: -5px;
  transform: rotate(45deg);
  border-right: 1px solid var(--key-metal);
  border-bottom: 1px solid var(--key-metal);
  background: var(--coach-bg);
}
#coach.below::after { bottom: auto; top: -6px; transform: rotate(225deg); }
#coach .coach-x {
  display: block;
  margin-top: 4px;
  font-family: var(--font-mono);
  font-size: 0.68rem;
  letter-spacing: 0.06em;
  text-transform: uppercase;
  /* Not --text-dim: that ink was measured against the panel, and on the
     tinted paper it drops under 3:1 in the dark palettes. Derived from the
     body ink and the paper instead, so it tracks both. */
  color: color-mix(in srgb, var(--text) 72%, var(--coach-bg));
}
/* The intro variant (D-169): a page's one-sentence introduction, and the
   guided tour's steps. Same element, same band, same tail — one bubble at a
   time is the rule wanted — with a title, and buttons when the bubble leads
   somewhere. */
#coach.is-intro { max-width: min(84vw, 300px); padding: 10px 12px; }
#coach .coach-t {
  display: block;
  margin-bottom: 2px;
  font-family: var(--font-title);
  font-size: 0.8rem;
  letter-spacing: 0.04em;
  color: var(--gold);
}
#coach .coach-b { display: block; }
#coach .coach-acts { display: flex; flex-wrap: wrap; gap: 6px; margin-top: 8px; }
/* A real tap target (G6), not the 36px .btn floor. */
#coach .coach-acts .btn { min-height: 44px; }
