/* ═══════════════════════════════════════════════════════════════════════════
   TALMAARA DESIGN TOKENS — single source of truth for every color in the app.
   Loaded FIRST on every page (includes/head.php), before style.css or any
   page sheet. No component rule anywhere should name a theme — only tokens.

   FOUR skins, one vocabulary — aligned 2026-08-22 to the four Talmaara Write
   ships, so the site and the app name and paint the same surfaces. The stored
   KEYS are unchanged (dark/dim/light + new sepia); only the labels are the
   app's, because renaming the keys would break every existing localStorage
   value and every [data-theme="dim"] selector in the codebase.

     key      label     what it is
     ──────   ───────   ─────────────────────────────────────────────────────
     dark     Night     NEUTRAL near-black, cream-white ink. Lives on :root
                        because dark is the brand default. Realigned from the
                        old navy "night sky" 2026-08-22 — see the note below.
     dim      Dusk      "Twilight slate" — mid slate ground, white ink. Already
                        matched Write's dusk, so it was left exactly as tuned
                        2026-07-19. html[data-theme="dim"].
     sepia    Sunset    Warm parchment, warm dark ink. NEW 2026-08-22.
                        html[data-theme="sepia"].
     light    Paper     "Porcelain" — cool blue-gray daylight, navy ink.
                        html[data-theme="light"].

   ⚠ WHY DARK IS NEUTRAL NOW. It was navy (#0B141F ground / #121E2D cards)
   from 2026-07-13. Write's night is a neutral near-black, and the two were
   deliberately divergent — the Dusk handoff doc even says "do not copy Write's
   night onto the site." That call was reversed by the site owner 2026-08-22:
   the app's darkest reads as a true dark rather than a colored one, and the
   site had to match. The navy did NOT disappear — it is pinned for the public
   cinematic pages at the bottom of this file. Don't "restore" it up here.

   Dark-family skins (dark, dim, sepia is NOT one) get the legacy `dark-mode`
   class stamped on <html> by head.php so older `.dark-mode`-scoped rules keep
   matching until the literal sweep retires them. Sepia and light are light
   skins and must never carry it. New code must never target `.dark-mode`.

   NOT governed here: illustration/art colors (book covers, planet & moon SVGs,
   nebula washes, map pins, tier emblems), the PORTAL_COLORS registry, email
   HTML (clients can't read vars), and PDF/manuscript output surfaces — those
   stay literal on purpose. The public cinematic pages (landing, pricing,
   legal) keep their fixed navy/cream palette and do not theme-switch.
   ═══════════════════════════════════════════════════════════════════════════ */

:root {
    color-scheme: dark;

    /* ── Ground & surfaces (elevation steps: page → card → raised → hover) ──
         "Night" — neutral near-black, mirroring Write's own night ladder
         (--app #16171a → --panel #1d1e22 → --panel-2 #24262b → --panel-3
         #2e3138). Hue-free on purpose: any blue here is what made the old
         navy read as a colored dark instead of a dark. */
    --bg:             #16171A;
    --surface-1:      #1D1E22;   /* cards, panels, subnav */
    --surface-2:      #24262B;   /* nested panels, menus, inputs, modals */
    --surface-3:      #2E3138;   /* hover / active fills on surface-2 */

    /* ── Ink (three tiers — stop improvising grays) ── */
    --text:           #E9EAEE;   /* primary */
    --text-2:         #9EA3AD;   /* secondary: body copy, labels */
    --text-3:         #697079;   /* muted: meta, placeholders, timestamps */

    /* ── Lines ── */
    --border:         #2A2C32;   /* default dividers, card edges */
    --border-strong:  #3A3E46;   /* inputs, emphasized separation */

    /* ── Brand accent (overridden live from the settings DB via head.php) ──
         Stays the brand blue: the 2026-08-22 realign was about the GROUND
         being neutral, not about muting the accent. Write's own #4ea3d8 is
         deliberately not copied here. */
    --accent:         #00B3FF;
    --accent-2:       #A6CDDD;   /* dim accent: kickers, quiet links */
    --on-accent:      #080E14;   /* text/icon color on accent-filled controls */
    --accent-glow:    rgba(0, 179, 255, 0.08);
    --accent-ring:    rgba(0, 179, 255, 0.15);
    --accent-tint:    rgba(0, 179, 255, 0.12);

    /* ── Status (semantic — never reuse the accent for state) ── */
    --good:           #4CCB8F;
    --warn:           #F0B754;
    --danger:         #F0605D;
    --good-tint:      rgba(76, 203, 143, 0.10);
    --warn-tint:      rgba(240, 183, 84, 0.10);
    --danger-tint:    rgba(240, 96, 93, 0.10);

    /* ── Elevation (deeper than the old navy's .45 — a near-black ground needs
         more separation before a shadow reads at all) ── */
    --shadow:         rgba(0, 0, 0, 0.55);

    /* ── GRAIN. Read this before touching any skin's --page-texture ──────────
       Consumed by body.has-sidebar::before (style.css). Procedural SVG noise
       as a data URI, tiled 256×256, costing no request. It is SELF-COLORED and
       composited NORMALLY — there is no background-blend-mode any more
       (removed 2026-08-22, see style.css body.has-sidebar::before).

       WHY. It used to be neutral gray noise soft-lit over --bg. soft-light on
       a near-black ground is lopsided: at --bg #16171A a black noise pixel can
       only pull luminance down ~0.08, while a white one pushes it up ~0.18 —
       2.3x more headroom upward. So symmetric noise NET-LIFTED the ground and
       the grain read as gray fuzz sitting on top of the dark instead of in it.
       Blending also forced a per-pixel composite over an element sized to the
       whole document height, which is the only real cost this layer ever had.

       HOW IT WORKS NOW. feTurbulence → saturate 0 (neutral 0..1 noise) →
       feComponentTransfer remaps each channel onto a narrow band CENTERED ON
       THIS SKIN'S OWN --bg, and pins alpha to a flat 1. So the tile's mean
       color IS --bg: the grain modulates around the ground instead of over it,
       and can never shift it lighter or darker overall.

       ⚠ color-interpolation-filters='sRGB' IS LOAD-BEARING. SVG filters run in
       linearRGB by DEFAULT. Without that attribute the intercepts below are
       read as linear-light values and gamma-encoded on the way out, so Night's
       0.086 ground paints as sRGB ~0.33 — a mid gray, roughly #555. The grain
       half of the page comes out visibly lighter than the flat half and the
       whole centering exercise is undone. Never drop it from these four URIs.

       ⚠⚠ CURRENTLY RUNNING A TEST — NOT A SETTLED VALUE. All four skins are on
       type='turbulence', baseFrequency 0.5885, 1 octave, 256 tile, slope 0.110
       (mean 0.16511, measured). Single-octave turbulence is a different animal
       from everything below: one scale only, ~1.7px, and a hard-skewed
       distribution (p05 0.008 / p95 0.357 but max 0.929) so it reads as sparse
       bright specks rather than an even wash. Slope 0.110 is ~6x the strength
       of the values it replaced, because that distribution puts most pixels
       near the bottom of the band.

       Paper's blue band top computes to 1.025, i.e. past white — but COUNTED,
       only 29 pixels of 65536 (0.04%) actually clamp, because this
       distribution almost never reaches its own max. Measured drift stays at
       0.5/255, same as the other three, so it is not visible. Worth knowing
       rather than fixing: Paper's true ceiling here is slope 0.077, and any
       further slope increase starts eating blue highlights for real.

       THE SETTLED VALUE THIS REPLACED, to restore if the test is rejected:
       type='fractalNoise', baseFrequency 0.12, 4 octaves, 256px tile, ~8px
       base features with detail down to ~1px, mean 0.5, and slopes
       Night 0.020 · Dusk 0.028 · Sunset 0.035 · Paper 0.034.

       ⚠ THAT SETTLED VALUE SITS BETWEEN TWO REJECTED EXTREMES. Do not drift
       toward either without a reason:
         · 0.02 / 5 oct / 512  (~50px blobs) — read as WALLPAPER on staging.
           However well a tile stitches, ~10 features per tile edge lets the eye
           catch the repeat.
         · 0.45 / 2 oct / 160  (~2px)        — read as PURE NOISE. Too fine to
           have structure; one dominant scale always looks like static.

       The two numbers that decide whether a config works:
           feat     = 1 / baseFrequency          base feature size, px
           per-edge = tile x baseFrequency       features across the tile
       Keep per-edge at 25+ or the repeat becomes legible; the wallpaper version
       had ~10, this has 31. Note that means tile size and frequency are NOT
       independent — raising the frequency lets you shrink the tile, and
       lowering it forces the tile up.

       Octaves are what stop it looking like static: they stack detail at
       1/2, 1/4, 1/8 of the base feature. But feTurbulence DOUBLES the frequency
       each octave, so anything past ~0.5 cycles/px is sub-pixel and pure waste
       — at 0.12 the 4 octaves run 0.12/0.24/0.48/0.96 and the last is already
       marginal.

       type barely matters at this scale — turbulence's creases are near
       sub-pixel — and fractalNoise wins on a symmetric distribution and a mean
       of exactly 0.5 at any config. An earlier coarse version used turbulence
       on the two light skins, when the creases were still visible.

       ⚠ filterUnits='userSpaceOnUse' x=0 y=0 width/height = THE TILE SIZE is
       LOAD-BEARING, and must be updated with the tile size. stitchTiles only
       stitches across the FILTER REGION, and SVG's default region is
       x=-10% y=-10% w=120% h=120% of the element — so a 512 rect gets noise
       stitched to a 614.4 field and shows an arbitrary 512 crop of it, leaving
       a hard line at every tile edge. Measured across the wrap: the jump at
       the seam was 11.5x a normal neighbor jump with the default region, and
       1.1x (i.e. invisible) with it pinned. The OLD sand tile had this bug too
       and got away with it — at baseFrequency 0.9 the seam jump was no larger
       than the pixel-to-pixel jump everywhere else, so nothing showed. Drop to
       any low frequency and it becomes obvious.

       PER SKIN:  slope     = grain strength (how wide the band is)
                  intercept = <that channel of --bg, 0..1> - slope x MEAN_G

       ⚠ MEAN_G is the mean of the RAW noise. It happens to be 0.5 for
       fractalNoise at ANY config, but it is NOT for turbulence — summing
       absolute values skews that distribution low, and it shifts with
       baseFrequency, octaves AND tile size (stitchTiles snaps the frequency to
       fit the tile). MEASURE it for the exact combo you ship, never assume it.
       Measured in Chromium:
           fractalNoise  ANY config     -> 0.5000   <- what all four use now
           turbulence   0.45 / 2 / 160  -> 0.2546
           turbulence   0.02 / 5 / 512  -> 0.3143
       Slope per skin: Night 0.020 · Dusk 0.028 · Sunset 0.035 · Paper 0.034 —
       all halved 2026-08-22 after the first staging look. Note the histogram
       barely changes with frequency: fine noise LOOKS fainter because the eye
       integrates it, but it measures the same, so a genuine fade has to come
       from the slope.

       ⚠ SLOPE IS NEAR ITS FLOOR NOW, especially on Night. slope x 255 is the
       whole band in 8-bit levels, and the bulk of the noise (p05..p95) occupies
       only ~30% of it: Night's 0.020 works out to ~5 levels total and ~1.5 for
       the bulk. Halving again would put the visible part under one level —
       nothing renders, or it bands. If it still reads too strong at this point,
       the answer is a different pattern, not a smaller slope.

       ⚠ The intercepts HARD-CODE --bg. Change a skin's --bg and you must
       recompute its three intercepts, or the grain drifts off the ground and
       the lift comes back.

       There is a live tuner (measures MEAN_G itself and auto-centers, with a
       grain-vs-flat seam test) at
       https://claude.ai/code/artifact/3755ac24-68ab-4bca-b202-db0260b2ba80

       (--page-tint-a also lived here — the per-Project accent tint strength.
       Removed 2026-08-22 with the tint itself.)

       Night — --bg #16171A = 0.086 / 0.090 / 0.102, slope 0.020 */
    --page-texture:   url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='256' height='256'%3E%3Cfilter id='g' color-interpolation-filters='sRGB' filterUnits='userSpaceOnUse' x='0' y='0' width='256' height='256'%3E%3CfeTurbulence type='turbulence' baseFrequency='0.5885' numOctaves='1' stitchTiles='stitch'/%3E%3CfeColorMatrix type='saturate' values='0'/%3E%3CfeComponentTransfer%3E%3CfeFuncR type='linear' slope='0.110' intercept='0.068'/%3E%3CfeFuncG type='linear' slope='0.110' intercept='0.072'/%3E%3CfeFuncB type='linear' slope='0.110' intercept='0.084'/%3E%3CfeFuncA type='linear' slope='0' intercept='1'/%3E%3C/feComponentTransfer%3E%3C/filter%3E%3Crect width='256' height='256' filter='url(%23g)'/%3E%3C/svg%3E");

    /* ── Background motif (middle layer of the ::before stack, style.css) ──
         ⚠ FALLBACK ONLY. The live background is chosen in
         includes/backgrounds/sorter.php, which emits --page-motif for all
         three skins AFTER this sheet and therefore wins. Editing the values
         below will NOT change the site background — edit the sorter.
         They survive so that a missing /assets/images/backgrounds folder
         degrades to the old chevron instead of to no motif at all.

         The legacy artwork is a per-skin SVG (assets/images/motif-*.svg);
         --page-motif-size zooms the whole pattern (the SVG has a viewBox, so
         it scales uniformly), and --page-motif: none switches a skin off.
         These legacy files need their ?v= bumped by hand when they change;
         the sorter's tiles are filemtime-versioned automatically. ── */
    --page-motif:       url('/assets/images/motif-dark.svg?v=3');
    --page-motif-size:  672px 552px;
}

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

    /* "Porcelain" (candidate E, adopted 2026-07-15) — cool blue-gray daylight,
       the day-sky partner to the navy dark. Replaced the warm manuscript-paper
       values, which fought the portal colors on real pages. */
    --bg:             #E6E9F0;
    --surface-1:      #F5F7FB;
    --surface-2:      #FFFFFF;
    --surface-3:      #EAEEF5;

    --text:           #1C2836;
    --text-2:         #4E5B6C;
    --text-3:         #7C899A;

    --border:         #D3DAE5;
    --border-strong:  #BCC6D6;

    --accent:         #155999;
    --accent-2:       #0A638A;
    --on-accent:      #FFFFFF;
    --accent-glow:    rgba(21, 89, 153, 0.09);
    --accent-ring:    rgba(21, 89, 153, 0.20);
    --accent-tint:    rgba(21, 89, 153, 0.14);

    --good:           #1F8A57;
    --warn:           #A87413;
    --danger:         #C0403D;
    --good-tint:      rgba(31, 138, 87, 0.10);
    --warn-tint:      rgba(168, 116, 19, 0.10);
    --danger-tint:    rgba(192, 64, 61, 0.10);

    --shadow:         rgba(28, 39, 56, 0.12);

    /* Paper — --bg #E6E9F0 = 0.902 / 0.914 / 0.941, slope 0.034.
       (Was multiply with white-scaled noise plus an alpha-coverage hack; the
       self-colored tile makes both unnecessary. See the note on :root.)
       ⚠ Least headroom of the four: blue sits at 0.941, so its band reaches
       0.907 + 0.068 = 0.975 at the theoretical top. Raising the slope HERE
       clips blue first, which tints the highlights warm rather than just
       flattening them. */
    --page-texture:   url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='256' height='256'%3E%3Cfilter id='g' color-interpolation-filters='sRGB' filterUnits='userSpaceOnUse' x='0' y='0' width='256' height='256'%3E%3CfeTurbulence type='turbulence' baseFrequency='0.5885' numOctaves='1' stitchTiles='stitch'/%3E%3CfeColorMatrix type='saturate' values='0'/%3E%3CfeComponentTransfer%3E%3CfeFuncR type='linear' slope='0.110' intercept='0.884'/%3E%3CfeFuncG type='linear' slope='0.110' intercept='0.896'/%3E%3CfeFuncB type='linear' slope='0.110' intercept='0.923'/%3E%3CfeFuncA type='linear' slope='0' intercept='1'/%3E%3C/feComponentTransfer%3E%3C/filter%3E%3Crect width='256' height='256' filter='url(%23g)'/%3E%3C/svg%3E");

    --page-motif:       url('/assets/images/motif-light.svg?v=3');
    --page-motif-size:  672px 552px;
}

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

    /* "Twilight Slate" (hand-tuned 2026-07-19) — the between-skin for rooms
       where night sky is too dark and porcelain too bright. Mid slate ground,
       solid white ink. Deliberate inversion: surface-2 is DARKER than the
       cards, so inputs, menus, and modals read dark-on-light. 4D5359*/
    --bg:             #27303e;
    --surface-1:      #394558;
    --surface-2:      #353C45;
    --surface-3:      #424C56;

    --text:           #FFFFFF;
    --text-2:         #D6DADF;
    --text-3:         #B1B8C2;

    --border:         #B6CCEC;
    --border-strong:  #CCDBF2;

    --accent:         #3DB8FF;
    --accent-2:       #8BD4FF;
    --on-accent:      #0A1420;
    --accent-glow:    rgba(61, 184, 255, 0.10);
    --accent-ring:    rgba(61, 184, 255, 0.24);
    --accent-tint:    rgba(61, 184, 255, 0.16);

    --good:           #4CCB8F;
    --warn:           #F0B754;
    --danger:         #F0605D;
    --good-tint:      rgba(76, 203, 143, 0.14);
    --warn-tint:      rgba(240, 183, 84, 0.14);
    --danger-tint:    rgba(240, 96, 93, 0.14);

    --shadow:         rgba(0, 0, 0, 0.28);

    /* Dusk — --bg #27303E = 0.153 / 0.188 / 0.243, slope 0.028. */
    --page-texture:   url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='256' height='256'%3E%3Cfilter id='g' color-interpolation-filters='sRGB' filterUnits='userSpaceOnUse' x='0' y='0' width='256' height='256'%3E%3CfeTurbulence type='turbulence' baseFrequency='0.5885' numOctaves='1' stitchTiles='stitch'/%3E%3CfeColorMatrix type='saturate' values='0'/%3E%3CfeComponentTransfer%3E%3CfeFuncR type='linear' slope='0.110' intercept='0.135'/%3E%3CfeFuncG type='linear' slope='0.110' intercept='0.170'/%3E%3CfeFuncB type='linear' slope='0.110' intercept='0.225'/%3E%3CfeFuncA type='linear' slope='0' intercept='1'/%3E%3C/feComponentTransfer%3E%3C/filter%3E%3Crect width='256' height='256' filter='url(%23g)'/%3E%3C/svg%3E");

    --page-motif:       url('/assets/images/motif-dim.svg?v=3');
    --page-motif-size:  672px 552px;
}

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

    /* "Sunset" (added 2026-08-22) — the warm partner to porcelain, and the app
       skin counterpart of the sepia reading surface. Built from Write's sepia
       (#DDCFB2 ground / #F4EAD2 sheet) but PULLED BACK a step on the two big
       surfaces: at full-window scale #DDCFB2 reads as a tan wash over the whole
       app, where the reading sheet only ever shows it in a column. The cards
       stay close to Write's value so a manuscript still looks like the same
       paper. Ink and accent are unchanged from the app. */
    --bg:             #E5DCC8;
    --surface-1:      #F6EFDE;
    --surface-2:      #FFFCF5;   /* menus, inputs, modals — the raised surface */
    --surface-3:      #EDE4D0;

    --text:           #3A2F22;
    --text-2:         #6B5D46;
    --text-3:         #8E7F63;

    --border:         #D9CDB2;
    --border-strong:  #C4B593;

    /* Brand blue, exactly as porcelain and as the sepia reading theme — a warm
       accent on a warm ground collapses the two and kills link legibility. */
    --accent:         #155999;
    --accent-2:       #0A638A;
    --on-accent:      #FAF8F3;
    --accent-glow:    rgba(21, 89, 153, 0.09);
    --accent-ring:    rgba(21, 89, 153, 0.20);
    --accent-tint:    rgba(21, 89, 153, 0.14);

    --good:           #1F8A57;
    --warn:           #A87413;
    --danger:         #C0403D;
    --good-tint:      rgba(31, 138, 87, 0.10);
    --warn-tint:      rgba(168, 116, 19, 0.10);
    --danger-tint:    rgba(192, 64, 61, 0.10);

    --shadow:         rgba(58, 47, 34, 0.14);

    /* Sunset — --bg #E5DCC8 = 0.898 / 0.863 / 0.784, amplitude 0.045. Unlike
       the old shared-with-porcelain grain, this one is genuinely warm: the
       band is centered on the warm ground, so the noise rides the cream
       instead of being a neutral gray multiplied over it. The turbulence
       mottle is doing real work on this skin — it is the one that actually
       wants to look like parchment.

       Sunset — --bg #E5DCC8 = 0.898 / 0.863 / 0.784, slope 0.035. It briefly
       ran type='turbulence' on a 256 tile, when the mottle was coarse and the
       creases were worth having; at 0.45 the creases are sub-pixel and the two
       noise types converge, so it is back on the shared recipe. */
    --page-texture:   url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='256' height='256'%3E%3Cfilter id='g' color-interpolation-filters='sRGB' filterUnits='userSpaceOnUse' x='0' y='0' width='256' height='256'%3E%3CfeTurbulence type='turbulence' baseFrequency='0.5885' numOctaves='1' stitchTiles='stitch'/%3E%3CfeColorMatrix type='saturate' values='0'/%3E%3CfeComponentTransfer%3E%3CfeFuncR type='linear' slope='0.110' intercept='0.880'/%3E%3CfeFuncG type='linear' slope='0.110' intercept='0.845'/%3E%3CfeFuncB type='linear' slope='0.110' intercept='0.766'/%3E%3CfeFuncA type='linear' slope='0' intercept='1'/%3E%3C/feComponentTransfer%3E%3C/filter%3E%3Crect width='256' height='256' filter='url(%23g)'/%3E%3C/svg%3E");

    /* No sepia tile is drawn — sepia borrows the LIGHT motif, both here and in
       includes/backgrounds/_emit.php (which must agree). Generate sepia tiles
       via tools/gen-backgrounds.js if it ever earns its own. */
    --page-motif:       url('/assets/images/motif-light.svg?v=3');
    --page-motif-size:  672px 552px;
}

/* ═══ PINNED NAVY — public cinematic pages ════════════════════════════════════
   The landing / pricing / legal pages are fixed navy and never theme-switch;
   head.php stamps data-theme-locked on them ($leanHead). They are NOT literal
   all the way down — landing.css paints .v2-page with var(--bg-light) and
   .lp-features with var(--card-bg-light), both compat aliases of the slots
   above — so when dark went neutral 2026-08-22 they would have gone neutral
   with it. This block holds the brand navy exactly where it is supposed to be
   fixed. html[data-theme-locked] (0,1,1) outranks :root (0,1,0), and it is
   later in the file besides. Do not delete: it is the ONLY thing keeping the
   marketing surfaces off the app palette. */
html[data-theme-locked] {
    color-scheme: dark;

    --bg:             #0B141F;
    --surface-1:      #121E2D;
    --surface-2:      #182636;
    --surface-3:      #1E3044;

    --text:           #EFEBE1;
    --text-2:         #B7BAB5;
    --text-3:         #84909B;

    --border:         #1F2E40;
    --border-strong:  #2C405A;

    --on-accent:      #0A1420;
    --shadow:         rgba(0, 0, 0, 0.45);

    --page-motif:       url('/assets/images/motif-dark.svg?v=3');
    --page-motif-size:  672px 552px;
}

/* ═══ COMPATIBILITY ALIASES ═══════════════════════════════════════════════
   The pre-2026 token names, re-pointed at the semantic set. Theme-agnostic
   (defined once — they follow whatever the slots above resolve to), so every
   existing var(--bg-light) rule switches skins for free. New code uses the
   semantic names; delete this block when the literal sweep finishes. */
:root {
    --bg-light:       var(--bg);
    --bg-base:        var(--bg);
    --card-bg-light:  var(--surface-1);
    --border-light:   var(--border);
    --text-light:     var(--text);
    --text-muted:     var(--text-3);      /* was scope-local to reading themes */
    --accent-primary: var(--accent);
    --accent-secondary: var(--accent-2);
    --btn-on-accent:  var(--on-accent);
    --subnav-bg:      var(--surface-1);
    --shadow-light:   var(--shadow);

    /* Dead-twin legacy tokens — frozen at their historical literals so any
       straggler rule renders exactly as before. Sweep usages, then delete. */
    --bg-dark:        #1A1A1A;
    --text-dark:      #E8E8E8;
    --card-bg-dark:   #252525;
    --border-dark:    #3A3A3A;
    --shadow-dark:    rgba(0, 0, 0, 0.3);
}
