diff --git a/CLAUDE.md b/CLAUDE.md
index 1f3c81c2..8948b09e 100644
--- a/CLAUDE.md
+++ b/CLAUDE.md
@@ -158,7 +158,7 @@ Stage with **explicit paths only** — never `git add -A` or `git add .`. Cursor
- `docs/VISUAL-STYLE.md` is the **binding** render standard; deviations are defects.
- **Framing gate:** Layer 1 framing is measured, not eyeballed — the example's `--output` render path calls the shared helper `examples/gallery_framing.py` (`check_framing`, exit 10 on violation) before writing the still: hero fill 0.70–0.90 of frame in at least one axis, every element that matters clearing all four edges by ≥ 0.02. The check-only path never invokes it, so smoke runtimes are unaffected. The helper measures; call sites enforce. Bleed compositions call `measure_framing_deviation` and assert their own cap at the call site — `check_framing` has no `deviation=` flag.
-- **Contact-sheet gate:** composite the candidate hero beside the pinned calibration set — currently `armature-bend`, `damped-track-aim`, `bmesh-gear` — commit the composite under `docs/gallery/contact-sheets/`, link it in the PR body, and report per-criterion verdicts (stage darkness, wedge warmth, subject fill, saturation, thumbnail legibility) including mean luminance versus the calibration images. A claim without the committed composite is not acceptable evidence. **This list is the canonical home of the pinned set** — update it here when a new example outclasses a member; `docs/new-example-prompt.md` points here rather than naming members. The longer "calibration references" list in `docs/VISUAL-STYLE.md` is a style reference, not this contact-sheet set.
+- **Contact-sheet gate:** composite the candidate hero beside the pinned calibration set — currently `armature-bend`, `damped-track-aim`, `bmesh-gear` — commit the composite under `docs/gallery/contact-sheets/`, link it in the PR body, and report per-criterion verdicts (stage darkness, wedge warmth, subject fill, saturation, thumbnail legibility) including mean luminance versus the calibration images. A claim without the committed composite is not acceptable evidence. **This list is the canonical home of the pinned set** — update it here when a new example outclasses a member; `docs/new-example-prompt.md` points here rather than naming members. The longer "calibration references" list in `docs/VISUAL-STYLE.md` is a style reference, not this contact-sheet set. Tooling: `python scripts/render_hero.py --blender PATH --only NAME` renders the README `--output` command and writes the hero and preview webps (the one committed PNG-to-webp path); `python scripts/contact_sheet.py NAME` builds the composite in the committed format and prints each tile's luma, stage, wedge warmth and saturation; `python scripts/measure_hero_drift.py` checks committed heroes against what the code renders now.
- **Asset-sheet gate (asset-type examples — game props/kits):** composite the hero asset rendered alone (neutral three-quarter view, plain studio lighting, no staging tricks, no labels, no comparison props) beside the pinned asset-quality reference set — currently `collision-hull-proxy`, `custom-normals-shade`, `vertex-weight-limit`, `lod-decimate-chain` — rendered the same way; commit under `docs/gallery/asset-sheets/`, link it in the PR body, and report a verdict. The asset ships only if it is not identifiable as the least-designed object in that lineup — a strong scene can carry a weak model; this gate removes the scene. **This list is the canonical home of the reference set** — update it here when a new asset outclasses a member. The measurable floors behind the gate (naming, material variation, edge treatment) live in `examples/gallery_asset_quality.py` — render path only, same call pattern as `gallery_framing`, exit 11 on violation — with the calibration table and dropped-floor evidence in `docs/VISUAL-STYLE.md` § Asset quality.
- **Falsification:** every check must be proven to fail once — break the contract, observe the non-zero exit, restore — with the probe and the measured error reported in the PR body. An assertion that cannot fail witnesses nothing.
- **After gallery regeneration** (`python scripts/build_gallery.py`), read the **generated HTML** character by character — the `
` text and witnesses callouts in `docs/gallery/index.html` and `docs/gallery//index.html` — not just `examples/gallery.json`. Precedent: the `teaches.split(".")[0]` bug truncated 14/21 card alts at dotted API paths like `bmesh.ops` while the source JSON looked fine (fixed in PR #68).
diff --git a/assets/og-card.jpg b/assets/og-card.jpg
index ae24d1b7..9a612f7e 100644
Binary files a/assets/og-card.jpg and b/assets/og-card.jpg differ
diff --git a/docs/gallery/assets/anvil-hero.webp b/docs/gallery/assets/anvil-hero.webp
index 39b42162..52c8c3c6 100644
Binary files a/docs/gallery/assets/anvil-hero.webp and b/docs/gallery/assets/anvil-hero.webp differ
diff --git a/docs/gallery/assets/bake-normal-high-to-low-hero.webp b/docs/gallery/assets/bake-normal-high-to-low-hero.webp
index 69e6c21a..bda5faac 100644
Binary files a/docs/gallery/assets/bake-normal-high-to-low-hero.webp and b/docs/gallery/assets/bake-normal-high-to-low-hero.webp differ
diff --git a/docs/gallery/assets/campfire-hero.webp b/docs/gallery/assets/campfire-hero.webp
index 9f1d0ac4..15745d09 100644
Binary files a/docs/gallery/assets/campfire-hero.webp and b/docs/gallery/assets/campfire-hero.webp differ
diff --git a/docs/gallery/assets/collision-hull-proxy-hero.webp b/docs/gallery/assets/collision-hull-proxy-hero.webp
index 82a49b21..2e3e1093 100644
Binary files a/docs/gallery/assets/collision-hull-proxy-hero.webp and b/docs/gallery/assets/collision-hull-proxy-hero.webp differ
diff --git a/docs/gallery/assets/compositor-glare-hero.webp b/docs/gallery/assets/compositor-glare-hero.webp
index 0bb46b15..5824a1c5 100644
Binary files a/docs/gallery/assets/compositor-glare-hero.webp and b/docs/gallery/assets/compositor-glare-hero.webp differ
diff --git a/docs/gallery/assets/custom-normals-shade-hero.webp b/docs/gallery/assets/custom-normals-shade-hero.webp
index 81ffa591..00dbfe1d 100644
Binary files a/docs/gallery/assets/custom-normals-shade-hero.webp and b/docs/gallery/assets/custom-normals-shade-hero.webp differ
diff --git a/docs/gallery/assets/damped-track-aim-hero.webp b/docs/gallery/assets/damped-track-aim-hero.webp
index e0b81346..dfef6de3 100644
Binary files a/docs/gallery/assets/damped-track-aim-hero.webp and b/docs/gallery/assets/damped-track-aim-hero.webp differ
diff --git a/docs/gallery/assets/fence-kit-hero.webp b/docs/gallery/assets/fence-kit-hero.webp
index 836bf9d7..ad6fd732 100644
Binary files a/docs/gallery/assets/fence-kit-hero.webp and b/docs/gallery/assets/fence-kit-hero.webp differ
diff --git a/docs/gallery/assets/gn-modifier-inputs-hero.webp b/docs/gallery/assets/gn-modifier-inputs-hero.webp
index 4da5f22b..0e2494f2 100644
Binary files a/docs/gallery/assets/gn-modifier-inputs-hero.webp and b/docs/gallery/assets/gn-modifier-inputs-hero.webp differ
diff --git a/docs/gallery/assets/grindstone-hero.webp b/docs/gallery/assets/grindstone-hero.webp
index 5e8245df..1b323765 100644
Binary files a/docs/gallery/assets/grindstone-hero.webp and b/docs/gallery/assets/grindstone-hero.webp differ
diff --git a/docs/gallery/assets/hand-pump-hero.webp b/docs/gallery/assets/hand-pump-hero.webp
index d52e9080..9760145e 100644
Binary files a/docs/gallery/assets/hand-pump-hero.webp and b/docs/gallery/assets/hand-pump-hero.webp differ
diff --git a/docs/gallery/assets/iron-cauldron-hero.webp b/docs/gallery/assets/iron-cauldron-hero.webp
index f981b302..9c348e6b 100644
Binary files a/docs/gallery/assets/iron-cauldron-hero.webp and b/docs/gallery/assets/iron-cauldron-hero.webp differ
diff --git a/docs/gallery/assets/light-link-studio-hero.webp b/docs/gallery/assets/light-link-studio-hero.webp
index ca53cca4..28de01ec 100644
Binary files a/docs/gallery/assets/light-link-studio-hero.webp and b/docs/gallery/assets/light-link-studio-hero.webp differ
diff --git a/docs/gallery/assets/park-bench-hero.webp b/docs/gallery/assets/park-bench-hero.webp
index 3305f65e..a443e1ea 100644
Binary files a/docs/gallery/assets/park-bench-hero.webp and b/docs/gallery/assets/park-bench-hero.webp differ
diff --git a/docs/gallery/assets/shipping-crate-hero.webp b/docs/gallery/assets/shipping-crate-hero.webp
index abcf26fc..48c80d4f 100644
Binary files a/docs/gallery/assets/shipping-crate-hero.webp and b/docs/gallery/assets/shipping-crate-hero.webp differ
diff --git a/docs/gallery/assets/signpost-hero.webp b/docs/gallery/assets/signpost-hero.webp
index c8c1ef4e..2bcd5523 100644
Binary files a/docs/gallery/assets/signpost-hero.webp and b/docs/gallery/assets/signpost-hero.webp differ
diff --git a/docs/gallery/assets/tavern-stool-hero.webp b/docs/gallery/assets/tavern-stool-hero.webp
index d59703a2..deaa34b4 100644
Binary files a/docs/gallery/assets/tavern-stool-hero.webp and b/docs/gallery/assets/tavern-stool-hero.webp differ
diff --git a/docs/gallery/assets/temp-override-join-hero.webp b/docs/gallery/assets/temp-override-join-hero.webp
index 32071492..c1031726 100644
Binary files a/docs/gallery/assets/temp-override-join-hero.webp and b/docs/gallery/assets/temp-override-join-hero.webp differ
diff --git a/docs/gallery/assets/terrain-scatter-hero.webp b/docs/gallery/assets/terrain-scatter-hero.webp
index 8325bc55..31dc7538 100644
Binary files a/docs/gallery/assets/terrain-scatter-hero.webp and b/docs/gallery/assets/terrain-scatter-hero.webp differ
diff --git a/docs/gallery/assets/text-version-stamp-hero.webp b/docs/gallery/assets/text-version-stamp-hero.webp
index 654dff5d..68d6087f 100644
Binary files a/docs/gallery/assets/text-version-stamp-hero.webp and b/docs/gallery/assets/text-version-stamp-hero.webp differ
diff --git a/docs/gallery/assets/treasure-chest-hero.webp b/docs/gallery/assets/treasure-chest-hero.webp
index a83ab3c0..a1a55cf1 100644
Binary files a/docs/gallery/assets/treasure-chest-hero.webp and b/docs/gallery/assets/treasure-chest-hero.webp differ
diff --git a/docs/gallery/assets/vse-cut-list-hero.webp b/docs/gallery/assets/vse-cut-list-hero.webp
index a49bbdcf..4ede31ff 100644
Binary files a/docs/gallery/assets/vse-cut-list-hero.webp and b/docs/gallery/assets/vse-cut-list-hero.webp differ
diff --git a/docs/gallery/assets/wall-torch-hero.webp b/docs/gallery/assets/wall-torch-hero.webp
index a790e58b..63ce96ac 100644
Binary files a/docs/gallery/assets/wall-torch-hero.webp and b/docs/gallery/assets/wall-torch-hero.webp differ
diff --git a/docs/gallery/assets/wave-displace-hero.webp b/docs/gallery/assets/wave-displace-hero.webp
index ffa6409a..a15156a1 100644
Binary files a/docs/gallery/assets/wave-displace-hero.webp and b/docs/gallery/assets/wave-displace-hero.webp differ
diff --git a/docs/gallery/assets/wheelbarrow-hero.webp b/docs/gallery/assets/wheelbarrow-hero.webp
index 44cfcb7b..509778c6 100644
Binary files a/docs/gallery/assets/wheelbarrow-hero.webp and b/docs/gallery/assets/wheelbarrow-hero.webp differ
diff --git a/docs/gallery/contact-sheets/anvil-contact-sheet.webp b/docs/gallery/contact-sheets/anvil-contact-sheet.webp
new file mode 100644
index 00000000..99487a66
Binary files /dev/null and b/docs/gallery/contact-sheets/anvil-contact-sheet.webp differ
diff --git a/docs/gallery/contact-sheets/bake-normal-high-to-low-contact-sheet.webp b/docs/gallery/contact-sheets/bake-normal-high-to-low-contact-sheet.webp
index 60419763..65a127be 100644
Binary files a/docs/gallery/contact-sheets/bake-normal-high-to-low-contact-sheet.webp and b/docs/gallery/contact-sheets/bake-normal-high-to-low-contact-sheet.webp differ
diff --git a/docs/gallery/contact-sheets/campfire-contact-sheet.webp b/docs/gallery/contact-sheets/campfire-contact-sheet.webp
new file mode 100644
index 00000000..6decf7fc
Binary files /dev/null and b/docs/gallery/contact-sheets/campfire-contact-sheet.webp differ
diff --git a/docs/gallery/contact-sheets/collision-hull-proxy-contact-sheet.webp b/docs/gallery/contact-sheets/collision-hull-proxy-contact-sheet.webp
index d90a0ab0..36e6de9a 100644
Binary files a/docs/gallery/contact-sheets/collision-hull-proxy-contact-sheet.webp and b/docs/gallery/contact-sheets/collision-hull-proxy-contact-sheet.webp differ
diff --git a/docs/gallery/contact-sheets/compositor-glare-contact-sheet.webp b/docs/gallery/contact-sheets/compositor-glare-contact-sheet.webp
new file mode 100644
index 00000000..bd1eb689
Binary files /dev/null and b/docs/gallery/contact-sheets/compositor-glare-contact-sheet.webp differ
diff --git a/docs/gallery/contact-sheets/custom-normals-shade-contact-sheet.webp b/docs/gallery/contact-sheets/custom-normals-shade-contact-sheet.webp
index 29b627f8..b5ad0a09 100644
Binary files a/docs/gallery/contact-sheets/custom-normals-shade-contact-sheet.webp and b/docs/gallery/contact-sheets/custom-normals-shade-contact-sheet.webp differ
diff --git a/docs/gallery/contact-sheets/damped-track-aim-contact-sheet.webp b/docs/gallery/contact-sheets/damped-track-aim-contact-sheet.webp
new file mode 100644
index 00000000..e6cc3343
Binary files /dev/null and b/docs/gallery/contact-sheets/damped-track-aim-contact-sheet.webp differ
diff --git a/docs/gallery/contact-sheets/fence-kit-contact-sheet.webp b/docs/gallery/contact-sheets/fence-kit-contact-sheet.webp
new file mode 100644
index 00000000..346afba7
Binary files /dev/null and b/docs/gallery/contact-sheets/fence-kit-contact-sheet.webp differ
diff --git a/docs/gallery/contact-sheets/gn-modifier-inputs-contact-sheet.webp b/docs/gallery/contact-sheets/gn-modifier-inputs-contact-sheet.webp
index 7b075203..7bc87258 100644
Binary files a/docs/gallery/contact-sheets/gn-modifier-inputs-contact-sheet.webp and b/docs/gallery/contact-sheets/gn-modifier-inputs-contact-sheet.webp differ
diff --git a/docs/gallery/contact-sheets/grindstone-contact-sheet.webp b/docs/gallery/contact-sheets/grindstone-contact-sheet.webp
new file mode 100644
index 00000000..9e4c8dfc
Binary files /dev/null and b/docs/gallery/contact-sheets/grindstone-contact-sheet.webp differ
diff --git a/docs/gallery/contact-sheets/hand-pump-contact-sheet.webp b/docs/gallery/contact-sheets/hand-pump-contact-sheet.webp
new file mode 100644
index 00000000..b5fedebd
Binary files /dev/null and b/docs/gallery/contact-sheets/hand-pump-contact-sheet.webp differ
diff --git a/docs/gallery/contact-sheets/hitching-post-contact-sheet.webp b/docs/gallery/contact-sheets/hitching-post-contact-sheet.webp
new file mode 100644
index 00000000..720885c9
Binary files /dev/null and b/docs/gallery/contact-sheets/hitching-post-contact-sheet.webp differ
diff --git a/docs/gallery/contact-sheets/iron-cauldron-contact-sheet.webp b/docs/gallery/contact-sheets/iron-cauldron-contact-sheet.webp
new file mode 100644
index 00000000..15e3766e
Binary files /dev/null and b/docs/gallery/contact-sheets/iron-cauldron-contact-sheet.webp differ
diff --git a/docs/gallery/contact-sheets/light-link-studio-contact-sheet.webp b/docs/gallery/contact-sheets/light-link-studio-contact-sheet.webp
index a80ebe45..e3ac6081 100644
Binary files a/docs/gallery/contact-sheets/light-link-studio-contact-sheet.webp and b/docs/gallery/contact-sheets/light-link-studio-contact-sheet.webp differ
diff --git a/docs/gallery/contact-sheets/market-stall-contact-sheet.webp b/docs/gallery/contact-sheets/market-stall-contact-sheet.webp
new file mode 100644
index 00000000..b6377487
Binary files /dev/null and b/docs/gallery/contact-sheets/market-stall-contact-sheet.webp differ
diff --git a/docs/gallery/contact-sheets/park-bench-contact-sheet.webp b/docs/gallery/contact-sheets/park-bench-contact-sheet.webp
new file mode 100644
index 00000000..c71c1830
Binary files /dev/null and b/docs/gallery/contact-sheets/park-bench-contact-sheet.webp differ
diff --git a/docs/gallery/contact-sheets/shipping-crate-contact-sheet.webp b/docs/gallery/contact-sheets/shipping-crate-contact-sheet.webp
new file mode 100644
index 00000000..4350dd54
Binary files /dev/null and b/docs/gallery/contact-sheets/shipping-crate-contact-sheet.webp differ
diff --git a/docs/gallery/contact-sheets/signpost-contact-sheet.webp b/docs/gallery/contact-sheets/signpost-contact-sheet.webp
new file mode 100644
index 00000000..f51a2c98
Binary files /dev/null and b/docs/gallery/contact-sheets/signpost-contact-sheet.webp differ
diff --git a/docs/gallery/contact-sheets/street-lantern-contact-sheet.webp b/docs/gallery/contact-sheets/street-lantern-contact-sheet.webp
new file mode 100644
index 00000000..a202917b
Binary files /dev/null and b/docs/gallery/contact-sheets/street-lantern-contact-sheet.webp differ
diff --git a/docs/gallery/contact-sheets/tavern-stool-contact-sheet.webp b/docs/gallery/contact-sheets/tavern-stool-contact-sheet.webp
new file mode 100644
index 00000000..40deb61d
Binary files /dev/null and b/docs/gallery/contact-sheets/tavern-stool-contact-sheet.webp differ
diff --git a/docs/gallery/contact-sheets/temp-override-join-contact-sheet.webp b/docs/gallery/contact-sheets/temp-override-join-contact-sheet.webp
new file mode 100644
index 00000000..34ca79bb
Binary files /dev/null and b/docs/gallery/contact-sheets/temp-override-join-contact-sheet.webp differ
diff --git a/docs/gallery/contact-sheets/terrain-scatter-contact-sheet.webp b/docs/gallery/contact-sheets/terrain-scatter-contact-sheet.webp
new file mode 100644
index 00000000..099cdccf
Binary files /dev/null and b/docs/gallery/contact-sheets/terrain-scatter-contact-sheet.webp differ
diff --git a/docs/gallery/contact-sheets/text-version-stamp-contact-sheet.webp b/docs/gallery/contact-sheets/text-version-stamp-contact-sheet.webp
index a84de1c3..b48f2a81 100644
Binary files a/docs/gallery/contact-sheets/text-version-stamp-contact-sheet.webp and b/docs/gallery/contact-sheets/text-version-stamp-contact-sheet.webp differ
diff --git a/docs/gallery/contact-sheets/treasure-chest-contact-sheet.webp b/docs/gallery/contact-sheets/treasure-chest-contact-sheet.webp
new file mode 100644
index 00000000..6ef410bc
Binary files /dev/null and b/docs/gallery/contact-sheets/treasure-chest-contact-sheet.webp differ
diff --git a/docs/gallery/contact-sheets/vse-cut-list-contact-sheet.webp b/docs/gallery/contact-sheets/vse-cut-list-contact-sheet.webp
index 55d8d11b..bb190f12 100644
Binary files a/docs/gallery/contact-sheets/vse-cut-list-contact-sheet.webp and b/docs/gallery/contact-sheets/vse-cut-list-contact-sheet.webp differ
diff --git a/docs/gallery/contact-sheets/wall-torch-contact-sheet.webp b/docs/gallery/contact-sheets/wall-torch-contact-sheet.webp
new file mode 100644
index 00000000..e8adf93e
Binary files /dev/null and b/docs/gallery/contact-sheets/wall-torch-contact-sheet.webp differ
diff --git a/docs/gallery/contact-sheets/watchtower-contact-sheet.webp b/docs/gallery/contact-sheets/watchtower-contact-sheet.webp
new file mode 100644
index 00000000..8a2ddd29
Binary files /dev/null and b/docs/gallery/contact-sheets/watchtower-contact-sheet.webp differ
diff --git a/docs/gallery/contact-sheets/water-trough-contact-sheet.webp b/docs/gallery/contact-sheets/water-trough-contact-sheet.webp
new file mode 100644
index 00000000..783523f5
Binary files /dev/null and b/docs/gallery/contact-sheets/water-trough-contact-sheet.webp differ
diff --git a/docs/gallery/contact-sheets/wave-displace-contact-sheet.webp b/docs/gallery/contact-sheets/wave-displace-contact-sheet.webp
new file mode 100644
index 00000000..d81131e6
Binary files /dev/null and b/docs/gallery/contact-sheets/wave-displace-contact-sheet.webp differ
diff --git a/docs/gallery/contact-sheets/wheelbarrow-contact-sheet.webp b/docs/gallery/contact-sheets/wheelbarrow-contact-sheet.webp
new file mode 100644
index 00000000..8081dd44
Binary files /dev/null and b/docs/gallery/contact-sheets/wheelbarrow-contact-sheet.webp differ
diff --git a/docs/gallery/contact-sheets/wooden-barrel-contact-sheet.webp b/docs/gallery/contact-sheets/wooden-barrel-contact-sheet.webp
new file mode 100644
index 00000000..92f6964c
Binary files /dev/null and b/docs/gallery/contact-sheets/wooden-barrel-contact-sheet.webp differ
diff --git a/docs/gallery/contact-sheets/wooden-bucket-contact-sheet.webp b/docs/gallery/contact-sheets/wooden-bucket-contact-sheet.webp
new file mode 100644
index 00000000..2e9b519f
Binary files /dev/null and b/docs/gallery/contact-sheets/wooden-bucket-contact-sheet.webp differ
diff --git a/docs/gallery/contact-sheets/wooden-ladder-contact-sheet.webp b/docs/gallery/contact-sheets/wooden-ladder-contact-sheet.webp
new file mode 100644
index 00000000..d1701705
Binary files /dev/null and b/docs/gallery/contact-sheets/wooden-ladder-contact-sheet.webp differ
diff --git a/examples/bake-normal-high-to-low/preview.webp b/examples/bake-normal-high-to-low/preview.webp
index 54c1dc88..be76ca54 100644
Binary files a/examples/bake-normal-high-to-low/preview.webp and b/examples/bake-normal-high-to-low/preview.webp differ
diff --git a/examples/collision-hull-proxy/preview.webp b/examples/collision-hull-proxy/preview.webp
index 8e781f60..b8564e8d 100644
Binary files a/examples/collision-hull-proxy/preview.webp and b/examples/collision-hull-proxy/preview.webp differ
diff --git a/examples/compositor-glare/preview.webp b/examples/compositor-glare/preview.webp
index 26a1ff4b..f5eae363 100644
Binary files a/examples/compositor-glare/preview.webp and b/examples/compositor-glare/preview.webp differ
diff --git a/examples/custom-normals-shade/preview.webp b/examples/custom-normals-shade/preview.webp
index 96977767..8953e5f5 100644
Binary files a/examples/custom-normals-shade/preview.webp and b/examples/custom-normals-shade/preview.webp differ
diff --git a/examples/damped-track-aim/preview.webp b/examples/damped-track-aim/preview.webp
index 4a9b3145..bf8ee317 100644
Binary files a/examples/damped-track-aim/preview.webp and b/examples/damped-track-aim/preview.webp differ
diff --git a/examples/gn-modifier-inputs/preview.webp b/examples/gn-modifier-inputs/preview.webp
index dfc19c7c..ddf547f8 100644
Binary files a/examples/gn-modifier-inputs/preview.webp and b/examples/gn-modifier-inputs/preview.webp differ
diff --git a/examples/light-link-studio/preview.webp b/examples/light-link-studio/preview.webp
index 9b8d8dbd..49ad31b9 100644
Binary files a/examples/light-link-studio/preview.webp and b/examples/light-link-studio/preview.webp differ
diff --git a/examples/temp-override-join/preview.webp b/examples/temp-override-join/preview.webp
index 0ba9b3a2..b5234a56 100644
Binary files a/examples/temp-override-join/preview.webp and b/examples/temp-override-join/preview.webp differ
diff --git a/examples/text-version-stamp/preview.webp b/examples/text-version-stamp/preview.webp
index 315e3608..6b65c980 100644
Binary files a/examples/text-version-stamp/preview.webp and b/examples/text-version-stamp/preview.webp differ
diff --git a/examples/vse-cut-list/preview.webp b/examples/vse-cut-list/preview.webp
index 3458671e..e0d96577 100644
Binary files a/examples/vse-cut-list/preview.webp and b/examples/vse-cut-list/preview.webp differ
diff --git a/examples/wave-displace/preview.webp b/examples/wave-displace/preview.webp
index fa11060e..60750a36 100644
Binary files a/examples/wave-displace/preview.webp and b/examples/wave-displace/preview.webp differ
diff --git a/scripts/contact_sheet.py b/scripts/contact_sheet.py
new file mode 100644
index 00000000..7622b7d5
--- /dev/null
+++ b/scripts/contact_sheet.py
@@ -0,0 +1,99 @@
+#!/usr/bin/env python3
+"""Build a gallery contact sheet and print the numbers its verdict reports.
+
+Composites a candidate hero beside the pinned calibration set (canonical
+membership: CLAUDE.md, Quality Gates) in the committed format: four 640x360
+tiles, candidate first, captioned, on the dark sheet background, written to
+``docs/gallery/contact-sheets/-contact-sheet.webp``.
+
+It also prints what the gate's verdict needs measured: mean luminance
+(Rec. 709) of each tile, the stage (mean luminance of the frame border, where
+the backdrop is), the wedge warmth (red minus blue over the brightest tenth
+of the lower half, where the warm floor pool sits), and mean saturation. The
+verdict itself - "is this sortable as the odd one out?" - stays a judgment
+made by looking at the sheet; this only makes the numbers repeatable.
+
+Authoring tool: host Python with Pillow and numpy.
+
+Usage:
+ python scripts/contact_sheet.py NAME [NAME ...]
+
+Exit codes: 0 written, 2 usage.
+"""
+from __future__ import annotations
+
+import sys
+
+import numpy as np
+from PIL import Image, ImageDraw
+
+from measure_hero_drift import REPO, entries
+
+CALIBRATION = ("armature-bend", "damped-track-aim", "bmesh-gear")
+TILE = (640, 360)
+GAP, CAPTION = 10, 32
+BG = (22, 23, 26)
+TEXT = (150, 152, 160)
+OUT = REPO / "docs" / "gallery" / "contact-sheets"
+
+
+def metrics(im: Image.Image) -> dict:
+ a = np.asarray(im.convert("RGB"), dtype=np.float64) / 255.0
+ luma = a @ np.array([0.2126, 0.7152, 0.0722])
+ h, w = luma.shape
+ b = max(1, h // 12)
+ border = np.concatenate([luma[:b].ravel(), luma[-b:].ravel(),
+ luma[:, :b].ravel(), luma[:, -b:].ravel()])
+ lower = a[h // 2:].reshape(-1, 3)
+ lower_l = luma[h // 2:].ravel()
+ hot = lower[lower_l >= np.quantile(lower_l, 0.9)]
+ mx, mn = a.max(axis=2), a.min(axis=2)
+ sat = np.where(mx > 0, (mx - mn) / np.maximum(mx, 1e-9), 0.0)
+ return {
+ "luma": float(luma.mean()),
+ "stage": float(border.mean()),
+ "warmth": float((hot[:, 0] - hot[:, 2]).mean()),
+ "sat": float(sat.mean()),
+ }
+
+
+def build(name: str, heroes: dict[str, str]) -> list[tuple[str, dict]]:
+ names = [name] + [c for c in CALIBRATION if c != name]
+ tw, th = TILE
+ sheet = Image.new("RGB", (GAP + len(names) * (tw + GAP), GAP + th + CAPTION), BG)
+ draw = ImageDraw.Draw(sheet)
+ rows = []
+ for i, n in enumerate(names):
+ im = Image.open(REPO / heroes[n]).convert("RGB")
+ x = GAP + i * (tw + GAP)
+ sheet.paste(im.resize(TILE, Image.LANCZOS), (x, GAP))
+ label = f"{n} <- candidate" if i == 0 else f"{n} (calibration)"
+ draw.text((x + 4, GAP + th + 10), label, fill=TEXT)
+ rows.append((label, metrics(im)))
+ OUT.mkdir(parents=True, exist_ok=True)
+ sheet.save(OUT / f"{name}-contact-sheet.webp", "WEBP", quality=90)
+ return rows
+
+
+def main(argv=None) -> int:
+ argv = sys.argv[1:] if argv is None else argv
+ if not argv:
+ print(__doc__.split("Usage:")[1].strip(), file=sys.stderr)
+ return 2
+ heroes = {e["name"]: e["hero"] for e in entries()}
+ unknown = [n for n in argv if n not in heroes]
+ if unknown:
+ print(f"unknown entries: {', '.join(unknown)}", file=sys.stderr)
+ return 2
+ for name in argv:
+ rows = build(name, heroes)
+ print(f"\n{name} -> docs/gallery/contact-sheets/{name}-contact-sheet.webp")
+ print(f" {'tile':34} {'luma':>6} {'stage':>6} {'warmth':>7} {'sat':>6}")
+ for label, m in rows:
+ print(f" {label:34} {m['luma']:6.4f} {m['stage']:6.4f} "
+ f"{m['warmth']:+7.4f} {m['sat']:6.4f}")
+ return 0
+
+
+if __name__ == "__main__":
+ sys.exit(main())
diff --git a/scripts/render_hero.py b/scripts/render_hero.py
new file mode 100644
index 00000000..a3b42997
--- /dev/null
+++ b/scripts/render_hero.py
@@ -0,0 +1,99 @@
+#!/usr/bin/env python3
+"""Regenerate gallery hero and preview webps from the code, one committed path.
+
+For each named entry in ``examples/gallery.json`` / ``showcase/gallery.json``:
+
+1. render the README's documented ``--output`` command to a lossless PNG,
+ keeping every other documented flag (``--engine cycles`` ...);
+2. write the hero, ``docs/gallery/assets/-hero.webp`` at 1280x720;
+3. write the preview at the entry's ``preview`` path, 1200x675 (Lanczos).
+
+Both are webp quality 90, the setting every showcase script uses when it
+writes webp itself. Before this file the PNG -> webp step lived in uncommitted
+scratch scripts, so nobody could regenerate a still and get the committed
+result (#200). The script's own ``--output`` render path runs the framing
+(exit 10) and asset-quality (exit 11) gates before it writes, so a
+non-zero exit here leaves the committed files untouched.
+
+Authoring tool, not CI: host Python with Pillow; one full render per entry.
+
+Usage:
+ python scripts/render_hero.py --blender PATH --only NAME [--only NAME ...]
+
+Exit codes: 0 all written, 2 usage, 3 at least one render failed.
+"""
+from __future__ import annotations
+
+import argparse
+import subprocess
+import sys
+import tempfile
+from pathlib import Path
+
+from PIL import Image
+
+from measure_hero_drift import REPO, entries, render_args
+
+HERO_SIZE = (1280, 720)
+PREVIEW_SIZE = (1200, 675)
+QUALITY = 90
+
+
+def main(argv=None) -> int:
+ p = argparse.ArgumentParser(description=__doc__.split("\n")[0])
+ p.add_argument("--blender", required=True, help="Blender binary to render with")
+ p.add_argument("--only", action="append", required=True,
+ help="entry to regenerate (repeatable)")
+ args = p.parse_args(argv)
+
+ blender = str(Path(args.blender).resolve())
+ version = subprocess.run([blender, "--version"], capture_output=True, text=True,
+ encoding="utf-8", errors="replace").stdout.splitlines()[0]
+ print(f"# renderer: {blender}\n# version: {version}", flush=True)
+
+ by_name = {e["name"]: e for e in entries()}
+ unknown = [n for n in args.only if n not in by_name]
+ if unknown:
+ print(f"unknown entries: {', '.join(unknown)}", file=sys.stderr)
+ return 2
+
+ failed = []
+ with tempfile.TemporaryDirectory() as tmp:
+ for name in args.only:
+ e = by_name[name]
+ png = Path(tmp) / f"{name}.png"
+ cmd = render_args(e, png)
+ if cmd is None:
+ print(f"{name}: no --output command in README", flush=True)
+ failed.append(name)
+ continue
+ script = next((REPO / e["dir"]).glob("*.py"))
+ proc = subprocess.run(
+ [blender, "--background", "--factory-startup", "--python", str(script),
+ "--", *cmd],
+ capture_output=True, text=True, encoding="utf-8", errors="replace", cwd=REPO,
+ )
+ if proc.returncode != 0 or not png.is_file():
+ tail = (proc.stdout + proc.stderr).strip().splitlines()[-3:]
+ print(f"{name}: render failed, exit {proc.returncode}: {' | '.join(tail)}",
+ flush=True)
+ failed.append(name)
+ continue
+ im = Image.open(png).convert("RGB")
+ if im.size != HERO_SIZE:
+ print(f"{name}: render is {im.size[0]}x{im.size[1]}, expected "
+ f"{HERO_SIZE[0]}x{HERO_SIZE[1]}", flush=True)
+ failed.append(name)
+ continue
+ hero, preview = REPO / e["hero"], REPO / e["preview"]
+ im.save(hero, "WEBP", quality=QUALITY)
+ im.resize(PREVIEW_SIZE, Image.LANCZOS).save(preview, "WEBP", quality=QUALITY)
+ print(f"{name}: wrote {e['hero']} ({hero.stat().st_size} B) and "
+ f"{e['preview']} ({preview.stat().st_size} B)", flush=True)
+
+ print(f"\n# {len(args.only) - len(failed)} regenerated, {len(failed)} failed")
+ return 3 if failed else 0
+
+
+if __name__ == "__main__":
+ sys.exit(main())
diff --git a/showcase/anvil/preview.webp b/showcase/anvil/preview.webp
index 61f49b02..c5fb1520 100644
Binary files a/showcase/anvil/preview.webp and b/showcase/anvil/preview.webp differ
diff --git a/showcase/campfire/preview.webp b/showcase/campfire/preview.webp
index 106ae2f3..307046ab 100644
Binary files a/showcase/campfire/preview.webp and b/showcase/campfire/preview.webp differ
diff --git a/showcase/fence-kit/preview.webp b/showcase/fence-kit/preview.webp
index 66ae37f5..7a710a26 100644
Binary files a/showcase/fence-kit/preview.webp and b/showcase/fence-kit/preview.webp differ
diff --git a/showcase/grindstone/preview.webp b/showcase/grindstone/preview.webp
index 90a7ba37..789cbc88 100644
Binary files a/showcase/grindstone/preview.webp and b/showcase/grindstone/preview.webp differ
diff --git a/showcase/hand-pump/preview.webp b/showcase/hand-pump/preview.webp
index 06e69f45..433b95b0 100644
Binary files a/showcase/hand-pump/preview.webp and b/showcase/hand-pump/preview.webp differ
diff --git a/showcase/iron-cauldron/preview.webp b/showcase/iron-cauldron/preview.webp
index 1ce8a2ae..e10ba3f0 100644
Binary files a/showcase/iron-cauldron/preview.webp and b/showcase/iron-cauldron/preview.webp differ
diff --git a/showcase/park-bench/preview.webp b/showcase/park-bench/preview.webp
index c17c89b4..443a36fc 100644
Binary files a/showcase/park-bench/preview.webp and b/showcase/park-bench/preview.webp differ
diff --git a/showcase/shipping-crate/preview.webp b/showcase/shipping-crate/preview.webp
index 379ecee8..658045f5 100644
Binary files a/showcase/shipping-crate/preview.webp and b/showcase/shipping-crate/preview.webp differ
diff --git a/showcase/signpost/preview.webp b/showcase/signpost/preview.webp
index 3048328b..c325d82e 100644
Binary files a/showcase/signpost/preview.webp and b/showcase/signpost/preview.webp differ
diff --git a/showcase/tavern-stool/preview.webp b/showcase/tavern-stool/preview.webp
index e466bed7..ecfa8df0 100644
Binary files a/showcase/tavern-stool/preview.webp and b/showcase/tavern-stool/preview.webp differ
diff --git a/showcase/terrain-scatter/preview.webp b/showcase/terrain-scatter/preview.webp
index 8399ff29..8e728664 100644
Binary files a/showcase/terrain-scatter/preview.webp and b/showcase/terrain-scatter/preview.webp differ
diff --git a/showcase/treasure-chest/preview.webp b/showcase/treasure-chest/preview.webp
index 449a1c53..51be3b28 100644
Binary files a/showcase/treasure-chest/preview.webp and b/showcase/treasure-chest/preview.webp differ
diff --git a/showcase/wall-torch/preview.webp b/showcase/wall-torch/preview.webp
index 438de785..c9559de6 100644
Binary files a/showcase/wall-torch/preview.webp and b/showcase/wall-torch/preview.webp differ
diff --git a/showcase/wheelbarrow/preview.webp b/showcase/wheelbarrow/preview.webp
index 89e4a3df..09c62574 100644
Binary files a/showcase/wheelbarrow/preview.webp and b/showcase/wheelbarrow/preview.webp differ