# La colonna che la regola delle vedove lasciava corta

> Colonne a bandiera che finiscono sulla griglia: fillShortColumns allunga o accorcia di una riga un paragrafo della colonna corta.

- Versione HTML: https://postext.dev/it/cookbook/filling-short-columns
- Ricetta Nº 177 · Pagina e griglia · Livello 2 (Intermedio) · Output: Canvas
- Generi: Riviste e fanzine
- Richiede postext ≥ 1.65.0 · provata con 1.65.0 il 2026-10-31
- Pagine: [1](https://postext.dev/cookbook/filling-short-columns/en/p01.webp?v=76529999), [1](https://postext.dev/cookbook/filling-short-columns/en/p04.webp?v=76529999), [2](https://postext.dev/cookbook/filling-short-columns/en/p05.webp?v=76529999), [3](https://postext.dev/cookbook/filling-short-columns/en/p06.webp?v=76529999)
- Apri nella Sandbox: https://postext.dev/it/sandbox#recipe=filling-short-columns&lang=en (.postext: https://postext.dev/cookbook/filling-short-columns/en/filling-short-columns.postext)
- Ultimo aggiornamento: 2026-10-31
- Altre lingue: [en](https://postext.dev/en/cookbook/filling-short-columns.md), [es](https://postext.dev/es/cookbook/filling-short-columns.md), [ca](https://postext.dev/ca/cookbook/filling-short-columns.md), [de](https://postext.dev/de/cookbook/filling-short-columns.md), [fr](https://postext.dev/fr/cookbook/filling-short-columns.md), [pt](https://postext.dev/pt/cookbook/filling-short-columns.md), [zh](https://postext.dev/zh/cookbook/filling-short-columns.md), [ja](https://postext.dev/ja/cookbook/filling-short-columns.md), [ar](https://postext.dev/ar/cookbook/filling-short-columns.md)

## In breve

Quando una colonna si ferma una riga sopra il piede perché un paragrafo non si può spezzare lì, questa impostazione ricompone un paragrafo della colonna perché arrivi fino in fondo.

## Che cosa comporrai

Tre pagine di *Littoral*, una rivista di costa: un lungo articolo sulla lente di Fresnel, aperto dal dipinto di una lente accesa nella sua lanterna. Il testo corre su due colonne a bandiera in Public Sans, con una riga di griglia vuota tra i paragrafi, e le regole su vedove e orfane tengono almeno due righe di un paragrafo da ciascun lato di un'interruzione. Il pen compone l'articolo due volte. Senza l'impostazione, nell'[apertura](https://postext.dev/cookbook/filling-short-columns/en/p01.webp?v=76529999) la colonna sinistra si ferma una riga sopra la destra: il paragrafo seguente non può cominciare con una sola riga al piede. Con l'impostazione, nella [stessa pagina](https://postext.dev/cookbook/filling-short-columns/en/p04.webp?v=76529999), il primo paragrafo va a capo su una riga in più e le due colonne finiscono sull'ultima riga della griglia. Nella pagina seguente l'impostazione riprende anche l'aria in più che il bilanciamento aveva messo sopra due titoletti.

**Questa ricetta risponde a:**

- Perché una colonna a bandiera finisce una riga prima del piede, e come la riempio senza vedove né orfane?
- Come ottengo colonne pareggiate al piede e un’ultima pagina bilanciata (giustificazione verticale)?
- Come evito vedove, orfane e ultime righe di una sola parola (righe finali troppo corte), e tengo un titolo unito al suo testo?

## La risposta breve

```js
// script.js, righe 37–55
const balancing = {
  // A ragged column has no lever of its own when the widow and orphan rules hold its last
  // line back: no heading or list end to open, no justified paragraph to set looser. This
  // one lets a paragraph of the short column run a line long, within its own word spacing
  // (or a justified one no looser than maxWordSpacing), or, when none can, set one a line
  // short so the split after it takes the line. A change is kept only when the pages end
  // with fewer short columns; otherwise the column stays as it was.
  fillShortColumns: true, // default false
  // The paragraphs go first: a crosshead keeps its one line of air unless they cannot help.
  maxLinesBeforeLoose: 0,
};
const bodyText = {
  fontFamily: 'Public Sans', fontSize: pt(9.4), lineHeight: pt(LEAD), color: col('ink'),
  boldColor: col('ink'), italicColor: col('ink'), referenceColor: col('ink'),
  textAlign: 'left', firstLineIndent: pt(0), paragraphSpacing: true, // a grid line between
  hyphenation: { enabled: true }, optimalLineBreaking: true, optimalRagged: true,
  avoidWidows: true, widowMinLines: 2, avoidOrphans: true, orphanMinLines: 2, // the cause
  avoidRunts: true,
};
```

## Ingredienti

**Insegna**

- [Colonne corte riempite](https://postext.dev/it/docs/configuration-page-flow.md#bilanciamento-delle-colonne): Quando le regole su vedove e orfane fanno finire una colonna una o due righe sopra il piede, il bilanciamento distribuisce il vuoto tra i titoli, gli elenchi e i paragrafi della colonna perché finisca a filo (headings.balancing.fillShortColumns).

**Usa anche**

- [Bilanciamento delle colonne](https://postext.dev/it/docs/configuration-page-flow.md#bilanciamento-delle-colonne)
- [Vedove, orfane e righe corte](https://postext.dev/it/docs/configuration-page-flow.md#orfane-vedove-righe-finali-corte-e-regole-di-tenuta)
- [Divisione in righe ottimale (Knuth–Plass)](https://postext.dev/it/docs/justification.md#knuth-plass-vedere-lintero-paragrafo)
- [Aperture progettate](https://postext.dev/it/docs/configuration-headings.md#estensione-e-grafica-avanzata)
- [Immagini nei progetti di pagina](https://postext.dev/it/docs/configuration-headers-footers.md#elementi-immagine)
- [Attributi dei titoli](https://postext.dev/it/docs/document-format.md#attributi-dei-titoli)
- [Testatine e folio](https://postext.dev/it/docs/configuration-headers-footers.md#intestazioni-e-piè-di-pagina)
- [Testatine secondo il ruolo della pagina](https://postext.dev/it/docs/configuration-headers-footers.md#elementi-di-testo)
- [Tavolozza di colori semantica](https://postext.dev/it/docs/configuration-fonts-colors-viewers.md#tavolozza-dei-colori)
- [Stili di paragrafo](https://postext.dev/it/docs/configuration-styles.md#stili-di-paragrafo)
- [Fascia di capitolo a tutta larghezza](https://postext.dev/it/docs/configuration-headings.md#estensione-e-grafica-avanzata)
- [Figure e tabelle come risorse](https://postext.dev/it/docs/document-format-resources.md#risorse)

**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-headers-footers.md#intestazioni-e-piè-di-pagina), [`header`](https://postext.dev/it/docs/configuration-headers-footers.md#intestazioni-e-piè-di-pagina), [`headings`](https://postext.dev/it/docs/configuration-headings.md#titoli), [`layout`](https://postext.dev/it/docs/configuration-page-layout.md#impaginazione), [`locale`](https://postext.dev/it/docs/configuration-language.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#rasterizzare-una-pagina)

**Caratteri**

- Public Sans (OFL-1.1), Libre Caslon Display (OFL-1.1), Libre Caslon Text (OFL-1.1)

## Preparazione

### 1 · Dai alla colonna a bandiera una leva sua

```js
// script.js, righe 37–55
const balancing = {
  // A ragged column has no lever of its own when the widow and orphan rules hold its last
  // line back: no heading or list end to open, no justified paragraph to set looser. This
  // one lets a paragraph of the short column run a line long, within its own word spacing
  // (or a justified one no looser than maxWordSpacing), or, when none can, set one a line
  // short so the split after it takes the line. A change is kept only when the pages end
  // with fewer short columns; otherwise the column stays as it was.
  fillShortColumns: true, // default false
  // The paragraphs go first: a crosshead keeps its one line of air unless they cannot help.
  maxLinesBeforeLoose: 0,
};
const bodyText = {
  fontFamily: 'Public Sans', fontSize: pt(9.4), lineHeight: pt(LEAD), color: col('ink'),
  boldColor: col('ink'), italicColor: col('ink'), referenceColor: col('ink'),
  textAlign: 'left', firstLineIndent: pt(0), paragraphSpacing: true, // a grid line between
  hyphenation: { enabled: true }, optimalLineBreaking: true, optimalRagged: true,
  avoidWidows: true, widowMinLines: 2, avoidOrphans: true, orphanMinLines: 2, // the cause
  avoidRunts: true,
};
```

Il bilanciamento delle colonne riempie una colonna corta aggiungendo righe di griglia sopra un titolo, dopo un elenco, sotto un riquadro o una figura e, come ultima risorsa, componendo un paragrafo giustificato una riga più largo. Una colonna di testo corrente a bandiera non ha nulla di tutto questo tra i suoi titoletti, e la riga che le regole su vedove e orfane trattengono resta vuota. `fillShortColumns` lascia che un paragrafo a bandiera di quella colonna occupi una riga in più con la propria spaziatura; quando nessuno può, ne compone uno con una riga in meno perché il paragrafo dopo l'interruzione salga e riempia la riga. Il motore tiene la modifica solo se finiscono corte meno colonne, quindi attivarla non peggiora mai una pagina. `maxLinesBeforeLoose: 0` prova i paragrafi prima dei titoli, così ogni titoletto conserva una sola riga d'aria sopra di sé.

### 2 · Componi l'articolo due volte e confronta

```js
// script.js, righe 204–208
const withFill = (fill) => ({ ...config, headings: { ...config.headings,
  balancing: { ...balancing, fillShortColumns: fill } } }); // a copy: config keeps true
const build = (fill) => buildDocumentWithFonts({ markdown, resources }, withFill(fill),
  kitFonts(FONTS));
const docs = { off: await build(false), on: await build(true) };
```

I due documenti nascono dalla stessa configurazione e dalla stessa stringa Markdown. La seconda composizione passa la configurazione così com'è; la prima, una copia con l'impostazione spenta, così il pacchetto che si apre nel Sandbox porta `fillShortColumns: true`. I due pulsanti sopra il tavolo passano da una composizione all'altra. Si riempiono fino al piede solo le pagine che proseguono: la pagina 3 chiude l'articolo e le sue colonne vengono tagliate alla stessa altezza in entrambe le composizioni.

### 3 · Apri con il dipinto

```js
// script.js, righe 59–85
const DEPTH = 150; // mm: how far down the page the painting bleeds
const sans = { fontFamily: 'Public Sans', fontWeight: 700, textTransform: 'uppercase' };
const at = (to, edge, x, y, width) => ({ anchor: { to, edge },
  offset: { x: mm(x), y: mm(y) }, ...(width && { size: { width: mm(width) } }) });
const air = { padding: { bottom: mm(6) } }; // counts: the text starts 6 mm under the byline
const opener = {
  enabled: true,
  slot: { elements: [
    { kind: 'image', id: 'art', resourceId: 'lens', placement: { anchor: { to: 'bleed',
      edge: 'top-left' }, size: { width: 'fill', height: mm(DEPTH) } } },
    { kind: 'text', id: 'credit', content: '{attr.credit}', ...sans, fontWeight: 500,
      fontSize: pt(6.5), letterSpacing: pt(0.6), color: col('muted'), align: 'right',
      placement: at('container', 'top-right', 0, DEPTH - TOP + 2) },
    { kind: 'text', id: 'kicker', content: '{attr.kicker}', ...sans, fontSize: pt(8.5),
      letterSpacing: pt(1.7), color: col('brass'), align: 'left',
      placement: at('container', 'top-left', 0, DEPTH - TOP + 9) },
    { kind: 'text', id: 'headline', content: '{titleText}', fontFamily: 'Libre Caslon Display',
      fontSize: pt(46), lineHeight: 1.02, color: col('ink'), align: 'left',
      placement: at('#kicker', 'below', 0, 2.5, 112) },
    { kind: 'text', id: 'standfirst', content: '{attr.standfirst}', fontFamily: 'Libre Caslon Text',
      italic: true, fontSize: pt(12), lineHeight: 1.3, color: col('sea'), align: 'left',
      placement: at('#headline', 'right-of', 8, 2.5) },
    { kind: 'text', id: 'byline', content: '{attr.byline}', ...sans, fontSize: pt(7.5),
      letterSpacing: pt(1.3), color: col('ink'), align: 'left', box: air,
      placement: at('#standfirst', 'below', 0, 3.5) },
  ] },
};
```

Il dipinto è un elemento immagine ancorato al margine di stampa, alto 150 mm, quindi arriva al taglio su tre lati, cosa che nessun elemento flottante fa. L'occhiello, il titolo, il sommario e la firma vengono dagli attributi della riga `#`. La firma termina con un margine interno di 6 mm che conta nell'altezza dell'apertura, così le colonne cominciano sulla prima riga di griglia sottostante.

### 4 · Testatine fuori dall'apertura

```js
// script.js, righe 89–106
const label = { ...sans, fontWeight: 600, fontSize: pt(7.5), letterSpacing: pt(1.3),
  color: col('muted') };
const folio = { fontFamily: 'Public Sans', fontWeight: 700, fontSize: pt(8.5), color: col('ink') };
const pin = (edge, x, y) => ({ anchor: { to: 'page', edge }, offset: { x: mm(x), y: mm(y) } });
const sides = [['even', 'left', 1], ['odd', 'right', -1]]; // 1: the outer edge is on the left
const header = { elements: sides.flatMap(([parity, edge, s]) => [
  { kind: 'text', id: `folio-${parity}`, content: '{pageNumber}', ...folio,
    placement: pin(`top-${edge}`, s * OUTER, 12) },
  { kind: 'text', id: `head-${parity}`, ...label,
    content: s > 0 ? '{title} · {subtitle}' : '{chapterTitle}',
    placement: pin(`top-${edge}`, s * (OUTER + 9), 12.35) }, // the smaller size's baseline
  { kind: 'rule', id: `rule-${parity}`, direction: 'horizontal', thickness: pt(0.5),
    color: col('rule'), placement: { ...pin(`top-${edge}`, s * OUTER, 17),
      size: { width: mm(197) } } },
].map((element) => ({ ...element, parity, pages: 'body' }))) }; // none on the opener
const footer = { elements: sides.map(([parity, edge, s]) => ({ kind: 'text', parity,
  id: `drop-folio-${parity}`, ...folio, content: '{pageNumber}', pages: 'opener', align: edge,
  placement: pin(`bottom-${edge}`, s * OUTER, -11) })) }; // the opener's only folio
```

La pagina pari nomina la rivista e il numero dal frontmatter, la dispari l'articolo, e `pages: 'body'` le tiene fuori dall'apertura, che porta solo un numero di pagina al piede. L'etichetta scende di 0,35 mm rispetto al numero di pagina perché i due corpi condividano la linea di base. Il filetto sotto misura 197 mm, la larghezza della gabbia del testo.

## 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/filling-short-columns

### script.js

```js
// ═══ Postext Cookbook · Nº 177 · Filling the column the widow rule left short ═════
// https://postext.dev/en/cookbook/filling-short-columns
// Code: MIT · Text: original (CC BY 4.0) · Painting: diffusion model
// Fonts: Public Sans, Libre Caslon Display, Libre Caslon Text (OFL) · Needs postext ≥ 1.65.0
//
// A magazine long read in two ragged columns, set twice. Without fillShortColumns a column
// whose paragraph would split into a widow ends a line above the foot; with it, a paragraph
// of that column is set a line longer or shorter, and the column ends on the last grid line.
import {
  buildDocumentWithFonts, renderPageToCanvas, registerResourceImage,
} from 'https://esm.sh/postext';

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

// ─── 1 · Design ─────────────────────────────────────────────────────────────
// Every colour below is linked to this palette by id.
const palette = {
  ink: '#1b2026', // text: a slate near-black
  brass: '#875a14', // the accent: kicker and crossheads (6:1 on paper)
  sea: '#2c4a5e', // the standfirst
  rule: '#c9ccd0', // hairlines
  muted: '#5d656e', // running heads, credit, colophon
  paper: '#ffffff',
};
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 defaults link to 'main-color': point it at the accent, so nothing prints blue.
  { id: 'main-color', name: 'brass (defaults)', value: { hex: palette.brass, model: 'hex' } },
];
const TOP = 22; // top margin in mm: the opener's container starts here
const OUTER = 14; // outer margin in mm: folios and running heads align to it
const LEAD = 13.2; // body leading in pt: the baseline grid every column ends on

// #region answer: ragged columns that end on the grid: fillShortColumns
const balancing = {
  // A ragged column has no lever of its own when the widow and orphan rules hold its last
  // line back: no heading or list end to open, no justified paragraph to set looser. This
  // one lets a paragraph of the short column run a line long, within its own word spacing
  // (or a justified one no looser than maxWordSpacing), or, when none can, set one a line
  // short so the split after it takes the line. A change is kept only when the pages end
  // with fewer short columns; otherwise the column stays as it was.
  fillShortColumns: true, // default false
  // The paragraphs go first: a crosshead keeps its one line of air unless they cannot help.
  maxLinesBeforeLoose: 0,
};
const bodyText = {
  fontFamily: 'Public Sans', fontSize: pt(9.4), lineHeight: pt(LEAD), color: col('ink'),
  boldColor: col('ink'), italicColor: col('ink'), referenceColor: col('ink'),
  textAlign: 'left', firstLineIndent: pt(0), paragraphSpacing: true, // a grid line between
  hyphenation: { enabled: true }, optimalLineBreaking: true, optimalRagged: true,
  avoidWidows: true, widowMinLines: 2, avoidOrphans: true, orphanMinLines: 2, // the cause
  avoidRunts: true,
};
// #endregion

// #region opener: the painting bled across the top, then kicker, headline and standfirst
const DEPTH = 150; // mm: how far down the page the painting bleeds
const sans = { fontFamily: 'Public Sans', fontWeight: 700, textTransform: 'uppercase' };
const at = (to, edge, x, y, width) => ({ anchor: { to, edge },
  offset: { x: mm(x), y: mm(y) }, ...(width && { size: { width: mm(width) } }) });
const air = { padding: { bottom: mm(6) } }; // counts: the text starts 6 mm under the byline
const opener = {
  enabled: true,
  slot: { elements: [
    { kind: 'image', id: 'art', resourceId: 'lens', placement: { anchor: { to: 'bleed',
      edge: 'top-left' }, size: { width: 'fill', height: mm(DEPTH) } } },
    { kind: 'text', id: 'credit', content: '{attr.credit}', ...sans, fontWeight: 500,
      fontSize: pt(6.5), letterSpacing: pt(0.6), color: col('muted'), align: 'right',
      placement: at('container', 'top-right', 0, DEPTH - TOP + 2) },
    { kind: 'text', id: 'kicker', content: '{attr.kicker}', ...sans, fontSize: pt(8.5),
      letterSpacing: pt(1.7), color: col('brass'), align: 'left',
      placement: at('container', 'top-left', 0, DEPTH - TOP + 9) },
    { kind: 'text', id: 'headline', content: '{titleText}', fontFamily: 'Libre Caslon Display',
      fontSize: pt(46), lineHeight: 1.02, color: col('ink'), align: 'left',
      placement: at('#kicker', 'below', 0, 2.5, 112) },
    { kind: 'text', id: 'standfirst', content: '{attr.standfirst}', fontFamily: 'Libre Caslon Text',
      italic: true, fontSize: pt(12), lineHeight: 1.3, color: col('sea'), align: 'left',
      placement: at('#headline', 'right-of', 8, 2.5) },
    { kind: 'text', id: 'byline', content: '{attr.byline}', ...sans, fontSize: pt(7.5),
      letterSpacing: pt(1.3), color: col('ink'), align: 'left', box: air,
      placement: at('#standfirst', 'below', 0, 3.5) },
  ] },
};
// #endregion

// #region heads: magazine and issue on the verso, the story on the recto
const label = { ...sans, fontWeight: 600, fontSize: pt(7.5), letterSpacing: pt(1.3),
  color: col('muted') };
const folio = { fontFamily: 'Public Sans', fontWeight: 700, fontSize: pt(8.5), color: col('ink') };
const pin = (edge, x, y) => ({ anchor: { to: 'page', edge }, offset: { x: mm(x), y: mm(y) } });
const sides = [['even', 'left', 1], ['odd', 'right', -1]]; // 1: the outer edge is on the left
const header = { elements: sides.flatMap(([parity, edge, s]) => [
  { kind: 'text', id: `folio-${parity}`, content: '{pageNumber}', ...folio,
    placement: pin(`top-${edge}`, s * OUTER, 12) },
  { kind: 'text', id: `head-${parity}`, ...label,
    content: s > 0 ? '{title} · {subtitle}' : '{chapterTitle}',
    placement: pin(`top-${edge}`, s * (OUTER + 9), 12.35) }, // the smaller size's baseline
  { kind: 'rule', id: `rule-${parity}`, direction: 'horizontal', thickness: pt(0.5),
    color: col('rule'), placement: { ...pin(`top-${edge}`, s * OUTER, 17),
      size: { width: mm(197) } } },
].map((element) => ({ ...element, parity, pages: 'body' }))) }; // none on the opener
const footer = { elements: sides.map(([parity, edge, s]) => ({ kind: 'text', parity,
  id: `drop-folio-${parity}`, ...folio, content: '{pageNumber}', pages: 'opener', align: edge,
  placement: pin(`bottom-${edge}`, s * OUTER, -11) })) }; // the opener's only folio
// #endregion

const config = {
  locale: t({ en: 'en-gb', es: 'es' }), // each edition's hyphenation patterns
  colorPalette,
  page: { width: mm(225), height: mm(297), dpi: 150,
    margins: { top: mm(TOP), bottom: mm(21), left: mm(16), right: mm(OUTER), mirror: true } },
  layout: { layoutType: 'double', gutterWidth: mm(7) }, // a 94 mm measure, about 62 characters
  bodyText,
  headings: {
    fontFamily: 'Libre Caslon Text', fontWeight: 400, color: col('ink'),
    balancing, // the first build turns it off (see builds)
    levels: [
      { level: 1, span: 'page', breakBefore: { enabled: true, parity: 'any' },
        marginBottom: pt(0), advancedDesign: opener },
      { level: 2, fontSize: pt(13), lineHeight: pt(LEAD), italic: true, color: col('brass'),
        marginTop: pt(LEAD), marginBottom: pt(0) }, // crossheads, one grid line above
    ],
  },
  paragraphStyles: [{ id: 'colophon', fontFamily: 'Public Sans', fontSize: pt(6.8),
    lineHeight: pt(9.6), color: col('muted'), textAlign: 'left', firstLineIndent: pt(0),
    marginTop: pt(LEAD) }],
  header, footer,
};

// ─── 2 · Content ────────────────────────────────────────────────────────────
const resources = [
  { id: 'lens', typeId: 'figure', kind: 'bitmap', createdAt: 0, updatedAt: 0, // never cited:
    // only the opener draws it, cropped to its 225 × 150 mm box
    bitmap: { fileId: 'lens-1400.jpg', format: 'jpeg', width: 1400, height: 933 },
    altText: t({ en: 'A lit Fresnel lens of brass and glass rings in a lighthouse lantern at '
      + 'dusk, with the sea and a sunset behind the diagonal panes.',
    es: 'Una lente de Fresnel encendida, de anillos de vidrio y latón, en la linterna de un faro '
      + 'al anochecer, con el mar y la puesta de sol tras los cristales en rombo.' }) },
];
const markdown = String.raw`---
title: "Littoral"
subtitle: "Autumn 2026"
---

# The Lens That Lit the Coast {kicker="History · Lights" standfirst="Two hundred years ago a road engineer in poor health took the mirrors out of a lighthouse and put rings of glass in their place. His lenses still turn on a few headlands, fewer every winter." byline="Words by Marta Quiroga" credit="Painting made for this issue"}

The tower of Cordouan stands on a reef at the mouth of the Gironde, six kilometres from either shore, and at high tide it seems to stand on the sea itself. The masons of Louis de Foix began it in 1584 and finished it in 1611, a lighthouse dressed as a palace, with a royal chapel on its third floor. For two centuries its light was a fire of oak and pitch, and later of whale oil, burning in an open brazier under the lantern. Sailors coming in from the Bay of Biscay often saw the tower before they saw its fire. On a hazy night they saw neither, and more than one ship found the reef of Cordouan before it found the channel into the river and the port of Bordeaux beyond it.

In the summer of 1823 a new apparatus went up the stairs of Cordouan in crates. It was a cage of brass, taller than a man, holding panels of glass cut into steps, and in the middle of it a single oil lamp with several concentric wicks. On the first night it was lit, the light reached the pilots at Royan more clearly than the old reflectors ever had. The engineer who designed it, Augustin Fresnel, was thirty-five and already ill with the tuberculosis that would kill him four years later.

Fresnel was not a lighthouse man. He trained at the École polytechnique and spent his first years building roads in the Drôme for the Corps des Ponts et Chaussées, and he did his real work at night, on the nature of light. His memoir on diffraction won the prize of the Academy of Sciences in 1819, against judges who expected light to be a stream of particles. That same year the Commission des Phares, the body in charge of France's coastal lights, asked him to look at its lamps. He found a waste of light that offended him as a physicist.

The lamp came first. With the physicist François Arago, Fresnel built a burner with several round wicks set one inside another, fed with oil from below by a small pump so that the flames never ran dry. It gave a broad, steady flame, much brighter than one wick, but a brighter flame was no use if most of it lit the inside of the lantern. The question was how to send it out to sea.

## A lamp in a ring of prisms

Before Fresnel, the best lighthouses used reflectors. An Argand lamp burned at the focus of a polished copper bowl, silvered on the inside, and the bowl threw the light forward in a beam. A tower needed a dozen or more of these lamps and mirrors, each with its own wick to trim. The silver tarnished, the bowls were never quite parabolic, and much of the light that fell on them was absorbed or scattered. Keepers spent their days polishing, and the beam was still weak.

A lens could do better, in theory, but a lens large enough for a lighthouse would have been a block of glass thick at the centre, heavy enough to crack its own frame, and too cloudy to pass much light. Buffon had suggested a century earlier that a burning glass could be cut into steps, keeping only the curved surface that does the bending. Fresnel took the idea further. He built his lens from separate rings, each ground to its own curve and all bringing the light to the same point, so that no piece of glass was thicker than a few centimetres.

Around the central belt of rings he added prisms above and below. These catch the light that a lens would let escape towards the sky or the sea and turn it back towards the horizon, by refraction and then, in the outer rows, by reflection inside the glass itself. Fresnel worked out these reflecting prisms in the last years of his life, writing to his brother Léonor about their angles. He did not live to see a light built with them from top to bottom: Léonor, who took over his post at the Commission, saw the first ones lit in the 1840s.

The result was a lamp in a ring of prisms that sent most of its light where a ship could see it. A single wick in a large Fresnel lens did the work of the twenty or so lamps and mirrors it replaced, on a fraction of the oil. On a clear night the light of a coast station could be seen from more than twenty nautical miles, limited less by the lamp than by the curve of the Earth.

The glass itself came in pieces. Each ring of a large lens was ground on a turning table against a cast-iron tool of the right curve, then polished with rouge for days, and the finished pieces were bedded in their brass frame with putty. The lens arrived at the lighthouse as a kit of several hundred parts, and the engineer who put it together had to align every ring with the flame to within a millimetre or two, by eye, at night, moving a lamp and watching where its light fell on the rocks below.

## Seven orders of glass

The Commission soon needed lenses of several sizes, and Fresnel's successors sorted them into orders by focal distance, the space between the flame and the inner face of the glass. A first-order lens, for the great coastal lights, had a focal distance of 920 millimetres and stood about two and a half metres high. A sixth-order lens, for a harbour pier or a river mouth, had 150 millimetres and could be carried by two people. Between them came the second, third and fourth orders, with a three-and-a-half added later for the middle distances of estuaries.

Each lens had to say which light it was. A sailor who sees a light on a dark coast needs to know whether it is Cordouan, the Pointe de Grave or a fishing village, and the answer is in its rhythm. Fixed lights burned steadily. Flashing lights used panels of rings like bull's eyes, set round the lamp and turned by clockwork, so that each panel swept a beam across the sea as it passed. The number of panels and the speed of the turn gave each tower its own count of flashes and dark intervals, printed in the lists of lights that every navigator carried.

The clockwork had to be wound by hand. A heavy weight hung in a tube running down the centre of the tower, and the keeper on watch cranked it back up every few hours through the night. If he fell asleep, the lens slowed and stopped, and the light lost its character: to a ship offshore it became another light, or no light at all. In the 1890s French engineers floated the rotating lens on a bath of mercury, so that a small push turned several tonnes of brass and glass. The lens could turn faster, and the flashes grew shorter and brighter.

Paris made most of the lenses. The workshops of Henry-Lepaute, Sautter and Barbier ground and polished the rings by hand and shipped them in numbered crates to coasts all over the world, with drawings for assembling them on the spot. Chance Brothers, near Birmingham, made glass for the British lights and later lenses of their own, including the largest of all, the hyperradiant lenses of the 1880s, with a focal distance of 1,330 millimetres. In the United States, the Lighthouse Board set up in 1852 replaced the old reflectors with French lenses within a decade.

## When the keepers left

A lens could last for more than a century. The glass did not wear out, and the brass needed only cleaning. What changed around the lenses was the lamp. Oil gave way to paraffin vapour burners with incandescent mantles at the end of the nineteenth century, then to electric lamps, at first in the towers that had a supply nearby and later almost everywhere. The light grew brighter, but the lens around it stayed the same, because it already put nearly all of that light on the horizon.

For the keepers, the routine hardly changed in a hundred years. At sunset they lit the lamp and wound the clockwork; through the night they took turns on watch in the service room under the lantern, writing the weather and the state of the light in the station's log every few hours; at dawn they put the light out, drew linen curtains round the lens so that the sun would not set fire to anything through it, and began cleaning. Brass was polished, glass wiped with spirit and a soft cloth, never anything harder.

Electricity also made the keepers unnecessary. A lamp that did not need its wick trimmed and a motor that did not need winding could be watched from a distance. Through the second half of the twentieth century, lighthouse after lighthouse was automated, its family moved ashore and its door locked. The last keepers of the British lights left North Foreland, in Kent, in November 1998. In France the keepers of the offshore towers, the ones the service called hells, were among the last to go.

The lenses went next. A modern beacon of light-emitting diodes weighs a few kilograms, uses less power than a kettle and needs no mercury, which has its own risks for those who climb the tower. Some lenses were taken down and sold, some broke in storage, and some were cut into pieces for collectors. The ones that survive have mostly gone to museums, where they are lit by a bulb inside and look like what they are, a machine for sending light a long way.

A few still work. At Cordouan a first-order lens still turns in the lantern, and the reef that holds the tower became a World Heritage site in 2021. Visitors who make the crossing at low tide can climb the stairs past the chapel to the lantern gallery and stand beside the rings of glass. The light still shows its own rhythm to the pilots of the Gironde, white over the channel and coloured over the shoals, as the ships coming in from Biscay have read it for two hundred years.

:::paragraphs{style="colophon"}
Set in Public Sans, Libre Caslon Display and Libre Caslon Text (SIL Open Font License) · Text: Postext Cookbook, CC BY 4.0 · Painting made for this issue with a diffusion model · The magazine and the writer are fictional.
:::
`; // content.<lang>.md, inlined by the Cookbook

// ─── 3 · Fonts ──────────────────────────────────────────────────────────────
const FONTS = { // text, display and label faces, loaded before the build
  'Public Sans': ['400', '400i', '500', '600', '700'], 'Libre Caslon Display': ['400'],
  'Libre Caslon Text': ['400', '400i'] };

// ─── 4 · Build & show ───────────────────────────────────────────────────────
await loadImage('lens-1400.jpg', asset('lens-1400.jpg'));
// #region builds: the same text and design, without and then with fillShortColumns
const withFill = (fill) => ({ ...config, headings: { ...config.headings,
  balancing: { ...balancing, fillShortColumns: fill } } }); // a copy: config keeps true
const build = (fill) => buildDocumentWithFonts({ markdown, resources }, withFill(fill),
  kitFonts(FONTS));
const docs = { off: await build(false), on: await build(true) };
// #endregion
const title = t({ en: 'Filling the column the widow rule left short',
  es: 'La columna que la regla de viudas dejaba corta' });
// Two buttons put either build on the desk.
const LABELS = { off: 'fillShortColumns: false', on: 'fillShortColumns: true' };
const switches = Object.keys(docs).map((key) => Object.assign(document.createElement('button'),
  { type: 'button', value: key, textContent: LABELS[key] }));
const show = (key) => {
  showPages(docs[key], { title });
  for (const b of switches) b.ariaPressed = String(b.value === key);
};
for (const b of switches) b.addEventListener('click', () => show(b.value));
show('on');
document.getElementById('pt-actions').prepend(...switches);
document.head.insertAdjacentHTML('beforeend',
  '<style>#pt-actions [aria-pressed=true] { text-decoration: underline }</style>');

// ─── 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 and asks the kit
// for Fontsource's files, the ones the PDF embeds too.

/** buildDocumentWithFonts' options: the resolver, and FONTS' faces loaded
 *  with the config's. 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 files with their ranges: latin, then latin-ext and greek when
 *  `text` needs them (kitSubsetsFor). Null for a face Fontsource lacks. */
async function fontsourceResolver(family, weight, style, { text = '' } = {}) {
  const id = fontsourceId(family);
  const meta = await fontsourceMeta(family);
  if (meta && !(await fontsourceWeights(family, style)).includes(weight)) 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],
  }));
}

/** Each file's unicode-range (a function: the kit runs after the pen's code). */
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: loads `faces` (as FONTS) for the
 *  sample `text`; `optional` skips a face Fontsource lacks. Files are added in
 *  the order asked, the last winning a shared character. Resolves to the count. */
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 && (await fontsourceWeights(family, style)).includes(weight))) 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 face (fonts.check() says yes to any family). */
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, id = fontsourceId(family)) {
  return (fontsourceMeta.c ??= {})[id] ??= fetch(`https://api.fontsource.org/v1/fonts/${id}`)
    .then((res) => (res.ok ? res.json() : null), () => null);
}

/** The weights the package ships in `style`: the API pairs none (no Libre
 *  Caslon 700 italic file), so italics are read from latin-italic.css. */
function fontsourceWeights(family, style, id = fontsourceId(family)) {
  return (fontsourceWeights.c ??= {})[id + style] ??= fontsourceMeta(family).then(async (m) => {
    if (!m?.styles.includes(style)) return m ? [] : [400, 700];
    const css = style === 'italic' ? await fetch(`${await fontsourceAt(id)}latin-italic.css`).then((r) => (r.ok ? r.text() : ''), () => '') : '';
    const w = [...css.matchAll(/weight: (\d+)/g)].map((x) => +x[1]);
    return w.length ? w : m.weights;
  });
}

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

### Lascia che i titoletti prendano il vuoto per primi

Con l'ordine predefinito un titolo prende fino a due righe di griglia prima che un paragrafo si allunghi, e i titoletti della pagina seguente tornano ad aprirsi.

```diff
-  maxLinesBeforeLoose: 0,
+  maxLinesBeforeLoose: 2,
```

### Giustifica il testo

Un paragrafo giustificato poteva già occupare una riga in più senza l'impostazione, entro `maxWordSpacing`. Con `fillShortColumns` può occuparne anche una in meno, e non viene mai composto più largo della sua riga più larga.

```diff
-  textAlign: 'left', firstLineIndent: pt(0), paragraphSpacing: true, // a grid line between
+  textAlign: 'justify', firstLineIndent: pt(0), paragraphSpacing: true, // a grid line between
```

## Errori frequenti

- L'impostazione chiude un vuoto di una riga. Una colonna a cui ne mancano due, per esempio una che finisce dopo lo spazio tra paragrafi con posto per una sola riga del seguente, può restare corta: aggiusta il testo con una frase.
- Un paragrafo la cui nuova composizione finirebbe su una riga troppo corta non viene usato, quindi una colonna può restare corta quando ha solo paragrafi brevi.

## Crediti

- Ricetta: Ignacio Ferro ([@drnachio](https://github.com/drnachio))
- Immagini: A lit Fresnel lens in a lighthouse lantern at dusk: Generated With Diffusion Models, originale
- Caratteri: Public Sans (OFL-1.1), Libre Caslon Display (OFL-1.1), Libre Caslon Text (OFL-1.1)
- Codice: MIT · Contenuto di esempio: CC-BY-4.0

## Ricette correlate

- [Nº 116 · Foto che crescono per riempire una colonna corta](https://postext.dev/it/cookbook/photos-fill-short-columns.md): Ogni foto segna una zona sicura; il bilanciamento può ritagliare fuori da essa per alzarla, e una colonna accorciata da un titolo o un flottante finisce a filo. · Livello 2 (Intermedio) · Riviste e fanzine
- [Nº 037 · Bilancio annuale con colonne pareggiate al piede](https://postext.dev/it/cookbook/annual-report-flush-columns.md): Il bilancio annuale di una cooperativa energetica su due colonne giustificate che finiscono sulla stessa riga, un riquadro di cifre chiave e una chiusa pari. · Livello 3 (Avanzato) · Rapporti
- [Nº 004 · Servizio di rivista: dall’apertura con foto al segno finale](https://postext.dev/it/cookbook/magazine-feature-opener.md): Un’apertura advancedDesign compone una foto al vivo con occhiello, titolo, sommario e firma del titolo; poi due riquadri flottanti e un segno finale in un chip. · Livello 3 (Avanzato) · Riviste e fanzine
