@tenphi/glaze 2.0.2 → 3.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/migration.md CHANGED
@@ -1,263 +1,75 @@
1
- # Migration & Integration
1
+ ---
2
+ title: Migration and upgrading
3
+ description: Move an existing color system to Glaze without renaming tokens, and upgrade between major Glaze versions.
4
+ ---
2
5
 
3
- How to move an existing CSS, design-token, or application color system to
4
- Glaze, then export the resulting palette in the shape your app and design tools
5
- consume.
6
-
7
- If you're starting from scratch, see [methodology.md](methodology.md) first — that's about _designing_ the palette. This doc is about _consuming_ it.
8
-
9
- ## Contents
10
-
11
- - [Choosing an export](#choosing-an-export)
12
- - [Wiring exports into the app](#wiring-exports-into-the-app)
13
- - [Prefix map strategies](#prefix-map-strategies)
14
- - [Migrating an existing color system](#migrating-from-an-existing-color-system)
15
- - [Upgrading Glaze](#upgrading-glaze)
16
- - [Common pitfalls](#common-pitfalls)
17
-
18
- ## Choosing an export
19
-
20
- Glaze has two layers of “export”:
21
-
22
- 1. **Authoring snapshots** — `theme.export(override?)` / `token.export(override?)` / `palette.export(override?)`, restored with `glaze.themeFrom` / `colorFrom` / `paletteFrom`. These carry definitions, relations, and a config freeze taken at export time (`kind` + `version`). Live instances keep only a sparse local override; omitted global fields track `configure()` until export.
23
- 2. **Resolved output** — the seven shapes below. These are color strings / design-token documents for apps and tools, **not** restorable as authoring config.
24
-
25
- Glaze emits the same resolved colors in seven shapes. Pick one based on your
26
- renderer or tooling.
27
-
28
- | Method | Output shape | Use it for |
29
- | -------------------------------- | ------------------------------------------------------------------------------------------------------------------ | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
30
- | `palette.tasty(options?)` | `{ '#name': { '': value, '@media(prefers-color-scheme: dark)': value, '@media(prefers-contrast: more)': value } }` | The [Tasty](https://tasty.style) style system. Single object, state aliases keyed inside each token. |
31
- | `palette.tokens(options?)` | `{ light: { name: value }, dark: { name: value }, ... }` | Most CSS-in-JS systems. Per-variant flat maps, easy to feed into a `:root { ... }` selector via your framework's globals. |
32
- | `palette.css(options?)` | `{ light: '--name-color: rgb(...);', dark: '...', ... }` | Framework-free CSS / static stylesheets. Variant-grouped CSS custom property strings ready to wrap in `:root` and `prefers-color-scheme` queries. |
33
- | `palette.json(options?)` | `{ themeName: { name: { light, dark, ... } } }` | Tooling, JSON pipelines. |
34
- | `palette.dtcg(options?)` | `{ light: { name: { $type, $value } }, dark: { ... }, ... }` | W3C [DTCG 2025.10](https://www.designtokens.org/) `.tokens.json` — Figma, Tokens Studio, Style Dictionary, Terrazzo, Penpot. One document per scheme. |
35
- | `palette.dtcgResolver(options?)` | `{ version, sets, modifiers, resolutionOrder }` | W3C DTCG **Resolver-Module** — a single document describing every scheme variant as `sets` + a `scheme` modifier with a context per variant. For resolver tools such as Dispersa. |
36
- | `palette.tailwind(options?)` | `'@theme { --color-*: ... } .dark { ... } ...'` | Tailwind CSS v4. A single ready-to-paste `@theme` block plus dark / high-contrast overrides. |
37
-
38
- `tasty()`, `tokens()`, `json()`, `dtcg()`, `dtcgResolver()`, and `tailwind()` accept `modes` (`{ dark, highContrast }`). `css()` always returns all four strings (`light`, `dark`, `lightContrast`, `darkContrast`). CSS-string exports accept `format` and default to `'oklch'` (`tokens`/`json`/`css`/`tailwind`/`tasty`; `'rgb'` and `'hsl'` also available). `'okhsl'` and `'okhst'` are supported only on [Tasty](https://tasty.style)-shaped exports (`tasty()`, standalone `token()` / `.tasty()`). `dtcg()` and `dtcgResolver()` ignore `format` and use `colorSpace` instead (`'srgb'` default, `'oklch'` opt-in). See [api.md → Palette](api.md#palette) for full options.
39
-
40
- ## Wiring exports into the app
41
-
42
- ### [Tasty](https://tasty.style)
43
-
44
- Spread the result of `palette.tasty()` into a global style call:
45
-
46
- ```ts
47
- import type { Styles } from '@tenphi/tasty';
48
- import { useGlobalStyles } from '@tenphi/tasty';
49
- import { tastyStatic } from '@tenphi/tasty/static';
50
-
51
- export const PALETTE_TOKENS = palette.tasty({
52
- /* prefix map */
53
- }) as Styles;
54
-
55
- // In your root component:
56
- useGlobalStyles('body', PALETTE_TOKENS);
57
-
58
- // Or, for zero-runtime builds:
59
- tastyStatic('body', PALETTE_TOKENS);
60
- ```
61
-
62
- By default the dark / high-contrast variants are keyed by media-query states — `'@media(prefers-color-scheme: dark)'` and `'@media(prefers-contrast: more)'` — so the tokens react to the OS preference out of the box, with no extra [Tasty](https://tasty.style) setup.
63
-
64
- If you'd rather drive the schemes from custom aliases (e.g. a manual toggle that also falls back to the OS preference), set your own [`glaze.configure({ states })`](api.md#configuration) and register what those states _mean_:
65
-
66
- ```ts
67
- import { setGlobalPredefinedStates } from '@tenphi/tasty';
68
-
69
- // glaze.configure({ states: { dark: '@dark', highContrast: '@hc' } });
70
- setGlobalPredefinedStates({
71
- '@dark':
72
- '@root(schema=dark) | (!@root(schema) & @media(prefers-color-scheme: dark))',
73
- '@hc':
74
- '@root(contrast=high) | (!@root(contrast) & @media(prefers-contrast: more))',
75
- });
76
- ```
77
-
78
- The state names here must match the `states` you set in [`glaze.configure({ states })`](api.md#configuration).
79
-
80
- You can also register the tokens as a [Tasty](https://tasty.style) recipe instead of spreading them globally:
81
-
82
- ```ts
83
- import { configure, tasty } from '@tenphi/tasty';
84
-
85
- configure({ recipes: { 'theme-tokens': PALETTE_TOKENS } });
86
-
87
- const Page = tasty({
88
- styles: { recipe: 'theme-tokens', fill: '#surface', color: '#surface-text' },
89
- });
90
- ```
91
-
92
- ### CSS custom properties
93
-
94
- ```ts
95
- const css = palette.css();
96
- const stylesheet = `
97
- :root { ${css.light} }
98
- @media (prefers-color-scheme: dark) { :root { ${css.dark} } }
99
- @media (prefers-contrast: more) { :root { ${css.lightContrast} } }
100
- @media (prefers-color-scheme: dark) and (prefers-contrast: more) {
101
- :root { ${css.darkContrast} }
102
- }
103
- `;
104
- ```
105
-
106
- Each property name is `--<prefix><name>-color` by default. Override the suffix:
107
-
108
- ```ts
109
- palette.css({ suffix: '' }); // → '--surface: rgb(...);'
110
- palette.css({ format: 'oklch' }); // → '--surface-color: oklch(...);'
111
- ```
112
-
113
- ### Framework-agnostic JSON
114
-
115
- ```ts
116
- const data = palette.json();
117
- // → { primary: { surface: { light: 'oklch(...)', dark: 'oklch(...)' } }, ... }
118
- ```
119
-
120
- Feed into your tooling pipeline. Each color is grouped by theme name, then by token name, then by variant — no prefix logic to undo.
121
-
122
- ### W3C DTCG (`.tokens.json`)
123
-
124
- ```ts
125
- import { writeFileSync } from 'node:fs';
126
- const dtcg = palette.dtcg();
127
-
128
- writeFileSync('tokens.light.tokens.json', JSON.stringify(dtcg.light, null, 2));
129
- if (dtcg.dark) {
130
- writeFileSync('tokens.dark.tokens.json', JSON.stringify(dtcg.dark, null, 2));
131
- }
132
- ```
133
-
134
- Each document is a spec-conformant token tree. One file per scheme is the most tool-compatible convention — Style Dictionary treats them as themes, Tokens Studio as sets, and Figma as variable modes. Use `colorSpace: 'oklch'` for wide-gamut, Glaze-native values (no `hex`); the default `'srgb'` emits components plus a `hex` hint that every reader understands. Feed the files straight into Style Dictionary v4+, Tokens Studio, or any DTCG-compatible tool — no Glaze-specific transform needed.
135
-
136
- ### W3C DTCG Resolver-Module (single document)
137
-
138
- When you want **one file** describing every scheme variant — for a resolver tool such as [Dispersa](https://github.com/dispersa-core/dispersa) — use `dtcgResolver()` instead of `dtcg()`:
139
-
140
- ```ts
141
- import { writeFileSync } from 'node:fs';
142
- const resolver = palette.dtcgResolver({ modes: { highContrast: true } });
143
-
144
- writeFileSync('resolver.json', JSON.stringify(resolver, null, 2));
145
- ```
146
-
147
- The document places the light tokens in `sets.base.sources[0]` (the default context) and emits a single `scheme` modifier with a context per variant (`light` / `dark` / `lightContrast` / `darkContrast`). Each non-default context holds that variant's exact resolved tokens — Glaze resolves `darkContrast` independently, so the four-context shape keeps every value correct (two independent modifiers would compose additively and produce wrong dark + high-contrast values). Rename the set, modifier, or contexts via `setName` / `modifierName` / `contextNames` if your resolver expects different labels. Prefer `dtcg()` when you need maximum per-file tool compatibility; prefer `dtcgResolver()` when a resolver tool consumes the document for you.
148
-
149
- ### Tailwind CSS v4
150
-
151
- ```ts
152
- const css = palette.tailwind();
153
- // Write to your CSS entry, or paste into a `@import "tailwindcss"` stylesheet:
154
- // @theme { --color-primary-surface: oklch(...); ... }
155
- // .dark { --color-primary-surface: oklch(...); ... }
156
- ```
157
-
158
- The `--color-*` namespace makes every color available as `bg-*` / `text-*` / `border-*`. Drive dark mode from the OS preference instead of a class with `darkSelector: '@media (prefers-color-scheme: dark)'` (it nests `:root` automatically). High-contrast overrides land under `.high-contrast` and `.dark.high-contrast` by default; both are omitted unless `modes.highContrast` is enabled.
159
-
160
- ## Prefix map strategies
161
-
162
- `palette.tokens()` / `tasty()` / `css()` / `dtcg()` / `tailwind()` accept a `prefix` option:
163
-
164
- | Value | Result |
165
- | ------------------------ | -------------------------------------------------------------------------- |
166
- | `true` (default) | Every theme prefixes its tokens with `<themeName>-`. |
167
- | `false` | No prefixes. Colliding keys produce a `console.warn`; first-write wins. |
168
- | `Record<string, string>` | Per-theme prefix overrides. Themes not listed fall back to `<themeName>-`. |
169
-
170
- The most common production pattern: **default theme unprefixed, every other theme prefixed with its name**:
171
-
172
- ```ts
173
- palette.tasty({
174
- prefix: {
175
- default: '',
176
- primary: 'primary-',
177
- success: 'success-',
178
- danger: 'danger-',
179
- warning: 'warning-',
180
- note: 'note-',
181
- },
182
- });
183
- ```
184
-
185
- This makes neutral tokens consume as `#surface`, `#border`, `#disabled-surface` (no theme namespace) while status colors live under `#danger-surface`, `#success-accent-text`, etc.
186
-
187
- This explicit map is usually clearer for a neutral `default` theme. The
188
- separate `primary` palette option below solves a different namespace problem:
189
- it duplicates one named theme without a prefix while retaining that theme's
190
- prefixed tokens.
191
-
192
- ### Alias themes for legacy names
193
-
194
- Two aliases for the same theme instance produce identical token values under different prefixes — useful when you want to support a legacy token name without duplicating definitions:
195
-
196
- ```ts
197
- const palette = glaze.palette({
198
- default: defaultTheme,
199
- primary: primaryTheme,
200
- purple: primaryTheme, // legacy alias — same theme, different prefix
201
- // ...
202
- });
203
-
204
- palette.tasty({
205
- prefix: {
206
- default: '',
207
- primary: 'primary-',
208
- purple: 'purple-', // emits #purple-surface alongside #primary-surface
209
- },
210
- });
211
- ```
212
-
213
- Both `#primary-surface` and `#purple-surface` resolve to the exact same color. Drop the alias when the legacy name is no longer referenced.
214
-
215
- ### `primary` (the unprefixed alias)
216
-
217
- `glaze.palette(themes, { primary })` and the per-export `primary` option duplicate one theme's tokens _without_ prefix on top of any prefix map. Equivalent to listing that theme twice with different prefixes; useful when the "primary" theme is conceptually distinct from `default` and you want both sets of unprefixed tokens.
218
-
219
- ```ts
220
- const palette = glaze.palette({ brand, accent }, { primary: 'brand' });
221
- palette.tokens();
222
- // → { light: { 'brand-surface': '...', 'surface': '...', 'accent-surface': '...' } }
223
- ```
224
-
225
- Override per call: `palette.tokens({ primary: 'accent' })`, or disable with `palette.tokens({ primary: false })`.
6
+ This page covers two kinds of move: bringing an existing CSS, design-token, or
7
+ application color system over to Glaze, and upgrading Glaze itself across a
8
+ major version. For designing a palette from scratch, see
9
+ [Designing a palette](./methodology.mdx); for choosing an export shape and
10
+ wiring it into your app, see [Exporting and integration](./exporting.mdx).
226
11
 
227
12
  ## Migrating from an existing color system
228
13
 
229
- The fastest path: **map first, design second**. Get every existing token name producing a Glaze-resolved color value (even if it's a rough first pass), wire the new palette in, then iterate on the resolved colors without touching component code.
14
+ The fastest path is **map first, design second**. Get every existing token name
15
+ producing a Glaze-resolved color (even a rough first pass), wire the new
16
+ palette in, then iterate on the resolved colors without touching component
17
+ code.
230
18
 
231
19
  ### 1. Inventory the existing tokens
232
20
 
233
- Walk your current color system and bucket every token into one of these categories:
21
+ Walk your current color system and put every token into one of these buckets:
234
22
 
235
- - **Surfaces** — backgrounds, panels, cards, banners.
236
- - **Surface text** — body text, headings, captions, labels.
237
- - **Borders / dividers / focus rings** — neutral structural lines.
238
- - **Brand fills** — solid CTA backgrounds, badges, status banners.
239
- - **Brand foregrounds** — link text, status text, icon colors on neutral backgrounds.
240
- - **Disabled** — disabled chip + label.
241
- - **Shadows / overlays** — elevation, scrims.
242
- - **One-off colors** — syntax highlighting, charts, illustrations.
23
+ - **Surfaces**: backgrounds, panels, cards, banners.
24
+ - **Surface text**: body text, headings, captions, labels.
25
+ - **Borders, dividers, focus rings**: neutral structural lines.
26
+ - **Brand fills**: solid CTA backgrounds, badges, status banners.
27
+ - **Brand foregrounds**: link text, status text, icon colors on neutral
28
+ backgrounds.
29
+ - **Disabled**: disabled chip and its label.
30
+ - **Shadows and overlays**: elevation and state layers (`shadow` / `glow`),
31
+ and fixed-alpha overlays such as scrims (`opacity`).
32
+ - **One-off colors**: syntax highlighting, charts, illustrations.
243
33
 
244
- Each bucket maps cleanly to one of the patterns in [methodology.md](methodology.md). The bucket determines the _shape_ of the Glaze definition (root vs dependent, `mode: 'auto'` vs `'fixed'`, contrast-floor vs absolute tone), not the value.
34
+ Each bucket maps onto one of the patterns in
35
+ [Designing a palette](./methodology.mdx). The bucket decides the _shape_ of the
36
+ Glaze definition (root or dependent, `mode: 'auto'` or `'fixed'`, a contrast
37
+ floor or an absolute tone), not its value.
245
38
 
246
39
  ### 2. Reproduce the existing values
247
40
 
248
- Pick a Glaze definition shape that lands the new color _close to_ the legacy hex in light mode. The methodology doc explains the shape per bucket; the API doc covers the levers (`tone`, `saturation`, `contrast`, `mode`, `hue`).
249
-
250
- Two tactics that make matching easier:
251
-
252
- - **Anchor strong text at the edge** instead of solving for `'AAA'`. The contrast solver stops at the floor (cr=7), which usually leaves text noticeably softer than a legacy hex like `#1a1a1a`. An absolute `tone: 2` (or wherever the legacy token sits) preserves the look.
41
+ Pick a definition shape that lands the new color _close to_ the legacy value in
42
+ light mode. [Designing a palette](./methodology.mdx) explains the shape per
43
+ bucket; the [Color definitions](./reference/color-definitions.md) reference
44
+ covers the levers (`tone`, `saturation`, `contrast`, `mode`, `hue`).
45
+
46
+ Three tactics make matching easier:
47
+
48
+ - **Anchor strong text at the edge** instead of solving for `'AAA'`. The
49
+ contrast solver stops at the floor, which usually leaves text noticeably
50
+ softer than the legacy value: on a white surface, a neutral
51
+ `contrast: 'AAA'` (7:1) solves to tone 36, while a legacy `#1a1a1a` sits at
52
+ tone 6.2 (17.4:1). An absolute `tone` preserves the look, but author it a
53
+ little below the measured value: the light
54
+ [tone window](./schemes.mdx#tone-windows) lifts authored tones (tone 0
55
+ renders at 4.95, tone 2 at 6.85), so a neutral `tone: 1.3` renders
56
+ `#1a1a1a`. With `lightTone: false` on the theme, light tones render exactly
57
+ as authored.
253
58
  - **Match the metric the old system actually used.** Numeric WCAG ratios are
254
- useful when reproducing a measured legacy ratio. For new content roles,
59
+ useful when you reproduce a measured legacy ratio. For new content roles,
255
60
  prefer the APCA presets from the methodology. For low-stakes spacing, use a
256
- tone delta instead of inventing a contrast target.
61
+ tone delta instead of inventing a contrast target. See
62
+ [Contrast solving](./contrast.mdx).
63
+ - **Pin values that are a contract with `from`.** A brand color you must honor
64
+ can be given as a literal: `{ from: '#2f5bff' }` reproduces that exact value
65
+ in the light scheme whatever the theme's seed saturation, while dark and high
66
+ contrast still adapt. Add `base` and `contrast` when it must stay legible
67
+ against something. See [`from`](./reference/color-definitions.md#from).
257
68
 
258
69
  ### 3. Keep the old token names
259
70
 
260
- Use a custom `prefix` map (and theme aliases if needed) so the names your components already consume keep working:
71
+ Use a custom `prefix` map (and [alias themes](./exporting.mdx#alias-themes-for-legacy-names)
72
+ if needed) so the names your components already consume keep working:
261
73
 
262
74
  ```ts
263
75
  // Old: components consume `#dark`, `#dark-02`, `#dark-03`.
@@ -280,34 +92,395 @@ defaultTheme.colors({
280
92
  });
281
93
 
282
94
  palette.tasty({ prefix: { default: '' } });
283
- // → '#dark', '#dark-02', '#dark-03' — components don't change.
95
+ // → '#dark', '#dark-02', '#dark-03': components don't change.
284
96
  ```
285
97
 
286
- Once consumers are off the legacy names, rename the Glaze tokens to match your conventions.
287
-
288
- ### 4. Verify dark mode and HC before promoting
289
-
290
- Glaze gives you light/dark/HC for free, but only the light mode is matched against the legacy palette. Before promoting the migration:
291
-
292
- - Spot-check every surface, text, accent, and disabled pair in dark mode. The tone inversion plus per-color `mode` choices may produce results that _look right_ in light but feel off in dark (typical fix: switch a brand color to `mode: 'fixed'`, or anchor a foreground to `surface` instead of the brand fill — see [methodology.md](methodology.md)).
293
- - If the legacy system had no high-contrast mode, audit the HC variants Glaze emits. Anywhere the resolved cr is too low or the color blows out, add an HC pair (`tone: ['-7', '-20']`, `contrast: [4.5, 7]`, etc.).
294
- - Run real screens, not just the token grid. The interaction of multiple Glaze tokens against each other (text on chip, hover bg vs. fill, disabled label on disabled chip) is where mismatches show up.
98
+ Once consumers are off the legacy names, rename the Glaze tokens to match your
99
+ conventions. [Prefix map strategies](./exporting.mdx#prefix-map-strategies)
100
+ shows every naming option with real output.
101
+
102
+ ### 4. Verify dark mode and high contrast before promoting
103
+
104
+ Glaze gives you light, dark, and high contrast for free, but only the light
105
+ scheme was matched against the legacy palette. Before you promote the
106
+ migration:
107
+
108
+ - Spot-check every surface, text, accent, and disabled pair in dark mode. The
109
+ tone inversion plus the per-color `mode` can produce results that look right
110
+ in light but feel off in dark. The usual fixes: switch a brand color to
111
+ `mode: 'fixed'`, or anchor a foreground to `surface` instead of the brand
112
+ fill (see [adaptation modes](./schemes.mdx#adaptation-modes) and
113
+ [Designing a palette](./methodology.mdx)).
114
+ - If the legacy system had no high-contrast mode, audit the high-contrast
115
+ variants Glaze emits. Wherever the resolved contrast is too low or a color
116
+ blows out, add a `[normal, highContrast]` pair (`tone: ['-7', '-20']`,
117
+ `contrast: [4.5, 7]`, and so on).
118
+ - Run real screens, not just the token grid. The interaction of several Glaze
119
+ tokens against each other (text on a chip, a hover background against its
120
+ fill, a disabled label on a disabled chip) is where mismatches show up.
295
121
 
296
122
  ### 5. Trim what `extend()` doesn't need
297
123
 
298
- After migration, mark every default-only token (borders, shadows, disabled chip, code highlighting, etc.) `inherit: false`. Colored sibling themes only need the accent + tinted-surface chain — flagging the rest cuts the emitted token set per theme dramatically.
124
+ After the migration, mark every default-only token (borders, shadows, glows,
125
+ the disabled chip, code highlighting, and so on) `inherit: false`. Colored
126
+ sibling themes created with `extend()` only need the accent and tinted-surface
127
+ chain; flagging the rest cuts the token set each theme emits dramatically.
299
128
 
300
129
  ## Upgrading Glaze
301
130
 
131
+ ### 3.0 — shadows and glows
132
+
133
+ `shadow` was rebuilt so `intensity` produces the same visible change on every
134
+ surface, and `glow` was added as its mirror. Both emit a translucent pigment
135
+ whose opacity is solved, per surface and per scheme, so the composite lands an
136
+ exact **tone step** away from `bg`: darker for `shadow`, lighter for `glow`.
137
+ See [Shadows and glows](./shadows-and-glows.mdx).
138
+
139
+ What changed:
140
+
141
+ - **`intensity` is a tone step.** In 2.x it went through a `tanh` curve in
142
+ OKHSL lightness, so the same number darkened white by 9 tones, a surface
143
+ rendered at tone 35 by 13.9, and one rendered at tone 6 by 4.4. Now
144
+ `intensity: 10` is 10 tones on every surface with 10 tones of room. A
145
+ step past black (or white, for a glow) is pinned to the strongest composite
146
+ the pigment reaches, and Glaze warns; see
147
+ [Unreachable steps](./shadows-and-glows.mdx#unreachable-steps).
148
+ - **`fg` is the object, not a contrast reference.** In 2.x, strength scaled
149
+ with `|l_bg − l_fg|` and the docs pointed `fg` at `text`, which ran opposite
150
+ to the physics: a white card on a light surface got almost no shadow. Now
151
+ `fg` is the element casting the shadow; a darker object deepens it, a lighter
152
+ one softens it, and an object at the surface's tone (or no `fg`) gives
153
+ exactly `intensity`.
154
+ - **The pigment is neutral by default**: pure black for `shadow`, pure white
155
+ for `glow`. The surface keeps its hue and the output is clean: `intensity: 10`
156
+ on a white surface emits `rgb(0 0 0 / 0.1326)`, which composites to tone 90.
157
+ - **The `ShadowTuning` fields were replaced.** `saturationFactor`,
158
+ `maxSaturation`, `lightnessFactor`, `lightnessBounds`, `minGapTarget`,
159
+ `alphaMax`, and `bgHueBlend` are gone. The new knobs are `objectInfluence`,
160
+ `tint`, `reach`, and `hueBleed`, shared by `shadowTuning` and the new
161
+ `glowTuning`. The type is now `LightingTuning`, and `ShadowTuning` is an
162
+ alias of it. The new `GlowTuning` is its own interface: `LightingTuning`
163
+ plus `headroom`, which a shadow's tuning rejects.
164
+ - **`GLAZE_EXPORT_VERSION` is 2.** A version-1 theme or palette snapshot that
165
+ contains shadow colors or a `shadowTuning` now throws on restore instead of
166
+ silently re-rendering every shadow.
167
+ - **`ColorDef` has a fourth member, `GlowColorDef`.** Exhaustive `switch`es
168
+ over `def.type` need a `'glow'` branch.
169
+
170
+ How 2.x shadows map onto 3.0 (2.x steps measured with browser compositing;
171
+ tones are where the surface renders, so under the default light window a
172
+ `tone: 10` surface, rendered at 14.5, stepped 10.0):
173
+
174
+ | 2.x definition | 2.x step for `intensity: 10` | 3.0 equivalent |
175
+ | --- | --- | --- |
176
+ | No `fg`, light surface (tone 97–100) | ≈ 9 tones | `intensity: 9`, or keep `10` (a tone stronger) |
177
+ | No `fg`, mid surface (tone 50) | ≈ 13.6 tones | `intensity: 13.6`, or settle on one value for all surfaces |
178
+ | No `fg`, dark surface (tone 10) | ≈ 7.6 tones, and saturating | `intensity: 7.6`; dark shadows were weaker than authored and now aren't |
179
+ | `fg: 'text'` on a light surface | ≈ 5.5–8 tones, by the text's tone (7.8 for near-black text, 5.6 for `'AAA'` text on a tone-95 surface) | Drop `fg` and use the step your text gave (`intensity: 8` for near-black text), or point `fg` at the element and re-tune |
180
+ | `fg` = a near-white card on light | ≈ 0.3 tones (barely visible) | `fg: 'card'`, then pick `intensity` for the look you want |
181
+
182
+ Then:
183
+
184
+ 1. Replace `fg: 'text'` (or any contrast-reference `fg`) with either no `fg` or
185
+ the element that casts the shadow.
186
+ 2. Pick `intensity` as the number of tones you want. For a ladder, `4` / `8` /
187
+ `12` reads as evenly spaced on every surface with room for the deepest step.
188
+ Check the dark schemes: a white `auto` page sits at tone 9.25 in dark and at
189
+ tone 0 in dark high contrast. Shadows on it deeper than about `8`–`9` (a
190
+ saturated page has a little less room) pin to black and warn in dark, and
191
+ every shadow on it stays transparent and warns in dark high contrast. Use
192
+ glows for dark elevation, on a surface that also leaves them room in light
193
+ ([Dark-mode elevation](./shadows-and-glows.mdx#dark-mode-elevation)). Leaving
194
+ a shadow token out of the dark schemes hides the pinned result but not the
195
+ warning: every color resolves in all four schemes.
196
+ 3. Delete the old fields inside `tuning` / `shadowTuning` (`saturationFactor`,
197
+ `alphaMax`, …); TypeScript flags them. A tinted pigment is
198
+ `tuning: { tint: 0.3, reach: 0.85 }`. There is no opacity cap: lower
199
+ `intensity` for a lighter overlay.
200
+ 4. Replace hand-tuned translucent white mixes (dark-mode elevation, edge
201
+ highlights, lightening state layers) with
202
+ `{ type: 'glow', bg, intensity }`. Direction is fixed per type, so a
203
+ `shadow` darkens and a `glow` lightens in light and dark alike. A glow on a
204
+ white light-mode surface has nothing to lighten and warns, unless it has
205
+ HDR headroom (below).
206
+ 5. Re-export snapshots. A 2.x snapshot with shadows or a `shadowTuning` throws
207
+ on restore, so re-author it. In code, rebuild the theme with 3.0 shadow defs
208
+ and call `export()`. In the JSON, fix the shadow defs, drop
209
+ `config.shadowTuning`, and set `version` to `2` (a palette snapshot has one at
210
+ the top and one per theme), then load it and export again. A snapshot
211
+ without shadows passes the version check and restores, but it resolves with
212
+ the other 3.0 changes on this page, and one that holds a value 3.0 rejects
213
+ throws, on its first output or (for a token's color value) when restored
214
+ (see [stricter input validation](#30--stricter-input-validation)).
215
+
216
+ `glaze.shadow()` keeps its signature with the new semantics, and `glaze.glow()`
217
+ takes the same input. See
218
+ [`glaze.shadow()` / `glaze.glow()`](./reference/utilities.md#glazeshadowinput--glazeglowinput).
219
+
220
+ HDR glows are new in 3.0 and opt-in, so they need no migration. A glow
221
+ variant gets an `hdr` value only with a `headroom` above 0
222
+ (`glowTuning.headroom`, or a glow's own `tuning.headroom`; the default is 0),
223
+ and exports carry it only with `modes.hdr` (default `false`), or for `css()`
224
+ its `hdr` option. At the defaults nothing changes. See [HDR glows](./hdr.mdx)
225
+ and [Output is opt-in](./hdr.mdx#output-is-opt-in).
226
+
227
+ ### 3.0 — even dark steps for tone deltas
228
+
229
+ In 2.x an `auto` tone delta's dark variant stepped from the base's light tone.
230
+ That tone carries the light window's lift, and on a contrast-solved base or a
231
+ mix it sits wherever light put it, so dark steps came out uneven: on a tone-50
232
+ base, `'+4'` stepped −5.6 in dark and `'-4'` +1.3, and in `glaze(280, 40)` a
233
+ `'-4'` hover on an `'AAA'` button stepped darker in dark as well (−5.5).
234
+
235
+ In 3.0 a dark delta steps from where its base sits on the authored scale: the
236
+ base's authored tone, or, for a `fixed` or `static` base, an `auto`
237
+ contrast-solved base, a mix, or an `auto` `'max'` / `'min'` on a base, the
238
+ base's own dark tone read back through the dark window. A dark delta then
239
+ lands where the same authored tone would, on every base, along chains of
240
+ deltas, and for `from` colors. At the default dark window that is about 0.87×
241
+ the delta: `'+4'` steps −3.5, `'-4'` +3.5, and the `'AAA'` hover above +3.5.
242
+ Under a custom dark `eps` the steps follow that window's curve instead of a
243
+ fixed scale. On a `fixed` or `static` base the `auto` dependent still inverts
244
+ on its own, mirrored around wherever the base sits. See
245
+ [How relative tone adapts](./schemes.mdx#how-relative-tone-adapts).
246
+
247
+ What moves:
248
+
249
+ - **Dark variants** of `auto` tone-delta colors, and anything solved, mixed,
250
+ or lit from them in dark: at the default light window, by up to about
251
+ 4 tones on the lowest bases, about 2 on a tone-50 base, and a fraction of a
252
+ tone on a light surface. Deltas on a contrast-solved, mixed, or extreme base
253
+ move more (the `'AAA'` hover above moves 9 tones), and so do deltas on a
254
+ base past the dark window or under a custom light window: the move grows
255
+ with how far the window lifts the base, so `lightTone: [15, 100]` moves a
256
+ tone-50 base's deltas about 4 tones and up to 8 at the bottom, and a `hi`
257
+ below 100 moves a white surface's about 0.7 tones per tone it drops (about
258
+ 3.5 at `hi` 95). A custom light `eps` moves them too. A contrast-solved delta whose 2.x dark step
259
+ crossed its base now lands on the side that mirrors light, which can move
260
+ it most of the scale: in `glaze(0, 0)`,
261
+ `{ base: 'fill', tone: '-1', contrast: 3 }` on a tone-50 fill goes from
262
+ 16.2 to 89.0 in dark.
263
+ - **`auto` deltas on a `fixed` or `static` base**, in dark and dark high
264
+ contrast. 2.x reflected them across the whole scale, so how far one landed
265
+ from its base depended on the base's tone. They now mirror around the base:
266
+ in `glaze(280, 30)`, an `auto` `'+30'` label on a `fixed` tone-52 fill goes
267
+ from 31.5 to 26.0 tones below it in dark (34 to 30 in dark high contrast),
268
+ and a `'-4'` hover on `{ base: 'surface', tone: '-10', mode: 'fixed' }`
269
+ over a tone-97 surface goes from 22.0 to 3.5 tones above it.
270
+ - **High-contrast variants**, only for deltas on contrast-solved, mixed,
271
+ extreme, `fixed`, or `static` bases, or under a custom light `eps`: at the
272
+ default dark `eps`, a high-contrast dark step is now exactly the delta,
273
+ short of the limits below. `contrastLevel: 100` still equals the
274
+ high-contrast tier.
275
+ - **`glaze.color()` links** whose base token has its own `darkTone`, is a
276
+ value-form token with its own `lightTone`, or is contrast-solved or an
277
+ `auto` `'max'` / `'min'` on a base. They now step like any other link in
278
+ dark, and a `'+0'` link lands on its base there: in 2.x a `'+0'` link on an
279
+ `'AAA'` token over `#f7f7fb` sat 8.95 tones below it in dark. Links on
280
+ other `auto` bases already stepped this way; links on a `fixed` or `static`
281
+ base move as described for those bases above.
282
+ - **Other light variants** only through a float-noise tie: a contrast-solved
283
+ color whose preferred tone lands within 1e-9 tones of its base's (a `'+0'`
284
+ / `'-0'` delta, or an absolute tone equal to the base's) takes the solver's
285
+ own tie rule instead of a side picked by float noise, in every scheme, so
286
+ it can land on the other side of its base. Under a manual `contrastLevel`,
287
+ a level probe that lands on its base pins no side either.
288
+
289
+ Two limits apply. The flip is still decided on the light tone, so a step
290
+ that fits in light can run past 0 or 100 from the anchor and stop at the dark
291
+ window's edge. On an `auto` base placed by a tone that happens at the bottom,
292
+ where the light window lifts the light tone: `'-4'` on tone 2 steps about
293
+ +1.7 in dark, and on tone 0 it doesn't move. On a base anchored at its own
294
+ dark tone it can happen at either end, in high contrast too: in
295
+ `glaze(280, 40)`, a `'-4'` hover on `contrast: 12` text over a tone-97
296
+ surface steps +2.3 in dark, not +3.5. A base sitting past the dark window
297
+ (pushed there by a contrast floor or an extreme, or placed there as a
298
+ `fixed` or `static` color) is stepped from where it sits: a step back toward
299
+ the window moves the full scaled delta, and one pointing further out stops
300
+ at the base.
301
+ Re-check dark values you have pinned in tests or snapshots.
302
+
303
+ ### 3.0 — other changes to resolved colors
304
+
305
+ These fixes move colors that 2.x resolved from the same input. Re-check values
306
+ you have pinned:
307
+
308
+ - **Contrast-solved colors.** The solver keeps its 1% (WCAG) / 0.5 Lc (APCA)
309
+ margin and lands on the side it reports. Saturated hues that read darker
310
+ than their gray tone (blues, violets, reds, oranges) land on their own
311
+ crossing instead of stopping at the gray seed: an `'AA'` text at hue 280 and
312
+ saturation 100 on a light gray surface goes from 5.03:1 to 4.55:1, 2.8 tones
313
+ nearer its authored tone. A contrast-solved color with no tone of its own
314
+ on a mid-tone base, where both sides meet the floor at the same distance,
315
+ now keeps the side it searches first instead of a side picked by search
316
+ noise. It starts toward the extreme that scores higher, and darker on a
317
+ gray base at tone 50, so it can land on the other side of its base: in
318
+ `glaze(0, 0)`, `{ base: 'b', contrast: 3 }` goes from 11.3 to 84.1 on a
319
+ tone-45 base and from 93.6 to 20.8 on a tone-55 one. Apart from those side
320
+ changes, most colors move less than half a tone, a few saturated ones up to
321
+ about 5.
322
+ - **Standalone `glaze.color()` tokens** default to the `'text'` role; 2.x
323
+ resolved a token without a `base` as a surface. `name` no longer drives role
324
+ inference. An APCA floor on a token without `role` can land elsewhere:
325
+ `glaze.color({ from: '#ffffff', contrast: { apca: 60 } })` now gives
326
+ `rgb(141.23 141.23 141.23)`, not `rgb(151.87 151.87 151.87)`. Set
327
+ `role: 'surface'` to keep a fill's 2.x polarity. WCAG floors don't change.
328
+ A relative high-contrast tone on a token (`tone: [50, '+20']`) is measured
329
+ from the token's own color; 2.x read `'+20'` as tone 20.
330
+ - **Value-form `glaze.color()` links on a structured base**
331
+ (`glaze.color({ from, base })` with `base` made by
332
+ `glaze.color({ hue, saturation, tone })`), in light, and in dark where the
333
+ painted light tone changes which way `autoFlip` sends a step. 2.x read such
334
+ a base at its raw input tone in light, not at the tone it renders, so the
335
+ link measured against a color that is never painted: a `'+0'` link on
336
+ `glaze.color({ hue: 280, saturation: 80, tone: 50 })` sat 2.5 tones below
337
+ it, and an `'AA'` link measured 4.16:1 against it, with no warning. A
338
+ `'+9.8'` link on a tone-90 structured base, which fit under 100 at the raw
339
+ tone, now flips in light, and so lands elsewhere in dark too. Links now
340
+ resolve against what the base paints in every scheme.
341
+ - **Transparent mixes with `contrast`** solve against browser compositing
342
+ (`source-over` on gamma-encoded sRGB) instead of linear light. A white
343
+ `'AA'` overlay on a surface rendered at tone 12
344
+ (`{ tone: 12, mode: 'static' }`) goes from alpha 0.26, which rendered at
345
+ 2.3:1, to 0.48; a black overlay with a 1.2:1 floor on a tone-97 surface goes
346
+ from 0.18 to 0.09.
347
+ - **`glaze(seed, config)`** applies the config. 2.x read the object form's
348
+ override only from a third argument, so the documented two-argument call
349
+ dropped it. `glaze(seed, undefined, config)` still works but is deprecated.
350
+
351
+ ### 3.0 — stricter input validation
352
+
353
+ 3.0 throws a `glaze:` error for input that 2.x resolved, usually into wrong or
354
+ `NaN` colors:
355
+
356
+ | Input | 2.x |
357
+ | --- | --- |
358
+ | A tone window with `lo > hi` (`lightTone: [90, 20]`) | Inverted the scale |
359
+ | A window endpoint that isn't a finite number (`NaN`, `Infinity`, `'10'`) | `NaN` colors, or a string read as a number |
360
+ | An object window whose `eps` is missing, not above 0, or above `1e6` | The default `eps` when missing; `NaN` colors at 0 or below and from about `1e15` |
361
+ | A window that isn't `[lo, hi]`, `{ lo, hi, eps }`, or `false` (`50`, `[10]`, `true`, `null`) | `NaN` colors; `configure()` ignored `null`, and a theme or token override with `null` threw a `TypeError` when it resolved |
362
+ | A `darkDesaturation` that isn't a finite number (`NaN`, `Infinity`, `'0.5'`, `true`) | `NaN` dark colors (`NaN`), fully gray ones (`Infinity`, `true`), or a string read as a number |
363
+ | A relative tone in the high-contrast entry of a root color (`tone: [50, '+20']`, no `base`) | Read `'+20'` as tone 20. Now throws: add a `base`, or write the tone you got (`[50, 20]`) |
364
+ | A hex color with a non-hex digit (`'#1z2z3z'`) in `glaze.color()`, `glaze.fromHex()`, a color's `from`, or the `bg` / `fg` of `glaze.shadow()` | Read only the first digit of each pair (`#010203`) |
365
+ | A color-function component that isn't a number (`'hsl(abc 50% 50%)'`, `'rgb(Infinity 0 0)'`) | `NaN` colors, or a silent gray |
366
+ | A value object with a negative `s`, `l`, `t`, or `c` (`{ l: 0.5, c: -0.05, h: 30 }`) | A plausible color: a negative chroma turned the hue around |
367
+ | A malformed `contrast`: a misspelled preset (`'aa'`, `{ apca: 'Body' }`), `NaN`, `{}`, both `wcag` and `apca`, or a pair without two entries | A `TypeError` from the warning code, or a color pinned to black or white; both keys used APCA, and a longer pair its first two entries |
368
+ | A color field that isn't a finite number (`tone: NaN`, `saturation: NaN`), a relative string that isn't `'+N'` / `'-N'` (`hue: 'abc'`, `tone: '+-5'`, `tone: '+5px'`), an `opacity` outside 0–1, or a non-finite mix `value` or shadow `intensity` | `NaN` colors, or an alpha outside 0–1; a trailing unit was read past (`'+5px'` as `'+5'`) |
369
+ | A theme seed whose hue or saturation isn't a finite number (`glaze({ hue: 280 })` without `saturation`, `glaze.fromRgb(NaN, 0, 0)`) | `NaN` colors |
370
+ | `null` for `contrastLevel` or `darkDesaturation` | Ignored: the current value stayed |
371
+
372
+ ```text
373
+ glaze: lightTone lo (90) is above hi (20).
374
+ A window runs from its dark end to its light end: [lo, hi] with lo ≤ hi,
375
+ e.g. [20, 90].
376
+
377
+ glaze: color "a" has relative "tone" without "base".
378
+
379
+ glaze: invalid hex color "#1z2z3z".
380
+
381
+ glaze: color "text" has an invalid "contrast" "aa". Use a WCAG ratio or
382
+ 'AA' / 'AAA' / 'AA-large' / 'AAA-large', { wcag: … }, { apca: … }, or a
383
+ [normal, highContrast] pair of these.
384
+ ```
385
+
386
+ `configure()` throws at the call and leaves the config as it was. A theme or
387
+ token override (`glaze(hue, saturation, config)`, `glaze.color(value, config)`,
388
+ `extend({ config })`) is checked when it resolves, so it throws on the first
389
+ output or `resolve()`, and so are a theme's colors. A theme or palette
390
+ snapshot that holds such a value still restores with `glaze.themeFrom()` /
391
+ `paletteFrom()` and throws on its first output, and so does a token snapshot
392
+ whose frozen config holds one. A token snapshot whose color value (its input,
393
+ or a raw `base`) 3.0 rejects throws in `glaze.colorFrom()` itself, as
394
+ `glaze.color()` does, and so does a theme snapshot whose seed does. Fix the
395
+ value in the JSON and export again.
396
+
397
+ The exported `parseHex()` / `parseHexAlpha()` return `null` for such hex, as
398
+ their type always allowed; 2.x read the first digit of each pair, and
399
+ `'#-1-2-3'` gave negative channels. Check for `null` before using the result.
400
+
401
+ Color strings also change in two ways that move colors instead of throwing. A
402
+ hue in `turn`, `rad`, or `grad` is converted (2.x read `hsl(0.5turn 50% 50%)`
403
+ as hue 0.5°, not 180°), and the CSS keyword `none` reads as 0. Out-of-range
404
+ color-function values clamp as CSS clamps them, which moves an `rgb()` channel
405
+ outside 0–255, an `okhsl()` / `okhst()` saturation outside 0–100%, a negative
406
+ `okhsl()` lightness (2.x gave `NaN` or a wrong tone), a negative `oklch()`
407
+ chroma (2.x turned its hue around), and an `oklch()` lightness below about
408
+ −0.37 (2.x emitted `NaN`). A saturation past 100% moves a token in dark, and
409
+ one outside 0–100% moves a theme color's `from` in every scheme: 2.x painted
410
+ its light variant out of gamut, which could turn the hue
411
+ (`'okhsl(200 -20% 50%)'` came out reddish), and colors solved, mixed, or lit
412
+ from it move with it. An `hsl()` or
413
+ `oklch()` lightness past its range already rendered clamped. A value object's
414
+ 0–1 field between 1 and 1.5 clamps to 1 too, which moves a saturation there
415
+ the same way.
416
+
417
+ Two out-of-range values clamp instead, which can move output: window endpoints
418
+ clamp to 0–100 (2.x extended a `[-10, 120]` window past the range), and
419
+ `darkDesaturation` clamps to 0–1 (2.x let `-0.5` raise dark saturation). See
420
+ [Validation](./reference/configuration.md#validation).
421
+
422
+ ### 3.0 — output options
423
+
424
+ Options an output method accepted but never read are gone from its type, so
425
+ TypeScript flags them. Delete them; the output doesn't change.
426
+
427
+ - `palette.json()` takes `GlazeJsonOptions` only: no `prefix`.
428
+ - `splitHue` is gone from `palette.tokens()`, `dtcg()`, `dtcgResolver()`, and
429
+ `tailwind()` (it left `GlazePaletteExportOptions`), and from `token.token()` /
430
+ `token.tasty()`.
431
+ - `theme.tasty()` takes the new `GlazeTastyOptions`, without `prefix`.
432
+ `GlazeTokenOptions` is now just `format`, `modes`, and `states`, as taken by
433
+ `token.token()` / `token.tasty()`.
434
+ - `palette.tasty()` and `palette.css()` take the new
435
+ `GlazePaletteTastyOptions` / `GlazePaletteCssOptions`, without `name`: their
436
+ hue vars are always named after each theme.
437
+
438
+ `splitHue` also stops failing silently. With a `format` other than `'oklch'`
439
+ it now throws instead of emitting inline colors, and a color that sets its own
440
+ hue under the theme hue var's name (a `brand` color with `name: 'brand'`, or a
441
+ color named after another theme in an unprefixed palette) throws instead of
442
+ rendering at the theme's hue, unless it is achromatic in light. In `css()`,
443
+ a color's value property that lands on a hue var (`suffix: '-hue'`, or
444
+ `suffix: ''` with a color named `*-hue`) throws the same way instead of
445
+ emitting a `var()` cycle. Hue values in its
446
+ declarations are rounded to 2 decimals, so a `from` color emits
447
+ `--logo-hue: 266.16` rather than `266.15797905637027`, and a color achromatic
448
+ in light no longer emits a dead `--*-hue` declaration in dark. See
449
+ [`splitHue`](./reference/output.md#hue-channel-splitting-splithue).
450
+
451
+ ### 3.0 — contrast solver utilities
452
+
453
+ For code that calls the solver directly:
454
+
455
+ - `findValueForMixContrast()` no longer takes `targetLinearRgb`. It was
456
+ required in 2.x but never read: `luminanceAtValue` describes the mix. Delete
457
+ it from the options object; results don't change. TypeScript flags it as an
458
+ excess property and suggests `baseLinearRgb`, but the fix is to delete it,
459
+ not rename it.
460
+ - `findToneForContrast()` returns `branch: 'preferred'`, met, for a preferred
461
+ tone that clears the floor but not the 1% / 0.5 Lc margin whenever the
462
+ search can't improve on it: no searched side meets the floor, or the
463
+ starting side's only position is the preferred tone (it sits on that side's
464
+ bound). 2.x labelled it with the side it had searched, which could name a
465
+ side of the base it doesn't sit on. A WCAG search that started away from it
466
+ lost it to the far extreme: white at 7.24:1 against a tone-35 gray with a
467
+ 7.2:1 floor came back as black at 2.9:1, unmet. With no room to search
468
+ (`toneRange`), it came back `met: false`.
469
+ - Results move with the solver fixes above. The WCAG gray seed is only the
470
+ first probe, so the [`findToneForContrast`
471
+ example](./reference/utilities.md#findtoneforcontrastoptions) now returns
472
+ tone 0.4846 at 4.55:1, not 0.4759 at 4.67:1. With no `initialDirection`, a
473
+ tie between the two ends' scores starts darker.
474
+
302
475
  ### 2.0 — `format*` takes 0–1
303
476
 
304
477
  The `format*` writers (`formatOkhsl`, `formatOkhst`, `formatRgb`, `formatHsl`,
305
- `formatOklch`) used to take `s` / `l` / `t` on a 0–100 percentage scale while
306
- everything that *produces* those values — `resolve()`, `variantToOkhsl`,
307
- `srgbToOkhsl`, `oklabToOkhsl`, `okhslToSrgb` — returns them on 0–1. Composing
308
- the two, which is the obvious thing to do, was off by 100x and failed silently:
309
- `0.7` is a legal percentage, so you got a valid CSS string naming the wrong
310
- (near-black) color.
478
+ `formatOklch`) used to take `s` / `l` / `t` on a 0–100 percentage scale, while
479
+ everything that _produces_ those values (`resolve()`, `variantToOkhsl`,
480
+ `srgbToOkhsl`, `oklabToOkhsl`, `okhslToSrgb`) returns them on 0–1. Composing
481
+ the two, which is the obvious thing to do, was off by 100x and failed
482
+ silently: `0.7` is a legal percentage, so you got a valid CSS string naming the
483
+ wrong (near-black) color.
311
484
 
312
485
  The writers now take the 0–1 factors, so the whole library speaks one scale.
313
486
  Drop the `* 100`:
@@ -321,31 +494,49 @@ Drop the `* 100`:
321
494
  ```
322
495
 
323
496
  Nothing else moves: `h` was always 0–360, `alpha` was always 0–1, and every
324
- export method (`css()` / `tokens()` / `json()` / `tasty()` / `dtcg()` /
325
- `tailwind()` / `glaze.format()`) emits the same colors — they were compensating
326
- internally. Dropping the redundant `×100 ÷100` round-trip shifts float noise by
327
- an ULP, which shows up nowhere except the (meaningless) hue term of a
328
- fully-desaturated `hsl()` string.
329
-
330
- A leftover `* 100` is not silent any more: a value above 1 cannot be a factor,
331
- so the writers `console.warn` once per writer and you get an obviously-broken
332
- `7040.68%` instead of a plausible wrong color.
497
+ export method (`css()`, `tokens()`, `json()`, `tasty()`, `dtcg()`,
498
+ `tailwind()`, `glaze.format()`) emits the same colors, because they were
499
+ compensating internally. Dropping the redundant `×100 ÷100` round-trip shifts
500
+ float noise by an ULP, which shows up nowhere except the (meaningless) hue term
501
+ of a fully desaturated `hsl()` string. `toTone` / `fromTone` still speak the
502
+ 0–100 tone axis the authoring API takes, so divide a tone from them by 100
503
+ before passing it to `formatOkhst`.
504
+
505
+ A leftover `* 100` is no longer silent: a value above 1 cannot be a factor, so
506
+ the writers `console.warn` once per writer, and you get an obviously broken
507
+ string such as `okhsl(280 7040.68% 5000%)` instead of a plausible wrong color.
508
+ See [format writers](./reference/utilities.md#format-writers).
509
+
510
+ ### 0.14 — `lightness` became `tone`
511
+
512
+ Colors used to be authored on OKHSL lightness. They are now authored on
513
+ **tone**, a contrast-uniform 0–100 axis (see [Tone and OKHST](./okhst.mdx)):
514
+
515
+ - The `lightness` prop is gone everywhere (theme colors, `glaze.color()`
516
+ structured input, tone deltas); use `tone`. Equal tone steps give equal
517
+ WCAG contrast, so numeric values don't map to the same OKHSL lightness as
518
+ before. Re-check absolute mid-range values.
519
+ - The config windows `lightLightness` / `darkLightness` became `lightTone` /
520
+ `darkTone`, and `darkCurve` was removed.
521
+ - `ResolvedColorVariant` stores `{ h, s, t, alpha }` instead of `{ h, s, l }`.
522
+ Use `variantToOkhsl()` to recover OKHSL lightness.
523
+ - Tone deltas that overshoot `[0, 100]` mirror to the other side of the base
524
+ by default instead of clamping. Set `autoFlip: false` on the color
525
+ (or globally) to clamp.
333
526
 
334
527
  ## Common pitfalls
335
528
 
336
- | Symptom | Cause | Fix |
337
- | ------------------------------------------------------------------------------ | --------------------------------------------------------------------------------------------------------------------------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
338
- | Disabled state stops looking disabled in dark mode. | Alpha-tinted overlay on `surface-text` (which inverts), giving asymmetric perceived contrast. | Replace it with an adaptive color anchored to `surface`; use a tone delta for visual spacing or a contrast floor when required. See [Chips and disabled states](methodology.md#chips-and-disabled-states). |
339
- | Brand color flips to its complement in dark mode. | Default `mode: 'auto'` inverts the tone. | Set `mode: 'fixed'` so the tone is remapped (not inverted). |
340
- | Brand text washes out against the dark surface. | Foreground was anchored to `accent-surface` (the brand fill), so contrast was only enforced against that fill — not the actual surface. | Anchor `accent-text` etc. to `surface` with `mode: 'auto'`. |
341
- | Tokens look right in light, broken in HC. | The HC pass bypasses the tone window — solver runs over the full `[0, 100]` range. | Add explicit `[normal, hc]` pairs to `tone` / `contrast` for the affected tokens. |
342
- | A relative `tone` like `'+48'` lands on the _wrong_ (darker) side of its base. | Overshooting offsets now mirror to the other side of the base by default (`autoFlip` inherits `autoFlip`). | Set `autoFlip: false` on the color to clamp to the boundary instead, or use `tone: 'max'`/`'min'` to force the extreme. |
343
- | `palette.tokens()` emits unexpected unprefixed names. | A `primary` was set on the palette (or per-call) and is duplicating the theme's tokens without prefix. | Pass `primary: false` to disable for that export, or rename `glaze.palette(themes, { primary })`. |
344
- | `console.warn: token "foo" collides with theme "bar"`. | Two themes resolved to the same output key under your prefix config. | Adjust the prefix map so each token is unique, or accept the first-write-wins behavior. |
345
- | `console.warn: formatOkhsl() got a value above 1`. | Pre-2.0 percentage-scale input to a `format*` writer (a leftover `* 100`). | Pass the 0–1 factors the converters return — see [2.0 — `format*` takes 0–1](#20--format-takes-01). |
346
- | `console.warn: color "X" cannot meet contrast`. | The requested contrast target is physically unreachable for the color's hue/saturation against its base. | Lower the floor, change the base, or accept the closest passing variant. Use the `name` override on standalone colors to make the warning identifiable. |
347
-
348
- ## See also
349
-
350
- - [methodology.md](methodology.md) — how to design the palette in the first place.
351
- - [api.md](api.md) — full reference for every option mentioned here.
529
+ | Symptom | Cause | Fix |
530
+ | --- | --- | --- |
531
+ | A disabled state stops looking disabled in dark mode. | An alpha-tinted overlay on `surface-text` (which inverts) gives asymmetric perceived contrast. | Replace it with an adaptive color anchored to `surface`: a tone delta for visual spacing, or a contrast floor when one is required. See [chips and disabled states](./methodology.mdx#chips-and-disabled-states). |
532
+ | A brand fill turns dark in dark mode and stops reading as the brand. | The default `mode: 'auto'` inverts the tone. | Set `mode: 'fixed'` so the tone is remapped into the dark window, not inverted. See [adaptation modes](./schemes.mdx#adaptation-modes). |
533
+ | Brand text washes out against the dark surface. | The foreground was anchored to `accent-surface` (the brand fill), so contrast was enforced only against that fill, not against the surface it sits on. | Anchor `accent-text` and friends to `surface` with `mode: 'auto'`. |
534
+ | Tokens look right in light but broken in high contrast. | High contrast bypasses the tone window, so absolute tones and deltas land on the full `[0, 100]` range (a white `auto` page is black in dark high contrast), and floors without an explicit high-contrast value rise a tier (`'AA'` → `'AAA'`, APCA +15 Lc). | Add explicit `[normal, highContrast]` pairs to `tone` / `contrast` for the affected tokens. |
535
+ | A tone delta like `'+48'` lands on the _wrong_ (darker) side of its base. | Overshooting tone deltas mirror to the other side of the base by default (a color's `autoFlip` defaults to the global `autoFlip`, which is on). | Set `autoFlip: false` on the color to clamp to the boundary instead, or use `tone: 'max'` / `'min'` to force the extreme. |
536
+ | `palette.tokens()` emits unexpected unprefixed names. | A `primary` was set on the palette (or per call) and duplicates that theme's tokens without a prefix. | Pass `primary: false` to disable it for that export, or change `glaze.palette(themes, { primary })`. See [the `primary` alias](./exporting.mdx#the-primary-alias). |
537
+ | `glaze: token "surface" from theme "danger" collides with theme "default" — skipping.` | Two themes resolved to the same output name under your prefix config. | Adjust the prefix map so each name is unique, or accept first-write-wins. See [Collisions](./exporting.mdx#collisions). |
538
+ | `glaze: formatOkhsl() got a value above 1 …` | Pre-2.0 percentage-scale input to a `format*` writer (a leftover `* 100`). | Pass the 0–1 factors the converters return; see [2.0 — `format*` takes 0–1](#20--format-takes-01). |
539
+ | `glaze: color "X" cannot meet WCAG … in light scheme (got …)` | The contrast target is physically unreachable for the color's hue and saturation against its base. The color pins to the extreme of the side it searched first, which may not be the closer one; see [When a floor can't be met](./contrast.mdx#when-a-floor-cant-be-met). | Lower the floor or change the base. If only the high-contrast schemes fail, give `contrast` an explicit, reachable high-contrast entry (`['AA', 'AA']`). On standalone colors, set `name` so the warning is identifiable; see [`name`](./reference/color-tokens.md#name). |
540
+ | `glaze: mix "X" cannot meet WCAG … against its base in light scheme (got …)` | Even the pure target doesn't reach the floor against the mix's `base`. The mix pins to the pure target, or to the pure base when its `value` is already 100; see [Contrast on mixes](./mixes.mdx#contrast-on-mixes). | Mix toward a target further from the base, or lower the floor. If only the high-contrast schemes fail, give `contrast` an explicit, reachable high-contrast entry. Keep `value` below 100 when you set a floor. |
541
+ | `glaze: shadow "X" cannot reach a 10.0-tone step …` (or `glow`) | The surface has fewer tones of room than `intensity` asks for: a glow on white, a deep shadow on near-black, or any shadow on a light `auto` page in dark high contrast (at or near tone 0 there). The overlay is pinned to its strongest composite (transparent when the surface is already at the pigment's extreme). | Lower `intensity` or give the overlay a surface with room in every scheme, and use glows for dark elevation. Otherwise expect the warning: every color resolves in all four schemes, so not using the token in a scheme doesn't silence it. A glow with `headroom` gets an HDR value past white instead and warns only when the cap leaves the step short (`… even with 1 stop of headroom (reached …)`): lower `intensity` or raise `headroom`. See [Unreachable steps](./shadows-and-glows.mdx#unreachable-steps) and [Headroom](./hdr.mdx#headroom). |
542
+ | `glaze.themeFrom: this snapshot is export version 1 (Glaze 1.1–2) …` | A 2.x snapshot with shadow colors or `shadowTuning`; shadows changed meaning in 3.0. | Re-author the shadows and export again; see [3.0 — shadows and glows](#30--shadows-and-glows). |