Item 1: ArrowLeft/Right/Up drive nope/like/maybe through commit(); ArrowDown
and Backspace (kept alias) drive undo(); Esc closes the top open sheet only
(viewer -> dossier -> deck, inert on report). Text inputs/TEXTAREA/SELECT/
contentEditable swallow keys; arrow defaults are prevented (no page scroll).
Item 2: card center tap now opens the dossier (bike data first); side taps
keep flipping photos; the direct card -> viewer door is retired. The deck
dossier records its pile entry; the hero tap opens the viewer with a
backToDetail chain so Esc walks viewer -> dossier -> deck (repeatable).
Item 3: deck card + dossier hero photos switch from cover to contain over a
soft blurred backdrop of the same image (option C, demo panel C); thumb and
fullscreen viewer untouched; nophoto paths render no backdrop.
- cross-province groups now pair ns<->nb by completeness (img > km > older)
up to min per region instead of collapsing the whole group to one row;
surplus siblings (multi-unit dealer ads) stay active — no false merges
- verified on live DB: dup 80 -> 77, active 560 -> 563, total 640; the 3
freed rows are the BMW G 310 R dealer siblings (450/451/452); deck top
order unchanged (Indian Scout 91 x3 then Vulcan 900 73); re-import
idempotent (77 stable)
- selfcheck +2: multi-unit pairing semantics, reversibility (flip to
active + re-import re-derives the pairing) — 16/16 green
- 8 P1 spine checks unchanged; 6 new API checks via fastapi.testclient
against create_app() on a temp COPY of the live DB (live file never
opened by tests): root/static, stats-live==active+health, deck page 1
monotonic + undecided-only + full cursor walk zero-overlap vs SQL truth,
swipe like->list->edit->delete lifecycle (+invalid payload rejection),
/img/{id} cached-200/hotlink-302/none-404, badge roundtrip
- bare-python run keeps the 8 P1 checks and notes the skipped P2 set
- known framework quirk documented: this fastapi+httpx TestClient pair
mangles RedirectResponse to a bare 404; real uvicorn serves the true 302
(proof run verifies over the wire)
- db.init_db gains guarded ALTER TABLE additions for listings.year /
seller_type / extras (schema.sql v1 untouched; PRAGMA-checked idempotent)
- rubric: extras_from_text() returns matched EXTRAS labels (table order)
- importer: normalize computes year (existing helper), seller_type ONLY
from a real state-item signal (dealer/private, else NULL), extras JSON;
upsert rewrites the new columns on both paths
- verified on live DB: import idempotent, counts stay 640/560/80, year
backfilled on 599 rows, extras on 45, seller_type 0 (no state signal)