/* =========================================================
   TYPEFLOW — DESIGN SYSTEM
   One source of truth. Components below only ever read these
   tokens, so a theme (the body[data-theme="…"] blocks near the
   end of this file) reskins the whole UI by overriding tokens
   once — and, new in this redesign, by flipping a small set of
   "character kit" knobs (surface finish, corner language, the
   signature accent detail) so themes read as distinct moods,
   not just palettes.

   Token groups: palette · type · shape · depth · character · motion
========================================================= */

:root {
  /* PALETTE — "precision" dark. A near-black cooled a touch blue, one
     confident blue accent spent sparingly (focus, caret, the single
     primary action in view, live stats) and nothing else competing
     with it. Deliberately its own look, not a copy of any purchasable
     theme below, so the free default still reads as "typeflow" and
     every unlock is a real change. */
  --bg: #0a0c10;
  --surface: #14171f;
  --surface-2: #1c2029;
  --text: #edeff3;
  --muted: #9ba3b4;

  /* These four derive from the tokens above via color-mix so that a theme
     block near the end of the file — which typically only sets bg /
     surface / surface-2 / text / muted / accent — still gets sensible
     values for all of them without restating each. Each theme MAY still
     override any of these for a sharper look. */
  --surface-3: color-mix(in srgb, var(--surface-2) 80%, var(--text));  /* hover / pressed */
  --border: color-mix(in srgb, var(--text) 12%, transparent);
  --border-strong: color-mix(in srgb, var(--text) 24%, transparent);
  /* Untyped test characters, disabled text, faint labels. */
  --faint: color-mix(in srgb, var(--muted) 55%, var(--bg));

  --accent: #4c8dff;
  /* Hover brightens toward the text colour (lighter on dark themes,
     darker on light — right either way); press deepens. Both derive so
     a theme only has to set --accent. */
  --accent-hover: color-mix(in srgb, var(--accent) 86%, var(--text));
  --accent-press: color-mix(in srgb, var(--accent) 84%, #000);
  --accent-soft: color-mix(in srgb, var(--accent) 14%, transparent);
  /* Text/icons ON a solid --accent fill — dark here (bright-blue button
     with an ink label, the crisp pro-tool look); white on the light
     default below, where --accent is the deeper blue. */
  --on-accent: #08101d;
  /* Kept (≈40 rules reference it) but the redesign retires the
     violet→coral gradient: it's now just a lighter tint of the one
     accent, for the few places a second stop still earns its keep
     (charts, one hero sweep). */
  --accent-2: #7fb0ff;

  --success: #35c98b;
  --danger: #ff5c5c;
  --warning: #f5a623;

  /* TYPE */
  --font: 'Inter', system-ui, -apple-system, 'Segoe UI', Roboto, sans-serif;
  --font-display: var(--font);   /* headings + big numerals — a theme may swap */
  --test-font: var(--font);      /* the typing passage — code mode forces mono */

  /* SHAPE — tighter than before (was 10/16/24). A theme can go sharper
     (cyber, terminal, obsidian) or softer (sakura, linen, lilac). */
  --radius-sm: 8px;
  --radius-md: 12px;
  --radius-lg: 16px;
  --radius-full: 999px;

  /* DEPTH — elevation reads from a crisp hairline plus a short, tight
     shadow, not a big soft blur.
     A dark drop-shadow is doing nothing here — the page behind a card is
     already near-black, so a black shadow has no ground to show against
     (verified: computed box-shadow renders, but is genuinely invisible).
     Light mode's identical shadow formula works because it sits on white.
     The fix dark UIs actually use: a card "catches light" via a faint
     inset highlight along its top edge instead of relying on a shadow
     that can't read against a dark ground — every --shadow-* below pairs
     one with the drop-shadow (harmless if the drop-shadow doesn't show;
     it's what now gives cards their visible lift). */
  --shadow-sm: 0 1px 3px rgba(0, 0, 0, 0.4), inset 0 1px 0 rgba(255, 255, 255, 0.045);
  --shadow-md: 0 10px 30px -10px rgba(0, 0, 0, 0.5), inset 0 1px 0 rgba(255, 255, 255, 0.05);
  --shadow-lg: 0 28px 64px -18px rgba(0, 0, 0, 0.6), inset 0 1px 0 rgba(255, 255, 255, 0.06);
  --focus-ring: 0 0 0 3px var(--accent-soft), 0 0 0 1px var(--accent);

  /* CARET — one set of knobs, read by the per-character ::before caret,
     the floating smooth caret (.live-caret), and the customize-modal
     preview bar alike. A cursor skin reskins the caret purely by
     overriding these, so a skin now shows in every caret mode
     (previously each skin hard-styled ::before and nothing else). The
     caret-shape display pref layers geometry on top. */
  --caret-bg: var(--accent);
  --caret-bg-size: auto;
  --caret-glow: 0 0 10px color-mix(in srgb, var(--accent) 55%, transparent);
  --caret-anim: blink 1.1s var(--ease) infinite;
  --caret-width: 2px;
  --caret-clip: none;

  /* CHARACTER KIT — neutral defaults so component rules can read them
     unconditionally; theme blocks near the end of the file flip them.
     Deliberately NOT here: radius/shadow/font. Every theme shares those,
     so the shell/layout never reshapes when you switch themes — only
     colour, the signature accent below, and a theme's own background
     layer do. */
  --card-border: 1px solid var(--border);  /* premium themes → a gradient frame */
  --pill-active-bg: var(--accent-soft);    /* the signature accent detail */
  --pill-active-fg: var(--accent);
  --pill-active-ring: inset 0 0 0 1px color-mix(in srgb, var(--accent) 40%, transparent);

  /* MOTION */
  --ease: cubic-bezier(0.4, 0, 0.2, 1);
  --ease-out: cubic-bezier(0.16, 1, 0.3, 1);
  --dur-1: 120ms;
  --dur-2: 200ms;
  --dur-3: 320ms;

  /* LAYOUT — every full-width page region (header, home sections, the
     word box, results, community, dashboard, shop, profile, race,
     achievements, footer) is exactly `min(--container, 100% - 2*--gutter)`
     and centred, so their left/right edges line up down the whole page.
     --container-narrow is for single-column reading pages (legal, contact,
     account settings). Nothing sets its own width outside this system. */
  --container: 1080px;
  --container-narrow: 680px;
  --gutter: 24px;

  color-scheme: dark;
}

/* The one width utility. Every page shell resolves to this. */
.shell,
.page-container,
.site-header-inner,
.results-shell,
.community-wrapper,
.dashboard-shell,
.profile-container,
.race-track-shell,
.race-lobby-shell,
.settings-shell,
.legal-page,
.footer-inner,
.footer-bottom,
#challenge,
.daily-challenge-bar,
#word-container {
  width: min(var(--container), 100% - 2 * var(--gutter));
  margin-inline: auto;
}

.legal-page,
.settings-shell {
  width: min(var(--container-narrow), 100% - 2 * var(--gutter));
}

@media (max-width: 600px) {
  :root { --gutter: 16px; }
}

/* The default LIGHT look — shown to logged-out / no-theme-equipped
   visitors whose OS asks for light. Not an inverted afterthought:
   --accent deepens to #2e6fe0 (5.3:1 on white) because it's a literal
   text colour in dozens of rules, not just fills, and --on-accent flips
   to white (the filled button becomes that deeper blue). Theme blocks
   set these same properties on <body>, which always beats what :root
   gives here regardless of source order. */
@media (prefers-color-scheme: light) {
  :root {
    --bg: #fafbff;
    --surface: #ffffff;
    --surface-2: #f2f4f9;
    --surface-3: #e8ebf2;
    --border: #e5e8f0;
    --border-strong: #cfd5e2;

    --text: #10131a;
    --muted: #5c6475;

    --accent: #2e6fe0;
    --accent-hover: #2563d6;
    --accent-press: #1f56c0;
    --accent-soft: rgba(46, 111, 224, 0.1);
    --on-accent: #ffffff;
    --accent-2: #5b93f0;

    --success: #12905c;
    --danger: #d92d2d;
    --warning: #b26a00;

    --shadow-sm: 0 1px 2px rgba(15, 23, 42, 0.08);
    --shadow-md: 0 8px 24px -8px rgba(15, 23, 42, 0.12);
    --shadow-lg: 0 24px 60px -16px rgba(15, 23, 42, 0.16);

    color-scheme: light;
  }
}

/* Honour a reduced-motion request — this site has ~40 decorative
   animations (theme shimmer sweeps, particle drifts, ambient pulses,
   the caret blink) and had zero handling for it before. Near-instant
   rather than a hard 0 so state changes still register. */
@media (prefers-reduced-motion: reduce) {
  *,
  *::before,
  *::after {
    animation-duration: 0.001ms !important;
    animation-iteration-count: 1 !important;
    transition-duration: 0.001ms !important;
    scroll-behavior: auto !important;
  }
}

/* =========================================================
   THEMES — token blocks + character kits
   Slugs match the real ShopItem catalog (item_type "theme" /
   "limited"). Each block sets its palette and flips whichever
   "kit" knobs (radii, surface finish, the --pill-active-*
   signature, fonts) give it a personality beyond colour.
   Tokens NOT set here derive from the ones that are (see the
   color-mix defaults in :root) — so --accent-soft / --border /
   --accent-hover / --surface-3 / --border-strong usually don't
   need restating.

   `[data-theme-scope="x"]` is an alias on every block so the
   customize-modal swatch can render a real mini-preview by
   scoping the theme's tokens locally. Decorative ::before /
   background layers stay body-only and live further down.

   Reusable font stacks:
   mono  → ui-monospace, 'Cascadia Code', 'JetBrains Mono', 'Source Code Pro', Menlo, Consolas, monospace
   serif → 'Iowan Old Style', 'Palatino Linotype', Palatino, Georgia, Cambria, 'Times New Roman', serif
========================================================= */

/* ---- NIGHT / DARK family ---------------------------------------------
   Solid surfaces, soft elevation, the slim bar caret. */

/* The quiet one — even darker and more neutral than the default, zero
   texture, zero motion, a muted steel-indigo accent. "Lights off." */
body[data-theme="focus-dark"],
[data-theme-scope="focus-dark"] {
  --bg: #0a0e17;
  --surface: #131826;
  --surface-2: #1b2233;
  --surface-3: color-mix(in srgb, var(--surface-2) 80%, var(--text));
  --border: color-mix(in srgb, var(--text) 12%, transparent);
  --border-strong: color-mix(in srgb, var(--text) 24%, transparent);
  --text: #eef2f8;
  --muted: #93a0b6;
  --accent: #8896ff;
  --accent-2: #55d6d0;
  --on-accent: #0a0e1c;
  color-scheme: dark;
}

/* Moonlight on water — deep navy, a moonlit-blue accent, pale silver
   secondary. A low shimmer band + sparse far stars (see ::before). */
body[data-theme="midnight"],
[data-theme-scope="midnight"] {
  --bg: #060a17;
  --surface: #0e182e;
  --surface-2: #16223f;
  --surface-3: color-mix(in srgb, var(--surface-2) 80%, var(--text));
  --border-strong: color-mix(in srgb, var(--text) 24%, transparent);
  --border: color-mix(in srgb, var(--text) 12%, transparent);
  --text: #e9eefc;
  --muted: #93a4c6;
  --accent: #90b4ff;
  --accent-2: #dbe4ff;
  --on-accent: #081226;
  color-scheme: dark;
}

/* Submerged — teal caustics, a faint glass on the surfaces, slow bubbles
   rising (see ::before). */
body[data-theme="deep-ocean"],
[data-theme-scope="deep-ocean"] {
  --bg: #04141a;
  --surface: #0f2830;
  --surface-2: #17414d;
  --surface-3: color-mix(in srgb, var(--surface-2) 80%, var(--text));
  --border-strong: color-mix(in srgb, var(--text) 24%, transparent);
  --border: color-mix(in srgb, var(--text) 12%, transparent);
  --text: #e2fbff;
  --muted: #82b4bb;
  --accent: #34dccb;
  --accent-2: #63a4f5;
  --accent-soft: rgba(52, 220, 203, 0.14);
  --on-accent: #032019;
  color-scheme: dark;
}

/* Deep space — the flagship "cosmos" look. Solid tinted surfaces (same
   shape/depth system as every other theme) with a dense drifting
   starfield + nebula glow behind them. */
body[data-theme="galaxy"],
[data-theme-scope="galaxy"] {
  --bg: #0a0817;
  --surface: #171233;
  --surface-2: #1e1842;
  --surface-3: color-mix(in srgb, var(--surface-2) 80%, var(--text));
  --border: color-mix(in srgb, var(--text) 12%, transparent);
  --border-strong: color-mix(in srgb, var(--text) 24%, transparent);
  --text: #f2f0ff;
  --muted: #b4addb;
  --accent: #a683ff;
  --accent-2: #d8d5ff;
  --accent-soft: rgba(166, 131, 255, 0.16);
  --on-accent: #100a20;
  color-scheme: dark;
}

/* Aurora borealis — violet ribbons over a teal sky-glow, on the same
   solid surfaces every theme uses (the ribbons live in a mix-blend body
   layer instead, so they read without needing translucent cards). */
body[data-theme="aurora"],
[data-theme-scope="aurora"] {
  --bg: #0c1124;
  --surface: #171d38;
  --surface-2: #1f2645;
  --surface-3: color-mix(in srgb, var(--surface-2) 80%, var(--text));
  --border: color-mix(in srgb, var(--text) 12%, transparent);
  --border-strong: color-mix(in srgb, var(--text) 24%, transparent);
  --text: #f6f8ff;
  --muted: #c3cbe0;
  --accent: #a78bfa;
  --accent-2: #34d8c2;
  --accent-soft: rgba(167, 139, 250, 0.16);
  --on-accent: #120a24;
  color-scheme: dark;
}

/* ---- CYBER / SYNTH family ------------------------------------------
   Same shape/type system as every other theme — the signature here is a
   hard 1px accent ring on the active segment plus glow, not a reshaped
   UI. Matrix's typing passage alone switches to mono (below). */

/* Cyan/magenta terminal. Scanline texture, block caret, phosphor glow. */
body[data-theme="cyber-pulse"],
[data-theme-scope="cyber-pulse"] {
  --bg: #04060c;
  --surface: #0b1220;
  --surface-2: #101a2c;
  --surface-3: color-mix(in srgb, var(--surface-2) 80%, var(--text));
  --border: color-mix(in srgb, var(--text) 12%, transparent);
  --border-strong: color-mix(in srgb, var(--text) 24%, transparent);
  --text: #e6fffb;
  --muted: #79b4ad;
  --accent: #2de2d2;
  --accent-2: #ff3dbb;
  --accent-soft: rgba(45, 226, 210, 0.12);
  --on-accent: #022019;
  --pill-active-bg: transparent;
  --pill-active-fg: var(--accent);
  --pill-active-ring: inset 0 0 0 1px var(--accent), 0 0 12px -2px var(--accent);
  color-scheme: dark;
}

/* Tokyo at night — hot pink + amber signage over a sign-grid glow. */
body[data-theme="neon-tokyo"],
[data-theme-scope="neon-tokyo"] {
  --bg: #0c0117;
  --surface: #180a2a;
  --surface-2: #221139;
  --surface-3: color-mix(in srgb, var(--surface-2) 80%, var(--text));
  --border: color-mix(in srgb, var(--text) 12%, transparent);
  --border-strong: color-mix(in srgb, var(--text) 24%, transparent);
  --text: #ffe7fa;
  --muted: #bd93d4;
  --accent: #ff3d92;
  --accent-2: #ffb020;
  --accent-soft: rgba(255, 61, 146, 0.16);
  --on-accent: #22000f;
  --pill-active-bg: transparent;
  --pill-active-fg: var(--accent);
  --pill-active-ring: inset 0 0 0 1px var(--accent), 0 0 14px -3px var(--accent);
  color-scheme: dark;
}

/* Phosphor-green terminal. Same corners/shell as every theme; the
   passage itself switches to mono, a solid green block caret, and a
   real falling digital rain (rebuilt — see ::before) carry the identity. */
body[data-theme="matrix"],
[data-theme-scope="matrix"] {
  --bg: #000000;
  --surface: #050d05;
  --surface-2: #0a180a;
  --surface-3: color-mix(in srgb, var(--surface-2) 80%, var(--text));
  --border: color-mix(in srgb, var(--text) 12%, transparent);
  --border-strong: color-mix(in srgb, var(--text) 24%, transparent);
  --text: #c9ffd2;
  --muted: #62b06c;
  --accent: #22e34a;
  --accent-2: #33e0e0;
  --accent-soft: rgba(34, 227, 74, 0.12);
  --on-accent: #011a09;
  --test-font: ui-monospace, 'Cascadia Code', 'JetBrains Mono', 'Source Code Pro', Menlo, Consolas, monospace;
  --pill-active-bg: var(--accent);
  --pill-active-fg: #011a09;
  --pill-active-ring: 0 0 14px -2px var(--accent);
  color-scheme: dark;
}

/* Charcoal ash + a banked ember. Surfaces lifted a little further off
   the background for separation, accent deepened from a hot orange-red
   toward a smoulder. */
body[data-theme="volcanic-ash"],
[data-theme-scope="volcanic-ash"] {
  --bg: #13110e;
  --surface: #221d18;
  --surface-2: #2f2822;
  --surface-3: color-mix(in srgb, var(--surface-2) 80%, var(--text));
  --border: color-mix(in srgb, var(--text) 12%, transparent);
  --border-strong: color-mix(in srgb, var(--text) 24%, transparent);
  --text: #f2ece8;
  --muted: #a1938b;
  --accent: #db5228;
  --accent-2: #c9b8ac;
  --accent-soft: rgba(219, 82, 40, 0.14);
  --on-accent: #1c0803;
  color-scheme: dark;
}

/* Banked coals — a deep charcoal-brown with one ember accent. Pulled
   back from bright orange + gold to something moodier: dark, warm,
   glowing rather than blazing. */
body[data-theme="lava-flow"],
[data-theme-scope="lava-flow"] {
  --bg: #140a06;
  --surface: #21140d;
  --surface-2: #2d1c13;
  --surface-3: color-mix(in srgb, var(--surface-2) 80%, var(--text));
  --border: color-mix(in srgb, var(--text) 12%, transparent);
  --border-strong: color-mix(in srgb, var(--text) 24%, transparent);
  --text: #f2e4da;
  --muted: #b28f7d;
  --accent: #e0561f;
  --accent-2: #ef8a52;
  --accent-soft: rgba(224, 86, 31, 0.16);
  --on-accent: #1c0a02;
  color-scheme: dark;
}

/* ---- DAYLIGHT / LIGHT family --------------------------------------
   Bright surfaces, crisp hairlines, a soft short shadow. */

/* Frost — icy blue-white, everything crisp, snow swaying past. */
body[data-theme="frost"],
[data-theme-scope="frost"] {
  --bg: #eef5fb;
  --surface: #ffffff;
  --surface-2: #e3eef8;
  --surface-3: color-mix(in srgb, var(--surface-2) 80%, var(--text));
  --border: color-mix(in srgb, var(--text) 12%, transparent);
  --border-strong: color-mix(in srgb, var(--text) 24%, transparent);
  --text: #0e2a44;
  --muted: #5a7891;
  --accent: #0a6a9c;
  --accent-2: #0f8279;
  --accent-soft: rgba(14, 122, 176, 0.1);
  --on-accent: #ffffff;
  --shadow-sm: 0 1px 3px rgba(14, 42, 68, 0.08);
  --shadow-md: 0 10px 30px -10px rgba(14, 42, 68, 0.16);
  color-scheme: light;
}

/* Sakura — a quiet blush. One dusty-rose accent on a warm paper white;
   the old vivid magenta + bamboo-green pair fought each other. */
body[data-theme="sakura"],
[data-theme-scope="sakura"] {
  --bg: #fdf4f5;
  --surface: #fffdfd;
  --surface-2: #f6e9ec;
  --surface-3: color-mix(in srgb, var(--surface-2) 80%, var(--text));
  --border: color-mix(in srgb, var(--text) 12%, transparent);
  --border-strong: color-mix(in srgb, var(--text) 24%, transparent);
  --text: #3c2830;
  --muted: #977a83;
  --accent: #b25074;
  --accent-2: #d18ba5;
  --accent-soft: rgba(178, 80, 116, 0.1);
  --on-accent: #ffffff;
  --shadow-sm: 0 1px 3px rgba(120, 60, 80, 0.07);
  --shadow-md: 0 12px 34px -12px rgba(120, 60, 80, 0.14);
  color-scheme: light;
}

/* Solar flare — warm cream + a single terracotta ember. The old palette
   set a hot ember against a cold violet, which never sat right. */
body[data-theme="solar-flare"],
[data-theme-scope="solar-flare"] {
  --bg: #fdf4e9;
  --surface: #fffdf8;
  --surface-2: #f7e7d3;
  --surface-3: color-mix(in srgb, var(--surface-2) 80%, var(--text));
  --border: color-mix(in srgb, var(--text) 12%, transparent);
  --border-strong: color-mix(in srgb, var(--text) 24%, transparent);
  --text: #3a2306;
  --muted: #8a6842;
  --accent: #b8481f;
  --accent-2: #d9885c;
  --accent-soft: rgba(184, 72, 31, 0.1);
  --on-accent: #ffffff;
  /* Warm amber "success" so the Equipped tag doesn't go flat teal here. */
  --success: #97701a;
  --shadow-sm: 0 1px 3px rgba(90, 55, 20, 0.08);
  color-scheme: light;
}

/* Autumn harvest — parchment + rust + olive, a fibre-grain paper,
   leaves drifting down. */
body[data-theme="autumn-harvest"],
[data-theme-scope="autumn-harvest"] {
  --bg: #fbf3e6;
  --surface: #fffdf8;
  --surface-2: #f6e6cf;
  --surface-3: color-mix(in srgb, var(--surface-2) 80%, var(--text));
  --border: color-mix(in srgb, var(--text) 12%, transparent);
  --border-strong: color-mix(in srgb, var(--text) 24%, transparent);
  --text: #43280f;
  --muted: #94704e;
  --accent: #a63d1f;
  --accent-2: #5c6b2f;
  --accent-soft: rgba(166, 61, 31, 0.1);
  --on-accent: #ffffff;
  --shadow-sm: 0 1px 3px rgba(80, 50, 20, 0.09);
  color-scheme: light;
}

/* Linen — oatmeal, understated, a woven crosshatch, no motion. The
   quiet-light mirror of focus-dark. */
body[data-theme="linen"],
[data-theme-scope="linen"] {
  --bg: #f8f4ee;
  --surface: #fffdf9;
  --surface-2: #efe8db;
  --surface-3: color-mix(in srgb, var(--surface-2) 80%, var(--text));
  --border: color-mix(in srgb, var(--text) 12%, transparent);
  --border-strong: color-mix(in srgb, var(--text) 24%, transparent);
  --text: #2c261e;
  --muted: #776a56;
  --accent: #9a5a1e;
  --accent-2: #a2402a;
  --accent-soft: rgba(154, 90, 30, 0.1);
  --on-accent: #ffffff;
  --shadow-sm: 0 1px 2px rgba(60, 48, 30, 0.08);
  color-scheme: light;
}

/* Meadow — a calm sage. One muted botanical green on a soft green-white;
   the old grass-green + wildflower-gold pair was two competing hues. */
body[data-theme="meadow"],
[data-theme-scope="meadow"] {
  --bg: #f4f8f0;
  --surface: #fdfffb;
  --surface-2: #e9f0e2;
  --surface-3: color-mix(in srgb, var(--surface-2) 80%, var(--text));
  --border: color-mix(in srgb, var(--text) 12%, transparent);
  --border-strong: color-mix(in srgb, var(--text) 24%, transparent);
  --text: #253a1d;
  --muted: #5f7355;
  --accent: #3f7a45;
  --accent-2: #79a879;
  --accent-soft: rgba(63, 122, 69, 0.1);
  --on-accent: #ffffff;
  --shadow-sm: 0 1px 3px rgba(35, 58, 25, 0.08);
  color-scheme: light;
}

/* Rose quartz — a soft mauve. One dusty rose-mauve on a warm grey-white;
   the old rose + gold pair pulled in two directions. Kept deliberately
   softer and cooler than Sakura so the two aren't the same theme. */
body[data-theme="rose-quartz"],
[data-theme-scope="rose-quartz"] {
  --bg: #f9f4f5;
  --surface: #fffdfd;
  --surface-2: #f1e7e9;
  --surface-3: color-mix(in srgb, var(--surface-2) 80%, var(--text));
  --border: color-mix(in srgb, var(--text) 12%, transparent);
  --border-strong: color-mix(in srgb, var(--text) 24%, transparent);
  --text: #362a2d;
  --muted: #857077;
  --accent: #97607a;
  --accent-2: #bd9aab;
  --accent-soft: rgba(151, 96, 122, 0.1);
  --on-accent: #ffffff;
  --shadow-sm: 0 1px 3px rgba(80, 60, 70, 0.07);
  color-scheme: light;
}

/* Citrus grove — reframed as warm honey. A single amber on soft cream;
   the old lemon-yellow surfaces + lime-green accent read as a soda
   label, and its filter-based "zing" animation (removed below) was
   shifting the layout. */
body[data-theme="citrus-grove"],
[data-theme-scope="citrus-grove"] {
  --bg: #fdf8ea;
  --surface: #fffef8;
  --surface-2: #f6ecd6;
  --surface-3: color-mix(in srgb, var(--surface-2) 80%, var(--text));
  --border: color-mix(in srgb, var(--text) 12%, transparent);
  --border-strong: color-mix(in srgb, var(--text) 24%, transparent);
  --text: #3a2c12;
  --muted: #877343;
  --accent: #a5710f;
  --accent-2: #cca258;
  --accent-soft: rgba(165, 113, 15, 0.1);
  --on-accent: #ffffff;
  --shadow-sm: 0 1px 3px rgba(90, 70, 20, 0.09);
  color-scheme: light;
}

/* Lilac bloom — a soft lavender. One muted violet on a cool grey-white;
   the old electric violet + royal blue pair was loud, and its
   filter-based "breathe" animation is dropped below. */
body[data-theme="lilac-bloom"],
[data-theme-scope="lilac-bloom"] {
  --bg: #f6f4fb;
  --surface: #fffdff;
  --surface-2: #ece7f5;
  --surface-3: color-mix(in srgb, var(--surface-2) 80%, var(--text));
  --border: color-mix(in srgb, var(--text) 12%, transparent);
  --border-strong: color-mix(in srgb, var(--text) 24%, transparent);
  --text: #2e2542;
  --muted: #776d90;
  --accent: #6d55b0;
  --accent-2: #9c8bd0;
  --accent-soft: rgba(109, 85, 176, 0.1);
  --on-accent: #ffffff;
  --shadow-sm: 0 1px 3px rgba(55, 45, 85, 0.08);
  color-scheme: light;
}

/* Daybreak — sunrise warmth. A burnt-orange accent on soft peach;
   accent-2 warmed from a cold sky-blue to a lighter tint of the same
   orange, borders lifted (bg and surface were near-identical). */
body[data-theme="daybreak"],
[data-theme-scope="daybreak"] {
  --bg: #fdf2e4;
  --surface: #fffdf9;
  --surface-2: #f8e6d0;
  --surface-3: color-mix(in srgb, var(--surface-2) 80%, var(--text));
  --border: color-mix(in srgb, var(--text) 17%, transparent);
  --border-strong: color-mix(in srgb, var(--text) 30%, transparent);
  --text: #3a2a1a;
  --muted: #8a7057;
  --accent: #c2410c;
  --accent-2: #e08a52;
  --accent-soft: rgba(194, 65, 12, 0.1);
  --on-accent: #ffffff;
  --shadow-sm: 0 1px 3px rgba(80, 50, 20, 0.09);
  color-scheme: light;
}

/* ---- PREMIUM (gems) — the extra-polish tier -----------------------
   Animated gradient card frames, richer motion, glass. */

/* Prism storm — reworked around a single deep amethyst instead of a full
   spectrum. The purple + cyan pair plus a rainbow card frame made the
   typing area feel unsettled; now it's one refined violet with a quiet
   sheen (see the ::before / frame below). */
body[data-theme="prism-storm"],
[data-theme-scope="prism-storm"] {
  --bg: #0a0812;
  --surface: #181425;
  --surface-2: #231d33;
  --surface-3: color-mix(in srgb, var(--surface-2) 80%, var(--text));
  --border: color-mix(in srgb, var(--text) 12%, transparent);
  --border-strong: color-mix(in srgb, var(--text) 24%, transparent);
  --text: #f2effa;
  --muted: #a79dc0;
  --accent: #a970e8;
  --accent-2: #c6a3f0;
  --accent-soft: rgba(169, 112, 232, 0.16);
  --on-accent: #130826;
  --card-border: 1px solid transparent;
  color-scheme: dark;
}

/* Celestial eclipse — a quiet deep-space indigo with one champagne-gold
   accent. The old bright-gold-against-cool-lavender clash and the
   radiating-dot corona are both gone (see ::before below). */
body[data-theme="celestial-eclipse"],
[data-theme-scope="celestial-eclipse"] {
  --bg: #07060f;
  --surface: #14121f;
  --surface-2: #1e1b2c;
  --surface-3: color-mix(in srgb, var(--surface-2) 80%, var(--text));
  --border: color-mix(in srgb, var(--text) 12%, transparent);
  --border-strong: color-mix(in srgb, var(--text) 24%, transparent);
  --text: #eeecf6;
  --muted: #9a96b2;
  --accent: #cebd91;
  --accent-2: #e0d4b4;
  --accent-soft: rgba(206, 189, 145, 0.14);
  --on-accent: #191408;
  --card-border: 1px solid transparent;
  color-scheme: dark;
}

/* ---- LIMITED / ULTRA — must visibly out-class everything ---------- */

/* Halloween — purple + orange, an unstable double-pulse, flickering
   embers. */
body[data-theme="halloween"],
[data-theme-scope="halloween"] {
  --bg: #0d0221;
  --surface: #1b0c30;
  --surface-2: #261142;
  --surface-3: color-mix(in srgb, var(--surface-2) 80%, var(--text));
  --border: color-mix(in srgb, var(--text) 12%, transparent);
  --border-strong: color-mix(in srgb, var(--text) 24%, transparent);
  --text: #ffeadb;
  --muted: #bb9ecf;
  --accent: #ff7a1a;
  --accent-2: #a55ce0;
  --accent-soft: rgba(255, 122, 26, 0.14);
  --on-accent: #240c00;
  --pill-active-bg: transparent;
  --pill-active-fg: var(--accent);
  --pill-active-ring: inset 0 0 0 1px var(--accent), 0 0 14px -3px var(--accent);
  color-scheme: dark;
}

/* Christmas — pine green, string lights along the top, a faint snow. */
body[data-theme="christmas"],
[data-theme-scope="christmas"] {
  --bg: #071c11;
  --surface: #0d2b1a;
  --surface-2: #123720;
  --surface-3: color-mix(in srgb, var(--surface-2) 80%, var(--text));
  --border: color-mix(in srgb, var(--text) 12%, transparent);
  --border-strong: color-mix(in srgb, var(--text) 24%, transparent);
  --text: #f2f8f0;
  --muted: #92b19e;
  --accent: #ff5a5a;
  --accent-2: #f5c451;
  --accent-soft: rgba(255, 90, 90, 0.14);
  --on-accent: #260404;
  color-scheme: dark;
}

/* Prestige gold — make it feel like money. Near-black, real gold, a
   brushed-gold hairline frame, a slow specular sweep across the
   surfaces themselves, warm-tinted shadows. */
body[data-theme="prestige-gold"],
[data-theme-scope="prestige-gold"] {
  --bg: #0a0703;
  --surface: #18130a;
  --surface-2: #221b0d;
  --surface-3: color-mix(in srgb, var(--surface-2) 80%, var(--text));
  --border: color-mix(in srgb, var(--text) 12%, transparent);
  --border-strong: color-mix(in srgb, var(--text) 24%, transparent);
  --text: #fdf6df;
  --muted: #c6ad74;
  --accent: #e6c458;
  --accent-2: #f7e6a8;
  --accent-soft: rgba(230, 196, 88, 0.14);
  --on-accent: #2a1d00;
  --card-border: 1px solid transparent;
  --pill-active-bg: linear-gradient(180deg, #f4dd8f, #d8b348);
  --pill-active-fg: #2a1d00;
  --pill-active-ring: inset 0 1px 0 rgba(255, 255, 255, 0.4);
  --shadow-sm: 0 1px 3px rgba(0, 0, 0, 0.5), 0 0 0 1px rgba(230, 196, 88, 0.06), inset 0 1px 0 rgba(255, 255, 255, 0.05);
  --shadow-md: 0 12px 34px -12px rgba(0, 0, 0, 0.6), 0 0 24px -12px rgba(230, 196, 88, 0.25), inset 0 1px 0 rgba(255, 255, 255, 0.05);
  color-scheme: dark;
}

/* Founder — indigo/pink launch theme, diagonal launch streaks + stars,
   a gradient frame. */
body[data-theme="founder"],
[data-theme-scope="founder"] {
  --bg: #0a0c12;
  --surface: #141824;
  --surface-2: #1c2130;
  --surface-3: color-mix(in srgb, var(--surface-2) 80%, var(--text));
  --border: color-mix(in srgb, var(--text) 12%, transparent);
  --border-strong: color-mix(in srgb, var(--text) 24%, transparent);
  --text: #eef1ff;
  --muted: #9aa4c6;
  --accent: #7c8bff;
  --accent-2: #f472b6;
  --accent-soft: rgba(124, 139, 255, 0.16);
  --on-accent: #0a0e22;
  --card-border: 1px solid transparent;
  color-scheme: dark;
}

/* Obsidian — the anti-prestige-gold. Near-black, a cool platinum accent,
   a faint steel gradient frame, no colour, a slow platinum sweep.
   Understated luxury. */
body[data-theme="obsidian"],
[data-theme-scope="obsidian"] {
  --bg: #06070b;
  --surface: #10121a;
  --surface-2: #171922;
  --surface-3: color-mix(in srgb, var(--surface-2) 80%, var(--text));
  --border: color-mix(in srgb, var(--text) 12%, transparent);
  --border-strong: color-mix(in srgb, var(--text) 24%, transparent);
  --text: #eef0f6;
  --muted: #8b8f9e;
  --accent: #cdd6e8;
  --accent-2: #9aabc6;
  --accent-soft: rgba(205, 214, 232, 0.1);
  --on-accent: #0c0e14;
  --card-border: 1px solid transparent;
  --pill-active-bg: linear-gradient(180deg, #e6ebf5, #b7c0d4);
  --pill-active-fg: #0c0e14;
  --pill-active-ring: inset 0 1px 0 rgba(255, 255, 255, 0.5);
  color-scheme: dark;
}

/* =========================================================
   CHARACTER-KIT SUPPORT
   The bits a theme's token block can't express on its own: the
   animated gradient frame for premium/limited, per-theme caret
   shape, grain/scanline textures. Everything below is opt-in per
   theme — the default look never sees any of it. Every theme
   shares the same --radius-*/--shadow-*/font tokens (no per-theme
   overrides) so the shell/layout never shifts shape when you
   switch themes — only colour and these opt-in accents do.
========================================================= */

/* Gradient frame — for the five premium/limited themes whose token block
   set --card-border:1px solid transparent. A gradient painted into just
   the 1px border via the standard double-mask trick.
   v2 — prism-storm/celestial-eclipse/founder originally rotated a conic-
   gradient here via `transform: rotate()` on this masked pseudo-element;
   on screen that produced hard, bright diagonal lines slicing across the
   card instead of a ring (confirmed — a plain rotation transform on a
   masked element is unreliable across engines). prestige-gold/obsidian
   used a static linear-gradient with no transform and always rendered
   correctly, so the fix is to standardise on that proven approach for
   all five: a static soft sheen, no rotation. */
body[data-theme="prism-storm"] .card,
body[data-theme="celestial-eclipse"] .card,
body[data-theme="founder"] .card,
body[data-theme="prism-storm"] #word-container,
body[data-theme="celestial-eclipse"] #word-container,
body[data-theme="founder"] #word-container {
  position: relative;
}

body[data-theme="prism-storm"] .card::after,
body[data-theme="celestial-eclipse"] .card::after,
body[data-theme="founder"] .card::after,
body[data-theme="prestige-gold"] .card::after,
body[data-theme="obsidian"] .card::after {
  content: "";
  position: absolute;
  inset: -1px;
  border-radius: inherit;
  padding: 1px;
  pointer-events: none;
  -webkit-mask: linear-gradient(#000 0 0) content-box, linear-gradient(#000 0 0);
  -webkit-mask-composite: xor;
          mask-composite: exclude;
}

body[data-theme="prism-storm"] .card::after {
  background: linear-gradient(135deg, rgba(169, 112, 232, 0.7), rgba(169, 112, 232, 0.12) 45%, rgba(198, 163, 240, 0.6));
}
body[data-theme="celestial-eclipse"] .card::after {
  background: linear-gradient(135deg, rgba(206, 189, 145, 0.65), rgba(206, 189, 145, 0.1) 45%, rgba(224, 212, 180, 0.5));
}
body[data-theme="founder"] .card::after {
  background: linear-gradient(135deg, rgba(124, 139, 255, 0.7), rgba(244, 114, 182, 0.2) 45%, rgba(244, 114, 182, 0.6));
}
body[data-theme="prestige-gold"] .card::after {
  background: linear-gradient(135deg, rgba(247,230,168,0.7), rgba(230,196,88,0.15) 40%, rgba(247,230,168,0.7));
}
body[data-theme="obsidian"] .card::after {
  background: linear-gradient(135deg, rgba(230,235,245,0.5), rgba(150,171,198,0.08) 45%, rgba(230,235,245,0.4));
}

/* (matrix / cyber-pulse used to force a block caret here; the caret
   shape is a user preference now — Customize → Display → Caret shape —
   so a theme no longer overrides it. Anyone who wants a terminal block
   on those themes just picks "Block".) */

/* (Grain / weave textures for volcanic-ash, autumn-harvest and linen are
   folded into each of those themes' own body::before layers further
   down — body::before is a child of body so it paints over the opaque
   body background, which a fixed html-level layer cannot.) */

/* Reusable inline-SVG grain (greyscale, low alpha), referenced by the
   themes that want a paper/ash texture. */
/* v2 — 0.4 alpha read as visible static/noise rather than a paper
   texture, especially on volcanic-ash's dark surfaces (high-contrast
   flecks against a dark ground read as a broken image, not grain).
   Alpha cut to a barely-there 0.05 so it still breaks up flat colour on
   close inspection without registering as texture on its own. */
:root { --grain-svg: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='160' height='160'%3E%3Cfilter id='n'%3E%3CfeTurbulence type='fractalNoise' baseFrequency='0.82' numOctaves='2' stitchTiles='stitch'/%3E%3CfeColorMatrix type='saturate' values='0'/%3E%3C/filter%3E%3Crect width='100%25' height='100%25' filter='url(%23n)' opacity='0.05'/%3E%3C/svg%3E"); }

/* Midnight's own token comment already says "night sky over water" but
   had zero decorative layer to back that up, so next to Focus Dark it
   just read as "a slightly different blue." A soft moon glow tucked in a
   corner and a handful of sparse pale stars (far fewer than Galaxy's
   dense field — this is a quiet theme). v2 — dropped the diagonal
   "shimmer band" that swept left-to-right across everything; it read as
   a glitch passing over the page, and it rode on a `filter` on <body>
   that broke position:fixed anyway. Just the moon glow + stars now,
   completely static. */
body[data-theme="midnight"]::before {
  content: "";
  position: fixed;
  inset: 0;
  z-index: 0;
  pointer-events: none;
  background-image:
    radial-gradient(circle at 14% 12%, rgba(217, 226, 255, 0.35), rgba(138, 176, 255, 0.12) 32%, transparent 58%),
    radial-gradient(1.5px 1.5px at 30% 25%, #d9e2ff, transparent),
    radial-gradient(1px 1px at 60% 15%, #d9e2ff, transparent),
    radial-gradient(1.5px 1.5px at 80% 30%, #d9e2ff, transparent),
    radial-gradient(1px 1px at 45% 8%, #d9e2ff, transparent);
  background-size: 100% 100%, 600px 600px, 600px 600px, 600px 600px, 600px 600px;
  background-repeat: no-repeat, repeat, repeat, repeat, repeat;
}

body[data-theme="aurora"] {
  background:
    radial-gradient(circle at 15% 20%, rgba(167, 139, 250, 0.28), transparent 45%),
    radial-gradient(circle at 85% 15%, rgba(45, 212, 191, 0.22), transparent 40%),
    radial-gradient(circle at 50% 100%, rgba(124, 58, 237, 0.2), transparent 50%),
    var(--bg);
  background-size: 200% 200%;
  animation: auroraDrift 18s ease infinite;
}

@keyframes auroraDrift {
  0% { background-position: 0% 50%; }
  50% { background-position: 100% 50%; }
  100% { background-position: 0% 50%; }
}

body[data-theme="galaxy"] {
  background: radial-gradient(circle at top, #1c1440, #0a0817 65%);
  overflow-x: hidden;
}

body[data-theme="galaxy"]::before {
  content: "";
  position: fixed;
  inset: 0;
  z-index: 0;
  pointer-events: none;
  background-image:
    radial-gradient(1px 1px at 20% 30%, white, transparent),
    radial-gradient(1px 1px at 70% 60%, white, transparent),
    radial-gradient(1.5px 1.5px at 40% 80%, white, transparent),
    radial-gradient(1px 1px at 90% 20%, white, transparent),
    radial-gradient(1px 1px at 55% 10%, white, transparent);
  background-size: 600px 600px;
  opacity: 0.5;
  animation: driftStars 60s linear infinite;
}

@keyframes driftStars {
  from { background-position: 0 0; }
  to { background-position: -600px 600px; }
}

/* v3 — the old rain layer was a `to bottom` repeating-linear-gradient,
   which has no horizontal variation, so every "column" was identical
   and they merged into solid full-width green BANDS marching down the
   page — nothing like rain. Rebuilt with the discrete-glyph technique
   the other particle themes use: short bright vertical dashes at
   scattered x-positions on a tall tile, plus a fainter trailing set,
   falling on a loop. A faint CRT scanline sits under it, static. */
body[data-theme="matrix"]::before {
  content: "";
  position: fixed;
  inset: 0;
  z-index: 0;
  pointer-events: none;
  /* Digital rain — a few faint falling streaks over a static CRT
     scanline. v4 — pulled back to five slower, dimmer columns; eight
     fast bright ones read as "a bit much" over the typing area. */
  background-image:
    repeating-linear-gradient(to bottom, rgba(0, 255, 65, 0.03) 0 1px, transparent 1px 3px),
    radial-gradient(2px 150px at 12% 30%, rgba(125,255,160,0.5), rgba(34,227,74,0.1) 55%, transparent 80%),
    radial-gradient(2px 190px at 34% 68%, rgba(34,227,74,0.45), rgba(34,227,74,0.08) 55%, transparent 80%),
    radial-gradient(2px 170px at 56% 22%, rgba(125,255,160,0.5), rgba(34,227,74,0.1) 55%, transparent 80%),
    radial-gradient(2px 200px at 74% 52%, rgba(34,227,74,0.45), rgba(34,227,74,0.08) 55%, transparent 80%),
    radial-gradient(2px 140px at 91% 34%, rgba(125,255,160,0.5), rgba(34,227,74,0.1) 55%, transparent 80%);
  background-repeat: repeat-y, repeat, repeat, repeat, repeat, repeat;
  background-size:
    auto,
    640px 640px, 900px 900px, 640px 640px, 900px 900px, 640px 640px;
  opacity: 0.7;
  animation: matrixRain 6s linear infinite;
}

@keyframes matrixRain {
  from { background-position: 0 0, 0 0, 0 0, 0 0, 0 0, 0 0; }
  to   { background-position: 0 0, 0 640px, 0 900px, 0 640px, 0 900px, 0 640px; }
}

body[data-theme="cyber-pulse"]::before {
  content: "";
  position: fixed;
  inset: 0;
  z-index: 0;
  pointer-events: none;
  background: repeating-linear-gradient(
    to bottom,
    rgba(6, 247, 214, 0.03) 0px,
    rgba(6, 247, 214, 0.03) 1px,
    transparent 1px,
    transparent 3px
  );
}

body[data-theme="lava-flow"] {
  background:
    radial-gradient(circle at 20% 92%, rgba(224, 86, 31, 0.2), transparent 52%),
    radial-gradient(circle at 82% 6%, rgba(224, 86, 31, 0.1), transparent 46%),
    var(--bg);
  background-size: 160% 160%;
  animation: lavaPulse 12s ease-in-out infinite;
}

@keyframes lavaPulse {
  0%, 100% { background-position: 0% 0%; }
  50% { background-position: 24% 16%; }
}

/* A few small ember sparks rising and fading. v2 — recoloured to the
   single deep-ember accent (was bright orange + gold) and dimmed. */
body[data-theme="lava-flow"]::before {
  content: "";
  position: fixed;
  inset: 0;
  z-index: 0;
  pointer-events: none;
  background-image:
    radial-gradient(2px 2px at 15% 95%, #e0561f, transparent),
    radial-gradient(1.5px 1.5px at 40% 85%, #ef8a52, transparent),
    radial-gradient(2px 2px at 65% 92%, #e0561f, transparent),
    radial-gradient(1.5px 1.5px at 85% 80%, #ef8a52, transparent),
    radial-gradient(1.5px 1.5px at 25% 70%, #e0561f, transparent);
  background-size: 300px 300px;
  animation: lavaEmberRise 9s ease-in infinite;
}

@keyframes lavaEmberRise {
  0% { opacity: 0; background-position: 0 0; }
  25% { opacity: 0.5; }
  100% { opacity: 0; background-position: 8px -300px; }
}

/* Petals fall and sway from side to side, not an in-place diagonal
   twinkle-drift — driftStars (also used by Galaxy's actual starfield)
   made cherry blossoms move exactly like distant stars.
   v2 — the dots were a pale pink disconnected from the theme's actual
   accent (#d81b60) and its "bamboo-green foliage" half (#128a4d), so the
   effect read as generic pink confetti rather than this specific theme's
   palette. Deepened toward the real accent pink, mixed in a couple of
   small green leaf specks for that green half, and varied the dot
   aspect ratio (elliptical, not perfect circles) for a less uniform
   polka-dot look. */
/* Petals drifting down. v3 — dropped the bamboo-green specks (the green
   half of the palette is gone) and softened the pink toward the new
   dusty accent so it reads as blossom, not confetti. */
body[data-theme="sakura"]::before {
  content: "";
  position: fixed;
  inset: 0;
  z-index: 0;
  pointer-events: none;
  background-image:
    radial-gradient(4px 3px at 15% 20%, #dda6b8, transparent),
    radial-gradient(3px 2.5px at 70% 10%, #e8c3ce, transparent),
    radial-gradient(3.5px 3px at 40% 60%, #dda6b8, transparent),
    radial-gradient(2.5px 2px at 85% 70%, #e8c3ce, transparent),
    radial-gradient(3px 2.5px at 55% 85%, #dda6b8, transparent);
  background-size: 500px 500px;
  opacity: 0.45;
  animation: sakuraPetalFall 24s linear infinite;
}

@keyframes sakuraPetalFall {
  0%   { background-position: 0px 0px; }
  25%  { background-position: 15px 125px; }
  50%  { background-position: -10px 250px; }
  75%  { background-position: 10px 375px; }
  100% { background-position: 0px 500px; }
}

/* var(--text) instead of hardcoded white — same fix the equippable
   Snowfall background effect already uses (see BACKGROUND EFFECTS below):
   white dots would nearly disappear against Frost's own pale icy-blue
   background. Sway-and-fall like Sakura's petals rather than a straight
   vertical drift, so it reads as snow tumbling in the wind. */
body[data-theme="frost"]::before {
  content: "";
  position: fixed;
  inset: 0;
  z-index: 0;
  pointer-events: none;
  background-image:
    radial-gradient(3px 3px at 15% 15%, var(--text), transparent),
    radial-gradient(2.5px 2.5px at 65% 10%, var(--text), transparent),
    radial-gradient(3px 3px at 40% 55%, var(--text), transparent),
    radial-gradient(2px 2px at 85% 65%, var(--text), transparent),
    radial-gradient(2.5px 2.5px at 55% 80%, var(--text), transparent),
    radial-gradient(2px 2px at 10% 85%, var(--text), transparent);
  background-size: 460px 460px;
  opacity: 0.4;
  animation: frostSnowfall 16s linear infinite;
}

@keyframes frostSnowfall {
  0%   { background-position: 0px 0px; }
  25%  { background-position: 12px 115px; }
  50%  { background-position: -8px 230px; }
  75%  { background-position: 10px 345px; }
  100% { background-position: 0px 460px; }
}

/* A single warm ember wash from the top — no animation. The old
   brightness-pulse was a `filter` on <body>, which makes <body> the
   containing block for position:fixed, so the modal/sticky header
   silently shifted while it was equipped. */
body[data-theme="solar-flare"] {
  background:
    radial-gradient(circle at 50% -15%, rgba(184, 72, 31, 0.16), transparent 55%),
    var(--bg);
}

/* A faster, asymmetric double-pulse instead of Lava Flow's smooth
   symmetric breathing — feels unstable/ominous rather than molten. */
body[data-theme="halloween"] {
  background:
    radial-gradient(circle at 20% 100%, rgba(255, 122, 26, 0.28), transparent 50%),
    radial-gradient(circle at 80% 0%, rgba(157, 78, 221, 0.3), transparent 45%),
    var(--bg);
  background-size: 160% 160%;
  animation: halloweenPulse 4s ease-in-out infinite;
}

/* Position-only — the brightness pulse this used to carry was a `filter`
   on <body>, which broke position:fixed containing-block behaviour. */
@keyframes halloweenPulse {
  0%, 100% { background-position: 0% 0%; }
  30% { background-position: 15% 10%; }
  65% { background-position: 25% 15%; }
}

/* An irregular flicker/jitter, not a smooth diagonal drift — driftStars
   made these embers twinkle exactly like Galaxy's calm starfield. */
body[data-theme="halloween"]::before {
  content: "";
  position: fixed;
  inset: 0;
  z-index: 0;
  pointer-events: none;
  background-image:
    radial-gradient(2px 2px at 15% 25%, #ff9640, transparent),
    radial-gradient(1.5px 1.5px at 70% 55%, #ffb066, transparent),
    radial-gradient(2px 2px at 40% 80%, #ff9640, transparent),
    radial-gradient(1.5px 1.5px at 88% 15%, #9d4edd, transparent),
    radial-gradient(2px 2px at 55% 5%, #ffb066, transparent);
  background-size: 550px 550px;
  animation: halloweenFlicker 3s linear infinite;
}

@keyframes halloweenFlicker {
  0%, 100% { opacity: 0.55; background-position: 0 0; }
  15% { opacity: 0.3; }
  30% { opacity: 0.65; background-position: 4px -6px; }
  45% { opacity: 0.25; }
  60% { opacity: 0.6; background-position: -3px 5px; }
  80% { opacity: 0.35; }
}

body[data-theme="christmas"]::before {
  content: "";
  position: fixed;
  top: 0;
  left: 0;
  right: 0;
  height: 10px;
  z-index: 0;
  pointer-events: none;
  background-image:
    radial-gradient(3px 3px at 5% 50%, #ff4d4d, transparent),
    radial-gradient(3px 3px at 15% 50%, #facc15, transparent),
    radial-gradient(3px 3px at 25% 50%, #16a34a, transparent),
    radial-gradient(3px 3px at 35% 50%, #ff4d4d, transparent),
    radial-gradient(3px 3px at 45% 50%, #facc15, transparent),
    radial-gradient(3px 3px at 55% 50%, #16a34a, transparent),
    radial-gradient(3px 3px at 65% 50%, #ff4d4d, transparent),
    radial-gradient(3px 3px at 75% 50%, #facc15, transparent),
    radial-gradient(3px 3px at 85% 50%, #16a34a, transparent),
    radial-gradient(3px 3px at 95% 50%, #ff4d4d, transparent);
  background-size: 100% 10px;
  animation: christmasTwinkle 1.6s ease-in-out infinite;
}

@keyframes christmasTwinkle {
  0%, 100% { opacity: 1; }
  50% { opacity: 0.4; }
}

body[data-theme="prestige-gold"] {
  background:
    linear-gradient(120deg, transparent 30%, rgba(253, 224, 71, 0.14) 45%, transparent 60%),
    var(--bg);
  background-size: 300% 300%;
  animation: goldShimmer 6s ease-in-out infinite;
}

@keyframes goldShimmer {
  0% { background-position: 0% 50%; }
  50% { background-position: 100% 50%; }
  100% { background-position: 0% 50%; }
}

/* v2 — a diagonal linear-gradient "streak" painted onto a 260%-wide
   canvas projects as a hard, thin, cross-screen line once it's sliced to
   a normal viewport (confirmed on screen, not just in code) — it read as
   a glitch, not a launch trail. Corner-anchored soft blobs (the same
   technique aurora/lava-flow/halloween already use successfully) plus
   the starfield instead: no straight edge anywhere, so nothing can read
   as a "line". */
body[data-theme="founder"]::before {
  content: "";
  position: fixed;
  inset: 0;
  z-index: 0;
  pointer-events: none;
  background-image:
    radial-gradient(circle at 10% 0%, rgba(124, 139, 255, 0.22), transparent 42%),
    radial-gradient(circle at 100% 85%, rgba(244, 114, 182, 0.16), transparent 45%),
    radial-gradient(1px 1px at 20% 30%, white, transparent),
    radial-gradient(1px 1px at 70% 60%, white, transparent),
    radial-gradient(1.5px 1.5px at 40% 80%, white, transparent),
    radial-gradient(1px 1px at 90% 20%, white, transparent);
  background-size: 100% 100%, 100% 100%, 600px 600px, 600px 600px, 600px 600px, 600px 600px;
  background-repeat: no-repeat, no-repeat, repeat, repeat, repeat, repeat;
  opacity: 0.6;
  animation: founderLaunch 9s linear infinite;
}

@keyframes founderLaunch {
  0%   { background-position: 0 0, 0 0, 0 0, 0 0, 0 0, 0 0; filter: brightness(1); }
  50%  { background-position: 0 0, 0 0, 0 0, 0 0, 0 0, 0 0; filter: brightness(1.18); }
  100% { background-position: 0 0, 0 0, -600px 600px, -600px 600px, -600px 600px, -600px 600px; filter: brightness(1); }
}

/* A slow, subtle platinum sweep — restrained compared to Prestige Gold's
   shimmer, since Obsidian is meant to read as understated, not flashy. */
body[data-theme="obsidian"] {
  background:
    linear-gradient(135deg, transparent 40%, rgba(201, 210, 228, 0.05) 50%, transparent 60%),
    var(--bg);
  background-size: 260% 260%;
  animation: obsidianSweep 10s ease-in-out infinite;
}

@keyframes obsidianSweep {
  0%, 100% { background-position: 0% 0%; }
  50% { background-position: 100% 100%; }
}

body[data-theme="deep-ocean"]::before {
  content: "";
  position: fixed;
  inset: 0;
  z-index: 0;
  pointer-events: none;
  background-image:
    radial-gradient(3px 3px at 20% 90%, rgba(45, 212, 191, 0.45), transparent),
    radial-gradient(2px 2px at 50% 80%, rgba(96, 165, 250, 0.4), transparent),
    radial-gradient(2.5px 2.5px at 75% 95%, rgba(45, 212, 191, 0.45), transparent),
    radial-gradient(2px 2px at 90% 70%, rgba(96, 165, 250, 0.4), transparent);
  background-size: 300px 300px;
  animation: oceanBubbles 10s linear infinite;
}

@keyframes oceanBubbles {
  from { background-position: 0 0, 0 0, 0 0, 0 0; }
  to { background-position: 20px -300px, -15px -300px, 10px -300px, -20px -300px; }
}

/* Paper-fibre grain on the body itself + leaves drifting down on ::before. */
body[data-theme="autumn-harvest"] {
  background-image: var(--grain-svg);
  background-size: 160px 160px;
  background-attachment: fixed;
}

body[data-theme="autumn-harvest"]::before {
  content: "";
  position: fixed;
  inset: 0;
  z-index: 0;
  pointer-events: none;
  background-image:
    radial-gradient(3px 3px at 15% 10%, #a63d1f, transparent),
    radial-gradient(2.5px 2.5px at 45% 25%, #5c6b2f, transparent),
    radial-gradient(3px 3px at 70% 5%, #a63d1f, transparent),
    radial-gradient(2.5px 2.5px at 90% 30%, #5c6b2f, transparent);
  background-size: 280px 280px;
  opacity: 0.5;
  animation: leavesFall 9s linear infinite;
}

@keyframes leavesFall {
  from { background-position: 0 0; }
  to { background-position: 20px 280px; }
}

/* Zero decorative layer previously, which felt static/flat for a nature
   theme next to its falling-effect siblings (Sakura, Autumn Harvest,
   Frost). Pollen/dandelion seeds drift UP and sway — deliberately the
   opposite direction from every "falling" theme, so Meadow reads as its
   own thing rather than a green reskin of them. Warm wheat-gold instead
   of white/cream so the dots actually show up against the pale
   green-white background. */
body[data-theme="meadow"]::before {
  content: "";
  position: fixed;
  inset: 0;
  z-index: 0;
  pointer-events: none;
  /* v2 — dots recoloured from wheat-gold to a soft sage now the gold
     half of the palette is gone, and opacity pulled down. */
  background-image:
    radial-gradient(2.5px 2.5px at 20% 90%, #9cc088, transparent),
    radial-gradient(2px 2px at 45% 75%, #9cc088, transparent),
    radial-gradient(2.5px 2.5px at 70% 95%, #9cc088, transparent),
    radial-gradient(1.5px 1.5px at 85% 60%, #9cc088, transparent),
    radial-gradient(2px 2px at 10% 55%, #9cc088, transparent);
  background-size: 420px 420px;
  opacity: 0.4;
  animation: meadowPollenDrift 22s linear infinite;
}

@keyframes meadowPollenDrift {
  0%   { background-position: 0px 0px; }
  25%  { background-position: 10px -105px; }
  50%  { background-position: -8px -210px; }
  75%  { background-position: 8px -315px; }
  100% { background-position: 0px -420px; }
}

body[data-theme="neon-tokyo"]::before {
  content: "";
  position: fixed;
  inset: 0;
  z-index: 0;
  pointer-events: none;
  background-image:
    repeating-linear-gradient(90deg, rgba(255, 46, 136, 0.08) 0px, transparent 2px, transparent 60px),
    repeating-linear-gradient(0deg, rgba(255, 176, 32, 0.06) 0px, transparent 1px, transparent 40px);
  animation: neonFlicker 3s ease-in-out infinite;
}

@keyframes neonFlicker {
  0%, 100% { opacity: 0.7; }
  50% { opacity: 1; }
}

body[data-theme="volcanic-ash"]::before {
  content: "";
  position: fixed;
  inset: 0;
  z-index: 0;
  pointer-events: none;
  background-image:
    radial-gradient(2.5px 2.5px at 20% 85%, #fb5c2d, transparent),
    radial-gradient(2px 2px at 50% 70%, #c2410c, transparent),
    radial-gradient(2.5px 2.5px at 75% 90%, #fb5c2d, transparent),
    radial-gradient(2px 2px at 90% 60%, #c2410c, transparent);
  background-size: 260px 260px;
  filter: drop-shadow(0 0 1.5px rgba(0, 0, 0, 0.5));
  animation: emberRise 7s ease-in-out infinite;
}

/* Static charcoal grain on the body itself (behind body::before's
   embers) — body's background is opaque so this needs to be a layer on
   body, not a fixed element behind it. */
body[data-theme="volcanic-ash"] {
  background-image: var(--grain-svg);
  background-size: 160px 160px;
  background-attachment: fixed;
}

@keyframes emberRise {
  0%, 100% { opacity: 0.4; background-position: 0 0; }
  50% { opacity: 0.75; background-position: 10px -40px; }
}

/* Linen had zero decorative layer at all, which made it read as flat and
   bland next to every other theme's motion — a woven crosshatch texture
   (not particles/motion, this is a "common"-tier fabric theme) gives it
   some life while staying restrained. Two low-opacity diagonal line sets
   at ±45° in var(--border) so it always matches the theme's own palette. */
body[data-theme="linen"]::before {
  content: "";
  position: fixed;
  inset: 0;
  z-index: 0;
  pointer-events: none;
  background-image:
    repeating-linear-gradient(45deg, var(--border) 0px, var(--border) 1px, transparent 1px, transparent 9px),
    repeating-linear-gradient(-45deg, var(--border) 0px, var(--border) 1px, transparent 1px, transparent 9px);
  opacity: 0.35;
  animation: linenWeaveShift 24s ease-in-out infinite;
}

@keyframes linenWeaveShift {
  0%, 100% { background-position: 0 0, 0 0; opacity: 0.28; }
  50% { background-position: 6px 6px, -6px 6px; opacity: 0.4; }
}

/* A gentle vertical bob, like petals floating on water, rather than the
   one-directional diagonal drift driftStars gives every other user of
   it (Galaxy's stars, now Sakura's falling petals).
   v3 — dropped the warm-gold glint dots (the gold half of the palette is
   gone) and the brightness flicker; just soft mauve motes drifting now. */
body[data-theme="rose-quartz"]::before {
  content: "";
  position: fixed;
  inset: 0;
  z-index: 0;
  pointer-events: none;
  background-image:
    radial-gradient(3px 3px at 20% 15%, #e4d2d6, transparent),
    radial-gradient(2.5px 2.5px at 65% 25%, #ecdfe1, transparent),
    radial-gradient(3px 3px at 35% 65%, #e4d2d6, transparent),
    radial-gradient(2px 2px at 80% 75%, #ecdfe1, transparent),
    radial-gradient(2.5px 2.5px at 50% 90%, #e4d2d6, transparent);
  background-size: 480px 480px;
  animation: roseQuartzFloat 8s ease-in-out infinite;
}

@keyframes roseQuartzFloat {
  0%, 100% { background-position: 0px 0px; opacity: 0.35; }
  50% { background-position: 0px -16px; opacity: 0.5; }
}

/* A quick, punchy "zing" — brightness, saturation, and a slight
   yellow-to-lime hue shift — instead of Solar Flare's slow pure-
   brightness breathing. Citrus should feel zesty, not molten. */
/* A soft honey wash — no animation. The old `citrusZing` was a
   brightness/saturate/hue-rotate `filter` on <body>: it made <body> the
   containing block for position:fixed, which is what was shifting the
   theme picker / modal around while this theme was equipped. */
body[data-theme="citrus-grove"] {
  background:
    radial-gradient(circle at 50% -15%, rgba(165, 113, 15, 0.12), transparent 55%),
    var(--bg);
}

/* A gentle radial "breathing bloom" (the clouds expand/soften in place)
   instead of Aurora's directional drift — suits a soft pastel theme
   better than a cosmic sweep. */
/* A single soft lavender wash — static. (Old version breathed via a
   `filter` on <body>, which broke position:fixed.) */
body[data-theme="lilac-bloom"] {
  background:
    radial-gradient(circle at 18% 12%, rgba(109, 85, 176, 0.1), transparent 46%),
    radial-gradient(circle at 82% 100%, rgba(109, 85, 176, 0.08), transparent 48%),
    var(--bg);
}

/* Slow-drifting lavender petals — the only motion this theme has now. */
body[data-theme="lilac-bloom"]::before {
  content: "";
  position: fixed;
  inset: 0;
  z-index: 0;
  pointer-events: none;
  background-image:
    radial-gradient(3px 2.5px at 18% 18%, #cbbdea, transparent),
    radial-gradient(2.5px 2px at 62% 12%, #bfb0e2, transparent),
    radial-gradient(3px 2.5px at 38% 58%, #cbbdea, transparent),
    radial-gradient(2px 2px at 82% 68%, #bfb0e2, transparent),
    radial-gradient(2.5px 2px at 55% 88%, #cbbdea, transparent);
  background-size: 520px 520px;
  opacity: 0.4;
  animation: lilacPetalDrift 26s linear infinite;
}

@keyframes lilacPetalDrift {
  0%   { background-position: 0px 0px; }
  25%  { background-position: -14px 130px; }
  50%  { background-position: 10px 260px; }
  75%  { background-position: -10px 390px; }
  100% { background-position: 0px 520px; }
}

/* A slow one-directional rise (85% of the cycle) then a quick reset,
   mimicking an actual sunrise, instead of Lava Flow's symmetric
   in-out breathing pulse. */
body[data-theme="daybreak"] {
  background:
    radial-gradient(circle at 22% 92%, rgba(194, 65, 12, 0.22), transparent 52%),
    radial-gradient(circle at 82% 6%, rgba(224, 138, 82, 0.16), transparent 46%),
    var(--bg);
  background-size: 160% 160%;
  animation: daybreakRise 12s ease-in-out infinite;
}

/* Position-only — the brightness ramp this used to carry was a `filter`
   on <body>, which broke position:fixed containing-block behaviour. */
@keyframes daybreakRise {
  0%, 100% { background-position: 20% 100%, 80% 30%; }
  85% { background-position: 20% 40%, 80% -20%; }
}

/* v3 — the multi-hue version (pink + cyan + violet blobs, hue-cycling)
   was the "colours are off, makes the test feel unsettled" note. Now a
   single deep-amethyst glow in two corners with a slow, narrow hue
   drift (violet↔magenta only, ±30°) for a faint iridescence — no full
   spectrum, nothing that pulls focus off the passage. */
body[data-theme="prism-storm"]::before {
  content: "";
  position: fixed;
  inset: 0;
  z-index: 0;
  pointer-events: none;
  background-image:
    radial-gradient(ellipse 60% 50% at 0% 0%, #a970e8, transparent 70%),
    radial-gradient(ellipse 55% 45% at 100% 100%, #8f5fd6, transparent 70%);
  filter: blur(70px);
  opacity: 0.22;
  animation: prismHueShift 20s ease-in-out infinite;
}

@keyframes prismHueShift {
  0%, 100% { filter: blur(70px) hue-rotate(-24deg); }
  50% { filter: blur(70px) hue-rotate(24deg); }
}

/* v4 — the tight cluster of gold/violet/cream blobs in one corner still
   read as a "radiating dot" stamped on the page. Replaced with one big,
   very soft champagne glow bleeding in from a corner (no visible centre,
   no rim) plus a sparse slow-drifting starfield. Nothing pulses. */
body[data-theme="celestial-eclipse"]::before {
  content: "";
  position: fixed;
  inset: 0;
  z-index: 0;
  pointer-events: none;
  background-image:
    radial-gradient(ellipse 70% 55% at 100% 0%, rgba(206, 189, 145, 0.16), transparent 70%),
    radial-gradient(1px 1px at 20% 30%, rgba(255,255,255,0.8), transparent),
    radial-gradient(1px 1px at 70% 60%, rgba(255,255,255,0.7), transparent),
    radial-gradient(1.5px 1.5px at 40% 80%, rgba(255,255,255,0.75), transparent),
    radial-gradient(1px 1px at 90% 25%, rgba(255,255,255,0.7), transparent),
    radial-gradient(1px 1px at 15% 65%, rgba(255,255,255,0.7), transparent),
    radial-gradient(1.5px 1.5px at 85% 85%, rgba(255,255,255,0.75), transparent);
  background-size: auto, 620px 620px, 620px 620px, 620px 620px, 620px 620px, 620px 620px, 620px 620px;
  background-repeat: no-repeat, repeat, repeat, repeat, repeat, repeat, repeat;
  animation: eclipseStarDrift 90s linear infinite;
}

@keyframes eclipseStarDrift {
  from { background-position: 0 0, 0 0, 0 0, 0 0, 0 0, 0 0, 0 0; }
  to   { background-position: 0 0, -310px 310px, -310px 310px, -310px 310px, -310px 310px, -310px 310px, -310px 310px; }
}

/* =========================================================
   BACKGROUND EFFECTS
   Matches ShopItem slugs (item_type="background"). Layered via
   body::after (themes already use ::before) so any background
   can be combined with any theme. No data-background = plain.
========================================================= */

body[data-background]::after {
  content: "";
  position: fixed;
  inset: 0;
  z-index: 0;
  pointer-events: none;
}

/* Small live preview inside the customize modal's swatch box — same
   background-image/animation as the real effect, just contained to a
   52px box instead of the full viewport, so equipping (or just browsing
   the picker) shows an unmistakable demo even on pages with no visible
   full-screen effect to compare against. */
.customize-swatch[data-background-demo] {
  position: relative;
  overflow: hidden;
}

.customize-swatch[data-background-demo]::after {
  content: "";
  position: absolute;
  inset: 0;
}

body[data-background="particles"]::after,
.customize-swatch[data-background-demo="particles"]::after {
  background-image:
    radial-gradient(3px 3px at 10% 90%, var(--accent), transparent),
    radial-gradient(3px 3px at 30% 80%, var(--accent-2), transparent),
    radial-gradient(2.5px 2.5px at 55% 95%, var(--accent), transparent),
    radial-gradient(3px 3px at 75% 85%, var(--accent-2), transparent),
    radial-gradient(2.5px 2.5px at 90% 92%, var(--accent), transparent),
    radial-gradient(2px 2px at 20% 40%, var(--accent-2), transparent),
    radial-gradient(2.5px 2.5px at 45% 15%, var(--accent), transparent),
    radial-gradient(2px 2px at 68% 55%, var(--accent-2), transparent);
  background-size: 220px 220px;
  opacity: 0.6;
  animation: particlesRise 9s linear infinite;
}

@keyframes particlesRise {
  from { background-position: 0 0; }
  to { background-position: 0 -220px; }
}

/* Individual falling streaks on a repeating tile — matches the technique
   every other background effect uses (particles/snowfall/starfield are
   all sparse discrete shapes on a large tile with a slow loop). The old
   version was a dense, screen-covering diagonal hatch with no tile at
   all, looping every 0.4s — that read as flickering noise, not rain. */
/* Two streak layers at different speeds/scales for real depth — a fast
   near layer and a slower, fainter far one. */
body[data-background="rain"]::after,
.customize-swatch[data-background-demo="rain"]::after {
  background-image:
    repeating-linear-gradient(94deg, transparent 0 34px,
      color-mix(in srgb, var(--accent) 55%, transparent) 34px 35.5px),
    repeating-linear-gradient(96deg, transparent 0 58px,
      color-mix(in srgb, var(--accent) 30%, transparent) 58px 59px);
  background-size: 90px 220px, 150px 340px;
  opacity: 0.5;
  animation: rainFall 1.1s linear infinite;
}

@keyframes rainFall {
  from { background-position: 0 0, 0 0; }
  to   { background-position: -18px 220px, -10px 340px; }
}

/* var(--text) instead of a hardcoded white — snow/stars need to flip to
   dark dots on light themes (frost, sakura, solar-flare) or they'd be
   nearly invisible against a light background. */
body[data-background="snowfall"]::after,
.customize-swatch[data-background-demo="snowfall"]::after {
  background-image:
    radial-gradient(4px 4px at 20% 10%, var(--text), transparent),
    radial-gradient(3px 3px at 60% 30%, var(--text), transparent),
    radial-gradient(4px 4px at 80% 5%, var(--text), transparent),
    radial-gradient(3px 3px at 40% 50%, var(--text), transparent),
    radial-gradient(3.5px 3.5px at 10% 60%, var(--text), transparent),
    radial-gradient(3px 3px at 90% 45%, var(--text), transparent),
    radial-gradient(2.5px 2.5px at 30% 75%, var(--text), transparent);
  background-size: 320px 320px;
  opacity: 0.5;
  animation: snowDrift 8s linear infinite;
}

@keyframes snowDrift {
  from { background-position: 0 0; }
  to { background-position: 32px 320px; }
}

body[data-background="starfield"]::after,
.customize-swatch[data-background-demo="starfield"]::after {
  background-image:
    radial-gradient(1.5px 1.5px at 15% 25%, var(--text), transparent),
    radial-gradient(1.5px 1.5px at 65% 55%, var(--text), transparent),
    radial-gradient(2px 2px at 35% 80%, var(--text), transparent),
    radial-gradient(1.5px 1.5px at 85% 15%, var(--text), transparent),
    radial-gradient(1.5px 1.5px at 50% 5%, var(--text), transparent),
    radial-gradient(2px 2px at 95% 75%, var(--text), transparent),
    radial-gradient(1.5px 1.5px at 25% 45%, var(--text), transparent),
    radial-gradient(1.5px 1.5px at 70% 90%, var(--text), transparent);
  background-size: 400px 400px;
  opacity: 0.55;
  animation: starTwinkle 3s ease-in-out infinite;
}

@keyframes starTwinkle {
  0%, 100% { opacity: 0.35; }
  50% { opacity: 0.85; }
}

/* A soft accent-2 spotlight now sweeps diagonally across the grid and
   the whole thing pulses brighter at the sweep's midpoint — the plain
   two-layer grid at a flat 0.28 opacity read as less elaborate than
   Confetti Burst, a legendary (one tier below mythical) background, so
   this needed real motion depth, not just a recolor. */
/* v3 — a brighter grid with a diagonal light-sweep passing over it, so
   it reads as an active surface, not a faint static mesh. */
body[data-background="animated-grid"]::after,
.customize-swatch[data-background-demo="animated-grid"]::after {
  background-image:
    linear-gradient(115deg, transparent 40%,
      color-mix(in srgb, var(--accent-2) 60%, transparent) 50%, transparent 60%),
    repeating-linear-gradient(0deg,  color-mix(in srgb, var(--accent) 45%, transparent) 0 1.5px, transparent 1.5px 40px),
    repeating-linear-gradient(90deg, color-mix(in srgb, var(--accent) 45%, transparent) 0 1.5px, transparent 1.5px 40px);
  background-size: 300% 100%, 40px 40px, 40px 40px;
  opacity: 0.5;
  animation: gridScroll 6s linear infinite;
}

@keyframes gridScroll {
  0%   { background-position: 120% 0, 0 0, 0 0; }
  100% { background-position: -120% 0, 40px 40px, 40px 40px; }
}

/* Darker amber + a drop-shadow safety net — same fix as The Vault below,
   the original pale #fde047/#fbbf24 had poor contrast against the
   default light theme's near-white background. */
body[data-background="fireflies"]::after,
.customize-swatch[data-background-demo="fireflies"]::after {
  background-image:
    radial-gradient(2.5px 2.5px at 15% 70%, #d97706, transparent),
    radial-gradient(2px 2px at 35% 30%, #92400e, transparent),
    radial-gradient(3px 3px at 55% 80%, #d97706, transparent),
    radial-gradient(2px 2px at 75% 20%, #92400e, transparent),
    radial-gradient(2.5px 2.5px at 90% 60%, #d97706, transparent),
    radial-gradient(2px 2px at 25% 45%, #92400e, transparent);
  background-size: 280px 280px;
  opacity: 0.75;
  filter: drop-shadow(0 0 1.5px rgba(0, 0, 0, 0.45));
  animation: firefliesDrift 6s ease-in-out infinite;
}

@keyframes firefliesDrift {
  0%, 100% { opacity: 0.5; background-position: 0 0; }
  50% { opacity: 0.85; background-position: 14px -18px; }
}

/* Legendary tier gets real color instead of the accent-only palette every
   other background uses — a burst of festive confetti dots rather than
   a monochrome ambient effect, since this is meant to feel like the
   flashiest background in the shop. */
body[data-background="confetti-burst"]::after,
.customize-swatch[data-background-demo="confetti-burst"]::after {
  background-image:
    radial-gradient(3px 3px at 10% 10%, #ec4899, transparent),
    radial-gradient(3px 3px at 30% 40%, #22d3ee, transparent),
    radial-gradient(3px 3px at 50% 15%, #fde047, transparent),
    radial-gradient(3px 3px at 70% 50%, #a855f7, transparent),
    radial-gradient(3px 3px at 90% 25%, #4ade80, transparent),
    radial-gradient(3px 3px at 20% 65%, #fb923c, transparent),
    radial-gradient(3px 3px at 45% 85%, #ec4899, transparent),
    radial-gradient(3px 3px at 65% 70%, #22d3ee, transparent),
    radial-gradient(3px 3px at 85% 90%, #fde047, transparent);
  background-size: 260px 260px;
  opacity: 0.7;
  /* Several of these (cyan, green, yellow) have poor contrast against the
     default light theme's near-white background — a drop-shadow darkens
     every dot's silhouette regardless of its own color, fixing all of
     them at once instead of needing per-color contrast tuning. */
  filter: drop-shadow(0 0 1px rgba(0, 0, 0, 0.35));
  animation: confettiFall 3s linear infinite;
}

@keyframes confettiFall {
  from { background-position: 0 0; }
  to { background-position: 12px 260px; }
}

/* Limited to 30 units ever — the rarest background in the shop.
   v2 — the original used pale gold (#fde047) at low opacity, which has
   almost no contrast against the default light theme's near-white
   background (#f5f7fb) — pale-on-pale is invisible regardless of dot
   count or opacity. Switched to a darker, richer gold plus a drop-shadow
   filter, which (unlike background-image alone) reads against ANY
   background color since it darkens the whole layer's silhouette. */
body[data-background="the-vault"]::after,
.customize-swatch[data-background-demo="the-vault"]::after {
  background-image:
    radial-gradient(3px 3px at 15% 15%, #b45309, transparent),
    radial-gradient(2.5px 2.5px at 45% 55%, #d4af37, transparent),
    radial-gradient(3px 3px at 75% 25%, #b45309, transparent),
    radial-gradient(2.5px 2.5px at 30% 75%, #d4af37, transparent),
    radial-gradient(3px 3px at 60% 85%, #b45309, transparent),
    radial-gradient(2.5px 2.5px at 85% 60%, #d4af37, transparent),
    radial-gradient(3px 3px at 10% 45%, #b45309, transparent),
    radial-gradient(2.5px 2.5px at 90% 90%, #d4af37, transparent),
    radial-gradient(circle at 50% 50%, rgba(180, 83, 9, 0.35), transparent 70%);
  background-size:
    260px 260px, 260px 260px, 260px 260px, 260px 260px,
    260px 260px, 260px 260px, 260px 260px, 260px 260px,
    100% 100%;
  opacity: 0.85;
  filter: drop-shadow(0 0 1.5px rgba(0, 0, 0, 0.5));
  animation: vaultDrift 8s ease-in-out infinite;
}

@keyframes vaultDrift {
  0%, 100% { opacity: 0.65; }
  50% { opacity: 0.95; }
}

/* Actual leaf shapes (a rounded-diamond clip on each dot) tumbling down
   two layers at different rates, rather than round dots. */
body[data-background="autumn-leaves"]::after,
.customize-swatch[data-background-demo="autumn-leaves"]::after {
  background-image:
    radial-gradient(5px 8px at 15% 10%, #c2410c, transparent),
    radial-gradient(6px 4px at 62% 30%, #b45309, transparent),
    radial-gradient(4px 7px at 88% 12%, #92400e, transparent),
    radial-gradient(7px 5px at 34% 66%, #d97706, transparent),
    radial-gradient(5px 8px at 78% 82%, #c2410c, transparent);
  background-size: 320px 320px;
  opacity: 0.68;
  filter: drop-shadow(0 1px 1px rgba(0, 0, 0, 0.35));
  animation: leavesDrift 9s linear infinite;
}

@keyframes leavesDrift {
  from { background-position: 0 0; }
  to   { background-position: 40px 320px; }
}

/* Hollow bubbles (a bright ring, transparent centre) of varied size
   rising on two layers — reads as bubbles, not filled dots. */
body[data-background="bubbles"]::after,
.customize-swatch[data-background-demo="bubbles"]::after {
  background-image:
    radial-gradient(circle 7px at 12% 90%, transparent 55%, color-mix(in srgb, var(--accent) 70%, transparent) 58% 72%, transparent 74%),
    radial-gradient(circle 4px at 34% 70%, transparent 50%, color-mix(in srgb, var(--accent-2) 70%, transparent) 55% 74%, transparent 76%),
    radial-gradient(circle 9px at 58% 96%, transparent 58%, color-mix(in srgb, var(--accent) 60%, transparent) 61% 72%, transparent 74%),
    radial-gradient(circle 5px at 80% 66%, transparent 52%, color-mix(in srgb, var(--accent-2) 65%, transparent) 56% 74%, transparent 76%),
    radial-gradient(circle 6px at 92% 88%, transparent 55%, color-mix(in srgb, var(--accent) 65%, transparent) 58% 73%, transparent 75%);
  background-size: 260px 260px, 180px 180px, 260px 260px, 180px 180px, 260px 260px;
  opacity: 0.6;
  animation: bubblesRise 9s linear infinite;
}

@keyframes bubblesRise {
  from { background-position: 0 0, 0 0, 0 0, 0 0, 0 0; }
  to   { background-position: 8px -260px, -12px -180px, 8px -260px, -12px -180px, 8px -260px; }
}

/* A soft ambient wash (like the Aurora theme's background), not sharp
   dots — diffuse gradients read fine at low opacity since there's no
   hard edge needing contrast against the page behind it. */
body[data-background="northern-lights"]::after,
.customize-swatch[data-background-demo="northern-lights"]::after {
  background-image:
    radial-gradient(circle at 20% 20%, rgba(34, 211, 238, 0.35), transparent 45%),
    radial-gradient(circle at 80% 30%, rgba(74, 222, 128, 0.3), transparent 45%),
    radial-gradient(circle at 50% 80%, rgba(168, 85, 247, 0.3), transparent 50%);
  background-size: 200% 200%;
  opacity: 0.8;
  animation: northernLightsDrift 10s ease-in-out infinite;
}

@keyframes northernLightsDrift {
  0%, 100% { background-position: 0% 0%; }
  50% { background-position: 100% 50%; }
}

/* v2 — a CRT/VHS look: fine scanlines + a shifting speckle grain + a
   slow rolling bright band. Was opacity 0.06 (invisible); now it's a
   deliberate, readable texture. */
body[data-background="static-noise"]::after,
.customize-swatch[data-background-demo="static-noise"]::after {
  background-image:
    linear-gradient(color-mix(in srgb, var(--text) 8%, transparent), transparent 8%, transparent 92%, color-mix(in srgb, var(--text) 8%, transparent)),
    repeating-linear-gradient(0deg, color-mix(in srgb, var(--text) 55%, transparent) 0 1px, transparent 1px 3px),
    radial-gradient(circle 0.6px at 20% 30%, var(--text), transparent),
    radial-gradient(circle 0.6px at 65% 55%, var(--text), transparent),
    radial-gradient(circle 0.6px at 40% 80%, var(--text), transparent),
    radial-gradient(circle 0.6px at 85% 20%, var(--text), transparent);
  background-size: 100% 240px, 100% 3px, 7px 7px, 7px 7px, 7px 7px, 7px 7px;
  opacity: 0.13;
  animation: staticNoise 0.5s steps(3) infinite;
}

@keyframes staticNoise {
  0%   { background-position: 0 0,   0 0, 0 0,   2px 1px, 1px 3px, 3px 2px; }
  50%  { background-position: 0 40%, 0 0, 3px 2px, 0 3px, 2px 1px, 1px 0; }
  100% { background-position: 0 90%, 0 0, 1px 3px, 3px 0, 0 2px,   2px 1px; }
}


/* Premium (Gems). Real parallax within a single ::after (themes already
   claim ::before, so backgrounds only ever get one pseudo-element) — two
   slow ambient ribbons and a layer of sparkle dots move at different
   rates in the same keyframe, the same multi-layer-differential-position
   technique the Founder theme already uses for its two streak layers.
   mix-blend-mode:screen is new here — no existing background uses it. */
body[data-background="aurora-storm"]::after,
.customize-swatch[data-background-demo="aurora-storm"]::after {
  background-image:
    radial-gradient(ellipse 60% 40% at 30% 0%, var(--accent), transparent 60%),
    radial-gradient(ellipse 50% 35% at 70% 10%, var(--accent-2), transparent 60%),
    radial-gradient(1.5px 1.5px at 20% 70%, var(--text), transparent),
    radial-gradient(1.5px 1.5px at 60% 85%, var(--text), transparent),
    radial-gradient(1.5px 1.5px at 85% 60%, var(--text), transparent);
  background-size: 100% 100%, 100% 100%, 300px 300px, 300px 300px, 300px 300px;
  mix-blend-mode: screen;
  opacity: 0.4;
  animation: auroraStormDrift 10s ease-in-out infinite;
}

@keyframes auroraStormDrift {
  0%, 100% { background-position: 0% 0%, 0% 0%, 0 0, 0 0, 0 0; }
  50% { background-position: -15% 8%, 15% -6%, 40px -30px, -30px 20px, 20px -25px; }
}

/* Premium (Gems). Diagonal streaks (Founder's technique) fading in/out
   on a loop, trailing a drifting star layer behind them — every existing
   streak/sparkle background moves in one direction at a constant
   opacity; this one cycles like an actual meteor passing overhead. */
body[data-background="meteor-shower"]::after,
.customize-swatch[data-background-demo="meteor-shower"]::after {
  background-image:
    linear-gradient(115deg, transparent 46%, var(--accent) 49.5%, transparent 53%),
    linear-gradient(115deg, transparent 66%, var(--accent-2) 69%, transparent 72%),
    radial-gradient(2.5px 2.5px at 30% 10%, var(--text), transparent),
    radial-gradient(2px 2px at 70% 40%, var(--text), transparent),
    radial-gradient(2.5px 2.5px at 15% 60%, var(--text), transparent);
  background-size: 100% 100%, 100% 100%, 260px 260px, 260px 260px, 260px 260px;
  animation: meteorStreak 3s linear infinite;
}

@keyframes meteorStreak {
  0% { background-position: -40% 60%, -60% 80%, 0 0, 0 0, 0 0; opacity: 0; }
  10% { opacity: 0.6; }
  60% { opacity: 0.6; }
  100% { background-position: 140% -60%, 120% -40%, 30px -260px, 30px -260px, 30px -260px; opacity: 0; }
}

/* --- New backgrounds: real depth + shape variety, not just dot fields.
   Every layer is theme-adaptive (color-mix off --accent / --text) so it
   reads on light and dark without per-theme patching. --- */

/* Drifting Fog — three soft cloud banks at different depths, each
   crawling at its own rate. */
body[data-background="drifting-fog"]::after,
.customize-swatch[data-background-demo="drifting-fog"]::after {
  background-image:
    radial-gradient(ellipse 55% 40% at 25% 30%, color-mix(in srgb, var(--accent) 22%, transparent), transparent 70%),
    radial-gradient(ellipse 60% 45% at 75% 55%, color-mix(in srgb, var(--accent-2) 20%, transparent), transparent 72%),
    radial-gradient(ellipse 70% 50% at 50% 85%, color-mix(in srgb, var(--text) 8%, transparent), transparent 75%);
  background-size: 180% 180%, 200% 200%, 220% 220%;
  opacity: 0.55;
  animation: fogDrift 26s ease-in-out infinite;
}
@keyframes fogDrift {
  0%, 100% { background-position: 0% 0%, 0% 0%, 0% 0%; }
  50%      { background-position: 30% 12%, -25% -10%, 12% -18%; }
}

/* Contours — a slow-drifting topographic line map. */
body[data-background="contours"]::after,
.customize-swatch[data-background-demo="contours"]::after {
  background-image:
    repeating-radial-gradient(circle at 30% 40%,
      transparent 0 26px, color-mix(in srgb, var(--accent) 32%, transparent) 26px 28px, transparent 28px 54px),
    repeating-radial-gradient(circle at 78% 72%,
      transparent 0 30px, color-mix(in srgb, var(--accent-2) 26%, transparent) 30px 32px, transparent 32px 62px);
  background-size: 520px 520px, 640px 640px;
  opacity: 0.4;
  animation: contourDrift 30s linear infinite;
}
@keyframes contourDrift {
  from { background-position: 0 0, 0 0; }
  to   { background-position: 120px 90px, -100px 70px; }
}

/* Floating Shapes — translucent triangles + hexagons, rotating as they
   drift. Two clip-path'd gradient tiles. */
body[data-background="floating-shapes"]::after,
.customize-swatch[data-background-demo="floating-shapes"]::after {
  background-image:
    conic-gradient(from 0deg at 20% 30%, color-mix(in srgb, var(--accent) 30%, transparent) 0 25%, transparent 25% 100%),
    conic-gradient(from 90deg at 72% 66%, color-mix(in srgb, var(--accent-2) 26%, transparent) 0 16%, transparent 16% 100%),
    conic-gradient(from 45deg at 50% 88%, color-mix(in srgb, var(--accent) 22%, transparent) 0 12%, transparent 12% 100%);
  background-size: 300px 300px, 240px 240px, 360px 360px;
  background-repeat: repeat;
  opacity: 0.4;
  animation: shapesDrift 22s linear infinite;
}
@keyframes shapesDrift {
  from { background-position: 0 0, 0 0, 0 0; }
  to   { background-position: -300px 180px, 240px -160px, 120px 360px; }
}

/* Dust Motes — a fine warm-gold particle field with gentle parallax
   swirl. Best on warm / light themes but token-safe anywhere. */
body[data-background="dust-motes"]::after,
.customize-swatch[data-background-demo="dust-motes"]::after {
  background-image:
    radial-gradient(1px 1px at 12% 20%, color-mix(in srgb, var(--text) 60%, #f5c451), transparent),
    radial-gradient(1.5px 1.5px at 42% 55%, color-mix(in srgb, var(--text) 40%, #f5c451), transparent),
    radial-gradient(1px 1px at 68% 30%, color-mix(in srgb, var(--text) 55%, #f5c451), transparent),
    radial-gradient(1.5px 1.5px at 85% 70%, color-mix(in srgb, var(--text) 45%, #f5c451), transparent),
    radial-gradient(1px 1px at 30% 85%, color-mix(in srgb, var(--text) 60%, #f5c451), transparent);
  background-size: 160px 160px, 260px 260px, 200px 200px, 320px 320px, 220px 220px;
  opacity: 0.5;
  animation: dustSwirl 24s ease-in-out infinite;
}
@keyframes dustSwirl {
  0%, 100% { background-position: 0 0, 0 0, 0 0, 0 0, 0 0; }
  50%      { background-position: 20px -14px, -24px 18px, 16px 10px, -18px -12px, 10px 20px; }
}

/* Data Stream — sparse thin vertical code-rain streaks in the theme
   accent (not matrix green), dim enough to sit under text. */
body[data-background="data-stream"]::after,
.customize-swatch[data-background-demo="data-stream"]::after {
  background-image:
    linear-gradient(color-mix(in srgb, var(--accent) 55%, transparent), transparent 55%),
    linear-gradient(color-mix(in srgb, var(--accent) 40%, transparent), transparent 40%),
    linear-gradient(color-mix(in srgb, var(--accent-2) 45%, transparent), transparent 50%);
  background-size: 2px 120px, 2px 90px, 2px 150px;
  background-position: 14% 0, 47% 0, 79% 0;
  background-repeat: repeat-y;
  opacity: 0.35;
  animation: dataStream 2.4s linear infinite;
}
@keyframes dataStream {
  from { background-position-y: -120px, -60px, -150px; }
  to   { background-position-y: 120px, 90px, 150px; }
}

/* Ripple Pond — concentric rings expanding from two points on a loop. */
body[data-background="ripple-pond"]::after,
.customize-swatch[data-background-demo="ripple-pond"]::after {
  background-image:
    repeating-radial-gradient(circle at 32% 38%,
      transparent 0 18px, color-mix(in srgb, var(--accent) 30%, transparent) 18px 20px, transparent 20px 40px),
    repeating-radial-gradient(circle at 74% 68%,
      transparent 0 22px, color-mix(in srgb, var(--accent-2) 26%, transparent) 22px 24px, transparent 24px 48px);
  background-size: 100% 100%, 100% 100%;
  opacity: 0.4;
  animation: ripplePond 5s ease-out infinite;
}
@keyframes ripplePond {
  0%   { background-size: 40% 40%, 30% 30%; opacity: 0; }
  15%  { opacity: 0.45; }
  100% { background-size: 180% 180%, 150% 150%; opacity: 0; }
}

/* =========================================================
   RESET / BASE
========================================================= */

* {
  box-sizing: border-box;
}

html {
  /* Stops the browser's pull-to-refresh/rubber-band gesture from
     swallowing vertical drags meant for the page — the single biggest
     thing that makes scrolling feel like "a website" instead of "an
     app" on mobile. */
  overscroll-behavior-y: contain;
  -webkit-text-size-adjust: 100%;
}

a, button, .icon-btn, .bottom-tab, .duration-btn, .mode-btn, .pill {
  /* Kills the ~300ms delay + double-tap-to-zoom browsers apply to taps
     near clickable elements, and the gray flash Android/iOS show on
     tap by default — both read as "web page", not "app". */
  touch-action: manipulation;
  -webkit-tap-highlight-color: transparent;
}

body {
  margin: 0;
  min-height: 100vh;
  background: var(--bg);
  color: var(--text);
  font-family: var(--font);
  font-size: 14px;
  line-height: 1.55;
  -webkit-font-smoothing: antialiased;
  -moz-osx-font-smoothing: grayscale;
  text-rendering: optimizeLegibility;
  transition: background var(--dur-3) var(--ease), color var(--dur-3) var(--ease);
  position: relative;

  display: flex;
  flex-direction: column;
}

.site-main {
  flex: 1 1 auto;
  /* The header is sticky/full-bleed now, so page content needs its own
     top breathing room (was the header's old bottom margin). */
  padding-top: 28px;
}

body > * {
  position: relative;
  z-index: 1;
}

a {
  text-decoration: none;
  color: inherit;
}

button {
  font-family: inherit;
  cursor: pointer;
}

img { max-width: 100%; }

::selection {
  background: var(--accent-soft);
  color: var(--text);
}

/* One consistent keyboard-focus ring everywhere. Mouse clicks stay
   clean (:focus-visible, not :focus); individual components can still
   opt into a tighter ring where the default would clip. */
:focus-visible {
  outline: none;
  box-shadow: var(--focus-ring);
  border-radius: var(--radius-sm);
}

h1, h2, h3, h4 {
  font-family: var(--font-display);
  font-weight: 700;
  letter-spacing: -0.014em;
  line-height: 1.2;
}

/* Every stat / counter / timer reads with fixed-width figures so it
   never jitters as digits change mid-test. */
.tabular,
#timer, #live-wpm, #live-accuracy, #live-wpm-big,
.hero-score, .mini-value, .stat-card h2, .stat-card h3,
.lb-wpm, .coin-total, .live-wpm-hero-value {
  font-variant-numeric: tabular-nums;
  font-feature-settings: "tnum";
}

/* .page-container width + centring come from the shell rule near the top. */
.page-container {
  margin-top: 4px;
}

.legal-page {
  /* width from the narrow-shell rule near the top */
  margin-top: 36px;
  margin-bottom: 40px;
}

.legal-card {
  padding: 40px 44px;
  line-height: 1.65;
  color: var(--text);
}

.legal-card h1 {
  margin: 0 0 4px;
  font-size: 30px;
  font-weight: 700;
}

.legal-card h2 {
  margin: 32px 0 10px;
  font-size: 19px;
  font-weight: 700;
}

.legal-card p,
.legal-card ul {
  margin: 0 0 14px;
  color: var(--muted);
}

.legal-card ul {
  padding-left: 20px;
}

.legal-card li {
  margin-bottom: 6px;
}

.legal-card a {
  color: var(--accent);
  font-weight: 600;
}

.legal-updated {
  font-size: 13px;
  color: var(--muted);
  margin-bottom: 24px !important;
}

/* Off-screen, not display:none/visibility:hidden — some spam bots
   specifically skip fields hidden that way, defeating the point of a
   honeypot. Positioning it out of the viewport instead fools that check
   while staying invisible (and untabbable, see tabindex="-1" in the
   template) to a real visitor. */
.contact-honeypot {
  position: absolute;
  left: -9999px;
  top: -9999px;
}

.section-label {
  display: block;
  font-size: 12px;
  font-weight: 700;
  letter-spacing: 0.08em;
  text-transform: uppercase;
  color: var(--muted);
  margin-bottom: 4px;
}

/* =========================================================
   SHARED PRIMITIVES
   Every page is built from these. A theme reskins the whole
   site by moving tokens (above); nothing below hard-codes a
   colour, radius, or shadow.
========================================================= */

.card {
  background: var(--surface);
  border: var(--card-border);
  border-radius: var(--radius-lg);
  box-shadow: var(--shadow-sm);
  transition: border-color var(--dur-2) var(--ease), box-shadow var(--dur-2) var(--ease),
              transform var(--dur-2) var(--ease), background var(--dur-2) var(--ease);
}

/* Only cards that are themselves a link/button lift on hover — a plain
   informational card (leaderboard, stats) just warms its border. Opt in
   with .card-interactive or by being an <a>/<button>. */
.card:hover {
  border-color: var(--border-strong);
}

a.card:hover,
button.card:hover,
.card-interactive:hover {
  border-color: var(--accent);
  transform: translateY(-2px);
  box-shadow: var(--shadow-md);
}

/* ---- Buttons ------------------------------------------------------- */

.btn {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  gap: 8px;

  padding: 10px 16px;
  border-radius: var(--radius-sm);
  border: 1px solid var(--border-strong);

  background: var(--surface-2);
  color: var(--text);

  font-family: inherit;
  font-weight: 600;
  font-size: 13.5px;
  line-height: 1;
  white-space: nowrap;

  transition: background var(--dur-1) var(--ease), border-color var(--dur-1) var(--ease),
              transform var(--dur-1) var(--ease), box-shadow var(--dur-1) var(--ease),
              opacity var(--dur-1) var(--ease);
}

.btn:hover {
  background: var(--surface-3);
  border-color: var(--accent);
}

.btn:active {
  transform: translateY(1px);
}

.btn:disabled,
.btn[disabled] {
  opacity: 0.5;
  pointer-events: none;
}

/* The single primary action in a view — a solid accent fill with an
   ink label (the crisp "pro tool" look), not a gradient. */
.btn-primary {
  border: 1px solid transparent;
  background: var(--accent);
  color: var(--on-accent);
}

.btn-primary:hover {
  background: var(--accent-hover);
  border-color: transparent;
  box-shadow: 0 6px 18px -6px var(--accent);
}

.btn-primary:active {
  background: var(--accent-press);
}

/* Low-emphasis: text + hairline only, for secondary/cancel actions. */
.btn-ghost {
  background: transparent;
  border-color: var(--border);
  color: var(--muted);
}

.btn-ghost:hover {
  background: var(--surface-2);
  color: var(--text);
  border-color: var(--border-strong);
}

.btn-danger {
  background: var(--danger);
  border-color: transparent;
  color: #fff;
}

.btn-danger:hover {
  background: color-mix(in srgb, var(--danger) 88%, #000);
  border-color: transparent;
}

/* ---- Pills (mode / duration / scope / tab selectors) -------------- */

.pill,
.mode-btn,
.duration-btn,
.lb-scope-btn,
.customize-tab {
  display: inline-flex;
  align-items: center;
  gap: 6px;

  border: 1px solid var(--border);
  border-radius: var(--radius-full);
  background: var(--surface-2);
  color: var(--muted);

  font-family: inherit;
  font-weight: 600;
  line-height: 1;
  white-space: nowrap;

  transition: color var(--dur-1) var(--ease), background var(--dur-1) var(--ease),
              border-color var(--dur-1) var(--ease), box-shadow var(--dur-1) var(--ease);
}

.pill:hover,
.mode-btn:hover,
.duration-btn:hover,
.lb-scope-btn:hover,
.customize-tab:hover {
  color: var(--text);
  border-color: var(--border-strong);
}

/* The signature accent detail — a theme can restyle just this (a hard
   double-stroke for cyber, a metallic fill for prestige) via the
   --pill-active-* knobs without touching component markup. */
.pill.active,
.mode-btn.active,
.duration-btn.active,
.lb-scope-btn.active,
.customize-tab.active {
  background: var(--pill-active-bg);
  color: var(--pill-active-fg);
  border-color: transparent;
  box-shadow: var(--pill-active-ring);
}

/* ---- Chips (nav counters: coins / gems / level) ------------------ */

.chip {
  display: inline-flex;
  align-items: center;
  gap: 6px;
  height: 34px;
  padding: 0 12px;
  border-radius: var(--radius-full);
  border: 1px solid var(--border);
  background: var(--surface-2);
  color: var(--text);
  font-weight: 700;
  font-size: 13px;
  font-variant-numeric: tabular-nums;
  transition: border-color var(--dur-1) var(--ease), transform var(--dur-1) var(--ease);
}

.chip:hover {
  border-color: var(--border-strong);
  transform: translateY(-1px);
}

/* ---- Form controls --------------------------------------------- */

.field,
.modal-card input,
.modal-card select,
.modal-card textarea,
.settings-card input,
.settings-card textarea {
  width: 100%;
  padding: 10px 13px;
  border-radius: var(--radius-sm);
  border: 1px solid var(--border-strong);
  background: var(--surface-2);
  color: var(--text);
  font-family: inherit;
  font-size: 14px;
  outline: none;
  transition: border-color var(--dur-1) var(--ease), box-shadow var(--dur-1) var(--ease);
}

.field::placeholder,
.modal-card input::placeholder,
.modal-card textarea::placeholder { color: var(--muted); }

.field:focus,
.modal-card input:focus,
.modal-card select:focus,
.modal-card textarea:focus,
.settings-card input:focus,
.settings-card textarea:focus {
  border-color: var(--accent);
  box-shadow: 0 0 0 3px var(--accent-soft);
}

/* ---- Section headings ---------------------------------------------- */

.eyebrow {
  display: block;
  margin: 0 0 6px;
  font-size: 11px;
  font-weight: 700;
  letter-spacing: 0.12em;
  text-transform: uppercase;
  color: var(--muted);
}

/* =========================================================
   HEADER
========================================================= */

/* Full-bleed sticky bar with a translucent blurred ground — the inner
   wrapper is what's constrained to the content width. */
.site-header {
  position: sticky;
  top: 0;
  z-index: 100;

  background: color-mix(in srgb, var(--bg) 78%, transparent);
  backdrop-filter: saturate(1.4) blur(14px);
  border-bottom: 1px solid var(--border);
  /* Clears the status-bar / camera cutout when running standalone on a
     notched phone. */
  padding-top: env(safe-area-inset-top, 0px);
}

.site-header-inner {
  /* width + centring from the shell rule near the top */
  height: 60px;

  display: flex;
  align-items: center;
  gap: 20px;
}

.brand-logo {
  display: flex;
  align-items: center;
  gap: 9px;
  flex-shrink: 0;
}

.brand-logo .logo-mark {
  width: 30px;
  height: 30px;
  border-radius: 9px;

  display: flex;
  align-items: center;
  justify-content: center;

  font-size: 12px;
  font-weight: 800;
  letter-spacing: -0.02em;
  color: var(--on-accent);

  background: var(--accent);
}

.brand-logo .logo-text {
  font-family: var(--font-display);
  font-size: 17px;
  font-weight: 700;
  letter-spacing: -0.02em;
  color: var(--text);
}

/* Primary nav — quiet text links, active one carries the accent. */
.primary-nav {
  display: flex;
  align-items: center;
  gap: 2px;
  margin-right: auto;
}

.primary-nav-link {
  padding: 7px 12px;
  border-radius: var(--radius-sm);
  color: var(--muted);
  font-size: 13.5px;
  font-weight: 600;
  transition: color var(--dur-1) var(--ease), background var(--dur-1) var(--ease);
}

.primary-nav-link:hover {
  color: var(--text);
  background: var(--surface-2);
}

.primary-nav-link.active {
  color: var(--text);
  background: var(--pill-active-bg);
}

.nav-actions {
  display: flex;
  align-items: center;
  gap: 10px;
  flex-shrink: 0;
}

.nav-chips {
  display: flex;
  align-items: center;
  gap: 6px;
}

.nav-icons {
  display: flex;
  align-items: center;
  gap: 2px;
}

.icon-btn {
  position: relative;
  width: 36px;
  height: 36px;

  display: flex;
  align-items: center;
  justify-content: center;

  border: none;
  border-radius: var(--radius-full);

  background: transparent;
  color: var(--muted);
  font-size: 16px;

  transition: background var(--dur-1) var(--ease), color var(--dur-1) var(--ease);
}

.icon-btn:hover {
  background: var(--surface-2);
  color: var(--text);
}

.nav-avatar {
  display: flex;
  align-items: center;
  justify-content: center;
  width: 22px;
  height: 22px;
  font-size: 20px;
  line-height: 1;
}

.account-trigger:hover .nav-avatar,
.account-trigger[aria-expanded="true"] .nav-avatar { color: var(--text); }

.icon-badge {
  position: absolute;
  top: 3px;
  right: 3px;

  min-width: 15px;
  height: 15px;
  padding: 0 3px;
  border-radius: var(--radius-full);
  border: 2px solid var(--bg);

  align-items: center;
  justify-content: center;

  background: var(--danger);
  color: #fff;

  font-size: 9px;
  font-weight: 800;
  line-height: 1;
}

/* Nav counter chips — inherit the shared .chip primitive; only the
   accent treatment on the coin/gem values lives here. Gems stay a
   fixed indigo (not the theme accent) so "premium currency" reads the
   same on every theme. */
.coin-pill { color: var(--text); }
.coin-pill span { color: var(--warning); }

.gem-pill {
  color: var(--text);
  text-decoration: none;
}
.gem-pill span { color: #8b9dff; }

.level-pill { color: var(--muted); }
.level-pill span { color: var(--text); }

/* =========================================================
   ACCOUNT MENU
========================================================= */

.account-menu {
  position: relative;
}

.account-dropdown {
  position: absolute;
  top: calc(100% + 8px);
  right: 0;
  z-index: 500;

  min-width: 190px;
  padding: 6px;
  border-radius: var(--radius-md);

  background: var(--surface);
  border: 1px solid var(--border);
  box-shadow: var(--shadow-lg);

  animation: popIn 0.15s ease;
}

.account-dropdown-header {
  padding: 8px 10px 6px;
  font-size: 13px;
  font-weight: 800;
  color: var(--text);
}

.account-dropdown-item {
  display: flex;
  align-items: center;
  gap: 10px;
  width: 100%;

  padding: 9px 10px;
  border: none;
  border-radius: 8px;

  background: none;
  color: var(--text);
  text-decoration: none;

  font-size: 13.5px;
  font-weight: 600;
  text-align: left;
  cursor: pointer;

  transition: background 0.15s ease;
}

.account-dropdown-item:hover {
  background: var(--surface-2);
}

.account-dropdown-item.danger {
  color: var(--danger);
}

.account-dropdown-item i {
  font-size: 14px;
  width: 16px;
  text-align: center;
}

.account-dropdown-divider {
  height: 1px;
  margin: 6px 4px;
  background: var(--border);
}

/* =========================================================
   MODALS
========================================================= */

.modal-overlay {
  position: fixed;
  inset: 0;
  z-index: 9999;

  display: none;
  align-items: center;
  justify-content: center;
  /* Without this, a modal-card taller than the visible viewport gets
     centered partly above y:0 with no way to scroll up to it — flex
     centering doesn't reflow to the overflow start on its own. */
  overflow-y: auto;
  padding: 24px 0;
  /* Equipping a theme rebuilds #customize-grid's contents in place
     (see renderCustomizeTab) — Chrome's scroll anchoring can read that
     mutation as a reason to silently readjust this container's own
     scrollTop to "preserve" the user's view, which visibly yanks the
     whole modal card away from its centered position. The grid's own
     scroll position is already explicitly saved/restored in JS, so
     anchoring here only ever fights that instead of helping. */
  overflow-anchor: none;

  background: color-mix(in srgb, #05070c 62%, transparent);
  backdrop-filter: blur(8px);
}

.modal-card {
  width: min(480px, 90%);
  max-height: 82vh;
  /* vh is calculated against the browser's largest possible viewport
     (address bar hidden) — on mobile, with the address bar visible,
     that's taller than what's actually on screen, which is exactly
     when this modal most needs to be capped. dvh tracks the real,
     currently-visible viewport instead. Older browsers ignore this
     line and keep the vh value above. */
  max-height: 82dvh;
  overflow-y: auto;

  padding: 26px;
  border-radius: var(--radius-lg);

  background: var(--surface);
  border: 1px solid var(--border-strong);
  color: var(--text);

  box-shadow: var(--shadow-lg);
  animation: popIn 0.2s var(--ease-out);
}

@media (max-width: 768px) {
  /* Top-anchored instead of centered — on a short mobile viewport a
     centered tall card has no headroom, so centering math alone can't
     keep the whole thing on screen even with the overlay scroll fix
     above. Anchoring to the top guarantees the card's own header/close
     button are always the first thing visible. */
  .modal-overlay {
    align-items: flex-start;
  }
}

@keyframes popIn {
  from { transform: scale(0.95); opacity: 0; }
  to { transform: scale(1); opacity: 1; }
}

.modal-card h2 {
  margin: 0 0 16px;
  font-family: var(--font-display);
  font-size: 19px;
  font-weight: 700;
}

/* A <form> wrapper (auth modals) that generates no box of its own, so
   wrapping fields in it for real Enter-submit / password-manager
   behaviour changes nothing about the layout. */
.modal-form {
  display: contents;
}

/* input / select / textarea styling comes from the shared .field rule in
   PRIMITIVES; only the modal's own spacing quirk (fields stacked with a
   top gap instead of a wrapping <label>) lives here. */
.modal-card input,
.modal-card select,
.modal-card textarea {
  margin-top: 10px;
}

.modal-card textarea {
  resize: vertical;
}

.cp-panel-hint {
  margin-top: 8px;
  font-size: 12.5px;
  color: var(--muted);
}

.display-pref-row {
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: 12px;
  padding: 10px 0;
  border-bottom: 1px solid var(--border);
}

.display-pref-row:last-child {
  border-bottom: none;
}

.display-pref-row label {
  font-size: 14px;
  font-weight: 600;
}

.display-pref-row select {
  width: auto;
  margin-top: 0;
}

.display-pref-row input[type="checkbox"] {
  width: 18px;
  height: 18px;
  accent-color: var(--accent);
}

#login-error {
  margin-top: 10px;
  font-size: 13px;
  font-weight: 600;
  color: var(--danger);
  min-height: 1em;
}

.modal-card p {
  margin-top: 14px;
  font-size: 13px;
  color: var(--muted);
}

.auth-divider {
  display: flex;
  align-items: center;
  gap: 10px;
  margin: 16px 0;

  color: var(--muted);
  font-size: 12px;
  text-transform: uppercase;
  letter-spacing: 0.05em;
}

.auth-divider::before,
.auth-divider::after {
  content: "";
  flex: 1;
  height: 1px;
  background: var(--border);
}

.google-auth-btn {
  display: flex;
  align-items: center;
  justify-content: center;
  gap: 10px;
  width: 100%;
  padding: 10px;

  border-radius: var(--radius-sm);
  border: 1px solid var(--border-strong);
  background: var(--surface-2);
  color: var(--text);

  font-weight: 600;
  font-size: 13.5px;

  transition: background var(--dur-1) var(--ease), border-color var(--dur-1) var(--ease);
}

.google-auth-btn:hover {
  background: var(--surface-3);
  border-color: var(--accent);
}

.google-auth-btn i {
  color: #4285f4;
  font-size: 16px;
}

.modal-card p a {
  color: var(--accent);
  font-weight: 600;
}

.modal-actions {
  display: flex;
  gap: 10px;
  margin-top: 20px;
}

.modal-actions button {
  flex: 1;
  border-radius: var(--radius-sm);
  padding: 11px;
  font-family: inherit;
  font-weight: 700;
  font-size: 13.5px;
  transition: background var(--dur-1) var(--ease), border-color var(--dur-1) var(--ease);
}

.modal-actions .secondary {
  background: transparent;
  color: var(--muted);
  border: 1px solid var(--border-strong);
  font-weight: 600;
}

.modal-actions .secondary:hover {
  background: var(--surface-2);
  color: var(--text);
}

.modal-actions button:not(.secondary) {
  border: 1px solid transparent;
  color: var(--on-accent);
  background: var(--accent);
}

.modal-actions button:not(.secondary):hover {
  background: var(--accent-hover);
}

.modal-actions button.btn-danger {
  background: var(--danger);
  color: #fff;
  box-shadow: none;
}

.modal-actions button.btn-danger:hover {
  background: color-mix(in srgb, var(--danger) 88%, #000);
}

.confirm-modal-card {
  width: min(360px, 90%);
}

#confirm-modal-message {
  margin: 0;
  font-size: 14.5px;
  line-height: 1.5;
  color: var(--text);
}

/* =========================================================
   CUSTOMIZE MODAL (themes / cursors / sounds / backgrounds)
========================================================= */

.customize-tabs {
  display: flex;
  gap: 4px;
  margin-bottom: 16px;
  padding-bottom: 2px;
  overflow-x: auto;
}

/* colour / border / radius / active treatment come from the shared .pill
   rule in PRIMITIVES; size + shrink behaviour is all that's local. */
.customize-tab {
  flex-shrink: 0;
  padding: 7px 13px;
  font-size: 12.5px;
  font-weight: 600;
}

.customize-login-prompt {
  padding: 20px;
  border-radius: var(--radius-md);
  background: var(--surface-2);
  border: 1px solid var(--border);
  text-align: center;
  color: var(--muted);
  font-size: 13px;
}

.customize-login-prompt a {
  color: var(--accent);
  font-weight: 700;
}

/* =========================================================
   FRIENDS MODAL
========================================================= */

#friends-body .section-label {
  display: block;
  margin: 4px 0 10px;
}

#friend-requests-section {
  margin-bottom: 20px;
  padding-bottom: 16px;
  border-bottom: 1px solid var(--border);
}

.friends-list {
  display: flex;
  flex-direction: column;
  gap: 8px;
  max-height: 260px;
  overflow-y: auto;
}

.friend-row {
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: 10px;

  padding: 10px 12px;
  border-radius: var(--radius-sm);

  background: var(--surface-2);
  border: 1px solid var(--border);
}

.friend-row a {
  font-weight: 700;
  font-size: 13.5px;
  color: var(--text);
}

.friend-row a:hover {
  color: var(--accent);
}

.friend-row-actions {
  display: flex;
  gap: 6px;
  flex-shrink: 0;
}

.friend-action-btn {
  padding: 6px 12px;
  border: none;
  border-radius: 8px;

  font-size: 12px;
  font-weight: 700;
  cursor: pointer;
}

.friend-action-btn.accept {
  background: var(--accent);
  color: var(--on-accent);
}

.friend-action-btn.decline {
  background: var(--surface);
  color: var(--muted);
  border: 1px solid var(--border);
}

/* Profile-page friend button states (see .btn in DESIGN SYSTEM) */
.friend-btn-active {
  background: color-mix(in srgb, var(--success) 14%, transparent);
  color: var(--success);
  border-color: transparent;
}

.friend-meta {
  font-size: 11.5px;
  color: var(--muted);
  margin-top: 2px;
}

.friend-action-btn.message {
  display: flex;
  align-items: center;
  gap: 4px;
  background: var(--accent-soft);
  color: var(--accent);
}

/* =========================================================
   MESSAGES
========================================================= */

.conversation-row {
  cursor: pointer;
  transition: transform 0.15s ease;
}

.conversation-row:hover {
  transform: translateX(2px);
}

.conversation-preview {
  font-size: 12px;
  color: var(--muted);
  margin-top: 2px;
  white-space: nowrap;
  overflow: hidden;
  text-overflow: ellipsis;
  max-width: 260px;
}

.thread-header {
  display: flex;
  align-items: center;
  gap: 10px;
  margin-bottom: 14px;
}

.thread-header strong {
  font-size: 15px;
}

.thread-messages {
  display: flex;
  flex-direction: column;
  gap: 8px;

  height: 320px;
  overflow-y: auto;

  padding: 4px 2px;
  margin-bottom: 12px;
}

.thread-bubble {
  align-self: flex-start;
  max-width: 75%;

  padding: 9px 13px;
  border-radius: var(--radius-md);

  background: var(--surface-2);
  border: 1px solid var(--border);

  font-size: 13.5px;
  line-height: 1.4;
  word-break: break-word;
}

.thread-bubble.mine {
  align-self: flex-end;
  background: var(--accent);
  color: var(--on-accent);
  border: none;
}

.thread-bubble small {
  display: block;
  margin-top: 4px;
  font-size: 10.5px;
  opacity: 0.7;
}

.thread-input-row {
  display: flex;
  gap: 8px;
}

.thread-input-row input {
  flex: 1;
  padding: 10px 14px;
  border-radius: 999px;

  border: 1px solid var(--border);
  background: var(--surface-2);
  color: var(--text);

  font-size: 13.5px;
  outline: none;
}

.thread-input-row input:focus {
  border-color: var(--accent);
}

.thread-input-row .btn {
  flex-shrink: 0;
  padding: 10px 18px;
}

/* =========================================================
   GROUP CHAT
========================================================= */

.messages-list-header {
  display: flex;
  align-items: center;
  justify-content: space-between;
  margin-bottom: 18px;
}

.messages-list-header h2 {
  margin: 0;
}

.thread-bubble-sender {
  display: block;
  font-size: 11px;
  font-weight: 700;
  color: var(--accent);
  margin-bottom: 2px;
}

.group-role-badge {
  display: inline-block;
  margin-left: 6px;
  padding: 2px 8px;
  border-radius: 999px;

  background: var(--accent-soft);
  color: var(--accent);

  font-size: 10.5px;
  font-weight: 700;
  text-transform: uppercase;
  letter-spacing: 0.03em;
}

.group-rename-row {
  display: flex;
  gap: 8px;
  margin-bottom: 18px;
}

.group-rename-row input {
  flex: 1;
  padding: 10px 14px;
  border-radius: var(--radius-sm);
  border: 1px solid var(--border);
  background: var(--surface-2);
  color: var(--text);
}

#group-add-member-row {
  margin-top: 18px;
  padding-top: 14px;
  border-top: 1px solid var(--border);
}

.group-friend-picker-row {
  cursor: pointer;
}

.group-friend-picker-row input[type="checkbox"] {
  width: 18px;
  height: 18px;
  accent-color: var(--accent);
}

/* min-height + max-height (NOT a plain `height`) — categories have very
   different item counts (Themes ~23 vs. Limited ~6), so a max-height-
   only grid renders shorter for smaller tabs, and since #theme-modal is
   vertically centered (.modal-overlay's align-items:center), a shorter
   grid shrinks the whole modal-card and shifts its top — including the
   tab row — down the screen when switching between tabs of different
   sizes. A plain `height: 360px` (tried first) fixes that but breaks
   card rendering: with a definite height smaller than the natural
   content size, the grid's implicit auto-sized rows COMPRESS to fit
   instead of overflowing/scrolling, squashing every card down to ~34px
   with its content clipped. min-height alongside the original
   max-height keeps the box always at least 360px (stabilizing the
   modal) while leaving row sizing unconstrained, so cards keep their
   natural size and long tabs still scroll internally past 360px. */
.customize-grid {
  display: grid;
  grid-template-columns: repeat(3, 1fr);
  gap: 12px;
  min-height: 360px;
  max-height: 360px;
  overflow-y: auto;
  padding: 2px;
  /* This is the element whose contents actually get replaced on every
     equip — same scroll-anchoring opt-out as .modal-overlay above, and
     for the same reason (its own scrollTop is already explicitly
     preserved in JS, so anchoring here can only misfire). */
  overflow-anchor: none;
}

.customize-card {
  position: relative;
  padding: 10px;
  border-radius: var(--radius-md);
  overflow: hidden;

  background: var(--surface-2);
  border: 1px solid var(--border);

  transition: border-color var(--dur-1) var(--ease), transform var(--dur-1) var(--ease);
}

.customize-card:hover {
  border-color: var(--border-strong);
  transform: translateY(-2px);
}

.customize-card.equipped {
  border-color: var(--accent);
  box-shadow: 0 0 0 1px var(--accent);
}

.customize-card.locked .customize-swatch {
  filter: grayscale(0.4);
  opacity: 0.7;
}

.customize-swatch {
  height: 54px;
  border-radius: var(--radius-sm);
  margin-bottom: 8px;

  display: flex;
  align-items: center;
  justify-content: center;
  font-size: 22px;
  overflow: hidden;
}

/* Theme swatch — a real mini-preview. [data-theme-scope="<slug>"] on this
   element (emitted by swatchHtml) applies that theme's token block
   locally, so every var() below resolves to the theme's own values. */
.theme-swatch {
  flex-direction: column;
  align-items: stretch;
  justify-content: center;
  gap: 6px;
  padding: 9px 11px;
  background: var(--bg);
  box-shadow: inset 0 0 0 1px var(--border);
}

.theme-swatch .ts-row {
  display: flex;
  align-items: center;
  gap: 7px;
}

.theme-swatch .ts-dot {
  width: 9px;
  height: 9px;
  border-radius: 50%;
  background: var(--accent);
  flex-shrink: 0;
}

.theme-swatch .ts-bar {
  flex: 1;
  height: 8px;
  border-radius: 999px;
  background: var(--surface-2);
  box-shadow: inset 0 0 0 1px var(--border);
}

.theme-swatch .ts-text {
  font-family: var(--font-display, var(--font));
  font-size: 12px;
  font-weight: 700;
  letter-spacing: -0.01em;
  line-height: 1;
}

.theme-swatch .ts-text b { color: var(--text); font-weight: 700; }
.theme-swatch .ts-text i { color: var(--faint); font-style: normal; font-weight: 600; }

/* The locked-card grayscale would wash the preview out entirely — keep it
   readable, just dim it. */
.customize-card.locked .theme-swatch {
  filter: none;
  opacity: 0.85;
}

.customize-card p {
  margin: 0;
  font-size: 12px;
  font-weight: 700;
  line-height: 1.3;
}

.customize-card small {
  display: block;
  margin-top: 2px;
  color: var(--muted);
  font-size: 10.5px;
  text-transform: capitalize;
}

.customize-card.exclusive {
  border-color: color-mix(in srgb, var(--warning) 45%, transparent);
}

.customize-scarcity {
  display: block;
  margin-top: 3px;
  color: var(--warning);
  font-size: 10.5px;
  font-weight: 700;
}

.customize-scarcity.sold-out {
  color: var(--danger);
}

.customize-card button {
  width: 100%;
  margin-top: 8px;
  padding: 7px;
  border: 1px solid transparent;
  border-radius: var(--radius-sm);

  font-family: inherit;
  font-size: 11.5px;
  font-weight: 700;
  cursor: pointer;
  transition: background var(--dur-1) var(--ease);
}

.customize-card .btn-equip {
  background: var(--accent-soft);
  color: var(--accent);
}

.customize-card .btn-equip:hover {
  background: color-mix(in srgb, var(--accent) 22%, transparent);
}

/* "Equipped" is a status, not an action — a soft check-tag, not a solid
   green button competing with the real CTAs. */
.customize-card .btn-equipped {
  background: color-mix(in srgb, var(--success) 15%, transparent);
  color: var(--success);
  border-color: color-mix(in srgb, var(--success) 30%, transparent);
  cursor: default;
}

.customize-card .btn-buy {
  background: var(--accent);
  color: var(--on-accent);
}

.customize-card .btn-buy:hover {
  background: var(--accent-hover);
}

@media (max-width: 480px) {
  .customize-grid { grid-template-columns: repeat(2, 1fr); }
}

/* =========================================================
   TYPING TEST
========================================================= */

.test-toolbar {
  display: flex;
  align-items: center;
  justify-content: center;
  flex-wrap: wrap;
  gap: 8px;
  margin: 0 auto;
  /* Recedes while a test is running (see body.test-active below) so the
     passage is the only thing asking for attention. */
  transition: opacity var(--dur-3) var(--ease);
}

/* The little vertical rules between groups read as clutter now that each
   group is its own segmented control — a touch of extra gap does the job. */
.toolbar-divider {
  width: 4px;
  height: 1px;
  background: transparent;
}

/* Each control group is one segmented control: a recessed track with
   transparent segments, the active one raised. Fewer distinct shapes to
   parse than a row of separate pills. */
.mode-picker,
.duration-picker {
  display: flex;
  align-items: center;
  gap: 2px;
  padding: 3px;
  border-radius: var(--radius-full);
  background: var(--surface-2);
  border: 1px solid var(--border);
}

.mode-btn,
.duration-btn {
  display: inline-flex;
  align-items: center;
  gap: 6px;
  padding: 6px 13px;
  border: 1px solid transparent;
  border-radius: var(--radius-full);
  background: transparent;
  color: var(--muted);
  font-size: 12.5px;
  font-weight: 600;
  white-space: nowrap;
  transition: color var(--dur-1) var(--ease), background var(--dur-1) var(--ease);
}

.mode-btn:hover,
.duration-btn:hover {
  color: var(--text);
  background: transparent;
  border-color: transparent;
}

.mode-btn.active,
.duration-btn.active {
  background: var(--surface);
  color: var(--text);
  border-color: var(--border);
  box-shadow: var(--shadow-sm);
}

/* The signature-accent moment stays on the *primary* segmented control
   (mode) so the equipped theme still speaks here. */
.mode-btn.active {
  background: var(--pill-active-bg);
  color: var(--pill-active-fg);
  border-color: transparent;
  box-shadow: var(--pill-active-ring);
}

.typing-panel {
  display: flex;
  justify-content: center;
  margin: 4px 0 22px;
  transition: opacity var(--dur-3) var(--ease);
}

.typing-controls {
  display: flex;
  align-items: center;
  gap: 12px;

  padding: 7px 8px 7px 16px;
  border-radius: var(--radius-full);

  background: var(--surface-2);
  border: 1px solid var(--border);
}

.timer-chip {
  min-width: 30px;
  text-align: center;
  font-size: 14px;
  font-weight: 700;
  color: var(--accent);
  font-variant-numeric: tabular-nums;
}

.live-stat {
  font-size: 12.5px;
  font-weight: 600;
  color: var(--muted);
  white-space: nowrap;
}

.live-stat span {
  color: var(--text);
  font-weight: 700;
  font-variant-numeric: tabular-nums;
}

#timer.low {
  color: var(--danger);
  animation: pulse 0.6s infinite;
}

@keyframes pulse {
  50% { transform: scale(1.15); }
}

.restart-btn {
  width: 32px;
  height: 32px;

  display: flex;
  align-items: center;
  justify-content: center;

  border: none;
  border-radius: 50%;
  background: var(--surface-3);
  color: var(--muted);

  transition: transform var(--dur-3) var(--ease), background var(--dur-2) var(--ease),
              color var(--dur-2) var(--ease);
}

.restart-btn:hover {
  background: var(--accent);
  color: var(--on-accent);
  transform: rotate(-180deg);
}

/* Zen mode's manual end-test control — .restart-btn is a fixed 32×32
   circle sized for a single icon glyph, which "Finish" (icon + text)
   overflowed and overlapped the adjacent restart button. This is sized
   to the same 32px row height but lets its width grow with the label. */
.finish-btn {
  display: flex;
  align-items: center;
  gap: 6px;
  height: 32px;
  padding: 0 15px;
  flex-shrink: 0;

  border: none;
  border-radius: var(--radius-full);
  background: var(--accent);
  color: var(--on-accent);

  font-size: 12.5px;
  font-weight: 700;
  white-space: nowrap;

  transition: transform var(--dur-1) var(--ease), background var(--dur-1) var(--ease);
}

.finish-btn:hover {
  transform: translateY(-1px);
  background: var(--accent-hover);
}

/* The passage — the actual product. Bigger, calmer, and the one
   element on the page with real presence. No hover animation (you're
   about to type into it, not click it). */
#word-container {
  /* width + centring from the shell rule near the top — this box lines
     up with every other section on the page. */
  margin-bottom: 22px;
  padding: 22px 34px 28px;

  background: var(--surface);
  border: var(--card-border);
  border-radius: var(--radius-lg);
  box-shadow: var(--shadow-sm);

  font-family: var(--test-font);
  font-size: 1.6rem;
  line-height: 2.5rem;
  letter-spacing: 0.01em;

  position: relative;
  overflow: hidden;
  white-space: nowrap;

  cursor: text;
  transition: box-shadow var(--dur-3) var(--ease), border-color var(--dur-3) var(--ease);
}

/* Running a test: the field gains a quiet accent presence and the rest
   of the page steps back. */
body.test-active #word-container {
  border-color: color-mix(in srgb, var(--accent) 45%, var(--border-strong));
  box-shadow: 0 0 0 1px var(--accent-soft),
              0 24px 80px -32px color-mix(in srgb, var(--accent) 70%, transparent);
}

body.test-active .test-toolbar,
body.test-active .daily-challenge-bar {
  opacity: 0.4;
}

body.test-active .test-toolbar:hover,
body.test-active .test-toolbar:focus-within {
  opacity: 1;
}

@media (max-width: 768px) {
  #word-container { padding: 28px 22px; }
}

.guest-teaser-banner {
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: 16px;
  flex-wrap: wrap;
  padding: 18px 22px;
  font-weight: 600;
  color: var(--text);
}

.guest-teaser-banner .buy-btn {
  width: auto;
  padding: 10px 20px;
  white-space: nowrap;
}

/* The big live WPM readout rises into view above the word box once
   typing starts — kept as its own visual cue even without the chrome
   fade this used to accompany (that was cut after repeated feedback
   that dimming the background while typing didn't look good). */
.live-wpm-hero {
  text-align: center;
  margin: 0 auto 4px;
  opacity: 0;
  transform: translateY(10px);
  transition: opacity 0.4s ease, transform 0.4s ease;
  pointer-events: none;
}

body.test-active .live-wpm-hero {
  opacity: 1;
  transform: translateY(0);
}

.live-wpm-hero-value {
  font-family: var(--font-display);
  font-size: 58px;
  font-weight: 800;
  line-height: 1;
  letter-spacing: -0.03em;
  color: var(--accent);
  font-variant-numeric: tabular-nums;
}

.live-wpm-hero-label {
  display: block;
  font-size: 11px;
  font-weight: 700;
  color: var(--muted);
  letter-spacing: 0.14em;
  text-transform: uppercase;
  margin-top: 4px;
}

/* Quiet marker under the word actually being typed — a soft underline
   rather than a glowing box, so it guides the eye without lighting up. */
body.test-active .word.active-word {
  border-radius: var(--radius-sm);
  box-shadow: inset 0 -2px 0 var(--accent);
  transition: box-shadow var(--dur-1) var(--ease);
}

#word-track {
  display: inline-block;
  white-space: nowrap;
  /* Anchor for the absolutely-positioned #live-caret child even before
     scrollWords() sets the first translateX (a transform would also
     establish the containing block, but not until it exists). */
  position: relative;
  transition: transform 0.12s ease-out;
}

/* Code mode: real snippets have line breaks, unlike every other test
   source which is one long horizontally-scrolling line. #word-track
   switches to a wrapping flex row instead of the nowrap/translateX setup
   above (scrollWords() in home.html has a matching JS branch — it
   scrollIntoView()s instead of setting a horizontal transform here), and
   the container grows a scrollable max-height instead of clipping. */
#word-container:has(.code-mode) {
  white-space: normal;
  max-height: 340px;
  overflow-y: auto;
  overflow-x: hidden;
}

/* #word-track.code-mode (not just .code-mode): a plain class selector
   loses to the #word-track ID rule above regardless of source order —
   ID specificity always beats class specificity. */
#word-track.code-mode {
  display: flex;
  flex-wrap: wrap;
  white-space: normal;
}

/* A code-mode sentinel token — see wordlists.flatten_code_snippet and
   build_render_words. Forces the wrap via flex-basis:100%, carries no
   visible content or indent itself (indent is a margin on the next real
   word's span, applied server-side from render_words). */
.word.linebreak {
  flex-basis: 100%;
  height: 0;
  padding: 0;
  margin: 0;
}

/* Code mode was reusing the word-salad "answer chip" look (16px gaps,
   padded/rounded boxes, proportional font) — fine for prose, but it made
   code read as a row of detached blobs instead of code. Monospace +
   tight single-space-width gaps + no chip padding reads as an actual
   code block. Specificity note: #word-track.code-mode (1 ID) beats the
   plain .word rule below regardless of source order. */
#word-track.code-mode {
  font-family: ui-monospace, 'Cascadia Code', 'Source Code Pro', Menlo, Consolas, 'Courier New', monospace;
  line-height: 1.6em;
}

/* Code mode types character by character now (see handleCodeInput in
   home.html), so every .word span is exactly one character — no gap
   between them beyond what real space characters in the code itself
   provide (margin-right:0, not the 0.6em this used when .word meant
   "chunk"; that would now add extra spacing after every character). */
#word-track.code-mode .word {
  margin-right: 0;
  padding: 0 1px;
  border-radius: 3px;
}

/* A lone space character renders at 0 width under white-space:normal
   (confirmed by measuring it) since there's no other content to give the
   inline-block any intrinsic size — it needs to preserve its own glyph
   width to actually show as a gap. */
#word-track.code-mode .char {
  white-space: pre;
}

.word {
  display: inline-block;
  margin-right: 14px;
  padding: 2px 3px;
  border-radius: var(--radius-sm);
  transition: color var(--dur-1) var(--ease);
}

/* The focus-typing-tool convention: untyped text is dim, correctly
   typed text is just normal text (no reward colour on every keystroke),
   and only a wrong word carries a mark. Calmer, and errors read
   instantly because they're the only coloured thing. */
.word.correct-word {
  background: transparent;
}

.word.incorrect-word {
  background: color-mix(in srgb, var(--danger) 12%, transparent);
}

.word.word-shake {
  animation: wordShake 0.28s var(--ease);
  background: color-mix(in srgb, var(--danger) 20%, transparent);
}

@keyframes wordShake {
  0%, 100% { transform: translateX(0); }
  25% { transform: translateX(-4px); }
  75% { transform: translateX(4px); }
}

.char {
  display: inline-block;
  color: var(--faint);
  transition: color var(--dur-1) var(--ease);
}

.char.correct {
  color: var(--text);
}

.char.incorrect {
  color: var(--danger);
  text-decoration: underline;
  text-decoration-thickness: 2px;
  text-underline-offset: 3px;
}

.char.current {
  position: relative;
}

/* The caret — one shape (a slim bar), one paint path (the --caret-*
   tokens). A cursor skin changes colour/glow/animation by overriding
   those tokens; the caret-shape display pref changes geometry via the
   html[data-caret-shape] rules further down. Both compose. */
.char.current::before {
  content: "";
  position: absolute;
  left: -1px;
  top: -0.06em;
  width: var(--caret-width);
  height: 1.12em;
  border-radius: 1px;
  background: var(--caret-bg);
  background-size: var(--caret-bg-size);
  box-shadow: var(--caret-glow);
  clip-path: var(--caret-clip);
  animation: var(--caret-anim);
}

@keyframes blink {
  0%, 45% { opacity: 1; }
  55%, 95% { opacity: 0; }
  100% { opacity: 1; }
}

/* Block-caret pulse. Two translucent stops (never a hard 0) so the glyph
   under the block never fully disappears, and low enough that a
   semi-transparent accent fill reads on both dark and light themes. Ends
   on the visible stop so the reduced-motion freeze (which holds the
   final keyframe) leaves a clearly-visible steady block, not a ghost. */
@keyframes caretBlockBlink {
  0%, 45% { opacity: 0.42; }
  50%, 95% { opacity: 0.16; }
  100% { opacity: 0.42; }
}

/* =========================================================
   DISPLAY PREFS — free, client-side (typeflow-display-prefs), not
   ShopItems. Set on <html> before <body> exists (base.html head IIFE)
   for no flash-of-wrong-caret.
========================================================= */

/* Caret shape — geometry only, so a cursor skin's colour/glow (carried
   on the --caret-* tokens) still shows through. Scoped `html[…] body …`
   to clear both the base rule above and any skin's own width. "bar" is
   the default and needs no rule. The smooth caret (.live-caret) takes
   its geometry from JS (updateLiveCaret in home.html) instead, except
   "off", handled here for both. */
html[data-caret-shape="block"] body .char.current::before {
  left: 0;
  top: 0;
  bottom: auto;
  width: 100%;
  height: 100%;
  border-radius: 2px;
  clip-path: none;
  /* No mix-blend-mode: plus-lighter here — it adds light, so the block
     vanished against white on every light theme. A plain translucent
     accent fill (opacity from caretBlockBlink) reads on any background. */
  animation: caretBlockBlink 1.05s steps(1) infinite;
}

html[data-caret-shape="underline"] body .char.current::before {
  left: 0;
  top: auto;
  bottom: -0.08em;
  width: 100%;
  height: 0.14em;
  border-radius: 2px;
  clip-path: none;
}

html[data-caret-shape="off"] body .char.current::before,
html[data-caret-shape="off"] body .live-caret {
  display: none !important;
}

/* Font size presets for the typing area — "md" is the default saved
   pref, so it's stated explicitly rather than left to #word-container. */
html[data-font-size="sm"] #word-container {
  font-size: 1.25rem;
  line-height: 2rem;
}

html[data-font-size="md"] #word-container {
  font-size: 1.55rem;
  line-height: 2.45rem;
}

html[data-font-size="lg"] #word-container {
  font-size: 1.9rem;
  line-height: 2.75rem;
}

html[data-font-size="xl"] #word-container {
  font-size: 2.3rem;
  line-height: 3.1rem;
}

html[data-live-wpm="off"] .live-wpm-hero {
  display: none;
}

/* Smooth caret — one floating element that glides between characters
   (the per-character ::before can't be transitioned, it jumps parents).
   A child of #word-track (home.html markup) so the virtual scroll
   carries it; positioned per-frame by the ease loop in home.html, which
   is why there's NO CSS transition on transform here — it would fight
   the JS. Takes the same --caret-* paint as everything else, so an
   equipped cursor skin shows here too. */
.live-caret {
  display: none;
  position: absolute;
  left: 0;
  top: 0;
  width: var(--caret-width);
  height: 1em;
  border-radius: 1px;
  background: var(--caret-bg);
  background-size: var(--caret-bg-size);
  box-shadow: var(--caret-glow);
  clip-path: var(--caret-clip);
  pointer-events: none;
  transform: translate(0, 0);
}

html[data-smooth-caret="on"] .live-caret {
  display: block;
  will-change: transform;
}

html[data-smooth-caret="on"] .char.current::before {
  display: none;
}

/* Block + smooth: the ease loop sizes the floating caret to the whole
   character cell, and it paints on top of the glyph — so drop it to a
   translucent wash and kill the outer glow, or it's a solid slab over the
   letter it marks. Bar and underline stay fully opaque (they sit beside /
   under the glyph, not over it). */
html[data-caret-shape="block"][data-smooth-caret="on"] .live-caret {
  opacity: 0.4;
  border-radius: 3px;
  box-shadow: none;
}

/* =========================================================
   CURSOR SKINS
   Matches ShopItem slugs (item_type="cursor"). No data-cursor
   attribute = the default accent-colored blinking bar above.

   Each rule targets both the real typing-test cursor AND a small
   demo bar in the customize modal ([data-cursor-demo="slug"]) so
   equipping shows an unmistakable live preview even on pages that
   have no typing test (Shop, Dashboard, etc).
========================================================= */

.cursor-demo-bar {
  display: inline-block;
  width: var(--caret-width);
  height: 26px;
  border-radius: 2px;
  background: var(--caret-bg);
  background-size: var(--caret-bg-size);
  box-shadow: var(--caret-glow);
  clip-path: var(--caret-clip);
  animation: var(--caret-anim);
}

body[data-cursor="neon-cursor"],
[data-cursor-demo="neon-cursor"] {
  --caret-bg: #00f7ff;
  --caret-glow: 0 0 10px #00f7ff, 0 0 22px rgba(0, 247, 255, 0.6);
  --caret-anim: cursorGlowPulse 1s ease-in-out infinite;
}
@keyframes cursorGlowPulse { 0%, 100% { opacity: 1; } 50% { opacity: 0.55; } }

/* Fixed, always-vivid colour, not var(--accent) — reusing the accent
   made this indistinguishable from the default caret on every theme. */
body[data-cursor="pulse-cursor"],
[data-cursor-demo="pulse-cursor"] {
  --caret-width: 4px;
  --caret-bg: #ec4899;
  --caret-glow: 0 0 10px rgba(236, 72, 153, 0.6);
  --caret-anim: cursorPulse 0.7s ease-in-out infinite;
}
@keyframes cursorPulse {
  0%, 100% { transform: scaleY(1) scaleX(1); opacity: 1; }
  50% { transform: scaleY(0.5) scaleX(1.6); opacity: 0.65; }
}

body[data-cursor="fire-cursor"],
[data-cursor-demo="fire-cursor"] {
  --caret-width: 3px;
  --caret-bg: linear-gradient(to bottom, #ff5722, #ffc107);
  --caret-glow: 0 0 10px rgba(255, 87, 34, 0.7);
  --caret-anim: cursorFlicker 0.4s ease-in-out infinite;
}
@keyframes cursorFlicker {
  0%, 100% { opacity: 1; transform: scaleY(1); }
  50% { opacity: 0.75; transform: scaleY(0.92); }
}

body[data-cursor="lightning-cursor"],
[data-cursor-demo="lightning-cursor"] {
  --caret-width: 3px;
  --caret-bg: #fde047;
  --caret-glow: 0 0 14px #fde047, 0 0 28px rgba(253, 224, 71, 0.5);
  --caret-anim: cursorFlash 0.3s steps(2) infinite;
}
@keyframes cursorFlash { 50% { opacity: 0.25; } }

body[data-cursor="rainbow-trail"],
[data-cursor-demo="rainbow-trail"] {
  --caret-width: 3px;
  --caret-bg: linear-gradient(to bottom, #ff5252, #ffb74d, #fff176, #66bb6a, #42a5f5, #ba68c8);
  --caret-bg-size: 100% 300%;
  --caret-glow: 0 0 12px rgba(255, 255, 255, 0.4);
  --caret-anim: rainbowShift 2s linear infinite;
}
@keyframes rainbowShift {
  0% { background-position: 0 0%; }
  100% { background-position: 0 100%; }
}

body[data-cursor="toxic-cursor"],
[data-cursor-demo="toxic-cursor"] {
  --caret-width: 3px;
  --caret-bg: linear-gradient(to bottom, #a3e635, #4d7c0f);
  --caret-glow: 0 0 10px rgba(163, 230, 53, 0.7), 0 0 20px rgba(77, 124, 15, 0.4);
  --caret-anim: cursorToxicDrip 1.1s ease-in-out infinite;
}
@keyframes cursorToxicDrip {
  0%, 100% { transform: scaleY(1); opacity: 1; }
  50% { transform: scaleY(1.15) translateY(2px); opacity: 0.8; }
}

/* Legendary — a deep violet glow that visibly pulses outward. */
body[data-cursor="void-cursor"],
[data-cursor-demo="void-cursor"] {
  --caret-width: 4px;
  --caret-bg: #1e1033;
  --caret-glow: 0 0 8px #7c3aed, 0 0 18px rgba(124, 58, 237, 0.7), 0 0 32px rgba(124, 58, 237, 0.4);
  --caret-anim: cursorVoidPulse 1.4s ease-in-out infinite;
}
@keyframes cursorVoidPulse {
  0%, 100% { box-shadow: 0 0 8px #7c3aed, 0 0 18px rgba(124, 58, 237, 0.7), 0 0 32px rgba(124, 58, 237, 0.4); }
  50% { box-shadow: 0 0 14px #a855f7, 0 0 28px rgba(168, 85, 247, 0.85), 0 0 48px rgba(124, 58, 237, 0.6); }
}

/* Limited to 40 — shifts between gold and diamond-white. */
body[data-cursor="crown-jewels-cursor"],
[data-cursor-demo="crown-jewels-cursor"] {
  --caret-width: 4px;
  --caret-bg: linear-gradient(to bottom, #fde047, #fff, #eab308);
  --caret-bg-size: 100% 200%;
  --caret-glow: 0 0 10px #fde047, 0 0 22px rgba(253, 224, 71, 0.6);
  --caret-anim: cursorJewelShimmer 1.6s ease-in-out infinite;
}
@keyframes cursorJewelShimmer {
  0%, 100% { background-position: 0 0%; box-shadow: 0 0 10px #fde047, 0 0 22px rgba(253, 224, 71, 0.6); }
  50% { background-position: 0 100%; box-shadow: 0 0 16px #fff, 0 0 30px rgba(255, 255, 255, 0.8); }
}

body[data-cursor="ice-cursor"],
[data-cursor-demo="ice-cursor"] {
  --caret-width: 3px;
  --caret-bg: #38bdf8;
  --caret-glow: 0 0 10px #38bdf8, 0 0 20px rgba(56, 189, 248, 0.5);
  --caret-anim: cursorIceGlint 1.2s ease-in-out infinite;
}
@keyframes cursorIceGlint { 0%, 100% { opacity: 1; } 50% { opacity: 0.7; } }

/* Dark bar, bright silver glow so it stays visible on any theme. */
body[data-cursor="shadow-cursor"],
[data-cursor-demo="shadow-cursor"] {
  --caret-width: 4px;
  --caret-bg: #1c1917;
  --caret-glow: 0 0 8px #a8a29e, 0 0 18px rgba(168, 162, 158, 0.5);
  --caret-anim: cursorShadowWisp 1.8s ease-in-out infinite;
}
@keyframes cursorShadowWisp {
  0%, 100% { transform: scaleX(1); box-shadow: 0 0 8px #a8a29e, 0 0 18px rgba(168, 162, 158, 0.5); }
  50% { transform: scaleX(1.8); box-shadow: 0 0 12px #d6d3d1, 0 0 26px rgba(214, 211, 209, 0.6); }
}

body[data-cursor="plasma-cursor"],
[data-cursor-demo="plasma-cursor"] {
  --caret-width: 3px;
  --caret-bg: linear-gradient(to bottom, #22d3ee, #a855f7);
  --caret-glow: 0 0 10px #22d3ee, 0 0 20px rgba(168, 85, 247, 0.6);
  --caret-anim: cursorPlasmaArc 0.5s steps(3) infinite;
}
@keyframes cursorPlasmaArc {
  0%, 100% { transform: translateX(0); }
  33% { transform: translateX(-1px); }
  66% { transform: translateX(1px); }
}

body[data-cursor="starlight-cursor"],
[data-cursor-demo="starlight-cursor"] {
  --caret-width: 3px;
  --caret-bg: #fff;
  --caret-glow: 0 0 8px #fff, 0 0 16px rgba(255, 255, 255, 0.7), 0 0 26px rgba(199, 210, 254, 0.5);
  --caret-anim: cursorStarTwinkle 1.5s ease-in-out infinite;
}
@keyframes cursorStarTwinkle {
  0%, 100% { opacity: 1; box-shadow: 0 0 8px #fff, 0 0 16px rgba(255, 255, 255, 0.7), 0 0 26px rgba(199, 210, 254, 0.5); }
  50% { opacity: 0.6; box-shadow: 0 0 4px #fff, 0 0 10px rgba(255, 255, 255, 0.4); }
}

/* Premium (Gems). clip-path facets the caret into a gem-cut silhouette
   (dropped by the Block/Underline caret shapes). */
body[data-cursor="diamond-cursor"],
[data-cursor-demo="diamond-cursor"] {
  --caret-width: 6px;
  --caret-bg: linear-gradient(160deg, #e0f2fe, #67e8f9 35%, #0ea5e9 70%, #e0f2fe);
  --caret-clip: polygon(50% 0%, 100% 15%, 100% 85%, 50% 100%, 0% 85%, 0% 15%);
  --caret-glow: 0 0 10px #67e8f9, 0 0 20px rgba(14, 165, 233, 0.6);
  --caret-anim: cursorDiamondSparkle 1.6s ease-in-out infinite;
}
@keyframes cursorDiamondSparkle {
  0%, 100% { filter: brightness(1); box-shadow: 0 0 10px #67e8f9, 0 0 20px rgba(14, 165, 233, 0.6); }
  50% { filter: brightness(1.3); box-shadow: 0 0 16px #fff, 0 0 30px rgba(103, 232, 249, 0.8); }
}

/* Premium (Gems). A radial "core" under the flame body, 3-stop glow. */
body[data-cursor="phoenix-cursor"],
[data-cursor-demo="phoenix-cursor"] {
  --caret-width: 4px;
  --caret-bg:
    radial-gradient(circle at 50% 20%, #fff7ed 0%, transparent 55%),
    linear-gradient(to bottom, #fde047, #f97316 45%, #7c2d12);
  --caret-glow: 0 0 10px #f97316, 0 0 22px rgba(249, 115, 22, 0.6), 0 0 34px rgba(124, 45, 18, 0.4);
  --caret-anim: cursorPhoenixRise 0.8s ease-in-out infinite;
}
@keyframes cursorPhoenixRise {
  0%, 100% { transform: scaleY(1) translateY(0); }
  50% { transform: scaleY(1.12) translateY(-1px); }
}

/* --- Limited-collection cursors (see main/limited.py) --- */

/* Founder's Mark — a slow gold specular sweep down a warm bar, matched
   to the Founder theme's brushed-gold frame. */
body[data-cursor="founder-cursor"],
[data-cursor-demo="founder-cursor"] {
  --caret-width: 4px;
  --caret-bg: linear-gradient(to bottom, #fff4d6, #f5c451 45%, #b8860b);
  --caret-bg-size: 100% 220%;
  --caret-glow: 0 0 10px #f5c451, 0 0 24px rgba(245, 196, 81, 0.55);
  --caret-anim: cursorFounderSweep 2.4s ease-in-out infinite;
}
@keyframes cursorFounderSweep {
  0%, 100% { background-position: 0 100%; }
  50%      { background-position: 0 0; }
}

/* Hollow Flame — jack-o'-lantern ember, an uneasy orange flicker. */
body[data-cursor="hollow-flame"],
[data-cursor-demo="hollow-flame"] {
  --caret-width: 3px;
  --caret-bg: linear-gradient(to bottom, #fde047, #f97316 55%, #7c2d12);
  --caret-glow: 0 0 10px #f97316, 0 0 20px rgba(124, 45, 18, 0.6);
  --caret-anim: cursorHollowFlicker 0.5s ease-in-out infinite;
}
@keyframes cursorHollowFlicker {
  0%, 100% { opacity: 1; transform: scaleY(1); }
  35% { opacity: 0.72; transform: scaleY(1.1); }
  70% { opacity: 0.9; transform: scaleY(0.94); }
}

/* First Frost — a pale ice shard with a crisp glint. */
body[data-cursor="first-frost"],
[data-cursor-demo="first-frost"] {
  --caret-width: 3px;
  --caret-bg: linear-gradient(to bottom, #ffffff, #bae6fd 50%, #38bdf8);
  --caret-glow: 0 0 10px #7dd3fc, 0 0 20px rgba(56, 189, 248, 0.5);
  --caret-anim: cursorFrostGlint 1.8s ease-in-out infinite;
}
@keyframes cursorFrostGlint {
  0%, 100% { filter: brightness(1); }
  50% { filter: brightness(1.35); }
}

/* Grandmaster — polished obsidian-and-platinum, a slow authoritative
   pulse. Earned, not bought (110 WPM PB). */
body[data-cursor="grandmaster"],
[data-cursor-demo="grandmaster"] {
  --caret-width: 4px;
  --caret-bg: linear-gradient(to bottom, #f8fafc, #cbd5e1 40%, #64748b 70%, #1e293b);
  --caret-glow: 0 0 8px rgba(226, 232, 240, 0.7), 0 0 22px rgba(148, 163, 184, 0.4);
  --caret-anim: cursorGrandmaster 2s ease-in-out infinite;
}
@keyframes cursorGrandmaster {
  0%, 100% { opacity: 1; box-shadow: var(--caret-glow); }
  50% { opacity: 0.85; box-shadow: 0 0 12px rgba(226, 232, 240, 0.9), 0 0 30px rgba(148, 163, 184, 0.6); }
}

/* =========================================================
   AVATARS
   Matches ShopItem slugs (item_type="avatar"). Rendered as
   [data-avatar="slug"] on any .avatar-badge circle — race lanes, the
   waiting room, results, leaderboard rows, friends list, profile hero.
   No avatar equipped = the fallback initial-letter span stays visible.
========================================================= */

.avatar-badge {
  position: relative;
}

.avatar-badge[data-avatar]:not([data-avatar=""]) .avatar-letter {
  visibility: hidden;
}

/* An equipped avatar is a full-color emoji — a saturated brand-gradient
   disc behind it just fights the emoji's own colors and buries it.
   Swap to a quiet neutral chip instead, so the emoji itself reads
   clearly. Wins on specificity over every individual .race-lane-avatar
   /.race-avatar-circle/.lb-avatar/etc. background rule (class + two
   attribute selectors beats their single class), so one rule covers
   every avatar badge on the site regardless of context. */
.avatar-badge[data-avatar]:not([data-avatar=""]) {
  background: var(--surface-2);
  border: 1px solid var(--border);
  box-shadow: none;
}

/* ::before (not ::after) deliberately — race lane/circle avatars also use
   ::after for the equipped trail cosmetic (see RACE MODE below), and a
   single pseudo-element can't serve both jobs at once. Layout only here,
   no "content", so the per-slug rules below (lower specificity, since
   they don't repeat the :not()) are free to set it without being
   overridden. */
.avatar-badge[data-avatar]:not([data-avatar=""])::before {
  position: absolute;
  inset: 0;
  display: flex;
  align-items: center;
  justify-content: center;
  font-size: 1.1em;
  line-height: 1;
}

.avatar-badge[data-avatar="cool-cat-avatar"]::before { content: "😼"; }
.avatar-badge[data-avatar="sly-fox-avatar"]::before { content: "🦊"; }
.avatar-badge[data-avatar="panda-avatar"]::before { content: "🐼"; }

/* Rare/epic/legendary avatars below all get a filter:drop-shadow glow
   scaled to their tier — before this, only Champion/Superhero/Genesis
   had any treatment and every other avatar (even legendary ones) was a
   bare emoji swap, identical visual weight to a 70-coin common avatar
   regardless of price. Rare = one subtle, slow color pulse; epic = a
   faster two-color pulse; legendary = a three-stop color cycle, one
   step below Genesis's shimmer. Colors picked per emoji. */
.avatar-badge[data-avatar="night-owl-avatar"]::before {
  content: "🦉";
  animation: avatarNightOwlGlow 2.4s ease-in-out infinite;
}

@keyframes avatarNightOwlGlow {
  0%, 100% { filter: drop-shadow(0 0 1.5px rgba(99, 102, 241, 0.5)); }
  50% { filter: drop-shadow(0 0 5px rgba(99, 102, 241, 0.85)); }
}

.avatar-badge[data-avatar="lone-wolf-avatar"]::before {
  content: "🐺";
  animation: avatarLoneWolfGlow 2.6s ease-in-out infinite;
}

@keyframes avatarLoneWolfGlow {
  0%, 100% { filter: drop-shadow(0 0 1.5px rgba(148, 163, 184, 0.5)); }
  50% { filter: drop-shadow(0 0 5px rgba(148, 163, 184, 0.85)); }
}

.avatar-badge[data-avatar="keybot-avatar"]::before {
  content: "🤖";
  animation: avatarKeybotGlow 2.2s ease-in-out infinite;
}

@keyframes avatarKeybotGlow {
  0%, 100% { filter: drop-shadow(0 0 1.5px rgba(34, 211, 238, 0.5)); }
  50% { filter: drop-shadow(0 0 5px rgba(34, 211, 238, 0.85)); }
}

.avatar-badge[data-avatar="dragon-avatar"]::before {
  content: "🐉";
  animation: avatarDragonGlow 1.4s ease-in-out infinite;
}

@keyframes avatarDragonGlow {
  0%, 100% { filter: drop-shadow(0 0 2px rgba(249, 115, 22, 0.6)); }
  50% { filter: drop-shadow(0 0 7px rgba(220, 38, 38, 0.9)); }
}

.avatar-badge[data-avatar="shark-avatar"]::before {
  content: "🦈";
  animation: avatarSharkGlow 1.5s ease-in-out infinite;
}

@keyframes avatarSharkGlow {
  0%, 100% { filter: drop-shadow(0 0 2px rgba(30, 64, 175, 0.6)); }
  50% { filter: drop-shadow(0 0 7px rgba(34, 211, 238, 0.9)); }
}

.avatar-badge[data-avatar="alien-avatar"]::before {
  content: "👽";
  animation: avatarAlienGlow 1.3s ease-in-out infinite;
}

@keyframes avatarAlienGlow {
  0%, 100% { filter: drop-shadow(0 0 2px rgba(163, 230, 53, 0.6)); }
  50% { filter: drop-shadow(0 0 7px rgba(168, 85, 247, 0.9)); }
}

.avatar-badge[data-avatar="phoenix-avatar"]::before {
  content: "🐦‍🔥";
  animation: avatarPhoenixGlow 2.4s ease-in-out infinite;
}

@keyframes avatarPhoenixGlow {
  0%, 100% { filter: drop-shadow(0 0 3px rgba(220, 38, 38, 0.8)); }
  33% { filter: drop-shadow(0 0 6px rgba(249, 115, 22, 0.9)); }
  66% { filter: drop-shadow(0 0 8px rgba(250, 204, 21, 0.95)); }
}

.avatar-badge[data-avatar="cosmic-avatar"]::before {
  content: "🌌";
  animation: avatarCosmicGlow 2.6s ease-in-out infinite;
}

@keyframes avatarCosmicGlow {
  0%, 100% { filter: drop-shadow(0 0 3px rgba(168, 85, 247, 0.8)); }
  33% { filter: drop-shadow(0 0 6px rgba(99, 102, 241, 0.9)); }
  66% { filter: drop-shadow(0 0 8px rgba(34, 211, 238, 0.9)); }
}

.avatar-badge[data-avatar="champion-avatar"]::before {
  content: "👑";
  animation: avatarChampionGlow 1.6s ease-in-out infinite;
}

@keyframes avatarChampionGlow {
  0%, 100% { filter: drop-shadow(0 0 2px rgba(250, 204, 21, 0.7)); }
  50% { filter: drop-shadow(0 0 7px rgba(250, 204, 21, 0.95)); }
}

/* Limited to 100 units ever — a shifting white-gold shimmer, distinct
   from Champion's steady gold glow, marking it as one of the earliest
   avatars anyone could ever claim. */
.avatar-badge[data-avatar="genesis-avatar"]::before {
  content: "🌟";
  animation: avatarGenesisShimmer 2s ease-in-out infinite;
}

@keyframes avatarGenesisShimmer {
  0%, 100% { filter: drop-shadow(0 0 2px rgba(255, 255, 255, 0.8)); }
  50% { filter: drop-shadow(0 0 8px rgba(250, 204, 21, 1)); }
}

.avatar-badge[data-avatar="ninja-avatar"]::before { content: "🥷"; }

.avatar-badge[data-avatar="wizard-avatar"]::before {
  content: "🧙";
  animation: avatarWizardGlow 2.3s ease-in-out infinite;
}

@keyframes avatarWizardGlow {
  0%, 100% { filter: drop-shadow(0 0 1.5px rgba(168, 85, 247, 0.5)); }
  50% { filter: drop-shadow(0 0 5px rgba(168, 85, 247, 0.85)); }
}

.avatar-badge[data-avatar="astronaut-avatar"]::before {
  content: "👨‍🚀";
  animation: avatarAstronautGlow 1.5s ease-in-out infinite;
}

@keyframes avatarAstronautGlow {
  0%, 100% { filter: drop-shadow(0 0 2px rgba(226, 232, 240, 0.6)); }
  50% { filter: drop-shadow(0 0 7px rgba(56, 189, 248, 0.9)); }
}

.avatar-badge[data-avatar="superhero-avatar"]::before {
  content: "🦸";
  animation: avatarSuperheroGlow 1.3s ease-in-out infinite;
}

@keyframes avatarSuperheroGlow {
  0%, 100% { filter: drop-shadow(0 0 2px rgba(59, 130, 246, 0.6)); }
  50% { filter: drop-shadow(0 0 6px rgba(220, 38, 38, 0.8)); }
}

#input-field {
  position: absolute;
  opacity: 0;
  pointer-events: none;
}

/* =========================================================
   RESULTS
========================================================= */

.results-shell {
  display: none;
  /* width + centring from the shell rule near the top */
  margin-top: 36px;
  padding-bottom: 56px;
}

.results-shell.active {
  display: block;
  /* A short delay lets scrollIntoView get moving first; fading in with a
     vertical slide (the old translateY) fights the scroll since both are
     vertical motion happening at once — scale instead reads as "arriving"
     rather than "sliding against the scroll". */
  animation: resultsReveal 0.4s ease 0.1s both;
}

@keyframes resultsReveal {
  from { opacity: 0; transform: scale(0.98); }
  to { opacity: 1; transform: scale(1); }
}

.results-hero {
  display: flex;
  flex-wrap: wrap;
  align-items: flex-start;
  gap: 16px 28px;
  margin-bottom: 20px;
}

.hero-main {
  flex: 1 1 240px;
  padding: 4px 0;
}

.hero-kicker {
  display: inline-block;
  font-size: 11px;
  font-weight: 700;
  letter-spacing: 0.16em;
  text-transform: uppercase;
  color: var(--muted);
  margin-bottom: 8px;
}

.hero-score-wrap {
  display: flex;
  align-items: baseline;
  gap: 18px;
  flex-wrap: wrap;
}

.hero-score {
  font-family: var(--font-display);
  font-size: 92px;
  line-height: 0.95;
  font-weight: 800;
  letter-spacing: -0.04em;
  color: var(--text);
}

.hero-score-meta span {
  font-size: 14px;
  color: var(--muted);
}

.hero-trend {
  margin-top: 6px;
  color: var(--success);
  font-weight: 700;
  font-size: 13px;
}

/* Scoped to .results-hero — a second, unrelated .hero-side (the
   dashboard welcome strip) also exists further down this file. */
.results-hero .hero-side {
  display: grid;
  grid-template-columns: repeat(3, minmax(0, 1fr));
  gap: 10px;
  flex: 1 1 320px;
}

.mini-stat {
  padding: 14px 16px;

  border-radius: var(--radius-lg);

  background: var(--surface);
  border: var(--card-border);
  box-shadow: var(--shadow-sm);
}

.mini-label {
  display: block;
  font-size: 11px;
  font-weight: 600;
  letter-spacing: 0.04em;
  text-transform: uppercase;
  color: var(--muted);
  margin-bottom: 6px;
}

.mini-value {
  font-family: var(--font-display);
  font-size: 26px;
  font-weight: 800;
  letter-spacing: -0.02em;
}

.performance-card,
.recent-card,
.chart-card {
  padding: 24px 26px;
  margin-bottom: 20px;
}

.section-head h3,
.section-header h3 {
  margin: 0 0 3px;
  font-size: 18px;
  font-weight: 700;
}

.section-head p,
.section-header p {
  margin: 0;
  color: var(--muted);
  font-size: 12.5px;
}

.insight-grid {
  display: grid;
  grid-template-columns: repeat(auto-fit, minmax(220px, 1fr));
  gap: 14px;
}

.insight-card {
  display: flex;
  align-items: center;
  gap: 14px;
  padding: 18px;
}

.insight-icon {
  width: 44px;
  height: 44px;
  flex-shrink: 0;

  display: flex;
  align-items: center;
  justify-content: center;
  border-radius: var(--radius-md);

  background: var(--accent-soft);
  color: var(--accent);
  font-size: 18px;
}

.insight-card h4 {
  margin: 0 0 2px;
  font-size: 14px;
  font-weight: 700;
}

.insight-card p {
  margin: 0;
  color: var(--muted);
  font-size: 12.5px;
}

.results-actions {
  margin-top: 20px;
  display: flex;
  gap: 10px;
  flex-wrap: wrap;
}

.primary-action {
  border: 1px solid transparent;
  padding: 12px 22px;
  border-radius: var(--radius-sm);

  font-family: inherit;
  font-weight: 700;
  font-size: 13.5px;
  color: var(--on-accent);

  background: var(--accent);
  transition: background var(--dur-1) var(--ease), transform var(--dur-1) var(--ease);
}

.primary-action:hover {
  background: var(--accent-hover);
  transform: translateY(-1px);
}

/* =========================================================
   COMMUNITY (leaderboards + activity)
========================================================= */

.community-wrapper {
  /* width + centring from the shell rule near the top */
  margin-block: 40px 80px;

  display: grid;
  grid-template-columns: repeat(3, 1fr);
  gap: 16px;
  align-items: start;
}

.community-card,
.leaderboard-card,
.coins-board-card {
  padding: 20px;
  min-height: 420px;
  max-height: 420px;

  display: flex;
  flex-direction: column;
  overflow: hidden;
}

.community-header,
.leaderboard-top,
.coins-board-header {
  display: flex;
  justify-content: space-between;
  align-items: flex-start;
  gap: 10px;
  margin-bottom: 14px;
}

.community-header h3,
.leaderboard-top h3,
.coins-board-header h2 {
  margin: 0;
  font-family: var(--font-display);
  font-size: 16px;
  font-weight: 700;
}

.community-header p,
.leaderboard-top p,
.coins-board-header p {
  margin: 3px 0 0;
  font-size: 12px;
  color: var(--muted);
}

.leaderboard-scope-toggle {
  display: flex;
  flex-wrap: wrap;
  gap: 5px;
  margin-bottom: 12px;
}

/* scope pills inherit the shared .pill rule (colour, active treatment) —
   only the tighter size lives here. */
.lb-scope-btn {
  padding: 5px 10px;
  font-size: 11px;
  font-weight: 600;
}

.live-pill,
.season-pill {
  display: flex;
  align-items: center;
  gap: 6px;
  flex-shrink: 0;

  padding: 5px 9px;
  border-radius: var(--radius-full);

  background: var(--surface-2);
  border: 1px solid var(--border);
  color: var(--muted);

  font-size: 10px;
  font-weight: 700;
  letter-spacing: 0.08em;
  text-transform: uppercase;
}

.live-dot {
  width: 6px;
  height: 6px;
  border-radius: 50%;
  background: var(--success);
  box-shadow: 0 0 0 3px color-mix(in srgb, var(--success) 25%, transparent);
  animation: pulseDot 1.5s infinite;
}

@keyframes pulseDot {
  0%, 100% { transform: scale(1); opacity: 1; }
  50% { transform: scale(1.4); opacity: 0.6; }
}

.activity-feed,
.leaderboard-list,
.coins-board-list {
  flex: 1;
  overflow-y: auto;

  display: flex;
  flex-direction: column;
  gap: 8px;

  padding-right: 4px;
  margin: 0;
  list-style: none;
}

.activity-feed::-webkit-scrollbar,
.leaderboard-list::-webkit-scrollbar,
.coins-board-list::-webkit-scrollbar {
  width: 5px;
}

/* A single empty line of text at the top of a 420px card left a wall of
   dead space below it — this fills and centers within whatever room the
   parent flex column gives it instead. */
.empty-state-mini {
  flex: 1;
  display: flex;
  flex-direction: column;
  align-items: center;
  justify-content: center;
  gap: 8px;

  text-align: center;
  padding: 20px;

  color: var(--muted);
  font-size: 13px;
  line-height: 1.5;
}

.empty-state-mini .empty-state-icon {
  font-size: 28px;
  opacity: 0.6;
}

.activity-feed::-webkit-scrollbar-thumb,
.leaderboard-list::-webkit-scrollbar-thumb,
.coins-board-list::-webkit-scrollbar-thumb {
  background: var(--border);
  border-radius: 999px;
}

.lb-row,
.coin-rank-row,
.activity-item {
  display: flex;
  align-items: center;
  gap: 11px;

  padding: 10px 11px;
  border-radius: var(--radius-md);

  background: var(--surface-2);
  border: 1px solid transparent;
  transition: border-color var(--dur-1) var(--ease), background var(--dur-1) var(--ease);
}

.lb-row:hover,
.coin-rank-row:hover,
.activity-item:hover {
  border-color: var(--border-strong);
  background: var(--surface-3);
}

/* Legendary+ purchases get a quiet warm frame so a rare unlock stands
   out from the usual common-tier stream — the whole value of a public
   feed is seeing someone else get something you don't have yet. */
.activity-item-exclusive {
  border-color: color-mix(in srgb, var(--warning) 45%, transparent);
  background: color-mix(in srgb, var(--warning) 8%, var(--surface-2));
  animation: activityExclusiveGlow 2.6s ease-in-out infinite;
}

@keyframes activityExclusiveGlow {
  0%, 100% { box-shadow: 0 0 0 1px color-mix(in srgb, var(--warning) 12%, transparent); }
  50% { box-shadow: 0 0 0 1px color-mix(in srgb, var(--warning) 32%, transparent),
                    0 0 16px -2px color-mix(in srgb, var(--warning) 22%, transparent); }
}

.lb-rank,
.coin-rank-number {
  width: 28px;
  height: 28px;
  flex-shrink: 0;

  display: flex;
  align-items: center;
  justify-content: center;
  border-radius: var(--radius-sm);

  background: var(--surface);
  border: 1px solid var(--border);

  font-size: 11px;
  font-weight: 800;
  font-variant-numeric: tabular-nums;
  color: var(--muted);
}

/* Podium — a metal tint + ring rather than a solid pastel block, so it
   sits right on a dark surface too. */
.rank-1 { background: color-mix(in srgb, #f5c451 20%, var(--surface)); color: #f5c451; border-color: color-mix(in srgb, #f5c451 45%, transparent); }
.rank-2 { background: color-mix(in srgb, #c8cdd8 18%, var(--surface)); color: #cdd2dc; border-color: color-mix(in srgb, #c8cdd8 40%, transparent); }
.rank-3 { background: color-mix(in srgb, #d08a52 18%, var(--surface)); color: #db9666; border-color: color-mix(in srgb, #d08a52 42%, transparent); }

.lb-avatar {
  width: 28px;
  height: 28px;
  flex-shrink: 0;

  display: flex;
  align-items: center;
  justify-content: center;
  border-radius: 50%;

  background: var(--accent-soft);
  color: var(--accent);

  font-size: 11px;
  font-weight: 800;
}

.lb-user,
.coin-user-info,
.activity-content {
  flex: 1;
  min-width: 0;
  display: flex;
  flex-direction: column;
}

.lb-user a,
.coin-user-info strong,
.activity-text strong {
  font-size: 12.5px;
  font-weight: 700;
  color: var(--text);
}

.lb-user a:hover,
.coin-user-info strong a:hover,
.activity-text strong a:hover {
  color: var(--accent);
}

.lb-sub,
.coin-user-info span,
.activity-meta {
  margin-top: 2px;
  font-size: 11px;
  color: var(--muted);
}

.lb-wpm,
.coin-total,
.activity-coins {
  font-size: 13px;
  font-weight: 800;
  font-variant-numeric: tabular-nums;
  white-space: nowrap;
}

.lb-wpm span,
.coin-total span {
  margin-left: 3px;
  font-size: 10px;
  font-weight: 600;
  color: var(--muted);
}

.activity-avatar {
  width: 32px;
  height: 32px;
  flex-shrink: 0;
  border-radius: var(--radius-sm);

  display: flex;
  align-items: center;
  justify-content: center;

  background: var(--accent-soft);
  font-size: 14px;
}

.activity-text {
  font-size: 13px;
  color: var(--text);
}

.activity-text .reward {
  font-weight: 700;
  color: var(--accent);
}

.activity-coins.positive { color: var(--success); }
.activity-coins:not(.positive) { color: var(--danger); }

@media (max-width: 1100px) {
  .community-wrapper {
    grid-template-columns: 1fr;
  }
}

/* =========================================================
   DASHBOARD
========================================================= */

.dashboard-shell {
  /* width + centring from the shell rule near the top */
  margin-block: 0 80px;

  display: flex;
  flex-direction: column;
  gap: 20px;
}

.dashboard-hero {
  display: flex;
  gap: 20px;
  flex-wrap: wrap;
}

/* =========================================================
   ACCOUNT SETTINGS
========================================================= */

/* .settings-shell width from the narrow-shell rule near the top. */

.settings-heading {
  font-family: var(--font-display);
  font-size: 22px;
  margin: 0;
}

.settings-card {
  padding: 24px;
}

.settings-card form {
  margin-top: 16px;
}

.settings-card label {
  display: block;
  font-size: 12px;
  font-weight: 700;
  letter-spacing: 0.03em;
  text-transform: uppercase;
  color: var(--muted);
  margin: 14px 0 5px;
}

.settings-card label:first-child {
  margin-top: 0;
}

/* input / textarea styling comes from the shared .field rule in PRIMITIVES. */
.settings-card textarea {
  resize: vertical;
}

.settings-field-note {
  min-height: 1.4em;
  margin: 6px 0 0;
  font-size: 13px;
  font-weight: 600;
}

.settings-field-note.available {
  color: var(--success);
}

.settings-field-note.unavailable {
  color: var(--danger);
}

.settings-submit {
  margin-top: 16px;
  width: auto;
}

a.settings-submit {
  display: inline-flex;
  align-items: center;
  text-decoration: none;
}

.hero-profile {
  flex: 2;
  min-width: 280px;

  display: flex;
  align-items: flex-start;
  gap: 18px;
  padding: 28px;
}

.hero-avatar {
  width: 60px;
  height: 60px;
  flex-shrink: 0;
  border-radius: var(--radius-md);

  display: flex;
  align-items: center;
  justify-content: center;

  font-family: var(--font-display);
  font-size: 22px;
  font-weight: 800;
  color: var(--on-accent);

  background: var(--accent);
}

.hero-profile h1 {
  margin: 0;
  font-family: var(--font-display);
  font-size: 24px;
  font-weight: 700;
}

.hero-profile p {
  margin: 6px 0 0;
  color: var(--muted);
  font-size: 14px;
}

/* The dashboard's welcome-strip column — a distinct .hero-side from the
   results one (scoped to .results-hero above). */
.dashboard-hero .hero-side {
  flex: 1;
  min-width: 200px;

  display: flex;
  flex-direction: column;
  gap: 12px;
}

.mini-card {
  padding: 16px 18px;
}

.mini-card span {
  display: block;
  font-size: 12px;
  color: var(--muted);
  margin-bottom: 5px;
}

.mini-card strong {
  font-family: var(--font-display);
  font-size: 21px;
  font-weight: 800;
  font-variant-numeric: tabular-nums;
}

.mini-card-level {
  display: block;
  color: var(--text);
  transition: border-color 0.15s ease;
}

.mini-card-level:hover {
  border-color: var(--accent);
}

.mini-card-level span {
  font-size: 15px;
  font-weight: 700;
  color: var(--text);
  margin-bottom: 8px;
}

.mini-level-bar {
  height: 6px;
  border-radius: 999px;
  background: var(--surface-2);
  overflow: hidden;
  margin-bottom: 6px;
}

.mini-level-bar-fill {
  height: 100%;
  border-radius: inherit;
  background: var(--accent);
}

.mini-card-level small {
  color: var(--muted);
  font-size: 11.5px;
}

.stats-grid {
  display: grid;
  grid-template-columns: repeat(4, 1fr);
  gap: 16px;
}

.stat-card {
  padding: 20px;
}

.stat-card span {
  color: var(--muted);
  font-size: 12px;
  font-weight: 600;
  letter-spacing: 0.03em;
  text-transform: uppercase;
}

.stat-card h2,
.stat-card h3 {
  margin-top: 8px;
  font-family: var(--font-display);
  font-size: 32px;
  font-weight: 800;
  letter-spacing: -0.03em;
}

.stat-card.best {
  background: var(--accent);
  color: var(--on-accent);
  border: 1px solid transparent;
}

.stat-card.best span {
  color: color-mix(in srgb, var(--on-accent) 68%, var(--accent));
}

.dashboard-main {
  display: grid;
  grid-template-columns: 1.2fr 0.8fr;
  gap: 20px;
  /* Grid items stretch to match the tallest sibling by default — with up
     to 30 rows in the recent-tests list and nothing to expand into the
     leftover room, the shorter chart card was being stretched taller than
     its own content, leaving blank space below the chart. Size each card
     to its own content instead. */
  align-items: start;
}

.dashboard-main > * {
  /* Grid items default to min-width:auto, which refuses to shrink below
     the widest piece of content — the WPM chart's <canvas> in particular,
     which Chart.js sizes in actual pixels. Without this, the chart card
     forces the whole grid (and the page) wider than the viewport on
     narrow screens instead of the chart shrinking to fit. */
  min-width: 0;
}

.performance-header {
  display: flex;
  justify-content: space-between;
  align-items: flex-start;
  gap: 20px;
  margin-bottom: 24px;
}

/* .eyebrow is defined once in PRIMITIVES (muted, uppercase) — no override here. */

.performance-header h2 {
  margin: 0;
  font-family: var(--font-display);
  font-size: 24px;
  font-weight: 700;
  letter-spacing: -0.02em;
}

.performance-stats {
  display: flex;
  gap: 12px;
  flex-wrap: wrap;
}

.performance-stats .mini-stat,
.mini-stat {
  min-width: 96px;
  padding: 12px 16px;
}

.performance-stats .mini-stat span {
  display: block;
  font-size: 12px;
  color: var(--muted);
  margin-bottom: 4px;
}

.performance-stats .mini-stat strong {
  font-size: 20px;
  font-weight: 800;
}

.chart-wrap {
  position: relative;
  height: 320px;
  display: flex;
}

/* =========================================================
   ANALYTICS (internal, superuser-only dashboard)
========================================================= */

/* Full-width row below the traffic chart — each card gets real room
   instead of being squeezed into a narrow sidebar column, which is what
   was pushing long paths/referrers outside their box. */
.analytics-detail-grid {
  display: grid;
  grid-template-columns: repeat(auto-fit, minmax(300px, 1fr));
  gap: 20px;
  align-items: start;
}

.analytics-detail-grid > .card {
  min-width: 0;
}

/* .card is a bare primitive (background/border/shadow only, no padding —
   see .stat-card/.performance-card for the same pattern) — without this,
   every row's text and bars sit flush against the card's edge, reading
   as "escaping the container" even though nothing technically overflows. */
.analytics-detail-card {
  padding: 24px;
}

.analytics-group-label {
  margin: 18px 0 8px;
  font-size: 12px;
  font-weight: 700;
  color: var(--muted);
  text-transform: uppercase;
  letter-spacing: 0.5px;
}

.analytics-group-label:first-of-type {
  margin-top: 4px;
}

.analytics-bar-list {
  display: flex;
  flex-direction: column;
  gap: 12px;
}

.analytics-bar-row-top {
  display: flex;
  align-items: baseline;
  justify-content: space-between;
  gap: 12px;
  margin-bottom: 6px;
  font-size: 13px;
}

.analytics-bar-row-label {
  color: var(--text);
  font-weight: 600;
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
  min-width: 0;
  flex: 1 1 auto;
}

.analytics-bar-row-meta {
  color: var(--muted);
  flex-shrink: 0;
  font-size: 12px;
}

.analytics-bar-track {
  height: 6px;
  border-radius: 4px;
  background: var(--surface-2);
  overflow: hidden;
}

.analytics-bar-fill {
  height: 100%;
  border-radius: 4px;
  background: var(--accent);
}

.analytics-empty {
  color: var(--muted);
  text-align: center;
  padding: 12px 0;
}

.analytics-footnote {
  margin: 0;
  font-size: 12px;
  color: var(--muted);
}

.recent-tests,
.recent-feed {
  display: flex;
  flex-direction: column;
  gap: 10px;

  max-height: 420px;
  overflow-y: auto;
  padding-right: 4px;
}

.test-item,
.activity-row {
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: 14px;

  padding: 14px 16px;
  border-radius: var(--radius-md);

  background: var(--surface-2);
  transition: transform 0.15s ease;
}

.test-item:hover,
.activity-row:hover {
  background: var(--surface-3);
}

.test-left,
.activity-left {
  display: flex;
  align-items: center;
  gap: 12px;
}

.activity-score {
  width: 46px;
  height: 46px;
  flex-shrink: 0;
  border-radius: var(--radius-sm);

  display: flex;
  align-items: center;
  justify-content: center;

  background: var(--accent-soft);
  color: var(--accent);
  font-family: var(--font-display);
  font-weight: 800;
  font-variant-numeric: tabular-nums;
}

.test-main,
.activity-meta {
  display: flex;
  flex-direction: column;
  gap: 4px;
}

.test-wpm {
  font-family: var(--font-display);
  font-size: 19px;
  font-weight: 800;
  font-variant-numeric: tabular-nums;
}

.test-wpm span,
.wpm {
  font-size: 12px;
  color: var(--muted);
  font-weight: 500;
}

.wpm {
  font-weight: 800;
  font-size: inherit;
  color: var(--accent);
}

.mode-pill {
  display: inline-block;
  padding: 5px 10px;
  border-radius: 999px;
  background: var(--accent-soft);
  color: var(--accent);
  font-size: 11px;
  font-weight: 700;
  width: fit-content;
}

.mode-pill-quote {
  display: inline-flex;
  align-items: center;
  gap: 4px;
  background: var(--surface-2, var(--surface));
  color: var(--muted);
  border: 1px solid var(--border);
}

.mode,
.date {
  font-size: 12px;
  color: var(--muted);
}

.accuracy,
.accuracy-pill {
  font-weight: 700;
  color: var(--success);
}

.accuracy-pill {
  padding: 7px 12px;
  border-radius: var(--radius-full);
  background: color-mix(in srgb, var(--success) 14%, transparent);
  font-size: 13px;
}

/* Badge showcase */

.badge-showcase {
  margin-top: 20px;
  width: 100%;
}

.badge-showcase-head {
  display: flex;
  justify-content: space-between;
  align-items: flex-end;
  gap: 16px;
  margin-bottom: 14px;
}

.badge-showcase-head h3 {
  margin: 0;
  font-size: 16px;
}

.badge-link {
  font-size: 13px;
  font-weight: 600;
  color: var(--accent);
}

/* Flexbox, not grid — with an auto-fit grid, an incomplete last row
   (e.g. 3 items in a 2-column layout) leaves a visible gap instead of
   the lone item stretching to fill it, since grid tracks are fixed by
   the container regardless of how many items land in a given row.
   Flexbox's basis+grow naturally fills whatever space is left. */
.badge-grid {
  display: flex;
  flex-wrap: wrap;
  gap: 12px;
}

.badge-grid .badge-card {
  flex: 1 1 220px;
}

/* =========================================================
   PROFILE PAGE EXTRAS — Equipped and Trophy Case are now separate
   cards instead of one card with subsections, so they read as
   distinct moments on the page instead of one dense contained block.
   Reuses the .badge-card/.badge-icon-wrap pattern already proven on
   this page for the real Badges section below.
========================================================= */

.hero-visitors {
  margin: 4px 0 0;
  font-size: 13px;
  color: var(--muted);
}

.hero-visitors a {
  color: var(--text);
}

.profile-bio {
  margin: 6px 0 0;
  font-size: 14px;
  color: var(--text);
  max-width: 480px;
}

.profile-bio-empty {
  color: var(--muted);
}

.profile-bio-empty a {
  color: var(--accent);
  font-weight: 600;
}

.hero-level-bar {
  margin: 10px 0 0;
  max-width: 320px;
}

.hero-level-bar small {
  color: var(--muted);
  font-size: 11.5px;
}

/* Loadout: a plain label/value ledger for equipped items and a real
   table for the showcase — no colored chips or tiles, rarity reads as
   text, matching the collection list below instead of fighting it. */
.loadout-card {
  padding: 24px;
}

.ledger {
  display: flex;
  flex-direction: column;
}

.ledger-row {
  display: flex;
  align-items: center;
  gap: 10px;
  padding: 10px 12px;
  margin: 0 -12px;
  border-radius: var(--radius-sm);
  border-bottom: 1px solid var(--border);
  color: var(--text);
  transition: background 0.15s ease;
}

.ledger-row:last-child {
  border-bottom: none;
}

a.ledger-row:hover {
  background: var(--surface-2);
}

a.ledger-row:hover .ledger-value {
  text-decoration: underline;
}

.ledger-label {
  /* Wide enough for "background" (the longest category label) — at
     74px it overflowed and visually ran into the value text next to it. */
  width: 94px;
  flex-shrink: 0;
  white-space: nowrap;
  font-size: 12px;
  font-weight: 700;
  text-transform: uppercase;
  letter-spacing: 0.03em;
  color: var(--muted);
}

.ledger-value {
  flex: 1;
  font-size: 13.5px;
  font-weight: 600;
}

.ledger-row .ic {
  font-size: 15px;
  margin-right: 2px;
}

.ledger-row.is-empty {
  color: var(--muted);
}

.ledger-row.is-empty .ledger-value {
  font-weight: 500;
}

.rt {
  font-size: 10.5px;
  font-weight: 700;
  text-transform: uppercase;
  letter-spacing: 0.03em;
}

.ledger-row .rt {
  margin-left: auto;
  flex-shrink: 0;
}

.loadout-empty-note {
  margin: 0;
  color: var(--muted);
  font-size: 13.5px;
}

.table-wrap {
  overflow-x: auto;
}

.showcase-table {
  width: 100%;
  border-collapse: collapse;
  font-size: 13.5px;
}

.showcase-table th {
  text-align: left;
  padding: 0 8px 8px 0;
  border-bottom: 1px solid var(--border);
  color: var(--muted);
  font-size: 11px;
  font-weight: 700;
  text-transform: uppercase;
  letter-spacing: 0.04em;
}

.showcase-table td {
  padding: 10px 8px 10px 0;
  border-bottom: 1px solid var(--border);
}

.showcase-table tr:last-child td {
  border-bottom: none;
}

.showcase-table .ic {
  font-size: 15px;
  margin-right: 2px;
}

.showcase-table a {
  color: var(--text);
  font-weight: 600;
}

.clickable-row {
  cursor: pointer;
  transition: background 0.15s ease;
}

.clickable-row:hover {
  background: var(--surface-2);
}

.clickable-row:hover a {
  text-decoration: underline;
}

/* Rarity as plain colored text/tags rather than colored shapes — reuses
   the .rarity-* text colors already defined for the badge system. */
.rtag {
  display: inline-block;
  padding: 3px 9px;
  border-radius: 999px;
  font-size: 11px;
  font-weight: 700;
  text-transform: capitalize;
  background: var(--surface-2);
}

.rtag.rarity-rare { background: rgba(59, 130, 246, 0.1); }
.rtag.rarity-epic { background: rgba(168, 85, 247, 0.1); }
.rtag.rarity-legendary { background: rgba(250, 204, 21, 0.15); }
.rtag.rarity-mythical { background: rgba(236, 72, 153, 0.1); }
.rtag.rarity-exclusive { background: rgba(249, 115, 22, 0.1); }
.rtag.rarity-ultra { background: rgba(220, 38, 38, 0.12); }

/* Badges: a two-column collection list — icon, name, rarity as text —
   instead of a grid of colored tiles/cards. */
.badges-card {
  padding: 24px;
}

.badge-count-pill {
  flex-shrink: 0;
  padding: 5px 12px;
  border-radius: 999px;
  background: var(--surface-2);
  border: 1px solid var(--border);
  color: var(--muted);
  font-size: 12px;
  font-weight: 700;
  white-space: nowrap;
}

.badge-list {
  display: grid;
  grid-template-columns: 1fr 1fr;
  gap: 0 24px;
}

.badge-list-row {
  display: flex;
  align-items: center;
  gap: 10px;
  padding: 9px 8px;
  margin: 0 -8px;
  border-radius: var(--radius-sm);
  border-bottom: 1px solid var(--border);
  font-size: 13px;
  cursor: pointer;
  transition: background 0.15s ease;
}

.badge-list-row:hover,
.badge-list-row:focus-visible {
  background: var(--surface-2);
}

.badge-list-row:focus-visible {
  outline: 2px solid var(--accent);
  outline-offset: -2px;
}

.badge-list-row:nth-last-child(2),
.badge-list-row:last-child {
  border-bottom: none;
}

.badge-list-row .ic {
  width: 20px;
  text-align: center;
  font-size: 15px;
  flex-shrink: 0;
}

.badge-list-row .nm {
  flex: 1;
  min-width: 0;
  font-weight: 600;
  white-space: nowrap;
  overflow: hidden;
  text-overflow: ellipsis;
}

.badge-list-row .rt {
  flex-shrink: 0;
}

.badge-mini-hidden {
  display: none;
}

.badge-mini-grid-expanded .badge-mini-hidden {
  display: flex;
}

.badge-show-more {
  display: block;
  margin: 10px 0 0;
  padding: 0;
  background: none;
  border: none;

  color: var(--accent);
  font-size: 13px;
  font-weight: 700;
  cursor: pointer;
}

@media (max-width: 640px) {
  .badge-list { grid-template-columns: 1fr; }
}

.badge-show-more:hover {
  text-decoration: underline;
}

.badge-card {
  display: flex;
  align-items: center;
  gap: 12px;

  padding: 14px;
  border-radius: var(--radius-md);

  background: var(--surface-2);
  border: 1px solid var(--border);
  transition: transform 0.2s ease;
}

.badge-card:hover {
  transform: translateY(-3px);
}

.badge-icon-wrap {
  width: 48px;
  height: 48px;
  flex-shrink: 0;
  border-radius: var(--radius-sm);

  display: flex;
  align-items: center;
  justify-content: center;

  background: var(--surface);
  border: 1px solid var(--border);
  font-size: 20px;
}

.badge-title-row {
  display: flex;
  align-items: center;
  gap: 8px;
  flex-wrap: wrap;
  margin-bottom: 3px;
}

.badge-body strong {
  font-size: 14px;
}

.badge-body small {
  color: var(--muted);
  font-size: 12px;
}

.badge-rarity,
.rarity-pill,
.shop-rarity {
  display: inline-flex;
  align-items: center;
  padding: 3px 8px;
  border-radius: var(--radius-full);

  font-size: 10px;
  font-weight: 700;
  text-transform: uppercase;
  letter-spacing: 0.06em;

  background: var(--surface);
  border: 1px solid var(--border);
  color: var(--muted);
}

/* The rarity colour language — kept recognizable but each tier now also
   carries a faint tinted fill so it reads as a chip, and "rare" shifts
   off the exact accent blue so the two don't blur together. */
.rarity-common  { color: var(--muted); }
.rarity-rare      { color: #58b6f0; border-color: color-mix(in srgb, #58b6f0 40%, transparent); background: color-mix(in srgb, #58b6f0 10%, var(--surface)); }
.rarity-epic      { color: #b98cff; border-color: color-mix(in srgb, #b98cff 40%, transparent); background: color-mix(in srgb, #b98cff 10%, var(--surface)); }
.rarity-legendary { color: #e8b23c; border-color: color-mix(in srgb, #e8b23c 42%, transparent); background: color-mix(in srgb, #e8b23c 10%, var(--surface)); }
.rarity-mythical  { color: #f472b6; border-color: color-mix(in srgb, #f472b6 42%, transparent); background: color-mix(in srgb, #f472b6 10%, var(--surface)); }
/* Limited-tab rarities — sit above mythical; ordered by how hard to get. */
.rarity-seasonal  { color: #4ade80; border-color: color-mix(in srgb, #4ade80 42%, transparent); background: color-mix(in srgb, #4ade80 10%, var(--surface)); }
.rarity-prestige  { color: #cbd5e1; border-color: color-mix(in srgb, #cbd5e1 45%, transparent); background: color-mix(in srgb, #cbd5e1 12%, var(--surface)); }
.rarity-founder   { color: #f5c451; border-color: color-mix(in srgb, #f5c451 50%, transparent); background: color-mix(in srgb, #f5c451 14%, var(--surface)); }
/* legacy labels still referenced by pre-0054 badge/activity rows */
.rarity-exclusive { color: #f59e42; border-color: color-mix(in srgb, #f59e42 42%, transparent); background: color-mix(in srgb, #f59e42 10%, var(--surface)); }
.rarity-ultra     { color: #f2604f; border-color: color-mix(in srgb, #f2604f 45%, transparent); background: color-mix(in srgb, #f2604f 10%, var(--surface)); }

.badge-empty {
  margin-top: 18px;
  display: flex;
  align-items: center;
  gap: 16px;
  flex-wrap: wrap;

  padding: 18px;
  border-radius: var(--radius-md);

  background: var(--surface-2);
  border: 1px solid var(--border);
}

.empty-icon {
  width: 52px;
  height: 52px;
  flex-shrink: 0;
  border-radius: var(--radius-sm);

  display: flex;
  align-items: center;
  justify-content: center;

  background: var(--surface);
  border: 1px solid var(--border);
  font-size: 22px;
}

.badge-empty h3 {
  margin: 0 0 4px;
  font-size: 15px;
}

.badge-empty p {
  margin: 0;
  color: var(--muted);
  font-size: 13px;
  max-width: 420px;
}

@media (max-width: 950px) {
  .dashboard-main { grid-template-columns: 1fr; }
  .stats-grid { grid-template-columns: repeat(2, 1fr); }
  .dashboard-hero { flex-direction: column; }
}

@media (max-width: 640px) {
  .stats-grid { grid-template-columns: 1fr; }
  .hero-profile { flex-direction: column; text-align: center; }
}

/* =========================================================
   DAILY CHALLENGE
========================================================= */

.daily-challenge-bar {
  /* width + centring from the shell rule near the top */
  margin-bottom: 16px;

  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: 16px;
  flex-wrap: wrap;

  padding: 12px 16px;
}

.dc-info {
  display: flex;
  align-items: center;
  gap: 12px;
}

.dc-icon {
  width: 38px;
  height: 38px;
  flex-shrink: 0;
  border-radius: var(--radius-sm);

  display: flex;
  align-items: center;
  justify-content: center;
  font-size: 17px;

  background: var(--surface-2);
  border: 1px solid var(--border);
}

.dc-text {
  display: flex;
  flex-direction: column;
  gap: 1px;
}

.dc-text strong {
  font-size: 13px;
  font-weight: 700;
}

.dc-text span {
  font-size: 12.5px;
  color: var(--muted);
}

.dc-reward {
  flex-shrink: 0;
  padding: 8px 16px;
  border: none;
  border-radius: 999px;

  font-size: 13px;
  font-weight: 800;

  background: var(--accent-soft);
  color: var(--accent);
  white-space: nowrap;
}

.dc-reward.done {
  background: color-mix(in srgb, var(--success) 14%, transparent);
  color: var(--success);
}

.dc-reward.locked {
  background: var(--surface-2);
  color: var(--muted);
  cursor: pointer;
  transition: background 0.15s ease, color 0.15s ease;
}

.dc-reward.locked:hover {
  background: var(--accent-soft);
  color: var(--accent);
}

/* =========================================================
   ACHIEVEMENT PROGRESS (locked shop badges)
========================================================= */

.achievement-progress {
  margin-top: 10px;
}

.achievement-bar {
  height: 6px;
  border-radius: 999px;
  background: var(--surface-2);
  overflow: hidden;
  margin-bottom: 6px;
}

.achievement-bar-fill {
  height: 100%;
  border-radius: inherit;
  background: var(--accent);
  transition: width 0.3s ease;
}

.achievement-progress small {
  color: var(--muted);
  font-size: 11px;
}

/* =========================================================
   SHOP
========================================================= */

.shop-layout {
  width: 100%;
  margin: 0 0 80px;

  display: flex;
  gap: 24px;
  align-items: flex-start;
}

/* =========================================================
   ACHIEVEMENTS SECTION (shop page)
========================================================= */

.achievements-section {
  padding: 24px;
  margin-bottom: 28px;
}

.achievements-teaser {
  display: flex;
  align-items: center;
  gap: 16px;
  padding: 18px 22px;
  margin-bottom: 28px;

  color: var(--text);
  transition: transform 0.15s ease, border-color 0.15s ease;
}

.achievements-teaser:hover {
  transform: translateY(-2px);
  border-color: var(--accent);
}

.achievements-teaser-icon {
  font-size: 26px;
}

.achievements-teaser-body {
  flex: 1;
  min-width: 0;
}

.achievements-teaser-body h3 {
  font-size: 15px;
  margin: 0 0 2px;
}

.achievements-teaser-body p {
  font-size: 12.5px;
  color: var(--muted);
  margin: 0;
}

.achievements-grid {
  display: grid;
  grid-template-columns: repeat(auto-fill, minmax(260px, 1fr));
  gap: 14px;
  margin-top: 18px;
}

.achievement-card {
  display: flex;
  gap: 14px;
  padding: 16px;
  border-radius: var(--radius-md);

  background: var(--surface-2);
  border: 1px solid var(--border);
  opacity: 0.7;

  transition: transform 0.2s ease, opacity 0.2s ease;
}

.achievement-card.unlocked {
  opacity: 1;
  border-color: var(--accent);
}

.achievement-card:hover {
  transform: translateY(-2px);
}

.achievement-icon {
  width: 48px;
  height: 48px;
  flex-shrink: 0;
  border-radius: var(--radius-sm);

  display: flex;
  align-items: center;
  justify-content: center;
  font-size: 22px;

  background: var(--surface);
  border: 1px solid var(--border);
}

.achievement-body {
  flex: 1;
  min-width: 0;
}

.achievement-body h3 {
  font-size: 15px;
  margin: 0;
}

.achievement-status {
  margin-top: 8px;
  font-size: 12px;
  font-weight: 700;
  color: var(--muted);
}

.achievement-status.done {
  color: var(--success);
}

/* =========================================================
   ACHIEVEMENTS PAGE (level hero + tiered family ladders)
========================================================= */

.level-hero {
  display: flex;
  align-items: center;
  gap: 24px;
  padding: 24px;
  margin-bottom: 24px;
}

.level-hero-badge {
  display: flex;
  flex-direction: column;
  align-items: center;
  justify-content: center;
  width: 80px;
  height: 80px;
  flex-shrink: 0;
  border-radius: 50%;
  background: var(--accent);
  color: var(--on-accent);
}

.level-hero-number {
  font-family: var(--font-display);
  font-size: 26px;
  font-weight: 800;
  font-variant-numeric: tabular-nums;
  line-height: 1;
}

.level-hero-label {
  font-size: 10px;
  text-transform: uppercase;
  letter-spacing: 0.06em;
  opacity: 0.85;
}

.level-hero-progress {
  flex: 1;
  min-width: 0;
}

.level-hero-bar {
  height: 10px;
  border-radius: 999px;
  background: var(--surface-2);
  overflow: hidden;
  margin-bottom: 8px;
}

.level-hero-bar-fill {
  height: 100%;
  border-radius: inherit;
  background: var(--accent);
  transition: width 0.3s ease;
}

.level-hero-progress small {
  color: var(--muted);
  font-size: 12.5px;
  font-weight: 600;
}

.family-grid {
  display: grid;
  grid-template-columns: repeat(auto-fill, minmax(320px, 1fr));
  gap: 18px;
  margin-bottom: 60px;
}

.family-card {
  padding: 20px;
}

.family-card.maxed {
  border-color: var(--accent);
}

.family-head {
  display: flex;
  align-items: center;
  gap: 14px;
  margin-bottom: 16px;
}

.family-icon {
  width: 44px;
  height: 44px;
  flex-shrink: 0;
  border-radius: var(--radius-sm);

  display: flex;
  align-items: center;
  justify-content: center;
  font-size: 20px;

  background: var(--surface-2);
  border: 1px solid var(--border);
}

.family-head h3 {
  font-size: 15px;
  margin: 0 0 2px;
}

.family-count {
  font-size: 12px;
  color: var(--muted);
  font-weight: 600;
}

.tier-ladder {
  display: flex;
  gap: 8px;
  margin-bottom: 14px;
}

.tier-pip {
  flex: 1;
  display: flex;
  flex-direction: column;
  align-items: center;
  gap: 4px;
  padding: 10px 4px;
  border-radius: var(--radius-sm);

  background: var(--surface-2);
  border: 1px solid var(--border);
  opacity: 0.45;

  transition: transform 0.15s ease, opacity 0.15s ease;
}

.tier-pip.unlocked {
  opacity: 1;
  background: var(--surface);
}

.tier-pip:hover {
  transform: translateY(-2px);
}

.tier-pip-icon {
  font-size: 18px;
}

.tier-pip-label {
  font-size: 9.5px;
  font-weight: 700;
  text-transform: uppercase;
  letter-spacing: 0.03em;
  color: var(--muted);
}

.shop-sidebar {
  width: 190px;
  flex-shrink: 0;
  padding: 8px;
  position: sticky;
  top: 80px;
}

.shop-tab {
  padding: 9px 12px;
  border-radius: var(--radius-sm);
  color: var(--muted);
  font-size: 13.5px;
  font-weight: 600;
  cursor: pointer;
  transition: color var(--dur-1) var(--ease), background var(--dur-1) var(--ease);
}

.shop-tab:hover {
  background: var(--surface-2);
  color: var(--text);
}

.shop-tab.active {
  background: var(--pill-active-bg);
  color: var(--pill-active-fg);
}

/* Fixed indigo rather than the theme accent, same reasoning as .gem-pill
   — this tab needs to read as "real money" regardless of which theme is
   equipped. */
.shop-tab.gems-tab.active {
  background: color-mix(in srgb, #8b9dff 16%, transparent);
  color: #8b9dff;
}

.shop-content {
  flex: 1;
  min-width: 0;
}

.page-header {
  margin-bottom: 24px;
}

.page-header h1 {
  margin: 0;
  font-family: var(--font-display);
  font-size: 28px;
  font-weight: 700;
  letter-spacing: -0.02em;
}

.page-header p {
  margin: 6px 0 0;
  color: var(--muted);
}

.shop-grid {
  display: grid;
  grid-template-columns: repeat(auto-fill, minmax(240px, 1fr));
  gap: 18px;
}

.shop-card {
  position: relative;
  overflow: hidden;
  padding: 0;
}

/* Transient highlight for a card jumped to via a #shop-item-<slug> link
   (e.g. a Profile Room chip) — added/removed by JS, not meant to persist. */
.shop-card.jump-highlight {
  box-shadow: 0 0 0 2px var(--accent), var(--shadow-lg);
  animation: jumpHighlightPulse 0.9s ease-in-out 2;
}

@keyframes jumpHighlightPulse {
  0%, 100% { box-shadow: 0 0 0 2px var(--accent), var(--shadow-lg); }
  50% { box-shadow: 0 0 0 4px var(--accent-2), var(--shadow-lg); }
}

/* Limited-supply items get their own visual treatment site-wide — a gold
   ribbon + border — so scarcity reads at a glance, not just in the text. */
.shop-card.exclusive {
  border: 1px solid color-mix(in srgb, var(--warning) 42%, transparent);
  box-shadow: 0 0 0 1px color-mix(in srgb, var(--warning) 14%, transparent), var(--shadow-sm);
}

.exclusive-ribbon {
  position: absolute;
  top: 12px;
  right: -32px;
  transform: rotate(45deg);
  width: 120px;
  padding: 3px 0;
  text-align: center;

  background: linear-gradient(135deg, #f5c451, #dd9022);
  color: #241700;

  font-size: 10px;
  font-weight: 800;
  letter-spacing: 0.06em;

  z-index: 2;
  box-shadow: 0 2px 6px rgba(0, 0, 0, 0.2);
}

/* Gems-priced items (ShopItem.currency="gems") get a violet ribbon
   instead of gold — gold already means scarcity (max_supply/"LIMITED"),
   this means real-money-only, an independent axis (an item is never
   both, see shop.html/base.html's premium/exclusive branching).
   A static border alone read as "just another rarity color" next to
   the existing gold/pink/purple rarity badges — same glowing-pulse +
   gradient-tint treatment .activity-item-exclusive already uses to make
   a rare unlock stand out in the feed, so this reads as an established
   "this one's special" language rather than a one-off effect. */
/* --gem is the fixed "premium currency" indigo used for the gem pill and
   the gem-package cards too, so real-money items read the same on every
   theme. */
.shop-card.premium,
.customize-card.premium {
  --gem: #8b9dff;
  border-color: color-mix(in srgb, var(--gem) 50%, transparent);
  background: linear-gradient(135deg, color-mix(in srgb, var(--gem) 10%, var(--surface)), var(--surface));
  animation: premiumCardGlow 2.6s ease-in-out infinite;
}

@keyframes premiumCardGlow {
  0%, 100% { box-shadow: 0 0 0 1px color-mix(in srgb, var(--gem) 26%, transparent), var(--shadow-sm); }
  50% { box-shadow: 0 0 0 1px color-mix(in srgb, var(--gem) 55%, transparent), 0 0 22px -4px color-mix(in srgb, var(--gem) 40%, transparent), var(--shadow-sm); }
}

/* The icon itself gets a rotating rainbow aura behind it — every other
   shop card's preview box is a flat --surface-2 square, so this is the
   one visual cue that reads as "premium" even before you buy or equip
   it, previewing the shop grid instead of only reskinning the live site
   after purchase. */
.shop-card.premium .shop-preview {
  position: relative;
  background: var(--surface-2);
  overflow: hidden;
}

/* z-index: -1, not the default auto — .shop-preview's icon is plain
   text with no stacking context of its own, so a positioned ::before at
   the default stacking level would paint OVER it, not behind it.
   Negative z-index is the one layer guaranteed to sit below ordinary
   in-flow content within .shop-preview's own stacking context
   (established by its position: relative above). */
.shop-card.premium .shop-preview::before {
  content: "";
  position: absolute;
  inset: -40%;
  z-index: -1;
  background: conic-gradient(from 0deg, #ff2ec4, #a78bfa, #22d3ee, #34d399, #facc15, #ff2ec4);
  opacity: 0.35;
  filter: blur(20px);
  animation: prismSpin 6s linear infinite;
}

/* Same aura, scaled down for the navbar customize modal's smaller
   swatch — the modal is equipped/browsed at least as often as the shop
   page, so premium items need to stand out there too, not just on
   /shop/. */
.customize-card.premium .customize-swatch {
  position: relative;
  overflow: hidden;
}

.customize-card.premium .customize-swatch::before {
  content: "";
  position: absolute;
  inset: -60%;
  z-index: -1;
  background: conic-gradient(from 0deg, #ff2ec4, #a78bfa, #22d3ee, #34d399, #facc15, #ff2ec4);
  opacity: 0.4;
  filter: blur(10px);
  animation: prismSpin 6s linear infinite;
}

/* Prism Storm and Crystal Chime keep the shared rainbow above (both are
   explicitly prismatic concepts) — every other premium item gets its
   own aura color matched to its identity instead of the identical
   rainbow swirl, so each one reads as its own thing in the shop grid
   and the customize modal, not a copy-pasted "premium" stamp.
   base.html's renderCustomizeTab() sets card.dataset.slug so the
   [data-slug=...] selectors below can target the modal's cards too. */
#shop-item-celestial-eclipse .shop-preview::before,
.customize-card.premium[data-slug="celestial-eclipse"] .customize-swatch::before {
  background: conic-gradient(from 0deg, #78350f, #fbbf24, #fef3c7, #fbbf24, #78350f);
}

#shop-item-diamond-cursor .shop-preview::before,
.customize-card.premium[data-slug="diamond-cursor"] .customize-swatch::before {
  background: conic-gradient(from 0deg, #0c4a6e, #38bdf8, #e0f2fe, #38bdf8, #0c4a6e);
}

#shop-item-phoenix-cursor .shop-preview::before,
.customize-card.premium[data-slug="phoenix-cursor"] .customize-swatch::before {
  background: conic-gradient(from 0deg, #7c2d12, #f97316, #fde047, #dc2626, #7c2d12);
}

#shop-item-aurora-storm .shop-preview::before,
.customize-card.premium[data-slug="aurora-storm"] .customize-swatch::before {
  background: conic-gradient(from 0deg, #4c1d95, #2dd4bf, #86efac, #a78bfa, #4c1d95);
}

#shop-item-meteor-shower .shop-preview::before,
.customize-card.premium[data-slug="meteor-shower"] .customize-swatch::before {
  background: conic-gradient(from 0deg, #1e1b4b, #f97316, #facc15, #dc2626, #1e1b4b);
}

#shop-item-synth-wave .shop-preview::before,
.customize-card.premium[data-slug="synth-wave"] .customize-swatch::before {
  background: conic-gradient(from 0deg, #4a044e, #d946ef, #22d3ee, #d946ef, #4a044e);
}

.premium-ribbon {
  background: linear-gradient(135deg, #8b9dff, #5b6ef0);
  color: #fff;
  overflow: hidden;
}

.premium-ribbon::after {
  content: "";
  position: absolute;
  inset: 0;
  background: linear-gradient(115deg, transparent 40%, rgba(255, 255, 255, 0.55) 50%, transparent 60%);
  background-size: 220% 100%;
  animation: premiumRibbonShine 2.6s ease-in-out infinite;
}

@keyframes premiumRibbonShine {
  0% { background-position: 120% 0; }
  100% { background-position: -120% 0; }
}

.shop-scarcity {
  font-size: 12px;
  font-weight: 700;
  color: var(--warning);
  margin-top: 2px;
}

.shop-scarcity.sold-out {
  color: var(--danger);
}

.shop-preview {
  height: 124px;
  display: flex;
  align-items: center;
  justify-content: center;
  font-size: 42px;
  background: var(--surface-2);
  border-bottom: 1px solid var(--border);
}

.shop-info {
  padding: 15px 16px;
}

.shop-title-row {
  display: flex;
  justify-content: space-between;
  align-items: center;
  gap: 8px;
  margin-bottom: 10px;
}

.shop-card h3 {
  margin: 0;
  font-family: var(--font-display);
  font-size: 15px;
  font-weight: 700;
}

.shop-price {
  font-size: 15px;
  font-weight: 800;
  font-variant-numeric: tabular-nums;
  margin: 6px 0 12px;
}

/* Pricing card for a gem package (real money, not a ShopItem) — inherits
   .shop-card's shell (preview/info/title/price layout) so it slots into
   the grid looking consistent, this just adds the indigo accent border
   .shop-card.premium also uses, so "spend real money here" reads the
   same visual language throughout the page. */
.gem-package-card {
  border: 1px solid color-mix(in srgb, #8b9dff 36%, transparent);
  box-shadow: 0 0 0 1px color-mix(in srgb, #8b9dff 12%, transparent), var(--shadow-sm);
}

.gem-earn-card {
  grid-column: 1 / -1;
  padding: 18px 20px;
  border: 1px solid color-mix(in srgb, #8b9dff 30%, transparent);
  background: linear-gradient(135deg, color-mix(in srgb, #8b9dff 8%, var(--surface)), var(--surface));
}

.gem-earn-card h3 {
  font-family: var(--font-display);
  font-size: 15px;
  font-weight: 800;
}

.gem-earn-card p { color: var(--muted); font-size: 13px; margin-top: 5px; }

.gem-earn-card ul {
  margin: 10px 0 0;
  padding-left: 18px;
  columns: 2;
  font-size: 13px;
}

.gem-earn-card li { margin: 3px 0; }

.gem-earn-card .gem-earn-or {
  margin-top: 12px;
  font-weight: 600;
  color: color-mix(in srgb, #8b9dff 70%, var(--text));
}

@media (max-width: 520px) {
  .gem-earn-card ul { columns: 1; }
}

.gem-package-card .shop-price {
  color: #8b9dff;
}

.buy-btn,
.owned-btn {
  width: 100%;
  padding: 10px;
  border-radius: var(--radius-sm);
  border: 1px solid transparent;
  font-family: inherit;
  font-weight: 700;
  font-size: 13px;
  transition: background var(--dur-1) var(--ease);
}

.buy-btn {
  color: var(--on-accent);
  background: var(--accent);
}

.buy-btn:hover {
  background: var(--accent-hover);
}

.owned-btn {
  background: transparent;
  color: var(--muted);
  cursor: default;
  border-color: var(--border-strong);
}

/* ---------- Limited tab: Founder / Seasonal / Prestige ---------- */

.shop-section-head {
  grid-column: 1 / -1;
  margin: 6px 0 -4px;
  padding-left: 14px;
  border-left: 3px solid var(--accent);
}

.shop-section-head:not(:first-child) { margin-top: 22px; }
.shop-section-head.limited-founder  { border-left-color: #f5c451; }
.shop-section-head.limited-seasonal { border-left-color: #4ade80; }
.shop-section-head.limited-prestige { border-left-color: #cbd5e1; }

.shop-section-head h2 {
  font-family: var(--font-display);
  font-size: 17px;
  font-weight: 800;
  letter-spacing: -0.01em;
}

.shop-section-head p {
  margin-top: 3px;
  font-size: 12.5px;
  color: var(--muted);
}

.shop-card--limited {
  border: 1px solid color-mix(in srgb, var(--limited-accent, var(--accent)) 45%, transparent);
  box-shadow: 0 0 0 1px color-mix(in srgb, var(--limited-accent, var(--accent)) 12%, transparent), var(--shadow-sm);
}

.shop-card--limited.limited-founder  { --limited-accent: #f5c451; }
.shop-card--limited.limited-seasonal { --limited-accent: #4ade80; }
.shop-card--limited.limited-prestige { --limited-accent: #cbd5e1; }

.shop-card--limited .shop-preview {
  background:
    radial-gradient(120% 120% at 50% 0%, color-mix(in srgb, var(--limited-accent) 22%, transparent), transparent 70%),
    var(--surface-2);
}

.limited-tag {
  position: absolute;
  top: 10px;
  left: 10px;
  z-index: 2;
  padding: 3px 8px;
  border-radius: 999px;
  font-size: 9.5px;
  font-weight: 800;
  letter-spacing: 0.08em;
  color: #1a1305;
  background: var(--limited-accent);
}

.shop-card--limited.limited-prestige .limited-tag { color: #10131a; }

.limited-note {
  font-size: 12px;
  font-weight: 600;
  color: var(--muted);
  margin: 2px 0 4px;
}

.limited-note.state-open {
  color: color-mix(in srgb, var(--limited-accent) 70%, var(--text));
  font-weight: 700;
}

.limited-note.state-sold_out { color: var(--danger); }
.limited-note.state-out_of_season,
.limited-note.state-locked { color: var(--warning); }

/* dim a card you can't act on yet */
.shop-card--limited.state-locked,
.shop-card--limited.state-out_of_season,
.shop-card--limited.state-sold_out {
  opacity: 0.72;
}

.shop-card--limited.state-locked .shop-preview,
.shop-card--limited.state-out_of_season .shop-preview {
  filter: grayscale(0.5);
}

@media (max-width: 768px) {
  .shop-layout { flex-direction: column; }
  .shop-sidebar {
    width: 100%;
    position: static;
    display: flex;
    gap: 6px;
    overflow-x: auto;
  }
}

/* =========================================================
   PROFILE PAGE
========================================================= */

.profile-container {
  /* width + centring from the shell rule near the top */
  margin-block: 0 80px;

  display: flex;
  flex-direction: column;
  gap: 20px;
}

.hero-profile { flex-wrap: wrap; }

.hero-profile > .btn,
.hero-profile > .profile-actions {
  margin-left: auto;
  align-self: center;
}

.profile-actions {
  display: flex;
  align-items: center;
  gap: 8px;
  flex-wrap: wrap;
}

.btn-text-danger {
  background: none;
  border: none;
  padding: 6px 4px;

  color: var(--danger);
  font-size: 13px;
  font-weight: 600;
  cursor: pointer;
}

.btn-text-danger:hover {
  text-decoration: underline;
}

.profile-title-row {
  display: flex;
  align-items: center;
  gap: 10px;
  flex-wrap: wrap;
}

.profile-title-row h1 {
  margin: 0;
  font-size: 26px;
  font-weight: 800;
}

.rank-pill,
.you-pill {
  display: inline-flex;
  align-items: center;
  gap: 4px;
  padding: 5px 10px;
  border-radius: 999px;
  font-size: 12px;
  font-weight: 700;
  white-space: nowrap;
}

.rank-pill {
  background: var(--accent-soft);
  color: var(--accent);
}

.you-pill {
  background: var(--surface-2);
  color: var(--muted);
  border: 1px solid var(--border);
}

.badge-detail-card {
  width: min(320px, 90%);
  text-align: center;
  padding: 32px 28px 24px;
}

.badge-detail-icon {
  display: block;
  font-size: 40px;
  margin-bottom: 12px;
}

.badge-detail-card h2 {
  margin: 0 0 8px;
}

.badge-detail-card .rtag {
  font-size: 12px;
  padding: 4px 12px;
}

.badge-detail-date {
  margin: 12px 0 0;
  color: var(--muted);
  font-size: 13px;
}

/* =========================================================
   FOOTER
========================================================= */

.site-footer {
  margin-top: 72px;
  padding: 52px 24px 28px;
  border-top: 1px solid var(--border);
  background: var(--surface);
}

.footer-inner {
  max-width: var(--container);
  margin: 0 auto;

  display: flex;
  justify-content: space-between;
  flex-wrap: wrap;
  gap: 40px;
}

.footer-left {
  max-width: 320px;
}

.footer-logo {
  font-family: var(--font-display);
  font-size: 1.15rem;
  font-weight: 700;
  letter-spacing: -0.02em;
  margin-bottom: 10px;
}

.footer-left p {
  color: var(--muted);
  line-height: 1.65;
  font-size: 0.86rem;
}

.footer-links {
  display: flex;
  gap: 50px;
  flex-wrap: wrap;
}

.footer-column {
  display: flex;
  flex-direction: column;
  gap: 9px;
}

.footer-column h4 {
  font-size: 0.72rem;
  font-weight: 700;
  letter-spacing: 0.1em;
  text-transform: uppercase;
  color: var(--muted);
  margin: 0 0 4px;
}

.footer-column a {
  color: var(--text);
  font-size: 0.86rem;
  transition: color var(--dur-1) var(--ease);
}

.footer-column a:hover {
  color: var(--accent);
}

.footer-bottom {
  max-width: var(--container);
  margin: 40px auto 0;
  padding-top: 20px;
  border-top: 1px solid var(--border);

  display: flex;
  justify-content: space-between;
  flex-wrap: wrap;
  gap: 10px;

  color: var(--muted);
  font-size: 0.85rem;
}

.footer-credit {
  font-weight: 600;
}

@media (max-width: 900px) {
  .footer-inner { flex-direction: column; }
  .footer-links { width: 100%; justify-content: space-between; gap: 24px; }
}

/* =========================================================
   COIN FX
========================================================= */

.coin-fx {
  position: fixed;
  font-size: 18px;
  pointer-events: none;
  z-index: 9999;
  animation: coinPop 0.7s ease-out forwards;
}

@keyframes coinPop {
  0% { transform: translate(0, 0) scale(1); opacity: 1; }
  100% { transform: translate(var(--x), var(--y)) scale(0.4) rotate(180deg); opacity: 0; }
}

.coin-pop {
  animation: coinBounce 0.3s ease;
}

@keyframes coinBounce {
  0% { transform: scale(1); }
  50% { transform: scale(1.3); color: #facc15; }
  100% { transform: scale(1); }
}

.gem-pop {
  animation: gemBounce 0.3s ease;
}

@keyframes gemBounce {
  0% { transform: scale(1); }
  50% { transform: scale(1.3); color: #c4b5fd; }
  100% { transform: scale(1); }
}

.coin-popup {
  position: fixed;
  top: 20%;
  left: 50%;
  transform: translateX(-50%);
  z-index: 9999;

  padding: 10px 18px;
  border-radius: var(--radius-sm);

  background: var(--text);
  color: var(--bg);

  font-weight: 700;
  font-size: 15px;

  animation: coinFade 1.5s ease forwards;
}

@keyframes coinFade {
  0% { opacity: 0; transform: translate(-50%, 10px); }
  20% { opacity: 1; transform: translate(-50%, 0); }
  100% { opacity: 0; transform: translate(-50%, -20px); }
}

.error-popup {
  background: var(--danger);
  color: #fff;
  max-width: min(360px, 90vw);
  text-align: center;
  animation: coinFade 2.6s ease forwards;
}

/* =========================================================
   RESPONSIVE — TYPING TEST / RESULTS
========================================================= */

@media (max-width: 768px) {
  .hero-score { font-size: 60px; }
  .performance-header { flex-direction: column; }
}

/* =========================================================
   RESPONSIVE — HEADER
========================================================= */

@media (max-width: 900px) {
  .site-header-inner { gap: 12px; }
  .primary-nav-link { padding: 7px 9px; }
}

@media (max-width: 768px) {
  /* Below here the bottom tab bar owns primary navigation. */
  .primary-nav { display: none; }
}

@media (max-width: 560px) {
  .site-header-inner { gap: 10px; height: 56px; }
  .brand-logo .logo-text { display: none; }

  .nav-actions { gap: 6px; }
  .nav-icons { gap: 0; }

  /* 40px rather than the 44px ideal — anything larger and the row
     (customize / friends / messages / account) stops fitting a narrow
     phone alongside the gems chip. Still a meaningful bump from 36px. */
  .icon-btn { width: 40px; height: 40px; }

  /* Coins + level are on the You / Awards screens on mobile — three
     chips plus three icons don't fit a phone width. Gems stays (it's
     the "spend real money" entry point). */
  .coin-pill,
  .level-pill { display: none; }

  .chip { height: 32px; padding: 0 10px; font-size: 12.5px; }
}

/* =========================================================
   BOTTOM TAB BAR (mobile only)
========================================================= */

.bottom-tab-bar {
  display: none;
}

@media (max-width: 768px) {
  .bottom-tab-bar {
    display: flex;
    position: fixed;
    left: 0;
    right: 0;
    bottom: 0;
    z-index: 500;

    background: color-mix(in srgb, var(--bg) 82%, transparent);
    backdrop-filter: saturate(1.4) blur(14px);
    border-top: 1px solid var(--border);
    /* Safe-area padding keeps tabs clear of the home-indicator bar on
       notched phones when running standalone (added-to-homescreen). */
    padding-bottom: env(safe-area-inset-bottom, 0);
  }

  /* Content would otherwise sit underneath the fixed bar. */
  body {
    padding-bottom: calc(58px + env(safe-area-inset-bottom, 0));
  }

  /* Customize / Friends / Messages stay reachable from the top bar; the
     bottom bar covers primary navigation, so the top-row primary nav and
     the coin/level chips (both duplicated on the You / Awards screens)
     are trimmed to keep the row from overflowing a phone width. */
  .primary-nav,
  .coin-pill,
  .level-pill {
    display: none;
  }
}

.bottom-tab {
  flex: 1;
  display: flex;
  flex-direction: column;
  align-items: center;
  justify-content: center;
  gap: 2px;
  padding: 8px 4px 6px;

  color: var(--muted);
  font-size: 10.5px;
  font-weight: 600;

  -webkit-tap-highlight-color: transparent;
}

.bottom-tab i {
  font-size: 20px;
  line-height: 1;
}

.bottom-tab.active {
  color: var(--accent);
}

/* =========================================================
   RACE MODE
   Lobby, waiting-room starting grid, the live track (rendered by
   static/js/race-track.js), and results. One width, on the design
   tokens, dark-first. Trail cosmetics + the customize-modal trail demo
   stay in their own block further down.
========================================================= */

.race-room {
  width: min(1040px, 100% - 2 * var(--gutter));
  margin-inline: auto;
}

/* ---------- Lobby ---------- */

.race-lobby {
  display: flex;
  flex-direction: column;
  gap: 18px;
  padding-top: 6px;
}

.race-lobby-hero {
  position: relative;
  display: grid;
  grid-template-columns: 1fr auto;
  gap: 30px;
  align-items: center;
  padding: 30px 32px;
  border-radius: var(--radius-lg);
  border: var(--card-border);
  box-shadow: var(--shadow-sm);
  background:
    radial-gradient(130% 150% at 100% 0%, color-mix(in srgb, var(--accent) 13%, transparent), transparent 58%),
    var(--surface);
  overflow: hidden;
}

/* faint speed-lines rushing in from the right */
.race-lobby-hero::after {
  content: "";
  position: absolute;
  inset: 0;
  background-image: repeating-linear-gradient(
    100deg,
    transparent 0 36px,
    color-mix(in srgb, var(--accent) 7%, transparent) 36px 38px
  );
  -webkit-mask-image: linear-gradient(90deg, transparent, #000 55%);
          mask-image: linear-gradient(90deg, transparent, #000 55%);
  opacity: 0.6;
  pointer-events: none;
}

.race-lobby-hero > * {
  position: relative;
  min-width: 0;
}

.race-lobby-hero h1 {
  font-family: var(--font-display);
  font-size: 30px;
  font-weight: 800;
  letter-spacing: -0.02em;
}

.race-lobby-hero p {
  color: var(--muted);
  margin-top: 5px;
  font-size: 14.5px;
}

.race-record {
  display: flex;
  gap: 26px;
  margin-top: 20px;
}

.race-record > div {
  display: flex;
  flex-direction: column;
}

.race-record strong {
  font-size: 24px;
  font-weight: 800;
  line-height: 1;
  font-variant-numeric: tabular-nums;
}

.race-record span {
  margin-top: 6px;
  font-size: 10.5px;
  font-weight: 700;
  text-transform: uppercase;
  letter-spacing: 0.09em;
  color: var(--muted);
}

.race-quick-btn {
  display: flex;
  flex-direction: column;
  align-items: center;
  justify-content: center;
  gap: 4px;
  min-width: 208px;
  padding: 22px 28px;
  border: none;
  border-radius: var(--radius-md);
  background: var(--accent);
  color: var(--on-accent);
  cursor: pointer;
  box-shadow: 0 12px 34px -12px color-mix(in srgb, var(--accent) 62%, transparent);
  transition: transform var(--dur-1) var(--ease), background var(--dur-1) var(--ease),
              box-shadow var(--dur-1) var(--ease);
}

.race-quick-btn:hover {
  transform: translateY(-2px);
  background: var(--accent-hover);
  box-shadow: 0 18px 44px -14px color-mix(in srgb, var(--accent) 72%, transparent);
}

.race-quick-btn:disabled {
  opacity: 0.6;
  cursor: default;
  transform: none;
}

.race-quick-btn i { font-size: 22px; }
.race-quick-btn span { font-size: 16px; font-weight: 800; }
.race-quick-btn small { font-size: 11.5px; font-weight: 600; opacity: 0.85; }

.race-lobby-secondary {
  display: grid;
  grid-template-columns: 1fr 1fr;
  gap: 16px;
}

.race-mini-card {
  display: flex;
  flex-direction: column;
  gap: 10px;
  padding: 20px 22px;
  border-radius: var(--radius-md);
  border: var(--card-border);
  box-shadow: var(--shadow-sm);
  background: var(--surface);
}

.race-mini-card-icon {
  display: grid;
  place-items: center;
  width: 38px;
  height: 38px;
  border-radius: var(--radius-sm);
  background: var(--accent-soft);
  color: var(--accent);
  font-size: 18px;
}

.race-mini-card h3 { font-size: 15px; font-weight: 700; }
.race-mini-card p { flex: 1; color: var(--muted); font-size: 13px; }

.race-join-row { display: flex; gap: 8px; }

.race-join-row input {
  flex: 1;
  min-width: 0;
  padding: 9px 12px;
  border-radius: var(--radius-sm);
  border: 1px solid var(--border);
  background: var(--bg);
  color: var(--text);
  font-family: ui-monospace, SFMono-Regular, Menlo, monospace;
  font-weight: 700;
  text-transform: uppercase;
  letter-spacing: 0.12em;
}

.race-join-row input:focus-visible {
  outline: none;
  border-color: var(--accent);
  box-shadow: var(--focus-ring);
}

.race-inline-error {
  min-height: 15px;
  color: var(--danger);
  font-size: 12.5px;
}

/* ---------- Shared stage (waiting + results) ---------- */

.race-stage {
  padding: 26px 28px 30px;
  border-radius: var(--radius-lg);
  border: var(--card-border);
  box-shadow: var(--shadow-md);
  background: var(--surface);
}

.race-stage-head {
  display: flex;
  align-items: flex-start;
  justify-content: space-between;
  gap: 14px;
  margin-bottom: 22px;
}

.race-code-label {
  display: block;
  margin-bottom: 6px;
  font-size: 10.5px;
  font-weight: 700;
  text-transform: uppercase;
  letter-spacing: 0.1em;
  color: var(--muted);
}

.race-code-value {
  display: inline-flex;
  align-items: center;
  gap: 10px;
  padding: 6px 14px;
  border-radius: var(--radius-sm);
  border: 1px dashed var(--border-strong);
  background: var(--bg);
  color: var(--text);
  font-family: ui-monospace, SFMono-Regular, Menlo, monospace;
  font-size: 23px;
  font-weight: 700;
  letter-spacing: 0.16em;
  cursor: pointer;
  transition: border-color var(--dur-1) var(--ease), background var(--dur-1) var(--ease);
}

.race-code-value:hover {
  border-color: var(--accent);
  background: var(--accent-soft);
}

.race-code-value i { font-size: 14px; color: var(--muted); }

/* ---------- Waiting room: the starting grid ---------- */

.race-grid {
  position: relative;
  display: flex;
  flex-direction: column;
  gap: 9px;
  min-height: 92px;
  padding: 15px 18px;
  border-radius: var(--radius-md);
  border: 1px solid var(--border);
  background: var(--bg);
  overflow: hidden;
}

/* start line the tokens sit against */
.race-grid::before {
  content: "";
  position: absolute;
  top: 12px;
  bottom: 12px;
  left: 62px;
  width: 2px;
  background: repeating-linear-gradient(var(--text) 0 5px, transparent 5px 11px);
  opacity: 0.25;
}

.race-grid-row {
  display: flex;
  align-items: center;
  gap: 13px;
  font-size: 13px;
  font-weight: 600;
  animation: raceGridJoin 0.4s var(--ease-out);
}

@keyframes raceGridJoin {
  from { opacity: 0; transform: translateX(-8px); }
  to   { opacity: 1; transform: translateX(0); }
}

.race-grid-row .race-avatar { flex-shrink: 0; }
.race-grid-row.is-you { color: var(--accent); font-weight: 700; }

.race-countdown-wrap {
  display: flex;
  flex-direction: column;
  align-items: center;
  gap: 6px;
  margin: 22px 0 6px;
  min-height: 58px;
  justify-content: center;
}

.race-countdown-num {
  font-family: var(--font-display);
  font-size: 52px;
  font-weight: 800;
  line-height: 1;
  color: var(--accent);
  font-variant-numeric: tabular-nums;
}

.race-countdown-num.tick {
  animation: raceCountdownPop 0.9s var(--ease-out);
}

.race-countdown-num.go {
  letter-spacing: 0.04em;
  animation: raceCountdownGo 0.6s var(--ease-out);
}

@keyframes raceCountdownPop {
  0%   { transform: scale(1.5); opacity: 0; }
  35%  { transform: scale(1);   opacity: 1; }
  100% { transform: scale(1);   opacity: 1; }
}

@keyframes raceCountdownGo {
  0%   { transform: scale(0.6); opacity: 0; }
  40%  { transform: scale(1.1); opacity: 1; }
  100% { transform: scale(1);   opacity: 1; }
}

.race-stage-hint {
  display: flex;
  align-items: center;
  gap: 8px;
  color: var(--muted);
  font-size: 13.5px;
}

.race-pulse-dot {
  width: 8px;
  height: 8px;
  border-radius: 50%;
  background: var(--accent);
  animation: racePulseDot 1.4s ease-in-out infinite;
}

@keyframes racePulseDot {
  0%, 100% { opacity: 1;   transform: scale(1); }
  50%      { opacity: 0.3; transform: scale(0.7); }
}

.race-stage .btn-primary,
.race-stage #race-start-btn {
  margin-top: 4px;
}

/* ---------- Avatars (grid rows, standings, podium) ---------- */

.race-avatar {
  display: grid;
  place-items: center;
  width: 34px;
  height: 34px;
  border-radius: 50%;
  background: var(--accent);
  color: var(--on-accent);
  font-size: 13px;
  font-weight: 800;
  box-shadow: 0 4px 12px color-mix(in srgb, var(--accent) 32%, transparent);
}

.race-avatar.is-bot { background: var(--surface-3); color: var(--text); box-shadow: none; }

/* ---------- The live track ---------- */

#race-typing {
  display: flex;
  flex-direction: column;
  gap: 16px;
}

.race-track {
  position: relative;
  margin-bottom: 16px;
  padding: 16px 18px;
  border-radius: var(--radius-lg);
  border: var(--card-border);
  box-shadow: var(--shadow-sm);
  background: var(--surface);
  overflow: hidden;
  transition: padding var(--dur-3) var(--ease);
}

/* After you finish, the track sits above your result card — keep it
   present but visually secondary. */
#race-results { position: relative; }

.race-status-line {
  display: flex;
  align-items: center;
  gap: 8px;
  margin: 4px 0 16px;
  color: var(--muted);
  font-size: 13px;
  font-weight: 600;
}

/* Spectating (you're done, race isn't): the result card shows only your
   placement + a live "still racing" line — the track above is the
   standings until everyone's in. */
.race-stage.is-spectating .race-podium,
.race-stage.is-spectating #race-standings {
  display: none;
}

.race-stage.is-spectating .race-result-hero {
  padding-bottom: 4px;
}

.race-track--lanes {
  display: flex;
  flex-direction: column;
  gap: 6px;
}

/* the test is running — let the track recede a little so the passage leads */
body.test-active .race-track {
  padding: 12px 18px;
  box-shadow: none;
  opacity: 0.92;
}

/* finish line: dashed rail on the right with an accent flag block */
.race-track-finish {
  position: absolute;
  top: 14px;
  bottom: 14px;
  right: 18px;
  width: 8px;
  border-radius: 2px;
  background: repeating-linear-gradient(45deg,
    color-mix(in srgb, var(--text) 55%, transparent) 0 4px, transparent 4px 8px);
  pointer-events: none;
}

.race-track-finish span {
  position: absolute;
  top: -3px;
  left: 50%;
  transform: translateX(-50%);
  width: 14px;
  height: 14px;
  border-radius: 3px;
  background: var(--accent);
  box-shadow: 0 0 12px var(--accent);
}

.race-lane {
  display: flex;
  flex-direction: column;
  gap: 4px;
  padding: 6px 44px 6px 10px;
  border-radius: var(--radius-sm);
  transition: background var(--dur-2) var(--ease);
}

.race-lane.is-you {
  background: color-mix(in srgb, var(--accent) 7%, transparent);
  box-shadow: inset 3px 0 0 var(--accent);
}

.race-lane.is-rank-up {
  animation: raceRankUp 0.65s var(--ease);
}

@keyframes raceRankUp {
  0%   { background: color-mix(in srgb, var(--accent) 28%, transparent); }
  100% { background: color-mix(in srgb, var(--accent) 7%, transparent); }
}

.race-lane-meta {
  display: flex;
  align-items: baseline;
  gap: 8px;
  font-size: 12px;
  line-height: 1.2;
}

.race-lane-rank {
  min-width: 30px;
  font-weight: 800;
  font-variant-numeric: tabular-nums;
  color: var(--muted);
}

.race-lane.is-you .race-lane-rank { color: var(--accent); }

.race-lane-name {
  font-weight: 700;
  white-space: nowrap;
  overflow: hidden;
  text-overflow: ellipsis;
}

.race-lane-stat {
  margin-left: auto;
  color: var(--muted);
  font-weight: 600;
  font-variant-numeric: tabular-nums;
}

.race-lane-runway {
  position: relative;
  height: 22px;
}

.race-lane-avatar {
  position: absolute;
  top: 50%;
  left: 0;
  transform: translate(-50%, -50%);
  width: 24px;
  height: 24px;
  border-radius: 50%;
  display: grid;
  place-items: center;
  background: var(--surface-3);
  color: var(--text);
  font-size: 11px;
  font-weight: 800;
  box-shadow: 0 0 0 2px var(--surface);
  z-index: 2;
}

.race-lane.is-you .race-lane-avatar {
  background: var(--accent);
  color: var(--on-accent);
  box-shadow: 0 0 0 2px var(--surface), 0 0 14px color-mix(in srgb, var(--accent) 60%, transparent);
}

.race-lane.is-finished .race-lane-avatar {
  box-shadow: 0 0 0 2px var(--surface), 0 0 0 4px #f5c451;
  animation: raceFinishPop 0.5s var(--ease-out);
}

@keyframes raceFinishPop {
  0%   { transform: translate(-50%, -50%) scale(1); }
  45%  { transform: translate(-50%, -50%) scale(1.35); }
  100% { transform: translate(-50%, -50%) scale(1); }
}

/* generic speed streak — length + glow scale with WPM (set inline by JS) */
.race-lane-trail {
  position: absolute;
  top: 50%;
  left: 0;
  transform: translate(-100%, -50%);
  height: 4px;
  border-radius: 999px;
  background: linear-gradient(to left, var(--accent), transparent);
  pointer-events: none;
  z-index: 1;
}

.race-lane:not(.is-you) .race-lane-trail {
  background: linear-gradient(to left, color-mix(in srgb, var(--muted) 80%, transparent), transparent);
}

/* an equipped Trail cosmetic (::after on the token, further down) replaces
   the generic streak so people still see what they bought */
.race-lane-avatar[data-trail]:not([data-trail=""]) ~ .race-lane-trail {
  opacity: 0 !important;
}

/* ---------- Crowd view (13+ racers) ---------- */

.race-track--crowd {
  display: flex;
  flex-direction: column;
  gap: 14px;
}

.race-crowd-podium {
  display: grid;
  grid-template-columns: repeat(3, 1fr);
  gap: 8px;
}

.race-crowd-podium-card {
  display: flex;
  align-items: center;
  gap: 8px;
  padding: 9px 12px;
  border-radius: var(--radius-sm);
  border: 1px solid var(--border);
  background: var(--bg);
  font-size: 12px;
  min-width: 0;
}

.race-crowd-podium-card.is-first {
  border-color: transparent;
  background: linear-gradient(135deg,
    color-mix(in srgb, #f5c451 28%, var(--bg)), var(--bg));
}

.race-crowd-podium-rank {
  flex-shrink: 0;
  width: 22px;
  height: 22px;
  display: grid;
  place-items: center;
  border-radius: 50%;
  background: var(--surface-2);
  color: var(--muted);
  font-weight: 800;
  font-size: 11px;
}

.race-crowd-podium-card.is-first .race-crowd-podium-rank {
  background: linear-gradient(135deg, #f5c451, #dd9022);
  color: #241700;
}

.race-crowd-podium-name {
  font-weight: 700;
  white-space: nowrap;
  overflow: hidden;
  text-overflow: ellipsis;
}

.race-crowd-podium-stat { margin-left: auto; color: var(--muted); flex-shrink: 0; }

.race-crowd-lane {
  position: relative;
  height: 40px;
  border-radius: 999px;
  border: 1px solid var(--border);
  background: var(--bg);
}

.race-crowd-lane .race-track-finish {
  top: 8px;
  bottom: 8px;
  right: 10px;
}

.race-crowd-dots {
  position: absolute;
  inset: 0 18px;
}

.race-crowd-dot {
  position: absolute;
  top: 50%;
  left: 0;
  width: 10px;
  height: 10px;
  border-radius: 50%;
  transform: translate(-50%, -50%);
  background: var(--muted);
  border: 2px solid var(--surface);
}

.race-crowd-dot.is-leader {
  width: 12px;
  height: 12px;
  background: linear-gradient(135deg, #f5c451, #dd9022);
  z-index: 2;
}

.race-crowd-dot.is-you {
  width: 15px;
  height: 15px;
  background: var(--accent);
  z-index: 3;
  box-shadow: 0 0 0 3px var(--accent-soft);
}

.race-crowd-dot.is-finished { outline: 2px solid #f5c451; outline-offset: 1px; }

.race-crowd-you {
  display: flex;
  align-items: center;
  gap: 10px;
  padding: 10px 14px;
  border-radius: var(--radius-sm);
  border: 1px solid var(--accent);
  background: var(--accent-soft);
}

.race-crowd-you-rank { font-weight: 800; color: var(--accent); font-size: 13px; }
.race-crowd-you-name { flex: 1; font-weight: 700; font-size: 13px; }
.race-crowd-you-stat { color: var(--muted); font-size: 12.5px; }

.race-crowd-list {
  max-height: 300px;
  overflow-y: auto;
  border: 1px solid var(--border);
  border-radius: var(--radius-sm);
}

.race-crowd-list-row {
  display: flex;
  align-items: center;
  gap: 10px;
  padding: 7px 12px;
  font-size: 13px;
  border-bottom: 1px solid var(--border);
}

.race-crowd-list-row:last-child { border-bottom: none; }
.race-crowd-list-row.is-you { background: var(--accent-soft); }

.race-crowd-list-rank {
  width: 30px;
  flex-shrink: 0;
  color: var(--muted);
  font-variant-numeric: tabular-nums;
}

.race-crowd-list-name {
  flex: 1;
  min-width: 0;
  white-space: nowrap;
  overflow: hidden;
  text-overflow: ellipsis;
}

.race-crowd-list-stat {
  color: var(--muted);
  font-variant-numeric: tabular-nums;
}

/* Bots must always read as bots — one badge next to the name everywhere. */
.bot-tag {
  display: inline-block;
  padding: 1px 5px;
  border-radius: 999px;
  border: 1px solid var(--border);
  background: var(--surface-2);
  color: var(--muted);
  font-size: 9.5px;
  font-weight: 800;
  letter-spacing: 0.03em;
  vertical-align: middle;
}

@media (prefers-reduced-motion: reduce) {
  .race-grid-row,
  .race-countdown-num.tick,
  .race-countdown-num.go,
  .race-pulse-dot,
  .race-lane.is-rank-up,
  .race-lane.is-finished .race-lane-avatar {
    animation: none;
  }
}

@media (max-width: 720px) {
  .race-lobby-hero { grid-template-columns: 1fr; }
  .race-quick-btn { width: 100%; }
  .race-lobby-secondary { grid-template-columns: 1fr; }
  .race-crowd-podium { grid-template-columns: 1fr; }
}

/* --- Race trail cosmetics ---
   Matches ShopItem slugs (item_type="trail"). Applied both on the live
   lane avatar and the small circle avatars in the waiting room / results
   so equipping a trail is visible everywhere a player shows up. */

.race-lane-avatar[data-trail]::after,
.race-avatar-circle[data-trail]::after {
  content: "";
  position: absolute;
  top: 50%;
  right: 100%;
  transform: translateY(-50%);
  height: 6px;
  width: 26px;
  border-radius: 999px;
  pointer-events: none;
}

.race-avatar-circle[data-trail]::after { display: none; }

.race-lane-avatar[data-trail="spark-trail"]::after {
  background: linear-gradient(to left, #fde047, transparent);
  box-shadow: 0 0 8px #fde047;
  animation: raceTrailFlicker 0.5s ease-in-out infinite;
}

.race-lane-avatar[data-trail="comet-trail"]::after {
  width: 40px;
  background: linear-gradient(to left, #38bdf8, transparent);
  box-shadow: 0 0 10px #38bdf8;
}

/* The flame visibly grows/shrinks, not just fades — raceTrailFlicker
   (opacity-only) is shared with the common-tier Spark/Ice trails, so an
   epic trail needs more than just a faster copy of the same fade. */
.race-lane-avatar[data-trail="flame-trail"]::after {
  width: 34px;
  background: linear-gradient(to left, #f97316, #facc15, transparent);
  box-shadow: 0 0 10px #f97316;
  animation: flameTrailPulse 0.6s ease-in-out infinite;
}

@keyframes flameTrailPulse {
  0%, 100% { width: 34px; opacity: 1; }
  50% { width: 40px; opacity: 0.8; }
}

.race-lane-avatar[data-trail="ghost-trail"]::after {
  width: 34px;
  background: linear-gradient(to left, rgba(226,232,240,0.9), transparent);
  box-shadow: 0 0 10px rgba(226,232,240,0.6);
  animation: raceTrailDrift 1.2s ease-in-out infinite;
}

.race-lane-avatar[data-trail="prism-trail"]::after {
  width: 40px;
  background: linear-gradient(to left, #ec4899, #a855f7, #38bdf8, transparent);
  background-size: 200% 100%;
  box-shadow: 0 0 12px rgba(168,85,247,0.6);
  animation: raceTrailShift 1.4s linear infinite;
}

@keyframes raceTrailFlicker {
  0%, 100% { opacity: 1; }
  50% { opacity: 0.6; }
}

@keyframes raceTrailDrift {
  0%, 100% { transform: translateY(-50%) translateX(0); opacity: 0.9; }
  50% { transform: translateY(-50%) translateX(-4px); opacity: 0.5; }
}

@keyframes raceTrailShift {
  0% { background-position: 0 0; }
  100% { background-position: 200% 0; }
}

/* Limited to 75 units ever — the biggest, brightest trail in the shop,
   wider and hotter than the regular Flame Trail. */
/* Width pulse + a brightness/box-shadow surge toward white-hot at the
   peak — the most dynamic trail in the shop, matching its mythical/
   limited-75 status instead of sharing Spark Trail's plain flicker. */
.race-lane-avatar[data-trail="eternal-flame-trail"]::after {
  width: 48px;
  height: 8px;
  background: linear-gradient(to left, #fff7ed, #fb923c, #dc2626, transparent);
  box-shadow: 0 0 14px #f97316, 0 0 26px rgba(220, 38, 38, 0.5);
  animation: eternalFlameSurge 0.7s ease-in-out infinite;
}

@keyframes eternalFlameSurge {
  0%, 100% { width: 48px; filter: brightness(1); box-shadow: 0 0 14px #f97316, 0 0 26px rgba(220, 38, 38, 0.5); }
  50% { width: 58px; filter: brightness(1.3); box-shadow: 0 0 20px #fff7ed, 0 0 36px rgba(251, 146, 60, 0.7); }
}

.race-lane-avatar[data-trail="ice-trail"]::after {
  background: linear-gradient(to left, #e0f2fe, #38bdf8, transparent);
  box-shadow: 0 0 8px #38bdf8;
  animation: raceTrailFlicker 0.6s ease-in-out infinite;
}

/* Dark core + bright silver glow — same "visible regardless of what's
   behind it" trick as the Shadow Cursor above. */
.race-lane-avatar[data-trail="shadow-trail"]::after {
  background: linear-gradient(to left, #78716c, #1c1917, transparent);
  box-shadow: 0 0 10px rgba(214, 211, 209, 0.7);
  animation: raceTrailDrift 1s ease-in-out infinite;
}

.race-lane-avatar[data-trail="electric-trail"]::after {
  width: 34px;
  background: linear-gradient(to left, #fde047, #22d3ee, transparent);
  box-shadow: 0 0 10px #fde047, 0 0 18px rgba(34, 211, 238, 0.6);
  animation: raceTrailFlicker 0.2s steps(2) infinite;
}

.race-lane-avatar[data-trail="galaxy-trail"]::after {
  width: 40px;
  background: linear-gradient(to left, #a855f7, #6366f1, #38bdf8, transparent);
  background-size: 200% 100%;
  box-shadow: 0 0 12px rgba(99, 102, 241, 0.6);
  animation: raceTrailShift 1.6s linear infinite;
}

/* Small moving-dot demo used in the Customize modal's Race Trails tab */
.trail-demo-track {
  position: relative;
  overflow: hidden;
}

.trail-demo-dot {
  position: absolute;
  top: 50%;
  left: 8px;
  width: 8px;
  height: 8px;
  border-radius: 50%;
  background: var(--accent);
  transform: translateY(-50%);
  animation: trailDemoMove 1.8s ease-in-out infinite;
}

[data-trail-demo] .trail-demo-dot { background: var(--accent); }

[data-trail-demo="spark-trail"] .trail-demo-dot {
  background: #fde047;
  box-shadow: -14px 0 10px -4px #fde047;
}

[data-trail-demo="comet-trail"] .trail-demo-dot {
  background: #38bdf8;
  box-shadow: -18px 0 12px -4px #38bdf8;
}

[data-trail-demo="flame-trail"] .trail-demo-dot {
  background: #f97316;
  box-shadow: -18px 0 12px -4px #f97316;
}

[data-trail-demo="ghost-trail"] .trail-demo-dot {
  background: #e2e8f0;
  box-shadow: -18px 0 12px -4px rgba(226,232,240,0.8);
}

[data-trail-demo="prism-trail"] .trail-demo-dot {
  background: #a855f7;
  box-shadow: -18px 0 14px -4px #ec4899;
}

@keyframes trailDemoMove {
  0%, 100% { left: 8px; }
  50% { left: calc(100% - 16px); }
}

/* ---------- Results ---------- */

.race-result-hero {
  position: relative;
  text-align: center;
  padding: 8px 0 22px;
}

.race-result-hero-place {
  font-family: var(--font-display);
  font-size: 40px;
  font-weight: 800;
  letter-spacing: -0.02em;
  line-height: 1.1;
}

.race-result-hero-place .medal { margin-right: 8px; }

.race-result-hero-sub {
  margin-top: 6px;
  color: var(--muted);
  font-size: 14px;
  font-variant-numeric: tabular-nums;
}

.race-result-hero-reward {
  display: inline-flex;
  align-items: center;
  gap: 8px;
  margin-top: 14px;
  padding: 7px 14px;
  border-radius: 999px;
  border: 1px solid var(--border);
  background: var(--bg);
  font-size: 13px;
  font-weight: 700;
  font-variant-numeric: tabular-nums;
}

.race-result-hero-reward b { color: var(--accent); }

/* podium — top 3 as blocks of descending height */
.race-podium {
  display: grid;
  grid-template-columns: 1fr 1.15fr 1fr;
  align-items: end;
  gap: 10px;
  max-width: 440px;
  margin: 0 auto 24px;
}

.race-podium-col {
  display: flex;
  flex-direction: column;
  align-items: center;
  gap: 8px;
}

.race-podium-col .race-avatar { width: 40px; height: 40px; font-size: 15px; }

.race-podium-name {
  font-size: 12px;
  font-weight: 700;
  max-width: 100%;
  white-space: nowrap;
  overflow: hidden;
  text-overflow: ellipsis;
}

.race-podium-wpm {
  font-size: 11px;
  color: var(--muted);
  font-variant-numeric: tabular-nums;
}

.race-podium-block {
  width: 100%;
  border-radius: var(--radius-sm) var(--radius-sm) 0 0;
  border: 1px solid var(--border);
  border-bottom: none;
  background: var(--surface-2);
  display: grid;
  place-items: start center;
  padding-top: 8px;
  font-weight: 800;
  font-size: 13px;
  color: var(--muted);
}

.race-podium-col.p1 .race-podium-block {
  height: 74px;
  background: linear-gradient(180deg, color-mix(in srgb, #f5c451 26%, var(--surface-2)), var(--surface-2));
  border-color: color-mix(in srgb, #f5c451 40%, var(--border));
  color: var(--text);
}
.race-podium-col.p2 .race-podium-block { height: 54px; }
.race-podium-col.p3 .race-podium-block { height: 40px; }

.race-podium-col.p1 .race-avatar {
  box-shadow: 0 0 0 3px color-mix(in srgb, #f5c451 55%, transparent),
              0 6px 18px color-mix(in srgb, #f5c451 35%, transparent);
}

/* full standings */
.race-standings {
  display: flex;
  flex-direction: column;
  gap: 6px;
  margin: 4px 0 20px;
}

.race-standing-row {
  display: grid;
  grid-template-columns: 40px 34px 1fr auto auto;
  align-items: center;
  gap: 12px;
  padding: 9px 14px;
  border-radius: var(--radius-sm);
  border: 1px solid var(--border);
  background: var(--bg);
}

.race-standing-row.is-winner {
  border-color: color-mix(in srgb, #f5c451 45%, var(--border));
  background: linear-gradient(90deg, color-mix(in srgb, #f5c451 12%, var(--bg)), var(--bg) 60%);
}

.race-standing-row.is-you { border-color: var(--accent); }

.race-standing-place {
  font-weight: 800;
  color: var(--muted);
  text-align: center;
  font-variant-numeric: tabular-nums;
}

.race-standing-row.is-winner .race-standing-place { color: #d99320; }
.race-standing-row.is-you .race-standing-place { color: var(--accent); }

.race-standing-name {
  font-weight: 700;
  white-space: nowrap;
  overflow: hidden;
  text-overflow: ellipsis;
}

.race-standing-wpm { font-weight: 700; font-variant-numeric: tabular-nums; }
.race-standing-wpm span { color: var(--muted); font-weight: 500; font-size: 11px; }
.race-standing-acc { color: var(--muted); font-size: 12.5px; font-variant-numeric: tabular-nums; }

.race-actions {
  display: flex;
  gap: 10px;
  flex-wrap: wrap;
}

/* win burst — accent + gold confetti flung from the hero */
.race-burst {
  position: absolute;
  inset: 0;
  overflow: visible;
  pointer-events: none;
}

.race-burst i {
  position: absolute;
  top: 20%;
  left: 50%;
  width: 8px;
  height: 8px;
  border-radius: 1px;
  opacity: 0;
  animation: raceBurst 1s var(--ease-out) forwards;
}

@keyframes raceBurst {
  0%   { opacity: 1; transform: translate(0, 0) rotate(0); }
  100% { opacity: 0; transform: translate(var(--bx), var(--by)) rotate(var(--br)); }
}

@media (prefers-reduced-motion: reduce) {
  .race-burst { display: none; }
}

@media (max-width: 640px) {
  .race-standing-row { grid-template-columns: 32px 30px 1fr; }
  .race-standing-wpm, .race-standing-acc { grid-column: span 3; padding-left: 74px; }
  .race-podium-name { font-size: 11px; }
}

/* =========================================================
   ORGANIZATIONS
========================================================= */

.org-list {
  display: flex;
  flex-direction: column;
  gap: 10px;
  margin-top: 16px;
}

.org-list-row {
  display: flex;
  align-items: center;
  gap: 14px;
  padding: 14px 16px;
  border-radius: var(--radius-md);

  background: var(--surface-2);
  border: 1px solid var(--border);
  color: var(--text);

  transition: border-color 0.15s ease, transform 0.15s ease;
}

.org-list-row:hover {
  border-color: var(--accent);
  transform: translateY(-1px);
}

.org-list-icon {
  width: 40px;
  height: 40px;
  flex-shrink: 0;
  border-radius: var(--radius-sm);

  display: flex;
  align-items: center;
  justify-content: center;
  font-size: 18px;

  background: var(--surface);
  border: 1px solid var(--border);
}

.org-list-body {
  flex: 1;
  min-width: 0;
  display: flex;
  flex-direction: column;
}

.org-list-body small {
  color: var(--muted);
  font-size: 12px;
}

.org-roster {
  display: flex;
  flex-direction: column;
  gap: 10px;
  margin-top: 16px;
}

.org-roster-row {
  display: flex;
  align-items: center;
  gap: 14px;
  padding: 14px 16px;
  border-radius: var(--radius-md);

  background: var(--surface-2);
  border: 1px solid var(--border);
  flex-wrap: wrap;
}

.org-roster-rank {
  width: 32px;
  flex-shrink: 0;
  text-align: center;
  font-weight: 800;
  font-size: 13px;
  color: var(--muted);
  font-variant-numeric: tabular-nums;
}

.org-roster-avatar {
  width: 40px;
  height: 40px;
  flex-shrink: 0;
  border-radius: 50%;

  display: flex;
  align-items: center;
  justify-content: center;
  font-family: var(--font-display);
  font-weight: 800;
  font-size: 15px;
  color: var(--accent);

  background: var(--accent-soft);
}

.org-roster-body {
  flex: 1;
  min-width: 140px;
  display: flex;
  flex-direction: column;
  gap: 2px;
}

.org-roster-name-row {
  display: flex;
  align-items: center;
  gap: 8px;
}

.org-roster-body small {
  color: var(--muted);
  font-size: 12px;
}

.org-roster-stats {
  display: flex;
  gap: 18px;
}

.org-roster-stats div {
  display: flex;
  flex-direction: column;
  align-items: center;
  min-width: 56px;
}

.org-roster-stats strong {
  font-size: 15px;
  font-variant-numeric: tabular-nums;
}

.org-roster-stats span {
  font-size: 10.5px;
  color: var(--muted);
  text-transform: uppercase;
  letter-spacing: 0.03em;
}

.org-roster-actions {
  display: flex;
  gap: 4px;
}

.org-race-banner,
.org-race-cta {
  display: flex;
  align-items: center;
  gap: 16px;
  padding: 18px 22px;
  margin-bottom: 20px;

  color: var(--text);
  text-align: left;
  width: 100%;
  transition: transform 0.15s ease, border-color 0.15s ease;
}

.org-race-banner {
  border-color: var(--accent);
  background: var(--accent-soft);
}

.org-race-banner:hover,
.org-race-cta:hover {
  transform: translateY(-1px);
  border-color: var(--accent);
}

.org-race-banner-icon {
  font-size: 26px;
}

.org-race-banner-body {
  flex: 1;
  min-width: 0;
  display: flex;
  flex-direction: column;
}

.org-race-banner-body span {
  font-size: 12.5px;
  color: var(--muted);
}

.org-challenge-card {
  padding: 24px;
  margin-top: 20px;
}

.org-challenge-goal {
  font-size: 13.5px;
  color: var(--muted);
  margin: 4px 0 16px;
}

.org-challenge-bars {
  display: flex;
  flex-direction: column;
  gap: 10px;
}

.org-challenge-participants {
  margin: 14px 0 0;
  font-size: 12px;
  color: var(--muted);
}

.org-challenge-form {
  display: grid;
  grid-template-columns: 1fr 1fr 1fr auto;
  gap: 8px;
  margin-top: 12px;
}

@media (max-width: 640px) {
  .org-challenge-form { grid-template-columns: 1fr 1fr; }
  .org-race-banner, .org-race-cta { flex-wrap: wrap; }
}

@media (max-width: 640px) {
  .org-roster-row { justify-content: space-between; }
  .org-roster-stats { gap: 12px; }
  .org-roster-actions { width: 100%; justify-content: flex-end; }
}
