@tenphi/glaze 0.19.0 → 1.1.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/docs/api.md CHANGED
@@ -6,12 +6,17 @@ Full reference for every public method, option, and type exported by `@tenphi/gl
6
6
 
7
7
  - [Theme creation](#theme-creation)
8
8
  - [Theme methods](#theme-methods)
9
+ - [DTCG](#themedtcgoptions)
10
+ - [DTCG Resolver-Module](#themedtcgresolveroptions)
11
+ - [Tailwind CSS](#themetailwindoptions)
12
+ - [High-contrast pairs](#high-contrast-pairs)
9
13
  - [Color definitions](#color-definitions)
10
14
  - [Standalone color tokens](#standalone-color-tokens)
11
15
  - [Shadows](#shadows)
12
16
  - [Mix colors](#mix-colors)
13
17
  - [Palette](#palette)
14
18
  - [Output formats](#output-formats)
19
+ - [Hue channel splitting](#hue-channel-splitting-splithue)
15
20
  - [Adaptation modes](#adaptation-modes)
16
21
  - [Light / dark scheme mapping](#light--dark-scheme-mapping)
17
22
  - [Configuration](#configuration)
@@ -23,25 +28,41 @@ Full reference for every public method, option, and type exported by `@tenphi/gl
23
28
 
24
29
  ## Theme creation
25
30
 
26
- | Method | Description |
27
- |---|---|
28
- | `glaze(hue, saturation?, config?)` | Create a theme from hue (0–360) and saturation (0–100). Optional `config` overrides the global config for this theme. |
29
- | `glaze({ hue, saturation }, config?)` | Create a theme from an options object, with optional per-theme config override. |
30
- | `glaze.from(data)` | Create a theme from an exported configuration (`theme.export()` snapshot). |
31
- | `glaze.fromHex(hex)` | Create a theme from a hex color (`#rgb` or `#rrggbb`). Extracts hue and saturation. |
32
- | `glaze.fromRgb(r, g, b)` | Create a theme from RGB values (0–255). Extracts hue and saturation. |
31
+ | Method | Description |
32
+ | ------------------------------------- | --------------------------------------------------------------------------------------------------------------------- |
33
+ | `glaze(hue, saturation?, config?)` | Create a theme from hue (0–360) and saturation (0–100). Optional `config` overrides the global config for this theme. |
34
+ | `glaze({ hue, saturation }, config?)` | Create a theme from an options object, with optional per-theme config override. |
35
+ | `glaze.themeFrom(data)` | Create a theme from a `theme.export()` snapshot (`kind: 'theme'`). |
36
+ | `glaze.from(data)` | Compat alias for `glaze.themeFrom`. |
37
+ | `glaze.fromHex(hex)` | Create a theme from a hex color (`#rgb` or `#rrggbb`). Extracts hue and saturation. |
38
+ | `glaze.fromRgb(r, g, b)` | Create a theme from RGB values (0–255). Extracts hue and saturation. |
39
+ | `glaze.paletteFrom(data)` | Create a palette from a `palette.export()` snapshot (`kind: 'palette'`). |
40
+ | `glaze.colorFrom(data)` | Create a color token from a `token.export()` snapshot (`kind: 'color'`). |
41
+ | `glaze.isThemeExport(data)` | Type guard for theme authoring snapshots. |
42
+ | `glaze.isColorTokenExport(data)` | Type guard for color-token authoring snapshots. |
43
+ | `glaze.isPaletteExport(data)` | Type guard for palette authoring snapshots. |
33
44
 
34
45
  ```ts
35
46
  const a = glaze(280, 80);
36
47
  const b = glaze({ hue: 280, saturation: 80 });
37
48
  const c = glaze.fromHex('#7a4dbf');
38
49
  const d = glaze.fromRgb(122, 77, 191);
39
- const e = glaze.from(a.export());
50
+ const e = glaze.themeFrom(a.export());
40
51
 
41
52
  // Per-theme config override:
42
53
  const rawTheme = glaze(280, 80, { lightTone: false, darkTone: false });
43
54
  ```
44
55
 
56
+ Authoring restore triad (parallel to `.export()` on each instance):
57
+
58
+ | Export | Restore |
59
+ | ------ | ------- |
60
+ | `theme.export()` | `glaze.themeFrom()` |
61
+ | `token.export()` | `glaze.colorFrom()` |
62
+ | `palette.export()` | `glaze.paletteFrom()` |
63
+
64
+ Every snapshot includes `kind` + `version` (`GLAZE_EXPORT_VERSION`, currently `1`). Legacy snapshots without those fields still restore. Wrong `kind`, or a `version` outside `1..=GLAZE_EXPORT_VERSION`, throws.
65
+
45
66
  The optional `config` parameter is a `GlazeConfigOverride` — see [Per-instance config override](#per-instance-config-override).
46
67
 
47
68
  ---
@@ -50,24 +71,27 @@ The optional `config` parameter is a `GlazeConfigOverride` — see [Per-instance
50
71
 
51
72
  A `GlazeTheme` exposes:
52
73
 
53
- | Method | Description |
54
- |---|---|
55
- | `theme.hue` (readonly) | The hue seed (0–360). |
56
- | `theme.saturation` (readonly) | The saturation seed (0–100). |
57
- | `theme.colors(defs)` | Add/replace colors (additive merge — adds new, overwrites existing by name, doesn't remove others). |
58
- | `theme.color(name)` | Get a color definition by name. |
59
- | `theme.color(name, def)` | Set a single color definition. |
60
- | `theme.remove(name \| names[])` | Remove one or more color definitions. |
61
- | `theme.has(name)` | Check if a color is defined. |
62
- | `theme.list()` | List all defined color names. |
63
- | `theme.reset()` | Clear all color definitions. |
64
- | `theme.export()` | Export the theme configuration as a JSON-safe object. |
65
- | `theme.extend(options)` | Create a child theme inheriting all color definitions (see [`extend`](#themeextendoptions) below). |
66
- | `theme.resolve()` | Resolve all colors and return a `Map<string, ResolvedColor>`. |
67
- | `theme.tokens(options?)` | Export as a flat token map grouped by scheme variant. |
68
- | `theme.tasty(options?)` | Export as Tasty style-to-state bindings. |
69
- | `theme.json(options?)` | Export as plain JSON. |
70
- | `theme.css(options?)` | Export as CSS custom property declarations. |
74
+ | Method | Description |
75
+ | ------------------------------- | --------------------------------------------------------------------------------------------------- |
76
+ | `theme.hue` (readonly) | The hue seed (0–360). |
77
+ | `theme.saturation` (readonly) | The saturation seed (0–100). |
78
+ | `theme.colors(defs)` | Add/replace colors (additive merge — adds new, overwrites existing by name, doesn't remove others). |
79
+ | `theme.color(name)` | Get a color definition by name. |
80
+ | `theme.color(name, def)` | Set a single color definition. |
81
+ | `theme.remove(name \| names[])` | Remove one or more color definitions. |
82
+ | `theme.has(name)` | Check if a color is defined. |
83
+ | `theme.list()` | List all defined color names. |
84
+ | `theme.reset()` | Clear all color definitions. |
85
+ | `theme.export(override?)` | Export the theme configuration as a JSON-safe object (optional config override at export time). |
86
+ | `theme.extend(options)` | Create a child theme inheriting all color definitions (see [`extend`](#themeextendoptions) below). |
87
+ | `theme.resolve()` | Resolve all colors and return a `Map<string, ResolvedColor>`. |
88
+ | `theme.tokens(options?)` | Export as a flat token map grouped by scheme variant. |
89
+ | `theme.tasty(options?)` | Export as [Tasty](https://tasty.style) style-to-state bindings. |
90
+ | `theme.json(options?)` | Export as plain JSON. |
91
+ | `theme.css(options?)` | Export as CSS custom property declarations. |
92
+ | `theme.dtcg(options?)` | Export one W3C DTCG token tree per scheme. |
93
+ | `theme.dtcgResolver(options?)` | Export one DTCG Resolver-Module document containing every scheme. |
94
+ | `theme.tailwind(options?)` | Export a Tailwind CSS v4 theme and scheme overrides. |
71
95
 
72
96
  ### `theme.colors(defs)`
73
97
 
@@ -80,8 +104,8 @@ theme.colors({ text: { tone: 30 } });
80
104
  ### `theme.color(name) / theme.color(name, def)`
81
105
 
82
106
  ```ts
83
- theme.color('surface', { tone: 97, saturation: 0.75 }); // set
84
- const def = theme.color('surface'); // get
107
+ theme.color('surface', { tone: 97, saturation: 0.75 }); // set
108
+ const def = theme.color('surface'); // get
85
109
  ```
86
110
 
87
111
  ### `theme.extend(options)`
@@ -100,54 +124,66 @@ const highSat = base.extend({ config: { darkTone: [10, 100] } });
100
124
 
101
125
  `GlazeExtendOptions`:
102
126
 
103
- | Field | Type | Description |
104
- |---|---|---|
105
- | `hue` | `number` | Replace the hue seed. Defaults to the parent's hue. |
106
- | `saturation` | `number` | Replace the saturation seed. Defaults to the parent's saturation. |
107
- | `colors` | `ColorMap` | Per-theme overrides (additive merge over the inherited map). |
108
- | `config` | `GlazeConfigOverride` | Config override for the child. Shallow-merged with the parent's override — child fields win. |
127
+ | Field | Type | Description |
128
+ | ------------ | --------------------- | -------------------------------------------------------------------------------------------- |
129
+ | `hue` | `number` | Replace the hue seed. Defaults to the parent's hue. |
130
+ | `saturation` | `number` | Replace the saturation seed. Defaults to the parent's saturation. |
131
+ | `colors` | `ColorMap` | Per-theme overrides (additive merge over the inherited map). |
132
+ | `config` | `GlazeConfigOverride` | Config override for the child. Shallow-merged with the parent's override — child fields win. |
109
133
 
110
134
  Colors marked with `inherit: false` on the parent are **not** copied into the child.
111
135
 
136
+ ### `theme.resolve()`
137
+
138
+ Resolves the dependency graph and returns a
139
+ `Map<string, ResolvedColor>`. Export methods call it automatically; use it
140
+ directly for tests, diagnostics, or a custom output pipeline.
141
+
142
+ ```ts
143
+ const resolved = theme.resolve();
144
+ const surface = resolved.get('surface');
145
+ // surface?.light.t is canonical tone on 0–1.
146
+ ```
147
+
112
148
  ### `theme.tokens(options?)`
113
149
 
114
150
  Flat token map grouped by scheme variant.
115
151
 
116
152
  ```ts
117
- theme.tokens()
153
+ theme.tokens();
118
154
  // → { light: { surface: 'oklch(...)' }, dark: { surface: 'oklch(...)' } }
119
155
  ```
120
156
 
121
157
  `GlazeJsonOptions`:
122
158
 
123
- | Option | Default | Description |
124
- |---|---|---|
125
- | `format` | `'oklch'` | Output color format. One of `'rgb' \| 'hsl' \| 'oklch'`. `'okhsl'` and `'okhst'` throw — use `tasty()` for those. |
126
- | `modes` | `{ dark: true, highContrast: false }` (or global config) | Which scheme variants to include. |
159
+ | Option | Default | Description |
160
+ | -------- | -------------------------------------------------------- | ----------------------------------------------------------------------------------------------------------------- |
161
+ | `format` | `'oklch'` | Output color format. One of `'rgb' \| 'hsl' \| 'oklch'`. `'okhsl'` and `'okhst'` throw — use `tasty()` for those. |
162
+ | `modes` | `{ dark: true, highContrast: false }` (or global config) | Which scheme variants to include. |
127
163
 
128
164
  ### `theme.tasty(options?)`
129
165
 
130
- Tasty style-to-state bindings for the [Tasty style system](https://tasty.style/docs). Uses `#name` color token keys and state aliases. By default the dark and high-contrast variants are keyed by media-query states (`'@media(prefers-color-scheme: dark)'`, `'@media(prefers-contrast: more)'`) so tokens work without registering custom states.
166
+ Style-to-state bindings for the [Tasty](https://tasty.style) style system. Uses `#name` color token keys and state aliases. By default the dark and high-contrast variants are keyed by media-query states (`'@media(prefers-color-scheme: dark)'`, `'@media(prefers-contrast: more)'`) so tokens work without registering custom states.
131
167
 
132
168
  ```ts
133
- theme.tasty()
169
+ theme.tasty();
134
170
  // → {
135
- // '#surface': { '': 'okhsl(...)', '@media(prefers-color-scheme: dark)': 'okhsl(...)' },
171
+ // '#surface': { '': 'oklch(...)', '@media(prefers-color-scheme: dark)': 'oklch(...)' },
136
172
  // ...
137
173
  // }
138
174
  ```
139
175
 
140
176
  `GlazeTokenOptions`:
141
177
 
142
- | Option | Default | Description |
143
- |---|---|---|
144
- | `format` | `'okhsl'` | Output color format. `'okhsl'` and `'okhst'` are supported here (Tasty-only spaces). |
145
- | `modes` | global config | Which scheme variants to include. |
146
- | `states.dark` | `'@media(prefers-color-scheme: dark)'` (or global config) | State alias for dark mode tokens. |
147
- | `states.highContrast` | `'@media(prefers-contrast: more)'` (or global config) | State alias for high-contrast tokens. |
148
- | `splitHue` | `false` | Emit hue as a separate custom property (`$name-hue` token + `var()` in `oklch` values). Requires `format: 'oklch'` and every color to be pastel. |
149
- | `name` | `'theme'` | Base name for the theme-level hue var (`$theme-hue` / `--theme-hue`). Palette export auto-derives this from the theme name. |
150
- | `prefix` | (palette only) | See [Palette](#palette). |
178
+ | Option | Default | Description |
179
+ | --------------------- | --------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------ |
180
+ | `format` | `'oklch'` | Output color format. `'okhsl'` and `'okhst'` are also supported here ([Tasty](https://tasty.style)-only spaces). |
181
+ | `modes` | global config | Which scheme variants to include. |
182
+ | `states.dark` | `'@media(prefers-color-scheme: dark)'` (or global config) | State alias for dark mode tokens. |
183
+ | `states.highContrast` | `'@media(prefers-contrast: more)'` (or global config) | State alias for high-contrast tokens. |
184
+ | `splitHue` | `false` | Emit hue as a separate custom property (`$name-hue` token + `var()` in `oklch` values). Requires `format: 'oklch'` and every color to be pastel. |
185
+ | `name` | `'theme'` | Base name for the theme-level hue var (`$theme-hue` / `--theme-hue`). Palette export auto-derives this from the theme name. |
186
+ | `prefix` | (palette only) | See [Palette](#palette). |
151
187
 
152
188
  When both `dark` and `highContrast` modes are enabled, dark high-contrast variants are emitted under the combined key `<dark> & <highContrast>` (e.g. `'@media(prefers-color-scheme: dark) & @media(prefers-contrast: more)'`).
153
189
 
@@ -156,7 +192,7 @@ When both `dark` and `highContrast` modes are enabled, dark high-contrast varian
156
192
  Per-color JSON map.
157
193
 
158
194
  ```ts
159
- theme.json()
195
+ theme.json();
160
196
  // → {
161
197
  // surface: { light: 'oklch(...)', dark: 'oklch(...)' },
162
198
  // text: { light: 'oklch(...)', dark: 'oklch(...)' },
@@ -172,8 +208,8 @@ CSS custom property declaration strings, grouped by scheme variant.
172
208
  ```ts
173
209
  theme.css();
174
210
  // → {
175
- // light: '--surface-color: rgb(...);\n--text-color: rgb(...);',
176
- // dark: '--surface-color: rgb(...);\n--text-color: rgb(...);',
211
+ // light: '--surface-color: oklch(...);\n--text-color: oklch(...);',
212
+ // dark: '--surface-color: oklch(...);\n--text-color: oklch(...);',
177
213
  // lightContrast: '...',
178
214
  // darkContrast: '...',
179
215
  // }
@@ -181,12 +217,12 @@ theme.css();
181
217
 
182
218
  `GlazeCssOptions`:
183
219
 
184
- | Option | Default | Description |
185
- |---|---|---|
186
- | `format` | `'rgb'` | Output color format. `'okhsl'` and `'okhst'` throw — use `tasty()` for those. |
187
- | `suffix` | `'-color'` | Suffix appended to each CSS property name. Pass `''` for bare property names. |
188
- | `splitHue` | `false` | Emit hue as a separate `--*-hue` custom property referenced via `var()` in `oklch` color values. Requires `format: 'oklch'` and every color to be pastel. Shadow/mix colors stay inline (blended hue; they do not follow `--hue` rotation). |
189
- | `name` | `'theme'` | Base name for the theme-level hue var (`--theme-hue`). Palette export auto-derives this from the theme name. |
220
+ | Option | Default | Description |
221
+ | ---------- | ---------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
222
+ | `format` | `'oklch'` | Output color format. One of `'rgb' \| 'hsl' \| 'oklch'`. `'okhsl'` and `'okhst'` throw — use `tasty()` for those. |
223
+ | `suffix` | `'-color'` | Suffix appended to each CSS property name. Pass `''` for bare property names. |
224
+ | `splitHue` | `false` | Emit hue as a separate `--*-hue` custom property referenced via `var()` in `oklch` color values. Requires `format: 'oklch'` and every color to be pastel. Shadow/mix colors stay inline (blended hue; they do not follow `--hue` rotation). |
225
+ | `name` | `'theme'` | Base name for the theme-level hue var (`--theme-hue`). Palette export auto-derives this from the theme name. |
190
226
 
191
227
  `GlazeCssResult` always contains all four keys (`light`, `dark`, `lightContrast`, `darkContrast`); empty if no colors are defined for that variant.
192
228
 
@@ -195,7 +231,7 @@ theme.css();
195
231
  W3C [Design Tokens Format Module (2025.10)](https://www.designtokens.org/) documents — the vendor-neutral JSON format consumed by Figma, Tokens Studio, Style Dictionary v4+, Terrazzo, Penpot, and every DTCG-compatible tool. Returns one spec-conformant token tree per scheme variant.
196
232
 
197
233
  ```ts
198
- theme.dtcg()
234
+ theme.dtcg();
199
235
  // → {
200
236
  // light: {
201
237
  // surface: {
@@ -216,10 +252,10 @@ Write each document to its own `.tokens.json` file — one file per scheme is th
216
252
 
217
253
  `GlazeDtcgOptions`:
218
254
 
219
- | Option | Default | Description |
220
- |---|---|---|
221
- | `colorSpace` | `'srgb'` | Color space for `$value`. `'srgb'` emits gamma sRGB `components` (0–1) plus a `hex` hint — universally understood. `'oklch'` emits `[L, C, H]` components with no hex — Glaze-native, wide-gamut. |
222
- | `modes` | global config | Which scheme variants to include. `light` is always present. |
255
+ | Option | Default | Description |
256
+ | ------------ | ------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
257
+ | `colorSpace` | `'srgb'` | Color space for `$value`. `'srgb'` emits gamma sRGB `components` (0–1) plus a `hex` hint — universally understood. `'oklch'` emits `[L, C, H]` components with no hex — Glaze-native, wide-gamut. |
258
+ | `modes` | global config | Which scheme variants to include. `light` is always present. |
223
259
 
224
260
  `alpha` is included on `$value` only when the color's opacity is below 1. `$type` is always `'color'`.
225
261
 
@@ -228,7 +264,7 @@ Write each document to its own `.tokens.json` file — one file per scheme is th
228
264
  A single W3C [DTCG Resolver-Module](https://www.designtokens.org/) document describing **every scheme variant in one file** — an alternative to `dtcg()`'s per-scheme files for tools that resolve sets + modifiers (e.g. Dispersa). The light document becomes `sets.base.sources[0]` (the default context); each other variant becomes a context override on a single `scheme` modifier.
229
265
 
230
266
  ```ts
231
- theme.dtcgResolver({ modes: { highContrast: true } })
267
+ theme.dtcgResolver({ modes: { highContrast: true } });
232
268
  // → {
233
269
  // version: '2025.10',
234
270
  // sets: {
@@ -272,14 +308,14 @@ theme.dtcgResolver({ modes: { highContrast: true } })
272
308
 
273
309
  `GlazeDtcgResolverOptions` (extends `GlazeDtcgOptions`, so `modes` and `colorSpace` pass through):
274
310
 
275
- | Option | Default | Description |
276
- |---|---|---|
277
- | `colorSpace` | `'srgb'` | Same as `dtcg()` — flows through to every source and context. |
278
- | `modes` | global config | Which scheme variants to emit as contexts. `light` is always present (the default); absent variants are omitted. |
279
- | `setName` | `'base'` | Name of the single set holding the default (light) token tree. |
280
- | `modifierName` | `'scheme'` | Name of the modifier describing the scheme axis. |
281
- | `contextNames` | identity | Override the four context names (`light` / `dark` / `lightContrast` / `darkContrast`) — e.g. `{ dark: 'night' }`. |
282
- | `version` | `'2025.10'` | Resolver document version. |
311
+ | Option | Default | Description |
312
+ | -------------- | ------------- | ----------------------------------------------------------------------------------------------------------------- |
313
+ | `colorSpace` | `'srgb'` | Same as `dtcg()` — flows through to every source and context. |
314
+ | `modes` | global config | Which scheme variants to emit as contexts. `light` is always present (the default); absent variants are omitted. |
315
+ | `setName` | `'base'` | Name of the single set holding the default (light) token tree. |
316
+ | `modifierName` | `'scheme'` | Name of the modifier describing the scheme axis. |
317
+ | `contextNames` | identity | Override the four context names (`light` / `dark` / `lightContrast` / `darkContrast`) — e.g. `{ dark: 'night' }`. |
318
+ | `version` | `'2025.10'` | Resolver document version. |
283
319
 
284
320
  ### `theme.tailwind(options?)`
285
321
 
@@ -306,24 +342,63 @@ A Tailwind CSS v4 `@theme` block (light baseline) plus dark / high-contrast over
306
342
 
307
343
  `GlazeTailwindOptions`:
308
344
 
309
- | Option | Default | Description |
310
- |---|---|---|
311
- | `format` | `'oklch'` | Output color format for the values. |
312
- | `namespace` | `'color-'` | CSS custom property namespace, forming `--<namespace><name>` (e.g. `--color-surface`). Named `namespace` to avoid clashing with the palette theme-prefix option. |
313
- | `darkSelector` | `'.dark'` | Selector wrapping the dark overrides. Pass an at-rule like `'@media (prefers-color-scheme: dark)'` to drive dark mode from the OS preference (it nests `:root` automatically). |
314
- | `highContrastSelector` | `'.high-contrast'` | Selector wrapping the light high-contrast overrides. The combined dark + high-contrast block uses `${darkSelector}${highContrastSelector}` (e.g. `.dark.high-contrast`). |
315
- | `modes` | global config | Which scheme variants to include. The `@theme` block (light) is always emitted when colors exist. |
345
+ | Option | Default | Description |
346
+ | ---------------------- | ------------------ | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ |
347
+ | `format` | `'oklch'` | Output color format for the values. |
348
+ | `namespace` | `'color-'` | CSS custom property namespace, forming `--<namespace><name>` (e.g. `--color-surface`). Named `namespace` to avoid clashing with the palette theme-prefix option. |
349
+ | `darkSelector` | `'.dark'` | Selector wrapping the dark overrides. Pass an at-rule like `'@media (prefers-color-scheme: dark)'` to drive dark mode from the OS preference (it nests `:root` automatically). |
350
+ | `highContrastSelector` | `'.high-contrast'` | Selector wrapping the light high-contrast overrides. The combined dark + high-contrast block uses `${darkSelector}${highContrastSelector}` (e.g. `.dark.high-contrast`). |
351
+ | `modes` | global config | Which scheme variants to include. The `@theme` block (light) is always emitted when colors exist. |
316
352
 
317
- ### `theme.export()`
353
+ ### `theme.export(override?)`
318
354
 
319
355
  ```ts
320
356
  const snapshot = theme.export();
321
- // → { hue: 280, saturation: 80, colors: { surface: { ... }, ... } }
357
+ // → {
358
+ // kind: 'theme',
359
+ // version: 1,
360
+ // hue: 280,
361
+ // saturation: 80,
362
+ // colors: { surface: { ... }, ... },
363
+ // config: { lightTone: {...}, darkTone: {...}, pastel: false, ... },
364
+ // }
322
365
 
323
- const restored = glaze.from(snapshot);
366
+ const restored = glaze.themeFrom(snapshot);
324
367
  ```
325
368
 
326
- The export contains only the configuration not resolved color values. Resolved values are recomputed on demand.
369
+ Returns a deep-cloned, JSON-safe authoring snapshot (definitions + frozen
370
+ effective config — not resolved color strings). Freezes
371
+ `getConfig() ∪ instance local ∪ override` at call time. Restored themes pin
372
+ that freeze as their local override. Distinct from `theme.json()`, which
373
+ emits resolved color strings.
374
+
375
+ ---
376
+
377
+ ## High-contrast pairs
378
+
379
+ `HCPair<T>` means either one value used in both ordinary and high-contrast
380
+ schemes, or an explicit `[normal, highContrast]` pair:
381
+
382
+ ```ts
383
+ type HCPair<T> = T | [T, T];
384
+ ```
385
+
386
+ It is used by `tone`, `contrast`, shadow `intensity`, and mix `value`:
387
+
388
+ ```ts
389
+ tone: '-8'; // -8 in normal and HC
390
+ tone: ['-8', '-16']; // -8 normal, -16 HC
391
+ contrast: {
392
+ apca: 'content';
393
+ } // preset with automatic HC enhancement
394
+ contrast: {
395
+ apca: ['content', 'body'];
396
+ } // explicit normal/HC targets
397
+ ```
398
+
399
+ For `contrast`, the pair may wrap the whole spec or live inside the selected
400
+ metric. An explicit HC value disables automatic APCA enhancement or WCAG preset
401
+ promotion for that color.
327
402
 
328
403
  ---
329
404
 
@@ -337,34 +412,42 @@ type ColorDef = RegularColorDef | ShadowColorDef | MixColorDef;
337
412
 
338
413
  ### `RegularColorDef`
339
414
 
340
- | Field | Type | Description |
341
- |---|---|---|
342
- | `tone` | `HCPair<ToneValue>` | Number = absolute (0–100, contrast-uniform). `'+N'`/`'-N'` = relative to base's tone (requires `base`). `'max'`/`'min'` = forced to the scheme's tone extreme (no `base`). Optional HC pair `[normal, hc]`. |
343
- | `saturation` | `number` | Saturation factor applied to the seed saturation (0–1). Default: `1`. |
344
- | `hue` | `number \| RelativeValue` | Number = absolute (0–360). String (`'+N'`/`'-N'`) = relative to the **theme seed hue** (never to a base color). |
345
- | `base` | `string` | Name of another color in the same theme — makes this a *dependent* color. |
346
- | `contrast` | `HCPair<ContrastSpec>` | Contrast floor against `base`. Requires `base`. See [`contrast`](#contrast-floor). |
347
- | `mode` | `'auto' \| 'fixed' \| 'static'` | Adaptation mode. Default: `'auto'`. See [Adaptation modes](#adaptation-modes). |
348
- | `autoFlip` | `boolean` | Flip out-of-bounds results (relative `tone` overshoot / unmet `contrast`) to the opposite side instead of clamping. Default: the global `autoFlip` (`true`). See [`autoFlip`](#autoflip). |
349
- | `opacity` | `number` | Fixed alpha 0–1. Output includes alpha in the CSS value. Combining with `contrast` is not recommended (a `console.warn` is emitted). |
350
- | `pastel` | `boolean` | Per-color override for the hue-independent "safe" chroma limit used in OKHSL↔sRGB conversions (luminance, contrast solving, output formatting). Falls through to the global / per-theme `pastel` config when omitted. Default: unset. See [Per-color `pastel`](#per-color-pastel). |
351
- | `role` | `RoleInput` | Semantic role against `base` (`'text'` / `'surface'` / `'border'` or an alias). Fixes APCA contrast polarity. Resolved via: explicit `role` → name inference → opposite of the base's role → `'text'`. See [Roles](#roles). |
352
- | `inherit` | `boolean` | Whether this color is inherited by child themes via `extend()`. Default: `true`. Set to `false` to make the color local to the current theme. |
415
+ | Field | Type | Description |
416
+ | ------------ | ------------------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
417
+ | `tone` | `HCPair<ToneValue>` | Number = absolute (0–100). `'+N'`/`'-N'` = a signed **tone delta** from the base (requires `base`). `'max'`/`'min'` = forced to the scheme's tone extreme (no `base`). Optional HC pair `[normal, hc]`. |
418
+ | `saturation` | `number` | Saturation factor applied to the seed saturation (0–1). Default: `1`. |
419
+ | `hue` | `number \| RelativeValue` | Number = absolute (0–360). String (`'+N'`/`'-N'`) = relative to the **theme seed hue** (never to a base color). |
420
+ | `base` | `string` | Name of another color in the same theme — makes this a _dependent_ color. |
421
+ | `contrast` | `HCPair<ContrastSpec>` | Contrast floor against `base`. Requires `base`. See [`contrast`](#contrast-floor). |
422
+ | `mode` | `'auto' \| 'fixed' \| 'static'` | Adaptation mode. Default: `'auto'`. See [Adaptation modes](#adaptation-modes). |
423
+ | `autoFlip` | `boolean` | Flip out-of-bounds results (relative `tone` overshoot / unmet `contrast`) to the opposite side instead of clamping. Default: the global `autoFlip` (`true`). See [`autoFlip`](#autoflip). |
424
+ | `opacity` | `number` | Fixed alpha 0–1. Output includes alpha in the CSS value. Combining with `contrast` is not recommended (a `console.warn` is emitted). |
425
+ | `pastel` | `boolean` | Per-color override for the hue-independent "safe" chroma limit used in OKHSL↔sRGB conversions (luminance, contrast solving, output formatting). Falls through to the per-theme / per-token `pastel` override when omitted. Default: unset. See [Per-color `pastel`](#per-color-pastel). |
426
+ | `role` | `RoleInput` | Semantic role against `base` (`'text'` / `'surface'` / `'border'` or an alias). Fixes APCA contrast polarity. Resolved via: explicit `role` → name inference → opposite of the base's role → `'text'`. See [Roles](#roles). |
427
+ | `inherit` | `boolean` | Whether this color is inherited by child themes via `extend()`. Default: `true`. Set to `false` to make the color local to the current theme. |
353
428
 
354
429
  #### Tone values
355
430
 
356
- `tone` (0–100) replaces OKHSL lightness with a contrast-uniform axis — equal tone steps give equal WCAG contrast. See [`docs/okhst.md`](okhst.md) for the math. (To port old `lightness` values, see [migration.md](migration.md).)
431
+ `tone` (0–100) replaces authored OKHSL lightness with a contrast-shaped axis.
432
+ Equal tone differences give equal WCAG contrast for neutrals; chromatic results
433
+ can drift in measured luminance. See [OKHST in Glaze](okhst.md). To port old
434
+ `lightness` values, see [migration.md](migration.md).
357
435
 
358
- | Form | Example | Meaning |
359
- |---|---|---|
360
- | Number (absolute) | `tone: 45` | Absolute tone 0–100. |
361
- | String (relative) | `tone: '-52'` | Relative to base color's tone (requires `base`). |
362
- | Extreme | `tone: 'max'` / `'min'` | Force to the scheme's highest (`'max'` = 100) or lowest (`'min'` = 0) tone. No `base` needed. |
363
- | HC pair | `tone: ['-7', '-20']` | `[normal, high-contrast]`. A single value applies to both. |
436
+ | Form | Example | Meaning |
437
+ | ------------------- | ----------------------- | --------------------------------------------------------------------------------------------- |
438
+ | Number (absolute) | `tone: 45` | Absolute tone 0–100. |
439
+ | String (tone delta) | `tone: '-52'` | Signed difference from the base color's resolved tone (requires `base`). |
440
+ | Extreme | `tone: 'max'` / `'min'` | Force to the scheme's highest (`'max'` = 100) or lowest (`'min'` = 0) tone. No `base` needed. |
441
+ | HC pair | `tone: ['-7', '-20']` | `[normal, high-contrast]`. A single value applies to both. |
364
442
 
365
443
  **Absolute tone** on a dependent color (`base` set) positions the color independently. In dark mode it is tone-mapped (inverted + windowed) on its own. The `contrast` solver acts as a safety net.
366
444
 
367
- **Relative tone** applies a signed delta to the base color's resolved tone. Because tone is contrast-uniform, a fixed delta yields a fixed contrast step. In dark mode with `mode: 'auto'`, the offset is anchored to the base's per-scheme tone. If `base + delta` falls outside `[0, 100]`, the result is clamped to the boundary, or — with `autoFlip` (default on) — mirrored to the other side of the base.
445
+ **A tone delta** applies a signed difference to the base color's resolved tone.
446
+ It gives an exact contrast step for neutrals and a stable visual progression for
447
+ chromatic colors. In dark mode with `mode: 'auto'`, it is anchored to the
448
+ base's per-scheme tone. If `base + delta` falls outside `[0, 100]`, the result
449
+ is clamped to the boundary, or — with `autoFlip` (default on) — mirrored to the
450
+ other side of the base.
368
451
 
369
452
  **Extreme tone** (`'max'` / `'min'`) forces the color to the scheme's tone extreme without a contrast hack or a magic number. `'max'` resolves to author tone 100 and `'min'` to 0; both flow through scheme mapping like an absolute tone, so under `mode: 'auto'` they invert in dark (`'max'` is lightest in light, darkest in dark). Use `mode: 'static'` to pin the same extreme across schemes, or `mode: 'fixed'` to keep the same end without inverting. No `base` required.
370
453
 
@@ -384,30 +467,42 @@ A dependent color with `base` but no `tone` inherits the base's tone (equivalent
384
467
  ```ts
385
468
  type ContrastPreset = 'AA' | 'AAA' | 'AA-large' | 'AAA-large';
386
469
  type ContrastSpec =
387
- | number // bare WCAG ratio
388
- | ContrastPreset // named WCAG preset
470
+ | number // bare WCAG ratio
471
+ | ContrastPreset // named WCAG preset
389
472
  | { wcag: HCPair<number | ContrastPreset> }
390
- | { apca: HCPair<number> }; // APCA Lc target
473
+ | { apca: HCPair<number> }; // APCA Lc target
391
474
  ```
392
475
 
393
- | Preset | WCAG ratio |
394
- |---|---|
395
- | `'AA-large'` | 3 |
396
- | `'AA'` | 4.5 |
397
- | `'AAA-large'` | 4.5 |
398
- | `'AAA'` | 7 |
476
+ | Preset | WCAG ratio |
477
+ | ------------- | ---------- |
478
+ | `'AA-large'` | 3 |
479
+ | `'AA'` | 4.5 |
480
+ | `'AAA-large'` | 4.5 |
481
+ | `'AAA'` | 7 |
399
482
 
400
483
  A bare number or preset means **WCAG**. Use `{ wcag }` / `{ apca }` to pick the metric explicitly. The `[normal, highContrast]` pair may live at the outer level (`[4.5, 7]`, `[{ wcag: 4.5 }, { wcag: 7 }]`) or inside the metric (`{ wcag: [4.5, 7] }`, `{ apca: [45, 60] }`).
401
484
 
402
485
  ```ts
403
- contrast: 4.5 // WCAG 4.5
404
- contrast: 'AAA' // WCAG 7
405
- contrast: { wcag: 6 } // WCAG 6
406
- contrast: { wcag: [4.5, 7] } // WCAG 4.5 normal / 7 high-contrast (explicit)
407
- contrast: { apca: 60 } // APCA Lc 60 normal / 75 high-contrast (auto)
408
- contrast: { apca: [45, 60] } // APCA Lc 45 normal / 60 high-contrast (explicit)
409
- contrast: { apca: 'content' } // APCA preset -> Lc 60 normal / 75 high-contrast (auto)
410
- contrast: { apca: ['content', 'body'] } // Lc 60 normal / 75 high-contrast (explicit)
486
+ contrast: 4.5; // WCAG 4.5
487
+ contrast: 'AAA'; // WCAG 7
488
+ contrast: {
489
+ wcag: 6;
490
+ } // WCAG 6
491
+ contrast: {
492
+ wcag: [4.5, 7];
493
+ } // WCAG 4.5 normal / 7 high-contrast (explicit)
494
+ contrast: {
495
+ apca: 60;
496
+ } // APCA Lc 60 normal / 75 high-contrast (auto)
497
+ contrast: {
498
+ apca: [45, 60];
499
+ } // APCA Lc 45 normal / 60 high-contrast (explicit)
500
+ contrast: {
501
+ apca: 'content';
502
+ } // APCA preset -> Lc 60 normal / 75 high-contrast (auto)
503
+ contrast: {
504
+ apca: ['content', 'body'];
505
+ } // Lc 60 normal / 75 high-contrast (explicit)
411
506
  ```
412
507
 
413
508
  **WCAG HC auto-promotion:** a bare WCAG preset (no `[normal, hc]` pair at either
@@ -424,30 +519,30 @@ boosted by **+15 Lc** in high-contrast mode, the APCA analog of WCAG's
424
519
  AAA-over-AA step. On by default; an explicit HC value via either pair
425
520
  overrides it and skips the boost. The enhanced target is clamped to 106 Lc.
426
521
  For large/bold text (where APCA caps contrast at Lc 90 to avoid glare), pass
427
- an explicit HC pair to hold that ceiling — see
428
- [`docs/okhst.md`](okhst.md) §Enhanced Level.
522
+ an explicit HC pair to hold that ceiling.
429
523
 
430
524
  APCA preset keywords (Bronze Simple Mode conformance levels, role-independent):
431
525
  `'preferred'` (Lc 90), `'body'` (75), `'content'` (60, ~AA), `'large'` (45, ~3:1),
432
- `'non-text'` (30), `'min'` (15, point of invisibility). See
433
- [`docs/okhst.md`](okhst.md) §APCA.
526
+ `'non-text'` (30), `'min'` (15, point of invisibility).
434
527
 
435
- The floor is applied independently per scheme if the `tone` already satisfies it the tone is kept, otherwise the solver searches in tone (contrast-uniform → a closed-form WCAG seed and fast convergence) until the target is met.
528
+ The floor is applied independently per scheme. If the preferred `tone` already
529
+ satisfies it, the tone is kept; otherwise the solver uses the tone-shaped scale
530
+ for a closed-form WCAG seed and fast search until the target is met.
436
531
 
437
532
  By default, the solver crosses to the opposite side of the base color when the requested tone direction cannot satisfy the floor. This is controlled per-color by [`autoFlip`](#autoflip) (which defaults to the global `autoFlip`). Set `glaze.configure({ autoFlip: false })` — or `autoFlip: false` on a single color — to keep strict directionality: unmet colors pin to that direction's 0 or 100 tone extreme instead of falling back to the original requested value.
438
533
 
439
534
  **Full tone spectrum in HC mode:** in high-contrast variants the `lightTone` and `darkTone` window constraints are bypassed entirely (the window is forced to `[0, 100]`). Colors can reach the full range, maximizing perceivable contrast.
440
535
 
441
- **Chromatic drift (verification):** tone is contrast-uniform for grays. A chromatic swatch at a given tone shares its OKHSL lightness with the equivalent gray but drifts in real luminance, so a contrast-floored color may land slightly under its gray-tone expectation. Glaze measures the resolved result against the base and emits a deduped advisory `console.warn` when it drifts below the target. See [`docs/okhst.md`](okhst.md) §Verification.
536
+ **Chromatic drift (verification):** tone is contrast-uniform for grays. A chromatic swatch at a given tone shares its OKHSL lightness with the equivalent gray but drifts in real luminance, so a contrast-floored color may land slightly under its gray-tone expectation. Glaze measures the resolved result against the base and emits a deduped advisory `console.warn` when it drifts below the target. See [Contrast verification](okhst.md#contrast-verification).
442
537
 
443
538
  #### Per-color hue override
444
539
 
445
540
  ```ts
446
541
  const theme = glaze(280, 80);
447
542
  theme.colors({
448
- surface: { tone: 97 },
449
- gradientEnd: { tone: 90, hue: '+20' }, // 280 + 20 = 300
450
- warning: { tone: 60, hue: 40 }, // absolute
543
+ surface: { tone: 97 },
544
+ gradientEnd: { tone: 90, hue: '+20' }, // 280 + 20 = 300
545
+ warning: { tone: 60, hue: 40 }, // absolute
451
546
  });
452
547
  ```
453
548
 
@@ -455,19 +550,19 @@ Relative hue is always relative to the **theme seed hue**, not to a base color.
455
550
 
456
551
  #### Per-color `pastel`
457
552
 
458
- `pastel: true` on a single color def overrides the global / per-theme `pastel` config for that color only. It toggles the hue-independent "safe" chroma limit used in every OKHSL↔sRGB conversion that touches this color: luminance calculations during contrast solving, gamut clamping during sRGB blend / mix edges, and output formatting. The effective flag is carried on the resolved variant (`ResolvedColorVariant.pastel`) so formatting matches the gamut mapping applied during resolution.
553
+ `pastel: true` on a single color def overrides the per-theme / per-token `pastel` override for that color only. It toggles the hue-independent "safe" chroma limit used in every OKHSL↔sRGB conversion that touches this color: luminance calculations during contrast solving, gamut clamping during sRGB blend / mix edges, and output formatting. The effective flag is carried on the resolved variant (`ResolvedColorVariant.pastel`) so formatting matches the gamut mapping applied during resolution.
459
554
 
460
555
  ```ts
461
556
  const theme = glaze(280, 80);
462
557
  theme.colors({
463
558
  plain: { tone: 50, saturation: 1 },
464
- soft: { tone: 50, saturation: 1, pastel: true },
559
+ soft: { tone: 50, saturation: 1, pastel: true },
465
560
  });
466
561
  // theme.resolve().get('soft')!.light.pastel === true
467
562
  // theme.css().light contains different rgb() triples for `--plain` and `--soft`
468
563
  ```
469
564
 
470
- Omit the field to inherit the global / per-theme `pastel` config — useful for keeping the default behavior while opting a single accent into the pastel gamut.
565
+ Omit the field to inherit the theme/token `pastel` override (default `false`) — useful for keeping the default behavior while opting a single accent into the pastel gamut.
471
566
 
472
567
  The flag is part of the def object, so `extend()` copies it through to child themes alongside the rest of the def. Override it again on the child to flip a single color back:
473
568
 
@@ -489,11 +584,11 @@ const child = parent.extend({
489
584
 
490
585
  A color's `role` describes how it is used against its `base` and fixes **APCA contrast polarity** — which side is the foreground vs the background. APCA is asymmetric (`|apca(a,b)| ≠ |apca(b,a)|`), so the role picks the correct argument order; WCAG is symmetric and unaffected.
491
586
 
492
- | Role | Polarity | Use | Aliases (name inference) |
493
- |---|---|---|---|
494
- | `'text'` | fg | Text / icons / foreground content | `text`, `fg`, `foreground`, `content`, `ink`, `label`, `stroke` |
495
- | `'border'` | fg | Non-text spot elements (borders, dividers, outlines) | `border`, `divider`, `outline`, `separator`, `hairline`, `rule` |
496
- | `'surface'` | bg | Backgrounds / fills | `surface`, `bg`, `background`, `fill`, `canvas`, `paper`, `layer` |
587
+ | Role | Polarity | Use | Aliases (name inference) |
588
+ | ----------- | -------- | ---------------------------------------------------- | ----------------------------------------------------------------- |
589
+ | `'text'` | fg | Text / icons / foreground content | `text`, `fg`, `foreground`, `content`, `ink`, `label`, `stroke` |
590
+ | `'border'` | fg | Non-text spot elements (borders, dividers, outlines) | `border`, `divider`, `outline`, `separator`, `hairline`, `rule` |
591
+ | `'surface'` | bg | Backgrounds / fills | `surface`, `bg`, `background`, `fill`, `canvas`, `paper`, `layer` |
497
592
 
498
593
  Resolution chain (per color):
499
594
 
@@ -506,8 +601,8 @@ Resolution chain (per color):
506
601
  const theme = glaze(280, 60);
507
602
  theme.colors({
508
603
  surface: { tone: 90 },
509
- text: { base: 'surface', contrast: { apca: 'content' } }, // inferred text
510
- border: { base: 'surface', tone: '-10' }, // inferred border
604
+ text: { base: 'surface', contrast: { apca: 'content' } }, // inferred text
605
+ border: { base: 'surface', tone: '-10' }, // inferred border
511
606
  });
512
607
  // role fixes APCA polarity; set `pastel: true` explicitly if a border
513
608
  // needs the hue-independent safe chroma limit.
@@ -517,32 +612,32 @@ Disable name inference with `glaze.configure({ inferRole: false })` (the base-op
517
612
 
518
613
  ### `ShadowColorDef`
519
614
 
520
- | Field | Type | Description |
521
- |---|---|---|
522
- | `type` | `'shadow'` | Discriminator. |
523
- | `bg` | `string` | Background color name — must reference a non-shadow color in the same theme. |
524
- | `fg` | `string` | Optional foreground color name for tinting and intensity modulation. Must reference a non-shadow color. Omit for an achromatic shadow at full user-specified intensity. |
525
- | `intensity` | `HCPair<number>` | Shadow intensity, 0–100. Supports HC pairs. |
526
- | `tuning` | `ShadowTuning` | Per-color tuning overrides. Merged field-by-field with the global `shadowTuning`. |
527
- | `pastel` | `boolean` | Per-color `pastel` override. See [Per-color `pastel`](#per-color-pastel). |
528
- | `inherit` | `boolean` | Inheritance flag, default `true`. |
615
+ | Field | Type | Description |
616
+ | ----------- | ---------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
617
+ | `type` | `'shadow'` | Discriminator. |
618
+ | `bg` | `string` | Background color name — must reference a non-shadow color in the same theme. |
619
+ | `fg` | `string` | Optional foreground color name for tinting and intensity modulation. Must reference a non-shadow color. Omit for an achromatic shadow at full user-specified intensity. |
620
+ | `intensity` | `HCPair<number>` | Shadow intensity, 0–100. Supports HC pairs. |
621
+ | `tuning` | `ShadowTuning` | Per-color tuning overrides. Merged field-by-field with the global `shadowTuning`. |
622
+ | `pastel` | `boolean` | Per-color `pastel` override. See [Per-color `pastel`](#per-color-pastel). |
623
+ | `inherit` | `boolean` | Inheritance flag, default `true`. |
529
624
 
530
625
  See [Shadows](#shadows) below for the algorithm and tuning details.
531
626
 
532
627
  ### `MixColorDef`
533
628
 
534
- | Field | Type | Description |
535
- |---|---|---|
536
- | `type` | `'mix'` | Discriminator. |
537
- | `base` | `string` | "From" color name. |
538
- | `target` | `string` | "To" color name. |
539
- | `value` | `HCPair<number>` | Mix ratio 0–100 (0 = pure base, 100 = pure target). In `'transparent'` blend, this becomes the target's opacity. Supports HC pairs. |
540
- | `blend` | `'opaque' \| 'transparent'` | Default `'opaque'`. |
541
- | `space` | `'okhsl' \| 'srgb'` | Interpolation space for opaque blending. Default `'okhsl'`. Ignored for `'transparent'` (always composites in linear sRGB). |
542
- | `contrast` | `HCPair<ContrastSpec>` | Optional contrast floor against `base` (WCAG or APCA — see [`contrast`](#contrast-floor)). The solver adjusts the mix ratio (opaque) or opacity (transparent). |
543
- | `pastel` | `boolean` | Per-color `pastel` override. See [Per-color `pastel`](#per-color-pastel). |
544
- | `role` | `RoleInput` | Semantic role of the mixed result against `base`. Same semantics as `RegularColorDef.role` (see [Roles](#roles)). |
545
- | `inherit` | `boolean` | Inheritance flag, default `true`. |
629
+ | Field | Type | Description |
630
+ | ---------- | --------------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------- |
631
+ | `type` | `'mix'` | Discriminator. |
632
+ | `base` | `string` | "From" color name. |
633
+ | `target` | `string` | "To" color name. |
634
+ | `value` | `HCPair<number>` | Mix ratio 0–100 (0 = pure base, 100 = pure target). In `'transparent'` blend, this becomes the target's opacity. Supports HC pairs. |
635
+ | `blend` | `'opaque' \| 'transparent'` | Default `'opaque'`. |
636
+ | `space` | `'okhsl' \| 'srgb'` | Interpolation space for opaque blending. Default `'okhsl'`. Ignored for `'transparent'` (always composites in linear sRGB). |
637
+ | `contrast` | `HCPair<ContrastSpec>` | Optional contrast floor against `base` (WCAG or APCA — see [`contrast`](#contrast-floor)). The solver adjusts the mix ratio (opaque) or opacity (transparent). |
638
+ | `pastel` | `boolean` | Per-color `pastel` override. See [Per-color `pastel`](#per-color-pastel). |
639
+ | `role` | `RoleInput` | Semantic role of the mixed result against `base`. Same semantics as `RegularColorDef.role` (see [Roles](#roles)). |
640
+ | `inherit` | `boolean` | Inheritance flag, default `true`. |
546
641
 
547
642
  See [Mix colors](#mix-colors) below.
548
643
 
@@ -562,143 +657,149 @@ glaze.color(color: GlazeFromInput | GlazeColorInput | GlazeColorValue, config?:
562
657
 
563
658
  `glaze.color()` accepts **four input shapes**, discriminated by structure:
564
659
 
565
- | Shape | Example | Notes |
566
- |---|---|---|
567
- | **Bare string** | `'#26fcb2'` | Hex or CSS color function (`rgb()`, `hsl()`, `okhsl()`, `okhst()`, `oklch()`). |
568
- | **Value object** | `{ h: 152, s: 0.95, l: 0.74 }` | OKHSL, OKHST (`{ h, s, t }`), `{ r, g, b }` (sRGB 0–255), or `{ l, c, h }` (OKLCh). |
569
- | **`{ from, ...overrides }`** | `{ from: '#1a1a2e', base: bg, contrast: 'AA' }` | Value + color overrides in one object. |
570
- | **Structured** | `{ hue: 152, saturation: 95, tone: 74 }` | Full theme-style token (hue/saturation in 0–100, tone in 0–100). |
660
+ | Shape | Example | Notes |
661
+ | ---------------------------- | ----------------------------------------------- | ----------------------------------------------------------------------------------- |
662
+ | **Bare string** | `'#26fcb2'` | Hex or CSS color function (`rgb()`, `hsl()`, `okhsl()`, `okhst()`, `oklch()`). |
663
+ | **Value object** | `{ h: 152, s: 0.95, l: 0.74 }` | OKHSL, OKHST (`{ h, s, t }`), `{ r, g, b }` (sRGB 0–255), or `{ l, c, h }` (OKLCh). |
664
+ | **`{ from, ...overrides }`** | `{ from: '#1a1a2e', base: bg, contrast: 'AA' }` | Value + color overrides in one object. |
665
+ | **Structured** | `{ hue: 152, saturation: 95, tone: 74 }` | Full theme-style token (hue/saturation in 0–100, tone in 0–100). |
571
666
 
572
667
  `GlazeColorValue` (bare string or value-object forms) accepts:
573
668
 
574
- | Form | Example | Notes |
575
- |---|---|---|
576
- | Hex | `'#26fcb2'`, `'#26fcb2ff'`, `'#abc'` | 3, 6, or 8 digits. Alpha is dropped with a `console.warn` — use `opacity` instead. |
577
- | `rgb()` | `'rgb(38 252 178)'`, `'rgb(38 252 178 / 0.8)'` | Modern space syntax. Alpha dropped with warning. |
578
- | `hsl()` | `'hsl(152 97% 57%)'` | Modern space syntax. Alpha dropped with warning. |
579
- | `okhsl()` | `'okhsl(152 95% 74%)'` | Glaze's own emit format. Alpha dropped with warning. |
580
- | `okhst()` | `'okhst(152 95% 70%)'` | OKHST tone input (third value is tone 0–100). **Input only** never emitted. Alpha dropped with warning. |
581
- | `oklch()` | `'oklch(0.85 0.18 152)'` | Glaze's own emit format. Alpha dropped with warning. |
582
- | `OkhslColor` object | `{ h: 152, s: 0.95, l: 0.74 }` | OKHSL shape (h: 0–360, s/l: 0–1). Passing 0–100 for `s`/`l` throws with a hint to use the structured form. |
583
- | `OkhstColor` object | `{ h: 152, s: 0.95, t: 0.70 }` | OKHST shape (h: 0–360, s/t: 0–1). The `t` key disambiguates it from `{ h, s, l }`. **Input only.** |
584
- | `RgbColor` object | `{ r: 38, g: 252, b: 178 }` | sRGB 0–255. RGB tuple `[r, g, b]` is not supported — use this object form. |
585
- | `OklchColor` object | `{ l: 0.85, c: 0.18, h: 152 }` | OKLCh (L/C: 0–1, H: degrees), same semantics as `oklch()` strings. |
669
+ | Form | Example | Notes |
670
+ | ------------------- | ---------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------- |
671
+ | Hex | `'#26fcb2'`, `'#26fcb2ff'`, `'#abc'` | 3, 6, or 8 digits. Alpha is dropped with a `console.warn` — use `opacity` instead. |
672
+ | `rgb()` | `'rgb(38 252 178)'`, `'rgb(38 252 178 / 0.8)'` | Modern space syntax. Alpha dropped with warning. |
673
+ | `hsl()` | `'hsl(152 97% 57%)'` | Modern space syntax. Alpha dropped with warning. |
674
+ | `okhsl()` | `'okhsl(152 95% 74%)'` | Glaze's own emit format. Alpha dropped with warning. |
675
+ | `okhst()` | `'okhst(152 95% 70%)'` | OKHST tone input (third value is tone 0–100). Not native CSS; it can also be serialized by [Tasty](https://tasty.style) exports. Alpha dropped with warning. |
676
+ | `oklch()` | `'oklch(0.85 0.18 152)'` | Glaze's own emit format. Alpha dropped with warning. |
677
+ | `OkhslColor` object | `{ h: 152, s: 0.95, l: 0.74 }` | OKHSL shape (h: 0–360, s/l: 0–1). Passing 0–100 for `s`/`l` throws with a hint to use the structured form. |
678
+ | `OkhstColor` object | `{ h: 152, s: 0.95, t: 0.70 }` | Direct OKHST input shape (h: 0–360, s/t: 0–1). The `t` key disambiguates it from `{ h, s, l }`. |
679
+ | `RgbColor` object | `{ r: 38, g: 252, b: 178 }` | sRGB 0–255. RGB tuple `[r, g, b]` is not supported — use this object form. |
680
+ | `OklchColor` object | `{ l: 0.85, c: 0.18, h: 152 }` | OKLCh (L/C: 0–1, H: degrees), same semantics as `oklch()` strings. |
586
681
 
587
682
  `GlazeColorInput` (structured form) is `{ hue, saturation, tone, ... }`:
588
683
 
589
- | Field | Type | Description |
590
- |---|---|---|
591
- | `hue` | `number` | 0–360. |
592
- | `saturation` | `number` | 0–100. |
593
- | `tone` | `HCPair<number \| ExtremeValue>` | 0–100 (contrast-uniform) or `'max'`/`'min'`, optional HC pair. |
594
- | `saturationFactor` | `number` | Multiplier on the seed (0–1). Default: `1`. |
595
- | `mode` | `AdaptationMode` | Default: `'auto'`. |
596
- | `autoFlip` | `boolean` | Flip out-of-bounds results instead of clamping. Default: global `autoFlip`. |
597
- | `opacity` | `number` | Fixed alpha 0–1. |
598
- | `base` | `GlazeColorToken \| GlazeColorValue` | Optional dependency. See [Pairing colors](#pairing-colors). |
599
- | `contrast` | `HCPair<ContrastSpec>` | Contrast floor against `base` (WCAG or APCA). Without `base`, anchored to the literal seed. |
600
- | `pastel` | `boolean` | Per-color `pastel` override. Falls through to the global / per-theme `pastel` config when omitted. See [Per-color `pastel`](#per-color-pastel). |
601
- | `role` | `RoleInput` | Semantic role against `base` / the seed (see [Roles](#roles)). Fixes APCA polarity. |
602
- | `name` | `string` | Debug label for warnings; doesn't change output keys. Reserved names (`'value'`, `'seed'`, `'externalBase'`) are rejected. |
684
+ | Field | Type | Description |
685
+ | ------------------ | ------------------------------------ | ----------------------------------------------------------------------------------------------------------------------------------------------- |
686
+ | `hue` | `number` | 0–360. |
687
+ | `saturation` | `number` | 0–100. |
688
+ | `tone` | `HCPair<number \| ExtremeValue>` | 0–100 (contrast-shaped) or `'max'`/`'min'`, optional HC pair. |
689
+ | `saturationFactor` | `number` | Multiplier on the seed (0–1). Default: `1`. |
690
+ | `mode` | `AdaptationMode` | Default: `'auto'`. |
691
+ | `autoFlip` | `boolean` | Flip out-of-bounds results instead of clamping. Default: global `autoFlip`. |
692
+ | `opacity` | `number` | Fixed alpha 0–1. |
693
+ | `base` | `GlazeColorToken \| GlazeColorValue` | Optional dependency. See [Pairing colors](#pairing-colors). |
694
+ | `contrast` | `HCPair<ContrastSpec>` | Contrast floor against `base` (WCAG or APCA). Without `base`, anchored to the literal seed. |
695
+ | `pastel` | `boolean` | Per-color `pastel` override. Falls through to the per-theme / per-token `pastel` override when omitted. See [Per-color `pastel`](#per-color-pastel). |
696
+ | `role` | `RoleInput` | Semantic role against `base` / the seed (see [Roles](#roles)). Fixes APCA polarity. |
697
+ | `name` | `string` | Debug label for warnings; doesn't change output keys. Reserved names (`'value'`, `'seed'`, `'externalBase'`) are rejected. |
603
698
 
604
699
  `GlazeFromInput` (from form) is `{ from: GlazeColorValue, ...colorOverrides }`:
605
700
 
606
- | Field | Notes |
607
- |---|---|
608
- | `from` | **Required.** The source color value — same forms as `GlazeColorValue`. |
609
- | `hue` | Number (absolute 0–360) or `'+N'`/`'-N'` (relative to seed, never to `base`). |
610
- | `saturation` | Override seed saturation (0–100). |
611
- | `tone` | Number (absolute 0–100), `'+N'`/`'-N'`, or `'max'`/`'min'`. Without `base`, relative anchors to the seed; with `base`, anchors to `base`'s tone per scheme. |
612
- | `saturationFactor` | Multiplier on the seed (0–1). |
613
- | `mode` | `'auto'` (default) / `'fixed'` / `'static'`. |
614
- | `autoFlip` | Flip out-of-bounds results instead of clamping. Default: global `autoFlip`. |
615
- | `contrast` | Contrast floor (WCAG or APCA). Without `base`, anchored to the literal seed; with `base`, solved per scheme. |
616
- | `base` | `GlazeColorToken` or raw `GlazeColorValue`. See [Pairing colors](#pairing-colors). |
617
- | `opacity` | Fixed alpha 0–1. Combining with `contrast` is not recommended — `console.warn` is emitted. |
618
- | `pastel` | Per-color `pastel` override. Falls through to the global / per-theme `pastel` config when omitted. See [Per-color `pastel`](#per-color-pastel). |
619
- | `role` | Semantic role against `base` / the seed (see [Roles](#roles)). Fixes APCA polarity. |
620
- | `name` | Debug label only — surfaces in warnings/errors. Does not change output keys. |
701
+ | Field | Notes |
702
+ | ------------------ | ----------------------------------------------------------------------------------------------------------------------------------------------------------- |
703
+ | `from` | **Required.** The source color value — same forms as `GlazeColorValue`. |
704
+ | `hue` | Number (absolute 0–360) or `'+N'`/`'-N'` (relative to seed, never to `base`). |
705
+ | `saturation` | Override seed saturation (0–100). |
706
+ | `tone` | Number (absolute 0–100), `'+N'`/`'-N'`, or `'max'`/`'min'`. Without `base`, relative anchors to the seed; with `base`, anchors to `base`'s tone per scheme. |
707
+ | `saturationFactor` | Multiplier on the seed (0–1). |
708
+ | `mode` | `'auto'` (default) / `'fixed'` / `'static'`. |
709
+ | `autoFlip` | Flip out-of-bounds results instead of clamping. Default: global `autoFlip`. |
710
+ | `contrast` | Contrast floor (WCAG or APCA). Without `base`, anchored to the literal seed; with `base`, solved per scheme. |
711
+ | `base` | `GlazeColorToken` or raw `GlazeColorValue`. See [Pairing colors](#pairing-colors). |
712
+ | `opacity` | Fixed alpha 0–1. Combining with `contrast` is not recommended — `console.warn` is emitted. |
713
+ | `pastel` | Per-color `pastel` override. Falls through to the per-theme / per-token `pastel` override when omitted. See [Per-color `pastel`](#per-color-pastel). |
714
+ | `role` | Semantic role against `base` / the seed (see [Roles](#roles)). Fixes APCA polarity. |
715
+ | `name` | Debug label only — surfaces in warnings/errors. Does not change output keys. |
621
716
 
622
717
  Named CSS colors (`'red'`, `'blueviolet'`) are not supported.
623
718
 
624
719
  ### Defaults
625
720
 
626
- Every input form defaults to `mode: 'auto'` so the resolved token adapts between light and dark like an ordinary theme color. The config snapshot taken at create time differs by input form:
721
+ Every input form defaults to `mode: 'auto'` so the resolved token adapts between light and dark like an ordinary theme color. Tokens store a **sparse local** config override; omitted fields fall through to the live global at resolve time (same as themes). Authoring `.export(override?)` freezes `getConfig() ∪ local ∪ override` at call time.
627
722
 
628
723
  - **Value-shorthand** (bare strings, value objects, and `{ from, ...overrides }`):
629
- - Light variant preserves the input tone exactly (`lightTone: false`).
630
- - All other config fields (`darkTone`, `darkDesaturation`, `autoFlip`) snapshot from `globalConfig` at create time.
724
+ - Light variant preserves the input tone exactly (`lightTone: false` as a local default).
725
+ - Other omitted fields track the live global config.
631
726
  - **Structured input** (`{ hue, saturation, tone, ... }`):
632
- - Both tone windows snapshot from `globalConfig` at create time (same as a theme color).
633
- - All fields are **snapshotted at color-creation time** later `glaze.configure()` calls don't retroactively change existing tokens.
727
+ - Omitted tone windows and other fields track the live global config (same as a theme color).
728
+ - `pastel` is instance-only set via the config override or per-color `pastel`, not `glaze.configure()`.
634
729
 
635
730
  ```ts
636
731
  // Bare string — adapts automatically
637
- glaze.color('#26fcb2')
732
+ glaze.color('#26fcb2');
638
733
 
639
734
  // Value-object — same behavior
640
- glaze.color({ h: 152, s: 0.95, l: 0.74 })
735
+ glaze.color({ h: 152, s: 0.95, l: 0.74 });
641
736
 
642
737
  // OKHST value-object — tone axis
643
- glaze.color({ h: 152, s: 0.95, t: 0.70 })
738
+ glaze.color({ h: 152, s: 0.95, t: 0.7 });
644
739
 
645
740
  // From form — value + color overrides
646
- glaze.color({ from: '#1a1a2e', hue: '+20', contrast: 'AA' })
741
+ glaze.color({ from: '#1a1a2e', hue: '+20', contrast: 'AA' });
647
742
 
648
743
  // Structured form — explicit hue/saturation/tone (0–100)
649
- glaze.color({ hue: 152, saturation: 95, tone: 74 })
744
+ glaze.color({ hue: 152, saturation: 95, tone: 74 });
650
745
  ```
651
746
 
652
747
  ### Token methods
653
748
 
654
749
  A `GlazeColorToken` exposes:
655
750
 
656
- | Method | Description |
657
- |---|---|
658
- | `token.resolve()` | Resolve as a `ResolvedColor` (light/dark/lightContrast/darkContrast variants). |
659
- | `token.token(options?)` | Flat token map (no color-name key). Options: `format`, `modes`, `states`. |
660
- | `token.tasty(options?)` | [Tasty](https://tasty.style) state map (no color-name key). Same options as `token.token`. |
661
- | `token.json(options?)` | JSON map (no color-name key). Options: `format`, `modes`. |
751
+ | Method | Description |
752
+ | --------------------------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
753
+ | `token.resolve()` | Resolve as a `ResolvedColor` (light/dark/lightContrast/darkContrast variants). |
754
+ | `token.token(options?)` | Flat token map (no color-name key). Options: `format`, `modes`, `states`. |
755
+ | `token.tasty(options?)` | [Tasty](https://tasty.style) state map (no color-name key). Same options as `token.token`. |
756
+ | `token.json(options?)` | JSON map (no color-name key). Options: `format`, `modes`. |
662
757
  | `token.css({ name, format?, suffix? })` | CSS custom property declarations grouped by scheme variant. `name` is **required** and becomes the variable identifier (`'brand'` → `--brand-color`). Defaults: `format: 'rgb'`, `suffix: '-color'` (matches `theme.css`). |
663
- | `token.dtcg(options?)` | DTCG color tokens, one per scheme variant (no color-name key). Each entry is a full `{ $type: 'color', $value }` token. Options: `colorSpace` (`'srgb'` \| `'oklch'`), `modes`. |
664
- | `token.dtcgResolver({ name, ... })` | A single DTCG Resolver-Module document for this color, keyed by `name` across all scheme variants. `name` is **required**. Same options as `theme.dtcgResolver()` plus `name`. |
665
- | `token.tailwind({ name, ... })` | Tailwind v4 `@theme` block + dark / high-contrast overrides for this color. `name` is **required** (forms `--color-<name>`). Same options as `theme.tailwind()` plus `name`. |
666
- | `token.export()` | JSON-safe snapshot — pass to `glaze.colorFrom(...)` to rehydrate. |
758
+ | `token.dtcg(options?)` | DTCG color tokens, one per scheme variant (no color-name key). Each entry is a full `{ $type: 'color', $value }` token. Options: `colorSpace` (`'srgb'` \| `'oklch'`), `modes`. |
759
+ | `token.dtcgResolver({ name, ... })` | A single DTCG Resolver-Module document for this color, keyed by `name` across all scheme variants. `name` is **required**. Same options as `theme.dtcgResolver()` plus `name`. |
760
+ | `token.tailwind({ name, ... })` | Tailwind v4 `@theme` block + dark / high-contrast overrides for this color. `name` is **required** (forms `--color-<name>`). Same options as `theme.tailwind()` plus `name`. |
761
+ | `token.export(override?)` | JSON-safe snapshot — freezes effective config at call time; pass to `glaze.colorFrom(...)` to rehydrate. Optional `override` merges over the instance local (and nested `base` exports). |
667
762
 
668
763
  ### Per-instance config override
669
764
 
670
- The optional `config` second argument (`GlazeConfigOverride`) overrides the resolve-relevant global config fields for a single token or theme. Fields that are omitted fall through to the live global config at create time (and are snapshotted). A tone window can be `[lo, hi]`, `{ lo, hi, eps }`, or `false` (disable clamping).
765
+ The optional `config` argument (`GlazeConfigOverride`) overrides
766
+ resolve-relevant fields for a token or theme. A tone window can be
767
+ `[lo, hi]`, `{ lo, hi, eps }`, or `false` (full range). Both themes and
768
+ standalone tokens keep a sparse local override — omitted fields (except
769
+ instance-only `pastel`) fall through to the live global at resolve time.
671
770
 
672
771
  `GlazeConfigOverride`:
673
772
 
674
- | Field | Default (from global) | Description |
675
- |---|---|---|
676
- | `lightTone` | `[10, 100]` | Light tone window: `[lo, hi]`, `{ lo, hi, eps }`, or `false` (disable clamping). |
677
- | `darkTone` | `[15, 95]` | Dark tone window: `[lo, hi]`, `{ lo, hi, eps }`, or `false` (disable clamping). |
678
- | `darkDesaturation` | `0.1` | Saturation reduction in dark scheme (0–1). |
679
- | `autoFlip` | `true` | Default for each color's `autoFlip`: when solving `contrast` (or applying a relative `tone` that overshoots), allow crossing to the opposite side instead of clamping. |
680
- | `shadowTuning` | `undefined` | Default shadow tuning (meaningful for themes; harmless on color tokens). |
773
+ | Field | Default (from global / fixed) | Description |
774
+ | ------------------ | ----------------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
775
+ | `lightTone` | `[10, 100]` | Light tone window: `[lo, hi]`, `{ lo, hi, eps }`, or `false` (disable clamping). |
776
+ | `darkTone` | `[15, 95]` | Dark tone window: `[lo, hi]`, `{ lo, hi, eps }`, or `false` (disable clamping). |
777
+ | `darkDesaturation` | `0.1` | Saturation reduction in dark scheme (0–1). |
778
+ | `autoFlip` | `true` | Default for each color's `autoFlip`: when solving `contrast` (or applying a relative `tone` that overshoots), allow crossing to the opposite side instead of clamping. |
779
+ | `pastel` | `false` (instance-only) | Theme/token-level pastel default for colors that omit per-color `pastel`. Not available on `glaze.configure()`. |
780
+ | `inferRole` | `true` | Infer color `role` from the name when unset. |
781
+ | `shadowTuning` | `undefined` | Default shadow tuning (meaningful for themes; harmless on color tokens). |
681
782
 
682
783
  Config overrides apply to both `glaze.color()` tokens and `glaze()` themes:
683
784
 
684
785
  ```ts
685
786
  // Standalone color — preserve raw tone in both schemes
686
- glaze.color('#26fcb2', { darkTone: false })
787
+ glaze.color('#26fcb2', { darkTone: false });
687
788
 
688
789
  // Restore the #000 → white dark flip (full dark range)
689
790
  glaze.color('#000000', {
690
791
  lightTone: false,
691
792
  darkTone: [15, 100],
692
- })
793
+ });
693
794
 
694
795
  // Structured form with config override
695
- glaze.color({ hue: 152, saturation: 95, tone: 74 }, { darkTone: false })
796
+ glaze.color({ hue: 152, saturation: 95, tone: 74 }, { darkTone: false, pastel: true });
696
797
 
697
798
  // Theme with config override
698
- const rawTheme = glaze(280, 80, { lightTone: false })
799
+ const rawTheme = glaze(280, 80, { lightTone: false, pastel: true });
699
800
  ```
700
801
 
701
- The override is **snapshotted at create time** so later `glaze.configure()` calls don't change already-created tokens or themes (for non-overridden fields, the snapshot captured the global value at creation time; for themes, non-overridden fields are re-read from the live global at resolve time — see [Theme config override](#theme-config-override)).
802
+ See [Theme config override](#theme-config-override).
702
803
 
703
804
  ### Theme config override
704
805
 
@@ -722,11 +823,17 @@ const child = t.extend({ config: { darkTone: false } });
722
823
  // child: lightTone { lo: 0, hi: 50 } (inherited) + darkTone: false (added)
723
824
  ```
724
825
 
725
- `theme.export()` includes `config`; `glaze.from(data)` restores it.
826
+ `theme.export(override?)` freezes `getConfig() ∪ instance local ∪ override` at
827
+ call time. Restoring via `glaze.themeFrom(data)` (or the compat alias
828
+ `glaze.from`) pins that freeze as the restored theme's local override — matching
829
+ standalone color-token behavior.
726
830
 
727
831
  ### `glaze.colorFrom(data)`
728
832
 
729
- Inverse of `token.export()`. The exported snapshot includes the original input, all overrides (with any `base` token recursively serialized), and the full effective config — so later `glaze.configure()` calls don't change rehydrated tokens.
833
+ Inverse of `token.export()`. The exported snapshot includes the original input,
834
+ all overrides (with any `base` token recursively serialized), and the effective
835
+ config freeze from export time — so later `glaze.configure()` calls don't change
836
+ rehydrated tokens.
730
837
 
731
838
  ```ts
732
839
  const text = glaze.color({ from: '#1a1a1a', contrast: 'AA' });
@@ -748,7 +855,8 @@ const bg = glaze.color('#1a1a2e');
748
855
  const text = glaze.color({ from: '#ffffff', base: bg, contrast: 'AA' });
749
856
 
750
857
  // Border 8 tone units lighter than `bg` in each scheme.
751
- const border = glaze.color({ from: '#000000',
858
+ const border = glaze.color({
859
+ from: '#000000',
752
860
  base: bg,
753
861
  tone: '+8',
754
862
  mode: 'fixed',
@@ -789,7 +897,7 @@ The `name` override appears in `console.warn` / Error messages but **does not**
789
897
  ```ts
790
898
  theme.colors({
791
899
  surface: { tone: 95 },
792
- text: { base: 'surface', tone: '-52', contrast: 'AAA' },
900
+ text: { base: 'surface', tone: '-52', contrast: 'AAA' },
793
901
 
794
902
  'shadow-sm': { type: 'shadow', bg: 'surface', fg: 'text', intensity: 5 },
795
903
  'shadow-md': { type: 'shadow', bg: 'surface', fg: 'text', intensity: 10 },
@@ -828,20 +936,22 @@ theme.colors({
828
936
 
829
937
  Fine-tune behavior per-color or globally via `glaze.configure({ shadowTuning })`. Per-color `tuning` is merged field-by-field with the global one.
830
938
 
831
- | Parameter | Default | Description |
832
- |---|---|---|
833
- | `saturationFactor` | `0.18` | Fraction of fg saturation kept in pigment. |
834
- | `maxSaturation` | `0.25` | Upper clamp on pigment saturation. |
835
- | `lightnessFactor` | `0.25` | Multiplier for bg lightness → pigment lightness. |
836
- | `lightnessBounds` | `[0.05, 0.20]` | Clamp range for pigment lightness. |
837
- | `minGapTarget` | `0.05` | Target minimum gap between pigment and bg lightness. |
838
- | `alphaMax` | `1.0` | Asymptotic maximum alpha. |
839
- | `bgHueBlend` | `0.2` | Blend weight pulling pigment hue toward bg hue. `0` = pure fg hue, `1` = pure bg hue. |
939
+ | Parameter | Default | Description |
940
+ | ------------------ | -------------- | ------------------------------------------------------------------------------------- |
941
+ | `saturationFactor` | `0.18` | Fraction of fg saturation kept in pigment. |
942
+ | `maxSaturation` | `0.25` | Upper clamp on pigment saturation. |
943
+ | `lightnessFactor` | `0.25` | Multiplier for bg lightness → pigment lightness. |
944
+ | `lightnessBounds` | `[0.05, 0.20]` | Clamp range for pigment lightness. |
945
+ | `minGapTarget` | `0.05` | Target minimum gap between pigment and bg lightness. |
946
+ | `alphaMax` | `1.0` | Asymptotic maximum alpha. |
947
+ | `bgHueBlend` | `0.2` | Blend weight pulling pigment hue toward bg hue. `0` = pure fg hue, `1` = pure bg hue. |
840
948
 
841
949
  ```ts
842
950
  theme.colors({
843
951
  'shadow-soft': {
844
- type: 'shadow', bg: 'surface', intensity: 10,
952
+ type: 'shadow',
953
+ bg: 'surface',
954
+ intensity: 10,
845
955
  tuning: { alphaMax: 0.3, saturationFactor: 0.1 },
846
956
  },
847
957
  });
@@ -869,12 +979,12 @@ const css = glaze.format(v, 'oklch');
869
979
 
870
980
  `GlazeShadowInput`:
871
981
 
872
- | Field | Type | Description |
873
- |---|---|---|
874
- | `bg` | `GlazeColorValue` | Background. Any `GlazeColorValue` form. Alpha components dropped with warning. |
875
- | `fg` | `GlazeColorValue` | Optional foreground. Same forms as `bg`. |
876
- | `intensity` | `number` | 0–100. |
877
- | `tuning` | `ShadowTuning` | Optional. |
982
+ | Field | Type | Description |
983
+ | ----------- | ----------------- | ------------------------------------------------------------------------------ |
984
+ | `bg` | `GlazeColorValue` | Background. Any `GlazeColorValue` form. Alpha components dropped with warning. |
985
+ | `fg` | `GlazeColorValue` | Optional foreground. Same forms as `bg`. |
986
+ | `intensity` | `number` | 0–100. |
987
+ | `tuning` | `ShadowTuning` | Optional. |
878
988
 
879
989
  ### Fixed opacity (regular colors)
880
990
 
@@ -898,8 +1008,8 @@ Produces a solid color by interpolating between `base` and `target`:
898
1008
  ```ts
899
1009
  theme.colors({
900
1010
  surface: { tone: 95 },
901
- accent: { tone: 30 },
902
- tint: { type: 'mix', base: 'surface', target: 'accent', value: 30 },
1011
+ accent: { tone: 30 },
1012
+ tint: { type: 'mix', base: 'surface', target: 'accent', value: 30 },
903
1013
  });
904
1014
  ```
905
1015
 
@@ -914,10 +1024,13 @@ Produces the target color with controlled opacity — useful for hover overlays:
914
1024
  ```ts
915
1025
  theme.colors({
916
1026
  surface: { tone: 95 },
917
- black: { tone: 0, saturation: 0 },
1027
+ black: { tone: 0, saturation: 0 },
918
1028
  hover: {
919
- type: 'mix', base: 'surface', target: 'black',
920
- value: 8, blend: 'transparent',
1029
+ type: 'mix',
1030
+ base: 'surface',
1031
+ target: 'black',
1032
+ value: 8,
1033
+ blend: 'transparent',
921
1034
  },
922
1035
  });
923
1036
  // hover → black with alpha = 0.08
@@ -927,10 +1040,10 @@ The output color has `h`, `s`, `l` from the target and `alpha = value / 100`.
927
1040
 
928
1041
  ### Blend space (opaque only)
929
1042
 
930
- | `space` | Behavior | Best for |
931
- |---|---|---|
932
- | `'okhsl'` (default) | Perceptually uniform OKHSL interpolation. | Design token derivation. |
933
- | `'srgb'` | Linear sRGB channel interpolation. | Matching browser compositing of CSS color-mix / overlay. |
1043
+ | `space` | Behavior | Best for |
1044
+ | ------------------- | ----------------------------------------- | -------------------------------------------------------- |
1045
+ | `'okhsl'` (default) | Perceptually uniform OKHSL interpolation. | Design token derivation. |
1046
+ | `'srgb'` | Linear sRGB channel interpolation. | Matching browser compositing of CSS color-mix / overlay. |
934
1047
 
935
1048
  Transparent blending always composites in linear sRGB (matches browser alpha compositing).
936
1049
 
@@ -963,8 +1076,20 @@ Mix colors can reference other mix colors:
963
1076
  theme.colors({
964
1077
  white: { tone: 100, saturation: 0 },
965
1078
  black: { tone: 0, saturation: 0 },
966
- gray: { type: 'mix', base: 'white', target: 'black', value: 50, space: 'srgb' },
967
- lightGray: { type: 'mix', base: 'white', target: 'gray', value: 50, space: 'srgb' },
1079
+ gray: {
1080
+ type: 'mix',
1081
+ base: 'white',
1082
+ target: 'black',
1083
+ value: 50,
1084
+ space: 'srgb',
1085
+ },
1086
+ lightGray: {
1087
+ type: 'mix',
1088
+ base: 'white',
1089
+ target: 'gray',
1090
+ value: 50,
1091
+ space: 'srgb',
1092
+ },
968
1093
  });
969
1094
  ```
970
1095
 
@@ -986,35 +1111,67 @@ const palette = glaze.palette(
986
1111
 
987
1112
  `GlazePaletteOptions`:
988
1113
 
989
- | Option | Description |
990
- |---|---|
1114
+ | Option | Description |
1115
+ | --------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
991
1116
  | `primary` | Name of the primary theme. The primary's tokens are duplicated **without** prefix in all exports, providing convenient short aliases alongside the prefixed versions. Throws if the name doesn't match any theme. |
992
1117
 
993
1118
  A `GlazePalette` exposes:
994
1119
 
995
- | Method | Description |
996
- |---|---|
997
- | `palette.tokens(options?)` | Flat token map grouped by scheme variant. |
998
- | `palette.tasty(options?)` | Tasty style-to-state bindings. |
999
- | `palette.json(options?)` | Per-theme JSON map (no prefix needed — keyed by theme name). |
1000
- | `palette.css(options?)` | CSS custom property declaration strings. |
1120
+ | Method | Description |
1121
+ | -------------------------------- | ---------------------------------------------------------------------------------------------------- |
1122
+ | `palette.list()` | Theme names in insertion order. |
1123
+ | `palette.primary` | Primary theme name, if set. |
1124
+ | `palette.theme(name)` | Get a live theme instance by name. |
1125
+ | `palette.themes()` | Shallow copy of the theme map (same instances the palette holds). |
1126
+ | `palette.export(override?)` | Authoring snapshot — restorable via `glaze.paletteFrom()`. Optional override forwarded to themes. |
1127
+ | `palette.tokens(options?)` | Flat token map grouped by scheme variant. |
1128
+ | `palette.tasty(options?)` | [Tasty](https://tasty.style) style-to-state bindings. |
1129
+ | `palette.json(options?)` | Per-theme **resolved** color JSON (not restorable as authoring config). |
1130
+ | `palette.css(options?)` | CSS custom property declaration strings. |
1131
+ | `palette.dtcg(options?)` | Per-scheme W3C DTCG token trees. |
1132
+ | `palette.dtcgResolver(options?)` | One DTCG Resolver-Module document for every scheme. |
1133
+ | `palette.tailwind(options?)` | One Tailwind CSS v4 theme with scheme overrides. |
1134
+
1135
+ ### `palette.export(override?)` / `glaze.paletteFrom()`
1136
+
1137
+ ```ts
1138
+ const snapshot = palette.export();
1139
+ // → {
1140
+ // kind: 'palette',
1141
+ // version: 1,
1142
+ // primary: 'brand',
1143
+ // themes: { brand: { kind: 'theme', ... }, danger: { ... } },
1144
+ // }
1145
+
1146
+ const restored = glaze.paletteFrom(JSON.parse(JSON.stringify(snapshot)));
1147
+ const brand = restored.theme('brand')!;
1148
+ ```
1149
+
1150
+ Optional `override` is forwarded to each nested `theme.export(override)`.
1151
+ Config snapshot vs resolved output: use `export()` / `paletteFrom()` to
1152
+ persist and restore the authoring graph (themes, color defs, relations). Use
1153
+ `json()` / `tokens()` / `css()` / … to emit resolved color strings for apps
1154
+ and design tools.
1001
1155
 
1002
1156
  ### `GlazePaletteExportOptions`
1003
1157
 
1004
1158
  Shared by `tokens`, `tasty`, and `css`:
1005
1159
 
1006
- | Option | Default | Description |
1007
- |---|---|---|
1008
- | `prefix` | `true` (= `"<themeName>-"`) | `false` disables prefixing. Or pass a custom map: `{ primary: 'brand-', danger: 'error-' }`. |
1009
- | `primary` | inherits from palette creation | `string` to override, `false` to disable for this call. |
1160
+ | Option | Default | Description |
1161
+ | --------- | ------------------------------ | -------------------------------------------------------------------------------------------- |
1162
+ | `prefix` | `true` (= `"<themeName>-"`) | `false` disables prefixing. Or pass a custom map: `{ primary: 'brand-', danger: 'error-' }`. |
1163
+ | `primary` | inherits from palette creation | `string` to override, `false` to disable for this call. |
1010
1164
 
1011
1165
  Each export method also accepts its own format/options shape:
1012
1166
 
1013
- | Method | Additional options |
1014
- |---|---|
1015
- | `palette.tokens(options?)` | `format`, `modes` |
1016
- | `palette.tasty(options?)` | `format`, `modes`, `states` |
1017
- | `palette.css(options?)` | `format`, `suffix` |
1167
+ | Method | Additional options |
1168
+ | -------------------------------- | ----------------------------------------- |
1169
+ | `palette.tokens(options?)` | `format`, `modes` |
1170
+ | `palette.tasty(options?)` | `format`, `modes`, `states` |
1171
+ | `palette.css(options?)` | `format`, `suffix` |
1172
+ | `palette.dtcg(options?)` | `colorSpace`, `modes` |
1173
+ | `palette.dtcgResolver(options?)` | `colorSpace`, `modes`, resolver names |
1174
+ | `palette.tailwind(options?)` | `format`, `modes`, selectors, `namespace` |
1018
1175
 
1019
1176
  `palette.css()` does not accept `modes`; it always returns all four CSS strings (`light`, `dark`, `lightContrast`, `darkContrast`).
1020
1177
 
@@ -1025,8 +1182,8 @@ By default all palette tokens are prefixed:
1025
1182
  ```ts
1026
1183
  palette.tokens();
1027
1184
  // → {
1028
- // light: { 'primary-surface': 'okhsl(...)', 'danger-surface': 'okhsl(...)' },
1029
- // dark: { 'primary-surface': 'okhsl(...)', 'danger-surface': 'okhsl(...)' },
1185
+ // light: { 'primary-surface': 'oklch(...)', 'danger-surface': 'oklch(...)' },
1186
+ // dark: { 'primary-surface': 'oklch(...)', 'danger-surface': 'oklch(...)' },
1030
1187
  // }
1031
1188
  ```
1032
1189
 
@@ -1062,10 +1219,10 @@ const palette = glaze.palette(
1062
1219
  palette.tokens();
1063
1220
  // → {
1064
1221
  // light: {
1065
- // 'primary-surface': 'okhsl(...)',
1066
- // 'danger-surface': 'okhsl(...)',
1067
- // 'success-surface': 'okhsl(...)',
1068
- // 'surface': 'okhsl(...)', // unprefixed alias
1222
+ // 'primary-surface': 'oklch(...)',
1223
+ // 'danger-surface': 'oklch(...)',
1224
+ // 'success-surface': 'oklch(...)',
1225
+ // 'surface': 'oklch(...)', // unprefixed alias
1069
1226
  // },
1070
1227
  // }
1071
1228
  ```
@@ -1091,8 +1248,8 @@ JSON export groups by theme name (no prefix needed):
1091
1248
  ```ts
1092
1249
  palette.json();
1093
1250
  // → {
1094
- // primary: { surface: { light: 'okhsl(...)', dark: 'okhsl(...)' } },
1095
- // danger: { surface: { light: 'okhsl(...)', dark: 'okhsl(...)' } },
1251
+ // primary: { surface: { light: 'oklch(...)', dark: 'oklch(...)' } },
1252
+ // danger: { surface: { light: 'oklch(...)', dark: 'oklch(...)' } },
1096
1253
  // }
1097
1254
  ```
1098
1255
 
@@ -1116,7 +1273,7 @@ It does not accept `modes`; all four result fields are always returned.
1116
1273
  DTCG export for a palette. Prefix defaults to `true` and the palette-level `primary` is honored (the primary theme's tokens are duplicated without prefix as aliases).
1117
1274
 
1118
1275
  ```ts
1119
- palette.dtcg()
1276
+ palette.dtcg();
1120
1277
  // → {
1121
1278
  // light: {
1122
1279
  // 'primary-surface': { $type: 'color', $value: { ... } },
@@ -1134,7 +1291,7 @@ Accepts `GlazeDtcgOptions` plus `GlazePaletteExportOptions`.
1134
1291
  Resolver-Module export for a palette. Same as `theme.dtcgResolver()` but merges every theme (with prefix / `primary` aliasing) into the single `sets.base` source and each `scheme` context. Prefix defaults to `true`; the palette-level `primary` is honored.
1135
1292
 
1136
1293
  ```ts
1137
- palette.dtcgResolver()
1294
+ palette.dtcgResolver();
1138
1295
  // → {
1139
1296
  // version: '2025.10',
1140
1297
  // sets: { base: { sources: [ { 'primary-surface': {…}, 'surface': {…}, 'danger-surface': {…} } ] } },
@@ -1167,24 +1324,24 @@ Accepts `GlazeTailwindOptions` plus `GlazePaletteExportOptions`. The palette `pr
1167
1324
 
1168
1325
  Control the color format with the `format` option on any export method:
1169
1326
 
1170
- | Format | Output (alpha = 1) | Output (alpha < 1) | Notes |
1171
- |---|---|---|---|
1172
- | `'okhsl'` (default for `tasty()`) | `okhsl(H S% L%)` | `okhsl(H S% L% / A)` | Glaze's native format, not a CSS function. **Tasty-only** (`tasty()`, `token()`, `.tasty()`). |
1173
- | `'okhst'` | `okhst(H S% T%)` | `okhst(H S% T% / A)` | OKHST tone axis. **Tasty-only** — same restriction as `okhsl`. |
1174
- | `'oklch'` (default for `tokens()` / `json()`) | `oklch(L C H)` | `oklch(L C H / A)` | OKLab-based LCH. Native CSS. Required for `splitHue`. |
1175
- | `'rgb'` (default for `css()`) | `rgb(R G B)` | `rgb(R G B / A)` | Rounded integers, modern space syntax. |
1176
- | `'hsl'` | `hsl(H S% L%)` | `hsl(H S% L% / A)` | Modern space syntax. |
1327
+ | Format | Output (alpha = 1) | Output (alpha < 1) | Notes |
1328
+ | --------------------------------------------- | ------------------ | -------------------- | --------------------------------------------------------------------------------------------- |
1329
+ | `'oklch'` (default for CSS-string exports) | `oklch(L C H)` | `oklch(L C H / A)` | OKLab-based LCH. Native CSS. Required for `splitHue`. |
1330
+ | `'rgb'` | `rgb(R G B)` | `rgb(R G B / A)` | Rounded integers, modern space syntax. |
1331
+ | `'hsl'` | `hsl(H S% L%)` | `hsl(H S% L% / A)` | Modern space syntax. |
1332
+ | `'okhsl'` | `okhsl(H S% L%)` | `okhsl(H S% L% / A)` | Glaze's native format, not a CSS function. **[Tasty](https://tasty.style)-only** (`tasty()`, `token()`, `.tasty()`). |
1333
+ | `'okhst'` | `okhst(H S% T%)` | `okhst(H S% T% / A)` | OKHST tone axis. **[Tasty](https://tasty.style)-only** — same restriction as `okhsl`. |
1177
1334
 
1178
1335
  ```ts
1179
- theme.tokens(); // 'oklch(0.965 0.0123 280)' (default)
1180
- theme.tokens({ format: 'rgb' }); // 'rgb(244 240 250)'
1181
- theme.tasty(); // 'okhsl(280 60% 97%)' (default)
1182
- theme.tasty({ format: 'okhst' }); // 'okhst(280 60% 97%)'
1336
+ theme.tokens(); // 'oklch(0.965 0.0123 280)' (default)
1337
+ theme.tokens({ format: 'rgb' }); // 'rgb(244 240 250)'
1338
+ theme.tasty(); // 'oklch(0.965 0.0123 280)' (default)
1339
+ theme.tasty({ format: 'okhst' }); // 'okhst(280 60% 97%)'
1183
1340
  ```
1184
1341
 
1185
1342
  All numeric output strips trailing zeros for cleaner CSS (e.g. `95` not `95.0`).
1186
1343
 
1187
- The `format` option works on CSS-string exports: `theme.tokens()`, `theme.tasty()`, `theme.json()`, `theme.css()`, `theme.tailwind()`, the same on `palette`, and on `token.token()` / `.tasty()` / `.json()` / `.css()` / `.tailwind()`. **`okhsl` and `okhst` throw on non-Tasty exports** (`tokens`, `json`, `css`, `tailwind`) — they are not native CSS color spaces.
1344
+ The `format` option works on CSS-string exports: `theme.tokens()`, `theme.tasty()`, `theme.json()`, `theme.css()`, `theme.tailwind()`, the same on `palette`, and on `token.token()` / `.tasty()` / `.json()` / `.css()` / `.tailwind()`. **`okhsl` and `okhst` throw on non-[Tasty](https://tasty.style) exports** (`tokens`, `json`, `css`, `tailwind`) — they are not native CSS color spaces.
1188
1345
 
1189
1346
  ### Hue channel splitting (`splitHue`)
1190
1347
 
@@ -1198,7 +1355,7 @@ On `theme.css()`, `theme.tasty()`, `palette.css()`, `palette.tasty()`, and stand
1198
1355
  --accent-color: oklch(0.62 0.03 var(--accent-hue));
1199
1356
  ```
1200
1357
 
1201
- **Requirements:** every exported color must be pastel (`pastel: true` globally or per-color). Pastel mode bounds chroma by the hue-independent safe chroma at each lightness, so emitted `C` stays in sRGB for any rotated hue. Non-pastel palettes throw rather than emit values that would clip under rotation.
1358
+ **Requirements:** every exported color must be pastel (`pastel: true` on the theme/token override or per-color). Pastel mode bounds chroma by the hue-independent safe chroma at each lightness, so emitted `C` stays in sRGB for any rotated hue. Non-pastel palettes throw rather than emit values that would clip under rotation.
1202
1359
 
1203
1360
  **Limitations:** `oklch` only (native CSS `var()` in the hue slot). Shadow and mix colors stay inline (blended hue). Standalone `.token()` / `.tasty()` do not support `splitHue` (return shape cannot carry the `$name-hue` declaration).
1204
1361
 
@@ -1210,11 +1367,11 @@ On `theme.css()`, `theme.tasty()`, `palette.css()`, `palette.tasty()`, and stand
1210
1367
 
1211
1368
  `mode` controls how a color adapts across schemes:
1212
1369
 
1213
- | Mode | Behavior |
1214
- |---|---|
1215
- | `'auto'` (default) | Full adaptation. Dark is a single tone inversion (`100 − t`) remapped into the dark window. High-contrast uses the full range. |
1216
- | `'fixed'` | Color stays recognizable. Tone is *mapped* (not inverted) into the dark window. Use for brand buttons, CTAs, status banners. |
1217
- | `'static'` | No adaptation. Same tone in every scheme. |
1370
+ | Mode | Behavior |
1371
+ | ------------------ | -------------------------------------------------------------------------------------------------------------------------------------------- |
1372
+ | `'auto'` (default) | Full adaptation. Dark uses **dark tone inversion** (`100 − t`) and then remaps into the dark tone window. High-contrast uses the full range. |
1373
+ | `'fixed'` | Color stays recognizable. Tone is _mapped_ (not inverted) into the dark window. Use for brand buttons, CTAs, status banners. |
1374
+ | `'static'` | No adaptation. Same tone in every scheme. |
1218
1375
 
1219
1376
  ### How relative tone adapts
1220
1377
 
@@ -1241,11 +1398,17 @@ Offsets that would push past `[0, 100]` clamp to the boundary, or — with `auto
1241
1398
 
1242
1399
  ## Light / dark scheme mapping
1243
1400
 
1244
- The mapping is now a single tone pipeline; there is no Möbius curve. See [`docs/okhst.md`](okhst.md) for the full math and the calibrated default constants.
1401
+ The mapping is a single tone pipeline; there is no Möbius curve. See
1402
+ [Scheme adaptation](okhst.md#scheme-adaptation) for the product-level model and
1403
+ the [canonical OKHST specification](https://github.com/tenphi/okhst) for the
1404
+ transfer math.
1245
1405
 
1246
1406
  ### Light scheme
1247
1407
 
1248
- An authored tone (0–100) is remapped into the `lightTone` window. The window's `lo`/`hi` are OKHSL-lightness endpoints (0–100); the tone is positioned within the window's tone interval and converted to a final OKHSL lightness. `static` mode and HC variants use the full range.
1408
+ An authored tone (0–100) is remapped into the `lightTone` **tone window**.
1409
+ The window's `lo`/`hi` are OKHSL-lightness boundaries (0–100); authored tone is
1410
+ positioned within the corresponding tone interval and converted to final OKHSL
1411
+ lightness. `static` mode and HC variants use the full range.
1249
1412
 
1250
1413
  ```
1251
1414
  window = lightTone // default [10, 100]
@@ -1263,7 +1426,10 @@ inverted = 100 - authorTone
1263
1426
  finalTone = remap(inverted, window)
1264
1427
  ```
1265
1428
 
1266
- Because tone is contrast-uniform, the inversion preserves contrast steps without a fitted curve the asymmetry between light and dark lives entirely in the two windows' `(lo, hi, eps)` scalars (`eps` defaults to the reference `0.05`).
1429
+ The inversion preserves authored tone spacing without a fitted curve. This is
1430
+ exactly contrast-even for neutrals and approximate for chromatic colors. The
1431
+ ordinary light/dark asymmetry lives in the two windows' `(lo, hi, eps)` values
1432
+ (`eps` defaults to the reference `0.05`).
1267
1433
 
1268
1434
  **`fixed`** — remap into the dark window without inversion:
1269
1435
 
@@ -1278,7 +1444,7 @@ In high-contrast variants both windows are bypassed (forced to the full `[0, 100
1278
1444
  `darkDesaturation` reduces saturation for all colors in dark scheme:
1279
1445
 
1280
1446
  ```ts
1281
- S_dark = S_light * (1 - darkDesaturation) // default: 0.1
1447
+ S_dark = S_light * (1 - darkDesaturation); // default: 0.1
1282
1448
  ```
1283
1449
 
1284
1450
  `static` mode skips desaturation.
@@ -1290,7 +1456,7 @@ S_dark = S_light * (1 - darkDesaturation) // default: 0.1
1290
1456
  ```ts
1291
1457
  glaze.configure({
1292
1458
  lightTone: [10, 100], // [lo, hi]; or { lo, hi, eps } / false to disable clamping
1293
- darkTone: [15, 95], // [lo, hi]; or { lo, hi, eps } / false to disable clamping
1459
+ darkTone: [15, 95], // [lo, hi]; or { lo, hi, eps } / false to disable clamping
1294
1460
  darkDesaturation: 0.1,
1295
1461
  states: {
1296
1462
  dark: '@media(prefers-color-scheme: dark)',
@@ -1307,31 +1473,33 @@ glaze.configure({
1307
1473
  });
1308
1474
  ```
1309
1475
 
1310
- A `ToneWindow` is `[lo, hi]` (OKHSL-lightness endpoints, reference eps — the common form), `{ lo, hi, eps }` (advanced: explicit per-mode render eps), or `false` to disable clamping (full range `[0, 100]` at the reference eps). `false` removes the *boundaries*, not the contrast-uniform tone curve.
1476
+ A `ToneWindow` is `[lo, hi]` (OKHSL-lightness boundaries, reference eps — the
1477
+ common form), `{ lo, hi, eps }` (advanced: explicit per-scheme render eps), or
1478
+ `false` for the full range `[0, 100]` at the reference eps. `false` removes the
1479
+ boundaries, not the tone transfer.
1311
1480
 
1312
1481
  `GlazeConfig`:
1313
1482
 
1314
- | Field | Default | Description |
1315
- |---|---|---|
1316
- | `lightTone` | `[10, 100]` | Light scheme tone window: `[lo, hi]`, `{ lo, hi, eps }`, or `false` to disable clamping. Bypassed in HC. |
1317
- | `darkTone` | `[15, 95]` | Dark scheme tone window: `[lo, hi]`, `{ lo, hi, eps }`, or `false` to disable clamping. Bypassed in HC. |
1318
- | `darkDesaturation` | `0.1` | Saturation reduction in dark scheme (0–1). |
1319
- | `states.dark` | `'@media(prefers-color-scheme: dark)'` | State alias for dark mode tokens (Tasty export). Defaults to a media query so tokens react to the OS preference without registering custom states. |
1320
- | `states.highContrast` | `'@media(prefers-contrast: more)'` | State alias for HC tokens (Tasty export). |
1321
- | `modes.dark` | `true` | Include dark variants in exports. |
1322
- | `modes.highContrast` | `false` | Include HC variants. |
1323
- | `shadowTuning` | `undefined` | Default tuning for all shadow colors. Per-color tuning merges field-by-field. |
1324
- | `autoFlip` | `true` | Default for each color's `autoFlip`. When solving `contrast` (or applying a relative `tone` that overshoots `[0, 100]`), allow crossing to the opposite side instead of clamping. With `false`, only the requested direction is considered; unmet contrasts pin the tone to that direction's extreme (and emit a warning) and overshooting offsets clamp to the boundary. Override per color via [`autoFlip`](#autoflip). |
1325
- | `pastel` | `false` | Hue-independent "safe" chroma limit across all colors so scaling saturation never exceeds the sRGB boundary at any hue for the given lightness. Override per color via [`pastel`](#per-color-pastel). |
1326
- | `inferRole` | `true` | Infer each color's [`role`](#roles) from its name when no explicit `role` is set. Set to `false` to opt out of name-based inference (the base-opposite and foreground-default fallbacks still apply). |
1327
-
1328
- | Method | Description |
1329
- |---|---|
1483
+ | Field | Default | Description |
1484
+ | --------------------- | -------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
1485
+ | `lightTone` | `[10, 100]` | Light scheme tone window: `[lo, hi]`, `{ lo, hi, eps }`, or `false` to disable clamping. Bypassed in HC. |
1486
+ | `darkTone` | `[15, 95]` | Dark scheme tone window: `[lo, hi]`, `{ lo, hi, eps }`, or `false` to disable clamping. Bypassed in HC. |
1487
+ | `darkDesaturation` | `0.1` | Saturation reduction in dark scheme (0–1). |
1488
+ | `states.dark` | `'@media(prefers-color-scheme: dark)'` | State alias for dark mode tokens ([Tasty](https://tasty.style) export). Defaults to a media query so tokens react to the OS preference without registering custom states. |
1489
+ | `states.highContrast` | `'@media(prefers-contrast: more)'` | State alias for HC tokens ([Tasty](https://tasty.style) export). |
1490
+ | `modes.dark` | `true` | Include dark variants in exports. |
1491
+ | `modes.highContrast` | `false` | Include HC variants. |
1492
+ | `shadowTuning` | `undefined` | Default tuning for all shadow colors. Per-color tuning merges field-by-field. |
1493
+ | `autoFlip` | `true` | Default for each color's `autoFlip`. When solving `contrast` (or applying a relative `tone` that overshoots `[0, 100]`), allow crossing to the opposite side instead of clamping. With `false`, only the requested direction is considered; unmet contrasts pin the tone to that direction's extreme (and emit a warning) and overshooting offsets clamp to the boundary. Override per color via [`autoFlip`](#autoflip). |
1494
+ | `inferRole` | `true` | Infer each color's [`role`](#roles) from its name when no explicit `role` is set. Set to `false` to opt out of name-based inference (the base-opposite and foreground-default fallbacks still apply). |
1495
+
1496
+ | Method | Description |
1497
+ | ------------------------- | ----------------------------------------------------------------------------------- |
1330
1498
  | `glaze.configure(config)` | Merge into the global config. Bumps a config version that invalidates theme caches. |
1331
- | `glaze.getConfig()` | Snapshot the current resolved config (shallow copy). |
1332
- | `glaze.resetConfig()` | Reset to defaults (also bumps the version counter). |
1499
+ | `glaze.getConfig()` | Snapshot the current resolved config (shallow copy). |
1500
+ | `glaze.resetConfig()` | Reset to defaults (also bumps the version counter). |
1333
1501
 
1334
- Standalone `glaze.color()` tokens snapshot the resolve-relevant fields at create time, so later `configure()` calls don't change already-created tokens. Themes merge the live global at resolve time for fields not overridden via `GlazeConfigOverride`.
1502
+ Themes and standalone color tokens keep a sparse local `GlazeConfigOverride` and merge the live global at resolve time for omitted fields. Authoring `.export(override?)` freezes the effective merge at call time; restored instances pin that freeze. `pastel` is instance-only (theme/token override or per-color) — not set via `configure()`.
1335
1503
 
1336
1504
  ---
1337
1505
 
@@ -1361,32 +1529,38 @@ Resolution priority (highest first):
1361
1529
 
1362
1530
  ## Validation
1363
1531
 
1364
- | Condition | Behavior |
1365
- |---|---|
1366
- | `contrast` without `base` in a **theme** color | Validation error |
1367
- | Relative `tone` without `base` in a **theme** color | Validation error |
1368
- | `contrast` without `base` in `glaze.color()` | Anchors against the literal seed (no error) |
1369
- | Relative `tone` without `base` in `glaze.color()` | Anchors against the literal seed (no error) |
1370
- | Relative `tone` overshoots `[0, 100]` | Mirror to the other side of the base (`autoFlip` on, default), or clamp to the boundary (`autoFlip` off) |
1371
- | `tone` resolves outside 0–100 | Clamp silently |
1372
- | `'max'` / `'min'` without `base` | Allowed resolves to the scheme's tone extreme (root color) |
1373
- | `saturation` outside 0–1 | Clamp silently |
1374
- | Circular `base` references | Validation error |
1375
- | `base` references non-existent name | Validation error |
1376
- | Shadow `bg` references non-existent color | Validation error |
1377
- | Shadow `fg` references non-existent color | Validation error |
1378
- | Shadow `bg` references another shadow color | Validation error |
1379
- | Shadow `fg` references another shadow color | Validation error |
1380
- | Regular color `base` references a shadow color | Validation error |
1381
- | Shadow `intensity` outside 0–100 | Clamp silently |
1382
- | `contrast` + `opacity` combined | `console.warn` |
1383
- | Mix `base` references non-existent color | Validation error |
1384
- | Mix `target` references non-existent color | Validation error |
1385
- | Mix `base` references a shadow color | Validation error |
1386
- | Mix `target` references a shadow color | Validation error |
1387
- | Mix `value` outside 0–100 | Clamp silently |
1388
- | Circular references involving mix colors | Validation error |
1389
- | Contrast target physically unreachable | `console.warn` (deduped per `(name, scheme, target)`); closest passing variant returned |
1532
+ Invalid definitions throw before resolution when Glaze cannot produce a
1533
+ well-defined dependency graph. Recoverable numeric bounds are clamped. A
1534
+ physically unreachable contrast floor or a potentially misleading
1535
+ contrast/opacity combination emits `console.warn` and returns the closest
1536
+ available result.
1537
+
1538
+ | Condition | Behavior |
1539
+ | --------------------------------------------------- | -------------------------------------------------------------------------------------------------------- |
1540
+ | `contrast` without `base` in a **theme** color | Validation error |
1541
+ | Relative `tone` without `base` in a **theme** color | Validation error |
1542
+ | `contrast` without `base` in `glaze.color()` | Anchors against the literal seed (no error) |
1543
+ | Relative `tone` without `base` in `glaze.color()` | Anchors against the literal seed (no error) |
1544
+ | Relative `tone` overshoots `[0, 100]` | Mirror to the other side of the base (`autoFlip` on, default), or clamp to the boundary (`autoFlip` off) |
1545
+ | `tone` resolves outside 0–100 | Clamp silently |
1546
+ | `'max'` / `'min'` without `base` | Allowed resolves to the scheme's tone extreme (root color) |
1547
+ | `saturation` outside 0–1 | Clamp silently |
1548
+ | Circular `base` references | Validation error |
1549
+ | `base` references non-existent name | Validation error |
1550
+ | Shadow `bg` references non-existent color | Validation error |
1551
+ | Shadow `fg` references non-existent color | Validation error |
1552
+ | Shadow `bg` references another shadow color | Validation error |
1553
+ | Shadow `fg` references another shadow color | Validation error |
1554
+ | Regular color `base` references a shadow color | Validation error |
1555
+ | Shadow `intensity` outside 0–100 | Clamp silently |
1556
+ | `contrast` + `opacity` combined | `console.warn` |
1557
+ | Mix `base` references non-existent color | Validation error |
1558
+ | Mix `target` references non-existent color | Validation error |
1559
+ | Mix `base` references a shadow color | Validation error |
1560
+ | Mix `target` references a shadow color | Validation error |
1561
+ | Mix `value` outside 0–100 | Clamp silently |
1562
+ | Circular references involving mix colors | Validation error |
1563
+ | Contrast target physically unreachable | `console.warn` (deduped per `(name, scheme, target)`); closest passing variant returned |
1390
1564
 
1391
1565
  ---
1392
1566
 
@@ -1412,19 +1586,19 @@ import {
1412
1586
  } from '@tenphi/glaze';
1413
1587
  ```
1414
1588
 
1415
- | Function | Description |
1416
- |---|---|
1417
- | `okhslToLinearSrgb(h, s, l)` | OKHSL (h: 0–360, s/l: 0–1) → linear sRGB tuple. |
1418
- | `okhslToSrgb(h, s, l)` | OKHSL → gamma-encoded sRGB tuple (0–1 per channel). |
1419
- | `okhslToOklab([h, s, l])` | OKHSL → OKLab `[L, a, b]`. |
1420
- | `oklabToOkhsl([L, a, b])` | OKLab → OKHSL. |
1421
- | `srgbToOkhsl([r, g, b])` | Gamma sRGB (0–1) → OKHSL. |
1422
- | `hslToSrgb(h, s, l)` | CSS HSL → sRGB tuple. |
1423
- | `parseHex(hex)` | Parse `#rgb` / `#rrggbb` to sRGB tuple. Returns `null` on invalid input. |
1424
- | `parseHexAlpha(hex)` | Parse `#rgb` / `#rrggbb` / `#rrggbbaa`; returns `[r, g, b, a?]`. |
1425
- | `relativeLuminanceFromLinearRgb(rgb)` | WCAG relative luminance from linear sRGB. |
1426
- | `contrastRatioFromLuminance(yA, yB)` | WCAG contrast ratio from two luminances. |
1427
- | `gamutClampedLuminance(linearRgb)` | Relative luminance with channel clamping for out-of-gamut colors. |
1589
+ | Function | Description |
1590
+ | ------------------------------------- | ------------------------------------------------------------------------ |
1591
+ | `okhslToLinearSrgb(h, s, l)` | OKHSL (h: 0–360, s/l: 0–1) → linear sRGB tuple. |
1592
+ | `okhslToSrgb(h, s, l)` | OKHSL → gamma-encoded sRGB tuple (0–1 per channel). |
1593
+ | `okhslToOklab([h, s, l])` | OKHSL → OKLab `[L, a, b]`. |
1594
+ | `oklabToOkhsl([L, a, b])` | OKLab → OKHSL. |
1595
+ | `srgbToOkhsl([r, g, b])` | Gamma sRGB (0–1) → OKHSL. |
1596
+ | `hslToSrgb(h, s, l)` | CSS HSL → sRGB tuple. |
1597
+ | `parseHex(hex)` | Parse `#rgb` / `#rrggbb` to sRGB tuple. Returns `null` on invalid input. |
1598
+ | `parseHexAlpha(hex)` | Parse `#rgb` / `#rrggbb` / `#rrggbbaa`; returns `[r, g, b, a?]`. |
1599
+ | `relativeLuminanceFromLinearRgb(rgb)` | WCAG relative luminance from linear sRGB. |
1600
+ | `contrastRatioFromLuminance(yA, yB)` | WCAG contrast ratio from two luminances. |
1601
+ | `gamutClampedLuminance(linearRgb)` | Relative luminance with channel clamping for out-of-gamut colors. |
1428
1602
 
1429
1603
  ### Format writers
1430
1604
 
@@ -1432,8 +1606,8 @@ import {
1432
1606
  import { formatOkhsl, formatRgb, formatHsl, formatOklch } from '@tenphi/glaze';
1433
1607
 
1434
1608
  formatOkhsl(280, 60, 95); // 'okhsl(280 60% 95%)'
1435
- formatRgb(280, 60, 95); // 'rgb(244 240 250)'
1436
- formatHsl(280, 60, 95); // 'hsl(280 60% 95%)'
1609
+ formatRgb(280, 60, 95); // 'rgb(244 240 250)'
1610
+ formatHsl(280, 60, 95); // 'hsl(280 60% 95%)'
1437
1611
  formatOklch(280, 60, 95); // 'oklch(0.95 ... 280)'
1438
1612
  ```
1439
1613
 
@@ -1454,17 +1628,19 @@ import {
1454
1628
  } from '@tenphi/glaze';
1455
1629
  ```
1456
1630
 
1457
- | Function | Description |
1458
- |---|---|
1459
- | `toTone(l, eps?)` | OKHSL lightness (0–1) → tone (0–100). Defaults to `REF_EPS`. |
1460
- | `fromTone(t, eps?)` | Tone (0–100) → OKHSL lightness (0–1). Inverse of `toTone`. |
1461
- | `toneFromY(y, eps?)` / `yFromTone(t, eps?)` | Same transfer in luminance space (0–1). |
1462
- | `okhstToOkhsl({ h, s, t })` | OKHST → OKHSL (`{ h, s, l }`). |
1463
- | `okhslToOkhst({ h, s, l })` | OKHSL → OKHST (`{ h, s, t }`). |
1464
- | `variantToOkhsl(variant)` | `ResolvedColorVariant` (stores `t`) → `{ h, s, l, alpha }` for rendering. |
1465
- | `REF_EPS` | Reference epsilon (`0.05`) for the canonical tone axis. |
1631
+ | Function | Description |
1632
+ | ------------------------------------------- | ------------------------------------------------------------------------- |
1633
+ | `toTone(l, eps?)` | OKHSL lightness (0–1) → tone (0–100). Defaults to `REF_EPS`. |
1634
+ | `fromTone(t, eps?)` | Tone (0–100) → OKHSL lightness (0–1). Inverse of `toTone`. |
1635
+ | `toneFromY(y, eps?)` / `yFromTone(t, eps?)` | Same transfer in luminance space (0–1). |
1636
+ | `okhstToOkhsl({ h, s, t })` | OKHST → OKHSL (`{ h, s, l }`). |
1637
+ | `okhslToOkhst({ h, s, l })` | OKHSL → OKHST (`{ h, s, t }`). |
1638
+ | `variantToOkhsl(variant)` | `ResolvedColorVariant` (stores `t`) → `{ h, s, l, alpha }` for rendering. |
1639
+ | `REF_EPS` | Reference epsilon (`0.05`) for the canonical tone axis. |
1466
1640
 
1467
- `ResolvedColorVariant` now stores `{ h, s, t, alpha }` (tone, not lightness). Use `variantToOkhsl(variant).l` to recover OKHSL lightness. See [`docs/okhst.md`](okhst.md) for the full model.
1641
+ `ResolvedColorVariant` stores `{ h, s, t, alpha }` (tone, not lightness). Use
1642
+ `variantToOkhsl(variant).l` to recover OKHSL lightness. See
1643
+ [OKHST in Glaze](okhst.md) for the model.
1468
1644
 
1469
1645
  ### Contrast solver
1470
1646
 
@@ -1478,29 +1654,29 @@ import {
1478
1654
  } from '@tenphi/glaze';
1479
1655
  ```
1480
1656
 
1481
- | Function | Description |
1482
- |---|---|
1483
- | `findToneForContrast(opts)` | Binary-search for the tone (0–1) that meets a contrast floor (WCAG or APCA) against a base color. Returns `{ tone, contrast, met, branch, flipped? }`. |
1484
- | `findValueForMixContrast(opts)` | Same, but searches for a mix `value` (0–1) that meets a contrast floor between a base and a target. |
1657
+ | Function | Description |
1658
+ | ----------------------------------------------------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
1659
+ | `findToneForContrast(opts)` | Binary-search for the tone (0–1) that meets a contrast floor (WCAG or APCA) against a base color. Returns `{ tone, contrast, met, branch, flipped? }`. |
1660
+ | `findValueForMixContrast(opts)` | Same, but searches for a mix `value` (0–1) that meets a contrast floor between a base and a target. |
1485
1661
  | `resolveContrastForMode(spec, isHC, polarity?, outerExplicitHC?)` | Resolves a `ContrastSpec` to `{ metric: 'wcag' \| 'apca', target }` for the requested mode (picks the normal or HC entry of any pair). In HC, applies the metric's auto-enhancement unless `outerExplicitHC` is set or the inner metric pair carries an explicit HC value: APCA +15 Lc (clamped to 106); WCAG AA → AAA / AA-large → AAA-large (AAA-family and bare numbers unchanged). |
1486
- | `resolveMinContrast(value)` | Resolves a `MinContrast` (WCAG preset or number) to a numeric ratio. |
1487
- | `apcaContrast(yText, yBg)` | APCA Lc magnitude (0–106) for two relative luminances. |
1662
+ | `resolveMinContrast(value)` | Resolves a `MinContrast` (WCAG preset or number) to a numeric ratio. |
1663
+ | `apcaContrast(yText, yBg)` | APCA Lc magnitude (0–106) for two relative luminances. |
1488
1664
 
1489
1665
  Exported constants: `APCA_PRESETS`, `APCA_HC_ENHANCEMENT` (`15`, the Enhanced Level delta), `APCA_MAX_LC` (`106`).
1490
1666
 
1491
1667
  `findToneForContrast` options:
1492
1668
 
1493
- | Option | Default | Description |
1494
- |---|---|---|
1495
- | `hue` | — | Candidate hue (0–360). |
1496
- | `saturation` | — | Candidate saturation (0–1). |
1497
- | `preferredTone` | — | Preferred candidate tone (0–1). Kept if it already meets the target. |
1498
- | `baseLinearRgb` | — | Base color as linear sRGB tuple. |
1499
- | `contrast` | — | `ResolvedContrast` (`{ metric, target }`). |
1500
- | `toneRange` | `[0, 1]` | Search bounds in tone. |
1501
- | `epsilon` | `1e-4` | Convergence threshold. |
1502
- | `maxIterations` | `18` | Max binary-search iterations per branch. |
1503
- | `initialDirection` | higher-contrast side | Direction to search first (`'lighter'` or `'darker'`). |
1504
- | `flip` | `false` | When `true`, try the opposite direction if the initial one doesn't meet the target. When `false`, only the initial direction is searched — unmet contrasts pin the result to that direction's extreme. |
1669
+ | Option | Default | Description |
1670
+ | ------------------ | -------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ |
1671
+ | `hue` | — | Candidate hue (0–360). |
1672
+ | `saturation` | — | Candidate saturation (0–1). |
1673
+ | `preferredTone` | — | Preferred candidate tone (0–1). Kept if it already meets the target. |
1674
+ | `baseLinearRgb` | — | Base color as linear sRGB tuple. |
1675
+ | `contrast` | — | `ResolvedContrast` (`{ metric, target }`). |
1676
+ | `toneRange` | `[0, 1]` | Search bounds in tone. |
1677
+ | `epsilon` | `1e-4` | Convergence threshold. |
1678
+ | `maxIterations` | `18` | Max binary-search iterations per branch. |
1679
+ | `initialDirection` | higher-contrast side | Direction to search first (`'lighter'` or `'darker'`). |
1680
+ | `flip` | `false` | When `true`, try the opposite direction if the initial one doesn't meet the target. When `false`, only the initial direction is searched — unmet contrasts pin the result to that direction's extreme. |
1505
1681
 
1506
1682
  Result: `{ tone, contrast, met, branch: 'lighter' | 'darker' | 'preferred', flipped? }`. `flipped: true` indicates the initial direction failed and the opposite direction satisfied the target.