Skip to content

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 ​

  1. Brand #006D37 is sound and stays; success #14804A is 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 #14804A is 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 #00796A for 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 #014B40 for 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). #00796A sits at the hue of the shipped "open/recruiting" accent badge, so success and accent become one teal and brand is the only grass-green.
  2. 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.
  3. Optic lime #C6E84B is 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).
  4. The content hues collide with the semantic ones; fix three pairs, not "add colours". Court grass #006633 vs brand 2.3 (identical); court clay vs grass protan 1.7; clay #B84A2E vs 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).
  5. 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 #006D37 on 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.
  6. Dark parity fails in three places today: dark $error text #D4736E is Lc 40.5 on the card (the gate has no APCA watch on $error; helper LUT: 58 px at weight 400); dark success #68BF8E vs dark primary #70D5A2 is Δ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 #E2B13C with 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.

RoleCurrentIssueEvidence (URL)Final (light / dark)Key numbers
Brand solid (CTA, hero block, toggle on)#006D37 / #70D5A2none; L* 39.9 / 78.3 are the Material tone-40 / tone-80 logicMaterial primary tone 40 light, 80 dark, on-primary 100/20: color_spec_2021.tskeep #006D37 / #70D5A2white-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/basementfine 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.5Polaris 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) / #D4A849text-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 inkPolaris 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.2keep $warning #946200 for icons/graphics (5.24:1); add $warningText #6B4A00 / #E1B75C; add $warningFill #F5B301 / #E2B13C + $onWarningFill #0D100Efill 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 / #D4736Elight is right: OKLCH h 27.0 vs peers 24.5–28.8; dark text is Lc 40.5Polaris 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.tslight 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 / #C7B5F2none in the set; purple is shared with tier master (h 302 vs 315)—keepon-white Lc 97.2 (12.95:1); vs brand 50.5/50.6/44.3/36.7; dark Lc −65.7
Optic (level accent)#C6E84Bzero 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 hkekeep #C6E84B both modes; $levelChipBg = optic, $levelChipText = #0D100Eink-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 / neutralgray ladder, canvas #F8FBF9, basement #F0F7F3, track #E5EBE7within envelope; basement is 2.7× Radix sage chromaRadix tinted greys pair with accent hue — Radix scales; M3 neutral chroma 4 / variant 8 — core_palette.tskeep all; dark $controlTrack #374039, $controlPill #515B53basement ΔL* 3.4 vs white; track vs white 1.209:1, vs basement 1.111:1; dark track vs card 1.544:1
Court triohard #247BA0, clay #B84A2E, grass #006633grass = brand (ΔE 2.3); clay ≈ error (6.6; deutan 2.9); clay vs grass protan 1.7Okabe–Ito: avoid colours between yellow and green, and red/green combinations — jfly.uni-koeln.de/coloralias 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 quintetbronze #CD7F32, silver #8E8E93, gold #D4A017, master #9B59B6, grandmaster #E74C3Cbronze–GM deutan 3.4; bronze–gold deutan 7.5; gold 2.38:1 on whiteWCAG 1.4.11 3:1 — Understandingbronze #B2580B, gold #D9A00F, grandmaster #9A0F3A; dark bronze #DA8A11, grandmaster #D8516Alight 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)noneno governed chart palette; Carbon/Spectrum/Tol all publish oneCarbon 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 #67D3FClight 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 huecontinuous 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 budgetbrand, ink, neutral, semantic ×4, optic, content ≤ 510 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 capHealey 1996: "up to seven isoluminant colors" identified rapidly — Healeyper-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) and text-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 information are the same blue #1868DB; success is olive #5B7F24 / text #4C6B1F (OKLCH h 129); warning is a bright fill #FBC828 with 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 #00796A becomes 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 #BD2703 that 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 #0D100E on 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 $borderSubtle edge (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 (#9FE870 with #163300), Atlassian (warning.bold with inverse #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:

searchmin ΔE n / p / d / t
equal L = 0.60, free hues11.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.6019.4 / 5.2 / 5.2 / 10.4
one hue per family, L 0.52 / 0.58 / 0.6420.5 / 13.4 / 12.2 / 7.3
one hue per family, L 0.48 / 0.56 / 0.6420.9 / 15.1 / 15.0 / 11.9
free hues (not family-constrained), L 0.48 / 0.56 / 0.6420.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.1122.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):

roledark valueLc on card / canvas / tertiarynote
primary#70D5A2 (kept)67.8 / 69.5 / 60.7ink-on Lc 70.4
success, successText#75D1C667.6 / 69.3 / 60.4vs 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#E1B75C64.9 / 66.6 / 57.7was 56.8
warning fill + ink#E2B13C + #0D100Eink-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#FA9B9460.4 / 62.1 / 53.2M3 #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.4brightest object in dark by design; chip area ≤ 1%
brand block (new)#1E5236white-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 #67D3FC5.85–9.74:1 on card; Lc 45.8–70.5min ΔE 18.9 / 10.0 / 10.1 / 7.1
tiersbronze #DA8A11, grandmaster #D8516A (silver/gold/master kept)6.0 / 4.2:1 on cardmin 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:

contexthue families on screencount
홈 hubbrand block 153 · optic chips 121 · success/open chip 180 · warning chip 80 · court dots 225 + 48 + 1197
기록 hubbrand block 153 · optic progress 121 · tier ring (1 hue) · success delta 180 · 1 series line colour≤ 6
session detailbrand CTA 153 · court dot (1) · success/warning/error chip (1–2)≤ 4
chart5 series hues, no semantic hue inside the plot5

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) ​

fgbgLcWCAGmin px @400@500@700
#FFFFFF#006D37-86.66.4816.315.614
#006D37#FFFFFF81.56.481715.814
#006D37#F0F7F375.75.9517.91614
#FFFFFF#14804A-79.44.9817.315.814
#14804A#FFFFFF74.04.9818.316.414.1
#FFFFFF#00796A-81.35.3317.115.814
#00796A#FFFFFF75.95.3317.91614
#00796A#E0F8F368.74.7920.118.314.6
#00796A#F0F7F370.14.9019.51814.5
#FFFFFF#B8332E-83.65.9116.715.714
#B8332E#FFFFFF78.35.9117.515.914
#B8332E#FDECEC69.35.1819.818.114.6
#946200#FFF4D669.44.7819.818.114.6
#6B4A00#FFF4D681.47.361715.814
#6B4A00#FFFFFF87.78.0616.215.614
#0D100E#F5B30168.810.3220.118.214.6
#4B0082#F2E8FF85.910.9516.415.614
#FFFFFF#4B0082-100.812.9514.914.412.8
#0D100E#C6E84B84.113.7116.615.714
#C6E84B#006D37-63.24.6422.619.715.4
#C6E84B#FFFFFF19.11.40n/an/an/a
#C6E84B#F0F7F313.21.28n/an/an/a
#0D100E#F0F7F399.717.591514.513
#0D100E#FFFFFF105.519.1314.413.911.9
#4F5851#F0F7F379.86.7817.315.814
#626C64#F0F7F371.45.0219.117.414.4
#4F5851#FFFFFF85.67.3816.415.614
#626C64#FFFFFF77.35.4617.715.914
#0D100E#70D5A270.410.6919.417.814.5
#70D5A2#1A201B-67.89.2620.518.414.7
#75D1C6#1A201B-67.69.2320.618.514.7
#E1B75C#1A201B-64.98.7921.81915
#FA9B94#1A201B-60.48.0423.820.815.9
#D4736E#1A201B-40.55.1258.346.531.3
#C7B5F2#1A201B-65.78.9421.418.914.9
#C6E84B#1A201B-82.511.8816.915.714
#0D100E#E2B13C65.29.6521.61915
#EFF3F0#1A201B-97.414.8115.314.813.3
#BBC3BD#1A201B-67.29.2020.718.614.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 huebest hexL / CΔE vs brand n / p / d / ton-white Lcwhite-on Lcreads as
150#1F542E0.400 / 0.0858.8 / 8.5 / 7.0 / 8.190.194.6brand green
155#1C54340.400 / 0.0809.3 / 9.6 / 8.1 / 8.090.194.5
160#16563A0.405 / 0.0809.5 / 10.0 / 9.3 / 7.089.493.9green
165#0A553D0.400 / 0.08010.6 / 11.7 / 11.0 / 7.389.894.3
170#0358440.410 / 0.08011.3 / 12.5 / 12.4 / 6.188.793.3jade green
175#2D79680.525 / 0.08012.6 / 13.6 / 14.7 / 6.075.580.9jade
180#29796C0.525 / 0.08014.1 / 15.3 / 17.4 / 6.175.580.8teal (= shipped accent badge hue)
185#1B7A710.525 / 0.08515.8 / 17.7 / 20.3 / 6.875.280.6teal
190#2279750.525 / 0.08017.9 / 20.7 / 22.4 / 6.975.380.7blue-teal
195#0D76770.515 / 0.08519.8 / 23.4 / 24.5 / 6.276.681.9blue-teal
200#13797E0.525 / 0.08522.0 / 25.3 / 26.0 / 7.575.280.5cyan-teal
205#1478820.525 / 0.08524.4 / 27.8 / 28.0 / 7.775.480.7cyan
210#0478870.525 / 0.09026.7 / 30.0 / 29.7 / 8.375.280.6cyan

A. Role numbers: current vs final (light) ​

rolehexL*C*h(Lab)OKLCH L/C/hon white Lcon white WCAGwhite-on Lcink-on Lcon tint Lc (WCAG)
brand (kept)#006D3739.946.31510.469/0.121/15381.56.48-86.622.0-
success CURRENT#14804A47.147.11530.530/0.125/15574.04.98-79.429.667.9 (4.56)
success FINAL (icon / fill / large figure)#00796A45.232.41800.516/0.094/18075.95.33-81.327.668.7 (4.79)
successText FINAL (small text, badge text)#014B4027.923.51780.370/0.067/17993.010.09-97.110.485.7 (9.07)
warning text CURRENT#94620045.755.0760.537/0.114/7475.75.24-81.127.869.4 (4.78)
warning text FINAL#6B4A0034.043.8790.434/0.090/7987.78.06-92.415.881.4 (7.36)
warning fill FINAL (ink text)#F5B30177.180.7810.807/0.166/8234.61.85-38.768.8-
error (kept)#B8332E42.463.1330.524/0.171/2778.35.91-83.625.269.3 (5.18)
info (kept)#4B008220.574.33140.339/0.179/30297.212.95-100.80.085.9 (10.95)
optic (kept)#C6E84B87.176.21150.878/0.182/12119.11.40-21.884.1-

B1. CURRENT set: brand, success, warning, error, info, optic ​

pairnormalprotandeutantritanflagged (<10)
brand vs success6.77.06.47.0normal, protan, deutan, tritan
brand vs warning36.17.714.946.9protan
brand vs error61.06.511.358.7protan
brand vs info50.550.644.336.7-
brand vs optic43.640.343.942.1-
success vs warning36.510.213.548.2-
success vs error61.913.29.560.9deutan
success vs info52.752.445.440.5-
success vs optic37.433.737.435.7-
warning vs error27.612.14.212.0deutan
warning vs info59.258.359.928.1-
warning vs optic42.837.331.846.9-
error vs info39.746.455.833.2-
error vs optic67.448.533.353.9-
info vs optic102.587.585.369.1-

B2. FINAL set, TEXT level (light): brand, successText, warningText, error, info, optic, neutral-badge text ​

pairnormalprotandeutantritanflagged (<10)
brand vs successText16.317.618.110.4-
brand vs warnText31.99.69.843.2protan, deutan
brand vs error61.06.511.358.7protan
brand vs info50.550.644.336.7-
brand vs optic43.640.343.942.1-
brand vs neutral27.223.120.017.8-
successText vs warnText31.618.524.340.7-
successText vs error51.512.928.353.9-
successText vs info37.030.523.332.1-
successText vs optic60.558.162.254.8-
successText vs neutral23.613.613.118.5-
warnText vs error28.97.410.317.4protan
warnText vs info55.053.453.720.1-
warnText vs optic51.950.045.455.4-
warnText vs neutral27.928.228.627.0-
error vs info39.746.455.833.2-
error vs optic67.448.533.353.9-
error vs neutral29.521.927.733.7-
info vs optic102.587.585.369.1-
info vs neutral27.025.024.425.2-
optic vs neutral50.950.349.637.4-

B3. FINAL FILL / ICON level (light): brand, success, warningFill, error, info, optic, neutral-badge text ​

pairnormalprotandeutantritanflagged (<10)
brand vs success13.414.717.26.0tritan
brand vs warnFill49.631.940.757.4-
brand vs error61.06.511.358.7protan
brand vs info50.550.644.336.7-
brand vs optic43.640.343.942.1-
brand vs neutral27.223.120.017.8-
success vs warnFill48.333.843.854.3-
success vs error55.516.224.258.9-
success vs info44.937.027.139.7-
success vs optic44.140.846.136.9-
success vs neutral23.310.43.018.4deutan
warnFill vs error46.440.429.530.7-
warnFill vs info85.479.783.757.3-
warnFill vs optic25.89.75.833.9protan, deutan
warnFill vs neutral45.543.847.541.3-
error vs info39.746.455.833.2-
error vs optic67.448.533.353.9-
error vs neutral29.521.927.733.7-
info vs optic102.587.585.369.1-
info vs neutral27.025.024.425.2-
optic vs neutral50.950.349.637.4-

B4. CURRENT set plus neutral badge text (for comparison) ​

pairnormalprotandeutantritanflagged (<10)
brand vs success6.77.06.47.0normal, protan, deutan, tritan
brand vs warning36.17.714.946.9protan
brand vs error61.06.511.358.7protan
brand vs info50.550.644.336.7-
brand vs optic43.640.343.942.1-
brand vs neutral27.223.120.017.8-
success vs warning36.510.213.548.2-
success vs error61.913.29.560.9deutan
success vs info52.752.445.440.5-
success vs optic37.433.737.435.7-
success vs neutral27.523.519.119.4-
warning vs error27.612.14.212.0deutan
warning vs info59.258.359.928.1-
warning vs optic42.837.331.846.9-
warning vs neutral29.529.130.829.0-
error vs info39.746.455.833.2-
error vs optic67.448.533.353.9-
error vs neutral29.521.927.733.7-
info vs optic102.587.585.369.1-
info vs neutral27.025.024.425.2-
optic vs neutral50.950.349.637.4-

C. Brand vs success: ΔE2000 (normal / protan / deutan / tritan) against brand #006D37 ​

candidatehexOKLCH hvs brandvs error #B8332Eon-white Lcwhite-on Lcnote
CURRENT success#14804A1556.7 / 7.0 / 6.4 / 7.061.9 / 13.2 / 9.5 / 60.974.079.4shipped
Polaris bg-fill-success (npm @shopify/polaris 13.9.5)#047B5D1689.0 / 9.6 / 10.3 / 6.158.7 / 13.0 / 17.1 / 59.775.680.9h168
Polaris text-success#014B4017916.3 / 17.6 / 18.1 / 10.451.5 / 12.9 / 28.3 / 53.993.097.1
Wise sentiment-positive (npm @transferwise/neptune-tokens 8.20.2)#054D2815410.5 / 10.8 / 9.8 / 10.156.7 / 4.3 / 20.3 / 55.592.896.9dark forest
Atlassian text.success (npm @atlaskit/tokens 20.4.0)#4C6B1F12911.4 / 5.6 / 7.3 / 11.051.2 / 10.0 / 5.5 / 44.680.485.6olive
Seed fg.positive (daangn/seed-design color.yaml)#07917117015.3 / 16.1 / 15.7 / 14.261.4 / 20.9 / 18.2 / 62.866.471.9
Carbon green60#1980381487.2 / 7.6 / 7.3 / 6.363.2 / 14.0 / 5.1 / 61.074.279.6
FINAL success (icon/fill)#00796A18013.4 / 14.7 / 17.2 / 6.055.5 / 16.2 / 24.2 / 58.975.981.3h180
FINAL successText#014B4017916.3 / 17.6 / 18.1 / 10.451.5 / 12.9 / 28.3 / 53.993.097.1h179, L* 27.9
cyan end of window (ΔE>=20 feasible)#0D767719619.8 / 23.4 / 24.5 / 6.250.7 / 23.4 / 32.1 / 57.876.681.9h195-200: reads blue-teal

D. Categorical sets: min pairwise ΔE2000 per vision type, L* spread, 3:1 on white / basement ​

setnmin ΔE normal / protan / deutan / tritanL* range (spread)>=3:1 on white>=3:1 on basement #F0F7F3worst deutan pairworst protan pair
Okabe-Ito 7 (no black)721.7 / 12.3 / 11.5 / 11.146.0-89.1 (43.1)4/73/7#E69F00/#F0E442 11.5#0072B2/#CC79A7 12.3
Tol bright 6 (no grey)623.7 / 14.0 / 14.7 / 8.741.6-77.4 (35.8)4/63/6#4477AA/#AA3377 14.7#4477AA/#AA3377 14.0
Tol vibrant 6 (no grey)618.3 / 16.1 / 14.5 / 8.646.0-71.2 (25.2)4/64/6#EE7733/#CC3311 14.5#0077BB/#EE3377 16.1
Tol muted 9915.0 / 11.8 / 14.0 / 11.622.4-81.8 (59.4)6/95/9#CC6677/#44AA99 14.0#332288/#882255 11.8
Tableau 101018.1 / 1.3 / 0.7 / 7.049.6-82.0 (32.4)5/104/10#E15759/#59A14F 0.7#FF9DA7/#BAB0AC 1.3
Tableau 10 first 5522.9 / 6.2 / 0.7 / 9.649.6-70.1 (20.5)3/52/5#E15759/#59A14F 0.7#F28E2B/#59A14F 6.2
Carbon categorical first 5525.7 / 9.5 / 15.8 / 18.035.2-58.5 (23.4)5/55/5#005D5D/#9F1853 15.8#005D5D/#9F1853 9.5
Carbon categorical 141412.2 / 5.8 / 3.0 / 4.515.2-58.5 (43.4)14/1414/14#1192E8/#A56EFF 3.0#1192E8/#A56EFF 5.8
d3 category10 first 5526.4 / 1.4 / 4.8 / 11.946.8-66.9 (20.0)4/54/5#2CA02C/#D62728 4.8#FF7F0E/#2CA02C 1.4
OURS court trio (light, current)336.2 / 1.7 / 15.7 / 13.037.4-48.4 (11.0)3/33/3#B84A2E/#006633 15.7#B84A2E/#006633 1.7
OURS court trio (dark, current)328.2 / 9.1 / 14.7 / 6.066.0-70.4 (4.4)0/30/3#E09050/#4DB380 14.7#E09050/#4DB380 9.1
OURS tiers (light, current)514.8 / 10.0 / 3.4 / 8.748.9-68.9 (20.0)4/52/5#CD7F32/#E74C3C 3.4#CD7F32/#D4A017 10.0
OURS tiers (dark, current)519.7 / 13.2 / 4.8 / 10.664.0-86.9 (22.9)0/50/5#D4944A/#FF6B6B 4.8#D4944A/#FF6B6B 13.2
PROPOSED series 1-5 (light)522.6 / 15.1 / 14.9 / 8.835.2-59.1 (23.9)5/55/5#3566B8/#883170 14.9#3566B8/#883170 15.1
PROPOSED series 1-5 (dark)518.9 / 10.0 / 10.1 / 7.163.4-80.0 (16.6)0/50/5#DA875A/#BBD068 10.1#6C9EF1/#D57CB8 10.0

E1. Court trio CURRENT: hard #247BA0, clay #B84A2E, grass #006633 ​

pairnormalprotandeutantritanflagged (<10)
hard vs clay47.938.743.655.4-
hard vs grass36.237.335.013.0-
clay vs grass54.91.715.756.3protan

E2. Court trio PROPOSED (= series 5, 2, 3): hard #089AC3, clay #8D3D00, grass #83971F ​

pairnormalprotandeutantritanflagged (<10)
hard vs clay54.451.149.960.2-
hard vs grass48.146.948.422.2-
clay vs grass42.627.420.038.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) ​

pairnormalprotandeutantritanflagged (<10)
bronze vs silver27.527.328.024.7-
bronze vs gold14.810.07.58.7deutan, tritan
bronze vs master48.651.951.118.7-
bronze vs grandmaster21.112.23.410.5deutan
silver vs gold31.431.231.924.3-
silver vs master25.122.720.121.4-
silver vs grandmaster29.225.126.730.0-
gold vs master61.857.955.922.2-
gold vs grandmaster36.522.110.918.2-
master vs grandmaster35.544.649.223.5-

F2. Tiers PROPOSED (light): bronze #B2580B, gold #D9A00F, grandmaster #9A0F3A (silver, master kept) ​

pairnormalprotandeutantritanflagged (<10)
bronze vs silver30.331.829.328.3-
bronze vs gold25.823.518.420.5-
bronze vs master45.251.150.118.0-
bronze vs grandmaster30.126.818.111.4-
silver vs gold31.731.332.324.9-
silver vs master25.122.720.121.4-
silver vs grandmaster36.131.128.036.2-
gold vs master61.758.056.223.0-
gold vs grandmaster56.447.436.934.5-
master vs grandmaster28.029.639.424.6-
tiercurrenton white WCAGproposedon white WCAG
bronze#CD7F323.14#B2580B4.89
silver#8E8E933.26#8E8E933.26
gold#D4A0172.38#D9A00F2.34
master#9B59B64.67#9B59B64.67
grandmaster#E74C3C3.82#9A0F3A8.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) ​

pairnormalprotandeutantritanflagged (<10)
s1 blue vs s2 terracotta45.850.552.758.5-
s1 blue vs s3 olive-lime61.756.457.024.0-
s1 blue vs s4 plum30.415.114.954.4-
s1 blue vs s5 sky22.616.615.516.3-
s2 terracotta vs s3 olive-lime42.627.420.038.0-
s2 terracotta vs s4 plum36.740.835.88.8tritan
s2 terracotta vs s5 sky54.451.149.960.2-
s3 olive-lime vs s4 plum68.053.542.838.7-
s3 olive-lime vs s5 sky48.146.948.422.2-
s4 plum vs s5 sky54.330.221.066.5-

G2. PROPOSED series 1-5 (dark) ​

pairnormalprotandeutantritanflagged (<10)
s1 vs s242.946.150.165.9-
s1 vs s358.253.253.825.4-
s1 vs s432.110.014.561.7-
s1 vs s518.913.512.112.6-
s2 vs s337.317.010.133.3-
s2 vs s432.837.333.57.1tritan
s2 vs s549.242.145.259.6-
s3 vs s462.846.137.034.0-
s3 vs s545.544.546.624.0-
s4 vs s550.417.413.369.9-

G3. Series values ​

tokenlightOKLCH (light)WCAG on white / basementdarkWCAG on dark card / canvasLc on dark card
$series1#3566B80.52/0.140/2605.61 / 5.16#6C9EF16.16 / 7.3047.9
$series2#8D3D000.46/0.125/487.50 / 6.89#DA875A6.00 / 7.1146.8
$series3#83971F0.64/0.140/1193.28 / 3.01#BBD0689.74 / 11.5470.5
$series4#8831700.46/0.140/3407.72 / 7.10#D57CB85.85 / 6.9345.8
$series5#089AC30.64/0.120/2263.27 / 3.01#67D3FC9.71 / 11.5170.5

H. Dark parity: current vs proposed on card #1A201B / canvas #090C0A / tertiary #374039 ​

rolecurrentLc cardWCAG cardproposedLc cardLc canvasLc tertiaryWCAG card
primary#70D5A267.89.26#70D5A267.869.560.79.26
success#68BF8E56.57.45#75D1C667.669.360.49.23
warning text#D4A84956.87.50#E1B75C64.966.657.78.79
error#D4736E40.55.12#FA9B9460.462.153.28.04
info#C7B5F265.78.94#C7B5F265.767.458.58.94
optic#C6E84B82.511.88#C6E84B82.584.275.411.88

H2. DARK current: primary, success, warning, error, info, optic ​

pairnormalprotandeutantritanflagged (<10)
primary vs success5.55.55.35.5normal, protan, deutan, tritan
primary vs warning33.315.016.448.4-
primary vs error57.621.212.861.4-
primary vs info38.935.829.327.6-
primary vs optic22.117.920.220.0-
success vs warning31.712.615.946.5-
success vs error54.816.18.258.0deutan
success vs info38.735.830.427.3-
success vs optic23.420.022.221.0-
warning vs error32.520.713.913.1-
warning vs info47.647.645.621.9-
warning vs optic23.814.410.333.8-
error vs info31.335.337.429.5-
error vs optic56.132.923.042.0-
info vs optic61.851.748.417.3-

H3. DARK proposed: primary, success, warningText, error, info, optic ​

pairnormalprotandeutantritanflagged (<10)
primary vs success12.013.916.31.8tritan
primary vs warnText31.912.215.647.3-
primary vs error54.310.05.157.1deutan
primary vs info38.935.829.327.6-
primary vs optic22.117.920.220.0-
success vs warnText34.423.730.747.1-
success vs error48.212.021.456.7-
success vs info32.419.612.327.7-
success vs optic30.528.134.120.6-
warnText vs error30.116.711.08.4tritan
warnText vs info47.146.445.021.0-
warnText vs optic21.611.47.332.3deutan
error vs info27.630.434.824.4-
error vs optic51.725.316.337.0-
info vs optic61.851.748.417.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 ​

pairnormalprotandeutantritanflagged (<10)
bronze vs silver26.026.126.225.9-
bronze vs gold22.818.916.715.6-
bronze vs master41.843.141.615.4-
bronze vs grandmaster27.113.24.810.6deutan
silver vs gold34.433.734.623.2-
silver vs master19.714.612.816.6-
silver vs grandmaster27.818.522.521.5-
gold vs master58.651.049.415.7-
gold vs grandmaster49.729.719.525.0-
master vs grandmaster28.032.937.522.6-

dark tiers PROPOSED (bronze #DA8A11, grandmaster #D8516A; silver, gold, master kept) ​

pairnormalprotandeutantritanflagged (<10)
bronze vs silver29.730.129.527.2-
bronze vs gold23.319.315.918.7-
bronze vs master46.447.245.218.6-
bronze vs grandmaster37.727.517.711.2-
silver vs gold34.433.734.623.2-
silver vs master19.714.612.816.6-
silver vs grandmaster29.521.019.332.6-
gold vs master58.651.049.415.7-
gold vs grandmaster58.141.330.129.4-
master vs grandmaster26.328.332.725.4-

H5. Control track / pill ​

  • track #E5EBE7 on #FFFFFF: WCAG 1.209, ΔL* 7.5
  • track #E5EBE7 on #F0F7F3: WCAG 1.111, ΔL* 4.1
  • track #E5EBE7 on #F8FBF9: WCAG 1.161, ΔL* 5.8
  • track #374039 on #1A201B: WCAG 1.544, ΔL* 14.6
  • track #374039 on #090C0A: WCAG 1.829, ΔL* 23.0
  • track #2A332C on #1A201B: WCAG 1.270, ΔL* 8.7
  • dark pill #515B53 on track #374039: WCAG 1.520 ; text #EFF3F0 on pill Lc 80.9
  • light pill #FFFFFF on track #E5EBE7: WCAG 1.209

I. Peer semantic/brand values read from published packages (hex -> our metrics) ​

system / tokenhexL*Lab hOKLCH hon-white Lcwhite-on Lcsource
Polaris --p-color-bg-fill-brand (light)#30303019.91589099.6102.8@shopify/polaris 13.9.5 build/esm/styles.css
Polaris bg-fill-success#047B5D45.616716875.680.9same
Polaris text-success#014B4027.917817993.097.1same
Polaris bg-fill-critical#C70A2442.0322477.582.8same
Polaris bg-fill-warning#FFB80079.4808131.135.0same
Polaris text-warning#5E420030.1818191.395.6same
Wise interactive-primary (personal)#16330018.1130135100.3103.4@transferwise/neptune-tokens 8.20.2 themes/personal/tokens.css
Wise interactive-accent#9FE87085.113013522.225.3same
Wise sentiment-positive#054D2828.115215492.896.9same
Wise sentiment-negative#CB272F44.7312575.380.7same
Wise sentiment-warning#FFD11A85.5899221.624.6same
Wise dark sentiment-positive#BAE5A086.513213420.122.9themes/personal--dark
Atlassian background.brand.bold = information.bold#1868DB45.928825975.180.4@atlaskit/tokens 20.4.0 figma/atlassian-light.json
Atlassian success.bold#5B7F2448.912212972.277.7same
Atlassian text.success#4C6B1F41.512312980.485.6same
Atlassian danger.bold#C9372C46.1362974.279.7same
Atlassian warning.bold#FBC82882.9878925.729.1same
Atlassian text.warning#9E4C0041.9605279.584.7same
Carbon green60 / red60 / yellow30 / orange40#19803846.914514874.279.6carbondesignsystem.com/data-visualization/color-palettes (alert palette)
red60#DA1E2847.0332672.577.9same
yellow30#F1C21B80.4889029.633.4same
Seed (당근) fg.positive light#07917153.516917066.471.9github.com/daangn/seed-design packages/rootage/color.yaml
Seed fg.critical light#FA342C55.0362863.368.7same
Seed bg.brand-solid (carrot-600)#FF660062.3524354.860.1same
Seed fg.positive dark#22B27F64.716216352.157.3same

J. Tinted neutrals: CIELAB C* by step (Radix 3.0.0 light; first sRGB block of each CSS file) ​

scaleC* steps 1-12mean C* steps 1-8max C* steps 1-8step 9step 11
radix gray0.0 0.0 0.0 0.0 0.0 0.0 0.0 0.0 0.0 0.0 0.0 0.00.00.00.00.0
radix sage0.9 0.9 1.2 1.2 1.5 1.2 1.8 1.8 3.6 3.6 2.8 4.11.31.83.62.8
radix olive0.6 1.2 1.3 1.3 1.9 1.9 1.9 2.4 4.4 4.5 4.2 4.21.62.44.44.2
radix sand0.5 0.5 0.6 1.1 1.6 1.6 2.1 3.2 4.0 3.4 3.0 2.91.43.24.03.0
radix slate0.5 1.0 1.5 2.1 2.6 3.7 4.2 5.9 6.3 5.8 5.0 3.42.75.96.35.0
radix mauve0.6 1.1 2.3 2.8 3.9 4.3 5.4 7.0 7.4 7.0 6.1 5.23.47.07.46.1

Tailwind CSS 4.1.11 theme.css neutral families, OKLCH chroma (steps 50/100/200/300/400/500/600/700/800/900/950):

familychroma by step
slate0.003 0.007 0.013 0.022 0.04 0.046 0.043 0.044 0.041 0.042 0.042
gray0.002 0.003 0.006 0.01 0.022 0.027 0.03 0.034 0.033 0.034 0.028
zinc0 0.001 0.004 0.006 0.015 0.016 0.017 0.013 0.006 0.006 0.005
neutral0 0 0 0 0 0 0 0 0 0 0
stone0.001 0.001 0.003 0.005 0.01 0.013 0.011 0.01 0.007 0.006 0.004
ourshexL*C* (Lab)OKLCH Chue (OKLCH)
canvas#F8FBF998.31.50.0041157
basement (proposed)#F0F7F396.63.20.0091161
controlTrack#E5EBE792.53.00.0083157
hairline gray200#DFE7E190.94.20.0117154
border gray300#CAD4CC84.05.60.0154152
gray400#A8B5AB72.47.30.0202153
gray500 (borderStrong)#7E8A8056.37.50.0204150
textTertiary gray600#626C6444.66.30.0174151
textSecondary gray700#4F585136.45.80.0160152
ink gray950#0D100E4.41.50.0063156
dark canvas#090C0A3.11.20.0066157
dark card#1A201B11.54.80.0129150
dark gray300#262D2817.74.80.0133154
dark gray500#515B5337.66.50.0179151

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) ​

stimuluscanvas #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 #006D370.270.760.550.693.71
success #00796A0.471.030.981.017.04
error #B8332E0.240.650.460.463.32
optic #C6E84B0.531.070.881.075.98
info #4B00820.160.210.210.211.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) ​

colourhexY (white=1)L*Gamma VCCGamma VAC
optic#C6E84B0.70287.10.9301.045
warning fill#F5B3010.51777.10.8901.052
brand#006D370.11239.91.0561.074
success#00796A0.14745.21.0571.054
hard (current)#247BA00.17148.41.1211.074
clay (current)#B84A2E0.15346.01.1961.140
error#B8332E0.12842.41.3111.184
info#4B00820.03120.51.5901.259
grey 8E8E93#8E8E930.27259.21.0121.006

M. Optic lime #C6E84B: appearance under simulated deficiency, and contrast to grounds ​

visionsimulated opticvs white dEvs basement dEvs brand dEvs warningFill dE
normal#C6E84B29.227.743.625.8
protan#F6DB3329.528.140.39.7
deutan#F2DA5727.726.643.95.8
tritan#D1DCC913.210.242.133.9

N. Final-fill luminance separation (SC 1.4.1 "3:1 luminance difference" alternative) ​

pairdL*WCAG luminance ratio>= 3:1
brand / success5.31.22no
brand / error2.51.10no
brand / info19.52.00no
brand / warningText5.91.24no
brand / warningFill37.13.49yes
brand / optic47.24.64yes
success / error2.81.11no
success / info24.82.43no
success / warningText11.21.51no
success / warningFill31.82.87no
success / optic41.93.82yes
error / info21.92.19no
error / warningText8.41.36no
error / warningFill34.73.19yes
error / optic44.74.24yes
info / warningText13.61.61no
info / warningFill56.66.99yes
info / optic66.69.28yes
warningText / warningFill43.04.35yes
warningText / optic53.15.78yes
warningFill / optic10.01.33no

O. Series vs semantic roles: ΔE2000 normal/protan/deutan/tritan (! = any of n/p/d < 10) ​

seriesbrandsuccesswarnTextwarnFillerrorinfooptic
s1 blue44/45/41/1232/29/22/949/50/52/5063/62/68/5942/43/51/6428/20/17/3972/66/69/38
s2 terracotta47/10/11/5346/23/26/5414/1/4/10 !43/43/37/3716/7/7/8 !47/53/56/2760/51/41/56
s3 olive-lime26/20/24/2630/26/31/2331/27/23/3526/12/17/3254/27/13/3881/68/65/4121/21/21/21
s4 plum66/38/26/5345/26/10/5445/41/35/1068/64/56/3826/35/35/1418/9/17/21 !83/70/58/57
s5 sky40/42/40/2225/27/22/1650/51/50/5253/49/56/5059/44/46/6349/37/30/5155/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 (=).

tokenlightdarkroleallowed usesnotes / replaces
$primary#006D37#70D5A2brand solidthe screen's one primary CTA, toggle on, checkbox, selected date disc; inline linkunchanged
$brandBlock (new)#006D37#1E5236 (= ramp 300)hero block fillone 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 groundhub/dashboard screens behind white cards; lists stay whiteΔL* 3.4 vs white; C* 3.2
$controlTrack (new)#E5EBE7#374039 (= gray400)segmented/tab/progress trackneutral controls1.209 vs white, 1.111 vs basement; dark 1.544 vs card
$controlPill (new)#FFFFFF#515B53 (= gray500)selected segmentwith 1 px $borderSubtle in lightpill vs track 1.209 / 1.520
$success#00796A#75D1C6success icon / fill / large figurepaid, RSVP yes, +delta glyph, validation OK, always with a check glyph; solid fill with white inkwas #14804A / #68BF8E; SHARED_STATUS_*.success, tokens.ts success, THEME_HEX_FALLBACK.success, toast inverse.successIcon
$successText (new)#014B40#75D1C6success small text, badge textbadge text, inline "+24" at < 18 pxbadge 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#D4A849warning icon / graphic onlylighting icon, facility accentsunchanged (5.24:1 on white, 7.50:1 on dark card)
$warningText (new)#6B4A00#E1B75Cwarning textbadge text on #FFF4D6, inline warning copylight badge text was #946200 (Lc 69.4, 4.78:1) → Lc 81.4, 7.36:1
$warningFill (new)#F5B301#E2B13Cwarning soliddeadline banner, "마감 임박" solid chip$onWarningFill #0D100E / #0D100E; replaces deadlineBannerBg (#946200 / #4A3106) and deadlineBannerText
$error#B8332E#FA9B94errortext, icon, fill (white ink in light)dark was #D4736E (Lc 40.5); SHARED_STATUS_DARK.error, toast inverse.errorIcon
$info#4B0082#C7B5F2infobadge, noticeunchanged
$levelChipBg (new)#C6E84B#C6E84Blevel accent (= optic)NTRP/ELO chip under avatars and in rows, today dot, progress fill inside $brandBlocknever text, never on white without 1 px edge, never beside $warningFill; $levelChipText #0D100E both (Lc 84.1, 13.71:1)
$series1 (new)#3566B8#6C9EF1chart/content hue 1chart series 1OKLCH 0.52/0.14/260
$series2 (new)#8D3D00#DA875Ahue 2 (terracotta)chart series 2; = $courtClayOKLCH 0.46/0.125/48
$series3 (new)#83971F#BBD068hue 3 (olive-lime)chart series 3; = $courtGreenOKLCH 0.64/0.14/119
$series4 (new)#883170#D57CB8hue 4 (plum)chart series 4OKLCH 0.46/0.14/340
$series5 (new)#089AC3#67D3FChue 5 (sky)chart series 5; = $courtHardOKLCH 0.64/0.12/226
$courtHard / $courtClay / $courtGreen#089AC3 / #8D3D00 / #83971F#67D3FC / #DA875A / #BBD068court surface identity12–40 px dots and CourtLinesTile fillswere #247BA0 / #B84A2E / #006633 (dark #67B7D1 / #E09050 / #4DB380); other three themes keep their own court values unless the owner extends
$tierBronze#B2580B#DA8A11tier huetier ring/dot beside a text labeltier.config.ts color/colorDark; bgTint #B2580B15
$tierSilver#8E8E93#A8A9ADtier huesameunchanged
$tierGold#D9A00F#FFD700tier huesame; 2.34:1 on white ⇒ needs an ink outline or numeralbgTint #D9A00F15
$tierMaster#9B59B6#C39BD3tier huesameunchanged
$tierGrandmaster#9A0F3A#D8516Atier huesamewas #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):

  1. status.success/warning/error/info are not derived as text/icon on a surface — they enter only through badge pairs and the toast icons. Today's $error dark #D4736E on the card (Lc 40.5, 5.12:1) and light $success (Lc 74.0) therefore pass silently.
  2. court.* and medal.* are info only; tiers (tier.config.ts), optic, basement, controlTrack, series do not appear at all.
  3. No CVD assertion of any kind.
  4. 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):

idassertionthreshold (kind)final valuecanary (current value that must fail)
A1status.{error,info}, success, successText, warningText as text on surface.primary/secondary and basementWCAG 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.3dark $error #D4736E Lc 40.5 → fail; light $success #14804A Lc 74.0 → warn
A2dark status text on surface.primary, secondary, tertiary, elevatedAPCA ≥ 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
A3solid fills with their ink: success/error/info/brandBlock + text.inverse; warningFill + onWarningFill; levelChipBg + levelChipTextAPCA ≥ 75 (fail) for white-ink fills; ≥ 60 for warningFill (fail), ≥ 65 (warn); levelChip ≥ 7581.3 / 83.6 / 100.8 / 86.6; 68.8; 84.1—
A4optic on brandBlock light/darkWCAG 4.5 (fail)4.64 / 6.50optic on dark primary #70D5A2 (invisible)
A5series1–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—
A6controlTrack vs surface.primary; vs basement; dark vs card; pill vs trackWCAG ≥ 1.2 / ≥ 1.1 / ≥ 1.5 / ≥ 1.2 (fail)1.209 / 1.111 / 1.544 / 1.209gray100 #F0F5F1 (1.10 / 1.01)
A7basement vs card: ΔL* ∈ [3.0, 5.5]; canvas of list screens = #FFFFFF; neutral chroma: Lab C* ≤ 4.5 for L* ≥ 88 and hue 145–165fail3.4; 3.2 / 4.2#EEF2EF (grey-everywhere reading withdrawn in the benchmark)
A8CVD role separation (new cvdSimulate + deltaE2000 in color-contrast-core.mjs; Machado 2009 severity 1.0, matrices applied in linear RGB)config table belowbelowbelow
A9hue-family count of {brand, success, warning fill, error, info, optic, series1–5} at 30° bucketsreport-only; warn above 1010—

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)normalprotandeutantritanfinal measured (n/p/d/t)
brand ↔ success≥ 12≥ 12≥ 12warn ≥ 513.4 / 14.7 / 17.2 / 6.0
brand ↔ successText≥ 14≥ 14≥ 14warn ≥ 816.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≥ 20warn ≥ 7warn ≥ 8—28.9 / 7.4 / 10.3 / 17.4
brand ↔ optic≥ 30≥ 30≥ 30≥ 3043.6 / 40.3 / 43.9 / 42.1
series 1–5 pairwise (light)≥ 20≥ 12≥ 12warn ≥ 822.6 / 15.1 / 14.9 / 8.8
series 1–5 pairwise (dark)≥ 18≥ 9.5≥ 9.5warn ≥ 718.9 / 9.99 / 10.09 / 7.13
court trio pairwise (aliases of series 5, 2, 3)≥ 20≥ 12≥ 12warn ≥ 842.6 / 27.4 / 20.0 / 22.2
grass ↔ brand; clay ↔ error≥ 12warn ≥ 7warn ≥ 7—25.9 / 20.5 / 23.8; 15.9 / 7.4 / 7.5
tiers pairwise (light / dark)≥ 15≥ 10≥ 10warn ≥ 825.1 / 22.7 / 18.1 / 11.4; 19.7 / 14.6 / 12.8 / 11.2
dark primary ↔ dark success≥ 9≥ 12≥ 12warn12.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/polaris 13.9.5, @transferwise/neptune-tokens 8.20.2, @atlaskit/tokens 20.4.0); Seed from color.yaml via 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's CAT_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 #0D100E on white is 14.4 px @400). Under that LUT no pair in this app reaches 14 px at weight 400 (even #0D100E on 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") into badge.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 the tokens-theme-agreement test were not exercised; token names above are proposals.

7. Sources (every claim above traces to one of these; "S" = secondary) ​

Markdown remains the source of truth. Run yarn docs:check before handoff.