@tenphi/glaze 0.0.0-snapshot.c13086f → 0.0.0-snapshot.c61bbdf

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.
@@ -0,0 +1,437 @@
1
+ ---
2
+ title: Configuration
3
+ description: The global config, per-instance overrides, tone windows, the scheme mapping formulas, validation rules, and every warning Glaze emits.
4
+ ---
5
+
6
+ Glaze has one global config that every theme, palette, and color token reads at resolve time. Themes and tokens can override part of it with a sparse per-instance `GlazeConfigOverride`. This page lists every field, how the scheme mapping uses them, what Glaze validates, and what it warns about. For the ideas behind tone windows and adaptation, see [Light, dark, and high contrast](../schemes.mdx) and [Tone and OKHST](../okhst.mdx).
7
+
8
+ ## Global config
9
+
10
+ ```ts
11
+ import { glaze } from '@tenphi/glaze';
12
+
13
+ glaze.configure({
14
+ lightTone: [10, 100],
15
+ darkTone: [15, 95],
16
+ darkDesaturation: 0.1,
17
+ states: {
18
+ dark: '@media(prefers-color-scheme: dark)',
19
+ highContrast: '@media(prefers-contrast: more)',
20
+ },
21
+ modes: { dark: true, highContrast: false },
22
+ shadowTuning: { objectInfluence: 0.5 },
23
+ glowTuning: { objectInfluence: 0.5 },
24
+ autoFlip: true,
25
+ inferRole: true,
26
+ contrastLevel: 'auto',
27
+ });
28
+ ```
29
+
30
+ | Function | Returns | Description |
31
+ | --- | --- | --- |
32
+ | `glaze.configure(config)` | `void` | Merges a `GlazeConfig` into the global config and invalidates every theme, palette, and token cache. |
33
+ | `glaze.getConfig()` | `GlazeConfigResolved` | A snapshot of the current global config. |
34
+ | `glaze.resetConfig()` | `void` | Restores the defaults and invalidates every cache. |
35
+
36
+ ### `glaze.configure(config)`
37
+
38
+ - Omitted fields keep their current value. `configure()` never clears a field by omission, and passing `undefined` is the same as omitting it. Use `resetConfig()` to start over, or `contrastLevel: 'auto'` to leave manual contrast mode.
39
+ - `states` and `modes` merge field by field: `configure({ modes: { highContrast: true } })` keeps `modes.dark`.
40
+ - `lightTone`, `darkTone`, `shadowTuning`, and `glowTuning` are replaced as a whole. `configure({ shadowTuning: { tint: 0.3 } })` followed by `configure({ shadowTuning: { reach: 0.9 } })` leaves `{ reach: 0.9 }`.
41
+ - `contrastLevel` is validated on the spot: numbers clamp to 0–100, and a non-finite number throws `glaze: contrastLevel must be a finite number 0–100 or 'auto' (got NaN).`
42
+ - Every call bumps an internal config version, so cached resolutions recompute on the next output call. Existing themes pick up the change for every field their own override doesn't set.
43
+
44
+ ### `glaze.getConfig()`
45
+
46
+ Returns a `GlazeConfigResolved` — every field present, defaults filled in:
47
+
48
+ ```ts
49
+ glaze.getConfig();
50
+ // → {
51
+ // lightTone: { lo: 10, hi: 100, eps: 0.05 },
52
+ // darkTone: { lo: 15, hi: 95, eps: 0.05 },
53
+ // darkDesaturation: 0.1,
54
+ // states: {
55
+ // dark: '@media(prefers-color-scheme: dark)',
56
+ // highContrast: '@media(prefers-contrast: more)',
57
+ // },
58
+ // modes: { dark: true, highContrast: false },
59
+ // autoFlip: true,
60
+ // pastel: false,
61
+ // inferRole: true,
62
+ // contrastLevel: 'auto',
63
+ // }
64
+ ```
65
+
66
+ The default windows come back in the `{ lo, hi, eps }` form; a window you set comes back in the form you passed (`[12, 98]` stays `[12, 98]`). `shadowTuning` and `glowTuning` are absent until you set them. `pastel` is always `false` here because it is instance-only.
67
+
68
+ The snapshot is a shallow copy. Reassigning a top-level field on it is harmless, but the nested objects (`states`, `modes`, object windows, tunings) are shared with the live config, so don't mutate them — use `configure()`.
69
+
70
+ For the config a particular theme resolves with (global merged with its override), use [`theme.getConfig()`](./theme.md#themegetconfig).
71
+
72
+ ## `GlazeConfig`
73
+
74
+ The input to `glaze.configure()`. Every field is optional.
75
+
76
+ | Field | Type | Default | Description |
77
+ | --- | --- | --- | --- |
78
+ | `lightTone` | `ToneWindow` | `[10, 100]` | Light-scheme [tone window](#tone-windows). Bypassed by high-contrast variants and `mode: 'static'` colors; widened by `contrastLevel`. |
79
+ | `darkTone` | `ToneWindow` | `[15, 95]` | Dark-scheme tone window. Same bypasses and widening. |
80
+ | `darkDesaturation` | `number` | `0.1` | Fraction (0–1) of saturation removed in `dark` and `darkContrast`. See [Saturation](#saturation). |
81
+ | `states.dark` | `string` | `'@media(prefers-color-scheme: dark)'` | State key for dark variants in [`tasty()`](./output.md#state-aliases) output. |
82
+ | `states.highContrast` | `string` | `'@media(prefers-contrast: more)'` | State key for high-contrast variants in `tasty()` output. |
83
+ | `modes.dark` | `boolean` | `true` | Emit dark variants. See [Output modes](./output.md#output-modes). |
84
+ | `modes.highContrast` | `boolean` | `false` | Emit high-contrast variants. Independent of `contrastLevel`, except that a global level of `100` drops the tier (it would duplicate the normal one). |
85
+ | `shadowTuning` | `ShadowTuning` | — | Default tuning for every `shadow` color. A per-color `tuning` merges over it field by field. Fields and defaults: [`LightingTuning`](./color-definitions.md#lightingtuning). |
86
+ | `glowTuning` | `GlowTuning` | — | Default tuning for every `glow` color. Same shape and merge rule. |
87
+ | `autoFlip` | `boolean` | `true` | Default for each color's `autoFlip`. When on, a contrast floor that can't be met on the requested side of the base may be met on the other side, and a tone delta that overshoots `[0, 100]` mirrors to the other side of its base. When off, only the requested direction is used: an unmet floor pins the tone to that direction's extreme (with a warning) and an overshooting tone delta clamps. Mix colors have no per-color `autoFlip` and always use this value. |
88
+ | `inferRole` | `boolean` | `true` | Infer a color's `role` (APCA polarity) from its name when it has no explicit `role`. With `false`, the base-opposite and `'text'` fallbacks still apply. See [How a role is chosen](../contrast.mdx#how-a-role-is-chosen). |
89
+ | `contrastLevel` | `number \| 'auto'` | `'auto'` | Manual contrast level, 0–100. Resolves the `light` / `dark` variants at that point between normal (`0`) and high contrast (`100`); `lightContrast` / `darkContrast` stay the true high-contrast resolution. Numbers clamp to 0–100; non-finite numbers throw. Full rules: [Manual contrast level](./contrast.md#manual-contrast-level). |
90
+
91
+ `pastel` is not part of `GlazeConfig`: it can only be set per instance or per color.
92
+
93
+ ## `GlazeConfigResolved`
94
+
95
+ What `glaze.getConfig()` and `theme.getConfig()` return: the same fields with defaults applied, plus `pastel`.
96
+
97
+ | Field | Type |
98
+ | --- | --- |
99
+ | `lightTone`, `darkTone` | `ToneWindow` |
100
+ | `darkDesaturation` | `number` |
101
+ | `states` | `{ dark: string; highContrast: string }` |
102
+ | `modes` | `{ dark: boolean; highContrast: boolean }` |
103
+ | `shadowTuning`, `glowTuning` | `LightingTuning \| undefined` |
104
+ | `autoFlip` | `boolean` |
105
+ | `inferRole` | `boolean` |
106
+ | `contrastLevel` | `number \| 'auto'` |
107
+ | `pastel` | `boolean` — always `false` globally; the override's value on `theme.getConfig()`. |
108
+
109
+ ## Tone windows
110
+
111
+ ```ts
112
+ type ToneWindow =
113
+ | [number, number] // [lo, hi]
114
+ | { lo: number; hi: number; eps: number }
115
+ | false;
116
+ ```
117
+
118
+ `lo` and `hi` are OKHSL lightness boundaries (0–100). The authored tone scale `0–100` is spread linearly across the tone interval between them.
119
+
120
+ | Form | Meaning |
121
+ | --- | --- |
122
+ | `[lo, hi]` | Boundaries at the reference eps `0.05`. The common form. |
123
+ | `{ lo, hi, eps }` | Boundaries plus a per-scheme render eps. Advanced; see [Reference and render epsilon](../okhst.mdx#reference-and-render-epsilon). |
124
+ | `false` | The full range `[0, 100]` at the reference eps. Removes the boundaries, not the tone transfer. |
125
+
126
+ | Window | Default | Stored tone range |
127
+ | --- | --- | --- |
128
+ | `lightTone` | `[10, 100]` | 4.95–100 |
129
+ | `darkTone` | `[15, 95]` | 9.25–95.9 |
130
+
131
+ - **High contrast** replaces both windows with `[0, 100]`, keeping the window's `eps`.
132
+ - **`contrastLevel`** moves each boundary toward `[0, 100]` (formula below). A `false` window is already the full range, so it doesn't change with the level.
133
+ - **`mode: 'static'`** colors skip the window entirely.
134
+ - **Colors seeded with `from`** skip the light window so the light variant reproduces the literal; dark and high contrast still adapt. See [`from`](./color-definitions.md#from).
135
+ - Window endpoints aren't validated. Keep `0 ≤ lo < hi ≤ 100`.
136
+
137
+ ## Per-instance overrides
138
+
139
+ A `GlazeConfigOverride` overrides resolve-relevant fields for one theme or color token. It is accepted by:
140
+
141
+ | Call | Effect |
142
+ | --- | --- |
143
+ | `glaze(hue, saturation, config)` / `glaze(seed, config)` | The theme's override. See [Themes](./theme.md#per-theme-config-override). |
144
+ | `glaze.color(input, config)` | The token's override. See [Color tokens](./color-tokens.md). |
145
+ | `theme.extend({ config })` | Shallow-merged over the parent's override for the child theme. |
146
+ | `theme.export(override)`, `token.export(override)`, `palette.export(override)` | Merged into the frozen snapshot config only; the live instance is unchanged. |
147
+
148
+ ### `GlazeConfigOverride`
149
+
150
+ | Field | Type | Falls back to | Description |
151
+ | --- | --- | --- | --- |
152
+ | `lightTone` | `ToneWindow` | global `lightTone` | Light tone window, or `false` for the full range. |
153
+ | `darkTone` | `ToneWindow` | global `darkTone` | Dark tone window, or `false` for the full range. |
154
+ | `darkDesaturation` | `number` | global `darkDesaturation` | Dark saturation reduction (0–1). |
155
+ | `autoFlip` | `boolean` | global `autoFlip` | Default for each color's `autoFlip`. |
156
+ | `inferRole` | `boolean` | global `inferRole` | Name-based role inference. |
157
+ | `contrastLevel` | `number \| 'auto'` | global `contrastLevel` | Manual contrast level for this instance. `'auto'` opts out of a global level. A per-instance level never changes which modes are emitted. Validated when the instance resolves. |
158
+ | `shadowTuning` | `ShadowTuning` | global `shadowTuning` | Replaces the global shadow tuning as a whole for this instance. Per-color `tuning` still merges over it field by field. Meaningful for themes; harmless on tokens. |
159
+ | `glowTuning` | `GlowTuning` | global `glowTuning` | Same for glows. |
160
+ | `pastel` | `boolean` | `false` | Instance-wide [`pastel`](./color-definitions.md#per-color-pastel) default for colors that don't set their own. Instance-only: never read from the global config. |
161
+
162
+ `states` and `modes` are not part of the override. They are global, or per call on the [output methods](./output.md).
163
+
164
+ ### Merge rules
165
+
166
+ - **Sparse and live.** An instance stores only the fields you set. At resolve time they are merged over the *current* global config, so fields you didn't set keep following `glaze.configure()`.
167
+ - **Overridden fields are pinned.** A later `configure()` can't change them for this instance.
168
+ - **`extend`** inherits the parent's override and shallow-merges the child's on top.
169
+ - **Export freezes.** `.export(override?)` writes `getConfig() ∪ instance override ∪ override` into the snapshot, and `themeFrom` / `colorFrom` / `paletteFrom` pin that freeze as the restored instance's override. Nested objects (windows, tunings) are replaced wholesale, not merged. `states` and `modes` are never frozen.
170
+ - **`contrastLevel` is frozen only when authored.** A level set on the instance or passed to `.export()` is written to the snapshot; a level inherited from the global config is a live preference and is left out, so a restored theme keeps following the global slider.
171
+
172
+ ```ts
173
+ import { glaze } from '@tenphi/glaze';
174
+
175
+ const t = glaze(280, 80, { lightTone: [0, 50] });
176
+ t.colors({ text: { tone: 50, saturation: 1 } });
177
+
178
+ t.resolve().get('text')!.light.t;
179
+ // → 0.2534… — inside [0, 50], whatever the global lightTone
180
+ t.resolve().get('text')!.dark.s;
181
+ // → 0.72 (darkDesaturation 0.1, not overridden)
182
+
183
+ glaze.configure({ darkDesaturation: 0.5 });
184
+ t.resolve().get('text')!.dark.s;
185
+ // → 0.4 — follows the global change
186
+
187
+ const child = t.extend({ config: { darkTone: false } });
188
+ child.getConfig().lightTone; // → [0, 50] (inherited)
189
+ child.getConfig().darkTone; // → false (added)
190
+ ```
191
+
192
+ ```ts
193
+ // Standalone token without a dark window: light keeps the input tone,
194
+ // dark is its exact inversion (100 − T)
195
+ glaze.color('#26fcb2', { darkTone: false });
196
+
197
+ // Restore the #000 → white dark flip with a full-height dark window
198
+ glaze.color('#000000', { lightTone: false, darkTone: [15, 100] });
199
+
200
+ // Structured token with an override
201
+ glaze.color(
202
+ { hue: 152, saturation: 95, tone: 74 },
203
+ { darkTone: false, pastel: true },
204
+ );
205
+ ```
206
+
207
+ ## Adaptation modes
208
+
209
+ `mode` on a regular color (`AdaptationMode`) selects which steps of the [scheme mapping](#scheme-mapping) apply to it. What each mode is for, with visuals: [Adaptation modes](../schemes.mdx#adaptation-modes).
210
+
211
+ | Mode | Inverted in dark | Tone windows | `darkDesaturation`, dark seed |
212
+ | --- | --- | --- | --- |
213
+ | `'auto'` (default) | Yes, in `dark` and `darkContrast` | Applied | Applied |
214
+ | `'fixed'` | No | Applied | Applied |
215
+ | `'static'` | No | Skipped | Skipped |
216
+
217
+ Tone deltas (`'+N'` / `'-N'`) are measured from the base. Under `'fixed'` and `'static'` the delta is applied to the base's tone in each scheme, sign kept. Under `'auto'`, dark applies it to the base's *light* tone, then inverts and remaps the result like an authored tone, so the dark step is up to about 0.87× the authored size on a light base (`'-20'` on tone 97 steps 17.2) and uneven on a mid-tone one. High-contrast steps stay exact. Details: [How relative tone adapts](../schemes.mdx#how-relative-tone-adapts).
218
+
219
+ ## Scheme mapping
220
+
221
+ Every scheme runs the same tone pipeline; there is no fitted dark curve. The transfer functions are specified in [Tone and OKHST](../okhst.mdx) and the [canonical OKHST specification](https://github.com/tenphi/okhst).
222
+
223
+ ### Tone
224
+
225
+ For an authored tone `T` (0–100) and the scheme's active window `{ lo, hi, eps }`:
226
+
227
+ ```text
228
+ T' = 100 − T if mode is 'auto' and the scheme is dark
229
+ = T otherwise
230
+ loT = toTone(lo / 100, eps)
231
+ hiT = toTone(hi / 100, eps)
232
+ l = fromTone(loT + (T' / 100) × (hiT − loT), eps) // OKHSL lightness
233
+ t = toTone(l, REF_EPS) // stored tone
234
+ ```
235
+
236
+ `mode: 'static'` skips all of it: `t = clamp(T, 0, 100)`. The stored `t` is what `ResolvedColorVariant.t` holds (divided by 100), what tone deltas are measured against, and what the contrast solver searches.
237
+
238
+ The active window per variant:
239
+
240
+ | Variant | Window |
241
+ | --- | --- |
242
+ | `light` | `lightTone` (widened by `contrastLevel`) |
243
+ | `dark` | `darkTone` (widened by `contrastLevel`) |
244
+ | `lightContrast`, `darkContrast` | `[0, 100]`, with the configured window's `eps` |
245
+
246
+ With a numeric `contrastLevel` `L` and `f = L / 100`, the normal windows move toward the full range:
247
+
248
+ ```text
249
+ lo(f) = lo × (1 − f)
250
+ hi(f) = hi + (100 − hi) × f
251
+ ```
252
+
253
+ The defaults at level 50 are light `[5, 100]` (stored tone 1.63–100) and dark `[7.5, 97.5]` (stored tone 3.15–97.97). `eps` is not interpolated.
254
+
255
+ Authored tones under the default windows (neutral seed), as stored tone:
256
+
257
+ | Authored | Mode | `light` | `dark` | `lightContrast` | `darkContrast` |
258
+ | ---: | --- | ---: | ---: | ---: | ---: |
259
+ | 0 | `auto` | 4.95 | 95.9 | 0 | 100 |
260
+ | 50 | `auto` | 52.47 | 52.57 | 50 | 50 |
261
+ | 100 | `auto` | 100 | 9.25 | 100 | 0 |
262
+ | 0 | `fixed` | 4.95 | 9.25 | 0 | 0 |
263
+ | 100 | `fixed` | 100 | 95.9 | 100 | 100 |
264
+ | 50 | `static` | 50 | 50 | 50 | 50 |
265
+
266
+ A render `eps` other than the reference bends mid tones: with `lightTone: { lo: 10, hi: 100, eps: 0.2 }`, authored tone 50 stores 63.89 in `light` (instead of 52.47) and 62.95 in `lightContrast`.
267
+
268
+ ### Saturation
269
+
270
+ ```text
271
+ S_dark = S_light × (1 − darkDesaturation) // dark and darkContrast
272
+ ```
273
+
274
+ - Skipped for `mode: 'static'`.
275
+ - Bypassed when the dark saturation is authored: always for a per-color `darkSaturation`, and for a theme `darkSaturation` seed on every color that reads the seed. A [`from`](./color-definitions.md#from) color without its own `saturation` or `darkSaturation` starts from the literal's saturation instead, so a seed `darkSaturation` doesn't bypass its reduction. See [Dark seed](../schemes.mdx#dark-seed).
276
+ - `darkDesaturation` isn't range-checked; the result is clamped to 0–1, so values above 1 give 0 and negative values raise saturation.
277
+
278
+ With seed saturation 80 and the default `0.1`, an `auto` color's saturation is `0.8` in `light` and `0.72` in `dark` and `darkContrast`; a `static` one stays `0.8`.
279
+
280
+ ### Hue
281
+
282
+ Hue isn't mapped. Dark variants re-anchor relative hues to the theme's `darkHue` seed and use a per-color `darkHue` when one is set. An absolute per-color `hue` (or a `from` color's literal hue, when it has no `hue` or `darkHue`) is kept as is, and `static` colors ignore both. See [Dark seed](../schemes.mdx#dark-seed) and [`darkHue` / `darkSaturation`](./color-definitions.md#dark-seed-darkhue--darksaturation).
283
+
284
+ ## Validation
285
+
286
+ Glaze validates a theme's definitions when it resolves — on the first `resolve()` or output call after the definitions change, not when you call `colors()`. Graph errors throw; recoverable numeric bounds are clamped; a physically unreachable target pins to the limit of its search and warns (a mix pins without a warning); risky combinations resolve and warn.
287
+
288
+ ### Errors
289
+
290
+ | Condition | Message |
291
+ | --- | --- |
292
+ | `contrast` without `base` (theme color) | `glaze: color "a" has "contrast" without "base".` |
293
+ | Relative `tone` without `base` (theme color) | `glaze: color "a" has relative "tone" without "base".` |
294
+ | No absolute `tone`, no `from`, and no `base` | `glaze: color "a" must have either absolute "tone" (root), "from" (a literal color), or "base" (dependent).` |
295
+ | `from` can't be parsed | `glaze: color "a" has an invalid "from" value. glaze: invalid hex color "#zzz".` |
296
+ | `contrast` pair switches metric (`[4.5, { apca: 75 }]`), regular or mix | `glaze: color "a" has a "contrast" pair that switches metric (wcag → apca). A WCAG ratio and an APCA Lc are different scales; use one metric for both entries, e.g. { wcag: [normal, highContrast] }.` |
297
+ | `base` names a missing color | `glaze: color "a" references non-existent base "nope".` |
298
+ | `base` names a shadow or glow | `glaze: color "b" base "a" references a glow color.` |
299
+ | Shadow / glow `bg` or `fg` names a missing color | `glaze: shadow "a" references non-existent bg "nope".` |
300
+ | Shadow / glow `bg` or `fg` names a shadow or glow | `glaze: glow "b" bg "a" references a shadow color.` |
301
+ | Mix `base` names a missing color | `glaze: mix "a" references non-existent base "nope".` |
302
+ | Mix `target` names a missing color | `glaze: mix "a" references non-existent target "nope".` |
303
+ | Mix `base` / `target` names a shadow or glow | `glaze: mix "a" target "sh" references a shadow color.` |
304
+ | Circular reference through `base`, `bg`, `fg`, or a mix's `base` / `target` | `glaze: circular base reference detected involving "a".` |
305
+ | Non-finite `contrastLevel` (thrown by `configure()`, or when an instance with such an override resolves) | `glaze: contrastLevel must be a finite number 0–100 or 'auto' (got NaN).` |
306
+
307
+ ### Color value errors
308
+
309
+ Every place that takes a `GlazeColorValue` — a theme color's `from`, `glaze.color()`, and the `bg` / `fg` of `glaze.shadow()` / `glaze.glow()` — uses one parser. It throws immediately for standalone calls; for `from` the message is wrapped in the color's name (above).
310
+
311
+ | Input | Message |
312
+ | --- | --- |
313
+ | Hex of the wrong length (not 3, 4, 6, or 8 digits) or with non-hex digits | `glaze: invalid hex color "#zzz".` |
314
+ | A string that isn't hex or `rgb()` / `rgba()` / `hsl()` / `hsla()` / `okhsl()` / `okhst()` / `oklch()` (named colors included) | `glaze: unsupported color string "red".` |
315
+ | A color function without exactly 3 components (alpha aside) | `glaze: expected 3 components in "rgb(1 2)".` |
316
+ | `{ h, s, l }` with a non-finite field | `glaze.color: OkhslColor h/s/l must be finite numbers.` |
317
+ | `{ h, s, l }` with `s` or `l` above 1.5 | `glaze.color: OkhslColor s/l must be in 0–1 range. Did you mean the structured form { hue, saturation, tone } (which uses 0–100)?` |
318
+ | `{ h, s, t }` with a non-finite field, or `s` / `t` above 1.5 | `glaze.color: OkhstColor h/s/t must be finite numbers.` / `glaze.color: OkhstColor s/t must be in 0–1 range. …` |
319
+ | `{ r, g, b }` with a channel outside 0–255 | `glaze.color: RgbColor r must be a finite number in 0–255 (got 300).` |
320
+ | `{ l, c, h }` with a non-finite field, or `l` / `c` above 1.5 | `glaze.color: OklchColor l/c/h must be finite numbers.` / `glaze.color: OklchColor l/c must be in 0–1 range (matching oklch() strings).` |
321
+ | An `[r, g, b]` array | `glaze.color: RGB tuple [r, g, b] is no longer supported — use { r, g, b } instead.` |
322
+
323
+ The hex check is loose in the 6- and 8-digit forms: only the first digit of each pair is checked, so `#1z2z3z` reads as `#010203` instead of throwing.
324
+
325
+ Errors specific to the structured `glaze.color()` input, restoring snapshots, and building palettes are listed on [Color tokens](./color-tokens.md), [Themes](./theme.md#glazethemefromdata--glazefromdata), and [Palettes](./palette.md).
326
+
327
+ ### Allowed without `base`
328
+
329
+ | Condition | Behavior |
330
+ | --- | --- |
331
+ | `tone: 'max'` / `'min'` | A root color at the scheme's tone extreme. |
332
+ | `contrast` without `base` in `glaze.color()` | Measured against the token's own input color. |
333
+ | A tone delta (`tone: '+N'`) without `base` in `glaze.color()` | Measured from the token's own input color. |
334
+
335
+ ### Clamped silently
336
+
337
+ | Value | Range |
338
+ | --- | --- |
339
+ | Resolved `tone` | 0–100 |
340
+ | Per-color `saturation` / `darkSaturation` factor | 0–1 |
341
+ | Shadow / glow `intensity` | 0–100 (an unreachable step then warns) |
342
+ | Lighting `tint`, `reach`, `hueBleed` | 0–1. `objectInfluence` isn't clamped; the object factor `k` floors at 0. |
343
+ | Mix `value` | 0–100 |
344
+ | `contrastLevel` | 0–100 |
345
+ | WCAG numeric target | At least 1 |
346
+ | APCA numeric target | Its magnitude (`-60` → `60`) |
347
+
348
+ A tone delta that overshoots `[0, 100]` mirrors to the other side of its base when `autoFlip` is on; if the mirror also overshoots, it clamps on the authored side. With `autoFlip: false` it clamps on the authored side. For example, a `'static'` color with `tone: '-20'` on a static base at tone 10 resolves to 30 with `autoFlip` and to 0 without, in every scheme; with `'-60'` on a base at 50 it resolves to 0 either way. An `'auto'` child gets the same results in light; in dark it follows the light-anchored rule in [Adaptation modes](#adaptation-modes).
349
+
350
+ ### Resolved with a warning
351
+
352
+ | Condition | Result |
353
+ | --- | --- |
354
+ | A regular color's `contrast` floor is out of reach | Pins to the extreme of the side searched first; [contrast unmet](#contrast-unmet) warning. |
355
+ | A shadow / glow step is out of the pigment's reach | Pins to the pigment's strongest composite (or stays transparent if it can't move the surface at all); [lighting](#shadow-or-glow-out-of-reach) warning. |
356
+ | `contrast` and `opacity` on the same color | Resolves the floor for the opaque color; [contrast and opacity](#contrast-and-opacity) warning. |
357
+ | A color string with an alpha component (`#rrggbbaa`, `rgb(… / a)`) where Glaze has no opacity field | Alpha dropped; [alpha dropped](#alpha-dropped) warning. |
358
+
359
+ A mix whose `contrast` target is out of reach resolves **without** a warning: the value pins to the end of the base → target range it searched (normally the pure target). A mix whose value is already 100, including the high-contrast entry of `value: [80, 100]`, can only search toward the base, so it pins to the pure base instead.
360
+
361
+ ## Warnings
362
+
363
+ Glaze reports problems it can recover from through `console.warn`, always prefixed with `glaze:`. Nothing is thrown.
364
+
365
+ | Warning | Emitted when | Deduplicated |
366
+ | --- | --- | --- |
367
+ | [Contrast unmet](#contrast-unmet) | The solver can't reach a regular color's floor in a scheme. | Yes |
368
+ | [Contrast drift](#contrast-drift) | A contrast-floored color measures under its target after resolution. | Yes |
369
+ | [Shadow or glow out of reach](#shadow-or-glow-out-of-reach) | A `shadow` / `glow` step exceeds what its pigment can do. | Yes |
370
+ | [Contrast and opacity](#contrast-and-opacity) | A regular color has both `contrast` and `opacity`. | No — once per resolve |
371
+ | [Alpha dropped](#alpha-dropped) | A color value carries alpha that Glaze discards. | No |
372
+ | [Percentage scale](#percentage-scale) | A `format*` writer gets a value above 1. | Once per writer |
373
+ | Palette collision | Two palette themes produce the same token name. See [Collision detection](./palette.md#collision-detection). | No |
374
+
375
+ The first three share one session cache of 256 keys, which starts over when full. A key includes the color name, scheme, target, and measured value, so a dev server that re-resolves on every change prints each distinct problem once. Resolved themes are cached until their definitions or the config change, so the contrast-and-opacity warning prints once per actual resolve. The alpha-dropped warning for a theme color's `from` can print several times within one resolve, since the value is parsed more than once.
376
+
377
+ ### Contrast unmet
378
+
379
+ ```text
380
+ glaze: color "badge-text" cannot meet WCAG 7.00 in light scheme
381
+ (got 4.94).
382
+ Try widening the tone window, lowering the contrast target, or picking
383
+ a base color further from this color's tone.
384
+ ```
385
+
386
+ Fires when the result is more than 2% under a WCAG target, or more than 1.5 Lc under an APCA target (the solver aims slightly past the target, so smaller shortfalls are rounding). The scheme is `light`, `dark`, `lightContrast`, or `darkContrast`; APCA targets print as `APCA Lc 75.0`. Standalone tokens print their `name` option, or `"value"` when it isn't set. Fixes: [When a floor can't be met](../contrast.mdx#when-a-floor-cant-be-met).
387
+
388
+ ### Contrast drift
389
+
390
+ ```text
391
+ glaze: color "badge-text" drifts below WCAG 7.00 in light scheme
392
+ (measured 4.94).
393
+ Chromatic luminance differs from the gray tone; nudge the tone or
394
+ saturation if the floor matters.
395
+ ```
396
+
397
+ After all schemes resolve, Glaze re-measures every regular color with `contrast` and `base` against its base, in the floor's metric and the color's role polarity, with the same margins. An unmet floor triggers this advisory as well, for the same scheme. Mixes are not re-measured. At an effective `contrastLevel` of 100, global or per instance, the high-contrast slots are skipped because they mirror the normal ones. Background: [Chromatic drift advisory](../contrast.mdx#chromatic-drift-advisory).
398
+
399
+ ### Shadow or glow out of reach
400
+
401
+ ```text
402
+ glaze: glow "g" cannot reach a 5.0-tone step in light scheme
403
+ (reached 0.0 at full strength).
404
+ Lower the intensity, move the surface away from white, or raise a
405
+ tuned-down `reach`.
406
+ ```
407
+
408
+ Fires when the composite lands more than 0.05 tones short of `intensity × k`. The standalone functions name themselves instead of a color and omit the scheme: `glaze: glaze.glow() cannot reach a 5.0-tone step (reached 0.0 at full strength). …`. A shadow's message suggests moving the surface away from black. See [Unreachable steps](../shadows-and-glows.mdx#unreachable-steps).
409
+
410
+ ### Contrast and opacity
411
+
412
+ ```text
413
+ glaze: color "text" has both "contrast" and "opacity".
414
+ Opacity makes perceived tone unpredictable.
415
+ ```
416
+
417
+ Emitted during validation. For a translucent color with a floor, use a [transparent mix](../mixes.mdx).
418
+
419
+ ### Alpha dropped
420
+
421
+ ```text
422
+ glaze: alpha component dropped from "#ffffff80"
423
+ (standalone color has no opacity field).
424
+ ```
425
+
426
+ Any color string Glaze parses — a `from` value, a `glaze.color()` value, or a `glaze.shadow()` / `glaze.glow()` input — keeps only its color. Use `opacity` on a regular color, or a transparent mix, for translucency.
427
+
428
+ ### Percentage scale
429
+
430
+ ```text
431
+ glaze: formatRgb() got a value above 1 — it takes the 0-1 factors every
432
+ Glaze producer returns (resolve(), variantToOkhsl, srgbToOkhsl), not
433
+ 0-100 percentages.
434
+ Divide by 100, or drop the "* 100" left over from the pre-2.0 signature.
435
+ ```
436
+
437
+ The [format writers](./utilities.md#format-writers) take `s` / `l` / `t` on 0–1. A value above 1 can only be pre-2.0 percentage input, so each writer warns the first time it sees one in the process (and still formats the value). Upgrade notes: [2.0 — `format*` takes 0–1](../migration.md#20--format-takes-01).