:root {
    /* Brand — Flat UI */
    --brand-blue: #3498db;
    --brand-blue-dark: #2980b9;
    --brand-purple: #9b59b6;
    --brand-purple-dark: #8e44ad;
    --brand-pink: #e84393;            /* deliberate accent; pilot decides if gradient-strays map here vs purple */
    --brand-blue-rgb: 52, 152, 219;   /* for rgba(var(--brand-blue-rgb), a) tints */
    /* Completing the channel family. Five post-type templates each declared this
       locally with the SAME value (231, 76, 60 = --danger), so promoting it is
       visually inert: no page changes, and 14 usages stop depending on a
       per-template declaration that a sixth template would have had to remember. */
    --danger-rgb: 231, 76, 60;
    --brand-gradient: linear-gradient(135deg, #3498db, #9b59b6);
    --brand-gradient-h: linear-gradient(90deg, #3498db, #9b59b6);

    /* -- Extended accents: indigo and amber ---------------------------------
       ⚠ NOT Flat UI, and deliberately NOT in the --brand-* family above. These
       are Tailwind's indigo-400/500 and amber-500, and they are declared here
       because they were already load-bearing, not because anyone chose them as
       brand colours. Measured 2026-09-10, as raw literals:

           indigo-500  #6366f1   55 hex +  466 rgba  =  521 uses
           indigo-400  #818cf8  112 hex +   29 rgba  =  141 uses
           amber-500   #f59e0b  213 hex +  270 rgba  =  483 uses

       1,145 usages across 256 files, none of them declared anywhere. That is a
       second palette running beside the brand one, and leaving it undeclared
       meant "use the token" and "match what is on screen" gave different
       answers. The 500 is the tint and border base, the 400 the lighter glyph
       colour on dark - the usual pairing, which is why both earn a name.

       ⚠ --accent-amber (further down) is an alias onto --warning (#f1c40f) and
       is a DIFFERENT yellow from --amber-500. It has 30 consumers, so it is
       left alone rather than repointed - changing it would restyle all 30 to
       make a naming tidy-up look neater. If you want the Tailwind amber, ask
       for it by name.

       Promoting these is visually inert, exactly as --danger-rgb was: the only
       local declarations were forgot-password and reset-password, holding these
       same values, and both are removed in this change.

       DECLARING IS NOT MIGRATING. The 1,145 raw literals are untouched; that is
       a separate pass, and a blanket sed would be the wrong way to do it. */
    --indigo-500: #6366f1;
    --indigo-500-rgb: 99, 102, 241;
    --indigo-400: #818cf8;
    --indigo-400-rgb: 129, 140, 248;
    --amber-500: #f59e0b;
    --amber-500-rgb: 245, 158, 11;

    /* -- Theme channels -----------------------------------------------------
       Foreground and background as bare RGB triplets, so an alpha layer can be
       written rgba(var(--fg-rgb), 0.6) rather than rgba(255, 255, 255, 0.6).

       This is the part a light or high-contrast mode actually needs. The
       codebase carries 6,225 white-alpha literals spread over 41 distinct
       alphas - 0.70, 0.60, 0.85, 0.80, 0.55, 0.45 - which is a continuum, not
       a scale, so there is no ladder to snap them onto without moving pixels
       on hundreds of surfaces. But the alpha is not what a theme changes: a
       label at 60% foreground stays at 60% whether the page is black or white.
       Only the CHANNEL flips. Routing the literals through --fg-rgb therefore
       makes the whole layer themeable with no design decisions and no visual
       change, and leaves collapsing those 41 alphas as a separate, optional
       design pass rather than a prerequisite for a theme.

       The *-rgb convention ships here for the brand and semantic colours (see
       --brand-blue-rgb below and the --danger-rgb block further down).
       CORRECTION: this paragraph used to name --danger-rgb, --warning-rgb,
       --resonance-rgb and --ac-primary-rgb as already live HERE. They were
       not - they were per-page :root definitions in a handful of post-type
       templates, so anything shared that tinted through them computed to the
       empty string everywhere else (#94). The semantic ones are now defined
       here for real; --resonance-rgb and --ac-primary-rgb remain per-page, and
       correctly so, being one feature's colours.

       BLACK IS NOT THE INVERSE OF --fg-rgb, deliberately. A shadow is an
       absence of light rather than a foreground mark, and a media scrim exists
       to make the media the only thing on screen - both stay black under a
       light theme. So --shadow-*, --modal-scrim-* and --modal-bg-media keep
       their literal black and are NOT routed through a channel. --bg-rgb is
       only for surfaces that genuinely track the page ground. */
    --fg-rgb: 255, 255, 255;
    --bg-rgb: 0, 0, 0;

    /* Page base + surface elevation (body pure black; surfaces tint blue, lighter as they rise) */
    --bg: rgb(var(--bg-rgb));
    --surface-1: #12151c;
    --surface-2: #171b24;
    --surface-3: #1e2430;
    --surface-hover: #232a38;
    /* The tinted surface as a GRADIENT, for large singular surfaces - page bands,
       hero sections, modal shells, panel headers - where the ramp has room to read
       as depth. Renamed from --hero-bg on 2026-08-26: it had drifted onto 91
       backgrounds, most of them cards and panels rather than heroes, so the name was
       telling readers it did not apply to them.

       THIS IS ITS OWN COLOUR FAMILY, and that is deliberate rather than an oversight.
       An earlier attempt built it out of var(--surface-1/2), reasoning that a surface
       token should sit on the surface ladder. It does not: the ladder is a low-chroma
       GREY-blue (MEASURED at 22% saturation) while this is a real BLUE, and deriving
       from the ladder stripped 30% of the colour out and read as mud. Take LIGHTNESS
       from the ladder; never take chroma from it.

       Hue and saturation are held CONSTANT across all three stops. The pre-2026-08-26
       ramp drifted 28% -> 33% saturation and 5 degrees of hue from end to end, so its
       dark end was visibly greyer than its bright end - part of what read as
       inconsistent.

       MEASURED, against the ramp it replaces:
         saturation   31% (drifting)  ->  35% (constant)   - bluer, and evenly so
         brightest L*         13.25   ->  9.63             - that stop used to sit at
                                                             --surface-3, the third
                                                             elevation tier, against a
                                                             base surface of 6.76

       The three stops are separate tokens purely so --surface-grad-flat below can BE
       the middle one by reference. Two hexes that must stay equal drift; a reference
       cannot. */
    --surface-grad-lo: #0b0f16;
    --surface-grad-mid: #0f151f;
    --surface-grad-hi: #141b27;
    --surface-grad: linear-gradient(160deg,
        var(--surface-grad-lo),
        var(--surface-grad-mid),
        var(--surface-grad-hi));
    /* The same surface as a FLAT COLOUR, and it exists because a gradient is an
       <image>, not a <color>. MEASURED: var(--surface-grad) in a colour-only slot is
       invalid at computed-value time and the whole declaration is dropped in silence -
       background-color falls back to transparent, and border-color and color fall back
       to WHITE, on a theme that has no white in it. Use this wherever the gradient's
       colour is wanted but a gradient cannot go: box-shadow, border, outline, fill,
       or a solid fallback.

       It IS the ramp's midpoint, by reference rather than by a repeated hex, so a ring
       drawn in it sits invisibly on the gradient near the middle of a panel and stays
       that way if the ramp is ever retuned. */
    --surface-grad-flat: var(--surface-grad-mid);

    /* Glass — blue-tinted so translucent cards match the tinted --surface-* family
       (over the black body this composites to ~#121417, close to --surface-1) */
    --glass-bg: rgba(200, 225, 255, 0.09);
    --glass-blur-sm: 8px;
    --glass-blur-md: 12px;
    --glass-blur-lg: 20px;
    --glass-blur: 20px;   /* alias for --glass-blur-lg; prefer the named tier */

    /* Radius — scale is 4 / 8 / 12 / 16 / 24. Off-scale values (10px, 14px, 6px,
       3px) are NOT swept; they snap to the nearest step only when a template is
       touched for other reasons.
       20px WAS swept (351 sites, 2026-08-13). The earlier pass had left it literal
       because 20 is exactly equidistant between 16 and 24, so no snap direction was
       derivable — that tie is now broken UP, by decision, onto --radius-xl. It is a
       judgement call and not a correction: every one of those surfaces is 4px rounder
       than it was. Do not reintroduce a literal 20px radius.
       --radius-full was deliberately NOT promoted: it meant 9999px in 7 files and
       50% in 5, i.e. pill and circle under one name. Splitting is the fix, so the
       ambiguous name is retired rather than given a winning value. */
    --radius-xxs: 2px;   /* micro-chrome only: scrollbar thumbs, progress bars, hairline
                            accents. Their median declared size is 4px, so snapping these
                            to --radius-xs would double the radius proportionally. */
    --radius-xs: 4px;
    --radius-sm: 8px;
    --radius-md: 12px;
    --radius-lg: 16px;
    --radius-xl: 24px;
    --radius-2xl: 2rem;   /* hero-card tier: billboards and flagship cards sit a full
                             step above the ladder (2rem x27, 30px x19 in the census).
                             rem on purpose: it is what the sites already used, and it
                             scales with the user's font preference. = 32px default. */
    --radius-pill: 9999px;
    --radius-circle: 50%;
    --card-radius: var(--radius-lg);   /* alias — 32 uses, unanimously 16px */

    /* Borders. Four tiers now: --border-hairline (0.08) was the second most
       common border alpha in the codebase at 270 sites and had no name, which
       is most of why that literal persisted. Alphas are unchanged; they are
       expressed through --fg-rgb so a theme flip carries them. */
    --border-subtle: rgba(var(--fg-rgb), 0.06);
    --border-hairline: rgba(var(--fg-rgb), 0.08);
    --border: rgba(var(--fg-rgb), 0.10);
    --border-strong: rgba(var(--fg-rgb), 0.15);
    --border-brand: rgba(52, 152, 219, 0.35);

    /* Stacking layers. 1,946 z-index declarations existed at 0% tokenised, and
       the raw number overstates the problem: 670 sit in a rule that also creates
       its own stacking context, and 1,162 are single digits doing local layering
       inside a component. Neither group competes globally. The real surface is
       the 221 position:fixed declarations.

       The bands below are DERIVED, not invented - the codebase already layers on
       1s / 10s / 100s / 10,000s and the selector names line up with them. Gaps of
       100 exist so a component can offset within its layer via calc() instead of
       bidding a bigger number, which is how 99999999 happened.

       TOAST SITS ABOVE MODAL. toast.css (100000) and modal.css (100001) shipped
       the opposite, so a dialog covered a system notification. Three abandoned
       local copies of a layer scale - characters-list, persons-list, users-page,
       each with 0 uses - all put toast on top too, so the shipped order was the
       outlier rather than the intent. */
    --z-raised: 10;        /* cards, hover lift, badges */
    --z-sticky: 100;       /* sticky in-page headers and toolbars */
    /* The FIXED top nav. It is the reference point the tiers above are described
       against, so it needs a name of its own: while it was an unnamed literal, the
       tier documented as clearing it was set to the SAME 1000 and merely tied,
       leaving paint order to fall to DOM position. */
    --z-nav: 1000;
    --z-dropdown: 1100;    /* menus, popovers, selects - ABOVE --z-nav, not equal to it */
    --z-float: 10000;      /* FAB, mini-player, floating chrome */
    --z-overlay: 10100;    /* full-screen scrims and drawers */
    --z-modal: 10200;      /* dialogs, above their own scrim */
    --z-tooltip: 10300;    /* has to stay readable over a modal */
    --z-toast: 10400;      /* system notifications - always visible */
    --z-boot: 10500;       /* the first-paint intro overlay ONLY */

    /* Type leading. 1,237 declarations at 0% tokenised, clustering onto seven
       values that map to real typographic roles: 1 for buttons and icons where
       leading would misalign them, 1.2 for headings, up through 1.7 for spacious
       long-form.

       NOT collapsed. A half-step tail exists - 1.55 x33, 1.45 x33, 1.35 x30,
       1.25 x22, 1.15 x24, 1.05 x11, 1.65 x10 - and every one of those sits
       equidistant between two tiers, so snapping is a coin flip that moves text
       density on 178 surfaces for no gain. They stay literal, as does 1.1 x38:
       there is no natural name between "none" and "tight", and inventing one for
       38 sites would be naming drift rather than a role.

       Unlike a spacing scale, these are worth naming: "body leading" is a thing a
       designer retunes, and retuning it should move every body surface at once. */
    --leading-none: 1;
    --leading-tight: 1.2;
    --leading-snug: 1.3;
    --leading-base: 1.4;
    --leading-normal: 1.5;
    --leading-relaxed: 1.6;
    --leading-loose: 1.7;

    /* Type weight. 5,574 declarations existed at 0% tokenised across effectively
       six values - 600 x2404, 700 x1261, 500 x1222, 800 x351, 900 x238, 400 x97 -
       which clusters harder than radius did and neither programme had looked at
       it.

       These are ROLE names, not value synonyms, which is the distinction that
       ruled OUT a spacing scale: --space-12 is named for its value and would
       never be retuned, while --weight-semibold is named for what it does, and
       switching to a face whose 600 reads too heavy is a real single-place
       change. They are also the standard CSS weight keywords, so there is no
       mapping to memorise.

       300 is deliberately absent: 3 uses is not a tier. */
    --weight-normal: 400;
    --weight-medium: 500;
    --weight-semibold: 600;
    --weight-bold: 700;
    --weight-extrabold: 800;
    --weight-black: 900;

    /* A foreground that must NOT follow the theme, because it sits on a surface
       that stays coloured. A label on a brand button is white on dark and still
       white on light - the button is blue either way. Deliberately literal, and
       deliberately named, so the intent is legible at the call site instead of
       looking like a literal somebody forgot to tokenise.

       Found by the runtime contrast probe (docs/design/contrast-probe.js): with
       the channel flipped, a "View List" CTA on #4f46e5 indigo scored 2.77:1. */
    --text-on-brand: #ffffff;

    /* Text - expressed through --fg-rgb so a theme flip carries them. The
       ladder itself is unchanged. */
    --text: rgb(var(--fg-rgb));
    --text-secondary: rgba(var(--fg-rgb), 0.70);
    --text-muted: rgba(var(--fg-rgb), 0.50);
    --text-dim: rgba(var(--fg-rgb), 0.40);

    /* ── Elevation ──────────────────────────────────────────────────────────
       A 5-step ladder derived from the 668 single-layer black shadows in the
       codebase, whose blur radii cluster into five bands that map onto real
       component roles. Blur is what reads as height, so the bands are drawn on
       blur and the y-offset tracks it at roughly 1:3.

         sm   resting    badges, slider thumbs, small chips        (63 sites)
         md   raised     hover lift, posters, buttons             (132 sites)
         lg   floating   cards, dropdowns, squad icons            (190 sites)
         xl   detached   menus, panels, toasts, popovers          (198 sites)
         2xl  modal      dialogs, lightboxes, search overlay       (85 sites)

       Values come from the LITERALS, not from the handful of older --shadow-*
       definitions: `0 4px 12px` outnumbers `0 4px 16px` 53:17, and
       `0 8px 24px` outnumbers `0 8px 32px` 69:25. B3 correctly used the
       definition plurality when promoting a name its consumers were bound to;
       for a design pass over 668 literals the literals are the population.

       xl CHANGED MEANING here: the 29-48 band was the largest in the codebase
       and had no token at all, which is most of why the tail existed. The old
       xl value is now 2xl. */
    --shadow-sm:  0 2px 8px rgba(0, 0, 0, 0.30);
    --shadow-md:  0 4px 12px rgba(0, 0, 0, 0.35);
    --shadow-lg:  0 8px 24px rgba(0, 0, 0, 0.45);
    --shadow-xl:  0 12px 40px rgba(0, 0, 0, 0.50);
    --shadow-2xl: 0 24px 80px rgba(0, 0, 0, 0.60);

    /* ── Coloured shadows: ring / glow / lift ────────────────────────────────
       Three shapes x five semantic hues, from 842 coloured shadows that map to
       a brand family. The shapes are distinct affordances, not sizes:

         ring   0 0 0 3px    focus indication on inputs and controls
         glow   0 0 20px     ambient state: status dots, active chips
         lift   0 4px 14px   coloured drop shadow under a hovered control

       Deliberately NOT a single shape token plus an inherited --glow-rgb hue
       variable: a hue set on .btn-danger would cascade into every descendant
       that uses a glow, which is a silent and very hard-to-find footgun. 15
       explicit names have no inheritance surprises.

       Retires --shadow-brand and --shadow-focus, which were exact duplicates
       of --lift-blue and --ring-blue under different names (#71). */
    --ring-blue:      0 0 0 3px rgba(52, 152, 219, 0.15);
    --ring-purple:    0 0 0 3px rgba(155, 89, 182, 0.15);
    --ring-danger:    0 0 0 3px rgba(231, 76, 60, 0.15);
    --ring-success:   0 0 0 3px rgba(46, 204, 113, 0.15);
    --ring-warning:   0 0 0 3px rgba(241, 196, 15, 0.15);

    --glow-blue:      0 0 20px rgba(52, 152, 219, 0.25);
    --glow-purple:    0 0 20px rgba(155, 89, 182, 0.25);
    --glow-danger:    0 0 20px rgba(231, 76, 60, 0.25);
    --glow-success:   0 0 20px rgba(46, 204, 113, 0.25);
    --glow-warning:   0 0 20px rgba(241, 196, 15, 0.25);

    --lift-blue:      0 4px 14px rgba(52, 152, 219, 0.30);
    --lift-purple:    0 4px 14px rgba(155, 89, 182, 0.30);
    --lift-danger:    0 4px 14px rgba(231, 76, 60, 0.30);
    --lift-success:   0 4px 14px rgba(46, 204, 113, 0.30);
    --lift-warning:   0 4px 14px rgba(241, 196, 15, 0.30);

    /* Motion. The composite --transition-* names are expressed in terms of the
       primitives, so ~595 existing uses need no edit.

       ⚠ MEASURED 2026-08-24, and this scale failed the same way the spacing one
       below does — it could not express what people actually reached for, so
       they hand-typed instead. 38 distinct cubic-beziers exist across the repo:

           244  0.4,0,0.2,1        --ease-standard
           122  0.34,1.56,0.64,1   --ease-spring
           171  the ease-out family, ALL hand-typed, no token:
                 93  0.22,1,0.36,1     51  0.25,1,0.5,1
                 25  0.16,1,0.3,1       2  0.23,1,0.32,1
             7  0.32,0.72,0,1      drawer curve, hand-typed
            +3 spellings of one curve (0.4,0,0.2,1 / .4,0,.2,1 / 0.4,0.0,0.2,1)

       The three names below close that gap. Values are the documented standards
       (animations.dev), not our nearest hand-typed variant: the perceptual
       difference against 0.22,1,0.36,1 is nil, and picking the deliberate curve
       over the accidental one is the whole point of having a token.

       INTENT decides which you use, not appearance:
         entering / exiting          -> --ease-out      (starts fast, feels responsive)
         moving or morphing on screen-> --ease-in-out
         drawers, sheets, panels     -> --ease-drawer
         hover / colour change       -> ease            (the bare keyword is correct here)
         constant motion (marquee)   -> linear
       Never ease-in on UI: it delays the moment the user is watching most.

       DURATIONS need no new steps — the existing four already cover the budgets:
         press feedback 100-160ms -> --dur-fast    tooltip/popover 125-200ms -> --dur-base
         dropdown 150-250ms       -> --dur-base    modal/drawer 200-500ms    -> --dur-slow
       UI motion stays under 300ms. --dur-overlay is for takeover surfaces only. */
    --ease-standard: cubic-bezier(0.4, 0, 0.2, 1);
    --ease-spring:   cubic-bezier(0.34, 1.56, 0.64, 1);
    --ease-out:      cubic-bezier(0.23, 1, 0.32, 1);
    --ease-in-out:   cubic-bezier(0.77, 0, 0.175, 1);
    --ease-drawer:   cubic-bezier(0.32, 0.72, 0, 1);
    --dur-fast: 0.15s;
    --dur-base: 0.2s;
    --dur-slow: 0.3s;
    --dur-overlay: 0.45s;
    --transition-fast:   var(--dur-fast) ease;
    --transition-base:   var(--dur-base) ease;
    --transition-normal: var(--dur-slow) ease;
    --transition-smooth: var(--dur-slow) var(--ease-standard);
    /* There is deliberately no bare `--transition` token. It was defined as
       `all var(--dur-slow) var(--ease-standard)` and had 83 callers, which
       meant the design system itself shipped `transition: all` as a
       sanctioned value. All 83 were migrated to named property lists on
       2026-08-24 and the token was deleted. Do not reintroduce it: name the
       properties at the call site. */

    /* Spacing. Keeps the existing --spacing-* name: renaming 566 uses to
       --space-* would buy nothing. --container-pad is separate on purpose — the
       project rule is "container left/right padding: always 15px", which is NOT
       a step on the 4/8/16/24/32 scale and must not be reconciled onto one.

       ⚠ MEASURED 2026-08-16, before assuming this is a working system. It is
       not, and it fails twice over:

         26,544 spacing px atoms exist in templates and CSS
          8,941 (33%) are a value this scale can express
         17,603 (66%) are NOT - 12px x3053, 10px x2906, 20px x2048, 6px x2046,
                14px x1655, 2px x958 all have no token at all

       and adoption is 519 uses against those 8,941 expressible atoms, i.e. ~5%
       even where the scale DOES work.

       So the low adoption is not a discipline problem to be fixed by a sweep.
       The scale was drawn as a doubling ladder while the codebase actually
       spaces on a 2px granularity, and a ladder that cannot name the second and
       third most common values will always lose to a literal.

       Deliberately NOT fixed here. Extending the scale without converting the
       call sites mints tokens nobody uses; converting 26,544 call sites is the
       largest mechanical churn available and buys no capability, since a light
       or contrast mode does not change spacing (CLAUDE.md #78 and #82 are both
       real defects introduced by exactly this kind of scripted CSS edit).

       If you do revisit it: fix the SCALE first, because sweeping onto a ladder
       that cannot express two-thirds of the values would strand the remainder as
       literals and leave the system looking adopted while it is not. */
    --spacing-xs: 4px;
    --spacing-sm: 8px;
    --spacing-md: 16px;
    --spacing-lg: 24px;
    --spacing-xl: 32px;
    --container-pad: 15px;

    /* The bottom edge of the FIXED top nav -- i.e. how far a page must clear.
       THIS DEFAULT IS A FALLBACK, NOT THE VALUE. desktop-navigation.ftlh
       measures the nav (which changes with rails, auth state and viewport) and
       writes the real number to document.documentElement, where an inline style
       beats this declaration. The default only covers first paint and no-JS, so
       it is set to the nav's current resting height rather than a round number.

       Everything that hangs off the nav should consume this: page clearance,
       the mega/search panels, dropdowns, sticky sub-bars. Before it existed
       those were 22 distinct hardcoded numbers across 86 pages, every one of
       them a snapshot of the nav at one moment in one auth state.

       Replaces --upnav-top, which was deleted with ZERO consumers -- it named a
       position ("where the strip hangs") rather than a measurement, and the
       strip it was named for no longer exists. */
    --chrome-bottom: 80px;

    /* Modal surfaces — three tiers, because each has a reason to differ rather
       than just a different author.
         dialog: small, centred, translucent — the page showing through is fine
                 and makes a confirm feel light.
         panel:  editors, sheets, drawers. SOLID, because long scrollable text
                 over a busy page is what makes translucency unreadable.
         media:  lightbox, video, gif picker, gallery. Near-black so the media is
                 the subject and the frame disappears.
       A modal with a GRADIENT background is left alone deliberately: 8 of them
       tint delete/cancel confirms warm, which is a semantic signal, not drift,
       and 7 more share var(--surface-grad), which is already a convention. */
    --modal-bg-dialog: rgba(18, 21, 28, 0.95);
    --modal-bg-panel:  var(--surface-2);
    --modal-bg-media:  rgba(0, 0, 0, 0.92);

    /* Modal scrim — the dimming layer behind an overlay.
       Two tiers, not one: every full-viewport backdrop at alpha >= 0.9 in this
       codebase turned out to be a media viewer (lightbox, video, gif, gallery),
       which wants a near-opaque ground so the media is the only thing you see.
       That is the same reason the Media modal tier is near-black, so it gets its
       own scrim rather than being averaged into the default. */
    --modal-scrim: rgba(0, 0, 0, 0.70);
    --modal-scrim-media: rgba(0, 0, 0, 0.92);
    --modal-scrim-blur: 4px;

    /* Zone accent — deliberate, not drift. The news zone uses a harder editorial
       blue than the brand. Two of its three former definitions carried an
       intentional-marker comment; only the NAME was wrong, since it claimed to
       be "primary". Kept as a value, renamed honestly, defined once. */
    --news-blue: #0066FF;

    /* Semantics — one each, Flat UI */
    --danger: #e74c3c;
    --danger-dark: #c0392b;
    --warning-dark: #d35400;
    --success: #2ecc71;
    --warning: #f1c40f;
    --info: #3498db;

    /* Semantic CHANNELS, for alpha tints of the semantic colours.
       Each is the channel form of the hex directly above, so the pair cannot
       drift — a tint and its solid always name the same colour.

       (Written without a literal example on purpose: TokenAdoptionContractTest
       scans this file for token references and does not skip comments, so a
       sample name in prose reads as a real reference to a token that does not
       exist. It caught exactly that here.)

       These are needed because an alpha tint of an UNDEFINED channel computes
       to the EMPTY STRING and the whole declaration is dropped silently (#94),
       so a SHARED component in components.css can only tint through a channel
       that is defined globally. --danger-rgb and --warning-rgb existed only as
       per-page :root definitions in six post-type templates; the comment at the
       top of this file asserted they shipped here, and they did not. Those
       pages' local definitions were removed in the 2026-08-25 consolidation, so
       these are now the only definitions - nothing overrides them.

       --info-rgb is deliberately the same channel as --brand-blue-rgb, matching
       --info being the same hex as --brand-blue. */
    --danger-rgb: 231, 76, 60;
    --success-rgb: 46, 204, 113;
    --warning-rgb: 241, 196, 15;
    --info-rgb: 52, 152, 219;
    --brand-purple-rgb: 155, 89, 182;

    /* Aliases — a second vocabulary that grew alongside the canonical one. Each
       is now ONE definition pointing at the token it always meant, so ~1400 uses
       resolve consistently with no call-site edits. Where a majority already
       pointed at a canonical token, that won. Where there was no majority (a
       one-vote margin, or --bg-hover's 6-6 tie), the NAME decided against the
       #27 ladder, because a one-vote margin is noise and a name is stated intent.
       Prefer the canonical token in new code; these die out as templates are
       touched. --pa is deliberately NOT here: its two definitions disagree
       (brand-blue vs pitch-accent) and aliasing it would break pitch theming. */
    --text-primary: var(--text);
    --text-light: var(--text);
    --text-tertiary: var(--text-dim);
    --surface: var(--surface-1);
    --surface-elevated: var(--surface-2);
    --background: var(--bg);
    --bg-elevated: var(--surface-3);
    --bg-secondary: var(--surface-1);
    --bg-hover: var(--surface-hover);
    --bg-glass: var(--glass-bg);
    --bg-glass-hover: var(--surface-hover);
    --border-color: var(--border-subtle);
    --border-medium: var(--border);
    --border-hover: var(--border-strong);
    --accent: var(--brand-blue);
    --accent-blue: var(--brand-blue);
    --accent-primary: var(--brand-blue);
    --accent-secondary: var(--brand-purple);
    --accent-light: var(--brand-purple);
    --accent-red: var(--danger);
    --red: var(--danger);
    --accent-green: var(--success);
    --accent-amber: var(--warning);
    --warning-color: var(--warning);
}
