/* reppo.games BRAND CONTRACT - the single source of truth for chrome, typography and motion
   across all 14 games (epic mreppo/reppo-games#65, phase 2).

   The problem it solves: the games look like 14 unrelated projects. Measured 24.07.2026 - nine CSS
   token naming conventions and four font strategies, so nothing was shareable even in principle.
   Recognition is meant to come from TYPE, CHROME and MOTION.

   ===========================================================================================
   SCOPE - read before editing
   ===========================================================================================
   IN scope:  typography, motion, radii, elevation, focus ring, hit targets, and the furniture
              every game reinvented (buttons, modal, toast, result card).
   OUT of scope, deliberately: GAME PALETTES. Sudoku is a light paper grid, Rallijs is a 3D
              outdoor rally - a forced shared palette makes both worse. The `--brand-*` colours
              below dress the FURNITURE only. Never point a board, field, tile, card-face or piece
              of game art at them, and never change a game's own `--rl-*` / `--rw-*` / etc.

   ===========================================================================================
   HOW THIS REACHES THE SHARED HEADER - the part that is easy to get wrong
   ===========================================================================================
   The hosted header (a custom element with Shadow DOM) reads `--font-display`, `--font-body` and
   `--font-logo`. This file defines those three names canonically, so consistent type lands
   portfolio-wide with ZERO changes to the header's own source.

   The selector list matters, and NOT for style reasons. Verified in Chrome against the real header:

     :root { --font-body: X }              -> does NOT reach the header. Its shadow stylesheet sets
                                              the property on `:host`, i.e. directly on the host
                                              element, and a direct declaration beats an inherited
                                              one. Adding `!important` does not help either.
     reppo-header { --font-body: X }       -> DOES reach it. A document-tree rule matching the host
                                              outranks the shadow tree's own `:host` rule.

   So the contract is declared on BOTH `:root` (for the game's own DOM) and `reppo-header` (for the
   shared header). Dropping the second selector silently leaves the header in its built-in fallback
   font on every game - which is the exact defect this epic exists to fix. Keep both.

   ===========================================================================================
   LATVIAN - a hard gate, enforced by a test
   ===========================================================================================
   Every one of the 22 Latvian diacritics lives ONLY in the latin-ext subset; the plain latin subset
   has zero of them. Both @font-face blocks below must ship, with these exact unicode-ranges. If you
   "optimise" to latin only, strings like `Kalnu serpentins` (long i) render in fallback glyphs
   MID-WORD and nothing else goes red. `scripts/font-coverage.test.mjs` asserts the shipped bytes
   really contain U+0122/0123/0136/0137/013B/013C/0145/0146 - do not delete it.

   No Google Fonts CDN: traffic is EU, so the font is self-hosted and there is no third-party
   request. Licence OFL. */

/* ---- Space Grotesk, self-hosted, variable (wght 300-700) --------------------------------------
   Ranges copied verbatim from Google's own stylesheet so the browser fetches latin-ext ONLY for
   pages that actually use those codepoints - which is every page in LV mode and none in EN mode. */
@font-face {
  font-family: 'Space Grotesk';
  font-style: normal;
  font-weight: 300 700;
  font-display: swap;
  src: url('/fonts/space-grotesk-latin-wght-normal.woff2') format('woff2');
  unicode-range: U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6, U+02DA, U+02DC, U+0304,
    U+0308, U+0329, U+2000-206F, U+20AC, U+2122, U+2191, U+2193, U+2212, U+2215, U+FEFF, U+FFFD;
}
@font-face {
  /* THE LATVIAN SUBSET. All 22 diacritics are here and nowhere else. */
  font-family: 'Space Grotesk';
  font-style: normal;
  font-weight: 300 700;
  font-display: swap;
  src: url('/fonts/space-grotesk-latin-ext-wght-normal.woff2') format('woff2');
  unicode-range: U+0100-02BA, U+02BD-02C5, U+02C7-02CC, U+02CE-02D7, U+02DD-02FF, U+0304, U+0308,
    U+0329, U+1D00-1DBF, U+1E00-1E9F, U+1EF2-1EFF, U+2020, U+20A0-20AB, U+20AD-20C0, U+2113,
    U+2C60-2C7F, U+A720-A7FF;
}

/* ---- the contract ----------------------------------------------------------------------------
   Theme sets appear THREE times on purpose, and the selector lists are load-bearing:
     :root, [data-theme='light'], reppo-header   the light set
     [data-theme='dark']                          the dark set
     :root:not([data-theme]) + prefers-color-scheme   the no-JS OS fallback
   `[data-theme='light']` is not redundant. Without it a subtree forced to light inside a dark page
   has nothing to re-assert and inherits the DARK values - so a game could not put a light modal on
   a dark page, and the component gallery's light-vs-dark comparison silently collapsed to dark-vs-
   dark whenever the theme cookie was dark. Both directions have to be declared. */
/* GROUP 1 - the font tokens, and the ONLY group the shared header's host is given. Keeping the
   host out of the colour groups is deliberate: the header dresses itself from its own theme
   tokens, so handing it brand colours here would make it a light-token island on a dark page, and
   would silently start mattering the day the header consumes a `--brand-*` colour. */
:root,
reppo-header {
  /* These three names are the ones the shared header consumes - see the note above on why
     `reppo-header` has to be in the selector list. A game defines them NOWHERE else. */
  --font-display: 'Space Grotesk', system-ui, -apple-system, 'Segoe UI', sans-serif;
  --font-body: 'Space Grotesk', system-ui, -apple-system, 'Segoe UI', sans-serif;
  /* The brand coin ("r.") stays Fraunces - it is a logotype, not body copy. Declared here so a
     game cannot repoint it by accident; the header loads the file itself. */
  --font-logo: 'Fraunces', Georgia, serif;
  /* Un-namespaced deliberately: `--font-mono` is not a NEW concern, it is an existing global (the
     portal's tokens.css and tanku-kauja both already define it), and splitting the four font
     tokens across two naming schemes would be worse than the collision risk - any game defining it
     wants the same thing, a mono stack. Everything genuinely new below is `--brand-*`. */
  --font-mono: ui-monospace, 'SF Mono', 'Cascadia Mono', Menlo, monospace;
}

/* GROUP 2 - theme-INDEPENDENT tokens. On `:root` only, deliberately: nothing re-declares them per
   theme, so the `prefers-reduced-motion` override further down cannot be beaten by a themed subtree
   re-asserting the normal durations. Anything whose value depends on light-vs-dark belongs in
   group 3 instead. */
:root {
  /* Type scale. Sizes are rem so a user's browser setting still scales the whole portfolio. */
  --brand-text-xs: 0.75rem;
  --brand-text-sm: 0.8125rem;
  --brand-text-md: 0.9375rem;
  --brand-text-lg: 1.125rem;
  --brand-text-xl: 1.5rem;
  --brand-text-2xl: 2rem;
  --brand-text-3xl: 2.75rem;
  --brand-leading-tight: 1.15;
  --brand-leading-body: 1.55;
  /* Space Grotesk is already fairly tight; display sizes want a touch more negative tracking. */
  --brand-tracking-display: -0.02em;
  --brand-tracking-body: 0;
  --brand-tracking-caps: 0.08em;
  --brand-weight-body: 400;
  --brand-weight-medium: 500;
  --brand-weight-bold: 700;

  /* MOTION. Two curves and four durations - deliberately few, so 14 games cannot each invent one.
     `ease-out` for anything entering or responding to a tap; `ease-in-out` for anything that moves
     and settles; `ease-emphasis` overshoots slightly, for a result card landing. */
  --brand-ease-out: cubic-bezier(0.22, 0.61, 0.36, 1);
  --brand-ease-in-out: cubic-bezier(0.45, 0.05, 0.55, 0.95);
  --brand-ease-emphasis: cubic-bezier(0.34, 1.32, 0.64, 1);
  --brand-dur-instant: 80ms;
  --brand-dur-fast: 140ms;
  --brand-dur-base: 240ms;
  --brand-dur-slow: 380ms;

  /* RADII. */
  --brand-radius-sm: 10px;
  --brand-radius-md: 14px;
  --brand-radius-lg: 20px;
  --brand-radius-xl: 26px;
  --brand-radius-pill: 999px;

  /* ELEVATION is theme-dependent, so it is declared in group 3 below alongside the colours.

     FOCUS RING. One ring everywhere, always visible against both themes.
     `--brand-focus-color` is REPEATED in each theme block below, and must stay that way: a custom
     property is substituted at computed-value time, so declaring it only here would bake in this
     block's accent and a subtree forced to the other theme would inherit that literal - measured at
     2.7:1 against the dark surface, under the 3:1 WCAG minimum for a non-text indicator. */
  --brand-focus-width: 2px;
  --brand-focus-offset: 2px;

  /* HIT TARGETS. 44px is the floor for anything a thumb has to hit (WCAG 2.5.5, Apple HIG).
     Half the portfolio's surfaces are small, so this is a real constraint, not a nicety. */
  --brand-hit-min: 44px;
  --brand-hit-min-dense: 36px;
}

/* GROUP 3 - theme-DEPENDENT tokens: the furniture colours, the elevation shadows and the focus
   colour. Declared for BOTH themes (see the note at the top of this section on why
   `[data-theme='light']` is not redundant), because a subtree forced to either theme has to be able
   to re-assert the whole set.
   CHROME COLOUR is furniture only, never game art (see SCOPE). Neutral surfaces plus one brand
   accent: the violet from the logo coin, so a primary button reads as reppo.games rather than as
   whatever blue the platform defaults to. A game may repoint --brand-accent if it must. */
:root,
[data-theme='light'] {
  --brand-ink: #14141a;
  --brand-ink-muted: rgba(24, 24, 34, 0.66);
  --brand-ink-faint: rgba(24, 24, 34, 0.42);
  --brand-surface: rgba(255, 255, 255, 0.72);
  --brand-surface-strong: #ffffff;
  --brand-surface-sunken: rgba(18, 18, 28, 0.04);
  --brand-line: rgba(16, 16, 26, 0.12);
  --brand-line-strong: rgba(16, 16, 26, 0.2);
  --brand-accent: #5b3fd6;
  --brand-accent-hover: #4c33bd;
  --brand-accent-active: #402ba1;
  --brand-accent-ink: #ffffff;
  --brand-accent-soft: rgba(91, 63, 214, 0.12);
  --brand-ok: #1f8a44;
  --brand-warn: #b26a00;
  --brand-danger: #cc2f26;
  --brand-scrim: rgba(10, 10, 16, 0.44);
  --brand-focus-color: var(--brand-accent);
  /* Elevation lives here, not in group 2: the dark block overrides it, so a light subtree needs its
     own copy or it would inherit the dark shadows. */
  --brand-shadow-1: 0 1px 2px rgba(16, 16, 24, 0.06), 0 2px 8px rgba(16, 16, 24, 0.06);
  --brand-shadow-2: 0 1px 3px rgba(16, 16, 24, 0.08), 0 8px 24px rgba(16, 16, 24, 0.1);
  --brand-shadow-3: 0 2px 6px rgba(16, 16, 24, 0.12), 0 18px 50px rgba(16, 16, 24, 0.18);
}

/* Dark. Attribute selector (not `:root[data-theme]`) on purpose: it lets any SUBTREE be forced to a
   theme, which is what the component gallery uses to show light and dark side by side. */
[data-theme='dark'] {
  --brand-ink: #f4f4f7;
  --brand-ink-muted: rgba(255, 255, 255, 0.62);
  --brand-ink-faint: rgba(255, 255, 255, 0.4);
  --brand-surface: rgba(255, 255, 255, 0.08);
  --brand-surface-strong: #16161c;
  --brand-surface-sunken: rgba(255, 255, 255, 0.05);
  --brand-line: rgba(255, 255, 255, 0.14);
  --brand-line-strong: rgba(255, 255, 255, 0.26);
  --brand-accent: #a78bfa;
  --brand-accent-hover: #b9a3fc;
  --brand-accent-active: #c9b8fd;
  --brand-accent-ink: #17111f;
  --brand-accent-soft: rgba(167, 139, 250, 0.18);
  --brand-focus-color: #a78bfa; /* see the note on --brand-focus-color above - must be re-declared */
  --brand-ok: #4ade80;
  --brand-warn: #fbbf24;
  --brand-danger: #fb7185;
  --brand-scrim: rgba(4, 4, 8, 0.62);
  --brand-shadow-1: 0 1px 2px rgba(0, 0, 0, 0.4), 0 2px 8px rgba(0, 0, 0, 0.32);
  --brand-shadow-2: 0 1px 3px rgba(0, 0, 0, 0.45), 0 10px 28px rgba(0, 0, 0, 0.45);
  --brand-shadow-3: 0 2px 8px rgba(0, 0, 0, 0.5), 0 22px 56px rgba(0, 0, 0, 0.6);
}

/* No-JS dark: the theme attribute is set by script from the shared cookie, so without JS fall back
   to the OS preference. Scoped to :root only, so an explicitly themed subtree still wins. */
@media (prefers-color-scheme: dark) {
  :root:not([data-theme]) {
    --brand-ink: #f4f4f7;
    --brand-ink-muted: rgba(255, 255, 255, 0.62);
    --brand-ink-faint: rgba(255, 255, 255, 0.4);
    --brand-surface: rgba(255, 255, 255, 0.08);
    --brand-surface-strong: #16161c;
    --brand-surface-sunken: rgba(255, 255, 255, 0.05);
    --brand-line: rgba(255, 255, 255, 0.14);
    --brand-line-strong: rgba(255, 255, 255, 0.26);
    --brand-accent: #a78bfa;
    --brand-accent-hover: #b9a3fc;
    --brand-accent-active: #c9b8fd;
    --brand-accent-ink: #17111f;
    --brand-accent-soft: rgba(167, 139, 250, 0.18);
    --brand-focus-color: #a78bfa; /* see --brand-focus-color above - must be re-declared per theme */
    --brand-ok: #4ade80;
    --brand-warn: #fbbf24;
    --brand-danger: #fb7185;
    --brand-scrim: rgba(4, 4, 8, 0.62);
    --brand-shadow-1: 0 1px 2px rgba(0, 0, 0, 0.4), 0 2px 8px rgba(0, 0, 0, 0.32);
    --brand-shadow-2: 0 1px 3px rgba(0, 0, 0, 0.45), 0 10px 28px rgba(0, 0, 0, 0.45);
    --brand-shadow-3: 0 2px 8px rgba(0, 0, 0, 0.5), 0 22px 56px rgba(0, 0, 0, 0.6);
  }
}

/* Reduced motion, once, for the whole portfolio: collapse the durations rather than ask 14 games to
   each remember a media query. Anything that reads the duration tokens becomes instant. */
@media (prefers-reduced-motion: reduce) {
  :root {
    --brand-dur-instant: 1ms;
    --brand-dur-fast: 1ms;
    --brand-dur-base: 1ms;
    --brand-dur-slow: 1ms;
  }
}

/* ADOPTING THIS FILE IS NOT ENOUGH ON ITS OWN. It defines tokens and ships the kit; it deliberately
   does NOT restyle a game's existing markup, because a contract that silently repaints `body` on
   drop-in is not safe to roll into 14 repos at once. Seven of the fourteen games hardcode a bare
   system stack, so for those, phase 3 must ALSO point the game's own root/body `font-family` at
   `var(--font-body)` - otherwise the header renders in Space Grotesk on a system-font page, which is
   the same split-typeface defect moved somewhere new. That per-game edit is a phase-3 checklist item.

   ==============================================================================================
   UI KIT - the furniture every game currently reinvents.
   Classes are `.brand-*`, matching the custom-property prefix. Self-contained: nothing here needs
   the portal's own styles.css, so a game can adopt this file alone.
   ============================================================================================== */

/* ---- buttons ---------------------------------------------------------------------------------
   EVERY :hover RULE BELOW IS WRAPPED IN `@media (hover: hover)`. Keep it that way, and wrap any
   hover state added later the same way.

   Without the guard iOS Safari leaves `:hover` applied after a tap - it sticks until the user taps
   something else - so a button holds its hover fill after every press. Most reppo.games play is on
   phones, so an unguarded hover is a live bug on the majority platform, not a desktop nicety.
   Several games had guarded their own buttons for exactly this reason (novuss `.nv-btn-primary`,
   rallijs `.rl-btn`) and CONVERTING TO THE KIT SILENTLY LOST THAT GUARD, because these rules
   replace theirs. Fixed centrally in #69 rather than per repo: this file is a verbatim copy in
   fourteen repos and the drift check pins its hash, so a local fix is a fork.

   What deliberately stays UNGUARDED, and why:
     `:active`        - the real press feedback on touch. It is the only one of the three that
                        fires on a phone, so guarding it would remove the last bit of feedback.
     `:focus-visible` - keyboard affordance; the browser already withholds it for touch taps.
     `:disabled`      - state, not input modality.

   `:active` rules are also kept AFTER their `:hover` counterpart in source order, so on a
   hover-capable device a press still beats the hover fill at equal specificity. A media query adds
   no specificity, so moving the guarded block below `:active` would silently invert that.
   ---------------------------------------------------------------------------------------------- */
.brand-btn {
  /* Layout + type. `min-height` is the hit-target floor; dense variants opt down explicitly. */
  display: inline-flex;
  align-items: center;
  justify-content: center;
  gap: 0.5rem;
  min-height: var(--brand-hit-min);
  padding: 0.5rem 1.125rem;
  border: 1px solid transparent;
  border-radius: var(--brand-radius-pill);
  font-family: var(--font-body);
  font-size: var(--brand-text-md);
  font-weight: var(--brand-weight-medium);
  line-height: 1.2;
  letter-spacing: var(--brand-tracking-body);
  text-align: center;
  text-decoration: none;
  cursor: pointer;
  -webkit-appearance: none;
  appearance: none;
  transition: background-color var(--brand-dur-fast) var(--brand-ease-out),
    border-color var(--brand-dur-fast) var(--brand-ease-out),
    color var(--brand-dur-fast) var(--brand-ease-out),
    box-shadow var(--brand-dur-fast) var(--brand-ease-out),
    transform var(--brand-dur-instant) var(--brand-ease-out);
  /* Stops a double-tap zoom eating the second tap on a mobile control. */
  touch-action: manipulation;
}

.brand-btn:focus-visible {
  outline: var(--brand-focus-width) solid var(--brand-focus-color);
  outline-offset: var(--brand-focus-offset);
}

/* Press is a 1px settle, not a scale: a scaling button on a 44px target reads as a wobble. */
.brand-btn:active:not(:disabled):not([aria-disabled='true']) {
  transform: translateY(1px);
}

.brand-btn:disabled,
.brand-btn[aria-disabled='true'] {
  cursor: not-allowed;
  opacity: 0.45;
  box-shadow: none;
  transform: none;
}

.brand-btn--primary {
  background: var(--brand-accent);
  color: var(--brand-accent-ink);
  box-shadow: var(--brand-shadow-1);
}
/* HOVER IS GUARDED, ON PURPOSE - see the note above the kit's button block. */
@media (hover: hover) {
  .brand-btn--primary:hover:not(:disabled):not([aria-disabled='true']) {
    background: var(--brand-accent-hover);
  }
}
.brand-btn--primary:active:not(:disabled):not([aria-disabled='true']) {
  background: var(--brand-accent-active);
}

.brand-btn--secondary {
  background: var(--brand-surface);
  color: var(--brand-ink);
  border-color: var(--brand-line-strong);
  box-shadow: var(--brand-shadow-1);
}
@media (hover: hover) {
  .brand-btn--secondary:hover:not(:disabled):not([aria-disabled='true']) {
    background: var(--brand-surface-strong);
    border-color: var(--brand-accent);
  }
}
.brand-btn--secondary:active:not(:disabled):not([aria-disabled='true']) {
  background: var(--brand-surface-sunken);
}

.brand-btn--ghost {
  background: transparent;
  color: var(--brand-ink-muted);
}
@media (hover: hover) {
  .brand-btn--ghost:hover:not(:disabled):not([aria-disabled='true']) {
    background: var(--brand-accent-soft);
    color: var(--brand-ink);
  }
}
.brand-btn--ghost:active:not(:disabled):not([aria-disabled='true']) {
  background: var(--brand-surface-sunken);
}

/* DESTRUCTIVE, and deliberately an OUTLINE rather than a fill (reppo-games#82).
   A filled variant would need three colours the kit does not have: --primary reads
   --brand-accent PLUS -hover, -active and -ink, and danger has exactly one token. Picking
   a plausible hover shade is how a contract stops being one, so the fill is left as an
   open Design question and this variant is built from what already exists.

   Surfaces come from the same three tokens --secondary uses, so it sits at the same
   elevation in every theme; only the ink, the border and the focus ring are semantic.
   --brand-danger is already consumed as `color` (.brand-field__error), `border-color` and
   `outline-color` (.brand-input error state), so all three uses have precedent here. */
.brand-btn--danger {
  background: var(--brand-surface);
  color: var(--brand-danger);
  border-color: var(--brand-danger);
  box-shadow: var(--brand-shadow-1);
}
@media (hover: hover) {
  .brand-btn--danger:hover:not(:disabled):not([aria-disabled='true']) {
    background: var(--brand-surface-strong);
  }
}
.brand-btn--danger:active:not(:disabled):not([aria-disabled='true']) {
  background: var(--brand-surface-sunken);
}
/* The ring follows the button, not the global accent: a green ring on a destructive
   control says the opposite of the control. Same override .brand-input's error state makes. */
.brand-btn--danger:focus-visible {
  outline-color: var(--brand-danger);
}

/* Dense: for a HUD row where 44px genuinely does not fit. Opt in, never the default. */
.brand-btn--dense {
  min-height: var(--brand-hit-min-dense);
  padding: 0.3125rem 0.75rem;
  font-size: var(--brand-text-sm);
}

/* ---- text input + field ------------------------------------------------------------------------
   THE KIT'S FIRST FORM CONTROL (rallijs#50). Until multiplayer arrived no game in the portfolio
   asked a player to type anything, so the kit shipped buttons, a modal, a toast and a result card
   and no input at all - and the first screen that needed one (rallijs' "Race online") rendered two
   raw browser inputs on a blank page. That was the kit's gap, not that game's carelessness.

   It carries two DIFFERENT shapes on purpose, because the first two consumers are not alike:
     - a DISPLAY NAME, which is free text and can be anything a person calls themselves. It gets the
       plain `.brand-input`: no transform, no tracking, nothing that fights a name.
     - a ROOM CODE, which is a short fixed alphabet the player reads off someone else's screen. That
       gets `.brand-input--code`.
   Keeping the base neutral is what lets one class serve both; a base that shouted in caps would
   make every name field wrong.

   HOVER IS GUARDED - see the long note above the button block. Same trap (#69), same reason: iOS
   Safari leaves `:hover` applied after a tap, so an unguarded hover border sticks on a field the
   player has finished with. `:focus-visible`, `:disabled` and the invalid state stay unguarded -
   none of them is about pointer capability.

   FOCUS USES `:focus-visible`, and for a text input that is not the same bet it is on a button. A
   UA always indicates focus for elements that expect text entry, so `:focus-visible` matches on a
   click and a tap as well as on Tab - which is what a form wants. The same selector on a button
   deliberately withholds the ring for a mouse press. One selector, correct in both places.
   ------------------------------------------------------------------------------------------------ */
.brand-input {
  display: block;
  /* Full width of whatever the FIELD gives it. A bare input's default width comes from its `size`
     attribute and differs per browser, so "as wide as its container" is the only predictable
     default; a consumer that wants a narrow field sizes the container, not the control. */
  width: 100%;
  /* Declared, not assumed. The file's own header promises a game can adopt it ALONE - and without a
     reset an <input> is `content-box`, so `width: 100%` plus padding and border overflows its
     container and the 44px floor becomes 62px. Every game on disk happens to ship a `*` reset, so
     this costs nothing today and makes the promise true tomorrow. */
  box-sizing: border-box;
  min-height: var(--brand-hit-min);
  padding: 0.5rem 0.875rem;
  border: 1px solid var(--brand-line-strong);
  border-radius: var(--brand-radius-md);
  /* SOLID, not the translucent `--brand-surface`. This one class ships to fourteen games whose
     backgrounds run from a paper sudoku grid to a 3D rally scene, and a glassy field would take
     the colour of whatever is behind it - which is fine for chrome and bad for something you have
     to read your own typing in. */
  background: var(--brand-surface-strong);
  color: var(--brand-ink);
  font-family: var(--font-body);
  /* NOT `--brand-text-md` alone, and this is a mobile bug rather than a taste call: iOS Safari
     zooms the viewport when a focused input computes under 16px, and the scale it applies is not
     undone when the field blurs. `--brand-text-md` is 0.9375rem = 15px at the default root, so the
     token on its own would zoom every form in the portfolio on an iPhone. `max()` keeps 16px as
     the floor while still honouring a user who has set a LARGER root size. */
  font-size: max(1rem, var(--brand-text-md));
  font-weight: var(--brand-weight-body);
  line-height: 1.4;
  letter-spacing: var(--brand-tracking-body);
  -webkit-appearance: none;
  appearance: none;
  /* No `background-color` here: the read-only and disabled fills are LAYERED backgrounds, which do
     not interpolate, so listing it would promise a transition that never happens. */
  transition: border-color var(--brand-dur-fast) var(--brand-ease-out),
    box-shadow var(--brand-dur-fast) var(--brand-ease-out);
  touch-action: manipulation;
}

.brand-input::placeholder {
  /* MUTED, NOT FAINT. Measured against `--brand-surface-strong`, `--brand-ink-faint` is 2.67:1
     (light) and 3.82:1 (dark) - both under the 4.5:1 AA floor for text, and this file holds itself
     to WCAG explicitly elsewhere. `--brand-ink-muted` measures 5.65:1 and 7.46:1 and is already the
     help text's colour two rules down, so the field reads as one family. */
  color: var(--brand-ink-muted);
  /* Some engines dim the placeholder further on top of the colour; pin it so the two themes agree. */
  opacity: 1;
}

/* HOVER IS GUARDED, ON PURPOSE - see the note above this block and above the buttons.
   It also EXCLUDES the invalid state, and that exclusion is not tidiness. `:hover:not():not()` is
   specificity (0,4,0) and `[aria-invalid='true']` is (0,2,0), so without it hover simply outranks
   invalid - and source order cannot save it, unlike the button's hover-vs-press pair. A player who
   submits a bad room code and then moves the pointer to the field that is wrong would watch the red
   border turn violet under the cursor, losing the signal exactly when it is being pointed at.

   `:user-invalid` is excluded through `:not(:is(...))`, not `:not(:user-invalid)`, and the wrapper
   is load-bearing: `:not()` is NOT forgiving, so an engine that does not know `:user-invalid` would
   throw away this whole hover rule. `:is()` is forgiving, so there the argument evaluates to
   "matches nothing" and hover simply applies - which is right, because on that engine the state
   does not exist. Same specificity either way. */
@media (hover: hover) {
  .brand-input:hover:not(:disabled):not([readonly]):not([aria-invalid='true']):not(:is(:user-invalid)) {
    border-color: var(--brand-accent);
  }
}

.brand-input:focus-visible {
  outline: var(--brand-focus-width) solid var(--brand-focus-color);
  outline-offset: var(--brand-focus-offset);
  border-color: var(--brand-accent);
}

/* INVALID IS BOUND TO `aria-invalid`, NEVER TO `:invalid`. `:invalid` matches from first paint - an
   empty `required` field is invalid before the player has typed a character - so a kit built on it
   would greet every form in red. `aria-invalid` is set by the consumer when it has actually decided
   something is wrong, and it is the attribute assistive tech reads, so the visual and the announced
   state cannot disagree. Colour is never the only signal: pair it with `.brand-field__error`. */
.brand-input[aria-invalid='true'] {
  border-color: var(--brand-danger);
}
.brand-input[aria-invalid='true']:focus-visible {
  outline-color: var(--brand-danger);
  border-color: var(--brand-danger);
}

/* `:user-invalid` is the bridge for a consumer using NATIVE constraint validation and no JS: unlike
   `:invalid` it matches only after the player has interacted, so it cannot paint an untouched field
   red.

   IN ITS OWN RULES, NEVER COMMA-JOINED TO THE `aria-invalid` ONES. A comma-separated selector list
   is NOT forgiving - one selector the engine cannot parse invalidates the ENTIRE rule - so joining
   them would make the universally-supported path contingent on support for the optional one, and an
   older Safari or Android WebView would lose invalid styling altogether. (`:is()` is the forgiving
   construct; a plain comma list is not. Same trap, same fix, as the `:autofill` pair below.) */
.brand-input:user-invalid {
  border-color: var(--brand-danger);
}
.brand-input:user-invalid:focus-visible {
  outline-color: var(--brand-danger);
  border-color: var(--brand-danger);
}

/* READONLY is not disabled: still focusable, still copyable, which is what a "here is your room
   code" field wants. It reads as inert rather than unavailable. */
/* The tint is LAYERED OVER the solid, never swapped for it. `--brand-surface-sunken` is a 4-5%
   alpha, so assigning it alone would leave the field effectively transparent - the exact glassy
   result the solid fill above exists to avoid, and worst on the background this state is FOR (a
   room code shown over a 3D rally scene). Two gradient stops of the same colour is the shortest way
   to composite a translucent tint onto an opaque base in one property. */
.brand-input[readonly] {
  background: linear-gradient(var(--brand-surface-sunken), var(--brand-surface-sunken)),
    var(--brand-surface-strong);
  /* Full-strength ink, unlike disabled: read-only content is meant to be READ and copied, and it is
     also what stops the two states looking identical. */
  color: var(--brand-ink);
  cursor: default;
}

.brand-input:disabled {
  cursor: not-allowed;
  opacity: 0.45;
  /* Matches the button's disabled treatment, so a disabled row reads as one thing. Layered over the
     solid for the same reason as read-only above. */
  background: linear-gradient(var(--brand-surface-sunken), var(--brand-surface-sunken)),
    var(--brand-surface-strong);
}

/* AUTOFILL. Chromium paints its own background and text colour over a filled field and ignores
   `background`, so without this a name field the browser remembers renders as a pale blue box with
   near-black text - unreadable in dark mode, and off-brand in both. The inset shadow is the only
   way to repaint that background; `-webkit-text-fill-color` is the only way to repaint the text.
   Named for the first consumer that will hit it: a remembered display name. */
.brand-input:-webkit-autofill {
  -webkit-text-fill-color: var(--brand-ink);
  -webkit-box-shadow: 0 0 0 100px var(--brand-surface-strong) inset;
  caret-color: var(--brand-ink);
}
/* The standard pseudo-class, in its OWN rule. Never comma-joined to the -webkit- one: an engine
   that knows only one of the two would drop the whole selector list and with it the fix. */
.brand-input:autofill {
  -webkit-text-fill-color: var(--brand-ink);
  box-shadow: 0 0 0 100px var(--brand-surface-strong) inset;
  caret-color: var(--brand-ink);
}

/* A short fixed-alphabet code read off someone else's screen: a join code, a seed, a share code.
   Uppercase and spaced so O/0 and I/1 are told apart at a glance, centred because the content is
   shorter than the field.

   NO MONOSPACE FACE. Adding one would be a fifteenth type decision for every game to ship, for a
   field a few characters long; `tabular-nums` fixes the only thing that actually jitters, which is
   digit width. `text-transform` is presentational only - the VALUE the consumer reads is unchanged,
   so uppercase it in code too if the comparison matters. */
.brand-input--code {
  text-transform: uppercase;
  letter-spacing: var(--brand-tracking-caps);
  text-align: center;
  font-variant-numeric: tabular-nums;
}
.brand-input--code::placeholder {
  /* The caps tracking pushes a placeholder off-centre otherwise. */
  letter-spacing: var(--brand-tracking-caps);
}

/* The field wrapper: label, control, then help OR error - and optionally a trailing action, which
   is the shape the first consumer actually needs (a room code with a Join button under it). Nothing
   here is required - a bare `.brand-input` is valid - but a label always is, in markup terms. */
.brand-field {
  display: flex;
  flex-direction: column;
}
/* Spaced with margins rather than `gap`, so an EMPTY error node can stay rendered at zero height
   and cost no space - see the note on `.brand-field__error` for why it must stay rendered.

   The reset comes FIRST and is not optional: a `<p>` carries a UA `margin-block: 1em`, so without
   it an empty error node still cost 12px and the "costs no space" claim was only half true. Both
   selectors are (0,1,0), so reversing these two would wipe the spacing instead. */
.brand-field > * {
  margin: 0;
}
.brand-field > * + * {
  margin-top: 0.375rem;
}

.brand-field__label {
  font-family: var(--font-body);
  font-size: var(--brand-text-sm);
  font-weight: var(--brand-weight-medium);
  line-height: var(--brand-leading-tight);
  color: var(--brand-ink);
}

.brand-field__help {
  font-family: var(--font-body);
  font-size: var(--brand-text-xs);
  line-height: var(--brand-leading-body);
  color: var(--brand-ink-muted);
}

.brand-field__error {
  font-family: var(--font-body);
  font-size: var(--brand-text-xs);
  line-height: var(--brand-leading-body);
  font-weight: var(--brand-weight-medium);
  color: var(--brand-danger);
}

/* An error node is meant to be in the DOM BEFORE it has anything to say - a live region inserted
   already-populated is commonly treated as a NEW region rather than an update, and the first message
   is dropped. So the consumer renders an empty `role="status"` node and fills it later.

   THIS IS WHY IT IS NOT `display: none`. Hiding it removes it from the accessibility tree, so while
   empty it is not a registered live region at all - and revealing it and filling it in the same
   frame recreates the exact "region appeared already populated" case above. Zeroing the margin
   instead leaves an empty <p> rendered at zero height: it costs no space and stays announced.
   (Note `:empty` is literal - `<p class="brand-field__error">\n</p>` contains a text node and does
   NOT match, so write the node with no whitespace inside it.) */
.brand-field__error:empty {
  margin-top: 0;
}

/* ---- modal dialog ---------------------------------------------------------------------------- */
/* Built on <dialog> so focus trapping, Esc and inertness come from the platform rather than from
   14 hand-rolled implementations. */
.brand-dialog {
  /* `100%`, never `100vw`: a dialog opened with showModal() sits in the top layer whose containing
     block IS the viewport, so 100% resolves to the same thing there - but when a game renders the
     same markup inline inside a narrower panel (as the gallery does, so it works with JS off), a
     `100vw` clamp overflows and any ancestor with overflow:hidden clips it SILENTLY. One value that
     is right in both placements beats a value that is right in one and invisible-wrong in the other. */
  width: min(28rem, 100%);
  padding: 0;
  border: 1px solid var(--brand-line);
  border-radius: var(--brand-radius-xl);
  background: var(--brand-surface-strong);
  color: var(--brand-ink);
  box-shadow: var(--brand-shadow-3);
  font-family: var(--font-body);
}

.brand-dialog::backdrop {
  /* Literal fallback as well as the token: ::backdrop custom-property inheritance is a moving
     target across engines, and a scrim that silently resolves to nothing is a broken modal. */
  background: var(--brand-scrim, rgba(10, 10, 16, 0.44));
  backdrop-filter: blur(2px);
  -webkit-backdrop-filter: blur(2px);
}

.brand-dialog[open] {
  animation: brand-dialog-in var(--brand-dur-base) var(--brand-ease-emphasis);
}

@keyframes brand-dialog-in {
  from {
    opacity: 0;
    transform: translateY(8px) scale(0.98);
  }
  to {
    opacity: 1;
    transform: none;
  }
}

/* `position: relative` is here, and NOT on `.brand-dialog`, on purpose (reppo-games#82).
   The UA stylesheet gives an open <dialog> `position: absolute` - that is what centres it
   in the top layer - so declaring `relative` on the dialog would override the UA and move
   the dialog itself in every consuming game. `__body` is a plain div with no UA
   positioning, so `relative` is inert here unless a consumer already absolutely-positions
   something inside it. It exists solely to anchor `__close` below. */
.brand-dialog__body {
  position: relative;
  padding: 1.5rem 1.5rem 1.25rem;
}

/* The close affordance is COMPOSED, not invented: `.brand-btn .brand-btn--ghost
   .brand-btn--dense` already supply the surface, ink, hover, press, focus ring and hit
   target. This class contributes placement and nothing else, which is why it carries no
   colour, size or type of its own. Games were each inventing their own bare `×`.

     <button class="brand-btn brand-btn--ghost brand-btn--dense brand-dialog__close"
             aria-label="Close">✕</button> */
.brand-dialog__close {
  position: absolute;
  top: 0.5rem;
  right: 0.5rem;
}

.brand-dialog__title {
  margin: 0 0 0.5rem;
  font-family: var(--font-display);
  font-size: var(--brand-text-xl);
  font-weight: var(--brand-weight-bold);
  line-height: var(--brand-leading-tight);
  letter-spacing: var(--brand-tracking-display);
}

.brand-dialog__text {
  margin: 0;
  color: var(--brand-ink-muted);
  font-size: var(--brand-text-md);
  line-height: var(--brand-leading-body);
}

.brand-dialog__actions {
  display: flex;
  flex-wrap: wrap;
  gap: 0.625rem;
  justify-content: flex-end;
  padding: 0 1.5rem 1.5rem;
}

/* ---- toast ----------------------------------------------------------------------------------- */
.brand-toast {
  display: flex;
  align-items: center;
  /* A toast WITH an action (the PWA "a new version is ready / update" case) cannot fit its text and
     its button on one line at 320px: flex items default to min-width:auto, so neither shrinks and
     the row forces itself wider than its container. Let the action drop to its own line instead. */
  flex-wrap: wrap;
  gap: 0.75rem;
  width: fit-content;
  /* Same reasoning as .brand-dialog above: `100%` is correct whether the toast is fixed to the
     viewport or sitting inside a container. */
  max-width: min(26rem, 100%);
  padding: 0.75rem 1rem;
  border: 1px solid var(--brand-line);
  border-left: 3px solid var(--brand-accent);
  border-radius: var(--brand-radius-md);
  background: var(--brand-surface-strong);
  color: var(--brand-ink);
  box-shadow: var(--brand-shadow-2);
  font-family: var(--font-body);
  font-size: var(--brand-text-sm);
  line-height: 1.4;
}

.brand-toast--ok {
  border-left-color: var(--brand-ok);
}
.brand-toast--danger {
  border-left-color: var(--brand-danger);
}

.brand-toast__text {
  margin: 0;
  /* Overrides the flex default of min-width:auto so long copy wraps instead of widening the toast. */
  min-width: 0;
  flex: 1 1 auto;
}

/* A toast is announced, so it needs no click target; a dismiss is optional and stays ghost. */
.brand-toast .brand-btn {
  margin-left: auto;
}

.brand-toast--enter {
  animation: brand-toast-in var(--brand-dur-base) var(--brand-ease-out);
}

@keyframes brand-toast-in {
  from {
    opacity: 0;
    transform: translateY(-6px);
  }
  to {
    opacity: 1;
    transform: none;
  }
}

/* ---- result card ----------------------------------------------------------------------------- */
/* The end-of-game surface: outcome, the numbers, then the actions. Every game had its own. */
.brand-result {
  display: flex;
  flex-direction: column;
  gap: 1rem;
  width: min(22rem, 100%);
  padding: 1.5rem;
  border: 1px solid var(--brand-line);
  border-radius: var(--brand-radius-xl);
  background: var(--brand-surface);
  color: var(--brand-ink);
  box-shadow: var(--brand-shadow-2);
  font-family: var(--font-body);
  /* Glass, matching the portal's surfaces. Degrades to a flat tint where unsupported. */
  backdrop-filter: blur(14px) saturate(1.3);
  -webkit-backdrop-filter: blur(14px) saturate(1.3);
}

.brand-result__eyebrow {
  margin: 0;
  color: var(--brand-ink-faint);
  font-size: var(--brand-text-xs);
  font-weight: var(--brand-weight-bold);
  letter-spacing: var(--brand-tracking-caps);
  text-transform: uppercase;
}

.brand-result__title {
  margin: 0;
  font-family: var(--font-display);
  font-size: var(--brand-text-2xl);
  font-weight: var(--brand-weight-bold);
  line-height: var(--brand-leading-tight);
  letter-spacing: var(--brand-tracking-display);
}

.brand-result__stats {
  display: grid;
  grid-template-columns: repeat(auto-fit, minmax(6rem, 1fr));
  gap: 0.75rem;
  margin: 0;
  padding: 0.875rem 0;
  border-top: 1px solid var(--brand-line);
  border-bottom: 1px solid var(--brand-line);
}

.brand-result__stat {
  display: flex;
  flex-direction: column;
  gap: 0.125rem;
}

.brand-result__stat dt {
  color: var(--brand-ink-faint);
  font-size: var(--brand-text-xs);
  font-weight: var(--brand-weight-medium);
  letter-spacing: var(--brand-tracking-caps);
  text-transform: uppercase;
}

/* Tabular numerals: a timer or a score must not reflow as the digits change. */
.brand-result__stat dd {
  margin: 0;
  font-family: var(--font-display);
  font-size: var(--brand-text-lg);
  font-weight: var(--brand-weight-bold);
  font-variant-numeric: tabular-nums;
  letter-spacing: var(--brand-tracking-display);
}

.brand-result__actions {
  display: flex;
  flex-wrap: wrap;
  gap: 0.625rem;
}

.brand-result__actions .brand-btn {
  flex: 1 1 auto;
}
/* ============================================================
   Dig Through Earth - Design Tokens (v2)
   iOS 26 Liquid Glass. Two axes:
     data-mode   = "kid" | "standard"   (type, motion, radii, tap)
     data-scheme = "light" | "dark"     (palette, glass, ink)

   Default: data-mode="kid" + data-scheme follows prefers-color-scheme.
   The no-FOUC boot script in index.html sets both attributes before this
   file loads.
   ============================================================ */

/* ============================================================
   SELF-HOSTED NUNITO (#50 PR 4, resolves #55)
   ============================================================
   Nunito is KID mode's entire typeface - display and body both. It is served
   from our own origin now; the fonts.googleapis.com stylesheet and its two
   preconnects are gone, so the page makes no third-party font request. Traffic
   is EU and this is a kids' site.

   BOTH SUBSETS ARE REQUIRED. Verified against the shipped bytes with the
   portal's cmap parser, not assumed:

     nunito-latin      comma-below 0/8   Latvian 0/22
     nunito-latin-ext  comma-below 8/8   Latvian 22/22

   So every diacritic in `Izrok cauri Zemei`, `Ģenerēt jaunu` and the whole LV
   UI lives ONLY in latin-ext. Drop it as an "optimisation" and Latvian breaks
   mid-word in fallback glyphs with nothing else going red. tests/font-coverage
   .test.js asserts the eight comma-below codepoints exactly as the portal does.

   unicode-range and font-weight are copied verbatim from Fontsource's own
   stylesheet rather than invented, so the browser fetches latin-ext only for
   pages that actually use those codepoints - every page in LV, none in EN.
   font-display: swap so text paints immediately in the fallback.
   Licence OFL - public/fonts/nunito-OFL.txt.
   ============================================================ */
@font-face {
  font-family: "Nunito";
  font-style: normal;
  font-weight: 200 1000;
  font-display: swap;
  src: url("/fonts/nunito-latin-wght-normal.woff2") format("woff2");
  unicode-range: U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6, U+02DA, U+02DC, U+0304,
    U+0308, U+0329, U+2000-206F, U+20AC, U+2122, U+2191, U+2193, U+2212, U+2215, U+FEFF, U+FFFD;
}
@font-face {
  /* THE LATVIAN SUBSET. All 22 diacritics are here and nowhere else. */
  font-family: "Nunito";
  font-style: normal;
  font-weight: 200 1000;
  font-display: swap;
  src: url("/fonts/nunito-latin-ext-wght-normal.woff2") format("woff2");
  unicode-range: U+0100-02BA, U+02BD-02C5, U+02C7-02CC, U+02CE-02D7, U+02DD-02FF, U+0304, U+0308,
    U+0329, U+1D00-1DBF, U+1E00-1E9F, U+1EF2-1EFF, U+2020, U+20A0-20AB, U+20AD-20C0, U+2113,
    U+2C60-2C7F, U+A720-A7FF;
}

:root,
[data-mode="kid"] {
  /* ---------- Radii (concentric, iOS 26 style) ---------- */
  --dte-r-xs:    8px;
  --dte-r-sm:    14px;
  --dte-r-md:    20px;
  --dte-r-lg:    28px;
  --dte-r-xl:    36px;
  --dte-r-2xl:   44px;
  --dte-r-pill:  999px;
  --dte-r-btn:   var(--dte-r-pill);   /* Kid: pill buttons */
  --dte-r-card:  var(--dte-r-lg);
  --dte-r-sheet: var(--dte-r-xl);

  /* ---------- Spacing ---------- */
  --dte-s-1:   4px;
  --dte-s-2:   8px;
  --dte-s-3:   12px;
  --dte-s-4:   16px;
  --dte-s-5:   20px;
  --dte-s-6:   24px;
  --dte-s-7:   32px;
  --dte-s-8:   40px;
  --dte-s-9:   48px;
  --dte-s-10:  64px;
  --dte-screen-pad-x: 20px;

  /* ---------- Type ---------- */
  --dte-font-display: "Nunito", "SF Pro Rounded", -apple-system, system-ui, sans-serif;
  --dte-font-body:    "Nunito", "SF Pro Text", -apple-system, system-ui, sans-serif;
  --dte-font-mono:    "SF Mono", ui-monospace, "Menlo", monospace;

  --dte-t-display:   800 44px/1.05 var(--dte-font-display);
  --dte-t-title-xl:  800 32px/1.1  var(--dte-font-display);
  --dte-t-title-lg:  800 26px/1.15 var(--dte-font-display);
  --dte-t-title-md:  700 20px/1.2  var(--dte-font-display);
  --dte-t-eyebrow:   800 12px/1.2  var(--dte-font-body);
  --dte-t-body-lg:   500 18px/1.45 var(--dte-font-body);
  --dte-t-body:      500 17px/1.45 var(--dte-font-body);
  --dte-t-body-sm:   500 15px/1.4  var(--dte-font-body);
  --dte-t-caption:   700 13px/1.3  var(--dte-font-body);
  --dte-t-button:    800 17px/1    var(--dte-font-display);

  /* ---------- Tap targets ---------- */
  --dte-tap-min:        56px;
  --dte-tap-comfortable:64px;

  /* ---------- Motion ---------- */
  --dte-motion-fast:   140ms cubic-bezier(.2,.7,.3,1);
  --dte-motion-base:   220ms cubic-bezier(.2,.7,.3,1);
  --dte-motion-slow:   360ms cubic-bezier(.2,.7,.3,1);
  --dte-motion-spring: 520ms cubic-bezier(.34, 1.56, .64, 1);
  --dte-press-scale:   .92;

  /* ---------- Accents (same hues, scheme adjusts intensity) ---------- */
  --dte-accent:    #FFB627;
  --dte-accent-2:  #FF5E5B;
  --dte-accent-3:  #00C9A8;
  --dte-accent-4:  #7C5CFF;
  --dte-accent-5:  #FF8FCF;
  --dte-ok:        #2DA34A;
  --dte-warn:      #F19200;
  --dte-error:     #E5403D;
}

[data-mode="standard"] {
  /* Tighter radii, smaller type, no-overshoot motion, smaller tap */
  --dte-r-sm:    10px;
  --dte-r-md:    16px;
  --dte-r-lg:    22px;
  --dte-r-xl:    28px;
  --dte-r-btn:   16px;                /* Std: rounded rect */
  --dte-r-card:  var(--dte-r-lg);

  /* STANDARD mode drops Inter for Space Grotesk (#50 PR 4, resolves #55).
     Inter was standard mode's entire typeface - not a "generic UI face"
     alongside Nunito - so this is a real change to that mode, and only that
     mode. Kid mode, the default, keeps Nunito and is untouched.

     Why Space Grotesk rather than self-hosting Inter: it is ALREADY loaded on
     every page for the shared header, it is a geometric grotesque which is what
     standard mode wants, it passes the same Latvian gate, and its variable
     range 300-700 covers the 400-700 standard mode used. Self-hosting Inter
     instead would have added a third family and ~85 kB of latin-ext alone to a
     kids' game for a role a present font already fills.

     var(--font-display) / var(--font-body) are the brand contract's own tokens
     from css/brand.css, referenced not re-declared - re-declaring them is what
     breaks the shared header's typography, and tests/brand-binding.test.js
     fails the build if this file ever does. */
  --dte-font-display: var(--font-display);
  --dte-font-body:    var(--font-body);

  --dte-t-display:   700 36px/1.08 var(--dte-font-display);
  --dte-t-title-xl:  700 26px/1.15 var(--dte-font-display);
  --dte-t-title-lg:  700 22px/1.2  var(--dte-font-display);
  --dte-t-title-md:  600 18px/1.25 var(--dte-font-display);
  --dte-t-eyebrow:   700 11px/1.2  var(--dte-font-body);
  --dte-t-body-lg:   400 17px/1.5  var(--dte-font-body);
  --dte-t-body:      400 15px/1.5  var(--dte-font-body);
  --dte-t-body-sm:   400 13px/1.4  var(--dte-font-body);
  --dte-t-caption:   500 12px/1.35 var(--dte-font-body);
  --dte-t-button:    600 15px/1    var(--dte-font-display);

  --dte-tap-min:        44px;
  --dte-tap-comfortable:52px;

  --dte-motion-spring: 320ms cubic-bezier(.2,.7,.3,1);
  --dte-press-scale:   .97;
  --dte-screen-pad-x:  18px;

  --dte-accent:    #E89B1A;
  --dte-accent-2:  #E04E4B;
  --dte-accent-3:  #00B294;
  --dte-accent-4:  #6A4FE0;
  --dte-accent-5:  #D87BB4;
}

/* ============================================================
   SCHEME: LIGHT (daylight, warm) - both modes
   ============================================================ */
[data-scheme="light"] {
  --dte-bg-deep:        #FFF7E8;
  --dte-bg-base:        #FFF1DE;
  --dte-bg-elevated:    #FFFFFF;
  --dte-bg-aurora-1:    #FFD4A8;
  --dte-bg-aurora-2:    #FFC9D8;
  --dte-bg-aurora-3:    #B8E0FF;
  --dte-bg-aurora-4:    #FFE085;

  --dte-ink:            #1F1208;
  --dte-ink-sec:        rgba(31, 18, 8, 0.78);
  --dte-ink-soft:       rgba(31, 18, 8, 0.66);
  --dte-ink-faint:      rgba(31, 18, 8, 0.34);
  --dte-ink-on-accent:  #1A0E00;
  --dte-ink-on-dark:    #FFFFFF;
  --dte-link:           #A65A00;

  --dte-glass-bg:        rgba(255, 255, 255, 0.55);
  --dte-glass-bg-strong: rgba(255, 255, 255, 0.72);
  --dte-glass-border:    rgba(255, 255, 255, 0.95);
  --dte-glass-inner:     inset 0 1.5px 0 rgba(255, 255, 255, 0.85),
                         inset 0 -1px 0 rgba(255, 200, 140, 0.25);
  --dte-glass-blur:      blur(24px) saturate(180%);
  --dte-glass-blur-sm:   blur(12px) saturate(160%);

  --dte-glass-bg-solid:        #FFF8EC;
  --dte-glass-bg-solid-strong: #FFFFFF;

  --dte-sh-card:    0 1px 2px rgba(120, 60, 0, .08),
                    0 12px 36px rgba(120, 60, 0, .14);
  --dte-sh-float:   0 8px 28px rgba(120, 60, 0, .18);
  --dte-sh-btn:     0 8px 20px rgba(255, 140, 30, .45);
  --dte-sh-btn-2:   0 8px 20px rgba(255, 94, 91, .40);
  --dte-sh-icon-sq: inset 0 1px 0 rgba(255, 255, 255, .55),
                    0 3px 8px rgba(120, 60, 0, .22);

  --dte-wallpaper:
    radial-gradient(70% 55% at 18% 8%,
      color-mix(in oklab, var(--dte-bg-aurora-1) 90%, transparent) 0%, transparent 65%),
    radial-gradient(60% 50% at 90% 12%,
      color-mix(in oklab, var(--dte-bg-aurora-2) 85%, transparent) 0%, transparent 65%),
    radial-gradient(70% 60% at 12% 95%,
      color-mix(in oklab, var(--dte-bg-aurora-3) 85%, transparent) 0%, transparent 70%),
    radial-gradient(60% 50% at 92% 88%,
      color-mix(in oklab, var(--dte-bg-aurora-4) 80%, transparent) 0%, transparent 70%),
    linear-gradient(180deg, var(--dte-bg-deep) 0%, var(--dte-bg-base) 100%);

  color-scheme: light;
}

/* ============================================================
   SCHEME: DARK (night sky, vibrant accents) - both modes
   ============================================================ */
[data-scheme="dark"] {
  --dte-bg-deep:        #050B1A;
  --dte-bg-base:        #0A1628;
  --dte-bg-elevated:    #142139;
  --dte-bg-aurora-1:    #1E3A6B;
  --dte-bg-aurora-2:    #4B1E5C;
  --dte-bg-aurora-3:    #0F4A52;
  --dte-bg-aurora-4:    #5C2A1E;

  --dte-ink:            #FFFFFF;
  --dte-ink-sec:        rgba(255, 255, 255, 0.82);
  --dte-ink-soft:       rgba(255, 255, 255, 0.70);
  --dte-ink-faint:      rgba(255, 255, 255, 0.40);
  --dte-ink-on-accent:  #1A0E00;
  --dte-ink-on-dark:    #FFFFFF;
  --dte-link:           var(--dte-accent);

  --dte-glass-bg:        rgba(255, 255, 255, 0.10);
  --dte-glass-bg-strong: rgba(255, 255, 255, 0.16);
  --dte-glass-border:    rgba(255, 255, 255, 0.22);
  --dte-glass-inner:     inset 0 1.5px 0 rgba(255, 255, 255, 0.45),
                         inset 0 -1px 0 rgba(255, 255, 255, 0.08);
  --dte-glass-blur:      blur(24px) saturate(180%);
  --dte-glass-blur-sm:   blur(12px) saturate(160%);

  --dte-glass-bg-solid:        #1B2A45;
  --dte-glass-bg-solid-strong: #243352;

  --dte-sh-card:    0 1px 2px rgba(0, 0, 0, .35),
                    0 14px 40px rgba(0, 0, 0, .45);
  --dte-sh-float:   0 8px 28px rgba(0, 0, 0, .5);
  --dte-sh-btn:     0 6px 18px rgba(255, 182, 39, .45);
  --dte-sh-btn-2:   0 6px 18px rgba(255, 94, 91, .4);
  --dte-sh-icon-sq: inset 0 1px 0 rgba(255, 255, 255, .35),
                    0 4px 10px rgba(0, 0, 0, .35);

  --dte-wallpaper:
    radial-gradient(60% 50% at 20% 10%,
      color-mix(in oklab, var(--dte-bg-aurora-1) 85%, transparent) 0%, transparent 70%),
    radial-gradient(55% 45% at 85% 12%,
      color-mix(in oklab, var(--dte-bg-aurora-2) 80%, transparent) 0%, transparent 70%),
    radial-gradient(70% 55% at 30% 95%,
      color-mix(in oklab, var(--dte-bg-aurora-3) 75%, transparent) 0%, transparent 70%),
    radial-gradient(45% 40% at 90% 85%,
      color-mix(in oklab, var(--dte-bg-aurora-4) 75%, transparent) 0%, transparent 70%),
    linear-gradient(180deg, var(--dte-bg-deep) 0%, var(--dte-bg-base) 100%);

  color-scheme: dark;
}

/* ============================================================
   STANDARD-MODE TONAL ADJUSTMENTS (per scheme)
   ============================================================ */
[data-mode="standard"][data-scheme="light"] {
  --dte-bg-deep:        #F4F2EE;
  --dte-bg-base:        #EEEAE3;
  --dte-bg-aurora-1:    #E5C9A8;
  --dte-bg-aurora-2:    #DCC6D2;
  --dte-bg-aurora-3:    #BFD4E5;
  --dte-bg-aurora-4:    #E8DAB2;

  --dte-ink:            #14110C;
  --dte-ink-sec:        rgba(20, 17, 12, 0.78);
  --dte-ink-soft:       rgba(20, 17, 12, 0.66);
  --dte-ink-faint:      rgba(20, 17, 12, 0.30);
  --dte-link:           #8A4A00;

  --dte-glass-bg:        rgba(255, 255, 255, 0.50);
  --dte-glass-bg-strong: rgba(255, 255, 255, 0.65);
  --dte-glass-border:    rgba(255, 255, 255, 0.80);
  --dte-glass-bg-solid:        #F8F5EF;
  --dte-glass-bg-solid-strong: #FFFFFF;

  --dte-sh-card:    0 1px 2px rgba(50, 40, 20, .06),
                    0 8px 24px rgba(50, 40, 20, .10);
  --dte-sh-btn:     0 4px 14px rgba(232, 155, 26, .32);
  --dte-sh-btn-2:   0 4px 14px rgba(224, 78, 75, .28);
}

[data-mode="standard"][data-scheme="dark"] {
  --dte-bg-deep:        #060912;
  --dte-bg-base:        #0E1622;
  --dte-bg-aurora-1:    #1A2C4A;
  --dte-bg-aurora-2:    #2E1B3F;
  --dte-bg-aurora-3:    #0E353A;
  --dte-bg-aurora-4:    #3A1E1A;

  --dte-ink-sec:        rgba(255, 255, 255, 0.78);
  --dte-ink-soft:       rgba(255, 255, 255, 0.66);

  --dte-glass-bg:        rgba(255, 255, 255, 0.07);
  --dte-glass-bg-strong: rgba(255, 255, 255, 0.12);
  --dte-glass-border:    rgba(255, 255, 255, 0.16);
  --dte-glass-bg-solid:        #182234;
  --dte-glass-bg-solid-strong: #1F2A40;

  --dte-sh-btn:     0 4px 14px rgba(232, 155, 26, .32);
  --dte-sh-btn-2:   0 4px 14px rgba(224, 78, 75, .28);
}

/* ============================================================
   ACCESSIBILITY FALLBACKS
   ============================================================ */

/* Reduced transparency: solid surfaces in every theme */
@media (prefers-reduced-transparency: reduce) {
  :root {
    --dte-glass-bg:        var(--dte-glass-bg-solid);
    --dte-glass-bg-strong: var(--dte-glass-bg-solid-strong);
    --dte-glass-blur:      none;
    --dte-glass-blur-sm:   none;
  }
}

/* Reduced motion: kill transitions + bouncy springs in every theme */
@media (prefers-reduced-motion: reduce) {
  :root {
    --dte-motion-fast:   1ms linear;
    --dte-motion-base:   1ms linear;
    --dte-motion-slow:   1ms linear;
    --dte-motion-spring: 1ms linear;
    --dte-press-scale:   1;
  }
  *, *::before, *::after {
    animation-duration: 1ms !important;
    animation-iteration-count: 1 !important;
    transition-duration: 1ms !important;
    scroll-behavior: auto !important;
  }
}

/* High-contrast: bump borders + ink in every theme */
@media (prefers-contrast: more) {
  [data-scheme="dark"] {
    --dte-glass-border: rgba(255,255,255,0.55);
    --dte-ink-sec:      rgba(255,255,255,0.95);
    --dte-ink-soft:     rgba(255,255,255,0.82);
  }
  [data-scheme="light"] {
    --dte-glass-border: rgba(30,15,0,0.40);
    --dte-ink-sec:      rgba(31,18,8,0.92);
    --dte-ink-soft:     rgba(31,18,8,0.80);
  }
}

/* ==============================================================================================
   BRAND CONTRACT BINDING (epic mreppo/reppo-games#65 phase 4, dig-through-earth#49)
   ==============================================================================================

   `css/brand.css` is a VERBATIM copy of the portal's canonical stylesheet, linked from the HTML
   BEFORE this file. It owns typography, motion, radii, elevation, focus ring and hit targets.
   This block is the seam between it and Dig's own tokens.

   SCOPE IS CHROME, TYPE AND MOTION. The palette, the aurora wallpaper, the map and globe are
   untouched: every declaration below points a `--brand-*` name at a `--dte-*` value that already
   existed, so the contract's furniture renders in DIG's colours, never the contract's violet.

   -------------------------------------------------------------------------------------------
   WHY THE SELECTORS LOOK LIKE THIS - the dig-specific trap, recorded on #49
   -------------------------------------------------------------------------------------------
   brand.css declares its colour set THREE times:

     :root, [data-theme='light']                              (0,1,0)
     [data-theme='dark']                                      (0,1,0)
     @media (prefers-color-scheme: dark) { :root:not([data-theme]) }   (0,2,0)

   Dig never sets `data-theme` - its axes are `data-mode` x `data-scheme`. So the second block can
   never match, and THE THIRD ALWAYS MATCHES ON A DARK-PREFERENCE OS, at (0,2,0), whatever Dig's
   own `data-scheme` says. A plain `:root` repoint is (0,1,0) and loses to it. The visible symptom
   is narrow and easy to miss: a player on a dark-OS machine who picks LIGHT gets light Dig chrome
   with contract-violet brand furniture sitting on it. It is invisible on a light-OS machine, which
   is why it is asserted in tests/brand-binding.test.js rather than left to the eye.

   Hence the two `:root[data-scheme=...]` blocks: (0,2,0) matches the media block's specificity,
   and this file is linked after brand.css, so source order settles the tie in Dig's favour. The
   plain `:root` block below is the base case - it beats brand.css's own `:root` light block the
   same way, and covers the instant before the boot script sets the attribute.

   -------------------------------------------------------------------------------------------
   WHY THE BODY IS REPEATED RATHER THAN DECLARED ONCE
   -------------------------------------------------------------------------------------------
   `--x: var(--y)` substitutes at COMPUTED-VALUE time, so a declaration only ever bakes in the
   value `--y` has where that declaration won. Declaring the mapping once on `:root` and trusting
   it to "follow" a themed block is the portfolio's most-repeated brand bug (sudoku, then solitaire
   and burtulis). The bodies are identical on purpose; it is the selectors that do the work.
   ============================================================================================== */

:root,
:root[data-scheme="light"],
:root[data-scheme="dark"] {
  /* Ink. Dig's scheme blocks already flip these, so pointing at them is all that is needed. */
  --brand-ink: var(--dte-ink);
  --brand-ink-muted: var(--dte-ink-sec);
  --brand-ink-faint: var(--dte-ink-faint);

  /* Surfaces map to the glass tiers. --brand-surface-sunken has no Dig equivalent - the design has
     no recessed surface - so it takes the plain glass tier rather than inventing a colour. */
  --brand-surface: var(--dte-glass-bg);
  --brand-surface-strong: var(--dte-glass-bg-strong);
  --brand-surface-sunken: var(--dte-glass-bg);

  /* Hairlines are derived from ink, NOT from --dte-glass-border. That token is
     rgba(255,255,255,0.95) in light - a white *highlight* edge for glass, which is
     right on a glass card and invisible as a divider on cream. The contract uses
     --brand-line for `.brand-btn--secondary`'s border and the result-stat dividers,
     so binding it to the glass edge would draw white-on-cream and make "strong" no
     stronger than plain. Deriving from --dte-ink keeps it in Dig's palette and flips
     with the scheme for free, because --dte-ink already does. */
  --brand-line: color-mix(in oklab, var(--dte-ink) 14%, transparent);
  --brand-line-strong: color-mix(in oklab, var(--dte-ink) 26%, transparent);

  /* Accent. --dte-accent is declared per MODE (kid #FFB627, standard #E89B1A), not per scheme, and
     var() resolves against whichever won on this element - so one mapping covers both modes. */
  --brand-accent: var(--dte-accent);
  --brand-accent-ink: var(--dte-ink-on-accent);
  /* Dig has no hover/active accent tokens; derived so a hover can never fall through to the
     contract's violet. color-mix keeps them in Dig's own hue whatever the mode sets. */
  --brand-accent-hover: color-mix(in oklab, var(--dte-accent) 88%, black);
  --brand-accent-active: color-mix(in oklab, var(--dte-accent) 78%, black);
  --brand-accent-soft: color-mix(in oklab, var(--dte-accent) 16%, transparent);

  /* Status. Dig has --dte-ok and --dte-warn; danger is the coral accent-2 it already uses for
     wrong answers, not a new red. */
  --brand-ok: var(--dte-ok);
  --brand-warn: var(--dte-warn);
  --brand-danger: var(--dte-accent-2);

  /* brand.css derives --brand-focus-color from --brand-accent in its LIGHT block but hard-codes a
     violet literal in the DARK one, so repointing the accent alone fixes light for free and leaves
     dark on brand violet. It has to be named explicitly. */
  --brand-focus-color: var(--dte-accent);

  /* Elevation. Tier 1 is the contract's 1-2px RESTING shadow and Dig has no equivalent that small
     (--dte-sh-card is a 36px card shadow), so tier 1 keeps the contract default - already
     theme-aware - rather than putting a resting control at a dialog's elevation. */
  --brand-shadow-2: var(--dte-sh-card);
  --brand-shadow-3: var(--dte-sh-float);
}
/* ===========================================================
   Dig Through Earth - components & layout (v2.1)
   iOS 26 Liquid Glass language. Variables come from tokens.css.
   Multi-screen app shell: topbar + active <section.screen> + tabbar.
   =========================================================== */

* {
  box-sizing: border-box;
}

[hidden] {
  display: none !important;
}

html {
  -webkit-text-size-adjust: 100%;
  text-size-adjust: 100%;
  /* Preflight set this on `html` too (#50 PR 3). Without it Android Chrome
     paints a 40%-opacity #33b5e5 overlay on every tap of a tab, city chip,
     method card, CTA, the mole and the install button - glaringly off-brand on
     a warm amber/cream kids' site, and invisible to any desktop check: desktop
     Chromium reports rgba(0,0,0,0.18) here, so only a real touch context shows
     the blue. Measured on a Pixel 5 context: rgba(0,0,0,0) with Tailwind,
     rgba(51,181,229,0.4) without it. */
  -webkit-tap-highlight-color: transparent;
}

html, body {
  margin: 0;
  padding: 0;
  /* Width safety: never let a runaway child cause horizontal scroll. */
  max-width: 100%;
  overflow-x: hidden;
}

html, body {
  /* Dynamic viewport unit follows Safari's collapsing address bar so the
     bottom tab bar never gets covered. svh is the small viewport fallback
     for older Safari that doesn't support dvh. */
  height: 100svh;
  height: 100dvh;
}

body {
  font: var(--dte-t-body);
  color: var(--dte-ink);
  -webkit-font-smoothing: antialiased;
  text-rendering: optimizeLegibility;
  background-color: var(--dte-bg-base);
  background-image: var(--dte-wallpaper);
  background-attachment: fixed;
  display: flex;
  flex-direction: column;
  overflow: hidden;
}

/* ---- Preflight replacements (#50 PR 3) -------------------------------------
   Dropping @tailwindcss/browser removed Tailwind's Preflight reset with it.
   Most of Preflight was already duplicated below; these FOUR were not, and the
   site was silently relying on them. Found by diffing computed styles of
   the built page against the built page WITH Tailwind - a passing build says
   nothing about a reset that quietly vanished. Full reasoning in the PR.

     padding    UA defaults returned once `* { padding: 0 }` went: .tab
                0 -> 1px 6px, the search input 0 -> 1px 2px.
     svg        Preflight set vertical-align: middle; baseline reserves
                descender space. Inert for today's flex-child icons, not for
                the first icon placed in flowing text.
     headings   every heading sets its own `font:` today, so this only stops a
                future one silently getting the UA's 1.5em bold.
     tap        -webkit-tap-highlight-color, restored on `html` above - the one
                gap a desktop diff cannot see. Caught in review, not by the
                23-probe sweep, because the property was not in the probe list.

   NOT restored: `* { margin: 0 }` (already covered for every element Dig uses)
   and `* { border: 0 solid }` (no rule sets border-width without a style, and
   the UA default `border-style: none` renders the same nothing).
   ---------------------------------------------------------------------------- */
button,
input,
select,
textarea {
  /* The whole of Preflight's form-control group, not just the padding that had
     an observable diff. `.search input` styles Dig's only <input> today, so the
     rest is inert - but restored on the same forward-looking grounds as the
     heading rule below: an <input> added outside `.search` would otherwise get
     13.33px Arial and a UA background. */
  padding: 0;
  font: inherit;
  color: inherit;
  background-color: transparent;
  border-radius: 0;
}

svg {
  vertical-align: middle;
}

h1, h2, h3, h4, h5, h6 {
  font-size: inherit;
  font-weight: inherit;
}

button {
  font: inherit;
  color: inherit;
  cursor: pointer;
}

h1, h2, h3, p, ul, ol {
  margin: 0;
}

ul, ol {
  padding: 0;
  list-style: none;
}

a {
  color: var(--dte-link);
  text-decoration-thickness: 0.1em;
  text-underline-offset: 0.18em;
  font-weight: 700;
}

a:hover { color: var(--dte-accent-2); }

:focus-visible {
  outline: 3px solid var(--dte-accent);
  outline-offset: 2px;
  border-radius: 6px;
}

.sr-only {
  position: absolute;
  width: 1px;
  height: 1px;
  padding: 0;
  margin: -1px;
  overflow: hidden;
  /* Tailwind's .sr-only added clip-path additively; the project rule only ever
     had the deprecated `clip`. Both kept - geometry and a11y-tree exposure are
     unchanged either way, but clip-path is the non-deprecated one. */
  clip: rect(0, 0, 0, 0);
  clip-path: inset(50%);
  white-space: nowrap;
  border: 0;
}

.skip-link {
  position: absolute;
  left: 12px;
  top: 12px;
  padding: 10px 14px;
  background: var(--dte-ink);
  color: var(--dte-bg-base);
  border-radius: var(--dte-r-md);
  text-decoration: none;
  font-weight: 800;
  transform: translateY(-200%);
  transition: transform var(--dte-motion-base);
  z-index: 1000;
}
.skip-link:focus { transform: translateY(0); }

/* ============================================================
   Glass primitives
   ============================================================ */
.glass,
.glass-strong {
  border: .5px solid var(--dte-glass-border);
  border-radius: var(--dte-r-card);
  backdrop-filter: var(--dte-glass-blur);
  -webkit-backdrop-filter: var(--dte-glass-blur);
  box-shadow: var(--dte-glass-inner), var(--dte-sh-card);
}
.glass        { background: var(--dte-glass-bg); }
.glass-strong { background: var(--dte-glass-bg-strong); }

/* ============================================================
   Shared reppo.games header (web component at www.reppo.games/header.js)
   ============================================================ */
/* The brand bar is the shared <reppo-header>. It renders its own appearance in
   shadow DOM (light/dark, following the reppo-theme cookie - same source as
   Dig's data-scheme). Reserve its height on the host so the layout does not
   shift when the component upgrades, and so offline (the cross-origin script is
   network-only and absent) it is a clean themed bar, not a content jump. */
reppo-header {
  display: block;
  min-height: 54px;
  background: var(--dte-bg-elevated);
  border-bottom: 1px solid var(--dte-ink-faint);
}

/* ============================================================
   Topbar (persistent across screens, full-width)
   ============================================================ */
.topbar {
  position: relative;
  z-index: 50;
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: var(--dte-s-2);
  padding: max(env(safe-area-inset-top, 0px), 10px) var(--dte-screen-pad-x) 8px;
  flex-shrink: 0;
}

.topbar__left {
  display: flex;
  align-items: center;
  min-width: 0;
  flex: 1 1 auto;
}

.wordmark {
  font: var(--dte-t-title-md);
  letter-spacing: -.02em;
  white-space: nowrap;
  overflow: hidden;
  text-overflow: ellipsis;
}
.wordmark::after {
  content: ".";
  color: var(--dte-accent);
}

.topbar__controls {
  display: flex;
  align-items: center;
  gap: 6px;
  flex-shrink: 0;
}

/* ============================================================
   Toggle groups
   ============================================================ */
.toggle {
  display: inline-flex;
  padding: 4px;
  border-radius: var(--dte-r-pill);
  background: var(--dte-glass-bg-strong);
  backdrop-filter: var(--dte-glass-blur);
  -webkit-backdrop-filter: var(--dte-glass-blur);
  border: .5px solid var(--dte-glass-border);
  box-shadow: var(--dte-glass-inner);
  gap: 2px;
}

.toggle__btn {
  display: inline-flex;
  align-items: center;
  gap: 6px;
  min-height: 36px;
  padding: 6px 12px;
  border: 0;
  background: transparent;
  color: var(--dte-ink-soft);
  font: 700 13px/1 var(--dte-font-display);
  border-radius: var(--dte-r-pill);
  transition: color var(--dte-motion-fast), background var(--dte-motion-base);
  white-space: nowrap;
}
.toggle__btn[aria-pressed="true"] {
  background: var(--dte-ink);
  color: var(--dte-bg-base);
}
.toggle__btn:hover:not([aria-pressed="true"]) { color: var(--dte-ink); }

.toggle--compact .toggle__btn {
  min-height: 32px;
  padding: 4px 10px;
  font-size: 12px;
}

/* ============================================================
   Scheme + mole buttons (theme switcher)
   ============================================================ */
.scheme-btn {
  width: 44px;
  height: 44px;
  min-width: 44px;
  border-radius: 50%;
  border: .5px solid var(--dte-glass-border);
  background: var(--dte-glass-bg-strong);
  backdrop-filter: var(--dte-glass-blur);
  -webkit-backdrop-filter: var(--dte-glass-blur);
  box-shadow: var(--dte-glass-inner), var(--dte-sh-card);
  color: var(--dte-ink);
  display: inline-flex;
  align-items: center;
  justify-content: center;
  padding: 0;
  transition: transform var(--dte-motion-base);
}
.scheme-btn:hover  { transform: scale(1.05); }
.scheme-btn:active { transform: scale(.92); }
.scheme-btn svg {
  width: 20px;
  height: 20px;
  transition: transform var(--dte-motion-spring);
}
.scheme-btn .scheme-moon { display: none; }
[data-scheme="dark"] .scheme-btn .scheme-sun  { display: none; }
[data-scheme="dark"] .scheme-btn .scheme-moon { display: block; }
.scheme-btn.swapping svg {
  animation: schemeSwap var(--dte-motion-spring) ease-out;
}
@keyframes schemeSwap {
  0%   { transform: rotate(0)      scale(1); }
  50%  { transform: rotate(180deg) scale(.6); opacity: .5; }
  100% { transform: rotate(360deg) scale(1); opacity: 1; }
}
[data-scheme="light"] .scheme-btn {
  border-color: rgba(120, 70, 20, .22);
  box-shadow: var(--dte-glass-inner), 0 2px 8px rgba(120, 60, 0, .14);
}

.mole-btn {
  position: relative;
  width: 56px;
  height: 56px;
  min-width: 56px;
  border-radius: 50%;
  border: none;
  background: transparent;
  padding: 0;
  isolation: isolate;
}
[data-mode="standard"] .mole-btn {
  width: 52px;
  height: 52px;
  min-width: 52px;
}
.mole-btn:focus-visible {
  outline: 3px solid var(--dte-accent);
  outline-offset: 4px;
}
.mole-halo {
  position: absolute;
  inset: 4px;
  border-radius: 50%;
  background: var(--dte-glass-bg-strong);
  backdrop-filter: var(--dte-glass-blur);
  -webkit-backdrop-filter: var(--dte-glass-blur);
  border: .5px solid var(--dte-glass-border);
  box-shadow: var(--dte-glass-inner), var(--dte-sh-card);
  transition: transform var(--dte-motion-spring), background var(--dte-motion-base);
  pointer-events: none;
}
.mole-svg {
  position: absolute;
  inset: 0;
  width: 100%;
  height: 100%;
  display: block;
  transition: transform var(--dte-motion-spring);
}
.mole-btn:hover  .mole-halo { transform: scale(1.06); }
.mole-btn:active .mole-svg  { transform: scale(var(--dte-press-scale)); }
.dirt-puff {
  position: absolute;
  inset: -8px;
  pointer-events: none;
  z-index: 60;
}
.dirt-puff .pt {
  position: absolute;
  left: 50%; top: 50%;
  width: 8px; height: 8px;
  border-radius: 50%;
  background: var(--dte-accent-2);
  transform: translate(-50%, -50%) scale(.6);
  opacity: 0;
}
.mole-btn.flipping .dirt-puff .pt {
  animation: puff var(--dte-motion-spring) ease-out forwards;
}
.mole-btn.flipping .mole-svg {
  animation: flip var(--dte-motion-spring) ease-out;
}
@keyframes flip {
  0%   { transform: rotateY(0)      scale(1); }
  40%  { transform: rotateY(180deg) scale(.85); }
  100% { transform: rotateY(360deg) scale(1); }
}
@keyframes puff {
  0%   { opacity: 0; transform: translate(-50%, -50%) scale(.4) translate(0,0); }
  30%  { opacity: 1; }
  100% { opacity: 0; transform: translate(-50%, -50%) scale(1.2) translate(var(--dx), var(--dy)); }
}
.mode-pill {
  position: absolute;
  bottom: -8px;
  left: 50%;
  transform: translateX(-50%);
  background: var(--dte-accent);
  color: var(--dte-ink-on-accent);
  border-radius: var(--dte-r-pill);
  font: 800 9px/1 var(--dte-font-display);
  padding: 3px 7px;
  letter-spacing: .04em;
  text-transform: uppercase;
  white-space: nowrap;
  box-shadow: 0 2px 6px rgba(0,0,0,.35);
  pointer-events: none;
}
[data-mode="standard"] .mode-pill {
  background: var(--dte-ink);
  color: var(--dte-bg-base);
}

/* ============================================================
   App shell + screens
   ============================================================ */
.app {
  position: relative;
  flex: 1 1 auto;
  overflow: hidden;
  isolation: isolate;
}

.screen {
  position: absolute;
  inset: 0;
  display: none;
  flex-direction: column;
  overflow: hidden;
}
.screen[data-active="true"] {
  display: flex;
}

.screen-scroll {
  flex: 1 1 auto;
  overflow-y: auto;
  overflow-x: hidden;
  padding: 0 var(--dte-screen-pad-x) calc(96px + env(safe-area-inset-bottom, 0px));
  scrollbar-width: none;
}
.screen-scroll::-webkit-scrollbar { display: none; }

/* Full-bleed screens (map / globe) skip the screen-scroll wrapper */
.screen--full {
  display: none;
}
.screen--full[data-active="true"] {
  display: block;
}

/* Screen title row used by result + quiz */
.screen-title {
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: var(--dte-s-3);
  padding: 4px 0 var(--dte-s-4);
}
.screen-title__heading {
  font: var(--dte-t-title-md);
  letter-spacing: -.01em;
  flex: 1 1 auto;
  text-align: center;
}
.screen-title__spacer {
  width: var(--dte-tap-min);
  height: 1px;
  flex-shrink: 0;
}

/* ============================================================
   Locator screen (hero, mini-globe, methods, search, cities)
   ============================================================ */
.hero {
  padding: var(--dte-s-4) 4px var(--dte-s-4);
}
.hero__heading {
  font: var(--dte-t-display);
  letter-spacing: -.025em;
  margin: var(--dte-s-2) 0 var(--dte-s-2);
  text-wrap: balance;
}
.hero__subtitle {
  margin-top: 4px;
  color: var(--dte-ink-sec);
}

.eyebrow {
  font: var(--dte-t-eyebrow);
  letter-spacing: .14em;
  text-transform: uppercase;
  color: var(--dte-accent);
}
.eyebrow--soft {
  color: var(--dte-ink-soft);
  margin: var(--dte-s-3) 0 var(--dte-s-2);
}

.body-lg { font: var(--dte-t-body-lg); color: var(--dte-ink-sec); }

/* Mini-globe orb */
.mini-globe {
  position: relative;
  width: 200px;
  height: 200px;
  margin: 0 auto var(--dte-s-4);
  display: flex;
  align-items: center;
  justify-content: center;
}
.mini-globe__ring {
  position: absolute;
  inset: 0;
  border-radius: 50%;
  border: 1px dashed var(--dte-ink-faint);
  animation: spinSlow 40s linear infinite;
}
@keyframes spinSlow {
  to { transform: rotate(360deg); }
}
.mini-globe__orb {
  position: relative;
  width: 152px;
  height: 152px;
  border-radius: 50%;
  background:
    radial-gradient(circle at 30% 25%, rgba(255,255,255,.4) 0%, transparent 35%),
    radial-gradient(circle at 75% 75%, var(--dte-accent-4) 0%, transparent 50%),
    radial-gradient(circle at 25% 75%, var(--dte-accent-3) 0%, transparent 45%),
    radial-gradient(circle at 80% 30%, var(--dte-accent-2) 0%, transparent 40%),
    linear-gradient(180deg, #1A3568, #0A1628);
  box-shadow:
    inset -20px -20px 50px rgba(0,0,0,.5),
    inset 10px 10px 30px rgba(255,255,255,.1),
    0 20px 50px rgba(0,0,0,.5);
}
[data-scheme="light"] .mini-globe__orb {
  box-shadow:
    inset -20px -20px 50px rgba(0,0,0,.55),
    inset 10px 10px 30px rgba(255,255,255,.15),
    0 18px 40px rgba(60, 30, 0, .25);
}
.mini-globe__pin {
  position: absolute;
  top: 32%;
  left: 28%;
  width: 12px;
  height: 12px;
  border-radius: 50%;
  background: var(--dte-accent);
  box-shadow: 0 0 0 4px rgba(255,182,39,.3), 0 0 0 8px rgba(255,182,39,.15);
  animation: pinPulse 2s ease-in-out infinite;
}
@keyframes pinPulse {
  0%, 100% { box-shadow: 0 0 0 4px rgba(255,182,39,.3), 0 0 0 8px rgba(255,182,39,.15); }
  50%      { box-shadow: 0 0 0 8px rgba(255,182,39,.2), 0 0 0 16px rgba(255,182,39,.0); }
}

/* Method cards */
.method-grid {
  display: grid;
  grid-template-columns: 1fr 1fr;
  gap: var(--dte-s-3);
  margin-bottom: var(--dte-s-4);
}
.method-card {
  display: flex;
  flex-direction: column;
  align-items: flex-start;
  gap: var(--dte-s-2);
  padding: var(--dte-s-4);
  min-height: 116px;
  border-radius: var(--dte-r-card);
  background: var(--dte-glass-bg);
  backdrop-filter: var(--dte-glass-blur);
  -webkit-backdrop-filter: var(--dte-glass-blur);
  border: 1px solid var(--dte-ink-faint);
  box-shadow: var(--dte-glass-inner);
  text-align: left;
  color: inherit;
  transition: transform var(--dte-motion-fast), background var(--dte-motion-base);
}
.method-card:hover  { background: var(--dte-glass-bg-strong); }
.method-card:active { transform: scale(.97); }
.method-card__icon {
  display: flex;
  align-items: center;
  justify-content: center;
  width: 40px;
  height: 40px;
  border-radius: 12px;
  box-shadow: var(--dte-sh-icon-sq);
  color: #fff;
}
.method-card__label {
  font: var(--dte-t-title-md);
  color: var(--dte-ink);
}
.method-card__sub {
  font: var(--dte-t-body-sm);
  color: var(--dte-ink-soft);
}

/* Search field */
.search {
  position: relative;
  /* Lift the form into its own stacking context so the absolutely-positioned
   * autocomplete dropdown paints over the city chips below, which create
   * their own stacking contexts via backdrop-filter. */
  z-index: 50;
  display: flex;
  align-items: center;
  gap: var(--dte-s-3);
  padding: 0 var(--dte-s-4);
  height: var(--dte-tap-min);
  border-radius: var(--dte-r-pill);
  background: var(--dte-glass-bg);
  backdrop-filter: var(--dte-glass-blur);
  -webkit-backdrop-filter: var(--dte-glass-blur);
  border: 1px solid var(--dte-ink-faint);
  margin-bottom: var(--dte-s-2);
}
.search input {
  flex: 1;
  border: none;
  background: transparent;
  color: var(--dte-ink);
  outline: none;
  font: var(--dte-t-body);
  height: 100%;
  min-width: 0;
}
.search input::placeholder { color: var(--dte-ink-soft); }
.search svg { color: var(--dte-ink-soft); flex-shrink: 0; }
.search__submit {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  width: 36px;
  height: 36px;
  border-radius: 50%;
  border: none;
  background: var(--dte-accent);
  color: var(--dte-ink-on-accent);
  flex-shrink: 0;
}

/* Autocomplete dropdown (Photon-powered city suggestions).
 * Uses the solid token (not the glass alpha) so suggestions stay legible
 * when the dropdown overlaps the city chips and tab bar below. */
.autocomplete__listbox {
  position: absolute;
  top: calc(100% + var(--dte-s-2));
  left: 0;
  right: 0;
  margin: 0;
  padding: var(--dte-s-2);
  list-style: none;
  z-index: 60;
  max-height: 60vh;
  overflow-y: auto;
  border-radius: var(--dte-r-lg);
  background: var(--dte-glass-bg-solid-strong);
  border: 1px solid var(--dte-glass-border);
  box-shadow: var(--dte-sh-float);
}
.autocomplete__option {
  display: flex;
  align-items: center;
  min-height: 44px;
  padding: var(--dte-s-2) var(--dte-s-4);
  border-radius: var(--dte-r-md);
  color: var(--dte-ink);
  font: var(--dte-t-body);
  cursor: pointer;
  transition: background var(--dte-motion-fast);
}
.autocomplete__option.is-active,
.autocomplete__option:hover {
  background: color-mix(in oklab, var(--dte-accent) 18%, transparent);
}
.autocomplete__option:focus-visible {
  outline: 2px solid var(--dte-accent);
  outline-offset: 2px;
}
.autocomplete__status {
  display: flex;
  align-items: center;
  gap: var(--dte-s-2);
  min-height: 44px;
  padding: var(--dte-s-2) var(--dte-s-4);
  color: var(--dte-ink-soft);
  font: var(--dte-t-body-sm);
}
.autocomplete__status--loading::before {
  content: "";
  width: 14px;
  height: 14px;
  border-radius: 50%;
  border: 2px solid var(--dte-ink-faint);
  border-top-color: var(--dte-accent);
  animation: autocomplete-spin 0.8s linear infinite;
  flex-shrink: 0;
}
@keyframes autocomplete-spin {
  to { transform: rotate(360deg); }
}
@media (prefers-reduced-motion: reduce) {
  .autocomplete__status--loading::before {
    animation: none;
  }
}

/* City chips */
.city-row {
  display: flex;
  gap: var(--dte-s-2);
  flex-wrap: wrap;
}
.city-chip {
  display: inline-flex;
  align-items: center;
  gap: 6px;
  padding: 10px 14px;
  min-height: 40px;
  border-radius: var(--dte-r-pill);
  background: var(--dte-glass-bg);
  backdrop-filter: var(--dte-glass-blur-sm);
  -webkit-backdrop-filter: var(--dte-glass-blur-sm);
  border: 1px solid var(--dte-ink-faint);
  font: 700 14px/1 var(--dte-font-display);
  color: var(--dte-ink);
  transition: transform var(--dte-motion-fast), background var(--dte-motion-base);
}
.city-chip:active { transform: scale(.95); }
.city-chip__flag { font-size: 16px; }

.locator__error {
  margin-top: var(--dte-s-3);
  color: var(--dte-error);
  font: var(--dte-t-body-sm);
  font-weight: 700;
  padding: var(--dte-s-3) var(--dte-s-4);
  border-radius: var(--dte-r-md);
  background: color-mix(in oklab, var(--dte-error) 14%, transparent);
  border: .5px solid color-mix(in oklab, var(--dte-error) 40%, transparent);
}

/* ============================================================
   Buttons
   ============================================================ */
.btn {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  gap: var(--dte-s-2);
  min-height: var(--dte-tap-min);
  padding: 0 var(--dte-s-6);
  border-radius: var(--dte-r-btn);
  border: none;
  font: var(--dte-t-button);
  color: var(--dte-ink);
  transition: transform var(--dte-motion-fast), box-shadow var(--dte-motion-base), background var(--dte-motion-base);
  will-change: transform;
  text-decoration: none;
}
.btn:active { transform: scale(var(--dte-press-scale)); }
.btn[disabled] { opacity: .6; cursor: not-allowed; }

.btn--primary {
  background: var(--dte-accent);
  color: var(--dte-ink-on-accent);
  box-shadow: var(--dte-sh-btn);
}
.btn--primary:hover:not([disabled]) {
  background: color-mix(in oklab, var(--dte-accent) 90%, white);
}

.btn--secondary {
  background: var(--dte-glass-bg-strong);
  color: var(--dte-ink);
  border: 1px solid var(--dte-ink-faint);
  backdrop-filter: var(--dte-glass-blur);
  -webkit-backdrop-filter: var(--dte-glass-blur);
  box-shadow: var(--dte-glass-inner);
}

.btn--sm {
  min-height: 36px;
  padding: 0 var(--dte-s-4);
  font-size: 13px;
}

.btn--icon {
  width: var(--dte-tap-min);
  height: var(--dte-tap-min);
  padding: 0;
  border-radius: 50%;
}

/* ============================================================
   Result screen
   ============================================================ */
.result-placeholder {
  color: var(--dte-ink-soft);
  font: var(--dte-t-body-lg);
  text-align: center;
  padding: var(--dte-s-7) var(--dte-s-3);
}

.result-body {
  display: flex;
  flex-direction: column;
  gap: var(--dte-s-4);
}

.dig-stage {
  position: relative;
  padding: var(--dte-s-3) 4px;
}
.dig-line {
  position: absolute;
  top: var(--dte-s-4);
  bottom: var(--dte-s-4);
  left: 50%;
  width: 3px;
  background: linear-gradient(180deg,
    var(--dte-accent) 0%,
    var(--dte-accent-2) 50%,
    var(--dte-accent-4) 100%);
  transform: translateX(-50%);
  border-radius: var(--dte-r-pill);
  opacity: .9;
}
.dig-line::before,
.dig-line::after {
  content: "";
  position: absolute;
  left: 50%;
  width: 18px;
  height: 18px;
  border-radius: 50%;
  transform: translateX(-50%);
}
.dig-line::before {
  top: -6px;
  background: var(--dte-accent);
  box-shadow: 0 0 0 4px rgba(255,182,39,.25);
}
.dig-line::after {
  bottom: -6px;
  background: var(--dte-accent-4);
  box-shadow: 0 0 0 4px rgba(124,92,255,.25);
}

.dig-pair {
  display: flex;
  flex-direction: column;
  gap: var(--dte-s-3);
  margin: 0;
  padding: 0;
  list-style: none;
}

.place-card {
  padding: var(--dte-s-5) var(--dte-s-5);
  border: 1px solid var(--dte-ink-faint);
}
.place-card__tag {
  display: inline-flex;
  align-items: center;
  gap: 6px;
  padding: 4px 10px;
  border-radius: var(--dte-r-pill);
  font: 800 11px/1 var(--dte-font-display);
  letter-spacing: .06em;
  text-transform: uppercase;
  margin-bottom: var(--dte-s-2);
}
.place-card__tag--you {
  background: var(--dte-accent-3);
  color: var(--dte-ink-on-accent);
}
.place-card__tag--anti {
  background: var(--dte-accent-4);
  color: #fff;
}
.place-card__name {
  font: var(--dte-t-title-md);
  letter-spacing: -.01em;
  margin-bottom: 4px;
}
.place-card__coords {
  font: var(--dte-t-caption);
  color: var(--dte-ink-soft);
  font-family: var(--dte-font-mono);
}

.stat-row {
  display: grid;
  grid-template-columns: 1fr 1fr;
  gap: var(--dte-s-3);
}
.stat-card {
  padding: var(--dte-s-3) var(--dte-s-4);
  border-radius: var(--dte-r-md);
  background: var(--dte-glass-bg);
  backdrop-filter: var(--dte-glass-blur-sm);
  -webkit-backdrop-filter: var(--dte-glass-blur-sm);
  border: 1px solid var(--dte-ink-faint);
}
.stat-card__label {
  font: var(--dte-t-eyebrow);
  letter-spacing: .14em;
  text-transform: uppercase;
  color: var(--dte-ink-soft);
  margin-bottom: 4px;
}
.stat-card__value {
  font: 800 22px/1.05 var(--dte-font-display);
  letter-spacing: -.02em;
  color: var(--dte-ink);
  font-variant-numeric: tabular-nums;
}

.fun-fact {
  display: flex;
  align-items: flex-start;
  gap: var(--dte-s-3);
  padding: var(--dte-s-4);
  border-radius: var(--dte-r-card);
  background:
    linear-gradient(135deg,
      color-mix(in oklab, var(--dte-accent-2) 22%, transparent),
      color-mix(in oklab, var(--dte-accent-4) 18%, transparent)),
    var(--dte-glass-bg);
  backdrop-filter: var(--dte-glass-blur);
  -webkit-backdrop-filter: var(--dte-glass-blur);
  border: 1px solid var(--dte-ink-faint);
  font: var(--dte-t-body);
  color: var(--dte-ink);
  font-weight: 600;
}
.fun-fact::before {
  content: "🌍";
  font-size: 28px;
  line-height: 1;
  flex-shrink: 0;
}
.fun-fact--water::before { content: "🐋"; }

.cta-row {
  display: flex;
  gap: var(--dte-s-3);
}
.cta-row__btn { flex: 1; }

/* ============================================================
   Map screen (full-bleed Leaflet canvas)
   ============================================================ */
.map-canvas {
  position: absolute;
  inset: 0;
  background: var(--dte-bg-elevated);
}
.map-canvas:focus-visible {
  outline: 3px solid var(--dte-accent);
  outline-offset: -3px;
}
.leaflet-container {
  touch-action: pan-x pan-y;
  font-family: var(--dte-font-body);
}
.leaflet-bar a,
.leaflet-touch .leaflet-bar a {
  background: var(--dte-glass-bg-strong);
  backdrop-filter: var(--dte-glass-blur-sm);
  -webkit-backdrop-filter: var(--dte-glass-blur-sm);
  color: var(--dte-ink);
  border-bottom-color: var(--dte-glass-border);
}
.leaflet-bar {
  border-radius: var(--dte-r-md);
  overflow: hidden;
  border: .5px solid var(--dte-glass-border);
  box-shadow: var(--dte-glass-inner), var(--dte-sh-card);
}
.leaflet-bar a:hover { background: var(--dte-glass-bg); }
.leaflet-control-attribution {
  background: var(--dte-glass-bg-strong);
  backdrop-filter: var(--dte-glass-blur-sm);
  -webkit-backdrop-filter: var(--dte-glass-blur-sm);
  border: .5px solid var(--dte-glass-border);
  border-radius: var(--dte-r-pill);
  color: var(--dte-ink-soft);
  font: 600 10px/1.2 var(--dte-font-body);
  padding: 4px 10px;
  margin: 6px 8px;
}
.leaflet-control-attribution a {
  color: var(--dte-ink-sec);
  font-weight: 700;
}

/* Marker bubble */
.marker-bubble {
  display: flex;
  align-items: center;
  justify-content: center;
  width: 2.5rem;
  height: 2.5rem;
  font-size: 1.6rem;
  background: var(--dte-glass-bg-solid-strong);
  border: 3px solid var(--dte-ink);
  border-radius: 50%;
  box-shadow: 0 4px 12px rgba(0, 0, 0, 0.35);
  line-height: 1;
  transform: translate(-50%, -100%);
}

/* Map peek (floating bottom card) */
.map-peek {
  position: absolute;
  left: 14px;
  right: 14px;
  bottom: calc(100px + env(safe-area-inset-bottom, 0px));
  z-index: 40;
  padding: var(--dte-s-3) var(--dte-s-4);
  display: flex;
  align-items: center;
  gap: var(--dte-s-3);
}
.map-peek__from-to {
  flex: 1;
  display: flex;
  align-items: center;
  gap: 6px;
  font: var(--dte-t-body-sm);
  flex-wrap: wrap;
}
.map-peek__from-to strong {
  font-weight: 800;
  color: var(--dte-ink);
}
.map-peek__arrow {
  color: var(--dte-accent);
  font-weight: 800;
}
.map-peek__through {
  color: var(--dte-ink-soft);
}
.map-peek__km {
  font: 800 15px/1 var(--dte-font-display);
  color: var(--dte-ink);
}

/* ============================================================
   Globe screen
   ============================================================ */
.globe-canvas {
  position: absolute;
  inset: 0;
  background:
    radial-gradient(60% 60% at 50% 50%, rgba(124, 92, 255, .18) 0%, transparent 70%),
    #050B1A;
  overflow: hidden;
}
.globe-canvas:focus-visible {
  outline: 3px solid var(--dte-accent);
  outline-offset: -3px;
}
.globe-canvas canvas { display: block; }
.globe-canvas__loading {
  position: absolute;
  inset: 0;
  display: flex;
  align-items: center;
  justify-content: center;
  margin: 0;
  font: var(--dte-t-body-lg);
  font-weight: 700;
  color: #fff;
  text-shadow: 0 1px 6px rgba(0,0,0,.8);
  pointer-events: none;
  background: linear-gradient(180deg, rgba(5,11,26,.55), rgba(5,11,26,.25));
  z-index: 30;
}

/* ============================================================
   Drilling overlay (between locator method choice + result screen)
   ============================================================ */
.drilling-overlay {
  position: absolute;
  inset: 0;
  display: flex;
  flex-direction: column;
  align-items: center;
  justify-content: center;
  gap: var(--dte-s-3);
  background: rgba(5, 11, 26, .94);
  backdrop-filter: blur(12px);
  -webkit-backdrop-filter: blur(12px);
  z-index: 200;
  pointer-events: none;
  animation: drillFade 1500ms ease forwards;
  color: #fff;
  text-align: center;
  padding: 0 var(--dte-s-6);
}

.drilling-overlay__icon {
  font-size: 4rem;
  animation: drillSpin 1400ms ease-in-out;
  text-shadow: 0 4px 18px rgba(255, 140, 30, .6);
  filter: drop-shadow(0 0 20px rgba(255, 94, 91, .5));
}

.drilling-overlay__title {
  font: var(--dte-t-title-md);
  font-weight: 800;
  color: #fff;
}
.drilling-overlay__sub {
  font: var(--dte-t-body-sm);
  color: rgba(255, 255, 255, .72);
}

@keyframes drillSpin {
  0%   { transform: translateY(-20%) rotate(0deg) scale(1); }
  50%  { transform: translateY(0%)   rotate(180deg) scale(.92); }
  100% { transform: translateY(20%)  rotate(360deg) scale(1); opacity: .55; }
}
@keyframes drillFade {
  0%, 70% { opacity: 1; }
  100%    { opacity: 0; }
}

/* ============================================================
   Quiz
   ============================================================ */
.quiz__intro {
  color: var(--dte-ink-soft);
  font: var(--dte-t-body);
  margin-bottom: var(--dte-s-3);
}

.quiz__body {
  display: flex;
  flex-direction: column;
  gap: var(--dte-s-3);
  padding: var(--dte-s-5);
  background: var(--dte-glass-bg-strong);
  backdrop-filter: var(--dte-glass-blur);
  -webkit-backdrop-filter: var(--dte-glass-blur);
  border: 1px solid var(--dte-ink-faint);
  border-radius: var(--dte-r-card);
  box-shadow: var(--dte-glass-inner), var(--dte-sh-card);
}

.quiz__progress {
  display: inline-flex;
  align-self: flex-start;
  padding: 4px 12px;
  border-radius: var(--dte-r-pill);
  background: var(--dte-accent);
  color: var(--dte-ink-on-accent);
  font: 800 12px/1 var(--dte-font-display);
  letter-spacing: .06em;
  text-transform: uppercase;
}

.quiz__question {
  font: var(--dte-t-title-md);
  letter-spacing: -.01em;
  text-wrap: pretty;
}

.quiz__options {
  display: flex;
  flex-direction: column;
  gap: var(--dte-s-2);
}

.quiz__option {
  display: flex;
  align-items: center;
  text-align: left;
  min-height: var(--dte-tap-min);
  padding: var(--dte-s-3) var(--dte-s-4);
  background: var(--dte-glass-bg);
  backdrop-filter: var(--dte-glass-blur-sm);
  -webkit-backdrop-filter: var(--dte-glass-blur-sm);
  border: 1px solid var(--dte-ink-faint);
  border-radius: var(--dte-r-md);
  color: var(--dte-ink);
  font: var(--dte-t-body);
  transition: transform var(--dte-motion-fast),
              background var(--dte-motion-base),
              border-color var(--dte-motion-base);
}
.quiz__option:hover:not(:disabled) {
  background: var(--dte-glass-bg-strong);
  border-color: var(--dte-ink-sec);
}
.quiz__option:active:not(:disabled) { transform: scale(.98); }
.quiz__option:disabled { cursor: default; }

.quiz__option--correct {
  background: color-mix(in oklab, var(--dte-ok) 28%, transparent);
  border-color: var(--dte-ok);
  color: var(--dte-ink);
  font-weight: 700;
}
.quiz__option--wrong {
  background: color-mix(in oklab, var(--dte-error) 28%, transparent);
  border-color: var(--dte-error);
  color: var(--dte-ink);
  font-weight: 700;
}

.quiz__feedback {
  min-height: 1.5rem;
  font: var(--dte-t-body);
  font-weight: 700;
  padding: var(--dte-s-3) var(--dte-s-4);
  border-radius: var(--dte-r-md);
  background: var(--dte-glass-bg);
  border: 1px solid var(--dte-ink-faint);
}
.quiz__feedback:empty { display: none; }
.quiz__feedback--correct {
  color: var(--dte-ok);
  background: color-mix(in oklab, var(--dte-ok) 14%, transparent);
  border-color: color-mix(in oklab, var(--dte-ok) 45%, transparent);
}
.quiz__feedback--wrong {
  color: var(--dte-error);
  background: color-mix(in oklab, var(--dte-error) 14%, transparent);
  border-color: color-mix(in oklab, var(--dte-error) 45%, transparent);
}

.quiz__next { align-self: stretch; }

.quiz__end {
  display: flex;
  flex-direction: column;
  gap: var(--dte-s-3);
  align-items: center;
  text-align: center;
  padding: var(--dte-s-5);
  background: var(--dte-glass-bg-strong);
  border: 1px solid var(--dte-ink-faint);
  border-radius: var(--dte-r-card);
  box-shadow: var(--dte-glass-inner), var(--dte-sh-card);
}
.quiz__end-heading {
  font: var(--dte-t-title-md);
  letter-spacing: -.01em;
}
.quiz__end-score {
  color: var(--dte-ink-soft);
  font: var(--dte-t-body);
}

/* ============================================================
   Bottom tab bar (floats above content on every screen)
   ============================================================ */
.tabbar {
  position: fixed;
  left: 16px;
  right: 16px;
  bottom: calc(env(safe-area-inset-bottom, 0px) + 14px);
  height: 64px;
  display: grid;
  grid-template-columns: repeat(4, 1fr);
  padding: 6px;
  border-radius: var(--dte-r-pill);
  background: var(--dte-glass-bg-strong);
  backdrop-filter: var(--dte-glass-blur);
  -webkit-backdrop-filter: var(--dte-glass-blur);
  border: .5px solid var(--dte-glass-border);
  box-shadow: var(--dte-glass-inner), var(--dte-sh-card);
  z-index: 100;
  max-width: 480px;
  margin: 0 auto;
}

.tab {
  display: flex;
  flex-direction: column;
  align-items: center;
  justify-content: center;
  gap: 2px;
  border: none;
  background: transparent;
  border-radius: var(--dte-r-pill);
  color: var(--dte-ink-soft);
  min-height: 52px;
  transition: color var(--dte-motion-fast), background var(--dte-motion-base);
}
.tab svg {
  width: 22px;
  height: 22px;
}
.tab__label {
  font: 700 10px/1 var(--dte-font-display);
  letter-spacing: .02em;
}
.tab[aria-current="page"] {
  color: var(--dte-ink-on-accent);
  background: var(--dte-accent);
  box-shadow: 0 4px 12px rgba(255,182,39,.4);
}
[data-mode="standard"] .tab[aria-current="page"] {
  box-shadow: 0 3px 10px rgba(232,155,26,.32);
}

/* ============================================================
   404 page
   ============================================================ */
.page-404 { overflow: auto; }
.app--404 {
  flex: 1 1 auto;
  display: flex;
  align-items: center;
  justify-content: center;
}
.notfound {
  display: flex;
  flex-direction: column;
  align-items: center;
  text-align: center;
  padding: var(--dte-s-7) var(--dte-screen-pad-x) var(--dte-s-9);
  gap: var(--dte-s-3);
  max-width: 480px;
}
.notfound__eyebrow {
  display: inline-flex;
  padding: 4px 12px;
  border-radius: var(--dte-r-pill);
  background: var(--dte-accent-2);
  color: #fff;
  font: 800 11px/1 var(--dte-font-display);
  letter-spacing: .14em;
  text-transform: uppercase;
}
.notfound__heading {
  font: var(--dte-t-title-xl);
  letter-spacing: -.02em;
}
.notfound__body {
  color: var(--dte-ink-sec);
  font: var(--dte-t-body-lg);
  max-width: 28ch;
}
.notfound__tunnel {
  position: relative;
  width: 220px;
  height: 220px;
}
.notfound__ring {
  position: absolute;
  inset: 0;
  border-radius: 50%;
  border: 2px solid var(--dte-accent-2);
  opacity: .15;
  animation: tunnelPulse 3s ease-out infinite;
}
.notfound__ring:nth-child(1) { animation-delay: 0s;   border-color: var(--dte-accent); }
.notfound__ring:nth-child(2) { animation-delay: .5s;  border-color: var(--dte-accent-2); }
.notfound__ring:nth-child(3) { animation-delay: 1s;   border-color: var(--dte-accent-4); }
.notfound__ring:nth-child(4) { animation-delay: 1.5s; border-color: var(--dte-accent-3); }
@keyframes tunnelPulse {
  0%   { opacity: 0; transform: scale(.4); }
  50%  { opacity: 1; }
  100% { opacity: 0; transform: scale(1.4); }
}
.notfound__mole {
  position: absolute;
  top: 50%;
  left: 50%;
  width: 84px;
  height: 84px;
  transform: translate(-50%, -50%);
}

/* ============================================================
   Wider screens
   ============================================================ */
@media (min-width: 640px) {
  .screen-scroll {
    padding-left: max(var(--dte-screen-pad-x), 24px);
    padding-right: max(var(--dte-screen-pad-x), 24px);
  }
  .hero {
    text-align: center;
  }
  .hero__subtitle {
    margin-left: auto;
    margin-right: auto;
    max-width: 36ch;
  }
}

@media (min-width: 720px) {
  /* Centre the column on tablet+, the tabbar is already capped. */
  .screen-scroll {
    max-width: 560px;
    margin: 0 auto;
  }
}

/* ============================================================
   PWA install UI
   - install-btn  : header icon button, glass like .scheme-btn
   - install-banner : one-shot bottom nudge on first visit
   - install-ios  : modal sheet with the iOS AtH steps
   - update-toast / installed-toast : transient status surfaces
   All consume tokens so they re-skin correctly across the 2x2 matrix.
   ============================================================ */

.install-btn {
  width: 44px;
  height: 44px;
  min-width: 44px;
  border-radius: 50%;
  border: .5px solid var(--dte-glass-border);
  background: var(--dte-glass-bg-strong);
  backdrop-filter: var(--dte-glass-blur);
  -webkit-backdrop-filter: var(--dte-glass-blur);
  box-shadow: var(--dte-glass-inner), var(--dte-sh-card);
  color: var(--dte-ink);
  display: inline-flex;
  align-items: center;
  justify-content: center;
  padding: 0;
  transition: transform var(--dte-motion-base);
}
.install-btn:hover  { transform: scale(1.05); }
.install-btn:active { transform: scale(.92); }
[data-scheme="light"] .install-btn {
  border-color: rgba(120, 70, 20, .22);
  box-shadow: var(--dte-glass-inner), 0 2px 8px rgba(120, 60, 0, .14);
}

/* ----- Install banner (first-visit nudge, floats above the tab bar) ----- */

.install-banner {
  position: fixed;
  left: 16px;
  right: 16px;
  bottom: calc(env(safe-area-inset-bottom, 0px) + 90px);
  z-index: 95;
  display: flex;
  align-items: center;
  gap: var(--dte-s-3);
  padding: var(--dte-s-3) var(--dte-s-4);
  border-radius: var(--dte-r-card);
  max-width: 480px;
  margin: 0 auto;
}
.install-banner__icon {
  flex-shrink: 0;
  width: 40px;
  height: 40px;
  display: inline-flex;
  align-items: center;
  justify-content: center;
  border-radius: 12px;
  background: var(--dte-accent);
  color: var(--dte-ink-on-accent);
  box-shadow: var(--dte-sh-icon-sq);
}
.install-banner__text { flex: 1 1 auto; min-width: 0; }
.install-banner__title {
  font: var(--dte-t-body);
  font-weight: 800;
  color: var(--dte-ink);
}
.install-banner__body {
  font: var(--dte-t-body-sm);
  color: var(--dte-ink-soft);
}
.install-banner__actions {
  display: flex;
  align-items: center;
  gap: var(--dte-s-2);
  flex-shrink: 0;
}
.install-banner__dismiss {
  width: 36px;
  height: 36px;
  border-radius: 50%;
  border: 1px solid var(--dte-ink-faint);
  background: transparent;
  color: var(--dte-ink-soft);
  display: inline-flex;
  align-items: center;
  justify-content: center;
  padding: 0;
}
.install-banner__dismiss:hover { color: var(--dte-ink); }

/* ----- iOS Add-to-Home-Screen modal ----- */

.install-ios {
  position: fixed;
  inset: 0;
  z-index: 200;
  display: flex;
  align-items: flex-end;
  justify-content: center;
  padding: 0 16px calc(env(safe-area-inset-bottom, 0px) + 24px);
}
.install-ios__backdrop {
  position: absolute;
  inset: 0;
  background: rgba(5, 11, 26, .55);
  backdrop-filter: blur(6px);
  -webkit-backdrop-filter: blur(6px);
}
.install-ios__sheet {
  position: relative;
  width: 100%;
  max-width: 440px;
  border-radius: var(--dte-r-sheet);
  padding: var(--dte-s-6) var(--dte-s-5) var(--dte-s-5);
  display: flex;
  flex-direction: column;
  gap: var(--dte-s-4);
}
.install-ios__close {
  position: absolute;
  top: 12px;
  right: 12px;
  width: 36px;
  height: 36px;
  border-radius: 50%;
  border: 1px solid var(--dte-ink-faint);
  background: transparent;
  color: var(--dte-ink-soft);
  display: inline-flex;
  align-items: center;
  justify-content: center;
  padding: 0;
}
.install-ios__close:hover { color: var(--dte-ink); }
.install-ios__title {
  font: var(--dte-t-title-md);
  letter-spacing: -.01em;
  padding-right: 44px;
}
.install-ios__steps {
  display: flex;
  flex-direction: column;
  gap: var(--dte-s-3);
}
.install-ios__step {
  display: grid;
  grid-template-columns: 28px 36px 1fr;
  align-items: center;
  gap: var(--dte-s-3);
  padding: var(--dte-s-3) var(--dte-s-4);
  border-radius: var(--dte-r-md);
  background: var(--dte-glass-bg);
  border: 1px solid var(--dte-ink-faint);
}
.install-ios__step-num {
  width: 28px;
  height: 28px;
  border-radius: 50%;
  background: var(--dte-accent);
  color: var(--dte-ink-on-accent);
  font: 800 14px/1 var(--dte-font-display);
  display: inline-flex;
  align-items: center;
  justify-content: center;
}
.install-ios__step-glyph {
  width: 36px;
  height: 36px;
  border-radius: 8px;
  background: var(--dte-glass-bg-strong);
  border: 1px solid var(--dte-ink-faint);
  color: var(--dte-ink);
  display: inline-flex;
  align-items: center;
  justify-content: center;
}
.install-ios__step-text {
  font: var(--dte-t-body);
  color: var(--dte-ink);
}

/* ----- Update + installed toasts (floating, bottom-centre) ----- */

.update-toast {
  position: fixed;
  left: 16px;
  right: 16px;
  bottom: calc(env(safe-area-inset-bottom, 0px) + 90px);
  z-index: 96;
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: var(--dte-s-3);
  padding: var(--dte-s-3) var(--dte-s-4);
  border-radius: var(--dte-r-card);
  max-width: 480px;
  margin: 0 auto;
}
.update-toast__text {
  font: var(--dte-t-body);
  font-weight: 700;
  color: var(--dte-ink);
}

.installed-toast {
  position: fixed;
  left: 16px;
  right: 16px;
  bottom: calc(env(safe-area-inset-bottom, 0px) + 90px);
  z-index: 97;
  padding: var(--dte-s-3) var(--dte-s-4);
  border-radius: var(--dte-r-pill);
  text-align: center;
  font: var(--dte-t-body);
  font-weight: 700;
  color: var(--dte-ink);
  max-width: 360px;
  margin: 0 auto;
}
