/* theme.css — the shared design-token layer.
 *
 * ONE source of truth for the platform's "bones": spacing, radii, elevation,
 * motion, and type. Colours stay where they are (each page's inline :root and
 * the server team PALETTE) — this file deliberately does NOT redefine --bg,
 * --accent, etc., so it can be linked anywhere without fighting a page's theme.
 *
 * Linked from the game-rendering documents (index.html = phone, screen.html =
 * big screen), so every game's injected <style> can reference these tokens via
 * var(--token). Adoption is incremental: new/edited styling should reach for a
 * token instead of a magic number; nothing breaks if a game keeps its own.
 *
 * SEALED BUNDLE: this file and any font under fonts/ must be listed in
 * scripts/build-site.mjs (see game-visuals skill). No external hosts.
 */

/* ---- Self-hosted faces ---------------------------------------------------
 * All committed under fonts/ and copied by scripts/build-site.mjs. Every face
 * keeps a system fallback behind it and font-display: swap, so a missing or
 * blocked woff2 degrades to the system stack rather than to nothing.
 *
 * NOTE the phone DOES now fetch a font: the body face below is the house
 * typeface on both surfaces, not a big-screen-only display face. It is one
 * Latin-subset variable file (~38 KB) and it is the only one the phone pulls
 * unless a game asks for its own (Boomtown's Rye, Cold Snap's Atkinson, …).
 */
@font-face {
  font-family: "Bricolage Grotesque";
  src: url("/fonts/bricolage-grotesque-variable.woff2") format("woff2");
  font-weight: 200 800;
  font-style: normal;
  font-display: swap;
}
@font-face {
  font-family: "Space Grotesk";
  src: url("/fonts/space-grotesk-variable.woff2") format("woff2");
  font-weight: 300 700;
  font-style: normal;
  font-display: swap;
}

/* ---- The house BODY face: Nunito, with the house WEIGHT REMAP ------------
 * The typeface for every surface's ordinary text — phone, big screen, how-tos,
 * the guided tour. Games keep their own display faces on top of it.
 *
 * WHY a specified face at all: --font-rounded used to lead with `ui-rounded`,
 * which only resolves on Apple platforms, so the same game rendered in SF Pro
 * Rounded on an iPhone, Roboto on a Pixel and Segoe UI on the venue laptop —
 * and the big screen, the one surface that must look identical every night,
 * depended on whichever machine was plugged into the projector.
 *
 * WHY THE REMAP: Nunito is measurably lighter than its peers at the same
 * nominal weight — about 100 at the light end, 75 mid, 50 heavy (ink-matched
 * against Figtree, the runner-up). Rather than re-weighting ~690 declarations
 * across 46 games, the correction lives HERE: one @font-face per requested
 * weight, each pinning a different point on the wght axis via the
 * font-variation-settings descriptor. So:
 *
 *     ask for   100 200 300 400 500 600 700 800 900
 *     you get   200 300 400 500 600 700 750 800 900
 *
 * +100 up to 600, +50 at 700, and 800/900 pass through UNTOUCHED — deliberate:
 * they land ~50 lighter than Figtree did, which is the point (900 headings
 * read as too dense in it). The mapping is strictly increasing, so no two
 * requested weights collapse onto one rendered weight.
 *
 * ⚠ EVERY weight the suite requests must be declared here, or a request falls
 * back to the nearest declared face — with only 600+ present, body text at 400
 * would render at 700. Measured usage is 100/200/300/400/500/600/700/800/900;
 * all nine are below. ADD A BLOCK before using a new weight anywhere.
 *
 * ⚠ 700 and 750 are close (~8% apart in ink). Where both sit in one component
 * the weight hierarchy is subtle — the guided tour's caption (600→700) against
 * its Skip button (700→750) is the live example. Lean on size or colour there.
 *
 * ⚠ Verified in Chromium, DOM and canvas (so it covers ctx.font too). The
 * descriptor is CSS Fonts 4; where an engine ignores it the remap silently
 * no-ops and that browser renders plain, lighter Nunito. Safe, but it is a
 * per-browser divergence — the exact fault this change exists to remove — so
 * re-check on iOS Safari after any change here.
 *
 * Nunito is OFL. Latin subset, variable, axis wght 200..1000.
 */
@font-face {
  font-family: "Nunito";
  src: url("/fonts/nunito-variable.woff2") format("woff2");
  font-weight: 100;
  font-variation-settings: "wght" 200;
  font-style: normal;
  font-display: swap;
}
@font-face {
  font-family: "Nunito";
  src: url("/fonts/nunito-variable.woff2") format("woff2");
  font-weight: 200;
  font-variation-settings: "wght" 300;
  font-style: normal;
  font-display: swap;
}
@font-face {
  font-family: "Nunito";
  src: url("/fonts/nunito-variable.woff2") format("woff2");
  font-weight: 300;
  font-variation-settings: "wght" 400;
  font-style: normal;
  font-display: swap;
}
@font-face {
  font-family: "Nunito";
  src: url("/fonts/nunito-variable.woff2") format("woff2");
  font-weight: 400;
  font-variation-settings: "wght" 500;
  font-style: normal;
  font-display: swap;
}
@font-face {
  font-family: "Nunito";
  src: url("/fonts/nunito-variable.woff2") format("woff2");
  font-weight: 500;
  font-variation-settings: "wght" 600;
  font-style: normal;
  font-display: swap;
}
@font-face {
  font-family: "Nunito";
  src: url("/fonts/nunito-variable.woff2") format("woff2");
  font-weight: 600;
  font-variation-settings: "wght" 700;
  font-style: normal;
  font-display: swap;
}
@font-face {
  font-family: "Nunito";
  src: url("/fonts/nunito-variable.woff2") format("woff2");
  font-weight: 700;
  font-variation-settings: "wght" 750;
  font-style: normal;
  font-display: swap;
}
@font-face {
  font-family: "Nunito";
  src: url("/fonts/nunito-variable.woff2") format("woff2");
  font-weight: 800;
  font-variation-settings: "wght" 800;
  font-style: normal;
  font-display: swap;
}
@font-face {
  font-family: "Nunito";
  src: url("/fonts/nunito-variable.woff2") format("woff2");
  font-weight: 900;
  font-variation-settings: "wght" 900;
  font-style: normal;
  font-display: swap;
}
/* ---- Nunito ITALIC — the same nine weights, the same remap ---------------
 * The suite asks for font-style: italic in ~70 places across 14 games, and
 * until this file carried an italic face every one of them was a synthesised
 * oblique of the upright (Derivative's placard titles, The Drop's riddle).
 * Same wght remap as above so an italic sits beside its upright at the same
 * ink. Latin subset, variable, axis wght 200..1000. */
@font-face {
  font-family: "Nunito";
  src: url("/fonts/nunito-italic-variable.woff2") format("woff2");
  font-weight: 100;
  font-variation-settings: "wght" 200;
  font-style: italic;
  font-display: swap;
}
@font-face {
  font-family: "Nunito";
  src: url("/fonts/nunito-italic-variable.woff2") format("woff2");
  font-weight: 200;
  font-variation-settings: "wght" 300;
  font-style: italic;
  font-display: swap;
}
@font-face {
  font-family: "Nunito";
  src: url("/fonts/nunito-italic-variable.woff2") format("woff2");
  font-weight: 300;
  font-variation-settings: "wght" 400;
  font-style: italic;
  font-display: swap;
}
@font-face {
  font-family: "Nunito";
  src: url("/fonts/nunito-italic-variable.woff2") format("woff2");
  font-weight: 400;
  font-variation-settings: "wght" 500;
  font-style: italic;
  font-display: swap;
}
@font-face {
  font-family: "Nunito";
  src: url("/fonts/nunito-italic-variable.woff2") format("woff2");
  font-weight: 500;
  font-variation-settings: "wght" 600;
  font-style: italic;
  font-display: swap;
}
@font-face {
  font-family: "Nunito";
  src: url("/fonts/nunito-italic-variable.woff2") format("woff2");
  font-weight: 600;
  font-variation-settings: "wght" 700;
  font-style: italic;
  font-display: swap;
}
@font-face {
  font-family: "Nunito";
  src: url("/fonts/nunito-italic-variable.woff2") format("woff2");
  font-weight: 700;
  font-variation-settings: "wght" 750;
  font-style: italic;
  font-display: swap;
}
@font-face {
  font-family: "Nunito";
  src: url("/fonts/nunito-italic-variable.woff2") format("woff2");
  font-weight: 800;
  font-variation-settings: "wght" 800;
  font-style: italic;
  font-display: swap;
}
@font-face {
  font-family: "Nunito";
  src: url("/fonts/nunito-italic-variable.woff2") format("woff2");
  font-weight: 900;
  font-variation-settings: "wght" 900;
  font-style: italic;
  font-display: swap;
}
/* ---- The house MONO face: JetBrains Mono ----------------------------------
 * Seven games spelled their own monospace stack (ui-monospace, "SF Mono",
 * Menlo, Consolas, "Courier New" …) for tapes, receipts, blueprints and
 * typewriter slips — Apple-first names that resolve differently on every
 * device, and differently between a phone and the wall at one table. One
 * self-hosted mono behind --font-mono ends that; a game that wants a
 * DIFFERENT typewriter voice still declares its own face (String Theory's
 * Special Elite). OFL. Latin subset, variable, axis wght 100..800. */
@font-face {
  font-family: "JetBrains Mono";
  src: url("/fonts/jetbrains-mono-variable.woff2") format("woff2");
  font-weight: 100 800;
  font-style: normal;
  font-display: swap;
  /* ⚠ JetBrains Mono's own line box is 1.32em (ascender 1020 / descender 300
     on a 1000 upm), against ~1.17em for the system monos it stands in for
     (Menlo, Consolas, SF Mono, DejaVu). Left alone, every mono line grew ~13%
     taller at `line-height: normal` and Tiny Bot's campaign slide overflowed
     the viewport top and bottom. These descriptors pin the LAYOUT metrics to
     the system-mono line box; glyph rendering is untouched. */
  ascent-override: 92%;
  descent-override: 25%;
  line-gap-override: 0%;
}

/* ---- The house TEXT SERIF: Newsreader -------------------------------------
 * Three games are set in printed matter and each spelled `Georgia, serif` for
 * its body copy: Cross Swords' newsprint columns and clue list, The Mandelbrot
 * Express's 1845 survey sheet, Double Dutch Auction's 1637 ledger. Georgia is
 * absent from Android and most Linux, so those surfaces resolved to whatever
 * serif the device had — DejaVu on the venue laptop, Liberation elsewhere, a
 * ±20% swing in set width between them. One self-hosted text serif behind
 * --font-serif ends that, the same job --font-mono did for the seven
 * hand-spelled monospace stacks. A game wanting a DIFFERENT serif voice still
 * declares its own face (Cross Swords' blackletter nameplate, Marcellus).
 *
 * Newsreader (OFL) is designed for continuous on-screen reading in
 * content-rich environments, which is the hardest of the three briefs; its
 * italic slants a real -17deg, which matters because Double Dutch Auction's
 * ledger italic is doing semantic work beside upright prices, and it carries
 * `tnum` for the price columns.
 *
 * ⚠ INSTANCED AT opsz 10, and the pinning is mandatory rather than cosmetic.
 * Google's file defaults to opsz 18, where the x-height is 426/1000 — BELOW
 * the Georgia (481) it replaces, so dropping it in unpinned is a legibility
 * REGRESSION on the one surface that reads at 11px. Pinned at 10 it is 483,
 * within half a percent of Georgia, and instancing the axis away also takes
 * roman+italic from 272 KiB to 88, inside the house norm.
 *
 * ⚠ IT IS NOT A METRIC DROP-IN. A small optical size is drawn wider on
 * purpose — that is what buys the legibility — so at a matched x-height the
 * set width runs ~17% above Georgia's (n 648 vs 555 per 1000em). The
 * x-height and the width cannot both be matched: opsz 18 matches the width
 * and loses 11% of the x-height. Legibility won, so any FIXED box that used
 * to hold a line of Georgia needs re-checking rather than assuming.
 */
@font-face {
  font-family: "Newsreader";
  src: url("/fonts/newsreader-variable.woff2") format("woff2");
  font-weight: 400 800;
  font-style: normal;
  font-display: swap;
}
@font-face {
  font-family: "Newsreader";
  src: url("/fonts/newsreader-italic-variable.woff2") format("woff2");
  font-weight: 400 800;
  font-style: italic;
  font-display: swap;
}

:root {
  /* ---- The browser's own chrome -------------------------------------------
   * iOS Safari lays a `position: fixed` element out against the LARGE viewport
   * — the one you get with the toolbars collapsed — so `inset: 0` puts the
   * bottom of a full-screen surface UNDERNEATH the browser's toolbar, and
   * anything anchored `bottom: 0` lands under it entirely. On a phone whose
   * Safari keeps that toolbar on screen (the bottom tab-bar layout) that is
   * where a game's primary button, its bottom sheet and its toasts live: a
   * 2026-08 Cold Snap playtest had two phones in five unable to press FREEZE
   * IT at all, and the logs show them aiming at cells for a whole round
   * without ever landing a word.
   *
   * Two fixes, and which one you want depends on how the thing is anchored:
   *   · a surface that FILLS the screen sizes itself `height: 100dvh` (with a
   *     `100vh` line before it as the fallback) rather than trusting `inset:0`;
   *   · a surface anchored to the BOTTOM lifts itself by this token.
   * It is 0 wherever the chrome is not overlaying anything — desktop, a
   * standalone/PWA launch, Android with the bar hidden — so it costs nothing
   * on the surfaces that never had the problem.
   */
  --chrome-bottom: 0px;

  /* ---- Type --------------------------------------------------------------
   * Reference these instead of pasting the stacks. The display/numeric tokens
   * put the custom face FIRST and fall back to the system stack, so they're
   * safe to use before the woff2 files exist.
   */
  /* THE one line to change to swap the house body face. The system stacks stay
   * behind it, so a missing/blocked woff2 degrades to the pre-Nunito render.
   * NOTE a different face would ALSO want its own weight remap (or none) —
   * the mapping above is calibrated to Nunito's axis, not a universal fix. */
  --font-body: "Nunito";
  --font-ui: var(--font-body), -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, "Helvetica Neue", Arial, sans-serif;
  --font-rounded: var(--font-body), ui-rounded, "SF Pro Rounded", system-ui, -apple-system, "Segoe UI", Roboto, sans-serif;
  --font-display: "Bricolage Grotesque", var(--font-rounded);   /* big-screen headers / title cards */
  --font-numeric: "Space Grotesk", var(--font-ui);              /* scores, clocks, leaderboards */
  --font-mono: "JetBrains Mono", ui-monospace, SFMono-Regular, Menlo, Consolas, "Liberation Mono", monospace; /* tapes, receipts, blueprints, code */
  --font-serif: "Newsreader", Georgia, "Times New Roman", serif;   /* printed matter: newsprint, survey sheets, ledgers */

  --track-tight: -0.02em;   /* big headings / clock */
  --track-wide: 0.14em;     /* uppercase labels / pills */

  /* ---- Spacing (4px base) ----------------------------------------------- */
  --sp-1: 4px;
  --sp-2: 8px;
  --sp-3: 12px;
  --sp-4: 16px;
  --sp-5: 24px;
  --sp-6: 32px;
  --sp-8: 48px;

  /* ---- Radii (codify what's already in use: 8/12/16/24 + pill) ---------- */
  --r-xs: 4px;
  --r-sm: 8px;
  --r-md: 12px;
  --r-lg: 16px;
  --r-xl: 24px;
  --r-pill: 999px;
  /* Two common off-scale radii, absorbed as-is (no re-rounding) so existing
     UI keeps its exact corners while still flowing from a token. */
  --r-10: 10px;
  --r-14: 14px;

  /* ---- Elevation (on the light shell) ----------------------------------- */
  --shadow-1: 0 2px 8px rgba(15, 23, 42, 0.06);
  --shadow-2: 0 10px 36px rgba(15, 23, 42, 0.07);   /* the existing card shadow */
  --shadow-3: 0 20px 60px rgba(15, 23, 42, 0.12);   /* modal / hero lift */

  /* ---- Motion ------------------------------------------------------------
   * A shared easing vocabulary is what makes 27 games feel like one product.
   * Keep reveal/deck timing deterministic off slideStartedAt (see the
   * game-visuals skill) — these curves are for UI transitions, not the
   * replay clock.
   */
  --ease-out: cubic-bezier(0.22, 1, 0.36, 1);      /* settle — default for enters */
  --ease-spring: cubic-bezier(0.34, 1.56, 0.64, 1);/* playful overshoot — chips, pops */
  --ease-in-out: cubic-bezier(0.65, 0, 0.35, 1);   /* symmetric moves */
  --dur-fast: 140ms;
  --dur-mid: 260ms;
  --dur-slow: 480ms;
}

@media (prefers-reduced-motion: reduce) {
  :root { --dur-fast: 0ms; --dur-mid: 0ms; --dur-slow: 0ms; }
}

/* The measurement behind --chrome-bottom. lvh is the viewport with the chrome
   collapsed (what a fixed element is laid out against); dvh is what you can
   actually see right now. The difference IS the overlaying chrome, and the two
   units shipped together, so one @supports covers both. */
@supports (height: 100dvh) {
  :root { --chrome-bottom: calc(100lvh - 100dvh); }
}

/* Free-text game inputs use type=search so Chrome's Android autofill accessory
   bar (the key/card/location strip that ignores autocomplete=off) is skipped.
   Strip the default search chrome (rounded box, clear-x) so they look and
   behave like the plain text inputs they replace. */
input[type=search] { -webkit-appearance: none; appearance: none; }
input[type=search]::-webkit-search-cancel-button,
input[type=search]::-webkit-search-decoration { -webkit-appearance: none; display: none; }
