/* signal — graphite and one live cobalt. Structure lives in tokens.css; this file is colour, type and shape. ── Direction ───────────────────────────────────────────────────────── THESIS: contrast is spent, not sprinkled. One cobalt marks the place a decision is being made and nothing else borrows it. The arrangement this refuses is the default product-dashboard palette — mid-grey text, an accent tint smeared across every surface, and pillowy corners that make a destructive button look like a suggestion. OWN-WORLD: an anodised instrument faceplate. Cool graphite neutrals with a blue undertone, silk-screened labels, one cobalt indicator that reads as powered. Corners are cut, not moulded — 4px and 2px, the radius of a milled edge. Shadows are short and dense: a panel seated on a panel, never floating over it. Type is grotesk-first, the workshop label face rather than a display serif. MODE: operate. Assertive is not the same as loud — the page is calm graphite until something is actionable, and then it is unmistakable. RISK, stated: near-black plus one bright accent is a well-worn look, and the way this earns its own ground is restraint plus the shape axis, not a brighter blue. If a screen ends up with cobalt in four places, the theme is being used against its grain. FINISH: every pair in both schemes is measured below and gated by ui/contrast_test.go; the direction above is the contract the palette is audited against. ── One block, both schemes ─────────────────────────────────────────── Every token is declared once, as light-dark(, ), under a single :root that sets color-scheme: light dark. The two rules at the bottom are the whole toggle: setting color-scheme on :root[data-theme=…] re-resolves every light-dark() in the file at once, in both directions. Tokens with one value in both schemes — the font stack, the radii — are written plain. light-dark() is a colour function: it may only stand where a is expected. So a shadow token wraps its colour in it and writes the geometry once, outside the call — `0 4px 10px light-dark(a, b)`, never `light-dark(0 4px 10px a, …)`, which no browser would accept as a box-shadow. ── WCAG 2.2 AA, measured ───────────────────────────────────────────── Normal text needs 4.5:1; large text and UI component boundaries need 3:1. Every pair below was computed from these exact hex values, in both schemes. If you change a token, recompute its rows. LIGHT ratio requirement --rst-text on --rst-surface 19.32 4.5:1 text --rst-text on --rst-bg 17.72 4.5:1 text --rst-text on --rst-surface-2 16.93 4.5:1 text --rst-text on --rst-accent-soft 16.68 4.5:1 text --rst-text-muted on --rst-surface 8.76 4.5:1 text --rst-text-muted on --rst-bg 8.04 4.5:1 text --rst-text-muted on --rst-surface-2 7.68 4.5:1 text --rst-text-muted on --rst-accent-soft 7.57 4.5:1 text --rst-text-faint on --rst-surface 6.13 4.5:1 text --rst-text-faint on --rst-bg 5.62 4.5:1 text --rst-text-faint on --rst-surface-2 5.37 4.5:1 text --rst-text-faint on --rst-accent-soft 5.29 4.5:1 text --rst-accent on --rst-surface 5.50 4.5:1 text + focus ring --rst-accent on --rst-bg 5.04 4.5:1 text + focus ring --rst-accent on --rst-surface-2 4.82 4.5:1 text --rst-accent on --rst-accent-soft 4.75 4.5:1 text --rst-on-accent on --rst-accent 5.50 4.5:1 button label --rst-on-accent on --rst-accent-strong 7.77 4.5:1 button label, hover --rst-line-strong on --rst-surface 4.70 3:1 control border --rst-line-strong on --rst-bg 4.31 3:1 control border --rst-line-strong on --rst-surface-2 4.12 3:1 control border pill neutral fg on its own tint 8.36 4.5:1 text pill positive fg on its own tint 6.07 4.5:1 text pill warning fg on its own tint 6.21 4.5:1 text pill negative fg on its own tint 6.00 4.5:1 text --rst-on-accent on negative fg (fill) 7.58 4.5:1 danger button label DARK ratio requirement --rst-text on --rst-surface 15.05 4.5:1 text --rst-text on --rst-bg 16.30 4.5:1 text --rst-text on --rst-surface-2 15.59 4.5:1 text --rst-text on --rst-accent-soft 14.47 4.5:1 text --rst-text-muted on --rst-surface 7.85 4.5:1 text --rst-text-muted on --rst-bg 8.51 4.5:1 text --rst-text-muted on --rst-surface-2 8.14 4.5:1 text --rst-text-muted on --rst-accent-soft 7.55 4.5:1 text --rst-text-faint on --rst-surface 6.58 4.5:1 text --rst-text-faint on --rst-bg 7.13 4.5:1 text --rst-text-faint on --rst-surface-2 6.82 4.5:1 text --rst-text-faint on --rst-accent-soft 6.33 4.5:1 text --rst-accent on --rst-surface 6.06 4.5:1 text + focus ring --rst-accent on --rst-bg 6.56 4.5:1 text + focus ring --rst-accent on --rst-surface-2 6.28 4.5:1 text --rst-accent on --rst-accent-soft 5.82 4.5:1 text --rst-on-accent on --rst-accent 6.79 4.5:1 button label --rst-on-accent on --rst-accent-strong 9.77 4.5:1 button label, hover --rst-line-strong on --rst-surface 4.65 3:1 control border --rst-line-strong on --rst-bg 5.03 3:1 control border --rst-line-strong on --rst-surface-2 4.81 3:1 control border pill neutral fg on its own tint 9.26 4.5:1 text pill positive fg on its own tint 9.07 4.5:1 text pill warning fg on its own tint 10.00 4.5:1 text pill negative fg on its own tint 7.84 4.5:1 text --rst-on-accent on negative fg (fill) 9.25 4.5:1 danger button label The tightest pair in the set is --rst-accent on --rst-accent-soft in light, at 4.75:1 against a 4.5:1 floor — the cobalt is as bright as it can be and still be readable as text on its own tint. Lighten it and the gate goes red; that is the intended tension. --rst-line is a hairline divider, not a control border, so 1.4.11's 3:1 does not apply to it. Control borders use --rst-line-strong. --rst-header-rule is not in the table either, and deliberately so (design doc §6-v2.2): the page header's rule is decoration, not structure — the heading's size, weight and spacing carry the structure — so no contrast floor applies to it and it is not in ui/contrast_test.go's pair table. Do not argue it up to 3:1. --rst-shadow-pop, --rst-shadow-knob, --rst-shadow-lift and --rst-overlay are not in the table: they paint a translucent layer over whatever sits underneath, not text over a fixed background, so a contrast ratio does not apply. ──────────────────────────────────────────────────────────────────── */ :root { color-scheme: light dark; /* Type: grotesk first, on purpose. The stack asks for the neutral workhorse grotesks a machine actually has before it falls back to whatever the platform calls its UI face. No webfont: a theme is one stylesheet and must not open a connection. */ --rst-font: "Helvetica Neue", Helvetica, "Segoe UI Variable Text", "Segoe UI", Roboto, Arial, ui-sans-serif, system-ui, sans-serif; /* Shape: milled, not moulded. */ --rst-radius: 4px; --rst-radius-sm: 2px; --rst-radius-pill: 999px; /* Surfaces and lines: graphite with a blue undertone, so the cobalt reads as the only warm-running thing on the panel. */ --rst-bg: light-dark(#f4f5f8, #0b0d10); --rst-surface: light-dark(#ffffff, #14171c); --rst-surface-2: light-dark(#eef0f4, #101318); --rst-line: light-dark(#dde0e7, #232830); --rst-line-strong: light-dark(#6d7482, #7b828f); /* The page header's rule, derived rather than authored: the cobalt folded into the line at 45%, so a theme that changes its accent gets a matching rule with no second value to keep in step. signal spends the tint it earns — the rule is visibly blue, which is the loudest this theme is anywhere it is not asking for a click. Decorative: it carries no contrast floor. */ --rst-header-rule: color-mix(in oklab, var(--rst-accent) 45%, var(--rst-line)); /* Text */ --rst-text: light-dark(#0c0e12, #e9ebef); --rst-text-muted: light-dark(#454b57, #a4acb9); --rst-text-faint: light-dark(#5b6270, #959dac); /* Accent: one electric cobalt. It appears where a decision is made and nowhere else. */ --rst-accent: light-dark(#1a56ff, #6d90ff); --rst-accent-strong: light-dark(#0f3fd6, #9ab2ff); --rst-accent-soft: light-dark(#e9eeff, #121a30); --rst-on-accent: light-dark(#ffffff, #05070d); /* Status tones: saturated and unambiguous. A pill is never the accent, so an error can never read as a link. */ --rst-tone-neutral-fg: light-dark(#3b414d, #c6cdd8); --rst-tone-neutral-bg: light-dark(#e6e8ee, #232830); --rst-tone-positive-fg: light-dark(#08624a, #4fdba2); --rst-tone-positive-bg: light-dark(#d6efe6, #082720); --rst-tone-warning-fg: light-dark(#7a4708, #f3c669); --rst-tone-warning-bg: light-dark(#f8e5c2, #2a2009); --rst-tone-negative-fg: light-dark(#a8121f, #ff8f99); --rst-tone-negative-bg: light-dark(#fbdee0, #2e1418); /* Depth: short throw, tight blur, high alpha. Seated, not floating. */ --rst-shadow-pop: 0 4px 10px light-dark(rgba(12, 14, 18, 0.22), rgba(0, 0, 0, 0.62)); --rst-shadow-knob: 0 1px 2px light-dark(rgba(12, 14, 18, 0.45), rgba(0, 0, 0, 0.6)); --rst-shadow-lift: 0 1px 2px light-dark(rgba(12, 14, 18, 0.2), rgba(0, 0, 0, 0.5)); --rst-overlay: light-dark(rgba(9, 11, 16, 0.55), rgba(3, 4, 7, 0.72)); } /* The whole toggle. Setting color-scheme re-resolves every light-dark() above, so an explicit choice beats the OS in both directions without restating a colour. */ :root[data-theme="light"] { color-scheme: light; } :root[data-theme="dark"] { color-scheme: dark; }