Color System
Overview
A curated library of ready-to-use color palettes (light + dark) across four domains — web-app UI, marketing/landing, data visualization, and terminal/TUI — plus the methodology to build new palettes and verify their accessibility.
Core principle: choose colors by semantic role (background, text, primary, error…), not by raw hue. Every palette here maps hexes to roles, so a theme stays swappable, consistent, and accessible. Pick a role first, then read its hex — hardcoding a hex where a role belongs is what breaks theming later.
Navigation
The index and role contract below answer "which palette" on their own. Load one reference file when the task needs its payload — not all four.
| Load |
When |
| references/palettes.md |
You need actual hex values — any of the four domains, light + dark; the fill/subtle/emphasis triads; the 12-step scales behind Dusk, Driftwood and Meadow |
| references/contrast.md |
Verifying WCAG/APCA thresholds, colorblind safety, or debugging a pair that fails |
| references/build-your-own.md |
No library palette fits and you're generating a new scale (the OKLCH recipe) |
| references/theory.md |
Choosing a harmony scheme, or justifying a color-space / scale decision |
Palette Library (quick index)
Where one palette is the right first reach it is marked ⭐; marketing and TUI have no default because the choice follows the brand or the user's own terminal theme.
Web App UI — light + dark, 13 semantic roles + a triad per color
- Carbon ⭐ (default) — deep slate-blue, dark-first. Analytics dashboards, perf reports & dev tooling. Ships a full dashboard kit (A–F grade pills, ordered stage sequence, inline-code tone, success highlight). Reach for Graphite instead when light mode must be hand-tuned-equal.
- Graphite — cool slate + blue. Dense B2B dashboards, dev tools; the light+dark parity default.
- Evergreen — emerald brand on true-neutral zinc. Fresh, confident, non-blue identity.
- Terracotta — warm clay/espresso neutrals + rust. Editorial, content platforms, writing tools.
- Bloom — pastel periwinkle/violet, deep dark mode. Friendly dev tools, playful-but-clean apps.
- Teal Slate — deep teal on cool slate, single warm accent. Technical reports, SRE / rate-limit dashboards; pairs with a mono face for figures.
Generated from 12-step OKLCH scales, which ship alongside their roles so they can be extended or re-derived:
- Dusk — indigo primary, turquoise accent, near-neutral cool greys. Focus tools, editors, long-session apps.
- Driftwood — deep marine primary on warm sand neutrals, clay accent. The one that swings its neutrals cool in dark mode.
- Meadow — muted sage primary, old-gold accent, warm bone neutrals. The softest of the set; restful rather than confident.
Marketing / Landing — light + dark, hero gradients
- Sunbloom — refined coral/amber warmth. Approachable consumer/creator brands.
- Tidewater — teal/sage. Fintech, wellness, B2B trust.
- Obsidian & Gold — near-black + metallic gold. Premium, luxury, agency.
- Paper & Ink — warm paper + ink black + one terracotta accent. Blogs, long-form, portfolios.
Data Viz — colorblind-aware, warm/cool earthy
- Categorical: Lunar Valley ⭐ (default) · Hearthstead · Vintage Warm · Glass Wall · Okabe-Ito (use when CVD-safety is a hard requirement)
- Sequential: Viridis ⭐ (default) · Magma · Inferno · Plasma · ColorBrewer Blues · YlOrRd (all perceptually uniform)
- Diverging: Console & Window ⭐ (default) · Alien Sun · Orchard Dusk · Coffee & Coolant — all warm↔cool, no red↔green
Terminal / TUI — 16-ANSI + bg/fg/cursor/selection
- Solarized Dark · Nord · Catppuccin Mocha · Catppuccin Latte (the one light scheme) · Dracula · Tokyo Night
→ Full hex tables for every palette: references/palettes.md
Semantic roles (the contract)
UI palettes fill these roles. Map intent to a role, then the role to a hex.
| Role |
What it is |
background |
App/page base canvas |
surface |
Cards, panels, sheets |
surface-elevated |
Raised surfaces (popovers, modals); in dark mode lighter = more elevated |
border |
Dividers, input borders, focus rings |
text-primary |
High-emphasis foreground |
text-secondary |
Muted labels, hints, captions |
primary / primary-hover |
Brand action color + its hover state |
accent |
Secondary emphasis, distinct from primary |
success / warning / error / info |
Semantic states (green / amber / red / blue by Western convention) |
Each colored role then needs a triad, because one hex cannot do three jobs. A value authored to be sat on is not automatically legible as text, and a solid fill is far too loud as a badge background at table density.
| Token |
Job |
Bootstrap 5.3 calls it |
Material 3 calls it |
{c}-fill + {c}-on-fill |
Buttons, active chips |
--bs-{c} + color-contrast() |
primary + on-primary |
{c}-subtle + {c}-emphasis |
Badge / callout background + its label |
--bs-{c}-bg-subtle + -text-emphasis |
primary-container + on-primary-container |
{c}-subtle-border |
Border around that background |
--bs-{c}-border-subtle |
— |
link |
The brand hue as text on surface |
(split in v6) |
— |
Ready-made triads for all nine UI palettes are in palettes.md. Dark-mode fills are light hues, so their labels are the palette's darkest neutral, not white.
Data-viz palettes instead provide ordered color lists (categorical = distinct series; sequential = low→high ramp; diverging = warm↔cool with a neutral midpoint). TUI schemes provide the 16 ANSI slots plus 4 special roles.
Core Concepts
- Design in OKLCH, not HSL. OKLCH is perceptually uniform; HSL "lightness" lies (equal-L blue looks far darker than equal-L yellow), so even HSL steps produce lumpy scales.
- Harmony: pick a scheme (monochromatic / analogous / complementary / triadic…) and let one color dominate in saturation and area; desaturate the rest.
- Scales: 10–12 steps, step lightness evenly, peak chroma in the mid-range and taper it at the extremes so tints aren't washed out and shades aren't muddy.
- Contrast: WCAG AA — body text ≥ 4.5:1, large text & UI/borders ≥ 3:1.
- Light vs dark is not an inversion, and WCAG 2 ratios overstate contrast near black. Both failure modes and their fixes are in Gotchas below; thresholds and the APCA procedure are in references/contrast.md.
- Colorblind-safety: encode meaning with text, icon, or position in addition to hue, since ~8% of men can't separate the hues alone. For diverging data use warm↔cool (blue/orange, teal/rose) rather than red↔green.
Examples
Example: choosing colors for a SaaS dashboard
✅ Desired
User: "I need colors for an admin dashboard, light and dark."
→ Recommend Carbon (deep slate-blue, dark-first, made for dense data UI) — the default;
offer Graphite if hand-tuned light-mode parity matters more than the dashboard kit.
→ Copy its role→hex table from palettes.md (both modes).
→ Apply by role: background→surface→border→text, primary for the main CTA,
success/warning/error for status badges. Confirm text-primary on background ≥ 4.5:1.
❌ Anti-pattern
→ Pick four hues from a Coolors palette and assign them ad hoc: #2b2d42 nav, #8d99ae text,
#edf2f4 background, #ef233c buttons. Dark mode = invert the hexes.
Why it works: a role-mapped palette drops straight into CSS variables and is accessible by construction. The ad-hoc version has no neutral ramp, no elevation tiers, no status colors, and inverting a light palette produces pure-black surfaces and washed-out brand hues (see Gotchas: light vs dark is not an inversion).
Example: status badge color
✅ Desired
.badge-error { background: var(--error); color: #fff; } /* role, not a hardcoded red */
❌ Anti-pattern
.badge-error { background: #ff0000; color: #000; } /* raw hue; #f00 on #000 ≈ 5.2:1 but glares,
and won't theme or switch to dark mode */
Why it fails: hardcoding a hue breaks theming and dark mode, and pure-red/pure-black causes halation.
Example: picking a chart palette
✅ Desired
Categorical series (≤8) → Lunar Valley (default), or Okabe-Ito when CVD-safety is required.
Continuous low→high → Viridis (perceptually uniform, colorblind-safe).
Signed data with a midpoint → Console & Window (warm↔cool, neutral center).
Past ~8 categorical colors, series stop being distinguishable — aggregate into "Other" instead.
❌ Anti-pattern
Twelve categorical series colored from the UI palette's success/warning/error/info roles plus
brand tints; profit/loss encoded green↔red with no labels.
Why it works: matches the data's structure to the right palette family and respects the distinguishability limit. The anti-pattern reuses UI status roles that collide with series identity, exceeds the ~8-hue limit, and encodes red↔green with no text or icon backup for colorblind viewers.
Gotchas
- Symptom: Brand color is unreadable as body text. Cause: Saturated mid-tones (amber, coral, teal) often fail 4.5:1 on their own background — a fill color and a text color are different steps of the same ramp. Fix: keep
fill and link as separate values; step the text one darker until it clears. In this library only Evergreen light diverges (#059669 carries a button label at 4.70:1 but reaches 3.77:1 as a link on white, so its link is #00875b), which is exactly why the split gets missed.
- Symptom: Status badges look correct but shout at table density. Cause: a solid
--error fill behind white text is right for a button and far too loud for a row-level badge. Fix: use the {c}-subtle background with its paired {c}-emphasis label. Reusing the status hue as its own label on that tint misses 4.5:1 more often than not — take the paired value from palettes.md rather than assuming.
- Symptom: A palette generated from a 12-step scale still fails contrast. Cause: the step map is not a contrast guarantee. Radix guarantees steps 11 and 12 against step 2 and nothing else; step 9 rarely carries a 4.5:1 button label, and step 8 rarely reaches 3:1 as a control edge. Fix: solve those two against their actual ground. In light mode commit to a white label and step the fill darker — solving toward whichever label already scores higher produces a pale wash that passes and looks weak.
- Symptom: A palette that looks right in light mode feels muddy in dark. Cause: the neutral tint was chosen once and inherited. A warm hue that reads as paper at high lightness reads as brown at low lightness. Fix: re-decide neutral hue and chroma per mode; swinging cool at very low chroma is the usual answer. Driftwood in palettes.md does this (hue 64 light, 224 dark); Meadow's sage did not need it.
- Symptom: A palette copied from Color Hunt, Coolors or a trend article cannot build an interface. Cause: gallery palettes are four decorative hues with no neutral ramp, no dark ink and no states — 16 of the 30 most-liked Color Hunt palettes cannot carry 4.5:1 body text with any pair of their colors. Fix: take the hues as seed material and build scales from them (build-your-own.md); Dusk, Driftwood and Meadow were made this way.
- Symptom: Secondary/muted text ("dimmed",
text-secondary) looks fine in the design tool but fails contrast in the app. Cause: component libraries ship a default muted-text color (e.g. Mantine's dimmed) tuned for visual hierarchy, not contrast — commonly landing around ~3.4:1, well under the 4.5:1 AA floor. Fix: pick and verify your own text-secondary hex against contrast.md instead of inheriting the library default, and re-check per theme — the same override can pass in one theme and fail in another. Re-verify on tinted/elevated surfaces (cards, striped rows) too, not just the flat canvas.
- Symptom: Dark mode "passes WCAG" but is hard to read. Cause: WCAG 2 math overstates contrast near black. Fix: re-check dark pairs with APCA (Lc), not the 4.5:1 ratio alone.
- Symptom: Dark theme looks flat, elevation unreadable. Cause: pure
#000 background + same-lightness surfaces. Fix: raise the base to ~`#0d1117–#1e1e2e` and make each elevation tier lighter.
- Symptom: A chart is unreadable for colorblind viewers. Cause: red↔green encoding or hue-only meaning. Fix: switch to a warm↔cool diverging palette and add labels/icons; for categorical use the Okabe-Ito safe set (in palettes.md).
- Symptom: Chart text stays low-contrast even though your palette defines the right text color. Cause: charting libraries (Recharts, Mantine charts) render axis/legend/value text as SVG with their own inline
fill, bypassing your color tokens entirely. Fix: target the library's text elements directly (e.g. .recharts-wrapper text { fill: var(--text) }) — the CSS cascade never reaches it.
- Symptom: TUI colors vanish on some terminals. Cause: hardcoded hex, or load-bearing text placed in bright-black (slot 8). Fix: bind meaning to ANSI slots 1–6 so the user's own theme renders it, and keep slot 8 for de-emphasis only.
- Symptom: Palette steps look lumpy/uneven. Cause: stepping lightness in HSL/RGB. Fix: rebuild the scale in OKLCH; see build-your-own.md.
Integration
- frontend-design — builds the components and layout; this skill supplies the color decisions. Pick a palette here, implement the UI there.
- typography — owns size, weight, and line-height; the two share only the contrast floor. When text is unreadable, decide first whether the defect is the color pair (here) or the size/weight (typography).
- dataviz — owns chart form, axes, and encoding. Take the categorical/sequential/diverging palette from here and hand it to that skill as the series colors.
1---2name: color-system3description: Use this skill for any color decision — picking or building a palette, dark mode, contrast, chart and terminal colors. Triggers include "what colors should I use", "pick a palette for my dashboard", "set up dark mode", "does this pass WCAG contrast", "colorblind-safe chart colors", or "give me a terminal theme" — and it applies even when the user never says "color", as in "theme this app", "these status badges look wrong", or "this text is hard to read on the background". Ships curated light+dark palettes for web UI, marketing, data viz, and TUI, plus an OKLCH build-your-own recipe and WCAG/APCA thresholds. Do NOT use this skill for font size, weight, or pairing (see typography), for layout and component structure (see frontend-design), or for chart type, axis, and encoding choices that are not about color (see dataviz).4license: MIT5---67# Color System89## Overview1011A curated library of ready-to-use color palettes (light + dark) across four domains — **web-app UI**, **marketing/landing**, **data visualization**, and **terminal/TUI** — plus the methodology to build new palettes and verify their accessibility.1213**Core principle:** choose colors by **semantic role** (background, text, primary, error…), not by raw hue. Every palette here maps hexes to roles, so a theme stays swappable, consistent, and accessible. Pick a role first, then read its hex — hardcoding a hex where a role belongs is what breaks theming later.1415## Navigation1617The index and role contract below answer "which palette" on their own. Load one reference file when the task needs its payload — not all four.1819| Load | When |20|---|---|21| [references/palettes.md](references/palettes.md) | You need actual hex values — any of the four domains, light + dark; the fill/subtle/emphasis triads; the 12-step scales behind Dusk, Driftwood and Meadow |22| [references/contrast.md](references/contrast.md) | Verifying WCAG/APCA thresholds, colorblind safety, or debugging a pair that fails |23| [references/build-your-own.md](references/build-your-own.md) | No library palette fits and you're generating a new scale (the OKLCH recipe) |24| [references/theory.md](references/theory.md) | Choosing a harmony scheme, or justifying a color-space / scale decision |2526## Palette Library (quick index)2728Where one palette is the right first reach it is marked ⭐; marketing and TUI have no default because the choice follows the brand or the user's own terminal theme.2930### Web App UI — light + dark, 13 semantic roles + a triad per color31- **Carbon** ⭐ *(default)* — deep slate-blue, dark-first. Analytics dashboards, perf reports & dev tooling. Ships a full dashboard kit (A–F grade pills, ordered stage sequence, inline-code tone, success highlight). Reach for **Graphite** instead when light mode must be hand-tuned-equal.32- **Graphite** — cool slate + blue. Dense B2B dashboards, dev tools; the light+dark parity default.33- **Evergreen** — emerald brand on true-neutral zinc. Fresh, confident, non-blue identity.34- **Terracotta** — warm clay/espresso neutrals + rust. Editorial, content platforms, writing tools.35- **Bloom** — pastel periwinkle/violet, deep dark mode. Friendly dev tools, playful-but-clean apps.36- **Teal Slate** — deep teal on cool slate, single warm accent. Technical reports, SRE / rate-limit dashboards; pairs with a mono face for figures.3738Generated from 12-step OKLCH scales, which ship alongside their roles so they can be extended or re-derived:39- **Dusk** — indigo primary, turquoise accent, near-neutral cool greys. Focus tools, editors, long-session apps.40- **Driftwood** — deep marine primary on warm sand neutrals, clay accent. The one that swings its neutrals cool in dark mode.41- **Meadow** — muted sage primary, old-gold accent, warm bone neutrals. The softest of the set; restful rather than confident.4243### Marketing / Landing — light + dark, hero gradients44- **Sunbloom** — refined coral/amber warmth. Approachable consumer/creator brands.45- **Tidewater** — teal/sage. Fintech, wellness, B2B trust.46- **Obsidian & Gold** — near-black + metallic gold. Premium, luxury, agency.47- **Paper & Ink** — warm paper + ink black + one terracotta accent. Blogs, long-form, portfolios.4849### Data Viz — colorblind-aware, warm/cool earthy50- **Categorical:** Lunar Valley ⭐ *(default)* · Hearthstead · Vintage Warm · Glass Wall · Okabe-Ito *(use when CVD-safety is a hard requirement)*51- **Sequential:** Viridis ⭐ *(default)* · Magma · Inferno · Plasma · ColorBrewer Blues · YlOrRd *(all perceptually uniform)*52- **Diverging:** Console & Window ⭐ *(default)* · Alien Sun · Orchard Dusk · Coffee & Coolant — all warm↔cool, no red↔green5354### Terminal / TUI — 16-ANSI + bg/fg/cursor/selection55- Solarized Dark · Nord · Catppuccin Mocha · Catppuccin Latte *(the one light scheme)* · Dracula · Tokyo Night5657→ **Full hex tables for every palette:** [references/palettes.md](references/palettes.md)5859## Semantic roles (the contract)6061UI palettes fill these roles. Map intent to a role, then the role to a hex.6263| Role | What it is |64|---|---|65| `background` | App/page base canvas |66| `surface` | Cards, panels, sheets |67| `surface-elevated` | Raised surfaces (popovers, modals); in dark mode **lighter = more elevated** |68| `border` | Dividers, input borders, focus rings |69| `text-primary` | High-emphasis foreground |70| `text-secondary` | Muted labels, hints, captions |71| `primary` / `primary-hover` | Brand action color + its hover state |72| `accent` | Secondary emphasis, distinct from primary |73| `success` / `warning` / `error` / `info` | Semantic states (green / amber / red / blue by Western convention) |7475**Each colored role then needs a triad, because one hex cannot do three jobs.** A value authored to be *sat on* is not automatically legible *as text*, and a solid fill is far too loud as a badge background at table density.7677| Token | Job | Bootstrap 5.3 calls it | Material 3 calls it |78|---|---|---|---|79| `{c}-fill` + `{c}-on-fill` | Buttons, active chips | `--bs-{c}` + `color-contrast()` | `primary` + `on-primary` |80| `{c}-subtle` + `{c}-emphasis` | Badge / callout background + its label | `--bs-{c}-bg-subtle` + `-text-emphasis` | `primary-container` + `on-primary-container` |81| `{c}-subtle-border` | Border around that background | `--bs-{c}-border-subtle` | — |82| `link` | The brand hue as text on `surface` | (split in v6) | — |8384Ready-made triads for all nine UI palettes are in palettes.md. Dark-mode fills are light hues, so their labels are the palette's *darkest* neutral, not white.8586Data-viz palettes instead provide ordered color **lists** (categorical = distinct series; sequential = low→high ramp; diverging = warm↔cool with a neutral midpoint). TUI schemes provide the 16 ANSI slots plus 4 special roles.8788## Core Concepts8990- **Design in OKLCH, not HSL.** OKLCH is perceptually uniform; HSL "lightness" lies (equal-L blue looks far darker than equal-L yellow), so even HSL steps produce lumpy scales.91- **Harmony:** pick a scheme (monochromatic / analogous / complementary / triadic…) and let **one** color dominate in saturation and area; desaturate the rest.92- **Scales:** 10–12 steps, step lightness evenly, peak chroma in the mid-range and taper it at the extremes so tints aren't washed out and shades aren't muddy.93- **Contrast:** WCAG AA — body text ≥ 4.5:1, large text & UI/borders ≥ 3:1.94- **Light vs dark is not an inversion, and WCAG 2 ratios overstate contrast near black.** Both failure modes and their fixes are in Gotchas below; thresholds and the APCA procedure are in [references/contrast.md](references/contrast.md).95- **Colorblind-safety:** encode meaning with text, icon, or position *in addition to* hue, since ~8% of men can't separate the hues alone. For diverging data use warm↔cool (blue/orange, teal/rose) rather than red↔green.9697## Examples9899### Example: choosing colors for a SaaS dashboard100101✅ Desired102103```104User: "I need colors for an admin dashboard, light and dark."105→ Recommend Carbon (deep slate-blue, dark-first, made for dense data UI) — the default;106 offer Graphite if hand-tuned light-mode parity matters more than the dashboard kit.107→ Copy its role→hex table from palettes.md (both modes).108→ Apply by role: background→surface→border→text, primary for the main CTA,109 success/warning/error for status badges. Confirm text-primary on background ≥ 4.5:1.110```111112❌ Anti-pattern113114```115→ Pick four hues from a Coolors palette and assign them ad hoc: #2b2d42 nav, #8d99ae text,116 #edf2f4 background, #ef233c buttons. Dark mode = invert the hexes.117```118119Why it works: a role-mapped palette drops straight into CSS variables and is accessible by construction. The ad-hoc version has no neutral ramp, no elevation tiers, no status colors, and inverting a light palette produces pure-black surfaces and washed-out brand hues (see Gotchas: light vs dark is not an inversion).120121### Example: status badge color122123✅ Desired124125```css126.badge-error { background: var(--error); color: #fff; } /* role, not a hardcoded red */127```128129❌ Anti-pattern130131```css132.badge-error { background: #ff0000; color: #000; } /* raw hue; #f00 on #000 ≈ 5.2:1 but glares,133 and won't theme or switch to dark mode */134```135136Why it fails: hardcoding a hue breaks theming and dark mode, and pure-red/pure-black causes halation.137138### Example: picking a chart palette139140✅ Desired141142```143Categorical series (≤8) → Lunar Valley (default), or Okabe-Ito when CVD-safety is required.144Continuous low→high → Viridis (perceptually uniform, colorblind-safe).145Signed data with a midpoint → Console & Window (warm↔cool, neutral center).146Past ~8 categorical colors, series stop being distinguishable — aggregate into "Other" instead.147```148149❌ Anti-pattern150151```152Twelve categorical series colored from the UI palette's success/warning/error/info roles plus153brand tints; profit/loss encoded green↔red with no labels.154```155156Why it works: matches the data's structure to the right palette family and respects the distinguishability limit. The anti-pattern reuses UI status roles that collide with series identity, exceeds the ~8-hue limit, and encodes red↔green with no text or icon backup for colorblind viewers.157158## Gotchas159160- **Symptom:** Brand color is unreadable as body text. **Cause:** Saturated mid-tones (amber, coral, teal) often fail 4.5:1 on their own background — a fill color and a text color are different steps of the same ramp. **Fix:** keep `fill` and `link` as separate values; step the text one darker until it clears. In this library only Evergreen light diverges (`#059669` carries a button label at 4.70:1 but reaches 3.77:1 as a link on white, so its `link` is `#00875b`), which is exactly why the split gets missed.161- **Symptom:** Status badges look correct but shout at table density. **Cause:** a solid `--error` fill behind white text is right for a button and far too loud for a row-level badge. **Fix:** use the `{c}-subtle` background with its paired `{c}-emphasis` label. Reusing the status hue as its own label on that tint misses 4.5:1 more often than not — take the paired value from palettes.md rather than assuming.162- **Symptom:** A palette generated from a 12-step scale still fails contrast. **Cause:** the step map is not a contrast guarantee. Radix guarantees steps 11 and 12 against step 2 and nothing else; step 9 rarely carries a 4.5:1 button label, and step 8 rarely reaches 3:1 as a control edge. **Fix:** solve those two against their actual ground. In light mode commit to a white label and step the fill *darker* — solving toward whichever label already scores higher produces a pale wash that passes and looks weak.163- **Symptom:** A palette that looks right in light mode feels muddy in dark. **Cause:** the neutral tint was chosen once and inherited. A warm hue that reads as paper at high lightness reads as brown at low lightness. **Fix:** re-decide neutral hue and chroma per mode; swinging cool at very low chroma is the usual answer. Driftwood in palettes.md does this (hue 64 light, 224 dark); Meadow's sage did not need it.164- **Symptom:** A palette copied from Color Hunt, Coolors or a trend article cannot build an interface. **Cause:** gallery palettes are four decorative hues with no neutral ramp, no dark ink and no states — 16 of the 30 most-liked Color Hunt palettes cannot carry 4.5:1 body text with *any* pair of their colors. **Fix:** take the hues as seed material and build scales from them (build-your-own.md); Dusk, Driftwood and Meadow were made this way.165- **Symptom:** Secondary/muted text ("dimmed", `text-secondary`) looks fine in the design tool but fails contrast in the app. **Cause:** component libraries ship a default muted-text color (e.g. Mantine's `dimmed`) tuned for visual hierarchy, not contrast — commonly landing around ~3.4:1, well under the 4.5:1 AA floor. **Fix:** pick and verify your own `text-secondary` hex against contrast.md instead of inheriting the library default, and re-check per theme — the same override can pass in one theme and fail in another. Re-verify on **tinted/elevated surfaces** (cards, striped rows) too, not just the flat canvas.166- **Symptom:** Dark mode "passes WCAG" but is hard to read. **Cause:** WCAG 2 math overstates contrast near black. **Fix:** re-check dark pairs with APCA (Lc), not the 4.5:1 ratio alone.167- **Symptom:** Dark theme looks flat, elevation unreadable. **Cause:** pure `#000` background + same-lightness surfaces. **Fix:** raise the base to ~`#0d1117`–`#1e1e2e` and make each elevation tier *lighter*.168- **Symptom:** A chart is unreadable for colorblind viewers. **Cause:** red↔green encoding or hue-only meaning. **Fix:** switch to a warm↔cool diverging palette and add labels/icons; for categorical use the Okabe-Ito safe set (in palettes.md).169- **Symptom:** Chart text stays low-contrast even though your palette defines the right text color. **Cause:** charting libraries (Recharts, Mantine charts) render axis/legend/value text as SVG with their own inline `fill`, bypassing your color tokens entirely. **Fix:** target the library's text elements directly (e.g. `.recharts-wrapper text { fill: var(--text) }`) — the CSS cascade never reaches it.170- **Symptom:** TUI colors vanish on some terminals. **Cause:** hardcoded hex, or load-bearing text placed in bright-black (slot 8). **Fix:** bind meaning to ANSI slots 1–6 so the user's own theme renders it, and keep slot 8 for de-emphasis only.171- **Symptom:** Palette steps look lumpy/uneven. **Cause:** stepping lightness in HSL/RGB. **Fix:** rebuild the scale in OKLCH; see build-your-own.md.172173## Integration174175- **frontend-design** — builds the components and layout; this skill supplies the color decisions. Pick a palette here, implement the UI there.176- **typography** — owns size, weight, and line-height; the two share only the contrast floor. When text is unreadable, decide first whether the defect is the color pair (here) or the size/weight (typography).177- **dataviz** — owns chart form, axes, and encoding. Take the categorical/sequential/diverging *palette* from here and hand it to that skill as the series colors.