@tenphi/cookbook 0.32.0 → 0.33.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.
Files changed (28) hide show
  1. package/docs/customization-rules.md +7 -5
  2. package/docs/migration.md +52 -26
  3. package/docs/theme-and-components.md +32 -15
  4. package/docs/upstream/glaze/README.md +46 -26
  5. package/docs/upstream/glaze/docs/contrast.mdx +751 -0
  6. package/docs/upstream/glaze/docs/exporting.mdx +634 -0
  7. package/docs/upstream/glaze/docs/getting-started.mdx +489 -0
  8. package/docs/upstream/glaze/docs/hdr.mdx +523 -0
  9. package/docs/upstream/glaze/docs/methodology.mdx +922 -0
  10. package/docs/upstream/glaze/docs/migration.md +477 -286
  11. package/docs/upstream/glaze/docs/mixes.mdx +348 -0
  12. package/docs/upstream/glaze/docs/okhst.mdx +679 -0
  13. package/docs/upstream/glaze/docs/reference/color-definitions.md +552 -0
  14. package/docs/upstream/glaze/docs/reference/color-tokens.md +420 -0
  15. package/docs/upstream/glaze/docs/reference/configuration.md +527 -0
  16. package/docs/upstream/glaze/docs/reference/contrast.md +277 -0
  17. package/docs/upstream/glaze/docs/reference/index.md +225 -0
  18. package/docs/upstream/glaze/docs/reference/output.md +713 -0
  19. package/docs/upstream/glaze/docs/reference/palette.md +434 -0
  20. package/docs/upstream/glaze/docs/reference/theme.md +331 -0
  21. package/docs/upstream/glaze/docs/reference/utilities.md +646 -0
  22. package/docs/upstream/glaze/docs/schemes.mdx +635 -0
  23. package/docs/upstream/glaze/docs/shadows-and-glows.mdx +519 -0
  24. package/docs/upstream/manifest.json +2 -2
  25. package/package.json +3 -3
  26. package/docs/upstream/glaze/docs/api.md +0 -2021
  27. package/docs/upstream/glaze/docs/methodology.md +0 -686
  28. package/docs/upstream/glaze/docs/okhst.md +0 -207
@@ -0,0 +1,713 @@
1
+ ---
2
+ title: Output formats
3
+ description: Every output method (tokens, tasty, json, css, dtcg, dtcgResolver, tailwind), its options, color formats, output modes, state aliases, splitHue, and HDR output.
4
+ ---
5
+ Output methods turn a resolved theme or palette into color strings or token documents. They exist on [themes](./theme.md) and [palettes](./palette.md) with the same options, except that palette `tasty()` and `css()` have no `name` (each theme's hue var is named after the theme); palettes add [prefix and primary options](./palette.md#glazepaletteexportoptions). Standalone [color tokens](./color-tokens.md) have single-color versions of the same methods. For choosing an export and wiring it into an app, see [Exporting and integration](../exporting.mdx).
6
+
7
+ Examples on this page use this theme:
8
+
9
+ ```ts
10
+ import { glaze } from '@tenphi/glaze';
11
+
12
+ const theme = glaze(280, 80);
13
+ theme.colors({
14
+ surface: { tone: 97 },
15
+ 'surface-text': { base: 'surface', tone: '-52', contrast: 'AA' },
16
+ accent: { tone: 52, mode: 'fixed' },
17
+ });
18
+ ```
19
+
20
+ ## Output methods at a glance
21
+
22
+ | Method | Returns | Options type | Honors `modes` |
23
+ | ---------------------------------------- | -------------------------------------------- | -------------------------- | -------------------------------------- |
24
+ | [`tokens()`](#tokensoptions) | `{ light: { name: value }, dark: {…}, … }` | `GlazeJsonOptions` | Yes |
25
+ | [`tasty()`](#tastyoptions) | `{ '#name': { '': value, [state]: value } }` | `GlazeTastyOptions` | Yes |
26
+ | [`json()`](#jsonoptions) | `{ name: { light: value, dark: value, … } }` | `GlazeJsonOptions` | Yes |
27
+ | [`css()`](#cssoptions) | `GlazeCssResult` (four strings) | `GlazeCssOptions` | No: all four, and its own `hdr` option |
28
+ | [`dtcg()`](#dtcgoptions) | `GlazeDtcgResult` (one document per variant) | `GlazeDtcgOptions` | Yes |
29
+ | [`dtcgResolver()`](#dtcgresolveroptions) | `GlazeDtcgResolverDocument` | `GlazeDtcgResolverOptions` | Yes |
30
+ | [`tailwind()`](#tailwindoptions) | `string` (CSS) | `GlazeTailwindOptions` | Yes |
31
+
32
+ With `modes.hdr`, every method also emits the HDR values of glows past SDR white, next to the SDR ones. See [HDR output](#hdr-output).
33
+
34
+ [`format`](#color-formats) applies to every method except `dtcg()` and `dtcgResolver()`: `'oklch'`, `'rgb'`, or `'hsl'`, and on `tasty()` also the Tasty-only `'okhsl'` and `'okhst'`. The two DTCG methods emit structured values and take `colorSpace` instead.
35
+
36
+ Every method resolves the theme first (results are cached; see [`theme.resolve()`](./theme.md#themeresolve)). Tokens are emitted in dependency order.
37
+
38
+ ## Scheme variants
39
+
40
+ Glaze always resolves four variants per color. `ResolvedColor` and the results of `tokens()`, `json()`, `css()`, and `dtcg()` key them by the names below. `tasty()` keys them by [state alias](#state-aliases), `tailwind()` by selector, and `dtcgResolver()` uses the names as its default context names, which `contextNames` can rename.
41
+
42
+ | Variant | Scheme | Emitted when |
43
+ | --------------- | ---------------------- | ------------------------------------- |
44
+ | `light` | Light, normal contrast | Always. |
45
+ | `dark` | Dark, normal contrast | `modes.dark` |
46
+ | `lightContrast` | Light, high contrast | `modes.highContrast` |
47
+ | `darkContrast` | Dark, high contrast | `modes.dark` and `modes.highContrast` |
48
+
49
+ How the variants are computed is in [Light, dark, and high contrast](../schemes.mdx).
50
+
51
+ ## Output modes
52
+
53
+ `GlazeOutputModes` controls which variants `tokens()`, `tasty()`, `json()`, `dtcg()`, `dtcgResolver()`, and `tailwind()` emit, and whether they add HDR values. `css()` has no `modes` option: it always returns all four strings, and its own [`hdr`](#cssoptions) option falls back to the global `modes.hdr`.
54
+
55
+ | Field | Type | Default | Description |
56
+ | -------------- | --------- | ------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
57
+ | `dark` | `boolean` | `true` | Include the `dark` variant (and `darkContrast` when `highContrast` is on). |
58
+ | `highContrast` | `boolean` | `false` | Include `lightContrast` (and `darkContrast` when `dark` is on). |
59
+ | `hdr` | `boolean` | `false` | Add the HDR values of glows past SDR white, for the variants the other two fields include. Off, every output is byte-identical to SDR, even with a `headroom` set. See [HDR output](#hdr-output). |
60
+
61
+ ```ts
62
+ // Light only
63
+ theme.tokens({ modes: { dark: false } });
64
+
65
+ // Light + dark (the default)
66
+ theme.tokens({ modes: { highContrast: false } });
67
+
68
+ // All four variants
69
+ theme.tokens({ modes: { dark: true, highContrast: true } });
70
+ // → { light, dark, lightContrast, darkContrast }
71
+
72
+ // Light + light high contrast
73
+ theme.tokens({ modes: { dark: false, highContrast: true } });
74
+ // → { light, lightContrast }
75
+ ```
76
+
77
+ Each field resolves separately, highest priority first:
78
+
79
+ 1. A **global** [`contrastLevel`](./contrast.md#manual-contrast-level) of `100` forces `highContrast: false`, even against an explicit per-call `highContrast: true`. At that level the normal variants already *are* the high-contrast ones, so the tier would duplicate them. Its HDR maps and blocks go with it. Any other level leaves `highContrast` alone.
80
+ 2. The per-call `modes` option.
81
+ 3. `glaze.configure({ modes })` — the global config ([Configuration](./configuration.md)).
82
+ 4. The built-in default `{ dark: true, highContrast: false, hdr: false }`.
83
+
84
+ `modes` is global or per call only — it is not part of a theme's config override. A `contrastLevel` set on one theme or token (rather than globally) never changes which modes are emitted: its high-contrast variants are still emitted when requested, with values equal to its normal ones at level `100`. With a global level of `100`, `css()` still returns all four strings, and `lightContrast` / `darkContrast` equal `light` / `dark`.
85
+
86
+ ## Color formats
87
+
88
+ The `format` option picks the color string syntax for `tokens()`, `tasty()`, `json()`, `css()`, and `tailwind()` on themes and palettes, and for the [color token](./color-tokens.md) methods `token()`, `tasty()`, `json()`, `css()`, and `tailwind()`. The default is `'oklch'`.
89
+
90
+ | `format` | Output (alpha = 1) | Output (alpha < 1) | Notes |
91
+ | ------------------- | ------------------ | -------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------- |
92
+ | `'oklch'` (default) | `oklch(L C H)` | `oklch(L C H / A)` | Native CSS. `L` and `C` to 4 decimals, `H` to 2. An achromatic color is `oklch(L 0 0)`. Required for [`splitHue`](#hue-channel-splitting-splithue). |
93
+ | `'rgb'` | `rgb(R G B)` | `rgb(R G B / A)` | Channels 0–255 with up to 2 decimals — not rounded to integers, so 8-bit quantization can't shift a solved contrast. |
94
+ | `'hsl'` | `hsl(H S% L%)` | `hsl(H S% L% / A)` | Up to 2 decimals. |
95
+ | `'okhsl'` | `okhsl(H S% L%)` | `okhsl(H S% L% / A)` | Glaze's native space; not a CSS function. **[Tasty](../../../tasty/README.md) only** (see the guard below). |
96
+ | `'okhst'` | `okhst(H S% T%)` | `okhst(H S% T% / A)` | OKHSL with the [tone](../okhst.mdx) axis. **Tasty only.** |
97
+
98
+ All values use the modern space-separated syntax and strip trailing zeros (`95`, not `95.0`). Alpha is printed to at most 4 decimals. A glow's HDR value ignores `format`: it is always extended `color(srgb R G B)` (see [HDR strings](#hdr-strings)).
99
+
100
+ ```ts
101
+ theme.tokens().light.surface;
102
+ // → 'oklch(0.97 0.0128 280)'
103
+ theme.tokens({ format: 'rgb' }).light.surface;
104
+ // → 'rgb(243.08 244.36 253.89)'
105
+ theme.tokens({ format: 'hsl' }).light.surface;
106
+ // → 'hsl(232.87 82.96% 97.45%)'
107
+ theme.tasty({ format: 'okhsl' })['#surface'][''];
108
+ // → 'okhsl(280 80% 96.51%)'
109
+ theme.tasty({ format: 'okhst' })['#surface'][''];
110
+ // → 'okhst(280 80% 97.15%)'
111
+ ```
112
+
113
+ **Native-format guard.** `okhsl` and `okhst` are not CSS color spaces. Only the methods that return Tasty state maps accept them — `tasty()` on themes and palettes, and `token()` / `tasty()` on color tokens. Every other method throws:
114
+
115
+ ```text
116
+ glaze: okhst output is only supported by tasty() (not a native CSS
117
+ color space).
118
+ Use tasty({ format: 'okhst' }) or pick a native format (oklch|hsl|rgb)
119
+ for css().
120
+ ```
121
+
122
+ The guard is exported as [`assertNativeFormat`](./utilities.md#export-guards) for custom pipelines.
123
+
124
+ **Pastel colors.** For a [`pastel`](./color-definitions.md#per-color-pastel) color, `okhsl` and `okhst` output re-express the saturation as its standard (non-pastel) OKHSL equivalent, so parsers that don't know about `pastel` render the same color. The other formats are computed from the gamut-mapped color directly.
125
+
126
+ DTCG output doesn't use `format`; it emits structured `$value` objects. Pick their representation with [`colorSpace`](#dtcgoptions).
127
+
128
+ To format a single resolved variant yourself, use [`glaze.format()`](./utilities.md#glazeformatvariant-format-pastel).
129
+
130
+ ## `tokens(options?)`
131
+
132
+ A flat token map grouped by scheme variant. Keys are color names (prefixed in palettes).
133
+
134
+ ```ts
135
+ theme.tokens();
136
+ // → {
137
+ // light: {
138
+ // surface: 'oklch(0.97 0.0128 280)',
139
+ // 'surface-text': 'oklch(0.5286 0.1912 280)',
140
+ // accent: 'oklch(0.5961 0.181 280)',
141
+ // },
142
+ // dark: {
143
+ // surface: 'oklch(0.279 0.1015 280)',
144
+ // 'surface-text': 'oklch(0.6531 0.144 280)',
145
+ // accent: 'oklch(0.5955 0.1601 280)',
146
+ // },
147
+ // }
148
+ ```
149
+
150
+ `GlazeJsonOptions`:
151
+
152
+ | Option | Type | Default | Description |
153
+ | -------- | ------------------ | ----------------------------------------------------------------- | ------------------------------------------------------------- |
154
+ | `format` | `GlazeColorFormat` | `'oklch'` | `'oklch'`, `'rgb'`, or `'hsl'`. `'okhsl'` / `'okhst'` throw. |
155
+ | `modes` | `GlazeOutputModes` | Global config (`{ dark: true, highContrast: false, hdr: false }`) | Which variants to include. See [Output modes](#output-modes). |
156
+
157
+ Variant keys that are not emitted are absent from the result. With `modes.hdr`, each emitted variant map is followed by a dense `<variant>Hdr` map (`light`, `lightHdr`, `dark`, `darkHdr`, …); see [HDR output](#hdr-in-each-exporter).
158
+
159
+ ## `tasty(options?)`
160
+
161
+ Style-to-state bindings for the [Tasty](../../../tasty/README.md) style system. Keys are `#name` color tokens; each value maps state aliases to colors, with `''` as the default (light) state. Spread the result into component styles or register it as a recipe.
162
+
163
+ ```ts
164
+ theme.tasty();
165
+ // → {
166
+ // '#surface': {
167
+ // '': 'oklch(0.97 0.0128 280)',
168
+ // '@media(prefers-color-scheme: dark)': 'oklch(0.279 0.1015 280)',
169
+ // },
170
+ // '#surface-text': {
171
+ // '': 'oklch(0.5286 0.1912 280)',
172
+ // '@media(prefers-color-scheme: dark)': 'oklch(0.6531 0.144 280)',
173
+ // },
174
+ // '#accent': { … },
175
+ // }
176
+ ```
177
+
178
+ `GlazeTastyOptions` — `GlazeTokenOptions` (the first five rows, also taken by [`token.tasty()`](./color-tokens.md#token-methods)) plus `splitHue` and `name`:
179
+
180
+ | Option | Type | Default | Description |
181
+ | --------------------- | ------------------ | ----------------------------------------------------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------- |
182
+ | `format` | `GlazeColorFormat` | `'oklch'` | Any of the five formats, including the Tasty-only `'okhsl'` and `'okhst'`. |
183
+ | `modes` | `GlazeOutputModes` | Global config | Which variants to include, and whether to add HDR keys. |
184
+ | `states.dark` | `string` | Global `states.dark` (`'@media(prefers-color-scheme: dark)'`) | State alias for the dark variant. |
185
+ | `states.highContrast` | `string` | Global `states.highContrast` (`'@media(prefers-contrast: more)'`) | State alias for the light high-contrast variant. |
186
+ | `states.hdr` | `string` | Global `states.hdr` (`'@media(dynamic-range: high)'`) | State alias for a glow's HDR value. Used only with `modes.hdr`. Experimental; see [HDR output](#hdr-output). |
187
+ | `splitHue` | `boolean` | `false` | Emit hue as separate `$name-hue` tokens referenced with `var()`. Throws unless `format` is `'oklch'`. See [`splitHue`](#hue-channel-splitting-splithue). |
188
+ | `name` | `string` | `'theme'` | Base name of the theme-level hue var (`$theme-hue` / `--theme-hue`). Used only with `splitHue`. `palette.tasty()` has no `name`: it uses each theme's name. |
189
+
190
+ ### State aliases
191
+
192
+ `tasty()` is the only output that uses state aliases. The defaults are media-query states, so the tokens follow the operating system preference without registering custom states:
193
+
194
+ | Variant | State key |
195
+ | ------------------------ | --------------------------------------------------------------------------------------------------------------------------------------------- |
196
+ | `light` | `''` |
197
+ | `dark` | `states.dark` — default `'@media(prefers-color-scheme: dark)'` |
198
+ | `lightContrast` | `states.highContrast` — default `'@media(prefers-contrast: more)'` |
199
+ | `darkContrast` | `` `${states.dark} & ${states.highContrast}` `` — default `'@media(prefers-color-scheme: dark) & @media(prefers-contrast: more)'` |
200
+ | HDR value of any variant | `states.hdr` after `''`, `` `${key} & ${states.hdr}` `` after any other key — default `'@media(dynamic-range: high)'`. Only with `modes.hdr`. |
201
+
202
+ With `modes.hdr`, a variant that has an HDR value adds its key right after its SDR key, so the order is `''`, hdr, dark, dark & hdr, hc, hc & hdr, dark & hc, dark & hc & hdr, with only the HDR keys that exist. Tasty gives later keys priority, so a dark key still beats the light HDR one. See [HDR output](#hdr-in-each-exporter).
203
+
204
+ Set them globally with `glaze.configure({ states })` or per call. Custom states such as `@dark` must be registered in your Tasty config:
205
+
206
+ ```ts
207
+ glaze.configure({
208
+ states: { dark: '@dark', highContrast: '@hc' },
209
+ modes: { highContrast: true },
210
+ });
211
+
212
+ theme.tasty()['#surface'];
213
+ // → {
214
+ // '': 'oklch(0.97 0.0128 280)',
215
+ // '@dark': 'oklch(0.279 0.1015 280)',
216
+ // '@hc': 'oklch(0.9685 0.0134 280)',
217
+ // '@dark & @hc': 'oklch(0.1685 0.0615 280)',
218
+ // }
219
+ ```
220
+
221
+ A per-call `states` object overrides only the fields it sets; `theme.tasty({ states: { dark: '@night' } })` keeps the global high-contrast alias.
222
+
223
+ ## `json(options?)`
224
+
225
+ A per-color map of variant values.
226
+
227
+ ```ts
228
+ theme.json();
229
+ // → {
230
+ // surface: {
231
+ // light: 'oklch(0.97 0.0128 280)',
232
+ // dark: 'oklch(0.279 0.1015 280)',
233
+ // },
234
+ // 'surface-text': {
235
+ // light: 'oklch(0.5286 0.1912 280)',
236
+ // dark: 'oklch(0.6531 0.144 280)',
237
+ // },
238
+ // accent: {
239
+ // light: 'oklch(0.5961 0.181 280)',
240
+ // dark: 'oklch(0.5955 0.1601 280)',
241
+ // },
242
+ // }
243
+ ```
244
+
245
+ Same options as [`tokens()`](#tokensoptions) (`GlazeJsonOptions`). With `modes.hdr`, a variant that has an HDR value adds a `<variant>Hdr` key right after its SDR key (`light`, `lightHdr`, `dark`, …); see [HDR output](#hdr-in-each-exporter). The result holds resolved strings; it is not an authoring snapshot — use [`theme.export()`](./theme.md#themeexportoverride) for that. [`palette.json()`](./palette.md#palettejson) groups by theme name.
246
+
247
+ ## `css(options?)`
248
+
249
+ CSS custom property declarations, one string per variant. Each string is newline-separated `--<name><suffix>: <value>;` lines, ready to drop into a rule.
250
+
251
+ ```ts
252
+ theme.css();
253
+ // → {
254
+ // light:
255
+ // '--surface-color: oklch(0.97 0.0128 280);\n' +
256
+ // '--surface-text-color: oklch(0.5286 0.1912 280);\n' +
257
+ // '--accent-color: oklch(0.5961 0.181 280);',
258
+ // dark:
259
+ // '--surface-color: oklch(0.279 0.1015 280);\n' +
260
+ // '--surface-text-color: oklch(0.6531 0.144 280);\n' +
261
+ // '--accent-color: oklch(0.5955 0.1601 280);',
262
+ // lightContrast: '--surface-color: oklch(0.9685 0.0134 280);\n…',
263
+ // darkContrast: '--surface-color: oklch(0.1685 0.0615 280);\n…',
264
+ // }
265
+ ```
266
+
267
+ `GlazeCssOptions`:
268
+
269
+ | Option | Type | Default | Description |
270
+ | ---------- | ------------------ | ---------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
271
+ | `format` | `GlazeColorFormat` | `'oklch'` | `'oklch'`, `'rgb'`, or `'hsl'`. `'okhsl'` / `'okhst'` throw. |
272
+ | `suffix` | `string` | `'-color'` | Appended to each property name. Pass `''` for bare names (`--surface`). With `splitHue`, a suffix that lands a value on a hue var (`'-hue'`) throws; see [Variable names](#variable-names). |
273
+ | `splitHue` | `boolean` | `false` | Emit hue as separate `--*-hue` properties referenced with `var()`. Throws unless `format` is `'oklch'`. See [`splitHue`](#hue-channel-splitting-splithue). |
274
+ | `name` | `string` | `'theme'` | Base name of the theme-level hue var (`--theme-hue`). Used only with `splitHue`. `palette.css()` has no `name`: it uses each theme's name. |
275
+ | `hdr` | `boolean` | Global `modes.hdr` (`false`) | Add `result.hdr`, the HDR tier. Experimental; see [HDR output](#hdr-output). |
276
+
277
+ `GlazeCssResult` always has all four keys — `light`, `dark`, `lightContrast`, `darkContrast` — whatever `modes` says. A key is `''` when the theme has no colors. When HDR output is on, it also has `hdr`: a `GlazeCssSchemeBlocks` with the same four keys. Which selectors or media queries to wrap each string in is up to you; [Exporting and integration](../exporting.mdx) shows common setups.
278
+
279
+ ## `dtcg(options?)`
280
+
281
+ W3C [Design Tokens Format Module (2025.10)](https://www.designtokens.org/) documents — the vendor-neutral JSON format read by Figma, Tokens Studio, Style Dictionary v4+, Terrazzo, Penpot, and other DTCG tools. Returns one spec-conformant token tree per scheme variant.
282
+
283
+ ```ts
284
+ theme.dtcg();
285
+ // → {
286
+ // light: {
287
+ // surface: {
288
+ // $type: 'color',
289
+ // $value: {
290
+ // colorSpace: 'srgb',
291
+ // components: [0.953256, 0.958291, 0.995646],
292
+ // hex: '#f3f4fe',
293
+ // },
294
+ // },
295
+ // …
296
+ // },
297
+ // dark: {
298
+ // surface: {
299
+ // $type: 'color',
300
+ // $value: {
301
+ // colorSpace: 'srgb',
302
+ // components: [0.130873, 0.118318, 0.346863],
303
+ // hex: '#211e58',
304
+ // },
305
+ // },
306
+ // …
307
+ // },
308
+ // }
309
+ ```
310
+
311
+ Write each document to its own `.tokens.json` file. One file per scheme is the most tool-compatible convention: one per Style Dictionary theme, Tokens Studio set, or Figma variable mode.
312
+
313
+ `GlazeDtcgOptions`:
314
+
315
+ | Option | Type | Default | Description |
316
+ | ------------ | ------------------ | ------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
317
+ | `colorSpace` | `DtcgColorSpace` | `'srgb'` | `'srgb'` emits gamma sRGB `components` (0–1) plus a `hex` hint — understood everywhere. `'oklch'` emits `[L, C, H]` components and no `hex` — Glaze-native and wide-gamut. |
318
+ | `modes` | `GlazeOutputModes` | Global config | Which variants to include. `light` is always present. |
319
+
320
+ `GlazeDtcgResult` has `light` (always) and `dark`, `lightContrast`, `darkContrast`, each `undefined` when its mode is off.
321
+
322
+ Token shapes:
323
+
324
+ | Type | Shape |
325
+ | --------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ |
326
+ | `DtcgColorToken` | `{ $type: 'color', $value: DtcgColorValue, $description?: string, $extensions?: GlazeDtcgExtensions }`. `$type` is always `'color'`; Glaze never writes `$description`. `$extensions` carries a glow's HDR value, only with `modes.hdr`; see [DTCG `$extensions`](#dtcg-extensions). |
327
+ | `DtcgSrgbColorValue` | `{ colorSpace: 'srgb', components: [r, g, b], hex: '#rrggbb', alpha?: number }`. Components rounded to 6 decimals; `hex` is 6-digit lowercase and never carries alpha. |
328
+ | `DtcgOklchColorValue` | `{ colorSpace: 'oklch', components: [L, C, H], alpha?: number }`. `L` and `C` rounded to 6 decimals, `H` to 4. |
329
+ | `DtcgColorValue` | `DtcgSrgbColorValue \| DtcgOklchColorValue`. |
330
+ | `DtcgDocument` | `Record<string, DtcgColorToken>` — one flat tree per scheme. |
331
+
332
+ `alpha` (rounded to 6 decimals) is present only when the color's opacity is below 1.
333
+
334
+ ```ts
335
+ theme.dtcg({ colorSpace: 'oklch' }).light.surface;
336
+ // → {
337
+ // $type: 'color',
338
+ // $value: {
339
+ // colorSpace: 'oklch',
340
+ // components: [0.970004, 0.012759, 280],
341
+ // },
342
+ // }
343
+ ```
344
+
345
+ ## `dtcgResolver(options?)`
346
+
347
+ One W3C [DTCG Resolver-Module](https://www.designtokens.org/) document that describes **every scheme variant in one file** — an alternative to `dtcg()`'s per-scheme files for tools that resolve sets and modifiers (for example Dispersa). The light document becomes `sets.base.sources[0]`, the default context; each other emitted variant becomes a context override on a single `scheme` modifier.
348
+
349
+ ```ts
350
+ theme.dtcgResolver({ modes: { highContrast: true } });
351
+ // → {
352
+ // version: '2025.10',
353
+ // sets: {
354
+ // base: {
355
+ // sources: [
356
+ // {
357
+ // surface: {
358
+ // $type: 'color',
359
+ // $value: {
360
+ // colorSpace: 'srgb',
361
+ // components: [0.953256, 0.958291, 0.995646],
362
+ // hex: '#f3f4fe',
363
+ // },
364
+ // },
365
+ // …
366
+ // },
367
+ // ],
368
+ // },
369
+ // },
370
+ // modifiers: {
371
+ // scheme: {
372
+ // default: 'light',
373
+ // contexts: {
374
+ // light: [],
375
+ // dark: [{ surface: { $type: 'color', $value: {…} }, … }],
376
+ // lightContrast: [{ surface: {…}, … }],
377
+ // darkContrast: [{ surface: {…}, … }],
378
+ // },
379
+ // },
380
+ // },
381
+ // resolutionOrder: [
382
+ // { $ref: '#/sets/base' },
383
+ // { $ref: '#/modifiers/scheme' },
384
+ // ],
385
+ // }
386
+ ```
387
+
388
+ **Why one modifier with four contexts.** Glaze resolves `darkContrast` independently — it is not `dark` plus `lightContrast` layered. The resolver model composes modifiers additively (the last in `resolutionOrder` wins on conflict), so two independent modifiers (`scheme` × `contrast`) would produce wrong values for the dark high-contrast combination. One `scheme` modifier with a context per variant keeps every resolved value exact. Choose `dtcgResolver()` for single-file theming fed to a resolver tool, and `dtcg()` for the widest per-file tool compatibility.
389
+
390
+ `GlazeDtcgResolverOptions` extends `GlazeDtcgOptions`:
391
+
392
+ | Option | Type | Default | Description |
393
+ | -------------- | ------------------------------- | ------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------ |
394
+ | `colorSpace` | `DtcgColorSpace` | `'srgb'` | Same as [`dtcg()`](#dtcgoptions); applies to every source and context. |
395
+ | `modes` | `GlazeOutputModes` | Global config | Which variants become contexts. `light` is always present (it is the default context, with an empty override list); variants that are off are omitted. |
396
+ | `setName` | `string` | `'base'` | Name of the single set holding the light token tree. |
397
+ | `modifierName` | `string` | `'scheme'` | Name of the modifier for the scheme axis. |
398
+ | `contextNames` | `GlazeDtcgResolverContextNames` | Variant names | Rename any of the four contexts, e.g. `{ dark: 'night' }`. Fields: `light`, `dark`, `lightContrast`, `darkContrast`. |
399
+ | `version` | `string` | `'2025.10'` | The document's `version` field. |
400
+
401
+ `setName` and `modifierName` also flow into `resolutionOrder` (`#/sets/<setName>`, `#/modifiers/<modifierName>`), and a renamed `light` context is also the modifier's `default`.
402
+
403
+ With `modes.hdr`, the sources and contexts are `dtcg()` documents, so a glow's HDR value rides in `$extensions` in each document that has one. A context replaces whole tokens: a dark token without `$extensions` drops the light HDR value.
404
+
405
+ Document types:
406
+
407
+ | Type | Shape |
408
+ | --------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------- |
409
+ | `GlazeDtcgResolverDocument` | `{ version: string, sets: Record<string, DtcgResolverSet>, modifiers: Record<string, DtcgResolverModifier>, resolutionOrder: DtcgResolverRef[] }` |
410
+ | `DtcgResolverSet` | `{ sources: DtcgTokenTree[] }` |
411
+ | `DtcgResolverModifier` | `{ default: string, contexts: Record<string, DtcgTokenTree[]> }` |
412
+ | `DtcgResolverRef` | `{ $ref: string }` — a JSON pointer. |
413
+ | `DtcgTokenTree` | `{ [key: string]: DtcgColorToken \| DtcgTokenTree }`. Glaze's flat `DtcgDocument` is a valid shallow tree. |
414
+
415
+ ## `tailwind(options?)`
416
+
417
+ A Tailwind CSS v4 `@theme` block (the light baseline) plus dark and high-contrast overrides under configurable selectors, returned as one ready-to-paste CSS string. The `--color-*` namespace makes Tailwind generate `bg-*`, `text-*`, `border-*`, and the other color utilities.
418
+
419
+ ```css
420
+ /* theme.tailwind({ modes: { highContrast: true } }) */
421
+ @theme {
422
+ --color-surface: oklch(0.97 0.0128 280);
423
+ --color-surface-text: oklch(0.5286 0.1912 280);
424
+ --color-accent: oklch(0.5961 0.181 280);
425
+ }
426
+
427
+ .dark {
428
+ --color-surface: oklch(0.279 0.1015 280);
429
+ --color-surface-text: oklch(0.6531 0.144 280);
430
+ --color-accent: oklch(0.5955 0.1601 280);
431
+ }
432
+
433
+ .high-contrast {
434
+ --color-surface: oklch(0.9685 0.0134 280);
435
+ --color-surface-text: oklch(0.4567 0.1819 280);
436
+ --color-accent: oklch(0.5784 0.1853 280);
437
+ }
438
+
439
+ .dark.high-contrast {
440
+ --color-surface: oklch(0.1685 0.0615 280);
441
+ --color-surface-text: oklch(0.7012 0.1258 280);
442
+ --color-accent: oklch(0.5784 0.1633 280);
443
+ }
444
+ ```
445
+
446
+ `GlazeTailwindOptions`:
447
+
448
+ | Option | Type | Default | Description |
449
+ | ---------------------- | ------------------ | -------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------ |
450
+ | `format` | `GlazeColorFormat` | `'oklch'` | `'oklch'`, `'rgb'`, or `'hsl'`. `'okhsl'` / `'okhst'` throw. |
451
+ | `namespace` | `string` | `'color-'` | Custom property namespace, forming `--<namespace><name>` (e.g. `--color-surface`). Named `namespace` so it doesn't clash with the palette's theme `prefix` option. |
452
+ | `darkSelector` | `string` | `'.dark'` | Selector wrapping the dark overrides. An at-rule such as `'@media (prefers-color-scheme: dark)'` drives dark mode from the OS preference. |
453
+ | `highContrastSelector` | `string` | `'.high-contrast'` | Selector wrapping the light high-contrast overrides. |
454
+ | `hdrSelector` | `string` | `'@media (dynamic-range: high)'` | Selector wrapping the HDR overrides, emitted only with `modes.hdr`. Experimental; see [HDR output](#hdr-output). |
455
+ | `modes` | `GlazeOutputModes` | Global config | Which override blocks to include. The `@theme` block is always emitted when the theme has colors. |
456
+
457
+ How the override blocks are built:
458
+
459
+ * Blocks appear in the order `@theme`, dark, high contrast, dark high contrast, separated by blank lines. With `modes.hdr`, the HDR blocks follow all of them, in the same order: `hdrSelector`, then combined with `darkSelector`, with `highContrastSelector`, and with both.
460
+ * The dark high-contrast block combines both selectors. Class-like selectors concatenate into one compound selector (`.dark.high-contrast`).
461
+ * An at-rule (any selector starting with `@`) wraps the block instead: Glaze nests the remaining selectors inside it, or `:root` when there are none.
462
+ * Empty blocks are skipped, and a theme with no colors returns `''`.
463
+
464
+ ```ts
465
+ theme.tailwind({
466
+ darkSelector: '@media (prefers-color-scheme: dark)',
467
+ modes: { highContrast: true },
468
+ });
469
+ ```
470
+
471
+ ```css
472
+ /* The override blocks only; the @theme block is unchanged. */
473
+ @media (prefers-color-scheme: dark) {
474
+ :root {
475
+ --color-surface: oklch(0.279 0.1015 280);
476
+ /* … */
477
+ }
478
+ }
479
+
480
+ .high-contrast {
481
+ --color-surface: oklch(0.9685 0.0134 280);
482
+ /* … */
483
+ }
484
+
485
+ @media (prefers-color-scheme: dark) {
486
+ .high-contrast {
487
+ --color-surface: oklch(0.1685 0.0615 280);
488
+ /* … */
489
+ }
490
+ }
491
+ ```
492
+
493
+ ## Hue channel splitting (`splitHue`)
494
+
495
+ With `splitHue: true` and `format: 'oklch'`, Glaze emits hue as its own custom property and references it with `var()` in the color values, so you can re-skin colors at runtime by changing one variable.
496
+
497
+ Supported on `theme.css()`, `theme.tasty()`, `palette.css()`, `palette.tasty()`, and the standalone [`token.css()`](./color-tokens.md). Not supported on `token.token()` / `token.tasty()`, whose return shape can't carry the hue declaration, nor on `tokens()`, `json()`, `dtcg()`, `dtcgResolver()`, or `tailwind()`; their option types have no `splitHue`.
498
+
499
+ ```ts
500
+ const theme = glaze(
501
+ { hue: 240, saturation: 18, darkHue: 200 },
502
+ { pastel: true },
503
+ );
504
+ theme.colors({
505
+ surface: { tone: 97 },
506
+ accent: { tone: 52, hue: '+20' },
507
+ });
508
+
509
+ theme.css({ splitHue: true, name: 'brand' });
510
+ ```
511
+
512
+ ```css
513
+ /* light */
514
+ --brand-hue: 240;
515
+ --accent-hue: calc(var(--brand-hue) + 20);
516
+ --surface-color: oklch(0.97 0.0025 var(--brand-hue));
517
+ --accent-color: oklch(0.5961 0.0182 var(--accent-hue));
518
+
519
+ /* dark */
520
+ --brand-hue: 200;
521
+ --accent-hue: calc(var(--brand-hue) + 20);
522
+ --surface-color: oklch(0.279 0.0077 var(--brand-hue));
523
+ --accent-color: oklch(0.5699 0.0157 var(--accent-hue));
524
+ ```
525
+
526
+ ### Requirements
527
+
528
+ * **`format: 'oklch'`.** CSS `var()` can stand in for the hue slot of `oklch()` only, so any other format throws before anything is emitted:
529
+
530
+ ```text
531
+ glaze: splitHue requires format 'oklch' (got 'rgb').
532
+ Use format: 'oklch' or drop splitHue.
533
+ ```
534
+ * **Every exported color is pastel.** Set `pastel: true` on the theme or token override, or per color. Pastel mode bounds chroma by the hue-independent safe chroma at each lightness, so the emitted `C` stays inside sRGB for any rotated hue. A non-pastel color in any emitted variant throws rather than emit values that would clip under rotation:
535
+
536
+ ```text
537
+ glaze: splitHue requires every color to be pastel (hue rotation is
538
+ only clip-free when chroma is bounded by the hue-independent safe
539
+ chroma).
540
+ Non-pastel: surface, accent.
541
+ Set pastel: true (per-theme, per-token, or per-color) or drop splitHue.
542
+ ```
543
+
544
+ The check is exported as [`assertAllPastel`](./utilities.md#export-guards).
545
+
546
+ ### Variable names
547
+
548
+ | Variable | CSS name | Tasty token |
549
+ | -------------------------- | --------------------------------------------- | ---------------------- |
550
+ | Theme hue | `--<name>-hue` (`name` defaults to `'theme'`) | `$<name>-hue` |
551
+ | Per-color hue | `--<prefix><color>-hue` | `$<prefix><color>-hue` |
552
+ | Color value (`css()` only) | `--<prefix><color><suffix>` | `#<prefix><color>` |
553
+
554
+ In `tasty()` the hue declarations become `$…-hue` keys next to the `#color` keys, with the same state map shape. In palettes `<name>` is the theme name and `<prefix>` the theme's prefix; see [`splitHue` in palettes](./palette.md#splithue-in-palettes).
555
+
556
+ Theme hue vars, per-color hue vars, and `css()` value properties share one namespace. A color named the same as `name` (a `brand` color with `name: 'brand'`) that sets its own hue would declare `--brand-hue` a second time, and one declaration would silently override the other, so the export throws instead:
557
+
558
+ ```text
559
+ glaze: splitHue would declare "--brand-hue" twice: for the theme hue
560
+ (name: 'brand') and for color "brand"'s own hue.
561
+ One would silently override the other.
562
+ Rename the color or pass a different `name`.
563
+ ```
564
+
565
+ A same-named color without its own hue, or one achromatic in light, is fine: it declares no per-color var.
566
+
567
+ A `css()` value property can land on a hue var too: `suffix: '-hue'` puts `--accent-hue` on a color's own hue var, and `suffix: ''` does the same for a color named `accent-hue`. That throws rather than emit a `var()` cycle:
568
+
569
+ ```text
570
+ glaze: splitHue would declare "--accent-hue" twice: for color "accent"'s
571
+ own hue and for color "accent"'s value.
572
+ One would silently override the other.
573
+ Rename the color, or pass a different `suffix` or `name`.
574
+ ```
575
+
576
+ `tasty()` is not affected: a `#accent` token compiles to `--accent-color`. Palettes run the same checks across themes; see [`splitHue` in palettes](./palette.md#splithue-in-palettes).
577
+
578
+ ### How each color references hue
579
+
580
+ | Color | Hue reference |
581
+ | ------------------------------------------------------------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
582
+ | Regular color with no `hue`, `darkHue`, or `from` | `var(--<name>-hue)` directly; no per-color var. |
583
+ | Relative `hue: '+N'` / `'-N'` | Per-color var `calc(var(--<name>-hue) + N)` (or `- N`). |
584
+ | Absolute `hue: N` | Per-color var set to `N`, normalized to 0–360. |
585
+ | Only `darkHue` set | Per-color var: `var(--<name>-hue)` in light, the dark value in dark. A color that authors a hue in either scheme gets its own var in both, so the shared `var()` reference stays valid. |
586
+ | `from` literal color | Per-color var pinned to the color's resolved hue, so re-skinning the theme doesn't re-tint a literal brand color. |
587
+ | `mode: 'static'` while the theme has a dark seed hue different from `hue` | Per-color var pinned to the resolved hue, so the color doesn't drift with the dark seed. |
588
+ | `mode: 'static'` with a `darkHue` | Its light reference in both schemes: a static color has one hue, so its `darkHue` is ignored, as the resolver ignores it. |
589
+ | Shadow, glow, and mix colors | Inline full `oklch()` value; they don't follow hue rotation. A glow's HDR value is a literal `color(srgb …)`. |
590
+ | Achromatic in light (saturation 0) | Inline in every scheme: `oklch(L 0 0)`, or `oklch()` at its own hue in a scheme where it has chroma (a `darkSaturation`). No per-color var in any scheme. |
591
+
592
+ The light variant decides whether a color is inline: every scheme renders with the color's light plan.
593
+
594
+ Every number in a hue declaration — the seed, an authored hue or offset, a resolved hue — is rounded to 2 decimals, the precision of `H` in `oklch()` output: a `from: '#2f5bff'` color gets `--logo-hue: 266.16`.
595
+
596
+ ### Dark hues
597
+
598
+ Hue vars are scheme-independent unless the theme or a color authors a dark hue (`darkHue` on the [seed](./theme.md#glazethemeseed) or on a [color definition](./color-definitions.md#dark-seed-darkhue--darksaturation)). When the dark set differs from the light one, Glaze re-declares the **whole** hue set for dark: in `css()` at the top of the `dark` and `darkContrast` strings, in `tasty()` under the dark state (only when `modes.dark` is on). The dark set holds only the vars the light plans reference, so a color achromatic in light declares none. The `lightContrast` string never carries hue declarations; it inherits the light ones. The `css()` HDR blocks carry none either: they only re-declare color values.
599
+
600
+ `--accent-hue` in the example repeats verbatim on purpose. A custom property substitutes `var()` at computed-value time on the element that declares it, so a per-color var has to be re-declared next to the new `--brand-hue` to pick it up.
601
+
602
+ ### Planning types
603
+
604
+ `HuePlan`, `HueDeclaration`, and `ChannelCtx` are exported for custom pipelines that reproduce this splitting:
605
+
606
+ | Type | Fields |
607
+ | ---------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
608
+ | `HueDeclaration` | `prop` — the custom property name (`--accent-hue`); `value` — its value (`240`, `calc(var(--theme-hue) + 20)`). |
609
+ | `HuePlan` | `hueVar` — the `var()` reference spliced into `oklch(L C <hueVar>)`; `inline` — `true` when the color is emitted as a full inline value; `declarations` — the hue declarations backing it. |
610
+ | `ChannelCtx` | `seedHue`, optional `darkSeedHue`, `baseName` (theme var name without `--` / `-hue`), `prefix`, `defs` (the `ColorMap`), `mode` (`'theme'` or `'standalone'`), optional `resolvedHue` (standalone tokens), optional `emitDeclarations` (default `true`; `false` for a pass that only references vars another pass declares, like a palette's primary aliases). |
611
+
612
+ ## Translucent colors
613
+
614
+ Shadow and glow colors, transparent mixes, and colors with `opacity` resolve with `alpha` below 1. String formats append it as `/ A`; DTCG adds an `alpha` field:
615
+
616
+ ```ts
617
+ theme.colors({
618
+ 'accent-pressed': { type: 'shadow', bg: 'accent', intensity: 10 },
619
+ scrim: { tone: 0, saturation: 0, mode: 'static', opacity: 0.5 },
620
+ });
621
+
622
+ theme.tokens({ format: 'rgb' }).light;
623
+ // → {
624
+ // …,
625
+ // 'accent-pressed': 'rgb(0 0 0 / 0.1697)',
626
+ // scrim: 'rgb(0 0 0 / 0.5)',
627
+ // }
628
+
629
+ theme.dtcg().light['accent-pressed'].$value;
630
+ // → {
631
+ // colorSpace: 'srgb',
632
+ // components: [0, 0, 0],
633
+ // hex: '#000000',
634
+ // alpha: 0.16972,
635
+ // }
636
+ ```
637
+
638
+ A shadow's or glow's alpha is solved for its `bg` surface, and a transparent mix's alpha is its `value` (or the value solved for `contrast`) over its `base`; composite each over that color. An `opacity` color carries its authored alpha unchanged in every scheme. See [Shadows and glows](../shadows-and-glows.mdx) and [Mixes and transparency](../mixes.mdx).
639
+
640
+ ## HDR output
641
+
642
+ With `glowTuning.headroom` above 0 (stops above white), a glow whose target tone passes 100 while its SDR value is pinned at its brightest gets an HDR value next to its SDR one: `variant.hdr`, an opaque base plus an exposure in stops ([`ResolvedHdr`](./color-definitions.md#hdr-values-resolvedhdr)). When that happens and where it shows is covered in [HDR glows](../hdr.mdx); this section covers how the exporters write it.
643
+
644
+ HDR output is opt-in. With `modes.hdr` off (the default), every exporter's output is byte-identical to SDR, even with a `headroom` set. Turn it on for the export that needs it rather than globally:
645
+
646
+ ```ts
647
+ theme.dtcg({ modes: { hdr: true } });
648
+ // css() has no modes: its hdr option falls back to the global modes.hdr
649
+ theme.css({ hdr: true });
650
+ // For every export:
651
+ glaze.configure({ modes: { hdr: true } });
652
+ ```
653
+
654
+ The data exports (`dtcg()`, `dtcgResolver()`, `json()`, `tokens()`) are what native apps and WebGPU code read. The CSS exports (`css()`'s `hdr` blocks, `tasty()`'s `states.hdr` keys, and `tailwind()`'s `hdrSelector` blocks) are experimental: no browser paints a CSS color past SDR white yet, and their shape may change, for example to CSS `hdr-color()`, once one does. See [Browsers](../hdr.mdx#browsers) for what browsers paint today, and [Fills, not overlays](../hdr.mdx#fills-not-overlays) for where that matters.
655
+
656
+ ### HDR strings
657
+
658
+ Every string exporter writes an HDR value as extended sRGB, `color(srgb R G B)`: the base's linear sRGB times `2^exposure`, encoded with the sRGB transfer curve continued past 1, so `1` is SDR white and `1.3533` one stop above it. Channels have 4 decimals and are unclamped, with the brightest above 1. This holds whatever `format` is, and the value stays literal under `splitHue`. `oklch()` would clamp its lightness when parsed, and `rgb()` / `hsl()` stop at white.
659
+
660
+ ```ts
661
+ const v = glaze.glow({
662
+ bg: '#ffffff',
663
+ intensity: 12,
664
+ tuning: { headroom: 1 },
665
+ });
666
+
667
+ glaze.formatHdr(v); // 'color(srgb 1.1813 1.1813 1.1813)'
668
+ glaze.formatHdr(v, 'srgb-linear');
669
+ // → 'color(srgb-linear 1.4631 1.4631 1.4631)'
670
+ ```
671
+
672
+ [`glaze.formatHdr()`](./utilities.md#glazeformathdrvariant-hdrformat) writes one variant's HDR value, and returns `undefined` for a variant without one. With `'srgb-linear'` it writes the same color in linear light; exporters always write `'srgb'`. For the channels as numbers, [`hdrToSrgb()` / `hdrToLinearSrgb()`](./utilities.md#hdrtosrgbhdr--hdrtolinearsrgbhdr) return the extended sRGB and the linear ones; [`formatSrgb` and `formatSrgbLinear`](./utilities.md#format-writers) write such channels as strings.
673
+
674
+ A white base keeps its surface's `h` and `s`: in `glaze(280, 80, { glowTuning: { headroom: 1 } })`, a 12-tone glow on a `tone: 100` surface has `hdr` `{ h: 280, s: 0.8, t: 1, exposure: 0.5489… }`, which is still white. Check `t` before reading `s` as chroma.
675
+
676
+ ### HDR in each exporter
677
+
678
+ | Method | Where the HDR value goes | Colors included |
679
+ | -------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------- | ---------------------------- |
680
+ | `tasty()` | A state key right after each SDR key that has one: `states.hdr` after `''`, `` `${key} & ${states.hdr}` `` after any other key. | Sparse |
681
+ | `json()` | A `<variant>Hdr` key right after each SDR key that has one. | Sparse |
682
+ | `tokens()` | A `<variant>Hdr` map right after each emitted variant map. Always present; `{}` when nothing passes white. | Dense |
683
+ | `css()` | `result.hdr`: `{ light, dark, lightContrast, darkContrast }`. Every string is `''` when nothing passes white. | Dense, over all four schemes |
684
+ | `tailwind()` | Blocks after every SDR block, under `hdrSelector`, then combined with `darkSelector`, `highContrastSelector`, and both. Empty blocks are skipped. | Dense |
685
+ | `dtcg()`, `dtcgResolver()` | `$extensions['me.tenphi.glaze'].hdr` on the token, in each scheme document that has one. `$value` stays SDR. | Per document |
686
+
687
+ **Sparse** entries exist only for the variants that have an HDR value. **Dense** blocks hold every color with an HDR value in any emitted scheme, re-declared with its SDR value in a scheme that has none, so a dense block layered over the SDR ones can't leak a light HDR value into dark. Only the schemes `modes` emits count; `css()` counts all four.
688
+
689
+ Order matters too. Wire each `css()` HDR block after the SDR blocks, under the same selector plus `(dynamic-range: high)`: in a dark HDR context both the light and the dark HDR blocks match, and the later dark one wins. `tailwind()` emits its blocks in that order, and `tasty()` emits its keys in the order [State aliases](#state-aliases) lists, so don't sort them.
690
+
691
+ [HDR glows](../hdr.mdx#output-is-opt-in) shows each export for a theme with two glows: [CSS blocks](../hdr.mdx#css-blocks), [Tasty keys](../hdr.mdx#tasty-keys), [Tailwind blocks](../hdr.mdx#tailwind-blocks), [JSON and tokens](../hdr.mdx#json-and-tokens), and [DTCG](../hdr.mdx#dtcg).
692
+
693
+ ### DTCG `$extensions`
694
+
695
+ `$value` stays the SDR color: DTCG color components stop at 1. With `modes.hdr`, a token whose variant has an HDR value gains `$extensions` after `$value`, in that scheme's document only. [DTCG](../hdr.mdx#dtcg) shows a full token.
696
+
697
+ `GlazeDtcgExtensions` is `{ 'me.tenphi.glaze': { hdr: GlazeDtcgHdr } }`.
698
+
699
+ `GlazeDtcgHdr`:
700
+
701
+ | Field | Type | Description |
702
+ | ---------- | ----------------------------------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
703
+ | `exposure` | `number` | Gain over `base` in stops, rounded to 6 decimals. |
704
+ | `tone` | `number` | Luminance tone of the HDR value, rounded to 4 decimals. Past 100 when the headroom covers the step. A saturated base capped by `headroom` can land below 100. |
705
+ | `base` | `DtcgSrgbColorValue` | The opaque base, what SDR paints at its brightest. Always sRGB with a `hex`, whatever `colorSpace`; never pastel; no `alpha`. |
706
+ | `value` | `{ colorSpace: 'srgb', components: [r, g, b] }` | The HDR color, `base` times `2^exposure` in linear light, as extended sRGB: the same numbers as the [HDR string](#hdr-strings), to 6 decimals. Only the brightest component is guaranteed above 1. |
707
+
708
+ Only DTCG carries `base` and `exposure`, which Apple's exposure initializers take; `json()` and `tokens()` carry the string, whose numbers are the extended sRGB channels. [Native apps](../hdr.mdx#native-apps) maps each field onto Apple, Android, and Windows APIs.
709
+
710
+ ### Palettes and color tokens
711
+
712
+ * Palette HDR entries follow the same prefixes, primary alias, and collision filtering as the SDR ones; see [HDR in palettes](./palette.md#hdr-in-palettes).
713
+ * `glaze.color()` tokens are never glows, so they never carry an HDR value: `modes.hdr`, `states.hdr`, and `hdrSelector` change nothing, and `token.css()` has no `hdr` option.