Foundations

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.

Text size
From the site
Muted grey White on blue Error red Blue on black
Mixed colour Ratio

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.