@tenphi/glaze 0.18.0 → 1.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/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,13 +28,13 @@ 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.from(data)` | Create a theme from an exported configuration (`theme.export()` snapshot). |
36
+ | `glaze.fromHex(hex)` | Create a theme from a hex color (`#rgb` or `#rrggbb`). Extracts hue and saturation. |
37
+ | `glaze.fromRgb(r, g, b)` | Create a theme from RGB values (0–255). Extracts hue and saturation. |
33
38
 
34
39
  ```ts
35
40
  const a = glaze(280, 80);
@@ -50,24 +55,27 @@ The optional `config` parameter is a `GlazeConfigOverride` — see [Per-instance
50
55
 
51
56
  A `GlazeTheme` exposes:
52
57
 
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. |
58
+ | Method | Description |
59
+ | ------------------------------- | --------------------------------------------------------------------------------------------------- |
60
+ | `theme.hue` (readonly) | The hue seed (0–360). |
61
+ | `theme.saturation` (readonly) | The saturation seed (0–100). |
62
+ | `theme.colors(defs)` | Add/replace colors (additive merge — adds new, overwrites existing by name, doesn't remove others). |
63
+ | `theme.color(name)` | Get a color definition by name. |
64
+ | `theme.color(name, def)` | Set a single color definition. |
65
+ | `theme.remove(name \| names[])` | Remove one or more color definitions. |
66
+ | `theme.has(name)` | Check if a color is defined. |
67
+ | `theme.list()` | List all defined color names. |
68
+ | `theme.reset()` | Clear all color definitions. |
69
+ | `theme.export()` | Export the theme configuration as a JSON-safe object. |
70
+ | `theme.extend(options)` | Create a child theme inheriting all color definitions (see [`extend`](#themeextendoptions) below). |
71
+ | `theme.resolve()` | Resolve all colors and return a `Map<string, ResolvedColor>`. |
72
+ | `theme.tokens(options?)` | Export as a flat token map grouped by scheme variant. |
73
+ | `theme.tasty(options?)` | Export as [Tasty](https://tasty.style) style-to-state bindings. |
74
+ | `theme.json(options?)` | Export as plain JSON. |
75
+ | `theme.css(options?)` | Export as CSS custom property declarations. |
76
+ | `theme.dtcg(options?)` | Export one W3C DTCG token tree per scheme. |
77
+ | `theme.dtcgResolver(options?)` | Export one DTCG Resolver-Module document containing every scheme. |
78
+ | `theme.tailwind(options?)` | Export a Tailwind CSS v4 theme and scheme overrides. |
71
79
 
72
80
  ### `theme.colors(defs)`
73
81
 
@@ -80,8 +88,8 @@ theme.colors({ text: { tone: 30 } });
80
88
  ### `theme.color(name) / theme.color(name, def)`
81
89
 
82
90
  ```ts
83
- theme.color('surface', { tone: 97, saturation: 0.75 }); // set
84
- const def = theme.color('surface'); // get
91
+ theme.color('surface', { tone: 97, saturation: 0.75 }); // set
92
+ const def = theme.color('surface'); // get
85
93
  ```
86
94
 
87
95
  ### `theme.extend(options)`
@@ -100,63 +108,75 @@ const highSat = base.extend({ config: { darkTone: [10, 100] } });
100
108
 
101
109
  `GlazeExtendOptions`:
102
110
 
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. |
111
+ | Field | Type | Description |
112
+ | ------------ | --------------------- | -------------------------------------------------------------------------------------------- |
113
+ | `hue` | `number` | Replace the hue seed. Defaults to the parent's hue. |
114
+ | `saturation` | `number` | Replace the saturation seed. Defaults to the parent's saturation. |
115
+ | `colors` | `ColorMap` | Per-theme overrides (additive merge over the inherited map). |
116
+ | `config` | `GlazeConfigOverride` | Config override for the child. Shallow-merged with the parent's override — child fields win. |
109
117
 
110
118
  Colors marked with `inherit: false` on the parent are **not** copied into the child.
111
119
 
120
+ ### `theme.resolve()`
121
+
122
+ Resolves the dependency graph and returns a
123
+ `Map<string, ResolvedColor>`. Export methods call it automatically; use it
124
+ directly for tests, diagnostics, or a custom output pipeline.
125
+
126
+ ```ts
127
+ const resolved = theme.resolve();
128
+ const surface = resolved.get('surface');
129
+ // surface?.light.t is canonical tone on 0–1.
130
+ ```
131
+
112
132
  ### `theme.tokens(options?)`
113
133
 
114
134
  Flat token map grouped by scheme variant.
115
135
 
116
136
  ```ts
117
- theme.tokens()
137
+ theme.tokens();
118
138
  // → { light: { surface: 'oklch(...)' }, dark: { surface: 'oklch(...)' } }
119
139
  ```
120
140
 
121
141
  `GlazeJsonOptions`:
122
142
 
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. |
143
+ | Option | Default | Description |
144
+ | -------- | -------------------------------------------------------- | ----------------------------------------------------------------------------------------------------------------- |
145
+ | `format` | `'oklch'` | Output color format. One of `'rgb' \| 'hsl' \| 'oklch'`. `'okhsl'` and `'okhst'` throw — use `tasty()` for those. |
146
+ | `modes` | `{ dark: true, highContrast: false }` (or global config) | Which scheme variants to include. |
127
147
 
128
148
  ### `theme.tasty(options?)`
129
149
 
130
- Tasty style-to-state bindings for the [Tasty style system](https://tasty.style/docs). Uses `#name` color token keys and state aliases (`''`, `@dark`, etc.).
150
+ 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
151
 
132
152
  ```ts
133
- theme.tasty()
153
+ theme.tasty();
134
154
  // → {
135
- // '#surface': { '': 'okhsl(...)', '@dark': 'okhsl(...)' },
155
+ // '#surface': { '': 'oklch(...)', '@media(prefers-color-scheme: dark)': 'oklch(...)' },
136
156
  // ...
137
157
  // }
138
158
  ```
139
159
 
140
160
  `GlazeTokenOptions`:
141
161
 
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` | `'@dark'` (or global config) | State alias for dark mode tokens. |
147
- | `states.highContrast` | `'@high-contrast'` (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). |
162
+ | Option | Default | Description |
163
+ | --------------------- | --------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------ |
164
+ | `format` | `'oklch'` | Output color format. `'okhsl'` and `'okhst'` are also supported here ([Tasty](https://tasty.style)-only spaces). |
165
+ | `modes` | global config | Which scheme variants to include. |
166
+ | `states.dark` | `'@media(prefers-color-scheme: dark)'` (or global config) | State alias for dark mode tokens. |
167
+ | `states.highContrast` | `'@media(prefers-contrast: more)'` (or global config) | State alias for high-contrast tokens. |
168
+ | `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. |
169
+ | `name` | `'theme'` | Base name for the theme-level hue var (`$theme-hue` / `--theme-hue`). Palette export auto-derives this from the theme name. |
170
+ | `prefix` | (palette only) | See [Palette](#palette). |
151
171
 
152
- When both `dark` and `highContrast` modes are enabled, dark high-contrast variants are emitted under the combined key `<dark> & <highContrast>` (e.g. `'@dark & @high-contrast'`).
172
+ 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
173
 
154
174
  ### `theme.json(options?)`
155
175
 
156
176
  Per-color JSON map.
157
177
 
158
178
  ```ts
159
- theme.json()
179
+ theme.json();
160
180
  // → {
161
181
  // surface: { light: 'oklch(...)', dark: 'oklch(...)' },
162
182
  // text: { light: 'oklch(...)', dark: 'oklch(...)' },
@@ -172,8 +192,8 @@ CSS custom property declaration strings, grouped by scheme variant.
172
192
  ```ts
173
193
  theme.css();
174
194
  // → {
175
- // light: '--surface-color: rgb(...);\n--text-color: rgb(...);',
176
- // dark: '--surface-color: rgb(...);\n--text-color: rgb(...);',
195
+ // light: '--surface-color: oklch(...);\n--text-color: oklch(...);',
196
+ // dark: '--surface-color: oklch(...);\n--text-color: oklch(...);',
177
197
  // lightContrast: '...',
178
198
  // darkContrast: '...',
179
199
  // }
@@ -181,12 +201,12 @@ theme.css();
181
201
 
182
202
  `GlazeCssOptions`:
183
203
 
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. |
204
+ | Option | Default | Description |
205
+ | ---------- | ---------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
206
+ | `format` | `'oklch'` | Output color format. One of `'rgb' \| 'hsl' \| 'oklch'`. `'okhsl'` and `'okhst'` throw — use `tasty()` for those. |
207
+ | `suffix` | `'-color'` | Suffix appended to each CSS property name. Pass `''` for bare property names. |
208
+ | `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). |
209
+ | `name` | `'theme'` | Base name for the theme-level hue var (`--theme-hue`). Palette export auto-derives this from the theme name. |
190
210
 
191
211
  `GlazeCssResult` always contains all four keys (`light`, `dark`, `lightContrast`, `darkContrast`); empty if no colors are defined for that variant.
192
212
 
@@ -195,7 +215,7 @@ theme.css();
195
215
  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
216
 
197
217
  ```ts
198
- theme.dtcg()
218
+ theme.dtcg();
199
219
  // → {
200
220
  // light: {
201
221
  // surface: {
@@ -216,10 +236,10 @@ Write each document to its own `.tokens.json` file — one file per scheme is th
216
236
 
217
237
  `GlazeDtcgOptions`:
218
238
 
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. |
239
+ | Option | Default | Description |
240
+ | ------------ | ------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
241
+ | `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. |
242
+ | `modes` | global config | Which scheme variants to include. `light` is always present. |
223
243
 
224
244
  `alpha` is included on `$value` only when the color's opacity is below 1. `$type` is always `'color'`.
225
245
 
@@ -228,7 +248,7 @@ Write each document to its own `.tokens.json` file — one file per scheme is th
228
248
  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
249
 
230
250
  ```ts
231
- theme.dtcgResolver({ modes: { highContrast: true } })
251
+ theme.dtcgResolver({ modes: { highContrast: true } });
232
252
  // → {
233
253
  // version: '2025.10',
234
254
  // sets: {
@@ -272,14 +292,14 @@ theme.dtcgResolver({ modes: { highContrast: true } })
272
292
 
273
293
  `GlazeDtcgResolverOptions` (extends `GlazeDtcgOptions`, so `modes` and `colorSpace` pass through):
274
294
 
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. |
295
+ | Option | Default | Description |
296
+ | -------------- | ------------- | ----------------------------------------------------------------------------------------------------------------- |
297
+ | `colorSpace` | `'srgb'` | Same as `dtcg()` — flows through to every source and context. |
298
+ | `modes` | global config | Which scheme variants to emit as contexts. `light` is always present (the default); absent variants are omitted. |
299
+ | `setName` | `'base'` | Name of the single set holding the default (light) token tree. |
300
+ | `modifierName` | `'scheme'` | Name of the modifier describing the scheme axis. |
301
+ | `contextNames` | identity | Override the four context names (`light` / `dark` / `lightContrast` / `darkContrast`) — e.g. `{ dark: 'night' }`. |
302
+ | `version` | `'2025.10'` | Resolver document version. |
283
303
 
284
304
  ### `theme.tailwind(options?)`
285
305
 
@@ -306,13 +326,13 @@ A Tailwind CSS v4 `@theme` block (light baseline) plus dark / high-contrast over
306
326
 
307
327
  `GlazeTailwindOptions`:
308
328
 
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. |
329
+ | Option | Default | Description |
330
+ | ---------------------- | ------------------ | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ |
331
+ | `format` | `'oklch'` | Output color format for the values. |
332
+ | `namespace` | `'color-'` | CSS custom property namespace, forming `--<namespace><name>` (e.g. `--color-surface`). Named `namespace` to avoid clashing with the palette theme-prefix option. |
333
+ | `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). |
334
+ | `highContrastSelector` | `'.high-contrast'` | Selector wrapping the light high-contrast overrides. The combined dark + high-contrast block uses `${darkSelector}${highContrastSelector}` (e.g. `.dark.high-contrast`). |
335
+ | `modes` | global config | Which scheme variants to include. The `@theme` block (light) is always emitted when colors exist. |
316
336
 
317
337
  ### `theme.export()`
318
338
 
@@ -327,6 +347,34 @@ The export contains only the configuration — not resolved color values. Resolv
327
347
 
328
348
  ---
329
349
 
350
+ ## High-contrast pairs
351
+
352
+ `HCPair<T>` means either one value used in both ordinary and high-contrast
353
+ schemes, or an explicit `[normal, highContrast]` pair:
354
+
355
+ ```ts
356
+ type HCPair<T> = T | [T, T];
357
+ ```
358
+
359
+ It is used by `tone`, `contrast`, shadow `intensity`, and mix `value`:
360
+
361
+ ```ts
362
+ tone: '-8'; // -8 in normal and HC
363
+ tone: ['-8', '-16']; // -8 normal, -16 HC
364
+ contrast: {
365
+ apca: 'content';
366
+ } // preset with automatic HC enhancement
367
+ contrast: {
368
+ apca: ['content', 'body'];
369
+ } // explicit normal/HC targets
370
+ ```
371
+
372
+ For `contrast`, the pair may wrap the whole spec or live inside the selected
373
+ metric. An explicit HC value disables automatic APCA enhancement or WCAG preset
374
+ promotion for that color.
375
+
376
+ ---
377
+
330
378
  ## Color definitions
331
379
 
332
380
  `ColorDef` is a discriminated union:
@@ -337,34 +385,42 @@ type ColorDef = RegularColorDef | ShadowColorDef | MixColorDef;
337
385
 
338
386
  ### `RegularColorDef`
339
387
 
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. |
388
+ | Field | Type | Description |
389
+ | ------------ | ------------------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
390
+ | `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]`. |
391
+ | `saturation` | `number` | Saturation factor applied to the seed saturation (0–1). Default: `1`. |
392
+ | `hue` | `number \| RelativeValue` | Number = absolute (0–360). String (`'+N'`/`'-N'`) = relative to the **theme seed hue** (never to a base color). |
393
+ | `base` | `string` | Name of another color in the same theme — makes this a _dependent_ color. |
394
+ | `contrast` | `HCPair<ContrastSpec>` | Contrast floor against `base`. Requires `base`. See [`contrast`](#contrast-floor). |
395
+ | `mode` | `'auto' \| 'fixed' \| 'static'` | Adaptation mode. Default: `'auto'`. See [Adaptation modes](#adaptation-modes). |
396
+ | `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). |
397
+ | `opacity` | `number` | Fixed alpha 0–1. Output includes alpha in the CSS value. Combining with `contrast` is not recommended (a `console.warn` is emitted). |
398
+ | `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). |
399
+ | `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). |
400
+ | `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
401
 
354
402
  #### Tone values
355
403
 
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).)
404
+ `tone` (0–100) replaces authored OKHSL lightness with a contrast-shaped axis.
405
+ Equal tone differences give equal WCAG contrast for neutrals; chromatic results
406
+ can drift in measured luminance. See [OKHST in Glaze](okhst.md). To port old
407
+ `lightness` values, see [migration.md](migration.md).
357
408
 
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. |
409
+ | Form | Example | Meaning |
410
+ | ------------------- | ----------------------- | --------------------------------------------------------------------------------------------- |
411
+ | Number (absolute) | `tone: 45` | Absolute tone 0–100. |
412
+ | String (tone delta) | `tone: '-52'` | Signed difference from the base color's resolved tone (requires `base`). |
413
+ | Extreme | `tone: 'max'` / `'min'` | Force to the scheme's highest (`'max'` = 100) or lowest (`'min'` = 0) tone. No `base` needed. |
414
+ | HC pair | `tone: ['-7', '-20']` | `[normal, high-contrast]`. A single value applies to both. |
364
415
 
365
416
  **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
417
 
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.
418
+ **A tone delta** applies a signed difference to the base color's resolved tone.
419
+ It gives an exact contrast step for neutrals and a stable visual progression for
420
+ chromatic colors. In dark mode with `mode: 'auto'`, it is anchored to the
421
+ base's per-scheme tone. If `base + delta` falls outside `[0, 100]`, the result
422
+ is clamped to the boundary, or — with `autoFlip` (default on) — mirrored to the
423
+ other side of the base.
368
424
 
369
425
  **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
426
 
@@ -384,30 +440,42 @@ A dependent color with `base` but no `tone` inherits the base's tone (equivalent
384
440
  ```ts
385
441
  type ContrastPreset = 'AA' | 'AAA' | 'AA-large' | 'AAA-large';
386
442
  type ContrastSpec =
387
- | number // bare WCAG ratio
388
- | ContrastPreset // named WCAG preset
443
+ | number // bare WCAG ratio
444
+ | ContrastPreset // named WCAG preset
389
445
  | { wcag: HCPair<number | ContrastPreset> }
390
- | { apca: HCPair<number> }; // APCA Lc target
446
+ | { apca: HCPair<number> }; // APCA Lc target
391
447
  ```
392
448
 
393
- | Preset | WCAG ratio |
394
- |---|---|
395
- | `'AA-large'` | 3 |
396
- | `'AA'` | 4.5 |
397
- | `'AAA-large'` | 4.5 |
398
- | `'AAA'` | 7 |
449
+ | Preset | WCAG ratio |
450
+ | ------------- | ---------- |
451
+ | `'AA-large'` | 3 |
452
+ | `'AA'` | 4.5 |
453
+ | `'AAA-large'` | 4.5 |
454
+ | `'AAA'` | 7 |
399
455
 
400
456
  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
457
 
402
458
  ```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)
459
+ contrast: 4.5; // WCAG 4.5
460
+ contrast: 'AAA'; // WCAG 7
461
+ contrast: {
462
+ wcag: 6;
463
+ } // WCAG 6
464
+ contrast: {
465
+ wcag: [4.5, 7];
466
+ } // WCAG 4.5 normal / 7 high-contrast (explicit)
467
+ contrast: {
468
+ apca: 60;
469
+ } // APCA Lc 60 normal / 75 high-contrast (auto)
470
+ contrast: {
471
+ apca: [45, 60];
472
+ } // APCA Lc 45 normal / 60 high-contrast (explicit)
473
+ contrast: {
474
+ apca: 'content';
475
+ } // APCA preset -> Lc 60 normal / 75 high-contrast (auto)
476
+ contrast: {
477
+ apca: ['content', 'body'];
478
+ } // Lc 60 normal / 75 high-contrast (explicit)
411
479
  ```
412
480
 
413
481
  **WCAG HC auto-promotion:** a bare WCAG preset (no `[normal, hc]` pair at either
@@ -424,30 +492,30 @@ boosted by **+15 Lc** in high-contrast mode, the APCA analog of WCAG's
424
492
  AAA-over-AA step. On by default; an explicit HC value via either pair
425
493
  overrides it and skips the boost. The enhanced target is clamped to 106 Lc.
426
494
  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.
495
+ an explicit HC pair to hold that ceiling.
429
496
 
430
497
  APCA preset keywords (Bronze Simple Mode conformance levels, role-independent):
431
498
  `'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.
499
+ `'non-text'` (30), `'min'` (15, point of invisibility).
434
500
 
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.
501
+ The floor is applied independently per scheme. If the preferred `tone` already
502
+ satisfies it, the tone is kept; otherwise the solver uses the tone-shaped scale
503
+ for a closed-form WCAG seed and fast search until the target is met.
436
504
 
437
505
  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
506
 
439
507
  **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
508
 
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.
509
+ **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
510
 
443
511
  #### Per-color hue override
444
512
 
445
513
  ```ts
446
514
  const theme = glaze(280, 80);
447
515
  theme.colors({
448
- surface: { tone: 97 },
449
- gradientEnd: { tone: 90, hue: '+20' }, // 280 + 20 = 300
450
- warning: { tone: 60, hue: 40 }, // absolute
516
+ surface: { tone: 97 },
517
+ gradientEnd: { tone: 90, hue: '+20' }, // 280 + 20 = 300
518
+ warning: { tone: 60, hue: 40 }, // absolute
451
519
  });
452
520
  ```
453
521
 
@@ -461,7 +529,7 @@ Relative hue is always relative to the **theme seed hue**, not to a base color.
461
529
  const theme = glaze(280, 80);
462
530
  theme.colors({
463
531
  plain: { tone: 50, saturation: 1 },
464
- soft: { tone: 50, saturation: 1, pastel: true },
532
+ soft: { tone: 50, saturation: 1, pastel: true },
465
533
  });
466
534
  // theme.resolve().get('soft')!.light.pastel === true
467
535
  // theme.css().light contains different rgb() triples for `--plain` and `--soft`
@@ -489,11 +557,11 @@ const child = parent.extend({
489
557
 
490
558
  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
559
 
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` |
560
+ | Role | Polarity | Use | Aliases (name inference) |
561
+ | ----------- | -------- | ---------------------------------------------------- | ----------------------------------------------------------------- |
562
+ | `'text'` | fg | Text / icons / foreground content | `text`, `fg`, `foreground`, `content`, `ink`, `label`, `stroke` |
563
+ | `'border'` | fg | Non-text spot elements (borders, dividers, outlines) | `border`, `divider`, `outline`, `separator`, `hairline`, `rule` |
564
+ | `'surface'` | bg | Backgrounds / fills | `surface`, `bg`, `background`, `fill`, `canvas`, `paper`, `layer` |
497
565
 
498
566
  Resolution chain (per color):
499
567
 
@@ -506,8 +574,8 @@ Resolution chain (per color):
506
574
  const theme = glaze(280, 60);
507
575
  theme.colors({
508
576
  surface: { tone: 90 },
509
- text: { base: 'surface', contrast: { apca: 'content' } }, // inferred text
510
- border: { base: 'surface', tone: '-10' }, // inferred border
577
+ text: { base: 'surface', contrast: { apca: 'content' } }, // inferred text
578
+ border: { base: 'surface', tone: '-10' }, // inferred border
511
579
  });
512
580
  // role fixes APCA polarity; set `pastel: true` explicitly if a border
513
581
  // needs the hue-independent safe chroma limit.
@@ -517,32 +585,32 @@ Disable name inference with `glaze.configure({ inferRole: false })` (the base-op
517
585
 
518
586
  ### `ShadowColorDef`
519
587
 
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`. |
588
+ | Field | Type | Description |
589
+ | ----------- | ---------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
590
+ | `type` | `'shadow'` | Discriminator. |
591
+ | `bg` | `string` | Background color name — must reference a non-shadow color in the same theme. |
592
+ | `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. |
593
+ | `intensity` | `HCPair<number>` | Shadow intensity, 0–100. Supports HC pairs. |
594
+ | `tuning` | `ShadowTuning` | Per-color tuning overrides. Merged field-by-field with the global `shadowTuning`. |
595
+ | `pastel` | `boolean` | Per-color `pastel` override. See [Per-color `pastel`](#per-color-pastel). |
596
+ | `inherit` | `boolean` | Inheritance flag, default `true`. |
529
597
 
530
598
  See [Shadows](#shadows) below for the algorithm and tuning details.
531
599
 
532
600
  ### `MixColorDef`
533
601
 
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`. |
602
+ | Field | Type | Description |
603
+ | ---------- | --------------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------- |
604
+ | `type` | `'mix'` | Discriminator. |
605
+ | `base` | `string` | "From" color name. |
606
+ | `target` | `string` | "To" color name. |
607
+ | `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. |
608
+ | `blend` | `'opaque' \| 'transparent'` | Default `'opaque'`. |
609
+ | `space` | `'okhsl' \| 'srgb'` | Interpolation space for opaque blending. Default `'okhsl'`. Ignored for `'transparent'` (always composites in linear sRGB). |
610
+ | `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). |
611
+ | `pastel` | `boolean` | Per-color `pastel` override. See [Per-color `pastel`](#per-color-pastel). |
612
+ | `role` | `RoleInput` | Semantic role of the mixed result against `base`. Same semantics as `RegularColorDef.role` (see [Roles](#roles)). |
613
+ | `inherit` | `boolean` | Inheritance flag, default `true`. |
546
614
 
547
615
  See [Mix colors](#mix-colors) below.
548
616
 
@@ -562,62 +630,62 @@ glaze.color(color: GlazeFromInput | GlazeColorInput | GlazeColorValue, config?:
562
630
 
563
631
  `glaze.color()` accepts **four input shapes**, discriminated by structure:
564
632
 
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). |
633
+ | Shape | Example | Notes |
634
+ | ---------------------------- | ----------------------------------------------- | ----------------------------------------------------------------------------------- |
635
+ | **Bare string** | `'#26fcb2'` | Hex or CSS color function (`rgb()`, `hsl()`, `okhsl()`, `okhst()`, `oklch()`). |
636
+ | **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). |
637
+ | **`{ from, ...overrides }`** | `{ from: '#1a1a2e', base: bg, contrast: 'AA' }` | Value + color overrides in one object. |
638
+ | **Structured** | `{ hue: 152, saturation: 95, tone: 74 }` | Full theme-style token (hue/saturation in 0–100, tone in 0–100). |
571
639
 
572
640
  `GlazeColorValue` (bare string or value-object forms) accepts:
573
641
 
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. |
642
+ | Form | Example | Notes |
643
+ | ------------------- | ---------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------- |
644
+ | Hex | `'#26fcb2'`, `'#26fcb2ff'`, `'#abc'` | 3, 6, or 8 digits. Alpha is dropped with a `console.warn` — use `opacity` instead. |
645
+ | `rgb()` | `'rgb(38 252 178)'`, `'rgb(38 252 178 / 0.8)'` | Modern space syntax. Alpha dropped with warning. |
646
+ | `hsl()` | `'hsl(152 97% 57%)'` | Modern space syntax. Alpha dropped with warning. |
647
+ | `okhsl()` | `'okhsl(152 95% 74%)'` | Glaze's own emit format. Alpha dropped with warning. |
648
+ | `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. |
649
+ | `oklch()` | `'oklch(0.85 0.18 152)'` | Glaze's own emit format. Alpha dropped with warning. |
650
+ | `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. |
651
+ | `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 }`. |
652
+ | `RgbColor` object | `{ r: 38, g: 252, b: 178 }` | sRGB 0–255. RGB tuple `[r, g, b]` is not supported — use this object form. |
653
+ | `OklchColor` object | `{ l: 0.85, c: 0.18, h: 152 }` | OKLCh (L/C: 0–1, H: degrees), same semantics as `oklch()` strings. |
586
654
 
587
655
  `GlazeColorInput` (structured form) is `{ hue, saturation, tone, ... }`:
588
656
 
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. |
657
+ | Field | Type | Description |
658
+ | ------------------ | ------------------------------------ | ----------------------------------------------------------------------------------------------------------------------------------------------- |
659
+ | `hue` | `number` | 0–360. |
660
+ | `saturation` | `number` | 0–100. |
661
+ | `tone` | `HCPair<number \| ExtremeValue>` | 0–100 (contrast-shaped) or `'max'`/`'min'`, optional HC pair. |
662
+ | `saturationFactor` | `number` | Multiplier on the seed (0–1). Default: `1`. |
663
+ | `mode` | `AdaptationMode` | Default: `'auto'`. |
664
+ | `autoFlip` | `boolean` | Flip out-of-bounds results instead of clamping. Default: global `autoFlip`. |
665
+ | `opacity` | `number` | Fixed alpha 0–1. |
666
+ | `base` | `GlazeColorToken \| GlazeColorValue` | Optional dependency. See [Pairing colors](#pairing-colors). |
667
+ | `contrast` | `HCPair<ContrastSpec>` | Contrast floor against `base` (WCAG or APCA). Without `base`, anchored to the literal seed. |
668
+ | `pastel` | `boolean` | Per-color `pastel` override. Falls through to the global / per-theme `pastel` config when omitted. See [Per-color `pastel`](#per-color-pastel). |
669
+ | `role` | `RoleInput` | Semantic role against `base` / the seed (see [Roles](#roles)). Fixes APCA polarity. |
670
+ | `name` | `string` | Debug label for warnings; doesn't change output keys. Reserved names (`'value'`, `'seed'`, `'externalBase'`) are rejected. |
603
671
 
604
672
  `GlazeFromInput` (from form) is `{ from: GlazeColorValue, ...colorOverrides }`:
605
673
 
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. |
674
+ | Field | Notes |
675
+ | ------------------ | ----------------------------------------------------------------------------------------------------------------------------------------------------------- |
676
+ | `from` | **Required.** The source color value — same forms as `GlazeColorValue`. |
677
+ | `hue` | Number (absolute 0–360) or `'+N'`/`'-N'` (relative to seed, never to `base`). |
678
+ | `saturation` | Override seed saturation (0–100). |
679
+ | `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. |
680
+ | `saturationFactor` | Multiplier on the seed (0–1). |
681
+ | `mode` | `'auto'` (default) / `'fixed'` / `'static'`. |
682
+ | `autoFlip` | Flip out-of-bounds results instead of clamping. Default: global `autoFlip`. |
683
+ | `contrast` | Contrast floor (WCAG or APCA). Without `base`, anchored to the literal seed; with `base`, solved per scheme. |
684
+ | `base` | `GlazeColorToken` or raw `GlazeColorValue`. See [Pairing colors](#pairing-colors). |
685
+ | `opacity` | Fixed alpha 0–1. Combining with `contrast` is not recommended — `console.warn` is emitted. |
686
+ | `pastel` | Per-color `pastel` override. Falls through to the global / per-theme `pastel` config when omitted. See [Per-color `pastel`](#per-color-pastel). |
687
+ | `role` | Semantic role against `base` / the seed (see [Roles](#roles)). Fixes APCA polarity. |
688
+ | `name` | Debug label only — surfaces in warnings/errors. Does not change output keys. |
621
689
 
622
690
  Named CSS colors (`'red'`, `'blueviolet'`) are not supported.
623
691
 
@@ -634,71 +702,78 @@ Every input form defaults to `mode: 'auto'` so the resolved token adapts between
634
702
 
635
703
  ```ts
636
704
  // Bare string — adapts automatically
637
- glaze.color('#26fcb2')
705
+ glaze.color('#26fcb2');
638
706
 
639
707
  // Value-object — same behavior
640
- glaze.color({ h: 152, s: 0.95, l: 0.74 })
708
+ glaze.color({ h: 152, s: 0.95, l: 0.74 });
641
709
 
642
710
  // OKHST value-object — tone axis
643
- glaze.color({ h: 152, s: 0.95, t: 0.70 })
711
+ glaze.color({ h: 152, s: 0.95, t: 0.7 });
644
712
 
645
713
  // From form — value + color overrides
646
- glaze.color({ from: '#1a1a2e', hue: '+20', contrast: 'AA' })
714
+ glaze.color({ from: '#1a1a2e', hue: '+20', contrast: 'AA' });
647
715
 
648
716
  // Structured form — explicit hue/saturation/tone (0–100)
649
- glaze.color({ hue: 152, saturation: 95, tone: 74 })
717
+ glaze.color({ hue: 152, saturation: 95, tone: 74 });
650
718
  ```
651
719
 
652
720
  ### Token methods
653
721
 
654
722
  A `GlazeColorToken` exposes:
655
723
 
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`. |
724
+ | Method | Description |
725
+ | --------------------------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
726
+ | `token.resolve()` | Resolve as a `ResolvedColor` (light/dark/lightContrast/darkContrast variants). |
727
+ | `token.token(options?)` | Flat token map (no color-name key). Options: `format`, `modes`, `states`. |
728
+ | `token.tasty(options?)` | [Tasty](https://tasty.style) state map (no color-name key). Same options as `token.token`. |
729
+ | `token.json(options?)` | JSON map (no color-name key). Options: `format`, `modes`. |
662
730
  | `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. |
731
+ | `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`. |
732
+ | `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`. |
733
+ | `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`. |
734
+ | `token.export()` | JSON-safe snapshot — pass to `glaze.colorFrom(...)` to rehydrate. |
667
735
 
668
736
  ### Per-instance config override
669
737
 
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).
738
+ The optional `config` argument (`GlazeConfigOverride`) overrides
739
+ resolve-relevant global fields for a token or theme. A tone window can be
740
+ `[lo, hi]`, `{ lo, hi, eps }`, or `false` (full range). Standalone tokens
741
+ snapshot omitted fields at creation; themes continue reading omitted fields
742
+ from the live global config at resolve time.
671
743
 
672
744
  `GlazeConfigOverride`:
673
745
 
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). |
746
+ | Field | Default (from global) | Description |
747
+ | ------------------ | --------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
748
+ | `lightTone` | `[10, 100]` | Light tone window: `[lo, hi]`, `{ lo, hi, eps }`, or `false` (disable clamping). |
749
+ | `darkTone` | `[15, 95]` | Dark tone window: `[lo, hi]`, `{ lo, hi, eps }`, or `false` (disable clamping). |
750
+ | `darkDesaturation` | `0.1` | Saturation reduction in dark scheme (0–1). |
751
+ | `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. |
752
+ | `shadowTuning` | `undefined` | Default shadow tuning (meaningful for themes; harmless on color tokens). |
681
753
 
682
754
  Config overrides apply to both `glaze.color()` tokens and `glaze()` themes:
683
755
 
684
756
  ```ts
685
757
  // Standalone color — preserve raw tone in both schemes
686
- glaze.color('#26fcb2', { darkTone: false })
758
+ glaze.color('#26fcb2', { darkTone: false });
687
759
 
688
760
  // Restore the #000 → white dark flip (full dark range)
689
761
  glaze.color('#000000', {
690
762
  lightTone: false,
691
763
  darkTone: [15, 100],
692
- })
764
+ });
693
765
 
694
766
  // Structured form with config override
695
- glaze.color({ hue: 152, saturation: 95, tone: 74 }, { darkTone: false })
767
+ glaze.color({ hue: 152, saturation: 95, tone: 74 }, { darkTone: false });
696
768
 
697
769
  // Theme with config override
698
- const rawTheme = glaze(280, 80, { lightTone: false })
770
+ const rawTheme = glaze(280, 80, { lightTone: false });
699
771
  ```
700
772
 
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)).
773
+ Standalone color tokens snapshot their effective override at creation time.
774
+ Themes behave differently: overridden fields stay fixed, while fields omitted
775
+ from the override are read from the live global config at resolve time. See
776
+ [Theme config override](#theme-config-override).
702
777
 
703
778
  ### Theme config override
704
779
 
@@ -748,7 +823,8 @@ const bg = glaze.color('#1a1a2e');
748
823
  const text = glaze.color({ from: '#ffffff', base: bg, contrast: 'AA' });
749
824
 
750
825
  // Border 8 tone units lighter than `bg` in each scheme.
751
- const border = glaze.color({ from: '#000000',
826
+ const border = glaze.color({
827
+ from: '#000000',
752
828
  base: bg,
753
829
  tone: '+8',
754
830
  mode: 'fixed',
@@ -789,7 +865,7 @@ The `name` override appears in `console.warn` / Error messages but **does not**
789
865
  ```ts
790
866
  theme.colors({
791
867
  surface: { tone: 95 },
792
- text: { base: 'surface', tone: '-52', contrast: 'AAA' },
868
+ text: { base: 'surface', tone: '-52', contrast: 'AAA' },
793
869
 
794
870
  'shadow-sm': { type: 'shadow', bg: 'surface', fg: 'text', intensity: 5 },
795
871
  'shadow-md': { type: 'shadow', bg: 'surface', fg: 'text', intensity: 10 },
@@ -828,20 +904,22 @@ theme.colors({
828
904
 
829
905
  Fine-tune behavior per-color or globally via `glaze.configure({ shadowTuning })`. Per-color `tuning` is merged field-by-field with the global one.
830
906
 
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. |
907
+ | Parameter | Default | Description |
908
+ | ------------------ | -------------- | ------------------------------------------------------------------------------------- |
909
+ | `saturationFactor` | `0.18` | Fraction of fg saturation kept in pigment. |
910
+ | `maxSaturation` | `0.25` | Upper clamp on pigment saturation. |
911
+ | `lightnessFactor` | `0.25` | Multiplier for bg lightness → pigment lightness. |
912
+ | `lightnessBounds` | `[0.05, 0.20]` | Clamp range for pigment lightness. |
913
+ | `minGapTarget` | `0.05` | Target minimum gap between pigment and bg lightness. |
914
+ | `alphaMax` | `1.0` | Asymptotic maximum alpha. |
915
+ | `bgHueBlend` | `0.2` | Blend weight pulling pigment hue toward bg hue. `0` = pure fg hue, `1` = pure bg hue. |
840
916
 
841
917
  ```ts
842
918
  theme.colors({
843
919
  'shadow-soft': {
844
- type: 'shadow', bg: 'surface', intensity: 10,
920
+ type: 'shadow',
921
+ bg: 'surface',
922
+ intensity: 10,
845
923
  tuning: { alphaMax: 0.3, saturationFactor: 0.1 },
846
924
  },
847
925
  });
@@ -869,12 +947,12 @@ const css = glaze.format(v, 'oklch');
869
947
 
870
948
  `GlazeShadowInput`:
871
949
 
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. |
950
+ | Field | Type | Description |
951
+ | ----------- | ----------------- | ------------------------------------------------------------------------------ |
952
+ | `bg` | `GlazeColorValue` | Background. Any `GlazeColorValue` form. Alpha components dropped with warning. |
953
+ | `fg` | `GlazeColorValue` | Optional foreground. Same forms as `bg`. |
954
+ | `intensity` | `number` | 0–100. |
955
+ | `tuning` | `ShadowTuning` | Optional. |
878
956
 
879
957
  ### Fixed opacity (regular colors)
880
958
 
@@ -898,8 +976,8 @@ Produces a solid color by interpolating between `base` and `target`:
898
976
  ```ts
899
977
  theme.colors({
900
978
  surface: { tone: 95 },
901
- accent: { tone: 30 },
902
- tint: { type: 'mix', base: 'surface', target: 'accent', value: 30 },
979
+ accent: { tone: 30 },
980
+ tint: { type: 'mix', base: 'surface', target: 'accent', value: 30 },
903
981
  });
904
982
  ```
905
983
 
@@ -914,10 +992,13 @@ Produces the target color with controlled opacity — useful for hover overlays:
914
992
  ```ts
915
993
  theme.colors({
916
994
  surface: { tone: 95 },
917
- black: { tone: 0, saturation: 0 },
995
+ black: { tone: 0, saturation: 0 },
918
996
  hover: {
919
- type: 'mix', base: 'surface', target: 'black',
920
- value: 8, blend: 'transparent',
997
+ type: 'mix',
998
+ base: 'surface',
999
+ target: 'black',
1000
+ value: 8,
1001
+ blend: 'transparent',
921
1002
  },
922
1003
  });
923
1004
  // hover → black with alpha = 0.08
@@ -927,10 +1008,10 @@ The output color has `h`, `s`, `l` from the target and `alpha = value / 100`.
927
1008
 
928
1009
  ### Blend space (opaque only)
929
1010
 
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. |
1011
+ | `space` | Behavior | Best for |
1012
+ | ------------------- | ----------------------------------------- | -------------------------------------------------------- |
1013
+ | `'okhsl'` (default) | Perceptually uniform OKHSL interpolation. | Design token derivation. |
1014
+ | `'srgb'` | Linear sRGB channel interpolation. | Matching browser compositing of CSS color-mix / overlay. |
934
1015
 
935
1016
  Transparent blending always composites in linear sRGB (matches browser alpha compositing).
936
1017
 
@@ -963,8 +1044,20 @@ Mix colors can reference other mix colors:
963
1044
  theme.colors({
964
1045
  white: { tone: 100, saturation: 0 },
965
1046
  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' },
1047
+ gray: {
1048
+ type: 'mix',
1049
+ base: 'white',
1050
+ target: 'black',
1051
+ value: 50,
1052
+ space: 'srgb',
1053
+ },
1054
+ lightGray: {
1055
+ type: 'mix',
1056
+ base: 'white',
1057
+ target: 'gray',
1058
+ value: 50,
1059
+ space: 'srgb',
1060
+ },
968
1061
  });
969
1062
  ```
970
1063
 
@@ -986,35 +1079,41 @@ const palette = glaze.palette(
986
1079
 
987
1080
  `GlazePaletteOptions`:
988
1081
 
989
- | Option | Description |
990
- |---|---|
1082
+ | Option | Description |
1083
+ | --------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
991
1084
  | `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
1085
 
993
1086
  A `GlazePalette` exposes:
994
1087
 
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. |
1088
+ | Method | Description |
1089
+ | -------------------------------- | ------------------------------------------------------------ |
1090
+ | `palette.tokens(options?)` | Flat token map grouped by scheme variant. |
1091
+ | `palette.tasty(options?)` | [Tasty](https://tasty.style) style-to-state bindings. |
1092
+ | `palette.json(options?)` | Per-theme JSON map (no prefix needed — keyed by theme name). |
1093
+ | `palette.css(options?)` | CSS custom property declaration strings. |
1094
+ | `palette.dtcg(options?)` | Per-scheme W3C DTCG token trees. |
1095
+ | `palette.dtcgResolver(options?)` | One DTCG Resolver-Module document for every scheme. |
1096
+ | `palette.tailwind(options?)` | One Tailwind CSS v4 theme with scheme overrides. |
1001
1097
 
1002
1098
  ### `GlazePaletteExportOptions`
1003
1099
 
1004
1100
  Shared by `tokens`, `tasty`, and `css`:
1005
1101
 
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. |
1102
+ | Option | Default | Description |
1103
+ | --------- | ------------------------------ | -------------------------------------------------------------------------------------------- |
1104
+ | `prefix` | `true` (= `"<themeName>-"`) | `false` disables prefixing. Or pass a custom map: `{ primary: 'brand-', danger: 'error-' }`. |
1105
+ | `primary` | inherits from palette creation | `string` to override, `false` to disable for this call. |
1010
1106
 
1011
1107
  Each export method also accepts its own format/options shape:
1012
1108
 
1013
- | Method | Additional options |
1014
- |---|---|
1015
- | `palette.tokens(options?)` | `format`, `modes` |
1016
- | `palette.tasty(options?)` | `format`, `modes`, `states` |
1017
- | `palette.css(options?)` | `format`, `suffix` |
1109
+ | Method | Additional options |
1110
+ | -------------------------------- | ----------------------------------------- |
1111
+ | `palette.tokens(options?)` | `format`, `modes` |
1112
+ | `palette.tasty(options?)` | `format`, `modes`, `states` |
1113
+ | `palette.css(options?)` | `format`, `suffix` |
1114
+ | `palette.dtcg(options?)` | `colorSpace`, `modes` |
1115
+ | `palette.dtcgResolver(options?)` | `colorSpace`, `modes`, resolver names |
1116
+ | `palette.tailwind(options?)` | `format`, `modes`, selectors, `namespace` |
1018
1117
 
1019
1118
  `palette.css()` does not accept `modes`; it always returns all four CSS strings (`light`, `dark`, `lightContrast`, `darkContrast`).
1020
1119
 
@@ -1025,8 +1124,8 @@ By default all palette tokens are prefixed:
1025
1124
  ```ts
1026
1125
  palette.tokens();
1027
1126
  // → {
1028
- // light: { 'primary-surface': 'okhsl(...)', 'danger-surface': 'okhsl(...)' },
1029
- // dark: { 'primary-surface': 'okhsl(...)', 'danger-surface': 'okhsl(...)' },
1127
+ // light: { 'primary-surface': 'oklch(...)', 'danger-surface': 'oklch(...)' },
1128
+ // dark: { 'primary-surface': 'oklch(...)', 'danger-surface': 'oklch(...)' },
1030
1129
  // }
1031
1130
  ```
1032
1131
 
@@ -1062,10 +1161,10 @@ const palette = glaze.palette(
1062
1161
  palette.tokens();
1063
1162
  // → {
1064
1163
  // light: {
1065
- // 'primary-surface': 'okhsl(...)',
1066
- // 'danger-surface': 'okhsl(...)',
1067
- // 'success-surface': 'okhsl(...)',
1068
- // 'surface': 'okhsl(...)', // unprefixed alias
1164
+ // 'primary-surface': 'oklch(...)',
1165
+ // 'danger-surface': 'oklch(...)',
1166
+ // 'success-surface': 'oklch(...)',
1167
+ // 'surface': 'oklch(...)', // unprefixed alias
1069
1168
  // },
1070
1169
  // }
1071
1170
  ```
@@ -1091,8 +1190,8 @@ JSON export groups by theme name (no prefix needed):
1091
1190
  ```ts
1092
1191
  palette.json();
1093
1192
  // → {
1094
- // primary: { surface: { light: 'okhsl(...)', dark: 'okhsl(...)' } },
1095
- // danger: { surface: { light: 'okhsl(...)', dark: 'okhsl(...)' } },
1193
+ // primary: { surface: { light: 'oklch(...)', dark: 'oklch(...)' } },
1194
+ // danger: { surface: { light: 'oklch(...)', dark: 'oklch(...)' } },
1096
1195
  // }
1097
1196
  ```
1098
1197
 
@@ -1116,7 +1215,7 @@ It does not accept `modes`; all four result fields are always returned.
1116
1215
  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
1216
 
1118
1217
  ```ts
1119
- palette.dtcg()
1218
+ palette.dtcg();
1120
1219
  // → {
1121
1220
  // light: {
1122
1221
  // 'primary-surface': { $type: 'color', $value: { ... } },
@@ -1134,7 +1233,7 @@ Accepts `GlazeDtcgOptions` plus `GlazePaletteExportOptions`.
1134
1233
  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
1234
 
1136
1235
  ```ts
1137
- palette.dtcgResolver()
1236
+ palette.dtcgResolver();
1138
1237
  // → {
1139
1238
  // version: '2025.10',
1140
1239
  // sets: { base: { sources: [ { 'primary-surface': {…}, 'surface': {…}, 'danger-surface': {…} } ] } },
@@ -1167,24 +1266,24 @@ Accepts `GlazeTailwindOptions` plus `GlazePaletteExportOptions`. The palette `pr
1167
1266
 
1168
1267
  Control the color format with the `format` option on any export method:
1169
1268
 
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. |
1269
+ | Format | Output (alpha = 1) | Output (alpha < 1) | Notes |
1270
+ | --------------------------------------------- | ------------------ | -------------------- | --------------------------------------------------------------------------------------------- |
1271
+ | `'oklch'` (default for CSS-string exports) | `oklch(L C H)` | `oklch(L C H / A)` | OKLab-based LCH. Native CSS. Required for `splitHue`. |
1272
+ | `'rgb'` | `rgb(R G B)` | `rgb(R G B / A)` | Rounded integers, modern space syntax. |
1273
+ | `'hsl'` | `hsl(H S% L%)` | `hsl(H S% L% / A)` | Modern space syntax. |
1274
+ | `'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()`). |
1275
+ | `'okhst'` | `okhst(H S% T%)` | `okhst(H S% T% / A)` | OKHST tone axis. **[Tasty](https://tasty.style)-only** — same restriction as `okhsl`. |
1177
1276
 
1178
1277
  ```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%)'
1278
+ theme.tokens(); // 'oklch(0.965 0.0123 280)' (default)
1279
+ theme.tokens({ format: 'rgb' }); // 'rgb(244 240 250)'
1280
+ theme.tasty(); // 'oklch(0.965 0.0123 280)' (default)
1281
+ theme.tasty({ format: 'okhst' }); // 'okhst(280 60% 97%)'
1183
1282
  ```
1184
1283
 
1185
1284
  All numeric output strips trailing zeros for cleaner CSS (e.g. `95` not `95.0`).
1186
1285
 
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.
1286
+ 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
1287
 
1189
1288
  ### Hue channel splitting (`splitHue`)
1190
1289
 
@@ -1210,11 +1309,11 @@ On `theme.css()`, `theme.tasty()`, `palette.css()`, `palette.tasty()`, and stand
1210
1309
 
1211
1310
  `mode` controls how a color adapts across schemes:
1212
1311
 
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. |
1312
+ | Mode | Behavior |
1313
+ | ------------------ | -------------------------------------------------------------------------------------------------------------------------------------------- |
1314
+ | `'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. |
1315
+ | `'fixed'` | Color stays recognizable. Tone is _mapped_ (not inverted) into the dark window. Use for brand buttons, CTAs, status banners. |
1316
+ | `'static'` | No adaptation. Same tone in every scheme. |
1218
1317
 
1219
1318
  ### How relative tone adapts
1220
1319
 
@@ -1241,11 +1340,17 @@ Offsets that would push past `[0, 100]` clamp to the boundary, or — with `auto
1241
1340
 
1242
1341
  ## Light / dark scheme mapping
1243
1342
 
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.
1343
+ The mapping is a single tone pipeline; there is no Möbius curve. See
1344
+ [Scheme adaptation](okhst.md#scheme-adaptation) for the product-level model and
1345
+ the [canonical OKHST specification](https://github.com/tenphi/okhst) for the
1346
+ transfer math.
1245
1347
 
1246
1348
  ### Light scheme
1247
1349
 
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.
1350
+ An authored tone (0–100) is remapped into the `lightTone` **tone window**.
1351
+ The window's `lo`/`hi` are OKHSL-lightness boundaries (0–100); authored tone is
1352
+ positioned within the corresponding tone interval and converted to final OKHSL
1353
+ lightness. `static` mode and HC variants use the full range.
1249
1354
 
1250
1355
  ```
1251
1356
  window = lightTone // default [10, 100]
@@ -1263,7 +1368,10 @@ inverted = 100 - authorTone
1263
1368
  finalTone = remap(inverted, window)
1264
1369
  ```
1265
1370
 
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`).
1371
+ The inversion preserves authored tone spacing without a fitted curve. This is
1372
+ exactly contrast-even for neutrals and approximate for chromatic colors. The
1373
+ ordinary light/dark asymmetry lives in the two windows' `(lo, hi, eps)` values
1374
+ (`eps` defaults to the reference `0.05`).
1267
1375
 
1268
1376
  **`fixed`** — remap into the dark window without inversion:
1269
1377
 
@@ -1278,7 +1386,7 @@ In high-contrast variants both windows are bypassed (forced to the full `[0, 100
1278
1386
  `darkDesaturation` reduces saturation for all colors in dark scheme:
1279
1387
 
1280
1388
  ```ts
1281
- S_dark = S_light * (1 - darkDesaturation) // default: 0.1
1389
+ S_dark = S_light * (1 - darkDesaturation); // default: 0.1
1282
1390
  ```
1283
1391
 
1284
1392
  `static` mode skips desaturation.
@@ -1290,11 +1398,11 @@ S_dark = S_light * (1 - darkDesaturation) // default: 0.1
1290
1398
  ```ts
1291
1399
  glaze.configure({
1292
1400
  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
1401
+ darkTone: [15, 95], // [lo, hi]; or { lo, hi, eps } / false to disable clamping
1294
1402
  darkDesaturation: 0.1,
1295
1403
  states: {
1296
- dark: '@dark',
1297
- highContrast: '@high-contrast',
1404
+ dark: '@media(prefers-color-scheme: dark)',
1405
+ highContrast: '@media(prefers-contrast: more)',
1298
1406
  },
1299
1407
  modes: {
1300
1408
  dark: true,
@@ -1307,29 +1415,32 @@ glaze.configure({
1307
1415
  });
1308
1416
  ```
1309
1417
 
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.
1418
+ A `ToneWindow` is `[lo, hi]` (OKHSL-lightness boundaries, reference eps — the
1419
+ common form), `{ lo, hi, eps }` (advanced: explicit per-scheme render eps), or
1420
+ `false` for the full range `[0, 100]` at the reference eps. `false` removes the
1421
+ boundaries, not the tone transfer.
1311
1422
 
1312
1423
  `GlazeConfig`:
1313
1424
 
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` | `'@dark'` | State alias for dark mode tokens (Tasty export). |
1320
- | `states.highContrast` | `'@high-contrast'` | State alias for HC tokens. |
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
- |---|---|
1425
+ | Field | Default | Description |
1426
+ | --------------------- | -------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
1427
+ | `lightTone` | `[10, 100]` | Light scheme tone window: `[lo, hi]`, `{ lo, hi, eps }`, or `false` to disable clamping. Bypassed in HC. |
1428
+ | `darkTone` | `[15, 95]` | Dark scheme tone window: `[lo, hi]`, `{ lo, hi, eps }`, or `false` to disable clamping. Bypassed in HC. |
1429
+ | `darkDesaturation` | `0.1` | Saturation reduction in dark scheme (0–1). |
1430
+ | `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. |
1431
+ | `states.highContrast` | `'@media(prefers-contrast: more)'` | State alias for HC tokens ([Tasty](https://tasty.style) export). |
1432
+ | `modes.dark` | `true` | Include dark variants in exports. |
1433
+ | `modes.highContrast` | `false` | Include HC variants. |
1434
+ | `shadowTuning` | `undefined` | Default tuning for all shadow colors. Per-color tuning merges field-by-field. |
1435
+ | `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). |
1436
+ | `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). |
1437
+ | `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). |
1438
+
1439
+ | Method | Description |
1440
+ | ------------------------- | ----------------------------------------------------------------------------------- |
1330
1441
  | `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). |
1442
+ | `glaze.getConfig()` | Snapshot the current resolved config (shallow copy). |
1443
+ | `glaze.resetConfig()` | Reset to defaults (also bumps the version counter). |
1333
1444
 
1334
1445
  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`.
1335
1446
 
@@ -1361,32 +1472,38 @@ Resolution priority (highest first):
1361
1472
 
1362
1473
  ## Validation
1363
1474
 
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 |
1475
+ Invalid definitions throw before resolution when Glaze cannot produce a
1476
+ well-defined dependency graph. Recoverable numeric bounds are clamped. A
1477
+ physically unreachable contrast floor or a potentially misleading
1478
+ contrast/opacity combination emits `console.warn` and returns the closest
1479
+ available result.
1480
+
1481
+ | Condition | Behavior |
1482
+ | --------------------------------------------------- | -------------------------------------------------------------------------------------------------------- |
1483
+ | `contrast` without `base` in a **theme** color | Validation error |
1484
+ | Relative `tone` without `base` in a **theme** color | Validation error |
1485
+ | `contrast` without `base` in `glaze.color()` | Anchors against the literal seed (no error) |
1486
+ | Relative `tone` without `base` in `glaze.color()` | Anchors against the literal seed (no error) |
1487
+ | Relative `tone` overshoots `[0, 100]` | Mirror to the other side of the base (`autoFlip` on, default), or clamp to the boundary (`autoFlip` off) |
1488
+ | `tone` resolves outside 0–100 | Clamp silently |
1489
+ | `'max'` / `'min'` without `base` | Allowed resolves to the scheme's tone extreme (root color) |
1490
+ | `saturation` outside 0–1 | Clamp silently |
1491
+ | Circular `base` references | Validation error |
1492
+ | `base` references non-existent name | Validation error |
1493
+ | Shadow `bg` references non-existent color | Validation error |
1494
+ | Shadow `fg` references non-existent color | Validation error |
1495
+ | Shadow `bg` references another shadow color | Validation error |
1496
+ | Shadow `fg` references another shadow color | Validation error |
1497
+ | Regular color `base` references a shadow color | Validation error |
1498
+ | Shadow `intensity` outside 0–100 | Clamp silently |
1499
+ | `contrast` + `opacity` combined | `console.warn` |
1500
+ | Mix `base` references non-existent color | Validation error |
1501
+ | Mix `target` references non-existent color | Validation error |
1502
+ | Mix `base` references a shadow color | Validation error |
1503
+ | Mix `target` references a shadow color | Validation error |
1504
+ | Mix `value` outside 0–100 | Clamp silently |
1505
+ | Circular references involving mix colors | Validation error |
1506
+ | Contrast target physically unreachable | `console.warn` (deduped per `(name, scheme, target)`); closest passing variant returned |
1390
1507
 
1391
1508
  ---
1392
1509
 
@@ -1412,19 +1529,19 @@ import {
1412
1529
  } from '@tenphi/glaze';
1413
1530
  ```
1414
1531
 
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. |
1532
+ | Function | Description |
1533
+ | ------------------------------------- | ------------------------------------------------------------------------ |
1534
+ | `okhslToLinearSrgb(h, s, l)` | OKHSL (h: 0–360, s/l: 0–1) → linear sRGB tuple. |
1535
+ | `okhslToSrgb(h, s, l)` | OKHSL → gamma-encoded sRGB tuple (0–1 per channel). |
1536
+ | `okhslToOklab([h, s, l])` | OKHSL → OKLab `[L, a, b]`. |
1537
+ | `oklabToOkhsl([L, a, b])` | OKLab → OKHSL. |
1538
+ | `srgbToOkhsl([r, g, b])` | Gamma sRGB (0–1) → OKHSL. |
1539
+ | `hslToSrgb(h, s, l)` | CSS HSL → sRGB tuple. |
1540
+ | `parseHex(hex)` | Parse `#rgb` / `#rrggbb` to sRGB tuple. Returns `null` on invalid input. |
1541
+ | `parseHexAlpha(hex)` | Parse `#rgb` / `#rrggbb` / `#rrggbbaa`; returns `[r, g, b, a?]`. |
1542
+ | `relativeLuminanceFromLinearRgb(rgb)` | WCAG relative luminance from linear sRGB. |
1543
+ | `contrastRatioFromLuminance(yA, yB)` | WCAG contrast ratio from two luminances. |
1544
+ | `gamutClampedLuminance(linearRgb)` | Relative luminance with channel clamping for out-of-gamut colors. |
1428
1545
 
1429
1546
  ### Format writers
1430
1547
 
@@ -1432,8 +1549,8 @@ import {
1432
1549
  import { formatOkhsl, formatRgb, formatHsl, formatOklch } from '@tenphi/glaze';
1433
1550
 
1434
1551
  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%)'
1552
+ formatRgb(280, 60, 95); // 'rgb(244 240 250)'
1553
+ formatHsl(280, 60, 95); // 'hsl(280 60% 95%)'
1437
1554
  formatOklch(280, 60, 95); // 'oklch(0.95 ... 280)'
1438
1555
  ```
1439
1556
 
@@ -1454,17 +1571,19 @@ import {
1454
1571
  } from '@tenphi/glaze';
1455
1572
  ```
1456
1573
 
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. |
1574
+ | Function | Description |
1575
+ | ------------------------------------------- | ------------------------------------------------------------------------- |
1576
+ | `toTone(l, eps?)` | OKHSL lightness (0–1) → tone (0–100). Defaults to `REF_EPS`. |
1577
+ | `fromTone(t, eps?)` | Tone (0–100) → OKHSL lightness (0–1). Inverse of `toTone`. |
1578
+ | `toneFromY(y, eps?)` / `yFromTone(t, eps?)` | Same transfer in luminance space (0–1). |
1579
+ | `okhstToOkhsl({ h, s, t })` | OKHST → OKHSL (`{ h, s, l }`). |
1580
+ | `okhslToOkhst({ h, s, l })` | OKHSL → OKHST (`{ h, s, t }`). |
1581
+ | `variantToOkhsl(variant)` | `ResolvedColorVariant` (stores `t`) → `{ h, s, l, alpha }` for rendering. |
1582
+ | `REF_EPS` | Reference epsilon (`0.05`) for the canonical tone axis. |
1466
1583
 
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.
1584
+ `ResolvedColorVariant` stores `{ h, s, t, alpha }` (tone, not lightness). Use
1585
+ `variantToOkhsl(variant).l` to recover OKHSL lightness. See
1586
+ [OKHST in Glaze](okhst.md) for the model.
1468
1587
 
1469
1588
  ### Contrast solver
1470
1589
 
@@ -1478,29 +1597,29 @@ import {
1478
1597
  } from '@tenphi/glaze';
1479
1598
  ```
1480
1599
 
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. |
1600
+ | Function | Description |
1601
+ | ----------------------------------------------------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
1602
+ | `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? }`. |
1603
+ | `findValueForMixContrast(opts)` | Same, but searches for a mix `value` (0–1) that meets a contrast floor between a base and a target. |
1485
1604
  | `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. |
1605
+ | `resolveMinContrast(value)` | Resolves a `MinContrast` (WCAG preset or number) to a numeric ratio. |
1606
+ | `apcaContrast(yText, yBg)` | APCA Lc magnitude (0–106) for two relative luminances. |
1488
1607
 
1489
1608
  Exported constants: `APCA_PRESETS`, `APCA_HC_ENHANCEMENT` (`15`, the Enhanced Level delta), `APCA_MAX_LC` (`106`).
1490
1609
 
1491
1610
  `findToneForContrast` options:
1492
1611
 
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. |
1612
+ | Option | Default | Description |
1613
+ | ------------------ | -------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ |
1614
+ | `hue` | — | Candidate hue (0–360). |
1615
+ | `saturation` | — | Candidate saturation (0–1). |
1616
+ | `preferredTone` | — | Preferred candidate tone (0–1). Kept if it already meets the target. |
1617
+ | `baseLinearRgb` | — | Base color as linear sRGB tuple. |
1618
+ | `contrast` | — | `ResolvedContrast` (`{ metric, target }`). |
1619
+ | `toneRange` | `[0, 1]` | Search bounds in tone. |
1620
+ | `epsilon` | `1e-4` | Convergence threshold. |
1621
+ | `maxIterations` | `18` | Max binary-search iterations per branch. |
1622
+ | `initialDirection` | higher-contrast side | Direction to search first (`'lighter'` or `'darker'`). |
1623
+ | `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
1624
 
1506
1625
  Result: `{ tone, contrast, met, branch: 'lighter' | 'darker' | 'preferred', flipped? }`. `flipped: true` indicates the initial direction failed and the opposite direction satisfied the target.