# Un dictamen jurídico citado en OSCOLA

> Un comentario de sentencia de cuatro páginas cuyas citas [@robinson2018, para 21] salen como notas OSCOLA: casos en cursiva, cita neutra, ibid y (n 1).

- Versión HTML: https://postext.dev/es/cookbook/legal-opinion-oscola
- Receta N.º 099 · Estructura del libro · Nivel 2 (Intermedio) · Salidas: Canvas, PDF
- Géneros: Artículos y trabajos académicos
- Requiere postext ≥ 1.12.1, postext-pdf ≥ 1.12.1 · probada con 1.12.1, postext-pdf 1.12.1 el 2026-10-01
- Páginas: [1](https://postext.dev/cookbook/legal-opinion-oscola/es/p01.webp?v=a69d9ce1), [2](https://postext.dev/cookbook/legal-opinion-oscola/es/p02.webp?v=a69d9ce1), [3](https://postext.dev/cookbook/legal-opinion-oscola/es/p03.webp?v=a69d9ce1), [4](https://postext.dev/cookbook/legal-opinion-oscola/es/p04.webp?v=a69d9ce1)
- PDF: https://postext.dev/cookbook/legal-opinion-oscola/es/legal-opinion-oscola.pdf?v=a69d9ce1
- Abrir en el Sandbox: https://postext.dev/es/sandbox#recipe=legal-opinion-oscola&lang=es (.postext: https://postext.dev/cookbook/legal-opinion-oscola/es/legal-opinion-oscola.postext)
- Última actualización: 2026-10-01
- Otros idiomas: [en](https://postext.dev/en/cookbook/legal-opinion-oscola.md), [zh](https://postext.dev/zh/cookbook/legal-opinion-oscola.md)

## En pocas palabras

Un comentario breve sobre una sentencia inglesa acerca de la policía. Cada sentencia, ley o artículo en que se apoya se cita en una nota numerada al pie, con el estilo de las facultades de derecho británicas.

## Lo que vas a componer

Una separata de la serie de comentarios de sentencia de una revista jurídica: cuatro páginas sobre *Robinson v Chief Constable of West Yorkshire Police*, la sentencia del Tribunal Supremo británico de 2018 sobre cuándo la policía tiene un deber de diligencia. La página mide 162 × 240 mm, con márgenes en espejo, en Libre Baskerville de 9,4/13,6 pt, y la primera abre con una banda burdeos y el nombre del caso en cursiva de 60 pt. La autora escribe cada fuente como una clave con su localizador, `[@caparo1990, 617–18]`, y Postext compone 23 notas al pie en OSCOLA, la norma de cita de las facultades de derecho británicas: casos en cursiva, leyes en redonda, sin punto tras AC, HL o edn, *ibid* para la fuente de la nota anterior y *Robinson (n 1) [55]* después. Las resoluciones son reales; la autora y la revista, no.

**Esta receta responde a:**

- ¿Cómo cito sentencias y leyes en notas al pie con el estilo OSCOLA?
- ¿Cómo pongo las citas en notas al pie, al estilo de Chicago, con forma breve en las siguientes?

## La respuesta corta

```js
// script.js, líneas 50–68
// The text cites with [@robinson2018, para 21]; OSCOLA is a note style, so each citation
// becomes a footnote: the case in full the first time, "ibid" when the note before cites
// the same authority, "Robinson (n 1) [55]" after that. Case names come out italic,
// statutes roman, and no abbreviation takes a full stop (AC, HL, UKSC, edn, n, s).
registerCitationEngine(createCiteprocEngine({ styles: STYLES, locales: LOCALES }));
const citations = {
  style: 'custom', customStyle: OSCOLA, // the bundled 'oscola', patched above
  locale: 'en-GB', // OSCOLA's words stay English in the Spanish edition too
  notes: 'footnote',
  link: false, // a case comment has no bibliography to link to
  bibliography: { auto: false }, // OSCOLA articles cite in the notes only
};
// The citation notes share the numbering and the look of the author's own [^notes].
const footnotes = {
  placement: 'column', numbering: 'chapter',
  fontSize: pt(7.8), lineHeight: pt(10.2), spaceBetween: pt(2.2),
  textAlign: 'left', // ragged: long report citations would open wide gaps if justified
  separator: { width: 0.25, lineWidth: pt(0.5), color: col('rule') },
};
```

## Ingredientes

**Enseña**

- [Citas en notas](https://postext.dev/es/docs/document-format.md#citas-y-bibliografía): Un estilo de notas (Chicago notas, OSCOLA, GB/T 7714 notas) pone cada cita en una nota al pie, con forma breve en las citas siguientes, o en una nota de dos líneas dentro del renglón chino.
- [Citas en un estilo de cita](https://postext.dev/es/docs/document-format.md#citas-y-bibliografía): Obras citadas como [@clave, pág. 33] y formateadas en un estilo CSL (APA, Chicago, MLA, IEEE, Vancouver, ISO 690, GB/T 7714…) elegido en los ajustes, enlazadas con sus entradas.

**También usa**

- [Notas al pie](https://postext.dev/es/docs/document-format.md#notas-al-pie)
- [Banda de capítulo a todo el ancho](https://postext.dev/es/docs/configuration.md#span-y-diseño-avanzado)
- [Aperturas diseñadas](https://postext.dev/es/docs/configuration.md#span-y-diseño-avanzado)
- [Atributos de título](https://postext.dev/es/docs/document-format.md#atributos-de-encabezado)
- [Cabeceras y folios](https://postext.dev/es/docs/configuration.md#encabezados-y-pies)
- [Márgenes simétricos](https://postext.dev/es/docs/configuration.md#márgenes-simétricos-espejo)
- [Estilos de párrafo](https://postext.dev/es/docs/configuration.md#estilos-de-párrafo)
- [Paleta de color semántica](https://postext.dev/es/docs/configuration.md#paleta-de-colores)
- [Exportación a PDF](https://postext.dev/es/docs/configuration.md#generación-de-pdf)
- [Referencias cruzadas](https://postext.dev/es/docs/document-format.md#referencias-cruzadas-y-anclas)
- [Bibliografía a partir de las referencias](https://postext.dev/es/docs/document-format.md#citas-y-bibliografía)
- [Color del papel](https://postext.dev/es/docs/configuration.md#página)
- [Cabeceras según el tipo de página](https://postext.dev/es/docs/configuration.md#elementos-de-texto)
- [Fuentes incrustadas en el PDF](https://postext.dev/es/docs/configuration.md#por-qué-un-proveedor-de-fuentes)

**La configuración de un vistazo**

- [`bodyText`](https://postext.dev/es/docs/configuration.md#texto-de-cuerpo), [`citations`](https://postext.dev/es/docs/configuration.md#citas), [`colorPalette`](https://postext.dev/es/docs/configuration.md#paleta-de-colores), [`footer`](https://postext.dev/es/docs/configuration.md#encabezados-y-pies), `footnotes`, [`header`](https://postext.dev/es/docs/configuration.md#encabezados-y-pies), [`headings`](https://postext.dev/es/docs/configuration.md#encabezados), [`layout`](https://postext.dev/es/docs/configuration.md#disposición), [`locale`](https://postext.dev/es/docs/configuration.md#separación-silábica), [`page`](https://postext.dev/es/docs/configuration.md#página), [`paragraphStyles`](https://postext.dev/es/docs/configuration.md#estilos-de-párrafo)

**API**

- [`LOCALES`](https://postext.dev/es/docs/document-format.md#citas-y-bibliografía), [`STYLES`](https://postext.dev/es/docs/document-format.md#citas-y-bibliografía), [`buildDocument`](https://postext.dev/es/docs/configuration.md#construir-un-documento), [`clearMeasurementCache`](https://postext.dev/es/docs/configuration.md#caché-de-medidas), [`createCiteprocEngine`](https://postext.dev/es/docs/document-format.md#citas-y-bibliografía), [`decompressWoff2`](https://postext.dev/es/docs/configuration.md#proveedor-de-fuentes-en-el-navegador-fontsource--woff2), [`registerCitationEngine`](https://postext.dev/es/docs/document-format.md#citas-y-bibliografía), [`renderPageToCanvas`](https://postext.dev/es/docs/configuration.md#renderizar-una-página-a-un-bitmap), [`renderToPdf`](https://postext.dev/es/docs/configuration.md#generación-de-pdf)

**Tipografías**

- Libre Baskerville (OFL-1.1), Libre Caslon Display (OFL-1.1), Libre Franklin (OFL-1.1)

## Elaboración

### 1 · Una nota OSCOLA para cada fuente

El código es [la respuesta corta](#la-respuesta-corta) de arriba. OSCOLA es un estilo de notas: `[@robinson2018, para 21]` deja un número volado tras el punto y su texto pasa al pie. La nota 1 da el caso completo, *Robinson v Chief Constable of West Yorkshire Police [2018] UKSC 4, [2018] AC 736*; la nota 2, que lo cita de nuevo enseguida, dice *ibid [21]*; la nota 8, con otros casos por medio, *Robinson (n 1) [26–27]*. Un artículo de revista jurídica cita solo en notas, así que `bibliography.auto: false` deja la lista de obras fuera de la última página. Las palabras del estilo siguen en inglés en esta edición (`locale: 'en-GB'`), como en una traducción que conserva el aparato original.

### 2 · Un caso es un dato, escrito como lo imprimen los repertorios

Un caso es un elemento CSL de `type: legal_case`: el nombre en `title`, el tribunal en `authority`, la serie del repertorio en `container-title` y la primera página en `page`. OSCOLA pone el año de un repertorio entre corchetes cuando el año identifica el tomo, como en los Appeal Cases, así que la receta escribe ese año en `volume`, seguido del número de tomo: `volume: "[1990] 2"` da *[1990] 2 AC 605 (HL)*. Un caso posterior a 2001 tiene además cita neutra: `authority: UKSC`, `number: "4"` e `issued: 2018` dan *[2018] UKSC 4*, y detrás va el repertorio. Una ley es `type: legislation`, con el año en el título y sin `issued`, para que el año no salga dos veces, y `[@hra1998, § 6]` cita un artículo como *s 6*.

### 3 · Dos retoques al estilo incluido

```js
// script.js, líneas 32–46
// The bundled oscola.csl writes a paragraph pinpoint in brackets only in a case's first
// note: a later note printed "Robinson (n 1) para 55" and "ibid 27", and a repeated
// statute "ibid 7". These two replacements give "Robinson (n 1) [55]", "ibid [27]" and
// "ibid s 7", as OSCOLA prints them.
const LOCATOR = `<macro name="label-locator"><choose>
  <if type="legal_case" locator="paragraph">
    <text variable="locator" prefix="[" suffix="]"/></if>
  <else-if locator="page"><text variable="locator"/></else-if>
  <else><group delimiter=" "><label form="short" strip-periods="true" variable="locator"/>
    <text variable="locator"/></group></else>
</choose></macro>`;
const OSCOLA = STYLES.oscola
  .replace(/<macro name="label-locator">[\s\S]*?<\/macro>/, LOCATOR)
  .replace(/(<if position="ibid-with-locator">[\s\S]*?)<text variable="locator"\/>/,
    '$1<text macro="label-locator"/>');
```

El `oscola.csl` incluido pone entre corchetes el párrafo citado solo en la primera nota de un caso. Las siguientes imprimían *para 55* e *ibid 27*, y una segunda nota a la misma ley perdía su *s*. El retoque cambia una macro y una línea del XML del estilo antes de componer y lo pasa como estilo propio, sin tocar el texto ni las referencias.

### 4 · El nombre del caso en la banda

```js
// script.js, líneas 72–96
const BAND = 124; // mm from the trim's top
const big = (id, content, family, size, color, y, extra = {}) => ({
  kind: 'text', id, content, fontFamily: family, fontSize: pt(size), lineHeight: 1.08, color,
  align: 'left', overflow: 'wrap', ...extra,
  placement: { ...at('container', 'top-left', 0, y), size: { width: mm(118), height: 'auto' } },
});
const opener = {
  enabled: true,
  minHeight: mm(BAND - MARGIN.top + 8), // the text starts 8 mm under the band
  slot: { elements: [
    { kind: 'box', id: 'band', style: { backgroundColor: col('claret') },
      placement: { ...at('page', 'top-left'), size: { width: 'fill', height: mm(BAND) } } },
    big('kicker', '{attr.kicker}', LABEL, 7.4, col('tint'), 2,
      { fontWeight: 600, letterSpacing: pt(1.4), textTransform: 'uppercase' }),
    big('case', '{attr.case}', TEXT, 60, col('paper'), 9, { italic: true }),
    big('parties', '{attr.parties}', TEXT, 12.5, col('tint'), 33, { italic: true }),
    big('citation', '{attr.citation}', TEXT, 10.5, col('tint'), 40),
    { kind: 'box', id: 'hairline', style: { backgroundColor: col('tint') },
      placement: { ...at('container', 'top-left', 0, 51),
        size: { width: mm(30), height: pt(0.6) } } },
    big('title', '{titleText}', DISPLAY, 27, col('paper'), 57),
    big('byline', '{attr.byline}', LABEL, 8.5, col('tint'), 82,
      { fontWeight: 500, letterSpacing: pt(0.5) }),
  ] },
};
```

La banda baja 124 mm desde el corte. El nombre del caso, las partes y la cita son atributos del título, así que otro comentario de la serie solo cambia su Markdown. *Robinson* va en Libre Baskerville cursiva de 60 pt porque OSCOLA pone en cursiva los nombres de los casos, y la cita de debajo sigue en redonda, como en las notas.

```js
// script.js, líneas 16–23
const palette = {
  ink: '#1f1b1a', claret: '#5e1b26', tint: '#ecdcd8', rule: '#bfb4ad', muted: '#6b625d',
  paper: '#fcfaf6',
};
// A design element paints the hex written beside its paletteId (gotcha: palette-skips-designs).
const col = (id) => ({ hex: palette[id], model: 'hex', paletteId: id });
const colorPalette = Object.entries({ ...palette, 'main-color': palette.claret })
  .map(([id, hex]) => ({ id, name: id, value: { hex, model: 'hex' } }));
```

## La receta completa

Un solo archivo, compuesto a partir de la carpeta de la receta con el texto de ejemplo y el kit común del Recetario ya incluidos; construye su propia página. Para ejecutarlo, ponlo en un `<script type="module">` de una página vacía o pégalo en el panel JS de un pen nuevo de CodePen (como módulo). Importa postext desde esm.sh, así que no hay nada que instalar ni compilar.

- Carpeta de la receta: https://github.com/drnachio/postext/tree/main/cookbook/legal-opinion-oscola

### script.js

```js
// ═══ Postext Cookbook · Nº 099 · A legal opinion cited in OSCOLA ══════════════════
// https://postext.dev/en/cookbook/legal-opinion-oscola
// Code: MIT · Text: original (CC BY 4.0) · Pictures: none
// Fonts: Libre Baskerville, Libre Caslon Display, Libre Franklin (OFL 1.1) · Needs postext ≥ 1.12.1
import {
  buildDocument, renderPageToCanvas, clearMeasurementCache, registerCitationEngine,
} from 'https://esm.sh/postext';
import { renderToPdf, decompressWoff2 } from 'https://esm.sh/postext-pdf';
import { createCiteprocEngine, STYLES, LOCALES } from 'https://esm.sh/postext-citeproc';

const LANG = 'es'; // @lang: the language of the sample document ('en' | 'es')
const RECIPE = 'legal-opinion-oscola';

// ─── 1 · Design ─────────────────────────────────────────────────────────────
// #region palette: a law-report claret for the band, ink and cream paper for the rest
const palette = {
  ink: '#1f1b1a', claret: '#5e1b26', tint: '#ecdcd8', rule: '#bfb4ad', muted: '#6b625d',
  paper: '#fcfaf6',
};
// A design element paints the hex written beside its paletteId (gotcha: palette-skips-designs).
const col = (id) => ({ hex: palette[id], model: 'hex', paletteId: id });
const colorPalette = Object.entries({ ...palette, 'main-color': palette.claret })
  .map(([id, hex]) => ({ id, name: id, value: { hex, model: 'hex' } }));
// #endregion
const TEXT = 'Libre Baskerville', DISPLAY = 'Libre Caslon Display', LABEL = 'Libre Franklin';
const TRIM = { width: 162, height: 240 }; // a law-journal offprint
const MARGIN = { top: 23, bottom: 22, inner: 19, outer: 21 };
const LEAD = 13.6; // pt
const at = (to, edge, x = 0, y = 0) => ({ anchor: { to, edge }, offset: { x: mm(x), y: mm(y) } });

// #region oscola: two patches to the bundled OSCOLA style, applied before the build
// The bundled oscola.csl writes a paragraph pinpoint in brackets only in a case's first
// note: a later note printed "Robinson (n 1) para 55" and "ibid 27", and a repeated
// statute "ibid 7". These two replacements give "Robinson (n 1) [55]", "ibid [27]" and
// "ibid s 7", as OSCOLA prints them.
const LOCATOR = `<macro name="label-locator"><choose>
  <if type="legal_case" locator="paragraph">
    <text variable="locator" prefix="[" suffix="]"/></if>
  <else-if locator="page"><text variable="locator"/></else-if>
  <else><group delimiter=" "><label form="short" strip-periods="true" variable="locator"/>
    <text variable="locator"/></group></else>
</choose></macro>`;
const OSCOLA = STYLES.oscola
  .replace(/<macro name="label-locator">[\s\S]*?<\/macro>/, LOCATOR)
  .replace(/(<if position="ibid-with-locator">[\s\S]*?)<text variable="locator"\/>/,
    '$1<text macro="label-locator"/>');
// #endregion

// #region answer: OSCOLA footnotes for cases, statutes and articles
// The text cites with [@robinson2018, para 21]; OSCOLA is a note style, so each citation
// becomes a footnote: the case in full the first time, "ibid" when the note before cites
// the same authority, "Robinson (n 1) [55]" after that. Case names come out italic,
// statutes roman, and no abbreviation takes a full stop (AC, HL, UKSC, edn, n, s).
registerCitationEngine(createCiteprocEngine({ styles: STYLES, locales: LOCALES }));
const citations = {
  style: 'custom', customStyle: OSCOLA, // the bundled 'oscola', patched above
  locale: 'en-GB', // OSCOLA's words stay English in the Spanish edition too
  notes: 'footnote',
  link: false, // a case comment has no bibliography to link to
  bibliography: { auto: false }, // OSCOLA articles cite in the notes only
};
// The citation notes share the numbering and the look of the author's own [^notes].
const footnotes = {
  placement: 'column', numbering: 'chapter',
  fontSize: pt(7.8), lineHeight: pt(10.2), spaceBetween: pt(2.2),
  textAlign: 'left', // ragged: long report citations would open wide gaps if justified
  separator: { width: 0.25, lineWidth: pt(0.5), color: col('rule') },
};
// #endregion

// #region opener: the claret band holds the case name, its citation and the title
const BAND = 124; // mm from the trim's top
const big = (id, content, family, size, color, y, extra = {}) => ({
  kind: 'text', id, content, fontFamily: family, fontSize: pt(size), lineHeight: 1.08, color,
  align: 'left', overflow: 'wrap', ...extra,
  placement: { ...at('container', 'top-left', 0, y), size: { width: mm(118), height: 'auto' } },
});
const opener = {
  enabled: true,
  minHeight: mm(BAND - MARGIN.top + 8), // the text starts 8 mm under the band
  slot: { elements: [
    { kind: 'box', id: 'band', style: { backgroundColor: col('claret') },
      placement: { ...at('page', 'top-left'), size: { width: 'fill', height: mm(BAND) } } },
    big('kicker', '{attr.kicker}', LABEL, 7.4, col('tint'), 2,
      { fontWeight: 600, letterSpacing: pt(1.4), textTransform: 'uppercase' }),
    big('case', '{attr.case}', TEXT, 60, col('paper'), 9, { italic: true }),
    big('parties', '{attr.parties}', TEXT, 12.5, col('tint'), 33, { italic: true }),
    big('citation', '{attr.citation}', TEXT, 10.5, col('tint'), 40),
    { kind: 'box', id: 'hairline', style: { backgroundColor: col('tint') },
      placement: { ...at('container', 'top-left', 0, 51),
        size: { width: mm(30), height: pt(0.6) } } },
    big('title', '{titleText}', DISPLAY, 27, col('paper'), 57),
    big('byline', '{attr.byline}', LABEL, 8.5, col('tint'), 82,
      { fontWeight: 500, letterSpacing: pt(0.5) }),
  ] },
};
// #endregion

const head = (id, content, parity, edge, x, align) => ({
  kind: 'text', id, content, parity, pages: 'body', fontFamily: LABEL, fontWeight: 500,
  fontSize: pt(7.2), letterSpacing: pt(1), textTransform: 'uppercase', color: col('muted'),
  align, placement: at('page', edge, x, 13),
});
const journal = t({ en: 'Notes on Obligations', es: 'Cuadernos de Obligaciones' });

const config = () => ({ // a factory: the engine caches resolved configs per object
  locale: t({ en: 'en-us', es: 'es' }), // exact codes (gotcha: hyphenation-locales)
  colorPalette,
  citations,
  footnotes,
  page: {
    sizePreset: 'custom', width: mm(TRIM.width), height: mm(TRIM.height), dpi: 150,
    backgroundColor: col('paper'),
    margins: { top: mm(MARGIN.top), bottom: mm(MARGIN.bottom), left: mm(MARGIN.inner),
      right: mm(MARGIN.outer), mirror: true },
  },
  layout: { layoutType: 'single' },
  bodyText: {
    fontFamily: TEXT, fontSize: pt(9.4), lineHeight: pt(LEAD), color: col('ink'),
    boldColor: col('ink'), italicColor: col('ink'), referenceColor: col('ink'),
    textAlign: 'justify', firstLineIndent: mm(4.5), indentAfterHeading: false,
    hyphenation: { enabled: true }, optimalLineBreaking: true,
    avoidWidows: true, avoidOrphans: true, avoidRunts: true,
  },
  headings: {
    fontFamily: TEXT, fontWeight: 400, color: col('ink'),
    levels: [
      // Restated: any headings object drops the H1 break (gotcha: headings-drop-h1-break).
      { level: 1, fontSize: pt(27), breakBefore: { enabled: true, parity: 'any' },
        advancedDesign: opener },
      { level: 2, italic: true, fontSize: pt(12), color: col('claret'),
        lineHeight: pt(LEAD), marginTop: pt(LEAD), marginBottom: pt(LEAD / 2) },
    ],
  },
  paragraphStyles: [
    { id: 'colophon', fontFamily: LABEL, fontSize: pt(6.8), lineHeight: pt(9.2),
      color: col('muted'), textAlign: 'left', firstLineIndent: pt(0), marginTop: pt(LEAD) },
  ],
  header: { elements: [
    head('verso-folio', '{pageNumber}', 'even', 'top-left', MARGIN.outer, 'left'),
    head('verso-journal', journal, 'even', 'top-left', MARGIN.outer + 8, 'left'),
    head('recto-title', '{title}', 'odd', 'top-right', -(MARGIN.outer + 8), 'right'),
    head('recto-folio', '{pageNumber}', 'odd', 'top-right', -MARGIN.outer, 'right'),
  ] },
  footer: { elements: [ // a drop folio on the opener page only
    { kind: 'text', id: 'drop-folio', content: '{pageNumber}', pages: 'opener',
      fontFamily: LABEL, fontWeight: 500, fontSize: pt(7.2), color: col('muted'),
      align: 'center', placement: at('page', 'bottom', 0, -11) },
  ] },
});

// ─── 2 · Content ────────────────────────────────────────────────────────────
const markdown = String.raw`---
title: "Acción, omisión y policía"
author: "Harriet Okafor"
---

# Acción, omisión y policía {#comment kicker="Cuadernos de Obligaciones · Comentario" case="Robinson" parties="v Chief Constable of West Yorkshire Police" citation="[2018] UKSC 4, [2018] AC 736" byline="Harriet Okafor"}

Una tarde de julio de 2008, dos agentes de paisano intentaron detener a un hombre sospechoso de vender droga en una calle comercial de Huddersfield. El hombre se resistió, y los tres cayeron sobre la señora Robinson, una mujer de 76 años que pasaba por allí, y la tiraron al suelo. El juez de instancia declaró que los agentes habían actuado con negligencia al intentar la detención allí y entonces, pero sostuvo que la policía no tenía un deber de diligencia frente al transeúnte herido en una operación contra el delito. El Tribunal de Apelación le dio la razón. El Tribunal Supremo, no.[@robinson2018]

Lord Reed, a cuyo voto se adhirieron Lady Hale y Lord Hodge, empezó por corregir una lectura del derecho que llevaba casi treinta años sin corregirse.[@robinson2018, para 21] Este comentario sostiene que la sentencia importa menos por lo que decidió sobre la policía que por el método que restableció. El tribunal que se pregunta si existe un deber de diligencia debe buscar primero una categoría ya reconocida y razonar por analogía a partir de ella; las consideraciones de política jurídica solo entran cuando el caso es nuevo.

## No existe un «test de Caparo»

El derecho moderno arranca de la fórmula de Lord Atkin: hay que poner un cuidado razonable para evitar los actos u omisiones que uno puede prever razonablemente que dañarán a su prójimo.[@donoghue1932, 580] En los años setenta, la Cámara de los Lores quiso convertir esa frase en una regla general. Lord Wilberforce propuso dos pasos: una relación de proximidad en la que el daño fuera previsible y, después, cualquier consideración que debiera negar o limitar el deber.[@anns1978, 751–52] La regla extendió la responsabilidad más deprisa de lo que los tribunales podían justificar, y *Anns* quedó revocada en 1990.[@murphy1991]

Ese mismo año, Lord Bridge describió los elementos del deber: la previsibilidad del daño, una relación de proximidad y una situación en la que al tribunal le parezca justo y razonable imponerlo.[@caparo1990, 617–18] Muchos jueces lo aplicaron después como un test de tres pasos a cualquier demanda, incluidas las de daños corporales causados por un acto positivo. Lord Bridge había dicho lo contrario: dudaba de que una sola prueba pudiera resolver todos los casos y prefería que el derecho abriera categorías nuevas paso a paso.[^brennan]

[^brennan]: Lord Bridge hizo suyas las palabras del juez Brennan en el Tribunal Supremo de Australia; véase [@caparo1990, 618; @sutherland1985, 481]

*Robinson* recupera esa lectura. Cuando un caso entra en una categoría reconocida, el tribunal aplica el derecho establecido; la pregunta por lo justo y razonable solo surge cuando se le pide ir más allá.[@robinson2018, para 26–27] El daño corporal causado por un acto positivo negligente es una categoría reconocida desde *Donoghue*. Detener a alguien en una calle llena de gente era un acto de ese tipo, el riesgo para quienes estaban cerca era previsible y no hacía falta nada más para fundar el deber.[@robinson2018, para 74]

## Actos y omisiones

El *common law* no impone, en general, el deber de proteger a otro del daño que causa un tercero o la naturaleza.[@smith1987, 271] Lord Hoffmann dio la razón en términos políticos: obligar a alguien a tener cuidado cuando decide actuar invade menos su libertad que obligarlo a actuar, y un deber de socorro plantea por qué ha de recaer en un transeúnte y no en otro.[@stovin1996, 943–44] La regla tiene excepciones: que el demandado haya asumido la responsabilidad, controle la fuente del peligro o lo haya creado.

Leídos así, los casos anteriores sobre la policía eran casos de omisión. En *Hill*, la madre de la última víctima de Peter Sutcliffe demandó a la policía que no había sabido detenerlo. La Cámara de los Lores sostuvo que no había deber alguno frente a su hija, que formaba parte de un público indeterminado, y añadió razones de política sobre el efecto de la responsabilidad en el trabajo policial.[@hill1989, 63] Lord Reed trató esas razones como un complemento de la regla ordinaria: la policía, como cualquiera, no tiene el deber de impedir el daño que causa un tercero, salvo que se dé alguna excepción; pero sí tiene el deber ordinario de no lesionar a nadie con sus propios actos.[@robinson2018, para 55]

La distinción ya había decidido *Michael*, donde una llamada al teléfono de emergencias se clasificó mal y la mujer que llamaba murió antes de que llegaran los agentes. La mayoría sostuvo que la policía no había asumido responsabilidad por su seguridad y que, por tanto, no le debía diligencia.[@michael2015, para 97–100] En la doctrina, una postura sostiene que un organismo público creado para proteger a las personas debería tener un deber estrecho cuando conoce una amenaza concreta contra una persona concreta;[^peripheral] la otra, que la regla de las omisiones se aplica a las autoridades públicas igual que a los particulares y que cualquier deber más amplio corresponde al Parlamento.[@nolan2011]

[^peripheral]: [@tofaris2016, 130–32; cf @stapleton1995, 304–06, sobre el efecto disuasorio de demandar a quien no es el autor principal del daño]

## La vía legal

El artículo 6(1) de la Human Rights Act 1998 declara ilícito que una autoridad pública actúe de forma incompatible con un derecho del Convenio,[@hra1998, § 6] y la víctima puede demandarla conforme al artículo 7.[@hra1998, § 7] En *Michael*, la demanda basada en el artículo 2 del Convenio pudo llegar a juicio aunque la de negligencia fracasara.[@michael2015, para 139] En *DSD*, el Tribunal Supremo condenó a la policía conforme al artículo 3 por fallos graves en la investigación de una serie de agresiones sexuales cometidas por un taxista.[@dsd2018, para 24]

El temor de *Hill* a una policía que actúe a la defensiva tiene su propia respuesta legal. Cuando un tribunal decide qué medidas debió tomar el demandado para cumplir el estándar de diligencia, puede tener en cuenta si exigirlas impediría una actividad deseable.[@compensation2006, § 1] Es una cuestión de incumplimiento, no de deber.

## Comentario

Un año después, el Tribunal Supremo aplicó el mismo método a un ayuntamiento que no había realojado a una familia acosada por sus vecinos, y no apreció deber alguno: no había creado el peligro ni asumido la seguridad de los niños.[@poole2019, para 64–65]

Quedan dos dificultades. La línea entre acción y omisión es fácil de enunciar y difícil de trazar: el agente que abre una puerta y deja entrar a un hombre violento ha actuado; el que no la cierra con llave, no, y para quien está al otro lado la diferencia puede ser mínima. La asunción de responsabilidad puede nacer de una promesa, de una práctica o de un servicio, y tras *Robinson* los tribunales no se ponen de acuerdo sobre cuándo apreciarla.[^generally] La sentencia no las resuelve, pero las lleva al análisis de las categorías reconocidas.

[^generally]: Sobre las excepciones y la jurisprudencia de cada una, véase en general [@winfield2020]

:::paragraphs{style="colophon"}
Escrito para el Recetario de Postext (CC BY 4.0). Autora y revista ficticias; no es asesoramiento jurídico. Las resoluciones son reales; algunas referencias son ilustrativas. Compuesto en Libre Baskerville, Libre Caslon Display y Libre Franklin (SIL OFL).
:::

:::references{format=csl-yaml}
- id: donoghue1932
  type: legal_case
  title: Donoghue v Stevenson
  title-short: Donoghue
  volume: "[1932]"
  container-title: AC
  page: "562"
  authority: HL
- id: anns1978
  type: legal_case
  title: Anns v Merton London Borough Council
  title-short: Anns
  volume: "[1978]"
  container-title: AC
  page: "728"
  authority: HL
- id: murphy1991
  type: legal_case
  title: Murphy v Brentwood District Council
  title-short: Murphy
  volume: "[1991] 1"
  container-title: AC
  page: "398"
  authority: HL
- id: caparo1990
  type: legal_case
  title: Caparo Industries plc v Dickman
  title-short: Caparo
  volume: "[1990] 2"
  container-title: AC
  page: "605"
  authority: HL
- id: sutherland1985
  type: legal_case
  title: Sutherland Shire Council v Heyman
  title-short: Sutherland
  volume: "157"
  container-title: CLR
  page: "424"
  authority: HCA
  issued: 1985
- id: smith1987
  type: legal_case
  title: Smith v Littlewoods Organisation Ltd
  title-short: Smith
  volume: "[1987]"
  container-title: AC
  page: "241"
  authority: HL
- id: stovin1996
  type: legal_case
  title: Stovin v Wise
  title-short: Stovin
  volume: "[1996]"
  container-title: AC
  page: "923"
  authority: HL
- id: hill1989
  type: legal_case
  title: Hill v Chief Constable of West Yorkshire
  title-short: Hill
  volume: "[1989]"
  container-title: AC
  page: "53"
  authority: HL
- id: michael2015
  type: legal_case
  title: Michael v Chief Constable of South Wales Police
  title-short: Michael
  authority: UKSC
  number: "2"
  issued: 2015
  volume: "[2015]"
  container-title: AC
  page: "1732"
- id: robinson2018
  type: legal_case
  title: Robinson v Chief Constable of West Yorkshire Police
  title-short: Robinson
  authority: UKSC
  number: "4"
  issued: 2018
  volume: "[2018]"
  container-title: AC
  page: "736"
- id: dsd2018
  type: legal_case
  title: Commissioner of Police of the Metropolis v DSD
  title-short: DSD
  authority: UKSC
  number: "11"
  issued: 2018
  volume: "[2019]"
  container-title: AC
  page: "196"
- id: poole2019
  type: legal_case
  title: N v Poole Borough Council
  title-short: Poole
  authority: UKSC
  number: "25"
  issued: 2019
  volume: "[2020]"
  container-title: AC
  page: "780"
- id: hra1998
  type: legislation
  title: Human Rights Act 1998
- id: compensation2006
  type: legislation
  title: Compensation Act 2006
- id: stapleton1995
  type: article-journal
  author: [{family: Stapleton, given: Jane}]
  title: "Duty of Care: Peripheral Parties and Alternative Opportunities for Deterrence"
  container-title: LQR
  volume: "111"
  page: "301"
  issued: 1995
- id: nolan2011
  type: article-journal
  author: [{family: Nolan, given: Donal}]
  title: The Liability of Public Authorities for Failing to Confer Benefits
  container-title: LQR
  volume: "127"
  page: "260"
  issued: 2011
- id: tofaris2016
  type: article-journal
  author: [{family: Tofaris, given: Stelios}, {family: Steel, given: Sandy}]
  title: Negligence Liability for Omissions and the Police
  container-title: CLJ
  volume: "75"
  page: "128"
  issued: 2016
- id: winfield2020
  type: book
  author: [{family: Goudkamp, given: James}, {family: Nolan, given: Donal}]
  title: Winfield and Jolowicz on Tort
  edition: 20
  publisher: Sweet & Maxwell
  issued: 2020
:::
`;

// ─── 3 · Fonts ──────────────────────────────────────────────────────────────
// Every face the design uses (gotcha: fonts-first).
const FONTS = {
  'Libre Baskerville': ['400', '400i', '700'],
  'Libre Caslon Display': ['400'],
  'Libre Franklin': ['400', '500', '600'],
};

// ─── 4 · Build & show ───────────────────────────────────────────────────────
await loadFonts(FONTS, markdown);
const doc = await buildWithFonts(() => buildDocument({ markdown }, config()), markdown);
const title = t({ en: 'A legal opinion cited in OSCOLA',
  es: 'Un dictamen jurídico citado en OSCOLA' });
showPages(doc, { title });
offerPdf(() => renderToPdf(doc, { fontProvider: fontsourceProvider }), `${RECIPE}.pdf`);

// ─── Kit ── helpers shared by every Cookbook recipe · postext.dev/cookbook ─────

// ─── Kit · core v1 ── the same in every recipe · postext.dev/cookbook ─────────
function mm(value) { return { value, unit: 'mm' }; }
function pt(value) { return { value, unit: 'pt' }; }
function em(value) { return { value, unit: 'em' }; }
/** The sample language's string: t({ en: 'Figure', es: 'Figura' }). */
function t(strings) { return strings[LANG] ?? Object.values(strings)[0]; }
/** A file in this recipe's assets folder, served from the Postext repo by jsDelivr. */
function asset(file) { return `https://cdn.jsdelivr.net/gh/drnachio/postext@main/cookbook/${RECIPE}/assets/${file}`; }

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

/** faces = { 'Family Name': ['400', '400i', '700'] }. `text` is the sample:
 *  letters beyond Latin-1 (č, ł, ő…) also load the latin-ext files. With
 *  `optional`, a face Fontsource does not ship is skipped instead of failing.
 *  Resolves to the number of faces added. */
async function loadFonts(faces, text = '', { optional = false } = {}) {
  kitStatus('Loading fonts…');
  const ranges = {
    latin: 'U+0000-00FF,U+0131,U+0152-0153,U+02BB-02BC,U+02C6,U+02DA,U+02DC,U+0304,U+0308,U+0329,'
      + 'U+2000-206F,U+20AC,U+2122,U+2191,U+2193,U+2212,U+2215,U+FEFF,U+FFFD',
    'latin-ext': 'U+0100-02BA,U+02BD-02C5,U+02C7-02CC,U+02CE-02D7,U+02DD-02FF,U+0304,U+0308,U+0329,'
      + 'U+1D00-1DBF,U+1E00-1E9F,U+1EF2-1EFF,U+2020,U+20A0-20AB,U+20AD-20C0,U+2113,U+2C60-2C7F,U+A720-A7FF',
  };
  const subsets = /[Ā-˿Ḁ-ỿ]/.test(text) ? ['latin', 'latin-ext'] : ['latin'];
  const jobs = [];
  let added = 0;
  for (const [family, specs] of Object.entries(faces)) {
    const id = fontsourceId(family);
    const meta = optional ? await fontsourceMeta(family) : null;
    for (const spec of new Set(specs)) {
      const weight = parseInt(spec, 10);
      const style = spec.endsWith('i') ? 'italic' : 'normal';
      if (hasFace(family, weight, style)) continue;
      if (optional && !(meta?.weights.includes(weight) && meta.styles.includes(style))) continue;
      for (const subset of subsets) {
        const url = `https://cdn.jsdelivr.net/npm/@fontsource/${id}@5/files/${id}-${subset}-${weight}-${style}.woff2`;
        const face = new FontFace(family, `url(${url}) format('woff2')`,
          { weight: String(weight), style, unicodeRange: ranges[subset] });
        jobs.push(face.load().then((ready) => { document.fonts.add(ready); added++; }, () => {
          if (subset === 'latin' && !optional) throw new Error(`Fontsource has no ${family} ${weight} ${style}`);
        }));
      }
    }
  }
  await Promise.all(jobs).catch((error) => { kitFail(error); throw error; });
  return added;
}

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

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

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

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

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

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

// ─── Kit · viewer v1 ── the same in every recipe · postext.dev/cookbook ───────
/** Shows the pages as facing spreads on a dark desk: the first page is a
 *  recto on its own, then verso | recto pairs, as in a bound book. Pages
 *  are painted when they scroll near the screen. */
function showPages(docs, { title, width = 460 } = {}) {
  const root = viewer(title);
  const pages = [docs].flat().flatMap((doc) =>
    doc.pages.map((page) => ({ doc, page, n: (doc.pageIndexOffset ?? 0) + page.index })));
  const spreads = [];
  let verso = null;
  for (const p of pages) {
    if (p.n % 2 === 1) { if (verso) spreads.push([verso, null]); verso = p; }
    else { spreads.push([verso, p]); verso = null; }
  }
  if (verso) spreads.push([verso, null]);
  const density = Math.min(window.devicePixelRatio || 1, 2);
  showPages.painter?.disconnect();
  const painter = new IntersectionObserver((entries) => {
    for (const { isIntersecting, target } of entries) {
      if (!isIntersecting) continue;
      painter.unobserve(target);
      const { doc, page } = target.postext;
      renderPageToCanvas(page, doc, target, { scale: (width * density) / page.width });
    }
  }, { rootMargin: '800px' });
  showPages.painter = painter;
  root.replaceChildren(...spreads.map((pair) => {
    const spread = document.createElement('div');
    spread.className = 'pt-spread';
    for (const p of pair) {
      const figure = document.createElement('figure');
      if (p) {
        const label = p.page.pageLabel || String(p.n + 1);
        const canvas = document.createElement('canvas');
        canvas.postext = p;
        canvas.style.aspectRatio = `${p.page.width} / ${p.page.height}`;
        canvas.setAttribute('role', 'img');
        canvas.setAttribute('aria-label', `Page ${label}`);
        const folio = document.createElement('figcaption');
        folio.textContent = label;
        figure.append(canvas, folio);
        painter.observe(canvas);
      } else figure.className = 'pt-blank';
      spread.append(figure);
    }
    return spread;
  }));
  kitStatus(`${pages.length} ${pages.length === 1 ? 'page' : 'pages'}`);
  document.documentElement.dataset.postext = 'ready';
  return pages.length;
}

/** The desk, the bar and the error reporting, created once. */
function viewer(title) {
  if (!document.getElementById('pt-kit')) {
    document.head.insertAdjacentHTML('beforeend', `<style id="pt-kit">
      :root { color-scheme: dark; }
      body { margin: 0; background: #0e1014; color: #b9bcc4; font: 13px/1.45 system-ui, sans-serif; }
      #pt-bar { position: sticky; top: 0; z-index: 1; display: flex; flex-wrap: wrap; align-items: center;
        gap: 6px 16px; padding: 10px 16px; background: rgb(14 16 20 / .92); backdrop-filter: blur(6px);
        border-bottom: 1px solid #23262d; }
      #pt-bar strong { color: #f4f1ea; font-weight: 600; }
      #pt-actions { display: flex; gap: 12px; margin-left: auto; }
      #pt-actions a, #pt-actions button { color: #d8a21a; font: inherit; background: none; border: 0; padding: 0; cursor: pointer; }
      #pages { display: grid; justify-items: center; gap: 48px; padding: 32px 16px 72px; }
      .pt-spread { display: flex; }
      .pt-spread figure { margin: 0; width: min(460px, 44vw); }
      .pt-spread canvas { display: block; width: 100%; background: #fff;
        box-shadow: 0 1px 2px rgb(0 0 0 / .5), 0 22px 44px -16px rgb(0 0 0 / .8); }
      .pt-spread figure:first-child canvas { box-shadow: inset -14px 0 14px -14px rgb(0 0 0 / .18), 0 1px 2px rgb(0 0 0 / .5), 0 22px 44px -16px rgb(0 0 0 / .8); }
      .pt-spread figcaption { margin-top: 10px; text-align: center; font: 600 10px/1 system-ui, sans-serif;
        letter-spacing: .18em; text-transform: uppercase; color: #6c7079; }
      .pt-blank { visibility: hidden; }
      @media (max-width: 760px) {
        .pt-spread { flex-direction: column; gap: 32px; }
        .pt-spread figure { width: min(460px, 92vw); }
        .pt-blank { display: none; }
      }
    </style>`);
    document.body.insertAdjacentHTML('afterbegin',
      '<header id="pt-bar"><strong id="pt-title"></strong><span id="pt-status" role="status"></span><span id="pt-actions"></span></header>');
    document.getElementById('pt-title').textContent = document.title || 'Postext';
    addEventListener('error', (event) => kitFail(event.error ?? event.message));
    addEventListener('unhandledrejection', (event) => kitFail(event.reason));
  }
  if (title) document.getElementById('pt-title').textContent = title;
  return document.getElementById('pages')
    ?? document.body.appendChild(Object.assign(document.createElement('main'), { id: 'pages' }));
}

function kitStatus(text) {
  viewer();
  document.getElementById('pt-status').textContent = text;
}

function kitFail(error) {
  document.documentElement.dataset.postext = 'error';
  kitStatus(`Error: ${error?.message ?? error}`);
}

// ─── Kit · pdf v1 ── the same in every recipe that exports a PDF ──────────────
/** postext-pdf embeds TrueType bytes. Fetch the Fontsource file the screen
 *  used, snapping to a weight the family ships and falling back to upright
 *  when it has no italic: the PDF asks for every face a block could use. */
async function fontsourceProvider(family, weight, style) {
  const id = fontsourceId(family);
  const meta = await fontsourceMeta(family);
  const weights = meta?.weights?.length ? meta.weights : [400, 700];
  const w = weights.reduce((a, b) => (Math.abs(b - weight) < Math.abs(a - weight) ? b : a));
  const s = style === 'italic' && meta && !meta.styles.includes('italic') ? 'normal' : style;
  const res = await fetch(`https://cdn.jsdelivr.net/npm/@fontsource/${id}@5/files/${id}-latin-${w}-${s}.woff2`);
  if (!res.ok) throw new Error(`Fontsource has no ${family} ${w} ${s} (${res.status})`);
  return decompressWoff2(new Uint8Array(await res.arrayBuffer()));
}

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

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

## Variantes

### Usa el estilo tal como viene

Para comparar con la salida sin retocar, usa el identificador incluido. Los párrafos de las notas siguientes saldrán como *para 55*.

```diff
-  style: 'custom', customStyle: OSCOLA, // the bundled 'oscola', patched above
+  style: 'oscola',
```

### Reúne las notas al final

Algunas revistas imprimen las notas después del texto. Las citas conservan sus formas completa y breve y su número.

```diff
-  placement: 'column', numbering: 'chapter',
+  placement: 'chapterEnd', numbering: 'chapter',
```

## Errores frecuentes

- **Cualquier objeto headings desactiva el salto de página del H1.** Por defecto un H1 salta a una página impar (always-odd), pero cualquier objeto headings anula ese valor, así que los capítulos van seguidos y span: 'page' no hace nada. Vuelve a declarar headings.levels[0].breakBefore: { enabled: true, parity } en cada configuración.
- **Carga todas las fuentes antes de componer.** La composición mide el texto con las fuentes que el navegador ha cargado y guarda los anchos, así que una fuente que llega después de la primera composición deja cortes de línea erróneos y un PDF que ya no coincide con la pantalla. Carga antes todos los pesos y estilos, y llama a clearMeasurementCache() antes de recomponer si alguna llega tarde.
- **Una paleta cambiada no llega a los elementos de diseño ni al color de las remisiones.** postext 1.4.1 aplica colorPalette a los estilos de texto (cuerpo, títulos, listas, pies, tablas, recuadros), pero no a los elementos de cabeceras, pies de página, aperturas y portadillas, ni a bodyText.referenceColor: conservan el hex escrito junto a su paletteId. Si cambias la paleta, para una edición de pantalla oscura o para recolorear, reescribe cada color enlazado a partir de colorPalette antes de componer.
- **Solo 8 idiomas tienen separación silábica, con el código exacto.** La separación silábica existe para en-us, es, fr, de, it, pt, ca y nl, con el código exacto: 'es-ES' o cualquier otro idioma pasa sin aviso al inglés americano.
- **Una configuración se cachea por identidad: crea un objeto nuevo.** El motor guarda en caché las configuraciones resueltas según la identidad del objeto, así que modificar el mismo objeto y volver a componer reutiliza el resultado anterior. Crea un objeto nuevo en cada composición: por eso la configuración de una receta es una función, config().

- Un repertorio cuyo tomo es un número a secas, `volume: "2"`, imprime el año entre paréntesis, *(1990) 2 AC*, que OSCOLA reserva para series numeradas a lo largo de los años, como los Commonwealth Law Reports (nota 7). En los Appeal Cases y los Weekly Law Reports escribe el año entre corchetes dentro de `volume`.
- `s` delante de un localizador significa página (la *Seite* alemana), no artículo: escribe `§ 6`. Un apartado como `6(1)` no se lee como localizador; nómbralo en la frase y cita el artículo.
- OSCOLA no lleva bibliografía en un artículo, y el estilo no separa tablas de casos y de leyes. Una tesis que las necesite obtiene de `:::bibliography` una sola lista ordenada, con la doctrina primero y después casos y leyes mezclados.

## Créditos

- Receta: Ignacio Ferro ([@drnachio](https://github.com/drnachio))
- Tipografías: Libre Baskerville (OFL-1.1), Libre Caslon Display (OFL-1.1), Libre Franklin (OFL-1.1)
- Código: MIT · Contenido de ejemplo: CC-BY-4.0

## Relacionadas

- [N.º 089 · Un ensayo de historia con notas de Chicago](https://postext.dev/es/cookbook/history-essay-chicago-notes.md): Un ensayo de historia de cuatro páginas cuyas citas [@clave, 87] salen como notas de Chicago al pie: completas la primera vez, breves después. · Nivel 2 (Intermedio) · Artículos y trabajos académicos
- [N.º 096 · Un ensayo de humanidades con obras citadas en MLA](https://postext.dev/es/cookbook/mla-humanities-essay.md): Un ensayo literario cuyas citas [@clave, 48] salen en MLA 9, autor y página, con una cita en bloque y la lista de obras citadas tomada de datos CSL-YAML. · Nivel 2 (Intermedio) · Artículos y trabajos académicos
- [N.º 095 · Un artículo médico en estilo Vancouver](https://postext.dev/es/cookbook/medical-article-vancouver.md): Un ensayo clínico en A4 a dos columnas: números de cita volados, una bibliografía numerada al modo Vancouver y DOI que se abren como enlaces en el PDF. · Nivel 2 (Intermedio) · Artículos y trabajos académicos
