/* Paper palette. Contrast measured against the *adjacent* surface:
   ink #23201c on paper = 14.3:1, on the board = 12.6:1; muted text on paper
   = 6.6:1; the accent food mark on the board = 4.9:1 (needs 3:1); the snake
   head on the board = 3.4:1 and on the body = 3.7:1 (needs 3:1 — measured on
   the rendered canvas, see the palette note in app.js). --edge is the only
   affordance the buttons have, so it is held to 3:1 as well: 4.12:1 on paper,
   3.63:1 on the hover fill. --line and --frame are decorative rules. */
:root {
  --paper: #f4f0e8;
  --board: #e8e2d6;
  --line: #cbc2ae;
  --frame: #cdc3ae;
  --edge: #7a736a;
  --ink: #23201c;
  --muted: #5b544a;
  --accent: #a83c1b;
  --radius: 10px;
}

* { box-sizing: border-box; }

html, body {
  margin: 0;
  height: 100%;
}

body {
  background: var(--paper);
  color: var(--ink);
  font: 400 16px/1.45 ui-sans-serif, system-ui, -apple-system, "Segoe UI", Helvetica, Arial, sans-serif;
  -webkit-text-size-adjust: 100%;
}

/* One column: HUD, square board, footer. The board takes whatever vertical
   space is left over (see .board). min-height, never height: below roughly
   280 px of viewport the column is taller than the screen, and a fixed height
   plus justify-content:center pushed the HUD off the top edge where no amount
   of scrolling could reach it. With min-height the column grows past the
   viewport instead and the page becomes an ordinary scrolling document. */
.wrap {
  min-height: 100%;
  max-width: 560px;
  margin: 0 auto;
  padding: max(12px, env(safe-area-inset-top)) 16px max(12px, env(safe-area-inset-bottom));
  display: flex;
  flex-direction: column;
  justify-content: center;
  gap: 10px;
}

/* Wraps, because a flex row that cannot fit its items overflows them rather
   than shrinking. At 320 px under 200% page zoom — a 160 px CSS viewport —
   the titles and the two scores do not fit on one line, and `best` ran off
   the right edge of the page. Wrapping drops the scores to their own line at
   that width and at no other: every width above it is one row as before. */
.hud {
  display: flex;
  flex-wrap: wrap;
  align-items: baseline;
  justify-content: space-between;
  gap: 12px;
  flex: 0 0 auto;
}

.title {
  margin: 0;
  font-size: 17px;
  font-weight: 600;
  letter-spacing: 0.02em;
}
.title-dim { color: var(--muted); font-weight: 400; }
.titles { display: flex; flex-direction: column; gap: 2px; }
.tagline { margin: 0; color: var(--muted); font-size: 12.5px; }

.scores {
  display: flex;
  gap: 14px;
  font-variant-numeric: tabular-nums;
}
.score { color: var(--muted); font-size: 13px; }
/* The record mark, while the run is above the stored best. Two channels, not
   one: the label takes the accent colour *and* a star appears beside the
   number. The colour was carrying it alone at 5.55:1, which is a difference
   only a reader who sees the hue can read; the star survives greyscale, a
   colourblind eye and a screenshot. Quiet either way — the number beside it
   is already the news. */
.score.is-record .label { color: var(--accent); }
/* The star's box is reserved in both states. display:none took the glyph out
   of the line entirely, so `.score` slid 15 px left the instant the record
   was taken (11 px at 320) and jumped back on restart: the reward for a
   record was a shove sideways. visibility keeps the box and shows nothing. */
.score .mark {
  visibility: hidden;
  color: var(--accent);
  font-size: 13px;
  margin-left: 3px;
}
.score.is-record .mark { visibility: visible; }
.score b { color: var(--ink); font-size: 17px; font-weight: 600; margin-left: 2px; }

/* Sized by JS from the space the HUD and footer leave over — the element
   itself must not grow, or measuring it would feed back into the size. */
.board {
  flex: 0 0 auto;
  display: grid;
  place-items: center;
}

/* JS sets the width each layout pass; these are only the pre-script defaults
   so nothing flashes oversized before the first frame. The height is not set
   in JS and must not be: max-width clamps the width whenever the 160 px floor
   is wider than the column — every viewport under 192 px — and an inline
   height would then sit at 160 while the width was cut to 118, rendering the
   board 118x160. The whole mechanic is defined in square cells, so that is a
   wrong shape and not a cosmetic one. aspect-ratio makes the height follow
   whatever the clamp leaves, at every width, with no breakpoint to get wrong. */
canvas {
  display: block;
  max-width: 100%;
  aspect-ratio: 1 / 1;
  height: auto;
  background: var(--board);
  border-radius: var(--radius);
  touch-action: none; /* swipes steer, they must not scroll or zoom */
}

.stage {
  position: relative;
  line-height: 0;
  border-radius: var(--radius);
  /* Hairline frame: keeps the board readable as an object even when the
     overlay covers it. Shadow, not border, so it costs no layout box. */
  box-shadow: 0 0 0 1px var(--frame);
  touch-action: none;         /* the stage owns swipes; the page must not scroll */
  -webkit-user-select: none;
  user-select: none;
  -webkit-tap-highlight-color: transparent;
}

/* Overlay sits on the board, never outside it, so nothing overlaps the HUD. */
.overlay {
  position: absolute;
  inset: 0;
  border-radius: var(--radius);
  background: rgba(244, 240, 232, 0.93);
  display: flex;
  flex-direction: column;
  align-items: center;
  justify-content: center;
  gap: 6px;
  line-height: 1.4;
  text-align: center;
  padding: 12px;
}
/* Two cards do not veil the board. The start gate, because the board is the
   thing being introduced; and the win, because a filled 20x20 field of ink is
   the only payoff this game has and 93% paper over it turns that into a faint
   grey checker. Both put their copy on its own paper plate instead, so the
   text keeps its contrast against paper and the board behind keeps all of
   its. The other two cards still veil: nothing behind them is worth seeing. */
.overlay.is-start,
.overlay.is-won { background: transparent; }
.overlay.is-start { justify-content: flex-end; }  /* out of the middle, where the snake is */
.overlay.is-start > p,
.overlay.is-won > p {
  background: var(--paper);
  border-radius: 6px;
  padding: 1px 10px;
}
.overlay.is-start > p:empty,
.overlay.is-won > p:empty { display: none; }
.overlay[hidden] { display: none; }
.over-title { margin: 0; font-size: 20px; font-weight: 600; }
.over-line { margin: 0; font-size: 15px; font-variant-numeric: tabular-nums; }
/* Same two channels as the HUD mark: the accent colour and the star, so the
   line reads as the record even with no hue at all. */
.over-record { margin: 0; font-size: 14px; font-weight: 600; color: var(--accent); }
.over-hint { margin: 0; font-size: 13px; color: var(--muted); }

/* A card small enough for the board it is on. app.js adds .is-tight to the
   stage below a 232 px board (see TIGHT_SIZE) — measured, because a width
   media query cannot see a small board on a wide window. Without it the copy
   overflowed the stage it is anchored to: at a 160 px viewport (a 320 px
   phone at 200% page zoom) the board is 128 px and the win card's lowest
   line sat 75 px under it, over both pills, with the plate over the tagline
   and the scores. The type, the gaps, the padding and the plates all step
   down together; below 232 px the card is the only thing on the board worth
   reading, so smaller type there costs nothing a player wanted. */
.is-tight .overlay { gap: 1px; padding: 5px; line-height: 1.25; }
.is-tight .over-title { font-size: 13px; }
.is-tight .over-line { font-size: 10px; }
.is-tight .over-record { font-size: 10px; }
.is-tight .over-hint { font-size: 10px; }
.is-tight .overlay.is-start > p,
.is-tight .overlay.is-won > p { padding: 0 4px; }

.foot {
  flex: 0 0 auto;
  display: flex;
  flex-direction: column;
  align-items: center;
  gap: 8px;
}

.controls { display: flex; gap: 8px; }

.btn {
  font: inherit;
  font-size: 14px;
  color: var(--ink);
  background: transparent;
  border: 1px solid var(--edge);
  border-radius: 999px;
  padding: 7px 16px;
  min-height: 44px;   /* thumb target; 38 px was under the 44 px floor */
  cursor: pointer;
  touch-action: manipulation;
}
.btn:hover { background: var(--board); }
/* Unavailable, and it has to look it: the edge drops back to the decorative
   rule, the label goes muted, the pointer and the hover fill stop. */
.btn:disabled {
  color: var(--muted);
  border-color: var(--line);
  cursor: default;
}
.btn:disabled:hover { background: transparent; }
.btn:focus-visible { outline: 2px solid var(--accent); outline-offset: 2px; }

.sr-only {
  position: absolute;
  width: 1px; height: 1px;
  margin: -1px; padding: 0; border: 0;
  overflow: hidden;
  clip: rect(0 0 0 0);
  clip-path: inset(50%);
  white-space: nowrap;
}

.hint {
  margin: 0;
  color: var(--muted);
  font-size: 12.5px;
  text-align: center;
}

/* The two pills and their gap measure 165 px and never shrink, and .controls
   is centred, so below 165 px of viewport the row hangs off both edges: at
   320 px under 200% page zoom — a 160 px CSS viewport — `pause` started at
   -2.5 px and `restart` ended at 162.5, and the page scrolled sideways.
   165 px is where the row stops fitting the window, so that is the query.
   This is a width question and nothing else, which is what a width query can
   answer; which arrangement gives the larger board is not, and stays in
   chooseLayout(). */
@media (max-width: 164px) {
  .controls { flex-wrap: wrap; justify-content: center; }
}

/* Nothing on this page transitions or animates today; the one piece of motion
   is the run itself, and the start gate already holds that until the player
   asks for it. This block is the standing promise for anything added later. */
@media (prefers-reduced-motion: reduce) {
  *, *::before, *::after {
    animation-duration: 0.01ms !important;
    animation-iteration-count: 1 !important;
    transition-duration: 0.01ms !important;
    scroll-behavior: auto !important;
  }
}

/* Short viewports: put the HUD and the footer either side of the board
   instead of above and below it, so the board is limited by the height
   instead of by what the two stacked rows leave over. On 844x320 that is a
   187 px board turning into 296 px.

   This is a class, not a media query, and JS owns it. A query can only test
   the window; which arrangement gives the larger board depends on the HUD's
   and the footer's own measured size in each of them, and the old
   `(orientation: landscape) and (max-height: 520px) and (min-width: 560px)`
   guessed at that. It guessed wrong twice: 560x500 rendered a 204 px board
   where 559x500 — one pixel narrower, the same page in the column layout —
   rendered 341 px, and 844x521 rendered 362 px where 844x520 rendered 488.
   chooseLayout() in app.js now measures both arrangements and adds .lay-row
   only when the row actually wins, holding it to the same 528 px ceiling the
   column's max-width imposes, so neither a pixel of width nor a pixel of
   height can cost board size and a tall window still gets the column. */
.wrap.lay-row {
  flex-direction: row;
  align-items: center;
  justify-content: center;
  max-width: none;
  gap: 14px;
}
.lay-row .hud {
  flex-direction: column;
  align-items: flex-start;
  justify-content: center;
  gap: 8px;
  min-width: 104px;
  max-width: 140px;
}
.lay-row .scores { flex-direction: column; gap: 4px; }
.lay-row .foot { justify-content: center; max-width: 156px; }
.lay-row .hint { text-align: left; }
