diff --git a/tools/vendor-fonts.py b/tools/vendor-fonts.py new file mode 100644 index 00000000..1e163cbf --- /dev/null +++ b/tools/vendor-fonts.py @@ -0,0 +1,96 @@ +#!/usr/bin/env python3 +"""Download the web fonts from Google Fonts and vendor them into the repo. + +Run by hand when the font set changes, never at build or run time: + + python3 tools/vendor-fonts.py + +The app must render itself with no outbound network, so nothing here may +happen while it is running. This script fetches once, writes the woff2 files +and a stylesheet that points at them by RELATIVE url, and everything is +committed. `web/static/` is copied verbatim into the SvelteKit build, which +Go then embeds — so the fonts travel inside the binary. + +Relative urls in the stylesheet (`./Inter-latin.woff2`, not `/fonts/...`) +are deliberate: a CSS url() resolves against the stylesheet's own address, so +the whole directory keeps working when the app is served under a base path. + +Subsets are kept as Google slices them, with their `unicode-range` intact. +That is not a size compromise — the browser downloads only the ranges a page +actually uses, so vendoring every subset costs repository bytes rather than +request bytes, and a library full of non-English artist names renders instead +of falling back mid-list. +""" +import re +import sys +import urllib.request +from pathlib import Path + +ROOT = Path(__file__).resolve().parent.parent +OUT = ROOT / "web/static/fonts" + +# Matches the faces the design system actually permits: two weights only, 400 +# and 500 (never 600/700), and Fraunces' optical-size axis across its range. +FAMILIES = ( + "family=Fraunces:opsz,wght@9..144,400;9..144,500" + "&family=Inter:wght@400;500" + "&family=JetBrains+Mono:wght@400;500" +) +CSS_URL = f"https://fonts.googleapis.com/css2?{FAMILIES}&display=swap" + +# A modern browser UA is required, not cosmetic: Google serves ancient TTF to +# unrecognised clients and woff2 only to browsers known to support it. +UA = ("Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 " + "(KHTML, like Gecko) Chrome/126.0.0.0 Safari/537.36") + + +def fetch(url, timeout=30): + req = urllib.request.Request(url, headers={"User-Agent": UA}) + with urllib.request.urlopen(req, timeout=timeout) as r: + return r.read() + + +def main(): + css = fetch(CSS_URL).decode() + + # Google emits a comment naming each subset before its @font-face block. + blocks = re.split(r"(?=/\*\s*[a-z0-9-]+\s*\*/)", css) + OUT.mkdir(parents=True, exist_ok=True) + for old in OUT.glob("*.woff2"): + old.unlink() + + out_css, seen, total = [], {}, 0 + for block in blocks: + subset = re.search(r"/\*\s*([a-z0-9-]+)\s*\*/", block) + family = re.search(r"font-family:\s*'([^']+)'", block) + weight = re.search(r"font-weight:\s*([^;]+);", block) + url = re.search(r"url\((https://[^)]+\.woff2)\)", block) + if not (subset and family and url): + continue + w = weight.group(1).strip().replace(" ", "-") if weight else "400" + name = f"{family.group(1).replace(' ', '')}-{w}-{subset.group(1)}.woff2" + if name not in seen: + data = fetch(url.group(1)) + (OUT / name).write_bytes(data) + seen[name] = len(data) + total += len(data) + out_css.append(block.replace(url.group(1), f"./{name}")) + + header = ( + "/* Generated by tools/vendor-fonts.py — do not edit by hand.\n" + " *\n" + " * Vendored from Google Fonts and served from our own origin: the app\n" + " * has to render with no outbound network, so a font provider link is\n" + " * not an option. Urls are relative so this keeps working under a base\n" + " * path. unicode-range is preserved, so the browser still fetches only\n" + " * the subsets a page needs.\n" + " */\n" + ) + (OUT / "fonts.css").write_text(header + "".join(out_css)) + for n, size in sorted(seen.items()): + print(f" {n:<44} {size/1024:7.1f} KB") + print(f"{len(seen)} files, {total/1024/1024:.2f} MB total -> {OUT.relative_to(ROOT)}") + + +if __name__ == "__main__": + sys.exit(main()) diff --git a/web/src/app.html b/web/src/app.html index 6e7479b6..1a1de560 100644 --- a/web/src/app.html +++ b/web/src/app.html @@ -30,12 +30,12 @@ } })(); - - - + + %sveltekit.head% diff --git a/web/src/lib/no-external-assets.test.ts b/web/src/lib/no-external-assets.test.ts new file mode 100644 index 00000000..9cb71f54 --- /dev/null +++ b/web/src/lib/no-external-assets.test.ts @@ -0,0 +1,80 @@ +import { readFileSync, readdirSync } from 'node:fs'; +import { dirname, join } from 'node:path'; +import { fileURLToPath } from 'node:url'; +import { describe, expect, test } from 'vitest'; + +// A deployed instance has no outbound network, so every asset the app draws +// itself with has to come from our own origin. This regressed once already +// (the font stylesheet was linked straight from Google), and the failure is +// invisible in development — the dev machine HAS internet, so the fonts load +// fine and only a real deployment renders in fallback faces. +// +// The assertion is on the PROPERTY, not on a vendor: any absolute URL in a +// resource-loading attribute fails, whoever is hosting it. Naming +// `fonts.googleapis.com` would pass the day someone reached for a different +// CDN, which is the same bug. + +// fileURLToPath rather than import.meta.dirname: the latter needs Node 20.11+ +// and this repo's toolchain is still on 18, where it is silently undefined. +const WEB = join(dirname(fileURLToPath(import.meta.url)), '../..'); + +/** Strip comments before asserting an ABSENCE — otherwise prose describing + * the forbidden thing satisfies the check that forbids it. */ +const stripComments = (html: string) => html.replace(//g, ''); + +/** href/src on anything that makes the browser fetch a subresource. */ +const RESOURCE_URL = /<(?:link|script|img|source|iframe)\b[^>]*?\b(?:href|src)\s*=\s*["']([^"']+)["']/gi; + +const externalUrlsIn = (html: string) => + [...stripComments(html).matchAll(RESOURCE_URL)] + .map((m) => m[1]) + .filter((u) => /^(?:https?:)?\/\//i.test(u)); + +describe('the app ships every asset it draws itself with', () => { + test('app.html loads no subresource from a third-party origin', () => { + const html = readFileSync(join(WEB, 'src/app.html'), 'utf8'); + expect(externalUrlsIn(html)).toEqual([]); + }); + + test('app.html does not preconnect or dns-prefetch to any host', () => { + // preconnect carries no href-fetch of its own, so the check above misses + // it — but it is the fingerprint of a third-party asset about to be added. + const html = stripComments(readFileSync(join(WEB, 'src/app.html'), 'utf8')); + const hints = [...html.matchAll(/]*\brel\s*=\s*["']([^"']+)["'][^>]*>/gi)] + .filter((m) => /\b(?:preconnect|dns-prefetch)\b/i.test(m[1])); + expect(hints.map((m) => m[0])).toEqual([]); + }); + + test('the vendored font stylesheet exists and points only at local files', () => { + const dir = join(WEB, 'static/fonts'); + const css = readFileSync(join(dir, 'fonts.css'), 'utf8'); + + const urls = [...css.matchAll(/url\(\s*['"]?([^'")]+)['"]?\s*\)/gi)].map((m) => m[1]); + expect(urls.length).toBeGreaterThan(0); + expect(urls.filter((u) => /^(?:https?:)?\/\//i.test(u))).toEqual([]); + + // Every referenced file is actually present — a stylesheet pointing at a + // woff2 nobody committed fails exactly like a CDN link would, offline. + const present = new Set(readdirSync(dir)); + const missing = urls + .map((u) => u.replace(/^\.\//, '')) + .filter((f) => !present.has(f)); + expect(missing).toEqual([]); + }); + + test('every font family the tokens name is actually vendored', () => { + // Pins the families to the TOKENS rather than to a hardcoded list, so + // swapping a typeface in tokens.json fails here until it is vendored. + const tokens = JSON.parse( + readFileSync(join(WEB, 'src/lib/styles/tokens.json'), 'utf8') + ) as { fonts: Record }; + const files = readdirSync(join(WEB, 'static/fonts')); + for (const family of Object.values(tokens.fonts)) { + const prefix = `${family.replace(/\s+/g, '')}-`; + expect( + files.some((f) => f.startsWith(prefix) && f.endsWith('.woff2')), + `no vendored woff2 for "${family}"` + ).toBe(true); + } + }); +}); diff --git a/web/static/fonts/Fraunces-400-latin-ext.woff2 b/web/static/fonts/Fraunces-400-latin-ext.woff2 new file mode 100644 index 00000000..b6e3e3a5 Binary files /dev/null and b/web/static/fonts/Fraunces-400-latin-ext.woff2 differ diff --git a/web/static/fonts/Fraunces-400-latin.woff2 b/web/static/fonts/Fraunces-400-latin.woff2 new file mode 100644 index 00000000..cb295bf0 Binary files /dev/null and b/web/static/fonts/Fraunces-400-latin.woff2 differ diff --git a/web/static/fonts/Fraunces-400-vietnamese.woff2 b/web/static/fonts/Fraunces-400-vietnamese.woff2 new file mode 100644 index 00000000..43126c27 Binary files /dev/null and b/web/static/fonts/Fraunces-400-vietnamese.woff2 differ diff --git a/web/static/fonts/Fraunces-500-latin-ext.woff2 b/web/static/fonts/Fraunces-500-latin-ext.woff2 new file mode 100644 index 00000000..b6e3e3a5 Binary files /dev/null and b/web/static/fonts/Fraunces-500-latin-ext.woff2 differ diff --git a/web/static/fonts/Fraunces-500-latin.woff2 b/web/static/fonts/Fraunces-500-latin.woff2 new file mode 100644 index 00000000..cb295bf0 Binary files /dev/null and b/web/static/fonts/Fraunces-500-latin.woff2 differ diff --git a/web/static/fonts/Fraunces-500-vietnamese.woff2 b/web/static/fonts/Fraunces-500-vietnamese.woff2 new file mode 100644 index 00000000..43126c27 Binary files /dev/null and b/web/static/fonts/Fraunces-500-vietnamese.woff2 differ diff --git a/web/static/fonts/Inter-400-cyrillic-ext.woff2 b/web/static/fonts/Inter-400-cyrillic-ext.woff2 new file mode 100644 index 00000000..de83a9c7 Binary files /dev/null and b/web/static/fonts/Inter-400-cyrillic-ext.woff2 differ diff --git a/web/static/fonts/Inter-400-cyrillic.woff2 b/web/static/fonts/Inter-400-cyrillic.woff2 new file mode 100644 index 00000000..d7509147 Binary files /dev/null and b/web/static/fonts/Inter-400-cyrillic.woff2 differ diff --git a/web/static/fonts/Inter-400-greek-ext.woff2 b/web/static/fonts/Inter-400-greek-ext.woff2 new file mode 100644 index 00000000..6e7141f8 Binary files /dev/null and b/web/static/fonts/Inter-400-greek-ext.woff2 differ diff --git a/web/static/fonts/Inter-400-greek.woff2 b/web/static/fonts/Inter-400-greek.woff2 new file mode 100644 index 00000000..024f0770 Binary files /dev/null and b/web/static/fonts/Inter-400-greek.woff2 differ diff --git a/web/static/fonts/Inter-400-latin-ext.woff2 b/web/static/fonts/Inter-400-latin-ext.woff2 new file mode 100644 index 00000000..479d010d Binary files /dev/null and b/web/static/fonts/Inter-400-latin-ext.woff2 differ diff --git a/web/static/fonts/Inter-400-latin.woff2 b/web/static/fonts/Inter-400-latin.woff2 new file mode 100644 index 00000000..d15208de Binary files /dev/null and b/web/static/fonts/Inter-400-latin.woff2 differ diff --git a/web/static/fonts/Inter-400-vietnamese.woff2 b/web/static/fonts/Inter-400-vietnamese.woff2 new file mode 100644 index 00000000..a40c4699 Binary files /dev/null and b/web/static/fonts/Inter-400-vietnamese.woff2 differ diff --git a/web/static/fonts/Inter-500-cyrillic-ext.woff2 b/web/static/fonts/Inter-500-cyrillic-ext.woff2 new file mode 100644 index 00000000..de83a9c7 Binary files /dev/null and b/web/static/fonts/Inter-500-cyrillic-ext.woff2 differ diff --git a/web/static/fonts/Inter-500-cyrillic.woff2 b/web/static/fonts/Inter-500-cyrillic.woff2 new file mode 100644 index 00000000..d7509147 Binary files /dev/null and b/web/static/fonts/Inter-500-cyrillic.woff2 differ diff --git a/web/static/fonts/Inter-500-greek-ext.woff2 b/web/static/fonts/Inter-500-greek-ext.woff2 new file mode 100644 index 00000000..6e7141f8 Binary files /dev/null and b/web/static/fonts/Inter-500-greek-ext.woff2 differ diff --git a/web/static/fonts/Inter-500-greek.woff2 b/web/static/fonts/Inter-500-greek.woff2 new file mode 100644 index 00000000..024f0770 Binary files /dev/null and b/web/static/fonts/Inter-500-greek.woff2 differ diff --git a/web/static/fonts/Inter-500-latin-ext.woff2 b/web/static/fonts/Inter-500-latin-ext.woff2 new file mode 100644 index 00000000..479d010d Binary files /dev/null and b/web/static/fonts/Inter-500-latin-ext.woff2 differ diff --git a/web/static/fonts/Inter-500-latin.woff2 b/web/static/fonts/Inter-500-latin.woff2 new file mode 100644 index 00000000..d15208de Binary files /dev/null and b/web/static/fonts/Inter-500-latin.woff2 differ diff --git a/web/static/fonts/Inter-500-vietnamese.woff2 b/web/static/fonts/Inter-500-vietnamese.woff2 new file mode 100644 index 00000000..a40c4699 Binary files /dev/null and b/web/static/fonts/Inter-500-vietnamese.woff2 differ diff --git a/web/static/fonts/JetBrainsMono-400-cyrillic-ext.woff2 b/web/static/fonts/JetBrainsMono-400-cyrillic-ext.woff2 new file mode 100644 index 00000000..289de931 Binary files /dev/null and b/web/static/fonts/JetBrainsMono-400-cyrillic-ext.woff2 differ diff --git a/web/static/fonts/JetBrainsMono-400-cyrillic.woff2 b/web/static/fonts/JetBrainsMono-400-cyrillic.woff2 new file mode 100644 index 00000000..de39acbc Binary files /dev/null and b/web/static/fonts/JetBrainsMono-400-cyrillic.woff2 differ diff --git a/web/static/fonts/JetBrainsMono-400-greek.woff2 b/web/static/fonts/JetBrainsMono-400-greek.woff2 new file mode 100644 index 00000000..a53d52d6 Binary files /dev/null and b/web/static/fonts/JetBrainsMono-400-greek.woff2 differ diff --git a/web/static/fonts/JetBrainsMono-400-latin-ext.woff2 b/web/static/fonts/JetBrainsMono-400-latin-ext.woff2 new file mode 100644 index 00000000..82f96681 Binary files /dev/null and b/web/static/fonts/JetBrainsMono-400-latin-ext.woff2 differ diff --git a/web/static/fonts/JetBrainsMono-400-latin.woff2 b/web/static/fonts/JetBrainsMono-400-latin.woff2 new file mode 100644 index 00000000..4d09cda4 Binary files /dev/null and b/web/static/fonts/JetBrainsMono-400-latin.woff2 differ diff --git a/web/static/fonts/JetBrainsMono-400-vietnamese.woff2 b/web/static/fonts/JetBrainsMono-400-vietnamese.woff2 new file mode 100644 index 00000000..e38f5538 Binary files /dev/null and b/web/static/fonts/JetBrainsMono-400-vietnamese.woff2 differ diff --git a/web/static/fonts/JetBrainsMono-500-cyrillic-ext.woff2 b/web/static/fonts/JetBrainsMono-500-cyrillic-ext.woff2 new file mode 100644 index 00000000..289de931 Binary files /dev/null and b/web/static/fonts/JetBrainsMono-500-cyrillic-ext.woff2 differ diff --git a/web/static/fonts/JetBrainsMono-500-cyrillic.woff2 b/web/static/fonts/JetBrainsMono-500-cyrillic.woff2 new file mode 100644 index 00000000..de39acbc Binary files /dev/null and b/web/static/fonts/JetBrainsMono-500-cyrillic.woff2 differ diff --git a/web/static/fonts/JetBrainsMono-500-greek.woff2 b/web/static/fonts/JetBrainsMono-500-greek.woff2 new file mode 100644 index 00000000..a53d52d6 Binary files /dev/null and b/web/static/fonts/JetBrainsMono-500-greek.woff2 differ diff --git a/web/static/fonts/JetBrainsMono-500-latin-ext.woff2 b/web/static/fonts/JetBrainsMono-500-latin-ext.woff2 new file mode 100644 index 00000000..82f96681 Binary files /dev/null and b/web/static/fonts/JetBrainsMono-500-latin-ext.woff2 differ diff --git a/web/static/fonts/JetBrainsMono-500-latin.woff2 b/web/static/fonts/JetBrainsMono-500-latin.woff2 new file mode 100644 index 00000000..4d09cda4 Binary files /dev/null and b/web/static/fonts/JetBrainsMono-500-latin.woff2 differ diff --git a/web/static/fonts/JetBrainsMono-500-vietnamese.woff2 b/web/static/fonts/JetBrainsMono-500-vietnamese.woff2 new file mode 100644 index 00000000..e38f5538 Binary files /dev/null and b/web/static/fonts/JetBrainsMono-500-vietnamese.woff2 differ diff --git a/web/static/fonts/fonts.css b/web/static/fonts/fonts.css new file mode 100644 index 00000000..172a244d --- /dev/null +++ b/web/static/fonts/fonts.css @@ -0,0 +1,296 @@ +/* Generated by tools/vendor-fonts.py — do not edit by hand. + * + * Vendored from Google Fonts and served from our own origin: the app + * has to render with no outbound network, so a font provider link is + * not an option. Urls are relative so this keeps working under a base + * path. unicode-range is preserved, so the browser still fetches only + * the subsets a page needs. + */ +/* vietnamese */ +@font-face { + font-family: 'Fraunces'; + font-style: normal; + font-weight: 400; + font-display: swap; + src: url(./Fraunces-400-vietnamese.woff2) format('woff2'); + unicode-range: U+0102-0103, U+0110-0111, U+0128-0129, U+0168-0169, U+01A0-01A1, U+01AF-01B0, U+0300-0301, U+0303-0304, U+0308-0309, U+0323, U+0329, U+1EA0-1EF9, U+20AB; +} +/* latin-ext */ +@font-face { + font-family: 'Fraunces'; + font-style: normal; + font-weight: 400; + font-display: swap; + src: url(./Fraunces-400-latin-ext.woff2) format('woff2'); + unicode-range: U+0100-02BA, U+02BD-02C5, U+02C7-02CC, U+02CE-02D7, U+02DD-02FF, U+0304, U+0308, U+0329, U+1D00-1DBF, U+1E00-1E9F, U+1EF2-1EFF, U+2020, U+20A0-20AB, U+20AD-20C0, U+2113, U+2C60-2C7F, U+A720-A7FF; +} +/* latin */ +@font-face { + font-family: 'Fraunces'; + font-style: normal; + font-weight: 400; + font-display: swap; + src: url(./Fraunces-400-latin.woff2) format('woff2'); + unicode-range: U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6, U+02DA, U+02DC, U+0304, U+0308, U+0329, U+2000-206F, U+20AC, U+2122, U+2191, U+2193, U+2212, U+2215, U+FEFF, U+FFFD; +} +/* vietnamese */ +@font-face { + font-family: 'Fraunces'; + font-style: normal; + font-weight: 500; + font-display: swap; + src: url(./Fraunces-500-vietnamese.woff2) format('woff2'); + unicode-range: U+0102-0103, U+0110-0111, U+0128-0129, U+0168-0169, U+01A0-01A1, U+01AF-01B0, U+0300-0301, U+0303-0304, U+0308-0309, U+0323, U+0329, U+1EA0-1EF9, U+20AB; +} +/* latin-ext */ +@font-face { + font-family: 'Fraunces'; + font-style: normal; + font-weight: 500; + font-display: swap; + src: url(./Fraunces-500-latin-ext.woff2) format('woff2'); + unicode-range: U+0100-02BA, U+02BD-02C5, U+02C7-02CC, U+02CE-02D7, U+02DD-02FF, U+0304, U+0308, U+0329, U+1D00-1DBF, U+1E00-1E9F, U+1EF2-1EFF, U+2020, U+20A0-20AB, U+20AD-20C0, U+2113, U+2C60-2C7F, U+A720-A7FF; +} +/* latin */ +@font-face { + font-family: 'Fraunces'; + font-style: normal; + font-weight: 500; + font-display: swap; + src: url(./Fraunces-500-latin.woff2) format('woff2'); + unicode-range: U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6, U+02DA, U+02DC, U+0304, U+0308, U+0329, U+2000-206F, U+20AC, U+2122, U+2191, U+2193, U+2212, U+2215, U+FEFF, U+FFFD; +} +/* cyrillic-ext */ +@font-face { + font-family: 'Inter'; + font-style: normal; + font-weight: 400; + font-display: swap; + src: url(./Inter-400-cyrillic-ext.woff2) format('woff2'); + unicode-range: U+0460-052F, U+1C80-1C8A, U+20B4, U+2DE0-2DFF, U+A640-A69F, U+FE2E-FE2F; +} +/* cyrillic */ +@font-face { + font-family: 'Inter'; + font-style: normal; + font-weight: 400; + font-display: swap; + src: url(./Inter-400-cyrillic.woff2) format('woff2'); + unicode-range: U+0301, U+0400-045F, U+0490-0491, U+04B0-04B1, U+2116; +} +/* greek-ext */ +@font-face { + font-family: 'Inter'; + font-style: normal; + font-weight: 400; + font-display: swap; + src: url(./Inter-400-greek-ext.woff2) format('woff2'); + unicode-range: U+1F00-1FFF; +} +/* greek */ +@font-face { + font-family: 'Inter'; + font-style: normal; + font-weight: 400; + font-display: swap; + src: url(./Inter-400-greek.woff2) format('woff2'); + unicode-range: U+0370-0377, U+037A-037F, U+0384-038A, U+038C, U+038E-03A1, U+03A3-03FF; +} +/* vietnamese */ +@font-face { + font-family: 'Inter'; + font-style: normal; + font-weight: 400; + font-display: swap; + src: url(./Inter-400-vietnamese.woff2) format('woff2'); + unicode-range: U+0102-0103, U+0110-0111, U+0128-0129, U+0168-0169, U+01A0-01A1, U+01AF-01B0, U+0300-0301, U+0303-0304, U+0308-0309, U+0323, U+0329, U+1EA0-1EF9, U+20AB; +} +/* latin-ext */ +@font-face { + font-family: 'Inter'; + font-style: normal; + font-weight: 400; + font-display: swap; + src: url(./Inter-400-latin-ext.woff2) format('woff2'); + unicode-range: U+0100-02BA, U+02BD-02C5, U+02C7-02CC, U+02CE-02D7, U+02DD-02FF, U+0304, U+0308, U+0329, U+1D00-1DBF, U+1E00-1E9F, U+1EF2-1EFF, U+2020, U+20A0-20AB, U+20AD-20C0, U+2113, U+2C60-2C7F, U+A720-A7FF; +} +/* latin */ +@font-face { + font-family: 'Inter'; + font-style: normal; + font-weight: 400; + font-display: swap; + src: url(./Inter-400-latin.woff2) format('woff2'); + unicode-range: U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6, U+02DA, U+02DC, U+0304, U+0308, U+0329, U+2000-206F, U+20AC, U+2122, U+2191, U+2193, U+2212, U+2215, U+FEFF, U+FFFD; +} +/* cyrillic-ext */ +@font-face { + font-family: 'Inter'; + font-style: normal; + font-weight: 500; + font-display: swap; + src: url(./Inter-500-cyrillic-ext.woff2) format('woff2'); + unicode-range: U+0460-052F, U+1C80-1C8A, U+20B4, U+2DE0-2DFF, U+A640-A69F, U+FE2E-FE2F; +} +/* cyrillic */ +@font-face { + font-family: 'Inter'; + font-style: normal; + font-weight: 500; + font-display: swap; + src: url(./Inter-500-cyrillic.woff2) format('woff2'); + unicode-range: U+0301, U+0400-045F, U+0490-0491, U+04B0-04B1, U+2116; +} +/* greek-ext */ +@font-face { + font-family: 'Inter'; + font-style: normal; + font-weight: 500; + font-display: swap; + src: url(./Inter-500-greek-ext.woff2) format('woff2'); + unicode-range: U+1F00-1FFF; +} +/* greek */ +@font-face { + font-family: 'Inter'; + font-style: normal; + font-weight: 500; + font-display: swap; + src: url(./Inter-500-greek.woff2) format('woff2'); + unicode-range: U+0370-0377, U+037A-037F, U+0384-038A, U+038C, U+038E-03A1, U+03A3-03FF; +} +/* vietnamese */ +@font-face { + font-family: 'Inter'; + font-style: normal; + font-weight: 500; + font-display: swap; + src: url(./Inter-500-vietnamese.woff2) format('woff2'); + unicode-range: U+0102-0103, U+0110-0111, U+0128-0129, U+0168-0169, U+01A0-01A1, U+01AF-01B0, U+0300-0301, U+0303-0304, U+0308-0309, U+0323, U+0329, U+1EA0-1EF9, U+20AB; +} +/* latin-ext */ +@font-face { + font-family: 'Inter'; + font-style: normal; + font-weight: 500; + font-display: swap; + src: url(./Inter-500-latin-ext.woff2) format('woff2'); + unicode-range: U+0100-02BA, U+02BD-02C5, U+02C7-02CC, U+02CE-02D7, U+02DD-02FF, U+0304, U+0308, U+0329, U+1D00-1DBF, U+1E00-1E9F, U+1EF2-1EFF, U+2020, U+20A0-20AB, U+20AD-20C0, U+2113, U+2C60-2C7F, U+A720-A7FF; +} +/* latin */ +@font-face { + font-family: 'Inter'; + font-style: normal; + font-weight: 500; + font-display: swap; + src: url(./Inter-500-latin.woff2) format('woff2'); + unicode-range: U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6, U+02DA, U+02DC, U+0304, U+0308, U+0329, U+2000-206F, U+20AC, U+2122, U+2191, U+2193, U+2212, U+2215, U+FEFF, U+FFFD; +} +/* cyrillic-ext */ +@font-face { + font-family: 'JetBrains Mono'; + font-style: normal; + font-weight: 400; + font-display: swap; + src: url(./JetBrainsMono-400-cyrillic-ext.woff2) format('woff2'); + unicode-range: U+0460-052F, U+1C80-1C8A, U+20B4, U+2DE0-2DFF, U+A640-A69F, U+FE2E-FE2F; +} +/* cyrillic */ +@font-face { + font-family: 'JetBrains Mono'; + font-style: normal; + font-weight: 400; + font-display: swap; + src: url(./JetBrainsMono-400-cyrillic.woff2) format('woff2'); + unicode-range: U+0301, U+0400-045F, U+0490-0491, U+04B0-04B1, U+2116; +} +/* greek */ +@font-face { + font-family: 'JetBrains Mono'; + font-style: normal; + font-weight: 400; + font-display: swap; + src: url(./JetBrainsMono-400-greek.woff2) format('woff2'); + unicode-range: U+0370-0377, U+037A-037F, U+0384-038A, U+038C, U+038E-03A1, U+03A3-03FF; +} +/* vietnamese */ +@font-face { + font-family: 'JetBrains Mono'; + font-style: normal; + font-weight: 400; + font-display: swap; + src: url(./JetBrainsMono-400-vietnamese.woff2) format('woff2'); + unicode-range: U+0102-0103, U+0110-0111, U+0128-0129, U+0168-0169, U+01A0-01A1, U+01AF-01B0, U+0300-0301, U+0303-0304, U+0308-0309, U+0323, U+0329, U+1EA0-1EF9, U+20AB; +} +/* latin-ext */ +@font-face { + font-family: 'JetBrains Mono'; + font-style: normal; + font-weight: 400; + font-display: swap; + src: url(./JetBrainsMono-400-latin-ext.woff2) format('woff2'); + unicode-range: U+0100-02BA, U+02BD-02C5, U+02C7-02CC, U+02CE-02D7, U+02DD-02FF, U+0304, U+0308, U+0329, U+1D00-1DBF, U+1E00-1E9F, U+1EF2-1EFF, U+2020, U+20A0-20AB, U+20AD-20C0, U+2113, U+2C60-2C7F, U+A720-A7FF; +} +/* latin */ +@font-face { + font-family: 'JetBrains Mono'; + font-style: normal; + font-weight: 400; + font-display: swap; + src: url(./JetBrainsMono-400-latin.woff2) format('woff2'); + unicode-range: U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6, U+02DA, U+02DC, U+0304, U+0308, U+0329, U+2000-206F, U+20AC, U+2122, U+2191, U+2193, U+2212, U+2215, U+FEFF, U+FFFD; +} +/* cyrillic-ext */ +@font-face { + font-family: 'JetBrains Mono'; + font-style: normal; + font-weight: 500; + font-display: swap; + src: url(./JetBrainsMono-500-cyrillic-ext.woff2) format('woff2'); + unicode-range: U+0460-052F, U+1C80-1C8A, U+20B4, U+2DE0-2DFF, U+A640-A69F, U+FE2E-FE2F; +} +/* cyrillic */ +@font-face { + font-family: 'JetBrains Mono'; + font-style: normal; + font-weight: 500; + font-display: swap; + src: url(./JetBrainsMono-500-cyrillic.woff2) format('woff2'); + unicode-range: U+0301, U+0400-045F, U+0490-0491, U+04B0-04B1, U+2116; +} +/* greek */ +@font-face { + font-family: 'JetBrains Mono'; + font-style: normal; + font-weight: 500; + font-display: swap; + src: url(./JetBrainsMono-500-greek.woff2) format('woff2'); + unicode-range: U+0370-0377, U+037A-037F, U+0384-038A, U+038C, U+038E-03A1, U+03A3-03FF; +} +/* vietnamese */ +@font-face { + font-family: 'JetBrains Mono'; + font-style: normal; + font-weight: 500; + font-display: swap; + src: url(./JetBrainsMono-500-vietnamese.woff2) format('woff2'); + unicode-range: U+0102-0103, U+0110-0111, U+0128-0129, U+0168-0169, U+01A0-01A1, U+01AF-01B0, U+0300-0301, U+0303-0304, U+0308-0309, U+0323, U+0329, U+1EA0-1EF9, U+20AB; +} +/* latin-ext */ +@font-face { + font-family: 'JetBrains Mono'; + font-style: normal; + font-weight: 500; + font-display: swap; + src: url(./JetBrainsMono-500-latin-ext.woff2) format('woff2'); + unicode-range: U+0100-02BA, U+02BD-02C5, U+02C7-02CC, U+02CE-02D7, U+02DD-02FF, U+0304, U+0308, U+0329, U+1D00-1DBF, U+1E00-1E9F, U+1EF2-1EFF, U+2020, U+20A0-20AB, U+20AD-20C0, U+2113, U+2C60-2C7F, U+A720-A7FF; +} +/* latin */ +@font-face { + font-family: 'JetBrains Mono'; + font-style: normal; + font-weight: 500; + font-display: swap; + src: url(./JetBrainsMono-500-latin.woff2) format('woff2'); + unicode-range: U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6, U+02DA, U+02DC, U+0304, U+0308, U+0329, U+2000-206F, U+20AC, U+2122, U+2191, U+2193, U+2212, U+2215, U+FEFF, U+FFFD; +}