The theme was one enum — system | light | dark | ihasmail — where one value carried a whole palette and implied dark. That works for exactly one palette. The two questions now come apart: which palette, and which side.
Palette
Classic
The plain light and dark this app has always had
ihasmail
The project's own, now with a day version
Dracula
with Alucard as its light half
Gruvbox
Rosé Pine
with Dawn
Tokyo Night
with Day
Sourcing
All four borrowed palettes are MIT and were fetched from each project's own repository, not from anyone's reimplementation. What was fetched is recorded verbatim in .palette-sources/palettes-upstream.md, and credited in a new NOTICE.
Worth flagging a correction to something I said earlier in planning: I believed Alucard was part of the commercial Dracula PRO and that Dracula would have to ship dark-only. It is not — the repository's section is titled "Color Palette (OSS)" and contains both.
The contrast pass is the substance
These projects publish twelve to twenty values; ihasmail needs about thirty. scripts/build-palettes.py derives the tiers between them, then measures every text colour against the surface it sits on — 4.5:1 for prose, 3:1 for borders and marks — and lifts anything short towards white on a dark ground and towards black on a light one, so a lifted tier keeps its hue instead of washing out. It refuses to write a palette that would not pass.
Every one of the nine halves needed at least one lift. These palettes are built for code editors, not prose at this size:
Dracula's comment grey #6272a4 — 3.03:1 on its own background, lifted to #8390b7 at 4.50
Rosé Pine's gold on Dawn — 2.7:1, lifted
Almost every border-strong tier was under 3:1
Shipping them as published would have quietly ended the WCAG AA claim two sections down in FEATURES.md.
What giving ihasmail a light half removed
Nothing is one-sided any more, and a whole special case went with it: a palette can no longer override the mode, the toggle no longer sets a palette aside on the way to light and remembers it, and the greyed-out control that explained all that is gone. lastDarkTheme — which existed only for that — is gone too.
Two bugs caught while checking, not while writing
Choosing an accent over a new palette did nothing at all. The generated blocks were appended to the end of the stylesheet, putting them after the accent variants at equal specificity. Verified live: picking purple on Gruvbox left the accent at #076678. They now sit before those rules — which the existing ihasmail block's own comment says is exactly where they have to be.
The hand-written ihasmail block was unqualified (:root[data-palette="ihasmail"]), so it would have shadowed the new light half. It is now explicitly the dark one.
Migration
Settings written before this carry theme and no palette, and are read through the old enum — indefinitely, not for one release, since settings live in the account's Files and are opened by whatever version runs next. theme is still written back, derived, because a device on an older build reads it and would otherwise be stranded on a theme nobody chose.
Testing
16 new palette tests plus rewritten theme and settings-sync tests: migration of all four old values, the derived legacy write-back, the toggle keeping colours across every palette, and a file carrying both shapes preferring the new one.
Suite green: web 787 across 84 files, server 116, typecheck clean.
Verified in the running app
Gruvbox light paints upstream's light0#fbf1c7 exactly; ihasmail day gives --bg #f4f9f9 with --fg #0d2430 (the dark palette's background as its text); theme-color follows --bg; and the accent override works again on a palette.
The theme was one enum — `system | light | dark | ihasmail` — where one value carried a whole palette and implied dark. That works for exactly one palette. The two questions now come apart: **which palette**, and **which side**.
| Palette | |
| --- | --- |
| **Classic** | The plain light and dark this app has always had |
| **ihasmail** | The project's own, now with a day version |
| **Dracula** | with Alucard as its light half |
| **Gruvbox** | |
| **Rosé Pine** | with Dawn |
| **Tokyo Night** | with Day |
## Sourcing
All four borrowed palettes are MIT and were fetched from **each project's own repository**, not from anyone's reimplementation. What was fetched is recorded verbatim in `.palette-sources/palettes-upstream.md`, and credited in a new `NOTICE`.
Worth flagging a correction to something I said earlier in planning: I believed Alucard was part of the commercial Dracula PRO and that Dracula would have to ship dark-only. It is not — the repository's section is titled *"Color Palette (OSS)"* and contains both.
## The contrast pass is the substance
These projects publish twelve to twenty values; ihasmail needs about thirty. `scripts/build-palettes.py` derives the tiers between them, then measures every text colour against the surface it sits on — 4.5:1 for prose, 3:1 for borders and marks — and lifts anything short *towards white on a dark ground and towards black on a light one*, so a lifted tier keeps its hue instead of washing out. **It refuses to write a palette that would not pass.**
**Every one of the nine halves needed at least one lift.** These palettes are built for code editors, not prose at this size:
- Dracula's comment grey `#6272a4` — **3.03:1** on its own background, lifted to `#8390b7` at 4.50
- Rosé Pine's gold on Dawn — **2.7:1**, lifted
- Almost every `border-strong` tier was under 3:1
Shipping them as published would have quietly ended the WCAG AA claim two sections down in FEATURES.md.
## What giving ihasmail a light half removed
Nothing is one-sided any more, and a whole special case went with it: a palette can no longer override the mode, the toggle no longer sets a palette aside on the way to light and remembers it, and the greyed-out control that explained all that is gone. `lastDarkTheme` — which existed only for that — is gone too.
## Two bugs caught while checking, not while writing
**Choosing an accent over a new palette did nothing at all.** The generated blocks were appended to the end of the stylesheet, putting them *after* the accent variants at equal specificity. Verified live: picking purple on Gruvbox left the accent at `#076678`. They now sit before those rules — which the existing ihasmail block's own comment says is exactly where they have to be.
**The hand-written ihasmail block was unqualified** (`:root[data-palette="ihasmail"]`), so it would have shadowed the new light half. It is now explicitly the dark one.
## Migration
Settings written before this carry `theme` and no palette, and are read through the old enum — indefinitely, not for one release, since settings live in the account's Files and are opened by whatever version runs next. `theme` is still **written back, derived**, because a device on an older build reads it and would otherwise be stranded on a theme nobody chose.
## Testing
16 new palette tests plus rewritten theme and settings-sync tests: migration of all four old values, the derived legacy write-back, the toggle keeping colours across every palette, and a file carrying both shapes preferring the new one.
Suite green: web 787 across 84 files, server 116, typecheck clean.
## Verified in the running app
Gruvbox light paints upstream's `light0` `#fbf1c7` exactly; ihasmail day gives `--bg #f4f9f9` with `--fg #0d2430` (the dark palette's background as its text); `theme-color` follows `--bg`; and the accent override works again on a palette.
**Merged** 2026-09-02 as coffey-labs/ihasmail@0ac5f7878964
<sub>Rebuilt from: git history, session transcript.</sub>
This repo is archived. You cannot comment on issues.
Blocking a user prevents them from interacting with repositories, such as opening or commenting on pull requests or issues. Learn more about blocking a user.
The theme was one enum —
system | light | dark | ihasmail— where one value carried a whole palette and implied dark. That works for exactly one palette. The two questions now come apart: which palette, and which side.Sourcing
All four borrowed palettes are MIT and were fetched from each project's own repository, not from anyone's reimplementation. What was fetched is recorded verbatim in
.palette-sources/palettes-upstream.md, and credited in a newNOTICE.Worth flagging a correction to something I said earlier in planning: I believed Alucard was part of the commercial Dracula PRO and that Dracula would have to ship dark-only. It is not — the repository's section is titled "Color Palette (OSS)" and contains both.
The contrast pass is the substance
These projects publish twelve to twenty values; ihasmail needs about thirty.
scripts/build-palettes.pyderives the tiers between them, then measures every text colour against the surface it sits on — 4.5:1 for prose, 3:1 for borders and marks — and lifts anything short towards white on a dark ground and towards black on a light one, so a lifted tier keeps its hue instead of washing out. It refuses to write a palette that would not pass.Every one of the nine halves needed at least one lift. These palettes are built for code editors, not prose at this size:
#6272a4— 3.03:1 on its own background, lifted to#8390b7at 4.50border-strongtier was under 3:1Shipping them as published would have quietly ended the WCAG AA claim two sections down in FEATURES.md.
What giving ihasmail a light half removed
Nothing is one-sided any more, and a whole special case went with it: a palette can no longer override the mode, the toggle no longer sets a palette aside on the way to light and remembers it, and the greyed-out control that explained all that is gone.
lastDarkTheme— which existed only for that — is gone too.Two bugs caught while checking, not while writing
Choosing an accent over a new palette did nothing at all. The generated blocks were appended to the end of the stylesheet, putting them after the accent variants at equal specificity. Verified live: picking purple on Gruvbox left the accent at
#076678. They now sit before those rules — which the existing ihasmail block's own comment says is exactly where they have to be.The hand-written ihasmail block was unqualified (
:root[data-palette="ihasmail"]), so it would have shadowed the new light half. It is now explicitly the dark one.Migration
Settings written before this carry
themeand no palette, and are read through the old enum — indefinitely, not for one release, since settings live in the account's Files and are opened by whatever version runs next.themeis still written back, derived, because a device on an older build reads it and would otherwise be stranded on a theme nobody chose.Testing
16 new palette tests plus rewritten theme and settings-sync tests: migration of all four old values, the derived legacy write-back, the toggle keeping colours across every palette, and a file carrying both shapes preferring the new one.
Suite green: web 787 across 84 files, server 116, typecheck clean.
Verified in the running app
Gruvbox light paints upstream's
light0#fbf1c7exactly; ihasmail day gives--bg #f4f9f9with--fg #0d2430(the dark palette's background as its text);theme-colorfollows--bg; and the accent override works again on a palette.Merged 2026-09-02 as coffey-labs/ihasmail@0ac5f78789
Rebuilt from: git history, session transcript.