# Numbered chorale examples in a two-column harmony method

> Four-part chorales written in Humdrum **kern become score resources typed example: numbered Example 4.1, cited with :ref and floated like figures.

- HTML version: https://postext.dev/en/cookbook/conservatory-method-chorales
- Recipe Nº 166 · Figures & images · Level 2 (Intermediate) · Outputs: Canvas
- Genres: Textbooks
- Requires postext ≥ 1.54.0 · tested with 1.54.0 on 2026-10-22
- Pages: [61](https://postext.dev/cookbook/conservatory-method-chorales/en/p01.webp?v=892c2a86), [62](https://postext.dev/cookbook/conservatory-method-chorales/en/p02.webp?v=892c2a86), [63](https://postext.dev/cookbook/conservatory-method-chorales/en/p03.webp?v=892c2a86)
- Open in Sandbox: https://postext.dev/en/sandbox#recipe=conservatory-method-chorales&lang=en (.postext: https://postext.dev/cookbook/conservatory-method-chorales/en/conservatory-method-chorales.postext)
- Last updated: 2026-10-22
- Other languages: [es](https://postext.dev/es/cookbook/conservatory-method-chorales.md), [ca](https://postext.dev/ca/cookbook/conservatory-method-chorales.md), [de](https://postext.dev/de/cookbook/conservatory-method-chorales.md), [fr](https://postext.dev/fr/cookbook/conservatory-method-chorales.md), [it](https://postext.dev/it/cookbook/conservatory-method-chorales.md), [pt](https://postext.dev/pt/cookbook/conservatory-method-chorales.md), [zh](https://postext.dev/zh/cookbook/conservatory-method-chorales.md), [ja](https://postext.dev/ja/cookbook/conservatory-method-chorales.md), [ar](https://postext.dev/ar/cookbook/conservatory-method-chorales.md)

## In short

A chapter on cadences from a conservatory harmony book, in two columns. Its five music examples are written as text, drawn as real scores, numbered and captioned like figures, and placed near the paragraph that mentions them.

## What you'll build

Chapter 4 of *Four-Part Harmony*, a method for the first years of the conservatory, on a 210 × 280 mm page in two columns of Crimson Pro. The lesson is on cadences, and its five examples are chorales in four parts on two staves, with Roman numerals under the bass: the five cadences of G major, three lines of *Nun danket alle Gott* and the Old Hundredth complete. Each one is numbered Example 4.1 to 4.5 (Ejemplo in the Spanish edition), captioned in the label face with a claret label, and placed at the head or the foot of the column after the paragraph that cites it; the whole Old Hundredth runs across both columns at the head of page 63. The settings were written for this method and checked for parallel fifths and octaves, spacing and the resolution of the leading tone.

**This recipe answers:**

- How do I number four-part music examples and float them like figures in two columns?
- How do I add a figure with a numbered caption and cite it in the text ("see Fig. 3.2")?
- How do I control where a figure goes: top of page, across both columns, exactly here, or in the margin?

## The short answer

Score resources typed 'example', floated and numbered like figures.

```js
// script.js, lines 26–41
// In the Markdown, :ref{id="ex-old-hundredth" style="full"} prints 'Example 4.4' ('Ejemplo
// 4.4' in Spanish) and ::resource{id="…"} anchors the float after the paragraph that cites it.
const example = (id, source, placement, styleId) => {
  const [caption, note, altText] = CAPTIONS[id];
  return { id, typeId: 'example', kind: 'score', caption, note, altText, placement,
    score: { format: 'humdrum', source, styleId }, createdAt: 0, updatedAt: 0 };
};
// The **kern sources are in #region art-scores; captions in content.captions.<lang>.md.
const resources = () => [
  example('ex-cadences', CADENCES, { position: 'bottom' }, 'column'),
  example('ex-nun-danket', NUN_DANKET, { position: 'top' }, 'column'),
  example('ex-nun-danket-3', NUN_DANKET_3, { position: 'auto' }, 'column'),
  // Across both columns in the document's own score style: the whole tune in two systems.
  example('ex-old-hundredth', OLD_HUNDREDTH, { position: 'top', span: 'page' }),
  example('ex-deceptive', DECEPTIVE, { position: 'auto', span: 'page' }),
];
```

## Ingredients

**Teaches**

- [Numbered music examples](https://postext.dev/en/docs/music.md#numbered-music-examples): Resources of kind score typed example: numbered in the document's language (Example 3.2), captioned, cited with :ref and placed or floated like figures; each is kept whole.
- [Figure placement](https://postext.dev/en/docs/document-format-resources.md#placement): Top, bottom, auto or here, in a column or across the page, at a fraction of the width, per resource or per type; floats of one sequence never overtake each other.
- [Citations that place figures](https://postext.dev/en/docs/document-format-resources.md#inline-reference-the-primary-form): A :ref cites a resource ("see Fig. 3.2") and its first citation places it: the figure floats to the first free slot after it.

**Also uses**

- [Music notation](https://postext.dev/en/docs/music.md#a-score-in-the-text)
- [Score style](https://postext.dev/en/docs/configuration-resources.md#score-style)
- [Numbered captions](https://postext.dev/en/docs/document-format-resources.md#first-reference-numbering)
- [Figure and Table in your language](https://postext.dev/en/docs/configuration-resources.md#resource-types)
- [Custom resource types](https://postext.dev/en/docs/configuration-resources.md#resource-types)
- [Caption style](https://postext.dev/en/docs/configuration-resources.md#caption-style)
- [Source and credit lines](https://postext.dev/en/docs/configuration-resources.md#caption-style)
- [One or two columns](https://postext.dev/en/docs/configuration-page-layout.md#layout-types)
- [Designed openers](https://postext.dev/en/docs/configuration-headings.md#span-and-advanced-design)
- [Heading attributes](https://postext.dev/en/docs/document-format.md#heading-attributes)
- [Running heads and folios](https://postext.dev/en/docs/configuration-headers-footers.md#headers--footers)
- [Mirrored margins](https://postext.dev/en/docs/configuration-page-layout.md#mirrored-margins)
- [Heads by page role](https://postext.dev/en/docs/configuration-headers-footers.md#text-elements)
- [Callout boxes](https://postext.dev/en/docs/configuration-callouts.md#callout-styles)
- [Paragraph styles](https://postext.dev/en/docs/configuration-styles.md#paragraph-styles)
- [Semantic colour palette](https://postext.dev/en/docs/configuration-fonts-colors-viewers.md#color-palette)
- [Figures exactly here](https://postext.dev/en/docs/document-format-resources.md#block-embed-optional-explicit-inline-placement)
- [Full-width chapter band](https://postext.dev/en/docs/configuration-headings.md#span-and-advanced-design)
- [Superscripts and subscripts](https://postext.dev/en/docs/document-format-inline.md#inline-formatting)

**Config at a glance**

- [`bodyText`](https://postext.dev/en/docs/configuration-text.md#body-text), [`calloutStyles`](https://postext.dev/en/docs/configuration-callouts.md#callout-styles), [`captionStyle`](https://postext.dev/en/docs/configuration-resources.md#caption-style), [`colorPalette`](https://postext.dev/en/docs/configuration-fonts-colors-viewers.md#color-palette), [`footer`](https://postext.dev/en/docs/configuration-headers-footers.md#headers--footers), [`header`](https://postext.dev/en/docs/configuration-headers-footers.md#headers--footers), [`headings`](https://postext.dev/en/docs/configuration-headings.md#headings), [`layout`](https://postext.dev/en/docs/configuration-page-layout.md#layout), [`locale`](https://postext.dev/en/docs/configuration-language.md#hyphenation), [`orderedLists`](https://postext.dev/en/docs/configuration-lists-math.md#ordered-lists), [`page`](https://postext.dev/en/docs/configuration-page-layout.md#page), [`paragraphStyles`](https://postext.dev/en/docs/configuration-styles.md#paragraph-styles), [`resourceTypes`](https://postext.dev/en/docs/configuration-resources.md#resource-types), [`scoreStyle`](https://postext.dev/en/docs/configuration-resources.md#score-style), [`scoreStyles`](https://postext.dev/en/docs/configuration-resources.md#score-style), [`unorderedLists`](https://postext.dev/en/docs/configuration-lists-math.md#unordered-lists)

**APIs**

- [`buildDocumentWithFonts`](https://postext.dev/en/docs/configuration-programmatic-usage.md#loading-fonts-before-layout), [`defaultResourceTypes`](https://postext.dev/en/docs/configuration-resources.md#resource-types), [`initMusicEngine`](https://postext.dev/en/docs/music.md#a-score-in-the-text), [`renderPageToCanvas`](https://postext.dev/en/docs/configuration-programmatic-usage.md#rendering-a-page-to-a-bitmap)

**Typefaces**

- Crimson Pro (OFL-1.1), Playfair Display (OFL-1.1), Fira Sans Condensed (OFL-1.1)

## Method

### 1 · Write each chorale as Humdrum **kern

```js
// script.js, lines 263–273
// Each example lists its spines as Humdrum wants them, lowest first: the bass staff split into
// tenor and bass, the **harm spine (printed under that staff), the treble staff split into
// soprano and alto. Writing them with spaces keeps the pen free of tab characters.
const kern = (meter, body) => [
  '**kern **harm **kern', '*clefF4 * *clefG2', '*k[f#] * *k[f#]', `*M${meter} * *M${meter}`,
  '*^ * *^', ...body.trim().split('\n'), '== == == == ==', '*- *- *- *- *-',
].map((row) => row.trim().split(/ +/)
  // Spanish harmony books write every degree in capitals: vi is VI, iib is IIb.
  .map((cell) => (LANG === 'es' && /^(?:i{1,3}|iv|vi{0,2})[bc7]?$/.test(cell)
    ? cell.replace(/^[iv]+/, (degree) => degree.toUpperCase()) : cell))
  .join('\t')).join('\n');
```

Humdrum lists the voices as columns, lowest first, separated by tabs. Each staff is one spine split in two with `*^`: tenor and bass on the bass staff, soprano and alto on the treble staff, so the engraver sets two voices per staff with their stems apart (the left voice up, the right voice down). A `**harm` spine after the bass staff prints the Roman numerals under it; `Ib` is a first inversion, `Ic` a six-four. The pen writes the columns with spaces and turns them into tabs, so no editor can break the file, and the Spanish edition gets its degrees in capitals (VI, IIb), as Spanish harmony books write them.

### 2 · Make every example a resource of type example

The [short answer](https://postext.dev/en/cookbook/conservatory-method-chorales.md#the-short-answer) builds the five resources. `typeId: 'example'` takes the built-in Example type, which the engine adds to the book's resource types when a resource uses it, named in the document's language (`locale: 'es'` gives Ejemplo) and numbered per chapter, so the first example of chapter 4 is 4.1. A score resource is kept whole and floats like a figure: `position: 'top'` or `'bottom'` takes the head or foot of the next column with room after the citation, and `span: 'page'` sets the Old Hundredth across both columns. Captions, credits and alt texts come from a second Markdown file per edition.

### 3 · Give the column examples a smaller staff

```js
// script.js, lines 45–53
const scoreStyle = {
  staffSize: mm(5.4), musicFont: 'Bravura', barNumbers: 'system', color: col('ink'),
  textFont: TEXT, // the Roman numerals under the bass, in the text face
  marginTop: pt(4), marginBottom: pt(2),
};
const scoreStyles = [ // rastral 4.6 mm: four staves of a chorale fit a column in two systems
  { id: 'column', name: t({ en: 'Column examples', es: 'Ejemplos de columna' }),
    staffSize: mm(4.6), barNumbers: 'none', spacing: 0.8 },
];
```

The two page-wide chorales use the document's `scoreStyle`, with 5.4 mm staves and bar numbers at the start of each system. The examples in a column pick the named `column` style: 4.6 mm staves, no bar numbers and spacing 0.8, which keeps the five bars of Example 4.2 on one system instead of leaving its last bar alone on a second one.

### 4 · Start the engraver before the build

```js
// script.js, lines 398–404
await initMusicEngine(); // until it runs, every score is laid out as a grey box
const continuation = { pageNumbering: { startAt: 61 }, // odd: the opener is a recto
  headings: { h1: 3, h2: 0, h3: 0, h4: 0, h5: 0, h6: 0 } }; // the next # is chapter 4
const doc = await buildDocumentWithFonts({ markdown, resources: resources(), continuation },
  config, kitFonts(FONTS));
showPages(doc, { title: t({ en: 'Numbered chorale examples in a harmony method',
  es: 'Corales numerados en un método de armonía' }) });
```

`initMusicEngine()` loads Verovio, the engraver, from the package; until it runs, a score is laid out as a grey box. The `continuation` makes this chapter 4 of a longer book, so the examples number 4.1 to 4.5 and the folios start at 61 on a recto.

### 5 · Open the chapter across both columns

```js
// script.js, lines 57–77
const label = { fontFamily: LABEL, fontWeight: 600, textTransform: 'uppercase' };
const opener = {
  enabled: true, minHeight: mm(46),
  slot: { elements: [
      { kind: 'text', id: 'kicker', content: '{attr.kicker}', ...label, fontSize: pt(9),
        letterSpacing: pt(1.8), color: col('accent'), align: 'left',
        placement: { anchor: { to: 'container', edge: 'top-left' },
          offset: { x: mm(0), y: mm(6) } } },
      { kind: 'text', id: 'title', content: '{titleText}', fontFamily: DISPLAY, fontSize: pt(36),
        lineHeight: 1.05, fontWeight: 400, color: col('ink'), align: 'left',
        placement: { anchor: { to: '#kicker', edge: 'below' }, offset: { x: mm(0), y: mm(3) },
          size: { width: mm(150), height: 'auto' } } },
      { kind: 'text', id: 'deck', content: '{attr.deck}', fontFamily: TEXT, fontSize: pt(13),
        lineHeight: 1.25, italic: true, color: col('muted'), align: 'left',
        placement: { anchor: { to: '#title', edge: 'below' }, offset: { x: mm(0), y: mm(3) },
          size: { width: mm(120), height: 'auto' } } },
      { kind: 'rule', id: 'rule', direction: 'horizontal', thickness: pt(1.5), color: col('accent'),
        placement: { anchor: { to: '#deck', edge: 'below' }, offset: { x: mm(0), y: mm(5) },
          size: { width: mm(18) } } },
  ] },
};
```

The opener takes both columns (`span: 'page'` on the level-one heading) and draws its kicker and deck from the heading's attributes. `minHeight` sinks the text 46 mm, so both columns start on the same line under the rule.

## The whole recipe

One file, composed from the recipe's folder with the sample text and the Cookbook's shared kit inlined; it builds its own page. To run it, put it in a `<script type="module">` on an empty page, or paste it into the JS panel of a new CodePen (as a module). It imports postext from esm.sh, so there is nothing to install or build.

- Source folder: https://github.com/drnachio/postext/tree/main/cookbook/conservatory-method-chorales

### script.js

```js
// ═══ Postext Cookbook · Nº 166 · Numbered chorale examples in a harmony method ══
// https://postext.dev/en/cookbook/conservatory-method-chorales
// Code: MIT · Text and four-part settings: original (CC BY 4.0) · Melodies: public domain
// Fonts: Crimson Pro, Playfair Display, Fira Sans Condensed (OFL 1.1) · Needs postext ≥ 1.54.0
import {
  buildDocumentWithFonts, renderPageToCanvas, defaultResourceTypes, initMusicEngine,
} from 'https://esm.sh/postext';

const LANG = 'en'; // @lang: the language of the sample document ('en' | 'es')
const RECIPE = 'conservatory-method-chorales';

// ─── 1 · Design ─────────────────────────────────────────────────────────────
// A claret accent on warm paper tones, every colour linked by id.
const palette = { ink: '#1f1a17', accent: '#7a2531', tint: '#f5eee6', rule: '#cdbfb2',
  muted: '#6a5f57', paper: '#ffffff' };
const col = (id) => ({ hex: palette[id], model: 'hex', paletteId: id });
const colorPalette = [
  ...Object.entries(palette).map(([id, hex]) => ({ id, name: id, value: { hex, model: 'hex' } })),
  { id: 'main-color', name: 'accent (defaults)', value: { hex: palette.accent, model: 'hex' } },
];
const [TEXT, DISPLAY, LABEL] = ['Crimson Pro', 'Playfair Display', 'Fira Sans Condensed'];
const OUTER = 16; // outer margin in mm: running heads and folios align to it
const LEAD = 14.2; // body leading in pt

// #region answer: score resources typed 'example', floated and numbered like figures
// In the Markdown, :ref{id="ex-old-hundredth" style="full"} prints 'Example 4.4' ('Ejemplo
// 4.4' in Spanish) and ::resource{id="…"} anchors the float after the paragraph that cites it.
const example = (id, source, placement, styleId) => {
  const [caption, note, altText] = CAPTIONS[id];
  return { id, typeId: 'example', kind: 'score', caption, note, altText, placement,
    score: { format: 'humdrum', source, styleId }, createdAt: 0, updatedAt: 0 };
};
// The **kern sources are in #region art-scores; captions in content.captions.<lang>.md.
const resources = () => [
  example('ex-cadences', CADENCES, { position: 'bottom' }, 'column'),
  example('ex-nun-danket', NUN_DANKET, { position: 'top' }, 'column'),
  example('ex-nun-danket-3', NUN_DANKET_3, { position: 'auto' }, 'column'),
  // Across both columns in the document's own score style: the whole tune in two systems.
  example('ex-old-hundredth', OLD_HUNDREDTH, { position: 'top', span: 'page' }),
  example('ex-deceptive', DECEPTIVE, { position: 'auto', span: 'page' }),
];
// #endregion

// #region scores: one score style for the page-wide chorale, a smaller one for the columns
const scoreStyle = {
  staffSize: mm(5.4), musicFont: 'Bravura', barNumbers: 'system', color: col('ink'),
  textFont: TEXT, // the Roman numerals under the bass, in the text face
  marginTop: pt(4), marginBottom: pt(2),
};
const scoreStyles = [ // rastral 4.6 mm: four staves of a chorale fit a column in two systems
  { id: 'column', name: t({ en: 'Column examples', es: 'Ejemplos de columna' }),
    staffSize: mm(4.6), barNumbers: 'none', spacing: 0.8 },
];
// #endregion

// #region opener: kicker, title, deck and a short rule across both columns, sunk 46 mm
const label = { fontFamily: LABEL, fontWeight: 600, textTransform: 'uppercase' };
const opener = {
  enabled: true, minHeight: mm(46),
  slot: { elements: [
      { kind: 'text', id: 'kicker', content: '{attr.kicker}', ...label, fontSize: pt(9),
        letterSpacing: pt(1.8), color: col('accent'), align: 'left',
        placement: { anchor: { to: 'container', edge: 'top-left' },
          offset: { x: mm(0), y: mm(6) } } },
      { kind: 'text', id: 'title', content: '{titleText}', fontFamily: DISPLAY, fontSize: pt(36),
        lineHeight: 1.05, fontWeight: 400, color: col('ink'), align: 'left',
        placement: { anchor: { to: '#kicker', edge: 'below' }, offset: { x: mm(0), y: mm(3) },
          size: { width: mm(150), height: 'auto' } } },
      { kind: 'text', id: 'deck', content: '{attr.deck}', fontFamily: TEXT, fontSize: pt(13),
        lineHeight: 1.25, italic: true, color: col('muted'), align: 'left',
        placement: { anchor: { to: '#title', edge: 'below' }, offset: { x: mm(0), y: mm(3) },
          size: { width: mm(120), height: 'auto' } } },
      { kind: 'rule', id: 'rule', direction: 'horizontal', thickness: pt(1.5), color: col('accent'),
        placement: { anchor: { to: '#deck', edge: 'below' }, offset: { x: mm(0), y: mm(5) },
          size: { width: mm(18) } } },
  ] },
};
// #endregion

// #region running-heads: book title on the verso, chapter on the recto, folios outside
const head = (id, content, parity, edge, x, extra = {}) => ({
  kind: 'text', id, content, parity, pages: 'body', ...label, fontSize: pt(7.8),
  letterSpacing: pt(1.2), color: col('muted'),
  placement: { anchor: { to: 'page', edge }, offset: { x: mm(x), y: mm(12) } }, ...extra,
});
const folio = { fontWeight: 700, color: col('accent') };
const header = {
  elements: [
    head('verso-folio', '{pageNumber}', 'even', 'top-left', OUTER, folio),
    head('verso-title', '{title}', 'even', 'top-left', OUTER + 9),
    head('recto-title', '{chapterTitle}', 'odd', 'top-right', -(OUTER + 9)),
    head('recto-folio', '{pageNumber}', 'odd', 'top-right', -OUTER, folio),
  ],
};
const footer = { // the opener's drop folio, centred under the text block
  elements: [head('drop-folio', '{pageNumber}', 'all', 'top', 0, { ...folio, pages: 'opener',
    placement: { anchor: { to: 'container', edge: 'top' }, offset: { x: mm(0), y: mm(8) } } })],
};
// #endregion

const config = {
  locale: t({ en: 'en-us', es: 'es' }), // hyphenation, and the name of the Example type
  resourceTypes: defaultResourceTypes(LANG), // the engine adds Example (Ejemplo) in locale
  colorPalette,
  page: {
    sizePreset: 'custom', width: mm(210), height: mm(280), dpi: 150,
    margins: { top: mm(22), bottom: mm(22), left: mm(20), right: mm(OUTER), mirror: true },
  },
  layout: { layoutType: 'double', gutterWidth: mm(7) },
  bodyText: {
    fontFamily: TEXT, fontSize: pt(10.8), lineHeight: pt(LEAD), color: col('ink'),
    boldColor: col('ink'), italicColor: col('ink'), referenceColor: col('accent'),
    textAlign: 'justify', firstLineIndent: mm(4), indentAfterHeading: false,
    hyphenation: { enabled: true }, optimalLineBreaking: true,
    avoidWidows: true, avoidOrphans: true, avoidRunts: true,
  },
  headings: {
    fontFamily: DISPLAY, color: col('ink'), fontWeight: 700,
    levels: [
      { level: 1, span: 'page', fontSize: pt(36), breakBefore: { parity: 'odd' },
        advancedDesign: opener, marginBottom: pt(0) },
      { level: 2, fontSize: pt(13.5), lineHeight: pt(LEAD), marginTop: pt(LEAD),
        marginBottom: pt(LEAD / 2) },
    ],
  },
  unorderedLists: { bulletChar: '–', color: col('accent'),
    marginTop: pt(0), marginBottom: pt(0) },
  orderedLists: { numberFormat: 'arabic', color: col('accent'), fontWeight: 700,
    marginTop: pt(0), marginBottom: pt(0) },
  paragraphStyles: [
    { id: 'colophon', fontFamily: LABEL, fontSize: pt(7.5), lineHeight: pt(10),
      color: col('muted'), textAlign: 'left', firstLineIndent: pt(0), marginTop: pt(LEAD) },
  ],
  calloutStyles: [
    { id: 'rule', title: t({ en: 'Rule', es: 'Regla' }), background: col('tint'),
      stripe: { enabled: true, side: 'top', width: pt(2.5), color: col('accent') },
      padding: { top: mm(3), right: mm(3.5), bottom: mm(3), left: mm(3.5) },
      titleStyle: { ...label, fontSize: pt(8), fontWeight: 700, letterSpacing: pt(1.2),
        color: col('accent') },
      body: { fontFamily: TEXT, fontSize: pt(10), lineHeight: pt(13), color: col('ink'),
        firstLineIndent: pt(0), boldColor: col('ink'), italicColor: col('ink') } },
  ],
  captionStyle: { fontFamily: LABEL, fontSize: pt(8.4), color: col('ink'), gap: mm(1.5),
    labelBold: true, labelColor: col('accent'), note: { color: col('muted'), gap: mm(0.5) } },
  scoreStyle,
  scoreStyles,
  header,
  footer,
};

// ─── 2 · Content ────────────────────────────────────────────────────────────
const markdown = String.raw`---
title: "Four-Part Harmony"
---

# Cadences {kicker="Chapter 4" deck="Where a phrase comes to rest, and how the four voices take it there"}

A phrase of music breathes out at its end, and the chord that closes it tells the listener how far the music has come to rest. That closing formula is the cadence. Students who can already connect any two chords in four parts tend to treat the last two chords of a phrase like any others, and their exercises end without conviction. This chapter slows down at the end of the phrase. It names the five cadences of the major key, shows how each one is voiced, and follows them through two chorale melodies that every harmony class sings: *Nun danket alle Gott* and the Old Hundredth.

:ref{id="ex-cadences" style="full"} sets the five side by side in G major, one to a bar, with the Roman numerals under the bass. Play them at the keyboard before you read on, and listen for how final each one sounds: the order of the bars runs from the most closed to the most open.

::resource{id="ex-cadences"}

## The authentic cadence

An authentic cadence is the progression V–I. It is the strongest close the key has, because the bass falls a fifth (or rises a fourth) from the dominant to the tonic while the leading tone, the seventh degree, rises a semitone to the tonic in one of the upper voices.

We call it **perfect** when both chords are in root position and the soprano ends on the tonic. If either condition fails, the cadence is **imperfect**: the soprano ends on the third or the fifth of the tonic chord, or one of the two chords is inverted. The first two bars of :ref{id="ex-cadences" case="lower"} keep the same bass and change the top: F sharp rising to G in the first, A rising to B in the second.

The leading tone needs care. In the soprano it must rise to the tonic. In an inner voice it may fall a third to the fifth of the tonic chord, so that the final chord is complete, and Bach does this in many of his chorales; but never let it fall in the bass or the soprano. After a dominant seventh the seventh itself (C in G major) resolves down by step. If the leading tone rises as well, the tonic chord that follows has a tripled root and no fifth; letting it fall in an inner voice keeps the chord complete. The last bar of :ref{id="ex-nun-danket" case="lower"} takes the second way: the tenor holds C from the chord before and falls to B, the alto lets F sharp drop to D, and the soprano sings A–G over the bass D–G.

::resource{id="ex-nun-danket"}

## The plagal cadence

The progression IV–I is the plagal cadence. It has no leading tone, and its effect is gentler; hymn books use it for the *Amen* after a final authentic cadence. In four parts the tonic is the common tone of the two chords and stays in the same voice, while the other two upper voices move down by step, the sixth degree to the fifth and the fourth to the third, as in the fourth bar of :ref{id="ex-cadences" case="lower"}.

The first line of *Nun danket alle Gott* ends this way in our setting. The soprano repeats E over C and then falls to D over G (bars 1–2 of :ref{id="ex-nun-danket" case="lower"}). The cadence comes on the downbeat and the fermata is short, so the singers take a breath and go on: a plagal close in the middle of a hymn reads as a comma, not a full stop.

## The half cadence

A phrase that stops on the dominant ends with a **half cadence**. Any chord may lead to it, most often I, IV or ii, and the dominant is always in root position. The ear hears a question waiting for its answer, which is why the half cadence closes the first half of so many tunes and almost never the last line. In the third line of *Nun danket alle Gott* (:ref{id="ex-nun-danket-3" case="lower"}) the tonic in first inversion leads to V under a soprano that falls from B to A.

::resource{id="ex-nun-danket-3"}

Many half cadences approach the dominant through the cadential six-four: the tonic chord with its fifth in the bass, on a strong beat, followed by the dominant on the same bass note. The sixth and the fourth above the bass fall by step to the fifth and the third. Treat the six-four as an ornament of the dominant, not as a tonic, and double its bass.

## Four cadences in one tune

The Old Hundredth, printed with Psalm 134 in the Genevan Psalter of 1551, closes its four lines on three kinds of cadence. :ref{id="ex-old-hundredth" style="full"} sets it complete, barred in 2/2, with the long and short notes of the Genevan rhythm.

::resource{id="ex-old-hundredth"}

- **Line 1** ends on B over G: an imperfect authentic cadence, because the soprano stops on the third.
- **Line 2** ends with the cadential six-four and V, a half cadence with the suspension in the soprano.
- **Line 3** ends F sharp–G over D–G: a perfect authentic cadence with the leading tone resolving in the soprano. Coming before the last line, it is answered by a stronger close.
- **Line 4** descends B–A–G over the cadential six-four, V and I: the most complete close of the four.

Sing the bass while a classmate plays the upper voices: the bass line alone tells you which cadence is coming.

## The deceptive cadence

When V moves to vi instead of I, the cadence is **deceptive**. The leading tone still rises to the tonic, but the bass climbs a step to the sixth degree, and the phrase that seemed finished has to go on. Double the third of vi (the tonic of the key) so that the leading tone can resolve and the other voices fall without parallel octaves.

:::callout{type="rule" title="Before you write the cadence"}
- Decide the cadence from the melody first: the last two notes of the soprano allow only some of them.
- Put the dominant in root position for authentic, half and deceptive cadences.
- Resolve the leading tone up in the outer voices, and the seventh of V^7^ down.
- Check the outer voices for parallel and hidden fifths and octaves.
:::

Composers use the deceptive cadence to lengthen a phrase. :ref{id="ex-deceptive" style="full"} sings the last line of the Old Hundredth twice: the first time it closes on vi, an E minor chord under the tonic note, and the second time on the perfect cadence of :ref{id="ex-old-hundredth" case="lower"}.

::resource{id="ex-deceptive"}

## In the minor key

Everything in this chapter holds in minor, with one condition: the leading tone is not in the key signature. In A minor the dominant chord is E–G sharp–B, and the sharp has to be written each time; without it the chord is minor and the authentic cadence loses its pull. In the deceptive cadence the raised leading tone rises to the tonic and the sixth degree, F, sits a semitone above the dominant in the bass, so the alto or the tenor must avoid the augmented second G sharp–F.

Two cadences belong to the minor mode. A chorale in minor often closes on a major tonic chord, raising its third at the last moment: this is the *tierce de Picardie*, which Bach uses at the end of most of his minor chorales. And a half cadence may approach the dominant from the first inversion of iv, with the bass falling a semitone from the sixth degree to the fifth: the Phrygian half cadence, common at the end of a slow movement before a fast one.

## Exercises

1. Write the five cadences of :ref{id="ex-cadences" case="lower"} in D major and in F major, in close and in open position.
2. Harmonize the first line of the Old Hundredth with a perfect authentic cadence in place of the imperfect one. Which note of the melody must change?
3. Find the cadence at the end of each line in three chorales of your choice and label it.
4. Rewrite the last two bars of :ref{id="ex-old-hundredth" case="lower"} with a plagal *Amen* after the final chord.

:::paragraphs{style="colophon"}
*Four-Part Harmony*, a method for the first years of the conservatory · Set in Crimson Pro, Playfair Display and Fira Sans Condensed (SIL OFL) · Music engraved in Bravura · Melodies in the public domain; harmonizations and text original, CC BY 4.0.
:::
`; // content.<lang>.md, inlined by the Cookbook
// Caption, credit note ('-' for none) and alt text of each example, one block per example.
const CAPTIONS = Object.fromEntries((String.raw`ex-cadences
The five cadences in G major: perfect and imperfect authentic, half, plagal, deceptive
-
Five two-chord cadences in G major for four voices on two staves, one per bar, with Roman numerals under the bass: V–I twice, I–V, IV–I and V–vi.

ex-nun-danket
*Nun danket alle Gott*, lines 1 and 2: a plagal close, then a perfect cadence
Melody: Johann Crüger, 1647, as in Bach’s BWV 386, in G · Setting: original
The first two lines of the chorale Nun danket alle Gott in G major for four voices, the first ending IV–I and the second ending V7–I.

ex-nun-danket-3
*Nun danket alle Gott*, line 3: a half cadence
-
The third line of Nun danket alle Gott in G major for four voices, ending on the dominant chord.

ex-old-hundredth
The Old Hundredth: an imperfect authentic cadence, a half cadence and two perfect ones
Melody: Genevan Psalter, 1551, attributed to Louis Bourgeois · Setting: original
The Old Hundredth complete in G major for four voices on two staves, thirteen bars in 2/2 with a fermata at the end of each of its four lines.

ex-deceptive
The last line of the Old Hundredth, closing first on vi, then on I
-
The last line of the Old Hundredth sung twice in four parts: the first time the dominant moves to an E minor chord, the second time to the G major tonic.
`).trim().split(/\n\s*\n/)
  .map((block) => block.split('\n').map((line) => line.trim()))
  .map(([id, caption, note, alt]) => [id, [caption, note === '-' ? undefined : note, alt]]));

// #region humdrum: aligned columns become tab-separated **kern, the Spanish numerals uppercase
// Each example lists its spines as Humdrum wants them, lowest first: the bass staff split into
// tenor and bass, the **harm spine (printed under that staff), the treble staff split into
// soprano and alto. Writing them with spaces keeps the pen free of tab characters.
const kern = (meter, body) => [
  '**kern **harm **kern', '*clefF4 * *clefG2', '*k[f#] * *k[f#]', `*M${meter} * *M${meter}`,
  '*^ * *^', ...body.trim().split('\n'), '== == == == ==', '*- *- *- *- *-',
].map((row) => row.trim().split(/ +/)
  // Spanish harmony books write every degree in capitals: vi is VI, iib is IIb.
  .map((cell) => (LANG === 'es' && /^(?:i{1,3}|iv|vi{0,2})[bc7]?$/.test(cell)
    ? cell.replace(/^[iv]+/, (degree) => degree.toUpperCase()) : cell))
  .join('\t')).join('\n');
// #endregion

// #region art-scores: the five examples; spines: tenor bass | harm | soprano alto
const CADENCES = kern('2/2', `
  2A   2D    V    2f#  2d
  2B   2GG   I    2g   2d
  =||  =||   =||  =||  =||
  2d   2D    V    2a   2f#
  2d   2GG   I    2b   2g
  =||  =||   =||  =||  =||
  2d   2GG   I    2b   2g
  2d   2D    V    2a   2f#
  =||  =||   =||  =||  =||
  2c   2C    IV   2g   2e
  2B   2GG   I    2g   2d
  =||  =||   =||  =||  =||
  2A   2D    V    2f#  2d
  2G   2E    vi   2g   2B`);

const NUN_DANKET = kern('4/4', `
  4B   4GG   I    4dd  4g
  =    =     =    =    =
  4A   4D    V    4dd  4f#
  4G   4BB   Ib   4dd  4g
  4c   4AA   ii   4ee  4e
  4G   4C    IV   4ee  4e
  =    =     =    =    =
  2B   2GG;  I    2dd; 2g
  4r   4ry   .    4r   4ry
  4d   4GG   I    4b   4g
  =    =     =    =    =
  4e   4C    IV   4cc  4g
  4d   4GG   I    4b   4g
  4A   4FF#  Vb   4a   4d
  8G   8GG   I    8b   8d
  8G   8EE   IVb  8cc  8c
  =    =     =    =    =
  2c   4C    iib  2a   4e
  .    4D    V7   .    4f#
  4B   4GG;  I    4g;  4d`);

const NUN_DANKET_3 = kern('4/4', `
  4c   4AA   ii   4a   4e
  =    =     =    =    =
  4A   4C    iib  4a   4e
  4d   4D    V    4a   4f#
  4d   4GG   I    4b   4g
  4d   4BB   Ib   4b   4g
  =    =     =    =    =
  2d   2D;   V    2a;  2f#
  4r   4ry   .    4r   4ry`);

const OLD_HUNDREDTH = kern('2/2', `
  2B   2G    I    2g   2d
  4B   4E    vi   4g   4e
  4B   4BB   iii  4f#  4d
  =    =     =    =    =
  4G   4C    IV   4e   4c
  4G   4GG   I    4d   4B
  2G   2E    vi   2g   2B
  =    =     =    =    =
  2F#  2D    V    2a   2d
  2G   2GG;  I    2b;  2d
  =||  =||   =||  =||  =||
  2G   2GG   I    2b   2d
  4G   4E    vi   4b   4e
  4G   4D    Ic   4b   4g
  =    =     =    =    =
  4c   4C    iib  4a   4e
  4G   4BB   Ib   4g   4d
  2G   2E    IVb  2cc  2g
  =    =     =    =    =
  2d   2D    Ic   2b   2g
  2d   2D;   V    2a;  2f#
  =||  =||   =||  =||  =||
  2B   2GG   I    2g   2d
  4A   4FF#  Vb   4a   4d
  4G   4GG   I    4b   4d
  =    =     =    =    =
  4E   4C    iib  4a   4e
  4G   4BB   Ib   4g   4d
  2G   2E    IVb  2e   2c
  =    =     =    =    =
  2A   2D    V    2f#  2d
  2B   2GG;  I    2g;  2d
  =||  =||   =||  =||  =||
  2G   2BB   Ib   2dd  2d
  2G   2GG   I    2b   2d
  =    =     =    =    =
  2G   2C    IV   2g   2e
  4c   4C    iib  4a   4e
  4c   4AA   ii   4cc  4e
  =    =     =    =    =
  2G   2D    Ic   2b   2d
  2F#  2D    V    2a   2d
  =    =     =    =    =
  1G   1GG;  I    1g;  1B`);

// The last line twice: the first time V moves to vi (E minor under the tonic note).
const LAST_LINE = (bass, degree) => `
  2G   2BB   Ib   2dd  2d
  2G   2GG   I    2b   2d
  =    =     =    =    =
  2G   2C    IV   2g   2e
  4c   4C    iib  4a   4e
  4c   4AA   ii   4cc  4e
  =    =     =    =    =
  2G   2D    Ic   2b   2d
  2F#  2D    V    2a   2d
  =    =     =    =    =
  1G   ${bass}  ${degree}  1g;  1B`;
const DECEPTIVE = kern('2/2', `${LAST_LINE('1E;', 'vi')}
  =  =  =  =  =${LAST_LINE('1GG;', 'I')}`);
// #endregion

// ─── 3 · Fonts ──────────────────────────────────────────────────────────────
const FONTS = {
  'Crimson Pro': ['400', '400i', '600', '700'], // text, and the numerals under the music
  'Playfair Display': ['400', '700'], // display: chapter title, section heads
  'Fira Sans Condensed': ['400', '600', '700'], // labels: kicker, heads, captions, the box
};

// ─── 4 · Build & show ───────────────────────────────────────────────────────
// #region build: start the engraver, then set chapter 4 of the method from page 61
await initMusicEngine(); // until it runs, every score is laid out as a grey box
const continuation = { pageNumbering: { startAt: 61 }, // odd: the opener is a recto
  headings: { h1: 3, h2: 0, h3: 0, h4: 0, h5: 0, h6: 0 } }; // the next # is chapter 4
const doc = await buildDocumentWithFonts({ markdown, resources: resources(), continuation },
  config, kitFonts(FONTS));
showPages(doc, { title: t({ en: 'Numbered chorale examples in a harmony method',
  es: 'Corales numerados en un método de armonía' }) });
// #endregion

// ─── Kit ── helpers shared by every Cookbook recipe · postext.dev/cookbook ─────

// ─── Kit · core v1 ── the same in every recipe · postext.dev/cookbook
function mm(value) { return { value, unit: 'mm' }; }
function pt(value) { return { value, unit: 'pt' }; }
function em(value) { return { value, unit: 'em' }; }
/** The sample language's string: t({ en: 'Figure', es: 'Figura' }). */
function t(strings) { return strings[LANG] ?? Object.values(strings)[0]; }
/** A file in this recipe's assets folder, served from the Postext repo by jsDelivr. */
function asset(file) { return `https://cdn.jsdelivr.net/gh/drnachio/postext@main/cookbook/${RECIPE}/assets/${file}`; }

// ─── Kit · fonts v3 ── the same in every recipe · postext.dev/cookbook
// Postext loads the faces a config asks for before layout (prepareFonts,
// buildDocumentWithFonts) and asks the kit for the files: Fontsource's, the
// files the PDF embeds too.

/** The options of buildDocumentWithFonts / prepareFonts / withLoadedFonts:
 *  the Fontsource resolver, and the faces of FONTS loaded with the ones the
 *  config names. FONTS = { 'Family Name': ['400', '400i', '700'] }. */
function kitFonts(FONTS = {}) {
  const faces = Object.entries(FONTS).flatMap(([family, specs]) => specs.map((spec) =>
    ({ family, weight: parseInt(spec, 10), style: spec.endsWith('i') ? 'italic' : 'normal' })));
  return { resolve: fontsourceResolver, faces };
}

/** A face's Fontsource files, in a fixed order: latin, then latin-ext and greek
 *  when `text` needs them and the family ships them (kitSubsetsFor), each with
 *  its unicode-range. Null for a weight or slant Fontsource does not ship. */
async function fontsourceResolver(family, weight, style, { text = '' } = {}) {
  const id = fontsourceId(family);
  const meta = await fontsourceMeta(family);
  if (meta && !(meta.weights.includes(weight) && meta.styles.includes(style))) return null;
  kitStatus('Loading fonts…');
  const at = await fontsourceAt(id);
  return ['latin', ...kitSubsetsFor(text, meta)].map((subset) => ({
    source: `${at}files/${id}-${subset}-${weight}-${style}.woff2`,
    unicodeRange: kitRanges()[subset],
  }));
}

/** The unicode-range of each Fontsource file the kit loads (a function: the
 *  kit is inlined after the pen's code, which runs before a const would be set). */
function kitRanges() {
  return {
    latin: 'U+0000-00FF,U+0131,U+0152-0153,U+02BB-02BC,U+02C6,U+02DA,U+02DC,U+0304,U+0308,U+0329,'
      + 'U+2000-206F,U+20AC,U+2122,U+2191,U+2193,U+2212,U+2215,U+FEFF,U+FFFD',
    'latin-ext': 'U+0100-02BA,U+02BD-02C5,U+02C7-02CC,U+02CE-02D7,U+02DD-02FF,U+0304,U+0308,U+0329,'
      + 'U+1D00-1DBF,U+1E00-1E9F,U+1EF2-1EFF,U+2020,U+20A0-20AB,U+20AD-20C0,U+2113,U+2C60-2C7F,U+A720-A7FF',
    greek: 'U+0370-03FF',
  };
}

/** For the cjk, arabic and comics blocks, which add their own files: the
 *  faces of `faces` = { 'Family Name': ['400', '400i', '700'] }. `text` is the sample:
 *  č ł † α χ also load latin-ext and greek files (kitSubsetsFor). With
 *  `optional`, a face Fontsource does not ship is skipped instead of failing.
 *  The files are added once all have loaded, in the order asked (latin, then
 *  latin-ext, then greek), whatever order they arrive in: the last one added
 *  wins a character two files hold. Resolves to the number of faces added. */
async function loadFonts(faces, text = '', { optional = false } = {}) {
  kitStatus('Loading fonts…');
  const ranges = kitRanges();
  const jobs = [];
  for (const [family, specs] of Object.entries(faces)) {
    const id = fontsourceId(family);
    const todo = [...new Set(specs)].map((spec) => [parseInt(spec, 10), spec.endsWith('i') ? 'italic' : 'normal'])
      .filter(([weight, style]) => !hasFace(family, weight, style)); // before any await
    const at = fontsourceAt(id);
    const meta = optional || /[^\0-ÿ]/u.test(text) ? await fontsourceMeta(family) : null;
    const subsets = ['latin', ...kitSubsetsFor(text, meta)];
    for (const [weight, style] of todo) {
      if (optional && !(meta?.weights.includes(weight) && meta.styles.includes(style))) continue;
      for (const subset of subsets) {
        const url = `${await at}files/${id}-${subset}-${weight}-${style}.woff2`;
        const face = new FontFace(family, `url(${url}) format('woff2')`,
          { weight: String(weight), style, unicodeRange: ranges[subset] });
        jobs.push(face.load().catch(() => {
          if (subset === 'latin' && !optional) throw new Error(`Fontsource has no ${family} ${weight} ${style}`);
        }));
      }
    }
  }
  const ready = await Promise.all(jobs).catch((error) => { kitFail(error); throw error; });
  return ready.filter((face) => face && document.fonts.add(face)).length;
}

/** A loaded FontFace covers this family, weight and style (fonts.check() would
 *  also say yes for families nobody declared). */
function hasFace(family, weight, style) {
  for (const face of document.fonts) {
    if (face.status !== 'loaded' || face.style !== style) continue;
    if (face.family.replace(/^["']|["']$/g, '') !== family) continue;
    const [low, high = low] = face.weight.split(' ').map(Number);
    if (weight >= low && weight <= high) return true;
  }
  return false;
}

/** The files beyond latin `text` needs that `meta`'s family ships. */
function kitSubsetsFor(text, meta) {
  return [[/[Ā-˿ᴀ-ᶿḀ-ỿ†ℓⱠ-Ɀ꜠-ꟿ]/u, 'latin-ext'], [/[Ͱ-Ͽ]/u, 'greek']]
    .filter(([re, x]) => re.test(text) && meta?.subsets?.includes(x)).map(([, x]) => x);
}

/** Fontsource's id for a family: 'Source Serif 4' → 'source-serif-4'. */
function fontsourceId(family) { return family.toLowerCase().replace(/\s+/g, '-'); }

/** A package's folder at the one version @5 names now (#766). */
function fontsourceAt(id, u = `https://cdn.jsdelivr.net/npm/@fontsource/${id}@`) {
  return (fontsourceAt.c ??= {})[id] ??= fetch(u + '5/package.json').then((r) => r.json())
    .then((p) => p.version || 5, () => 5).then((v) => `${u}${v}/`);
}

/** The family's Fontsource metadata (weights, styles, subsets), or null. */
function fontsourceMeta(family) {
  fontsourceMeta.cache ??= new Map();
  const id = fontsourceId(family);
  if (!fontsourceMeta.cache.has(id)) {
    fontsourceMeta.cache.set(id, fetch(`https://api.fontsource.org/v1/fonts/${id}`)
      .then((res) => (res.ok ? res.json() : null), () => null));
  }
  return fontsourceMeta.cache.get(id);
}

// ─── Kit · viewer v2 ── the same in every recipe · postext.dev/cookbook
/** The pages as spreads on a dark desk, page 1 alone, then verso | recto,
 *  each painted when it scrolls near. Other options go to renderPageToCanvas. */
function showPages(docs, { title, width = 460, ...render } = {}) {
  const root = viewer(title);
  const pages = [docs].flat().flatMap((doc) =>
    doc.pages.map((page) => ({ doc, page, n: (doc.pageIndexOffset ?? 0) + page.index })));
  const spreads = [];
  let verso = null;
  for (const p of pages) {
    if (p.n % 2 === 1) { if (verso) spreads.push([verso, null]); verso = p; }
    else { spreads.push([verso, p]); verso = null; }
  }
  if (verso) spreads.push([verso, null]);
  const density = Math.min(window.devicePixelRatio || 1, 2);
  showPages.painter?.disconnect();
  const painter = new IntersectionObserver((entries) => {
    for (const { isIntersecting, target } of entries) {
      if (!isIntersecting) continue;
      painter.unobserve(target);
      const { doc, page } = target.postext;
      renderPageToCanvas(page, doc, target, { ...render, scale: (width * density) / page.width });
    }
  }, { rootMargin: '800px' });
  showPages.painter = painter;
  root.replaceChildren(...spreads.map((pair) => {
    const spread = document.createElement('div');
    spread.className = 'pt-spread';
    for (const p of pair) {
      const figure = document.createElement('figure');
      if (p) {
        const label = p.page.pageLabel || String(p.n + 1);
        const canvas = document.createElement('canvas');
        canvas.postext = p;
        canvas.style.aspectRatio = `${p.page.width} / ${p.page.height}`;
        canvas.setAttribute('role', 'img');
        canvas.setAttribute('aria-label', `Page ${label}`);
        const folio = document.createElement('figcaption');
        folio.textContent = label;
        figure.append(canvas, folio);
        painter.observe(canvas);
      } else figure.className = 'pt-blank';
      spread.append(figure);
    }
    return spread;
  }));
  kitStatus(`${pages.length} ${pages.length === 1 ? 'page' : 'pages'}`);
  document.documentElement.dataset.postext = 'ready';
  return pages.length;
}

/** The desk, the bar and the error reporting, created once. */
function viewer(title) {
  if (!document.getElementById('pt-kit')) {
    document.head.insertAdjacentHTML('beforeend', `<style id="pt-kit">
      :root { color-scheme: dark; }
      body { margin: 0; background: #0e1014; color: #b9bcc4; font: 13px/1.45 system-ui, sans-serif; }
      #pt-bar { position: sticky; top: 0; z-index: 1; display: flex; flex-wrap: wrap; align-items: center;
        gap: 6px 16px; padding: 10px 16px; background: rgb(14 16 20 / .92); backdrop-filter: blur(6px);
        border-bottom: 1px solid #23262d; }
      #pt-bar strong { color: #f4f1ea; font-weight: 600; }
      #pt-actions { display: flex; gap: 12px; margin-left: auto; }
      #pt-actions a, #pt-actions button { color: #d8a21a; font: inherit; background: none; border: 0; padding: 0; cursor: pointer; }
      #pages { display: grid; justify-items: center; gap: 48px; padding: 32px 16px 72px; }
      .pt-spread { display: flex; }
      .pt-spread figure { margin: 0; width: min(460px, 44vw); }
      .pt-spread canvas { display: block; width: 100%; background: #fff;
        box-shadow: 0 1px 2px rgb(0 0 0 / .5), 0 22px 44px -16px rgb(0 0 0 / .8); }
      .pt-spread figure:first-child canvas { box-shadow: inset -14px 0 14px -14px rgb(0 0 0 / .18), 0 1px 2px rgb(0 0 0 / .5), 0 22px 44px -16px rgb(0 0 0 / .8); }
      .pt-spread figcaption { margin-top: 10px; text-align: center; font: 600 10px/1 system-ui, sans-serif;
        letter-spacing: .18em; text-transform: uppercase; color: #6c7079; }
      .pt-blank { visibility: hidden; }
      @media (max-width: 760px) {
        .pt-spread { flex-direction: column; gap: 32px; }
        .pt-spread figure { width: min(460px, 92vw); }
        .pt-blank { display: none; }
      }
    </style>`);
    document.body.insertAdjacentHTML('afterbegin',
      '<header id="pt-bar"><strong id="pt-title"></strong><span id="pt-status" role="status"></span><span id="pt-actions"></span></header>');
    document.getElementById('pt-title').textContent = document.title || 'Postext';
    addEventListener('error', (event) => kitFail(event.error ?? event.message));
    addEventListener('unhandledrejection', (event) => kitFail(event.reason));
  }
  if (title) document.getElementById('pt-title').textContent = title;
  return document.getElementById('pages')
    ?? document.body.appendChild(Object.assign(document.createElement('main'), { id: 'pages' }));
}

function kitStatus(text) {
  viewer();
  document.getElementById('pt-status').textContent = text;
}

function kitFail(error) {
  document.documentElement.dataset.postext = 'error';
  kitStatus(`Error: ${error?.message ?? error}`);
}

// ─── /Kit ───────────────────────────────────────────────────────────────────────
```

## Variations

### Set an unnumbered example in the text

A fence holds a score that the text does not cite, like a display formula: no number, no caption.

```diff
- ::resource{id="ex-cadences"}
+ ```humdrum {style="column" alt="The five cadences in G major"}
+ **kern	**harm	**kern
+ …
+ ```
```

### Call them music examples

Define a type of your own for another name, and point `typeId` at it.

```diff
- resourceTypes: defaultResourceTypes(LANG),
+ resourceTypes: [...defaultResourceTypes(LANG), { id: 'musex', name: 'Music example',
+   shortLabel: 'Mus. ex.', captionPrefix: 'Music example', numberingTemplate: '{h1}.{n}',
+   resetOn: 'h1', counterFormat: 'decimal' }],
```

## Pitfalls

- **::resource{id="…"} takes double quotes only.** A block embed is recognised only as ::resource{id="…"} with double quotes; any other form stays in the text as a visible line.

- A score resource never breaks across columns or pages. Keep floated examples to a few bars, and put a longer piece in a fence, which breaks between systems.
- The engraver fills each system to the width of its slot, and the last system keeps its natural width. When an example leaves one bar alone on a second system, lower `spacing` or the staff size of its style.

## Credits

- Recipe: Ignacio Ferro ([@drnachio](https://github.com/drnachio))
- Text: Chapter text and the four-part settings of every example: Ignacio Ferro, CC-BY-4.0
- Text: Melody of Nun danket alle Gott (Praxis pietatis melica, 1647), read from the soprano of Bach's setting BWV 386: Johann Crüger ([source](https://github.com/craigsapp/bach-370-chorales/blob/master/kern/chor032.krn)), public domain
- Text: Melody of the Old Hundredth (Psalm 134 in the Genevan Psalter, 1551): Louis Bourgeois (attributed) ([source](https://en.wikipedia.org/wiki/Old_100th)), public domain
- Type: Crimson Pro (OFL-1.1), Playfair Display (OFL-1.1), Fira Sans Condensed (OFL-1.1)
- Code: MIT · Sample content: CC-BY-4.0

## Related

- [Nº 009 · Figures that float to where you cite them](https://postext.dev/en/cookbook/figures-float-where-cited.md): A two-column chapter with seven numbered figures: six float from their first :ref to the first slot their placement allows; one sits where ::resource puts it. · Level 3 (Advanced) · Textbooks
- [Nº 163 · A harmony textbook with music examples in ABC](https://postext.dev/en/cookbook/harmony-textbook-abc-examples.md): A chapter on cadences whose examples are ABC fences in the Markdown, engraved at the column width, one of them running on over two pages. · Level 2 (Intermediate) · Textbooks
- [Nº 001 · Textbook with a margin column](https://postext.dev/en/cookbook/textbook-margin-column.md): A column-and-a-half page whose outer column holds only floats: span 'side' figures and glosses stack there, and captionSide moves the other captions into it. · Level 3 (Advanced) · Textbooks
