26. September 2026
Light and dark, on every page
Blog · 26 September 2026 · Behind the scenes
This site now follows your system setting, and a small control in the navigation lets you override it: system, light, dark. Getting there was more interesting than we expected, mostly because of what an automatic colour inversion gets wrong.
The starting point
Seventeen pages, each with its own hand-written stylesheet. Some were designed dark — ICD-10, Read, Kompendium, Dockti, Facet, ProxyButler, Screenshotli, Leakhound. Others were designed light: the home page, the four audit apps, the legal pages. Between them, several hundred colours written as hex, rgba and oklch, largely without variables.
One declaration, two values
Every colour is now a token that carries both values at once:
--ic-bg1: #0a0f1a;
--ic-bg1: light-dark(#f0f7ff, #0a0f1a);
light-dark(); they keep exactly today's appearance.
CSS color-scheme decides which half applies, so the switch only has to set one attribute. 491 pairs across the site, generated rather than typed.
What the naive approach gets wrong
Our first attempt simply mirrored each colour's lightness. It produced a muddy grey-blue where a clean near-white belonged: a very dark navy page background mirrors to a mid-tone, not to paper. Mapping each group of colours onto a designed ladder instead — preserving the order and the hue, not the raw number — fixed that.
Four more lessons arrived one bug at a time:
- Islands must not flip. A light page with a dark footer has two polarities. Invert everything and the footer turns pale while its light text stays light. Only colours belonging to the page's dominant zone may flip — and text sitting on an island has to be frozen along with it, or it collapses into its own background.
- Pure white is two different things. As a card background it must flip. As the label on a coloured button it must not. The distinction we settled on: white text keeps its value only when the same rule also sets a background.
- Mid-tones need a reference. A grey at 55 % lightness is dark text on a white page and light text on a black one. Judging it against a fixed midpoint mislabels it; judging it against the page background does not.
- Token names collide. A page and a shared stylesheet that both define
--bg1will quietly blend two palettes. Every file got its own prefix.
The forest illustration on the home page is exempt from all of it. It is artwork, not interface, and it looks the same in both modes.
Measuring instead of squinting
Every page was then measured in both modes: each text element against the background it actually sits on, with OKLCH converted properly rather than guessed. The first run found roughly 75 places below the 4.5:1 threshold. After adjusting the affected side of each colour pair — dark mode and light mode are tuned independently — 14 remain.
All 14 are white text on brand colours, and all of them were already there in the light design. Dark mode is nowhere worse than light: where a brand colour is lightened for dark mode, the button label switches to dark text instead of white.
Details that are easy to miss
A short blocking script in the <head> applies your stored choice before the first paint, so nothing flashes on load. The choice is remembered per browser and applies across the whole site, and a change in one tab is picked up by the others.
One small warning from our own testing: measure after the CSS transition has finished. We spent a while chasing colour values that were simply halfway through a 300-millisecond fade.