Lane G — palette engineering: are the six roles sound, distinguishable, and built the way the best systems build them?
Status: Reference · Date: 2026-10-09 · Evidence appendix for colour-system-research-2026-10-09.md. Scan scripts referenced by path live in the session scratch directory (not committed); every number comes from the shared helper whose formulas are reproduced in §3.
Date: 2026-10-09 · Author: lane G · Evidence for docs/design/research/visual-benchmark-2026-10-08.md §3 and §5 · English only · repo files were read, never modified. How to read the numbers. Every colour number (ΔE2000, Lc, WCAG ratio, L*/C*/h, OKLCH, CVD-simulated hex) comes from bench/colour_lab.py (functions apca, wcag, lab/lch/oklch, de2000, cvd = Machado 2009 severity 1.0, cvd_table). Searches and scans are scripts in bench/lane_g/ that import the helper (lg.py adds only OKLCH→hex conversion and a gamut check). Three computations need a model the helper does not have and are marked: CAT16 adaptation (adapt.py, matrix verified against colour-science 0.4.7), Helmholtz–Kohlrausch Γ (colour-science 0.4.7, Nayatani 1997), and the peer hexes (read from published npm packages). APCA font-size minima use the helper's published-LUT version (coordinator note 2026-10-09; Lc values are unchanged). Vocabulary. Lc = APCA lightness contrast (|Lc|; positive = dark text on light). "n/p/d/t" = ΔE2000 under normal / protan / deutan / tritan vision. "OI parity" = the worst pairwise ΔE2000 of Okabe–Ito's own palette under the same simulation (21.7 / 12.3 / 11.5 / 11.1, table D).
0. The six verdicts that matter
- Brand
#006D37is sound and stays; success#14804Ais not. Brand vs success is ΔE2000 6.7 / 7.0 / 6.4 / 7.0 under all four visions — the same colour at a glance — and#14804Ais also under the APCA 75 line on white (Lc 74.0). The ΔE ≥ 20 target is not reachable while staying green: inside the text-contrast window (on-white Lc ≥ 75, white-on Lc ≥ 60) the best min(n,p,d) against brand is 11.3 at OKLCH hue 170, 14.1 at 180, and only crosses 20 at hue ~195 (blue-teal) — table P. Final rule: meet Okabe–Ito parity (≥ 12 under protan/deutan, ≥ 20 normal) for every pair that can share a component, and carry meaning with glyph + label. Final values — two tiers, as Polaris and Atlassian split text / icon / fill:$success#00796Afor icons, fills and large figures (dark#75D1C6): ΔE vs brand 13.4 / 14.7 / 17.2, vs error 55.5 / 16.2 / 24.2 (was 61.9 / 13.2 / 9.5), on-white Lc 75.9, white-on Lc 81.3; and$successText#014B40for small text and badge text (coincides with Polaris'text-success, which is where the candidate came from; scan-confirmed; L* 27.9, Lc 93.0 on white, 85.7 on the tint): ΔE vs brand 16.3 / 17.6 / 18.1, vs the neutral badge text 23.6 / 13.6 / 13.1, vs error 51.5 / 12.9 / 28.3. The two-tier split is forced by a second finding: a plain teal (#00796A) as text turns into the same mid-grey as the neutral badge under deutan (ΔE 3.0, bg 0.7), whereas the deep teal does not (13.1).#00796Asits at the hue of the shipped "open/recruiting" accent badge, so success and accent become one teal and brand is the only grass-green. - The luminance-only separation the brief asks for is impossible inside the text window; the fill tier supplies it. Text-capable mid-tones must sit at L* ≲ 47 (Lc 75 on white falls at L* 46.7); brand 39.9, error 42.4, success fill 45.2 differ by ≤ 5.3 L*. A ΔL* of 19.5–21.9 gives only 2.0–2.2:1, not the 3:1 luminance difference SC 1.4.1 accepts; 3:1 against error needs L* ≥ 75. Only warning as a bright fill with ink text (
#F5B301, L* 77.1) does it: vs error WCAG 3.19:1, ΔE 46.4 / 40.4 / 29.5 (current deadline-vs-live banners: 27.6 / 12.1 / 4.2). Red/green pairs outside that tier are separated by hue on the blue–yellow axis (teal vs red) plus glyph, as Okabe–Ito prescribe. - Optic lime
#C6E84Bis correct as a low-area level accent, but the stated reason is half wrong. It is the brightest role (Y 0.702, ink numerals Lc 84.1) — that is V(λ)/luminance. Helmholtz–Kohlrausch does the opposite of what the brief assumes: Nayatani Γ (VCC) is 0.930 for optic, among the lowest of any role (warning amber 0.890; error 1.311, info 1.590). It must never be text or a bare shape on white (Lc 19.1 on white, 13.2 on the basement — under APCA's Lc 15 invisibility line), and it merges with warning amber under deutan (5.8) and turns pale grey-green under tritan (#D1DCC9). - The content hues collide with the semantic ones; fix three pairs, not "add colours". Court grass
#006633vs brand 2.3 (identical); court clay vs grass protan 1.7; clay#B84A2Evs error 6.6 (deutan 2.9); tiers: bronze vs grandmaster deutan 3.4, gold on white 2.38:1. Proposed: one 5-series set that also is the court trio (#3566B8,#8D3D00,#83971F,#883170,#089AC3; n/p/d/t min 22.6 / 15.1 / 14.9 / 8.8, all ≥ 3:1 on white and basement), tiers bronze#B2580B, gold#D9A00F, grandmaster#9A0F3A(min 25.1 / 22.7 / 18.1 / 11.4 vs 14.8 / 10.0 / 3.4 / 8.7 now). Strictly equal-L is not viable: the best equal-L 5-set at OKLCH L 0.60 has min ΔE 11.5 / 9.3 / 8.2 / 5.2; staggering L by ±0.08 lifts deutan to 15.0 — Okabe–Ito and Tol also stagger (L* spread 43 / 36). - Neutrals are inside the industry envelope; the tint cannot shift the brand. Canvas C* 1.5, basement 3.2, track 3.0, hairline 4.2 vs Radix sage steps 1–8 C* 0.9–1.8, Wise's green-tinted surface 2.8, iOS grouped 2.6. CAT16 appearance shift of
#006D37on the basement: ΔE2000 0.76 (full von Kries, upper bound) / 0.55 (CAM16 D = 0.85) — at or below the ~1 level conventionally treated as a just-noticeable difference. Recommended ceiling: Lab C* ≤ 4.5 for L* ≥ 88, hue 145–165. - Dark parity fails in three places today: dark
$errortext#D4736Eis Lc 40.5 on the card (the gate has no APCA watch on$error; helper LUT: 58 px at weight 400); dark success#68BF8Evs dark primary#70D5A2is ΔE 5.5 (the same collision as light); and the dark deadline/live banners are ΔE 3.1 under deutan. Proposed dark: error#FA9B94(Lc 60.4), success#75D1C6(ΔE vs primary 12.0 / 13.9 / 16.3), warning text#E1B75C(Lc 64.9), dark deadline banner = amber#E2B13Cwith ink (ΔE vs live 62.2 / 56.5 / 55.9), hero block#1E5236(optic 6.50:1).
1. Verdict table per role
Legend: keep = no change; hex pairs are light / dark. Numbers are helper outputs; full tables in §2.
| Role | Current | Issue | Evidence (URL) | Final (light / dark) | Key numbers |
|---|---|---|---|---|---|
| Brand solid (CTA, hero block, toggle on) | #006D37 / #70D5A2 | none; L* 39.9 / 78.3 are the Material tone-40 / tone-80 logic | Material primary tone 40 light, 80 dark, on-primary 100/20: color_spec_2021.ts | keep #006D37 / #70D5A2 | white-on Lc −86.6 (6.48:1; 16.3 px @400, 14 px @700); dark ink-on #70D5A2 Lc 70.4; #70D5A2 on card Lc −67.8 |
| Brand as text | $primary on white/basement | fine for links ≥ 16 px/500; the helper LUT wants ≥ 15.8 px @500 on white (Lc 81.5) and 16 px @500 on basement (Lc 75.7) | APCA nutshell (Lc 75 = body-column minimum) | keep; inline links only (budget §5) | on basement Lc 75.7, WCAG 5.95 |
| Success | #14804A / #68BF8E | ΔE vs brand 6.7/7.0/6.4/7.0; on-white Lc 74.0 < 75; vs error deutan 9.5; dark vs dark primary 5.5 | Polaris success fill #047B5D (h168) and text #014B40, brand fill #303030 — @shopify/polaris 13.9.5 styles.css; Seed positive #079171 (h170) — daangn/seed-design color.yaml; Wise positive #054D28 — neptune-tokens; Okabe–Ito: bluish green is the recommended green so it is not confused with red or brown — jfly.uni-koeln.de/color; Polaris: "Avoid using green to entice merchants or to draw unnecessary attention" — shopify.dev | $success #00796A / #75D1C6 (OKLCH 0.516/0.094/180) for icon, fill, figure; $successText #014B40 / #75D1C6 for text; tint #E0F8F3 / #14433E | #00796A: vs brand 13.4/14.7/17.2/6.0; vs error 55.5/16.2/24.2/58.9; on-white Lc 75.9 (17.9 px @400); white-on Lc −81.3. #014B40: Lc 93.0 on white (15.7 px @400), 85.7 on tint (9.07:1); vs brand 16.3/17.6/18.1/10.4; vs neutral badge text 23.6/13.6/13.1/18.5. Dark #75D1C6: Lc 67.6 on card; vs dark primary 12.0/13.9/16.3/1.8; vs dark neutral text 21.9/6.7/3.0 |
| Warning | #946200 (text, fill, icon) / #D4A849 | text-on-fill and fill both at L* 45.7, same lightness as error 42.4: deutan 4.2; deadline vs live banner identical to deutan; peers use a bright fill + dark ink | Polaris warning fill #FFB800 + text #5E4200; Atlassian warning.bold #FBC828 with inverse text #292A2E; Carbon yellow30 #F1C21B; Wise #FFD11A — packages and Carbon, @atlaskit/tokens 20.4.0, @transferwise/neptune-tokens 8.20.2 | keep $warning #946200 for icons/graphics (5.24:1); add $warningText #6B4A00 / #E1B75C; add $warningFill #F5B301 / #E2B13C + $onWarningFill #0D100E | fill vs error 46.4/40.4/29.5/30.7 (WCAG 3.19:1); text vs error 28.9/7.4/10.3/17.4 (was 27.6/12.1/4.2/12.0); text on tint Lc 81.4 (was 69.4), 7.36:1 (was 4.78); ink on fill Lc 68.8 |
| Error | #B8332E / #D4736E | light is right: OKLCH h 27.0 vs peers 24.5–28.8; dark text is Lc 40.5 | Polaris critical #C70A24 (h24.5), Wise #CB272F (h24.8), Carbon red60 #DA1E28 (h25.9), Atlassian #C9372C (h28.8), M3 error hue 25 chroma 84 — core_palette.ts | light keep; dark #FA9B94 (M3 tone-80 #FFB4AB is Lc 70.7 if the protan/deutan price vs dark primary is accepted) | light white-on Lc −83.6, on-white 78.3; dark Lc 40.5 → 60.4 (min px @500 46.5 → 20.8). Irreducible: brand vs error protan 6.5 for every red tested (table B2, scan in §2.2) |
| Info | #4B0082 / #C7B5F2 | none in the set; purple is shared with tier master (h 302 vs 315) | — | keep | on-white Lc 97.2 (12.95:1); vs brand 50.5/50.6/44.3/36.7; dark Lc −65.7 |
| Optic (level accent) | #C6E84B | zero consumers today; not a surface colour (Lc 19.1 on white, 13.2 on basement) | APCA Lc 15 = "invisible" for non-text, Lc 30 = absolute minimum for spot text — APCA nutshell; Nayatani Γ — colour-science hke | keep #C6E84B both modes; $levelChipBg = optic, $levelChipText = #0D100E | ink-on Lc 84.1 (13.71:1; 16.6 px @400); on brand block Lc −63.2 (4.64:1), on #1E5236 6.50:1; deutan sim #F2DA57; tritan sim #D1DCC9 (ΔE vs basement 10.2) |
| Ink / neutral | gray ladder, canvas #F8FBF9, basement #F0F7F3, track #E5EBE7 | within envelope; basement is 2.7× Radix sage chroma | Radix tinted greys pair with accent hue — Radix scales; M3 neutral chroma 4 / variant 8 — core_palette.ts | keep all; dark $controlTrack #374039, $controlPill #515B53 | basement ΔL* 3.4 vs white; track vs white 1.209:1, vs basement 1.111:1; dark track vs card 1.544:1 |
| Court trio | hard #247BA0, clay #B84A2E, grass #006633 | grass = brand (ΔE 2.3); clay ≈ error (6.6; deutan 2.9); clay vs grass protan 1.7 | Okabe–Ito: avoid colours between yellow and green, and red/green combinations — jfly.uni-koeln.de/color | alias the series: hard #089AC3, clay #8D3D00, grass #83971F (dark #67D3FC, #DA875A, #BBD068) | trio n/p/d/t min 42.6 / 27.4 / 20.0 / 22.2; grass vs brand 25.9/20.5/23.8/26.1; clay vs error 15.9/7.4/7.5/8.5 |
| Tier quintet | bronze #CD7F32, silver #8E8E93, gold #D4A017, master #9B59B6, grandmaster #E74C3C | bronze–GM deutan 3.4; bronze–gold deutan 7.5; gold 2.38:1 on white | WCAG 1.4.11 3:1 — Understanding | bronze #B2580B, gold #D9A00F, grandmaster #9A0F3A; dark bronze #DA8A11, grandmaster #D8516A | light set min n/p/d/t 25.1/22.7/18.1/11.4 (was 14.8/10.0/3.4/8.7); dark 19.7/14.6/12.8/11.2 (was 19.7/13.2/4.8/10.6). Gold still needs an ink outline (2.34:1) |
| Series 1–5 (charts) | none | no governed chart palette; Carbon/Spectrum/Tol all publish one | Carbon first 5: min 25.7/9.5/15.8/18.0 — Carbon; Tol — sronpersonalpages.nl/~pault | $series1–5 light #3566B8 #8D3D00 #83971F #883170 #089AC3; dark #6C9EF1 #DA875A #BBD068 #D57CB8 #67D3FC | light min 22.6/15.1/14.9/8.8, ≥ 3:1 on white and basement; dark min 18.9/10.0/10.1/7.1, 5.85–9.74:1 on card |
| Club identity hue | continuous hue picker (hueFlagPalette, hsl) | not a fixed set; cannot be CVD-gated | — | no change; rule: identity is never colour-only (monogram/photo always present) | — |
| Hue budget | brand, ink, neutral, semantic ×4, optic, content ≤ 5 | 10 hue families exist in the token set (27 error, 48 clay, 80 warning, 119–121 optic/grass, 153 brand, 180 success, 225 hard, 260 blue, 302 info, 340 plum); ≤ 7 per viewport is the defensible cap | Healey 1996: "up to seven isoluminant colors" identified rapidly — Healey | per-viewport cap 7 hue families, role-exclusive contexts (§2.7) | — |
2. Evidence by question
2.1 Brand vs success — how green-brand systems keep a distinct success colour
What the peers actually do (read from published packages, not screenshots; table I). Only Wise is a green-brand system whose tokens could be read; the others show where a "positive" colour goes relative to their own brand.
- Wise (lime brand):
interactive-primary#163300(ink-green),interactive-accent#9FE870(lime),sentiment-positive#054D28(L* 28.1). Brand accent and positive are separated by lightness (85.1 vs 28.1), and the lime is only ever a fill with forest-green ink on it — our optic pattern. Dark positive#BAE5A0, negative#CB272F, warning#FFD11A(neptune-tokens 8.20.2,themes/personal*). - Polaris (Shopify): the chrome brand fill token is
#303030(near-black), not green; success fill#047B5D(OKLCH h 168) andtext-success#014B40(h 179, L* 27.9, Lc 93.0 on white) (@shopify/polaris 13.9.5 styles.css). Shopify's guidance: "Green should be used to indicate that a status is positive or that an action has been completed successfully," "Avoid using green to entice merchants or to draw unnecessary attention," and "avoid relying on color only to provide context" (shopify.dev). I could not verify any statement about Polaris' older green brand. - Atlassian: brand and
informationare the same blue#1868DB; success is olive#5B7F24/ text#4C6B1F(OKLCH h 129); warning is a bright fill#FBC828with dark inverse text (@atlaskit/tokens 20.4.0). Brand shares a hue only with the least critical status, never with success/error. - 당근 Seed (orange brand
#FF6600):fg.positive#079171(h 170), dark#22B27F, critical#FA342C(color.yaml). Positive is a teal-leaning green. - Carbon: success is plain
green60#198038(ΔE vs our brand 7.2), but status must "rely on at least two of the following elements: color, shape, or symbol" (status indicators). - Robinhood: the only primary evidence found is the "Accessible colors" setting, "to improve the colors for red-green colorblindness" (support). Spotify, Naver and Grab: no primary token or guideline source could be fetched, so I make no claim about them.
Reading: where positive and brand could both be read, positive is moved off the brand hue by hue (Polaris, Seed, Atlassian) or by lightness (Wise), and every system that discusses accessibility asks for a second cue.
Why ΔE ≥ 20 cannot be the rule. Table P scans every in-gamut colour satisfying the lane's own constraints (on-white Lc ≥ 75, white-on Lc ≥ 60, OKLCH C ≥ 0.08). The best achievable min(n,p,d) against #006D37 by hue: 150° → 7.0, 160° → 9.3, 170° → 11.3, 175° → 12.6, 180° → 14.1, 185° → 15.8, 190° → 17.9, 195° → 19.8, 200° → 22.0. ΔE ≥ 20 therefore needs a blue-teal such as #0D7677/#13797E (h 195–200), which (a) reads as blue, not "success", (b) lands only 13–15 ΔE from the current hard-court blue #247BA0 (8.6–10.0 under deutan), and (c) still fails tritan (6.2 / 7.5). Table D shows the realistic bar: Okabe–Ito's worst pair is 12.3 / 11.5 / 11.1 (protan / deutan / tritan), Tol bright 14.0 / 14.7 / 8.7, and Tol, who designed his sets with CIEDE2000 differences and red-blind / green-blind simulation (Tol), publishes no numeric threshold.
Final rule (replaces ΔE ≥ 20): every pair of roles that can sit in one component must reach OI parity — ≥ 12 under protan and deutan, ≥ 20 normal — and carry meaning by glyph + label (WCAG 1.4.1: "Color is not used as the only visual means of conveying information"; a luminance difference counts as a second cue only at ≥ 3:1, and an additional indicator is still required to identify a specific colour whatever the ratio — Understanding 1.4.1). #00796A gives 13.4 / 14.7 / 17.2 against brand (tritan 6.0 is flagged, not gated: every candidate fails it). But a teal moves the confusion partner to the neutral grey: under deutan #00796A simulates to #66676B, ΔE 3.0 from the neutral-badge text #60646C (bg 0.7, table B3) — the old green was 19.1 from it. Pushing the text to the deep end (#014B40, L* 27.9) restores 23.6 / 13.6 / 13.1. Hue scan against {brand, error, neutral, warning text}: best compromise for a single mid-tone is hue 168–172 (#137B5E: worst n/p/d 9.2 / 9.9 / 10.5) — still below OI parity vs brand — so no single mid-tone satisfies all three confusers; the two-tier split does.
One product consequence to decide. #00796A sits at the same OKLCH hue (180) as the shipped badge.accent text #007B6B (ΔE 1.1). "모집 중 / open" (accent) and "완납 / paid" (success) become the same teal. Today success vs accent is already only 11.6 / 12.8 / 15.2 / 2.1 apart, so the merge removes an existing near-duplicate rather than creating one. If the owner wants "open" distinct from "paid", the alternative is to render "open" as ink-on-neutral with an "open door" glyph, not to return to a second green. Also note the pastel tints: brand-subtle #E5FAEA vs success tint is ΔE 4.5 (current #EBF8F1: 4.7) — brand-tint fills and success chips must not share a row.
2.2 Semantic set under CVD — how much lightness separation is needed?
Method (Okabe–Ito / Tol): ensure the pair differs on luminance or sits on opposite sides of the blue–yellow axis. The Okabe–Ito page recommends vermilion for red (it stays recognizable to protanopes) and bluish green for green (so it is not confused with red or brown), and advises against colours that share brightness and differ only in hue. Measured:
- Window. Text-capable colours must satisfy WCAG 4.5:1 (L* ≤ 49.9) and, by the lane's APCA bar, Lc 75 on white (L* ≤ 46.7 at the brand hue; Lc 60 falls at L* 59.3). Brand 39.9, error 42.4, success fill 45.2: pairwise ΔL* ≤ 5.3 (the deep-teal success text at L* 27.9 adds ΔL* 12–15 but only at text level). Table N: only pairs involving the warning fill (L 77.1)* or optic reach the 3:1 luminance ratio; error/info (ΔL* 21.9) is 2.19:1 and brand/info (19.5) is 2.00:1 — so the brief's "ΔL* ≥ 20" heuristic is weaker than the WCAG 3:1 route by about a third.
- Therefore two tiers. Tier 1, text/solid-with-white-ink: brand, success, error, info, warning text — separated by hue and glyph, plus a deep-tone text variant for success (L* 27.9). Tier 2, bright fill with ink: warning (and optic).
- Hue placement. Moving success from green (h 155) to teal (h 180) changes success-vs-error deutan from 9.5 to 24.2 and protan from 13.2 to 16.2 (table B1 vs B3): under deutan/protan red collapses to dark olive (
#7B6F29/#5A522C) while#00796Abecomes blue-grey (#66676B/#737169). - What stays weak (honest list). Brand vs error protan 6.5 (best red found in the scan: 9.2, a vivid
#BD2703that leaves the peer hue consensus; lighter reds are worse, 0.8–5.3). Warning text vs error protan 7.4 (deutan 4.2 → 10.3). Terracotta series vs warning text protan 1.0 (table O). These are resolved by glyph + label + position, not by hex. - Peers agree on the tier split: danger/success/info fills cluster at L* 42–49 (Polaris 42.0/45.6, Wise 44.7, Atlassian 46.1/48.9/45.9, Carbon 47.0/46.9) while warning fills sit at L* 79–86 with dark ink (table I).
Proposed text-level and fill-level sets are tables B2 and B3.
2.3 Optic lime as level accent
- Numbers. Ink
#0D100Eon optic: Lc 84.1, 13.71:1 (helper LUT: 16.6 px @400, 14 px @700). Optic on white: 1.40:1, Lc 19.1; on basement 1.28:1, Lc 13.2 (below APCA's Lc 15 "designers should treat anything below this level as invisible", APCA nutshell); on brand#006D37: Lc −63.2, 4.64:1 (22.6 px @400, 15.4 px @700 — numerals only, ≥ 16 px bold); on dark card 11.88:1, Lc −82.5; on dark canvas 14.08:1. - Why it works at small area. Relative luminance Y = 0.702 (brand 0.112, error 0.128). The CIE photopic V(λ) peaks at 555 nm (luminous efficiency function, secondary; CIE S 010 not fetched), so a yellow-green of given chroma carries more luminance than any other hue — that is a lightness (L* 87.1) advantage, and it is why a 22 pt chip with dark ink reads on a white card (ΔE vs white 29.2 even at 1.4:1 luminance — chroma carries it).
- Correction to the premise. Helmholtz–Kohlrausch does not help it. Nayatani 1997 Γ (object colours, L_a 12.7 cd/m², implementation): optic 0.930 (VCC) / 1.045 (VAC); warning amber 0.890 / 1.052; brand 1.056 / 1.074; error 1.311 / 1.184; indigo 1.590 / 1.259 (table L). H–K adds apparent brightness mostly to saturated reds, blues and purples, and least to yellow-green. An earlier run at L_a = 60 cd/m² gives the same ordering (optic 0.952 / 1.067, error 1.363 / 1.236, indigo 1.654 / 1.323).
- CVD. Simulated: protan
#F6DB33, deutan#F2DA57(both yellow — still ΔE 40.3 / 43.9 from brand, but only 9.7 / 5.8 from warning fill#F5B301), tritan#D1DCC9(pale grey-green; ΔE vs white 13.2, vs basement 10.2). Rules: never put optic and warning amber in one component; give optic chips on white a 1 px$borderSubtleedge (chip shape otherwise fails in greyscale/tritan). - Small-text guidance. Not as text: Lc 19.1 on white is below APCA's "Lc 30 absolute minimum for other text and solid non-text". The peer pattern is yellow/lime only as a fill carrying dark ink — Wise (
#9FE870with#163300), Atlassian (warning.boldwithinverse #292A2E), Playtomic (lane D).
2.4 Categorical content set
What the references say.
- Okabe–Ito (7 hues + black): alternate warm and cool hues; "Do not convey information in color only. Show difference BOTH in color and shape" (jfly.uni-koeln.de/color); hexes via Wong 2011 (values read from a package that cites it, secondary).
- Tol: distinguishability limits the count; beyond nine colours he recommends a sequential/banded scheme; his default Bright set has 7 hues (Tol).
- Carbon: 14 categorical colours applied in order "curated to maximize contrast between neighboring colors" (Carbon); the 14-set measured here has min deutan ΔE 3.0 (table D) — the order, not the set, carries the safety.
- ColorBrewer: qualitative schemes "rely on differences in hue with only subtle lightness differences" (ColorBrewer) — the equal-L tradition for maps, not for CVD.
- Healey 1996: a hue circle at constant L* = 67.1 (CIELUV); "up to seven isoluminant colors can be displayed simultaneously, while still allowing for the rapid and accurate identification" of one — with neighbour distance shrinking 129.9 → 88.2 → 65.1 ΔE*uv as the count goes 3 → 5 → 7 (UBC TR-96-10; read from the author's PDF).
- Spectrum: categorical palette "optimized to be distinguishable for users with color vision deficiencies"; becomes difficult "starting at 6 colors" and "extremely difficult… at 12" (spectrum.adobe.com; via search excerpt — the page body could not be fetched). Leonardo checks "measurable color difference for each color vision deficiency type" and a 3:1 minimum (leonardocolor.io).
- Size matters: "perceived color difference varies inversely with size" (Szafir 2018, IEEE TVCG 24(1), DOI 10.1109/TVCG.2017.2744359) — so ΔE thresholds for 12–16 px dots must exceed those for swatches. WCAG 1.4.11 asks 3:1 for "each line in a graph" (Understanding).
- Count: ≤ 5–8 per chart is the defensible bound (Spectrum 6 / Healey 7).
Benchmark (table D, helper). Okabe–Ito 21.7 / 12.3 / 11.5 / 11.1; Tol bright 23.7 / 14.0 / 14.7 / 8.7; Tol vibrant 18.3 / 16.1 / 14.5 / 8.6; Carbon first 5: 25.7 / 9.5 / 15.8 / 18.0; Tableau 10 first 5: 22.9 / 6.2 / 0.7 / 9.6; d3 category10 first 5: 26.4 / 1.4 / 4.8 / 11.9. So "≥ 15–20 under CVD" is not met by any published set; the evidence-based bar is OI/Tol parity, i.e. normal ≥ 20, protan/deutan ≥ 12 (OI 11.5), tritan ≥ 8 (Tol 8.7).
Our current sets (table D, E1, F1). Court trio light 36.2 / 1.7 / 15.7 / 13.0 (clay vs grass protan 1.7); dark trio L* spread 4.4 and 0/3 reach 3:1 on white (irrelevant on dark, 6.4–7.3:1 on the card); tiers 14.8 / 10.0 / 3.4 / 8.7; gold 2.38:1.
Equal-L vs staggered-L (the question as asked). Constraint: 3:1 on white and basement ⇒ L* ≲ 58.6. Searches over hue (5° grid, C cap 0.13–0.14), excluding the error and brand/success hue windows:
| search | min ΔE n / p / d / t |
|---|---|
| equal L = 0.60, free hues | 11.5 / 9.3 / 8.2 / 5.2 |
| equal L = 0.58, one hue per family (blue, orange, gold, magenta, cyan) | 23.2 / 7.5 / 2.2 / 6.9 |
| one hue per family, L 0.56 / 0.60 | 19.4 / 5.2 / 5.2 / 10.4 |
| one hue per family, L 0.52 / 0.58 / 0.64 | 20.5 / 13.4 / 12.2 / 7.3 |
| one hue per family, L 0.48 / 0.56 / 0.64 | 20.9 / 15.1 / 15.0 / 11.9 |
| free hues (not family-constrained), L 0.48 / 0.56 / 0.64 | 20.4 / 19.6 / 19.6 / 12.8 |
| final: families fixed by use (blue, terracotta, olive-lime, plum, sky), L 0.46 / 0.52 / 0.64, C ≥ 0.11 | 22.6 / 15.1 / 14.9 / 8.8 |
So a strictly equal-L set cannot reach OI parity under deutan (best 8.2); a three-step stagger can. This is consistent with the sets that do pass (Okabe–Ito L* spread 43.1, Tol bright 35.8, Carbon first-5 23.4). Proposed: $series1–5 = #3566B8 (OKLCH 0.52/0.14/260), #8D3D00 (0.46/0.125/48), #83971F (0.64/0.14/119), #883170 (0.46/0.14/340), #089AC3 (0.64/0.12/226); court tokens alias series 5 / 2 / 3. Equal chroma ≈ 0.12–0.14, L split 0.46 / 0.52 / 0.64 (so "two ranks": dark = blue, terracotta, plum; light = olive, sky). Tables G1–G3, O.
Known collisions left in the final set (table O): terracotta vs warning text protan 1.0 / deutan 4.0 and vs error 15.9 / 7.4 / 7.5 / 8.5; plum vs info 18 / 9 / 17; olive-lime shares hue 119 with optic (ΔE 21, ΔL* 28). Rule: series never appear inside a status chip or beside an optic chip; a legend always carries direct labels (Okabe–Ito: show the difference "BOTH in color and shape").
Tiers. The gaming-convention hues are kept (bronze orange-brown, silver neutral, gold, master purple, grandmaster red-family), only lightness/hue are moved to clear the CVD failures: bronze #B2580B (4.89:1 on white; was 3.14), gold #D9A00F (unchanged class; 2.34:1 — needs an ink 1.5 px outline or numeral to meet SC 1.4.11), grandmaster crimson #9A0F3A (8.39:1; vs error 13.6 / 16.6 / 12.1; was 12.4 / 10.4 / 12.6). Dark: bronze #DA8A11, grandmaster #D8516A, others unchanged.
2.5 Neutrals with a hue
Data (table J). Radix tinted greys, chroma C* steps 1–8 (backgrounds, borders): gray 0, sage 0.9–1.8 (mean 1.3), olive 0.6–2.4, sand 0.5–3.2, slate 0.5–5.9, mauve 0.6–7.0; chroma peaks at step 9 (sage 3.6, olive 4.4, mauve 7.4) where it is a solid, not a ground. Radix's guidance: use the tinted grey whose hue is closest to the accent, "the difference is subtle" (Radix, via search excerpt); step 1/2 is the dark-mode app background and white the light one (understanding the scale). Tailwind v4.1.11 OKLCH chroma: slate ≤ 0.046, gray ≤ 0.034, zinc ≤ 0.017, neutral 0 (npm tarball); surfaces (steps 50–200) are ≤ 0.013 (slate) / 0.006 (gray). Material: neutral palette chroma 4, neutral variant 8 (HCT, core_palette.ts). Peers' grounds: Seed basement #F3F4F5 C* 0.6, iOS grouped #F2F2F7 2.6, Wise surface #E8EBE6 2.8.
Ours. Canvas C* 1.5 (OKLCH 0.004), basement 3.2 (0.009), track 3.0 (0.008), hairline 4.2 (0.012), border 5.6 (0.015), mid greys 6–7.5 (0.016–0.020), text greys 5.8–6.3. In OKLCH terms every step sits at or below Tailwind gray and far below slate, and at or above Radix sage. Verdict: keep; ceiling C ≤ 4.5 for L ≥ 88** (largest area, avoids the mint-tint read that lane D measured) and hue 145–165 so the tint stays in the brand family. Hue-consistent greys (all 150–161°) match Radix's "pick the tinted grey whose hue is closest to the accent".
Does the tinted canvas shift the brand? Table K, CAT16 (Li et al. 2017, doi:10.1002/col.22131; matrix checked against colour-science): adapting to the basement #F0F7F3 instead of #FFFFFF, the corresponding colour of #006D37 is #106C38 (full von Kries) — ΔE2000 0.76, 0.55 at CAM16 D = 0.85 (L_A = 12.7 cd/m²); canvas 0.27; error 0.65; optic 1.07; success 1.03. All ≲ 1. Dark-card adaptation (3–7) is an upper bound: the eye adapts to the near-white text, not the card.
2.6 Dark theme parity
Reference logic. Material 3 primary is tone 40 in light and 80 in dark, onPrimary 100 / 20, error 40 / 80, surface 98 / 6 (color_spec_2021.ts); a 40-step tone difference guarantees ≥ 3:1 and 50 guarantees ≥ 4.5:1 (Material blog, via search summary of the source comments). Ours: brand L* 39.9 (tone ≈ 40) and dark primary L* 78.3 (≈ 80): the brand pair follows the logic. Seed's dark foregrounds are lighter and flatter (positive #22B27F L* 64.7, critical #FF6E60, warning #CA901C, informative #41A2F9; all |Lc| ≈ 47–49 on #16171B); Wise's are much lighter (positive #BAE5A0 L* 86.5, negative #FFA8AD 77.5, warning #FADC65 88.2; |Lc| 68–86 on #121511). Proposed semantic text tones sit between them at L* 73–78 (|Lc| 60–68 on our card), anchored to M3's tone 80.
What fails today (table H, helper LUT): dark $error #D4736E — Lc 40.5 on card (min px 58.3 @400, 46.5 @500, 31.3 @700; fine as a graphic at 5.12:1, unusable as text); $success #68BF8E and $warning #D4A849 Lc 56.5 / 56.8; dark success vs dark primary ΔE 5.5 (same collision as light); dark live vs deadline banners deutan 3.1.
Each role in dark (all verified, tables H–H5):
| role | dark value | Lc on card / canvas / tertiary | note |
|---|---|---|---|
| primary | #70D5A2 (kept) | 67.8 / 69.5 / 60.7 | ink-on Lc 70.4 |
| success, successText | #75D1C6 | 67.6 / 69.3 / 60.4 | vs dark primary 12.0 / 13.9 / 16.3 / 1.8 (tritan); vs dark neutral text 21.9 / 6.7 / 3.0 — the aqua-vs-grey limit is inherent to any light aqua; the badge bg #14433E and the glyph carry it |
| warning text | #E1B75C | 64.9 / 66.6 / 57.7 | was 56.8 |
| warning fill + ink | #E2B13C + #0D100E | ink-on Lc 65.2 (9.65:1) | deadline banner: vs dark live banner 62.2 / 56.5 / 55.9 / 53.6 (was 20.2 / 9.3 / 3.1 / 10.8) |
| error | #FA9B94 | 60.4 / 62.1 / 53.2 | M3 #FFB4AB is Lc 70.7 but 6.9 / 4.8 from dark primary under protan / deutan; #FA9B94 is 10.0 / 5.1 — protan/deutan primary-vs-error is weak in dark for every tone-80 red (M3 itself pairs them at the same tone) |
| info | #C7B5F2 (kept) | 65.7 / 67.4 / 58.5 | |
| optic | #C6E84B (kept) | 82.5 / 84.2 / 75.4 | brightest object in dark by design; chip area ≤ 1% |
| brand block (new) | #1E5236 | white-on Lc 95.0 (9.07:1); optic-on 6.50:1; block vs card 1.83:1, ΔE 20.6 | #70D5A2 cannot be the dark hero block: optic on it is invisible |
$controlTrack | #374039 (existing gray400) | vs card 1.544:1, vs canvas 1.829:1 | |
$controlPill | #515B53 (existing gray500) | vs track 1.520:1; label #EFF3F0 Lc 80.9 | |
| series 1–5 | #6C9EF1 #DA875A #BBD068 #D57CB8 #67D3FC | 5.85–9.74:1 on card; Lc 45.8–70.5 | min ΔE 18.9 / 10.0 / 10.1 / 7.1 |
| tiers | bronze #DA8A11, grandmaster #D8516A (silver/gold/master kept) | 6.0 / 4.2:1 on card | min 19.7 / 14.6 / 12.8 / 11.2 (was 19.7 / 13.2 / 4.8 / 10.6) |
Tamagui dark values reuse existing ramp hexes wherever possible (#374039, #515B53, #1E5236, #14433E) so the dark theme gains eleven new hexes (success #75D1C6, warning text #E1B75C, warning fill #E2B13C, error #FA9B94, tier bronze #DA8A11, tier grandmaster #D8516A, and the five series).
2.7 How many colours is "clean"?
Measured / published counts.
- Healey 1996 (CIELUV, constant L* = 67.1): "up to seven isoluminant colors can be displayed simultaneously, while still allowing for the rapid and accurate identification of any one of the seven"; the introduction records anecdotal military-display guidance of "no more than five to seven colours at a time", with no explanation (Healey, Choosing Effective Colours for Data Visualization, PDF read directly). The 2012 Healey & Enns survey (IEEE TVCG 18(7)) is the usual citation for preattentive features, but the file served there was a different Healey document, so I rely on the 1996 paper and do not attribute "≤ 5–7 preattentively distinguishable" to the 2012 survey.
- Spectrum: categorical colour "become[s] more difficult to comprehend starting at 6 colors, and extremely difficult… at 12" (spectrum.adobe.com, search-excerpt only — page body not fetched). EU data-visualisation guide: "Seven is a good maximum" and CVD-safe categorical palettes "are rare and hard to design" (data.europa.eu, secondary).
- Ware: nominal colour coding is usable up to roughly 12 hues (UBC lecture slides crediting Ware, slide text, and the book Information Visualization: Perception for Design) — secondary; the book was not accessible.
- ISO 9241-125:2017 covers visual-information coding (iso.org) but the colour-count provision is paywalled and was not read; the standard states it does not address charts. ISO 22324 (colour-coded alert levels) is summarised as "not exceed seven colors" (Wikipedia summary, secondary). NN/g: no article on a colour-count limit was found, so no NN/g claim is made.
- Carbon (status): "at least two of the following elements: color, shape, or symbol" (Carbon).
Reconciling with the budget. The token set contains 10 hue families at 30° resolution: error 27, terracotta/clay 48, warning amber 80, optic and grass 119–121, brand 153, success 180, hard/sky 225, blue 260, info 302, plum 340. That exceeds 7 only if all appear at once, which the role-exclusive budget (§5 of the benchmark) prevents:
| context | hue families on screen | count |
|---|---|---|
| 홈 hub | brand block 153 · optic chips 121 · success/open chip 180 · warning chip 80 · court dots 225 + 48 + 119 | 7 |
| 기록 hub | brand block 153 · optic progress 121 · tier ring (1 hue) · success delta 180 · 1 series line colour | ≤ 6 |
| session detail | brand CTA 153 · court dot (1) · success/warning/error chip (1–2) | ≤ 4 |
| chart | 5 series hues, no semantic hue inside the plot | 5 |
Rules that follow: (1) ≤ 7 hue families per viewport (Healey 7) and ≤ 5 series per chart (Spectrum 6); (2) role-exclusive: semantic hues only on chips/banners/validation, content hues only on dots/charts/rings, brand only on action + one block; (3) every colour-coded fact has a second cue (WCAG 1.4.1; Carbon). The budget's "content hues ≤ 5" is the series set; court dots alias three of its five hues, tiers are the only content hues outside it, and they never share a row with series.
3. Full computed tables (all from bench/colour_lab.py unless marked in the header note)
Letters are referenced from §0–§2. Scripts: bench/lane_g/final_tables.py, extra_tables.py, series_slots.py, series_final.py, court_search.py, adapt.py.
3.0 APCA / WCAG pairs (python3 colour_lab.py pairs FG:BG ..., published-LUT font minima in px by weight)
| fg | bg | Lc | WCAG | min px @400 | @500 | @700 |
|---|---|---|---|---|---|---|
#FFFFFF | #006D37 | -86.6 | 6.48 | 16.3 | 15.6 | 14 |
#006D37 | #FFFFFF | 81.5 | 6.48 | 17 | 15.8 | 14 |
#006D37 | #F0F7F3 | 75.7 | 5.95 | 17.9 | 16 | 14 |
#FFFFFF | #14804A | -79.4 | 4.98 | 17.3 | 15.8 | 14 |
#14804A | #FFFFFF | 74.0 | 4.98 | 18.3 | 16.4 | 14.1 |
#FFFFFF | #00796A | -81.3 | 5.33 | 17.1 | 15.8 | 14 |
#00796A | #FFFFFF | 75.9 | 5.33 | 17.9 | 16 | 14 |
#00796A | #E0F8F3 | 68.7 | 4.79 | 20.1 | 18.3 | 14.6 |
#00796A | #F0F7F3 | 70.1 | 4.90 | 19.5 | 18 | 14.5 |
#FFFFFF | #B8332E | -83.6 | 5.91 | 16.7 | 15.7 | 14 |
#B8332E | #FFFFFF | 78.3 | 5.91 | 17.5 | 15.9 | 14 |
#B8332E | #FDECEC | 69.3 | 5.18 | 19.8 | 18.1 | 14.6 |
#946200 | #FFF4D6 | 69.4 | 4.78 | 19.8 | 18.1 | 14.6 |
#6B4A00 | #FFF4D6 | 81.4 | 7.36 | 17 | 15.8 | 14 |
#6B4A00 | #FFFFFF | 87.7 | 8.06 | 16.2 | 15.6 | 14 |
#0D100E | #F5B301 | 68.8 | 10.32 | 20.1 | 18.2 | 14.6 |
#4B0082 | #F2E8FF | 85.9 | 10.95 | 16.4 | 15.6 | 14 |
#FFFFFF | #4B0082 | -100.8 | 12.95 | 14.9 | 14.4 | 12.8 |
#0D100E | #C6E84B | 84.1 | 13.71 | 16.6 | 15.7 | 14 |
#C6E84B | #006D37 | -63.2 | 4.64 | 22.6 | 19.7 | 15.4 |
#C6E84B | #FFFFFF | 19.1 | 1.40 | n/a | n/a | n/a |
#C6E84B | #F0F7F3 | 13.2 | 1.28 | n/a | n/a | n/a |
#0D100E | #F0F7F3 | 99.7 | 17.59 | 15 | 14.5 | 13 |
#0D100E | #FFFFFF | 105.5 | 19.13 | 14.4 | 13.9 | 11.9 |
#4F5851 | #F0F7F3 | 79.8 | 6.78 | 17.3 | 15.8 | 14 |
#626C64 | #F0F7F3 | 71.4 | 5.02 | 19.1 | 17.4 | 14.4 |
#4F5851 | #FFFFFF | 85.6 | 7.38 | 16.4 | 15.6 | 14 |
#626C64 | #FFFFFF | 77.3 | 5.46 | 17.7 | 15.9 | 14 |
#0D100E | #70D5A2 | 70.4 | 10.69 | 19.4 | 17.8 | 14.5 |
#70D5A2 | #1A201B | -67.8 | 9.26 | 20.5 | 18.4 | 14.7 |
#75D1C6 | #1A201B | -67.6 | 9.23 | 20.6 | 18.5 | 14.7 |
#E1B75C | #1A201B | -64.9 | 8.79 | 21.8 | 19 | 15 |
#FA9B94 | #1A201B | -60.4 | 8.04 | 23.8 | 20.8 | 15.9 |
#D4736E | #1A201B | -40.5 | 5.12 | 58.3 | 46.5 | 31.3 |
#C7B5F2 | #1A201B | -65.7 | 8.94 | 21.4 | 18.9 | 14.9 |
#C6E84B | #1A201B | -82.5 | 11.88 | 16.9 | 15.7 | 14 |
#0D100E | #E2B13C | 65.2 | 9.65 | 21.6 | 19 | 15 |
#EFF3F0 | #1A201B | -97.4 | 14.81 | 15.3 | 14.8 | 13.3 |
#BBC3BD | #1A201B | -67.2 | 9.20 | 20.7 | 18.6 | 14.8 |
P. Brand-vs-success frontier: best achievable min(ΔE normal, protan, deutan) vs brand #006D37 per OKLCH hue, subject to on-white Lc >= 75, white-on Lc >= 60, OKLCH chroma >= 0.08 (grid L 0.40-0.60 step .005, C 0.08-0.17 step .005)
| OKLCH hue | best hex | L / C | ΔE vs brand n / p / d / t | on-white Lc | white-on Lc | reads as |
|---|---|---|---|---|---|---|
| 150 | #1F542E | 0.400 / 0.085 | 8.8 / 8.5 / 7.0 / 8.1 | 90.1 | 94.6 | brand green |
| 155 | #1C5434 | 0.400 / 0.080 | 9.3 / 9.6 / 8.1 / 8.0 | 90.1 | 94.5 | |
| 160 | #16563A | 0.405 / 0.080 | 9.5 / 10.0 / 9.3 / 7.0 | 89.4 | 93.9 | green |
| 165 | #0A553D | 0.400 / 0.080 | 10.6 / 11.7 / 11.0 / 7.3 | 89.8 | 94.3 | |
| 170 | #035844 | 0.410 / 0.080 | 11.3 / 12.5 / 12.4 / 6.1 | 88.7 | 93.3 | jade green |
| 175 | #2D7968 | 0.525 / 0.080 | 12.6 / 13.6 / 14.7 / 6.0 | 75.5 | 80.9 | jade |
| 180 | #29796C | 0.525 / 0.080 | 14.1 / 15.3 / 17.4 / 6.1 | 75.5 | 80.8 | teal (= shipped accent badge hue) |
| 185 | #1B7A71 | 0.525 / 0.085 | 15.8 / 17.7 / 20.3 / 6.8 | 75.2 | 80.6 | teal |
| 190 | #227975 | 0.525 / 0.080 | 17.9 / 20.7 / 22.4 / 6.9 | 75.3 | 80.7 | blue-teal |
| 195 | #0D7677 | 0.515 / 0.085 | 19.8 / 23.4 / 24.5 / 6.2 | 76.6 | 81.9 | blue-teal |
| 200 | #13797E | 0.525 / 0.085 | 22.0 / 25.3 / 26.0 / 7.5 | 75.2 | 80.5 | cyan-teal |
| 205 | #147882 | 0.525 / 0.085 | 24.4 / 27.8 / 28.0 / 7.7 | 75.4 | 80.7 | cyan |
| 210 | #047887 | 0.525 / 0.090 | 26.7 / 30.0 / 29.7 / 8.3 | 75.2 | 80.6 | cyan |
A. Role numbers: current vs final (light)
| role | hex | L* | C* | h(Lab) | OKLCH L/C/h | on white Lc | on white WCAG | white-on Lc | ink-on Lc | on tint Lc (WCAG) |
|---|---|---|---|---|---|---|---|---|---|---|
| brand (kept) | #006D37 | 39.9 | 46.3 | 151 | 0.469/0.121/153 | 81.5 | 6.48 | -86.6 | 22.0 | - |
| success CURRENT | #14804A | 47.1 | 47.1 | 153 | 0.530/0.125/155 | 74.0 | 4.98 | -79.4 | 29.6 | 67.9 (4.56) |
| success FINAL (icon / fill / large figure) | #00796A | 45.2 | 32.4 | 180 | 0.516/0.094/180 | 75.9 | 5.33 | -81.3 | 27.6 | 68.7 (4.79) |
| successText FINAL (small text, badge text) | #014B40 | 27.9 | 23.5 | 178 | 0.370/0.067/179 | 93.0 | 10.09 | -97.1 | 10.4 | 85.7 (9.07) |
| warning text CURRENT | #946200 | 45.7 | 55.0 | 76 | 0.537/0.114/74 | 75.7 | 5.24 | -81.1 | 27.8 | 69.4 (4.78) |
| warning text FINAL | #6B4A00 | 34.0 | 43.8 | 79 | 0.434/0.090/79 | 87.7 | 8.06 | -92.4 | 15.8 | 81.4 (7.36) |
| warning fill FINAL (ink text) | #F5B301 | 77.1 | 80.7 | 81 | 0.807/0.166/82 | 34.6 | 1.85 | -38.7 | 68.8 | - |
| error (kept) | #B8332E | 42.4 | 63.1 | 33 | 0.524/0.171/27 | 78.3 | 5.91 | -83.6 | 25.2 | 69.3 (5.18) |
| info (kept) | #4B0082 | 20.5 | 74.3 | 314 | 0.339/0.179/302 | 97.2 | 12.95 | -100.8 | 0.0 | 85.9 (10.95) |
| optic (kept) | #C6E84B | 87.1 | 76.2 | 115 | 0.878/0.182/121 | 19.1 | 1.40 | -21.8 | 84.1 | - |
B1. CURRENT set: brand, success, warning, error, info, optic
| pair | normal | protan | deutan | tritan | flagged (<10) |
|---|---|---|---|---|---|
| brand vs success | 6.7 | 7.0 | 6.4 | 7.0 | normal, protan, deutan, tritan |
| brand vs warning | 36.1 | 7.7 | 14.9 | 46.9 | protan |
| brand vs error | 61.0 | 6.5 | 11.3 | 58.7 | protan |
| brand vs info | 50.5 | 50.6 | 44.3 | 36.7 | - |
| brand vs optic | 43.6 | 40.3 | 43.9 | 42.1 | - |
| success vs warning | 36.5 | 10.2 | 13.5 | 48.2 | - |
| success vs error | 61.9 | 13.2 | 9.5 | 60.9 | deutan |
| success vs info | 52.7 | 52.4 | 45.4 | 40.5 | - |
| success vs optic | 37.4 | 33.7 | 37.4 | 35.7 | - |
| warning vs error | 27.6 | 12.1 | 4.2 | 12.0 | deutan |
| warning vs info | 59.2 | 58.3 | 59.9 | 28.1 | - |
| warning vs optic | 42.8 | 37.3 | 31.8 | 46.9 | - |
| error vs info | 39.7 | 46.4 | 55.8 | 33.2 | - |
| error vs optic | 67.4 | 48.5 | 33.3 | 53.9 | - |
| info vs optic | 102.5 | 87.5 | 85.3 | 69.1 | - |
B2. FINAL set, TEXT level (light): brand, successText, warningText, error, info, optic, neutral-badge text
| pair | normal | protan | deutan | tritan | flagged (<10) |
|---|---|---|---|---|---|
| brand vs successText | 16.3 | 17.6 | 18.1 | 10.4 | - |
| brand vs warnText | 31.9 | 9.6 | 9.8 | 43.2 | protan, deutan |
| brand vs error | 61.0 | 6.5 | 11.3 | 58.7 | protan |
| brand vs info | 50.5 | 50.6 | 44.3 | 36.7 | - |
| brand vs optic | 43.6 | 40.3 | 43.9 | 42.1 | - |
| brand vs neutral | 27.2 | 23.1 | 20.0 | 17.8 | - |
| successText vs warnText | 31.6 | 18.5 | 24.3 | 40.7 | - |
| successText vs error | 51.5 | 12.9 | 28.3 | 53.9 | - |
| successText vs info | 37.0 | 30.5 | 23.3 | 32.1 | - |
| successText vs optic | 60.5 | 58.1 | 62.2 | 54.8 | - |
| successText vs neutral | 23.6 | 13.6 | 13.1 | 18.5 | - |
| warnText vs error | 28.9 | 7.4 | 10.3 | 17.4 | protan |
| warnText vs info | 55.0 | 53.4 | 53.7 | 20.1 | - |
| warnText vs optic | 51.9 | 50.0 | 45.4 | 55.4 | - |
| warnText vs neutral | 27.9 | 28.2 | 28.6 | 27.0 | - |
| error vs info | 39.7 | 46.4 | 55.8 | 33.2 | - |
| error vs optic | 67.4 | 48.5 | 33.3 | 53.9 | - |
| error vs neutral | 29.5 | 21.9 | 27.7 | 33.7 | - |
| info vs optic | 102.5 | 87.5 | 85.3 | 69.1 | - |
| info vs neutral | 27.0 | 25.0 | 24.4 | 25.2 | - |
| optic vs neutral | 50.9 | 50.3 | 49.6 | 37.4 | - |
B3. FINAL FILL / ICON level (light): brand, success, warningFill, error, info, optic, neutral-badge text
| pair | normal | protan | deutan | tritan | flagged (<10) |
|---|---|---|---|---|---|
| brand vs success | 13.4 | 14.7 | 17.2 | 6.0 | tritan |
| brand vs warnFill | 49.6 | 31.9 | 40.7 | 57.4 | - |
| brand vs error | 61.0 | 6.5 | 11.3 | 58.7 | protan |
| brand vs info | 50.5 | 50.6 | 44.3 | 36.7 | - |
| brand vs optic | 43.6 | 40.3 | 43.9 | 42.1 | - |
| brand vs neutral | 27.2 | 23.1 | 20.0 | 17.8 | - |
| success vs warnFill | 48.3 | 33.8 | 43.8 | 54.3 | - |
| success vs error | 55.5 | 16.2 | 24.2 | 58.9 | - |
| success vs info | 44.9 | 37.0 | 27.1 | 39.7 | - |
| success vs optic | 44.1 | 40.8 | 46.1 | 36.9 | - |
| success vs neutral | 23.3 | 10.4 | 3.0 | 18.4 | deutan |
| warnFill vs error | 46.4 | 40.4 | 29.5 | 30.7 | - |
| warnFill vs info | 85.4 | 79.7 | 83.7 | 57.3 | - |
| warnFill vs optic | 25.8 | 9.7 | 5.8 | 33.9 | protan, deutan |
| warnFill vs neutral | 45.5 | 43.8 | 47.5 | 41.3 | - |
| error vs info | 39.7 | 46.4 | 55.8 | 33.2 | - |
| error vs optic | 67.4 | 48.5 | 33.3 | 53.9 | - |
| error vs neutral | 29.5 | 21.9 | 27.7 | 33.7 | - |
| info vs optic | 102.5 | 87.5 | 85.3 | 69.1 | - |
| info vs neutral | 27.0 | 25.0 | 24.4 | 25.2 | - |
| optic vs neutral | 50.9 | 50.3 | 49.6 | 37.4 | - |
B4. CURRENT set plus neutral badge text (for comparison)
| pair | normal | protan | deutan | tritan | flagged (<10) |
|---|---|---|---|---|---|
| brand vs success | 6.7 | 7.0 | 6.4 | 7.0 | normal, protan, deutan, tritan |
| brand vs warning | 36.1 | 7.7 | 14.9 | 46.9 | protan |
| brand vs error | 61.0 | 6.5 | 11.3 | 58.7 | protan |
| brand vs info | 50.5 | 50.6 | 44.3 | 36.7 | - |
| brand vs optic | 43.6 | 40.3 | 43.9 | 42.1 | - |
| brand vs neutral | 27.2 | 23.1 | 20.0 | 17.8 | - |
| success vs warning | 36.5 | 10.2 | 13.5 | 48.2 | - |
| success vs error | 61.9 | 13.2 | 9.5 | 60.9 | deutan |
| success vs info | 52.7 | 52.4 | 45.4 | 40.5 | - |
| success vs optic | 37.4 | 33.7 | 37.4 | 35.7 | - |
| success vs neutral | 27.5 | 23.5 | 19.1 | 19.4 | - |
| warning vs error | 27.6 | 12.1 | 4.2 | 12.0 | deutan |
| warning vs info | 59.2 | 58.3 | 59.9 | 28.1 | - |
| warning vs optic | 42.8 | 37.3 | 31.8 | 46.9 | - |
| warning vs neutral | 29.5 | 29.1 | 30.8 | 29.0 | - |
| error vs info | 39.7 | 46.4 | 55.8 | 33.2 | - |
| error vs optic | 67.4 | 48.5 | 33.3 | 53.9 | - |
| error vs neutral | 29.5 | 21.9 | 27.7 | 33.7 | - |
| info vs optic | 102.5 | 87.5 | 85.3 | 69.1 | - |
| info vs neutral | 27.0 | 25.0 | 24.4 | 25.2 | - |
| optic vs neutral | 50.9 | 50.3 | 49.6 | 37.4 | - |
C. Brand vs success: ΔE2000 (normal / protan / deutan / tritan) against brand #006D37
| candidate | hex | OKLCH h | vs brand | vs error #B8332E | on-white Lc | white-on Lc | note |
|---|---|---|---|---|---|---|---|
| CURRENT success | #14804A | 155 | 6.7 / 7.0 / 6.4 / 7.0 | 61.9 / 13.2 / 9.5 / 60.9 | 74.0 | 79.4 | shipped |
| Polaris bg-fill-success (npm @shopify/polaris 13.9.5) | #047B5D | 168 | 9.0 / 9.6 / 10.3 / 6.1 | 58.7 / 13.0 / 17.1 / 59.7 | 75.6 | 80.9 | h168 |
| Polaris text-success | #014B40 | 179 | 16.3 / 17.6 / 18.1 / 10.4 | 51.5 / 12.9 / 28.3 / 53.9 | 93.0 | 97.1 | |
| Wise sentiment-positive (npm @transferwise/neptune-tokens 8.20.2) | #054D28 | 154 | 10.5 / 10.8 / 9.8 / 10.1 | 56.7 / 4.3 / 20.3 / 55.5 | 92.8 | 96.9 | dark forest |
| Atlassian text.success (npm @atlaskit/tokens 20.4.0) | #4C6B1F | 129 | 11.4 / 5.6 / 7.3 / 11.0 | 51.2 / 10.0 / 5.5 / 44.6 | 80.4 | 85.6 | olive |
| Seed fg.positive (daangn/seed-design color.yaml) | #079171 | 170 | 15.3 / 16.1 / 15.7 / 14.2 | 61.4 / 20.9 / 18.2 / 62.8 | 66.4 | 71.9 | |
| Carbon green60 | #198038 | 148 | 7.2 / 7.6 / 7.3 / 6.3 | 63.2 / 14.0 / 5.1 / 61.0 | 74.2 | 79.6 | |
| FINAL success (icon/fill) | #00796A | 180 | 13.4 / 14.7 / 17.2 / 6.0 | 55.5 / 16.2 / 24.2 / 58.9 | 75.9 | 81.3 | h180 |
| FINAL successText | #014B40 | 179 | 16.3 / 17.6 / 18.1 / 10.4 | 51.5 / 12.9 / 28.3 / 53.9 | 93.0 | 97.1 | h179, L* 27.9 |
| cyan end of window (ΔE>=20 feasible) | #0D7677 | 196 | 19.8 / 23.4 / 24.5 / 6.2 | 50.7 / 23.4 / 32.1 / 57.8 | 76.6 | 81.9 | h195-200: reads blue-teal |
D. Categorical sets: min pairwise ΔE2000 per vision type, L* spread, 3:1 on white / basement
| set | n | min ΔE normal / protan / deutan / tritan | L* range (spread) | >=3:1 on white | >=3:1 on basement #F0F7F3 | worst deutan pair | worst protan pair |
|---|---|---|---|---|---|---|---|
| Okabe-Ito 7 (no black) | 7 | 21.7 / 12.3 / 11.5 / 11.1 | 46.0-89.1 (43.1) | 4/7 | 3/7 | #E69F00/#F0E442 11.5 | #0072B2/#CC79A7 12.3 |
| Tol bright 6 (no grey) | 6 | 23.7 / 14.0 / 14.7 / 8.7 | 41.6-77.4 (35.8) | 4/6 | 3/6 | #4477AA/#AA3377 14.7 | #4477AA/#AA3377 14.0 |
| Tol vibrant 6 (no grey) | 6 | 18.3 / 16.1 / 14.5 / 8.6 | 46.0-71.2 (25.2) | 4/6 | 4/6 | #EE7733/#CC3311 14.5 | #0077BB/#EE3377 16.1 |
| Tol muted 9 | 9 | 15.0 / 11.8 / 14.0 / 11.6 | 22.4-81.8 (59.4) | 6/9 | 5/9 | #CC6677/#44AA99 14.0 | #332288/#882255 11.8 |
| Tableau 10 | 10 | 18.1 / 1.3 / 0.7 / 7.0 | 49.6-82.0 (32.4) | 5/10 | 4/10 | #E15759/#59A14F 0.7 | #FF9DA7/#BAB0AC 1.3 |
| Tableau 10 first 5 | 5 | 22.9 / 6.2 / 0.7 / 9.6 | 49.6-70.1 (20.5) | 3/5 | 2/5 | #E15759/#59A14F 0.7 | #F28E2B/#59A14F 6.2 |
| Carbon categorical first 5 | 5 | 25.7 / 9.5 / 15.8 / 18.0 | 35.2-58.5 (23.4) | 5/5 | 5/5 | #005D5D/#9F1853 15.8 | #005D5D/#9F1853 9.5 |
| Carbon categorical 14 | 14 | 12.2 / 5.8 / 3.0 / 4.5 | 15.2-58.5 (43.4) | 14/14 | 14/14 | #1192E8/#A56EFF 3.0 | #1192E8/#A56EFF 5.8 |
| d3 category10 first 5 | 5 | 26.4 / 1.4 / 4.8 / 11.9 | 46.8-66.9 (20.0) | 4/5 | 4/5 | #2CA02C/#D62728 4.8 | #FF7F0E/#2CA02C 1.4 |
| OURS court trio (light, current) | 3 | 36.2 / 1.7 / 15.7 / 13.0 | 37.4-48.4 (11.0) | 3/3 | 3/3 | #B84A2E/#006633 15.7 | #B84A2E/#006633 1.7 |
| OURS court trio (dark, current) | 3 | 28.2 / 9.1 / 14.7 / 6.0 | 66.0-70.4 (4.4) | 0/3 | 0/3 | #E09050/#4DB380 14.7 | #E09050/#4DB380 9.1 |
| OURS tiers (light, current) | 5 | 14.8 / 10.0 / 3.4 / 8.7 | 48.9-68.9 (20.0) | 4/5 | 2/5 | #CD7F32/#E74C3C 3.4 | #CD7F32/#D4A017 10.0 |
| OURS tiers (dark, current) | 5 | 19.7 / 13.2 / 4.8 / 10.6 | 64.0-86.9 (22.9) | 0/5 | 0/5 | #D4944A/#FF6B6B 4.8 | #D4944A/#FF6B6B 13.2 |
| PROPOSED series 1-5 (light) | 5 | 22.6 / 15.1 / 14.9 / 8.8 | 35.2-59.1 (23.9) | 5/5 | 5/5 | #3566B8/#883170 14.9 | #3566B8/#883170 15.1 |
| PROPOSED series 1-5 (dark) | 5 | 18.9 / 10.0 / 10.1 / 7.1 | 63.4-80.0 (16.6) | 0/5 | 0/5 | #DA875A/#BBD068 10.1 | #6C9EF1/#D57CB8 10.0 |
E1. Court trio CURRENT: hard #247BA0, clay #B84A2E, grass #006633
| pair | normal | protan | deutan | tritan | flagged (<10) |
|---|---|---|---|---|---|
| hard vs clay | 47.9 | 38.7 | 43.6 | 55.4 | - |
| hard vs grass | 36.2 | 37.3 | 35.0 | 13.0 | - |
| clay vs grass | 54.9 | 1.7 | 15.7 | 56.3 | protan |
E2. Court trio PROPOSED (= series 5, 2, 3): hard #089AC3, clay #8D3D00, grass #83971F
| pair | normal | protan | deutan | tritan | flagged (<10) |
|---|---|---|---|---|---|
| hard vs clay | 54.4 | 51.1 | 49.9 | 60.2 | - |
| hard vs grass | 48.1 | 46.9 | 48.4 | 22.2 | - |
| clay vs grass | 42.6 | 27.4 | 20.0 | 38.0 | - |
Court vs semantic: current grass vs brand ΔE 2.3 / 2.6 / 2.2 / 2.4 ; current clay vs error 6.6 / 5.4 / 2.9 / 4.1 ; proposed grass vs brand 25.9 / 20.5 / 23.8 / 26.1 ; proposed clay vs error 15.9 / 7.4 / 7.5 / 8.5 ; proposed clay vs warningText 14.4 / 0.8 / 3.5 / 10.3
F1. Tiers CURRENT (light)
| pair | normal | protan | deutan | tritan | flagged (<10) |
|---|---|---|---|---|---|
| bronze vs silver | 27.5 | 27.3 | 28.0 | 24.7 | - |
| bronze vs gold | 14.8 | 10.0 | 7.5 | 8.7 | deutan, tritan |
| bronze vs master | 48.6 | 51.9 | 51.1 | 18.7 | - |
| bronze vs grandmaster | 21.1 | 12.2 | 3.4 | 10.5 | deutan |
| silver vs gold | 31.4 | 31.2 | 31.9 | 24.3 | - |
| silver vs master | 25.1 | 22.7 | 20.1 | 21.4 | - |
| silver vs grandmaster | 29.2 | 25.1 | 26.7 | 30.0 | - |
| gold vs master | 61.8 | 57.9 | 55.9 | 22.2 | - |
| gold vs grandmaster | 36.5 | 22.1 | 10.9 | 18.2 | - |
| master vs grandmaster | 35.5 | 44.6 | 49.2 | 23.5 | - |
F2. Tiers PROPOSED (light): bronze #B2580B, gold #D9A00F, grandmaster #9A0F3A (silver, master kept)
| pair | normal | protan | deutan | tritan | flagged (<10) |
|---|---|---|---|---|---|
| bronze vs silver | 30.3 | 31.8 | 29.3 | 28.3 | - |
| bronze vs gold | 25.8 | 23.5 | 18.4 | 20.5 | - |
| bronze vs master | 45.2 | 51.1 | 50.1 | 18.0 | - |
| bronze vs grandmaster | 30.1 | 26.8 | 18.1 | 11.4 | - |
| silver vs gold | 31.7 | 31.3 | 32.3 | 24.9 | - |
| silver vs master | 25.1 | 22.7 | 20.1 | 21.4 | - |
| silver vs grandmaster | 36.1 | 31.1 | 28.0 | 36.2 | - |
| gold vs master | 61.7 | 58.0 | 56.2 | 23.0 | - |
| gold vs grandmaster | 56.4 | 47.4 | 36.9 | 34.5 | - |
| master vs grandmaster | 28.0 | 29.6 | 39.4 | 24.6 | - |
| tier | current | on white WCAG | proposed | on white WCAG |
|---|---|---|---|---|
| bronze | #CD7F32 | 3.14 | #B2580B | 4.89 |
| silver | #8E8E93 | 3.26 | #8E8E93 | 3.26 |
| gold | #D4A017 | 2.38 | #D9A00F | 2.34 |
| master | #9B59B6 | 4.67 | #9B59B6 | 4.67 |
| grandmaster | #E74C3C | 3.82 | #9A0F3A | 8.39 |
GM vs error: current 12.4 / 10.4 / 12.6 / 11.9 ; proposed 13.6 / 16.6 / 12.1 / 7.0 ; gold(proposed) vs warningFill 6.0 / 5.7 / 6.1 / 4.5 ; bronze(proposed) vs clay(proposed) 10.8 / 10.0 / 11.3 / 10.5
G1. PROPOSED series 1-5 (light)
| pair | normal | protan | deutan | tritan | flagged (<10) |
|---|---|---|---|---|---|
| s1 blue vs s2 terracotta | 45.8 | 50.5 | 52.7 | 58.5 | - |
| s1 blue vs s3 olive-lime | 61.7 | 56.4 | 57.0 | 24.0 | - |
| s1 blue vs s4 plum | 30.4 | 15.1 | 14.9 | 54.4 | - |
| s1 blue vs s5 sky | 22.6 | 16.6 | 15.5 | 16.3 | - |
| s2 terracotta vs s3 olive-lime | 42.6 | 27.4 | 20.0 | 38.0 | - |
| s2 terracotta vs s4 plum | 36.7 | 40.8 | 35.8 | 8.8 | tritan |
| s2 terracotta vs s5 sky | 54.4 | 51.1 | 49.9 | 60.2 | - |
| s3 olive-lime vs s4 plum | 68.0 | 53.5 | 42.8 | 38.7 | - |
| s3 olive-lime vs s5 sky | 48.1 | 46.9 | 48.4 | 22.2 | - |
| s4 plum vs s5 sky | 54.3 | 30.2 | 21.0 | 66.5 | - |
G2. PROPOSED series 1-5 (dark)
| pair | normal | protan | deutan | tritan | flagged (<10) |
|---|---|---|---|---|---|
| s1 vs s2 | 42.9 | 46.1 | 50.1 | 65.9 | - |
| s1 vs s3 | 58.2 | 53.2 | 53.8 | 25.4 | - |
| s1 vs s4 | 32.1 | 10.0 | 14.5 | 61.7 | - |
| s1 vs s5 | 18.9 | 13.5 | 12.1 | 12.6 | - |
| s2 vs s3 | 37.3 | 17.0 | 10.1 | 33.3 | - |
| s2 vs s4 | 32.8 | 37.3 | 33.5 | 7.1 | tritan |
| s2 vs s5 | 49.2 | 42.1 | 45.2 | 59.6 | - |
| s3 vs s4 | 62.8 | 46.1 | 37.0 | 34.0 | - |
| s3 vs s5 | 45.5 | 44.5 | 46.6 | 24.0 | - |
| s4 vs s5 | 50.4 | 17.4 | 13.3 | 69.9 | - |
G3. Series values
| token | light | OKLCH (light) | WCAG on white / basement | dark | WCAG on dark card / canvas | Lc on dark card |
|---|---|---|---|---|---|---|
$series1 | #3566B8 | 0.52/0.140/260 | 5.61 / 5.16 | #6C9EF1 | 6.16 / 7.30 | 47.9 |
$series2 | #8D3D00 | 0.46/0.125/48 | 7.50 / 6.89 | #DA875A | 6.00 / 7.11 | 46.8 |
$series3 | #83971F | 0.64/0.140/119 | 3.28 / 3.01 | #BBD068 | 9.74 / 11.54 | 70.5 |
$series4 | #883170 | 0.46/0.140/340 | 7.72 / 7.10 | #D57CB8 | 5.85 / 6.93 | 45.8 |
$series5 | #089AC3 | 0.64/0.120/226 | 3.27 / 3.01 | #67D3FC | 9.71 / 11.51 | 70.5 |
H. Dark parity: current vs proposed on card #1A201B / canvas #090C0A / tertiary #374039
| role | current | Lc card | WCAG card | proposed | Lc card | Lc canvas | Lc tertiary | WCAG card |
|---|---|---|---|---|---|---|---|---|
| primary | #70D5A2 | 67.8 | 9.26 | #70D5A2 | 67.8 | 69.5 | 60.7 | 9.26 |
| success | #68BF8E | 56.5 | 7.45 | #75D1C6 | 67.6 | 69.3 | 60.4 | 9.23 |
| warning text | #D4A849 | 56.8 | 7.50 | #E1B75C | 64.9 | 66.6 | 57.7 | 8.79 |
| error | #D4736E | 40.5 | 5.12 | #FA9B94 | 60.4 | 62.1 | 53.2 | 8.04 |
| info | #C7B5F2 | 65.7 | 8.94 | #C7B5F2 | 65.7 | 67.4 | 58.5 | 8.94 |
| optic | #C6E84B | 82.5 | 11.88 | #C6E84B | 82.5 | 84.2 | 75.4 | 11.88 |
H2. DARK current: primary, success, warning, error, info, optic
| pair | normal | protan | deutan | tritan | flagged (<10) |
|---|---|---|---|---|---|
| primary vs success | 5.5 | 5.5 | 5.3 | 5.5 | normal, protan, deutan, tritan |
| primary vs warning | 33.3 | 15.0 | 16.4 | 48.4 | - |
| primary vs error | 57.6 | 21.2 | 12.8 | 61.4 | - |
| primary vs info | 38.9 | 35.8 | 29.3 | 27.6 | - |
| primary vs optic | 22.1 | 17.9 | 20.2 | 20.0 | - |
| success vs warning | 31.7 | 12.6 | 15.9 | 46.5 | - |
| success vs error | 54.8 | 16.1 | 8.2 | 58.0 | deutan |
| success vs info | 38.7 | 35.8 | 30.4 | 27.3 | - |
| success vs optic | 23.4 | 20.0 | 22.2 | 21.0 | - |
| warning vs error | 32.5 | 20.7 | 13.9 | 13.1 | - |
| warning vs info | 47.6 | 47.6 | 45.6 | 21.9 | - |
| warning vs optic | 23.8 | 14.4 | 10.3 | 33.8 | - |
| error vs info | 31.3 | 35.3 | 37.4 | 29.5 | - |
| error vs optic | 56.1 | 32.9 | 23.0 | 42.0 | - |
| info vs optic | 61.8 | 51.7 | 48.4 | 17.3 | - |
H3. DARK proposed: primary, success, warningText, error, info, optic
| pair | normal | protan | deutan | tritan | flagged (<10) |
|---|---|---|---|---|---|
| primary vs success | 12.0 | 13.9 | 16.3 | 1.8 | tritan |
| primary vs warnText | 31.9 | 12.2 | 15.6 | 47.3 | - |
| primary vs error | 54.3 | 10.0 | 5.1 | 57.1 | deutan |
| primary vs info | 38.9 | 35.8 | 29.3 | 27.6 | - |
| primary vs optic | 22.1 | 17.9 | 20.2 | 20.0 | - |
| success vs warnText | 34.4 | 23.7 | 30.7 | 47.1 | - |
| success vs error | 48.2 | 12.0 | 21.4 | 56.7 | - |
| success vs info | 32.4 | 19.6 | 12.3 | 27.7 | - |
| success vs optic | 30.5 | 28.1 | 34.1 | 20.6 | - |
| warnText vs error | 30.1 | 16.7 | 11.0 | 8.4 | tritan |
| warnText vs info | 47.1 | 46.4 | 45.0 | 21.0 | - |
| warnText vs optic | 21.6 | 11.4 | 7.3 | 32.3 | deutan |
| error vs info | 27.6 | 30.4 | 34.8 | 24.4 | - |
| error vs optic | 51.7 | 25.3 | 16.3 | 37.0 | - |
| info vs optic | 61.8 | 51.7 | 48.4 | 17.3 | - |
Dark warning fill #E2B13C: ink #0D100E on it Lc 65.2 (WCAG 9.65); vs card WCAG 8.36 ; vs error(dark) 32.4 / 19.7 / 13.8 / 6.8
H4. Dark tiers PROPOSED vs current
dark tiers CURRENT
| pair | normal | protan | deutan | tritan | flagged (<10) |
|---|---|---|---|---|---|
| bronze vs silver | 26.0 | 26.1 | 26.2 | 25.9 | - |
| bronze vs gold | 22.8 | 18.9 | 16.7 | 15.6 | - |
| bronze vs master | 41.8 | 43.1 | 41.6 | 15.4 | - |
| bronze vs grandmaster | 27.1 | 13.2 | 4.8 | 10.6 | deutan |
| silver vs gold | 34.4 | 33.7 | 34.6 | 23.2 | - |
| silver vs master | 19.7 | 14.6 | 12.8 | 16.6 | - |
| silver vs grandmaster | 27.8 | 18.5 | 22.5 | 21.5 | - |
| gold vs master | 58.6 | 51.0 | 49.4 | 15.7 | - |
| gold vs grandmaster | 49.7 | 29.7 | 19.5 | 25.0 | - |
| master vs grandmaster | 28.0 | 32.9 | 37.5 | 22.6 | - |
dark tiers PROPOSED (bronze #DA8A11, grandmaster #D8516A; silver, gold, master kept)
| pair | normal | protan | deutan | tritan | flagged (<10) |
|---|---|---|---|---|---|
| bronze vs silver | 29.7 | 30.1 | 29.5 | 27.2 | - |
| bronze vs gold | 23.3 | 19.3 | 15.9 | 18.7 | - |
| bronze vs master | 46.4 | 47.2 | 45.2 | 18.6 | - |
| bronze vs grandmaster | 37.7 | 27.5 | 17.7 | 11.2 | - |
| silver vs gold | 34.4 | 33.7 | 34.6 | 23.2 | - |
| silver vs master | 19.7 | 14.6 | 12.8 | 16.6 | - |
| silver vs grandmaster | 29.5 | 21.0 | 19.3 | 32.6 | - |
| gold vs master | 58.6 | 51.0 | 49.4 | 15.7 | - |
| gold vs grandmaster | 58.1 | 41.3 | 30.1 | 29.4 | - |
| master vs grandmaster | 26.3 | 28.3 | 32.7 | 25.4 | - |
H5. Control track / pill
- track
#E5EBE7on#FFFFFF: WCAG 1.209, ΔL* 7.5 - track
#E5EBE7on#F0F7F3: WCAG 1.111, ΔL* 4.1 - track
#E5EBE7on#F8FBF9: WCAG 1.161, ΔL* 5.8 - track
#374039on#1A201B: WCAG 1.544, ΔL* 14.6 - track
#374039on#090C0A: WCAG 1.829, ΔL* 23.0 - track
#2A332Con#1A201B: WCAG 1.270, ΔL* 8.7 - dark pill
#515B53on track#374039: WCAG 1.520 ; text#EFF3F0on pill Lc 80.9 - light pill
#FFFFFFon track#E5EBE7: WCAG 1.209
I. Peer semantic/brand values read from published packages (hex -> our metrics)
| system / token | hex | L* | Lab h | OKLCH h | on-white Lc | white-on Lc | source |
|---|---|---|---|---|---|---|---|
Polaris --p-color-bg-fill-brand (light) | #303030 | 19.9 | 158 | 90 | 99.6 | 102.8 | @shopify/polaris 13.9.5 build/esm/styles.css |
Polaris bg-fill-success | #047B5D | 45.6 | 167 | 168 | 75.6 | 80.9 | same |
Polaris text-success | #014B40 | 27.9 | 178 | 179 | 93.0 | 97.1 | same |
Polaris bg-fill-critical | #C70A24 | 42.0 | 32 | 24 | 77.5 | 82.8 | same |
Polaris bg-fill-warning | #FFB800 | 79.4 | 80 | 81 | 31.1 | 35.0 | same |
Polaris text-warning | #5E4200 | 30.1 | 81 | 81 | 91.3 | 95.6 | same |
Wise interactive-primary (personal) | #163300 | 18.1 | 130 | 135 | 100.3 | 103.4 | @transferwise/neptune-tokens 8.20.2 themes/personal/tokens.css |
Wise interactive-accent | #9FE870 | 85.1 | 130 | 135 | 22.2 | 25.3 | same |
Wise sentiment-positive | #054D28 | 28.1 | 152 | 154 | 92.8 | 96.9 | same |
Wise sentiment-negative | #CB272F | 44.7 | 31 | 25 | 75.3 | 80.7 | same |
Wise sentiment-warning | #FFD11A | 85.5 | 89 | 92 | 21.6 | 24.6 | same |
Wise dark sentiment-positive | #BAE5A0 | 86.5 | 132 | 134 | 20.1 | 22.9 | themes/personal--dark |
Atlassian background.brand.bold = information.bold | #1868DB | 45.9 | 288 | 259 | 75.1 | 80.4 | @atlaskit/tokens 20.4.0 figma/atlassian-light.json |
Atlassian success.bold | #5B7F24 | 48.9 | 122 | 129 | 72.2 | 77.7 | same |
Atlassian text.success | #4C6B1F | 41.5 | 123 | 129 | 80.4 | 85.6 | same |
Atlassian danger.bold | #C9372C | 46.1 | 36 | 29 | 74.2 | 79.7 | same |
Atlassian warning.bold | #FBC828 | 82.9 | 87 | 89 | 25.7 | 29.1 | same |
Atlassian text.warning | #9E4C00 | 41.9 | 60 | 52 | 79.5 | 84.7 | same |
| Carbon green60 / red60 / yellow30 / orange40 | #198038 | 46.9 | 145 | 148 | 74.2 | 79.6 | carbondesignsystem.com/data-visualization/color-palettes (alert palette) |
| red60 | #DA1E28 | 47.0 | 33 | 26 | 72.5 | 77.9 | same |
| yellow30 | #F1C21B | 80.4 | 88 | 90 | 29.6 | 33.4 | same |
Seed (당근) fg.positive light | #079171 | 53.5 | 169 | 170 | 66.4 | 71.9 | github.com/daangn/seed-design packages/rootage/color.yaml |
Seed fg.critical light | #FA342C | 55.0 | 36 | 28 | 63.3 | 68.7 | same |
Seed bg.brand-solid (carrot-600) | #FF6600 | 62.3 | 52 | 43 | 54.8 | 60.1 | same |
Seed fg.positive dark | #22B27F | 64.7 | 162 | 163 | 52.1 | 57.3 | same |
J. Tinted neutrals: CIELAB C* by step (Radix 3.0.0 light; first sRGB block of each CSS file)
| scale | C* steps 1-12 | mean C* steps 1-8 | max C* steps 1-8 | step 9 | step 11 |
|---|---|---|---|---|---|
| radix gray | 0.0 0.0 0.0 0.0 0.0 0.0 0.0 0.0 0.0 0.0 0.0 0.0 | 0.0 | 0.0 | 0.0 | 0.0 |
| radix sage | 0.9 0.9 1.2 1.2 1.5 1.2 1.8 1.8 3.6 3.6 2.8 4.1 | 1.3 | 1.8 | 3.6 | 2.8 |
| radix olive | 0.6 1.2 1.3 1.3 1.9 1.9 1.9 2.4 4.4 4.5 4.2 4.2 | 1.6 | 2.4 | 4.4 | 4.2 |
| radix sand | 0.5 0.5 0.6 1.1 1.6 1.6 2.1 3.2 4.0 3.4 3.0 2.9 | 1.4 | 3.2 | 4.0 | 3.0 |
| radix slate | 0.5 1.0 1.5 2.1 2.6 3.7 4.2 5.9 6.3 5.8 5.0 3.4 | 2.7 | 5.9 | 6.3 | 5.0 |
| radix mauve | 0.6 1.1 2.3 2.8 3.9 4.3 5.4 7.0 7.4 7.0 6.1 5.2 | 3.4 | 7.0 | 7.4 | 6.1 |
Tailwind CSS 4.1.11 theme.css neutral families, OKLCH chroma (steps 50/100/200/300/400/500/600/700/800/900/950):
| family | chroma by step |
|---|---|
| slate | 0.003 0.007 0.013 0.022 0.04 0.046 0.043 0.044 0.041 0.042 0.042 |
| gray | 0.002 0.003 0.006 0.01 0.022 0.027 0.03 0.034 0.033 0.034 0.028 |
| zinc | 0 0.001 0.004 0.006 0.015 0.016 0.017 0.013 0.006 0.006 0.005 |
| neutral | 0 0 0 0 0 0 0 0 0 0 0 |
| stone | 0.001 0.001 0.003 0.005 0.01 0.013 0.011 0.01 0.007 0.006 0.004 |
| ours | hex | L* | C* (Lab) | OKLCH C | hue (OKLCH) |
|---|---|---|---|---|---|
| canvas | #F8FBF9 | 98.3 | 1.5 | 0.0041 | 157 |
| basement (proposed) | #F0F7F3 | 96.6 | 3.2 | 0.0091 | 161 |
| controlTrack | #E5EBE7 | 92.5 | 3.0 | 0.0083 | 157 |
| hairline gray200 | #DFE7E1 | 90.9 | 4.2 | 0.0117 | 154 |
| border gray300 | #CAD4CC | 84.0 | 5.6 | 0.0154 | 152 |
| gray400 | #A8B5AB | 72.4 | 7.3 | 0.0202 | 153 |
| gray500 (borderStrong) | #7E8A80 | 56.3 | 7.5 | 0.0204 | 150 |
| textTertiary gray600 | #626C64 | 44.6 | 6.3 | 0.0174 | 151 |
| textSecondary gray700 | #4F5851 | 36.4 | 5.8 | 0.0160 | 152 |
| ink gray950 | #0D100E | 4.4 | 1.5 | 0.0063 | 156 |
| dark canvas | #090C0A | 3.1 | 1.2 | 0.0066 | 157 |
| dark card | #1A201B | 11.5 | 4.8 | 0.0129 | 150 |
| dark gray300 | #262D28 | 17.7 | 4.8 | 0.0133 | 154 |
| dark gray500 | #515B53 | 37.6 | 6.5 | 0.0179 | 151 |
Reference grounds: Seed basement #F3F4F5 C* 0.6 ; iOS grouped #F2F2F7 C* 2.6 ; Wise light surface #E8EBE6 C* 2.8 ; Radix sage-3 #EEF1F0 C* 1.2 ; Seed dark layer #16171B C* 3.0
K. Chromatic-adaptation shift of a stimulus when the adapting white is the tinted ground instead of #FFFFFF (CAT16, ΔE2000 between the stimulus and its corresponding colour)
| stimulus | canvas #F8FBF9 (D=1) | basement #F0F7F3 (D=1) | basement (D=0.85, CAM16 at L_A=12.7) | track #E5EBE7 (D=1) | dark card #1A201B (D=1, upper bound) |
|---|---|---|---|---|---|
| brand #006D37 | 0.27 | 0.76 | 0.55 | 0.69 | 3.71 |
| success #00796A | 0.47 | 1.03 | 0.98 | 1.01 | 7.04 |
| error #B8332E | 0.24 | 0.65 | 0.46 | 0.46 | 3.32 |
| optic #C6E84B | 0.53 | 1.07 | 0.88 | 1.07 | 5.98 |
| info #4B0082 | 0.16 | 0.21 | 0.21 | 0.21 | 1.57 |
L. Relative luminance and Helmholtz-Kohlrausch luminance factor Gamma (Nayatani 1997 object-colour estimate, colour-science 0.4.7, L_a = 12.7 cd/m2, D65 white)
| colour | hex | Y (white=1) | L* | Gamma VCC | Gamma VAC |
|---|---|---|---|---|---|
| optic | #C6E84B | 0.702 | 87.1 | 0.930 | 1.045 |
| warning fill | #F5B301 | 0.517 | 77.1 | 0.890 | 1.052 |
| brand | #006D37 | 0.112 | 39.9 | 1.056 | 1.074 |
| success | #00796A | 0.147 | 45.2 | 1.057 | 1.054 |
| hard (current) | #247BA0 | 0.171 | 48.4 | 1.121 | 1.074 |
| clay (current) | #B84A2E | 0.153 | 46.0 | 1.196 | 1.140 |
| error | #B8332E | 0.128 | 42.4 | 1.311 | 1.184 |
| info | #4B0082 | 0.031 | 20.5 | 1.590 | 1.259 |
| grey 8E8E93 | #8E8E93 | 0.272 | 59.2 | 1.012 | 1.006 |
M. Optic lime #C6E84B: appearance under simulated deficiency, and contrast to grounds
| vision | simulated optic | vs white dE | vs basement dE | vs brand dE | vs warningFill dE |
|---|---|---|---|---|---|
| normal | #C6E84B | 29.2 | 27.7 | 43.6 | 25.8 |
| protan | #F6DB33 | 29.5 | 28.1 | 40.3 | 9.7 |
| deutan | #F2DA57 | 27.7 | 26.6 | 43.9 | 5.8 |
| tritan | #D1DCC9 | 13.2 | 10.2 | 42.1 | 33.9 |
N. Final-fill luminance separation (SC 1.4.1 "3:1 luminance difference" alternative)
| pair | dL* | WCAG luminance ratio | >= 3:1 |
|---|---|---|---|
| brand / success | 5.3 | 1.22 | no |
| brand / error | 2.5 | 1.10 | no |
| brand / info | 19.5 | 2.00 | no |
| brand / warningText | 5.9 | 1.24 | no |
| brand / warningFill | 37.1 | 3.49 | yes |
| brand / optic | 47.2 | 4.64 | yes |
| success / error | 2.8 | 1.11 | no |
| success / info | 24.8 | 2.43 | no |
| success / warningText | 11.2 | 1.51 | no |
| success / warningFill | 31.8 | 2.87 | no |
| success / optic | 41.9 | 3.82 | yes |
| error / info | 21.9 | 2.19 | no |
| error / warningText | 8.4 | 1.36 | no |
| error / warningFill | 34.7 | 3.19 | yes |
| error / optic | 44.7 | 4.24 | yes |
| info / warningText | 13.6 | 1.61 | no |
| info / warningFill | 56.6 | 6.99 | yes |
| info / optic | 66.6 | 9.28 | yes |
| warningText / warningFill | 43.0 | 4.35 | yes |
| warningText / optic | 53.1 | 5.78 | yes |
| warningFill / optic | 10.0 | 1.33 | no |
O. Series vs semantic roles: ΔE2000 normal/protan/deutan/tritan (! = any of n/p/d < 10)
| series | brand | success | warnText | warnFill | error | info | optic |
|---|---|---|---|---|---|---|---|
| s1 blue | 44/45/41/12 | 32/29/22/9 | 49/50/52/50 | 63/62/68/59 | 42/43/51/64 | 28/20/17/39 | 72/66/69/38 |
| s2 terracotta | 47/10/11/53 | 46/23/26/54 | 14/1/4/10 ! | 43/43/37/37 | 16/7/7/8 ! | 47/53/56/27 | 60/51/41/56 |
| s3 olive-lime | 26/20/24/26 | 30/26/31/23 | 31/27/23/35 | 26/12/17/32 | 54/27/13/38 | 81/68/65/41 | 21/21/21/21 |
| s4 plum | 66/38/26/53 | 45/26/10/54 | 45/41/35/10 | 68/64/56/38 | 26/35/35/14 | 18/9/17/21 ! | 83/70/58/57 |
| s5 sky | 40/42/40/22 | 25/27/22/16 | 50/51/50/52 | 53/49/56/50 | 59/44/46/63 | 49/37/30/51 | 55/53/56/27 |
4. Final token proposal (ready for classic-grass.theme.ts, shared-semantics.ts, status-badge-palette.ts, packages/ui/src/tokens.ts, tier.config.ts)
All hexes verified in §3. "New" = token does not exist today. Dark values reuse existing ramp hexes where marked (=).
| token | light | dark | role | allowed uses | notes / replaces |
|---|---|---|---|---|---|
$primary | #006D37 | #70D5A2 | brand solid | the screen's one primary CTA, toggle on, checkbox, selected date disc; inline link | unchanged |
$brandBlock (new) | #006D37 | #1E5236 (= ramp 300) | hero block fill | one per hub screen (홈 day block, 기록 ELO block, 프로필 identity) | $onBrandBlock #FFFFFF / #EFF3F0; optic on block 4.64:1 light, 6.50:1 dark |
$basement (new) | #F0F7F3 | #090C0A (= canvas) | hub ground | hub/dashboard screens behind white cards; lists stay white | ΔL* 3.4 vs white; C* 3.2 |
$controlTrack (new) | #E5EBE7 | #374039 (= gray400) | segmented/tab/progress track | neutral controls | 1.209 vs white, 1.111 vs basement; dark 1.544 vs card |
$controlPill (new) | #FFFFFF | #515B53 (= gray500) | selected segment | with 1 px $borderSubtle in light | pill vs track 1.209 / 1.520 |
$success | #00796A | #75D1C6 | success icon / fill / large figure | paid, RSVP yes, +delta glyph, validation OK, always with a check glyph; solid fill with white ink | was #14804A / #68BF8E; SHARED_STATUS_*.success, tokens.ts success, THEME_HEX_FALLBACK.success, toast inverse.successIcon |
$successText (new) | #014B40 | #75D1C6 | success small text, badge text | badge text, inline "+24" at < 18 px | badge pair: light {bg #E0F8F3, text #014B40}, dark {bg #14433E, text #75D1C6}; badge.accent becomes an alias of badge.success (was #E0F8F3/#007B6B and #14433E/#44D2BA) |
$warning | #946200 | #D4A849 | warning icon / graphic only | lighting icon, facility accents | unchanged (5.24:1 on white, 7.50:1 on dark card) |
$warningText (new) | #6B4A00 | #E1B75C | warning text | badge text on #FFF4D6, inline warning copy | light badge text was #946200 (Lc 69.4, 4.78:1) → Lc 81.4, 7.36:1 |
$warningFill (new) | #F5B301 | #E2B13C | warning solid | deadline banner, "마감 임박" solid chip | $onWarningFill #0D100E / #0D100E; replaces deadlineBannerBg (#946200 / #4A3106) and deadlineBannerText |
$error | #B8332E | #FA9B94 | error | text, icon, fill (white ink in light) | dark was #D4736E (Lc 40.5); SHARED_STATUS_DARK.error, toast inverse.errorIcon |
$info | #4B0082 | #C7B5F2 | info | badge, notice | unchanged |
$levelChipBg (new) | #C6E84B | #C6E84B | level accent (= optic) | NTRP/ELO chip under avatars and in rows, today dot, progress fill inside $brandBlock | never text, never on white without 1 px edge, never beside $warningFill; $levelChipText #0D100E both (Lc 84.1, 13.71:1) |
$series1 (new) | #3566B8 | #6C9EF1 | chart/content hue 1 | chart series 1 | OKLCH 0.52/0.14/260 |
$series2 (new) | #8D3D00 | #DA875A | hue 2 (terracotta) | chart series 2; = $courtClay | OKLCH 0.46/0.125/48 |
$series3 (new) | #83971F | #BBD068 | hue 3 (olive-lime) | chart series 3; = $courtGreen | OKLCH 0.64/0.14/119 |
$series4 (new) | #883170 | #D57CB8 | hue 4 (plum) | chart series 4 | OKLCH 0.46/0.14/340 |
$series5 (new) | #089AC3 | #67D3FC | hue 5 (sky) | chart series 5; = $courtHard | OKLCH 0.64/0.12/226 |
$courtHard / $courtClay / $courtGreen | #089AC3 / #8D3D00 / #83971F | #67D3FC / #DA875A / #BBD068 | court surface identity | 12–40 px dots and CourtLinesTile fills | were #247BA0 / #B84A2E / #006633 (dark #67B7D1 / #E09050 / #4DB380); other three themes keep their own court values unless the owner extends |
$tierBronze | #B2580B | #DA8A11 | tier hue | tier ring/dot beside a text label | tier.config.ts color/colorDark; bgTint #B2580B15 |
$tierSilver | #8E8E93 | #A8A9AD | tier hue | same | unchanged |
$tierGold | #D9A00F | #FFD700 | tier hue | same; 2.34:1 on white ⇒ needs an ink outline or numeral | bgTint #D9A00F15 |
$tierMaster | #9B59B6 | #C39BD3 | tier hue | same | unchanged |
$tierGrandmaster | #9A0F3A | #D8516A | tier hue | same | was #E74C3C / #FF6B6B; bgTint #9A0F3A15 |
Wiring notes (all repo paths read, none edited): SHARED_STATUS_LIGHT/DARK, SHARED_SEMANTICS_*.deadlineBanner* in packages/app/src/config/themes/shared-semantics.ts (theme-invariant, so the change reaches all four themes at once); STATUS_BADGE_LIGHT/DARK.success/accent/warning in status-badge-palette.ts; colors.court/colorsDark.court, inverse.successIcon/errorIcon and the new fields in classic-grass.theme.ts; tokens.ts. Side finding: tokens.ts primitives textPrimary #131A14, textSecondary #455D49, surfaceSecondary #F6F8F5, border #D5E3D6, borderDark #617A65 differ from the live theme (#0D100E, #4F5851, #F8FBF9, #CAD4CC, #7E8A80); tokens-theme-agreement.test.ts pins only warning and textTertiary, so these have drifted unguarded.
Blast radius (grep, excl. tests): $success 40 sites, $warning 40, $error 112, $info 2, $optic 0, court tokens 14, deadlineBannerBg 4, $badgeSuccess 11, $badgeAccent 4. The value changes to $success (40 sites) and dark $error (112 sites) reach the most call sites; keeping $success as the icon/fill token and adding $successText for badge text means no call site has to change, only colour values (and the badge palette).
5. What to add to scripts/check-color-contrast.mjs
What it asserts today (read from scripts/check-color-contrast.mjs and scripts/lib/color-contrast-core.mjs): WCAG 4.5:1 for derived {bg,text} pairs, text tiers on every surface, brand-as-ink, the primary button; 3:1 for border.dark and icon.muted; ≥ 1.5:1 for dark badge fills; four APCA watch entries (dark badge.muted hard at 45; dark badge.success/accent, dark text.tertiary on tertiary/elevated, dark button warn at 60); empty baseline; exemptions for the Naver logotype and disabled text.
Gaps (verified by reading derivePairs):
status.success/warning/error/infoare not derived as text/icon on a surface — they enter only through badge pairs and the toast icons. Today's$errordark#D4736Eon the card (Lc 40.5, 5.12:1) and light$success(Lc 74.0) therefore pass silently.court.*andmedal.*areinfoonly; tiers (tier.config.ts),optic,basement,controlTrack, series do not appear at all.- No CVD assertion of any kind.
- APCA is gated on four hand-picked pairs only.
Additions (each row = a derived pair or an assertion; thresholds are the numbers the final tokens already meet):
| id | assertion | threshold (kind) | final value | canary (current value that must fail) |
|---|---|---|---|---|
| A1 | status.{error,info}, success, successText, warningText as text on surface.primary/secondary and basement | WCAG 4.5 (fail) · APCA ≥ 60 (fail) · APCA ≥ 75 on surface.primary (warn) | success 5.33 / Lc 75.9; successText 10.09 / 93.0; warningText 8.06 / 87.7; error 5.91 / 78.3 | dark $error #D4736E Lc 40.5 → fail; light $success #14804A Lc 74.0 → warn |
| A2 | dark status text on surface.primary, secondary, tertiary, elevated | APCA ≥ 60 on primary/secondary (fail), ≥ 55 on tertiary/elevated (warn) | success 67.6/69.3/60.4; warning 64.9/66.6/57.7; error 60.4/62.1/53.2 | #D4736E |
| A3 | solid fills with their ink: success/error/info/brandBlock + text.inverse; warningFill + onWarningFill; levelChipBg + levelChipText | APCA ≥ 75 (fail) for white-ink fills; ≥ 60 for warningFill (fail), ≥ 65 (warn); levelChip ≥ 75 | 81.3 / 83.6 / 100.8 / 86.6; 68.8; 84.1 | — |
| A4 | optic on brandBlock light/dark | WCAG 4.5 (fail) | 4.64 / 6.50 | optic on dark primary #70D5A2 (invisible) |
| A5 | series1–5 vs surface.primary and basement (light), vs card and canvas (dark) | WCAG 3.0 (fail, SC 1.4.11) | min 3.27 / 3.01 light; 5.85 dark | — |
| A6 | controlTrack vs surface.primary; vs basement; dark vs card; pill vs track | WCAG ≥ 1.2 / ≥ 1.1 / ≥ 1.5 / ≥ 1.2 (fail) | 1.209 / 1.111 / 1.544 / 1.209 | gray100 #F0F5F1 (1.10 / 1.01) |
| A7 | basement vs card: ΔL* ∈ [3.0, 5.5]; canvas of list screens = #FFFFFF; neutral chroma: Lab C* ≤ 4.5 for L* ≥ 88 and hue 145–165 | fail | 3.4; 3.2 / 4.2 | #EEF2EF (grey-everywhere reading withdrawn in the benchmark) |
| A8 | CVD role separation (new cvdSimulate + deltaE2000 in color-contrast-core.mjs; Machado 2009 severity 1.0, matrices applied in linear RGB) | config table below | below | below |
| A9 | hue-family count of {brand, success, warning fill, error, info, optic, series1–5} at 30° buckets | report-only; warn above 10 | 10 | — |
A8 config (scripts/color-separation.config.json), thresholds chosen at Okabe–Ito parity (worst OI pair 21.7 / 12.3 / 11.5 / 11.1):
| pair (light unless noted) | normal | protan | deutan | tritan | final measured (n/p/d/t) |
|---|---|---|---|---|---|
| brand ↔ success | ≥ 12 | ≥ 12 | ≥ 12 | warn ≥ 5 | 13.4 / 14.7 / 17.2 / 6.0 |
| brand ↔ successText | ≥ 14 | ≥ 14 | ≥ 14 | warn ≥ 8 | 16.3 / 17.6 / 18.1 / 10.4 |
| successText ↔ neutral badge text | ≥ 20 | ≥ 12 | ≥ 12 | — | 23.6 / 13.6 / 13.1 / 18.5 |
| success ↔ error | ≥ 40 | ≥ 15 | ≥ 15 | — | 55.5 / 16.2 / 24.2 / 58.9 |
| warningFill ↔ error (+ WCAG luminance ratio ≥ 3.0) | ≥ 25 | ≥ 25 | ≥ 25 | — | 46.4 / 40.4 / 29.5 / 30.7 (3.19:1) |
| warningText ↔ error | ≥ 20 | warn ≥ 7 | warn ≥ 8 | — | 28.9 / 7.4 / 10.3 / 17.4 |
| brand ↔ optic | ≥ 30 | ≥ 30 | ≥ 30 | ≥ 30 | 43.6 / 40.3 / 43.9 / 42.1 |
| series 1–5 pairwise (light) | ≥ 20 | ≥ 12 | ≥ 12 | warn ≥ 8 | 22.6 / 15.1 / 14.9 / 8.8 |
| series 1–5 pairwise (dark) | ≥ 18 | ≥ 9.5 | ≥ 9.5 | warn ≥ 7 | 18.9 / 9.99 / 10.09 / 7.13 |
| court trio pairwise (aliases of series 5, 2, 3) | ≥ 20 | ≥ 12 | ≥ 12 | warn ≥ 8 | 42.6 / 27.4 / 20.0 / 22.2 |
| grass ↔ brand; clay ↔ error | ≥ 12 | warn ≥ 7 | warn ≥ 7 | — | 25.9 / 20.5 / 23.8; 15.9 / 7.4 / 7.5 |
| tiers pairwise (light / dark) | ≥ 15 | ≥ 10 | ≥ 10 | warn ≥ 8 | 25.1 / 22.7 / 18.1 / 11.4; 19.7 / 14.6 / 12.8 / 11.2 |
| dark primary ↔ dark success | ≥ 9 | ≥ 12 | ≥ 12 | warn | 12.0 / 13.9 / 16.3 / 1.8 |
| dark live banner ↔ dark deadline banner | ≥ 25 | ≥ 25 | ≥ 25 | — | 62.2 / 56.5 / 55.9 / 53.6 |
Canaries (project law: a gate needs a known positive). Ship the gate with fixtures that must fail: current #14804A vs brand (6.7 / 7.0 / 6.4); current tiers (bronze↔grandmaster deutan 3.4); current court trio (clay↔grass protan 1.7, grass↔brand 2.3); dark #68BF8E vs #70D5A2 (5.5); dark error Lc 40.5; light/dark banner pair deutan 4.2 / 3.1.
Porting test vectors (helper → JS), to 0.1 / 0.01: cvd('#006D37','deutan') = #625B3B, protan #6D6333, tritan #006B60; cvd('#14804A','deutan') = #736C4E; cvd('#00796A','deutan') = #66676B; cvd('#B8332E','protan') = #5A522C, deutan #7B6F29; cvd('#F5B301','deutan') = #DDC518; ΔE2000 #006D37/#14804A = 6.699 (normal), 6.389 (deutan); #006D37/#00796A = 13.415 / 14.695 (protan) / 17.173 (deutan); apca('#00796A','#FFFFFF') = 75.936, wcag = 5.326; apca('#D4736E','#1A201B') = −40.463. The repo's APCA implementation (apcaContrast, constants 0.1.9) and the helper's (0.0.98G-4g) agree to ≤ 0.01 Lc on the 11 pairs compared (bench/lane_g/apca_cmp.mjs: e.g. #00796A/white 75.936 vs 75.936, #D4736E/card −40.463 vs −40.463), and WCAG ratios to ≤ 0.01; the self-test should still pin both.
6. Caveats
- CVD simulation. Machado 2009 at severity 1.0 is dichromacy, the worst case; most colour-vision-deficient people are anomalous trichromats (Machado et al., IEEE TVCG 15(6)). The page does not state the colour space the matrices act in; the helper applies them in linear RGB (the usual implementation). ΔE2000 on simulated colours is a proxy, not a discrimination experiment, and it is not size-aware: smaller marks need more difference (Szafir 2018), so 12 px dots deserve more margin than the 40 px court tiles measured here.
- Thresholds are benchmarks, not standards. "OI parity" (≥ 11.5 under deutan) is derived from the best published sets measured with the same tool (table D); no standard fixes a ΔE for colour coding. ISO 9241-125 was not readable (paywall).
- Peers. Polaris, Wise, Atlassian values come from published npm packages (
@shopify/polaris13.9.5,@transferwise/neptune-tokens8.20.2,@atlaskit/tokens20.4.0); Seed fromcolor.yamlvia fetch; Carbon from its docs page. Spotify, Naver, Grab: nothing primary found, no claim made. Robinhood: only the accessibility setting. Okabe–Ito hexes were read from a package that cites Wong 2011 (secondary); the Okabe–Ito and Tol guidance quotes come from fetched pages summarised by the fetch tool. - Not from the helper: CAT16 adaptation (
adapt.py; matrix equals colour-science'sCAT_CAT16), Helmholtz–Kohlrausch Γ (colour-science 0.4.7, Nayatani 1997 object-colour method), the OKLCH→hex inverse used by the scans. Adaptation assumes the whole field adapts to the ground colour (an upper bound) and ignores simultaneous contrast; the dark-card rows are overestimates because the eye adapts to the near-white text. - Healey & Enns 2012 survey was not read (wrong file served); the seven-colour finding is from Healey 1996. Spectrum's 6 / 12 statement is from a search excerpt. Ware figures are via slides. Adobe Leonardo and Spectrum token values were not retrieved (JS-rendered).
- APCA. The helper implements 0.0.98G-4g; the font-size minima use the published LUT (strict: for example
#0D100Eon white is 14.4 px @400). Under that LUT no pair in this app reaches 14 px at weight 400 (even#0D100Eon white needs 14.4 px); secondary and tertiary grey text (Lc 79.8 / 71.4 on basement) need ≥ 15.8 / 17.4 px at weight 500 — a typography finding outside this lane, noted because the neutrals verdict (keep) holds on WCAG/Lc but not on the LUT. - Residual weak pairs (honest list): brand ↔ error protan 6.5; warning text ↔ error protan 7.4; success icon ↔ neutral badge deutan 3.0 (hence
$successText); terracotta series ↔ warning text 14 / 1 / 4 and ↔ error 16 / 7 / 7; plum ↔ info 18 / 9 / 17; olive-lime shares hue 119 with optic (ΔE 21); dark primary ↔ dark error deutan 5.1; every teal vs brand under tritan ≤ 8.3 (table P). All are mitigated by glyph + label + role-exclusive placement, not by hex. - Eyeball on device before shipping:
#6B4A00(reads brown),#8D3D00(terracotta, darker than today's clay),#83971F(olive-lime grass),#014B40(very dark teal). They are computed, not judged; a Pixel-class low-gamut panel may crush the three dark ones. - Product decisions embedded: merging
badge.accent("open") intobadge.success; replacing the deadline banner's brown fill by amber with ink; grandmaster moving from red to ruby; court dots becoming series hues (clay darker). - Tamagui v5
styleCompat: 'legacy'and thetokens-theme-agreementtest were not exercised; token names above are proposals.
7. Sources (every claim above traces to one of these; "S" = secondary)
- WCAG 2.2 Understanding 1.4.1: https://www.w3.org/WAI/WCAG22/Understanding/use-of-color.html · 1.4.11: https://www.w3.org/WAI/WCAG22/Understanding/non-text-contrast.html
- APCA in a Nutshell (Lc 90/75/60/45/30/15): https://git.apcacontrast.com/documentation/APCA_in_a_Nutshell.html
- Okabe & Ito, Color Universal Design: https://jfly.uni-koeln.de/color/ · Wong 2011 (palette): https://www.nature.com/articles/nmeth.1618 (S for hexes: https://www.npmjs.com/package/safepalette)
- Paul Tol, colour schemes: https://sronpersonalpages.nl/~pault/
- Machado, Oliveira, Fernandes 2009: https://www.inf.ufrgs.br/~oliveira/pubs_files/CVD_Simulation/CVD_Simulation.html
- Brewer, ColorBrewer scheme types: https://colorbrewer2.org/learnmore/schemes_full.html
- Healey 1996: https://healey.csc.ncsu.edu/publications/15843-choosing-effective-colours-for-data-visualization · UBC TR-96-10: https://www.cs.ubc.ca/tr/1996/tr-96-10 · Healey & Enns 2012 (not read): https://www.csc2.ncsu.edu/faculty/healey/download/tvcg.12a.pdf
- Szafir 2018, Modeling Color Difference for Visualization Design: https://cmci.colorado.edu/visualab/VisColors/ (paper read from https://par.nsf.gov/servlets/purl/10074616)
- IBM Carbon: palettes https://carbondesignsystem.com/data-visualization/color-palettes/ · status indicators https://carbondesignsystem.com/patterns/status-indicator-pattern/
- Adobe Spectrum data viz (S, search excerpt): https://spectrum.adobe.com/page/color-for-data-visualization · Leonardo (S): https://leonardocolor.io/
- Material 3: color_spec_2021.ts https://raw.githubusercontent.com/material-foundation/material-color-utilities/main/typescript/dynamiccolor/color_spec_2021.ts · core_palette.ts https://raw.githubusercontent.com/material-foundation/material-color-utilities/main/typescript/palettes/core_palette.ts · blog (S, summary) https://material.io/blog/science-of-color-design
- Radix Colors: https://www.radix-ui.com/colors/docs/palette-composition/understanding-the-scale · composing a palette (S, excerpt) https://www.radix-ui.com/colors/docs/palette-composition/composing-a-palette · package https://www.npmjs.com/package/@radix-ui/colors
- Tailwind CSS 4.1.11 theme.css: https://www.npmjs.com/package/tailwindcss
- Shopify Polaris: https://www.npmjs.com/package/@shopify/polaris (13.9.5
build/esm/styles.css) · https://shopify.dev/docs/apps/design/visual-design - Wise Neptune tokens: https://www.npmjs.com/package/@transferwise/neptune-tokens (8.20.2
themes/personal*/tokens.css) - Atlassian tokens: https://www.npmjs.com/package/@atlaskit/tokens (20.4.0
figma/atlassian-light.json) - 당근 Seed: https://github.com/daangn/seed-design/blob/dev/packages/rootage/color.yaml
- Robinhood accessibility options: https://www.robinhood.com/us/en/support/articles/accessibility-options
- CIE 1924 V(λ) (S): https://en.wikipedia.org/wiki/Luminous_efficiency_function · Nayatani 1997 implementation: https://colour.readthedocs.io/en/latest/_modules/colour/appearance/hke.html
- CAT16: Li et al. 2017 https://doi.org/10.1002/col.22131 (matrix checked against colour-science)
- Colour count (S): https://data.europa.eu/apps/data-visualisation-guide/colour-for-categories · https://www.cs.ubc.ca/~tmm/talks/sig03.multimodal/mgp00025.txt · https://shop.elsevier.com/books/information-visualization/ware/9780123814647 · ISO 9241-125 scope https://www.iso.org/standard/64839.html (body paywalled) · ISO 22324 https://en.wikipedia.org/wiki/ISO_22324
- Repo (read-only):
packages/app/src/config/themes/{classic-grass.theme,shared-semantics,status-badge-palette}.ts,packages/ui/src/{tokens,flag-medallion-presets,theme-hex}.ts,packages/app/src/domain/rules/tier.config.ts,packages/app/src/config/visual-accents.ts,scripts/check-color-contrast.mjs,scripts/lib/color-contrast-core.mjs,docs/design/research/visual-benchmark-2026-10-08.md(+ lane D appendix).