# Reader mode — the focus overlay (plan) Status: **draft — recorded 2026-06-14 (Fritz). Next-session candidate.** Fritz: "I really like the layout, it keeps the context nicely. One thing that would help: a quick reader mode that overlays in the middle the document you want to focus on" — since the trail has multiple columns, sometimes you want to sink into one document. ## One-line A **reader overlay**: the focused document rendered centered, at a comfortable measure, over the dimmed trail — context preserved *behind* it, one click (or Back) to return. A lens over the trail, NOT a new pane: the reasoning chain is untouched; you're just reading one link of it up close. ## Why it fits the room The trail's whole thesis is "context is spatial, never amputated." Reader mode honors that: the columns stay on screen, dimmed — you can see where you are, you just zoom your attention. It's the opposite of a modal that hides everything; the context is still there, deliberately. Complements `focus` (which pane gets the margin + live read) with a deeper "solo read" lens. ## THE design decision: reader is URL state, not client state (ADR 0003) Reader mode is a query param on the trail URL — `/t/?reader=` (0-based; omitted ⇒ no overlay; out of range ⇒ ignored). Consequences, all the htmx-hard wins the trail already banks: - **Shareable**: a link can say "here's the chain, start by reading pane 2." - **Browser Back closes it** — temporal history over spatial state, free, no JS. - **Zero client state** — the whole overlay renders server-side from the URL. - Coexists with `?focus=` (reader implies attention on that pane; keep both or fold focus into reader — settle when building). ## Render - The full canvas renders as today, with a **dimmed backdrop** layer over it. - A centered **reader panel** renders the pane's already-fetched Document at the Tufte reading measure (≈60ch body + margin), so it's the same prose the column shows, just unconstrained by column width and centered. - **No extra world read**: reader shows the Document already fetched for that pane (focused-live or cached) — the overlay is pure presentation. - **Margin stays** (status, tags, provenance, backlinks, graph/edit/new/append affordances): reading mode is not a dead-end — you can still branch and act. Possibly collapsible; default shown (the trust layer is the point). - SSR + CSS only; ADR 0003's canvas island stays unspent. Degrades: no CSS ⇒ the doc just renders after the canvas; no JS ⇒ close via the link / Back. ## Affordance + close - Each pane (header/spine) gets a **"reader"** control → link to `trailURL?reader=`. - Close: an explicit **✕** AND the **backdrop itself** are `` links back to the bare trail URL (no reader param). Optional **Esc-to-close** via a one-line _hyperscript snippet (an ADR 0003-sanctioned tiny concession) — or rely on Back. Recommend the snippet; it's the expected reader-mode gesture. ## Load-bearing UX decision: does reader persist across link clicks? - **RECOMMENDED — persist:** a link clicked *inside* the reader continues the trail AND stays in reader on the newly-focused pane (read straight through the argument without surfacing). Implemented by the reader panel's links using `trailAfterClick` then setting `reader=`. This is the feature's whole point ("focus on the document you want," then the next). - Alternative (simpler v1): links exit reader to the canvas, focused on the new pane. Less magical; falls out for free. Decide with Fritz; recommend persist. ## Architecture (web-only — no port/service/domain change) - `trail.go`: parse the `reader` query param into the `trail` struct (e.g. `Reader int`, -1 = none); `trailURL` optionally encodes it; a helper builds the reader/close links. The codec stays the single source of truth. - `trail_handlers.go`: when `Reader` is set and in range, render the canvas plus the reader-overlay view model (reuse the focused pane's Document; for persist-on-navigate, rewrite the reader panel's links to carry the new reader index). - Templates: a `reader` overlay block (backdrop + centered panel reusing the doc-body + doc-meta partials). CSS: backdrop, centered panel, dim. - Restrict to prose panes (doc/listing/tag); a floor/graph pane in reader is just a bigger SVG — allow later if wanted, not v1. ## Tests Codec round-trip with `?reader=`; overlay renders the addressed pane; ✕/backdrop link back to the bare trail; persist-on-navigate link rewriting; out-of-range ignored; no-reader URL renders exactly today's canvas (no regression); auth-gated affordances still present in the reader margin. ## Scope / non-goals - A presentation lens, not a pane kind — the trail path never changes. - No client zoom/pan, no JS reader engine (htmx + CSS + at most a 1-line Esc hyperscript). - Doc/listing/tag panes only for v1. ## Open before building (settle with Fritz) 1. Persist-on-navigate vs exit-on-navigate (recommend persist). 2. `?reader=` as its own param vs folding `focus` into it. 3. Esc-to-close hyperscript vs Back-only. 4. Margin shown vs collapsible in reader. This is a self-contained next-session feature: web-layer only, no new reads, no broker/deploy coupling. Slots into the Reading Room family as "R4 — reader mode." --- ## Decisions SETTLED + BUILT (2026-06-15, branch feature/reader-mode) Fritz settled all four open questions — each matched the plan's recommendation: 1. **Persist-on-navigate** — a link inside the overlay continues the trail AND stays in reader on the newly focused pane. 2. **Standalone `?reader=`** — its own query param, orthogonal to `?focus=`. 3. **✕ + backdrop + Esc** — all three close; Esc via a 4-line vanilla handler in islands.js (NOT `_hyperscript` — pulling a ~40KB lib for one keybinding is worse than 4 lines in the ADR-0005-sanctioned canvas JS file). 4. **Margin shown** by default in the overlay (the trust layer is the point). ### Load-bearing implementation decision: reader implies focus A valid `?reader=` also takes `Focus := i` in `parseTrail`. This keeps the single-live-read invariant (ADR 0005 d9): the overlay reuses the **focused pane's** already-fetched Document — no second world read — and so always has the full margin. Honors "standalone" (reader is its own param, present/absent independently of focus) and the shareable "read pane 2" use case (`?reader=2` focuses + overlays pane 2). Out-of-range / non-prose (floor, graph) / junk ⇒ no overlay (-1), never a 400 — a stale shared link degrades to the canvas. ### Web-only build (no port/service/domain/broker/deploy change) - `trail.go`: `trail.Reader int` (-1 = none); `parseTrail` gains a 3rd arg (readerParam); new `trailReaderURL(t, reader)` — the ONLY builder that emits `?reader=`; `trailURL` stays reader-free (canonical canvas URL, so every existing click closes the overlay). `trailAfterClick`/`trailFocused` set `Reader: -1`. - `trail_links.go`: `trailizeLinks`/`trailizeNode` gain `reader bool` — prose targets (doc/tag) persist the overlay, non-prose (graph/floor) exit it. - `trail_handlers.go`: `paneView` gains `reader bool` (mode "reader"; skips RecordLinks on the overlay copy; backlinks persist, graph/map exit; no self-referential reader affordance in the overlay). Trail handler captures the focused doc and builds `vm.Reader *paneVM` + `vm.CloseURL` (bare trail). `ReaderURL` added to `paneVM` AND the single-doc page VM (unused there — the shared `doc-meta` template references it). - Templates: `reader` overlay block (backdrop scrim + centered panel reusing doc-body + doc-meta) in canvas.html; "reader" affordance in the pane-head and the margin escape line (content.html). - islands.js: Esc-to-close (clicks the close link; hx-boost keeps it a swap). - page.html: reader CSS (fixed backdrop, scrim-link behind panel, centered Tufte-width panel, dark-mode aware). Tests: codec (reader takes focus, reader>focus precedence, tag overlays, out-of-range/negative/junk/absent → -1, floor+graph rejected, trailReaderURL round-trip + close URL); handler (overlay renders + canvas behind + margin ×2, no-reader regression, out-of-range ignored, persist-on-navigate link rewrite, auth affordances present + no self-reader-link in overlay). Gate green (./pre-commit.sh, lint 0 issues). **Live QUIC smoke passed**: overlay renders only on valid prose `?reader=`, body links persist `?reader=`, scrim/✕ → bare trail, floor + out-of-range ignored. Gotcha: a `strings.Contains(body, "reader-panel")` negative assertion false-matched the `.reader-panel` CSS rule (the canvas page inlines all CSS). Fix: assert the rendered element `class="reader-backdrop"`, not the class name. NOT committed (Fritz commits). Next: Fritz + CodeRabbit (`@coderabbitai review`) → merge (library 0.1.15) → deploy bump (reader-only, no broker/worlds/ agent change).