Skip to content
AstroCraft Docs
On this theme

Typography

Indexa sets one family: Host Grotesk, variable, self-hosted through Fontsource, latin-only, at weights 300–800. Every text style in the design is that family at 400, 500, 600 or 700 — display and body both — so there is no second font to load and no pairing to maintain.

The font

src/styles/fonts.css declares one @font-face pointing at the variable woff2 inside @fontsource-variable/host-grotesk, with font-display: swap and an explicit unicode-range for latin. --font-sans in the theme wires it to Tailwind’s font-sans, which html uses, so nothing needs a font utility.

The same file is preloaded in BaseHead for first paint:

import hostGrotesk from "@fontsource-variable/host-grotesk/files/host-grotesk-latin-wght-normal.woff2";

<link rel="preload" href={hostGrotesk} as="font" type="font/woff2" crossorigin="anonymous" />

Importing the file rather than hard-coding a path means the hashed build URL and the preload cannot drift apart.

Swapping families is two edits: pnpm add @fontsource-variable/<family>, then change the import and the @font-face in fonts.css and the stack in --font-sans. If the new family is not variable you will need one @font-face per weight; the comment in the file says so.

The four classes

The design’s type scale is carried by four @apply classes in global.css’s components layer, not by utilities repeated across a hundred sections:

Class Scale Where
.h1 Bold, 36px → 60px/64 at md, -0.015em Page heroes
.h2 Bold 36/42, -0.01em Section headings
.h3 SemiBold 22/28 Card titles
.description Regular 18/28 in muted #575757 Section sub-copy

These exist because they are the design’s named text styles, used on nearly every page. A one-off heading is still a one-off — write utilities for it. The bar for adding a fifth class is that the design names it and more than a handful of sections use it.

Article bodies are the deliberate exception. Sections/Blog/ArticleBody.astro styles its rendered markdown with arbitrary-child variants ([&_h2]:text-[28px], [&>p]:leading-[31px]) rather than a global .prose, keeping the article scale inside the one component that renders articles. Analysis & Blog has more on that.

The container

.site-container is the page’s measure, and it comes straight from the design’s frame: 1200px of content inside a 1440px canvas.

.site-container {
  @apply mx-auto max-w-[1200px] px-4 sm:px-6 xl:px-0;
}

The gutter shrinks to nothing at xl because the max-width takes over there. A full-bleed band puts the background on the section and .site-container on the content inside it — which is how every tinted band and the navy footer are built.

The other two shared classes

.form__input is the design’s field: white surface, 1px #d4eef5 border, 8px radius, a cyan border on hover and a cyan ring on focus-visible. Anything that is genuinely a text field should use it rather than re-deriving the look, and the input primitive already does.

.primary-focus is the visible keyboard focus ring — focus:outline-hidden with a 2px cyan focus-visible:outline. It is a utility rather than a base rule so that a component can opt into it explicitly, and it should never be removed to make something look tidier.

Body settings

html sets line-height: 1.6, font-feature-settings: "liga" 1, "calt" 1, text-size-adjust: none and scrollbar-gutter: stable. The last one is worth knowing about: it reserves the scrollbar’s width permanently, so a page that grows past the viewport does not shift its content sideways. It is the cheapest layout-shift fix in the file.

NEXT STEPLayout & Page Shell