diff --git a/book.toml b/book.toml
index 6087acc..b611417 100644
--- a/book.toml
+++ b/book.toml
@@ -15,6 +15,10 @@ after = ["links", "graphviz"]
command = "mdbook-katex"
after = ["links"]
+[preprocessor.figures]
+command = "python3 contrib/figures.py"
+after = ["links", "graphviz", "mermaid", "katex"]
+
[output.html]
additional-css = ["theme/diagrams.css"]
git-repository-url = "https://github.com/fungi-protocol/docs"
diff --git a/collaborative_txn_privacy.md b/collaborative_txn_privacy.md
index ace8c1c..5484c20 100644
--- a/collaborative_txn_privacy.md
+++ b/collaborative_txn_privacy.md
@@ -596,6 +596,14 @@ For example, a chain analysis vendor with KYC information may already be able to
If the CoinJoin graph is *robustly connected*[^flow], then no small cut separates any output from the mass of its candidate origin coins. Stated differently, every output is connected to its candidate origins by multiple disjoint paths.
This redundancy forestalls the cliff of exponential decay, where privacy becomes much more brittle, extending the duration of the linear decay regime by requiring the adversary to deanonymize a much larger proportion of users before divide-and-conquer tactics start coming into play.
+
+
+
+
+
+
+
+*The same ten peers, deanonymized in the same order, in both graphs.*
But when two post-CoinJoin outputs are linked, on-chain or otherwise, this makes **intersection attacks**[^goldfeder][^scroll-intersection] possible. In such attacks, we take the candidate origins of these now-linked coins and check for any overlaps.
diff --git a/contrib/figures.py b/contrib/figures.py
new file mode 100644
index 0000000..7551bcf
--- /dev/null
+++ b/contrib/figures.py
@@ -0,0 +1,74 @@
+#!/usr/bin/env python3
+"""Embed chart pictures in the book; keep repository image paths on GitHub."""
+import json
+import re
+import sys
+from html.parser import HTMLParser
+from pathlib import Path
+
+PICTURE = re.compile(r'\s*]*>\s*]*>\s*', re.S)
+
+
+class Picture(HTMLParser):
+ def __init__(self, markup):
+ super().__init__()
+ self.images = {}
+ self.feed(markup)
+
+ def handle_starttag(self, tag, attrs):
+ attrs = dict(attrs)
+ if tag == 'img':
+ self.images['light'] = attrs.get('src', '')
+ elif tag == 'source' and attrs.get('media') == '(prefers-color-scheme: dark)':
+ self.images['dark'] = attrs.get('srcset', '')
+
+
+def embed(match, root):
+ images = Picture(match.group()).images
+ # Only handle the paired, checked-in chart assets, not unrelated pictures.
+ if set(images) != {'light', 'dark'} or not all(
+ re.fullmatch(r'src/figures/figure-\d+-' + theme + r'\.svg', path)
+ for theme, path in images.items()
+ ):
+ return match.group()
+ fragments = []
+ for theme in ('light', 'dark'):
+ svg = (root / images[theme]).read_text()
+ scope = 'chart-' + Path(images[theme]).stem
+ # Inline SVG styles belong to the whole document. Scope the asset's
+ # class selectors so its palette cannot affect other figures/diagrams.
+ svg = re.sub(
+ r'',
+ lambda style: '',
+ svg,
+ flags=re.S,
+ )
+ # A blank line ends a CommonMark raw HTML block. Keep the SVG in
+ # one block so its text and shapes are not parsed as Markdown.
+ svg = '\n'.join(line for line in svg.splitlines() if line.strip())
+ fragments.append(f'
\n{svg}\n
')
+ return '\n'.join(fragments)
+
+
+def walk(items, root):
+ for item in items:
+ chapter = item.get('Chapter')
+ if chapter:
+ chapter['content'] = PICTURE.sub(lambda match: embed(match, root), chapter['content'])
+ walk(chapter['sub_items'], root)
+
+
+def main():
+ if len(sys.argv) > 1 and sys.argv[1] == 'supports':
+ sys.exit(0 if len(sys.argv) > 2 and sys.argv[2] == 'html' else 1)
+ context, book = json.load(sys.stdin)
+ walk(book.get('items', book.get('sections', [])), Path(context['root']))
+ json.dump(book, sys.stdout)
+
+
+if __name__ == '__main__':
+ main()
diff --git a/src/figures/figure-32-dark.svg b/src/figures/figure-32-dark.svg
new file mode 100644
index 0000000..cd671f6
--- /dev/null
+++ b/src/figures/figure-32-dark.svg
@@ -0,0 +1,1492 @@
+
\ No newline at end of file
diff --git a/src/figures/figure-32-light.svg b/src/figures/figure-32-light.svg
new file mode 100644
index 0000000..cf9e015
--- /dev/null
+++ b/src/figures/figure-32-light.svg
@@ -0,0 +1,1492 @@
+
\ No newline at end of file
diff --git a/theme/diagrams.css b/theme/diagrams.css
index bb71486..6256bb3 100644
--- a/theme/diagrams.css
+++ b/theme/diagrams.css
@@ -67,3 +67,36 @@ html:is(.coal, .navy, .ayu) .mdbook-graphviz-output [fill="black"] {
html:is(.coal, .navy, .ayu) .mdbook-graphviz-output text:not([fill]) {
fill: #b7b7b7;
}
+
+/* The build embeds both palettes as inline SVG. GitHub keeps the source
+ picture; book themes select a palette without fetching another image. */
+.chart-src {
+ margin: 1rem auto;
+}
+
+.chart-src svg {
+ display: block;
+ max-width: 100%;
+ height: auto;
+}
+
+.chart-dark {
+ display: none;
+}
+
+@media screen {
+ html:is(.coal, .navy, .ayu) .chart-dark {
+ display: block;
+ }
+
+ html:is(.coal, .navy, .ayu) .chart-light {
+ display: none;
+ }
+}
+
+/* Print the light palette even when the reader selected a dark book theme. */
+@media print {
+ .chart-src {
+ break-inside: avoid;
+ }
+}