diff --git a/docs/new-example-prompt.md b/docs/new-example-prompt.md
new file mode 100644
index 0000000..934388e
--- /dev/null
+++ b/docs/new-example-prompt.md
@@ -0,0 +1,131 @@
+# Prompt: Create a new Blender example
+
+Copy everything below this line into the agent.
+
+---
+
+Create, ship, and merge one original, runnable example for this repository. The bar is
+not "a working example"; it is an example strong enough to anchor the gallery on its own.
+
+First, review `CLAUDE.md`, `AGENTS.md`, `ROADMAP.md`, `examples/gallery.json`,
+`docs/VISUAL-STYLE.md`, and several existing examples—study the strongest ones, not
+just the nearest one. Use them to understand the repository's current conventions,
+integration points, visual standard, and test structure. Check all existing examples
+before choosing a subject so your contribution does not duplicate or slightly
+repackage something already covered.
+
+Before editing, inspect the Git state. Start from an up-to-date `main`, preserve unrelated
+work, and create a focused feature branch. Never force-push or bypass hooks. Do not include
+secrets, temporary renders, caches, or unrelated files.
+
+Subject selection. Prefer the ROADMAP candidate pool if it has entries; otherwise choose
+freely. Whatever the source, the subject must pass all of these tests before you write
+code:
+
+- It witnesses a real, load-bearing API contract—something with observable behavior
+ that can drift, not a convenience wrapper or a tutorial topic. The strongest subjects
+ are ones AI-generated Blender code commonly gets wrong: object-mode versus edit-mode
+ data lifetimes, evaluated versus original data, ordering constraints, references that
+ dangle after CustomData reallocation, APIs renamed or restructured between 4.5 LTS
+ and 5.1.
+- Its check can fail for a real reason. Before trusting any assertion, prove it catches
+ the failure it claims to catch: temporarily break the contract (skip the pose, swap
+ the buffer, use the legacy path) and confirm the check exits non-zero, then restore
+ it. An assertion that cannot fail witnesses nothing. State in the README what failure
+ each check would catch.
+- Its correctness is independently derivable. The best checks compare Blender's output
+ against a closed-form or independently computed expectation (re-implemented math, a
+ round-trip through a raw buffer, a known geometric invariant), not against a value
+ captured from a previous run of the same code.
+- Its render is legible as proof. Someone glancing at the thumbnail should be able to
+ infer what contract is being demonstrated. If the render would look the same whether
+ the API worked or not, redesign the scene until failure would be visible.
+
+Where an API diverges between 4.5 and 5.1, asserting each side's actual contract is part
+of the witness—version-gate explicitly and document the divergence rather than papering
+over it. If you discover an undocumented hazard while authoring (a crash, a dangling
+reference, an ordering constraint), that discovery belongs in the code comments and
+README; it is often more valuable than the original subject.
+
+The example must:
+
+- run headlessly in Blender 5.1 and Blender 4.5 LTS;
+- perform deterministic checks of a real Blender API contract and exit non-zero on
+ failure, with numeric tolerances chosen deliberately and printed on success so CI
+ logs carry the measured values;
+- default to a fast check-only run and optionally render its own gallery image;
+- follow repository conventions and relevant Blender safety rules;
+- include concise documentation explaining what it teaches, what each check witnesses,
+ what failure it would catch, and any version-gated divergence between 4.5 and 5.1;
+- produce a deliberate, well-framed render rather than a mockup, primitive dump, or
+ placeholder.
+
+Complete every integration required for a shipped example. Infer the exact current
+shape from neighboring examples and repository configuration, including the example
+directory, README, gallery metadata and assets, plugin manifest, smoke workflow,
+top-level README, and generated gallery pages. After regenerating the gallery with
+`python scripts/build_gallery.py`, read the **generated** output character by
+character — not only `examples/gallery.json` source fields. Open
+`docs/gallery/index.html` and `docs/gallery/
/index.html` and inspect the
+rendered `
` text and the witnesses callout for duplicated words, truncation,
+and generator artifacts (the previous `teaches.split(".")[0]` bug truncated alts at
+dotted API paths like `bmesh.ops`; source JSON looked fine). Keep the ROADMAP
+candidate pool in sync: remove the shipped subject, and add any promising subjects
+you identified but did not build. Do not hand-edit release-owned version fields or
+generated pages; use the repository's generator.
+
+Run the new example's check-only path on both supported Blender versions, render and
+visually inspect its final image, regenerate the gallery, and run all relevant repository
+validation. If Blender 4.5 is unavailable locally, say so precisely and use the repository's
+4.5 CI job—do not substitute another version and report it as 4.5. If the render path
+requires an engine or device unavailable locally (Cycles on a GPU-less host), say so and
+identify exactly which paths were exercised only in CI or by inspection. Fix every
+failure you introduce.
+
+Treat visual quality as a shipping gate with the same rigor as the checks. Conform to
+`docs/VISUAL-STYLE.md` explicitly (Standard view transform, dark studio stage, AREA
+key/fill/rim/wedge, designed hero materials, framing, 1280×720 → hero/preview webp).
+Inspect the actual rendered pixels at full size and as the gallery thumbnail.
+Iterate—the first render is a draft, not a candidate. Revise until the subject and
+demonstrated API contract are immediately readable, the composition is intentional,
+important geometry is not clipped, highlights are not blown out, lights or helper
+objects are not visible unintentionally, and materials, lighting, background, and
+camera feel designed rather than left at Blender defaults.
+
+Contact-sheet gate (required before shipping the still): place the new hero beside
+the pinned calibration set — currently `armature-bend`, `damped-track-aim`,
+`bmesh-gear` — and compare stage darkness, wedge warmth, subject fill, saturation,
+and thumbnail legibility side by side. Do not ship until the new image holds up in
+that lineup—not merely "looks fine alone." Update this named set in this prompt
+whenever a new example outclasses one of them. One successful render command is not
+proof that the image is good, and neither is the second. Report that the
+contact-sheet comparison was done against those three references.
+
+After implementation and local verification:
+
+1. Review the complete diff and remove accidental churn or temporary files.
+2. Commit the focused change with a conventional, release-worthy `feat:` subject that
+ explains why the example belongs in the repository.
+3. Push the feature branch and open a pull request against `main` with a concise summary,
+ the API contract witnessed, what failure each check catches (and proof you falsified
+ it once), visual notes, and an exact test plan. Label explicitly what was proven by
+ live run versus established by inspection only.
+4. Watch every attached PR check, including validation, manifest/count checks, ecosystem
+ drift, security checks, and Blender 4.5/5.1 smoke jobs. Investigate and fix failures
+ within this change's scope, push fixes, and repeat until all checks pass.
+5. Review PR comments and requested changes. Apply valid feedback and re-run affected
+ checks. Do not merge with unresolved failures or requested changes.
+6. Once the PR is mergeable and green, merge it using the repository's normal merge
+ strategy and delete the remote feature branch.
+7. Wait for any automated release/version-sync commit triggered by the merge, then
+ fast-forward local `main` to `origin/main`, and verify main HEAD is green including
+ all post-merge jobs (release, validate, drift, pages deploy). Do not hand-edit
+ release-owned version fields.
+8. Confirm the final working tree is clean and report the PR URL, merge commit, resulting
+ version (if released), measured check values, and checks completed.
+
+Do not stop at a proposal or ask me to choose the topic. Explore, choose, implement,
+verify, ship, merge, synchronize, and report the completed result. Only stop for a real
+blocker that requires user action, such as authentication, an unavailable required
+runtime with no CI substitute, a merge policy requiring approval, or unrelated local
+changes that cannot be safely preserved.
\ No newline at end of file
diff --git a/scripts/build_gallery.py b/scripts/build_gallery.py
index 9d9bf4d..fd9c4bc 100644
--- a/scripts/build_gallery.py
+++ b/scripts/build_gallery.py
@@ -34,6 +34,60 @@
DATA = REPO / "examples" / "gallery.json"
OUT_DIR = REPO / "docs" / "gallery"
+# Soft cap for gallery index card alt text (accessibility + layout).
+_ALT_CAP = 160
+
+
+def first_sentence(text: str) -> str:
+ """First sentence of *text*, splitting on period-followed-by-whitespace.
+
+ Do not use ``str.split(".")[0]``: bpy teaches strings are full of dotted
+ API paths (``bmesh.ops.create_grid``, ``bpy.context.temp_override``), and
+ that split truncates mid-identifier.
+ """
+ m = re.search(r"\.\s+", text)
+ if m:
+ return text[: m.start() + 1].strip()
+ return text.strip()
+
+
+def card_alt(name: str, teaches: str, *, cap: int = _ALT_CAP) -> str:
+ """Gallery card ``
``: ``{name} — {first sentence}``, length-capped."""
+ first = first_sentence(teaches)
+ if len(first) > cap:
+ cut = first[:cap].rsplit(" ", 1)[0].rstrip(".,;: —-")
+ first = (cut if cut else first[:cap].rstrip()) + "…"
+ return f"{name} — {first}"
+
+
+def assert_alts_survive_dotted_paths(examples: list) -> None:
+ """Fail the build if any card alt is still truncated at the first ``.``.
+
+ Catches regressions of the old ``teaches.split(".")[0]`` bug for every
+ example whose first sentence contains a dotted identifier.
+ """
+ for ex in examples:
+ teaches = ex["teaches"]
+ name = ex["name"]
+ alt = card_alt(name, teaches)
+ first_dot = teaches.find(".")
+ if first_dot < 0:
+ continue
+ # First "." is a real sentence end (EOS or whitespace after it).
+ if first_dot == len(teaches) - 1 or teaches[first_dot + 1].isspace():
+ continue
+ suffix = alt.split(" — ", 1)[-1]
+ if len(suffix) <= first_dot:
+ raise SystemExit(
+ f"gallery alt truncated at dotted API path for {name!r}: {alt!r}"
+ )
+ # Old bug would have produced exactly this string:
+ legacy = f"{name} — {teaches.split('.')[0]}"
+ if alt == legacy:
+ raise SystemExit(
+ f"gallery alt still matches legacy split('.')[0] for {name!r}: {alt!r}"
+ )
+
# ---------------------------------------------------------------------------
# Shared page shell. __ROOT__ is the relative prefix from the page to the
# gallery root ("" for the index, "../" for detail pages); __SITEROOT__ is the
@@ -534,7 +588,7 @@ def build_index(data: dict, *, base: str, repo_root_url: str, site: str) -> str:
cards = []
for ex in examples:
- alt = f'{ex["name"]} — {ex["teaches"].split(".")[0]}'
+ alt = card_alt(ex["name"], ex["teaches"])
cards.append(
CARD
.replace("__TAGS__", html.escape(" ".join(ex.get("tags", [])), quote=True))
@@ -590,6 +644,8 @@ def main() -> int:
print(f"ERROR: no .py script in {ex['dir']}", file=sys.stderr)
return 4
+ assert_alts_survive_dotted_paths(examples)
+
OUT_DIR.mkdir(parents=True, exist_ok=True)
(OUT_DIR / "index.html").write_text(
build_index(data, base=base, repo_root_url=repo_root_url, site=site),