diff --git a/ROADMAP.md b/ROADMAP.md
new file mode 100644
index 0000000..5af8c51
--- /dev/null
+++ b/ROADMAP.md
@@ -0,0 +1,60 @@
+# The Booth — roadmap
+
+Design: [`docs/design/information-architecture.md`](docs/design/information-architecture.md).
+Current version: `0.1.15` (the accreted service, extracted from eshpfi 2026-09-21).
+
+## v1 target
+
+What must be true to cut 1.0. Seven capabilities, each closing a **measured**
+defect — not a wish. The measurements are in the IA doc.
+
+| # | capability | closes | unit |
+|---|---|---|---|
+| 1 | **One item record** — resolved once, carried to gallery, zoom, doc, zip | captions never reach the zoom view (never sent, not lost) | U1 |
+| 2 | **Marks** — `pick` / `note` / `flag`, one primitive, one read path | 5 mechanisms for 1 job; operator→session loop runs through chat | U2 |
+| 3 | **Declared embed seam** — `/_booth/embed.js`, chrome mounts via DOM | 6 regexes injected into arbitrary author HTML, load-bearing for asks | U3 |
+| 4 | **Derived lifetime** — open marks pin; viewing is activity | 54% of booths on the `.forever` escape hatch | U4 |
+| 5 | **Self-announcing booths** — `.booth.json`, provenance on the index | job 5 had no home, so it lived on the link board as 145 dead rows | U5 |
+| 6 | **Benches** — registry, identity, enforced rule, migration | 69% link-board rot; the same bench posted 5× | U6 |
+| 7 | **Navigation at 270 items** — sections, rail, filters, grid keyboard | one flat wall; subfolder structure discarded at render | U7 |
+
+Ordering is dependency-driven, not priority-driven: **U1 → U2 → {U3, U4, U5} →
+U7**, with **U6 independent** of all of them (different storage, different
+surface) and therefore the safest thing to land first or in parallel.
+
+### Explicitly NOT in v1
+
+- **Backward compatibility with the `ask` CLI verbs.** Pre-1.0, and `ask` /
+ `asks` / `answer` become thin aliases over `marks` rather than a second code
+ path. The 17 consuming handles get one althing note naming the change — the
+ one case where telling peers is real coordination and not a broadcast.
+- **A migration that deletes anything.** `links.md` is archived verbatim and
+ committed before the registry is seeded from it.
+
+## Parking lot
+
+Deferred with a home, per the anti-creep gate. Default is park; these were
+weighed against the v1 path and lost on purpose.
+
+| item | why parked |
+|---|---|
+| **Compare mode** — pair-by-name A/B across subfolders | The best idea in the set, and the only one that is a *new capability* rather than a fix for a measured defect. The four-booth `pancake-v3/v4` dance still works. First thing in v1.1. |
+| Virtualized / progressive grid loading | Speculative. 270 `
` may be fine. **Measure the real booth before optimising it** — if it renders inside a second, this is invented work. |
+| Bench uptime history + graphs | The v1 need is "is it dead", which one flag answers. A time series is a different product. |
+| Cross-booth search | No evidence of the need in the usage data. |
+| Per-viewer state (who has seen what) | The Booth has one viewer. Revisit if that stops being true. |
+| Auth | Standing non-goal. LAN/mesh-internal. Blur stays cosmetic and says so. |
+
+## Post-v1, already committed
+
+- **SVOS theme retrofit by `design-dev`.** Runs as a parallel track, not a v1
+ gate: we own the information architecture (it is driven by the measurement
+ above), design-dev owns the visual and interaction system. The handoff is a
+ `/vor-ui` brief written against the landed v1 structure — the same shape
+ `hamr-dev` and `pewpew-dev` used.
+
+## Gate
+
+A proposed feature is on the v1 path or it is parked. **Default: parked.** When
+both are defensible, park it — same asymmetry as the patch-default in SemVer.
+Applies regardless of who proposed it.
diff --git a/booth.service b/booth.service
index 1311910..f1f489e 100644
--- a/booth.service
+++ b/booth.service
@@ -5,8 +5,8 @@ Wants=network-online.target
[Service]
Type=simple
-WorkingDirectory=/home/lkraven/development/eshpfi-management/services/booth
-ExecStart=/home/lkraven/development/eshpfi-management/services/booth/.venv/bin/uvicorn booth.app:app --host 0.0.0.0 --port 8090
+WorkingDirectory=/home/lkraven/development/booth
+ExecStart=/home/lkraven/development/booth/.venv/bin/uvicorn booth.app:app --host 0.0.0.0 --port 8090
Environment=BOOTH_DATA_DIR=/home/lkraven/booth-data
Environment=BOOTH_TTL_HOURS=24
Environment=BOOTH_HOST_LABEL=nh3-dev 10.100.10.50
diff --git a/docs/design/information-architecture.md b/docs/design/information-architecture.md
new file mode 100644
index 0000000..694b964
--- /dev/null
+++ b/docs/design/information-architecture.md
@@ -0,0 +1,252 @@
+# The Booth — information architecture
+
+_Written 2026-09-21, after a measurement pass over the live service. Supersedes
+the accreted model described in `README.md`; the README is rewritten when this
+lands._
+
+## What the Booth is, stated once
+
+**The Booth is the fleet's operator-review surface.** Agents post work; the
+operator looks at it, judges it, and the judgment gets back to the agent.
+
+It was built as a file-shuttle. That was the whim, and the whim was wrong in a
+productive way — the thing people actually needed was the review loop, and they
+built it out of folders because folders were what was on offer.
+
+## The measurement that drove this
+
+Taken 2026-09-21 against the live service and `~/booth-data`:
+
+| observation | number |
+|---|---|
+| rows on the standing link board | 211 |
+| …pointing at a booth that no longer exists | **145 (69%)** |
+| …that are the *same target re-posted* (talk 5×, peedlar 4×) | 22 |
+| …that are genuinely durable targets | 40 (19%) |
+| live booths | 24 |
+| …carrying the `.forever` escape hatch | **13 (54%)** |
+| largest booth | 270 images, rendered as one flat wall |
+| distinct agent handles posting | 17 |
+| commits to ship the asks primitive, in one day | 5 |
+
+Two of those are load-bearing and neither was obvious before counting:
+
+- **69% rot is structural, not drift.** `booth link` is an `O_APPEND` write with
+ no identity and no stated rule, so re-announcing a bench creates a row rather
+ than updating one, and a booth URL rots the moment its booth is swept.
+- **54% kept means the escape hatch is carrying the main load.** A 24h TTL with
+ a boolean override is not "ephemeral with an exception"; it is two lifetimes
+ wearing one lifetime's clothes, and the operator is doing the sorting by hand.
+
+## The defect behind every tacked-on part
+
+> **One lifetime (24h from last touch) and one shape (a folder), serving five
+> jobs with different lifetimes and different shapes.**
+
+Every bolt-on is a correct local fix to that one global mismatch:
+
+| bolt-on | the mismatch it patches |
+|---|---|
+| `.forever` | "this one shouldn't expire" |
+| the link board | "this one isn't a folder" |
+| `inline.py` (a placeholder DSL) | "this one isn't a gallery" |
+| `wrap_verbatim_html` (6 regexes into arbitrary HTML) | "the Booth doesn't own this page" |
+| the floating amber asks chip + `/b//asks` | "the operator can't see the question" |
+
+Five mechanisms exist to get **one question next to one artifact**. That is the
+definition of accretion, and the git log shows its signature: every feature
+ships, then takes 2–5 patches for cases the single shape did not anticipate.
+
+## The five jobs
+
+| # | job | example on the live service | what it needs |
+|---|---|---|---|
+| 1 | **Review a set** | `sindra20-engines` (197 files), `pancake-v3-full` (270) | grid, sections, zoom, filter, flag-the-winners |
+| 2 | **Review a change** | `pancake-v3-full` **vs** `pancake-v4-full` | pairing by name, A/B |
+| 3 | **Read a report** | `pewpew-ui-brief`, `run07-decisions` | prose + figures + nav, chrome the Booth owns |
+| 4 | **Decide** | asks | the question beside the artifact |
+| 5 | **Announce** | *145 dead link rows* | "operator, look at this" |
+
+Job 5 is the one nobody named. The link board was absorbing it: an agent posts a
+booth URL because **a booth has no way to announce itself** — the index card
+shows a name, an item count and a countdown, but not who made it or what it is
+for. Enforcing the link rule without giving job 5 a home just makes it homeless.
+
+---
+
+# The model
+
+Four nouns. Everything else is a view over them.
+
+## 1. Booth — a review surface with a derived lifetime
+
+A folder, as before. What changes is that **lifetime is derived from state, not
+set by a boolean**:
+
+- Default: swept 24h after last activity. Unchanged, and still the contract that
+ means nobody cleans up after themselves.
+- **An open mark pins its booth.** A booth the operator still owes an answer to
+ cannot be swept. (Today it can — a real defect, and one of the reasons
+ `.forever` gets pressed.)
+- **Viewing a booth page is activity.** If he is still looking at it, it is still
+ alive. Deliberate act, so it cannot be triggered by browsing the index.
+- `keep` survives as an explicit, reasoned pin — not as the only way to say
+ "not yet".
+
+> Expected effect: the 54% kept rate falls to the booths that are genuinely
+> durable references. **This is a prediction, and it is measurable** — re-count
+> the `.forever` rate a fortnight after this lands. If it does not move, the
+> diagnosis was wrong and the boolean was doing something else.
+
+Each booth carries `.booth.json`: `{handle, title, why, created}` — written by
+the CLI from `$ALTHING_HANDLE`. **This is what closes job 5.** The index card
+gains provenance and a one-line purpose, and the index becomes the "what landed"
+feed the link board was being used as.
+
+## 2. Item — one record, resolved once, carried everywhere
+
+Today `build_gallery()` folds a `.txt` sidecar into an item — and
+`booth_view_file()` passes only `{name, file, file_url}` to the zoom template.
+**The caption is not lost in rendering; it is never sent.** Same for the doc view.
+
+So: one `booth_items(booth)` resolver returns the full record —
+`{rel, kind, caption, marks, flagged, blurred, section, url}` — and the gallery,
+the zoom view, the doc view, the compare view and the zip manifest all read from
+it. The zoom-loses-annotation bug is then fixed *by construction*, not by
+patching one more template and hoping the next one remembers.
+
+**Sections come from subfolders, which already exist on disk and are currently
+thrown away.** `pewpew-ui-brief` has `integration/` and `blueprint/`;
+`dfa-concepts` has `source/`. `rglob` flattens them into one wall. Rendering the
+structure the poster already created costs nothing and is most of the navigation
+fix.
+
+## 3. Mark — operator judgment attached to a target
+
+The unification, and the reason "comments, votes and asks" stop being three
+tacked-on things:
+
+> **An ask is the session asking the operator. An annotation is the operator
+> telling the session. A vote is the operator pointing at the good ones.** All
+> three are *operator judgment attached to an artifact*. One primitive.
+
+```
+MARK
+ target : the booth, or one item in it
+ shape : pick — one of N options the session declared in advance (was: ask)
+ note — free text the operator volunteered (was: comment)
+ flag — this one / not this one (was: vote)
+ writer : the operator, in the browser
+ reader : the session — `booth marks [--wait]`
+```
+
+One storage model, one read path, one rendering slot (beside the artifact), one
+place where "is anything still open?" is computed. `pick` keeps today's ask
+semantics wholesale, including partial answers and the multi-question form — that
+behaviour is operator-settled (2026-09-09) and is preserved, not redesigned.
+
+`flag` is the one that makes a 270-image booth tractable, and it closes a loop
+that is currently running through chat: `golden-candidates`, `sindra-finalists`
+and the `pancake-*` ladders are all the operator picking winners from a set and
+then *telling the session in conversation*. Flags make that readable by the
+session that posted the set.
+
+## 4. Bench — a running thing, registered
+
+**Not a booth.** The operator's rule, stated by him and now enforced in code:
+
+> Links are for durable middle-to-long-term testing surfaces — jackdaw's current
+> bench, talk's current bench — the things that get promoted to Homepage when
+> fully deployed.
+
+```
+BENCH
+ id : normalized URL (the identity — re-posting UPDATES, never appends)
+ name : what it is
+ owner : the agent handle that registered it
+ state : live → promoted (to Homepage) → retired
+ health : last_checked, last_ok (probed; flagged when dead, never auto-deleted)
+```
+
+- Stored at `~/booth-data/.benches.json` — a dotfile, outside the booth
+ namespace, so it can neither be swept nor mistaken for a booth.
+- `booth bench add ""` upserts on the normalized URL. The 5 `talk`
+ rows and 4 `peedlar` rows collapse to one each, by construction.
+- **`booth link` refuses a `…:8090/b/…` URL** and names the right surface. It
+ survives as a deprecated alias rather than vanishing — 17 handles have the
+ muscle memory, and the teaching moment belongs at the point of use.
+- Liveness is *flagged*, not enforced. A bench that stops answering gets a
+ marker and a date; deleting is the operator's call. Nothing here deletes the
+ operator's data on a timer.
+
+### Migrating the 211 existing rows
+
+1. `links.md` is archived verbatim to `~/booth-data/links/links-archive-.md`
+ **and** committed to this repo. Nothing the operator wrote is destroyed.
+2. The ~40 durable rows seed the registry, deduplicated.
+3. The 145 dead rows and 26 live booth-links are not carried over — the first
+ point at nothing, the second are booths, which the index now announces itself.
+
+---
+
+# The frame: who owns the page
+
+Operator ruling, 2026-09-21: **the page declares itself, the Booth mounts into
+it.**
+
+A report includes one line:
+
+```html
+
+```
+
+…and the Booth's chrome — nav, marks, annotations, the way home — mounts through
+a real DOM API. Marks land at `data-booth-mark=""` anchors, which keeps the
+2026-09-09 ruling that *asks render inline beside the artifact, not on a separate
+page*. If the line is absent, the Booth injects it at **one** insertion point, so
+every existing verbatim booth keeps working untouched.
+
+**What this deletes**, and this is the whole point of the decision:
+
+- `booth/inline.py` — 114 lines of placeholder DSL, entirely
+- `wrap_verbatim_html` and its six regexes against arbitrary HTML
+ (`_HEAD_CLOSE_RE`, `_HTML_OPEN_RE`, `_DOCTYPE_RE`, `_BODY_CLOSE_RE`,
+ `_HTML_CLOSE_RE`, `_ICON_RE`) and the doctype/charset-ordering constraints
+ they are threading
+- `_BACK_CHIP`, `asks_chip` — two floating chips positioned by guessed offsets
+- `GET /b//asks` — the standalone page that existed only because a verbatim
+ booth could not show its own asks
+
+Regex-injecting into arbitrary author HTML is the single most fragile thing in
+the service, and it is load-bearing for the operator's most important workflow.
+A declared seam costs the author one line and removes the whole class.
+
+---
+
+# Navigation
+
+Sized for the real case — 270 items — not the demo case.
+
+- **Sections** from subfolders (free; the data is already there).
+- **A sticky rail**: total, per-section counts, and filters — all / flagged /
+ annotated / unanswered. Jump-to-section.
+- **Keyboard in the grid, not only in the viewer.** Today prev/next exists only
+ *inside* the zoom view: `←/→` move, `f` flags, `n` opens a note, `Enter`
+ zooms, `Esc` backs out. A 270-item review is a keyboard task.
+- **Zoom keeps the item's annotations** — the same item record, so the caption,
+ notes and flag state are all present at full size.
+- **Compare**, detected rather than configured: when two subfolders share
+ filenames, offer pair-by-name A/B. This is the `pancake-v3-full` /
+ `pancake-v4-full` dance — currently four separate booths diffed by eye across
+ two browser tabs — collapsed into one booth with two subfolders.
+
+---
+
+# Non-goals, kept from the original and still right
+
+- **No auth.** LAN/mesh-internal, and blur stays explicitly cosmetic. Anything
+ that must not be seen by whoever can reach `:8090` must not be in a booth.
+- **No database.** The filesystem stays the state; `ls ~/booth-data` still tells
+ you everything. Marks, benches and booth metadata are all flat files.
+- **No upload API for booths.** A session makes a folder. That is the whole API,
+ and it is why every agent family can use this without a client.