Typography
Olsa sets everything in one typeface: Host Grotesk Variable, weights 300–800, self-hosted through Fontsource. There is no second family, no Google Fonts request and no use of Astro’s fonts API — which means no third-party request on any page load and no layout shift from a font arriving late.
Where the font is defined
Three files touch it, and they do different jobs.
src/styles/fonts.css declares the @font-face. It ships only the variable, latin-subset woff2, which is the leanest thing that covers the theme:
@font-face {
font-family: "Host Grotesk Variable";
font-style: normal;
font-display: swap;
font-weight: 300 800;
src: url(@fontsource-variable/host-grotesk/files/host-grotesk-latin-wght-normal.woff2)
format("woff2-variations");
unicode-range: /* latin */;
}
font-display: swap means text paints in the fallback immediately and swaps when the font lands. The single font-weight: 300 800 range is what a variable font buys you — every weight in that span is available with no additional request.
The Fontsource package already vendors the static weights and the latin-ext subset. If you need either, add another @font-face block here pointing at the extra woff2; nothing needs installing.
src/styles/tailwind-theme.css wires it into Tailwind’s --font-sans with a full system fallback stack, and declares --font-mono for code. Because --font-sans is a theme token, font-sans works as a utility and html { font-family: var(--font-sans) } in global.css makes it the default for the whole document.
src/layouts/BaseHead.astro preloads the woff2:
import hostGroteskVariable from "@fontsource-variable/host-grotesk/files/host-grotesk-latin-wght-normal.woff2";
<link rel="preload" href={hostGroteskVariable} as="font" type="font/woff2" crossorigin="anonymous" />
Importing the file rather than hardcoding a path means the preload href is the hashed build URL, so it matches what the stylesheet actually requests. A preload pointing at a different URL than the one the CSS uses is a classic own-goal — it downloads the font twice and helps with neither.
Document defaults
global.css sets the baseline on <html>:
html {
font-family: var(--font-sans);
line-height: 1.6;
-webkit-text-size-adjust: none;
text-size-adjust: none;
font-feature-settings: "liga" 1, "calt" 1;
}
line-height: 1.6 is the body rhythm everything inherits. Ligatures and contextual alternates are on. text-size-adjust: none stops mobile browsers inflating text on rotation.
The shared type classes
Six @apply classes in global.css carry the recurring type recipes. They exist because each is repeated across genuinely unrelated elements — the house rule is that three or more repeats earn a class.
| Class | Renders |
|---|---|
.h1 |
text-3xl font-medium md:text-4xl |
.h2 |
text-3xl font-medium |
.h3 |
text-xl font-medium |
.description |
text-base-700 dark:text-base-300 md:text-lg |
.panel-title |
text-4xl font-bold text-white sm:text-5xl lg:text-6xl, leading 1.05 |
.panel-lede |
text-base-300 text-lg leading-7 font-medium |
.section-title |
text-base-950 dark:text-base-50 text-4xl font-bold text-balance sm:text-5xl, leading tight |
The last three are the interesting ones. .panel-title and .panel-lede are the two-line recipe every dark panel hero opens with — the home hero, the shared PageHero, the auth aside and the 404. Their text is fixed white because the panel is fixed dark in both themes.
.section-title is the light-band twin: the h2 recipe every violet-band section opens with. It flips with the theme and stops one size step earlier than .panel-title, which is why the two are separate classes rather than one with a modifier.
Only the type is shared. Max-width and alignment stay at the call site, because those are per-page decisions.
There is one thing to know before you try to override these. An @apply class cannot have its responsive steps overridden per call. If .panel-title sets sm:text-5xl and you pass text-3xl, the sm: step still fires at 640px. That is why the classes carry only what is genuinely uniform, and it is the same reasoning behind .panel-shell being opt-in rather than a default — see Layout & Spacing.
The gradient text
.main-text-gradient is the brand mark applied to type — purple to fuchsia, clipped to the text:
.main-text-gradient {
@apply from-primary-700 dark:from-primary-400 bg-gradient-to-r to-fuchsia-600 bg-clip-text text-transparent dark:to-fuchsia-400;
}
Both stops shift by theme. Fuchsia is a deliberate second brand hue rather than a themeable semantic token — repoint both stops together when rebranding.
Article prose
Blog post bodies are styled by an is:global block inside Sections/Blog/PostBody.astro, scoped under .post-prose. There is no @tailwindcss/typography dependency; the rules are written against the theme’s own tokens, so an article follows light and dark mode like everything else.
One deliberate exception lives there: code blocks are a fixed-dark panel — base-950 with a violet border in both themes, the same treatment the CTA panel gets — which means Shiki’s own background is overridden. If you want code blocks to flip with the theme, that override is the single place to change.
The table of contents lists ## headings only. Astro’s markdown pipeline gives every heading an id, so those are plain anchors.
Changing the typeface
Four edits, in this order:
- Install the family:
pnpm add @fontsource-variable/<family>(or@fontsource/<family>for static weights). - Rewrite the
@font-faceinsrc/styles/fonts.css— the family name, the woff2 path, the weight range and the unicode range. - Update
--font-sansinsrc/styles/tailwind-theme.cssso the first entry is your new family name. Leave the fallback stack. - Update the preload import in
src/layouts/BaseHead.astroto point at the new woff2.
Miss step 4 and you preload a font nothing uses while the real one waits for the stylesheet. Miss step 3 and the font loads but nothing asks for it.
If your replacement is not variable, you will need one @font-face per weight and should trim the set to the weights the theme actually uses — a quick grep -r "font-\(light\|medium\|semibold\|bold\)" src/ tells you which.
Adding a second family
If you want a display face alongside the body face, add its @font-face to fonts.css, declare --font-display in the @theme block beside --font-sans, preload it in BaseHead if it appears above the fold, and use font-display in the heading classes. Both preloads are worth it only for fonts that appear on first paint; a face used only in the footer should not be preloaded at all.