Archilyzer · Source

archilyzer

Archilyzer
git clone https://archilyzer.pages.dev/source/archilyzer.git
Log | Files | Refs | README | LICENSE

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:
Acommon/bin/brand-media.test.ts | 87+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Acommon/bin/brand-media.ts | 234+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Mcommon/lib/brandIcons.ts | 24++++++++++++++++--------
Aumtool/report-to-video/brands/archilyzer-media.json | 23+++++++++++++++++++++++
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, "&amp;").replace(/</g, "&lt;").replace(/>/g, "&gt;").replace(/"/g, "&quot;"); +} + +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>" + } +}