@hanzo/design 0.4.13 → 0.5.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.
@@ -140,40 +140,6 @@ const pass = (msg) => console.log(` ok ${msg}`)
140
140
  : fail('base.css is UNLAYERED — its element rules outrank every utility an app writes')
141
141
  }
142
142
 
143
- // ── 1d. exactly ONE rule decides focus ───────────────────────────────────
144
- // Until 0.4.9 there were two: a field rule that suppressed the outline and drew
145
- // a brightened edge + halo, and the generic ring. Both computed to (0,1,0) —
146
- // :where() zeroes its contents and each side keeps one pseudo-class — so the
147
- // cascade fell through to SOURCE ORDER inside @layer base, the generic rule was
148
- // written later, and it overrode the `outline:none` the field rule stated
149
- // expressly to prevent it. Every focused input on every consumer drew BOTH.
150
- // Each rule read as correct alone; the defect existed only in their order,
151
- // which is why it survived review in both files.
152
- //
153
- // Order is not testable as intent, so this tests the property that replaced it:
154
- // one rule paints the indicator, and nothing else touches focus. A second rule
155
- // is how they disagree, `outline:none` is how an indicator disappears, and
156
- // box-shadow is how a second one appears — none of the three can return quietly.
157
- {
158
- const base = strip(read(join(tokensDir, 'base.css')))
159
- // `outline-offset` is a different property and never matches: the colon must
160
- // follow `outline` itself.
161
- const OUTLINE = /(?:^|[;{\s])outline\s*:\s*([^;}]+)/
162
- const rules = [...base.matchAll(/([^{}]+)\{([^{}]*)\}/g)]
163
- .map(([, sel, body]) => ({ sel: sel.trim().replace(/\s+/g, ' '), body }))
164
- .filter(({ sel }) => sel.includes(':focus-visible'))
165
-
166
- const paints = rules.filter(({ body }) => { const m = body.match(OUTLINE); return m && m[1].trim() !== 'none' })
167
- const mutes = rules.filter(({ body }) => { const m = body.match(OUTLINE); return m && m[1].trim() === 'none' })
168
- const halos = rules.filter(({ body }) => /(?:^|[;{\s])box-shadow\s*:/.test(body))
169
-
170
- paints.length === 1
171
- ? pass(`one focus indicator, one rule: \`${paints[0].sel}\``)
172
- : fail(`${paints.length} rules paint a focus outline (${paints.map((r) => r.sel).join(' / ')}) — equal specificity inside @layer base, so source order decides which one a user actually sees`)
173
- mutes.forEach(({ sel }) => fail(`\`${sel}\` sets outline:none — it removes the focus indicator instead of replacing it`))
174
- halos.forEach(({ sel }) => fail(`\`${sel}\` adds a box-shadow on focus — a second indicator beside the ring`))
175
- }
176
-
177
143
  // ── 2. every var() used inside the token layer must resolve ──────────────
178
144
  {
179
145
  const declared = new Set()
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  name: design-system
3
3
  description: "Use whenever you generate, edit, or review a Hanzo user interface — a page, component, screen, email, or any code change touching colour, type, spacing, elevation, motion, or stacking order. Use it when PLANNING UI work too, so the plan names tokens rather than values. Teaches the Hanzo token layer (@hanzo/design, derived from @hanzo/brand plus @hanzo/logo) and runs the linter that proves generated code actually reaches it. Triggers — build a page, add a component, style, theme, dark mode, colour, hex, palette, font size, spacing, padding, z-index, modal, dropdown, toast, button, card, dialog, icon, make it look Hanzo, brand, design review."
4
- license: MIT OR Apache-2.0
4
+ license: BSD-3-Clause
5
5
  ---
6
6
 
7
7
  # The Hanzo design system
package/src/brand.ts ADDED
@@ -0,0 +1,76 @@
1
+ // A brand's declared theme, projected onto the semantic tokens.
2
+ //
3
+ // A brand package (@luxfi/brand, @zooai/brand) carries its own light and dark
4
+ // theme in brand.json. This is the ONE place that says which semantic token each
5
+ // of those answers for — the design system owns the token contract, a brand owns
6
+ // the values, and neither holds a copy of the other.
7
+ //
8
+ // It exists because that mapping was about to be written twice: once in each
9
+ // brand package to generate a stylesheet, and once in every app that resolves its
10
+ // brand at runtime and cannot import a build-time stylesheet. Two copies of a
11
+ // mapping is how a brand comes to look like itself in one surface and not
12
+ // another.
13
+
14
+ /** The palette a brand declares. Every field is optional: an undeclared key
15
+ * leaves the design system's own value standing rather than overwriting it. */
16
+ export interface BrandTheme {
17
+ surface1?: string
18
+ surface2?: string
19
+ surface3?: string
20
+ neutral1?: string
21
+ neutral2?: string
22
+ neutral3?: string
23
+ accent1?: string
24
+ accent2?: string
25
+ accent3?: string
26
+ border?: string
27
+ success?: string
28
+ warning?: string
29
+ error?: string
30
+ }
31
+
32
+ /** theme key → the semantic tokens it answers for. */
33
+ const OWNS: Record<keyof BrandTheme, readonly string[]> = {
34
+ surface1: ['--background'],
35
+ surface2: ['--card', '--popover', '--muted'],
36
+ surface3: ['--accent'],
37
+ neutral1: ['--foreground', '--card-foreground', '--popover-foreground', '--accent-foreground'],
38
+ neutral2: ['--muted-foreground'],
39
+ neutral3: [],
40
+ accent1: ['--primary'],
41
+ accent2: [],
42
+ accent3: [],
43
+ border: ['--border'],
44
+ success: ['--state-success'],
45
+ warning: ['--state-warning'],
46
+ error: ['--state-error'],
47
+ }
48
+
49
+ /**
50
+ * The tokens a brand's theme sets, as `{ '--background': '#000', … }`.
51
+ *
52
+ * Only ground, ink, accent and the edges drawn on them. The radius scale, the
53
+ * type ramp, spacing, motion and z are NOT here and must not be: they are the
54
+ * system's grammar rather than a brand's voice, and a brand that redefines them
55
+ * is a fork wearing a stylesheet.
56
+ */
57
+ export function themeToTokens(theme: BrandTheme): Record<string, string> {
58
+ const out: Record<string, string> = {}
59
+ for (const [key, tokens] of Object.entries(OWNS) as [keyof BrandTheme, readonly string[]][]) {
60
+ const value = theme[key]
61
+ if (!value) continue
62
+ for (const token of tokens) out[token] = value
63
+ }
64
+ // --primary carries ink on top of it, so the accent names its own contrast
65
+ // partner rather than leaving whatever the previous theme set.
66
+ if (theme.accent1 && theme.surface1) out['--primary-foreground'] = theme.surface1
67
+ return out
68
+ }
69
+
70
+ /** Apply a brand's theme to a live document — for an app that resolves its brand
71
+ * at runtime and so cannot import a build-time stylesheet. */
72
+ export function applyBrandTheme(theme: BrandTheme, el: HTMLElement): void {
73
+ for (const [name, value] of Object.entries(themeToTokens(theme))) {
74
+ el.style.setProperty(name, value)
75
+ }
76
+ }
package/src/index.ts CHANGED
@@ -10,6 +10,7 @@
10
10
  // import { colors, spacing, radius, cssVar } from '@hanzo/design' // the code layer
11
11
  //
12
12
  export * from './tokens.gen.js'
13
+ export * from './brand.js'
13
14
  import { cssVars, type CssVarName } from './tokens.gen.js'
14
15
 
15
16
  /** A token name with the leading `--` omitted: `'background'` for `'--background'`. */
@@ -62,9 +63,3 @@ export function injectDesignCss(href: string): void {
62
63
  l.setAttribute('data-hanzo-design', '')
63
64
  document.head.appendChild(l)
64
65
  }
65
-
66
- // A person's own reading of the system — type size, density, accent — as CSS
67
- // custom properties. Pure: it maps a preference to variables and returns them,
68
- // so an app, an embedded preview and a server render all apply it the same way.
69
- export { vars, css, isColor, TYPE_MIN, TYPE_MAX } from './preference.js';
70
- export type { Preference, Density } from './preference.js';
package/src/tokens.gen.ts CHANGED
@@ -96,32 +96,31 @@ export const colors = {
96
96
 
97
97
  /** typography tokens (from tokens/typography.css). Values are raw CSS. */
98
98
  export const typography = {
99
- 'type-scale': '1',
100
- 'text-xs': 'calc(0.6875rem * var(--type-scale, 1))',
101
- 'leading-xs': 'calc(1rem * var(--type-scale, 1))',
102
- 'text-sm': 'calc(0.8125rem * var(--type-scale, 1))',
103
- 'leading-sm': 'calc(1.15rem * var(--type-scale, 1))',
104
- 'text-base': 'calc(0.875rem * var(--type-scale, 1))',
105
- 'leading-base': 'calc(1.35rem * var(--type-scale, 1))',
106
- 'text-lg': 'calc(0.9375rem * var(--type-scale, 1))',
107
- 'leading-lg': 'calc(1.4rem * var(--type-scale, 1))',
108
- 'text-xl': 'calc(1.0625rem * var(--type-scale, 1))',
109
- 'leading-xl': 'calc(1.55rem * var(--type-scale, 1))',
110
- 'text-2xl': 'calc(1.3125rem * var(--type-scale, 1))',
111
- 'leading-2xl': 'calc(1.7rem * var(--type-scale, 1))',
112
- 'text-3xl': 'calc(1.625rem * var(--type-scale, 1))',
113
- 'leading-3xl': 'calc(1.95rem * var(--type-scale, 1))',
114
- 'text-4xl': 'calc(2rem * var(--type-scale, 1))',
115
- 'leading-4xl': 'calc(2.25rem * var(--type-scale, 1))',
116
- 'text-5xl': 'calc(2.5rem * var(--type-scale, 1))',
99
+ 'text-xs': '0.6875rem',
100
+ 'leading-xs': '1rem',
101
+ 'text-sm': '0.8125rem',
102
+ 'leading-sm': '1.15rem',
103
+ 'text-base': '0.875rem',
104
+ 'leading-base': '1.35rem',
105
+ 'text-lg': '0.9375rem',
106
+ 'leading-lg': '1.4rem',
107
+ 'text-xl': '1.0625rem',
108
+ 'leading-xl': '1.55rem',
109
+ 'text-2xl': '1.3125rem',
110
+ 'leading-2xl': '1.7rem',
111
+ 'text-3xl': '1.625rem',
112
+ 'leading-3xl': '1.95rem',
113
+ 'text-4xl': '2rem',
114
+ 'leading-4xl': '2.25rem',
115
+ 'text-5xl': '2.5rem',
117
116
  'leading-5xl': '1.05',
118
- 'text-6xl': 'calc(3.25rem * var(--type-scale, 1))',
117
+ 'text-6xl': '3.25rem',
119
118
  'leading-6xl': '1',
120
- 'text-7xl': 'calc(4rem * var(--type-scale, 1))',
119
+ 'text-7xl': '4rem',
121
120
  'leading-7xl': '1',
122
- 'text-8xl': 'calc(5.25rem * var(--type-scale, 1))',
121
+ 'text-8xl': '5.25rem',
123
122
  'leading-8xl': '1',
124
- 'text-9xl': 'calc(7rem * var(--type-scale, 1))',
123
+ 'text-9xl': '7rem',
125
124
  'leading-9xl': '1',
126
125
  'font-size-xs': 'var(--text-xs)',
127
126
  'font-size-sm': 'var(--text-sm)',
@@ -163,22 +162,21 @@ export const typography = {
163
162
 
164
163
  /** spacing tokens (from tokens/spacing.css). Values are raw CSS. */
165
164
  export const spacing = {
166
- 'density': '1',
167
165
  'space-0': '0',
168
- 'space-1': 'calc(0.25rem * var(--density, 1))',
169
- 'space-2': 'calc(0.5rem * var(--density, 1))',
170
- 'space-3': 'calc(0.75rem * var(--density, 1))',
171
- 'space-4': 'calc(1rem * var(--density, 1))',
172
- 'space-5': 'calc(1.25rem * var(--density, 1))',
173
- 'space-6': 'calc(1.5rem * var(--density, 1))',
174
- 'space-8': 'calc(2rem * var(--density, 1))',
175
- 'space-10': 'calc(2.5rem * var(--density, 1))',
176
- 'space-12': 'calc(3rem * var(--density, 1))',
177
- 'space-14': 'calc(3.5rem * var(--density, 1))',
178
- 'space-16': 'calc(4rem * var(--density, 1))',
179
- 'space-20': 'calc(5rem * var(--density, 1))',
180
- 'space-24': 'calc(6rem * var(--density, 1))',
181
- 'space-32': 'calc(8rem * var(--density, 1))',
166
+ 'space-1': '0.25rem',
167
+ 'space-2': '0.5rem',
168
+ 'space-3': '0.75rem',
169
+ 'space-4': '1rem',
170
+ 'space-5': '1.25rem',
171
+ 'space-6': '1.5rem',
172
+ 'space-8': '2rem',
173
+ 'space-10': '2.5rem',
174
+ 'space-12': '3rem',
175
+ 'space-14': '3.5rem',
176
+ 'space-16': '4rem',
177
+ 'space-20': '5rem',
178
+ 'space-24': '6rem',
179
+ 'space-32': '8rem',
182
180
  'golden-1': '0.25rem',
183
181
  'golden-2': '0.405rem',
184
182
  'golden-3': '0.654rem',
@@ -392,32 +390,31 @@ export const cssVars = {
392
390
  '--chrome-dot-red': 'rgb(239 68 68 / .6)',
393
391
  '--chrome-dot-yellow': 'rgb(234 179 8 / .6)',
394
392
  '--chrome-dot-green': 'rgb(34 197 94 / .6)',
395
- '--type-scale': '1',
396
- '--text-xs': 'calc(0.6875rem * var(--type-scale, 1))',
397
- '--leading-xs': 'calc(1rem * var(--type-scale, 1))',
398
- '--text-sm': 'calc(0.8125rem * var(--type-scale, 1))',
399
- '--leading-sm': 'calc(1.15rem * var(--type-scale, 1))',
400
- '--text-base': 'calc(0.875rem * var(--type-scale, 1))',
401
- '--leading-base': 'calc(1.35rem * var(--type-scale, 1))',
402
- '--text-lg': 'calc(0.9375rem * var(--type-scale, 1))',
403
- '--leading-lg': 'calc(1.4rem * var(--type-scale, 1))',
404
- '--text-xl': 'calc(1.0625rem * var(--type-scale, 1))',
405
- '--leading-xl': 'calc(1.55rem * var(--type-scale, 1))',
406
- '--text-2xl': 'calc(1.3125rem * var(--type-scale, 1))',
407
- '--leading-2xl': 'calc(1.7rem * var(--type-scale, 1))',
408
- '--text-3xl': 'calc(1.625rem * var(--type-scale, 1))',
409
- '--leading-3xl': 'calc(1.95rem * var(--type-scale, 1))',
410
- '--text-4xl': 'calc(2rem * var(--type-scale, 1))',
411
- '--leading-4xl': 'calc(2.25rem * var(--type-scale, 1))',
412
- '--text-5xl': 'calc(2.5rem * var(--type-scale, 1))',
393
+ '--text-xs': '0.6875rem',
394
+ '--leading-xs': '1rem',
395
+ '--text-sm': '0.8125rem',
396
+ '--leading-sm': '1.15rem',
397
+ '--text-base': '0.875rem',
398
+ '--leading-base': '1.35rem',
399
+ '--text-lg': '0.9375rem',
400
+ '--leading-lg': '1.4rem',
401
+ '--text-xl': '1.0625rem',
402
+ '--leading-xl': '1.55rem',
403
+ '--text-2xl': '1.3125rem',
404
+ '--leading-2xl': '1.7rem',
405
+ '--text-3xl': '1.625rem',
406
+ '--leading-3xl': '1.95rem',
407
+ '--text-4xl': '2rem',
408
+ '--leading-4xl': '2.25rem',
409
+ '--text-5xl': '2.5rem',
413
410
  '--leading-5xl': '1.05',
414
- '--text-6xl': 'calc(3.25rem * var(--type-scale, 1))',
411
+ '--text-6xl': '3.25rem',
415
412
  '--leading-6xl': '1',
416
- '--text-7xl': 'calc(4rem * var(--type-scale, 1))',
413
+ '--text-7xl': '4rem',
417
414
  '--leading-7xl': '1',
418
- '--text-8xl': 'calc(5.25rem * var(--type-scale, 1))',
415
+ '--text-8xl': '5.25rem',
419
416
  '--leading-8xl': '1',
420
- '--text-9xl': 'calc(7rem * var(--type-scale, 1))',
417
+ '--text-9xl': '7rem',
421
418
  '--leading-9xl': '1',
422
419
  '--font-size-xs': 'var(--text-xs)',
423
420
  '--font-size-sm': 'var(--text-sm)',
@@ -455,22 +452,21 @@ export const cssVars = {
455
452
  '--type-caption': '400 var(--text-xs)/var(--leading-xs) var(--font-sans)',
456
453
  '--type-code': '400 var(--text-sm)/var(--leading-relaxed) var(--font-mono)',
457
454
  '--type-eyebrow': '600 0.625rem/1 var(--font-sans)',
458
- '--density': '1',
459
455
  '--space-0': '0',
460
- '--space-1': 'calc(0.25rem * var(--density, 1))',
461
- '--space-2': 'calc(0.5rem * var(--density, 1))',
462
- '--space-3': 'calc(0.75rem * var(--density, 1))',
463
- '--space-4': 'calc(1rem * var(--density, 1))',
464
- '--space-5': 'calc(1.25rem * var(--density, 1))',
465
- '--space-6': 'calc(1.5rem * var(--density, 1))',
466
- '--space-8': 'calc(2rem * var(--density, 1))',
467
- '--space-10': 'calc(2.5rem * var(--density, 1))',
468
- '--space-12': 'calc(3rem * var(--density, 1))',
469
- '--space-14': 'calc(3.5rem * var(--density, 1))',
470
- '--space-16': 'calc(4rem * var(--density, 1))',
471
- '--space-20': 'calc(5rem * var(--density, 1))',
472
- '--space-24': 'calc(6rem * var(--density, 1))',
473
- '--space-32': 'calc(8rem * var(--density, 1))',
456
+ '--space-1': '0.25rem',
457
+ '--space-2': '0.5rem',
458
+ '--space-3': '0.75rem',
459
+ '--space-4': '1rem',
460
+ '--space-5': '1.25rem',
461
+ '--space-6': '1.5rem',
462
+ '--space-8': '2rem',
463
+ '--space-10': '2.5rem',
464
+ '--space-12': '3rem',
465
+ '--space-14': '3.5rem',
466
+ '--space-16': '4rem',
467
+ '--space-20': '5rem',
468
+ '--space-24': '6rem',
469
+ '--space-32': '8rem',
474
470
  '--golden-1': '0.25rem',
475
471
  '--golden-2': '0.405rem',
476
472
  '--golden-3': '0.654rem',
package/styles.css CHANGED
@@ -229,23 +229,23 @@
229
229
  silent (the border simply stops existing) and has shipped before. */
230
230
  .light{
231
231
  color-scheme:light;
232
- --background:#f7f7f7;
232
+ --background:#ffffff;
233
233
  --foreground:#0a0a0a;
234
234
  /* A ladder, not four names for #f5f5f5. Light lifts by the same tiny steps
235
235
  dark does — ~2% per rung — so --surface-0..3 mean something in both themes. */
236
- --card:#f2f2f2;
236
+ --card:#fafafa;
237
237
  --card-foreground:#0a0a0a;
238
- --popover:#fbfbfb;
238
+ --popover:#ffffff;
239
239
  --popover-foreground:#0a0a0a;
240
240
  --primary:#0a0a0a;
241
241
  --primary-hover:#262626;
242
242
  --primary-foreground:#fafafa;
243
- --secondary:#e4e4e4;
243
+ --secondary:#ededed;
244
244
  --secondary-hover:#e0e0e0;
245
245
  --secondary-foreground:#0a0a0a;
246
- --muted:#ededed;
246
+ --muted:#f5f5f5;
247
247
  --muted-foreground:#525252;
248
- --accent:#e4e4e4;
248
+ --accent:#ededed;
249
249
  --accent-foreground:#0a0a0a;
250
250
  --destructive:var(--state-error);
251
251
  --destructive-hover:#dc2626;
@@ -270,9 +270,9 @@
270
270
  --selection:rgb(0 0 0 / .16);
271
271
  --glass:rgb(0 0 0 / .04);
272
272
  --glass-strong:rgb(0 0 0 / .07);
273
- --surface-card:#f2f2f2;
274
- --surface-card-emphasis:#fdfdfd;
275
- --surface-card-quiet:#f5f5f5;
273
+ --surface-card:#fafafa;
274
+ --surface-card-emphasis:#ffffff;
275
+ --surface-card-quiet:#fcfcfc;
276
276
  --surface-overlay:rgb(255 255 255 / .95);
277
277
  --surface-header:rgb(255 255 255 / .8);
278
278
  --surface-scrim:rgb(0 0 0 / .5);
@@ -290,25 +290,19 @@
290
290
  — the two are the SAME scale, mirrored. A surface/tenant overrides any --text-*
291
291
  on :root to retune density on demand. */
292
292
  :root{
293
- /* The type knob. 1 is the published scale; every rung below multiplies by
294
- it, so a surface or a person retunes the WHOLE ramp by setting one
295
- number and no rung can drift out of relation with the others.
296
- @hanzo/design's `vars({type})` emits exactly this name. */
297
- --type-scale:1;
298
-
299
- --text-xs:calc(0.6875rem * var(--type-scale, 1)); --leading-xs:calc(1rem * var(--type-scale, 1)); /* 11px — eyebrows / section labels */
300
- --text-sm:calc(0.8125rem * var(--type-scale, 1)); --leading-sm:calc(1.15rem * var(--type-scale, 1)); /* 13px — nav labels, dense body */
301
- --text-base:calc(0.875rem * var(--type-scale, 1)); --leading-base:calc(1.35rem * var(--type-scale, 1)); /* 14px — base app text (was 16px) */
302
- --text-lg:calc(0.9375rem * var(--type-scale, 1)); --leading-lg:calc(1.4rem * var(--type-scale, 1)); /* 15px */
303
- --text-xl:calc(1.0625rem * var(--type-scale, 1)); --leading-xl:calc(1.55rem * var(--type-scale, 1)); /* 17px */
304
- --text-2xl:calc(1.3125rem * var(--type-scale, 1)); --leading-2xl:calc(1.7rem * var(--type-scale, 1)); /* 21px */
305
- --text-3xl:calc(1.625rem * var(--type-scale, 1)); --leading-3xl:calc(1.95rem * var(--type-scale, 1)); /* 26px */
306
- --text-4xl:calc(2rem * var(--type-scale, 1)); --leading-4xl:calc(2.25rem * var(--type-scale, 1)); /* 32px */
307
- --text-5xl:calc(2.5rem * var(--type-scale, 1)); --leading-5xl:1.05; /* 40px */
308
- --text-6xl:calc(3.25rem * var(--type-scale, 1)); --leading-6xl:1; /* 52px */
309
- --text-7xl:calc(4rem * var(--type-scale, 1)); --leading-7xl:1; /* 64px */
310
- --text-8xl:calc(5.25rem * var(--type-scale, 1)); --leading-8xl:1; /* 84px */
311
- --text-9xl:calc(7rem * var(--type-scale, 1)); --leading-9xl:1; /* 112px */
293
+ --text-xs:0.6875rem; --leading-xs:1rem; /* 11px eyebrows / section labels */
294
+ --text-sm:0.8125rem; --leading-sm:1.15rem; /* 13px nav labels, dense body */
295
+ --text-base:0.875rem; --leading-base:1.35rem; /* 14px base app text (was 16px) */
296
+ --text-lg:0.9375rem; --leading-lg:1.4rem; /* 15px */
297
+ --text-xl:1.0625rem; --leading-xl:1.55rem; /* 17px */
298
+ --text-2xl:1.3125rem; --leading-2xl:1.7rem; /* 21px */
299
+ --text-3xl:1.625rem; --leading-3xl:1.95rem; /* 26px */
300
+ --text-4xl:2rem; --leading-4xl:2.25rem; /* 32px */
301
+ --text-5xl:2.5rem; --leading-5xl:1.05; /* 40px */
302
+ --text-6xl:3.25rem; --leading-6xl:1; /* 52px */
303
+ --text-7xl:4rem; --leading-7xl:1; /* 64px */
304
+ --text-8xl:5.25rem; --leading-8xl:1; /* 84px */
305
+ --text-9xl:7rem; --leading-9xl:1; /* 112px */
312
306
 
313
307
  /* The SAME scale under @hanzo/brand's spelling. @hanzo/gui's shell theme and
314
308
  Hanzo Studio address the ramp as --font-size-*; both names are one value, so
@@ -373,7 +367,7 @@
373
367
  form feels broken. pointer:coarse is the real signal — a desktop mouse keeps
374
368
  the compact 13px field. */
375
369
  @media (pointer:coarse){
376
- :root{--text-control:calc(1rem * var(--type-scale, 1))}
370
+ :root{--text-control:1rem}
377
371
  }
378
372
 
379
373
  /* ── tokens/spacing.css ─────────────────────────────────────── */
@@ -381,27 +375,21 @@
381
375
  declared in hanzo.ai's tailwind.config.ts (legacy v3 config, kept for
382
376
  reference) — use it for editorial layouts, not for component padding. */
383
377
  :root{
384
- /* The density knob. 1 is the published spacing; every --space-* rung below
385
- multiplies by it, and --grid-gap-* reference those rungs rather than
386
- restating them, so gaps, padding and section rhythm move together.
387
- @hanzo/design's `vars({density})` emits exactly this name. */
388
- --density:1;
389
-
390
378
  --space-0:0;
391
- --space-1:calc(0.25rem * var(--density, 1));
392
- --space-2:calc(0.5rem * var(--density, 1));
393
- --space-3:calc(0.75rem * var(--density, 1));
394
- --space-4:calc(1rem * var(--density, 1));
395
- --space-5:calc(1.25rem * var(--density, 1));
396
- --space-6:calc(1.5rem * var(--density, 1));
397
- --space-8:calc(2rem * var(--density, 1));
398
- --space-10:calc(2.5rem * var(--density, 1));
399
- --space-12:calc(3rem * var(--density, 1));
400
- --space-14:calc(3.5rem * var(--density, 1));
401
- --space-16:calc(4rem * var(--density, 1));
402
- --space-20:calc(5rem * var(--density, 1));
403
- --space-24:calc(6rem * var(--density, 1));
404
- --space-32:calc(8rem * var(--density, 1));
379
+ --space-1:0.25rem;
380
+ --space-2:0.5rem;
381
+ --space-3:0.75rem;
382
+ --space-4:1rem;
383
+ --space-5:1.25rem;
384
+ --space-6:1.5rem;
385
+ --space-8:2rem;
386
+ --space-10:2.5rem;
387
+ --space-12:3rem;
388
+ --space-14:3.5rem;
389
+ --space-16:4rem;
390
+ --space-20:5rem;
391
+ --space-24:6rem;
392
+ --space-32:8rem;
405
393
 
406
394
  /* golden ramp (φ) — hanzo.ai tailwind.config.ts */
407
395
  --golden-1:0.25rem;
@@ -846,26 +834,19 @@
846
834
  padding:0 var(--space-3);
847
835
  }
848
836
  :where(textarea){padding:var(--space-2) var(--space-3);resize:vertical}
849
- /* A field has NO focus rule of its own — see the ring at the bottom of this
850
- layer, which is the one focus indicator for everything.
851
-
852
- There used to be one here: `outline:none` plus a brightened edge (.15 ->
853
- .22) plus a soft halo, the composer look. It was removed because it could
854
- not do the job in either of the two ways that matter.
855
-
856
- It was never VISIBLE ENOUGH. Composited on --background the brightened edge
857
- measures 1.91:1 and the halo 1.25:1, against the 3:1 that WCAG 1.4.11 asks
858
- of a focus indicator and that this package already gates --ring on. The
859
- budget was documented as being spent entirely on --ring "because that is
860
- what a keyboard user navigates by" true for a button, and false for a
861
- field for exactly as long as this rule told fields not to use it.
862
-
863
- And it was SUPPRESSIBLE. It carried the indicator on `border-color`, so any
864
- app that states `border` on its own fields overrode it — @hanzo/id does,
865
- unlayered, which beats this layer whatever its specificity, and its focused
866
- fields sat at the resting .15 while both files read as correct. An outline
867
- is not a border: nothing in an app's field styling reaches it, so the ring
868
- paints whether or not the app has opinions about edges. */
837
+ /* ——— focus, on a control ———
838
+ Not the generic ring. A field already HAS an edge, so focus brightens that
839
+ edge (.15 -> .22) and adds a soft halo just outside it — which is what the
840
+ reference does (.composer-box:focus-within) and what separates a focused
841
+ field from a browser default. The generic `outline` is suppressed here
842
+ precisely because it would draw a second, harder box around this one. */
843
+ :where(input,select,textarea):focus-visible{
844
+ outline:none;
845
+ border-color:var(--border-focus);
846
+ box-shadow:var(--ring-focus);
847
+ }
848
+ /* A hovered field lifts its surface a rung. Nudging its EDGE from .15 to .16
849
+ is a state nobody can see. */
869
850
  :where(input,select,textarea):hover:not(:focus-visible):not(:disabled){background:var(--surface-3)}
870
851
  :where(input,textarea)::placeholder{color:var(--text-disabled)}
871
852
  :where(input,select,textarea,button):disabled{opacity:.5;cursor:not-allowed}
@@ -905,19 +886,6 @@
905
886
  }
906
887
  }
907
888
 
908
- /* THE focus indicator. One rule, every focusable thing, no exceptions — a
909
- button, a link, a summary, a field. 2px at --ring is 3.77:1 on the darkest
910
- canvas and clears the 2px perimeter WCAG 2.4.13 asks for; the gate in
911
- check-tokens holds --ring to that and nothing else here may weaken it.
912
-
913
- There were two rules until 0.4.9, and they collided invisibly. Both computed
914
- to (0,1,0) — :where() zeroes whatever it wraps, leaving one pseudo-class on
915
- each side — so the cascade fell through to SOURCE ORDER inside this layer,
916
- this rule was written later, and it overrode the `outline:none` the field
917
- rule stated expressly to prevent it. Every focused input on every consumer
918
- drew BOTH the ring and the edge+halo. Each rule read as correct alone, which
919
- is why it survived review in both files; the defect existed only in their
920
- order. One rule cannot disagree with itself. */
921
889
  :focus-visible{outline:2px solid var(--ring);outline-offset:2px}
922
890
  /* --white-20 is white-on-white in the light theme, so selection reads through
923
891
  --selection, which BOTH themes define. */