Files
observer/web
rob thijssen 4774b8cfaa
All checks were successful
deploy / build (push) Successful in 1m12s
deploy / deploy-api (push) Has been skipped
deploy / deploy-web (push) Successful in 4s
feat(ui): a light theme, following the browser, with a toggle in the masthead
Dark, light, or whatever the browser asks for. Auto is the default, so a reader
who has never touched it gets their own system's answer rather than ours.

The light palette is selected, not inverted. `#bd8829` carries every magnitude
on this site at 6.6:1 against the warm-black and **2.77:1** against paper — the
validator says so, and 2.77 is under the 3:1 floor a mark has to clear. So light
mode gets its own step of the same bronze, and its `--data-bright` sits *darker*
than `--data`, because emphasis on paper is weight rather than glare. Same for
the washes: a glow at 0.08 alpha on warm-black is a smear at 0.08 on paper, so
the nine colour literals that were still loose in the stylesheet became tokens —
each one was a colour the second theme could not have overridden.

Every value was chosen by running the dataviz validator against the surface it
actually sits on, both modes. The single-hue rule is untouched and still
load-bearing in both: bronze and crimson fail CVD separation as a categorical
pair whichever ground they are on.

`auto` is a preference rather than a third palette. It resolves to a concrete
`light` or `dark` before the stylesheet ever sees it, which is what keeps this to
one definition per palette instead of one per palette per media query — and it
has to resolve before the *first paint*, because anything running after the
bundle loads runs after the page has been painted once, and on a light
preference that is a full-screen flash of warm-black. Hence the inline script,
whose duplication of `lib/theme.ts` is the cheaper of the two costs.

Two things that would otherwise bite: `localStorage` throws rather than returning
null where site data is blocked, so every access is guarded and falls back to
what the browser wants; and `auto` keeps listening, so a machine that turns dark
at sunset does not leave a reader on the daylight palette until they reload.

The toggle shows the state it is in, never the state it would move to — a control
that displays its own destination is why these get guessed at — and its
accessible name carries that state, since the icon cannot.

Checked in a browser, both themes, on the standings, a miner page and the share
chart. Worth recording what that turned up: dark carries 46 text elements under
4.5:1 and light carries 3, each beating its dark counterpart. The gap is
`--text-muted` at 3.7, the deliberate existing value CLAUDE.md has always
documented — not introduced here, and not something to change without deciding
to change the dark design.

Closes #12

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Jp6a8EDar9ueEhAxzep4V5
2026-09-10 12:22:30 +03:00
..