# Ein Physik-Letter mit Datenabbildung oben auf der Seite

> Der Entdeckungs-Letter zu GW150914 zweispaltig neu gesetzt: Strain-Daten aus offenen Daten über Seite 1, Zitate im Stil [1], nummerierte Formeln und Table I.

- HTML-Fassung: https://postext.dev/de/cookbook/physics-letter-two-columns
- Rezept Nº 140 · Abbildungen und Bilder · Stufe 3 (Fortgeschritten) · Ausgaben: Canvas, PDF
- Gattungen: Fachartikel und Wissenschaft
- Benötigt postext ≥ 1.25.0, postext-pdf ≥ 1.25.0 · getestet mit 1.31.0, postext-pdf 1.31.0 am 2026-10-13
- Seiten: [1](https://postext.dev/cookbook/physics-letter-two-columns/en/p01.webp?v=0c4d011d), [2](https://postext.dev/cookbook/physics-letter-two-columns/en/p02.webp?v=0c4d011d), [3](https://postext.dev/cookbook/physics-letter-two-columns/en/p03.webp?v=0c4d011d), [4](https://postext.dev/cookbook/physics-letter-two-columns/en/p04.webp?v=0c4d011d), [5](https://postext.dev/cookbook/physics-letter-two-columns/en/p05.webp?v=0c4d011d)
- PDF: https://postext.dev/cookbook/physics-letter-two-columns/en/physics-letter-two-columns.pdf?v=0c4d011d
- In der Sandbox öffnen: https://postext.dev/de/sandbox#recipe=physics-letter-two-columns&lang=en (.postext: https://postext.dev/cookbook/physics-letter-two-columns/en/physics-letter-two-columns.postext)
- Zuletzt aktualisiert: 2026-10-10
- Andere Sprachen: [en](https://postext.dev/en/cookbook/physics-letter-two-columns.md), [es](https://postext.dev/es/cookbook/physics-letter-two-columns.md), [ca](https://postext.dev/ca/cookbook/physics-letter-two-columns.md), [fr](https://postext.dev/fr/cookbook/physics-letter-two-columns.md), [it](https://postext.dev/it/cookbook/physics-letter-two-columns.md), [pt](https://postext.dev/pt/cookbook/physics-letter-two-columns.md), [zh](https://postext.dev/zh/cookbook/physics-letter-two-columns.md), [ja](https://postext.dev/ja/cookbook/physics-letter-two-columns.md), [ar](https://postext.dev/ar/cookbook/physics-letter-two-columns.md)

## Kurz gesagt

Der Letter von 2016, der die ersten Gravitationswellen verkündete, gekürzt und neu gesetzt auf US Letter. Das Signal beider Detektoren, aus öffentlichen Daten gezeichnet, läuft über Seite 1; die Quellen sind in eckigen Klammern nummeriert.

## Was Sie setzen

Fünf Seiten des Letters, der die ersten Gravitationswellen verkündete, *Observation of Gravitational Waves from a Binary Black Hole Merger* (Abbott et al., 2016), gekürzt und zweispaltig auf US Letter neu gesetzt. Seite 1 beginnt mit dem Beweis: Der in Hanford und Livingston aufgezeichnete Strain, neu gezeichnet aus den offenen Datendateien des Gravitational Wave Open Science Center, läuft unter dem Titel über beide Spalten. Darunter folgen die Autorenzeile der Kollaboration mit ihrer Fußnote, das Abstract über die ganze Seitenbreite und der Text, mit Zitaten nummeriert als [1,2] und [25–27], wie die APS-Zeitschriften sie drucken. Die Chirp-Masse ist Gleichung (1). Table I setzt ihre asymmetrischen Fehler als übereinandergestellte Hoch- und Tiefstellungen, in den Zellen als Mathematik geschrieben, und die Bildunterschriften drucken die eigenen Formeln des Letters. Bringen Sie Ihre eigenen BibTeX- und Datendateien mit, und dasselbe Skript setzt Ihren Letter.

**Dieses Rezept beantwortet:**

- Wie setze ich einen Physik-Letter mit Kollaborationszeile, seitenbreitem Abstract und Datenabbildung oben?
- Wie bestimme ich, wo eine Abbildung steht: oben auf der Seite, über beide Spalten, genau hier oder am Rand?
- Wie setze ich Formeln (im Text, abgesetzt, Gleichungen) und behalte sie im PDF als Vektoren?
- Wie setze ich einen Open-Access-Artikel aus arXiv oder PubMed Central mit Postext neu, mit seinen Zitaten, Abbildungen und seiner Lizenzzeile?

## Die kurze Antwort

```js
// script.js, Zeilen 32–60
// [@key] numbers works by first citation. APS journals print [1,2] and [2–4] in one pair of
// brackets and list "A. Einstein, Sitzungsber. K. Preuss. Akad. Wiss. 1, 688 (1916).": no
// title, volume bold. No APS style is bundled: four edits turn the IEEE file into one.
registerCitationEngine(createCiteprocEngine({ styles: STYLES, locales: LOCALES }));
const ARTICLE = '<group delimiter=" "><text variable="container-title"/><text '
  + 'variable="volume" font-weight="bold" suffix=","/><text variable="page"/><date '
  + 'variable="issued" prefix="(" suffix=")"><date-part name="year"/></date></group>';
const BOOK = '<group delimiter=" "><group delimiter=", "><choose><if type="chapter"><text '
  + 'variable="container-title" prefix="in " font-style="italic"/></if><else><text variable='
  + '"title" font-style="italic"/></else></choose><names variable="editor" prefix="edited by ">'
  + '<name and="text" initialize-with=". "/></names><text variable="collection-title"/><text '
  + 'variable="volume" prefix="Vol. "/></group><group prefix="(" suffix=")" delimiter=", ">'
  + '<text variable="publisher"/><text variable="publisher-place"/><date variable="issued">'
  + '<date-part name="year"/></date></group></group><text variable="page" prefix="pp. "/>';
const aps = STYLES.ieee
  .replace('<citation>', '<citation collapse="citation-number">') // [2–4], not [2], [3], [4]
  .replace(/<layout delimiter=", ">\s*<group prefix="\[" suffix="\]" delimiter=", ">/,
    '<layout prefix="[" suffix="]" delimiter=","><group delimiter=", ">') // [1,2]
  .replace('et-al-min="7"', 'et-al-min="11"') // ten authors are listed, as in the letter
  .replace(/<bibliography[\s\S]*<\/bibliography>/, '<bibliography second-field-align="flush">'
    + '<layout suffix="."><text variable="citation-number" prefix="[" suffix="]"/><text '
    + 'macro="author"/><choose><if type="manuscript"><text variable="note" prefix=" "/></if>'
    + `<else><group prefix=", " delimiter=", "><choose><if type="article-journal">${ARTICLE}`
    + `</if><else-if type="book chapter" match="any">${BOOK}</else-if><else><text variable=`
    + '"URL"/><text variable="note"/></else></choose></group></else></choose></layout>'
    + '</bibliography>');
const citations = { style: 'custom', customStyle: aps, link: true,
  bibliography: { fontSize: em(0.84), lineHeight: pt(9.5), entrySpacing: pt(0.4),
    labelWidth: mm(6.2) } }; // the [64] column: turnovers line up after the widest label
```

## Zutaten

**Zeigt**

- [Abbildungen genau hier](https://postext.dev/de/docs/document-format.md#blockeinbettung-optional-ausdrückliche-platzierung-im-text): Eine Ressource mit Position hier, mit ::resource an einer exakten Stelle im Fluss eingebettet statt gleitend.
- [Platzierung von Abbildungen](https://postext.dev/de/docs/document-format.md#platzierung): Oben, unten, automatisch oder hier, in einer Spalte oder über die Seite, mit einem Bruchteil der Breite, je Ressource oder je Typ; Gleitobjekte einer Folge überholen einander nie.
- [Zitate in einem Zitierstil](https://postext.dev/de/docs/document-format.md#zitate-und-literaturverzeichnis): Werke, zitiert als [@key, p. 33] und in einem CSL-Stil formatiert (APA, Chicago, MLA, IEEE, Vancouver, ISO 690, GB/T 7714…), der in den Einstellungen gewählt wird, mit Links zu ihren Einträgen.

**Verwendet außerdem**

- [Mathematik](https://postext.dev/de/docs/document-format.md#mathematische-formeln)
- [Tabellen aus Daten](https://postext.dev/de/docs/document-format.md#blockeinbettung-optional-ausdrückliche-platzierung-im-text)
- [Nummerierte Bildunterschriften](https://postext.dev/de/docs/document-format.md#nummerierung-beim-ersten-verweis)
- [Eigene Ressourcentypen](https://postext.dev/de/docs/configuration-resources.md#ressourcentypen)
- [Format der Bildunterschrift](https://postext.dev/de/docs/configuration-resources.md#legendenstil)
- [Quellen- und Bildnachweise](https://postext.dev/de/docs/configuration-resources.md#legendenstil)
- [Fußnoten](https://postext.dev/de/docs/document-format.md#fußnoten)
- [Kästen über die Seite](https://postext.dev/de/docs/configuration-styles.md#der-container-callout)
- [Kästen](https://postext.dev/de/docs/configuration-styles.md#kastenstile)
- [Überschriftenformate](https://postext.dev/de/docs/configuration-styles.md#überschriftenstile)
- [Nummerierte Überschriften](https://postext.dev/de/docs/configuration-text.md#überschreibungen-pro-ebene)
- [Überschriftenattribute](https://postext.dev/de/docs/document-format.md#attribute-von-überschriften)
- [Gestaltete Auftakte](https://postext.dev/de/docs/configuration-text.md#seitenbreite-und-erweiterte-gestaltung)
- [Kolumnentitel und Seitenzahlen](https://postext.dev/de/docs/configuration-page-layout.md#kopf--und-fußzeilen)
- [Literaturverzeichnis aus den Quellen](https://postext.dev/de/docs/document-format.md#zitate-und-literaturverzeichnis)
- [Absatzformate](https://postext.dev/de/docs/configuration-styles.md#absatzformate)
- [PDF-Export](https://postext.dev/de/docs/configuration-programmatic-usage.md#pdfs-erzeugen)
- [Verweise, die Abbildungen platzieren](https://postext.dev/de/docs/document-format.md#verweis-im-text-die-hauptform)
- [Abbildung und Tabelle in Ihrer Sprache](https://postext.dev/de/docs/configuration-resources.md#ressourcentypen)
- [Kopfzeilen nach Seitenrolle](https://postext.dev/de/docs/configuration-page-layout.md#textelemente)
- [Im PDF eingebettete Schriften](https://postext.dev/de/docs/configuration-programmatic-usage.md#warum-ein-font-provider)
- [Abbildungen und Tabellen als Ressourcen](https://postext.dev/de/docs/document-format.md#ressourcen)
- [Hoch- und Tiefstellung](https://postext.dev/de/docs/document-format.md#inline-formatierung)
- [Zeilenumbrüche in Titeln](https://postext.dev/de/docs/document-format.md#zeilenumbrüche-in-titeln)
- [Kapitel ohne Nummer](https://postext.dev/de/docs/configuration-styles.md#überschriftenstile)
- [Expliziter vertikaler Abstand](https://postext.dev/de/docs/document-format.md#space)

**Die Konfiguration auf einen Blick**

- [`bodyText`](https://postext.dev/de/docs/configuration-text.md#fließtext), [`calloutStyles`](https://postext.dev/de/docs/configuration-styles.md#kastenstile), [`captionStyle`](https://postext.dev/de/docs/configuration-resources.md#legendenstil), [`citations`](https://postext.dev/de/docs/configuration-notes-references.md#zitationen), [`colorPalette`](https://postext.dev/de/docs/configuration-fonts-colors-viewers.md#farbpalette), [`footer`](https://postext.dev/de/docs/configuration-page-layout.md#kopf--und-fußzeilen), `footnotes`, [`header`](https://postext.dev/de/docs/configuration-page-layout.md#kopf--und-fußzeilen), [`headingStyles`](https://postext.dev/de/docs/configuration-styles.md#überschriftenstile), [`headings`](https://postext.dev/de/docs/configuration-text.md#überschriften), [`layout`](https://postext.dev/de/docs/configuration-page-layout.md#layout), [`locale`](https://postext.dev/de/docs/configuration-text.md#silbentrennung), [`math`](https://postext.dev/de/docs/configuration-text.md#mathematik), [`page`](https://postext.dev/de/docs/configuration-page-layout.md#seite), [`paragraphStyles`](https://postext.dev/de/docs/configuration-styles.md#absatzformate), [`resourceTypes`](https://postext.dev/de/docs/configuration-resources.md#ressourcentypen), [`tableStyle`](https://postext.dev/de/docs/configuration-resources.md#tabellenstil)

**APIs**

- [`LOCALES`](https://postext.dev/de/docs/document-format.md#zitate-und-literaturverzeichnis), [`STYLES`](https://postext.dev/de/docs/document-format.md#zitate-und-literaturverzeichnis), [`buildDocumentWithFonts`](https://postext.dev/de/docs/configuration-programmatic-usage.md#schriften-vor-dem-layout-laden), [`createCiteprocEngine`](https://postext.dev/de/docs/document-format.md#zitate-und-literaturverzeichnis), [`decompressWoff2`](https://postext.dev/de/docs/configuration-programmatic-usage.md#font-provider-im-browser-fontsource--woff2), [`defaultResourceTypes`](https://postext.dev/de/docs/configuration-resources.md#ressourcentypen), [`initMathEngine`](https://postext.dev/de/docs/document-format.md#mathematische-formeln), [`registerCitationEngine`](https://postext.dev/de/docs/document-format.md#zitate-und-literaturverzeichnis), [`registerResourceImage`](https://postext.dev/de/docs/architecture.md#api-oberfläche), `renderMath`, [`renderPageToCanvas`](https://postext.dev/de/docs/configuration-programmatic-usage.md#eine-seite-als-bitmap-rendern), [`renderToPdf`](https://postext.dev/de/docs/configuration-programmatic-usage.md#pdfs-erzeugen)

**Schriften**

- Gelasio (OFL-1.1), Albert Sans (OFL-1.1)

## Zubereitung

### 1 · Nummerierte Physikzitate aus der IEEE-Datei

Der Code steht oben in [der kurzen Antwort](https://postext.dev/de/cookbook/physics-letter-two-columns.md#die-kurze-antwort). postext-citeproc bringt keinen APS-Stil mit, und der mitgelieferte Stil `ieee` kommt ihm am nächsten: Reihenfolge der Zitate, Nummern in eckigen Klammern, Initialen vor dem Familiennamen. Vier Zeichenkettenersetzungen in `STYLES.ieee` schließen die Lücke. `collapse="citation-number"` fasst Folgen zusammen (*[25–27]*), ein einziges Klammerpaar im Layout druckt *[1,2]*, `et-al-min` steigt auf elf, sodass eine Liste von zehn Autoren vollständig bleibt, und ein neuer Block `<bibliography>` druckt die Quelle so wie Physical Review, mit Zeitschrift, fetter Bandnummer, Seite und Jahr: *Phys. Rev. Lett.* **74**, 3515 (1995). Die 43 Werke stehen in einem eigenen Block `:::references{format=bibtex}`; ein Name in doppelten geschweiften Klammern wie `{{J. Aasi et al.}}` wird so gedruckt, wie er geschrieben ist, und so zitiert der Letter die langen Autorenlisten seiner eigenen Kollaboration. Siehe [Zitate und Literaturverzeichnis](https://postext.dev/de/docs/document-format.md#zitate-und-literaturverzeichnis).

### 2 · Abbildung 1 unter dem Titel, über beide Spalten

```js
// script.js, Zeilen 64–89
const text = (id, content, family, size, extra) => ({ kind: 'text', id, content, align: 'left',
  fontFamily: family, fontSize: pt(size), color: col('ink'), overflow: 'wrap', ...extra });
const at = (to, edge, x, y, width) => ({ anchor: { to, edge }, offset: { x: mm(x), y: mm(y) },
  ...(width && { size: { width: mm(width), height: 'auto' } }) });
const caps = (size) => ({ fontWeight: 700, letterSpacing: pt(size * 0.2),
  textTransform: 'uppercase', color: col('accent') });
const letter = { id: 'letter', numbered: false, span: 'page', marginBottom: pt(0),
  advancedDesign: { enabled: true, minHeight: mm(21), slot: { elements: [
    text('kicker', '{attr.kicker}', SANS, 7.5, { ...caps(7.5),
      placement: at('container', 'top-left', 0, 2, MEASURE) }),
    text('title', '{titleText}', SANS, 23, { fontWeight: 700, lineHeight: 1.08,
      placement: at('#kicker', 'below', 0, 2.2, MEASURE) }),
  ] } } };
const box = { padding: { top: mm(2.6), right: mm(12), bottom: mm(2.4), left: mm(12) },
  background: col('tint'), marginTop: pt(0), marginBottom: pt(LEAD), span: 'page',
  body: { fontSize: pt(9.3), lineHeight: pt(12.6), firstLineIndent: pt(0) } };
// ::resource{id="fig1"} right under the title, placed { position: 'here', span: 'page' }:
// it spans both columns on page 1, where a float could only follow its citation.
const paragraphStyles = [
  { id: 'byline', fontFamily: SANS, fontSize: pt(10), lineHeight: pt(12.6), fontWeight: 600,
    textAlign: 'center', firstLineIndent: pt(0) },
  { id: 'dates', fontFamily: SANS, fontSize: pt(7.6), lineHeight: pt(12.6), textAlign: 'center',
    color: col('muted'), firstLineIndent: pt(0) },
  { id: 'colophon', fontFamily: SANS, fontSize: pt(7), lineHeight: pt(9.4), color: col('muted'),
    textAlign: 'left', firstLineIndent: pt(0), marginTop: pt(LEAD) },
];
```

Ein Gleitobjekt steht nie über der Zeile, die es zitiert, und der Text zitiert Abbildung 1 erstmals in Abschnitt II; als Gleitobjekt käme sie also nicht auf Seite 1. Der Letter setzt sie direkt dorthin: `::resource{id="fig1"}` folgt unmittelbar auf den Titel, und seine Platzierung `{ position: 'here', span: 'page' }` setzt die Abbildung an dieser Stelle über beide Spalten, die darunter weiterlaufen. Autorenzeile und Abstract folgen in einem Kasten über die ganze Seitenbreite, `front`, dessen Blöcke `:::paragraphs` die Autorenzeile zentrieren und die Daten klein setzen. Das Fußnotenzeichen nach *et al.* schickt seine Anmerkung an den Fuß der ersten Spalte, wie die APS-Zeitschriften die Autorennotiz drucken, und `footnotes.numberFormat: 'lower-alpha'` zählt die Anmerkungen mit Buchstaben, damit sie nie wie Quellennummern aussehen. Siehe [Blockeinbettung](https://postext.dev/de/docs/document-format.md#blockeinbettung-optional-ausdrückliche-platzierung-im-text) und [Fußnoten](https://postext.dev/de/docs/configuration-notes-references.md#fußnoten).

### 3 · Table I mit Mathematik in den Zellen

```js
// script.js, Zeilen 345–353
// A cell sets $…$ in the line, at the table's size: the errors stack over and under the value.
const VALUES = [['Primary black hole mass', '$36^{+5}_{-4}\\,M_\\odot$'],
  ['Secondary black hole mass', '$29^{+4}_{-4}\\,M_\\odot$'],
  ['Final black hole mass', '$62^{+4}_{-4}\\,M_\\odot$'],
  ['Final black hole spin', '$0.67^{+0.05}_{-0.07}$'],
  ['Luminosity distance', '$410^{+160}_{-180}$ Mpc'],
  ['Source redshift $z$', '$0.09^{+0.03}_{-0.04}$']];
const table = { model: { columnWidths: [1.3, 1], rows: VALUES.map(([label, value]) =>
  [{ content: label, verticalAlign: 'middle' }, { content: value }]) } };
```

Eine Tabellenzelle setzt `$…$` wie der Text (seit postext 1.19); jeder Wert wird also als TeX geschrieben, `$36^{+5}_{-4}\,M_\odot$`, und seine beiden Fehler stehen über und unter der Zahl, auf die Zeile der Zelle skaliert; das Markdown `^+5^~−4~` würde sie nacheinander drucken. Bildunterschriften und Anmerkungen nehmen dieselbe Mathematik auf: Abbildung 2 schreibt `$R_S = 2GM/c^2$` und Abbildung 4 `$5.1\sigma$`, gesetzt in den Glyphen von MathJax, die das Griechische haben, das keine der beiden Textschriften enthält, und das PDF behält sie als Vektorpfade. Die Ressourcentypen drucken in den Bildunterschriften *FIG. 1* und *TABLE I* und im Text *Fig. 1* und *Table I*: ein `captionPrefix` und für Tabellen `counterFormat: 'upper-roman'`. Siehe [Mathematische Formeln](https://postext.dev/de/docs/document-format.md#mathematische-formeln).

### 4 · Aus den offenen Daten gezeichnete Abbildungen

```js
// script.js, Zeilen 17–22
const palette = { ink: '#171a21', accent: '#25578a', tint: '#eef2f6', rule: '#b7c0ca',
  muted: '#5a636e', paper: '#ffffff' };
const data = { hanford: '#cf5f24', livingston: palette.accent, purple: '#74519c' }; // figures
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' } }));
```

Die vier Abbildungen sind in Code aus den Datendateien gezeichnet, die GWOSC mit dem Letter unter CC BY 4.0 veröffentlicht. Jede Kurve behält jeden achten Messwert, 430 Punkte für die 0,21 s von Abbildung 1, gespeichert als ein Base64-Zeichen je Punkt für die Änderung gegenüber dem vorigen, was die fünf Kurven im Skript unter 3 KB hält. Die Reihe der Residuen ist, wie veröffentlicht, Daten minus Wellenform. Hanford ist in jeder Abbildung orange und Livingston blau, und der Text behält die Textfarbe der Seite und ein einziges Blau. Die Beschriftungen sind MathJax-Pfade in dessen serifenloser Schrift (`\textsf`), sodass ein Exponent oder ein Symbol an einer Achse wie die Formeln des Texts gesetzt ist, und das PDF behält jede Linie und jede Beschriftung als Vektoren.

## Das ganze Rezept

Eine Datei, aus dem Ordner des Rezepts zusammengesetzt, mit dem Beispieltext und dem gemeinsamen Baukasten der Rezepte bereits eingebettet; sie baut ihre eigene Seite auf. Zum Ausführen setzen Sie sie in ein `<script type="module">` auf einer leeren Seite oder fügen sie in das JS-Feld eines neuen CodePen-Pens ein (als Modul). Sie importiert postext von esm.sh, es gibt also nichts zu installieren oder zu bauen.

- Quellordner: https://github.com/drnachio/postext/tree/main/cookbook/physics-letter-two-columns

### script.js

```js
// ═══ Postext Cookbook · Nº 140 · A physics letter with a data figure across the top ═══
// https://postext.dev/en/cookbook/physics-letter-two-columns
// Code: MIT · Text: Abbott et al., PRL 116, 061102 (CC BY 3.0) · Data: GWOSC (CC BY 4.0)
// Fonts: Gelasio, Albert Sans (SIL OFL 1.1) · Needs postext ≥ 1.25.0
import {
  buildDocumentWithFonts, renderPageToCanvas, registerResourceImage, registerCitationEngine,
  defaultResourceTypes, initMathEngine, renderMath,
} 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 = 'en'; // @lang: the language of the sample document ('en' | 'es')
const RECIPE = 'physics-letter-two-columns';

// ─── 1 · Design ─────────────────────────────────────────────────────────────
// #region palette: ink and one blue for the text; the detectors' colours for the data
const palette = { ink: '#171a21', accent: '#25578a', tint: '#eef2f6', rule: '#b7c0ca',
  muted: '#5a636e', paper: '#ffffff' };
const data = { hanford: '#cf5f24', livingston: palette.accent, purple: '#74519c' }; // figures
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' } }));
// #endregion
const [SERIF, SANS] = ['Gelasio', 'Albert Sans'];
// mm: US Letter, head, foot and side margins, gutter; then the measure and one column
const [TRIM_W, TRIM_H, TOP, BOTTOM, SIDE, GUTTER] = [215.9, 279.4, 21, 21, 16, 7];
const MEASURE = TRIM_W - 2 * SIDE;
const COLUMN = (MEASURE - GUTTER) / 2;
const [BODY, LEAD] = [9.3, 12.4]; // pt: a letter journal's density, two columns of 88 mm

// #region answer: physics citations [1] and [2–4], from the bundled IEEE style
// [@key] numbers works by first citation. APS journals print [1,2] and [2–4] in one pair of
// brackets and list "A. Einstein, Sitzungsber. K. Preuss. Akad. Wiss. 1, 688 (1916).": no
// title, volume bold. No APS style is bundled: four edits turn the IEEE file into one.
registerCitationEngine(createCiteprocEngine({ styles: STYLES, locales: LOCALES }));
const ARTICLE = '<group delimiter=" "><text variable="container-title"/><text '
  + 'variable="volume" font-weight="bold" suffix=","/><text variable="page"/><date '
  + 'variable="issued" prefix="(" suffix=")"><date-part name="year"/></date></group>';
const BOOK = '<group delimiter=" "><group delimiter=", "><choose><if type="chapter"><text '
  + 'variable="container-title" prefix="in " font-style="italic"/></if><else><text variable='
  + '"title" font-style="italic"/></else></choose><names variable="editor" prefix="edited by ">'
  + '<name and="text" initialize-with=". "/></names><text variable="collection-title"/><text '
  + 'variable="volume" prefix="Vol. "/></group><group prefix="(" suffix=")" delimiter=", ">'
  + '<text variable="publisher"/><text variable="publisher-place"/><date variable="issued">'
  + '<date-part name="year"/></date></group></group><text variable="page" prefix="pp. "/>';
const aps = STYLES.ieee
  .replace('<citation>', '<citation collapse="citation-number">') // [2–4], not [2], [3], [4]
  .replace(/<layout delimiter=", ">\s*<group prefix="\[" suffix="\]" delimiter=", ">/,
    '<layout prefix="[" suffix="]" delimiter=","><group delimiter=", ">') // [1,2]
  .replace('et-al-min="7"', 'et-al-min="11"') // ten authors are listed, as in the letter
  .replace(/<bibliography[\s\S]*<\/bibliography>/, '<bibliography second-field-align="flush">'
    + '<layout suffix="."><text variable="citation-number" prefix="[" suffix="]"/><text '
    + 'macro="author"/><choose><if type="manuscript"><text variable="note" prefix=" "/></if>'
    + `<else><group prefix=", " delimiter=", "><choose><if type="article-journal">${ARTICLE}`
    + `</if><else-if type="book chapter" match="any">${BOOK}</else-if><else><text variable=`
    + '"URL"/><text variable="note"/></else></choose></group></else></choose></layout>'
    + '</bibliography>');
const citations = { style: 'custom', customStyle: aps, link: true,
  bibliography: { fontSize: em(0.84), lineHeight: pt(9.5), entrySpacing: pt(0.4),
    labelWidth: mm(6.2) } }; // the [64] column: turnovers line up after the widest label
// #endregion

// #region title: the title, Figure 1 across the page, then the byline and the abstract
const text = (id, content, family, size, extra) => ({ kind: 'text', id, content, align: 'left',
  fontFamily: family, fontSize: pt(size), color: col('ink'), overflow: 'wrap', ...extra });
const at = (to, edge, x, y, width) => ({ anchor: { to, edge }, offset: { x: mm(x), y: mm(y) },
  ...(width && { size: { width: mm(width), height: 'auto' } }) });
const caps = (size) => ({ fontWeight: 700, letterSpacing: pt(size * 0.2),
  textTransform: 'uppercase', color: col('accent') });
const letter = { id: 'letter', numbered: false, span: 'page', marginBottom: pt(0),
  advancedDesign: { enabled: true, minHeight: mm(21), slot: { elements: [
    text('kicker', '{attr.kicker}', SANS, 7.5, { ...caps(7.5),
      placement: at('container', 'top-left', 0, 2, MEASURE) }),
    text('title', '{titleText}', SANS, 23, { fontWeight: 700, lineHeight: 1.08,
      placement: at('#kicker', 'below', 0, 2.2, MEASURE) }),
  ] } } };
const box = { padding: { top: mm(2.6), right: mm(12), bottom: mm(2.4), left: mm(12) },
  background: col('tint'), marginTop: pt(0), marginBottom: pt(LEAD), span: 'page',
  body: { fontSize: pt(9.3), lineHeight: pt(12.6), firstLineIndent: pt(0) } };
// ::resource{id="fig1"} right under the title, placed { position: 'here', span: 'page' }:
// it spans both columns on page 1, where a float could only follow its citation.
const paragraphStyles = [
  { id: 'byline', fontFamily: SANS, fontSize: pt(10), lineHeight: pt(12.6), fontWeight: 600,
    textAlign: 'center', firstLineIndent: pt(0) },
  { id: 'dates', fontFamily: SANS, fontSize: pt(7.6), lineHeight: pt(12.6), textAlign: 'center',
    color: col('muted'), firstLineIndent: pt(0) },
  { id: 'colophon', fontFamily: SANS, fontSize: pt(7), lineHeight: pt(9.4), color: col('muted'),
    textAlign: 'left', firstLineIndent: pt(0), marginTop: pt(LEAD) },
];
// #endregion

// Running heads 12 mm from the trim; the opener has the source line at its foot.
const head = (id, content, parity, edge, x, extra) => text(id, content, SANS, 7.5, {
  parity, pages: 'body', fontWeight: 500, color: col('muted'), overflow: 'clip',
  placement: at('page', edge, x, 12, 120), ...extra });
const folio = { fontWeight: 700, color: col('accent') };
const right = { align: 'right' };
const header = { elements: [
  head('v-folio', '{pageNumber}', 'even', 'top-left', SIDE, folio),
  head('v-title', 'B. P. Abbott et al. · Observation of gravitational waves', 'even',
    'top-left', SIDE + 7),
  head('r-title', 'GW150914 · a binary black hole merger', 'odd', 'top-right', -SIDE - 7,
    right),
  head('r-folio', '{pageNumber}', 'odd', 'top-right', -SIDE, { ...folio, ...right }),
] };
const footer = { elements: [
  head('source', 'Re-set from Phys. Rev. Lett. 116, 061102 (2016) · CC BY 3.0 · '
    + 'doi:10.1103/PhysRevLett.116.061102', 'all', 'bottom-left', SIDE,
  { pages: 'opener', placement: at('page', 'bottom-left', SIDE, -12, 160) }),
  head('drop-folio', '{pageNumber}', 'all', 'bottom-right', -SIDE, { ...folio, ...right,
    pages: 'opener', placement: at('page', 'bottom-right', -SIDE, -12, 10) }),
] };

const sans = (size, weight) => ({ fontFamily: SANS, fontSize: pt(size), fontWeight: weight });
const config = () => ({
  locale: 'en-us', colorPalette, citations, header, footer, paragraphStyles,
  // FIG. 1 and TABLE I in the captions, Fig. 1 and Table I in the text: the APS convention
  resourceTypes: defaultResourceTypes(LANG).map((type) => ({ ...type, numberingTemplate: '{n}',
    resetOn: 'never', ...(type.id === 'figure' ? { captionPrefix: 'FIG.' }
      : { captionPrefix: 'TABLE', counterFormat: 'upper-roman', shortLabel: 'Table',
        captionStyle: { position: 'above' } }) })),
  calloutStyles: [{ id: 'front', ...box }],
  headingStyles: [letter, { id: 'back', numbered: false, ...sans(8, 700),
    textTransform: 'uppercase', color: col('ink') }],
  page: { sizePreset: 'custom', width: mm(TRIM_W), height: mm(TRIM_H), dpi: 150,
    margins: { top: mm(TOP), bottom: mm(BOTTOM), left: mm(SIDE), right: mm(SIDE),
      mirror: true } },
  layout: { layoutType: 'double', gutterWidth: mm(GUTTER) },
  bodyText: { fontFamily: SERIF, fontSize: pt(BODY), lineHeight: pt(LEAD), color: col('ink'),
    boldColor: col('ink'), italicColor: col('ink'), referenceColor: col('ink'),
    referenceBold: false, textAlign: 'justify', firstLineIndent: mm(3.5),
    indentAfterHeading: false, hyphenation: { enabled: true }, optimalLineBreaking: true,
    avoidWidows: true, avoidOrphans: true, avoidRunts: true },
  math: { marginTop: pt(LEAD / 2), marginBottom: pt(LEAD / 2) },
  // Notes lettered a, b: the numbers in brackets belong to the references.
  footnotes: { numberFormat: 'lower-alpha', fontSize: pt(7.8), lineHeight: pt(10),
    separator: { color: col('rule') } },
  headings: { fontFamily: SANS, color: col('accent'), fontWeight: 700, levels: [
    { level: 1, breakBefore: { enabled: true, parity: 'any' } }, // any page: no forced recto
    { level: 2, numberingTemplate: '{2:I}.', ...sans(11.5, 700), lineHeight: pt(LEAD * 1.5),
      marginTop: pt(LEAD / 2), marginBottom: pt(0) },
    { level: 3, numberingTemplate: '{3:A}.', ...sans(10, 600), color: col('ink'),
      lineHeight: pt(LEAD), marginTop: pt(LEAD / 2), marginBottom: pt(0) },
  ] },
  tableStyle: { rules: 'horizontal', borderColor: col('rule'), borderWidth: pt(0.5),
    headerBackgroundEnabled: false, bodyFontFamily: SANS, bodyFontSize: pt(8.2),
    bodyColor: col('ink'), cellPadding: mm(0.9) },
  captionStyle: { fontFamily: SANS, fontSize: pt(7.6), lineHeight: pt(10), color: col('ink'),
    labelBold: true, labelColor: col('accent'), gap: mm(2),
    note: { fontSize: pt(6.8), color: col('muted') } },
});

// ─── 2 · Content ────────────────────────────────────────────────────────────
const markdown = String.raw`---
title: "Observation of Gravitational Waves from a Binary Black Hole Merger"
author: "B. P. Abbott et al. (LIGO Scientific Collaboration and Virgo Collaboration)"
---

# Observation of Gravitational Waves \\ from a Binary Black Hole Merger {style="letter" kicker="Letter · Gravitational-wave astronomy"}

::resource{id="fig1"}

:::callout{type="front"}
:::paragraphs{style="byline"}
B. P. Abbott *et al.*[^authors]

(LIGO Scientific Collaboration and Virgo Collaboration)
:::

:::paragraphs{style="dates"}
(Received 21 January 2016; published 11 February 2016) · DOI: 10.1103/PhysRevLett.116.061102
:::

On September 14, 2015 at 09:50:45 UTC the two detectors of the Laser Interferometer Gravitational-Wave Observatory simultaneously observed a transient gravitational-wave signal. The signal sweeps upwards in frequency from 35 to 250 Hz with a peak gravitational-wave strain of $1.0\times10^{-21}$. It matches the waveform predicted by general relativity for the inspiral and merger of a pair of black holes and the ringdown of the resulting single black hole. The signal was observed with a matched-filter signal-to-noise ratio of 24 and a false alarm rate estimated to be less than 1 event per 203 000 years, equivalent to a significance greater than $5.1\sigma$. The source lies at a luminosity distance of $410^{+160}_{-180}$ Mpc corresponding to a redshift $z = 0.09^{+0.03}_{-0.04}$. In the source frame, the initial black hole masses are $36^{+5}_{-4}\,M_\odot$ and $29^{+4}_{-4}\,M_\odot$, and the final black hole mass is $62^{+4}_{-4}\,M_\odot$, with $3.0^{+0.5}_{-0.5}\,M_\odot c^2$ radiated in gravitational waves. All uncertainties define 90% credible intervals. These observations demonstrate the existence of binary stellar-mass black hole systems. This is the first direct detection of gravitational waves and the first observation of a binary black hole merger.
:::

[^authors]: Full author list given at the end of the published article; this abridged re-setting does not reproduce it.

## Introduction

In 1916, the year after the final formulation of the field equations of general relativity, Albert Einstein predicted the existence of gravitational waves. He found that the linearized weak-field equations had wave solutions: transverse waves of spatial strain that travel at the speed of light, generated by time variations of the mass quadrupole moment of the source [@einstein1916; @einstein1918]. Einstein understood that gravitational-wave amplitudes would be remarkably small; moreover, until the Chapel Hill conference in 1957 there was significant debate about the physical reality of gravitational waves [@saulson2011].

The discovery of the binary pulsar system PSR B1913+16 by Hulse and Taylor [@hulse1975] and subsequent observations of its energy loss by Taylor and Weisberg [@taylor1982] demonstrated the existence of gravitational waves. This discovery, along with emerging astrophysical understanding [@press1972], led to the recognition that direct observations of the amplitude and phase of gravitational waves would enable studies of additional relativistic systems and provide new tests of general relativity, especially in the dynamic strong-field regime.

A century after the fundamental predictions of Einstein and Schwarzschild, we report the first direct detection of gravitational waves and the first direct observation of a binary black hole system merging to form a single black hole. Our observations provide unique access to the properties of space-time in the strong-field, high-velocity regime and confirm predictions of general relativity for the nonlinear dynamics of highly disturbed black holes.[^abridged]

[^abridged]: Abridged here: Sections IV (Detector validation) and VII (Outlook), parts of Sections I, III, V and VI, some figure panels and most acknowledgments are left out; sections and references are renumbered.

## Observation

On September 14, 2015 at 09:50:45 UTC, the LIGO Hanford, WA, and Livingston, LA, observatories detected the coincident signal GW150914 shown in :ref{id="fig1"}. The initial detection was made by low-latency searches for generic gravitational-wave transients [@ligo-p1500229] and was reported within three minutes of data acquisition [@klimenko2016]. Subsequently, matched-filter analyses that use relativistic models of compact binary waveforms [@ligo-p1500269] recovered GW150914 as the most significant event from each detector for the observations reported here. Occurring within the 10-ms intersite propagation time, the events have a combined signal-to-noise ratio (SNR) of 24 [@usman2015].

Only the LIGO detectors were observing at the time of GW150914. The Virgo detector was being upgraded, and GEO 600, though not sufficiently sensitive to detect this event, was operating but not in observational mode. With only two detectors the source position is primarily determined by the relative arrival time and localized to an area of approximately 600 deg^2^ (90% credible region) [@ligo-p1500218; @ligo-p1500227].

The basic features of GW150914 point to it being produced by the coalescence of two black holes—i.e., their orbital inspiral and merger, and subsequent final black hole ringdown. Over 0.2 s, the signal increases in frequency and amplitude in about 8 cycles from 35 to 150 Hz, where the amplitude reaches a maximum. The most plausible explanation for this evolution is the inspiral of two orbiting masses, $m_1$ and $m_2$, due to gravitational-wave emission. At the lower frequencies, such evolution is characterized by the chirp mass [@blanchet1995]
$$\mathcal{M} = \frac{(m_1 m_2)^{3/5}}{(m_1+m_2)^{1/5}} = \frac{c^3}{G}\left[\frac{5}{96}\,\pi^{-8/3} f^{-11/3}\,\dot f\,\right]^{3/5} , \tag{1}$$
where $f$ and $\dot f$ are the observed frequency and its time derivative and $G$ and $c$ are the gravitational constant and speed of light. Estimating $f$ and $\dot f$ from the data in :ref{id="fig1"}, we obtain a chirp mass of $\mathcal{M} \simeq 30\,M_\odot$, implying that the total mass $M = m_1 + m_2$ is $\gtrsim 70\,M_\odot$ in the detector frame. This bounds the sum of the Schwarzschild radii of the binary components to $2GM/c^2 \gtrsim 210$ km. To reach an orbital frequency of 75 Hz (half the gravitational-wave frequency) the objects must have been very close and very compact; equal Newtonian point masses orbiting at this frequency would be only $\simeq 350$ km apart. A pair of neutron stars, while compact, would not have the required mass, while a black hole neutron star binary with the deduced chirp mass would have a very large total mass, and would thus merge at much lower frequency. This leaves black holes as the only known objects compact enough to reach an orbital frequency of 75 Hz without contact. Furthermore, the decay of the waveform after it peaks is consistent with the damped oscillations of a black hole relaxing to a final stationary Kerr configuration. Below, we present a general-relativistic analysis of GW150914; :ref{id="fig2"} shows the calculated waveform using the resulting source parameters.

## Detectors

Gravitational-wave astronomy exploits multiple, widely separated detectors to distinguish gravitational waves from local instrumental and environmental noise, to provide source sky localization, and to measure wave polarizations. The LIGO sites each operate a single Advanced LIGO detector [@aasi2015], a modified Michelson interferometer (see :ref{id="fig3"}) that measures gravitational-wave strain as a difference in length of its orthogonal arms. Each arm is formed by two mirrors, acting as test masses, separated by $L_x = L_y = L = 4$ km. A passing gravitational wave effectively alters the arm lengths such that the measured difference is $\Delta L(t) = \delta L_x - \delta L_y = h(t)L$, where $h$ is the gravitational-wave strain amplitude projected onto the detector. This differential length variation alters the phase difference between the two light fields returning to the beam splitter, transmitting an optical signal proportional to the gravitational-wave strain to the output photodetector.

To achieve sufficient sensitivity to measure gravitational waves, the detectors include several enhancements to the basic Michelson interferometer. First, each arm contains a resonant optical cavity, formed by its two test mass mirrors, that multiplies the effect of a gravitational wave on the light phase by a factor of 300 [@drever1991]. Second, a partially transmissive power-recycling mirror at the input provides additional resonant buildup of the laser light in the interferometer as a whole [@drever1983; @schilling]: 20 W of laser input is increased to 700 W incident on the beam splitter, which is further increased to 100 kW circulating in each arm cavity. Third, a partially transmissive signal-recycling mirror at the output optimizes the gravitational-wave signal extraction by broadening the bandwidth of the arm cavities [@meers1988; @mizuno1993].
`; // title, abstract, sections I–III
const results = String.raw`## Searches

We present the analysis of 16 days of coincident observations between the two LIGO detectors from September 12 to October 20, 2015. This is a subset of the data from Advanced LIGO’s first observational period that ended on January 12, 2016.

GW150914 is confidently detected by two different types of searches. One aims to recover signals from the coalescence of compact objects, using optimal matched filtering with waveforms predicted by general relativity. The other search targets a broad range of generic transient signals, with minimal assumptions about waveforms. These searches use independent methods, and their response to detector noise consists of different, uncorrelated, events. However, strong signals from binary black hole mergers are expected to be detected by both searches.

### Generic transient search

Designed to operate without a specific waveform model, this search identifies coincident excess power in time-frequency representations of the detector strain data [@klimenko2016; @klimenko2008], for signal frequencies up to 1 kHz and durations up to a few seconds.

The search reconstructs signal waveforms consistent with a common gravitational-wave signal in both detectors using a multidetector maximum likelihood method. Each event is ranked according to the detection statistic
$$\eta_c = \sqrt{2E_c/(1+E_n/E_c)} ,$$
where $E_c$ is the dimensionless coherent signal energy obtained by cross-correlating the two reconstructed waveforms, and $E_n$ is the dimensionless residual noise energy after the reconstructed signal is subtracted from the data. The statistic $\eta_c$ thus quantifies the SNR of the event and the consistency of the data between the two detectors.

Based on their time-frequency morphology, the events are divided into three mutually exclusive search classes, as described in [@ligo-p1500229]: events with time-frequency morphology of known populations of noise transients (class C1), events with frequency that increases with time (class C3), and all remaining events (class C2).

Detected with $\eta_c = 20.0$, GW150914 is the strongest event of the entire search. Consistent with its coalescence signal signature, it is found in the search class C3 of events with increasing time-frequency evolution. Measured on a background equivalent to over 67 400 years of data and including a trials factor of 3 to account for the search classes, its false alarm rate is lower than 1 in 22 500 years. This corresponds to a probability $< 2\times10^{-6}$ of observing one or more noise events as strong as GW150914 during the analysis time, equivalent to $4.6\sigma$. The left panel of :ref{id="fig4"} shows the C3 class results and background.

### Binary coalescence search

This search targets gravitational-wave emission from binary systems with individual masses from 1 to $99\,M_\odot$, total mass less than $100\,M_\odot$, and dimensionless spins up to 0.99 [@ligo-p1500269]. To model systems with total mass larger than $4\,M_\odot$, we use the effective-one-body formalism [@buonanno2000], which combines results from the post-Newtonian approach [@blanchet1995; @blanchet2004] with results from black hole perturbation theory and numerical relativity. The waveform model [@taracchini2014; @purrer2014] assumes that the spins of the merging objects are aligned with the orbital angular momentum, but the resulting templates can, nonetheless, effectively recover systems with misaligned spins in the parameter region of GW150914 [@ligo-p1500269]. Approximately 250 000 template waveforms are used to cover this parameter space.

The search calculates the matched-filter signal-to-noise ratio $\rho(t)$ for each template in each detector and identifies maxima of $\rho(t)$ with respect to the time of arrival of the signal [@allen2012; @sathyaprakash1991; @owen1999]. For each maximum we calculate a chi-squared statistic $\chi^2_r$ to test whether the data in several different frequency bands are consistent with the matching template [@allen2005]. Values of $\chi^2_r$ near unity indicate that the signal is consistent with a coalescence. If $\chi^2_r$ is greater than unity, $\rho(t)$ is reweighted as $\hat\rho = \rho/\{[1+(\chi^2_r)^3]/2\}^{1/6}$ [@abadie2012; @babak2013]. The final step enforces coincidence between detectors by selecting event pairs that occur within a 15-ms window and come from the same template. The 15-ms window is determined by the 10-ms intersite propagation time plus 5 ms for uncertainty in arrival time of weak signals. We rank coincident events based on the quadrature sum $\hat\rho_c$ of the $\hat\rho$ from both detectors [@usman2015].

To produce background data for this search the SNR maxima of one detector are time shifted and a new set of coincident events is computed. Repeating this procedure $\sim 10^7$ times produces a noise background analysis time equivalent to 608 000 years.

To account for the search background noise varying across the target signal space, candidate and background events are divided into three search classes based on template length. The right panel of :ref{id="fig4"} shows the background for the search class of GW150914. The GW150914 detection-statistic value of $\hat\rho_c = 23.6$ is larger than any background event, so only an upper bound can be placed on its false alarm rate. Across the three search classes this bound is 1 in 203 000 years. This translates to a false alarm probability $< 2\times10^{-7}$, corresponding to $5.1\sigma$.

When an event is confidently identified as a real gravitational-wave signal, as for GW150914, the background used to determine the significance of other events is reestimated without the contribution of this event. This is the background distribution shown as a purple line in the right panel of :ref{id="fig4"}. Based on this, the second most significant event has a false alarm rate of 1 per 2.3 years and corresponding Poissonian false alarm probability of 0.02. Waveform analysis of this event indicates that if it is astrophysical in origin it is also a binary black hole merger [@ligo-p1500269].

## Source discussion

The matched-filter search is optimized for detecting signals, but it provides only approximate estimates of the source parameters. To refine them we use general relativity-based models [@taracchini2014; @purrer2014; @hannam2014; @khan2016], some of which include spin precession, and for each model perform a coherent Bayesian analysis to derive posterior distributions of the source parameters [@veitch2015]. The initial and final masses, final spin, distance, and redshift of the source are shown in :ref{id="tab1"}. The spin of the primary black hole is constrained to be $< 0.7$ (90% credible interval) indicating it is not maximally spinning, while the spin of the secondary is only weakly constrained. These source parameters are discussed in detail in [@ligo-p1500218]. The parameter uncertainties include statistical errors and systematic errors from averaging the results of different waveform models.

Using the fits to numerical simulations of binary black hole mergers in [@healy2014; @husa2016], we provide estimates of the mass and spin of the final black hole, the total energy radiated in gravitational waves, and the peak gravitational-wave luminosity [@ligo-p1500218]. The estimated total energy radiated in gravitational waves is $3.0^{+0.5}_{-0.5}\,M_\odot c^2$. The system reached a peak gravitational-wave luminosity of $3.6^{+0.5}_{-0.4}\times10^{56}$ erg/s, equivalent to $200^{+30}_{-20}\,M_\odot c^2/\mathrm{s}$.

GW150914 demonstrates the existence of stellar-mass black holes more massive than $\simeq 25\,M_\odot$, and establishes that binary black holes can form in nature and merge within a Hubble time. Binary black holes have been predicted to form both in isolated binaries [@tutukov1973; @lipunov1997; @belczynski2016] and in dense environments by dynamical interactions [@sigurdsson1993; @portegieszwart2000; @rodriguez2015]. The formation of such massive black holes from stellar evolution requires weak massive-star winds, which are possible in stellar environments with metallicity lower than $\simeq 1/2$ the solar value [@belczynski2010; @spera2015].

## Conclusion

The LIGO detectors have observed gravitational waves from the merger of two stellar-mass black holes. The detected waveform matches the predictions of general relativity for the inspiral and merger of a pair of black holes and the ringdown of the resulting single black hole. These observations demonstrate the existence of binary stellar-mass black hole systems. This is the first direct detection of gravitational waves and the first observation of a binary black hole merger.

## Acknowledgments {style="back"}

The authors gratefully acknowledge the support of the United States National Science Foundation (NSF) for the construction and operation of the LIGO Laboratory and Advanced LIGO as well as the Science and Technology Facilities Council (STFC) of the United Kingdom, the Max-Planck Society (MPS), and the State of Niedersachsen, Germany, for support of the construction of Advanced LIGO and construction and operation of the GEO600 detector.

:::space{lines=1}

:::bibliography{title=""}

:::paragraphs{style="colophon"}
Abridged from B. P. Abbott et al. (LIGO Scientific Collaboration and Virgo Collaboration), Phys. Rev. Lett. 116, 061102 (2016), doi:10.1103/PhysRevLett.116.061102, CC BY 3.0: sections and references renumbered, figures redrawn, captions shortened. Figure data from the Gravitational Wave Open Science Center (gwosc.org), a service of the LIGO Scientific Collaboration, the Virgo Collaboration and KAGRA (CC BY 4.0). Set in Gelasio and Albert Sans (SIL OFL); formulas by MathJax.
:::
`; // sections IV–VI, acknowledgments, colophon
const refs = `:::references{format=bibtex}
@string{spaw={Sitzungsber. K. Preuss. Akad. Wiss.}}
@string{cqg={Classical Quantum Gravity}}
@string{prd={Phys. Rev. D}}
@string{prl={Phys. Rev. Lett.}}
@string{apj={Astrophys. J.}}
@article{einstein1916,author={Einstein, A.},journal=spaw,volume=1,pages={688},year=1916}
@article{einstein1918,author={Einstein, A.},journal=spaw,volume=1,pages={154},year=1918}
@article{saulson2011,author={Saulson, P. R.},journal={Gen. Relativ. Gravit.},volume=43,pages={3289},year=2011}
@article{blanchet1995,author={Blanchet, L. and Damour, T. and Iyer, B. R. and Will, C. M. and Wiseman, A. G.},journal=prl,volume=74,pages={3515},year=1995}
@article{hulse1975,author={Hulse, R. A. and Taylor, J. H.},journal=apj,volume=195,pages={L51},year=1975}
@article{taylor1982,author={Taylor, J. H. and Weisberg, J. M.},journal=apj,volume=253,pages={908},year=1982}
@article{press1972,author={Press, W. and Thorne, K.},journal={Annu. Rev. Astron. Astrophys.},volume=10,pages={335},year=1972}
@article{aasi2015,author={{J. Aasi et al.}},journal=cqg,volume=32,pages={074001},year=2015}
@misc{ligo-p1500218,author={{B. Abbott et al.}},url={https://dcc.ligo.org/LIGO-P1500218/public/main}}
@misc{ligo-p1500229,author={{B. Abbott et al.}},url={https://dcc.ligo.org/LIGO-P1500229/public/main}}
@misc{klimenko2016,author={{S. Klimenko et al.}},note={arXiv:1511.05999 [Phys. Rev. D (to be published)]}}
@misc{ligo-p1500269,author={{B. Abbott et al.}},url={https://dcc.ligo.org/LIGO-P1500269/public/main}}
@misc{usman2015,author={{S. A. Usman et al.}},note={arXiv:1508.02357}}
@misc{ligo-p1500227,author={{B. Abbott et al.}},url={https://dcc.ligo.org/LIGO-P1500227/public/main}}
@book{drever1991,author={Drever, R. W. P.},title={The Detection of Gravitational Waves},editor={Blair, D. G.},publisher={Cambridge University Press},address={Cambridge, England},year={1991}}
@incollection{drever1983,author={{R. W. P. Drever et al.}},booktitle={Quantum Optics, Experimental Gravity, and Measurement Theory},editor={Meystre, P. and Scully, M. O.},series={NATO ASI, Ser. B},volume={94},publisher={Plenum Press},address={New York},year={1983},pages={503--514}}
@unpublished{schilling,author={Schilling, R.},note={(unpublished)}}
@article{meers1988,author={Meers, B. J.},journal=prd,volume=38,pages={2317},year=1988}
@article{mizuno1993,author={Mizuno, J. and Strain, K. A. and Nelson, P. G. and Chen, J. M. and Schilling, R. and R{\\"u}diger, A. and Winkler, W. and Danzmann, K.},journal={Phys. Lett. A},volume=175,pages={273},year=1993}
@article{klimenko2008,author={Klimenko, S. and Yakushin, I. and Mercer, A. and Mitselmakher, G.},journal=cqg,volume=25,pages={114029},year=2008}
@article{buonanno2000,author={Buonanno, A. and Damour, T.},journal=prd,volume=62,pages={064015},year=2000}
@article{blanchet2004,author={Blanchet, L. and Damour, T. and Esposito-Far{\\\`e}se, G. and Iyer, B. R.},journal=prl,volume=93,pages={091101},year=2004}
@article{taracchini2014,author={{A. Taracchini et al.}},journal=prd,volume=89,pages={061502},year=2014}
@article{purrer2014,author={P{\\"u}rrer, M.},journal=cqg,volume=31,pages={195010},year=2014}
@article{allen2012,author={Allen, B. and Anderson, W. G. and Brady, P. R. and Brown, D. A. and Creighton, J. D. E.},journal=prd,volume=85,pages={122006},year=2012}
@article{sathyaprakash1991,author={Sathyaprakash, B. S. and Dhurandhar, S. V.},journal=prd,volume=44,pages={3819},year=1991}
@article{owen1999,author={Owen, B. J. and Sathyaprakash, B. S.},journal=prd,volume=60,pages={022002},year=1999}
@article{allen2005,author={Allen, B.},journal=prd,volume=71,pages={062001},year=2005}
@article{abadie2012,author={{J. Abadie et al.}},journal=prd,volume=85,pages={082002},year=2012}
@article{babak2013,author={{S. Babak et al.}},journal=prd,volume=87,pages={024033},year=2013}
@article{hannam2014,author={Hannam, M. and Schmidt, P. and Boh{\\'e}, A. and Haegel, L. and Husa, S. and Ohme, F. and Pratten, G. and P{\\"u}rrer, M.},journal=prl,volume=113,pages={151101},year=2014}
@article{khan2016,author={Khan, S. and Husa, S. and Hannam, M. and Ohme, F. and P{\\"u}rrer, M. and Jim{\\'e}nez Forteza, X. and Boh{\\'e}, A.},journal=prd,volume=93,pages={044007},year=2016}
@article{veitch2015,author={{J. Veitch et al.}},journal=prd,volume=91,pages={042003},year=2015}
@article{healy2014,author={Healy, J. and Lousto, C. O. and Zlochower, Y.},journal=prd,volume=90,pages={104004},year=2014}
@article{husa2016,author={Husa, S. and Khan, S. and Hannam, M. and P{\\"u}rrer, M. and Ohme, F. and Jim{\\'e}nez Forteza, X. and Boh{\\'e}, A.},journal=prd,volume=93,pages={044006},year=2016}
@article{tutukov1973,author={Tutukov, A. and Yungelson, L.},journal={Nauchnye Informatsii},volume=27,pages={70},year=1973}
@article{lipunov1997,author={Lipunov, V. M. and Postnov, K. A. and Prokhorov, M. E.},journal={Mon. Not. R. Astron. Soc.},volume=288,pages={245},year=1997}
@misc{belczynski2016,author={Belczynski, K. and Repetto, S. and Holz, D. and O'Shaughnessy, R. and Bulik, T. and Berti, E. and Fryer, C. and Dominik, M.},note={arXiv:1510.04615 [Astrophys. J. (to be published)]}}
@article{sigurdsson1993,author={Sigurdsson, S. and Hernquist, L.},journal={Nature (London)},volume=364,pages={423},year=1993}
@article{portegieszwart2000,author={Portegies Zwart, S. F. and McMillan, S. L. W.},journal={Astrophys. J. Lett.},volume=528,pages={L17},year=2000}
@article{rodriguez2015,author={Rodriguez, C. L. and Morscher, M. and Pattabiraman, B. and Chatterjee, S. and Haster, C.-J. and Rasio, F. A.},journal=prl,volume=115,pages={051101},year=2015}
@article{belczynski2010,author={Belczynski, K. and Bulik, T. and Fryer, C. L. and Ruiter, A. and Valsecchi, F. and Vink, J. S. and Hurley, J. R.},journal=apj,volume=714,pages={1217},year=2010}
@article{spera2015,author={Spera, M. and Mapelli, M. and Bressan, A.},journal={Mon. Not. R. Astron. Soc.},volume=451,pages={4086},year=2015}
:::
`; // BibTeX of the works the kept text cites
const captions = String.raw`fig1
The gravitational-wave event GW150914 observed by the LIGO Hanford (H1, left column panels) and Livingston (L1, right column panels) detectors. Times are shown relative to September 14, 2015 at 09:50:45 UTC. For visualization, all time series are filtered with a 35–350 Hz bandpass filter to suppress large fluctuations outside the detectors’ most sensitive frequency band, and band-reject filters to remove the strong instrumental spectral lines. Top row, left: H1 strain. Top row, right: L1 strain. GW150914 arrived first at L1 and $6.9^{+0.5}_{-0.4}$ ms later at H1; for a visual comparison, the H1 data are also shown, shifted in time by this amount and inverted (to account for the detectors’ relative orientations). Second row: Gravitational-wave strain projected onto each detector in the 35–350 Hz band. Solid lines show a numerical relativity waveform for a system with parameters consistent with those recovered from GW150914 confirmed to 99.9% by an independent calculation. Third row: Residuals after subtracting the filtered numerical relativity waveform from the filtered detector time series.

fig1.note
Redrawn from GWOSC open data (LIGO and Virgo Collaborations, CC BY 4.0); reconstructions and time–frequency row omitted.

fig2
Top: Estimated gravitational-wave strain amplitude from GW150914 projected onto H1. This shows the full bandwidth of the waveforms, without the filtering used for :ref{id="fig1"}. Bottom: The Keplerian effective black hole separation in units of Schwarzschild radii ($R_S = 2GM/c^2$) and the effective relative velocity given by the post-Newtonian parameter $v/c = (GM\pi f/c^3)^{1/3}$, where $f$ is the gravitational-wave frequency calculated with numerical relativity and $M$ is the total mass (value from :ref{id="tab1"}).

fig2.note
Redrawn from GWOSC data (CC BY 4.0); insets omitted.

fig3
Simplified diagram of an Advanced LIGO detector (not to scale). A gravitational wave propagating orthogonally to the detector plane and linearly polarized parallel to the 4-km optical cavities will have the effect of lengthening one 4-km arm and shortening the other during one half-cycle of the wave; these length changes are reversed during the other half-cycle.

fig3.note
Redrawn after the published diagram; its insets (a) and (b) are omitted.

fig4
Search results from the generic transient search (left) and the binary coalescence search (right). These histograms show the number of candidate events (orange markers) and the mean number of background events (black lines) in the search class where GW150914 was found as a function of the search detection statistic and with a bin width of 0.2. The significance of GW150914 is greater than $5.1\sigma$ and $4.6\sigma$ for the binary coalescence and the generic transient searches, respectively. Right: The tail in the black-line background of the binary coalescence search is due to random coincidences of GW150914 in one detector with noise in the other detector. (This type of event is practically absent in the generic transient search background because they do not pass the time-frequency consistency requirements used in that search.) The purple curve is the background excluding those coincidences, which is used to assess the significance of the second strongest event.

fig4.note
Redrawn from GWOSC data (CC BY 4.0); the C2 + C3 search and the significance scales omitted.

tab1
Source parameters for GW150914. We report median values with 90% credible intervals that include statistical errors, and systematic errors from averaging the results of different waveform models. Masses are given in the source frame; to convert to the detector frame multiply by $(1+z)$. The source redshift assumes standard cosmology.

tab1.note
Values as published. The citations in the caption are omitted.
`; // "fig1" on a line, then its caption
const source = [markdown, results, refs].join('\n\n');
const caption = Object.fromEntries(captions.trim().split(/\n\s*\n/)
  .map((part) => [part.slice(0, part.indexOf('\n')), part.slice(part.indexOf('\n') + 1)]));

// #region table: Table I, its asymmetric errors written as maths in the cells
// A cell sets $…$ in the line, at the table's size: the errors stack over and under the value.
const VALUES = [['Primary black hole mass', '$36^{+5}_{-4}\\,M_\\odot$'],
  ['Secondary black hole mass', '$29^{+4}_{-4}\\,M_\\odot$'],
  ['Final black hole mass', '$62^{+4}_{-4}\\,M_\\odot$'],
  ['Final black hole spin', '$0.67^{+0.05}_{-0.07}$'],
  ['Luminosity distance', '$410^{+160}_{-180}$ Mpc'],
  ['Source redshift $z$', '$0.09^{+0.03}_{-0.04}$']];
const table = { model: { columnWidths: [1.3, 1], rows: VALUES.map(([label, value]) =>
  [{ content: label, verticalAlign: 'middle' }, { content: value }]) } };
// #endregion

// #region art: the four figures, drawn from GWOSC's open data in the page's palette
// GWOSC's Fig. 1, 2 and 4 files (strain in 10^-21, log10 of counts) on a regular grid, each
// sample written as its change from the one before: a base-64 digit, 32 for none.
const SERIES = { // [first value, step, one character per sample]
  hObs: [0.025, 0.03,
    'eghgfegiigddfkmkeaaeijhgfhjhgdcdimplfXWZionhbadkmkfdglnjcZZgjjebdhlkdZZfjmiijmlhcbfjkgdaZa'
    + 'dgkkigefhijiihifedefhhhggfhihhfefghhiijifeddddgkjhddgjifbdfjkhdcdhiihgefghjjhedeghihhjkjhf'
    + 'fgihgfegfeeeggeedghhcabgijjiijjihgeeghggffhihhimnjecdgigecddfecacdfgghiggfgikkjhijkljihihg'
    + 'ffghgeccbehhfbWWbinmhbcglmjgeehloomgefkpohXUVaeecaZbfiiijjmmjijnppkfaZaYXWXZfjnqrtsojaSNOV'
    + 'hszynaQRalqpibbchiijjjgdbdhkkgdcdfijifecdehjkmjfdegheccfjmkigfffeghih'],
  lObs: [-0.119, 0.03,
    'deghhhfgklifdegjihddgjjgdcdhjjgdcehlmjgdeikkhbaafhggfghhhgefgiigedeghiijiiijhedceiiigeeffg'
    + 'hiihggeddgihgeddfhihikjgdbbfkomhdbdfgjlljhecdghgeeghiigeddffeeehiifdcfhfffhjmmkifffddehjkg'
    + 'dcfijjhhihgghiigdceggdcbcfigfdeijjiffhihedgmpoicXYeknnjhgikieZYadfhhhfedefffdfgjjihhfhhjln'
    + 'mjfddghggfeeegffaYYchjihgihiiiiilmnlifdefdcbbbccbbdhjknnnmllljfZVSWbfgjnuxtjVMOZmuuofZXafg'
    + 'ggggefefhkmlhebcfhigddfjmnjfddfiigfefihgffhigfedefghhgffgghhfefghgghi'],
  hNR: [0.0, 0.03,
    'ggghgggfgfgggggghggfgfggfggggggggghghhghggggggghghghggfgffggfgggfggggghghgggfggggghghgggff'
    + 'gfgfgfggfggggghghghgggggghhhhhgggggggfgggggfggggggggfgffffgggggggfgfgfggggghghghhhihihhhgg'
    + 'gghghggggfffeefefeffefffggghhghhghhhhiiiiihighggggfgffffefefffeffeefefgggihhiihihiiiiiiiih'
    + 'hghggfgeeddccdcddeeffffghhijjkklklkkkjjihffdcaaaaabbddffhiklmnonnljhebZXVVWZcgmrvxwriYPLPX'
    + 'lvyvkaSSYgopmhcacfikkifedffhiihhfgfggfgffgfgfgggfhgghhhggfggggghhhhhg'],
  lNR: [0.031, 0.03,
    'gfgghgghggggggggggfgfgfgfggghggfggfgggghghghggghgggggggfgfggghggggggfgggghghghgghgggggggfg'
    + 'fgfggggggfgffgfgggghggghgggghggggggghghghhghgfggggghggghfggfgfgffffffffgfgghggggfhgghihhih'
    + 'highhhghgggffffgfffgffefeeffgfghghghhghihhihhhhghhhhhhgggeffeefeffeffeefffgggghhhhiiijjjjj'
    + 'ihhhhgfgffeeddccdcddeeefggiikkllllkkjhhgfedcbbaabcdfhjlnnponkheaWUTUZgnuxuncURTclssngaYafj'
    + 'lkhedcfgiiigffefgghhgghghghgggghgggfgffggggghgfgffgfggghhgghgggggghgf'],
  hFull: [0.436, 0.03,
    'fgffffeffefefefeffefffffgfgfggghghhghhihhihihihiihhihhhhhhghggggfgfffffeffeefeefeefeffffff'
    + 'ggfhgghghhihhihiiihiiihihihhhhghgggfgffffefeeeeeefeeeeffefgfgfghghhhhihiiiiiiiiiiiihhhhhgg'
    + 'ggfffefeeeeedeedeeefeffgfgghhhhihijijijijiiiiihhghgffffeeededdddedeeeeffgghhhiijijkjjjjjji'
    + 'iihggffeeedcdcccdcdeeefghhijjklklklkjjiigfeddbbaababcdegijkmnooonlkhebZWVUVYbgmrwxwqiXOKNX'
    + 'lw1wlZRSZhppnhcabeijkhgeeeghihgfgfggghgggfggghgggfgggghggfgggggghfggg'],
  sep: [4.705, 0.01,
    'ffeffeffefffeffefefefefefefefeefeeefeeeeeeeeeedeeedededdedddddcddccccccbbbaaaZZYXWVUSQNL'],
  vel: [0.326, 0.001,
    'ghghghghgghghghghhghghghghhghhghhghhhghhhhhhhhhhhhhhhhihhihihiiihjiiijjjjjkkklmmnppsuy28'],
  pyBg: [0.453, 0.1,
    'lihdcccbcbccccdeeefeeeheggdihhdiiggfWkcijOweVU3hRXggggg-icStiPU-iVUXggggggggggggg'],
  pyBgEx: [0.453, 0.1,
    'lihdcccbcbccccddedfdffcZfdZXpgXgggggggggggggggggggggggggggggggggggggggggggggggggg'],
  cwbBg3: [1.247, 0.1,
    'ededdeddddeddddddddddeecgZOyOygOgggggggggggggggggggggggggggggggggggggggggggggggggggggg'],
};
const B64 = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789-_';
const decode = (key) => { const [first, step, s] = SERIES[key]; const out = [first];
  for (const c of s) out.push(out.at(-1) + step * (B64.indexOf(c) - 32));
  return out; };
const PX_PER_MM = 10; // drawn in mm, declared at 10 px to the mm
const R = (x) => Math.round(x * 100) / 100;
const svg = (w, h, body) => `<svg xmlns="http://www.w3.org/2000/svg" width="${R(w * PX_PER_MM)}" `
  + `height="${R(h * PX_PER_MM)}" viewBox="0 0 ${R(w)} ${R(h)}">${body}</svg>`;
const line = (x1, y1, x2, y2, color, width, extra = '') => `<path d="M${R(x1)} ${R(y1)}L${R(x2)} `
  + `${R(y2)}" stroke="${color}" stroke-width="${width}" fill="none" ${extra}/>`;
const poly = (pts, color, width, extra = '') => `<path d="${pts.map(([x, y], i) =>
  `${i ? 'L' : 'M'}${R(x)} ${R(y)}`).join('')}" fill="none" `
  + `stroke="${color}" stroke-width="${width}" stroke-linejoin="round" ${extra}/>`;
// Labels are MathJax paths, \textsf for words: the axes carry formulas, set like the
// maths of the text, and paths stay vector in the PDF.
function tex(markup, x, y, size, anchor = 0, color = palette.ink, rotate = false) {
  const r = renderMath(markup, false, 100);
  const k = size / 1000;
  const dx = -anchor * r.viewBox.width * k;
  return `<g transform="translate(${R(x)} ${R(y)})${rotate ? ' rotate(-90)' : ''} `
    + `translate(${R(dx)} 0) scale(${k})" fill="${color}">`
    + `${r.paths.map((p) => `<path d="${p.d}"/>`).join('')}</g>`;
}
const sf = (s) => `\\textsf{${s}}`;
function panel(x, y, w, h, [t0, t1], [v0, v1], series) {
  const X = (t) => x + ((t - t0) / (t1 - t0)) * w;
  const Y = (v) => y + h - ((Math.max(v0, Math.min(v1, v)) - v0) / (v1 - v0)) * h;
  let out = `<rect x="${R(x)}" y="${R(y)}" width="${R(w)}" height="${R(h)}" fill="none" `
    + `stroke="${palette.rule}" stroke-width="0.2"/>`;
  if (v0 < 0) out += line(x, Y(0), x + w, Y(0), palette.rule, 0.15);
  for (const [values, start, step, color, width] of series) {
    const pts = values.map((v, i) => [start + i * step, v]).filter(([t]) => t >= t0 && t <= t1);
    out += poly(pts.map(([t, v]) => [X(t), Y(v)]), color, width);
  }
  return { out, X, Y };
}
const residual = (d) => { // the published residual is the data less the waveform
  const nr = decode(`${d}NR`);
  return decode(`${d}Obs`).map((v, i) => v - nr[i]);
};
const DT = 8 / 16384; // s: every eighth sample of the 16 384 Hz files

function figure1(W) { // the hero: three rows, Hanford left, Livingston right
  const [LM, GAP, TOPR, RH, RG] = [12, 5, 6, 14, 2];
  const PW = (W - LM - GAP - 1) / 2;
  const H = TOPR + 3 * RH + 2 * RG + 8.4;
  const T = [0.25, 0.46];
  const V = [-1.35, 1.35];
  const hObs = decode('hObs');
  const flipped = hObs.map((v) => -v); // H1 inverted, and moved 6.9 ms earlier below
  const rowsOf = [[[[hObs, 0.25, DT, data.hanford, 0.32]],
    [[decode('lObs'), 0.25, DT, data.livingston, 0.32], [flipped, 0.25 - 0.0069, DT,
      data.hanford, 0.22]]],
  [[[decode('hNR'), 0.25, DT, data.hanford, 0.38]], [[decode('lNR'), 0.25, DT,
    data.livingston, 0.38]]],
  [[[residual('h'), 0.25, DT, palette.muted, 0.28]], [[residual('l'), 0.25, DT,
    palette.muted, 0.28]]]];
  const labels = [['H1 observed', 'L1 observed'], ['Numerical relativity',
    'Numerical relativity'], ['Residual', 'Residual']];
  let out = tex(sf('Hanford, Washington (H1)'), LM, 3.8, 3.1, 0, data.hanford)
    + tex(sf('Livingston, Louisiana (L1)'), LM + PW + GAP, 3.8, 3.1, 0, data.livingston)
    + tex(sf('with H1 shifted and inverted'), W - 1, 3.8, 2.5, 1, data.hanford);
  rowsOf.forEach((row, r) => row.forEach((series, c) => {
    const x = LM + c * (PW + GAP);
    const y = TOPR + r * (RH + RG);
    const p = panel(x, y, PW, RH, T, V, series);
    out += p.out + tex(sf(labels[r][c]), x + 1.6, y + 3.2, 2.5, 0, palette.muted);
    if (c === 0) {
      for (const v of [-1, 0, 1]) out += tex(v.toFixed(1), x - 1.2,
        p.Y(v) + 0.9, 2.4, 1, palette.muted);
    }
    if (r === 2) {
      for (const t of [0.3, 0.35, 0.4, 0.45]) {
        out += line(p.X(t), y + RH, p.X(t), y + RH + 0.9, palette.muted, 0.2)
          + tex(t.toFixed(2), p.X(t), y + RH + 4, 2.4, 0.5, palette.muted);
      }
      out += tex(sf('Time (s)'), x + PW / 2, y + RH + 7.8, 2.6, 0.5, palette.muted);
    }
  }));
  out += tex(`${sf('Strain (')}10^{-21}${sf(')')}`, 3.2, TOPR + (3 * RH + 2 * RG) / 2, 2.7, 0.5,
    palette.muted, true);
  return svg(W, H, out);
}

function figure2(W) { // one column: the full-band strain, then separation and velocity
  const [LM, RM, PH] = [10, 10, 20];
  const PW = W - LM - RM;
  const H = 6 + PH + 4 + PH + 11;
  const T = [0.25, 0.46];
  const top = panel(LM, 6, PW, PH, T, [-1.4, 1.4],
    [[decode('hFull'), 0.25, DT, data.hanford, 0.32]]);
  const y2 = 6 + PH + 4;
  const sep = panel(LM, y2, PW, PH, T, [0, 5],
    [[decode('sep'), 0.25, 32 / 16384, palette.ink, 0.38]]);
  const vel = panel(LM, y2, PW, PH, T, [0.3, 0.6],
    [[decode('vel'), 0.25, 32 / 16384, data.livingston, 0.38]]);
  let out = top.out + sep.out + vel.out
    + tex(`${sf('Strain (')}10^{-21}${sf(')')}`, 3, 6 + PH / 2, 2.5, 0.5, palette.muted, true)
    + tex(`${sf('Separation (')}R_S${sf(')')}`, 3, y2 + PH / 2, 2.5, 0.5, palette.ink, true)
    + tex(`${sf('Velocity (')}c${sf(')')}`, W - 1.2, y2 + PH / 2, 2.5, 0.5, data.livingston,
      true)
    + tex('v/c = (GM\\pi f/c^3)^{1/3}', LM + 2, vel.Y(0.36), 2.6, 0, data.livingston)
    + tex(sf('Separation'), LM + 2, sep.Y(3.9), 2.5, 0, palette.ink);
  const tick = (v, y) => tex(String(v), LM - 1.2, y + 0.9, 2.3, 1, palette.muted);
  for (const v of [-1, 0, 1]) out += tick(v, top.Y(v));
  for (const v of [1, 2, 3, 4]) out += tick(v, sep.Y(v));
  for (const v of [0.3, 0.4, 0.5]) {
    out += tex(v.toFixed(1), LM + PW + 1.2, vel.Y(v) + 0.9, 2.3, 0, data.livingston);
  }
  for (const t of [0.3, 0.35, 0.4, 0.45]) {
    out += tex(t.toFixed(2), top.X(t), y2 + PH + 4, 2.3, 0.5, palette.muted);
  }
  return svg(W, H, out + tex(sf('Time (s)'), LM + PW / 2, H - 1.4, 2.5, 0.5, palette.muted));
}

function figure3(W) { // the interferometer, after the published diagram (not to scale)
  const [H, bx, by, iy, sy] = [57, 28, 40, 27, 46]; // height; beam splitter; mirror heights
  const beam = (x1, y1, x2, y2, w = 0.9) => line(x1, y1, x2, y2, data.hanford, w,
    'stroke-opacity="0.55"');
  const mirror = (x, y, vertical, color = palette.ink) => (vertical
    ? `<rect x="${x - 0.7}" y="${y - 3}" width="1.4" height="6" fill="${color}"/>`
    : `<rect x="${x - 3}" y="${y - 0.7}" width="6" height="1.4" fill="${color}"/>`);
  const label = (s, x, y, anchor = 0) => tex(sf(s), x, y, 2.4, anchor, palette.ink);
  const small = (s, x, y, anchor = 0) => tex(sf(s), x, y, 2.2, anchor, palette.muted);
  const mid = (40 + W - 6) / 2; // the middle of the inline arm
  return svg(W, H, beam(9, by, bx, by) + beam(bx, by, W - 6, by, 1.6) + beam(bx, by, bx, 4, 1.6)
    + beam(bx, by, bx, 51, 0.6)
    + `<rect x="1" y="${by - 3}" width="8" height="6" rx="0.6" fill="${palette.ink}"/>`
    + mirror(17, by, true) + mirror(40, by, true, palette.accent)
    + mirror(W - 6, by, true, palette.accent) + mirror(bx, iy, false, palette.accent)
    + mirror(bx, 4, false, palette.accent) + mirror(bx, sy, false)
    + line(bx - 3, by + 3, bx + 3, by - 3, palette.ink, 0.8)
    + `<rect x="${bx - 2.2}" y="51" width="4.4" height="3.4" fill="${palette.muted}"/>`
    + label('Laser source', 1, by + 6.4) + small('20 W', 5, by - 4.2, 0.5)
    + label('Power recycling', 17, by - 7.4, 0.5) + small('700 W', 22.5, by - 1.4, 0.5)
    + label('Beam splitter', bx + 2.4, by + 4.4) + label('Signal recycling', bx + 4, sy + 1.2)
    + label('Photodetector', bx + 4, 54) + label('Input test mass', 41.6, by - 4.2)
    + label('End test mass', W - 7.6, by - 4.2, 1) + label('Input test mass', bx + 4, iy + 1.2)
    + label('End test mass', bx + 4, 5.2) + small('4 km', mid, by + 4.4, 0.5)
    + small('4 km', bx - 2, (4 + iy) / 2 + 1, 1) + small('100 kW', mid, by - 1.4, 0.5)
    + line(40, by + 2, W - 6, by + 2, palette.muted, 0.15)
    + line(bx - 1.2, 4, bx - 1.2, iy, palette.muted, 0.15));
}

function figure4(W) { // two histograms on a log scale, from GWOSC's Fig. 4 data
  const [LM, GAP, PH] = [12, 9, 44];
  const PW = (W - LM - GAP - 2) / 2;
  const H = 4 + PH + 9.4;
  const X = [7, 24.5];
  const LV = [-7, 2.6]; // 10^-7 to 400 events
  const stairs = (logs, first, p) => { // one step per 0.2-wide bin, broken where it is empty
    const pts = [];
    logs.forEach((v, i) => {
      if (v <= LV[0]) { if (pts.length && pts[pts.length - 1]) pts.push(null); return; }
      const [a, b] = [first + 0.2 * i, first + 0.2 * (i + 1)];
      pts.push([p.X(a), p.Y(v)], [p.X(b), p.Y(v)]);
    });
    return pts.reduce((acc, q) => {
      if (q) acc[acc.length - 1].push(q); else acc.push([]);
      return acc;
    }, [[]]).filter((run) => run.length);
  };
  const marks = (list, p, color) => list.map(([x, n]) => `<circle cx="${R(p.X(x + 0.1))}" `
    + `cy="${R(p.Y(Math.log10(n)))}" r="0.75" fill="${color}"/>`).join('');
  const sides = [
    { x: LM, label: '\\eta_c', lines: [['cwbBg3', 7.0724, palette.ink]],
      dots: [[C3, data.hanford]] },
    { x: LM + PW + GAP, label: '\\hat\\rho_c', lines: [['pyBg', 7.2159, palette.ink],
      ['pyBgEx', 7.2159, data.purple]], dots: [[PYCBC, data.hanford]] },
  ];
  let out = '';
  for (const side of sides) {
    const p = panel(side.x, 4, PW, PH, X, LV, []);
    out += p.out;
    for (const [key, first, color] of side.lines) {
      for (const run of stairs(decode(key), first, p)) out += poly(run, color, 0.35);
    }
    for (const [list, color] of side.dots) out += marks(list, p, color);
    for (const e of [-6, -4, -2, 0, 2]) {
      out += line(side.x, p.Y(e), side.x + 0.9, p.Y(e), palette.muted, 0.2)
        + tex(`10^{${e}}`, side.x - 1.2, p.Y(e) + 0.9, 2.3, 1, palette.muted);
    }
    for (const x of [8, 12, 16, 20, 24]) {
      out += line(p.X(x), 4 + PH, p.X(x), 4 + PH - 0.9, palette.muted, 0.2)
        + tex(String(x), p.X(x), 4 + PH + 3.8, 2.3, 0.5, palette.muted);
    }
    out += tex(`${sf('Detection statistic')}\\ ${side.label}`, side.x + PW / 2, H - 0.8, 2.6,
      0.5, palette.muted)
      + tex(sf('GW150914'), p.X(side.dots[0][0].at(-1)[0] + 0.1) - 1.4, p.Y(0) - 2.2, 2.4, 1,
        data.hanford);
  }
  return svg(W, H, out + tex(sf('Number of events'), 3, 4 + PH / 2, 2.6, 0.5, palette.muted,
    true));
}
// Candidate events [statistic, count]: the nonzero bins of the same files.
const C3 = [[7.07, 11], [7.27, 8], [7.47, 7], [7.67, 3], [7.87, 3], [8.07, 1], [19.97, 1]];
const PYCBC = [[7.22, 4], [7.42, 14], [7.62, 12], [7.82, 16], [8.02, 7], [8.22, 3], [8.42, 1],
  [8.82, 1], [9.42, 1], [23.42, 1]]; // left edges of the 0.2-wide bins, as in the files
// #endregion

// ─── 3 · Fonts ──────────────────────────────────────────────────────────────
const FONTS = { // every face the pages paint
  Gelasio: ['400', '400i', '700', '700i'],
  'Albert Sans': ['400', '400i', '500', '600', '600i', '700', '700i'],
};

// ─── 4 · Build & show ───────────────────────────────────────────────────────
await initMathEngine(); // gotcha: math-bundle. Unawaited, every formula is a grey box
const figures = { // width, drawing, placement, alt text
  fig1: [MEASURE, figure1, { position: 'here', span: 'page' }, 'One chirp in both detectors.'],
  fig2: [COLUMN, figure2, { position: 'top' }, 'Strain; separation falls, speed rises.'],
  fig3: [COLUMN, figure3, { position: 'auto' }, 'A Michelson interferometer with arm cavities.'],
  fig4: [MEASURE, figure4, { position: 'top', span: 'page' }, 'GW150914 far beyond the noise.'] };
const resources = [];
for (const [id, [width, draw, placement, altText]] of Object.entries(figures)) {
  const markup = draw(width);
  const height = Number(/height="([\d.]+)"/.exec(markup)[1]) / PX_PER_MM;
  await loadSvg(`${id}.svg`, markup);
  resources.push({ id, typeId: 'figure', kind: 'svg', createdAt: 0, updatedAt: 0, placement,
    svg: { fileId: `${id}.svg`, width: width * PX_PER_MM, height: height * PX_PER_MM },
    caption: caption[id], note: caption[`${id}.note`], altText });
}
resources.push({ id: 'tab1', typeId: 'table', kind: 'table', createdAt: 0, updatedAt: 0,
  placement: { position: 'auto' }, caption: caption.tab1, note: caption['tab1.note'], table });
const doc = await buildDocumentWithFonts({ markdown: source, resources }, config(),
  kitFonts(FONTS));
showPages(doc, { title: 'A physics letter with its figure across both columns' });
offerPdf(() => renderToPdf(doc, { fontProvider: fontsourceProvider, resourceBytes: imageBytes }),
  `${RECIPE}.pdf`); // formulas and figures stay vector paths

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

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

// ─── Kit · fonts v3 ── the same in every recipe · postext.dev/cookbook
// Postext loads the faces a config asks for before layout (prepareFonts,
// buildDocumentWithFonts) and asks the kit for the files: Fontsource's, the
// files the PDF embeds too.

/** The options of buildDocumentWithFonts / prepareFonts / withLoadedFonts:
 *  the Fontsource resolver, and the faces of FONTS loaded with the ones the
 *  config names. FONTS = { 'Family Name': ['400', '400i', '700'] }. */
function kitFonts(FONTS = {}) {
  const faces = Object.entries(FONTS).flatMap(([family, specs]) => specs.map((spec) =>
    ({ family, weight: parseInt(spec, 10), style: spec.endsWith('i') ? 'italic' : 'normal' })));
  return { resolve: fontsourceResolver, faces };
}

/** A face's Fontsource files, in a fixed order: latin, then latin-ext and greek
 *  when `text` needs them and the family ships them (kitSubsetsFor), each with
 *  its unicode-range. Null for a weight or slant Fontsource does not ship. */
async function fontsourceResolver(family, weight, style, { text = '' } = {}) {
  const id = fontsourceId(family);
  const meta = await fontsourceMeta(family);
  if (meta && !(meta.weights.includes(weight) && meta.styles.includes(style))) return null;
  kitStatus('Loading fonts…');
  const at = await fontsourceAt(id);
  return ['latin', ...kitSubsetsFor(text, meta)].map((subset) => ({
    source: `${at}files/${id}-${subset}-${weight}-${style}.woff2`,
    unicodeRange: kitRanges()[subset],
  }));
}

/** The unicode-range of each Fontsource file the kit loads (a function: the
 *  kit is inlined after the pen's code, which runs before a const would be set). */
function kitRanges() {
  return {
    latin: 'U+0000-00FF,U+0131,U+0152-0153,U+02BB-02BC,U+02C6,U+02DA,U+02DC,U+0304,U+0308,U+0329,'
      + 'U+2000-206F,U+20AC,U+2122,U+2191,U+2193,U+2212,U+2215,U+FEFF,U+FFFD',
    'latin-ext': 'U+0100-02BA,U+02BD-02C5,U+02C7-02CC,U+02CE-02D7,U+02DD-02FF,U+0304,U+0308,U+0329,'
      + 'U+1D00-1DBF,U+1E00-1E9F,U+1EF2-1EFF,U+2020,U+20A0-20AB,U+20AD-20C0,U+2113,U+2C60-2C7F,U+A720-A7FF',
    greek: 'U+0370-03FF',
  };
}

/** For the cjk, arabic and comics blocks, which add their own files: the
 *  faces of `faces` = { 'Family Name': ['400', '400i', '700'] }. `text` is the sample:
 *  č ł † α χ also load latin-ext and greek files (kitSubsetsFor). With
 *  `optional`, a face Fontsource does not ship is skipped instead of failing.
 *  The files are added once all have loaded, in the order asked (latin, then
 *  latin-ext, then greek), whatever order they arrive in: the last one added
 *  wins a character two files hold. Resolves to the number of faces added. */
async function loadFonts(faces, text = '', { optional = false } = {}) {
  kitStatus('Loading fonts…');
  const ranges = kitRanges();
  const jobs = [];
  for (const [family, specs] of Object.entries(faces)) {
    const id = fontsourceId(family);
    const todo = [...new Set(specs)].map((spec) => [parseInt(spec, 10), spec.endsWith('i') ? 'italic' : 'normal'])
      .filter(([weight, style]) => !hasFace(family, weight, style)); // before any await
    const at = fontsourceAt(id);
    const meta = optional || /[^\0-ÿ]/u.test(text) ? await fontsourceMeta(family) : null;
    const subsets = ['latin', ...kitSubsetsFor(text, meta)];
    for (const [weight, style] of todo) {
      if (optional && !(meta?.weights.includes(weight) && meta.styles.includes(style))) continue;
      for (const subset of subsets) {
        const url = `${await at}files/${id}-${subset}-${weight}-${style}.woff2`;
        const face = new FontFace(family, `url(${url}) format('woff2')`,
          { weight: String(weight), style, unicodeRange: ranges[subset] });
        jobs.push(face.load().catch(() => {
          if (subset === 'latin' && !optional) throw new Error(`Fontsource has no ${family} ${weight} ${style}`);
        }));
      }
    }
  }
  const ready = await Promise.all(jobs).catch((error) => { kitFail(error); throw error; });
  return ready.filter((face) => face && document.fonts.add(face)).length;
}

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

/** The files beyond latin `text` needs that `meta`'s family ships. */
function kitSubsetsFor(text, meta) {
  return [[/[Ā-˿ᴀ-ᶿḀ-ỿ†ℓⱠ-Ɀ꜠-ꟿ]/u, 'latin-ext'], [/[Ͱ-Ͽ]/u, 'greek']]
    .filter(([re, x]) => re.test(text) && meta?.subsets?.includes(x)).map(([, x]) => x);
}

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

/** A package's folder at the one version @5 names now (#766). */
function fontsourceAt(id, u = `https://cdn.jsdelivr.net/npm/@fontsource/${id}@`) {
  return (fontsourceAt.c ??= {})[id] ??= fetch(u + '5/package.json').then((r) => r.json())
    .then((p) => p.version || 5, () => 5).then((v) => `${u}${v}/`);
}

/** The family's Fontsource metadata (weights, styles, subsets), or null. */
function fontsourceMeta(family) {
  fontsourceMeta.cache ??= new Map();
  const id = fontsourceId(family);
  if (!fontsourceMeta.cache.has(id)) {
    fontsourceMeta.cache.set(id, fetch(`https://api.fontsource.org/v1/fonts/${id}`)
      .then((res) => (res.ok ? res.json() : null), () => null));
  }
  return fontsourceMeta.cache.get(id);
}

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

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

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

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

// ─── Kit · pdf v3 ── the same in every recipe that exports a PDF
/** The Fontsource files the screen used, as TrueType, with their range and
 *  place: the nearest weight the family ships, upright with no italic;
 *  latin, then what its letters need (kitSubsetsFor). */
async function fontsourceProvider(family, weight, style, request) {
  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 text = String.fromCodePoint(...(request?.codePoints ?? []));
  const files = await Promise.all(['latin', ...kitSubsetsFor(text, meta)].map(async (subset, order) => {
    const res = await fetch(`${await fontsourceAt(id)}files/${id}-${subset}-${w}-${s}.woff2`);
    if (!res.ok) throw new Error(`Fontsource has no ${family} ${w} ${s} ${subset}`);
    return Object.assign(await decompressWoff2(new Uint8Array(await res.arrayBuffer())),
      { unicodeRange: kitRanges()[subset], order });
  }));
  return files.length === 1 ? files[0] : files;
}

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

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

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

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

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

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

## Varianten

### Den mitgelieferten IEEE-Stil unverändert verwenden

Lassen Sie die Änderungen weg, und die Quellen lauten im Text *[2], [3], [4]* und in der Liste *A. Einstein, “title”, journal…*.

```diff
-const citations = { style: 'custom', customStyle: aps, link: true,
+const citations = { style: 'ieee', link: true,
```

### Die Nummern hochstellen wie Nature

Der Stil von Nature setzt hochgestellte Nummern und führt Titel auf; er ist mitgeliefert.

```diff
-const citations = { style: 'custom', customStyle: aps, link: true,
+const citations = { style: 'nature', link: true,
```

### Auf A4 setzen

Ändern Sie das Endformat; Satzbreite, Spalten und jede Abbildung folgen daraus.

```diff
-const [TRIM_W, TRIM_H, TOP, BOTTOM, SIDE, GUTTER] = [215.9, 279.4, 21, 21, 16, 7];
+const [TRIM_W, TRIM_H, TOP, BOTTOM, SIDE, GUTTER] = [210, 297, 22, 22, 15, 7];
```

## Häufige Fehler

- **Mathematik braucht await initMathEngine() vor dem ersten Build.** Formeln, die gesetzt werden, bevor die Mathematik-Engine läuft, sind graue Platzhalterkästen. Importieren Sie initMathEngine mit den anderen Symbolen aus https://esm.sh/postext und warten Sie es vor dem ersten Build ab; ein Dokument mit Mathematik, das ohne es gebaut wird, gibt eine Konsolenwarnung aus. Seit postext 1.5 wird MathJax im Paket als ein Modul mitgeliefert, das initMathEngine nachlädt, sodass der einfache Import von einem CDN funktioniert und ?bundle nicht gebraucht wird (bis 1.4 musste jedes Symbol von https://esm.sh/postext?bundle kommen). Importieren Sie nur von einer URL: zwei URLs laden zwei Engines.

- Bis postext 1.18 wurde eine Ressource `'here'` mit `span: 'page'` in einem zweispaltigen Fluss nur eine Spalte breit gesetzt, und ein `$` in einer Bildunterschrift oder einer Zelle wurde so gedruckt, wie es getippt war. Legen Sie 1.19 oder neuer fest.
- Ein griechischer Buchstabe, ⊙ oder ≃, als Zeichen in eine Bildunterschrift oder eine Zelle getippt, fällt am Bildschirm auf eine Systemschrift zurück und fehlt im PDF, das die Latin-Dateien jeder Schrift einbettet. Schreiben Sie ihn als Mathematik, `$\sigma$` oder `$M_\odot$`, und zeichnen Sie ihn in Abbildungen.
- Eine Liste von Hunderten Autoren gehört nicht in eine BibTeX-Datei. Schreiben Sie den Namen so, wie die Zeitschrift ihn druckt, in doppelten geschweiften Klammern: `author={{B. Abbott et al.}}`.
- Behalten Sie die Lizenzzeile des Papers und der Daten. Hier sagen eine Fußnote und das Impressum, was gestrichen wurde, und die Anmerkung jeder Bildunterschrift nennt die Daten und das, was die neu gezeichnete Abbildung weglässt.

## Nachweise

- Rezept: Ignacio Ferro ([@drnachio](https://github.com/drnachio))
- Text: Text: B. P. Abbott et al. (LIGO Scientific Collaboration and Virgo Collaboration), “Observation of Gravitational Waves from a Binary Black Hole Merger”, Phys. Rev. Lett. 116, 061102 (2016), doi:10.1103/PhysRevLett.116.061102. Abridged from the published letter (Sections IV and VII, parts of Sections I, III, V and VI and most of the acknowledgments cut; sections and references renumbered); figures redrawn; figure captions shortened. Licence: CC BY 3.0, https://creativecommons.org/licenses/by/3.0/: LIGO Scientific Collaboration and Virgo Collaboration ([Quelle](https://doi.org/10.1103/PhysRevLett.116.061102)), CC-BY-3.0
- Text: Data for Figures 1, 2 and 4: the GW150914 data files released by the Gravitational Wave Open Science Center (gwosc.org), a service of the LIGO Scientific Collaboration, the Virgo Collaboration and KAGRA; redrawn in code: LIGO Scientific Collaboration and Virgo Collaboration, GWOSC ([Quelle](https://gwosc.org/events/GW150914/)), CC-BY-4.0
- Schrift: Gelasio (OFL-1.1), Albert Sans (OFL-1.1)
- Code: MIT · Beispielinhalt: CC-BY-4.0

## Verwandte Rezepte

- [Nº 002 · Zweispaltiger Aufsatz mit nummerierten Gleichungen](https://postext.dev/de/cookbook/journal-article-with-maths.md): Ein zweispaltiger Physikaufsatz, dessen Formeln im Text und sieben nummerierte Gleichungen MathJax setzt und die im PDF als Vektoren bleiben. · Stufe 3 (Fortgeschritten) · Fachartikel und Wissenschaft
- [Nº 088 · Ein zweispaltiger Konferenzbeitrag im IEEE-Stil](https://postext.dev/de/cookbook/ieee-conference-paper.md): Ein Workshopbeitrag in zwei Spalten: Titelblock über die Seite, IEEE-Zitate mit Bereichen [2]–[4], Verweise auf Section II und eine kompakte nummerierte Liste. · Stufe 2 (Mittel) · Fachartikel und Wissenschaft
- [Nº 095 · Ein medizinischer Fachartikel im Vancouver-Stil](https://postext.dev/de/cookbook/medical-article-vancouver.md): Ein Studienbericht auf A4 in zwei Spalten: hochgestellte Zitatnummern, ein nummeriertes Literaturverzeichnis nach Vancouver und DOIs als Links im PDF. · Stufe 2 (Mittel) · Fachartikel und Wissenschaft
