Files
zeekay afa05263a4 IBM Plex Sans Arabic as a third Arabic companion; 0.5.38
@hanzo/font 1.9.6 carries IBM Plex Sans Arabic beside the two Noto faces,
and every companion now draws every letter the Arabic-script locales Hanzo
ships write. tokens/fonts.css declares Plex under its own family, one
@font-face per weight (IBM's own file each, uncut: the OFL reserves
"Plex"), and --font-arabic-plex names it. The preference's arabic axis
gains `plex`.

Plex is never declared under "Zen": a static face there wins the weight
match for Latin and refuses every Latin letter. The default stays Noto
Kufi Arabic, one variable file over Zen's 100-900.

Measured in Chromium against the built styles.css: an English page fetches
Zen and nothing else; an Arabic page fetches Noto Kufi Arabic and draws its
Latin in Zen; --font-arabic set to plex fetches the two Plex weights the
page sets (400, 700) and no others.

Claude-Session: https://claude.ai/code/session_015XYKLjZtDTJEnMo4eK3AFH
2026-10-29 23:39:53 -07:00

223 lines
10 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 three 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 three Arabic
companions, copied here with the Zen faces: Noto Kufi Arabic (the default,
geometric like Zen) and Noto Sans Arabic, each one variable file cut to the
Arabic blocks, and IBM Plex Sans Arabic, IBM's own file for each of seven
weights. All three draw every letter of every Arabic-script language Hanzo
ships. They are not ours and keep their names; their licences are
assets/fonts/LICENSE-Arabic.txt (Noto) and assets/fonts/LICENSE-Plex.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;
}
/* One file per weight, so it is offered under its own name and never under "Zen":
a static face in Zen's family would win the weight match for Latin at its own
weight and refuse every Latin letter (above). A page in Plex fetches the
weights it sets and no others. */
@font-face{
font-family:"IBM Plex Sans Arabic";
src:url("../assets/fonts/IBMPlexSansArabic-Thin.woff2") format("woff2");
font-weight:100;
font-style:normal;
font-display:block;
unicode-range:U+0600-06FF, U+0750-077F, U+FB50-FDFF, U+FE70-FEFC;
}
@font-face{
font-family:"IBM Plex Sans Arabic";
src:url("../assets/fonts/IBMPlexSansArabic-ExtraLight.woff2") format("woff2");
font-weight:200;
font-style:normal;
font-display:block;
unicode-range:U+0600-06FF, U+0750-077F, U+FB50-FDFF, U+FE70-FEFC;
}
@font-face{
font-family:"IBM Plex Sans Arabic";
src:url("../assets/fonts/IBMPlexSansArabic-Light.woff2") format("woff2");
font-weight:300;
font-style:normal;
font-display:block;
unicode-range:U+0600-06FF, U+0750-077F, U+FB50-FDFF, U+FE70-FEFC;
}
@font-face{
font-family:"IBM Plex Sans Arabic";
src:url("../assets/fonts/IBMPlexSansArabic-Regular.woff2") format("woff2");
font-weight:400;
font-style:normal;
font-display:block;
unicode-range:U+0600-06FF, U+0750-077F, U+FB50-FDFF, U+FE70-FEFC;
}
@font-face{
font-family:"IBM Plex Sans Arabic";
src:url("../assets/fonts/IBMPlexSansArabic-Medium.woff2") format("woff2");
font-weight:500;
font-style:normal;
font-display:block;
unicode-range:U+0600-06FF, U+0750-077F, U+FB50-FDFF, U+FE70-FEFC;
}
@font-face{
font-family:"IBM Plex Sans Arabic";
src:url("../assets/fonts/IBMPlexSansArabic-SemiBold.woff2") format("woff2");
font-weight:600;
font-style:normal;
font-display:block;
unicode-range:U+0600-06FF, U+0750-077F, U+FB50-FDFF, U+FE70-FEFC;
}
@font-face{
font-family:"IBM Plex Sans Arabic";
src:url("../assets/fonts/IBMPlexSansArabic-Bold.woff2") format("woff2");
font-weight:700;
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 their short names, and the one in use. A person's
choice replaces --font-arabic with one of the three names above it. */
--font-arabic-kufi:"Noto Kufi Arabic";
--font-arabic-sans:"Noto Sans Arabic";
--font-arabic-plex:"IBM Plex 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"}