/* Design tokens - single source for every color/shadow/overlay token used
   across dashboard.html, help.html, and the auth pages (main.py). Two
   first-class palettes selected via [data-theme] on <html>, set by a small
   blocking init script in each page's <head> (localStorage override, else
   prefers-color-scheme) - see THEME_INIT_JS. No @media queries live here;
   the JS always resolves to an explicit "light" or "dark" before this
   stylesheet's selectors ever need to match, so the cascade stays flat.

   All light/dark pairs below are WCAG AA contrast-checked (4.5:1 text) -
   see the mission's contrast report for the full ratio table. --muted and
   the heat-tile gain/loss text colors were tuned during that pass; every
   accent/warn/danger/success value was already AA-safe and is carried
   over unchanged from the pre-token-system palette.

   Mission UI-1 (2026-07-28): dark mode's --success/--danger, the heat-
   ramp gain/loss text, and the categorical --pal-* segment colors were
   already comfortably AA-safe by ratio alone, but were pale/low-saturation
   pastels that read as visually dim next to the light palette's richer
   equivalents and made adjacent donut/treemap segments hard to tell
   apart. Re-tuned toward the same hues at higher saturation and slightly
   lower lightness - still checked against every relevant background
   (--bg, --surface, and each token's own -bg/tile variant), not just
   re-picked by eye.

   Mission D1 (2026-08-04): a second, --ds-*-namespaced token set was
   added below (color/heat/typography/spacing/radius/motion/font) as
   phase 1 of a new design system - deliberately not consumed by any
   selector in this app yet, and deliberately namespaced rather than
   reusing --bg/--muted/--accent/etc, which already exist above with
   different values. Nothing visual changes until a later phase migrates
   real rules onto these tokens. */

:root[data-theme="light"] {
  /* warm-neutral paper */
  --bg: #F7F6F3;
  --surface: #FFFFFF;
  --ink: #1F2124;
  --muted: #6B6660;
  --hairline: #E8E6E1;
  --hairline-width: 0.5px;
  --accent: #2F4BC7;
  --accent-bg: #E9EDFB;
  --warn: #854F0B;
  --warn-bg: #FAEEDA;
  --danger: #A32D2D;
  --danger-bg: #FCEBEB;
  --success: #3B6D11;
  --success-bg: #EAF3DE;

  /* ---- Mission D1, design system phase 1 - --ds-* namespace ----
     A second, not-yet-consumed palette living alongside the tokens
     above. Deliberately prefixed --ds- rather than reusing --bg/
     --muted/--accent/etc: those names already exist above with
     DIFFERENT values (e.g. --accent here is #2F4BC7, --ds-color-accent
     below is #3857D6) - reusing a bare name would let the later
     declaration silently win the cascade, a real visual change with
     no corresponding UI change to explain it. Nothing in dashboard.html/
     help.html/the auth pages reads any --ds-* token yet; that migration
     is a later phase. */
  --ds-color-bg: #F7F6F3;
  --ds-color-surface: #FFFFFF;
  --ds-color-elevated: #FCFCFA;
  --ds-color-border: #E7E4DE;
  --ds-color-divider: #ECE8E2;
  --ds-color-text-primary: #171717;
  --ds-color-text-secondary: #555555;
  --ds-color-text-muted: #8A8A8A;
  --ds-color-accent: #3857D6;
  /* Mission D3.5: success/danger/warning darkened from their original
     Mission D1 values - see the contrast-check comment below (they were
     fine against --ds-color-bg/-surface, which is all Mission D1 ever
     checked, but hadn't been tested against the new chip background
     tokens introduced here until now). Same hue in all three cases. */
  --ds-color-success: #117E4B;
  --ds-color-danger: #BF3F39;
  --ds-color-warning: #9C6300;

  /* Mission D3.5: semantic background tokens for chips/tags/badges and
     small notification banners ONLY - never cards, lists, dialogs,
     sheets, page backgrounds, treemap tiles, heatmaps, or charts (those
     already have their own background story: --ds-color-surface/
     -elevated, the heat scale below, --pal-* below that). Phase 3 found
     27 such backgrounds with no --ds-* equivalent; these are the tokens
     for them - declared, not consumed by any selector yet, same state
     the rest of this namespace started in (see the Mission D1 comment
     above). Phase 4 consumes them when chips are consolidated.

     Contrast-checked against their intended foreground before adding,
     not assumed safe by inheriting from an already-passing pair
     elsewhere - the heat scale's own dark-mode corrections below are
     proof that assumption fails often enough to be worth checking every
     time. 3 of these 5 light pairs failed AA (4.5:1 normal text) as
     originally specified; foreground darkened, same hue, background
     untouched in all 3 (a light background needs a DARKER foreground
     for more contrast, not lighter - the dark theme block below has one
     failing pair too, and it needed the opposite direction):
       success-bg/success: #138A52 on #EAF7EE was 3.98:1 -> darkened to #117E4B (4.61:1)
       danger-bg/danger:   #C4413A on #FCEDEC was 4.43:1 -> darkened to #BF3F39 (4.61:1)
       warning-bg/warning: #B87400 on #FFF5DF was 3.50:1 -> darkened to #9C6300 (4.62:1)
     neutral-bg/text-primary (16.29:1) and info-bg/accent (5.36:1)
     passed as originally specified, unchanged. */
  --ds-color-neutral-bg: #F3F4F6;
  --ds-color-success-bg: #EAF7EE;
  --ds-color-danger-bg: #FCEDEC;
  --ds-color-warning-bg: #FFF5DF;
  --ds-color-info-bg: #EAF3FF;

  /* Mission D3: the overlay surface (bottom sheets/dialogs/modals/
     popovers only - never cards, lists, page backgrounds or persistent
     content) and the scrim behind it (a dimming layer, not the overlay
     surface itself). Distinct from --ds-color-surface on purpose: an
     overlay floats above the whole page including cards, so it needs
     its own token even though light mode's value (#FFFFFF) happens to
     match surface's - dark mode's does not (#1D2026 vs #20242A). */
  --ds-color-overlay: #FFFFFF;
  --ds-scrim: rgba(17, 24, 39, 0.12);

  /* Heat scale: background tints only in light mode - text on top of
     these is --ds-color-text-primary/-secondary, not a separate token
     (see the dark block below for why dark mode needs one). */
  --ds-heat-pos-1-bg: #EEF8F1;
  --ds-heat-pos-3-bg: #D8F0DF;
  --ds-heat-pos-5-bg: #BCE7CA;
  --ds-heat-pos-10-bg: #8FD8AB;
  --ds-heat-pos-20-bg: #56BE83;
  --ds-heat-neg-1-bg: #FCF1F0;
  --ds-heat-neg-3-bg: #F7DDDB;
  --ds-heat-neg-5-bg: #F0C0BC;
  --ds-heat-neg-10-bg: #E4938B;
  --ds-heat-neg-20-bg: #D86057;

  /* heat ramp (treemap/movers/day-change) - 4 steps, <1% / <2.5% / <5% / >=5% */
  --heat-gain-0: #EAF3DE; --heat-gain-1: #C0DD97; --heat-gain-2: #97C459; --heat-gain-3: #639922;
  --heat-loss-0: #FCEBEB; --heat-loss-1: #F7C1C1; --heat-loss-2: #F09595; --heat-loss-3: #E24B4A;
  --heat-gain-text: #0D2400;
  --heat-loss-text: #280505;
  --heat-neutral-bg: #EFEEEA;
  --heat-neutral-text: #2C2C2A;

  /* Orbit landing view (2026-09): tokens named by ROLE (centre/satellite),
     not by fixed appearance - dark mode inverts which gradient each role
     gets (real finding, checked visually: dark satellites on a dark
     backdrop barely read), so "pale" and "charcoal" are assigned per
     theme below rather than one role always meaning one look. Light
     theme: pale centre, charcoal satellites (satellites read clearly
     against this light backdrop as-is). Dominance between the two is
     size, not brightness - see ORBIT_SLOTS/centre sizing in
     dashboard.html - so this pairing works in both directions. */
  --orbit-centre-1: #FFFFFF; --orbit-centre-2: #F2F0EA; --orbit-centre-3: #DCD7CD;
  --orbit-centre-text: #16181D;
  --orbit-centre-rim: rgba(0,0,0,0.06);
  --orbit-satellite-1: #45484F; --orbit-satellite-2: #1D1F25; --orbit-satellite-3: #090A0D;
  --orbit-satellite-text: #FFFFFF;
  --orbit-satellite-rim: rgba(255,255,255,0.14);
  --orbit-dust: rgba(26,26,26,0.4);
  --orbit-shadow: rgba(31,33,36,.35);

  /* categorical (sector donut/legend) */
  --pal-0: #5B8DEF; --pal-1: #63A375; --pal-2: #E0A458; --pal-3: #C76B6B;
  --pal-4: #8B7BC7; --pal-5: #4FADAD; --pal-6: #D98CB3; --pal-7: #A3A857;
  --pal-8: #C78F5B; --pal-9: #7A9CC6; --pal-10: #B56576; --pal-11: #6B8E23;

  --overlay-scrim: rgba(31,33,36,.62);
  --shadow: rgba(31,33,36,.18);
  --shadow-strong: rgba(31,33,36,.28);
}

:root[data-theme="dark"] {
  /* charcoal terminal */
  --bg: #16181D;
  --surface: #1E2128;
  --ink: #E8EAED;
  --muted: #8B92A0;
  --hairline: #2A2E37;
  --hairline-width: 0.5px;
  --accent: #9AB0F5;
  --accent-bg: #232C45;
  --warn: #FAC775;
  --warn-bg: #3A2C12;
  /* Mission UI-1: raised from the original #F09595/#C0DD97 - those passed
     WCAG AA by a wide margin (contrast ratio alone was never the problem)
     but were pale, low/mid-saturation pastels that read as washed-out
     next to the light palette's much richer equivalents. Lower lightness
     + higher saturation, same hue family - still comfortably AA (5.7:1+
     against --bg/--surface/their own -bg, verified against all three,
     not just one). */
  --danger: #EE7B77;
  --danger-bg: #3D1B1B;
  --success: #88D85A;
  --success-bg: #24330F;

  /* ---- Mission D1, design system phase 1 - --ds-* namespace ----
     Same not-yet-consumed second palette as the light block above. */
  --ds-color-bg: #16181D;
  --ds-color-surface: #20242A;
  --ds-color-elevated: #272C33;
  --ds-color-border: #31363D;
  --ds-color-divider: #393E45;
  --ds-color-text-primary: #F2F2EF;
  --ds-color-text-secondary: #C3C5C8;
  --ds-color-text-muted: #8D9198;
  --ds-color-accent: #8EA8FF;
  --ds-color-success: #4FCB83;
  /* Mission D3.5: danger lightened from its original Mission D1 value -
     see the contrast-check comment below. Same hue; success/warning
     passed as originally specified and are unchanged. */
  --ds-color-danger: #F1746D;
  --ds-color-warning: #E0B341;

  /* Mission D3.5: same chip/tag/badge/banner-only scope as the light
     block above - never cards, lists, dialogs, sheets, page backgrounds,
     treemap tiles, heatmaps, or charts. Not consumed by any selector yet
     (Phase 4 consumes them when chips are consolidated).

     1 of these 5 dark pairs failed AA (4.5:1) as originally specified -
     danger reused #472929 from the heat scale's own neg-20 background,
     but paired with --ds-color-danger rather than the heat scale's own
     corrected text (#DA817C), so it's a genuinely different pair from
     the one already fixed there and needed checking on its own:
       danger-bg/danger: #F06A63 on #472929 was 4.30:1 -> lightened to #F1746D (4.61:1)
     neutral-bg/text-primary (12.30:1), success-bg/success (6.01:1),
     warning-bg/warning (5.56:1), and info-bg/accent (5.79:1) all passed
     as originally specified, unchanged. */
  --ds-color-neutral-bg: #2A2D33;
  --ds-color-success-bg: #1F3A2A;
  --ds-color-danger-bg: #472929;
  --ds-color-warning-bg: #493B1A;
  --ds-color-info-bg: #1E3148;

  /* Mission D3: same overlay/scrim pair as light - see that block's
     comment. Dark's overlay (#1D2026) is deliberately distinct from
     surface (#20242A), unlike light where they happen to coincide. */
  --ds-color-overlay: #1D2026;
  --ds-scrim: rgba(0, 0, 0, 0.48);

  /* Heat scale: dark mode pairs a background AND text token per band -
     a darker fill alone goes muddy on a dark background, so the text
     color has to carry the semantic (gain/loss) weight instead. Every
     pair below is contrast-checked; 3 of the 10 as originally specified
     failed WCAG AA for normal text (4.5:1) - text lightened, same hue,
     background untouched in all 3 cases, per mission instruction to
     never silently ship a failing pair:
       pos-20: #49B96D on #2A4433 was 4.28:1 -> text raised to #57BE78 (4.60:1)
       neg-10: #D95A54 on #3D2525 was 3.72:1 -> text raised to #DF746F (4.60:1)
       neg-20: #CB4C45 on #472929 was 2.88:1 (worst) -> text raised to #DA817C (4.60:1)
     The other 7 dark pairs (and all 10 light pairs) passed as given. */
  --ds-heat-pos-1-bg: #1C2420;   --ds-heat-pos-1-text: #7BDFA8;
  --ds-heat-pos-3-bg: #1E2A22;   --ds-heat-pos-3-text: #74D99F;
  --ds-heat-pos-5-bg: #223126;   --ds-heat-pos-5-text: #68D08F;
  --ds-heat-pos-10-bg: #263A2C;  --ds-heat-pos-10-text: #59C37E;
  --ds-heat-pos-20-bg: #2A4433;  --ds-heat-pos-20-text: #57BE78;
  --ds-heat-neg-1-bg: #271E1E;   --ds-heat-neg-1-text: #F08B86;
  --ds-heat-neg-3-bg: #2D2020;   --ds-heat-neg-3-text: #EA7D77;
  --ds-heat-neg-5-bg: #352323;   --ds-heat-neg-5-text: #E36C65;
  --ds-heat-neg-10-bg: #3D2525;  --ds-heat-neg-10-text: #DF746F;
  --ds-heat-neg-20-bg: #472929;  --ds-heat-neg-20-text: #DA817C;

  /* heat ramp - desaturated moss/brick tints, not the light palette's
     bright pastels (which vibrate against charcoal). Text colors are a
     separate light tint per direction, not the light-mode dark text -
     dark tiles need light text, not the same near-black used on light
     tiles. Mission UI-1: gain/loss text brightened the same way as
     --success/--danger above (more saturated, less pale) - checked
     against the most saturated tile in each ramp (heat-gain-3/heat-loss-3),
     the tightest case, still 5.1:1+ there. */
  --heat-gain-0: #212922; --heat-gain-1: #243A28; --heat-gain-2: #2C3B2E; --heat-gain-3: #35502F;
  --heat-loss-0: #2A2224; --heat-loss-1: #3A272A; --heat-loss-2: #4A2F31; --heat-loss-3: #5C3438;
  --heat-gain-text: #9EDA81;
  --heat-loss-text: #EEA2A0;
  --heat-neutral-bg: #262A31;
  --heat-neutral-text: #C9CDD3;

  /* Orbit landing view - inverted from light mode on purpose: dark
     satellites on a dark backdrop barely read (checked visually against
     the piece-1 preview), so dark mode gives satellites the pale gradient
     and the centre the charcoal one - centre stays unambiguously the
     dominant sphere on size alone (dashboard.html), not brightness. */
  --orbit-centre-1: #383B42; --orbit-centre-2: #202329; --orbit-centre-3: #0A0B0E;
  --orbit-centre-text: #F2F2EF;
  --orbit-centre-rim: rgba(255,255,255,0.10);
  --orbit-satellite-1: #FFFFFF; --orbit-satellite-2: #F2F0EA; --orbit-satellite-3: #DCD7CD;
  --orbit-satellite-text: #16181D;
  --orbit-satellite-rim: rgba(0,0,0,0.08);
  --orbit-dust: rgba(232,234,237,0.32);
  --orbit-shadow: rgba(0,0,0,.5);

  /* Mission UI-1: same hue rotation as before, saturation raised and
     lightness lowered a touch across the board (was ~45% sat/~70% light,
     now ~55-78% sat/~58-68% light) - segments were too dim and adjacent
     ones too close in lightness to tell apart on the donut/treemap. */
  --pal-0: #5B8BEB; --pal-1: #59CF85; --pal-2: #E9A64A; --pal-3: #E15050;
  --pal-4: #8365DD; --pal-5: #4FD8D8; --pal-6: #EA71AD; --pal-7: #CED553;
  --pal-8: #E39345; --pal-9: #6098E2; --pal-10: #D35579; --pal-11: #ADD751;

  --overlay-scrim: rgba(0,0,0,.72);
  --shadow: rgba(0,0,0,.4);
  --shadow-strong: rgba(0,0,0,.55);
}

/* ---- Mission D1, design system phase 1 - theme-independent tokens ----
   Typography, spacing, radius, motion, and font-loading tokens. These
   don't vary by [data-theme], so they live on a plain :root rather than
   inside either theme block above. Not yet applied to any element -
   see the @font-face block below and its own comment for why. */
:root {
  /* Typography: size/weight/line-height kept as separate scalars per
     role (not bundled into a single `font` shorthand string) so any one
     of the three can be read or overridden independently later. Weights
     450 and 650 are not standard static cuts - see the @font-face
     comment below for why this only works with a variable font. */
  --ds-type-hero-metric-size: 40px;   --ds-type-hero-metric-weight: 700; --ds-type-hero-metric-line: 44px;
  --ds-type-hero-label-size: 14px;    --ds-type-hero-label-weight: 500;  --ds-type-hero-label-line: 18px;
  --ds-type-section-heading-size: 24px; --ds-type-section-heading-weight: 650; --ds-type-section-heading-line: 30px;
  --ds-type-card-heading-size: 18px;  --ds-type-card-heading-weight: 600; --ds-type-card-heading-line: 24px;
  --ds-type-body-size: 16px;          --ds-type-body-weight: 450;        --ds-type-body-line: 24px;
  --ds-type-supporting-size: 14px;    --ds-type-supporting-weight: 450;  --ds-type-supporting-line: 20px;
  --ds-type-caption-size: 12px;       --ds-type-caption-weight: 500;     --ds-type-caption-line: 16px;
  --ds-type-tiny-label-size: 11px;    --ds-type-tiny-label-weight: 600;  --ds-type-tiny-label-line: 14px;

  /* Spacing - 4px base scale, named by px value so a value in code
     reads directly off the variable name. */
  --ds-space-4: 4px;
  --ds-space-8: 8px;
  --ds-space-12: 12px;
  --ds-space-16: 16px;
  --ds-space-24: 24px;
  --ds-space-32: 32px;
  --ds-space-40: 40px;
  --ds-space-48: 48px;
  --ds-space-64: 64px;

  /* Radius - only one value was specified (card). Not inventing a
     small/default step that wasn't given. */
  --ds-radius-card: 20px;

  /* Motion */
  --ds-motion-tap: 100ms;
  --ds-motion-expand: 220ms;
  --ds-motion-collapse: 180ms;
  --ds-motion-page: 240ms;
  --ds-motion-modal: 260ms;
  --ds-motion-sheet: 280ms;

  /* Font loading - tokens ready for phase 2, not applied to any
     selector yet. font-variant-numeric/font-feature-settings are
     stored as tokens rather than left as literals so the one place
     that later applies the font can also apply these in the same
     declaration. */
  --ds-font-family: "IBM Plex Sans", system-ui, -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, Arial, sans-serif;
  --ds-font-variant-numeric: tabular-nums lining-nums;
  --ds-font-feature-settings: "tnum" 1, "lnum" 1;
}

/* Self-hosted IBM Plex Sans (Mission D1) - bundled under /fonts/, no
   Google Fonts/CDN dependency at runtime. A true variable font (single
   file, weight axis 100-700) rather than static per-weight files,
   because the type tokens above use 450 and 650 - values no static cut
   provides. Two subsets, both needed: "latin" alone excludes U+20B9
   (Rupee), which this app displays everywhere - "latin-ext" carries it.
   font-display: swap per spec. This declaration is inert until phase 2
   sets font-family: var(--ds-font-family) somewhere - declaring
   @font-face alone doesn't make the browser fetch anything or change
   how any element renders. */
@font-face {
  font-family: "IBM Plex Sans";
  font-style: normal;
  font-weight: 100 700;
  font-display: swap;
  src: url("/fonts/ibmplexsans-latin.woff2") format("woff2-variations");
  unicode-range: U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6, U+02DA, U+02DC, U+0304, U+0308, U+0329, U+2000-206F, U+20AC, U+2122, U+2191, U+2193, U+2212, U+2215, U+FEFF, U+FFFD;
}
@font-face {
  font-family: "IBM Plex Sans";
  font-style: normal;
  font-weight: 100 700;
  font-display: swap;
  src: url("/fonts/ibmplexsans-latin-ext.woff2") format("woff2-variations");
  unicode-range: U+0100-02BA, U+02BD-02C5, U+02C7-02CC, U+02CE-02D7, U+02DD-02FF, U+0304, U+0308, U+0329, U+1D00-1DBF, U+1E00-1E9F, U+1EF2-1EFF, U+2020, U+20A0-20AB, U+20AD-20C0, U+2113, U+2C60-2C7F, U+A720-A7FF;
}

/* Mission D2: IBM Plex Sans app-wide, plus tabular lining numerals
   (prices, values, percentages, metrics, risk numbers, timestamps) so
   columns stop jittering on refresh - applied once at body level rather
   than hunting down every numeric element; harmless no-op wherever
   there's no numeral. .mono (genuine code only, as of D2 - see
   dashboard.html) keeps its own explicit monospace font-family, which
   wins over this by cascade specificity where it's still used. */
body {
  font-family: var(--ds-font-family);
  font-variant-numeric: var(--ds-font-variant-numeric);
  font-feature-settings: var(--ds-font-feature-settings);
}

/* ---------- UI mutation-feedback doctrine (project memory) ----------
   Every control that triggers a server mutation shows visible progress for
   its full duration and is never silent - see withBusy() in dashboard.html,
   which wraps a button/checkbox in this state for the length of its async
   work. Dependency-light (no animation library) and theme-aware by
   construction: both use currentColor/tokens, so there is nothing separate
   to tune per palette. */
.spinner {
  display: inline-block;
  width: 12px; height: 12px;
  margin-right: 5px;
  border: 2px solid currentColor;
  border-top-color: transparent;
  opacity: .85;
  border-radius: 50%;
  vertical-align: -2px;
  animation: spin .7s linear infinite;
}
@keyframes spin { to { transform: rotate(360deg); } }

/* Inline status text next to a checkbox/control that can't host a spinner
   directly (e.g. a native checkbox) - "Updating…" while in flight, cleared
   once the view reflects the change. Mission G4 item 5: was muted-color
   static text, easy to miss against a real round trip that can run
   several seconds - full-strength text plus the same .spinner used for
   buttons gives it a moving cue, not just a color change. */
.busy-note {
  display: inline-flex;
  align-items: center;
  font-size: 12.5px;
  font-weight: 600;
  color: var(--ds-color-text-primary);
  margin-left: 8px;
}
.busy-note:empty {
  display: none;
}

/* Content-shaped loading placeholder, for a first paint that would
   otherwise be a blank gap rather than a specific control's busy state.
   Available alongside .spinner; not every surface needs it. */
.skeleton {
  display: inline-block;
  min-height: 1em;
  background: linear-gradient(90deg, var(--ds-color-divider) 25%, var(--ds-color-surface) 50%, var(--ds-color-divider) 75%);
  background-size: 200% 100%;
  border-radius: 6px;
  animation: skeleton-shimmer 1.2s ease-in-out infinite;
}
@keyframes skeleton-shimmer { 0% { background-position: 200% 0; } 100% { background-position: -200% 0; } }
