Foundations · Color
Foundations

Color

Scout Blue over a cool slate ramp, with neutral grey for text. Every value below is measured against its intended ground, and the measurement is part of the token's definition — not a check someone did once.

Layer 1 — primitives

Declared outside @theme, so they generate no utilities. No component may reference these names. Ratios are on #ffffff.

Token
Hex
On white
Use
--color-scout-blue-600#0077b3
4.90:1
Passes AA small text. An earlier requirement to darken it was over-cautious.
--color-scout-blue-700#005a8c
7.39:1
Hover, and text on tinted fills.
--color-scout-blue-400#21b2fc
2.38:1
Decorative only. Never text, never a border that carries meaning. Source comment says 2.11 — see below.
--color-slate-50#f5f7fa
Muted surface. Alternating page sections, app ground.
--color-slate-200#e2e8f0
The hairline. Also the empty ScoreBar track.
--color-slate-400#94a3b8
2.56:1
Decoration and disabled only.
--color-slate-450#8595a9
3.06:1
The WCAG 1.4.11 floor for a control boundary. Input and secondary-button borders.
--color-slate-500#64748b
4.76:1
The AA floor for small text. Eyebrows, table heads, captions.
--color-grey-600#666666
5.74:1
Body copy — the reading colour.
--color-grey-800#333333
12.63:1
Headlines, figures, the CTA fill.
--color-grey-100#ececec
Neutral, not slate: a media void is not part of the chrome.
Audit note

theme.css and tokens.css both annotate #21b2fc as 2.11:1. Measured, it is 2.38:1 — the checker below agrees. The decision is unaffected (it fails 3:1 either way, and stays decorative-only), but the comment should be corrected at source so nobody re-derives a rule from a wrong number.

Layer 2 — semantic

The only names you write. Named by role, not by shade — which is why surface and surface-raised are two tokens even though they are identical in light mode.

Surfaces
surface
surface-muted
surface-raised
surface-sunken
surface-inverse
Text & borders
text-primaryHeadlines
text-secondaryBody copy
text-mutedCaptions
border
border-strong
border-focus

Action vs commitment

The single most-misapplied rule in the system. --color-primary is not the primary button.

--color-primary
#0077b3 · blue navigates
  • Links and text buttons
  • The focus ring
  • Category icons
  • Select item indicators
--color-cta
#333333 · black commits
  • Button variant="primary"
  • The selected Rating chip
  • Step markers
  • Nothing else

This was briefly reversed on the evidence of a screenshot showing a blue primary button, then reverted the same day. If the shipping product still renders a blue primary button, that is a product divergence from the system, not a system update.

Status — fill and text are different colours

The three brand status hexes are fill colours; all three fail AA as text. Each status therefore ships a pair. -fill may never carry text; the base token may never flood a surface.

Status
Text token
Fill token
Subtle token
Success
Saved
#047857 · 5.48:1
#10b981 · 2.57:1
12% mix
Warning
Unverified
#b45309 · 5.02:1
#f59e0b · 2.15:1
14% mix
Destructive
Delete review
#b91c1c · 6.47:1
#ef4444 · 3.76:1
10% mix

Rating scale

Five tiers plus two off-scale states. These are data colours: they belong to bar fills, tier badges and the aggregate figure. They never touch the stars.

Tier
Hex
On white
As text
Excellent ≥ 4.5
#1f7a4d
5.32:1
AA pass
Very Good ≥ 3.5
#4f9d69
3.30:1
Graphics only
Good ≥ 2.5
#b08307
3.45:1
Graphics only
Fair ≥ 1.5
#c2660f
4.04:1
Graphics only
Poor > 0
#b3261e
6.54:1
AA pass
Neutral no rating
#94a3b8
2.56:1
Never text
Star gold all tiers
#f59e0b
2.15:1
Glyph shape carries state
Audit note

theme.css states the rating hues are "already AA-safe as text on white". Measured, three of the five are not: very-good (3.30), good (3.45) and fair (4.04) clear the 3:1 graphics threshold but miss the 4.5:1 text threshold. This is not currently a live defect, because tintValue is off by default and the ScoreBar figure stays ink. It becomes one the moment anybody turns tintValue on. Either restrict it to excellent/poor, or darken three values.

Foreground on a subtle fill

Every -subtle token is a 10% tint of its own hue over the surface. Putting that same hue back on top at full strength is the most common contrast failure in this system: the tint has already spent part of the lightness delta the pair depends on.

ForegroundGroundMeasured
--color-primary--color-primary-subtle #e5f1f74.26:1Fails
--color-primary-hover--color-primary-subtle6.43:1Passes
--color-text-primary--color-primary-subtle10.98:1Passes
--color-primary--color-surface #ffffff4.89:1Passes

The rule: a subtle fill takes ink, not its own hue. --color-text-primary on chips and badges, --color-text-secondary on banner body text. Where the tinted chip must stay blue, --color-primary-hover is the darker blue that survives the tint.

The hue itself is not the problem — --color-primary on plain white passes. It is the tint underneath that breaks it. The hue also stays correct on icons and borders inside a subtle fill, which are non-text and answer to 3:1, not 4.5:1.

Status hues need a value per theme

The subtle-fill rule above is about the ground. This one is about the hue itself: no single status colour clears 4.5:1 against both #ffffff and #121212. A status token that is declared once, on :root, is therefore broken in one of the two themes — and it will be the theme nobody measured.

TokenLight valueon #ffffffDark valueon #121212
--color-destructivered-700 #b91c1c6.47:1red-500 #ef44444.98:1
--color-warningamber-700 #b453095.02:1amber-500 #f59e0b8.72:1
--color-successemerald-700 #0478575.48:1emerald-500 #10b9817.39:1

Swapped, each fails: #b91c1c measures 2.90:1 on the dark ground, and #ef4444 measures 3.76:1 on white. The pair is not a preference — it is the only way one name can be legible in both themes.

The rule: every status token that carries text or a border gets a .dark value. The dark value is the -500 step, which the ramp already holds as the -fill. What does not change is -fill and -subtle: a fill is non-text at 3:1, and a 10–14% oklab tint of the same hue resolves against whatever surface is under it.

One consequence worth naming: in dark mode --color-destructive and --color-destructive-fill resolve to the same hex. That is correct, not a redundancy — the two names still mean different things, and only the light theme needs them to differ.

The exception: a solid button

Flipping the hue lighter fixes text and breaks any surface that carries white on it. White on #ef4444 is 3.76:1. So a solid destructive button uses --color-destructive-solid, which is red-700 in both themes and holds white at 6.47:1 either way — the one status token with no .dark value, deliberately.

The distinction is which side of the pair is fixed. Destructive text sits on the theme surface, so the hue must move when the surface moves. A destructive button brings its own surface, so nothing needs to move.

Contrast checker

Measures the live computed value of any two semantic tokens, including color-mix() results and the dark-mode re-points. Toggle the theme in the header and the numbers move.

Aa
Sample copy 4.7

Do & don't

Delivered

All 412 debriefs sent.

Do

Tone paints the 3px rule. Body copy stays in the reading colour.

Delivered

All 412 debriefs sent.

Don't

A -fill flooding a surface, carrying white text at 2.57:1.

Sporting Scouter Design System
Every token on this site is generated from theme.css.