Contrast, measured
Every ratio below was computed from the resolved token values (WCAG 2.1 relative luminance). Thresholds: 4.5:1 for normal text, 3:1 for large text (≥18.66px bold or ≥24px) and for the visual boundary of a control.
Light
| Ink | Surface | Ratio | |
|---|---|---|---|
body #1C1C1E | cream #FAF8F5 | 16.05:1 | AA |
body #1C1C1E | card #F5F3EF | 15.35:1 | AA |
body #1C1C1E | zebra #F1EEE8 | 14.69:1 | AA |
body #1C1C1E | row hover #DAECE2 | 13.84:1 | AA |
body #1C1C1E | row selected #DED8CE | 12.01:1 | AA |
secondary #636366 | card #F5F3EF | 5.40:1 | AA |
secondary #636366 | zebra #F1EEE8 | 5.17:1 | AA |
tertiary #8E8E93 | card #F5F3EF | 2.94:1 | disabled / hint only |
accent ink iq #036B4E | zebra #F1EEE8 | 5.64:1 | AA |
accent ink rei #036EBC | zebra #F1EEE8 | 4.58:1 | AA |
accent ink roof #B84A19 | zebra #F1EEE8 | 4.50:1 | AA |
header ink #FAF8F5 | header band #18181B | 16.71:1 | AA |
destructive #A4231C | card #F5F3EF | 6.68:1 | AA |
caution #8A5A00 | card #F5F3EF | 5.35:1 | AA |
Dark
| Ink | Surface | Ratio | |
|---|---|---|---|
body #F5F3EF | charcoal #18181B | 15.99:1 | AA |
body #F5F3EF | card #2C2C2E | 12.58:1 | AA |
body #F5F3EF | zebra #29292B | 13.10:1 | AA |
body #F5F3EF | row hover #293D38 | 10.42:1 | AA |
body #F5F3EF | row selected #434346 | 8.90:1 | AA |
secondary white 60% | card #2C2C2E | ≈6.65:1 | AA |
tertiary white 40% | card #2C2C2E | ≈4.04:1 | large / labels only |
accent ink iq #34D399 | card #2C2C2E | 7.25:1 | AA |
accent ink rei #0495FE | card #2C2C2E | 4.47:1 | AA-large |
accent ink roof #FF6723 | card #2C2C2E | 4.79:1 | AA |
header ink #F5F3EF | header band #0F0F11 | 17.28:1 | AA |
destructive #FF5F57 | card #2C2C2E | 4.66:1 | AA |
caution #FEBC2E | card #2C2C2E | 8.26:1 | AA |
Two known gaps. Do not "fix" them silently
1. White on the accent fill fails. The primary button paints
--primary-foreground: #FFFFFF on the brand accent:
| Brand | White on accent | Ink #1C1C1E on accent |
|---|---|---|
iq #10B981 | 2.54:1 | 6.71:1 |
rei #0495FE | 3.12:1 | 5.46:1 |
roof #FF6723 | 2.91:1 | 5.85:1 |
The accents are mid-lightness, so white loses on all three and ink wins on all
three. Switching --primary-foreground to the ink token fixes every brand at
once and changes how every primary button looks — that is a brand-owner
decision, not a developer one. It is recorded here with the numbers so the
decision can be made rather than inherited. The kit ships the current values so
a new app matches the live product.
Where tertiary is allowed, and where it is a bug. #8E8E93 is 3.08:1 on
cream — the disabled tier, which WCAG exempts. It is correct for a glyph (a
breadcrumb separator, a chevron), for placeholder text, and for a badge on a row
that is itself disabled. It is WRONG for anything a reader has to read: a
specimen's label, a finding like "Non-metro", a count, a field name. Four of
those were caught and lifted on 2026-08-20 — each had been 3.08:1 and is now
5.65:1.
The test is one question: if this text went missing, would the reader lose something? If yes, it is secondary.
2. Tertiary ink is below 4.5:1 in light. #8E8E93 on card is 2.94:1. It is
correct for genuinely disabled text and placeholder hints (which WCAG exempts)
and wrong for anything a reader has to read. If a label matters, it is
secondary, not tertiary.
The one thing the kit did change
--accent-ink — the accent as TEXT — was hard-coded to emerald in the source
app for both themes. Correct for iq, and it painted green ink on a Roof or
REI app, which breaks the system's central promise. In this kit it is
per-brand, darkened in light mode until it clears 4.5:1 against the zebra row
(the worst of the three light surfaces): iq #036B4E, rei #036EBC, roof
#B84A19. Dark mode keeps the raw accent, which already clears it on titanium.
The guide's own display value (--brand-accent-text) is untouched and still
used for large type, where 3:1 applies.
Rules that follow from the numbers
- Never use
--brand-accentas text. It fails in all three brands. Use--accent-ink. - Any new surface must keep body ink ≥4.5:1 and hold the row ladder's floor (≥8.9:1 was the measured worst case across the four row states).
- A control's boundary needs 3:1 against what is behind it — that is why the
select chevron is a full-opacity muted token and not
opacity-50on top of one. - Colour is never the only cue: selection also has an inset bar, an error also
has a title and an icon, a
soonrow also has a badge. - Recompute rather than eyeball. The script that produced these numbers is twenty lines of luminance arithmetic; the ratios in this file were not estimated.