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.