# Final de tesi: apèndix, glossari i índex

> El final d'una tesi en blanc i negre: apèndix amb lletra, glossari a dues columnes, bibliografia APA i índex tret de marques en el text.

- Versió HTML: https://postext.dev/ca/cookbook/thesis-back-matter
- Recepta Núm. 031 · Estructura del llibre · Nivell 3 (Avançat) · Sortides: Canvas, PDF
- Gèneres: Articles i treballs acadèmics
- Requereix postext ≥ 1.7.0, postext-pdf ≥ 1.7.0 · provada amb 1.7.0, postext-pdf 1.7.0 el 2026-09-28
- Pàgines: [171](https://postext.dev/cookbook/thesis-back-matter/en/p01.webp?v=ca82c0ba), [172](https://postext.dev/cookbook/thesis-back-matter/en/p02.webp?v=ca82c0ba), [173](https://postext.dev/cookbook/thesis-back-matter/en/p03.webp?v=ca82c0ba), [174](https://postext.dev/cookbook/thesis-back-matter/en/p04.webp?v=ca82c0ba), [175](https://postext.dev/cookbook/thesis-back-matter/en/p05.webp?v=ca82c0ba), [176](https://postext.dev/cookbook/thesis-back-matter/en/p06.webp?v=ca82c0ba), [177](https://postext.dev/cookbook/thesis-back-matter/en/p07.webp?v=ca82c0ba)
- PDF: https://postext.dev/cookbook/thesis-back-matter/en/thesis-back-matter.pdf?v=ca82c0ba
- Obre al Sandbox: https://postext.dev/ca/sandbox#recipe=thesis-back-matter&lang=en (.postext: https://postext.dev/cookbook/thesis-back-matter/en/thesis-back-matter.postext)
- Última actualització: 2026-09-28
- Altres idiomes: [en](https://postext.dev/en/cookbook/thesis-back-matter.md), [es](https://postext.dev/es/cookbook/thesis-back-matter.md), [zh](https://postext.dev/zh/cookbook/thesis-back-matter.md), [ar](https://postext.dev/ar/cookbook/thesis-back-matter.md)

## En poques paraules

Les últimes pàgines d'una tesi doctoral, del capítol final a l'índex. Postext crea l'índex a partir de paraules marcades en el text i hi posa els números de pàgina.

## Què compondràs

Les set últimes pàgines d'una tesi doctoral sobre la lectura en pantalla i en paper, en format B5 i en blanc i negre. El capítol 6, l'apèndix A, el glossari, la bibliografia i l'índex alfabètic obren sota la mateixa banda negra de 62 mm, amb el títol calat en blanc. El capítol porta el numeral a la banda; l'apèndix, la lletra; i les seccions finals, l'avanttítol BACK MATTER i una nota breu en cursiva. El capítol va a una columna justificada. El glossari i l'índex passen a dues columnes en bandera i la bibliografia torna a una, amb les línies de continuació de cada entrada sagnades. L'índex surt de marques en el text: el motor ordena els termes sota la seva lletra, cerca la pàgina de cada marca i agrupa les seguides en trams com 171–73. Els números en negreta remeten a les definicions del glossari.

**Aquesta recepta respon a:**

- Com componc el glossari, la bibliografia i l'índex d'una tesi, amb sagnat francès i cos menor?
- Com faig un índex analític els números de pàgina del qual canviïn sols quan el text es mou?
- Com evito que els títols, les negretes i les vinyetes surtin en blau?
- Com numero els títols (1, 1.1, 1.1.1) i dono a cada nivell un estil diferent?
- Com poso capçaleres: el títol del llibre a la pàgina esquerra, el del capítol a la dreta i el foli per fora?
- Com forço un salt de pàgina o de columna, i faig que cada capítol comenci en pàgina senar?

## La resposta curta

```js
// script.js, línies 27–50
// '# Glossary {style="glossary"}' in the Markdown picks a style. Each style starts a page
// of either parity, the appendix a recto (a style that sets no break inherits its level's
// 'odd': gotcha style-inherits-break), stays out of the chapter count (numbered: false, so
// its band has no numeral) and brings its own running heads; the glossary and the index
// set their pages in two columns until the next '#'. config() takes both lists below.
const twoColumns = { layoutType: 'double', gutterWidth: mm(6) };
const backMatter = (id, extra) => ({ id, numbered: false, breakBefore: { enabled: true,
  parity: 'any' }, advancedDesign: opener('Back matter'), header: sectionHeads, ...extra });
const headingStyles = () => [
  backMatter('appendix', { breakBefore: { enabled: true, parity: 'odd' }, // {letter="A"}
    header: appendixHeads, advancedDesign: opener('Appendix', '{attr.letter}') }),
  backMatter('glossary', { layout: twoColumns }),
  backMatter('references'),
  backMatter('index', { layout: twoColumns }),
];
// One paragraph per entry, in :::paragraphs{style="…"}: the turnover lines hang, so the
// first word of every entry stands clear at the left. Ragged, as APA asks of references,
// and so never hyphenated (gotcha: ragged-no-hyphenation). The index has its own settings.
const entries = (id, size, lead, hang, extra) => ({ id, fontSize: pt(size),
  lineHeight: pt(lead), textAlign: 'left', hangingIndent: em(hang), ...extra });
const paragraphStyles = () => [
  entries('term', 9.3, 12.4, 1), // the glossary: a bold term, then its definition
  entries('reference', 9.3, 12.4, 1.5, { spaceBetween: pt(2.4) }),
];
```

## Ingredients

**Ensenya**

- [Geometria per secció](https://postext.dev/ca/docs/configuration.md#estils-dencapçalament): Un estil de títol que canvia marges i columnes a la seva secció, com un pròleg a una columna en un llibre a dues.
- [Bibliografies i glossaris](https://postext.dev/ca/docs/configuration.md#estils-de-paràgraf): Llistes de referències i glossaris en cos menor amb sagnat francès, un paràgraf per entrada.
- [Capítols sense número](https://postext.dev/ca/docs/configuration.md#estils-dencapçalament): Un títol de pròleg, apèndix o colofó que no porta número ni altera el recompte de capítols.

**També fa servir**

- [Índex analític](https://postext.dev/ca/docs/document-format.md#índex-analític)
- [Capçaleres per secció](https://postext.dev/ca/docs/configuration.md#estils-dencapçalament)
- [Estils de títol](https://postext.dev/ca/docs/configuration.md#estils-dencapçalament)
- [Títols numerats](https://postext.dev/ca/docs/configuration.md#configuració-per-nivell)
- [Capítols que obren en pàgina senar](https://postext.dev/ca/docs/configuration.md#saltar-abans)
- [Banda de capítol d'amplada completa](https://postext.dev/ca/docs/configuration.md#span-i-disseny-avançat)
- [Obertures dissenyades](https://postext.dev/ca/docs/configuration.md#span-i-disseny-avançat)
- [Atributs de títol](https://postext.dev/ca/docs/document-format.md#atributs-dencapçalament)
- [Capçaleres segons el tipus de pàgina](https://postext.dev/ca/docs/configuration.md#elements-de-text)
- [Estils de paràgraf](https://postext.dev/ca/docs/configuration.md#estils-de-paràgraf)
- [Tipus de recurs propis](https://postext.dev/ca/docs/configuration.md#tipus-de-recurs)
- [Cites que col·loquen les figures](https://postext.dev/ca/docs/document-format.md#referència-en-línia-la-forma-principal)
- [Taules a partir de dades](https://postext.dev/ca/docs/document-format.md#inserció-en-bloc-opcional-collocació-en-línia-explícita)
- [Estil dels peus](https://postext.dev/ca/docs/configuration.md#estil-dels-peus-de-recurs)
- [Paleta de color semàntica](https://postext.dev/ca/docs/configuration.md#paleta-de-colors)
- [Negreta, cursiva i els seus colors](https://postext.dev/ca/docs/configuration.md#text-de-cos)
- [Exportació a PDF](https://postext.dev/ca/docs/configuration.md#generació-de-pdf)
- [Requadres](https://postext.dev/ca/docs/configuration.md#estils-davís)
- citations
- [Equilibri de columnes](https://postext.dev/ca/docs/configuration.md#equilibratge-de-columnes)
- [Figura i Taula en la teva llengua](https://postext.dev/ca/docs/configuration.md#tipus-de-recurs)
- [Sortir de la retícula a propòsit](https://postext.dev/ca/docs/architecture.md#elements-que-trenquen-la-retícula)
- [Fonts incrustades al PDF](https://postext.dev/ca/docs/configuration.md#per-què-un-proveïdor-de-fonts)

**La configuració d'un cop d'ull**

- [`bodyText`](https://postext.dev/ca/docs/configuration.md#text-de-cos), [`calloutStyles`](https://postext.dev/ca/docs/configuration.md#estils-davís), [`captionStyle`](https://postext.dev/ca/docs/configuration.md#estil-dels-peus-de-recurs), [`colorPalette`](https://postext.dev/ca/docs/configuration.md#paleta-de-colors), [`footer`](https://postext.dev/ca/docs/configuration.md#capçaleres-i-peus), [`header`](https://postext.dev/ca/docs/configuration.md#capçaleres-i-peus), [`headingStyles`](https://postext.dev/ca/docs/configuration.md#estils-dencapçalament), [`headings`](https://postext.dev/ca/docs/configuration.md#encapçalaments), [`index`](https://postext.dev/ca/docs/configuration.md#índex-analític), [`layout`](https://postext.dev/ca/docs/configuration.md#disposició), [`orderedLists`](https://postext.dev/ca/docs/configuration.md#llistes-ordenades), [`page`](https://postext.dev/ca/docs/configuration.md#pàgina), [`paragraphStyles`](https://postext.dev/ca/docs/configuration.md#estils-de-paràgraf), [`resourceTypes`](https://postext.dev/ca/docs/configuration.md#tipus-de-recurs), [`tableStyle`](https://postext.dev/ca/docs/configuration.md#estil-de-taules), [`unorderedLists`](https://postext.dev/ca/docs/configuration.md#llistes-no-ordenades)

**API**

- [`buildDocument`](https://postext.dev/ca/docs/configuration.md#construir-un-document), [`clearMeasurementCache`](https://postext.dev/ca/docs/configuration.md#memòria-cau-de-mesures), [`decompressWoff2`](https://postext.dev/ca/docs/configuration.md#proveïdor-de-fonts-al-navegador-fontsource--woff2), [`defaultResourceTypes`](https://postext.dev/ca/docs/configuration.md#tipus-de-recurs), [`renderPageToCanvas`](https://postext.dev/ca/docs/configuration.md#renderitzar-una-pàgina-a-un-bitmap), [`renderToPdf`](https://postext.dev/ca/docs/configuration.md#generació-de-pdf)

**Tipus de lletra**

- Libertinus Serif (OFL-1.1), Libertinus Serif Display (OFL-1.1), Libertinus Sans (OFL-1.1)

## Elaboració

### 1 · Un estil de títol per a cada part del final

El codi és [la resposta curta](#la-resposta-curta) de més amunt. `# Glossary {style="glossary" note="…"}` obre una secció que arriba fins al títol de nivell 1 següent, i les seves pàgines prenen la disposició, les capçaleres i l'obertura de l'estil ([estils d'encapçalament](/ca/docs/configuration#estils-dencapçalament)), de manera que el glossari i l'índex passen a dues columnes i la bibliografia, l'estil de la qual no fixa disposició, torna a la columna única del document. `numbered: false` deixa aquests títols fora del recompte de capítols (sense aquesta opció, a la banda del glossari sortiria un 8), i cada estil declara el seu salt de pàgina, perquè el que no el declara hereta el `'odd'` del capítol i deixa pàgines parelles en blanc. Les entrades del glossari i de la bibliografia són paràgrafs d'un bloc `:::paragraphs{style="…"}` en cos 9,3 sobre 12,4 pt, enfront de l'11 sobre 14,6 del text, amb les línies de continuació sagnades 1 em al glossari i 1,5 em a la bibliografia ([estils de paràgraf](/ca/docs/configuration#estils-de-paràgraf)).

### 2 · Una sola banda per a totes les obertures

```js
// script.js, línies 54–78
const SINK = 8; // lines reserved, 41.2 mm: 3.2 mm more than the band, and text on the grid
// Design text sets each baseline 0.8 of its line under the line's top, and a line is 1.2 × the
// size unless lineHeight says otherwise. In mm, a line's part above its baseline and below it:
const PT = 25.4 / 72;
const above = (size, lineHeight = 1.2) => 0.8 * size * lineHeight * PT;
const below = (size, lineHeight = 1.2) => 0.2 * size * lineHeight * PT;
const KICKER = 4.3, TITLE = BAND - TOP - 9.5; // mm under the text block's top: two baselines
// A bottom-aligned box that ends below() under a baseline sets its last line on it. The
// numeral's line is 0.72 of its size: a line taller than its box would hang from its top.
const text = (id, content, family, size, lineHeight, baseline, edge, w, extra) => ({
  kind: 'text', id, content, fontFamily: family, fontSize: pt(size), lineHeight,
  color: col('paper'), overflow: 'wrap', align: edge.endsWith('right') ? 'right' : 'left',
  verticalAlign: 'bottom', ...extra, placement: { anchor: { to: 'container', edge },
    size: { width: mm(w), height: mm(baseline + below(size, lineHeight)) } } });
// The mark: '{number}', empty on an unnumbered heading, or the appendix's '{attr.letter}'.
const opener = (label, mark = '{number}') => ({ enabled: true, minHeight: pt(SINK * LEAD),
  slot: { elements: [
    { kind: 'box', id: 'band', style: { backgroundColor: col('band') }, placement: {
      anchor: { to: 'page', edge: 'top-left' }, size: { width: 'fill', height: mm(BAND) } } },
    text('label', label, LABEL, 8, 1.2, KICKER, 'top-left', 80,
      { fontWeight: 700, letterSpacing: pt(1.6), textTransform: 'uppercase' }),
    text('title', '{titleText}', DISPLAY, 34, 1.04, TITLE, 'top-left', 84),
    text('mark', mark, DISPLAY, 118, 0.72, TITLE, 'top-right', 34),
    text('note', '{attr.note}', TEXT, 8.6, 1.3, TITLE, 'top-right', 44, { italic: true }),
  ] } });
```

La marca és `{number}`, que queda buit en un títol sense numerar, així que el mateix disseny serveix per al capítol, per a l'apèndix, que passa `{attr.letter}` com a marca, i per a les seccions finals, que imprimeixen el seu `{attr.note}` on aniria el numeral. `minHeight` reserva vuit línies de 14,6 pt, 41,2 mm des del capdamunt de la caixa de text, que deixen 3,2 mm lliures sota la banda i mantenen el text a la seva retícula. En els textos de disseny, la línia de base cau a 0,8 de l'alçada de línia, de manera que una caixa alineada a baix el peu de la qual queda a `below()` d'una línia de base hi assenta la seva última línia. Així, el títol, el numeral de 118 pt i l'última línia de la nota es recolzen a la línia de base del títol, a 52,5 mm del tall. La línia del numeral fa 0,72 vegades el seu cos perquè a la 1.4.1 una línia més alta que la seva caixa ignora `verticalAlign: 'bottom'`.

### 3 · La lletra de l'apèndix es posa a mà

```js
// script.js, línies 108–112
// In 1.4.1 a heading style cannot change the numbering: the appendix is unnumbered, and its
// letter feeds the band (see answer), the running head and a table type that counts A.1.
const appendixHeads = heads('Appendix {attr.letter}. {chapterTitle}');
const appendixTables = { ...defaultResourceTypes(LANG).find((type) => type.id === 'table'),
  id: 'table-a', numberingTemplate: 'A.{n}' }; // a copy of 'table'
```

A postext 1.4.1 un estil de títol no pot canviar la numeració del seu nivell, així que l'apèndix va sense numerar i la seva lletra s'escriu com a atribut: `# Interview guide {style="appendix" letter="A"}`. La banda i la capçalera de l'apèndix la prenen d'aquí, i un tipus de recurs propi numera les seves taules A.1, A.2, etc.; amb el tipus per defecte, la taula seria la 6.2, perquè un títol sense numerar deixa el comptador de capítols en 6. Els títols del capítol es numeren amb les plantilles de cada nivell: `'{1}.{2}'` imprimeix 6.1, i `continuation.headings.h1: 5` fa d'aquest el sisè capítol.

### 4 · Les capçaleres, cap al marge exterior

```js
// script.js, línies 82–104
const HEAD = 17.5, GAP = 9; // mm: the heads' baseline under the trim; the folio to the words
// Each text is placed by its top, above() over HEAD: the folio and the capitals share a baseline.
const head = (id, content, parity, edge, x, size = 7.5, extra) => ({ kind: 'text', id, content,
  parity, pages: 'body', fontFamily: LABEL, fontSize: pt(size), fontWeight: 700,
  letterSpacing: pt(1.3), textTransform: 'uppercase', color: col('ink'), ...extra, placement: {
    anchor: { to: 'page', edge }, offset: { x: mm(x), y: mm(HEAD - above(size)) } } });
const folio = { fontFamily: TEXT, fontWeight: 400, letterSpacing: pt(0) };
const heads = (recto) => ({ elements: [
  head('verso-folio', '{pageNumber}', 'even', 'top-left', OUTER, 9.5, folio),
  head('verso', '{title}', 'even', 'top-left', OUTER + GAP),
  head('recto', recto, 'odd', 'top-right', -(OUTER + GAP)),
  head('recto-folio', '{pageNumber}', 'odd', 'top-right', -OUTER, 9.5, folio),
  // The header's container spans the text block, so one rule serves both pages.
  { kind: 'rule', id: 'hairline', pages: 'body', direction: 'horizontal', thickness: pt(0.5),
    color: col('rule'), placement: { anchor: { to: 'container', edge: 'top-left' },
      offset: { y: mm(HEAD + 2) }, size: { width: 'fill' } } },
] });
const chapterHeads = heads('Chapter {chapterNumber}. {chapterTitle}');
const sectionHeads = heads('{chapterTitle}'); // 'Glossary', 'References', 'Index'
// Openers drop the folio to the foot, centred under the text block, its baseline 12 mm below.
const footer = { elements: [{ kind: 'text', id: 'drop-folio', content: '{pageNumber}',
  pages: 'opener', ...folio, fontSize: pt(9.5), color: col('ink'), align: 'center',
  placement: { anchor: { to: 'container', edge: 'top' }, offset: { y: mm(12 - above(9.5)) } } }] };
```

Els quatre textos s'ancoren a la pàgina i es filtren per `parity`, cosa que manté el foli a la vora exterior de les dues pàgines; cadascun es col·loca per la vora superior, a `above()` per sobre d'una línia de base situada a 17,5 mm del tall, així que el foli de 9,5 pt i les majúscules de 7,5 pt queden a la mateixa línia. La parella porta el títol de la tesi (`{title}`, del frontmatter) i la senar el de la secció (`{chapterTitle}`), com INDEX a la [pàgina 177](https://postext.dev/cookbook/thesis-back-matter/en/p07.webp?v=ca82c0ba). `pages: 'body'` les deixa fora de les obertures, on el foli passa al peu. La senar del capítol diria *Chapter 6. Conclusion* i la de l'apèndix *Appendix A. Interview guide*, però en aquesta mostra només l'índex continua en una pàgina senar després de la seva obertura.

### 5 · Marca els termes on el text en parla

```js
// script.js, línies 116–122
// Glossary definitions carry 'main' (bold numbers); runs of pages join as 171–72 (Chicago).
// The heads stand on the entries' 11.6 pt pitch, 7.5 pt of space above them: 19 pt from the
// last entry of a letter to the next letter's baseline, 11.6 pt from a letter to its first entry.
const index = { fontFamily: TEXT, fontSize: pt(9), lineHeight: pt(11.6), color: col('ink'),
  indent: em(1), turnoverIndent: em(2), rangeFormat: 'chicago',
  groups: { fontFamily: DISPLAY, fontSize: pt(13), fontWeight: 400, color: col('ink'),
    marginTop: pt(7.5) } };
```

`:index[tablet]` imprimeix la paraula i la registra tal com és; `Rayner:index{term="Rayner, Keith"}` no imprimeix res i registra la pàgina de *Rayner* sota el nom complet; `term="interviews!timing of"` crea una subentrada. Els termes del glossari porten `main`, així que la seva pàgina surt en negreta, com avisa la nota de la banda de l'índex. `:::index`, sota `# Index {style="index"}`, imprimeix les entrades a les dues columnes de l'estil, i `buildDocument` torna a compondre el document fins que els números de pàgina deixen de moure's. Les entrades van en cos 9 sobre 11,6 pt, amb les línies de continuació sagnades 2 em i les subentrades 1 em; les lletres, en 13 pt amb la font dels titulars. Les marques no funcionen a les cel·les de taula, així que les files de la taula 6.1 no hi afegeixen pàgines: *look-backs* remet al paràgraf que les comenta, no a la taula.

### 6 · Tots els valors per defecte, en negre

```js
// script.js, línies 15–19
const palette = { ink: '#000000', band: '#000000', rule: '#000000', paper: '#ffffff' };
const col = (id) => ({ hex: palette[id], model: 'hex', paletteId: id });
// Bold, italic and list markers default to 'main-color': pointed at the ink, they print black.
const colorPalette = Object.entries({ ...palette, 'main-color': palette.ink })
  .map(([id, hex]) => ({ id, name: id, value: { hex, model: 'hex' } }));
```

La negreta, la cursiva i les vinyetes i els números de les llistes segueixen per defecte l'entrada `main-color` de la paleta, així que, si li dones el color de la tinta, surten en negre i no en blau ([paleta de colors](/ca/docs/configuration#paleta-de-colors)). A la 1.4.1 la paleta no arriba a `bodyText.referenceColor`, i per això la configuració el repeteix: sense aquesta línia, *Table 6.1* surt al text en blau #295AA3. `referenceBold: false` posa la referència en rodona, com les cites autor-any que l'envolten.

> Tres coses encara es fan a mà: una llista de taules, les remissions a una pàgina del text (*vegeu p. 172*) i, a la 1.4.1, un comptador amb lletres per als apèndixs. En una tesi sencera composta capítol a capítol amb `buildBundle`, el capítol que conté `:::index` rep les marques de tots els capítols.

## La recepta completa

Un sol fitxer, compost a partir de la carpeta de la recepta amb el text d'exemple i el kit comú del Receptari ja inclosos; construeix la seva pròpia pàgina. Per executar-lo, posa'l en un `<script type="module">` d'una pàgina buida o enganxa'l al tauler JS d'un pen nou de CodePen (com a mòdul). Importa postext des d'esm.sh, així que no cal instal·lar ni compilar res.

- Carpeta de la recepta: https://github.com/drnachio/postext/tree/main/cookbook/thesis-back-matter

### script.js

```js
// ═══ Postext Cookbook · Nº 031 · Thesis back matter: appendix, glossary and index ═══
// https://postext.dev/en/cookbook/thesis-back-matter
// Code: MIT · Text: original (CC BY 4.0) · Pictures: none
// Fonts: Libertinus Serif, Serif Display and Sans (SIL OFL 1.1) · Needs postext ≥ 1.7.0
import {
  buildDocument, renderPageToCanvas, clearMeasurementCache, defaultResourceTypes,
} from 'https://esm.sh/postext';
import { renderToPdf, decompressWoff2 } from 'https://esm.sh/postext-pdf';

const LANG = 'en'; // @lang: the language of the sample document ('en')
const RECIPE = 'thesis-back-matter';

// ─── 1 · Design ─────────────────────────────────────────────────────────────
// #region palette: one ink; every colour is black or white, each under its own name
const palette = { ink: '#000000', band: '#000000', rule: '#000000', paper: '#ffffff' };
const col = (id) => ({ hex: palette[id], model: 'hex', paletteId: id });
// Bold, italic and list markers default to 'main-color': pointed at the ink, they print black.
const colorPalette = Object.entries({ ...palette, 'main-color': palette.ink })
  .map(([id, hex]) => ({ id, name: id, value: { hex, model: 'hex' } }));
// #endregion
const TEXT = 'Libertinus Serif', DISPLAY = 'Libertinus Serif Display', LABEL = 'Libertinus Sans';
const TOP = 24, INNER = 25, OUTER = 31; // mm: a 120 mm measure, about 70 characters at 11 pt
const LEAD = 14.6; // pt: the body's leading, the grid every page is set on
const BAND = 62; // mm from the trim's top: the black band at the head of every opener

// #region answer: back matter as unnumbered heading styles, entries in hanging indents
// '# Glossary {style="glossary"}' in the Markdown picks a style. Each style starts a page
// of either parity, the appendix a recto (a style that sets no break inherits its level's
// 'odd': gotcha style-inherits-break), stays out of the chapter count (numbered: false, so
// its band has no numeral) and brings its own running heads; the glossary and the index
// set their pages in two columns until the next '#'. config() takes both lists below.
const twoColumns = { layoutType: 'double', gutterWidth: mm(6) };
const backMatter = (id, extra) => ({ id, numbered: false, breakBefore: { enabled: true,
  parity: 'any' }, advancedDesign: opener('Back matter'), header: sectionHeads, ...extra });
const headingStyles = () => [
  backMatter('appendix', { breakBefore: { enabled: true, parity: 'odd' }, // {letter="A"}
    header: appendixHeads, advancedDesign: opener('Appendix', '{attr.letter}') }),
  backMatter('glossary', { layout: twoColumns }),
  backMatter('references'),
  backMatter('index', { layout: twoColumns }),
];
// One paragraph per entry, in :::paragraphs{style="…"}: the turnover lines hang, so the
// first word of every entry stands clear at the left. Ragged, as APA asks of references,
// and so never hyphenated (gotcha: ragged-no-hyphenation). The index has its own settings.
const entries = (id, size, lead, hang, extra) => ({ id, fontSize: pt(size),
  lineHeight: pt(lead), textAlign: 'left', hangingIndent: em(hang), ...extra });
const paragraphStyles = () => [
  entries('term', 9.3, 12.4, 1), // the glossary: a bold term, then its definition
  entries('reference', 9.3, 12.4, 1.5, { spaceBetween: pt(2.4) }),
];
// #endregion

// #region opener: a black band across the head of the page, the title reversed out of it
const SINK = 8; // lines reserved, 41.2 mm: 3.2 mm more than the band, and text on the grid
// Design text sets each baseline 0.8 of its line under the line's top, and a line is 1.2 × the
// size unless lineHeight says otherwise. In mm, a line's part above its baseline and below it:
const PT = 25.4 / 72;
const above = (size, lineHeight = 1.2) => 0.8 * size * lineHeight * PT;
const below = (size, lineHeight = 1.2) => 0.2 * size * lineHeight * PT;
const KICKER = 4.3, TITLE = BAND - TOP - 9.5; // mm under the text block's top: two baselines
// A bottom-aligned box that ends below() under a baseline sets its last line on it. The
// numeral's line is 0.72 of its size: a line taller than its box would hang from its top.
const text = (id, content, family, size, lineHeight, baseline, edge, w, extra) => ({
  kind: 'text', id, content, fontFamily: family, fontSize: pt(size), lineHeight,
  color: col('paper'), overflow: 'wrap', align: edge.endsWith('right') ? 'right' : 'left',
  verticalAlign: 'bottom', ...extra, placement: { anchor: { to: 'container', edge },
    size: { width: mm(w), height: mm(baseline + below(size, lineHeight)) } } });
// The mark: '{number}', empty on an unnumbered heading, or the appendix's '{attr.letter}'.
const opener = (label, mark = '{number}') => ({ enabled: true, minHeight: pt(SINK * LEAD),
  slot: { elements: [
    { kind: 'box', id: 'band', style: { backgroundColor: col('band') }, placement: {
      anchor: { to: 'page', edge: 'top-left' }, size: { width: 'fill', height: mm(BAND) } } },
    text('label', label, LABEL, 8, 1.2, KICKER, 'top-left', 80,
      { fontWeight: 700, letterSpacing: pt(1.6), textTransform: 'uppercase' }),
    text('title', '{titleText}', DISPLAY, 34, 1.04, TITLE, 'top-left', 84),
    text('mark', mark, DISPLAY, 118, 0.72, TITLE, 'top-right', 34),
    text('note', '{attr.note}', TEXT, 8.6, 1.3, TITLE, 'top-right', 44, { italic: true }),
  ] } });
// #endregion

// #region running-heads: the thesis on the verso, the section on the recto, a hairline under
const HEAD = 17.5, GAP = 9; // mm: the heads' baseline under the trim; the folio to the words
// Each text is placed by its top, above() over HEAD: the folio and the capitals share a baseline.
const head = (id, content, parity, edge, x, size = 7.5, extra) => ({ kind: 'text', id, content,
  parity, pages: 'body', fontFamily: LABEL, fontSize: pt(size), fontWeight: 700,
  letterSpacing: pt(1.3), textTransform: 'uppercase', color: col('ink'), ...extra, placement: {
    anchor: { to: 'page', edge }, offset: { x: mm(x), y: mm(HEAD - above(size)) } } });
const folio = { fontFamily: TEXT, fontWeight: 400, letterSpacing: pt(0) };
const heads = (recto) => ({ elements: [
  head('verso-folio', '{pageNumber}', 'even', 'top-left', OUTER, 9.5, folio),
  head('verso', '{title}', 'even', 'top-left', OUTER + GAP),
  head('recto', recto, 'odd', 'top-right', -(OUTER + GAP)),
  head('recto-folio', '{pageNumber}', 'odd', 'top-right', -OUTER, 9.5, folio),
  // The header's container spans the text block, so one rule serves both pages.
  { kind: 'rule', id: 'hairline', pages: 'body', direction: 'horizontal', thickness: pt(0.5),
    color: col('rule'), placement: { anchor: { to: 'container', edge: 'top-left' },
      offset: { y: mm(HEAD + 2) }, size: { width: 'fill' } } },
] });
const chapterHeads = heads('Chapter {chapterNumber}. {chapterTitle}');
const sectionHeads = heads('{chapterTitle}'); // 'Glossary', 'References', 'Index'
// Openers drop the folio to the foot, centred under the text block, its baseline 12 mm below.
const footer = { elements: [{ kind: 'text', id: 'drop-folio', content: '{pageNumber}',
  pages: 'opener', ...folio, fontSize: pt(9.5), color: col('ink'), align: 'center',
  placement: { anchor: { to: 'container', edge: 'top' }, offset: { y: mm(12 - above(9.5)) } } }] };
// #endregion

// #region appendix: the letter comes from the heading, '# Interview guide {letter="A"}'
// In 1.4.1 a heading style cannot change the numbering: the appendix is unnumbered, and its
// letter feeds the band (see answer), the running head and a table type that counts A.1.
const appendixHeads = heads('Appendix {attr.letter}. {chapterTitle}');
const appendixTables = { ...defaultResourceTypes(LANG).find((type) => type.id === 'table'),
  id: 'table-a', numberingTemplate: 'A.{n}' }; // a copy of 'table'
// #endregion

// #region index: the pages of the :index marks, sorted under letters in the display face
// Glossary definitions carry 'main' (bold numbers); runs of pages join as 171–72 (Chicago).
// The heads stand on the entries' 11.6 pt pitch, 7.5 pt of space above them: 19 pt from the
// last entry of a letter to the next letter's baseline, 11.6 pt from a letter to its first entry.
const index = { fontFamily: TEXT, fontSize: pt(9), lineHeight: pt(11.6), color: col('ink'),
  indent: em(1), turnoverIndent: em(2), rangeFormat: 'chicago',
  groups: { fontFamily: DISPLAY, fontSize: pt(13), fontWeight: 400, color: col('ink'),
    marginTop: pt(7.5) } };
// #endregion

const config = () => ({ // a factory, never a shared object (gotcha: config-cache-identity)
  colorPalette, header: chapterHeads, footer, layout: { layoutType: 'single' },
  page: { sizePreset: 'custom', width: mm(176), height: mm(250), dpi: 150, // B5
    margins: { top: mm(TOP), bottom: mm(24), left: mm(INNER), right: mm(OUTER), mirror: true } },
  bodyText: { fontFamily: TEXT, fontSize: pt(11), lineHeight: pt(LEAD), color: col('ink'),
    // 'Table 6.1' in roman and in ink, outside the palette's reach (gotcha: palette-skips-designs)
    referenceColor: col('ink'), referenceBold: false,
    firstLineIndent: mm(4.5), indentAfterHeading: false, minWordSpacing: 0.8, maxWordSpacing: 1.8 },
  // Exact heading margins (snapToGrid: false), no lines added above them; the chapter's heads
  // measure whole grid lines.
  headings: { fontFamily: DISPLAY, fontWeight: 400, color: col('ink'), snapToGrid: false,
    balancing: { maxLinesPerHeading: 0 }, levels: [
      // The H1 break restated (gotcha: headings-drop-h1-break). span: 'page' (the styles inherit
      // it) sets the band above the columns: inside a column, its top would be clipped.
      { level: 1, numberingTemplate: '{1}', span: 'page', marginBottom: pt(0),
        advancedDesign: opener('Chapter'), breakBefore: { enabled: true, parity: 'odd' } },
      { level: 2, numberingTemplate: '{1}.{2}', fontSize: pt(14), lineHeight: pt(LEAD),
        marginTop: pt(LEAD * 1.5), marginBottom: pt(LEAD / 2) }, // three lines in all
    ] },
  headingStyles: headingStyles(), paragraphStyles: paragraphStyles(), index,
  orderedLists: { marginTop: pt(LEAD / 2), marginBottom: pt(LEAD / 2) },
  unorderedLists: { bulletChar: '–' },
  resourceTypes: [...defaultResourceTypes(LANG), appendixTables], // tables 6.1… and A.1…
  // Captions in the text face, as APA sets a table's number and title.
  captionStyle: { fontSize: pt(9), position: 'above', gap: pt(4), note: { fontSize: pt(8) } },
  // Rules only and a bold header: filled header cells show seams between the columns.
  tableStyle: { rules: 'horizontal', borderColor: col('rule'), borderWidth: pt(0.5),
    headerBackgroundEnabled: false, headerFontSize: pt(9.5),
    bodyFontSize: pt(9.5), cellPadding: mm(1) },
  calloutStyles: [{ id: 'colophon', span: 'page', marginTop: pt(LEAD), backgroundEnabled: false,
    stripe: { enabled: true, side: 'top', width: pt(0.5), color: col('rule') },
    padding: { top: mm(2.5), right: mm(0), bottom: mm(0), left: mm(0) },
    body: { fontSize: pt(8), lineHeight: pt(10.5), firstLineIndent: pt(0), textAlign: 'left' } }],
});

// ─── 2 · Content ────────────────────────────────────────────────────────────
const markdown = String.raw`---
title: "Reading on Screens and Paper"
subtitle: "A Mixed-Methods Study of Comprehension, Confidence and Navigation"
author: "Ines Varley"
---

# Conclusion

This thesis set out to test whether it matters if a long text:index{term="texts, length of"} is read on paper or on a screen. Chapters 3 to 5 reported a within-subjects:index{term="within-subjects design"} experiment with forty-eight :index[undergraduates] and :index[interviews] with sixteen of them, combined in the :index[convergent design] described by Creswell:index{term="Creswell, John W."} and Plano Clark:index{term="Plano Clark, Vicki L."} (2018). This chapter brings the two strands together and sets out what they mean for :index[teaching] and for the :index[design of reading software].

## What the study found

:ref{id="findings" style="full"} summarises the results.:index{term="comprehension"} On :index[literal]{term="comprehension!literal"} questions, answerable from a single sentence, the medium made no difference. On :index[inferential]{term="comprehension!inferential"} questions, which required connecting ideas across paragraphs, paper readers scored higher. The difference points the same way as the :index[meta-analyses] of Delgado:index{term="Delgado, Pablo"} et al. (2018) and Clinton:index{term="Clinton, Virginia"} (2019), which found the :index[paper advantage] in :index[expository]{term="expository text"} rather than :index[narrative]{term="narrative text"} texts.

Calibration:index{term="calibration"} showed the larger difference. Screen readers:index{term="calibration!on screen"} predicted:index{term="confidence!judgements of"} higher scores than paper readers and obtained lower ones, so the gap between :index[confidence] and :index[accuracy]:index{term="calibration!bias in"} was nearly three times as wide. The result repeats the :index[overconfidence] that Ackerman:index{term="Ackerman, Rakefet"} and Goldsmith:index{term="Goldsmith, Morris"} (2011) found in students who read on screen and set their own study time.:index{term="self-regulated study"} Such readers stop once they judge a text understood, so overconfidence cuts their study short.

Paper readers also turned back:index{term="look-backs"} almost twice as often as screen readers scrolled back:index{term="scrolling"}, most often just before an inferential question.:index{term="comprehension!inferential"} In the interviews:index{term="interviews"}, eleven of the sixteen :index[participants] remembered:index{term="memory"} where on a page:index{term="spatial memory"} an idea had been (“top left, next to the diagram”), and three gave up looking for a passage on the :index[tablet] because “it could have been anywhere”. Liu:index{term="Liu, Ziming"} (2005) described a drift towards :index[browsing] and :index[keyword spotting] on screen; these readers went through the whole text but had fewer :index[landmarks] to return to.

## Implications for teaching and design

For short texts and factual questions, screens serve as well as paper.:index{term="teaching"} For long expository:index{term="expository text"} texts that students must understand:index{term="comprehension"} rather than search, paper remains the safer choice.:index{term="paper advantage"} Where it is not available, students should test their understanding instead of trusting their sense of it: in the :index[pilot sessions], a short :index[self-test] after reading halved the overconfidence:index{term="overconfidence"} on screen.:index{term="calibration!on screen"}

Readers also used the fixed position of text on a page as a map,:index{term="landmarks"} one of the uses of paper that Sellen:index{term="Sellen, Abigail J."} and Harper:index{term="Harper, Richard H. R."} (2002) observed in offices.:index{term="offices, paper in"} Reading applications:index{term="design of reading software"} that keep a stable page and show the reader’s place in the whole text may restore some of that map.

## Limitations and further work

The participants:index{term="participants"} were students at one university:index{term="university, single"} who read English fluently,:index{term="limitations"} and the medium matters more for some readers, texts and tasks than it does for others (Singer:index{term="Singer, Lauren M."} & Alexander:index{term="Alexander, Patricia A."}, 2017). The texts were expository and about 1,800 words long,:index{term="texts, length of"} and the screen condition used a single tablet.:index{term="tablet"} A :index[replication] with a larger sample, several devices and the eye-movement:index{term="eye movements"} recording reviewed by Rayner:index{term="Rayner, Keith"} (1998) would show where on the page the two media part company.

Huey:index{term="Huey, Edmund Burke"} (1908) thought that a complete analysis of what we do when we read would be almost the acme of a psychologist’s achievements. The experiments reported here add a small part to that analysis; the replication proposed above could measure how far readers rely on the position of a passage on the page when they look back.

# Interview guide {style="appendix" letter="A"}

The interviews:index{term="interviews"} took place within a week of each participant’s:index{term="participants"} second session. They were audio-recorded,:index{term="interviews!recording of"} transcribed:index{term="transcription"} in full and analysed thematically:index{term="thematic analysis"} following Braun:index{term="Braun, Virginia"} and Clarke:index{term="Clarke, Victoria"} (2006); :ref{id="session-plan" style="full"} gives their timing:index{term="interviews!timing of"}: a free recall:index{term="recall"} of the two study texts, the questions below and a short debriefing.:index{term="debriefing"} The questions were asked in this order,:index{term="interviews!questions asked"} and a prompt:index{term="interviews!prompts in"} only when the participant had not already covered its point.

1. Tell me about the last long text:index{term="texts, length of"} you read for a course.:index{term="courses, reading for"}
   - Where did you read it, and on paper or on a screen?
2. Which of the texts in this study do you remember:index{term="memory"} best, and why?
3. When you wanted to check an earlier passage, what did you do?:index{term="look-backs"}
   - How did you know where to look?
4. How sure were you of your answers?:index{term="confidence"} What made you more or less sure?
5. Did reading on the tablet:index{term="tablet"} feel different from reading on paper?
6. Some students say they read more carefully on paper. Do you?
7. What would the ideal way to read a long text for study be like?:index{term="design of reading software"}

# Glossary {style="glossary" note="Words in italics are defined under entries of their own."}

:::paragraphs{style="term"}
**calibration**:index{term="calibration" main} The agreement between a reader’s confidence in having understood a text and the accuracy of that understanding, measured here as the difference between predicted and actual scores.

**comprehension, inferential**:index{term="comprehension!inferential" main} Understanding that requires the reader to connect information from different parts of a text or to add knowledge the text does not state.

**comprehension, literal**:index{term="comprehension!literal" main} Understanding of what a single sentence or passage states directly.

**confidence judgement**:index{term="confidence!judgements of" main} A reader’s estimate, made after reading and before seeing the questions, of how many answers will be correct.

**convergent design**:index{term="convergent design" main} A mixed-methods design in which quantitative and qualitative data are collected in the same period, analysed separately and then compared.

**expository text**:index{term="expository text" main} A text written to explain or inform, such as a textbook chapter or a report, as opposed to a narrative text.

**fixation**:index{term="fixation" main} A pause of the eyes, typically about a quarter of a second, during which the reader takes in text; fixations alternate with *saccades*.

**look-back**:index{term="look-backs" main} Any return to an earlier part of a text during reading: turning back a page, scrolling up or following a link to a previous section.

**metacomprehension**:index{term="metacomprehension" main} A reader’s knowledge and monitoring of their own understanding of a text; *calibration* is one of its measures.

**navigation**:index{term="navigation" main} The movements a reader makes through a text as a whole, as distinct from the movements of the eyes along a line.

**overconfidence**:index{term="overconfidence" main} Positive *calibration* bias: predicting a higher score than the one actually obtained.

**saccade**:index{term="saccade" main} A rapid movement of the eyes from one *fixation* to the next, during which little or no text is taken in.

**screen inferiority effect**:index{term="screen inferiority effect" main} The finding that comprehension of the same text is lower on screen than on paper, most consistently for *expository texts* read under time pressure.

**self-regulated study**:index{term="self-regulated study" main} Reading in which the reader, not the experimenter, decides how long to spend on a text.

**spatial memory for text**:index{term="spatial memory" main} Memory of where on a page or in a document a piece of information appeared, used as a cue for *look-backs*.

**thematic analysis**:index{term="thematic analysis" main} A method for identifying, analysing and reporting patterns of meaning across qualitative data such as interview transcripts.

**within-subjects design**:index{term="within-subjects design" main} An experimental design in which every participant takes part in every condition, here reading on both paper and screen.
:::

# References {style="references" note="Every work cited in the thesis, set in APA style (7th edition)."}

:::paragraphs{style="reference"}
Ackerman, R., & Goldsmith, M. (2011). Metacognitive regulation of text learning: On screen versus on paper. *Journal of Experimental Psychology: Applied, 17*(1), 18–32.

Baron, N. S. (2015). *Words onscreen: The fate of reading in a digital world.* Oxford University Press.

Braun, V., & Clarke, V. (2006). Using thematic analysis in psychology. *Qualitative Research in Psychology, 3*(2), 77–101.

Clinton, V. (2019). Reading from paper compared to screens: A systematic review and meta-analysis. *Journal of Research in Reading, 42*(2), 288–325.

Creswell, J. W., & Plano Clark, V. L. (2018). *Designing and conducting mixed methods research* (3rd ed.). SAGE.

Delgado, P., Vargas, C., Ackerman, R., & Salmerón, L. (2018). Don’t throw away your printed books: A meta-analysis on the effects of reading media on reading comprehension. *Educational Research Review, 25*, 23–38.

Dillon, A. (1992). Reading from paper versus screens: A critical review of the empirical literature. *Ergonomics, 35*(10), 1297–1326.

Huey, E. B. (1908). *The psychology and pedagogy of reading.* Macmillan.

Liu, Z. (2005). Reading behavior in the digital environment: Changes in reading behavior over the past ten years. *Journal of Documentation, 61*(6), 700–712.

Mangen, A., Walgermo, B. R., & Brønnick, K. (2013). Reading linear texts on paper versus computer screen: Effects on reading comprehension. *International Journal of Educational Research, 58*, 61–68.

Noyes, J. M., & Garland, K. J. (2008). Computer- vs. paper-based tasks: Are they equivalent? *Ergonomics, 51*(9), 1352–1375.

Paterson, D. G., & Tinker, M. A. (1940). *How to make type readable.* Harper & Brothers.

Rayner, K. (1998). Eye movements in reading and information processing: 20 years of research. *Psychological Bulletin, 124*(3), 372–422.

Sellen, A. J., & Harper, R. H. R. (2002). *The myth of the paperless office.* MIT Press.

Singer, L. M., & Alexander, P. A. (2017). Reading on paper and digitally: What the past decades of empirical research reveal. *Review of Educational Research, 87*(6), 1007–1041.

Tinker, M. A. (1963). *Legibility of print.* Iowa State University Press.

Wolf, M. (2018). *Reader, come home: The reading brain in a digital world.* Harper.
:::

# Index {style="index" note="Bold numbers refer to the definitions in the glossary."}

:::index

:::callout{type="colophon"}
Set in Libertinus Serif, Libertinus Serif Display and Libertinus Sans (SIL Open Font License). Text: original, CC BY 4.0. The thesis, its author, its participants and its results are fictional; the works in the references are real.
:::
`; // content.<lang>.md, inlined by the Cookbook

// A table from rows of 'cell|cell|cell'; aligns has a letter a column, l or r.
const table = (id, typeId, caption, note, widths, aligns, rows) => ({ id, typeId, kind: 'table',
  caption, note, createdAt: 0, updatedAt: 0, table: { model: { headerRowCount: 1,
    columnWidths: widths, rows: rows.map((row, r) => row.split('|').map((content, c) => ({
      content, isHeader: r === 0, align: aligns[c] === 'r' ? 'right' : 'left' }))) } } });
const resources = [
  table('findings', 'table', 'Main results by medium',
    'Means for 48 participants. Bias is the predicted minus the actual score.', [5, 1.4, 1.4],
    'lrr', ['Measure|Paper|Screen', 'Literal comprehension (of 10)|7.8|7.7',
      'Inferential comprehension (of 10)|6.4|5.6', 'Predicted score (%)|75|78',
      'Actual score (%)|71|66.5', 'Calibration bias (points)|+4.0|+11.5',
      'Look-backs per text|5.8|3.1']),
  table('session-plan', 'table-a', 'Timing of an interview session', undefined, [1, 6, 1.4],
    'llr', ['Part|Content|Minutes', '1|Welcome, consent and a check of the recorder|3',
      '2|Free recall of the two study texts|5',
      '3|Questions 1–4: reading habits, look-backs and confidence|12',
      '4|Questions 5–7: the two media and an ideal design|12', '5|Debrief|3']),
];

// ─── 3 · Fonts ──────────────────────────────────────────────────────────────
const FONTS = { 'Libertinus Serif': ['400', '400i', '700'], 'Libertinus Serif Display': ['400'],
  'Libertinus Sans': ['700'] }; // every face the pages use, loaded first (gotcha: fonts-first)

// ─── 4 · Build & show ───────────────────────────────────────────────────────
// The thesis's sixth and last chapter opens on page 171, a recto.
const continuation = { pageIndexOffset: 170, pageNumbering: { startAt: 171 }, headings: { h1: 5 } };
await loadFonts(FONTS, markdown);
// The index is laid out again until its page numbers settle, inside this one call.
const doc = await buildWithFonts(
  () => buildDocument({ markdown, resources, continuation }, config()), markdown);
showPages(doc, { title: 'Reading on Screens and Paper: the back matter' });
offerPdf(() => renderToPdf(doc, { fontProvider: fontsourceProvider }), `${RECIPE}.pdf`);

// ─── 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 v1 ── the same in every recipe · postext.dev/cookbook ────────
// Postext measures text with the faces the browser has loaded, and caches the
// widths, so every face must be ready before the first build. Faces come from
// Fontsource: the same static files the PDF embeds, so screen and PDF agree.

/** faces = { 'Family Name': ['400', '400i', '700'] }. `text` is the sample:
 *  letters beyond Latin-1 (č, ł, ő…) also load the latin-ext files. With
 *  `optional`, a face Fontsource does not ship is skipped instead of failing.
 *  Resolves to the number of faces added. */
async function loadFonts(faces, text = '', { optional = false } = {}) {
  kitStatus('Loading fonts…');
  const ranges = {
    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',
  };
  const subsets = /[Ā-˿Ḁ-ỿ]/.test(text) ? ['latin', 'latin-ext'] : ['latin'];
  const jobs = [];
  let added = 0;
  for (const [family, specs] of Object.entries(faces)) {
    const id = fontsourceId(family);
    const meta = optional ? await fontsourceMeta(family) : null;
    for (const spec of new Set(specs)) {
      const weight = parseInt(spec, 10);
      const style = spec.endsWith('i') ? 'italic' : 'normal';
      if (hasFace(family, weight, style)) continue;
      if (optional && !(meta?.weights.includes(weight) && meta.styles.includes(style))) continue;
      for (const subset of subsets) {
        const url = `https://cdn.jsdelivr.net/npm/@fontsource/${id}@5/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().then((ready) => { document.fonts.add(ready); added++; }, () => {
          if (subset === 'latin' && !optional) throw new Error(`Fontsource has no ${family} ${weight} ${style}`);
        }));
      }
    }
  }
  await Promise.all(jobs).catch((error) => { kitFail(error); throw error; });
  return added;
}

/** Runs `build` (a buildDocument or buildBundle call) and checks the faces
 *  the pages use. A regular face missing from FONTS is loaded with a warning;
 *  bold and italic variants are loaded when the family ships them. Then the
 *  measurement caches are cleared and the build runs again. */
async function buildWithFonts(build, text = '') {
  const tried = new Set();
  for (let round = 0; round < 3; round++) {
    kitStatus('Laying out…');
    await new Promise(requestAnimationFrame);          // let the status paint first
    const result = await Promise.resolve().then(build).catch((error) => { kitFail(error); throw error; });
    const wanted = { base: {}, variants: {} };
    for (const { font, base } of [result].flat().flatMap(fontStringsOf)) {
      const { family, weight, style } = parseFont(font);
      const key = `${family}|${weight}|${style}`;
      if (tried.has(key) || hasFace(family, weight, style)) continue;
      tried.add(key);
      (wanted[base ? 'base' : 'variants'][family] ??= []).push(`${weight}${style === 'italic' ? 'i' : ''}`);
    }
    if (Object.keys(wanted.base).length) {
      console.warn(`[cookbook] FONTS does not list ${JSON.stringify(wanted.base)}: loading them.`);
    }
    const added = await loadFonts(wanted.base, text) + await loadFonts(wanted.variants, text, { optional: true });
    if (added === 0) return result;
    clearMeasurementCache();
  }
  throw new Error('The fonts did not settle after three builds.');
}

/** Every font string of the layout. `base` marks a block's own face; its
 *  bold, italic and bold-italic variants are listed whether or not used. */
function fontStringsOf(doc) {
  const found = new Map();
  const walk = (node) => {
    if (!node || typeof node !== 'object') return;
    if (Array.isArray(node)) { node.forEach(walk); return; }
    for (const [key, value] of Object.entries(node)) {
      if (typeof value === 'string' && /fontString$/i.test(key)) {
        found.set(value, found.get(value) || key === 'fontString');
      } else if (value && typeof value === 'object') walk(value);
    }
  };
  walk(doc.pages);
  walk(doc.blocks);
  return [...found].map(([font, base]) => ({ font, base }));
}

/** '700 37.5px Open Sans' / 'italic 400 13px "Source Serif 4"' → { family, weight, style }.
 *  A string with no weight ('95.8px Young Serif', from a design text) is 400. */
function parseFont(font) {
  const m = /^(?:(italic|oblique)\s+)?(?:small-caps\s+)?(?:(\d+|bold|normal)\s+)?[\d.]+px\s+(.+)$/.exec(font.trim());
  if (!m) throw new Error(`Unexpected font string: ${font}`);
  const weight = m[2] === 'bold' ? 700 : !m[2] || m[2] === 'normal' ? 400 : Number(m[2]);
  return { family: m[3].replace(/^["']|["']$/g, ''), weight, style: m[1] ? 'italic' : 'normal' };
}

/** True when a loaded FontFace covers exactly this family, weight and style
 *  (document.fonts.check() is also true for families nobody declared). */
function hasFace(family, weight, style) {
  for (const face of document.fonts) {
    if (face.status !== 'loaded' || face.style !== style) continue;
    if (face.family.replace(/^["']|["']$/g, '') !== family) continue;
    const [low, high = low] = face.weight.split(' ').map(Number);
    if (weight >= low && weight <= high) return true;
  }
  return false;
}

/** Fontsource's id for a family: 'Source Serif 4' → 'source-serif-4'. */
function fontsourceId(family) { return family.toLowerCase().replace(/\s+/g, '-'); }

/** The weights and styles a family ships ({ weights: [400, 700], styles: ['normal', 'italic'] }), or null. */
function fontsourceMeta(family) {
  fontsourceMeta.cache ??= new Map();
  const id = fontsourceId(family);
  if (!fontsourceMeta.cache.has(id)) {
    fontsourceMeta.cache.set(id, fetch(`https://api.fontsource.org/v1/fonts/${id}`)
      .then((res) => (res.ok ? res.json() : null), () => null));
  }
  return fontsourceMeta.cache.get(id);
}

// ─── Kit · viewer v1 ── the same in every recipe · postext.dev/cookbook ───────
/** Shows the pages as facing spreads on a dark desk: the first page is a
 *  recto on its own, then verso | recto pairs, as in a bound book. Pages
 *  are painted when they scroll near the screen. */
function showPages(docs, { title, width = 460 } = {}) {
  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, { 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 · pdf v1 ── the same in every recipe that exports a PDF ──────────────
/** postext-pdf embeds TrueType bytes. Fetch the Fontsource file the screen
 *  used, snapping to a weight the family ships and falling back to upright
 *  when it has no italic: the PDF asks for every face a block could use. */
async function fontsourceProvider(family, weight, style) {
  const id = fontsourceId(family);
  const meta = await fontsourceMeta(family);
  const weights = meta?.weights?.length ? meta.weights : [400, 700];
  const w = weights.reduce((a, b) => (Math.abs(b - weight) < Math.abs(a - weight) ? b : a));
  const s = style === 'italic' && meta && !meta.styles.includes('italic') ? 'normal' : style;
  const res = await fetch(`https://cdn.jsdelivr.net/npm/@fontsource/${id}@5/files/${id}-latin-${w}-${s}.woff2`);
  if (!res.ok) throw new Error(`Fontsource has no ${family} ${w} ${s} (${res.status})`);
  return decompressWoff2(new Uint8Array(await res.arrayBuffer()));
}

/** A "Build the PDF" button in the bar. Once built: "Open the PDF" (a new
 *  tab, since CodePen's preview frame cannot show PDFs) and a download link. */
function offerPdf(makePdf, filename) {
  viewer();
  const button = Object.assign(document.createElement('button'), { type: 'button', textContent: 'Build the PDF' });
  button.dataset.postextPdf = filename;
  button.addEventListener('click', async () => {
    button.disabled = true;
    button.textContent = 'Building the PDF…';
    try {
      const bytes = await makePdf();
      const url = URL.createObjectURL(new Blob([bytes], { type: 'application/pdf' }));
      const size = `${Math.max(1, Math.round(bytes.length / 1024))} KB`;
      button.replaceWith(
        Object.assign(document.createElement('a'), { href: url, target: '_blank', rel: 'noopener', textContent: 'Open the PDF ↗' }),
        Object.assign(document.createElement('a'), { href: url, download: filename, textContent: `Download ${filename} · ${size}` }));
    } catch (error) {
      button.disabled = false;
      button.textContent = 'Build the PDF';
      kitFail(error);
    }
  });
  document.getElementById('pt-actions').append(button);
}

// ─── /Kit ───────────────────────────────────────────────────────────────────────
```

## Variants

### Obre cada secció en pàgina senar

Les tesis que s'enquadernen per a una biblioteca solen obrir cada secció en pàgina senar. El glossari, la bibliografia i l'índex passen llavors a les pàgines 175, 177 i 179, cadascuna després d'una parella en blanc, i els números en negreta de l'índex apunten a la 175.

```diff
-const backMatter = (id, extra) => ({ id, numbered: false, breakBefore: { enabled: true,
-  parity: 'any' }, advancedDesign: opener('Back matter'), header: sectionHeads, ...extra });
+const backMatter = (id, extra) => ({ id, numbered: false, breakBefore: { enabled: true,
+  parity: 'odd' }, advancedDesign: opener('Back matter'), header: sectionHeads, ...extra });
```

### Compon també el principi de la tesi

Els preliminars, numerats en romans abans de la pàgina 1, són a [Preliminars amb folis romans i després la pàgina 1](https://postext.dev/ca/cookbook/front-matter-roman-to-arabic.md).

## Errors freqüents

- **Un estil de títol hereta el salt de pàgina del seu nivell.** Una entrada de headingStyles pren del seu nivell de títol tot allò que no fixa, també breakBefore. Un índex o un colofó amb estil sobre un H1 després d'un :::pagebreak hereta la paritat 'odd' i cau darrere d'una pàgina en blanc. Dona a aquest estil breakBefore: { enabled: false }.
- **Qualsevol objecte headings desactiva el salt de pàgina de l'H1.** Per defecte un H1 salta a una pàgina senar (always-odd), però qualsevol objecte headings anul·la aquest valor, de manera que els capítols van seguits i span: 'page' no fa res. Torna a declarar headings.levels[0].breakBefore: { enabled: true, parity } a cada configuració.
- **Una paleta canviada no arriba als elements de disseny ni al color de les remissions.** postext 1.4.1 aplica colorPalette als estils de text (cos, títols, llistes, peus, taules, requadres), però no als elements de capçaleres, peus de pàgina, obertures i portadelles, ni a bodyText.referenceColor: conserven l'hex escrit al costat del seu paletteId. Si canvies la paleta, per a una edició de pantalla fosca o per recolorejar, reescriu cada color enllaçat a partir de colorPalette abans de compondre.
- **En el text en bandera no hi ha partició de mots.** La partició de mots només s'aplica al text justificat; el text en bandera talla entre paraules, així que una columna estreta en bandera queda molt desigual. Justifica el passatge o eixampla la mesura.
- **El PDF demana tots els gruixos i estils de cada família.** renderToPdf demana al proveïdor de fonts la negreta, la cursiva i la negreta cursiva de cada família que un bloc podria fer servir, encara que no s'imprimeixi mai, i un sol rebuig atura l'exportació. El proveïdor s'ha d'ajustar al gruix més proper que tingui la família i tornar a la rodona quan no hi hagi cursiva.
- **La pàgina 1 és senar: planifica amb números físics.** La pàgina 1 queda a la dreta i la 2 és la primera pàgina parella, així que planifica els plecs amb números de pàgina físics: una obertura en pàgina parella queda davant de la senar que la segueix.
- **Posa entre cometes cada valor del frontmatter.** YAML llegeix title: 1984 com un nombre i una data com un objecte Date, i els valors que no són cadenes s'imprimeixen buits als marcadors i deixen el PDF sense títol. Posa entre cometes cada valor: title: "1984".
- **Una configuració es desa a la memòria cau per identitat: crea un objecte nou.** El motor desa a la memòria cau les configuracions resoltes segons la identitat de l'objecte, de manera que modificar el mateix objecte i tornar a compondre reutilitza el resultat anterior. Crea un objecte nou a cada composició: per això la configuració d'una recepta és una funció, config().
- **Carrega totes les fonts abans de compondre.** La composició mesura el text amb les fonts que el navegador ha carregat i en desa les amplades, així que una font que arriba després de la primera composició deixa talls de línia erronis i un PDF que ja no coincideix amb la pantalla. Carrega abans tots els pesos i estils, i crida clearMeasurementCache() abans de recompondre si alguna arriba tard.

## Crèdits

- Recepta: Ignacio Ferro ([@drnachio](https://github.com/drnachio))
- Tipus de lletra: Libertinus Serif (OFL-1.1), Libertinus Serif Display (OFL-1.1), Libertinus Sans (OFL-1.1)
- Codi: MIT · Contingut d'exemple: CC-BY-4.0

## Relacionades

- [Núm. 006 · Preliminars amb folis romans i després la pàgina 1](https://postext.dev/ca/cookbook/front-matter-roman-to-arabic.md): La coberta i els preliminars són títols sense numerar comptats en romans; :::numbering torna a comptar des de 1 a la pàgina senar on comença la novel·la. · Nivell 3 (Avançat) · Narrativa, teatre i prosa literària
- [Núm. 020 · Notes finals en pàgina pròpia, a dues columnes](https://postext.dev/ca/cookbook/endnotes-instead-of-footnotes.md): Un preprocessador breu numera les notes [^1] i les reuneix sota un títol Notes, l'estil del qual les compon en pàgina pròpia, a dues columnes. · Nivell 2 (Intermedi) · Articles i treballs acadèmics
- [Núm. 002 · Article a dues columnes amb equacions numerades](https://postext.dev/ca/cookbook/journal-article-with-maths.md): Article de física a dues columnes amb set equacions numerades, compostes amb el MathJax de la versió ?bundle. Al PDF continuen sent vectorials. · Nivell 3 (Avançat) · Articles i treballs acadèmics
