# Manual de produto com avisos de segurança

> O manual de uma chaleira em alemão: os boxes WARNUNG e VORSICHT levam o triângulo numa faixa da cor de alerta, e as legendas dizem Abbildung e Tabelle.

- Versão HTML: https://postext.dev/pt/cookbook/product-manual-warnings
- Receita Nº 050 · Boxes e notas · Nível 2 (Intermediário) · Saídas: Canvas
- Gêneros: Manuais, guias e obras de referência
- Requer postext ≥ 1.4.1 · testada com 1.19.1 em 2026-10-06
- Páginas: [1](https://postext.dev/cookbook/product-manual-warnings/en/p01.webp?v=4177ea74), [2](https://postext.dev/cookbook/product-manual-warnings/en/p02.webp?v=4177ea74), [3](https://postext.dev/cookbook/product-manual-warnings/en/p03.webp?v=4177ea74), [4](https://postext.dev/cookbook/product-manual-warnings/en/p04.webp?v=4177ea74), [5](https://postext.dev/cookbook/product-manual-warnings/en/p05.webp?v=4177ea74), [6](https://postext.dev/cookbook/product-manual-warnings/en/p06.webp?v=4177ea74), [7](https://postext.dev/cookbook/product-manual-warnings/en/p07.webp?v=4177ea74)
- Abrir no Sandbox: https://postext.dev/pt/sandbox#recipe=product-manual-warnings&lang=en (.postext: https://postext.dev/cookbook/product-manual-warnings/en/product-manual-warnings.postext)
- Última atualização: 2026-09-26
- Outros idiomas: [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), [ja](https://postext.dev/ja/cookbook/product-manual-warnings.md), [ar](https://postext.dev/ar/cookbook/product-manual-warnings.md)

## Em poucas palavras

Um manual de instruções de sete páginas, em alemão, para uma chaleira elétrica. Mostra como fazer quadros de aviso de segurança com o triângulo de alerta, em vermelho ou âmbar conforme o nível de perigo.

## O que você vai compor

Um manual de instruções de sete páginas para a Verra W1, uma chaleira elétrica inventada, em alemão e em A5. A página de segurança põe os dois níveis de perigo em boxes: uma faixa na cor de alerta desce pelo lado esquerdo e leva o triângulo de advertência, e uma moldura fina da mesma cor fecha o box, vermelha em WARNUNG e âmbar em VORSICHT. HINWEIS, o nível para danos ao aparelho, troca a faixa e o triângulo por um fundo verde-azulado de cantos arredondados. O parágrafo acima dos boxes traz as três palavras de advertência em chips preenchidos. A página 3 tem um desenho das peças com linhas de chamada numeradas sobre a legenda, e na página 4 os nomes das teclas são chips contornados ao lado de números de passo grandes. A tabela de solução de problemas ocupa as páginas 6 e 7. As legendas dizem Abbildung e Tabelle, e a tabela dividida leva Fortsetzung nas duas páginas.

**Esta receita responde a:**

- Como faço boxes de aviso com um ícone sobre uma faixa colorida, num idioma que o Postext não rotula?
- Como faço chips em linha: teclas, etiquetas, bancos de palavras para exercícios?
- Como personalizo listas: marcadores por nível, numeração (a)/(i), caixas de seleção de tarefas e um espaçamento que fique na grade?
- Como acrescento uma figura com legenda numerada e a cito no texto (“ver fig. 3.2”)?
- Como faço uma tabela com linhas de cabeçalho, células mescladas, larguras de coluna e alinhamento por célula?
- Como divido uma tabela longa entre páginas com o cabeçalho repetido e um aviso de “continua”?
- Como faço os rótulos “Figura” e “Tabela” saírem no idioma do meu documento?

## A resposta curta

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

## Ingredientes

**Ensina**

- [Ícones e selos de canto nos boxes](https://postext.dev/pt/docs/configuration.md#estilos-de-boxe): Um glifo ou uma imagem ao lado do conteúdo de um boxe, pendurado no canto superior ou disposto como uma faixa larga de pictogramas.
- [Boxes](https://postext.dev/pt/docs/configuration.md#estilos-de-boxe): Estilos de boxe com nome para notas, dicas e advertências: fundo, borda, raio, faixa, título e tipografia própria para o texto e as listas.
- [Tipos de recurso personalizados](https://postext.dev/pt/docs/configuration.md#tipos-de-recurso): Novas famílias numeradas (Mapa, Prancha, Listagem, Gráfico) com contadores, rótulos e prefixos de legenda próprios.

**Também usa**

- [Chips no texto](https://postext.dev/pt/docs/configuration.md#estilos-de-chip)
- [Citações que posicionam as figuras](https://postext.dev/pt/docs/document-format.md#referência-em-linha-a-forma-principal)
- [Figuras exatamente aqui](https://postext.dev/pt/docs/document-format.md#inserção-em-bloco-opcional-posicionamento-explícito-em-linha)
- [Posicionamento de figuras](https://postext.dev/pt/docs/document-format.md#posicionamento)
- [Tabelas a partir de dados](https://postext.dev/pt/docs/document-format.md#inserção-em-bloco-opcional-posicionamento-explícito-em-linha)
- [Estilo de tabela](https://postext.dev/pt/docs/configuration.md#estilo-de-tabela)
- [Tabelas que passam de página](https://postext.dev/pt/docs/configuration.md#tabelas-mais-altas-que-a-página)
- [Títulos numerados](https://postext.dev/pt/docs/configuration.md#sobrescritas-por-nível)
- [Listas numeradas](https://postext.dev/pt/docs/configuration.md#listas-numeradas)
- [Listas com marcadores e de verificação](https://postext.dev/pt/docs/configuration.md#listas-com-marcadores)
- [Hifenização e idioma do documento](https://postext.dev/pt/docs/justification.md#idiomas-compatíveis)
- [Figura e Tabela no seu idioma](https://postext.dev/pt/docs/configuration.md#tipos-de-recurso)
- [Aberturas desenhadas](https://postext.dev/pt/docs/configuration.md#largura-e-design-avançado)
- [Estilos de título](https://postext.dev/pt/docs/configuration.md#estilos-de-título)
- [Capas, folhas de rosto e colofões](https://postext.dev/pt/docs/configuration.md#estilos-de-título)
- [Cabeços e fólios](https://postext.dev/pt/docs/configuration.md#cabeços-e-rodapés)
- [Textos, fios e caixas nos designs de página](https://postext.dev/pt/docs/configuration.md#cabeços-e-rodapés)
- [Atributos de título](https://postext.dev/pt/docs/document-format.md#atributos-de-título)
- [Quebras de página e de coluna](https://postext.dev/pt/docs/document-format.md#pagebreak)
- [Cabeços por tipo de página](https://postext.dev/pt/docs/configuration.md#elementos-de-texto)
- [Estilos de parágrafo](https://postext.dev/pt/docs/configuration.md#estilos-de-parágrafo)
- [Figuras e tabelas como recursos](https://postext.dev/pt/docs/document-format.md#recursos)
- [Estilos de tabela com nome](https://postext.dev/pt/docs/configuration.md#estilos-de-tabela-nomeados)
- [Capítulos sem número](https://postext.dev/pt/docs/configuration.md#estilos-de-título)

**A configuração em resumo**

- [`bodyText`](https://postext.dev/pt/docs/configuration.md#texto-do-corpo), [`calloutStyles`](https://postext.dev/pt/docs/configuration.md#estilos-de-boxe), [`captionStyle`](https://postext.dev/pt/docs/configuration.md#estilo-de-legenda), [`chipStyles`](https://postext.dev/pt/docs/configuration.md#estilos-de-chip), [`colorPalette`](https://postext.dev/pt/docs/configuration.md#paleta-de-cores), [`footer`](https://postext.dev/pt/docs/configuration.md#cabeços-e-rodapés), [`header`](https://postext.dev/pt/docs/configuration.md#cabeços-e-rodapés), [`headingStyles`](https://postext.dev/pt/docs/configuration.md#estilos-de-título), [`headings`](https://postext.dev/pt/docs/configuration.md#títulos), [`layout`](https://postext.dev/pt/docs/configuration.md#diagramação), [`locale`](https://postext.dev/pt/docs/configuration.md#hifenização), [`orderedLists`](https://postext.dev/pt/docs/configuration.md#listas-numeradas), [`page`](https://postext.dev/pt/docs/configuration.md#página), [`paragraphStyles`](https://postext.dev/pt/docs/configuration.md#estilos-de-parágrafo), [`resourceTypes`](https://postext.dev/pt/docs/configuration.md#tipos-de-recurso), [`tableStyle`](https://postext.dev/pt/docs/configuration.md#estilo-de-tabela), [`tableStyles`](https://postext.dev/pt/docs/configuration.md#estilos-de-tabela-nomeados), [`unorderedLists`](https://postext.dev/pt/docs/configuration.md#listas-com-marcadores)

**API**

- [`buildDocument`](https://postext.dev/pt/docs/configuration.md#compilar-um-documento), [`clearMeasurementCache`](https://postext.dev/pt/docs/configuration.md#cache-de-medidas), [`mergeCells`](https://postext.dev/pt/docs/document-format.md#inserção-em-bloco-opcional-posicionamento-explícito-em-linha), [`parseTSV`](https://postext.dev/pt/docs/document-format.md#inserção-em-bloco-opcional-posicionamento-explícito-em-linha), [`registerResourceImage`](https://postext.dev/pt/docs/architecture.md#superfície-da-api), [`renderPageToCanvas`](https://postext.dev/pt/docs/configuration.md#renderizar-uma-página-como-bitmap)

**Tipos**

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

## Preparo

### 1 · Uma faixa que leva o triângulo

O código é [a resposta curta](https://postext.dev/pt/cookbook/product-manual-warnings.md#a-resposta-curta) logo acima. `stripe` pinta a faixa pela borda esquerda. Quando um box tem faixa lateral, a 1.4.1 centraliza o ícone sobre a faixa em vez de lhe dar uma coluna própria ([estilos de boxe](https://postext.dev/pt/docs/configuration.md#estilos-de-boxe)), e `align: 'top'` alinha a borda superior do ícone com a do título. A `border` da mesma cor fecha a faixa numa moldura; sem ela, o texto do box ficaria solto na página, ao lado de uma barra colorida. `PAD_Y` é calculado para que o título, o espaço abaixo dele e os enchimentos superior e inferior somem duas linhas da grade de 12,8 pt. O corpo mantém a entrelinha da grade, e assim cada box tem um número inteiro de linhas de altura (10 em WARNUNG, 8 em VORSICHT), e `marginTop: pt(LEAD)` deixa exatamente uma linha entre os dois boxes da página 2. VORSICHT mantém o âmbar na faixa e na moldura e põe o título e os marcadores na cor do texto: âmbar sobre branco dá um contraste de 2,0:1, e texto pequeno precisa de 4,5:1. HINWEIS parte da mesma base (`...notice(…)`) e desliga a faixa e a moldura, então seu ícone de informação ganha uma coluna própria e `borderRadius` arredonda um fundo liso.

### 2 · Nomes em alemão para figuras e tabelas

```js
// script.js, linhas 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'` dá ao manual a hifenização alemã, mas a 1.4.1 só traz em inglês e em espanhol os nomes dos tipos de recurso e as notas de continuação de uma tabela dividida. Sem estas linhas, as legendas diriam Figure 2.1 e Table 5.1, e sob a primeira parte da tabela dividida se leria Continued. Um tipo de recurso escrito à mão precisa informar todos os campos, inclusive os da numeração, e é isso que `counted` fornece: `{n}` com `resetOn: 'never'` numera o manual inteiro em sequência (Abbildung 1, Tabelle 1 a 3) em vez de recomeçar em cada seção. Só o tipo tabela define `captionStyle.position: 'above'`, então as tabelas levam a legenda acima do cabeçalho e o desenho mantém a sua embaixo.

### 3 · Teclas, palavras de advertência e números de peça como chips

```js
// script.js, linhas 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` contorna o nome de uma tecla na fonte monoespaçada, sem preenchimento, do jeito que o rótulo vem impresso no botão. Os chips das palavras de advertência são preenchidos com as cores dos seus boxes, e o chip âmbar põe a palavra na cor do texto, como o título de VORSICHT. `nr` dá aos números de peça um raio maior que metade da altura, e eles saem como discos, iguais aos círculos numerados do desenho. O tamanho dos chips é dado em em do texto ao redor: uma tecla tem 8,6 pt no texto corrido e 7,5 pt numa célula de tabela ([estilos de chip](https://postext.dev/pt/docs/configuration.md#estilos-de-chip)).

### 4 · Números de passo na linha de base

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

Os passos são uma lista numerada com os algarismos em Red Hat Display 800 de 15 pt, em verde-azulado, sem separador. A 1.4.1 centraliza o número de uma lista na primeira linha em vez de apoiá-lo na linha de base, e um algarismo de 15 pt ao lado de texto de 9,3 pt fica cerca de 1 pt abaixo da linha. `numberVerticalOffset: pt(-1)`, um valor medido na captura, ergue o pé do algarismo até a linha de base. `itemSpacing` separa os passos em 5 pt para dar espaço aos algarismos grandes. Os marcadores aninhados sob um passo começam a 6,5 mm, na altura do texto do passo; são travessões verde-azulados, e os do terceiro nível são cinza ([listas numeradas](https://postext.dev/pt/docs/configuration.md#listas-numeradas)).

### 5 · Números de seção numa aba invertida

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

Cada seção é um título de nível 1 dentro da coluna, desenhado a partir de um encaixe de design. A aba imprime `{number}` (preenchido por `numberingTemplate: '{1}'`) em branco sobre um quadrado verde-azulado, e o título começa 3 mm à direita dela. Número e título têm o mesmo corpo e a mesma entrelinha, e por isso o mesmo enchimento superior os assenta na mesma linha de base. A aba é um quadrado de 22 pt, 3,6 pt a menos que duas linhas da grade de 12,8 pt, e o título não passa de duas linhas da grade. A capa também é um título de nível 1, com um estilo de título sem número, de modo que Sicherheitshinweise é a seção 1.

### 6 · Uma tabela de solução de problemas com células mescladas que se divide

```js
// script.js, linhas 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)
```

Cada tabela é um TSV num arquivo de conteúdo próprio. Nos dados de solução de problemas, uma linha cuja primeira célula está vazia pertence ao defeito de cima, e `mergeCells` junta essas primeiras células numa única célula alta e marca como ocultas as que ela cobre. `parseTSV` não define linha de cabeçalho, então `headerRowCount: 1` a declara, e é essa linha que se repete na página 7. A tabela é mais alta que uma página, e na 1.4.1 só uma tabela flutuante se divide; por isso `position: 'top'` a transforma em flutuante. O corte cai entre linhas e nunca atravessa uma célula mesclada, e cada defeito fica na mesma página que suas causas ([tabelas mais altas que a página](https://postext.dev/pt/docs/configuration.md#tabelas-mais-altas-que-a-página)). Ela é citada na página 5 e abre a página 6, a primeira depois da citação que uma flutuante no topo pode ocupar. `fold` põe as duas metades de uma lista lado a lado, na legenda e nos dados técnicos, e a legenda centraliza os chips de número com `align: 'center'`.

## A receita completa

Um único arquivo, composto a partir da pasta da receita com o texto de exemplo e o kit comum das Receitas já incluídos; ele monta a própria página. Para executá-lo, coloque-o em um `<script type="module">` de uma página vazia ou cole-o no painel JS de um pen novo do CodePen (como módulo). Ele importa o postext do esm.sh, então não há nada para instalar nem compilar.

- Pasta da receita: 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 v2 ── the same in every recipe · postext.dev/cookbook
// Postext measures with the loaded faces and caches the widths: load every face
// before the first build, from Fontsource, the files the PDF embeds too.

/** 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.
 *  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',
    greek: 'U+0370-03FF',
  };
  const jobs = [];
  let added = 0;
  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 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 = `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` and loads any face the pages use that FONTS missed (a regular
 *  one with a warning), then clears the measurement cache and builds 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. */
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' };
}

/** 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, '-'); }

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

## Variações

### Numere figuras e tabelas por seção

Com o número da seção no modelo e um reinício em cada seção, o desenho vira Abbildung 2.1 e a tabela de solução de problemas, Tabelle 5.1.

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

## Erros comuns

- **Traduza Figura e Tabela com defaultResourceTypes(locale).** O locale da configuração define a hifenização, não as legendas: sem resourceTypes, os tipos embutidos dizem Figure e Table, em inglês. Passe resourceTypes: defaultResourceTypes('es') para o espanhol; para qualquer outro idioma, escreva você mesmo os nomes em resourceTypes.
- **Uma tabela 'here' nunca se divide.** Só as tabelas flutuantes se dividem entre colunas e páginas; uma tabela posicionada como 'here' se move inteira. Deixe as tabelas longas flutuarem ou mantenha curtas as tabelas em linha.
- **Um flutuante 'top' nunca cai na página que o cita.** Um flutuante nunca fica acima da própria referência, então um flutuante 'top' na largura da página citado na página N abre a página N+1. Cite-o antes, ou use a posição 'auto' ou 'bottom', que podem ocupar o pé da página que o cita.
- **Células mescladas precisam de hiddenBy: use mergeCells.** As células são posicionadas pela posição no array da linha, então uma célula mesclada precisa de células de preenchimento marcadas com hiddenBy onde ela se estende; omiti-las, como no HTML, desloca todas as colunas seguintes. Crie as mesclagens com mergeCells.
- **O número de uma lista é centralizado na primeira linha, não apoiado na linha de base.** No postext 1.4.1, o número de uma lista numerada é pintado com a linha de base 'middle' do canvas, 0,3 em do texto acima da primeira linha de base do item, então um número em outra fonte ou com numberFontSize maior cresce para cima e para baixo a partir daí, em vez de se apoiar na linha: uma fonte de display fica alta, e um número grande de passo fica pendurado sobre a primeira linha. Posicione-o com orderedLists.numberVerticalOffset e confira o resultado em tamanho real.
- **O texto dentro de um SVG <img> não pode usar fontes web.** Um SVG é desenhado como imagem, e uma imagem não tem acesso às fontes web da página, então os rótulos dele caem em uma fonte do sistema. Converta o texto em contornos, incorpore um subconjunto @font-face no SVG ou passe os rótulos para a legenda.
- **Uma paleta trocada não chega aos elementos de design nem à cor das referências.** postext 1.4.1 aplica colorPalette aos estilos de texto (corpo, títulos, listas, legendas, tabelas, boxes), mas não aos elementos de cabeçalhos, rodapés, aberturas e páginas de parte, nem a bodyText.referenceColor: eles mantêm o hex escrito ao lado do seu paletteId. Se você trocar a paleta, para uma edição de tela escura ou para mudar as cores, reescreva cada cor vinculada a partir de colorPalette antes de compor.
- **O ajuste de linhas curtas pode apertar um tracking que nunca é pintado.** No postext 1.4.1, quando um parágrafo termina numa linha curta, a diagramação o compõe com uma linha a menos: primeiro aperta o espaçamento entre palavras, depois aplica até maxRuntTracking milésimos de em de tracking negativo. Os renderizadores de canvas e PDF só pintam tracking acima de zero, então o parágrafo sai impresso sem ele: as linhas justificadas perdem essa diferença nos espaços entre palavras, que ficam esmagados, e a última linha pode passar da medida e ser cortada na borda da coluna. Defina bodyText.maxRuntTracking: 0, que mantém o ajuste pelo espaçamento entre palavras, e reescreva os parágrafos que voltarem a terminar numa linha curta.

- Na 1.4.1, o item que vem depois de uma lista aninhada recebe o `itemSpacing` da lista aninhada, não o da sua própria lista. Com 5 pt entre os passos e nenhum entre os marcadores, um passo que viesse depois de uma lista com marcadores ficaria 5 pt mais perto que os outros; por isso, aqui, só o último passo tem marcadores.
- O equilíbrio de colunas empurra um box que fecha uma página até a última linha da grade, e todo o espaço que sobra na página se abre acima do box ([equilíbrio de colunas](https://postext.dev/pt/docs/configuration.md#equilíbrio-de-colunas)). O texto da página 2 foi ajustado para encher a coluna sem sobra. Apague um marcador de VORSICHT e o box continua terminando no pé, mas o espaço entre os dois boxes passa de 4,5 para 13,6 mm; com `headings: { balancing: { enabled: false } }`, o box fica uma linha abaixo de WARNUNG e a página termina duas linhas antes do pé.

## Créditos

- Receita: Ignacio Ferro ([@drnachio](https://github.com/drnachio))
- Tipos: Red Hat Text (OFL-1.1), Red Hat Display (OFL-1.1), Red Hat Mono (OFL-1.1)
- Código: MIT · Conteúdo de exemplo: CC-BY-4.0

## Relacionadas

- [Nº 008 · Uma família de boxes de livro didático com código de cores](https://postext.dev/pt/cookbook/textbook-box-family.md): Seis tipos de boxe de livro didático em três cores, diferenciados por faixa com ícone, selo, aba numerada, pictogramas ou um indicador fora da moldura. · Nível 2 (Intermediário) · Livros didáticos
- [Nº 048 · Listagens de código e teclas sem blocos de código](https://postext.dev/pt/cookbook/code-listings-and-keycaps.md): Um guia do terminal cujos blocos de código viram boxes escuros antes da composição, com negrito e itálico como cores de sintaxe e as teclas como chips. · Nível 2 (Intermediário) · Manuais, guias e obras de referência
- [Nº 010 · Datasheet: tabelas a partir de dados, cabeçalhos mesclados](https://postext.dev/pt/cookbook/technical-datasheet.md): Tabelas coladas como TSV, lidas com parseTSV e moldadas com mergeCells, setAlignment e setCellBackground; um mapa de registradores que se divide sozinho. · Nível 3 (Avançado) · Manuais, guias e obras de referência
