diff --git a/src/app/assets/fonts/inter-latin-opsz-italic.woff2 b/src/app/assets/fonts/inter-latin-opsz-italic.woff2
new file mode 100644
index 0000000..39eb636
Binary files /dev/null and b/src/app/assets/fonts/inter-latin-opsz-italic.woff2 differ
diff --git a/src/app/assets/fonts/inter-latin-opsz-normal.woff2 b/src/app/assets/fonts/inter-latin-opsz-normal.woff2
new file mode 100644
index 0000000..b0d0e2e
Binary files /dev/null and b/src/app/assets/fonts/inter-latin-opsz-normal.woff2 differ
diff --git a/src/app/assets/fonts/space-grotesk-latin-wght-normal.woff2 b/src/app/assets/fonts/space-grotesk-latin-wght-normal.woff2
new file mode 100644
index 0000000..0f3474e
Binary files /dev/null and b/src/app/assets/fonts/space-grotesk-latin-wght-normal.woff2 differ
diff --git a/src/app/assets/fonts/space-mono-latin-400-italic.woff2 b/src/app/assets/fonts/space-mono-latin-400-italic.woff2
new file mode 100644
index 0000000..d9b2583
Binary files /dev/null and b/src/app/assets/fonts/space-mono-latin-400-italic.woff2 differ
diff --git a/src/app/assets/fonts/space-mono-latin-400-normal.woff2 b/src/app/assets/fonts/space-mono-latin-400-normal.woff2
new file mode 100644
index 0000000..2752f60
Binary files /dev/null and b/src/app/assets/fonts/space-mono-latin-400-normal.woff2 differ
diff --git a/src/app/assets/fonts/space-mono-latin-700-italic.woff2 b/src/app/assets/fonts/space-mono-latin-700-italic.woff2
new file mode 100644
index 0000000..263487c
Binary files /dev/null and b/src/app/assets/fonts/space-mono-latin-700-italic.woff2 differ
diff --git a/src/app/assets/fonts/space-mono-latin-700-normal.woff2 b/src/app/assets/fonts/space-mono-latin-700-normal.woff2
new file mode 100644
index 0000000..6563bad
Binary files /dev/null and b/src/app/assets/fonts/space-mono-latin-700-normal.woff2 differ
diff --git a/src/app/assets/fonts/syne-latin-800-normal.woff2 b/src/app/assets/fonts/syne-latin-800-normal.woff2
new file mode 100644
index 0000000..bbea29f
Binary files /dev/null and b/src/app/assets/fonts/syne-latin-800-normal.woff2 differ
diff --git a/src/app/styles/app.css b/src/app/styles/app.css
index 909b793..616b15b 100644
--- a/src/app/styles/app.css
+++ b/src/app/styles/app.css
@@ -1,5 +1,6 @@
@import "tailwindcss";
@import "tw-animate-css";
+@import "./fonts.css";
@variant dark (&:where(.dark, .dark *));
@@ -216,9 +217,7 @@
/* Monospace label tracking — used in Loader and Footnote */
--tracking-wider-mono: 0.2em;
- /* ============================================
- SHADOW TOKENS
- ============================================ */
+ /* Shadow tokens */
/* Default resting shadow — equivalent to Tailwind's shadow-sm. Used on
buttons, sliders, popover triggers in non-floating state. */
@@ -245,9 +244,7 @@
/* Drawer / overlay shadow — full-strength shadow-2xl. */
--shadow-overlay: 0 25px 50px -12px rgb(0 0 0 / 0.25);
- /* ============================================
- MOTION TOKENS
- ============================================ */
+ /* Motion tokens */
--duration-fast: 150ms;
--duration-normal: 200ms;
@@ -274,7 +271,7 @@
body {
@apply bg-background text-foreground;
- font-family: "Karla", system-ui, -apple-system, "Segoe UI", Inter, Roboto, Arial, sans-serif;
+ font-family: var(--font-secondary);
font-optical-sizing: auto;
}
@@ -325,9 +322,7 @@
}
}
-/* ============================================
- DESIGN-SYSTEM UTILITIES
- ============================================
+/* Design-system utilities.
Defined via `@utility` (Tailwind v4) so they integrate with the variant
system (`hover:`, `dark:`, breakpoints) and don't rely on `@apply`
chains. Colors reference the mode-switching semantic vars defined in
@@ -362,7 +357,7 @@
}
}
-/* ── Surface utilities ────────────────────────────────────────── */
+/* Surface utilities */
@utility surface-canvas {
background-color: var(--color-surface);
@@ -391,7 +386,7 @@
border: 1px solid var(--color-border-subtle);
}
-/* ── Shape / layout ───────────────────────────────────────────── */
+/* Shape / layout */
@utility flex-center {
display: flex;
@@ -422,7 +417,7 @@
background-size: 10px 10px;
}
-/* ── Typography ───────────────────────────────────────────────── */
+/* Typography */
@utility text-label-mono {
font-family: var(--font-primary);
@@ -431,7 +426,7 @@
text-transform: uppercase;
}
-/* Global utility - useful across your app */
+/* Honor prefers-reduced-motion: collapse animation and transition timing. */
@media (prefers-reduced-motion: reduce) {
* {
animation-duration: 0.01ms !important;
@@ -440,12 +435,12 @@
}
}
-/* Performance optimization for collapsible elements */
+/* Hint the upcoming height animation on open collapsibles. */
[data-state="open"] {
will-change: height;
}
-/* Smooth focus transitions - good globally */
+/* Transition siblings of a focus-visible peer. */
.peer:focus-visible ~ * {
transition: all 150ms cubic-bezier(0.4, 0, 0.2, 1);
}
@@ -472,11 +467,9 @@
animation: nudge 10s ease-in-out infinite;
}
-/* ============================================
- SCROLLBAR STYLES
- ============================================ */
+/* Scrollbar styling */
-/* ---- Modern API: color + width (Chrome 121+, FF 64+) ---- */
+/* Standard API: color + width (Chrome 121+, Firefox 64+). */
@supports (scrollbar-width: auto) {
* {
scrollbar-width: thin;
@@ -488,8 +481,8 @@
}
}
-/* ---- Webkit layer: runs ON TOP in Chrome, standalone in old Safari ---- */
-/* Handles things scrollbar-width can't: hiding buttons, exact sizing */
+/* WebKit fallback: applies on top of the standard API in Chrome, standalone in
+ older Safari. Covers what scrollbar-width can't — hiding buttons, exact sizing. */
@supports selector(::-webkit-scrollbar) {
::-webkit-scrollbar {
width: 6px;
@@ -497,7 +490,7 @@
}
::-webkit-scrollbar-button {
- display: none; /* kills arrows */
+ display: none; /* hide scrollbar buttons */
}
::-webkit-scrollbar-track {
diff --git a/src/app/styles/fonts.css b/src/app/styles/fonts.css
new file mode 100644
index 0000000..2d953fd
--- /dev/null
+++ b/src/app/styles/fonts.css
@@ -0,0 +1,78 @@
+/*
+ Self-hosted interface fonts (latin subset only).
+ Vendored from @fontsource — see docs/interface-font-selfhost-benchmark.md.
+ Variable faces (Inter, Space Grotesk) keep their wght axis; Inter also keeps opsz.
+ url()s are resolved + content-hashed by Vite at build → immutable long-cache.
+*/
+
+/* Inter — variable wght + opsz, the body/secondary UI font (--font-secondary) */
+@font-face {
+ font-family: 'Inter';
+ font-style: normal;
+ font-display: swap;
+ font-weight: 100 900;
+ src: url('../assets/fonts/inter-latin-opsz-normal.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;
+}
+@font-face {
+ font-family: 'Inter';
+ font-style: italic;
+ font-display: swap;
+ font-weight: 100 900;
+ src: url('../assets/fonts/inter-latin-opsz-italic.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;
+}
+
+/* Space Grotesk — variable wght, the primary/display UI font (--font-primary) */
+@font-face {
+ font-family: 'Space Grotesk';
+ font-style: normal;
+ font-display: swap;
+ font-weight: 300 700;
+ src: url('../assets/fonts/space-grotesk-latin-wght-normal.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;
+}
+
+/* Space Mono — static 400/700 × roman/italic (--font-mono) */
+@font-face {
+ font-family: 'Space Mono';
+ font-style: normal;
+ font-display: swap;
+ font-weight: 400;
+ src: url('../assets/fonts/space-mono-latin-400-normal.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;
+}
+@font-face {
+ font-family: 'Space Mono';
+ font-style: italic;
+ font-display: swap;
+ font-weight: 400;
+ src: url('../assets/fonts/space-mono-latin-400-italic.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;
+}
+@font-face {
+ font-family: 'Space Mono';
+ font-style: normal;
+ font-display: swap;
+ font-weight: 700;
+ src: url('../assets/fonts/space-mono-latin-700-normal.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;
+}
+@font-face {
+ font-family: 'Space Mono';
+ font-style: italic;
+ font-display: swap;
+ font-weight: 700;
+ src: url('../assets/fonts/space-mono-latin-700-italic.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;
+}
+
+/* Syne — static 800, the logo font (--font-logo) */
+@font-face {
+ font-family: 'Syne';
+ font-style: normal;
+ font-display: swap;
+ font-weight: 800;
+ src: url('../assets/fonts/syne-latin-800-normal.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;
+}
diff --git a/src/app/types/ambient.d.ts b/src/app/types/ambient.d.ts
index c1a2807..3b236f8 100644
--- a/src/app/types/ambient.d.ts
+++ b/src/app/types/ambient.d.ts
@@ -38,6 +38,11 @@ declare module '*.jpg' {
declare module '*.css';
+declare module '*.woff2?url' {
+ const content: string;
+ export default content;
+}
+
///
interface ImportMetaEnv {
diff --git a/src/app/ui/Layout.svelte b/src/app/ui/Layout.svelte
index 42a85c7..40075be 100644
--- a/src/app/ui/Layout.svelte
+++ b/src/app/ui/Layout.svelte
@@ -9,6 +9,14 @@ import { ResponsiveProvider } from '$shared/lib';
import { cn } from '$shared/lib';
import { Footer } from '$widgets/Footer';
+/*
+ Preload the two render-critical interface faces (primary + secondary).
+ `?url` resolves to the content-hashed path Vite emits, so the binary is
+ fetched immediately rather than waiting for CSS @font-face discovery.
+*/
+import interWoff2 from '../assets/fonts/inter-latin-opsz-normal.woff2?url';
+import spaceGroteskWoff2 from '../assets/fonts/space-grotesk-latin-wght-normal.woff2?url';
+
import {
type Snippet,
onDestroy,
@@ -33,36 +41,21 @@ onDestroy(() => themeManager.destroy());
-
+
-
-
-
- ((e.currentTarget as HTMLLinkElement).media = 'all'))}
- />
-
GlyphDiff | Typography & Typefaces