/* THE CHROME IS THE PLATE. (David, 2026-09-22)
 *
 * The arena moved to warm field-guide paper and the chrome stayed the deep green and lime of the
 * meadow it replaced, so a player stepped from cream into dark green and back at every result — two
 * registers standing side by side, which bar exam attempt 8 scored `visual.direction` 6.5/10 for and
 * named as the one thing in the build that read as unfinished rather than as a choice. The decision
 * was to carry the paper through rather than to take the paper back.
 *
 * So every surface here is the page and every word on it is the house ink, and the accents are the
 * ones the ground already owns (`ground-art.js`): the snake's marigold, the burnt orange the bite
 * burst and the milestone ring are drawn in, and the plate's own soft ink.
 *
 * **THE ACCENT IS TWO TOKENS, AND THAT IS THE POINT.** `--lime` was one value doing both jobs, which
 * is only possible on a dark card — a bright accent is legible AS TYPE on dark and legible AS A FILL
 * on light, and nothing is both on cream. Measured on the card face `#fbf5e4`: marigold `#ffbe28`
 * reads 1.5:1 as type and is unusable, and 9.6:1 as a fill under ink. So `--accent` is the accent
 * where it is a WORD (7.2:1) and `--accent-fill` is the accent where it is a SHAPE. A future edit
 * that puts one where the other belongs is a contrast failure rather than a matter of taste, and
 * `test/hungry-hungry-snake-ui.test.js` measures every pair.
 */
:root {
  color-scheme: light;
  /* Two faces, and the second one has a job. `--font-ui` is the game's own rounded sans — every
     label, number and button on the shell. `--font-plate` is the READING face, and it exists for
     the one block on any card that is a paragraph the player is meant to stop and read rather than
     a control they are meant to skim: the snake's fact. The art direction is a naturalist's plate
     (see `_doc.md`, **Art and audio direction**), and a plate's caption is set in a serif.
     Both are stacks of faces every platform already has: this repo loads no font from a third
     party, and a vendored display face would be bytes on the path to a card that is already
     measured to the pixel. Named here rather than respelled at each call site, so the label inside
     the fact bubble can go back to the UI face in one declaration. */
  --font-ui: ui-rounded, "Avenir Next", "Trebuchet MS", system-ui, sans-serif;
  --font-plate: "Iowan Old Style", "Palatino Linotype", Palatino, Georgia, "Times New Roman", serif;
  font-family: var(--font-ui);
  /* The house ink every creature in the arena is outlined in, doing the same job in the chrome.
     13.2:1 on the page and 14.6:1 on a card face. */
  --ink: #10261f;
  /* The plate's own soft ink, for the words beside the words: 7.2:1 on the page. */
  --muted: #4f4c33;
  /* The guide's board, behind the page. A shade under the field so the arena reads as the plate
     rather than as a hole in it. */
  --deep: #ece0c4;
  /* A card's face, one step above the page so a card is a leaf laid on it rather than a hole. */
  --panel: #fbf5e4;
  --panel-light: #fffaf0;
  /* The page's cut edge: what defines a card now that contrast no longer can. */
  --panel-edge: #cdbf98;
  /* The topbar's band. 11.7:1 for the ink on it, and 1.13:1 against the field — a band the border
     and the shadow define rather than the value, which is the whole method on a light chrome. */
  --bar: #e8dcbd;
  /* The accent AS TYPE: 7.2:1 on a card face, 6.4:1 on the page. The same ink the arena's own
     announcement headline is drawn in, so the beat and the card agree. */
  --accent: #8a3a0c;
  /* The accent AS A FILL: the snake's own marigold, which carries the ink at 9.6:1. Never a word. */
  --accent-fill: #ffbe28;
  /* The warmer accent, for eyebrows and labels — a shade off the burnt orange the bite burst and the
     milestone ring are drawn in on this ground. `#a34a10` itself measures 4.35:1 on the topbar band,
     which is under the text floor, and the suite's palette gate is deliberately asked of EVERY ink
     against EVERY surface rather than only the pairs that exist today — so an accent that is safe
     to compose is worth two points of lightness. 4.67:1 at its worst. */
  --orange: #9c460e;
  --safe-top: env(safe-area-inset-top, 0px);
  --safe-right: env(safe-area-inset-right, 0px);
  --safe-bottom: env(safe-area-inset-bottom, 0px);
  --safe-left: env(safe-area-inset-left, 0px);
}

* { box-sizing: border-box; }
html, body { width: 100%; min-height: 100%; margin: 0; background: var(--deep); }
body { min-height: 100dvh; color: var(--ink); overflow: hidden; }
button, a { font: inherit; }
button, .button-link { min-width: 44px; min-height: 44px; border-radius: 999px; cursor: pointer; }
button:focus-visible, a:focus-visible { outline: 3px solid var(--ink); outline-offset: 3px; }
button:disabled { cursor: default; opacity: .5; }
.hidden { display: none !important; }
/* A class that sets `display` outranks the UA stylesheet's `[hidden] { display: none }`, so a panel
   laid out as flex or grid stays on screen with the attribute set — root lessons.md states this as a
   rule for any shell that toggles by attribute, and this one does. Without it `.account-line` was
   `display: flex` whatever `hidden` said, and a signed-OUT player was shown a Sign out button with
   nothing beside it. */
[hidden] { display: none !important; }
.visually-hidden { position: absolute !important; width: 1px; height: 1px; padding: 0; margin: -1px; overflow: hidden; clip: rect(0, 0, 0, 0); white-space: nowrap; border: 0; }

.app-shell { display: grid; grid-template-rows: auto 1fr; min-height: 100dvh; }
.topbar { position: relative; display: grid; grid-template-columns: minmax(180px, 1fr) auto minmax(180px, 1fr); align-items: center; gap: 12px; min-height: calc(70px + var(--safe-top)); padding: calc(9px + var(--safe-top)) calc(16px + var(--safe-right)) 9px calc(16px + var(--safe-left)); background: var(--bar); border-bottom: 1px solid var(--panel-edge); }
/* The bar is deliberately NOT a stacking context, so the one control cluster in it can rise above
   the overlays (below). Its shadow over the arena is what its old `z-index: 4` was for, so the
   shadow keeps that layer on a transparent pseudo-element that no click can land on. */
.topbar::after { content: ""; position: absolute; inset: 0; z-index: 4; box-shadow: 0 4px 18px #7a6a4422; pointer-events: none; }
/* The wordmark is quiet. As the largest element in the HUD it put the emphasis on the one thing a
   player already knows, above the run's own numbers. The name still belongs here — it is what says
   which tab this is — so it is quiet rather than gone. */
.brand { justify-self: start; margin: 0; color: var(--muted); font-size: clamp(.63rem, 1.1vw, .78rem); font-weight: 900; letter-spacing: .17em; line-height: 1; opacity: .82; }
.brand span { color: var(--accent); }
.run-metrics { display: flex; align-items: baseline; justify-content: center; gap: clamp(12px, 4vw, 44px); }
.run-metrics span { display: grid; gap: 2px; text-align: center; }
.run-metrics small { color: var(--muted); font-size: .68rem; font-weight: 800; letter-spacing: .13em; text-transform: uppercase; }
.run-metrics strong { color: var(--accent); font-size: clamp(1.2rem, 2.8vw, 1.7rem); font-variant-numeric: tabular-nums; }
.top-actions { justify-self: end; display: flex; gap: 6px; }
/* Mute and How to play are the two controls a player wants on EVERY screen — the music plays on
   under the result card, and a death is exactly when a rule needs looking up — so the cluster sits
   above the title, result and board overlays rather than under their scrim. What it opens goes
   above it in turn. Where a card is full bleed (below) the cluster would land on the card's own
   heading, so there it stays under the overlays as before. */
.top-actions { position: relative; z-index: 21; }
#help-overlay, #debug-overlay { z-index: 22; }
.icon-button, .text-button { border: 1px solid #10261f33; background: #ffffff5c; color: var(--ink); padding: 8px 13px; font-size: .8rem; font-weight: 800; }
.game-area { min-height: 0; background: var(--deep); }
/* The field sits at the TOP of the arena, not its middle, so the spare height a 3:2 canvas leaves
   on a tall screen collects UNDER it, where the turn controls are. Centred, a phone put ~280px
   of void above the field and the controls in the middle of nowhere. On a desktop the spare
   height is a few pixels and this changes nothing. */
.arena { position: relative; display: grid; container-type: size; width: 100%; height: 100%; min-height: 0; overflow: hidden; touch-action: none; user-select: none; }
/* THE CANVAS FILLS THE ARENA, AND THE WORLD FILLS THE CANVAS.
   The arena is a torus 24 by 16, so the camera can only avoid drawing a second copy of it when
   `unit` is at least `max(boxWidth / 24, boxHeight / 16)` — the cover fit. That floor is absolute
   and is enforced in `drawBoard`; measured on the build before it existed, a 1440x900 desktop drew
   1.31 worlds across and a 390x844 phone drew 3.21 DOWN the frame, which is three complete copies
   of the player's own snake.
   The canvas used to be given the world's own 3:2 ratio so that the floor and the contain fit were
   one number. It made the floor free and the play area a letterbox: 375x250 of a 375x702 phone
   arena, with the field-guide frame drawn as a box floating inside the page. `drawBoard` now
   expresses the camera against the COVER fit instead (`coverUnit * max(zoom, 1)`), which is the
   same framing on a 3:2 box and keeps the intimate opening on every other shape — so the canvas
   can simply be the arena.

   The BOX IS SET IN JAVASCRIPT, from the arena's own rectangle, and deliberately not here. Two CSS
   spellings were measured first and both failed: `width: 100%` with `aspect-ratio` and `max-height`
   does not clamp — the canvas grew to 1440x960 inside a 900px viewport and pushed the shell — and
   `min(100cqw, 100cqh * 24 / 16)` on a size container was dropped as invalid, leaving the canvas at
   its 300x150 intrinsic default, which then fed back through `drawBoard`'s own measurement and ran
   away to 614400px wide. `sizeCanvas` in main.js derives the box from the ARENA, so the element it
   measures is never the element it sizes and there is no loop to run away. */
#game-canvas { display: block; }
/* The canvas is the arena now, so the callout is back on the arena's own corner. It hung off a
   `--field-inset-x` while the field was a letterbox inside the page — without it the callout
   addressed a corner 237px of void away from the field it is about, in the paper ground's dark ink
   on the dark surround. */
.arena-callout { position: absolute; top: 16px; left: 18px; display: grid; gap: 4px; pointer-events: none; text-shadow: 0 2px 5px #08130e; }
.arena-callout span:first-child { color: var(--accent); font-size: clamp(.85rem, 2vw, 1.05rem); font-weight: 900; }
.arena-callout span:last-child { color: var(--muted); font-size: .76rem; }
/* On the paper ground (ground-art.js) the lime callout is light on light: ink it instead. */
.arena[data-ground="paper"] .arena-callout { text-shadow: 0 1px 3px #f2e9d0; }
.arena[data-ground="paper"] .arena-callout span:first-child { color: #10261f; }
.arena[data-ground="paper"] .arena-callout span:last-child { color: #4f4c33; }
/* THE PAUSE CHIP, and the line under it that says how to leave.
   A pill rather than a rounded box now that it carries two lines, and `pointer-events: none`
   throughout: it is a sign, not a control — the way out is the key it names or the topbar's own
   Resume button, both of which already exist. */
.pause-chip { position: absolute; top: 50%; left: 50%; translate: -50% -50%; display: grid; justify-items: center; gap: 7px; padding: 14px 26px; border: 1px solid var(--panel-edge); border-radius: 22px; background: #fbf5e4f2; box-shadow: 0 10px 26px #33291033; text-align: center; pointer-events: none; }
.pause-chip > b { color: var(--accent); font-size: 1.05rem; font-weight: 950; letter-spacing: .12em; text-transform: uppercase; }
.pause-chip small { color: var(--ink); font-size: .74rem; font-weight: 700; letter-spacing: .01em; opacity: .88; }
.pause-chip small b { color: var(--accent); font-weight: 900; }
/* The global keycap is sized for the title card's control hints; inside a chip it only has to read
   as a key. */
.pause-chip kbd { min-width: 0; height: 21px; padding: 0 6px; font-size: .7rem; }
/* One of the two lines, never both: `Esc` where there is a keyboard, the topbar's Resume where
   there is not. Same device test as the turn hint below, and for the same reason it gives — a
   1024px-wide laptop has a mouse, and a narrow desktop window is not a phone. */
.pause-resume-tap { display: none; }
@media (hover: none) and (pointer: coarse) {
  .pause-resume-key { display: none; }
  .pause-resume-tap { display: block; }
}
/* THE TOUCH HINT: on a touch device only, at the start of a run only.
   It was a pair of dark discs pinned to the field for the whole run on every device, which is two
   faults at once — furniture a keyboard player can never use, sitting on the one surface the game
   asks them to read. Steering is the arena's two halves now (main.js), so this is purely the card
   that teaches it: it comes up with the run and leaves on the first steer, or after a few seconds
   if the player has not tried one.
   `hover: none and pointer: coarse` is the phone-and-tablet test rather than a width query — a
   1024px-wide laptop has a mouse and wants none of this, and a narrow desktop window is not a
   phone. It is `pointer-events: none` throughout: the halves behind it do the work, so the hint can
   never eat the very tap it is asking for. */
.turn-controls { display: none; }
@media (hover: none) and (pointer: coarse) {
  .turn-controls {
    position: absolute; left: 50%; bottom: max(22px, var(--safe-bottom));
    display: grid; grid-template-columns: auto minmax(0, 1fr) auto; align-items: center;
    gap: clamp(10px, 4vw, 22px); width: min(92%, 430px); translate: -50% 0;
    padding: 12px 14px; border-radius: 22px;
    border: 1px solid #10261f1f; background: #fbf5e4e6; box-shadow: 0 14px 34px #33291033;
    pointer-events: none; opacity: 1; transition: opacity .5s ease, translate .5s ease;
  }
  .turn-controls.is-dismissed { opacity: 0; translate: -50% 14px; }
  .turn-half { display: grid; justify-items: center; gap: 3px; min-width: 66px; }
  .turn-half b { display: grid; place-items: center; width: 54px; height: 54px; border-radius: 50%;
    background: #10261f; color: #ffbe28; font-size: 1.7rem; font-weight: 900; line-height: 1; }
  .turn-half small { color: #4f4c33; font-size: .55rem; font-weight: 800; letter-spacing: .05em; text-transform: uppercase; }
  .turn-tip { color: #10261f; font-size: .78rem; font-weight: 800; line-height: 1.3; text-align: center; }
  /* On the meadow the card would be cream on dark; take the ground's own ink either way. */
  .arena[data-ground="meadow"] .turn-controls { border-color: #d7f27c33; background: #14342acc; box-shadow: 0 14px 34px #04100b66; }
  .arena[data-ground="meadow"] .turn-half b { background: #d7f27c; color: #10261f; }
  .arena[data-ground="meadow"] .turn-half small { color: var(--muted); }
  .arena[data-ground="meadow"] .turn-tip { color: var(--ink); }
}

/* CENTRED WHEN IT FITS, TOP-ALIGNED WHEN IT DOES NOT, and the difference is a card the player can
   never see. `place-items: center` centres a card that is TALLER than the overlay too, and the
   half that overflows the top is then unreachable — `scrollTop` is already 0 and cannot go
   negative. Measured on the running game at 812x375: the title sat at -83px and stayed there at
   minimum scroll; at 1024x600 it was clipped by 6px.

   The fix was already in this file, under `max-width: 640px`, which is the wrong condition — the
   question is whether the card is taller than the viewport, and a landscape phone is 812px WIDE.
   So it moves here, for every overlay: `align-items: start` with an auto block margin centres the
   card while there is free space (auto margins absorb it) and resolves to 0 when there is not,
   which is exactly the behaviour wanted in both directions. The result card, which this game's own
   doc records at 920px on a 900px viewport, was clipped the same way and is fixed with it. */
.overlay { position: fixed; z-index: 20; inset: 0; display: grid; justify-items: center; align-items: start; overflow: auto; padding: calc(18px + var(--safe-top)) calc(18px + var(--safe-right)) calc(18px + var(--safe-bottom)) calc(18px + var(--safe-left)); background: #2a241799; backdrop-filter: blur(10px); }
.card { position: relative; width: min(100%, 620px); margin: auto 0; padding: clamp(24px, 5vw, 50px); border: 1px solid var(--panel-edge); border-radius: 30px; background: linear-gradient(145deg, var(--panel-light), var(--panel) 72%); box-shadow: 0 26px 70px #33291066, 0 2px 0 #ffffffcc inset; text-align: center; }
.title-card { overflow: hidden; }
.title-card::before { content: ""; position: absolute; width: 290px; height: 290px; right: -110px; top: -125px; border: 30px solid #a34a1014; border-radius: 50%; transform: rotate(18deg); pointer-events: none; }
.eyebrow { position: relative; margin: 0 0 12px; color: var(--orange); font-size: .72rem; font-weight: 950; letter-spacing: .2em; text-transform: uppercase; }
h1, h2, h3, p { position: relative; }
/* A DISPLAY LINE HEIGHT IS A MEASUREMENT OF THE FACE, not a number that looks tight.
   This was `.82` here and `.84` on the title card, and the title is two lines — so the descenders
   of `Hungry Hungry` ran into the cap of `Snake`. Measured off the resolved font's own metrics
   (`actualBoundingBoxDescent` of the first line plus `actualBoundingBoxAscent` of the second,
   against the computed line box): 15 + 55 of ink in a 61.8px box at 1280x900, which is **8.2px of
   overlap** — and 4.7 to 5.4px at every phone width. A Range rect cannot see this, because the line
   BOXES stack correctly and it is the glyphs that overflow them.
   `1` clears it with about 3.6px of air and is still a tight display setting; it costs the title
   card ~24px, which `verify-start-screen.mjs` measures at five viewports. */
h1 { margin: 0; color: var(--ink); font-size: clamp(3rem, 10vw, 6.5rem); letter-spacing: -.08em; line-height: 1; }
h1 em { color: var(--accent); font-style: normal; }
h2 { margin: 0; color: var(--accent); font-size: clamp(2rem, 6vw, 3.5rem); letter-spacing: -.05em; line-height: .95; }
h3 { margin: 0; font-size: 1rem; }
.lead { margin: 20px auto 0; max-width: 470px; color: var(--muted); font-size: clamp(1rem, 2.5vw, 1.18rem); line-height: 1.45; }
.microcopy, .small-copy { color: var(--muted); font-size: .8rem; line-height: 1.4; }
/* ── The start screen ──────────────────────────────────────────────────────────────────────────
   Four bands under the title: the appetite ladder, the three things to know, what earns extra
   points, and the actions. Built from the Claude Design import; the vertical rhythm is a `clamp`
   off the viewport HEIGHT rather than the design's fixed margins, because the artboard it was drawn
   on had no height constraint and a 768px laptop has 81px of headroom over the old card. Measured
   before and after by working-material/hungry-hungry-snake/verify-start-screen.mjs. */
.title-card { width: min(100%, 780px); padding: clamp(20px, 3.4vw, 34px) clamp(20px, 4vw, 40px); }
.title-block { position: relative; display: flex; flex-direction: column; align-items: center; gap: clamp(6px, 1.4vh, 14px); }
/* Same measurement as the base rule above, and the same answer — plus the HEIGHT the answer costs.
   Two lines at `line-height: 1` are 24px taller than two at `.84`, and measured, that put
   `Start hunting` 15px below the fold at 1280x720 (a laptop this card had only 9px of air on
   before). The title was bounded by the viewport's WIDTH alone, which is the wrong axis for a card
   whose whole constraint is vertical: `min(…, 8.5vh)` caps it by the screen it has to fit in, so
   the h1 is 61px on a 720-tall window (2px SHORTER than the overlapping version it replaces), 65px
   at 768 and its full 73.6px at 900, where there is 89px of room to spend.
   Phones do not reach this rule — both narrow breakpoints below set their own size — and they have
   hundreds of pixels of air besides. */
.title-card h1 { font-size: min(clamp(2.7rem, 8vw, 4.6rem), 8.5vh); letter-spacing: -.07em; line-height: 1; }
.title-card .lead { margin: 0; max-width: 460px; font-size: clamp(1rem, 2.2vw, 1.12rem); }
/* The ladder. Its own panel because it is the one band that is a TABLE of the rules rather than
   prose, and the player will come back to it between runs. */
.ladder-panel { position: relative; margin-top: clamp(14px, 2.6vh, 26px); padding: clamp(10px, 1.6vh, 16px) clamp(10px, 2vw, 16px) clamp(8px, 1.2vh, 12px); border: 1px solid #10261f1f; border-radius: 18px; background: #efe3c766; }
.ladder-head { display: flex; align-items: baseline; justify-content: space-between; gap: 12px; margin-bottom: 4px; }
.panel-label { margin: 0; color: var(--orange); font-size: .68rem; font-weight: 950; letter-spacing: .16em; line-height: 1.2; text-transform: uppercase; }
.panel-note { margin: 0; color: var(--muted); font-size: .68rem; font-weight: 800; line-height: 1.2; text-align: right; }
/* `--box` is the one height the six animals stand in, and the RAMP IS DERIVED FROM IT. Authored as
   three fixed `--ramp` values per breakpoint it was wrong the first time anyone measured it: the box
   is a `clamp` off viewport height, so at 1024x768 it came out 84px while the elephant was still
   drawn at its full 88 and hung out of the panel. Now the top rung exactly fills the box less its
   padding and every other rung is proportional, so "inside the box" is structural rather than
   something a breakpoint has to remember — and the desktop ramp is still the design's own
   30/42/52/62/74/88. */
.ladder { display: grid; grid-template-columns: repeat(6, minmax(0, 1fr)); gap: 4px; align-items: end; margin: 0; padding: 0; list-style: none; --box: clamp(64px, 11vh, 96px); --rung-top: 88; --rung-pad: 8px; }
.rung { display: flex; flex-direction: column; align-items: center; min-width: 0; }
/* One box height for all six, bottom-aligned, so the six animals stand on one line and the ramp is
   the read. `--rung` is each animal's own drawn height inside it. */
.rung-art { display: flex; align-items: flex-end; justify-content: center; height: var(--box); padding-bottom: var(--rung-pad); }
/* `--rung` is UNITLESS — a share of the top rung — so the whole ladder scales with `--box`. It was
   a clamped pixel height first, and clamping flattened the 3x2 phone layout: the mouse, goat and
   elephant all hit the cap and the top half of the ladder read as three animals the same size,
   which is the one thing this band exists to say. */
.rung-art img { display: block; width: auto; height: calc((var(--box) - var(--rung-pad)) * var(--rung) / var(--rung-top)); max-width: 100%; filter: drop-shadow(0 5px 7px #33291040); }
.rung-rule { width: 100%; height: 2px; border-radius: 2px; background: #10261f26; }
.rung-name { margin-top: 9px; overflow: hidden; max-width: 100%; color: var(--ink); font-size: .7rem; font-weight: 900; letter-spacing: .06em; text-overflow: ellipsis; text-transform: uppercase; white-space: nowrap; }
.rung-len { margin-top: 3px; color: var(--accent); font-size: .68rem; font-weight: 950; font-variant-numeric: tabular-nums; }
.rung-pts { margin-top: 2px; color: var(--muted); font-size: .62rem; font-weight: 800; font-variant-numeric: tabular-nums; }
/* The top rung wears the citrus, and it is addressed as "the last one" rather than as "the
   elephant" — the ladder is meant to stay re-orderable, which is the lesson every other per-tier
   thing in this game already learned by being keyed to a position. */
.ladder .rung:last-child .rung-rule { background: #a34a1066; }
.ladder .rung:last-child .rung-name { color: var(--orange); }
/* Keyframes with no fill mode, so the RESTING style is the visible one: a hidden automated tab
   freezes an animation at its first frame, and root lessons.md records that a transition there is
   never created at all. Out of phase per rung, or six animals bob as one. */
@keyframes rungBob { 0%, 100% { translate: 0 0; } 50% { translate: 0 -4px; } }
.rung img { animation: rungBob 3.4s ease-in-out infinite; }
.rung:nth-child(2) img { animation-delay: .28s; }
.rung:nth-child(3) img { animation-delay: .56s; }
.rung:nth-child(4) img { animation-delay: .84s; }
.rung:nth-child(5) img { animation-delay: 1.12s; }
.rung:nth-child(6) img { animation-delay: 1.4s; }

/* The three things to know. Left-aligned inside a centred card, because these are sentences. */
/* Four columns since v13 added the tail bite: the three things you have to know, and the one way
   to finish on purpose. `auto-fit` is deliberately not used — the minimum a column needs is a
   measurement of the longest title plus its copy at this type, and a track count chosen by the
   browser would ellipsise one of them at some width nobody tested. */
.howto { position: relative; display: grid; grid-template-columns: repeat(4, minmax(0, 1fr)); gap: clamp(10px, 1.6vw, 16px); margin-top: clamp(14px, 2.4vh, 24px); text-align: left; }
.howto-col { display: flex; flex-direction: column; gap: 7px; }
.howto-icon { display: flex; align-items: center; gap: 7px; height: 26px; margin: 0; }
.howto-title { margin: 0; color: var(--ink); font-size: .82rem; font-weight: 950; letter-spacing: .02em; }
.howto-copy { margin: 0; color: var(--muted); font-size: .79rem; line-height: 1.42; text-wrap: pretty; }
.howto-aside { color: var(--muted); font-size: .72rem; font-weight: 800; }
kbd { display: inline-grid; place-items: center; min-width: 26px; height: 26px; padding: 0 7px; border: 1px solid #10261f33; border-radius: 8px; background: #ffffff99; color: var(--ink); font: inherit; font-size: .74rem; font-weight: 950; }
/* The mouth, as the only thing on this screen that cannot be said in a keycap: three openings,
   because the aperture is what the tier gate actually is. */
/* A MARIGOLD MARK ON CREAM NEEDS THE OUTLINE EVERY CREATURE IN THE ARENA WEARS. The fill carries
   the house ink at 9.6:1 when the ink is ON it — that is the primary button — and measures 1.5:1
   as a shape drawn on a card, which is below even the 3:1 a graphic needs. The arena has always
   answered this the same way: a flat fill inside the house ink. So do these. */
.mouth-dots span { border: 1.5px solid var(--ink); border-radius: 50%; background: var(--accent-fill); }
.mouth-dots span:nth-child(1) { width: 11px; height: 11px; opacity: .62; }
.mouth-dots span:nth-child(2) { width: 16px; height: 16px; opacity: .82; }
.mouth-dots span:nth-child(3) { width: 22px; height: 22px; }
/* The ouroboros mark: a ring with a bite out of it, drawn rather than set as a glyph — an emoji is
   a different picture on every platform, which is a second art direction inside a game that has
   one. The gap is the mouth, and it faces the tail it is about to close on. */
.ouroboros span { width: 22px; height: 22px; border: 4px solid var(--ink); border-radius: 50%; border-right-color: transparent; transform: rotate(-38deg); }

/* The best hunt on the title card: one quiet line beside the buttons, never a second headline — the
   card has one obvious first move and it is the lime pill next to this. No margin of its own,
   because it rides the actions row's height rather than adding a row (see index.html). */
.title-best { position: relative; margin: 0; align-self: center; max-width: 100%; color: var(--muted); font-size: .82rem; font-weight: 800; }
.title-best b { color: var(--accent); font-variant-numeric: tabular-nums; }
.bonus-row { position: relative; display: flex; flex-wrap: wrap; align-items: center; gap: 8px; margin-top: clamp(12px, 2.2vh, 22px); padding-top: clamp(10px, 1.8vh, 18px); border-top: 1px solid #10261f1f; text-align: left; }
.bonus-label { color: var(--muted); font-size: .7rem; font-weight: 950; letter-spacing: .14em; text-transform: uppercase; }
.bonus { padding: 6px 11px; border: 1px solid #a34a1040; border-radius: 999px; background: #ffffff8c; color: var(--ink); font-size: .75rem; font-weight: 800; }
.bonus b { color: var(--orange); }

.primary, .secondary, .ghost { border: 0; padding: 12px 20px; font-weight: 950; text-decoration: none; }
.primary { background: var(--accent-fill); color: var(--ink); }
.secondary { background: #ffffffb3; color: var(--ink); border: 1px solid #10261f2b; }
/* The quiet leaderboard button. Shape AND colour on one class — root lessons.md: a class carrying
   only a background renders as a bare UA button wearing the brand colour in any other container. */
.ghost { border: 1px solid #10261f33; background: #ffffff59; color: var(--ink); font-size: .88rem; transition: background .12s ease, border-color .12s ease; }
.ghost:hover { background: #ffffffa6; border-color: #10261f66; }
.title-actions { margin-top: clamp(14px, 2.4vh, 26px); gap: 14px; }
.title-actions .primary, .title-actions .ghost { min-height: 52px; }
.title-card .primary { position: relative; min-width: 230px; border-radius: 999px; padding: 14px 26px; font-size: 1.02rem; box-shadow: 0 9px 0 #c98a12; transition: translate .12s ease, box-shadow .12s ease; }
.title-card .primary:active { translate: 0 4px; box-shadow: 0 5px 0 #c98a12; }
.card-actions { position: relative; display: flex; flex-wrap: wrap; justify-content: center; gap: 10px; margin-top: 20px; }
.button-link { display: inline-grid; place-items: center; }
.close-button { position: absolute; top: 13px; right: 15px; border: 0; background: transparent; color: var(--muted); font-size: 2rem; line-height: 1; }
.compact-card { width: min(100%, 510px); }
.compact-card p { line-height: 1.55; }

/* FIELD NOTES: WIDE ON A DESKTOP, and figures beside the prose.
   It was a 510px column of centred text on a 1440px screen — the narrowest thing in the game on
   the widest surface it has — and entirely wordless, which is the worst combination for the three
   rules here that are SPATIAL: which animal fits, what counts as a bite, and how you ever meet
   your own tail. Two columns at 900px, the copy left-aligned because a ragged centre is hard to
   read at this length, and one column again below 860px where the second would be a squeeze. */
/* Capped and scrollable, because a help card that runs off the screen hides its own way out: the
   bar exam measured this card overflowing a 705px-tall desktop with its last two paragraphs and its
   own `Got it` button below the fold. The cap is on the CARD rather than the overlay so the heading
   scrolls with the text rather than the card scrolling inside a frame. */
.help-card { width: min(100%, 1060px); max-height: 86dvh; overflow-y: auto; overscroll-behavior: contain; text-align: left; }
.help-card .eyebrow, .help-card h2 { text-align: center; }
.help-card h2 { margin-bottom: 18px; }
.help-grid { display: grid; grid-template-columns: minmax(0, 1.35fr) minmax(0, 1fr); gap: clamp(18px, 3vw, 34px); align-items: start; }
.help-copy { display: grid; gap: 13px; }
.help-copy p { margin: 0; line-height: 1.55; }
/* `minmax(0, 1fr)` and not the implicit `auto` track: root lessons.md's rule, and it bit here
   immediately — a grid with no template takes an auto column sized to its largest min-content, so
   the 960px-wide figures sized the track and ran off the card, taking the ladder with them. */
.help-figures { display: grid; grid-template-columns: minmax(0, 1fr); gap: 14px; }
.help-fig img { display: block; width: 100%; height: auto; border-radius: 10px; }
.help-fig { margin: 0; display: grid; gap: 7px; padding: 12px 12px 10px; border: 1px solid #10261f1a; border-radius: 16px; background: #efe3c77a; }
.help-fig svg { display: block; width: 100%; height: auto; }
.help-fig figcaption { color: var(--muted); font-size: .72rem; line-height: 1.4; text-align: center; }
/* The ladder, from the arena's own rigs. `min-content` rows keep the six columns even however
   different the artwork's aspects are. */
.help-ladder { display: grid; grid-template-columns: repeat(6, minmax(0, 1fr)); gap: 4px; align-items: end; }
.help-ladder span { display: grid; justify-items: center; gap: 2px; }
.help-ladder img { width: 100%; max-width: 40px; height: 34px; object-fit: contain; }
.help-ladder b { color: var(--ink); font-size: .58rem; font-weight: 800; letter-spacing: .02em; text-transform: uppercase; }
.help-ladder i { color: var(--accent); font-size: .68rem; font-style: normal; font-weight: 900; }
.help-card .secondary { margin-top: 20px; }
@media (max-width: 860px) {
  .help-grid { grid-template-columns: minmax(0, 1fr); }
  /* Figures first on a phone: the pictures are the half that survives a small screen. */
  .help-figures { order: -1; }
}
.rule { padding: 13px 15px; border-left: 3px solid var(--orange); background: #efe3c77a; text-align: left; }
.debug-card { width: min(100%, 650px); padding: clamp(24px, 4vw, 38px); }
.debug-card code { color: var(--accent); font-size: .9em; }
.debug-controls { display: grid; gap: 13px; margin-top: 22px; text-align: left; }
.debug-controls label { display: grid; gap: 6px; color: var(--ink); font-size: .82rem; font-weight: 800; }
.debug-controls label > span { display: flex; align-items: baseline; justify-content: space-between; gap: 12px; }
.debug-controls output { color: var(--accent); font-variant-numeric: tabular-nums; }
.debug-controls input[type="range"] { width: 100%; accent-color: var(--accent-fill); }
.debug-controls select { width: 100%; padding: 9px 11px; border: 1px solid #10261f33; border-radius: 10px; background: #fffaf0; color: var(--ink); }
.debug-food-types { display: grid; grid-template-columns: repeat(auto-fit, minmax(145px, 1fr)); gap: 8px; margin: 22px 0 0; padding: 13px; border: 1px solid #10261f1f; border-radius: 14px; text-align: left; }
.debug-food-types legend { padding: 0 6px; color: var(--orange); font-size: .72rem; font-weight: 950; letter-spacing: .12em; text-transform: uppercase; }
.debug-food-types label { display: flex; align-items: center; gap: 7px; color: var(--ink); font-size: .78rem; }
.debug-food-types input { accent-color: var(--accent-fill); }
.debug-actions { margin-top: 18px; }
.debug-export { max-height: 140px; overflow: auto; margin: 18px 0 0; padding: 12px; border-radius: 12px; background: #efe3c7cc; color: var(--accent); font: .72rem/1.45 ui-monospace, SFMono-Regular, Consolas, monospace; text-align: left; white-space: pre-wrap; }
.result-card { width: min(100%, 700px); padding: clamp(22px, 4vw, 38px); }
/* The unit needs a gap, and the gap is in `em` of the SCORE's own font. At `gap: 0` the comma of
   "10,000" painted about a pixel past the bottom of a line box that `line-height: .85` has already
   made shorter than the type in it, so POINTS sat on the number. Measured rather than guessed: the
   painted descent below the box is 1px, so .13em lands ~11px of air at the 6rem end and ~7px at the
   3.8rem one — which a pixel value could not do, because the score is `clamp(3.8rem, 12vw, 6rem)`
   and a number tuned at one end of that range is wrong at the other. */
.result-score { position: relative; display: grid; gap: .13em; margin: 18px 0 9px; color: var(--accent); font-size: clamp(3.8rem, 12vw, 6rem); font-weight: 950; letter-spacing: -.09em; line-height: .85; }
.result-score small { color: var(--orange); font-size: .72rem; font-weight: 900; letter-spacing: .18em; text-transform: uppercase; }
/* The ouroboros' arithmetic, under the points label. Lime rather than orange because it is the
   score's own line rather than the score's unit — and normal-cased, because it is a sum to read
   rather than a label to skim.

   `.result-score small` above is (0,1,1) and a bare `.result-doubled` is (0,1,0), so the plain class
   lost every one of these declarations to the label it sits under and shipped as an orange, tracked,
   uppercase caption — root lessons.md's rule about giving a shared token element specificity where a
   page is expected to override it, seen from the other side. */
.result-score small.result-doubled { color: var(--accent); font-size: .82rem; font-weight: 800; letter-spacing: 0; text-transform: none; }
/* The personal best on the result card, directly under the score it is about. A new best says so in
   the game's own lime; a run under the standing one is a quiet reference point and nothing more. */
.result-best { position: relative; margin: 0 0 4px; color: var(--muted); font-size: .84rem; font-weight: 800; }
.result-best b { color: var(--ink); font-variant-numeric: tabular-nums; }
.result-best.is-new { color: var(--accent); }
.result-best.is-new b { color: var(--accent); }

/* The run, drawn.

   Every colour here is one the game already owns, which is what makes the picture readable without a
   legend: a block is `SNAKE_PALETTE.dorsal`, the snake's own marigold back, outlined in the house
   ink every creature wears, and a dot is the body fill of that prey's rig in `art/`. The runtime
   sets `--strip-cell` from the body's length (`stripCellSize`), and the gap is derived from it, so a
   long run shrinks the strip instead of growing it another four rows. */
.result-body-block { position: relative; display: grid; gap: 11px; margin: 24px 0 0; text-align: left; }
/* `--strip-cell` is the runtime's answer and arrives as an inline style, which no stylesheet rule
   can override without `!important`. `--cell` is what everything below reads, so a viewport too
   short for the card can cap the size in a media query without fighting the element's own attribute
   — and the runtime keeps deciding how big a long body should be everywhere else. */
.result-body { --strip-cell: 15px; --cell: var(--strip-cell); display: flex; flex-wrap: wrap; align-items: center; gap: calc(var(--cell) * .26); min-height: var(--cell); }
.result-body span { flex: 0 0 auto; width: var(--cell); height: var(--cell); border-radius: calc(var(--cell) / 3); background: #ffbe28; }
/* The length every run is handed. Drawn as an outline rather than a block because it is body the
   player did not earn, and the strip is a picture of what they did. */
.result-body span.start { border: 1px dashed #ffbe2866; background: none; }
/* The animal that paid for this length: round, in its own colour, wearing the house outline. */
.result-body span.mark { border: 2px solid #10261f; border-radius: 999px; }
.result-body-line { display: flex; flex-wrap: wrap; align-items: baseline; gap: 14px; margin: 0; color: var(--muted); font-size: .75rem; font-weight: 800; letter-spacing: .13em; text-transform: uppercase; }
.result-body-line b { color: var(--ink); font-size: 1.25rem; font-weight: 950; letter-spacing: 0; font-variant-numeric: tabular-nums; }
.result-body-key { margin-left: auto; font-weight: 700; letter-spacing: .06em; text-transform: none; }
/* The field guide's snake, saying one fact.

   The bubble is that animal's SPEECH, so its tail meets the snake's tongue: a bubble merely parked
   beside a character is a caption, and a caption is not the moment this is for. The cream and the
   ink outline are the game's own — the card is dark, so the one bright panel on it has to be the
   thing the player is meant to read. */
/* `--fact-art` is the artwork's width, and everything else here is derived from it. The tail has to
   meet the TONGUE, which sits 41% down a 260x320 drawing — so its offset is
   width x (320/260) x 0.41, minus half the tail. Written out, three breakpoints each guessed their
   own number and two of them were 16-21px out. Setting one variable per breakpoint keeps the
   picture and the thing pointing at it in step. */
.fact-line { position: relative; display: grid; --fact-art: 78px; --fact-tail: 17px; grid-template-columns: var(--fact-art) minmax(0, 1fr); align-items: start; gap: 3px; margin: 13px 0 0; text-align: left; }
/* Keyframes only, with no fill mode, so the RESTING style is the visible one: a hidden automated
   tab freezes an animation at its first frame, and an entrance that fades in from `both` would sit
   at zero opacity for ever there (root lessons.md). */
.fact-snake { display: block; width: 100%; height: auto; filter: drop-shadow(0 8px 10px #33291045); animation: factSnakeIn .5s cubic-bezier(.2, 1.3, .35, 1), factSnakeBob 3.2s ease-in-out .5s infinite; }
@keyframes factSnakeIn { from { transform: translateY(16px) rotate(-5deg) scale(.88); } to { transform: none; } }
@keyframes factSnakeBob { 0%, 100% { translate: 0 0; } 50% { translate: 0 -5px; } }
.fact-bubble { position: relative; padding: 8px 14px; border: 3px solid var(--ink); border-radius: 18px; background: #fffdf6; color: var(--ink); box-shadow: 0 6px 0 #33291033; }
/* The tail is the bubble's own edge rather than a node between the two. */
.fact-bubble::before { content: ""; position: absolute; left: calc(1px - var(--fact-tail)); top: calc(var(--fact-art) * .505 - var(--fact-tail) / 2); width: calc(var(--fact-tail) + 1px); height: var(--fact-tail); background: #fffdf6; clip-path: polygon(100% 0, 100% 100%, 0 46%); filter: drop-shadow(-3px 2px 0 #10261f); }
/* The shared eyebrow is citrus on a dark card and would be unreadable on cream, so the label has
   its own colour — and it leads the sentence rather than taking a line. */
/* The label stays on the UI face: it is a caption on the bubble, not part of the sentence, and the
   `em` size keeps it proportional to whatever the fact is set at. */
.fact-label { color: #7a4a07; font-family: var(--font-ui); font-size: .68em; font-weight: 950; letter-spacing: .12em; text-transform: uppercase; }
.fact-label::after { content: " · "; letter-spacing: 0; }
/* THE FACT IS THE ONE PARAGRAPH ON THIS CARD, so it is set to be read rather than skimmed: the
   plate's serif (see `:root`) at a real reading size instead of `.84rem` of bold UI sans, which was
   the smallest prose in the game on the one screen that asks the player to stop. Bigger and lighter
   together — a serif at 700 on cream is a shout, and the weight was only ever carrying the size. */
.fact-text { margin: 0; font-family: var(--font-plate); font-size: 1.02rem; font-weight: 500; line-height: 1.45; }

.submit-status { position: relative; min-height: 1.35em; margin: 15px 0 0; color: var(--orange); font-size: .82rem; font-weight: 800; }
/* The board, on a screen of its own. It left the result card because ten ranked rows and a fact
   card cannot both fit under a score, the run's own body, a submission line and the sign-in
   offer. */
.board-card { width: min(100%, 540px); padding: clamp(24px, 4vw, 34px); text-align: left; }
.board-card .eyebrow { margin-bottom: 8px; }
.board-verify { margin: 10px 0 0; }
.board-columns { display: grid; grid-template-columns: 38px 1fr auto; gap: 7px; margin: 18px 0 7px; padding: 0 9px; color: var(--muted); font-size: .64rem; font-weight: 900; letter-spacing: .13em; text-transform: uppercase; }
.board-columns span:last-child { text-align: right; }
.board-status { margin: 10px 0 0; }
.board-card .card-actions { justify-content: flex-start; }
.text-button { min-width: 0; min-height: 34px; padding: 5px 8px; color: var(--muted); }
/* Who this browser ranks as, and the way out of it: a sentence with one quiet control in it. */
.account-line { display: flex; align-items: center; justify-content: center; gap: 6px; margin: 10px 0 0; color: var(--muted); font-size: .82rem; }
.account-line b { color: var(--ink); font-weight: 900; }
.board-rows { display: grid; gap: 5px; padding: 0; margin: 0; list-style: none; min-height: 54px; }
.board-row { display: grid; grid-template-columns: 38px 1fr auto; align-items: center; gap: 7px; padding: 8px 9px; border-radius: 8px; background: #10261f0d; font-size: .86rem; }
.board-row.viewer { background: #ffbe2833; outline: 1px solid #a34a1066; }
.board-row .rank { color: var(--orange); font-weight: 950; }
.board-row .player { overflow: hidden; color: var(--ink); font-weight: 800; text-overflow: ellipsis; white-space: nowrap; }
.board-row .score { display: flex; flex-direction: column; align-items: flex-end; gap: 1px; text-align: right; color: var(--accent); font-variant-numeric: tabular-nums; font-weight: 950; }
/* What the score is worth in ZapJam points. The sub-line is the shared renderer's — a cell declares
   `zaps` and `zapjam-board.js` builds it — so it is the same shape on every board in the catalogue;
   only these colours are this game's. The coin is the ZapJam mark drawn as a CSS MASK: the mark
   wears a gradient, an `<img>` cannot be recoloured, and a script is a thing that can fail
   mid-render. It is inline data, so nothing is fetched. */
.board-zaps { display: inline-flex; align-items: center; gap: 4px; color: var(--muted); font-size: .62rem; font-weight: 900; }
.board-zap-coin { width: .95em; height: .95em; flex: none; background: linear-gradient(135deg, #e0f050, #f4d344 42%, #f88b50);
  -webkit-mask-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 640 640'%3E%3Cpath d='M 515.4 142.5 A 264 264 0 1 1 440.9 85.3' fill='none' stroke='%23000' stroke-width='47' stroke-linecap='round'/%3E%3Cpath d='M -84 -120 L 156 -120 L -12 48 L 156 48 L 84 120 L -156 120 L 12 -48 L -156 -48 Z' fill='%23000' transform='translate(320 310)'/%3E%3C/svg%3E"); mask-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 640 640'%3E%3Cpath d='M 515.4 142.5 A 264 264 0 1 1 440.9 85.3' fill='none' stroke='%23000' stroke-width='47' stroke-linecap='round'/%3E%3Cpath d='M -84 -120 L 156 -120 L -12 48 L 156 48 L 84 120 L -156 120 L 12 -48 L -156 -48 Z' fill='%23000' transform='translate(320 310)'/%3E%3C/svg%3E");
  -webkit-mask-size: contain; mask-size: contain; -webkit-mask-repeat: no-repeat; mask-repeat: no-repeat; -webkit-mask-position: center; mask-position: center; }
/* The word, for a screen reader that would otherwise be handed a bare number under a score. */
.board-zap-unit { position: fixed; width: 1px; height: 1px; clip: rect(0 0 0 0); overflow: hidden; }

@media (max-width: 640px) {
  .topbar { grid-template-columns: 1fr auto; }
  .brand { grid-column: 1; grid-row: 1; }
  .run-metrics { grid-column: 1 / -1; grid-row: 2; order: 3; gap: 28px; }
  .top-actions { grid-column: 2; grid-row: 1; }
  .arena-callout { top: 12px; left: 13px; }
  .title-card { padding: 24px 16px 22px; }
  h1 { font-size: clamp(3.1rem, 16vw, 5rem); }
  .title-card h1 { font-size: clamp(2.5rem, 12vw, 3.4rem); }
  /* Six columns in 300px is 46px a column, which cannot hold the word ELEPHANT — so the ladder
     re-flows to 3x2, still ascending left to right and then down. */
  .ladder { grid-template-columns: repeat(3, minmax(0, 1fr)); gap: 4px 6px; }
  .rung { padding-bottom: 6px; }
  .ladder { --box: 66px; }
  .ladder-head { flex-direction: column; align-items: center; gap: 2px; }
  .panel-note { text-align: center; }
  /* One column, but as ROWS rather than stacked blocks: the icon keeps its own column beside the
     words, which costs about 50px a item instead of the 90px three stacked columns would. */
  .howto { grid-template-columns: minmax(0, 1fr); gap: 12px; }
  .howto-col { display: grid; grid-template-columns: 58px minmax(0, 1fr); align-items: baseline; gap: 4px 10px; }
  .howto-icon { grid-row: span 2; height: auto; flex-wrap: wrap; gap: 4px; }
  .bonus { font-size: .72rem; padding: 5px 9px; }
  .title-actions { gap: 10px; }
  .title-card .primary { min-width: 100%; }
  .title-actions .ghost { flex: 1 1 40%; }
  /* THE BUTTON THAT STARTS THE GAME COMES SECOND WHERE THE CARD CANNOT FIT.
     Measured: the design's reading order (teach, then play) put `Start hunting` 133px below the
     fold on a 375x812 phone and 280px below it on a 360x640 one, where the old card fit at every
     viewport — a scroll before the first interaction, against the promise that outranks every other
     feature in the root doc. Nothing is hidden or dropped; the four teaching bands simply sit under
     the action instead of over it, which is the order the same screen would be written in if it had
     been drawn for a phone. Focus order is untouched, because the actions are the only focusable
     things on this card. */
  .title-card { display: flex; flex-direction: column; }
  .title-block { order: 1; }
  .title-actions { order: 2; }
  .ladder-panel { order: 3; }
  .howto { order: 4; }
  .bonus-row { order: 5; }
  .result-card { padding: 23px 15px; }
  /* The character gives up width to the words here. A 217-character fact in a 250px column is seven
     lines; every pixel the art does not take is a line the bubble does not need. */
  /* The strip is the block that grows with the run, so it is the one that gives ground on a
     narrow card: the same body over fewer, tighter rows. */
  .result-body-block { margin-top: 17px; gap: 8px; }
  .result-body-line { gap: 10px; font-size: .7rem; }
  .result-body-line b { font-size: 1.05rem; }
  /* The legend is what makes the picture readable, so it keeps its own line here rather than being
     pushed onto one by a flex wrap that nobody chose. */
  .result-body-key { flex-basis: 100%; margin-left: 0; }
  /* THE BIGGER FACT IS PAID FOR IN THE GAPS AROUND IT, not by the reader.
     Measured with the LONGEST of the hundred shipped facts (217 characters) and the ouroboros
     sentence above it, the reading size took `Hunt again` 10px below the fold at 375x812. These
     three margins give back 17px on a card that is now edge to edge and no longer owes the overlay
     a gutter — the block the player reads keeps every pixel, and the air between blocks pays. */
  .result-card .submit-status { margin-top: 10px; }
  .result-card .card-actions { margin-top: 12px; }
  .fact-line { --fact-art: 58px; --fact-tail: 13px; margin-top: 9px; gap: 3px; }
  .fact-bubble { padding: 8px 11px; border-width: 2px; border-radius: 14px; box-shadow: 0 5px 0 #33291033; }
  .fact-bubble::before { filter: drop-shadow(-2px 1px 0 #10261f); }
  .fact-text { font-size: .94rem; line-height: 1.42; }
}

/* A landscape phone has to fit the score, the body, the fact and both actions in about 390px of
   height, so the character and the strip shrink rather than the card scrolling past its own
   buttons. This viewport has always needed some scroll; the point is to give back what the picture
   took rather than to pretend it costs nothing. */
@media (max-height: 520px) and (orientation: landscape) {
  /* The start screen has the same problem here and the same answer: the ladder's animals give up
     height, because they are the one band whose size is decorative rather than a sentence. */
  .ladder { --box: 54px; }
  .title-card h1 { font-size: clamp(2rem, 6vw, 2.8rem); }
  .title-actions .primary, .title-actions .ghost { min-height: 46px; }
  /* Same reason as the narrow breakpoint above: 375px of height cannot hold four teaching bands
     and the action, so the action is the one that stays on screen. */
  .title-card { display: flex; flex-direction: column; }
  .title-block { order: 1; }
  .title-actions { order: 2; }
  .ladder-panel { order: 3; }
  .howto { order: 4; }
  .bonus-row { order: 5; }
  .result-body-block { margin-top: 15px; gap: 7px; }
  .result-body { --cell: min(var(--strip-cell), 11px); }
  .result-body-line { gap: 11px; font-size: .69rem; }
  .result-body-line b { font-size: 1rem; }
  .result-body-key { flex-basis: 100%; margin-left: 0; }
  .fact-line { --fact-art: 60px; --fact-tail: 13px; margin-top: 11px; }
  .fact-bubble { padding: 7px 11px; border-width: 2px; border-radius: 13px; box-shadow: 0 4px 0 #33291033; }
  .fact-bubble::before { filter: drop-shadow(-2px 1px 0 #10261f); }
  .fact-text { font-size: .9rem; line-height: 1.38; }
}

/* ── FULL BLEED ON A PHONE ─────────────────────────────────────────────────────────────────────
   Every overlay card takes the whole screen here: no 18px gutter, no 30px radius, no border and no
   outer shadow, and `min-height: 100dvh` so a card SHORTER than the screen still paints all of it
   rather than leaving the blurred surround showing under it.

   A floating card inside a dark margin is a desktop idea — it says "there is a page behind this,
   and you can see it". On a phone there is nothing behind it worth seeing: the arena is under a
   10px blur, and the margin is screen the card has to give back in the one direction it is already
   short of. Measured before this rule, the title card was 339px wide in a 375px viewport and 1,107
   tall in 812 — already scrolling, and paying 36px of gutter for the privilege.

   The condition is WIDTH rather than the turn hint's `pointer: coarse`, deliberately: this is the
   breakpoint that already re-orders these two cards actions-first for a phone, so the full-bleed
   layout and the phone layout cannot come apart. The landscape clause is bounded at 900px so a
   short DESKTOP window — which the existing `max-height: 520px` query also matches — keeps its
   card.

   The safe-area insets move from the overlay to the card, because a card asked for `100dvh` inside
   a padded overlay overflows by exactly the padding. */
@media (max-width: 640px), (max-height: 520px) and (max-width: 900px) and (orientation: landscape) {
  .overlay { padding: 0; }
  .card { width: 100%; min-height: 100dvh; margin: 0; border: 0; border-radius: 0; box-shadow: none; }
  .title-card { padding: calc(24px + var(--safe-top)) calc(16px + var(--safe-right)) calc(22px + var(--safe-bottom)) calc(16px + var(--safe-left)); }
  .result-card { padding: calc(23px + var(--safe-top)) calc(15px + var(--safe-right)) calc(23px + var(--safe-bottom)) calc(15px + var(--safe-left)); }
  .help-card, .board-card, .debug-card { padding: calc(22px + var(--safe-top)) calc(16px + var(--safe-right)) calc(22px + var(--safe-bottom)) calc(16px + var(--safe-left)); }
  /* The help card capped itself at `86dvh` and scrolled inside its own frame, which was the answer
     to a card floating in a gutter. Full bleed, the page is the frame. */
  .help-card { max-height: none; }
  .close-button { top: calc(10px + var(--safe-top)); right: calc(12px + var(--safe-right)); }
  .top-actions { z-index: auto; }
}

/* One picture that moves on a card the player reads. Both animations come off together, and the
   resting style is already the finished pose. */
@media (prefers-reduced-motion: reduce) {
  .fact-snake { animation: none; }
  .rung img { animation: none; }
}

/* You & friends, above the global rows. A friend row IS a board row — the shared renderer draws it
   with the same cells — plus a fourth, auto-sized column for the Remove control. */
.board-friends { display: flex; flex-direction: column; gap: 6px; margin-top: 16px; padding-bottom: 10px; border-bottom: 1px dashed #10261f2b; text-align: left; }
.board-friends[hidden], .board-friends-head[hidden], .board-friends-rows[hidden] { display: none !important; }
.board-friends-head { display: flex; align-items: center; justify-content: space-between; gap: 10px; }
.board-friends-heading { margin: 0; color: var(--muted); font-size: .66rem; font-weight: 900; letter-spacing: .13em; text-transform: uppercase; }
.friends-btn { min-height: 34px; padding: 5px 12px; font-size: .8rem; }
.board-friends-rows { min-height: 0; }
.board-friends-rows .board-row { grid-template-columns: 38px 1fr auto auto; }
.board-row.friend { background: #ffbe281f; }
.board-row.unranked { opacity: .78; }
.board-row.unranked .score { color: var(--muted); font-size: .72rem; font-weight: 700; }
.friend-remove { justify-self: end; font-size: .74rem; }
.board-friends-note, .board-friends-status, .board-friends-links { margin: 0; text-align: left; }
/* The sign-in INSIDE the guest's friends line, drawn there by the shared client. A word in a
   sentence, not a chip beside one: `.text-button`'s border and padding and the 44px minimum every
   other button here carries all read as furniture glued into prose, and they break the line box
   around the words. An inline link in a block of text is the one target-size exemption. */
.friends-signin { min-width: 0; min-height: 0; border: 0; border-radius: 0; background: none; padding: 0; color: var(--accent); font: inherit; font-weight: 900; text-decoration: underline; text-underline-offset: 3px; }
/* The dare card on the title screen: the card's own small copy, with a headline in it. */
.dare-card { margin: 14px 0 0; padding: 10px 14px; border: 1px solid #a34a1040; border-radius: 12px; background: #ffbe2814; text-align: left; }
.dare-headline { display: block; margin-bottom: 3px; color: var(--ink); font-size: .95rem; font-weight: 900; }
/* Armed: the next press removes. */
.friend-remove.confirming { color: var(--orange); font-weight: 900; }
