/* ===================================================================
   common/_tokens.css — the single source for values that are the same
   across every surface (admin dashboards + customer/tenant mobile app).

   Values here are pinned to docs/brand-palette.md (COLOR_001, the
   production-authority brand sheet) — don't hand-edit a hex without
   checking that sheet first. "-deep" variants aren't in the sheet (it
   defines no hover/pressed states); they're a uniform 80% darken of the
   base color, a shade rather than a new hue.

   Each family's own _layout.css still owns its own design-language
   tokens (radius scale, display font, sidebar sizing) — those are
   deliberately different between the admin dashboards and the customer
   mobile app, not duplication.
   Load this file first, before any family-specific stylesheet.
   =================================================================== */

:root {
  /* Primary brand */
  --teal:      #2CB1C1;
  --teal-deep: #238E9A;
  --teal-pale: #E6F7F8;

  /* Secondary brand — Secondary Teal isn't consumed by any component yet,
     kept available since the brand sheet defines it as part of the closed
     palette. Secondary Blue doubles as the sheet's "Information" accent. */
  --secondary-blue: #0E6E9D;
  --secondary-blue-deep: #0B587D;
  --secondary-teal: #61E5F0;

  /* Neutrals identical across every surface today */
  --ink:           #0E1F2D;
  --white:         #FFFFFF;
  --border:        #C9D1D8;
  --text-tertiary: #A8B0B7;
  --font-body:     "Inter", system-ui, -apple-system, sans-serif;

  /* Surfaces and secondary text. These four were referenced by 160-odd
     rules before they were declared anywhere: every one of those sites
     named a second colour after the comma and rendered off that, and the
     nine that named none rendered off nothing at all. Declaring
     them here is what makes the fallback redundant rather than load-
     bearing. --text-muted is the same grey as --text-secondary under the
     name half the app reached for; keeping the alias is cheaper than
     renaming 96 call sites, and it points at one value either way.
     Each family's _layout.css still declares --text-secondary itself —
     harmless, identical, and the family sheet stays readable alone. */
  --surface:        #FFFFFF;
  --bg-subtle:      #F5F7F9;
  --text-primary:   var(--ink);
  /* #6E7B87 until 2026-08-14, which was 4.33:1 on --surface, 4.04 on
     --bg-subtle and 3.69 on --mist — under WCAG AA's 4.5 on all three, for
     the token that colours every muted label in the app. This is the same
     hue and saturation, 5.5% darker in HLS, the least that clears 4.5 on
     all three grounds (5.29 / 4.93 / 4.51). Nothing reads --text-secondary
     on a dark surface: all 19 rules that pair it with a background pair it
     with transparent or a 6-7% ink tint. */
  --text-secondary: #616D78;
  --text-muted:     var(--text-secondary);

  /* ── The hero family: the surface that does NOT follow the scheme ───
     A navy statement card — the membership card, the tenant hero, the
     rates promise banner, the auth visual panel, the customer sidebar
     and topbar — is dark *because everything around it is light*. It is
     a contrast device, and the thing it contrasts against is the page.

     All of them painted themselves `background: var(--ink)` with
     `color: var(--white)` on top, which is right in light and inverts in
     dark: --ink flips to near-white, the card turns pale, --white flips
     to near-black so the headline survives, and every muted label
     written for a dark ground does not. That was the most visible
     failure of the first dark pass.

     The app had already solved it. `Theme.heroGround` in moby_ios
     DesignSystem/Theme.swift is exactly this: the ONE background use of
     `ink`, split out so it stays dark in both schemes, with `onDark`
     (always white) as its ink — deliberately a different question from
     `onAccent`, where the ground brightens and the text must go dark.
     This is that split, ported.

     So these three are declared here ONCE and deliberately never appear
     in the dark block below. They are not second names for --ink and
     --white: --ink is text and --white is a surface, both of which move,
     and these are the roles that must not. In dark mode the hero stops
     being the darkest thing on the page and becomes merely the most
     deliberate, which is what the app does too.

     --text-on-dark predates the family, is already scheme-independent
     and is already correctly named, so it becomes the alias rather than
     churning its 16 call sites — the same trade --text-muted makes
     above, and it points at one value either way. */
  --hero-ground:    #0E1F2D;
  --hero-ink:       #FFFFFF;
  --hero-ink-muted: #C9D7DE;
  --text-on-dark:   var(--hero-ink-muted);

  /* ── The seven status buckets ──────────────────────────────────────
     Explicit values, not derivations, and deliberately NOT reachable
     from the brand. See the block at the foot of this file for why the
     old --teal-derived `accent` bucket had to go. Ported from the app's
     MobyStatus, which is where these were settled. */
  --status-new-ink:      #6D28D9;
  --status-accent-ink:   #9A3412;
  --status-wait-ink:     #92400E;
  --status-progress-ink: #0E7490;
  --status-good-ink:     #067647;
  --status-bad-ink:      #B91C1C;
  --status-idle-ink:     #526071;

  --status-new-fill:      #EDE7FB;
  --status-accent-fill:   #FBE7DA;
  --status-wait-fill:     #FBEED7;
  --status-progress-fill: #DDF3F8;
  --status-good-fill:     #DEF5EA;
  --status-bad-fill:      #FBE2E2;
  --status-idle-fill:     #E8ECF2;

  /* Semantic state colors */
  --danger:       #EF4444;
  --danger-deep:  #BF3636;
  --success:      #2CB34A;
  /* The deep half of the pair --danger and --warning already had. Needed
     because the score ladder has TWO passing bands: "good" is --success and
     "excellent" has to read as better than it at a glance, not merely as
     more of the same green. */
  --success-deep: #1E8C39;
  --warning:      #FFB020;
  --warning-deep: #CC8D1A;
}

/* ===================================================================
   "moby" theme — the navy/stamp-orange/paper look already live on the
   marketing site (templates/index/index.html), the /demo signup funnel
   (_demo_page_shell.html), and guest auth (guest/_layout.html). Those
   three built it independently as inline <style> blocks with no shared
   token layer; this is that layer, so every family CSS gets it from one
   place instead of a fourth copy-paste. Applied per-org via <html
   data-theme="..."> (app/__init__.py's _apply_org_theme(), Organisations.
   theme) — free on every plan, unlike the paywalled brand_color hex. An
   org with both wins on brand_color: this block's attribute selector is
   *more* specific than partials/_brand_theme.html's plain
   ":root { --teal: ... }", so that partial marks its three properties
   !important to force the paid, explicit pick to outrank this free
   preset default regardless of load order.
   Only the shared color/font values live here — radius scale, sidebar
   sizing, etc. stay in each family's own _layout.css/shell.css, same
   split as the default theme (see the file header above). =============
   =================================================================== */
:root[data-theme="moby"] {
  --teal:          #D9581E; /* stamp-600 becomes the primary accent */
  --teal-deep:     #B84711; /* stamp-700 */
  --teal-pale:     #FBE7DB; /* same 12%-white-tint formula as _brand_palette() */
  --font-body:     "IBM Plex Sans", system-ui, -apple-system, sans-serif;
}

/* "slate" — cool, minimal, steel-blue-on-graphite. Same three-block split
   as "moby" above: this file owns the shared color/font values, each
   family's own _layout.css/_shell.css owns font-display/paper/radius. */
:root[data-theme="slate"] {
  --teal:          #3B6E91;
  --teal-deep:     #2A5170;
  --teal-pale:     #E3EDF3;
  --font-body:     "Work Sans", system-ui, -apple-system, sans-serif;
}

/* "meadow" — warm, sage-green, residential-property feel. */
:root[data-theme="meadow"] {
  --teal:          #4C7A52;
  --teal-deep:     #395F3D;
  --teal-pale:     #E6F0E4;
  --font-body:     "Karla", system-ui, -apple-system, sans-serif;
}

/* "clay" — earthy terracotta, boutique/hospitality feel. */
:root[data-theme="clay"] {
  --teal:          #B5652E;
  --teal-deep:     #8C4E23;
  --teal-pale:     #F6E8DB;
  --font-body:     "Public Sans", system-ui, -apple-system, sans-serif;
}

/* ── The five brand families, flat ────────────────────────────────────
   The same five palettes as the [data-theme] blocks above, declared on a
   bare :root so they can be read WITHOUT being the active theme.

   That is the whole reason this exists, and it is not a second name for a
   token this file already has. `--teal` is whichever family is currently
   applied; these are all five at once. Two surfaces need that and cannot
   get it from the attribute blocks:

     · a printed document, which picks its palette independently of the
       portal theme (common/print_document.css's [data-doc-palette] blocks
       read straight from here)
     · a picker, which has to draw five swatches on one page — org/
       settings.css's theme swatches and the shared .docstyle-swatch

   The [data-theme] blocks above still carry their own literals rather than
   pointing here. Folding them in is a value-preserving pass worth doing on
   its own, not inside a feature: it touches the live theming of every
   portal page, and this block adds no copy of anything — it removes the
   one org/settings.css was keeping privately. */
:root {
  --brand-classic-accent: #2CB1C1;
  --brand-classic-deep:   #238E9A;
  --brand-classic-pale:   #E6F7F8;
  --brand-classic-ink:    #0E1F2D;
  --brand-classic-rule:   #C9D1D8;

  --brand-moby-accent:    #D9581E;
  --brand-moby-deep:      #B84711;
  --brand-moby-pale:      #FBE7DB;
  --brand-moby-ink:       #12263A;
  --brand-moby-rule:      #E3DFD1;

  --brand-slate-accent:   #3B6E91;
  --brand-slate-deep:     #2A5170;
  --brand-slate-pale:     #E3EDF3;
  --brand-slate-ink:      #1B2733;
  --brand-slate-rule:     #D6DEE4;

  --brand-meadow-accent:  #4C7A52;
  --brand-meadow-deep:    #395F3D;
  --brand-meadow-pale:    #E6F0E4;
  --brand-meadow-ink:     #21301F;
  --brand-meadow-rule:    #D7E3D2;

  --brand-clay-accent:    #B5652E;
  --brand-clay-deep:      #8C4E23;
  --brand-clay-pale:      #F6E8DB;
  --brand-clay-ink:       #2C221B;
  --brand-clay-rule:      #E7D9C7;
}

/* ── Colours used in more than one stylesheet ─────────────────────────
   Hoisted out of 42 sheets so every colour in this app has a name and one
   place to change it. That pass made nothing look different; it made the
   palette visible, and what it showed was four ambers and seven greys
   doing one job each.

   The ambers and the greys are now collapsed — see the two blocks below
   for what each became. Nothing else here is: nearest-by-distance is not
   a usable rule for the rest, because per-channel distance calls #F1F3EC,
   a warm off-white, the same thing as --teal-pale, a cyan tint. Each
   remaining colour is a one-line edit away from a merge, when someone has
   the surfaces in front of them. */
:root {
  /* severity bands — inspection score tiers and the report roadmap */
  --severity-immediate: #D1453B;  /* tier 1 — act now. inspection danger tier, report roadmap band */
  --severity-monitor  : #D99A2B;  /* tier 2 — watch it. inspection warn tier, report roadmap band */
  --severity-warn-ink : #B5641F;  /* text on a tier-2 band, dark enough to read on it */
  --severity-planned  : #3E7CB1;  /* tier 4 — scheduled, no urgency. report roadmap band */

  /* danger, in the shades already in use */
  --danger-brick: #C0392B;  /* declined timeline marker, delete-hover, annotation status */
  --danger-muted: #D9534F;  /* password strength 'weak', 'no match' on confirm fields */

  /* the password-strength meter, and the affirmative toggle */
  --strength-fair  : #F0A000;  /* the meter's middle band, and its empty track */
  --strength-strong: #2E7D32;  /* the meter's top band, and 'passwords match' */
  --affirm         : #2F8F5B;  /* a yes/checked switch — greener and flatter than --success */

  /* amber text, and the pale chip it sits on.
     ONE ink, down from four. --amber-ink-deep (#B87A00) and
     --amber-ink-soft (#B07D16) were 3.61:1 and 3.63:1 on white — both
     under AA, and the two of them were the same colour four points apart.
     --amber-ink-dark (#92400E) was a real step darker but read as the same
     warning; it survives here as 5.02:1 rather than 7.09, which is the
     price of the app having one warning ink instead of four.
     --amber-bright (#E08A1E) is gone outright: its only two sites were the
     fallback half of `var(--warning, var(--amber-bright))`, and --warning
     is declared on :root above, so it had never once rendered. */
  --amber-ink : #B45309;  /* warning text — 5.02:1 on white, 4.51 on --amber-tint */
  --amber-tint: #FEF3C7;  /* the pale chip it sits on */

  /* the ember accent — demo tour, scanned-asset banner, settings */
  --ember      : #D9581E;  /* scanned-asset banner, demo tour title, settings accent */
  --ember-ink  : #12263A;  /* the navy it is paired with on those same surfaces */
  --ember-paper: #EDEAE0;  /* the warm paper behind them */

  /* blues that are not the brand's */
  --sky       : #4A90C2;  /* auth-page and shell accents */
  --sky-light : #5BA3D9;  /* the lighter step of it */
  --indigo-ink: #3730A3;  /* 'signed'/'pending' chips in chat and the shell */

  /* The greys that were here — the informal second palette CLAUDE.md 7
     names — are gone, onto the ramp this file already had. Seven neutral
     greys were a second, parallel copy of it, one step off each rung:

       --grey-slate     #4B5563 ──┐
       --grey-ink       #666666 ──┼─▶ --text-secondary   readable muted text
       --grey-ink-soft  #888888 ──┤
       --grey-ink-faint #999999 ──┘   (except the two below)
       --grey-line      #CCCCCC ──┬─▶ --border           hairlines and tracks
       --grey-line-soft #E5E5E5 ──┘
       --grey-fill      #E8E8E8 ────▶ --mist             flat pale fills

     Three of the four text greys became ONE because the app's ramp has
     exactly two muted levels and only one of them is for reading:
     --text-tertiary is 2.20:1 on white, which is decoration. So the three
     sites that are decoration — a ::placeholder, an empty calendar day's
     number, the N/A score bar's fill — took --text-tertiary, and every
     site a person actually reads took --text-secondary. The contractor
     inspection form's apparent three-level hierarchy went with them: 10%
     of lightness between steps is not a hierarchy anyone reads, and two of
     its three levels were under AA.

     The neutral cast was the tell. Every grey in this app is cool
     (blue-tinted); a perfectly neutral #666666 is what a grey looks like
     when it came from somewhere else. */

  /* cool pales */
  --mist      : #E6EEF2;  /* the cool pale surface — already spelled --mist in three sheets */
  --mist-warm : #EEF1F3;  /* its slightly warmer neighbour */
  --paper-cool: #F7F9FA;  /* the report and customer shells' page ground */
  --paper-flat: #E7E9E6;  /* card ground in the health report */

  /* warm papers — the contractor and marketing surfaces */
  --paper-warm     : #F4F1E8;  /* shell and contractor layout ground */
  --paper-warm-deep: #F5EFE7;  /* its shaded step */
  --paper-linen    : #F6F4EF;  /* chat card ground */
  --paper-bone     : #FAFAF8;  /* the palest of them */
  --paper-sage     : #F1F3EC;  /* the green-cast one */
  --paper-sage-line: #DCDFDA;  /* the rule drawn on it — invoice tables */
  --paper-sand-line: #E3DFD1;  /* the warmer rule — contractor layout, report-issue */
}

/* ═══════════════════════════════════════════════════════════════════
   THE DARK HALF — P3, ported from the app's Theme.swift
   ═══════════════════════════════════════════════════════════════════

   Everything above this line is the LIGHT palette and is unchanged by
   this pass, so a default-theme page renders byte-identically. What did
   change above: the four non-classic presets stopped overriding --ink,
   --border and --text-tertiary. That is the decision the app shipped in
   P1 — the shell is fixed per scheme and only the ACCENT follows the
   org's brand — and it is what makes a dark scheme possible at all. A
   preset that pins a near-black --ink would have carried it straight
   into dark mode and rendered ink-on-ink.

   Values come from moby_ios `DesignSystem/Theme.swift`, which is the
   source of truth for the design language until this phase lands; iOS
   ran ahead of the web deliberately.

   ── Why `data-appearance` and not `data-theme` ──────────────────────
   `data-theme` is already spoken for by the five brand presets above.
   Scheme and brand are orthogonal — a "meadow" org has a dark mode too —
   so they get separate attributes. The name matches the app's own
   `Appearance` (System / Light / Dark, stored under "moby.appearance"),
   so the two platforms answer the same question with the same word.

   ── Two blocks, one palette, and they must stay identical ───────────
   The dark palette is declared TWICE below and there is no way around
   it: a media query and an attribute selector cannot be merged into one
   rule, and CSS has no way to say "apply this block under either
   condition". So the values are duplicated, and duplication drifts —
   which is why `ui_lint`'s `dark-half` check compares the two blocks
   property by property and fails if they disagree. That drift has
   already shipped once, a contrast fix landing in the media copy while
   the attribute copy kept the broken value, so the page was legible or
   not depending on how you arrived at it.

   Change one, change the other, and let the check confirm it.

   ── It was opt-in-only for a release, and why ───────────────────────
   The media query was removed and then restored. The first dark pass
   shipped it with 58 of the 89 tokens above still light-only, so every
   customer on a dark-set phone was handed a half-converted palette with
   no way out — `--paper` stayed near-white under near-white text and
   took every card label with it, and `--ink` was painted as a BACKGROUND
   in 40 places, which inverted the membership card and hid the labels
   written for it. It came back once every ground had a dark value, the
   hero family existed to hold the surfaces that must NOT move, and a
   person could choose: More -> Appearance, System / Light / Dark
   (partials/_appearance.html). A scheme that arrives uninvited needs an
   escape hatch, and there was not one — `data-appearance` was written by
   nothing in the tree.

   ── When it does come back, the guard is load-bearing ───────────────
   "System" writes NO attribute, so `prefers-color-scheme` decides. An
   explicit pick stamps the attribute and must win in BOTH directions:
   dark-when-system-says-light, and light-when-system-says-dark. Hence
   the `:not([data-appearance="light"])` guard the media block carried —
   with a bare `:root` the two rules tie on specificity and the media
   query wins on source order, stranding anyone who picked Light on a
   dark-set OS.

   ── --teal-pale is derived here, not listed ─────────────────────────
   In light it stays a literal per preset, because those are hand-picked
   12% tints and reproducing them exactly matters. In dark a 12% white
   tint is nonsense, and listing five more literals would leave a PAID
   brand_color — which is an arbitrary hex chosen by the org — with no
   dark pale at all. `color-mix` derives it from whatever --teal ends up
   being, so the presets, the paid colour and any future accent are all
   covered by one rule. Same reasoning as the app deriving `pale` rather
   than storing it.
   =================================================================== */

@media (prefers-color-scheme: dark) {
  :root:not([data-appearance="light"]) {
    --surface:        #13161F;
    --bg-subtle:      #0A0C12;
    /* The third rung, named. The app's dark language is exactly three
       grounds — bg #0A0C12, surface #13161F, surface-2 #1B1F2E — and the
       top one only existed here as --mist's value, which a portal sheet
       cannot point at: the four preset blocks in customer/_layout.css and
       contractor/_layout.css each pin --mist to a warm literal at the same
       specificity, so those sheets have to RE-declare it in their own dark
       block and need a name for what to re-declare it to. */
    --surface-2:      #1B1F2E;
    --mist:           var(--surface-2);
    --ink:            #EEF0F6;
    --text-primary:   var(--ink);
    --text-secondary: #8A90A6;
    --text-muted:     var(--text-secondary);
    --text-tertiary:  #7D8299;
    --border:         #2A3042;
    --white:          #13161F;

    /* ── The paper family, collapsed onto the three-step dark scale ─────
       In light these are eight grounds separated by WARMTH: the report's
       cool near-white, the contractor shell's warm linen, the sage one the
       invoice tables draw a rule on. That difference is a paper metaphor,
       and paper is a light-mode idea — the app's dark language has no warm
       and cool halves, it has bg / surface / surface-2 and nothing else.

       So they collapse by ROLE rather than by hue: a page ground becomes
       --bg-subtle, a card becomes --surface, a raised or inset panel
       becomes --mist, a hairline becomes --border. Pointing at the tokens
       rather than restating their literals is deliberate — those four are
       redeclared six lines above, a custom property resolves against the
       element it is used on, and this way the dark scale has exactly three
       values in it however many names reach for them. Adding a ninth paper
       needs no line here at all if it picks one of the four.

       Every one of these was light-only through the first pass, and each
       was a near-white ground with --ink text on it. That is the failure
       the whole sweep is about. */
    --paper-cool:      var(--bg-subtle);
    --paper-warm:      var(--bg-subtle);
    --paper-warm-deep: var(--surface);
    --paper-linen:     var(--surface);
    --paper-bone:      var(--surface);
    --paper-sage:      var(--surface);
    --paper-flat:      var(--mist);
    --mist-warm:       var(--mist);
    --paper-sage-line: var(--border);
    --paper-sand-line: var(--border);

    /* The two warning/accent chips that carry their own ground AND their
       own ink. Those pairs stayed legible through the first pass — pale
       fill, dark text, both light-only, internally consistent — so this is
       the only change here that is about looking right rather than being
       readable at all: a cream chip on a #13161F card is the one thing on
       the page still pretending it is daytime. Same 15%-of-the-hue tint
       the status fills use, over the card rather than over nothing. */
    --amber-ink:   var(--warning);
    --amber-tint:  color-mix(in srgb, var(--warning) 15%, transparent);
    --ember-paper: var(--surface);

    /* Two semantic colours that are painted as grounds and so have to
       move with the scheme: the affirmative switch, and the tier-2
       severity band. Both are lifted the same way --danger and
       --success are below — a saturated mid tone reads as mud on a
       #13161F card, and these two carry text. */
    --affirm:            #34D399;
    --severity-immediate: #F87171;
    --severity-monitor:   #FBBF24;
    --severity-warn-ink: #FB923C;
    --danger-brick:      #F87171;
    /* The password-strength meter's three bands, painted as grounds and
       so on the same footing as --danger and --success below: a
       mid-saturation band that reads clearly on white reads as mud on a
       #13161F card. They point at the semantic pair rather than carrying
       a fourth and fifth literal of the same two hues. */
    --strength-fair:   var(--warning);
    --strength-strong: var(--success);
    --danger-muted:    var(--danger);

    --danger:       #F87171;
    --danger-deep:  #FCA5A5;
    --success:      #34D399;
    --success-deep: #6EE7B7;
    --warning:      #FBBF24;
    --warning-deep: #FCD34D;

    --teal-pale: color-mix(in srgb, var(--teal) 18%, var(--surface));

    --status-new-ink:      #A78BFA;
    --status-accent-ink:   #FB923C;
    --status-wait-ink:     #FBBF24;
    --status-progress-ink: #22D3EE;
    --status-good-ink:     #34D399;
    --status-bad-ink:      #F87171;
    --status-idle-ink:     #94A3B8;

    /* 15% of the hue, as the app does it — a tint that works on any
       ground rather than a second set of literals. */
    --status-new-fill:      color-mix(in srgb, var(--status-new-ink) 15%, transparent);
    --status-accent-fill:   color-mix(in srgb, var(--status-accent-ink) 15%, transparent);
    --status-wait-fill:     color-mix(in srgb, var(--status-wait-ink) 15%, transparent);
    --status-progress-fill: color-mix(in srgb, var(--status-progress-ink) 15%, transparent);
    --status-good-fill:     color-mix(in srgb, var(--status-good-ink) 15%, transparent);
    --status-bad-fill:      color-mix(in srgb, var(--status-bad-ink) 15%, transparent);
    --status-idle-fill:     color-mix(in srgb, var(--status-idle-ink) 15%, transparent);

    color-scheme: dark;
  }
}

:root[data-appearance="dark"] {
  --surface:        #13161F;
  --bg-subtle:      #0A0C12;
  /* The third rung, named. The app's dark language is exactly three
     grounds — bg #0A0C12, surface #13161F, surface-2 #1B1F2E — and the
     top one only existed here as --mist's value, which a portal sheet
     cannot point at: the four preset blocks in customer/_layout.css and
     contractor/_layout.css each pin --mist to a warm literal at the same
     specificity, so those sheets have to RE-declare it in their own dark
     block and need a name for what to re-declare it to. */
  --surface-2:      #1B1F2E;
  --mist:           var(--surface-2);
  --ink:            #EEF0F6;
  --text-primary:   var(--ink);
  --text-secondary: #8A90A6;
  --text-muted:     var(--text-secondary);
  --text-tertiary:  #7D8299;
  --border:         #2A3042;
  --white:          #13161F;

  /* ── The paper family, collapsed onto the three-step dark scale ─────
     In light these are eight grounds separated by WARMTH: the report's
     cool near-white, the contractor shell's warm linen, the sage one the
     invoice tables draw a rule on. That difference is a paper metaphor,
     and paper is a light-mode idea — the app's dark language has no warm
     and cool halves, it has bg / surface / surface-2 and nothing else.

     So they collapse by ROLE rather than by hue: a page ground becomes
     --bg-subtle, a card becomes --surface, a raised or inset panel
     becomes --mist, a hairline becomes --border. Pointing at the tokens
     rather than restating their literals is deliberate — those four are
     redeclared six lines above, a custom property resolves against the
     element it is used on, and this way the dark scale has exactly three
     values in it however many names reach for them. Adding a ninth paper
     needs no line here at all if it picks one of the four.

     Every one of these was light-only through the first pass, and each
     was a near-white ground with --ink text on it. That is the failure
     the whole sweep is about. */
  --paper-cool:      var(--bg-subtle);
  --paper-warm:      var(--bg-subtle);
  --paper-warm-deep: var(--surface);
  --paper-linen:     var(--surface);
  --paper-bone:      var(--surface);
  --paper-sage:      var(--surface);
  --paper-flat:      var(--mist);
  --mist-warm:       var(--mist);
  --paper-sage-line: var(--border);
  --paper-sand-line: var(--border);

  /* The two warning/accent chips that carry their own ground AND their
     own ink. Those pairs stayed legible through the first pass — pale
     fill, dark text, both light-only, internally consistent — so this is
     the only change here that is about looking right rather than being
     readable at all: a cream chip on a #13161F card is the one thing on
     the page still pretending it is daytime. Same 15%-of-the-hue tint
     the status fills use, over the card rather than over nothing. */
  --amber-ink:   var(--warning);
  --amber-tint:  color-mix(in srgb, var(--warning) 15%, transparent);
  --ember-paper: var(--surface);

  /* Two semantic colours that are painted as grounds and so have to
     move with the scheme: the affirmative switch, and the tier-2
     severity band. Both are lifted the same way --danger and
     --success are below — a saturated mid tone reads as mud on a
     #13161F card, and these two carry text. */
  --affirm:            #34D399;
  --severity-immediate: #F87171;
  --severity-monitor:   #FBBF24;
  --severity-warn-ink: #FB923C;
  --danger-brick:      #F87171;
  /* The password-strength meter's three bands, painted as grounds and
     so on the same footing as --danger and --success below: a
     mid-saturation band that reads clearly on white reads as mud on a
     #13161F card. They point at the semantic pair rather than carrying
     a fourth and fifth literal of the same two hues. */
  --strength-fair:   var(--warning);
  --strength-strong: var(--success);
  --danger-muted:    var(--danger);

  --danger:       #F87171;
  --danger-deep:  #FCA5A5;
  --success:      #34D399;
  --success-deep: #6EE7B7;
  --warning:      #FBBF24;
  --warning-deep: #FCD34D;

  --teal-pale: color-mix(in srgb, var(--teal) 18%, var(--surface));

  --status-new-ink:      #A78BFA;
  --status-accent-ink:   #FB923C;
  --status-wait-ink:     #FBBF24;
  --status-progress-ink: #22D3EE;
  --status-good-ink:     #34D399;
  --status-bad-ink:      #F87171;
  --status-idle-ink:     #94A3B8;

  /* 15% of the hue, as the app does it — a tint that works on any
     ground rather than a second set of literals. */
  --status-new-fill:      color-mix(in srgb, var(--status-new-ink) 15%, transparent);
  --status-accent-fill:   color-mix(in srgb, var(--status-accent-ink) 15%, transparent);
  --status-wait-fill:     color-mix(in srgb, var(--status-wait-ink) 15%, transparent);
  --status-progress-fill: color-mix(in srgb, var(--status-progress-ink) 15%, transparent);
  --status-good-fill:     color-mix(in srgb, var(--status-good-ink) 15%, transparent);
  --status-bad-fill:      color-mix(in srgb, var(--status-bad-ink) 15%, transparent);
  --status-idle-fill:     color-mix(in srgb, var(--status-idle-ink) 15%, transparent);

  color-scheme: dark;
}
