# Bedienungsanleitung mit Sicherheitshinweisen

> Eine deutsche Wasserkocher-Anleitung, deren Kästen WARNUNG und VORSICHT das Warndreieck auf einem Band in Signalfarbe tragen, mit deutschen Beschriftungen.

- HTML-Fassung: https://postext.dev/de/cookbook/product-manual-warnings
- Rezept Nº 050 · Kästen und Anmerkungen · Stufe 2 (Mittel) · Ausgaben: Canvas
- Gattungen: Handbücher und Nachschlagewerke
- Benötigt postext ≥ 1.25.0 · getestet mit 1.31.0 am 2026-10-13
- Seiten: [1](https://postext.dev/cookbook/product-manual-warnings/en/p01.webp?v=e8a22a11), [2](https://postext.dev/cookbook/product-manual-warnings/en/p02.webp?v=e8a22a11), [3](https://postext.dev/cookbook/product-manual-warnings/en/p03.webp?v=e8a22a11), [4](https://postext.dev/cookbook/product-manual-warnings/en/p04.webp?v=e8a22a11), [5](https://postext.dev/cookbook/product-manual-warnings/en/p05.webp?v=e8a22a11), [6](https://postext.dev/cookbook/product-manual-warnings/en/p06.webp?v=e8a22a11), [7](https://postext.dev/cookbook/product-manual-warnings/en/p07.webp?v=e8a22a11)
- In der Sandbox öffnen: https://postext.dev/de/sandbox#recipe=product-manual-warnings&lang=en (.postext: https://postext.dev/cookbook/product-manual-warnings/en/product-manual-warnings.postext)
- Zuletzt aktualisiert: 2026-10-12
- Andere Sprachen: [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), [fr](https://postext.dev/fr/cookbook/product-manual-warnings.md), [it](https://postext.dev/it/cookbook/product-manual-warnings.md), [pt](https://postext.dev/pt/cookbook/product-manual-warnings.md), [zh](https://postext.dev/zh/cookbook/product-manual-warnings.md), [ja](https://postext.dev/ja/cookbook/product-manual-warnings.md), [ar](https://postext.dev/ar/cookbook/product-manual-warnings.md)

## Kurz gesagt

Eine siebenseitige Bedienungsanleitung auf Deutsch für einen Wasserkocher. Sie zeigt, wie man Warnkästen mit Warndreieck baut, rot oder bernsteinfarben je nach Gefahrenstufe.

## Was Sie setzen

Eine siebenseitige Bedienungsanleitung für den Verra W1, einen erfundenen Wasserkocher, auf Deutsch in A5. Die Sicherheitsseite setzt ihre zwei Gefahrenstufen als Kästen: Ein Band in der Signalfarbe läuft an der linken Seite hinunter und trägt das Warndreieck, und ein dünner Rahmen in derselben Farbe schließt den Kasten, rot für WARNUNG und bernsteinfarben für VORSICHT. HINWEIS, die Stufe für Schäden am Gerät, verzichtet auf Band und Dreieck zugunsten einer abgerundeten Petroltönung. Der Absatz über den Kästen setzt alle drei Signalwörter als gefüllte Chips. Seite 3 zeigt eine Teilezeichnung mit nummerierten Hinweislinien über ihrer Legende, und auf Seite 4 sind die Tastennamen umrandete Chips neben großen Schrittnummern. Die Störungstabelle läuft über die Seiten 6 und 7. Die Legenden lauten Abbildung und Tabelle, und die geteilte Tabelle ist auf beiden Seiten mit Fortsetzung gekennzeichnet.

**Dieses Rezept beantwortet:**

- Wie baue ich Warnkästen mit Symbol auf farbigem Streifen, in einer Sprache, die Postext nicht beschriftet?
- Wie erstelle ich Inline-Chips: Tastaturtasten, Schlagwörter, Wortspeicher für Übungen?
- Wie passe ich Listen an: Aufzählungszeichen je Ebene, Nummerierung (a)/(i), Aufgaben-Kontrollkästchen, Abstände, die im Raster bleiben?
- Wie füge ich eine Abbildung mit nummerierter Bildunterschrift ein und verweise im Text darauf („siehe Abb. 3.2“)?
- Wie erstelle ich eine Tabelle mit Kopfzeilen, verbundenen Zellen, Spaltenbreiten und Ausrichtung je Zelle?
- Wie teile ich eine lange Tabelle über mehrere Seiten, mit wiederholtem Kopf und einem Fortsetzungsvermerk?
- Wie erhalte ich die Beschriftungen „Abbildung“ und „Tabelle“ in der Sprache meines Dokuments?

## Die kurze Antwort

```js
// script.js, Zeilen 29–47
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; a box title's line is 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
];
```

## Zutaten

**Zeigt**

- [Kastensymbole und Eckplaketten](https://postext.dev/de/docs/configuration-styles.md#kastenstile): Ein Zeichen oder Bild neben dem Inhalt eines Kastens, an seiner oberen Ecke aufgehängt oder als breiter Streifen aus Piktogrammen.
- [Kästen](https://postext.dev/de/docs/configuration-styles.md#kastenstile): Benannte Kastenformate für Hinweise, Tipps und Warnungen: Hintergrund, Rahmen, Radius, Randstreifen, Titel und eigene Typografie für Inhalt und Listen.
- [Eigene Ressourcentypen](https://postext.dev/de/docs/configuration-resources.md#ressourcentypen): Neue nummerierte Familien (Karte, Tafel, Listing, Diagramm) mit eigenen Zählern, Bezeichnungen und Präfixen für Bildunterschriften.

**Verwendet außerdem**

- [Inline-Chips](https://postext.dev/de/docs/configuration-styles.md#chip-stile)
- [Verweise, die Abbildungen platzieren](https://postext.dev/de/docs/document-format.md#verweis-im-text-die-hauptform)
- [Abbildungen genau hier](https://postext.dev/de/docs/document-format.md#blockeinbettung-optional-ausdrückliche-platzierung-im-text)
- [Platzierung von Abbildungen](https://postext.dev/de/docs/document-format.md#platzierung)
- [Tabellen aus Daten](https://postext.dev/de/docs/document-format.md#blockeinbettung-optional-ausdrückliche-platzierung-im-text)
- [Tabellenformat](https://postext.dev/de/docs/configuration-resources.md#tabellenstil)
- [Tabellen über mehrere Seiten](https://postext.dev/de/docs/configuration-resources.md#tabellen-die-höher-als-die-seite-sind)
- [Nummerierte Überschriften](https://postext.dev/de/docs/configuration-text.md#überschreibungen-pro-ebene)
- [Nummerierte Listen](https://postext.dev/de/docs/configuration-text.md#nummerierte-listen)
- [Aufzählungen und Checklisten](https://postext.dev/de/docs/configuration-text.md#aufzählungen)
- [Silbentrennung und Dokumentsprache](https://postext.dev/de/docs/justification.md#unterstützte-sprachen)
- [Abbildung und Tabelle in Ihrer Sprache](https://postext.dev/de/docs/configuration-resources.md#ressourcentypen)
- [Gestaltete Auftakte](https://postext.dev/de/docs/configuration-text.md#seitenbreite-und-erweiterte-gestaltung)
- [Überschriftenformate](https://postext.dev/de/docs/configuration-styles.md#überschriftenstile)
- [Umschläge, Titelseiten und Impressum](https://postext.dev/de/docs/configuration-styles.md#überschriftenstile)
- [Kolumnentitel und Seitenzahlen](https://postext.dev/de/docs/configuration-page-layout.md#kopf--und-fußzeilen)
- [Text, Linien und Kästen in Seitengestaltungen](https://postext.dev/de/docs/configuration-page-layout.md#kopf--und-fußzeilen)
- [Überschriftenattribute](https://postext.dev/de/docs/document-format.md#attribute-von-überschriften)
- [Seiten- und Spaltenumbrüche](https://postext.dev/de/docs/document-format.md#pagebreak)
- [Kopfzeilen nach Seitenrolle](https://postext.dev/de/docs/configuration-page-layout.md#textelemente)
- [Absatzformate](https://postext.dev/de/docs/configuration-styles.md#absatzformate)
- [Abbildungen und Tabellen als Ressourcen](https://postext.dev/de/docs/document-format.md#ressourcen)
- [Benannte Tabellenformate](https://postext.dev/de/docs/configuration-resources.md#benannte-tabellenstile)
- [Kapitel ohne Nummer](https://postext.dev/de/docs/configuration-styles.md#überschriftenstile)

**Die Konfiguration auf einen Blick**

- [`bodyText`](https://postext.dev/de/docs/configuration-text.md#fließtext), [`calloutStyles`](https://postext.dev/de/docs/configuration-styles.md#kastenstile), [`captionStyle`](https://postext.dev/de/docs/configuration-resources.md#legendenstil), [`chipStyles`](https://postext.dev/de/docs/configuration-styles.md#chip-stile), [`colorPalette`](https://postext.dev/de/docs/configuration-fonts-colors-viewers.md#farbpalette), [`footer`](https://postext.dev/de/docs/configuration-page-layout.md#kopf--und-fußzeilen), [`header`](https://postext.dev/de/docs/configuration-page-layout.md#kopf--und-fußzeilen), [`headingStyles`](https://postext.dev/de/docs/configuration-styles.md#überschriftenstile), [`headings`](https://postext.dev/de/docs/configuration-text.md#überschriften), [`layout`](https://postext.dev/de/docs/configuration-page-layout.md#layout), [`locale`](https://postext.dev/de/docs/configuration-text.md#silbentrennung), [`orderedLists`](https://postext.dev/de/docs/configuration-text.md#nummerierte-listen), [`page`](https://postext.dev/de/docs/configuration-page-layout.md#seite), [`paragraphStyles`](https://postext.dev/de/docs/configuration-styles.md#absatzformate), [`resourceTypes`](https://postext.dev/de/docs/configuration-resources.md#ressourcentypen), [`tableStyle`](https://postext.dev/de/docs/configuration-resources.md#tabellenstil), [`tableStyles`](https://postext.dev/de/docs/configuration-resources.md#benannte-tabellenstile), [`unorderedLists`](https://postext.dev/de/docs/configuration-text.md#aufzählungen)

**APIs**

- [`buildDocumentWithFonts`](https://postext.dev/de/docs/configuration-programmatic-usage.md#schriften-vor-dem-layout-laden), [`defaultResourceTypes`](https://postext.dev/de/docs/configuration-resources.md#ressourcentypen), [`inlineSvgFonts`](https://postext.dev/de/docs/configuration-resources.md#schriften-in-svg-text), [`mergeCells`](https://postext.dev/de/docs/document-format.md#blockeinbettung-optional-ausdrückliche-platzierung-im-text), [`parseTSV`](https://postext.dev/de/docs/document-format.md#blockeinbettung-optional-ausdrückliche-platzierung-im-text), [`prepareFonts`](https://postext.dev/de/docs/configuration-programmatic-usage.md#schriften-vor-dem-layout-laden), [`registerResourceImage`](https://postext.dev/de/docs/architecture.md#api-oberfläche), [`renderPageToCanvas`](https://postext.dev/de/docs/configuration-programmatic-usage.md#eine-seite-als-bitmap-rendern)

**Schriften**

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

## Zubereitung

### 1 · Ein Band, das das Dreieck trägt

Der Code steht oben in [der kurzen Antwort](https://postext.dev/de/cookbook/product-manual-warnings.md#die-kurze-antwort). `stripe` malt das Band am linken Rand hinunter. Hat ein Kasten einen seitlichen Streifen, wird das Symbol auf dem Streifen zentriert, statt eine eigene Spalte zu belegen ([Kastenformate](https://postext.dev/de/docs/configuration-styles.md#kastenstile)), und `align: 'top'` setzt die Oberkante des Symbols auf die Höhe der Oberkante des Titels. Der `border` in derselben Farbe schließt das Band zu einem Rahmen; ohne ihn stünde der Text des Kastens auf der nackten Seite neben einem farbigen Balken. `PAD_Y` ist so berechnet, dass Titel, Abstand dazu sowie oberer und unterer Innenabstand zusammen zwei Zeilen des 12,8-pt-Rasters ergeben. Der Inhalt behält den Zeilenabstand des Rasters, sodass jeder Kasten eine ganze Zahl von Zeilen hoch ist (WARNUNG 10, VORSICHT 8), und `marginTop: pt(LEAD)` lässt auf Seite 2 genau eine Zeile zwischen den beiden Kästen. VORSICHT behält Bernstein für Band und Rahmen und setzt Titel und Aufzählungszeichen in Schwarz: Bernstein auf Weiß misst 2,0:1, und kleine Schrift braucht 4,5:1. HINWEIS geht von derselben Basis aus (`...notice(…)`) und schaltet Band und Rahmen ab, sodass sein Info-Symbol eine eigene Spalte belegt und `borderRadius` eine schlichte Tönung abrundet.

### 2 · Deutsche Namen für Abbildungen und Tabellen

```js
// script.js, Zeilen 51–59
const counted = { numberingTemplate: '{n}', resetOn: 'never', counterFormat: 'decimal' }; // 1, 2…
// The built-in types in German, Abbildung and Tabelle; a table is captioned over its head.
const resourceTypes = defaultResourceTypes('de').map((type) => ({ ...type, ...counted,
  ...(type.id === 'table' && { captionStyle: { position: 'above' } }) }));
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),
  // The engine's own German strings are '(Forts.)' and 'Wird fortgesetzt': these spell them out.
  continuedSuffix: '(Fortsetzung)', continuesMarker: 'Fortsetzung auf der nächsten Seite' };
```

`locale: 'de'` gibt der Anleitung deutsche Silbentrennung und deutsche Namen für die eingebauten Typen: Ohne weiteres Zutun lauten die Legenden Abbildung 2.1 und Tabelle 5.1, und der Hinweis unter dem ersten Teil der geteilten Tabelle sagt Wird fortgesetzt. Das Tabellenformat schreibt diesen Hinweis und den Zusatz der Legende aus, und `defaultResourceTypes('de')` wird für zwei Änderungen übergeben. `counted` ist die erste: `{n}` mit `resetOn: 'never'` zählt durch die ganze Anleitung (Abbildung 1, Tabelle 1 bis 3), statt in jedem Abschnitt neu zu beginnen. Nur der Tabellentyp setzt `captionStyle.position: 'above'`, sodass Tabellen ihre Legende über dem Kopf tragen und die Zeichnung ihre Legende darunter behält.

### 3 · Tasten, Signalwörter und Teilenummern als Chips

```js
// script.js, Zeilen 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` umrandet einen Tastennamen in der Monospace-Schrift ohne Füllung, so wie die Beschriftung auf die Taste gedruckt ist. Die Signalwort-Chips sind mit den Farben ihrer Kästen gefüllt, und der bernsteinfarbene Chip setzt sein Wort in Schwarz, wie der Titel VORSICHT. `nr` gibt den Teilenummern einen Radius größer als ihre halbe Höhe, sodass sie als Scheiben erscheinen wie die nummerierten Kreise in der Zeichnung. Chipgrößen werden in em des umgebenden Textes angegeben: Eine Taste misst im Fließtext 8,6 pt und in einer Tabellenzelle 7,5 pt ([Chip-Formate](https://postext.dev/de/docs/configuration-styles.md#chip-stile)).

### 4 · Schrittnummern auf der Grundlinie

```js
// script.js, Zeilen 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 –
```

Die Schritte sind eine nummerierte Liste mit den Nummern in Red Hat Display 800 zu 15 pt, in Petrol, ohne Trennzeichen. Eine Listennummer wird auf ihrer ersten Zeile zentriert, nicht auf die Grundlinie gestellt, sodass eine 15-pt-Ziffer neben 9,3-pt-Text etwa 1 pt unter der Zeile hängt. `numberVerticalOffset: pt(-1)`, ein an der Aufnahme gemessener Wert, hebt ihren Fuß auf die Grundlinie. `itemSpacing` trennt die Schritte um 5 pt, um Platz für die großen Ziffern zu schaffen. Die unter einem Schritt verschachtelten Aufzählungspunkte beginnen bei 6,5 mm, auf Höhe des Schritttextes; es sind petrolfarbene Gedankenstriche, und die dritte Ebene hat graue ([nummerierte Listen](https://postext.dev/de/docs/configuration-text.md#nummerierte-listen)).

### 5 · Abschnittsnummern in einem negativen Reiter

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

Jeder Abschnitt ist eine Überschrift der ersten Ebene in der Spalte, gezeichnet aus einem Gestaltungsplatz. Der Reiter druckt `{number}` (gefüllt durch `numberingTemplate: '{1}'`) weiß auf einem petrolfarbenen Quadrat, und der Titel beginnt 3 mm rechts davon. Nummer und Titel teilen Größe und Zeilenabstand, sodass derselbe obere Innenabstand sie auf eine Grundlinie setzt. Der Reiter ist ein Quadrat von 22 pt, 3,6 pt weniger als zwei Zeilen des 12,8-pt-Rasters, sodass die Überschrift innerhalb von zwei Rasterzeilen bleibt. Auch der Umschlag ist eine Überschrift der ersten Ebene, gesetzt in einem unnummerierten Überschriftenformat, sodass „Sicherheitshinweise“ Abschnitt 1 ist.

### 6 · Eine Störungstabelle mit verbundenen Zellen, die umbricht

```js
// script.js, Zeilen 293–311
function faultTable(tsv) { // the first line is the head row; the widths are weights
  let m = { ...parseTSV(tsv, { headerRows: 1 }), columnWidths: [30, 34, 36] };
  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: it heads the page after its citation
```

Jede Tabelle ist TSV in einer eigenen Inhaltsdatei. In den Störungsdaten gehört eine Zeile, deren erste Zelle leer ist, zur Störung darüber, und `mergeCells` verbindet diese ersten Zellen zu einer hohen Zelle und markiert die überdeckten Zellen als verborgen. `parseTSV` setzt keine Kopfzeile, darum deklariert `headerRowCount: 1` sie, und diese Zeile wird auf Seite 7 wiederholt. Die Tabelle ist höher als eine Seite, wird also geteilt und geht auf der Rückseite weiter. `position: 'top'` macht sie zu einem Gleitobjekt am Kopf von Seite 6; mit `'here'` würde sie seit postext 1.25 genauso geteilt, aber der Text danach begänne auf Seite 7 tiefer und schöbe die letzten Zeilen auf eine achte Seite. Der Schnitt fällt zwischen Zeilen und nie durch eine verbundene Zelle, sodass jede Störung auf derselben Seite bleibt wie ihre Ursachen ([Tabellen, die höher als die Seite sind](https://postext.dev/de/docs/configuration-resources.md#tabellen-die-höher-als-die-seite-sind)). Sie wird auf Seite 5 zitiert und führt Seite 6 an, die erste Seite nach ihrem Zitat, die ein Gleitobjekt am Kopf aufnehmen kann. `fold` setzt zwei Hälften einer Liste nebeneinander, für die Legende und für die technischen Daten, und die Legende zentriert ihre Nummern-Chips mit `align: 'center'`.

## Das ganze Rezept

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

- Quellordner: https://github.com/drnachio/postext/tree/main/cookbook/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.25.0
import {
  buildDocumentWithFonts, prepareFonts, renderPageToCanvas, registerResourceImage, parseTSV,
  mergeCells, defaultResourceTypes, inlineSvgFonts } 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
// Each colour carries its hex and the palette entry it follows.
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; a box title's line is 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…
// The built-in types in German, Abbildung and Tabelle; a table is captioned over its head.
const resourceTypes = defaultResourceTypes('de').map((type) => ({ ...type, ...counted,
  ...(type.id === 'table' && { captionStyle: { position: 'above' } }) }));
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),
  // The engine's own German strings are '(Forts.)' and 'Wird fortgesetzt': these spell them out.
  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', // an 'opener' page: the running head and the folio keep to 'body' pages
  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 = () => ({
  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
  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) { // the first line is the head row; the widths are weights
  let m = { ...parseTSV(tsv, { headerRows: 1 }), columnWidths: [30, 34, 36] };
  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: it heads the page after its citation
// #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, face = '') => `<svg xmlns="http://www.w3.org/2000/svg" `
  + `width="${w * 10}" height="${h * 10}" viewBox="0 0 ${w} ${h}"${face}>${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() { // the digits name their face on the root; loadSvg embeds it
  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, ` font-family="${DISPLAY}" font-weight="700" font-size="3.3"`);
}
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));
}
const drawings = () => ({ 'kettle.svg': coverArt(), 'teile.svg': partsArt(),
  '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

// ─── 4 · Build & show ───────────────────────────────────────────────────────
const allText = [markdown, parts, faults, data].join('\n');
await Promise.all([prepareFonts(allText, config(), kitFonts(FONTS)),
  ...Object.entries(drawings()).map(([id, markup]) => loadSvg(id, markup))]);
const doc = await buildDocumentWithFonts({ markdown, resources }, config(),
  { ...kitFonts(FONTS), text: 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 v3 ── the same in every recipe · postext.dev/cookbook
// Postext loads the faces a config asks for before layout (prepareFonts,
// buildDocumentWithFonts) and asks the kit for the files: Fontsource's, the
// files the PDF embeds too.

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

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

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

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

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

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

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

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

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

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

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

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

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

// ─── Kit · images v3 ── 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 as a vector image, its fonts inside (inlineSvgFonts, ≥ 1.25).
 *  `options` go to registerResourceImage: { singleInk: true } tints it on the canvas. */
async function loadSvg(fileId, svg, options) {
  if (typeof inlineSvgFonts === 'function') svg = await inlineSvgFonts(svg, svgFaces);
  const img = new Image();
  img.src = `data:image/svg+xml;charset=utf-8,${encodeURIComponent(svg)}`;
  await img.decode();
  registerResourceImage(fileId, img, options);
  (loadImage.bytes ??= new Map()).set(fileId, new TextEncoder().encode(svg));
}

/** A face's Fontsource files: nearest weight; latin + what the SVG sets. */
async function svgFaces(family, weight, style, { codePoints = [] } = {}) {
  const [id, m] = [fontsourceId(family), await fontsourceMeta(family)];
  const w = m?.weights.reduce((a, b) => (Math.abs(b - weight) < Math.abs(a - weight) ? b : a)) ?? weight;
  const s = m?.styles.includes(style) === false ? 'normal' : style;
  return Promise.all(['latin', ...kitSubsetsFor(String.fromCodePoint(...codePoints), m)].map(async (x) => new Uint8Array(
    await (await fetch(`${await fontsourceAt(id)}files/${id}-${x}-${w}-${s}.woff2`)).arrayBuffer())));
}

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

## Varianten

### Abbildungen und Tabellen nach Abschnitt nummerieren

Mit der Abschnittsnummer in der Vorlage und einem Neustart in jedem Abschnitt wird die Zeichnung zu Abbildung 2.1 und die Störungstabelle zu Tabelle 5.1.

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

## Häufige Fehler

- **Ein 'top'-Gleitobjekt kommt auf die Seite nach seinem Verweis, außer es darf die verweisende Seite eröffnen.** Standardmäßig steht ein Gleitobjekt nie über seinem eigenen Verweis, sodass ein seitenbreites 'top'-Gleitobjekt, auf das auf Seite N verwiesen wird, Seite N+1 eröffnet. Setzen Sie placement.citingPage: true an der Ressource oder am defaultPlacement ihres Typs (oder layout.floatsAtCitingPage für das ganze Buch, seit postext 1.25), dann steht es stattdessen oben auf Seite N, und der Text über dem Verweis rückt darunter, sofern es in layout.maxTopFraction passt und die verweisende Zeile auf dieser Seite bleibt. Es eröffnet nie die erste Seite eines Kapitels oder eine Seite nach einem erzwungenen Umbruch: dort eröffnet es weiterhin die nächste Seite, und 'auto' oder 'bottom' können den Fuß der verweisenden Seite einnehmen.
- **Verbundene Zellen brauchen hiddenBy-Platzhalter: mergeCells verwenden.** Zellen werden nach ihrer Position im Array der Zeile gesetzt, deshalb braucht eine verbundene Zelle Platzhalterzellen mit hiddenBy, wo sie sich ausdehnt; lässt man sie weg, wie in HTML, verschiebt sich jede spätere Spalte. Erstellen Sie Verbindungen mit mergeCells. Seit postext 1.5 wird eine Tabelle mit unregelmäßigem Raster in doc.contentWarnings als raggedTableGrid gemeldet, und parseTSV(text, { headerRows }) kennzeichnet die Kopfzeilen.
- **Eine Listennummer wird auf ihrer ersten Zeile zentriert, nicht auf die Grundlinie gesetzt.** Die Nummer einer nummerierten Liste wird mit der Canvas-Grundlinie 'middle' gezeichnet, 0,3 em der Schrift über der ersten Grundlinie des Eintrags. Eine Nummer in einer anderen Schrift oder mit größerem numberFontSize wächst daher von dort nach oben und unten, statt auf der Zeile zu stehen: Eine Displayschrift sitzt zu hoch, und eine große Schrittnummer hängt über die erste Zeile. Positionieren Sie sie mit orderedLists.numberVerticalOffset und prüfen Sie das Ergebnis in voller Größe. Die Nummern im Inhaltsverzeichnis stehen seit postext 1.5 auf der Grundlinie; Listennummern sind weiterhin zentriert.

- Der Spaltenausgleich schiebt einen Kasten, der eine Seite abschließt, auf die letzte Rasterzeile der Seite, sodass jeder auf der Seite übrige Platz sich über dem Kasten öffnet ([Spaltenausgleich](https://postext.dev/de/docs/configuration-text.md#spaltenausgleich)). Der Text von Seite 2 wurde so eingepasst, dass er seine Spalte genau füllt. Löschen Sie einen Aufzählungspunkt bei VORSICHT, und der Kasten endet weiterhin am Fuß, während der Abstand zwischen den beiden Kästen von 4,5 auf 13,6 mm wächst; mit `headings: { balancing: { enabled: false } }` bleibt der Kasten eine Zeile unter WARNUNG, und die Seite endet zwei Zeilen zu früh.

## Nachweise

- Rezept: Ignacio Ferro ([@drnachio](https://github.com/drnachio))
- Schrift: Red Hat Text (OFL-1.1), Red Hat Display (OFL-1.1), Red Hat Mono (OFL-1.1)
- Code: MIT · Beispielinhalt: CC-BY-4.0

## Verwandte Rezepte

- [Nº 008 · Eine farbcodierte Familie von Lehrbuchkästen](https://postext.dev/de/cookbook/textbook-box-family.md): Sechs Arten von Lehrbuchkästen in drei Farben, unterschieden durch Streifen mit Icon, Badge, nummerierten Reiter, Piktogrammleiste oder Marke neben dem Rahmen. · Stufe 2 (Mittel) · Lehrbücher
- [Nº 048 · Codelistings und Tastenkappen](https://postext.dev/de/cookbook/code-listings-and-keycaps.md): Ein Shell-Leitfaden, dessen Codeblöcke als dunkle Listing-Kästen gesetzt sind, nach Token eingefärbt, mit Tasten als Chips in Form von Tastenkappen. · Stufe 2 (Mittel) · Handbücher und Nachschlagewerke
- [Nº 010 · Datenblatt: Tabellen aus Daten, verbundene Köpfe](https://postext.dev/de/cookbook/technical-datasheet.md): Als TSV eingefügte Tabellen, mit parseTSV gelesen und mit mergeCells, setAlignment und setCellBackground geformt; eine Registertabelle, die selbst umbricht. · Stufe 3 (Fortgeschritten) · Handbücher und Nachschlagewerke
