# 裁ち落としの帯を敷いた章扉

> レベル1見出しにadvancedDesignの章扉を付けます。裁ち落としの帯、その下端に立つ章番号、見出しの属性から取るキッカーとリード文。

- HTML版: https://postext.dev/ja/cookbook/chapter-opener-bleed-band
- レシピ No. 003 · 見出しと章扉 · 難易度 3 (上級) · 出力: Canvas
- ジャンル: 教科書
- 必要なもの postext ≥ 1.4.1 · テスト環境 1.4.1 ／テスト日 2026-09-26
- ページ: [41](https://postext.dev/cookbook/chapter-opener-bleed-band/en/p01.webp?v=1e51b4f6), [42](https://postext.dev/cookbook/chapter-opener-bleed-band/en/p02.webp?v=1e51b4f6), [43](https://postext.dev/cookbook/chapter-opener-bleed-band/en/p03.webp?v=1e51b4f6), [44](https://postext.dev/cookbook/chapter-opener-bleed-band/en/p04.webp?v=1e51b4f6)
- Sandboxで開く: https://postext.dev/ja/sandbox#recipe=chapter-opener-bleed-band&lang=en (.postext: https://postext.dev/cookbook/chapter-opener-bleed-band/en/chapter-opener-bleed-band.postext)
- 最終更新: 2026-09-26
- 他の言語: [en](https://postext.dev/en/cookbook/chapter-opener-bleed-band.md), [es](https://postext.dev/es/cookbook/chapter-opener-bleed-band.md), [ca](https://postext.dev/ca/cookbook/chapter-opener-bleed-band.md), [zh](https://postext.dev/zh/cookbook/chapter-opener-bleed-band.md), [ar](https://postext.dev/ar/cookbook/chapter-opener-bleed-band.md)

## かんたんな説明

地質学の教科書の章扉です。大きな章番号を載せた色の帯がページの上端からはみ出して刷られ、デザインは一度設定すればすべての章に使えます。

## できあがり

『Earth in Motion』という、210 × 280 mm判の地質学入門の教科書の章扉です。第3章は、ページ上端から裁ち落としにしたテラコッタ色の帯の下で始まります。帯には字間を広げたキッカー、著者が選んだ位置で改行したタイトル、イタリックのリード文、そして小口側で帯の濃い下端に立つ168 ptの章番号が載ります。帯の下から両端そろえの2段組みが始まり、沈み込むプレートの断面図がページの地を占めます。第4章は同じデザインをティール色で繰り返し、ツメ、節番号、参照、図のラベルまで色が変わります。デザインはレベル1見出しに一度だけ設定します。各章の見出し行が、キッカーの話題とリード文を属性として渡し、章が別の色を取るときはスタイルの名前を指定します。

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

- 裁ち落としの色帯、大きな章番号、2行のタイトルを持つ章扉はどう作りますか？
- 章タイトルをページ幅いっぱいに置き、その下で本文を2段組みにするには？
- 章扉に著者名の行、リード文、ドロップキャップ付きの導入文を加えるには？
- 章ごとに異なる写真、色、章扉のバリエーションを与えるには？
- 見出しに番号（1、1.1、1.1.1）を振り、レベルごとに異なるスタイルにするには？

## 手短な答え

```js
// script.js, 行 40–79
const TOP = 24; // top margin, mm: the opener's container starts here
const OUTER = 16; // outer margin, mm: the numeral and the folios line up on it
const BLEED = 0; // mm: 0 on screen; about 3 for print, which also switches on page.cutLines
const BAND = 100; // mm from the trim to the foot of the band
const NUMERAL = 168; // pt; the baseline of a design text sits 0.8 down its line box
const PT = 25.4 / 72; // mm in a point
const AIR = 12; // mm, foot to text: ≥ 0.2 × NUMERAL × PT ≈ 11.9, the numeral box below its baseline
const BEARING = 2; // mm past the text edge: a rough optical nudge, as each digit's bearing differs
const bleedTop = { to: 'bleed', edge: 'top-left' }; // the trim's corner, pushed out by BLEED
// Design text centres and cuts with '…' by default (gotcha: overflow-ellipsis-default).
const onBand = { color: col('paper'), align: 'left', overflow: 'wrap' };
const below = (id, y, width) => ({ anchor: { to: `#${id}`, edge: 'below' }, offset: { y: mm(y) },
  size: { width: mm(width) } }); // chained: a longer title pushes the standfirst down
const opener = { enabled: true,
  // The text starts below the lowest element or minHeight (gotcha: opener-reserves-anchored).
  minHeight: mm(BAND - TOP + AIR), // clears the numeral, so the text starts AIR below the foot
  slot: {
    elements: [ // array order is paint order: the band first, the type on top
      { kind: 'box', id: 'band', style: { backgroundColor: col('band') }, placement: {
        anchor: bleedTop, size: { height: mm(BLEED + BAND) } } }, // no width: runs to the far edge
      { kind: 'rule', id: 'foot', color: col('ink'), thickness: mm(1.6), placement: {
        anchor: bleedTop, offset: { y: mm(BLEED + BAND) } } },
      // {numberDecimal} is empty on headings (gotcha: heading-number-placeholders)
      { kind: 'text', id: 'numeral', content: '{chapterNumber}', ...onBand,
        fontFamily: 'Archivo', fontWeight: 800, fontSize: pt(NUMERAL),
        lineHeight: 1, // a multiple of the size, never pt() (gotcha: design-lineheight-multiple)
        placement: { anchor: { to: 'page', edge: 'top-right' }, // the fore-edge of a recto
          offset: { x: mm(BEARING - OUTER), y: mm(BAND - 0.8 * NUMERAL * PT) } } }, // on the foot
      { kind: 'text', id: 'kicker', ...onBand, fontFamily: 'Archivo Narrow', fontWeight: 600,
        content: `${t({ en: 'Chapter', es: 'Capítulo' })} {chapterNumber} · {attr.topic}`,
        fontSize: pt(8.5), letterSpacing: pt(2), textTransform: 'uppercase',
        placement: { anchor: { to: 'container', edge: 'top-left' }, offset: { y: mm(7) } } },
      { kind: 'text', id: 'title', content: '{titleText}', ...onBand, // breaks at the \\
        fontFamily: 'Archivo', fontWeight: 800, fontSize: pt(34), lineHeight: 1.02,
        placement: below('kicker', 3, 118) },
      { kind: 'text', id: 'lead', content: '{attr.lead}', ...onBand, fontFamily: 'Roboto Serif',
        italic: true, fontSize: pt(10.5), lineHeight: 1.36, placement: below('title', 6, 100) },
    ],
  },
}; // hook-up: headings.levels[0] = { span: 'page', breakBefore, advancedDesign: opener }
```

## 材料

**学べること**

- [デザインした章扉](https://postext.dev/ja/docs/configuration.md#幅と詳細デザイン): テキスト、罫、ボックス、画像を自由に組み合わせて描く見出しです。本文の上に必要な高さを確保します。
- [段抜きの章見出し帯](https://postext.dev/ja/docs/configuration.md#幅と詳細デザイン): すべての段にまたがる章扉の帯として組む見出しです。本文はその下に段組みで続きます。

**ほかに使うもの**

- [見出しの属性](https://postext.dev/ja/docs/document-format.md#見出しの属性)
- [タイトル内の改行](https://postext.dev/ja/docs/document-format.md#タイトル内の改行)
- [見出しスタイル](https://postext.dev/ja/docs/configuration.md#見出しスタイル)
- [部ごとの色](https://postext.dev/ja/docs/configuration.md#partコンテナー)
- [番号付き見出し](https://postext.dev/ja/docs/configuration.md#レベルごとの上書き)
- [奇数ページから始まる章](https://postext.dev/ja/docs/configuration.md#前で改ページ)
- [デザイン要素の基準位置](https://postext.dev/ja/docs/configuration.md#要素の配置)
- [ページデザインのテキスト・罫・ボックス](https://postext.dev/ja/docs/configuration.md#柱とノンブル)
- [裁ち落としの帯とツメ](https://postext.dev/ja/docs/configuration.md#要素の配置)
- [セマンティックカラーパレット](https://postext.dev/ja/docs/configuration.md#カラーパレット)
- [柱とノンブル](https://postext.dev/ja/docs/configuration.md#柱とノンブル)
- [ページの役割ごとの柱](https://postext.dev/ja/docs/configuration.md#テキスト要素)
- [図を配置する参照](https://postext.dev/ja/docs/document-format.md#インライン参照基本の形)
- [図の配置](https://postext.dev/ja/docs/document-format.md#配置)
- [リソースとしての図と表](https://postext.dev/ja/docs/document-format.md#リソース)
- [章ごとに組む本](https://postext.dev/ja/docs/configuration.md#バンドルを組んで描画する)
- [キャンバス上のページ](https://postext.dev/ja/docs/configuration.md#ページをビットマップに描画する)
- [裁ち落としとトンボ](https://postext.dev/ja/docs/configuration.md#トンボ)
- citations
- [「図」と「表」を文書の言語で](https://postext.dev/ja/docs/configuration.md#リソースの種類)
- [段落スタイル](https://postext.dev/ja/docs/configuration.md#段落スタイル)
- [独自のリソースの種類](https://postext.dev/ja/docs/configuration.md#リソースの種類)

**設定の一覧**

- [`bodyText`](https://postext.dev/ja/docs/configuration.md#本文), [`captionStyle`](https://postext.dev/ja/docs/configuration.md#キャプションスタイル), [`colorPalette`](https://postext.dev/ja/docs/configuration.md#カラーパレット), [`footer`](https://postext.dev/ja/docs/configuration.md#柱とノンブル), [`header`](https://postext.dev/ja/docs/configuration.md#柱とノンブル), [`headingStyles`](https://postext.dev/ja/docs/configuration.md#見出しスタイル), [`headings`](https://postext.dev/ja/docs/configuration.md#見出し), [`layout`](https://postext.dev/ja/docs/configuration.md#レイアウト), [`locale`](https://postext.dev/ja/docs/configuration.md#ハイフネーション), [`orderedLists`](https://postext.dev/ja/docs/configuration.md#番号付きリスト), [`page`](https://postext.dev/ja/docs/configuration.md#ページ), [`paragraphStyles`](https://postext.dev/ja/docs/configuration.md#段落スタイル), [`resourceTypes`](https://postext.dev/ja/docs/configuration.md#リソースの種類)

**API**

- [`buildDocument`](https://postext.dev/ja/docs/configuration.md#文書のビルド), [`clearMeasurementCache`](https://postext.dev/ja/docs/configuration.md#計測キャッシュ), [`defaultResourceTypes`](https://postext.dev/ja/docs/configuration.md#リソースの種類), [`registerResourceImage`](https://postext.dev/ja/docs/architecture.md#apiの概要), [`renderPageToCanvas`](https://postext.dev/ja/docs/configuration.md#ページをビットマップに描画する)

**書体**

- Roboto Serif (OFL-1.1), Archivo (OFL-1.1), Archivo Narrow (OFL-1.1)

## 作り方

### 1 · 帯と数字をページから吊るす

コードは上の[手短な答え](#手短な答え)にあります。帯とその下端は、裁ち落とし領域の左上の角から吊るします。幅を指定しないボックスや罫線はどれもそうですが、その枠の反対側の端まで伸びるので、仕上がり線まで届き、`BLEED`を設定すれば裁ち落としまで届きます。数字はページの右上の角から吊るします。`y: BAND - 0.8 * NUMERAL * PT`はベースラインを帯の下端に置き、`BEARING`は数字のサイドベアリングを補うためにボックスを2 mm余白側へ動かすので、インクがおおよそ本文の端にそろいます。キッカーはコンテナーから吊るします。タイトルはキッカーの下に、リード文はタイトルの下に、`overflow: 'wrap'`付きの`below`アンカーで吊るすので、タイトルが長くなるとリード文は下へ押されます。リード文は見出しの`lead`属性で埋まるテキスト要素です。著者名の行を加えるなら、もうひとつ`{attr.author}`を同じようにつなげます。`minHeight: BAND - TOP + AIR`は本文の始まる位置を決めます。数字のボックスは下端よりサイズの0.2倍（11.9 mm）下まで届き、章扉は最も低い要素までのスペースを確保するので、`AIR`は少なくとも11.9 mm必要です。それより小さいと、本文の始まる位置を数字のボックスが決めてしまいます。

### 2 · 章見出しにページを与える

```js
// script.js, 行 128–142
  headings: {
    fontFamily: 'Archivo', color: col('band'),
    levels: [
      // span: 'page' alone opens each chapter on the next page, whatever its side. The break,
      // which any headings object drops (gotcha: headings-drop-h1-break), is restated for its
      // parity: 'odd' keeps openers on rectos, so the numeral's right-edge anchor is the fore-edge.
      // The copy is fitted so no chapter spills a few lines onto a page before an odd opener:
      // 1.4.1 sets such a page's column shorter than its text and drops the last line.
      { level: 1, span: 'page', breakBefore: { enabled: true, parity: 'odd' },
        // 0: the hidden title's default 0.5 em would add a grid line, and AIR alone sets the gap
        marginBottom: pt(0), advancedDesign: opener },
      { level: 2, fontSize: pt(11.5), lineHeight: pt(LEAD), numberingTemplate: '{1}.{2}',
        marginTop: pt(LEAD), marginBottom: pt(0) }, // one grid line above, none below
    ],
  },
```

`span: 'page'`は各章を新しいページで始め、レベル1見出しを2段にまたがって組みます。`advancedDesign`は見出しのテキストの代わりに章扉を描き、本文は確保した高さの下から始まります。`span`だけでは、奇数ページか偶数ページかを問わず次のページを取ります。また`headings`オブジェクトがあると、奇数ページへ改ページするという既定の動作がなくなります。そこで`parity: 'odd'`で改ページを明示し直し、どの章扉も奇数ページに置きます。奇数ページなら、数字の右側のアンカーが小口になります。レベル2では、`numberingTemplate: '{1}.{2}'`が節を3.1、3.2…と帯の色で番号付けします。レベル3に`'{1}.{2}.{3}'`を付ければ、3.1.1へと続きます。

### 3 · 章の色に一度だけ名前を付ける

```js
// script.js, 行 17–36
const palette = {
  ink: '#1b1f23', // text: a cool near-black
  band: '#c2562b', // the chapter colour: band, tab, section numbers, references, captions
  band2: '#2e6f73', // the second chapter colour, a teal; the sea in Figure 3.1
  land: '#d5cbc0', // the crust in the drawings
  muted: '#6b6259', // running heads and the colophon
  paper: '#ffffff', // type on the band
};
// col(id): a palette-linked colour. It carries the hex too, because 1.4.1 paints design
// elements and the reference colour from the hex (gotcha: palette-skips-designs).
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' } })),
  // The engine's defaults link to 'main-color': point it at the band, so nothing prints blue.
  { id: 'main-color', name: 'band (defaults)', value: { hex: palette.band, model: 'hex' } },
];
// {style="teal"} on a heading line swaps 'band' up to the next level-1 heading: the style's
// palette reaches design slots through their link and repaints every text colour equal to
// the base 'band'. (Swapping colorPalette itself would not reach the design slots.)
const headingStyles = [{ id: 'teal', palette: { band: palette.band2 } }];
```

設定の中の色は、すべてこれらの項目のどれかにリンクしています。エンジン自身の既定値は`main-color`にリンクしているので、この項目を帯の色に向けると、設定で指定していない既定の色もすべて変わります。第4章の見出し行`# Fire from \\ Below {style="teal" topic="Volcanoes" lead="…"}`はスタイルを選び、次のレベル1見出しまで`band`を入れ替えます。デザインの各要素はパレットのリンクを通じてこの入れ替えに従い、基本の`band`と同じ文字色も塗り直されます。そのため[43ページと44ページ](https://postext.dev/cookbook/chapter-opener-bleed-band/en/p03.webp?v=1e51b4f6)では、ツメ、節とリストの番号、参照、キャプションのラベル、下ノンブルがティール色になります。同じ行が`{attr.topic}`と`{attr.lead}`を埋め、その中の`\\`が`{titleText}`を著者の望む位置で改行します（[見出しの属性](/ja/docs/document-format#見出しの属性)、[タイトル内の改行](/ja/docs/document-format#タイトル内の改行)）。

### 4 · 図は最初の参照に置き場所を決めさせる

```js
// script.js, 行 321–336
const figure = (id, placement, caption) => ({ id, typeId: 'figure', kind: 'svg', createdAt: 0,
  updatedAt: 0, svg: { fileId: `${id}.svg`, width: ART[id].width, height: ART[id].height },
  placement, caption, altText: ALT[id] }); // altText: read aloud in a PDF or HTML edition
const resources = [
  // 'auto' may take the citing page's foot, 'top' never can (gotcha: top-float-next-page)
  figure('plate-cycle', { position: 'auto', span: 'page' },
    t({ en: 'A plate’s life in cross-section: born at a ridge (left), it cools as it drifts away '
      + 'and sinks under a continent; dots mark earthquakes.',
    es: 'La vida de una placa, en corte: nace en la dorsal (izquierda), se enfría al alejarse y '
      + 'se hunde bajo el continente; los puntos son terremotos.' })),
  figure('two-volcanoes', { position: 'bottom', span: 'page' }, // under the text on a last page
    t({ en: 'Two volcanoes in cross-section: a broad shield built of runny basalt flows (left) '
      + 'and a steep stratovolcano of lava and ash layers (right).',
    es: 'Dos volcanes en corte: un escudo ancho, de coladas de basalto fluido (izquierda), y un '
      + 'estratovolcán de capas de lava y ceniza (derecha).' })),
];
```

`:ref`は図に番号を付け、それを参照する段落の後の最初の空きスロットに置きます。図3.1は`auto`です。両方の段にまだ余裕があるうちに章扉で参照されるので、そのページの地を取ります。`top`のフロートなら42ページの頭に置かれていたはずです。フロートは自分の参照より上には決して行かないからです。図4.1は43ページの第2段の下のほうで参照され、そこにはもうページ幅の帯が収まらないので、`bottom`が44ページへ送ります。44ページは章の最後のページで、最後のページでは、エンジンが地のフロートを持ち上げ、上に隙間を空けずに本文の最終行の直下に置きます。

### 5 · 柱、ツメ、下ノンブル

```js
// script.js, 行 83–112
const HEAD = 15.5; // mm from the trim to the folio's and the head's baseline: 8.5 above TOP
const INSET = 9; // mm from the folio's edge to the head: room for a three-digit folio
const TAB = { y: 36, w: 9, h: 26 }; // the thumb tab, mm: its top, its width, its height
const tabSize = (w) => ({ width: mm(w), height: mm(TAB.h) });
const baseline = (size) => mm(HEAD - 0.96 * size * PT); // box: 1.2 × size, baseline 0.8 down
const label = { fontFamily: 'Archivo Narrow', fontSize: pt(7.5), fontWeight: 600,
  letterSpacing: pt(1.3), textTransform: 'uppercase', color: col('muted') };
const folio = { fontFamily: 'Archivo', fontSize: pt(9), fontWeight: 800, color: col('ink') };
const side = (parity) => { // the fore-edge is a verso's left edge and a recto's right edge
  const [edge, s] = parity === 'even' ? ['left', 1] : ['right', -1]; // s: into the page
  const at = (to, x, y, size) => ({ anchor: { to, edge: `top-${edge}` },
    offset: { x: mm(s * x), y }, ...(size && { size }) });
  return [
    { kind: 'text', id: `folio-${parity}`, content: '{pageNumber}', ...folio,
      placement: at('page', OUTER, baseline(9)) },
    // Textbooks name the chapter on the verso and the section on the recto; 1.4.1 has no
    // section placeholder, so the recto names the book.
    { kind: 'text', id: `head-${parity}`, content: s > 0 ? '{chapterTitle}' : '{title}',
      ...label, placement: at('page', OUTER + INSET, baseline(7.5)) },
    { kind: 'box', id: `tab-${parity}`, style: { backgroundColor: col('band') }, // off the trim
      placement: at('bleed', 0, mm(BLEED + TAB.y), tabSize(BLEED + TAB.w)) },
    { kind: 'text', id: `tab-no-${parity}`, content: '{chapterNumber}', ...folio, fontSize: pt(13),
      color: col('paper'), // design text centres by default: here, on the trimmed tab
      placement: at('page', 0, mm(TAB.y), tabSize(TAB.w)) },
  ].map((element) => ({ ...element, parity, pages: 'body' })); // never on an opener
};
const header = { elements: [...side('even'), ...side('odd')] };
const footer = { elements: [{ kind: 'text', id: 'drop-folio', content: '{pageNumber}',
  ...folio, color: col('band'), pages: 'opener', // centred by its anchor
  placement: { anchor: { to: 'container', edge: 'top' }, offset: { y: mm(10) } } }] };
```

ヘッダーの各要素はページの上の角から吊るし、`parity`で絞り込むので、ノンブルとツメは常に小口側に来ます。ツメのボックスは代わりに裁ち落とし領域の角から吊るし、`BLEED`に応じて幅が広がるので、断裁後も裁ち落としのままです。`pages: 'body'`はこれらを章扉から外します。章扉にはフッターから下ノンブルが付きます。偶数ページの柱は`{chapterTitle}`で章名を1行で示します。`\\`がタイトルを改行するのは章扉の中だけだからです。教科書では奇数ページの柱に節名を入れるのが普通です。Postext 1.4.1には節名のプレースホルダーがないので、奇数ページの柱には本の`{title}`を印刷します。この4ページでは、奇数ページが2つとも章扉なので、それが見えることはありません。

### 6 · 本を第3章から始める

```js
// script.js, 行 348–353
const continuation = { pageNumbering: { startAt: 41 }, // an odd folio: page 1 is still a recto
  headings: { h1: 2, h2: 0, h3: 0, h4: 0, h5: 0, h6: 0 } }; // the next # is chapter 3
const doc = await buildWithFonts(
  () => buildDocument({ markdown, resources, continuation }, config()), markdown);
showPages(doc, { title: t({ en: 'Chapter opener on a full-bleed band',
  es: 'Apertura de capítulo sobre banda a sangre' }) });
```

この4ページは長い本の第3章と第4章なので、`buildDocument`には、第1章と第2章が終わった時点のカウンターを持つ`continuation`を渡します。その`headings`に`h1: 2`があるので、次の`#`は第3章になります。`{chapterNumber}`は3を印刷し、節は3.1から番号付けされ、最初の図は3.1になります。`pageNumbering.startAt: 41`はノンブルを41から始めます。偶奇は物理的なページで数え、`pageIndexOffset`を0のままにしているので、最初のページはノンブルが何であっても奇数ページのままです。それに合わせて、最初のノンブル41も奇数にしています。

## レシピの全体

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

- レシピのフォルダー: https://github.com/drnachio/postext/tree/main/cookbook/chapter-opener-bleed-band

### script.js

```js
// ═══ Postext Cookbook · Nº 003 · Chapter opener on a full-bleed band ══════════════
// https://postext.dev/en/cookbook/chapter-opener-bleed-band
// Code: MIT · Text: original (CC BY 4.0) · Drawings: generated in code (CC BY 4.0)
// Fonts: Roboto Serif, Archivo, Archivo Narrow (SIL OFL 1.1) · Needs postext ≥ 1.4.1
// Two chapters of a geology textbook. Every level-1 heading becomes a colour band bled off the
// top of the page, its number standing on the band's foot; a heading style recolours chapter 4.
import {
  buildDocument, renderPageToCanvas, clearMeasurementCache, registerResourceImage,
  defaultResourceTypes,
} from 'https://esm.sh/postext';

const LANG = 'en'; // @lang: the language of the sample document ('en' | 'es')
const RECIPE = 'chapter-opener-bleed-band';

// ─── 1 · Design ─────────────────────────────────────────────────────────────
// #region palette: semantic colours, and the style that swaps a chapter's
const palette = {
  ink: '#1b1f23', // text: a cool near-black
  band: '#c2562b', // the chapter colour: band, tab, section numbers, references, captions
  band2: '#2e6f73', // the second chapter colour, a teal; the sea in Figure 3.1
  land: '#d5cbc0', // the crust in the drawings
  muted: '#6b6259', // running heads and the colophon
  paper: '#ffffff', // type on the band
};
// col(id): a palette-linked colour. It carries the hex too, because 1.4.1 paints design
// elements and the reference colour from the hex (gotcha: palette-skips-designs).
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' } })),
  // The engine's defaults link to 'main-color': point it at the band, so nothing prints blue.
  { id: 'main-color', name: 'band (defaults)', value: { hex: palette.band, model: 'hex' } },
];
// {style="teal"} on a heading line swaps 'band' up to the next level-1 heading: the style's
// palette reaches design slots through their link and repaints every text colour equal to
// the base 'band'. (Swapping colorPalette itself would not reach the design slots.)
const headingStyles = [{ id: 'teal', palette: { band: palette.band2 } }];
// #endregion

// #region answer: the opener: a bleed band, the numeral on its foot, texts from the heading
const TOP = 24; // top margin, mm: the opener's container starts here
const OUTER = 16; // outer margin, mm: the numeral and the folios line up on it
const BLEED = 0; // mm: 0 on screen; about 3 for print, which also switches on page.cutLines
const BAND = 100; // mm from the trim to the foot of the band
const NUMERAL = 168; // pt; the baseline of a design text sits 0.8 down its line box
const PT = 25.4 / 72; // mm in a point
const AIR = 12; // mm, foot to text: ≥ 0.2 × NUMERAL × PT ≈ 11.9, the numeral box below its baseline
const BEARING = 2; // mm past the text edge: a rough optical nudge, as each digit's bearing differs
const bleedTop = { to: 'bleed', edge: 'top-left' }; // the trim's corner, pushed out by BLEED
// Design text centres and cuts with '…' by default (gotcha: overflow-ellipsis-default).
const onBand = { color: col('paper'), align: 'left', overflow: 'wrap' };
const below = (id, y, width) => ({ anchor: { to: `#${id}`, edge: 'below' }, offset: { y: mm(y) },
  size: { width: mm(width) } }); // chained: a longer title pushes the standfirst down
const opener = { enabled: true,
  // The text starts below the lowest element or minHeight (gotcha: opener-reserves-anchored).
  minHeight: mm(BAND - TOP + AIR), // clears the numeral, so the text starts AIR below the foot
  slot: {
    elements: [ // array order is paint order: the band first, the type on top
      { kind: 'box', id: 'band', style: { backgroundColor: col('band') }, placement: {
        anchor: bleedTop, size: { height: mm(BLEED + BAND) } } }, // no width: runs to the far edge
      { kind: 'rule', id: 'foot', color: col('ink'), thickness: mm(1.6), placement: {
        anchor: bleedTop, offset: { y: mm(BLEED + BAND) } } },
      // {numberDecimal} is empty on headings (gotcha: heading-number-placeholders)
      { kind: 'text', id: 'numeral', content: '{chapterNumber}', ...onBand,
        fontFamily: 'Archivo', fontWeight: 800, fontSize: pt(NUMERAL),
        lineHeight: 1, // a multiple of the size, never pt() (gotcha: design-lineheight-multiple)
        placement: { anchor: { to: 'page', edge: 'top-right' }, // the fore-edge of a recto
          offset: { x: mm(BEARING - OUTER), y: mm(BAND - 0.8 * NUMERAL * PT) } } }, // on the foot
      { kind: 'text', id: 'kicker', ...onBand, fontFamily: 'Archivo Narrow', fontWeight: 600,
        content: `${t({ en: 'Chapter', es: 'Capítulo' })} {chapterNumber} · {attr.topic}`,
        fontSize: pt(8.5), letterSpacing: pt(2), textTransform: 'uppercase',
        placement: { anchor: { to: 'container', edge: 'top-left' }, offset: { y: mm(7) } } },
      { kind: 'text', id: 'title', content: '{titleText}', ...onBand, // breaks at the \\
        fontFamily: 'Archivo', fontWeight: 800, fontSize: pt(34), lineHeight: 1.02,
        placement: below('kicker', 3, 118) },
      { kind: 'text', id: 'lead', content: '{attr.lead}', ...onBand, fontFamily: 'Roboto Serif',
        italic: true, fontSize: pt(10.5), lineHeight: 1.36, placement: below('title', 6, 100) },
    ],
  },
}; // hook-up: headings.levels[0] = { span: 'page', breakBefore, advancedDesign: opener }
// #endregion

// #region heads: folios and heads on body pages, a thumb tab that bleeds, drop folios
const HEAD = 15.5; // mm from the trim to the folio's and the head's baseline: 8.5 above TOP
const INSET = 9; // mm from the folio's edge to the head: room for a three-digit folio
const TAB = { y: 36, w: 9, h: 26 }; // the thumb tab, mm: its top, its width, its height
const tabSize = (w) => ({ width: mm(w), height: mm(TAB.h) });
const baseline = (size) => mm(HEAD - 0.96 * size * PT); // box: 1.2 × size, baseline 0.8 down
const label = { fontFamily: 'Archivo Narrow', fontSize: pt(7.5), fontWeight: 600,
  letterSpacing: pt(1.3), textTransform: 'uppercase', color: col('muted') };
const folio = { fontFamily: 'Archivo', fontSize: pt(9), fontWeight: 800, color: col('ink') };
const side = (parity) => { // the fore-edge is a verso's left edge and a recto's right edge
  const [edge, s] = parity === 'even' ? ['left', 1] : ['right', -1]; // s: into the page
  const at = (to, x, y, size) => ({ anchor: { to, edge: `top-${edge}` },
    offset: { x: mm(s * x), y }, ...(size && { size }) });
  return [
    { kind: 'text', id: `folio-${parity}`, content: '{pageNumber}', ...folio,
      placement: at('page', OUTER, baseline(9)) },
    // Textbooks name the chapter on the verso and the section on the recto; 1.4.1 has no
    // section placeholder, so the recto names the book.
    { kind: 'text', id: `head-${parity}`, content: s > 0 ? '{chapterTitle}' : '{title}',
      ...label, placement: at('page', OUTER + INSET, baseline(7.5)) },
    { kind: 'box', id: `tab-${parity}`, style: { backgroundColor: col('band') }, // off the trim
      placement: at('bleed', 0, mm(BLEED + TAB.y), tabSize(BLEED + TAB.w)) },
    { kind: 'text', id: `tab-no-${parity}`, content: '{chapterNumber}', ...folio, fontSize: pt(13),
      color: col('paper'), // design text centres by default: here, on the trimmed tab
      placement: at('page', 0, mm(TAB.y), tabSize(TAB.w)) },
  ].map((element) => ({ ...element, parity, pages: 'body' })); // never on an opener
};
const header = { elements: [...side('even'), ...side('odd')] };
const footer = { elements: [{ kind: 'text', id: 'drop-folio', content: '{pageNumber}',
  ...folio, color: col('band'), pages: 'opener', // centred by its anchor
  placement: { anchor: { to: 'container', edge: 'top' }, offset: { y: mm(10) } } }] };
// #endregion

const LEAD = 13.2; // body leading in pt: the baseline grid
const config = () => ({ // a factory: configs are cached by identity (gotcha: config-cache-identity)
  locale: t({ en: 'en-us', es: 'es' }), // exact codes only (gotcha: hyphenation-locales)
  resourceTypes: defaultResourceTypes(LANG), // "Figura" in Spanish (gotcha: resource-types-locale)
  colorPalette, headingStyles,
  page: { width: mm(210), height: mm(280), dpi: 150, // a textbook trim; 150 dpi is for the screen
    cutLines: { enabled: BLEED > 0, bleed: mm(BLEED) }, // bleed and crop marks once BLEED is set
    margins: { top: mm(TOP), bottom: mm(22), left: mm(20), right: mm(OUTER), mirror: true } },
  layout: { layoutType: 'double', gutterWidth: mm(8) },
  bodyText: { fontFamily: 'Roboto Serif', fontSize: pt(9.3), lineHeight: pt(LEAD),
    color: col('ink'), boldColor: col('ink'), italicColor: col('ink'), referenceColor: col('band'),
    textAlign: 'justify', firstLineIndent: mm(4), indentAfterHeading: false }, // book texture
  // #region levels: the H1 takes the page and draws the opener; sections number in the band
  headings: {
    fontFamily: 'Archivo', color: col('band'),
    levels: [
      // span: 'page' alone opens each chapter on the next page, whatever its side. The break,
      // which any headings object drops (gotcha: headings-drop-h1-break), is restated for its
      // parity: 'odd' keeps openers on rectos, so the numeral's right-edge anchor is the fore-edge.
      // The copy is fitted so no chapter spills a few lines onto a page before an odd opener:
      // 1.4.1 sets such a page's column shorter than its text and drops the last line.
      { level: 1, span: 'page', breakBefore: { enabled: true, parity: 'odd' },
        // 0: the hidden title's default 0.5 em would add a grid line, and AIR alone sets the gap
        marginBottom: pt(0), advancedDesign: opener },
      { level: 2, fontSize: pt(11.5), lineHeight: pt(LEAD), numberingTemplate: '{1}.{2}',
        marginTop: pt(LEAD), marginBottom: pt(0) }, // one grid line above, none below
    ],
  },
  // #endregion
  orderedLists: { fontFamily: 'Archivo', color: col('band'), // numbers bold by default
    marginTop: pt(0), marginBottom: pt(0) },
  captionStyle: { fontFamily: 'Archivo Narrow', fontSize: pt(8.5), labelColor: col('band'),
    gap: mm(2.2) }, // the text in bodyText's ink, the label bold in the chapter colour
  paragraphStyles: [{ id: 'colophon', fontFamily: 'Archivo Narrow', fontSize: pt(7.5),
    lineHeight: pt(10), color: col('muted'), textAlign: 'left', firstLineIndent: pt(0),
    marginTop: pt(LEAD) }],
  header, footer,
});

// ─── 2 · Content ────────────────────────────────────────────────────────────
const markdown = String.raw`---
title: "Earth in Motion"
subtitle: "An Introduction to Geology"
---

# The Restless \\ Crust {topic="Plate tectonics" lead="The ground under your feet is moving at about the speed your fingernails grow. This chapter follows the plates that carry the continents and the oceans, and the seams where they part, collide and grind past each other."}

The Earth’s surface is a mosaic of rigid plates, some of them larger than a continent, that move slowly but without pause over the hotter rock beneath them. New sea floor forms along the ridges in the middle of the oceans and is destroyed where it sinks back into the mantle at the deep trenches (:ref{id="plate-cycle"}). The continents travel with the plates like luggage on a conveyor belt, and nearly every earthquake, volcano and mountain range on the planet lies where two plates meet.

The idea took a long time to be accepted. In 1915 the German meteorologist Alfred Wegener noticed that the coasts on either side of the Atlantic fit together like the pieces of a jigsaw, and argued that the continents had once been joined in a single landmass. He pointed to fossils of the same land reptiles on both sides of the ocean, rock formations that matched across it and the scratches of ancient glaciers in what are now the tropics. What he could not find was a force strong enough to push a continent through solid rock, and for half a century most geologists rejected the idea. The answer came from the ocean floor, where surveys after the Second World War revealed long undersea mountain chains, and rocks that grow steadily older with distance from their crests.

## A planet in pieces

The outer shell of the Earth, the lithosphere, is cold and rigid. It takes in the crust and the uppermost part of the mantle, and it is about a hundred kilometres thick: much more under the old cores of the continents, much less under young oceans. Beneath it lies the asthenosphere, rock that is solid but hot enough to flow, very slowly, like warm wax. The lithosphere is broken into seven large plates and a dozen or so smaller ones, and each of them moves as a single slab over the softer layer below.

Plates carry two kinds of crust. Oceanic crust is thin, about seven kilometres, and made of dark, dense basalt. Continental crust is 30 to 50 kilometres thick and made largely of lighter, granite-like rock. When plates meet, the denser ocean floor sinks into the mantle and the continent stays on top. That is why the oldest ocean floor anywhere is only about 180 million years old, while rocks on the continents reach back four billion years.

Plates move between one and fifteen centimetres a year. At five centimetres a year a plate travels fifty kilometres in a million years, and the Atlantic has opened by several thousand kilometres since the age of the dinosaurs.

## Where plates meet

Geologists recognise three kinds of plate boundary, named for the way the plates move against each other:

1. **Divergent boundaries**, where plates pull apart and magma rises to fill the gap. Most lie along the mid-ocean ridges; Iceland is a piece of the Mid-Atlantic Ridge that stands above the sea.
2. **Convergent boundaries**, where plates collide. When ocean floor meets a continent, the denser plate slides beneath the other and sinks into the mantle, a process called *subduction*. When two continents meet, neither sinks, and the crust crumples into ranges such as the Himalaya.
3. **Transform boundaries**, where plates slide past each other sideways: they lock, strain and then slip again. The San Andreas Fault in California is the best-known example on land.

Each kind has its own pattern of earthquakes and volcanoes. Where plates part, the earthquakes are shallow and the eruptions quiet, pouring out runny basalt. Where they collide, they cause the most powerful earthquakes on Earth, open deep trenches and raise chains of explosive volcanoes. Where they slide past each other, earthquakes are frequent but volcanoes are almost absent, because crust is neither made nor destroyed there.

## Reading the evidence

The clearest evidence for plate tectonics comes from the magnetism of the sea floor. As basalt cools at a ridge, tiny crystals of magnetite line up with the Earth’s magnetic field, like compass needles frozen in place. The field reverses its direction every few hundred thousand years on average, so the sea floor records the reversals as stripes of normal and reversed magnetism. In 1963 Fred Vine and Drummond Matthews explained the stripes, and by 1966 surveys had shown that those on either side of a ridge are mirror images of each other, which is what you would expect if new floor forms at the crest and moves away in both directions.

The earthquake record agrees. On a map, earthquakes fall in narrow belts along the plate boundaries. Beneath the trenches they fall on an inclined plane that dips under the neighbouring plate to depths of 700 kilometres, marking the path of the sinking slab.

Chains of volcanic islands add a third line of evidence. The Hawaiian Islands grew over a hot spot, a plume of unusually hot rock rising from deep in the mantle that stays put while the plate slides over it. Each volcano is carried away from the plume in turn and dies, and a new one grows in its place. The volcanoes line up like a trail of footprints: the Big Island still erupts, Kauai is about five million years old, and the drowned volcanoes beyond Midway are older still. The direction and the spacing of the chain record where the Pacific Plate has gone, and how fast.

Today the motion can be measured directly. GPS receivers bolted to bedrock on every continent fix their own positions to within a millimetre or two, and they show Hawaii creeping north-west towards Japan and Australia moving so fast, about seven centimetres a year, that the country has to revise its official coordinates every few decades.

Wegener thought that the continents ploughed through the rock of the ocean floor. In fact they ride on the plates, and the plates are driven by the slow escape of heat from the Earth’s interior. The next chapter turns to the places where that heat reaches the surface.

# Fire from \\ Below {style="teal" topic="Volcanoes" lead="Volcanoes are where the heat of the Earth’s interior reaches the surface. This chapter asks why solid rock melts, why some magma pours out quietly while some explodes, and how the shape of a mountain records the way it erupts."}

About 1,350 volcanoes on land are considered potentially active, meaning that they have erupted in the last 10,000 years, and many more lie hidden on the sea floor. Three quarters of the active ones stand around the rim of the Pacific Ocean, the so-called Ring of Fire, above the subduction zones described in the previous chapter (:ref{id="plate-cycle"}). Most of the others line the mid-ocean ridges or sit over isolated hot spots such as Hawaii.

Every volcano is a pile of its own eruptions. Each one adds a layer of lava or ash to the slopes, so the shape of the mountain records how it has erupted over thousands of years. Its eruptions, in turn, depend on where and how its magma formed.

## Where magma comes from

The mantle is almost entirely solid. It melts only when something upsets the balance between temperature and pressure, and there are three ways that happens. At ridges and hot spots, hot rock rises and the pressure on it falls; lower pressure lowers the melting point, so some of the rock melts without getting any hotter. This is *decompression melting*. At subduction zones, the sinking slab releases water into the hot mantle above it, and water, like salt on an icy road, lowers the melting point. This is *flux melting*, and it feeds the volcanoes of the Andes, Japan and the Cascades. Finally, a body of hot magma can melt the crust around it, adding to its own volume and changing its chemistry.

Molten rock is lighter than the rock around it, so it rises and collects in magma chambers a few kilometres below the surface. There it may cool slowly into granite or gabbro and never erupt at all, or it may find a way out through the central conduit or through a crack in the side of the mountain.

## Quiet and violent eruptions

Whether an eruption oozes or explodes depends mostly on two things: how much silica the magma holds and how much gas is dissolved in it. Silica makes magma thick. Basalt, about half silica by weight, is runny, and its gas escapes easily; its eruptions send out rivers of lava, as on Hawaii and Iceland, and build broad, gently sloping shield volcanoes. Magmas richer in silica, such as andesite and rhyolite, are so stiff that the gas cannot get out. Pressure builds until the magma shatters into ash and pumice, and the volcano blows apart.

The cone volcanoes of subduction zones, called *stratovolcanoes*, record both kinds of behaviour. They are built of alternating layers of lava and ash, each layer an eruption, which is why their slopes are steep and their outlines so regular (:ref{id="two-volcanoes"}). Mount Fuji is one. Mount St Helens had the same outline until 18 May 1980, when the north side of the mountain collapsed in a landslide and the sudden release of pressure drove a sideways blast across 600 square kilometres of forest.

The largest explosive eruptions reach far beyond their own region. In 1991 Mount Pinatubo in the Philippines injected about fifteen million tonnes of sulphur dioxide into the stratosphere. There it formed a veil of fine droplets that reflected sunlight back into space and lowered the average temperature of the whole planet by about half a degree Celsius for more than a year.

Volcanologists compare eruptions on the Volcanic Explosivity Index, a scale from 0 to 8 based mainly on the volume of rock and ash thrown out, on which most steps are ten times larger than the one below. The lava fountains of Kilauea rate 0 or 1, Mount St Helens rated 5 and Pinatubo 6. In 1815 Tambora, in Indonesia, rated 7, and the year that followed was remembered in Europe and North America as the year without a summer.

## Living with volcanoes

Some 800 million people live within a hundred kilometres of an active volcano, drawn by fertile soils and geothermal heat, or because their families have always lived there. Most volcanoes give warning before they erupt: swarms of small earthquakes as magma forces its way upwards, a swelling of the ground that satellites can measure to the centimetre, and changes in the gases that seep from the summit. Observatories that watch for these signs have given many towns time to evacuate.

In Iceland, geothermal heat from young volcanic rock warms about nine homes in every ten and generates nearly a third of the country’s electricity, and the richest farmland of Java and southern Italy lies on weathered volcanic ash.

Most lava flows move slowly enough for people to walk away; they bury houses, roads and fields but seldom take lives. Far more people have died in pyroclastic flows, fast, scorching avalanches of gas, ash and rock that can race down a mountainside at more than a hundred kilometres an hour, and in mudflows, which form when an eruption melts snow or when rain soaks loose ash.

Scientists can say that an eruption is becoming more likely, but they can rarely say when it will happen or how large it will be, and a false alarm has its own cost when a city has to be emptied. Around Naples, where more than half a million people live in the zone most exposed to Vesuvius, the emergency plan counts on about three days of warning to move them all to safety.

## Check your understanding

1. Why does ocean floor sink at a subduction zone while a continent stays at the surface?
2. Explain how adding water to hot mantle rock can make it melt without heating it.
3. Mount Fuji and Mauna Loa are both volcanoes. Why is one steep and the other broad?
4. What signs warn volcanologists that magma is rising towards the surface?
5. Why can a single large eruption lower the temperature of the whole planet for a year?

:::paragraphs{style="colophon"}
From *Earth in Motion*, an imaginary textbook. Set in Roboto Serif, Archivo and Archivo Narrow (SIL OFL) · Text and drawings: original, CC BY 4.0.
:::
`; // content.<lang>.md, inlined by the Cookbook

// #region art: two drawings made of shapes only; their labels live in the captions
// No text in the SVG: an SVG image cannot use the page's web fonts (gotcha: svg-no-webfonts).
const PX = 10; // SVG pixels per viewBox unit: 348 units = 174 mm, so about 500 dpi in print
const svg = (w, h, body) => ({ width: w * PX, height: h * PX, // the size the resource declares
  markup: `<svg xmlns="http://www.w3.org/2000/svg" width="${w * PX}" height="${h * PX}" `
    + `viewBox="0 0 ${w} ${h}">${body}</svg>` });
const fill = (id, a = 1) => `fill="${palette[id]}" fill-opacity="${a}"`;
const path = (d, paint) => `<path d="${d}" ${paint}/>`;
const poly = (pts, paint) => path(`M${pts.join('L').replaceAll(',', ' ')}Z`, paint);
const dot = (x, y, r, paint) => `<circle cx="${x}" cy="${y}" r="${r}" ${paint}/>`;
const plume = (x, y, w, h, paint, m = y + h / 2, f = y + h * 0.8) => path(`M${x} ${y}`
  + `C${x} ${m} ${x + w} ${m} ${x + w} ${f}A${w} ${h / 5} 0 0 1 ${x - w} ${f}`
  + `C${x - w} ${m} ${x} ${m} ${x} ${y}Z`, paint); // a teardrop of magma, its tip at (x, y)
const arrow = (x, y, dx, id, a = 1, turn = 0, s = Math.sign(dx)) => path( // never a <marker>
  `M${x} ${y - 0.8}h${dx - s * 5}v-1.8l${s * 5} 2.6l${-s * 5} 2.6v-1.8H${x}Z`,
  `${fill(id, a)} transform="rotate(${turn} ${x} ${y})"`);

// Each drawing takes its chapter's colour: a picture's colours are fixed when it is drawn.
function plateCycle(hot) { // a plate is born at a ridge and sinks under a continent
  const top = [[0, 30], [40, 23], [80, 30], [160, 32], [212, 34], [222, 41], [254, 54],
    [300, 74], [348, 96]]; // the sea floor, then the top of the sinking slab
  const under = (d) => top.map(([x, y], i) => [x, y + (d[i] ?? d.at(-1))]);
  const back = (points) => [...points].reverse();
  const crust = under([5]);
  const lith = under([9, 4, 11, 15]);
  const quakes = [226, 239, 252, 265, 278, 291, 304, 317, 330].map((x, i) => // in the slab
    dot(x, 40 + (x - 222) * 0.44 + (i % 3), 1.5, `${fill('ink')} stroke="${palette.paper}" `
      + 'stroke-width=".6"'));
  return svg(348, 102, `<rect y="24" width="348" height="80" ${fill(hot, 0.15)}/>`
    + poly([[0, 16], [238, 16], [222, 41], ...back(top.slice(0, 5))], fill('band2', 0.3))
    + poly([...crust, ...back(lith)], fill('muted', 0.45)) // the plate
    + poly([...top, ...back(crust)], fill('ink', 0.82)) // its oceanic crust
    + poly([[222, 41], [238, 16], [262, 15], [270, 10], [277, 5], [281, 5], [288, 10],
      [296, 14], [318, 12], [327, 9], [336, 12], [348, 10], [348, 50], [300, 45], [258, 44],
      [240, 47]], fill('land')) // the continent and its volcano
    + plume(41, 23, 9, 26, fill(hot)) + plume(279, 5, 5.5, 26, fill(hot)) + quakes.join('')
    + dot(272, 51, 2.2, fill(hot)) + dot(275, 43, 1.7, fill(hot)) // melt rising
    + arrow(34, 19, -26, 'ink', 0.7) + arrow(90, 23, 70, 'ink', 0.7) // the plates part
    + arrow(60, 68, 56, hot, 0.5) + arrow(284, 74, 31, 'paper', 1, 25));
}

function twoVolcanoes(hot) { // a shield (left) and a stratovolcano (right), cut in half
  const G = 70; // the ground line
  const shield = (x, w, h) => `M${x - w} ${G}C${x - w * 0.55} ${G - 2} ${x - w * 0.35} ${G - h} `
    + `${x} ${G - h}C${x + w * 0.35} ${G - h} ${x + w * 0.55} ${G - 2} ${x + w} ${G}Z`; // convex
  const cone = (x, w, h, c = w * 0.055) => `M${x - w} ${G}C${x - w * 0.5} ${G - 3} ${x - w * 0.2} `
    + `${G - h * 0.6} ${x - c} ${G - h}H${x + c}` // concave flanks around a crater
    + `C${x + w * 0.2} ${G - h * 0.6} ${x + w * 0.5} ${G - 3} ${x + w} ${G}Z`;
  const layers = (shape, x, sizes, paints) => sizes.map(([w, h], i) => // eruption on eruption
    path(shape(x, w, h), fill('paper')) + path(shape(x, w, h), paints[i % 2])).join('');
  const ash = [[267, 10, 4], [274, 7, 5.5], [282, 8.5, 5], [289, 5.5, 5], [297, 7, 4.5],
    [304, 5, 3.5], [279, 12, 3.5], [292, 10, 3.5]].map(([x, y, r]) => dot(x, y, r, fill('land')));
  return svg(348, 92, `<rect y="${G}" width="348" height="${92 - G}" ${fill('land')}/>`
    + layers(shield, 91, [[86, 24], [70, 18], [54, 12], [38, 7]], [fill(hot, 0.3), fill(hot, 0.5)])
    + layers(cone, 269, [[74, 56], [61, 46], [48, 36], [35, 26], [22, 16]],
      [fill(hot, 0.55), fill('muted', 0.35)]) // lava, then ash
    + ash.join('') // the explosive one blows ash; the shield only pours lava
    + `<ellipse cx="91" cy="80" rx="24" ry="4.5" ${fill(hot)}/>` // a shallow magma chamber
    + `<ellipse cx="269" cy="86" rx="20" ry="5" ${fill(hot)}/>` // a deeper one
    + path(`M89.5 77V${G - 24}h3V77Z`, fill(hot)) + path(`M267.5 83V${G - 56}h3V83Z`, fill(hot)));
}
const ART = { 'plate-cycle': plateCycle('band'), 'two-volcanoes': twoVolcanoes('band2') };
const ALT = { // the drawings described, for the figures' altText
  'plate-cycle': t({ en: 'A dark plate spreads from a ridge on the left, where magma rises, runs '
    + 'under a pale sea and bends down beneath a continent topped by a volcano; a row of dots '
    + 'along the sinking slab marks earthquakes, and arrows show the plates moving apart.',
  es: 'Una placa oscura nace en una dorsal a la izquierda, donde sube el magma, corre bajo un mar '
    + 'claro y se dobla hacia abajo bajo un continente coronado por un volcán; una fila de puntos '
    + 'a lo largo de la placa que se hunde marca los terremotos, y unas flechas muestran cómo se '
    + 'separan las placas.' }),
  'two-volcanoes': t({ en: 'On the left, a low, broad volcano of thin layers over a shallow magma '
    + 'chamber; on the right, a steep cone of alternating lava and ash layers over a deeper '
    + 'chamber, with a cloud of ash above its crater.',
  es: 'A la izquierda, un volcán bajo y ancho, de capas finas, sobre una cámara magmática poco '
    + 'profunda; a la derecha, un cono empinado de capas alternas de lava y ceniza sobre una '
    + 'cámara más profunda, con una nube de ceniza sobre el cráter.' }),
};
// #endregion

// #region figure: each figure takes the first free slot after its first citation
const figure = (id, placement, caption) => ({ id, typeId: 'figure', kind: 'svg', createdAt: 0,
  updatedAt: 0, svg: { fileId: `${id}.svg`, width: ART[id].width, height: ART[id].height },
  placement, caption, altText: ALT[id] }); // altText: read aloud in a PDF or HTML edition
const resources = [
  // 'auto' may take the citing page's foot, 'top' never can (gotcha: top-float-next-page)
  figure('plate-cycle', { position: 'auto', span: 'page' },
    t({ en: 'A plate’s life in cross-section: born at a ridge (left), it cools as it drifts away '
      + 'and sinks under a continent; dots mark earthquakes.',
    es: 'La vida de una placa, en corte: nace en la dorsal (izquierda), se enfría al alejarse y '
      + 'se hunde bajo el continente; los puntos son terremotos.' })),
  figure('two-volcanoes', { position: 'bottom', span: 'page' }, // under the text on a last page
    t({ en: 'Two volcanoes in cross-section: a broad shield built of runny basalt flows (left) '
      + 'and a steep stratovolcano of lava and ash layers (right).',
    es: 'Dos volcanes en corte: un escudo ancho, de coladas de basalto fluido (izquierda), y un '
      + 'estratovolcán de capas de lava y ceniza (derecha).' })),
];
// #endregion

// ─── 3 · Fonts ──────────────────────────────────────────────────────────────
const FONTS = { // text, display and label faces, loaded before the build (gotcha: fonts-first)
  'Roboto Serif': ['400', '400i', '700'], Archivo: ['700', '800'],
  'Archivo Narrow': ['400', '400i', '600', '700'] }; // 400i: the colophon's book title

// ─── 4 · Build & show ───────────────────────────────────────────────────────
await loadFonts(FONTS, markdown);
for (const [id, art] of Object.entries(ART)) await loadSvg(`${id}.svg`, art.markup);
// #region build: chapters 3 and 4 of a longer book, so the counters start where 2 ended
const continuation = { pageNumbering: { startAt: 41 }, // an odd folio: page 1 is still a recto
  headings: { h1: 2, h2: 0, h3: 0, h4: 0, h5: 0, h6: 0 } }; // the next # is chapter 3
const doc = await buildWithFonts(
  () => buildDocument({ markdown, resources, continuation }, config()), markdown);
showPages(doc, { title: t({ en: 'Chapter opener on a full-bleed band',
  es: 'Apertura de capítulo sobre banda a sangre' }) });
// #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 v1 ── the same in every recipe · postext.dev/cookbook ────────
// Postext measures text with the faces the browser has loaded, and caches the
// widths, so every face must be ready before the first build. Faces come from
// Fontsource: the same static files the PDF embeds, so screen and PDF agree.

/** faces = { 'Family Name': ['400', '400i', '700'] }. `text` is the sample:
 *  letters beyond Latin-1 (č, ł, ő…) also load the latin-ext files. With
 *  `optional`, a face Fontsource does not ship is skipped instead of failing.
 *  Resolves to the number of faces added. */
async function loadFonts(faces, text = '', { optional = false } = {}) {
  kitStatus('Loading fonts…');
  const ranges = {
    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',
  };
  const subsets = /[Ā-˿Ḁ-ỿ]/.test(text) ? ['latin', 'latin-ext'] : ['latin'];
  const jobs = [];
  let added = 0;
  for (const [family, specs] of Object.entries(faces)) {
    const id = fontsourceId(family);
    const meta = optional ? await fontsourceMeta(family) : null;
    for (const spec of new Set(specs)) {
      const weight = parseInt(spec, 10);
      const style = spec.endsWith('i') ? 'italic' : 'normal';
      if (hasFace(family, weight, style)) continue;
      if (optional && !(meta?.weights.includes(weight) && meta.styles.includes(style))) continue;
      for (const subset of subsets) {
        const url = `https://cdn.jsdelivr.net/npm/@fontsource/${id}@5/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().then((ready) => { document.fonts.add(ready); added++; }, () => {
          if (subset === 'latin' && !optional) throw new Error(`Fontsource has no ${family} ${weight} ${style}`);
        }));
      }
    }
  }
  await Promise.all(jobs).catch((error) => { kitFail(error); throw error; });
  return added;
}

/** Runs `build` (a buildDocument or buildBundle call) and checks the faces
 *  the pages use. A regular face missing from FONTS is loaded with a warning;
 *  bold and italic variants are loaded when the family ships them. Then the
 *  measurement caches are cleared and the build runs again. */
async function buildWithFonts(build, text = '') {
  const tried = new Set();
  for (let round = 0; round < 3; round++) {
    kitStatus('Laying out…');
    await new Promise(requestAnimationFrame);          // let the status paint first
    const result = await Promise.resolve().then(build).catch((error) => { kitFail(error); throw error; });
    const wanted = { base: {}, variants: {} };
    for (const { font, base } of [result].flat().flatMap(fontStringsOf)) {
      const { family, weight, style } = parseFont(font);
      const key = `${family}|${weight}|${style}`;
      if (tried.has(key) || hasFace(family, weight, style)) continue;
      tried.add(key);
      (wanted[base ? 'base' : 'variants'][family] ??= []).push(`${weight}${style === 'italic' ? 'i' : ''}`);
    }
    if (Object.keys(wanted.base).length) {
      console.warn(`[cookbook] FONTS does not list ${JSON.stringify(wanted.base)}: loading them.`);
    }
    const added = await loadFonts(wanted.base, text) + await loadFonts(wanted.variants, text, { optional: true });
    if (added === 0) return result;
    clearMeasurementCache();
  }
  throw new Error('The fonts did not settle after three builds.');
}

/** Every font string of the layout. `base` marks a block's own face; its
 *  bold, italic and bold-italic variants are listed whether or not used. */
function fontStringsOf(doc) {
  const found = new Map();
  const walk = (node) => {
    if (!node || typeof node !== 'object') return;
    if (Array.isArray(node)) { node.forEach(walk); return; }
    for (const [key, value] of Object.entries(node)) {
      if (typeof value === 'string' && /fontString$/i.test(key)) {
        found.set(value, found.get(value) || key === 'fontString');
      } else if (value && typeof value === 'object') walk(value);
    }
  };
  walk(doc.pages);
  walk(doc.blocks);
  return [...found].map(([font, base]) => ({ font, base }));
}

/** '700 37.5px Open Sans' / 'italic 400 13px "Source Serif 4"' → { family, weight, style }.
 *  A string with no weight ('95.8px Young Serif', from a design text) is 400. */
function parseFont(font) {
  const m = /^(?:(italic|oblique)\s+)?(?:small-caps\s+)?(?:(\d+|bold|normal)\s+)?[\d.]+px\s+(.+)$/.exec(font.trim());
  if (!m) throw new Error(`Unexpected font string: ${font}`);
  const weight = m[2] === 'bold' ? 700 : !m[2] || m[2] === 'normal' ? 400 : Number(m[2]);
  return { family: m[3].replace(/^["']|["']$/g, ''), weight, style: m[1] ? 'italic' : 'normal' };
}

/** True when a loaded FontFace covers exactly this family, weight and style
 *  (document.fonts.check() is also true 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;
}

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

/** The weights and styles a family ships ({ weights: [400, 700], styles: ['normal', 'italic'] }), 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 v1 ── the same in every recipe · postext.dev/cookbook ───────
/** Shows the pages as facing spreads on a dark desk: the first page is a
 *  recto on its own, then verso | recto pairs, as in a bound book. Pages
 *  are painted when they scroll near the screen. */
function showPages(docs, { title, width = 460 } = {}) {
  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, { 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 · images v1 ── recipes with pictures · postext.dev/cookbook ──────────
/** Registers a photo or PNG for the canvas and keeps its bytes for the PDF.
 *  fetch → ImageBitmap never taints the canvas (a plain cross-origin <img> would). */
async function loadImage(fileId, url) {
  const res = await fetch(url);
  if (!res.ok) throw new Error(`Image not found (${res.status}): ${url}`);
  const bytes = new Uint8Array(await res.arrayBuffer());
  registerResourceImage(fileId, await createImageBitmap(new Blob([bytes])));
  (loadImage.bytes ??= new Map()).set(fileId, bytes);
}

/** Registers SVG markup (drawn in code, or fetched) as a vector image. */
async function loadSvg(fileId, svg) {
  const img = new Image();
  img.src = `data:image/svg+xml;charset=utf-8,${encodeURIComponent(svg)}`;
  await img.decode();
  registerResourceImage(fileId, img);
  (loadImage.bytes ??= new Map()).set(fileId, new TextEncoder().encode(svg));
}

/** renderToPdf({ resourceBytes: imageBytes }) */
function imageBytes(fileId) { return loadImage.bytes?.get(fileId); }

/** renderToHtml({ resourceImageUrl: imageUrl }) */
function imageUrl(fileId) {
  const bytes = imageBytes(fileId);
  if (!bytes) return undefined;
  imageUrl.urls ??= new Map();
  if (!imageUrl.urls.has(fileId)) {
    const type = /\.svg$/i.test(fileId) ? 'image/svg+xml' : /\.png$/i.test(fileId) ? 'image/png' : 'image/jpeg';
    imageUrl.urls.set(fileId, URL.createObjectURL(new Blob([bytes], { type })));
  }
  return imageUrl.urls.get(fileId);
}

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

## アレンジ

### 帯を深くする

`BAND`は帯の深さ、数字の位置、本文の上に確保する高さを決めます。帯を深くすると本文が次のページへ押し出されるので、章ごとに原稿量を合わせ直すか（最後のつまずきを参照）、章を追い込みにします。

```diff
-const BAND = 100; // mm from the trim to the foot of the band
+const BAND = 128; // mm from the trim to the foot of the band
```

### 裁ち落とし付きで印刷する

3 mmの裁ち落としを付けると、帯、その下端、ツメは仕上がり線より3 mm外へはみ出し、数字は下端に立ったままで、`page.cutLines`が同じ値で有効になります。

```diff
-const BLEED = 0; // mm: 0 on screen; about 3 for print, which also switches on page.cutLines
+const BLEED = 3; // mm: 0 on screen; about 3 for print, which also switches on page.cutLines
```

### 別の章の色を足す

章の色ひとつにつき、パレットの項目とスタイルをひとつずつ足します。その章の見出し行に`{style="slate"}`と書きます。

```diff
   band2: '#2e6f73', // the second chapter colour, a teal; the sea in Figure 3.1
+  band3: '#3d4f6b', // a third chapter colour, a slate blue
@@
-const headingStyles = [{ id: 'teal', palette: { band: palette.band2 } }];
+const headingStyles = [{ id: 'teal', palette: { band: palette.band2 } },
+  { id: 'slate', palette: { band: palette.band3 } }];
```

### 章を追い込みにする

`'any'`にすると、章は次のページで始まります。奇数ページで終わった章の次は偶数ページで、前に白ページを挟まずに始まり、数字はページの右端から吊るしているので、のど側に立ちます。第3章はすでに偶数ページで終わっているので、この4ページは変わりません。

```diff
-      { level: 1, span: 'page', breakBefore: { enabled: true, parity: 'odd' },
+      { level: 1, span: 'page', breakBefore: { enabled: true, parity: 'any' },
```

## よくあるつまずき

- **章扉はページに固定した要素のうち最も下のものまで高さを確保する.** 詳細デザインの章扉は、最も下にある要素までの高さを確保します。見出しより下にあるページや裁ち落としに固定した要素もこれに含まれるため、ページの下端に置いた装飾があると本文が次のページへ押し出されます。こうした装飾は見出しより上に置くか、ヘッダーやフッターのスロットに移すか、minHeightで確保する高さを指定してください。
- **デザインのテキストのoverflowの既定値は'ellipsis-end'.** 幅に収まらないデザインのテキスト要素は、既定では省略記号で終わります。複数行に折り返したいタイトルにはoverflow: 'wrap'を設定してください。
- **デザインのテキストのlineHeightは倍率で、寸法ではない.** デザインのスロットでは、テキスト要素のlineHeightはフォントサイズに掛ける倍率です（lineHeight: 1.05）。postext 1.4.1ではpt(15)のような寸法を指定しても拒否されず、章扉の高さがNaNと計測されて、minHeightを含め確保する高さが警告なしに失われ、本文がタイトルに重なって組まれます。
- **{number}/{chapterNumber}はH1の番号を出す。{numberRoman}は部だけ.** {number}と{chapterNumber}は見出しの書式付きの番号を出力しますが、{numberRoman}、{numberDecimal}などの数値のバリエーションが埋まるのは部扉だけです。章番号の書式はnumberingTemplate（{1:I}）で指定するか、属性として渡してください。
- **属性値には{も}も書けない。"を含む値は一重引用符で囲む.** 属性値は閉じ波かっこで終わるため、{や}を含められません。二重引用符を含む値は一重引用符で囲みます。ドル記号は問題ありません。
- **パレットを差し替えても、デザイン要素と参照色は変わらない.** postext 1.4.1はcolorPaletteをテキストのスタイル（本文、見出し、リスト、キャプション、表、囲み）には反映しますが、ヘッダー、フッター、章扉、部扉の要素と、bodyText.referenceColorには反映しません。これらはpaletteIdの横に書いた16進の色のままです。画面用のダーク版や色替えのためにパレットを差し替えるときは、ビルドの前に、リンクしたすべての色をcolorPaletteから書き直してください。
- **'top'のフロートは参照したページには入らない.** フロートが自分の参照より上に置かれることはありません。そのため、Nページで参照したページ幅の'top'フロートはN+1ページの頭に置かれます。もっと前で参照するか、参照したページの下端に入れる位置'auto'または'bottom'を使ってください。
- **SVGの<img>内のテキストはWebフォントを使えない.** SVGは画像として描かれ、画像はページのWebフォントにアクセスできないため、ラベルはシステムの書体にフォールバックします。テキストをアウトライン化するか、SVGに@font-faceのサブセットを埋め込むか、ラベルをキャプションに移してください。
- **Figure/TableはdefaultResourceTypes(locale)でローカライズする.** 設定のlocaleが決めるのはハイフネーションで、キャプションではありません。resourceTypesがなければ、組み込みの種類は英語でFigureとTableと表示されます。スペイン語ならresourceTypes: defaultResourceTypes('es')を渡し、それ以外の言語ではresourceTypesに名前を自分で書いてください。
- **ハイフネーションできるのは8つのロケールだけで、コードは完全一致.** ハイフネーションが用意されているのはen-us、es、fr、de、it、pt、ca、nlで、コードは完全一致で照合されます。'es-ES'やほかの言語は、何の知らせもなくアメリカ英語にフォールバックします。

- 帯の高さは固定ですが、キッカー、タイトル、リード文は天の余白から吊るして下へ伸びるので、リード文が長かったり、タイトルが3行になったりすると、帯の下端にぶつかります。リード文は両方の版と同じく5行程度に収めるか、いちばん長い章扉から`BAND`を求めてください。
- 数字の`y`の式は`lineHeight: 1`を前提にしています。デザインテキストのベースラインは行ボックスの上から0.8の位置にあり、`0.8 * NUMERAL`がその距離に一致するのは、ボックスの高さが活字と同じときだけです。行の高さやアンカーを変えたら、式も変える必要があります。`BEARING`は目で合わせています。数字ごとにサイドベアリングが違うからです（ここでは3と4の終わりが0.8 mmずれています）。
- スタイルのパレットはデザイン要素と文字の色を変えます。画像は描かれた色のままです。図4.1がティール色なのはコードが`band2`で描いているからで、テラコッタ色の図を第4章に置けばテラコッタ色のままです。
- `parity: 'odd'`では、最後のページから数行はみ出した章は、ほとんど空のページと白の偶数ページを残してから次の章扉に進みます。しかもpostext 1.4.1はそのページを本文より短い段で組むので、章の最終行が失われます。ここの両方の版と同じく原稿量を合わせ、本文や`BAND`を変えるたびに各章の最後のページを確かめてください。

## クレジット

- レシピ: Ignacio Ferro ([@drnachio](https://github.com/drnachio))
- 書体: Roboto Serif (OFL-1.1), Archivo (OFL-1.1), Archivo Narrow (OFL-1.1)
- コード: MIT · サンプルの内容: CC-BY-4.0

## 関連レシピ

- [No. 017 · 1冊の本に5種類の章扉](https://postext.dev/ja/cookbook/five-chapter-openers.md): レベル1に章扉を1つ、Markdownで名前を指す見出しスタイルを4つ用意します。どのスタイルもアクセントの色を差し替え、余白や段組み、ノンブルを変えるものもあります。 · 難易度 3 (上級) · すべてのジャンル
- [No. 001 · 余白に段を設けた教科書](https://postext.dev/ja/cookbook/textbook-margin-column.md): 小口側の段にフロートだけを入れる1段半組みのページです。span 'side'の図と傍注がそこに積み重なり、captionSideでほかの図のキャプションもそこへ移します。 · 難易度 3 (上級) · 教科書
- [No. 007 · 章ごとのファイルから1冊の本を組む](https://postext.dev/ja/cookbook/book-from-chapters.md): buildBundleで5つのMarkdownファイルを1冊の本として組みます。各章は奇数ページから始まり、ページ番号、章番号、図番号はファイルをまたいで続きます。 · 難易度 3 (上級) · マニュアル・ガイド・リファレンス
