commit d74ece99182cd36ec5243d0634c5870260a03df4
parent 723d785bb8c91d7a287d73bcbbc4ba3302487ed4
Author: I Mean I'm Just Saying <imeanimjustsaying@kiwifarms.st>
Date: Sat, 26 Sep 2026 02:27:01 -0400
common: bin/brand-media.ts — the YouTube channel's assets (avatar-800 maskable, watermark-150, banner-2560x1440 and its 1546x423 safe crop, each beside its SVG, plus lockup.svg and an INDEX.html handoff with the Studio upload steps), rendered through brandIcons' new renderSvgPng (next/og; renderIconPng now calls it, bytes unchanged); --video-kit writes the palette, mark and lockup umtool's preset reads
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Diffstat:
4 files changed, 360 insertions(+), 8 deletions(-)
diff --git a/common/bin/brand-media.test.ts b/common/bin/brand-media.test.ts
@@ -0,0 +1,87 @@
+import { test } from "node:test";
+import assert from "node:assert/strict";
+import { mkdtemp, readFile, readdir, rm } from "node:fs/promises";
+import { readFileSync } from "node:fs";
+import { tmpdir } from "node:os";
+import path from "node:path";
+import { mediaBannerSvg, mediaLockupGeometry, mediaMarkSvg, MEDIA_BANNER, MEDIA_VIDEO_KIT_PX } from "../lib/brandMedia";
+import { BANNER_MAX_BYTES, MEDIA_ASSETS, VIDEO_KIT_PATH, videoKitJson, writeMediaAssets } from "./brand-media";
+
+const PNG_MAGIC = [0x89, 0x50, 0x4e, 0x47, 0x0d, 0x0a, 0x1a, 0x0a];
+
+function ihdr(png: Buffer): { width: number; height: number; colourType: number } {
+ assert.deepEqual([...png.subarray(0, 8)], PNG_MAGIC, "PNG magic");
+ assert.equal(png.toString("latin1", 12, 16), "IHDR");
+ return { width: png.readUInt32BE(16), height: png.readUInt32BE(20), colourType: png[25] };
+}
+
+test("the asset list: YouTube's four sizes, each from its own SVG", () => {
+ assert.deepEqual(
+ MEDIA_ASSETS.map((a) => [a.png, a.svg, a.width, a.height]),
+ [
+ ["avatar-800.png", "avatar.svg", 800, 800],
+ ["watermark-150.png", "watermark.svg", 150, 150],
+ ["banner-2560x1440.png", "banner.svg", 2560, 1440],
+ ["banner-safe-1546x423.png", "banner-safe.svg", 1546, 423],
+ ],
+ );
+ const by = Object.fromEntries(MEDIA_ASSETS.map((a) => [a.png, a.render()]));
+ assert.equal(by["avatar-800.png"], mediaMarkSvg("maskable"));
+ assert.equal(by["watermark-150.png"], mediaMarkSvg("any"));
+ assert.equal(by["banner-2560x1440.png"], mediaBannerSvg());
+ assert.equal(by["banner-safe-1546x423.png"], mediaBannerSvg({ view: MEDIA_BANNER.safe }));
+});
+
+test("writeMediaAssets: every PNG at its IHDR size, the banner under 6 MB, sources and INDEX.html beside them", async () => {
+ const dir = await mkdtemp(path.join(tmpdir(), "brand-media-"));
+ try {
+ const rows = await writeMediaAssets(dir);
+ const names = (await readdir(dir)).sort();
+ assert.deepEqual(names, [
+ "INDEX.html",
+ "avatar-800.png",
+ "avatar.svg",
+ "banner-2560x1440.png",
+ "banner-safe-1546x423.png",
+ "banner-safe.svg",
+ "banner.svg",
+ "lockup.svg",
+ "watermark-150.png",
+ "watermark.svg",
+ ]);
+ for (const r of rows) {
+ const png = await readFile(path.join(dir, r.png));
+ const h = ihdr(png);
+ assert.deepEqual([h.width, h.height], [r.width, r.height], r.png);
+ assert.equal(png.byteLength, r.bytes);
+ assert.equal(await readFile(path.join(dir, r.svg), "utf8"), r.render());
+ }
+ const banner = await readFile(path.join(dir, "banner-2560x1440.png"));
+ assert.ok(banner.byteLength <= BANNER_MAX_BYTES, `${banner.byteLength} B`);
+ const html = await readFile(path.join(dir, "INDEX.html"), "utf8");
+ for (const r of rows) assert.ok(html.includes(`src="${r.png}"`), r.png);
+ for (const step of ["Customization", "Branding", "Picture", "Banner image", "Video watermark"]) {
+ assert.ok(html.includes(step), step);
+ }
+ } finally {
+ await rm(dir, { recursive: true, force: true });
+ }
+});
+
+// umtool's report-to-video preset cannot import TypeScript, so it reads this
+// JSON. Stale means the preset draws a lockup the channel no longer uses.
+test("umtool/report-to-video/brands/archilyzer-media.json is what --video-kit writes", () => {
+ assert.equal(readFileSync(VIDEO_KIT_PATH, "utf8"), videoKitJson());
+ const kit = JSON.parse(videoKitJson());
+ const g = mediaLockupGeometry(MEDIA_VIDEO_KIT_PX);
+ assert.equal(kit.lockup.px, MEDIA_VIDEO_KIT_PX);
+ assert.ok(Math.abs(kit.lockup.blockHeight - g.blockHeight) < 1e-3);
+ assert.deepEqual(kit.palette, {
+ bg: "#151b20",
+ fg: "#e7edf1",
+ muted: "#8496a2",
+ accent: "#5fa8a0",
+ amber: "#e3b15c",
+ dim: "#3f4c56",
+ });
+});
diff --git a/common/bin/brand-media.ts b/common/bin/brand-media.ts
@@ -0,0 +1,234 @@
+#!/usr/bin/env tsx
+// The Archilyzer Media YouTube channel's assets, rendered from lib/brandMedia.ts.
+//
+// pnpm --filter yt-dlp-transcript-common exec tsx bin/brand-media.ts [--out <dir>]
+// pnpm --filter yt-dlp-transcript-common exec tsx bin/brand-media.ts --video-kit
+//
+// Without --video-kit it writes, into --out (default ~/reports/archilyzer-media/brand/),
+// each PNG beside the SVG it was rasterised from:
+//
+// avatar-800.png the channel picture: mark M2, maskable (YouTube crops a circle)
+// watermark-150.png the Studio video watermark: mark M2, `any` (transparent corners)
+// banner-2560x1440.png the channel banner, the approved canvas design
+// banner-safe-1546x423.png the part of the banner every device shows
+// lockup.svg the lockup on its own (no PNG: it is a source)
+// INDEX.html the operator's handoff: previews, sizes, the Studio upload steps
+//
+// PNGs go through lib/brandIcons.ts renderSvgPng (next/og: satori + resvg), the same
+// path the site icons take, so nothing here needs a system rasteriser or a font.
+//
+// --video-kit instead writes umtool/report-to-video/brands/archilyzer-media.json, the
+// palette, mark and lockup the report-to-video preset draws with (its scripts are plain
+// .mjs and cannot import this TypeScript). brandMedia.test.ts fails when it is stale.
+
+import { mkdir, writeFile } from "node:fs/promises";
+import os from "node:os";
+import path from "node:path";
+import { fileURLToPath } from "node:url";
+import { renderSvgPng } from "../lib/brandIcons";
+import {
+ MEDIA_BANNER,
+ MEDIA_PALETTE,
+ mediaBannerSvg,
+ mediaLockupGeometry,
+ mediaLockupSvg,
+ mediaMarkSvg,
+ mediaVideoKit,
+} from "../lib/brandMedia";
+import { PROJECT_URL } from "../lib/project";
+import { runIfEntryPoint } from "./_cli";
+import { parseFlags } from "./_parseFlags";
+
+const REPO = path.resolve(path.dirname(fileURLToPath(import.meta.url)), "../..");
+export const VIDEO_KIT_PATH = path.join(REPO, "umtool/report-to-video/brands/archilyzer-media.json");
+export const DEFAULT_OUT = path.join(os.homedir(), "reports/archilyzer-media/brand");
+
+// The kit as the file holds it: two-space JSON and a trailing newline.
+export function videoKitJson(): string {
+ return JSON.stringify(mediaVideoKit(), null, 2) + "\n";
+}
+
+export type MediaAsset = {
+ png: string;
+ svg: string;
+ width: number;
+ height: number;
+ title: string;
+ studio: string;
+ note: string;
+ render: () => string;
+};
+
+const S = MEDIA_BANNER.safe;
+
+export const MEDIA_ASSETS: ReadonlyArray<MediaAsset> = [
+ {
+ png: "avatar-800.png",
+ svg: "avatar.svg",
+ width: 800,
+ height: 800,
+ title: "Channel picture",
+ studio: "Picture",
+ note: "Mark M2 · Signal, maskable: the lines sit inside the circle YouTube crops to.",
+ render: () => mediaMarkSvg("maskable"),
+ },
+ {
+ png: "watermark-150.png",
+ svg: "watermark.svg",
+ width: 150,
+ height: 150,
+ title: "Video watermark",
+ studio: "Video watermark",
+ note: "Mark M2 · Signal, rounded square; the corners are transparent.",
+ render: () => mediaMarkSvg("any"),
+ },
+ {
+ png: "banner-2560x1440.png",
+ svg: "banner.svg",
+ width: MEDIA_BANNER.width,
+ height: MEDIA_BANNER.height,
+ title: "Banner image",
+ studio: "Banner image",
+ note: "The whole banner: a TV shows all of it, a desktop the full-width strip through the middle.",
+ render: () => mediaBannerSvg(),
+ },
+ {
+ png: "banner-safe-1546x423.png",
+ svg: "banner-safe.svg",
+ width: S.width,
+ height: S.height,
+ title: "Banner, safe area",
+ studio: "(not uploaded)",
+ note: "What every device shows, phones included. A preview of the crop, not a separate upload.",
+ render: () => mediaBannerSvg({ view: S }),
+ },
+];
+
+// The lockup at the head of INDEX.html (and lockup.svg), wordmark px.
+const INDEX_LOCKUP_PX = 64;
+
+// YouTube's own limits, checked before anything is written.
+export const BANNER_MAX_BYTES = 6 * 1024 * 1024;
+
+function esc(s: string): string {
+ return s.replace(/&/g, "&").replace(/</g, "<").replace(/>/g, ">").replace(/"/g, """);
+}
+
+function kb(n: number): string {
+ return n >= 1024 * 1024 ? `${(n / 1024 / 1024).toFixed(2)} MB` : `${(n / 1024).toFixed(1)} KB`;
+}
+
+export function indexHtml(rows: ReadonlyArray<MediaAsset & { bytes: number }>, generatedAt: string): string {
+ const P = MEDIA_PALETTE;
+ const bannerBytes = rows.find((r) => r.png.startsWith("banner-2560"))?.bytes ?? 0;
+ const lockup = mediaLockupGeometry(INDEX_LOCKUP_PX);
+ const card = (r: MediaAsset & { bytes: number }) => {
+ const round = r.png.startsWith("avatar");
+ return `<figure class="asset${r.width > 1000 ? " wide" : ""}">
+ <div class="stage${round ? " round" : ""}"><img src="${esc(r.png)}" alt="${esc(r.title)}" width="${r.width}" height="${r.height}"></div>
+ <figcaption>
+ <strong>${esc(r.title)}</strong>
+ <span class="meta">${esc(r.png)} · ${r.width} × ${r.height} · ${kb(r.bytes)} · source <a href="${esc(r.svg)}">${esc(r.svg)}</a></span>
+ <span>${esc(r.note)}</span>
+ </figcaption>
+</figure>`;
+ };
+ return `<!doctype html>
+<html lang="en">
+<head>
+<meta charset="utf-8">
+<meta name="viewport" content="width=device-width, initial-scale=1">
+<title>Archilyzer Media assets</title>
+<style>
+:root { --bg: ${P.ground}; --fg: ${P.fg}; --muted: ${P.muted}; --line: #2a343c; --accent: ${P.lit}; --amber: ${P.amber}; }
+* { box-sizing: border-box; }
+body { margin: 0; background: var(--bg); color: var(--fg); font: 16px/1.5 "IBM Plex Sans", system-ui, sans-serif; }
+main { max-width: 1120px; margin: 0 auto; padding: 40px 16px 64px; }
+h1 { font: 700 28px/1.2 Archivo, system-ui, sans-serif; font-stretch: 118%; margin: 24px 0 4px; }
+h2 { font-size: 18px; margin: 40px 0 12px; }
+p, li { color: var(--muted); max-width: 70ch; }
+strong, b { color: var(--fg); }
+a { color: var(--accent); }
+code { font-family: "IBM Plex Mono", ui-monospace, monospace; font-size: 0.9em; color: var(--fg); }
+.lockup { display: block; max-width: 100%; height: auto; }
+.grid { display: grid; grid-template-columns: repeat(auto-fit, minmax(260px, 1fr)); gap: 24px; }
+.asset { margin: 0; display: flex; flex-direction: column; gap: 10px; }
+.asset.wide { grid-column: 1 / -1; }
+.stage { background: #0f0f0f; border: 1px solid var(--line); border-radius: 10px; padding: 16px; display: flex; align-items: center; justify-content: center; }
+.stage img { display: block; max-width: 100%; height: auto; }
+.stage.round img { border-radius: 50%; max-width: 200px; }
+figcaption { display: flex; flex-direction: column; gap: 2px; color: var(--muted); font-size: 14px; }
+.meta { font-family: "IBM Plex Mono", ui-monospace, monospace; font-size: 12px; overflow-wrap: anywhere; }
+ol li { margin: 6px 0; }
+</style>
+</head>
+<body>
+<main>
+<img class="lockup" src="lockup.svg" alt="Archilyzer Media" width="${Math.round(lockup.blockWidth)}" height="${Math.round(lockup.blockHeight + lockup.inkTop)}">
+<h1>Archilyzer Media: channel assets</h1>
+<p>Generated ${esc(generatedAt)} by <code>common/bin/brand-media.ts</code> from <code>common/lib/brandMedia.ts</code>
+(mark M2 · Signal, lockup "flush · caps matched"). Re-run it to regenerate; never edit these files by hand.</p>
+
+<h2>Upload in YouTube Studio</h2>
+<ol>
+<li>Open <a href="https://studio.youtube.com">YouTube Studio</a> signed in as the channel, then <b>Customization</b> → <b>Branding</b>.</li>
+<li><b>Picture</b> → Upload → <code>avatar-800.png</code>. Keep the crop at the whole square; YouTube shows it as a circle.</li>
+<li><b>Banner image</b> → Upload → <code>banner-2560x1440.png</code> (${kb(bannerBytes)}; YouTube takes up to 6 MB). Leave the crop at the default: the device preview should show the lockup centred, as in the safe-area picture below.</li>
+<li><b>Video watermark</b> → Upload → <code>watermark-150.png</code>. Display time: <i>Entire video</i>.</li>
+<li><b>Publish</b> (top right). Changes can take a few minutes to show on the channel page.</li>
+</ol>
+
+<h2>Assets</h2>
+<div class="grid">
+${rows.map(card).join("\n")}
+</div>
+
+<h2>In the videos</h2>
+<p>The report-to-video preset <code>render.brand: "archilyzer-media"</code> in a umtool manifest draws the title card,
+the mark in the clip header, the end card (right half clear for YouTube's end-screen elements) and the thumbnail
+in the same palette. See <code>umtool/report-to-video/README.md</code>, "The Archilyzer Media preset". The site is
+<a href="${esc(PROJECT_URL)}">${esc(PROJECT_URL.replace(/^https?:\/\//, ""))}</a>.</p>
+</main>
+</body>
+</html>
+`;
+}
+
+export async function writeMediaAssets(outDir: string): Promise<Array<MediaAsset & { bytes: number; path: string }>> {
+ await mkdir(outDir, { recursive: true });
+ const rows: Array<MediaAsset & { bytes: number; path: string }> = [];
+ for (const a of MEDIA_ASSETS) {
+ const svg = a.render();
+ const png = await renderSvgPng(svg, { width: a.width, height: a.height });
+ if (a.png.startsWith("banner-2560") && png.byteLength > BANNER_MAX_BYTES) {
+ throw new Error(`${a.png} is ${png.byteLength} bytes; YouTube's banner limit is ${BANNER_MAX_BYTES}`);
+ }
+ await writeFile(path.join(outDir, a.svg), svg, "utf8");
+ await writeFile(path.join(outDir, a.png), png);
+ rows.push({ ...a, bytes: png.byteLength, path: path.join(outDir, a.png) });
+ }
+ await writeFile(path.join(outDir, "lockup.svg"), mediaLockupSvg({ px: INDEX_LOCKUP_PX }), "utf8");
+ await writeFile(path.join(outDir, "INDEX.html"), indexHtml(rows, new Date().toISOString().slice(0, 16).replace("T", " ")), "utf8");
+ return rows;
+}
+
+export async function main(argv: string[] = process.argv.slice(2)): Promise<number> {
+ const flags = parseFlags(argv);
+ if (flags.help) {
+ console.log("usage: brand-media.ts [--out <dir>] | --video-kit");
+ return 0;
+ }
+ if (flags["video-kit"]) {
+ await mkdir(path.dirname(VIDEO_KIT_PATH), { recursive: true });
+ await writeFile(VIDEO_KIT_PATH, videoKitJson(), "utf8");
+ console.log(`wrote ${path.relative(REPO, VIDEO_KIT_PATH)}`);
+ return 0;
+ }
+ const out = path.resolve(flags.out && flags.out !== "true" ? flags.out.replace(/^~(?=\/)/, os.homedir()) : DEFAULT_OUT);
+ const rows = await writeMediaAssets(out);
+ for (const r of rows) console.log(`${r.path} ${r.width}x${r.height} ${r.bytes} B`);
+ console.log(`${path.join(out, "INDEX.html")}`);
+ return 0;
+}
+
+runIfEntryPoint(import.meta.url, main);
diff --git a/common/lib/brandIcons.ts b/common/lib/brandIcons.ts
@@ -26,19 +26,27 @@ export function svgDataUri(svg: string): string {
return `data:image/svg+xml;base64,${Buffer.from(svg).toString("base64")}`;
}
-// The mark as a PNG of `size` × `size`. satori lays out one <img> of the SVG;
-// resvg rasterises it.
+// Any SVG document as a PNG of `width` × `height`. satori lays out one <img>
+// of the SVG; resvg rasterises it.
+export async function renderSvgPng(
+ svg: string,
+ opts: { width: number; height: number },
+): Promise<Uint8Array> {
+ const { width, height } = opts;
+ const res = new ImageResponse(
+ createElement("img", { src: svgDataUri(svg), width, height, alt: "" }),
+ { width, height },
+ );
+ return new Uint8Array(await res.arrayBuffer());
+}
+
+// The mark as a PNG of `size` × `size`.
export async function renderIconPng(
palette: IconPalette,
opts: { variant?: MarkVariant; size: number },
): Promise<Uint8Array> {
const { size } = opts;
- const src = svgDataUri(markSvg(palette, { variant: opts.variant ?? "any" }));
- const res = new ImageResponse(
- createElement("img", { src, width: size, height: size, alt: "" }),
- { width: size, height: size },
- );
- return new Uint8Array(await res.arrayBuffer());
+ return renderSvgPng(markSvg(palette, { variant: opts.variant ?? "any" }), { width: size, height: size });
}
// A PNG-payload ICO: the 6-byte ICONDIR, one 16-byte ICONDIRENTRY per image,
diff --git a/umtool/report-to-video/brands/archilyzer-media.json b/umtool/report-to-video/brands/archilyzer-media.json
@@ -0,0 +1,23 @@
+{
+ "$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": "#151b20",
+ "fg": "#e7edf1",
+ "muted": "#8496a2",
+ "accent": "#5fa8a0",
+ "amber": "#e3b15c",
+ "dim": "#3f4c56"
+ },
+ "mark": {
+ "any": "<svg xmlns=\"http://www.w3.org/2000/svg\" viewBox=\"0 0 512 512\"><rect width=\"512\" height=\"512\" rx=\"112\" fill=\"#151b20\"/><rect x=\"112\" y=\"128\" width=\"288\" height=\"44\" rx=\"22\" fill=\"#3f4c56\"/><polygon points=\"112,210 172,234 112,258\" fill=\"#5fa8a0\"/><rect x=\"188\" y=\"212\" width=\"212\" height=\"44\" rx=\"22\" fill=\"#5fa8a0\"/><rect x=\"112\" y=\"296\" width=\"232\" height=\"44\" rx=\"22\" fill=\"#3f4c56\"/><rect x=\"112\" y=\"380\" width=\"152\" height=\"44\" rx=\"22\" fill=\"#3f4c56\"/></svg>"
+ },
+ "lockup": {
+ "px": 100,
+ "width": 749.001,
+ "height": 173.401,
+ "blockTop": 3.8,
+ "blockHeight": 169.601,
+ "svg": "<svg xmlns=\"http://www.w3.org/2000/svg\" width=\"749.001\" height=\"173.401\" viewBox=\"0 0 749.001 173.401\"><svg x=\"0\" y=\"3.8\" width=\"169.601\" height=\"169.601\" viewBox=\"0 0 512 512\"><rect width=\"512\" height=\"512\" rx=\"112\" fill=\"#151b20\"/><rect x=\"112\" y=\"128\" width=\"288\" height=\"44\" rx=\"22\" fill=\"#3f4c56\"/><polygon points=\"112,210 172,234 112,258\" fill=\"#5fa8a0\"/><rect x=\"188\" y=\"212\" width=\"212\" height=\"44\" rx=\"22\" fill=\"#5fa8a0\"/><rect x=\"112\" y=\"296\" width=\"232\" height=\"44\" rx=\"22\" fill=\"#3f4c56\"/><rect x=\"112\" y=\"380\" width=\"152\" height=\"44\" rx=\"22\" fill=\"#3f4c56\"/></svg><g fill=\"#e7edf1\"><path transform=\"translate(215.601 72.4) scale(0.1)\" 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\"/><path transform=\"translate(301.568 72.4) scale(0.1)\" 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\"/><path transform=\"translate(343.334 72.4) scale(0.1)\" 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\"/><path transform=\"translate(412.801 72.4) scale(0.1)\" 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\"/><path transform=\"translate(482.068 72.4) scale(0.1)\" d=\"M66.7 -605.3V-724.1H223.8V-605.3ZM66.7 0V-527.1H223.8V0Z\"/></g><g fill=\"#8496a2\"><path transform=\"translate(510.334 72.4) scale(0.1)\" d=\"M74.8 0V-723.4H166.4V0Z\"/><path transform=\"translate(533.701 72.4) scale(0.1)\" 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\"/><path transform=\"translate(590.568 72.4) scale(0.1)\" d=\"M33.7 0V-48.6L384.7 -451.4H59.2V-526.4H525.6V-480L172.2 -74.9H540.3V0Z\"/><path transform=\"translate(645.234 72.4) scale(0.1)\" 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\"/><path transform=\"translate(711.001 72.4) scale(0.1)\" 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\"/></g><g fill=\"#5fa8a0\"><path transform=\"translate(215.601 173.401) scale(0.103)\" 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\"/><path transform=\"translate(355.86 173.401) scale(0.103)\" d=\"M87.6 0V-687H747.7V-554.3H254V-414.7H690.5V-283.9H254V-132.7H755.2V0Z\"/><path transform=\"translate(471.175 173.401) scale(0.103)\" 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\"/><path transform=\"translate(593.217 173.401) scale(0.103)\" d=\"M87.4 0V-687H253.6V0Z\"/><path transform=\"translate(659.577 173.401) scale(0.103)\" 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\"/></g></svg>"
+ }
+}