| 1 | /* plain — the skeleton: greyscale, system type, almost no shape. |
| 2 | Structure lives in tokens.css; this file is colour, type and shape. |
| 3 | |
| 4 | The look: nothing. plain is the theme you pick when you intend to |
| 5 | write your own — a working, accessible, entirely unstyled-looking app |
| 6 | that shows you the structure and none of the taste, so the first |
| 7 | colour you add is visibly yours. There is no accent hue: the accent |
| 8 | is near-black in light and near-white in dark, which is to say a link |
| 9 | reads as text with an underline on hover and a primary button reads as |
| 10 | a filled rectangle. That is deliberate. Add the hue. |
| 11 | |
| 12 | Status still has to signal. A skeleton that renders an error the same |
| 13 | grey as a success is not minimal, it is broken, so the four tone |
| 14 | pairs keep conventional hues at very low chroma — a green you would |
| 15 | call grey-green, an ochre, a brick — each still clearing 4.5:1 on its |
| 16 | own tint. Colour is never the only channel (the pills carry words), |
| 17 | but it is not thrown away either. |
| 18 | |
| 19 | Shadows are real and minimal. tokens.css paints with |
| 20 | var(--rst-shadow-*) unconditionally and a theme cannot opt out of a |
| 21 | property it must declare, so "no shadows" here means one hairline |
| 22 | layer at low alpha rather than a value the CSS parser would drop. |
| 23 | You get an edge, not a lift. |
| 24 | |
| 25 | ── One block, both schemes ─────────────────────────────────────────── |
| 26 | Every token is declared once, as light-dark(<light>, <dark>), under a |
| 27 | single :root that sets color-scheme: light dark. The two rules at the |
| 28 | bottom are the whole toggle: setting color-scheme on |
| 29 | :root[data-theme=…] re-resolves every light-dark() in the file at |
| 30 | once, in both directions. Tokens with one value in both schemes — the |
| 31 | font stack, the radii — are written plain. |
| 32 | |
| 33 | light-dark() is a colour function: it may only stand where a <color> |
| 34 | is expected. So a shadow token wraps its colour in it and writes the |
| 35 | geometry once, outside the call — `0 8px 24px light-dark(a, b)`, never |
| 36 | `light-dark(0 8px 24px a, …)`, which no browser would accept as a |
| 37 | box-shadow. |
| 38 | |
| 39 | ── WCAG 2.2 AA, measured ───────────────────────────────────────────── |
| 40 | Normal text needs 4.5:1; large text and UI component boundaries need |
| 41 | 3:1. Every pair below was computed from these exact hex values, in |
| 42 | both schemes. If you change a token, recompute its rows. |
| 43 | |
| 44 | LIGHT ratio requirement |
| 45 | --rst-text on --rst-surface 18.88 4.5:1 text |
| 46 | --rst-text on --rst-bg 18.88 4.5:1 text |
| 47 | --rst-text on --rst-surface-2 17.17 4.5:1 text |
| 48 | --rst-text on --rst-accent-soft 16.13 4.5:1 text |
| 49 | --rst-text-muted on --rst-surface 8.86 4.5:1 text |
| 50 | --rst-text-muted on --rst-bg 8.86 4.5:1 text |
| 51 | --rst-text-muted on --rst-surface-2 8.06 4.5:1 text |
| 52 | --rst-text-muted on --rst-accent-soft 7.57 4.5:1 text |
| 53 | --rst-text-faint on --rst-surface 6.01 4.5:1 text |
| 54 | --rst-text-faint on --rst-bg 6.01 4.5:1 text |
| 55 | --rst-text-faint on --rst-surface-2 5.46 4.5:1 text |
| 56 | --rst-text-faint on --rst-accent-soft 5.13 4.5:1 text |
| 57 | --rst-accent on --rst-surface 18.88 4.5:1 text + focus ring |
| 58 | --rst-accent on --rst-bg 18.88 4.5:1 text + focus ring |
| 59 | --rst-accent on --rst-surface-2 17.17 4.5:1 text |
| 60 | --rst-accent on --rst-accent-soft 16.13 4.5:1 text |
| 61 | --rst-on-accent on --rst-accent 18.88 4.5:1 button label |
| 62 | --rst-on-accent on --rst-accent-strong 21.00 4.5:1 button label, hover |
| 63 | --rst-line-strong on --rst-surface 4.54 3:1 control border |
| 64 | --rst-line-strong on --rst-bg 4.54 3:1 control border |
| 65 | --rst-line-strong on --rst-surface-2 4.13 3:1 control border |
| 66 | pill neutral fg on its own tint 8.75 4.5:1 text |
| 67 | pill positive fg on its own tint 6.46 4.5:1 text |
| 68 | pill warning fg on its own tint 6.09 4.5:1 text |
| 69 | pill negative fg on its own tint 7.55 4.5:1 text |
| 70 | --rst-on-accent on negative fg (fill) 9.44 4.5:1 danger button label |
| 71 | --rst-on-accent-fill on --rst-accent-fill 18.88 4.5:1 primary button label |
| 72 | --rst-on-accent-fill on --rst-accent-fill-hover 21.00 4.5:1 primary button label, hover |
| 73 | --rst-on-danger-fill on --rst-danger-fill 9.44 4.5:1 danger button label |
| 74 | --rst-on-danger-fill-hover on --rst-danger-fill-hover 9.44 4.5:1 danger button label, hover |
| 75 | --rst-text on --rst-row-tint-hover 17.17 4.5:1 list-row-action text, hovered |
| 76 | --rst-text on --rst-row-tint-press 17.17 4.5:1 list-row-action text, pressed |
| 77 | --rst-control-text-hover on --rst-control-face-hover 18.88 4.5:1 plain button label, hover |
| 78 | --rst-text-muted on --rst-control-face-hover 8.86 4.5:1 muted label on a hovered plain face |
| 79 | --rst-text on --rst-tint-hover 16.13 4.5:1 row text on the hover tint |
| 80 | --rst-text on --rst-tint-press 16.13 4.5:1 row text on the press tint |
| 81 | --rst-text-muted on --rst-tint-hover 7.57 4.5:1 muted cell on the hover tint |
| 82 | --rst-text-muted on --rst-tint-press 7.57 4.5:1 muted cell on the press tint |
| 83 | --rst-text-faint on --rst-tint-hover 5.13 4.5:1 faint cell on the hover tint |
| 84 | --rst-text-faint on --rst-tint-press 5.13 4.5:1 faint cell on the press tint |
| 85 | --rst-accent on --rst-tint-hover 16.13 4.5:1 a link on a hovered row |
| 86 | --rst-nav-current-fg on --rst-nav-current-bg 16.13 4.5:1 the current nav item |
| 87 | --rst-track-off on --rst-surface 4.54 3:1 switch off track (1.4.11 boundary) |
| 88 | --rst-track-off on --rst-bg 4.54 3:1 switch off track on the page |
| 89 | --rst-knob-shade on --rst-track-off 4.54 3:1 switch knob on the off track |
| 90 | --rst-knob-shade on --rst-accent-fill 18.88 3:1 switch knob on the on track |
| 91 | |
| 92 | DARK ratio requirement |
| 93 | --rst-text on --rst-surface 16.01 4.5:1 text |
| 94 | --rst-text on --rst-bg 17.12 4.5:1 text |
| 95 | --rst-text on --rst-surface-2 16.60 4.5:1 text |
| 96 | --rst-text on --rst-accent-soft 13.87 4.5:1 text |
| 97 | --rst-text-muted on --rst-surface 8.55 4.5:1 text |
| 98 | --rst-text-muted on --rst-bg 9.14 4.5:1 text |
| 99 | --rst-text-muted on --rst-surface-2 8.86 4.5:1 text |
| 100 | --rst-text-muted on --rst-accent-soft 7.40 4.5:1 text |
| 101 | --rst-text-faint on --rst-surface 6.86 4.5:1 text |
| 102 | --rst-text-faint on --rst-bg 7.33 4.5:1 text |
| 103 | --rst-text-faint on --rst-surface-2 7.11 4.5:1 text |
| 104 | --rst-text-faint on --rst-accent-soft 5.94 4.5:1 text |
| 105 | --rst-accent on --rst-surface 16.01 4.5:1 text + focus ring |
| 106 | --rst-accent on --rst-bg 17.12 4.5:1 text + focus ring |
| 107 | --rst-accent on --rst-surface-2 16.60 4.5:1 text |
| 108 | --rst-accent on --rst-accent-soft 13.87 4.5:1 text |
| 109 | --rst-on-accent on --rst-accent 17.12 4.5:1 button label |
| 110 | --rst-on-accent on --rst-accent-strong 19.17 4.5:1 button label, hover |
| 111 | --rst-line-strong on --rst-surface 5.54 3:1 control border |
| 112 | --rst-line-strong on --rst-bg 5.93 3:1 control border |
| 113 | --rst-line-strong on --rst-surface-2 5.75 3:1 control border |
| 114 | pill neutral fg on its own tint 9.05 4.5:1 text |
| 115 | pill positive fg on its own tint 8.70 4.5:1 text |
| 116 | pill warning fg on its own tint 8.82 4.5:1 text |
| 117 | pill negative fg on its own tint 7.63 4.5:1 text |
| 118 | --rst-on-accent on negative fg (fill) 8.93 4.5:1 danger button label |
| 119 | --rst-on-accent-fill on --rst-accent-fill 17.12 4.5:1 primary button label |
| 120 | --rst-on-accent-fill on --rst-accent-fill-hover 19.17 4.5:1 primary button label, hover |
| 121 | --rst-on-danger-fill on --rst-danger-fill 8.93 4.5:1 danger button label |
| 122 | --rst-on-danger-fill-hover on --rst-danger-fill-hover 8.93 4.5:1 danger button label, hover |
| 123 | --rst-text on --rst-row-tint-hover 16.60 4.5:1 list-row-action text, hovered |
| 124 | --rst-text on --rst-row-tint-press 16.60 4.5:1 list-row-action text, pressed |
| 125 | --rst-control-text-hover on --rst-control-face-hover 16.01 4.5:1 plain button label, hover |
| 126 | --rst-text-muted on --rst-control-face-hover 8.55 4.5:1 muted label on a hovered plain face |
| 127 | --rst-text on --rst-tint-hover 13.87 4.5:1 row text on the hover tint |
| 128 | --rst-text on --rst-tint-press 13.87 4.5:1 row text on the press tint |
| 129 | --rst-text-muted on --rst-tint-hover 7.40 4.5:1 muted cell on the hover tint |
| 130 | --rst-text-muted on --rst-tint-press 7.40 4.5:1 muted cell on the press tint |
| 131 | --rst-text-faint on --rst-tint-hover 5.94 4.5:1 faint cell on the hover tint |
| 132 | --rst-text-faint on --rst-tint-press 5.94 4.5:1 faint cell on the press tint |
| 133 | --rst-accent on --rst-tint-hover 13.87 4.5:1 a link on a hovered row |
| 134 | --rst-nav-current-fg on --rst-nav-current-bg 13.87 4.5:1 the current nav item |
| 135 | --rst-track-off on --rst-surface 5.54 3:1 switch off track (1.4.11 boundary) |
| 136 | --rst-track-off on --rst-bg 5.93 3:1 switch off track on the page |
| 137 | --rst-knob-shade on --rst-track-off 5.54 3:1 switch knob on the off track |
| 138 | --rst-knob-shade on --rst-accent-fill 16.01 3:1 switch knob on the on track |
| 139 | |
| 140 | The tightest pair in the set is --rst-text-faint on the accent tint — |
| 141 | 5.13:1 in light, 5.94:1 in dark, against a 4.5:1 floor. Everything |
| 142 | else has room, which is what a greyscale palette buys you. |
| 143 | |
| 144 | --rst-line is a hairline divider, not a control border, so 1.4.11's |
| 145 | 3:1 does not apply to it. Control borders use --rst-line-strong. |
| 146 | |
| 147 | --rst-header-rule is not in the table either, and deliberately so |
| 148 | (design doc §6-v2.2): the page header's rule is decoration, not |
| 149 | structure — the heading's size, weight and spacing carry the |
| 150 | structure — so no contrast floor applies to it and it is not in |
| 151 | ui/contrast_test.go's pair table. Do not argue it up to 3:1. |
| 152 | |
| 153 | --rst-shadow-pop, --rst-shadow-knob, --rst-shadow-lift and |
| 154 | --rst-overlay are not in the table: they paint a translucent layer |
| 155 | over whatever sits underneath, not text over a fixed background, so a |
| 156 | contrast ratio does not apply. |
| 157 | ──────────────────────────────────────────────────────────────────── */ |
| 158 | |
| 159 | :root { |
| 160 | color-scheme: light dark; |
| 161 | |
| 162 | /* Type: whatever the machine already has. */ |
| 163 | --rst-font: system-ui, -apple-system, "Segoe UI", Roboto, "Helvetica Neue", Arial, sans-serif; |
| 164 | |
| 165 | /* Shape: barely rounded. A corner you notice is a decision plain has |
| 166 | not made yet. */ |
| 167 | --rst-radius: 4px; |
| 168 | --rst-radius-sm: 3px; |
| 169 | --rst-radius-pill: 999px; |
| 170 | |
| 171 | /* Surfaces and lines */ |
| 172 | --rst-bg: light-dark(#ffffff, #0f0f0f); |
| 173 | --rst-surface: light-dark(#ffffff, #171717); |
| 174 | --rst-surface-2: light-dark(#f4f4f4, #131313); |
| 175 | --rst-line: light-dark(#e0e0e0, #2b2b2b); |
| 176 | --rst-line-strong: light-dark(#767676, #8f8f8f); |
| 177 | |
| 178 | /* The page header's rule. plain has no accent hue to fold in, so it |
| 179 | is the line and nothing else — the same derived token every theme |
| 180 | declares, resolving here to plain grey. Decorative: it carries no |
| 181 | contrast floor. */ |
| 182 | --rst-header-rule: var(--rst-line); |
| 183 | |
| 184 | /* Text */ |
| 185 | --rst-text: light-dark(#111111, #f2f2f2); |
| 186 | --rst-text-muted: light-dark(#4a4a4a, #b3b3b3); |
| 187 | --rst-text-faint: light-dark(#636363, #a0a0a0); |
| 188 | |
| 189 | /* Accent: none. The "accent" is the text colour, so a link is text |
| 190 | that underlines on hover and a primary button is a filled |
| 191 | rectangle. */ |
| 192 | --rst-accent: light-dark(#111111, #f2f2f2); |
| 193 | --rst-accent-strong: light-dark(#000000, #ffffff); |
| 194 | --rst-accent-soft: light-dark(#ededed, #242424); |
| 195 | --rst-on-accent: light-dark(#ffffff, #0f0f0f); |
| 196 | |
| 197 | /* Status tones: conventional hues held to very low chroma. Muted |
| 198 | enough to read as greyscale at a glance, separated enough that an |
| 199 | error is not a success. */ |
| 200 | --rst-tone-neutral-fg: light-dark(#3f3f3f, #c8c8c8); |
| 201 | --rst-tone-neutral-bg: light-dark(#eaeaea, #262626); |
| 202 | --rst-tone-positive-fg: light-dark(#2e5b3e, #9fc8ac); |
| 203 | --rst-tone-positive-bg: light-dark(#e4ebe6, #1a231d); |
| 204 | --rst-tone-warning-fg: light-dark(#6a5320, #d3c08a); |
| 205 | --rst-tone-warning-bg: light-dark(#efeadd, #262218); |
| 206 | --rst-tone-negative-fg: light-dark(#7a2c2c, #dba3a3); |
| 207 | --rst-tone-negative-bg: light-dark(#f0e3e3, #2a1c1c); |
| 208 | |
| 209 | /* Depth: one hairline layer. Real values, minimal effect. */ |
| 210 | --rst-shadow-pop: 0 1px 3px light-dark(rgba(0, 0, 0, 0.14), rgba(0, 0, 0, 0.5)); |
| 211 | --rst-shadow-knob: 0 1px 1px light-dark(rgba(0, 0, 0, 0.22), rgba(0, 0, 0, 0.4)); |
| 212 | --rst-shadow-knob-hover: var(--rst-shadow-knob); |
| 213 | --rst-shadow-lift: 0 1px 1px light-dark(rgba(0, 0, 0, 0.07), rgba(0, 0, 0, 0.3)); |
| 214 | --rst-overlay: light-dark(rgba(0, 0, 0, 0.4), rgba(0, 0, 0, 0.6)); |
| 215 | |
| 216 | /* The depth axis, declared as the semantic tokens tokens.css read in |
| 217 | each place before the axis existed: the fills are the accent, the |
| 218 | faces the surface, the tints the soft accent, the linked rows the |
| 219 | second surface, the borders, the off track and the focused edge |
| 220 | the strong line, the rim the line, the danger fill the negative |
| 221 | tone; the highlights, shadows, sheen and press are transparent and |
| 222 | 0. tokens.css reads every one of these, so a theme cannot leave |
| 223 | them out; it can only turn them off or point them back, which is |
| 224 | what this does, so this theme renders as it did, and an app's |
| 225 | override of --rst-accent, --rst-surface or --rst-line on :root |
| 226 | still reaches the controls it reached before (Astra A2). */ |
| 227 | --rst-accent-fill: var(--rst-accent); |
| 228 | --rst-accent-fill-hover: var(--rst-accent-strong); |
| 229 | --rst-accent-fill-edge: var(--rst-accent); |
| 230 | --rst-accent-fill-edge-hover: var(--rst-accent-strong); |
| 231 | --rst-on-accent-fill: var(--rst-on-accent); |
| 232 | --rst-danger-fill: var(--rst-tone-negative-fg); |
| 233 | --rst-danger-fill-hover: var(--rst-tone-negative-fg); |
| 234 | --rst-danger-fill-edge: var(--rst-tone-negative-fg); |
| 235 | --rst-danger-fill-edge-hover: var(--rst-tone-negative-fg); |
| 236 | --rst-on-danger-fill: var(--rst-on-accent); |
| 237 | --rst-on-danger-fill-hover: var(--rst-on-accent); |
| 238 | --rst-danger-hover-filter: brightness(0.9); |
| 239 | --rst-control-face: var(--rst-surface); |
| 240 | --rst-control-face-hover: var(--rst-surface); |
| 241 | --rst-control-face-press: transparent; |
| 242 | --rst-control-border: var(--rst-line-strong); |
| 243 | --rst-control-border-hover: var(--rst-accent); |
| 244 | --rst-control-text-hover: var(--rst-accent); |
| 245 | --rst-ghost-edge: var(--rst-line-strong); |
| 246 | --rst-ghost-face-hover: transparent; |
| 247 | --rst-ghost-edge-hover: var(--rst-line-strong); |
| 248 | --rst-summary-face: transparent; |
| 249 | --rst-summary-face-hover: transparent; |
| 250 | --rst-summary-edge: transparent; |
| 251 | --rst-summary-edge-hover: transparent; |
| 252 | --rst-summary-radius: 0; |
| 253 | --rst-depth-hl: transparent; |
| 254 | --rst-depth-contact: transparent; |
| 255 | --rst-depth-ambient: transparent; |
| 256 | --rst-depth-face: 0; |
| 257 | --rst-press-drop: 0px; |
| 258 | --rst-knob: var(--rst-surface); |
| 259 | --rst-knob-shade: var(--rst-surface); |
| 260 | --rst-track-off: var(--rst-line-strong); |
| 261 | /* The switch's one size, under a mouse and a finger alike: today's. */ |
| 262 | --rst-switch-w: 34px; |
| 263 | --rst-switch-h: 19px; |
| 264 | --rst-switch-knob: 15px; |
| 265 | --rst-switch-touch-w: 34px; |
| 266 | --rst-switch-touch-h: 19px; |
| 267 | --rst-switch-touch-knob: 15px; |
| 268 | --rst-tint-hover: var(--rst-accent-soft); |
| 269 | --rst-tint-press: var(--rst-accent-soft); |
| 270 | --rst-tint-tap: transparent; |
| 271 | --rst-tint-card-hover: transparent; |
| 272 | --rst-tint-tile-hover: var(--rst-surface); |
| 273 | --rst-tint-tile-press: var(--rst-surface); |
| 274 | --rst-tint-group-hover: transparent; |
| 275 | --rst-row-tint-hover: var(--rst-surface-2); |
| 276 | --rst-row-tint-press: var(--rst-surface-2); |
| 277 | --rst-focus-edge: var(--rst-line-strong); |
| 278 | --rst-focus-halo: transparent; |
| 279 | --rst-sheen-fill: transparent; |
| 280 | --rst-sheen-plain: transparent; |
| 281 | --rst-light-rim: var(--rst-line); |
| 282 | --rst-light-wash: transparent; |
| 283 | --rst-press-wash: 0; |
| 284 | --rst-press-spot: 0; |
| 285 | --rst-press-wash-plain: 0; |
| 286 | --rst-press-spot-plain: 0; |
| 287 | --rst-title-size: 1.375rem; |
| 288 | --rst-title-weight: 600; |
| 289 | --rst-title-track: -0.015em; |
| 290 | --rst-heading-size: var(--rst-fs-base); |
| 291 | --rst-label-case: uppercase; |
| 292 | --rst-label-track: 0.06em; |
| 293 | --rst-rail-bg: var(--rst-surface); |
| 294 | --rst-nav-current-bg: var(--rst-accent-soft); |
| 295 | --rst-nav-current-fg: var(--rst-accent); |
| 296 | } |
| 297 | |
| 298 | /* The whole toggle. Setting color-scheme re-resolves every light-dark() |
| 299 | above, so an explicit choice beats the OS in both directions without |
| 300 | restating a colour. */ |
| 301 | :root[data-theme="light"] { color-scheme: light; } |
| 302 | :root[data-theme="dark"] { color-scheme: dark; } |
| 303 | |