@getvitops/generator 0.9.0 → 1.0.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.
package/CHANGELOG.md CHANGED
@@ -1,5 +1,293 @@
1
1
  # @getvitops/generator
2
2
 
3
+ ## 1.0.0
4
+
5
+ ### Major Changes
6
+
7
+ - bb92a14: **The colour system is rebuilt on a target-prefixed grammar and a shared lightness ladder.**
8
+ This is a breaking change to every colour token and utility class.
9
+
10
+ ## Why
11
+
12
+ Two axes shared one class namespace — functional _planes_ (`--<role>-bg-muted`) and
13
+ appearance-relative _stops_ (`--color-<role>-muted`) — arbitrated by a "plane wins" rule. The
14
+ result was not a scale. On the shipped palette, light mode:
15
+
16
+ | class | resolved to | step |
17
+ | ---------------------------------- | ------------ | --------------------------------------------- |
18
+ | `bg-ui-accent-x-muted` | stop | 100 |
19
+ | `bg-ui-accent-muted` | plane | 100 — identical, so `x-muted` was a dead rung |
20
+ | `bg-ui-accent` | plane | 50 — _lighter_ than both its "muted" rungs |
21
+ | `border-ui-accent-muted` / `-bold` | stop / plane | 300 / 300 — identical |
22
+ | `text-ui-accent-bold` | stop | 700 — _lighter_ than `text-…-muted` (800) |
23
+
24
+ Every family was non-monotonic, two had duplicate rungs, and `--color-<role>-muted` was
25
+ unreachable through any `bg-` class.
26
+
27
+ ## The new grammar
28
+
29
+ ```
30
+ --color-<target>-<role>[-<variant>] target ∈ bg | text | icon | border
31
+ ```
32
+
33
+ The target is **inside** the name, so `bg-danger-muted` and `text-danger-muted` are different
34
+ tokens and there is nothing left to arbitrate. **The class name is the token name minus
35
+ `--color-`** — one vocabulary instead of two.
36
+
37
+ Variants are ordinal (`xx-muted` < `x-muted` < `muted` < bare < `bold` < `x-bold`) and the
38
+ tables are sparse: only cells that hold their contrast target exist.
39
+
40
+ ## Role kinds
41
+
42
+ `colors.roles` values may now be `{ hue, kind }` as well as a bare hue string:
43
+
44
+ ```jsonc
45
+ "roles": {
46
+ "danger": "rust", // shorthand => chromatic
47
+ "surface": { "hue": "navy", "kind": "surface" }
48
+ }
49
+ ```
50
+
51
+ - **`surface`** — a page/panel colour: `bg-<role>` is the card, `bg-<role>-muted` the page
52
+ behind it, `bg-<role>-x-muted` a well, `bg-<role>-bold` the inverse surface. Full text scale.
53
+ - **`chromatic`** (default) — a signal colour: tints (`bg-<role>-x-muted`/`-muted`) and solids
54
+ (`bg-<role>-solid[-bold|-x-bold]`), with **no bare `bg-<role>`** — say how loud you mean.
55
+ `text-on-<role>` is the guaranteed foreground for the solid family.
56
+
57
+ ## Palette generation
58
+
59
+ Every ramp now sits on one **fixed lightness ladder** (50 → L 0.98 … 950 → L 0.21); only chroma
60
+ and hue vary. That is what makes a step mean the same lightness in every hue. Previously each
61
+ seed transposed the curve, so relative luminance at step 300 ranged from 0.253 to 0.384 across
62
+ the shipped palette — the same variant read differently depending on the role.
63
+
64
+ Authored colours are still reproduced **exactly**. A `seed`, an `anchors` entry or a `tones`
65
+ value is pinned verbatim at its nearest step; every other step takes the ladder. Snapping brand
66
+ colours to the ladder was measured and rejected: 11 of 18 real brand hexes moved beyond a
67
+ just-noticeable difference (worst ΔE-OK 0.050 — Facebook's `#1877F2` → `#0067e1`) and six left
68
+ sRGB gamut. Deviation is now bounded and local to pinned steps, and warns past ~0.03 L.
69
+
70
+ Two tones that claim the same step now **error** instead of one silently overwriting the other;
71
+ the record form (`tones: { "600": "…", "700": "…" }`) is how you resolve it.
72
+
73
+ ## New
74
+ - **`icon-<role>`** — a non-text tier, so a glyph may run more vivid than text. `icon` is now a
75
+ default utility family alongside `bg`/`text`/`border`.
76
+ - **`--color-border-focus`** — the focus-ring tone, taken from `ui-primary`'s solid.
77
+ - **Contrast is enforced at build time**, not only in tests: text ≥ APCA Lc 75 on its primary
78
+ background, ≥ 60 on secondary planes, icons and surface boundaries ≥ 45, both appearances.
79
+ A violation now fails `generate`. Chromatic text is checked against the _surface_ planes it
80
+ actually sits on, not only its own tints. `text-<role>-x-muted` (placeholder) and `-xx-muted`
81
+ (disabled) are explicitly exempt; nothing else is.
82
+
83
+ ## Migrating
84
+
85
+ Rename `--<role>-<suffix>` → `--color-<target>-<role>[-<variant>]`, and the same for classes:
86
+
87
+ | before | after |
88
+ | --------------------------------------- | ---------------------------------------------------------------- |
89
+ | `--<role>-bg` / `bg-<role>` (chromatic) | `--color-bg-<role>-x-muted` / `bg-<role>-x-muted` |
90
+ | `--<role>-bg-muted` | `--color-bg-<role>-muted` |
91
+ | `--<role>-solid` / `-solid-bold` | `--color-bg-<role>-solid` / `-solid-bold` |
92
+ | `--<role>-on-solid` / `text-on-<role>` | `--color-text-on-<role>` (class unchanged) |
93
+ | `--<role>-text` / `-text-muted` | `--color-text-<role>` / `-muted` |
94
+ | `--<role>-border` / `-border-bold` | `--color-border-<role>` / `-bold` |
95
+ | `--color-<role>-muted` (stop) | judge by use: `--color-bg-<role>-muted` or `--color-text-<role>` |
96
+
97
+ **`surface` background names rotate**, value-preserving: what was `--surface-bg` (the page) is
98
+ now `--color-bg-surface-muted`; what was `--surface-bg-bold` (raised) is now
99
+ `--color-bg-surface`. Elevation is expressed by which token you reach for — page `bg-muted`,
100
+ card `bg` — rather than a raised/sunken pair, which is what lets a future surfaces axis flatten
101
+ it without touching markup.
102
+
103
+ `vitops lint` reports role classes that no longer resolve, and now derives its suggestions from
104
+ what the generator actually emits rather than a hand-maintained list.
105
+
106
+ ### Minor Changes
107
+
108
+ - bb92a14: **Base page typography can now be bound to a type role, and the live editor's Body controls work.**
109
+
110
+ `typography.headings` was documented as an h1…h6 map, but the key has always been used verbatim as
111
+ a selector. Mapping `"body"` to your prose role now binds base page typography to that role:
112
+
113
+ ```jsonc
114
+ "typography": {
115
+ "headings": { "body": "body", "h1": "display", "h2": "heading" }
116
+ }
117
+ ```
118
+
119
+ The generator emits `body { font-family: var(--body-ff, …); line-height: var(--body-lh, …); … }`,
120
+ so prose inherits the role and the role's tokens become the single place it is edited.
121
+ `defaultConfig()` includes the binding; existing configs are unaffected until they add it.
122
+
123
+ This is what made `<wc-theme-editor>`'s **Typography → Body** controls appear dead. They set
124
+ `--body-lh` and friends on `:root`, but only `.font-body` read them — page prose was styled by
125
+ hand-written `body { line-height: 1.55 }` rules that consumers had to write precisely _because_ the
126
+ framework offered no binding. If you keep such a block, drop the properties the role now owns:
127
+ restating them shadows the role's tokens, and because that CSS is typically unlayered it wins.
128
+
129
+ **Fixed: typography edits that previewed live and were dropped on save.** The design manifest's
130
+ `reverseIndex` only mapped hooks a role explicitly declared, while the editor renders a control for
131
+ every hook. Editing a property the role omitted (`--body-ls`, `--body-tt`, …) updated the page and
132
+ then silently vanished from the `design-system.json` patch. Every hook of every role is now indexed.
133
+
134
+ **Added: `validate()` warns when a shadow value can't survive `drop-shadow()`.** A `--shadow-<name>`
135
+ token feeds both `box-shadow` (pattern geometry) and `filter: drop-shadow(…)` (the
136
+ `.drop-shadow-<name>` utilities and the `shadow:` state shortcut). `drop-shadow()` accepts a single
137
+ layer of at most three lengths — a spread radius, a second comma-separated layer or `inset`
138
+ invalidates the whole `filter`, so the utility renders **no** shadow while the token still looks
139
+ correct everywhere it is authored. Keep shadow values in that intersection.
140
+
141
+ - bb92a14: Add a fourth output format, `design`, that emits `DESIGN.md` — the agent-facing brief in
142
+ [google-labs-code/design.md](https://github.com/google-labs-code/design.md) format.
143
+
144
+ ```sh
145
+ vitops generate --format design --out . # DESIGN.md, and nothing else
146
+ vitops generate --format css,design # compose it with a stylesheet
147
+ ```
148
+
149
+ The file is YAML front matter carrying the tokens (`colors`, `typography`, `rounded`,
150
+ `spacing`, `components`, cross-referenced with `{group.token}`) followed by a prose body
151
+ carrying the rationale — colour model, fluid scales, layout vocabulary, elevation, shape
152
+ cascade, component tiers, do's and don'ts. Every section is rendered from your config, so
153
+ it cannot describe a system the other formats don't build. Point a coding agent, a Figma
154
+ import, or a designer at this one file when they don't have the toolchain; `vitops docs`
155
+ remains the richer reference for those who do.
156
+
157
+ It is emitted with `--out .` in mind: DESIGN.md conventionally lives at a repo root beside
158
+ `AGENTS.md`, not in a build directory.
159
+
160
+ Three things the spec cannot express, handled the same way every time and explained in the
161
+ emitted prose so the file is self-describing:
162
+ - **Fluid `clamp()` sizes** → the maximum (desktop) value, since a spec `Dimension` is a
163
+ bare number plus px/em/rem.
164
+ - **Dark mode** → light values only, with the automatic functional flip explained. Role
165
+ tokens are emitted as `{colors.<hue>-<step>}` references into the raw ramps rather than
166
+ flattened hexes, so the role → ramp lineage survives the export — flattening them is
167
+ exactly what breaks dark mode downstream.
168
+ - **A `50%` radius** → dropped from `rounded` (it is not a `Dimension`) and named in the
169
+ Shapes prose instead, so nothing is silently lost.
170
+
171
+ **New:** an optional `meta` key in `design-system.json` (`{ name, description }`) supplies
172
+ the brand name and the Overview paragraph. It affects no other format.
173
+
174
+ **New:** `StylesheetFormat` (`Exclude<Format, 'design'>`), exported from
175
+ `@getvitops/generator`. `@getvitops/astro`'s `css.format` now takes that narrower type —
176
+ `design` produces no stylesheet to inject, so passing it there is a type error rather than
177
+ a missing-file failure at build time. `vitops lint --format` is likewise restricted to the
178
+ three CSS formats. No change for anyone already passing `tailwind`, `css` or `bricks`.
179
+
180
+ - bb92a14: Generate legal documents from your site config
181
+
182
+ `vitops legal` renders a privacy policy, terms of service and cookie notice from a site
183
+ config, in markdown, HTML or EmDash Portable Text:
184
+
185
+ ```sh
186
+ vitops legal --out ./content # every enabled document, as markdown
187
+ vitops legal --doc privacy --format html # one document, as an HTML fragment
188
+ ```
189
+
190
+ The documents are **derived from your config**, not filled into a form. The analytics
191
+ provider they name is the one whose ID you set; the personal information they list is what
192
+ your configured forms actually collect; the countries they name come from the providers you
193
+ use. So a provider swap updates the policy on the next build, and the fix for a wrong policy
194
+ is a corrected config — hand-editing the output is overwritten.
195
+
196
+ Enable documents under `legal`, which gains the facts the prose asserts:
197
+
198
+ ```jsonc
199
+ {
200
+ "legal": {
201
+ "jurisdiction": "ca", // only 'ca' (PIPEDA) ships today
202
+ "privacyPolicy": {
203
+ "enabled": true,
204
+ "lastUpdated": "2026-08-01",
205
+ "retention": "24 months after our last contact with you",
206
+ // Third parties the config cannot imply. Analytics, Turnstile and your
207
+ // deploy platform are detected automatically — list only the rest.
208
+ "processors": [
209
+ {
210
+ "name": "Stripe",
211
+ "purpose": "payment processing",
212
+ "country": "the United States",
213
+ },
214
+ ],
215
+ },
216
+ "termsOfService": { "enabled": true },
217
+ "cookieConsent": {
218
+ "enabled": true,
219
+ "type": "opt-in",
220
+ "categories": ["Essential", "Analytics"],
221
+ },
222
+ },
223
+ }
224
+ ```
225
+
226
+ Delivery, by stack:
227
+ - **Any stack** — `vitops legal`. No integration code; prints to stdout without `--out`.
228
+ - **WordPress/Bricks** — `vitops generate --site <path>` also writes `dist/legal/*.html`, and
229
+ the theme loader now registers `[vitops_legal doc="privacy"]` to render one in a page. The
230
+ document updates on the next deploy with no action in WordPress.
231
+ - **Astro** — `getvitops({ legal: { input: 'site.json', out: 'src/content/legal' } })` writes
232
+ markdown into a content collection and re-renders when the site config changes. It needs a
233
+ `css` config (that is what registers the Vite plugin); without one, use the CLI.
234
+ - **EmDash** — `--format portable-text`, pasted into the admin.
235
+
236
+ Also new on the public API: `generateLegal()`, `renderMarkdown()`, `renderNodes()`,
237
+ `derivePolicyVars()`, `parseMarkdown()` / `toHtmlFragment()` / `toPortableText()`, and
238
+ `resolvePrivacyContact()`.
239
+
240
+ Two things to know before you publish anything this produces:
241
+ - **It is not legal advice.** Every document opens with a review banner saying so. The
242
+ bundled terms-of-service prose in particular is generic website boilerplate and
243
+ deliberately does not cover sales, refunds, subscriptions, accounts or user-generated
244
+ content — a site doing any of those needs clauses drafted for it.
245
+ - **It is only as true as your config.** A policy asserting things your site does not do is
246
+ worse than no policy. Check that the config describes reality before you ship the output.
247
+
248
+ `validateSite` now rejects a config that enables a privacy policy without a contact for
249
+ privacy requests or a `domains.canonical`, since both are interpolated into sentences that
250
+ would otherwise render blank.
251
+
252
+ - eeb059f: **A pattern's fill can now be undone — flat, border-only cards.**
253
+
254
+ `background` was the only `base` property with no per-pattern override hook. Every other one had
255
+ `--p-card`, `--br-card`, `--b-card`, `--ds-card`; the background was hard-wired, so a flat card
256
+ meant an inline `style="background: transparent"` rather than the documented mechanism.
257
+
258
+ Two additions:
259
+ - **`background` and `background-color` join `BASE_HOOK`**, both mapping to `bg`, since patterns
260
+ author either spelling. `.card` now emits
261
+ `background: var(--bg-card, var(--surface-bg))`. This also covers the fill the generator
262
+ _injects_ for a `default_role` pattern, so `.cta` gets `--bg-cta`.
263
+
264
+ Role variants (`.card-danger`) are emitted as separate rules and stay unwrapped, so tuning
265
+ `--bg-card` adjusts the default fill without silently defeating them.
266
+
267
+ - **`bg-transparent` and `bg-inherit` utilities** in the `css` and `bricks` formats. Neither is
268
+ derived from the palette, so neither the generated scale nor Bricks' palette import produced
269
+ them. The `tailwind` format deliberately emits neither — Tailwind ships both as built-ins, the
270
+ same deferral it makes for the `TW_CLASH` names.
271
+
272
+ ```html
273
+ <!-- via the hook — set it anywhere, including :root for every card -->
274
+ <div class="card" style="--bg-card: transparent; --ds-card: none">…</div>
275
+
276
+ <!-- or compose the utility, which is what most authors will reach for -->
277
+ <div class="card bg-transparent" style="--ds-card: none">…</div>
278
+ ```
279
+
280
+ There is no utility for the shadow, so `--ds-card: none` is how you drop it in every format.
281
+
282
+ These hooks also reach the live theme editor: `--bg-card`, `--bg-btn` and `--bg-status` now
283
+ appear in `design-manifest.json`'s reverse index, so pattern backgrounds are tunable in the
284
+ browser alongside their geometry.
285
+
286
+ ### Patch Changes
287
+
288
+ - Updated dependencies [bb92a14]
289
+ - @getvitops/utils@1.0.0
290
+
3
291
  ## 0.9.0
4
292
 
5
293
  ### Minor Changes
package/README.md CHANGED
@@ -1,7 +1,8 @@
1
1
  # @getvitops/generator
2
2
 
3
- Generate design-system outputs — **Bricks** (WordPress), standalone **CSS**, and **Tailwind
4
- v4** (Astro/EmDash) — from a single `design-system.json`. This is the library that powers
3
+ Generate design-system outputs — **Tailwind v4** (Astro/EmDash), **Bricks** (WordPress), an
4
+ agent-facing **`DESIGN.md`** brief, and standalone **CSS** — from a single `design-system.json`.
5
+ This is the library that powers
5
6
  [`@getvitops/cli`](https://www.npmjs.com/package/@getvitops/cli) and
6
7
  [`@getvitops/vite`](https://www.npmjs.com/package/@getvitops/vite).
7
8
 
@@ -10,7 +11,7 @@ import { generate, validate, defaultConfig } from '@getvitops/generator';
10
11
 
11
12
  await generate({
12
13
  input: './design-system.json', // path or a config object
13
- format: 'tailwind', // 'bricks' | 'css' | 'tailwind'
14
+ format: 'tailwind', // 'tailwind' | 'bricks' | 'design' | 'css'
14
15
  outDir: './src/styles',
15
16
  });
16
17
  ```
@@ -29,8 +30,19 @@ await generate({
29
30
  | format | writes |
30
31
  | ---------- | -------------------------------------------------------------------------------------------------------- |
31
32
  | `tailwind` | `tailwind.css` (self-contained, `@import "tailwindcss"`) + `tokens.json` |
32
- | `css` | `styles.css` (bundled, standalone) + `tokens.json` + `design-manifest.json` |
33
33
  | `bricks` | `styles.min.css`, `bricks-colors-*.json`, `bricks-variables.json`, `tokens.json`, JS, `bricks/`, `docs/` |
34
+ | `design` | `DESIGN.md` — and nothing else |
35
+ | `css` | `styles.css` (bundled, standalone) + `tokens.json` + `design-manifest.json` |
36
+
37
+ `design` is the odd one out: it emits **no CSS**, just the agent-facing brief in
38
+ [design.md](https://github.com/google-labs-code/design.md) format — YAML token front matter plus a
39
+ prose body, meant to sit at a repo root beside `AGENTS.md` (so run it with `outDir: '.'`). It's what
40
+ you hand to a tool that has never heard of Vitops; the stylesheet formats are what the browser gets.
41
+ `Format` covers all four, and `StylesheetFormat` (`Exclude<Format, 'design'>`) is the narrower type
42
+ anything expecting an importable stylesheet uses.
43
+
44
+ `generate()` takes one format per call, so pair the brief with a stylesheet by calling it twice
45
+ (`@getvitops/cli`'s `--format` accepts a comma-separated list and does exactly that).
34
46
 
35
47
  See the [`design-system.json` schema](./schema.json) for the config shape.
36
48
 
@@ -6,13 +6,15 @@
6
6
  *
7
7
  * require_once get_stylesheet_directory() . '/dist/bricks/load.php';
8
8
  *
9
- * It does three things:
9
+ * It does the following:
10
10
  * 1. Registers every repo-owned Bricks element under dist/bricks/elements/.
11
11
  * 2. Enqueues the framework CSS + JS bundles — polyfills.js and elements.js as
12
12
  * ES modules (so the Lit custom elements upgrade), deferred.js with `defer`.
13
13
  * The enqueue runs on `wp_enqueue_scripts`, which the Bricks builder canvas
14
14
  * iframe also fires, so the elements upgrade while editing too.
15
15
  * 3. Registers a "Vitops" builder category so the elements group together.
16
+ * 4. Defaults the design system to dark mode.
17
+ * 5. Registers [vitops_legal] for the generated legal documents.
16
18
  *
17
19
  * Owned by the framework repo (vitops: bricks/); copied into the theme's
18
20
  * dist/bricks/ on build — do not hand-edit in the theme.
@@ -127,3 +129,46 @@ add_filter(
127
129
  },
128
130
  99
129
131
  );
132
+
133
+ /**
134
+ * 5. [vitops_legal doc="privacy"] — render a generated legal document.
135
+ *
136
+ * The build emits dist/legal/*.html from the site config, so the document updates
137
+ * on the next deploy with no action in WordPress. Drop this shortcode into a
138
+ * Bricks page (or any content) and the fragment renders there, styled by the
139
+ * framework CSS the theme already loads.
140
+ *
141
+ * `doc` is matched against a fixed allowlist rather than interpolated into the
142
+ * path. The attribute is author-supplied and lands in a filesystem read, so
143
+ * anything less would be a traversal waiting to happen — an unknown value
144
+ * renders nothing.
145
+ *
146
+ * The fragment is generated by our own build from a closed markdown subset, and
147
+ * is already HTML-escaped at render time. It is echoed as-is deliberately:
148
+ * running it through wp_kses would strip the markup the document depends on.
149
+ */
150
+ add_shortcode(
151
+ 'vitops_legal',
152
+ function ( $atts ) {
153
+ $allowed = array(
154
+ 'privacy' => 'privacy-policy',
155
+ 'terms' => 'terms-of-service',
156
+ 'cookies' => 'cookie-notice',
157
+ );
158
+
159
+ $atts = shortcode_atts( array( 'doc' => 'privacy' ), $atts, 'vitops_legal' );
160
+ $key = is_string( $atts['doc'] ) ? strtolower( trim( $atts['doc'] ) ) : '';
161
+
162
+ if ( ! isset( $allowed[ $key ] ) ) {
163
+ return '';
164
+ }
165
+
166
+ $path = get_stylesheet_directory() . '/dist/legal/' . $allowed[ $key ] . '.html';
167
+
168
+ if ( ! file_exists( $path ) ) {
169
+ return '';
170
+ }
171
+
172
+ return file_get_contents( $path );
173
+ }
174
+ );
@@ -311,8 +311,8 @@
311
311
  @supports block it resolves to that transparent value. So default to the
312
312
  functional text tokens (they flip with the appearance) with a literal
313
313
  backstop, since this is precisely the declaration that must never fail. */
314
- --_reveal-from: var(--text-reveal-color-from, var(--neutral-text-x-muted, #9099a8));
315
- --_reveal-to: var(--text-reveal-color-to, var(--neutral-text, #05070d));
314
+ --_reveal-from: var(--text-reveal-color-from, var(--color-text-neutral-x-muted, #9099a8));
315
+ --_reveal-to: var(--text-reveal-color-to, var(--color-text-neutral, #05070d));
316
316
  /* Fallback baseline: ordinary visible text in the start color. */
317
317
  color: var(--text-reveal-color-from, currentColor);
318
318
  }
@@ -64,7 +64,7 @@
64
64
  border: none;
65
65
  border-radius: 50%;
66
66
  color: var(--carousel-button-color, currentColor);
67
- background: var(--surface-bg-muted);
67
+ background: var(--color-bg-surface-muted);
68
68
  opacity: 0.9;
69
69
  transition:
70
70
  opacity 0.2s ease,
@@ -73,7 +73,7 @@
73
73
 
74
74
  &::scroll-button(*):hover {
75
75
  opacity: 1;
76
- background: var(--color-surface-muted);
76
+ background: var(--color-bg-surface-muted);
77
77
  }
78
78
 
79
79
  &::scroll-button(*):focus-visible {
@@ -211,7 +211,7 @@
211
211
  block-size: 2rem;
212
212
  border: none;
213
213
  border-radius: 50%;
214
- background: var(--surface-bg-muted);
214
+ background: var(--color-bg-surface-muted);
215
215
  color: var(--carousel-button-color, currentColor);
216
216
  opacity: 0.7;
217
217
  cursor: pointer;
@@ -10,8 +10,8 @@
10
10
  :where(:not(pre) > code) {
11
11
  font-family: var(--font-mono);
12
12
  font-size: 0.85em;
13
- background: var(--neutral-border);
14
- color: var(--neutral-text);
13
+ background: var(--color-border-neutral);
14
+ color: var(--color-text-neutral);
15
15
  padding: 0.1em 0.4em;
16
16
  border-radius: var(--br-default);
17
17
  }
@@ -20,7 +20,7 @@
20
20
  border-radius: var(--_br);
21
21
 
22
22
  &:focus {
23
- outline: 2px solid var(--combobox-focus-color, var(--color-brand-primary-muted));
23
+ outline: 2px solid var(--combobox-focus-color, var(--color-border-focus));
24
24
  outline-offset: 2px;
25
25
  }
26
26
  }
@@ -35,7 +35,7 @@
35
35
  border: var(--_b);
36
36
  border-radius: var(--_br);
37
37
  box-shadow: var(--_ds);
38
- background: var(--surface-bg);
38
+ background: var(--color-bg-surface);
39
39
  max-block-size: 15rem;
40
40
  overflow-y: auto;
41
41
  list-style: none;
@@ -55,7 +55,7 @@
55
55
 
56
56
  &:hover,
57
57
  &[aria-selected='true'] {
58
- background: var(--color-surface-muted);
58
+ background: var(--color-bg-surface-muted);
59
59
  }
60
60
 
61
61
  &[aria-disabled='true'] {
@@ -62,7 +62,7 @@
62
62
  grid-column: 1 / -1;
63
63
  margin-inline-start: var(--comment-indent, 2rem);
64
64
  padding-inline-start: var(--comment-indent, 2rem);
65
- border-inline-start: 2px solid var(--color-surface-muted);
65
+ border-inline-start: 2px solid var(--color-bg-surface-muted);
66
66
  display: flex;
67
67
  flex-direction: column;
68
68
  gap: 1rem;
@@ -38,7 +38,7 @@
38
38
  }
39
39
 
40
40
  &:focus-visible {
41
- outline: 2px solid var(--copy-focus-color, var(--color-brand-primary-muted));
41
+ outline: 2px solid var(--copy-focus-color, var(--color-border-focus));
42
42
  outline-offset: 2px;
43
43
  }
44
44
  }
@@ -66,8 +66,8 @@
66
66
  font-size: 0.75rem;
67
67
  border-radius: 0.25rem;
68
68
  white-space: nowrap;
69
- background: var(--surface-text);
70
- color: var(--surface-bg);
69
+ background: var(--color-text-surface);
70
+ color: var(--color-bg-surface);
71
71
  opacity: 0;
72
72
  visibility: hidden;
73
73
  transition:
@@ -25,8 +25,8 @@ dialog.dialog {
25
25
  border: var(--_b);
26
26
  border-radius: var(--_br);
27
27
  box-shadow: var(--_ds);
28
- background: var(--surface-bg);
29
- color: var(--surface-text);
28
+ background: var(--color-bg-surface);
29
+ color: var(--color-text-surface);
30
30
  max-block-size: calc(100vh - 2rem);
31
31
 
32
32
  &::backdrop {
@@ -15,8 +15,8 @@
15
15
  padding: var(--_p);
16
16
  border: var(--_b);
17
17
  box-shadow: var(--_ds);
18
- background: var(--surface-bg);
19
- color: var(--surface-text);
18
+ background: var(--color-bg-surface);
19
+ color: var(--color-text-surface);
20
20
  max-inline-size: none;
21
21
  max-block-size: none;
22
22
 
@@ -32,7 +32,7 @@
32
32
 
33
33
  .form-group__error {
34
34
  font-size: var(--form-error-size, 0.75rem);
35
- color: var(--color-danger-bold);
35
+ color: var(--color-text-danger);
36
36
  display: flex;
37
37
  align-items: center;
38
38
  gap: 0.25rem;
@@ -87,7 +87,7 @@ fieldset.fieldset,
87
87
  border: var(--_b);
88
88
  border-radius: var(--_br);
89
89
  padding: var(--_p);
90
- background: var(--surface-bg);
90
+ background: var(--color-bg-surface);
91
91
  }
92
92
 
93
93
  /* ── Range + colour controls ───────────────────────────────────────────────── */
@@ -95,7 +95,7 @@ fieldset.fieldset,
95
95
  * natively, in both appearances, with no pseudo-element rewriting. The colour
96
96
  * swatch only needs its chrome removed so the well fills the control box. */
97
97
  input[type='range'] {
98
- accent-color: var(--range-accent, var(--ui-primary-solid));
98
+ accent-color: var(--range-accent, var(--color-bg-ui-primary-solid));
99
99
  inline-size: 100%;
100
100
  min-inline-size: 0;
101
101
  }
@@ -202,7 +202,7 @@ input[type='color'] {
202
202
  block-size: var(--_check-size);
203
203
  border: var(--_b);
204
204
  border-radius: var(--_br);
205
- background: var(--surface-bg);
205
+ background: var(--color-bg-surface);
206
206
  color: #fff;
207
207
  display: flex;
208
208
  align-items: center;
@@ -225,8 +225,8 @@ input[type='color'] {
225
225
  }
226
226
 
227
227
  input:checked + .custom-check__box {
228
- background: var(--brand-primary-solid);
229
- border-color: var(--brand-primary-solid);
228
+ background: var(--color-bg-brand-primary-solid);
229
+ border-color: var(--color-bg-brand-primary-solid);
230
230
 
231
231
  .custom-check__indicator {
232
232
  opacity: 1;
@@ -235,7 +235,7 @@ input[type='color'] {
235
235
  }
236
236
 
237
237
  input:focus-visible + .custom-check__box {
238
- outline: 2px solid var(--check-focus-color, var(--color-brand-primary-muted));
238
+ outline: 2px solid var(--check-focus-color, var(--color-border-focus));
239
239
  outline-offset: 2px;
240
240
  }
241
241
 
@@ -288,7 +288,7 @@ input[type='color'] {
288
288
  inline-size: var(--_switch-width);
289
289
  block-size: var(--_switch-height);
290
290
  border-radius: var(--br-switch-track, var(--br-pill, 999px));
291
- background: var(--color-surface-muted);
291
+ background: var(--color-bg-surface-muted);
292
292
  transition: background-color 0.2s ease;
293
293
  }
294
294
 
@@ -306,7 +306,7 @@ input[type='color'] {
306
306
  }
307
307
 
308
308
  input:checked + .switch__track {
309
- background: var(--brand-primary-solid);
309
+ background: var(--color-bg-brand-primary-solid);
310
310
 
311
311
  .switch__thumb {
312
312
  inset-inline-start: calc(100% - var(--_switch-thumb-size) - 2px);
@@ -314,7 +314,7 @@ input[type='color'] {
314
314
  }
315
315
 
316
316
  input:focus-visible + .switch__track {
317
- outline: 2px solid var(--switch-focus-color, var(--color-brand-primary-muted));
317
+ outline: 2px solid var(--switch-focus-color, var(--color-border-focus));
318
318
  outline-offset: 2px;
319
319
  }
320
320
 
@@ -360,7 +360,7 @@ select.custom-select {
360
360
  cursor: pointer;
361
361
 
362
362
  &:focus-visible {
363
- outline: 2px solid var(--select-focus-color, var(--color-brand-primary-muted));
363
+ outline: 2px solid var(--select-focus-color, var(--color-border-focus));
364
364
  outline-offset: 2px;
365
365
  }
366
366
  }
@@ -9,7 +9,7 @@
9
9
  --_bs: var(--bs, solid);
10
10
  border-width: var(--_bw);
11
11
  border-style: var(--_bs);
12
- border-color: var(--bc, var(--surface-border, currentColor));
12
+ border-color: var(--bc, var(--color-border-surface, currentColor));
13
13
  }
14
14
 
15
15
  /* ── Group — collapse border-radius (+ shared borders) across direct children ─
@@ -57,11 +57,11 @@
57
57
  transition: background-color 0.15s ease;
58
58
 
59
59
  &:hover {
60
- background: var(--color-surface-muted);
60
+ background: var(--color-bg-surface-muted);
61
61
  }
62
62
 
63
63
  &:focus-visible {
64
- outline: 2px solid var(--icon-button-focus-color, var(--color-brand-primary-muted));
64
+ outline: 2px solid var(--icon-button-focus-color, var(--color-border-focus));
65
65
  outline-offset: 2px;
66
66
  }
67
67