From 9e58a36915bb1fe8cf88423c9519c0c12dbbf2e2 Mon Sep 17 00:00:00 2001 From: Christian Cleberg Date: Mon, 20 Jul 2026 20:25:01 -0500 Subject: fix(a11y): rebalance the light palette so hues survive MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Reported as "colors seem muted and hard to see on light mode", and correct. The light palette optimised contrast and produced mud: #7A5600 reads olive rather than amber, #146C2E bottle-dark rather than green. Contrast passed while the UI got harder to read, because hue identity is what distinguishes warning from critical at a glance. Two causes, both fixed. Every foreground was required to clear 4.5:1 against its own 16% badge tint — the harshest surface it ever sits on — which pushed each colour about 20% darker than the common case needed. Most of what is actually on screen is plain text on a card, with far more headroom. The fill is now decoupled from the foreground: AppStatusTone carries a foreground and a surface authored independently, with matching …Surface colorsets, so a foreground no longer has to survive a wash of itself. Every status foreground is now fully saturated. And warning was yellow. Yellow cannot stay yellow at a lightness low enough to pass 4.5:1 on white — it becomes olive. That is colorimetric, not a tuning problem. Warning is now orange: #AD5100 light, #FF9F0A dark. New light values: positive #008035, warning #AD5100, critical #CC0700. Worst-case ratios 4.54–6.76 across page, card, and surface in both schemes. Audit findings are unchanged — light 21, dark 18 — so the vividness costs nothing. Also picks up a literal .blue missed in phase 1: DomainDiffItem's low-severity change colour, which the phase 1 sweep did not cover because its pattern listed only cyan/yellow/green/red/orange/pink. --- Docs/ACCESSIBILITY.md | 36 +++++++++++++++++++++++++++++++----- 1 file changed, 31 insertions(+), 5 deletions(-) (limited to 'Docs') diff --git a/Docs/ACCESSIBILITY.md b/Docs/ACCESSIBILITY.md index 7153381..40acc1c 100644 --- a/Docs/ACCESSIBILITY.md +++ b/Docs/ACCESSIBILITY.md @@ -24,11 +24,37 @@ worst of those three is shown: | Role | Light | Dark | Worst light | Worst dark | | --- | --- | --- | --- | --- | -| `StatusInfo` / `AccentColor` | `#0000FF` | `#4DA3FF` | 5.50 | 5.85 | -| `StatusPositive` | `#146C2E` | `#30D158` | 4.65 | 7.32 | -| `StatusWarning` | `#7A5600` | `#FFD60A` | 4.74 | 9.61 | -| `StatusCritical` | `#B3261E` | `#FF6961` | 4.51 | 5.54 | -| `StatusNeutral` | `#5A5A5F` | `#A1A1A6` | 4.92 | 5.88 | +| `StatusInfo` / `AccentColor` | `#0000FF` | `#4DA3FF` | 6.76 | 6.47 | +| `StatusPositive` | `#008035` | `#30D158` | 4.54 | 7.62 | +| `StatusWarning` | `#AD5100` | `#FF9F0A` | 4.59 | 7.76 | +| `StatusCritical` | `#CC0700` | `#FF6961` | 4.68 | 6.12 | +| `StatusNeutral` | `#5A5A5F` | `#A1A1A6` | 5.84 | 6.76 | + +Each status foreground has a matching `…Surface` colour for the fill behind it, +paired through `AppStatusTone`. + +### Contrast alone is not a palette + +The first version of this palette maximised contrast and produced mud. Requiring +every foreground to clear 4.5:1 against *its own 16% tint* — the harshest +surface it ever sits on — pushed each colour ~20% darker than the common case +needed. `#7A5600` is not amber, it is olive; `#146C2E` is not green so much as +bottle-dark. Contrast passed and the UI was still hard to read, because hue +identity is what tells "warning" from "critical" at a glance. + +Two fixes: + +1. **Decouple the fill from the foreground.** `AppStatusTone` carries a + `foreground` and a `surface` that are authored independently, so the + foreground no longer has to survive a wash of itself. Every status foreground + is now fully saturated (`S = 1.0`). +2. **Warning is orange, not yellow.** Yellow cannot stay yellow at a lightness + low enough to clear 4.5:1 on white — it *becomes* olive. That is + colorimetric, not a tuning problem. Orange holds its identity when darkened, + so warning is `#AD5100` in light and `#FF9F0A` in dark. + +When adding a colour, search for the most saturated value that passes, not the +darkest. The darkest is always easy and always wrong. | `AppTextSecondary` | `#5A5A5F` | `#A1A1A6` | 6.15 | 7.50 | `AppTextSecondary` replaces `.secondary` for body text. iOS's own `secondaryLabel` -- cgit v1.2.3