@nurix/etna 0.19.3 → 0.19.4

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.
Files changed (119) hide show
  1. package/data/harness-index.json +1 -1
  2. package/data/harnesses/code/harness.json +1 -1
  3. package/data/harnesses/code/rules/frontend.md +3 -3
  4. package/data/harnesses/code/skills/design/SKILL.md +20 -8
  5. package/data/harnesses/code/skills/design/data/catalog/rules.csv +10 -5
  6. package/data/harnesses/code/skills/design/data/combinations/Readme.md +8 -4
  7. package/data/harnesses/code/skills/design/data/combinations/accent-presets.md +113 -0
  8. package/data/harnesses/code/skills/design/data/combinations/fonts-known.json +11 -0
  9. package/data/harnesses/code/skills/design/data/combinations/manifest.json +57 -10
  10. package/data/harnesses/code/skills/design/data/combinations/motion.csv +10 -0
  11. package/data/harnesses/code/skills/design/data/combinations/palettes.csv +92 -49
  12. package/data/harnesses/code/skills/design/data/combinations/styles.csv +67 -54
  13. package/data/harnesses/code/skills/design/data/combinations/typography.csv +64 -61
  14. package/data/harnesses/code/skills/design/data/design-md/library/expo.md +4 -4
  15. package/data/harnesses/code/skills/design/data/design-md/library/lovable.md +5 -5
  16. package/data/harnesses/code/skills/design/data/design-md/library/minimax.md +6 -6
  17. package/data/harnesses/code/skills/design/data/design-md/library/notion.md +4 -4
  18. package/data/harnesses/code/skills/design/data/design-md/library/runwayml.md +4 -4
  19. package/data/harnesses/code/skills/design/data/design-md/library/sentry.md +2 -2
  20. package/data/harnesses/code/skills/design/data/design-md/library/slack.md +6 -6
  21. package/data/harnesses/code/skills/design/data/design-md/library/webflow.md +5 -5
  22. package/data/harnesses/code/skills/design/data/design-md/nurix.md +11 -7
  23. package/data/harnesses/code/skills/design/data/design-tokens/SCHEMA.md +256 -141
  24. package/data/harnesses/code/skills/design/data/design-tokens/airbnb/design.tokens.json +309 -1
  25. package/data/harnesses/code/skills/design/data/design-tokens/airtable/design.tokens.json +309 -1
  26. package/data/harnesses/code/skills/design/data/design-tokens/anthropic-claude/design.tokens.json +309 -1
  27. package/data/harnesses/code/skills/design/data/design-tokens/apple/design.tokens.json +309 -1
  28. package/data/harnesses/code/skills/design/data/design-tokens/binance/design.tokens.json +309 -1
  29. package/data/harnesses/code/skills/design/data/design-tokens/bmw/design.tokens.json +309 -1
  30. package/data/harnesses/code/skills/design/data/design-tokens/cal/design.tokens.json +313 -1
  31. package/data/harnesses/code/skills/design/data/design-tokens/clay/design.tokens.json +309 -1
  32. package/data/harnesses/code/skills/design/data/design-tokens/clickhouse/design.tokens.json +309 -1
  33. package/data/harnesses/code/skills/design/data/design-tokens/cohere/design.tokens.json +309 -1
  34. package/data/harnesses/code/skills/design/data/design-tokens/coinbase/design.tokens.json +309 -1
  35. package/data/harnesses/code/skills/design/data/design-tokens/composio/design.tokens.json +309 -1
  36. package/data/harnesses/code/skills/design/data/design-tokens/cursor/design.tokens.json +309 -1
  37. package/data/harnesses/code/skills/design/data/design-tokens/elevenlabs/design.tokens.json +313 -1
  38. package/data/harnesses/code/skills/design/data/design-tokens/expo/design.tokens.json +309 -1
  39. package/data/harnesses/code/skills/design/data/design-tokens/figma/design.tokens.json +313 -1
  40. package/data/harnesses/code/skills/design/data/design-tokens/framer/design.tokens.json +309 -1
  41. package/data/harnesses/code/skills/design/data/design-tokens/goldens.json +123 -123
  42. package/data/harnesses/code/skills/design/data/design-tokens/hashicorp/design.tokens.json +309 -1
  43. package/data/harnesses/code/skills/design/data/design-tokens/ibm/design.tokens.json +309 -1
  44. package/data/harnesses/code/skills/design/data/design-tokens/intercom/design.tokens.json +309 -1
  45. package/data/harnesses/code/skills/design/data/design-tokens/kraken/design.tokens.json +313 -1
  46. package/data/harnesses/code/skills/design/data/design-tokens/linear-app/design.resolver.json +1 -1
  47. package/data/harnesses/code/skills/design/data/design-tokens/linear-app/design.tokens.dark.json +10 -0
  48. package/data/harnesses/code/skills/design/data/design-tokens/linear-app/design.tokens.json +309 -1
  49. package/data/harnesses/code/skills/design/data/design-tokens/lovable/design.tokens.json +313 -1
  50. package/data/harnesses/code/skills/design/data/design-tokens/meta/design.tokens.json +313 -1
  51. package/data/harnesses/code/skills/design/data/design-tokens/minimax/design.tokens.json +309 -1
  52. package/data/harnesses/code/skills/design/data/design-tokens/mintlify/design.tokens.json +309 -1
  53. package/data/harnesses/code/skills/design/data/design-tokens/miro/design.tokens.json +313 -1
  54. package/data/harnesses/code/skills/design/data/design-tokens/mistral-ai/design.tokens.json +309 -1
  55. package/data/harnesses/code/skills/design/data/design-tokens/mongodb/design.tokens.json +313 -1
  56. package/data/harnesses/code/skills/design/data/design-tokens/notion/design.tokens.json +309 -1
  57. package/data/harnesses/code/skills/design/data/design-tokens/nurix/design.tokens.json +307 -2
  58. package/data/harnesses/code/skills/design/data/design-tokens/nvidia/design.tokens.json +309 -1
  59. package/data/harnesses/code/skills/design/data/design-tokens/ollama/design.tokens.json +309 -1
  60. package/data/harnesses/code/skills/design/data/design-tokens/opencode-ai/design.tokens.json +309 -1
  61. package/data/harnesses/code/skills/design/data/design-tokens/posthog/design.tokens.json +309 -1
  62. package/data/harnesses/code/skills/design/data/design-tokens/raycast/design.tokens.json +309 -1
  63. package/data/harnesses/code/skills/design/data/design-tokens/replicate/design.tokens.json +309 -1
  64. package/data/harnesses/code/skills/design/data/design-tokens/resend/design.resolver.json +1 -1
  65. package/data/harnesses/code/skills/design/data/design-tokens/resend/design.tokens.dark.json +10 -0
  66. package/data/harnesses/code/skills/design/data/design-tokens/resend/design.tokens.json +309 -1
  67. package/data/harnesses/code/skills/design/data/design-tokens/revolut/design.tokens.json +309 -1
  68. package/data/harnesses/code/skills/design/data/design-tokens/runwayml/design.tokens.json +310 -2
  69. package/data/harnesses/code/skills/design/data/design-tokens/sanity/design.tokens.json +313 -1
  70. package/data/harnesses/code/skills/design/data/design-tokens/sentry/design.tokens.json +309 -1
  71. package/data/harnesses/code/skills/design/data/design-tokens/shopify/design.tokens.json +309 -1
  72. package/data/harnesses/code/skills/design/data/design-tokens/slack/design.tokens.json +309 -1
  73. package/data/harnesses/code/skills/design/data/design-tokens/spacex/design.tokens.json +309 -1
  74. package/data/harnesses/code/skills/design/data/design-tokens/spotify/design.tokens.json +313 -1
  75. package/data/harnesses/code/skills/design/data/design-tokens/stripe/design.tokens.json +313 -1
  76. package/data/harnesses/code/skills/design/data/design-tokens/supabase/design.tokens.json +309 -1
  77. package/data/harnesses/code/skills/design/data/design-tokens/superhuman/design.tokens.json +313 -1
  78. package/data/harnesses/code/skills/design/data/design-tokens/tesla/design.tokens.json +309 -1
  79. package/data/harnesses/code/skills/design/data/design-tokens/theverge/design.tokens.json +309 -1
  80. package/data/harnesses/code/skills/design/data/design-tokens/together-ai/design.tokens.json +313 -1
  81. package/data/harnesses/code/skills/design/data/design-tokens/uber/design.tokens.json +309 -1
  82. package/data/harnesses/code/skills/design/data/design-tokens/vercel/design.tokens.json +309 -1
  83. package/data/harnesses/code/skills/design/data/design-tokens/voltagent/design.resolver.json +1 -1
  84. package/data/harnesses/code/skills/design/data/design-tokens/voltagent/design.tokens.dark.json +10 -0
  85. package/data/harnesses/code/skills/design/data/design-tokens/voltagent/design.tokens.json +309 -1
  86. package/data/harnesses/code/skills/design/data/design-tokens/warp/design.tokens.json +309 -1
  87. package/data/harnesses/code/skills/design/data/design-tokens/webflow/design.tokens.json +309 -1
  88. package/data/harnesses/code/skills/design/data/design-tokens/wired/design.tokens.json +309 -1
  89. package/data/harnesses/code/skills/design/data/design-tokens/wise/design.tokens.json +309 -1
  90. package/data/harnesses/code/skills/design/data/design-tokens/x-ai/design.tokens.json +309 -1
  91. package/data/harnesses/code/skills/design/data/design-tokens/zapier/design.tokens.json +309 -1
  92. package/data/harnesses/code/skills/design/data/operator-pack/exemplars/command-center.tsx +25 -25
  93. package/data/harnesses/code/skills/design/data/operator-pack/exemplars/create-edit.tsx +18 -18
  94. package/data/harnesses/code/skills/design/data/operator-pack/exemplars/dashboard.tsx +82 -105
  95. package/data/harnesses/code/skills/design/data/operator-pack/exemplars/entity-detail.tsx +97 -93
  96. package/data/harnesses/code/skills/design/data/operator-pack/exemplars/list-detail.tsx +16 -15
  97. package/data/harnesses/code/skills/design/data/operator-pack/exemplars/log-stream.tsx +34 -34
  98. package/data/harnesses/code/skills/design/data/operator-pack/surface-catalog.md +5 -5
  99. package/data/harnesses/code/skills/design/references/create.md +49 -7
  100. package/data/harnesses/code/skills/design/references/diagnosis.md +3 -3
  101. package/data/harnesses/code/skills/design/references/hard-rules.md +25 -3
  102. package/data/harnesses/code/skills/design/references/improve.md +32 -3
  103. package/data/harnesses/code/skills/design/references/interaction.md +3 -3
  104. package/data/harnesses/code/skills/design/references/introspect.md +1 -1
  105. package/data/harnesses/code/skills/design/references/lifecycle.md +2 -2
  106. package/data/harnesses/code/skills/design/references/personality.md +280 -133
  107. package/data/harnesses/code/skills/design/references/structure.md +22 -1
  108. package/data/harnesses/code/skills/design/scripts/convert-design-md.mjs +306 -26
  109. package/data/harnesses/code/skills/design/scripts/derive-personality.mjs +596 -104
  110. package/data/harnesses/code/skills/design/scripts/lib/checks.mjs +200 -11
  111. package/data/harnesses/code/skills/design/scripts/lib/vocabulary.mjs +65 -0
  112. package/data/harnesses/code/skills/design/scripts/phase-gate.mjs +95 -11
  113. package/data/harnesses/code/skills/design/scripts/preship-sweep.mjs +50 -5
  114. package/data/harnesses/code/skills/design/scripts/render-rung.mjs +214 -14
  115. package/data/harnesses/code/skills/design/scripts/scaffold.mjs +242 -25
  116. package/data/harnesses/code/skills/design/scripts/self-integrity.mjs +81 -6
  117. package/data/harnesses/code/skills/design/scripts/snapshot-goldens.mjs +101 -5
  118. package/data/harnesses/code/skills/design/scripts/validate-combinations.mjs +142 -17
  119. package/package.json +1 -1
@@ -4,15 +4,21 @@
4
4
  DTCG JSON files whose grammar, payload, and resolution rules are the frozen transport schema,
5
5
  [SCHEMA.md](../data/design-tokens/SCHEMA.md). [`derive-personality.mjs`](../scripts/derive-personality.mjs)
6
6
  is the reference consumer: `--check` is the coverage gate that must pass before `--emit` writes
7
- `personality.css`, `globals.css`, and the `<html>` attributes. The emitted files *are* the state —
8
- no sidecar cache, no intermediate document. (The design.md format exists only as the library's
9
- curated documentation corpus, `data/design-md/library/`; the machine path reads token files.)
7
+ `personality.css`, `globals.css`, `fonts.css`, and the `<html>` attributes. The emitted files *are*
8
+ the state — no sidecar cache, no intermediate document. (The design.md format exists only as the
9
+ library's curated documentation corpus, `data/design-md/library/`; the machine path reads token files.)
10
10
 
11
11
  The look comes entirely from the cascade. A surface composes in semantic tokens (`bg-card`,
12
12
  `text-foreground`) and reads its personality from the CSS variables emission writes. Swap the
13
13
  artifact, re-emit, and every surface re-skins with zero JSX edits — the invariant the exemplars are
14
14
  validated against.
15
15
 
16
+ Emission is a **contract with checks**, not a template with hopes: every authored token group
17
+ reaches consumable, renderable output, and each contract names the check that fails closed when it
18
+ doesn't — the cascade armor (§4, verified rendered by cod-15 on the render rung), font binding
19
+ (§5, cod-16 at `--emit`), emitter completeness (§6, at `--emit`), and the Tailwind compile smoke
20
+ (`snapshot-goldens.mjs`, run at every goldens refresh).
21
+
16
22
  Every CSS and comment excerpt in this document is pasted from a real emission of the shipped Nurix
17
23
  default (`data/design-tokens/nurix/`); gate transcripts are from real runs, with scratch paths
18
24
  shown as `<artifact-dir>`.
@@ -21,32 +27,34 @@ shown as `<artifact-dir>`.
21
27
 
22
28
  ```
23
29
  design-tokens artifact ──▶ derive-personality.mjs --check ──▶ (green) ──▶ --emit <outdir>
24
- design.tokens.json the coverage gate (rules 1–12): src/styles/personality.css
25
- design.tokens.dark.json? 26 primitives + 2 dials + density tier src/styles/globals.css
26
- design.resolver.json + 34 color roles + typography 3+6, index-html-attributes.txt
30
+ design.tokens.json the coverage gate (rules 1–15): src/styles/personality.css
31
+ design.tokens.dark.json? 27 primitives + 2 dials + density tier src/styles/globals.css
32
+ design.resolver.json + 36 color roles + typography 3+7 src/styles/fonts.css
33
+ + elevation 4 + optional wash, index-html-attributes.txt
27
34
  each assigned or hinted, or exit 1
28
35
  ```
29
36
 
30
37
  ```
31
- derive-personality.mjs <artifact-dir> [--check | --emit <outdir>] [--theme light|dark]
38
+ derive-personality.mjs <artifact-dir> [--check | --emit <outdir> [--provisional]] [--theme light|dark]
32
39
  ```
33
40
 
34
41
  **The artifact.** Three files in one directory: `design.tokens.json` (the base set — primitives,
35
- color roles, typography, and the file-level `$extensions["com.nurix.design"]` payload carrying
36
- `product`, `provenance`, the dials, and the lever manifest), `design.tokens.dark.json` (only when
37
- the look has dark values), and `design.resolver.json`. The script resolves them in the resolver's
38
- deterministic order — concatenate the set files, overlay the selected theme context, resolve aliases
39
- after overlay — then validates or emits. File shapes, `$type` grammar, and payload spec live in
42
+ color roles, typography, elevation, the optional wash pair, and the file-level
43
+ `$extensions["com.nurix.design"]` payload carrying `product`, `provenance`, `theme-default`,
44
+ `anchors`, the dials, and the lever manifest), `design.tokens.dark.json` (only when the look has
45
+ dark values), and `design.resolver.json`. The script resolves them in the resolver's deterministic
46
+ order — concatenate the set files, overlay the selected theme context, resolve aliases after
47
+ overlay — then validates or emits. File shapes, `$type` grammar, and payload spec live in
40
48
  [SCHEMA.md](../data/design-tokens/SCHEMA.md); this document covers what emission does with a
41
49
  resolved artifact.
42
50
 
43
- **`--check` never best-efforts.** It runs the schema's rules 1–12 and exits non-zero naming every
51
+ **`--check` never best-efforts.** It runs the schema's rules 1–15 and exits non-zero naming every
44
52
  offending path, so the fix is mechanical; the `tokens` phase gate (and the verify/pre-ship sweeps)
45
53
  stay shut until it is green. Over the shipped default, from the skill root,
46
54
  `node scripts/derive-personality.mjs data/design-tokens/nurix --check` prints:
47
55
 
48
56
  ```
49
- check PASSED (data/design-tokens/nurix): rules 1–11 OK — 26 primitives, 34 color roles, typography 3+6, payload/manifest/parse-back OK — provenance=default, style=nurix, theme=light
57
+ check PASSED (data/design-tokens/nurix): rules 1–15 OK — 27 primitives, 36 color roles, typography 3+7, elevation 4, payload/manifest/parse-back OK — provenance=default, style=nurix, theme=light
50
58
  ```
51
59
 
52
60
  Rule 12 (the emission precondition) is enforced at emit time; `--check` surfaces it as the
@@ -59,9 +67,13 @@ check FAILED (<artifact-dir>): 1 issue(s)
59
67
  ```
60
68
 
61
69
  **`--emit <outdir>`** runs only over a passing artifact and refuses while any token carries a hint
62
- (§3). It writes three files: `<outdir>/src/styles/personality.css`, `<outdir>/src/styles/globals.css`,
63
- and `<outdir>/index-html-attributes.txt` — the attribute set the project's `index.html` `<html>` tag
64
- must carry. In real use `<outdir>` is the project root.
70
+ (§3), while any authored group lacks an emitter (§6), or while any emitted stack family fails the
71
+ font-binding gate (§5). It writes four files: `<outdir>/src/styles/personality.css`,
72
+ `<outdir>/src/styles/globals.css`, `<outdir>/src/styles/fonts.css`, and
73
+ `<outdir>/index-html-attributes.txt` — the attribute set the project's `index.html` `<html>` tag
74
+ must carry. In real use `<outdir>` is the project root. `--provisional` (the golden/regression
75
+ flow only, never shipped output) emits hinted tokens at their provisional `$values` and demotes the
76
+ font-binding gate to a loud warning.
65
77
 
66
78
  ### The emitted header is the cache record
67
79
 
@@ -79,7 +91,7 @@ attributes carry the same state in markup:
79
91
  * density: standard (<html data-density="standard">)
80
92
  * verbosity: default (<html data-verbosity="default">)
81
93
  * feedback: subtle (<html data-feedback="subtle">)
82
- * theme: light (<html data-theme="light">) — light is the default; dark is opt-in
94
+ * theme: light (<html data-theme="light">) — the payload's theme-default decides the boot theme
83
95
  * artifact: design.tokens.json (resolved via design.resolver.json) — provenance: default, sourced 2026-07-04
84
96
  *
85
97
  * Semantic-token doctrine: components name purpose (bg-card, text-foreground, border-border);
@@ -106,21 +118,26 @@ Recovery therefore reads two things: the token artifact (the source) and the emi
106
118
 
107
119
  ## 2. Derivation — token paths to emitted CSS variables
108
120
 
109
- Leaf names are law: a `primitive.*` or `color.*` token maps 1:1 to the CSS variable of the same
110
- name, without the leading `--`.
121
+ Leaf names are law: a `primitive.*`, `color.*`, `elevation.*`, or `wash.*` token maps 1:1 to the
122
+ CSS variable of the same name (elevation as `--shadow-<level>`, wash as `--wash-<slot>`).
111
123
 
112
124
  | Artifact source | Emitted target | Rule |
113
125
  | --- | --- | --- |
114
- | `primitive.<name>` (24 static) | `[data-style="<slug>"] { --<name>: … }` | 1:1 by leaf name |
126
+ | `primitive.<name>` (25 static) | `[data-style="<slug>"] { --<name>: … }` | 1:1 by leaf name |
115
127
  | `primitive.density-gap` / `density-inset` | governed refs `var(--density-base-gap/inset)` in `[data-style]` | the density tier writes the base (§4) |
116
- | `color.<name>` (34) | `:root, :root[data-theme="light"] { --<name>: … }` | 1:1 by leaf name |
117
- | dark set `color.<name>` | `[data-theme="dark"] { --<name>: … }` | only when the dark file ships (§5) |
128
+ | `color.<name>` (36) | `:root, :root[data-theme="light"] { --<name>: … }` | 1:1 by leaf name, armored (§4) |
129
+ | dark set `color.<name>` (36) | `:root[data-theme="dark"] { --<name>: … }` | only when the dark file ships (§7); same 0,2,0 armor |
130
+ | `elevation.<level>` (4) | `--shadow-sm/md/lg/overlay` in `[data-style]` **and** the `@theme` shadow utilities | every layer alpha rides `calc(… * var(--shadow-intensity))` |
131
+ | `wash.<slot>` (≤ 2, optional) | `--wash-surface` / `--wash-emphasis` in `[data-style]` | consumed only on budget-granted surfaces (col-16) |
132
+ | `typography.ramp.<step>` (7) | `@theme` text utilities `text-h1` … `text-display` (size / line-height / weight / tracking per step) | the ramp becomes *classes* — the type scale IS the ramp (typ-09) |
133
+ | `primitive.stagger-unit` | `--stagger-delay` + the named motion classes `.motion-lift` / `.motion-stagger` | mot-08 caps the cascade; mot-03 gates it behind `prefers-reduced-motion` |
118
134
  | payload `product` | `<html data-style="<slug>">` | slugified (lowercase, non-alphanumeric runs → `-`) |
119
135
  | payload `verbosity` / `feedback` / `density-tier` | `<html data-verbosity / data-feedback / data-density>` | dial → attribute |
120
- | payload `product-traits` | read by the front gate; **not** emitted to CSS | — |
136
+ | payload `theme-default` | `<html data-theme>` | the boot theme; the `theme.ts` helper round-trips the same attribute (§7) |
137
+ | payload `product-traits` / `anchors` | read by the front gate and improve/judge runs; **not** emitted to CSS | — |
121
138
  | `primitive.font-sans` / `font-mono`, `font-weight-normal` | `[data-style]` **and** literals in `@theme inline` | §4, two-weight discipline / no self-reference |
122
- | `typography.family.*` / `typography.ramp.*` | data for emitters and exemplars | reaches CSS only through the primitives it aliases |
123
- | — (derived at emit) | `--ease-enter`, `--ease-exit`, `--stagger-delay` | computed from `ease-default` + `duration-unit` |
139
+ | `typography.family.*` / `typography.ramp.*` fontFamily | data for emitters, exemplars, and the fonts.css derivation (§5) | reaches CSS variables only through the primitives it aliases |
140
+ | — (derived at emit) | `--ease-enter`, `--ease-exit`, `--stagger-delay` | computed from `ease-default` + `stagger-unit` |
124
141
 
125
142
  **The preserved-css rule.** For every emitted value the emitters prefer the token's
126
143
  `$extensions["com.nurix.design"].css` — the exact source string — so output is byte-stable across
@@ -128,27 +145,49 @@ convert/emit round trips (an aliased token takes its target's `.css`). Absent `.
128
145
  deterministic serialization applies: `dimension`/`duration` as `<value><unit>`; `number`/`fontWeight`
129
146
  as the plain number; `cubicBezier` as `cubic-bezier(x1, y1, x2, y2)`; `fontFamily` as the families
130
147
  joined with `", "`, multi-word names double-quoted; `color` as the `hex` field when srgb, else
131
- `oklch(…)`. The parse-back check (rule 9) guarantees the two forms agree before anything is emitted.
148
+ `oklch(…)`; `shadow` and `gradient` as their serialized composite byte forms. The parse-back check
149
+ (rule 9) guarantees the two forms agree before anything is emitted.
132
150
 
133
- **Derived, never stored.** Three temporal variables are computed at emit from `ease-default` and
134
- `duration-unit` — they are not tokens, not authored, and not gated (do not add them to an artifact):
151
+ **Derived, never stored.** Three temporal variables are computed at emit — they are not tokens, not
152
+ authored beyond their sources, and not gated (do not add them to an artifact):
135
153
 
136
154
  - `--ease-exit: var(--ease-default)` — departure mirrors the neutral curve.
137
155
  - `--ease-enter` — the emphasized-decelerate arrival curve `cubic-bezier(0.16, 1, 0.3, 1)` (fast
138
156
  in, settles clean); `ease-default` is reused verbatim when it is already a decelerate curve
139
157
  (y1 ≥ 0.9).
140
- - `--stagger-delay` — `duration-unit × 0.15`, rounded, clamped 0–40ms; a snappier tempo staggers
141
- less.
158
+ - `--stagger-delay: var(--stagger-unit)` — the `.motion-stagger` cascade base, riding the authored
159
+ `primitive.stagger-unit` (legal 0–60ms). This is the feel calibration in one number: **30ms reads
160
+ polished; 0ms reads utilitarian.**
142
161
 
143
- Emitted into the `[data-style]` block, directly after `ease-default`:
162
+ Emitted into the `[data-style]` block, directly after the temporal family:
144
163
 
145
164
  ```css
146
- --ease-default: cubic-bezier(0.4,0,0.2,1);
147
165
  --ease-enter: cubic-bezier(0.16, 1, 0.3, 1); /* derived from ease-default */
148
166
  --ease-exit: var(--ease-default); /* derived: departure mirrors the neutral curve */
149
- --stagger-delay: 8ms; /* derived: duration-unit × 0.15, clamped 0–40ms */
167
+ --stagger-delay: var(--stagger-unit); /* derived: the motion-stagger cascade base */
150
168
  ```
151
169
 
170
+ ### The emitted block order — law
171
+
172
+ The runtime cascade carries **contexts only** — theme, density, and the behavioral dials; every
173
+ value was already resolved at generation time into the artifact. The emitted `personality.css`
174
+ blocks, in order:
175
+
176
+ 1. **Header comment** — provenance (artifact → emit command), the CASCADE CONTRACT (naming **both**
177
+ armors and the exact loss mode, §4), the regeneration instruction.
178
+ 2. **The `:root` system baseline** — the 27 primitives at neutral operator defaults, the safety net
179
+ if `<html>` ever carries no `data-style`.
180
+ 3. **The light theme context** — `:root, :root[data-theme="light"]`, all 36 color roles, armored.
181
+ 4. **`[data-style="<slug>"]`** — the 27 primitives + the derived temporals (with their derivation
182
+ comments), the four elevation composites, and the optional wash pair; the one block
183
+ improve/extract rewrites.
184
+ 5. **The dial and density contexts** — `[data-verbosity]` / `[data-feedback]` / `[data-density]` /
185
+ `[data-density-step]` attribute blocks.
186
+ 6. **The dark theme context** — `:root[data-theme="dark"]`, all 36 dark roles (when the dark set
187
+ ships; a placeholder comment otherwise), same armor.
188
+ 7. **Motion moments** — `.motion-lift` / `.motion-stagger`, behind `prefers-reduced-motion`.
189
+ 8. **Accessibility contexts** — `prefers-reduced-motion: reduce` and `prefers-contrast: more`.
190
+
152
191
  **Dial semantics.** The payload's two behavioral dials become `<html>` attributes, and
153
192
  `personality.css` carries one block per dial value setting the behavioral variables the lifecycle
154
193
  and interaction layers read:
@@ -176,6 +215,16 @@ the ring variables ([interaction.md](./interaction.md)). A component that must b
176
215
  on a dial reads the `<html>` attribute — it never duplicates the state. Density (`data-density`) is
177
216
  spatial chrome, not behavior — it composes with the dials, never replaces them (§4).
178
217
 
218
+ **Motion moments exist in behavior, not just variables.** Emission writes two named classes
219
+ consuming the temporal tokens — `.motion-lift` (hover: −1px translateY + `elevation.sm` at
220
+ `duration-fast`/`ease-default`) and `.motion-stagger` (children arrival cascade at
221
+ `--stagger-delay`, capped at 8 children, mot-08; children past the cap arrive with the 8th) — both
222
+ inert under `prefers-reduced-motion` (mot-03). `scaffold.mjs` consumes them where the archetype
223
+ calls for them (table rows ride `motion-lift`; brand-forward card grids ride `motion-stagger` —
224
+ baseline-stamped surfaces skip arrival cascades), and the exemplars demonstrate the same
225
+ consumption. The render rung audits rendered stagger containers against the cap and the 1s
226
+ total-sequence budget.
227
+
179
228
  ## 3. Hints — provisional values that pass the gate but block emission
180
229
 
181
230
  Where a value needs judgment, the token carries a **valid provisional `$value`** plus
@@ -193,7 +242,7 @@ A hinted artifact is checkable — the pass line counts the outstanding hints (t
193
242
  copy of the default with a hint added to `primitive.contrast-boost`):
194
243
 
195
244
  ```
196
- check PASSED (<artifact-dir>): rules 1–11 OK — 26 primitives, 34 color roles, typography 3+6, payload/manifest/parse-back OK — provenance=default, style=nurix, theme=light, 1 hint(s) outstanding (emit will refuse)
245
+ check PASSED (<artifact-dir>): rules 1–15 OK — 27 primitives, 36 color roles, typography 3+7, elevation 4, payload/manifest/parse-back OK — provenance=default, style=nurix, theme=light, 1 hint(s) outstanding (emit will refuse)
197
246
  ```
198
247
 
199
248
  — but not emittable. `--emit` over the same artifact:
@@ -206,81 +255,102 @@ emit REFUSED (<artifact-dir>): 1 unresolved hint(s) — rule 12: every gated tok
206
255
 
207
256
  Resolving a hint is exactly that three-step: set the real `$value`, delete the `hint` key, record
208
257
  the rationale in the token's `$description` — so the resolved artifact stays self-explaining and
209
- re-derivable. Judgment is recorded, never silent.
258
+ re-derivable. Judgment is recorded, never silent. (`--emit --provisional`, the golden/regression
259
+ flow, emits hinted tokens at their provisional `$values` instead — loudly, and never as shipped
260
+ output.)
210
261
 
211
262
  ## 4. The substrate contracts — living with `@nurix/components`
212
263
 
213
- `@nurix/components/dist/styles.css` ships its own bare-`:root` token defaults and its own `@theme`
264
+ `@nurix/components/styles.css` ships its own bare-`:root` token defaults and its own `@theme`
214
265
  radius scale. Two cascade contracts keep the personality in charge; both are hardened with the
215
- emitted in-CSS comments below and enforced by the code-and-token rules (`cod-*`) in
216
- [hard-rules.md](./hard-rules.md). The library's actual consumption is enumerated in
217
- [the substrate audit](../../../docs/history/brainstorms/design/u1-audit-report.md).
266
+ emitted in-CSS comments below, enforced by the code-and-token rules (`cod-*`) in
267
+ [hard-rules.md](./hard-rules.md), and verified **rendered** by the render rung's per-theme drift
268
+ check (cod-15). Measured at `@nurix/components@0.14.0`, the library directly consumes 15 of the 17
269
+ v1 semantic color roles but only one personality primitive (`--radius`) — everything else reaches
270
+ it through the consumer Tailwind build this file feeds (shadows, weights, tracking, and the density
271
+ remap below).
218
272
 
219
- **CASCADE CONTRACT 1 — the specificity/attribute pair.** The color tokens are emitted under a
220
- doubled selector, and `<html>` must carry `data-theme="light"`:
273
+ **CASCADE CONTRACT 1 — both theme blocks carry the same armor.** Light emits as
274
+ `:root, :root[data-theme="light"]` and dark as `:root[data-theme="dark"]` — both specificity
275
+ (0,2,0):
221
276
 
222
277
  ```css
223
278
  /* ── Semantic + substrate color tokens — light theme. ──
224
- * CASCADE CONTRACT 1: the doubled selector out-specifies the library's bare
225
- * :root defaults in @nurix/components/styles.css — but ONLY because <html> carries
226
- * data-theme="light". A bare :root here loses to the later library import. All 34
227
- * tokens are set (D6): no consumed token falls back to a shadcn-gray default. */
279
+ * CASCADE CONTRACT 1: both theme blocks carry the same 0,2,0 armor — light as
280
+ * :root, :root[data-theme="light"] and dark as :root[data-theme="dark"] — because
281
+ * a bare :root or a lone attribute selector loses to the library's later :root
282
+ * block in @nurix/components/styles.css by source order. All 36 tokens are set
283
+ * (D6): no consumed token falls back to a shadcn-gray default. */
228
284
  :root,
229
285
  :root[data-theme="light"] {
230
286
  ```
231
287
 
232
- *Failure mode:* a bare `:root { --background: … }` is specificity `(0,1,0)` — the library's own
233
- `:root { --background: … }` is also `(0,1,0)` and imported *later*, so it wins and every surface
234
- silently reverts to shadcn gray. The `:root[data-theme="light"]` arm is `(0,2,0)` and out-wins — but
235
- only if `data-theme="light"` is actually on `<html>`; drop the attribute and the arm never matches,
236
- leaving only the losing bare `:root`. Both halves are load-bearing together.
288
+ *The exact loss mode:* a bare `:root { --background: … }` is specificity `(0,1,0)` — the library's
289
+ own `:root { --background: … }` is also `(0,1,0)` and imported *later*, so it wins by source order
290
+ and every surface silently reverts to shadcn gray. A lone `[data-theme="dark"]` attribute selector
291
+ is the same `(0,1,0)` and loses the same way — the historical dark defect was exactly that
292
+ unarmored selector, shipped while every static gate stayed green. The `:root[data-theme="…"]` arm
293
+ is `(0,2,0)` and out-wins — but only if `<html>` actually carries the attribute; scaffold writes it
294
+ from the payload's `theme-default`, and the render rung's cod-15 measures the rendered
295
+ `--background`/`--foreground`/`--primary` per declared theme context against the authored values,
296
+ so a cascade-dead theme block can no longer pass.
237
297
 
238
- **CASCADE CONTRACT 2 — the four-step import order.** The emitted `globals.css` opens:
298
+ **CASCADE CONTRACT 2 — the five-step import order.** The emitted `globals.css` opens:
239
299
 
240
300
  ```css
241
301
  /* src/styles/globals.css — Tailwind v4 glue + @theme inline mappings (emitted). */
242
302
 
243
- /* CASCADE CONTRACT 2: this four-step order is LOAD-BEARING. The library's
244
- * styles.css ships its own :root token defaults and its own @theme radius scale;
245
- * our @theme MUST come AFTER the import to win the radius scale and own every
246
- * mapping. rules/frontend.md states the same order — co-authority. */
247
- @import "./personality.css"; /* 1. token source (must be first) */
248
- @import "tailwindcss"; /* 2. framework */
249
- @import "@nurix/components/styles.css"; /* 3. library (before our @theme) */
250
-
251
- @theme inline { /* 4. consumer theme — AFTER the library, so it wins */
303
+ /* CASCADE CONTRACT 2: this five-step order is LOAD-BEARING. fonts.css registers the
304
+ * @fontsource faces before any stack consumes them; the library's styles.css ships
305
+ * its own :root token defaults and its own @theme radius scale, so our @theme MUST
306
+ * come AFTER the import to win the radius scale and own every mapping.
307
+ * rules/frontend.md states the same order — co-authority. */
308
+ @import "./fonts.css"; /* 1. font registration (faces before stacks) */
309
+ @import "./personality.css"; /* 2. token source */
310
+ @import "tailwindcss"; /* 3. framework */
311
+ @import "@nurix/components/styles.css"; /* 4. library (before our @theme) */
312
+
313
+ @theme inline { /* 5. consumer theme — AFTER the library, so it wins */
252
314
  ```
253
315
 
254
316
  *Failure mode:* put `@theme inline` (or the whole consumer theme) *before* the library import and
255
317
  the library's own `@theme` radius scale (`--radius-lg: var(--radius)`, its own offsets) comes later
256
318
  and wins — the personality `--radius` stops driving the scale and archetype radius switching breaks.
257
- `rules/frontend.md` states this same order — co-authority, not a restatement to
258
- drift from. `@theme inline` inlines each value into the generated utilities (so `bg-card` compiles
259
- to `background-color: var(--card)`).
260
-
261
- **The gray-fallback trap — all 34 roles, always.** Beyond the 15 semantic color tokens it maps
262
- (888 semantic-token class uses), the library consumes **16 more shadcn tokens** — `--popover(-foreground)`,
263
- `--secondary(-foreground)`, `--chart-1..5`, and the `--sidebar` family — that silently fall back to
264
- the shadcn-gray defaults inside `dist/styles.css` when unset: popovers, menus, secondary buttons,
265
- sidebars, and charts render visibly off-palette next to personality surfaces. The artifact's
266
- substrate group carries 17 roles covering them, and emission sets all 34 color roles every time.
267
- `--color-destructive-foreground` is mapped explicitly in `@theme` because the library leaves it
268
- unmapped.
319
+ The library also ships a `.dark`-class `@custom-variant`; the consumer's later redefinition (§7) is
320
+ what makes library `dark:` utilities follow the `data-theme` attribute. `rules/frontend.md` states
321
+ this same order — co-authority, not a restatement to drift from. `@theme inline` inlines each value
322
+ into the generated utilities (so `bg-card` compiles to `background-color: var(--card)`).
323
+
324
+ **The derivation-comment landmine.** Emitted comments explain derivations — and a CSS comment
325
+ containing the literal sequence `p-*/gap-*` **closes the comment early** (`*/`) and breaks every
326
+ consuming project's build. Emitters write "`p-*` and `gap-*`" instead; never paste a
327
+ `utility-*/utility-*` pair into an emitted CSS comment. Goldens hash bytes and never compile them,
328
+ so only the Tailwind compile smoke (`snapshot-goldens.mjs`, emission contract 5) catches this class
329
+ of defect — it is part of every goldens refresh, not optional.
330
+
331
+ **The gray-fallback trap — all 36 roles, always.** Beyond the semantic color tokens it maps, the
332
+ library consumes shadcn tokens — `--popover(-foreground)`, `--secondary(-foreground)`,
333
+ `--chart-1..5`, and the `--sidebar` family — that silently fall back to the shadcn-gray defaults
334
+ inside its `styles.css` when unset: popovers, menus, secondary buttons, sidebars, and charts render
335
+ visibly off-palette next to personality surfaces. The artifact's substrate group carries 17 roles
336
+ covering them, the semantic group carries 19 (including the `surface-raised`/`surface-overlay` dark
337
+ ladder), and emission sets all 36 color roles every time. `--color-destructive-foreground` is
338
+ mapped explicitly in `@theme` because the library leaves it unmapped.
269
339
 
270
340
  **Coverage — every token the library consumes is emitted.**
271
341
 
272
342
  | Library consumes | How | Emitted at |
273
343
  | --- | --- | --- |
274
- | `--radius` (283 `rounded-*`) | direct primitive + `@theme` radius scale | `[data-style]` · `@theme` |
275
- | shadow (`shadow-xs/sm/md/lg`, 58) | `--shadow-*` alpha rides `--shadow-intensity` | `@theme` |
276
- | font weight (`font-medium/semibold/bold/normal`, 139) | two-weight map to `--font-weight-strong` | `@theme` |
277
- | tracking (`tracking-*`, 13) | `--tracking-tight/tighter → --heading-tracking` | `@theme` |
278
- | spacing (all `p-*`/`gap-*`) | `--spacing` density remap | `@theme` |
279
- | `font-mono` (9) / `font-sans` | literal font stacks | `@theme` |
280
- | 15 semantic colors (888 uses) | direct token values | `:root[data-theme=light]` |
281
- | `--success`, `--warning` | direct token values (library ignores today — upstream gaps) | `:root[data-theme=light]` |
344
+ | `--radius` (`rounded-*`) | direct primitive + `@theme` radius scale | `[data-style]` · `@theme` |
345
+ | shadows (`shadow-*`) | the four `elevation.*` composites; every alpha rides `--shadow-intensity` | `[data-style]` · `@theme` |
346
+ | font weight (`font-medium/semibold/bold/normal`) | two-weight map to `--font-weight-strong` | `@theme` |
347
+ | tracking (`tracking-*`) | `--tracking-tight/tighter → --heading-tracking` | `@theme` |
348
+ | spacing (all `p-*` and `gap-*`) | `--spacing` density remap | `@theme` |
349
+ | `font-sans` / `font-mono` | literal font stacks (faces registered by `fonts.css`, §5) | `@theme` |
350
+ | type ramp (`text-h1` … `text-display`) | the seven authored steps as text utilities | `@theme` |
351
+ | 19 semantic colors | direct token values | `:root, :root[data-theme="light"]` (+ dark armor) |
352
+ | 17 substrate colors (`popover`/`secondary`/`chart-1..5`/`sidebar` family) | direct token values | same theme blocks |
282
353
  | `--destructive-foreground` (unmapped by library) | explicit `--color-destructive-foreground` map | `@theme` |
283
- | 17 substrate (`popover`/`secondary`/`chart-1..5`/`sidebar` family) | direct token values | `:root[data-theme=light]` |
284
354
  | `--sidebar-width` / `--sidebar-width-icon` | SidebarProvider `style` prop | shell (below) |
285
355
 
286
356
  **Density — global tier composed with a per-surface relative step.** Two composed levels, pure
@@ -296,25 +366,17 @@ emitted by `scaffold.mjs`) and re-derives one relative step off whatever tier th
296
366
  [data-density="dense"] { --density-base-gap: 0.85; --density-base-inset: 0.85; }
297
367
  [data-density="standard"] { --density-base-gap: 1; --density-base-inset: 1; }
298
368
  [data-density="spacious"] { --density-base-gap: 1.15; --density-base-inset: 1.15; }
299
-
300
- /* ── Per-archetype density modifier (D31) — relative step on the tier, pure cascade, no JS. ──
301
- * A surface element carries data-density-step; it re-derives one step off the
302
- * inherited base. Reads --density-base-* (never itself) so it composes with the
303
- * tier and never absolutely overrides it. The recomputed --density-gap flows into
304
- * the library's p-*/gap-* through the @theme --spacing remap. */
305
- [data-density-step="tighter"] { --density-gap: calc(var(--density-base-gap) * 0.85); --density-inset: calc(var(--density-base-inset) * 0.85); }
306
- [data-density-step="looser"] { --density-gap: calc(var(--density-base-gap) * 1.15); --density-inset: calc(var(--density-base-inset) * 1.15); }
307
369
  ```
308
370
 
309
371
  A log-stream inside a `standard` app resolves one step denser without pinning an absolute value.
310
- Both levels reach the library's internals through one `@theme` line — every library `p-*`/`gap-*`
311
- is `calc(var(--spacing) × N)`, and `--density-gap` resolves per element (respecting a
372
+ Both levels reach the library's internals through one `@theme` line — every library padding and gap
373
+ utility is `calc(var(--spacing) × N)`, and `--density-gap` resolves per element (respecting a
312
374
  `data-density-step` ancestor):
313
375
 
314
376
  ```css
315
- /* density remap — the single line that scales ALL library spacing. Every p-*/gap-*
316
- * is calc(var(--spacing) × N); driving --spacing off --density-gap flows the tier
317
- * AND the per-surface density step into the library's internals. */
377
+ /* density remap — the single line that scales ALL library spacing. Every p-* and
378
+ * gap-* is calc(var(--spacing) x N); driving --spacing off --density-gap flows the
379
+ * tier AND the per-surface density step into the library's internals. */
318
380
  --spacing: calc(0.25rem * var(--density-gap));
319
381
  ```
320
382
 
@@ -349,36 +411,98 @@ primitives in:
349
411
  >
350
412
  ```
351
413
 
352
- **Upstream gaps — expected, never worked around.** Some values live in `@nurix/components` JS, not
353
- CSS, so they do not follow tokens until the library adopts them. This is user-owned upstream work —
354
- never patched with brittle per-instance overrides:
355
-
356
- - **Chart palettes** — the d3 family hardcodes oklch/hex and ignores `--chart-1..5`; color props are
357
- per-instance overridable, but the defaults are upstream. Emit `--chart-1..5` regardless, so
358
- adoption is a drop-in.
359
- - **Status literals** — `changes-list` and `statistics` hardcode emerald/amber/sky rather than
360
- `--success`/`--warning`.
361
- - **Motion & icon stroke** — `duration: .2/.3` and `strokeWidth: 2/1.75` are JS numbers;
362
- `--duration-*` and `--icon-stroke` reach consumer-emitted markup but not library internals.
414
+ **Substrate limits — expected, never worked around.** Some values live in `@nurix/components` JS,
415
+ not CSS, so they do not follow tokens until the library adopts them (measured at 0.14.0; re-verify
416
+ on upgrades). This is user-owned upstream work — never patched with brittle per-instance overrides:
417
+
418
+ - **Chart palettes** — the d3 family's *defaults* embed literals, but the chart components accept
419
+ color props: scaffold's `src/lib/chart-colors.ts` wires `var(--chart-1..5)` through the series
420
+ props and the status roles (`--success`/`--warning`/`--destructive` + the accent/muted homes for
421
+ info/empty) through the BarGraph map, so charts inherit the theme today. Chart *chrome*
422
+ (gridlines, axis labels) stays hardcoded — a substrate limit, filed, never a v3 bug.
423
+ - **Motion & icon stroke** — JS motion durations and icon `strokeWidth` numbers; `--duration-*` and
424
+ `--icon-stroke` reach consumer-emitted markup but not library internals. The density dial also
425
+ no-ops inside library tables (`h-8` row height).
363
426
  - **Filled-control foreground** — `text-white` pinned on filled primary/destructive; the mapped
364
427
  `--destructive-foreground` waits on the library reading it.
365
428
 
366
429
  The stance: emit the complete token surface so the library "just works" the moment it adopts each
367
430
  token; do not build an override layer that fights the library and rots.
368
431
 
369
- ## 5. Dark and themes — the base set is light
432
+ ## 5. Fonts — one derivation, not two authored artifacts (contract 2)
433
+
434
+ The artifact's font stacks and the project's installed fonts drift when they are two authored
435
+ artifacts — a package registering `"Inter Variable"` while the stack says `"Inter"` means the
436
+ flagship font silently never renders. Emission therefore derives both from one source:
437
+
438
+ - `--emit` additionally writes **`src/styles/fonts.css`** — the `@fontsource` import list
439
+ **generated from the artifact's `fontFamily` tokens**, resolved through `typography.csv`'s
440
+ `css-import` column (paste-ready static packages, registering the plain family names) with
441
+ `fonts-known.json` as the resolver fallback. The emitted stacks use the **registered** family
442
+ names. `globals.css` loads it first (Cascade Contract 2), so faces register before any stack
443
+ consumes them.
444
+
445
+ ```css
446
+ /* Inter — @fontsource/inter */
447
+ @import '@fontsource/inter/400.css';
448
+ @import '@fontsource/inter/500.css';
449
+ @import '@fontsource/inter/600.css';
450
+ @import '@fontsource/inter/700.css';
451
+
452
+ /* Geist Mono — @fontsource/geist-mono */
453
+ @import '@fontsource/geist-mono/400.css';
454
+ ```
455
+
456
+ **The check (cod-16, fail-closed at `--emit`).** Every family name in every emitted stack either
457
+ (a) matches a family registered by a generated import whose package exists in the emit target's
458
+ `package.json` (when the target is a project; a standalone target degrades to import-list
459
+ validation), or (b) is a recognized system/generic family (`system-ui`, `ui-monospace`,
460
+ `sans-serif`, `serif`, `monospace`, `ui-sans-serif`, the conventional OS fallback faces, …the
461
+ vocabulary is shared with the source-shell check in `lib/checks.mjs`). Failure exits non-zero
462
+ naming the unbound family and the exact package to install:
463
+
464
+ ```
465
+ emit FAILED (<artifact-dir>): font binding (cod-16) — 1 unbound family
466
+ - "Inter" (emitted --font-sans stack): its registering import needs @fontsource/inter, absent from package.json — npm install @fontsource/inter
467
+ ```
468
+
469
+ A consumer that installed the variable package instead (`@fontsource-variable/<slug>`, registering
470
+ the `"<Family> Variable"` name) also binds — the slug match strips the ` Variable` suffix. Brand
471
+ fonts with no `@fontsource` package fail the gate by design: swap to a bindable family, or register
472
+ the face via `@font-face` and record the deviation.
473
+
474
+ ## 6. Emitter completeness — no authored group vanishes (contract 3)
475
+
476
+ Every authored token group has a named consumer in the emitted output; `--emit` fails if it
477
+ recognizes a group it has no emitter for — machinery that accepts data it never emits is a silent
478
+ contract breach (an authored ramp with no emitter simply vanishes). The registry: `primitive` (the
479
+ `[data-style]` block), `color` (the armored theme blocks), `typography` (the `@theme` ramp text
480
+ utilities; subgroups `family`/`ramp`), `elevation` (the shadow composites + utilities), `wash` (the
481
+ wash pair — optional presence). Against a scratch artifact carrying an unconsumed `spacing` group:
482
+
483
+ ```
484
+ emit FAILED (<artifact-dir>): 1 issue(s)
485
+ - emitter completeness — authored token group "spacing" has no emitter (contract 3): an authored group with no emitter simply vanishes; add its emitter to this script or remove the group. Emitters exist for: primitive, color, typography, elevation, wash.
486
+ ```
487
+
488
+ ## 7. Dark and themes — the base set is light; the brief's theme binds
370
489
 
371
490
  The base token set **is** the light rendering — light is never omitted, and the resolver's `light`
372
- context is always the empty overlay. A look with dark values ships `design.tokens.dark.json` —
373
- exactly the `color` group, all 34 roles, nothing else — named by the resolver's `dark` context and
374
- overlaid per the resolution order. When it ships, emission adds a `[data-theme="dark"]` block
375
- remapping all 34 roles:
491
+ context is always the empty overlay. **The brief's stated theme is binding**: a dark-first look
492
+ sets `theme-default: dark` (payload and resolver both), scaffold writes `<html data-theme="dark">`,
493
+ and the app boots dark; light is the default only when the brief is silent. A look with dark values
494
+ ships `design.tokens.dark.json` — exactly the `color` group, all 36 roles, nothing else — named by
495
+ the resolver's `dark` context and overlaid per the resolution order. When it ships, emission adds
496
+ the armored dark context remapping all 36 roles (including the `surface-raised`/`surface-overlay`
497
+ lightness ladder — raised surfaces get *lighter* in dark, the physics shadows cannot fake):
376
498
 
377
499
  ```css
378
500
  /* ── Dark theme — OPT-IN ONLY (this look ships a dark overlay). ──
379
- * design.tokens.dark.json remaps all 34 color tokens; <html data-theme="dark">
380
- * opts in. Light stays the default. */
381
- [data-theme="dark"] {
501
+ * CASCADE CONTRACT 1b: :root[data-theme="dark"] (0,2,0) — a lone [data-theme="dark"]
502
+ * attribute selector (0,1,0) loses to the library's later bare :root block by source
503
+ * order. design.tokens.dark.json remaps all 36 color tokens; <html data-theme="dark">
504
+ * opts in (theme-default decides the boot attribute). */
505
+ :root[data-theme="dark"] {
382
506
  ```
383
507
 
384
508
  For a light-only artifact (the shipped default is one), the emitted file carries a placeholder
@@ -386,43 +510,66 @@ comment instead:
386
510
 
387
511
  ```css
388
512
  /* ── Dark theme — OPT-IN ONLY. ──
389
- * Emitted only when the artifact ships a design.tokens.dark.json dark overlay.
390
- * Light stays the default; this block never appears for a light-only project.
391
- [data-theme="dark"] {
392
- --background: ...; --foreground: ...; ... all 34 tokens remapped ...
513
+ * Emitted only when the artifact ships a design.tokens.dark.json dark overlay,
514
+ * as :root[data-theme="dark"] (0,2,0 — armored against the library's later :root).
515
+ * This block never appears for a light-only project.
516
+ :root[data-theme="dark"] {
517
+ --background: ...; --foreground: ...; ... all 36 tokens remapped ...
393
518
  } */
394
519
  ```
395
520
 
396
521
  `--theme dark` selects the dark context and sets `<html data-theme="dark">` in the emitted
397
- attributes (the header's `theme:` line follows). The default is `light`; requesting dark from a
398
- light-only artifact fails cleanly —
399
- `node scripts/derive-personality.mjs data/design-tokens/nurix --check --theme dark` prints:
522
+ attributes (the header's `theme:` line follows). Requesting dark from a light-only artifact fails
523
+ cleanly — `node scripts/derive-personality.mjs data/design-tokens/nurix --check --theme dark`
524
+ prints:
400
525
 
401
526
  ```
402
527
  check FAILED (data/design-tokens/nurix): 1 issue(s)
403
- - rule 1 — resolver: --theme dark requested but the resolver carries no dark context
528
+ - rule 1 — resolver: theme dark selected but the resolver carries no dark context
404
529
  ```
405
530
 
406
- The library's `dark:` utilities are class-based; a dark-shipping project redefines the variant to
407
- follow the attribute. `globals.css` carries the instruction as a comment, enabled only when dark
408
- ships:
531
+ **The `@custom-variant dark` line is always active.** The library's `dark:` utilities are
532
+ class-based; the emitted `globals.css` redefines the variant to follow the attribute — always, not
533
+ only when dark ships (the line is inert until `<html data-theme="dark">`, and library `dark:`
534
+ utilities — legal *inside* the library, banned in project JSX by the no-literals rule cod-05 —
535
+ must follow the same attribute the moment a dark set arrives):
409
536
 
410
537
  ```css
411
- /* dark variant — emitted ONLY when the project ships dark; redefines the library's
412
- * class-based dark to follow data-theme so the library's dark: utilities honor our
413
- * convention. Absent for a light-only project.
414
- @custom-variant dark (&:where([data-theme="dark"], [data-theme="dark"] *)); */
538
+ /* dark variant — ALWAYS ACTIVE: redefines the library's class-based dark: variant to
539
+ * follow the data-theme attribute, so library dark: utilities (legal inside the library,
540
+ * banned in project JSX by the no-literals rule) track the same attribute the theme
541
+ * blocks armor. Inert until <html data-theme="dark">; harmless for a light-only project. */
542
+ @custom-variant dark (&:where([data-theme="dark"], [data-theme="dark"] *));
415
543
  ```
416
544
 
545
+ **The `theme.ts` helper — one selector convention, four writers, zero disagreement.** Scaffold
546
+ emits `src/lib/theme.ts`: `getTheme`/`setTheme`/`toggleTheme` write the same `data-theme` attribute
547
+ the armor selectors and the `@custom-variant` read, persisting the preference; `initTheme()`
548
+ (called by `main.tsx` before mount) applies a stored preference over the boot attribute. Code is
549
+ the cache: the attribute is the single runtime theme state — the helper never keeps a parallel
550
+ copy. The four writers of the one convention: `personality.css` (the armored blocks), scaffold's
551
+ `<html data-theme>` (the boot value from `theme-default`), the `@custom-variant dark` line, and
552
+ `theme.ts` (the runtime toggle).
553
+
554
+ **The check (cod-15, on the render rung).** With the app rendered and `data-theme` flipped through
555
+ each theme context the resolver declares, the rendered
556
+ `getComputedStyle(document.documentElement).getPropertyValue('--background')` — plus
557
+ `--foreground`/`--primary` — must equal that context's authored values: rendered token drift is
558
+ measured *per theme context*, not just for the boot theme. Create runs invoke the rung with
559
+ `--require` (no headless browser → exit non-zero with the exact install command); improve keeps
560
+ graceful degradation, but its skip lands as a named finding, never a one-line notice.
561
+
417
562
  Components never use `dark:` prefixes or literal colors regardless — the semantic tokens remap and
418
563
  every surface follows (the semantic-token doctrine in the emitted header).
419
564
 
420
- ## 6. Precedence and provenance — which artifact gets emitted
565
+ ## 8. Precedence and provenance — which artifact gets emitted
421
566
 
422
567
  Look resolution is project-first: a `project` or `extracted` artifact wins outright; a `library`
423
568
  entry serves only when the brief names or resembles the product and the user confirms; the Nurix
424
569
  default (`data/design-tokens/nurix/`) is worn when nothing else speaks. The origin is the payload's
425
570
  `provenance` field, and the emitted header's `artifact:` line records it alongside the `sourced`
426
- date — so the emitted CSS always says where its look came from. The full precedence table, payload
427
- spec, and per-value provenance convention (`$description`) are
428
- [SCHEMA.md](../data/design-tokens/SCHEMA.md).
571
+ date — so the emitted CSS always says where its look came from. Create-authored artifacts may also
572
+ persist the full intake **look block** (`payload.look` — theme/anchors/delight/resolution/
573
+ resolution-mode, enum-checked when present); `theme-default` and the top-level `anchors` remain the
574
+ canonical projections emission consumes. The full precedence table, payload spec, and per-value
575
+ provenance convention (`$description`) are [SCHEMA.md](../data/design-tokens/SCHEMA.md).