# Nummerierte Choralbeispiele in einer Harmonielehre

> Vierstimmige Choräle in Humdrum **kern werden Notenressourcen vom Typ example: nummeriert als Notenbeispiel 4.1, mit :ref zitiert und wie Abbildungen platziert.

- HTML-Fassung: https://postext.dev/de/cookbook/conservatory-method-chorales
- Rezept Nº 166 · Abbildungen und Bilder · Stufe 2 (Mittel) · Ausgaben: Canvas
- Gattungen: Lehrbücher
- Benötigt postext ≥ 1.54.0 · getestet mit 1.54.0 am 2026-10-22
- Seiten: [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)
- In der Sandbox öffnen: https://postext.dev/de/sandbox#recipe=conservatory-method-chorales&lang=en (.postext: https://postext.dev/cookbook/conservatory-method-chorales/en/conservatory-method-chorales.postext)
- Zuletzt aktualisiert: 2026-10-22
- Andere Sprachen: [en](https://postext.dev/en/cookbook/conservatory-method-chorales.md), [es](https://postext.dev/es/cookbook/conservatory-method-chorales.md), [ca](https://postext.dev/ca/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)

## Kurz gesagt

Ein Kapitel über Kadenzen aus einer Harmonielehre fürs Konservatorium, in zwei Spalten. Die fünf Notenbeispiele werden als Text geschrieben, als echte Noten gestochen, wie Abbildungen nummeriert und nahe dem Absatz gesetzt, der sie nennt.

## Was Sie setzen

Kapitel 4 von *Four-Part Harmony*, einer Harmonielehre für die ersten Jahre am Konservatorium, auf einer Seite von 210 × 280 mm in zwei Spalten aus der Crimson Pro. Die Lektion behandelt Kadenzen, und ihre fünf Beispiele sind vierstimmige Choräle auf zwei Systemen mit römischen Stufenziffern unter dem Bass: die fünf Kadenzen in G-Dur, drei Zeilen von *Nun danket alle Gott* und der Genfer Psalm 134 (*Old Hundredth*) vollständig. Jedes trägt eine Nummer von Example 4.1 bis 4.5 (Ejemplo in der spanischen Ausgabe), eine Legende in der Auszeichnungsschrift mit weinrotem Etikett und steht am Kopf oder Fuß der Spalte nach dem Absatz, der es zitiert; der ganze Psalm läuft am Kopf von Seite 63 über beide Spalten. Die Sätze wurden für diese Harmonielehre geschrieben und auf Quint- und Oktavparallelen, Stimmabstände und die Auflösung des Leittons geprüft.

**Dieses Rezept beantwortet:**

- Wie nummeriere ich vierstimmige Notenbeispiele und lasse sie in zwei Spalten wie Abbildungen fließen?
- Wie füge ich eine Abbildung mit nummerierter Bildunterschrift ein und verweise im Text darauf („siehe Abb. 3.2“)?
- Wie bestimme ich, wo eine Abbildung steht: oben auf der Seite, über beide Spalten, genau hier oder am Rand?

## Die kurze Antwort

```js
// script.js, Zeilen 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' }),
];
```

## Zutaten

**Zeigt**

- [Nummerierte Notenbeispiele](https://postext.dev/de/docs/music.md#nummerierte-notenbeispiele): Ressourcen der Art score mit dem Typ example: in der Sprache des Dokuments nummeriert (Notenbeispiel 3.2), mit Bildunterschrift, mit :ref zitiert und wie Abbildungen gesetzt oder verschoben; jede bleibt ganz.
- [Platzierung von Abbildungen](https://postext.dev/de/docs/document-format-resources.md#platzierung): Oben, unten, automatisch oder hier, in einer Spalte oder über die Seite, mit einem Bruchteil der Breite, je Ressource oder je Typ; Gleitobjekte einer Folge überholen einander nie.
- [Verweise, die Abbildungen platzieren](https://postext.dev/de/docs/document-format-resources.md#verweis-im-text-die-hauptform): Ein :ref verweist auf eine Ressource („siehe Abb. 3.2“), und der erste Verweis platziert sie: die Abbildung gleitet in den ersten freien Platz danach.

**Verwendet außerdem**

- [Notensatz](https://postext.dev/de/docs/music.md#noten-im-text)
- [Notenstil](https://postext.dev/de/docs/configuration-resources.md#notenstil)
- [Nummerierte Bildunterschriften](https://postext.dev/de/docs/document-format-resources.md#nummerierung-beim-ersten-verweis)
- [Abbildung und Tabelle in Ihrer Sprache](https://postext.dev/de/docs/configuration-resources.md#ressourcentypen)
- [Eigene Ressourcentypen](https://postext.dev/de/docs/configuration-resources.md#ressourcentypen)
- [Format der Bildunterschrift](https://postext.dev/de/docs/configuration-resources.md#legendenstil)
- [Quellen- und Bildnachweise](https://postext.dev/de/docs/configuration-resources.md#legendenstil)
- [Eine oder zwei Spalten](https://postext.dev/de/docs/configuration-page-layout.md#layout-typen)
- [Gestaltete Auftakte](https://postext.dev/de/docs/configuration-headings.md#seitenbreite-und-erweiterte-gestaltung)
- [Überschriftenattribute](https://postext.dev/de/docs/document-format.md#attribute-von-überschriften)
- [Kolumnentitel und Seitenzahlen](https://postext.dev/de/docs/configuration-headers-footers.md#kopf--und-fußzeilen)
- [Gespiegelte Ränder](https://postext.dev/de/docs/configuration-page-layout.md#gespiegelte-ränder)
- [Kopfzeilen nach Seitenrolle](https://postext.dev/de/docs/configuration-headers-footers.md#textelemente)
- [Kästen](https://postext.dev/de/docs/configuration-callouts.md#kastenstile)
- [Absatzformate](https://postext.dev/de/docs/configuration-styles.md#absatzformate)
- [Semantische Farbpalette](https://postext.dev/de/docs/configuration-fonts-colors-viewers.md#farbpalette)
- [Abbildungen genau hier](https://postext.dev/de/docs/document-format-resources.md#blockeinbettung-optional-ausdrückliche-platzierung-im-text)
- [Kapitelband über die ganze Breite](https://postext.dev/de/docs/configuration-headings.md#seitenbreite-und-erweiterte-gestaltung)
- [Hoch- und Tiefstellung](https://postext.dev/de/docs/document-format-inline.md#inline-formatierung)

**Die Konfiguration auf einen Blick**

- [`bodyText`](https://postext.dev/de/docs/configuration-text.md#fließtext), [`calloutStyles`](https://postext.dev/de/docs/configuration-callouts.md#kastenstile), [`captionStyle`](https://postext.dev/de/docs/configuration-resources.md#legendenstil), [`colorPalette`](https://postext.dev/de/docs/configuration-fonts-colors-viewers.md#farbpalette), [`footer`](https://postext.dev/de/docs/configuration-headers-footers.md#kopf--und-fußzeilen), [`header`](https://postext.dev/de/docs/configuration-headers-footers.md#kopf--und-fußzeilen), [`headings`](https://postext.dev/de/docs/configuration-headings.md#überschriften), [`layout`](https://postext.dev/de/docs/configuration-page-layout.md#layout), [`locale`](https://postext.dev/de/docs/configuration-language.md#silbentrennung), [`orderedLists`](https://postext.dev/de/docs/configuration-lists-math.md#nummerierte-listen), [`page`](https://postext.dev/de/docs/configuration-page-layout.md#seite), [`paragraphStyles`](https://postext.dev/de/docs/configuration-styles.md#absatzformate), [`resourceTypes`](https://postext.dev/de/docs/configuration-resources.md#ressourcentypen), [`scoreStyle`](https://postext.dev/de/docs/configuration-resources.md#notenstil), [`scoreStyles`](https://postext.dev/de/docs/configuration-resources.md#notenstil), [`unorderedLists`](https://postext.dev/de/docs/configuration-lists-math.md#aufzählungen)

**APIs**

- [`buildDocumentWithFonts`](https://postext.dev/de/docs/configuration-programmatic-usage.md#schriften-vor-dem-layout-laden), [`defaultResourceTypes`](https://postext.dev/de/docs/configuration-resources.md#ressourcentypen), [`initMusicEngine`](https://postext.dev/de/docs/music.md#noten-im-text), [`renderPageToCanvas`](https://postext.dev/de/docs/configuration-programmatic-usage.md#eine-seite-als-bitmap-rendern)

**Schriften**

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

## Zubereitung

### 1 · Jeden Choral in Humdrum **kern schreiben

```js
// script.js, Zeilen 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 schreibt die Stimmen in Spalten, durch Tabulatoren getrennt, von der tiefsten zur höchsten. Jedes Notensystem ist eine Spalte (*spine*), die `*^` in zwei teilt: Tenor und Bass im Bassschlüssel, Sopran und Alt im Violinschlüssel. So setzt der Notenstecher zwei Stimmen pro System mit getrennten Hälsen (die linke Stimme nach oben, die rechte nach unten). Eine `**harm`-Spalte nach dem Basssystem druckt die Stufen darunter; `Ib` ist die erste Umkehrung, `Ic` der Quartsextakkord. Das Skript schreibt die Spalten mit Leerzeichen und macht Tabulatoren daraus, damit kein Editor die Datei verdirbt, und die spanische Ausgabe erhält ihre Stufen in Großbuchstaben (VI, IIb), wie spanische Harmonielehren sie schreiben.

### 2 · Jedes Beispiel zur Ressource vom Typ example machen

Die [kurze Antwort](https://postext.dev/de/cookbook/conservatory-method-chorales.md#die-kurze-antwort) baut die fünf Ressourcen. `typeId: 'example'` nimmt den eingebauten Typ Notenbeispiel, den die Engine den Ressourcentypen des Buchs hinzufügt, sobald eine Ressource ihn verwendet, benannt in der Sprache des Dokuments (`locale: 'es'` ergibt Ejemplo) und pro Kapitel nummeriert: Das erste Beispiel von Kapitel 4 ist 4.1. Eine Notenressource bleibt ganz und fließt wie eine Abbildung: `position: 'top'` oder `'bottom'` nimmt Kopf oder Fuß der nächsten Spalte mit Platz nach dem Zitat, und `span: 'page'` setzt den *Old Hundredth* über beide Spalten. Legenden, Nachweise und Alternativtexte kommen aus einer zweiten Markdown-Datei pro Ausgabe.

### 3 · Den Spaltenbeispielen ein kleineres System geben

```js
// script.js, Zeilen 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 },
];
```

Die beiden seitenbreiten Choräle folgen dem `scoreStyle` des Dokuments, mit 5,4 mm hohen Systemen und Taktzahlen am Anfang jeder Zeile. Die Beispiele in einer Spalte wählen den benannten Stil `column`: 4,6 mm, keine Taktzahlen und eine Spationierung von 0,8, die die fünf Takte von Beispiel 4.2 in einer Zeile hält, statt den letzten allein in eine zweite zu schicken.

### 4 · Den Notenstecher vor dem Satz starten

```js
// script.js, Zeilen 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()` lädt Verovio, den Notenstecher, aus dem Paket; bis er läuft, wird jede Partitur als graue Box gesetzt. Die `continuation` macht dies zu Kapitel 4 eines längeren Buchs, sodass die Beispiele 4.1 bis 4.5 heißen und die Seitenzahlen bei 61 auf einer rechten Seite beginnen.

### 5 · Das Kapitel über beide Spalten öffnen

```js
// script.js, Zeilen 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) } } },
  ] },
};
```

Der Auftakt nimmt beide Spalten ein (`span: 'page'` an der Überschrift der ersten Ebene) und holt Dachzeile und Vorspann aus den Attributen der Überschrift. `minHeight` senkt den Text um 46 mm, sodass beide Spalten auf derselben Zeile unter der Linie beginnen.

## Das ganze Rezept

Eine Datei, aus dem Ordner des Rezepts zusammengesetzt, mit dem Beispieltext und dem gemeinsamen Baukasten der Rezepte bereits eingebettet; sie baut ihre eigene Seite auf. Zum Ausführen setzen Sie sie in ein `<script type="module">` auf einer leeren Seite oder fügen sie in das JS-Feld eines neuen CodePen-Pens ein (als Modul). Sie importiert postext von esm.sh, es gibt also nichts zu installieren oder zu bauen.

- Quellordner: 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 ───────────────────────────────────────────────────────────────────────
```

## Varianten

### Ein Beispiel ohne Nummer in den Text setzen

Ein Codeblock hält eine Partitur im Text, die der Text nicht zitiert, wie eine abgesetzte Formel: ohne Nummer und ohne Legende.

```diff
- ::resource{id="ex-cadences"}
+ ```humdrum {style="column" alt="Die fünf Kadenzen in G-Dur"}
+ **kern	**harm	**kern
+ …
+ ```
```

### Sie Musikbeispiele nennen

Legen Sie einen eigenen Typ für einen anderen Namen an und lassen Sie `typeId` darauf zeigen.

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

## Häufige Fehler

- **::resource{id="…"} nimmt nur doppelte Anführungszeichen.** Eine Block-Einbettung wird nur als ::resource{id="…"} mit doppelten Anführungszeichen erkannt; jede andere Form bleibt als sichtbare Zeile im Text.

- Eine Notenressource wird nie zwischen Spalten oder Seiten geteilt. Halten Sie fließende Beispiele auf wenige Takte und setzen Sie ein längeres Stück in einen Codeblock, der zwischen den Zeilen umbricht.
- Der Notenstecher füllt jede Zeile auf die Breite ihres Platzes, die letzte behält ihre natürliche Breite. Lässt ein Beispiel einen Takt allein in einer zweiten Zeile, senken Sie `spacing` oder die Systemgröße seines Stils.

## Nachweise

- Rezept: 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 ([Quelle](https://github.com/craigsapp/bach-370-chorales/blob/master/kern/chor032.krn)), gemeinfrei
- Text: Melody of the Old Hundredth (Psalm 134 in the Genevan Psalter, 1551): Louis Bourgeois (attributed) ([Quelle](https://en.wikipedia.org/wiki/Old_100th)), gemeinfrei
- Schrift: Crimson Pro (OFL-1.1), Playfair Display (OFL-1.1), Fira Sans Condensed (OFL-1.1)
- Code: MIT · Beispielinhalt: CC-BY-4.0

## Verwandte Rezepte

- [Nº 009 · Abbildungen, die dorthin gleiten, wo Sie sie zitieren](https://postext.dev/de/cookbook/figures-float-where-cited.md): Zweispaltiges Kapitel mit sieben nummerierten Abbildungen: Sechs gleiten vom ersten :ref an den ersten erlaubten Platz, eine steht, wo ::resource sie setzt. · Stufe 3 (Fortgeschritten) · Lehrbücher
- [Nº 163 · Ein Harmonielehrbuch mit Notenbeispielen in ABC](https://postext.dev/de/cookbook/harmony-textbook-abc-examples.md): Ein Kapitel über Kadenzen, dessen Notenbeispiele ABC-Blöcke im Markdown sind, auf Spaltenbreite gestochen; eines davon läuft über zwei Seiten. · Stufe 2 (Mittel) · Lehrbücher
- [Nº 001 · Lehrbuch mit Randspalte](https://postext.dev/de/cookbook/textbook-margin-column.md): Anderthalb Spalten, die äußere nur für Gleitobjekte: Abbildungen mit span 'side' und Erläuterungen stapeln sich dort, captionSide holt die Legenden hinein. · Stufe 3 (Fortgeschritten) · Lehrbücher
