typeset

A typographic specification for printed documents — essays, letters, reports. Anything destined for paper or a PDF.

Each section below shows what an element must look like, the normative values it must hit, and the same values expressed in two engines. The specification is the source of truth; CSS and Typst are conformant implementations of it, not the definition. Point an engine — or an AI wiring one up — at this page and the output should match.

Raw files, for a machine

  • spec.json — the normative source. 22 sections, 106 elements, 2 page templates, every length absolute.
  • SPEC.md — the same thing as prose, generated from spec.json. Paste this into a model's context.
  • llms.txt — what to read first, and in what order.
Serif EB Garamond Sans Source Sans 3 Mono IBM Plex Mono Base 11pt / 1.45 Page A4, 20mm Spec 2.2.1 · 2026-10-09

Running a snippet

Every HTML pane below is a fragment. To run one on its own: link typeset.css, load the three families from typeset-css.zip, and wrap the fragment in an element carrying class="typeset" — every rule in the stylesheet is scoped under that class, so nothing outside it is touched. Bind every face, not one per family: the stylesheet asks the sans for 300, 600 and 700 and the serif for its italic and its semibold, and a family declared once with no font-weight and no font-style leaves the browser to slant and smear that single file into all of them. The essay is the same thing as a whole document.

<link rel="stylesheet" href="typeset.css">
<style>
  @font-face { font-family: "EB Garamond"; font-weight: 400; font-style: normal; src: url("fonts/EB-Garamond/EBGaramond-Regular.otf"); }
  @font-face { font-family: "EB Garamond"; font-weight: 600; font-style: normal; src: url("fonts/EB-Garamond/EBGaramond-SemiBold.otf"); }
  @font-face { font-family: "EB Garamond"; font-weight: 400; font-style: italic; src: url("fonts/EB-Garamond/EBGaramond-Italic.otf"); }
  @font-face { font-family: "EB Garamond"; font-weight: 600; font-style: italic; src: url("fonts/EB-Garamond/EBGaramond-SemiBoldItalic.otf"); }

  @font-face { font-family: "Source Sans 3"; font-weight: 300; font-style: normal; src: url("fonts/Source-Sans-3/SourceSans3-300.ttf"); }
  @font-face { font-family: "Source Sans 3"; font-weight: 400; font-style: normal; src: url("fonts/Source-Sans-3/SourceSans3-400.ttf"); }
  @font-face { font-family: "Source Sans 3"; font-weight: 600; font-style: normal; src: url("fonts/Source-Sans-3/SourceSans3-600.ttf"); }
  @font-face { font-family: "Source Sans 3"; font-weight: 700; font-style: normal; src: url("fonts/Source-Sans-3/SourceSans3-700.ttf"); }
  @font-face { font-family: "Source Sans 3"; font-weight: 400; font-style: italic; src: url("fonts/Source-Sans-3/SourceSans3-400Italic.ttf"); }
  @font-face { font-family: "Source Sans 3"; font-weight: 600; font-style: italic; src: url("fonts/Source-Sans-3/SourceSans3-600Italic.ttf"); }

  @font-face { font-family: "IBM Plex Mono"; font-weight: 400; font-style: normal; src: url("fonts/IBM-Plex-Mono/IBMPlexMono-400.ttf"); }
  @font-face { font-family: "IBM Plex Mono"; font-weight: 600; font-style: normal; src: url("fonts/IBM-Plex-Mono/IBMPlexMono-600.ttf"); }
  @font-face { font-family: "IBM Plex Mono"; font-weight: 400; font-style: italic; src: url("fonts/IBM-Plex-Mono/IBMPlexMono-400Italic.ttf"); }
</style>

<div class="typeset">
  <!-- the fragment from any HTML pane goes here -->
</div>

Every Typst pane below assumes typeset.typ sits beside the document and has been imported and shown once, above the snippet. A snippet that opens with its own #show: typeset.with(…), or with a template that sets a page of its own such as #show: two-column.with(…) or #show: letter-page, replaces the second line rather than nesting under it — both set the page, and a page cannot be set inside a container.

#import "typeset.typ": *
#show: typeset

01 — Foundations

Tokens & scale

One named value per decision. The scale is 11pt base on roughly a 1.2 ratio — calm enough that four heading levels fit inside it, so an engine needs no sizes of its own invention.

The ink is --ts-ink, not black. Pure black blooms on a laser printer and glares on a backlit PDF.

How it must look

24pt Heading one
18pt Heading two
14pt Heading three
12pt Heading four
11pt Body text — EB Garamond
9.5pt Captions, footnotes, tables
8pt Labels and running heads
ink muted faint rule wash accent

fonts

embeddingFont files MUST be embedded in the output. A document that resolves its fonts at print time is not reproducible.

serif

rolebody text — the reading face
familyEB Garamond
licenseOFL
fallbacksIowan Old Style, Palatino, Georgia, serif
weights400, 600
stylesnormal, italic
featuresoldstyle figures, small caps, standard ligatures, kerning
noteSet no smaller than 11pt. Below that the strokes thin out on office laser output.

sans

roleheadings, tables, captions, labels
familySource Sans 3
licenseOFL
fallbacksHelvetica Neue, Helvetica, Arial, sans-serif
weights300, 600, 700
stylesnormal, italic
noteHumanist, so it sits with an old-style serif instead of fighting it.

mono

rolecode, listings, technical identifiers
familyIBM Plex Mono
licenseOFL
fallbacksSF Mono, Menlo, Consolas, monospace
weights400, 600
stylesnormal, italic
featuresligatures disabled
noteMonospaced faces run optically large beside a serif; every size below is already corrected for it.

scale

ratio1.2
base11pt

steps

xs8pt
sm9.5pt
base11pt
h412pt
h314pt
h218pt
h124pt

color

ink

hex#1a1a1a
rolebody text

ink muted

hex#5a5a5a
rolecaptions, attributions, secondary text

ink faint

hex#8a8a8a
rolelabels, list markers, running heads

rule

hex#c9c4bd
rolehairlines, table body rules

rule strong

hex#6f6a64
roletable header and footer rules, block accents

wash

hex#f4f1ec
rolecode and callout backgrounds

accent

hex#7a1f1f
roledrop caps, note markers, fleurons
noteOxblood. Chosen to read as a dark grey when printed in greyscale.
.typeset {
  /* Families */
  --ts-serif: "EB Garamond", "Iowan Old Style", Palatino, Georgia, serif;
  --ts-sans: "Source Sans 3", "Helvetica Neue", Helvetica, Arial, sans-serif;
  --ts-mono: "IBM Plex Mono", "SF Mono", Menlo, Consolas, monospace;

  /* Scale — 11pt base, ~1.2 ratio */
  --ts-xs: 8pt;
  --ts-sm: 9.5pt;
  --ts-base: 11pt;
  --ts-h4: 12pt;
  --ts-h3: 14pt;
  --ts-h2: 18pt;
  --ts-h1: 24pt;

  /* Rhythm */
  --ts-leading: 1.45;
  --ts-leading-tight: 1.2;
  --ts-space: 11pt;          /* one line of base leading, near enough */
  --ts-measure: 33em;        /* ~66 characters at 11pt serif */

  /* Ink — never pure black: it blooms on laser and glares in a PDF */
  --ts-ink: #1a1a1a;
  --ts-ink-muted: #5a5a5a;
  --ts-ink-faint: #8a8a8a;
  --ts-rule: #c9c4bd;
  --ts-rule-strong: #6f6a64;
  --ts-wash: #f4f1ec;
  --ts-accent: #7a1f1f;      /* oxblood: reads as dark grey in greyscale */
}

Full implementation in typeset.typ — tokens

#let steps = (
  (scale-single-column.h1, [24pt], [Heading one]),
  (scale-single-column.h2, [18pt], [Heading two]),
  (scale-single-column.h3, [14pt], [Heading three]),
  (scale-single-column.h4, [12pt], [Heading four]),
  (scale-single-column.base, [11pt], [Body text — EB Garamond]),
  (scale-single-column.sm, [9.5pt], [Captions, footnotes, tables]),
  (scale-single-column.xs, [8pt], [Labels and running heads]),
)

#for (size, label, sample) in steps [
  #grid(
    columns: (3em, 1fr),
    column-gutter: 0.75em,
    align(horizon + right)[#text(font: sans, size: xs, weight: 700, label)],
    align(horizon)[#text(size: size, sample)],
  )
  #v(0.3em)
]

#v(1em)

#let swatches = (
  (ink, [ink]),
  (ink-muted, [muted]),
  (ink-faint, [faint]),
  (rule-color, [rule]),
  (wash, [wash]),
  (accent, [accent]),
)

#grid(
  columns: (auto,) * swatches.len(),
  column-gutter: 1em,
  ..swatches.map(pair => align(horizon)[
    #rect(width: 1em, height: 1em, fill: pair.at(0), radius: 2pt)
    #h(0.35em)
    #text(font: sans, size: xs, fill: ink-muted, pair.at(1))
  ])
)

02 — Foundations

Measure & rhythm

The single largest legibility lever in a printed document. A line longer than about 75 characters loses the reader on the return sweep; shorter than 45 and the eye jumps too often. 33em at 11pt lands near 66.

The measure constrains the text column, not the page. A table or a figure may exceed it; a paragraph never may.

How it must look

Typography exists to honour content. A page set to a comfortable measure asks nothing of the reader; they simply arrive at the end having forgotten there was a page at all. This paragraph is set to the default 33em.

And this one is deliberately overset to 52em, so you can feel the return sweep failing. By the third line your eye has to hunt for the start of the next, and the hunt is what tires a reader out over twenty pages.

rhythm

line height1.45
line height tight1.2
baseline advance15.95pt
space11pt
space noteOne unit of vertical space is 11pt — one line of base leading, near enough. Every gap below is a multiple of it.
measure33em
measure mm128
measure chars66
measure noteThe single largest legibility lever. Past about 75 characters the return sweep fails and readers re-read lines without noticing; below 45 the eye fixates too often. `measure` is canonical — an em measure follows a type-scale change, where a millimetre one does not. `measure_mm` is derived from it, 33 × the 11pt base, and is kept only as a millimetre convenience for readers who think in paper dimensions; tools/check.mjs asserts the two agree.
measure positionCentred, not flush to one edge. The measure is a maximum column width; where foundation.page's text_width (the paper less its margins) is wider than it, the surplus splits evenly between the two sides of the column, so the column sits in the middle of the text area. A template whose measure IS the column, with no surplus to split, is exempt — templates.two-column names its own exemption.

measure variants

narrow27em
wide40em
.typeset {
  font-family: var(--ts-serif);
  font-size: var(--ts-base);
  font-variant-numeric: oldstyle-nums proportional-nums;
  line-height: var(--ts-leading);
  color: var(--ts-ink);
  text-rendering: optimizeLegibility;
  font-kerning: normal;
  font-feature-settings: "liga" 1, "kern" 1;
}

/* Measure is a property of the text column, not the page. Centred, not flush
   to one edge: foundation.rhythm.measure_position — where the measure is
   narrower than the page's own text width, the surplus splits evenly on
   both sides. `margin-inline: auto` is inert wherever there is no surplus
   (measure: none in two columns, or any block already at 100%), so it needs
   no opt-out for those; `.typeset--sidenotes` is the one case that DOES
   carry a surplus and still wants it flush, and it resets this back below. */
/* Every block rule below sets its vertical margins alone (margin-block), so
   this rule owns the horizontal ones and the measure stays centred. The two
   blocks whose browser default carries a side margin when nested — blockquote
   and figure — zero it in their own rules and are re-centred as direct
   children further down.

   The measure is counted in the body's ems, as Typst counts it. An em in
   --ts-measure would otherwise resolve against each block's own size — 33em of
   an 18pt heading is wider than the page — so it is computed once, on
   .typeset, into a registered length that every child inherits as is. */
@property --ts-measure-computed {
  syntax: "<length-percentage> | none";
  inherits: true;
  initial-value: none;
}
.typeset { --ts-measure-computed: var(--ts-measure); }
.typeset > * {
  box-sizing: border-box;   /* a block's own padding sits inside the measure, as Typst's inset does */
  max-width: var(--ts-measure-computed);
  margin-inline: auto;
}

.typeset--measure-wide { --ts-measure: 40em; }
.typeset--measure-narrow { --ts-measure: 27em; }

Full implementation in typeset.typ — foundation

#measured[
  Typography exists to honour content. A page set to a comfortable measure asks
  nothing of the reader; they simply arrive at the end having forgotten there
  was a page at all. This paragraph is set to the default measure.
]

#measured(width: measure-full)[
  And this one is deliberately overset to the full width between the margins, so
  you can feel the return sweep failing. By the third line your eye has to hunt
  for the start of the next, and the hunt is what tires a reader out over twenty
  pages.
]

03 — Foundations

Page setup

A4 at the standard margin, 20mm on all four sides — symmetric by default. A document being bound can opt into duplex margins instead, so a duplex-printed document keeps its gutter. The running head is the current level-2 heading, resolved by the engine — never typed by the author, and never allowed to drift from the section it names.

Ask for a margin by name: typeset--margin-narrow, typeset--margin-standard or typeset--margin-wide in CSS, and the matching duplex class — typeset--margin-duplex-standard, for instance — for a bound document. Typst takes the same six names as a margin: value.

Engine capability. Running heads and folios require the engine to write into the page margin. Typst and LaTeX do this natively. In CSS it is Paged Media, which no browser implements — Chrome's print dialog ignores it and substitutes its own header and footer, so the CSS implementation needs Paged.js, WeasyPrint or Prince. The two CSS examples do exactly that.

How it must look

Measure & rhythm
7
20mm
A4 · 20mm, symmetric

page

sizeA4
sizes noteEvery paper this spec is set for, width before height. `size` names the one a document gets by default; the others are chosen per document. A paper is added here and nowhere else — every page box, text width and column width in this spec is derived from this table and from `margins`.
text widthNot a stated constant: the paper width minus the left and right margins — twice the symmetric value, or inner plus outer for a duplex pair. The two are equal for any one size, by the rule above.

sizes mm

A4210, 297
A5148, 210
Letter215.9, 279.4

margins

defaultstandard
symmetric mmnarrow: 10
standard: 20
wide: 30
duplex inner mmnarrow: 13
standard: 23
wide: 33
duplex outer mmnarrow: 7
standard: 17
wide: 27
noteSymmetric is what a document gets unless it asks otherwise: the named size applies to all four sides. Duplex is the explicit opt-in, for a sheet that will be bound — top and bottom stay at the symmetric value, and left and right split into an inner (binding) edge and an outer edge. A duplex pair preserves its symmetric pair's total, inner plus outer equal to twice the symmetric value, and shifts the gutter by 3mm: 20+20=40 becomes 23+17. A document keeps the same text width whichever it chooses, so switching between symmetric and duplex never reflows it.

running head

positiontop centre
contentthe current level-2 heading, uppercased
fontsans
size8pt
tracking0.08em
colorink_faint
suppressed onfirst page

folio

positionbottom centre
contentpage number
fontserif
size9pt
numeralsoldstyle
colorink_muted
suppressed onfirst page
/* Symmetric is what a document gets unless it asks for binding: the named
   size applies to all four sides. `standard` is the default size, set here on
   the unqualified page — every named page below is a page-name override of
   just its margins, and everything else on this rule (size, running head,
   folio, first-page suppression) still cascades onto it. */
@page {
  size: A4;                        /* or: Letter */
  margin: 20mm;                    /* foundation.page.margins.symmetric_mm.standard */

  /* Margin boxes need a paged-media engine: Paged.js, WeasyPrint, Prince.
     Chrome's own print dialog ignores them. */
  @top-center {
    content: string(ts-running-head);
    font: 8pt/1 "Source Sans 3", sans-serif;
    letter-spacing: 0.08em;
    text-transform: uppercase;
    color: #8a8a8a;
  }
  @bottom-center {
    content: counter(page);
    font: 9pt/1 "EB Garamond", serif;
    font-variant-numeric: oldstyle-nums;
    color: #5a5a5a;
  }
}

/* No running head or folio on the opening page. */
@page :first {
  @top-center { content: none; }
  @bottom-center { content: none; }
}

/* The running head follows the current section. */
.typeset h2 { string-set: ts-running-head content(text); }

/* The three symmetric sizes, the same value on every side. `standard` names
   the default set above; the class exists so a document can ask for it back
   explicitly, the same as narrow and wide. */
.typeset--margin-narrow   { page: ts-margin-narrow; }
.typeset--margin-standard { page: ts-margin-standard; }
.typeset--margin-wide     { page: ts-margin-wide; }

@page ts-margin-narrow   { margin: 10mm; }
@page ts-margin-standard { margin: 20mm; }
@page ts-margin-wide     { margin: 30mm; }

/* Duplex: the explicit opt-in for a sheet that will be bound. Top and bottom
   stay at the symmetric value; left and right split into an inner (binding)
   edge and an outer edge, mirrored by page side so the gutter stays on the
   spine rather than swapping sides every other sheet. */
.typeset--margin-duplex-narrow   { page: ts-margin-duplex-narrow; }
.typeset--margin-duplex-standard { page: ts-margin-duplex-standard; }
.typeset--margin-duplex-wide     { page: ts-margin-duplex-wide; }

@page ts-margin-duplex-narrow { margin-top: 10mm; margin-bottom: 10mm; }
@page ts-margin-duplex-narrow:right { margin-left: 13mm; margin-right: 7mm; }
@page ts-margin-duplex-narrow:left  { margin-left: 7mm;  margin-right: 13mm; }

@page ts-margin-duplex-standard { margin-top: 20mm; margin-bottom: 20mm; }
@page ts-margin-duplex-standard:right { margin-left: 23mm; margin-right: 17mm; }
@page ts-margin-duplex-standard:left  { margin-left: 17mm; margin-right: 23mm; }

@page ts-margin-duplex-wide { margin-top: 30mm; margin-bottom: 30mm; }
@page ts-margin-duplex-wide:right { margin-left: 33mm; margin-right: 27mm; }
@page ts-margin-duplex-wide:left  { margin-left: 27mm; margin-right: 33mm; }

Full implementation in typeset.typ — page

= On the Measure of a Line

== Composition

A4 at the standard margin, 20mm on all four sides — the default a document
gets unless it asks otherwise. Between them lies 170mm of text width, of
which the measure claims 128mm and leaves the remainder as slack.

That slack is not waste. It is where a marginal note goes, where a thumb rests,
and where the eye finds somewhere to stop. A page whose text runs to both
margins reads as a wall, and a reader meets a wall by skimming it.

The running head and the folio are the two marks the engine writes into that
margin without being asked. Neither is typed by the author, and neither may be
allowed to drift from the thing it names: a running head that still announces a
section the reader left four pages ago is worse than no running head at all,
because it is confidently wrong rather than merely absent.

Both are suppressed on the opening page. An opening page announces itself by
being the opening page, and a folio at the foot of it only tells the reader what
they already know. The same argument applies to the head: there is no previous
section for it to name.

The running head is set in the sans at 8pt, uppercased, tracked out by 0.08em
and printed in the faintest of the three inks. Every one of those decisions
pushes it further from the text, which is the point. It is a navigational aid,
consulted perhaps twice in a reading, and it should cost nothing the rest of the
time. The folio is set differently — in the reading face at 9pt, on old-style
figures, in the muted ink — because a page number is read, not scanned, and a
lining figure at the foot of a page shouts.

Neither carries a rule. A rule under a running head is a habit inherited from
newspapers, where the head separates two unrelated stories; in a document with a
single argument running through it there is nothing to separate.

== A bound document opts into duplex margins

A document that will be bound and read as a spread can ask for duplex margins
instead of symmetric ones. Top and bottom stay at the standard value, 20mm; left
and right split into an inner edge, at the binding, and an outer edge — 23mm
inside, 17mm outside, mirrored by page side so the wider edge is always the
binding edge. The pair preserves the symmetric total, 40mm, and shifts the
gutter by 3mm, so the text width is unchanged: a document does not reflow
between the two.

It is not symmetrical, and that surprises people who expect a page to be. A
bound document is read as a spread of two pages, and the two inner margins meet
in the middle to form a single gutter. Set them equal to the outer margins and
that gutter reads as twice the width of everything else, which pulls the two
pages apart exactly where the reader needs them to be one thing.

23mm inside against 17mm outside gives a gutter of 46mm on the spread and 34mm
at the two outer edges — close enough to equal that the spread reads as a unit,
and the binding still has somewhere to take its fold.

The margin is equal on all four sides in the default, symmetric case — 20mm —
which is the concession this specification makes to the photocopier and the
office printer. The classical proportions put more space at the foot than at
the head, so the text block sits slightly above the optical centre of the
sheet; it is the better setting, and it is the first thing lost when a
document is reproduced on equipment that cannot be trusted to centre
anything.

04 — Page templates

Two column, equal

For papers, journal articles, newsletters and technical notes — documents scanned and referenced as much as read. A template sets the page and the scale; it does not get its own typography, so everything else on this page still applies unchanged.

The base size drops to 9.5pt here, and that is arithmetic rather than taste. A4 less the standard 20mm margin on each side leaves 170mm; a 6mm gutter divides it into two 82mm columns. At 11pt an 82mm column carries 42 characters — below the 45-character floor this spec sets for prose. 10.5pt still falls short, at 44. 9.5pt restores 49, and every other step comes down with it.

Two consequences worth stating: justification stops being optional, because at 49 characters a ragged edge serrates the column visibly; and sidenotes are forbidden, because there is no margin left to put them in.

Engine capability. A column container cannot be fragmented by Paged.js — it moves content between page boxes and cannot split a column flow, so it produces zero pages. The CSS two-column example therefore prints directly from the browser and simulates one sheet on screen. Typst paginates columns natively.

How it must look

The Measure of a Column

What changes when a page is divided in two, and what must not

Abstract Dividing an A4 page into two equal columns leaves 82mm of measure. At the single-column base of 11pt that carries 42 characters — below the floor at which a line of prose stays readable.

The arithmetic

A4 is 210mm wide. Take off the standard margin, 20mm on each side, and 170mm of text width remains. A 6mm gutter divides it into two columns of 82mm.

At 11pt, an 82mm column carries about 42 characters. This specification sets 45 as the floor for a line of prose, so neither 11pt nor 10.5pt (44) is available here. Dropping the base to 9.5pt restores 49 characters.

What follows

Leading tightens from 1.45 to 1.4. A shorter line needs less vertical separation for the return sweep to stay unambiguous, and the saving is real: at 13.3pt of advance a column holds 54 lines instead of 45.

Headings come down hardest. A 24pt level-2 heading inside an 82mm column spends three lines saying two words, so the scale runs 20/13/11/9.5.

What does not change

The palette, the rule weights, the numeral conventions and the pagination rules are all identical.

Justification

At 49 characters a ragged right edge serrates the column visibly, and the word gaps in an unhyphenated justified column read as rivers. Both faults are tolerable at 66 characters and neither is tolerable here.

The one place the spec removes a choice.

So the template turns justification on itself rather than offering it as an option.

Table 1 — Characters per line for an 82mm column.
Base11pt10pt9.5pt9pt
Characters42474952
Conformantnoyesyesyes

What the template forbids

Sidenotes have nowhere to go: there is no margin left. They become footnotes or endnotes, and an implementation should degrade them visibly rather than positioning them off the page and losing them.

A three-line drop cap at 3.05em is 29pt — 10mm of the column given to one letter. The cap is neutralised and the opening words carry the signal in small caps instead.

Spanning

An element spans both columns fully or neither. Spanning costs a break in both columns, so it is opt-in per instance and belongs at the top or the bottom of a page.

Set in EB Garamond, 9.5 on 13.3 points, two columns of 82mm with a 6mm gutter.

A4 at 42%. Open the full document →

Opt-in · typeset--two-column

Use for — Papers, journal articles, newsletters, technical notes — documents that are scanned and referenced as much as read, and where a figure is worth more than an uninterrupted argument.

derivation

derived forThe paper named in foundation.page.size, at the margin named in foundation.page.margins.default. No copy of either is kept here: every number below is a function of those two and of the gutter.
column gap mm6
column width mm82
floor45
margins supportednarrow, standard
conclusionAn 82mm column carries 42 characters at the 11pt base — below the 45-character floor this spec sets for a line of prose. 10.5pt still falls short, at 44. The base therefore DROPS to 10pt, the first step that clears the floor — but only just, at 47, two characters of headroom. The template drops one step further anyway, to 9.5pt and 49: a floor is not a target, and a bare pass is not the same as room to breathe. This is arithmetic, not preference: an implementation that keeps 11pt or 10.5pt in two columns violates the measure rule, which is the rule everything else in this spec is downstream of.
refusalTwo columns are refused, not set badly, on any paper and margin whose column falls below the floor. That is a computed rule rather than a list of papers: margins_supported names the margins that reach the floor on the paper in foundation.page.size, and tools/check.mjs recomputes it. The checker fails when this declaration disagrees with the arithmetic, when the default margin cannot reach the floor, or when no named margin on the paper can.
recompute whenThe paper in foundation.page.size, the default margin, or the gutter. Characters per line = (foundation.rhythm.measure_chars / measure_mm) x column_mm x (11 / base_pt), rounded; column_mm = (paper width - twice the margin - the gutter) / 2. tools/check.mjs recomputes every number here from foundation.page, so a margin cannot move without these following it or the checker going red.

characters per line

11pt42
10.5pt44
10pt47
9.5pt49
9pt52

requirements

  • Justification with hyphenation is MANDATORY, not optional. At 49 characters a ragged right edge produces a visibly serrated column and word gaps wide enough to read as rivers. This is the one place the spec removes a choice it otherwise offers.
  • The last line of a paragraph is still flush left. Two columns make a stretched last line more visible, not less.
  • Balance the columns on the final page. A last page with one full column and one empty third reads as a printing error.
  • A spanning element must span BOTH columns fully or neither. An element that spans one and a half columns has no correct reading order.
  • Keep a spanning element at the top or the bottom of the page, never mid-column. Interrupting both columns in the middle forces the reader to find their place twice.

page

sizeas foundation.page.size
marginsas foundation.page.margins, at the default size — symmetric unless the sheet is bound, duplex if it is. A duplex pair leaves the text width unchanged, so a two-column document does not reflow between them.
columns2
column widthsequal — the two columns are the same width, always
column gap mm6
column width mm82
column rulenone by default; an optional 0.5pt hairline in `rule` where the columns need separating
column balancethe final page balances its columns to equal height
lines per column54
running headas foundation.page — spans the full text width, not a column
folioas foundation.page

scale

ratio1.2
base9.5pt
noteEvery step comes down. A 24pt heading inside an 82mm column takes three lines to say two words; 18pt takes two. The h1 keeps display size because it spans both columns.

steps

xs7pt
sm8.5pt
base9.5pt
h49.5pt
h311pt
h213pt
h120pt

rhythm

line height1.4
baseline advance13.3pt
space9.5pt
measurethe column — 82mm, not a character count
measure chars49
measure positionexempt from foundation.rhythm.measure_position — the column has no surplus to split, because the column IS the measure. Centring would have nothing to act on.
noteLeading tightens with the measure: a shorter line needs less vertical separation to keep the return sweep unambiguous.

spanning

alwaysfrontmatter-title-block, frontmatter-subtitle, frontmatter-byline, frontmatter-dateline, frontmatter-abstract, heading-h1, bibliography-heading, frontmatter-colophon
optionala figure, a table, a code block, a pull quote
nevera paragraph, a list, a block quote, a callout, heading 2 and below, a block of endnotes
noteA wide figure or table opts in per instance. The default is column-width, because a spanning element costs a break in both columns. `always` is element ids, so tools/check.mjs can hold both implementations to it; `optional` and `never` are prose, because they describe author choice and default flow rather than a property either implementation can be held to. Each of those entries is written so that it cannot be read as an id — tools/check.mjs fails if one ever matches an element or section id, which is what keeps the two arms distinguishable by shape now that `figure`, `table`, `list` and `callout` are ids in their own right. `bibliography-heading` is on `always` as its own element, distinct from `heading-h2`: `never`'s "heading 2 and below" names the generic heading levels heading-h2 through heading-h6, not a differently-identified element that happens to share their scale — so a bibliography heading may span both columns while an ordinary section heading may not.

forbidden

sidenoteThere is no margin to put it in. Use a footnote or an endnote.
dropcapA three-line cap at 3.05em is 29pt in an 82mm column — 10mm of the column given to one letter. Open with small caps instead.
measure variantsnarrow and wide are meaningless: the column is the measure.

element overrides

h1

size20pt
spansboth columns
space after9.5pt

h2

size13pt
space before19pt
space after4.75pt

h3

size11pt
space before19pt
space after4.75pt

h4

size9.5pt
space before19pt
space after4.75pt

paragraph

size9.5pt
line height1.4
space after0
first line indent1.25em
noteIndented paragraphs are the default here, not an option: a blank line costs 3% of a column.

blockquote

size9pt
indent left9.5pt

table

size8pt
line height1.3

codeblock

size7.5pt
line height1.4

figure caption

size8pt

footnote

size8pt
noteScoped to the page, not the column: one notes area spanning both columns at the page foot.

endnotes

size8.5pt

pullquote

size12pt
noteColumn width by default. Spanning both columns turns it into a divider, which is a different and heavier thing.

abstract

size9pt
max widththe full text width, set apart above the columns

Full rule in typeset.css — two-column

<article class="typeset typeset--two-column">
  <div class="ts-frontmatter-title-block">
    <h1>The Measure of a Column</h1>
    <p class="ts-frontmatter-subtitle">What changes when a page is divided in two, and what must not</p>
    <p class="ts-frontmatter-byline">Adilson Carvalho</p>
    <p class="ts-frontmatter-dateline">Sydney, 28 August 2026</p>
  </div>
  <div class="ts-frontmatter-abstract">
    <span class="ts-label ts-frontmatter-abstract-label">Abstract</span>
    Dividing an A4 page into two equal columns leaves 82mm of measure. At the single-column base of 11pt that carries 42 characters — below the floor at which a line of prose stays readable.
  </div>
  <h2>The arithmetic</h2>
  <p>A4 is 210mm wide. Take off the standard margin, 20mm on each side, and 170mm of text width remains. A 6mm gutter divides it into two columns of 82mm.</p>
  <p>At 11pt, an 82mm column carries about 42 characters. This specification sets 45 as the floor for a line of prose, so neither 11pt nor 10.5pt (44) is available here. Dropping the base to 9.5pt restores 49 characters.</p>
  <h3>What follows</h3>
  <p>Leading tightens from 1.45 to 1.4. A shorter line needs less vertical separation for the return sweep to stay unambiguous, and the saving is real: at 13.3pt of advance a column holds 54 lines instead of 45.</p>
  <p>Headings come down hardest. A 24pt level-2 heading inside an 82mm column spends three lines saying two words, so the scale runs 20/13/11/9.5.</p>
  <div class="ts-callout">
    <p class="ts-callout-title">What does not change</p>
    <p>The palette, the rule weights, the numeral conventions and the pagination rules are all identical.</p>
  </div>
  <h2>Justification</h2>
  <p>At 49 characters a ragged right edge serrates the column visibly, and the word gaps in an unhyphenated justified column read as rivers. Both faults are tolerable at 66 characters and neither is tolerable here.</p>
  <blockquote class="ts-quote-pullquote"><p>The one place the spec removes a choice.</p></blockquote>
  <p>So the template turns justification on itself rather than offering it as an option.</p>
  <figure class="ts-span">
    <table>
      <caption><span class="ts-label ts-table-caption-label">Table 1</span> — Characters per line for an 82mm column.</caption>
      <thead><tr><th>Base</th><th class="ts-table-cell-numeric">11pt</th><th class="ts-table-cell-numeric">10pt</th><th class="ts-table-cell-numeric">9.5pt</th><th class="ts-table-cell-numeric">9pt</th></tr></thead>
      <tbody>
        <tr><td>Characters</td><td class="ts-table-cell-numeric">42</td><td class="ts-table-cell-numeric">47</td><td class="ts-table-cell-numeric">49</td><td class="ts-table-cell-numeric">52</td></tr>
        <tr><td>Conformant</td><td class="ts-table-cell-numeric">no</td><td class="ts-table-cell-numeric">yes</td><td class="ts-table-cell-numeric">yes</td><td class="ts-table-cell-numeric">yes</td></tr>
      </tbody>
    </table>
  </figure>
  <h2>What the template forbids</h2>
  <p>Sidenotes have nowhere to go: there is no margin left. They become footnotes or endnotes, and an implementation should degrade them visibly rather than positioning them off the page and losing them.</p>
  <p>A three-line drop cap at 3.05em is 29pt — 10mm of the column given to one letter. The cap is neutralised and the opening words carry the signal in small caps instead.</p>
  <h3>Spanning</h3>
  <p>An element spans both columns fully or neither. Spanning costs a break in both columns, so it is opt-in per instance and belongs at the top or the bottom of a page.</p>
  <div class="ts-frontmatter-colophon">Set in EB Garamond, 9.5 on 13.3 points, two columns of 82mm with a 6mm gutter.</div>
</article>

Full implementation in typeset.typ — two-column

#show: two-column.with(
  front: [
    #frontmatter-title-block(
      title: [The Measure of a Column],
      subtitle: [What changes when a page is divided in two, and what must not],
      author: [Adilson Carvalho],
      place-date: [Sydney, 28 August 2026],
    )
    #frontmatter-abstract(width: 100%)[
      Dividing an A4 page into two equal columns leaves 82mm of measure. At the
      single-column base of 11pt that carries 42 characters — below the floor at
      which a line of prose stays readable.
    ]
  ],
)

== The arithmetic

A4 is 210mm wide. Take off the standard margin, 20mm on each side, and 170mm of
text width remains. A 6mm gutter divides it into two columns of 82mm.

At 11pt, an 82mm column carries about 42 characters. This specification sets 45
as the floor for a line of prose, so neither 11pt nor 10.5pt (44) is available
here. Dropping the base to 9.5pt restores 49 characters. The template derives
all of this from the paper and the margin, and refuses the page outright where
the arithmetic does not reach the floor.

=== What follows

Leading tightens from 1.45 to 1.4. A shorter line needs less vertical
separation for the return sweep to stay unambiguous, and the saving is real: at
13.3pt of advance a column holds 54 lines instead of 45.

Headings come down hardest. A 24pt level-2 heading inside an 82mm column spends
three lines saying two words, so the scale runs 20/13/11/9.5.

#callout(title: [What does not change])[
  The palette, the rule weights, the numeral conventions and the pagination
  rules are all identical.
]

== Justification

At 49 characters a ragged right edge serrates the column visibly, and the word
gaps in an unhyphenated justified column read as rivers. Both faults are
tolerable at 66 characters and neither is tolerable here.

#quote(type: "pullquote")[The one place the spec removes a choice.]

So the template asserts justification rather than offering it as an option.

#span(figure(
  caption: [Characters per line for an 82mm column.],
  kind: table,
  supplement: [Table],
  ts-table(
    columns: (1fr, auto, auto, auto, auto),
    rows: 2,
    align: (left + horizon,) + (right + horizon,) * 4,
    table.header([Base], [11pt], [10pt], [9.5pt], [9pt]),
    [Characters], [42], [47], [49], [52],
    [Conformant], [no], [yes], [yes], [yes],
  ),
))

== What the template forbids

Sidenotes have nowhere to go: there is no margin left. They become footnotes or
endnotes, and an implementation should degrade them visibly rather than
positioning them off the page and losing them.

A three-line drop cap at 3.05em is 29pt — 10mm of the column given to one
letter. The cap is neutralised and the opening words carry the signal in small
caps instead.

=== Spanning

An element spans both columns fully or neither. Spanning costs a break in both
columns, so it is opt-in per instance and belongs at the top or the bottom of a
page.

#frontmatter-colophon[
  Set in EB Garamond, 9.5 on 13.3 points, two columns of 82mm with a 6mm
  gutter.
]

05 — Structure

Headings

Sans against the serif body, so hierarchy reads by contrast rather than by size alone. Below level 4 the size stops changing and the register shifts instead: small caps, then italic. Six visually distinct levels inside a 24pt–11pt range.

Two non-negotiable rules: the space above is four times the space below, because a heading belongs to what follows it; and a heading must never be the last thing on a page. Whatever the engine calls that — break-after: avoid, sticky, \needspace — it is mandatory, not a preference.

1 · Heading 1 — document title

24pt sans at a light weight, tracked tighter than body text. It opens flush with the top of the page — no space above it, since nothing on the page comes before it.

The Elements

Body text follows immediately, at the ordinary rhythm — a heading is never the last thing on a page, and never alone.

2 · Heading 2 — section

A section heading also sets the page's own running head, so a reader mid-chapter can see which section they are in without turning back to find it.

Composition

A section opens here, and from this point the running head at the top of the page reads "Composition" until the next one changes it.

3 · Heading 3 — subsection

Smaller again, and still bold sans — clearly subordinate to the section above it, without needing a different colour or register to say so.

The measure

A subsection narrows the topic without opening a new run in the running head.

4 · Heading 4

The last level where size alone still carries the hierarchy — one step above body text, and the last one worth spending a size change on.

Choosing a ratio

Fourth level: still sans, still bold, one size larger than the paragraph beneath it.

5 · Heading 5

No size change from body — the register carries the level instead. Serif, small caps, tracked wider, so the eye reads it as a heading without the page gaining another size step.

On old-style figures

Fifth level: serif small caps at exactly the body size around it.

6 · Heading 6

One register further down: serif italic, still at body size — the quietest heading the scale offers, for a level that barely needs announcing.

A closing remark

Sixth level: serif italic, the same size as the text it introduces.

7 · Run-in heading

The heading shares the first line of its own paragraph, marked off by a muted mid dot — for a dense report where a full heading line costs more vertical space than the point is worth.

Run-in

a heading that shares the first line of its paragraph, which saves vertical space in a dense report.

  • Sans against the serif body, so hierarchy reads by contrast rather than by size alone.
  • Below level 4 the size stops changing and the register shifts instead — small caps, then italic. Six distinct levels inside a 24pt–11pt range.
  • Space above is four times the space below: a heading belongs to what follows it.
  • A heading MUST NOT be the last thing on a page. This is the most common defect in printed documents.

Heading 1 — document title

fontsans
weight300
size24pt
line height1.2
baseline advance28.8pt
tracking-0.015em
colorink
alignleft
space before0
space after5.5pt
break afteravoid
break insideavoid
wrapbalanced

Heading 2 — section

fontsans
weight600
size18pt
line height1.2
baseline advance21.6pt
tracking-0.01em
colorink
alignleft
space before22pt
space after5.5pt
break afteravoid
break insideavoid
wrapbalanced
sets running headyes

Heading 3 — subsection

fontsans
weight600
size14pt
line height1.2
baseline advance16.8pt
colorink
alignleft
space before22pt
space after5.5pt
break afteravoid

Heading 4

fontsans
weight600
size12pt
line height1.2
baseline advance14.4pt
colorink
alignleft
space before22pt
space after5.5pt
break afteravoid

Heading 5

fontserif
weight600
size11pt
variantall small caps
tracking0.06em
colorink
space before22pt
space after5.5pt
break afteravoid

No size change from body — the register carries the level.

Heading 6

fontserif
weight400
styleitalic
size11pt
colorink
space before22pt
space after5.5pt
break afteravoid

Run-in heading

displayinline with the first line of its paragraph
separator ·
separator colorink_faint
space before0
space after0

For dense reports where a full heading line is too much vertical cost.

Full rule in typeset.css — heading

<!-- Heading 1 — document title -->
<h1>The Elements</h1>
<p>Body text follows immediately, at the ordinary rhythm — a heading is never the last thing on a page, and never alone.</p>

<!-- Heading 2 — section -->
<h2>Composition</h2>
<p>A section opens here, and from this point the running head at the top of the page reads "Composition" until the next one changes it.</p>

<!-- Heading 3 — subsection -->
<h3>The measure</h3>
<p>A subsection narrows the topic without opening a new run in the running head.</p>

<!-- Heading 4 -->
<h4>Choosing a ratio</h4>
<p>Fourth level: still sans, still bold, one size larger than the paragraph beneath it.</p>

<!-- Heading 5 -->
<h5>On old-style figures</h5>
<p>Fifth level: serif small caps at exactly the body size around it.</p>

<!-- Heading 6 -->
<h6>A closing remark</h6>
<p>Sixth level: serif italic, the same size as the text it introduces.</p>

<!-- Run-in heading -->
<h6 class="ts-heading-run-in">Run-in</h6><p>a heading that shares the first line of its paragraph, which saves vertical space in a dense report.</p>

Full implementation in typeset.typ — heading

= The Elements

Body text follows immediately, at the ordinary rhythm — a heading is never
the last thing on a page, and never alone.

== Composition

A section opens here, and from this point the running head at the top of
the page reads "Composition" until the next one changes it.

=== The measure

A subsection narrows the topic without opening a new run in the running
head.

==== Choosing a ratio

Fourth level: still sans, still bold, one size larger than the paragraph
beneath it.

===== On old-style figures

Fifth level: serif small caps at exactly the body size around it.

====== A closing remark

Sixth level: serif italic, the same size as the text it introduces.

// heading-run-in has no Typst counterpart here: `show heading: it =>
// block(...)` above always opens a new block for a heading, so nothing in
// this file can share a heading with the first line of its own paragraph.

06 — Structure

Section numbering

Opt-in. Numbers are generated by a counter, never typed, so they cannot drift out of sync when a section moves — and they disappear cleanly when the option comes off, which matters because an essay wants no numbers and a report wants them.

1 · Level-2 number

Generated by a counter, never typed — it cannot drift out of sync when a section moves, and it disappears cleanly wherever typeset--numbered is not applied. Each pane here is its own document, so each starts counting again from one; the Typst tab sets the same sections as one continuous document, and numbers them accordingly.

Scope

The opening section.

Method

A second top-level section — the counter advanced on its own, with nobody typing a number.

2 · Level-3 number

The level-2 counter names the section a subsection belongs to; the level-3 counter resets the moment a new level-2 section begins.

Method

A second top-level section — the counter advanced on its own, with nobody typing a number.

Instrumentation

A subsection, numbered from the section that contains it.

Results

Baseline

The level-3 counter reset when the new level-2 section began.

Opt-in · typeset--numbered

  • Numbers are generated by a counter, never typed. A typed number drifts the moment a section moves.
  • Reports are numbered; essays and letters are not. It must be one switch.

Level-2 number

format1
separatortwo spaces
colorink_faint
numeralslining tabular
resetsthe level-3 counter

Level-3 number

format1.1
separatortwo spaces
colorink_faint
numeralslining tabular

Full rule in typeset.css — numbering

<!-- Level-2 number -->
<div class="typeset typeset--numbered">
  <h2>Scope</h2>
  <p>The opening section.</p>
  <h2>Method</h2>
  <p>A second top-level section — the counter advanced on its own, with nobody typing a number.</p>
</div>

<!-- Level-3 number -->
<div class="typeset typeset--numbered">
  <h2>Method</h2>
  <p>A second top-level section — the counter advanced on its own, with nobody typing a number.</p>
  <h3>Instrumentation</h3>
  <p>A subsection, numbered from the section that contains it.</p>
  <h2>Results</h2>
  <h3>Baseline</h3>
  <p>The level-3 counter reset when the new level-2 section began.</p>
</div>

Set globally rather than per element — this is the “heading” region of typeset.typ: heading

// Each HTML pane is its own document, so each starts its counters again from
// one: the second pane there opens at "Method" and numbers it 1, where this
// one continuous document has already spent 1 on "Scope". No sentence below
// names a number, for that reason — the headings carry them, and the headings
// are what differ between one document and two.
#show: typeset.with(numbered: true)

= Scope

The opening section.

= Method

A second top-level section — the counter advanced on its own, with nobody
typing a number.

== Instrumentation

A subsection, numbered from the section that contains it.

= Results

== Baseline

The level-3 counter reset when the new level-2 section began.

07 — Prose

Paragraphs

Two conventions, one switch. Spaced paragraphs are the default and suit reports and letters. Indented paragraphs with no gap are the book convention and read better in continuous prose; the first paragraph after any heading, block or break stays flush left in both.

Orphan and widow control set to 2: never a single line left behind at a page foot, never one carried alone to the next head.

1 · Spaced

The first duty of a paragraph is to be findable. Spaced paragraphs announce themselves with white space, which is why they suit documents that get skimmed: memos, reports, letters.

The cost is vertical space, and a certain restlessness over many pages. Every gap is a small invitation to stop reading.

2 · Indented

On continuous prose

The first paragraph after a heading needs no indent: the heading has already marked the start. So it sits flush.

Subsequent paragraphs indent by 1.5em, with no gap. The page becomes a continuous field of text, which is exactly what you want when the reader is meant to read every word rather than hunt for one.

This is the convention of nearly every printed book you own, and it is almost never seen on the web — which is the clearest sign that print and screen are different media.

  • Two conventions, one switch. Spaced paragraphs suit documents that get skimmed. Indented paragraphs with no gap suit continuous prose and are the convention of nearly every printed book.
  • The first paragraph after any heading, block or break is flush left: the thing above it has already marked the start.

Paragraph — spaced (default)

fontserif
weight400
size11pt
line height1.45
baseline advance15.95pt
numeralsoldstyle proportional
colorink
space after11pt
first line indent0
orphans2
widows2
max width33em

Paragraph — indented opt-in · typeset--indented

space after0
first line indent1.5em
first line indent after block0

Applies after a heading, blockquote, figure or section break, and to the first paragraph of the document.

Full rule in typeset.css — paragraph

<!-- Spaced -->
<p>The first duty of a paragraph is to be findable. Spaced paragraphs announce themselves with white space, which is why they suit documents that get skimmed: memos, reports, letters.</p>
<p>The cost is vertical space, and a certain restlessness over many pages. Every gap is a small invitation to stop reading.</p>

<!-- Indented -->
<div class="typeset typeset--indented">
  <h4>On continuous prose</h4>
  <p>The first paragraph after a heading needs no indent: the heading has already marked the start. So it sits flush.</p>
  <p>Subsequent paragraphs indent by 1.5em, with no gap. The page becomes a continuous field of text, which is exactly what you want when the reader is meant to read every word rather than hunt for one.</p>
  <p>This is the convention of nearly every printed book you own, and it is almost never seen on the web — which is the clearest sign that print and screen are different media.</p>
</div>

Full implementation in typeset.typ — paragraph

// Spaced is the document default, so it needs no wrapper here.
The first duty of a paragraph is to be findable. Spaced paragraphs announce
themselves with white space, which is why they suit documents that get skimmed:
memos, reports, letters.

The cost is vertical space, and a certain restlessness over many pages. Every
gap is a small invitation to stop reading.

#block-indented[
  ==== On continuous prose

  The first paragraph after a heading needs no indent: the heading has already
  marked the start. So it sits flush.

  Subsequent paragraphs indent by 1.5em, with no gap. The page becomes a
  continuous field of text, which is exactly what you want when the reader is
  meant to read every word rather than hunt for one.

  This is the convention of nearly every printed book you own, and it is almost
  never seen on the web — which is the clearest sign that print and screen are
  different media.
]

08 — Prose

Alignment: ragged or justified

Ragged right is the default, and it is a choice rather than an absence. Justification buys a clean right edge and pays for it in uneven word spacing; a ragged setting keeps the spacing even and gives up the edge. Neither is more correct — but the cost lands differently depending on who the document is addressed to.

Ragged right for a letter, a memorandum, a short note: anything addressed to a person rather than to a readership. Justification for continuous prose at a full measure: an essay, a report, a paper. It is mandatory in two columns, where a ragged edge at 49 characters serrates the column.

Justification and hyphenation are a single decision. Justified text without hyphenation opens rivers of white space; hyphenation without justification breaks words for no gain. Take both or neither — and hyphenation needs the document language declared, since an English dictionary applied to Portuguese produces confident nonsense.

Two traps, both learned the hard way on this stylesheet. text-align inherits, so setting it on the document container justifies the subtitle, the byline, the captions and the address blocks too — every block that must stay flush declares its own text-align: left rather than relying on an exception list here. And text-align-last: left has to be explicit: under a paginating engine the fragmented text no longer looks like the end of a paragraph, and every last line gets stretched.

1 · Ragged right — the default

Even word spacing throughout. The right edge is uneven, and the rag itself is what to judge: an even rag reads as deliberate.

Typographical craftsmanship consists in the appropriate use of the incomparable resources of a well-designed typeface, and in the discipline required to leave those resources alone when the content does not need them. Justification is one such resource: expensive, occasionally beautiful, and demanding of the hyphenation dictionary that makes it possible.

2 · Justified, with hyphenation

A clean right edge. The word spacing now varies line by line, and hyphens appear to keep that variation small.

Typographical craftsmanship consists in the appropriate use of the incomparable resources of a well-designed typeface, and in the discipline required to leave those resources alone when the content does not need them. Justification is one such resource: expensive, occasionally beautiful, and demanding of the hyphenation dictionary that makes it possible.

3 · Justified, no hyphenation — the mistake

Neither edge nor spacing. With no hyphens to absorb the slack, the gaps open into rivers running down the page.

Typographical craftsmanship consists in the appropriate use of the incomparable resources of a well-designed typeface, and in the discipline required to leave those resources alone when the content does not need them. Justification is one such resource: expensive, occasionally beautiful, and demanding of the hyphenation dictionary that makes it possible.

4 · An exception inside an exception

The override reaches through surrounding context in either direction, not only against the document's own default: a ragged block nested inside a justified one stays ragged, because a direct match always beats an inherited value.

A justified block can carry its own exception, the same as the document can. This sentence is stretched to the measure like the ones above it.

This paragraph is nested inside the justified block above and still reads ragged right — the override reaches through surrounding context in either direction, not only against the document's own default.

Opt-in · typeset--justified

  • Ragged right is the DEFAULT, and it is a choice rather than an absence — a document states it. Justification buys a clean right edge at the cost of uneven word spacing; a ragged setting keeps the spacing even and gives up the edge. Neither is more correct, but the cost lands differently by document.
  • Choose ragged right for a letter, a memorandum, a short note — anything addressed to a person rather than to a readership. Justification reads as institutional, and its even edge is the visual signature of print that was set for strangers.
  • Choose justification for continuous prose at a full measure: an essay, a report, a paper. It is mandatory in two columns, where a ragged edge at 49 characters serrates the column.
  • These are a single decision. Justification without hyphenation opens rivers of white space; hyphenation without justification breaks words for no gain. Take both or neither.
  • Hyphenation is per-language and requires the document language to be declared. An English dictionary applied to Portuguese produces confident nonsense.
  • The last line of a paragraph is NEVER stretched. State this explicitly — a paginating engine fragments the text, so the visual last line stops looking like the end of a paragraph and gets justified. It does not reproduce in an unpaginated preview.
  • Justification applies to prose only. It MUST NOT reach a subtitle, byline, caption, address block, table cell, heading or listing. In an engine where alignment inherits, every such block declares its own alignment rather than relying on an exception list.
  • Alignment inherits, and so does last-line alignment. A block that sets its own alignment — a centred pull quote, a centred section break — must also set its own last-line alignment, or the document's justification flushes its last line (its only line, for a one-liner) to the left.

Ragged right (default) opt-in · typeset--ragged — names the default; no class needed to get it

alignleft
align last lineleft
hyphenationmanual — never automatic
line breakinghigh effort: avoid a very short last line, and even out the right edge
applies toparagraph, list item, blockquote, definition description, callout

Hyphenation exists to serve justification. Without justification a hyphen breaks a word for no gain, so it stays off.

The rag itself is the thing to judge: an even rag reads as deliberate, a rag with one very short line and one very long one reads as an accident.

Justified

alignjustify
align last lineleft
justify methodinter-word
hyphenationautomatic
hyphenation min word chars6
hyphenation min chars before break3
hyphenation min chars after break3
max consecutive hyphens2
applies toparagraph, list item, blockquote, definition description, callout

Where unsupported — automatic hyphenation, but at the engine's own built-in limits — the four tuning values above are not configurable

Never justified

alignleft
hyphenationmanual
applies toall headings, subtitle, byline, dateline, abstract, attribution, epigraph, verse, caption, figure caption, address block, table cell, table header, code block, table of contents, bibliography, endnotes, sidenote, letterhead, salutation, closing, signature, enclosures, postscript, definition term

Full rule in typeset.css — justification

<!-- Ragged right — the default -->
<div class="typeset typeset--ragged" lang="en">
  <p>Typographical craftsmanship consists in the appropriate use of the incomparable resources of a well-designed typeface, and in the discipline required to leave those resources alone when the content does not need them. Justification is one such resource: expensive, occasionally beautiful, and demanding of the hyphenation dictionary that makes it possible.</p>
</div>

<!-- Justified, with hyphenation -->
<div class="typeset typeset--justified" lang="en">
  <p>Typographical craftsmanship consists in the appropriate use of the incomparable resources of a well-designed typeface, and in the discipline required to leave those resources alone when the content does not need them. Justification is one such resource: expensive, occasionally beautiful, and demanding of the hyphenation dictionary that makes it possible.</p>
</div>

<!-- Justified, no hyphenation — the mistake -->
<div class="typeset typeset--justified ts-utility-no-hyphens" lang="en">
  <p>Typographical craftsmanship consists in the appropriate use of the incomparable resources of a well-designed typeface, and in the discipline required to leave those resources alone when the content does not need them. Justification is one such resource: expensive, occasionally beautiful, and demanding of the hyphenation dictionary that makes it possible.</p>
</div>

<!-- An exception inside an exception -->
<div class="typeset typeset--justified" lang="en">
  <p>A justified block can carry its own exception, the same as the document can. This sentence is stretched to the measure like the ones above it.</p>
  <div class="typeset--ragged">
    <p>This paragraph is nested inside the justified block above and still reads ragged right — the override reaches through surrounding context in either direction, not only against the document's own default.</p>
  </div>
</div>

Full implementation in typeset.typ — justification

#ragged-right[
  Typographical craftsmanship consists in the appropriate use of the
  incomparable resources of a well-designed typeface, and in the discipline
  required to leave those resources alone when the content does not need them.
  Justification is one such resource: expensive, occasionally beautiful, and
  demanding of the hyphenation dictionary that makes it possible.
]

#justified[
  Typographical craftsmanship consists in the appropriate use of the
  incomparable resources of a well-designed typeface, and in the discipline
  required to leave those resources alone when the content does not need them.
  Justification is one such resource: expensive, occasionally beautiful, and
  demanding of the hyphenation dictionary that makes it possible.
]

#justified(lang: none)[
  Typographical craftsmanship consists in the appropriate use of the
  incomparable resources of a well-designed typeface, and in the discipline
  required to leave those resources alone when the content does not need them.
  Justification is one such resource: expensive, occasionally beautiful, and
  demanding of the hyphenation dictionary that makes it possible.
]

// `justified` also names a parameter of `typeset()` — the document-wide
// default. The two are not the same knob: this whole demo sits in a document
// left at that default (ragged), so every `#justified[...]` block above is
// already the per-block override winning over it. The reverse direction —
// `#ragged-right` winning inside a block that is itself justified — nests
// the same way, with no document-wide setting involved at all:
#justified[
  A justified block can carry its own exception, the same as the document
  can. This sentence is stretched to the measure like the ones above it.

  #ragged-right[
    This paragraph is nested inside the justified block above and still reads
    ragged right — the override reaches through surrounding context in either
    direction, not only against the document's own default.
  ]
]

09 — Prose

Drop cap

For the opening of an essay or a chapter, and nowhere else. The font-size and line-height are tuned together so the cap covers exactly three lines at the default leading — change the leading and this needs retuning.

The opening words go in small caps: without them the jump from 33pt to 11pt is too abrupt, and the eye skips the first line.

1 · Drop cap

Three lines deep, serif, in the accent colour — for the opening of an essay or a chapter, and nowhere else.

There is nothing especially modern about wanting a document to look considered. The drop cap descends from the illuminated initial, which existed for a practical reason: in a manuscript with no title page and no table of contents, a large decorated letter was the only way to tell the reader that something had begun.

The practical reason is gone. The signal is not.

2 · Opening words

The first few words in small caps, so the jump from a 33pt initial back to 11pt body text has a step in between rather than a cliff.

There is nothing especially modern about wanting a document to look considered — the small-caps run carries the eye back down to body size before the paragraph continues.

  • For the opening of an essay or a chapter, and nowhere else.
  • The size and line height are tuned together to cover exactly three lines. Change the leading and the cap must be retuned.
  • The opening words go in small caps. Without them the jump from 33pt to 11pt is too abrupt and the eye skips the first line.

Drop cap

fontserif
weight400
size3.05em
size pt33.55pt
line height0.86
coloraccent
floatleft
lines covered3
padding right0.06em
padding top0.02em
first line indent0

Opening words

variantall small caps
tracking0.04em
extentthe first two to four words

Full rule in typeset.css — dropcap

<!-- Drop cap -->
<p class="ts-dropcap">There is nothing especially modern about wanting a document to look considered. The drop cap descends from the illuminated initial, which existed for a practical reason: in a manuscript with no title page and no table of contents, a large decorated letter was the only way to tell the reader that something had begun.</p>
<p>The practical reason is gone. The signal is not.</p>

<!-- Opening words -->
<p class="ts-dropcap"><span class="ts-dropcap-lede">There is nothing</span> especially modern about wanting a document to look considered — the small-caps run carries the eye back down to body size before the paragraph continues.</p>

Full implementation in typeset.typ — dropcap

// Typst has no counterpart to the lede. The CSS implementation sets the opening
// words of this paragraph in small caps with .ts-dropcap-lede; dropcap() takes
// the whole paragraph as one body and offers no way to mark a run inside it.
// So the HTML file's second pane — the same opening with its first three words
// in small caps — has no paragraph of its own here: it would set identically
// to the one below, which is the difference the pane exists to show.
//
// dropcap() reads body.text, so the body must be a single text element. Typst's
// markup parser may split a paragraph into several, and the call is then refused
// with `sequence does not have field "text"` — a colon, a parenthesis, an
// apostrophe and an ellipsis are four marks that split it, not the whole set.
// So this paragraph is written in plain words, where the CSS implementation
// punctuates it "a practical reason: in a manuscript".

#dropcap[There is nothing especially modern about wanting a document to look considered. The drop cap descends from the illuminated initial, which existed for a practical reason. In a manuscript with no title page and no table of contents, a large decorated letter was the only way to tell the reader that something had begun.]

The practical reason is gone. The signal is not.

10 — Prose

Inline emphasis

Nested emphasis flips to roman rather than compounding — correct typographic behaviour, and something most engines get wrong by default. A superscript or subscript must not alter the leading of the line it sits on; a footnote reference that opens up its line is a defect.

Underline is deliberately reserved for <ins>. On paper it is a typewriter's substitute for italic, and there is no reason to imitate a typewriter.

1 · Bold

Weight, not italics, for the sentence's own load-bearing clause — the thing a skimming reader must not miss.

The deadline is Friday, not Monday, whatever the earlier email said.

2 · Italic

For a title or a term of art — not a general-purpose emphasis dial reached for whenever a word feels important.

She is still reading Typographie, Ruder's own case for restraint.

3 · Nested italic

Emphasis inside emphasis flips back to roman rather than compounding into a heavier italic — correct behaviour, and something most engines get wrong by default.

The review called it the definitive account — definitive, not merely thorough — of the period.

4 · Small caps

For named entities, acronyms and stage directions — never a whole sentence shouted in capitals.

The Bauhaus closed its doors in 1933.

5 · Abbreviation

Small caps and a dotted underline mark a term the reader can expand. The expansion itself lives in the title attribute, for a screen reader or a hovering pointer — paper offers neither, so the mark is what a printed page keeps.

Exported as a PDF, the file survives whatever the recipient has installed.

6 · Superscript

Raised, without opening up the line's own leading — a reference mark must not stretch the line it sits in.

The claim is contested,3 and the footnote says why.

7 · Subscript

The same rule below the baseline — a formula's line keeps exactly the surrounding paragraph's height.

Water is H2O, nothing more exotic.

8 · Deletion

A struck, muted run for text a revision removes — present in the document, marked as gone rather than cut.

Delivery takes ten business days.

9 · Insertion

An underlined run for text a revision adds — the tracked-changes counterpart to deletion, meant to sit beside it rather than replace it silently.

Delivery takes ten five business days.

10 · Highlight

A wash behind the glyphs, bleeding slightly beyond them, for one phrase the reader's eye should catch first — not a device for a whole paragraph.

The clause that actually changes the deadline is buried in paragraph four.

11 · Key

A drawn keycap for a literal keystroke — the one inline style with its own border, because a key is an object on a keyboard, not a word to emphasise.

Save early: a document is one ⌘S away from being backed up.

  • Nested emphasis FLIPS to roman rather than compounding. This is correct typographic behaviour and most engines get it wrong by default.
  • Superscripts and subscripts must not disturb the line's leading.
  • Underline is reserved for insertions. On paper it is a typewriter's substitute for italic, and there is no reason to imitate a typewriter.

Bold

weight600

For the load-bearing clause.

Italic

styleitalic

For a title or a term of art.

Nested italic

stylenormal

Small caps

variantall small caps
tracking0.05em
numeralsoldstyle

Named entities, acronyms, stage directions.

Abbreviation

variantall small caps
tracking0.05em
underline0.5pt dotted
underline colorink_faint

Superscript

size0.72em
baseline shift+0.45em
line height0
affects leadingno

Subscript

size0.72em
baseline shift-0.22em
line height0
affects leadingno

Deletion

decorationline-through
colorink_muted

Insertion

decorationunderline
underline offset0.15em

Highlight

backgroundwash
bleed2pt beyond the glyphs

Key

fontsans
size0.85em
padding0.15em 0.35em
border0.5pt rule
border bottom1.5pt rule
radius2pt

Full rule in typeset.css — inline

<!-- Bold -->
<p>The deadline is <strong>Friday, not Monday</strong>, whatever the earlier email said.</p>

<!-- Italic -->
<p>She is still reading <em>Typographie</em>, Ruder's own case for restraint.</p>

<!-- Nested italic -->
<p>The review called it <em>the definitive account — <em>definitive</em>, not merely thorough — of the period</em>.</p>

<!-- Small caps -->
<p>The <span class="ts-inline-small-caps">Bauhaus</span> closed its doors in 1933.</p>

<!-- Abbreviation -->
<p>Exported as a <abbr title="Portable Document Format">PDF</abbr>, the file survives whatever the recipient has installed.</p>

<!-- Superscript -->
<p>The claim is contested,<sup>3</sup> and the footnote says why.</p>

<!-- Subscript -->
<p>Water is H<sub>2</sub>O, nothing more exotic.</p>

<!-- Deletion -->
<p>Delivery takes <del>ten</del> business days.</p>

<!-- Insertion -->
<p>Delivery takes <del>ten</del> <ins>five</ins> business days.</p>

<!-- Highlight -->
<p>The clause that actually <mark>changes the deadline</mark> is buried in paragraph four.</p>

<!-- Key -->
<p>Save early: a document is one <kbd>⌘S</kbd> away from being backed up.</p>

Full implementation in typeset.typ — inline

The deadline is *Friday, not Monday*, whatever the earlier email said.

She is still reading _Typographie_, Ruder's own case for restraint.

The review called it _the definitive account — #emph[definitive], not merely
thorough — of the period_.

The #text(..smcp)[Bauhaus] closed its doors in 1933.

// No abbreviation element in Typst, and no title metadata for a reader to
// expand — paper offers neither a screen reader nor a hovering pointer
// anyway. The two primitives underneath (small caps, dotted underline)
// compose directly here, the same as the CSS selector `abbr[title]` does.
Exported as a #underline(
  stroke: (paint: ink-faint, thickness: 0.5pt, dash: "dotted"),
  offset: 0.15em,
)[#text(..smcp)[PDF]], the file survives whatever the recipient has installed.

The claim is contested,#super[3] and the footnote says why.

Water is H#sub[2]O, nothing more exotic.

Delivery takes #strike[ten] business days.

Delivery takes #strike[ten] #underline[five] business days.

The clause that actually #highlight(fill: wash)[changes the deadline] is
buried in paragraph four.

Save early: a document is one #key[⌘S] away from being backed up.

11 — Prose

Inline code

A monospace face reads optically larger than the serif body at the same size, so the run is set at 0.86em to correct for it — not to shrink it for its own sake.

Ligatures are off. An arrow rendered as one glyph is charming in an editor and wrong in a document that quotes source verbatim.

How it must look

Ligatures off: a quoted arrow stays two glyphs, not one — charming in an editor, wrong in a document that quotes source.

The type signature fn(a) -> b is what the compiler sees; a document that quotes it verbatim must not silently improve it.

Inline code

fontmono
size0.86em
ligaturesdisabled
backgroundwash
padding0.1em 0.28em
radius2pt
wrapmay break within a word

The 0.86em corrects for the monospace face running optically large beside the serif.

Ligatures off: an arrow rendered as one glyph is charming in an editor and wrong in a document that quotes source.

Full rule in typeset.css — code-inline

<p>The type signature <code>fn(a) -&gt; b</code> is what the compiler sees; a document that quotes it verbatim must not silently improve it.</p>

Set globally rather than per element — this is the “codeblock” region of typeset.typ: codeblock

The type signature `fn(a) -> b` is what the compiler sees; a document that
quotes it verbatim must not silently improve it.

13 — Prose

Numerals

Three kinds, three jobs. Old-style figures have ascenders and descenders, sit inside the x-height, and belong in running prose where uniform-height digits shout. Lining figures are all cap-height, for headings. Tabular figures share one advance width, which is the only reason a column of numbers can align.

Getting this wrong is the most visible amateur tell in a set document — and in every engine it is one setting.

1 · In running prose

Old-style: ascenders and descenders, sitting inside the x-height rather than shouting at cap height.

Between 1908 and 1935 the foundry cut 47 sizes, of which 12 survive.

2 · In headings and display

Lining, for a heading or a title — cap-height figures that sit level with the capitals around them.

The 1908–1935 Catalogue: 47 Sizes

3 · In tables and columns

One advance width per digit — the only reason a column of numbers can align on its own decimal point without a typesetter's help.

CutSizes
Original47
Revival8
Digital1,204

4 · Fractions

A diagonal fraction, built from the digits and a slash — not a plain "1/2" left to set as three characters in a row.

Fractions: 1/2, 3/4, 7/8

  • Three kinds, three jobs. Getting this wrong is the most visible amateur tell in a set document, and it is one setting.
  • Old-style figures have ascenders and descenders and sit inside the x-height. Lining figures are uniform cap height. Tabular figures share one advance width, which is the only reason a column of numbers can align.

In running prose

numeralsoldstyle proportional

In headings and display

numeralslining proportional

In tables and columns

numeralslining tabular

Fractions

formdiagonal

Full rule in typeset.css — numeral

<!-- In running prose -->
<p class="ts-numeral-prose">Between 1908 and 1935 the foundry cut 47 sizes, of which 12 survive.</p>

<!-- In headings and display -->
<h2 class="ts-numeral-display">The 1908–1935 Catalogue: 47 Sizes</h2>

<!-- In tables and columns -->
<table>
  <thead><tr><th>Cut</th><th>Sizes</th></tr></thead>
  <tbody>
    <tr><td>Original</td><td class="ts-numeral-tabular">47</td></tr>
    <tr><td>Revival</td><td class="ts-numeral-tabular">8</td></tr>
    <tr><td>Digital</td><td class="ts-numeral-tabular">1,204</td></tr>
  </tbody>
</table>

<!-- Fractions -->
<p>Fractions: <span class="ts-numeral-fraction">1/2</span>, <span class="ts-numeral-fraction">3/4</span>, <span class="ts-numeral-fraction">7/8</span></p>

Set globally rather than per element — this is the “foundation” region of typeset.typ: foundation

#text(..oldstyle)[Old-style, in prose — between 1908 and 1935 the foundry cut 47
sizes, of which 12 survive.]

#text(..lining)[Lining, for display — BETWEEN 1908 AND 1935, 47 SIZES]

#table(
  columns: (1fr, auto, auto),
  align: (left + horizon, right + horizon, right + horizon),
  [Cut], [Sizes], [Surviving],
  [Original], [47], [12],
  [Revival], [8], [8],
  [Digital], [1,204], [1,204],
)

// No dedicated fraction setting: the "frac" OpenType feature is requested
// directly on the run, the same feature a diagonal fraction needs in any
// engine.
Fractions: #text(features: ("frac",))[1/2], #text(features: ("frac",))[3/4],
#text(features: ("frac",))[7/8].

14 — Blocks

Quotations

Four different jobs, four treatments. They are shown one at a time below, because the whole point is that a reader should be able to tell them apart at a glance — if you cannot say which is which, the styling has failed.

Attribution always sits outside the quotation, prefixed with an em dash. Putting it inside makes the source part of what was said.

1 · Block quote

Evidence: someone else's words, quoted to be examined. A hairline rule down the left, indented, one point smaller than the body. The rule is the signal — it marks the full vertical extent of what is quoted, so the reader knows exactly where the quotation stops.

Ruder puts the case for restraint more sharply than anyone since:

Typography has one plain duty before it and that is to convey information in writing. No argument or consideration can absolve typography from this duty.

Which is a hard standard, and the right one.

2 · Attribution

Who said it, on a line of its own below the quoted words and never inside them — inside, it becomes part of what was said. Smaller than the quotation, muted, and opened by an em dash the stylesheet supplies, so the markup carries the name and nothing else.

Ruder puts the case for restraint more sharply than anyone since:

Typography has one plain duty before it and that is to convey information in writing. No argument or consideration can absolve typography from this duty.

Emil Ruder, Typographie, 1967

Which is a hard standard, and the right one.

3 · Epigraph

A quotation that opens a chapter, before the argument starts. No rule and no indent — it is not evidence, so it gets no apparatus. Instead it is set italic, smaller, in muted ink, and pushed to the right margin. Nothing follows it except the text it introduces.

A book is a machine to think with.

I. A. Richards

There is nothing especially modern about wanting a document to look considered…

4 · Pull quote

Display type lifted out of the body — the reader has already met these words, or will. Rules above and below, centred, sans-serif at 15pt. Because it repeats, it must never be the only place a claim appears: a skimming reader would get the claim without its qualification.

…the sentence applies with more force to layout than to decoration. An over-wide column is not an aesthetic misjudgement; it is a failure to convey.

No argument can absolve typography from this duty.

And the duty is discharged in the measure long before it is discharged in the typeface.

5 · Verse

Quoted poetry, where the line breaks are the author's and must survive. Indented as a block, with runover lines indented further than the verse line they continue — so a line that wrapped can never be mistaken for a line the poet wrote.

Whose woods these are I think I know. His house is in the village though; He will not see me stopping here To watch his woods fill up with snow.Robert Frost
  • Four jobs, four treatments. A block quote is evidence. An epigraph opens a chapter. A pull quote is display type lifted from the body. Verse preserves the poet's line breaks.
  • A pull quote repeats text that already appears in the body, so it MUST NOT be the only place a claim appears — a skimming reader would get the claim without its qualification.
  • Attribution always sits outside the quotation. Inside, it becomes part of what was said.

Block quote

fontserif
size10.5pt
line height1.45
colorink
space before13.75pt
space after13.75pt
indent left16.5pt
border left1pt rule
break insideavoid
last child space after0

Attribution

displayblock
alignleft
space before5.5pt
size9.5pt
stylenormal
colorink_muted
prefixem dash and a space

Epigraph

fontserif
styleitalic
size10pt
colorink_muted
alignleft
block alignmentflush right
max width24em
bordernone
space after22pt

Cite inside an epigraph is roman, not italic — the surrounding block is already italic.

Pull quote

fontsans
weight300
size15pt
line height1.3
aligncentre
wrapbalanced
border top1.5pt rule_strong
border bottom0.5pt rule
padding top11pt
padding bottom11pt
space before16.5pt
space after16.5pt

Verse

fontserif
size11pt
alignleft
line breakspreserved as authored
indent left22pt
runover indent1.5em
bordernone

Where unsupported — in Typst: the left indent and the authored line breaks, but no runover indent — a wrapped line reads the same as one the poet wrote

Runover lines indent further than the verse line they continue, so a wrapped line cannot be mistaken for a new one.

Full rule in typeset.css — quote

<!-- Block quote -->
<p>Ruder puts the case for restraint more sharply than anyone since:</p>
<blockquote>
  <p>Typography has one plain duty before it and that is to convey information in writing. No argument or consideration can absolve typography from this duty.</p>
</blockquote>
<p>Which is a hard standard, and the right one.</p>

<!-- Attribution -->
<p>Ruder puts the case for restraint more sharply than anyone since:</p>
<blockquote>
  <p>Typography has one plain duty before it and that is to convey information in writing. No argument or consideration can absolve typography from this duty.</p>
  <cite>Emil Ruder, <em>Typographie</em>, 1967</cite>
</blockquote>
<p>Which is a hard standard, and the right one.</p>

<!-- Epigraph -->
<blockquote class="ts-quote-epigraph">
  <p>A book is a machine to think with.</p>
  <cite>I. A. Richards</cite>
</blockquote>
<p>There is nothing especially modern about wanting a document to look considered…</p>

<!-- Pull quote -->
<p>…the sentence applies with more force to layout than to decoration. An over-wide column is not an aesthetic misjudgement; it is a failure to convey.</p>
<blockquote class="ts-quote-pullquote">
  <p>No argument can absolve typography from this duty.</p>
</blockquote>
<p>And the duty is discharged in the measure long before it is discharged in the typeface.</p>

<!-- Verse -->
<blockquote class="ts-quote-verse">Whose woods these are I think I know.
His house is in the village though;
He will not see me stopping here
To watch his woods fill up with snow.<cite>Robert Frost</cite></blockquote>

Full implementation in typeset.typ — quote

// The HTML file shows this quotation twice: once bare, for the block quote
// itself, and once attributed, for quote-attribution. Here it appears once,
// attributed — `attribution:` is the whole difference between the two panes,
// and a second copy of the same four lines would teach nothing the parameter
// name does not already say.
Ruder puts the case for restraint more sharply than anyone since:

#quote(attribution: [Emil Ruder, _Typographie_, 1967])[
  Typography has one plain duty before it and that is to convey information in
  writing. No argument or consideration can absolve typography from this duty.
]

Which is a hard standard, and the right one.

#quote(type: "epigraph", attribution: [I. A. Richards])[
  A book is a machine to think with.
]

There is nothing especially modern about wanting a document to look considered…

…the sentence applies with more force to layout than to decoration. An over-wide
column is not an aesthetic misjudgement; it is a failure to convey.

#quote(type: "pullquote")[No argument can absolve typography from this duty.]

And the duty is discharged in the measure long before it is discharged in the
typeface.

// type: "verse" sets the stanza and nothing else: it takes no attribution,
// where the CSS implementation closes the verse with <cite>Robert Frost</cite>.
#quote(type: "verse")[
  Whose woods these are I think I know. \
  His house is in the village though; \
  He will not see me stopping here \
  To watch his woods fill up with snow.
]

15 — Blocks

Lists

The marker's colour, weight and column width must be under the spec's control, not the engine's defaults. A grey marker with black text reads as one thing; a black marker competes with the first word. Runover lines align to the text, never to the marker.

Definition lists are underused. They are the right shape for a glossary, a set of terms, or the field-and-value blocks in a letter — anywhere a short label governs a paragraph.

1 · Unordered list

A grey mid-dot marker, weighted so it reads as a mark rather than competing with the words beside it. A runover line aligns to the text, never to the marker.

  • An unordered item.
  • A second item, long enough to wrap onto a second line so you can see that the runover aligns to the text, not to the marker.

2 · Unordered list, nested

The nested marker switches to an en dash, lighter in weight than the level above it — two levels deep is the limit worth designing for.

  • A first-level item, followed by a nested list:
    • The nested marker is an en dash.
    • Two levels is the limit worth designing for.

3 · Ordered list

Tabular lining figures, so a run past nine keeps every marker in the same column rather than drifting once a second digit appears.

  1. First item.
  2. Second item.
  3. Third item.
  4. Fourth item.
  5. Fifth item.
  6. Sixth item.
  7. Seventh item.
  8. Eighth item.
  9. Ninth item.
  10. Tenth item — the second digit arrives here, and the marker still sits in the column the single digits held.
  11. Eleventh item.

4 · Ordered list, nested

Sub-items restart the counter and switch to lower alpha — the same demotion the nested unordered marker makes.

  1. A first-level item, followed by sub-items:
    1. First sub-item.
    2. Second sub-item.

5 · Tight list

No space between items — for an enumeration of values, not a sequence of sentences a reader pauses between.

  • 11pt body
  • 9.5pt table
  • 8pt caption label

6 · Definition term

Sans, bold, and a size down from body — a term reads as a label the description answers, not as a sentence in its own right.

Measure
The length of a line of type, counted in characters rather than in millimetres.

7 · Definition description

Serif, at body size, indented under its term — the answer reads as prose, set apart from the label that introduces it.

Measure
The length of a line of type, counted in characters rather than in millimetres.
Leading
The vertical distance between baselines. Named for the strips of lead once inserted between lines of metal type.
  • The marker's colour, weight and width must be under control. A grey marker with black text reads as one thing; a black marker competes with the first word.
  • Runover lines align to the text, never to the marker.
  • Two levels of nesting is the limit worth designing for.
  • Definition lists are underused: they are the right shape for a glossary, a set of terms, or the field-and-value blocks of a letter.

Unordered list

marker·
marker colorink_muted
marker weight700
marker column width1.4em
indent1.4em
item space after2.75pt
space after11pt

Unordered list, nested

marker–
marker weight400
space before2.75pt

Ordered list

marker1.
marker colorink_muted
numeralslining tabular
marker column width1.4em
indent1.4em
item space after2.75pt

Tabular numerals so 9 and 10 align.

Ordered list, nested

markera.

Tight list opt-in · ts-list-tight

item space after0

For enumerations that are not prose.

Definition term

fontsans
weight600
size9.5pt
alignleft
space before6.6pt
space before first0
break afteravoid

Definition description

fontserif
size11pt
indent left16.5pt
space after0

Full rule in typeset.css — list

<!-- Unordered list -->
<ul>
  <li>An unordered item.</li>
  <li>A second item, long enough to wrap onto a second line so you can see that the runover aligns to the text, not to the marker.</li>
</ul>

<!-- Unordered list, nested -->
<ul>
  <li>A first-level item, followed by a nested list:
    <ul>
      <li>The nested marker is an en dash.</li>
      <li>Two levels is the limit worth designing for.</li>
    </ul>
  </li>
</ul>

<!-- Ordered list -->
<ol>
  <li>First item.</li>
  <li>Second item.</li>
  <li>Third item.</li>
  <li>Fourth item.</li>
  <li>Fifth item.</li>
  <li>Sixth item.</li>
  <li>Seventh item.</li>
  <li>Eighth item.</li>
  <li>Ninth item.</li>
  <li>Tenth item — the second digit arrives here, and the marker still sits in the column the single digits held.</li>
  <li>Eleventh item.</li>
</ol>

<!-- Ordered list, nested -->
<ol>
  <li>A first-level item, followed by sub-items:
    <ol>
      <li>First sub-item.</li>
      <li>Second sub-item.</li>
    </ol>
  </li>
</ol>

<!-- Tight list -->
<ul class="ts-list-tight">
  <li>11pt body</li>
  <li>9.5pt table</li>
  <li>8pt caption label</li>
</ul>

<!-- Definition term -->
<dl>
  <dt>Measure</dt>
  <dd>The length of a line of type, counted in characters rather than in millimetres.</dd>
</dl>

<!-- Definition description -->
<dl>
  <dt>Measure</dt>
  <dd>The length of a line of type, counted in characters rather than in millimetres.</dd>
  <dt>Leading</dt>
  <dd>The vertical distance between baselines. Named for the strips of lead once inserted between lines of metal type.</dd>
</dl>

Full implementation in typeset.typ — list

- An unordered item.
- A second item, long enough to wrap onto a second line so you can see that the
  runover aligns to the text, not to the marker.

- A first-level item, followed by a nested list:
  - The nested marker is an en dash.
  - Two levels is the limit worth designing for.

#enum(
  [First item.],
  [Second item.],
  [Third item.],
  [Fourth item.],
  [Fifth item.],
  [Sixth item.],
  [Seventh item.],
  [Eighth item.],
  [Ninth item.],
  [Tenth item — the second digit arrives here, and the marker still sits in the column the single digits held.],
  [Eleventh item.],
)

// Nesting is indent-driven here, the same as the unordered list above; this
// file's `set enum(numbering: "1.")` applies at every depth, so the sub-items
// below count 1, 2 rather than switching to lower alpha the way the
// stylesheet's `ol ol` selector does.
+ A first-level item, followed by sub-items:
  + First sub-item.
  + Second sub-item.

#list(tight: true,
  [11pt body],
  [9.5pt table],
  [8pt caption label],
)

/ Measure: The length of a line of type, counted in characters rather than in
  millimetres.
/ Leading: The vertical distance between baselines. Named for the strips of lead
  once inserted between lines of metal type.

16 — Blocks

Tables

Rules, not grids. Vertical rules are almost never needed — the columns already read as columns, and every line you add is ink competing with data. Three horizontal rules: above the header, below it, and below the body.

Numbers align right, on tabular figures, and units go in the header rather than being repeated in every cell. Sans-serif at 9.5pt: a table is scanned, not read, and the serif's job is reading.

A table that spans pages repeats its header on every one. Without that, page two of a table is unreadable — this is a requirement, not a nicety.

1 · Table

Sans at 9.5pt, tabular figures, and rules rather than a grid: one above the header, one below it, one below the last row of the body — never a vertical rule between columns.

TypefaceCategory
EB GaramondSerif text face
Source Sans 3Sans display face

2 · Header cell

Uppercase, tracked, and muted — a header cell reads as a label for the column beneath it, not as another row of data.

SettingValue
Body size11pt

3 · Body cell

Baseline-aligned and flush left, with a hairline rule under every row but the last, which carries the heavier rule that closes the table.

TypefaceCategory
EB GaramondSerif text face
Source Sans 3Sans display face
IBM Plex MonoMonospace face

4 · Numeric cell

Right-aligned, on tabular figures, so a column of numbers lines up on its own right edge instead of trailing off like prose.

TypefaceSize (pt)
EB Garamond11.0
Source Serif10.5
Charis SIL10.0

5 · Footer cell

Bold, with its own rule dropped — the row above it already carries the table's closing rule, so a second one directly beneath would only double it.

TypefaceSize (pt)
EB Garamond11.0
Source Serif10.5
Median10.75

6 · Caption

Above the table, not below — a reader meets the table's subject before the table itself, and the caption keeps the body's own measure rather than shrinking to the table's width.

Setting widths for a single-column A4 page, by point size.
TypefaceMeasure (mm)
EB Garamond128
Source Serif124

7 · Caption label

Small caps and tracking mark "Table 1" itself as a label, distinct from the sentence describing the table that follows it.

Table 1 — Setting widths for a single-column A4 page, by point size.
TypefaceMeasure (mm)
EB Garamond128
Source Serif124

8 · Zebra striping

A wash on every even body row — reached for on a wide, dense table only, not a default worth applying here.

TypefaceSize (pt)
EB Garamond11.0
Source Serif10.5
Charis SIL10.0
IBM Plex Serif10.5
  • Rules, not grids. Vertical rules are almost never needed — the columns already read as columns, and every added line is ink competing with data.
  • Three horizontal rules only: above the header, below the header, below the body.
  • A table is scanned, not read. Sans at 9.5pt; the serif's job is reading.
  • Numbers align on their right edge, on tabular figures. Units go in the header, not in every cell.
  • A table that spans pages MUST repeat its header on each. Without it, page two of a table is unreadable.

Table

fontsans
size9.5pt
line height1.35
numeralslining tabular
widthfull measure
space before13.75pt
space after13.75pt
border collapseyes

Header cell

fontsans
weight600
size8pt
caseuppercase
tracking0.07em
colorink_muted
alignleft
border top1pt rule_strong
border bottom1pt rule_strong
padding0.45em 0.7em 0.45em 0
repeats across pagesyes

Body cell

alignleft
vertical alignbaseline
border bottom0.5pt rule
padding0.45em 0.7em 0.45em 0
last row border bottom1pt rule_strong

Numeric cell

alignright
numeralslining tabular

Footer cell

weight600
border bottomnone

Caption

positionabove the table
alignleft
fontsans
size9.5pt
colorink
space after0.5em
max widthnone

Row

break insideavoid

Zebra striping opt-in · ts-table-zebra

backgroundwash on even body rows

For wide, dense tables only. Not a default.

Caption label

weight600
capsall small caps
tracking0.06em

Full rule in typeset.css — table

<!-- Table -->
<table>
  <thead>
    <tr><th>Typeface</th><th>Category</th></tr>
  </thead>
  <tbody>
    <tr><td>EB Garamond</td><td>Serif text face</td></tr>
    <tr><td>Source Sans 3</td><td>Sans display face</td></tr>
  </tbody>
</table>

<!-- Header cell -->
<table>
  <thead>
    <tr><th>Setting</th><th>Value</th></tr>
  </thead>
  <tbody>
    <tr><td>Body size</td><td>11pt</td></tr>
  </tbody>
</table>

<!-- Body cell -->
<table>
  <thead><tr><th>Typeface</th><th>Category</th></tr></thead>
  <tbody>
    <tr><td>EB Garamond</td><td>Serif text face</td></tr>
    <tr><td>Source Sans 3</td><td>Sans display face</td></tr>
    <tr><td>IBM Plex Mono</td><td>Monospace face</td></tr>
  </tbody>
</table>

<!-- Numeric cell -->
<table>
  <thead><tr><th>Typeface</th><th class="ts-table-cell-numeric">Size (pt)</th></tr></thead>
  <tbody>
    <tr><td>EB Garamond</td><td class="ts-table-cell-numeric">11.0</td></tr>
    <tr><td>Source Serif</td><td class="ts-table-cell-numeric">10.5</td></tr>
    <tr><td>Charis SIL</td><td class="ts-table-cell-numeric">10.0</td></tr>
  </tbody>
</table>

<!-- Footer cell -->
<table>
  <thead><tr><th>Typeface</th><th class="ts-table-cell-numeric">Size (pt)</th></tr></thead>
  <tbody>
    <tr><td>EB Garamond</td><td class="ts-table-cell-numeric">11.0</td></tr>
    <tr><td>Source Serif</td><td class="ts-table-cell-numeric">10.5</td></tr>
  </tbody>
  <tfoot>
    <tr><td>Median</td><td class="ts-table-cell-numeric">10.75</td></tr>
  </tfoot>
</table>

<!-- Caption -->
<table>
  <caption>Setting widths for a single-column A4 page, by point size.</caption>
  <thead><tr><th>Typeface</th><th class="ts-table-cell-numeric">Measure (mm)</th></tr></thead>
  <tbody>
    <tr><td>EB Garamond</td><td class="ts-table-cell-numeric">128</td></tr>
    <tr><td>Source Serif</td><td class="ts-table-cell-numeric">124</td></tr>
  </tbody>
</table>

<!-- Caption label -->
<table>
  <caption><span class="ts-label ts-table-caption-label">Table 1</span> — Setting widths for a single-column A4 page, by point size.</caption>
  <thead><tr><th>Typeface</th><th class="ts-table-cell-numeric">Measure (mm)</th></tr></thead>
  <tbody>
    <tr><td>EB Garamond</td><td class="ts-table-cell-numeric">128</td></tr>
    <tr><td>Source Serif</td><td class="ts-table-cell-numeric">124</td></tr>
  </tbody>
</table>

<!-- Zebra striping -->
<table class="ts-table-zebra">
  <thead><tr><th>Typeface</th><th class="ts-table-cell-numeric">Size (pt)</th></tr></thead>
  <tbody>
    <tr><td>EB Garamond</td><td class="ts-table-cell-numeric">11.0</td></tr>
    <tr><td>Source Serif</td><td class="ts-table-cell-numeric">10.5</td></tr>
    <tr><td>Charis SIL</td><td class="ts-table-cell-numeric">10.0</td></tr>
    <tr><td>IBM Plex Serif</td><td class="ts-table-cell-numeric">10.5</td></tr>
  </tbody>
</table>

Full implementation in typeset.typ — table

#ts-table(
  columns: (1fr, 1fr),
  rows: 2,
  table.header([Typeface], [Category]),
  [EB Garamond], [Serif text face],
  [Source Sans 3], [Sans display face],
)

#ts-table(
  columns: (1fr, 1fr),
  rows: 1,
  table.header([Setting], [Value]),
  [Body size], [11pt],
)

#ts-table(
  columns: (1fr, 1fr),
  rows: 3,
  table.header([Typeface], [Category]),
  [EB Garamond], [Serif text face],
  [Source Sans 3], [Sans display face],
  [IBM Plex Mono], [Monospace face],
)

#ts-table(
  columns: (1fr, auto),
  rows: 3,
  align: (left + horizon, right + horizon),
  table.header([Typeface], [Size (pt)]),
  [EB Garamond], [11.0],
  [Source Serif], [10.5],
  [Charis SIL], [10.0],
)

// table-footer-cell drops its own bottom rule in the stylesheet; ts-table
// draws a rule under every row but the header and the last, so the closing
// rule below falls on this footer row regardless — close enough to the
// spec's own look that a second, hand-written stroke override isn't worth
// the row it would cost this file's readability.
#ts-table(
  columns: (1fr, auto),
  rows: 3,
  align: (left + horizon, right + horizon),
  table.header([Typeface], [Size (pt)]),
  [EB Garamond], [11.0],
  [Source Serif], [10.5],
  [#strong[Median]], [#strong[10.75]],
)

// One figure() call stands for both table-caption and table-caption-label:
// the show rule at "@s figure" always renders the supplement and counter
// ("Table 1") in the label's own small caps ahead of the caption body, so
// Typst has no way to produce a caption without that label already applied.
#figure(
  caption: [Setting widths for a single-column A4 page, by point size.],
  kind: table,
  supplement: [Table],
  ts-table(
    columns: (1fr, auto),
    rows: 2,
    align: (left + horizon, right + horizon),
    table.header([Typeface], [Measure (mm)]),
    [EB Garamond], [128],
    [Source Serif], [124],
  ),
)

// table-row is not shown: its one property, break-inside: avoid, only ever
// changes what happens at a page boundary, which a screen-rendered document
// never reaches — see tools/check.mjs's EXEMPTIONS entry for it.

// No zebra parameter on ts-table — the stylesheet reaches an even body row
// by its position in the DOM, which a Typst table has no equivalent
// selector for, so the fill is set cell by cell instead.
#ts-table(
  columns: (1fr, auto),
  rows: 4,
  align: (left + horizon, right + horizon),
  table.header([Typeface], [Size (pt)]),
  [EB Garamond], [11.0],
  table.cell(fill: wash)[Source Serif], table.cell(fill: wash)[10.5],
  [Charis SIL], [10.0],
  table.cell(fill: wash)[IBM Plex Serif], table.cell(fill: wash)[10.5],
)

17 — Blocks

Code

Monospaced faces run optically large beside a serif, so inline code is pulled back to 0.86em and ligatures are switched off — an arrow rendered as a single glyph is charming in an editor and wrong in a document that quotes source.

Paper cannot scroll, so blocks wrap rather than overflow. And long listings must be allowed to split across pages: forbidding the break on a fifty-line block pushes a blank page ahead of it.

How it must look

The declaration is a single line — set font-variant-numeric on the table and every cell inherits it:

.typeset table {
  font-variant-numeric: lining-nums tabular-nums;
}

/* Repeat the header on every page the table spans. */
.typeset thead { display: table-header-group; }

Which is all it takes.

Inline code

Inline code

fontmono
size0.86em
ligaturesdisabled
backgroundwash
padding0.1em 0.28em
radius2pt
wrapmay break within a word

The 0.86em corrects for the monospace face running optically large beside the serif.

Ligatures off: an arrow rendered as one glyph is charming in an editor and wrong in a document that quotes source.

Code blocks

  • Paper cannot scroll. Lines wrap; they are never clipped.
  • A long listing MUST be allowed to split across pages. Forbidding the break pushes a blank page ahead of it.

Code block

fontmono
size8.5pt
line height1.45
backgroundwash
border left2pt rule_strong
padding0.8em 1em
space before13.75pt
space after13.75pt
wrapsoft-wrap long lines
tab size2
break insideavoid on screen, allowed in print

Full rules in typeset.css — code-inline, codeblock

<p>The declaration is a single line — set <code>font-variant-numeric</code> on the table and every cell inherits it:</p>
<pre><code>.typeset table {
  font-variant-numeric: lining-nums tabular-nums;
}

/* Repeat the header on every page the table spans. */
.typeset thead { display: table-header-group; }</code></pre>
<p>Which is all it takes.</p>

Full implementation in typeset.typ — codeblock

// Typst colours a raw block by its language. typeset.css sets code in a single
// ink, so the CSS implementation renders this same sample monochrome.

The declaration is a single line — set `font-variant-numeric` on the table and
every cell inherits it:

```css
.typeset table {
  font-variant-numeric: lining-nums tabular-nums;
}

/* Repeat the header on every page the table spans. */
.typeset thead { display: table-header-group; }
```

Which is all it takes.

18 — Blocks

Figures & captions

A caption is not a title. It goes below the figure, sans at 9.5pt, separated by a hair rule, and it carries a small-caps label so a cross-reference in the text has something to point at. A figure and its caption stay on one page — the one place a no-break rule is unambiguously right.

1 · Figure

A figure and its caption stay on one page — the one place a no-break rule is unambiguously right.

A figure and its caption never split across a page break.

2 · Figure caption

Below the figure, sans at 9.5pt, muted, set off by a hair rule — a caption reads as apparatus, not as more body text.

characters per line 55–75 30 120
Reading comfort against line length. The plateau between 55 and 75 characters is why the measure, not the font size, is the first decision.

3 · Figure caption label

Set apart from the caption prose that follows it: small caps, tracked wider, one weight heavier — so a cross-reference in the text has something to point at.

Figure 1 — The label that a cross-reference in the text points back to.
  • A caption is not a title: it goes below the figure.
  • A figure and its caption stay on one page. This is the one place a no-break rule is unambiguously right.
  • The caption carries a small-caps label so a cross-reference in the text has something to point at.

Figure

space before16.5pt
space after16.5pt
break insideavoid
image max widthfull measure

Figure caption

positionbelow the figure
fontsans
size9.5pt
line height1.4
colorink_muted
alignleft
space before0.5em
border top0.5pt rule
padding top0.4em

Figure caption label

weight600
colorink
capsall small caps
tracking0.06em

Full rule in typeset.css — figure

<!-- Figure -->
<figure>
  <svg viewBox="0 0 300 120" role="img" aria-label="A placeholder figure, framed and crossed like an image proof.">
    <rect x="1" y="1" width="298" height="118" fill="none" stroke="var(--ts-rule-strong)" stroke-width="1"/>
    <line x1="1" y1="1" x2="299" y2="119" stroke="var(--ts-rule)" stroke-width="1"/>
    <line x1="299" y1="1" x2="1" y2="119" stroke="var(--ts-rule)" stroke-width="1"/>
  </svg>
  <figcaption>A figure and its caption never split across a page break.</figcaption>
</figure>

<!-- Figure caption -->
<figure>
  <svg viewBox="0 0 400 150" role="img" aria-label="Characters per line plotted against reading comfort, peaking between 55 and 75.">
    <line x1="30" y1="120" x2="380" y2="120" stroke="var(--ts-rule-strong)" stroke-width="1"/>
    <line x1="30" y1="120" x2="30" y2="15" stroke="var(--ts-rule-strong)" stroke-width="1"/>
    <path d="M40 112 C 90 100, 120 40, 175 32 C 230 26, 260 34, 300 62 C 335 88, 355 106, 375 114" fill="none" stroke="var(--ts-accent)" stroke-width="1.6"/>
    <rect x="150" y="20" width="90" height="100" fill="var(--ts-accent)" opacity="0.07"/>
    <text x="195" y="138" text-anchor="middle" font-family="Source Sans 3, sans-serif" font-size="9" fill="var(--ts-ink-muted)">characters per line</text>
    <text x="195" y="14" text-anchor="middle" font-family="Source Sans 3, sans-serif" font-size="9" fill="var(--ts-accent)">55–75</text>
    <text x="40" y="132" font-family="IBM Plex Mono, monospace" font-size="8" fill="var(--ts-ink-faint)">30</text>
    <text x="360" y="132" font-family="IBM Plex Mono, monospace" font-size="8" fill="var(--ts-ink-faint)">120</text>
  </svg>
  <figcaption>Reading comfort against line length. The plateau between 55 and 75 characters is why the measure, not the font size, is the first decision.</figcaption>
</figure>

<!-- Figure caption label -->
<figure>
  <svg viewBox="0 0 300 120" role="img" aria-label="A second placeholder figure.">
    <rect x="1" y="1" width="298" height="118" fill="none" stroke="var(--ts-rule-strong)" stroke-width="1"/>
    <line x1="1" y1="1" x2="299" y2="119" stroke="var(--ts-rule)" stroke-width="1"/>
    <line x1="299" y1="1" x2="1" y2="119" stroke="var(--ts-rule)" stroke-width="1"/>
  </svg>
  <figcaption><span class="ts-label ts-figure-caption-label">Figure 1</span> — The label that a cross-reference in the text points back to.</figcaption>
</figure>

Full implementation in typeset.typ — figure

#let s = measure-standard / 400

// A placeholder standing in for an image, the same crossed box the HTML file
// draws in SVG — the pane it belongs to is about the figure block and its
// caption, not about what is in the frame.
#figure(
  caption: [A figure and its caption never split across a page break.],
  block(width: 300 * s, height: 120 * s, {
    place(rect(width: 300 * s, height: 120 * s, stroke: 0.75pt + rule-strong))
    place(line(start: (0pt, 0pt), end: (300 * s, 120 * s), stroke: 0.75pt + rule-color))
    place(line(start: (300 * s, 0pt), end: (0pt, 120 * s), stroke: 0.75pt + rule-color))
  }),
)

#figure(
  caption: [
    Reading comfort against line length. The plateau between 55 and 75
    characters is why the measure, not the font size, is the first decision.
  ],
  block(width: 400 * s, height: 150 * s, {
    place(dx: 150 * s, dy: 20 * s,
      rect(width: 90 * s, height: 100 * s, stroke: none, fill: accent.transparentize(93%)))
    place(dx: 30 * s, dy: 120 * s, line(length: 350 * s, stroke: 0.75pt + rule-strong))
    place(dx: 30 * s, dy: 15 * s, line(angle: 90deg, length: 105 * s, stroke: 0.75pt + rule-strong))
    place(curve(
      stroke: 1.2pt + accent,
      curve.move((40 * s, 112 * s)),
      curve.cubic((90 * s, 100 * s), (120 * s, 40 * s), (175 * s, 32 * s)),
      curve.cubic((230 * s, 26 * s), (260 * s, 34 * s), (300 * s, 62 * s)),
      curve.cubic((335 * s, 88 * s), (355 * s, 106 * s), (375 * s, 114 * s)),
    ))
    place(dx: 145 * s, dy: 128 * s,
      box(width: 100 * s, align(center, text(font: sans, size: xs, fill: ink-muted)[characters per line])))
    place(dx: 145 * s, dy: 4 * s,
      box(width: 100 * s, align(center, text(font: sans, size: xs, fill: accent)[55–75])))
    place(dx: 40 * s, dy: 124 * s, text(font: mono, size: 7pt, fill: ink-faint)[30])
    place(dx: 358 * s, dy: 124 * s, text(font: mono, size: 7pt, fill: ink-faint)[120])
  }),
)

// The third figure, for figure-caption-label. In the HTML file that pane is
// the first one that writes a label into its caption by hand; here every
// caption already carries one, because the show rule at "@s figure" renders
// the supplement and the counter — "FIGURE 3" — in the label's small caps
// ahead of every caption body, and a Typst figure cannot be set without it.
#figure(
  caption: [The label that a cross-reference in the text points back to.],
  block(width: 300 * s, height: 120 * s, {
    place(rect(width: 300 * s, height: 120 * s, stroke: 0.75pt + rule-strong))
    place(line(start: (0pt, 0pt), end: (300 * s, 120 * s), stroke: 0.75pt + rule-color))
    place(line(start: (300 * s, 0pt), end: (0pt, 120 * s), stroke: 0.75pt + rule-color))
  }),
)

19 — Blocks

Callouts

For a report or a technical note: an aside the reader may skip without losing the argument. If skipping it would lose the argument, it is not a callout, it is a paragraph. Two variants only — neutral and warning — because a document with five callout colours has none.

1 · Callout

Serif, on a wash, with a rule down the left edge — set apart from the argument without shouting. An aside the reader may skip without losing the thread.

Point sizes assume the document is printed at 100%. “Fit to page” scaling in a print dialog silently invalidates every measurement in this stylesheet.

2 · Callout title

Sans, small, uppercase, tracked wide, muted — a label for the callout below it, not a heading competing with the document's own.

Note

Fonts must be embedded in the exported PDF, or the recipient's own installed fonts silently take over.

3 · Callout — warning

The same shape, in the accent colour — one variant only, alongside the neutral one, because a document with five callout colours has none.

Before you send it

Check that fonts are embedded in the PDF. A document that falls back to Times on the recipient's machine has lost every decision above.

  • A callout is an aside the reader may skip without losing the argument. If skipping it would lose the argument, it is a paragraph.
  • Two variants only. A document with five callout colours has none.

Callout

fontserif
size10pt
backgroundwash
border0.5pt rule
border left2.5pt rule_strong
padding0.75em 1em
space before13.75pt
space after13.75pt
break insideavoid

Callout title

fontsans
weight700
size8pt
caseuppercase
tracking0.09em
colorink_muted
space after0.35em

Callout — warning

border left2.5pt accent
title coloraccent

Full rule in typeset.css — callout

<!-- Callout -->
<div class="ts-callout">
  <p>Point sizes assume the document is printed at 100%. “Fit to page” scaling in a print dialog silently invalidates every measurement in this stylesheet.</p>
</div>

<!-- Callout title -->
<div class="ts-callout">
  <p class="ts-callout-title">Note</p>
  <p>Fonts must be embedded in the exported PDF, or the recipient's own installed fonts silently take over.</p>
</div>

<!-- Callout — warning -->
<div class="ts-callout ts-callout-warning">
  <p class="ts-callout-title">Before you send it</p>
  <p>Check that fonts are embedded in the PDF. A document that falls back to Times on the recipient's machine has lost every decision above.</p>
</div>

Full implementation in typeset.typ — callout

// title: none is the default — a callout does not require one.
#callout[
  Point sizes assume the document is printed at 100%. “Fit to page” scaling in a
  print dialog silently invalidates every measurement in this stylesheet.
]

#callout(title: [Note])[
  Fonts must be embedded in the exported PDF, or the recipient's own installed
  fonts silently take over.
]

#callout(title: [Before you send it], warning: true)[
  Check that fonts are embedded in the PDF. A document that falls back to Times
  on the recipient's machine has lost every decision above.
]

20 — Blocks

Section breaks

A blank line cannot survive a page break. If the break happens to fall exactly on the gap, the reader never learns that the scene changed — and this is not a rare accident, it is a one-in-forty-lines certainty over a long document. Use a visible mark.

Four registers: three asterisks for prose, an asterism for a heavier division, a fleuron for a decorative one, a plain rule for a report.

1 · Break — three asterisks

The default, for a change of scene in prose — three widely spaced asterisks, centred.

…and so the first argument closes, having established rather less than it set out to.


The second begins somewhere else entirely.

2 · Break — asterism

A heavier division: a change of part, not of scene.

The first part closes here, its argument made.


The second part opens somewhere the first could not reach.

3 · Break — fleuron

Where the document can afford ornament.

The chapter ends on its own quiet note.


And the next opens with a small flourish rather than a bare gap.

4 · Break — rule

Where it cannot. Reports.

The section ends here, its findings stated.


The next section begins without ornament, only the rule to mark the gap.

  • A blank line cannot survive a page break: if the break falls on the gap, the reader never learns the scene changed. Over a long document this is a certainty, not an accident. The mark MUST be visible.
  • Four registers, one per kind of division.

Break — three asterisks

content* * *
size10pt
tracking0.6em
colorink_faint
aligncentre
space before16.5pt
space after16.5pt
break afteravoid

The default, for a change of scene in prose.

Break — asterism

content⁂
size14pt
colorink_faint
aligncentre

A heavier division: a change of part.

Break — fleuron

content❦
size12pt
coloraccent
aligncentre

Where the document can afford ornament.

Break — rule

border top0.5pt rule
contentnone

Where it cannot. Reports.

Full rule in typeset.css — break

<!-- Break — three asterisks -->
<p>…and so the first argument closes, having established rather less than it set out to.</p>
<hr>
<p>The second begins somewhere else entirely.</p>

<!-- Break — asterism -->
<p>The first part closes here, its argument made.</p>
<hr class="ts-break-asterism">
<p>The second part opens somewhere the first could not reach.</p>

<!-- Break — fleuron -->
<p>The chapter ends on its own quiet note.</p>
<hr class="ts-break-fleuron">
<p>And the next opens with a small flourish rather than a bare gap.</p>

<!-- Break — rule -->
<p>The section ends here, its findings stated.</p>
<hr class="ts-break-rule">
<p>The next section begins without ornament, only the rule to mark the gap.</p>

Full implementation in typeset.typ — break

…and so the first argument closes, having established rather less than it set
out to.

#break-asterisks()

The second begins somewhere else entirely.

The first part closes here, its argument made.

#break-asterism()

The second part opens somewhere the first could not reach.

The chapter ends on its own quiet note.

#break-fleuron()

And the next opens with a small flourish rather than a bare gap.

The section ends here, its findings stated.

#break-rule()

The next section begins without ornament, only the rule to mark the gap.

21 — Apparatus

Footnotes & sidenotes

The honest position: true bottom-of-page footnotes need an engine that can measure the page while laying it out. Typst and LaTeX do this natively. WeasyPrint 53+ and Prince implement the CSS footnote model. No browser does. So the note text lives in the document inside .ts-note-footnote, and @supports (float: footnote) promotes it to a real footnote where the engine can — degrading to a numbered notes block where it cannot, never to a lost note.

There is no heading over the notes. The rule above the block is the only mark it needs; a label over two lines of small print is heavier than the thing it labels.

Sidenotes are the better default where the page can afford the margin: no page-break arithmetic, they work in every renderer, and the reader never leaves the sentence they are in. They need the full page width to make sense, so the second example below is a whole A4 sheet at 42% rather than a fragment.

1 · Reference mark

Raised, without brackets, and without opening up the line's own leading — the mark must not stretch the line it sits in.

Bringhurst puts the ideal measure between 45 and 75 characters, a range narrow enough to be useful and wide enough to survive a change of typeface.

2 · Footnote

The note text lives in the document, marked .ts-note-footnote. Where the engine implements the CSS footnote model, that promotes it to the foot of the page carrying the mark; no browser does, so what a screen shows is the fallback below — the same numbered block either way.

The lower bound matters more in a two-column layout, where the column is the measure and there is no slack to give away.

  1. Below about 40 characters, hyphenation stops being optional.

3 · Endnotes block

No heading. The rule above the list and the numbered items already read as a notes area, and a label over two lines of small print is heavier than the thing it labels.

Bringhurst puts the ideal measure between 45 and 75 characters, a range narrow enough to be useful and wide enough to survive a change of typeface. The lower bound matters more in a two-column layout.

  1. Robert Bringhurst, The Elements of Typographic Style, 4th ed., §2.1.2.
  2. Below about 40 characters, hyphenation stops being optional.
  • Bottom-of-page footnotes require an engine that can measure the page while laying it out. Typst and LaTeX do this natively. WeasyPrint 53+ and Prince implement the CSS footnote model. No browser does.
  • Where footnotes are unavailable the note text MUST still live in the document, degrading to a numbered endnotes block — never to a lost note.
  • Sidenotes are the better default where the page can afford the margin: no page-break arithmetic, and the reader never leaves the sentence they are in.
  • The reference mark must not disturb the leading of the line it sits on.

Reference mark

contentthe note number
size0.7em
baseline shift+0.45em
line height0
coloraccent
numeralslining
decorationnone
bracketsnone
affects leadingno

Footnote

positionfoot of the page carrying the mark
fontserif
size9.5pt
line height1.35
separatora rule above the notes area

Where unsupported — an endnotes block at the end of the document

Endnotes block

fontserif
size9.5pt
line height1.4
alignleft
space before22pt
border top0.5pt rule
padding top11pt
list indent1.8em
item space after0.4em
headingnone — the rule above the block is the only mark it needs

No 'NOTES' label. In a one-page document the rule plus the numbered list already reads as a notes area, and a heading over two lines of small print is heavier than the thing it labels.

Sidenote opt-in · typeset--sidenotes

fontsans
size8pt
line height1.4
colorink_muted
alignleft
width11em
positionthe right margin, 2em past the text column's own right edge
vertical offset-0.3em from the marker's line
marker prefixthe note number in accent, weight 600
measure when active27em
reserved margin14em — the gap plus the note, with an em to spare

Anchor the note to the text column's right edge, not to the measure. The measure is a maximum: where the reserved margin is the binding constraint the column is narrower than it, and a note offset from the measure lands on top of the text.

Sidenotes are positioned, not flowed: a long note followed closely by another will overlap it. Keep each note short and consecutive notes apart — one per paragraph, and not in every paragraph. An implementation cannot fix this, and should not pretend to; it is a constraint on the writing.

Full rule in typeset.css — note

<!-- Reference mark -->
<p>Bringhurst puts the ideal measure between 45 and 75 characters,<a class="ts-note-marker" href="#n1"></a> a range narrow enough to be useful and wide enough to survive a change of typeface.</p>

<!-- Footnote -->
<p>The lower bound matters more in a two-column layout, where the column is the measure and there is no slack to give away.<a class="ts-note-marker" href="#n1"></a></p>
<div class="ts-note-endnotes">
  <ol>
    <li class="ts-note-footnote" id="n1">Below about 40 characters, hyphenation stops being optional.</li>
  </ol>
</div>

<!-- Endnotes block -->
<p>Bringhurst puts the ideal measure between 45 and 75 characters,<a class="ts-note-marker" href="#n1"></a> a range narrow enough to be useful and wide enough to survive a change of typeface. The lower bound matters more in a two-column layout.<a class="ts-note-marker" href="#n2"></a></p>
<div class="ts-note-endnotes">
  <ol>
    <li id="n1">Robert Bringhurst, <cite>The Elements of Typographic Style</cite>, 4th ed., §2.1.2.</li>
    <li id="n2">Below about 40 characters, hyphenation stops being optional.</li>
  </ol>
</div>

Full implementation in typeset.typ — note

// Two examples here, one in the CSS implementation. Typst sets footnotes at the
// foot of the page natively; no browser implements the CSS footnote model, so
// the CSS example degrades to the numbered endnote block the spec names as the
// fallback, and its sidenotes are shown as a separate full-page example.
//
// Sidenotes are paid for out of the measure, so the whole document narrows
// rather than one block inside it: the notes sit in the margin the narrowing
// frees, which is a property of the page, not of a container. `sidenotes:
// true` is what reserves that margin — note-sidenote() below refuses to
// place a note without it, rather than land one wherever the ambient measure
// happens to end.
#show: typeset.with(sidenotes: true)

Bringhurst puts the ideal measure between 45 and 75
characters,#footnote[Robert Bringhurst, _The Elements of Typographic Style_,
4th ed., §2.1.2.] a range narrow enough to be useful and wide enough to survive
a change of typeface. The lower bound matters more in a two-column
layout.#footnote[Below about 40 characters, hyphenation stops being optional.]

== The measure of a line

Bringhurst puts the ideal measure between 45 and 75 characters, a range narrow
enough to be useful and wide enough to survive a change of
typeface.#note-sidenote[_Elements_, 4th ed., §2.1.2.]

The lower bound matters more in a two-column layout, where the column is the
measure and there is no slack to give away.#note-sidenote[Below 40 characters,
hyphenation stops being optional.]

What the range really encodes is the number of fixations the eye makes per
line, which depends on the reader, the difficulty of the prose, and the size of
the type relative to the viewing distance.

=== The cost

Sidenotes are paid for in horizontal space: the text column gives up six ems so
the margin can have twelve.#note-sidenote[Which is why they are forbidden in
two columns: no margin is left to give.] On a single-column A4 page that is
affordable, and the margin was going to be white anyway.

The alternative is a footnote, which asks the reader to travel to the foot of
the page and back. A sidenote asks them to glance right. Over forty pages the
difference is not small.

Sidenotes — on a full page

The measure narrows to 27em and the freed margin carries the notes, anchored to the text column's own right edge. The reader glances right instead of travelling to the foot of the page and back. Shown at 85% because a sidenote layout cannot be judged in half a column.

The measure of a line

Bringhurst puts the ideal measure between 45 and 75 characters, a range narrow enough to be useful and wide enough to survive a change of typeface.Elements, 4th ed., §2.1.2.

The lower bound matters more in a two-column layout, where the column is the measure and there is no slack to give away.Below 40 characters, hyphenation stops being optional.

What the range really encodes is the number of fixations the eye makes per line, which depends on the reader, the difficulty of the prose, and the size of the type relative to the viewing distance. It is a heuristic standing in for a measurement nobody takes, and heuristics right within a factor of 1.5 are worth more than measurements nobody takes.

The cost

Sidenotes are paid for in horizontal space: the text column gives up six ems so the margin can have twelve.Which is why they are forbidden in two columns: no margin is left to give. On a single-column A4 page that is affordable, and the margin was going to be white anyway.

The alternative is a footnote, which asks the reader to travel to the foot of the page and back. A sidenote asks them to glance right. Over forty pages the difference is not small.

The rule that follows

Keep each note short, and keep consecutive notes apart.A long note followed by a close one will collide: they are positioned, not flowed. A note is an aside, not a second argument running down the margin.

A4 at 85%. Sidenotes are positioned, not flowed: keep each one short and consecutive ones apart, or they collide.

22 — Apparatus

Table of contents

Dot leaders work anywhere. Page numbers require the engine to resolve a cross-reference after pagination — native in Typst and LaTeX, Paged Media in CSS. Where it cannot, the leaders render and the number is simply absent. That is the correct failure: a table of contents with wrong page numbers is worse than one with none.

The numbers below are written into the demo by hand so you can see the finished shape. In a real document they come from the engine, and nobody types them.

1 · Table of contents

Sans, 9.5pt, left aligned — a register of its own, distinct from the serif document it points into. The page numbers are hand-written here so a browser can show the finished shape; the real rule resolves them after pagination, which is what the Typst tab does.

2 · Entry

No underline — the leader and the folio are the only decoration a contents entry gets.

3 · Entry — level 2

Indented past the level-1 entries above it, so the two levels read as a hierarchy without a second column of numbers.

4 · Dot leader

Fills whatever the title leaves behind, so a short title and a long one both land on a number flush with the right margin. Both entries are lifted straight out of the contents list above.

  • Page numbers require the engine to resolve a cross-reference after pagination. Where it cannot, the entry MUST omit the number rather than print a wrong one — a TOC with wrong numbers is worse than one with none.

Table of contents

fontsans
size9.5pt
alignleft

Entry

space after0.35em
decorationnone
page numberresolved after pagination
page number numeralslining tabular
page number colorink_muted

Where unsupported — leaders render, page numbers are omitted

Entry — level 2

indent left1.5em

Dot leader

leaderdotted 0.5pt rule, colour rule, filling the space between title and number
baseline offset-0.15em

Full rule in typeset.css — toc

<!-- Table of contents -->
<h3>Contents</h3>
<nav class="ts-toc">
  <ol>
    <li><a href="#preface"><span>Preface</span><span class="ts-toc-leader"></span></a></li>
    <li><a href="#composition"><span>Composition</span><span class="ts-toc-leader"></span></a></li>
    <li class="ts-toc-entry-2"><a href="#the-measure-of-a-line"><span>The measure of a line</span><span class="ts-toc-leader"></span></a></li>
    <li class="ts-toc-entry-2"><a href="#justification-and-its-costs"><span>Justification and its costs</span><span class="ts-toc-leader"></span></a></li>
    <li class="ts-toc-entry-2"><a href="#opening-a-chapter"><span>Opening a chapter: the drop cap and the small-caps lede</span><span class="ts-toc-leader"></span></a></li>
    <li><a href="#tabular-matter"><span>Tabular matter</span><span class="ts-toc-leader"></span></a></li>
    <li class="ts-toc-entry-2"><a href="#figures-and-their-captions"><span>Figures and their captions</span><span class="ts-toc-leader"></span></a></li>
    <li><a href="#apparatus"><span>Apparatus</span><span class="ts-toc-leader"></span></a></li>
    <li class="ts-toc-entry-2"><a href="#references"><span>References</span><span class="ts-toc-leader"></span></a></li>
  </ol>
</nav>

<!-- Entry -->
<nav class="ts-toc">
  <ol>
    <li><a href="#apparatus"><span>Apparatus</span><span class="ts-toc-leader"></span></a></li>
  </ol>
</nav>

<!-- Entry — level 2 -->
<nav class="ts-toc">
  <ol>
    <li class="ts-toc-entry-2"><a href="#references"><span>References</span><span class="ts-toc-leader"></span></a></li>
  </ol>
</nav>

<!-- Dot leader -->
<nav class="ts-toc">
  <ol>
    <li><a href="#preface"><span>Preface</span><span class="ts-toc-leader"></span></a></li>
    <li class="ts-toc-entry-2"><a href="#opening-a-chapter"><span>Opening a chapter: the drop cap and the small-caps lede</span><span class="ts-toc-leader"></span></a></li>
  </ol>
</nav>

Full implementation in typeset.typ — toc

// toc() generates the list from the document's own headings, which is why a
// whole document follows it here and only the list appears in the HTML file:
// there, the entries and their page numbers are written by hand, because a
// browser resolves neither. The HTML file's other three panes are entries
// lifted out of this same list — one at level 1, one at level 2, and the
// shortest title beside the longest so the leader has something to fill.
#heading(level: 3, outlined: false)[Contents]

#toc()

= Preface

What follows was set twice, in two implementations, to find out which of its
rules were about type and which were about one engine.

= Composition

The page is the first decision and the typeface is very nearly the last. What
follows is an argument for taking them in that order.

== The measure of a line

Between 45 and 75 characters, and 66 at the centre of the range. Past 75 the
return sweep fails; below 45 the eye fixates too often for the sentence to hold
together.

== Justification and its costs

A clean right edge is bought with uneven word spacing, and the currency is the
hyphenation dictionary. Take both or neither.

== Opening a chapter: the drop cap and the small-caps lede

A drop cap, small caps on the opening words, and nothing else. The signal is
that something has begun, and one signal is enough.

= Tabular matter

Rules, not grids. Three horizontal rules and no vertical ones, because the
columns already read as columns.

== Figures and their captions

The caption goes below the figure, carries a small-caps label, and never leaves
the page its figure is on.

= Apparatus

Footnotes where the engine can set them, endnotes where it cannot, and sidenotes
wherever the page can afford the margin.

== References

Hanging indent, always. The surname is the thing being scanned, so it must be
the leftmost thing on the entry.

23 — Apparatus

Bibliography

Hanging indent, always. The surname is the thing being scanned, so it must be the leftmost thing on the entry — an indented first line puts the one useful token where the eye is not looking.

1 · Heading

Sans, bold, 18pt — the same weight and register as any other top-level heading, so a bibliography reads as a section of the document rather than an appendix bolted on.

References

The entries follow immediately, each hanging-indented so the surname stays the leftmost thing on the line.

2 · Entry

Hanging indent, always. The surname is what the eye is scanning for, so it must be the leftmost token — an indented first line puts it where the eye is not looking.

  1. Bringhurst, Robert. The Elements of Typographic Style. 4th ed. Hartley & Marks, 2012.
  2. Hochuli, Jost. Detail in Typography. Hyphen Press, 2008.
  3. Ruder, Emil. Typographie: A Manual of Design. Niggli, 1967. Still the clearest statement of the case for the grid, and long enough ago to be read without argument.
  4. Tschichold, Jan. The Form of the Book. Hartley & Marks, 1991.
  • Hanging indent, always. The surname is what is being scanned, so it must be the leftmost thing on the entry.
  • The heading is its own element, not an instance of heading-h2, even though it shares h2's scale and weight: the distinct id is what lets it span both columns in the two-column template while an ordinary section heading — heading-h2 through heading-h6 — cannot.

Heading

fontsans
weight600
size18pt
line height1.2
baseline advance21.6pt
tracking-0.01em
colorink
alignleft
space before22pt
space after5.5pt
break afteravoid
break insideavoid
wrapbalanced
sets running headyes

Entry

fontserif
size9.5pt
line height1.4
alignleft
hanging indent1.8em
space after0.55em
markernone
break insideavoid
title styleitalic

Where unsupported — in Typst: every property here except the hanging indent — a wrapped line sits flush with the surname instead of indented past it

Full rule in typeset.css — bibliography

<!-- Heading -->
<h2 class="ts-bibliography-heading">References</h2>
<p>The entries follow immediately, each hanging-indented so the surname stays the leftmost thing on the line.</p>

<!-- Entry -->
<ol class="ts-bibliography-entry">
  <li>Bringhurst, Robert. <cite>The Elements of Typographic Style</cite>. 4th ed. Hartley &amp; Marks, 2012.</li>
  <li>Hochuli, Jost. <cite>Detail in Typography</cite>. Hyphen Press, 2008.</li>
  <li>Ruder, Emil. <cite>Typographie: A Manual of Design</cite>. Niggli, 1967. Still the clearest statement of the case for the grid, and long enough ago to be read without argument.</li>
  <li>Tschichold, Jan. <cite>The Form of the Book</cite>. Hartley &amp; Marks, 1991.</li>
</ol>

Full implementation in typeset.typ — bibliography

// references() sets the heading and the entries together, so the two HTML
// panes — the heading, then the entries under it — are one call here. The
// sentence the first pane runs between them ("The entries follow
// immediately…") has nowhere to go: nothing separates the heading from the
// list this function composes.
#references[
  #reference(
    author: [Bringhurst, Robert],
    title: [The Elements of Typographic Style],
    edition: [4th ed],
    publisher: [Hartley & Marks],
    year: [2012],
  )

  #reference(
    author: [Hochuli, Jost],
    title: [Detail in Typography],
    publisher: [Hyphen Press],
    year: [2008],
  )

  #reference(
    author: [Ruder, Emil],
    title: [Typographie: A Manual of Design],
    publisher: [Niggli],
    year: [1967],
    note: [Still the clearest statement of the case for the grid, and long enough ago to be read without argument.],
  )

  #reference(
    author: [Tschichold, Jan],
    title: [The Form of the Book],
    publisher: [Hartley & Marks],
    year: [1991],
  )
]

24 — Documents

Front matter

Title, subtitle, byline, dateline, abstract — and a colophon to close. The colophon is the note at the back of a book recording how it was made and in what type. It is the one piece of front-and-back matter that is purely for pleasure, and the cheapest thing in this stylesheet to include.

1 · Title block

The whole cluster — title, subtitle, byline and dateline — closes on a hairline rule, with the widest gap in the document below it: whatever follows is a fresh start.

On the Measure of a Line

Why the width of a text column is the first decision, and the font is the last

2 · Subtitle

Light, sans, and wrapped to balance rather than to the column edge — normally set directly under the title, shown alone here for its own type.

On the Measure of a Line

Why the width of a text column is the first decision, and the font is the last

3 · Byline

Small caps at body size, tracked open — the author's name read as data, not as another heading.

On the Measure of a Line

4 · Dateline

The smallest, quietest line in the block — place and date, sized like a caption because that is what it is.

5 · Abstract

Set narrower than the body and one size down, so a reader recognises a summary before reading a word of it — shown here unlabelled, which is all the block itself provides.

Every argument about typefaces is downstream of an argument about column width. This essay makes the case that a document set at the wrong measure cannot be rescued by any choice of face, and that a document set at the right one is nearly indifferent to it.

6 · Abstract label

The caption that names the block: sans, uppercase, tracked open and faint, on a line of its own. It never appears outside an abstract, which is why it is shown inside one — but it is set by eight declarations of its own, and nothing about it is inherited from the paragraph beneath it.

Abstract Every argument about typefaces is downstream of an argument about column width. This essay makes the case that a document set at the wrong measure cannot be rescued by any choice of face, and that a document set at the right one is nearly indifferent to it.

7 · Colophon

Closes the document below a rule of its own — the note at the back of a book recording how it was made, and the cheapest thing in this stylesheet to include.

The claim is not original, and it is not complicated…

Set in EB Garamond, with Source Sans 3 for headings and IBM Plex Mono for code. Composed in HTML and rendered to PDF with Paged.js. A4, 11 on 16 points.
  • A colophon closes the document, recording how it was made and in what type. It is the one piece of matter that is purely for pleasure, and the cheapest thing here to include.

Title block

space after33pt
padding bottom11pt
border bottom0.5pt rule

Subtitle

fontsans
weight300
size14pt
line height1.25
colorink_muted
alignleft
wrapbalanced
space after11pt

Byline

fontserif
size11pt
variantall small caps
tracking0.08em
alignleft
space after0

Dateline

fontsans
size9.5pt
colorink_muted
alignleft
space before0.2em

Abstract

fontserif
size10pt
line height1.45
colorink_muted
alignleft
max width30em
space after22pt

Colophon

fontserif
styleitalic
size9.5pt
colorink_muted
alignleft
max width26em
space before33pt
padding top11pt
border top0.5pt rule
break beforeavoid

Abstract label

displayblock
fontsans
sizexs
weight700
tracking0.1em
capsuppercase
colorink_faint

Full rule in typeset.css — frontmatter

<!-- Title block -->
<div class="ts-frontmatter-title-block">
  <h1>On the Measure of a Line</h1>
  <p class="ts-frontmatter-subtitle">Why the width of a text column is the first decision, and the font is the last</p>
  <p class="ts-frontmatter-byline">Adilson Carvalho</p>
  <p class="ts-frontmatter-dateline">Sydney, 28 August 2026</p>
</div>

<!-- Subtitle -->
<h1>On the Measure of a Line</h1>
<p class="ts-frontmatter-subtitle">Why the width of a text column is the first decision, and the font is the last</p>

<!-- Byline -->
<h1>On the Measure of a Line</h1>
<p class="ts-frontmatter-byline">Adilson Carvalho</p>

<!-- Dateline -->
<p class="ts-frontmatter-byline">Adilson Carvalho</p>
<p class="ts-frontmatter-dateline">Sydney, 28 August 2026</p>

<!-- Abstract -->
<div class="ts-frontmatter-abstract">
  Every argument about typefaces is downstream of an argument about column width. This essay makes the case that a document set at the wrong measure cannot be rescued by any choice of face, and that a document set at the right one is nearly indifferent to it.
</div>

<!-- Abstract label -->
<div class="ts-frontmatter-abstract">
  <span class="ts-label ts-frontmatter-abstract-label">Abstract</span>
  Every argument about typefaces is downstream of an argument about column width. This essay makes the case that a document set at the wrong measure cannot be rescued by any choice of face, and that a document set at the right one is nearly indifferent to it.
</div>

<!-- Colophon -->
<p>The claim is not original, and it is not complicated…</p>
<div class="ts-frontmatter-colophon">
  Set in EB Garamond, with Source Sans 3 for headings and IBM Plex Mono for code. Composed in HTML and rendered to PDF with Paged.js. A4, 11 on 16 points.
</div>

Full implementation in typeset.typ — frontmatter

// frontmatter-subtitle, frontmatter-byline and frontmatter-dateline carry no
// symbol of their own: frontmatter-title-block composes all three itself, by
// name, from the params below — there is no way to call for just one line
// of the block.
#frontmatter-title-block(
  title: [On the Measure of a Line],
  subtitle: [Why the width of a text column is the first decision, and the font is the last],
  author: [Adilson Carvalho],
  place-date: [Sydney, 28 August 2026],
)

// frontmatter-abstract always sets the "ABSTRACT" caption itself, so
// frontmatter-abstract-label has no symbol of its own and — unlike
// letter-address-block's `label:` — no way to be left off. The HTML file
// shows the block both ways, unlabelled and labelled; only the labelled one
// below is reachable from here.
#frontmatter-abstract[
  Every argument about typefaces is downstream of an argument about column
  width. This essay makes the case that a document set at the wrong measure
  cannot be rescued by any choice of face, and that a document set at the right
  one is nearly indifferent to it.
]

The claim is not original, and it is not complicated…

#frontmatter-colophon[
  Set in EB Garamond, with Source Sans 3 for headings and IBM Plex Mono for
  code. Composed and paginated in Typst. A4, 11 on 16 points.
]

25 — Documents

Letter

Set justified, at 12pt, across the full width a 35mm margin leaves — about 66 characters on A4. A letter is read once, closely, by one person, and an even page with a quiet edge serves that reading.

The sender and address blocks are address data, not a masthead: the name line in small caps at body size, the lines below it a step smaller, so the eye finds who before where. Nothing bold, nothing in the sans, nothing larger than the text. A personal letter does not announce itself, and the recipient already knows who writes to them. Omit what you do not use — a personal letter needs no email address and no telephone number.

The letterhead holds the standard margin from the paper's edge. Where the text sits further in — under a wider margin or a centred measure — the addresses and any crest stay out at the standard margin, where an envelope window and a reader's eye expect them.

The signature is the typed name, with room above it to sign. No ruled line: a rule above a name is a form to be filled in, and this is not a form. For the same reason “Enc.” and “P.S.” match the text they introduce exactly — they begin sentences, they do not head sections.

Footnotes belong in a letter as much as in an essay. On a one-page letter the rule and the numbered note at the end are the page foot, so it reads correctly even in an engine that cannot set true footnotes.

1 · Letter page

A letter's own page, beside the default one: 35mm at the head, 35mm at the sides, 35mm at the foot — the standard 20mm margin plus a 15mm inset on every edge, with the text justified at 12pt across the full width that leaves, about 66 characters. The letterhead reaches back out to the standard margin, so the addresses and any crest frame the text. No running head and no folio, which is what the empty head and foot of the right-hand sheet show. The markup beside this pane is the whole of it: the class goes on the document element, and every pane below carries it.

Composition
7
The default page
A letter's page

2 · Sender block

The name line in small caps at body size, the lines beneath it a step smaller — a hierarchy of data, not decoration. Never bold and never larger: that turns a letter into stationery. Leave out what a letter does not need: no email address, no telephone number, unless the correspondence calls for one.

Adilson Carvalho

10 Wentworth Avenue

Surry Hills NSW 2010

Australia

3 · Address block

Line-broken data, not prose — the recipient's address exactly as it would sit on an envelope: the name in small caps at body size, the rest a step smaller, tighter leading than the body, never justified, and the line breaks as authored.

The Registrar
Institute of Typographic Studies
88 Rundle Street
Adelaide SA 5000

4 · Address label

The caption that says whose address this is: sans, uppercase, tracked open and faint, on a line of its own above the block. It never appears outside an address block — but it is set by eight declarations of its own, and the address beneath it shares none of them.

To The Registrar
Institute of Typographic Studies
88 Rundle Street
Adelaide SA 5000

5 · Address column

The sender and recipient blocks together, held at the standard margin from the paper's edge. Where the text sits further in — as it does on a letter's own page, 15mm inside the standard margin — the column reaches out by the difference, so the letterhead stays where an envelope window expects it. Where the text is nearer the paper than the standard margin, the column stays with the text.

From Adilson Carvalho
10 Wentworth Avenue
Surry Hills NSW 2010
To The Registrar
88 Rundle Street
Adelaide SA 5000

The text of the letter begins here, at the page's own margin.

6 · Crest

Optional: a crest or monogram in the top right corner, 20mm wide, held at the standard margin from the paper's right edge by the same rule as the address column. A mark drawn in one ink takes the accent. A letter without one leaves the corner empty.

To The Registrar
88 Rundle Street
Adelaide SA 5000

7 · Date line

Oldstyle figures, flush left — the same numeral convention the rest of the document already sets, not a date format of its own. Three lines of space above it set the letterhead off from the letter.

27 August 2026

8 · Line under the date

A dedication, a feast, a devotion — whatever the writer puts under the date. Italic, muted and a step smaller, on the date's own next line, so the two read as one block.

27 August 2026

St Monica, mother of St Augustine Bishop, ora pro nobis

9 · Salutation

A greeting, set like any other paragraph in the letter — no weight change, no size change, no heading trying to happen — with two lines of room above and below it.

Dear Registrar,

10 · Quotation — in a letter

A prayer, a line, a remembered phrase: indented on both sides, italic and muted, with no rule. A rule is the essay's quotation; a letter sets its quotations apart by indent and colour. Inside a letter, every block quote takes this form.

That was my prayer while I waited.

Forgive me my failure to act, O Lord, and if I miss confession today, I will take that as a warning.

The queue moved at a glacial pace.

11 · Footnote — in a letter

The same marker as the rest of the document's apparatus — superscript, no brackets. On a one-page letter, the rule and the numbered note it sits above are the page foot in every sense but name.

Dear Registrar,

I am writing about the setting of the Institute’s annual report, which arrived this morning and which I read with more attention to its margins than to its contents. I hope that is taken in the spirit intended.

The text column runs to something near a hundred and ten characters. By the second page I had lost my place twice, and by the fourth I had stopped reading and started measuring.

This is a cheap problem to fix, and I would be glad to send the specification I use for my own documents.

  1. Measured on page four, between the outer margins: 168mm at a 10pt body, which is about 112 characters.

12 · Closing

The same plain rhythm as the salutation it echoes — the only difference is the room around it: three lines above, two below, not a style of its own.

Yours sincerely,

13 · Signature block

The typed name, with room above it to sign. No ruled line: a rule above a name is a form to be filled in, and this is not a form.

Yours sincerely,

Adilson Carvalho

14 · Enclosures

Enc. introduces a sentence; it does not head a section, so it carries no small caps and no weight change of its own.

Adilson Carvalho

Enc. typeset.css; two specimen pages

15 · Postscript

A postscript is a sentence that happens to begin with P.S. — the label is not a heading, so it matches the text it introduces exactly. It stands off what precedes it by the signature's own three lines.

Enc. typeset.css; two specimen pages

P.S. The tables were excellent — tabular figures throughout, which is more than most annual reports manage.

  • A letter is set justified, at 12pt, across the full width its margins leave. It is read once, closely, by one person, and an even page with a quiet edge serves that reading.
  • Sender and address blocks are address data, not a masthead. The name line is set in small caps at body size, and the lines below it a step smaller, so the eye finds who before where. Nothing bold, nothing in the sans, nothing larger than the text: a personal letter does not announce itself.
  • Address blocks are line-broken data, not prose: tighter leading, never justified, line breaks as authored.
  • The letterhead — the addresses and any crest — sits the standard margin from the paper's edge, or at the text's own edge where that is nearer the paper. Where the text sits further in, under a wider margin or a centred measure, the letterhead stays out at the standard margin rather than moving in with it.
  • The signature is the typed name with room above it to sign. No ruled line — a rule is a form to be filled in.
  • A letter carries no running head and no folio.
  • Footnotes belong in a letter as much as in an essay. Where the engine cannot set them at the page foot, the rule plus the numbered note at the end of a one-page letter reads as a footnote area regardless.

Letter page

margin top mm35
margin bottom mm35
margin sides mm35
base size12pt
measurefull — the width the margins leave, about 66 characters on A4
justificationjustified
running headnone
folionone
block quoteletter-quote

The text sits letter_inset_mm inside the standard margin on every edge. The letterhead — letter-addresses and letter-crest — reaches back out by the same amount, so the addresses and the crest stand at the standard margin and the text is framed inside them.

At 12pt, the full width inside 35mm margins on A4 carries about 66 characters: the standard measure, reached by the margin rather than by a cap.

Sender block

fontserif
weight400
size12pt
line height1.35
colorink
alignleft
line breaksas authored — name, street, suburb and postcode, country
space after30pt
name linesmall caps (smcp and c2sc), at body size
detail size10.2pt — 0.85 of body

The name is the one line in small caps, at body size; street, suburb and country sit a step smaller beneath it. Never larger and never bold — that turns a letter into stationery.

Omit what you do not use. A personal letter needs no email address and no telephone number.

Address block

fontserif
size12pt
line height1.35
stylenormal
alignleft
line breaksas authored
space after16.5pt
name linesmall caps (smcp and c2sc), at body size
detail size10.2pt — 0.85 of body

Address column

left edgefoundation.page.margins.symmetric_mm.standard from the paper's left edge, or the text's own left edge where that is nearer the paper
reachhow far the text's left edge sits beyond symmetric_mm.standard — from a wider margin, a centred measure or both — or none

Holds the sender and address blocks. The reach is measured from where the text actually starts, so it follows the page margin and the measure together.

Crest

positiontop right of the first page, its top on the top margin
right edgesymmetric_mm.standard from the paper's right edge, or the text's own right edge where that is nearer the paper
width20mm
coloraccent, where the mark is drawn in a single ink

Optional. A letter with no crest leaves the corner empty; nothing moves into it.

Date line

numeralsoldstyle
alignleft
space before36pt
space after24pt

The date and the line under it are one block. The space after belongs to the pair, not to the date alone.

Only the date is required. The line under it, where there is one, is the saint of the day, closed with “ora pro nobis”; with no saint the date stands alone.

Line under the date

fontserif
styleitalic
size10.8pt — 0.9 of body
colorink_muted
alignleft
space beforenone — the date's own next line, at body leading
space after24pt, the date's

A dedication, a feast, a devotion — whatever the writer puts under the date. Italic, at body size, immediately beneath.

Salutation

alignleft
space before24pt
space after24pt
formgreeting, name, comma — “Dear” unless the writer gives another greeting

Quotation — in a letter

fontserif
styleitalic
weight400
size12.6pt — 1.05 of body
colorink_muted
indent left24pt
indent right24pt
rulenone
space before18pt
space after18pt

A letter quotes a prayer, a line, a remembered phrase. It is set apart by indent and colour, not by a rule: a rule is the essay's quotation, and a letter is not an essay.

Closing

alignleft
space before36pt
space after24pt
formclosing words and a comma — “Yours sincerely” unless the writer gives others

Signature block

fontserif
size12pt
alignleft
space before36pt — the room to sign
rulenone
break insideavoid
signature image max height16mm

The typed name only. A ruled line above a name is a form; a letter is not a form.

Enclosures

fontserif
size12pt
alignleft
space before24pt
labelsame style as the text it introduces — no small caps, no weight change

Same reasoning as the postscript: "Enc." introduces a sentence, it does not head a section.

Postscript

fontserif
size12pt
alignleft
space before36pt — the signature's own distance from the closing
labelsame style as the text it introduces — no small caps, no weight change

A postscript is a sentence that happens to begin with "P.S.". The label is not a heading.

Footnote — in a letter

markeras the note marker in Apparatus — superscript, accent, no brackets
positionfoot of the page where the engine can; otherwise after the signature block
fontserif
size9.5pt
separator0.5pt rule above the notes area
headingnone

Where unsupported — a numbered note after the signature, under a rule — which on a one-page letter is the page foot anyway

Address label

displayblock
fontsans
sizexs
tracking0.1em
capsuppercase
colorink_faint

Full rule in typeset.css — letter

<!-- Letter page -->
<div class="typeset typeset--justified typeset--letter" lang="en">
</div>

<!-- Sender block -->
<div class="typeset typeset--justified typeset--letter" lang="en">
  <div class="ts-letter-sender">
    <p>Adilson Carvalho</p>
    <p>10 Wentworth Avenue</p>
    <p>Surry Hills NSW 2010</p>
    <p>Australia</p>
  </div>
</div>

<!-- Address block -->
<div class="typeset typeset--justified typeset--letter" lang="en">
  <address class="ts-letter-address-block">
    The Registrar<br>
    Institute of Typographic Studies<br>
    88 Rundle Street<br>
    Adelaide SA 5000
  </address>
</div>

<!-- Address label -->
<div class="typeset typeset--justified typeset--letter" lang="en">
  <address class="ts-letter-address-block">
    <span class="ts-label ts-letter-address-label">To</span>
    The Registrar<br>
    Institute of Typographic Studies<br>
    88 Rundle Street<br>
    Adelaide SA 5000
  </address>
</div>

<!-- Address column -->
<div class="typeset typeset--justified typeset--letter" lang="en">
  <div class="ts-letter-addresses">
    <address class="ts-letter-address-block">
      <span class="ts-label ts-letter-address-label">From</span>
      Adilson Carvalho<br>
      10 Wentworth Avenue<br>
      Surry Hills NSW 2010
    </address>
    <address class="ts-letter-address-block">
      <span class="ts-label ts-letter-address-label">To</span>
      The Registrar<br>
      88 Rundle Street<br>
      Adelaide SA 5000
    </address>
  </div>
  <p>The text of the letter begins here, at the page's own margin.</p>
</div>

<!-- Crest -->
<div class="typeset typeset--justified typeset--letter" lang="en">
  <div class="ts-letter-crest">
    <svg viewBox="0 0 40 48" role="img" aria-label="Crest">
      <path d="M4 4h32v18c0 12-8 20-16 24C12 42 4 34 4 22z" fill="none" stroke="#7a1f1f" stroke-width="1.5"/>
      <path d="M20 12l3 7h7l-5.5 4.5 2 7-6.5-4.5-6.5 4.5 2-7L10 19h7z" fill="none" stroke="#7a1f1f" stroke-width="1"/>
    </svg>
  </div>
  <address class="ts-letter-address-block">
    <span class="ts-label ts-letter-address-label">To</span>
    The Registrar<br>
    88 Rundle Street<br>
    Adelaide SA 5000
  </address>
</div>

<!-- Date line -->
<div class="typeset typeset--justified typeset--letter" lang="en">
  <p class="ts-letter-date">27 August 2026</p>
</div>

<!-- Line under the date -->
<div class="typeset typeset--justified typeset--letter" lang="en">
  <p class="ts-letter-date">27 August 2026</p>
  <p class="ts-letter-date-note">St Monica, mother of St Augustine Bishop, ora pro nobis</p>
</div>

<!-- Salutation -->
<div class="typeset typeset--justified typeset--letter" lang="en">
  <p class="ts-letter-salutation">Dear Registrar,</p>
</div>

<!-- Quotation — in a letter -->
<div class="typeset typeset--justified typeset--letter" lang="en">
  <p>That was my prayer while I waited.</p>
  <blockquote class="ts-letter-quote">
    <p>Forgive me my failure to act, O Lord, and if I miss confession today, I will take that as a warning.</p>
  </blockquote>
  <p>The queue moved at a glacial pace.</p>
</div>

<!-- Footnote — in a letter -->
<div class="typeset typeset--justified typeset--letter" lang="en">
  <p class="ts-letter-salutation">Dear Registrar,</p>

  <p>I am writing about the setting of the Institute’s annual report, which arrived this morning and which I read with more attention to its margins than to its contents. I hope that is taken in the spirit intended.</p>

  <p>The text column runs to something near a hundred and ten characters.<a class="ts-note-marker" href="#ln1"></a> By the second page I had lost my place twice, and by the fourth I had stopped reading and started measuring.</p>

  <p>This is a cheap problem to fix, and I would be glad to send the specification I use for my own documents.</p>

  <div class="ts-note-endnotes">
    <ol>
      <li id="ln1">Measured on page four, between the outer margins: 168mm at a 10pt body, which is about 112 characters.</li>
    </ol>
  </div>
</div>

<!-- Closing -->
<div class="typeset typeset--justified typeset--letter" lang="en">
  <p class="ts-letter-closing">Yours sincerely,</p>
</div>

<!-- Signature block -->
<div class="typeset typeset--justified typeset--letter" lang="en">
  <p class="ts-letter-closing">Yours sincerely,</p>
  <div class="ts-letter-signature">Adilson Carvalho</div>
</div>

<!-- Enclosures -->
<div class="typeset typeset--justified typeset--letter" lang="en">
  <div class="ts-letter-signature">Adilson Carvalho</div>
  <p class="ts-letter-enclosures">Enc. typeset.css; two specimen pages</p>
</div>

<!-- Postscript -->
<div class="typeset typeset--justified typeset--letter" lang="en">
  <p class="ts-letter-enclosures">Enc. typeset.css; two specimen pages</p>
  <p class="ts-letter-postscript">P.S. The tables were excellent — tabular figures throughout, which is more than most annual reports manage.</p>
</div>

Full implementation in typeset.typ — letter

// letter-page's #show call opens the document itself, in place of the
// boilerplate's own #show: typeset — a letter's page (a deeper top margin,
// no running head, no folio) cannot be set from inside a container, so one
// replaces the other rather than nesting under it.
#show: letter-page

// letter-crest takes the image itself. A crest kept as a file is read from
// the document and recoloured with svg-recolor:
//   #letter-crest(svg-recolor(read("crest.svg"), width: 100%))
#letter-crest(image(bytes(
  "<svg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 40 48'>"
    + "<path d='M4 4h32v18c0 12-8 20-16 24C12 42 4 34 4 22z' fill='none' stroke='#7a1f1f' stroke-width='1.5'/>"
    + "</svg>"
), width: 100%))

// letter-address-label is reached through letter-address-block's `label:`
// parameter; letter-address-from and letter-address-to set it by role. The
// first line of each block is the name, set in small caps; the lines after it
// a step smaller.
#letter-addresses[
  #letter-sender[
    Adilson Carvalho \
    10 Wentworth Avenue \
    Surry Hills NSW 2010 \
    Australia
  ]

  #letter-address-to[
    The Registrar \
    Institute of Typographic Studies \
    88 Rundle Street \
    Adelaide SA 5000
  ]
]

#letter-date([27 August 2026], saint: [St Monica, mother of St Augustine Bishop])

#letter-salutation[Registrar]

I am writing about the setting of the Institute's annual report, which arrived
this morning and which I read with more attention to its margins than to its
contents. I hope that is taken in the spirit intended.

// letter-footnote has no symbol: it is Typst's own footnote, set at the foot
// of the page where the engine can — which on a one-page letter is the
// numbered-note fallback this spec asks for anyway.
The text column runs to something near a hundred and ten
characters.#footnote[Measured on page four, between the outer margins: 168mm at
a 10pt body, which is about 112 characters.] By the second page I had lost my
place twice, and by the fourth I had stopped reading and started measuring.

// Inside letter-page every block quote is a letter-quote.
#quote[Set the column to sixty-six characters, and the margins will look after themselves.]

This is a cheap problem to fix, and I would be glad to send the specification I
use for my own documents.

#letter-closing()

#letter-signature[Adilson Carvalho]

#letter-enclosures[typeset.typ; two specimen pages]

#letter-postscript[The tables were excellent — tabular figures throughout, which
is more than most annual reports manage.]

26 — Documents

Pagination utilities

The small set of overrides reached for while proofing a real document: force a break, forbid one, stop a hyphen, tie two words together. Every one is a manual decision about a specific page, not a style. The last of them tells the output device not to strip backgrounds and rules to save ink.

1 · Break before

Starts this element on a new page — a chapter opening, most often. A screen has one continuous column and no page boundary to show it against, so the boundary is drawn: the marked bar on the right-hand sheet is the heading, opening a page of its own, and the dashed outline on the left is where it would have fallen without the override.

…and that is the last that needs saying about the measure.

Leading

A shorter line needs less separation between baselines for the return sweep to stay unambiguous, which is why the two decisions cannot be taken apart.

4
where the heading would have fallen
5
where it falls instead

2 · Break after

The mirror: whatever follows the marked element starts a new page. Reached for when a block must be the last thing a reader sees on a page — a summary before the evidence, a plate before the discussion of it.

The three rules are the whole of the table style, and the rest follows from them.

In short: rules and not grids, sans at 9.5pt, numbers on their right edge.

The argument for each of the three takes the rest of this chapter.

6
the marked block ends the page
7
what followed it starts here

3 · Keep together

Never lets a page break fall inside this element — a short table, a signature block, a callout. Where the room left on the page is not enough for the whole of it, all of it moves to the next page rather than half of it staying behind.

The measurements are worth reading as one set, so they are held to one page.

MarginMillimetres
Narrow10
Standard20
Wide30
8
room for part of it, so it takes none
9
all of it arrives here, unsplit

4 · No hyphenation

Stops the dictionary from breaking one specific run — a name, a trademark — while everything else in a justified paragraph keeps hyphenating as usual. Disabling hyphenation for a whole justified paragraph is the mistake the justification section shows; this narrows the override to the one word that must never take a hyphen. Whether the first paragraph below breaks the name at all is the browser's decision, and rests on it having a hyphenation dictionary for the page's language; the second must never break it, dictionary or not.

The village of Llanfairpwllgwyngyllgogerychwyrndrobwllllantysiliogogogoch sits on Anglesey, and this justified paragraph is left to hyphenate its name wherever the dictionary allows, the same as any other word here.

The village of Llanfairpwllgwyngyllgogerychwyrndrobwllllantysiliogogogoch sits on Anglesey, and this justified paragraph must never hyphenate that one name, however freely it hyphenates every other word here.

5 · Tie

A non-breaking join between a figure and its unit, a section mark and its number — so a line break can never fall between them, wherever the paragraph happens to wrap.

Keep a figure with its unit: 11 pt, §2.1, Fig. 1 — none of them ever break across a line.

6 · Print only / screen only

A pair: one class hides its element on screen, the other hides it on paper. Exactly one of the two sentences below is present in any medium, and the one you can see here is the screen's.

Every source is linked where it is cited — follow any of them for the full reference.

Every source is listed in full on the last page, because a page has no links to follow.

Printed, the sentence above disappears and this one takes its place: Every source is listed in full on the last page, because a page has no links to follow. Never both, and never neither.

7 · Exact colour

Print dialogs save ink by leaving out background graphics. A zebra table whose bands are left out is not a plainer table — it is a table whose rows have lost the thing that told them apart. This forces every background and rule in the document through to the paper instead.

MarginTopSides
Narrow1010
Standard2020
Wide3030
Letter3225

Printed, the bands and the three rules above arrive on the paper exactly as they are here. Without this rule, a dialog set to leave out background graphics drops all of them, and four rows of numbers are left with nothing telling them apart.

  • The small set of overrides reached for while proofing a real document. Every one of them is a manual decision about a specific page, not a style.

Break before

effectstart this element on a new page

Chapter openings.

Break after

effectstart the next element on a new page

Keep together

effectnever split this element across pages

A short table, a signature block, a callout.

No hyphenation

hyphenationmanual

Tie

effectno line break inside

A figure with its unit, an initial with a surname, a section mark with its number.

Print only / screen only

effectpresent in one medium, absent in the other

The URL list a page needs and a screen does not; the navigation a screen needs and a page does not.

Exact colour

effectbackgrounds and rules survive the output device's ink-saving

Without it a print dialog can strip every rule in a table.

Full rule in typeset.css — utility

<!-- Break before -->
<p>…and that is the last that needs saying about the measure.</p>
<h2 class="ts-utility-break-before">Leading</h2>
<p>A shorter line needs less separation between baselines for the return sweep to stay unambiguous, which is why the two decisions cannot be taken apart.</p>

<!-- Break after -->
<p>The three rules are the whole of the table style, and the rest follows from them.</p>
<div class="ts-frontmatter-abstract ts-utility-break-after">
  In short: rules and not grids, sans at 9.5pt, numbers on their right edge.
</div>
<p>The argument for each of the three takes the rest of this chapter.</p>

<!-- Keep together -->
<p>The measurements are worth reading as one set, so they are held to one page.</p>
<div class="ts-utility-keep-together">
  <table>
    <thead><tr><th>Margin</th><th class="ts-table-cell-numeric">Millimetres</th></tr></thead>
    <tbody>
      <tr><td>Narrow</td><td class="ts-table-cell-numeric">10</td></tr>
      <tr><td>Standard</td><td class="ts-table-cell-numeric">20</td></tr>
      <tr><td>Wide</td><td class="ts-table-cell-numeric">30</td></tr>
    </tbody>
  </table>
</div>

<!-- No hyphenation -->
<div class="typeset typeset--justified typeset--measure-narrow" lang="en">
  <p>The village of Llanfairpwllgwyngyllgogerychwyrndrobwllllantysiliogogogoch sits on Anglesey, and this justified paragraph is left to hyphenate its name wherever the dictionary allows, the same as any other word here.</p>
  <p>The village of <span class="ts-utility-no-hyphens">Llanfairpwllgwyngyllgogerychwyrndrobwllllantysiliogogogoch</span> sits on Anglesey, and this justified paragraph must never hyphenate that one name, however freely it hyphenates every other word here.</p>
</div>

<!-- Tie -->
<p>Keep a figure with its unit: <span class="ts-utility-tie">11 pt</span>, <span class="ts-utility-tie">§2.1</span>, <span class="ts-utility-tie">Fig. 1</span> — none of them ever break across a line.</p>

<!-- Print only / screen only -->
<p class="ts-screen-only">Every source is linked where it is cited — follow any of them for the full reference.</p>
<p class="ts-print-only">Every source is listed in full on the last page, because a page has no links to follow.</p>

<!-- Exact colour -->
<table class="ts-table-zebra">
  <thead><tr><th>Margin</th><th class="ts-table-cell-numeric">Top</th><th class="ts-table-cell-numeric">Sides</th></tr></thead>
  <tbody>
    <tr><td>Narrow</td><td class="ts-table-cell-numeric">10</td><td class="ts-table-cell-numeric">10</td></tr>
    <tr><td>Standard</td><td class="ts-table-cell-numeric">20</td><td class="ts-table-cell-numeric">20</td></tr>
    <tr><td>Wide</td><td class="ts-table-cell-numeric">30</td><td class="ts-table-cell-numeric">30</td></tr>
    <tr><td>Letter</td><td class="ts-table-cell-numeric">32</td><td class="ts-table-cell-numeric">25</td></tr>
  </tbody>
</table>

Full implementation in typeset.typ — utility

// utility-break-before and utility-break-after are not shown here, and cannot
// be: both are Typst's own #pagebreak(), and a pagebreak is refused inside a
// container. typeset() holds the flow to the measure by wrapping the whole
// document in a block(width: …), so every document set under the boilerplate
// the masthead publishes is inside one — `#show: typeset` then `#pagebreak()`
// fails with "pagebreaks are not allowed inside of containers". The same
// document under `typeset.with(measure: none)` compiles, which is the whole
// of the difference. The text below is the two panes the HTML file breaks
// across pages, running on without the break.
…and that is the last that needs saying about the measure.

== Leading

A shorter line needs less separation between baselines for the return sweep to
stay unambiguous, which is why the two decisions cannot be taken apart.

The three rules are the whole of the table style, and the rest follows from
them.

#frontmatter-abstract[
  In short: rules and not grids, sans at 9.5pt, numbers on their right edge.
]

The argument for each of the three takes the rest of this chapter.

The measurements are worth reading as one set, so they are held to one page.

#utility-keep-together[
  #ts-table(
    columns: (1fr, auto),
    rows: 3,
    align: (left + horizon, right + horizon),
    table.header([Margin], [Millimetres]),
    [Narrow], [10],
    [Standard], [20],
    [Wide], [30],
  )
]

#measured(width: measure-narrow)[
  #justified[
    The village of Llanfairpwllgwyngyllgogerychwyrndrobwllllantysiliogogogoch
    sits on Anglesey, and this justified paragraph is left to hyphenate its
    name wherever the dictionary allows, the same as any other word here.
  ]
]

#measured(width: measure-narrow)[
  #justified[
    The village of #text(hyphenate: false)[Llanfairpwllgwyngyllgogerychwyrndrobwllllantysiliogogogoch]
    sits on Anglesey, and this justified paragraph must never hyphenate that
    one name, however freely it hyphenates every other word here.
  ]
]

Keep a figure with its unit: #utility-tie[11 pt], #utility-tie[§2.1],
#utility-tie[Fig. 1] — none of them ever break across a line.

// utility-print-only and utility-color-adjust have no counterpart here
// either, for the opposite reason: both name a difference between two media,
// and Typst composes for one. There is no screen rendering of this document
// for a sentence to be absent from, and no print dialog between the compiler
// and the paper to drop a background on the way — so the two sentences the
// HTML file swaps would both simply be written, and the fills below are in
// the PDF because they were set.
#ts-table(
  columns: (1fr, auto, auto),
  rows: 4,
  align: (left + horizon, right + horizon, right + horizon),
  table.header([Margin], [Top], [Sides]),
  [Narrow], [10], [10],
  table.cell(fill: wash)[Standard], table.cell(fill: wash)[20], table.cell(fill: wash)[20],
  [Wide], [30], [30],
  table.cell(fill: wash)[Letter], table.cell(fill: wash)[32], table.cell(fill: wash)[25],
)