@hanzo/design 0.3.2 → 0.3.4

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.
@@ -178,6 +178,20 @@ export declare const spacing: {
178
178
  readonly 'hero-y-lg': "8rem";
179
179
  readonly 'header-height': "4rem";
180
180
  };
181
+ /** grid tokens (from tokens/grid.css). Values are raw CSS. */
182
+ export declare const grid: {
183
+ readonly 'grid-columns': "12";
184
+ readonly 'grid-gap': "var(--space-6)";
185
+ readonly 'grid-gap-tight': "var(--space-4)";
186
+ readonly 'grid-gap-loose': "var(--space-8)";
187
+ readonly 'breakpoint-sm': "40rem";
188
+ readonly 'breakpoint-md': "48rem";
189
+ readonly 'breakpoint-lg': "64rem";
190
+ readonly 'breakpoint-xl': "80rem";
191
+ readonly 'breakpoint-2xl': "96rem";
192
+ readonly 'grid-card-min': "18rem";
193
+ readonly 'grid-card-min-wide': "24rem";
194
+ };
181
195
  /** radius tokens (from tokens/radius.css). Values are raw CSS. */
182
196
  export declare const radius: {
183
197
  readonly radius: "0.5rem";
@@ -418,6 +432,17 @@ export declare const cssVars: {
418
432
  readonly '--hero-y': "5rem";
419
433
  readonly '--hero-y-lg': "8rem";
420
434
  readonly '--header-height': "4rem";
435
+ readonly '--grid-columns': "12";
436
+ readonly '--grid-gap': "var(--space-6)";
437
+ readonly '--grid-gap-tight': "var(--space-4)";
438
+ readonly '--grid-gap-loose': "var(--space-8)";
439
+ readonly '--breakpoint-sm': "40rem";
440
+ readonly '--breakpoint-md': "48rem";
441
+ readonly '--breakpoint-lg': "64rem";
442
+ readonly '--breakpoint-xl': "80rem";
443
+ readonly '--breakpoint-2xl': "96rem";
444
+ readonly '--grid-card-min': "18rem";
445
+ readonly '--grid-card-min-wide': "24rem";
421
446
  readonly '--radius': "0.5rem";
422
447
  readonly '--radius-sm': "0.375rem";
423
448
  readonly '--radius-md': "0.5rem";
@@ -1 +1 @@
1
- {"version":3,"file":"tokens.gen.d.ts","sourceRoot":"","sources":["../src/tokens.gen.ts"],"names":[],"mappings":"AAIA,kEAAkE;AAClE,eAAO,MAAM,MAAM;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;CA2ET,CAAA;AAEV,0EAA0E;AAC1E,eAAO,MAAM,UAAU;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;CA8Db,CAAA;AAEV,oEAAoE;AACpE,eAAO,MAAM,OAAO;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;CAqCV,CAAA;AAEV,kEAAkE;AAClE,eAAO,MAAM,MAAM;;;;;;;;;CAST,CAAA;AAEV,wEAAwE;AACxE,eAAO,MAAM,SAAS;;;;;;;;;;;;;;;;;CAiBZ,CAAA;AAEV,kEAAkE;AAClE,eAAO,MAAM,MAAM;;;;;;;;;;;CAWT,CAAA;AAEV,6DAA6D;AAC7D,eAAO,MAAM,MAAM;;;;;;;;;;;;CAYT,CAAA;AAEV,gEAAgE;AAChE,eAAO,MAAM,KAAK;;;;;;CAMR,CAAA;AAEV,8DAA8D;AAC9D,eAAO,MAAM,IAAI,IAEP,CAAA;AAEV,sFAAsF;AACtF,eAAO,MAAM,OAAO;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;CA8NV,CAAA;AAEV,MAAM,MAAM,UAAU,GAAG,MAAM,OAAO,OAAO,CAAA"}
1
+ {"version":3,"file":"tokens.gen.d.ts","sourceRoot":"","sources":["../src/tokens.gen.ts"],"names":[],"mappings":"AAIA,kEAAkE;AAClE,eAAO,MAAM,MAAM;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;CA2ET,CAAA;AAEV,0EAA0E;AAC1E,eAAO,MAAM,UAAU;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;CA8Db,CAAA;AAEV,oEAAoE;AACpE,eAAO,MAAM,OAAO;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;CAqCV,CAAA;AAEV,8DAA8D;AAC9D,eAAO,MAAM,IAAI;;;;;;;;;;;;CAYP,CAAA;AAEV,kEAAkE;AAClE,eAAO,MAAM,MAAM;;;;;;;;;CAST,CAAA;AAEV,wEAAwE;AACxE,eAAO,MAAM,SAAS;;;;;;;;;;;;;;;;;CAiBZ,CAAA;AAEV,kEAAkE;AAClE,eAAO,MAAM,MAAM;;;;;;;;;;;CAWT,CAAA;AAEV,6DAA6D;AAC7D,eAAO,MAAM,MAAM;;;;;;;;;;;;CAYT,CAAA;AAEV,gEAAgE;AAChE,eAAO,MAAM,KAAK;;;;;;CAMR,CAAA;AAEV,8DAA8D;AAC9D,eAAO,MAAM,IAAI,IAEP,CAAA;AAEV,sFAAsF;AACtF,eAAO,MAAM,OAAO;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;CAyOV,CAAA;AAEV,MAAM,MAAM,UAAU,GAAG,MAAM,OAAO,OAAO,CAAA"}
@@ -181,6 +181,20 @@ export const spacing = {
181
181
  'hero-y-lg': '8rem',
182
182
  'header-height': '4rem',
183
183
  };
184
+ /** grid tokens (from tokens/grid.css). Values are raw CSS. */
185
+ export const grid = {
186
+ 'grid-columns': '12',
187
+ 'grid-gap': 'var(--space-6)',
188
+ 'grid-gap-tight': 'var(--space-4)',
189
+ 'grid-gap-loose': 'var(--space-8)',
190
+ 'breakpoint-sm': '40rem',
191
+ 'breakpoint-md': '48rem',
192
+ 'breakpoint-lg': '64rem',
193
+ 'breakpoint-xl': '80rem',
194
+ 'breakpoint-2xl': '96rem',
195
+ 'grid-card-min': '18rem',
196
+ 'grid-card-min-wide': '24rem',
197
+ };
184
198
  /** radius tokens (from tokens/radius.css). Values are raw CSS. */
185
199
  export const radius = {
186
200
  'radius': '0.5rem',
@@ -421,6 +435,17 @@ export const cssVars = {
421
435
  '--hero-y': '5rem',
422
436
  '--hero-y-lg': '8rem',
423
437
  '--header-height': '4rem',
438
+ '--grid-columns': '12',
439
+ '--grid-gap': 'var(--space-6)',
440
+ '--grid-gap-tight': 'var(--space-4)',
441
+ '--grid-gap-loose': 'var(--space-8)',
442
+ '--breakpoint-sm': '40rem',
443
+ '--breakpoint-md': '48rem',
444
+ '--breakpoint-lg': '64rem',
445
+ '--breakpoint-xl': '80rem',
446
+ '--breakpoint-2xl': '96rem',
447
+ '--grid-card-min': '18rem',
448
+ '--grid-card-min-wide': '24rem',
424
449
  '--radius': '0.5rem',
425
450
  '--radius-sm': '0.375rem',
426
451
  '--radius-md': '0.5rem',
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@hanzo/design",
3
- "version": "0.3.2",
3
+ "version": "0.3.4",
4
4
  "packageManager": "pnpm@11.17.0",
5
5
  "description": "Hanzo Design System \u2014 monochrome, dark-default tokens + components + brand assets, the single source of truth for every Hanzo surface. CSS + typed programmatic tokens.",
6
6
  "license": "BSD-3-Clause",
@@ -68,4 +68,4 @@
68
68
  "bin": {
69
69
  "hanzo-design-lint": "./scripts/lint.mjs"
70
70
  }
71
- }
71
+ }
@@ -16,16 +16,34 @@ let failures = 0
16
16
  const fail = (msg) => { console.error(` FAIL ${msg}`); failures++ }
17
17
  const pass = (msg) => console.log(` ok ${msg}`)
18
18
 
19
- // ── 1. styles.css must serve every token file ────────────────────────────
19
+ // ── 1. styles.css must CARRY every token file ────────────────────────────
20
20
  // tokens/z.css was authored, exported and documented — and left out of
21
21
  // styles.css, so the whole ladder resolved to nothing on every consumer.
22
+ //
23
+ // The entry point is now generated and FLATTENED, so this checks for the
24
+ // declarations themselves rather than for an @import naming the file. That is
25
+ // the stronger test and it is the one that was actually needed: the old shape
26
+ // passed for years while no bundler could resolve a single token, because a
27
+ // relative url() resolves against the CONSUMER's directory. Presence of the
28
+ // import proved nothing about presence of the tokens.
22
29
  {
23
- const entry = read(join(root, 'styles.css'))
30
+ const entry = strip(read(join(root, 'styles.css')))
24
31
  const files = readdirSync(tokensDir).filter((f) => f.endsWith('.css'))
25
- const missing = files.filter((f) => !entry.includes(`tokens/${f}`))
32
+ const missing = files.filter((f) => {
33
+ const decls = [...strip(read(join(tokensDir, f))).matchAll(/--([A-Za-z0-9-]+)\s*:/g)].map((m) => m[1])
34
+ // A file with no custom properties (pure element rules, e.g. base.css) is
35
+ // carried if one of its selectors made it across.
36
+ if (!decls.length) return !entry.includes(strip(read(join(tokensDir, f))).trim().split('\n')[0].trim())
37
+ return !decls.every((d) => entry.includes(`--${d}`))
38
+ })
26
39
  missing.length
27
- ? fail(`styles.css does not import: ${missing.join(', ')}`)
28
- : pass(`styles.css serves all ${files.length} token files`)
40
+ ? fail(`styles.css does not carry the tokens from: ${missing.join(', ')}`)
41
+ : pass(`styles.css carries all ${files.length} token files, flattened`)
42
+
43
+ // And it must stay resolvable: an @import here is the exact defect above.
44
+ entry.includes('@import')
45
+ ? fail('styles.css contains an @import — a consumer cannot resolve it (see gen-tokens.mjs)')
46
+ : pass('styles.css has no @import to resolve')
29
47
  }
30
48
 
31
49
  // ── 2. every var() used inside the token layer must resolve ──────────────
@@ -14,7 +14,7 @@ const tokensDir = join(root, 'tokens')
14
14
  // The token files, in the same order styles.css imports them. base.css is the
15
15
  // semantic-alias layer (references other vars) — parsed too, so `--background`
16
16
  // etc. are available programmatically.
17
- const FILES = ['colors', 'typography', 'spacing', 'radius', 'elevation', 'motion', 'z', 'fonts', 'base']
17
+ const FILES = ['colors', 'typography', 'spacing', 'grid', 'radius', 'elevation', 'motion', 'z', 'fonts', 'base']
18
18
 
19
19
  // group name for each file (the export const); a couple read better renamed.
20
20
  const GROUP = { z: 'zIndex' }
@@ -76,3 +76,42 @@ ts += `export type CssVarName = keyof typeof cssVars\n`
76
76
 
77
77
  writeFileSync(join(root, 'src', 'tokens.gen.ts'), ts)
78
78
  console.log(`gen-tokens: wrote src/tokens.gen.ts — ${flat.length} tokens across ${Object.keys(groups).length} groups`)
79
+
80
+ // ── styles.css, FLATTENED ────────────────────────────────────────────────
81
+ // The entry point says "import THIS one file", and for a long time no bundler
82
+ // could. It was a list of `@import url("tokens/*.css")`, and a relative url
83
+ // resolves against the file doing the importing — so a consumer's
84
+ // `@import "@hanzo/design/styles.css"` sent webpack looking for
85
+ // `<their app>/tokens/fonts.css` and the build died on a module it never had.
86
+ // Nested imports also have to precede every other rule, which the spec then
87
+ // invalidates the moment the consumer imports anything before them; browsers
88
+ // drop them silently, every token resolves to nothing, and `border-border`
89
+ // falls back to currentColor — a stark white hairline on black.
90
+ //
91
+ // Both failures are the same fact: an @import list is not a stylesheet a
92
+ // consumer can use. So the entry point is now the CONTENT, concatenated in the
93
+ // same order, with no imports to resolve and no order to get wrong. That is why
94
+ // nothing consumed this package and every app hand-rolled its own palette.
95
+ //
96
+ // Authoring does not change: tokens/*.css stays the source of truth and this is
97
+ // its build product, exactly like tokens.gen.ts.
98
+ const banner = `/* Hanzo Design System — the entry point. Import THIS one file.
99
+ *
100
+ * AUTO-GENERATED by scripts/gen-tokens.mjs from tokens/*.css — DO NOT EDIT.
101
+ * Edit the token in tokens/<group>.css and re-run "npm run gen".
102
+ *
103
+ * Flattened deliberately: a consumer's bundler resolves a nested url() against
104
+ * ITS OWN directory, not ours, so an @import list here is a build error in
105
+ * every app that follows the instruction above. Nested imports must also
106
+ * precede all other rules, which the spec invalidates the moment the consumer
107
+ * imports anything first — browsers then drop them silently and every token
108
+ * resolves to nothing. The content is inlined in the order the groups were
109
+ * always imported in.
110
+ */
111
+ `
112
+ const bundle = banner + FILES.map((f) => {
113
+ const css = readFileSync(join(tokensDir, `${f}.css`), 'utf8').trim()
114
+ return `\n/* ── tokens/${f}.css ─────────────────────────────────────── */\n${css}\n`
115
+ }).join('')
116
+ writeFileSync(join(root, 'styles.css'), bundle)
117
+ console.log(`gen-tokens: wrote styles.css — ${FILES.length} token groups, flattened`)
package/src/tokens.gen.ts CHANGED
@@ -185,6 +185,21 @@ export const spacing = {
185
185
  'header-height': '4rem',
186
186
  } as const
187
187
 
188
+ /** grid tokens (from tokens/grid.css). Values are raw CSS. */
189
+ export const grid = {
190
+ 'grid-columns': '12',
191
+ 'grid-gap': 'var(--space-6)',
192
+ 'grid-gap-tight': 'var(--space-4)',
193
+ 'grid-gap-loose': 'var(--space-8)',
194
+ 'breakpoint-sm': '40rem',
195
+ 'breakpoint-md': '48rem',
196
+ 'breakpoint-lg': '64rem',
197
+ 'breakpoint-xl': '80rem',
198
+ 'breakpoint-2xl': '96rem',
199
+ 'grid-card-min': '18rem',
200
+ 'grid-card-min-wide': '24rem',
201
+ } as const
202
+
188
203
  /** radius tokens (from tokens/radius.css). Values are raw CSS. */
189
204
  export const radius = {
190
205
  'radius': '0.5rem',
@@ -433,6 +448,17 @@ export const cssVars = {
433
448
  '--hero-y': '5rem',
434
449
  '--hero-y-lg': '8rem',
435
450
  '--header-height': '4rem',
451
+ '--grid-columns': '12',
452
+ '--grid-gap': 'var(--space-6)',
453
+ '--grid-gap-tight': 'var(--space-4)',
454
+ '--grid-gap-loose': 'var(--space-8)',
455
+ '--breakpoint-sm': '40rem',
456
+ '--breakpoint-md': '48rem',
457
+ '--breakpoint-lg': '64rem',
458
+ '--breakpoint-xl': '80rem',
459
+ '--breakpoint-2xl': '96rem',
460
+ '--grid-card-min': '18rem',
461
+ '--grid-card-min-wide': '24rem',
436
462
  '--radius': '0.5rem',
437
463
  '--radius-sm': '0.375rem',
438
464
  '--radius-md': '0.5rem',
package/styles.css CHANGED
@@ -1,14 +1,497 @@
1
- /* Hanzo Design System — global entry point. Import THIS one file.
2
- Every token group is served here; nothing is authored but left unimported.
3
- (`tokens/z.css` went missing from this list once and the whole ladder stopped
4
- resolving while still type-checking see scripts/check-tokens.mjs, which now
5
- fails the build if any tokens/*.css is not imported below.) */
6
- @import url("tokens/fonts.css");
7
- @import url("tokens/colors.css");
8
- @import url("tokens/typography.css");
9
- @import url("tokens/spacing.css");
10
- @import url("tokens/radius.css");
11
- @import url("tokens/elevation.css");
12
- @import url("tokens/motion.css");
13
- @import url("tokens/z.css");
14
- @import url("tokens/base.css");
1
+ /* Hanzo Design System — the entry point. Import THIS one file.
2
+ *
3
+ * AUTO-GENERATED by scripts/gen-tokens.mjs from tokens/*.css DO NOT EDIT.
4
+ * Edit the token in tokens/<group>.css and re-run "npm run gen".
5
+ *
6
+ * Flattened deliberately: a consumer's bundler resolves a nested url() against
7
+ * ITS OWN directory, not ours, so an @import list here is a build error in
8
+ * every app that follows the instruction above. Nested imports must also
9
+ * precede all other rules, which the spec invalidates the moment the consumer
10
+ * imports anything first — browsers then drop them silently and every token
11
+ * resolves to nothing. The content is inlined in the order the groups were
12
+ * always imported in.
13
+ */
14
+
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
+ :root{
22
+ /* ——— base neutral ladder ——— */
23
+ --neutral-50:#FAFAFA;
24
+ --neutral-100:#F5F5F5;
25
+ --neutral-200:#E5E5E5;
26
+ --neutral-300:#D4D4D4;
27
+ --neutral-400:#A3A3A3;
28
+ --neutral-500:#737373;
29
+ --neutral-600:#525252;
30
+ --neutral-700:#404040;
31
+ --neutral-800:#262626;
32
+ --neutral-900:#171717;
33
+ --neutral-950:#0A0A0A;
34
+ --pure-black:#000000;
35
+ --pure-white:#FFFFFF;
36
+ /* Press-kit brand constants (public/press/hanzo/README.md). */
37
+ --hanzo-black:#0A0A0B;
38
+ --hanzo-white:#FFFFFF;
39
+
40
+ /* ——— the opacity ladder: the real palette ——— */
41
+ --white-05:rgb(255 255 255 / .05);
42
+ --white-10:rgb(255 255 255 / .10);
43
+ --white-15:rgb(255 255 255 / .15);
44
+ --white-20:rgb(255 255 255 / .20);
45
+ --white-30:rgb(255 255 255 / .30);
46
+ --white-40:rgb(255 255 255 / .40);
47
+ --white-60:rgb(255 255 255 / .60);
48
+ --white-80:rgb(255 255 255 / .80);
49
+
50
+ /* ——— semantic aliases (dark, the default) ——— */
51
+ --background:#000000;
52
+ --foreground:#ededed;
53
+ --card:#0a0a0a;
54
+ --card-foreground:#f5f5f5;
55
+ --popover:#0a0a0a;
56
+ --popover-foreground:#f5f5f5;
57
+ --primary:#ffffff;
58
+ --primary-foreground:#000000;
59
+ --secondary:#1a1a1a;
60
+ --secondary-foreground:#f5f5f5;
61
+ --muted:#101010;
62
+ --muted-foreground:#888888;
63
+ --accent:#1a1a1a;
64
+ --accent-foreground:#f5f5f5;
65
+ --destructive:#666666;
66
+ --destructive-foreground:#f5f5f5;
67
+ --border:#1f1f1f;
68
+ --input:#1f1f1f;
69
+ /* A focus indicator is a NON-TEXT CONTRAST target: WCAG 2.4.11/1.4.11 require
70
+ 3:1 against every surface it can land on. --neutral-500 is the only rung on
71
+ this ladder that clears 3:1 on all of them — #000000, #0a0a0a, #101010,
72
+ #1a1a1a, AND the light theme's #ffffff/#f5f5f5 — so one value serves both
73
+ themes. (Was #333333 = 1.66:1 on --background: not a focus indicator.) */
74
+ --ring:var(--neutral-500);
75
+ --brand:#e4e4e7;
76
+ --brand-foreground:#09090b;
77
+ --brand-muted:#a3a3a3;
78
+ --black:#000000;
79
+ --white:#f5f5f5;
80
+
81
+ /* ——— surface recipes (card fills used across hanzo.ai) ——— */
82
+ --surface-page:var(--background);
83
+ --surface-card:rgb(23 23 23 / .5); /* bg-neutral-900/50 — grid tiles */
84
+ --surface-card-emphasis:rgb(23 23 23 / .8);/* bg-neutral-900/80 — featured */
85
+ --surface-card-quiet:rgb(23 23 23 / .4); /* bg-neutral-900/40 — story cards */
86
+ --surface-overlay:rgb(10 10 10 / .95); /* dropdown / popover panels */
87
+ --surface-header:rgb(0 0 0 / .7); /* fixed nav, with backdrop blur */
88
+ --surface-scrim:rgb(0 0 0 / .8); /* the dialog / sheet backdrop */
89
+ /* Boundaries come in two kinds and they are NOT interchangeable.
90
+ DECORATIVE (--border, --border-hairline, --border-card): separates content;
91
+ WCAG imposes no ratio. Keep them quiet.
92
+ PERCEIVABLE (--border-strong): identifies a CONTROL — an input edge, a
93
+ switch, a checkbox — and must clear 3:1 (WCAG 1.4.11) on every surface.
94
+ Reach for --border-strong whenever the boundary IS the affordance. */
95
+ --border-hairline:var(--neutral-800);
96
+ --border-card:var(--white-10);
97
+ --border-strong:var(--neutral-500); /* 3.59:1 worst case — see --ring */
98
+
99
+ /* ——— the numeric surface ladder ——— */
100
+ /* Aliases onto the semantic canvases above, so a brand fork that retunes
101
+ --card/--muted/--secondary retunes the ladder with it and the light theme
102
+ inverts for free. Ascending lift: 0 is the page, 3 is a hovered control. */
103
+ --surface-0:var(--background);
104
+ --surface-1:var(--card);
105
+ --surface-2:var(--muted);
106
+ --surface-3:var(--secondary);
107
+
108
+ /* ——— text ranks ——— */
109
+ --text-primary:var(--pure-white);
110
+ --text-secondary:var(--white-80);
111
+ --text-tertiary:var(--white-60);
112
+ --text-helper:var(--muted-foreground);
113
+ --text-disabled:var(--white-30);
114
+
115
+ /* ——— the ONLY permitted hues (DESIGN.md §2.4) ——— */
116
+ --state-error:#ef4444; /* red-500 — destructive / blocking error */
117
+ --state-error-text:#fca5a5; /* red-300 */
118
+ --state-error-bg:rgb(239 68 68 / .1);
119
+ --state-online:#4ade80; /* green-400 — live status dot */
120
+ --state-success:#22c55e; /* green-500 — "Free" / "Save N%" callouts */
121
+ --chrome-dot-red:rgb(239 68 68 / .6);
122
+ --chrome-dot-yellow:rgb(234 179 8 / .6);
123
+ --chrome-dot-green:rgb(34 197 94 / .6);
124
+ }
125
+
126
+ /* Light theme — the same tokens, inverted. Rare: only /brand-style docs pages. */
127
+ .light{
128
+ --background:#ffffff;
129
+ --foreground:#0a0a0a;
130
+ --card:#f5f5f5;
131
+ --card-foreground:#0a0a0a;
132
+ --popover:#ffffff;
133
+ --popover-foreground:#0a0a0a;
134
+ --primary:#0a0a0a;
135
+ --primary-foreground:#ffffff;
136
+ --secondary:#f5f5f5;
137
+ --secondary-foreground:#0a0a0a;
138
+ --muted:#f5f5f5;
139
+ --muted-foreground:#525252;
140
+ --accent:#f5f5f5;
141
+ --accent-foreground:#0a0a0a;
142
+ --destructive:#999999;
143
+ --destructive-foreground:#ffffff;
144
+ --border:#e5e5e5;
145
+ --input:#e5e5e5;
146
+ /* Same rung as dark: #d4d4d4 measured 1.48:1 on white and could not carry a
147
+ focus indicator either. --neutral-500 is 4.74:1 on #ffffff / 4.38:1 on
148
+ #f5f5f5, so ONE value is conformant in both themes. */
149
+ --ring:var(--neutral-500);
150
+ --black:#0a0a0a;
151
+ --white:#ffffff;
152
+ --surface-card:#f5f5f5;
153
+ --surface-card-emphasis:#ffffff;
154
+ --surface-card-quiet:#fafafa;
155
+ --surface-overlay:rgb(255 255 255 / .95);
156
+ --surface-header:rgb(255 255 255 / .8);
157
+ --surface-scrim:rgb(0 0 0 / .5);
158
+ --border-hairline:var(--neutral-200);
159
+ --border-card:rgb(0 0 0 / .1);
160
+ --border-strong:var(--neutral-500); /* was --neutral-300 = 1.48:1 on white */
161
+ /* The white-opacity ladder does NOT invert, so --white-40 is white-on-white
162
+ here (1.00:1). Anything that needs a visible edge in BOTH themes must use
163
+ --border-strong, never a --white-* rung. */
164
+ --text-primary:var(--neutral-950);
165
+ --text-secondary:rgb(10 10 10 / .8);
166
+ --text-tertiary:rgb(10 10 10 / .6);
167
+ --text-disabled:rgb(10 10 10 / .3);
168
+ }
169
+
170
+ /* ── tokens/typography.css ─────────────────────────────────────── */
171
+ /* TIGHT app-first type scale — the compact developer-app register (linear.app /
172
+ vercel.com / the Codex desktop look), the Hanzo default across chat / app /
173
+ desktop. Base is 14px, nav 13px, labels 11px; display sizes tightened. Kept in
174
+ lockstep with @hanzo/brand (styles/variables.css --font-size-* + typography.ts)
175
+ — the two are the SAME scale, mirrored. A surface/tenant overrides any --text-*
176
+ on :root to retune density on demand. */
177
+ :root{
178
+ --text-xs:0.6875rem; --leading-xs:1rem; /* 11px — eyebrows / section labels */
179
+ --text-sm:0.8125rem; --leading-sm:1.15rem; /* 13px — nav labels, dense body */
180
+ --text-base:0.875rem; --leading-base:1.35rem; /* 14px — base app text (was 16px) */
181
+ --text-lg:0.9375rem; --leading-lg:1.4rem; /* 15px */
182
+ --text-xl:1.0625rem; --leading-xl:1.55rem; /* 17px */
183
+ --text-2xl:1.3125rem; --leading-2xl:1.7rem; /* 21px */
184
+ --text-3xl:1.625rem; --leading-3xl:1.95rem; /* 26px */
185
+ --text-4xl:2rem; --leading-4xl:2.25rem; /* 32px */
186
+ --text-5xl:2.5rem; --leading-5xl:1.05; /* 40px */
187
+ --text-6xl:3.25rem; --leading-6xl:1; /* 52px */
188
+ --text-7xl:4rem; --leading-7xl:1; /* 64px */
189
+ --text-8xl:5.25rem; --leading-8xl:1; /* 84px */
190
+ --text-9xl:7rem; --leading-9xl:1; /* 112px */
191
+
192
+ /* The SAME scale under @hanzo/brand's spelling. @hanzo/gui's shell theme and
193
+ Hanzo Studio address the ramp as --font-size-*; both names are one value, so
194
+ a component written against either resolves here. --text-* is canonical. */
195
+ --font-size-xs:var(--text-xs);
196
+ --font-size-sm:var(--text-sm);
197
+ --font-size-base:var(--text-base);
198
+ --font-size-lg:var(--text-lg);
199
+ --font-size-xl:var(--text-xl);
200
+ --font-size-2xl:var(--text-2xl);
201
+ --font-size-3xl:var(--text-3xl);
202
+ --font-size-4xl:var(--text-4xl);
203
+ --font-size-5xl:var(--text-5xl);
204
+ --font-size-6xl:var(--text-6xl);
205
+ --font-size-7xl:var(--text-7xl);
206
+ --font-size-8xl:var(--text-8xl);
207
+ --font-size-9xl:var(--text-9xl);
208
+
209
+ --weight-normal:400;
210
+ --weight-medium:500;
211
+ --weight-semibold:600;
212
+ --weight-bold:700;
213
+
214
+ --tracking-tight:-0.025em;
215
+ --tracking-normal:0em;
216
+ --tracking-wide:0.025em;
217
+ --tracking-widest:0.1em; /* eyebrows / uppercase category labels */
218
+
219
+ --leading-none:1;
220
+ --leading-tight:1.25;
221
+ --leading-snug:1.375;
222
+ --leading-normal:1.5;
223
+ --leading-relaxed:1.625;
224
+ --leading-golden:1.618;
225
+
226
+ /* named roles */
227
+ --type-hero:600 var(--text-5xl)/1.05 var(--font-display);
228
+ --type-h2:700 var(--text-4xl)/var(--leading-4xl) var(--font-display);
229
+ --type-h3:600 var(--text-xl)/var(--leading-xl) var(--font-display);
230
+ --type-lead:400 var(--text-lg)/var(--leading-relaxed) var(--font-sans);
231
+ --type-body:400 var(--text-sm)/var(--leading-sm) var(--font-sans);
232
+ --type-caption:400 var(--text-xs)/var(--leading-xs) var(--font-sans);
233
+ --type-code:400 var(--text-sm)/var(--leading-relaxed) var(--font-mono);
234
+ --type-eyebrow:600 0.625rem/1 var(--font-sans);
235
+ }
236
+
237
+ /* ── tokens/spacing.css ─────────────────────────────────────── */
238
+ /* Spacing: the 4px Tailwind ramp is what ships. The golden-ratio ramp below is
239
+ declared in hanzo.ai's tailwind.config.ts (legacy v3 config, kept for
240
+ reference) — use it for editorial layouts, not for component padding. */
241
+ :root{
242
+ --space-0:0;
243
+ --space-1:0.25rem;
244
+ --space-2:0.5rem;
245
+ --space-3:0.75rem;
246
+ --space-4:1rem;
247
+ --space-5:1.25rem;
248
+ --space-6:1.5rem;
249
+ --space-8:2rem;
250
+ --space-10:2.5rem;
251
+ --space-12:3rem;
252
+ --space-14:3.5rem;
253
+ --space-16:4rem;
254
+ --space-20:5rem;
255
+ --space-24:6rem;
256
+ --space-32:8rem;
257
+
258
+ /* golden ramp (φ) — hanzo.ai tailwind.config.ts */
259
+ --golden-1:0.25rem;
260
+ --golden-2:0.405rem;
261
+ --golden-3:0.654rem;
262
+ --golden-4:1.059rem;
263
+ --golden-5:1.713rem;
264
+ --golden-6:2.772rem;
265
+ --golden-7:4.487rem;
266
+ --golden-8:7.26rem;
267
+ --golden-9:11.749rem;
268
+ --golden-split:38.2% 61.8%; /* @kind other */
269
+
270
+ /* layout rules (DESIGN.md §1.3) */
271
+ --container-max:80rem; /* max-w-7xl — grids */
272
+ --container-prose:48rem; /* max-w-3xl — centered text */
273
+ --container-wide:72rem; /* max-w-6xl — landing sections */
274
+ --gutter:1rem; /* px-4 */
275
+ --gutter-sm:1.5rem; /* sm:px-6 */
276
+ --gutter-lg:2rem; /* lg:px-8 */
277
+ --section-y:4rem; /* py-16 — content sections */
278
+ --section-y-lg:6rem; /* py-24 — landing sections */
279
+ --hero-y:5rem; /* py-20 … */
280
+ --hero-y-lg:8rem; /* … lg:py-32 */
281
+ --header-height:4rem;
282
+ }
283
+
284
+ /* ── tokens/grid.css ─────────────────────────────────────── */
285
+ /* Grid — one definition of the field every Hanzo surface lays out on.
286
+
287
+ Derived, not invented. Across hanzo.ai and hanzo.app the grid written by hand
288
+ is overwhelmingly ONE grid: `grid-cols-1 md:grid-cols-2 lg:grid-cols-3` with a
289
+ 1.5rem gutter (240 / 197 / 127 / 185 occurrences on hanzo.ai alone, and the
290
+ same shape leads on app). 1, 2, 3 and 4 columns account for 97% of every
291
+ grid-cols- in both repos, and all four divide 12 cleanly — so 12 is the base
292
+ the spans are cut from, and the long tail (5, 6, 7, 8, 12) still lands on it.
293
+
294
+ Containers, gutters and section rhythm are NOT restated here: spacing.css
295
+ already owns --container-max / --container-prose / --container-wide /
296
+ --gutter* / --section-y*, and a grid that redeclared them would be a second
297
+ answer to a settled question. This file adds only what a grid knows that
298
+ spacing does not — how many columns, how far apart, and where it reflows. */
299
+ :root{
300
+ /* The base. Spans are cut from twelve because 1/2/3/4/6 all divide it. */
301
+ --grid-columns:12;
302
+
303
+ /* Gutter. --grid-gap is the measured card-grid default (gap-6 leads hanzo.ai
304
+ at 185 uses); tight and loose are the neighbouring steps that cover app's
305
+ gap-4 (45) and the wide editorial grids (gap-8, 75). All three reference the
306
+ space ramp — the grid does not get its own private set of distances. */
307
+ --grid-gap:var(--space-6);
308
+ --grid-gap-tight:var(--space-4);
309
+ --grid-gap-loose:var(--space-8);
310
+
311
+ /* Reflow points, byte-identical to the Tailwind v4 defaults these surfaces
312
+ already compile against. Same NAMES and same VALUES on purpose: the utility
313
+ layer and anything reading tokens then resolve one value, not two that agree
314
+ by luck. md and lg carry ~85% of every breakpoint prefix in use; xl is 44
315
+ occurrences across both repos and 2xl is 1. */
316
+ --breakpoint-sm:40rem;
317
+ --breakpoint-md:48rem;
318
+ --breakpoint-lg:64rem;
319
+ --breakpoint-xl:80rem;
320
+ --breakpoint-2xl:96rem;
321
+
322
+ /* Intrinsic card grids: state ONE track minimum instead of three column counts
323
+ at three breakpoints. `repeat(auto-fit, minmax(var(--grid-card-min), 1fr))`
324
+ reflows 1 -> 2 -> 3 at the same widths the explicit recipe does, and it does
325
+ it from the space available rather than from the viewport — so a card grid
326
+ inside a sidebar behaves correctly, which the breakpoint version cannot.
327
+
328
+ 18rem is chosen, not rounded to: inside --container-max less --gutter-lg on
329
+ both sides, lg (64rem) leaves ~60rem, where 3 tracks need 3x18 + 2x1.5 =
330
+ 57rem and 4 would need 76.5rem; md (48rem) leaves ~44rem, where 2 tracks
331
+ need 37.5rem and 3 would need 57rem. The measured 1/2/3 ladder falls out. */
332
+ --grid-card-min:18rem;
333
+ --grid-card-min-wide:24rem;
334
+ }
335
+
336
+ /* ── tokens/radius.css ─────────────────────────────────────── */
337
+ :root{
338
+ --radius:0.5rem; /* the base token (globals.css) */
339
+ --radius-sm:0.375rem; /* rounded-md — buttons, inputs */
340
+ --radius-md:0.5rem;
341
+ --radius-lg:0.75rem; /* rounded-xl — cards */
342
+ --radius-xl:1rem; /* rounded-2xl — dropdown panels */
343
+ --radius-2xl:1.5rem; /* rounded-3xl — story / hero cards */
344
+ --radius-composer:28px; /* the chat composer, exactly 28px */
345
+ --radius-full:9999px; /* pills, CTAs, avatars, badges */
346
+ }
347
+
348
+ /* ── tokens/elevation.css ─────────────────────────────────────── */
349
+ /* Hanzo barely uses shadow: on black, elevation reads as a hairline border plus
350
+ a wide, very dark drop. Only two levels ship (Tailwind's shadow-2xl for
351
+ floating surfaces) plus the ambient radial glow used behind heroes. */
352
+ :root{
353
+ --shadow-none:none;
354
+ --shadow-floating:0 25px 50px -12px rgb(0 0 0 / .25); /* shadow-2xl: composer, dropdowns, mega panel */
355
+ --shadow-inset-hairline:inset 0 0 0 1px var(--white-10);
356
+ --ring-focus:0 0 0 2px var(--ring);
357
+
358
+ /* The t-shirt ramp. Named by size rather than by role, because that is how
359
+ every component library already asks for a shadow (and how @hanzo/brand
360
+ spells it). Alphas are heavier than Tailwind's defaults: on a near-black
361
+ ground a 10% black drop is invisible, so each rung is tuned to read on
362
+ --background. --shadow-2xl and --shadow-floating are the same rung. */
363
+ --shadow-sm:0 1px 2px 0 rgb(0 0 0 / .40);
364
+ --shadow:0 1px 3px 0 rgb(0 0 0 / .45), 0 1px 2px -1px rgb(0 0 0 / .45);
365
+ --shadow-md:0 4px 6px -1px rgb(0 0 0 / .50), 0 2px 4px -2px rgb(0 0 0 / .50);
366
+ --shadow-lg:0 10px 15px -3px rgb(0 0 0 / .55), 0 4px 6px -4px rgb(0 0 0 / .55);
367
+ --shadow-xl:0 20px 25px -5px rgb(0 0 0 / .60), 0 8px 10px -6px rgb(0 0 0 / .60);
368
+ --shadow-2xl:var(--shadow-floating);
369
+ /* Ambient hero glow — a single white radial, blurred 120px, low opacity. */
370
+ --glow-hero:radial-gradient(circle,rgb(255 255 255 / .12) 0%,transparent 68%); /* @kind color */
371
+ --glow-hero-blur:120px;
372
+ /* Card top-corner sheen used on the story cards. */
373
+ --sheen-card:radial-gradient(120% 120% at 80% 0%,rgb(255 255 255 / .08) 0%,transparent 55%); /* @kind color */
374
+ /* Chrome text: the canonical headline gradient. Never a saturated rainbow. */
375
+ --gradient-chrome:linear-gradient(to right,#ffffff,var(--white-80),var(--white-60));
376
+ --gradient-chrome-2:linear-gradient(to right,#ffffff,var(--neutral-500));
377
+ /* Section-top protection gradient (hero overlays). */
378
+ --gradient-protect:linear-gradient(to bottom,var(--white-10),transparent);
379
+ }
380
+
381
+ /* ── tokens/motion.css ─────────────────────────────────────── */
382
+ /* Motion is restrained: fade + small rise, CSS-only hovers, one breathing glow.
383
+ No springs, no bounce, no parallax, no autoplay carousels. */
384
+ :root{
385
+ --duration-fast:150ms; /* @kind other */ /* dropdown / panel open */
386
+ --duration-base:300ms; /* @kind other */ /* slide-up-fade */
387
+ --duration-slow:400ms; /* @kind other */ /* hero element entry */
388
+ --duration-slower:500ms; /* @kind other */ /* section entry */
389
+ --duration-glow:9s; /* @kind other */ /* ambient radial breathe */
390
+ --ease-out:cubic-bezier(0,0,0.2,1); /* @kind other */
391
+ --ease-in-out:cubic-bezier(0.4,0,0.2,1); /* @kind other */
392
+ --stagger:60ms; /* @kind other */ /* per-element delay in a group */
393
+ --entry-rise:16px; /* hero y-offset */
394
+ --entry-rise-lg:24px; /* card y-offset */
395
+ }
396
+
397
+ @keyframes hanzo-fade-up{from{opacity:0;transform:translateY(10px)}to{opacity:1;transform:translateY(0)}}
398
+ @keyframes hanzo-fade-down{from{opacity:0;transform:translateY(-10px)}to{opacity:1;transform:translateY(0)}}
399
+ @keyframes hanzo-slide-up-fade{from{opacity:0;transform:translateY(20px)}to{opacity:1;transform:translateY(0)}}
400
+ @keyframes hanzo-glow{0%,100%{transform:scale(1);opacity:.45}50%{transform:scale(1.08);opacity:.65}}
401
+ @keyframes hanzo-pulse-dot{0%,100%{opacity:1}50%{opacity:.35}}
402
+
403
+ @media (prefers-reduced-motion:reduce){
404
+ *,*::before,*::after{animation-duration:.001ms!important;animation-iteration-count:1!important;transition-duration:.001ms!important}
405
+ }
406
+
407
+ /* ── tokens/z.css ─────────────────────────────────────── */
408
+ /* Stacking order — the one z-index ladder. Layers are named by role, never by a
409
+ magic number, so a dropdown opened from the fixed header always sits above it
410
+ and nothing ever reaches for 9999. Below --z-raised is ordinary document flow.
411
+
412
+ Rungs are 100 apart so a surface can slot a one-off BETWEEN two roles
413
+ (calc(var(--z-modal) + 1)) without inventing a new decade. */
414
+ :root{
415
+ --z-base:0;
416
+ --z-raised:10; /* hover-lifted cards, sticky table headers */
417
+ --z-sticky:200; /* pinned section rails */
418
+ --z-header:300; /* the fixed site header */
419
+ --z-dropdown:400; /* menus, selects, comboboxes */
420
+ --z-overlay:500; /* dialog / sheet scrim */
421
+ --z-modal:600; /* dialogs, sheets, command palette */
422
+ --z-popover:700; /* popovers, tooltips (also when anchored in modals) */
423
+ --z-toast:800; /* toasts / notifications — always on top */
424
+
425
+ /* @hanzo/brand spells the top two rungs --z-tooltip and --z-notification.
426
+ Same rungs, so a component written against either vocabulary stacks
427
+ identically. --z-popover / --z-toast are canonical. */
428
+ --z-tooltip:var(--z-popover);
429
+ --z-notification:var(--z-toast);
430
+ }
431
+
432
+ /* ── tokens/fonts.css ─────────────────────────────────────── */
433
+ /* Geist Sans + Geist Mono — the only two faces on Hanzo surfaces.
434
+ SELF-HOSTED. The faces ship inside this package (assets/fonts/*.woff2, two
435
+ variable files, 141 KB total, SIL OFL-1.1 — see assets/fonts/LICENSE-Geist.txt).
436
+
437
+ Why self-hosted rather than @import from fonts.googleapis.com:
438
+ - A sign-in page must not make a third-party request. hanzoai/id refused to
439
+ import this file for exactly that reason, which split the token layer: id
440
+ took the colours and not the typeface. Self-hosting removes the reason, so
441
+ every surface can import styles.css unchanged.
442
+ - The @import was a render-blocking request to a host we do not control, on
443
+ the critical path of every surface, and it broke offline/air-gapped dev.
444
+ - One variable file per family replaces nine static weights, and it is fewer
445
+ bytes than the CSS-then-woff2 round trip Google served.
446
+
447
+ The url()s are relative to THIS file, so they resolve wherever the package is
448
+ mounted — node_modules, a CDN, a copied dist — with no configuration. */
449
+
450
+ @font-face{
451
+ font-family:"Geist";
452
+ src:url("../assets/fonts/Geist-Variable.woff2") format("woff2");
453
+ font-weight:100 900;
454
+ font-style:normal;
455
+ font-display:swap;
456
+ }
457
+ @font-face{
458
+ font-family:"Geist Mono";
459
+ src:url("../assets/fonts/GeistMono-Variable.woff2") format("woff2");
460
+ font-weight:100 900;
461
+ font-style:normal;
462
+ font-display:swap;
463
+ }
464
+
465
+ :root{
466
+ --font-sans:"Geist","Geist Sans",ui-sans-serif,system-ui,sans-serif;
467
+ --font-display:var(--font-sans);
468
+ --font-mono:"Geist Mono",ui-monospace,SFMono-Regular,monospace;
469
+ --font-serif:Georgia,serif;
470
+ /* hanzo.ai sets these OpenType features on <body>. */
471
+ --font-feature-settings:"ss01","ss02","cv01","cv02","cv03";
472
+ }
473
+
474
+ /* ── tokens/base.css ─────────────────────────────────────── */
475
+ /* Minimal element defaults so specimen cards and kits inherit the brand without
476
+ a utility framework. Components carry their own styles inline. */
477
+ *{box-sizing:border-box;border-color:var(--border)}
478
+ html{-webkit-font-smoothing:antialiased;text-rendering:optimizeLegibility;scroll-behavior:smooth}
479
+ /* Each font-family carries the stack as a literal fallback. --font-sans lives in
480
+ tokens/fonts.css, which a surface may legitimately import separately; without
481
+ the fallback an unresolved var() makes font-family invalid and the UA drops to
482
+ its SERIF default — the whole console silently rendered in Times. */
483
+ 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)}
484
+ 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)}
485
+ p{margin:0;text-wrap:pretty}
486
+ code,pre,kbd{font-family:var(--font-mono,ui-monospace,SFMono-Regular,monospace)}
487
+ a{color:var(--text-primary);text-decoration:none;text-underline-offset:4px;transition:color var(--duration-fast) var(--ease-out)}
488
+ /* Underline is the accessible affordance for a link in RUNNING TEXT and a visual
489
+ bug everywhere else: nav items, cards and anchor-buttons are all <a> too, so a
490
+ blanket `a:hover` underlined every one of them on every surface that imports
491
+ these tokens. Scope it to the elements that actually carry prose; a link that
492
+ is a component states its own hover. The old rule also re-set `color` to the
493
+ value `a` already has — a no-op that only served to outrank a component's own
494
+ hover colour. */
495
+ :is(p,li,blockquote,dd,dt,td,th,figcaption) a:hover{text-decoration:underline}
496
+ :focus-visible{outline:2px solid var(--ring);outline-offset:2px}
497
+ ::selection{background:var(--white-20);color:#fff}
@@ -0,0 +1,50 @@
1
+ /* Grid — one definition of the field every Hanzo surface lays out on.
2
+
3
+ Derived, not invented. Across hanzo.ai and hanzo.app the grid written by hand
4
+ is overwhelmingly ONE grid: `grid-cols-1 md:grid-cols-2 lg:grid-cols-3` with a
5
+ 1.5rem gutter (240 / 197 / 127 / 185 occurrences on hanzo.ai alone, and the
6
+ same shape leads on app). 1, 2, 3 and 4 columns account for 97% of every
7
+ grid-cols- in both repos, and all four divide 12 cleanly — so 12 is the base
8
+ the spans are cut from, and the long tail (5, 6, 7, 8, 12) still lands on it.
9
+
10
+ Containers, gutters and section rhythm are NOT restated here: spacing.css
11
+ already owns --container-max / --container-prose / --container-wide /
12
+ --gutter* / --section-y*, and a grid that redeclared them would be a second
13
+ answer to a settled question. This file adds only what a grid knows that
14
+ spacing does not — how many columns, how far apart, and where it reflows. */
15
+ :root{
16
+ /* The base. Spans are cut from twelve because 1/2/3/4/6 all divide it. */
17
+ --grid-columns:12;
18
+
19
+ /* Gutter. --grid-gap is the measured card-grid default (gap-6 leads hanzo.ai
20
+ at 185 uses); tight and loose are the neighbouring steps that cover app's
21
+ gap-4 (45) and the wide editorial grids (gap-8, 75). All three reference the
22
+ space ramp — the grid does not get its own private set of distances. */
23
+ --grid-gap:var(--space-6);
24
+ --grid-gap-tight:var(--space-4);
25
+ --grid-gap-loose:var(--space-8);
26
+
27
+ /* Reflow points, byte-identical to the Tailwind v4 defaults these surfaces
28
+ already compile against. Same NAMES and same VALUES on purpose: the utility
29
+ layer and anything reading tokens then resolve one value, not two that agree
30
+ by luck. md and lg carry ~85% of every breakpoint prefix in use; xl is 44
31
+ occurrences across both repos and 2xl is 1. */
32
+ --breakpoint-sm:40rem;
33
+ --breakpoint-md:48rem;
34
+ --breakpoint-lg:64rem;
35
+ --breakpoint-xl:80rem;
36
+ --breakpoint-2xl:96rem;
37
+
38
+ /* Intrinsic card grids: state ONE track minimum instead of three column counts
39
+ at three breakpoints. `repeat(auto-fit, minmax(var(--grid-card-min), 1fr))`
40
+ reflows 1 -> 2 -> 3 at the same widths the explicit recipe does, and it does
41
+ it from the space available rather than from the viewport — so a card grid
42
+ inside a sidebar behaves correctly, which the breakpoint version cannot.
43
+
44
+ 18rem is chosen, not rounded to: inside --container-max less --gutter-lg on
45
+ both sides, lg (64rem) leaves ~60rem, where 3 tracks need 3x18 + 2x1.5 =
46
+ 57rem and 4 would need 76.5rem; md (48rem) leaves ~44rem, where 2 tracks
47
+ need 37.5rem and 3 would need 57rem. The measured 1/2/3 ladder falls out. */
48
+ --grid-card-min:18rem;
49
+ --grid-card-min-wide:24rem;
50
+ }