# 安全上の注意を載せた製品の取扱説明書

> ドイツ語の電気ケトルの取扱説明書。WARNUNGとVORSICHTの囲みは信号色の帯に警告の三角形を載せ、図と表のラベルもドイツ語です。

- HTML版: https://postext.dev/ja/cookbook/product-manual-warnings
- レシピ No. 050 · 囲みと注 · 難易度 2 (中級) · 出力: Canvas
- ジャンル: マニュアル・ガイド・リファレンス
- 必要なもの postext ≥ 1.4.1 · テスト環境 1.4.1 ／テスト日 2026-09-26
- ページ: [1](https://postext.dev/cookbook/product-manual-warnings/en/p01.webp?v=f8dcac8e), [2](https://postext.dev/cookbook/product-manual-warnings/en/p02.webp?v=f8dcac8e), [3](https://postext.dev/cookbook/product-manual-warnings/en/p03.webp?v=f8dcac8e), [4](https://postext.dev/cookbook/product-manual-warnings/en/p04.webp?v=f8dcac8e), [5](https://postext.dev/cookbook/product-manual-warnings/en/p05.webp?v=f8dcac8e), [6](https://postext.dev/cookbook/product-manual-warnings/en/p06.webp?v=f8dcac8e), [7](https://postext.dev/cookbook/product-manual-warnings/en/p07.webp?v=f8dcac8e)
- Sandboxで開く: https://postext.dev/ja/sandbox#recipe=product-manual-warnings&lang=en (.postext: https://postext.dev/cookbook/product-manual-warnings/en/product-manual-warnings.postext)
- 最終更新: 2026-09-26
- 他の言語: [en](https://postext.dev/en/cookbook/product-manual-warnings.md), [es](https://postext.dev/es/cookbook/product-manual-warnings.md), [ca](https://postext.dev/ca/cookbook/product-manual-warnings.md), [zh](https://postext.dev/zh/cookbook/product-manual-warnings.md), [ar](https://postext.dev/ar/cookbook/product-manual-warnings.md)

## かんたんな説明

電気ケトルのドイツ語の取扱説明書、7ページです。危険の度合いに応じて赤や黄色に塗り分け、警告の三角形を付けた安全上の注意の囲みの作り方を示します。

## できあがり

架空の電気ケトルVerra W1の取扱説明書7ページを、A5判にドイツ語で組みます。安全上の注意のページでは、2段階の危険の度合いを囲みで示します。信号色の帯が左側を縦に走って警告の三角形を載せ、同じ色の細枠が囲みを閉じます。WARNUNGは赤、VORSICHTは黄色です。機器の損傷を示す段階のHINWEISは、帯と三角形を使わず、角丸のティールの地色にします。囲みの上の段落では、3つの信号語をすべて塗りのチップで示します。3ページ目には番号付きの引出線を付けた部品図と、その下に凡例があり、4ページ目では大きな手順番号の横に、キーの名前を枠線のチップで組みます。トラブルシューティングの表は6ページと7ページにまたがります。キャプションはAbbildungとTabelleで、分割した表には両方のページにFortsetzungと入ります。

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

- Postextにラベルがない言語で、色帯にアイコンを載せた警告の囲みを作るには？
- キーボードのキー、タグ、練習問題の語群などのインラインのチップを作るには？
- リストをカスタマイズするには（レベルごとの記号、(a)/(i)の番号、タスクのチェックボックス、グリッドに乗る間隔）？
- 番号付きキャプションのある図を加え、本文で参照するには（「図3.2を参照」）？
- 見出し行、結合セル、列幅、セルごとのそろえを備えた表を作るには？
- 長い表を、見出し行を繰り返し「続く」の表示を付けて、ページをまたいで分割するには？
- 「図」や「表」のラベルを文書の言語で表示するには？

## 手短な答え

```js
// script.js, 行 28–46
const BAND = 8.5; // mm: a stripe on the side is the icon's column; the icon is centred on it
const [TITLE, TITLE_GAP] = [8.6, 2.4]; // pt; 1.4.1 sets a box title 1.2 times its size
const PAD_Y = (2 * LEAD - 1.2 * TITLE - TITLE_GAP) / 2; // pt: title and padding fill two lines
const notice = (id, hue, ink, icon) => ({ id, backgroundEnabled: false,
  stripe: { enabled: true, side: 'left', width: mm(BAND), color: col(hue) },
  border: { enabled: true, color: col(hue), width: pt(0.75) }, // closes the band into a frame
  icon: { kind: 'resource', resourceId: icon, size: mm(5.6), align: 'top' },
  padding: { top: pt(PAD_Y), right: mm(3.2), bottom: pt(PAD_Y), left: mm(3.2) },
  titleStyle: { fontFamily: DISPLAY, fontWeight: 800, fontSize: pt(TITLE), color: col(ink),
    textTransform: 'uppercase', letterSpacing: pt(1.3), gap: pt(TITLE_GAP) },
  body: { fontSize: pt(8.8), lineHeight: pt(LEAD) }, // on the grid: a box is whole lines tall
  lists: { color: col(ink), gap: mm(2) }, marginTop: pt(LEAD), marginBottom: pt(0) });
const calloutStyles = [
  notice('warnung', 'warning', 'warning', 'triangle-white'), // white triangle, red '!'
  notice('vorsicht', 'caution', 'ink', 'triangle-ink'), // amber type would fail contrast
  { ...notice('hinweis', 'brand', 'brand'), stripe: { enabled: false }, border: { enabled: false },
    backgroundEnabled: true, background: col('tint'), borderRadius: mm(2), // property damage:
    icon: { kind: 'resource', resourceId: 'info', size: mm(4.6) } }, // no band, an icon column
];
```

## 材料

**学べること**

- [囲みのアイコンと角のバッジ](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/document-format.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/justification.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#柱とノンブル)
- citations
- [見出しの属性](https://postext.dev/ja/docs/document-format.md#見出しの属性)
- [改ページと改段](https://postext.dev/ja/docs/document-format.md#pagebreak)
- [ページの役割ごとの柱](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/configuration.md#名前付きの表スタイル)
- [番号のない章](https://postext.dev/ja/docs/configuration.md#見出しスタイル)

**設定の一覧**

- [`bodyText`](https://postext.dev/ja/docs/configuration.md#本文), [`calloutStyles`](https://postext.dev/ja/docs/configuration.md#囲みスタイル), [`captionStyle`](https://postext.dev/ja/docs/configuration.md#キャプションスタイル), [`chipStyles`](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#リソースの種類), [`tableStyle`](https://postext.dev/ja/docs/configuration.md#表スタイル), [`tableStyles`](https://postext.dev/ja/docs/configuration.md#名前付きの表スタイル), [`unorderedLists`](https://postext.dev/ja/docs/configuration.md#箇条書きリスト)

**API**

- [`buildDocument`](https://postext.dev/ja/docs/configuration.md#文書のビルド), [`clearMeasurementCache`](https://postext.dev/ja/docs/configuration.md#計測キャッシュ), [`mergeCells`](https://postext.dev/ja/docs/document-format.md#ブロック埋め込み任意本文中への明示的な配置), [`parseTSV`](https://postext.dev/ja/docs/document-format.md#ブロック埋め込み任意本文中への明示的な配置), [`registerResourceImage`](https://postext.dev/ja/docs/architecture.md#apiの概要), [`renderPageToCanvas`](https://postext.dev/ja/docs/configuration.md#ページをビットマップに描画する)

**書体**

- Red Hat Text (OFL-1.1), Red Hat Display (OFL-1.1), Red Hat Mono (OFL-1.1)

## 作り方

### 1 · 三角形を載せる帯

コードは前出の[手短な答え](#手短な答え)にあります。`stripe`が左端に帯を塗ります。側面に帯のある囲みでは、1.4.1はアイコンに専用の列を与えず、帯の中央に置きます（[囲みスタイル](/ja/docs/configuration#囲みスタイル)）。`align: 'top'`でアイコンの上端をタイトルの上端にそろえます。同じ色の`border`が帯を閉じて枠にします。これがないと、囲みの本文は色のついた帯の横で、紙の上にむき出しで立つことになります。`PAD_Y`は、タイトル、その下の間隔、上下のパディングの合計が12.8 ptのグリッドの2行分になるように計算します。本文はグリッドの行送りを保つので、どの囲みも高さが行の整数倍になり（WARNUNGは10行、VORSICHTは8行）、`marginTop: pt(LEAD)`が2ページ目の2つの囲みの間をちょうど1行空けます。VORSICHTは帯と枠に黄色を使い、タイトルと箇条書きの記号は墨で組みます。白地に黄色のコントラストは2.0:1しかなく、小さな文字には4.5:1が必要だからです。HINWEISは同じ土台（`...notice(…)`）から作り、帯と枠をオフにします。そのためinfoアイコンには専用の列が与えられ、`borderRadius`が無地の地色の角を丸めます。

### 2 · 図と表のドイツ語の名前

```js
// script.js, 行 50–59
const counted = { numberingTemplate: '{n}', resetOn: 'never', counterFormat: 'decimal' }; // 1, 2…
const resourceTypes = [ // 1.4.1 names them in English or Spanish (gotcha: resource-types-locale)
  { id: 'figure', name: 'Abbildung', shortLabel: 'Abb.', captionPrefix: 'Abbildung', ...counted },
  { id: 'table', name: 'Tabelle', shortLabel: 'Tab.', captionPrefix: 'Tabelle', ...counted,
    captionStyle: { position: 'above' } }, // a table is captioned over its head
];
const tableStyle = { rules: 'horizontal', borderColor: col('rule'), borderWidth: pt(0.5),
  headerBackground: col('ink'), headerColor: col('paper'), headerFontFamily: MONO,
  headerFontSize: pt(7.6), bodyFontSize: pt(8.2), cellPadding: mm(1.3),
  continuedSuffix: '(Fortsetzung)', continuesMarker: 'Fortsetzung auf der nächsten Seite' };
```

`locale: 'de'`で取扱説明書にドイツ語のハイフネーションが適用されます。ただし1.4.1では、リソースの種類の名前と分割した表の継続表示は英語とスペイン語にしかありません。この数行がないと、キャプションはFigure 2.1やTable 5.1となり、分割した表の前半の下の注記はContinuedになります。手で書くリソースの種類では、番号付けを含むすべての項目を指定する必要があります。それを補うのが`counted`です。`resetOn: 'never'`を付けた`{n}`で、節ごとにやり直さず取扱説明書全体を通して数えます（Abbildung 1、Tabelle 1から3）。`captionStyle.position: 'above'`を指定するのは表の種類だけなので、表はキャプションを上に置き、図はキャプションを下に置いたままです。

### 3 · キー、信号語、部品番号をチップにする

```js
// script.js, 行 63–71
const filled = (id, fill, ink) => ({ id, fontFamily: DISPLAY, bold: true, fontSize: em(0.82),
  background: col(fill), color: col(ink), borderWidth: pt(0), paddingX: em(0.45) }); // no outline
const chipStyles = [
  { id: 'taste', fontFamily: MONO, bold: true, fontSize: em(0.92), backgroundEnabled: false,
    borderColor: col('ink'), borderWidth: pt(0.6), borderRadius: pt(2.2), paddingX: em(0.4) },
  filled('warnung', 'warning', 'paper'), filled('vorsicht', 'caution', 'ink'),
  filled('hinweis', 'brand', 'paper'), { ...filled('nr', 'brand', 'paper'), fontSize: em(0.95),
    borderRadius: em(1) }, // a part number, round like the drawing's
];
```

`taste`は、ボタンに刷られたラベルのように、キーの名前を等幅書体で塗りなしの枠線で囲みます。信号語のチップはそれぞれの囲みの色で塗り、黄色のチップはVORSICHTのタイトルと同じく語を墨で組みます。`nr`は部品番号に高さの半分より大きい角丸を与えるので、図の番号付きの丸と同じ円形になります。チップのサイズは周囲のテキストのemで指定します。キーは本文中では8.6 pt、表のセルでは7.5 ptになります（[チップスタイル](/ja/docs/configuration#チップスタイル)）。

### 4 · ベースラインに立つ手順番号

```js
// script.js, 行 75–80
const orderedLists = { separator: '', fontFamily: DISPLAY, fontWeight: 800, color: col('brand'),
  numberFontSize: pt(15), gap: mm(3), itemSpacing: pt(5), marginTop: pt(LEAD / 2),
  marginBottom: pt(0), numberVerticalOffset: pt(-1) }; // gotcha: list-number-centred
const unorderedLists = { color: col('brand'), gap: mm(2), marginTop: pt(0), marginBottom: pt(0),
  levels: [{ level: 2, bulletChar: '–', indent: mm(6.5) }, // at a step's text: number + 3 mm gap
    { level: 3, bulletChar: '–', color: col('muted') }] }; // teal •, teal –, grey –
```

手順は番号付きリストで、番号はRed Hat Display 800、15 pt、ティールで、区切り記号は付けません。1.4.1はリストの番号をベースラインに立てず、1行目の中央に置くので、9.3 ptの本文の横の15 ptの数字は行から約1 pt下がります。`numberVerticalOffset: pt(-1)`はキャプチャー上で測った値で、数字の下端をベースラインまで持ち上げます。`itemSpacing`で手順の間を5 pt空け、大きな数字の場所を作ります。手順の下に入れ子になった箇条書きは6.5 mmから始まり、手順の本文にそろいます。記号はティールのダッシュで、3階層目は灰色です（[番号付きリスト](/ja/docs/configuration#番号付きリスト)）。

### 5 · 白抜きのタブに入れる節番号

```js
// script.js, 行 84–94
const H1 = 14, TAB = 2 * LEAD - 3.6, PAD = (TAB - H1 * 1.2) / 2; // pt: a square 2 lines less 3.6
const face = { fontFamily: DISPLAY, fontWeight: 800, fontSize: pt(H1), lineHeight: 1.2 }; // both
const section = { level: 1, numberingTemplate: '{1}', // {number}: 1, 2, 3 …
  breakBefore: { enabled: false }, marginTop: pt(LEAD), marginBottom: pt(LEAD / 2), // run on
  advancedDesign: { enabled: true, slot: { elements: [
    { kind: 'text', id: 'tab', content: '{number}', ...face, color: col('paper'), align: 'center',
      box: { backgroundColor: col('brand'), padding: pad(PAD) },
      placement: pin('container', 'top-left', 0, 0, { width: pt(TAB) }) },
    { kind: 'text', id: 'title', content: '{titleText}', ...face, color: col('ink'),
      overflow: 'wrap', box: { padding: { top: pt(PAD) } }, placement: pin('#tab', 'right-of', 3) },
  ] } } };
```

各節は段内のレベル1の見出しで、デザイン要素の枠から描きます。タブはティールの正方形に`{number}`（`numberingTemplate: '{1}'`で埋まる）を白で刷り、タイトルはその3 mm右から始まります。番号とタイトルはサイズと行の高さが同じなので、同じ上パディングでひとつのベースラインにそろいます。タブは22 ptの正方形で、12.8 ptのグリッドの2行分より3.6 pt小さいため、見出しはグリッド2行の中に収まります。表紙も番号なしの見出しスタイルで組んだレベル1の見出しなので、Sicherheitshinweisesが第1節になります。

### 6 · セルを結合したトラブルシューティングの表を分割する

```js
// script.js, 行 294–312
function faultTable(tsv) { // parseTSV leaves the head row to you: headerRowCount
  let m = { ...parseTSV(tsv), headerRowCount: 1, columnWidths: [30, 34, 36] }; // weights
  for (let r = 2, top = 1; r < m.rows.length; r++) { // a rowspan per fault: no cut runs through it
    if (m.rows[r][0].content) top = r; // mergeCells hides what it covers: merged-cells-hiddenby
    else m = mergeCells(m, { start: { row: top, col: 0 }, end: { row: r, col: 0 } });
  }
  return m;
}
const fold = (rows, columnWidths) => ({ columnWidths, rows: rows.slice(0, rows.length / 2) // 2 up
  .map((row, k) => [...row, ...rows[k + rows.length / 2]]) });
const legend = fold(parseTSV(parts).rows.map(([chip, name]) => [{ ...chip, align: 'center' },
  name]), [7, 43, 7, 43]); // each part's number chip centred in its narrow column
const HERE = { placement: { position: 'here' } }; // at the resource's ::resource line
const table = (id, caption, model, styleId = 'bare', where = HERE) => ({ id, typeId: 'table',
  kind: 'table', caption, table: { model, styleId }, createdAt: 0, updatedAt: 0, ...where });
const tables = [table('legende', 'Teile des Verra W1', legend),
  table('daten', 'Kenndaten des VW-170', fold(parseTSV(data).rows, [20, 30, 20, 30])),
  table('stoerungen', 'Störungen und ihre Behebung', faultTable(faults), null, // the house style,
    { placement: { position: 'top' } })]; // a float, so it can split (gotcha: here-table-no-split)
```

表はそれぞれ専用のコンテンツファイルにTSVで書きます。トラブルシューティングのデータでは、最初のセルが空の行はその上の故障に属します。`mergeCells`はそうした最初のセルをひとつの縦長のセルに結合し、覆われたセルを非表示にします。`parseTSV`は見出し行を設定しないので、`headerRowCount: 1`で宣言します。7ページ目で繰り返されるのがこの行です。表は1ページより高く、1.4.1で分割できるのはフロートとして配置した表だけなので、`position: 'top'`でフロートにします。分割は行と行の間で行われ、結合したセルの途中では切れないため、どの故障もその原因と同じページに収まります（[ページより高い表](/ja/docs/configuration#ページより高い表)）。表は5ページ目で引用され、引用の後で上のフロートを置ける最初のページである6ページ目の頭に来ます。`fold`はリストを半分ずつ横に並べます。凡例と技術データに使い、凡例では`align: 'center'`で番号のチップを中央にそろえます。

## レシピの全体

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

- レシピのフォルダー: https://github.com/drnachio/postext/tree/main/cookbook/product-manual-warnings

### script.js

```js
// ═══ Postext Cookbook · Nº 050 · Product manual with safety notices ════════════════
// https://postext.dev/en/cookbook/product-manual-warnings
// Code: MIT · Text: original, in German (CC BY 4.0) · Drawings: generated in code (CC BY 4.0)
// Fonts: Red Hat Text, Red Hat Display, Red Hat Mono (SIL OFL 1.1) · Needs postext ≥ 1.4.1
import { buildDocument, renderPageToCanvas, clearMeasurementCache, registerResourceImage,
  parseTSV, mergeCells } from 'https://esm.sh/postext';

const LANG = 'en'; // @lang: the language of the sample document ('en' | 'es')
const RECIPE = 'product-manual-warnings';

// ─── 1 · Design ─────────────────────────────────────────────────────────────
const palette = { ink: '#1a1f24', paper: '#ffffff', // a blue-black on white
  brand: '#10727b', tint: '#e4f0f1', // the house teal (5.7:1 on white) and its pale tint
  warning: '#d62e1f', caution: '#f2a900', // the signal colours of WARNUNG and VORSICHT
  rule: '#cfd5da', muted: '#5b6570' }; // hairlines; running heads and notes
// 1.4.1 design slots read the hex, not the id: col() writes both (gotcha: palette-skips-designs)
const col = (id) => ({ hex: palette[id], model: 'hex', paletteId: id });
const colorPalette = Object.entries({ ...palette, 'main-color': palette.brand }) // the defaults
  .map(([id, hex]) => ({ id, name: id, value: { hex, model: 'hex' } }));
const [TEXT, DISPLAY, MONO] = ['Red Hat Text', 'Red Hat Display', 'Red Hat Mono'];
const PAGE = { w: 148, h: 210, top: 19, bottom: 19.4, inner: 17, outer: 13 }; // mm: A5, mirrored
const LEAD = 12.8; // pt: the body's leading and baseline grid, 38 lines to the page
const pin = (to, edge, x = 0, y = 0, size) => ({ anchor: { to, edge },
  offset: { x: mm(x), y: mm(y) }, ...(size && { size }) });
const pad = (y, x = 0) => ({ top: pt(y), bottom: pt(y), left: pt(x), right: pt(x) }); // pt

// #region answer: signal-word boxes: a band in the hazard's colour carries its triangle
const BAND = 8.5; // mm: a stripe on the side is the icon's column; the icon is centred on it
const [TITLE, TITLE_GAP] = [8.6, 2.4]; // pt; 1.4.1 sets a box title 1.2 times its size
const PAD_Y = (2 * LEAD - 1.2 * TITLE - TITLE_GAP) / 2; // pt: title and padding fill two lines
const notice = (id, hue, ink, icon) => ({ id, backgroundEnabled: false,
  stripe: { enabled: true, side: 'left', width: mm(BAND), color: col(hue) },
  border: { enabled: true, color: col(hue), width: pt(0.75) }, // closes the band into a frame
  icon: { kind: 'resource', resourceId: icon, size: mm(5.6), align: 'top' },
  padding: { top: pt(PAD_Y), right: mm(3.2), bottom: pt(PAD_Y), left: mm(3.2) },
  titleStyle: { fontFamily: DISPLAY, fontWeight: 800, fontSize: pt(TITLE), color: col(ink),
    textTransform: 'uppercase', letterSpacing: pt(1.3), gap: pt(TITLE_GAP) },
  body: { fontSize: pt(8.8), lineHeight: pt(LEAD) }, // on the grid: a box is whole lines tall
  lists: { color: col(ink), gap: mm(2) }, marginTop: pt(LEAD), marginBottom: pt(0) });
const calloutStyles = [
  notice('warnung', 'warning', 'warning', 'triangle-white'), // white triangle, red '!'
  notice('vorsicht', 'caution', 'ink', 'triangle-ink'), // amber type would fail contrast
  { ...notice('hinweis', 'brand', 'brand'), stripe: { enabled: false }, border: { enabled: false },
    backgroundEnabled: true, background: col('tint'), borderRadius: mm(2), // property damage:
    icon: { kind: 'resource', resourceId: 'info', size: mm(4.6) } }, // no band, an icon column
];
// #endregion

// #region types: German names for figures and tables, and for a table's continuation
const counted = { numberingTemplate: '{n}', resetOn: 'never', counterFormat: 'decimal' }; // 1, 2…
const resourceTypes = [ // 1.4.1 names them in English or Spanish (gotcha: resource-types-locale)
  { id: 'figure', name: 'Abbildung', shortLabel: 'Abb.', captionPrefix: 'Abbildung', ...counted },
  { id: 'table', name: 'Tabelle', shortLabel: 'Tab.', captionPrefix: 'Tabelle', ...counted,
    captionStyle: { position: 'above' } }, // a table is captioned over its head
];
const tableStyle = { rules: 'horizontal', borderColor: col('rule'), borderWidth: pt(0.5),
  headerBackground: col('ink'), headerColor: col('paper'), headerFontFamily: MONO,
  headerFontSize: pt(7.6), bodyFontSize: pt(8.2), cellPadding: mm(1.3),
  continuedSuffix: '(Fortsetzung)', continuesMarker: 'Fortsetzung auf der nächsten Seite' };
// #endregion

// #region chips: keys in the mono face, outlined; signal words and part numbers filled
const filled = (id, fill, ink) => ({ id, fontFamily: DISPLAY, bold: true, fontSize: em(0.82),
  background: col(fill), color: col(ink), borderWidth: pt(0), paddingX: em(0.45) }); // no outline
const chipStyles = [
  { id: 'taste', fontFamily: MONO, bold: true, fontSize: em(0.92), backgroundEnabled: false,
    borderColor: col('ink'), borderWidth: pt(0.6), borderRadius: pt(2.2), paddingX: em(0.4) },
  filled('warnung', 'warning', 'paper'), filled('vorsicht', 'caution', 'ink'),
  filled('hinweis', 'brand', 'paper'), { ...filled('nr', 'brand', 'paper'), fontSize: em(0.95),
    borderRadius: em(1) }, // a part number, round like the drawing's
];
// #endregion

// #region steps: big teal step numbers; teal dashes under them, grey at the third level
const orderedLists = { separator: '', fontFamily: DISPLAY, fontWeight: 800, color: col('brand'),
  numberFontSize: pt(15), gap: mm(3), itemSpacing: pt(5), marginTop: pt(LEAD / 2),
  marginBottom: pt(0), numberVerticalOffset: pt(-1) }; // gotcha: list-number-centred
const unorderedLists = { color: col('brand'), gap: mm(2), marginTop: pt(0), marginBottom: pt(0),
  levels: [{ level: 2, bulletChar: '–', indent: mm(6.5) }, // at a step's text: number + 3 mm gap
    { level: 3, bulletChar: '–', color: col('muted') }] }; // teal •, teal –, grey –
// #endregion

// #region section: the section number reversed out of a teal tab, the title beside it
const H1 = 14, TAB = 2 * LEAD - 3.6, PAD = (TAB - H1 * 1.2) / 2; // pt: a square 2 lines less 3.6
const face = { fontFamily: DISPLAY, fontWeight: 800, fontSize: pt(H1), lineHeight: 1.2 }; // both
const section = { level: 1, numberingTemplate: '{1}', // {number}: 1, 2, 3 …
  breakBefore: { enabled: false }, marginTop: pt(LEAD), marginBottom: pt(LEAD / 2), // run on
  advancedDesign: { enabled: true, slot: { elements: [
    { kind: 'text', id: 'tab', content: '{number}', ...face, color: col('paper'), align: 'center',
      box: { backgroundColor: col('brand'), padding: pad(PAD) },
      placement: pin('container', 'top-left', 0, 0, { width: pt(TAB) }) },
    { kind: 'text', id: 'title', content: '{titleText}', ...face, color: col('ink'),
      overflow: 'wrap', box: { padding: { top: pt(PAD) } }, placement: pin('#tab', 'right-of', 3) },
  ] } } };
// #endregion

const words = (id, content, family, size, weight, color, placement, extra) => ({ kind: 'text',
  id, content, fontFamily: family, fontSize: pt(size), fontWeight: weight, color: col(color),
  align: 'left', overflow: 'wrap', placement, ...extra });
const ART = { x: 36, y: 52, w: 73 }; // mm: the cover's white kettle, on a teal wall and a worktop
const COUNTER = ART.y + (ART.w * 119.4) / 112; // mm: its base's foot (row 119.4 of 112 wide)
const cover = { id: 'cover', numbered: false, breakBefore: { enabled: false },
  span: 'page', // in the column, 1.4.1 clips the design at the column top, 19 mm down the page
  advancedDesign: { enabled: true, minHeight: mm(PAGE.h - PAGE.top - PAGE.bottom),
    slot: { elements: [
      { kind: 'box', id: 'wall', style: { backgroundColor: col('brand') },
        placement: pin('page', 'top-left', 0, 0, { width: 'fill', height: mm(COUNTER) }) },
      { kind: 'image', id: 'art', resourceId: 'kettle',
        placement: pin('page', 'top-left', ART.x, ART.y, { width: mm(ART.w) }) },
      words('title', '{titleText}', DISPLAY, 48, 800, 'paper',
        pin('page', 'top-left', PAGE.inner - 0.6, 24), { lineHeight: 1 }),
      words('product', '{attr.product}', DISPLAY, 17, 500, 'tint', pin('#title', 'below', 0.4, 1)),
      words('manual', 'Bedienungsanleitung', DISPLAY, 15, 800, 'ink',
        pin('page', 'top-left', PAGE.inner, COUNTER + 12)),
      words('keep', 'Vor dem ersten Gebrauch lesen und aufbewahren.', TEXT, 8.6, 400, 'ink',
        pin('#manual', 'below', 0, 1)),
      words('lang', 'DE', DISPLAY, 11, 800, 'paper', pin('page', 'top-right', -PAGE.outer,
        COUNTER + 12), { box: { backgroundColor: col('brand'), padding: pad(3, 5) } }),
      words('model', '{attr.model}', MONO, 7.5, 500, 'muted',
        pin('page', 'bottom-left', PAGE.inner, -PAGE.bottom)),
    ] } } };

const head = (id, content, parity, edge, x) => words(id, content, MONO, 7.5, 500, 'muted',
  pin('page', edge, x, 10.5), { parity, pages: 'body', letterSpacing: pt(1.1),
    textTransform: 'uppercase', align: x > 0 ? 'left' : 'right' });
const folio = (parity, edge) => words(`folio-${parity}`, '{pageNumber}', DISPLAY, 9, 800,
  'paper', pin('page', edge, 0, -9, { width: mm(11) }), { parity, pages: 'body', align: 'center',
    box: { backgroundColor: col('brand'), padding: pad(3.5) } });

const config = () => ({ // a factory: configs are cached by identity (gotcha: config-cache-identity)
  locale: 'de', resourceTypes, colorPalette, calloutStyles, chipStyles, tableStyle, orderedLists,
  tableStyles: [{ id: 'bare', cellPadding: mm(1.1) }], // the legend and the data: no head row
  unorderedLists, headingStyles: [cover], layout: { layoutType: 'single' },
  page: { width: mm(PAGE.w), height: mm(PAGE.h), dpi: 150, margins: { top: mm(PAGE.top),
    bottom: mm(PAGE.bottom), left: mm(PAGE.inner), right: mm(PAGE.outer), mirror: true } },
  bodyText: { fontFamily: TEXT, fontSize: pt(9.3), lineHeight: pt(LEAD), color: col('ink'),
    boldFontWeight: 600, boldColor: col('ink'), italicColor: col('ink'),
    referenceColor: col('ink'), referenceBold: false, firstLineIndent: pt(0),
    paragraphSpacing: true, minWordSpacing: 0.8, maxWordSpacing: 1.8, // from 0.6 and 2
    maxRuntTracking: 0 }, // gotcha: runt-tracking-unpainted
  headings: { fontFamily: DISPLAY, fontWeight: 800, color: col('ink'), lineHeight: pt(LEAD),
    levels: [section, { level: 2, fontSize: pt(10.4), color: col('brand'), marginTop: pt(LEAD),
      marginBottom: pt(0), numberingTemplate: '{1}.{2}' }] }, // 3.1, 3.2 …
  captionStyle: { fontFamily: TEXT, fontSize: pt(8.2), labelColor: col('brand'), gap: mm(1.6) },
  paragraphStyles: [{ id: 'colophon', fontFamily: TEXT, fontSize: pt(7), lineHeight: pt(9.6),
    color: col('muted'), textAlign: 'left', marginTop: pt(LEAD) }],
  header: { elements: [head('verso', '{title}', 'even', 'top-left', PAGE.outer),
    head('recto', 'Wasserkocher VW-170', 'odd', 'top-right', -PAGE.outer)] },
  footer: { elements: [folio('odd', 'bottom-right'), folio('even', 'bottom-left')] }, // thumb
});

// ─── 2 · Content ────────────────────────────────────────────────────────────
const markdown = String.raw`---
title: "Verra W1 · Bedienungsanleitung"
author: "Verra Haushaltsgeräte"
---

# Verra W1 {style="cover" product="Wasserkocher" model="Modell VW-170 · 1,7 l · 220–240 V · 2200 W"}

:::pagebreak

# Sicherheitshinweise

Lesen Sie diese Anleitung vor dem ersten Gebrauch ganz durch und bewahren Sie sie auf; wer das Gerät nach Ihnen benutzt, braucht sie auch. Der Verra W1 ist nur zum Erhitzen von Trinkwasser bestimmt. Milch, Instantgetränke und Suppen brennen am Heizboden an und schäumen über.

Kinder ab 8 Jahren und Personen mit eingeschränkten körperlichen, sensorischen oder geistigen Fähigkeiten dürfen das Gerät unter Aufsicht benutzen, oder wenn sie in seinen sicheren Gebrauch eingewiesen wurden und die Gefahren verstehen. Kinder dürfen nicht mit dem Gerät spielen; reinigen dürfen sie es erst ab 8 Jahren und unter Aufsicht.

Die Warnhinweise sind nach der Schwere der Gefahr gestuft: :chip[WARNUNG]{style="warnung"} warnt vor Lebensgefahr und schweren Verletzungen, :chip[VORSICHT]{style="vorsicht"} vor leichten Verletzungen und :chip[HINWEIS]{style="hinweis"} vor Sachschäden. Die Hinweise finden Sie bei den Arbeitsschritten, für die sie gelten.

:::callout{type="warnung" title="Warnung · Stromschlag"}
- Schließen Sie das Gerät nur an eine ordnungsgemäß installierte Schutzkontakt-Steckdose mit 220 bis 240 Volt an.
- Tauchen Sie Kanne, Sockel und Netzkabel nie in Wasser, und ziehen Sie vor dem Reinigen den Netzstecker.
- Stellen Sie den Sockel nicht neben die Spüle oder unter den Wasserhahn. Er muss trocken bleiben.
- Benutzen Sie das Gerät nicht, wenn Netzkabel, Stecker oder Kanne beschädigt sind. Ein beschädigtes Kabel ersetzt nur der Kundendienst.
:::

:::callout{type="vorsicht" title="Vorsicht · Verbrühungsgefahr"}
- Füllen Sie höchstens bis zur Marke MAX. Aus einer zu vollen Kanne spritzt kochendes Wasser.
- Öffnen Sie den Deckel nicht, solange das Wasser kocht, und fassen Sie die Kanne nur am Griff an. Die Wand aus Edelstahl wird beim Kochen heiß.
- Stellen Sie das Gerät auf eine feste, ebene Fläche, und lassen Sie das Netzkabel nicht über die Tischkante hängen.
:::

# Gerät im Überblick

:ref{id="teile" style="full"} zeigt den Verra W1 von der Seite, :ref{id="legende" style="full"} nennt seine Teile. Die Nummern gelten in der ganzen Anleitung.

::resource{id="teile"}

::resource{id="legende"}

Überschüssiges Netzkabel wickeln Sie unter dem Sockel auf und führen es durch eine der beiden Kerben nach außen, damit der Sockel eben steht.

Prüfen Sie nach dem Auspacken, ob alle Teile vorhanden und unbeschädigt sind:

- [ ] Kanne mit Deckel und Kalkfilter
- [ ] Sockel mit Netzkabel
- [ ] diese Bedienungsanleitung

# Bedienung

## Vor dem ersten Gebrauch

Entfernen Sie alle Aufkleber und Verpackungsreste. Kochen Sie zweimal eine volle Kanne Wasser auf und gießen Sie es jedes Mal weg; so spülen Sie Rückstände aus der Fertigung heraus.

## Wasser kochen

1. Heben Sie die Kanne ab und öffnen Sie den Deckel (1) mit der Taste (5).
2. Füllen Sie frisches, kaltes Leitungswasser ein, mindestens bis MIN (0,5 Liter) und höchstens bis MAX (1,7 Liter). Drücken Sie den Deckel zu, bis er einrastet.
3. Setzen Sie die Kanne auf den Sockel; sie passt in jeder Richtung.
4. Wählen Sie mit :chip[°C]{style="taste"} die Temperatur. Jeder Druck senkt sie um 10 Grad, eine Leuchte am Bedienfeld (4) zeigt die Wahl.
5. Drücken Sie :chip[EIN/AUS]{style="taste"}. Ist die Temperatur erreicht, ertönt ein Signal, und das Gerät schaltet sich ab.
   - Ein zweiter Druck auf :chip[EIN/AUS]{style="taste"} bricht vorher ab.
   - Mit :chip[WARM]{style="taste"} hält das Gerät die Temperatur danach 30 Minuten lang.
     - Ohne Kanne auf dem Sockel endet das Warmhalten nach 2 Minuten.

:::callout{type="hinweis" title="Hinweis"}
Schalten Sie das Gerät nie leer ein. Läuft es trocken, schaltet der Überhitzungsschutz ab, und die Leuchte blinkt rot. Lassen Sie es dann 10 Minuten abkühlen, bevor Sie Wasser einfüllen.
:::

## Die richtige Temperatur

- 100 °C für schwarzen Tee und Kräutertee
- 90 °C für Filterkaffee
- 80 °C für weißen Tee und Oolong
- 70 °C für grünen Tee

# Reinigung und Entkalken

Ziehen Sie vor dem Reinigen den Netzstecker und lassen Sie das Gerät abkühlen. Wischen Sie Kanne und Sockel außen mit einem feuchten Tuch ab. Den Kalkfilter im Ausgießer (2) ziehen Sie nach oben heraus und spülen ihn unter fließendem Wasser ab. Innen genügt es, die Kanne nach Gebrauch zu leeren und mit offenem Deckel trocknen zu lassen.

Wie oft Sie entkalken, hängt von der Härte Ihres Wassers ab: bei hartem Wasser über 14 °dH jeden Monat, bei weichem Wasser unter 8,4 °dH etwa alle drei Monate. Den Härtegrad nennt Ihnen Ihr Wasserversorger. Spätestens wenn der Heizboden eine weiße Schicht zeigt oder das Gerät beim Aufheizen lauter wird, ist es Zeit.

1. Füllen Sie 1 Liter kaltes Wasser ein und lösen Sie 2 Esslöffel Zitronensäure (etwa 30 g) darin auf.
2. Lassen Sie die Lösung eine Stunde einwirken, ohne das Gerät einzuschalten. Erhitzte Zitronensäure bildet mit dem Kalk schwer lösliches Calciumcitrat.
3. Gießen Sie die Lösung weg und spülen Sie die Kanne zweimal gründlich aus. Kochen Sie einmal frisches Wasser auf und gießen Sie es ebenfalls weg.

:::callout{type="hinweis" title="Hinweis"}
Scheuermittel und Stahlwolle zerkratzen den Edelstahl. Essigessenz greift die Dichtung des Deckels an und darf nicht in die Kanne. Kanne und Sockel gehören nicht in die Spülmaschine.
:::

# Störungen beheben

Viele Störungen können Sie selbst beheben. Suchen Sie in :ref{id="stoerungen" style="full"} die Beschreibung, die zu Ihrem Fall passt, und prüfen Sie die Ursachen der Reihe nach; die häufigste steht jeweils oben. Hilft keine der Lösungen, wenden Sie sich an den Kundendienst und öffnen Sie das Gerät nicht selbst. Halten Sie dafür die Modellbezeichnung VW-170 bereit; sie steht auf dem Typenschild unter dem Sockel. Die Anschrift des Kundendienstes finden Sie auf der beiliegenden Garantiekarte.

# Technische Daten

::resource{id="daten"}

# Entsorgung

Elektrogeräte gehören nicht in den Hausmüll. Geben Sie den ausgedienten Wasserkocher bei einer Sammelstelle für Elektroaltgeräte ab, etwa beim Wertstoffhof Ihrer Gemeinde. Auch Händler, die auf mindestens 400 m² Elektrogeräte verkaufen, nehmen Altgeräte kostenlos zurück. Die Verpackung besteht aus Pappe und gehört ins Altpapier.

:::paragraphs{style="colophon"}
Verra W1 · Bedienungsanleitung DE · Ausgabe 09/2026. Verra ist eine erfundene Marke; Gerät und Anleitung sind ein Beispiel aus dem Postext Cookbook. Gesetzt in Red Hat Text, Red Hat Display und Red Hat Mono (SIL OFL). Text und Zeichnungen: CC BY 4.0.
:::
`; // content.<lang>.md: the manual, in German
const parts = String.raw`:chip[1]{style="nr"}	Deckel
:chip[2]{style="nr"}	Ausgießer mit Kalkfilter
:chip[3]{style="nr"}	Sockel
:chip[4]{style="nr"}	Bedienfeld: :chip[°C]{style="taste"} :chip[EIN/AUS]{style="taste"} :chip[WARM]{style="taste"}
:chip[5]{style="nr"}	Deckeltaste
:chip[6]{style="nr"}	Griff
:chip[7]{style="nr"}	Wasserstandsanzeige
:chip[8]{style="nr"}	Netzkabel mit Stecker
`; // TSV: number chip, part; in the drawing's order
const faults = String.raw`Störung	Mögliche Ursache	Abhilfe
**Das Gerät lässt sich nicht einschalten.**	Der Stecker steckt nicht, oder die Steckdose führt keinen Strom.	Stecker einstecken; die Sicherung im Sicherungskasten prüfen.
	Die Kanne sitzt nicht richtig auf dem Sockel.	Kanne abheben und gerade wieder aufsetzen.
	Der Überhitzungsschutz hat ausgelöst.	Gerät 10 Minuten abkühlen lassen, dann Wasser einfüllen.
**Die Leuchte blinkt rot.**	Das Gerät wurde leer oder mit zu wenig Wasser eingeschaltet.	Abkühlen lassen und mindestens bis MIN füllen.
	Die Elektronik meldet einen Fehler.	Netzstecker für eine Minute ziehen. Blinkt die Leuchte weiter: Kundendienst.
**Das Gerät schaltet ab, bevor das Wasser kocht.**	Eine niedrigere Temperatur ist gewählt.	Mit :chip[°C]{style="taste"} 100 °C wählen.
	Kalk bedeckt den Heizboden.	Gerät entkalken, siehe Abschnitt 4.
**Weiße Flocken schwimmen im Wasser.**	Kalk aus hartem Wasser; er ist gesundheitlich unbedenklich.	Gerät entkalken und den Kalkfilter ausspülen.
**Beim Ausgießen läuft Wasser am Deckel vorbei.**	Die Kanne ist über MAX gefüllt.	Nur bis MAX füllen.
	Der Deckel ist nicht eingerastet.	Deckel zudrücken, bis er hörbar einrastet.
**Wasser steht auf dem Sockel.**	Die Kanne war beim Aufsetzen außen nass.	Netzstecker ziehen und den Sockel trocknen lassen.
	Die Kanne ist undicht.	Gerät nicht mehr benutzen und den Kundendienst anrufen.
**Das Wasser schmeckt nach Kunststoff.**	Das Gerät ist neu.	Zweimal Wasser aufkochen und weggießen.
	Das Wasser stand lange in der Kanne.	Kanne nach Gebrauch leeren und stets frisches Wasser einfüllen.
**Kein Signalton ertönt.**	Der Signalton ist ausgeschaltet.	:chip[WARM]{style="taste"} 3 Sekunden gedrückt halten, bis die Leuchte zweimal blinkt.
`; // TSV: fault, cause, remedy
const data = String.raw`**Modell**	VW-170
**Nennspannung**	220–240 V ~, 50/60 Hz
**Nennleistung**	2200 W
**Füllmenge**	0,5 bis 1,7 Liter
**Temperaturen**	70, 80, 90, 100 °C
**Schutzklasse**	I
**Netzkabel**	75 cm
**Gewicht**	1,2 kg mit Sockel
`; // TSV: technical data, two columns

// #region tables: a TSV per table; a blank first cell shares the fault above it
function faultTable(tsv) { // parseTSV leaves the head row to you: headerRowCount
  let m = { ...parseTSV(tsv), headerRowCount: 1, columnWidths: [30, 34, 36] }; // weights
  for (let r = 2, top = 1; r < m.rows.length; r++) { // a rowspan per fault: no cut runs through it
    if (m.rows[r][0].content) top = r; // mergeCells hides what it covers: merged-cells-hiddenby
    else m = mergeCells(m, { start: { row: top, col: 0 }, end: { row: r, col: 0 } });
  }
  return m;
}
const fold = (rows, columnWidths) => ({ columnWidths, rows: rows.slice(0, rows.length / 2) // 2 up
  .map((row, k) => [...row, ...rows[k + rows.length / 2]]) });
const legend = fold(parseTSV(parts).rows.map(([chip, name]) => [{ ...chip, align: 'center' },
  name]), [7, 43, 7, 43]); // each part's number chip centred in its narrow column
const HERE = { placement: { position: 'here' } }; // at the resource's ::resource line
const table = (id, caption, model, styleId = 'bare', where = HERE) => ({ id, typeId: 'table',
  kind: 'table', caption, table: { model, styleId }, createdAt: 0, updatedAt: 0, ...where });
const tables = [table('legende', 'Teile des Verra W1', legend),
  table('daten', 'Kenndaten des VW-170', fold(parseTSV(data).rows, [20, 30, 20, 30])),
  table('stoerungen', 'Störungen und ihre Behebung', faultTable(faults), null, // the house style,
    { placement: { position: 'top' } })]; // a float, so it can split (gotcha: here-table-no-split)
// #endregion

const svgFile = (id, width = 240, height = 240) => ({ id, typeId: 'figure', kind: 'svg',
  svg: { fileId: `${id}.svg`, width, height }, createdAt: 0, updatedAt: 0 });
const resources = [
  { ...svgFile('teile', 1180, 560), ...HERE, caption: 'Der Verra W1 von links, mit Kanne und '
    + 'Sockel', altText: 'Wasserkocher von der Seite; acht Linien zeigen auf seine Teile.' },
  ...tables, svgFile('kettle', 1120, 1300), svgFile('triangle-white'), svgFile('triangle-ink'),
  svgFile('info'), // never cited, so never placed: the cover and the boxes draw them by id
];

// #region art: the kettle, its parts diagram with embedded digits, and the three notice icons
const n = (v) => +v.toFixed(2);
const svg = (w, h, body, style = '') => `<svg xmlns="http://www.w3.org/2000/svg" `
  + `width="${w * 10}" height="${h * 10}" viewBox="0 0 ${w} ${h}">${style}${body}</svg>`;
const path = (d, fill, stroke = 'none', width = 0, extra = '') => `<path d="${d}" `
  + `fill="${fill}" stroke="${stroke}" stroke-width="${width}" stroke-linejoin="round" `
  + `stroke-linecap="round"${extra}/>`;
const pill = (x, y, w, h, fill, stroke, sw) => `<rect x="${n(x)}" y="${n(y)}" width="${n(w)}" `
  + `height="${n(h)}" rx="${n(h / 2)}" fill="${fill}" stroke="${stroke}" stroke-width="${sw}"/>`;
// The kettle in profile, spout left, handle right, on a 112 × 128 grid.
const K = {
  body: 'M23 106Q17.6 106 18 101L22.8 41H77.2L82 101Q82.4 106 77 106Z',
  collar: 'M22.4 34H77.6L77.2 41H22.8Z',
  lid: 'M24 34C31.5 25.8 68.5 25.8 76 34Z',
  spout: 'M22.4 35.2L8 29.4Q5.9 28.8 7 30.8Q14.6 42.4 21.9 47.8Z',
  handle: 'M77.6 35.2H93Q100.5 35.2 100.5 42.7V83.5Q100.5 91 93 91H81.7L81.1 83.6H89Q92.4 83.6'
    + ' 92.4 80.2V46.2Q92.4 42.8 89 42.8H77.3Z',
  release: 'M80.5 30.6H90.6Q92.6 30.6 92.6 32.6V35.2H78.6Z',
  base: 'M14 106.6H86Q90.6 106.6 90.6 111.2V113.8Q90.6 118.4 86 118.4H14Q9.4 118.4 9.4 113.8'
    + 'V111.2Q9.4 106.6 14 106.6Z',
};
function kettle(line, fill, water, sw) {
  let out = ['body', 'collar', 'lid', 'handle', 'release', 'spout']
    .map((k) => path(K[k], fill, line, sw)).join('');
  out += path('M63.7 66H68.2L69.8 98H65Z', water) // water in the window
    + path('M62.5 47H67.3L69.8 98H65Z', 'none', line, sw * 0.8);
  for (const [y, w] of [[51, 3.2], [61, 1.8], [71, 1.8], [81, 1.8], [91, 3.2]]) { // MAX … MIN
    out += path(`M${n(60.4 - w)} ${y}H60.4`, 'none', line, sw * 0.7);
  }
  out += path('M11.8 32.4L18.9 35.4M13.4 35.6L19.6 38.2', 'none', line, sw * 0.6) // the filter
    + path('M30 45V98', 'none', water, sw * 1.6) // light on the steel
    + path(K.base, fill, line, sw)
    + path('M90.6 114C97.6 114.4 100.6 118.6 100.6 122.4S105 127 111 127', 'none', line, sw);
  for (const x of [33, 45, 57]) out += pill(x, 110.3, 9, 4.4, water, line, sw * 0.6); // keys
  return out;
}
function coverArt() { // a white kettle: its teal outline does not show on the teal wall
  const steam = [0, 1, 2].map((k) => path(`M${3.5 + k * 4.6} 24q-2.8-4.6 0-9.2t0-9.2`, 'none',
    palette.paper, 1.2, ' stroke-opacity=".6"')).join('');
  return svg(112, 130, `<g transform="translate(0 1)">${kettle(palette.brand, palette.paper,
    palette.tint, 1.5)}${steam}</g>`);
}
// Parts diagram, 118 × 56 mm: the kettle at half size, numbered leaders in two columns.
const [S, X0, Y0] = [0.5, 30, -8.5];
const PARTS = [ // a point on the kettle grid, then the leader's corners in mm; the disk ends it
  [[42, 28.6], [[X0 + 21, 2.5], [8, 2.5]]], [[15, 34], [[8, 14]]], [[13, 112.5], [[8, 42]]],
  [[37.5, 112.5], [[8, 52]]], [[87, 30.6], [[X0 + 43.5, 2.5], [110, 2.5]]],
  [[100.5, 58], [[110, 22]]], [[68.2, 94], [[110, 38.5]]], [[104, 125], [[110, 52]]]];
function partsArt(face) { // the digits need a face embedded in the SVG (gotcha: svg-no-webfonts)
  let out = `<g transform="translate(${X0} ${Y0}) scale(${S})">`
    + kettle(palette.ink, palette.paper, palette.tint, 1.4) + '</g>';
  PARTS.forEach(([[x, y], corners], i) => {
    const pts = [[X0 + x * S, Y0 + y * S], ...corners];
    const [dx, dy] = pts.at(-1);
    out += path(`M${pts.map(([px, py]) => `${n(px)} ${n(py)}`).join('L')}`, 'none',
      palette.brand, 0.3) + `<circle cx="${n(pts[0][0])}" cy="${n(pts[0][1])}" r="0.65" `
      + `fill="${palette.brand}"/><circle cx="${dx}" cy="${dy}" r="2.5" fill="${palette.brand}"/>`
      + `<text x="${dx}" y="${n(dy + 1.2)}" text-anchor="middle" fill="${palette.paper}">`
      + `${i + 1}</text>`;
  });
  return svg(118, 56, out, `<style>${face}text{font-family:N;font-size:3.3px}</style>`);
}
function triangle(fg, mark) { // a rounded safety alert triangle, its '!' in the band's colour
  return svg(24, 24, path('M12 2.4L22.6 20.6H1.4Z', fg, fg, 2.2) + path('M12 8.6V14.4', 'none',
    mark, 2.4) + `<circle cx="12" cy="17.6" r="1.35" fill="${mark}"/>`);
}
function info() {
  return svg(24, 24, `<circle cx="12" cy="12" r="11" fill="${palette.brand}"/>`
    + `<circle cx="12" cy="7.2" r="1.6" fill="${palette.paper}"/>`
    + path('M12 11V17.6', 'none', palette.paper, 2.8));
}
async function embeddedFace(family, weight) { // a Fontsource file as a data URL
  const id = family.toLowerCase().replace(/\s+/g, '-');
  const res = await fetch(`https://cdn.jsdelivr.net/npm/@fontsource/${id}@5/files/${id}-latin-`
    + `${weight}-normal.woff2`);
  if (!res.ok) throw new Error(`Font not found (${res.status}): ${family} ${weight}`);
  const bytes = new Uint8Array(await res.arrayBuffer());
  let bin = '';
  for (let i = 0; i < bytes.length; i += 8192) {
    bin += String.fromCharCode(...bytes.subarray(i, i + 8192));
  }
  return `@font-face{font-family:N;src:url(data:font/woff2;base64,${btoa(bin)}) format('woff2')}`;
}
const drawings = async () => ({ 'kettle.svg': coverArt(),
  'teile.svg': partsArt(await embeddedFace(DISPLAY, 700)),
  'triangle-white.svg': triangle(palette.paper, palette.warning),
  'triangle-ink.svg': triangle(palette.ink, palette.caution), 'info.svg': info() });
// #endregion

// ─── 3 · Fonts ──────────────────────────────────────────────────────────────
const FONTS = { 'Red Hat Text': ['400', '400i', '600'], // text, continuation notes, bold runs
  'Red Hat Display': ['500', '600', '700', '800'], // heads, tabs and numbers; chips; the SVG
  'Red Hat Mono': ['500', '600'] }; // running heads, keys, table heads (gotcha: fonts-first)

// ─── 4 · Build & show ───────────────────────────────────────────────────────
const allText = [markdown, parts, faults, data].join('\n');
await Promise.all([loadFonts(FONTS, allText),
  ...Object.entries(await drawings()).map(([id, markup]) => loadSvg(id, markup))]);
const doc = await buildWithFonts(() => buildDocument({ markdown, resources }, config()), allText);
showPages(doc, { title: 'Verra W1 · Bedienungsanleitung' }); // the sample is German in both

// ─── 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 ───────────────────────────────────────────────────────────────────────
```

## アレンジ

### 図と表に節ごとの番号を振る

テンプレートに節番号を入れ、節ごとにリセットすると、部品図はAbbildung 2.1、トラブルシューティングの表はTabelle 5.1になります。

```diff
-const counted = { numberingTemplate: '{n}', resetOn: 'never', counterFormat: 'decimal' }; // 1, 2…
+const counted = { numberingTemplate: '{h1}.{n}', resetOn: 'h1', counterFormat: 'decimal' };
```

## よくあるつまずき

- **Figure/TableはdefaultResourceTypes(locale)でローカライズする.** 設定のlocaleが決めるのはハイフネーションで、キャプションではありません。resourceTypesがなければ、組み込みの種類は英語でFigureとTableと表示されます。スペイン語ならresourceTypes: defaultResourceTypes('es')を渡し、それ以外の言語ではresourceTypesに名前を自分で書いてください。
- **'here'の表は分割されない.** 段やページをまたいで分割されるのはフロートの表だけで、'here'に置いた表はまとめて移動します。長い表はフロートにするか、インラインの表を短くしてください。
- **'top'のフロートは参照したページには入らない.** フロートが自分の参照より上に置かれることはありません。そのため、Nページで参照したページ幅の'top'フロートはN+1ページの頭に置かれます。もっと前で参照するか、参照したページの下端に入れる位置'auto'または'bottom'を使ってください。
- **結合セルにはhiddenByのプレースホルダーが要る。mergeCellsを使う.** セルは行の配列内の位置で配置されるため、結合セルが広がる位置にはhiddenByを付けたプレースホルダーのセルが必要です。HTMLのように省くと、それ以降の列がすべてずれます。結合はmergeCellsで作ってください。
- **リストの番号はベースラインではなく1行目の中央に置かれる.** postext 1.4.1では、番号付きリストの番号はCanvasの'middle'ベースラインで描かれ、項目の1行目のベースラインからテキストの0.3 em上が基準になります。そのため、別の書体やより大きなnumberFontSizeの番号は、行の上に立つのではなく、そこから上下に広がります。ディスプレイ書体は浮き、大きな手順番号は1行目にかぶさります。orderedLists.numberVerticalOffsetで位置を調整し、結果を原寸で確認してください。
- **SVGの<img>内のテキストはWebフォントを使えない.** SVGは画像として描かれ、画像はページのWebフォントにアクセスできないため、ラベルはシステムの書体にフォールバックします。テキストをアウトライン化するか、SVGに@font-faceのサブセットを埋め込むか、ラベルをキャプションに移してください。
- **パレットを差し替えても、デザイン要素と参照色は変わらない.** postext 1.4.1はcolorPaletteをテキストのスタイル（本文、見出し、リスト、キャプション、表、囲み）には反映しますが、ヘッダー、フッター、章扉、部扉の要素と、bodyText.referenceColorには反映しません。これらはpaletteIdの横に書いた16進の色のままです。画面用のダーク版や色替えのためにパレットを差し替えるときは、ビルドの前に、リンクしたすべての色をcolorPaletteから書き直してください。
- **ラントの修正が、描かれないトラッキングを詰めることがある.** postext 1.4.1では、段落がラント（最終行に残った短い一語）で終わると、レイアウトはその段落を1行短く組みます。まず語間を詰め、次にmaxRuntTracking（1000分の1 em単位）までの負のトラッキングを使います。CanvasとPDFのレンダラーは0より大きいトラッキングしか描かないため、トラッキングを詰めた段落はトラッキングなしで印字されます。両端そろえの行はその差の分だけ語間を失って詰まって見え、最終行は行長を超えて段の端で切れることがあります。bodyText.maxRuntTracking: 0を設定すれば語間による修正は残るので、それでも出るラントは文を書き換えてください。

- 1.4.1では、入れ子のリストの後の項目は、自分のリストではなく入れ子のリストの`itemSpacing`で間隔が決まります。手順の間が5 pt、箇条書きの間が0の場合、箇条書きの後に続く手順だけがほかより5 pt詰まります。そこでここでは、箇条書きを最後の手順の下にだけ入れています。
- 段末そろえは、ページを締めくくる囲みをページの最後のグリッド行まで押し下げるので、ページに残った余白は囲みの上に開きます（[段末そろえ](/ja/docs/configuration#段末そろえ)）。2ページ目の本文は、段をちょうど埋めるように調整しました。VORSICHTの箇条書きを1つ削っても囲みは地で終わりますが、2つの囲みの間隔は4.5 mmから13.6 mmに広がります。`headings: { balancing: { enabled: false } }`にすると、囲みはWARNUNGの1行下にとどまり、ページの終わりが2行足りなくなります。

## クレジット

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

## 関連レシピ

- [No. 008 · 色で分けた教科書の囲みのファミリー](https://postext.dev/ja/cookbook/textbook-box-family.md): 教科書の6種類の囲みを3色で組み、アイコン入りの帯、バッジ、番号付きのタブ、ピクトグラムの列、枠の外の目印でそれぞれを見分けられるようにします。 · 難易度 2 (中級) · 教科書
- [No. 048 · コードブロックを使わずに組むコードリストとキーキャップ](https://postext.dev/ja/cookbook/code-listings-and-keycaps.md): シェルの入門書。フェンス付きコードを組版前に濃色のリスト囲みへ書き換え、太字とイタリックをシンタックスの色に、キーをキーキャップのチップにします。 · 難易度 2 (中級) · マニュアル・ガイド・リファレンス
- [No. 010 · データシート：データから作る表とセル結合](https://postext.dev/ja/cookbook/technical-datasheet.md): TSVで貼り付けた表をparseTSVで読み、mergeCells、setAlignment、setCellBackgroundで整えます。レジスターマップは自動でページをまたいで分割されます。 · 難易度 3 (上級) · マニュアル・ガイド・リファレンス
