# Titoli a margine, numeri sporgenti e titoli in linea

> Un bando di concorso con i titoli di sezione in un canale a margine sulle linee di base del testo, numeri sporgenti nell’intercolonna e titoli in linea rossi.

- Versione HTML: https://postext.dev/it/cookbook/side-heads-hanging-numbers
- Ricetta Nº 045 · Titoli e aperture · Livello 3 (Avanzato) · Output: Canvas
- Generi: Rapporti
- Richiede postext ≥ 1.28.0 · provata con 1.31.0 il 2026-10-13
- Pagine: [1](https://postext.dev/cookbook/side-heads-hanging-numbers/en/p01.webp?v=4c3afd27), [2](https://postext.dev/cookbook/side-heads-hanging-numbers/en/p02.webp?v=4c3afd27), [3](https://postext.dev/cookbook/side-heads-hanging-numbers/en/p03.webp?v=4c3afd27), [4](https://postext.dev/cookbook/side-heads-hanging-numbers/en/p04.webp?v=4c3afd27), [5](https://postext.dev/cookbook/side-heads-hanging-numbers/en/p05.webp?v=4c3afd27)
- Apri nella Sandbox: https://postext.dev/it/sandbox#recipe=side-heads-hanging-numbers&lang=en (.postext: https://postext.dev/cookbook/side-heads-hanging-numbers/en/side-heads-hanging-numbers.postext)
- Ultimo aggiornamento: 2026-10-11
- Altre lingue: [en](https://postext.dev/en/cookbook/side-heads-hanging-numbers.md), [es](https://postext.dev/es/cookbook/side-heads-hanging-numbers.md), [ca](https://postext.dev/ca/cookbook/side-heads-hanging-numbers.md), [de](https://postext.dev/de/cookbook/side-heads-hanging-numbers.md), [fr](https://postext.dev/fr/cookbook/side-heads-hanging-numbers.md), [pt](https://postext.dev/pt/cookbook/side-heads-hanging-numbers.md), [zh](https://postext.dev/zh/cookbook/side-heads-hanging-numbers.md), [ja](https://postext.dev/ja/cookbook/side-heads-hanging-numbers.md), [ar](https://postext.dev/ar/cookbook/side-heads-hanging-numbers.md)

## In breve

Il bando di un comune per i progetti che trasformeranno un vecchio mercato del pesce in una biblioteca. I titoli di sezione stanno in un ampio margine sinistro, alla pari con la prima riga del testo accanto.

## Che cosa comporrai

Cinque pagine A4 del bando di un concorso di architettura, in cui il comune di Puerto Lince, un porto spagnolo inventato, chiede progetti per trasformare il suo mercato del pesce del 1931 nella biblioteca civica. L’impaginazione segue i rapporti municipali svizzeri. Un canale di 50 mm scende lungo il lato sinistro di ogni pagina, e ogni sezione si apre su un numero rosso e un filetto nero che attraversa tutta la gabbia. Il titolo della sezione sta nel canale in Noto Serif Display, alla pari con la prima riga del testo. I numeri delle sottosezioni sporgono nell’intercolonna in DM Mono rosso, così ogni titolo di sottosezione mantiene il bordo sinistro del testo. I criteri di valutazione della sezione 4 si aprono su termini in grassetto rosso. La pagina 1 compone un titolo in corpo 58 sopra una planimetria del sito, una pianta del lotto sta nel canale a pagina 2 e un prospetto del fronte del molo apre la pagina 3.

**Questa ricetta risponde a:**

- Come metto i titoli in un canale a margine, faccio sporgere i numeri e metto in linea i titoli minori?
- Come creo un layout a colonna e mezza, con una colonna di testo larga e una colonna laterale stretta?
- Come decido dove va una figura: in testa alla pagina, su entrambe le colonne, esattamente qui o nel margine?
- Come coloro i termini chiave (in grassetto o in corsivo) nel testo o dentro i riquadri?

## La risposta breve

```js
// script.js, righe 56–81
// The heading stays in the flow and keeps its number, but its design has no {titleText}:
// it prints the number in the channel and a rule from there to the column's right edge,
// both reserve: false, or the box below would keep a float gap under them (a line lower).
const [NUMBER, RULE] = [8.5, 0.75]; // pt: the DM Mono number, whose figures are 0.7 em tall
// Centre the rule on the figures: their baseline is 0.8 of the line down, less 0.35 em.
const RULE_Y = pt(NUMBER * (0.8 - 0.7 / 2) - RULE / 2);
const h2 = { level: 2, numberingTemplate: '{2}', lineHeight: pt(LEAD), // one grid line
  marginTop: pt(2 * LEAD), marginBottom: pt(0), advancedDesign: { enabled: true, slot: {
    elements: [
      { kind: 'text', id: 'number', content: '{number}', fontFamily: LABEL, fontWeight: 500,
        fontSize: pt(NUMBER), lineHeight: 1, color: col('signal'), reserve: false,
        placement: at('container', 'top-left', mm(-HANG)) },
      { kind: 'rule', id: 'rule', direction: 'horizontal', thickness: pt(RULE), reserve: false,
        color: col('ink'), placement: { ...at('#number', 'right-of', mm(2), RULE_Y),
          size: { width: 'fill' } } },
    ] } } };
// The title goes in a box fenced right after the heading: span="side" stands it in the
// channel on the grid line where the text resumes (gotcha: side-box-starts-at-fence).
// Attributes at the end of the line stay with the heading.
const sideHeads = (md) => md.replace(/^## (.+?)(\s*\{[^}]*\})?$/gm,
  (line, title) => `${line}\n\n:::callout{type="sidehead" span="side"}\n${title}\n:::`);
// The box has no background and no padding, and it takes the text's leading, so each
// line of the title sits on a baseline of the text beside it.
const sidehead = { id: 'sidehead', backgroundEnabled: false,
  padding: { top: pt(0), right: pt(0), bottom: pt(0), left: pt(0) },
  body: { fontFamily: DISPLAY, fontSize: pt(13.5), lineHeight: pt(LEAD) } };
```

## Ingredienti

**Insegna**

- [Note a margine](https://postext.dev/it/docs/configuration-styles.md#stili-dei-riquadri): Riquadri composti nella colonna laterale all’altezza del paragrafo che commentano, in un’impaginazione a colonna e mezza con un canale per gli elementi flottanti.
- [Aperture progettate](https://postext.dev/it/docs/configuration-text.md#estensione-e-progetto-grafico-avanzato): Un titolo disegnato come una composizione libera di testo, filetti, riquadri e immagini, che riserva sopra il testo corrente l’altezza di cui ha bisogno.
- [Colonna e mezza](https://postext.dev/it/docs/configuration-page-layout.md#tipi-di-impaginazione): Un’impaginazione asimmetrica: una colonna principale larga e una colonna laterale stretta, su un lato fisso o sul bordo esterno di ogni pagina.

**Usa anche**

- [Colonna a margine per gli elementi flottanti](https://postext.dev/it/docs/configuration-page-layout.md#impaginazione)
- [Titoli numerati](https://postext.dev/it/docs/configuration-text.md#sovrascritture-per-livello)
- [Attributi dei titoli](https://postext.dev/it/docs/document-format.md#attributi-dei-titoli)
- [Testo, filetti e riquadri nei progetti di pagina](https://postext.dev/it/docs/configuration-page-layout.md#intestazioni-e-piè-di-pagina)
- [Ancoraggio degli elementi del progetto](https://postext.dev/it/docs/configuration-page-layout.md#collocazione-degli-elementi)
- [Immagini nei progetti di pagina](https://postext.dev/it/docs/configuration-page-layout.md#elementi-immagine)
- [Citazioni che collocano le figure](https://postext.dev/it/docs/document-format.md#rimando-in-linea-la-forma-principale)
- [Collocazione delle figure](https://postext.dev/it/docs/document-format.md#collocazione)
- [Riquadri](https://postext.dev/it/docs/configuration-styles.md#stili-dei-riquadri)
- [Stili di paragrafo](https://postext.dev/it/docs/configuration-styles.md#stili-di-paragrafo)
- [Grassetto, corsivo e i loro colori](https://postext.dev/it/docs/configuration-text.md#testo-corrente)
- [Testatine e folio](https://postext.dev/it/docs/configuration-page-layout.md#intestazioni-e-piè-di-pagina)
- [Stile delle tabelle](https://postext.dev/it/docs/configuration-resources.md#stile-delle-tabelle)
- [Stile delle didascalie](https://postext.dev/it/docs/configuration-resources.md#stile-delle-didascalie)
- [Figura e Tabella nella tua lingua](https://postext.dev/it/docs/configuration-resources.md#tipi-di-risorsa)
- [Didascalie laterali](https://postext.dev/it/docs/document-format.md#collocazione)
- [Figure proprio qui](https://postext.dev/it/docs/document-format.md#inserimento-di-blocco-facoltativo-collocazione-in-linea-esplicita)
- [Bilanciamento delle colonne](https://postext.dev/it/docs/configuration-text.md#bilanciamento-delle-colonne)
- [Fascia di capitolo a tutta larghezza](https://postext.dev/it/docs/configuration-text.md#estensione-e-progetto-grafico-avanzato)
- [Colore della carta](https://postext.dev/it/docs/configuration-page-layout.md#pagina)
- [Tipi di risorsa personalizzati](https://postext.dev/it/docs/configuration-resources.md#tipi-di-risorsa)
- [Figure e tabelle come risorse](https://postext.dev/it/docs/document-format.md#risorse)

**La configurazione in breve**

- [`bodyText`](https://postext.dev/it/docs/configuration-text.md#testo-corrente), [`calloutStyles`](https://postext.dev/it/docs/configuration-styles.md#stili-dei-riquadri), [`captionStyle`](https://postext.dev/it/docs/configuration-resources.md#stile-delle-didascalie), [`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), [`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), [`resourceTypes`](https://postext.dev/it/docs/configuration-resources.md#tipi-di-risorsa), [`tableStyle`](https://postext.dev/it/docs/configuration-resources.md#stile-delle-tabelle)

**API**

- [`buildDocumentWithFonts`](https://postext.dev/it/docs/configuration-programmatic-usage.md#caricare-i-font-prima-dellimpaginazione), [`defaultResourceTypes`](https://postext.dev/it/docs/configuration-resources.md#tipi-di-risorsa), [`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**

- Mona Sans (OFL-1.1), Noto Serif Display (OFL-1.1), DM Mono (OFL-1.1)

## Preparazione

### 1 · Stampa il titolo della sezione da un riquadro nel canale

Il codice è nella [risposta breve](https://postext.dev/it/cookbook/side-heads-hanging-numbers.md#la-risposta-breve) qui sopra. Un titolo riserva spazio fino all’elemento più basso del suo progetto grafico, quindi un titolo disegnato nel canale alla pari con la prima riga della sezione spingerebbe il testo una riga più in basso, e due per un titolo di due righe. Il progetto grafico del titolo disegna invece solo il numero e il filetto, e `sideHeads()` copia il testo di ogni `## Title` in un `:::callout` delimitato subito dopo il titolo. Con `span="side"` quel riquadro esce dal flusso e sta nel canale sulla linea di griglia dove riprende il testo ([stili dei riquadri](https://postext.dev/it/docs/configuration-styles.md#stili-dei-riquadri)). Il numero e il filetto impostano `reserve: false`: altrimenti un riquadro nel canale mantiene uno spazio da elemento flottante, una riga, sotto gli elementi del titolo che stanno lì, e il titolo scenderebbe alla seconda riga del testo. Riquadri e testo corrente mettono entrambi la linea di base di una riga a 0,8 dell’interlinea, quindi un riquadro sull’interlinea di 14 pt del testo mette ogni riga di un titolo di due righe su una linea di base del paragrafo accanto. Il Markdown resta un semplice `## Schedule of accommodation`. Poiché il progetto grafico del titolo non stampa `{titleText}`, la Sandbox elenca l’avviso *Testo del titolo non usato*, anche se il riquadro stampa il titolo.

![Page 2: 2 The site. Figure 1. The plot, 1:2000. Market hall and weighbridge hut in grey; six tamarisks along the north edge. Red arrows: possible entrances; grey arrow: service.](https://postext.dev/cookbook/side-heads-hanging-numbers/en/p02.webp?v=4c3afd27)

*Pagina 2: il numero e il filetto vengono dal titolo, il titolo dal riquadro nel canale, alla pari con la prima riga del testo. La pianta del lotto si impila sotto.*

### 2 · Riserva il canale a titoli e figure

```js
// script.js, righe 46–52
const layout = {
  layoutType: 'oneAndHalf',
  sideColumnPercent: (SIDE / CONTENT) * 100, // 50 of the 178 mm between the margins
  sideColumnSide: 'left', // on every page: the brief is printed on one side of the sheet
  sideColumnRole: 'floats', // no text in the channel: what span: 'side' sends, side captions
  gutterWidth: mm(GUTTER),
};
```

`sideColumnRole: 'floats'` tiene il testo fuori dal canale ([tipi di impaginazione](https://postext.dev/it/docs/configuration-page-layout.md#tipi-di-impaginazione)). Dopo l’apertura, il canale contiene solo i sei titoli di sezione e la pianta del lotto, mandati entrambi lì con `span: 'side'`, e le didascalie delle tabelle 1 e 2. La colonna laterale è `'right'` per impostazione predefinita, e un bando stampato su un solo lato del foglio la tiene sullo stesso lato in ogni pagina. `sideColumnSide: 'left'` la mette a sinistra, dove ogni titolo di sezione si legge prima del suo testo.

### 3 · Fai sporgere i numeri e metti in linea i titoli più piccoli

```js
// script.js, righe 85–102
// The title keeps the text's left edge; the number, right-aligned in a 12 mm box, ends
// 2 mm short of it. The box needs that fixed width: an 'auto' width is clamped to the
// column and would shrink to nothing out here.
const H3 = { size: 10.5, number: 9 }; // pt; two boxes one grid line tall share a baseline
const h3 = { level: 3, numberingTemplate: '{2}.{3}', fontSize: pt(H3.size),
  lineHeight: pt(LEAD), marginTop: pt(LEAD), marginBottom: pt(0),
  advancedDesign: { enabled: true, slot: { elements: [
    { kind: 'text', id: 'title', content: '{titleText}', fontFamily: TEXT, fontWeight: 600,
      fontSize: pt(H3.size), lineHeight: LEAD / H3.size, color: col('ink'), align: 'left',
      overflow: 'wrap', placement: { ...at('container', 'top-left'), size: { width: 'fill' } } },
    { kind: 'text', id: 'number', content: '{number}', fontFamily: LABEL, fontWeight: 500,
      fontSize: pt(H3.number), lineHeight: LEAD / H3.number, color: col('signal'),
      align: 'right', placement: { ...at('#title', 'left-of', mm(-2)),
        size: { width: mm(12) } } },
  ] } } };
// Level 4 is not a heading: '**Accessibility.** A wheelchair user…' opens its paragraph.
// No other paragraph has bold; table cells keep tableStyle's ink, so Total stays black.
const runIn = { boldColor: col('signal') }; // spread into bodyText
```

Il titolo della sottosezione comincia sul bordo sinistro della colonna. Il suo numero è ancorato `'left-of'` al titolo, allineato a destra in un riquadro di 12 mm che finisce 2 mm prima del testo, così numeri come 2.1 e 3.3 sporgono nell’intercolonna e ogni titolo si allinea con il testo sotto ([collocazione degli elementi](https://postext.dev/it/docs/configuration-page-layout.md#collocazione-degli-elementi)). Il riquadro ha bisogno di una larghezza fissa. Una larghezza `'auto'` viene limitata alla colonna. Fuori, nell’intercolonna, questo non lascia alcuna larghezza, e il numero non viene disegnato. Il livello 2 stampa `'{2}'` e il livello 3 `'{2}.{3}'`, perché con `{1}` ogni numero comincerebbe con l’1 dell’unico H1 del bando. I criteri della sezione 4 sono un quarto livello composto senza titolo. Ogni paragrafo si apre su un termine in grassetto, e `bodyText.boldColor` lo stampa in rosso ([testo corrente](https://postext.dev/it/docs/configuration-text.md#testo-corrente)). Nessun altro paragrafo ha grassetto, e le celle delle tabelle prendono il colore da `tableStyle`, quindi la riga del totale in grassetto della tabella 1 resta nera.

### 4 · Dai a ogni figura e tabella la sua collocazione

```js
// script.js, righe 333–367
// The plot plan stands in the channel; the elevation crosses channel and column at the
// head of the next page; the table of areas floats to the head of the text column, with
// its caption beside it in the channel; the timetable sits where ::resource puts it, and
// its caption takes the channel too (captionSide serves an inline embed from 1.28).
const PLAN_W = 44; // mm: the plot plan, 88 m wide at 1:2000
const resource = (id, typeId, rest) => ({ id, typeId, createdAt: 0, updatedAt: 0, ...rest });
const drawing = (id, width, height, rest) => resource(id, 'figure', { kind: 'svg',
  svg: { fileId: `${id}.svg`, width, height }, ...rest });
const resources = [
  drawing('parcela', PLAN_W * 10, 400, {
    placement: { span: 'side', width: PLAN_W / SIDE }, // 44 mm of the channel's 50
    caption: t({ en: 'The plot, 1:2000. Market hall and weighbridge hut in grey; six tamarisks '
      + 'along the north edge. Red arrows: possible entrances; grey arrow: service.',
    es: 'La parcela, 1:2000. Lonja y caseta de básculas, en gris; seis tamarindos en el '
      + 'borde norte. Flechas rojas: accesos posibles; flecha gris: servicio.' }),
    altText: t({ en: 'Plan of the plot with the hall, the hut, six trees and three entrances.',
      es: 'Planta de la parcela con la lonja, la caseta, seis árboles y tres accesos.' }) }),
  drawing('fachada', 1780, 300, { placement: { position: 'top', span: 'page' },
    caption: t({ en: 'Quay front, 1:300. In red, the four frames at the east end, where the '
      + 'reinforcement has corroded.',
    es: 'Fachada al muelle, 1:300. En rojo, los cuatro pórticos del extremo este, con '
      + 'las armaduras corroídas.' }),
    altText: t({ en: 'Elevation of the hall: thirteen vaults, eleven arches, four frames in red.',
      es: 'Alzado de la lonja: trece bóvedas, once arcos y cuatro pórticos en rojo.' }) }),
  resource('programa', 'table', { kind: 'table', table: programme,
    placement: { position: 'top', captionSide: true }, caption: t({
      en: 'Schedule of net floor areas.', es: 'Programa de superficies útiles por zonas.' }) }),
  resource('calendario', 'table', { kind: 'table', table: calendar,
    placement: { position: 'here', captionSide: true },
    caption: t({ en: 'Competition timetable.', es: 'Calendario del concurso.' }) }),
  // No :ref cites the site plan: only the opener's image element draws it.
  drawing('situacion', MAP.w * 10, MAP.h * 10, { altText: t({
    en: 'Site plan of the Poniente quay with the market hall’s plot in red.',
    es: 'Plano de situación del muelle de Poniente con la parcela de la lonja en rojo.' }) }),
];
```

La pianta del lotto va nel canale con `span: 'side'` e si impila sotto il titolo a margine della sezione 2 ([collocazione](https://postext.dev/it/docs/document-format.md#collocazione)). `width: PLAN_W / SIDE` la rende larga 44 mm, il che mette i suoi 88 metri esattamente in scala 1:2000. Il prospetto della facciata è un elemento flottante `'top'` a tutta larghezza. È citato a pagina 2 e apre la pagina 3, perché un elemento flottante non si colloca mai sopra il suo rimando. La tabella delle superfici, anch’essa citata a pagina 2, flotta in testa alla colonna di testo di pagina 3, sotto il prospetto. `captionSide: true` mette la sua didascalia nel canale, alla pari con la testa della tabella. Il calendario sta dove lo mette `::resource`, alla fine del bando, e `captionSide: true` mette anche la sua didascalia nel canale, accanto alla sezione 6.3.

### 5 · Apri il bando sulla planimetria del sito

```js
// script.js, righe 106–131
const MAP = { y: 68.5, w: CONTENT, h: 52 }; // mm: from the top of the text block
const display = { fontFamily: DISPLAY, fontWeight: 300, color: col('ink'), align: 'left',
  overflow: 'wrap' };
const opener = { enabled: true,
  // The plan's foot falls between two grid lines. A floor on the next one leaves the rule
  // of section 1 as far from its text as the rule of every later section.
  minHeight: mm(GRID * Math.ceil((MAP.y + MAP.h) / GRID)),
  slot: { elements: [
    { kind: 'text', id: 'kicker', content: '{attr.kicker}', ...label, color: col('signal'),
      placement: at('container', 'top-left', mm(HANG)) },
    { kind: 'text', id: 'series', content: '{subtitle}', ...label,
      placement: { ...at('container', 'top-left'), size: { width: mm(SIDE) } } },
    { kind: 'text', id: 'title', content: '{titleText}', ...display, fontSize: pt(58),
      lineHeight: 0.96, placement: { ...at('#kicker', 'below', mm(0), mm(4.5)),
        size: { width: mm(MAIN) } } },
    { kind: 'text', id: 'lead', content: '{attr.lead}', ...display, italic: true,
      fontSize: pt(14), lineHeight: 1.3, placement: { ...at('#title', 'below', mm(0), mm(5)),
        size: { width: mm(MAIN) } } },
    { kind: 'text', id: 'legend', content: '{attr.map}', fontFamily: TEXT, fontSize: pt(7.5),
      lineHeight: 1.4, color: col('muted'), align: 'left', overflow: 'wrap',
      placement: { ...at('#lead', 'align-top', mm(-HANG), pt(2)),
        size: { width: mm(SIDE - 6) } } },
    { kind: 'image', id: 'map', resourceId: 'situacion', // across the channel and the column
      placement: { ...at('container', 'top-left', mm(0), mm(MAP.y)),
        size: { width: mm(MAP.w) } } },
  ] } };
```

Con `span: 'page'` il contenitore dell’apertura è l’intera gabbia, canale compreso ([estensione e progetto grafico avanzato](https://postext.dev/it/docs/configuration-text.md#estensione-e-progetto-grafico-avanzato)). L’occhiello e il titolo sono collocati a 57 mm, sul bordo sinistro della colonna di testo, e la planimetria attraversa canale e colonna. Senza di esso il contenitore è la colonna di testo, ogni elemento si sposta di 57 mm a destra, e il titolo, il sommario e la planimetria escono dal bordo destro della pagina. Il progetto grafico riserva spazio fino al piede della planimetria, che cade tra due linee di griglia. Un `minHeight` arrotondato per eccesso alla linea di griglia successiva lascia il filetto della sezione 1 alla stessa distanza dal suo testo del filetto di ogni sezione successiva. L’occhiello, il sommario e la legenda della carta sono attributi della riga `# Competition brief`.

## 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/side-heads-hanging-numbers

### script.js

```js
// ═══ Postext Cookbook · Nº 045 · Side heads, hanging numbers and run-in heads ═══════
// https://postext.dev/en/cookbook/side-heads-hanging-numbers
// Code: MIT · Text: original (CC BY 4.0) · Drawings: made in code (MIT)
// Fonts: Mona Sans, Noto Serif Display, DM Mono (SIL OFL 1.1) · Needs postext ≥ 1.28.0
import {
  buildDocumentWithFonts, renderPageToCanvas, registerResourceImage, defaultResourceTypes,
} from 'https://esm.sh/postext';

const LANG = 'en'; // @lang: the language of the sample document ('en' | 'es')
const RECIPE = 'side-heads-hanging-numbers';

// ─── 1 · Design ─────────────────────────────────────────────────────────────
const palette = { // a Swiss municipal report: black, white and one signal red
  ink: '#17171a', // text and rules
  signal: '#d42a1f', // section numbers, run-in heads, the plot on the plans (5.1:1)
  tint: '#f9dcd7', // the plot's ground on the plans
  sea: '#d8e2e8', // water on the plans
  stone: '#cacad0', // built ground on the plans
  rule: '#c3c3ca', // hairlines under the running head and in tables
  muted: '#5e5e67', // running heads, legends, the colophon (6.4:1)
  paper: '#ffffff',
};
// Every colour is linked to its palette entry by id.
const col = (id) => ({ hex: palette[id], model: 'hex', paletteId: id });
// Defaults this config does not restate link to 'main-color', so it points at the accent.
const colorPalette = Object.entries({ ...palette, 'main-color': palette.signal })
  .map(([id, hex]) => ({ id, name: id, value: { hex, model: 'hex' } }));
const [TEXT, DISPLAY, LABEL] = ['Mona Sans', 'Noto Serif Display', 'DM Mono'];
const TRIM = { width: 210, height: 297 }; // mm: A4, like every document of the competition
const [TOP, LEFT, RIGHT] = [26, 16, 16]; // mm; not mirrored: the brief prints one-sided
const LEAD = 14; // pt: the body leading, the pitch of the baseline grid
const LINES = 50; // grid lines in the text block
const PT = 25.4 / 72; // mm in a point
const GRID = LEAD * PT; // mm: one line of the grid
const CONTENT = TRIM.width - LEFT - RIGHT; // 178 mm
const [SIDE, GUTTER] = [50, 7]; // mm: the margin channel on the left, and the gap after it
const MAIN = CONTENT - SIDE - GUTTER; // 121 mm: the text column
const HANG = SIDE + GUTTER; // mm from the channel's left edge to the text's left edge
const at = (to, edge, x = mm(0), y = mm(0)) => ({ anchor: { to, edge }, offset: { x, y } });
// Kickers, running heads and folios: tracked capitals in the mono.
const LABEL_PT = 7.5;
const label = { fontFamily: LABEL, fontWeight: 500, fontSize: pt(LABEL_PT), lineHeight: 1.35,
  letterSpacing: pt(1.2), textTransform: 'uppercase', color: col('muted'), align: 'left' };

// #region channel: one text column, and on its left a channel for boxes and figures only
const layout = {
  layoutType: 'oneAndHalf',
  sideColumnPercent: (SIDE / CONTENT) * 100, // 50 of the 178 mm between the margins
  sideColumnSide: 'left', // on every page: the brief is printed on one side of the sheet
  sideColumnRole: 'floats', // no text in the channel: what span: 'side' sends, side captions
  gutterWidth: mm(GUTTER),
};
// #endregion

// #region answer: side heads: the heading draws a number and a rule, a box prints the title
// The heading stays in the flow and keeps its number, but its design has no {titleText}:
// it prints the number in the channel and a rule from there to the column's right edge,
// both reserve: false, or the box below would keep a float gap under them (a line lower).
const [NUMBER, RULE] = [8.5, 0.75]; // pt: the DM Mono number, whose figures are 0.7 em tall
// Centre the rule on the figures: their baseline is 0.8 of the line down, less 0.35 em.
const RULE_Y = pt(NUMBER * (0.8 - 0.7 / 2) - RULE / 2);
const h2 = { level: 2, numberingTemplate: '{2}', lineHeight: pt(LEAD), // one grid line
  marginTop: pt(2 * LEAD), marginBottom: pt(0), advancedDesign: { enabled: true, slot: {
    elements: [
      { kind: 'text', id: 'number', content: '{number}', fontFamily: LABEL, fontWeight: 500,
        fontSize: pt(NUMBER), lineHeight: 1, color: col('signal'), reserve: false,
        placement: at('container', 'top-left', mm(-HANG)) },
      { kind: 'rule', id: 'rule', direction: 'horizontal', thickness: pt(RULE), reserve: false,
        color: col('ink'), placement: { ...at('#number', 'right-of', mm(2), RULE_Y),
          size: { width: 'fill' } } },
    ] } } };
// The title goes in a box fenced right after the heading: span="side" stands it in the
// channel on the grid line where the text resumes (gotcha: side-box-starts-at-fence).
// Attributes at the end of the line stay with the heading.
const sideHeads = (md) => md.replace(/^## (.+?)(\s*\{[^}]*\})?$/gm,
  (line, title) => `${line}\n\n:::callout{type="sidehead" span="side"}\n${title}\n:::`);
// The box has no background and no padding, and it takes the text's leading, so each
// line of the title sits on a baseline of the text beside it.
const sidehead = { id: 'sidehead', backgroundEnabled: false,
  padding: { top: pt(0), right: pt(0), bottom: pt(0), left: pt(0) },
  body: { fontFamily: DISPLAY, fontSize: pt(13.5), lineHeight: pt(LEAD) } };
// #endregion

// #region lower: level 3 hangs its number in the gutter; level 4 runs into its paragraph
// The title keeps the text's left edge; the number, right-aligned in a 12 mm box, ends
// 2 mm short of it. The box needs that fixed width: an 'auto' width is clamped to the
// column and would shrink to nothing out here.
const H3 = { size: 10.5, number: 9 }; // pt; two boxes one grid line tall share a baseline
const h3 = { level: 3, numberingTemplate: '{2}.{3}', fontSize: pt(H3.size),
  lineHeight: pt(LEAD), marginTop: pt(LEAD), marginBottom: pt(0),
  advancedDesign: { enabled: true, slot: { elements: [
    { kind: 'text', id: 'title', content: '{titleText}', fontFamily: TEXT, fontWeight: 600,
      fontSize: pt(H3.size), lineHeight: LEAD / H3.size, color: col('ink'), align: 'left',
      overflow: 'wrap', placement: { ...at('container', 'top-left'), size: { width: 'fill' } } },
    { kind: 'text', id: 'number', content: '{number}', fontFamily: LABEL, fontWeight: 500,
      fontSize: pt(H3.number), lineHeight: LEAD / H3.number, color: col('signal'),
      align: 'right', placement: { ...at('#title', 'left-of', mm(-2)),
        size: { width: mm(12) } } },
  ] } } };
// Level 4 is not a heading: '**Accessibility.** A wheelchair user…' opens its paragraph.
// No other paragraph has bold; table cells keep tableStyle's ink, so Total stays black.
const runIn = { boldColor: col('signal') }; // spread into bodyText
// #endregion

// #region opener: the brief's first page: kicker, title, lead and the site plan
const MAP = { y: 68.5, w: CONTENT, h: 52 }; // mm: from the top of the text block
const display = { fontFamily: DISPLAY, fontWeight: 300, color: col('ink'), align: 'left',
  overflow: 'wrap' };
const opener = { enabled: true,
  // The plan's foot falls between two grid lines. A floor on the next one leaves the rule
  // of section 1 as far from its text as the rule of every later section.
  minHeight: mm(GRID * Math.ceil((MAP.y + MAP.h) / GRID)),
  slot: { elements: [
    { kind: 'text', id: 'kicker', content: '{attr.kicker}', ...label, color: col('signal'),
      placement: at('container', 'top-left', mm(HANG)) },
    { kind: 'text', id: 'series', content: '{subtitle}', ...label,
      placement: { ...at('container', 'top-left'), size: { width: mm(SIDE) } } },
    { kind: 'text', id: 'title', content: '{titleText}', ...display, fontSize: pt(58),
      lineHeight: 0.96, placement: { ...at('#kicker', 'below', mm(0), mm(4.5)),
        size: { width: mm(MAIN) } } },
    { kind: 'text', id: 'lead', content: '{attr.lead}', ...display, italic: true,
      fontSize: pt(14), lineHeight: 1.3, placement: { ...at('#title', 'below', mm(0), mm(5)),
        size: { width: mm(MAIN) } } },
    { kind: 'text', id: 'legend', content: '{attr.map}', fontFamily: TEXT, fontSize: pt(7.5),
      lineHeight: 1.4, color: col('muted'), align: 'left', overflow: 'wrap',
      placement: { ...at('#lead', 'align-top', mm(-HANG), pt(2)),
        size: { width: mm(SIDE - 6) } } },
    { kind: 'image', id: 'map', resourceId: 'situacion', // across the channel and the column
      placement: { ...at('container', 'top-left', mm(0), mm(MAP.y)),
        size: { width: mm(MAP.w) } } },
  ] } };
// #endregion

// #region furniture: letterhead and folio, the same on every page of a one-sided brief
const [HEAD_Y, FOOT_Y] = [13, 11]; // mm from the top and from the foot of the sheet
const FILE_NO = 'BML-2026/04'; // the council's file number, also in the opener's kicker
const header = { elements: [
  // The council's name in two lines, the second on the running head's baseline.
  { kind: 'text', id: 'city', content: '{author}', ...label, color: col('ink'),
    overflow: 'wrap', placement: { ...at('page', 'top-left', mm(LEFT),
      mm(HEAD_Y - LABEL_PT * label.lineHeight * PT)), size: { width: mm(40) } } },
  { kind: 'text', id: 'book', content: '{title}', ...label,
    placement: at('page', 'top-left', mm(LEFT + HANG), mm(HEAD_Y)) },
  { kind: 'text', id: 'folio', content: t({ en: 'Page {pageNumber} of {totalPages}',
    es: 'Página {pageNumber} de {totalPages}' }), ...label,
  placement: at('page', 'top-right', mm(-RIGHT), mm(HEAD_Y)) },
  { kind: 'rule', id: 'hairline', direction: 'horizontal', thickness: pt(0.5), color: col('rule'),
    placement: { ...at('page', 'top-left', mm(LEFT), mm(HEAD_Y + 5)),
      size: { width: mm(CONTENT) } } },
] };
const footer = { elements: [
  { kind: 'text', id: 'file', content: `{chapterTitle} · ${FILE_NO}`, ...label,
    placement: at('page', 'bottom-left', mm(LEFT + HANG), mm(-FOOT_Y)) },
] };
// #endregion

const config = () => ({
  locale: t({ en: 'en-gb', es: 'es' }), // each edition is hyphenated in its own language
  // Figure / Table, Figura / Tabla, counted through the whole brief: 1, 2…
  resourceTypes: defaultResourceTypes(LANG).map((type) => ({ ...type, numberingTemplate: '{n}' })),
  colorPalette,
  page: { sizePreset: 'custom', width: mm(TRIM.width), height: mm(TRIM.height), dpi: 150,
    backgroundColor: col('paper'),
    margins: { top: mm(TOP), bottom: mm(TRIM.height - TOP - LINES * GRID), left: mm(LEFT),
      right: mm(RIGHT), mirror: false } },
  layout,
  bodyText: { fontFamily: TEXT, fontSize: pt(10), lineHeight: pt(LEAD), color: col('ink'),
    ...runIn, boldFontWeight: 600, referenceColor: col('ink'), referenceBold: false,
    italicColor: col('ink'), // an italic would otherwise take main-color, the red
    // Report texture: ragged right, no indent, a blank line between paragraphs.
    textAlign: 'left', firstLineIndent: pt(0), paragraphSpacing: true },
  // The hidden text of levels 2 and 3 asks for this weight, and 600 is loaded already.
  headings: { fontFamily: TEXT, fontWeight: 600,
    // Balancing would add lines above heads to fill short pages: three blank lines over
    // some side heads instead of two. Off, the white above every head is the same.
    balancing: { enabled: false }, levels: [
    // parity 'any': a brief opens the next page, whichever side it falls on.
    // span: 'page' gives the opener the whole text block, channel included, as container.
    { level: 1, span: 'page', breakBefore: { enabled: true, parity: 'any' },
      advancedDesign: opener },
    h2, h3,
  ] },
  paragraphStyles: [{ id: 'colophon', fontSize: pt(7.5), lineHeight: pt(10.5),
    color: col('muted') }],
  calloutStyles: [sidehead],
  tableStyle: { rules: 'horizontal', borderColor: col('rule'), borderWidth: pt(0.5),
    headerBackground: col('ink'), headerColor: col('paper'), headerFontSize: pt(8),
    bodyFontSize: pt(8.6), cellPadding: mm(1.4) },
  captionStyle: { fontSize: pt(8), labelColor: col('signal'), gap: mm(2) },
  header, footer,
});

// ─── 2 · Content ────────────────────────────────────────────────────────────
const markdown = String.raw`---
title: "New Municipal Library"
subtitle: "Design competition"
author: "Puerto Lince Town Council"
---

# Competition brief {kicker="File BML-2026/04" lead="An open, anonymous competition in two stages to turn the fish market of 1931 into the town’s library." map="Site plan at 1:3000, with the fish market’s plot in red and the built town in grey."}

## The competition

Puerto Lince Town Council is holding a design competition, judged by a jury, to select the architectural proposal for the new Municipal Library, which will occupy the former fish market on the Poniente quay. The present library has operated since 1987 on the ground floor of the Casa de Cultura: 610 m² for a town of 24,300 inhabitants, with no room to grow.

The competition is governed by this brief and by Spain’s Law 9/2017 on Public Sector Contracts. It is open to architects qualified to practise in a Member State of the European Union, entering alone or as the lead of a team.

### Scope of the commission

The winning team will prepare the outline and detailed designs, the health and safety study and the activity licence application, and will supervise the works. The fee, set at €412,000 plus VAT, also covers the design of the fixed furniture and the signage.

### Budget and timescale

The maximum construction budget, which includes the landscaping of the plot, is €4,850,000 excluding VAT. Entries that exceed it by more than 10% will be disqualified. The works will take about twenty months, and the library should open in 2030.

## The site

The fish market stands on the town’s front to the old harbour, between the fishermen’s quarter and the extension of 1905. It is the only building in the old harbour from before 1936 that is still standing, and the Council bought it from the Port Authority in 2022.

### The plot

The plot is a rectangle of 70 by 45 metres, 3,150 m² (:ref{id="parcela" case="lower"}). It is bounded to the south by the Paseo del Muelle, to the east by Calle de la Aduana and to the north by the Plaza de las Redes, which the local plan intends to pedestrianise in 2028. To the west stands the weighbridge hut, owned by the Council, which may be demolished or brought into the scheme.

Outside the hall, 1,300 m² may be built on, in no more than two storeys and 9.5 m to the cornice. No new building may project beyond the line of the quay front.

### The 1931 fish market

The fish market is a reinforced-concrete hall of 52 by 24 metres, roofed by thirteen barrel vaults spanning four metres that rest on portal frames. It was built by the Harbour Works Board, and fish was auctioned in it until 2009, when the auction moved to the new port.

The municipal heritage list protects it at grade 2. The structure, the vaults and the quay front with its eleven round arches must be kept (:ref{id="fachada" case="lower"}). The structural survey of 2025 found corroded reinforcement in the four frames at the east end; their repair is included in the budget, and each entry will explain how it proposes to carry it out without dismantling any of the vaults.

### Access and trees

The main entrance may be from the square or from the promenade, but deliveries will be made from Calle de la Aduana. The six tamarisks along the north edge, some forty years old, are to be kept.

## Schedule of accommodation

The areas in the table are net internal areas (:ref{id="programa" style="full" case="lower"}). Each zone may vary by 10% either way, provided the total does not change. The hall will house the reading and meeting rooms; anything that needs enclosure, services or noise control may go in the new building.

### Public areas

The main reading room will hold the adult lending collection, some 38,000 items, with ninety reader seats. Periodicals and the local history collection will share an adjoining room, with twelve study places and a self-service scanner. The children’s library will have its own entrance from outside and a storytelling corner for twenty-five children. The young people’s room and the multi-purpose hall must be able to open outside library hours, with the rest of the building closed.

### Staff areas

Cataloguing, the offices and the closed stack add up to 260 m². These areas must connect with the loading bay without crossing the public rooms. The stack will have mobile shelving; its floor slab will be designed for an imposed load of 12 kN/m² and will be at street level.

### Outdoor spaces

Between the hall and the new building, an open-air reading courtyard of at least 180 m² is required, sheltered from the north wind, which in winter gusts here at over 60 km/h.

## Design criteria

The jury will assess the entries against the following criteria, listed in descending order of weight.

**The market hall.** Work on the hall must be recognisable as new and reversible. Floors that cut across the vaults will not be accepted; mezzanines that stand clear and leave the frames in full view will.

**Accessibility.** A wheelchair user must reach every part of the building unaided, courtyard and mezzanines included. No ramp may exceed 6%.

**Energy and comfort.** Entries will say how the hall is ventilated and shaded from the sun, since its vaults cannot be insulated on the inside. Credit will be given for heating and cooling controlled room by room.

**Maintenance.** The fish market is 18 metres from the water, and salt spray reaches it all year. New materials will be chosen for those conditions, and the report will include a plan for cleaning roofs and gutters.

**Cost.** The jury will check that the cost estimate keeps within the maximum set in section 1.2.

## Submission of entries

The competition is held in two stages. All documents are to be delivered on paper to the Council’s general registry, and as PDF files to the procurement platform, before 14:00 on the date set.

### Stage one

Each team will submit two A1 boards in portrait format, mounted on lightweight rigid board, showing the general idea, the ground-floor plan at 1:200 and a section through the hall. They will be accompanied by a report of no more than four A4 pages; the jury will not read any pages beyond that limit. Models are not accepted at this stage.

### Stage two

The jury will shortlist five entries, whose authors will develop their idea on three A1 boards, with a ten-page report and a cost estimate broken down by element. Each shortlisted team will receive €6,000 on delivery of the complete documents.

### Anonymity

The boards and the report will carry only a motto of up to five words, repeated on the sealed envelope that holds the team’s details. Any entry bearing names, logos or references by which its authors could be identified will be excluded. The envelopes will be opened at a public session, once the jury has signed its decision.

## Jury and timetable

The jury will be constituted before the deadline for questions, and its membership will be published on the Council’s buyer profile. Its deliberations will be confidential, and the record of the decision will give written reasons for each prize and each commendation.

### Members of the jury

The jury will have seven voting members: the councillor for Culture, who will chair it; the municipal architect; the director of the library; three architects, two of them nominated by the professional association; and a librarian from the regional library service. A procurement officer will act as secretary, with the right to speak but not to vote.

### Prizes

The first prize carries the award of the contract for the design and the supervision of the works. The second prize will receive €12,000 and the third, €8,000. There are also two commendations of €4,000 each, which the jury may decide not to award.

### Timetable

The dates in the table will not change (:ref{id="calendario" style="full" case="lower"}). Questions are to be sent in writing, and the answers will be published without naming whoever asked.

::resource{id="calendario"}

:::paragraphs{style="colophon"}
Sample document: Puerto Lince, its fish market and this competition are fictional. Set in Mona Sans, Noto Serif Display and DM Mono (SIL Open Font License). Original text, CC BY 4.0.
:::
`; // content.<lang>.md, inlined by the Cookbook

// The two tables: every column after the first is right-aligned, its header too.
const table = (columnWidths, rows) => ({ model: { headerRowCount: 1, columnWidths,
  rows: rows.map((cells, r) => cells.map((content, i) => ({ content, isHeader: r === 0,
    align: i > 0 ? 'right' : 'left' }))) } });
const AREAS = [120, 560, 240, 140, 150, 160, 100, 140, 120, 270]; // m²: 2,000 in all
const programme = table([3, 1], [[t({ en: 'Area', es: 'Zona' }), 'm²'], ...t({
  en: ['Reception and lending desk', 'Main reading room', 'Children’s library',
    'Young people’s room', 'Periodicals and local history', 'Multi-purpose hall',
    'Training rooms (2)', 'Cataloguing and offices', 'Closed stack', 'Toilets, stores and plant'],
  es: ['Acogida y préstamo', 'Sala general', 'Sala infantil', 'Espacio joven',
    'Hemeroteca y fondo local', 'Sala polivalente', 'Aulas de formación (2)',
    'Proceso técnico y dirección', 'Depósito cerrado', 'Aseos, almacenes e instalaciones'],
}).map((zone, i) => [zone, `${AREAS[i]}`]),
['**Total**', t({ en: '**2,000**', es: '**2.000**' })]]);
const calendar = table([3, 2], t({
  en: [['Milestone', 'Date'], ['Notice published', '15 October 2026'],
    ['Questions close', '13 November 2026'], ['Stage one submission', '11 January 2027, 14:00'],
    ['Five teams shortlisted', '5 February 2027'], ['Stage two submission', '22 April 2027, 14:00'],
    ['Jury’s decision', '20 May 2027']],
  es: [['Hito', 'Fecha'], ['Publicación del anuncio', '15 de octubre de 2026'],
    ['Fin del plazo de consultas', '13 de noviembre de 2026'],
    ['Entrega de la primera fase', '11 de enero de 2027, 14.00 h'],
    ['Selección de cinco equipos', '5 de febrero de 2027'],
    ['Entrega de la segunda fase', '22 de abril de 2027, 14.00 h'],
    ['Fallo del jurado', '20 de mayo de 2027']],
}));

// #region resources: four placements: the channel, across the page, a column top, here
// The plot plan stands in the channel; the elevation crosses channel and column at the
// head of the next page; the table of areas floats to the head of the text column, with
// its caption beside it in the channel; the timetable sits where ::resource puts it, and
// its caption takes the channel too (captionSide serves an inline embed from 1.28).
const PLAN_W = 44; // mm: the plot plan, 88 m wide at 1:2000
const resource = (id, typeId, rest) => ({ id, typeId, createdAt: 0, updatedAt: 0, ...rest });
const drawing = (id, width, height, rest) => resource(id, 'figure', { kind: 'svg',
  svg: { fileId: `${id}.svg`, width, height }, ...rest });
const resources = [
  drawing('parcela', PLAN_W * 10, 400, {
    placement: { span: 'side', width: PLAN_W / SIDE }, // 44 mm of the channel's 50
    caption: t({ en: 'The plot, 1:2000. Market hall and weighbridge hut in grey; six tamarisks '
      + 'along the north edge. Red arrows: possible entrances; grey arrow: service.',
    es: 'La parcela, 1:2000. Lonja y caseta de básculas, en gris; seis tamarindos en el '
      + 'borde norte. Flechas rojas: accesos posibles; flecha gris: servicio.' }),
    altText: t({ en: 'Plan of the plot with the hall, the hut, six trees and three entrances.',
      es: 'Planta de la parcela con la lonja, la caseta, seis árboles y tres accesos.' }) }),
  drawing('fachada', 1780, 300, { placement: { position: 'top', span: 'page' },
    caption: t({ en: 'Quay front, 1:300. In red, the four frames at the east end, where the '
      + 'reinforcement has corroded.',
    es: 'Fachada al muelle, 1:300. En rojo, los cuatro pórticos del extremo este, con '
      + 'las armaduras corroídas.' }),
    altText: t({ en: 'Elevation of the hall: thirteen vaults, eleven arches, four frames in red.',
      es: 'Alzado de la lonja: trece bóvedas, once arcos y cuatro pórticos en rojo.' }) }),
  resource('programa', 'table', { kind: 'table', table: programme,
    placement: { position: 'top', captionSide: true }, caption: t({
      en: 'Schedule of net floor areas.', es: 'Programa de superficies útiles por zonas.' }) }),
  resource('calendario', 'table', { kind: 'table', table: calendar,
    placement: { position: 'here', captionSide: true },
    caption: t({ en: 'Competition timetable.', es: 'Calendario del concurso.' }) }),
  // No :ref cites the site plan: only the opener's image element draws it.
  drawing('situacion', MAP.w * 10, MAP.h * 10, { altText: t({
    en: 'Site plan of the Poniente quay with the market hall’s plot in red.',
    es: 'Plano de situación del muelle de Poniente con la parcela de la lonja en rojo.' }) }),
];
// #endregion

// #region art: the three drawings, made in code with a seeded PRNG
const rng = (seed) => () => { // Mulberry32: the same town on every run
  seed = (seed + 0x6d2b79f5) | 0;
  let r = Math.imul(seed ^ (seed >>> 15), 1 | seed);
  r = (r + Math.imul(r ^ (r >>> 7), 61 | r)) ^ r;
  return ((r ^ (r >>> 14)) >>> 0) / 4294967296;
};
const n = (v) => (+v).toFixed(2);
const svg = (w, h, body, view = `0 0 ${w} ${h}`) => '<svg xmlns="http://www.w3.org/2000/svg" '
  + `width="${w * 10}" height="${h * 10}" viewBox="${view}">${body}</svg>`;
const rect = (x, y, w, h, fill, extra = '') =>
  `<rect x="${n(x)}" y="${n(y)}" width="${n(w)}" height="${n(h)}" fill="${fill}" ${extra}/>`;
const circle = (x, y, r, fill, extra = '') =>
  `<circle cx="${n(x)}" cy="${n(y)}" r="${r}" fill="${fill}" ${extra}/>`;
const path = (d, extra) => `<path d="${d}" ${extra}/>`;
const stroke = (color, width) => `fill="none" stroke="${color}" stroke-width="${width}"`;
const through = (points) => `M${points.map(([x, y]) => `${n(x)} ${n(y)}`).join(' L')}`;
const arrow = (x, y, angle, color) => path('M0 -4 L0 3 M-1.6 1.2 L0 3.6 L1.6 1.2 Z',
  `transform="translate(${x} ${y}) rotate(${angle}) scale(1.3)" fill="${color}" `
  + `stroke="${color}" stroke-width="0.7"`);
const north = (x, y, s) => path(`M${x} ${y} L${x + 0.3 * s} ${y + s} L${x} ${y + 0.8 * s} `
  + `L${x - 0.3 * s} ${y + s} Z`, `fill="${palette.ink}"`);

// Site plan, 1:3000 (1 mm = 3 m): the old quarter, the 1905 ensanche, the plaza, the plot.
function situacionSvg() {
  const { w, h } = MAP;
  const P = palette;
  const rand = rng(45);
  const m = (metres) => metres / 3; // mm on the plan
  const QUAY = h - 10; // the water's edge
  const PLOT = { x: 94, y: QUAY - m(18) - m(45), w: m(70), h: m(45) }; // behind the promenade
  const EAST = PLOT.x + PLOT.w; // the calle de la Aduana starts here
  // The sea: a beach on the west, then the quay of the old harbour and the fishing pier.
  let body = path(`M0 ${h - 5} C14 ${h - 5.5} 30 ${QUAY + 1} 42 ${QUAY} L${w} ${QUAY} V${h} H0 Z`,
    `fill="${P.sea}"`);
  body += rect(150, QUAY - 1, 7, h - QUAY + 1, P.stone);
  body += path(`M${w} ${h - 3.5} L161 ${h - 2.5}`, stroke(P.stone, 2.2));
  for (let i = 0; i < 7; i++) { // boats moored along the quay
    body += rect(90 + i * 8 + rand() * 3, QUAY + 1.2, 1.3, 3 + rand(), P.rule, 'rx="0.6"');
  }
  // The old quarter: one built mass cut by lanes that wander, and the old road to the port.
  const lane = (points, width) => path(through(points),
    `${stroke(P.paper, width)} stroke-linejoin="round" stroke-linecap="round"`);
  body += rect(-1, -1, EAST + 1, QUAY - m(18) + 1, P.stone);
  for (let x = 2; x < EAST; x += 7 + rand() * 5) { // lanes down to the sea
    const points = [];
    for (let y = -2; y <= QUAY - 5; y += 6) points.push([x + (rand() - 0.5) * 3.2, y]);
    body += lane(points, rand() < 0.25 ? 1.9 : 1);
  }
  for (let y = 4; y < QUAY - 10; y += 6 + rand() * 3.5) { // lanes along the coast
    const points = [];
    for (let x = -2; x <= EAST + 2; x += 8) points.push([x, y + (rand() - 0.5) * 2.4]);
    body += lane(points, rand() < 0.2 ? 1.7 : 0.9);
  }
  body += lane([[6, -2], [34, 12], [58, QUAY - 7]], 2.6);
  body += rect(PLOT.x - 4, 8, PLOT.w + 4, QUAY - m(18) - 8, P.paper); // the plaza de las Redes
  // The ensanche: chamfered blocks built round a courtyard.
  const [c, bw, bh] = [2, 13, 9];
  for (let x = EAST + 5; x < w; x += bw + 2.8) {
    for (let y = -4; y < QUAY - 16; y += bh + 2.6) {
      body += path(`${through([[x + c, y], [x + bw - c, y], [x + bw, y + c], [x + bw, y + bh - c],
        [x + bw - c, y + bh], [x + c, y + bh], [x, y + bh - c], [x, y + c]])} Z`,
      `fill="${P.stone}"`) + rect(x + 3.2, y + 3, bw - 6.4, bh - 6, P.paper, 'fill-opacity="0.55"');
    }
  }
  for (let x = 6; x < w - 4; x += 8.6) body += circle(x, QUAY - m(9), 0.9, P.rule); // promenade
  for (let i = 0; i < 10; i++) {
    body += circle(PLOT.x + (i % 5) * 5, 11 + Math.floor(i / 5) * 4.2, 0.9, P.rule);
  }
  // The plot, the lonja along its south edge and the weighbridge hut: as in figure 1.
  body += rect(PLOT.x, PLOT.y, PLOT.w, PLOT.h, P.tint, `stroke="${P.signal}" stroke-width="0.5"`);
  body += rect(PLOT.x + m(9), PLOT.y + m(21), m(52), m(24), P.signal)
    + rect(PLOT.x + m(1.5), PLOT.y + m(29), m(5.5), m(8), P.stone);
  body += north(86, h - 7, 5.5);
  body += rect(44, h - 3.4, m(100), 0.8, P.ink) + rect(44, h - 3.4, m(50), 0.8, P.paper,
    `stroke="${P.ink}" stroke-width="0.2"`); // 100 m
  return svg(w, h, body);
}

// The plot, 1:2000 (1 mm = 2 m), drawn in metres: 88 m across the figure's 44 mm.
function parcelaSvg() {
  const P = palette;
  let body = rect(-9, 63, 88, 3, P.sea); // the harbour, past the 18 m promenade
  body += rect(0, 0, 70, 45, P.tint, `stroke="${P.signal}" stroke-width="0.9"`);
  body += rect(9, 21, 52, 24, P.stone); // the lonja
  for (let x = 13; x < 61; x += 4) body += path(`M${x} 21 V45`, stroke(P.paper, 0.35));
  body += rect(1.5, 29, 5.5, 8, P.stone); // the weighbridge hut
  for (let i = 0; i < 6; i++) { // the six tamarinds
    body += circle(9 + i * 10.4, 6.5, 3.1, 'none', `stroke="${P.muted}" stroke-width="0.5"`)
      + circle(9 + i * 10.4, 6.5, 0.6, P.muted);
  }
  body += arrow(35, -7, 0, P.signal) + arrow(35, 52.5, 180, P.signal) // the entrances
    + arrow(75.5, 14, 90, P.muted); // service
  body += north(-6, -12, 6);
  return svg(PLAN_W, 40, body, '-9 -14 88 80');
}

// The quay front, 1:300 (1 m = 3.33 mm), drawn in metres: 13 vaults on 14 porticos.
function fachadaSvg() {
  const P = palette;
  const [VIEW_W, VIEW_H, SKY] = [CONTENT * 0.3, 9, 7.4]; // m; SKY: ground to the top edge
  const X = (x) => n(x + (VIEW_W - 52) / 2);
  const Y = (y) => n(SKY - y); // y up from the ground
  const CORNICE = 6.2;
  const R = (4 + 0.9 * 0.9) / (2 * 0.9); // radius of a 4 m vault that rises 0.9 m
  let roof = `M${X(0)} ${Y(0)} V${Y(CORNICE)}`;
  for (let i = 1; i <= 13; i++) roof += ` A${R} ${R} 0 0 1 ${X(4 * i)} ${Y(CORNICE)}`;
  let body = path(`${roof} V${Y(0)} Z`, `fill="${P.paper}" stroke="${P.ink}" stroke-width="0.08"`);
  body += path(`M${X(0)} ${Y(CORNICE - 0.35)} H${X(52)} M${X(0)} ${Y(0.5)} H${X(52)}`,
    stroke(P.ink, 0.04));
  for (let i = 0; i < 13; i++) { // eleven arches; a square door in each end bay
    const cx = 4 * i + 2;
    body += i === 0 || i === 12 ? rect(+X(cx - 1.1), +Y(3.6), 2.2, 3.6, P.stone)
      : path(`M${X(cx - 1.2)} ${Y(0.5)} V${Y(3.4)} A1.2 1.2 0 0 1 ${X(cx + 1.2)} ${Y(3.4)} `
        + `V${Y(0.5)} Z`, `fill="${P.stone}"`);
  }
  for (let i = 0; i <= 13; i++) { // the porticos' pilasters; the four eastern ones in red
    const x = Math.min(Math.max(4 * i - 0.25, 0), 51.5);
    body += rect(+X(x), +Y(CORNICE - 0.35), 0.5, CORNICE - 0.35, i >= 10 ? P.signal : P.paper,
      `stroke="${P.ink}" stroke-width="0.04"`);
  }
  body += path(`M0 ${Y(0)} H${VIEW_W}`, stroke(P.ink, 0.12)); // the quay
  body += rect(+X(0), +Y(-0.9), 5, 0.22, P.ink) // 10 m
    + rect(+X(5), +Y(-0.9), 5, 0.22, P.paper, `stroke="${P.ink}" stroke-width="0.04"`);
  return svg(CONTENT, VIEW_H / 0.3, body, `0 0 ${VIEW_W} ${VIEW_H}`);
}
// #endregion

// ─── 3 · Fonts ──────────────────────────────────────────────────────────────
const FONTS = {
  'Mona Sans': ['400', '600'],
  'Noto Serif Display': ['300', '300i', '400'],
  'DM Mono': ['500'],
};

// ─── 4 · Build & show ───────────────────────────────────────────────────────
const source = sideHeads(markdown);
await loadSvg('situacion.svg', situacionSvg());
await loadSvg('parcela.svg', parcelaSvg());
await loadSvg('fachada.svg', fachadaSvg());
const doc = await buildDocumentWithFonts({ markdown: source, resources }, config(),
  kitFonts(FONTS));
showPages(doc, { title: t({ en: 'Competition brief', es: 'Bases del concurso' }) });

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

### Metti il filetto solo sulla colonna di testo

Ancora il filetto al riquadro del titolo stesso invece che al numero, e va dal bordo sinistro del testo al bordo destro della colonna, con solo il numero e il titolo nel canale.

```diff
-        color: col('ink'), placement: { ...at('#number', 'right-of', mm(2), RULE_Y),
+        color: col('ink'), placement: { ...at('container', 'top-left', mm(0), RULE_Y),
```

## Errori frequenti

- **Un riquadro laterale parte all’altezza del blocco che segue il suo delimitatore.** In postext 1.4.1 un riquadro span: 'side' si colloca nella colonna laterale all’altezza a cui è arrivato il testo nel punto del suo delimitatore, sulla riga successiva della griglia e sotto qualunque riquadro già presente. Metti il delimitatore di una glossa subito prima del paragrafo che spiega: messa dopo, la glossa comincia accanto al paragrafo seguente. Un riquadro che sforerebbe il piede della colonna sale finché il suo piede non coincide con il piede della colonna, per quanto lo permette il riquadro sopra di esso; uno che ancora non ci sta aspetta la colonna laterale della pagina successiva.
- **Un elemento flottante 'top' va alla pagina dopo la sua citazione, a meno che non possa aprire la pagina che lo cita.** Per impostazione predefinita un elemento flottante non va mai sopra il proprio riferimento, quindi un flottante 'top' a tutta larghezza citato a pagina N apre la pagina N+1. Imposta placement.citingPage: true sulla risorsa o sul defaultPlacement del suo tipo (o layout.floatsAtCitingPage per tutto il libro, da postext 1.25) e aprirà invece la pagina N, con il testo sopra il riferimento che scende sotto di esso, quando entra entro layout.maxTopFraction e la riga che lo cita resta su quella pagina. Non apre mai la prima pagina di un capitolo né una pagina dopo un’interruzione forzata: lì apre ancora la pagina successiva, e 'auto' o 'bottom' possono prendere il piede della pagina che lo cita.
- **Avviso di impaginazione: Testo del titolo non stampato** (`headingAdvancedWithoutTitleText`). Una grafica avanzata non ha alcun elemento che stampi {titleText}, quindi le parole del titolo non compaiono mai. Soluzione: Aggiungi un elemento di testo con {titleText}, a meno che il titolo non debba essere invisibile. ([Documentazione](https://postext.dev/it/docs/configuration-text.md#estensione-e-progetto-grafico-avanzato))

- Una figura laterale prende la testa del canale nella pagina che la cita, e un titolo a margine delimitato dopo di essa si impila sotto la figura, al di sotto dell’inizio della propria sezione. Cita le figure laterali dopo il titolo di sezione della loro pagina, come la sezione 2 cita la pianta del lotto.
- Un inserimento in linea (`position: 'here'`) accetta `captionSide` da postext 1.28: la didascalia del calendario sta nel canale a pagina 5, come quella della tabella delle superfici a pagina 3. La didascalia ha bisogno del canale libero accanto alla tabella. Dove c’è già un titolo a margine o una figura laterale, resta sotto la tabella.

## Crediti

- Ricetta: Ignacio Ferro ([@drnachio](https://github.com/drnachio))
- Caratteri: Mona Sans (OFL-1.1), Noto Serif Display (OFL-1.1), DM Mono (OFL-1.1)
- Codice: MIT · Contenuto di esempio: CC-BY-4.0

## Ricette correlate

- [Nº 018 · Titoli di sezione su sette livelli](https://postext.dev/it/cookbook/section-heads-field-manual.md): Sezioni numerate da 1.1 a 1.12 in una pillola ambra che si allarga con il numero, altri quattro livelli sotto e un settimo creato con uno stile di titolo. · Livello 2 (Intermedio) · Manuali, guide e opere di consultazione
- [Nº 032 · Un classico annotato con glosse a margine](https://postext.dev/it/cookbook/annotated-classic-glosses.md): Il tè dei matti di Alice in edizione annotata: glosse verdi e rosse nel margine esterno, accanto alle righe che spiegano, richiamate da una lettera colorata. · Livello 3 (Avanzato) · Narrativa, teatro e prosa letteraria
- [Nº 001 · Libro di testo con una colonna a margine](https://postext.dev/it/cookbook/textbook-margin-column.md): Una pagina a colonna e mezza con la colonna esterna solo per i flottanti: vi si impilano figure e glosse span 'side', e captionSide vi sposta le didascalie. · Livello 3 (Avanzato) · Libri di testo
