Two grounds whose roles swap
Light and dark are one system with the roles exchanged, not two palettes.
Context
Outword needed a light and a dark mode. Two separately tuned palettes would double every contrast check and drift apart with each new screen.
Options
| Option | Why it lost |
|---|---|
| Dark only | No mode for players who want a light screen |
| Two palettes, tuned per mode | Every new component needs two sets of values and two contrast checks; drift is guaranteed |
| One set of roles, swapped | Chosen |
Decision
Define each colour once as a role (--ground, --fg, --tile…) with a baked dark and light value. Components use only roles. The page of one mode is the ink of the other, and one attribute, data-mode, swaps them all.
Evidence
| Pair | Dark | Light |
|---|---|---|
--fg on --groundbody text on the page | 12.70AAA | 12.70AAA |
| Pair | Dark | Light |
|---|---|---|
--tile-ink on --tilea letter on its tile | 12.70AAA | 12.70AAA |
Contrast is identical in both modes because the pair is the same two colours exchanged. Computed from the tokens on every build.
Consequences
- A new screen needs one contrast check, not two.
- A hardcoded colour is always wrong in one mode, which is why the rule against literals is absolute and the release check counts them.
- The two semantic hues (green, rose) can't simply invert; they get their own dark and light values, tuned to pass on both grounds.