@ponchia/ui 0.3.6 → 0.4.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,272 @@
1
+ # ADR-0001 — Color system: governed evolution beyond monochrome
2
+
3
+ Status: accepted (all steps 1–8 implemented in 0.4.0) · 2026-05-31 ·
4
+ supersedes the informal "monochrome + one accent" framing in `README.md` /
5
+ `package.json`
6
+
7
+ > This is the **color constitution** for `@ponchia/ui`. It is forward-looking:
8
+ > nothing here changes the default build. It defines the tiers color is
9
+ > allowed to occupy, the rules that keep restraint load-bearing, and the
10
+ > ordered, backward-compatible path to expand expression without diluting the
11
+ > brand. Companion to [`architecture.md`](../architecture.md).
12
+
13
+ ## Context
14
+
15
+ `@ponchia/ui` took its aesthetic from Nothing: monochrome surfaces, one
16
+ rationed red accent, dot-matrix motifs. The driving question for this ADR:
17
+ **is monochrome creatively restrictive, and should we evolve past Nothing's
18
+ literal one-hue constraint?**
19
+
20
+ Two facts frame the answer.
21
+
22
+ **1. We are already not monochrome — and that was the right call.** The README
23
+ says "monochrome, one rationed accent." The CSS says otherwise. We already
24
+ ship a full **functional status palette** (`--success`, `--warning`,
25
+ `--danger`, `--info`, each with light/dark/`-soft` variants, contrast-tuned
26
+ per theme, wired into `ui-alert` / `ui-toast` / `ui-meter`). It is rationed —
27
+ status-only, always paired with a leading dot/shape, never decorative — but it
28
+ exists. So the real model already has **three *active* tiers nobody wrote
29
+ down** (neutral + accent + status); the two new axes this ADR adds (display
30
+ expression, data-viz) are tiers 3–4. The question is not "should we abandon
31
+ monochrome" (we did, carefully, around 0.3.4) but **"what is our color theory
32
+ for the next axis."**
33
+
34
+ **2. Inspiration is not doctrine.** The thing worth inheriting from Nothing is
35
+ the *attitude* — restraint, intentionality, signal-over-noise — **not** the
36
+ literal one-hue limit. "We do not decorate with color; we signal with it" is a
37
+ stronger, more defensible principle than "we have no color," and we are most
38
+ of the way to it already. Monochrome *is* creatively restrictive, but only on
39
+ three specific axes: **categorical distinction** (tags/legends past ~3 items),
40
+ **multi-series data-viz** (one accent + grays cannot draw a 5-line chart — we
41
+ already felt this; `tokens/resolved.json` exists because charts needed real
42
+ color), and **brand/emotional range** (one hue at a time). What monochrome
43
+ *buys* and we must not casually spend: identity differentiation, forced
44
+ hierarchy discipline, accessibility-by-default, and the scarcity that makes the
45
+ one accent *mean* something.
46
+
47
+ ### What SOTA looks like (2025–2026)
48
+
49
+ | Practice | Who | Relevance to us |
50
+ | --- | --- | --- |
51
+ | **Perceptual color space** (equal numeric step = equal perceived step) | Tailwind v4 (OKLCH+P3), Radix 3 (P3), Material 3 (HCT) | We derive `--accent-1..6` via `color-mix(in srgb,…)`; OKLCH is the modern substrate and is **CSS-native** — no dependency cost |
52
+ | **Relative color syntax** `oklch(from var(--base) calc(l-.1) c h)` | CSS Color 5 | ~90% support, Baseline in 2026 — lets a single hue derive a whole tonal ramp natively |
53
+ | **WCAG 2.2 to pass, APCA to design** | Radix text steps spec'd in APCA Lc | WCAG 2.2 AA stays the legal/hard gate; APCA (Lc, accounts for size/weight, WCAG 3 candidate) as an **advisory** track |
54
+ | **Role-bound scale steps** (1–2 bg, 3–5 component, 6–8 border, 9–10 solid, 11–12 text) | Radix 12-step | Our tiers are coarser; the role-bound scale is where depth lives if we ever need it |
55
+ | **Contrast-first generation** (pick a target Lc, derive the swatch) | Adobe Leonardo, Material dynamic | Natural evolution of "one `--accent` derives everything" |
56
+ | **Categorical color is its own discipline** | Wong / IBM / ColorBrewer | Hard limit **6–8 hues**, ≥30 CIELAB lightness delta per pair, **redundant encoding** (shape/pattern/label) — our dot-matrix wheelhouse |
57
+
58
+ SOTA is **not "more hues by default."** It is CSS-native token systems,
59
+ perceptual color, role-based scales, contrast-aware generation, and *governed
60
+ escape hatches*. Our restraint is already SOTA-adjacent; the work is to
61
+ modernize the engine and add opt-in expression without spending the brand.
62
+
63
+ ## Decision
64
+
65
+ Adopt a **five-tier color model** with a fixed governance contract. Color must
66
+ *earn its place by encoding meaning*; the discipline moves from "absence of
67
+ color" to "color is tiered, token-only, contrast-gated, and off-by-default."
68
+
69
+ | Tier | Name | What it is | Default state |
70
+ | --- | --- | --- | --- |
71
+ | **0** | Neutral canvas | The grayscale ramp — surfaces, lines, text | Always on |
72
+ | **1** | Brand accent | One themeable `--accent`, derives `--accent-1..6` | Always on (red) |
73
+ | **2** | Functional status | `success` / `warning` / `danger` / `info` — signal only | Always on |
74
+ | **3** | Display expression | Luminance, dot density/intensity, scan cadence, pulse, reveal timing — the dot-matrix substrate | Opt-in tokens |
75
+ | **4** | Categorical / data-viz | A 6–8 color colorblind-safe scale + sequential/diverging ramps — **charts only** | Opt-in module, never bundled |
76
+
77
+ ### The six rules (the constitution)
78
+
79
+ Each rule notes how it is held — **[gate]** = mechanically enforced by
80
+ `check:color-policy`; **[other gate]** = enforced elsewhere in `npm run check`
81
+ / the e2e suite; **[norm]** = a review norm, not yet machine-checked. Don't
82
+ read "rule" as "all six are CI-enforced" — only what's tagged **[gate]** is.
83
+
84
+ 1. **Token-only.** No raw chromatic color in component CSS — every color is a
85
+ tiered token (grays / `color-mix()` endpoints excepted). **[gate]** —
86
+ `check:color-policy` invariants 1 (tier classification) + 3 (raw-color scan).
87
+ 2. **Contrast-gated.** Every UI text/bg pairing passes **WCAG 2.2 AA**; **APCA
88
+ Lc is advisory** beside the matrix, never the sole gate while WCAG 3 is a
89
+ Working Draft. **[other gate]** — `check:contrast` + the e2e axe suite, not
90
+ `check:color-policy`.
91
+ 3. **Off-by-default.** The default red accent, all token names, and all visual
92
+ baselines remain **unchanged**; the strict-mono purist keeps their exact
93
+ system; everything new is opt-in. **[other gate]** — `check:dist`
94
+ byte-equality + visual baselines.
95
+ 4. **Semantic tier names.** A token's name declares its tier. **Status is never
96
+ decorative**; **chart color is never UI chrome.** No `primary/secondary`.
97
+ **[gate]** for "every color token is tiered" + the reserved `--chart/--cat/
98
+ --data-*` namespace (invariants 1–2); **[norm]** for "status never
99
+ decorative" (needs usage context a static gate can't see).
100
+ 5. **One active UI accent per scope.** No second brand accent. "Duotone" lives
101
+ in Tier 3 (dots/display ornament), not as a second interactive accent.
102
+ **[norm]**.
103
+ 6. **Meaning never by color or motion alone.** Redundant encoding (shape, dot,
104
+ label) is mandatory — satisfies WCAG 1.4.1 / 2.2.2 and is on-brand for the
105
+ dot-matrix. Reduced-motion users must get equivalent information statically.
106
+ **[other gate]** — the e2e axe/`forced-colors`/reduced-motion suite.
107
+
108
+ ### The brand's answer to "decorative color": luminance + time
109
+
110
+ For a *dot-matrix display* system, the authentic expressive channel is not
111
+ hue — it is **brightness and motion**, exactly like a real LED/phosphor panel
112
+ (and we already shipped reveal/pulse in 0.3.6). Tier 3 is the strategic
113
+ substitute for decorative polychrome: it deepens identity instead of diluting
114
+ it, and no competitor in this niche owns it.
115
+
116
+ ### Resolving the `--orange` orphan — RESOLVED (removed, 0.4.0)
117
+
118
+ Audit finding: `--orange` / `--orange-soft` were defined in every token file
119
+ (`tokens/index.js`, `.d.ts`, `tokens.dtcg.json`, `resolved.json`,
120
+ `css/tokens.css`, `reference.md`) but **consumed by no component and documented
121
+ nowhere** — an untiered, dangling hue. Resolution: **removed** (not adopted as
122
+ a categorical seed — there is no data-viz module or demand yet, and a stray
123
+ top-level hue contradicts the tier model). If categorical color lands it ships
124
+ as a governed Tier-4 module (step 7), not a loose token. The new
125
+ `check:color-policy` gate now makes a future untiered hue a hard CI failure.
126
+
127
+ ## Backward-compatibility freeze
128
+
129
+ Hard acceptance criteria for every step below:
130
+
131
+ - **Default rendered output is unchanged** — the red accent defaults, the
132
+ resolved value of every token a component references, and the light/dark
133
+ **visual baselines** are identical. (Removing a token nothing resolves —
134
+ e.g. `--orange` in 0.4.0 — does change `dist/bronto.css` *bytes* but not what
135
+ any selector renders; that is cleanup, not a visual change. The earlier draft
136
+ said "dist bytes unchanged" — the precise invariant is *rendered output*.)
137
+ - Token **names** are not renamed/repurposed (removals of provably-unreferenced
138
+ tokens follow the CONTRIBUTING.md deprecation-policy exception).
139
+ - The existing `--accent-1..6` **sRGB `color-mix` derivation is not silently
140
+ changed.** `scripts/gen-resolved.mjs` only evaluates `color-mix(in srgb,…)`;
141
+ for any other space it returns `null`, and a `null` token is **dropped
142
+ entirely** from `resolved.json` (not defaulted) — so a consumer doing
143
+ `resolved.dark['--accent-3']` would get `undefined` and may throw. Combined
144
+ with the rendered-pixel change, switching the ramp to OKLCH is a breaking
145
+ change twice over. Stable names alone do not make it non-breaking. Any OKLCH
146
+ migration of the **existing** ramp is a documented **minor/RC** decision
147
+ after tooling + visual review, never a silent patch.
148
+
149
+ ## Roadmap (ordered, backward-compatible)
150
+
151
+ 1. **This ADR.** Make the constitution explicit before adding surface. *(done)*
152
+ 2. **`check:color-policy` gate.** Fail on: raw chromatic color in component CSS;
153
+ any untiered palette token; use of the reserved `--chart/--cat/--data-*`
154
+ namespace. Resolve `--orange`. *(done — `scripts/check-color-policy.mjs`,
155
+ wired into `npm run check`; `--orange` removed)*
156
+ 3. **Tier 3 expression tokens.** *(done — `css/dots.css`)* `--dotmatrix-glow`
157
+ (phosphor bloom on lit cells), `--dotmatrix-pulse-min` (the `--pulse` floor),
158
+ `--dotmatrix-reveal-step` (scan cadence); density stays on
159
+ `--dotmatrix-cols/-gap/-dot`. All default to a no-op, so the default render
160
+ is byte-for-byte unchanged; reduced-motion still kills the animations.
161
+ 4. **Optional display colorways** *(done — `tokens/skins.js` →
162
+ `css/skins.css`, exported as `./css/skins.css`, NOT in the default bundle)*:
163
+ `data-bronto-skin="amber-crt | phosphor-green | e-ink"`. **A root-level
164
+ choice, like `data-theme`** (apply on `:root`/`<html>`) — it must live there
165
+ because the accent's derived family (`--accent-strong/-text/-soft`,
166
+ `--bg-accent`, `--field-dot-accent`, `--accent-1..6`) is `color-mix(…
167
+ var(--accent) …)` declared on `:root` and only re-evaluates against the new
168
+ accent on the element that carries it; a skin on a subtree would leave that
169
+ family stale, so the selectors are `:root`-anchored and a subtree skin
170
+ no-ops. Each skin re-points only `--accent` (+ a dark `--dotmatrix-glow`);
171
+ the family + dot-matrix + glyphs follow automatically. Single-hue per skin →
172
+ keeps the one-accent discipline while unlocking range across skins. No
173
+ multi-hue palette. (A future per-subtree skin would need to re-declare the
174
+ derived family — deferred; whole-page is the natural colorway scope.) Skins
175
+ inherit the existing `forced-colors` + `print` behavior in `css/base.css` /
176
+ `css/forms.css` (they touch only the accent, never the canvas).
177
+ 5. **OKLCH for new work first.** *(done for colorways — accents authored in
178
+ OKLCH in `tokens/skins.js`; the contrast tooling now parses `oklch()` →
179
+ sRGB, so skin accents are gated, not eyeballed.)* The **core** `--accent-1..6`
180
+ ramp stays sRGB (decision below). For any future OKLCH in contractual token
181
+ files (the core ramp), still upgrade `gen-resolved` first and add a
182
+ gamut-aware fallback (`@supports`, sRGB-first), and consider `light-dark()`
183
+ to collapse the twice-written dark palette — both are in the same CSS
184
+ Color 5 / Baseline-2026 bracket.
185
+ 6. **APCA advisory reporting.** *(done — `scripts/gen-contrast.mjs`)* APCA-W3
186
+ 0.1.9 `Lc` is computed beside the WCAG ratio for every pairing (core + skins)
187
+ in `docs/contrast.md`. **Advisory only** — WCAG 2.1 AA stays the hard gate.
188
+ 7. **Categorical chart module.** *(done in 0.4.0 — real demand arrived
189
+ (dashboards); `tokens/charts.js` → `css/dataviz.css` + `tokens/charts.json`
190
+ + `tokens/charts.d.ts`, gated by `check:charts`.)* Hybrid accent-led
191
+ (series 1 = `var(--accent)`) + Okabe-Ito colourblind-safe series 2–8 + a
192
+ sequential and a diverging ramp; **dot-matrix `--chart-pattern-*` fills** as
193
+ the mandated second channel. Gated for distinguishability under normal +
194
+ simulated protan/deutan/tritan vision (OKLab ΔE), `var(--chart-*)` forbidden
195
+ in core component CSS (charts-only), never in the default bundle. The
196
+ categorical fills are authored as the Okabe-Ito sRGB hex **verbatim** (a
197
+ CVD-proven *set* — re-deriving per-theme in OKLCH broke its colourblind
198
+ safety, which the gate caught); the ramps are OKLCH. A chart colour's WCAG
199
+ ratio vs the background is reported **advisory** (a fill is not body text;
200
+ the pattern + ΔE gate carry distinguishability).
201
+ 8. **OKLCH `--accent-1..6` ramp migration.** *(done in 0.4.0)* Steps 1–4 mix
202
+ the accent toward `--bg` `in oklch` (perceptually even). `gen-resolved` was
203
+ upgraded first to resolve `color-mix(in oklch,…)` → hex (bit-for-bit matching
204
+ the browser, incl. powerless-hue for near-neutral endpoints), so the
205
+ contractual `resolved.json`/DTCG never drop the tokens. Verified no rendered
206
+ change: `--accent-1..6` is consumed by **no** component (only JS/chart
207
+ consumers + the theme playground), so no visual baseline moved — the feared
208
+ "breaking + needs baseline review" turned out to touch only token *values*
209
+ (non-contractual under 0.x).
210
+
211
+ ## Risks & mitigations
212
+
213
+ | Risk | Mitigation |
214
+ | --- | --- |
215
+ | **Brand dilution** (secondary accents, decorative hues creep in) | Rule 4/5; `check:color-policy`; off-by-default |
216
+ | **Compat break** from OKLCH ramp swap | Freeze above; OKLCH for *new* work first; minor/RC only, never silent |
217
+ | **Tooling drift** (resolver/DTCG/contrast/baseline assume sRGB) | Upgrade tooling *before* OKLCH enters token files; add computed-style tests |
218
+ | **Chart color leakage** into UI chrome | `check:color-policy` forbids chart tokens in core CSS; docs forbid it |
219
+ | **Motion/status misuse** carrying sole meaning | Rule 6; reduced-motion equivalence; status stays locked, never skin-derived |
220
+ | **APCA overreach** | Advisory only; WCAG 2.2 AA stays the hard gate while WCAG 3 is draft |
221
+
222
+ ## Verification plan
223
+
224
+ - Existing `npm run check`, `npm test`, token/DTCG/resolved/dist/visual/a11y
225
+ gates stay green and **unchanged** for the default build.
226
+ - New `check:color-policy` (raw color, status misuse, chart leakage, untiered
227
+ hue).
228
+ - Contrast matrices extended to: default light/dark, `prefers-contrast`,
229
+ `forced-colors`, **every shipped skin**, and chart tokens if shipped.
230
+ - APCA output as non-blocking generated docs.
231
+ - Playwright computed-style tests for `oklch()` / `color-mix(in oklch,…)`
232
+ before any core reliance.
233
+ - Verify `prefers-reduced-motion`, `prefers-contrast`, `forced-colors`, and
234
+ native `accent-color` for every skin.
235
+ - Confirm optional skin/chart CSS is exported + packed but **excluded** from
236
+ default `dist/bronto.css`.
237
+
238
+ ## Decisions taken (0.4.0)
239
+
240
+ Resolved while implementing steps 3–6 (the open questions the earlier draft
241
+ flagged):
242
+
243
+ - **`--accent-1..6` is semver-stable and stays sRGB.** No silent ramp change.
244
+ → step 8 (default-ramp OKLCH migration) **deferred** — visual-diff review
245
+ cost outweighs the gain; reconsider as an opt-in engine / explicit minor.
246
+ - **Colorways ship as a supported package export** (`./css/skins.css`),
247
+ opt-in, excluded from the default bundle. Not docs-only recipes.
248
+ - **Colorways are root-level** (`data-bronto-skin` on `:root`, like
249
+ `data-theme`) — see step 4 for why per-subtree is deferred.
250
+ - **OKLCH is within the existing browser floor** (Chrome 111+/Safari 16.4+/
251
+ Firefox 121+ all support `oklch()`); used for skin accents now. The contrast
252
+ tooling parses it so skins are gated.
253
+ - **Build tooling stays dependency-free.** APCA + the OKLCH→sRGB conversion are
254
+ hand-rolled in `scripts/gen-contrast.mjs` (zero new deps), matching the
255
+ zero-dependency stance.
256
+ - **Data-viz categorical color (step 7) shipped in 0.4.0** — real demand
257
+ arrived (dashboards). Built as a governed Tier-4 module (opt-in, charts-only,
258
+ leakage-gated, CVD-distinguishability-gated). `--cat/--data-*` remain reserved
259
+ alternative-name guards; `--chart-*` is now the live data-viz namespace.
260
+ - **No second UI accent / "duotone" stays Tier-3 ornament only** (rule 5).
261
+
262
+ ## Method
263
+
264
+ Grounded in web research (OKLCH/relative-color/`color-mix` baseline; APCA vs
265
+ WCAG; Radix 12-step; Material 3 HCT; Adobe Leonardo; colorblind-safe
266
+ categorical limits) and a 14-leg AgentMix `deep` multi-POV pass
267
+ (Pi/MiniMax, Aider, OpenCode, Crush, Codex + architecture / implementation-risk
268
+ / testing / security / operations / contrarian legs). Key reconciled dissent:
269
+ several legs wanted an immediate OKLCH ramp swap — rejected as a silent break
270
+ (see freeze); the claim that `--accent-1..6` already serves as a categorical
271
+ scale was rejected (a same-hue ramp encodes sequence/intensity, not independent
272
+ categories); deriving status from skins was rejected (status stays locked).
package/docs/contrast.md CHANGED
@@ -38,63 +38,182 @@ Overall: **all contractual pairings meet their floor ✅**.
38
38
 
39
39
  ## Light theme
40
40
 
41
- | Foreground | Background | Role | Held to | Ratio | Verdict |
42
- | --- | --- | --- | --- | --- | --- |
43
- | `--text` | `--bg` | Body text on page background | AA text (4.5:1) | 17.98:1 | ✅ pass |
44
- | `--text` | `--surface` | Body text on a card/panel | AA text (4.5:1) | 19.80:1 | ✅ pass |
45
- | `--text` | `--surface-muted` | Body text on a muted panel | AA text (4.5:1) | 16.74:1 | ✅ pass |
46
- | `--text-soft` | `--bg` | Secondary text on page background | AA text (4.5:1) | 11.16:1 | ✅ pass |
47
- | `--text-soft` | `--surface` | Secondary text on a card | AA text (4.5:1) | 12.29:1 | ✅ pass |
48
- | `--text-soft` | `--surface-muted` | Secondary text on a muted panel | AA text (4.5:1) | 10.39:1 | ✅ pass |
49
- | `--text-dim` | `--bg` | Dim/meta text on page background | AA text (4.5:1) | 5.09:1 | ✅ pass |
50
- | `--text-dim` | `--surface` | Dim/meta text on a card | AA text (4.5:1) | 5.60:1 | ✅ pass |
51
- | `--text-dim` | `--surface-muted` | Dim/meta text on a muted panel | AA text (4.5:1) | 4.74:1 | ✅ pass |
52
- | `--accent-text` | `--bg` | Accent text on page background | AA text (4.5:1) | 6.32:1 | ✅ pass |
53
- | `--accent-text` | `--surface` | Accent text on a card | AA text (4.5:1) | 6.96:1 | ✅ pass |
54
- | `--button-text` | `--accent` | Label on the primary button | AA text (4.5:1) | 5.18:1 | ✅ pass |
55
- | `--focus-ring` | `--bg` | Focus ring vs page background | UI / large (3:1) | 4.71:1 | ✅ pass |
56
- | `--focus-ring` | `--surface` | Focus ring vs a card | UI / large (3:1) | 5.18:1 | ✅ pass |
57
- | `--accent` | `--bg` | Accent fill vs page background | UI / large (3:1) | 4.71:1 | ✅ pass |
58
- | `--success` | `--surface` | Success indicator vs a card | UI / large (3:1) | 5.04:1 | ✅ pass |
59
- | `--warning` | `--surface` | Warning indicator vs a card | UI / large (3:1) | 5.60:1 | ✅ pass |
60
- | `--danger` | `--surface` | Danger indicator vs a card | UI / large (3:1) | 6.21:1 | ✅ pass |
61
- | `--info` | `--surface` | Info indicator vs a card | UI / large (3:1) | 5.77:1 | ✅ pass |
62
- | `--line-strong` | `--surface` | Strong hairline vs a card | Decorative (1.4.11-exempt) | 2.39:1 | ℹ️ not gated |
41
+ | Foreground | Background | Role | Held to | Ratio | APCA _(advisory)_ | Verdict |
42
+ | --- | --- | --- | --- | --- | --- | --- |
43
+ | `--text` | `--bg` | Body text on page background | AA text (4.5:1) | 17.98:1 | Lc 99 | ✅ pass |
44
+ | `--text` | `--surface` | Body text on a card/panel | AA text (4.5:1) | 19.80:1 | Lc 106 | ✅ pass |
45
+ | `--text` | `--surface-muted` | Body text on a muted panel | AA text (4.5:1) | 16.74:1 | Lc 94 | ✅ pass |
46
+ | `--text-soft` | `--bg` | Secondary text on page background | AA text (4.5:1) | 11.16:1 | Lc 91 | ✅ pass |
47
+ | `--text-soft` | `--surface` | Secondary text on a card | AA text (4.5:1) | 12.29:1 | Lc 98 | ✅ pass |
48
+ | `--text-soft` | `--surface-muted` | Secondary text on a muted panel | AA text (4.5:1) | 10.39:1 | Lc 87 | ✅ pass |
49
+ | `--text-dim` | `--bg` | Dim/meta text on page background | AA text (4.5:1) | 5.09:1 | Lc 71 | ✅ pass |
50
+ | `--text-dim` | `--surface` | Dim/meta text on a card | AA text (4.5:1) | 5.60:1 | Lc 78 | ✅ pass |
51
+ | `--text-dim` | `--surface-muted` | Dim/meta text on a muted panel | AA text (4.5:1) | 4.74:1 | Lc 67 | ✅ pass |
52
+ | `--accent-text` | `--bg` | Accent text on page background | AA text (4.5:1) | 6.32:1 | Lc 75 | ✅ pass |
53
+ | `--accent-text` | `--surface` | Accent text on a card | AA text (4.5:1) | 6.96:1 | Lc 82 | ✅ pass |
54
+ | `--button-text` | `--accent` | Label on the primary button | AA text (4.5:1) | 5.18:1 | Lc 79 | ✅ pass |
55
+ | `--focus-ring` | `--bg` | Focus ring vs page background | UI / large (3:1) | 4.71:1 | Lc 67 | ✅ pass |
56
+ | `--focus-ring` | `--surface` | Focus ring vs a card | UI / large (3:1) | 5.18:1 | Lc 73 | ✅ pass |
57
+ | `--accent` | `--bg` | Accent fill vs page background | UI / large (3:1) | 4.71:1 | Lc 67 | ✅ pass |
58
+ | `--success` | `--surface` | Success indicator vs a card | UI / large (3:1) | 5.04:1 | Lc 75 | ✅ pass |
59
+ | `--warning` | `--surface` | Warning indicator vs a card | UI / large (3:1) | 5.60:1 | Lc 78 | ✅ pass |
60
+ | `--danger` | `--surface` | Danger indicator vs a card | UI / large (3:1) | 6.21:1 | Lc 79 | ✅ pass |
61
+ | `--info` | `--surface` | Info indicator vs a card | UI / large (3:1) | 5.77:1 | Lc 78 | ✅ pass |
62
+ | `--line-strong` | `--surface` | Strong hairline vs a card | Decorative (1.4.11-exempt) | 2.39:1 | Lc 47 | ℹ️ not gated |
63
63
 
64
64
  ## Dark theme
65
65
 
66
- | Foreground | Background | Role | Held to | Ratio | Verdict |
67
- | --- | --- | --- | --- | --- | --- |
68
- | `--text` | `--bg` | Body text on page background | AA text (4.5:1) | 18.76:1 | ✅ pass |
69
- | `--text` | `--surface` | Body text on a card/panel | AA text (4.5:1) | 17.47:1 | ✅ pass |
70
- | `--text` | `--surface-muted` | Body text on a muted panel | AA text (4.5:1) | 15.55:1 | ✅ pass |
71
- | `--text-soft` | `--bg` | Secondary text on page background | AA text (4.5:1) | 12.04:1 | ✅ pass |
72
- | `--text-soft` | `--surface` | Secondary text on a card | AA text (4.5:1) | 11.22:1 | ✅ pass |
73
- | `--text-soft` | `--surface-muted` | Secondary text on a muted panel | AA text (4.5:1) | 9.98:1 | ✅ pass |
74
- | `--text-dim` | `--bg` | Dim/meta text on page background | AA text (4.5:1) | 5.69:1 | ✅ pass |
75
- | `--text-dim` | `--surface` | Dim/meta text on a card | AA text (4.5:1) | 5.30:1 | ✅ pass |
76
- | `--text-dim` | `--surface-muted` | Dim/meta text on a muted panel | AA text (4.5:1) | 4.72:1 | ✅ pass |
77
- | `--accent-text` | `--bg` | Accent text on page background | AA text (4.5:1) | 6.88:1 | ✅ pass |
78
- | `--accent-text` | `--surface` | Accent text on a card | AA text (4.5:1) | 6.41:1 | ✅ pass |
79
- | `--button-text` | `--accent` | Label on the primary button | AA text (4.5:1) | 5.95:1 | ✅ pass |
80
- | `--focus-ring` | `--bg` | Focus ring vs page background | UI / large (3:1) | 5.95:1 | ✅ pass |
81
- | `--focus-ring` | `--surface` | Focus ring vs a card | UI / large (3:1) | 5.55:1 | ✅ pass |
82
- | `--accent` | `--bg` | Accent fill vs page background | UI / large (3:1) | 5.95:1 | ✅ pass |
83
- | `--success` | `--surface` | Success indicator vs a card | UI / large (3:1) | 8.70:1 | ✅ pass |
84
- | `--warning` | `--surface` | Warning indicator vs a card | UI / large (3:1) | 10.66:1 | ✅ pass |
85
- | `--danger` | `--surface` | Danger indicator vs a card | UI / large (3:1) | 6.00:1 | ✅ pass |
86
- | `--info` | `--surface` | Info indicator vs a card | UI / large (3:1) | 8.41:1 | ✅ pass |
87
- | `--line-strong` | `--surface` | Strong hairline vs a card | Decorative (1.4.11-exempt) | 2.01:1 | ℹ️ not gated |
66
+ | Foreground | Background | Role | Held to | Ratio | APCA _(advisory)_ | Verdict |
67
+ | --- | --- | --- | --- | --- | --- | --- |
68
+ | `--text` | `--bg` | Body text on page background | AA text (4.5:1) | 18.76:1 | Lc 99 | ✅ pass |
69
+ | `--text` | `--surface` | Body text on a card/panel | AA text (4.5:1) | 17.47:1 | Lc 99 | ✅ pass |
70
+ | `--text` | `--surface-muted` | Body text on a muted panel | AA text (4.5:1) | 15.55:1 | Lc 98 | ✅ pass |
71
+ | `--text-soft` | `--bg` | Secondary text on page background | AA text (4.5:1) | 12.04:1 | Lc 71 | ✅ pass |
72
+ | `--text-soft` | `--surface` | Secondary text on a card | AA text (4.5:1) | 11.22:1 | Lc 71 | ✅ pass |
73
+ | `--text-soft` | `--surface-muted` | Secondary text on a muted panel | AA text (4.5:1) | 9.98:1 | Lc 70 | ✅ pass |
74
+ | `--text-dim` | `--bg` | Dim/meta text on page background | AA text (4.5:1) | 5.69:1 | Lc 37 | ✅ pass |
75
+ | `--text-dim` | `--surface` | Dim/meta text on a card | AA text (4.5:1) | 5.30:1 | Lc 37 | ✅ pass |
76
+ | `--text-dim` | `--surface-muted` | Dim/meta text on a muted panel | AA text (4.5:1) | 4.72:1 | Lc 36 | ✅ pass |
77
+ | `--accent-text` | `--bg` | Accent text on page background | AA text (4.5:1) | 6.88:1 | Lc 45 | ✅ pass |
78
+ | `--accent-text` | `--surface` | Accent text on a card | AA text (4.5:1) | 6.41:1 | Lc 45 | ✅ pass |
79
+ | `--button-text` | `--accent` | Label on the primary button | AA text (4.5:1) | 5.95:1 | Lc 43 | ✅ pass |
80
+ | `--focus-ring` | `--bg` | Focus ring vs page background | UI / large (3:1) | 5.95:1 | Lc 41 | ✅ pass |
81
+ | `--focus-ring` | `--surface` | Focus ring vs a card | UI / large (3:1) | 5.55:1 | Lc 40 | ✅ pass |
82
+ | `--accent` | `--bg` | Accent fill vs page background | UI / large (3:1) | 5.95:1 | Lc 41 | ✅ pass |
83
+ | `--success` | `--surface` | Success indicator vs a card | UI / large (3:1) | 8.70:1 | Lc 58 | ✅ pass |
84
+ | `--warning` | `--surface` | Warning indicator vs a card | UI / large (3:1) | 10.66:1 | Lc 68 | ✅ pass |
85
+ | `--danger` | `--surface` | Danger indicator vs a card | UI / large (3:1) | 6.00:1 | Lc 43 | ✅ pass |
86
+ | `--info` | `--surface` | Info indicator vs a card | UI / large (3:1) | 8.41:1 | Lc 56 | ✅ pass |
87
+ | `--line-strong` | `--surface` | Strong hairline vs a card | Decorative (1.4.11-exempt) | 2.01:1 | Lc 10 | ℹ️ not gated |
88
+
89
+ ## Display colorways (skins)
90
+
91
+ The opt-in `data-bronto-skin` colorways (`@ponchia/ui/css/skins.css`,
92
+ authored in `tokens/skins.js`) re-point the one accent to a different single
93
+ hue. Because a shipped colorway is part of the framework — not an arbitrary
94
+ consumer re-brand — **its accent is gated to the same floors as the core**:
95
+ the primary-button label, accent-as-text, and the focus ring/accent fill. Only
96
+ the accent-touching pairings are shown (a skin leaves the canvas and status
97
+ palette untouched). Accents are authored in OKLCH; `--accent-text` is the
98
+ `color-mix`-derived `--accent-strong`, exactly as in the core palette.
99
+
100
+ ### Amber CRT — light
101
+
102
+ | Foreground | Background | Role | Held to | Ratio | APCA _(advisory)_ | Verdict |
103
+ | --- | --- | --- | --- | --- | --- | --- |
104
+ | `--accent-text` | `--bg` | Accent text on page background | AA text (4.5:1) | 6.75:1 | Lc 79 | ✅ pass |
105
+ | `--accent-text` | `--surface` | Accent text on a card | AA text (4.5:1) | 7.44:1 | Lc 86 | ✅ pass |
106
+ | `--button-text` | `--accent` | Label on the primary button | AA text (4.5:1) | 5.66:1 | Lc 83 | ✅ pass |
107
+ | `--focus-ring` | `--bg` | Focus ring vs page background | UI / large (3:1) | 5.14:1 | Lc 71 | ✅ pass |
108
+ | `--focus-ring` | `--surface` | Focus ring vs a card | UI / large (3:1) | 5.66:1 | Lc 78 | ✅ pass |
109
+ | `--accent` | `--bg` | Accent fill vs page background | UI / large (3:1) | 5.14:1 | Lc 71 | ✅ pass |
110
+
111
+ ### Amber CRT — dark
112
+
113
+ | Foreground | Background | Role | Held to | Ratio | APCA _(advisory)_ | Verdict |
114
+ | --- | --- | --- | --- | --- | --- | --- |
115
+ | `--accent-text` | `--bg` | Accent text on page background | AA text (4.5:1) | 12.97:1 | Lc 75 | ✅ pass |
116
+ | `--accent-text` | `--surface` | Accent text on a card | AA text (4.5:1) | 12.08:1 | Lc 75 | ✅ pass |
117
+ | `--button-text` | `--accent` | Label on the primary button | AA text (4.5:1) | 11.88:1 | Lc 72 | ✅ pass |
118
+ | `--focus-ring` | `--bg` | Focus ring vs page background | UI / large (3:1) | 11.88:1 | Lc 70 | ✅ pass |
119
+ | `--focus-ring` | `--surface` | Focus ring vs a card | UI / large (3:1) | 11.07:1 | Lc 70 | ✅ pass |
120
+ | `--accent` | `--bg` | Accent fill vs page background | UI / large (3:1) | 11.88:1 | Lc 70 | ✅ pass |
121
+
122
+ ### E-ink — light
123
+
124
+ | Foreground | Background | Role | Held to | Ratio | APCA _(advisory)_ | Verdict |
125
+ | --- | --- | --- | --- | --- | --- | --- |
126
+ | `--accent-text` | `--bg` | Accent text on page background | AA text (4.5:1) | 12.23:1 | Lc 93 | ✅ pass |
127
+ | `--accent-text` | `--surface` | Accent text on a card | AA text (4.5:1) | 13.47:1 | Lc 100 | ✅ pass |
128
+ | `--button-text` | `--accent` | Label on the primary button | AA text (4.5:1) | 11.74:1 | Lc 101 | ✅ pass |
129
+ | `--focus-ring` | `--bg` | Focus ring vs page background | UI / large (3:1) | 10.66:1 | Lc 90 | ✅ pass |
130
+ | `--focus-ring` | `--surface` | Focus ring vs a card | UI / large (3:1) | 11.74:1 | Lc 97 | ✅ pass |
131
+ | `--accent` | `--bg` | Accent fill vs page background | UI / large (3:1) | 10.66:1 | Lc 90 | ✅ pass |
132
+
133
+ ### E-ink — dark
134
+
135
+ | Foreground | Background | Role | Held to | Ratio | APCA _(advisory)_ | Verdict |
136
+ | --- | --- | --- | --- | --- | --- | --- |
137
+ | `--accent-text` | `--bg` | Accent text on page background | AA text (4.5:1) | 13.98:1 | Lc 80 | ✅ pass |
138
+ | `--accent-text` | `--surface` | Accent text on a card | AA text (4.5:1) | 13.02:1 | Lc 79 | ✅ pass |
139
+ | `--button-text` | `--accent` | Label on the primary button | AA text (4.5:1) | 12.86:1 | Lc 76 | ✅ pass |
140
+ | `--focus-ring` | `--bg` | Focus ring vs page background | UI / large (3:1) | 12.86:1 | Lc 75 | ✅ pass |
141
+ | `--focus-ring` | `--surface` | Focus ring vs a card | UI / large (3:1) | 11.98:1 | Lc 74 | ✅ pass |
142
+ | `--accent` | `--bg` | Accent fill vs page background | UI / large (3:1) | 12.86:1 | Lc 75 | ✅ pass |
143
+
144
+ ### Phosphor Green — light
145
+
146
+ | Foreground | Background | Role | Held to | Ratio | APCA _(advisory)_ | Verdict |
147
+ | --- | --- | --- | --- | --- | --- | --- |
148
+ | `--accent-text` | `--bg` | Accent text on page background | AA text (4.5:1) | 6.22:1 | Lc 77 | ✅ pass |
149
+ | `--accent-text` | `--surface` | Accent text on a card | AA text (4.5:1) | 6.85:1 | Lc 83 | ✅ pass |
150
+ | `--button-text` | `--accent` | Label on the primary button | AA text (4.5:1) | 5.19:1 | Lc 81 | ✅ pass |
151
+ | `--focus-ring` | `--bg` | Focus ring vs page background | UI / large (3:1) | 4.71:1 | Lc 69 | ✅ pass |
152
+ | `--focus-ring` | `--surface` | Focus ring vs a card | UI / large (3:1) | 5.19:1 | Lc 75 | ✅ pass |
153
+ | `--accent` | `--bg` | Accent fill vs page background | UI / large (3:1) | 4.71:1 | Lc 69 | ✅ pass |
154
+
155
+ ### Phosphor Green — dark
156
+
157
+ | Foreground | Background | Role | Held to | Ratio | APCA _(advisory)_ | Verdict |
158
+ | --- | --- | --- | --- | --- | --- | --- |
159
+ | `--accent-text` | `--bg` | Accent text on page background | AA text (4.5:1) | 14.54:1 | Lc 82 | ✅ pass |
160
+ | `--accent-text` | `--surface` | Accent text on a card | AA text (4.5:1) | 13.55:1 | Lc 82 | ✅ pass |
161
+ | `--button-text` | `--accent` | Label on the primary button | AA text (4.5:1) | 13.75:1 | Lc 80 | ✅ pass |
162
+ | `--focus-ring` | `--bg` | Focus ring vs page background | UI / large (3:1) | 13.75:1 | Lc 79 | ✅ pass |
163
+ | `--focus-ring` | `--surface` | Focus ring vs a card | UI / large (3:1) | 12.81:1 | Lc 79 | ✅ pass |
164
+ | `--accent` | `--bg` | Accent fill vs page background | UI / large (3:1) | 13.75:1 | Lc 79 | ✅ pass |
165
+
166
+ ## Data-viz palette (advisory)
167
+
168
+ The opt-in Tier-4 chart palette (`@ponchia/ui/css/dataviz.css`, authored in
169
+ `tokens/charts.js`) is gated differently: categorical series are held to
170
+ **mutual distinguishability under normal + simulated protan/deutan/tritan
171
+ vision** (`check:charts`, OKLab ΔE), and colour is **never the sole signal** —
172
+ each series ships a matching `--chart-pattern-*` dot-matrix fill. So the
173
+ WCAG ratios below are **advisory** (a chart fill is not body text); use them to
174
+ pick a darker series for thin lines/points, or rely on the pattern. Series 1 is
175
+ the brand accent.
176
+
177
+ ### Light theme — categorical vs `--bg`
178
+
179
+ | Series | Colour | Ratio _(advisory)_ | APCA _(advisory)_ |
180
+ | --- | --- | --- | --- |
181
+ | 1 _(accent)_ | `#d71921` | 4.71:1 | Lc 67 |
182
+ | 2 | `#e69f00` | 2.05:1 | Lc 37 |
183
+ | 3 | `#56b4e9` | 2.10:1 | Lc 39 |
184
+ | 4 | `#009e73` | 3.11:1 | Lc 54 |
185
+ | 5 | `#f0e442` | 1.20:1 | Lc 9 |
186
+ | 6 | `#0072b2` | 4.71:1 | Lc 68 |
187
+ | 7 | `#cc79a7` | 2.78:1 | Lc 51 |
188
+ | 8 | `#4d5358` | 7.08:1 | Lc 80 |
189
+
190
+ ### Dark theme — categorical vs `--bg`
191
+
192
+ | Series | Colour | Ratio _(advisory)_ | APCA _(advisory)_ |
193
+ | --- | --- | --- | --- |
194
+ | 1 _(accent)_ | `#ff3b41` | 5.95:1 | Lc 41 |
195
+ | 2 | `#e69f00` | 9.32:1 | Lc 58 |
196
+ | 3 | `#56b4e9` | 9.10:1 | Lc 57 |
197
+ | 4 | `#009e73` | 6.14:1 | Lc 41 |
198
+ | 5 | `#f0e442` | 15.88:1 | Lc 88 |
199
+ | 6 | `#0072b2` | 4.05:1 | Lc 27 |
200
+ | 7 | `#cc79a7` | 6.86:1 | Lc 45 |
201
+ | 8 | `#4d5358` | 2.69:1 | Lc 15 |
88
202
 
89
203
  ## Scope & caveats
90
204
 
91
- - This gates the **framework token contract**, not arbitrary consumer
92
- re-brands. Re-accenting via `--accent` is your contrast obligation
93
- (see [theming.md](theming.md) → "Re-brand obligations"); this table is
94
- the guarantee for the *shipped* palettes only.
205
+ - This gates the **framework token contract** (core palette **and** every
206
+ shipped colorway), not arbitrary consumer re-brands. Re-accenting via
207
+ `--accent` yourself is your contrast obligation (see [theming.md](theming.md)
208
+ → "Re-brand obligations"); this table is the guarantee for the *shipped*
209
+ palettes and skins only.
95
210
  - Status colours (`--success` / `--warning` / `--danger`) are gated
96
211
  as **indicators** (3:1). If you set status text *as body copy* on a
97
212
  surface, verify 4.5:1 yourself or pair it with an icon/label.
98
- - Ratios are sRGB WCAG 2.1. The newer APCA model is intentionally not
99
- used WCAG 2.1 AA is the testable legal/again-axe baseline the e2e
100
- suite already asserts.
213
+ - The gated **Ratio** is sRGB WCAG 2.1 AA the testable legal / axe-compatible
214
+ baseline the e2e suite also asserts. The **APCA** column (APCA-W3 0.1.9 `Lc`)
215
+ is **advisory only**: a perceptual cross-check that, unlike WCAG 2.1's
216
+ symmetric ratio, accounts for polarity and is the WCAG 3 candidate. It does
217
+ **not** gate the build while WCAG 3 is a Working Draft — use it to tune
218
+ quality, not to override the WCAG 2.1 floor. As a rough read, body text wants
219
+ `Lc` ≥ 75 and non-text/large ≥ 45–60.
package/docs/reference.md CHANGED
@@ -9,7 +9,7 @@ rendering of every class is the kitchen-sink demo:
9
9
  **<https://ponchia.github.io/bronto-ui/>**. Theming knobs and the token
10
10
  contract: [docs/theming.md](theming.md).
11
11
 
12
- - 262 classes across 109 component groups
12
+ - 263 classes across 110 component groups
13
13
  - Import the typed registry: `import { cls, ui, cx } from '@ponchia/ui/classes'`
14
14
  - Tokens as data: `import { cssVars, tokens, themeColor } from '@ponchia/ui/tokens'`
15
15
 
@@ -393,6 +393,12 @@ each one matches a real selector in the stylesheet.
393
393
  | `cls.hint` | `ui-hint` | base |
394
394
  | `cls.hintError` | `ui-hint--error` | modifier |
395
395
 
396
+ ### `.ui-icon`
397
+
398
+ | Registry key | Class | Kind |
399
+ | --- | --- | --- |
400
+ | `cls.icon` | `ui-icon` | base |
401
+
396
402
  ### `.ui-input`
397
403
 
398
404
  | Registry key | Class | Kind |
@@ -913,10 +919,10 @@ Exact mirror of the `:root` blocks in `css/tokens.css`
913
919
  | `--z-overlay` | `30` |
914
920
  | `--z-popover` | `50` |
915
921
  | `--z-toast` | `60` |
916
- | `--accent-1` | `color-mix(in srgb, var(--accent) 8%, var(--bg))` |
917
- | `--accent-2` | `color-mix(in srgb, var(--accent) 16%, var(--bg))` |
918
- | `--accent-3` | `color-mix(in srgb, var(--accent) 32%, var(--bg))` |
919
- | `--accent-4` | `color-mix(in srgb, var(--accent) 60%, var(--bg))` |
922
+ | `--accent-1` | `color-mix(in oklch, var(--accent) 8%, var(--bg))` |
923
+ | `--accent-2` | `color-mix(in oklch, var(--accent) 16%, var(--bg))` |
924
+ | `--accent-3` | `color-mix(in oklch, var(--accent) 32%, var(--bg))` |
925
+ | `--accent-4` | `color-mix(in oklch, var(--accent) 60%, var(--bg))` |
920
926
  | `--accent-5` | `var(--accent)` |
921
927
  | `--accent-6` | `var(--accent-strong)` |
922
928
  | `--surface-1` | `var(--bg)` |
@@ -968,8 +974,6 @@ Exact mirror of the `:root` blocks in `css/tokens.css`
968
974
  | `--success-soft` | `rgb(47, 125, 79, 0.12)` |
969
975
  | `--warning` | `#806414` |
970
976
  | `--warning-soft` | `rgb(128, 100, 20, 0.13)` |
971
- | `--orange` | `#a85f32` |
972
- | `--orange-soft` | `rgb(168, 95, 50, 0.13)` |
973
977
  | `--danger` | `#c01622` |
974
978
  | `--danger-soft` | `rgb(192, 22, 34, 0.1)` |
975
979
  | `--info` | `#1f63c4` |
@@ -1006,8 +1010,6 @@ Exact mirror of the `:root` blocks in `css/tokens.css`
1006
1010
  | `--success-soft` | `rgb(78, 194, 126, 0.14)` |
1007
1011
  | `--warning` | `#d8bd72` |
1008
1012
  | `--warning-soft` | `rgb(216, 189, 114, 0.14)` |
1009
- | `--orange` | `#d08c5b` |
1010
- | `--orange-soft` | `rgb(208, 140, 91, 0.15)` |
1011
1013
  | `--danger` | `#ff4d54` |
1012
1014
  | `--danger-soft` | `rgb(255, 77, 84, 0.15)` |
1013
1015
  | `--info` | `#6fb0e6` |
package/docs/theming.md CHANGED
@@ -141,7 +141,72 @@ coarser-grained handles.
141
141
  these; override one to slot your app's own layers around the
142
142
  framework's without specificity/`z-index` wars.
143
143
 
144
- All four tiers are in the DTCG export and the JS token model.
144
+ All four tiers are in the DTCG export and the JS token model. The full
145
+ five-tier colour model and its rules live in
146
+ [ADR-0001](adr/0001-color-system.md).
147
+
148
+ ## Display colorways (`data-bronto-skin`)
149
+
150
+ Opt-in single-hue colorways, shipped as a **separate** entrypoint (never in
151
+ the default bundle):
152
+
153
+ ```html
154
+ <link rel="stylesheet" href="@ponchia/ui/css/skins.css" />
155
+ <html data-theme="dark" data-bronto-skin="phosphor-green">
156
+
157
+ </html>
158
+ ```
159
+
160
+ `data-bronto-skin="amber-crt | phosphor-green | e-ink"` is a **root-level**
161
+ choice, like `data-theme` — put it on `:root`/`<html>`. It re-points the one
162
+ `--accent` (per theme, authored in OKLCH); the derived family, focus ring,
163
+ dot-matrix and glyphs follow automatically, and status colours + the neutral
164
+ canvas are untouched. A colorway is _not_ a second accent — it swaps the one
165
+ you have, so the one-accent discipline holds.
166
+
167
+ - **Root-level only.** The accent's derived family (`--accent-strong`/`-text`,
168
+ `--field-dot-accent`, `--accent-1..6`, …) is `color-mix(… var(--accent) …)`
169
+ declared on `:root`; it only re-evaluates on the element that carries it. A
170
+ skin on a subtree would leave that family stale, so the selectors are
171
+ `:root`-anchored and a subtree skin simply no-ops.
172
+ - **Phosphor bloom.** The `amber-crt` / `phosphor-green` skins set
173
+ `--dotmatrix-glow` (a Tier-3 display knob, default `0`) in dark, so the
174
+ dot-matrix gains a CRT-style glow. Set it yourself on any `.ui-dotmatrix`
175
+ to tune the bloom.
176
+ - **Contrast-gated.** Every shipped skin accent meets the same WCAG AA / 3:1
177
+ floors as the core palette — see [contrast.md](contrast.md) → "Display
178
+ colorways". (Your _own_ `--accent` re-brand is still your obligation; the
179
+ guarantee covers the shipped palettes and skins.)
180
+
181
+ ## Data-viz palette (`@ponchia/ui/css/dataviz.css`)
182
+
183
+ Opt-in Tier-4 chart colours for dashboards — **charts only, never UI chrome**
184
+ (a build gate fails on `var(--chart-*)` in component CSS), and never in the
185
+ default bundle.
186
+
187
+ ```html
188
+ <link rel="stylesheet" href="@ponchia/ui/css/dataviz.css" />
189
+ ```
190
+
191
+ ```js
192
+ // resolved hex for canvas / SVG / Chart.js etc.
193
+ import charts from '@ponchia/ui/charts.json' with { type: 'json' };
194
+ const series = charts.dark.categorical; // ['#ff3b41', '#e69f00', …] — series 0 = accent
195
+ ```
196
+
197
+ - **Categorical `--chart-1..8`** — hybrid accent-led: series 1 is the live
198
+ `var(--accent)` (your brand leads), series 2–8 are the Okabe-Ito
199
+ colourblind-safe set. The set is **gated for mutual distinguishability under
200
+ normal + simulated protanopia/deuteranopia/tritanopia** (OKLab ΔE).
201
+ - **Sequential `--chart-seq-1..6`** — single-hue light→dark, for
202
+ heatmaps/intensity. **Diverging `--chart-div-1..7`** — blue↔neutral↔orange,
203
+ for ±/gains-losses.
204
+ - **Pattern fills `--chart-pattern-1..8`** — a dot-matrix second channel so
205
+ colour is never the sole signal (WCAG 1.4.1). Pair colour N with pattern N:
206
+ `background: var(--chart-2); background-image: var(--chart-pattern-2); background-size: var(--chart-pattern-size); --chart-pattern-ink: rgb(0 0 0 / .34);`
207
+ - A chart colour's WCAG ratio vs the background is published **advisory** in
208
+ [contrast.md](contrast.md) (a fill is not body text) — pick a darker series
209
+ for thin lines/points, or lean on the pattern.
145
210
 
146
211
  ## Accessibility markup contracts
147
212