Files
zeekay 96c95b31ed Arabic: the companions from @hanzo/font 1.9.4, --font-arabic first in every stack, and an arabic axis; 0.5.37
Zen draws no Arabic. tokens/fonts.css now declares Noto Kufi Arabic (the
default) and Noto Sans Arabic, each under its own family with an Arabic
unicode-range, and names --font-arabic first in the sans, mono and serif
stacks. Latin passes the companion over without a download, so an
English page fetches what it did before, and the first available font a
line box is measured from is still Zen.

The preference gains an arabic axis (kufi | sans), set by reference to
--font-arabic-kufi and --font-arabic-sans. It is its own axis because a
face and an Arabic companion are two questions: serif with Kufi is two
names, not a pairing enum. check-preference holds the axis and that
every stack names the slot first.
2026-10-22 19:34:37 -07:00

161 lines
8.2 KiB
CSS

/* Zen — the family on Hanzo surfaces, sans and mono, and its Arabic companions.
SELF-HOSTED. The faces ship inside this package (assets/fonts/*.woff2, two
variable files, 141 KB total, SIL OFL-1.1 — see assets/fonts/LICENSE-Zen.txt),
with two Arabic companions beside them (below).
Zen is our own family, under the SIL Open Font License. The licence file
beside the binaries carries every copyright the OFL requires; nothing else in
this repository restates them, which is the whole of what §2 asks for.
SHIPPED here rather than @import-ed from `@hanzo/font`, and that is load
bearing. `@hanzo/ui` composes this file into the MIDDLE of its theme.css, and
CSS drops an @import that does not precede every other rule — measured, in
Chromium, on the built sheet: zero faces loaded and not one woff2 requested.
An @font-face has no such ordering rule, so declaring the faces is the only
form that survives being flattened into a larger stylesheet. `@hanzo/font`
remains the one place the faces are AUTHORED; this is a copy of its output,
the same way the colour tokens are copied rather than fetched.
A copy is only as good as what it copied from, and this one went stale: the
floor sat at @hanzo/font ^1.8.8, whose faces carry Vercel's vendor tag under
our filenames, so every `npm run gen` overwrote the good committed Zen with
Geist and 0.5.23 and 0.5.24 both went out that way — the release commits hold
the right bytes, the tarballs did not. Two things stop it recurring, each in
its own place: from 1.9.2 the font package's build refuses to publish a face
that is not its own, and gen-tokens.mjs refuses to copy from anything below
that floor.
Why self-hosted rather than a third-party CDN:
- A sign-in page must not make a third-party request. hanzoai/id refused to
import this file for exactly that reason, which split the token layer: id
took the colours and not the typeface. Self-hosting removes the reason, so
every surface can import styles.css unchanged.
- It was a render-blocking request to a host we do not control, on the
critical path of every surface, and it broke offline/air-gapped dev.
- One variable file per family replaces nine static weights, and it is fewer
bytes than the CSS-then-woff2 round trip.
The url()s are relative to THIS file, so they resolve wherever the package is
mounted — node_modules, a CDN, a copied dist — with no configuration.
`font-display: block`, NOT swap, and the difference is the whole brand. `swap`
paints the fallback IMMEDIATELY and repaints when the face lands, so every first
visit shows the platform's own sans for as long as the download takes — on Ubuntu
that is DejaVu, which looks nothing like us and is the single most common way a
Hanzo surface renders wrong. `block` holds the text invisible for a short period
(~3s) and then paints Zen, so the fallback is only ever reached when the face
genuinely fails to arrive. A held word is a moment; the wrong typeface is the
brand. The generic tail below stays for exactly that real-failure case. */
@font-face{
font-family:"Zen";
src:url("../assets/fonts/Zen-Variable.woff2") format("woff2");
font-weight:100 900;
font-style:normal;
font-display:block;
}
@font-face{
font-family:"Zen Mono";
src:url("../assets/fonts/ZenMono-Variable.woff2") format("woff2");
font-weight:100 900;
font-style:normal;
font-display:block;
}
/* ARABIC. Zen draws Latin and Cyrillic and no Arabic, so an Arabic page used to
fall to whatever face each browser picked. @hanzo/font carries two Arabic
companions, variable over Zen's own weight axis and cut to the Arabic blocks,
and they are copied here with the Zen faces: Noto Kufi Arabic (the default,
geometric like Zen) and Noto Sans Arabic. Both draw every Arabic-script
language Hanzo ships. They are not ours and keep their names; their licence is
assets/fonts/LICENSE-Arabic.txt.
THE RANGE IS THE WHOLE TRICK. Each face covers the Arabic blocks only, so a
page with no Arabic on it never requests one, and Latin inside an Arabic page
is still Zen. The joiners (U+200C-200F) are drawn by the files but kept out of
the range: an English page with an emoji ZWJ sequence would otherwise fetch an
Arabic face for one invisible character.
Every stack below names `--font-arabic` FIRST. A family whose range excludes a
character is passed over without a download, so Latin falls through to Zen,
and the "first available font" a line box and `ch` are measured from is still
Zen (its range covers the space; the companion's does not). That is what makes
the choice one property: `@hanzo/appearance` writes `--font-arabic`, and sans,
mono and serif follow.
The default is ALSO declared under "Zen" and "Zen Mono", for anything that
names a family rather than a token. That only works because its descriptors
are Zen's exactly (weight 100 900, normal): faces in one family composite only
when everything but the range matches, and a companion declared at another
weight wins the weight match for Latin too, refuses every Latin letter, and
sends the page to the system sans with no font request to show for it.
`block` for the reason Zen has it: an Arabic page waits a moment for its face
rather than painting the platform's and repainting. */
@font-face{
font-family:"Zen";
src:url("../assets/fonts/NotoKufiArabic-Variable.woff2") format("woff2");
font-weight:100 900;
font-style:normal;
font-display:block;
unicode-range:U+0600-06FF, U+0750-077F, U+FB50-FDFF, U+FE70-FEFC;
}
@font-face{
font-family:"Zen Mono";
src:url("../assets/fonts/NotoKufiArabic-Variable.woff2") format("woff2");
font-weight:100 900;
font-style:normal;
font-display:block;
unicode-range:U+0600-06FF, U+0750-077F, U+FB50-FDFF, U+FE70-FEFC;
}
@font-face{
font-family:"Noto Kufi Arabic";
src:url("../assets/fonts/NotoKufiArabic-Variable.woff2") format("woff2");
font-weight:100 900;
font-style:normal;
font-display:block;
unicode-range:U+0600-06FF, U+0750-077F, U+FB50-FDFF, U+FE70-FEFC;
}
@font-face{
font-family:"Noto Sans Arabic";
src:url("../assets/fonts/NotoSansArabic-Variable.woff2") format("woff2");
font-weight:100 900;
font-style:normal;
font-display:block;
unicode-range:U+0600-06FF, U+0750-077F, U+FB50-FDFF, U+FE70-FEFC;
}
:root{
/* The Arabic companions by what they are, and the one in use. A person's
choice replaces --font-arabic with one of the two names above it. */
--font-arabic-kufi:"Noto Kufi Arabic";
--font-arabic-sans:"Noto Sans Arabic";
--font-arabic:var(--font-arabic-kufi);
--font-sans:var(--font-arabic), Zen, -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, "Helvetica Neue", Arial, sans-serif;
--font-display:var(--font-sans);
--font-mono:var(--font-arabic), Zen Mono, ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, "Liberation Mono", monospace;
--font-serif:var(--font-arabic), Georgia, serif;
/* The default is Zen as drawn, and that is a decision rather than an omission.
`base.css` puts this on <body>, so whatever is here is the voice of every
surface in the estate — which is exactly why it should not carry a brand's.
It used to read `"ss01","ss02","cv01","cv02","cv03"`, of which FOUR did
nothing: Zen has no cv01/cv02/cv03 at all, and ss01 and ss02 both remap `a`,
so ss01 substitutes first and ss02 never matches the glyph it was written
for. The one that did work quietly gave Lux and Zoo the single-storey `a`
that belongs to Hanzo.
A brand sets its own voice by overriding THIS token — one knob, one place:
Hanzo "ss01" single-storey a, the neo-grotesque register
Lux "ss04" straight-leg R, to sit under the wordmark
Zoo normal the humanist default, the warmest of the three
Zen ships eleven sets; ss03 is a tailed l, ss05 a serifed I, ss06 an
alternate G and ss09 a slashed zero with a serif one. Kerning, ligatures and
contextual alternates are on by default in every browser and do not belong
in this list. */
--font-feature-settings:normal;
}
/* The three voices above, set by the host's `data-brand` on <html>. */
[data-brand=hanzo]{--font-feature-settings:"ss01"}
[data-brand=lux]{--font-feature-settings:"ss04"}