Color & Contrast Audit — COLOR.md

A real report, written by 56 · Color & Contrast Audit, run against this repo — dogfood output, committed unedited. This is the artifact every brief ends in: findings that cite their evidence, ranked by severity, with a fix sketch each.

Read-only extraction of the palette this product ships — every hex, rgba, and color-mix() in template.html, build.pySITE_CSS, studio.html, vitals.html — audited for coherence, contrast, meaning, and dark-mode integrity. Every color claim carries its value + file; every contrast claim carries a computed WCAG ratio (sRGB, scratchpad/contrast.py).

Headline: the surface ramp and primary text are clean and pass comfortably. Three real problems: (1) the --faint text tier fails AA everywhere (2.55–3.06:1); (2) meaning is overloaded — one green is both "success" and a brand family, amber carries four unrelated jobs, and "danger" is spread across three different reds; (3) the categorical system has 17 hues, well past the point of reliable distinction. This is a dark-only site (no prefers-color-scheme anywhere — grep = 0 hits).

Backlog reconciliation (2026-07-09). This audit predates the palette redesign, so its specific hexes, the "dark-only / no prefers-color-scheme" claim, and the "17 families" count are stale — the site now ships a dark and a light theme across 21 families, tokenized once in tokens.css. Dispositions: C1 (--faint fails AA) — FIXED: --faint now clears 4.5:1 on ink, panel, and panel-2 in both themes (dark #8B8D95, light #6A6C73). C9 (dead --panel-3) — FIXED: removed. C2–C5 (green/amber/red meaning collides with family/brand hues) — FIXED: the semantic tokens were recolored distinct from every family hue — --success #2FAF73 (was Trust #52C280), --warning #F59E2C (was Product #E8B44C), --danger #E23B4B (was Act #E84C3D), plus --down decoupled from Quality, with light-theme equivalents — verified in Chromium in both themes. "Green = success" and "green = Trust family" are no longer one pixel. C6 (two severity ramps), C7 (now 21 categorical hues, was 17), C8 (color as sole family signal) — DEFERRED (design calls). See FIXLOG.md.


1 · The real palette (clustered, with counts + roles)

36 distinct hex values + 6 rgba() + 19 color-mix() calls. Clustered:

Surfaces — dark ramp (tokenized)

TokenHexUsesRole
--ink-2#0F13197deepest — code bg, nav/report bars
--ink#14181E8page background
--panel#1B212B4→manycards, chips, inputs
--panel-2#2028344elevated panels, .out badge
--panel-3#27313F0dead token — defined template.html:30/build.py:321, consumed nowhere

Text tiers

Token/hexUsesRoleNote
#FFF4brightest (hover text)untokenized
--text #E2E6EC4primary body/headings
#C7CEDA7code / .cmd textuntokenized light gray
#C6CEDA7prose / lede / rulesuntokenized light gray
#C4CBD87prose and Meta familyuntokenized; doubles as a family hue
--dim #8B96A56secondary text
--faint #5C66754meta, captions, timestamps, placeholdersfails AA — §2

The three light grays #C7CEDA / #C6CEDA / #C4CBD8 are visually one color (ΔL ≈ 1–3) doing one job — the classic "#333 next to the #343434," and none is a token.

Borders

--line #2A3340, --line-2 #37424F, plus a one-off #3A4452 (studio.html:134, .c-low border — outside every token set).

Brand / accent

--amber #E8B44C (×20, the most-used color), --amber-2 #F2CE7C (hover), --fc #E8B44C (family-color default), --good #52C280, --act #E84C3D (Studio only). Dark inks on accents: #12161C (on amber/fc), #0C1510 (on green), #1A1205 (on nav CTA) — three near-blacks for one job.

Categorical / data-viz — the 17 family colors (build.py:30)

Venture #C878F0 · Product #E8B44C · Quality #E8705F · Speed #4D9FFF · Trust #52C280 · Growth #A98CF5 · Team #3FC1C9 · Clarity #9AD4E8 · Design #5CE8A0 · Data #F0904A · Ops #B4C64A · Subtract #E87FB0 · Meta #C4CBD8 · Act #E84C3D · Agent #8B7CF8 · Automation #E8DE5A · AI-UX #F06FD8

Severity (two conflicting ramps)

  • Landing .art (template.html:105): S1 #E8705F · S2 #E8B44C · S3 #8B96A5
  • Studio .c-* (studio.html:131): S1 #FF6B5C · S2 #F0904A · S3 #E8B44C · low #8B96A5 · fixed #52C280

Same labels, different colors: "S1" is coral on the landing, red in Studio; "S2" is amber on the landing, orange in Studio.


2 · Contrast table (computed sRGB ratios)

Body text needs 4.5:1; large text (≥24px, or ≥18.7px bold) and UI need 3:1.

PairRatio4.53.0Where it appears
--text #E2E6EC on --panel12.90all body/headings
--dim #8B96A5 on --panel5.39.lead, taglines
--dim #8B96A5 on --panel-24.95.out badge
prose #C6CEDA on --ink11.23.prose, .lede
--amber #E8B44C on --ink9.38links, kickers
dark #12161C on --amber btn9.56primary button text
--faint #5C6675 on --ink3.06.micro,.buildnote,.crumb
--faint #5C6675 on --panel2.78.card-meta, .chip .n, hints
--faint #5C6675 on --panel-22.55.out sub-text, badges
--fc Act #E84C3D on --panel4.26.detail,.kicker,.pid (11.5–12px) on Act family/pages
--fc Agent #8B7CF8 on --panel4.88⚠️same, marginal
--fc Quality #E8705F on --panel5.32marginal-ish
--fc Trust/Design/Speed… on --panel5.7–10.4most families pass
--line #2A3340 vs --panel (border)1.27card/panel outlines (non-text)
focus ring --amber on --ink9.38:focus-visible — good

Failures, by user harm:

  • --faint fails at every surface — and it's used for real content

(meta rows, timestamps, input placeholders, captions). Worst tier, most widespread, smallest type. #1 harm.

  • Act-red #E84C3D as small colored text = 4.26 (and Agent 4.88) — the

.detail/.kicker/.pid labels fail AA on the Act family and on any detail/playbook page whose accent is Act.

  • Card/panel borders at 1.27:1 are below 3:1, but cards also separate by

surface fill, so this is low harm (decorative boundary).


3 · Findings (ranked — meaning first, then contrast, then sprawl)

C1 — --faint is a readability failure across the whole site (fix first)

--faint:#5C6675 (template.html:31 et al.) computes 3.06 / 2.78 / 2.55 on ink / panel / panel-2 — below AA (4.5) everywhere, below even 3:1 on panels. It's applied to genuine content at 11–12.5px: .card-meta, .micro, .buildnote, .fine, .chip .n, .ctxbody .hint, input placeholders. Computed fixes (same calculator): #848E9C clears ink 5.37 / panel 4.87 / panel-2 4.48 (≈AA); #8A94A2 clears panel-2 strictly. But that value nearly equals --dim — which is the real tell: --faint should not carry reading text. Promote real content to --dim and keep a (lightened) faint only for decorative glyphs where 3:1 suffices.

C2 — Green means three things; one hex means two (semantic collision)

--good:#52C280 (success) is the identical hex to the Trust family color #52C280 (build.py:32), and Design is a third green #5CE8A0. So a green check, a "Trust" brief border, and a "Design" accent are the same or near-same color with unrelated meanings. If Trust or Design content ever sits near a success state, "done/good" and "this family" are indistinguishable.

C3 — Amber #E8B44C carries four unrelated jobs

One hex is simultaneously: brand primary (--amber), the default family color (--fc), the Product family (build.py:31), and severity-3 in Studio (.c-s3, studio.html:133). "Brand," "Product," and "medium severity" cannot be told apart. Severity especially should never borrow the brand hue.

C4 — "Danger" is three different reds, and one of them is also "action"

Act/--act #E84C3D, Quality #E8705F, and severity-1 #FF6B5C are three reds with no single authority. Worse, #E84C3D is both the Studio primary action button (.fixbtn, studio.html:162) and the Act family/danger hue — a "do this" and a "danger" reading on one color. And the top-severity color changes between surfaces (coral #E8705F on the landing vs #FF6B5C in Studio — C6).

C5 — Orange #F0904A = Data family and severity-2

Data #F0904A (build.py:34) is the same hex as Studio's .c-s2 (studio.html:132). The severity ramp (#FF6B5C → #F0904A → #E8B44C) is built entirely from family/brand hues, so severity and family are unreadable together.

C6 — Two severity ramps for the same concept

Landing .art S1/S2/S3 = #E8705F/#E8B44C/#8B96A5 (template.html:105) vs Studio .c-s1/2/3 = #FF6B5C/#F0904A/#E8B44C (studio.html:131). Pick one severity ramp and share it; today the marketing illustration and the tool disagree on what "S1" looks like.

C7 — 17 categorical hues exceed reliable distinction

The family palette (build.py:30) — the core "color = family" system — packs three purples (Growth #A98CF5, Agent #8B7CF8, Venture #C878F0), three warm-reds (Quality #E8705F, Act #E84C3D, Data #F0904A), two greens (Trust/Design), and two blues (Speed #4D9FFF, Clarity #9AD4E8). Adjacent in the hero .spectrum (template.html:348) these are hard to separate, and for red-green/blue-yellow colorblindness the warm-red and purple trios collapse. Because family is often signaled by color alone on a card (left-border #fc + number; the family name is only in the section header), a colorblind user can't map many cards to a family (C8).

C8 — Color as sole signal (localized)

Mostly OK — severity chips carry text labels ("S1"), focus is an outline not a hue. But an individual catalog card encodes its family only by color (.card{border-left:3px solid var(--fc)} template.html:240, .num color); the family name isn't on the card. Add a text/(icon) family tag per card, or rely on the section header being always visible.

C9 — Sprawl: dead + one-off + untokenized values

--panel-3 #27313F is defined twice and used zero times. #3A4452 (studio.html:134) is a one-off border gray outside the token set. Three light body grays (#C7CEDA/#C6CEDA/#C4CBD8) and three dark inks (#12161C/#0C1510/#1A1205) are untokenized near-duplicates. #FFF (×4) is an untokenized brightest-text.

What's already good

Surface ramp is a real, ordered dark system; --text, --dim, prose grays, amber, and all dark-on-accent button texts pass comfortably (7–14:1); interactive states are mostly derived systematically (filter:brightness on hover, opacity on disabled); focus rings are high-contrast outlines.


4 · The token proposal (smallest palette covering today's uses)

Surfaces (4): --ink-2 #0F1319, --ink #14181E, --panel #1B212B, --panel-2 #202834. Delete --panel-3.

Text (4): --text #E2E6EC, --body (one gray, replacing the C7/C6/C4 trio — keep #C6CEDA), --dim #8B96A5, --faint lightened to ≥#848E9C and reserved for decorative/large text only.

Ink-on-accent (1): collapse #12161C/#0C1510/#1A1205 → one --on-accent #111.

Brand (2): --brand #E8B44C + --brand-hi #F2CE7C (hover).

Semantic states (3), each a distinct hue not shared with a family or brand: --success (keep green but stop reusing it as the Trust family hex — give Trust a different green or rename), --warning, --danger — pick one red for danger and stop using it as a primary-action fill.

One severity ramp (shared): e.g. S1 → S2 → S3 → low → fixed, defined once and imported by both the landing illustration and Studio.

Categorical (family): keep the 17 for identity, but (a) never rely on it as the sole signal (C8), and (b) reserve --danger/--success/--warning as semantic tokens distinct from any family hex so meaning never collides with identity (C2–C5).

Net: removes 1 dead token, ~5 untokenized near-duplicates, and — most importantly — decouples meaning from brand identity and lifts --faint above the AA line.


*Report only — nothing edited; contrast math in scratchpad/contrast.py. The highest-harm, lowest-risk fix is C1 (lighten --faint / stop using it for content text — one token, site-wide AA win); the highest-value fixes are C2–C5 (separate semantic state colors from brand/family hexes). Which fixes should I make?*