/* 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 --rst-on-accent-fill on --rst-accent-fill 5.50 4.5:1 primary button label --rst-on-accent-fill on --rst-accent-fill-hover 7.77 4.5:1 primary button label, hover --rst-on-danger-fill on --rst-danger-fill 7.58 4.5:1 danger button label --rst-on-danger-fill-hover on --rst-danger-fill-hover 7.58 4.5:1 danger button label, hover --rst-text on --rst-row-tint-hover 16.93 4.5:1 list-row-action text, hovered --rst-text on --rst-row-tint-press 16.93 4.5:1 list-row-action text, pressed --rst-control-text-hover on --rst-control-face-hover 5.50 4.5:1 plain button label, hover --rst-text-muted on --rst-control-face-hover 8.76 4.5:1 muted label on a hovered plain face --rst-text on --rst-tint-hover 16.68 4.5:1 row text on the hover tint --rst-text on --rst-tint-press 16.68 4.5:1 row text on the press tint --rst-text-muted on --rst-tint-hover 7.57 4.5:1 muted cell on the hover tint --rst-text-muted on --rst-tint-press 7.57 4.5:1 muted cell on the press tint --rst-text-faint on --rst-tint-hover 5.29 4.5:1 faint cell on the hover tint --rst-text-faint on --rst-tint-press 5.29 4.5:1 faint cell on the press tint --rst-accent on --rst-tint-hover 4.75 4.5:1 a link on a hovered row --rst-nav-current-fg on --rst-nav-current-bg 4.75 4.5:1 the current nav item --rst-track-off on --rst-surface 4.70 3:1 switch off track (1.4.11 boundary) --rst-track-off on --rst-bg 4.31 3:1 switch off track on the page --rst-knob-shade on --rst-track-off 4.70 3:1 switch knob on the off track --rst-knob-shade on --rst-accent-fill 5.50 3:1 switch knob on the on track 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 --rst-on-accent-fill on --rst-accent-fill 6.79 4.5:1 primary button label --rst-on-accent-fill on --rst-accent-fill-hover 9.77 4.5:1 primary button label, hover --rst-on-danger-fill on --rst-danger-fill 9.25 4.5:1 danger button label --rst-on-danger-fill-hover on --rst-danger-fill-hover 9.25 4.5:1 danger button label, hover --rst-text on --rst-row-tint-hover 15.59 4.5:1 list-row-action text, hovered --rst-text on --rst-row-tint-press 15.59 4.5:1 list-row-action text, pressed --rst-control-text-hover on --rst-control-face-hover 6.06 4.5:1 plain button label, hover --rst-text-muted on --rst-control-face-hover 7.85 4.5:1 muted label on a hovered plain face --rst-text on --rst-tint-hover 14.47 4.5:1 row text on the hover tint --rst-text on --rst-tint-press 14.47 4.5:1 row text on the press tint --rst-text-muted on --rst-tint-hover 7.55 4.5:1 muted cell on the hover tint --rst-text-muted on --rst-tint-press 7.55 4.5:1 muted cell on the press tint --rst-text-faint on --rst-tint-hover 6.33 4.5:1 faint cell on the hover tint --rst-text-faint on --rst-tint-press 6.33 4.5:1 faint cell on the press tint --rst-accent on --rst-tint-hover 5.82 4.5:1 a link on a hovered row --rst-nav-current-fg on --rst-nav-current-bg 5.82 4.5:1 the current nav item --rst-track-off on --rst-surface 4.65 3:1 switch off track (1.4.11 boundary) --rst-track-off on --rst-bg 5.03 3:1 switch off track on the page --rst-knob-shade on --rst-track-off 4.65 3:1 switch knob on the off track --rst-knob-shade on --rst-accent-fill 6.06 3:1 switch knob on the on track 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-knob-hover: var(--rst-shadow-knob); --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 depth axis, declared as the semantic tokens tokens.css read in each place before the axis existed: the fills are the accent, the faces the surface, the tints the soft accent, the linked rows the second surface, the borders, the off track and the focused edge the strong line, the rim the line, the danger fill the negative tone; the highlights, shadows, sheen and press are transparent and 0. tokens.css reads every one of these, so a theme cannot leave them out; it can only turn them off or point them back, which is what this does, so this theme renders as it did, and an app's override of --rst-accent, --rst-surface or --rst-line on :root still reaches the controls it reached before (Astra A2). */ --rst-accent-fill: var(--rst-accent); --rst-accent-fill-hover: var(--rst-accent-strong); --rst-accent-fill-edge: var(--rst-accent); --rst-accent-fill-edge-hover: var(--rst-accent-strong); --rst-on-accent-fill: var(--rst-on-accent); --rst-danger-fill: var(--rst-tone-negative-fg); --rst-danger-fill-hover: var(--rst-tone-negative-fg); --rst-danger-fill-edge: var(--rst-tone-negative-fg); --rst-danger-fill-edge-hover: var(--rst-tone-negative-fg); --rst-on-danger-fill: var(--rst-on-accent); --rst-on-danger-fill-hover: var(--rst-on-accent); --rst-danger-hover-filter: brightness(0.9); --rst-control-face: var(--rst-surface); --rst-control-face-hover: var(--rst-surface); --rst-control-face-press: transparent; --rst-control-border: var(--rst-line-strong); --rst-control-border-hover: var(--rst-accent); --rst-control-text-hover: var(--rst-accent); --rst-ghost-edge: var(--rst-line-strong); --rst-ghost-face-hover: transparent; --rst-ghost-edge-hover: var(--rst-line-strong); --rst-summary-face: transparent; --rst-summary-face-hover: transparent; --rst-summary-edge: transparent; --rst-summary-edge-hover: transparent; --rst-summary-radius: 0; --rst-depth-hl: transparent; --rst-depth-contact: transparent; --rst-depth-ambient: transparent; --rst-depth-face: 0; --rst-press-drop: 0px; --rst-knob: var(--rst-surface); --rst-knob-shade: var(--rst-surface); --rst-track-off: var(--rst-line-strong); /* The switch's one size, under a mouse and a finger alike: today's. */ --rst-switch-w: 34px; --rst-switch-h: 19px; --rst-switch-knob: 15px; --rst-switch-touch-w: 34px; --rst-switch-touch-h: 19px; --rst-switch-touch-knob: 15px; --rst-tint-hover: var(--rst-accent-soft); --rst-tint-press: var(--rst-accent-soft); --rst-tint-tap: transparent; --rst-tint-card-hover: transparent; --rst-tint-tile-hover: var(--rst-surface); --rst-tint-tile-press: var(--rst-surface); --rst-tint-group-hover: transparent; --rst-row-tint-hover: var(--rst-surface-2); --rst-row-tint-press: var(--rst-surface-2); --rst-focus-edge: var(--rst-line-strong); --rst-focus-halo: transparent; --rst-sheen-fill: transparent; --rst-sheen-plain: transparent; --rst-light-rim: var(--rst-line); --rst-light-wash: transparent; --rst-press-wash: 0; --rst-press-spot: 0; --rst-press-wash-plain: 0; --rst-press-spot-plain: 0; --rst-title-size: 1.375rem; --rst-title-weight: 600; --rst-title-track: -0.015em; --rst-heading-size: var(--rst-fs-base); --rst-label-case: uppercase; --rst-label-track: 0.06em; --rst-rail-bg: var(--rst-surface); --rst-nav-current-bg: var(--rst-accent-soft); --rst-nav-current-fg: var(--rst-accent); } /* 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; }