@hanzo/ui 8.27.49 → 8.27.51

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/dist/theme.css CHANGED
@@ -13,24 +13,9 @@
13
13
  */
14
14
 
15
15
  /* ── tokens/colors.css ─────────────────────────────────────── */
16
- /* Hanzo is monochrome. One hue rendered through an opacity ladder.
17
- Base ladder = Tailwind neutral (tailwind.config.ts). Semantic names match
18
- hanzo.ai's CSS variables exactly, so code copies over 1:1.
19
- DARK IS THE DEFAULT THEME (hanzo.ai mounts ThemeProvider defaultTheme="dark").
20
-
21
- The dark palette is derived from the chat surface in hanzoai/extension
22
- (packages/browser/src/sidebar.css), which is the reference aesthetic: a
23
- #0a0a0a ground, #fafafa ink, and boundaries drawn as LOW-ALPHA HAIRLINES.
24
- Two properties of that reference are load-bearing and easy to lose:
25
-
26
- 1. Borders are alpha, not hex. rgb(255 255 255 / .10) composites correctly on
27
- #0a0a0a AND on #262626; a solid #1f1f1f reads on the page and vanishes the
28
- moment it lands on a lifted surface — which is why anyone who needed a
29
- visible edge reached past it for the CONTROL rung and got a wireframe.
30
- 2. Surfaces lift by tiny steps (0a -> 0f -> 17 -> 26), and ink is a graded
31
- ramp (#fafafa -> .78 -> .55 -> #a3a3a3), not one flat white. Pure #ffffff
32
- on pure #000000 is 21:1 — the maximum-contrast pair that exists, and it
33
- halates. #fafafa on #0a0a0a is 18.1:1: still far past AAA, visibly softer. */
16
+ /* AUTO-GENERATED by @hanzo/tokens scripts/gen-css.mjs from src/theme.ts — DO NOT EDIT.
17
+ * Edit the token in @hanzo/tokens src/theme.ts and re-run `pnpm --filter @hanzo/tokens gen-css`. */
18
+ /* Hanzo colour tokens — monochrome, dark-default. One hue, an opacity ladder. */
34
19
 
35
20
  :root{
36
21
  color-scheme:dark;
@@ -49,140 +34,71 @@
49
34
  --neutral-950:#0A0A0A;
50
35
  --pure-black:#000000;
51
36
  --pure-white:#FFFFFF;
52
- /* Press-kit brand constants (public/press/hanzo/README.md). */
53
37
  --hanzo-black:#0A0A0B;
54
38
  --hanzo-white:#FFFFFF;
55
39
 
40
+ /* ——— art: four hues, for artwork only ———
41
+ Light, never paint. They colour hero art and the few soft glows that give a
42
+ section form; chrome, type and every control stay on the monochrome ladder
43
+ below. The same in both themes, because art is not themed. */
44
+ --art-tide:#2F6BFF;
45
+ --art-iris:#8B5CF6;
46
+ --art-ember:#FF7A3D;
47
+ --art-glow:#F4F1FF;
48
+
56
49
  /* ——— the opacity ladder: the real palette ——— */
57
- /* Every rung below .30 is a HAIRLINE or a LIFT — the two things this system
58
- draws with. The rungs are the reference's own values, not a rounded ramp. */
59
- /* The five rungs marked (chrome) below were being retyped as raw rgba in
60
- @hanzogui/shell's theme.ts, which is the shared bar every Hanzo property
61
- mounts. That is the same defect this ladder exists to end — a value in two
62
- places drifts, and this pair already had: the shell's resting hairline sat
63
- at .06 against this file's .10 for long enough that the bar and the page
64
- under it drew different hairlines. They are rungs here now, and the shell
65
- references them, so the chrome has no colour of its own left to drift.
66
-
67
- They are the shell's OWN measured values, added at the alpha it already
68
- ships. Snapping them to neighbouring rungs instead would have been a
69
- silent redesign of the estate's chrome, and two of them carry decisions
70
- that a round number would have destroyed: .92 is ink deliberately short of
71
- the .98 hover so a hover still reads as a response, and .03 is the one
72
- lift that must stay below the .05 hairline it sits inside. */
73
- --white-03:rgb(255 255 255 / .03); /* (chrome) the raised fill */
50
+ --white-03:rgb(255 255 255 / .03); /* the raised fill */
74
51
  --white-05:rgb(255 255 255 / .05);
75
52
  --white-06:rgb(255 255 255 / .06);
76
53
  --white-08:rgb(255 255 255 / .08);
77
54
  --white-10:rgb(255 255 255 / .10);
78
- --white-14:rgb(255 255 255 / .14); /* (chrome) selection wash */
55
+ --white-14:rgb(255 255 255 / .14); /* selection wash */
79
56
  --white-15:rgb(255 255 255 / .15);
80
57
  --white-16:rgb(255 255 255 / .16);
81
- --white-18:rgb(255 255 255 / .18); /* (chrome) selection tint */
58
+ --white-18:rgb(255 255 255 / .18); /* selection tint */
82
59
  --white-20:rgb(255 255 255 / .20);
83
60
  --white-22:rgb(255 255 255 / .22);
84
61
  --white-30:rgb(255 255 255 / .30);
85
62
  --white-40:rgb(255 255 255 / .40);
86
- --white-45:rgb(255 255 255 / .45); /* (chrome) the dimmest ink */
63
+ --white-45:rgb(255 255 255 / .45); /* the dimmest ink */
87
64
  --white-55:rgb(255 255 255 / .55);
88
65
  --white-60:rgb(255 255 255 / .60);
89
66
  --white-78:rgb(255 255 255 / .78);
90
67
  --white-80:rgb(255 255 255 / .80);
91
- --white-92:rgb(255 255 255 / .92); /* (chrome) body ink */
68
+ --white-92:rgb(255 255 255 / .92); /* body ink */
92
69
 
93
70
  /* ——— semantic aliases (dark, the default) ——— */
94
71
  --background:#0a0a0a;
95
- /* READING ink, one rung below the top of the ramp. The note at the head of this
96
- file already made this argument once — #ffffff on #000000 halates, so #fafafa
97
- — and the argument does not stop being true at 19.0:1. Every heading and
98
- paragraph on every surface was set in the brightest ink the system has, while
99
- the CHROME beside them draws body text at --white-92; a page read as glare
100
- against its own header. #e5e5e5 is the neutral-200 rung this file already
101
- spends on --primary-hover, so the ramp gains no new value: 15.7:1 on the page,
102
- 15.2:1 on a card, both more than twice the 7:1 AAA asks of body text.
103
- Something that genuinely needs the brightest ink names --primary. */
104
- --foreground:#e5e5e5;
72
+ --foreground:#e5e5e5; /* the graded ink, one rung short of #fafafa */
105
73
  --card:#0f0f0f;
106
74
  --card-foreground:#e5e5e5;
107
75
  --popover:#0f0f0f;
108
76
  --popover-foreground:#e5e5e5;
109
77
  --primary:#fafafa;
110
- /* A 5% step, not the 16% this used to be. A filled button's fill is already
111
- the top of the ink ramp, so the only directions available on hover are
112
- "dimmer" and "brighter-around-the-edges" — and a 16% drop reads as the
113
- button going DISABLED under the cursor, which is the one thing a hover must
114
- never look like. The step is now small enough to register as pressure while
115
- --bloom does the actual work of lighting it up. */
116
78
  --primary-hover:#e5e5e5;
117
79
  --primary-foreground:#0a0a0a;
118
80
  --secondary:#262626;
119
- /* One rung up, so a secondary button HAS a hover. It previously resolved to
120
- --surface-3, which is --secondary — the same value it starts at. */
121
81
  --secondary-hover:#333333;
122
82
  --secondary-foreground:#fafafa;
123
83
  --muted:#171717;
124
84
  --muted-foreground:#a3a3a3;
125
85
  --accent:#262626;
86
+ --accent-hover:var(--secondary-hover); /* the accent under the cursor; a chosen accent restates it */
126
87
  --accent-foreground:#fafafa;
127
- /* Destructive is the ONE place the monochrome rule already yields, and it was
128
- yielding in only half the system: a field that fails validation draws
129
- --state-error (#ef4444, which DESIGN.md §2.4 lists as permitted precisely
130
- for "destructive / blocking error"), while the button that performs the
131
- destruction drew #666666 — a mid-grey that is indistinguishable from a
132
- disabled control. "Delete everything" and "you cannot click this" must not
133
- be the same colour. So the two converge on the one token that already
134
- names this meaning, and the ramp stays monochrome everywhere else. */
135
- --destructive:var(--state-error);
136
- --destructive-hover:#dc2626; /* red-600 — deepens under the cursor */
88
+ --destructive:#dc2626; /* red-600 — a FILL under its foreground, 4.6:1; the error hue is --state-error */
89
+ --destructive-hover:#b91c1c; /* red-700 — deepens under the cursor, 6.2:1 */
137
90
  --destructive-foreground:#fafafa;
138
91
 
139
- /* ——— boundaries ———
140
- ONE ladder, cut from alpha, graded by duty. Every rung is a rung of the
141
- opacity ladder above, so every boundary composites correctly on the page,
142
- on a card, and inside a popover — and every rung is the reference's own
143
- measured value, not a rounded ramp:
144
-
145
- --border .10 the hairline. Cards, panels, dividers, list rows.
146
- --border-strong .16 the same hairline, one step up. Hover, emphasis.
147
- --border-control .15 a control's resting edge — input, select, textarea.
148
- --border-focus .22 that control, focused.
149
- --border-selected .30 the thing that is currently CHOSEN — the active
150
- tab, the current tile, the featured plan.
151
-
152
- The previous release cut the last three from the NEUTRAL ladder instead,
153
- because a boundary that clears WCAG 1.4.11's 3:1 on a near-black ground IS
154
- a mid-grey and no amount of tuning changes that. It is true, and it is also
155
- what a wireframe looks like: a 1px #737373 box around every field, four
156
- times heavier than the hairline beside it, so a form read as a debug
157
- overlay rather than a surface. Aesthetics decided it — the reference draws
158
- an input at rgb(255 255 255 / .15) and a focused one at .22 (sidebar.css
159
- --input-border / .composer-box:focus-within), and that is what ships here.
160
-
161
- What replaces the ratio is the FOCUS INDICATOR, which is the thing a
162
- keyboard user actually navigates by and the one boundary still pinned at
163
- 3:1 (--ring, below). A resting edge is an affordance; a focus ring is a
164
- position. Only the second one is load-bearing when you cannot see well, and
165
- spending the contrast budget there rather than on all four edges of every
166
- idle field is what lets the system be quiet AND findable at once. */
167
- --border:var(--white-10);
168
- --border-strong:var(--white-16);
169
- --border-control:var(--white-15); /* sidebar.css --input-border */
170
- --border-focus:var(--white-22); /* .composer-box:focus-within */
171
- --border-selected:var(--white-30); /* the current tab / tile / plan */
172
- --input:var(--border-control); /* shadcn `border-input` — a control */
173
- /* A focus indicator is a NON-TEXT CONTRAST target: WCAG 2.4.11/1.4.11 require
174
- 3:1 against every surface it can land on, and unlike a resting edge it is
175
- worth the loudness — it is transient, it only appears on keyboard focus,
176
- and it is the whole of how a keyboard user knows where they are.
177
- --white-40 clears it on every canvas in both themes precisely BECAUSE it is
178
- alpha: on a lifted surface the ring lifts with it (3.77:1 on #0a0a0a,
179
- 3.66:1 on #262626), which a fixed grey cannot do. */
180
- --ring:var(--white-40);
181
- /* The soft halo that sits outside the ring — the reference's
182
- `box-shadow: 0 0 0 2px rgb(255 255 255 / .06)` on a focused composer. It
183
- carries no ratio; it is what makes focus read as designed rather than as a
184
- browser default. Spent through --ring-focus (tokens/elevation.css). */
185
- --ring-halo:var(--white-10);
92
+ /* ——— boundaries — ONE ladder, cut from alpha, graded by duty ——— */
93
+ --border:var(--white-10); /* the hairline */
94
+ --border-strong:var(--white-16); /* hover, emphasis */
95
+ --border-control:var(--white-15); /* a control's resting edge */
96
+ --border-focus:var(--white-22); /* that control, focused */
97
+ --border-selected:var(--white-30); /* the thing currently CHOSEN */
98
+ --edge:var(--white-06); /* fainter than the hairline: an edge between chrome and page */
99
+ --input:var(--border-control); /* shadcn `border-input` — a control */
100
+ --ring:var(--white-40); /* the focus indicator — 3:1 non-text contrast */
101
+ --ring-halo:var(--white-10); /* the soft halo outside the ring */
186
102
  --brand:#e4e4e7;
187
103
  --brand-foreground:#09090b;
188
104
  --brand-muted:#a3a3a3;
@@ -190,49 +106,26 @@
190
106
  --white:#fafafa;
191
107
  --selection:var(--white-20);
192
108
 
193
- /* ——— glass: the lift ———
194
- How a surface rises WITHOUT a block fill. An alpha-white wash composites
195
- over whatever it lands on, so one value works on the page, on a card and
196
- inside a popover. This is the mechanism behind the reference's depth. */
109
+ /* ——— glass: the lift ——— */
197
110
  --glass:var(--white-05);
198
111
  --glass-strong:var(--white-08);
199
112
 
200
- /* ——— surface recipes ———
201
- A card has to be a SURFACE, which means you have to be able to see that it
202
- is not the page. These were cut from neutral-900 (#171717) at .4/.5/.8, and
203
- at .5 that composites to #101010 on a #0a0a0a ground — six levels above the
204
- page, which is not a surface, it is a very slightly different black. Three
205
- cards side by side then read as one flat field with hairlines ruled across
206
- it, and no amount of shadow fixes it, because you cannot cast a shadow onto
207
- black.
208
-
209
- Cut from neutral-800 (#262626) instead, they land on 14 / 18 / 1f — the
210
- reference's own panel (#171717 on a #0f0f0f container) with room either
211
- side of it. They stay ALPHA so a card nested in a popover still lifts off
212
- its parent rather than punching a hole in it; that is the whole reason
213
- these are recipes and not just --surface-* rungs. */
113
+ /* ——— surface recipes ——— */
214
114
  --surface-page:var(--background);
215
- --surface-card:rgb(38 38 38 / .5); /* -> #181818 on the page */
216
- --surface-card-emphasis:rgb(38 38 38 / .75);/* -> #1f1f1f — featured */
217
- --surface-card-quiet:rgb(38 38 38 / .35); /* -> #141414 — story cards */
218
- /* A floating panel that is the SAME colour as the page is not floating. This
219
- was rgb(10 10 10 / .95) — the page, at 95% of the page. */
220
- --surface-overlay:rgb(23 23 23 / .92); /* dropdown / popover panels */
221
- --surface-header:rgb(0 0 0 / .7); /* fixed nav, with backdrop blur */
222
- --surface-scrim:rgb(0 0 0 / .8); /* the dialog / sheet backdrop */
115
+ --surface-card:rgb(38 38 38 / .5); /* -> #181818 on the page */
116
+ --surface-card-emphasis:rgb(38 38 38 / .75); /* -> #1f1f1f — featured */
117
+ --surface-card-quiet:rgb(38 38 38 / .35); /* -> #141414 — story cards */
118
+ --surface-overlay:rgb(23 23 23 / .92); /* dropdown / popover panels */
119
+ --surface-header:rgb(0 0 0 / .7); /* fixed nav, with backdrop blur */
120
+ --surface-scrim:rgb(0 0 0 / .8); /* the dialog / sheet backdrop */
223
121
 
224
122
  /* ——— the numeric surface ladder ——— */
225
- /* Aliases onto the semantic canvases above, so a brand fork that retunes
226
- --card/--muted/--secondary retunes the ladder with it and the light theme
227
- inverts for free. Ascending lift: 0 is the page, 3 is a hovered control.
228
- Dark resolves to the reference's own ladder — 0a / 0f / 17 / 26. */
229
123
  --surface-0:var(--background);
230
124
  --surface-1:var(--card);
231
125
  --surface-2:var(--muted);
232
126
  --surface-3:var(--secondary);
233
127
 
234
- /* ——— text ranks ———
235
- A graded ramp, not one flat white. The steps are the reference's. */
128
+ /* ——— text ranks — a graded ramp, not one flat white ——— */
236
129
  --text-primary:#fafafa;
237
130
  --text-secondary:var(--white-78);
238
131
  --text-tertiary:var(--white-55);
@@ -240,27 +133,22 @@
240
133
  --text-disabled:var(--white-30);
241
134
 
242
135
  /* ——— the ONLY permitted hues (DESIGN.md §2.4) ——— */
243
- --state-error:#ef4444; /* red-500 — destructive / blocking error */
244
- --state-error-text:#fca5a5; /* red-300 */
136
+ --state-error:#ef4444; /* red-500 — destructive / blocking error */
137
+ --state-error-text:#fca5a5; /* red-300 */
245
138
  --state-error-bg:rgb(239 68 68 / .1);
246
- --state-online:#4ade80; /* green-400 — live status dot */
247
- --state-success:#22c55e; /* green-500 — "Free" / "Save N%" callouts */
139
+ --state-online:#4ade80; /* green-400 — live status dot */
140
+ --state-success:#22c55e; /* green-500 — "Free" / "Save N%" callouts */
248
141
  --chrome-dot-red:rgb(239 68 68 / .6);
249
142
  --chrome-dot-yellow:rgb(234 179 8 / .6);
250
143
  --chrome-dot-green:rgb(34 197 94 / .6);
251
144
  }
252
145
 
253
- /* Light theme — the same tokens, inverted.
254
- The white-opacity ladder does NOT invert: --white-16 is white-on-white here.
255
- So EVERY token whose dark value is a --white-* rung must be restated below,
256
- and check-tokens.mjs fails the build if one is missed — that class of bug is
257
- silent (the border simply stops existing) and has shipped before. */
258
146
  .light, :root.t_light, .t_light{
259
147
  color-scheme:light;
148
+
149
+ /* ——— semantic aliases (light) ——— */
260
150
  --background:#f7f7f7;
261
151
  --foreground:#0a0a0a;
262
- /* A ladder, not four names for #f5f5f5. Light lifts by the same tiny steps
263
- dark does — ~2% per rung — so --surface-0..3 mean something in both themes. */
264
152
  --card:#f2f2f2;
265
153
  --card-foreground:#0a0a0a;
266
154
  --popover:#fbfbfb;
@@ -274,28 +162,27 @@
274
162
  --muted:#ededed;
275
163
  --muted-foreground:#525252;
276
164
  --accent:#e4e4e4;
165
+ --accent-hover:var(--secondary-hover);
277
166
  --accent-foreground:#0a0a0a;
278
- --destructive:var(--state-error);
279
- --destructive-hover:#dc2626;
167
+ --destructive:#dc2626;
168
+ --destructive-hover:#b91c1c;
280
169
  --destructive-foreground:#ffffff;
170
+
171
+ /* ——— boundaries — restated as black-alpha ——— */
281
172
  --border:rgb(0 0 0 / .10);
282
173
  --border-strong:rgb(0 0 0 / .16);
283
174
  --border-control:rgb(0 0 0 / .15);
284
- /* Focus and selection are pushed further here than the .22/.30 they sit at in
285
- dark. Black-on-white at a given alpha reads FAINTER than white-on-black at
286
- the same alpha — .22 black on #ffffff measures 1.69:1 where .22 white on
287
- #0a0a0a measures 1.97:1 — so mirroring the number would quietly make the
288
- light theme's focus state the weakest state in the system. */
289
- --border-focus:rgb(0 0 0 / .32);
175
+ --border-focus:rgb(0 0 0 / .32); /* pushed past .22: black-on-white reads fainter */
290
176
  --border-selected:rgb(0 0 0 / .42);
177
+ --edge:rgb(0 0 0 / .06);
291
178
  --input:var(--border-control);
292
- /* Same duty, same 3:1 floor, opposite direction: .50 black is 3.98:1 on
293
- #ffffff and 3.67:1 on #f5f5f5. */
294
- --ring:rgb(0 0 0 / .5);
179
+ --ring:rgb(0 0 0 / .5); /* 3.98:1 on #ffffff, 3.67:1 on #f5f5f5 */
295
180
  --ring-halo:rgb(0 0 0 / .07);
296
181
  --black:#0a0a0a;
297
182
  --white:#ffffff;
298
183
  --selection:rgb(0 0 0 / .16);
184
+
185
+ /* ——— glass + surfaces (light) ——— */
299
186
  --glass:rgb(0 0 0 / .04);
300
187
  --glass-strong:rgb(0 0 0 / .07);
301
188
  --surface-card:#f2f2f2;
@@ -304,6 +191,8 @@
304
191
  --surface-overlay:rgb(255 255 255 / .95);
305
192
  --surface-header:rgb(255 255 255 / .8);
306
193
  --surface-scrim:rgb(0 0 0 / .5);
194
+
195
+ /* ——— text ranks (light) ——— */
307
196
  --text-primary:var(--neutral-950);
308
197
  --text-secondary:rgb(10 10 10 / .78);
309
198
  --text-tertiary:rgb(10 10 10 / .55);
@@ -496,6 +385,10 @@
496
385
  Requires `<meta name="viewport" content="…,viewport-fit=cover">` on the
497
386
  host page; without it the UA reports 0 and the max() is inert, not wrong. */
498
387
  --gutter:max(1rem,var(--safe-left),var(--safe-right));
388
+ /* The chrome's inset: @hanzogui/shell lays its header and menus on this
389
+ value, so a page that reads it starts in the column the brand mark does.
390
+ 20px at 390, 41 at 1024, 58 at 1440. */
391
+ --page-gutter:clamp(20px,4vw,72px);
499
392
  --gutter-sm:1.5rem; /* sm:px-6 */
500
393
  --gutter-lg:2rem; /* lg:px-8 */
501
394
  --section-y:2.5rem; /* py-10 on a phone → py-16 at md */
@@ -503,6 +396,11 @@
503
396
  --hero-y:3rem; /* py-12 on a phone → py-20 at md */
504
397
  --hero-y-lg:4.5rem; /* py-18 on a phone → py-32 at md */
505
398
  --header-height:3.5rem; /* 56px is the phone bar; 64px from md */
399
+ /* The fixed bars a surface actually DRAWS, which base.css's scroll padding
400
+ clears. 0 until a surface with a fixed header or dock sets them, usually
401
+ from a measurement; --header-height is the bar's designed size. */
402
+ --header:0px;
403
+ --dock:0px;
506
404
 
507
405
  /* ——— touch ——— */
508
406
  /* 44px is the floor a pointer-coarse target may render at (Apple HIG 44pt /
@@ -607,14 +505,20 @@
607
505
 
608
506
  /* ── tokens/radius.css ─────────────────────────────────────── */
609
507
  :root{
610
- --radius:0.5rem; /* the base token (globals.css) */
611
- --radius-sm:0.375rem; /* rounded-md — buttons, inputs */
612
- --radius-md:0.5rem;
613
- --radius-lg:0.75rem; /* rounded-xl — cards */
614
- --radius-xl:1rem; /* rounded-2xl — dropdown panels */
615
- --radius-2xl:1.5rem; /* rounded-3xl — story / hero cards */
616
- --radius-composer:28px; /* the chat composer, exactly 28px */
617
- --radius-full:9999px; /* pills, CTAs, avatars, badges */
508
+ /* The CORNER knob — one multiplier on every rung below but the pill, the way
509
+ --type-scale is on type and --density on spacing. @hanzo/ui's gui radii
510
+ multiply by the same name, so a component and a stylesheet stay round
511
+ together. 1 is the published set. */
512
+ --radius-scale:1;
513
+
514
+ --radius:calc(0.5rem * var(--radius-scale, 1)); /* the base token (globals.css) */
515
+ --radius-sm:calc(0.375rem * var(--radius-scale, 1)); /* rounded-md — buttons, inputs */
516
+ --radius-md:calc(0.5rem * var(--radius-scale, 1));
517
+ --radius-lg:calc(0.75rem * var(--radius-scale, 1)); /* rounded-xl — cards */
518
+ --radius-xl:calc(1rem * var(--radius-scale, 1)); /* rounded-2xl — dropdown panels */
519
+ --radius-2xl:calc(1.5rem * var(--radius-scale, 1)); /* rounded-3xl — story / hero cards */
520
+ --radius-composer:calc(28px * var(--radius-scale, 1)); /* the chat composer, 28px at scale 1 */
521
+ --radius-full:9999px; /* pills, CTAs, avatars, badges — a shape, so never scaled */
618
522
  }
619
523
 
620
524
  /* ── tokens/elevation.css ─────────────────────────────────────── */
@@ -733,6 +637,37 @@
733
637
  --gradient-chrome-2:linear-gradient(to right,#ffffff,var(--neutral-500));
734
638
  /* Section-top protection gradient (hero overlays). */
735
639
  --gradient-protect:linear-gradient(to bottom,var(--white-10),transparent);
640
+
641
+ /* ——— chrome: the bars the page passes behind ———
642
+ The header and the dock are one material: a translucent fill over a blur.
643
+ Raise the alpha and it stops being glass and becomes a shelf. A menu that
644
+ hangs from the chrome stands on --chrome-panel, which is opaque, because
645
+ running text read through a menu's words is unreadable. @hanzogui/shell
646
+ paints its panels with it. */
647
+ --chrome:rgb(9 9 11 / .72);
648
+ --chrome-blur:blur(20px) saturate(1.8);
649
+ --chrome-panel:var(--popover);
650
+
651
+ /* ——— the pane: a card on the workspace ground ———
652
+ The fill is the paper ladder's; the pane adds what the ladder leaves to the
653
+ call site — a corner, an edge, the lens, and the gutter between panes.
654
+ --pane-solid is the fill composited once, for a reader who asked for no
655
+ transparency or a browser that cannot blur. --pane-ground is two lamps
656
+ under the threshold of reading as a gradient: a blur over a flat colour
657
+ does nothing a plain alpha would not. */
658
+ --pane-blur:var(--chrome-blur);
659
+ --pane-edge:var(--white-08);
660
+ --pane-round:var(--radius-xl);
661
+ --pane-fill:var(--sheet-1);
662
+ --pane-solid:color-mix(in srgb,var(--neutral-800) 50%,var(--background));
663
+ --pane-floor:var(--background);
664
+ --pane-gap:var(--space-2);
665
+ --pane-ground:radial-gradient(80rem 40rem at 12% -10%,rgb(255 255 255 / .035),transparent 60%),radial-gradient(60rem 34rem at 92% 108%,var(--white-03),transparent 60%); /* @kind color */
666
+ }
667
+
668
+ /* A desk gets room to see the ground between panes. */
669
+ @media (min-width:1024px){
670
+ :root{--pane-gap:var(--space-3)}
736
671
  }
737
672
 
738
673
  /* Light theme — the same three parts, redistributed.
@@ -769,6 +704,23 @@
769
704
  --gradient-chrome:linear-gradient(to right,#0a0a0a,rgb(10 10 10 / .8),rgb(10 10 10 / .6));
770
705
  --gradient-chrome-2:linear-gradient(to right,#0a0a0a,var(--neutral-500));
771
706
  --gradient-protect:linear-gradient(to bottom,rgb(0 0 0 / .10),transparent);
707
+ /* Light glass is the same idea from the other side: the fill goes white, the
708
+ edge and the lamps go dark. */
709
+ --chrome:rgb(255 255 255 / .72);
710
+ --pane-edge:rgb(0 0 0 / .08);
711
+ --pane-fill:var(--chrome);
712
+ --pane-solid:var(--background);
713
+ --pane-floor:var(--surface-card);
714
+ --pane-ground:radial-gradient(80rem 40rem at 12% -10%,rgb(0 0 0 / .03),transparent 60%),radial-gradient(60rem 34rem at 92% 108%,rgb(0 0 0 / .022),transparent 60%);
715
+ }
716
+
717
+ /* No transparency when asked for none, or where nothing can blur behind it:
718
+ a translucent fill with no blur is the page read through the words. */
719
+ @media (prefers-reduced-transparency:reduce){
720
+ :root,:root.light{--pane-fill:var(--pane-solid);--pane-blur:none}
721
+ }
722
+ @supports not ((backdrop-filter:blur(1px)) or (-webkit-backdrop-filter:blur(1px))){
723
+ :root,:root.light{--pane-fill:var(--pane-solid)}
772
724
  }
773
725
 
774
726
  /* ── tokens/motion.css ─────────────────────────────────────── */
@@ -830,6 +782,18 @@
830
782
  --z-popover:700; /* popovers, tooltips (also when anchored in modals) */
831
783
  --z-toast:800; /* toasts / notifications — always on top */
832
784
 
785
+ /* What a PAGE draws over itself, between --z-raised and --z-sticky: a panel
786
+ under its scrim under a drawer, then a sheet, the dock and a banner. All of
787
+ it stays under the header and every overlay above. --z-under sits behind
788
+ the flow, for a backdrop. */
789
+ --z-under:-10;
790
+ --z-panel:18;
791
+ --z-scrim:19;
792
+ --z-drawer:20;
793
+ --z-sheet:30;
794
+ --z-dock:40;
795
+ --z-banner:50;
796
+
833
797
  /* @hanzo/brand spells the top two rungs --z-tooltip and --z-notification.
834
798
  Same rungs, so a component written against either vocabulary stacks
835
799
  identically. --z-popover / --z-toast are canonical. */
@@ -855,6 +819,15 @@
855
819
  remains the one place the faces are AUTHORED; this is a copy of its output,
856
820
  the same way the colour tokens are copied rather than fetched.
857
821
 
822
+ A copy is only as good as what it copied from, and this one went stale: the
823
+ floor sat at @hanzo/font ^1.8.8, whose faces carry Vercel's vendor tag under
824
+ our filenames, so every `npm run gen` overwrote the good committed Zen with
825
+ Geist and 0.5.23 and 0.5.24 both went out that way — the release commits hold
826
+ the right bytes, the tarballs did not. Two things stop it recurring, each in
827
+ its own place: from 1.9.2 the font package's build refuses to publish a face
828
+ that is not its own, and gen-tokens.mjs refuses to copy from anything below
829
+ that floor.
830
+
858
831
  Why self-hosted rather than a third-party CDN:
859
832
  - A sign-in page must not make a third-party request. hanzoai/id refused to
860
833
  import this file for exactly that reason, which split the token layer: id
@@ -881,9 +854,9 @@
881
854
 
882
855
 
883
856
  :root{
884
- --font-sans:"Zen",ui-sans-serif,system-ui,sans-serif;
857
+ --font-sans:Zen, -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, "Helvetica Neue", Arial, sans-serif;
885
858
  --font-display:var(--font-sans);
886
- --font-mono:"Zen Mono",ui-monospace,SFMono-Regular,monospace;
859
+ --font-mono:Zen Mono, ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, "Liberation Mono", monospace;
887
860
  --font-serif:Georgia,serif;
888
861
  /* The default is Zen as drawn, and that is a decision rather than an omission.
889
862
  `base.css` puts this on <body>, so whatever is here is the voice of every
@@ -905,6 +878,9 @@
905
878
  in this list. */
906
879
  --font-feature-settings:normal;
907
880
  }
881
+ /* The three voices above, set by the host's `data-brand` on <html>. */
882
+ [data-brand=hanzo]{--font-feature-settings:"ss01"}
883
+ [data-brand=lux]{--font-feature-settings:"ss04"}
908
884
 
909
885
  /* ── tokens/base.css ─────────────────────────────────────── */
910
886
  /* Minimal element defaults so specimen cards and kits inherit the brand without
@@ -932,21 +908,41 @@
932
908
  *{box-sizing:border-box;border-color:var(--border)}
933
909
  html{
934
910
  -webkit-font-smoothing:antialiased;
911
+ -moz-osx-font-smoothing:grayscale;
935
912
  text-rendering:optimizeLegibility;
913
+ /* The page never scrolls sideways. On the root box as well as <body>,
914
+ because the scrollport is the documentElement; `clip` rather than
915
+ `hidden`, so neither becomes a scroll container and `sticky` still
916
+ holds. */
917
+ overflow-x:clip;
936
918
  scroll-behavior:smooth;
937
919
  /* iOS inflates text when a phone is rotated to landscape unless this is
938
920
  pinned. It is the single most common reason a mobile layout that was
939
921
  verified in portrait comes apart on its side. */
940
922
  -webkit-text-size-adjust:100%;
923
+ /* A scroll the browser makes for the page — an anchor jump, focus moving,
924
+ scrollIntoView — lands clear of a fixed header or dock. A surface that
925
+ has one sets --header / --dock; one that has none scrolls to the edge. */
926
+ scroll-padding-block:var(--header,0px) var(--dock,0px);
927
+ }
928
+ /* A slide settles where it begins: a page whose sections state
929
+ `scroll-snap-align` is the scroller's half here. `proximity`, never
930
+ `mandatory`, so a long section never holds a reader; a page with no snap
931
+ points is untouched. */
932
+ @media (min-width:1024px) and (prefers-reduced-motion:no-preference){
933
+ :where(html){scroll-snap-type:y proximity}
941
934
  }
942
935
  /* Each font-family carries the stack as a literal fallback. --font-sans lives in
943
936
  tokens/fonts.css, which a surface may legitimately import separately; without
944
937
  the fallback an unresolved var() makes font-family invalid and the UA drops to
945
938
  its SERIF default — the whole console silently rendered in Times. */
946
- body{margin:0;background:var(--background);color:var(--foreground);font-family:var(--font-sans,ui-sans-serif,system-ui,sans-serif);font-size:var(--text-base);line-height:var(--leading-base);font-feature-settings:var(--font-feature-settings);overflow-wrap:break-word}
939
+ /* The insets keep content out of the notch and the home indicator on a page
940
+ drawn under them (viewport-fit=cover); env() is 0 on a flat screen. The
941
+ top one is the header's to pay. */
942
+ body{margin:0;padding:0 var(--safe-right,0px) var(--safe-bottom,0px) var(--safe-left,0px);overflow-x:clip;background:var(--background);color:var(--foreground);font-family:var(--font-sans,ui-sans-serif,system-ui,sans-serif);font-size:var(--text-base);line-height:var(--leading-base);font-feature-settings:var(--font-feature-settings);overflow-wrap:break-word}
947
943
  h1,h2,h3,h4{margin:0;font-family:var(--font-display,var(--font-sans,ui-sans-serif,system-ui,sans-serif));letter-spacing:var(--tracking-tight);color:var(--text-primary)}
948
944
  p{margin:0;text-wrap:pretty}
949
- code,pre,kbd{font-family:var(--font-mono,ui-monospace,SFMono-Regular,monospace)}
945
+ code,pre,kbd,samp{font-family:var(--font-mono,ui-monospace,SFMono-Regular,monospace)}
950
946
  /* No `transition` here: the shared interactive rule below states one, and an
951
947
  `a` selector (0,0,1) would outrank that :where() (0,0,0) and replace the
952
948
  whole shorthand — anchors would transition colour and nothing else. */
@@ -958,7 +954,9 @@
958
954
  is a component states its own hover. The old rule also re-set `color` to the
959
955
  value `a` already has — a no-op that only served to outrank a component's own
960
956
  hover colour. */
961
- :is(p,li,blockquote,dd,dt,td,th,figcaption) a:hover{text-decoration:underline}
957
+ /* A list or a cell inside chrome is not running text: nav, aside, header and
958
+ the shell's slots carry links that never underline. */
959
+ :is(p,li,blockquote,dd,dt,td,th,figcaption) a:hover:not(:is(nav,aside,header,[role=navigation],[data-hanzo-shell],[data-slot=rail],[data-slot=bar]) a){text-decoration:underline}
962
960
 
963
961
  /* ——— the smooth ———
964
962
  Every interactive element in the reference transitions its colour, its fill
@@ -987,6 +985,16 @@
987
985
  transform:scale(.98);
988
986
  }
989
987
 
988
+ /* ——— the tap ———
989
+ No grey flash where a finger lands, and no wait for a double-tap zoom on
990
+ a control. A dialog or drawer scrolls itself and stops at its end rather
991
+ than carrying the page behind it along. */
992
+ :where(a,button,[role=button],input,select,textarea,summary,label){
993
+ -webkit-tap-highlight-color:transparent;
994
+ touch-action:manipulation;
995
+ }
996
+ :where(dialog,[role=dialog],[data-drawer]){overscroll-behavior:contain}
997
+
990
998
  /* ——— controls ———
991
999
  A bare <input> renders in the UA's face (Arial) at the UA's size with the
992
1000
  UA's border, which puts two typefaces and a foreign blue focus ring inside
@@ -1048,12 +1056,39 @@
1048
1056
  :where(input,textarea)::placeholder{color:var(--text-disabled)}
1049
1057
  :where(input,select,textarea,button):disabled{opacity:.5;cursor:not-allowed}
1050
1058
  :where(button,[role=button],summary,label,select){cursor:pointer}
1059
+ /* A bare <button> loses the UA's outset bevel and grey fill. A button that
1060
+ wants an edge or a ground states one, and any rule at all outranks this. */
1061
+ :where(button){border:0;background:none}
1062
+
1063
+ /* ——— the pointer floor ———
1064
+ WCAG 2.5.8 AA's 24px, for every pointer; the thumb's 44px is below, under
1065
+ pointer:coarse. A min-height cannot size an inline box, so a header or
1066
+ footer link is drawn as a box first. An icon-only control has no text to
1067
+ widen it, so it is a centred box too.
1068
+
1069
+ `:only-child` counts ELEMENTS: `<a>Docs <svg/></a>` matches as well, and a
1070
+ label+icon control states its own `justify-content`. */
1071
+ :where(button,[role=button],header a[href],footer a[href]){min-height:24px}
1072
+ :where(header a[href],footer a[href]){display:inline-flex;align-items:center}
1073
+ :where(:is(a[href],button,[role=button]):has(> svg:only-child)){display:inline-flex;align-items:center;justify-content:center}
1074
+
1075
+ /* MONO KEEPS ITS CELLS. Zen Mono fuses `--` into one glyph, and a brand's
1076
+ --font-feature-settings rides down into it from <body>, so a command meant
1077
+ to be retyped draws one column short. Wherever the mono face is asked for —
1078
+ these elements, a gui Text on the `$mono` face (gui names that face's scope
1079
+ `font_mono`), or a style naming the token — every character keeps its cell.
1080
+ After the controls' `font:inherit`, which would otherwise hand a mono
1081
+ button its parent's ligatures back. */
1082
+ :where(code,pre,kbd,samp,.font_mono,[style*='--font-mono']){font-variant-ligatures:none;font-feature-settings:normal}
1051
1083
 
1052
1084
  /* ——— media ———
1053
1085
  An image, a video or an embed at its intrinsic width is the usual cause of a
1054
1086
  390px page that scrolls sideways. */
1055
1087
  :where(img,svg,video,canvas,iframe,picture,object){max-width:100%}
1056
- :where(img,video){height:auto}
1088
+ /* `auto` keeps the aspect when max-width narrows the box. An element given a
1089
+ height and no width keeps the height it was given: an SVG mark has no
1090
+ intrinsic size, and `auto` collapses it to 0x0. */
1091
+ :where(:is(img,video):is(:not([height]),[width])){height:auto}
1057
1092
  /* A code block scrolls ITSELF rather than widening the document. */
1058
1093
  :where(pre){overflow-x:auto}
1059
1094
 
@@ -1068,6 +1103,13 @@
1068
1103
  input:not([type=checkbox]):not([type=radio]):not([type=hidden])){
1069
1104
  min-height:var(--tap-target);
1070
1105
  }
1106
+ /* @hanzo/gui writes a control's size INLINE, and only an important
1107
+ declaration outranks an inline one, so these are the floor that holds
1108
+ on a gui control. A switch or checkbox keeps its own box: the button IS
1109
+ the track, and the pseudo-element below widens its target. A radio drawn
1110
+ as a button — a segment of a toggle group — is a control like any other. */
1111
+ :where(button:not([role=switch],[role=checkbox]),[role=button],select,header a[href],footer a[href]){min-height:var(--tap-target) !important}
1112
+ :where(:is(a[href],button,[role=button]):has(> svg:only-child)){min-width:var(--tap-target) !important}
1071
1113
  :where(input,select,textarea){font-size:var(--text-control)}
1072
1114
  /* A checkbox, radio or switch must NOT grow to 44px — the box is 16px
1073
1115
  because that is what a checkbox looks like. What has to reach 44px is the
@@ -1102,6 +1144,9 @@
1102
1144
  is why it survived review in both files; the defect existed only in their
1103
1145
  order. One rule cannot disagree with itself. */
1104
1146
  :focus-visible{outline:2px solid var(--ring);outline-offset:var(--ring-offset,2px)}
1147
+ /* The ring's colour at REST too, so a control that transitions colours has
1148
+ no currentColor -> ring interpolation to flash when focus lands. */
1149
+ :where(*){outline-color:var(--ring)}
1105
1150
  /* --white-20 is white-on-white in the light theme, so selection reads through
1106
1151
  --selection, which BOTH themes define. */
1107
1152
  ::selection{background:var(--selection);color:var(--text-primary)}