| 1 | /* signal — graphite and one live cobalt. Structure lives in tokens.css; |
| 2 | this file is colour, type and shape. |
| 3 | |
| 4 | ── Direction ───────────────────────────────────────────────────────── |
| 5 | THESIS: contrast is spent, not sprinkled. One cobalt marks the place a |
| 6 | decision is being made and nothing else borrows it. The arrangement |
| 7 | this refuses is the default product-dashboard palette — mid-grey text, |
| 8 | an accent tint smeared across every surface, and pillowy corners that |
| 9 | make a destructive button look like a suggestion. |
| 10 | OWN-WORLD: an anodised instrument faceplate. Cool graphite neutrals |
| 11 | with a blue undertone, silk-screened labels, one cobalt indicator that |
| 12 | reads as powered. Corners are cut, not moulded — 4px and 2px, the |
| 13 | radius of a milled edge. Shadows are short and dense: a panel seated |
| 14 | on a panel, never floating over it. Type is grotesk-first, the |
| 15 | workshop label face rather than a display serif. |
| 16 | MODE: operate. Assertive is not the same as loud — the page is calm |
| 17 | graphite until something is actionable, and then it is unmistakable. |
| 18 | RISK, stated: near-black plus one bright accent is a well-worn look, |
| 19 | and the way this earns its own ground is restraint plus the shape |
| 20 | axis, not a brighter blue. If a screen ends up with cobalt in four |
| 21 | places, the theme is being used against its grain. |
| 22 | FINISH: every pair in both schemes is measured below and gated by |
| 23 | ui/contrast_test.go; the direction above is the contract the palette |
| 24 | is audited against. |
| 25 | |
| 26 | ── One block, both schemes ─────────────────────────────────────────── |
| 27 | Every token is declared once, as light-dark(<light>, <dark>), under a |
| 28 | single :root that sets color-scheme: light dark. The two rules at the |
| 29 | bottom are the whole toggle: setting color-scheme on |
| 30 | :root[data-theme=…] re-resolves every light-dark() in the file at |
| 31 | once, in both directions. Tokens with one value in both schemes — the |
| 32 | font stack, the radii — are written plain. |
| 33 | |
| 34 | light-dark() is a colour function: it may only stand where a <color> |
| 35 | is expected. So a shadow token wraps its colour in it and writes the |
| 36 | geometry once, outside the call — `0 4px 10px light-dark(a, b)`, never |
| 37 | `light-dark(0 4px 10px a, …)`, which no browser would accept as a |
| 38 | box-shadow. |
| 39 | |
| 40 | ── WCAG 2.2 AA, measured ───────────────────────────────────────────── |
| 41 | Normal text needs 4.5:1; large text and UI component boundaries need |
| 42 | 3:1. Every pair below was computed from these exact hex values, in |
| 43 | both schemes. If you change a token, recompute its rows. |
| 44 | |
| 45 | LIGHT ratio requirement |
| 46 | --rst-text on --rst-surface 19.32 4.5:1 text |
| 47 | --rst-text on --rst-bg 17.72 4.5:1 text |
| 48 | --rst-text on --rst-surface-2 16.93 4.5:1 text |
| 49 | --rst-text on --rst-accent-soft 16.68 4.5:1 text |
| 50 | --rst-text-muted on --rst-surface 8.76 4.5:1 text |
| 51 | --rst-text-muted on --rst-bg 8.04 4.5:1 text |
| 52 | --rst-text-muted on --rst-surface-2 7.68 4.5:1 text |
| 53 | --rst-text-muted on --rst-accent-soft 7.57 4.5:1 text |
| 54 | --rst-text-faint on --rst-surface 6.13 4.5:1 text |
| 55 | --rst-text-faint on --rst-bg 5.62 4.5:1 text |
| 56 | --rst-text-faint on --rst-surface-2 5.37 4.5:1 text |
| 57 | --rst-text-faint on --rst-accent-soft 5.29 4.5:1 text |
| 58 | --rst-accent on --rst-surface 5.50 4.5:1 text + focus ring |
| 59 | --rst-accent on --rst-bg 5.04 4.5:1 text + focus ring |
| 60 | --rst-accent on --rst-surface-2 4.82 4.5:1 text |
| 61 | --rst-accent on --rst-accent-soft 4.75 4.5:1 text |
| 62 | --rst-on-accent on --rst-accent 5.50 4.5:1 button label |
| 63 | --rst-on-accent on --rst-accent-strong 7.77 4.5:1 button label, hover |
| 64 | --rst-line-strong on --rst-surface 4.70 3:1 control border |
| 65 | --rst-line-strong on --rst-bg 4.31 3:1 control border |
| 66 | --rst-line-strong on --rst-surface-2 4.12 3:1 control border |
| 67 | pill neutral fg on its own tint 8.36 4.5:1 text |
| 68 | pill positive fg on its own tint 6.07 4.5:1 text |
| 69 | pill warning fg on its own tint 6.21 4.5:1 text |
| 70 | pill negative fg on its own tint 6.00 4.5:1 text |
| 71 | --rst-on-accent on negative fg (fill) 7.58 4.5:1 danger button label |
| 72 | --rst-on-accent-fill on --rst-accent-fill 5.50 4.5:1 primary button label |
| 73 | --rst-on-accent-fill on --rst-accent-fill-hover 7.77 4.5:1 primary button label, hover |
| 74 | --rst-on-danger-fill on --rst-danger-fill 7.58 4.5:1 danger button label |
| 75 | --rst-on-danger-fill-hover on --rst-danger-fill-hover 7.58 4.5:1 danger button label, hover |
| 76 | --rst-text on --rst-row-tint-hover 16.93 4.5:1 list-row-action text, hovered |
| 77 | --rst-text on --rst-row-tint-press 16.93 4.5:1 list-row-action text, pressed |
| 78 | --rst-control-text-hover on --rst-control-face-hover 5.50 4.5:1 plain button label, hover |
| 79 | --rst-text-muted on --rst-control-face-hover 8.76 4.5:1 muted label on a hovered plain face |
| 80 | --rst-text on --rst-tint-hover 16.68 4.5:1 row text on the hover tint |
| 81 | --rst-text on --rst-tint-press 16.68 4.5:1 row text on the press tint |
| 82 | --rst-text-muted on --rst-tint-hover 7.57 4.5:1 muted cell on the hover tint |
| 83 | --rst-text-muted on --rst-tint-press 7.57 4.5:1 muted cell on the press tint |
| 84 | --rst-text-faint on --rst-tint-hover 5.29 4.5:1 faint cell on the hover tint |
| 85 | --rst-text-faint on --rst-tint-press 5.29 4.5:1 faint cell on the press tint |
| 86 | --rst-accent on --rst-tint-hover 4.75 4.5:1 a link on a hovered row |
| 87 | --rst-nav-current-fg on --rst-nav-current-bg 4.75 4.5:1 the current nav item |
| 88 | --rst-track-off on --rst-surface 4.70 3:1 switch off track (1.4.11 boundary) |
| 89 | --rst-track-off on --rst-bg 4.31 3:1 switch off track on the page |
| 90 | --rst-knob-shade on --rst-track-off 4.70 3:1 switch knob on the off track |
| 91 | --rst-knob-shade on --rst-accent-fill 5.50 3:1 switch knob on the on track |
| 92 | |
| 93 | DARK ratio requirement |
| 94 | --rst-text on --rst-surface 15.05 4.5:1 text |
| 95 | --rst-text on --rst-bg 16.30 4.5:1 text |
| 96 | --rst-text on --rst-surface-2 15.59 4.5:1 text |
| 97 | --rst-text on --rst-accent-soft 14.47 4.5:1 text |
| 98 | --rst-text-muted on --rst-surface 7.85 4.5:1 text |
| 99 | --rst-text-muted on --rst-bg 8.51 4.5:1 text |
| 100 | --rst-text-muted on --rst-surface-2 8.14 4.5:1 text |
| 101 | --rst-text-muted on --rst-accent-soft 7.55 4.5:1 text |
| 102 | --rst-text-faint on --rst-surface 6.58 4.5:1 text |
| 103 | --rst-text-faint on --rst-bg 7.13 4.5:1 text |
| 104 | --rst-text-faint on --rst-surface-2 6.82 4.5:1 text |
| 105 | --rst-text-faint on --rst-accent-soft 6.33 4.5:1 text |
| 106 | --rst-accent on --rst-surface 6.06 4.5:1 text + focus ring |
| 107 | --rst-accent on --rst-bg 6.56 4.5:1 text + focus ring |
| 108 | --rst-accent on --rst-surface-2 6.28 4.5:1 text |
| 109 | --rst-accent on --rst-accent-soft 5.82 4.5:1 text |
| 110 | --rst-on-accent on --rst-accent 6.79 4.5:1 button label |
| 111 | --rst-on-accent on --rst-accent-strong 9.77 4.5:1 button label, hover |
| 112 | --rst-line-strong on --rst-surface 4.65 3:1 control border |
| 113 | --rst-line-strong on --rst-bg 5.03 3:1 control border |
| 114 | --rst-line-strong on --rst-surface-2 4.81 3:1 control border |
| 115 | pill neutral fg on its own tint 9.26 4.5:1 text |
| 116 | pill positive fg on its own tint 9.07 4.5:1 text |
| 117 | pill warning fg on its own tint 10.00 4.5:1 text |
| 118 | pill negative fg on its own tint 7.84 4.5:1 text |
| 119 | --rst-on-accent on negative fg (fill) 9.25 4.5:1 danger button label |
| 120 | --rst-on-accent-fill on --rst-accent-fill 6.79 4.5:1 primary button label |
| 121 | --rst-on-accent-fill on --rst-accent-fill-hover 9.77 4.5:1 primary button label, hover |
| 122 | --rst-on-danger-fill on --rst-danger-fill 9.25 4.5:1 danger button label |
| 123 | --rst-on-danger-fill-hover on --rst-danger-fill-hover 9.25 4.5:1 danger button label, hover |
| 124 | --rst-text on --rst-row-tint-hover 15.59 4.5:1 list-row-action text, hovered |
| 125 | --rst-text on --rst-row-tint-press 15.59 4.5:1 list-row-action text, pressed |
| 126 | --rst-control-text-hover on --rst-control-face-hover 6.06 4.5:1 plain button label, hover |
| 127 | --rst-text-muted on --rst-control-face-hover 7.85 4.5:1 muted label on a hovered plain face |
| 128 | --rst-text on --rst-tint-hover 14.47 4.5:1 row text on the hover tint |
| 129 | --rst-text on --rst-tint-press 14.47 4.5:1 row text on the press tint |
| 130 | --rst-text-muted on --rst-tint-hover 7.55 4.5:1 muted cell on the hover tint |
| 131 | --rst-text-muted on --rst-tint-press 7.55 4.5:1 muted cell on the press tint |
| 132 | --rst-text-faint on --rst-tint-hover 6.33 4.5:1 faint cell on the hover tint |
| 133 | --rst-text-faint on --rst-tint-press 6.33 4.5:1 faint cell on the press tint |
| 134 | --rst-accent on --rst-tint-hover 5.82 4.5:1 a link on a hovered row |
| 135 | --rst-nav-current-fg on --rst-nav-current-bg 5.82 4.5:1 the current nav item |
| 136 | --rst-track-off on --rst-surface 4.65 3:1 switch off track (1.4.11 boundary) |
| 137 | --rst-track-off on --rst-bg 5.03 3:1 switch off track on the page |
| 138 | --rst-knob-shade on --rst-track-off 4.65 3:1 switch knob on the off track |
| 139 | --rst-knob-shade on --rst-accent-fill 6.06 3:1 switch knob on the on track |
| 140 | |
| 141 | The tightest pair in the set is --rst-accent on --rst-accent-soft in |
| 142 | light, at 4.75:1 against a 4.5:1 floor — the cobalt is as bright as it |
| 143 | can be and still be readable as text on its own tint. Lighten it and |
| 144 | the gate goes red; that is the intended tension. |
| 145 | |
| 146 | --rst-line is a hairline divider, not a control border, so 1.4.11's |
| 147 | 3:1 does not apply to it. Control borders use --rst-line-strong. |
| 148 | |
| 149 | --rst-header-rule is not in the table either, and deliberately so |
| 150 | (design doc §6-v2.2): the page header's rule is decoration, not |
| 151 | structure — the heading's size, weight and spacing carry the |
| 152 | structure — so no contrast floor applies to it and it is not in |
| 153 | ui/contrast_test.go's pair table. Do not argue it up to 3:1. |
| 154 | |
| 155 | --rst-shadow-pop, --rst-shadow-knob, --rst-shadow-lift and |
| 156 | --rst-overlay are not in the table: they paint a translucent layer |
| 157 | over whatever sits underneath, not text over a fixed background, so a |
| 158 | contrast ratio does not apply. |
| 159 | ──────────────────────────────────────────────────────────────────── */ |
| 160 | |
| 161 | :root { |
| 162 | color-scheme: light dark; |
| 163 | |
| 164 | /* Type: grotesk first, on purpose. The stack asks for the neutral |
| 165 | workhorse grotesks a machine actually has before it falls back to |
| 166 | whatever the platform calls its UI face. No webfont: a theme is one |
| 167 | stylesheet and must not open a connection. */ |
| 168 | --rst-font: "Helvetica Neue", Helvetica, "Segoe UI Variable Text", "Segoe UI", Roboto, Arial, ui-sans-serif, system-ui, sans-serif; |
| 169 | |
| 170 | /* Shape: milled, not moulded. */ |
| 171 | --rst-radius: 4px; |
| 172 | --rst-radius-sm: 2px; |
| 173 | --rst-radius-pill: 999px; |
| 174 | |
| 175 | /* Surfaces and lines: graphite with a blue undertone, so the cobalt |
| 176 | reads as the only warm-running thing on the panel. */ |
| 177 | --rst-bg: light-dark(#f4f5f8, #0b0d10); |
| 178 | --rst-surface: light-dark(#ffffff, #14171c); |
| 179 | --rst-surface-2: light-dark(#eef0f4, #101318); |
| 180 | --rst-line: light-dark(#dde0e7, #232830); |
| 181 | --rst-line-strong: light-dark(#6d7482, #7b828f); |
| 182 | |
| 183 | /* The page header's rule, derived rather than authored: the cobalt |
| 184 | folded into the line at 45%, so a theme that changes its accent |
| 185 | gets a matching rule with no second value to keep in step. signal |
| 186 | spends the tint it earns — the rule is visibly blue, which is the |
| 187 | loudest this theme is anywhere it is not asking for a click. |
| 188 | Decorative: it carries no contrast floor. */ |
| 189 | --rst-header-rule: color-mix(in oklab, var(--rst-accent) 45%, var(--rst-line)); |
| 190 | |
| 191 | /* Text */ |
| 192 | --rst-text: light-dark(#0c0e12, #e9ebef); |
| 193 | --rst-text-muted: light-dark(#454b57, #a4acb9); |
| 194 | --rst-text-faint: light-dark(#5b6270, #959dac); |
| 195 | |
| 196 | /* Accent: one electric cobalt. It appears where a decision is made and |
| 197 | nowhere else. */ |
| 198 | --rst-accent: light-dark(#1a56ff, #6d90ff); |
| 199 | --rst-accent-strong: light-dark(#0f3fd6, #9ab2ff); |
| 200 | --rst-accent-soft: light-dark(#e9eeff, #121a30); |
| 201 | --rst-on-accent: light-dark(#ffffff, #05070d); |
| 202 | |
| 203 | /* Status tones: saturated and unambiguous. A pill is never the |
| 204 | accent, so an error can never read as a link. */ |
| 205 | --rst-tone-neutral-fg: light-dark(#3b414d, #c6cdd8); |
| 206 | --rst-tone-neutral-bg: light-dark(#e6e8ee, #232830); |
| 207 | --rst-tone-positive-fg: light-dark(#08624a, #4fdba2); |
| 208 | --rst-tone-positive-bg: light-dark(#d6efe6, #082720); |
| 209 | --rst-tone-warning-fg: light-dark(#7a4708, #f3c669); |
| 210 | --rst-tone-warning-bg: light-dark(#f8e5c2, #2a2009); |
| 211 | --rst-tone-negative-fg: light-dark(#a8121f, #ff8f99); |
| 212 | --rst-tone-negative-bg: light-dark(#fbdee0, #2e1418); |
| 213 | |
| 214 | /* Depth: short throw, tight blur, high alpha. Seated, not floating. */ |
| 215 | --rst-shadow-pop: 0 4px 10px light-dark(rgba(12, 14, 18, 0.22), rgba(0, 0, 0, 0.62)); |
| 216 | --rst-shadow-knob: 0 1px 2px light-dark(rgba(12, 14, 18, 0.45), rgba(0, 0, 0, 0.6)); |
| 217 | --rst-shadow-knob-hover: var(--rst-shadow-knob); |
| 218 | --rst-shadow-lift: 0 1px 2px light-dark(rgba(12, 14, 18, 0.2), rgba(0, 0, 0, 0.5)); |
| 219 | --rst-overlay: light-dark(rgba(9, 11, 16, 0.55), rgba(3, 4, 7, 0.72)); |
| 220 | |
| 221 | /* The depth axis, declared as the semantic tokens tokens.css read in |
| 222 | each place before the axis existed: the fills are the accent, the |
| 223 | faces the surface, the tints the soft accent, the linked rows the |
| 224 | second surface, the borders, the off track and the focused edge |
| 225 | the strong line, the rim the line, the danger fill the negative |
| 226 | tone; the highlights, shadows, sheen and press are transparent and |
| 227 | 0. tokens.css reads every one of these, so a theme cannot leave |
| 228 | them out; it can only turn them off or point them back, which is |
| 229 | what this does, so this theme renders as it did, and an app's |
| 230 | override of --rst-accent, --rst-surface or --rst-line on :root |
| 231 | still reaches the controls it reached before (Astra A2). */ |
| 232 | --rst-accent-fill: var(--rst-accent); |
| 233 | --rst-accent-fill-hover: var(--rst-accent-strong); |
| 234 | --rst-accent-fill-edge: var(--rst-accent); |
| 235 | --rst-accent-fill-edge-hover: var(--rst-accent-strong); |
| 236 | --rst-on-accent-fill: var(--rst-on-accent); |
| 237 | --rst-danger-fill: var(--rst-tone-negative-fg); |
| 238 | --rst-danger-fill-hover: var(--rst-tone-negative-fg); |
| 239 | --rst-danger-fill-edge: var(--rst-tone-negative-fg); |
| 240 | --rst-danger-fill-edge-hover: var(--rst-tone-negative-fg); |
| 241 | --rst-on-danger-fill: var(--rst-on-accent); |
| 242 | --rst-on-danger-fill-hover: var(--rst-on-accent); |
| 243 | --rst-danger-hover-filter: brightness(0.9); |
| 244 | --rst-control-face: var(--rst-surface); |
| 245 | --rst-control-face-hover: var(--rst-surface); |
| 246 | --rst-control-face-press: transparent; |
| 247 | --rst-control-border: var(--rst-line-strong); |
| 248 | --rst-control-border-hover: var(--rst-accent); |
| 249 | --rst-control-text-hover: var(--rst-accent); |
| 250 | --rst-ghost-edge: var(--rst-line-strong); |
| 251 | --rst-ghost-face-hover: transparent; |
| 252 | --rst-ghost-edge-hover: var(--rst-line-strong); |
| 253 | --rst-summary-face: transparent; |
| 254 | --rst-summary-face-hover: transparent; |
| 255 | --rst-summary-edge: transparent; |
| 256 | --rst-summary-edge-hover: transparent; |
| 257 | --rst-summary-radius: 0; |
| 258 | --rst-depth-hl: transparent; |
| 259 | --rst-depth-contact: transparent; |
| 260 | --rst-depth-ambient: transparent; |
| 261 | --rst-depth-face: 0; |
| 262 | --rst-press-drop: 0px; |
| 263 | --rst-knob: var(--rst-surface); |
| 264 | --rst-knob-shade: var(--rst-surface); |
| 265 | --rst-track-off: var(--rst-line-strong); |
| 266 | /* The switch's one size, under a mouse and a finger alike: today's. */ |
| 267 | --rst-switch-w: 34px; |
| 268 | --rst-switch-h: 19px; |
| 269 | --rst-switch-knob: 15px; |
| 270 | --rst-switch-touch-w: 34px; |
| 271 | --rst-switch-touch-h: 19px; |
| 272 | --rst-switch-touch-knob: 15px; |
| 273 | --rst-tint-hover: var(--rst-accent-soft); |
| 274 | --rst-tint-press: var(--rst-accent-soft); |
| 275 | --rst-tint-tap: transparent; |
| 276 | --rst-tint-card-hover: transparent; |
| 277 | --rst-tint-tile-hover: var(--rst-surface); |
| 278 | --rst-tint-tile-press: var(--rst-surface); |
| 279 | --rst-tint-group-hover: transparent; |
| 280 | --rst-row-tint-hover: var(--rst-surface-2); |
| 281 | --rst-row-tint-press: var(--rst-surface-2); |
| 282 | --rst-focus-edge: var(--rst-line-strong); |
| 283 | --rst-focus-halo: transparent; |
| 284 | --rst-sheen-fill: transparent; |
| 285 | --rst-sheen-plain: transparent; |
| 286 | --rst-light-rim: var(--rst-line); |
| 287 | --rst-light-wash: transparent; |
| 288 | --rst-press-wash: 0; |
| 289 | --rst-press-spot: 0; |
| 290 | --rst-press-wash-plain: 0; |
| 291 | --rst-press-spot-plain: 0; |
| 292 | --rst-title-size: 1.375rem; |
| 293 | --rst-title-weight: 600; |
| 294 | --rst-title-track: -0.015em; |
| 295 | --rst-heading-size: var(--rst-fs-base); |
| 296 | --rst-label-case: uppercase; |
| 297 | --rst-label-track: 0.06em; |
| 298 | --rst-rail-bg: var(--rst-surface); |
| 299 | --rst-nav-current-bg: var(--rst-accent-soft); |
| 300 | --rst-nav-current-fg: var(--rst-accent); |
| 301 | } |
| 302 | |
| 303 | /* The whole toggle. Setting color-scheme re-resolves every light-dark() |
| 304 | above, so an explicit choice beats the OS in both directions without |
| 305 | restating a colour. */ |
| 306 | :root[data-theme="light"] { color-scheme: light; } |
| 307 | :root[data-theme="dark"] { color-scheme: dark; } |
| 308 | |