@hanzo/design 0.5.17 → 0.5.19

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/README.md CHANGED
@@ -81,7 +81,7 @@ import { HanzoLogo } from "@hanzo/design/components/core/HanzoLogo.jsx";
81
81
  | `content/` | The words — brand voice and taglines. |
82
82
  | `docs/` | How to use the system — integrate the tokens, theme, extend. |
83
83
  | `guidelines/` | Specimen cards — color, type, spacing, brand, iconography — the visual reference. |
84
- | `assets/fonts/` | Geist Sans + Geist Mono, self-hosted (two variable `.woff2`, 141 KB, SIL OFL-1.1). No Google Fonts request. |
84
+ | `assets/fonts/` | Zen + Zen Mono, self-hosted (two variable `.woff2`, 141 KB, SIL OFL-1.1). No Google Fonts request. |
85
85
  | `assets/` | The mark, wordmark, favicon, provider + partner logos, brand imagery. |
86
86
  | `scripts/check-tokens.mjs` | The gate: every token file is served, every internal `var()` resolves, and the contrast floors hold. Runs on `npm run build`. |
87
87
  | `ui_kits/` | Composed surfaces (e.g. `SiteChrome`) assembled from the components. |
@@ -1,4 +1,5 @@
1
1
  Copyright (c) 2023 Vercel, in collaboration with basement.studio
2
+ Copyright (c) 2026 Hanzo AI, Inc.
2
3
 
3
4
  This Font Software is licensed under the SIL Open Font License, Version 1.1.
4
5
  This license is copied below, and is also available with a FAQ at:
Binary file
@@ -303,9 +303,9 @@ exports.zIndex = {
303
303
  };
304
304
  /** fonts tokens (from tokens/fonts.css). Values are raw CSS. */
305
305
  exports.fonts = {
306
- 'font-sans': '"Geist","Geist Sans",ui-sans-serif,system-ui,sans-serif',
306
+ 'font-sans': '"Zen",ui-sans-serif,system-ui,sans-serif',
307
307
  'font-display': 'var(--font-sans)',
308
- 'font-mono': '"Geist Mono",ui-monospace,SFMono-Regular,monospace',
308
+ 'font-mono': '"Zen Mono",ui-monospace,SFMono-Regular,monospace',
309
309
  'font-serif': 'Georgia,serif',
310
310
  'font-feature-settings': '"ss01","ss02","cv01","cv02","cv03"',
311
311
  };
@@ -588,9 +588,9 @@ exports.cssVars = {
588
588
  '--z-toast': '800',
589
589
  '--z-tooltip': 'var(--z-popover)',
590
590
  '--z-notification': 'var(--z-toast)',
591
- '--font-sans': '"Geist","Geist Sans",ui-sans-serif,system-ui,sans-serif',
591
+ '--font-sans': '"Zen",ui-sans-serif,system-ui,sans-serif',
592
592
  '--font-display': 'var(--font-sans)',
593
- '--font-mono': '"Geist Mono",ui-monospace,SFMono-Regular,monospace',
593
+ '--font-mono': '"Zen Mono",ui-monospace,SFMono-Regular,monospace',
594
594
  '--font-serif': 'Georgia,serif',
595
595
  '--font-feature-settings': '"ss01","ss02","cv01","cv02","cv03"',
596
596
  '--ring-offset': '-2px',
@@ -297,9 +297,9 @@ export declare const zIndex: {
297
297
  };
298
298
  /** fonts tokens (from tokens/fonts.css). Values are raw CSS. */
299
299
  export declare const fonts: {
300
- readonly 'font-sans': "\"Geist\",\"Geist Sans\",ui-sans-serif,system-ui,sans-serif";
300
+ readonly 'font-sans': "\"Zen\",ui-sans-serif,system-ui,sans-serif";
301
301
  readonly 'font-display': "var(--font-sans)";
302
- readonly 'font-mono': "\"Geist Mono\",ui-monospace,SFMono-Regular,monospace";
302
+ readonly 'font-mono': "\"Zen Mono\",ui-monospace,SFMono-Regular,monospace";
303
303
  readonly 'font-serif': "Georgia,serif";
304
304
  readonly 'font-feature-settings': "\"ss01\",\"ss02\",\"cv01\",\"cv02\",\"cv03\"";
305
305
  };
@@ -582,9 +582,9 @@ export declare const cssVars: {
582
582
  readonly '--z-toast': "800";
583
583
  readonly '--z-tooltip': "var(--z-popover)";
584
584
  readonly '--z-notification': "var(--z-toast)";
585
- readonly '--font-sans': "\"Geist\",\"Geist Sans\",ui-sans-serif,system-ui,sans-serif";
585
+ readonly '--font-sans': "\"Zen\",ui-sans-serif,system-ui,sans-serif";
586
586
  readonly '--font-display': "var(--font-sans)";
587
- readonly '--font-mono': "\"Geist Mono\",ui-monospace,SFMono-Regular,monospace";
587
+ readonly '--font-mono': "\"Zen Mono\",ui-monospace,SFMono-Regular,monospace";
588
588
  readonly '--font-serif': "Georgia,serif";
589
589
  readonly '--font-feature-settings': "\"ss01\",\"ss02\",\"cv01\",\"cv02\",\"cv03\"";
590
590
  readonly '--ring-offset': "-2px";
@@ -300,9 +300,9 @@ export const zIndex = {
300
300
  };
301
301
  /** fonts tokens (from tokens/fonts.css). Values are raw CSS. */
302
302
  export const fonts = {
303
- 'font-sans': '"Geist","Geist Sans",ui-sans-serif,system-ui,sans-serif',
303
+ 'font-sans': '"Zen",ui-sans-serif,system-ui,sans-serif',
304
304
  'font-display': 'var(--font-sans)',
305
- 'font-mono': '"Geist Mono",ui-monospace,SFMono-Regular,monospace',
305
+ 'font-mono': '"Zen Mono",ui-monospace,SFMono-Regular,monospace',
306
306
  'font-serif': 'Georgia,serif',
307
307
  'font-feature-settings': '"ss01","ss02","cv01","cv02","cv03"',
308
308
  };
@@ -585,9 +585,9 @@ export const cssVars = {
585
585
  '--z-toast': '800',
586
586
  '--z-tooltip': 'var(--z-popover)',
587
587
  '--z-notification': 'var(--z-toast)',
588
- '--font-sans': '"Geist","Geist Sans",ui-sans-serif,system-ui,sans-serif',
588
+ '--font-sans': '"Zen",ui-sans-serif,system-ui,sans-serif',
589
589
  '--font-display': 'var(--font-sans)',
590
- '--font-mono': '"Geist Mono",ui-monospace,SFMono-Regular,monospace',
590
+ '--font-mono': '"Zen Mono",ui-monospace,SFMono-Regular,monospace',
591
591
  '--font-serif': 'Georgia,serif',
592
592
  '--font-feature-settings': '"ss01","ss02","cv01","cv02","cv03"',
593
593
  '--ring-offset': '-2px',
@@ -15,42 +15,52 @@ and the dark-black palette.
15
15
 
16
16
  ---
17
17
 
18
- ## 1. Typography — Basel Grotesk + Geist Mono
18
+ ## 1. Typography — Zen
19
19
 
20
20
  | Role | Family | Notes |
21
21
  |------|--------|-------|
22
- | UI / body / display / heading (`sans`) | **Basel Grotesk** | Self-hosted. Book = weight **400**, Medium = weight **500**. |
23
- | code / data / mono (`mono`) | **Geist Mono** | `next/font/google` (`Geist_Mono`) or the geist CDN. |
22
+ | UI / body / display / heading (`sans`) | **Zen** | Variable, `wght` 100–900. One file covers every weight. |
23
+ | code / data / mono (`mono`) | **Zen Mono** | Variable, same axis. |
24
+ | display accents | **Zen Pixel** | Five cuts: Circle, Grid, Line, Square, Triangle. |
24
25
  | Arabic / Hebrew (`--font-ar` / `--font-he`) | unchanged | i18n only — keep. |
25
26
 
26
- **Dropped as defaults:** Geist Sans, DM Sans, Figtree, Inter, PT Sans, Roboto Mono.
27
+ **There is one family.** No Basel Grotesk, no Druk, no Inter, DM Sans, Figtree,
28
+ PT Sans or Roboto Mono — and no licensed third-party face at all, which is the
29
+ point: Zen is ours, so nothing here is a seat count or a domain licence.
27
30
 
28
- Basel is a **licensed, non-Google** face **self-host** the woff2/woff, do NOT use
29
- `next/font/google` for it. Canonical files (mirror lux.exchange):
30
- `Basel-Grotesk-Book.woff2/.woff` (400), `Basel-Grotesk-Medium.woff2/.woff` (500).
31
-
32
- `@font-face` (weights 400/500, `font-display: swap`, `font-style: normal`):
31
+ **An app writes nothing.** `@hanzo/design/styles.css` declares the `@font-face`
32
+ rules and serves the binaries out of this package, so importing the token layer
33
+ is the whole integration. Do NOT hand-write an `@font-face`, do NOT reach for
34
+ `next/font/google`, and do NOT add a per-app copy of the files.
33
35
 
34
36
  ```css
35
- @font-face {
36
- font-family: 'Basel';
37
- font-style: normal;
38
- font-weight: 400; /* Book; 500 = Medium */
39
- font-display: swap;
40
- src: url('.../Basel-Grotesk-Book.woff2') format('woff2'),
41
- url('.../Basel-Grotesk-Book.woff') format('woff');
42
- }
37
+ @import "@hanzo/design/styles.css"; /* faces + tokens, one line */
43
38
  ```
44
39
 
45
- Per-app adoption (converge the value, keep each app's own mechanism):
46
- - **@hanzo/ui / Next apps** → `next/font/local` for Basel (`--font-basel-sans`) +
47
- `next/font/google` `Geist_Mono` (`--font-geist-mono`). See `app/lib/fonts.ts`;
48
- tailwind `sans → var(--font-basel-sans)`, `mono → var(--font-geist-mono)`.
49
- - **Vite + Tailwind apps** (chat, launcher, desktop) self-host Basel `@font-face`
50
- + geist-mono CDN import; tailwind `fontFamily.sans = ['Basel', …]`,
51
- `mono = ['Geist Mono', …]`.
52
- - **Tamagui (console)** → Basel `@font-face` in globals + override the Tamagui
53
- `body`/`heading` font `family` to Basel; Geist Mono for `code`/`pre`.
40
+ Then name the role, never the face: `var(--font-sans)`, `var(--font-mono)`.
41
+
42
+ ### Brand presets
43
+
44
+ `@hanzo/font/presets.css` carries five settings, each a point in Zen's own
45
+ parameter space that a brand has settled on. Two were FITTED by pixel residual
46
+ against the faces they replace, so the swap holds on metrics rather than on
47
+ taste.
48
+
49
+ | preset | wght | scaleX | track | use |
50
+ |---|---|---|---|---|
51
+ | `.zen-air` | 220 | — | −.030em | thin display |
52
+ | `.zen-book` | 497 | — | — | text |
53
+ | `.zen-medium` | 606 | — | — | UI emphasis |
54
+ | `.zen-wide` | 845 | 1.56 | −.040em | monumental caps |
55
+ | `.zen-round` | 900 | — | −.018em | heavy and friendly |
56
+
57
+ `.zen-wide` transforms, so its LAYOUT box stays the untransformed width — give
58
+ it room or clip its container.
59
+
60
+ **Zen's own alternates cover the neo-grotesque register**, so a brand that wants
61
+ that voice changes a feature setting rather than a family: `ss01` is the
62
+ single-storey `a`, `ss04` the straight-leg `R`, `ss09` a slashed zero and serif
63
+ one. Eleven sets ship.
54
64
 
55
65
  ---
56
66
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@hanzo/design",
3
- "version": "0.5.17",
3
+ "version": "0.5.19",
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": "MIT OR Apache-2.0",
@@ -30,8 +30,7 @@
30
30
  "./prompts/*": "./prompts/*",
31
31
  "./content/*": "./content/*",
32
32
  "./assets/*": "./assets/*",
33
- "./skills/*": "./skills/*",
34
- "./tailwind.css": "./tailwind.css"
33
+ "./skills/*": "./skills/*"
35
34
  },
36
35
  "scripts": {
37
36
  "gen": "node scripts/gen-tokens.mjs",
@@ -11,8 +11,15 @@ You are the **Hanzo Design System designer**, an agent operating on the
11
11
  system as real files, using your file tools (read, write, edit, tree).
12
12
 
13
13
  ## The canon (never contradict)
14
- - **Typography:** Basel Grotesk (`sans`, weights 400 Book / 500 Medium, self-hosted
15
- woff2) + Geist Mono (`mono`). No Inter/Roboto/DM Sans as defaults.
14
+ - **Typography:** Zen (`sans`) and Zen Mono (`mono`), both variable on `wght`
15
+ 100–900, plus Zen Pixel for display accents. ONE family, ours, no licensed
16
+ third-party face and no Inter/Roboto/DM Sans. An app imports
17
+ `@hanzo/design/styles.css` and gets the faces with it — never hand-write an
18
+ `@font-face` and never reach for a font CDN. Name the role (`var(--font-sans)`),
19
+ never the face. Brand voice comes from a preset in `@hanzo/font/presets.css`
20
+ (`.zen-air` `.zen-book` `.zen-medium` `.zen-wide` `.zen-round`) or from Zen's own
21
+ stylistic sets (`ss01` single-storey a, `ss04` straight-leg R), never from a
22
+ second family.
16
23
  - **Palette:** true-black OLED. Page `#000`, surface `#0a0a0a`, press `#050505`,
17
24
  elevated `#171717`, border `rgba(255,255,255,.10)`, text `#ededf1`. **Monochrome —
18
25
  no colored accents.** Semantic color only for live/error/warning.
@@ -60,7 +60,6 @@ const pass = (msg) => console.log(` ok ${msg}`)
60
60
  }
61
61
 
62
62
  // ── 1a. the bundles must PARSE ───────────────────────────────────────────
63
- // 0.4.2 and 0.4.3 shipped a stray `*/` in styles.css AND tailwind.css: an
64
63
  // edit added prose to the end of a comment that was already closed, so a
65
64
  // paragraph of English sat in the stylesheet as raw CSS, terminated by a
66
65
  // second `*/`. Browsers recover from that — they skip to the next thing that
@@ -78,7 +77,7 @@ const pass = (msg) => console.log(` ok ${msg}`)
78
77
  // that skips comment bodies finds every stray terminator and every unclosed
79
78
  // opener. A parser dependency would be a heavier answer to a smaller question.
80
79
  {
81
- for (const f of ['styles.css', 'tailwind.css']) {
80
+ for (const f of ['styles.css']) {
82
81
  const s = read(join(root, f))
83
82
  let i = 0, opened = 0, strays = [], unterminated = null
84
83
  while (i < s.length - 1) {
@@ -100,31 +99,11 @@ const pass = (msg) => console.log(` ok ${msg}`)
100
99
  }
101
100
  }
102
101
 
103
- // ── 1b. the Tailwind bridge is complete and self-contained ───────────────
104
- // An app should write ONE import and get working utilities. Each thing checked
105
- // here failed silently in production before it was checked: a missing slot
106
- // makes that utility resolve to nothing (`border-border` -> currentColor -> a
107
- // white hairline on black), and a surviving @import makes the browser drop
108
- // every token without a word.
109
- {
110
- const tw = strip(read(join(root, 'tailwind.css')))
111
- tw.includes('@import')
112
- ? fail('tailwind.css contains an @import — invalid after `@import "tailwindcss"`, so the tokens are dropped')
113
- : pass('tailwind.css has no @import to invalidate')
114
-
115
- // Every Tailwind colour slot an app will reach for must be mapped.
116
- const slots = ['background', 'foreground', 'card', 'popover', 'primary', 'secondary',
117
- 'muted', 'muted-foreground', 'accent', 'destructive', 'border', 'input', 'ring']
118
- const unmapped = slots.filter((s) => !tw.includes(`--color-${s}:`))
119
- unmapped.length
120
- ? fail(`tailwind.css does not map: ${unmapped.join(', ')}`)
121
- : pass(`tailwind.css maps all ${slots.length} core colour slots`)
122
-
123
- // And it must carry the values, not merely reference them.
124
- tw.includes('--border:')
125
- ? pass('tailwind.css carries the token values inline')
126
- : fail('tailwind.css maps slots but carries no tokens — every utility resolves to nothing')
127
- }
102
+ // 1b was the Tailwind bridge a generated `tailwind.css` mapping every token
103
+ // onto a `--color-*` utility slot. Gone with the utility layer it existed to
104
+ // feed: this system's one substrate is @hanzo/gui, which reads the tokens
105
+ // directly, so a second sheet republishing them as utilities was the "one fact,
106
+ // two homes" shape the `second-publisher` lint rule exists to catch.
128
107
 
129
108
  // ── 1c. element defaults must LOSE to an app's utilities ─────────────────
130
109
  // A rule outside a cascade layer beats a rule inside one regardless of
@@ -185,81 +185,3 @@ const bundle = banner + FILES.map((f) => {
185
185
  writeFileSync(join(root, 'styles.css'), bundle)
186
186
  console.log(`gen-tokens: wrote styles.css — ${FILES.length} token groups, flattened`)
187
187
 
188
- // ── tailwind.css — the ONE line a Tailwind app writes ────────────────────
189
- // Every app that consumed these tokens still had to hand-write the bridge from
190
- // our variable names to Tailwind's utility namespace:
191
- //
192
- // @theme inline { --color-background: var(--background); ...twenty more }
193
- //
194
- // Twenty lines of boilerplate, copied into each new app, is a drift vector with
195
- // a silent failure mode: miss one and that utility resolves to nothing —
196
- // `border-border` becomes currentColor and paints a white hairline on black,
197
- // which reads as a design choice rather than a missing line. Tabs shipped with
198
- // three of these and used forty-one.
199
- //
200
- // So the bridge is generated here from SLOTS, and an app writes one import
201
- // after `@import "tailwindcss"`. There is nothing left to get wrong, and adding
202
- // a semantic token wires it up for every app at once.
203
- const SLOTS = [
204
- 'background', 'foreground',
205
- 'card', 'card-foreground',
206
- 'popover', 'popover-foreground',
207
- 'primary', 'primary-foreground',
208
- 'secondary', 'secondary-foreground',
209
- 'muted', 'muted-foreground',
210
- 'accent', 'accent-foreground',
211
- 'destructive', 'destructive-foreground',
212
- 'border', 'input', 'ring',
213
- ]
214
- // State colours are their own vocabulary and keep their names, so `text-error`
215
- // and `bg-online` mean what they say instead of borrowing a neutral slot.
216
- const STATE = { error: 'state-error', success: 'state-success', online: 'state-online' }
217
-
218
- const declared = new Set(flat.map(([n]) => n.replace(/^--/, '')))
219
- const missing = SLOTS.filter((s) => !declared.has(s))
220
- if (missing.length) {
221
- console.error(`gen-tokens: tokens/colors.css declares no --${missing.join(', --')}`)
222
- process.exit(1)
223
- }
224
-
225
- const tw = `/* Hanzo Design System — Tailwind v4 bridge. The ONE import a Tailwind app adds.
226
- *
227
- * AUTO-GENERATED by scripts/gen-tokens.mjs — DO NOT EDIT.
228
- *
229
- * @import "tailwindcss";
230
- * @import "@hanzo/design/tailwind.css";
231
- *
232
- * It carries the tokens AND maps them onto Tailwind's utility namespace, so
233
- * \`bg-background\`, \`text-muted-foreground\` and \`border-border\` resolve to the
234
- * system's values with nothing to wire up by hand.
235
- *
236
- * Import it AFTER tailwindcss: @theme has to come after the framework it
237
- * extends, and the element defaults are layered so your utilities still win.
238
- *
239
- * "inline" is deliberate — it compiles \`border-border\` to var(--border) rather
240
- * than to that variable's build-time value, so switching .light at runtime moves
241
- * the UI and not merely the variables.
242
- *
243
- * The tokens are inlined below rather than @imported for the reason styles.css
244
- * is flattened: an @import must precede every other rule, so importing this
245
- * after tailwindcss would invalidate it and the browser would drop the whole
246
- * token layer without a word.
247
- */
248
- ${bundle}
249
-
250
- @theme inline {
251
- ${SLOTS.map((s) => ` --color-${s}: var(--${s});`).join('\n')}
252
-
253
- ${Object.entries(STATE).map(([k, v]) => ` --color-${k}: var(--${v});`).join('\n')}
254
-
255
- --radius-sm: var(--radius-sm);
256
- --radius-md: var(--radius-md);
257
- --radius-lg: var(--radius-lg);
258
- --radius-xl: var(--radius-xl);
259
-
260
- --font-sans: var(--font-sans);
261
- --font-mono: var(--font-mono);
262
- }
263
- `
264
- writeFileSync(join(root, 'tailwind.css'), tw)
265
- console.log(`gen-tokens: wrote tailwind.css — ${SLOTS.length} colour slots + ${Object.keys(STATE).length} states`)
package/src/tokens.gen.ts CHANGED
@@ -309,9 +309,9 @@ export const zIndex = {
309
309
 
310
310
  /** fonts tokens (from tokens/fonts.css). Values are raw CSS. */
311
311
  export const fonts = {
312
- 'font-sans': '"Geist","Geist Sans",ui-sans-serif,system-ui,sans-serif',
312
+ 'font-sans': '"Zen",ui-sans-serif,system-ui,sans-serif',
313
313
  'font-display': 'var(--font-sans)',
314
- 'font-mono': '"Geist Mono",ui-monospace,SFMono-Regular,monospace',
314
+ 'font-mono': '"Zen Mono",ui-monospace,SFMono-Regular,monospace',
315
315
  'font-serif': 'Georgia,serif',
316
316
  'font-feature-settings': '"ss01","ss02","cv01","cv02","cv03"',
317
317
  } as const
@@ -596,9 +596,9 @@ export const cssVars = {
596
596
  '--z-toast': '800',
597
597
  '--z-tooltip': 'var(--z-popover)',
598
598
  '--z-notification': 'var(--z-toast)',
599
- '--font-sans': '"Geist","Geist Sans",ui-sans-serif,system-ui,sans-serif',
599
+ '--font-sans': '"Zen",ui-sans-serif,system-ui,sans-serif',
600
600
  '--font-display': 'var(--font-sans)',
601
- '--font-mono': '"Geist Mono",ui-monospace,SFMono-Regular,monospace',
601
+ '--font-mono': '"Zen Mono",ui-monospace,SFMono-Regular,monospace',
602
602
  '--font-serif': 'Georgia,serif',
603
603
  '--font-feature-settings': '"ss01","ss02","cv01","cv02","cv03"',
604
604
  '--ring-offset': '-2px',
package/styles.css CHANGED
@@ -838,44 +838,67 @@
838
838
  }
839
839
 
840
840
  /* ── tokens/fonts.css ─────────────────────────────────────── */
841
- /* Geist Sans + Geist Mono — the only two faces on Hanzo surfaces.
841
+ /* Zen — the only family on Hanzo surfaces, sans and mono.
842
842
  SELF-HOSTED. The faces ship inside this package (assets/fonts/*.woff2, two
843
- variable files, 141 KB total, SIL OFL-1.1 — see assets/fonts/LICENSE-Geist.txt).
844
-
845
- Why self-hosted rather than @import from fonts.googleapis.com:
843
+ variable files, 141 KB total, SIL OFL-1.1 — see assets/fonts/LICENSE-Zen.txt).
844
+
845
+ Zen is our own family, under the SIL Open Font License. The licence file
846
+ beside the binaries carries every copyright the OFL requires; nothing else in
847
+ this repository restates them, which is the whole of what §2 asks for.
848
+
849
+ SHIPPED here rather than @import-ed from `@hanzo/font`, and that is load
850
+ bearing. `@hanzo/ui` composes this file into the MIDDLE of its theme.css, and
851
+ CSS drops an @import that does not precede every other rule — measured, in
852
+ Chromium, on the built sheet: zero faces loaded and not one woff2 requested.
853
+ An @font-face has no such ordering rule, so declaring the faces is the only
854
+ form that survives being flattened into a larger stylesheet. `@hanzo/font`
855
+ remains the one place the faces are AUTHORED; this is a copy of its output,
856
+ the same way the colour tokens are copied rather than fetched.
857
+
858
+ Why self-hosted rather than a third-party CDN:
846
859
  - A sign-in page must not make a third-party request. hanzoai/id refused to
847
860
  import this file for exactly that reason, which split the token layer: id
848
861
  took the colours and not the typeface. Self-hosting removes the reason, so
849
862
  every surface can import styles.css unchanged.
850
- - The @import was a render-blocking request to a host we do not control, on
851
- the critical path of every surface, and it broke offline/air-gapped dev.
863
+ - It was a render-blocking request to a host we do not control, on the
864
+ critical path of every surface, and it broke offline/air-gapped dev.
852
865
  - One variable file per family replaces nine static weights, and it is fewer
853
- bytes than the CSS-then-woff2 round trip Google served.
866
+ bytes than the CSS-then-woff2 round trip.
854
867
 
855
868
  The url()s are relative to THIS file, so they resolve wherever the package is
856
- mounted — node_modules, a CDN, a copied dist — with no configuration. */
869
+ mounted — node_modules, a CDN, a copied dist — with no configuration.
870
+
871
+ `font-display: block`, NOT swap, and the difference is the whole brand. `swap`
872
+ paints the fallback IMMEDIATELY and repaints when the face lands, so every first
873
+ visit shows the platform's own sans for as long as the download takes — on Ubuntu
874
+ that is DejaVu, which looks nothing like us and is the single most common way a
875
+ Hanzo surface renders wrong. `block` holds the text invisible for a short period
876
+ (~3s) and then paints Zen, so the fallback is only ever reached when the face
877
+ genuinely fails to arrive. A held word is a moment; the wrong typeface is the
878
+ brand. The generic tail below stays for exactly that real-failure case. */
857
879
 
858
880
  @font-face{
859
- font-family:"Geist";
860
- src:url("./assets/fonts/Geist-Variable.woff2") format("woff2");
881
+ font-family:"Zen";
882
+ src:url("./assets/fonts/Zen-Variable.woff2") format("woff2");
861
883
  font-weight:100 900;
862
884
  font-style:normal;
863
- font-display:swap;
885
+ font-display:block;
864
886
  }
865
887
  @font-face{
866
- font-family:"Geist Mono";
867
- src:url("./assets/fonts/GeistMono-Variable.woff2") format("woff2");
888
+ font-family:"Zen Mono";
889
+ src:url("./assets/fonts/ZenMono-Variable.woff2") format("woff2");
868
890
  font-weight:100 900;
869
891
  font-style:normal;
870
- font-display:swap;
892
+ font-display:block;
871
893
  }
872
894
 
873
895
  :root{
874
- --font-sans:"Geist","Geist Sans",ui-sans-serif,system-ui,sans-serif;
896
+ --font-sans:"Zen",ui-sans-serif,system-ui,sans-serif;
875
897
  --font-display:var(--font-sans);
876
- --font-mono:"Geist Mono",ui-monospace,SFMono-Regular,monospace;
898
+ --font-mono:"Zen Mono",ui-monospace,SFMono-Regular,monospace;
877
899
  --font-serif:Georgia,serif;
878
- /* hanzo.ai sets these OpenType features on <body>. */
900
+ /* hanzo.ai sets these OpenType features on <body>. ss01 is Zen's
901
+ single-storey a — the neo-grotesque letterform. */
879
902
  --font-feature-settings:"ss01","ss02","cv01","cv02","cv03";
880
903
  }
881
904
 
package/tokens/fonts.css CHANGED
@@ -1,40 +1,63 @@
1
- /* Geist Sans + Geist Mono — the only two faces on Hanzo surfaces.
1
+ /* Zen — the only family on Hanzo surfaces, sans and mono.
2
2
  SELF-HOSTED. The faces ship inside this package (assets/fonts/*.woff2, two
3
- variable files, 141 KB total, SIL OFL-1.1 — see assets/fonts/LICENSE-Geist.txt).
3
+ variable files, 141 KB total, SIL OFL-1.1 — see assets/fonts/LICENSE-Zen.txt).
4
4
 
5
- Why self-hosted rather than @import from fonts.googleapis.com:
5
+ Zen is our own family, under the SIL Open Font License. The licence file
6
+ beside the binaries carries every copyright the OFL requires; nothing else in
7
+ this repository restates them, which is the whole of what §2 asks for.
8
+
9
+ SHIPPED here rather than @import-ed from `@hanzo/font`, and that is load
10
+ bearing. `@hanzo/ui` composes this file into the MIDDLE of its theme.css, and
11
+ CSS drops an @import that does not precede every other rule — measured, in
12
+ Chromium, on the built sheet: zero faces loaded and not one woff2 requested.
13
+ An @font-face has no such ordering rule, so declaring the faces is the only
14
+ form that survives being flattened into a larger stylesheet. `@hanzo/font`
15
+ remains the one place the faces are AUTHORED; this is a copy of its output,
16
+ the same way the colour tokens are copied rather than fetched.
17
+
18
+ Why self-hosted rather than a third-party CDN:
6
19
  - A sign-in page must not make a third-party request. hanzoai/id refused to
7
20
  import this file for exactly that reason, which split the token layer: id
8
21
  took the colours and not the typeface. Self-hosting removes the reason, so
9
22
  every surface can import styles.css unchanged.
10
- - The @import was a render-blocking request to a host we do not control, on
11
- the critical path of every surface, and it broke offline/air-gapped dev.
23
+ - It was a render-blocking request to a host we do not control, on the
24
+ critical path of every surface, and it broke offline/air-gapped dev.
12
25
  - One variable file per family replaces nine static weights, and it is fewer
13
- bytes than the CSS-then-woff2 round trip Google served.
26
+ bytes than the CSS-then-woff2 round trip.
14
27
 
15
28
  The url()s are relative to THIS file, so they resolve wherever the package is
16
- mounted — node_modules, a CDN, a copied dist — with no configuration. */
29
+ mounted — node_modules, a CDN, a copied dist — with no configuration.
30
+
31
+ `font-display: block`, NOT swap, and the difference is the whole brand. `swap`
32
+ paints the fallback IMMEDIATELY and repaints when the face lands, so every first
33
+ visit shows the platform's own sans for as long as the download takes — on Ubuntu
34
+ that is DejaVu, which looks nothing like us and is the single most common way a
35
+ Hanzo surface renders wrong. `block` holds the text invisible for a short period
36
+ (~3s) and then paints Zen, so the fallback is only ever reached when the face
37
+ genuinely fails to arrive. A held word is a moment; the wrong typeface is the
38
+ brand. The generic tail below stays for exactly that real-failure case. */
17
39
 
18
40
  @font-face{
19
- font-family:"Geist";
20
- src:url("../assets/fonts/Geist-Variable.woff2") format("woff2");
41
+ font-family:"Zen";
42
+ src:url("../assets/fonts/Zen-Variable.woff2") format("woff2");
21
43
  font-weight:100 900;
22
44
  font-style:normal;
23
- font-display:swap;
45
+ font-display:block;
24
46
  }
25
47
  @font-face{
26
- font-family:"Geist Mono";
27
- src:url("../assets/fonts/GeistMono-Variable.woff2") format("woff2");
48
+ font-family:"Zen Mono";
49
+ src:url("../assets/fonts/ZenMono-Variable.woff2") format("woff2");
28
50
  font-weight:100 900;
29
51
  font-style:normal;
30
- font-display:swap;
52
+ font-display:block;
31
53
  }
32
54
 
33
55
  :root{
34
- --font-sans:"Geist","Geist Sans",ui-sans-serif,system-ui,sans-serif;
56
+ --font-sans:"Zen",ui-sans-serif,system-ui,sans-serif;
35
57
  --font-display:var(--font-sans);
36
- --font-mono:"Geist Mono",ui-monospace,SFMono-Regular,monospace;
58
+ --font-mono:"Zen Mono",ui-monospace,SFMono-Regular,monospace;
37
59
  --font-serif:Georgia,serif;
38
- /* hanzo.ai sets these OpenType features on <body>. */
60
+ /* hanzo.ai sets these OpenType features on <body>. ss01 is Zen's
61
+ single-storey a — the neo-grotesque letterform. */
39
62
  --font-feature-settings:"ss01","ss02","cv01","cv02","cv03";
40
63
  }
Binary file