Colour
Two themes, one set of variables. Every surface, border and text colour is a token: switching theme swaps the values, never the rules.
The decision
- The problem
Five surfaces, three tints, four accents, and a muted grey that failed contrast in 46 places on the home.
- What I chose
Three surfaces, two tints named by weight, one accent, one status colour, and every text colour measured against the backdrop it really sits on.
- Ruled out
Naming tints by use (--input-bg, --code-bg): the next component invents a fourth instead of reusing one.
- What it cost
Mixing translucent text with its background before measuring it. An audit that skipped that step missed two real failures.
Surfaces and text
Three surfaces, not five. --card-bg was byte-identical to --surface on the light theme and one ΔE away on dark; --placeholder-bg sat 3.5 ΔE from --surface-alt in both. Below 2 ΔE the eye cannot separate two colours side by side, so those were four names for two greys.
One accent
Blue is the brand signal: links, active states, the highlighted phrase in every statement. --accent paints, --accent-text reads: the brand blue is for fills, borders, icons and large type, which only needs 3:1 of contrast; where blue is small text it uses --accent-text, which lightens on the dark theme to keep its own AA.
It is the only accent. There used to be three more: nothing used the green, the "yellow" (really an orange) only coloured a class no page used, and the teal only drew the keyboard focus ring, which is brand blue now too. The one other colour is --danger, for errors, and because it is text it has to reach AA.
Borders and tints
Two tints, not three: 4%, 6% and 7% of black were one grey wearing three hats. They are named by weight, soft and strong, so the next component reuses one instead of inventing another. Borders come in a real scale, roughly doubling.
Contrast is measured
AA is the floor: 4.5:1 for normal text, 3:1 from 24px. Every text on the site is checked against the backdrop it really sits on, in both themes. What that found:
- --text-muted, used for dates, numbering and labels: 2.95:1 in 46 places on the home. Now 4.60.
- White at 75% on the brand blue looks white and measures 3.72:1, because once mixed with the blue it is a pale blue. At 90% it reaches 4.74.
- The form error red: 3.53:1, unseen because it only appears when something fails. --danger is 4.78 on light and 6.22 on dark.
- Blue as small text on black: 3.81:1. On the dark theme --accent-text lightens to #4B85FF: 6.10.
Measure a pair
Pick a text colour, a background and an opacity. The ratio is worked out the way the site's audit does it: translucent text is mixed with its background first, then measured. The buttons load the four failures above, before and after.
Sep 2026 · Journal
Beautiful and unreadable is a failure, not a style.
Two themes
Light by default, unless the visitor chose otherwise. The theme is set in the head of every page before anything paints, so there is no flash, and every colour is a token, so switching swaps values and never rules.