@dataimago/tokens 0.1.0-alpha.2 → 0.1.0-alpha.22

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.
package/README.md CHANGED
@@ -5,15 +5,19 @@ Canonical design tokens for the dataimago ethical AI design system.
5
5
  ## What's in the box
6
6
 
7
7
  - `src/*.json` — the hand-authored JSON sources (colors, typography, spacing, effects, etc.).
8
- - `dist/tokens.css` / `dist/tokens.scss` — CSS custom properties emitted from the JSON sources, theme-aware (`[data-bs-theme]` + `prefers-color-scheme`).
8
+ - `dist/tokens.css` / `dist/tokens.scss` — CSS custom properties emitted from the JSON sources, theme-aware (`[data-bs-theme]` + `prefers-color-scheme`). The system preference applies only while the root has no `data-bs-theme="light"`, so an explicit choice (e.g. `@dataimago/ui`'s `ThemeToggle`) wins in both directions.
9
9
  - `dist/index.{mjs,cjs,d.ts}` — typed JS/TS imports for token values.
10
+ - `dist/tokens.dtcg.json` — additive DTCG-style export for tools and agents. It does not replace the current runtime JSON shape.
10
11
 
11
12
  ## Palette
12
13
 
13
14
  - `ink` — warm neutrals, 50–900
14
- - `cream` — warm paper, 50–500
15
- - `copper` — canonical interactive color (primary on light mode at 500, hover at 600)
16
- - `accent` — emancipation copper-red, reserved for high moral weight
15
+ - `stone` — canonical page/surface neutral scale
16
+ - `forest` — house primary interactive scale
17
+ - `paper` — warm-surface option, retained as the renamed successor to the older `cream` framing
18
+ - `presentation-blue` — presentation/accent source scale
19
+ - `accent` — mode-aware semantic accent mapped to presentation-blue stops
20
+ - `copper` — preserved decorative/editorial warmth scale
17
21
 
18
22
  All semantic tokens (`content.primary`, `surface.page`, `interactive.primary`, etc.) resolve to values in that palette.
19
23
 
@@ -25,12 +29,19 @@ import colors from "@dataimago/tokens/colors";
25
29
 
26
30
  // In CSS:
27
31
  // @import "@dataimago/tokens/css";
32
+
33
+ // Tooling / agents:
34
+ // import dtcg from "@dataimago/tokens/dtcg";
28
35
  ```
29
36
 
30
37
  ## Authoring
31
38
 
32
39
  Edit JSON under `src/` — this package is the source of truth. Run `pnpm --filter @dataimago/tokens build` to regenerate `dist/`.
33
40
 
41
+ CSS-emitting files (`colors.json`, `theme-colors.json`, `typography.json`, `spacing.json`, `effects.json`) are structurally validated before build output is emitted. `frequent.json` and `components.json` remain metadata/reference manifests in this alpha line; do not treat them as normalized CSS-emitting token files until a migration records that decision.
42
+
43
+ The current CSS custom property set is protected by `tools/check-css-vars.mjs` so Level-1 and Level-2 consumers do not lose variables during tightening work.
44
+
34
45
  ## Versioning
35
46
 
36
47
  Changes go through [Changesets](https://github.com/changesets/changesets). Any palette or semantic-token change is at least a minor version bump in alpha.
package/dist/index.cjs CHANGED
@@ -40,24 +40,24 @@ const tokens = {
40
40
  },
41
41
  "border": {
42
42
  "subtle": {
43
- "light": "rgba(12, 10, 9, 0.08)",
44
- "dark": "rgba(253, 249, 242, 0.08)",
45
- "description": "Subtle borders — element separation"
43
+ "light": "rgba(12, 10, 9, 0.45)",
44
+ "dark": "rgba(253, 249, 242, 0.45)",
45
+ "description": "Subtle borders — element separation. THE ALPHA IS THE ROLE: border.subtle / moderate / strong / card share one base color and are distinguished by opacity alone, so a gamut conversion that drops the alpha channel collapses four semantic roles into one opaque value (this happened in 17995c3, #72, 2026-06-28). No number is quoted here on purpose: the scale is set by the founder decision recorded in wi-20260817-border-alpha-scale-retune and gated by tools/contrast-check.mjs at the WCAG 1.4.11 non-text threshold (3:1). Read the value, not this sentence."
46
46
  },
47
47
  "moderate": {
48
- "light": "rgba(12, 10, 9, 0.15)",
49
- "dark": "rgba(253, 249, 242, 0.15)",
50
- "description": "Moderate borders — defined boundaries"
48
+ "light": "rgba(12, 10, 9, 0.60)",
49
+ "dark": "rgba(253, 249, 242, 0.60)",
50
+ "description": "Moderate borders — defined boundaries. See border.subtle on why the alpha is load-bearing and why no number is quoted here."
51
51
  },
52
52
  "strong": {
53
- "light": "rgba(12, 10, 9, 0.25)",
54
- "dark": "rgba(253, 249, 242, 0.25)",
55
- "description": "Strong borders — prominent separation"
53
+ "light": "rgba(12, 10, 9, 0.75)",
54
+ "dark": "rgba(253, 249, 242, 0.75)",
55
+ "description": "Strong borders — prominent separation. See border.subtle on why the alpha is load-bearing and why no number is quoted here."
56
56
  },
57
57
  "card": {
58
- "light": "rgba(12, 10, 9, 0.15)",
59
- "dark": "rgba(253, 249, 242, 0.15)",
60
- "description": "Card border color with transparency for subtle definition"
58
+ "light": "rgba(12, 10, 9, 0.60)",
59
+ "dark": "rgba(253, 249, 242, 0.60)",
60
+ "description": "Card border color with transparency for subtle definition. See border.subtle on why the alpha is load-bearing and why no number is quoted here."
61
61
  },
62
62
  "code": {
63
63
  "light": "#d6d3d1",
@@ -72,14 +72,14 @@ const tokens = {
72
72
  "description": "Primary text — stone-900 on stone-100 (light, matches presentation-theme.scss exactly) / paper-100 on dark surface"
73
73
  },
74
74
  "secondary": {
75
- "light": "#7c7c7c",
75
+ "light": "#6a6a6a",
76
76
  "dark": "#a8a29e",
77
- "description": "Secondary text — stone-500 (light, matches presentation-theme.scss --color-secondary) / ink-300 on dark"
77
+ "description": "Secondary text — WCAG-AA on all light surfaces (≥4.6:1, darkened from stone-500 #7c7c7c which was 3.6:1) / ink-300 on dark"
78
78
  },
79
79
  "muted": {
80
- "light": "#78716c",
81
- "dark": "#78716c",
82
- "description": "Muted text — captions, meta info"
80
+ "light": "#706964",
81
+ "dark": "#89827d",
82
+ "description": "Muted text — captions, meta info. WCAG-AA on all surfaces in both modes (darkened/lightened from #78716c which failed AA on page)"
83
83
  },
84
84
  "inverse": {
85
85
  "light": "#fdf9f2",
@@ -99,9 +99,9 @@ const tokens = {
99
99
  "description": "Hover state — forest-800 on light (slightly darker than canonical) / forest-400 on dark"
100
100
  },
101
101
  "accent": {
102
- "light": "#b54a27",
103
- "dark": "#b54a27",
104
- "description": "Accent — emancipation copper-red; moral-weight color (preserved from copper era as deliberate non-interactive emphasis)"
102
+ "light": "#1f618d",
103
+ "dark": "#3498DB",
104
+ "description": "Accent — sky-blue moral-weight color (iteration-2). Light: accent-500 (5.68:1 on stone-100, AA with headroom; 6.66:1 white-on-color for backplates). Dark: accent-300 (5.41:1 on stone-900, AA with headroom — closes the iteration-1 dark-mode AA-large-only contrast gap). FIRST instance of mode-aware role mapping in the system: light and dark point to different palette stops rather than the same hex. The scale-name `accent` is preserved across iteration-1 → iteration-2 (consumer-side text-accent-500 keeps working). See wiki/decisions/iteration-2-blue-accent.md for the dialectical reframe of the iteration-1 emancipation-copper framing (sky/horizon/Enlightenment-blueprint/Reformation-Protestant traditions vs. Western-traffic-coding STOP-red)."
105
105
  },
106
106
  "focus": {
107
107
  "light": "#434739",
@@ -120,6 +120,121 @@ const tokens = {
120
120
  "dark": "#2980B9",
121
121
  "description": "Presentation accent hover/emphasized variant — Flat-UI belize-hole"
122
122
  }
123
+ },
124
+ "vertical-accent": {
125
+ "$comment": "Per-vertical brand accents — each Level-2 property's identity color, distinct from the house interactive.accent (the L1 sky-blue hub color). Mode-aware (light/dark stops) like interactive.accent. Used for app icons, navbar-logo emphasis, and per-vertical theming. Contrast vs surface.page verified WCAG: light values are the on-light marks, dark values the on-dark marks. dataimago (L1 hub) intentionally has no vertical accent — it uses interactive.accent + ink.",
126
+ "dissertation": {
127
+ "light": "#B87333",
128
+ "dark": "#CE8E5C",
129
+ "description": "dissertation-ai — copper (manuscript / annotation / the human hand in the margin). Light: 3.23:1 on stone-100 (AA-large; large-mark use). Dark: #CE8E5C 6.72:1 on ink-900 (AA)."
130
+ },
131
+ "rpkg": {
132
+ "light": "#3B82F6",
133
+ "dark": "#60A5FA",
134
+ "description": "rpkg-ai — engineering blue. Light: 3.14:1 on stone-100 (AA-large; large-mark use). Dark: #60A5FA 7.26:1 on ink-900 (AA)."
135
+ },
136
+ "SGPc": {
137
+ "light": "#5A6310",
138
+ "dark": "#B2BF22",
139
+ "description": "SGPc-ai — growth yellow-green (decided 2026-06-26). The brand value is the bright #B2BF22, but it fails contrast on light surfaces (1.73:1), so LIGHT mode uses the darkened companion #5A6310 (5.55:1 on stone-100 — full AA) and DARK mode uses the bright #B2BF22 (9.12:1 on ink-900). See wiki/decisions/sgpc-accent.md."
140
+ }
141
+ },
142
+ "brand-mark": {
143
+ "$comment": "Brand-MARK colors (the dataimago monogram / wordmark logo treatment) — distinct from the interactive.accent role. The signature monogram red is a wide-gamut PILOT (see wiki/decisions/p3-oklch-color.md). It is mode-aware for AA-normal headroom: a single red cannot clear 4.5:1 on BOTH the light page (#ededeb) and ink-900 — light needs a deep red, dark needs a bright one. LIGHT uses the deepened #cc0000 (faithful P3, contrast-first); DARK keeps the vivid display-P3 primary red (#ff0000 fallback → color(display-p3 1 0 0)), where #ff0000 already passes AA-normal. Vivid override authored in tools/color-converter.mjs (dark-only for the red), emitted as a `#hex → color(display-p3 …)` cascade in the -p3 build.",
144
+ "red": {
145
+ "light": "#cc0000",
146
+ "dark": "#ff0000",
147
+ "description": "Signature monogram red — mode-aware for AA-normal. LIGHT: deepened #cc0000 (5.0:1 on stone-100 #ededeb / 5.5:1 on elevated — full AA-normal, safe at small sizes / as text). DARK: vivid #ff0000 fallback + display-P3 primary red (4.6:1 on ink-900 — AA-normal). Logo-mark color only — NOT the interactive accent (which is iteration-2 blue). Large navbar lift-wash marks keep the vivid display-P3 red (AA-large, large-mark use); this token is the AA-normal value for arbitrary/small mark reuse."
148
+ }
149
+ },
150
+ "dataviz": {
151
+ "okabe-ito": {
152
+ "orange": {
153
+ "value": "#E69F00",
154
+ "description": "Okabe-Ito orange (Okabe & Ito 2008, colour-blind safe). House data-viz primitive; maintainer decision 2026-10-29."
155
+ },
156
+ "sky-blue": {
157
+ "value": "#56B4E9",
158
+ "description": "Okabe-Ito sky blue (Okabe & Ito 2008, colour-blind safe). House data-viz primitive; maintainer decision 2026-10-29."
159
+ },
160
+ "bluish-green": {
161
+ "value": "#009E73",
162
+ "description": "Okabe-Ito bluish green (Okabe & Ito 2008, colour-blind safe). House data-viz primitive; maintainer decision 2026-10-29."
163
+ },
164
+ "yellow": {
165
+ "value": "#F0E442",
166
+ "description": "Okabe-Ito yellow — 1.13:1 on the light page: never use it without a mark outline or a label (Okabe & Ito 2008, colour-blind safe). House data-viz primitive; maintainer decision 2026-10-29."
167
+ },
168
+ "blue": {
169
+ "value": "#0072B2",
170
+ "description": "Okabe-Ito blue (Okabe & Ito 2008, colour-blind safe). House data-viz primitive; maintainer decision 2026-10-29."
171
+ },
172
+ "vermillion": {
173
+ "value": "#D55E00",
174
+ "description": "Okabe-Ito vermillion (Okabe & Ito 2008, colour-blind safe). House data-viz primitive; maintainer decision 2026-10-29."
175
+ },
176
+ "reddish-purple": {
177
+ "value": "#CC79A7",
178
+ "description": "Okabe-Ito reddish purple (Okabe & Ito 2008, colour-blind safe). House data-viz primitive; maintainer decision 2026-10-29."
179
+ },
180
+ "black": {
181
+ "light": "#000000",
182
+ "dark": "#FDF9F2",
183
+ "description": "Okabe-Ito black (Okabe & Ito 2008). In dark mode it becomes content.primary.dark: #000000 is 1.14:1 on the dark page. Dark value approved by the maintainer 2026-10-29 (dataviz-check --propose-dark)."
184
+ }
185
+ },
186
+ "categorical": {
187
+ "1": {
188
+ "value": "#E69F00",
189
+ "description": "= okabe-ito.orange (slot 1, Wilke order). Categorical colour is never the only channel: pair it with marker shape, line style or a label."
190
+ },
191
+ "2": {
192
+ "value": "#56B4E9",
193
+ "description": "= okabe-ito.sky-blue (slot 2, Wilke order). Categorical colour is never the only channel: pair it with marker shape, line style or a label."
194
+ },
195
+ "3": {
196
+ "value": "#009E73",
197
+ "description": "= okabe-ito.bluish-green (slot 3, Wilke order). Categorical colour is never the only channel: pair it with marker shape, line style or a label."
198
+ },
199
+ "4": {
200
+ "value": "#F0E442",
201
+ "description": "= okabe-ito.yellow (slot 4, Wilke order). Categorical colour is never the only channel: pair it with marker shape, line style or a label."
202
+ },
203
+ "5": {
204
+ "value": "#0072B2",
205
+ "description": "= okabe-ito.blue (slot 5, Wilke order). Categorical colour is never the only channel: pair it with marker shape, line style or a label."
206
+ },
207
+ "6": {
208
+ "value": "#D55E00",
209
+ "description": "= okabe-ito.vermillion (slot 6, Wilke order). Categorical colour is never the only channel: pair it with marker shape, line style or a label."
210
+ },
211
+ "7": {
212
+ "value": "#CC79A7",
213
+ "description": "= okabe-ito.reddish-purple (slot 7, Wilke order). Categorical colour is never the only channel: pair it with marker shape, line style or a label."
214
+ },
215
+ "8": {
216
+ "light": "#000000",
217
+ "dark": "#FDF9F2",
218
+ "description": "= okabe-ito.black (slot 8, Wilke order). Categorical colour is never the only channel: pair it with marker shape, line style or a label."
219
+ }
220
+ },
221
+ "uncertainty": {
222
+ "outer": {
223
+ "light": "#D6D6D6",
224
+ "dark": "#333333",
225
+ "description": "Outermost nested uncertainty band (gray 0.84 in the LIwLD PSTricks figures). The ramp is mirrored in dark mode so the innermost band stays the strongest. Dark value approved by the maintainer 2026-10-29 (dataviz-check --propose-dark)."
226
+ },
227
+ "union": {
228
+ "light": "#9E9E9E",
229
+ "dark": "#6B6B6B",
230
+ "description": "Union band (gray 0.62); mirrored in dark mode. Dark value approved by the maintainer 2026-10-29 (dataviz-check --propose-dark)."
231
+ },
232
+ "inner": {
233
+ "light": "#333333",
234
+ "dark": "#D6D6D6",
235
+ "description": "Innermost band (gray 0.20); mirrored in dark mode, where #333333 would be 1.46:1. Dark value approved by the maintainer 2026-10-29 (dataviz-check --propose-dark)."
236
+ }
237
+ }
123
238
  }
124
239
  },
125
240
  "spacing": {
@@ -200,14 +315,14 @@ const tokens = {
200
315
  },
201
316
  "border": {
202
317
  "subtle": {
203
- "light": "rgba(20, 20, 16, 0.1)",
204
- "dark": "rgba(237, 237, 235, 0.1)",
205
- "description": "Subtle borders - element separation"
318
+ "light": "rgba(20, 20, 16, 0.47)",
319
+ "dark": "rgba(237, 237, 235, 0.47)",
320
+ "description": "Subtle borders - element separation. THE ALPHA IS THE ROLE. Distinct from color.border.subtle: this family has its own base (20,20,16 / 237,237,235), so it needs a DIFFERENT alpha to render the same. 0.47 here matches 0.45 there at 3.07:1 on the worst surface; copying 0.45 would land at 2.90:1 and fail WCAG 1.4.11. Gated by tools/contrast-check.mjs."
206
321
  },
207
322
  "moderate": {
208
- "light": "rgba(20, 20, 16, 0.15)",
209
- "dark": "rgba(237, 237, 235, 0.15)",
210
- "description": "Moderate borders - defined boundaries"
323
+ "light": "rgba(20, 20, 16, 0.63)",
324
+ "dark": "rgba(237, 237, 235, 0.63)",
325
+ "description": "Moderate borders - defined boundaries. 0.63 here matches color.border.moderate 0.60 in rendered contrast; see border.subtle on why the numbers differ between the two families."
211
326
  },
212
327
  "focus": {
213
328
  "light": "#3498DB",
@@ -282,16 +397,16 @@ const tokens = {
282
397
  "description": "Primary sans-serif font - body text"
283
398
  },
284
399
  "mono": {
285
- "value": "JetBrains Mono, SF Mono, Monaco, Inconsolata, monospace",
286
- "description": "Monospace font - code blocks and technical content"
400
+ "value": "Noto Sans Mono, SF Mono, Monaco, Inconsolata, monospace",
401
+ "description": "Noto Sans Mono — code blocks and technical content; matches the shipped font"
287
402
  },
288
403
  "display": {
289
404
  "value": "var(--font-family-display-editorial)",
290
- "description": "DEPRECATED alias of display-editorial; preserved through 0.2.x and removed in 0.3.0. New code should reference --font-family-display-editorial or --font-family-display-documentation directly. See wiki/decisions/display-family-split.md."
405
+ "description": "Alias of display-editorial, and the load-bearing indirection .di-prose rebinds to swap a whole surface to the documentation family — do NOT rewrite heading rules to reference display-editorial directly or .di-prose stops working. Reference display-editorial/display-documentation explicitly only where the surface must NOT control the family (brand chrome, navbars). Its 0.3.0 removal was deferred 2026-08-10 under wiki/decisions/compatibility-contract.md (additive-only); removal now requires its own cleanup ADR. See wiki/decisions/display-family-split.md."
291
406
  },
292
407
  "display-editorial": {
293
- "value": "Josefin Sans, system-ui, sans-serif",
294
- "description": "Editorial display family — Josefin Sans, the brand voice. Used on surfaces where the heading does rhetorical/branding work: hero/landing, marketing, story showcases, slide titles. Default for unmarked headings. Pair with weight 500 (h1) per wiki/decisions/h1-weight-medium.md."
408
+ "value": "Josefin Sans, Noto Sans, system-ui, sans-serif",
409
+ "description": "Editorial display family — Josefin Sans, the brand voice. Used on surfaces where the heading does rhetorical/branding work: hero/landing, marketing, story showcases, slide titles. Default for unmarked headings. The Noto Sans fallback serves untagged text; tagged languages are handled by fonts/lang.css. Pair with weight 500 (h1) per wiki/decisions/h1-weight-medium.md."
295
410
  },
296
411
  "display-documentation": {
297
412
  "value": "Noto Sans, system-ui, sans-serif",
@@ -302,8 +417,8 @@ const tokens = {
302
417
  "description": "Brand font - dataimago navbar and branding elements"
303
418
  },
304
419
  "math": {
305
- "value": "Noto Sans Math, Noto Sans, system-ui, sans-serif",
306
- "description": "Mathematical notation font — Noto Sans Math for broad Unicode math coverage; harmonises with the Noto Sans body stack"
420
+ "value": "Noto Sans Math, math",
421
+ "description": "Noto Sans Math, falling back to the browser's maths face; native MathML is the maths contract"
307
422
  }
308
423
  },
309
424
  "size": {
package/dist/index.mjs CHANGED
@@ -40,24 +40,24 @@ const tokens = {
40
40
  },
41
41
  "border": {
42
42
  "subtle": {
43
- "light": "rgba(12, 10, 9, 0.08)",
44
- "dark": "rgba(253, 249, 242, 0.08)",
45
- "description": "Subtle borders — element separation"
43
+ "light": "rgba(12, 10, 9, 0.45)",
44
+ "dark": "rgba(253, 249, 242, 0.45)",
45
+ "description": "Subtle borders — element separation. THE ALPHA IS THE ROLE: border.subtle / moderate / strong / card share one base color and are distinguished by opacity alone, so a gamut conversion that drops the alpha channel collapses four semantic roles into one opaque value (this happened in 17995c3, #72, 2026-06-28). No number is quoted here on purpose: the scale is set by the founder decision recorded in wi-20260817-border-alpha-scale-retune and gated by tools/contrast-check.mjs at the WCAG 1.4.11 non-text threshold (3:1). Read the value, not this sentence."
46
46
  },
47
47
  "moderate": {
48
- "light": "rgba(12, 10, 9, 0.15)",
49
- "dark": "rgba(253, 249, 242, 0.15)",
50
- "description": "Moderate borders — defined boundaries"
48
+ "light": "rgba(12, 10, 9, 0.60)",
49
+ "dark": "rgba(253, 249, 242, 0.60)",
50
+ "description": "Moderate borders — defined boundaries. See border.subtle on why the alpha is load-bearing and why no number is quoted here."
51
51
  },
52
52
  "strong": {
53
- "light": "rgba(12, 10, 9, 0.25)",
54
- "dark": "rgba(253, 249, 242, 0.25)",
55
- "description": "Strong borders — prominent separation"
53
+ "light": "rgba(12, 10, 9, 0.75)",
54
+ "dark": "rgba(253, 249, 242, 0.75)",
55
+ "description": "Strong borders — prominent separation. See border.subtle on why the alpha is load-bearing and why no number is quoted here."
56
56
  },
57
57
  "card": {
58
- "light": "rgba(12, 10, 9, 0.15)",
59
- "dark": "rgba(253, 249, 242, 0.15)",
60
- "description": "Card border color with transparency for subtle definition"
58
+ "light": "rgba(12, 10, 9, 0.60)",
59
+ "dark": "rgba(253, 249, 242, 0.60)",
60
+ "description": "Card border color with transparency for subtle definition. See border.subtle on why the alpha is load-bearing and why no number is quoted here."
61
61
  },
62
62
  "code": {
63
63
  "light": "#d6d3d1",
@@ -72,14 +72,14 @@ const tokens = {
72
72
  "description": "Primary text — stone-900 on stone-100 (light, matches presentation-theme.scss exactly) / paper-100 on dark surface"
73
73
  },
74
74
  "secondary": {
75
- "light": "#7c7c7c",
75
+ "light": "#6a6a6a",
76
76
  "dark": "#a8a29e",
77
- "description": "Secondary text — stone-500 (light, matches presentation-theme.scss --color-secondary) / ink-300 on dark"
77
+ "description": "Secondary text — WCAG-AA on all light surfaces (≥4.6:1, darkened from stone-500 #7c7c7c which was 3.6:1) / ink-300 on dark"
78
78
  },
79
79
  "muted": {
80
- "light": "#78716c",
81
- "dark": "#78716c",
82
- "description": "Muted text — captions, meta info"
80
+ "light": "#706964",
81
+ "dark": "#89827d",
82
+ "description": "Muted text — captions, meta info. WCAG-AA on all surfaces in both modes (darkened/lightened from #78716c which failed AA on page)"
83
83
  },
84
84
  "inverse": {
85
85
  "light": "#fdf9f2",
@@ -99,9 +99,9 @@ const tokens = {
99
99
  "description": "Hover state — forest-800 on light (slightly darker than canonical) / forest-400 on dark"
100
100
  },
101
101
  "accent": {
102
- "light": "#b54a27",
103
- "dark": "#b54a27",
104
- "description": "Accent — emancipation copper-red; moral-weight color (preserved from copper era as deliberate non-interactive emphasis)"
102
+ "light": "#1f618d",
103
+ "dark": "#3498DB",
104
+ "description": "Accent — sky-blue moral-weight color (iteration-2). Light: accent-500 (5.68:1 on stone-100, AA with headroom; 6.66:1 white-on-color for backplates). Dark: accent-300 (5.41:1 on stone-900, AA with headroom — closes the iteration-1 dark-mode AA-large-only contrast gap). FIRST instance of mode-aware role mapping in the system: light and dark point to different palette stops rather than the same hex. The scale-name `accent` is preserved across iteration-1 → iteration-2 (consumer-side text-accent-500 keeps working). See wiki/decisions/iteration-2-blue-accent.md for the dialectical reframe of the iteration-1 emancipation-copper framing (sky/horizon/Enlightenment-blueprint/Reformation-Protestant traditions vs. Western-traffic-coding STOP-red)."
105
105
  },
106
106
  "focus": {
107
107
  "light": "#434739",
@@ -120,6 +120,121 @@ const tokens = {
120
120
  "dark": "#2980B9",
121
121
  "description": "Presentation accent hover/emphasized variant — Flat-UI belize-hole"
122
122
  }
123
+ },
124
+ "vertical-accent": {
125
+ "$comment": "Per-vertical brand accents — each Level-2 property's identity color, distinct from the house interactive.accent (the L1 sky-blue hub color). Mode-aware (light/dark stops) like interactive.accent. Used for app icons, navbar-logo emphasis, and per-vertical theming. Contrast vs surface.page verified WCAG: light values are the on-light marks, dark values the on-dark marks. dataimago (L1 hub) intentionally has no vertical accent — it uses interactive.accent + ink.",
126
+ "dissertation": {
127
+ "light": "#B87333",
128
+ "dark": "#CE8E5C",
129
+ "description": "dissertation-ai — copper (manuscript / annotation / the human hand in the margin). Light: 3.23:1 on stone-100 (AA-large; large-mark use). Dark: #CE8E5C 6.72:1 on ink-900 (AA)."
130
+ },
131
+ "rpkg": {
132
+ "light": "#3B82F6",
133
+ "dark": "#60A5FA",
134
+ "description": "rpkg-ai — engineering blue. Light: 3.14:1 on stone-100 (AA-large; large-mark use). Dark: #60A5FA 7.26:1 on ink-900 (AA)."
135
+ },
136
+ "SGPc": {
137
+ "light": "#5A6310",
138
+ "dark": "#B2BF22",
139
+ "description": "SGPc-ai — growth yellow-green (decided 2026-06-26). The brand value is the bright #B2BF22, but it fails contrast on light surfaces (1.73:1), so LIGHT mode uses the darkened companion #5A6310 (5.55:1 on stone-100 — full AA) and DARK mode uses the bright #B2BF22 (9.12:1 on ink-900). See wiki/decisions/sgpc-accent.md."
140
+ }
141
+ },
142
+ "brand-mark": {
143
+ "$comment": "Brand-MARK colors (the dataimago monogram / wordmark logo treatment) — distinct from the interactive.accent role. The signature monogram red is a wide-gamut PILOT (see wiki/decisions/p3-oklch-color.md). It is mode-aware for AA-normal headroom: a single red cannot clear 4.5:1 on BOTH the light page (#ededeb) and ink-900 — light needs a deep red, dark needs a bright one. LIGHT uses the deepened #cc0000 (faithful P3, contrast-first); DARK keeps the vivid display-P3 primary red (#ff0000 fallback → color(display-p3 1 0 0)), where #ff0000 already passes AA-normal. Vivid override authored in tools/color-converter.mjs (dark-only for the red), emitted as a `#hex → color(display-p3 …)` cascade in the -p3 build.",
144
+ "red": {
145
+ "light": "#cc0000",
146
+ "dark": "#ff0000",
147
+ "description": "Signature monogram red — mode-aware for AA-normal. LIGHT: deepened #cc0000 (5.0:1 on stone-100 #ededeb / 5.5:1 on elevated — full AA-normal, safe at small sizes / as text). DARK: vivid #ff0000 fallback + display-P3 primary red (4.6:1 on ink-900 — AA-normal). Logo-mark color only — NOT the interactive accent (which is iteration-2 blue). Large navbar lift-wash marks keep the vivid display-P3 red (AA-large, large-mark use); this token is the AA-normal value for arbitrary/small mark reuse."
148
+ }
149
+ },
150
+ "dataviz": {
151
+ "okabe-ito": {
152
+ "orange": {
153
+ "value": "#E69F00",
154
+ "description": "Okabe-Ito orange (Okabe & Ito 2008, colour-blind safe). House data-viz primitive; maintainer decision 2026-10-29."
155
+ },
156
+ "sky-blue": {
157
+ "value": "#56B4E9",
158
+ "description": "Okabe-Ito sky blue (Okabe & Ito 2008, colour-blind safe). House data-viz primitive; maintainer decision 2026-10-29."
159
+ },
160
+ "bluish-green": {
161
+ "value": "#009E73",
162
+ "description": "Okabe-Ito bluish green (Okabe & Ito 2008, colour-blind safe). House data-viz primitive; maintainer decision 2026-10-29."
163
+ },
164
+ "yellow": {
165
+ "value": "#F0E442",
166
+ "description": "Okabe-Ito yellow — 1.13:1 on the light page: never use it without a mark outline or a label (Okabe & Ito 2008, colour-blind safe). House data-viz primitive; maintainer decision 2026-10-29."
167
+ },
168
+ "blue": {
169
+ "value": "#0072B2",
170
+ "description": "Okabe-Ito blue (Okabe & Ito 2008, colour-blind safe). House data-viz primitive; maintainer decision 2026-10-29."
171
+ },
172
+ "vermillion": {
173
+ "value": "#D55E00",
174
+ "description": "Okabe-Ito vermillion (Okabe & Ito 2008, colour-blind safe). House data-viz primitive; maintainer decision 2026-10-29."
175
+ },
176
+ "reddish-purple": {
177
+ "value": "#CC79A7",
178
+ "description": "Okabe-Ito reddish purple (Okabe & Ito 2008, colour-blind safe). House data-viz primitive; maintainer decision 2026-10-29."
179
+ },
180
+ "black": {
181
+ "light": "#000000",
182
+ "dark": "#FDF9F2",
183
+ "description": "Okabe-Ito black (Okabe & Ito 2008). In dark mode it becomes content.primary.dark: #000000 is 1.14:1 on the dark page. Dark value approved by the maintainer 2026-10-29 (dataviz-check --propose-dark)."
184
+ }
185
+ },
186
+ "categorical": {
187
+ "1": {
188
+ "value": "#E69F00",
189
+ "description": "= okabe-ito.orange (slot 1, Wilke order). Categorical colour is never the only channel: pair it with marker shape, line style or a label."
190
+ },
191
+ "2": {
192
+ "value": "#56B4E9",
193
+ "description": "= okabe-ito.sky-blue (slot 2, Wilke order). Categorical colour is never the only channel: pair it with marker shape, line style or a label."
194
+ },
195
+ "3": {
196
+ "value": "#009E73",
197
+ "description": "= okabe-ito.bluish-green (slot 3, Wilke order). Categorical colour is never the only channel: pair it with marker shape, line style or a label."
198
+ },
199
+ "4": {
200
+ "value": "#F0E442",
201
+ "description": "= okabe-ito.yellow (slot 4, Wilke order). Categorical colour is never the only channel: pair it with marker shape, line style or a label."
202
+ },
203
+ "5": {
204
+ "value": "#0072B2",
205
+ "description": "= okabe-ito.blue (slot 5, Wilke order). Categorical colour is never the only channel: pair it with marker shape, line style or a label."
206
+ },
207
+ "6": {
208
+ "value": "#D55E00",
209
+ "description": "= okabe-ito.vermillion (slot 6, Wilke order). Categorical colour is never the only channel: pair it with marker shape, line style or a label."
210
+ },
211
+ "7": {
212
+ "value": "#CC79A7",
213
+ "description": "= okabe-ito.reddish-purple (slot 7, Wilke order). Categorical colour is never the only channel: pair it with marker shape, line style or a label."
214
+ },
215
+ "8": {
216
+ "light": "#000000",
217
+ "dark": "#FDF9F2",
218
+ "description": "= okabe-ito.black (slot 8, Wilke order). Categorical colour is never the only channel: pair it with marker shape, line style or a label."
219
+ }
220
+ },
221
+ "uncertainty": {
222
+ "outer": {
223
+ "light": "#D6D6D6",
224
+ "dark": "#333333",
225
+ "description": "Outermost nested uncertainty band (gray 0.84 in the LIwLD PSTricks figures). The ramp is mirrored in dark mode so the innermost band stays the strongest. Dark value approved by the maintainer 2026-10-29 (dataviz-check --propose-dark)."
226
+ },
227
+ "union": {
228
+ "light": "#9E9E9E",
229
+ "dark": "#6B6B6B",
230
+ "description": "Union band (gray 0.62); mirrored in dark mode. Dark value approved by the maintainer 2026-10-29 (dataviz-check --propose-dark)."
231
+ },
232
+ "inner": {
233
+ "light": "#333333",
234
+ "dark": "#D6D6D6",
235
+ "description": "Innermost band (gray 0.20); mirrored in dark mode, where #333333 would be 1.46:1. Dark value approved by the maintainer 2026-10-29 (dataviz-check --propose-dark)."
236
+ }
237
+ }
123
238
  }
124
239
  },
125
240
  "spacing": {
@@ -200,14 +315,14 @@ const tokens = {
200
315
  },
201
316
  "border": {
202
317
  "subtle": {
203
- "light": "rgba(20, 20, 16, 0.1)",
204
- "dark": "rgba(237, 237, 235, 0.1)",
205
- "description": "Subtle borders - element separation"
318
+ "light": "rgba(20, 20, 16, 0.47)",
319
+ "dark": "rgba(237, 237, 235, 0.47)",
320
+ "description": "Subtle borders - element separation. THE ALPHA IS THE ROLE. Distinct from color.border.subtle: this family has its own base (20,20,16 / 237,237,235), so it needs a DIFFERENT alpha to render the same. 0.47 here matches 0.45 there at 3.07:1 on the worst surface; copying 0.45 would land at 2.90:1 and fail WCAG 1.4.11. Gated by tools/contrast-check.mjs."
206
321
  },
207
322
  "moderate": {
208
- "light": "rgba(20, 20, 16, 0.15)",
209
- "dark": "rgba(237, 237, 235, 0.15)",
210
- "description": "Moderate borders - defined boundaries"
323
+ "light": "rgba(20, 20, 16, 0.63)",
324
+ "dark": "rgba(237, 237, 235, 0.63)",
325
+ "description": "Moderate borders - defined boundaries. 0.63 here matches color.border.moderate 0.60 in rendered contrast; see border.subtle on why the numbers differ between the two families."
211
326
  },
212
327
  "focus": {
213
328
  "light": "#3498DB",
@@ -282,16 +397,16 @@ const tokens = {
282
397
  "description": "Primary sans-serif font - body text"
283
398
  },
284
399
  "mono": {
285
- "value": "JetBrains Mono, SF Mono, Monaco, Inconsolata, monospace",
286
- "description": "Monospace font - code blocks and technical content"
400
+ "value": "Noto Sans Mono, SF Mono, Monaco, Inconsolata, monospace",
401
+ "description": "Noto Sans Mono — code blocks and technical content; matches the shipped font"
287
402
  },
288
403
  "display": {
289
404
  "value": "var(--font-family-display-editorial)",
290
- "description": "DEPRECATED alias of display-editorial; preserved through 0.2.x and removed in 0.3.0. New code should reference --font-family-display-editorial or --font-family-display-documentation directly. See wiki/decisions/display-family-split.md."
405
+ "description": "Alias of display-editorial, and the load-bearing indirection .di-prose rebinds to swap a whole surface to the documentation family — do NOT rewrite heading rules to reference display-editorial directly or .di-prose stops working. Reference display-editorial/display-documentation explicitly only where the surface must NOT control the family (brand chrome, navbars). Its 0.3.0 removal was deferred 2026-08-10 under wiki/decisions/compatibility-contract.md (additive-only); removal now requires its own cleanup ADR. See wiki/decisions/display-family-split.md."
291
406
  },
292
407
  "display-editorial": {
293
- "value": "Josefin Sans, system-ui, sans-serif",
294
- "description": "Editorial display family — Josefin Sans, the brand voice. Used on surfaces where the heading does rhetorical/branding work: hero/landing, marketing, story showcases, slide titles. Default for unmarked headings. Pair with weight 500 (h1) per wiki/decisions/h1-weight-medium.md."
408
+ "value": "Josefin Sans, Noto Sans, system-ui, sans-serif",
409
+ "description": "Editorial display family — Josefin Sans, the brand voice. Used on surfaces where the heading does rhetorical/branding work: hero/landing, marketing, story showcases, slide titles. Default for unmarked headings. The Noto Sans fallback serves untagged text; tagged languages are handled by fonts/lang.css. Pair with weight 500 (h1) per wiki/decisions/h1-weight-medium.md."
295
410
  },
296
411
  "display-documentation": {
297
412
  "value": "Noto Sans, system-ui, sans-serif",
@@ -302,8 +417,8 @@ const tokens = {
302
417
  "description": "Brand font - dataimago navbar and branding elements"
303
418
  },
304
419
  "math": {
305
- "value": "Noto Sans Math, Noto Sans, system-ui, sans-serif",
306
- "description": "Mathematical notation font — Noto Sans Math for broad Unicode math coverage; harmonises with the Noto Sans body stack"
420
+ "value": "Noto Sans Math, math",
421
+ "description": "Noto Sans Math, falling back to the browser's maths face; native MathML is the maths contract"
307
422
  }
308
423
  },
309
424
  "size": {