# Methodsのあるタンパク質構造の論文

> AlphaFoldの論文をA4で組み直す：全幅の太字要約、Nature式の上付き引用、本文の後に小さな文字で組むMethods、その引用番号は本文から続く。

- HTML版: https://postext.dev/ja/cookbook/protein-paper-methods-section
- レシピ No. 139 · 完成した出版物 · 難易度 3 (上級) · 出力: Canvas, PDF
- ジャンル: 論文・学術書
- 必要なもの postext ≥ 1.19.0, postext-pdf ≥ 1.19.0 · テスト環境 1.19.0, postext-pdf 1.19.0 ／テスト日 2026-10-06
- ページ: [1](https://postext.dev/cookbook/protein-paper-methods-section/en/p01.webp?v=1f9a1c62), [2](https://postext.dev/cookbook/protein-paper-methods-section/en/p02.webp?v=1f9a1c62), [3](https://postext.dev/cookbook/protein-paper-methods-section/en/p03.webp?v=1f9a1c62), [4](https://postext.dev/cookbook/protein-paper-methods-section/en/p04.webp?v=1f9a1c62), [5](https://postext.dev/cookbook/protein-paper-methods-section/en/p05.webp?v=1f9a1c62)
- PDF: https://postext.dev/cookbook/protein-paper-methods-section/en/protein-paper-methods-section.pdf?v=1f9a1c62
- Sandboxで開く: https://postext.dev/ja/sandbox#recipe=protein-paper-methods-section&lang=en (.postext: https://postext.dev/cookbook/protein-paper-methods-section/en/protein-paper-methods-section.postext)
- 最終更新: 2026-10-06
- 他の言語: [en](https://postext.dev/en/cookbook/protein-paper-methods-section.md), [es](https://postext.dev/es/cookbook/protein-paper-methods-section.md), [ca](https://postext.dev/ca/cookbook/protein-paper-methods-section.md), [zh](https://postext.dev/zh/cookbook/protein-paper-methods-section.md), [ar](https://postext.dev/ar/cookbook/protein-paper-methods-section.md)

## かんたんな説明

よく引用される生物学の論文を抄録し、学術誌の体裁で組み直したもの。Methodsは本文の後に小さな文字で置かれ、その参考文献の番号は本文が終わったところから続きます。

## できあがり

A4に組んだ生命科学の学術誌論文5ページ。Jumperらの*Highly accurate protein structure prediction with AlphaFold*を、CC BY 4.0のもとでNatureから抄録したものです。1ページ目は青い帯で始まり、その下に表題、7.4 ptで組んだ34名全員の著者（所属番号は上付き）、全幅の太字の要約が続きます。その下に全幅で図1。本文が引用するCASP14の精度の棒グラフと、PDBからたどった標的T1049の主鎖の中にAlphaFoldのモデルを信頼度で色分けして描いた図です。本文はNatureの上付き番号で引用します。本文は参考文献1–35で終わります。Methodsはその後に8.2 ptで続き、そこで初めて引用される文献は次の番号を取るので、Methodsのリストは学術誌と同じく36から始まります。

**このレシピが答える問い:**

- 本文の後にMethodsを小さな文字で組み、引用番号を本文から続けるにはどうすればよいですか？
- arXivやPubMed Centralのオープンアクセス論文を、引用・図・ライセンス表記を保ったままPostextで組み直すには？
- 医学雑誌のように、引用番号を上付きにし、DOIを有効なリンクにするには？
- 番号付きキャプションのある図を加え、本文で参照するには（「図3.2を参照」）？

## 手短な答え

```js
// script.js, 行 30–47
// [@key] prints a raised number in order of first citation, so a work the Methods cite for
// the first time takes the next number after the main text's last, and one cited again
// keeps its number. Nature prints two lists, and :::bibliography{scope=new} lists only the
// works no earlier list printed: the same block after the main text and after the Methods
// sets references 1–35, then 36–55.
registerCitationEngine(createCiteprocEngine({ styles: STYLES, locales: LOCALES }));
const citations = {
  style: 'nature', // raised 1–4, "Nature 577, 706–710 (2020)." entries
  bibliography: { fontSize: em(0.8), lineHeight: pt(9.6), entrySpacing: pt(0.8),
    labelWidth: mm(5), labelAlign: 'right' }, // 9. and 10. end together
};
const references = '### References\n\n:::bibliography{title="" scope=new}';
// "# Methods {style=\"methods\"}" opens a section that runs to the end of the article: an H1
// that breaks nothing, names no running chapter and sets its body two sizes down.
const methods = { id: 'methods', numbered: false, runningChapter: false,
  breakBefore: { enabled: false }, fontFamily: SANS, fontSize: pt(11.5), lineHeight: pt(LEAD),
  fontWeight: 700, color: col('ink'), marginTop: pt(LEAD), marginBottom: pt(LEAD / 2),
  bodyStyle: { fontSize: pt(8.2), lineHeight: pt(SMALL) } };
```

## 材料

**学べること**

- [見出しスタイル](https://postext.dev/ja/docs/configuration.md#見出しスタイル): 見出しの階層の名前付きの変種で、{style="…"}で選びます。章ごとに章扉、文字組み、柱、ページ設定を変えられます。
- [引用スタイルによる引用](https://postext.dev/ja/docs/document-format.md#引用と参考文献): 引用は[@key, p. 33]のように書き、設定で選んだCSLスタイル（APA、Chicago、MLA、IEEE、Vancouver、ISO 690、GB/T 7714…）で整形され、文献一覧の項目にリンクされます。
- [参考文献から作る文献一覧](https://postext.dev/ja/docs/document-format.md#引用と参考文献): 文書の参考文献（フロントマターまたはBibTeXブロック）から作る引用文献の一覧です。:::bibliographyを置いた位置か、最後の章の後に入ります。

**ほかに使うもの**

- [上付きと下付き](https://postext.dev/ja/docs/document-format.md#インライン書式)
- [デザインした章扉](https://postext.dev/ja/docs/configuration.md#幅と詳細デザイン)
- [見出しの属性](https://postext.dev/ja/docs/document-format.md#見出しの属性)
- [囲み](https://postext.dev/ja/docs/configuration.md#囲みスタイル)
- [段抜きの囲み](https://postext.dev/ja/docs/configuration.md#calloutコンテナー)
- [柱とノンブル](https://postext.dev/ja/docs/configuration.md#柱とノンブル)
- [ページの役割ごとの柱](https://postext.dev/ja/docs/configuration.md#テキスト要素)
- [その場に置く図](https://postext.dev/ja/docs/document-format.md#ブロック埋め込み任意本文中への明示的な配置)
- [キャプションのスタイル](https://postext.dev/ja/docs/configuration.md#キャプションスタイル)
- [出典とクレジット](https://postext.dev/ja/docs/configuration.md#キャプションスタイル)
- [表スタイル](https://postext.dev/ja/docs/configuration.md#表スタイル)
- [番号付きキャプション](https://postext.dev/ja/docs/document-format.md#初出順の番号付け)
- [段落スタイル](https://postext.dev/ja/docs/configuration.md#段落スタイル)
- [セマンティックカラーパレット](https://postext.dev/ja/docs/configuration.md#カラーパレット)
- [PDFの書き出し](https://postext.dev/ja/docs/configuration.md#pdfの生成)
- [PDFに埋め込むフォント](https://postext.dev/ja/docs/configuration.md#なぜフォントプロバイダーが必要か)
- [図を配置する参照](https://postext.dev/ja/docs/document-format.md#インライン参照基本の形)
- [「図」と「表」を文書の言語で](https://postext.dev/ja/docs/configuration.md#リソースの種類)
- [独自のリソースの種類](https://postext.dev/ja/docs/configuration.md#リソースの種類)
- [リソースとしての図と表](https://postext.dev/ja/docs/document-format.md#リソース)
- [番号のない章](https://postext.dev/ja/docs/configuration.md#見出しスタイル)

**設定の一覧**

- [`bodyText`](https://postext.dev/ja/docs/configuration.md#本文), [`calloutStyles`](https://postext.dev/ja/docs/configuration.md#囲みスタイル), [`captionStyle`](https://postext.dev/ja/docs/configuration.md#キャプションスタイル), [`citations`](https://postext.dev/ja/docs/configuration.md#引用), [`colorPalette`](https://postext.dev/ja/docs/configuration.md#カラーパレット), [`footer`](https://postext.dev/ja/docs/configuration.md#柱とノンブル), [`header`](https://postext.dev/ja/docs/configuration.md#柱とノンブル), [`headingStyles`](https://postext.dev/ja/docs/configuration.md#見出しスタイル), [`headings`](https://postext.dev/ja/docs/configuration.md#見出し), [`layout`](https://postext.dev/ja/docs/configuration.md#レイアウト), [`locale`](https://postext.dev/ja/docs/configuration.md#ハイフネーション), [`page`](https://postext.dev/ja/docs/configuration.md#ページ), [`paragraphStyles`](https://postext.dev/ja/docs/configuration.md#段落スタイル), [`resourceTypes`](https://postext.dev/ja/docs/configuration.md#リソースの種類), [`tableStyle`](https://postext.dev/ja/docs/configuration.md#表スタイル)

**API**

- [`LOCALES`](https://postext.dev/ja/docs/document-format.md#引用と参考文献), [`STYLES`](https://postext.dev/ja/docs/document-format.md#引用と参考文献), [`buildDocument`](https://postext.dev/ja/docs/configuration.md#文書のビルド), [`clearMeasurementCache`](https://postext.dev/ja/docs/configuration.md#計測キャッシュ), [`createCiteprocEngine`](https://postext.dev/ja/docs/document-format.md#引用と参考文献), [`decompressWoff2`](https://postext.dev/ja/docs/configuration.md#ブラウザー向けフォントプロバイダーfontsource--woff2), [`defaultResourceTypes`](https://postext.dev/ja/docs/configuration.md#リソースの種類), [`registerCitationEngine`](https://postext.dev/ja/docs/document-format.md#引用と参考文献), [`registerResourceImage`](https://postext.dev/ja/docs/architecture.md#apiの概要), [`renderPageToCanvas`](https://postext.dev/ja/docs/configuration.md#ページをビットマップに描画する), [`renderToPdf`](https://postext.dev/ja/docs/configuration.md#pdfの生成)

**書体**

- Noto Serif (OFL-1.1), Noto Sans (OFL-1.1)

## 作り方

### 1 · 二つの参考文献リストと一つの番号

コードは上の[手短な答え](#手短な答え)です。引用は初出順に番号が振られるので、番号を続けるための設定は要りません。Methodsで初めて引用されるjackhmmerは36、本文ですでに引用されたAmber力場は30のままです。リストは`:::bibliography{scope=new}`で組みます。これは、それより前のリストが載せていない文献だけを並べます。スクリプトは同じブロックをDiscussionの後とMethodsの後に置くので、一つ目は1–35、二つ目は36–55を組み、Amberは繰り返されません。Methodsの見出しは独自のスタイルを持つ第1レベルの見出しです。その`bodyStyle`がセクションを8.2 pt、行送り10.8 ptで組み、`breakBefore: { enabled: false }`で段の中にとどめ、`runningChapter: false`で柱を論文の表題のままにします。自分の論文では、`.bib`の書き出しを`:::references{format=bibtex}`ブロックに貼り付け、語の直後に`[@key]`で引用し、`nature`を投稿先のスタイルに替えます。

### 2 · 34名の著者を収める表題ブロック

```js
// script.js, 行 51–74
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 = { fontFamily: SANS, fontWeight: 600, letterSpacing: pt(1.4),
  textTransform: 'uppercase' };
const titleBlock = { enabled: true,
  minHeight: mm(50),
  slot: { elements: [
    { kind: 'box', id: 'band', style: { backgroundColor: col('accent') },
      placement: { anchor: { to: 'page', edge: 'top-left' },
        size: { width: 'fill', height: mm(14) } } },
    text('kicker', '{attr.kicker}', SANS, 8.5, { ...caps, color: col('paper'),
      placement: at('page', 'top-left', INNER, 5.6, 120) }),
    text('title', '{titleText}', SANS, 23, { fontWeight: 700, lineHeight: 1.12,
      placement: at('container', 'top-left', 0, 1, MEASURE - 30) }),
    text('authors', '{attr.authors}', SANS, 7.4, { inlineMarks: true, lineHeight: 1.42,
      placement: at('#title', 'below', 0, 4, MEASURE) }),
    { kind: 'rule', id: 'rule', thickness: pt(0.5), color: col('rule'),
      placement: at('#authors', 'below', 0, 3, MEASURE) },
    text('source', '{attr.source}', SANS, 7, { fontStyle: 'italic', color: col('muted'),
      placement: at('#rule', 'below', 0, 1.6, MEASURE) }),
  ] },
};
```

著者リストは見出しの属性一つ、`authors="John Jumper^1,4^, Richard Evans^1,4^, …"`で、7.4 ptのNoto Sansで版面の全幅に組み、34名が5行に収まります。`inlineMarks`が所属番号を上付きにします。所属と同等貢献の注記はNatureと同じく末尾の*Author information*に置き、1ページ目に要約と図の場所を残します。著者は一人も省いていません。偶数ページの柱だけがリストを*Jumper et al.*と略します。

### 3 · 全幅の要約と、その下の図1

```js
// script.js, 行 308–334
const resources = () => [
  { id: 'fig1', typeId: 'figure', kind: 'svg', createdAt: 0, updatedAt: 0,
    placement: { position: 'here', span: 'page' },
    svg: { fileId: 'fig1.svg', width: 1780, height: 640 },
    caption: '**AlphaFold produces highly accurate structures.** **a**, Median accuracy on the '
      + 'CASP14 domains of AlphaFold and of the next best method, as r.m.s.d.~95~ over Cα atoms '
      + '(backbone) and over all atoms, with 95% confidence intervals; the dashed line is the '
      + 'width of a carbon atom, about 1.4 Å. **b**, CASP14 target T1049 (PDB 6Y4F): the '
      + 'experimental Cα trace (grey) and the AlphaFold model of the same chain, coloured by '
      + 'its per-residue confidence (pLDDT).',
    note: 'Redrawn for this edition. a, from the values given in the text (the original plots '
      + 'the top 15 of 146 entries). b, from PDB 6Y4F (CC0) and AlphaFold DB model '
      + 'AF-B4EUK6-F1, AlphaFold Monomer v2.0 (CC BY 4.0), superposed on 134 Cα atoms; the '
      + 'original shows the CASP14 prediction.',
    altText: 'Bars: AlphaFold 0.96 and 1.5 Å, next best 2.8 and 3.5 Å. Two backbones that '
      + 'lie almost on top of each other.' },
  { id: 'tbl-timings', typeId: 'table', kind: 'table', createdAt: 0, updatedAt: 0,
    placement: { position: 'here' },
    caption: '**Inference times.** One model on one V100 GPU, by protein length.',
    note: 'Compiled for this edition from the timings given in ‘Inference regimen’.',
    table: { model: { headerRowCount: 1, columnWidths: [1, 1.3, 1.3], rows: [
      ['Residues', 'With ensembling (CASP14)', 'Without ensembling'],
      ['256', '4.8 min', '0.6 min'], ['384', '9.2 min', '1.1 min'],
      ['2,500', '18 h', '2.1 h'],
    ].map((row, r) => row.map((content, c) => ({ content, ...(r === 0 && { isHeader: true }),
      ...(c > 0 && { align: 'right' }) }))) } } },
];
```

要約は`span: 'page'`で枠のない囲みで、文字はNoto Sansのセミボールドです。図1はその直後に`::resource{id="fig1"}`として置き、配置`{ position: 'here', span: 'page' }`によってその位置で2段にまたがって組まれます。2段の本文は図の説明文の下から始まります。説明文はNatureの説明文と同じく太字の一文で始まります。リソース型の`captionPrefix`と説明文スタイルの`labelSeparator`で、ラベルは*Fig. 1 |*になります。表1は`position: 'here'`でMethodsのその位置に埋め込み、段の幅に収めます。

### 4 · 公開座標から描いたタンパク質の主鎖

```js
// script.js, 行 338–485
// PDB 6Y4F chain A, residues 25–158: Cα x, y in tenths of Å, depth z in Å (CC0)
const EXP = [
  -56,-91,-6,-36,-77,-3,-4,-98,-2,26,-81,-1,63,-86,-1,95,-73,1,116,-100,2,146,
  -79,3,159,-46,2,187,-34,4,209,-6,3,211,10,0,187,17,-3,182,53,-2,185,46,2,
  167,14,3,148,-4,6,122,-28,4,109,-58,6,76,-67,4,45,-89,4,11,-79,3,-2,-114,
  3,-37,-116,1,-64,-95,0,-87,-117,-2,-124,-110,-2,-135,-80,-5,-161,-95,-7,-158,
  -74,-10,-173,-41,-9,-141,-20,-9,-148,15,-8,-128,31,-5,-105,55,-7,-106,82,-4,
  -144,80,-3,-156,116,-3,-122,132,-2,-126,155,1,-102,131,3,-88,98,2,-68,72,4,
  -58,37,3,-23,33,4,6,10,5,32,28,2,59,21,0,68,31,-4,89,62,-3,96,70,-7,102,
  52,-10,65,59,-11,53,41,-8,39,71,-6,38,70,-2,64,79,0,59,86,4,33,60,5,42,
  36,8,7,42,9,-29,48,8,-55,21,9,-88,39,8,-105,69,6,-122,57,3,-134,87,1,-170,
  78,2,-167,42,1,-180,38,-3,-165,5,-4,-132,-13,-3,-150,-42,-2,-161,-18,1,-125,
  -6,1,-114,-43,2,-141,-49,4,-129,-20,6,-92,-30,6,-98,-66,7,-120,-57,10,-94,
  -33,11,-62,-52,11,-54,-89,11,-36,-104,8,-8,-109,7,3,-73,8,40,-75,9,67,-51,
  10,93,-26,9,92,8,7,119,30,5,128,47,2,151,77,2,149,93,-2,152,82,-5,173,52,
  -6,150,22,-6,162,-13,-8,144,-44,-6,119,-43,-9,109,-8,-8,106,-15,-4,72,-24,
  -3,53,-20,0,15,-23,0,-19,-8,1,-20,28,-1,-48,53,-1,-38,89,0,-52,122,1,-25,
  128,3,-19,107,7,18,115,7,47,129,5,57,121,1,26,112,-1,28,122,-4,-9,117,-5,
  -8,83,-3,-9,51,-5,-13,14,-5,-49,3,-4,-61,-14,-8,-50,-50,-8,-19,-46,-6,8,
  -72,-7,43,-65,-6,67,-94,-5,43,-121,-6,32,-148,-3,-6,-157,-3,-17,-192,-4,-23,
  -216,-1,
];
// AlphaFold DB AF-B4EUK6-F1, the same residues superposed on 6Y4F (CC BY 4.0)
const MODEL = [
  -57,-93,-6,-37,-77,-3,-5,-98,-2,25,-82,-1,63,-87,-1,94,-74,1,112,-103,3,144,
  -84,3,158,-50,2,189,-34,4,209,-2,3,212,16,0,187,21,-3,181,57,-2,181,48,2,
  166,13,2,148,-4,5,122,-29,4,110,-58,6,76,-66,4,46,-88,4,12,-79,3,0,-115,3,
  -37,-113,2,-64,-95,0,-87,-118,-2,-125,-110,-2,-136,-81,-4,-162,-95,-7,-155,
  -74,-10,-173,-42,-9,-141,-22,-9,-146,14,-8,-126,31,-5,-102,55,-7,-106,83,-4,
  -143,81,-4,-156,117,-3,-122,132,-2,-123,157,1,-101,132,3,-86,98,2,-68,72,4,
  -58,36,3,-22,32,4,7,8,4,32,26,2,61,20,0,67,31,-4,89,62,-3,95,73,-7,100,
  56,-10,64,67,-11,52,44,-8,40,74,-6,38,68,-2,65,77,0,59,84,4,35,56,5,45,
  30,8,10,37,9,-25,47,8,-54,21,9,-86,40,8,-105,69,6,-120,56,3,-132,87,1,
  -169,78,2,-166,42,0,-177,37,-3,-163,3,-4,-130,-15,-3,-150,-42,-2,-162,-17,1,
  -125,-5,1,-114,-42,2,-142,-47,5,-129,-15,7,-93,-28,6,-103,-62,8,-122,-46,11,
  -90,-26,12,-62,-52,11,-59,-89,10,-37,-104,7,-8,-110,7,3,-74,8,40,-74,9,66,
  -48,10,95,-26,9,95,7,7,124,30,6,127,45,2,147,78,2,145,92,-1,153,87,-5,172,
  54,-6,147,26,-7,158,-6,-8,145,-39,-7,117,-42,-10,105,-7,-8,106,-14,-5,72,
  -24,-3,54,-21,0,16,-25,0,-18,-8,0,-19,27,-1,-48,52,-1,-37,88,0,-51,122,1,
  -26,127,4,-23,104,7,15,108,7,45,124,5,57,120,2,26,111,-1,31,123,-4,-7,116,
  -5,-5,83,-3,-8,51,-5,-12,14,-5,-48,2,-5,-62,-14,-8,-53,-52,-8,-20,-47,-6,5,
  -74,-7,39,-66,-5,64,-95,-5,99,-89,-7,117,-72,-4,146,-94,-2,178,-75,-3,190,
  -46,-1,
];
// The model's per-residue pLDDT, read from its B-factor column
const PLDDT = [
  87,89,87,87,83,87,82,84,85,84,82,79,81,86,90,89,93,93,94,95,92,95,87,90,
  93,87,89,88,82,75,87,92,97,98,98,98,98,98,98,98,98,99,99,99,99,99,98,96,
  94,93,90,84,87,89,95,95,96,97,98,98,98,98,98,98,99,99,99,98,98,98,98,97,
  97,98,98,98,98,98,98,97,98,98,96,95,89,96,97,96,96,96,96,94,93,91,88,86,
  82,73,67,61,61,69,83,89,96,97,98,99,99,98,98,97,96,97,96,96,95,95,96,97,
  97,96,97,94,87,90,84,82,69,65,64,61,55,53,
];
const n2 = (v) => +v.toFixed(2);
// An SVG drawn as an image cannot see the page's web fonts (gotcha: svg-no-webfonts), so the
// figure carries Noto Sans inline, as a data URL of the Fontsource file.
async function inlineFace(family, weight) {
  const id = family.toLowerCase().replace(/\s+/g, '-');
  const url = `https://cdn.jsdelivr.net/npm/@fontsource/${id}@5/files/${id}-latin-${weight}`
    + '-normal.woff2';
  const bytes = new Uint8Array(await (await fetch(url)).arrayBuffer());
  let bin = '';
  for (const b of bytes) bin += String.fromCharCode(b);
  return `<style>@font-face{font-family:F;src:url(data:font/woff2;base64,${btoa(bin)}) `
    + `format('woff2')}text{font-family:F}</style>`;
}
const label = (x, y, s, size = 2.5, extra = '') => { // ink unless extra sets a fill
  const fill = extra.includes('fill') ? '' : `fill="${palette.ink}" `;
  return `<text x="${n2(x)}" y="${n2(y)}" font-size="${size}" ${fill}${extra}>${s}</text>`;
};
// a: median r.m.s.d.95 with its 95% interval, as the text gives them
const ACCURACY = [
  { group: 'Backbone', af: [0.96, 0.85, 1.16], next: [2.8, 2.7, 4.0] },
  { group: 'All-atom', af: [1.5, 1.2, 1.6], next: [3.5, 3.1, 4.2] },
];
function bars() {
  const [L, B, T, H] = [9, 54, 8, 4.5]; // axis left, baseline, top, Å at the top
  const y = (v) => B - (v / H) * (B - T);
  let s = label(0, 3.2, 'a', 3.6, 'font-weight="700"');
  for (const v of [0, 1, 2, 3, 4]) {
    s += `<path d="M${L} ${n2(y(v))}H58" stroke="${palette.rule}" stroke-width="0.15"/>`
      + label(L - 1.5, y(v) + 0.9, v, 2.5, 'text-anchor="end"');
  }
  s += label(1.6, 31, 'r.m.s.d.<tspan font-size="1.8" dy="0.6">95</tspan><tspan dy="-0.6"> (Å)'
    + '</tspan>', 2.5, 'text-anchor="middle" transform="rotate(-90 1.6 31)"');
  ACCURACY.forEach(({ group, af, next }, g) => {
    const x0 = L + 4 + g * 24;
    [[af, palette.accent], [next, palette.rule]].forEach(([[m, lo, hi], fill], k) => {
      const x = x0 + k * 10;
      s += `<rect x="${x}" y="${n2(y(m))}" width="8" height="${n2(B - y(m))}" fill="${fill}"/>`
        + `<path d="M${x + 4} ${n2(y(lo))}V${n2(y(hi))}M${x + 2.6} ${n2(y(lo))}h2.8M${x + 2.6} `
        + `${n2(y(hi))}h2.8" stroke="${palette.ink}" stroke-width="0.3" fill="none"/>`
        + label(x + 4, B - 1.4, m.toFixed(m < 1 ? 2 : 1), 2.3, // inside the bar's foot
          `text-anchor="middle" fill="${k ? palette.ink : palette.paper}"`);
    });
    s += label(x0 + 9, B + 4, group, 2.6, 'text-anchor="middle"');
  });
  let dash = '';
  for (let x = L; x < 57; x += 2.4) dash += `M${n2(x)} ${n2(y(1.4))}h1.4`;
  s += `<path d="${dash}" stroke="${palette.ink}" stroke-width="0.3"/>`
    + `<path d="M${L} ${B}H58" stroke="${palette.ink}" stroke-width="0.35"/>`
    + `<rect x="${L + 2}" y="59.3" width="2.6" height="2.6" fill="${palette.accent}"/>`
    + label(L + 5.5, 61.6, 'AlphaFold') + `<rect x="${L + 22}" y="59.3" width="2.6" `
    + `height="2.6" fill="${palette.rule}"/>` + label(L + 25.5, 61.6, 'Next best method');
  return s;
}
// b: both chains as smooth Cα traces, far segments first
const BANDS = [[90, '#0053d6', 'pLDDT > 90'], [70, '#65cbf3', 'pLDDT 70–90'],
  [50, '#ffdb13', 'pLDDT 50–70'], [0, '#ff7d45', 'pLDDT < 50']];
const band = (p) => BANDS.find(([min]) => p > min);
function trace(xyz, cx, cy, k) {
  const pt = (i) => [cx + xyz[3 * i] * k, cy - xyz[3 * i + 1] * k, xyz[3 * i + 2]];
  const n = xyz.length / 3;
  return Array.from({ length: n - 1 }, (_, i) => {
    const [p0, p1, p2, p3] = [pt(Math.max(i - 1, 0)), pt(i), pt(i + 1), pt(Math.min(i + 2, n - 1))];
    const c1 = [0, 1].map((a) => p1[a] + (p2[a] - p0[a]) / 6);
    const c2 = [0, 1].map((a) => p2[a] - (p3[a] - p1[a]) / 6);
    return { i, z: (p1[2] + p2[2]) / 2, d: `M${n2(p1[0])} ${n2(p1[1])}C${n2(c1[0])} `
      + `${n2(c1[1])} ${n2(c2[0])} ${n2(c2[1])} ${n2(p2[0])} ${n2(p2[1])}` };
  });
}
function backbone() {
  const [cx, cy, k] = [103, 32, 0.142]; // mm per tenth of an Å
  const exp = trace(EXP, cx, cy, k);
  const model = trace(MODEL, cx, cy, k);
  const width = (z) => 1 + z / 40;
  let s = label(66, 3.2, 'b', 3.6, 'font-weight="700"');
  // the experiment as a grey band, then the model on top of it: where they agree, the model's
  // colour runs inside the band
  for (const [segs, halo, wide, colour] of [[exp, 2.4, 1.6, () => palette.rule],
    [model, 1.3, 0.75, (i) => band(Math.min(PLDDT[i], PLDDT[i + 1]))[1]]]) {
    for (const { i, z, d } of [...segs].sort((a, b) => a.z - b.z)) {
      const w = width(z);
      s += `<path d="${d}" stroke="${palette.paper}" stroke-width="${n2(w * halo)}" `
        + `fill="none"/><path d="${d}" stroke="${colour(i)}" ` // a butt halo: no notch at joins
        + `stroke-width="${n2(w * wide)}" fill="none" stroke-linecap="round"/>`;
    }
  }
  const legend = [[palette.rule, 'Experiment (PDB 6Y4F)'], ...BANDS.slice(0, 3)
    .map(([, c, t]) => [c, `Model: ${t}`])];
  legend.forEach(([c, t], j) => {
    s += `<path d="M138 ${44 + j * 4.6}h5" stroke="${c}" stroke-width="${j ? 1 : 1.6}" `
      + 'stroke-linecap="round"/>' + label(145, 44.9 + j * 4.6, t, 2.4);
  });
  return s + label(138, 39, 'T1049 · residues 25–158', 2.5, 'font-weight="700"');
}
const figure1 = (face) => '<svg xmlns="http://www.w3.org/2000/svg" width="1780" height="640" '
  + `viewBox="0 0 178 64">${face}${bars()}${backbone()}</svg>`;
```

座標はレシピを書いたときに一度だけ読み込みました。PDBエントリー6Y4F（CC0）と、AlphaFold Databaseにある同じタンパク質のモデル（CC BY 4.0）をCα原子で重ね合わせ、実験構造の主軸に合わせて回したものです。スクリプトはそれを三つの短い配列として持ちます。各線はCαの位置を通るベジェ曲線の連なりで、遠いものから近いものへ紙の色の縁取りを付けて描くので、陰影なしで奥行きが出ます。モデルの色はAlphaFold Databaseが使うpLDDTの区分で、ファイルのB因子の列から読んでいます。

### 5 · PDFのギリシャ文字とアクセント付きの文字

```js
// script.js, 行 493–512
// Fontsource cuts each face by script. The PDF gets every file the text of a face needs and
// draws a character from the first file that has it (gotcha: latin-subset).
const SUBSETS = [['latin-ext', /[\u0100-\u024f]/u], ['greek', /[\u0370-\u03ff]/u]];
const fileOf = (family, subset, weight, style) => 'https://cdn.jsdelivr.net/npm/@fontsource/'
  + `${fontsourceId(family)}@5/files/${fontsourceId(family)}-${subset}-${weight}-${style}.woff2`;
const woff = async (url) => decompressWoff2(new Uint8Array(await (await fetch(url)).arrayBuffer()));
const loadGreek = () => Promise.all(Object.entries(FONTS).flatMap(([family, specs]) =>
  specs.map(async (spec) => {
    const [weight, style] = [parseInt(spec, 10), spec.endsWith('i') ? 'italic' : 'normal'];
    const url = fileOf(family, 'greek', weight, style);
    const face = new FontFace(family, `url(${url})`, { weight: `${weight}`, style,
      unicodeRange: 'U+0370-03FF' });
    document.fonts.add(await face.load());
  })));
async function fontProvider(family, weight, style, request) {
  const text = String.fromCodePoint(...(request?.codePoints ?? []));
  const more = SUBSETS.filter(([, test]) => test.test(text))
    .map(([subset]) => woff(fileOf(family, subset, weight, style)));
  return [await fontsourceProvider(family, weight, style), ...(await Promise.all(more))];
}
```

Fontsourceは各書体を文字体系ごとのファイルに分けています。キットはlatinのファイルと、本文が必要とするときはlatin-extも読み込みますが、ギリシャ文字のファイルは読み込みません。そのためCαやχは画面では代替フォントで表示され、PDFでは欠けてしまいます。このリージョンはキャンバス用に各書体のギリシャ文字ファイルを読み込み、`renderToPdf`には、ある書体が組む文字に必要なときにその書体のlatin-extとギリシャ文字のファイルを加えるフォント提供関数を渡します。

## レシピの全体

レシピのフォルダーから合成した1つのファイルで、サンプルのテキストとレシピ集の共通キットを埋め込んであり、自分でページを組み立てます。動かすには、空のページの`<script type="module">`に入れるか、新しいCodePenのpenのJSパネルに貼り付けます（モジュールとして）。esm.shからpostextを読み込むので、インストールもビルドも要りません。

- レシピのフォルダー: https://github.com/drnachio/postext/tree/main/cookbook/protein-paper-methods-section

### script.js

```js
// ═══ Postext Cookbook · Nº 139 · A protein paper with a Methods section ════════════
// https://postext.dev/en/cookbook/protein-paper-methods-section
// Code: MIT · Text: Jumper et al. 2021, abridged (CC BY 4.0) · Figures: drawn in code
// Fonts: Noto Serif, Noto Sans (SIL OFL 1.1) · Needs postext ≥ 1.19.0
import {
  buildDocument, renderPageToCanvas, clearMeasurementCache, registerCitationEngine,
  registerResourceImage, defaultResourceTypes,
} 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 = 'protein-paper-methods-section';

// ─── 1 · Design ─────────────────────────────────────────────────────────────
// #region palette: near-black text and one deep blue, the blue of confident pLDDT
const palette = { ink: '#161b22', accent: '#0b4ea2', tint: '#e9eff8', rule: '#b3bdcb',
  muted: '#59636f', paper: '#ffffff' };
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] = ['Noto Serif', 'Noto Sans'];
const [TRIM_W, TRIM_H, TOP, BOTTOM, INNER, OUTER, GUTTER] = [210, 297, 22, 22, 18, 16, 6];
const MEASURE = TRIM_W - INNER - OUTER;
const LEAD = 12.6; // pt: 9.3 pt type in two columns of 85 mm
const SMALL = 10.8; // pt: the Methods, at 8.2 pt

// #region answer: Methods in smaller type, with a second reference list that numbers on
// [@key] prints a raised number in order of first citation, so a work the Methods cite for
// the first time takes the next number after the main text's last, and one cited again
// keeps its number. Nature prints two lists, and :::bibliography{scope=new} lists only the
// works no earlier list printed: the same block after the main text and after the Methods
// sets references 1–35, then 36–55.
registerCitationEngine(createCiteprocEngine({ styles: STYLES, locales: LOCALES }));
const citations = {
  style: 'nature', // raised 1–4, "Nature 577, 706–710 (2020)." entries
  bibliography: { fontSize: em(0.8), lineHeight: pt(9.6), entrySpacing: pt(0.8),
    labelWidth: mm(5), labelAlign: 'right' }, // 9. and 10. end together
};
const references = '### References\n\n:::bibliography{title="" scope=new}';
// "# Methods {style=\"methods\"}" opens a section that runs to the end of the article: an H1
// that breaks nothing, names no running chapter and sets its body two sizes down.
const methods = { id: 'methods', numbered: false, runningChapter: false,
  breakBefore: { enabled: false }, fontFamily: SANS, fontSize: pt(11.5), lineHeight: pt(LEAD),
  fontWeight: 700, color: col('ink'), marginTop: pt(LEAD), marginBottom: pt(LEAD / 2),
  bodyStyle: { fontSize: pt(8.2), lineHeight: pt(SMALL) } };
// #endregion

// #region title: a blue band, the title, 34 authors in small type and the source line
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 = { fontFamily: SANS, fontWeight: 600, letterSpacing: pt(1.4),
  textTransform: 'uppercase' };
const titleBlock = { enabled: true,
  minHeight: mm(50),
  slot: { elements: [
    { kind: 'box', id: 'band', style: { backgroundColor: col('accent') },
      placement: { anchor: { to: 'page', edge: 'top-left' },
        size: { width: 'fill', height: mm(14) } } },
    text('kicker', '{attr.kicker}', SANS, 8.5, { ...caps, color: col('paper'),
      placement: at('page', 'top-left', INNER, 5.6, 120) }),
    text('title', '{titleText}', SANS, 23, { fontWeight: 700, lineHeight: 1.12,
      placement: at('container', 'top-left', 0, 1, MEASURE - 30) }),
    text('authors', '{attr.authors}', SANS, 7.4, { inlineMarks: true, lineHeight: 1.42,
      placement: at('#title', 'below', 0, 4, MEASURE) }),
    { kind: 'rule', id: 'rule', thickness: pt(0.5), color: col('rule'),
      placement: at('#authors', 'below', 0, 3, MEASURE) },
    text('source', '{attr.source}', SANS, 7, { fontStyle: 'italic', color: col('muted'),
      placement: at('#rule', 'below', 0, 1.6, MEASURE) }),
  ] },
};
// #endregion

const flat = { border: { enabled: false }, backgroundEnabled: false, marginTop: pt(0),
  padding: { top: mm(0), right: mm(0), bottom: mm(0), left: mm(0) } };
const calloutStyles = [
  { id: 'summary', span: 'page', ...flat, marginBottom: pt(LEAD), // the bold summary
    body: { fontFamily: SANS, fontSize: pt(9.4), lineHeight: pt(LEAD + 0.6), fontWeight: 600,
      boldColor: col('ink'), textAlign: 'justify', firstLineIndent: pt(0) } },
];

const head = (id, content, parity, edge, x, extra) => text(id, content, SANS, 7.8, {
  parity, pages: 'body', color: col('muted'), overflow: 'clip',
  placement: at('page', edge, x, 12.5, 110), ...extra });
const folio = { fontWeight: 700, color: col('accent') };
const right = { align: 'right' };
const header = { elements: [
  head('v-folio', '{pageNumber}', 'even', 'top-left', OUTER, folio),
  head('v-title', 'Jumper et al. · Abridged from Nature 596, 583–589 (2021)', 'even',
    'top-left', OUTER + 8),
  head('r-title', 'Highly accurate protein structure prediction with AlphaFold', 'odd',
    'top-right', -OUTER - 8, right),
  head('r-folio', '{pageNumber}', 'odd', 'top-right', -OUTER, { ...folio, ...right }),
] };
const footer = { elements: [text('licence', 'Open access · CC BY 4.0 · '
  + 'creativecommons.org/licenses/by/4.0', SANS, 7, { pages: 'opener',
  color: col('muted'), placement: at('page', 'bottom-left', INNER, -12, 150) })] };

const config = () => ({ // a factory: the engine caches resolved configs per object
  locale: 'en-gb',
  // Nature's labels: "Fig. 1 | Title." and "Table 1 | Title.", numbered 1, 2… not 1.1
  resourceTypes: defaultResourceTypes(LANG).map((type) => ({ ...type,
    numberingTemplate: '{n}', captionPrefix: type.id === 'figure' ? 'Fig.' : type.name })),
  colorPalette, citations, header, footer, calloutStyles,
  headingStyles: [
    { id: 'article', numbered: false, span: 'page', advancedDesign: titleBlock },
    methods,
  ],
  page: { sizePreset: 'custom', width: mm(TRIM_W), height: mm(TRIM_H), dpi: 150,
    margins: { top: mm(TOP), bottom: mm(BOTTOM), left: mm(INNER), right: mm(OUTER),
      mirror: true } },
  layout: { layoutType: 'double', gutterWidth: mm(GUTTER) },
  bodyText: { fontFamily: SERIF, fontSize: pt(9.3), 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 },
  headings: { fontFamily: SANS, color: col('ink'), fontWeight: 700, levels: [
    { level: 1, breakBefore: { enabled: true, parity: 'any' } }, // gotcha: headings-drop-h1-break
    { level: 2, fontSize: pt(10), lineHeight: pt(LEAD), marginTop: pt(LEAD),
      marginBottom: pt(0) },
    { level: 3, fontSize: pt(8.2), lineHeight: pt(SMALL), color: col('accent'),
      marginTop: pt(SMALL / 2), marginBottom: pt(0), snapToGrid: false }, // Methods' leading
  ] },
  paragraphStyles: [{ id: 'back', fontFamily: SANS, fontSize: pt(7.2), lineHeight: pt(9.6),
    textAlign: 'left', firstLineIndent: pt(0), spaceBetween: pt(3), marginTop: pt(6) }],
  tableStyle: { rules: 'horizontal', borderColor: col('rule'), borderWidth: pt(0.5),
    headerBackground: col('accent'), headerColor: col('paper'), headerBold: true,
    headerFontFamily: SANS, headerFontSize: pt(7.4), bodyFontFamily: SANS,
    bodyFontSize: pt(7.4), bodyColor: col('ink'), cellPadding: mm(1.2) },
  captionStyle: { fontFamily: SANS, fontSize: pt(7.6), color: col('ink'), labelBold: true,
    labelColor: col('accent'), labelSeparator: ' | ', gap: mm(2),
    note: { fontSize: pt(6.6), color: col('muted') } },
});

// ─── 2 · Content ────────────────────────────────────────────────────────────
const markdown = [String.raw`---
title: "Highly accurate protein structure prediction with AlphaFold"
author: "John Jumper, Richard Evans, Alexander Pritzel et al."
---

# Highly accurate protein structure prediction with AlphaFold {style="article" kicker="Article · Structural biology" source="Abridged from Nature 596, 583–589 (2021) · https://doi.org/10.1038/s41586-021-03819-2 · CC BY 4.0" authors="John Jumper^1,4^, Richard Evans^1,4^, Alexander Pritzel^1,4^, Tim Green^1,4^, Michael Figurnov^1,4^, Olaf Ronneberger^1,4^, Kathryn Tunyasuvunakool^1,4^, Russ Bates^1,4^, Augustin Žídek^1,4^, Anna Potapenko^1,4^, Alex Bridgland^1,4^, Clemens Meyer^1,4^, Simon A. A. Kohl^1,4^, Andrew J. Ballard^1,4^, Andrew Cowie^1,4^, Bernardino Romera-Paredes^1,4^, Stanislav Nikolov^1,4^, Rishub Jain^1,4^, Jonas Adler^1^, Trevor Back^1^, Stig Petersen^1^, David Reiman^1^, Ellen Clancy^1^, Michal Zielinski^1^, Martin Steinegger^2,3^, Michalina Pacholska^1^, Tamas Berghammer^1^, Sebastian Bodenstein^1^, David Silver^1^, Oriol Vinyals^1^, Andrew W. Senior^1^, Koray Kavukcuoglu^1^, Pushmeet Kohli^1^ & Demis Hassabis^1,4^"}

:::callout{type="summary"}
Proteins are essential to life, and understanding their structure can facilitate a mechanistic understanding of their function. Through an enormous experimental effort[@thompson2020; @bai2015; @jaskolski2014; @wuthrich2001], the structures of around 100,000 unique proteins have been determined[@wwpdb2018], but this represents a small fraction of the billions of known protein sequences[@mitchell2020; @steinegger2019]. Structural coverage is bottlenecked by the months to years of painstaking effort required to determine a single protein structure. Accurate computational approaches are needed to address this gap and to enable large-scale structural bioinformatics. Predicting the three-dimensional structure that a protein will adopt based solely on its amino acid sequence—the structure prediction component of the ‘protein folding problem’[@dill2008]—has been an important open research problem for more than 50 years[@anfinsen1973]. Despite recent progress[@senior2020; @wang2017; @zheng2019; @abriata2019; @pearce2021], existing methods fall far short of atomic accuracy, especially when no homologous structure is available. Here we provide the first computational method that can regularly predict protein structures with atomic accuracy even in cases in which no similar structure is known. We validated an entirely redesigned version of our neural network-based model, AlphaFold, in the challenging 14th Critical Assessment of protein Structure Prediction (CASP14)[@moult2020], demonstrating accuracy competitive with experimental structures in a majority of cases and greatly outperforming other methods. Underpinning the latest version of AlphaFold is a novel machine learning approach that incorporates physical and biological knowledge about protein structure, leveraging multi-sequence alignments, into the design of the deep learning algorithm.
:::

::resource{id="fig1"}

The development of computational methods to predict three-dimensional (3D) protein structures from the protein sequence has proceeded along two complementary paths that focus on either the physical interactions or the evolutionary history. The physical interaction programme heavily integrates our understanding of molecular driving forces into either thermodynamic or kinetic simulation of protein physics[@brini2020] or statistical approximations thereof[@sippl1990]. Although theoretically very appealing, this approach has proved highly challenging for even moderate-sized proteins due to the computational intractability of molecular simulation, the context dependence of protein stability and the difficulty of producing sufficiently accurate models of protein physics. The evolutionary programme has provided an alternative in recent years, in which the constraints on protein structure are derived from bioinformatics analysis of the evolutionary history of proteins, homology to solved structures[@sali1993; @roy2010] and pairwise evolutionary correlations[@altschuh1987; @shindyalov1994; @weigt2009; @marks2011; @jones2012]. This bioinformatics approach has benefited greatly from the steady growth of experimental protein structures deposited in the Protein Data Bank (PDB)[@wwpdb2018], the explosion of genomic sequencing and the rapid development of deep learning techniques to interpret these correlations. Despite these advances, contemporary physical and evolutionary-history-based approaches produce predictions that are far short of experimental accuracy in the majority of cases in which a close homologue has not been solved experimentally and this has limited their utility for many biological applications.

In this study, we develop the first, to our knowledge, computational approach capable of predicting protein structures to near experimental accuracy in a majority of cases. The neural network AlphaFold that we developed was entered into the CASP14 assessment (May–July 2020; entered under the team name ‘AlphaFold2’ and a completely different model from our CASP13 AlphaFold system[@senior2020]). The CASP assessment is carried out biennially using recently solved structures that have not been deposited in the PDB or publicly disclosed so that it is a blind test for the participating methods, and has long served as the gold-standard assessment for the accuracy of structure prediction[@moult1995; @kryshtafovych2019].

In CASP14, AlphaFold structures were vastly more accurate than competing methods. AlphaFold structures had a median backbone accuracy of 0.96 Å r.m.s.d.~95~ (Cα root-mean-square deviation at 95% residue coverage) (95% confidence interval = 0.85–1.16 Å) whereas the next best performing method had a median backbone accuracy of 2.8 Å r.m.s.d.~95~ (95% confidence interval = 2.7–4.0 Å) (measured on CASP domains; see :ref{id="fig1" text="Fig. 1a"} for backbone accuracy and Supplementary Fig. 14 for all-atom accuracy). As a comparison point for this accuracy, the width of a carbon atom is approximately 1.4 Å. In addition to very accurate domain structures (:ref{id="fig1" text="Fig. 1b"}), AlphaFold is able to produce highly accurate side chains when the backbone is highly accurate and considerably improves over template-based methods even when strong templates are available. The all-atom accuracy of AlphaFold was 1.5 Å r.m.s.d.~95~ (95% confidence interval = 1.2–1.6 Å) compared with the 3.5 Å r.m.s.d.~95~ (95% confidence interval = 3.1–4.2 Å) of the best alternative method. Finally, the model is able to provide precise, per-residue estimates of its reliability that should enable the confident use of these predictions.

## The AlphaFold network

The AlphaFold network directly predicts the 3D coordinates of all heavy atoms for a given protein using the primary amino acid sequence and aligned sequences of homologues as inputs (see Methods for details of inputs including databases, MSA construction and use of templates).

The network comprises two main stages. First, the trunk of the network processes the inputs through repeated layers of a novel neural network block that we term Evoformer to produce an *N*~seq~ × *N*~res~ array (*N*~seq~, number of sequences; *N*~res~, number of residues) that represents a processed MSA and an *N*~res~ × *N*~res~ array that represents residue pairs. The key innovations in the Evoformer block are new mechanisms to exchange information within the MSA and pair representations that enable direct reasoning about the spatial and evolutionary relationships.

The trunk of the network is followed by the structure module that introduces an explicit 3D structure in the form of a rotation and translation for each residue of the protein (global rigid body frames). Both within the structure module and throughout the whole network, we reinforce the notion of iterative refinement by repeatedly applying the final loss to outputs and then feeding the outputs recursively into the same modules. The iterative refinement using the whole network (which we term ‘recycling’ and is related to approaches in computer vision[@tu2010; @carreira2016]) contributes markedly to accuracy with minor extra training time (see Supplementary Methods 1.8 for details).

## Evoformer

The key principle of the building block of the network—named Evoformer—is to view the prediction of protein structures as a graph inference problem in 3D space in which the edges of the graph are defined by residues in proximity. The elements of the pair representation encode information about the relation between the residues. The columns of the MSA representation encode the individual residues of the input sequence while the rows represent the sequences in which those residues appear.

Within the pair representation, there are two different update patterns. Both are inspired by the necessity of consistency of the pair representation—for a pairwise description of amino acids to be representable as a single 3D structure, many constraints must be satisfied including the triangle inequality on distances. On the basis of this intuition, we arrange the update operations on the pair representation in terms of triangles of edges involving three different nodes. In particular, we add an extra logit bias to axial attention[@huang2019] to include the ‘missing edge’ of the triangle and we define a non-attention update operation ‘triangle multiplicative update’ that uses two edges to update the missing third edge (see Supplementary Methods 1.6.5 for details).

## End-to-end structure prediction

The structure module operates on a concrete 3D backbone structure using the pair representation and the original sequence row (single representation) of the MSA representation from the trunk. The 3D backbone structure is represented as *N*~res~ independent rotations and translations, each with respect to the global frame (residue gas). These rotations and translations—representing the geometry of the N-Cα-C atoms—prioritize the orientation of the protein backbone so that the location of the side chain of each residue is highly constrained within that frame. Conversely, the peptide bond geometry is completely unconstrained and the network is observed to frequently violate the chain constraint during the application of the structure module as breaking this constraint enables the local refinement of all parts of the chain without solving complex loop closure problems. Satisfaction of the peptide bond geometry is encouraged during fine-tuning by a violation loss term. Exact enforcement of peptide bond geometry is only achieved in the post-prediction relaxation of the structure by gradient descent in the Amber[@hornak2006] force field. Empirically, this final relaxation does not improve the accuracy of the model as measured by the global distance test (GDT)[@zemla2003] or lDDT-Cα[@mariani2013] but does remove distracting stereochemical violations without the loss of accuracy.

The residue gas representation is updated iteratively in two stages. First, a geometry-aware attention operation that we term ‘invariant point attention’ (IPA) is used to update an *N*~res~ set of neural activations (single representation) without changing the 3D positions, then an equivariant update operation is performed on the residue gas using the updated activations. The IPA augments each of the usual attention queries, keys and values with 3D points that are produced in the local frame of each residue such that the final value is invariant to global rotations and translations (see Methods ‘IPA’ for details).

Predictions of side-chain *χ* angles as well as the final, per-residue accuracy of the structure (pLDDT) are computed with small per-residue networks on the final activations at the end of the network. The estimate of the TM-score (pTM) is obtained from a pairwise error prediction that is computed as a linear projection from the final pair representation.

## Discussion

AlphaFold has already demonstrated its utility to the experimental community, both for molecular replacement[@pereira2021] and for interpreting cryogenic electron microscopy maps[@gupta2021]. Moreover, because AlphaFold outputs protein coordinates directly, AlphaFold produces predictions in graphics processing unit (GPU) minutes to GPU hours depending on the length of the protein sequence (for example, around one GPU minute per model for 384 residues; see Methods for details). This opens up the exciting possibility of predicting structures at the proteome-scale and beyond—in a companion paper[@tunyasuvunakool2021], we demonstrate the application of AlphaFold to the entire human proteome[@tunyasuvunakool2021].

The explosion in available genomic sequencing techniques and data has revolutionized bioinformatics but the intrinsic challenge of experimental structure determination has prevented a similar expansion in our structural knowledge. By developing an accurate protein structure prediction algorithm, coupled with existing large and well-curated structure and sequence databases assembled by the experimental community, we hope to accelerate the advancement of structural bioinformatics that can keep pace with the genomics revolution. We hope that AlphaFold—and computational approaches that apply its techniques for other biophysical problems—will become essential tools of modern biology.
`, references, String.raw`# Methods {style="methods"}

### IPA

The IPA module combines the pair representation, the single representation and the geometric representation to update the single representation (Supplementary Fig. 8). Each of these representations contributes affinities to the shared attention weights and then uses these weights to map its values to the output. The IPA operates in 3D space. Each residue produces query points, key points and value points in its local frame. These points are projected into the global frame using the backbone frame of the residue in which they interact with each other. The resulting points are then projected back into the local frame. The affinity computation in the 3D space uses squared distances and the coordinate transformations ensure the invariance of this module with respect to the global frame (see Supplementary Methods 1.8.2 ‘Invariant point attention (IPA)’ for the algorithm, proof of invariance and a description of the full multi-head version).

### Inputs and data sources

Inputs to the network are the primary sequence, sequences from evolutionarily related proteins in the form of a MSA created by standard tools including jackhmmer[@johnson2010] and HHBlits[@remmert2012], and 3D atom coordinates of a small number of homologous structures (templates) where available. For both the MSA and templates, the search processes are tuned for high recall; spurious matches will probably appear in the raw MSA but this matches the training condition of the network.

For MSA search on BFD + Uniclust30, and template search against PDB70, we used HHBlits[@remmert2012] and HHSearch[@steinegger2019b] from hh-suite v.3.0-beta.3 (version 14/07/2017). For MSA search on Uniref90 and clustered MGnify, we used jackhmmer from HMMER3[@eddy2011]. For constrained relaxation of structures, we used OpenMM v.7.3.1[@eastman2017] with the Amber99sb force field[@hornak2006]. For neural network construction, running and other analyses, we used TensorFlow[@ashish2015], Sonnet[@reynolds2017], NumPy[@harris2020], Python[@vanrossum2009] and Colab[@bisong2019].

### Inference regimen

Using our CASP14 configuration for AlphaFold, the trunk of the network is run multiple times with different random choices for the MSA cluster centres (see Supplementary Methods 1.11.2 for details of the ensembling procedure). The full time to make a structure prediction varies considerably depending on the length of the protein. Representative timings for the neural network using a single model on V100 GPU are 4.8 min with 256 residues, 9.2 min with 384 residues and 18 h at 2,500 residues. These timings are measured using our open-source code, and the open-source code is notably faster than the version we ran in CASP14 as we now use the XLA compiler[@xla2018].

Since CASP14, we have found that the accuracy of the network without ensembling is very close or equal to the accuracy with ensembling and we turn off ensembling for most inference. Without ensembling, the network is 8× faster and the representative timings for a single model are 0.6 min with 256 residues, 1.1 min with 384 residues and 2.1 h with 2,500 residues.

::resource{id="tbl-timings"}

### Metrics

The predicted structure is compared to the true structure from the PDB in terms of lDDT metric[@mariani2013], as this metric reports the domain accuracy without requiring a domain segmentation of chain structures. The distances are either computed between all heavy atoms (lDDT) or only the Cα atoms to measure the backbone accuracy (lDDT-Cα). As lDDT-Cα only focuses on the Cα atoms, it does not include the penalty for structural violations and clashes. Domain accuracies in CASP are reported as GDT[@zemla2003] and the TM-score[@zhang2004] is used as a full chain global superposition metric.

We also report accuracies using the r.m.s.d.~95~ (Cα r.m.s.d. at 95% coverage). We perform five iterations of (1) a least-squares alignment of the predicted structure and the PDB structure on the currently chosen Cα atoms (using all Cα atoms in the first iteration); (2) selecting the 95% of Cα atoms with the lowest alignment error. The r.m.s.d. of the atoms chosen for the final iterations is the r.m.s.d.~95~. This metric is more robust to apparent errors that can originate from crystal structure artefacts, although in some cases the removed 5% of residues will contain genuine modelling errors.

### Data availability

All input data are freely available from public sources. We show experimental structures from the PDB with accession numbers 6Y4F[@jiang2020], 6YJ1[@dunne2020], 6VR4[@drobysheva2021], 6SK0[@flaugnatti2020], 6FES[@elgamacy2018], 6W6W[@lim2020], 6T1Z[@debruycker2020] and 7JTL[@flower2021].

### Code availability

Source code for the AlphaFold model, trained weights and inference script are available under an open-source license at https://github.com/deepmind/alphafold.
`, references,
  String.raw`:::paragraphs{style="back"}
**Acknowledgements** We thank A. Rrustemi, A. Gu, A. Guseynov, B. Hechtman, C. Beattie, C. Jones, C. Donner, E. Parisotto, E. Elsen, F. Popovici, G. Necula, H. Maclean, J. Menick, J. Kirkpatrick, J. Molloy, J. Yim, J. Stanway, K. Simonyan, L. Sifre, L. Martens, M. Johnson, M. O’Neill, N. Antropova, R. Hadsell, S. Blackwell, S. Das, S. Hou, S. Gouws, S. Wheelwright, T. Hennigan, T. Ward, Z. Wu, Ž. Avsec and the Research Platform Team for their contributions; M. Mirdita for his help with the datasets; M. Piovesan-Forster, A. Nelson and R. Kemp for their help managing the project; the JAX, TensorFlow and XLA teams for detailed support and enabling machine learning models of the complexity of AlphaFold; our colleagues at DeepMind, Google and Alphabet for their encouragement and support; and J. Moult and the CASP14 organizers, and the experimentalists whose structures enabled the assessment. M.S. acknowledges support from the National Research Foundation of Korea grant (2019R1A6A1A10073437, 2020M3A9G7103933) and the Creative-Pioneering Researchers Program through Seoul National University.

**Author information** ^1^DeepMind, London, UK. ^2^School of Biological Sciences, Seoul National University, Seoul, South Korea. ^3^Artificial Intelligence Institute, Seoul National University, Seoul, South Korea. ^4^These authors contributed equally. Correspondence to John Jumper or Demis Hassabis.

**This edition** Abridged; Fig. 1 redrawn, Table 1 compiled from the Methods, Figs. 2–5 and the Supplementary information not reproduced. Set in Noto Serif and Noto Sans (SIL OFL).
:::
`, String.raw`:::references{format=bibtex}
@article{thompson2020, author = {Thompson, M. C. and Yeates, T. O. and Rodriguez, J. A.}, title = {Advances in methods for atomic resolution macromolecular structure determination}, journal = {F1000Res.}, year = {2020}, volume = {9}, pages = {667}}
@article{bai2015, author = {Bai, X.-C. and McMullan, G. and Scheres, S. H. W.}, title = {How cryo-EM is revolutionizing structural biology}, journal = {Trends Biochem. Sci.}, year = {2015}, volume = {40}, pages = {49--57}}
@article{jaskolski2014, author = {Jaskolski, M. and Dauter, Z. and Wlodawer, A.}, title = {A brief history of macromolecular crystallography, illustrated by a family tree and its Nobel fruits}, journal = {FEBS J.}, year = {2014}, volume = {281}, pages = {3985--4009}}
@article{wuthrich2001, author = {Wüthrich, K.}, title = {The way to NMR structures of proteins}, journal = {Nat. Struct. Biol.}, year = {2001}, volume = {8}, pages = {923--925}}
@article{wwpdb2018, author = {{wwPDB Consortium}}, title = {Protein Data Bank: the single global archive for 3D macromolecular structure data}, journal = {Nucleic Acids Res.}, year = {2018}, volume = {47}, pages = {D520--D528}}
@article{mitchell2020, author = {Mitchell, Alex L and others}, title = {MGnify: the microbiome analysis resource in 2020}, journal = {Nucleic Acids Res.}, year = {2020}, volume = {48}, pages = {D570--D578}}
@article{steinegger2019, author = {Steinegger, M. and Mirdita, M. and Söding, J.}, title = {Protein-level assembly increases protein sequence recovery from metagenomic samples manyfold}, journal = {Nat. Methods}, year = {2019}, volume = {16}, pages = {603--606}}
@article{dill2008, author = {Dill, K. A. and Ozkan, S. B. and Shell, M. S. and Weikl, T. R.}, title = {The protein folding problem}, journal = {Annu. Rev. Biophys.}, year = {2008}, volume = {37}, pages = {289--316}}
@article{anfinsen1973, author = {Anfinsen, C. B.}, title = {Principles that govern the folding of protein chains}, journal = {Science}, year = {1973}, volume = {181}, pages = {223--230}}
@article{senior2020, author = {Senior, Andrew W. and others}, title = {Improved protein structure prediction using potentials from deep learning}, journal = {Nature}, year = {2020}, volume = {577}, pages = {706--710}}
@article{wang2017, author = {Wang, S. and Sun, S. and Li, Z. and Zhang, R. and Xu, J.}, title = {Accurate de novo prediction of protein contact map by ultra-deep learning model}, journal = {PLOS Comput. Biol.}, year = {2017}, volume = {13}, pages = {e1005324}}
@article{zheng2019, author = {Zheng, Wei and others}, title = {Deep-learning contact-map guided protein structure prediction in CASP13}, journal = {Proteins}, year = {2019}, volume = {87}, pages = {1149--1164}}
@article{abriata2019, author = {Abriata, L. A. and Tamò, G. E. and Dal Peraro, M.}, title = {A further leap of improvement in tertiary structure prediction in CASP13 prompts new routes for future assessments}, journal = {Proteins}, year = {2019}, volume = {87}, pages = {1100--1112}}
@article{pearce2021, author = {Pearce, R. and Zhang, Y.}, title = {Deep learning techniques have significantly impacted protein structure prediction and protein design}, journal = {Curr. Opin. Struct. Biol.}, year = {2021}, volume = {68}, pages = {194--207}}
@online{moult2020, author = {Moult, J. and Fidelis, K. and Kryshtafovych, A. and Schwede, T. and Topf, M.}, title = {Critical assessment of techniques for protein structure prediction, fourteenth round}, booktitle = {CASP 14 Abstract Book}, year = {2020}, url = {https://www.predictioncenter.org/casp14/doc/CASP14_Abstracts.pdf}}
@article{brini2020, author = {Brini, E. and Simmerling, C. and Dill, K.}, title = {Protein storytelling through physics}, journal = {Science}, year = {2020}, volume = {370}, pages = {eaaz3041}}
@article{sippl1990, author = {Sippl, M. J.}, title = {Calculation of conformational ensembles from potentials of mean force. An approach to the knowledge-based prediction of local structures in globular proteins}, journal = {J. Mol. Biol.}, year = {1990}, volume = {213}, pages = {859--883}}
@article{sali1993, author = {Šali, A. and Blundell, T. L.}, title = {Comparative protein modelling by satisfaction of spatial restraints}, journal = {J. Mol. Biol.}, year = {1993}, volume = {234}, pages = {779--815}}
@article{roy2010, author = {Roy, A. and Kucukural, A. and Zhang, Y.}, title = {I-TASSER: a unified platform for automated protein structure and function prediction}, journal = {Nat. Protocols}, year = {2010}, volume = {5}, pages = {725--738}}
@article{altschuh1987, author = {Altschuh, D. and Lesk, A. M. and Bloomer, A. C. and Klug, A.}, title = {Correlation of co-ordinated amino acid substitutions with function in viruses related to tobacco mosaic virus}, journal = {J. Mol. Biol.}, year = {1987}, volume = {193}, pages = {693--707}}
@article{shindyalov1994, author = {Shindyalov, I. N. and Kolchanov, N. A. and Sander, C.}, title = {Can three-dimensional contacts in protein structures be predicted by analysis of correlated mutations?}, journal = {Protein Eng.}, year = {1994}, volume = {7}, pages = {349--358}}
@article{weigt2009, author = {Weigt, M. and White, R. A. and Szurmant, H. and Hoch, J. A. and Hwa, T.}, title = {Identification of direct residue contacts in protein–protein interaction by message passing}, journal = {Proc. Natl Acad. Sci. USA}, year = {2009}, volume = {106}, pages = {67--72}}
@article{marks2011, author = {Marks, Debora S. and others}, title = {Protein 3D structure computed from evolutionary sequence variation}, journal = {PLoS ONE}, year = {2011}, volume = {6}, pages = {e28766}}
@article{jones2012, author = {Jones, D. T. and Buchan, D. W. A. and Cozzetto, D. and Pontil, M.}, title = {PSICOV: precise structural contact prediction using sparse inverse covariance estimation on large multiple sequence alignments}, journal = {Bioinformatics}, year = {2012}, volume = {28}, pages = {184--190}}
@article{moult1995, author = {Moult, J. and Pedersen, J. T. and Judson, R. and Fidelis, K.}, title = {A large-scale experiment to assess protein structure prediction methods}, journal = {Proteins}, year = {1995}, volume = {23}, pages = {ii--iv}}
@article{kryshtafovych2019, author = {Kryshtafovych, A. and Schwede, T. and Topf, M. and Fidelis, K. and Moult, J.}, title = {Critical assessment of methods of protein structure prediction (CASP)-round XIII}, journal = {Proteins}, year = {2019}, volume = {87}, pages = {1011--1020}}
@article{tu2010, author = {Tu, Z. and Bai, X.}, title = {Auto-context and its application to high-level vision tasks and 3D brain image segmentation}, journal = {IEEE Trans. Pattern Anal. Mach. Intell.}, year = {2010}, volume = {32}, pages = {1744--1757}}
@inproceedings{carreira2016, author = {Carreira, J. and Agrawal, P. and Fragkiadaki, K. and Malik, J.}, title = {Human pose estimation with iterative error feedback}, booktitle = {Proc. IEEE Conference on Computer Vision and Pattern Recognition}, pages = {4733--4742}, year = {2016}}
@inproceedings{huang2019, author = {Huang, Zilong and others}, title = {CCNet: criss-cross attention for semantic segmentation}, booktitle = {Proc. IEEE/CVF International Conference on Computer Vision}, pages = {603--612}, year = {2019}}
@article{hornak2006, author = {Hornak, Viktor and others}, title = {Comparison of multiple Amber force fields and development of improved protein backbone parameters}, journal = {Proteins}, year = {2006}, volume = {65}, pages = {712--725}}
@article{zemla2003, author = {Zemla, A.}, title = {LGA: a method for finding 3D similarities in protein structures}, journal = {Nucleic Acids Res.}, year = {2003}, volume = {31}, pages = {3370--3374}}
@article{mariani2013, author = {Mariani, V. and Biasini, M. and Barbato, A. and Schwede, T.}, title = {lDDT: a local superposition-free score for comparing protein structures and models using distance difference tests}, journal = {Bioinformatics}, year = {2013}, volume = {29}, pages = {2722--2728}}
@article{pereira2021, author = {Pereira, Joana and others}, title = {High-accuracy protein structure prediction in CASP14}, journal = {Proteins}, year = {2021}, doi = {10.1002/prot.26171}}
@article{tunyasuvunakool2021, author = {Tunyasuvunakool, Kathryn and others}, title = {Highly accurate protein structure prediction for the human proteome}, journal = {Nature}, year = {2021}, doi = {10.1038/s41586-021-03828-1}}
:::
`, String.raw`:::references{format=bibtex}
@article{johnson2010, author = {Johnson, L. S. and Eddy, S. R. and Portugaly, E.}, title = {Hidden Markov model speed heuristic and iterative HMM search procedure}, journal = {BMC Bioinformatics}, year = {2010}, volume = {11}, pages = {431}}
@article{remmert2012, author = {Remmert, M. and Biegert, A. and Hauser, A. and Söding, J.}, title = {HHblits: lightning-fast iterative protein sequence searching by HMM-HMM alignment}, journal = {Nat. Methods}, year = {2012}, volume = {9}, pages = {173--175}}
@article{steinegger2019b, author = {Steinegger, Martin and others}, title = {HH-suite3 for fast remote homology detection and deep protein annotation}, journal = {BMC Bioinformatics}, year = {2019}, volume = {20}, pages = {473}}
@article{eddy2011, author = {Eddy, S. R.}, title = {Accelerated profile HMM searches}, journal = {PLOS Comput. Biol.}, year = {2011}, volume = {7}, pages = {e1002195}}
@article{eastman2017, author = {Eastman, Peter and others}, title = {OpenMM 7: rapid development of high performance algorithms for molecular dynamics}, journal = {PLOS Comput. Biol.}, year = {2017}, volume = {13}, pages = {e1005659}}
@article{harris2020, author = {Harris, Charles R. and others}, title = {Array programming with NumPy}, journal = {Nature}, year = {2020}, volume = {585}, pages = {357--362}}
@book{vanrossum2009, author = {Van Rossum, G. and Drake, F. L.}, title = {Python 3 Reference Manual}, publisher = {CreateSpace}, year = {2009}}
@incollection{bisong2019, author = {Bisong, E.}, booktitle = {Building Machine Learning and Deep Learning Models on Google Cloud Platform: A Comprehensive Guide for Beginners}, pages = {59--64}, publisher = {Apress}, year = {2019}}
@online{xla2018, author = {{TensorFlow}}, title = {XLA: Optimizing Compiler for TensorFlow}, year = {2018}, url = {https://www.tensorflow.org/xla}}
@article{zhang2004, author = {Zhang, Y. and Skolnick, J.}, title = {Scoring function for automated assessment of protein structure template quality}, journal = {Proteins}, year = {2004}, volume = {57}, pages = {702--710}}
@article{jiang2020, author = {Jiang, Wangshu and others}, title = {MrpH, a new class of metal-binding adhesin, requires zinc to mediate biofilm formation}, journal = {PLoS Pathog.}, year = {2020}, volume = {16}, pages = {e1008707}}
@dataset{dunne2020, author = {Dunne, M. and Ernst, P. and Sobieraj, A. and Pluckthun, A. and Loessner, M. J.}, title = {The M23 peptidase domain of the Staphylococcal phage 2638A endolysin}, publisher = {PDB}, year = {2020}, doi = {10.2210/pdb6YJ1/pdb}}
@article{drobysheva2021, author = {Drobysheva, Arina V. and others}, title = {Structure and function of virion RNA polymerase of a crAss-like phage}, journal = {Nature}, year = {2021}, volume = {589}, pages = {306--309}}
@article{flaugnatti2020, author = {Flaugnatti, Nicolas and others}, title = {Structural basis for loading and inhibition of a bacterial T6SS phospholipase effector by the VgrG spike}, journal = {EMBO J.}, year = {2020}, volume = {39}, pages = {e104129}}
@article{elgamacy2018, author = {ElGamacy, Mohammad and others}, title = {An interface-driven design strategy yields a novel, corrugated protein architecture}, journal = {ACS Synth. Biol.}, year = {2018}, volume = {7}, pages = {2226--2235}}
@article{lim2020, author = {Lim, Ci Ji and others}, title = {The structure of human CST reveals a decameric assembly bound to telomeric DNA}, journal = {Science}, year = {2020}, volume = {368}, pages = {1081--1085}}
@article{debruycker2020, author = {Debruycker, Vincent and others}, title = {An embedded lipid in the multidrug transporter LmrP suggests a mechanism for polyspecificity}, journal = {Nat. Struct. Mol. Biol.}, year = {2020}, volume = {27}, pages = {829--835}}
@article{flower2021, author = {Flower, Thomas G. and others}, title = {Structure of SARS-CoV-2 ORF8, a rapidly evolving immune evasion protein}, journal = {Proc. Natl Acad. Sci. USA}, year = {2021}, volume = {118}, pages = {e2021785118}}
:::

:::references{format=csl-yaml}
- id: gupta2021
  type: article
  author: ["Gupta, Meghna", "others"]
  title: "CryoEM and AI reveal a structure of SARS-CoV-2 Nsp2, a multifunctional protein involved in key host processes"
  DOI: "10.1101/2021.05.10.443524"
  issued: 2021
- id: ashish2015
  type: article
  author: ["Ashish, A. M. A.", "others"]
  title: "TensorFlow: large-scale machine learning on heterogeneous systems"
  URL: "https://arxiv.org/abs/1603.04467"
  issued: 2015
- id: reynolds2017
  type: webpage
  author: ["Reynolds, M.", "others"]
  title: "Open sourcing Sonnet – a new library for constructing neural networks"
  container-title: DeepMind
  URL: "https://deepmind.com/blog/open-sourcing-sonnet/"
  issued: 2017-04-07
:::
`].join('\n\n');

// #region resources: Fig. 1 across the page under the summary, the timings table in Methods
const resources = () => [
  { id: 'fig1', typeId: 'figure', kind: 'svg', createdAt: 0, updatedAt: 0,
    placement: { position: 'here', span: 'page' },
    svg: { fileId: 'fig1.svg', width: 1780, height: 640 },
    caption: '**AlphaFold produces highly accurate structures.** **a**, Median accuracy on the '
      + 'CASP14 domains of AlphaFold and of the next best method, as r.m.s.d.~95~ over Cα atoms '
      + '(backbone) and over all atoms, with 95% confidence intervals; the dashed line is the '
      + 'width of a carbon atom, about 1.4 Å. **b**, CASP14 target T1049 (PDB 6Y4F): the '
      + 'experimental Cα trace (grey) and the AlphaFold model of the same chain, coloured by '
      + 'its per-residue confidence (pLDDT).',
    note: 'Redrawn for this edition. a, from the values given in the text (the original plots '
      + 'the top 15 of 146 entries). b, from PDB 6Y4F (CC0) and AlphaFold DB model '
      + 'AF-B4EUK6-F1, AlphaFold Monomer v2.0 (CC BY 4.0), superposed on 134 Cα atoms; the '
      + 'original shows the CASP14 prediction.',
    altText: 'Bars: AlphaFold 0.96 and 1.5 Å, next best 2.8 and 3.5 Å. Two backbones that '
      + 'lie almost on top of each other.' },
  { id: 'tbl-timings', typeId: 'table', kind: 'table', createdAt: 0, updatedAt: 0,
    placement: { position: 'here' },
    caption: '**Inference times.** One model on one V100 GPU, by protein length.',
    note: 'Compiled for this edition from the timings given in ‘Inference regimen’.',
    table: { model: { headerRowCount: 1, columnWidths: [1, 1.3, 1.3], rows: [
      ['Residues', 'With ensembling (CASP14)', 'Without ensembling'],
      ['256', '4.8 min', '0.6 min'], ['384', '9.2 min', '1.1 min'],
      ['2,500', '18 h', '2.1 h'],
    ].map((row, r) => row.map((content, c) => ({ content, ...(r === 0 && { isHeader: true }),
      ...(c > 0 && { align: 'right' }) }))) } } },
];
// #endregion

// #region art: Fig. 1 drawn from the paper's numbers and from open structure coordinates
// PDB 6Y4F chain A, residues 25–158: Cα x, y in tenths of Å, depth z in Å (CC0)
const EXP = [
  -56,-91,-6,-36,-77,-3,-4,-98,-2,26,-81,-1,63,-86,-1,95,-73,1,116,-100,2,146,
  -79,3,159,-46,2,187,-34,4,209,-6,3,211,10,0,187,17,-3,182,53,-2,185,46,2,
  167,14,3,148,-4,6,122,-28,4,109,-58,6,76,-67,4,45,-89,4,11,-79,3,-2,-114,
  3,-37,-116,1,-64,-95,0,-87,-117,-2,-124,-110,-2,-135,-80,-5,-161,-95,-7,-158,
  -74,-10,-173,-41,-9,-141,-20,-9,-148,15,-8,-128,31,-5,-105,55,-7,-106,82,-4,
  -144,80,-3,-156,116,-3,-122,132,-2,-126,155,1,-102,131,3,-88,98,2,-68,72,4,
  -58,37,3,-23,33,4,6,10,5,32,28,2,59,21,0,68,31,-4,89,62,-3,96,70,-7,102,
  52,-10,65,59,-11,53,41,-8,39,71,-6,38,70,-2,64,79,0,59,86,4,33,60,5,42,
  36,8,7,42,9,-29,48,8,-55,21,9,-88,39,8,-105,69,6,-122,57,3,-134,87,1,-170,
  78,2,-167,42,1,-180,38,-3,-165,5,-4,-132,-13,-3,-150,-42,-2,-161,-18,1,-125,
  -6,1,-114,-43,2,-141,-49,4,-129,-20,6,-92,-30,6,-98,-66,7,-120,-57,10,-94,
  -33,11,-62,-52,11,-54,-89,11,-36,-104,8,-8,-109,7,3,-73,8,40,-75,9,67,-51,
  10,93,-26,9,92,8,7,119,30,5,128,47,2,151,77,2,149,93,-2,152,82,-5,173,52,
  -6,150,22,-6,162,-13,-8,144,-44,-6,119,-43,-9,109,-8,-8,106,-15,-4,72,-24,
  -3,53,-20,0,15,-23,0,-19,-8,1,-20,28,-1,-48,53,-1,-38,89,0,-52,122,1,-25,
  128,3,-19,107,7,18,115,7,47,129,5,57,121,1,26,112,-1,28,122,-4,-9,117,-5,
  -8,83,-3,-9,51,-5,-13,14,-5,-49,3,-4,-61,-14,-8,-50,-50,-8,-19,-46,-6,8,
  -72,-7,43,-65,-6,67,-94,-5,43,-121,-6,32,-148,-3,-6,-157,-3,-17,-192,-4,-23,
  -216,-1,
];
// AlphaFold DB AF-B4EUK6-F1, the same residues superposed on 6Y4F (CC BY 4.0)
const MODEL = [
  -57,-93,-6,-37,-77,-3,-5,-98,-2,25,-82,-1,63,-87,-1,94,-74,1,112,-103,3,144,
  -84,3,158,-50,2,189,-34,4,209,-2,3,212,16,0,187,21,-3,181,57,-2,181,48,2,
  166,13,2,148,-4,5,122,-29,4,110,-58,6,76,-66,4,46,-88,4,12,-79,3,0,-115,3,
  -37,-113,2,-64,-95,0,-87,-118,-2,-125,-110,-2,-136,-81,-4,-162,-95,-7,-155,
  -74,-10,-173,-42,-9,-141,-22,-9,-146,14,-8,-126,31,-5,-102,55,-7,-106,83,-4,
  -143,81,-4,-156,117,-3,-122,132,-2,-123,157,1,-101,132,3,-86,98,2,-68,72,4,
  -58,36,3,-22,32,4,7,8,4,32,26,2,61,20,0,67,31,-4,89,62,-3,95,73,-7,100,
  56,-10,64,67,-11,52,44,-8,40,74,-6,38,68,-2,65,77,0,59,84,4,35,56,5,45,
  30,8,10,37,9,-25,47,8,-54,21,9,-86,40,8,-105,69,6,-120,56,3,-132,87,1,
  -169,78,2,-166,42,0,-177,37,-3,-163,3,-4,-130,-15,-3,-150,-42,-2,-162,-17,1,
  -125,-5,1,-114,-42,2,-142,-47,5,-129,-15,7,-93,-28,6,-103,-62,8,-122,-46,11,
  -90,-26,12,-62,-52,11,-59,-89,10,-37,-104,7,-8,-110,7,3,-74,8,40,-74,9,66,
  -48,10,95,-26,9,95,7,7,124,30,6,127,45,2,147,78,2,145,92,-1,153,87,-5,172,
  54,-6,147,26,-7,158,-6,-8,145,-39,-7,117,-42,-10,105,-7,-8,106,-14,-5,72,
  -24,-3,54,-21,0,16,-25,0,-18,-8,0,-19,27,-1,-48,52,-1,-37,88,0,-51,122,1,
  -26,127,4,-23,104,7,15,108,7,45,124,5,57,120,2,26,111,-1,31,123,-4,-7,116,
  -5,-5,83,-3,-8,51,-5,-12,14,-5,-48,2,-5,-62,-14,-8,-53,-52,-8,-20,-47,-6,5,
  -74,-7,39,-66,-5,64,-95,-5,99,-89,-7,117,-72,-4,146,-94,-2,178,-75,-3,190,
  -46,-1,
];
// The model's per-residue pLDDT, read from its B-factor column
const PLDDT = [
  87,89,87,87,83,87,82,84,85,84,82,79,81,86,90,89,93,93,94,95,92,95,87,90,
  93,87,89,88,82,75,87,92,97,98,98,98,98,98,98,98,98,99,99,99,99,99,98,96,
  94,93,90,84,87,89,95,95,96,97,98,98,98,98,98,98,99,99,99,98,98,98,98,97,
  97,98,98,98,98,98,98,97,98,98,96,95,89,96,97,96,96,96,96,94,93,91,88,86,
  82,73,67,61,61,69,83,89,96,97,98,99,99,98,98,97,96,97,96,96,95,95,96,97,
  97,96,97,94,87,90,84,82,69,65,64,61,55,53,
];
const n2 = (v) => +v.toFixed(2);
// An SVG drawn as an image cannot see the page's web fonts (gotcha: svg-no-webfonts), so the
// figure carries Noto Sans inline, as a data URL of the Fontsource file.
async function inlineFace(family, weight) {
  const id = family.toLowerCase().replace(/\s+/g, '-');
  const url = `https://cdn.jsdelivr.net/npm/@fontsource/${id}@5/files/${id}-latin-${weight}`
    + '-normal.woff2';
  const bytes = new Uint8Array(await (await fetch(url)).arrayBuffer());
  let bin = '';
  for (const b of bytes) bin += String.fromCharCode(b);
  return `<style>@font-face{font-family:F;src:url(data:font/woff2;base64,${btoa(bin)}) `
    + `format('woff2')}text{font-family:F}</style>`;
}
const label = (x, y, s, size = 2.5, extra = '') => { // ink unless extra sets a fill
  const fill = extra.includes('fill') ? '' : `fill="${palette.ink}" `;
  return `<text x="${n2(x)}" y="${n2(y)}" font-size="${size}" ${fill}${extra}>${s}</text>`;
};
// a: median r.m.s.d.95 with its 95% interval, as the text gives them
const ACCURACY = [
  { group: 'Backbone', af: [0.96, 0.85, 1.16], next: [2.8, 2.7, 4.0] },
  { group: 'All-atom', af: [1.5, 1.2, 1.6], next: [3.5, 3.1, 4.2] },
];
function bars() {
  const [L, B, T, H] = [9, 54, 8, 4.5]; // axis left, baseline, top, Å at the top
  const y = (v) => B - (v / H) * (B - T);
  let s = label(0, 3.2, 'a', 3.6, 'font-weight="700"');
  for (const v of [0, 1, 2, 3, 4]) {
    s += `<path d="M${L} ${n2(y(v))}H58" stroke="${palette.rule}" stroke-width="0.15"/>`
      + label(L - 1.5, y(v) + 0.9, v, 2.5, 'text-anchor="end"');
  }
  s += label(1.6, 31, 'r.m.s.d.<tspan font-size="1.8" dy="0.6">95</tspan><tspan dy="-0.6"> (Å)'
    + '</tspan>', 2.5, 'text-anchor="middle" transform="rotate(-90 1.6 31)"');
  ACCURACY.forEach(({ group, af, next }, g) => {
    const x0 = L + 4 + g * 24;
    [[af, palette.accent], [next, palette.rule]].forEach(([[m, lo, hi], fill], k) => {
      const x = x0 + k * 10;
      s += `<rect x="${x}" y="${n2(y(m))}" width="8" height="${n2(B - y(m))}" fill="${fill}"/>`
        + `<path d="M${x + 4} ${n2(y(lo))}V${n2(y(hi))}M${x + 2.6} ${n2(y(lo))}h2.8M${x + 2.6} `
        + `${n2(y(hi))}h2.8" stroke="${palette.ink}" stroke-width="0.3" fill="none"/>`
        + label(x + 4, B - 1.4, m.toFixed(m < 1 ? 2 : 1), 2.3, // inside the bar's foot
          `text-anchor="middle" fill="${k ? palette.ink : palette.paper}"`);
    });
    s += label(x0 + 9, B + 4, group, 2.6, 'text-anchor="middle"');
  });
  let dash = '';
  for (let x = L; x < 57; x += 2.4) dash += `M${n2(x)} ${n2(y(1.4))}h1.4`;
  s += `<path d="${dash}" stroke="${palette.ink}" stroke-width="0.3"/>`
    + `<path d="M${L} ${B}H58" stroke="${palette.ink}" stroke-width="0.35"/>`
    + `<rect x="${L + 2}" y="59.3" width="2.6" height="2.6" fill="${palette.accent}"/>`
    + label(L + 5.5, 61.6, 'AlphaFold') + `<rect x="${L + 22}" y="59.3" width="2.6" `
    + `height="2.6" fill="${palette.rule}"/>` + label(L + 25.5, 61.6, 'Next best method');
  return s;
}
// b: both chains as smooth Cα traces, far segments first
const BANDS = [[90, '#0053d6', 'pLDDT > 90'], [70, '#65cbf3', 'pLDDT 70–90'],
  [50, '#ffdb13', 'pLDDT 50–70'], [0, '#ff7d45', 'pLDDT < 50']];
const band = (p) => BANDS.find(([min]) => p > min);
function trace(xyz, cx, cy, k) {
  const pt = (i) => [cx + xyz[3 * i] * k, cy - xyz[3 * i + 1] * k, xyz[3 * i + 2]];
  const n = xyz.length / 3;
  return Array.from({ length: n - 1 }, (_, i) => {
    const [p0, p1, p2, p3] = [pt(Math.max(i - 1, 0)), pt(i), pt(i + 1), pt(Math.min(i + 2, n - 1))];
    const c1 = [0, 1].map((a) => p1[a] + (p2[a] - p0[a]) / 6);
    const c2 = [0, 1].map((a) => p2[a] - (p3[a] - p1[a]) / 6);
    return { i, z: (p1[2] + p2[2]) / 2, d: `M${n2(p1[0])} ${n2(p1[1])}C${n2(c1[0])} `
      + `${n2(c1[1])} ${n2(c2[0])} ${n2(c2[1])} ${n2(p2[0])} ${n2(p2[1])}` };
  });
}
function backbone() {
  const [cx, cy, k] = [103, 32, 0.142]; // mm per tenth of an Å
  const exp = trace(EXP, cx, cy, k);
  const model = trace(MODEL, cx, cy, k);
  const width = (z) => 1 + z / 40;
  let s = label(66, 3.2, 'b', 3.6, 'font-weight="700"');
  // the experiment as a grey band, then the model on top of it: where they agree, the model's
  // colour runs inside the band
  for (const [segs, halo, wide, colour] of [[exp, 2.4, 1.6, () => palette.rule],
    [model, 1.3, 0.75, (i) => band(Math.min(PLDDT[i], PLDDT[i + 1]))[1]]]) {
    for (const { i, z, d } of [...segs].sort((a, b) => a.z - b.z)) {
      const w = width(z);
      s += `<path d="${d}" stroke="${palette.paper}" stroke-width="${n2(w * halo)}" `
        + `fill="none"/><path d="${d}" stroke="${colour(i)}" ` // a butt halo: no notch at joins
        + `stroke-width="${n2(w * wide)}" fill="none" stroke-linecap="round"/>`;
    }
  }
  const legend = [[palette.rule, 'Experiment (PDB 6Y4F)'], ...BANDS.slice(0, 3)
    .map(([, c, t]) => [c, `Model: ${t}`])];
  legend.forEach(([c, t], j) => {
    s += `<path d="M138 ${44 + j * 4.6}h5" stroke="${c}" stroke-width="${j ? 1 : 1.6}" `
      + 'stroke-linecap="round"/>' + label(145, 44.9 + j * 4.6, t, 2.4);
  });
  return s + label(138, 39, 'T1049 · residues 25–158', 2.5, 'font-weight="700"');
}
const figure1 = (face) => '<svg xmlns="http://www.w3.org/2000/svg" width="1780" height="640" '
  + `viewBox="0 0 178 64">${face}${bars()}${backbone()}</svg>`;
// #endregion

// ─── 3 · Fonts ──────────────────────────────────────────────────────────────
const FONTS = { 'Noto Serif': ['400', '400i', '700', '700i'],
  'Noto Sans': ['400', '400i', '600', '700'] };

// #region subsets: Cα and χ come from each face's greek file, which the kit does not load
// Fontsource cuts each face by script. The PDF gets every file the text of a face needs and
// draws a character from the first file that has it (gotcha: latin-subset).
const SUBSETS = [['latin-ext', /[\u0100-\u024f]/u], ['greek', /[\u0370-\u03ff]/u]];
const fileOf = (family, subset, weight, style) => 'https://cdn.jsdelivr.net/npm/@fontsource/'
  + `${fontsourceId(family)}@5/files/${fontsourceId(family)}-${subset}-${weight}-${style}.woff2`;
const woff = async (url) => decompressWoff2(new Uint8Array(await (await fetch(url)).arrayBuffer()));
const loadGreek = () => Promise.all(Object.entries(FONTS).flatMap(([family, specs]) =>
  specs.map(async (spec) => {
    const [weight, style] = [parseInt(spec, 10), spec.endsWith('i') ? 'italic' : 'normal'];
    const url = fileOf(family, 'greek', weight, style);
    const face = new FontFace(family, `url(${url})`, { weight: `${weight}`, style,
      unicodeRange: 'U+0370-03FF' });
    document.fonts.add(await face.load());
  })));
async function fontProvider(family, weight, style, request) {
  const text = String.fromCodePoint(...(request?.codePoints ?? []));
  const more = SUBSETS.filter(([, test]) => test.test(text))
    .map(([subset]) => woff(fileOf(family, subset, weight, style)));
  return [await fontsourceProvider(family, weight, style), ...(await Promise.all(more))];
}
// #endregion

// ─── 4 · Build & show ───────────────────────────────────────────────────────
await Promise.all([loadFonts(FONTS, markdown), loadGreek()]); // Žídek: latin-ext comes too
await loadSvg('fig1.svg', figure1(await inlineFace(SANS, 400)));
const doc = await buildWithFonts(() => buildDocument({ markdown, resources: resources() },
  config()), markdown);
showPages(doc, { title: 'A protein paper with a Methods section' });
offerPdf(() => renderToPdf(doc, { fontProvider, resourceBytes: imageBytes }), `${RECIPE}.pdf`);

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

/** Registers SVG markup (drawn in code, or fetched) as a vector image. */
async function loadSvg(fileId, svg) {
  const img = new Image();
  img.src = `data:image/svg+xml;charset=utf-8,${encodeURIComponent(svg)}`;
  await img.decode();
  registerResourceImage(fileId, img);
  (loadImage.bytes ??= new Map()).set(fileId, new TextEncoder().encode(svg));
}

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

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

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

## アレンジ

### Methodsを本文と同じ大きさにする

Methodsを本文と同じ文字で組む学術誌もあります。セクションの本文スタイルを外し、見出しはそのまま残します。

```diff
-  bodyStyle: { fontSize: pt(8.2), lineHeight: pt(SMALL) } };
+};
```

### 番号を角括弧で示す

同じ分野でも[1]を行内に組む学術誌があります。

```diff
-  style: 'nature', // raised 1–4, "Nature 577, 706–710 (2020)." entries
+  style: 'nature', marker: 'brackets',
```

### リストを最後に一つだけ置く

*Science*のようにすべての参考文献をMethodsの後に載せる学術誌なら、二つ目のブロックだけを残します。

```diff
-const markdown = [/* @content */ '', references, /* @content:methods */ '', references,
+const markdown = [/* @content */ '', /* @content:methods */ '', references,
```

### バンクーバー方式のリストにする

医学誌なら、同梱のNLMスタイルから始めます。[バンクーバー方式の医学論文](https://postext.dev/ja/cookbook/medical-article-vancouver.md)がその例です。

## よくあるつまずき

- **headingsオブジェクトを渡すとH1の改ページが消える.** 既定ではH1は奇数ページへ改ページします（always-odd）。ところがheadingsオブジェクトを渡すと中身にかかわらずこの既定がリセットされ、章は改ページせずに続けて組まれ、span: 'page'も効かなくなります。どの設定でもheadings.levels[0].breakBefore: { enabled: true, parity }を書き直してください。
- **Fontsourceのlatinファイルにはラテン文字以外のグリフがない.** PDFプロバイダーが埋め込むのはFontsourceのlatinファイルです。スペイン語や西欧語のテキストは収まりますが、→、≈、✓、★、ギリシャ文字、中欧語の文字は含まれず、PDFではそれらのグリフが欠けます。PDFのテキストはlatinの範囲に収めてください。
- **SVGの<img>内のテキストはWebフォントを使えない.** SVGは画像として描かれ、画像はページのWebフォントにアクセスできないため、ラベルはシステムの書体にフォールバックします。テキストをアウトライン化するか、SVGに@font-faceのサブセットを埋め込むか、ラベルをキャプションに移してください。
- **レイアウトの前にすべてのフォントを読み込む.** レイアウトはブラウザーが読み込んだフォントで文字を計測し、その幅をキャッシュします。最初のビルドのあとに届いたフォントがあると改行位置が狂い、PDFも画面と一致しなくなります。すべてのウェイトとスタイルを先に読み込み、遅れて届いたときは再ビルドの前にclearMeasurementCache()を呼んでください。
- **設定はオブジェクトの同一性でキャッシュされる。毎回新しいオブジェクトを作る.** エンジンは解決済みの設定をオブジェクトの同一性でキャッシュします。そのため、設定をその場で書き換えて再ビルドすると前の結果が再利用されます。ビルドのたびに新しいオブジェクトを作ってください。レシピの設定がファクトリー関数config()になっているのはこのためです。

- Natureは著者を5名まで載せ、それより多いときは第一著者の後に*et al.*を付けます。元の参考文献リストがそうしている項目は、BibTeXに`and others`と書けば（`{Senior, Andrew W. and others}`）スタイルが*Senior, A. W. et al.*と印字します。CSL-YAMLでは、リストの最後を`"others"`にします。
- 見出しスタイルの`bodyStyle`は本文の文字の大きさと行送りを変えますが、ベースライングリッドは変えません。Methodsの小見出しは`snapToGrid: false`にしてあります。そうしないと、各小見出しの下の行が本文のグリッドの次の行へ飛んでしまいます。

## クレジット

- レシピ: Ignacio Ferro ([@drnachio](https://github.com/drnachio))
- テキスト: “Highly accurate protein structure prediction with AlphaFold”, Nature 596, 583–589 (2021), doi:10.1038/s41586-021-03819-2, PMC8371605, abridged (the sections ‘Interpreting the neural network’ and ‘Related work’, the description of Fig. 2, parts of other sections and of the Methods, the Supplementary information and the peer review note cut; pointers to omitted figures removed; references renumbered by first citation); Fig. 1 redrawn in code (panels c–e omitted), Figs. 2–5 omitted; Table 1 compiled from the Methods text: John Jumper, Richard Evans, Alexander Pritzel, Tim Green, Michael Figurnov, Olaf Ronneberger, Kathryn Tunyasuvunakool, Russ Bates, Augustin Žídek, Anna Potapenko, Alex Bridgland, Clemens Meyer, Simon A. A. Kohl, Andrew J. Ballard, Andrew Cowie, Bernardino Romera-Paredes, Stanislav Nikolov, Rishub Jain, Jonas Adler, Trevor Back, Stig Petersen, David Reiman, Ellen Clancy, Michal Zielinski, Martin Steinegger, Michalina Pacholska, Tamas Berghammer, Sebastian Bodenstein, David Silver, Oriol Vinyals, Andrew W. Senior, Koray Kavukcuoglu, Pushmeet Kohli, Demis Hassabis ([出典](https://doi.org/10.1038/s41586-021-03819-2)), CC-BY-4.0
- 画像: Fig. 1b, experimental Cα coordinates of PDB 6Y4F (Jiang et al. 2020), drawn in code: Worldwide Protein Data Bank ([出典](https://www.rcsb.org/structure/6Y4F)), CC0-1.0
- 画像: Fig. 1b, AlphaFold DB model AF-B4EUK6-F1 (AlphaFold Monomer v2.0) and its pLDDT, superposed and drawn in code: DeepMind Technologies Limited and EMBL-EBI, AlphaFold Protein Structure Database ([出典](https://alphafold.ebi.ac.uk/entry/B4EUK6)), CC-BY-4.0
- 書体: Noto Serif (OFL-1.1), Noto Sans (OFL-1.1)
- コード: MIT · サンプルの内容: CC-BY-4.0

## 関連レシピ

- [No. 095 · バンクーバー方式の医学論文](https://postext.dev/ja/cookbook/medical-article-vancouver.md): A4判2段組みの臨床試験報告。上付きの引用番号、番号付きのバンクーバー方式の文献一覧、PDFでリンクとして開くDOIを備えます。 · 難易度 2 (中級) · 論文・学術書
- [No. 140 · データ図をページ上部に渡した物理学レター](https://postext.dev/ja/cookbook/physics-letter-two-columns.md): GW150914発見のレターを2段組みで組み直します。1ページ目にオープンデータから描き直した信号、[1]形式の番号付き引用、番号付きの数式と表I。 · 難易度 3 (上級) · 論文・学術書
- [No. 096 · MLA形式の引用文献一覧を付けた人文学の論文](https://postext.dev/ja/cookbook/mla-humanities-essay.md): 文学論文の[@key, 48]形式の引用を、MLA第9版の著者名とページによる参照に変えます。ブロック引用と、CSL-YAMLのデータから作るWorks Citedの一覧も入ります。 · 難易度 2 (中級) · 論文・学術書
