mirror of
https://github.com/hanzoai/design.git
synced 2026-10-30 14:58:37 +00:00
@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
223 lines
10 KiB
CSS
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"}
|