commit 723d785bb8c91d7a287d73bcbbc4ba3302487ed4
parent 318e0c46e54b014a745186ff7d61b270d4da2a15
Author: I Mean I'm Just Saying <imeanimjustsaying@kiwifarms.st>
Date: Sat, 26 Sep 2026 02:26:29 -0400
common: brandMedia — the Archilyzer Media lockup, banner and mark M2 as pure SVG; the lockup's letters are outlines generated by bin/gen-media-glyphs.py (fontTools instancer, Archivo at wdth 118), MEDIA at 1.035x spread to the wordmark's measured 5.334 em, the banner's field a port of the canvas generator (CPython's Random(11), pinned line for line)
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Diffstat:
4 files changed, 968 insertions(+), 0 deletions(-)
diff --git a/common/bin/gen-media-glyphs.py b/common/bin/gen-media-glyphs.py
@@ -0,0 +1,241 @@
+#!/usr/bin/env python3
+"""Generate common/lib/brandMediaGlyphs.ts: the Archilyzer Media lockup's letters as outlines.
+
+The lockup (common/lib/brandMedia.ts mediaLockupSvg) is the `Archi|lyzer` wordmark over
+`MEDIA`, all Archivo at wdth 118. It is drawn as PATHS, not <text>, so anything that
+rasterises it -- rsvg-convert, satori's <img>, ImageMagick -- needs no font installed and
+cannot substitute one. This script is where those paths come from.
+
+For each run (`Archi` at wght 720, `lyzer` at 380, `MEDIA` at 700) it pins the variable
+font to that instance with the fontTools instancer and records, per glyph, the advance and
+the outline, plus the GPOS pair kerning between neighbours inside the run. (A browser does
+not kern ACROSS two spans of different weight -- they are two shaping runs -- so there is
+no kerning between `Archi` and `lyzer`, and none is recorded.)
+
+Coordinates are font units (the em is `unitsPerEm`), with y pointing DOWN and the baseline
+at 0, which is what an SVG `translate(x baseline) scale(px / unitsPerEm)` wants.
+
+The font is the copy vendored for the umtool report-to-video preset,
+umtool/report-to-video/fonts/Archivo[wdth,wght].ttf, so the outlines and the Pango-rendered
+titles beside them come from the same bytes. `--font` takes another path; `--download`
+fetches google/fonts' copy instead (into a temp dir, not the repo).
+
+ python3 common/bin/gen-media-glyphs.py # rewrite common/lib/brandMediaGlyphs.ts
+ python3 common/bin/gen-media-glyphs.py --stdout # print it (the parity test diffs this)
+
+Needs fontTools (>= 4.40, for the instancer's partial-instance API).
+"""
+import argparse
+import hashlib
+import os
+import sys
+import tempfile
+import urllib.request
+
+from fontTools.pens.svgPathPen import SVGPathPen
+from fontTools.pens.transformPen import TransformPen
+from fontTools.ttLib import TTFont
+from fontTools.varLib import instancer
+
+HERE = os.path.dirname(os.path.abspath(__file__))
+REPO = os.path.normpath(os.path.join(HERE, "..", ".."))
+VENDORED = os.path.join(REPO, "umtool", "report-to-video", "fonts", "Archivo[wdth,wght].ttf")
+OUT = os.path.join(REPO, "common", "lib", "brandMediaGlyphs.ts")
+FONT_URL = "https://raw.githubusercontent.com/google/fonts/main/ofl/archivo/Archivo%5Bwdth%2Cwght%5D.ttf"
+
+WDTH = 118
+# key, text, weight -- the three runs of plans/brand-and-themes.md "S4": the wordmark's
+# lead and suffix, and MEDIA.
+RUNS = [
+ ("archi", "Archi", 720),
+ ("lyzer", "lyzer", 380),
+ ("media", "MEDIA", 700),
+]
+
+
+def num(v):
+ """Font units to one decimal, without a trailing .0 -- stable text for the diff."""
+ r = round(float(v), 1)
+ if r == int(r):
+ return str(int(r))
+ return f"{r:.1f}"
+
+
+def glyph_path(glyphset, name):
+ """The outline as SVG path data, y flipped so the baseline is 0 and down is +y."""
+ pen = SVGPathPen(glyphset, ntos=num)
+ glyphset[name].draw(TransformPen(pen, (1, 0, 0, -1, 0, 0)))
+ return pen.getCommands()
+
+
+def _value_x(v):
+ return (getattr(v, "XAdvance", 0) or 0) if v is not None else 0
+
+
+def _pair_subtables(font):
+ """Every PairPos subtable of the GPOS 'kern' feature, in lookup order, unwrapped
+ from Extension (type 9) lookups."""
+ if "GPOS" not in font:
+ return []
+ gpos = font["GPOS"].table
+ if not gpos.FeatureList or not gpos.LookupList:
+ return []
+ indices = set()
+ for rec in gpos.FeatureList.FeatureRecord:
+ if rec.FeatureTag == "kern":
+ indices.update(rec.Feature.LookupListIndex)
+ subs = []
+ for li in sorted(indices):
+ lookup = gpos.LookupList.Lookup[li]
+ for sub in lookup.SubTable:
+ kind = lookup.LookupType
+ if kind == 9:
+ kind = sub.ExtensionLookupType
+ sub = sub.ExtSubTable
+ if kind == 2:
+ subs.append(sub)
+ return subs
+
+
+def pair_kerning(font, left, right):
+ """The XAdvance adjustment GPOS 'kern' applies to `left` followed by `right`.
+
+ Resolved the way a shaper resolves one pair: the first subtable that covers
+ `left` and answers for `right` wins. A format-1 subtable with no record for the
+ pair passes; a format-2 (class) subtable that covers `left` always answers,
+ zero included.
+ """
+ for sub in _pair_subtables(font):
+ cov = sub.Coverage.glyphs
+ if left not in cov:
+ continue
+ if sub.Format == 1:
+ for rec in sub.PairSet[cov.index(left)].PairValueRecord:
+ if rec.SecondGlyph == right:
+ return _value_x(rec.Value1)
+ elif sub.Format == 2:
+ c1 = sub.ClassDef1.classDefs.get(left, 0)
+ c2 = sub.ClassDef2.classDefs.get(right, 0)
+ return _value_x(sub.Class1Record[c1].Class2Record[c2].Value1)
+ return 0
+
+
+def run_data(varfont_path, text, wght):
+ font = TTFont(varfont_path)
+ inst = instancer.instantiateVariableFont(font, {"wght": wght, "wdth": WDTH})
+ cmap = inst.getBestCmap()
+ hmtx = inst["hmtx"]
+ glyphset = inst.getGlyphSet()
+ glyphs = []
+ for ch in text:
+ name = cmap[ord(ch)]
+ g = inst["glyf"][name]
+ g.recalcBounds(inst["glyf"]) # the instancer moves points; the stored bbox is the default master's
+ # Ink extent: `left` / `right` from the glyph origin, and y down, so `top` is
+ # how far it rises above the baseline (negative) and `bottom` how far it
+ # descends (positive for a descender).
+ top, bottom = (-g.yMax, -g.yMin) if g.numberOfContours else (0, 0)
+ left, right = (g.xMin, g.xMax) if g.numberOfContours else (0, 0)
+ glyphs.append({"char": ch, "name": name, "advance": hmtx[name][0], "left": left, "right": right,
+ "top": top, "bottom": bottom, "d": glyph_path(glyphset, name)})
+ kerning = [pair_kerning(inst, a["name"], b["name"]) for a, b in zip(glyphs, glyphs[1:])]
+ os2 = inst["OS/2"]
+ return {
+ "glyphs": glyphs,
+ "kerning": kerning,
+ "capHeight": os2.sCapHeight,
+ "upem": inst["head"].unitsPerEm,
+ }
+
+
+def render_ts(font_path):
+ sha = hashlib.sha256(open(font_path, "rb").read()).hexdigest()
+ runs = {key: run_data(font_path, text, wght) for key, text, wght in RUNS}
+ upem = {r["upem"] for r in runs.values()}
+ caps = {r["capHeight"] for r in runs.values()}
+ assert len(upem) == 1, f"unitsPerEm differs across instances: {upem}"
+ assert len(caps) == 1, f"cap height differs across weights: {caps}"
+ out = [
+ "// GENERATED by common/bin/gen-media-glyphs.py -- DO NOT EDIT. Re-run it instead:",
+ "// python3 common/bin/gen-media-glyphs.py",
+ "// brandMedia.test.ts diffs this file against a fresh run.",
+ "//",
+ "// The Archilyzer Media lockup's letters as outlines: Archivo[wdth,wght].ttf pinned to",
+ f"// wdth {WDTH}, one instance per run. Font units, y down, baseline at 0; `kerning[i]` is",
+ "// the GPOS pair adjustment between glyph i and glyph i + 1 of the same run.",
+ "",
+ "// `left` / `right` / `top` / `bottom`: the glyph's ink box from its origin, y down",
+ "// (`top` < 0 is above the baseline).",
+ "export type MediaGlyph = {",
+ " char: string;",
+ " advance: number;",
+ " left: number;",
+ " right: number;",
+ " top: number;",
+ " bottom: number;",
+ " d: string;",
+ "};",
+ "export type MediaRun = {",
+ " text: string;",
+ " wght: number;",
+ " glyphs: ReadonlyArray<MediaGlyph>;",
+ " kerning: ReadonlyArray<number>;",
+ "};",
+ "",
+ "export const MEDIA_GLYPH_SOURCE = {",
+ ' font: "Archivo[wdth,wght].ttf",',
+ f' sha256: "{sha}",',
+ f" wdth: {WDTH},",
+ "} as const;",
+ "",
+ f"export const MEDIA_UNITS_PER_EM = {upem.pop()};",
+ f"export const MEDIA_CAP_HEIGHT = {caps.pop()};",
+ "",
+ "export const MEDIA_RUNS = {",
+ ]
+ for key, text, wght in RUNS:
+ r = runs[key]
+ out.append(f" {key}: {{")
+ out.append(f' text: "{text}",')
+ out.append(f" wght: {wght},")
+ out.append(" glyphs: [")
+ for g in r["glyphs"]:
+ out.append(
+ f' {{ char: "{g["char"]}", advance: {g["advance"]}, left: {g["left"]}, right: {g["right"]}, '
+ f'top: {g["top"]}, bottom: {g["bottom"]},'
+ )
+ out.append(f' d: "{g["d"]}" }},')
+ out.append(" ],")
+ out.append(f" kerning: [{', '.join(str(k) for k in r['kerning'])}],")
+ out.append(" },")
+ out.append("} as const satisfies Record<string, MediaRun>;")
+ out.append("")
+ return "\n".join(out)
+
+
+def main():
+ ap = argparse.ArgumentParser(description=__doc__.split("\n")[0])
+ ap.add_argument("--font", help="Archivo[wdth,wght].ttf to read (default: the umtool vendored copy)")
+ ap.add_argument("--download", action="store_true", help="fetch google/fonts' copy into a temp dir")
+ ap.add_argument("--stdout", action="store_true", help="print instead of writing " + os.path.relpath(OUT, REPO))
+ args = ap.parse_args()
+
+ font = args.font or VENDORED
+ if args.download:
+ tmp = tempfile.mkdtemp(prefix="archivo-")
+ font = os.path.join(tmp, "Archivo[wdth,wght].ttf")
+ urllib.request.urlretrieve(FONT_URL, font)
+ if not os.path.exists(font):
+ sys.exit(f"no font at {font} -- pass --font <path> or --download")
+
+ ts = render_ts(font)
+ if args.stdout:
+ sys.stdout.write(ts)
+ else:
+ with open(OUT, "w", encoding="utf-8") as f:
+ f.write(ts)
+ print(f"wrote {os.path.relpath(OUT, REPO)}", file=sys.stderr)
+
+
+if __name__ == "__main__":
+ main()
diff --git a/common/lib/brandMedia.test.ts b/common/lib/brandMedia.test.ts
@@ -0,0 +1,160 @@
+import { test } from "node:test";
+import assert from "node:assert/strict";
+import { execFileSync, spawnSync } from "node:child_process";
+import { readFileSync } from "node:fs";
+import path from "node:path";
+import { fileURLToPath } from "node:url";
+import { ACCENTS, ICON_PALETTES, markSvg } from "./brand";
+import {
+ MEDIA_BANNER,
+ MEDIA_MARK_PALETTE,
+ MEDIA_PALETTE,
+ MEDIA_RATIO,
+ PyRandom,
+ mediaBannerField,
+ mediaBannerLayout,
+ mediaBannerSvg,
+ mediaLockupGeometry,
+ mediaLockupSvg,
+ mediaMarkSvg,
+ mediaWordAdvanceEm,
+ mediaWordmarkAdvanceEm,
+} from "./brandMedia";
+import { MEDIA_CAP_HEIGHT, MEDIA_GLYPH_SOURCE, MEDIA_RUNS } from "./brandMediaGlyphs";
+
+const REPO = path.resolve(path.dirname(fileURLToPath(import.meta.url)), "../..");
+
+test("palette: mark M2 is the parent mark's slate, lit in Signal", () => {
+ assert.deepEqual(MEDIA_MARK_PALETTE, {
+ ground: ICON_PALETTES.archilyzer.ground,
+ dim: ICON_PALETTES.archilyzer.dim,
+ lit: ACCENTS.signal.onDark,
+ });
+ assert.deepEqual(MEDIA_PALETTE, {
+ ground: "#151b20",
+ dim: "#3f4c56",
+ lit: "#5fa8a0",
+ fg: "#e7edf1",
+ muted: "#8496a2",
+ media: "#5fa8a0",
+ amber: "#e3b15c",
+ });
+ assert.equal(MEDIA_RATIO, 1.035);
+ // The mark is markSvg's, not a second drawing.
+ assert.equal(mediaMarkSvg("maskable"), markSvg(MEDIA_MARK_PALETTE, { variant: "maskable" }));
+ assert.equal(mediaMarkSvg(), markSvg(MEDIA_MARK_PALETTE));
+});
+
+test("glyphs: the measured widths of plans/brand-and-themes.md S4 (5.334 em, 3.953 em, caps 0.686 em)", () => {
+ assert.equal(MEDIA_GLYPH_SOURCE.wdth, 118);
+ assert.deepEqual(
+ [MEDIA_RUNS.archi, MEDIA_RUNS.lyzer, MEDIA_RUNS.media].map((r) => [r.text, r.wght]),
+ [["Archi", 720], ["lyzer", 380], ["MEDIA", 700]],
+ );
+ assert.equal(Number(mediaWordmarkAdvanceEm().toFixed(3)), 5.334);
+ assert.equal(Number(mediaWordAdvanceEm().toFixed(3)), 3.953);
+ assert.equal(MEDIA_CAP_HEIGHT, 686);
+});
+
+test("lockup: MEDIA spans the wordmark's advance (within 0.5 %) with 0.30 em between its letters", () => {
+ for (const px of [18, 28, 64, 124]) {
+ const g = mediaLockupGeometry(px);
+ assert.ok(
+ Math.abs(g.mediaAdvance - g.wordmarkAdvance) / g.wordmarkAdvance < 0.005,
+ `px ${px}: MEDIA ${g.mediaAdvance} vs wordmark ${g.wordmarkAdvance}`,
+ );
+ assert.ok(Math.abs(g.mediaAdvance - g.textWidth) < 1e-6);
+ assert.ok(Math.abs(g.mediaSpacing / g.mediaPx - 0.3) < 0.005, `spacing ${g.mediaSpacing / g.mediaPx} em`);
+ assert.ok(Math.abs(g.mediaPx - px * MEDIA_RATIO) < 1e-9);
+ // The mark is as tall as the block, which is about 1.70 x the wordmark.
+ assert.equal(g.markSize, g.blockHeight);
+ assert.ok(Math.abs(g.blockHeight / px - 1.7) < 0.01, `block ${g.blockHeight / px} x`);
+ // The ink of both lines starts at the text origin and ends within a hair of
+ // the shared advance: flush left and right.
+ const lines = [g.glyphs.filter((p) => p.tone !== "media"), g.glyphs.filter((p) => p.tone === "media")];
+ for (const line of lines) {
+ const first = line[0];
+ const last = line[line.length - 1];
+ assert.ok(first.glyph.left * first.scale < 0.1 * px, "left bearing");
+ const right = last.x + last.glyph.right * last.scale;
+ assert.ok(Math.abs(g.textWidth - right) < 0.08 * px, `right edge ${right} vs ${g.textWidth}`);
+ }
+ }
+});
+
+test("lockup on the banner: the approved numbers, inside the safe area", () => {
+ const { geometry: g, block, ink } = mediaBannerLayout();
+ assert.equal(MEDIA_BANNER.wordmarkPx, 124);
+ assert.equal(Math.round(g.mediaPx), 128);
+ assert.equal(Math.round(g.textWidth), 661);
+ assert.equal(Math.round(g.markSize), 210);
+ const S = MEDIA_BANNER.safe;
+ for (const box of [block, ink]) {
+ assert.ok(box.x >= S.x && box.y >= S.y, `${JSON.stringify(box)} starts inside the safe area`);
+ assert.ok(box.x + box.width <= S.x + S.width, "ends inside horizontally");
+ assert.ok(box.y + box.height <= S.y + S.height, "ends inside vertically");
+ }
+ // Centred on it.
+ assert.ok(Math.abs(block.x + block.width / 2 - (S.x + S.width / 2)) < 1e-6);
+ assert.ok(Math.abs(block.y + block.height / 2 - (S.y + S.height / 2)) < 1e-6);
+ // And the field keeps out of the gutter round it.
+ for (const [x, y, w] of mediaBannerField()) {
+ const inBandY = y + MEDIA_BANNER.line.height > S.y - MEDIA_BANNER.gutter.y && y < S.y + S.height + MEDIA_BANNER.gutter.y;
+ if (!inBandY) continue;
+ const clear = x + w <= S.x - MEDIA_BANNER.gutter.x || x >= S.x + S.width + MEDIA_BANNER.gutter.x;
+ assert.ok(clear, `line ${x},${y} +${w} crosses the gutter`);
+ }
+});
+
+test("PyRandom is CPython's random.Random: the canvas field, line for line", () => {
+ // python3 -c "import random; r = random.Random(11); print([r.getrandbits(32) for _ in range(5)])"
+ const r = new PyRandom(11);
+ assert.deepEqual(
+ Array.from({ length: 5 }, () => r.genrandUint32()),
+ [1942955373, 3718334796, 2404204071, 3680198571, 3969454221],
+ );
+ // r = random.Random(11); [r.choice([0, 0, 0, 96, 192]) for _ in range(8)]
+ const c = new PyRandom(11);
+ assert.deepEqual(
+ Array.from({ length: 8 }, () => c.choice([0, 0, 0, 96, 192])),
+ [96, 192, 96, 96, 192, 192, 0, 0],
+ );
+ // The canvas generator's banner() field (gen_media.py), 87 lines.
+ const field = mediaBannerField();
+ assert.equal(field.length, 87);
+ assert.deepEqual(field.slice(0, 3), [[160, 40, 300], [488, 40, 380], [896, 40, 380]]);
+ assert.deepEqual(field.slice(-3), [[1658, 1384, 190], [1876, 1384, 300], [2204, 1384, 172]]);
+});
+
+test("SVGs: well-formed roots, colours validated, the safe crop is a viewBox", () => {
+ const lockup = mediaLockupSvg({ px: 28 });
+ assert.match(lockup, /^<svg xmlns="http:\/\/www\.w3\.org\/2000\/svg" width="[\d.]+" height="[\d.]+" viewBox="0 0 [\d.]+ [\d.]+">/);
+ assert.ok(lockup.includes(`fill="${MEDIA_PALETTE.fg}"`) && lockup.includes(`fill="${MEDIA_PALETTE.muted}"`));
+ assert.equal((lockup.match(/<path /g) ?? []).length, 15);
+ assert.doesNotMatch(lockup, /<text/);
+ assert.throws(() => mediaLockupSvg({ px: 28, palette: { ...MEDIA_PALETTE, fg: "red" } }), /palette\.fg/);
+ assert.throws(() => mediaLockupGeometry(0), /px must be > 0/);
+
+ const banner = mediaBannerSvg();
+ assert.match(banner, /^<svg [^>]*width="2560" height="1440" viewBox="0 0 2560 1440">/);
+ assert.equal((banner.match(/<rect x="\d+" y="\d+" width="\d+" height="44" rx="22"\/>/g) ?? []).length, 87);
+ const safe = mediaBannerSvg({ view: MEDIA_BANNER.safe });
+ assert.match(safe, /^<svg [^>]*width="1546" height="423" viewBox="507 508.5 1546 423">/);
+ assert.equal(safe.slice(safe.indexOf(">")), banner.slice(banner.indexOf(">")));
+});
+
+// The glyph file is generated; a hand edit or a font swap without a re-run is a
+// failure. Needs python3 + fontTools, which a test machine may not have.
+test("brandMediaGlyphs.ts is exactly what gen-media-glyphs.py writes", (t) => {
+ const probe = spawnSync("python3", ["-c", "import fontTools.varLib.instancer"], { stdio: "ignore" });
+ if (probe.status !== 0) {
+ t.skip("python3 with fontTools is not available");
+ return;
+ }
+ const fresh = execFileSync("python3", [path.join(REPO, "common/bin/gen-media-glyphs.py"), "--stdout"], {
+ encoding: "utf8",
+ maxBuffer: 1 << 24,
+ });
+ const committed = readFileSync(path.join(REPO, "common/lib/brandMediaGlyphs.ts"), "utf8");
+ assert.equal(committed, fresh);
+});
diff --git a/common/lib/brandMedia.ts b/common/lib/brandMedia.ts
@@ -0,0 +1,479 @@
+// ARCHILYZER MEDIA — the YouTube channel's brand, as data and pure SVG.
+//
+// plans/brand-and-themes.md "S4 — Archilyzer Media" is the source of every
+// value here, and the approved canvas (row "Archilyzer Media · the YouTube
+// channel") is what each drawing reproduces:
+//
+// - the mark is M2 "Signal": the parent mark's geometry (lib/brand.ts MARK,
+// drawn by markSvg — never redrawn here), lit in Signal;
+// - the lockup is "flush · caps matched": the `Archi|lyzer` wordmark over
+// `MEDIA`, both Archivo at wdth 118, MEDIA at MEDIA_RATIO × the wordmark's
+// size and letter-spaced to exactly the wordmark's width, the mark as tall
+// as the text block beside it;
+// - the banner is a field of dim transcript lines wrapping round a clear
+// gutter about YouTube's safe area, the lockup centred in it, and one lit
+// found line in the desktop strip to its right.
+//
+// The letters are OUTLINES from lib/brandMediaGlyphs.ts (generated by
+// common/bin/gen-media-glyphs.py from the Archivo the umtool preset vendors),
+// so rendering any of this needs no font: rsvg-convert, satori's <img> and
+// ImageMagick all draw the same shapes.
+//
+// PURE: no I/O, no framework imports. The PNG side is common/bin/brand-media.ts.
+
+import { ACCENTS, MARK_VIEWBOX, markSvg, type IconPalette, type MarkVariant } from "./brand";
+import {
+ MEDIA_CAP_HEIGHT,
+ MEDIA_RUNS,
+ MEDIA_UNITS_PER_EM,
+ type MediaGlyph,
+ type MediaRun,
+} from "./brandMediaGlyphs";
+
+// ── Palette ─────────────────────────────────────────────────────────────────
+
+// The slate of the parent mark (lib/brand.ts ICON_PALETTES.archilyzer), lit in
+// Signal. `media` is MEDIA's ink; `amber` is the annotation colour the video
+// preset carries for kickers and markers.
+export type MediaPalette = {
+ ground: string;
+ dim: string;
+ lit: string;
+ fg: string;
+ muted: string;
+ media: string;
+};
+
+export const MEDIA_PALETTE = {
+ ground: "#151b20",
+ dim: "#3f4c56",
+ lit: ACCENTS.signal.onDark,
+ fg: "#e7edf1",
+ muted: "#8496a2",
+ media: ACCENTS.signal.onDark,
+ amber: "#e3b15c",
+} as const satisfies MediaPalette & { amber: string };
+
+// Mark M2 · Signal.
+export const MEDIA_MARK_PALETTE: IconPalette = {
+ ground: MEDIA_PALETTE.ground,
+ dim: MEDIA_PALETTE.dim,
+ lit: MEDIA_PALETTE.lit,
+};
+
+// The dim lines of the banner's field: a step above the ground, below the
+// mark's own dim, so the field reads as texture and the mark as the object.
+export const MEDIA_FIELD_LINE = "#1e262d";
+
+// ── The lockup's proportions ────────────────────────────────────────────────
+
+// MEDIA's size over the wordmark's. At this ratio MEDIA, spread to the
+// wordmark's width, has 0.30 em between its letters.
+export const MEDIA_RATIO = 1.035;
+// The wordmark's tracking, as the sites set it (common/components/Wordmark.tsx).
+// Its measured width — the advances of `Archi` at 720 and `lyzer` at 380, less
+// this per letter — is the width both lines share: 5.334 em.
+export const MEDIA_WORDMARK_TRACKING_EM = -0.01;
+// From the wordmark's baseline to MEDIA's cap top, in wordmark em.
+export const MEDIA_LINE_GAP_EM = 0.3;
+// From the mark's right edge to the text, in wordmark em.
+export const MEDIA_MARK_GAP_EM = 0.46;
+
+const EM = MEDIA_UNITS_PER_EM;
+const HEX_COLOR_RE = /^#[0-9a-f]{6}$/i;
+
+const sum = (xs: ReadonlyArray<number>): number => xs.reduce((a, b) => a + b, 0);
+const advances = (r: MediaRun): number => sum(r.glyphs.map((g) => g.advance));
+
+// The wordmark's shared width in em: the spec's measurement, from the glyphs.
+export function mediaWordmarkAdvanceEm(): number {
+ const letters = MEDIA_RUNS.archi.glyphs.length + MEDIA_RUNS.lyzer.glyphs.length;
+ return (advances(MEDIA_RUNS.archi) + advances(MEDIA_RUNS.lyzer)) / EM + MEDIA_WORDMARK_TRACKING_EM * letters;
+}
+
+// MEDIA's natural advance (no spacing) in em of its own size.
+export function mediaWordAdvanceEm(): number {
+ return (advances(MEDIA_RUNS.media) + sum(MEDIA_RUNS.media.kerning)) / EM;
+}
+
+export type PlacedGlyph = { glyph: MediaGlyph; x: number; baseline: number; scale: number; tone: "fg" | "muted" | "media" };
+
+export type MediaLockupGeometry = {
+ px: number;
+ // The block: wordmark cap top (y 0) to MEDIA's baseline, mark to text end.
+ blockWidth: number;
+ blockHeight: number;
+ markSize: number;
+ textX: number;
+ textWidth: number;
+ wordmarkBaseline: number;
+ mediaPx: number;
+ mediaBaseline: number;
+ // MEDIA's letter gap, px.
+ mediaSpacing: number;
+ // The ink's overshoot above the block (the ascenders of h, i and l rise
+ // past the cap line), px; the SVG adds it on top so nothing is clipped.
+ inkTop: number;
+ glyphs: ReadonlyArray<PlacedGlyph>;
+ // Where each line's advance ends, px from the text origin.
+ wordmarkAdvance: number;
+ mediaAdvance: number;
+};
+
+// Place the lockup at a wordmark size of `px`. Block coordinates: the mark's
+// top-left is (0, 0), which is also the wordmark's cap line.
+export function mediaLockupGeometry(px: number): MediaLockupGeometry {
+ if (!(px > 0) || !Number.isFinite(px)) throw new Error(`mediaLockupGeometry: px must be > 0, got ${px}`);
+ const s = px / EM;
+ const mediaPx = px * MEDIA_RATIO;
+ const sm = mediaPx / EM;
+ const cap = MEDIA_CAP_HEIGHT / EM;
+
+ const wordmarkBaseline = cap * px;
+ const mediaBaseline = wordmarkBaseline + MEDIA_LINE_GAP_EM * px + cap * mediaPx;
+ const blockHeight = mediaBaseline;
+ const markSize = blockHeight;
+ const textX = markSize + MEDIA_MARK_GAP_EM * px;
+ const width = mediaWordmarkAdvanceEm() * px;
+
+ const glyphs: PlacedGlyph[] = [];
+
+ // The wordmark: kerned inside each run (a browser kerns inside a span, not
+ // across two spans of different weight), then one uniform adjustment per
+ // gap brings the whole line to the measured width. That is the canvas's
+ // `textLength` + `lengthAdjust="spacing"`.
+ const word: Array<{ glyph: MediaGlyph; tone: "fg" | "muted"; kernAfter: number }> = [];
+ for (const [run, tone] of [
+ [MEDIA_RUNS.archi, "fg"],
+ [MEDIA_RUNS.lyzer, "muted"],
+ ] as const) {
+ run.glyphs.forEach((glyph, i) => word.push({ glyph, tone, kernAfter: run.kerning[i] ?? 0 }));
+ }
+ const natural = sum(word.map((w, i) => w.glyph.advance + (i < word.length - 1 ? w.kernAfter : 0))) * s;
+ const perGap = (width - natural) / (word.length - 1);
+ let x = 0;
+ word.forEach((w, i) => {
+ glyphs.push({ glyph: w.glyph, x, baseline: wordmarkBaseline, scale: s, tone: w.tone });
+ x += w.glyph.advance * s + (i < word.length - 1 ? w.kernAfter * s + perGap : 0);
+ });
+ const wordmarkAdvance = x;
+
+ // MEDIA: its own advances (and kerning, if the font has any for it) at
+ // MEDIA_RATIO × px, the rest of the width shared equally between its gaps.
+ const media = MEDIA_RUNS.media;
+ const mediaSpacing = (width - mediaWordAdvanceEm() * mediaPx) / (media.glyphs.length - 1);
+ x = 0;
+ media.glyphs.forEach((glyph, i) => {
+ glyphs.push({ glyph, x, baseline: mediaBaseline, scale: sm, tone: "media" });
+ x += glyph.advance * sm + (i < media.glyphs.length - 1 ? (media.kerning[i] ?? 0) * sm + mediaSpacing : 0);
+ });
+ const mediaAdvance = x;
+
+ const inkTop = Math.max(0, -Math.min(...glyphs.map((g) => g.baseline + g.glyph.top * g.scale)));
+
+ return {
+ px,
+ blockWidth: textX + width,
+ blockHeight,
+ markSize,
+ textX,
+ textWidth: width,
+ wordmarkBaseline,
+ mediaPx,
+ mediaBaseline,
+ mediaSpacing,
+ inkTop,
+ glyphs,
+ wordmarkAdvance,
+ mediaAdvance,
+ };
+}
+
+const fmt = (n: number): string => String(Number(n.toFixed(3)));
+
+function checkPalette(fn: string, palette: Record<string, string>): void {
+ for (const [slot, colour] of Object.entries(palette)) {
+ if (!HEX_COLOR_RE.test(colour)) {
+ throw new Error(`${fn}: palette.${slot} must be #rrggbb, got ${JSON.stringify(colour)}`);
+ }
+ }
+}
+
+// markSvg's document, re-rooted as a nested <svg> at (x, y) and `size` square.
+function nestedMark(palette: IconPalette, x: number, y: number, size: number, variant: MarkVariant = "any"): string {
+ const doc = markSvg(palette, { variant });
+ const open = `<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 ${MARK_VIEWBOX} ${MARK_VIEWBOX}">`;
+ if (!doc.startsWith(open)) throw new Error("nestedMark: markSvg's root changed shape");
+ return (
+ `<svg x="${fmt(x)}" y="${fmt(y)}" width="${fmt(size)}" height="${fmt(size)}" viewBox="0 0 ${MARK_VIEWBOX} ${MARK_VIEWBOX}">` +
+ doc.slice(open.length)
+ );
+}
+
+// The lockup's shapes, positioned with its block's top-left at (x, y).
+function lockupBody(g: MediaLockupGeometry, palette: MediaPalette, x: number, y: number): string {
+ const mark = nestedMark({ ground: palette.ground, dim: palette.dim, lit: palette.lit }, x, y, g.markSize);
+ const byTone = new Map<string, string[]>();
+ for (const p of g.glyphs) {
+ const fill = palette[p.tone].toLowerCase();
+ const list = byTone.get(fill) ?? [];
+ list.push(
+ `<path transform="translate(${fmt(x + g.textX + p.x)} ${fmt(y + p.baseline)}) scale(${fmt(p.scale)})" d="${p.glyph.d}"/>`,
+ );
+ byTone.set(fill, list);
+ }
+ const text = [...byTone].map(([fill, paths]) => `<g fill="${fill}">${paths.join("")}</g>`).join("");
+ return mark + text;
+}
+
+// The lockup as a standalone SVG document at a wordmark size of `px`. Its
+// box is the block plus the ascenders' overshoot on top; `mediaLockupGeometry`
+// says where the block sits in it (y = inkTop).
+export function mediaLockupSvg({ px, palette = MEDIA_PALETTE }: { px: number; palette?: MediaPalette }): string {
+ checkPalette("mediaLockupSvg", palette);
+ const g = mediaLockupGeometry(px);
+ const w = fmt(g.blockWidth);
+ const h = fmt(g.blockHeight + g.inkTop);
+ return (
+ `<svg xmlns="http://www.w3.org/2000/svg" width="${w}" height="${h}" viewBox="0 0 ${w} ${h}">` +
+ lockupBody(g, palette, 0, g.inkTop) +
+ `</svg>`
+ );
+}
+
+// ── The video kit ───────────────────────────────────────────────────────────
+
+// What umtool's report-to-video preset needs from here, as JSON: its scripts
+// are plain `node` .mjs and cannot import TypeScript, so common/bin/
+// brand-media.ts --video-kit writes this to
+// umtool/report-to-video/brands/archilyzer-media.json and brandMedia.test.ts
+// holds the committed file to it. The lockup is drawn at `px` and scaled by
+// the renderer; `blockTop` is where its design box starts inside the SVG.
+export const MEDIA_VIDEO_KIT_PX = 100;
+
+export function mediaVideoKit() {
+ const g = mediaLockupGeometry(MEDIA_VIDEO_KIT_PX);
+ return {
+ $comment:
+ "GENERATED by common/bin/brand-media.ts --video-kit from common/lib/brandMedia.ts -- do not edit; re-run it.",
+ id: "archilyzer-media",
+ palette: {
+ bg: MEDIA_PALETTE.ground,
+ fg: MEDIA_PALETTE.fg,
+ muted: MEDIA_PALETTE.muted,
+ accent: MEDIA_PALETTE.lit,
+ amber: MEDIA_PALETTE.amber,
+ dim: MEDIA_PALETTE.dim,
+ },
+ mark: { any: mediaMarkSvg("any") },
+ lockup: {
+ px: MEDIA_VIDEO_KIT_PX,
+ width: Number(fmt(g.blockWidth)),
+ height: Number(fmt(g.blockHeight + g.inkTop)),
+ blockTop: Number(fmt(g.inkTop)),
+ blockHeight: Number(fmt(g.blockHeight)),
+ svg: mediaLockupSvg({ px: MEDIA_VIDEO_KIT_PX }),
+ },
+ };
+}
+
+// ── The banner ──────────────────────────────────────────────────────────────
+
+// YouTube's channel banner: 2560 × 1440, of which every device shows the
+// centred 1546 × 423 (the "safe area"); a desktop shows the full-width strip
+// through it, a TV the whole image.
+export const MEDIA_BANNER = {
+ width: 2560,
+ height: 1440,
+ safe: { x: 507, y: 508.5, width: 1546, height: 423 },
+ // The lockup's wordmark size on the banner: MEDIA comes out at 128 px, the
+ // text at 661 px wide, the mark 210 px — the approved canvas's numbers.
+ wordmarkPx: 124,
+ // The field's lines stay out of a gutter this wide round the safe area.
+ gutter: { x: 48, y: 36 },
+ line: { height: 44, pitch: 84, first: 40, last: 1400, left: 64, right: 2496, gap: 28, minWidth: 60 },
+ // The one lit line: a play head and a bar, right of the safe area.
+ found: { y: 712, head: [[2150, 710], [2210, 734], [2150, 758]] as ReadonlyArray<readonly [number, number]>, x: 2226, width: 236 },
+ seed: 11,
+} as const;
+
+// CPython's `random.Random(seed).choice`, exactly: MT19937 seeded by
+// init_by_array, and `_randbelow` by rejection over `getrandbits(k)`. The
+// canvas generator drew the field with Random(11); porting the generator
+// rather than transcribing its output keeps the banner a function, not a
+// table, and the test pins the two against each other.
+export class PyRandom {
+ private readonly mt = new Uint32Array(624);
+ private mti = 625;
+
+ constructor(seed: number) {
+ if (!Number.isInteger(seed) || seed < 0 || seed > 0xffffffff) {
+ throw new Error(`PyRandom: seed must be a 32-bit non-negative integer, got ${seed}`);
+ }
+ this.initByArray([seed]);
+ }
+
+ private initGenrand(s: number): void {
+ const mt = this.mt;
+ mt[0] = s >>> 0;
+ for (let i = 1; i < 624; i++) {
+ mt[i] = (Math.imul(1812433253, mt[i - 1] ^ (mt[i - 1] >>> 30)) + i) >>> 0;
+ }
+ this.mti = 624;
+ }
+
+ private initByArray(key: ReadonlyArray<number>): void {
+ const mt = this.mt;
+ this.initGenrand(19650218);
+ let i = 1;
+ let j = 0;
+ for (let k = Math.max(624, key.length); k > 0; k--) {
+ mt[i] = ((mt[i] ^ Math.imul(mt[i - 1] ^ (mt[i - 1] >>> 30), 1664525)) + key[j] + j) >>> 0;
+ i++;
+ j++;
+ if (i >= 624) {
+ mt[0] = mt[623];
+ i = 1;
+ }
+ if (j >= key.length) j = 0;
+ }
+ for (let k = 623; k > 0; k--) {
+ mt[i] = ((mt[i] ^ Math.imul(mt[i - 1] ^ (mt[i - 1] >>> 30), 1566083941)) - i) >>> 0;
+ i++;
+ if (i >= 624) {
+ mt[0] = mt[623];
+ i = 1;
+ }
+ }
+ mt[0] = 0x80000000;
+ }
+
+ genrandUint32(): number {
+ const mt = this.mt;
+ if (this.mti >= 624) {
+ for (let kk = 0; kk < 624; kk++) {
+ const y = (mt[kk] & 0x80000000) | (mt[(kk + 1) % 624] & 0x7fffffff);
+ mt[kk] = mt[(kk + 397) % 624] ^ (y >>> 1) ^ (y & 1 ? 0x9908b0df : 0);
+ }
+ this.mti = 0;
+ }
+ let y = mt[this.mti++];
+ y ^= y >>> 11;
+ y ^= (y << 7) & 0x9d2c5680;
+ y ^= (y << 15) & 0xefc60000;
+ y ^= y >>> 18;
+ return y >>> 0;
+ }
+
+ getrandbits(k: number): number {
+ if (!Number.isInteger(k) || k < 0 || k > 32) throw new Error(`getrandbits: k must be 0..32, got ${k}`);
+ return k === 0 ? 0 : this.genrandUint32() >>> (32 - k);
+ }
+
+ randbelow(n: number): number {
+ const k = Math.floor(Math.log2(n)) + 1; // n.bit_length()
+ let r = this.getrandbits(k);
+ while (r >= n) r = this.getrandbits(k);
+ return r;
+ }
+
+ choice<T>(seq: ReadonlyArray<T>): T {
+ return seq[this.randbelow(seq.length)];
+ }
+}
+
+// The field of dim lines: [x, y, width] per line, in the canvas generator's
+// order.
+export function mediaBannerField(): Array<[number, number, number]> {
+ const B = MEDIA_BANNER;
+ const rnd = new PyRandom(B.seed);
+ const zx0 = B.safe.x - B.gutter.x;
+ const zy0 = B.safe.y - B.gutter.y;
+ const zx1 = B.safe.x + B.safe.width + B.gutter.x;
+ const zy1 = B.safe.y + B.safe.height + B.gutter.y;
+ const L = B.line;
+ const out: Array<[number, number, number]> = [];
+ for (let y: number = L.first; y < L.last; y += L.pitch) {
+ const inBand = y + L.height > zy0 && y < zy1;
+ let x = L.left + rnd.choice([0, 0, 0, 96, 192]);
+ let end = L.right - rnd.choice([0, 120, 360, 640]);
+ // The found line's row: the field stops short of the gutter, and the lit
+ // line is drawn in the desktop strip instead.
+ if (y === B.found.y) end = Math.min(end, zx0);
+ while (x < end) {
+ let bw = Math.min(rnd.choice([140, 190, 240, 300, 380, 460]), end - x);
+ if (inBand && x < zx1 && x + bw > zx0) {
+ bw = zx0 - x;
+ if (bw >= L.minWidth) out.push([x, y, bw]);
+ x = zx1;
+ continue;
+ }
+ if (bw >= L.minWidth) out.push([x, y, bw]);
+ x += bw + L.gap;
+ }
+ }
+ return out;
+}
+
+// Where the lockup lands on the banner: centred on the safe area. `block` is
+// the lockup's design box (mark top to MEDIA baseline); `ink` adds the
+// ascenders.
+export function mediaBannerLayout(): {
+ geometry: MediaLockupGeometry;
+ x: number;
+ y: number;
+ block: { x: number; y: number; width: number; height: number };
+ ink: { x: number; y: number; width: number; height: number };
+} {
+ const B = MEDIA_BANNER;
+ const g = mediaLockupGeometry(B.wordmarkPx);
+ const cx = B.safe.x + B.safe.width / 2;
+ const cy = B.safe.y + B.safe.height / 2;
+ const x = cx - g.blockWidth / 2;
+ const y = cy - g.blockHeight / 2;
+ return {
+ geometry: g,
+ x,
+ y,
+ block: { x, y, width: g.blockWidth, height: g.blockHeight },
+ ink: { x, y: y - g.inkTop, width: g.blockWidth, height: g.blockHeight + g.inkTop },
+ };
+}
+
+// The banner, 2560 × 1440. `view` crops it (the safe-area PNG is the same
+// drawing with a viewBox of the safe rectangle).
+export function mediaBannerSvg(
+ opts: { palette?: MediaPalette; view?: { x: number; y: number; width: number; height: number } } = {},
+): string {
+ const palette = opts.palette ?? MEDIA_PALETTE;
+ checkPalette("mediaBannerSvg", palette);
+ const B = MEDIA_BANNER;
+ const view = opts.view ?? { x: 0, y: 0, width: B.width, height: B.height };
+ const L = B.line;
+ const lines = mediaBannerField()
+ .map(([x, y, w]) => `<rect x="${x}" y="${y}" width="${w}" height="${L.height}" rx="${L.height / 2}"/>`)
+ .join("");
+ const lit = palette.lit.toLowerCase();
+ const found =
+ `<polygon points="${B.found.head.map(([x, y]) => `${x},${y}`).join(" ")}" fill="${lit}"/>` +
+ `<rect x="${B.found.x}" y="${B.found.y}" width="${B.found.width}" height="${L.height}" rx="${L.height / 2}" fill="${lit}"/>`;
+ const { geometry, x, y } = mediaBannerLayout();
+ return (
+ `<svg xmlns="http://www.w3.org/2000/svg" width="${view.width}" height="${view.height}" ` +
+ `viewBox="${view.x} ${view.y} ${view.width} ${view.height}">` +
+ `<rect width="${B.width}" height="${B.height}" fill="${palette.ground.toLowerCase()}"/>` +
+ `<g fill="${MEDIA_FIELD_LINE}">${lines}</g>` +
+ found +
+ lockupBody(geometry, palette, x, y) +
+ `</svg>`
+ );
+}
+
+// ── The mark on its own ─────────────────────────────────────────────────────
+
+// Mark M2 as a standalone SVG: `maskable` is the avatar (YouTube crops it to
+// a circle, and the 0.8 scale keeps the lines inside it), `any` the Studio
+// watermark (rounded square, transparent corners).
+export function mediaMarkSvg(variant: MarkVariant = "any"): string {
+ return markSvg(MEDIA_MARK_PALETTE, { variant });
+}
diff --git a/common/lib/brandMediaGlyphs.ts b/common/lib/brandMediaGlyphs.ts
@@ -0,0 +1,88 @@
+// GENERATED by common/bin/gen-media-glyphs.py -- DO NOT EDIT. Re-run it instead:
+// python3 common/bin/gen-media-glyphs.py
+// brandMedia.test.ts diffs this file against a fresh run.
+//
+// The Archilyzer Media lockup's letters as outlines: Archivo[wdth,wght].ttf pinned to
+// wdth 118, one instance per run. Font units, y down, baseline at 0; `kerning[i]` is
+// the GPOS pair adjustment between glyph i and glyph i + 1 of the same run.
+
+// `left` / `right` / `top` / `bottom`: the glyph's ink box from its origin, y down
+// (`top` < 0 is above the baseline).
+export type MediaGlyph = {
+ char: string;
+ advance: number;
+ left: number;
+ right: number;
+ top: number;
+ bottom: number;
+ d: string;
+};
+export type MediaRun = {
+ text: string;
+ wght: number;
+ glyphs: ReadonlyArray<MediaGlyph>;
+ kerning: ReadonlyArray<number>;
+};
+
+export const MEDIA_GLYPH_SOURCE = {
+ font: "Archivo[wdth,wght].ttf",
+ sha256: "0e094a7d3c7c4c25cf1310c4b30014f1dae9332220b1c2c88f4fa996f0b05053",
+ wdth: 118,
+} as const;
+
+export const MEDIA_UNITS_PER_EM = 1000;
+export const MEDIA_CAP_HEIGHT = 686;
+
+export const MEDIA_RUNS = {
+ archi: {
+ text: "Archi",
+ wght: 720,
+ glyphs: [
+ { char: "A", advance: 867, left: 16, right: 851, top: -687, bottom: 0,
+ d: "M15.8 0 334.8 -687.1H532.2L851.4 0H664.3L609.1 -124.2H248.1L193.1 0ZM306.3 -256.8H550.8L483.3 -413.3Q479 -423.5 471.5 -441.8Q464.1 -460.2 456.4 -480.5Q448.6 -500.8 442.1 -518Q435.5 -535.3 432.6 -542.8H425.4Q418 -523.5 408.2 -498.6Q398.4 -473.6 389.3 -450.4Q380.2 -427.2 373.8 -412.8Z" },
+ { char: "r", advance: 439, left: 67, right: 428, top: -539, bottom: 0,
+ d: "M66.7 0V-527.1H194.4L205.7 -438.5H213.6Q226.2 -467.5 247.1 -490.4Q268 -513.2 297.3 -526.3Q326.6 -539.4 362.6 -539.4Q381.2 -539.4 397.9 -536.5Q414.7 -533.7 427.6 -528.8V-396.2H358.5Q322.8 -396.2 297.3 -384.9Q271.8 -373.6 255.6 -353.5Q239.4 -333.3 231.6 -307.2Q223.8 -281.2 223.8 -251.1V0Z" },
+ { char: "c", advance: 702, left: 45, right: 657, top: -539, bottom: 12,
+ d: "M359.3 12Q260.8 12 190.3 -18.3Q119.7 -48.6 82.2 -109.9Q44.6 -171.2 44.6 -263.8Q44.6 -356.7 82.3 -417.7Q119.9 -478.8 190.5 -509Q261 -539.2 359.3 -539.2Q422.3 -539.2 476.3 -525.7Q530.4 -512.2 571.1 -484.8Q611.8 -457.3 634.2 -416.5Q656.5 -375.7 656.5 -321.2H498.4Q498.4 -354.6 480.2 -377.6Q462 -400.6 430.6 -412.6Q399.2 -424.6 358.9 -424.6Q309 -424.6 274.9 -406.9Q240.9 -389.3 223.6 -355.8Q206.4 -322.4 206.4 -274.4V-252.9Q206.4 -205.6 224 -171.9Q241.7 -138.1 276.9 -120.3Q312.2 -102.5 364.6 -102.5Q404.4 -102.5 435.9 -115.3Q467.4 -128.1 486 -151.7Q504.6 -175.3 504.6 -206.7H656.5Q656.5 -151.9 633.9 -110.7Q611.3 -69.5 571 -42.3Q530.8 -15.1 476.5 -1.6Q422.3 12 359.3 12Z" },
+ { char: "h", advance: 700, left: 67, right: 637, top: -724, bottom: 0,
+ d: "M66.7 0V-724.1H223.8V-460.6H231Q255.7 -488.9 287 -506Q318.3 -523.2 353.5 -531.1Q388.8 -539.1 424.4 -539.1Q493.7 -539.1 540.9 -516.5Q588.1 -494 612.4 -447.8Q636.7 -401.7 636.7 -330.2V0H479.4V-306Q479.4 -334.8 471.6 -355.5Q463.8 -376.2 449.2 -389.2Q434.6 -402.2 413.6 -408.2Q392.5 -414.3 366 -414.3Q327.3 -414.3 295.1 -397.9Q262.8 -381.6 243.3 -352.7Q223.8 -323.9 223.8 -285.8V0Z" },
+ { char: "i", advance: 290, left: 67, right: 224, top: -724, bottom: 0,
+ d: "M66.7 -605.3V-724.1H223.8V-605.3ZM66.7 0V-527.1H223.8V0Z" },
+ ],
+ kerning: [0, -14, 0, 0],
+ },
+ lyzer: {
+ text: "lyzer",
+ wght: 380,
+ glyphs: [
+ { char: "l", advance: 241, left: 75, right: 166, top: -723, bottom: 0,
+ d: "M74.8 0V-723.4H166.4V0Z" },
+ { char: "y", advance: 576, left: 9, right: 566, top: -526, bottom: 188,
+ d: "M122.5 187.7Q100.1 187.7 81 184.7Q62 181.6 46.7 177.4V113.2H99.5Q142.7 113.2 170.5 102.3Q198.3 91.3 217.2 66.6Q236.1 41.9 253.5 0L9.2 -526.4H109.3L232.3 -251.9Q241.3 -233.3 253.1 -204.8Q264.8 -176.3 277.5 -146.4Q290.1 -116.5 299.5 -93.6H306.1Q309.9 -105 316.8 -124.9Q323.7 -144.8 332.5 -168.7Q341.4 -192.5 350.4 -215.9Q359.4 -239.2 366.4 -257.4L470.9 -526.4H566.4L357.1 -15.1Q336.4 34.3 315.4 72.3Q294.5 110.2 268.7 136Q242.9 161.8 207.4 174.8Q171.9 187.7 122.5 187.7Z" },
+ { char: "z", advance: 574, left: 34, right: 540, top: -526, bottom: 0,
+ d: "M33.7 0V-48.6L384.7 -451.4H59.2V-526.4H525.6V-480L172.2 -74.9H540.3V0Z" },
+ { char: "e", advance: 665, left: 52, right: 613, top: -538, bottom: 12,
+ d: "M338 12Q247.2 12 183.4 -17.7Q119.6 -47.4 86 -108.5Q52.5 -169.7 52.5 -263Q52.5 -355.5 85.5 -416.5Q118.5 -477.6 182.2 -508Q245.9 -538.4 337.4 -538.4Q430.4 -538.4 491.6 -507.4Q552.7 -476.5 582.7 -420.1Q612.7 -363.7 612.7 -286.4V-239H148.9Q150.3 -175.2 174.5 -136.1Q198.8 -97 241.9 -79.7Q285 -62.4 342.2 -62.4Q381.1 -62.4 412.4 -72.1Q443.6 -81.9 466.4 -98Q489.2 -114.1 502.3 -135Q515.4 -155.9 516.9 -178.3H608Q607.5 -142.1 591.1 -108Q574.6 -73.9 541.6 -46.9Q508.6 -20 457.7 -4Q406.9 12 338 12ZM149.2 -306.6H516.5Q516.5 -351.4 501.9 -381.4Q487.3 -411.5 461.9 -429.7Q436.6 -447.9 403.9 -455.9Q371.3 -463.8 334.8 -463.8Q282.4 -463.8 241.8 -447.3Q201.2 -430.7 177.4 -395.8Q153.5 -360.9 149.2 -306.6Z" },
+ { char: "r", advance: 380, left: 75, right: 363, top: -538, bottom: 0,
+ d: "M74.8 0V-526.4H148.3L155.8 -431.1H162.6Q169.9 -454.8 186.9 -479.5Q204 -504.1 233.4 -521.3Q262.8 -538.4 304.8 -538.4Q320.9 -538.4 336.3 -536.2Q351.8 -534.1 362.5 -530.5V-449.3H316Q273.7 -449.3 245 -433.8Q216.3 -418.2 199 -392.2Q181.8 -366.2 174.1 -335.4Q166.4 -304.7 166.4 -274.1V0Z" },
+ ],
+ kerning: [0, 0, -20, 0],
+ },
+ media: {
+ text: "MEDIA",
+ wght: 700,
+ glyphs: [
+ { char: "M", advance: 1055, left: 87, right: 968, top: -687, bottom: 0,
+ d: "M87.4 0V-687H344.8L474 -350.2Q480.8 -332.7 490.3 -305.1Q499.8 -277.4 509.7 -248.2Q519.7 -218.9 527.2 -195.6H534.5Q541.1 -216.2 550.3 -243.8Q559.6 -271.5 569.4 -299.8Q579.2 -328.2 586.8 -349.3L716.1 -687H967.7V0H802V-374.7Q802 -400.8 802.6 -430Q803.3 -459.1 804.3 -484.8Q805.4 -510.4 806 -525H798Q793.4 -509.8 785.4 -484.9Q777.3 -460.1 768.9 -434.4Q760.6 -408.6 753.5 -389.6L601.1 0H447.1L294 -389.4Q285.6 -411.5 277.1 -436.6Q268.6 -461.8 261.8 -485.3Q254.9 -508.9 249.7 -524.9H241.7Q242.7 -508.2 243.4 -482.6Q244 -456.9 244.6 -428.9Q245.2 -400.8 245.2 -374.7V0Z" },
+ { char: "E", advance: 814, left: 88, right: 755, top: -687, bottom: 0,
+ d: "M87.6 0V-687H747.7V-554.3H254V-414.7H690.5V-283.9H254V-132.7H755.2V0Z" },
+ { char: "D", advance: 879, left: 87, right: 822, top: -687, bottom: 0,
+ d: "M87.4 0V-687H434.6Q555.6 -687 642.3 -648.1Q728.9 -609.2 775.4 -532.9Q821.8 -456.6 821.8 -343.2Q821.8 -230.7 775.4 -154.2Q728.9 -77.8 642.3 -38.9Q555.6 0 434.6 0ZM253.6 -132.7H427.5Q477.7 -132.7 518.6 -145.6Q559.5 -158.5 589.1 -183.4Q618.6 -208.3 634.5 -245.4Q650.5 -282.5 650.5 -331.1V-356Q650.5 -404.8 634.5 -441.8Q618.6 -478.9 589.1 -503.8Q559.5 -528.7 518.6 -541.5Q477.7 -554.3 427.5 -554.3H253.6Z" },
+ { char: "I", advance: 341, left: 87, right: 254, top: -687, bottom: 0,
+ d: "M87.4 0V-687H253.6V0Z" },
+ { char: "A", advance: 864, left: 16, right: 847, top: -687, bottom: 0,
+ d: "M16.5 0 336.2 -687H527.3L847.3 0H666.5L609.7 -127.1H244.1L187.4 0ZM301.5 -256.6H552.2L482.5 -416.9Q478.1 -427.3 470.4 -445.9Q462.8 -464.6 454.9 -485.1Q446.9 -505.7 440.2 -522.9Q433.5 -540.2 430.8 -547.1H423.7Q416.1 -527.7 406.1 -502.5Q396.1 -477.3 386.8 -454Q377.6 -430.6 371.2 -416.3Z" },
+ ],
+ kerning: [0, 0, 0, 0],
+ },
+} as const satisfies Record<string, MediaRun>;