/* web/styles/tokens.css — the only place colours, radii, and focus rings * are defined for the product UI. style.css and admin.css consume these; * they never redefine a hex value. * * Light mode on bare :root. Dark mode ONLY redefines the same custom * properties — never introduces a colour that exists solely inside a * media query or [data-theme] block. --sql-* is the exception that * proves the rule: the SQL code surface is always-dark, so those tokens * are declared once and never flipped. * * See docs/design/DESIGN.md §4. */ :root { /* Brand / chrome * * --teal is the fill behind WHITE button labels (.btn-ask and every * other `background: var(--teal); color: #fff` control) — that pairing * is checked by tests/web_ui/test_web_ui_contrast_and_failure.py and * must clear 4.5:1 (a 15px bold label is not "large text"; that * exemption starts at 18.66px bold, so the ordinary text ratio * applies). The original brand hue (#0d9488) is only 3.74:1 — same hue * and saturation, darkened from lightness .32 to .26 to reach 5.21, * which is exactly where the palette's own --teal-d already sat, so * light mode's fix is that value promoted rather than a new one. * --teal-d steps one lightness stop further down (.22) for :hover. * --chart-focus below keeps the ORIGINAL, brighter hue on purpose: a * chart line is read next to a grey context line, not printed with * white text on top of it, so it never had a contrast problem and * DESIGN-INVARIANTS.md §10 fixes it by lightening --chart-context * instead of touching this. Flattening every teal in the app to one * accessible-for-white-text shade would have been simpler and is * exactly what "do not flatten the brand" (§11) rules out. */ --navy: #0e2a47; --navy-2: #14375c; --navy-3: #1b436e; --teal: #0b7a70; --teal-d: #09675f; --teal-l: #e7f6f2; /* Surface */ --bg: #f4f6fa; --card: #ffffff; --card-2: #f8fafc; --border: #e2e8f0; /* --border is for decorative hairlines (card edges, dividers) and * deliberately stays low-contrast — 1.4.11's 3:1 floor is for what * IDENTIFIES an interactive control, not for chrome that just * separates regions. --border-interactive is that control boundary: * the ask textarea (and every other input/textarea) previously relied * on --border for its edge (1.23:1 against --card) with a fill that * only differed from the page by 1.05 — neither signal reached 3:1, so * nothing marked the field as a field. Same hue family as --muted * (secondary text), one step more saturated so it reads as a boundary * rather than as text that wandered onto an edge. */ --border-interactive: #7c8ba3; --ink: #1f2937; /* #64748b on --card was 4.76 (fine) but only 4.40 on --bg — the page * ground is very slightly lighter than the card, and that was enough * to drop secondary text below 4.5 wherever it sits directly on the * page rather than inside a card. #5b6b81 is the same hue, one step * darker, and clears both surfaces (5.02 on --bg, 5.43 on --card). * Dark mode's --muted already passes (7.30) and is unchanged. */ --muted: #5b6b81; /* Legacy aliases still used by a few components */ --green: #10b981; --red: #e5484d; --amber: #f59e0b; /* SQL code surface — always-dark editor palette. Not chrome brand. */ --sql-bg: #0d1117; --sql-text: #e6edf3; --sql-keyword: #79c0ff; --sql-number: #e3b341; --sql-function: #d2a8ff; --sql-string: #a5d6ff; --sql-operator: #8b949e; --sql-comment: #8b949e; /* Status — a fact about the system right now, never brand. * * --status-critical was #e5484d: 1.54:1 against --status-good and * dE-deutan 8.6 (tests/web_ui/test_web_ui_state_not_colour_alone.py, * DESIGN-INVARIANTS.md §10's floor is dE >= 8 AND a lightness split, not * either alone -- 8.6 cleared the first and failed the second, so the * two "up"/"down" dots read as one colour at the same brightness to a * dichromat and the *only* other signal was hue). #d32f2f is a deeper, * more saturated red at the same identity: 1.96:1, dE-deutan 13.9. * Considered instead: darkening --status-good so --status-critical could * stay nearer its original shade. Rejected -- --status-good is read by * nearly twice as many rules (guard pill, admin pass/fail, cache badge, * turn outcome, live-key status …) as --status-critical, so moving it * risks a visual regression in places this task was not scoped to touch. * Moving only --status-critical, and keeping it inside the same red * family rather than the brick/rust the search space pushes toward when * --status-good is held fixed (see the dark block below), is the * narrower change. */ --status-good: #10b981; --status-good-bg: #e9f9f1; --status-warn: #f59e0b; --status-warn-bg: #fef6e7; --status-critical: #d32f2f; --status-critical-bg: #fdeeee; --status-neutral: #64748b; --status-neutral-bg: #eef1f5; /* Chart — a fourth, narrow colour axis: FOCUS vs CONTEXT within one * chart only (web/js/render/chart.js). Declared here as its own * literal pair, not aliased to --teal/--muted the way it used to be: * DESIGN-INVARIANTS.md §10 measured that alias-based scheme at a * focus/context contrast of 1.03 in dark mode — the same perceived * brightness, so hue was the only remaining signal, which is exactly * what colour-blind vision cannot use (ΔE 2.2 deutan, against a target * floor of 15). Emphasis is a lightness-and-weight job, not a hue job, * so --chart-focus keeps the full-brightness brand teal (identical to * the pre-fix --teal, kept independent of it so the two can move for * different reasons) and --chart-context is a new, deliberately paler * blue-grey rather than a darkened --muted. Stroke width carries the * rest of the distinction — see chart.js's "stroke-width" literals. */ --chart-focus: #0d9488; --chart-context: #b8c4d4; /* Source of assumption (third colour axis) */ --source-question: #0d9488; --source-question-bg: #e7f6f2; --source-session: #6366f1; --source-session-bg: #eef0fd; --source-default: #64748b; --source-default-bg: #eef1f5; --source-policy: #b45309; --source-policy-bg: #fbf1e0; --source-memory: #be185d; --source-memory-bg: #fce8f1; /* Geometry */ --radius: 12px; --radius-sm: 8px; --radius-pill: 999px; --shadow: 0 1px 2px rgba(15, 23, 42, .06), 0 4px 14px rgba(15, 23, 42, .05); --focus-ring: 0 0 0 3px rgba(13, 148, 136, .35); /* Type scale (see DESIGN.md §4.4) */ --fs-xs: 11px; --fs-sm: 12.5px; --fs-md: 14px; --fs-lg: 15px; --fs-xl: 18px; --fs-2xl: 21px; } /* Dark mode via OS preference, unless the page has an explicit choice. */ @media (prefers-color-scheme: dark) { :root:not([data-theme="light"]) { --navy: #16324f; --navy-2: #1c3f61; --navy-3: #234c74; /* Same hue/saturation as the original #14b8a6, darkened from * lightness .40 to .28 — white-on-teal was 2.49:1 (worse than light * mode's, since dark mode's brand teal was the brighter of the two * to begin with) and this reaches 4.76. --teal-d steps to .22 for * :hover. See the light :root's --teal comment for why --chart-focus * is exempt and keeps the original #14b8a6. */ --teal: #0e8174; --teal-d: #0b655b; --teal-l: #113330; --bg: #0b1220; --card: #131c2e; --card-2: #182238; --border: #263349; /* Same reasoning as the light :root's --border-interactive: a hue * near --muted, one step more saturated, so an input's edge reaches * 3:1 against --card (4.40) without touching decorative --border. */ --border-interactive: #71839b; --ink: #e5e9f0; --muted: #94a3b8; --green: #34d399; --red: #f87171; --amber: #fbbf24; /* --sql-* intentionally unchanged: always-dark code surface. */ /* --status-critical was #f87171: 1.44:1 against --status-good and * dE-deutan 7.1 -- BELOW DESIGN-INVARIANTS.md §10's floor of 8, not * merely short of the lightness split (see the light :root's * --status-critical comment for the full search and the rejected * alternative of moving --status-good instead). Dark is the tighter * case: holding --status-good at its light mint (#34d399) pushes the * feasible --status-critical values toward brick/rust hues once ΔE and * contrast both have to clear their floors, which reads oddly as a * "down" colour and was rejected for that reason. #e53e3e stays inside * the same red family as the original and clears both: 2.15:1, dE * -deutan 16.6. */ --status-good: #34d399; --status-good-bg: #10261e; --status-warn: #fbbf24; --status-warn-bg: #2b230f; --status-critical: #e53e3e; --status-critical-bg: #2b1414; --status-neutral: #94a3b8; --status-neutral-bg: #1c2536; /* See the light :root's --chart-focus/--chart-context comment. * Focus keeps the original, pre-fix dark --teal (#14b8a6); context * is a new dark blue-grey, not a darkened --muted, chosen so the * pair clears 1.8:1 (measures 3.50) instead of the old 1.03:1. */ --chart-focus: #14b8a6; --chart-context: #3d4c63; --source-question: #2dd4bf; --source-question-bg: #102a27; --source-session: #818cf8; --source-session-bg: #1e2144; --source-default: #94a3b8; --source-default-bg: #1c2536; --source-policy: #f0b95e; --source-policy-bg: #2e2410; --source-memory: #f472b6; --source-memory-bg: #341126; --shadow: 0 1px 2px rgba(0, 0, 0, .35), 0 6px 20px rgba(0, 0, 0, .3); --focus-ring: 0 0 0 3px rgba(20, 184, 166, .45); } } /* Explicit toggle wins over OS preference in both directions. */ :root[data-theme="dark"] { --navy: #16324f; --navy-2: #1c3f61; --navy-3: #234c74; /* Mirrors the @media dark block above — see its comments. */ --teal: #0e8174; --teal-d: #0b655b; --teal-l: #113330; --bg: #0b1220; --card: #131c2e; --card-2: #182238; --border: #263349; --border-interactive: #71839b; --ink: #e5e9f0; --muted: #94a3b8; --green: #34d399; --red: #f87171; --amber: #fbbf24; /* --sql-* intentionally unchanged: always-dark code surface. */ --status-good: #34d399; --status-good-bg: #10261e; --status-warn: #fbbf24; --status-warn-bg: #2b230f; --status-critical: #e53e3e; --status-critical-bg: #2b1414; --status-neutral: #94a3b8; --status-neutral-bg: #1c2536; --chart-focus: #14b8a6; --chart-context: #3d4c63; --source-question: #2dd4bf; --source-question-bg: #102a27; --source-session: #818cf8; --source-session-bg: #1e2144; --source-default: #94a3b8; --source-default-bg: #1c2536; --source-policy: #f0b95e; --source-policy-bg: #2e2410; --source-memory: #f472b6; --source-memory-bg: #341126; --shadow: 0 1px 2px rgba(0, 0, 0, .35), 0 6px 20px rgba(0, 0, 0, .3); --focus-ring: 0 0 0 3px rgba(20, 184, 166, .45); } :root[data-theme="light"] { color-scheme: light; } :root[data-theme="dark"] { color-scheme: dark; }