# Color Guide — Agent Instructions

This file is the single source of truth for how colors work in this design system. It covers the current palette, semantic usage of each stop, and rules for generating new scales.

---

## ⛔ Hard Rules

### 1. DO NOT RECOLOR 2D CHEMICAL STRUCTURE DRAWINGS

**You MUST NOT change, restyle, theme, or "harmonize" the colors in 2D chemical skeleton structure drawings** — i.e., any depiction of a molecule rendered from a SMILES string, MOL/SDF file, or equivalent (RDKit/OpenBabel/Indigo/ChemDraw-style 2D structure renders, atom-bond skeletal diagrams, Lewis structures, etc.).

**Why:** In 2D chemical structures, color is _not decoration_ — it is **semantic data**. Each color encodes the chemical element at that atom position by the standard CPK / Jmol convention (e.g., red = oxygen, blue = nitrogen, yellow = sulfur, green = chlorine, orange = phosphorus, dark grey/black = carbon). Replacing those colors with design-system tokens silently corrupts the chemistry that the figure is communicating. A chemist looking at a recolored structure will misread the molecule.

**What this means in practice:**

- Do **not** map atom colors to `--blue-500`, `--red-600`, `--green-text`, etc.
- Do **not** apply dark-mode overrides, theme filters, `currentColor`, `filter: hue-rotate`, CSS blend modes, or SVG `<style>` overrides to 2D structure SVGs/PNGs/canvases.
- Do **not** "fix" a 2D structure that looks off-palette — it is _correct_, the design system is the one that does not apply here.
- Leave any RDKit/OpenBabel/Indigo/ChemDraw output, atom-colored SMILES render, or skeletal formula **byte-for-byte untouched** in terms of stroke/fill colors.

**What IS allowed:**

- **3D molecular cartoons / ribbon diagrams / surface renders / ball-and-stick 3D scenes** — these can be themed, recolored, or styled to match the design system. CPK coloring on 3D structures is a convention but not load-bearing in the same way.
- The **container, caption, background, border, or label typography** around a 2D structure — recolor the frame, never the structure inside it.
- A 2D structure that is _explicitly monochrome by design_ (e.g., a black-on-white skeletal logo with no per-element coloring) — there are no element colors to preserve.

**If unsure whether something is a 2D chemical structure:** ask the user before changing any colors in it. Treat any SVG/image with atom-letter labels (C, N, O, S, P, Cl, Br, F, etc.) at vertex positions as a 2D chemical structure and leave its colors alone.

---

## Application Emphasis

The system is **greyscale-dominant**. The chromatic scales below are *highlights*, not surface tones — interfaces should read as grey punctuated by small moments of color, never as colorful. If a UI element does not need to draw the eye, default to grey.

Two highlight roles carry most of the chromatic weight in the app:

- **Blue** — primary highlight (links, focus rings, default interactive color)
- **Orange** — secondary highlight (callouts, accents that need contrast alongside blue, chart accents)

Other scales serve narrower roles:

- **Pink** — brand expression only (logo, brand gradient, welcome moments); not used as a general UI accent.
- **Green / Red / Yellow** — status only (success / error / warning). Orthogonal to the highlight hierarchy — they communicate state, not emphasis.
- **Purple** — present in the palette for data-vis use; not part of the UI accent hierarchy.

See `Color Application` → `Accent hierarchy` in [`../philosophy.md`](../philosophy.md) for the full rationale.

---

## Current Palette

All color tokens live in `colors.css` as CSS custom properties on `:root`.

### Blue

| Stop | Variable     | Hex       | Semantic Alias                                  |
| ---- | ------------ | --------- | ----------------------------------------------- |
| 0    | `--blue-0`   | `#FCFFFF` | `--blue-bg`                                     |
| 50   | `--blue-50`  | `#F1FEFF` | `--blue-surface` / `--blue-text-inverse`        |
| 100  | `--blue-100` | `#E0F1F4` | `--blue-surface-stroke`, `--blue-surface-hover` |
| 200  | `--blue-200` | `#BFDFE6` | `--blue-surface-stroke`                         |
| 300  | `--blue-300` | `#9FCEDB` | `--blue-md`                                     |
| 400  | `--blue-400` | `#6BAFC4` | `--blue-md-hover`                               |
| 500  | `--blue-500` | `#047897` | `--blue-md-stroke`                              |
| 600  | `--blue-600` | `#025F78` | `--blue-strong`                                 |
| 700  | `--blue-700` | `#054F6A` | `--blue-strong-pressed`                         |
| 800  | `--blue-800` | `#044055` | `--blue-strong-stroke`                          |
| 900  | `--blue-900` | `#03303F` | `--blue-text`                                   |

### Pink

| Stop | Variable     | Hex       | Semantic Alias                                  |
| ---- | ------------ | --------- | ----------------------------------------------- |
| 0    | `--pink-0`   | `#FFFCFE` | `--pink-bg`                                     |
| 50   | `--pink-50`  | `#FFF9FF` | `--pink-surface` / `--pink-text-inverse`        |
| 100  | `--pink-100` | `#F5E8F3` | `--pink-surface-stroke`, `--pink-surface-hover` |
| 200  | `--pink-200` | `#EAC3E4` | `--pink-surface-stroke`                         |
| 300  | `--pink-300` | `#DC9ED3` | `--pink-md`                                     |
| 400  | `--pink-400` | `#CB62BB` | `--pink-md-hover`                               |
| 500  | `--pink-500` | `#A81696` | `--pink-md-stroke`                              |
| 600  | `--pink-600` | `#8A1079` | `--pink-strong`                                 |
| 700  | `--pink-700` | `#6C0B5E` | `--pink-strong-pressed`                         |
| 800  | `--pink-800` | `#55094B` | `--pink-strong-stroke`                          |
| 900  | `--pink-900` | `#3E0638` | `--pink-text`                                   |

### Orange

| Stop      | Variable                        | Hex                   | Semantic Alias                                      |
| --------- | ------------------------------- | --------------------- | --------------------------------------------------- |
| 0         | `--orange-0`                    | `#FFFCFB`             | `--orange-bg`                                       |
| 50        | `--orange-50`                   | `#FFF9F5`             | `--orange-surface` / `--orange-text-inverse`        |
| 100       | `--orange-100`                  | `#FAEADE`             | `--orange-surface-stroke`, `--orange-surface-hover` |
| 200       | `--orange-200`                  | `#EED4BF`             | `--orange-surface-stroke`                           |
| 300       | `--orange-300`                  | `#DBB397`             | `--orange-md`                                       |
| 400       | `--orange-400`                  | `#CC8F66`             | `--orange-md-hover`                                 |
| 500       | `--orange-500`                  | `#9A5E2A`             | `--orange-md-stroke`                                |
| 600       | `--orange-600`                  | `#74431A`             | `--orange-strong`                                   |
| 700       | `--orange-700`                  | `#563010`             | `--orange-strong-pressed`                           |
| 800       | `--orange-800`                  | `#3D2009`             | `--orange-strong-stroke`                            |
| 900       | `--orange-900`                  | `#281204`             | `--orange-text`                                     |
| 300 / 600 | `--orange-300` / `--orange-600` | `#DBB397` / `#74431A` | `--orange-disabled` (light: 300, dark: 600)         |

### Green

| Stop | Variable      | Hex       | Semantic Alias                                    |
| ---- | ------------- | --------- | ------------------------------------------------- |
| 0    | `--green-0`   | `#F5FEF9` | `--green-bg`                                      |
| 50   | `--green-50`  | `#F2FEF8` | `--green-surface` / `--green-text-inverse`        |
| 100  | `--green-100` | `#E0F4EA` | `--green-surface-stroke`, `--green-surface-hover` |
| 200  | `--green-200` | `#C0E5D2` | `--green-surface-stroke`                          |
| 300  | `--green-300` | `#98D4B6` | `--green-md`                                      |
| 400  | `--green-400` | `#3EA676` | `--green-md-hover`                                |
| 500  | `--green-500` | `#0A7B54` | `--green-md-stroke`                               |
| 600  | `--green-600` | `#066141` | `--green-strong`                                  |
| 700  | `--green-700` | `#044F34` | `--green-strong-pressed`                          |
| 800  | `--green-800` | `#033F29` | `--green-strong-stroke`                           |
| 900  | `--green-900` | `#022F1E` | `--green-text`                                    |

### Red

| Stop | Variable    | Hex       | Semantic Alias                                |
| ---- | ----------- | --------- | --------------------------------------------- |
| 0    | `--red-0`   | `#FFFBFB` | `--red-bg`                                    |
| 50   | `--red-50`  | `#FFF5F5` | `--red-surface` / `--red-text-inverse`        |
| 100  | `--red-100` | `#FBE0E0` | `--red-surface-stroke`, `--red-surface-hover` |
| 200  | `--red-200` | `#F0C4C4` | `--red-surface-stroke`                        |
| 300  | `--red-300` | `#E09C9C` | `--red-md`                                    |
| 400  | `--red-400` | `#CC6464` | `--red-md-hover`                              |
| 500  | `--red-500` | `#A11B1B` | `--red-md-stroke`                             |
| 600  | `--red-600` | `#811212` | `--red-strong`                                |
| 700  | `--red-700` | `#660D0D` | `--red-strong-pressed`                        |
| 800  | `--red-800` | `#500A0A` | `--red-strong-stroke`                         |
| 900  | `--red-900` | `#3B0707` | `--red-text`                                  |

### Yellow

| Stop | Variable       | Hex       | Semantic Alias                                      |
| ---- | -------------- | --------- | --------------------------------------------------- |
| 0    | `--yellow-0`   | `#FFFEFB` | `--yellow-bg`                                       |
| 50   | `--yellow-50`  | `#FFFDF5` | `--yellow-surface` / `--yellow-text-inverse`        |
| 100  | `--yellow-100` | `#FBF2DA` | `--yellow-surface-stroke`, `--yellow-surface-hover` |
| 200  | `--yellow-200` | `#F0E2B8` | `--yellow-surface-stroke`                           |
| 300  | `--yellow-300` | `#DBCA8A` | `--yellow-md`                                       |
| 400  | `--yellow-400` | `#C0A64E` | `--yellow-md-hover` (bg-only; warm-hue exception)   |
| 500  | `--yellow-500` | `#866B0E` | `--yellow-md-stroke`                                |
| 600  | `--yellow-600` | `#6A5308` | `--yellow-strong`                                   |
| 700  | `--yellow-700` | `#534005` | `--yellow-strong-pressed`                           |
| 800  | `--yellow-800` | `#403103` | `--yellow-strong-stroke`                            |
| 900  | `--yellow-900` | `#2E2302` | `--yellow-text`                                     |

### Purple

| Stop | Variable       | Hex       | Semantic Alias                                      |
| ---- | -------------- | --------- | --------------------------------------------------- |
| 0    | `--purple-0`   | `#FDFCFF` | `--purple-bg`                                       |
| 50   | `--purple-50`  | `#FAF7FF` | `--purple-surface` / `--purple-text-inverse`        |
| 100  | `--purple-100` | `#F0E8FE` | `--purple-surface-stroke`, `--purple-surface-hover` |
| 200  | `--purple-200` | `#DFD0FC` | `--purple-surface-stroke`                           |
| 300  | `--purple-300` | `#C9B0F8` | `--purple-md`                                       |
| 400  | `--purple-400` | `#A882F2` | `--purple-md-hover`                                 |
| 500  | `--purple-500` | `#8B5CF6` | `--purple-md-stroke`                                |
| 600  | `--purple-600` | `#7240D9` | `--purple-strong`                                   |
| 700  | `--purple-700` | `#5B2FB5` | `--purple-strong-pressed`                           |
| 800  | `--purple-800` | `#462493` | `--purple-strong-stroke`                            |
| 900  | `--purple-900` | `#331A6E` | `--purple-text`                                     |

### Grey

Grey is the **UI structural scale** — backgrounds, surfaces, borders, and text. It uses a true neutral tone with a very slight cool lean (B channel 1–2 higher than R/G), keeping it clean and contemporary alongside the warm chromatic palette. The same 10-stop structure applies. In the app, grey stops are consumed via purpose-driven semantic tokens (e.g., `--bg-canvas`, `--fg-primary`) rather than `--grey-*` directly.

| Stop | Variable     | Hex       | Semantic Token                                                                  | Contrast vs White |
| ---- | ------------ | --------- | ------------------------------------------------------------------------------- | ----------------- |
| 0    | `--grey-0`   | `#FEFEFE` | `--bg-canvas`                                                                   | ~1.01:1           |
| 50   | `--grey-50`  | `#FAFAFA` | `--fg-inverse`                                                                  | ~1.03:1           |
| 100  | `--grey-100` | `#F2F2F3` | `--bg-surface-1`                                                                | ~1.08:1           |
| 200  | `--grey-200` | `#E6E6E7` | `--bg-surface-2`, `--border-subtle`                                             | ~1.16:1           |
| 300  | `--grey-300` | `#D4D4D6` | `--bg-surface-3`, `--border-default`, `--fg-inverse-secondary`, `--fg-disabled` | ~1.38:1           |
| 400  | `--grey-400` | `#929295` | `--border-strong`, `--fg-placeholder`                                           | ~3.0:1            |
| 500  | `--grey-500` | `#6F6F72` |                                                                                 | ~5.0:1            |
| 600  | `--grey-600` | `#565659` | `--fg-tertiary`, `--bg-inverse-surface`                                         | ~7.3:1            |
| 700  | `--grey-700` | `#404042` | `--fg-secondary`, `--bg-inverse-subtle`                                         | ~10.5:1           |
| 800  | `--grey-800` | `#2D2D2F` | `--bg-inverse`                                                                  | ~14.1:1           |
| 900  | `--grey-900` | `#1C1C1E` | `--fg-primary`                                                                  | ~17.4:1           |

#### UI mapping

Grey primitives map to purpose-driven semantic tokens in the app:

| UI Role                              | Semantic Token       | Primitive    | Notes                                                    |
| ------------------------------------ | -------------------- | ------------ | -------------------------------------------------------- |
| Page / canvas background             | `--bg-canvas`        | `--grey-0`   | Near-white, unified page + card bg                       |
| Card / panel surface                 | `--bg-surface-1`     | `--grey-100` |                                                          |
| Hover on cards, secondary surface    | `--bg-surface-2`     | `--grey-200` |                                                          |
| Tertiary / active surface            | `--bg-surface-3`     | `--grey-300` |                                                          |
| Subtle borders, dividers             | `--border-subtle`    | `--grey-200` |                                                          |
| Default borders                      | `--border-default`   | `--grey-300` |                                                          |
| Strong borders                       | `--border-strong`    | `--grey-400` | 3:1 contrast — usable for UI boundaries                  |
| Secondary foreground (text, icons)   | `--fg-secondary`     | `--grey-700` | AAA at 10.5:1                                            |
| Tertiary foreground (hints)          | `--fg-tertiary`      | `--grey-600` | AAA at 7.3:1                                             |
| Placeholder foreground               | `--fg-placeholder`   | `--grey-400` | 3:1 — readable hint in enabled inputs                    |
| Disabled foreground                  | `--fg-disabled`      | `--grey-300` | ~1.4:1 — intentionally faded to signal non-interactivity |
| Primary foreground (text, icons)     | `--fg-primary`       | `--grey-900` | AAA at 17.4:1                                            |
| Inverse foreground (on dark bg)      | `--fg-inverse`       | `--grey-50`  |                                                          |
| Inverse surface                      | `--bg-inverse`       | `--grey-800` |                                                          |
| Hover on resting surfaces            | `--hover-surface`    | `--grey-100` | Default hover background for elements on canvas          |
| Nested hover (inside hovered parent) | `--hover-on-surface` | `--grey-50`  | Punches lighter to create contrast, not darker           |
| Nav background                       | `--nav-surface`      | `--grey-100` | Distinct from canvas (grey-0)                            |
| Nav border                           | `--nav-border`       | `--grey-200` |                                                          |
| Nav item hover                       | `--nav-hover`        | `--grey-200` |                                                          |
| Nav item selected                    | `--nav-selected`     | `--grey-300` |                                                          |

#### Dark mode

Dark mode is implemented as a `@media (prefers-color-scheme: dark)` override in `colors.css`. Primitives (Layer 1) are unchanged — only semantic tokens (Layer 2) are remapped.

**Core strategy:**

- Surfaces invert: canvas uses grey-900, surfaces step lighter (800 → 700 → 600)
- Text inverts: primary uses grey-50, secondary grey-300, tertiary grey-400
- Accent/status colors shift to the 400 stop (desaturated, less vibrant on dark backgrounds) instead of 500
- Status surfaces use 800 stops, subtle uses 700 — dark tinted backgrounds instead of pastels
- Borders shift to grey-700/600/500 — visible against dark surfaces
- Focus ring uses blue-400 for better visibility on dark backgrounds
- Overlay uses `rgba(0,0,0,0.6)` — darker to maintain contrast against dark surfaces

**Key mappings:**

| Token                      | Light       | Dark        |
| -------------------------- | ----------- | ----------- |
| `--bg-canvas`              | grey-0      | grey-900    |
| `--bg-surface-1`           | grey-50     | grey-800    |
| `--bg-surface-2`           | grey-200    | grey-700    |
| `--fg-primary`             | grey-900    | grey-50     |
| `--fg-secondary`           | grey-700    | grey-200    |
| `--fg-tertiary`            | grey-600    | grey-300    |
| `--fg-disabled`            | grey-300    | grey-600    |
| `--border-subtle`          | grey-200    | grey-700    |
| `--accent-default`         | blue-500    | blue-400    |
| `--{color}-hover`          | {color}-400 | {color}-300 |
| `--{color}-surface`        | {color}-50  | {color}-800 |
| `--{color}-surface-stroke` | {color}-100 | {color}-700 |
| `--{color}-surface-hover`  | {color}-100 | {color}-700 |
| `--{color}-subtle`         | {color}-200 | {color}-700 |
| `--hover-surface`          | grey-100    | grey-700    |
| `--hover-on-surface`       | grey-50     | grey-800    |

**Do not** create separate dark-mode primitives or alternate hue scales. The same 0–900 ramp serves both modes — only the semantic pointer changes.

---

#### Critique of prior neutral tokens

The grey scale replaced a set of ad-hoc neutral tokens that had several issues:

1. **Inconsistent temperature.** `--bg-canvas` (rgba 254,253,253) was warm, `--bg-surface-3` (rgba 220,223,225) was cool/blue, `--fg-primary` (rgba 28,20,20) was reddish. The grey scale uses a consistent true neutral tone across all stops.
2. **Missing mid-tones.** Nothing existed between `--bg-surface-3` (~87% lightness) and `--bg-inverse` (~19% lightness). Grey 400–600 fill this gap for icons, secondary text, and disabled states.
3. **`--fg-disabled` and `--fg-placeholder` were identical** (both rgba 176,180,184). They are now split: `--fg-placeholder` maps to `--grey-400` (readable hint) and `--fg-disabled` maps to `--grey-300` (intentionally faded).
4. **border-subtle was nearly invisible.** The old `--border-subtle` and `--bg-surface-1` were only ~3.5% apart. `--border-subtle` now uses `--grey-200` for clearer separation from surfaces.
5. **Uneven surface spacing.** The jump from canvas to surface-1 was much larger than surface-1→surface-2. The grey 50/100/200 steps provide even perceptual spacing across surfaces.

---

## Semantic Stop Roles

Every color scale uses an 11-stop structure (0–900) with consistent semantic roles. Each stop has a semantic alias following the pattern `--{theme}-{role}`:

| Stop | Semantic Alias                                        | Description                                                                                                                                                      |
| ---- | ----------------------------------------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| 0    | `--{theme}-bg`                                        | Non-interactive reading surface — message bubbles, status banners, info panels. Near-white; comfortable for long-form text.                                      |
| 50   | `--{theme}-surface` / `--{theme}-text-inverse`        | Interactive component background — secondary button rest, selected chip, active filter pill. Noticeably tinted. Also used as text color on dark (500–900) fills. |
| 100  | `--{theme}-surface-stroke`, `--{theme}-surface-hover` | Soft border for surface components; hover state on surface (50) elements                                                                                         |
| 200  | `--{theme}-surface-stroke`                            | Borders, focus rings on surface elements                                                                                                                         |
| 300  | `--{theme}-md`                                        | Mid-tone background, selected state bg                                                                                                                           |
| 400  | `--{theme}-md-hover`                                  | Hover state on medium surfaces                                                                                                                                   |
| 500  | `--{theme}-md-stroke`                                 | Borders, icons, primary interactive color                                                                                                                        |
| 600  | `--{theme}-strong`                                    | Primary buttons, bold UI elements                                                                                                                                |
| 700  | `--{theme}-strong-pressed`                            | Pressed/active state on strong elements                                                                                                                          |
| 800  | `--{theme}-strong-stroke`                             | Borders on strong surfaces, deep accents                                                                                                                         |
| 900  | `--{theme}-text`                                      | Body text, icons on light backgrounds                                                                                                                            |

### The 0 vs 50 distinction

- **0 (`{theme}-bg`):** A tinted surface you _read off of_. Use for message bubbles, alert backgrounds, info cards, status banners — passive, non-interactive containers where the color provides ambient context. Close enough to white that body text is comfortable.
- **50 (`{theme}-surface`):** A tinted surface you _interact with_. Use for secondary button fills, selected chips, active filter pills, interactive card highlights. Noticeably tinted so it reads as "this is a clickable thing," not "this is background."

### Text pairing rules

- On backgrounds of **bg through md-hover** (0–400): use `--{theme}-text` (900) for text.
- On backgrounds of **md-stroke through strong-stroke** (500–900): use `--{theme}-text-inverse` (50) for text.

> `--{theme}-text-inverse` is an alias for `--{theme}-surface` (50). Both point to the same value — use whichever communicates intent more clearly.

---

## New Color Scale Generation

Guidelines for generating new color scales that are consistent with this design system's existing palettes.

### The 11-Stop Scale Structure

| Stop | Name           | Primary Role                                                    | Text Use                  |
| ---- | -------------- | --------------------------------------------------------------- | ------------------------- |
| 0    | Background     | Non-interactive reading surface (message bubbles, info panels)  | Never                     |
| 50   | Surface        | Interactive component background (secondary button fill, chips) | Never                     |
| 100  | Surface hover  | Hover state on surface (50) elements                            | Never                     |
| 200  | Surface stroke | Borders, focus rings on surface elements                        | Never                     |
| 300  | Soft           | Focus rings, selected state bg, borders                         | Never                     |
| 400  | Mid            | Bridges 300→500; hover on muted components                      | Large text only (if ≥3:1) |
| 500  | Default        | Primary interactive color (buttons, links, icons)               | Yes, if ≥4.5:1 on white   |
| 600  | Emphasis       | Primary button fill, bold UI elements                           | Yes                       |
| 700  | Strong         | Hover on primary buttons; active/pressed state                  | Yes                       |
| 800  | Deep           | Dark-mode surfaces, deep borders, secondary ink                 | Yes                       |
| 900  | Ink            | Body text, icons, dark-mode surfaces                            | Yes — must hit AAA        |

> **Note:** Stop 800 sits between the active/pressed dark (700) and the ink tone (900). Use it for dark-mode card surfaces, deep borders, or a secondary ink tone where 700 is too light and 900 is too heavy.

### Perceptual Spacing Rules

The scale must feel **visually even** when displayed as a row of swatches. This is the most important rule — stops that look too similar or too far apart break the system.

- **50 → 100:** Very subtle shift. Nearly indistinguishable side by side, but noticeable on different backgrounds.
- **100 → 200:** Soft step. Still light, clearly a background tone.
- **200 → 300:** Moderate step. 300 should read as "tinted" but not saturated.
- **300 → 400:** Noticeable step. 400 is where the hue starts to feel like itself.
- **400 → 500:** The largest perceptual jump is acceptable here — this is the light-to-dark threshold crossing. But it should not look like two unrelated colors.
- **500 → 600:** Small, intentional step. Side by side they should look like siblings.
- **600 → 700:** Same — small step. This is the rest-to-pressed transition on primary buttons — it should feel like a subtle physical press.
- **700 → 800:** Moderate step. 800 is noticeably darker than 700 but still retains hue identity.
- **800 → 900:** Small-to-moderate step. 900 can feel slightly more neutral/cooler but should still read as the same family.

**Red flag:** If any two adjacent stops look identical, or if any gap looks like two unrelated colors, the spacing is wrong.

### Contrast Requirements

Use the WCAG 2.1 contrast ratio standards against `#FFFFFF` (white):

| Stop   | Minimum Ratio  | Standard                     |
| ------ | -------------- | ---------------------------- |
| 50–300 | No requirement | Background only              |
| 400    | ≥ 3:1          | Large text only (or bg)      |
| 500    | ≥ 4.5:1        | AA normal text               |
| 600    | ≥ 7:1          | AAA preferred                |
| 700    | ≥ 7:1          | AAA                          |
| 800    | ≥ 10:1         | AAA — between strong and ink |
| 900    | ≥ 12:1         | Strong AAA — aim for 14:1+   |

> **Orange/Yellow exception:** Warm hues in the 400 range often cannot reach 3:1 without becoming muddy. If a 400-stop warm color cannot reach 3:1, document it as background-only and ensure 500 compensates with a stronger ratio.

### Saturation & Tone Behavior Across the Scale

Follow these directional rules when moving through the scale:

- **50–200:** Desaturate heavily. These should feel like white with a whisper of the hue — almost neutral.
- **200–400:** Gradually reintroduce saturation. Colors become more recognizable as the hue family.
- **400–500:** Peak or near-peak saturation. This is the "true" color.
- **500–700:** Reduce brightness, maintain or slightly reduce saturation. Avoid muddy brown/gray shifts — keep the hue identity intact.
- **900:** Can shift slightly cooler or more neutral. This is an ink tone, not a vivid color. It should feel like the hue's shadow.

**Avoid:** Letting any stop drift into a neighboring hue family. A blue scale should never produce a stop that reads as purple or teal. A pink scale should never drift into red or lavender. Stay on the hue axis.

### Generating From Seed Colors

When given one or more existing colors to anchor the scale, follow this process:

#### Step 1 — Identify the Seed's Position

Determine where the seed color belongs on the 10-stop scale based on its brightness and saturation:

- Near-white, very light → 50 or 100
- Light, clearly tinted → 100 or 200
- Medium light, some saturation → 300
- Saturated, mid-brightness → 400 or 500
- Dark, rich → 600 or 700
- Near-black → 900

#### Step 2 — Anchor and Extrapolate

Lock the seed color(s) in place and generate the remaining stops by extrapolating in both directions using the perceptual spacing and saturation rules above.

#### Step 3 — Verify Contrast

Check every stop from 400 upward against `#FFFFFF`. Adjust lightness until contrast targets are met. Never adjust hue to fix contrast — only adjust lightness/brightness.

#### Step 4 — Name and Document

Assign stop numbers and document the role for each. Flag any stops that are new additions vs. existing colors.

### Output Format

When presenting a new scale, always deliver:

1. **Swatch row** — all stops displayed left to right, 50 to 900, with hex codes labeled.
2. **Issues list** — any problems with the seed colors (gaps, contrast failures, redundancy).
3. **Contrast table** — ratio vs. white for each stop, with AA/AAA pass/fail.
4. **Gap visualization** — a horizontal bar showing where missing stops fall between existing ones.
5. **Semantic token map** — three groups: Surfaces & Backgrounds, Interactive States, Text & Icons.
6. **TL;DR table** — a compact table listing only the new colors being added, with hex, role, and contrast ratio.

### Common Mistakes to Avoid

- **Do not** create two stops that are perceptually identical. Consolidate and use the freed slot for a missing mid-tone.
- **Do not** use 50–400 as text colors, even if they technically pass contrast on a dark background. They are background-only tones in this system.
- **Do not** skip the 600 stop. It is the resting state for primary buttons — the 500 stop is hover (lighter) and 700 is pressed (darker).
- **Do not** use the 900 stop for anything other than text, icons, or dark-mode surfaces. It is too dark to use as a UI color.
- **Do not** change the hue angle to fix a contrast problem. Only adjust lightness. Hue drift breaks the color family's identity.
- **Do not** generate a scale with fewer than 11 stops. A 3-color or 6-color scale is always considered incomplete in this system.

### Quick Checklist

Before finalizing any new scale, confirm:

- [ ] All 11 stops are present (0, 50, 100, 200, 300, 400, 500, 600, 700, 800, 900)
- [ ] No two adjacent stops look perceptually identical
- [ ] No gap between adjacent stops looks like two unrelated colors
- [ ] 50–300 are clearly background-only (light, low contrast)
- [ ] 500 achieves ≥ 4.5:1 on white
- [ ] 600 achieves ≥ 7:1 on white
- [ ] 700 achieves ≥ 7:1 on white
- [ ] 800 achieves ≥ 10:1 on white
- [ ] 900 achieves ≥ 12:1 on white
- [ ] Hue identity is consistent across all stops — no drift into neighboring hue families
- [ ] Semantic token map covers: surfaces, interactive states, and text/icons

---

## Color Accessibility Quick Reference

| Element                        | Minimum Contrast | Standard |
| ------------------------------ | ---------------- | -------- |
| Body text                      | 4.5:1            | WCAG AA  |
| Large text (18px+ bold, 24px+) | 3:1              | WCAG AA  |
| UI components (borders, icons) | 3:1              | WCAG AA  |
| Body text (enhanced)           | 7:1              | WCAG AAA |

**Testing tools:**

- Chrome DevTools → Inspect → Color picker shows contrast ratio
- WebAIM Contrast Checker
- Stark (Figma plugin)

**Rule:** Never use color alone to convey meaning. Always pair with icon, text, pattern, or position.
