Typography
Develi ships one typeface, self-hosted, in one variable file. The type scale is three shared classes plus Tailwind’s own utilities. There is no typography plugin and no prose stylesheet for the site chrome — the blog post body is the one place that gets its own rules.
The typeface
Host Grotesk Variable, from Fontsource, latin subset only:
@font-face {
font-family: "HostGroteskVariable";
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: U+0000-00FF, U+0131, U+0152-0153, /* … */;
}
One file, one @font-face, a 300–800 weight axis covering every weight the scale asks for. font-display: swap means text renders in the fallback immediately rather than blocking.
It is preloaded in BaseHead for first paint:
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" />
Note that the href comes from an import rather than a hardcoded path, so it carries the hashed build URL and can never point at a stale file.
Only the latin subset ships, to keep the payload small. The package also carries host-grotesk-latin-ext-wght-normal.woff2 and italic files — add a second @font-face block in src/styles/fonts.css if you need them.
Wiring the family
--font-sans in tailwind-theme.css is the single place the family is named:
--font-sans:
"HostGroteskVariable", "Host Grotesk", ui-sans-serif, system-ui, -apple-system,
BlinkMacSystemFont, "Segoe UI", Roboto, Helvetica, Arial, sans-serif,
"Apple Color Emoji", "Segoe UI Emoji";
--font-mono:
"SFMono-Regular", "Menlo", "Monaco", "Consolas", "Liberation Mono",
"Courier New", "monospace";
html then takes it in global.css, so every element inherits it and no component sets a family:
html {
font-family: var(--font-sans);
line-height: 1.6;
font-feature-settings: "liga" 1, "calt" 1;
}
Ligatures and contextual alternates are on globally. --font-mono is a system stack — no monospace webfont ships.
The scale
Three shared classes in @layer components in global.css, one per heading level:
.h1 { @apply text-4xl font-bold md:text-6xl; } /* 60px bold at md+ */
.h2 { @apply text-3xl font-semibold md:text-5xl; } /* 48px semibold */
.h3 { @apply text-2xl font-semibold md:text-3xl; } /* 30px semibold */
.description { @apply text-muted-foreground md:text-lg; } /* Body Large, 18px */
They are @apply classes rather than components because they are a pattern repeated across many unrelated elements, which is exactly what @apply is for in this codebase. Anything with structure or variants becomes a component or a tv() config instead.
Two deliberate omissions in those rules:
No leading-*. Tailwind’s text-4xl and above already ship tight line-heights (1.11 down to 1.0) that override the 1.6 set on <html>, which is what the design shows. Adding a leading utility here would fight that.
Mobile sizes are the base, desktop is the md: step. The design is drawn at 1440, so the desktop size is the one the mock states and the mobile size is the scaled-down base. That is mobile-first, and it is why every one of these classes reads small-first.
Display (72px) and H4 (24px) are not written, because nothing in the theme uses them yet. Add them next to these three when a hero or a fourth level needs one, rather than reaching for an arbitrary text-[72px] at a call site.
Using them
<h1 class="h1">Rebuilding Meridian's trading interface</h1>
<p class="description">We rebuilt a legacy trading dashboard…</p>
For anything outside the scale, use Tailwind’s utilities directly — text-sm, font-medium, tracking-tight. The three classes exist for the three levels that repeat, not as a mandate.
The blog post body
Long-form prose is the one place that needs element-level rules, because the content is markdown and you cannot put a class on a generated <p>. Those rules live in src/components/Sections/Blog/_post-body.css, a sidecar imported by PostBody.astro.
That is the theme’s general pattern for CSS that will not express as utilities — a leading-underscore sidecar next to the component that uses it, imported from its frontmatter. ui/_overlay.css and ui/accordion/_accordion.css are the other two.
If you write a sidecar of your own, restate the reduced-motion guard inside it. An unlayered sidecar stylesheet outranks the layered global guard in
motion/index.css, so motion declared in a sidecar is not covered by it. Both existing sidecars carry their own@media (prefers-reduced-motion: reduce)block for this reason.
Changing the typeface
pnpm add @fontsource-variable/<family>(or drop a.woff2intosrc/assets/and point at it).- Edit the
@font-faceblock insrc/styles/fonts.css— family name, weight range, file path, unicode range. - Update
--font-sansinsrc/styles/tailwind-theme.cssto name it first. - Update the preload import in
src/layouts/BaseHead.astroto the new file. pnpm remove @fontsource-variable/host-grotesk.
Five edits, none of them in a component. If your replacement is not variable, you will need one @font-face per weight and should check the scale still reads — .h1 asks for 700 and .h2/.h3 for 600.
Two typographic details set globally
html {
-webkit-text-size-adjust: none;
text-size-adjust: none;
scrollbar-gutter: stable;
scroll-padding-top: 5rem;
}
text-size-adjust: none stops mobile Safari inflating text in landscape. scrollbar-gutter: stable reserves the scrollbar’s width so a page that grows past one viewport does not shift horizontally.
scroll-padding-top: 5rem exists because the header is fixed and out of flow, so an in-page anchor would otherwise land underneath it. The number is the header’s top-6 offset plus its h-11 height, rounded up — 68px of chrome, 80px of padding. If you change the header’s height or offset, change this too, or every anchor link on the site lands slightly under the bar.