Skip to content
milan.swiss Halbton v0.1.0
Concept project

Foundations

Typography

General Sans for prose and UI, JetBrains Mono for every token name, hex value and ratio. The monospace is not an accent here — it carries most of the load-bearing text on this site.

Two faces, and what each is for

The split is not decorative. Anything that is a value — a token name, a hex, a measured ratio, a keyboard key — is monospaced, because those are things a reader compares character by character and often copies. Anything that is an argument is set in the sans.

That rule is why the mono is self-hosted and the sans is not. If the CDN is unreachable, prose falls back to a system sans and reads fine; a hex value in a proportional fallback is genuinely harder to scan.

General Sans — the argument

JetBrains Mono — --ht-informational-content · #3D4A6B · 7.62:1

The scale

Seven sizes, and the ratio between neighbours is small on purpose. A dramatic scale is for pages that are read once; this is for software someone has open all day, where a heading only needs to be findable, not loud. Body is 15px rather than 16 because the measure is narrower than an article's.

xs · 0.6875rem · 11px Sustained attention
sm · 0.8125rem · 13px Sustained attention
base · 0.9375rem · 15px Sustained attention
md · 1.0625rem · 17px Sustained attention
lg · 1.25rem · 20px Sustained attention
xl · 1.5rem · 24px Sustained attention
2xl · 2rem · 32px Sustained attention
Seven steps. The ratio between neighbours is roughly 1.15, not 1.33.
Token rem px Use
--ht-type-xs 0.6875rem 11px Eyebrows, table meta, badge text. Never a sentence.
--ht-type-sm 0.8125rem 13px Dense UI — table cells, controls, captions.
--ht-type-base 0.9375rem 15px Body copy and everything unspecified.
--ht-type-md 1.0625rem 17px Page leads and callout text.
--ht-type-lg 1.25rem 20px Section headings (h2).
--ht-type-xl 1.5rem 24px Page headings (h1) on interior pages.
--ht-type-2xl 2rem 32px The overview title, and nothing else.

Line height and measure

Generous in prose, tight in data. The same line-height for both is the most common typographic mistake in documentation.

Line height is a function of what the text is for, not of its size.
Token Value Applies to
--ht-leading-tight 1.2 Display sizes, where the default gap already looks like a paragraph break.
--ht-leading-snug 1.35 Headings and multi-line labels.
--ht-leading-normal 1.5 UI text, table cells, anything scanned rather than read.
--ht-leading-prose 1.65 Paragraphs. The only tier meant for continuous reading.
--ht-measure-prose 68ch Body copy. Past roughly 75ch the eye loses the start of the next line.
--ht-measure-narrow 52ch Callouts and captions, where a shorter line reads as an aside.

Line length, in characters. Anything past 75ch costs the reader a return sweep on every line.

Using it

Set size and line height from tokens together. Picking a size and leaving line height to inherit is how documentation ends up with 15px body copy at 1.2 — technically legible, exhausting after twenty minutes.

prose vs data
/* Continuous reading */
.prose p {
  font-size: var(--ht-type-base);
  line-height: var(--ht-leading-prose);
  max-width: var(--ht-measure-prose);
}

/* Scanned, not read — tighter, and no measure limit */
.table td {
  font-size: var(--ht-type-sm);
  line-height: var(--ht-leading-normal);
}

When not to reach for a bigger size

  • To signal importance. Use a semantic role, or put the thing first. Size is a poor carrier of meaning and no carrier at all for a screen reader.
  • To fill space. If a panel looks empty, the answer is usually less padding, not larger type.
  • For a number that must be compared. Oversized figures stop being comparable across rows. Keep them at sm and use tabular numerals.