feat(r3): compare — two picked rels side by side, linked stepping, synced pan, flag the winner

GET /b/{name}/compare with the conjunction 404 (containment AND the review
ring), both sides recorded as seen, view state (side, link) mapped from a
closed set onto every link, side-keyed regions, and back=compare in
_mark_redirect. compare.html: two stages sharing one set of rows, the strip
as picker (the side active now), linked and per-side stepping, X/L/Z/A/B/C
keys under the review's guards, synced pan by fraction with an echo guard,
per-side blur reveals, JS-off parity.

The stage machinery moves out of view.html into _stage_js.html
(BoothMode.bind, BoothStage.attach), shared by the review and compare. The
review gains a Compare control and a C key. At phone width a full top bar
wraps.

Tables: r2c's 15 stage rows re-pointed to _stage_js.html; r2b's phone
top-bar row re-anchored (the wrap made it vacuous alone); new r3.toml. The
contract records the wrap, equal stages and C on the compare page.
This commit is contained in:
vh
2026-09-24 13:26:11 -07:00
parent 5d785fe3c4
commit 8c7fe77841
11 changed files with 1841 additions and 111 deletions
+14 -2
View File
@@ -248,11 +248,23 @@ new = '''e.target !== player) {'''
label = "the top-bar controls squeeze into multi-line stacks at phone width"
file = "booth/templates/base.html"
test = "tests/test_flow_browser.py::test_the_review_and_doc_top_bars_fit_a_phone"
# r3 re-anchored this row. r3 added a Compare control to the review's bar and
# made a full bar WRAP at phone width (a fogged booth already overflowed it by
# 3px at 390). A wrapping bar never squeezes, so removing the no-wrap rules
# alone went VACUOUS: the property is now held by both. The defeating change is
# losing both; r3.toml rows the wrap on its own.
old = '''
.blur-all button,.reveal-all-btn{white-space:nowrap}
@media (max-width:600px){.reveal-all-btn .ra-note{display:none}}'''
@media (max-width:600px){.reveal-all-btn .ra-note{display:none}}
/* R3: the review's Compare control keeps only its glyph at phone width; its
title still says what it does (and C does it). */
@media (max-width:600px){.vcompare-l{display:none}}
/* ...and at phone width a top bar that cannot hold its controls WRAPS rather
than scrolling the page sideways. The review's bar was full before R3 (a
fogged booth overflowed it by 3px at 390); compare's bar holds more. */
@media (max-width:600px){.vbar{flex-wrap:wrap;row-gap:6px}}'''
new = '''
'''
@media (max-width:600px){.vcompare-l{display:none}}'''
[[mutation]]
label = "the Desk strip under another booth's reveal is lifted by a whisker (blur(0px) is not blurred)"