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-scheme drives 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_gold is 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, NO prefers-contrast override is emitted and the browser’s native contrast handling applies (the app does NOT derive HC from light). The product palette ships HC (deepened danger/warning/success plus #fff/#000 on-state foregrounds, AAA 7:1).

The token set (12 role groups)

Group Tokens

Brand

primary · primary-hover · primary-tint · accent · accent-2*

Text

heading · body · muted · faint · on-primary

Surfaces

page · surface · sunken · surface-tint

Borders

border · border-strong · border-soft

Link

link · link-hover

Danger

danger · danger-surface · danger-ink

Warning

warning · warning-surface · warning-ink

Success

success · success-surface · success-ink

Info

info · info-surface · info-ink

State foregrounds

on-danger · on-success · on-info

Feedback

focus

Affordance

select-arrow

* 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

--color-dark-green ×36

--primary

--color-gold ×9

--accent

decoration only

--color-dhs-logo-gold

[branding].logo_gold

brand asset, NOT a UI token

--color-body-text ×17

--body

--color-heading ×12

--heading

--color-sage ×17

--muted

--color-link ×4

--link

--color-page-bg ×4

--page

--color-card-bg ×13

--surface

--color-light-green

--sunken

--color-light-tint

--surface-tint

--color-border ×20

--border

--color-input-border ×5

--border-strong

--color-alert-red ×15 / -bg

--danger / --danger-surface

--danger-ink is new

--color-warn-bg / -text ×6

--warning-surface / --warning-ink

--warning is new

--color-rules-engine / -border

--surface-tint + --accent

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

primary

#103052

#1d4a7d

header · nav · primary buttons

primary-hover

#1a4674

#2a5e96

primary hover/active

primary-tint

#e7edf6

#15273b

selected row · subtle brand fill

accent

#cca63b

#cca63b

decoration only — rules, marks

accent-2

#1f6f8a

#5aa7c2

optional secondary accent (slate-teal)

heading

#0d1b2a

#e9edf3

headings, strongest ink

body

#38424b

#cdd5df

body copy

muted

#5a6675

#99a5b4

secondary · captions

faint

#8893a1

#6a7686

placeholders, 4th tier

on-primary

#ffffff

#ffffff

text/icons on --primary

page

#f3f7f5

#0e1722

app background

surface

#ffffff

#16202e

cards · raised

sunken

#dfe8e2

#0a1119

recessed wells

surface-tint

#eef4f0

#1b2735

subtle tinted surface

border

#e2e6ea

#273646

default hairline

border-strong

#9aa6b2

#4a5b6b

inputs · stronger edges

border-soft

#eef1f4

#1e2a38

faint internal dividers

link

#244c8c

#8fb3e0

links

link-hover

#16335f

#b3ccec

danger

#b01116

#e0675c

RED = danger only

danger-surface

#f7dedd

#381b19

danger-ink

#8a1014

#f0b2aa

warning

#b5650f

#e0a64a

warning-surface

#fbedd6

#33260f

warning-ink

#7a4400

#f0cd97

success

#1f7a52

#5cba84

success-surface

#e2f1ea

#15281e

success-ink

#155c3d

#a7e0c2

info

#1f6f8a

#5aa7c2

info-surface

#e2eff3

#122a31

info-ink

#16505f

#a6d3e3

on-danger

#ffffff

#2a0e0c

text/icon on a solid --danger fill

on-success

#ffffff

#0d2419

text/icon on a solid --success fill

on-info

#ffffff

#0c1e26

text/icon on a solid --info fill

focus

#cca63b

#e3c155

3px focus ring (a11y)

select-arrow

url(…fill #8d8d8d)

url(…fill #99a5b4)

<select> dropdown-arrow SVG (URL-valued, #642)

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

danger

#8a0d11

#ef9c93

9.78:1 on #fff · 9.85:1 on #000

success

#14543a

#8fd3aa

8.90:1 on #fff · 12.07:1 on #000

warning

#7a4400

#f0c47a

7.91:1 on #fff · 12.88:1 on #000

on-danger

#ffffff

#000000

foreground on a solid HC --danger

on-success

#ffffff

#000000

foreground on a solid HC --success

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

primary

#02343d

#3a9080

deep teal — headers · primary buttons

primary-hover

#12443b

#4ba692

primary-tint

#e7f0ec

#1d2e28

accent

#fda578

#fda578

warm peach — decoration only

accent-2

#c8481a

#e0734a

terracotta — warm secondary

heading

#1b2a28

#eef0e8

body

#3f4d4a

#cdd6cf

muted

#6f7a76

#9aa49e

faint

#9aa49f

#6c7771

on-primary

#ffffff

#ffffff

page

#fdf7f0

#0e1a17

warm paper background

surface

#ffffff

#16241f

sunken

#f7ede1

#0b1512

surface-tint

#fff3e9

#231a12

border

#ecdfce

#26332e

border-strong

#bda98a

#45554d

border-soft

#f3ebdf

#1d2925

link

#0c5a4e

#7fd0c0

deep teal-green, AA on warm paper

link-hover

#02343d

#a3e0d2

danger

#b3261e

#e0675c

RED = danger only

danger-surface

#fbe9e7

#381b19

danger-ink

#8a1c16

#f0b2aa

warning

#9a5b00

#e0a64a

warning-surface

#fbeedd

#33260f

warning-ink

#7a4400

#f0cd97

success

#1f7a52

#5cba84

success-surface

#e6f2ec

#15281e

success-ink

#155c3d

#a7e0c2

info

#1f6f8a

#5aa7c2

info-surface

#e3eff3

#122a31

info-ink

#155264

#a6d3e3

on-danger

#ffffff

#2a0e0c

text/icon on a solid --danger fill

on-success

#ffffff

#0d2419

text/icon on a solid --success fill

on-info

#ffffff

#0c1e26

text/icon on a solid --info fill

focus

#c8481a

#e0734a

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/.toml lives in craig-state-bundle/theme/ (e.g. simple-statehouse.toml); its build.rs parses them at BUILD time — toml is a *build-dependency of craig-state-bundle, never a runtime one (ADR-036 §2 no-toml-leak) — and generates the Palette data as &'static str Rust. 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 own theme.toml via include_str!.

  • BFF: craig-web materializes the active bundle’s ThemeContribution into the /assets/theme.css route body once at boot (ADR-036 §4); the generated string is the registry.

  • Schema: this page is the contract theme.toml is validated against.

  • 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).

Edit this page · latest