# 2段組みの和声教本に入れる番号付きコラールの譜例

> Humdrum **kernで書いた4声のコラールを、example型の楽譜リソースにします。「譜例4.1」と番号が付き、:refで引用され、図と同じように浮動します。

- HTML版: https://postext.dev/ja/cookbook/conservatory-method-chorales
- レシピ No. 166 · 図と画像 · 難易度 2 (中級) · 出力: Canvas
- ジャンル: 教科書
- 必要なもの postext ≥ 1.54.0 · テスト環境 1.54.0 ／テスト日 2026-10-22
- ページ: [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)
- Sandboxで開く: https://postext.dev/ja/sandbox#recipe=conservatory-method-chorales&lang=en (.postext: https://postext.dev/cookbook/conservatory-method-chorales/en/conservatory-method-chorales.postext)
- 最終更新: 2026-10-22
- 他の言語: [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), [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), [ar](https://postext.dev/ar/cookbook/conservatory-method-chorales.md)

## かんたんな説明

音楽学校の和声教本から、終止形を扱う章を2段組みで組みます。5つの譜例はテキストで書かれ、本物の楽譜として刻譜され、図と同じように番号とキャプションが付いて、それに触れた段落の近くに置かれます。

## できあがり

音楽学校の初年次向け和声教本『Four-Part Harmony』の第4章です。210 × 280 mmの判型に、Crimson Proで2段に組みます。課題は終止形で、5つの譜例はすべて2段の譜表に書いた4声のコラールです。バスの下にはローマ数字で度数を付けています。収めるのはト長調の5つの終止形、『Nun danket alle Gott』の3行、そしてジュネーヴ詩編第134編（*Old Hundredth*）の全曲です。各譜例にはExample 4.1から4.5（スペイン語版ではEjemplo）の番号が付き、ラベル用の書体のキャプションにワインレッドのラベルが付きます。譜例はそれを引用した段落のあとの段の天か地に置かれ、詩編の全曲は63ページの天に2段にまたがって組まれます。和声づけはこの教本のために書いたもので、平行5度・8度、声部間の間隔、導音の解決を確認しています。

**このレシピが答える問い:**

- 4声の譜例に番号を付け、2段組みで図のように浮動させるには？
- 番号付きキャプションのある図を加え、本文で参照するには（「図3.2を参照」）？
- 図の位置を制御するには（ページの上端、2段をまたぐ、まさにこの位置、余白）？

## 手短な答え

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

## 材料

**学べること**

- [番号付き譜例](https://postext.dev/ja/docs/music.md#番号付き譜例): 種類（kind）がscoreでタイプがexampleのリソースです。文書の言語で番号を振り（譜例3.2）、キャプションを付け、:refで参照し、図と同じように置くか浮動させます。1つの譜例は分割しません。
- [図の配置](https://postext.dev/ja/docs/document-format-resources.md#配置): 上、下、自動、その場のいずれかを、段の中かページ幅で、幅の割合も指定して、リソースごとまたは種類ごとに選びます。同じ番号系列のフロートが互いを追い越すことはありません。
- [図を配置する参照](https://postext.dev/ja/docs/document-format-resources.md#インライン参照基本の形): :refはリソースを参照し（「図3.2参照」）、最初の参照がその位置を決めます。図は参照の後の最初の空き位置にフロートします。

**ほかに使うもの**

- [楽譜の記譜](https://postext.dev/ja/docs/music.md#本文中の楽譜)
- [楽譜スタイル](https://postext.dev/ja/docs/configuration-resources.md#楽譜スタイル)
- [番号付きキャプション](https://postext.dev/ja/docs/document-format-resources.md#初出順の番号付け)
- [「図」と「表」を文書の言語で](https://postext.dev/ja/docs/configuration-resources.md#リソースの種類)
- [独自のリソースの種類](https://postext.dev/ja/docs/configuration-resources.md#リソースの種類)
- [キャプションのスタイル](https://postext.dev/ja/docs/configuration-resources.md#キャプションスタイル)
- [出典とクレジット](https://postext.dev/ja/docs/configuration-resources.md#キャプションスタイル)
- [1段組みと2段組み](https://postext.dev/ja/docs/configuration-page-layout.md#レイアウトの種類)
- [デザインした章扉](https://postext.dev/ja/docs/configuration-headings.md#幅と詳細デザイン)
- [見出しの属性](https://postext.dev/ja/docs/document-format.md#見出しの属性)
- [柱とノンブル](https://postext.dev/ja/docs/configuration-headers-footers.md#柱とノンブル)
- [見開きの余白](https://postext.dev/ja/docs/configuration-page-layout.md#見開きの余白)
- [ページの役割ごとの柱](https://postext.dev/ja/docs/configuration-headers-footers.md#テキスト要素)
- [囲み](https://postext.dev/ja/docs/configuration-callouts.md#囲みスタイル)
- [段落スタイル](https://postext.dev/ja/docs/configuration-styles.md#段落スタイル)
- [セマンティックカラーパレット](https://postext.dev/ja/docs/configuration-fonts-colors-viewers.md#カラーパレット)
- [その場に置く図](https://postext.dev/ja/docs/document-format-resources.md#ブロック埋め込み任意本文中への明示的な配置)
- [段抜きの章見出し帯](https://postext.dev/ja/docs/configuration-headings.md#幅と詳細デザイン)
- [上付きと下付き](https://postext.dev/ja/docs/document-format-inline.md#インライン書式)

**設定の一覧**

- [`bodyText`](https://postext.dev/ja/docs/configuration-text.md#本文), [`calloutStyles`](https://postext.dev/ja/docs/configuration-callouts.md#囲みスタイル), [`captionStyle`](https://postext.dev/ja/docs/configuration-resources.md#キャプションスタイル), [`colorPalette`](https://postext.dev/ja/docs/configuration-fonts-colors-viewers.md#カラーパレット), [`footer`](https://postext.dev/ja/docs/configuration-headers-footers.md#柱とノンブル), [`header`](https://postext.dev/ja/docs/configuration-headers-footers.md#柱とノンブル), [`headings`](https://postext.dev/ja/docs/configuration-headings.md#見出し), [`layout`](https://postext.dev/ja/docs/configuration-page-layout.md#レイアウト), [`locale`](https://postext.dev/ja/docs/configuration-language.md#ハイフネーション), [`orderedLists`](https://postext.dev/ja/docs/configuration-lists-math.md#番号付きリスト), [`page`](https://postext.dev/ja/docs/configuration-page-layout.md#ページ), [`paragraphStyles`](https://postext.dev/ja/docs/configuration-styles.md#段落スタイル), [`resourceTypes`](https://postext.dev/ja/docs/configuration-resources.md#リソースの種類), [`scoreStyle`](https://postext.dev/ja/docs/configuration-resources.md#楽譜スタイル), [`scoreStyles`](https://postext.dev/ja/docs/configuration-resources.md#楽譜スタイル), [`unorderedLists`](https://postext.dev/ja/docs/configuration-lists-math.md#箇条書きリスト)

**API**

- [`buildDocumentWithFonts`](https://postext.dev/ja/docs/configuration-programmatic-usage.md#レイアウトの前にフォントを読み込む), [`defaultResourceTypes`](https://postext.dev/ja/docs/configuration-resources.md#リソースの種類), [`initMusicEngine`](https://postext.dev/ja/docs/music.md#本文中の楽譜), [`renderPageToCanvas`](https://postext.dev/ja/docs/configuration-programmatic-usage.md#ページをビットマップに描画する)

**書体**

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

## 作り方

### 1 · コラールをHumdrum **kernで書く

```js
// script.js, 行 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は声部をタブ区切りの列で書き、いちばん低い声部から並べます。各譜表は1つの列（*spine*）で、`*^`で2つに分けます。ヘ音記号の譜表にテノールとバス、ト音記号の譜表にソプラノとアルトを置くので、刻譜エンジンは1つの譜表に2声部を符尾を分けて書きます（左の声部が上向き、右の声部が下向き）。バスの譜表のあとの`**harm`列が、その下に度数を印刷します。`Ib`は第1転回形、`Ic`は四六の和音です。スクリプトは列をスペースで書いてタブに変えるので、どのエディターでもファイルが壊れません。スペイン語版では、スペインの和声教本の書き方にならって度数をすべて大文字（VI、IIb）にします。

### 2 · 譜例をexample型のリソースにする

[手短な答え](https://postext.dev/ja/cookbook/conservatory-method-chorales.md#手短な答え)が5つのリソースを作ります。`typeId: 'example'`は組み込みの「譜例」型を使います。リソースがこの型を使うと、エンジンが本のリソース型に加え、文書の言語で名前を付け（`locale: 'es'`ならEjemplo）、章ごとに番号を振ります。そのため第4章の最初の譜例は4.1になります。楽譜リソースは分割されず、図と同じように浮動します。`position: 'top'`または`'bottom'`は引用のあとで空きのある次の段の天か地を取り、`span: 'page'`は*Old Hundredth*を2段にまたがせます。キャプション、出典、代替テキストは、版ごとの2つ目のMarkdownファイルから読みます。

### 3 · 段に入れる譜例の譜表を小さくする

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

ページ幅の2つのコラールは文書の`scoreStyle`に従い、譜表の高さは5.4 mm、各段の頭に小節番号が付きます。段に入れる譜例は名前付きスタイル`column`を選びます。譜表は4.6 mm、小節番号なし、間隔0.8で、譜例4.2の5小節を1段に収め、最後の1小節だけが2段目に残らないようにしています。

### 4 · 組版の前に刻譜エンジンを起動する

```js
// script.js, 行 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()`はパッケージから刻譜エンジンVerovioを読み込みます。起動するまで、楽譜はすべて灰色の箱として組まれます。`continuation`でこの章を長い本の第4章にしているので、譜例は4.1から4.5と番号が付き、ノンブルは奇数ページの61から始まります。

### 5 · 章扉を2段にまたがせる

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

章扉は2段にまたがり（第1レベルの見出しに`span: 'page'`）、柱見出しとリードを見出しの属性から取ります。`minHeight`で本文を46 mm下げるので、2つの段は罫の下の同じ行から始まります。

## レシピの全体

レシピのフォルダーから合成した1つのファイルで、サンプルのテキストとレシピ集の共通キットを埋め込んであり、自分でページを組み立てます。動かすには、空のページの`<script type="module">`に入れるか、新しいCodePenのpenのJSパネルに貼り付けます（モジュールとして）。esm.shからpostextを読み込むので、インストールもビルドも要りません。

- レシピのフォルダー: 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 ───────────────────────────────────────────────────────────────────────
```

## アレンジ

### 番号なしの譜例を本文に置く

コードブロックに書いた楽譜は、本文から引用されずにそのまま本文に置かれます。別行立ての数式と同じで、番号もキャプションも付きません。

```diff
- ::resource{id="ex-cadences"}
+ ```humdrum {style="column" alt="ト長調の5つの終止形"}
+ **kern	**harm	**kern
+ …
+ ```
```

### 「音楽譜例」と呼ぶ

別の名前にするには独自の型を定義し、`typeId`をそれに向けます。

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

## よくあるつまずき

- **::resource{id="…"}は二重引用符でしか書けない.** ブロックの埋め込みとして認識されるのは、二重引用符を使った::resource{id="…"}の形だけです。ほかの書き方は、目に見える1行として本文に残ります。

- 楽譜リソースは段やページをまたいで分割されません。浮動させる譜例は数小節にとどめ、長い曲はコードブロックに入れます。コードブロックなら段と段のあいだで分かれます。
- 刻譜エンジンは各段を置き場所の幅いっぱいに広げ、最後の段だけは自然な幅のままにします。譜例の1小節だけが2段目に残るときは、そのスタイルの`spacing`か譜表の大きさを下げます。

## クレジット

- レシピ: Ignacio Ferro ([@drnachio](https://github.com/drnachio))
- テキスト: Chapter text and the four-part settings of every example: Ignacio Ferro, CC-BY-4.0
- テキスト: Melody of Nun danket alle Gott (Praxis pietatis melica, 1647), read from the soprano of Bach's setting BWV 386: Johann Crüger ([出典](https://github.com/craigsapp/bach-370-chorales/blob/master/kern/chor032.krn)), パブリックドメイン
- テキスト: Melody of the Old Hundredth (Psalm 134 in the Genevan Psalter, 1551): Louis Bourgeois (attributed) ([出典](https://en.wikipedia.org/wiki/Old_100th)), パブリックドメイン
- 書体: Crimson Pro (OFL-1.1), Playfair Display (OFL-1.1), Fira Sans Condensed (OFL-1.1)
- コード: MIT · サンプルの内容: CC-BY-4.0

## 関連レシピ

- [No. 009 · 引用した位置へフロートする図](https://postext.dev/ja/cookbook/figures-float-where-cited.md): 番号付きの図が7点ある2段組みの章。6点は最初の:refから、配置の指定が許す最初の場所へフロートし、1点は::resourceを置いた位置に入ります。 · 難易度 3 (上級) · 教科書
- [No. 163 · ABCで譜例を書く和声の教科書](https://postext.dev/ja/cookbook/harmony-textbook-abc-examples.md): 終止を扱う和声の教科書の1章です。譜例はMarkdownに書いたABCのブロックで、段の幅に合わせて浄書され、そのうち1つは2ページにわたって続きます。 · 難易度 2 (中級) · 教科書
- [No. 001 · 余白に段を設けた教科書](https://postext.dev/ja/cookbook/textbook-margin-column.md): 小口側の段にフロートだけを入れる1段半組みのページです。span 'side'の図と傍注がそこに積み重なり、captionSideでほかの図のキャプションもそこへ移します。 · 難易度 3 (上級) · 教科書
