diff --git a/.gitignore b/.gitignore
index a691249..173920a 100644
--- a/.gitignore
+++ b/.gitignore
@@ -17,6 +17,8 @@ node_modules/
__pycache__/
*.pyc
.venv/
+
+# --- 完全版ビルド成果物(build.py が生成。正本は各章の index.html)---
dist/
# --- Claude Code ローカル設定 ---
@@ -25,7 +27,7 @@ dist/
# --- 旧 PPTx(HTML 路線へ移行済み、新規 PPTx は受け付けない) ---
*.pptx
-# --- 未使用の制作物アセット(実際に lecture.html から参照されているものだけ track する) ---
+# --- 未使用の制作物アセット(実際に index.html から参照されているものだけ track する) ---
# 過去の制作 / 試作画像は手元には残してリポでは track しない。
01-claude-code-intro/_assets/horse-harness.png
01-claude-code-intro/_assets/model-service-layers.png
diff --git a/README.md b/README.md
index 9883ff7..51008ad 100644
--- a/README.md
+++ b/README.md
@@ -63,6 +63,20 @@ cd lectures
---
+## 完全版(1 ファイル)をビルドする
+
+各章を 1 つの HTML にまとめた「完全版」を生成できます。`shared/` の CSS・JS と画像をすべて埋め込むので、**リポジトリもネット接続もなしで開けます**。メール添付・USB 配布・オフライン閲覧に。
+
+```bash
+python build.py
+```
+
+`dist/00-about.html` などが生成されます(`dist/` は git 管理外のビルド成果物)。正本は各章の `index.html`(`shared/` 参照版)のままなので、`shared/` や本文を変更したら `python build.py` で作り直します。依存は Python 3 標準ライブラリのみ。
+
+> Web フォント(Google Fonts)だけは埋め込みません。完全オフラインではシステムフォントにフォールバックします(レイアウトは崩れません)。
+
+---
+
## 一緒に作る — 貢献の入口
このリポジトリは **教える側と教わる側の境界をゆるくする** ことを意図しています。受講者も貢献者です。
diff --git a/build.py b/build.py
new file mode 100644
index 0000000..fe2d1f8
--- /dev/null
+++ b/build.py
@@ -0,0 +1,144 @@
+#!/usr/bin/env python3
+"""build.py — 各章の index.html から「完全版」(単一ファイル)を生成する。
+
+shared/ の CSS・JS と、ローカル画像(qr.svg / _assets/*.png)を HTML に
+インライン化し、dist/<章名>.html として 1 ファイルにまとめる。
+リポジトリやネット接続なしで開ける、配布・オフライン閲覧用のファイル。
+
+正本は各章の index.html(shared/ 参照版)のまま。dist/ はビルド成果物で
+git 管理外。内容や shared/ を変更したら `python build.py` で再生成する。
+
+既知の制約:
+ theme.css は Google Fonts を @import している。完全オフラインでは
+ Web フォントが読めず、システムフォントにフォールバックする
+ (レイアウトは崩れない)。フォント自体の埋め込みは行わない。
+
+依存: Python 3 標準ライブラリのみ。
+使い方: リポジトリ直下で `python build.py`
+"""
+from __future__ import annotations
+
+import base64
+import mimetypes
+import re
+import sys
+from pathlib import Path
+
+ROOT = Path(__file__).resolve().parent
+DIST = ROOT / "dist"
+
+# 外部 / data URI は埋め込み対象外(そのまま残す)
+_SKIP_PREFIXES = ("http://", "https://", "data:", "//")
+
+
+def find_chapters() -> list[Path]:
+ """`NN-...` 形式で index.html を持つ章ディレクトリを返す。"""
+ return sorted(p.parent for p in ROOT.glob("[0-9][0-9]-*/index.html"))
+
+
+def data_uri(path: Path) -> str:
+ mime, _ = mimetypes.guess_type(path.name)
+ if mime is None:
+ mime = "application/octet-stream"
+ b64 = base64.b64encode(path.read_bytes()).decode("ascii")
+ return f"data:{mime};base64,{b64}"
+
+
+def inline(html: str, base_dir: Path) -> str:
+ """html 内のローカル CSS / JS / 画像参照をインライン化して返す。"""
+
+ def resolve(ref: str) -> Path:
+ return (base_dir / ref).resolve()
+
+ def is_local(ref: str) -> bool:
+ return not ref.startswith(_SKIP_PREFIXES)
+
+ # 1. →
+ def css_repl(m: re.Match) -> str:
+ href = m.group("href")
+ if not is_local(href):
+ return m.group(0)
+ css = resolve(href).read_text(encoding="utf-8-sig")
+ return f""
+
+ html = re.sub(
+ r'[^"]+)"\s*/?\s*>',
+ css_repl,
+ html,
+ )
+
+ # 2. はそのまま残り、インライン化したスクリプトを閉じる。
+ # (終了タグを正規表現で扱わないことで、script 要素全体をマッチする
+ # 脆い正規表現パターンを避ける)
+ # defer 属性は落ちるが、progress-strip.js は readyState を自前で
+ # ガードしており、deck-stage.js は head 配置で問題ない。
+ def js_repl(m: re.Match) -> str:
+ src = m.group("src")
+ if not is_local(src):
+ return m.group(0)
+ js = resolve(src).read_text(encoding="utf-8-sig")
+ # JS 内に "\n{js}\n"
+
+ html = re.sub(r'