Typography

Display serif for personality, humanist sans for readability, mono for code. The combination creates warmth without sacrificing legibility.

Font Stacks

TokenStackRole
--font-display'Fraunces', Georgia, serifHeadings h1–h3, display copy
--font-body'Instrument Sans', system-ui, sans-serifBody copy, paragraphs
--font-mono'JetBrains Mono', 'Courier New', monospaceCode, <pre> blocks

Fraunces is an optical-size variable serif — warm and considered rather than generic. Instrument Sans pairs naturally: geometric enough to feel clean, humanist enough to not feel cold. Together they create the “familiar but differentiated” quality the system aims for.

Fraunces Optical Size — Critical Rule

Fraunces is a variable font with an optical size axis (opsz). This axis must always be set explicitly — the font renders differently at different optical sizes and will look wrong without it.

Elementopsz valueWhy
h172Optimises letterform detail for large display sizes
h224Section heading weight — balanced stroke contrast
h320Subsection — slightly thicker strokes at smaller size
h1 {
  font-family: var(--font-display);
  font-weight: 800;
  font-variation-settings: 'opsz' 72;  /* mandatory */
}

h2 {
  font-family: var(--font-display);
  font-weight: 700;
  font-variation-settings: 'opsz' 24;  /* mandatory */
}

h3 {
  font-family: var(--font-display);
  font-weight: 600;
  font-variation-settings: 'opsz' 20;  /* mandatory */
}

Type Scale

A modular scale anchored at 16px base, stepping ~1.25× (major third).

rempxUse
0.75rem12pxCaptions, labels, badges
0.875rem14pxSmall body, UI secondary
1rem16pxDefault body
1.125rem18pxLarge body, intro copy
1.25rem20pxH3 base
1.5rem24pxH2 base
2.25rem36pxH1 base
4.5rem72pxDisplay / hero

Use clamp() for fluid heading sizes — no breakpoint jumps. The DocLayout already applies clamp() to all h1–h3 elements.

Heading Styles

ElementFontWeightSizeopszTracking
h1Fraunces800clamp(2.5rem, 5vw, 4rem)72-0.02em
h2Fraunces700clamp(1.5rem, 3vw, 2rem)24-0.02em
h3Fraunces600clamp(1.1rem, 2vw, 1.35rem)20-0.01em
h4Instrument Sans6001rem0
h5, h6Instrument Sans6000.875rem0.04em

Body & UI Styles

RoleFontWeightSizeLine height
BodyInstrument Sans40016px1.7
Body largeInstrument Sans40018px1.7
Body smallInstrument Sans40014px1.7
LabelInstrument Sans60012px
CaptionInstrument Sans40012px1.6
UI / buttonsInstrument Sans500–60013–14px1.6
CodeJetBrains Mono400–50013px1.6

Mono & Code

--font-mono is JetBrains Mono, loaded at weights 400 and 500. Use it exclusively for code — never decorative text. Coding ligatures (=>, !=, -->) are enabled by default. To disable: font-variant-ligatures: none.

ElementContextCSS
<code> Inline code in prose font-family: var(--font-mono); font-size: 0.875em
<pre><code> Code blocks font-family: var(--font-mono); font-size: 0.8125rem; line-height: 1.6

Inline code uses em so it scales with surrounding text size. Block code uses rem for a fixed 13px baseline regardless of context.

Live demo — inline code

Set the background colour with background-color: var(--color-bg) and pair it with color: var(--color-text) for proper contrast in both themes.

Live demo — code block

.card {
  background: var(--color-surface);
  color: var(--color-text);
  border-radius: var(--radius-md);
  padding: var(--space-lg);
  /* ligatures: => != --> are on by default */
  font-variation-settings: normal;
}

Letter Spacing

ValueUse
-0.02emDisplay, h1, h2 — tightens large headings
0emDefault for body and most UI
0.04emH5/h6, small UI labels
0.12emUppercase caps, overline labels

Responsive Typography

BreakpointH1H2Body
Mobile < 640px28px22px16px
Tablet 640–1079pxclamp(28px, 5vw, 52px)clamp(22px, 4vw, 36px)16px
Desktop ≥ 1080px36px24px16px

Loading Fonts

Load via a <link> tag in <head> — not via @import url() inside CSS. The import approach delays rendering.

<link rel="preconnect" href="https://fonts.googleapis.com" />
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin />
<link rel="stylesheet"
  href="https://fonts.googleapis.com/css2?family=Fraunces:ital,opsz,wght@0,9..144,300..900;1,9..144,300..900&family=Instrument+Sans:wght@400;500;600&family=JetBrains+Mono:wght@400;500&display=swap"
/>

Usage Rules

Typography Utility Classes

Opt-in utility classes that encode the full type scale — including mandatory Fraunces font-variation-settings — so consumers never have to look up opsz values or clamp ranges. Load alongside farn.css or farn-tokens.css:

<!-- npm -->
import 'farn-theme/typography';

<!-- CDN / manual -->
<link rel="stylesheet" href="farn-typography.css" />

Display & headings

Display

.text-display — Fraunces 800 · clamp(3rem, 7vw, 4.5rem) · opsz 72 · tracking -0.02em · lh 1.05

Heading 1

.text-h1 — Fraunces 800 · clamp(2.5rem, 5vw, 4rem) · opsz 72 · tracking -0.02em · lh 1.1

Heading 2

.text-h2 — Fraunces 700 · clamp(1.5rem, 3vw, 2rem) · opsz 24 · tracking -0.02em · lh 1.2

Heading 3

.text-h3 — Fraunces 600 · clamp(1.1rem, 2vw, 1.35rem) · opsz 20 · tracking -0.01em · lh 1.3

Heading 4

.text-h4 — Instrument Sans 600 · 1rem · lh 1.4

Heading 5

.text-h5 — Instrument Sans 600 · 0.875rem · tracking 0.04em · lh 1.4

Body & UI

Body large — intro copy and lead paragraphs

.text-body-lg — Instrument Sans 400 · 1.125rem · lh 1.7

Body — default prose and paragraph text

.text-body — Instrument Sans 400 · 1rem · lh 1.7

Body small — secondary copy and supporting text

.text-body-sm — Instrument Sans 400 · 0.875rem · lh 1.7

Label

.text-label — Instrument Sans 600 · 0.75rem

Caption — timestamps, metadata, footnotes

.text-caption — Instrument Sans 400 · 0.75rem · lh 1.6

Mono

text-mono · const result = compute(input);

.text-mono — JetBrains Mono 400 · 0.8125rem · lh 1.6

CSS reference

/* Display */
.text-display {
  font-family: var(--font-display);
  font-weight: 800;
  font-size: clamp(3rem, 7vw, 4.5rem);
  font-variation-settings: 'opsz' 72;
  letter-spacing: -0.02em;
  line-height: 1.05;
}

/* Heading 1 */
.text-h1 {
  font-family: var(--font-display);
  font-weight: 800;
  font-size: clamp(2.5rem, 5vw, 4rem);
  font-variation-settings: 'opsz' 72;
  letter-spacing: -0.02em;
  line-height: 1.1;
}

/* Heading 2 */
.text-h2 {
  font-family: var(--font-display);
  font-weight: 700;
  font-size: clamp(1.5rem, 3vw, 2rem);
  font-variation-settings: 'opsz' 24;
  letter-spacing: -0.02em;
  line-height: 1.2;
}

/* Heading 3 */
.text-h3 {
  font-family: var(--font-display);
  font-weight: 600;
  font-size: clamp(1.1rem, 2vw, 1.35rem);
  font-variation-settings: 'opsz' 20;
  letter-spacing: -0.01em;
  line-height: 1.3;
}

/* Heading 4 */
.text-h4 {
  font-family: var(--font-body);
  font-weight: 600;
  font-size: 1rem;
  line-height: 1.4;
}

/* Heading 5 */
.text-h5 {
  font-family: var(--font-body);
  font-weight: 600;
  font-size: 0.875rem;
  letter-spacing: 0.04em;
  line-height: 1.4;
}

/* Body */
.text-body    { font-family: var(--font-body); font-weight: 400; font-size: 1rem;      line-height: 1.7; }
.text-body-lg { font-family: var(--font-body); font-weight: 400; font-size: 1.125rem;  line-height: 1.7; }
.text-body-sm { font-family: var(--font-body); font-weight: 400; font-size: 0.875rem;  line-height: 1.7; }
.text-label   { font-family: var(--font-body); font-weight: 600; font-size: 0.75rem; }
.text-caption { font-family: var(--font-body); font-weight: 400; font-size: 0.75rem;  line-height: 1.6; }

/* Mono */
.text-mono { font-family: var(--font-mono); font-weight: 400; font-size: 0.8125rem; line-height: 1.6; }