--- contract_version: "0.1" status: "PROPOSED 2026-09-23 by design-dev, from operator rulings relayed by booth-dev the same day (thread 01M38BJ30WVQT870MS6WGM49EK): blur=A; three Desk-row rulings; a theme toggle. Contract panel folded. Both open points answered by the operator (thread 01M38CT9DH2N3Z4FSJ0MNE4DR1): × hides (A), and the theme reaches inside verbatim pages. DELIVERED IN TWO MERGES, blur first (operator: 'per booth blurring is now important since we are showing up to 4 images'): merge 1 = D2 + D2b, merge 2 = D1 + D3." module: "templates + base.html CSS/JS + the vendored token sheet (the Desk row, Reveal all, the theme toggle)" purpose: "Three operator rulings, one contract. THE DESK ROW: kept vs ephemeral reads at a glance; download/keep/release appear only on hover, at no space cost; the zip link leaves the middle. REVEAL ALL: one control reveals every blurred item in a booth for the life of the tab. THE THEME TOGGLE: System / Light / Dark at the top of every page." depends_on: - "booth.items.booth_items + Item.blurred (INV-1 of r2: the one resolver). Reveal all reads Item.blurred and nothing else. booth-dev is adding a booth-level blur flag that feeds Item.blurred (composes with `.blurred`, never overrides); this contract needs no change when it lands." - "templates/_lifetime.html `lifetime(kept, hold, expires_in)` — its OUTPUT is unchanged; the Desk wraps it." - "booth.app.index / list_booths row fields `kept`, `hold`, `expires_in`, `name`, `name_url`, `count`, `flags`, `marks_open`, `uploaded` (unchanged)." - "booth.app.booth_view / booth_view_file contexts (`name`, `items`, the review ring)." - "the in-place client in base.html (r2 C3): POSTs a form, re-fetches the CURRENT URL and swaps its `data-region` elements. It never navigates — every change of page, booth to booth included, is a full load — and it never touches , the top bar, or anything outside a region." - "booth.items.is_booth_blurred(booth) + BOOTH_BLUR_FILE `.blurbooth` (booth-dev, c1108a1): the whole-booth blur marker. Fails toward BLURRED on an unreadable read." - "POST /b/{name}/blurbooth with `on=1|0` and optional `back=` → 303 to the booth, or to `view?f=` (booth-dev, c1108a1). Not a mark route: no 204, always the 303." language: "jinja + css + a little javascript" complexity: "medium" estimated_loc: 350 confidence: 0.7 touches: - "booth/templates/index.html (the row: facts line, lifetime pill, the hover cluster, the `blurred` badge; the confirm script unchanged)" - "booth/app.py (READS only, no new route: `booth_blurred` in the booth_view and booth_view_file contexts and on each list_booths row; `blurred_self` on each gallery dict from build_gallery's one `read_blurred`)" - "booth/templates/base.html (Desk row CSS; reveal-all CSS; the theme toggle markup in the top bar; the early script; the toggle script)" - "booth/templates/booth.html (Reveal all in the booth header; per-tile reveal defers to it)" - "booth/templates/view.html (Reveal all in the review; the stage reveal defers to it)" - "booth/templates/_svos_tokens.css (RE-VENDORED at the same SVOS SHA ed2f8d8 with a new scoping transform; no value changes)" - "booth/static/embed.js (the `.bk-ask` colours follow the theme choice; D3)" - "tests/test_flow_browser.py, tests/test_flow.py (new tests; two assertions change, see below)" - "tests/mutations/r2_flow.toml (rows whose anchors this moves are re-aimed, never deleted without a replacement)" assumptions: - "ONE VIEWER, per r2. A reveal and a theme are per-browser; the server stores neither." - "EVERY JUDGMENT WORKS WITH JAVASCRIPT OFF (INV-3 of r2). Keep, release, wipe and zip stay plain forms and a link. Reveal all and the toggle are JS-only affordances and do not render without JS." resolved_questions: - "OPEN-1: does × (wipe) hide until hover with download/keep/release? ANSWERED YES by the operator (2026-09-23), the recommendation." - "Does the theme toggle reach the ask chrome (`.bk-ask`) inside verbatim pages? ANSWERED YES by the operator: 'theme toggle reaches inside'." --- # R2b — the Desk row, Reveal all, the theme toggle ## D1 — the Desk row The operator, verbatim: *"let's make it obvious which are kept and which are ephemeral"*; *"the zip download button is in between keep/release and wipe, and looks awkward"*; *"let's have the download, keep and release buttons only appear on mouseover"*. - **The lifetime is a pill in the row's right column, always visible.** It is state, not a control, so it stays when the controls hide, and the right column scans down the Desk as one column of state. - `life-kept` when `b.kept`: sage (SVOS: a judgment made), prefixed `★`. - `life-held` when not kept and `b.hold` is `open` or `unreadable`: amber. - `life-count` otherwise: neutral outline, prefixed `◷`. - The pill wraps `lifetime(...)`, whose output is unchanged; the class is chosen from `kept`/`hold` alone. - The badges (open count, new, pickup, marks unreadable) stay in the same column, above the pill. The column always renders now, because every row has a lifetime. - **The facts line is facts only**: item count and flag count. The lifetime and every control leave it. - **The controls are one cluster, `.desk-acts`, in this order:** `⬇ zip`, then `★ keep` or `release`, then `× wipe`, set apart from the other two. Zip leaves the middle; release stays next to × (the operator's earlier "x next to release"). - **Where a real hover exists, the cluster takes no room.** "Real hover" is `(hover: hover) and (pointer: fine)` with NO coarse pointer present (`any-pointer: coarse` does not match). A touch laptop reports a mouse, but a finger on it cannot hover, so it gets the touch treatment below. - At rest the cluster is absolutely positioned over the top-right corner of the row's preview strip, at opacity 0 and `pointer-events: none`. On row `:hover` or `:focus-within`, BOTH are restored: opacity 1 and `pointer-events: auto`. A visible control that cannot be clicked is a defect. - It covers pictures, never information: its box never intersects `.desk-main` or `.desk-side` at any width. - Keyboard: the controls stay in the tab order while hidden (opacity, never `visibility`/`display`), and focusing one reveals the cluster. - At ≤700px the strip is the row's first line, full width, and the text column, pill and badges wrap below it. The cluster sits at the strip's top-right, which is still picture. - **Everywhere else (no hover, a coarse primary pointer, or any coarse pointer present), the cluster is visible and in flow**, on its own line at the bottom of the row. The mechanism: the strip and the cluster share one `.desk-media` box. Under a real hover it is the positioning box; otherwise it dissolves (`display: contents`) and the cluster becomes the row's last line. - Known limit: the touch-LAPTOP branch (a fine primary pointer plus a coarse one) cannot be emulated, because Chromium's touch emulation makes the primary pointer coarse. That branch is covered by reading the media query, not by a test. Hover-only would mean no controls at all on touch. Every control there is at least 28px square (r2's Slate T2 floor). - **× hides with the others** (OPEN-1, answered yes). A visible × on every row would be a standing invitation to the one irreversible action. A visible × on every row is a standing invitation to the one irreversible action, and hiding the safe controls while the destructive one stays inverts the priority. It appears with the cluster, last, set apart. - Unchanged: the forms, their POST targets, `data-confirm`, `data-booth`, the confirm script and its `shown()`, the ≥28px coarse-pointer floor, and "no page scrolls sideways at any width". ### D1b — dates (operator, added 2026-09-23: "I think I want creation and update dates on the booths now too") Merge 2, with the row. booth-dev has put both on the record (thread 01M38D39ANKF2TW2F15TEYJ1GT): - `created_at` is the directory's birth time via `statx`, a float epoch, or None when the filesystem cannot say. None renders as NOTHING, never a guess. - "Updated" is `landed_at`, the content clock the "new" section already reads. They render on the Desk row's facts line and in the booth header's status line as dated FACTS, a different kind of thing from the lifetime pill (state) and the controls (actions). One macro, `_dates.html`, serves both: - **created** is a DATE, "created 12 Sep" (with the year only when it is not this year's); - **updated** is an AGE, "updated 5d ago", measured from ONE clock per page (`now` in the context), so every row is measured from the same instant; - each is a `