@tenphi/glaze 0.15.1 → 0.16.1

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.
@@ -1,346 +1,561 @@
1
1
  # Methodology
2
2
 
3
- A practical guide for designing a real, production-grade Glaze palette. Recipe-ordered: the sections follow the same sequence you'd actually build a palette in, and each one ties the choice back to a Glaze behavior.
3
+ A practical way to design a Glaze palette without fighting dark mode. Start from
4
+ tone relationships, add contrast only where a role needs a readable floor, and
5
+ let `extend()` carry the same decisions across status hues.
4
6
 
5
- ## The mental model
6
-
7
- A Glaze palette is a **default neutral theme** plus a small fan of **colored sibling themes** (`success`, `danger`, `warning`, `note`, …) created via `extend()`. Most colors live in the default theme as neutrals; brand-tinted colors come from `extend()` swapping the hue.
8
-
9
- The default theme is what most components consume — its tokens are emitted unprefixed (`#surface`, `#border`). Colored themes are scoped to status surfaces and accent variants, emitted with a theme-name prefix (`#success-surface`, `#danger-accent-surface`).
10
-
11
- You design the default theme once, and `extend()` propagates that design across every status hue.
12
-
13
- Every color definition has an **`inherit`** flag (default: `true`) controlling whether it flows into child themes via `extend()`. Set `inherit: false` to scope a color to its parent theme only — this is how sibling themes stay lean, carrying only the tokens they actually need.
7
+ For the full API surface, see [api.md](api.md). For the color model details, see
8
+ [okhst.md](okhst.md).
14
9
 
15
- ## Hue / saturation seeds
10
+ ## The mental model
16
11
 
17
- Declare hues as named constants up top, plus a single shared seed saturation:
12
+ Glaze palettes work best as one default neutral theme plus a few colored sibling
13
+ themes:
14
+
15
+ - `default` owns the neutral system most components consume: `#surface`,
16
+ `#surface-text`, `#border`, `#disabled-surface`, and so on.
17
+ - Status themes (`success`, `danger`, `warning`, `note`, ...) are created with
18
+ `extend()`. They swap the hue and keep only the inherited tokens that should
19
+ become status-aware.
20
+ - `inherit: false` keeps a token local to the parent theme. Use it for neutral
21
+ ladders, shadows, code colors, overlays, and anything that should not be
22
+ repeated for every status hue.
23
+
24
+ The main simplification is OKHST tone. Dark mode is now a single inversion
25
+ (`100 - t`) plus a scheme window, so relative tone shifts stay consistent between
26
+ light and dark. A token that is `-4` tone from its base is the same kind of step
27
+ in both schemes.
28
+
29
+ ## Authoring Rules
30
+
31
+ Use this order when defining a token:
32
+
33
+ 1. Pick the base it visually belongs to.
34
+ 2. Use `tone` for visual distance: surface ladders, soft chips, disabled states,
35
+ hover ramps, and other low-stakes relationships.
36
+ 3. Add `contrast` only when readability or recognizability needs a measured
37
+ floor.
38
+ 4. Use APCA presets for content-like colors:
39
+ `contrast: { apca: 'content' }` or
40
+ `contrast: { apca: ['content', 'body'] }`.
41
+ 5. Use WCAG numbers or presets when you specifically need a ratio target:
42
+ `contrast: 4.5`, `contrast: 'AAA'`, or `contrast: { wcag: [4.5, 7] }`.
43
+ 6. Let token names infer APCA roles. Names ending in `text`, `label`, `border`,
44
+ `surface`, `fill`, `bg`, and similar aliases already tell Glaze which side is
45
+ foreground or background. Set `role` only when a name is ambiguous.
46
+ 7. Add high-contrast pairs only where HC should intentionally tighten:
47
+ text/content contrast, border tone, shadow intensity, mix value, or similar.
48
+
49
+ `mode: 'auto'` is the default and should be the default choice. Use
50
+ `mode: 'fixed'` for brand fills or inverse surfaces that must stay recognizable
51
+ instead of tone-inverting. Use `mode: 'static'` only when the exact authored tone
52
+ must render in every scheme.
53
+
54
+ ## Seed and Configure
55
+
56
+ Keep hue decisions named and configure output modes once:
18
57
 
19
58
  ```ts
20
- const PURPLE_HUE = 280.3;
59
+ const PURPLE_HUE = 280.3;
21
60
  const SUCCESS_HUE = 156.9;
22
- const DANGER_HUE = 23.1;
61
+ const DANGER_HUE = 23.1;
23
62
  const WARNING_HUE = 84.3;
24
- const NOTE_HUE = 302.3;
63
+ const NOTE_HUE = 302.3;
25
64
 
26
65
  const SEED_SATURATION = 80;
27
- ```
28
-
29
- Hues are design tokens too — keeping them named in one place beats burying numbers in `extend()` calls. The shared `SEED_SATURATION` keeps every status theme on the same saturation budget; per-color `saturation` factors below are 0–1 *of this seed*, not absolute.
30
-
31
- ## Global `glaze.configure()`
32
66
 
33
- Configure state aliases and output modes once at module load:
34
-
35
- ```ts
36
67
  glaze.configure({
37
68
  states: { dark: '@dark', highContrast: '@hc' },
38
- modes: { dark: true, highContrast: true },
69
+ modes: { dark: true, highContrast: true },
39
70
  });
40
71
  ```
41
72
 
42
- Match the state alias names to whatever your app wires into the global predefined states (`@dark` / `@hc` is what Tasty expects). Setting `modes.highContrast: true` makes every export emit four variants — HC tokens are then available globally without per-call overrides.
73
+ Per-color `saturation` is a factor of the theme seed, not an absolute
74
+ saturation. With `SEED_SATURATION = 80`, `saturation: 0.25` means one quarter of
75
+ that seed.
43
76
 
44
- ## Naming conventions
77
+ ## Naming
45
78
 
46
- A tight, predictable vocabulary that the rest of the doc relies on:
79
+ Prefer purpose first and variant last:
47
80
 
48
- | Pattern | Tokens |
49
- |---|---|
50
- | Surface ladder | `surface`, `surface-2`, `surface-3` |
51
- | Text on surface (decreasing prominence) | `<surface>-text`, `<surface>-text-soft`, `<surface>-text-soft-2` |
52
- | Misc neutral primitives | `border`, `placeholder`, `focus`, `disabled` |
53
- | Neutral disabled chip | `disabled-surface`, `disabled-surface-text` |
54
- | Fixed-mode dark surface | `surface-inverse` |
55
- | Brand fills | `accent-surface`, `accent-surface-2`, `accent-surface-3`, `accent-surface-hover` |
56
- | Brand fill anchor | `accent-surface-text` (the fixed white token everything anchors to) |
57
- | Brand foregrounds on neutrals | `accent-text`, `accent-text-soft`, `accent-icon` |
58
- | Brand-tinted disabled | `accent-disabled-surface`, `accent-disabled-surface-text` |
59
- | Code syntax highlighting | `code-comment`, `code-keyword`, `code-string`, `code-number`, … |
60
- | Loading animation | `loading-face-1`, `loading-face-2`, `loading-face-3` |
61
- | Shadows | `shadow-sm`, `shadow-md`, `shadow-lg` |
62
- | Backdrop | `overlay` |
81
+ - Surfaces: `surface`, `surface-2`, `surface-3`.
82
+ - Foregrounds: `surface-text`, `surface-text-soft`, `surface-text-soft-2`.
83
+ - Structure: `border`, `divider`, `outline`, `placeholder`, `focus`.
84
+ - Fills: `accent-surface`, `accent-surface-2`,
85
+ `accent-surface-hover`.
86
+ - Foregrounds on neutral surfaces: `accent-text`, `accent-text-soft`,
87
+ `accent-icon`.
88
+ - Disabled states: `disabled-surface`, `disabled-surface-text`,
89
+ `accent-disabled-surface`, `accent-disabled-surface-text`.
90
+ - Effects: `shadow-sm`, `shadow-md`, `shadow-lg`, `overlay`, `hover`, `tint`.
63
91
 
64
- **Rule of thumb:** *purpose-name first, variant suffix last* (`-2`, `-text`, `-soft`, `-hover`, `-disabled`).
92
+ These names are not only readable. They also help APCA role inference pick the
93
+ right polarity. For example, `button-text` is foreground, `input-bg` is a
94
+ surface, and `card-outline` is a border.
65
95
 
66
- ## Surfaces (root colors)
96
+ ## Build the Default Theme
67
97
 
68
- `surface` is a root color (absolute `tone`, no `base`) with a low saturation factor. The ladder chains off it via small relative offsets:
98
+ Start with the surface family. It is mostly tone, with small saturation changes
99
+ to keep the ladder visually coherent:
69
100
 
70
101
  ```ts
102
+ const defaultTheme = glaze(PURPLE_HUE, SEED_SATURATION);
103
+
71
104
  defaultTheme.colors({
72
- surface: { tone: 100, saturation: 0.11 },
73
- 'surface-2': { base: 'surface', tone: '-2', saturation: 0.15, inherit: false },
74
- 'surface-3': { base: 'surface', tone: '-4', saturation: 0.19, inherit: false },
105
+ surface: { tone: 100, saturation: 0.11 },
106
+ 'surface-2': {
107
+ base: 'surface',
108
+ tone: '-2',
109
+ saturation: 0.15,
110
+ inherit: false,
111
+ },
112
+ 'surface-3': {
113
+ base: 'surface',
114
+ tone: '-4',
115
+ saturation: 0.19,
116
+ inherit: false,
117
+ },
75
118
  });
76
119
  ```
77
120
 
78
- A factor of `0.11` of the seed gives a barely-noticeable hue shift — enough that light/dark surfaces feel branded, not enough to look tinted. The slight saturation bump on `-2` / `-3` compensates for perceived saturation dropping as tone drops, so the ladder reads as one consistent surface family.
121
+ Because tone shifts are consistent across schemes, these small relative offsets
122
+ are enough. There is no separate dark-mode curve to tune.
79
123
 
80
- `mode: 'auto'` (the default) inverts these in tone (`100 − t`) and remaps into the dark window, so a `tone: 100` light-mode surface lands at the dark window's floor in dark mode. Because tone is contrast-uniform, the relative deltas (`-2`, `-4`) translate to the same contrast steps in both schemes — no fitted curve required. `inherit: false` on `-2` / `-3` keeps colored sibling themes lean — they only need a single tinted `surface`, not the whole ladder.
124
+ ### Text and Borders
81
125
 
82
- ## Text on surfaces (anchor at the edge)
83
-
84
- The headline trick of the whole methodology. Strong text uses an **absolute `tone` near the edge of the window**; soft variants use a **directional relative hint plus a numeric `contrast`**.
126
+ Use a hard edge tone for maximum-prominence text, and APCA floors for softer
127
+ content:
85
128
 
86
129
  ```ts
87
- 'surface-text': {
88
- base: 'surface', tone: 2, saturation: 0.475,
89
- },
90
- 'surface-text-soft': {
91
- base: 'surface', tone: '-1', saturation: 0.375,
92
- contrast: [9, 11], inherit: false,
93
- },
94
- 'surface-text-soft-2': {
95
- base: 'surface', tone: '-1', saturation: 0.24,
96
- contrast: [4.5, 5.5], inherit: false,
97
- },
130
+ defaultTheme.colors({
131
+ 'surface-text': {
132
+ base: 'surface',
133
+ tone: 2,
134
+ saturation: 0.475,
135
+ },
136
+ 'surface-text-soft': {
137
+ base: 'surface',
138
+ tone: '-1',
139
+ saturation: 0.375,
140
+ contrast: { apca: ['content', 'body'] },
141
+ inherit: false,
142
+ },
143
+ 'surface-text-soft-2': {
144
+ base: 'surface',
145
+ tone: '-1',
146
+ saturation: 0.24,
147
+ contrast: { apca: ['large', 'content'] },
148
+ inherit: false,
149
+ },
150
+ border: {
151
+ base: 'surface',
152
+ tone: ['-10', '-20'],
153
+ saturation: 0.175,
154
+ inherit: false,
155
+ },
156
+ });
98
157
  ```
99
158
 
100
- Repeat the same triple anchored to each subordinate surface (`surface-2-text`, `surface-2-text-soft`, `surface-3-text`, …) so the ladder stays self-consistent.
101
-
102
- The strong-text `tone: 2` pins the light-mode resolved value near the bottom of the light window (close to black against the near-white surface) and inverts to near-white in dark mode — both yielding very high contrast against their surface. A `contrast: 'AAA'` solver pass would have stopped at the AAA floor (cr = 7) and no further. **Anchoring at the edge** beats the contrast solver because the solver only needs to *meet* the floor, not exceed it.
103
-
104
- The soft variants use `tone: '-1'` only as a *directional hint* — the real positioning comes from the numeric `contrast`. Numeric ratios give designers precise perceived weight where presets would only guarantee the AA/AAA floor.
105
-
106
- In high-contrast mode the tone window is bypassed entirely (full `[0, 100]` range), so `tone: 2` reaches close to black in light HC and close to white in dark HC — maximal contrast against the surface in both.
159
+ `surface-text` does not need a contrast floor because it is intentionally pinned
160
+ near the edge. The soft variants use `tone: '-1'` as the direction and APCA as
161
+ the readable floor. `border` uses an HC tone pair because borders usually need a
162
+ larger visible step in high contrast.
107
163
 
108
- ## Other neutral primitives
164
+ Repeat the same pattern for `surface-2` and `surface-3` only if components need
165
+ text directly on those surfaces.
109
166
 
110
- Borders, placeholders, focus rings, and the floating "muted text" tone — all default-only:
167
+ ### Neutral Utility Tokens
111
168
 
112
- ```ts
113
- border: { base: 'surface', tone: ['-10', '-20'], saturation: 0.175, inherit: false },
114
- placeholder: { base: 'surface', tone: 67, saturation: 0.175, inherit: false },
115
- focus: { base: 'surface', tone: 71, saturation: 0.8625, inherit: false },
116
- disabled: { tone: 80.8, saturation: 0.4, inherit: false },
117
- ```
118
-
119
- `border` uses an HC pair — the border darkens twice as much in high-contrast mode for visibility. `placeholder` and `focus` give a `base` for namespacing but use absolute tone independently. `disabled` is a root color (no `base`) — it's used as a plain "muted text" token in some places, free of the surface chain.
120
-
121
- ## Disabled chip (contrast-driven for scheme symmetry)
122
-
123
- The disabled chip + label pair uses `mode: 'auto'` and **explicit numeric contrast** against `surface`, not preset `'AA'` / `'AAA'`:
169
+ Keep neutral-only primitives local to the default theme:
124
170
 
125
171
  ```ts
126
- 'disabled-surface': {
127
- base: 'surface', tone: '-1', saturation: 0.2,
128
- contrast: [1.5, 2], inherit: false,
129
- },
130
- 'disabled-surface-text': {
131
- base: 'disabled-surface', tone: '+1', saturation: 0.3,
132
- contrast: 3, inherit: false,
133
- },
172
+ defaultTheme.colors({
173
+ placeholder: {
174
+ base: 'surface',
175
+ tone: 67,
176
+ saturation: 0.175,
177
+ inherit: false,
178
+ },
179
+ focus: {
180
+ base: 'surface',
181
+ tone: 71,
182
+ saturation: 0.8625,
183
+ inherit: false,
184
+ },
185
+ disabled: {
186
+ tone: 80.8,
187
+ saturation: 0.4,
188
+ inherit: false,
189
+ },
190
+ });
134
191
  ```
135
192
 
136
- Each token anchors to its immediate parent surface `*-surface` contrasts against the root `surface`, while `*-surface-text` contrasts against its own chip (`disabled-surface`). This keeps the disabled state self-contained and resolves to consistent ratios in light, dark, and HC (chip ≈ 1.5–2× vs surface, label ≈ 3× on chip). An alpha-tinted overlay would have asymmetric behavior — composited alpha against a near-white light surface produces a much weaker chip than the same overlay against a near-dark dark surface, and the disabled state would stop *looking* disabled in one of the schemes.
193
+ Absolute tones are fine for primitives whose job is visual placement rather than
194
+ a strict relationship to a specific surface.
137
195
 
138
- The general rule: when a color needs to *feel the same across schemes*, anchor it with `mode: 'auto'` + a numeric contrast against a surface, not with a preset.
196
+ ## Chips and Disabled States
139
197
 
140
- ## `surface-inverse` (the fixed-mode escape hatch)
198
+ For subtle fills, tone is usually clearer than contrast:
141
199
 
142
200
  ```ts
143
- 'surface-inverse': {
144
- tone: 12, saturation: 0.475, mode: 'fixed', inherit: false,
145
- },
201
+ defaultTheme.colors({
202
+ 'disabled-surface': {
203
+ base: 'surface',
204
+ tone: '-3',
205
+ saturation: 0.2,
206
+ inherit: false,
207
+ },
208
+ 'disabled-surface-text': {
209
+ base: 'disabled-surface',
210
+ tone: '+18',
211
+ saturation: 0.3,
212
+ flip: false,
213
+ inherit: false,
214
+ },
215
+ });
146
216
  ```
147
217
 
148
- `mode: 'fixed'` skips the dark-scheme tone inversion and only remaps the tone into the dark window, so `surface-inverse` reads as a dark surface in *every* scheme — light, dark, and HC. In high-contrast variants the window is bypassed entirely (identity), so the color stays at its raw tone across all four schemes.
218
+ This says exactly what the pair should do: the chip sits a few tone steps off
219
+ the page, and the label sits a muted distance from the chip. `flip: false` keeps
220
+ the relative label offset on the authored side when it reaches the edge.
149
221
 
150
- Use it for tooltips, code blocks, popovers with their own dark theme. Pair with `#white` for foreground text.
151
-
152
- This is the canonical "I want this color to stay recognizable" pattern. The other `mode: 'fixed'` use is the entire accent system below.
153
-
154
- ## Accent system (anchor pattern)
155
-
156
- The load-bearing trick. Define a single fixed white anchor `accent-surface-text`, then derive every accent surface from it with a small relative tone offset and a numeric contrast under `mode: 'fixed'`:
222
+ Use contrast instead when the chip must hit an explicit accessibility floor:
157
223
 
158
224
  ```ts
159
- 'accent-surface-text': { tone: 100, mode: 'fixed' },
160
-
161
- 'accent-surface': { base: 'accent-surface-text', tone: '-1', contrast: [4.5, 7], mode: 'fixed' },
162
- 'accent-surface-2': { base: 'accent-surface-text', tone: '-1', contrast: [4.8, 7.5], mode: 'fixed' },
163
- 'accent-surface-3': { base: 'accent-surface-text', tone: '-1', contrast: [5.2, 8], mode: 'fixed' },
164
- 'accent-surface-hover': { base: 'accent-surface-text', tone: '-1', contrast: [6, 8.5], mode: 'fixed' },
225
+ defaultTheme.colors({
226
+ 'disabled-surface-text': {
227
+ base: 'disabled-surface',
228
+ tone: '+1',
229
+ saturation: 0.3,
230
+ contrast: { apca: 'non-text' },
231
+ inherit: false,
232
+ },
233
+ });
165
234
  ```
166
235
 
167
- Three things make this work:
168
-
169
- - **One anchor, one chain.** All accent surfaces stay in the same hue family because they all derive from `accent-surface-text`.
170
- - **`mode: 'fixed'` keeps the brand recognizable.** Without it, the dark-scheme tone inversion would turn the brand fill into a tone-inverted counterpart that may no longer read as the intended brand surface. Fixed remaps the tone into the dark window without inverting, so a `tone: 52` brand color stays mid-toned in dark mode — still recognizably the same color.
171
- - **Numeric contrasts, not presets.** `'AA'` / `'AAA'` would let the solver push the color far away from its anchor in dark schemes, breaking the relationship between `accent-surface` and its neighbors. Numeric ratios make the darkening between `accent-surface` (4.5/7), `-2` (4.8/7.5), `-3` (5.2/8), and `-hover` (6/8.5) a tight, designed sequence — a stepped gradient rather than four solver-generated outliers.
236
+ When a token needs the scheme extreme, use `tone: 'min'` or `tone: 'max'`
237
+ directly. Avoid large magic numbers or fake contrast floors just to push a color
238
+ to the edge.
172
239
 
173
- The hover variant is a dedicated *fixed* token. Reusing `accent-text` (which is `mode: 'auto'` and inverts direction in dark) would break the hover feel.
240
+ ## Fixed Surfaces and Accent Fills
174
241
 
175
- ## Adaptive accent foregrounds
176
-
177
- The opposite of the fills. Brand-colored *foregrounds* are anchored to **`surface`, not `accent-surface`**, with `mode: 'auto'` (default) and full saturation:
242
+ Use `mode: 'fixed'` when the authored color should stay recognizable across
243
+ schemes.
178
244
 
179
245
  ```ts
180
- 'accent-text': { base: 'surface', tone: '-1', saturation: 1, contrast: [6.4, 10] },
181
- 'accent-text-soft': { base: 'surface', tone: '-1', saturation: 1, contrast: [4.5, 7] },
182
- 'accent-icon': { base: 'surface', tone: '-1', saturation: 0.9375, contrast: [3.2, 5] },
246
+ defaultTheme.colors({
247
+ 'surface-inverse': {
248
+ tone: 12,
249
+ saturation: 0.475,
250
+ mode: 'fixed',
251
+ inherit: false,
252
+ },
253
+
254
+ 'accent-surface-text': {
255
+ tone: 100,
256
+ mode: 'fixed',
257
+ },
258
+ 'accent-surface': {
259
+ base: 'accent-surface-text',
260
+ tone: '-1',
261
+ contrast: { apca: ['content', 'body'] },
262
+ mode: 'fixed',
263
+ },
264
+ 'accent-surface-2': {
265
+ base: 'accent-surface-text',
266
+ tone: '-1',
267
+ contrast: { apca: [65, 80] },
268
+ mode: 'fixed',
269
+ },
270
+ 'accent-surface-hover': {
271
+ base: 'accent-surface-text',
272
+ tone: '-1',
273
+ contrast: { apca: ['body', 'preferred'] },
274
+ mode: 'fixed',
275
+ },
276
+ });
183
277
  ```
184
278
 
185
- Foregrounds need to stay readable on the surface they actually sit on — anchoring to the brand fill would only enforce contrast against that fill, leaving the dark-mode color washed out against the actual surface (e.g. SECONDARY button labels sit on `surface`, not on the brand fill). Anchoring to `surface` + `mode: 'auto'` lets the solver lift the tone in dark mode so the contrast floor holds in both schemes.
186
-
187
- `accent-text-soft` shares the anchor and saturation but relaxes the contrast floor for a visibly less prominent secondary foreground (link base color, subdued labels). Critically, it stays `mode: 'auto'` — a fixed version would collapse to cr≈3 against the dark surface and break AA.
279
+ The accent fill family is a fixed chain against a fixed text anchor. The names
280
+ infer `surface` and `text` roles, so APCA gets the right polarity without extra
281
+ fields.
188
282
 
189
- ## Brand-tinted disabled
283
+ ## Adaptive Accent Foregrounds
190
284
 
191
- Mirrors the neutral disabled pair from above but with higher saturation so the chip reads as a *muted brand color* rather than fully neutral grey. A disabled chip has no contrast *requirement* — it just needs to sit a hair off the surface and carry a faint label. That's a job for **tone**, not the contrast solver:
285
+ Brand foregrounds that sit on neutral surfaces should stay adaptive:
192
286
 
193
287
  ```ts
194
- 'accent-disabled-surface': {
195
- base: 'surface', tone: '+3', saturation: 0.5,
196
- },
197
- 'accent-disabled-surface-text': {
198
- base: 'accent-disabled-surface', tone: '+18', saturation: 0.4,
199
- flip: false,
200
- },
288
+ defaultTheme.colors({
289
+ 'accent-text': {
290
+ base: 'surface',
291
+ tone: '-1',
292
+ saturation: 1,
293
+ contrast: { apca: ['content', 'body'] },
294
+ },
295
+ 'accent-text-soft': {
296
+ base: 'surface',
297
+ tone: '-1',
298
+ saturation: 1,
299
+ contrast: { apca: ['large', 'content'] },
300
+ },
301
+ 'accent-icon': {
302
+ base: 'surface',
303
+ tone: '-1',
304
+ saturation: 0.9375,
305
+ contrast: { apca: ['non-text', 'large'] },
306
+ },
307
+ });
201
308
  ```
202
309
 
203
- Tone offsets say what we mean directly: the chip is three tone steps off `surface`, the label eighteen steps off the chip — a deliberately low-contrast pair, no solver involved. Because tone is contrast-uniform, those steps look the same in light and dark, and `mode: 'auto'` (the default) keeps the relationship intact when the scheme inverts.
204
-
205
- `flip: false` on the label is the safety rail. A relative `tone: '+18'` lands above the chip; if the chip is already near the top of the range (a light surface), `+18` would overshoot 100. With flip on (the default), Glaze mirrors the offset to `-18` so it reflects back into range — handy for contrast tokens, but here it would put the label on the *wrong* side of the chip. Turning flip off clamps to the boundary instead, keeping the label on the side you authored.
310
+ Anchor these to `surface`, not to `accent-surface`. Their real job is to remain
311
+ readable on neutral UI, so `mode: 'auto'` and a surface base are the right
312
+ defaults.
206
313
 
207
- When a token genuinely needs the extreme — the lightest or darkest tone the scheme allows — reach for `'max'` / `'min'` rather than a large number or a contrast hack:
314
+ Brand-tinted disabled states can usually be pure tone:
208
315
 
209
316
  ```ts
210
- 'card-floor': { tone: 'min' }, // darkest tone (root, no base needed)
211
- 'card-ceil': { tone: 'max' }, // lightest tone
317
+ defaultTheme.colors({
318
+ 'accent-disabled-surface': {
319
+ base: 'surface',
320
+ tone: '+3',
321
+ saturation: 0.5,
322
+ },
323
+ 'accent-disabled-surface-text': {
324
+ base: 'accent-disabled-surface',
325
+ tone: '+18',
326
+ saturation: 0.4,
327
+ flip: false,
328
+ },
329
+ });
212
330
  ```
213
331
 
214
- `'max'` resolves to the scheme's highest authored tone (100) and `'min'` to the lowest (0); neither needs a `base`. They flow through scheme mapping like any absolute tone, so under `mode: 'auto'` they invert in dark — `'max'` is the lightest tone in light mode and, after inversion, the *darkest* in dark. Use `mode: 'static'` if you want the same extreme pinned across every scheme, or `mode: 'fixed'` to keep it on the same end without inverting. Either way: no magic numbers, no contrast floor standing in for "push it all the way".
332
+ These are inherited, so status themes automatically get
333
+ `success-accent-disabled-surface`, `danger-accent-disabled-surface`, and the
334
+ matching text tokens.
215
335
 
216
- These are inherited (no `inherit: false`), so each colored sibling theme automatically emits `<theme>-accent-disabled-surface` and `<theme>-accent-disabled-surface-text`. PRIMARY-style disabled buttons stay tinted with the active theme's hue (danger-tinted danger button, success-tinted success button), preserving brand identity even in the disabled state.
336
+ ## Special Purpose Colors
217
337
 
218
- ## Per-color hue overrides (code highlighting)
219
-
220
- The `code-*` tokens use **absolute `hue` numbers** regardless of the seed. Each is `base: 'surface'` with `mode: 'auto'`, a per-token saturation, and a numeric contrast floor:
338
+ Use absolute `hue` overrides for tokens that should come from another hue family
339
+ but keep the same adaptation behavior:
221
340
 
222
341
  ```ts
223
- 'code-comment': { base: 'surface', hue: 280, saturation: 0.1, tone: '-1', contrast: [4.5, 7], inherit: false },
224
- 'code-keyword': { base: 'surface', hue: 348, saturation: 1, tone: '-1', contrast: [5, 7.5], inherit: false },
225
- 'code-string': { base: 'surface', hue: SUCCESS_HUE, saturation: 1, tone: '-1', contrast: [4.5, 7], inherit: false },
226
- // …code-punctuation, code-number, code-function, code-attribute follow the same shape
342
+ defaultTheme.colors({
343
+ 'code-comment': {
344
+ base: 'surface',
345
+ hue: 280,
346
+ saturation: 0.1,
347
+ tone: '-1',
348
+ contrast: { apca: ['large', 'content'] },
349
+ inherit: false,
350
+ },
351
+ 'code-keyword': {
352
+ base: 'surface',
353
+ hue: 348,
354
+ saturation: 1,
355
+ tone: '-1',
356
+ contrast: { apca: ['content', 'body'] },
357
+ inherit: false,
358
+ },
359
+ 'code-string': {
360
+ base: 'surface',
361
+ hue: SUCCESS_HUE,
362
+ saturation: 1,
363
+ tone: '-1',
364
+ contrast: { apca: ['large', 'content'] },
365
+ inherit: false,
366
+ },
367
+ });
227
368
  ```
228
369
 
229
- The canonical pattern for "I want a color from a different hue family but the same adaptive behavior". Absolute `hue` overrides the theme seed for a single color; everything else (contrast against `surface`, dark adaptation, HC tightening) still works. `inherit: false` because syntax highlighting is a default-only concern.
230
-
231
- ## Loading-animation faces
232
-
233
- A 3-step ramp using *absolute* tones with high saturation factors and tight numeric contrasts:
370
+ Use small tone ramps for decorative motion:
234
371
 
235
372
  ```ts
236
- 'loading-face-1': { base: 'surface', tone: 98, saturation: 0.3, contrast: [1.04, 1.5], inherit: false },
237
- 'loading-face-2': { base: 'surface', tone: 91, saturation: 0.62, contrast: [1.24, 2.5], inherit: false },
238
- 'loading-face-3': { base: 'surface', tone: 79, saturation: 0.66, contrast: [1.75, 4], inherit: false },
373
+ defaultTheme.colors({
374
+ 'loading-face-1': {
375
+ base: 'surface',
376
+ tone: 98,
377
+ saturation: 0.3,
378
+ inherit: false,
379
+ },
380
+ 'loading-face-2': {
381
+ base: 'surface',
382
+ tone: 91,
383
+ saturation: 0.62,
384
+ inherit: false,
385
+ },
386
+ 'loading-face-3': {
387
+ base: 'surface',
388
+ tone: 79,
389
+ saturation: 0.66,
390
+ inherit: false,
391
+ },
392
+ });
239
393
  ```
240
394
 
241
- Combines absolute tone positioning (so the ramp is deterministic in light mode) with a numeric contrast floor (so the ramp still reads in dark and HC). The HC contrast jumps significantly (`1.04 → 1.5`, `1.24 → 2.5`, `1.75 → 4`) so the animation stays perceivable for low-vision users.
395
+ Since tone steps now invert consistently across schemes, the same ramp keeps its
396
+ spacing in light and dark without involving the contrast solver. Use an HC tone
397
+ pair only when the animation should become more pronounced in high contrast.
242
398
 
243
- ## Shadows
399
+ ## Effects
244
400
 
245
- Three sizes, all sharing `bg: 'surface'` and `fg: 'surface-text'`, varying only `intensity`:
401
+ Define one neutral shadow system:
246
402
 
247
403
  ```ts
248
- 'shadow-sm': { type: 'shadow', bg: 'surface', fg: 'surface-text', intensity: 5, inherit: false },
249
- 'shadow-md': { type: 'shadow', bg: 'surface', fg: 'surface-text', intensity: 10, inherit: false },
250
- 'shadow-lg': { type: 'shadow', bg: 'surface', fg: 'surface-text', intensity: 15, inherit: false },
404
+ defaultTheme.colors({
405
+ 'shadow-sm': {
406
+ type: 'shadow',
407
+ bg: 'surface',
408
+ fg: 'surface-text',
409
+ intensity: 5,
410
+ inherit: false,
411
+ },
412
+ 'shadow-md': {
413
+ type: 'shadow',
414
+ bg: 'surface',
415
+ fg: 'surface-text',
416
+ intensity: [10, 20],
417
+ inherit: false,
418
+ },
419
+ 'shadow-lg': {
420
+ type: 'shadow',
421
+ bg: 'surface',
422
+ fg: 'surface-text',
423
+ intensity: [15, 30],
424
+ inherit: false,
425
+ },
426
+ });
251
427
  ```
252
428
 
253
- Including `fg` matters: shadow strength scales with `|l_bg − l_fg|`, so anchoring `fg` to `surface-text` (which is anchored at the edge of the window) makes shadows *automatically deeper* in dark mode where the bg/fg gap is larger. All shadows are `inherit: false` — there's only one shadow system for the whole UI, and colored sibling themes don't carry their own.
429
+ Including `fg` lets shadow strength follow the resolved foreground/background
430
+ gap. Use an HC pair for shadows that should deepen in high contrast.
254
431
 
255
- For HC, pass `intensity: [normal, hc]` (e.g. `[10, 20]`) to deepen shadows in high-contrast mode. The full algorithm and tuning knobs are in [api.md → Shadows](api.md#shadows).
256
-
257
- ## Overlay (fixed opacity)
432
+ Use `opacity` for one fixed-alpha color:
258
433
 
259
434
  ```ts
260
- overlay: { tone: 10, opacity: 0.5, inherit: false },
435
+ defaultTheme.colors({
436
+ overlay: { tone: 10, opacity: 0.5, inherit: false },
437
+ });
261
438
  ```
262
439
 
263
- The shortcut for *one solid color with a fixed alpha* — no shadow algorithm, no mix. `opacity` on a regular color attaches an alpha component to every variant. Use it for backdrops, scrims, modal overlays. (Combining `opacity` with `contrast` is not recommended — perceived lightness becomes unpredictable when alpha is fixed; Glaze emits a `console.warn`.)
264
-
265
- ## Mixes for hover / tint
266
-
267
- Reach for mix tokens when you want one color to "tint through" another:
440
+ Use mixes when one color should tint through another:
268
441
 
269
442
  ```ts
270
- hover: {
271
- type: 'mix', base: 'surface', target: 'accent-surface',
272
- value: 8, blend: 'transparent',
273
- },
274
- // hover → accent-surface with alpha = 0.08
275
-
276
- tint: {
277
- type: 'mix', base: 'surface', target: 'accent-surface',
278
- value: 20,
279
- },
443
+ defaultTheme.colors({
444
+ hover: {
445
+ type: 'mix',
446
+ base: 'surface',
447
+ target: 'accent-surface',
448
+ value: 8,
449
+ blend: 'transparent',
450
+ },
451
+ tint: {
452
+ type: 'mix',
453
+ base: 'surface',
454
+ target: 'accent-surface',
455
+ value: 20,
456
+ },
457
+ });
280
458
  ```
281
459
 
282
- - **Transparent mix** *the target color with controlled alpha*. Useful for hover overlays.
283
- - **Opaque mix** solid blend of two colors. Good for subtle tints.
460
+ Transparent mixes are good for hover overlays. Opaque mixes are good for solid
461
+ tints. Mix colors can also use `contrast`; the solver adjusts the value or
462
+ opacity to hit the floor.
284
463
 
285
- Choose `space: 'okhsl'` (default) for design tokens — perceptually uniform, consistent with the rest of Glaze. Choose `space: 'srgb'` to match what the browser would render with a plain CSS overlay. Mix colors support the same `contrast` prop as regular colors; the solver adjusts the mix ratio (opaque) or opacity (transparent) to meet the target.
464
+ ## Extend into Status Themes
286
465
 
287
- ## Colored sibling themes via `extend()`
288
-
289
- One shared `TINTED_SURFACE_OVERRIDE`, applied to every colored theme, with only the `hue` changing per status:
466
+ Once the default theme is shaped, create colored siblings by replacing hue and
467
+ overriding only the root surface that should become visibly tinted:
290
468
 
291
469
  ```ts
292
- const TINTED_SURFACE_OVERRIDE: ColorMap = {
470
+ const TINTED_SURFACE_OVERRIDE = {
293
471
  surface: { tone: 96, saturation: 0.8 },
294
472
  };
295
473
 
296
- const primaryTheme = defaultTheme.extend({ colors: TINTED_SURFACE_OVERRIDE });
297
- const successTheme = defaultTheme.extend({ hue: SUCCESS_HUE, colors: TINTED_SURFACE_OVERRIDE });
298
- const dangerTheme = defaultTheme.extend({ hue: DANGER_HUE, colors: TINTED_SURFACE_OVERRIDE });
299
- const warningTheme = defaultTheme.extend({ hue: WARNING_HUE, colors: TINTED_SURFACE_OVERRIDE });
300
- const noteTheme = defaultTheme.extend({ hue: NOTE_HUE, colors: TINTED_SURFACE_OVERRIDE });
474
+ const primaryTheme = defaultTheme.extend({
475
+ colors: TINTED_SURFACE_OVERRIDE,
476
+ });
477
+ const successTheme = defaultTheme.extend({
478
+ hue: SUCCESS_HUE,
479
+ colors: TINTED_SURFACE_OVERRIDE,
480
+ });
481
+ const dangerTheme = defaultTheme.extend({
482
+ hue: DANGER_HUE,
483
+ colors: TINTED_SURFACE_OVERRIDE,
484
+ });
485
+ const warningTheme = defaultTheme.extend({
486
+ hue: WARNING_HUE,
487
+ colors: TINTED_SURFACE_OVERRIDE,
488
+ });
489
+ const noteTheme = defaultTheme.extend({
490
+ hue: NOTE_HUE,
491
+ colors: TINTED_SURFACE_OVERRIDE,
492
+ });
301
493
  ```
302
494
 
303
- Colored themes need a visibly tinted surface for status banners — saturation jumps from the neutral `0.11` (default theme) to `0.8`. The `inherit: false` discipline pays off here: because most neutrals (`surface-2`, `surface-3`, `border`, `placeholder`, `disabled-*`, `code-*`, `loading-*`, `shadow-*`) are flagged default-only, each colored theme inherits *only* the accent + tinted surface chain and emits a small, focused token set.
304
-
305
- `primaryTheme` keeps the default hue but gets the tinted surface — useful for places that want a brand-tinted banner without semantic status meaning.
495
+ The inherited accent and disabled tokens now resolve in each status hue. Tokens
496
+ marked `inherit: false` stay default-only, so sibling themes remain small.
306
497
 
307
- ## Palette composition
498
+ ## Export the Palette
308
499
 
309
- Compose all themes into a palette so they can be exported as one token set:
500
+ Compose the themes once:
310
501
 
311
502
  ```ts
312
503
  const palette = glaze.palette({
313
504
  default: defaultTheme,
314
505
  primary: primaryTheme,
315
506
  success: successTheme,
316
- danger: dangerTheme,
507
+ danger: dangerTheme,
317
508
  warning: warningTheme,
318
- note: noteTheme,
509
+ note: noteTheme,
319
510
  });
320
511
  ```
321
512
 
322
- The default theme is conventionally exported unprefixed (its tokens land as `#surface`, `#border`); colored themes are prefixed with their name. See [migration.md](migration.md) for the prefix map shape, alias patterns, and how to wire the resulting tokens into Tasty / CSS / framework-agnostic JSON.
513
+ The usual export shape is default unprefixed and status themes prefixed:
514
+
515
+ ```ts
516
+ palette.tasty({
517
+ prefix: {
518
+ default: '',
519
+ primary: 'primary-',
520
+ success: 'success-',
521
+ danger: 'danger-',
522
+ warning: 'warning-',
523
+ note: 'note-',
524
+ },
525
+ });
526
+ ```
527
+
528
+ See [migration.md](migration.md) for export shapes, prefix maps, Tasty wiring,
529
+ CSS variables, and JSON integration.
530
+
531
+ ## High Contrast
323
532
 
324
- ## High-contrast strategy
533
+ High contrast is not a separate palette. Any value that accepts an HC pair can
534
+ tighten the HC variant: `tone`, `contrast`, shadow `intensity`, and mix `value`.
325
535
 
326
- Glaze's high-contrast mode is opt-in per token: anywhere `tone`, `contrast`, `intensity`, or `value` accepts an HC pair, you can pass `[normal, hc]` to tighten the HC variant. The heuristic is to pair anything that's already contrast-driven:
536
+ Use HC pairs where users should actually get more separation:
327
537
 
328
- - Text-against-surface contrasts (`[9, 11]`, `[4.5, 5.5]`, `[6.4, 10]`).
329
- - The accent surface ladder (`[4.5, 7]` `[5.2, 8]` → `[6, 8.5]`).
330
- - The loading ramp's contrasts.
331
- - Shadow `intensity` (e.g. `intensity: [10, 20]`).
332
- - `border` tone (e.g. `tone: ['-10', '-20']`).
538
+ - Text/content contrast: `{ apca: ['content', 'body'] }`.
539
+ - Accent fills: `{ apca: ['content', 'body'] }` or stronger.
540
+ - Borders: `tone: ['-10', '-20']`.
541
+ - Shadows: `intensity: [10, 20]`.
542
+ - Decorative ramps that must stay perceivable.
333
543
 
334
- In HC the tone window is **bypassed entirely** light HC and dark HC operate on the full `[0, 100]` range. That's why edge-anchored absolute tones like `surface-text: { tone: 2 }` reach close to black in light HC and close to white in dark HC, exactly what you want for maximum contrast.
544
+ In HC variants, Glaze bypasses the normal tone window and uses the full
545
+ `[0, 100]` range. Edge tones can reach the edge; contrast floors have more room
546
+ to solve.
335
547
 
336
- ## Closing checklist
548
+ ## Checklist
337
549
 
338
550
  Before shipping a palette, verify:
339
551
 
340
- - [ ] Every text token has an explicit `contrast` *or* an edge-anchored absolute `tone`.
341
- - [ ] Every accent surface uses `mode: 'fixed'` + numeric `contrast` (not preset `'AA'` / `'AAA'`).
342
- - [ ] Every brand foreground (`accent-text*`, `accent-icon`) is anchored to `surface`, **not** to `accent-surface`.
343
- - [ ] Every `inherit: false` is intentional colored sibling themes only carry the tokens they actually need.
344
- - [ ] HC pairs are present on every contrast-driven token, not just the strong ones.
345
- - [ ] Shadow `fg` is set when you want shadows to deepen in dark mode.
346
- - [ ] `glaze.configure({ states, modes })` matches the global predefined states wired in your app's root.
552
+ - Text, icon, and content tokens either have APCA/WCAG contrast or are
553
+ deliberately edge-anchored.
554
+ - Accent fills use `mode: 'fixed'`; accent foregrounds on neutral UI stay
555
+ `mode: 'auto'` and are based on `surface`.
556
+ - Ambiguous APCA tokens have an explicit `role`; obvious names rely on inference.
557
+ - Low-stakes visual relationships use relative `tone` instead of fake contrast
558
+ floors.
559
+ - `inherit: false` is set on default-only tokens so status themes stay focused.
560
+ - HC pairs exist where high contrast should visibly tighten.
561
+ - `glaze.configure({ states, modes })` matches the states registered in the app.