/* Platform design tokens — the ONE source of truth for colour, type and
 * elevation across every app.
 *
 * WHY THIS FILE EXISTS
 * --------------------
 * Before it, each app's styles.css opened with its own near-identical
 * `:root` block, and the four had already diverged: hub/admin carried a
 * 13-token set, trash-analysis had added --teal-* / --amber-* / --bg, and
 * rent-analysis had bolted a whole second palette (--rd-navy, --rd-slate-*)
 * on top of the shared one. On top of that, 186 raw hex literals bypassed
 * tokens entirely, most of them just re-typing a value a token already
 * held (#6b7280 twelve times, #d1d5db seven, and so on).
 *
 * That is the same class of drift libs/frontend_common/platform.css was
 * created to end for the widget layer. This file does it for the palette,
 * and platform.css's docstring ("Deliberately NOT here: :root tokens")
 * has been updated to point here rather than to four app stylesheets.
 *
 * LOAD ORDER
 * ----------
 * Every app's index.html loads, in this order:
 *
 *     tokens.css      <- this file: :root variables + @font-face only
 *     platform.css    <- shared widget layer, consumes these tokens
 *     styles.css      <- the app's own layout, consumes these tokens
 *
 * tokens.css must come FIRST because both files below it resolve var()
 * against it. tests/js/test_shared_css_loaded.js gates this ordering, and
 * scripts/new_app.py's scaffold ships it so a future app cannot miss it.
 *
 * NO DARK MODE, DELIBERATELY
 * --------------------------
 * These are print-oriented compliance tools — rent rolls and vendor
 * invoices that get printed and circulated to people who never open the
 * app (see the @media print rules in platform.css and each app's
 * styles.css). A dark theme would have to be designed against those print
 * rules and against ~200 remaining hardcoded colours, and nobody has asked
 * for one. If it is ever wanted, the work is: add a
 * @media (prefers-color-scheme: dark) block HERE that redefines the
 * semantic tokens only — never the legacy aliases, and never in an app's
 * own stylesheet.
 */

/* ── Typeface: IBM Plex, self-hosted ───────────────────────────────────
 * Self-hosted rather than linked from a font CDN: the apps sit behind one
 * CloudFront distribution with a WAF and a strict same-origin posture, and
 * a third-party font host would be both a new external dependency on every
 * page load and a privacy leak of user IPs to that host. These are the
 * latin subset only (U+0000-00FF + punctuation), 116KB across seven files,
 * served from each app's own bucket by cdn_stack.py's frontend_common
 * source — no build step, no npm.
 *
 * IBM Plex is licensed under the SIL Open Font License 1.1, which permits
 * redistribution like this. See fonts/OFL.txt.
 *
 * Three families, three jobs, one type system — which also retires the
 * THREE different monospace stacks that had accumulated across the apps
 * ("SF Mono"/"Fira Code" in trash-analysis, ui-monospace in admin, bare
 * `monospace` elsewhere), so aligned numeric data now looks the same
 * everywhere it appears.
 */
@font-face {
  font-family: 'IBM Plex Sans';
  font-style: normal;
  font-weight: 400;
  font-display: swap;
  src: url('fonts/ibm-plex-sans-400.woff2') format('woff2');
}
@font-face {
  font-family: 'IBM Plex Sans';
  font-style: normal;
  font-weight: 600;
  font-display: swap;
  src: url('fonts/ibm-plex-sans-600.woff2') format('woff2');
}
@font-face {
  font-family: 'IBM Plex Sans';
  font-style: normal;
  font-weight: 700;
  font-display: swap;
  src: url('fonts/ibm-plex-sans-700.woff2') format('woff2');
}
@font-face {
  font-family: 'IBM Plex Serif';
  font-style: normal;
  font-weight: 600;
  font-display: swap;
  src: url('fonts/ibm-plex-serif-600.woff2') format('woff2');
}
@font-face {
  font-family: 'IBM Plex Serif';
  font-style: normal;
  font-weight: 700;
  font-display: swap;
  src: url('fonts/ibm-plex-serif-700.woff2') format('woff2');
}
@font-face {
  font-family: 'IBM Plex Mono';
  font-style: normal;
  font-weight: 400;
  font-display: swap;
  src: url('fonts/ibm-plex-mono-400.woff2') format('woff2');
}
@font-face {
  font-family: 'IBM Plex Mono';
  font-style: normal;
  font-weight: 500;
  font-display: swap;
  src: url('fonts/ibm-plex-mono-500.woff2') format('woff2');
}

:root {
  /* ── Type ───────────────────────────────────────────────────────────
   * Display is used with RESTRAINT: the wordmark, page/section headings,
   * and KPI numerals. Everything operational — body copy, labels, buttons,
   * table cells — is sans. Mono is for data that must align in a column
   * (ids, amounts, timestamps); pair it with font-variant-numeric:
   * tabular-nums, which --font-mono does not imply on its own.
   */
  --font-display: 'IBM Plex Serif', Georgia, 'Times New Roman', serif;
  --font-sans: 'IBM Plex Sans', -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
  --font-mono: 'IBM Plex Mono', ui-monospace, 'SF Mono', Menlo, monospace;

  /* ── Ground and ink ─────────────────────────────────────────────────
   * TREATMENT B -- a deep navy ground, replacing the warm-parchment
   * "Treatment A" Ellen Rourke chose on 2026-09-08 and confirmed twice that
   * day. That history is not erased because it is directly relevant to
   * why this entry exists and is dated, sourced and explicit, the same way
   * A's own adoption was:
   *
   * Dan, 2026-09-17, relaying Ellen: she liked the colour scheme in the
   * British Airways app (a real reference screenshot, not a verbal
   * description) and asked to match something like it across the
   * platform. Confirmed explicitly, twice, before any code changed: (1)
   * this means the platform APP itself, not a separate marketing site --
   * see the OLD version of this comment, still worth reading, about the
   * exact mistake of reading a colour comment about the wrong site as
   * licence to repaint this one; (2) this means the FULL ground repaint,
   * not an accent-only refresh -- explicitly costed first as "hours, real
   * design work, re-verify every contrast pair," not a quick pass, and
   * chosen anyway.
   *
   * What actually changed and what didn't: the ground inverts (parchment
   * -> navy, dark ink -> light ink) and the interactive colour moves from
   * a terracotta clay to a blue closer to the reference screenshot's own
   * primary button. Type, layout, --content-max, radii and the semantic
   * MEANING of every token (accent = actionable, navy = structure, danger
   * = bad) are untouched -- this is the same design system wearing a
   * different, client-requested palette, not a new one.
   *
   * `infra/cdk/scripts/browser_client_requirements_check.py`'s
   * `palette-still-matches-option-a` pinned --paper and --accent on the
   * deployed page specifically so a drift away from Ellen's OWN choice
   * would fail loudly rather than be argued about. That gate is updated to
   * pin THIS palette now, for the identical reason -- it exists to catch
   * an accidental reversion to the old parchment values (or a future
   * repaint on a misread comment, the exact failure this section's own
   * history describes), not to freeze the palette forever. See that
   * script's own comment for the date this changed and why.
   *
   * A real, measured risk this repaint carries that Option A never did:
   * these are PRINT-ORIENTED compliance tools (rent rolls, vendor
   * invoices, circulated to people who never open the app), and
   * `rent-analysis`'s own print stylesheet forces
   * `print-color-adjust: exact` on every element specifically so coloured
   * badges reproduce faithfully -- which up to now was safe only because
   * the on-screen palette was ALREADY print-safe (dark text, light
   * ground). A dark ground is not. See the `@media print` block near the
   * end of this file, which resets every token back to paper-and-ink for
   * printing regardless of what's on screen -- required now, not merely
   * possible, precisely because print-color-adjust:exact means whatever
   * these tokens resolve to is what a printer actually receives.
   *
   * The ink ramp is four steps, and every one of them clears WCAG AA
   * (4.5:1) on BOTH --paper and --surface, computed against the real sRGB
   * values below (relative luminance per WCAG 2.x), not eyeballed --
   * the same discipline Option A's own ramp was held to, and the same
   * reason its own comment recorded two colours that shipped failing that
   * floor (2.54:1, 3.05:1) before being fixed.
   */
  --paper: #0F1E3A;          /* page background */
  --surface: #17294C;        /* cards, panels, table backgrounds */
  --surface-sunken: #0A1628; /* subtle fills, hover rows, inset wells */

  --ink: #F5F7FB;            /* headings, strong emphasis     15.5:1 on paper, 13.4:1 on surface */
  --ink-body: #CBD4E6;       /* body copy                     11.1:1 on paper,  9.7:1 on surface */
  --ink-soft: #93A3C4;       /* secondary text, labels          6.5:1 on paper,  5.7:1 on surface */
  --ink-faint: #8FA0C8;      /* muted/meta text — the floor     6.3:1 on paper,  5.5:1 on surface */

  --line: #26395F;           /* hairlines, card borders, dividers */
  --line-strong: #3A5081;    /* emphasized dividers, table rules */

  /* ── Accent: Blue ───────────────────────────────────────────────────
   * The one colour allowed to be loud, and the only one used for "this is
   * actionable": links, primary buttons, focus rings, active tabs,
   * selected state. Spend boldness here and nowhere else -- unchanged from
   * Option A's own rule, only the hue moved, matching the reference
   * screenshot's own primary-button blue rather than Option A's terracotta.
   *
   * MEASURED, not the obvious middle-ground guess: a saturated blue light
   * enough to read as TEXT on this dark ground (>=4.5:1) is, by the same
   * luminance math, too light for WHITE text to read AT 4.5:1 if used as a
   * button FILL — the two roles pull in opposite directions once the
   * ground is dark, which Option A's mid-toned clay never had to resolve
   * (white read fine on it either way). This value is chosen so ONE token
   * can serve as bordered/faint fills without ANY hardcoded blue elsewhere.
   * Every call site whose FILL still had `color: var(--white)` on it
   * (`.btn-primary`, `.btn-success`, `.btn-contract`, `.acct-btn.active` —
   * grepped for the actual pairing, not guessed) has that text colour
   * changed to `var(--paper)` alongside this file, for exactly that reason.
   *
   * It does not compete with --navy below because they are different
   * LIGHTNESSES of the same hue family now, not different hues the way
   * accent/navy were under Option A — navy is furniture (the ground and
   * its chrome), this is the thing you can click, and the two are kept
   * apart by contrast against the ground rather than by hue difference.
   */
  --accent: #6E97D6;         /* 5.6:1 on paper, 4.8:1 on surface, 5.6:1 as --paper-on-fill */
  --accent-hover: #8AAEE3;   /* LIGHTER than --accent, deliberately — the reverse of Option A's
                                 darken-on-hover, because hover states on a dark ground read as
                                 activated by getting brighter, not by getting darker toward it */
  --accent-soft: #182848;    /* tinted background for accent-marked blocks; 4.9:1 for --accent text on it */
  /* The readable text colour for content sitting ON a solid --accent (or
   * --success) FILL — `.btn-primary`, `.btn-success`, `.btn-contract`,
   * `.acct-btn.active`. Equals --paper on purpose, not a coincidence: it
   * resolves dark on screen (this ground is dark) and white in print
   * (the @media print block resets --paper to white), which is exactly
   * right in both cases without either mode needing its own rule. Named
   * separately from --paper anyway because "button text = page
   * background" reads as a mistake at the call site otherwise. */
  --accent-ink: var(--paper);

  /* ── Structure: Navy ────────────────────────────────────────────────
   * Equal-weight structural partner to the accent, not a second accent. It
   * carries chrome — the app header, KPI accent bars, chart axes — so the
   * accent never has to compete with the furniture. Deliberately a
   * DIFFERENT, richer navy than --paper (not merely --surface-sunken
   * reused) so header chrome still reads as its own structural layer
   * against the page body, the same distinction Option A's navy-on-parchment
   * made, just inverted. This is also where the platform's previous
   * --blue-900 identity survives: the change is meant to read as the same
   * product wearing a different palette, not a new one.
   */
  --navy: #0A1830;           /* header ground; 17.7:1 against white text */

  /* ── Layout ─────────────────────────────────────────────────────────
   * How wide the content column may get, for every app.
   *
   * Kevin Rourke, 2026-09-08: "make it bigger and make it fill up the page
   * more... I see a lot of white space to the side." Measured that day:
   * insurance capped at 980px in a 1378px viewport -- 358px of gutter, 26%
   * of the window -- while the shared footer capped at 1180px, so the FOOTER
   * rendered wider than the content above it. hub and admin were narrower
   * still at 900px, and rent-analysis and trash-analysis each carried their
   * own 1400px. Five answers to one question.
   *
   * 1600px, not 100%: on the ~1378-1440px laptops they actually use this is
   * full width, which is what was asked for, while a 2560px monitor still
   * gets a readable measure instead of a 300-character line. If they want it
   * edge-to-edge everywhere, this is the one line to change.
   *
   * OVERRIDDEN 2026-09-17, Dan, explicit go-ahead after being told this
   * reopens Kevin's request above: "lots of whitespace... Ellen's app was
   * small and compact... everything looks super wide" -- matched to her own
   * original app's `main { max-width:1180px }` exactly (nha-insurance-apps),
   * not picked freehand. This is a real, acknowledged reversal of the
   * 2026-09-08 decision, not an unawares one -- if width complaints come
   * back from Kevin's side, that history is still live above, not erased.
   */
  --content-max: 1180px;

  --navy-light: #1D3358;     /* the header gradient's lighter stop */
  --navy-soft: #17253F;      /* was a light tint on Option A; now a dark one, same structural role */

  /* ── Semantic state ─────────────────────────────────────────────────
   * Separate from the accent on purpose: --accent means "you can act on
   * this", these mean "this is the state of the data". A KPI that is
   * merely important is navy; a KPI that is BAD is danger. Every one of
   * these clears 4.5:1 as TEXT on both --paper and --surface — the same
   * floor Option A's own comment held these to, computed the same way.
   *
   * NOT "carries white text at 4.5:1 when used as a fill" any more, and
   * that line is deliberately removed rather than kept: a colour light
   * enough to pass as text on a dark ground cannot also be dark enough for
   * white text to pass on it as a fill (the same trade-off --accent's own
   * comment explains at length). The few call sites that filled with one of
   * these and set white text (`.btn-success`, `.btn-contract` — grepped,
   * not guessed) now set `color: var(--paper)` instead, alongside this file.
   */
  --success: #5FBE7A;        /* 7.2:1 on paper, 6.3:1 on surface */
  --success-hover: #7CCB92;  /* lighter on hover, same reasoning as --accent-hover */
  --success-soft: #132A1C;
  --warn: #E8B44A;           /* 8.7:1 on paper, 7.6:1 on surface */
  --warn-soft: #2E2410;
  --danger: #EC7284;         /* 5.8:1 on paper, 5.0:1 on surface */
  --danger-soft: #2E1620;
  /* The border for a soft-ground danger panel. --danger at full strength
     was too loud as a 1px rule around a tinted block; this is the tint's
     own lighter edge (inverted from Option A's darker edge, same idea:
     one step of contrast in from the tint, toward the strong colour). */
  --danger-border: #5A2430;
  /* A second, DARKER red for the one real exception to "these are text
   * tokens now": `.scope-alert`, the wrong-org warning banner
   * ("loud on purpose — the alternative is silently showing somebody a
   * different organization's figures"), fills solid AND pairs a literal
   * white pill-button against that fill — a white background regardless
   * of theme, so --danger itself being light-for-dark-ground text doesn't
   * help here either direction. Unchanged from Option A's own --danger
   * value: this component's white pairing was already correct, and stays
   * correct — verified again against the new component, not assumed. */
  --danger-strong: #B23B34;

  /* ── Categorical ────────────────────────────────────────────────────
   * For encoding WHICH, never HOW BAD. Housing program codes (PBV, PBRA,
   * LIHTC), unit statuses, vendor types, chart series — things that are
   * merely different from each other, with no ordering and no severity.
   *
   * These exist because collapsing them into the accent would have
   * destroyed real information: rent-analysis carried .badge-blue/-pink/
   * -peach/-orange and .program-PBV as raw hexes, and those distinctions
   * are what let someone scan a rent roll and see programme mix at a
   * glance. One set, chosen together, lightened as a set for the dark
   * ground rather than re-picked individually — the six keep the same
   * relative relationships to each other Option A's set had.
   *
   * Every pairing clears WCAG AA both ways: --cat-N as text on
   * --cat-N-soft (>=4.5:1, measured) and --cat-N as text on --paper
   * (>=4.5:1, measured). "White on --cat-N" is dropped from this
   * requirement for the same reason it is for the semantic states above —
   * these are text-on-soft-tint tokens in every real call site (grepped:
   * `background:-soft` paired with `color:` the strong value), never a
   * white-text fill, so holding them to a role nothing uses would only
   * force needlessly dark, muddy categoricals for no real call site.
   */
  --cat-teal: #5FBFC7;    --cat-teal-soft: #132E38;
  --cat-indigo: #8FA3E8;  --cat-indigo-soft: #1B2647;
  --cat-plum: #D690D0;    --cat-plum-soft: #2A1E38;
  --cat-olive: #B7C86A;   --cat-olive-soft: #232B18;
  --cat-rust: #E29A6B;    --cat-rust-soft: #2E2015;
  --cat-slate: #A7B4CC;   --cat-slate-soft: #1C2436;

  /* --alt predates the set above and is kept as the name trash-analysis's
   * --teal-* alias resolves through. It is simply the first categorical. */
  --alt: var(--cat-teal);
  --alt-soft: var(--cat-teal-soft);

  /* ── Elevation and shape ────────────────────────────────────────────
   * A pure-black shadow reads as genuine depth against a dark navy ground
   * (navy is lighter than black), unlike Option A's warm-tinted one, which
   * existed specifically because neutral black read as grey haze on
   * parchment — the opposite problem. Higher opacity than Option A's,
   * because a dark-on-dark shadow needs more of it to register at all.
   */
  --shadow-sm: 0 1px 2px rgba(0,0,0,.35), 0 1px 3px rgba(0,0,0,.30);
  --shadow: 0 2px 6px rgba(0,0,0,.35), 0 4px 12px rgba(0,0,0,.30);
  --shadow-lg: 0 4px 12px rgba(0,0,0,.40), 0 12px 32px rgba(0,0,0,.35);
  --radius: 12px;
  --radius-sm: 8px;
  --radius-xs: 6px;

  /* Focus ring, defined once. Keyboard focus was previously either the
   * browser default or, worse, `outline: none` with only a border-colour
   * change to replace it (see the old .modal-input:focus). rgba of the new
   * --accent's own RGB (110,151,214), same alpha Option A used. */
  --focus-ring: 0 0 0 3px rgba(110,151,214,.35);

  /* ── Legacy aliases ─────────────────────────────────────────────────
   * The previous token names, remapped onto the palette above. There are
   * ~228 var(--blue-*)/var(--gray-*) call sites across the four apps and
   * platform.css; aliasing them means the whole platform picks up the new
   * palette coherently in one step, instead of a half-migrated UI where
   * converted rules look new and unconverted ones look old.
   *
   * These are a MIGRATION SHIM, not part of the design system. Do not use
   * them in new code — use the semantic tokens above. They can be deleted
   * once no stylesheet references them; scripts/check_local.sh does not
   * enforce that yet because the remaining call sites are still being
   * converted app by app.
   *
   * The mapping that matters most: --blue-500 was the interactive accent
   * (links, primary buttons, focus, tile titles), so it becomes --accent.
   * --blue-900/-700 were chrome (the header gradient), so they become
   * navy. Getting those two backwards would put the loud colour on the
   * furniture and the quiet colour on the buttons.
   */
  --blue-900: var(--navy);
  --blue-700: var(--navy-light);
  --blue-500: var(--accent);
  --blue-300: #4A6FA5;       /* a muted, deeper blue for borders/outlines — distinct from --accent
                                 itself so a dashed border doesn't compete with real button/link colour */
  --blue-100: var(--accent-soft);

  --gray-900: var(--ink);
  --gray-700: var(--ink-body);
  --gray-500: var(--ink-soft);
  --gray-400: var(--ink-faint);
  --gray-300: var(--line);
  --gray-100: var(--surface-sunken);
  --white: #FFFFFF;

  --red-500: var(--danger);
  --red-100: var(--danger-soft);
  --green-500: var(--success);
  --green-100: var(--success-soft);
  --amber-500: var(--warn);
  --amber-100: var(--warn-soft);
  --teal-500: var(--alt);
  --teal-100: var(--alt-soft);

  /* trash-analysis's page-background token. */
  --bg: var(--paper);
}

/* ── Print: always light, regardless of the on-screen theme ────────────
 * These are print-oriented compliance tools — rent rolls and vendor
 * invoices that get printed and circulated to people who never open the
 * app — and `rent-analysis`'s own print stylesheet forces
 * `print-color-adjust: exact` on every element specifically so coloured
 * badges reproduce faithfully on paper. That was always safe under
 * Option A's warm-parchment ground (dark text, light background — already
 * what a printer wants) without this block existing at all. It is not
 * safe under a dark navy ground: without this override, a printed rent
 * roll would be a near-black page with pale text on it, or — on a printer
 * that drops background colours despite the exact-adjust hint — WHITE
 * TEXT ON WHITE PAPER, invisible.
 *
 * The fix is the same mechanism this file's own "NO DARK MODE,
 * DELIBERATELY" note already describes for a hypothetical future dark
 * MODE — a media-query block that redefines the semantic tokens only,
 * here keyed on `print` instead of `prefers-color-scheme: dark`. Every
 * component that reads `var(--surface)`/`var(--ink)`/etc. becomes
 * print-safe automatically, with no per-component print rule needed,
 * because the token IT resolves to changes, not the component.
 *
 * Deliberately close to Option A's OWN values, not a fresh design: this
 * is the ground truth for what "safe to print" already looked like on
 * this platform, kept rather than re-derived, and every value here still
 * clears the same 4.5:1 floor against --paper and --surface — verified
 * the same way as the on-screen ramp above, not assumed to carry over
 * just because the on-screen ramp does.
 */
@media print {
  :root {
    --paper: #FFFFFF;
    --surface: #FFFFFF;
    --surface-sunken: #F2F0EB;

    --ink: #211D17;
    --ink-body: #3E382E;
    --ink-soft: #6B6355;
    --ink-faint: #766D5E;

    --line: #E3DDCE;
    --line-strong: #CFC6B2;

    --accent: #B0451F;
    --accent-hover: #963A18;
    --accent-soft: #EFDFD3;

    --navy: #1B2B40;
    --navy-light: #2C425E;
    --navy-soft: #EBEEF1;

    --success: #387046;
    --success-hover: #2F6039;
    --success-soft: #DCEBDF;
    --warn: #8F5C0E;
    --warn-soft: #F7ECD5;
    --danger: #B23B34;
    --danger-soft: #F7E3E1;
    --danger-border: #E9BDB9;

    --cat-teal: #2F6F7B;    --cat-teal-soft: #E6EDEF;
    --cat-indigo: #3B4E8F;  --cat-indigo-soft: #E7E9F1;
    --cat-plum: #7A3E76;    --cat-plum-soft: #EFE7EE;
    --cat-olive: #56651F;   --cat-olive-soft: #EAECE4;
    --cat-rust: #8F4718;    --cat-rust-soft: #F1E8E3;
    --cat-slate: #48586B;   --cat-slate-soft: #E9EAED;
  }
}
