Vai al contenuto principale
Ricetta numero 102

Ricettario · Capitolo 10 · Output e integrazione

Una fanzine spillata con copertina in cartoncino

Due recinzioni :::paper mettono un cartoncino salmone sul foglio esterno di una fanzine di otto pagine, e postext-folio la mostra spillata su carta da giornale.

In questa pagina
Livello
Intermedio
Postext
Provata con Postext 1.31.0
Richiede ≥ 1.25.0
Licenza
Aggiornata il 10 ott 2026
Codice MIT · Testo CC BY 4.0
  • Esempio in inglese: non c’è ancora un’edizione italiana
  • Formato 140 × 200 mm
  • 1 colonna
  • Newsreader 10/13,6
  • Bricolage Grotesque
  • 8 pagine
  • Livello
  • Postext 1.31.0
  • Impaginato in 67 ms
  • 159 righe di codice

In breve

Una piccola rivista piegata su un fiume, con una copertina spessa e colorata e carta da giornale sottile all’interno. Nel visualizzatore 3D sta chiusa sulla scrivania e si apre come quella vera.

Che cosa comporrai

Il numero 3 di Low Tide, una fanzine gratuita su un estuario: otto pagine di 140 × 200 mm, due fogli piegati e spillati nella piega. Il foglio esterno, pagine 1–2 e 7–8, è un cartoncino salmone; il foglio interno, pagine 3–6, è carta da giornale economica. La copertina è stampata in un solo blu riso sul cartoncino nudo, senza un campo di colore proprio, quindi nelle pagine piatte qui sotto si stampa su bianco, e in Folio sta chiusa sulla scrivania nel colore del cartoncino finché non la giri. Le due carte sono due recinzioni nel Markdown, e la legatura sono tre impostazioni in config.folio.

Questa ricetta risponde a

  • Come stampo una copertina, un inserto o una sezione di tavole su una carta diversa dal resto del libro?
  • Come vedo in anteprima la rilegatura, la copertina, la carta e l’illuminazione di un libro prima di mandarlo in stampa?

La risposta breve

script.js · righe 242–253nel codice completo
const stage = document.createElement('section');
stage.id = 'folio';
stage.ariaLabel = 'The zine in 3D';
stage.style.cssText = 'height: min(78vh, 720px); margin: 0 auto; max-width: 1280px';
document.getElementById('pages').before(stage);
// Covers from the pages need an even count: page 8 must fall on a verso to be the back.
if (doc.pages.length % 2) console.warn(`${doc.pages.length} pages: the back cover is missing`);
createFolioFromDocument(stage, doc, {
  appearance: { textureBaseUrl: 'https://postext.dev/folio/textures' },
  onChange: ({ pages }) => kitStatus(pages.map((i) => `p. ${i + 1} on `
    + (doc.pages[i].paper ? 'card' : 'newsprint')).join(' · ')),
});

Ingredienti

Tipo
Newsreader, Bricolage Grotesque (SIL OFL 1.1)
Risorse
  • The tide lines on the cover, drawn in code (Ignacio Ferro, CC BY 4.0)

Preparazione

#1 · Recinta il foglio esterno

script.js · righe 137–228nel codice completo
// :::paper{type=cardStock grammage=250 shade=#f2c9b4}   pages 1–2: the front of the sheet
// # Low Tide {style="cover" …}   # In this issue {…}
// :::
// # The dunlin come back …   pages 3–6, outside any fence: folio.paper, the newsprint
// :::paper{type=cardStock grammage=250 shade=#f2c9b4}   pages 7–8: the back of the sheet
// # Join us {…}   # Low Tide {style="back" …}
// :::
const markdown = String.raw`---
title: "Low Tide"
subtitle: "A zine about the estuary"
author: "The Pill Estuary Group"
publishDate: "Issue 3 · Winter 2026"
---

:::paper{type=cardStock grammage=250 shade=#f2c9b4}

# Low Tide {style="cover" issue="Issue 3 · Winter 2026" strap="Mud, birds, boats and the people who watch them"}

# In this issue {style="feature" kicker="Welcome"}

Low Tide is made twice a year by the people who walk the river path at Pill. It is printed on the cheapest paper the printer has, folded by hand at the sailing club, and left in the pub, the post office and the bird hide. Take one. Leave it for somebody else when you have read it.

This winter we follow the dunlin that come back to the mud every October, talk to the last man in the village who still builds wooden boats, and print a tide table for the months when the light goes early. The cover is a heavier card than the pages inside it, so it survives a pocket and a little rain.

:::paragraphs{style="credits"}
**Edited by** Joan Avery and Sam Okonjo · **Drawings** Priya Lal · **Printed** in an edition of 400 copies, two sheets folded and stapled.
:::

:::

# The dunlin come back {style="feature" kicker="Birds"}

They arrive in the second week of October, a few dozen at first and then, within days, a thousand or more, feeding along the tideline in a moving grey band. Dunlin are small, about the size of a starling, with a long bill that droops at the tip. In winter they lose the black belly of their breeding plumage and turn the colour of the mud they feed on, which is why most people who walk the path never notice them until they lift.

When they lift, everybody notices. A flock of dunlin in the air turns as one bird: grey as it banks away from you, white as it turns back and shows its underside. On a clear afternoon at the top of the tide, when the mud is covered and the birds have nowhere left to feed, the flock rises off the saltmarsh and turns over the water for minutes at a time, flashing like a shoal of fish.

They come from the Arctic: from the tundra of northern Russia and Scandinavia, where they breed in the short summer and leave as soon as the young can fly. A dunlin ringed on the mud below the church in 2019 was caught again the following summer on the Taymyr Peninsula, five thousand kilometres away, and back on the same stretch of mud the October after.

Joan has counted them every Sunday morning for eleven winters, from the hide, with a tally counter in her coat pocket. Her highest count was 4,200, in January 2021, after a week of frost on the moors drove birds down from the reservoirs. Her lowest was 380. She does not know why the numbers swing so much from year to year, and nor, she says, does anybody else.

At dusk the flock goes to roost on the shingle spit below the old ferry steps, the last ground the tide leaves dry. From the path you hear them before you see them: a thin, purring trill passed along the flock like a rumour. By March the first of them show black on the belly again, and by the middle of April the mud is quiet.

## How to see them

Come two hours before high water and stand on the slipway by the sailing club. As the tide pushes in, the birds are driven up the mud towards you, closer and closer, until they give up and fly. Bring binoculars, and keep dogs on a lead below the hide: a bird that is flushed off the mud in winter spends energy it cannot afford.

The best days are the spring tides marked in bold in the tide table on page 6, when the water comes right up to the saltmarsh. On a neap tide the birds stay far out and you will want a telescope. If the wind is in the west, stand in the lee of the boat shed; the dunlin will be doing the same on the far side of the creek.

## What else to look for

Among the dunlin there are usually a few ringed plover, rounder and paler, that run and stop where the dunlin walk and probe. Curlew stand alone further out, and the redshank is the one that sees you first and tells the whole estuary. A peregrine has hunted the flock every winter since 2017: when the birds bunch tight and climb, look above them.

# The last boatbuilder {style="feature" kicker="People"}

Dennis Harcombe built his first boat at fourteen, in his father’s shed on Marine Parade, and his last one at seventy-eight. He reckons there were forty in between, give or take, most of them clinker-built dinghies of larch on oak, the kind every family on the river once kept at the bottom of the garden.

“The trick of it,” he says, “is that you never fight the wood. You look at a plank and you see which way it wants to bend, and you let it.” He still steams planks in a length of drainpipe over a camping stove, though these days only to repair other people’s boats.

His shed is the last on the river where you can buy a pair of oars made to measure. He makes them from ash, shaves the looms by hand and charges what he charged in 1990, because, he says, the price was fair then and the oars have not got any better.

When he talks about the river he talks about the mud first: where it shifts, where the channel has moved since he was a boy, where a boat can sit out a tide and where it will be swallowed to the thwarts. He has watched the estuary change for seventy years and does not think it is finished.

# Tide table {style="feature" kicker="December to February"}

High water at Pill, in local time. Add about twenty minutes for the tide to reach the slipway at the sailing club. Spring tides, the biggest of the month, are marked in bold: on those days the mud below the hide is covered completely.

:::paragraphs{style="tides"}
**Sun 6 Dec** 07:42 and 20:05 · **Sat 12 Dec** 12:18 · **Sun 20 Dec** 06:55 and 19:20

**Sun 3 Jan** 06:31 and 18:54 · **Sat 9 Jan** 11:47 · **Sun 17 Jan** 06:10 and 18:36

**Sun 31 Jan** 05:58 and 18:22 · **Sat 6 Feb** 10:59 · **Sun 14 Feb** 05:37 and 18:01
:::

Times are for a still day. A strong westerly holds the water up and brings high tide a little early; a long dry spell on the moors does the opposite. If you are walking out on the mud, go on a falling tide and come back with an hour to spare.

:::paper{type=cardStock grammage=250 shade=#f2c9b4}

# Join us {style="back-matter" kicker="The group"}

We walk the river path on the first Sunday of every month, meeting at the sailing club at ten. Nobody needs to know anything about birds, boats or tides; most of us didn’t when we started. Bring boots.

The next issue comes out in June. If you have something to say about the estuary, a drawing, a memory or a count of anything at all, leave it in the box behind the bar at the Duke of Cornwall, or write to us at the sailing club.

:::paragraphs{style="credits"}
Set in Newsreader and Bricolage Grotesque (SIL Open Font License) · Text: original, CC BY 4.0 · The Pill Estuary Group and the people in this issue are fictional.
:::

# Low Tide {style="back" strap="Free · take one, leave one"}

:::
`; // content.<lang>.md, inlined by the Cookbook

Un opuscolo a punto metallico è una pila di fogli piegati, quindi il foglio di copertina contiene le prime due e le ultime due pagine. Una recinzione contiene le pagine 1–2, un’altra le pagine 7–8, e le pagine in mezzo restano fuori da entrambe, sulla carta del libro. Ogni recinzione comincia e finisce su una nuova pagina, che qui è comunque la pagina di cui hanno bisogno la copertina e il benvenuto.

#2 · Carta da giornale, spillata, copertine dalle pagine

script.js · righe 40–49nel codice completo
const folio = {
  tilt: 20,
  // The book's paper: every page outside a :::paper fence, the inner sheet here.
  paper: { type: 'newsprint', grammage: 52 },
  // cover: 'pages' turns page 1 as the front and page 8 (a verso) as the back; on a saddle
  // stitch the cover is a sheet like the others, only heavier (gotcha: folio-cover-pages-even).
  binding: { type: 'saddleStitch', cover: 'pages' },
  surface: { type: 'plain', color: col('ink') },
  lighting: { environment: 'overcast', intensity: 1.1 },
};

folio.paper è la carta da giornale di ogni pagina fuori da una recinzione. saddleStitch piega i fogli attorno ai punti metallici senza dorso piatto, e cover: 'pages' fa girare la pagina 1 e la pagina 8 come copertine. Con il punto metallico la copertina si gira come gli altri fogli, solo più rigida, e un foglio di copertina che nomina la propria carta (il cartoncino) viene disegnato su quella carta.

#3 · Una copertina in un solo inchiostro

script.js · righe 53–86nel codice completo
// Eleven tide lines of a seeded length: blue rules across the foot of the cover.
let seed = 0x71de; // Mulberry32: the same lines on every run
const random = () => {
  seed = (seed + 0x6d2b79f5) | 0;
  let x = Math.imul(seed ^ (seed >>> 15), 1 | seed);
  x = (x + Math.imul(x ^ (x >>> 7), 61 | x)) ^ x;
  return ((x ^ (x >>> 14)) >>> 0) / 4294967296;
};
const tideLines = Array.from({ length: 11 }, (_, i) => ({ kind: 'rule', id: `tide-${i}`,
  direction: 'horizontal', thickness: pt(2.2 + i * 0.5), color: col('blue'),
  placement: { ...at('page', 'top-left', 10 + random() * 40, 128 + i * 5.2),
    size: { width: mm(50 + random() * 70) } } }));
const flush = { span: 'page', margins: { top: mm(0), bottom: mm(0), left: mm(0), right: mm(0) },
  footer: { elements: [] } }; // no folio on either cover (the style lasts one page)
const cover = { ...flush, id: 'cover', numbered: false, advancedDesign: { enabled: true,
  minHeight: mm(TRIM_H), slot: { elements: [
    text('issue', '{attr.issue}', caps(7.5), at('page', 'top-left', INNER, 16)),
    text('title', '{titleText}', { fontFamily: SANS, fontWeight: 800, fontSize: pt(70),
      lineHeight: 0.86, letterSpacing: pt(-2), color: col('blue') },
    { ...at('#issue', 'below', -1, 8), size: { width: mm(TRIM_W - INNER - OUTER) } }),
    text('strap', '{attr.strap}', { fontFamily: 'Newsreader', italic: true, fontSize: pt(13),
      lineHeight: 1.25, color: col('ink') },
    { ...at('#title', 'below', 1, 6), size: { width: mm(100) } }),
    ...tideLines,
  ] } } };
const back = { ...flush, id: 'back', numbered: false,
  breakBefore: { enabled: true, parity: 'even' }, advancedDesign: { enabled: true,
    minHeight: mm(TRIM_H), slot: { elements: [
      text('title', '{titleText}', { fontFamily: SANS, fontWeight: 800, fontSize: pt(26),
        letterSpacing: pt(-0.5), color: col('blue'), align: 'center' },
      { ...at('page', 'center', 0, -6), size: { width: mm(100) } }),
      text('strap', '{attr.strap}', { ...caps(7, col('ink')), align: 'center' },
        { ...at('#title', 'below', 0, 3), size: { width: mm(100) } }),
    ] } } };

La copertina è uno stile di titolo con margini a zero e un progetto grafico a pagina intera: la riga del numero, il titolo in Bricolage Grotesque da 70 pt, il sottotitolo in Newsreader corsivo e undici linee di marea le cui lunghezze vengono da un generatore con seme fisso, così ogni esecuzione disegna le stesse. Nulla dipinge il fondo della pagina: lo fa il cartoncino.

#4 · Una sola apertura per ogni pezzo

script.js · righe 90–100nel codice completo
const feature = (id) => ({ id, numbered: false, breakBefore: { enabled: true, parity: 'any' },
  marginBottom: pt(LEAD), advancedDesign: { enabled: true, minHeight: mm(30), slot: {
    elements: [
      text('kicker', '{attr.kicker}', caps(7.5), at('container', 'top-left', 0, 1)),
      text('title', '{titleText}', { fontFamily: SANS, fontWeight: 800, fontSize: pt(25),
        lineHeight: 0.98, letterSpacing: pt(-0.4), color: col('ink') },
      { ...at('#kicker', 'below', 0, 2.5), size: { width: 'fill' } }),
      { kind: 'rule', id: 'rule', direction: 'horizontal', thickness: pt(2.5), color: col('blue'),
        placement: { ...at('#title', 'below', 0, 4), size: { width: mm(14) } } },
    ] } } });
const headingStyles = [cover, back, feature('feature'), feature('back-matter')];

Ogni servizio è un titolo di primo livello nello stile feature: una nuova pagina, un occhiello dai suoi attributi, il titolo e un breve filetto blu. Far passare i cinque pezzi per un solo stile limita l’interno della fanzine a due caratteri e un solo colore d’accento.

#5 · Aprila spillata

Il codice è la risposta breve qui sopra. createFolioFromDocument legge la legatura e le copertine da doc.config.folio e la carta di ogni foglio da page.paper. Il pen avvisa quando il numero di pagine è dispari, perché la pagina 8 può essere la quarta di copertina solo quando cade su un verso.

La ricetta completa

Sandbox
// ═══ Postext Cookbook · Nº 102 · A stapled zine with a card cover sheet ═══════════════
// https://postext.dev/en/cookbook/saddle-stitched-card-cover
// Code: MIT · Text: original (CC BY 4.0) · Pictures: drawn in code
// Fonts: Newsreader, Bricolage Grotesque (SIL OFL 1.1) · Needs postext ≥ 1.25.0
//
// Eight pages, two sheets folded and stapled through the fold. The outer sheet (pages 1–2 and
// 7–8) is salmon card, the inner sheet newsprint: two :::paper fences put the card on the
// cover pages, and postext-folio draws the book saddle-stitched, closed until you turn the
// cover. The flat pages below print on white: only the 3D book knows about paper.
import { buildDocumentWithFonts, renderPageToCanvas } from 'https://esm.sh/postext';
import { createFolioFromDocument } from 'https://esm.sh/postext-folio';

const LANG = 'en'; // @lang: the language of the sample document ('en' | 'es')
const RECIPE = 'saddle-stitched-card-cover';

// ─── 1 · Design ─────────────────────────────────────────────────────────────
// #region palette: two inks of a risograph, and the greys of the newsprint text
const palette = {
  ink: '#1f2124', // the text
  blue: '#0b6fb3', // the riso blue: titles, the tide lines, kickers
  muted: '#5d6066', // credits and folios
  rule: '#b9bcc0',
};
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' } })),
  { id: 'main-color', name: 'accent (defaults)', value: { hex: palette.blue, model: 'hex' } },
];
// #endregion
const [TRIM_W, TRIM_H, TOP, BOTTOM, INNER, OUTER] = [140, 200, 20, 22, 17, 15]; // mm
const LEAD = 13.6; // pt
const SANS = 'Bricolage Grotesque';
const at = (to, edge, x = 0, y = 0) => ({ anchor: { to, edge }, offset: { x: mm(x), y: mm(y) } });
const text = (id, content, look, placement) => ({ kind: 'text', id, content, overflow: 'wrap',
  align: 'left', placement, ...look });
const caps = (size, color = col('blue')) => ({ fontFamily: SANS, fontSize: pt(size),
  fontWeight: 700, letterSpacing: pt(size * 0.18), textTransform: 'uppercase', color });

// #region folio: newsprint inside, stapled through the fold, covers taken from the pages
const folio = {
  tilt: 20,
  // The book's paper: every page outside a :::paper fence, the inner sheet here.
  paper: { type: 'newsprint', grammage: 52 },
  // cover: 'pages' turns page 1 as the front and page 8 (a verso) as the back; on a saddle
  // stitch the cover is a sheet like the others, only heavier (gotcha: folio-cover-pages-even).
  binding: { type: 'saddleStitch', cover: 'pages' },
  surface: { type: 'plain', color: col('ink') },
  lighting: { environment: 'overcast', intensity: 1.1 },
};
// #endregion

// #region covers: big type and tide lines on the page; the card colour comes from the paper
// Eleven tide lines of a seeded length: blue rules across the foot of the cover.
let seed = 0x71de; // Mulberry32: the same lines on every run
const random = () => {
  seed = (seed + 0x6d2b79f5) | 0;
  let x = Math.imul(seed ^ (seed >>> 15), 1 | seed);
  x = (x + Math.imul(x ^ (x >>> 7), 61 | x)) ^ x;
  return ((x ^ (x >>> 14)) >>> 0) / 4294967296;
};
const tideLines = Array.from({ length: 11 }, (_, i) => ({ kind: 'rule', id: `tide-${i}`,
  direction: 'horizontal', thickness: pt(2.2 + i * 0.5), color: col('blue'),
  placement: { ...at('page', 'top-left', 10 + random() * 40, 128 + i * 5.2),
    size: { width: mm(50 + random() * 70) } } }));
const flush = { span: 'page', margins: { top: mm(0), bottom: mm(0), left: mm(0), right: mm(0) },
  footer: { elements: [] } }; // no folio on either cover (the style lasts one page)
const cover = { ...flush, id: 'cover', numbered: false, advancedDesign: { enabled: true,
  minHeight: mm(TRIM_H), slot: { elements: [
    text('issue', '{attr.issue}', caps(7.5), at('page', 'top-left', INNER, 16)),
    text('title', '{titleText}', { fontFamily: SANS, fontWeight: 800, fontSize: pt(70),
      lineHeight: 0.86, letterSpacing: pt(-2), color: col('blue') },
    { ...at('#issue', 'below', -1, 8), size: { width: mm(TRIM_W - INNER - OUTER) } }),
    text('strap', '{attr.strap}', { fontFamily: 'Newsreader', italic: true, fontSize: pt(13),
      lineHeight: 1.25, color: col('ink') },
    { ...at('#title', 'below', 1, 6), size: { width: mm(100) } }),
    ...tideLines,
  ] } } };
const back = { ...flush, id: 'back', numbered: false,
  breakBefore: { enabled: true, parity: 'even' }, advancedDesign: { enabled: true,
    minHeight: mm(TRIM_H), slot: { elements: [
      text('title', '{titleText}', { fontFamily: SANS, fontWeight: 800, fontSize: pt(26),
        letterSpacing: pt(-0.5), color: col('blue'), align: 'center' },
      { ...at('page', 'center', 0, -6), size: { width: mm(100) } }),
      text('strap', '{attr.strap}', { ...caps(7, col('ink')), align: 'center' },
        { ...at('#title', 'below', 0, 3), size: { width: mm(100) } }),
    ] } } };
// #endregion

// #region features: every piece opens a page with a kicker and a title in the riso blue
const feature = (id) => ({ id, numbered: false, breakBefore: { enabled: true, parity: 'any' },
  marginBottom: pt(LEAD), advancedDesign: { enabled: true, minHeight: mm(30), slot: {
    elements: [
      text('kicker', '{attr.kicker}', caps(7.5), at('container', 'top-left', 0, 1)),
      text('title', '{titleText}', { fontFamily: SANS, fontWeight: 800, fontSize: pt(25),
        lineHeight: 0.98, letterSpacing: pt(-0.4), color: col('ink') },
      { ...at('#kicker', 'below', 0, 2.5), size: { width: 'fill' } }),
      { kind: 'rule', id: 'rule', direction: 'horizontal', thickness: pt(2.5), color: col('blue'),
        placement: { ...at('#title', 'below', 0, 4), size: { width: mm(14) } } },
    ] } } });
const headingStyles = [cover, back, feature('feature'), feature('back-matter')];
// #endregion

const config = () => ({
  locale: 'en-us',
  colorPalette,
  page: { sizePreset: 'custom', width: mm(TRIM_W), height: mm(TRIM_H), dpi: 150,
    margins: { top: mm(TOP), bottom: mm(BOTTOM), left: mm(INNER), right: mm(OUTER),
      mirror: true } },
  layout: { layoutType: 'single' },
  bodyText: { fontFamily: 'Newsreader', fontSize: pt(10), lineHeight: pt(LEAD),
    color: col('ink'), boldColor: col('ink'), italicColor: col('ink'),
    referenceColor: col('ink'), textAlign: 'justify', firstLineIndent: mm(4),
    indentAfterHeading: false, hyphenation: { enabled: true }, optimalLineBreaking: true,
    avoidWidows: true, avoidOrphans: true, avoidRunts: true },
  headings: { fontFamily: SANS, color: col('ink'), fontWeight: 800, levels: [
    // parity 'any': a level-1 heading opens the next page, whichever side it falls on.
    { level: 1, fontSize: pt(25), breakBefore: { enabled: true, parity: 'any' } },
    { level: 2, fontSize: pt(10), lineHeight: pt(LEAD), fontWeight: 700, color: col('blue'),
      marginTop: pt(LEAD), marginBottom: pt(0) },
  ] },
  headingStyles,
  paragraphStyles: [
    { id: 'credits', fontFamily: SANS, fontSize: pt(7.4), lineHeight: pt(10.4),
      color: col('muted'), boldColor: col('blue'), textAlign: 'left', firstLineIndent: pt(0),
      marginTop: pt(LEAD) },
    { id: 'tides', fontFamily: SANS, fontSize: pt(8.6), lineHeight: pt(LEAD),
      boldColor: col('blue'), textAlign: 'left', firstLineIndent: pt(0) },
  ],
  header: { elements: [] },
  footer: { elements: [text('folio', '{pageNumber}   {title} · {publishDate}', { ...caps(6.5,
    col('muted')), align: 'center' }, at('page', 'bottom', 0, -11))] },
  folio,
});

// ─── 2 · Content ────────────────────────────────────────────────────────────
// #region cover-sheet: the outer sheet's four pages in card, the inner sheet on newsprint
// :::paper{type=cardStock grammage=250 shade=#f2c9b4}   pages 1–2: the front of the sheet
// # Low Tide {style="cover" …}   # In this issue {…}
// :::
// # The dunlin come back …   pages 3–6, outside any fence: folio.paper, the newsprint
// :::paper{type=cardStock grammage=250 shade=#f2c9b4}   pages 7–8: the back of the sheet
// # Join us {…}   # Low Tide {style="back" …}
// :::
const markdown = String.raw`---
Esempio Markdown · 83 righe · content.en.mdtitle: "Low Tide" subtitle: "A zine about the estuary" author: "The Pill Estuary Group" publishDate: "Issue 3 · Winter 2026" --- :::paper{type=cardStock grammage=250 shade=#f2c9b4} # Low Tide {style="cover" issue="Issue 3 · Winter 2026" strap="Mud, birds, boats and the people who watch them"} # In this issue {style="feature" kicker="Welcome"} Low Tide is made twice a year by the people who walk the river path at Pill. It is printed on the cheapest paper the printer has, folded by hand at the sailing club, and left in the pub, the post office and the bird hide. Take one. Leave it for somebody else when you have read it. This winter we follow the dunlin that come back to the mud every October, talk to the last man in the village who still builds wooden boats, and print a tide table for the months when the light goes early. The cover is a heavier card than the pages inside it, so it survives a pocket and a little rain. :::paragraphs{style="credits"} **Edited by** Joan Avery and Sam Okonjo · **Drawings** Priya Lal · **Printed** in an edition of 400 copies, two sheets folded and stapled. ::: ::: # The dunlin come back {style="feature" kicker="Birds"} They arrive in the second week of October, a few dozen at first and then, within days, a thousand or more, feeding along the tideline in a moving grey band. Dunlin are small, about the size of a starling, with a long bill that droops at the tip. In winter they lose the black belly of their breeding plumage and turn the colour of the mud they feed on, which is why most people who walk the path never notice them until they lift. When they lift, everybody notices. A flock of dunlin in the air turns as one bird: grey as it banks away from you, white as it turns back and shows its underside. On a clear afternoon at the top of the tide, when the mud is covered and the birds have nowhere left to feed, the flock rises off the saltmarsh and turns over the water for minutes at a time, flashing like a shoal of fish. They come from the Arctic: from the tundra of northern Russia and Scandinavia, where they breed in the short summer and leave as soon as the young can fly. A dunlin ringed on the mud below the church in 2019 was caught again the following summer on the Taymyr Peninsula, five thousand kilometres away, and back on the same stretch of mud the October after. Joan has counted them every Sunday morning for eleven winters, from the hide, with a tally counter in her coat pocket. Her highest count was 4,200, in January 2021, after a week of frost on the moors drove birds down from the reservoirs. Her lowest was 380. She does not know why the numbers swing so much from year to year, and nor, she says, does anybody else. At dusk the flock goes to roost on the shingle spit below the old ferry steps, the last ground the tide leaves dry. From the path you hear them before you see them: a thin, purring trill passed along the flock like a rumour. By March the first of them show black on the belly again, and by the middle of April the mud is quiet. ## How to see them Come two hours before high water and stand on the slipway by the sailing club. As the tide pushes in, the birds are driven up the mud towards you, closer and closer, until they give up and fly. Bring binoculars, and keep dogs on a lead below the hide: a bird that is flushed off the mud in winter spends energy it cannot afford. The best days are the spring tides marked in bold in the tide table on page 6, when the water comes right up to the saltmarsh. On a neap tide the birds stay far out and you will want a telescope. If the wind is in the west, stand in the lee of the boat shed; the dunlin will be doing the same on the far side of the creek. ## What else to look for Among the dunlin there are usually a few ringed plover, rounder and paler, that run and stop where the dunlin walk and probe. Curlew stand alone further out, and the redshank is the one that sees you first and tells the whole estuary. A peregrine has hunted the flock every winter since 2017: when the birds bunch tight and climb, look above them. # The last boatbuilder {style="feature" kicker="People"} Dennis Harcombe built his first boat at fourteen, in his father’s shed on Marine Parade, and his last one at seventy-eight. He reckons there were forty in between, give or take, most of them clinker-built dinghies of larch on oak, the kind every family on the river once kept at the bottom of the garden. “The trick of it,” he says, “is that you never fight the wood. You look at a plank and you see which way it wants to bend, and you let it.” He still steams planks in a length of drainpipe over a camping stove, though these days only to repair other people’s boats. His shed is the last on the river where you can buy a pair of oars made to measure. He makes them from ash, shaves the looms by hand and charges what he charged in 1990, because, he says, the price was fair then and the oars have not got any better. When he talks about the river he talks about the mud first: where it shifts, where the channel has moved since he was a boy, where a boat can sit out a tide and where it will be swallowed to the thwarts. He has watched the estuary change for seventy years and does not think it is finished. # Tide table {style="feature" kicker="December to February"} High water at Pill, in local time. Add about twenty minutes for the tide to reach the slipway at the sailing club. Spring tides, the biggest of the month, are marked in bold: on those days the mud below the hide is covered completely. :::paragraphs{style="tides"} **Sun 6 Dec** 07:42 and 20:05 · **Sat 12 Dec** 12:18 · **Sun 20 Dec** 06:55 and 19:20 **Sun 3 Jan** 06:31 and 18:54 · **Sat 9 Jan** 11:47 · **Sun 17 Jan** 06:10 and 18:36 **Sun 31 Jan** 05:58 and 18:22 · **Sat 6 Feb** 10:59 · **Sun 14 Feb** 05:37 and 18:01 ::: Times are for a still day. A strong westerly holds the water up and brings high tide a little early; a long dry spell on the moors does the opposite. If you are walking out on the mud, go on a falling tide and come back with an hour to spare. :::paper{type=cardStock grammage=250 shade=#f2c9b4} # Join us {style="back-matter" kicker="The group"} We walk the river path on the first Sunday of every month, meeting at the sailing club at ten. Nobody needs to know anything about birds, boats or tides; most of us didn’t when we started. Bring boots. The next issue comes out in June. If you have something to say about the estuary, a drawing, a memory or a count of anything at all, leave it in the box behind the bar at the Duke of Cornwall, or write to us at the sailing club. :::paragraphs{style="credits"} Set in Newsreader and Bricolage Grotesque (SIL Open Font License) · Text: original, CC BY 4.0 · The Pill Estuary Group and the people in this issue are fictional. ::: # Low Tide {style="back" strap="Free · take one, leave one"} :::
`; // content.<lang>.md, inlined by the Cookbook // #endregion // ─── 3 · Fonts ────────────────────────────────────────────────────────────── const FONTS = { Newsreader: ['400', '400i', '700'], 'Bricolage Grotesque': ['400', '700', '800'], }; // ─── 4 · Build & show ─────────────────────────────────────────────────────── const doc = await buildDocumentWithFonts({ markdown }, config(), kitFonts(FONTS)); showPages(doc, { title: 'Low Tide · a stapled zine in Folio' }); // #region answer: the zine as a stapled booklet, closed on the desk until the cover turns const stage = document.createElement('section'); stage.id = 'folio'; stage.ariaLabel = 'The zine in 3D'; stage.style.cssText = 'height: min(78vh, 720px); margin: 0 auto; max-width: 1280px'; document.getElementById('pages').before(stage); // Covers from the pages need an even count: page 8 must fall on a verso to be the back. if (doc.pages.length % 2) console.warn(`${doc.pages.length} pages: the back cover is missing`); createFolioFromDocument(stage, doc, { appearance: { textureBaseUrl: 'https://postext.dev/folio/textures' }, onChange: ({ pages }) => kitStatus(pages.map((i) => `p. ${i + 1} on ` + (doc.pages[i].paper ? 'card' : 'newsprint')).join(' · ')), }); // #endregion
Kit · core, fonts, viewer: uguale in ogni ricetta · 224 righe// ─── Kit ── helpers shared by every Cookbook recipe · postext.dev/cookbook ───── // ─── Kit · core v1 ── the same in every recipe · postext.dev/cookbook function mm(value) { return { value, unit: 'mm' }; } function pt(value) { return { value, unit: 'pt' }; } function em(value) { return { value, unit: 'em' }; } /** The sample language's string: t({ en: 'Figure', es: 'Figura' }). */ function t(strings) { return strings[LANG] ?? Object.values(strings)[0]; } /** A file in this recipe's assets folder, served from the Postext repo by jsDelivr. */ function asset(file) { return `https://cdn.jsdelivr.net/gh/drnachio/postext@main/cookbook/${RECIPE}/assets/${file}`; } // ─── Kit · fonts v3 ── the same in every recipe · postext.dev/cookbook // Postext loads the faces a config asks for before layout (prepareFonts, // buildDocumentWithFonts) and asks the kit for the files: Fontsource's, the // files the PDF embeds too. /** The options of buildDocumentWithFonts / prepareFonts / withLoadedFonts: * the Fontsource resolver, and the faces of FONTS loaded with the ones the * config names. FONTS = { 'Family Name': ['400', '400i', '700'] }. */ function kitFonts(FONTS = {}) { const faces = Object.entries(FONTS).flatMap(([family, specs]) => specs.map((spec) => ({ family, weight: parseInt(spec, 10), style: spec.endsWith('i') ? 'italic' : 'normal' }))); return { resolve: fontsourceResolver, faces }; } /** A face's Fontsource files, in a fixed order: latin, then latin-ext and greek * when `text` needs them and the family ships them (kitSubsetsFor), each with * its unicode-range. Null for a weight or slant Fontsource does not ship. */ async function fontsourceResolver(family, weight, style, { text = '' } = {}) { const id = fontsourceId(family); const meta = await fontsourceMeta(family); if (meta && !(meta.weights.includes(weight) && meta.styles.includes(style))) return null; kitStatus('Loading fonts…'); const at = await fontsourceAt(id); return ['latin', ...kitSubsetsFor(text, meta)].map((subset) => ({ source: `${at}files/${id}-${subset}-${weight}-${style}.woff2`, unicodeRange: kitRanges()[subset], })); } /** The unicode-range of each Fontsource file the kit loads (a function: the * kit is inlined after the pen's code, which runs before a const would be set). */ function kitRanges() { return { latin: 'U+0000-00FF,U+0131,U+0152-0153,U+02BB-02BC,U+02C6,U+02DA,U+02DC,U+0304,U+0308,U+0329,' + 'U+2000-206F,U+20AC,U+2122,U+2191,U+2193,U+2212,U+2215,U+FEFF,U+FFFD', 'latin-ext': 'U+0100-02BA,U+02BD-02C5,U+02C7-02CC,U+02CE-02D7,U+02DD-02FF,U+0304,U+0308,U+0329,' + 'U+1D00-1DBF,U+1E00-1E9F,U+1EF2-1EFF,U+2020,U+20A0-20AB,U+20AD-20C0,U+2113,U+2C60-2C7F,U+A720-A7FF', greek: 'U+0370-03FF', }; } /** For the cjk, arabic and comics blocks, which add their own files: the * faces of `faces` = { 'Family Name': ['400', '400i', '700'] }. `text` is the sample: * č ł † α χ also load latin-ext and greek files (kitSubsetsFor). With * `optional`, a face Fontsource does not ship is skipped instead of failing. * The files are added once all have loaded, in the order asked (latin, then * latin-ext, then greek), whatever order they arrive in: the last one added * wins a character two files hold. Resolves to the number of faces added. */ async function loadFonts(faces, text = '', { optional = false } = {}) { kitStatus('Loading fonts…'); const ranges = kitRanges(); const jobs = []; for (const [family, specs] of Object.entries(faces)) { const id = fontsourceId(family); const todo = [...new Set(specs)].map((spec) => [parseInt(spec, 10), spec.endsWith('i') ? 'italic' : 'normal']) .filter(([weight, style]) => !hasFace(family, weight, style)); // before any await const at = fontsourceAt(id); const meta = optional || /[^\0-ÿ]/u.test(text) ? await fontsourceMeta(family) : null; const subsets = ['latin', ...kitSubsetsFor(text, meta)]; for (const [weight, style] of todo) { if (optional && !(meta?.weights.includes(weight) && meta.styles.includes(style))) continue; for (const subset of subsets) { const url = `${await at}files/${id}-${subset}-${weight}-${style}.woff2`; const face = new FontFace(family, `url(${url}) format('woff2')`, { weight: String(weight), style, unicodeRange: ranges[subset] }); jobs.push(face.load().catch(() => { if (subset === 'latin' && !optional) throw new Error(`Fontsource has no ${family} ${weight} ${style}`); })); } } } const ready = await Promise.all(jobs).catch((error) => { kitFail(error); throw error; }); return ready.filter((face) => face && document.fonts.add(face)).length; } /** A loaded FontFace covers this family, weight and style (fonts.check() would * also say yes for families nobody declared). */ function hasFace(family, weight, style) { for (const face of document.fonts) { if (face.status !== 'loaded' || face.style !== style) continue; if (face.family.replace(/^["']|["']$/g, '') !== family) continue; const [low, high = low] = face.weight.split(' ').map(Number); if (weight >= low && weight <= high) return true; } return false; } /** The files beyond latin `text` needs that `meta`'s family ships. */ function kitSubsetsFor(text, meta) { return [[/[Ā-˿ᴀ-ᶿḀ-ỿ†ℓⱠ-Ɀ꜠-ꟿ]/u, 'latin-ext'], [/[Ͱ-Ͽ]/u, 'greek']] .filter(([re, x]) => re.test(text) && meta?.subsets?.includes(x)).map(([, x]) => x); } /** Fontsource's id for a family: 'Source Serif 4' → 'source-serif-4'. */ function fontsourceId(family) { return family.toLowerCase().replace(/\s+/g, '-'); } /** A package's folder at the one version @5 names now (#766). */ function fontsourceAt(id, u = `https://cdn.jsdelivr.net/npm/@fontsource/${id}@`) { return (fontsourceAt.c ??= {})[id] ??= fetch(u + '5/package.json').then((r) => r.json()) .then((p) => p.version || 5, () => 5).then((v) => `${u}${v}/`); } /** The family's Fontsource metadata (weights, styles, subsets), or null. */ function fontsourceMeta(family) { fontsourceMeta.cache ??= new Map(); const id = fontsourceId(family); if (!fontsourceMeta.cache.has(id)) { fontsourceMeta.cache.set(id, fetch(`https://api.fontsource.org/v1/fonts/${id}`) .then((res) => (res.ok ? res.json() : null), () => null)); } return fontsourceMeta.cache.get(id); } // ─── Kit · viewer v2 ── the same in every recipe · postext.dev/cookbook /** The pages as spreads on a dark desk, page 1 alone, then verso | recto, * each painted when it scrolls near. Other options go to renderPageToCanvas. */ function showPages(docs, { title, width = 460, ...render } = {}) { const root = viewer(title); const pages = [docs].flat().flatMap((doc) => doc.pages.map((page) => ({ doc, page, n: (doc.pageIndexOffset ?? 0) + page.index }))); const spreads = []; let verso = null; for (const p of pages) { if (p.n % 2 === 1) { if (verso) spreads.push([verso, null]); verso = p; } else { spreads.push([verso, p]); verso = null; } } if (verso) spreads.push([verso, null]); const density = Math.min(window.devicePixelRatio || 1, 2); showPages.painter?.disconnect(); const painter = new IntersectionObserver((entries) => { for (const { isIntersecting, target } of entries) { if (!isIntersecting) continue; painter.unobserve(target); const { doc, page } = target.postext; renderPageToCanvas(page, doc, target, { ...render, scale: (width * density) / page.width }); } }, { rootMargin: '800px' }); showPages.painter = painter; root.replaceChildren(...spreads.map((pair) => { const spread = document.createElement('div'); spread.className = 'pt-spread'; for (const p of pair) { const figure = document.createElement('figure'); if (p) { const label = p.page.pageLabel || String(p.n + 1); const canvas = document.createElement('canvas'); canvas.postext = p; canvas.style.aspectRatio = `${p.page.width} / ${p.page.height}`; canvas.setAttribute('role', 'img'); canvas.setAttribute('aria-label', `Page ${label}`); const folio = document.createElement('figcaption'); folio.textContent = label; figure.append(canvas, folio); painter.observe(canvas); } else figure.className = 'pt-blank'; spread.append(figure); } return spread; })); kitStatus(`${pages.length} ${pages.length === 1 ? 'page' : 'pages'}`); document.documentElement.dataset.postext = 'ready'; return pages.length; } /** The desk, the bar and the error reporting, created once. */ function viewer(title) { if (!document.getElementById('pt-kit')) { document.head.insertAdjacentHTML('beforeend', `<style id="pt-kit"> :root { color-scheme: dark; } body { margin: 0; background: #0e1014; color: #b9bcc4; font: 13px/1.45 system-ui, sans-serif; } #pt-bar { position: sticky; top: 0; z-index: 1; display: flex; flex-wrap: wrap; align-items: center; gap: 6px 16px; padding: 10px 16px; background: rgb(14 16 20 / .92); backdrop-filter: blur(6px); border-bottom: 1px solid #23262d; } #pt-bar strong { color: #f4f1ea; font-weight: 600; } #pt-actions { display: flex; gap: 12px; margin-left: auto; } #pt-actions a, #pt-actions button { color: #d8a21a; font: inherit; background: none; border: 0; padding: 0; cursor: pointer; } #pages { display: grid; justify-items: center; gap: 48px; padding: 32px 16px 72px; } .pt-spread { display: flex; } .pt-spread figure { margin: 0; width: min(460px, 44vw); } .pt-spread canvas { display: block; width: 100%; background: #fff; box-shadow: 0 1px 2px rgb(0 0 0 / .5), 0 22px 44px -16px rgb(0 0 0 / .8); } .pt-spread figure:first-child canvas { box-shadow: inset -14px 0 14px -14px rgb(0 0 0 / .18), 0 1px 2px rgb(0 0 0 / .5), 0 22px 44px -16px rgb(0 0 0 / .8); } .pt-spread figcaption { margin-top: 10px; text-align: center; font: 600 10px/1 system-ui, sans-serif; letter-spacing: .18em; text-transform: uppercase; color: #6c7079; } .pt-blank { visibility: hidden; } @media (max-width: 760px) { .pt-spread { flex-direction: column; gap: 32px; } .pt-spread figure { width: min(460px, 92vw); } .pt-blank { display: none; } } </style>`); document.body.insertAdjacentHTML('afterbegin', '<header id="pt-bar"><strong id="pt-title"></strong><span id="pt-status" role="status"></span><span id="pt-actions"></span></header>'); document.getElementById('pt-title').textContent = document.title || 'Postext'; addEventListener('error', (event) => kitFail(event.error ?? event.message)); addEventListener('unhandledrejection', (event) => kitFail(event.reason)); } if (title) document.getElementById('pt-title').textContent = title; return document.getElementById('pages') ?? document.body.appendChild(Object.assign(document.createElement('main'), { id: 'pages' })); } function kitStatus(text) { viewer(); document.getElementById('pt-status').textContent = text; } function kitFail(error) { document.documentElement.dataset.postext = 'error'; kitStatus(`Error: ${error?.message ?? error}`); } // ─── /Kit ───────────────────────────────────────────────────────────────────────

Lo script.js composto funziona così com’è: incollalo nello script modulo di qualsiasi pagina, oppure apri la ricetta su CodePen. Cartella della ricetta su GitHub ↗ (si apre in una nuova scheda)

Varianti

#Una copertina più pesante e bianca

Cambia entrambe le recinzioni e il foglio di copertina diventa un cartoncino bianco da 300 g; le pagine interne restano su carta da giornale.

-:::paper{type=cardStock grammage=250 shade=#f2c9b4}
+:::paper{type=cardStock grammage=300 shade=#fbfaf6}

#Un opuscolo semplice

Togli le recinzioni e la copertina diventa un foglio della stessa carta da giornale delle pagine, un po’ più pesante. Il campionario di carte fa il contrario e dà a ogni foglio una carta propria.

Errori frequenti

Errore frequente

cover: 'pages' richiede un numero pari di pagine per il piatto posteriore

Con binding.cover impostato su 'pages' la prima pagina si gira come piatto anteriore e l’ultima come piatto posteriore solo quando cade su un verso. Un numero dispari di pagine lascia il libro con la copertina anteriore e senza quella posteriore: portalo a un numero pari oppure tieni la copertina rigida (case). Carta, rilegatura, scrivania e luce (Folio) →

Errore frequente

Una sequenza di carta comincia e finisce su una nuova pagina, e un foglio prende la carta del suo recto

Il testo dentro :::paper comincia su una nuova pagina, e così anche il testo che lo segue. Un foglio ha due pagine e una sola carta, quella del suo recto, quindi una sequenza che comincia su un verso stampa quella pagina sulla carta del foglio di cui fa parte. Metti un’interruzione di pagina dove una sequenza deve cominciare su un recto. Una carta diversa per una sequenza di pagine →

Errore frequente

Il visualizzatore Folio prende le dimensioni del suo contenitore

createFolioFromDocument adatta il libro all’elemento che gli passi, con i pulsanti e il conteggio delle pagine nei margini. Un elemento senza altezza (un div vuoto nel flusso normale) non mostra nulla: dagli un’altezza in CSS. Visualizzatore di libri in 3D (Folio) →

Crediti

Testo
Testo originale, CC BY 4.0
Immagini
  • The tide lines on the cover, drawn in code · Ignacio Ferro · CC BY 4.0
Font
Newsreader (SIL OFL 1.1) · Bricolage Grotesque (SIL OFL 1.1)
Sandbox