# Following the operating system

2026-08-06

Convoro picks light or dark from a cookie. A visitor whose machine is in dark
mode gets a white page on their first visit, and keeps getting one until they
find the toggle. Everything else on their screen is dark; we are the odd one
out.

The fix is a third option — **System** — and making it the default.

## Why this is a CSS problem, not a script problem

The server renders the palette into the document head so the right colours are
in the first byte. That is deliberate and it is what stops the white flash on
every page load for somebody using dark mode.

The operating system preference is the one thing the server cannot know. It is
not in a header; it is only ever visible to CSS, through
`prefers-color-scheme`, and to JavaScript once the page is already running.

So the only honest way to do this is in CSS. A script that reads the preference
and corrects the page afterwards *is* the white flash — it just makes it
shorter. Nothing here may depend on JavaScript running.

## What already worked

More of this was in place than expected. `Tokens::MODES` has always been
`['light', 'dark', 'auto']`, `ThemeController` already accepts any of the three,
and `StyleSheet::render()` already emits the correct thing for `auto`:

```
:root{ …light… }
@media (prefers-color-scheme: dark){ :root{ …dark… } }
```

Three things were missing, and the third is the real work.

## 1. The default

`Tokens::DEFAULTS['mode']` is `light`. A visitor with no cookie gets light
regardless of their machine. It becomes `auto`.

This is a change in what a *new* forum does out of the box, not a change to any
existing site: a theme that has explicitly saved `light` keeps it, and a
visitor who has used the toggle has a cookie that still wins.

## 2. The control

The toggle is two-state and computes "the other one". It becomes three
explicit choices — Light, Dark, System — because a cycling button cannot say
what it is currently on, and "System" is a state a person needs to be able to
see they are in, not just pass through.

## 3. The rules that never worked in auto

This is the part that would have made a half-fix look broken.

Twenty-four rules in `convoro.css` are written as `:root[data-theme="dark"] X`.
They re-tune the semantic tints — alerts, moderation badges, activity chips,
the select chevron, the card shadow — because those were picked as fixed
colours against a white background and are illegible on a dark one.

In `auto` the root element carries `data-theme="auto"`, so **none of them
match**. A System user on a dark machine would get dark surfaces with
light-mode alert tints painted on top. The block's own comment claims it
"applies both when the visitor has chosen dark explicitly and when 'auto'
resolves to dark via the OS", which was never true — it was written as an
intention and never tested against the case.

There is no way to write one CSS rule that means "explicit dark, or no
preference and the OS is dark" without either duplicating every selector or
moving the values into custom properties. Duplicating seventy lines to be kept
in step by hand is how the next one of these gets missed.

So the values move into the token block, which `StyleSheet` already emits
correctly per mode. Each semantic hue gets three slots:

| | soft background | border | ink |
|---|---|---|---|
| danger | `--c-danger-soft` | `--c-danger-line` | `--c-danger-ink` |
| success | `--c-success-soft` | `--c-success-line` | `--c-success-ink` |
| warning | `--c-warning-soft` | `--c-warning-line` | `--c-warning-ink` |
| info | `--c-info-soft` | `--c-info-line` | `--c-info-ink` |

plus a stronger pair per hue for chips that sit on a surface rather than in a
panel (`--c-*-chip`, `--c-*-chip-ink`), the two card shadows, and the select
chevron.

**The light values do not change.** Every light token is the exact colour that
rule used before. Light mode has had five design passes and a refactor is not
the place to quietly restyle it — if a light screenshot differs by one pixel,
the refactor is wrong.

The twenty-four rules then delete, because a rule that says
"in dark, use the dark colour" is what a token already means.

## What this costs

Two hues of divergence get normalised. `.kind-topic` and `.activity-verb-topic`
were `#059669` and `#166534` — two greens for the same idea, three hundred
lines apart, almost certainly because they were written on different days.
They keep their own ink tokens rather than being merged, so nothing shifts;
the note is here so the next person knows it was seen and left alone
deliberately.

## Checking it

The screenshot pass is the check, and it needs six shots rather than two: each
of Light, Dark and System, against an emulated OS preference of light and of
dark. System-on-a-dark-machine is the one that has never been looked at, and
it is the one this is for.
