Theme Token Schema (v1)
On this page
Status
Locked v1 on 2026-06-14 (external design engagement). This is the durable contract ADR-036 anchors and Plan U Step 8 (Plan U: State Bundle Pattern (child plan of Plan S umbrella)) implements. The engagement package that delivered it is archived under docs/handoffs/2026-06-14-token-system/ (provenance); this page is the durable source of truth — the canonical palette theme.toml files (crates/craig-state-bundle/theme/) carry the same values for the build.
Why a fixed token set
A deployment ships ONE theme.toml; the app generates the stylesheet from it — :root from [color.light], the dark block from [color.dark] — emitting one CSS custom property per token (--primary, --surface, --danger, …). The swap is mechanical because the token names are fixed: a new jurisdiction provides values, never new names. The names are roles, not hues (--surface, not --white; --danger, not --red), so a palette can recolor every surface without the markup knowing.
Rules
-
Names are roles, fixed across every palette. A jurisdiction provides values only.
-
Light + dark are both required. OS
prefers-color-schemedrives the switch; an explicit[data-theme]attribute on the root overrides it. A bundle cannot opt out of dark. -
Red is reserved for danger.
--danger*is the only red in the system; a palette must not spend red on brand or accent (see Brand Identity, Palettes, and Typography § Red is reserved exclusively for danger). -
logo_goldis not a UI token. It lives in[branding]and is consumed only by the agency seal / logo SVG — never for surfaces, text, or state, so the brand asset can’t leak into the UI. -
High-contrast is optional, 2-axis, and OS-driven. A palette MAY add
[color.high_contrast.light]/[color.high_contrast.dark]— SPARSE per-mode overrides (only the deepened role tokens), merged onto the base modes at build time, so HC inherits full light/dark token parity. They render into@media (prefers-contrast: more)(light) and@media (prefers-contrast: more) and (prefers-color-scheme: dark)(dark); there is no[data-contrast]toggle (ADR-036 §6). Absent, NOprefers-contrastoverride is emitted and the browser’s native contrast handling applies (the app does NOT derive HC fromlight). The product palette ships HC (deepeneddanger/warning/successplus#fff/#000on-state foregrounds, AAA 7:1).
The token set (12 role groups)
| Group | Tokens |
|---|---|
Brand |
|
Text |
|
Surfaces |
|
Borders |
|
Link |
|
Danger |
|
Warning |
|
Success |
|
Info |
|
State foregrounds |
|
Feedback |
|
Affordance |
|
* accent-2 is optional; if omitted the app falls back to accent.
† select-arrow is a URL-valued token — the <select> dropdown-arrow SVG data: URI with its fill baked per mode — not a color. A color inside a url("data:…") can’t reference a var(), so the whole data-URI is the token (#642); this keeps the arrow adaptive to light/dark and out of the static stylesheets' raw-hex guard.
Semantic-triad convention
Each state (danger / warning / success / info) ships three values, used consistently everywhere:
-
--<state>— the "loud" hue: icon, text, or 1px border accent. -
--<state>-surface— the soft background fill of a badge / banner. -
--<state>-ink— text placed ON-surface(AA-guaranteed).
A danger pill is background: var(--danger-surface); color: var(--danger-ink); its leading dot/icon is var(--danger).
For a SOLID state fill (a filled badge or step, not the soft surface), the foreground is --on-<state> (on-danger / on-success / on-info) — the per-mode text/icon color, mirroring --on-primary: white on the dark light-mode bases, a hue-tinted near-black on the light dark-mode bases (so AA holds in both modes; the design handoff proves 5.3–7.2:1). Warning stays soft (surface + ink), so there is no on-warning.
Migration from the legacy 18 --color-* tokens
Plan U Step 8 retired the Georgia-Orchard --color- set behind a temporary alias layer (--color-X: var(--new-token)) for the grace window; #596 then rewrote all ~176 references to the canonical tokens and removed the alias layer, so no --color- name remains (a craig-web test guards against reintroduction). The legacy→canonical map below is kept as a historical reference.
| Legacy (Georgia Orchard) | New token | Notes |
|---|---|---|
|
|
|
|
|
decoration only |
|
|
brand asset, NOT a UI token |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
composes — no new token |
New tokens the system gains (absent before): --faint, --on-primary, --primary-hover, --primary-tint, --border-soft, --link-hover, the full success / info triads, every -ink, and --focus.
Baseline palette values
Both baseline palettes ship today. Names are identical; only values differ. The bundle theme.toml is the build’s source; the values below are the durable record.
Simple Statehouse — worker UI (family = "professional")
[branding] agency_name Georgia DHS · logo_gold #D0AE56 · fonts Montserrat / JetBrains Mono.
| Token | Light | Dark | Role |
|---|---|---|---|
|
|
|
header · nav · primary buttons |
|
|
|
primary hover/active |
|
|
|
selected row · subtle brand fill |
|
|
|
decoration only — rules, marks |
|
|
|
optional secondary accent (slate-teal) |
|
|
|
headings, strongest ink |
|
|
|
body copy |
|
|
|
secondary · captions |
|
|
|
placeholders, 4th tier |
|
|
|
text/icons on |
|
|
|
app background |
|
|
|
cards · raised |
|
|
|
recessed wells |
|
|
|
subtle tinted surface |
|
|
|
default hairline |
|
|
|
inputs · stronger edges |
|
|
|
faint internal dividers |
|
|
|
links |
|
|
|
|
|
|
|
RED = danger only |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
text/icon on a solid |
|
|
|
text/icon on a solid |
|
|
|
text/icon on a solid |
|
|
|
3px focus ring (a11y) |
|
url(…fill |
url(…fill |
|
Simple Statehouse — high-contrast overrides
Sparse deepening merged onto the base modes under @media (prefers-contrast: more) (AAA 7:1; design handoff 2026-06-20). Every other token inherits the base mode.
| Token | HC Light | HC Dark | Note |
|---|---|---|---|
|
|
|
9.78:1 on |
|
|
|
8.90:1 on |
|
|
|
7.91:1 on |
|
|
|
foreground on a solid HC |
|
|
|
foreground on a solid HC |
The Foundation — household / constituent (family = "household")
[branding] agency_name Georgia DHS · logo_gold #D0AE56. Warm, trauma-informed register; --page is warm paper, not white.
| Token | Light | Dark | Role |
|---|---|---|---|
|
|
|
deep teal — headers · primary buttons |
|
|
|
|
|
|
|
|
|
|
|
warm peach — decoration only |
|
|
|
terracotta — warm secondary |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
warm paper background |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
deep teal-green, AA on warm paper |
|
|
|
|
|
|
|
RED = danger only |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
text/icon on a solid |
|
|
|
text/icon on a solid |
|
|
|
text/icon on a solid |
|
|
|
terracotta focus ring (warm palette) |
Generated CSS shape
The app (Plan U Step 8: craig-web materialize_theme_css) renders the active bundle’s palette into a single same-origin stylesheet served from GET /assets/theme.css (CSP-clean — style-src 'self' forbids inline <style>):
:root { --primary:#103052; --surface:#ffffff; --danger:#b01116; /* …all tokens (light) */ }
@media (prefers-color-scheme: dark) {
:root:not([data-theme="light"]) { --primary:#1d4a7d; /* …(dark) */ }
}
[data-theme="dark"] { --primary:#1d4a7d; /* …(dark) — explicit override */ }
[data-theme="light"] { --primary:#103052; /* …(light) — explicit override */ }
@media (prefers-contrast: more) {
:root { --danger:#8a0d11; /* …HC-light: full set, deepened tokens overridden */ }
}
@media (prefers-contrast: more) and (prefers-color-scheme: dark) {
:root:not([data-theme="light"]) { --danger:#ef9c93; /* …HC-dark */ }
}
The two @media (prefers-contrast: more) blocks are emitted only when the palette declares [color.high_contrast]; absent, neither block appears.
Where this lands in code
-
Bundle: the canonical palettes' source
theme/.tomllives incraig-state-bundle/theme/(e.g.simple-statehouse.toml); itsbuild.rsparses them at BUILD time —tomlis a *build-dependency ofcraig-state-bundle, never a runtime one (ADR-036 §2 no-toml-leak) — and generates thePalettedata as&'static strRust. A jurisdiction selects a named palette and overrides branding in plain Rust (e.g.craig-state-ga); only a jurisdiction shipping a CUSTOM palette embeds + parses its owntheme.tomlviainclude_str!. -
BFF:
craig-webmaterializes the active bundle’sThemeContributioninto the/assets/theme.cssroute body once at boot (ADR-036 §4); the generated string is the registry. -
Schema: this page is the contract
theme.tomlis validated against.
Related
-
ADR-036 — the anchoring decision (route + bundle-parsed Palette + alias layer +
--ink/-ink). -
Brand Identity, Palettes, and Typography — the design-language overview (brand tokens, typography, red-is-danger).
-
The Five Engineering Contracts — Contract 4 (token + theme export).
-
Plan U — Step 8 implements this.
-
Provenance:
docs/handoffs/2026-06-14-token-system/(the engagement delivery:token.schema.md, the two*.toml,CRAIG Token System.html).