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