Rework colour themes as greyscale + accent-only colour (ticket 032)

light/dark are now exact lightness mirrors of one true neutral scale,
grey is that same canonical neutral scale, and each named theme
(red/green/blue/yellow/pink/orange) derives its surface/border/text
greys by desaturating its own Tailwind hue at each scale step (HSL
S->0, lightness kept), leaving saturated colour only on --c-accent*.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01XFUNZwCvz3kcod3352ZR2o
This commit is contained in:
Hugh Kaznowski
2026-09-05 16:21:50 +01:00
co-authored by Claude Sonnet 5
parent 7694dcf120
commit 758feca780
2 changed files with 142 additions and 79 deletions
-47
View File
@@ -1,47 +0,0 @@
# 032 — Rework colour themes as greyscale equivalents with accent-only colour
## Summary
Today's colour themes (`webapp/src/index.css`) only vary `--c-accent`/
`--c-accent-soft`/`--c-accent-text` per theme (`html.red`, `.green`, `.blue`,
`.yellow`, `.pink`, `.orange` at lines 97-130) — every theme shares the same
dark `:root` surfaces/borders/text. `html.grey` (lines 75-95) is a Tailwind
slate palette, which reads blue-tinted rather than neutral. The wishlist
wants: `light`/`dark` as true black-and-white inverses of each other, `grey`
as true neutral greyscale, and every other named theme (`red`/`green`/etc.)
to desaturate its named hue to the matching greyscale lightness for
surfaces/borders/text, reserving actual saturated colour for accents only.
## Scope
- `:root` (dark) and `html.light`: convert to a true black/white greyscale
scale (surfaces, borders, text all `#000`–`#fff` greys, inverted between
the two), replacing the current warm/cool-tinted neutrals.
- `html.grey`: replace the slate-derived palette (lines 76-90) with the same
neutral greyscale scale as dark/light (i.e. `grey` becomes the canonical
greyscale theme other themes derive from), keeping only its own accent
distinct if one is still wanted.
- `red`/`green`/`blue`/`yellow`/`pink`/`orange`: for each, desaturate that
hue to compute a matching greyscale lightness scale for
`--c-surface-*`/`--c-border-*`/`--c-text-*` (e.g. convert the hue to HSL,
zero the saturation, keep the lightness steps), then keep that theme's
actual saturated colour only on `--c-accent*`.
- Document the derivation (e.g. a short comment or a small script/table) so a
new themed variant can be added consistently later.
## Acceptance criteria
- `light` and `dark` are literal greyscale inverses of each other (every
`--c-*` value in one is the other's lightness mirrored, no hue).
- `grey` uses a neutral (zero-saturation) scale, not a blue-tinted one.
- Each named colour theme's surfaces/borders/text render as neutral greys at
a lightness matching that colour's own luminance, with the named colour
visible only via `--c-accent*`.
## Depends on
None.
## Depended on by
- [033 — Downloadable/uploadable custom themes](033-custom-theme-download-upload.md)
+142 -32
View File
@@ -9,12 +9,16 @@
* so re-declaring the `--c-*` variables under a `html.<theme>` selector
* re-skins the whole app with no per-component changes.
*
* `dark` is the baseline defined on :root; every other theme only needs to
* override what differs. `light`/`grey` swap the full surface+text scale;
* the seven colour themes (red/green/blue/yellow/pink/orange/grey... ) keep
* `dark`'s neutral surfaces and swap only the accent trio, since accent is
* the only place the app uses a non-neutral colour (selection states, the
* timeline histogram, focus/active highlights).
* Derivation: surfaces/borders/text are always neutral (S=0) greys, never
* hue-tinted. `dark`/`grey` share one true black-based grey scale, `light`
* is its exact lightness mirror. Each named theme (red/green/blue/yellow/
* pink/orange) takes its hue's real Tailwind swatch at each scale step,
* converts it to HSL, zeroes the saturation, and keeps that lightness as
* its grey — so e.g. red's surface-0 is grey at red-950's lightness, not
* neutral-950. Colour survives only on --c-accent*. To add a new themed
* variant: pick its hue's Tailwind swatches at 950/900/800/700/600/500/
* 400/300/100, desaturate each (HSL S->0), and slot the greys into the
* same --c-surface-*/--c-border-*/--c-text-* roles used below.
*/
:root {
color-scheme: light dark;
@@ -45,22 +49,24 @@
--c-accent-text: #7dd3fc;
}
/* Exact lightness mirror of :root's neutral-950..50 scale (Tailwind neutral,
steps flipped: 950<->50, 900<->100, 800<->200, 700<->300, 600<->400). */
html.light {
--c-surface-0: #fafaf9;
--c-surface-1: #f5f5f4;
--c-surface-2: #e7e5e4;
--c-surface-0: #fafafa;
--c-surface-1: #f5f5f5;
--c-surface-2: #e5e5e5;
--c-border: #d6d3d1;
--c-border-soft: #a8a29e;
--c-border-faint: #e7e5e4;
--c-border-strong: #57534e;
--c-border: #e5e5e5;
--c-border-soft: #d4d4d4;
--c-border-faint: #f5f5f5;
--c-border-strong: #525252;
--c-text: #1c1917;
--c-text-secondary: #44403c;
--c-text-muted: #78716c;
--c-text-subtle: #a8a29e;
--c-text-faint: #d6d3d1;
--c-text-faintest: #d6d3d1;
--c-text: #171717;
--c-text-secondary: #404040;
--c-text-muted: #525252;
--c-text-subtle: #737373;
--c-text-faint: #a3a3a3;
--c-text-faintest: #d4d4d4;
--c-danger-bg: #fee2e2;
--c-danger-border: #fca5a5;
@@ -72,59 +78,163 @@ html.light {
--c-accent-text: #0369a1;
}
/* Canonical greyscale theme: identical neutral scale to :root (dark), so
every other theme's derivation above is measured against this one. */
html.grey {
--c-surface-0: #020617;
--c-surface-1: #0f172a;
--c-surface-2: #1e293b;
--c-surface-0: #0a0a0a;
--c-surface-1: #171717;
--c-surface-2: #262626;
--c-border: #1e293b;
--c-border-soft: #334155;
--c-border-faint: #0f172a;
--c-border-strong: #94a3b8;
--c-border: #262626;
--c-border-soft: #404040;
--c-border-faint: #171717;
--c-border-strong: #a3a3a3;
--c-text: #f1f5f9;
--c-text-secondary: #cbd5e1;
--c-text-muted: #94a3b8;
--c-text-subtle: #64748b;
--c-text-faint: #475569;
--c-text-faintest: #334155;
--c-text: #f5f5f5;
--c-text-secondary: #d4d4d4;
--c-text-muted: #a3a3a3;
--c-text-subtle: #737373;
--c-text-faint: #525252;
--c-text-faintest: #404040;
--c-accent: #38bdf8;
--c-accent-soft: #0369a1;
--c-accent-text: #7dd3fc;
}
/* red-950..300/100 desaturated (S->0), lightness kept */
html.red {
--c-surface-0: #282828;
--c-surface-1: #4e4e4e;
--c-surface-2: #5a5a5a;
--c-border: #5a5a5a;
--c-border-soft: #6a6a6a;
--c-border-faint: #4e4e4e;
--c-border-strong: #b4b4b4;
--c-text: #f0f0f0;
--c-text-secondary: #d0d0d0;
--c-text-muted: #b4b4b4;
--c-text-subtle: #9a9a9a;
--c-text-faint: #818181;
--c-text-faintest: #6a6a6a;
--c-accent: #f87171;
--c-accent-soft: #b91c1c;
--c-accent-text: #fca5a5;
}
/* green-950..300/100 desaturated (S->0), lightness kept */
html.green {
--c-surface-0: #1a1a1a;
--c-surface-1: #343434;
--c-surface-2: #3d3d3d;
--c-border: #3d3d3d;
--c-border-soft: #4a4a4a;
--c-border-faint: #343434;
--c-border-strong: #949494;
--c-text: #ececec;
--c-text-secondary: #bbbbbb;
--c-text-muted: #949494;
--c-text-subtle: #747474;
--c-text-faint: #5c5c5c;
--c-text-faintest: #4a4a4a;
--c-accent: #4ade80;
--c-accent-soft: #15803d;
--c-accent-text: #86efac;
}
/* blue-950..300/100 desaturated (S->0), lightness kept */
html.blue {
--c-surface-0: #363636;
--c-surface-1: #545454;
--c-surface-2: #666666;
--c-border: #666666;
--c-border-soft: #7a7a7a;
--c-border-faint: #545454;
--c-border-strong: #adadad;
--c-text: #ececec;
--c-text-secondary: #c8c8c8;
--c-text-muted: #adadad;
--c-text-subtle: #989898;
--c-text-faint: #888888;
--c-text-faintest: #7a7a7a;
--c-accent: #60a5fa;
--c-accent-soft: #1d4ed8;
--c-accent-text: #93c5fd;
}
/* yellow-950..300/100 desaturated (S->0), lightness kept */
html.yellow {
--c-surface-0: #242424;
--c-surface-1: #424242;
--c-surface-2: #4a4a4a;
--c-border: #4a4a4a;
--c-border-soft: #545454;
--c-border-faint: #424242;
--c-border-strong: #888888;
--c-text: #e0e0e0;
--c-text-secondary: #a2a2a2;
--c-text-muted: #888888;
--c-text-subtle: #797979;
--c-text-faint: #676767;
--c-text-faintest: #545454;
--c-accent: #facc15;
--c-accent-soft: #a16207;
--c-accent-text: #fde047;
}
/* pink-950..300/100 desaturated (S->0), lightness kept */
html.pink {
--c-surface-0: #2c2c2c;
--c-surface-1: #4e4e4e;
--c-surface-2: #5a5a5a;
--c-border: #5a5a5a;
--c-border-soft: #6b6b6b;
--c-border-faint: #4e4e4e;
--c-border-strong: #b3b3b3;
--c-text: #f2f2f2;
--c-text-secondary: #d0d0d0;
--c-text-muted: #b3b3b3;
--c-text-subtle: #9a9a9a;
--c-text-faint: #818181;
--c-text-faintest: #6b6b6b;
--c-accent: #f472b6;
--c-accent-soft: #be185d;
--c-accent-text: #f9a8d4;
}
/* orange-950..300/100 desaturated (S->0), lightness kept */
html.orange {
--c-surface-0: #252525;
--c-surface-1: #474747;
--c-surface-2: #565656;
--c-border: #565656;
--c-border-soft: #676767;
--c-border-faint: #474747;
--c-border-strong: #9b9b9b;
--c-text: #eaeaea;
--c-text-secondary: #b8b8b8;
--c-text-muted: #9b9b9b;
--c-text-subtle: #888888;
--c-text-faint: #7b7b7b;
--c-text-faintest: #676767;
--c-accent: #fb923c;
--c-accent-soft: #c2410c;
--c-accent-text: #fdba74;