# Riquadri che si spezzano, flottano e restano fissi

> Una scheda di laboratorio in cui la procedura si divide tra due colonne, la scheda dati va in testa alla pagina dopo e un badge resta fisso al piede.

- Versione HTML: https://postext.dev/it/cookbook/boxes-split-float-pin
- Ricetta Nº 021 · Riquadri e note · Livello 3 (Avanzato) · Output: Canvas
- Generi: Quaderni ed eserciziari, Libri di testo
- Richiede postext ≥ 1.25.0 · provata con 1.31.0 il 2026-10-13
- Pagine: [41](https://postext.dev/cookbook/boxes-split-float-pin/es/p01.webp?v=edd82f79), [42](https://postext.dev/cookbook/boxes-split-float-pin/es/p02.webp?v=edd82f79), [43](https://postext.dev/cookbook/boxes-split-float-pin/es/p03.webp?v=edd82f79), [44](https://postext.dev/cookbook/boxes-split-float-pin/es/p04.webp?v=edd82f79)
- Apri nella Sandbox: https://postext.dev/it/sandbox#recipe=boxes-split-float-pin&lang=es (.postext: https://postext.dev/cookbook/boxes-split-float-pin/es/boxes-split-float-pin.postext)
- Ultimo aggiornamento: 2026-10-10
- Altre lingue: [en](https://postext.dev/en/cookbook/boxes-split-float-pin.md), [es](https://postext.dev/es/cookbook/boxes-split-float-pin.md), [ca](https://postext.dev/ca/cookbook/boxes-split-float-pin.md), [de](https://postext.dev/de/cookbook/boxes-split-float-pin.md), [fr](https://postext.dev/fr/cookbook/boxes-split-float-pin.md), [pt](https://postext.dev/pt/cookbook/boxes-split-float-pin.md), [zh](https://postext.dev/zh/cookbook/boxes-split-float-pin.md), [ja](https://postext.dev/ja/cookbook/boxes-split-float-pin.md), [ar](https://postext.dev/ar/cookbook/boxes-split-float-pin.md)

## In breve

Una scheda scolastica di scienze su come costruire una meridiana. Mostra riquadri che si spezzano tra due colonne, passano in cima alla pagina successiva o restano fissi in fondo a una pagina.

## Che cosa comporrai

Quattro pagine di *Taller de ciencias*, un quaderno di laboratorio scolastico spagnolo: la Práctica 4, «Construye un reloj de sol» (Costruisci una meridiana). A pagina 42 l’avviso di sicurezza resta in un solo pezzo, e la procedura di quattordici passi, su un fondo giallo con un badge a bussola sull’angolo, comincia al piede della colonna sinistra e prosegue in testa a quella destra, alla stessa giustezza e senza titolo. La scheda dati di Madrid esce dal flusso su quella pagina e apre pagina 43, sopra il testo. Un badge antracite «Autoevaluación · 10 min» resta fisso al piede dell’ultima pagina, con una mano che indica nel margine accanto. Nell’apertura, una fascia giallo sole porta un grafico delle ombre di un bastone da mezzogiorno alle 17 alla latitudine di Madrid, e un pannello elenca i materiali in tre colonne a tutta larghezza.

**Questa ricetta risponde a:**

- Come lascio spezzare un riquadro lungo, faccio flottare un altro alla pagina dopo e fisso un badge al piede?
- Come faccio flottare un riquadro in testa o al piede della pagina mentre il testo continua a scorrere?
- Come fisso un badge o un adesivo in una posizione fissa della pagina?
- Come compongo un riquadro su entrambe le colonne a metà pagina, come un pannello «in cifre» a tre elementi?

## La risposta breve

```js
// script.js, righe 54–84
const HAND = 6, GAP = 2, RULE = 0.75 * PT; // mm: the badge's hand, its gap, the hand's rule
const calloutStyles = [
  // Kept whole: keepTogether defaults to true, so a box that does not fit the rest of a
  // column moves on in one piece; only a box taller than a whole column splits anyway.
  box('seguridad', bar('charcoal', 'aviso')),
  // Split: the procedure fits a column, so it takes keepTogether: false to break where it
  // falls instead of moving on whole: between steps, or inside one (splitMinLines, default
  // 2, keeps at least two lines of the step on each side of the cut). The rest goes on in
  // the next column or page without the title or the icon; a corner icon takes no room
  // from the text, where an inline one would keep its column in both parts.
  box('pasos', { keepTogether: false, background: col('light'),
    icon: icon('compas', 7, { position: 'corner', cornerSide: 'outer' }) }),
  // Floated: its fence adds placement="top", so the box leaves the flow where the fence
  // stands and heads the next page, while the text after it fills this one.
  box('datos', { span: 'page', background: col('tint') }),
  // Pinned: 'fixed' sets the box on the page where its fence falls, at the bottom-left corner
  // of the text block unless fixed.anchor says otherwise, and the column text keeps out of it.
  // width: 'auto' shrink-wraps the title, so an empty fence prints a badge.
  box('autoevaluacion', { placement: 'fixed', width: 'auto',
    // Hang the hand and its rule in the margin (the outer one on this verso), so the badge
    // itself lines up with the text: [hand][rule][GAP][badge].
    fixed: { offset: { x: mm(-(HAND + RULE + GAP)) } },
    marker: { ...icon('mano', HAND), gap: mm(GAP),
      rule: { enabled: true, color: col('charcoal'), width: mm(RULE) } },
    background: col('charcoal'), borderRadius: mm(3.2),
    padding: { top: mm(1.6), right: mm(3.4), bottom: mm(1.6), left: mm(3.4) },
    titleStyle: { ...TITLE, color: col('paper') } }),
  // Across the page, in the flow: the text above it is cut level and resumes under it.
  box('resumen', { span: 'page', backgroundEnabled: false,
    stripe: { enabled: true, side: 'top', width: pt(2.5), color: col('charcoal') } }),
];
```

## Ingredienti

**Insegna**

- [Riquadri che si dividono o restano interi](https://postext.dev/it/docs/configuration-styles.md#il-contenitore-callout): I riquadri lunghi si dividono tra colonne e pagine, mantenendo la cornice e lasciando cadere il titolo; quelli brevi si spostano interi; uno che non entra da nessuna parte genera calloutOverflow.
- [Riquadri flottanti](https://postext.dev/it/docs/configuration-styles.md#il-contenitore-callout): Un riquadro che lascia il flusso per la testa o il piede di una pagina mentre il testo continua a riempire la pagina da cui è uscito.
- [Riquadri e badge fissati](https://postext.dev/it/docs/configuration-styles.md#il-contenitore-callout): Un riquadro fissato in un punto esatto della sua pagina, stretto attorno al suo titolo se serve; ogni colonna di testo che si sovrappone a esso si accorcia per lasciarlo libero.

**Usa anche**

- [Colonna segnaletica accanto a un riquadro](https://postext.dev/it/docs/configuration-styles.md#stili-dei-riquadri)
- [Riquadri a tutta larghezza](https://postext.dev/it/docs/configuration-styles.md#il-contenitore-callout)
- [Colonne in un riquadro o nel testo](https://postext.dev/it/docs/document-format.md#columns)
- [Icone dei riquadri e badge d’angolo](https://postext.dev/it/docs/configuration-styles.md#stili-dei-riquadri)
- [Riquadri](https://postext.dev/it/docs/configuration-styles.md#stili-dei-riquadri)
- [Spazio verticale esplicito](https://postext.dev/it/docs/document-format.md#space)
- [Citazioni che collocano le figure](https://postext.dev/it/docs/document-format.md#rimando-in-linea-la-forma-principale)
- [Collocazione delle figure](https://postext.dev/it/docs/document-format.md#collocazione)
- [Bilanciamento delle colonne](https://postext.dev/it/docs/configuration-text.md#bilanciamento-delle-colonne)
- [Tabelle dai dati](https://postext.dev/it/docs/document-format.md#inserimento-di-blocco-facoltativo-collocazione-in-linea-esplicita)
- [Stili di tabella con nome](https://postext.dev/it/docs/configuration-resources.md#stili-di-tabella-con-nome)
- [Figura e Tabella nella tua lingua](https://postext.dev/it/docs/configuration-resources.md#tipi-di-risorsa)
- [Titoli numerati](https://postext.dev/it/docs/configuration-text.md#sovrascritture-per-livello)
- [Aperture progettate](https://postext.dev/it/docs/configuration-text.md#estensione-e-progetto-grafico-avanzato)
- [Attributi dei titoli](https://postext.dev/it/docs/document-format.md#attributi-dei-titoli)
- [Testatine e folio](https://postext.dev/it/docs/configuration-page-layout.md#intestazioni-e-piè-di-pagina)
- [Avvisi e diagnostica](https://postext.dev/it/docs/configuration-fonts-colors-viewers.md#debug)
- [Figure proprio qui](https://postext.dev/it/docs/document-format.md#inserimento-di-blocco-facoltativo-collocazione-in-linea-esplicita)
- [Fascia di capitolo a tutta larghezza](https://postext.dev/it/docs/configuration-text.md#estensione-e-progetto-grafico-avanzato)
- [Testatine secondo il ruolo della pagina](https://postext.dev/it/docs/configuration-page-layout.md#elementi-di-testo)
- [Stili di paragrafo](https://postext.dev/it/docs/configuration-styles.md#stili-di-paragrafo)
- [Figure e tabelle come risorse](https://postext.dev/it/docs/document-format.md#risorse)

**La configurazione in breve**

- [`bodyText`](https://postext.dev/it/docs/configuration-text.md#testo-corrente), [`calloutStyles`](https://postext.dev/it/docs/configuration-styles.md#stili-dei-riquadri), [`captionStyle`](https://postext.dev/it/docs/configuration-resources.md#stile-delle-didascalie), [`colorPalette`](https://postext.dev/it/docs/configuration-fonts-colors-viewers.md#tavolozza-dei-colori), [`footer`](https://postext.dev/it/docs/configuration-page-layout.md#intestazioni-e-piè-di-pagina), [`header`](https://postext.dev/it/docs/configuration-page-layout.md#intestazioni-e-piè-di-pagina), [`headings`](https://postext.dev/it/docs/configuration-text.md#titoli), [`layout`](https://postext.dev/it/docs/configuration-page-layout.md#impaginazione), [`locale`](https://postext.dev/it/docs/configuration-text.md#sillabazione), [`orderedLists`](https://postext.dev/it/docs/configuration-text.md#elenchi-numerati), [`page`](https://postext.dev/it/docs/configuration-page-layout.md#pagina), [`paragraphStyles`](https://postext.dev/it/docs/configuration-styles.md#stili-di-paragrafo), [`tableStyle`](https://postext.dev/it/docs/configuration-resources.md#stile-delle-tabelle), [`tableStyles`](https://postext.dev/it/docs/configuration-resources.md#stili-di-tabella-con-nome), [`unorderedLists`](https://postext.dev/it/docs/configuration-text.md#elenchi-puntati)

**API**

- [`buildDocumentWithFonts`](https://postext.dev/it/docs/configuration-programmatic-usage.md#caricare-i-font-prima-dellimpaginazione), [`prepareFonts`](https://postext.dev/it/docs/configuration-programmatic-usage.md#caricare-i-font-prima-dellimpaginazione), [`registerResourceImage`](https://postext.dev/it/docs/architecture.md#superficie-dellapi), [`renderPageToCanvas`](https://postext.dev/it/docs/configuration-programmatic-usage.md#rendere-una-pagina-come-bitmap)

**Caratteri**

- Host Grotesk (OFL-1.1), Commit Mono (OFL-1.1)

## Preparazione

### 1 · Un unico stile di base per ogni riquadro

```js
// script.js, righe 38–50
const TITLE = { fontFamily: LABEL, fontSize: pt(8), color: col('charcoal'),
  textTransform: 'uppercase', letterSpacing: pt(1.2) }; // bold by default
const BOX_TYPE = { fontSize: pt(8.8), lineHeight: pt(12.4) }; // colours inherit bodyText
const box = (id, device) => ({ id, // margins: the defaults, snapped to whole grid lines
  padding: { top: mm(3), right: mm(3.6), bottom: mm(3.4), left: mm(3.6) },
  titleStyle: { ...TITLE, gap: mm(2) }, body: BOX_TYPE,
  lists: { gap: mm(2), itemSpacing: pt(3) }, ...device });
const icon = (id, size, extra) => ({ kind: 'resource', resourceId: id, size: mm(size),
  ...extra });
// A side bar with its icon centred on it, 1.4 mm narrower than the bar.
const bar = (hue, id, width = 5.6) => ({ backgroundEnabled: false,
  stripe: { enabled: true, side: 'left', width: mm(width), color: col(hue) },
  icon: icon(id, width - 1.4) });
```

Ogni stile parte da `box()`: il titolo in monospaziato, un corpo di 8,8 pt contro i 9,4 pt del testo, e il padding. Ciascuno aggiunge poi un solo segno: una barra antracite sull’avviso di sicurezza, un fondo giallo sulla procedura, una tinta carta sulla scheda dati, un filetto superiore sul riepilogo, una pillola antracite per il badge. Uno studente distingue i riquadri da quel segno prima di leggerne il titolo. Sulla procedura il fondo segnala anche la continuazione: la parte in testa alla colonna destra non ha titolo, quindi solo il giallo la lega ai passi da 1 a 4 al piede di quella sinistra.

### 2 · Tenere unito, spezzare, far flottare e fissare

Il codice è nella [risposta breve](https://postext.dev/it/cookbook/boxes-split-float-pin.md#la-risposta-breve) qui sopra. La procedura misura 192 mm e una colonna ne contiene 211, quindi un riquadro tenuto unito salterebbe in testa alla colonna destra e lascerebbe 43 mm vuoti al piede di quella sinistra. `keepTogether: false` lo lascia cominciare dove cade e proseguire nella colonna successiva, o nella pagina successiva, come mostra la seconda variante ([il contenitore `:::callout`](https://postext.dev/it/docs/configuration-styles.md#il-contenitore-callout)). Una continuazione perde il titolo e l’icona. Un’icona in linea sottrarrebbe ai passi una colonna propria in entrambe le parti, quindi la bussola sta sull’angolo esterno del riquadro, dove non occupa larghezza. Il recinto della scheda dati porta `placement="top"`, così il riquadro esce dal flusso a [pagina 42](https://postext.dev/cookbook/boxes-split-float-pin/es/p02.webp?v=edd82f79) e apre la pagina successiva. Il badge è `fixed`, e un `offset.x` negativo sposta la mano e il suo filetto nel margine esterno, così il bordo sinistro del badge si allinea al testo.

### 3 · Attraversa la pagina con un pannello a tre colonne

```js
// script.js, righe 88–93
const materials = box('material', { span: 'page', background: col('sun'),
  marginBottom: pt(LEAD), // one more grid line of air before the text resumes
  padding: { top: mm(3.6), right: mm(4.4), bottom: mm(3.8), left: mm(4.4) },
  columnGap: mm(GUTTER), // the page's gutter: the panel's columns sit as far apart as the text's
  body: { ...BOX_TYPE, paragraphSpacing: false },
  lists: { color: col('ink'), gap: mm(1.8), itemSpacing: pt(1) } });
```

Con `span: 'page'` il riquadro dei materiali attraversa entrambe le colonne. L’introduzione sopra di esso viene tagliata in pari, e il testo riprende su due colonne sotto ([pagina 41](https://postext.dev/cookbook/boxes-split-float-pin/es/p01.webp?v=edd82f79)). Dentro il recinto, `:::columns{count=3 breaks="5,9"}` stabilisce dove comincia ogni colonna invece di bilanciarle. `breaks` conta i blocchi figli, e ogni voce di elenco ne è uno, quindi la seconda e la terza colonna si aprono al quinto e al nono blocco, le etichette in grassetto. `columnGap` è l’intercolonna di 7 mm della pagina, così le colonne del pannello distano tra loro quanto le colonne del testo.

```markdown
:::callout{type="material" title="Necesitarás"}
:::columns{count=3 breaks="5,9"}
**Para la esfera**

- Cartón pluma de 20 × 30 cm
- La plantilla de la esfera
- Pegamento y rotulador

**Para el gnomon y la base**
…
:::
:::
```

### 4 · Un giallo per le campiture, uno più chiaro per il riquadro lungo

```js
// script.js, righe 15–28
const palette = {
  ink: '#1b2430', // text
  charcoal: '#2b2d42', // the safety bar, the summary's rule, the table head, the badge, titles
  sun: '#f2b705', // the opener band and the materials panel: fields, never type
  light: '#fad65a', // the procedure
  tint: '#fff6d6', // the data sheet
  rule: '#d7dde3', // hairlines
  muted: '#5d6b78', // running heads, notes
  paper: '#ffffff',
};
const col = (id) => ({ hex: palette[id], model: 'hex', paletteId: id });
// The engine's defaults link to 'main-color': point it at charcoal, so nothing prints blue.
const colorPalette = Object.entries({ ...palette, 'main-color': palette.charcoal })
  .map(([id, hex]) => ({ id, name: id, value: { hex, model: 'hex' } }));
```

Il giallo sole è riservato alle campiture di colore, la fascia e il pannello, dove l’antracite e l’inchiostro si leggono a 7,4:1 e 8,6:1. Come colore del testo su bianco arriverebbe a 1,8:1, quindi non porta mai testo. La procedura prende `light`, un giallo più pallido: l’inchiostro vi si legge a 11:1, e la divisione si vede ancora nella miniatura della galleria. La scheda dati prende `tint`, più vicino alla carta, così non può essere scambiata per la procedura, l’altro riquadro con fondo della doppia pagina. `main-color` punta all’antracite, così i valori predefiniti che la configurazione non sovrascrive si stampano in antracite invece che nel blu del motore.

### 5 · Apri su una fascia gialla

```js
// script.js, righe 97–119
const BAND = 100; // mm from the trim to the foot of the band
const ART_W = 80; // mm: the width of the band's drawing, at the fore-edge
const AIR = 8; // mm between the band and the first line of text
const [TITLE_W, LEAD_W] = [104, 88]; // mm: the title's and the lead's measure
const onBand = { color: col('charcoal'), align: 'left' }; // a long title or lead wraps
const below = (id, y, width) => ({ anchor: { to: `#${id}`, edge: 'below' },
  offset: { y: mm(y) }, size: { width: mm(width) } });
const opener = { enabled: true, minHeight: mm(BAND - TOP + AIR), slot: { elements: [
  { kind: 'box', id: 'band', style: { backgroundColor: col('sun') },
    placement: { anchor: { to: 'page', edge: 'top-left' }, size: { height: mm(BAND) } } },
  { kind: 'image', id: 'art', resourceId: 'sombras', placement: { anchor: { to: 'page',
    edge: 'top-right' }, size: { width: mm(ART_W), height: mm(BAND) } } },
  { kind: 'text', id: 'kicker', content: '{attr.kicker}', ...onBand, fontFamily: LABEL,
    fontSize: pt(8.5), fontWeight: 700, letterSpacing: pt(1.7), textTransform: 'uppercase',
    placement: { anchor: { to: 'container', edge: 'top-left' }, offset: { y: mm(4) } } },
  { kind: 'text', id: 'title', content: '{titleText}', ...onBand, fontFamily: TEXT,
    fontSize: pt(44), fontWeight: 800, lineHeight: 0.98, placement: below('kicker', 3, TITLE_W) },
  { kind: 'text', id: 'lead', content: '{attr.lead}', ...onBand, fontFamily: TEXT,
    fontSize: pt(10.5), lineHeight: 1.4, placement: below('title', 5, LEAD_W) },
  { kind: 'text', id: 'meta', content: '{attr.meta}', ...onBand, fontFamily: LABEL,
    fontSize: pt(7.4), fontWeight: 700, letterSpacing: pt(1.1), textTransform: 'uppercase',
    placement: below('lead', 4, LEAD_W) },
] } };
```

La fascia è un elemento riquadro ancorato alla pagina e il disegno un elemento immagine sul margine esterno; l’occhiello, il sommario e la riga della sessione vengono da attributi sulla riga del titolo. Poiché la fascia è ancorata alla pagina, l’apertura riserva già la sua altezza fino al piede della fascia, e il testo comincerebbe 3,6 mm sotto. `minHeight: BAND - TOP + AIR` chiede 8 mm; la riserva si aggancia alla riga di griglia successiva, e il testo comincia 8,4 mm sotto la fascia.

## La ricetta completa

Un unico file, composto a partire dalla cartella della ricetta con il testo di esempio e il kit comune del Ricettario già inclusi; costruisce la propria pagina. Per eseguirlo, mettilo in uno `<script type="module">` di una pagina vuota, oppure incollalo nel pannello JS di un nuovo pen di CodePen (come modulo). Importa postext da esm.sh: non c’è niente da installare né da compilare.

- Cartella della ricetta: https://github.com/drnachio/postext/tree/main/cookbook/boxes-split-float-pin

### script.js

```js
// ═══ Postext Cookbook · Nº 021 · Boxes that split, float and pin ══════════════════
// https://postext.dev/en/cookbook/boxes-split-float-pin
// Code: MIT · Text: original (CC BY 4.0) · Drawings: generated in code (CC BY 4.0)
// Fonts: Host Grotesk, Commit Mono (SIL OFL 1.1) · Needs postext ≥ 1.25.0
// Four pages of a school lab workbook in Spanish, and five ways a box can sit on them.
import {
  buildDocumentWithFonts, prepareFonts, renderPageToCanvas, registerResourceImage,
} from 'https://esm.sh/postext';

const LANG = 'es'; // @lang: the language of the sample document (this recipe is Spanish only)
const RECIPE = 'boxes-split-float-pin';

// ─── 1 · Design ─────────────────────────────────────────────────────────────
// #region palette: one yellow for fields, paler ones for two boxes, near-blacks for type and bars
const palette = {
  ink: '#1b2430', // text
  charcoal: '#2b2d42', // the safety bar, the summary's rule, the table head, the badge, titles
  sun: '#f2b705', // the opener band and the materials panel: fields, never type
  light: '#fad65a', // the procedure
  tint: '#fff6d6', // the data sheet
  rule: '#d7dde3', // hairlines
  muted: '#5d6b78', // running heads, notes
  paper: '#ffffff',
};
const col = (id) => ({ hex: palette[id], model: 'hex', paletteId: id });
// The engine's defaults link to 'main-color': point it at charcoal, so nothing prints blue.
const colorPalette = Object.entries({ ...palette, 'main-color': palette.charcoal })
  .map(([id, hex]) => ({ id, name: id, value: { hex, model: 'hex' } }));
// #endregion
const TEXT = 'Host Grotesk'; // text and display
const LABEL = 'Commit Mono'; // labels: kickers, box titles, step numbers, heads, the table
const LEAD = 13.6; // pt: the body leading, the pitch of the baseline grid
const LINES = 44; // grid lines in a full column
const [TRIM_W, TRIM_H, TOP, INNER, OUTER, GUTTER] = [195, 255, 22, 18, 14, 7]; // mm
const PT = 25.4 / 72; // mm in a point

// #region look: the base of every box (a mono title, smaller type), then one device each
const TITLE = { fontFamily: LABEL, fontSize: pt(8), color: col('charcoal'),
  textTransform: 'uppercase', letterSpacing: pt(1.2) }; // bold by default
const BOX_TYPE = { fontSize: pt(8.8), lineHeight: pt(12.4) }; // colours inherit bodyText
const box = (id, device) => ({ id, // margins: the defaults, snapped to whole grid lines
  padding: { top: mm(3), right: mm(3.6), bottom: mm(3.4), left: mm(3.6) },
  titleStyle: { ...TITLE, gap: mm(2) }, body: BOX_TYPE,
  lists: { gap: mm(2), itemSpacing: pt(3) }, ...device });
const icon = (id, size, extra) => ({ kind: 'resource', resourceId: id, size: mm(size),
  ...extra });
// A side bar with its icon centred on it, 1.4 mm narrower than the bar.
const bar = (hue, id, width = 5.6) => ({ backgroundEnabled: false,
  stripe: { enabled: true, side: 'left', width: mm(width), color: col(hue) },
  icon: icon(id, width - 1.4) });
// #endregion

// #region answer: one style per behaviour: kept whole, split, floated, pinned, page-wide
const HAND = 6, GAP = 2, RULE = 0.75 * PT; // mm: the badge's hand, its gap, the hand's rule
const calloutStyles = [
  // Kept whole: keepTogether defaults to true, so a box that does not fit the rest of a
  // column moves on in one piece; only a box taller than a whole column splits anyway.
  box('seguridad', bar('charcoal', 'aviso')),
  // Split: the procedure fits a column, so it takes keepTogether: false to break where it
  // falls instead of moving on whole: between steps, or inside one (splitMinLines, default
  // 2, keeps at least two lines of the step on each side of the cut). The rest goes on in
  // the next column or page without the title or the icon; a corner icon takes no room
  // from the text, where an inline one would keep its column in both parts.
  box('pasos', { keepTogether: false, background: col('light'),
    icon: icon('compas', 7, { position: 'corner', cornerSide: 'outer' }) }),
  // Floated: its fence adds placement="top", so the box leaves the flow where the fence
  // stands and heads the next page, while the text after it fills this one.
  box('datos', { span: 'page', background: col('tint') }),
  // Pinned: 'fixed' sets the box on the page where its fence falls, at the bottom-left corner
  // of the text block unless fixed.anchor says otherwise, and the column text keeps out of it.
  // width: 'auto' shrink-wraps the title, so an empty fence prints a badge.
  box('autoevaluacion', { placement: 'fixed', width: 'auto',
    // Hang the hand and its rule in the margin (the outer one on this verso), so the badge
    // itself lines up with the text: [hand][rule][GAP][badge].
    fixed: { offset: { x: mm(-(HAND + RULE + GAP)) } },
    marker: { ...icon('mano', HAND), gap: mm(GAP),
      rule: { enabled: true, color: col('charcoal'), width: mm(RULE) } },
    background: col('charcoal'), borderRadius: mm(3.2),
    padding: { top: mm(1.6), right: mm(3.4), bottom: mm(1.6), left: mm(3.4) },
    titleStyle: { ...TITLE, color: col('paper') } }),
  // Across the page, in the flow: the text above it is cut level and resumes under it.
  box('resumen', { span: 'page', backgroundEnabled: false,
    stripe: { enabled: true, side: 'top', width: pt(2.5), color: col('charcoal') } }),
];
// #endregion

// #region panel: a yellow panel across both columns with three columns of its own
const materials = box('material', { span: 'page', background: col('sun'),
  marginBottom: pt(LEAD), // one more grid line of air before the text resumes
  padding: { top: mm(3.6), right: mm(4.4), bottom: mm(3.8), left: mm(4.4) },
  columnGap: mm(GUTTER), // the page's gutter: the panel's columns sit as far apart as the text's
  body: { ...BOX_TYPE, paragraphSpacing: false },
  lists: { color: col('ink'), gap: mm(1.8), itemSpacing: pt(1) } });
// #endregion

// #region opener: a sun-yellow band, a shadow chart, texts from the heading line
const BAND = 100; // mm from the trim to the foot of the band
const ART_W = 80; // mm: the width of the band's drawing, at the fore-edge
const AIR = 8; // mm between the band and the first line of text
const [TITLE_W, LEAD_W] = [104, 88]; // mm: the title's and the lead's measure
const onBand = { color: col('charcoal'), align: 'left' }; // a long title or lead wraps
const below = (id, y, width) => ({ anchor: { to: `#${id}`, edge: 'below' },
  offset: { y: mm(y) }, size: { width: mm(width) } });
const opener = { enabled: true, minHeight: mm(BAND - TOP + AIR), slot: { elements: [
  { kind: 'box', id: 'band', style: { backgroundColor: col('sun') },
    placement: { anchor: { to: 'page', edge: 'top-left' }, size: { height: mm(BAND) } } },
  { kind: 'image', id: 'art', resourceId: 'sombras', placement: { anchor: { to: 'page',
    edge: 'top-right' }, size: { width: mm(ART_W), height: mm(BAND) } } },
  { kind: 'text', id: 'kicker', content: '{attr.kicker}', ...onBand, fontFamily: LABEL,
    fontSize: pt(8.5), fontWeight: 700, letterSpacing: pt(1.7), textTransform: 'uppercase',
    placement: { anchor: { to: 'container', edge: 'top-left' }, offset: { y: mm(4) } } },
  { kind: 'text', id: 'title', content: '{titleText}', ...onBand, fontFamily: TEXT,
    fontSize: pt(44), fontWeight: 800, lineHeight: 0.98, placement: below('kicker', 3, TITLE_W) },
  { kind: 'text', id: 'lead', content: '{attr.lead}', ...onBand, fontFamily: TEXT,
    fontSize: pt(10.5), lineHeight: 1.4, placement: below('title', 5, LEAD_W) },
  { kind: 'text', id: 'meta', content: '{attr.meta}', ...onBand, fontFamily: LABEL,
    fontSize: pt(7.4), fontWeight: 700, letterSpacing: pt(1.1), textTransform: 'uppercase',
    placement: below('lead', 4, LEAD_W) },
] } };
// #endregion

// Running heads in the label face, the folio in bold on the outer edge.
const HEAD_Y = 13; // mm from the trim to the heads' baseline area
const RUN_X = OUTER + 9; // mm from the fore-edge to the running title, clear of the folio
const head = (id, content, parity, edge, x, extra) => ({ kind: 'text', id, content, parity,
  pages: 'body', fontFamily: LABEL, fontSize: pt(7.4), letterSpacing: pt(1.1),
  textTransform: 'uppercase', color: col('muted'), ...extra,
  placement: { anchor: { to: 'page', edge }, offset: { x: mm(x), y: mm(HEAD_Y) } } });
const folio = { fontWeight: 700, fontSize: pt(8.5), color: col('ink'), letterSpacing: pt(0) };
const header = { elements: [
  head('verso-folio', '{pageNumber}', 'even', 'top-left', OUTER, folio),
  head('verso-title', '{title}', 'even', 'top-left', RUN_X),
  head('recto-title', 'Práctica {chapterNumber} · {chapterTitle}', 'odd', 'top-right', -RUN_X),
  head('recto-folio', '{pageNumber}', 'odd', 'top-right', -OUTER, folio),
] };
const footer = { elements: [{ ...head('drop', '{pageNumber}', 'all', 'bottom', 0, folio),
  pages: 'opener', // the drop folio, 11 mm above the foot of the opener
  placement: { anchor: { to: 'page', edge: 'bottom' }, offset: { y: mm(-11) } } }] };

const config = () => ({
  // The document's language: captions say "Figura" and "Tabla". The text is ragged and keeps
  // its words whole (bodyText.hyphenation.ragged is off by default).
  locale: 'es',
  colorPalette, header, footer,
  page: { width: mm(TRIM_W), height: mm(TRIM_H), dpi: 150, margins: { top: mm(TOP),
    bottom: mm(TRIM_H - TOP - LINES * LEAD * PT), left: mm(INNER), right: mm(OUTER),
    mirror: true } }, // a text block of LINES whole lines; left is the inner margin on a recto
  // A table set in a box sits right under the text above it, with no extra line of air.
  layout: { layoutType: 'double', gutterWidth: mm(GUTTER), inlineResourceGapInBoxes: false },
  bodyText: { fontFamily: TEXT, fontSize: pt(9.4), lineHeight: pt(LEAD), color: col('ink'),
    boldColor: col('ink'), italicColor: col('ink'), // references follow the bold colour
    textAlign: 'left', firstLineIndent: pt(0), paragraphSpacing: true },
  headings: { fontFamily: TEXT, fontWeight: 800, color: col('ink'), levels: [
    // A recto, so the band's drawing stands at the fore-edge.
    { level: 1, span: 'page', breakBefore: { enabled: true, parity: 'odd' },
      marginTop: pt(0), marginBottom: pt(0), advancedDesign: opener },
    { level: 2, fontSize: pt(13), lineHeight: pt(LEAD), numberingTemplate: '{1}.{2}',
      marginTop: pt(LEAD), marginBottom: pt(0) },
  ] },
  unorderedLists: { color: col('charcoal'), marginTop: pt(0), marginBottom: pt(0) },
  orderedLists: { fontFamily: LABEL, fontWeight: 700, color: col('charcoal') }, // step numbers
  calloutStyles: [...calloutStyles, materials],
  tableStyle: { rules: 'horizontal', borderColor: col('rule'), borderWidth: pt(0.5),
    headerBackground: col('charcoal'), headerColor: col('paper'), headerFontFamily: LABEL,
    headerFontSize: pt(7.4), bodyFontFamily: LABEL, bodyFontSize: pt(7.4),
    bodyColor: col('ink'), cellPadding: mm(1.1) },
  tableStyles: [{ id: 'registro', cellPadding: mm(2.2), bodyFontSize: pt(8.4) }],
  captionStyle: { fontSize: pt(8), color: col('ink'), labelColor: col('charcoal'), gap: mm(2) },
  paragraphStyles: [{ id: 'colofon', fontFamily: LABEL, fontSize: pt(6.6), lineHeight: pt(9.4),
    color: col('muted') }],
});

// ─── 2 · Content ────────────────────────────────────────────────────────────
const markdown = String.raw`---
title: "Taller de ciencias · Cuaderno de prácticas"
---

# Construye un reloj de sol {kicker="Práctica 4 · El Sol y la hora" meta="2 sesiones · por parejas · un día de sol" lead="Con una brocheta y un disco de cartón pluma construirás un reloj de sol ecuatorial. Una vez orientado al norte, casi nunca marcará la misma hora que tu móvil, y en esta práctica verás por qué."}

Los egipcios ya medían las horas con sombras hace más de 3.000 años. Un reloj de sol funciona sin engranajes porque la Tierra gira sobre su eje a un ritmo casi constante, 360° en 24 horas, es decir, 15° cada hora. La sombra de una varilla bien orientada recorre entonces la esfera como la aguja de un reloj, siempre al mismo paso.

Vas a construir el modelo más fácil de trazar, el reloj ecuatorial. Su varilla, el **gnomon**, apunta al polo norte celeste, muy cerca de la estrella Polar, y queda paralela al eje de la Tierra. La esfera es perpendicular a ella y, por tanto, paralela al ecuador; por eso sus líneas horarias se separan 15°, como los radios de una rueda.

:::callout{type="material" title="Necesitarás"}
:::columns{count=3 breaks="5,9"}
**Para la esfera**

- Cartón pluma de 20 × 30 cm
- La plantilla de la esfera
- Pegamento y rotulador

**Para el gnomon y la base**

- Brocheta de 15 cm
- Cartón pluma para la base
- Plastilina

**Herramientas**

- Regla metálica y cúter
- Transportador y compás
- Brújula o la del móvil
:::
:::

## La inclinación es tu latitud

Para que el gnomon quede paralelo al eje terrestre, debe formar con el suelo un ángulo igual a la latitud del lugar: unos 40° en Madrid, 43° en Oviedo, 37° en Sevilla o 28° en Las Palmas de Gran Canaria. Cuanto más al norte vivas, más empinado quedará. Búscala en un atlas o en el mapa del móvil y redondéala al grado; un error de uno o dos grados apenas se nota en la lectura. Anótala: la necesitarás en los pasos 6 y 9.

La esfera, en cambio, forma con el suelo el ángulo complementario, 90° menos la latitud: unos 50° en Madrid. Ese es el ángulo que tendrán los dos triángulos que la sostienen.

Si el gnomon no queda paralelo al eje de la Tierra, la sombra ya no avanza a ritmo constante sobre la esfera y las líneas de 15° dejan de coincidir con las horas. A mediodía el error es nulo, pero crece a medida que te alejas de él, tanto hacia la mañana como hacia la tarde. Por eso el paso 9 pide medir con el transportador el ángulo entre la brocheta y la base, y corregirlo si hace falta.

## Una esfera con dos caras

La plantilla es un disco de 16 cm con 24 radios, uno por hora, separados 15°. Hay que numerarlos en las dos caras, porque el Sol ilumina una u otra según la época del año. En la cara superior, las horas avanzan en el sentido de las agujas del reloj, con las 12 junto a la marca N; en la inferior, que leerás desde abajo, van en sentido contrario.

:::callout{type="seguridad" title="Seguridad"}
- No mires nunca al Sol directamente, ni con gafas de sol ni a través de una lente: basta un instante para dañar la retina.
- El cúter lo maneja un adulto, siempre sobre la regla metálica y con el corte hacia fuera, lejos de los dedos.
- Protege la punta de la brocheta con una bola de plastilina.
:::

Trabajaréis por parejas: mientras uno sujeta las piezas, el otro mide y marca. Leed antes todo el procedimiento, repartid las tareas y tened a mano la hoja de datos de la página siguiente para comprobar las lecturas. Dedicad la primera sesión al montaje y la segunda, a las lecturas.

:::callout{type="datos" placement="top" title="Hoja de datos · Madrid, 40,4° N · 3,7° O"}
:::columns{count=2}
Cuándo marca las 12 tu reloj de sol en Madrid y qué sombra da entonces un palo de un metro.

Para otra localidad, suma 4 minutos por cada grado de longitud al oeste de Madrid; réstalos al este.
:::

:::space

::resource{id="mediodia"}
:::

:::callout{type="pasos" title="Procedimiento"}
1. Pega la plantilla sobre el cartón pluma y déjala secar cinco minutos.
2. Un adulto corta el disco de 16 cm con el cúter, en varias pasadas.
3. Pincha el centro con el compás y agranda el agujero con la brocheta.
4. Comprueba con el transportador que las líneas horarias están separadas 15° y repasa con rotulador las que van de las 6 de la mañana a las 6 de la tarde.
5. Numera las horas en las dos caras, como explica el apartado 4.2.
6. Recorta los dos soportes: triángulos rectángulos con 12 cm de base y, entre la base y la hipotenusa, 90° menos tu latitud (50° en Madrid).
7. Pasa la brocheta por el centro del disco, bien perpendicular a él, hasta que asomen 10 cm arriba y 4 cm abajo.
8. Pega los triángulos de pie sobre la base, a 12 cm uno de otro, de modo que la hipotenusa suba hacia el sur.
9. Apoya el disco en las hipotenusas, pégalo y comprueba que la brocheta forma con la base un ángulo igual a tu latitud.
10. Lleva el reloj a un sitio al que le dé el sol todo el día y nivela la base con el móvil.
11. Gira la base hasta que el extremo alto de la brocheta apunte al norte, con la brújula lejos de objetos de hierro.
12. A una hora en punto, lee la hora en el centro de la sombra y anota también la que marca el móvil.
13. Repite la lectura cada hora, al menos tres veces, y anota las horas en el apartado 4.5.
14. Calcula la diferencia media y compárala con la hoja de datos: si se aleja más de 15 minutos, revisa la orientación.
:::

## Cómo se lee

Lee la hora en el centro de la sombra y no en uno de sus bordes, porque la brocheta tiene grosor. Si la sombra cae entre dos líneas, calcula los minutos a ojo: cada línea es una hora, y cada cuarto de la separación entre dos líneas, 15 minutos. Con práctica apreciarás cinco minutos, casi dos milímetros en el borde del disco. Mira la esfera de frente, sin ladear la cabeza, y siempre desde el mismo lado. Y no esperes que coincida con el móvil: tu reloj marca la hora solar del lugar, y el móvil, la oficial; el apartado 4.4 explica de dónde sale la diferencia.

La sombra cae sobre la cara de la esfera que mira al Sol: la superior entre los equinoccios de marzo y de septiembre, y la inferior el resto del año.

De perfil se ve por qué (:ref{id="reloj"}): en verano, el Sol del mediodía está más alto que la esfera; en invierno, más bajo. Cerca de los equinoccios pasa rozando su plano, y durante unos días la sombra se ve tan borrosa que cuesta leerla, aunque el reloj esté bien montado.

## Hora solar y hora oficial

Tu reloj de sol marca la **hora solar**: las 12 en punto cuando el Sol cruza el meridiano del lugar y alcanza su mayor altura del día. El móvil da la **hora oficial**, la misma en toda la España peninsular. Entre las dos se acumulan tres diferencias.

La primera es el huso horario. La hora oficial española es la de Europa central, calculada para el meridiano 15° E, pero Madrid está a 3,7° al oeste de Greenwich: el Sol cruza su meridiano unos 75 minutos más tarde de lo que supone el reloj. Es una herencia de 1940, cuando España adelantó una hora sus relojes, que desde 1901 seguían la hora de Greenwich, para igualarlos con los de Europa central.

La segunda es el horario de verano, que añade otra hora entre el último domingo de marzo y el último de octubre.

La tercera es la ecuación del tiempo. Como la órbita de la Tierra es una elipse y su eje está inclinado, el mediodía solar se adelanta o se retrasa a lo largo del año, hasta unos 16 minutos en noviembre y 14 en febrero. Con las tres correcciones, en Madrid el Sol pasa por el meridiano entre las 12:58 y las 14:21, según la época del año.

**Un ejemplo.** El 15 de mayo miras el móvil a las 13:00. Según la hoja de datos, ese día el Sol cruza el meridiano de Madrid a las 14:11, así que aún faltan 71 minutos para el mediodía solar: la sombra de tu reloj debería marcar las 10:49. Si marca las 11:05, tu reloj adelanta 16 minutos y conviene revisar su orientación, como indica el paso 14 del procedimiento.

## Tus lecturas

Anota las lecturas de los pasos 12 y 13 en la :ref{id="lecturas" style="full" case="lower"} y compara cada diferencia con la que predice la hoja de datos para ese mes. Si tu reloj falla incluso a mediodía, revisa la orientación; si acierta a mediodía pero falla por la mañana y por la tarde, revisa la inclinación.

## Tu reloj mide la longitud

Los navegantes calculaban su longitud comparando el mediodía solar con la hora de un meridiano de referencia. Tú puedes hacer lo mismo. En el ejemplo, el Sol cruzó el meridiano de Madrid a las 14:11 de verano, es decir, a las 12:11 de Greenwich. Ese día la ecuación del tiempo adelanta el Sol unos 4 minutos, así que el mediodía medio habría llegado hacia las 12:15. Esos 15 minutos de retraso respecto a Greenwich, a 4 minutos por grado, dan unos 3,7° de longitud oeste: la de Madrid. Con tus lecturas no necesitas la ecuación del tiempo: anota la hora oficial a la que tu reloj marca las 12 y compárala con la que da la hoja de datos para ese mes. Cada 4 minutos de retraso te sitúan un grado al oeste de Madrid, y cada 4 de adelanto, un grado al este.

:::callout{type="resumen" title="Resumen"}
:::columns{count=3 breaks="2,3"}
- La Tierra gira 15° cada hora: por eso las líneas horarias de la esfera se separan 15°.
- El gnomon apunta al polo norte celeste, inclinado sobre el suelo tanto como la latitud del lugar.
- Huso horario, horario de verano y ecuación del tiempo apartan la hora solar de la oficial.
:::
:::

Antes de la próxima sesión, haced la autoevaluación de la práctica en el aula virtual y traed vuestras lecturas: con ellas calcularemos la longitud de nuestra localidad.

:::callout{type="autoevaluacion" title="Autoevaluación · 10 min"}
:::

:::paragraphs{style="colofon"}
Compuesto en Host Grotesk y Commit Mono (SIL Open Font License) · Texto y dibujos originales, CC BY 4.0 · Datos solares calculados para 2026.
:::
`; // content.<lang>.md, inlined by the Cookbook

// Madrid's solar noon on the 15th of each month of 2026 (NOAA's approximations; CET, and
// CEST from 29 March to 25 October), the Sun's height then and a 1 m stick's shadow.
const MONTHS = ['Ene', 'Feb', 'Mar', 'Abr', 'May', 'Jun', 'Jul', 'Ago', 'Sep', 'Oct', 'Nov', 'Dic'];
const NOON = ['13:23', '13:29', '13:24', '14:15', '14:11', '14:15', '14:21', '14:20', '14:10',
  '14:00', '13:00', '13:10'];
const HEIGHT = [28, 37, 47, 59, 68, 73, 71, 64, 53, 41, 31, 26]; // degrees
const SHADOW = ['1,86', '1,34', '0,93', '0,60', '0,40', '0,31', '0,34', '0,49', '0,76', '1,14',
  '1,65', '2,02']; // metres
const cell = (content, extra) => ({ content, align: 'center', ...extra });
const row = (label, values) => [cell(label, { align: 'left' }), ...values.map((v) => cell(v))];

// Each drawing's viewBox (in mm for the band's chart), rasterised at PX pixels a unit.
const ART = { sombras: [ART_W, BAND], reloj: [156, 36], aviso: [24, 24], compas: [24, 24],
  mano: [28, 24] };
const PX = 10;
const svgResource = (id, extra) => ({ id, typeId: 'figure', kind: 'svg', createdAt: 0,
  updatedAt: 0, svg: { fileId: `${id}.svg`, width: ART[id][0] * PX, height: ART[id][1] * PX },
  ...extra });
const resources = [
  // Uncited, so never placed: the opener and the box styles use them by id.
  svgResource('sombras'), svgResource('aviso'), svgResource('compas'), svgResource('mano'),
  svgResource('reloj', { placement: { position: 'top', span: 'page' },
    caption: 'El reloj visto desde el este. A mediodía, el Sol ilumina la cara superior en verano '
      + '(izquierda) y la inferior en invierno.',
    altText: 'Perfil del reloj ecuatorial con los rayos del Sol de verano y de invierno' }),
  { id: 'mediodia', typeId: 'table', kind: 'table', createdAt: 0, updatedAt: 0,
    placement: { position: 'here' },
    caption: 'Mediodía solar en Madrid, día 15 de cada mes de 2026. De abril a octubre rige el '
      + 'horario de verano.',
    table: { model: { headerRowCount: 1, columnWidths: [2.9, ...MONTHS.map(() => 1)], rows: [
      [cell('', { isHeader: true }), ...MONTHS.map((m) => cell(m, { isHeader: true }))],
      row('Mediodía solar', NOON),
      row('Altura del Sol', HEIGHT.map((h) => `${h}°`)),
      row('Sombra de 1 m', SHADOW),
    ] } } },
  // The students' log: the worked example, then empty rows tall enough to write in.
  { id: 'lecturas', typeId: 'table', kind: 'table', createdAt: 0, updatedAt: 0,
    placement: { position: 'top' }, caption: 'Tus lecturas. La primera fila es la del ejemplo.',
    table: { styleId: 'registro', model: { headerRowCount: 1, columnWidths: [1, 1, 1], rows: [
      ['Hora oficial', 'Reloj de sol', 'Diferencia'].map((h) => cell(h, { isHeader: true })),
      ['13:00', '10:49', '2 h 11 min'].map((v) => cell(v)),
      ...Array.from({ length: 3 }, () => ['', '', ''].map((v) => cell(v))),
    ] } } },
];

// #region art: the band's shadow chart, the figure and three icons, in the palette (no words)
const n = (v) => +v.toFixed(2);
const svg = (id, body) => `<svg xmlns="http://www.w3.org/2000/svg" width="${ART[id][0] * PX}" `
  + `height="${ART[id][1] * PX}" viewBox="0 0 ${ART[id].join(' ')}">${body}</svg>`;
const path = (d, stroke, width, extra = '') => `<path d="${d}" fill="none" stroke="${stroke}" `
  + `stroke-width="${width}" stroke-linecap="round" stroke-linejoin="round"${extra}/>`;
const shape = (d, fill, extra = '') => `<path d="${d}" fill="${fill}"${extra}/>`;
const dot = (x, y, r, fill) => `<circle cx="${n(x)}" cy="${n(y)}" r="${n(r)}" fill="${fill}"/>`;
const line = (pts) => `M${pts.map(([x, y]) => `${n(x)} ${n(y)}`).join('L')}`;
const rad = (deg) => (deg * Math.PI) / 180;
const LAT = rad(40.4); // Madrid

// Where the tip of a vertical stick's shadow falls on flat ground (x east, y north, in stick
// heights), for the Sun at declination d and hour angle h; null when the Sun is too low.
function tip(d, h) {
  const up = Math.sin(LAT) * Math.sin(d) + Math.cos(LAT) * Math.cos(d) * Math.cos(h);
  if (up < Math.sin(rad(6))) return null;
  const north = Math.cos(LAT) * Math.sin(d) - Math.sin(LAT) * Math.cos(d) * Math.cos(h);
  return [(Math.cos(d) * Math.sin(h)) / up, -north / up];
}
function sombras() { // a stick's shadows from noon to 5 p.m. on flat ground, from above, north up
  const [g, x0, y0] = [36, 7, BAND - 11]; // the stick's height and its foot, mm
  const at = ([x, y]) => [x0 + x * g, y0 - y * g];
  const hours = [12, 13, 14, 15, 16, 17];
  let out = '';
  for (const hour of hours) { // hour lines: straight, from the summer to the winter solstice
    const pts = [-23.44, -11.5, 0, 11.5, 23.44].map((d) => tip(rad(d), rad(15 * (hour - 12))));
    out += path(line(pts.filter(Boolean).map(at)), palette.charcoal, 0.35);
  }
  for (const d of [-23.44, 0, 23.44]) { // date lines: the equinox is straight, the rest curve
    const pts = [];
    for (let m = 0; m <= 84; m += 2) { const p = tip(rad(d), rad(m)); if (p) pts.push(at(p)); }
    out += path(line(pts), palette.charcoal, d === 0 ? 0.5 : 0.35);
  }
  for (const hour of hours) { // the equinox shadows themselves, from the foot of the stick
    out += path(line([[x0, y0], at(tip(0, rad(15 * (hour - 12))))]), palette.charcoal, 1.1);
  }
  return svg('sombras', out + dot(x0, y0, 1.8, palette.charcoal));
}
function reloj() { // the dial in profile, seen from the east: south left, north right
  const panel = (ox, sunDeg, lit) => { // one noon: the Sun at sunDeg above the south horizon
    const [ground, u] = [33, 1.2]; // the ground line; drawing units per centimetre
    const foot = [ox + 42, ground]; // the triangle's north corner, at the hypotenuse's foot
    const up = [-Math.cos(rad(50)), -Math.sin(rad(50))]; // along the hypotenuse, 90° − 40°
    const along = (p, d, k) => [p[0] + d[0] * k, p[1] + d[1] * k];
    const hyp = 12 * u / Math.cos(rad(50)); // a 12 cm base: the hypotenuse's length
    const top = along(foot, up, hyp);
    const mid = along(foot, up, hyp / 2); // the dial's centre
    const g = [Math.cos(LAT), -Math.sin(LAT)]; // the gnomon, up to the north at the latitude
    const s = [-Math.cos(rad(sunDeg)), -Math.sin(rad(sunDeg))]; // towards the Sun
    const sun = along(mid, s, 19);
    const face = along([0, 0], [g[0], g[1]], lit === 'top' ? 0.9 : -0.9); // the lit side
    const board = `<rect x="${n(top[0] - 3 * u)}" y="${ground - 0.7}" `
      + `width="${n(foot[0] - top[0] + 6 * u)}" height="0.7" fill="${palette.charcoal}"/>`; // base
    let out = path(`M${ox} ${ground}H${ox + 74}`, palette.charcoal, 0.4) + board
      + shape(`${line([[foot[0], ground - 0.7], top, [top[0], ground - 0.7]])}Z`, palette.rule)
      + path(line([along(mid, g, -4 * u), along(mid, g, 10 * u)]), palette.charcoal, 0.9)
      + path(line([along(mid, up, -8 * u), along(mid, up, 8 * u)]), palette.charcoal, 1.6)
      + path(line([along(along(mid, up, -8 * u), face, 1), along(along(mid, up, 8 * u), face, 1)]),
        palette.sun, 0.9)
      + path(line([along(mid, g, 10.6 * u), along(mid, g, 17 * u)]), palette.charcoal, 0.35,
        ' stroke-dasharray="1 1.4"') // on to the Pole Star
      + star(...along(mid, g, 18.6 * u), 1.6);
    for (const k of [-1, 0, 1]) { // three rays, travelling from the Sun to the dial
      const start = along(along(sun, [s[1], -s[0]], k * 4), s, -3.6);
      out += path(line([start, along(start, s, -8)]), palette.sun, 0.8);
    }
    return out + dot(...sun, 2.6, palette.sun);
  };
  const star = (x, y, r) => shape(`M${n(x)} ${n(y - r)}L${n(x + r * 0.3)} ${n(y - r * 0.3)} `
    + `${n(x + r)} ${n(y)} ${n(x + r * 0.3)} ${n(y + r * 0.3)} ${n(x)} ${n(y + r)} `
    + `${n(x - r * 0.3)} ${n(y + r * 0.3)} ${n(x - r)} ${n(y)} ${n(x - r * 0.3)} `
    + `${n(y - r * 0.3)}Z`, palette.charcoal);
  return svg('reloj', panel(2, 73, 'top') + panel(82, 26, 'bottom'));
}
function aviso() { // a yellow warning triangle on the charcoal bar
  return svg('aviso', shape('M12 2.6 22.4 20.6H1.6Z', palette.sun, ' stroke-linejoin="round" '
    + `stroke="${palette.sun}" stroke-width="1.6"`)
    + shape('M10.9 8.4h2.2l-.4 6.6h-1.4Z', palette.charcoal)
    + dot(12, 17.4, 1.2, palette.charcoal));
}
function compas() { // the procedure's corner badge: a pair of compasses on a charcoal disc
  return svg('compas', dot(12, 12, 12, palette.charcoal) + dot(12, 6.2, 1.7, palette.light)
    + path('M12 7.4 7.6 18.6M12 7.4 16.4 18.6', palette.light, 1.5)
    + path('M9.2 14.6q2.8 1.5 5.6 0', palette.light, 1));
}
function mano() { // a hand that points right, at the badge
  return svg('mano', shape('M3 9.6h7.4l2.2-2.8a1.6 1.6 0 0 1 2.5 2l-.9 1.2H25a1.6 1.6 0 0 1 0 3.2'
    + 'H16.4v.2h1.2a1.5 1.5 0 0 1 0 3h-1.2a1.5 1.5 0 0 1 0 3h-1.4a1.4 1.4 0 0 1 0 2.8H9.6'
    + 'L3 21.2Z', palette.charcoal));
}
// #endregion

// ─── 3 · Fonts ──────────────────────────────────────────────────────────────
// Every face the design uses: layout measures with the browser's fonts.
const FONTS = { 'Host Grotesk': ['400', '700', '800'], 'Commit Mono': ['400', '700'] };

// ─── 4 · Build & show ───────────────────────────────────────────────────────
const drawings = { sombras, reloj, aviso, compas, mano };
await Promise.all([prepareFonts(markdown, config(), kitFonts(FONTS)),
  ...Object.entries(drawings).map(([id, draw]) => loadSvg(`${id}.svg`, draw()))]);
// Folio 41 is odd like page 1, a recto; the next # is Práctica 4, so the figure is 4.1.
const continuation = { pageNumbering: { startAt: 41 }, headings: { h1: 3 } };
const doc = await buildDocumentWithFonts({ markdown, resources, continuation }, config(),
  kitFonts(FONTS));
showPages(doc, { title: 'Taller de ciencias · Práctica 4' });
// Layout warnings in the bar: a box that no cut could split overflows as calloutOverflow.
const warnings = (doc.warnings ?? []).map((w) => w.kind).join(', ') || 'none';
kitStatus(`${doc.pages.length} pages · layout warnings: ${warnings}`);

// ─── 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 · images v3 ── recipes with pictures · postext.dev/cookbook
/** Registers a photo or PNG for the canvas and keeps its bytes for the PDF.
 *  fetch → ImageBitmap never taints the canvas (a plain cross-origin <img> would). */
async function loadImage(fileId, url) {
  const res = await fetch(url);
  if (!res.ok) throw new Error(`Image not found (${res.status}): ${url}`);
  const bytes = new Uint8Array(await res.arrayBuffer());
  registerResourceImage(fileId, await createImageBitmap(new Blob([bytes])));
  (loadImage.bytes ??= new Map()).set(fileId, bytes);
}

/** Registers SVG markup as a vector image, its fonts inside (inlineSvgFonts, ≥ 1.25).
 *  `options` go to registerResourceImage: { singleInk: true } tints it on the canvas. */
async function loadSvg(fileId, svg, options) {
  if (typeof inlineSvgFonts === 'function') svg = await inlineSvgFonts(svg, svgFaces);
  const img = new Image();
  img.src = `data:image/svg+xml;charset=utf-8,${encodeURIComponent(svg)}`;
  await img.decode();
  registerResourceImage(fileId, img, options);
  (loadImage.bytes ??= new Map()).set(fileId, new TextEncoder().encode(svg));
}

/** A face's Fontsource files: nearest weight; latin + what the SVG sets. */
async function svgFaces(family, weight, style, { codePoints = [] } = {}) {
  const [id, m] = [fontsourceId(family), await fontsourceMeta(family)];
  const w = m?.weights.reduce((a, b) => (Math.abs(b - weight) < Math.abs(a - weight) ? b : a)) ?? weight;
  const s = m?.styles.includes(style) === false ? 'normal' : style;
  return Promise.all(['latin', ...kitSubsetsFor(String.fromCodePoint(...codePoints), m)].map(async (x) => new Uint8Array(
    await (await fetch(`${await fontsourceAt(id)}files/${id}-${x}-${w}-${s}.woff2`)).arrayBuffer())));
}

/** renderToPdf({ resourceBytes: imageBytes }) */
function imageBytes(fileId) { return loadImage.bytes?.get(fileId); }

/** renderToHtml({ resourceImageUrl: imageUrl }) */
function imageUrl(fileId) {
  const bytes = imageBytes(fileId);
  if (!bytes) return undefined;
  imageUrl.urls ??= new Map();
  if (!imageUrl.urls.has(fileId)) {
    const type = /\.svg$/i.test(fileId) ? 'image/svg+xml' : /\.png$/i.test(fileId) ? 'image/png' : 'image/jpeg';
    imageUrl.urls.set(fileId, URL.createObjectURL(new Blob([bytes], { type })));
  }
  return imageUrl.urls.get(fileId);
}

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

## Varianti

### Tieni la procedura in un solo pezzo

Senza l’impostazione, la procedura, che sta in una colonna, passa intera in testa alla colonna destra e lascia 43 mm vuoti al piede di quella sinistra.

```diff
-  box('pasos', { keepTogether: false, background: col('light'),
+  box('pasos', { background: col('light'),
```

### Fai flottare la scheda dati al piede

Con `placement="bottom"` in `content.es.md`, la scheda dati occupa il piede di pagina 42, dove cade il suo recinto. La procedura che sposta comincia in testa alla colonna destra e si divide al giro di pagina, finendo in testa a pagina 43.

```diff
-:::callout{type="datos" placement="top" title="Hoja de datos · Madrid, 40,4° N · 3,7° O"}
+:::callout{type="datos" placement="bottom" title="Hoja de datos · Madrid, 40,4° N · 3,7° O"}
```

## Errori frequenti

- **Un elemento flottante 'top' va alla pagina dopo la sua citazione, a meno che non possa aprire la pagina che lo cita.** Per impostazione predefinita un elemento flottante non va mai sopra il proprio riferimento, quindi un flottante 'top' a tutta larghezza citato a pagina N apre la pagina N+1. Imposta placement.citingPage: true sulla risorsa o sul defaultPlacement del suo tipo (o layout.floatsAtCitingPage per tutto il libro, da postext 1.25) e aprirà invece la pagina N, con il testo sopra il riferimento che scende sotto di esso, quando entra entro layout.maxTopFraction e la riga che lo cita resta su quella pagina. Non apre mai la prima pagina di un capitolo né una pagina dopo un’interruzione forzata: lì apre ancora la pagina successiva, e 'auto' o 'bottom' possono prendere il piede della pagina che lo cita.
- **La pagina 1 è un recto: pianifica le pagine con i numeri fisici.** La pagina 1 è una pagina destra e la pagina 2 il primo verso, quindi pianifica le doppie pagine con i numeri di pagina fisici: un’apertura su una pagina pari sta di fronte alla pagina dispari che la segue.
- **Avviso di impaginazione: Il riquadro esce dalla colonna** (`calloutOverflow`). Un riquadro che nessun taglio può dividere (una figura, una tabella o un riquadro annidato al suo interno più alto della colonna, o uno splitMinLines troppo alto) è stato collocato sforando; il motore lo registra in doc.warnings e la Sandbox lo elenca. Anche un riquadro span: 'side' più alto di una colonna laterale vuota viene collocato sforando. Soluzione: Accorcia il riquadro, lascia che si divida con keepTogether: false, abbassa splitMinLines, oppure dagli un altro span. ([Documentazione](https://postext.dev/it/docs/configuration-styles.md#il-contenitore-callout))

- Un riquadro più alto di un’intera colonna si spezza qualunque cosa dica `keepTogether`. L’impostazione decide solo per i riquadri che stanno in una colonna, come questa procedura di 192 mm in una colonna di 211 mm.
- `splitMinLines` (2 per impostazione predefinita) è il numero minimo di righe che un taglio lascia da ciascun lato, contate nel passo che taglia, così nessun passo lascia una riga da sola. Il testo prima della procedura è calibrato perché il taglio cada tra i passi 4 e 5. Qualsiasi modifica al testo sopra un riquadro spezzato può spostare il taglio, quindi controlla dove cade dopo ognuna.
- Un’icona in linea occupa una colonna propria, e una continuazione mantiene quella colonna, vuota: i passi perdono la larghezza dell’icona in entrambe le parti. Un’icona d’angolo, o una banda laterale su cui l’icona si appoggia, lascia loro l’intera giustezza.
- Un riquadro flottato con `placement="top"` apre la pagina successiva a quella su cui cade il suo recinto, quindi il recinto va nella pagina prima di quella che il riquadro deve aprire. Qui sta subito prima della procedura, a pagina 42.

## Crediti

- Ricetta: Ignacio Ferro ([@drnachio](https://github.com/drnachio))
- Testo: The solar-noon table: computed for Madrid with NOAA's solar equations: NOAA Global Monitoring Laboratory ([fonte](https://gml.noaa.gov/grad/solcalc/solareqns.PDF)), pubblico dominio
- Immagini: The shadow chart, the dial in profile and the icons, drawn in code in the page's palette: Ignacio Ferro, CC-BY-4.0
- Caratteri: Host Grotesk (OFL-1.1), Commit Mono (OFL-1.1)
- Codice: MIT · Contenuto di esempio: CC-BY-4.0

## Ricette correlate

- [Nº 008 · Una famiglia di riquadri da manuale, distinti per colore](https://postext.dev/it/cookbook/textbook-box-family.md): Sei tipi di riquadro da manuale in tre colori, distinti da una banda con icona, un badge, una linguetta numerata, dei pittogrammi o un segno fuori cornice. · Livello 2 (Intermedio) · Libri di testo
- [Nº 027 · Prima pagina di giornale](https://postext.dev/it/cookbook/newspaper-front-page.md): Prima pagina e pagina 2 di un settimanale locale su due colonne con filetti, titoli a tutta pagina come riquadri e brevi su quattro colonne flottanti al piede. · Livello 3 (Avanzato) · Giornali e newsletter
- [Nº 009 · Figure che flottano dove le citi](https://postext.dev/it/cookbook/figures-float-where-cited.md): Un capitolo a due colonne con sette figure numerate: sei flottano dal primo :ref al primo posto che il placement consente; una sta dove la mette ::resource. · Livello 3 (Avanzato) · Libri di testo
