Saltar al contingut principal
Recepta número 57

Receptari · Capítol 1 · Pàgina i retícula

Currículum d'una pàgina amb barra lateral

Un currículum A4 amb la barra pruna com a requadre a la columna esquerra, només per a flotants; unes caixes de la capçalera la prolonguen pels marges.

En aquesta pàgina
Sortida
Canvas
Nivell
Intermedi
Postext
Provada amb Postext 1.4.1
Requereix ≥ 1.4.1
Llicència
Actualitzada el 26 de set. del 2026
Codi MIT · Text CC BY 4.0
  • Mostra en castellà: encara no hi ha edició en català
  • Format 210 × 297 mm
  • Columna i mitja, espai entre columnes de 10 mm
  • Hanken Grotesk 9,3/13,2
  • Hedvig Letters Serif
  • 1 pàgina
  • Nivell
  • Postext 1.4.1
  • Compost en 5 ms
  • 178 línies de codi

En poques paraules

Un currículum d'una pàgina per a una dissenyadora de llibres, amb una franja de color a l'esquerra per a les dades de contacte i les habilitats. Les dates de cada lloc de feina s'alineen a la dreta.

Què compondràs

El currículum d'Irene Salcedo, dissenyadora editorial fictícia establerta a Madrid, en un sol full A4. Una banda pruna de 66 mm d'amplada recorre la vora esquerra de dalt a baix. A dalt de tot, una pila de llibres enquadernats en tela, dibuixada en codi, queda a l'altura de la línia de base del nom. A sota hi ha el contacte, les habilitats i eines en píndoles, les llengües, la formació, la docència i les seves aficions. El nom, en Hedvig Letters Serif de 46 pt, encapçala una columna de text de 118 mm amb un perfil de cinc línies, tres llocs de feina i els llibres que ha dissenyat. Cada lloc de feina porta el càrrec a l'esquerra i les dates a la dreta a la mateixa línia de base, amb l'empresa en cursiva a sota. El primer títol de la barra comparteix línia de base amb la professió, sota el nom.

Aquesta recepta respon a

  • Com compono en columna i mitja, amb una columna de text ampla i una de lateral estreta?
  • Com afegeixo una marca d'aigua, un fons de color o una imatge decorativa a totes les pàgines?
  • Com faig xips en línia: tecles, etiquetes, bancs de paraules per a exercicis?

La resposta curta

script.js · línies 47–67al codi complet
const layout = {
  layoutType: 'oneAndHalf',
  sideColumnPercent: (SIDE / CONTENT) * 100, // 57 of the 185 mm between the margins
  sideColumnSide: 'left',
  sideColumnRole: 'floats', // no body text: only boxes fenced with span="side"
  gutterWidth: mm(GUTTER), // the text column keeps 185 − 57 − 10 = 118 mm
};
// A side box stands where the text has reached at its fence, so the Markdown opens with it,
// before the name, and it starts at the head of the column (gotcha: side-box-starts-at-fence):
//   :::callout{type="sidebar" span="side"}
//   :::callout{type="section" title="Contact"} … :::     ← the sections nest inside it
//   …
//   :::space{lines=2.76}    ← last: runs the band to the column's foot; a fifth of a line
//   :::                       more and the whole box moves to page 2
//   # Irene Salcedo {role="Book designer and art director"}
const sidebar = { id: 'sidebar', background: col('band'),
  // The first title stands on the role's line. The text starts 10 mm from the trim, the
  // left margin plus 1 mm, and stops 10 mm short of the band's right edge.
  padding: { top: mm(ROLE_Y), right: mm(10), bottom: pt(0), left: mm(1) },
  body: { fontFamily: SANS, fontSize: pt(8.4), lineHeight: pt(12.4), color: col('paper'),
    boldColor: col('paper'), paragraphSpacing: false } };

Ingredients

Tipografia
Hedvig Letters Serif, Hanken Grotesk (SIL OFL 1.1)
Recursos
  • The stack of books at the head of the sidebar, drawn in code in the page's palette (Ignacio Ferro, CC BY 4.0)

Elaboració

#1 · Niua les seccions en un sol requadre lateral

script.js · línies 91–102al codi complet
// The sections nest in one side box. As separate side boxes they would stand at least a line
// of paper apart (gotcha: side-boxes-line-apart). A nested box ignores span and flows inside
// its parent (gotcha: nested-callout-limits).
const section = (id, lineHeight) => ({ id, backgroundEnabled: false, // the band shows through
  padding: { top: pt(0), right: pt(0), bottom: pt(0), left: pt(0) },
  marginTop: mm(10),
  titleStyle: { ...caps('glow'), gap: mm(2.4) },
  body: { ...sidebar.body, lineHeight: pt(lineHeight) } });
const chipStyles = [{ id: 'skill', fontFamily: SANS, fontSize: pt(7.6), bold: true,
  background: col('chip'), color: col('paper'), borderWidth: pt(0),
  borderRadius: em(1), // past half the chip's height: a pill
  paddingX: em(0.6), paddingY: em(0.22), gap: em(0.3) }];

Dins de la columna, la banda és el requadre de la resposta curta. S'obre abans del nom, i per això span="side" el col·loca al capdamunt de la columna només per a flotants. Les seccions hi van niuades com a requadres sense fons propi, de manera que el pruna corre sense talls de Contacte a Fora de la feina. Compostes com a requadres laterals solts, queden separades per franges de paper de 4,7 a 9,1 mm, fins i tot amb marginBottom a 0. Les píndoles fan uns 3,9 mm d'alçada. Habilitats i Eines fan servir un estil propi, chips, l'interlineat de 16 pt del qual deixa 1,8 mm entre files; amb els 12,4 pt de les altres seccions, quedarien a mig mil·límetre les unes de les altres (estils de xip).

#2 · Pinta els marges al voltant de la columna

script.js · línies 71–87al codi complet
// Header elements are painted after the text, over it, so these stay in the margins, where
// no text runs (gotcha: header-paints-over-text). They repeat on every page.
const BAND_W = LEFT + SIDE; // mm from the trim edge to the band's right edge
const SEAM = 0.5; // mm of overlap with the box, so no hairline of paper shows between them
const REACH = 1; // mm the foot box climbs above the column's foot, over the end of the box
const bandBox = (id, x, y, w, h) => ({ kind: 'box', id, style: { backgroundColor: col('band') },
  placement: { ...at('page', 'top-left', x, y), size: { width: mm(w), height: mm(h) } } });
const header = { elements: [
  bandBox('head', 0, 0, BAND_W, TOP + SEAM), // above the column: the top margin
  bandBox('edge', 0, 0, LEFT + SEAM, TRIM_H), // beside it: the left margin, top to bottom
  bandBox('foot', 0, TRIM_H - BOTTOM - REACH, BAND_W, BOTTOM + REACH), // below it
  // Painted last, over the box's top padding, where no text runs. The bottom of the stack
  // sits on the name's baseline.
  { kind: 'image', id: 'books', resourceId: 'books', placement: {
    ...at('page', 'top-left', LEFT + 1, TOP + BASELINE - BOOKS.h),
    size: { width: mm(BOOKS.w), height: mm(BOOKS.h) } } },
] };

Com que la columna lateral queda dins dels marges, el requadre s'atura a 9 mm de la vora esquerra i a 18 mm de la superior. Tres caixes de la capçalera omplen el marge superior damunt la columna, l'esquerre de dalt a baix i l'inferior sota la columna. Els elements de la capçalera es pinten després del text i a sobre; per això la recepta els deixa als marges, on no hi ha text. A més, es repeteixen a totes les pàgines, i una segona pàgina també portaria la banda (capçaleres i peus). Les caixes de dalt i de l'esquerra munten 0,5 mm sobre el requadre, i la del peu puja 1 mm per damunt del peu de la columna, sobre el final del requadre, perquè no s'hi vegi un fil de paper a les juntes. El :::space que tanca el requadre l'allarga fins a 278,0 mm, 0,8 mm per damunt del peu de la columna, amb 2,76 línies en anglès i 1,76 en castellà.

#3 · Compon el nom al capdamunt de la columna de text

script.js · línies 106–120al codi complet
const nameplate = { enabled: true, slot: { elements: [
  { kind: 'text', id: 'name', content: '{titleText}', fontFamily: SERIF,
    fontSize: pt(NAME.size), lineHeight: 1, color: col('ink'),
    placement: at('container', 'top-left') },
  { kind: 'text', id: 'role', content: '{attr.role}', ...caps('accent'),
    placement: at('#name', 'below', 0, NAME.gap) },
] } };
const nameLevel = { level: 1, // span stays 'column': the name heads the text column only
  // No break before the name: the sidebar box is already on the page, and a break would
  // move the name and the whole text column to page 2. 1.4.1 drops the H1 break anyway once
  // `headings` is set (gotcha: headings-drop-h1-break); the explicit value keeps the page
  // whole once that default returns.
  breakBefore: { enabled: false },
  marginBottom: pt(LEAD), // one grid line of air under the role
  advancedDesign: nameplate };

L'H1 conserva el span: 'column' per defecte. Amb 'page' es compon com a obertura sobre les dues columnes; com que va darrere del requadre lateral, passa a la pàgina 2, i allà la columna lateral comença sota el nom, a 45,9 mm. breakBefore està desactivat perquè el requadre lateral ja és a la pàgina: amb salt, el nom i tota la columna de text passen a la pàgina 2. El marginBottom del nivell, una línia de la retícula, baixa el perfil de 41,3 a 45,9 mm. El farciment superior del requadre lateral és ROLE_Y, l'alçada del nom més l'aire que el separa de la professió, de manera que el primer títol de la barra i la professió comparteixen línia de base, a 39,3 mm de la vora superior.

#4 · Posa les dates a la línia del càrrec

script.js · línies 132–151al codi complet
const JOB = 9.8; // pt: the job title, the dates and the employer share the size and leading
const jobText = (id, content, look) => ({ kind: 'text', id, content, fontFamily: SANS,
  fontSize: pt(JOB), lineHeight: LEAD / JOB, // a multiple (gotcha: design-lineheight-multiple)
  align: 'left', ...look }); // without align, design text is centred in its box
const job = { enabled: true, slot: { elements: [
  jobText('title', '{titleText}', { fontWeight: 700, color: col('ink'), overflow: 'wrap',
    placement: { ...at('container', 'top-left'), size: { width: mm(84) } } }), // clear of dates
  jobText('dates', '{attr.dates}', { color: col('muted'),
    placement: at('container', 'top-right') }), // the title's top and size, so its baseline
  jobText('org', '{attr.org}', { italic: true, color: col('muted'),
    placement: at('#title', 'below') }),
] } };
const jobLevel = { level: 3, fontSize: pt(JOB), lineHeight: pt(LEAD), marginTop: pt(9),
  marginBottom: pt(0), advancedDesign: job };
// The section head takes 20 pt under 19.6 pt of space, three grid lines, so a job head
// starts the same 9 pt under a grid line after a section head as after a list. The 9 pt
// below it merge with a job head's 9 pt above. Under Selected books they push the list to
// the next grid line, as the default 7 pt would; with 0 it would start one line higher.
const sectionLevel = { level: 2, fontSize: pt(14), lineHeight: pt(20),
  marginTop: pt(3 * LEAD - 20), marginBottom: pt(9), advancedDesign: sectionHead };

Cada lloc de feina és un H3 amb els atributs que porten l'empresa i les dates: ### Directora de arte {org="Ediciones del Albardín, Madrid" dates="2021 – actualidad"} (atributs d'encapçalament). El càrrec i les dates s'ancoren al capdamunt del mateix contenidor, cadascun en una cantonada, amb el mateix cos i interlineat, i per això comparteixen línia de base. El càrrec té una amplada màxima de 84 mm i admet diverses línies: un de llarg salta a la segona abans d'arribar a les dates, i l'empresa baixa amb ell. Cada títol de secció ocupa, amb l'espai de damunt, tres línies justes de la retícula, i així tots els llocs de feina comencen 9 pt per sota d'una línia, tant si vénen d'un títol de secció com d'una llista.

La recepta completa

Sandbox
// ═══ Postext Cookbook · Nº 057 · One-page CV with a sidebar ═══════════════════════
// https://postext.dev/en/cookbook/cv-with-sidebar
// Code: MIT · Text: original (CC BY 4.0) · Drawing: made in code (CC BY 4.0)
// Fonts: Hedvig Letters Serif, Hanken Grotesk (SIL OFL 1.1) · Needs postext ≥ 1.4.1
import {
  buildDocument, renderPageToCanvas, clearMeasurementCache, registerResourceImage,
} from 'https://esm.sh/postext';

const LANG = 'es'; // @lang: the language of the sample document ('en' | 'es')
const RECIPE = 'cv-with-sidebar';

// ─── 1 · Design ─────────────────────────────────────────────────────────────
const palette = {
  ink: '#231b22', // text: a plum-tinted near-black
  band: '#3a2235', // the sidebar
  accent: '#8f3b62', // on paper: the role and the section heads (7.1:1)
  glow: '#f1b98f', // on the band: the sidebar titles (8.3:1)
  chip: '#744d6c', // on the band: the skill chips
  rule: '#d9cdd5', // the hairlines after the section heads
  muted: '#6c5f69', // dates, employers, the colophon (6.0:1)
  paper: '#ffffff', // the page, and the text on the band (14.4:1)
};
// 1.4.1 design elements paint the hex and ignore the paletteId (gotcha: palette-skips-designs).
const col = (id) => ({ hex: palette[id], model: 'hex', paletteId: id });
const colorPalette = Object.entries({ ...palette, 'main-color': palette.accent })
  .map(([id, hex]) => ({ id, name: id, value: { hex, model: 'hex' } }));
const [SERIF, SANS] = ['Hedvig Letters Serif', 'Hanken Grotesk'];
const PT = 25.4 / 72; // mm in a point
const [TRIM_W, TRIM_H] = [210, 297]; // mm: A4, printed on one side, so nothing is mirrored
const [TOP, LEFT, RIGHT] = [18, 9, 16]; // mm
const LEAD = 13.2; // pt: the body leading
const BOTTOM = TRIM_H - TOP - 56 * LEAD * PT; // mm: the text block holds 56 lines
const CONTENT = TRIM_W - LEFT - RIGHT; // 185 mm between the margins
const [SIDE, GUTTER] = [57, 10]; // mm: the sidebar column, and the white after it
const at = (to, edge, x = 0, y = 0) => ({ anchor: { to, edge }, offset: { x: mm(x), y: mm(y) } });
const LABEL = 7.8; // pt: the role and the sidebar titles, tracked capitals
const caps = (colour) => ({ fontFamily: SANS, fontSize: pt(LABEL), fontWeight: 700,
  textTransform: 'uppercase', letterSpacing: pt(LABEL * 0.18), color: col(colour) });
const NAME = { size: 46, gap: 2.6 }; // pt and mm: the name, and the room under it
const ROLE_Y = NAME.size * PT + NAME.gap; // mm under the top margin: the role's line
// mm under the top margin: the name's baseline. Hedvig Letters Serif, set solid, puts it 0.795
// of the size down the line (measured on the page); another face needs its own ratio.
const BASELINE = 0.795 * NAME.size * PT;
const BOOKS = { w: 46, h: 18 }; // mm: the drawing at the head of the band

// #region answer: a float-only column on the left, filled by one box
const layout = {
  layoutType: 'oneAndHalf',
  sideColumnPercent: (SIDE / CONTENT) * 100, // 57 of the 185 mm between the margins
  sideColumnSide: 'left',
  sideColumnRole: 'floats', // no body text: only boxes fenced with span="side"
  gutterWidth: mm(GUTTER), // the text column keeps 185 − 57 − 10 = 118 mm
};
// A side box stands where the text has reached at its fence, so the Markdown opens with it,
// before the name, and it starts at the head of the column (gotcha: side-box-starts-at-fence):
//   :::callout{type="sidebar" span="side"}
//   :::callout{type="section" title="Contact"} … :::     ← the sections nest inside it
//   …
//   :::space{lines=2.76}    ← last: runs the band to the column's foot; a fifth of a line
//   :::                       more and the whole box moves to page 2
//   # Irene Salcedo {role="Book designer and art director"}
const sidebar = { id: 'sidebar', background: col('band'),
  // The first title stands on the role's line. The text starts 10 mm from the trim, the
  // left margin plus 1 mm, and stops 10 mm short of the band's right edge.
  padding: { top: mm(ROLE_Y), right: mm(10), bottom: pt(0), left: mm(1) },
  body: { fontFamily: SANS, fontSize: pt(8.4), lineHeight: pt(12.4), color: col('paper'),
    boldColor: col('paper'), paragraphSpacing: false } };
// #endregion

// #region margins: header boxes paint the band into the margins around the column
// Header elements are painted after the text, over it, so these stay in the margins, where
// no text runs (gotcha: header-paints-over-text). They repeat on every page.
const BAND_W = LEFT + SIDE; // mm from the trim edge to the band's right edge
const SEAM = 0.5; // mm of overlap with the box, so no hairline of paper shows between them
const REACH = 1; // mm the foot box climbs above the column's foot, over the end of the box
const bandBox = (id, x, y, w, h) => ({ kind: 'box', id, style: { backgroundColor: col('band') },
  placement: { ...at('page', 'top-left', x, y), size: { width: mm(w), height: mm(h) } } });
const header = { elements: [
  bandBox('head', 0, 0, BAND_W, TOP + SEAM), // above the column: the top margin
  bandBox('edge', 0, 0, LEFT + SEAM, TRIM_H), // beside it: the left margin, top to bottom
  bandBox('foot', 0, TRIM_H - BOTTOM - REACH, BAND_W, BOTTOM + REACH), // below it
  // Painted last, over the box's top padding, where no text runs. The bottom of the stack
  // sits on the name's baseline.
  { kind: 'image', id: 'books', resourceId: 'books', placement: {
    ...at('page', 'top-left', LEFT + 1, TOP + BASELINE - BOOKS.h),
    size: { width: mm(BOOKS.w), height: mm(BOOKS.h) } } },
] };
// #endregion

// #region sections: boxes nested in the band, one per section; chips for the skills
// The sections nest in one side box. As separate side boxes they would stand at least a line
// of paper apart (gotcha: side-boxes-line-apart). A nested box ignores span and flows inside
// its parent (gotcha: nested-callout-limits).
const section = (id, lineHeight) => ({ id, backgroundEnabled: false, // the band shows through
  padding: { top: pt(0), right: pt(0), bottom: pt(0), left: pt(0) },
  marginTop: mm(10),
  titleStyle: { ...caps('glow'), gap: mm(2.4) },
  body: { ...sidebar.body, lineHeight: pt(lineHeight) } });
const chipStyles = [{ id: 'skill', fontFamily: SANS, fontSize: pt(7.6), bold: true,
  background: col('chip'), color: col('paper'), borderWidth: pt(0),
  borderRadius: em(1), // past half the chip's height: a pill
  paddingX: em(0.6), paddingY: em(0.22), gap: em(0.3) }];
// #endregion

// #region name: the H1 sets the name and the role at the head of the text column
const nameplate = { enabled: true, slot: { elements: [
  { kind: 'text', id: 'name', content: '{titleText}', fontFamily: SERIF,
    fontSize: pt(NAME.size), lineHeight: 1, color: col('ink'),
    placement: at('container', 'top-left') },
  { kind: 'text', id: 'role', content: '{attr.role}', ...caps('accent'),
    placement: at('#name', 'below', 0, NAME.gap) },
] } };
const nameLevel = { level: 1, // span stays 'column': the name heads the text column only
  // No break before the name: the sidebar box is already on the page, and a break would
  // move the name and the whole text column to page 2. 1.4.1 drops the H1 break anyway once
  // `headings` is set (gotcha: headings-drop-h1-break); the explicit value keeps the page
  // whole once that default returns.
  breakBefore: { enabled: false },
  marginBottom: pt(LEAD), // one grid line of air under the role
  advancedDesign: nameplate };
// #endregion

const sectionHead = { enabled: true, slot: { elements: [
  { kind: 'text', id: 'title', content: '{titleText}', fontFamily: SERIF, fontSize: pt(14),
    lineHeight: 20 / 14, color: col('accent'), placement: at('container', 'top-left') },
  { kind: 'rule', id: 'rule', direction: 'horizontal', thickness: pt(0.5), color: col('rule'),
    // 3 mm after the title, level with the middle of its lower case, on to the column's edge
    placement: { ...at('#title', 'right-of', 3, 3.9), size: { width: 'fill' } } },
] } };

// #region jobs: each job heading sets the title left and the dates flush right
const JOB = 9.8; // pt: the job title, the dates and the employer share the size and leading
const jobText = (id, content, look) => ({ kind: 'text', id, content, fontFamily: SANS,
  fontSize: pt(JOB), lineHeight: LEAD / JOB, // a multiple (gotcha: design-lineheight-multiple)
  align: 'left', ...look }); // without align, design text is centred in its box
const job = { enabled: true, slot: { elements: [
  jobText('title', '{titleText}', { fontWeight: 700, color: col('ink'), overflow: 'wrap',
    placement: { ...at('container', 'top-left'), size: { width: mm(84) } } }), // clear of dates
  jobText('dates', '{attr.dates}', { color: col('muted'),
    placement: at('container', 'top-right') }), // the title's top and size, so its baseline
  jobText('org', '{attr.org}', { italic: true, color: col('muted'),
    placement: at('#title', 'below') }),
] } };
const jobLevel = { level: 3, fontSize: pt(JOB), lineHeight: pt(LEAD), marginTop: pt(9),
  marginBottom: pt(0), advancedDesign: job };
// The section head takes 20 pt under 19.6 pt of space, three grid lines, so a job head
// starts the same 9 pt under a grid line after a section head as after a list. The 9 pt
// below it merge with a job head's 9 pt above. Under Selected books they push the list to
// the next grid line, as the default 7 pt would; with 0 it would start one line higher.
const sectionLevel = { level: 2, fontSize: pt(14), lineHeight: pt(20),
  marginTop: pt(3 * LEAD - 20), marginBottom: pt(9), advancedDesign: sectionHead };
// #endregion

const config = () => ({ // a factory: the engine caches resolved configs per object
  colorPalette, layout, chipStyles, header, footer: { elements: [] },
  page: { sizePreset: 'custom', width: mm(TRIM_W), height: mm(TRIM_H), dpi: 150,
    margins: { top: mm(TOP), bottom: mm(BOTTOM), left: mm(LEFT), right: mm(RIGHT) } },
  bodyText: { fontFamily: SANS, fontSize: pt(9.3), lineHeight: pt(LEAD), color: col('ink'),
    italicColor: col('ink'), // the titles in Selected books
    referenceColor: col('ink'), // no :ref yet; one added later prints in ink, not default blue
    textAlign: 'left', firstLineIndent: pt(0), paragraphSpacing: true },
  // Each level draws its own design, but 1.4.1 still measures the hidden heading text in this
  // face. Without it the default is Open Sans, and the page would have to load that face too.
  headings: { fontFamily: SANS, levels: [nameLevel, sectionLevel, jobLevel] },
  unorderedLists: { bulletChar: '–', color: col('ink'), fontWeight: 400, itemSpacing: pt(0),
    marginTop: pt(0), marginBottom: pt(0) },
  paragraphStyles: [
    { id: 'lead', fontSize: pt(11), lineHeight: pt(16) },
    { id: 'colophon', fontSize: pt(7), lineHeight: pt(9.6), color: col('muted'),
      marginTop: pt(LEAD) },
  ],
  calloutStyles: [sidebar, section('section', 12.4), section('chips', 16)],
});

// #region art: a stack of cloth-bound books, drawn in the palette
function books() { // BOOKS.w × BOOKS.h mm, in tenths of a millimetre; the bottom book first
  const lilac = mix(palette.band, palette.paper, 0.55);
  const stack = [ // [left edge, length, thickness, cloth, label]
    [0, 430, 40, palette.glow, palette.paper],
    [22, 380, 32, palette.paper, palette.accent],
    [6, 400, 36, palette.accent, palette.glow],
    [40, 330, 30, lilac, palette.paper],
    [18, 300, 34, mix(palette.glow, palette.paper, 0.45), palette.accent],
  ];
  let y = BOOKS.h * 10;
  let art = '';
  for (const [x, length, thick, cloth, label] of stack) {
    y -= thick;
    const [top, h] = [y + 3, thick - 3]; // a dark hairline above each book
    art += `<rect x="${x}" y="${top}" width="${length}" height="${h}" rx="3" fill="${cloth}"/>`
      + `<rect x="${x + 14}" y="${top}" width="5" height="${h}" fill="${label}"/>` // bands
      + `<rect x="${x + length - 19}" y="${top}" width="5" height="${h}" fill="${label}"/>`
      + `<rect x="${x + length * 0.34}" y="${top + h * 0.28}" width="${length * 0.32}" `
      + `height="${h * 0.44}" rx="2" fill="${label}"/>`; // the title label
  }
  return `<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 ${BOOKS.w * 10} ${BOOKS.h * 10}" `
    + `width="${BOOKS.w * 10}" height="${BOOKS.h * 10}">${art}</svg>`;
}
function mix(a, b, k) { // a blend of two palette colours, k of the way from a to b
  const rgb = (hex) => [1, 3, 5].map((i) => parseInt(hex.slice(i, i + 2), 16));
  const [p, q] = [rgb(a), rgb(b)];
  return `#${p.map((v, i) => Math.round(v + (q[i] - v) * k).toString(16).padStart(2, '0'))
    .join('')}`;
}
// #endregion

// ─── 2 · Content ────────────────────────────────────────────────────────────
const markdown = String.raw`---
Mostra en Markdown · 89 línies · content.es.mdtitle: "Irene Salcedo · Currículum" author: "Irene Salcedo" --- :::callout{type="sidebar" span="side"} :::callout{type="section" title="Contacto"} Madrid irene@salcedo.example salcedo.example/libros ::: :::callout{type="chips" title="Habilidades"} :chip[Tipografía editorial]{style="skill"} :chip[Cubiertas]{style="skill"} :chip[Diseño de colecciones]{style="skill"} :chip[EPUB 3]{style="skill"} :chip[Retículas y estilos]{style="skill"} :chip[Maquetación]{style="skill"} :chip[Preimpresión]{style="skill"} :chip[Pruebas de color]{style="skill"} :chip[PDF accesible]{style="skill"} :chip[Dirección de arte]{style="skill"} ::: :::callout{type="chips" title="Herramientas"} :chip[InDesign]{style="skill"} :chip[Illustrator]{style="skill"} :chip[Photoshop]{style="skill"} :chip[Glyphs]{style="skill"} :chip[Python]{style="skill"} ::: :::callout{type="section" title="Idiomas"} **Español** · nativo **Catalán** · C2 **Inglés** · C1, lengua de trabajo **Italiano** · B2 ::: :::callout{type="section" title="Formación"} **Máster en Diseño del Libro** Escuela del Libro del Turia, Valencia, 2011–2012 :::space{lines=0.5} **Título Superior de Diseño Gráfico** Escuela de Diseño Almenara, Castellón, 2007–2011 ::: :::callout{type="section" title="Docencia"} Profesora invitada de tipografía editorial en la Escuela del Libro del Turia, cursos de primavera de 2022 a 2024 ::: :::callout{type="section" title="Fuera del trabajo"} Imprimo con tipos móviles en un taller compartido de Lavapiés y recorro a pie los senderos de la sierra de Guadarrama. ::: :::space{lines=1.76} ::: # Irene Salcedo {role="Diseñadora editorial y directora de arte"} :::paragraphs{style="lead"} Diseñadora editorial con catorce años de oficio en edición literaria y académica, los cinco últimos como directora de arte de una casa con tres sellos. Cuido colecciones y cubiertas tanto como la página y redacto las especificaciones para las imprentas. Busco un puesto de directora de arte en una editorial literaria o de libro ilustrado. ::: ## Experiencia ### Directora de arte {org="Ediciones del Albardín, Madrid" dates="2021 – actualidad"} - Fijo y mantengo el libro de estilo de los tres sellos: 58 novedades en 2025, del bolsillo a la no ficción ilustrada. - Rediseñé la colección de bolsillo en un formato de 110 × 178 mm. La nueva retícula da 32 líneas por página en lugar de 29, y el catálogo de 2024 necesitó un 9 % menos de páginas que el de 2023 con el mismo número de títulos. - Dirijo a dos diseñadores y un maquetador, y encargo cada temporada el trabajo de ilustradores y fotógrafos. - Pasé las ediciones impresa y digital a un único archivo de origen, y el libro electrónico sale ahora la misma semana que la tapa dura. ### Diseñadora editorial sénior {org="Casa Albero, Barcelona" dates="2016 – 2021"} - Diseñé interiores y cubiertas de unos 40 títulos al año de narrativa literaria y ensayo, en castellano y en catalán. - Redacté las normas de composición del catálogo en catalán: separación silábica, la ele geminada, comillas y rayas de diálogo. - Diseñé los doce volúmenes de la poesía reunida de Elvira Montcada, con las notas compuestas en el margen. ### Diseñadora y maquetadora {org="Imprenta Quirós, Valencia" dates="2012 – 2016"} - Compuse ediciones críticas con hasta tres niveles de notas, y sus bibliografías e índices, para dos editoriales universitarias. - Preparé archivos para impresión offset: imposición, pruebas de color y especificaciones de papel y encuadernación. ## Libros seleccionados - *Los años del salitre*, de Marta Ibarra. Albardín, 2023. Cubierta, interior y un pliego de mapas a dos tintas. - *Cartas desde Menorca*, de Miquel Truyols. Casa Albero, 2019. Edición bilingüe en páginas enfrentadas. - *Obra reunida*, de Elvira Montcada. Casa Albero, 2018–2020. Doce volúmenes en un estuche de tela. :::paragraphs{style="colophon"} Compuesto en Hedvig Letters Serif y Hanken Grotesk. Texto: CC BY 4.0. :::
`; // content.<lang>.md, inlined by the Cookbook // ─── 3 · Fonts ────────────────────────────────────────────────────────────── // Every face the design uses, loaded before the first build (gotcha: fonts-first). const FONTS = { 'Hedvig Letters Serif': ['400'], 'Hanken Grotesk': ['400', '400i', '700'] }; // ─── 4 · Build & show ─────────────────────────────────────────────────────── await loadFonts(FONTS, markdown); await loadSvg('books.svg', books()); const resources = [{ id: 'books', typeId: 'figure', kind: 'svg', createdAt: 0, updatedAt: 0, svg: { fileId: 'books.svg', width: BOOKS.w * 10, height: BOOKS.h * 10 }, altText: t({ en: 'A stack of five cloth-bound books', es: 'Una pila de cinco libros encuadernados en tela' }) }]; const doc = await buildWithFonts(() => buildDocument({ markdown, resources }, config()), markdown); showPages(doc, { title: t({ en: 'One-page CV with a sidebar', es: 'Currículum de una página con barra lateral' }) });
Kit · core, fonts, viewer, images: igual a totes les receptes · 270 línies// ─── Kit ── helpers shared by every Cookbook recipe · postext.dev/cookbook ───── // ─── Kit · core v1 ── the same in every recipe · postext.dev/cookbook ───────── function mm(value) { return { value, unit: 'mm' }; } function pt(value) { return { value, unit: 'pt' }; } function em(value) { return { value, unit: 'em' }; } /** The sample language's string: t({ en: 'Figure', es: 'Figura' }). */ function t(strings) { return strings[LANG] ?? Object.values(strings)[0]; } /** A file in this recipe's assets folder, served from the Postext repo by jsDelivr. */ function asset(file) { return `https://cdn.jsdelivr.net/gh/drnachio/postext@main/cookbook/${RECIPE}/assets/${file}`; } // ─── Kit · fonts v1 ── the same in every recipe · postext.dev/cookbook ──────── // Postext measures text with the faces the browser has loaded, and caches the // widths, so every face must be ready before the first build. Faces come from // Fontsource: the same static files the PDF embeds, so screen and PDF agree. /** faces = { 'Family Name': ['400', '400i', '700'] }. `text` is the sample: * letters beyond Latin-1 (č, ł, ő…) also load the latin-ext files. With * `optional`, a face Fontsource does not ship is skipped instead of failing. * Resolves to the number of faces added. */ async function loadFonts(faces, text = '', { optional = false } = {}) { kitStatus('Loading fonts…'); const ranges = { latin: 'U+0000-00FF,U+0131,U+0152-0153,U+02BB-02BC,U+02C6,U+02DA,U+02DC,U+0304,U+0308,U+0329,' + 'U+2000-206F,U+20AC,U+2122,U+2191,U+2193,U+2212,U+2215,U+FEFF,U+FFFD', 'latin-ext': 'U+0100-02BA,U+02BD-02C5,U+02C7-02CC,U+02CE-02D7,U+02DD-02FF,U+0304,U+0308,U+0329,' + 'U+1D00-1DBF,U+1E00-1E9F,U+1EF2-1EFF,U+2020,U+20A0-20AB,U+20AD-20C0,U+2113,U+2C60-2C7F,U+A720-A7FF', }; const subsets = /[Ā-˿Ḁ-ỿ]/.test(text) ? ['latin', 'latin-ext'] : ['latin']; const jobs = []; let added = 0; for (const [family, specs] of Object.entries(faces)) { const id = fontsourceId(family); const meta = optional ? await fontsourceMeta(family) : null; for (const spec of new Set(specs)) { const weight = parseInt(spec, 10); const style = spec.endsWith('i') ? 'italic' : 'normal'; if (hasFace(family, weight, style)) continue; if (optional && !(meta?.weights.includes(weight) && meta.styles.includes(style))) continue; for (const subset of subsets) { const url = `https://cdn.jsdelivr.net/npm/@fontsource/${id}@5/files/${id}-${subset}-${weight}-${style}.woff2`; const face = new FontFace(family, `url(${url}) format('woff2')`, { weight: String(weight), style, unicodeRange: ranges[subset] }); jobs.push(face.load().then((ready) => { document.fonts.add(ready); added++; }, () => { if (subset === 'latin' && !optional) throw new Error(`Fontsource has no ${family} ${weight} ${style}`); })); } } } await Promise.all(jobs).catch((error) => { kitFail(error); throw error; }); return added; } /** Runs `build` (a buildDocument or buildBundle call) and checks the faces * the pages use. A regular face missing from FONTS is loaded with a warning; * bold and italic variants are loaded when the family ships them. Then the * measurement caches are cleared and the build runs again. */ async function buildWithFonts(build, text = '') { const tried = new Set(); for (let round = 0; round < 3; round++) { kitStatus('Laying out…'); await new Promise(requestAnimationFrame); // let the status paint first const result = await Promise.resolve().then(build).catch((error) => { kitFail(error); throw error; }); const wanted = { base: {}, variants: {} }; for (const { font, base } of [result].flat().flatMap(fontStringsOf)) { const { family, weight, style } = parseFont(font); const key = `${family}|${weight}|${style}`; if (tried.has(key) || hasFace(family, weight, style)) continue; tried.add(key); (wanted[base ? 'base' : 'variants'][family] ??= []).push(`${weight}${style === 'italic' ? 'i' : ''}`); } if (Object.keys(wanted.base).length) { console.warn(`[cookbook] FONTS does not list ${JSON.stringify(wanted.base)}: loading them.`); } const added = await loadFonts(wanted.base, text) + await loadFonts(wanted.variants, text, { optional: true }); if (added === 0) return result; clearMeasurementCache(); } throw new Error('The fonts did not settle after three builds.'); } /** Every font string of the layout. `base` marks a block's own face; its * bold, italic and bold-italic variants are listed whether or not used. */ function fontStringsOf(doc) { const found = new Map(); const walk = (node) => { if (!node || typeof node !== 'object') return; if (Array.isArray(node)) { node.forEach(walk); return; } for (const [key, value] of Object.entries(node)) { if (typeof value === 'string' && /fontString$/i.test(key)) { found.set(value, found.get(value) || key === 'fontString'); } else if (value && typeof value === 'object') walk(value); } }; walk(doc.pages); walk(doc.blocks); return [...found].map(([font, base]) => ({ font, base })); } /** '700 37.5px Open Sans' / 'italic 400 13px "Source Serif 4"' → { family, weight, style }. * A string with no weight ('95.8px Young Serif', from a design text) is 400. */ function parseFont(font) { const m = /^(?:(italic|oblique)\s+)?(?:small-caps\s+)?(?:(\d+|bold|normal)\s+)?[\d.]+px\s+(.+)$/.exec(font.trim()); if (!m) throw new Error(`Unexpected font string: ${font}`); const weight = m[2] === 'bold' ? 700 : !m[2] || m[2] === 'normal' ? 400 : Number(m[2]); return { family: m[3].replace(/^["']|["']$/g, ''), weight, style: m[1] ? 'italic' : 'normal' }; } /** True when a loaded FontFace covers exactly this family, weight and style * (document.fonts.check() is also true for families nobody declared). */ function hasFace(family, weight, style) { for (const face of document.fonts) { if (face.status !== 'loaded' || face.style !== style) continue; if (face.family.replace(/^["']|["']$/g, '') !== family) continue; const [low, high = low] = face.weight.split(' ').map(Number); if (weight >= low && weight <= high) return true; } return false; } /** Fontsource's id for a family: 'Source Serif 4' → 'source-serif-4'. */ function fontsourceId(family) { return family.toLowerCase().replace(/\s+/g, '-'); } /** The weights and styles a family ships ({ weights: [400, 700], styles: ['normal', 'italic'] }), or null. */ function fontsourceMeta(family) { fontsourceMeta.cache ??= new Map(); const id = fontsourceId(family); if (!fontsourceMeta.cache.has(id)) { fontsourceMeta.cache.set(id, fetch(`https://api.fontsource.org/v1/fonts/${id}`) .then((res) => (res.ok ? res.json() : null), () => null)); } return fontsourceMeta.cache.get(id); } // ─── Kit · viewer v1 ── the same in every recipe · postext.dev/cookbook ─────── /** Shows the pages as facing spreads on a dark desk: the first page is a * recto on its own, then verso | recto pairs, as in a bound book. Pages * are painted when they scroll near the screen. */ function showPages(docs, { title, width = 460 } = {}) { const root = viewer(title); const pages = [docs].flat().flatMap((doc) => doc.pages.map((page) => ({ doc, page, n: (doc.pageIndexOffset ?? 0) + page.index }))); const spreads = []; let verso = null; for (const p of pages) { if (p.n % 2 === 1) { if (verso) spreads.push([verso, null]); verso = p; } else { spreads.push([verso, p]); verso = null; } } if (verso) spreads.push([verso, null]); const density = Math.min(window.devicePixelRatio || 1, 2); showPages.painter?.disconnect(); const painter = new IntersectionObserver((entries) => { for (const { isIntersecting, target } of entries) { if (!isIntersecting) continue; painter.unobserve(target); const { doc, page } = target.postext; renderPageToCanvas(page, doc, target, { scale: (width * density) / page.width }); } }, { rootMargin: '800px' }); showPages.painter = painter; root.replaceChildren(...spreads.map((pair) => { const spread = document.createElement('div'); spread.className = 'pt-spread'; for (const p of pair) { const figure = document.createElement('figure'); if (p) { const label = p.page.pageLabel || String(p.n + 1); const canvas = document.createElement('canvas'); canvas.postext = p; canvas.style.aspectRatio = `${p.page.width} / ${p.page.height}`; canvas.setAttribute('role', 'img'); canvas.setAttribute('aria-label', `Page ${label}`); const folio = document.createElement('figcaption'); folio.textContent = label; figure.append(canvas, folio); painter.observe(canvas); } else figure.className = 'pt-blank'; spread.append(figure); } return spread; })); kitStatus(`${pages.length} ${pages.length === 1 ? 'page' : 'pages'}`); document.documentElement.dataset.postext = 'ready'; return pages.length; } /** The desk, the bar and the error reporting, created once. */ function viewer(title) { if (!document.getElementById('pt-kit')) { document.head.insertAdjacentHTML('beforeend', `<style id="pt-kit"> :root { color-scheme: dark; } body { margin: 0; background: #0e1014; color: #b9bcc4; font: 13px/1.45 system-ui, sans-serif; } #pt-bar { position: sticky; top: 0; z-index: 1; display: flex; flex-wrap: wrap; align-items: center; gap: 6px 16px; padding: 10px 16px; background: rgb(14 16 20 / .92); backdrop-filter: blur(6px); border-bottom: 1px solid #23262d; } #pt-bar strong { color: #f4f1ea; font-weight: 600; } #pt-actions { display: flex; gap: 12px; margin-left: auto; } #pt-actions a, #pt-actions button { color: #d8a21a; font: inherit; background: none; border: 0; padding: 0; cursor: pointer; } #pages { display: grid; justify-items: center; gap: 48px; padding: 32px 16px 72px; } .pt-spread { display: flex; } .pt-spread figure { margin: 0; width: min(460px, 44vw); } .pt-spread canvas { display: block; width: 100%; background: #fff; box-shadow: 0 1px 2px rgb(0 0 0 / .5), 0 22px 44px -16px rgb(0 0 0 / .8); } .pt-spread figure:first-child canvas { box-shadow: inset -14px 0 14px -14px rgb(0 0 0 / .18), 0 1px 2px rgb(0 0 0 / .5), 0 22px 44px -16px rgb(0 0 0 / .8); } .pt-spread figcaption { margin-top: 10px; text-align: center; font: 600 10px/1 system-ui, sans-serif; letter-spacing: .18em; text-transform: uppercase; color: #6c7079; } .pt-blank { visibility: hidden; } @media (max-width: 760px) { .pt-spread { flex-direction: column; gap: 32px; } .pt-spread figure { width: min(460px, 92vw); } .pt-blank { display: none; } } </style>`); document.body.insertAdjacentHTML('afterbegin', '<header id="pt-bar"><strong id="pt-title"></strong><span id="pt-status" role="status"></span><span id="pt-actions"></span></header>'); document.getElementById('pt-title').textContent = document.title || 'Postext'; addEventListener('error', (event) => kitFail(event.error ?? event.message)); addEventListener('unhandledrejection', (event) => kitFail(event.reason)); } if (title) document.getElementById('pt-title').textContent = title; return document.getElementById('pages') ?? document.body.appendChild(Object.assign(document.createElement('main'), { id: 'pages' })); } function kitStatus(text) { viewer(); document.getElementById('pt-status').textContent = text; } function kitFail(error) { document.documentElement.dataset.postext = 'error'; kitStatus(`Error: ${error?.message ?? error}`); } // ─── Kit · images v1 ── 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 (drawn in code, or fetched) as a vector image. */ async function loadSvg(fileId, svg) { const img = new Image(); img.src = `data:image/svg+xml;charset=utf-8,${encodeURIComponent(svg)}`; await img.decode(); registerResourceImage(fileId, img); (loadImage.bytes ??= new Map()).set(fileId, new TextEncoder().encode(svg)); } /** 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 ───────────────────────────────────────────────────────────────────────

L'script.js compost funciona tal com és: enganxa'l com a script de mòdul en qualsevol pàgina o obre la recepta a CodePen. Carpeta de la recepta a GitHub ↗ (s'obre en una pestanya nova)

Variants

#Canvia el color de la banda

Cada color s'escriu una sola vegada, a palette, i d'allà el prenen les caixes de la banda, el requadre lateral, els xips, la professió, els títols i el dibuix. Amb aquestes tres línies, la banda, l'accent i els xips passen a verd blavós.

-  band: '#3a2235', // the sidebar
+  band: '#1d3b3a', // the sidebar
-  accent: '#8f3b62', // on paper: the role and the section heads (7.1:1)
+  accent: '#2b5f5b', // on paper: the role and the section heads (7.3:1)
-  chip: '#744d6c', // on the band: the skill chips
+  chip: '#456d69', // on the band: the skill chips

#Porta figures a la mateixa columna

El llibre de text amb columna al marge posa figures, peus laterals i glosses en una columna només per a flotants a la vora exterior de cada pàgina.

Errors freqüents

Error freqüent

Un requadre lateral comença a l'altura del bloc que segueix la seva tanca

A postext 1.4.1, un requadre amb span: 'side' es col·loca a la columna lateral a l'altura on ha arribat el text a la seva tanca, a la línia següent de la retícula i sota els requadres que ja hi hagi. Posa la tanca d'una glossa just abans del paràgraf que explica: si va després, la glossa comença al costat del paràgraf següent. Un requadre que passaria del peu de la columna puja fins que el seu peu coincideix amb el de la columna, si el requadre de sobre li deixa lloc; si tot i així no hi cap, espera la columna lateral de la pàgina següent. Notes al marge →

Error freqüent

Els requadres laterals apilats al canal deixen una línia de paper entre ells

A postext 1.4.1, un requadre amb span: 'side' deixa a sota almenys una línia de text abans del que vingui després al canal, arrodonida a la retícula de base, digui el que digui el seu marginBottom. Per això els requadres amb el mateix fons no es toquen mai: apilats, es llegeixen com a targetes soltes. Per a un tauler continu, obre un sol requadre lateral i niua-hi les seccions. Notes al marge →

Error freqüent

:::space es descarta al principi d'un requadre o columna

:::space es descarta al principi d'una columna, d'un requadre o d'un grup :::columns, encara que sigui l'únic contingut de la caixa, així que una caixa de resposta feta només d'espai es queda sense alçada. Obre la caixa amb un títol o una línia d'enunciat i afegeix-hi després l'espai. Espai vertical explícit →

Error freqüent

Els elements de capçalera i peu es pinten damunt del text

Els elements de capçalera i peu es pinten sobre la pàgina i l'àrea de text no els deixa lloc. Mantén-los dins dels marges, que són els que els reserven l'espai. Capçaleres i folis →

Error freqüent

Un requadre imbricat ignora span, placement i snapToGrid

Un requadre imbricat en un altre ignora el seu span, placement, snapToGrid i floatBarrier: sempre es compon dins del requadre que el conté, amb l'amplada interior d'aquest. Requadres niats →

Error freqüent

Una paleta canviada no arriba als elements de disseny ni al color de les remissions

postext 1.4.1 aplica colorPalette als estils de text (cos, títols, llistes, peus, taules, requadres), però no als elements de capçaleres, peus de pàgina, obertures i portadelles, ni a bodyText.referenceColor: conserven l'hex escrit al costat del seu paletteId. Si canvies la paleta, per a una edició de pantalla fosca o per recolorejar, reescriu cada color enllaçat a partir de colorPalette abans de compondre. Paleta de color semàntica →

Error freqüent

El lineHeight d'un text de disseny és un múltiple, mai una mesura

En una ranura de disseny, el lineHeight d'un element de text multiplica el seu cos (lineHeight: 1.05). A postext 1.4.1 una mesura com pt(15) no dona error: l'alçada de l'obertura surt NaN, l'espai que reserva, minHeight inclòs, es perd sense avís i el text se superposa al títol. Textos, filets i caixes als dissenys de pàgina →

Error freqüent

En el text en bandera no hi ha partició de mots

La partició de mots només s'aplica al text justificat; el text en bandera talla entre paraules, així que una columna estreta en bandera queda molt desigual. Justifica el passatge o eixampla la mesura. Partició de mots i llengua del document →

Error freqüent

En el text en bandera no s'eviten les línies curtes

optimalLineBreaking, avoidRunts, runtPenalty i runtMinCharacters actuen sobre l'algorisme de Knuth–Plass, que postext 1.4.1 només aplica al text justificat. Un paràgraf en bandera es talla línia a línia i pot acabar en una sola paraula curta, diguin el que diguin aquestes opcions. Revisa les últimes línies del text en bandera i reescriu el paràgraf que acabi en una línia curta. Vídues, òrfenes i línies curtes →

Error freqüent

Carrega totes les fonts abans de compondre

La composició mesura el text amb les fonts que el navegador ha carregat i en desa les amplades, així que una font que arriba després de la primera composició deixa talls de línia erronis i un PDF que ja no coincideix amb la pantalla. Carrega abans tots els pesos i estils, i crida clearMeasurementCache() abans de recompondre si alguna arriba tard. Fonts abans de compondre →

Error freqüent

Qualsevol objecte headings desactiva el salt de pàgina de l'H1

Per defecte un H1 salta a una pàgina senar (always-odd), però qualsevol objecte headings anul·la aquest valor, de manera que els capítols van seguits i span: 'page' no fa res. Torna a declarar headings.levels[0].breakBefore: { enabled: true, parity } a cada configuració. Capítols que obren en pàgina senar →

  • Ajusta el :::space a la pàgina. A l'edició castellana, 1,76 línies acaben el requadre 0,8 mm per damunt del peu de la columna. Amb 1,96 línies passa del peu en 0,1 mm i tota la barra se'n va a la pàgina 2; amb 1,66 s'obre una franja de paper entre el requadre i la caixa del peu.
  • Una caixa del disseny del nom no serveix per pintar la banda. Si acaba just damunt del peu de la columna, el títol reserva 265 mm, més que els 260,8 mm de la columna, i el text passa a la pàgina 2; si arriba fins al peu del full, el títol perd l'alçada que reservava i el perfil puja a 30,3 mm i es munta sobre la professió.

Crèdits

Text
Text original, CC BY 4.0
Imatges
  • The stack of books at the head of the sidebar, drawn in code in the page's palette · Ignacio Ferro · CC BY 4.0
Fonts
Hedvig Letters Serif (SIL OFL 1.1) · Hanken Grotesk (SIL OFL 1.1)
Sandbox