Skip to content
AstroCraft Docs
On this theme

Typography

TVfolio is a monospace design: three faces, three jobs, all of them mono. There is a fourth family — Inter — and it backs --font-sans for document content, but nothing the theme draws inside the tube uses it.

The four families

--font-sans:    "InterVariable", "Inter", ui-sans-serif, system-ui, …;
--font-display: "Space Mono", ui-monospace, "SFMono-Regular", "Menlo", …;
--font-mono:    "JetBrains Mono", ui-monospace, "SFMono-Regular", "Menlo", …;
--font-label:   "Courier Prime", "Courier New", ui-monospace, …;
Token Role
--font-display channel numbers, the name in lights, stat figures
--font-mono everything the tube renders as teletext
--font-label the legends silk-screened on the cabinet
--font-sans document content outside the tube

--font-mono is set on .tv-screen — the tube, not the viewport. That placement is deliberate and was a fix: pinning the font to the scrolling viewport left the channel bar, the clock and the fastext keys inheriting the document’s --font-sans, because the chrome is a sibling of the viewport rather than a child.

The three faces do not ship

This is the thing to know before you look at a fresh clone and wonder why it does not match the screenshots. Space Mono, JetBrains Mono and Courier Prime are named but not installed. Each stack ends in a full system-monospace fallback, so a clone renders legibly — just not in the drawn faces.

To match the design:

pnpm add @fontsource/space-mono @fontsource/jetbrains-mono @fontsource/courier-prime

Then add an @font-face block per family to src/styles/fonts.css, following the Inter block already there as the pattern, and <link rel="preload"> the display face in BaseHead.

The alternative is to keep the system monospace stack, which is a legitimate choice — the design’s geometry is in the unit system, not the letterforms. If you go that way, drop @fontsource-variable/inter instead: it backs --font-sans, and nothing the theme draws uses it, since every string in the tube and on the cabinet is one of the three mono faces.

There is no font preload, on purpose

BaseHead carries a comment where a preload would normally sit, and the reasoning is measured rather than assumed:

Measured on a built page: 134 rendered text elements, none of them Inter.

A preload is a high-priority fetch, so preloading 48 KB that paints nothing competes with the real LCP. The @font-face in fonts.css stays, so any content that does use the sans stack still gets Inter — just fetched when it is needed rather than ahead of the picture.

One stale comment to be aware of if you read the source: fonts.css still says the woff2 “is also preloaded in BaseHead”. It is not, and BaseHead explains why. Install one of the three mono faces and preload that instead — it is the face that actually paints.

The tube’s type scale

src/styles/tube-type.css defines 14 .tube-* steps plus two fastext key classes, loaded into @layer components. Every one of them reads --tv-t, the type unit from The TV Set, and none of them carries a floor — because the two Figma frames draw these sizes at the same pixel value on the 374 cabinet as on the 1120 one.

Class Size Draws
.tube-caption 9u CAM 01 and other stamped-on labels
.tube-index 10u a channel-index prefix, a panel head
.tube-spec-label 10.5u the key above a spec value — BASED, LANGUAGES
.tube-meta 11u stat labels, section-head hints, image captions
.tube-label 12u section headings, row indices, timestamps — the workhorse
.tube-spec 12.5u the value under a spec key
.tube-note 13u the briefing column, at prose leading
.tube-card-note 13u a card’s standfirst, at the cards’ tighter leading
.tube-body 14u body copy — the channel strap line, the clock
.tube-title 15u the name of a thing in a list
.tube-quote 20u a pull quote
.tube-card-title 20u a card’s title, at the cards’ own leading
.tube-channel 32u the channel number
.tube-display 34u the display title — floored at 22px, the one exception

Three things about that table are worth understanding rather than just reading.

A step owns font-size and line-height only. Letter-spacing is deliberately not baked in, because the design tracks the same size differently by role — 12u appears at 2u, 1.5u and 1u tracking in four places — so tracking stays on the element as calc(N * var(--tv-t)).

Two pairs share a size and differ only in leading. .tube-note and .tube-card-note are both 13u; .tube-quote and .tube-card-title are both 20u. They are separate steps rather than one step with an override, because a step owns its leading. Each of the card variants started as a local rule in the /work/ card and graduated to a step when the featured bulletin became its second drawer — an Astro <style> reaches only one component.

Half-pixel sizes are drawn, not rounding errors. .tube-spec-label at 10.5u and .tube-spec at 12.5u exist because both About frames draw them at 10.5px and 12.5px. Rounding them into their neighbours would have been the easy call and the wrong one.

What is not in the scale

Four display sizes shrink at their own individually measured ratios — 96→40, 26→22, 20→13 and 18→13 — so they stay beside the components that own them rather than becoming steps. That is the same call the surviving control-unit floors make.

The fastext key faces are the interesting exception: they do shrink at their own ratios (9u→8px and 13u→11px, ratios 0.889 and 0.846), which should put them beside their component — but they are in this file anyway, at the top. The reason is mechanical: two components draw a key face. FastextBar draws five and PagesDropup draws the sixth itself, because a <summary> must be its <details>’s first child. Astro scopes a component’s <style> to that component’s markup, so the alternative was a second copy.

That constraint — an Astro <style> reaches exactly one component — is why tube-type.css exists at all, and it is the rule for deciding whether something belongs there: a size drawn by one component stays with it; a size drawn by two moves to the scale.

There is one stale line in the file worth knowing about: its header says “the seven steps the CRT screen draws its UI in”. There are fourteen now. The prose did not keep up with the additions; the rules themselves are correct.

Document typography

Outside the tube — the legal pages — global.css provides a small set of @apply compositions:

.h1 { @apply text-3xl font-medium md:text-4xl; }
.h2 { @apply text-3xl font-medium; }
.h3 { @apply text-xl font-medium; }
.description { @apply text-base-700 dark:text-base-300 md:text-lg; }
.site-container { @apply mx-auto max-w-[1100px] px-4; }

These are the document’s language, not the tube’s, and the distinction matters when you are deciding where a new component belongs. UI Components covers the rule the theme draws between the two.

NEXT STEPUI Components