Tokens
Every color a component uses is a semantic role, resolved in one theme block. Components never touch raw values: no hex, no bare gray scales. Change the theme block and the whole product recolors, including the illustrations.
The roles
| Role | Value | Use | Utility |
|---|---|---|---|
| bg | #ffffff | Page background, set once on the body. Porcelain. | bg-bg |
| surface | #ffffff | Cards, inputs, the floating bar. The layer that sits on bg; borders do the separating. | bg-surface |
| brand | #fde047 | Primary buttons, active states, heading accents. Lemon. Fills only — never text on a light ground. | bg-brand / text-brand |
| brand-hover | #eab308 | Primary hover fill, one step deeper than brand. | bg-brand-hover |
| on-brand | #0f172a | Text and icons on a brand fill. Ink-on-Lemon is the AA pairing. | text-on-brand |
| brand-deep | #a16207 | The brand hue at text strength, for brand-colored text and icons on light grounds. | text-brand-deep |
| accent | #f59e0b | Secondary accent, the accent button. Amber. | bg-accent |
| accent-hover | #d97706 | Accent hover fill; also the text-safe amber and the focus ring (raw amber misses 3:1 on white). | bg-accent-hover |
| tint | #fef9c3 | Soft brand-tinted fills: badges, selected surfaces. | bg-tint |
| ink | #0f172a | Primary text. | text-ink |
| muted | #64748b | Secondary text, hints, captions. | text-muted |
| line | #e5e7eb | Every border. There is exactly one hairline color. | border-line |
| scrollbar-thumb | brand | The scrollbar thumb fill: brand, stepping deeper on hover (brand-hover) and drag (brand-deep). The track is transparent. | --scrollbar-thumb … |
| danger | #ef4444 | Errors only. Functional, not brand — that it equals Tassel Red is a coincidence the once-per-composition rule does not govern. | text-danger |
| warning | #d97706 | Functional status. | text-warning |
| success | #16a34a | Functional status. | text-success |
| info | #2563eb | Functional status. | text-info |
Data-viz slots
Charts take categorical color from six fixed slots, assigned in order and never cycled. The set is validated, not eyeballed: every slot sits in the OKLCH readable-lightness band, clears the chroma floor, holds color-vision-deficiency separation from its neighbors, and reaches 3:1 against the surface. A seventh series folds into "Other."
| Slot | Value | Note |
|---|---|---|
| chart-1 | #a16207 | the brand hue, snapped into the readable band |
| chart-2 | #2a78d6 | blue |
| chart-3 | #ef4444 | Tassel Red |
| chart-4 | #008300 | green |
| chart-5 | #4a3aa7 | violet |
| chart-6 | #0d9488 | teal |
Rules
- Token-driven, always. A component that hardcodes a hex or reaches for a raw gray is a bug, even when the result looks identical.
- Brand is a fill, not a text color. Lemon text on a light ground is illegible. Text on a brand fill wears on-brand; brand-colored text on a light ground wears brand-deep.
- Status colors are functional, not brand. Errors are danger-red even in a lemon product; warnings, success, and info keep their conventional hues. Never recolor a status to stay on-brand.
- One hairline. All borders share the single line token. If a border needs to be darker, the component actually wants a background change (see Surfaces).
- Utilities read as roles. Markup says bg-surface and border-line, never a value.
- There is no dark mode. One light theme. This is a stance, not an omission.
- Co-brands re-value roles, never markup. A partner theme (e.g. the Harvard crimson instance) overrides the theme block under its instance selector; component markup never branches on instance.