August 19, 2026 · 11 min read

A typography rebuild: from five faces to two families

This site began as a port of Kyle Conrad's portfolio, and the part I ported most faithfully was his typography. His fluid type scale is the best-argued thing in the original, and I wanted a working system under me before I started having opinions about it.

What I inherited was five typefaces and a mechanism. Lock Serif Light for display headings, Hauss Bold for labels and chrome, Inter for body copy, and then two faces that existed only to be swapped in mid-sentence: PP Pangaia Ultralight Italic wherever a headline contained an <em>, Sharp Roman wherever it contained a <u>.

The mechanism is more interesting than the list. Lock Serif ships exactly one weight, so a headline set in it has no internal emphasis available. You cannot go bold. Kyle's answer was to change the face instead, which is a clever solution to a real constraint, and it is why the count got to five.

Getting from there to here took two months and six prototype rounds. Almost every turn went the opposite way from what I expected, which is why the road is worth writing down.

Eight fonts, four in the headline

The push came from outside. Yan Wang reviewed the site and spent more time on typography than on anything else. She counted eight fonts, four of them in a single headline, against a working guideline of two.

My first instinct was that she had over-counted, folding weights into faces. I checked instead of arguing. She had not. Five faces ship on every content route, and the home hero really does render four of them in one sentence: display serif, the bold client name in Hauss, an italic phrase in PP Pangaia, and body Inter directly underneath.

That is the value of a specific critique. "Too many fonts" is arguable forever. "Four in that sentence" is a number, and a number can be checked in an afternoon.

Round one changed the wrong thing

I had tried to fix typography once already, back on July 1st, and it failed instructively. That round was pure face replacement, behind a ?font= preview: Instrument Serif and Fraunces against Lock Serif, Space Grotesk against Hauss. Every candidate was rejected.

The useful part of that failure took another six weeks to see. I had been swapping faces while leaving the mechanism alone, and the mechanism was the thing making the site look busy. Round one was a redesign of the signature when the problem was in the grammar.

The mechanism was the problem, not the faces

So the second round tested the swaps directly. I set the same string in all five faces at display size and looked at what each one actually buys.

Read lines two and three against line one. That is what each swap was buying.
The same headline string set five times at display size, labelled by face and CSS slot: Lock Serif Light, Sharp Roman, PP Pangaia Ultralight Italic, Hauss Bold and Inter Bold

Line two is the <u> swap. Sharp Roman and Lock Serif are the same genre, both light high-contrast serifs, and at reading distance Sharp Roman reads as the same face rendered slightly wider. It is a face change no reader can name, and it cost 84 KB on the critical path.

Line three is the <em> swap, and it has the opposite problem. PP Pangaia is a real contrast, an ultralight didone italic with a fashion-editorial voice. Across the long home hero it lands as an accent. What it does to a nine-word case-study title is another matter.

Baseline, two faces, three faces. In the top block, watch what the italic phrase does to the shorter titles.
Four case study headlines rendered three times: the five-face baseline with italic swaps, variant A with two faces, and variant B with three faces

In the baseline block, assistive technology is three words of a nine-word title in another face, and on the AethrDesign headline the swapped phrase wraps onto its own line and reads like a caption borrowed from a different site.

Deleting the two swap rules took the hero from four faces to two and a case-study viewport from five to three. Roughly eighty per cent of the critique was answered by removing two CSS rules and 145 KB of fonts, without changing a single typeface.

That comparison also settled something I would have got wrong by eye. The two-face version, where Inter takes the labels and the heading <strong>, looks like the obvious winner on a minimalism argument. It reads worse. Lock Serif is narrow, Hauss is narrow, and Inter is wider and more neutral, so the bold client name visually outweighs the headline it introduces.

Then I measured, and the eye was right for the wrong reason

At that point I stopped judging and started reading the font files. x-height and cap-height both live in the OS/2 table, so a short script gives you the real proportions instead of an impression.

FaceRolex-heightcap-height
Lock Serif Lightdisplay0.5030.700
Hauss Boldlabels0.5000.700
Inter Regularbody0.5460.728

Kyle did not pick two faces he liked. He picked two that sit on the same geometry, matching to three decimal places on the cap. Inter is the only face off that geometry, nine per cent taller on the x-height, which is the measured reason the bold name genuinely outweighed the line above it. My eye was right; my explanation had been "it feels heavy," which is not something you can act on.

I had assumed the fix was Geist, on the grounds that Inter is everywhere. That is backwards: create-next-app scaffolds Geist, so a Next.js portfolio on Vercel running it reads as an untouched scaffold.

The face I did not know I was shipping

While I was writing documentation about a three-face system, the site was serving four.

Tailwind v4 reserves --font-mono as a theme key: it emits --default-mono-font-family: var(--font-mono), and its preflight points code, kbd, samp and pre at that value. I had declared --font-mono on :root for the dark HUD on /lab. That one declaration silently repointed every inline <code> on the site at Martian Mono, plus a 24 KB font download on routes documented as shipping zero bytes of it.

Three separate comments in my own CSS asserted the invariant, and all three were correct about the thing they were checking, which was the var() consumers. Nobody had checked the variable's name.

So the rule I would keep if I could keep only one: count the faces from a network trace, never from the call sites. A typeface can ship without any rule in your stylesheet naming it. Renaming the slot to --font-hud-mono was the entire fix, with no rule added and none deleted.

Buying the constraint away

Every shortlist to this point had been filtered to free faces. When I stopped filtering on licence and started ranking on fit, the answer I had been walking past surfaced: Lock is not a face, it is a superfamily. CoType ships Lock Serif and Lock Sans on a unified skeleton, six weights each, and the two variable files measure identically at x-height 0.508 and cap 0.700.

Six weights meant the original constraint was gone. A display family with real weights does not need a face-swap system to produce emphasis, which retroactively explains why the mechanism existed at all.

Seven of the delivered files never made it to public/, all caught by reading the name table inside each file rather than trusting the filename. Six announce themselves as "TRIAL version only" and carry sixty-seven glyphs, missing the em dash that appears in nearly every lede on this site. A seventh has the family name Lock Serif OVERVIEW and a notice that it is not for retail sale.

Lock Sans was not the answer for body copy either. It is an editorial subhead face, drawn for labels, and at the twelve-pixel tier on a standard-density display it went visibly light and loose. So the body face got its own round: five candidates rendered against real page furniture.

Five candidates on real page furniture at 1× density, each panel carrying an italic so the slope is judged rather than assumed. IBM Plex Sans won on a 0.698 cap against Lock's 0.700.
Five body typefaces rendered on the same page layout with the same copy: Inter as control, IBM Plex Sans, Instrument Sans, Source Sans 3 and Newsreader, each labelled with its measured x-height and cap-height

What the lede was actually failing at

The next note was mine, not a reviewer's. Case-study ledes are long paragraphs set at display size, and in Lock Serif they were handsome and hard to read. My diagnosis was that serifs read worse than sans at length.

That diagnosis is wrong. The axis is display cut versus text cut. Lock Serif is drawn for headlines, CoType's own copy says so, and an eighty-three-word paragraph is outside the range it was drawn for. Measuring confirmed it: the lede ran about ninety characters per line against a comfortable band of forty-five to seventy-five. Moving the standfirst onto Lock Sans brought it to seventy-eight, and the testimonial from ninety-nine to seventy-one.

That fix worked and still left a hole. The right answer to a display cut carrying long text is the text cut of the same family, and Lock ships no text cut. I had reached for a different face because the correct one did not exist.

Two families

The last round is the one I would repeat. Instead of a specimen page, I put the candidate systems behind a ?type= parameter on the real site, so every variant could be read against real headlines, real ledes, real sidebar metadata and real captions. Eight variants, captured across the home page, a case study and an article, at both display densities.

Two reactions came back that sounded like taste and turned out to be measurable.

The first was that Tiempos Text is hard to read at body size. The obvious explanations are that it runs too small or too dense, and both are false: measured at identical size and column width, it renders a larger x-height than IBM Plex and sets fewer characters per line. The difficulty is texture. It is a high-contrast newspaper face, so at fifteen pixels its thin strokes land near one pixel and anti-alias to grey while the stems stay black.

The second was that Tiempos Headline reads heavy even at Light. That one took a number: the same string rendered in each face, with ink measured as a percentage of the line's area.

Display face at LightInk coveragevs. Lock Serif
Lock Serif Light13.65%
Lyon Display Light19.64%+44%
Tiempos Fine Light21.02%+54%
Tiempos Headline Light22.59%+65%

The third row saved money. Tiempos Fine is Klim's large-size optical cut and the obvious remedy for "too heavy," and it lands only seven per cent lighter than Headline. The weight is the design, not the optical size.

What shipped is Lyon Display for headings, Lyon Text for the standfirst, and IBM Plex Sans for labels and body. Two families, three files. The lede went back to a serif, and this time it is the right one: the text cut of the display family, at the size where Lyon Text's small x-height reads as elegance rather than as the liability it is at fifteen pixels.

The same hero and lede, before and after. Drag to compare: the headline changes face, and the standfirst moves from a sans to the text cut of the display family.
The same case study hero set in Lyon Display with the lede in Lyon Text, a serif standfirst paragraphLock Serif + Lock SansLyon Display + Lyon Text

The cost belongs next to the win. Lyon's cap-height is 0.664 against Plex's 0.698, so the geometric argument the previous system was defended on does not survive the swap intact. There is exactly one place on this site where the display face and the label face share a line: the bold client name opening a hero. That seam now carries an explicit font-size: 0.951em correction, which is 0.664 divided by 0.698, shipped in the same change rather than after it. The pairing rule got narrower and more honest at once. Match caps where two faces share a line; everywhere else, judge a face on the job it does.

The current system is visible at /style-guide, which reads its specimens out of the stylesheet at build time rather than restating them, so the page cannot drift from the site.

What the road actually taught

Change the mechanism before you change the faces. Round one swapped typefaces and failed. Round two deleted two rules and answered most of the critique without touching a face.

Prototype on the real site, not a specimen. Every round judged on a synthetic page produced a decision I later revisited. The round that ran behind a URL parameter on live routes surfaced consequences nobody had asked about, including the fact that the entire case-study metadata rail changes face when you change the body slot.

Measure the thing you are reacting to. Three reactions here were right as observations and wrong as diagnoses. Heavy, small, hard to read: each had a number behind it, and in two cases the number pointed at a different fix than the instinct did.

Read the file, not the filename. Trial cuts, overview builds and desktop-only licences all announce themselves in the name table, and every one of them looks like a perfectly ordinary font on disk.

None of this claims the current pairing is final. It claims that every face on the site is now there for a reason I can state in one sentence, and that when the next one changes, the argument it has to beat is written down.

Related Writing