The heid bug-hunt on r2b merge 1 found the /blur route stripping `f` before
writing, so the form for " a.png" blurred its neighbour "a.png". The route was
only half of it: `.blurred` was one stripped rel per line, so no writer could
store a rel with a leading space or a newline, whatever the route did.
Operator-ruled 2026-09-23 ("fix the blur").
- booth/blur.py (new, stdlib-only): read_blurred / set_blurred / BLUR_FILE.
`.blurred` is now a JSON array in sorted order, the `.seen` shape: opened
O_NOFOLLOW | O_NONBLOCK with an S_ISREG check and a 1 MiB cap, so a planted
symlink is refused and a FIFO can no longer hang every Desk render (the old
read_text() blocked on one). Writes go through mkstemp + os.replace. The
legacy line format is still READ, so the 6 live line-format files keep their
blur until their next write upgrades them. Measured before the change: 42
live rels, none with edge whitespace, so the defect had no live victims.
- The route no longer strips `f`.
- scripts/booth `blur`/`unblur` go through booth.blur.set_blurred instead of
their own grep/printf line writer. Two writers of one format is how the
formats drift, and after this change the shell writer would have appended a
line to a JSON array. Every path is checked before anything is written.
- Item.blurred_self (appended to the record): the item's own blur, resolved in
booth_items from the same read as `blurred`. It replaces build_gallery's
second read_blurred, which a write between the two reads could split
(invariant 3). app.py no longer reads blur state at all, and a test asserts
it.
Names stay importable from booth.app and booth.items (invariant 4). blur joins
test_stdlib_only. test_cli's per-item-survives test now reads through the reader
rather than asserting the old byte format. The r2b contract and its mutation
row follow blurred_self onto the record. tests/mutations/blur_storage.toml
proves 12 falsifiers by running the change each forbids.
Not in this change, and still ours: the "off"-means-ON idiom drift between
/blur, /blurbooth and /flag (forms only ever send 0/1), and the CLI's
`.blurbooth` touch following a symlink where the service no longer does.
370 lines
25 KiB
Markdown
370 lines
25 KiB
Markdown
---
|
||
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 <html>, 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=<rel>` → 303 to the booth, or to `view?f=<rel>` (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, read off `Item.blurred_self` — moved onto the item record by booth-dev 2026-09-23 so blur state has one reader, invariant 3)"
|
||
- "booth/templates/base.html (Desk row CSS; reveal-all CSS; the theme toggle markup in the top bar; the early <head> 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 cluster is LAST in the row's markup**, so the booth's name comes first
|
||
in tab order and wipe comes last. Where a real hover exists it is placed over
|
||
the strip from the row's own box: the strip keeps to the row's top, 210px
|
||
wide from the row's 12px padding.
|
||
- Accepted: the hover query uses Media Queries 4 `not (...)`. An engine
|
||
without MQ4 drops the whole query, and the failure is the safe
|
||
direction: the controls show, in flow.
|
||
- 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 `<time>` with `datetime` (ISO) and its exact local stamp as the
|
||
title;
|
||
- **updated shows whenever it differs from created by a minute or more,
|
||
EITHER way.** Copied files keep their mtimes while the folder is born now,
|
||
so content can be older than its booth. Within a minute, one date;
|
||
- a content clock AHEAD of now is said as its DATE, never as an age ("updated
|
||
just now" would be false);
|
||
- an age is in its largest whole unit, a day being 24h;
|
||
- a date the filesystem cannot give, or the calendar cannot hold, renders
|
||
NOTHING. The filters never raise: the Desk renders every row in one
|
||
response, so one unrenderable clock would otherwise 500 the index for every
|
||
booth.
|
||
|
||
## D2 — Reveal all (blur ruling A)
|
||
|
||
- **One STATE per booth, shown by a control in two places: "👁 reveal all —
|
||
blur is cosmetic" / "🙈 blur again".** It appears in the booth header, the
|
||
review's top bar and a blurred doc's own top bar. Every instance sits OUTSIDE
|
||
every `data-region`, so no in-place swap replaces it: its state lives in the
|
||
tab, and a swap must never reset it. Below 600px it reads "👁 reveal all";
|
||
its title still says the blur is cosmetic. The server puts the control in the markup only
|
||
where it can act, and always with the `hidden` attribute: in the header when
|
||
any item of the booth is blurred (`Item.blurred`), and in the review when any
|
||
item of the review RING is (a blurred doc is not on the review page, so a
|
||
control there would act on nothing). The script removes `hidden` and binds it. Without
|
||
JS it is in the markup but never shown.
|
||
- **State: `sessionStorage["booth.reveal:" + <booth name>] = "1"`.** Per booth,
|
||
per tab, gone when the tab closes, so a blurred booth is blurred again next
|
||
time. Nothing reaches the server.
|
||
- A READ that throws (a private window, blocked site data) reads as "not
|
||
revealed".
|
||
- A WRITE that throws still applies the click to the page in front of you:
|
||
it is only not remembered for the next page. A control that does nothing
|
||
when clicked is a defect. Neither case ever raises.
|
||
- **The mechanism is one class on `<html>`, `reveal-all`.** CSS lifts the blur
|
||
under it on every booth surface: tiles, the flag tray, the filmstrip and the
|
||
review stage. `<html>` is outside every `data-region`, so an in-place swap
|
||
can never drop it.
|
||
- `<html data-booth="<name>">` is on EVERY page rendered for one booth: the
|
||
booth page, the review, the doc view and the marks page. It is an
|
||
autoescaped attribute, read by `getAttribute` and never templated into
|
||
script. An early `<head>` script adds `reveal-all` before first paint when
|
||
that booth's key is set, so a revealed booth does not flash blurred on the
|
||
next page of the reel.
|
||
- Because every change of page is a full load, the class is re-decided per
|
||
page, from that page's `data-booth`. Booth A's reveal cannot follow you
|
||
into booth B.
|
||
- The index's `<html>` carries no `data-booth` (its rows' own `data-booth`
|
||
attributes are unrelated), so **nothing on the index is revealed by D2**,
|
||
the Desk strip included.
|
||
- **A board holding files gets both controls.** Only the one-click wipe is
|
||
board-suppressed; an item's "◉ booth" label points at the header control,
|
||
so the control must be there.
|
||
- **The full-page doc view is blurred honestly.** A blurred doc's own page
|
||
renders its body blurred, with its own JS-only reveal; Reveal all lifts it
|
||
by the same `<html>` class.
|
||
- **The per-item reveal defers to it, BY STYLESHEET.** Under `.reveal-all` the
|
||
per-tile and stage reveal buttons are `display: none`. That is a CSS
|
||
consequence of the class, so markup swapped in after a save obeys it with no
|
||
script. Reveal all never touches an item's own `revealed` class: "blur
|
||
again" returns every item to exactly the per-item state it had, an item
|
||
revealed on its own staying revealed.
|
||
- INV-8 of r2 holds: the server renders every blurred item blurred; the reveal
|
||
stays per-browser and client-side; the copy keeps saying it is cosmetic.
|
||
|
||
## D2b — the booth blur toggle (added after the contract panel was dispatched)
|
||
|
||
booth-dev landed the whole-booth marker and its route while the panel was
|
||
reading; this is the control the operator uses, which the blur ruling assumed.
|
||
|
||
- **"◌ blur booth" / "◉ booth blurred" in the booth header and the review's
|
||
top bar (`.vbar`)**, each wrapped in its OWN region, `blur-booth`. Its label is
|
||
server state, so an in-place save refreshes it with everything else; a fog
|
||
set elsewhere since the page loaded would otherwise leave it saying "blur
|
||
booth". It is a plain `<form method=post action=/b/<name>/blurbooth>` with
|
||
`on=1|0`, so it works with scripts off (INV-2). From the review it carries
|
||
`back=<rel>`. The route lands on the review only when `back` is an item of
|
||
the review ring, and otherwise on the booth page; the landing is built from
|
||
the ring, never echoed.
|
||
- **Fogging never writes through a link.** booth-dev's `set_booth_blurred` used
|
||
`touch()`, which followed a planted `.blurbooth` symlink: a click of this
|
||
control rewrote an outside file's mtime, or created a dangling target. Any
|
||
entry already at the name reads as fogged, so nothing is written; otherwise
|
||
the marker is created with `O_CREAT | O_EXCL | O_NOFOLLOW`.
|
||
- **Space never hijacks a focused control.** The review's Space-to-advance
|
||
ignores a focused button, link or summary, so a keyboard can press these
|
||
controls.
|
||
- **Its state comes from the server**, never from the client:
|
||
`booth_blurred = is_booth_blurred(booth)` in both contexts. The label says
|
||
what IS, and pressing it flips it.
|
||
- **The Desk row carries a `blurred` badge** when `booth_blurred`, so a fogged
|
||
strip says why. It is one `is_booth_blurred` call per booth in the pass
|
||
`list_booths` already makes. The badge is information: it is not the
|
||
control, and it does not hide on hover.
|
||
- It is independent of Reveal all. Fogging a booth sets server state for every
|
||
viewer; Reveal all lifts the fog for one tab.
|
||
- **Each item's own blur control tells the truth under a fogged booth.**
|
||
`Item.blurred` is the COMPOSED fact (own OR booth). The per-item form changes
|
||
only the item's own entry in `.blurred`, so the gallery also carries
|
||
`blurred_self`, from `Item.blurred_self` (the same single read `booth_items` makes for `blurred`).
|
||
- An item blurred only because the booth is shows "◉ booth", a label with no
|
||
form, pointing at the header. A per-item un-blur there would be overridden
|
||
by the booth flag and visibly do nothing.
|
||
- An item blurred on its own keeps its "◉ blurred" un-blur.
|
||
- Found by rendering the built page, not by any review.
|
||
|
||
## D3 — the theme toggle
|
||
|
||
- **System · Light · Dark, in the top bar of every page.** A segmented control
|
||
of three buttons with `aria-pressed`. The top bar is outside every
|
||
`data-region`, so no swap replaces it. It is in the markup with `hidden`,
|
||
and the script removes that and binds it.
|
||
- **State: `localStorage["booth.theme"]` ∈ {`light`, `dark`}; absent = System.**
|
||
The opposite lifetime to Reveal all, deliberately: a theme should outlive the
|
||
tab, a reveal must not. A READ that throws reads as System. A WRITE that
|
||
throws still applies the choice to this page, and it is only not
|
||
remembered.
|
||
- **Mechanism: `data-theme` on `<html>`.** Absent = the OS preference, exactly
|
||
today's sheet. An early `<head>` script sets it before first paint, so a
|
||
forced theme never flashes the other one.
|
||
- **A choice made in another tab moves every open Booth page** (the `storage`
|
||
event), as it moves the ask chrome.
|
||
- **System is live-following BY CONSTRUCTION.** Choosing System removes
|
||
`data-theme`, and the `prefers-color-scheme` media query takes over. A media
|
||
query tracks the OS live, so no `matchMedia` listener is needed: JS never
|
||
computes the theme.
|
||
- **The token sheet is re-vendored at the same SVOS SHA (ed2f8d8) with a new
|
||
scoping transform, and no value changes.** The complete selector list:
|
||
|
||
| block | selector |
|
||
|---|---|
|
||
| primitives, dark, art layer | `:root` (unconditional: dark is the default, and what forced dark leaves standing) |
|
||
| light + art-light | `@media (prefers-color-scheme: light)` → `:root:not([data-theme="dark"])` |
|
||
| light + art-light | `:root[data-theme="light"]` |
|
||
| dark-hc | `@media (prefers-contrast: more)` → `:root` |
|
||
| light-hc | `@media (prefers-contrast: more) and (prefers-color-scheme: light)` → `:root:not([data-theme="dark"])` |
|
||
| light-hc | `@media (prefers-contrast: more)` → `:root[data-theme="light"]` |
|
||
|
||
- **Forced dark** excludes both light rows, so the unconditional dark block
|
||
stands, with dark-hc under more contrast.
|
||
- **Forced light** matches the bare light row at specificity (0,2,0), which
|
||
beats dark-hc's `:root` (0,1,0). The light-hc row then applies under more
|
||
contrast.
|
||
- **The preservation check runs in BOTH directions at vendoring time**:
|
||
every declaration of the old sheet is in the new, and the new has none the
|
||
old lacked. The committed test checks each re-scoped copy against the
|
||
UNMOVED dark block. SVOS's light and dark declare the same 42 properties,
|
||
and its dark-hc and light-hc the same 41, so a declaration the transform
|
||
drops fails it. Light declares every dark-block property PLUS the art
|
||
layer's four light-only values (the three shadows and the armed glow).
|
||
That extra set is written in the test from SVOS, never derived from the
|
||
copies, so dropping it from both copies fails too.
|
||
- **No JS: no toggle, and the page follows the OS**, as today.
|
||
- **The toggle reaches inside verbatim pages** (operator: "theme toggle reaches
|
||
inside"). `embed.js` reads the same `localStorage["booth.theme"]` (the same
|
||
origin) and marks each `.bk-ask` IT MOUNTED (never an author's own element
|
||
of that class) with `data-bk-theme`. Its
|
||
colours follow that attribute exactly as the Booth's own sheet follows
|
||
`data-theme`: forced when set, OS when absent. It follows a change made in
|
||
another tab through the `storage` event. It sets nothing on the host page's
|
||
own `<html>`: the author's page is not ours to theme, only our guest chrome
|
||
inside it. If a forced theme makes the chrome look actively broken against
|
||
a host page, that goes back to the operator rather than being absorbed.
|
||
|
||
## Invariants
|
||
|
||
- **INV-1 — nothing new on the server beyond READS.** No route and no file are
|
||
added: `booth_blurred` in two contexts and on the Desk row (`is_booth_blurred`),
|
||
and `blurred_self` per gallery item (`Item.blurred_self`, from `booth_items`' one read). D2 and D3 are per-browser state; D1 is markup and CSS.
|
||
- **INV-2 — JS-off parity (r2 INV-3).** Every control on the row works with
|
||
scripts off. Reveal all and the toggle do not render without JS. The page
|
||
follows the OS.
|
||
- **INV-3 — no reserved room for a hidden control** where a real hover exists.
|
||
The box of every element in the row other than the cluster — the strip, each
|
||
preview image, the text column, the side column, the pill — is identical
|
||
with the cluster present or removed.
|
||
- **INV-4 — blur honesty (r2 INV-8).** Nothing on the index is revealed by D2.
|
||
- **INV-5 — autoescape.** The booth name reaches the reveal-state machinery
|
||
only as an escaped attribute value (`data-booth`), read by `getAttribute` and
|
||
never templated into a script.
|
||
- **INV-6 — no flash.** A forced theme and a set reveal are applied before
|
||
first paint.
|
||
|
||
## TESTS
|
||
|
||
- `the_row_controls_take_no_room_where_a_hover_exists` [tracer]: at rest the
|
||
cluster is at opacity 0 and cannot be clicked. On row hover it is at opacity 1
|
||
and a click on keep reaches the server. The box of every other element in
|
||
the row is identical with the cluster removed. The cluster's box never
|
||
intersects the text or side column, at 390 / 720 / 1000 / 1400px.
|
||
- `on_touch_the_row_controls_are_visible_in_flow_and_at_least_28px`: a touch
|
||
context (no hover, coarse pointer).
|
||
- `the_lifetime_pill_class_is_kept_held_or_counting`: the class is chosen by
|
||
state, the lifetime words are unchanged, and the pill is visible with no
|
||
hover.
|
||
- `the_row_controls_run_zip_keep_or_release_then_wipe`.
|
||
- `reveal_all_reveals_every_blurred_surface_and_survives_the_next_page`: tiles
|
||
and tray on the booth page, stage and filmstrip on the review, across a
|
||
navigation in the same tab; a fresh tab (new context) is blurred again.
|
||
- `reveal_all_never_reaches_the_desk`.
|
||
- `the_booth_blur_toggle_works_without_js_and_lands_back_on_the_review`: header
|
||
and review forms POST `/blurbooth`; the label follows `is_booth_blurred`; the
|
||
review form carries `back`; the Desk row shows `blurred`.
|
||
- `reveal_all_survives_an_in_place_save`: after a save, the blur is still
|
||
lifted, the control still reads "blur again" and still works, and the
|
||
per-tile buttons are still hidden.
|
||
- `reveal_all_on_booth_a_does_not_reveal_booth_b`.
|
||
- `blur_again_restores_each_items_own_reveal`.
|
||
- `reveal_all_is_absent_without_blurred_items_and_hidden_without_js`: no markup
|
||
when nothing is blurred; with blurred items, the markup carries `hidden` and
|
||
a JS-disabled context never shows it.
|
||
- `a_storage_failure_still_applies_the_click`: sessionStorage and localStorage
|
||
throwing on write; the reveal and the theme still apply to the page.
|
||
- `the_theme_toggle_forces_light_and_dark_and_system_follows_the_os_live`:
|
||
pressing Light/Dark changes `--surface-base` and survives a reload (new page,
|
||
same context); System plus an emulated OS scheme flip changes it WITHOUT a
|
||
reload.
|
||
- `a_forced_theme_follows_high_contrast`: under `prefers-contrast: more`,
|
||
forced dark resolves exactly what OS dark does, and forced light what OS
|
||
light does. The check reads tokens that DIFFER between a theme and its
|
||
high-contrast variant (`--text-faint`, `--border-default`; `--surface-card`
|
||
is the same in both and would prove nothing), and asserts that high contrast
|
||
actually changed them.
|
||
- `a_forced_theme_and_the_os_theme_are_the_same_declarations`: each light copy
|
||
equals the other and declares the dark block's property set plus the four
|
||
art-light values; each light-hc copy equals the other and declares exactly
|
||
the dark-hc block's.
|
||
- `created_and_updated_are_dated_facts_and_none_says_nothing`,
|
||
`updated_shows_whenever_it_differs_from_created_and_a_future_one_says_its_date`,
|
||
`a_date_no_calendar_can_hold_renders_nothing_and_never_500s`,
|
||
`an_age_is_said_in_its_largest_whole_unit` (D1b).
|
||
- `a_rows_booth_name_comes_before_its_controls_in_tab_order`,
|
||
`the_pill_shows_at_rest_and_focus_reveals_the_controls`,
|
||
`with_scripts_off_a_rows_controls_still_act` (D1).
|
||
- `a_theme_chosen_in_one_tab_moves_the_others`,
|
||
`the_theme_marks_only_the_ask_fragments_we_mounted` (D3).
|
||
|
||
## Assertions that change (declared before the code)
|
||
|
||
| test | today | after | why |
|
||
|---|---|---|---|
|
||
| test_flow_browser `test_a_rows_keep_release_and_wipe_take_no_room_of_their_own` | controls visible at rest; a row with no badge has no side column (gap ≤14px) | replaced by `the_row_controls_take_no_room_where_a_hover_exists` | the operator ruled hover-reveal; the side column now always holds the lifetime pill |
|
||
| test_flow_browser `test_on_a_touch_screen_the_row_controls_keep_their_tap_floor` | measures `.desk-facts form button` | the same floor, measured on `.desk-acts` controls | the controls moved; the floor did not |
|
||
| test_flow_browser `test_the_wipe_dialog_shows_what_is_being_wiped_and_never_fails_open` | clicks the row's wipe at rest | hovers the row first, then clicks | wipe is hidden until hover (OPEN-1, answered yes) |
|
||
| tests/mutations/r2_flow.toml, four rows on the facts-line controls | proved the controls visible at rest on the facts line | retired, with successors in r2b.toml | their tests were replaced, as declared above |
|
||
|
||
## Out of scope
|
||
|
||
- The booth page header's keep / release / wipe (`.keep-lg`, `.wipe-lg`) — the
|
||
rulings named the Desk row.
|
||
- A site-wide blur switch (ruling A, not B).
|
||
- The tagline copy.
|