commit 7bb1df046879f7b8afe2b2ff353f78f16e9dfe46
parent c2bc533abfdb7f617fc33f23f94dddf0912cdd45
Author: I Mean I'm Just Saying <imeanimjustsaying@kiwifarms.st>
Date: Fri, 25 Sep 2026 22:26:14 -0400
common: sepia status colours read at 4.5:1 on their own soft fills (brand S2 review)
The sheet promises that each status colour reads as TEXT on its -soft fill.
Sepia fell short: warning 4.32 and info 4.46 over the ground. They are now
#8c4c00 (4.65 on its fill over the ground, 4.96 over a card; 5.66 on the
ground) and #1858bc (4.74 / 5.07; 5.64), each with its -soft rgba to match.
The new test found two more:
- sepia success was 4.49, and is now #256829 (4.73 / 5.05);
- dark gone over a card was 4.37: its -soft alpha is now 0.12 (4.94 / 4.60).
themeTokens.test.ts now checks success, warning, info, destructive and gone on
their soft fills over the ground and a card, on sepia and dark. Light's
success and warning (4.18 / 3.89) predate the themes and are left, as noted.
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Diffstat:
2 files changed, 44 insertions(+), 7 deletions(-)
diff --git a/common/components/themeTokens.test.ts b/common/components/themeTokens.test.ts
@@ -202,3 +202,38 @@ test("text tokens read at 4.5:1 on each base's ground and card", () => {
assert.ok(inkOn >= 4.5, `${base}: primary-foreground on primary is ${inkOn.toFixed(2)}:1`);
}
});
+
+// "Soft = a low-alpha tint of the hue for badge/alert fills; the base color is
+// tuned to read as TEXT on that soft fill" (tokens.css @theme). Sepia and dark
+// keep that promise on the ground and on a card. The light block's success and
+// warning (4.18 / 3.89 on their soft fills) predate the base × accent themes
+// and are not covered here.
+function compositeOver(rgba: string, ground: string): string {
+ const m = /^rgba\((\d+),\s*(\d+),\s*(\d+),\s*([\d.]+)\)$/.exec(rgba);
+ assert.ok(m, `not an rgba(): ${rgba}`);
+ const a = Number(m[4]);
+ const g = parseInt(ground.slice(1), 16);
+ const G = [(g >> 16) & 255, (g >> 8) & 255, g & 255];
+ const out = [1, 2, 3].map((i, k) => Math.round(Number(m[i]) * a + G[k] * (1 - a)));
+ return `#${out.map((v) => v.toString(16).padStart(2, "0")).join("")}`;
+}
+
+test("status text reads at 4.5:1 on its own soft fill (sepia and dark)", () => {
+ const PAIRS = [
+ ["--success", "--success-soft"],
+ ["--warning", "--warning-soft"],
+ ["--info", "--info-soft"],
+ ["--destructive", "--destructive-soft"],
+ ["--state-gone", "--state-gone-soft"],
+ ] as const;
+ for (const base of ["sepia", "dark"] as const) {
+ const r = rule(BASE_SELECTOR[base]);
+ for (const bg of ["--background", "--card"]) {
+ for (const [text, soft] of PAIRS) {
+ const fill = compositeOver(r.decls.get(soft)!, r.decls.get(bg)!);
+ const ratio = contrastRatio(r.decls.get(text)!, fill);
+ assert.ok(ratio >= 4.5, `${base}: ${text} on ${soft} over ${bg} is ${ratio.toFixed(2)}:1`);
+ }
+ }
+ }
+});
diff --git a/common/styles/tokens.css b/common/styles/tokens.css
@@ -247,15 +247,17 @@ html[data-base="sepia"] {
--panel: rgba(250, 244, 230, 0.72);
--panel-2: rgba(236, 226, 202, 0.72);
- --success: #276c2b;
+ /* Each deep enough to read at 4.5:1 on its OWN soft fill over the ground
+ and the card, as the @theme note promises (themeTokens.test.ts). */
+ --success: #256829;
--success-foreground: #ffffff;
- --success-soft: rgba(39, 108, 43, 0.14);
- --warning: #945000;
+ --success-soft: rgba(37, 104, 41, 0.14);
+ --warning: #8c4c00;
--warning-foreground: #ffffff;
- --warning-soft: rgba(148, 80, 0, 0.14);
- --info: #1a5cc4;
+ --warning-soft: rgba(140, 76, 0, 0.14);
+ --info: #1858bc;
--info-foreground: #ffffff;
- --info-soft: rgba(26, 92, 196, 0.12);
+ --info-soft: rgba(24, 88, 188, 0.12);
/* Each named accent's on-sepia value (lib/brand.ts ACCENTS.onSepia). */
--swatch-signal: #2b756e;
@@ -349,7 +351,7 @@ html[data-base="dark"] {
--brand-ink: #0c0a08;
--state-gone: #d9644f;
- --state-gone-soft: rgba(217, 100, 79, 0.16);
+ --state-gone-soft: rgba(217, 100, 79, 0.12);
/* Same hue order, stepped into the validator's dark lightness band.
Adjacent pairs pass every check; all pairs pass with green ↔ magenta in