diff --git a/services/booth/README.md b/services/booth/README.md
index 302b163..3eb693e 100644
--- a/services/booth/README.md
+++ b/services/booth/README.md
@@ -161,12 +161,32 @@ Rules of the primitive:
- **A broken ask is shown as broken**, not hidden: if the JSON does not
validate, the page says why, so a session never thinks it posted a question
the operator cannot see.
-- **A booth with its own `index.html` shows a chip, not the panel.** That page is
- served verbatim by design, so the inline panel cannot appear on it: the Booth
- injects an amber `? N open asks` chip (next to the back chip) linking to
- **`/b//asks`**, a standalone page carrying the real forms. Answering
- there returns there. Put the media in the booth and the ask beside it either
- way — the ask is never lost, whichever shape the booth takes.
+- **A booth with its own `index.html` gets the ask INLINE, where you put it.**
+ That page is served verbatim, so the Booth substitutes placeholders in your
+ markup rather than rendering the panel above a gallery it does not have:
+
+ ```html
+
+
+
+
+ ```
+
+ Per-question fragments bind to **one** form with the HTML5 `form=` attribute,
+ so a four-voice audition can put each radio group under that voice's audio and
+ still submit all four picks in a single POST — which is what a multi-question
+ ask requires. The fragments ship their own scoped styles and inherit nothing
+ from your page. No JavaScript.
+
+ ⚠ Put the placeholder **outside** any CSS grid or flex container, or it
+ becomes a cell in it. A sibling of the block it belongs to is right.
+
+ Placement is optional: a page with no placeholders gets the whole ask appended
+ at the end, so an ask is never invisible — markup only moves it somewhere
+ better. Mark up some questions and not others and the rest are appended too,
+ because a multi-question form that is missing a question is a 400 the operator
+ would only meet after filling it in. The amber chip stays as a jump link to the
+ first open ask, and `/b//asks` still renders every ask on its own page.
- Ask/answer files are not gallery items and do not count toward the booth's
item count; they render as the panel above the gallery. Answering bumps the
booth's mtime, so it lives another TTL — the session has 24h to read it.
diff --git a/services/booth/booth/app.py b/services/booth/booth/app.py
index aa508b6..435081c 100644
--- a/services/booth/booth/app.py
+++ b/services/booth/booth/app.py
@@ -86,6 +86,11 @@ from booth.asks import ( # noqa: E402
valid_stem,
write_answer,
)
+from booth.inline import ( # noqa: E402
+ form_id as ask_form_id,
+ has_placeholders,
+ place as place_asks,
+)
from booth.links import ( # noqa: E402
LINK_LOCK,
LINKS_FILE,
@@ -399,12 +404,13 @@ _BACK_CHIP = (
# say so (found 2026-09-09 on `emmie-anchor`: valid ask, CLI listed it, page
# showed nothing). Same injection mechanism as the back chip; it links to the
# standalone /asks page, which renders the real forms.
-def asks_chip(name: str, open_count: int) -> str:
+def asks_chip(name: str, open_count: int, href: str | None = None) -> str:
if open_count < 1:
return ""
label = f"? {open_count} open ask" + ("" if open_count == 1 else "s")
+ href = href or f"/b/{quote(name, safe='')}/asks"
return (
- f'{label}'
+ f'{label}'
"
+{% endmacro %}
+
+{# One question's radio group, bound to the shared form by id. #}
+{% macro question(a, q, form_id, name_url, standalone=False) %}
+{% set field = 'choice.' ~ q.key if a.multi else 'choice' %}
+{% set qa = (a.answer.answers.get(q.key) if a.multi else a.answer) if a.answer else None %}
+
+ {% if qa %}✓ answered{% else %}? your pick{% endif %}
+