/* ============================================================================
   BetWizard — Shared Design Tokens (single source of truth)
   ----------------------------------------------------------------------------
   Served from repo root at /betwizard-tokens.css and linked once from
   base.html, so every page that extends base inherits these variables.

   Rules (see .interface-design/system.md):
     - Do NOT redefine these :root tokens per page.
     - Do NOT hardcode palette hexes — reference the variables.
     - Cyan = action.  Green = wins/positive ONLY.  Red = live + losses.
       Amber = warnings/pending.  Gold = MASTER tier only.

   This file only declares variables; it applies no styles to elements, so it
   is safe to load globally (including on public/marketing pages, which simply
   ignore the variables they don't reference).

   ── Light mode ──────────────────────────────────────────────────────────────
   Every token below has a light counterpart in the `html[data-theme="light"]`
   block at the bottom of this file. The attribute selector is deliberately
   more specific than `:root` (0,1,1 vs 0,1,0), so it overrides the page-local
   `:root` blocks a few pages still ship — no matter the source order. That is
   what lets one file re-theme the whole site.

   The rule for page CSS is therefore stricter than before: a colour written as
   a literal hex cannot follow the theme. Reference the tokens.
   ============================================================================ */

:root {
  color-scheme: dark;

  /* ── Canvas & surfaces ─────────────────────────────────────────────────
     Three literal surfaces rather than one canvas plus translucency. The app
     shipped a flat navy that every card had to tint its way off; depth now
     comes from the surfaces themselves, the way iOS layers a grouped list
     over a system background:

       --bg       the canvas, the darkest thing on screen
       --surface  an elevated card, one step up, drawn with --surface-border
                  and --shadow-elev
       --bg3      a panel nested inside a card, one step up again           */
  --bg:            #0B0E14;   /* page canvas — the SAME in both themes, see below */
  --bg2:           #141923;   /* elevated surface — cards */
  --bg3:           #1A2130;   /* raised surface — nested panels */
  --glass-bg:      rgba(255, 255, 255, 0.035);
  --glass-bg2:     rgba(255, 255, 255, 0.06);
  --glass-border:  rgba(255, 255, 255, 0.08);
  --glass-border2: rgba(255, 255, 255, 0.12);

  /* The elevated card, named by role so a card does not have to know which
     hex its theme uses. `--surface` is opaque where `--glass-bg` is a tint:
     a card that has to read as *raised* cannot be 3.5% white over whatever
     happens to be behind it. */
  --surface:        #141923;
  --surface-2:      #1A2130;
  --surface-border: rgba(255, 255, 255, 0.08);
  --shadow-elev:    0 4px 12px rgba(0, 0, 0, 0.4);

  /* ── Research surfaces (2026-09 tab redesign) ──────────────────────────
     The four tabs' cards separate what you READ from what you TAP by depth
     alone, so a card needs two more steps than the three above:

       --inset    a read-only panel sunk INTO a card, one step darker than
                  it: stat tiles, "your props", a rule summary. Tappable
                  things sit on --surface-2 instead (one step lighter), so
                  "recessed = information, raised = control" holds on every
                  card without a border or an icon to say so.
       --track    an empty bar: the rail a progress bar fills, and the short
                  bar a MISS draws beside a hit on a game log.
       --seg-on   the lit segment of a segmented control. The same grey as
                  the track in the dark theme; in the light theme a lit
                  segment is white on a tinted track, which a bar track must
                  not be — hence two names for one dark value.

     Used by static/css/bw-kit.css. */
  --inset:          #0F131B;
  --track:          #262E3D;
  --seg-on:         #262E3D;

  /* ── Semantic accents ──────────────────────────────────────────────── */
  --cyan:  #00F2FE;   /* action / navigation / links / active nav */
  --green: #10B981;   /* WINS / +EV / positive performance ONLY */
  --red:   #ff4d4d;   /* LIVE + losses / urgency */
  --amber: #FFB800;   /* high confidence / edges / pending */
  --gold:  #ffc832;   /* MASTER tier elements ONLY */
  --cyan-dim:  rgba(0, 242, 254, 0.12);
  --cyan-mid:  rgba(0, 242, 254, 0.22);
  --cyan-line: rgba(0, 242, 254, 0.35);   /* accent hairlines / rings */
  --green-dim: rgba(16, 185, 129, 0.12);
  --green-line: rgba(16, 185, 129, 0.35); /* green hairlines / tag borders */
  --amber-dim: rgba(255, 184, 0, 0.12);
  --amber-line: rgba(255, 184, 0, 0.35);  /* amber hairlines / tag borders */
  --gold-dim:  rgba(255, 200, 50, 0.12);  /* MASTER chips only */
  --gold-line: rgba(255, 200, 50, 0.35);
  --red-dim:   rgba(255, 82, 82, 0.10);
  --red-line:  rgba(255, 77, 77, 0.35);   /* red hairlines / tag borders */
  --grad:      linear-gradient(135deg, #00F2FE, #10B981);

  /* ── Text ──────────────────────────────────────────────────────────── */
  --text:   #e8edf2;
  --text2:  #c8d0da;
  --muted:  #7d8a9b;   /* >= 4.5:1 on --bg — WCAG AA */
  --muted2: #8a96a4;
  /* The strongest text on the surface. Dark mode's "pure white heading" —
     in light mode it has to invert, so headings written as `#fff` must use
     this instead. */
  --text-strong: #ffffff;
  /* Text/icons sitting ON a filled accent (cyan button, gradient pill,
     avatar disc). Dark mode fills are bright, so this is near-black; light
     mode fills are deep, so it flips to white. */
  --on-accent: #06131b;

  /* ── ON THE CANVAS ─────────────────────────────────────────────────────
     Owner ruling (2026-08b): **the canvas does not flip.** The navy fill,
     the ambient field and the film grain are the same in both themes — only
     cards and their contents change colour. `--bg` above is therefore the
     one surface token with a single value for the whole app, and light mode
     does not override it.

     Which splits the text tokens in two, because a colour cannot be right on
     both a white card and a navy canvas:

       --text / --text2 / --muted / --muted2 / --text-strong
           ink for whatever surface the theme is painting — dark ink in light
           mode, because in light mode those surfaces are white cards.
       --on-canvas / --on-canvas-2 / --on-canvas-muted / --on-canvas-strong
           ink for text drawn straight onto the canvas, with no card under
           it: a page title, a section label, a footer, a bare paragraph.
           Declared here and NEVER overridden in the light block, so they
           hold in both themes exactly as the canvas does.

     Same split for the one accent that lands on the canvas often enough to
     need it (links and active nav in a page footer or hero) and for the
     hairline that separates a canvas-level band from the canvas.

     Picking between them is a question about the surface, not the theme:
     is there a card under this text? Card → --text*. Canvas → --on-canvas*.
     Getting it wrong is visible in light mode and caught by
     `node tools/theme_shots.mjs`, which measures what actually rendered. */
  --on-canvas:        #e8edf2;
  --on-canvas-2:      #c8d0da;
  --on-canvas-muted:  #8a96a4;   /* 4.9:1 on the canvas — WCAG AA */
  --on-canvas-strong: #ffffff;
  --on-canvas-line:   rgba(255, 255, 255, 0.08);
  --on-canvas-line2:  rgba(255, 255, 255, 0.12);
  /* And the tint a canvas-level chip or pill is filled with. `--glass-bg` is
     a *card* surface in light mode (near-solid white), so a filter pill or a
     tab that lives on the canvas cannot use it — it would come out as a white
     lozenge floating on the navy. */
  --on-canvas-fill:   rgba(255, 255, 255, 0.06);
  --on-canvas-fill2:  rgba(255, 255, 255, 0.10);
  /* The accents, for the same reason: a section label, a footer link or a
     live dot drawn on the canvas keeps the bright hue in both themes. The
     deep light-mode accents exist to clear 4.5:1 on *white*, which is a card
     problem, not a canvas one. Same four meanings as always. */
  --on-canvas-accent: #00F2FE;   /* cyan — links / active nav */
  --on-canvas-amber:  #FFB800;   /* section labels / pending */
  --on-canvas-green:  #10B981;   /* wins / positive */
  --on-canvas-red:    #ff4d4d;   /* live / losses */

  /* ── Chrome surfaces ───────────────────────────────────────────────────
     Named by role rather than by colour so both themes can answer them.
     Pages hardcoded these as one-off hexes (#0B0E11 navs, rgba(0,0,0,0.35)
     blurred bars, rgba(10,13,18,0.97) menus); they are tokens now. */
  --nav-bg:     #0B0E14;                 /* solid desktop nav bar */
  --topbar-bg:  rgba(11, 14, 20, 0.65);  /* blurred mobile top bar */
  /* The mobile bottom bar carries more weight than the top bar — it sits over
     scrolling content and has to stay legible — but it is still a *frosted*
     bar, not a lid: at 0.92 the 20px blur behind it had nothing to see and
     the bar read as a solid black slab. Same role, two weights, so they
     cannot share one token. */
  --bar-bg:     rgba(11, 14, 20, 0.72);
  --menu-bg:    rgba(10, 13, 18, 0.97);  /* dropdowns / popovers */

  /* ── Liquid glass — the two floating MOBILE bars ──────────────────────
     The phone's top pills (_mobile_topbar.html) and its bottom capsule
     (_mobile_nav_style.html) are the same object twice: two pieces of glass
     floating over the page at opposite edges. They used to be written
     separately — the capsule a 72% dark fill, the pills a 9% white tint —
     and read as two different materials at two different tones. One recipe
     now, so a change to the material moves both.

     Both bars are theme-FIXED dark (owner ruling, 2026-08): the page
     re-themes and the chrome does not. So none of these values flips —
     except `--gl-fill`, and that one is dark in BOTH themes. It carries a
     second weight because 70% dark over a dark canvas still reads as the
     dark bar, while over a near-white page it reads as a washed grey. Same
     glass, leaned on harder against what is behind it.

     `--gl-fill` is deliberately short of opaque in both — owner ruling,
     2026-08: around 70%, and never 100%. The bars carry more weight than
     glass alone would give them, but the blur still has something to see
     behind it; at full opacity there is nothing to blur and the bar is a
     slab, not glass. */
  --gl-fill:   rgba(11, 14, 20, 0.70);
  --gl-blur:   blur(26px) saturate(185%);
  --gl-border: rgba(255, 255, 255, 0.11);
  /* The specular: one bright hairline along the top edge, which is what
     separates a pane of glass from a translucent rectangle. Inset, so it
     rides inside the radius on a capsule of any shape. */
  --gl-shadow: 0 8px 28px rgba(0, 0, 0, 0.40),
               inset 0 1px 0 rgba(255, 255, 255, 0.13);
  --input-bg:   rgba(255, 255, 255, 0.05);
  --hover-bg:   rgba(255, 255, 255, 0.06);
  --scrim:      rgba(0, 0, 0, 0.55);     /* modal backdrops */
  --card-bg:    rgba(255, 255, 255, 0.025);

  /* ── Elevation ─────────────────────────────────────────────────────────
     Dark mode leans on borders and uses shadow sparingly; light mode has to
     lean on shadow instead, because a white card on a light canvas has no
     edge without one. Both answer the same two names. */
  --shadow-card: 0 8px 24px rgba(0, 0, 0, 0.35);
  --shadow-pop:  0 18px 50px rgba(0, 0, 0, 0.55);
  --card-shadow:       0 10px 20px rgba(0, 0, 0, 0.25);
  --card-hover-shadow: 0 20px 40px rgba(0, 0, 0, 0.35);

  /* Wordmark fill: white→grey in the dark, ink→slate in the light. */
  --logo-grad: linear-gradient(90deg, #ffffff, #cccccc);

  /* ── Layout ────────────────────────────────────────────────────────── */
  --id-h:     62px;
  --bottom-h: 76px;
  --pad:      16px;
  --r-card:   18px;

  /* ── Safe areas ────────────────────────────────────────────────────────
     The app draws edge-to-edge: the page paints behind the status bar /
     Dynamic Island and behind the home indicator, and these four names are
     how anything anchored to an edge keeps its *content* clear of them.
     Zero on the web and on any device without a cutout, so a rule written
     against them is correct everywhere and needs no media query.

     Use these rather than env(safe-area-inset-*) directly: the Android
     override below cannot reach a raw env() call. Two rules of thumb —
     a bar fixed to an edge pads by the inset and lets its *background*
     run under it; a floating element offsets by it. */
  --safe-top:    env(safe-area-inset-top, 0px);
  --safe-bottom: env(safe-area-inset-bottom, 0px);
  --safe-left:   env(safe-area-inset-left, 0px);
  --safe-right:  env(safe-area-inset-right, 0px);

  /* ── Type scale (iOS) ──────────────────────────────────────────────────
     Named by role, sized against Apple's text styles rather than against
     "how small can this get and still fit". The floor is deliberate:
     `--fs-micro` is 11px, which is Caption 2, the smallest size iOS ships,
     and the smallest size a label may be set at anywhere in the app. The
     app shipped 8–10px labels in six places; they are legible on a 27"
     display at 100% zoom and not on a phone held at arm's length.

     Body sits at 15px and headline at 17px — iOS's own body size — so
     reading text is never smaller than the platform's default.

     These are a scale, not a licence to invent values between them: a page
     that needs "a bit bigger than caption" takes footnote. */
  --fs-micro:    0.6875rem;  /* 11px — uppercase micro-labels, badges, tags */
  --fs-caption:  0.75rem;    /* 12px — chip text, secondary captions */
  --fs-footnote: 0.8125rem;  /* 13px — dense secondary text, table cells */
  --fs-subhead:  0.875rem;   /* 14px — card subtitles, list secondary line */
  --fs-body:     0.9375rem;  /* 15px — body copy, list primary line */
  --fs-headline: 1.0625rem;  /* 17px — section headline (iOS body size) */
  --fs-title3:   1.25rem;    /* 20px — card titles */
  --fs-title2:   1.375rem;   /* 22px — page section titles */
  --fs-title1:   1.75rem;    /* 28px — page titles */
  /* iOS's Large Title. The four tab screens (Games, Best Picks, Search, My
     Slips) open on their name at this size and hand it to the top bar's
     pill once scrolled (_mobile_topbar.html), the navigation-bar move. */
  --fs-large-title: 2.125rem; /* 34px */

  /* ── Touch ─────────────────────────────────────────────────────────────
     44px is Apple's minimum comfortable hit size, and it is a *hit* size,
     not a paint size: a 26px pill can keep its 26px look and still answer
     44px of finger via a transparent ::after expander. See
     static/css/app-ios.css. */
  --tap: 44px;

  /* ── Spacing (4px base) ────────────────────────────────────────────────
     The system has always said "4px multiples"; the app drifted to 5px,
     6.4px, 7px and 9.6px because those are what 0.4rem and 0.6rem work out
     to. Named steps make the multiple the easy thing to reach for. */
  --sp-1:  4px;
  --sp-2:  8px;
  --sp-3: 12px;
  --sp-4: 16px;
  --sp-5: 20px;
  --sp-6: 24px;
  --sp-8: 32px;

  /* ── Radius (one scale) ────────────────────────────────────────────────
     --r-card 18px is the card. The rest exist so that the values already in
     use have a name instead of being re-picked per page (14px menus, 12px
     controls, 20px bottom sheets). Anything that reads as a *card* takes
     --r-card, including the ones currently shipping 14px and 20px. */
  --r-control: 12px;   /* buttons, inputs, small controls */
  --r-menu:    14px;   /* dropdowns, popovers */
  --r-sheet:   20px;   /* bottom sheets, modals */

  /* ── Pills & tags (one scale) ──────────────────────────────────────── */
  --pill-radius: 999px;
  --pill-h:      26px;
  --pill-pad-x:  14px;
  --pill-font:   0.72rem;
  --tag-h:       20px;
  --tag-pad-x:   10px;
  --tag-font:    0.62rem;

  /* ── Backward-compat aliases ───────────────────────────────────────────
     Older CSS/JS (charts, particles, some page styles) read these legacy
     --color-* names. Kept mapped to the canonical tokens so removing a
     page's inline :root does not break runtime code that references them. */
  --color-primary:   var(--cyan);
  --color-secondary: var(--amber);
  --color-accent:    var(--green);
  --color-success:   var(--green);
  --color-warning:   var(--amber);
  --color-danger:    var(--red);
  --color-muted:     var(--muted);
  --color-bg:        var(--bg);
  --color-section:   var(--bg2);
  --color-text:      var(--text);
  --transition-smooth: all 0.3s cubic-bezier(0.4, 0, 0.2, 1);
}

/* Android's WebView draws behind the status bar once the shell asks it to,
   but does not reliably report that inset to CSS — env(safe-area-inset-top)
   can come back 0 on a device whose content is genuinely underneath the
   clock. Floor it at Android's 24dp platform status bar (CSS px are dp in
   the WebView) so the top bar can never land under the system icons; a
   device that does report a taller cutout still wins the max(). The class
   is added by static/js/native-ui.js and only ever exists in the native
   Android shell, so neither iOS nor the website is touched. */
html.bw-native-android {
  --safe-top: max(env(safe-area-inset-top, 0px), 24px);
}

/* ============================================================================
   LIGHT MODE
   ----------------------------------------------------------------------------
   Set by the boot script in base.html (and live.html, which does not extend
   it) from localStorage['bw-theme'], before first paint. Toggled from the
   account button in the top bar — see static/js/theme.js.

   Why the selector is `html[data-theme="light"]` and not `:root[...]`:
   both work, but the attribute form documents the intent and, at (0,1,1),
   outranks every `:root { … }` a page declares locally. Those page blocks
   still carry the dark values; this is what overrules them.

   Contrast: every foreground token below was measured against both surfaces
   it can land on — the canvas (#eef2f7) and a white card — and clears WCAG AA
   4.5:1 on both. The accents are the shipped hues walked down in lightness
   until they passed, so cyan still reads as cyan and green still reads as the
   "win" colour; they are not a different palette.
   ============================================================================ */

html[data-theme="light"] {
  color-scheme: light;

  /* ── Canvas & surfaces ─────────────────────────────────────────────────
     Inverted depth order: the canvas is the *darkest* surface here and cards
     rise toward white, which is the opposite of dark mode and the reason a
     naive inversion of the hexes looks wrong.

     2026-08 re-tint: the first light palette put the canvas at #eef2f7 —
     within 1.06:1 of the white cards — and the whole app read as one
     unbroken white field. The canvas then carried a real cool tone (still
     the navy family, ~4% darker), the nested panel became a visible tint
     instead of a second near-white, and the hairlines/hovers were weighted
     up to match.

     2026-08b takes that ruling to its end: **the canvas does not flip at
     all.** It is the same navy, under the same ambient field and the same
     film grain, in both themes — light mode changes the cards and their
     contents and nothing else. `--bg` is deliberately absent from this
     block for that reason; the depth still comes from the canvas being
     darker than the cards, only now by the whole distance rather than 4%.
     Text with no card under it takes `--on-canvas*` — see :root. */
  --bg2:           #ffffff;   /* elevated surface — cards */
  --bg3:           #e9eef6;   /* raised surface — nested panels, visibly tinted */
  /* `--glass-bg` is a card surface first: near-solid white so cards separate
     from the canvas. `--glass-bg2` stays a *tint* (it is the hover/active
     fill), so it has to darken rather than lighten or it would vanish on the
     white it usually sits on. */
  --glass-bg:      rgba(255, 255, 255, 0.86);
  --glass-bg2:     rgba(14, 27, 46, 0.07);
  --glass-border:  rgba(14, 27, 46, 0.12);
  --glass-border2: rgba(14, 27, 46, 0.20);

  /* Same three roles, inverted: the elevated card is the *lightest* surface
     here, and its edge comes from the shadow rather than from the hairline. */
  --surface:        #ffffff;
  --surface-2:      #e9eef6;
  --surface-border: rgba(14, 27, 46, 0.12);
  --shadow-elev:    0 1px 2px rgba(14, 27, 46, 0.07), 0 10px 26px rgba(14, 27, 46, 0.10);

  /* Research surfaces: an inset is the input well's tint (a panel sunk into
     a white card reads like a field does); a track has to show on both the
     white card and that well; a lit segment is the white card itself. */
  --inset:          #eef2f8;
  --track:          #d3dbe7;
  --seg-on:         #ffffff;

  /* ── Semantic accents ──────────────────────────────────────────────────
     Same meanings, deepened one step further with the 2026-08 canvas: the
     first light accents cleared 4.5:1 on #eef2f7 and on white, but their
     text-on-own-tint chips composited to 4.38:1 — the audit's standing
     failure — and a darker canvas would have dragged every on-canvas pair
     under AA. These clear 4.5:1 on the new canvas, on white, and as text
     over their own -dim tint on both surfaces:
       cyan  5.74:1 on canvas / 7.02:1 on white
       green 5.36 / 6.55   red 5.39 / 6.60
       amber 5.45 / 6.66   gold 5.90 / 7.22                                */
  --cyan:  #08607f;
  --green: #056b49;
  --red:   #b3241a;
  --amber: #8c4c06;
  --gold:  #7d4c05;
  /* Tints ride at 0.12 (not dark's semantics-free 0.10/0.35) so a chip is a
     visible wash on a white card, and lines at 0.40 so a tag border still
     reads as an edge rather than a smudge. */
  --cyan-dim:  rgba(8, 96, 127, 0.12);
  --cyan-mid:  rgba(8, 96, 127, 0.22);
  --cyan-line: rgba(8, 96, 127, 0.40);
  --green-dim: rgba(5, 107, 73, 0.12);
  --green-line: rgba(5, 107, 73, 0.40);
  --amber-dim: rgba(140, 76, 6, 0.12);
  --amber-line: rgba(140, 76, 6, 0.40);
  --gold-dim:  rgba(125, 76, 5, 0.12);
  --gold-line: rgba(125, 76, 5, 0.40);
  /* red-dim stays at 0.10: red text over its own tint on the canvas is the
     tightest pair in the set, and the lighter wash is what keeps it AA. */
  --red-dim:   rgba(179, 36, 26, 0.10);
  --red-line:  rgba(179, 36, 26, 0.40);
  /* Kept brighter than the flat accents: this is a fill, and white sits on
     it (5.17:1 / 4.80:1 — AA even for the normal-size text it carries). */
  --grad:      linear-gradient(135deg, #0b74a6, #008264);

  /* ── Text ──────────────────────────────────────────────────────────── */
  /* Ratios are against a white card: since 2026-08b these inks only ever land
     on one. Text with no card under it takes --on-canvas* instead. */
  --text:   #0e1b2e;   /* 17.9:1 on white */
  --text2:  #33465f;   /*  9.0:1 */
  --muted:  #54647a;   /*  6.0:1 — WCAG AA */
  --muted2: #56657b;   /*  5.9:1 */
  --text-strong: #0e1b2e;
  --on-accent:   #ffffff;

  /* ── Chrome surfaces ─────────────────────────────────────────────────
     The three page-level bars are deliberately absent: a bar is chrome over
     the canvas, not a card, and the canvas does not flip (2026-08b). They
     were already being pinned back to the dark literals page-side — see
     base.html's `#bw-desktop-nav` block and _mobile_nav_style.html's
     `.id-header` / `.bottom-nav` — which is the same ruling reached one page
     at a time. Keeping :root's values is that ruling stated once.

     `--menu-bg` is NOT one of them: a dropdown is a small card, it carries
     `--text` ink, and it changes with the cards. */
  --menu-bg:   rgba(255, 255, 255, 0.98);
  /* A field is a *well*, one step below the surface it sits in — iOS greys
     its fields the same way. Pure white here made an input on a white card
     invisible until focused. */
  --input-bg:  #eef2f8;
  --hover-bg:  rgba(14, 27, 46, 0.07);
  /* A modal backdrop still darkens — a light scrim over a light page reads
     as a rendering fault, not as "the page behind is inactive". */
  --scrim:     rgba(14, 27, 46, 0.45);
  --card-bg:   rgba(255, 255, 255, 0.9);

  /* Liquid glass: still the DARK fill — the two mobile bars do not follow
     the theme — just weighted for what is behind it here. Everything else
     about the material (blur, border, specular) is theme-fixed and stays
     up in :root. Still not opaque: the ruling holds in both themes, and a
     bar with no blur behind it reads as a lid over a light page just as
     badly as over a dark one. */
  --gl-fill:   rgba(11, 14, 20, 0.86);

  /* ── Elevation ─────────────────────────────────────────────────────────
     Softer, tinted with the text navy rather than pure black: a black shadow
     on a light surface greys the whole card out. Weighted up a step with the
     2026-08 canvas — a toned canvas can carry a slightly firmer shadow
     without the card greying out, and the shadow is half of what makes a
     white card read as *raised* rather than merely white. */
  --shadow-card: 0 1px 2px rgba(14, 27, 46, 0.07), 0 10px 26px rgba(14, 27, 46, 0.10);
  --shadow-pop:  0 12px 40px rgba(14, 27, 46, 0.20);
  --card-shadow:       0 1px 2px rgba(14, 27, 46, 0.06), 0 8px 20px rgba(14, 27, 46, 0.09);
  --card-hover-shadow: 0 2px 4px rgba(14, 27, 46, 0.07), 0 16px 36px rgba(14, 27, 46, 0.14);

  /* --logo-grad is absent for the same reason: the wordmark is drawn on a bar
     or on the canvas, never on a card, so the dark-mode white gradient is the
     one that is legible in both themes. */

  /* ── Backward-compat aliases ───────────────────────────────────────────
     In :root these are aliases and re-theme for free. They are repeated here
     because several pages *redefine* them locally with dark literals
     (`--color-text: #dde3ea`, `--color-bg: #0f1114`), and a page-local
     `:root` beats the global alias. Re-declaring them at this specificity is
     what pulls those pages into light mode — without it they render dark text
     on the light canvas.

     The accent aliases are here for the same reason — twenty pages pin them to
     the standard palette as literals (`--color-primary: #33ccff`), and cyan at
     1.9:1 on white is not a link colour. A page that means something else by
     them (world-cup.html, whose primary is the competition's green) re-pins
     them in its own `html[data-theme="light"]` block, which is at equal
     specificity and later in source order, so it wins. */
  --color-bg:        var(--bg);
  --color-section:   var(--bg2);
  --color-text:      var(--text);
  --color-muted:     var(--muted);
  --color-primary:   var(--cyan);
  --color-secondary: var(--amber);
  --color-accent:    var(--green);
  --color-success:   var(--green);
  --color-warning:   var(--amber);
  --color-danger:    var(--red);
}

/* ── Cross-theme baseline ─────────────────────────────────────────────────
   The one place this file does touch elements. Both are theme *transitions*
   rather than theme colours: without them the canvas snaps while every
   tokenised child cross-fades, which reads as a flicker. Scoped to the class
   the toggle adds for the length of the switch, so it never taxes normal
   interaction (hover fills, opening menus) with an extra transition. */
html.bw-theme-switching,
html.bw-theme-switching body {
  transition: background-color 220ms ease, color 220ms ease;
}
@media (prefers-reduced-motion: reduce) {
  html.bw-theme-switching,
  html.bw-theme-switching body { transition: none; }
}
