swarm-ui: fix WCAG contrast failures in the light-theme accent slots

argus review: the literal Catppuccin Latte accent hexes (base08-0F)
fail badly as StatusChip fill-text (green/amber/red/yellow on
--purple-dim/base03: 1.4:1-3:1, need 4.5:1) and even as plain text on
--bg/base00 in the agent/dashboard packages that also import this file
(2.3:1-4.8:1). Root cause: Latte's own accents are calibrated against
Lattes near-white base/crust, not a mid-gray surface1; the Mocha row
does not hit this because Mochas pastel accents are already light, so
they contrast fine against a dark surface1 - Lattes saturated-but-mid
accents do not have the same headroom against Lattes own lighter
surface1.

Fix: darken each accent slot from stock Latte (same hue/saturation,
lower HSL lightness via binary search) until real WCAG contrast against
base03 clears 4.5:1 with margin - verified this also clears >7:1
against base00, so both the swarm-ui chip case and the plain-text case
elsewhere are covered by one set of values. base00-07 stay stock Latte
(no contrast role, just surfaces/foreground). Yellow and peach read
closer to olive/brown than a bright yellow/orange once darkened this
far - an inherent property of darkening a warm hue in sRGB, not a
mapping mistake; boosting saturation further did not rescue it
(checked).

Verified with headless-chromium screenshots against the built swarm-ui
bundle: StatusChip text (the exact case argus flagged) and every chip
tone on /components now read clearly.
This commit is contained in:
iris 2026-08-18 22:57:47 +02:00 committed by mara
commit 72e087b086
2 changed files with 53 additions and 20 deletions

View file

@ -18,12 +18,17 @@ for the raw `--baseNN` slots directly. (`base.css` holds only the shared
`body` typography — it references the palette but no longer defines it.) `body` typography — it references the palette but no longer defines it.)
`colors.css` ships two rows of the same 16 slots: an unconditional `colors.css` ships two rows of the same 16 slots: an unconditional
Catppuccin Mocha (dark) default, and a Catppuccin Latte (light) row gated Catppuccin Mocha (dark) default, and a Catppuccin-Latte-derived (light)
behind `@media (prefers-color-scheme: light)` — both ported through the row gated behind `@media (prefers-color-scheme: light)` — both use the
identical slot mapping, so which row is active never changes what a slot identical slot mapping, so which row is active never changes what a slot
means. This is the OS/browser-level default only; it's what a client with means. The light row's `base00`-`base07` (surfaces/foreground) are stock
no more specific theme decision gets (see "Theme swapping" below for how a Latte hexes; `base08`-`base0F` (the eight chromatic slots) are darkened
stylix deployment or a future per-user override supersede it). from stock Latte to actually clear WCAG AA against how this app uses
them (real contrast failures found in review — see the comment above
those declarations in `colors.css` for the numbers and why). This is the
OS/browser-level default only; it's what a client with no more specific
theme decision gets (see "Theme swapping" below for how a stylix
deployment or a future per-user override supersede it).
Both are deliberately standalone, not `@import`ed into the page bundles: Both are deliberately standalone, not `@import`ed into the page bundles:
each package re-exports them (`src/{colors,theme}.css` each package re-exports them (`src/{colors,theme}.css`

View file

@ -30,13 +30,16 @@
--base0F: #f5c2e7; /* extra accent — pink */ --base0F: #f5c2e7; /* extra accent — pink */
} }
/* Light-mode default Catppuccin Latte, the standard light complement /* Light-mode default. base00-07 (surfaces/foreground) are stock
to the Mocha defaults above, ported through the identical base16 Catppuccin Latte no contrast issue there, they're the ends of the
slot mapping (down to the same base0C sky-over-teal substitution) so bg/fg scale. base08-0F (the eight chromatic "tone" slots: red through
a swap between the two rows never changes which semantic role a slot pink) are deliberately NOT stock Latte hexes see the comment right
plays. This is a *default*, not a user preference store: it only above those declarations below for why; don't reuse stock Catppuccin
applies when nothing more specific has already decided the palette. Latte's own accent hexes there if this ever gets regenerated.
A stylix-generated colors.css (nix/host-modules/hive-c0re/theme.nix)
This is a *default*, not a user preference store: it only applies
when nothing more specific has already decided the palette. A
stylix-generated colors.css (nix/host-modules/hive-c0re/theme.nix)
replaces this entire file wholesale a fixed, unconditional `:root` replaces this entire file wholesale a fixed, unconditional `:root`
block with no media query in it at all so a stylix-themed block with no media query in it at all so a stylix-themed
deployment is naturally unaffected by, and never fights, this block. deployment is naturally unaffected by, and never fights, this block.
@ -59,13 +62,38 @@
--base05: #4c4f69; /* default foreground */ --base05: #4c4f69; /* default foreground */
--base06: #dc8a78; /* light foreground */ --base06: #dc8a78; /* light foreground */
--base07: #7287fd; /* lightest */ --base07: #7287fd; /* lightest */
--base08: #d20f39; /* red */ /* base08-0F: darkened from stock Catppuccin Latte's own accent
--base09: #fe640b; /* orange / peach */ hexes (same hue/saturation, lower HSL lightness), not a literal
--base0A: #df8e1d; /* yellow */ upstream port like base00-07 above. argus's review on this PR
--base0B: #40a02b; /* green */ computed real WCAG contrast ratios and found the literal Latte
--base0C: #04a5e5; /* cyan (our sky — note: Catppuccin's base0C is teal) */ accents fail badly as `StatusChip` fill-text (green/amber/red/
--base0D: #1e66f5; /* blue */ yellow on `--purple-dim`/base03: 1.4:1-3:1, need 4.5:1) and even
--base0E: #8839ef; /* magenta / mauve */ as plain text on `--bg`/base00 in the agent/dashboard packages
--base0F: #ea76cb; /* extra accent — pink */ that also consume this same file (2.3:1-4.8:1). Root cause:
Latte's own accent colors are calibrated against Latte's
near-white `base`/`crust`, not against a mid-gray `surface1`
the Mocha row doesn't have this problem because Mocha's pastel
accents are already *light*, so they contrast fine against a
*dark* surface1; Latte's saturated-but-mid-brightness accents
don't have the equivalent headroom against Latte's own
(much lighter) surface1. Each value below was picked by holding
the stock Latte hue+saturation fixed and binary-searching HSL
lightness down to the point real contrast against base03 clears
4.5:1 with a small margin (also verified against base00: all
land above 7:1 there) same hue identity, same base16 role,
just dark/saturated enough to actually be legible as filled-chip
or plain text either way. Yellow and peach read closer to
olive/brown than a bright yellow/orange once darkened this far
an inherent property of darkening a warm hue in sRGB (Bezold-
Brücke shift), not a mapping mistake; boosting saturation
further didn't rescue it (verified). */
--base08: #9c0b2a; /* red */
--base09: #883201; /* orange / peach */
--base0A: #6d450e; /* yellow */
--base0B: #235818; /* green */
--base0C: #025374; /* cyan (our sky — note: Catppuccin's base0C is teal) */
--base0D: #0843b8; /* blue */
--base0E: #6311ce; /* magenta / mauve */
--base0F: #8f166e; /* extra accent — pink */
} }
} }