/* Vault — a daily code to crack. Carries its own copy of the palette and chrome,
   like the other games, so moving between them feels like one place. */

:root {
  --bg: #f6f7f9;
  --surface: #ffffff;
  --tile: #ffffff;
  --tile-border: #d9dee5;
  --ink: #131720;
  --ink-soft: #5c6673;
  --ink-faint: #66717d;
  --accent: #2864ea;
  --correct: #2f9e5f;
  --correct-bg: #e6f5ec;
  --accent-bg: #e8effd;
  --wrong: #d2453c;
  --shadow: 0 1px 2px rgba(16, 22, 34, .08), 0 4px 14px rgba(16, 22, 34, .06);
  --radius: 12px;
}

@media (prefers-color-scheme: dark) {
  :root {
    --bg: #0e1116;
    --surface: #171c23;
    --tile: #1c222b;
    --tile-border: #2c3540;
    --ink: #eef2f7;
    --ink-soft: #9aa5b3;
    --ink-faint: #858f9c;
    --accent: #5c8dff;
    --correct: #43c17f;
    --correct-bg: #16342a;
    --accent-bg: #16233d;
    --wrong: #ef6a61;
    --shadow: 0 1px 2px rgba(0, 0, 0, .4);
  }
}

* { box-sizing: border-box; }

/* [hidden] must win, always.
   Any author `display` rule outranks the UA stylesheet's `[hidden] { display: none }`,
   so an element flagged hidden in js stays on screen and stays clickable. That has now
   caught the hub's feedback form and "Play another" in ALL THREE games, where
   `.primary, .secondary { display: block }` outranked it. That one stayed invisible for
   as long as PRACTICE was hardcoded true; making practice a server decision production
   answers no to is what finally showed it. One rule closes the whole class. */
[hidden] { display: none !important; }


html, body {
  margin: 0;
  background: var(--bg);
  color: var(--ink);
  font-family: ui-rounded, "SF Pro Rounded", system-ui, -apple-system, "Segoe UI",
               Roboto, "Helvetica Neue", sans-serif;
  -webkit-text-size-adjust: 100%;
  -webkit-tap-highlight-color: transparent;
}

body {
  height: 100dvh;
  /* pwa.js publishes --app-height only when Android has lied about the window size —
     the stale-window guard there says when and why. Anywhere else this is 100dvh. */
  height: var(--app-height, 100dvh);
  display: flex;
  flex-direction: column;
  overflow: hidden;
  padding: env(safe-area-inset-top) env(safe-area-inset-right)
           0 env(safe-area-inset-left);
}

button { touch-action: manipulation; -webkit-user-select: none; user-select: none; }
button:focus { outline: none; }
button:focus-visible { outline: 2px solid var(--ink); outline-offset: 3px; }

/* ---------- top bar ---------- */
.topbar {
  display: grid;
  grid-template-columns: 1fr auto 1fr;
  align-items: center;
  width: 100%;
  max-width: 520px;
  margin: 0 auto;
  padding: 7px 14px;
  border-bottom: 1px solid var(--tile-border);
  flex: none;
}
.topbar-side { display: flex; align-items: center; gap: 2px; }
.topbar-side.end { justify-self: end; }

.wordmark {
  margin: 0;
  text-align: center;
  font-size: 19px;
  font-weight: 800;
  letter-spacing: .24em;
  text-indent: .24em;
}
.mark { font-size: 17px; letter-spacing: 0; margin-right: 9px; vertical-align: -1px; }
/* The drawn glyph keeps the emoji's one job here: the only color in the header.
   Same hue as this game's tile on the hub shelf. */
.mark svg { width: 17px; height: 17px; display: inline-block; vertical-align: -2.5px; }
.mark { color: #9268d8; }
@media (prefers-color-scheme: dark) { .mark { color: #a984ea; } }

.icon-btn {
  width: 32px; height: 32px;
  border: 0; border-radius: 50%;
  background: transparent;
  color: var(--ink-soft);
  font-size: 17px;
  cursor: pointer;
}
.icon-btn:active { background: var(--tile-border); transform: scale(.88); }
a.icon-btn { display: grid; place-items: center; text-decoration: none; }

/* ---------- game ---------- */
#game {
  flex: 1 1 auto;
  min-height: 0;
  width: 100%;
  max-width: 520px;
  margin: 0 auto;
  padding: 10px 14px 0;
  display: flex;
  flex-direction: column;
  overflow-y: auto;
  overscroll-behavior: contain;
}

.head { flex: none; text-align: center; }

/* "Puzzle #34", top-left of the play area — the owner's label and place for every game
   (17 September 2026); it used to read "No. 34", centered. */
.puzzle-no {
  margin: 0;
  text-align: left;
  font-size: 12px;
  font-weight: 600;
  letter-spacing: .02em;
  color: var(--ink-faint);
}
.theme {
  margin: 3px 0 10px;
  font-size: 19px;
  font-weight: 800;
}

/* ---------- the board ---------- */
/* One row per guess: its number, the symbols played, and what the answer was. The rows
   scroll and the palette below them does not — the working row is always the last one, so
   the thing you are about to act on stays next to the thing you act with. */
.rows {
  flex: 1;
  min-height: 0;
  overflow-y: auto;
  display: flex;
  flex-direction: column;
  gap: 7px;
  padding: 4px 0 2px;
}

.row {
  display: flex;
  align-items: center;
  gap: 8px;
  padding: 6px 8px;
  border: 1.5px solid transparent;
  border-radius: 12px;
}
/* The working row is the only one you can touch, and it says so rather than relying on the
   player noticing that it is the empty one at the bottom. */
.row.working {
  border-color: var(--tile-border);
  background: var(--surface);
}

.row-no {
  flex: none;
  width: 14px;
  text-align: right;
  font-size: 11px;
  font-weight: 700;
  color: var(--ink-faint);
  font-variant-numeric: tabular-nums;
}

.slots { display: flex; gap: 6px; flex: 1; min-width: 0; }

.slot {
  flex: 1;
  max-width: 46px;
  aspect-ratio: 1;
  display: grid;
  place-items: center;
  padding: 4px;
  border: 1.5px solid var(--tile-border);
  border-radius: 10px;
  background: var(--tile);
  cursor: pointer;
}
.slot:disabled { cursor: default; }
.slot.empty { border-style: dashed; background: transparent; }
/* The cursor: the slot the next palette tap fills, selectable by tapping (Rung's
   cursor precedent, owner's ask 25 August 2026). The accent recolors the empty slot's
   own dashed border and nothing more — a first draft added an inset underline bar and
   the owner's phone read it as a pressed/inset state rather than a highlight. */
.row.working .slot.sel { border-color: var(--accent); }
.row.working .slot:not(.empty):active { transform: scale(.9); }

/* Shape carries the identity and the hue only reinforces it. Never the other way round:
   a palette told apart by colour alone is unplayable for a colour-blind player, and this
   game is entirely about telling the palette apart. */
.sym {
  width: 100%;
  height: 100%;
  display: block;
  fill: hsl(var(--hue) 70% 45%);
}
@media (prefers-color-scheme: dark) {
  .sym { fill: hsl(var(--hue) 65% 62%); }
}
.sym.big { width: 34px; height: 34px; }

.result {
  flex: none;
  display: flex;
  flex-direction: column;
  align-items: flex-end;
  gap: 1px;
  min-width: 62px;
}
.tally {
  font-size: 11px;
  line-height: 1.25;
  color: var(--ink-faint);
  font-variant-numeric: tabular-nums;
}
.tally b { font-size: 13px; font-weight: 800; }
.tally.exact b { color: var(--accent); }
.tally.near b { color: var(--ink-soft); }
.tally.far b { color: var(--ink-faint); }
.tally.waiting { color: var(--ink-faint); font-weight: 600; }

/* ---------- palette ---------- */
.palette {
  display: flex;
  justify-content: center;
  gap: 6px;
  flex-wrap: wrap;
  /* Tightened 26 August 2026 (owner's ask): the dock reads like Rung's keyboard area
     now — input close under the dock's top border, straight onto the divider below. */
  padding: 4px 0 0;
}
/* The answer to a tap on an empty slot (app.js's nudgePalette): one pulse to say "down
   here". On the container, not the buttons, so the row moves as one gesture. */
.palette.nudge { animation: pal-nudge .45s cubic-bezier(.2, .8, .3, 1); }
@keyframes pal-nudge { 35% { transform: scale(1.07); } }
@media (prefers-reduced-motion: reduce) {
  .palette.nudge { animation: none; }
}
.pal {
  width: 44px;
  height: 44px;
  padding: 6px;
  border: 1.5px solid var(--tile-border);
  border-radius: 12px;
  background: var(--tile);
  cursor: pointer;
}
.pal:active { transform: scale(.9); }
.pal:disabled { opacity: .35; cursor: default; transform: none; }

/* The answer, shown on a win and on a loss alike. */
.code-reveal {
  display: flex;
  justify-content: center;
  gap: 8px;
  margin: 4px 0 14px;
}

/* ---------- dock ---------- */
.dock {
  flex: none;
  width: 100%;
  max-width: 520px;
  margin: 0 auto;
  padding: 6px 14px calc(10px + max(env(safe-area-inset-bottom), var(--nav-pad, 0px)));
  border-top: 1px solid var(--tile-border);
  background: var(--bg);
}
/* Two rows rather than one: Undo, Reset and Check alongside the status text
   squeezed it to nothing on a narrow phone. */
/* 8px above the divider, the same breath Rung's keyboard keeps — flush was tried and
   the line read as touching the palette. */
.statusbar { display: flex; flex-direction: column; margin-top: 8px; }
/* Inside the actions row since 26 August 2026 (owner's pass) — quiet at idle, so a
   line of its own read as dead space; it fills the room between the minors and Check
   and gives way with an ellipsis rather than wrapping the dock taller. */
.actions .feedback {
  flex: 1 1 auto;
  min-width: 0;
  text-align: left;
  white-space: nowrap;
  overflow: hidden;
  text-overflow: ellipsis;
}

.actions {
  display: flex;
  align-items: center;
  gap: 8px;
  /* The divider above the controls (owner's ask, 26 August 2026) — the same fence
     Rung's dock draws above its Undo row, run full width through the dock's 14px side
     padding by the negative margins (inset ends read as a mistake on his screenshot).
     The numbers mirror .dock's padding; change one with the other. */
  border-top: 1px solid var(--tile-border);
  margin: 0 -14px;
  padding: 8px 14px 0;
}
/* A finished board hides the palette, and the fence above the controls goes with it —
   otherwise it sits right under the dock's own border as a stray double line (owner's
   report, 26 August 2026). */
.palette[hidden] + .statusbar .actions { border-top: 0; padding-top: 0; }
.actions .spacer { flex: 1; }

.minor {
  padding: 6px 13px;
  border: 1.5px solid var(--tile-border);
  border-radius: 10px;
  background: transparent;
  color: var(--ink-soft);
  font-family: inherit;
  font-size: 13px;
  font-weight: 700;
  cursor: pointer;
  white-space: nowrap;
}
.minor:active { transform: scale(.92); }
.minor:disabled { color: var(--ink-faint); opacity: .5; cursor: default; transform: none; }
/* Reset asks before it wipes the grid — one mistap should not cost the puzzle. */
.minor.armed {
  border-color: var(--wrong);
  color: var(--wrong);
  box-shadow: 0 0 0 3px color-mix(in srgb, var(--wrong) 18%, transparent);
}

.feedback {
  margin: 0;
  min-height: 17px;
  font-size: 13px;
  font-weight: 600;
  color: var(--ink-soft);
  font-variant-numeric: tabular-nums;
}
.feedback b { color: var(--ink); }
.feedback.bad b { color: var(--wrong); }

/* #demo-check rides every rule so the demo's pill can never drift from the real one. */
#btn-check,
#demo-check {
  flex: none;
  padding: 7px 16px;
  border: 0;
  border-radius: 10px;
  background: var(--accent);
  color: #fff;
  font-family: inherit;
  font-size: 13px;
  font-weight: 700;
  cursor: pointer;
  white-space: nowrap;
}
#btn-check:active { transform: scale(.92); }
#btn-check:disabled,
#demo-check:disabled {
  /* The outline is an inset shadow, not a border: this button has none while it is live,
     and swapping in 1.5px of border grew it 3px the moment the checks ran out, shifting
     the whole row (owner's report, 29 August 2026). A shadow paints inside the same box. */
  background: transparent;
  box-shadow: inset 0 0 0 1.5px var(--tile-border);
  color: var(--ink-faint);
  cursor: default;
  transform: none;
}

/* ---------- sheets ---------- */
.sheet-wrap[hidden] { display: none; }
.sheet-wrap {
  position: fixed;
  inset: 0;
  z-index: 50;
  display: flex;
  align-items: flex-end;
  justify-content: center;
  background: rgba(8, 11, 16, .55);
  animation: fade .18s ease;
}
@keyframes fade { from { opacity: 0; } }

.sheet {
  position: relative;
  width: 100%;
  max-width: 520px;
  max-height: 90dvh;
  overflow-y: auto;
  padding: 26px 20px calc(26px + max(env(safe-area-inset-bottom), var(--nav-pad, 0px)));
  background: var(--surface);
  border-radius: 20px 20px 0 0;
  animation: rise .26s cubic-bezier(.2, .8, .3, 1);
}
@keyframes rise { from { transform: translateY(24px); } }

@media (min-width: 560px) {
  .sheet-wrap { align-items: center; }
  .sheet { border-radius: 20px; }
}

.sheet h2 { margin: 0 0 6px; font-size: 22px; text-align: center; }

.close {
  position: absolute;
  top: 12px; right: 12px;
  width: 34px; height: 34px;
  border: 0; border-radius: 50%;
  background: transparent;
  color: var(--ink-soft);
  font-size: 24px;
  line-height: 1;
  cursor: pointer;
}

.result-sub {
  margin: 0 0 20px;      /* carries the gap the answer table used to leave */
  text-align: center;
  color: var(--ink-soft);
  font-size: 15px;
}

.primary, .secondary {
  display: block;
  width: fit-content;
  max-width: 100%;
  margin-inline: auto;
  border-radius: 10px;
  font-family: inherit;
  font-weight: 700;
  cursor: pointer;
}
.primary {
  padding: 13px 30px;
  border: 0;
  background: var(--accent);
  color: #fff;
  font-size: 16px;
}
.secondary {
  padding: 8px 18px;
  margin-top: 10px;
  border: 1.5px solid var(--tile-border);
  background: transparent;
  color: var(--ink-soft);
  font-size: 13px;
  font-weight: 600;
}
/* "Next: Spectrum" is an <a>, because it navigates. An anchor brings an underline and
   inherits text-align from the sheet, so it needs both reset to sit like a button. */
a.primary, a.secondary { text-decoration: none; text-align: center; }

.primary:active, .secondary:active { transform: scale(.94); }
.primary.press, .secondary.press, #btn-check.press {
  animation: press .26s cubic-bezier(.2, .8, .3, 1);
}
@keyframes press {
  0%   { transform: scale(.94); }
  55%  { transform: scale(1.035); }
  100% { transform: scale(1); }
}

.statrow {
  display: flex;
  justify-content: center;
  align-items: start;
  gap: 18px;
  margin: 20px 0 0;
}
.stat { text-align: center; }
.stat b { display: block; font-size: 26px; font-weight: 800; }
.stat span {
  display: block;
  max-width: 74px;
  margin-inline: auto;
  font-size: 11px; color: var(--ink-soft);
  text-transform: uppercase; letter-spacing: .06em;
  line-height: 1.25;
}

.countdown {
  margin: 18px 0 0;
  text-align: center;
  font-size: 13px;
  color: var(--ink-faint);
  font-variant-numeric: tabular-nums;
}

.dist-title {
  margin: 22px 0 10px;
  font-size: 13px;
  text-align: center;
  color: var(--ink-soft);
  text-transform: uppercase;
  letter-spacing: .09em;
}
.dist { display: flex; flex-direction: column; gap: 6px; }
.dist-row { display: flex; align-items: center; gap: 8px; font-size: 13px; }
.dist-row .track { flex: 1; min-width: 0; display: flex; }
.dist-row .lab {
  width: 76px;
  flex: none;
  font-weight: 700;
  font-size: 12px;
  color: var(--ink-soft);
  font-variant-numeric: tabular-nums;
}
.dist-row .bar {
  background: var(--accent);
  color: #fff;
  padding: 3px 8px;
  border-radius: 5px;
  font-weight: 700;
  font-size: 12px;
  min-width: 26px;
  text-align: right;
}

.help-list { margin: 16px 0; padding-left: 20px; line-height: 1.65; font-size: 15px; }
.help-list li { margin-bottom: 8px; }
.help-link { margin: 0 0 12px; font-size: 14px; }
.help-link a { color: var(--accent); }
/* short screens */
@media (max-height: 740px) {
  .topbar { padding: 5px 12px; }
  /* The demo's copy of the header follows it; the id outranks the base rule further down. */
  #demo-wrap .demo-head { padding: calc(5px + env(safe-area-inset-top)) 12px 5px; }
  #game { padding: 8px 12px 0; }
  .theme { font-size: 17px; margin-bottom: 8px; }
  .slot { max-width: 40px; }
  .pal { width: 40px; height: 40px; }
  .rows { gap: 5px; }
  #btn-check { padding: 6px 14px; font-size: 12px; }
  .minor { padding: 5px 11px; font-size: 12px; }
}

@media (prefers-reduced-motion: reduce) {
  * { animation-duration: .01ms !important; transition-duration: .01ms !important; }
}

/* ---------- menu + feedback ----------
   The header used to carry three buttons and a fourth that only appeared in practice.
   One menu button opens this instead, so the header stops competing with the board. */
.menu-list { display: flex; flex-direction: column; gap: 8px; margin-top: 4px; }

.menu-item {
  display: flex;
  align-items: center;
  gap: 12px;
  font: inherit;
  font-size: 15px;
  font-weight: 600;
  text-align: left;
  text-decoration: none;
  color: var(--ink);
  background: var(--bg);
  border: 1.5px solid var(--tile-border);
  border-radius: 12px;
  padding: 13px 14px;
  cursor: pointer;
}
.menu-item[hidden] { display: none; }
/* The menu in groups (owner's ask, 22 September 2026): a small heading over each, so a
   player scans four words instead of a column of eight rows. `.menu-list` is a flex
   column of groups now, and each group is a flex column of its items. */
.menu-group { display: flex; flex-direction: column; gap: 6px; }
.menu-group + .menu-group { margin-top: 6px; }
.menu-head {
  margin: 0 0 1px 4px;
  font-size: 11px;
  font-weight: 800;
  letter-spacing: .08em;
  text-transform: uppercase;
  color: var(--ink-faint);
}
/* Sized in its own box so the labels line up whatever the glyph's width — emoji metrics
   vary enough between platforms to stagger the text otherwise. */
.menu-icon {
  flex: none;
  width: 22px;
  text-align: center;
  font-size: 17px;
  line-height: 1;
}
/* Drawn glyphs, not emoji -- one geometry on every platform. Stroke attributes ride on
   the svg element so the shape survives the stylesheet failing; this sizes it, and
   currentColor keeps it in the ink beside it. Same rule as the hub's. */
.menu-icon svg { width: 18px; height: 18px; display: block; margin: 0 auto; }

.menu-item:active { transform: scale(.99); }
.menu-item:focus-visible { outline: 2px solid var(--accent); outline-offset: 2px; }
@media (hover: hover) {
  .menu-item:hover { border-color: var(--accent); }
}

/* Same trap as the hub: an author `display` rule beats the UA stylesheet's [hidden], so
   hiding the form after a send would leave it on screen and re-submittable. */
#fb-form[hidden] { display: none; }
#fb-form { display: flex; flex-direction: column; gap: 6px; }

.fb-label {
  font-size: 12px;
  font-weight: 700;
  color: var(--ink-soft);
  margin-top: 8px;
}
.fb-label:first-child { margin-top: 0; }
.fb-label span { font-weight: 400; color: var(--ink-faint); }

.fb-input {
  font: inherit;
  font-size: 14px;
  color: var(--ink);
  background: var(--bg);
  border: 1.5px solid var(--tile-border);
  border-radius: 10px;
  padding: 9px 10px;
  width: 100%;
}
.fb-input:focus-visible { outline: none; border-color: var(--accent); }
/* Vertical only — a textarea dragged wider than its sheet is the one way this layout can
   be made to overflow sideways on a phone. */
textarea.fb-input { resize: vertical; min-height: 84px; line-height: 1.45; }

.fb-send {
  font: inherit;
  font-size: 15px;
  font-weight: 700;
  margin-top: 12px;
  padding: 11px 14px;
  border: none;
  border-radius: 12px;
  background: var(--accent);
  color: #fff;
  cursor: pointer;
}
.fb-send:disabled { opacity: .55; cursor: default; }
.fb-send:focus-visible { outline: 2px solid var(--accent); outline-offset: 2px; }

.fb-note { margin: 10px 0 0; font-size: 11px; line-height: 1.45; color: var(--ink-faint); }

.fb-status {
  margin: 10px 0 0;
  font-size: 13px;
  color: var(--ink-soft);
  text-align: center;
  overflow-wrap: break-word;
}

/* ---------- the scoring sheet ----------
   Reached from the menu, so nobody has to leave a puzzle to find out what it is worth.
   Its numbers are a copy of scoring.js's curves — see the note in index.html. */
.score-lede { margin: 0 0 12px; font-size: 13px; color: var(--ink-soft); line-height: 1.5; }
.score-table {
  width: 100%;
  border-collapse: collapse;
  font-size: 13px;
  margin-bottom: 12px;
}
.score-table th, .score-table td {
  text-align: left;
  padding: 6px 8px;
  border-bottom: 1px solid var(--card-border, var(--tile-border));
}
.score-table th { font-size: 11px; text-transform: uppercase; letter-spacing: .06em; color: var(--ink-faint); }
.score-table td:last-child { text-align: right; font-weight: 700; font-variant-numeric: tabular-nums; white-space: nowrap; }
.score-table tr:last-child td { border-bottom: 0; }
.score-note { margin: 0 0 10px; font-size: 12px; line-height: 1.5; color: var(--ink-soft); }
.score-note:last-child { margin-bottom: 0; }
.score-note a { color: var(--accent); }

/* ---------- the demo ----------
   A whole crack played for you on a fixed code: the board reuses .row, .slot, .sym, .pal
   and the real dock classes so it looks like play, the sheet goes full-screen so it IS a
   screen, and the finger and callout are the demo's own. Watch-only: the board is inert
   and the live controls are back-5s, pause, forward-5s and close. Same kit as Spectrum's
   and Rung's, this game's own copy — games share a look, not code. */

#demo-wrap .sheet.demo-stage {
  position: relative;             /* the finger positions against this box */
  height: 100%;
  max-height: none;
  border-radius: 0;
  overflow: hidden;               /* nothing scrolls; the finger's travel must not either */
  display: flex;
  flex-direction: column;
  background: var(--bg);          /* the game page's ground, not a sheet's surface */
  padding: 8px 16px calc(10px + max(env(safe-area-inset-bottom), var(--nav-pad, 0px)));
}

/* The demo's head is the game's own header (owner's ask, 22 September 2026): the mark
   and the name on the left where the back arrow would be, the demo's controls where ?
   and the menu sit, the same divider under both — so leaving the demo is one header
   turning into the other, which the close animation in the script draws. Negative
   margins take it to the sheet's edges, where the real topbar's border runs; the top
   padding adds the safe area the body gives the real one, so the two names share a line. */
.demo-head {
  display: flex; align-items: center; gap: 2px; flex: none;
  margin: -8px -16px 0;
  padding: calc(7px + env(safe-area-inset-top)) 14px 7px;
  border-bottom: 1px solid var(--tile-border);
}
.demo-head .wordmark { flex: 1; margin: 0; font-size: 19px; text-align: left; text-indent: 0; }
.demo-head .icon-btn svg { width: 18px; height: 18px; display: block; margin: auto; }
/* The × is a text glyph beside 18px drawings; at .icon-btn's size it reads a size down
   from them, so it gets its own. */
.demo-head [data-close] { font-size: 24px; }
/* The opening callout points at these; the blink is the pointing. */
.demo-head .icon-btn.blink { animation: ctlBlink .55s ease-in-out 2; }
@keyframes ctlBlink { 50% { background: var(--accent); color: #fff; } }

/* The callout: what the player is thinking. An overlay hovering over the board, never in
   the flow, and only BETWEEN actions — say() hides it before the finger moves. Anchored
   under where the answered rows accumulate; on short screens it overlaps the dock, which
   is the right thing to cover. Emphasis is plain bold: accent-coloured text in a card
   reads as a link nobody can click. */
.demo-say {
  position: absolute;
  left: 16px; right: 16px;
  top: 415px;
  /* Over the finger's 12: the finger idles where it last tapped while a callout is up,
     and it was covering the words (owner's report, 26 August 2026). */
  z-index: 13;
  margin: 0;
  padding: 12px 14px;
  font-size: 13.5px;
  line-height: 1.5;
  color: var(--ink);
  background: var(--surface);
  border: 1.5px solid var(--tile-border);
  border-left: 4px solid var(--accent);
  border-radius: 10px;
  box-shadow: var(--shadow-lift);
}
.demo-say.pop { animation: sayPop .3s ease; }
@keyframes sayPop { from { opacity: 0; transform: translateY(6px); } }
/* Word by word (owner's ask, 22 September 2026): each word fades up on its own delay,
   which the script sets to pace the read; paused with the demo. */
.demo-say .w, .coach .w { display: inline-block; clip-path: inset(0 100% 0 0); opacity: 0; animation: wordIn .3s linear forwards; }
@keyframes wordIn { from { clip-path: inset(0 100% 0 0); opacity: 0; } 80% { opacity: 1; } to { clip-path: inset(0 0 0 0); opacity: 1; } }   /* written in left to right, like typing but a word at a time; the script sets each word's duration to its slot so the line extends continuously (owner, 22 September 2026 — a rise read as a pop, a fade was not it either) */
.demo-stage.paused .demo-say .w { animation-play-state: paused; }

/* The coach after the first-visit demo (owner's ask, 22 September 2026): a transparent
   sheet — so the game holds its intro and clock and takes no taps until it goes — with
   the card centered in it, in the callout's dress; its words are the callout's (`.w`
   above). Under the demo's 50 while the two overlap. */
.coach-wrap { background: transparent; animation: none; align-items: center; justify-content: center; z-index: 45; }
.coach {
  width: calc(100% - 32px);
  max-width: 488px;
  margin: 0;
  /* Positioned and lifted: Deduce frosts the page below the header with a positioned
     ::before on every sheet, which would otherwise paint over the card and blur its
     border away (owner's report, 22 September 2026); the same rule everywhere for one look. */
  position: relative;
  z-index: 1;
  padding: 12px 14px;
  font-size: 13.5px;
  line-height: 1.5;
  color: var(--ink);
  background: var(--surface);
  border: 1.5px solid var(--tile-border);
  border-left: 4px solid var(--accent);
  border-radius: 10px;
  box-shadow: var(--shadow-lift, 0 10px 28px rgba(0, 0, 0, .18));
}
.coach[hidden] { display: none; }
.coach.pop { animation: coachPop .3s ease; }
@keyframes coachPop { from { opacity: 0; transform: translateY(6px); } }
/* The ? blinks the way the demo's controls do, when the coach points at it. */
.topbar .icon-btn.blink { animation: ctlBlink .55s ease-in-out 2; }

/* The puzzle sits where the real one does: top-aligned, the pad standing in for what the
   real page has above the theme line — measured against the live page side by side. */
.demo-puzzle {
  flex: 1;
  min-height: 0;
  display: flex;
  flex-direction: column;
  justify-content: flex-start;
  padding-top: 33px;
  pointer-events: none;           /* watch-only */
}
/* The real page centres the title through its .head wrapper, which the demo lacks. */
#demo-theme { text-align: center; }
/* The real .rows flexes and scrolls inside #game; here the whole crack fits, and the
   stage must never scroll under the finger. */
#demo-rows { flex: none; overflow: visible; }
#demo-rows .slot { cursor: default; }

/* The demo's dock, mirroring the real one: rule across the foot, palette, actions row. */
.demo-dock {
  flex: none;
  margin-top: 8px;
  padding-top: 8px;
  border-top: 1px solid var(--tile-border);
}
#demo-palette, .demo-dock .actions { pointer-events: none; }
.demo-dock .actions { margin-top: 8px; }
/* Same rule as the real dock: the finished board hides the palette, and the fence above
   the controls must not linger as a stray line. */
#demo-palette[hidden] + .actions { border-top: 0; padding-top: 0; margin-top: 0; }

.demo-endrow { display: flex; align-items: center; gap: 10px; width: 100%; margin-top: 8px; }
#demo-again {
  padding: 8px 16px;
  font: inherit;
  font-size: 13px;
  font-weight: 700;
  border: 1.5px solid var(--tile-border);
  border-radius: 10px;
  background: transparent;
  color: var(--ink-soft);
  cursor: pointer;
}
#demo-again:active { transform: scale(.92); }
#demo-next {
  margin-left: auto;
  padding: 8px 18px;
  font: inherit;
  font-size: 14px;
  font-weight: 800;
  border: 0;
  border-radius: 10px;
  background: var(--accent);
  color: #fff;
  cursor: pointer;
}
#demo-next:active { transform: scale(.94); }

/* The ghost finger. The outer box carries the travel (JS sets its translate); the inner
   circle carries the press, so the two transforms never fight. The ring is the tap. */
.demo-finger {
  position: absolute;
  left: 0; top: 0;
  width: 38px; height: 38px;
  margin: -19px 0 0 -19px;        /* translate() addresses the centre */
  pointer-events: none;
  z-index: 12;
}
.demo-finger::after {
  content: "";
  position: absolute; inset: 0;
  border-radius: 50%;
  border: 2px solid var(--accent);
  background: color-mix(in srgb, var(--accent) 22%, transparent);
  box-shadow: 0 2px 10px rgba(0, 0, 0, .25);
  transition: transform .15s ease, background .15s ease;
}
.demo-finger.down::after {
  transform: scale(.8);
  background: color-mix(in srgb, var(--accent) 42%, transparent);
}
.demo-finger::before {
  content: "";
  position: absolute; inset: 0;
  border-radius: 50%;
  border: 2px solid var(--accent);
  opacity: 0;
}
.demo-finger.tap::before { animation: fingerTap .45s ease; }
@keyframes fingerTap {
  from { opacity: .8; transform: scale(.7); }
  to   { opacity: 0;  transform: scale(1.8); }
}

/* The demo button is pinned (matching Spectrum and Rung): the rules scroll inside
   .help-scroll, the way into the demo never leaves the screen. The sheet's own
   overflow-y moves inward for this one sheet, or there would be two scrollbars. The
   rule between them runs full bleed — the docks' divider grammar — via the negative
   margins that walk it back out through the sheet's padding. */
#help-wrap .sheet { display: flex; flex-direction: column; overflow: hidden; }
#help-wrap .help-scroll { overflow-y: auto; min-height: 0; }
#help-wrap .primary { flex: none; }
.help-rule {
  flex: none;
  width: auto;
  border: 0;
  border-top: 1px solid var(--tile-border);
  margin: 16px -20px 0;
}

/* The scroll hint (owner's ask, 26 August 2026): a small down arrow pinned to the
   scroll area's lower-right while there is more rule text below the fold; app.js hides
   it once the reader reaches the bottom. Sticky INSIDE the scroller, so it needs no
   measured offsets, and zero-height, so it costs the content no room. */
.help-more {
  position: sticky;
  bottom: 4px;
  height: 0;
  display: flex;
  justify-content: flex-end;
  pointer-events: none;
}
.help-more svg {
  width: 26px;
  height: 26px;
  padding: 5px;
  margin: -30px 4px 0 0;
  border-radius: 50%;
  background: var(--accent);
  color: #fff;
  box-shadow: 0 2px 8px rgba(0, 0, 0, .3);
}

/* The three offers under a result are one set of choices, so they are one size — asked for
   on 16 August 2026. Only the *scale* is equalised: "Next game" keeps the filled accent, so
   the hierarchy is carried by colour rather than by making one button bigger than the two
   beside it. Scoped to the result sheet, because .primary is also the pause sheet's
   Continue, which is alone on screen and should stay full size. */
#results-wrap .primary {
  padding: 8px 18px;
  font-size: 13px;
  font-weight: 600;
  /* The border is not decoration — it is the last 2px. `.secondary` is outlined and
     `.primary` is not, and with border-box an auto height still adds the border, so
     matching padding alone left the filled button 33px against 35. Transparent, so the
     accent fill paints straight through it and nothing looks different. */
  border: 1.5px solid transparent;
}

/* The demo button, which is the only thing to *do* on the help sheet and therefore the only
   thing styled as its primary. It sits at the foot, so it needs the gap the results sheet's
   siblings give theirs. `.demo-cta` used to live here and is gone with it. */
#help-wrap .primary,
#notice-wrap .primary { margin-top: 14px; }

/* Every secondary on the results sheet wears the accent outline (owner, 22 September
   2026 — Show statistics and Share feedback were the muted pair until then, since
   19 August), so the sheet reads as one filled button over a set of outlined ones. */
#btn-share, #btn-archive, #btn-stats, #btn-feedback { border-color: var(--accent); color: var(--accent); }

/* The day's score on the result sheet. Accent and bold because it is the one number the
   whole shelf is built around; hidden until the server has said what it is. */
.result-score {
  /* Sits under the next-game button now, so the gap moved to the top. */
  margin: 10px 0 0;
  text-align: center;
  font-size: 15px;
  font-weight: 800;
  color: var(--accent);
}

/* The "What changed" sheet, when it carries more than one update: each gets a small
   heading. Filled by ../notices.js; see there for the rules. */
.notice-sub { margin: 14px 0 6px; font-size: 15px; font-weight: 800; }

/* The results sheet's two-column tail (owner's design, 21 August 2026; reordered at
   his ask, 5 September 2026): the day's number over Show statistics and Share feedback
   on the left; Share score, Puzzle archive and the next-game link on the right, row for
   row. A real grid, not two flex columns: the next-game label can run long ("Back to
   today's puzzle", "Still loading… tap to try again") and wrap, and a grid row stretches
   its partner with it, so the rows stay level whatever the label. Dense flow is what
   lets each column's items fill from row 1 — the wrappers are `display: contents` and
   only pin the column. Staging's extra links (Play again, Practice) hang under the left
   column. The next-game link is a button of the secondaries' size, its hierarchy carried
   by the fill rather than by a bigger box (Mini's rule). */
.result-actions {
  display: grid;
  grid-template-columns: 1fr 1fr;
  grid-auto-flow: row dense;
  gap: 8px 10px;
  margin-top: 12px;
}
.result-actions .ra-col { display: contents; }
.result-actions .ra-col:first-child > * { grid-column: 1; }
.result-actions .ra-col:last-child > * { grid-column: 2; }
.result-actions .secondary,
.result-actions .primary {
  margin: 0;
  width: 100%;
  padding: 8px 10px;
  font-size: 13px;
  font-weight: 600;
  letter-spacing: 0;
  border-radius: 10px;
}
/* The same 1.5px the secondaries wear, so an unwrapped row is level; flex centers the
   label when the row is stretched. */
.result-actions .primary {
  border: 1.5px solid var(--accent);
  display: flex;
  align-items: center;
  justify-content: center;
}
.result-actions .result-score {
  margin: 0;
  display: flex;
  align-items: center;
  justify-content: center;
}
/* ---------- a tall desktop window ----------
   The shell is the viewport's height with the dock pinned to its foot, which is right
   on a phone — the controls sit under the thumb whatever the board is doing — and
   wrong in a 1270px browser window, where it parks the controls 900px below a board
   that ends a third of the way down (owner's screenshots, 29 August 2026). On a tall
   window driven by a mouse the shell is content-sized instead and the dock follows the
   board. Gated on `pointer: fine` so no phone or installed app ever sees it: their
   height is the whole problem being fixed elsewhere (--app-height, --nav-pad) and this
   must not touch it. */
@media (pointer: fine) and (min-height: 820px) {
  body { height: auto; min-height: 0; overflow: visible; }
  #game { flex: none; overflow: visible; }
  .rows { flex: none; overflow: visible; }
  /* Breathing room the phone deliberately does without — there the pane is the space
     above the dock; here the pane ends where the content does (owner's ask). */
  .dock { margin-top: 18px; }
}

/* ---------- the cheer ----------
   The cracking row's slots turn like dials, one after the next (--i is set by cheer()
   in app.js), and a burst of confetti in the code's own colors is thrown from the row.
   The burst is a fixed, zero-sized anchor on the body; each slip flies along its own
   --dx/--dy and spins by --rot, then fades. Under the sheets' z-index of 50 on purpose.
   Reduced motion zeroes the durations like every other animation here, and app.js
   removes the anchor afterwards either way. */
.slot.cheer {
  animation: cheerDial .6s cubic-bezier(.2, .8, .3, 1) both;
  animation-delay: calc(var(--i) * 90ms);
}
@keyframes cheerDial {
  50% {
    transform: rotate(180deg) scale(1.12);
    border-color: var(--accent);
    box-shadow: 0 0 0 3px color-mix(in srgb, var(--accent) 22%, transparent);
  }
  100% { transform: rotate(360deg); }
}
.cheer-burst {
  position: fixed;
  z-index: 40;
  width: 0;
  height: 0;
  pointer-events: none;
}
.cheer-burst i {
  position: absolute;
  left: -3px;
  top: -5px;
  width: 6px;
  height: 10px;
  border-radius: 2px;
  background: hsl(var(--hue) 65% 58%);
  animation: cheerBurst .95s cubic-bezier(.15, .7, .3, 1) both;
}
@keyframes cheerBurst {
  0%   { transform: translate(0, 0) rotate(0deg); opacity: 1; }
  70%  { opacity: 1; }
  100% { transform: translate(var(--dx), calc(var(--dy) + 60px)) rotate(var(--rot)); opacity: 0; }
}

/* ---------- the toast (2 September 2026) ---------- */
/* Errors and guidance slide up from under the dock and put themselves away
   (owner's ask). The clip is the letterbox: anchored to the dock's top edge with
   overflow hidden, so the toast below its lip simply is not there yet. */
.dock { position: relative; }
.toast-clip {
  position: absolute;
  bottom: 100%;
  left: 0;
  right: 0;
  overflow: hidden;
  pointer-events: none;
}
.toast {
  width: fit-content;
  max-width: min(92%, 480px);
  margin: 0 auto;
  padding: 8px 16px 10px;
  background: var(--surface);
  border: 1.5px solid var(--tile-border);
  border-bottom: 0;
  border-radius: 12px 12px 0 0;
  font-size: 14px;
  font-weight: 600;
  text-align: center;
  color: var(--ink);
  box-shadow: var(--shadow);
  transform: translateY(102%);
  transition: transform .28s cubic-bezier(.2, .8, .3, 1);
}
.toast.show { transform: translateY(0); }

/* ---------- the puzzle archive (owner's ask, 4 September 2026) ----------
   Every past day, newest first, its score beside it or the invitation to play it. The
   back arrow is the close button's mirror, top-left: this sheet is a step deeper than
   the results sheet and back is how it is left (Escape and the backdrop still close
   everything). The rows scroll inside the sheet, the help sheet's pattern, so the
   arrow and the title never leave the screen. Each game carries its own copy, per the
   share-a-look-not-code rule. */
.back {
  position: absolute;
  top: 12px; left: 12px;
  width: 34px; height: 34px;
  border: 0; border-radius: 50%;
  background: transparent;
  color: var(--ink-soft);
  font-size: 22px;
  line-height: 1;
  cursor: pointer;
}
.back:active { background: var(--tile-border); }
#archive-wrap .sheet { display: flex; flex-direction: column; overflow: hidden; }
#archive-wrap .archive-scroll { overflow-y: auto; min-height: 0; margin: 0 -20px; padding: 0 20px; }
.archive-list { list-style: none; margin: 6px 0 0; padding: 0; }
.archive-month {
  margin: 14px 0 4px;
  font-size: 12px;
  font-weight: 700;
  letter-spacing: .08em;
  text-transform: uppercase;
  color: var(--ink-soft);
}
.archive-month:first-child { margin-top: 2px; }
.archive-row {
  display: flex;
  align-items: center;
  gap: 10px;
  padding: 10px 4px;
  border-top: 1px solid var(--tile-border);
  color: inherit;
  text-decoration: none;
}
.archive-row:active { background: var(--bg); }
.archive-row .ar-date { flex: 1; font-size: 15px; font-weight: 700; }
.archive-row[aria-current="page"] .ar-date { color: var(--accent); }
.archive-row .ar-no {
  font-size: 13px;
  color: var(--ink-faint);
  font-variant-numeric: tabular-nums;
}
.archive-row .ar-score {
  min-width: 64px;
  text-align: right;
  font-weight: 800;
  color: var(--accent);
  font-variant-numeric: tabular-nums;
}
.archive-row .ar-cta {
  min-width: 64px;
  padding: 5px 12px;
  border-radius: 999px;
  background: var(--accent);
  color: #fff;
  font-size: 12px;
  font-weight: 700;
  text-align: center;
}
.archive-note { padding: 14px 4px; text-align: center; color: var(--ink-soft); font-size: 14px; }
/* The replay comparison under the result: quiet, because the number that stands is the
   accent one in the column below. */
.result-replay { margin: -4px 0 12px; text-align: center; font-size: 14px; color: var(--ink-soft); }
