# ウィドウの規則で短くなった段を埋める

> 左寄せの雑誌の段をグリッドの最終行で終わらせます。fillShortColumns は段末に空きを残す代わりに、短い段の段落を 1 行長く、または短く組み直します。

- HTML版: https://postext.dev/ja/cookbook/filling-short-columns
- レシピ No. 177 · ページとグリッド · 難易度 2 (中級) · 出力: Canvas
- ジャンル: 雑誌とジン
- 必要なもの postext ≥ 1.65.0 · テスト環境 1.65.0 ／テスト日 2026-10-31
- ページ: [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)
- Sandboxで開く: https://postext.dev/ja/sandbox#recipe=filling-short-columns&lang=en (.postext: https://postext.dev/cookbook/filling-short-columns/en/filling-short-columns.postext)
- 最終更新: 2026-10-31
- 他の言語: [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), [pt](https://postext.dev/pt/cookbook/filling-short-columns.md), [zh](https://postext.dev/zh/cookbook/filling-short-columns.md), [ar](https://postext.dev/ar/cookbook/filling-short-columns.md)

## かんたんな説明

段落をそこで分けられないために段がページ下端の 1 行手前で止まったとき、この設定はその段のどれかの段落を組み直して、段が下端まで届くようにします。

## できあがり

海辺の雑誌『Littoral』の 3 ページ。フレネルレンズについての長編記事で、灯室で光るレンズを描いた絵から始まります。本文は Public Sans の左寄せ 2 段組みで、段落のあいだにグリッド 1 行分の空きを入れ、ウィドウとオーファンの規則で、段落を分けるときは前後それぞれに少なくとも 2 行を残します。ペンはこの記事を 2 回組みます。設定なしでは、[扉ページ](https://postext.dev/cookbook/filling-short-columns/en/p01.webp?v=76529999)の左段が右段より 1 行早く終わります。次の段落を段末に 1 行だけ置くことができないからです。設定ありでは、[同じページ](https://postext.dev/cookbook/filling-short-columns/en/p04.webp?v=76529999)で最初の段落が 1 行増え、2 つの段がグリッドの最終行で終わります。次のページでは、段末そろえが 2 つの小見出しの上に足していた余分な空きも、この設定が引き取ります。

**このレシピが答える問い:**

- 左寄せの段が下端の 1 行手前で終わるのはなぜ？ウィドウとオーファンの規則を守ったまま埋めるには？
- 段の下端をそろえ、最終ページの段の長さを均等にするには（垂直方向の両端そろえ）？
- ウィドウ、オーファン、最終行の一語（ラント）を避け、見出しを本文と離さないようにするには？

## 手短な答え

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

## 材料

**学べること**

- [短い段を埋める](https://postext.dev/ja/docs/configuration-page-flow.md#段末そろえ): ウィドウとオーファンの規則で段が下端より 1〜2 行手前で終わるとき、段末そろえがその空きを段の見出し・リスト・段落に振り分け、下端にそろえます（headings.balancing.fillShortColumns）。

**ほかに使うもの**

- [段末そろえ](https://postext.dev/ja/docs/configuration-page-flow.md#段末そろえ)
- [ウィドウ、オーファン、ラント](https://postext.dev/ja/docs/configuration-page-flow.md#オーファンウィドウラント分割禁止の規則)
- [最適な改行（Knuth–Plass）](https://postext.dev/ja/docs/justification.md#knuth-plass段落全体を見渡す)
- [デザインした章扉](https://postext.dev/ja/docs/configuration-headings.md#幅と詳細デザイン)
- [ページデザインの画像](https://postext.dev/ja/docs/configuration-headers-footers.md#画像要素)
- [見出しの属性](https://postext.dev/ja/docs/document-format.md#見出しの属性)
- [柱とノンブル](https://postext.dev/ja/docs/configuration-headers-footers.md#柱とノンブル)
- [ページの役割ごとの柱](https://postext.dev/ja/docs/configuration-headers-footers.md#テキスト要素)
- [セマンティックカラーパレット](https://postext.dev/ja/docs/configuration-fonts-colors-viewers.md#カラーパレット)
- [段落スタイル](https://postext.dev/ja/docs/configuration-styles.md#段落スタイル)
- [段抜きの章見出し帯](https://postext.dev/ja/docs/configuration-headings.md#幅と詳細デザイン)
- [リソースとしての図と表](https://postext.dev/ja/docs/document-format-resources.md#リソース)

**設定の一覧**

- [`bodyText`](https://postext.dev/ja/docs/configuration-text.md#本文), [`colorPalette`](https://postext.dev/ja/docs/configuration-fonts-colors-viewers.md#カラーパレット), [`footer`](https://postext.dev/ja/docs/configuration-headers-footers.md#柱とノンブル), [`header`](https://postext.dev/ja/docs/configuration-headers-footers.md#柱とノンブル), [`headings`](https://postext.dev/ja/docs/configuration-headings.md#見出し), [`layout`](https://postext.dev/ja/docs/configuration-page-layout.md#レイアウト), [`locale`](https://postext.dev/ja/docs/configuration-language.md#ハイフネーション), [`page`](https://postext.dev/ja/docs/configuration-page-layout.md#ページ), [`paragraphStyles`](https://postext.dev/ja/docs/configuration-styles.md#段落スタイル)

**API**

- [`buildDocumentWithFonts`](https://postext.dev/ja/docs/configuration-programmatic-usage.md#レイアウトの前にフォントを読み込む), [`registerResourceImage`](https://postext.dev/ja/docs/architecture.md#apiの概要), [`renderPageToCanvas`](https://postext.dev/ja/docs/configuration-programmatic-usage.md#ページをビットマップに描画する)

**書体**

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

## 作り方

### 1 · 左寄せの段に専用の調整手段を与える

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

段末そろえは、見出しの上、リストの後、囲みや図の下にグリッド行を足し、最後の手段として両端そろえの段落を 1 行ゆるく組んで短い段を埋めます。左寄せの本文の段には、小見出しのあいだにそのどれもないため、ウィドウとオーファンの規則が残した 1 行は空いたままになります。`fillShortColumns` は、その段の左寄せの段落を自分の語間のまま 1 行長く組めるようにします。どの段落にもできないときは、1 つの段落を 1 行短く組み、分割点の後の段落を繰り上げてその行を埋めます。エンジンが変更を残すのは短い段が減るときだけなので、オンにしてページが悪くなることはありません。`maxLinesBeforeLoose: 0` は見出しより先に段落を試すため、どの小見出しも上の空きはグリッド 1 行分のままです。

### 2 · 2 回組んで比べる

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

2 つの文書は同じ設定と同じ Markdown から組みます。2 回目は設定をそのまま渡し、1 回目は設定をオフにしたコピーを渡すので、Sandbox で開くバンドルには `fillShortColumns: true` が入ります。デスクの上の 2 つのボタンで組版を切り替えます。下端まで埋めるのは、本文が次へ続くページだけです。3 ページ目は記事の終わりで、どちらの組版でも段は同じ高さでそろえて切られます。

### 3 · 絵で始める

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

絵は塗り足しに固定した画像要素で、高さ 150 mm あり、3 辺で仕上がり線まで届きます。フロートではこうはなりません。リード上の小見出し、見出し、リード、署名は `#` 行の属性から取ります。署名の下には扉の高さに数えられる 6 mm のパディングがあり、段はその下の最初のグリッド行から始まります。

### 4 · 扉ページには柱を出さない

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

偶数ページの柱は frontmatter から誌名と号を、奇数ページは記事名を出し、`pages: 'body'` で扉ページからは外します。扉ページは下端のノンブルだけです。ラベルはノンブルより 0.35 mm 下げ、2 つのサイズでベースラインを共有させます。その下の罫は版面の幅と同じ 197 mm です。

## レシピの全体

レシピのフォルダーから合成した1つのファイルで、サンプルのテキストとレシピ集の共通キットを埋め込んであり、自分でページを組み立てます。動かすには、空のページの`<script type="module">`に入れるか、新しいCodePenのpenのJSパネルに貼り付けます（モジュールとして）。esm.shからpostextを読み込むので、インストールもビルドも要りません。

- レシピのフォルダー: 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 ───────────────────────────────────────────────────────────────────────
```

## アレンジ

### 小見出しに先に空きを取らせる

既定の順序では、段落を長く組む前に見出しがグリッド行を 2 行まで取り、次のページの小見出しの上がまた広がります。

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

### 本文を両端そろえにする

両端そろえの段落は、設定なしでも `maxWordSpacing` の範囲で 1 行ゆるく組めました。`fillShortColumns` があると 1 行詰めて組むこともでき、その段落でいちばんゆるい行よりゆるく組まれることはありません。

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

## よくあるつまずき

- この設定が埋めるのは 1 行分の空きです。2 行足りない段、たとえば段落間の空きの後に次の段落の 1 行分しか場所がない段は、短いまま残ることがあります。文を 1 つ足すか削って調整します。
- 組み直すと最終行が短くなりすぎる段落は使われません。短い段落しかない段は短いまま残ることがあります。

## クレジット

- レシピ: Ignacio Ferro ([@drnachio](https://github.com/drnachio))
- 画像: A lit Fresnel lens in a lighthouse lantern at dusk: Generated With Diffusion Models, オリジナル
- 書体: Public Sans (OFL-1.1), Libre Caslon Display (OFL-1.1), Libre Caslon Text (OFL-1.1)
- コード: MIT · サンプルの内容: CC-BY-4.0

## 関連レシピ

- [No. 116 · 短い段を埋めるように伸びる写真](https://postext.dev/ja/cookbook/photos-fill-short-columns.md): 写真ごとにセーフエリアを指定すると、段末そろえはその外側を切り落として写真を高く組めます。見出しやフロートのせいで短くなった段も、グリッドの最終行で終わります。 · 難易度 2 (中級) · 雑誌とジン
- [No. 037 · 段末のそろった年次報告書](https://postext.dev/ja/cookbook/annual-report-flush-columns.md): エネルギー協同組合の年次報告書。両端そろえの2段組みが同じグリッド線で終わり、ページ幅の主要数値の囲みが入り、最終ページも段末がそろいます。 · 難易度 3 (上級) · 報告書
- [No. 004 · 雑誌の特集：写真の扉からエンドマークまで](https://postext.dev/ja/cookbook/magazine-feature-opener.md): 1つのadvancedDesignの扉が、裁ち落としの写真と、見出しのキッカー、見出し、リード、署名を組みます。続いて2つのフロートの囲みとチップのエンドマーク。 · 難易度 3 (上級) · 雑誌とジン
