fix(booth): release was painting over the delete ×, which was unclickable

Operator: "release button covers delete button". Measured before touching
anything: release 58x24 at (323,266), × 30x30 at (349,268) — 30x22 px of
overlap on a 30px button, and `elementFromPoint` at the ×'s centre returned the
release form. The × I added yesterday was 100% unclickable from the moment it
shipped.

Cause: both were `position:absolute` on the same corner, each with its own
guessed offset, and `release` is the later sibling so it won. Replaced with one
flex row positioned once — release left, × right at the card corner where the
ephemeral lane's × already lives, so muscle memory transfers and neither can
drift back on top of the other when a label changes width.

Verified by measurement, not inspection: overlap 0 px, and clicks at each
control's centre now land on that control. The ephemeral lane's × and ★ were
re-checked and are unaffected.

ADDS scripts/layout-probe.py, because markup inspection STRUCTURALLY cannot see
this and I have now shipped two dead controls in two days by reading templates
instead of rendering them. It asks a real browser what a click at each
control's centre would hit.

It took four iterations to become trustworthy and the failures are the point:

  1. `top.contains(el)` counted an ANCESTOR overlay as a hit -- the exact case
     it exists to catch. Version one reported OK for a real overlay.
  2. elementFromPoint is viewport-relative, so everything below the fold read
     as occluded and buried the real findings.
  3. getBoundingClientRect() on a WRAPPED INLINE element is the union of its
     line boxes, whose centre lands in the gutter between them -- three zip
     links reported occluded by their own parent. Real geometry, wrong question.

Only the fourth version fires on a genuine overlay while staying silent on the
clean page. Both controls were run; a probe never seen to fail proves nothing.

173 tests pass.
This commit is contained in:
vh
2026-09-21 08:54:56 -07:00
parent 21f4afc033
commit 5f2c9a6a60
4 changed files with 154 additions and 7 deletions
+29
View File
@@ -39,6 +39,35 @@ rsync -a ./out/ nh3-dev:booth-data/my-run/
Then hand the operator `http://10.100.10.50:8090/b/my-run/`.
## Checking that controls can actually be clicked
```bash
<a python with playwright> scripts/layout-probe.py [URL ...]
```
⚠ **Markup inspection structurally cannot catch occlusion, and this UI has
shipped two dead controls in two days** — a reveal button whose handler Jinja
discarded, and a `×` that a sibling `release` form painted over completely
(30x22 px overlap on a 30px button; `elementFromPoint` at its centre returned
the other form). Both were reported by the operator. Both passed every test,
because the markup, the routes and the CSS were each individually correct.
The probe walks every button and link, scrolls it into view, and asks the
browser what a click at its centre would actually hit. It took four iterations
to become trustworthy, and each failure is worth knowing because they are the
traps in writing this kind of check at all:
1. `top.contains(el)` counted an **ancestor** overlay as a hit — which is the
exact case the probe exists to catch. It reported OK for a real overlay.
2. `elementFromPoint` is **viewport-relative**, so everything below the fold
read as occluded. Scroll first.
3. `getBoundingClientRect()` on a **wrapped inline** element is the union of
its line boxes, whose centre can sit in the gutter between them, on the
parent. Use `getClientRects()[0]`.
4. Only after all three does the positive control (a real overlay) fire while
the negative control (the clean page) stays silent. **Both were run.** A
probe that has never been seen to fail is not evidence of anything.
## Blurring an item (cosmetic censoring)
⚠⚠ **Blur is NOT access control.** A blurred item is still served at its own