# A coluna que a regra das viúvas deixava curta

> Colunas alinhadas à esquerda que terminam na grade: fillShortColumns alonga ou encurta em uma linha um parágrafo da coluna curta em vez de deixar um vão no pé.

- Versão HTML: https://postext.dev/pt/cookbook/filling-short-columns
- Receita Nº 177 · Página e grade · Nível 2 (Intermediário) · Saídas: Canvas
- Gêneros: Revistas e fanzines
- Requer postext ≥ 1.65.0 · testada com 1.65.0 em 2026-10-31
- Páginas: [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)
- Abrir no Sandbox: https://postext.dev/pt/sandbox#recipe=filling-short-columns&lang=en (.postext: https://postext.dev/cookbook/filling-short-columns/en/filling-short-columns.postext)
- Última atualização: 2026-10-31
- Outros idiomas: [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), [it](https://postext.dev/it/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)

## Em poucas palavras

Quando uma coluna para uma linha acima do pé porque um parágrafo não pode ser quebrado ali, este ajuste refaz as linhas de um parágrafo da coluna para que ela chegue até embaixo.

## O que você vai compor

Três páginas de *Littoral*, uma revista do litoral: uma longa matéria sobre a lente de Fresnel, aberta pela pintura de uma lente acesa em sua lanterna. O texto corre em duas colunas alinhadas à esquerda em Public Sans, com uma linha de grade em branco entre os parágrafos, e as regras de viúvas e órfãs mantêm pelo menos duas linhas de um parágrafo de cada lado de uma quebra. O pen compõe a matéria duas vezes. Sem o ajuste, na [abertura](https://postext.dev/cookbook/filling-short-columns/en/p01.webp?v=76529999) a coluna da esquerda termina uma linha acima da da direita: o parágrafo seguinte não pode começar com uma só linha no pé. Com ele, na [mesma página](https://postext.dev/cookbook/filling-short-columns/en/p04.webp?v=76529999), o primeiro parágrafo ganha uma linha e as duas colunas terminam na última linha da grade. Na página seguinte, o ajuste também recolhe o respiro a mais que o equilíbrio havia posto acima de dois intertítulos.

**Esta receita responde a:**

- Por que uma coluna alinhada à esquerda termina uma linha antes do pé, e como preenchê-la sem viúvas nem órfãs?
- Como deixo as colunas alinhadas no pé e a última página equilibrada (justificação vertical)?
- Como evito viúvas, órfãs e últimas linhas de uma só palavra, e mantenho cada título junto do seu texto?

## A resposta curta

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

## Ingredientes

**Ensina**

- [Colunas curtas preenchidas](https://postext.dev/pt/docs/configuration-page-flow.md#equilíbrio-de-colunas): Quando as regras de viúvas e órfãs deixam uma coluna uma ou duas linhas acima do pé, o equilíbrio distribui o vão entre os títulos, listas e parágrafos da coluna para que ela termine rente (headings.balancing.fillShortColumns).

**Também usa**

- [Equilíbrio de colunas](https://postext.dev/pt/docs/configuration-page-flow.md#equilíbrio-de-colunas)
- [Viúvas, órfãs e linhas curtas](https://postext.dev/pt/docs/configuration-page-flow.md#órfãs-viúvas-linhas-curtas-e-regras-de-manter-junto)
- [Quebra de linha ótima (Knuth–Plass)](https://postext.dev/pt/docs/justification.md#knuth-plass-o-parágrafo-inteiro-de-uma-vez)
- [Aberturas desenhadas](https://postext.dev/pt/docs/configuration-headings.md#largura-e-design-avançado)
- [Imagens nos designs de página](https://postext.dev/pt/docs/configuration-headers-footers.md#elementos-de-imagem)
- [Atributos de título](https://postext.dev/pt/docs/document-format.md#atributos-de-título)
- [Cabeços e fólios](https://postext.dev/pt/docs/configuration-headers-footers.md#cabeçalhos-e-rodapés)
- [Cabeços por tipo de página](https://postext.dev/pt/docs/configuration-headers-footers.md#elementos-de-texto)
- [Paleta de cores semântica](https://postext.dev/pt/docs/configuration-fonts-colors-viewers.md#paleta-de-cores)
- [Estilos de parágrafo](https://postext.dev/pt/docs/configuration-styles.md#estilos-de-parágrafo)
- [Faixa de capítulo em largura total](https://postext.dev/pt/docs/configuration-headings.md#largura-e-design-avançado)
- [Figuras e tabelas como recursos](https://postext.dev/pt/docs/document-format-resources.md#recursos)

**A configuração em resumo**

- [`bodyText`](https://postext.dev/pt/docs/configuration-text.md#texto-do-corpo), [`colorPalette`](https://postext.dev/pt/docs/configuration-fonts-colors-viewers.md#paleta-de-cores), [`footer`](https://postext.dev/pt/docs/configuration-headers-footers.md#cabeçalhos-e-rodapés), [`header`](https://postext.dev/pt/docs/configuration-headers-footers.md#cabeçalhos-e-rodapés), [`headings`](https://postext.dev/pt/docs/configuration-headings.md#títulos), [`layout`](https://postext.dev/pt/docs/configuration-page-layout.md#diagramação), [`locale`](https://postext.dev/pt/docs/configuration-language.md#hifenização), [`page`](https://postext.dev/pt/docs/configuration-page-layout.md#página), [`paragraphStyles`](https://postext.dev/pt/docs/configuration-styles.md#estilos-de-parágrafo)

**API**

- [`buildDocumentWithFonts`](https://postext.dev/pt/docs/configuration-programmatic-usage.md#carregar-as-fontes-antes-da-diagramação), [`registerResourceImage`](https://postext.dev/pt/docs/architecture.md#superfície-da-api), [`renderPageToCanvas`](https://postext.dev/pt/docs/configuration-programmatic-usage.md#renderizar-uma-página-como-bitmap)

**Tipos**

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

## Preparo

### 1 · Dê à coluna alinhada à esquerda uma alavanca própria

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

O equilíbrio de colunas preenche uma coluna curta acrescentando linhas de grade acima de um título, depois de uma lista, sob um box ou uma figura e, em último caso, compondo um parágrafo justificado uma linha mais folgado. Uma coluna de texto corrido alinhada à esquerda não tem nada disso entre seus intertítulos, e a linha que as regras de viúvas e órfãs seguram fica vazia. `fillShortColumns` deixa um parágrafo dessa coluna ocupar uma linha a mais com o próprio espaçamento; quando nenhum consegue, compõe um com uma linha a menos para que o parágrafo depois da quebra suba e ocupe a linha. O motor só mantém a mudança se menos colunas terminarem curtas, então ativá-lo nunca piora uma página. `maxLinesBeforeLoose: 0` tenta os parágrafos antes dos títulos, e cada intertítulo mantém uma única linha de respiro acima.

### 2 · Componha a matéria duas vezes e compare

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

Os dois documentos saem da mesma configuração e da mesma string Markdown. A segunda composição passa a configuração como está; a primeira, uma cópia com o ajuste desligado, de modo que o pacote aberto no Sandbox leva `fillShortColumns: true`. Os dois botões acima da mesa alternam entre as composições. Só as páginas que continuam são preenchidas até o pé: a página 3 fecha a matéria, e suas colunas são cortadas na mesma altura nas duas composições.

### 3 · Abra com a pintura

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

A pintura é um elemento de imagem ancorado à sangria, com 150 mm de altura, e por isso chega ao corte em três lados, o que nenhum flutuante faz. O chapéu, o título, a linha fina e a assinatura vêm dos atributos da linha `#`. A assinatura termina em um preenchimento de 6 mm que conta na altura da abertura, e as colunas começam na primeira linha de grade abaixo dela.

### 4 · Cabeços fora da abertura

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

A página par traz a revista e a edição do frontmatter, a ímpar a matéria, e `pages: 'body'` tira os dois da abertura, que só leva um fólio no pé. O rótulo desce 0,35 mm em relação ao fólio para que os dois corpos dividam a linha de base. O fio abaixo deles mede 197 mm, a largura da mancha.

## A receita completa

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

- Pasta da receita: https://github.com/drnachio/postext/tree/main/cookbook/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 ───────────────────────────────────────────────────────────────────────
```

## Variações

### Deixe os intertítulos ocuparem o vão primeiro

Na ordem padrão, um título ocupa até duas linhas de grade antes de um parágrafo ser alongado, e os intertítulos da página seguinte voltam a se abrir.

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

### Justifique o texto

Um parágrafo justificado já podia ocupar uma linha a mais sem o ajuste, dentro de `maxWordSpacing`. Com `fillShortColumns` ele também pode ocupar uma linha a menos, e nunca é composto mais folgado que sua linha mais folgada.

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

## Erros comuns

- O ajuste fecha um vão de uma linha. Uma coluna à qual faltam duas, por exemplo uma que termina depois do espaço entre parágrafos com lugar para uma só linha do seguinte, pode ficar curta: ajuste o texto com uma frase.
- Um parágrafo cuja nova composição terminaria em uma linha curta demais não é usado, então uma coluna pode ficar curta quando só tem parágrafos breves.

## Créditos

- Receita: Ignacio Ferro ([@drnachio](https://github.com/drnachio))
- Imagens: A lit Fresnel lens in a lighthouse lantern at dusk: Generated With Diffusion Models, original
- Tipos: Public Sans (OFL-1.1), Libre Caslon Display (OFL-1.1), Libre Caslon Text (OFL-1.1)
- Código: MIT · Conteúdo de exemplo: CC-BY-4.0

## Relacionadas

- [Nº 116 · Fotos que crescem até encher uma coluna curta](https://postext.dev/pt/cookbook/photos-fill-short-columns.md): Cada foto marca uma área segura; o equilíbrio das colunas pode cortar fora dela para alongar a foto, e a coluna que tinha ficado curta termina rente. · Nível 2 (Intermediário) · Revistas e fanzines
- [Nº 037 · Relatório anual com colunas alinhadas no pé](https://postext.dev/pt/cookbook/annual-report-flush-columns.md): O relatório anual de uma cooperativa de energia em duas colunas que terminam na mesma linha, com um boxe de números na largura e a última página nivelada. · Nível 3 (Avançado) · Relatórios
- [Nº 004 · Reportagem de revista: da foto de abertura ao sinal final](https://postext.dev/pt/cookbook/magazine-feature-opener.md): Uma abertura advancedDesign põe uma foto sangrada e tira do título chapéu, título, olho e assinatura; seguem dois boxes flutuantes e um chip de fecho. · Nível 3 (Avançado) · Revistas e fanzines
