# Edizione di lettere: date topiche e firme

> Lettere che si susseguono senza salti di pagina: la data topica viene dagli attributi del titolo, stili di paragrafo compongono saluto, firma e poscritto.

- Versione HTML: https://postext.dev/it/cookbook/letters-edition
- Ricetta Nº 064 · Titoli e aperture · Livello 2 (Intermedio) · Output: Canvas
- Generi: Narrativa, teatro e prosa letteraria
- Richiede postext ≥ 1.25.0 · provata con 1.31.0 il 2026-10-13
- Pagine: [1](https://postext.dev/cookbook/letters-edition/en/p01.webp?v=fe2fbdd1), [2](https://postext.dev/cookbook/letters-edition/en/p02.webp?v=fe2fbdd1), [3](https://postext.dev/cookbook/letters-edition/en/p03.webp?v=fe2fbdd1), [4](https://postext.dev/cookbook/letters-edition/en/p04.webp?v=fe2fbdd1), [5](https://postext.dev/cookbook/letters-edition/en/p05.webp?v=fe2fbdd1)
- Apri nella Sandbox: https://postext.dev/it/sandbox#recipe=letters-edition&lang=en (.postext: https://postext.dev/cookbook/letters-edition/en/letters-edition.postext)
- Ultimo aggiornamento: 2026-10-10
- Altre lingue: [en](https://postext.dev/en/cookbook/letters-edition.md), [es](https://postext.dev/es/cookbook/letters-edition.md), [ca](https://postext.dev/ca/cookbook/letters-edition.md), [de](https://postext.dev/de/cookbook/letters-edition.md), [fr](https://postext.dev/fr/cookbook/letters-edition.md), [pt](https://postext.dev/pt/cookbook/letters-edition.md), [zh](https://postext.dev/zh/cookbook/letters-edition.md), [ja](https://postext.dev/ja/cookbook/letters-edition.md), [ar](https://postext.dev/ar/cookbook/letters-edition.md)

## In breve

Un piccolo libro di tre lettere tra il re di Prussia e Voltaire. Mostra come comporre la data, il saluto e la firma di ogni lettera, con le lettere che si susseguono senza pagine nuove.

## Che cosa comporrai

Cinque pagine di *Mon sort est changé*, un’edizione di 14 × 21 cm di tre lettere in francese: le due che Federico II scrisse a Voltaire sei e dodici giorni dopo essere diventato re di Prussia, nel giugno 1740, e la lettera di Voltaire del 1º aprile 1778, due mesi prima della sua morte. In copertina, una fotografia di due lettere piegate e di un sigillo di ceralacca rossa, generata con modelli di diffusione, poggia su un marocchino verde disegnato in codice dentro un doppio filetto dorato. All’interno, le lettere si susseguono senza interruzioni di pagina. Ogni intestazione dà il numero in maiuscoletto rosso e i due corrispondenti in corsivo Fell, con luogo e data allineati a destra sotto di loro. Il saluto ha una riga propria, e la firma, in maiuscoletto, chiude la lettera al margine destro; il poscritto di Federico segue la sua firma un corpo più piccolo. La testatina di ogni recto dà la data della lettera.

**Questa ricetta risponde a:**

- Come compongo saluto, firma allineata a destra e poscritto di ogni lettera in un epistolario?
- Come ottengo una buona giustificazione e sillabazione per testi in spagnolo, francese o tedesco?
- Come imposto le testatine: titolo del libro sulla pagina sinistra, titolo del capitolo sulla destra, numero di pagina all’esterno?

## La risposta breve

```js
// script.js, righe 43–68
// Each letter is a level-1 heading that carries its place and date as attributes:
//   # Frédéric à Voltaire {place="À Charlottembourg" date="6 juin 1740"}
// (a value holds no { or }, and one with " goes in single quotes: gotcha attr-values)
const letterHead = { enabled: true, slot: { elements: [
  // {number} prints the count as numberingTemplate '{1:I}' writes it.
  { kind: 'text', id: 'number', content: 'Lettre {number}', ...sc, fontSize: pt(9),
    letterSpacing: pt(1.8), color: col('seal'), placement: at('container', 'top-left') },
  { kind: 'text', id: 'title', content: '{titleText}', ...fell, fontSize: pt(15),
    color: col('ink'),
    placement: at('#number', 'below', 0, 1) },
  // The dateline spans the measure under the title and sets its words flush right;
  // inlineMarks reads the attribute's marks, so date="1^er^ avril 1778" raises its ordinal.
  { kind: 'text', id: 'dateline', content: '{attr.place}, le {attr.date}.', ...crimson,
    italic: true, fontSize: pt(10.4), color: col('ink'), align: 'right', inlineMarks: true,
    placement: { ...at('#title', 'below', 0, 1.5), size: { width: 'fill' } } },
] } };
// The salutation, the signature and the postscript, each a :::paragraphs{style="…"} container:
//   :::paragraphs{style="signature"}
//   Fédéric.
//   :::
const letterParts = [
  { id: 'vedette', firstLineIndent: pt(0) }, // 'Sire,' on a line of its own, flush left
  { id: 'signature', ...sc, fontSize: pt(10.5), textAlign: 'right', marginTop: pt(LEAD / 2) },
  { id: 'postscript', fontSize: pt(9), lineHeight: pt(12.6), marginTop: pt(LEAD / 2) },
];
// Hooked up below: letterHead designs the level-1 heading, letterParts joins paragraphStyles.
```

## Ingredienti

**Insegna**

- [Attributi dei titoli](https://postext.dev/it/docs/document-format.md#attributi-dei-titoli): Coppie chiave–valore su un titolo (author, kicker, lead) che aperture, testatine e indice stampano tramite {attr.name}.
- [Ancoraggio degli elementi del progetto](https://postext.dev/it/docs/configuration-page-layout.md#collocazione-degli-elementi): Posiziona gli elementi rispetto al contenitore, alla pagina, all’abbondanza o a un altro elemento (right-of, below, align-*) invece che per coordinate.
- [Stili di paragrafo](https://postext.dev/it/docs/configuration-styles.md#stili-di-paragrafo): Stili con nome per gruppi di paragrafi distinti dal testo corrente: versi, epigrafi, dediche, firme, testo in corpo minore.

**Usa anche**

- [Aperture progettate](https://postext.dev/it/docs/configuration-text.md#estensione-e-progetto-grafico-avanzato)
- [Titoli numerati](https://postext.dev/it/docs/configuration-text.md#sovrascritture-per-livello)
- [Capitoli che si aprono su un recto](https://postext.dev/it/docs/configuration-text.md#interruzione-prima-del-titolo)
- [Stili di titolo](https://postext.dev/it/docs/configuration-styles.md#stili-dei-titoli)
- [Copertine, frontespizi e colophon](https://postext.dev/it/docs/configuration-styles.md#stili-dei-titoli)
- [Immagini nei progetti di pagina](https://postext.dev/it/docs/configuration-page-layout.md#elementi-immagine)
- [A capo nei titoli](https://postext.dev/it/docs/document-format.md#a-capo-nei-titoli)
- [Testatine e folio](https://postext.dev/it/docs/configuration-page-layout.md#intestazioni-e-piè-di-pagina)
- [Versi riga per riga](https://postext.dev/it/docs/document-format.md#verse)
- [Margini speculari](https://postext.dev/it/docs/configuration-page-layout.md#margini-speculari)
- [Colore della carta](https://postext.dev/it/docs/configuration-page-layout.md#pagina)
- [Sillabazione e lingua del documento](https://postext.dev/it/docs/justification.md#lingue-supportate)
- [Apici e pedici](https://postext.dev/it/docs/document-format.md#formattazione-in-linea)
- [Metadati del documento](https://postext.dev/it/docs/document-format.md#frontmatter)
- [Tavolozza di colori semantica](https://postext.dev/it/docs/configuration-fonts-colors-viewers.md#tavolozza-dei-colori)
- [Pagine su un canvas](https://postext.dev/it/docs/configuration-programmatic-usage.md#rendere-una-pagina-come-bitmap)
- [Testatine secondo il ruolo della pagina](https://postext.dev/it/docs/configuration-page-layout.md#elementi-di-testo)
- [Figure e tabelle come risorse](https://postext.dev/it/docs/document-format.md#risorse)
- [Capitoli non numerati](https://postext.dev/it/docs/configuration-styles.md#stili-dei-titoli)

**La configurazione in breve**

- [`bodyText`](https://postext.dev/it/docs/configuration-text.md#testo-corrente), [`colorPalette`](https://postext.dev/it/docs/configuration-fonts-colors-viewers.md#tavolozza-dei-colori), [`footer`](https://postext.dev/it/docs/configuration-page-layout.md#intestazioni-e-piè-di-pagina), [`header`](https://postext.dev/it/docs/configuration-page-layout.md#intestazioni-e-piè-di-pagina), [`headingStyles`](https://postext.dev/it/docs/configuration-styles.md#stili-dei-titoli), [`headings`](https://postext.dev/it/docs/configuration-text.md#titoli), [`layout`](https://postext.dev/it/docs/configuration-page-layout.md#impaginazione), [`locale`](https://postext.dev/it/docs/configuration-text.md#sillabazione), [`page`](https://postext.dev/it/docs/configuration-page-layout.md#pagina), [`paragraphStyles`](https://postext.dev/it/docs/configuration-styles.md#stili-di-paragrafo)

**API**

- [`buildDocumentWithFonts`](https://postext.dev/it/docs/configuration-programmatic-usage.md#caricare-i-font-prima-dellimpaginazione), [`registerResourceImage`](https://postext.dev/it/docs/architecture.md#superficie-dellapi), [`renderPageToCanvas`](https://postext.dev/it/docs/configuration-programmatic-usage.md#rendere-una-pagina-come-bitmap)

**Caratteri**

- Crimson Pro (OFL-1.1), IM Fell French Canon (OFL-1.1), IM Fell DW Pica SC (OFL-1.1)

## Preparazione

### 1 · Leggi la data topica dal titolo

Il codice è nella [risposta breve](https://postext.dev/it/cookbook/letters-edition.md#la-risposta-breve) qui sopra. Ogni lettera è un titolo di livello 1 i cui attributi contengono luogo e data, e l’intestazione li stampa come `{attr.place}, le {attr.date}.` ([attributi del titolo](https://postext.dev/it/docs/document-format.md#attributi-dei-titoli)). Il riquadro della data topica arriva al bordo destro della colonna (`width: 'fill'`), e `align: 'right'` allinea le parole a quel bordo; senza di esso, il testo dei titoli grafici è centrato. Il saluto, la firma e il poscritto sono [stili di paragrafo](https://postext.dev/it/docs/configuration-styles.md#stili-di-paragrafo) applicati con `:::paragraphs{style="…"}`. La firma usa IM Fell DW Pica SC, una famiglia le cui minuscole sono disegnate come maiuscoletto. `inlineMarks: true` fa leggere alla data topica i segni nel suo attributo, così `date="1^er^ avril 1778"` porta in esponente l’ordinale.

### 2 · Fai susseguire le lettere

```js
// script.js, righe 72–78
const letters = { level: 1, numberingTemplate: '{1:I}', advancedDesign: letterHead,
  // No break: left at the level's default, each letter would open on a recto.
  breakBefore: { enabled: false }, marginTop: pt(2 * LEAD),
  // The hidden heading line is measured in the heading face: italic keeps it the IM Fell cut
  // that FONTS loads. Upright, it would need the roman, which FONTS
  // leaves out; the layout would change only for a title long enough to wrap.
  italic: true };
```

Con `breakBefore` disattivato, una lettera comincia due linee di griglia (`marginTop`) sotto la linea di griglia dove finisce la precedente. A [pagina 4](https://postext.dev/cookbook/letters-edition/en/p04.webp?v=fe2fbdd1) il poscritto, composto su un’interlinea di 12,6 pt, finisce tra due linee di griglia, così sopra la lettera III restano circa tre righe di bianco. A pagina 2, la lettera I arriva al piede della pagina e la lettera II apre la pagina 3. Lasciato al suo valore predefinito, il livello 1 aprirebbe ogni lettera su un recto ([interruzione prima](https://postext.dev/it/docs/configuration-text.md#interruzione-prima-del-titolo)). `numberingTemplate: '{1:I}'` numera le lettere da I a III, e l’intestazione stampa il numero romano tramite `{number}`.

### 3 · Data il recto con la lettera

```js
// script.js, righe 82–95
const HEAD_Y = 12; // mm from the top edge
const head = (id, content, parity, placement, look = {}) => ({ kind: 'text', id, content,
  parity, pages: 'body', ...sc, fontSize: pt(8.5), letterSpacing: pt(0.9), color: col('muted'),
  placement, ...look });
const folio = { ...crimson, fontSize: pt(9), letterSpacing: pt(0), color: col('ink') };
const header = { elements: [
  head('verso-folio', '{pageNumber}', 'even', at('page', 'top-left', MARGIN.outer, HEAD_Y), folio),
  head('verso-names', '{author}', 'even', at('page', 'top-left', MARGIN.outer + 8, HEAD_Y)),
  // {attr.date} reads the last letter that starts on or before the page.
  head('recto-date', '{attr.date}', 'odd', at('page', 'top-right', -(MARGIN.outer + 8), HEAD_Y),
    { ...crimson, italic: true, fontSize: pt(9.5), letterSpacing: pt(0), inlineMarks: true }),
  head('recto-folio', '{pageNumber}', 'odd', at('page', 'top-right', -MARGIN.outer, HEAD_Y),
    folio),
] };
```

In un’intestazione di pagina, `{attr.date}` prende l’attributo dell’ultimo titolo di livello 1 che comincia su quella pagina o prima. La pagina 3 stampa «12 juin 1740», e la pagina 5, dove continua la lettera di Voltaire cominciata a pagina 4, stampa «1er avril 1778» con l’ordinale in esponente ([elementi di testo](https://postext.dev/it/docs/configuration-page-layout.md#elementi-di-testo)). Le testatine stanno 12 mm sotto il bordo superiore della pagina, il folio sul margine esterno e le parole 8 mm più all’interno. `pages: 'body'` le tiene fuori dalla copertina, che conta come apertura perché il suo titolo occupa tutta la pagina. La data è composta nel corsivo Crimson Pro della data topica che ripete, e i nomi sul verso nel maiuscoletto dei numeri delle lettere.

### 4 · Fai della copertina un titolo

```js
// script.js, righe 99–121
// The Markdown: # Mon sort \\ est changé {style="cover"}. The binding fills the page, so the
// design reserves all of it and the headnote starts on page 2.
const onCover = (y) => at('page', 'top', 0, y); // centred, y mm below the top edge
const LETTERS = { x: 12, y: 72, w: 116, h: 120 }; // mm: the photograph, inside the fillet
// numbered: false keeps the cover out of the count, so the first letter is I.
const cover = { id: 'cover', numbered: false,
  // span: 'page' although the book has one column. Kept in the column, the design stops at
  // the column's foot (a band of paper under the leather), and page 1 counts as a 'body' page
  // and prints the running heads.
  span: 'page', advancedDesign: { enabled: true, slot: { elements: [
    { kind: 'image', id: 'binding', resourceId: 'binding',
      placement: { ...at('bleed', 'top-left'), size: { width: 'fill', height: 'fill' } } },
    { kind: 'image', id: 'letters', resourceId: 'letters', placement: { ...at('page', 'top-left',
      LETTERS.x, LETTERS.y), size: { width: mm(LETTERS.w), height: mm(LETTERS.h) } } },
    { kind: 'text', id: 'names', content: '{author}', ...sc, fontSize: pt(9.5),
      letterSpacing: pt(2), color: col('gilt'), placement: onCover(18) },
    // \\ in the heading breaks the title here.
    { kind: 'text', id: 'title', content: '{titleText}', ...fell, fontSize: pt(50),
      lineHeight: 1, color: col('paper'), align: 'center', overflow: 'wrap',
      placement: onCover(24) },
    { kind: 'text', id: 'subtitle', content: '{subtitle}', ...crimson, italic: true,
      fontSize: pt(12), color: col('paper'), placement: onCover(62) },
  ] } } };
```

La copertina è il primo titolo, nello [stile di titolo](https://postext.dev/it/docs/configuration-styles.md#stili-dei-titoli) `cover`: un elemento immagine ancorato all’abbondanza porta la legatura disegnata, un secondo colloca la fotografia 72 mm più in basso, e il titolo, i nomi e il sottotitolo vengono dal titolo e dal frontmatter. `numbered: false` la esclude dal conteggio, così la prima lettera di Federico è la I. L’immagine della legatura riempie la pagina, e un’apertura riserva spazio fino al suo elemento più basso, quindi la copertina occupa tutta la pagina 1 e la nota introduttiva comincia a pagina 2 senza `:::pagebreak`.

### 5 · Sillaba il francese e tieni aperti gli spazi

```js
// script.js, righe 125–135
// Justification, hyphenation, whole-paragraph line breaking and the widow, orphan and runt
// rules are defaults; locale 'fr' (in the config) picks the French patterns.
// The letters keep the transcription's unspaced ; : ? and !; a narrow no-break space
// (U+202F) typed before each would give French spacing and never end a line.
const bodyText = { fontFamily: 'Crimson Pro', fontSize: pt(10), lineHeight: pt(LEAD),
  color: col('ink'), firstLineIndent: mm(5),
  // No :ref here: written out with the other colours, for the default-skin check.
  referenceColor: col('ink'),
  // A word space never shrinks below 75 % of the font's. At the default 60 %, the tightest
  // line on page 5 sets its spaces at 0.70 (each VDT line carries its justifiedSpaceRatio).
  minWordSpacing: 0.75 };
```

`locale: 'fr'` sillaba con i pattern francesi (*ou-vrage* a pagina 2, *représen-tation* a pagina 4). La giustificazione, la divisione in righe sull’intero paragrafo e le regole su vedove, orfane e righe finali troppo corte sono attive per impostazione predefinita. `minWordSpacing: 0.75` impedisce a uno spazio tra parole di restringersi sotto i tre quarti di quello di Crimson Pro; con il valore predefinito 0.6, la riga giustificata più stretta di pagina 5 avrebbe gli spazi a 0,70 di quella larghezza. Con un’interlinea di 14,4 pt la pagina contiene 33 righe, e il margine inferiore è ciò che resta sotto di esse, 20,4 mm.

### 6 · Collega ogni colore alla palette

```js
// script.js, righe 14–29
const palette = {
  ink: '#2a2320', // the text: a warm near-black
  paper: '#f6efe2', // the page, and the lettering on the cover
  seal: '#9c2b24', // the letter numbers
  leather: '#2a4536', // the cover: a green morocco binding
  gilt: '#d0b67c', // its tooled border and the names on it
  muted: '#75695d', // the running heads
};
// col(id): a colour linked to its palette entry, in text styles and design elements alike.
const col = (id) => ({ hex: palette[id], model: 'hex', paletteId: id });
const colorPalette = [
  ...Object.entries(palette).map(([id, hex]) => ({ id, name: id, value: { hex, model: 'hex' } })),
  // The engine's default colours, the italic of the headnote among them, link to 'main-color';
  // here it is the ink, not the default blue.
  { id: 'main-color', name: 'ink (defaults)', value: { hex: palette.ink, model: 'hex' } },
];
```

`col()` collega ogni colore alla sua voce di palette, e il collegamento vale allo stesso modo negli stili del testo e negli elementi grafici: le intestazioni delle lettere, le testatine e la copertina. Il corsivo della nota introduttiva e del colophon prende il colore predefinito del corsivo, collegato a `main-color`; con quella voce impostata sull’inchiostro, si stampa in nero e non nel blu del motore, #295AA3.

## La ricetta completa

Un unico file, composto a partire dalla cartella della ricetta con il testo di esempio e il kit comune del Ricettario già inclusi; costruisce la propria pagina. Per eseguirlo, mettilo in uno `<script type="module">` di una pagina vuota, oppure incollalo nel pannello JS di un nuovo pen di CodePen (come modulo). Importa postext da esm.sh: non c’è niente da installare né da compilare.

- Cartella della ricetta: https://github.com/drnachio/postext/tree/main/cookbook/letters-edition

### script.js

```js
// ═══ Postext Cookbook · Nº 064 · Letters edition: datelines and signatures ════════════
// https://postext.dev/en/cookbook/letters-edition
// Code: MIT · Text: Frederick II and Voltaire, 1740 and 1778 (PD) · Cover photo: diffusion models
// Fonts: Crimson Pro, IM Fell French Canon, IM Fell DW Pica SC (SIL OFL) · Needs postext ≥ 1.25.0
import {
  buildDocumentWithFonts, renderPageToCanvas, registerResourceImage,
} from 'https://esm.sh/postext';

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

// ─── 1 · Design ─────────────────────────────────────────────────────────────
// #region palette: iron-gall ink on cream paper, wax red, green morocco and gilt
const palette = {
  ink: '#2a2320', // the text: a warm near-black
  paper: '#f6efe2', // the page, and the lettering on the cover
  seal: '#9c2b24', // the letter numbers
  leather: '#2a4536', // the cover: a green morocco binding
  gilt: '#d0b67c', // its tooled border and the names on it
  muted: '#75695d', // the running heads
};
// col(id): a colour linked to its palette entry, in text styles and design elements alike.
const col = (id) => ({ hex: palette[id], model: 'hex', paletteId: id });
const colorPalette = [
  ...Object.entries(palette).map(([id, hex]) => ({ id, name: id, value: { hex, model: 'hex' } })),
  // The engine's default colours, the italic of the headnote among them, link to 'main-color';
  // here it is the ink, not the default blue.
  { id: 'main-color', name: 'ink (defaults)', value: { hex: palette.ink, model: 'hex' } },
];
// #endregion
const TRIM = { width: 140, height: 210 }; // the French 14 × 21 format
const LEAD = 14.4; // pt: the body's leading, the grid every letter starts on
const LINES = 33; // lines of text on a full page
const TOP = 22; // mm: the top margin
const MARGIN = { top: TOP, inner: 17.5, outer: 14.5, // mm, mirrored
  bottom: TRIM.height - TOP - (LINES * LEAD * 25.4) / 72 }; // ends the page on line 33
const sc = { fontFamily: 'IM Fell DW Pica SC' }; // its lower case is cut as small capitals
const fell = { fontFamily: 'IM Fell French Canon', italic: true }; // the display italic
const crimson = { fontFamily: 'Crimson Pro' }; // the text face
const at = (to, edge, x = 0, y = 0) => ({ anchor: { to, edge }, offset: { x: mm(x), y: mm(y) } });

// #region answer: a letter head read from the heading, and styles for the letter's parts
// Each letter is a level-1 heading that carries its place and date as attributes:
//   # Frédéric à Voltaire {place="À Charlottembourg" date="6 juin 1740"}
// (a value holds no { or }, and one with " goes in single quotes: gotcha attr-values)
const letterHead = { enabled: true, slot: { elements: [
  // {number} prints the count as numberingTemplate '{1:I}' writes it.
  { kind: 'text', id: 'number', content: 'Lettre {number}', ...sc, fontSize: pt(9),
    letterSpacing: pt(1.8), color: col('seal'), placement: at('container', 'top-left') },
  { kind: 'text', id: 'title', content: '{titleText}', ...fell, fontSize: pt(15),
    color: col('ink'),
    placement: at('#number', 'below', 0, 1) },
  // The dateline spans the measure under the title and sets its words flush right;
  // inlineMarks reads the attribute's marks, so date="1^er^ avril 1778" raises its ordinal.
  { kind: 'text', id: 'dateline', content: '{attr.place}, le {attr.date}.', ...crimson,
    italic: true, fontSize: pt(10.4), color: col('ink'), align: 'right', inlineMarks: true,
    placement: { ...at('#title', 'below', 0, 1.5), size: { width: 'fill' } } },
] } };
// The salutation, the signature and the postscript, each a :::paragraphs{style="…"} container:
//   :::paragraphs{style="signature"}
//   Fédéric.
//   :::
const letterParts = [
  { id: 'vedette', firstLineIndent: pt(0) }, // 'Sire,' on a line of its own, flush left
  { id: 'signature', ...sc, fontSize: pt(10.5), textAlign: 'right', marginTop: pt(LEAD / 2) },
  { id: 'postscript', fontSize: pt(9), lineHeight: pt(12.6), marginTop: pt(LEAD / 2) },
];
// Hooked up below: letterHead designs the level-1 heading, letterParts joins paragraphStyles.
// #endregion

// #region letters: numbered I, II, III and run on, two grid lines apart
const letters = { level: 1, numberingTemplate: '{1:I}', advancedDesign: letterHead,
  // No break: left at the level's default, each letter would open on a recto.
  breakBefore: { enabled: false }, marginTop: pt(2 * LEAD),
  // The hidden heading line is measured in the heading face: italic keeps it the IM Fell cut
  // that FONTS loads. Upright, it would need the roman, which FONTS
  // leaves out; the layout would change only for a title long enough to wrap.
  italic: true };
// #endregion

// #region running-heads: the correspondents on the verso, the date of the letter on the recto
const HEAD_Y = 12; // mm from the top edge
const head = (id, content, parity, placement, look = {}) => ({ kind: 'text', id, content,
  parity, pages: 'body', ...sc, fontSize: pt(8.5), letterSpacing: pt(0.9), color: col('muted'),
  placement, ...look });
const folio = { ...crimson, fontSize: pt(9), letterSpacing: pt(0), color: col('ink') };
const header = { elements: [
  head('verso-folio', '{pageNumber}', 'even', at('page', 'top-left', MARGIN.outer, HEAD_Y), folio),
  head('verso-names', '{author}', 'even', at('page', 'top-left', MARGIN.outer + 8, HEAD_Y)),
  // {attr.date} reads the last letter that starts on or before the page.
  head('recto-date', '{attr.date}', 'odd', at('page', 'top-right', -(MARGIN.outer + 8), HEAD_Y),
    { ...crimson, italic: true, fontSize: pt(9.5), letterSpacing: pt(0), inlineMarks: true }),
  head('recto-folio', '{pageNumber}', 'odd', at('page', 'top-right', -MARGIN.outer, HEAD_Y),
    folio),
] };
// #endregion

// #region cover: page 1 is a heading style with the art and the title
// The Markdown: # Mon sort \\ est changé {style="cover"}. The binding fills the page, so the
// design reserves all of it and the headnote starts on page 2.
const onCover = (y) => at('page', 'top', 0, y); // centred, y mm below the top edge
const LETTERS = { x: 12, y: 72, w: 116, h: 120 }; // mm: the photograph, inside the fillet
// numbered: false keeps the cover out of the count, so the first letter is I.
const cover = { id: 'cover', numbered: false,
  // span: 'page' although the book has one column. Kept in the column, the design stops at
  // the column's foot (a band of paper under the leather), and page 1 counts as a 'body' page
  // and prints the running heads.
  span: 'page', advancedDesign: { enabled: true, slot: { elements: [
    { kind: 'image', id: 'binding', resourceId: 'binding',
      placement: { ...at('bleed', 'top-left'), size: { width: 'fill', height: 'fill' } } },
    { kind: 'image', id: 'letters', resourceId: 'letters', placement: { ...at('page', 'top-left',
      LETTERS.x, LETTERS.y), size: { width: mm(LETTERS.w), height: mm(LETTERS.h) } } },
    { kind: 'text', id: 'names', content: '{author}', ...sc, fontSize: pt(9.5),
      letterSpacing: pt(2), color: col('gilt'), placement: onCover(18) },
    // \\ in the heading breaks the title here.
    { kind: 'text', id: 'title', content: '{titleText}', ...fell, fontSize: pt(50),
      lineHeight: 1, color: col('paper'), align: 'center', overflow: 'wrap',
      placement: onCover(24) },
    { kind: 'text', id: 'subtitle', content: '{subtitle}', ...crimson, italic: true,
      fontSize: pt(12), color: col('paper'), placement: onCover(62) },
  ] } } };
// #endregion

// #region text: Crimson Pro at 10/14.4 pt, set in French
// Justification, hyphenation, whole-paragraph line breaking and the widow, orphan and runt
// rules are defaults; locale 'fr' (in the config) picks the French patterns.
// The letters keep the transcription's unspaced ; : ? and !; a narrow no-break space
// (U+202F) typed before each would give French spacing and never end a line.
const bodyText = { fontFamily: 'Crimson Pro', fontSize: pt(10), lineHeight: pt(LEAD),
  color: col('ink'), firstLineIndent: mm(5),
  // No :ref here: written out with the other colours, for the default-skin check.
  referenceColor: col('ink'),
  // A word space never shrinks below 75 % of the font's. At the default 60 %, the tightest
  // line on page 5 sets its spaces at 0.70 (each VDT line carries its justifiedSpaceRatio).
  minWordSpacing: 0.75 };
// #endregion

const config = () => ({
  locale: 'fr', // French hyphenation patterns
  colorPalette,
  page: { sizePreset: 'custom', width: mm(TRIM.width), height: mm(TRIM.height), dpi: 150,
    backgroundColor: col('paper'), margins: { top: mm(MARGIN.top), bottom: mm(MARGIN.bottom),
      left: mm(MARGIN.inner), right: mm(MARGIN.outer), mirror: true } }, // left = inner
  layout: { layoutType: 'single' },
  bodyText,
  // A heading's own line is hidden under its design but still measured, in this face and weight.
  headings: { fontFamily: 'IM Fell French Canon', fontWeight: 400, levels: [letters] },
  headingStyles: [cover],
  paragraphStyles: [...letterParts,
    // Frederick's verses, a :::verse block 7 mm in with a line of space above and below; the
    // octosyllables start two spaces (indentStep=3.5mm on the fence) further in, 14 mm, and the
    // two closing alexandrines at 7 mm.
    { id: 'verse', indent: mm(7), marginTop: pt(LEAD), marginBottom: pt(LEAD) },
    // The editor's headnote at 9.6 on 13 pt, italic through *…* in the Markdown.
    { id: 'headnote', fontSize: pt(9.6), lineHeight: pt(13), firstLineIndent: pt(0) },
    { id: 'colophon', fontSize: pt(7.8), lineHeight: pt(10.8), textAlign: 'left',
      firstLineIndent: pt(0), marginTop: pt(3 * LEAD) }],
  header,
  footer: { elements: [] }, // the folios ride in the header
});

// #region art: the binding drawn in code, and the photograph of the letters laid on it
const W = TRIM.width;
const H = TRIM.height;
// The binding: green leather to the edges, a gilt double fillet and a lozenge at each corner.
function bindingSvg() {
  const tooling = [6, 7.6].map((inset, i) => `<rect x="${inset}" y="${inset}" `
    + `width="${W - 2 * inset}" height="${H - 2 * inset}" fill="none" stroke="${palette.gilt}" `
    + `stroke-width="${i ? 0.25 : 0.7}"/>`).join('') + [[6, 6], [W - 6, 6], [6, H - 6],
    [W - 6, H - 6]].map(([x, y]) => `<path d="M${x} ${y - 2.4} L${x + 2.4} ${y} L${x} ${y + 2.4} `
    + `L${x - 2.4} ${y} Z" fill="${palette.gilt}"/>`).join('');
  return `<svg xmlns="http://www.w3.org/2000/svg" width="${W * 10}" height="${H * 10}" `
    + `viewBox="0 0 ${W} ${H}"><rect width="${W}" height="${H}" fill="${palette.leather}"/>`
    + `${tooling}</svg>`;
}
// The design's image elements name these by id; loadSvg() and loadImage() below register the
// files. The photograph is a JPEG in assets/ cut to LETTERS' 116 × 120 mm, declared at its
// pixels; its edges fade into the leather's green, so it sits on the drawn binding unseen.
const resources = [{ id: 'binding', typeId: 'figure', kind: 'svg', createdAt: 0, updatedAt: 0,
  svg: { fileId: 'binding.svg', width: W * 10, height: H * 10 },
  altText: 'A green leather cover with a gilt double fillet and a lozenge at each corner.' },
{ id: 'letters', typeId: 'figure', kind: 'bitmap', createdAt: 0, updatedAt: 0,
  bitmap: { fileId: 'letters-1160.jpg', format: 'jpeg', width: 1160, height: 1200 },
  altText: 'Two folded letters on the green leather: the upper one lies face down, its flap '
    + 'closed by a red wax seal; the lower one shows an address in brown ink.' }];
// #endregion

// ─── 2 · Content ────────────────────────────────────────────────────────────
const markdown = String.raw`---
title: "Mon sort est changé"
subtitle: "Trois lettres, 1740 et 1778"
author: "Frédéric II et Voltaire"
---

# Mon sort \\ est changé {style="cover"}

:::paragraphs{style="headnote"}
*Frédéric-Guillaume I^er^, roi de Prusse, meurt à Potsdam le 31 mai 1740. Six jours plus tard, son fils, qui règne désormais sous le nom de Frédéric II, écrit de Charlottembourg à Voltaire, son correspondant depuis août 1736. Les deux premières lettres de ce choix sont de ce mois de juin. La troisième, écrite de Paris le 1^er^ avril 1778, ferme le volume de 1889 d’où viennent ces textes. Voltaire meurt à Paris le 30 mai suivant. On garde l’orthographe de l’édition; la vedette et la signature sont détachées du texte.*
:::

# Frédéric à Voltaire {place="À Charlottembourg" date="6 juin 1740"}

:::paragraphs{style="vedette"}
Mon cher ami,
:::

Mon sort est changé, et j’ai assisté aux derniers moments d’un roi, à son agonie, à sa mort. En parvenant à la royauté, je n’avais pas besoin assurément de cette leçon pour être dégoûté de la vanité des grandeurs humaines.

J’avais projeté un petit ouvrage de métaphysique; il s’est changé en un ouvrage de politique. Je croyais joûter avec l’aimable Voltaire, et il me faut escrimer avec Machiavel. Enfin, mon cher Voltaire, nous ne sommes point maîtres de notre sort. Le tourbillon des événements nous entraîne, et il faut se laisser entraîner. Ne voyez en moi, je vous prie, qu’un citoyen zélé, un philosophe un peu sceptique, mais un ami véritablement fidèle. Pour dieu, ne m’écrivez qu’en homme, et méprisez avec moi les titres, les noms, et tout l’éclat extérieur.

Jusqu’à présent il me reste à peine le temps de me reconnaître; j’ai des occupations infinies: je m’en donne encore de surplus; mais malgré tout ce travail, il me reste toujours du temps assez pour admirer vos ouvrages et pour puiser chez vous des instructions et des délassements.

Assurez la marquise de mon estime. Je l’admire autant que ses vastes connaissances et la rare capacité de son esprit le méritent.

Adieu, mon cher Voltaire; si je vis, je vous verrai, et même dès cette année. Aimez-moi toujours, et soyez toujours sincère ami avec votre ami

:::paragraphs{style="signature"}
Fédéric.
:::

# Frédéric à Voltaire {place="À Charlottembourg" date="12 juin 1740"}

:::verse{style="verse" align=start indentStep=3.5mm}
  Non, ce n’est plus du mont Rémus,
  Douce et studieuse retraite
  D’où mes vers vous sont parvenus,
  Que je date ces vers confus:
  Car dans ce moment le poète
  Et le prince sont confondus.
  Désormais mon peuple que j’aime
  Est l’unique Dieu que je sers:
  Adieu les vers et les concerts.
  Tous les plaisirs. Voltaire même;
  Mon devoir est mon Dieu suprême.
  Qu’il entraîne de soins divers!
  Quel fardeau que le diadème!
  Quand ce dieu sera satisfait,
  Alors dans vos bras, cher Voltaire,
  Je volerai, plus prompt qu’un trait,
Puiser, dans les leçons de mon ami sincère,
Quel doit être d’un roi le sacré caractère.
:::

Vous voyez, mon cher ami, que le changement du sort ne m’a pas tout à fait guéri de la métromanie, et que peut-être je n’en guérirai jamais. J’estime trop l’art d’Horace et de Voltaire pour y renoncer; et je suis du sentiment que chaque chose de la vie a son temps.

J’avais commencé une épître sur les abus de la mode et de la coutume, lors même que la coutume de la primogéniture m’obligeait de monter sur le trône et de quitter mon épître pour quelque temps. J’aurais volontiers changé mon épître en satire contre cette même mode, si je ne savais que la satire doit être bannie de la bouche des princes.

Enfin, mon cher Voltaire, je flotte entre vingt occupations, et je ne déplore que la brièveté des jours, qui me paraissent trop courts de vingt-quatre heures.

Je vous avoue que la vie d’un homme qui n’existe que pour réfléchir et pour lui-même, me semble infiniment préférable à la vie d’un homme dont l’unique occupation doit être de faire le bonheur des autres.

Vos vers sont charmants. Je n’en dirai rien, car ils sont trop flatteurs.

Mon cher Voltaire, ne vous refusez pas plus longtemps à l’empressement que j’ai de vous voir. Faites en ma faveur tout ce que vous croyez que votre humanité comporte. J’irai à la fin d’auguste à Vesel, et peut-être plus loin. Promettez-moi de me joindre, car je ne saurais vivre heureux ni mourir tranquille sans vous avoir embrassé. Adieu.

:::paragraphs{style="signature"}
Fédéric.
:::

:::paragraphs{style="postscript"}
Mille compliments à la marquise. Je travaille des deux mains; d’un côté à l’armée, de l’autre au peuple et aux beaux-arts.
:::

# Voltaire à Frédéric {place="À Paris" date="1^er^ avril 1778"}

:::paragraphs{style="vedette"}
Sire,
:::

Le gentilhomme français qui rendra cette lettre à Votre Majesté, et qui passe pour être digne de paraître devant Elle, pourra vous dire que si je n’ai pas eu l’honneur de vous écrire depuis longtemps, c’est que j’ai été occupé à éviter deux choses qui me poursuivaient dans Paris: les sifflets et la mort.

Il est plaisant qu’à quatre-vingt-quatre ans j’aie échappé à deux maladies mortelles. Voilà ce que c’est que de vous être consacré: je me suis renommé de vous, et j’ai été sauvé.

J’ai vu avec surprise et avec une satisfaction bien douce, à la représentation d’une tragédie nouvelle, que le public, qui regardait il y a trente ans Constantin et Théodose comme les modèles des princes, et même des saints, a applaudi avec des transports inouïs à des vers qui disent que Constantin et Théodose n’ont été que des tyrans superstitieux. J’ai vu vingt preuves pareilles du progrès que la philosophie a fait enfin dans toutes les conditions. Je ne désespérerais pas de faire prononcer dans un mois le panégyrique de l’empereur Julien: et assurément si les Parisiens se souviennent qu’il a rendu chez eux la justice comme Caton, et qu’il a combattu pour eux comme César, ils lui doivent une éternelle reconnaissance.

Il est donc vrai, Sire, qu’à la fin les hommes s’éclairent, et que ceux qui se croient payés pour les aveugler ne sont pas toujours les maîtres de leur crever les yeux! Grâces en soient rendus à Votre Majesté! Vous avez vaincu les préjugés comme vos autres ennemis: vous jouissez de vos établissements en tout genre. Vous êtes le vainqueur de la superstition, ainsi que le soutien de la liberté germanique.

Vivez plus longtemps que moi, pour affermir tous les empires que vous avez fondés. Puisse Frédéric le Grand être Frédéric l’immortel!

Daignez agréer le profond respect et l’inviolable attachement de

:::paragraphs{style="signature"}
Voltaire.
:::

:::paragraphs{style="colophon"}
*Correspondance de Voltaire avec le roi de Prusse* (Paris, Librairie de la Bibliothèque nationale, 1889), texte du Project Gutenberg, nº 25734. Composé en Crimson Pro et IM Fell (licence SIL OFL).
:::
`; // content.<lang>.md, inlined by the Cookbook

// ─── 3 · Fonts ──────────────────────────────────────────────────────────────
const FONTS = { // text, display and label faces
  'Crimson Pro': ['400', '400i'], 'IM Fell French Canon': ['400i'], 'IM Fell DW Pica SC': ['400'] };

// ─── 4 · Build & show ───────────────────────────────────────────────────────
await loadSvg('binding.svg', bindingSvg());
await loadImage('letters-1160.jpg', asset('letters-1160.jpg'));
const doc = await buildDocumentWithFonts({ markdown, resources }, config(), kitFonts(FONTS));
showPages(doc, { title: t({ en: 'Letters edition', es: 'Edición de cartas' }) });

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

## Varianti

### Nomina il luogo nella testatina

Gli stessi attributi possono dare al recto l’intera data topica, *À Charlottembourg, le 12 juin 1740*.

```diff
-  head('recto-date', '{attr.date}', 'odd', at('page', 'top-right', -(MARGIN.outer + 8), HEAD_Y),
+  head('recto-date', '{attr.place}, le {attr.date}', 'odd',
+    at('page', 'top-right', -(MARGIN.outer + 8), HEAD_Y),
```

## Errori frequenti

- **Valori degli attributi: niente { o }; virgolette semplici per un valore con ".** Il valore di un attributo finisce alla graffa di chiusura, quindi non può contenere { o }. Un valore che contiene una virgoletta doppia va tra virgolette semplici; il simbolo del dollaro va bene.

- Il recto prende la data dell’ultima lettera che *comincia* su quella pagina o prima. Una pagina che chiude una lettera e apre la successiva porta la data della lettera nuova, come la testatina di un dizionario nomina l’ultima voce della pagina.
- Un titolo senza l’attributo stampa una stringa vuota, senza avvisi: una lettera senza `place="…"` riceve una data topica che comincia con una virgola. Controlla il titolo di ogni lettera.
- Le lettere conservano i ; : ? e ! senza spazio della trascrizione. L’uso francese mette prima di essi uno spazio stretto indivisibile (U+202F); digitato lì, tiene ogni segno sulla riga della sua parola.
- Crimson Pro non ha le lettere modificatrici ᵉ (U+1D49) né ʳ (U+02B3), quindi 1ᵉʳ scritto in Unicode si stamperebbe in un font di riserva. Scrivi invece `1^er^`: la nota introduttiva, che è testo corrente, lo compone in esponente, e così fanno la data topica e la testatina del recto, i cui elementi di testo hanno `inlineMarks: true`.

## Crediti

- Ricetta: Ignacio Ferro ([@drnachio](https://github.com/drnachio))
- Testo: Frederick II’s letters to Voltaire of 6 and 12 June 1740 and Voltaire’s letter to him of 1 April 1778, in Correspondance de Voltaire avec le roi de Prusse (Paris, Librairie de la Bibliothèque nationale, 1889), one misprint corrected (ouvragés), the salutations and signatures set apart: Frédéric II · Voltaire ([fonte](https://www.gutenberg.org/ebooks/25734)), pubblico dominio
- Testo: The headnote and the colophon: Ignacio Ferro, CC-BY-4.0
- Immagini: The cover: two folded letters and a wax seal, a photograph: Generated With Diffusion Models, originale
- Immagini: The cover’s green morocco and gilt fillet, drawn in code: Ignacio Ferro, CC-BY-4.0
- Caratteri: Crimson Pro (OFL-1.1), IM Fell French Canon (OFL-1.1), IM Fell DW Pica SC (OFL-1.1)
- Codice: MIT · Contenuto di esempio: CC-BY-4.0

## Ricette correlate

- [Nº 030 · Un’antologia con le firme degli autori](https://postext.dev/it/cookbook/anthology-with-bylines.md): Tre saggi sul camminare i cui titoli portano autore, anno e rivista: l’apertura li stampa tutti e tre; le testatine del verso e l’indice stampano l’autore. · Livello 2 (Intermedio) · Narrativa, teatro e prosa letteraria
- [Nº 015 · Poesie composte verso per verso](https://postext.dev/it/cookbook/poetry-collection.md): Ogni poesia è un blocco :::verse scritto come stampato: i rientri del 1918 sono spazi iniziali, i versi a capo sporgono di 2 em, righe vuote tra le strofe. · Livello 2 (Intermedio) · Poesia
- [Nº 058 · Lettera commerciale secondo la DIN 5008](https://postext.dev/it/cookbook/din-business-letter.md): Un preventivo tedesco di due pagine su A4. L’oggetto è l’H1, il cui progetto grafico fissa intestazione, indirizzo e blocco informativo dove vuole il modulo B. · Livello 2 (Intermedio) · Fogli singoli ed effimeri
