Give the scroller's gutter one reading, and drop the tolerance the fix retired - #65
Conversation
…x retired main is red on test_opposite_margin_residents_wait_for_the_room_they_need. #64 made the strip veto take the scroller's gutter off, so the column now holds a full 720 at and above the combined floor; the tolerance that came in beside it from the test side, `720 - roomy["gutter"]`, still subtracts a bar from what the roomy read expects. On a platform that draws one that is `720 == 705`, and there is no shortfall left for it to express. The gutter had three spellings across the two files that owe it — the veto off the scroller's boxes, the room off body's, and the test off the window against body's padding box — so the claim that they were the same number was true by inspection in a file whose rule is that one representation answers one question. It is one reading now, `scrollerGutter`, beside the scroller it is a fact about; stateStrip and stateRoom both call it, and the test reads its own `gutter` key rather than a fourth expression. The window spelling was the one that could drift: it agrees with the other two only while body carries no margin, and the panel's strip is a body margin. The veto also gained an input none of its callers announce. It is called rather than observed, because the strip it vetoes is body's own padding and syncLayout runs from an observation of that box, so the window and the panel reach it on occasions of their own while the bar's arrival does not — a replay retiring a slot or a late widget settlement can put a bar on the page with no resize behind it. Joining layoutSizes is the one fix the one-writer rule forbids, and what is left standing is a bar wide in the band where the floor is met within a bar. Recorded at the function rather than chased with a call from every path that can change the document's height.
|
I reproduced both ends on a CI Linux runner (Chrome for Testing 151.0.7922.34, The fb03469 run as evidence for the one-reading halfThat run predates a196d93, so its Both terms were wrong at once and they cancelled in the direction of a louder failure rather than a quieter one, which is luck. It is the drift this PR's Local runs, for the record: |
leaf-agent
left a comment
There was a problem hiding this comment.
Reviewing my own PR, so this is a COMMENT rather than an approval. The code change is right and I verified it end to end: test_opposite_margin_residents_wait_for_the_room_they_need fails on main at 6e655de with assert 720 == (720 - 15) and passes here, the whole of test_render_pages.py is green on the merged tree (43 passed), and the floor assertion is non-vacuous — taking scrollerGutter() back out of stateStrip fails it at 705 == 720 exactly as the description says. Both #64 review observations are answered. One finding, on the prose rather than the code.
The freshness caveat documents a hazard the layer already closed. The new paragraph on stateStrip says body "gains or loses its bar whenever the document's height crosses the viewport", and that replay retiring a slot, a late widget settlement, or an arriving image can each leave "a grant made when there was no bar to pay for". None of those can. runtime/chrome-style.js gives body scrollbar-gutter: stable in the same rule that makes it the scroller — body { height: 100%; overflow-y: auto; scrollbar-gutter: stable; … } under :where(html:not(.lf-copy)) — so the room is reserved whether or not a bar is drawn in it, and offsetWidth - clientWidth holds still for the page's life. Measured on this runner: a page shrunk from 1500px of content to 10px reads the gutter at 15 before and 15 after, with document.body.scrollHeight > document.body.clientHeight false at the second read.
That matters because the caveat is load-bearing in the way this repo's comments are — it names a live 15px defect and declines a fix on the strength of it, and the next reader will reason from it. The #64 review that asked for it was wrong on the same point, so the PR answered the observation faithfully; the honest answer to that observation is that the stylesheet had already answered it. The same claim rides in CLAUDE.md's new sentence ("its comment records what the gutter's freshness costs") and in the PR description's second bullet.
I'm pushing the prose correction rather than leaving it — no author but me will act on it, and it is comment-only with no behavioral risk. Revert it if you'd rather keep your own framing of the limit.
How the gutter probe was run
Against this PR's head in a worktree, with the served page's own module:
probe = "() => import('/runtime/scrolling.js').then(m => m.scrollerGutter())"
tall = leaf_page("tall", '<h1>T</h1><div id="pad" style="height: 1500px"></div>')
page, errors = open_page(browser, serve(tall))
resized(page, 1416, 800)
with_bar = page.evaluate(probe)
page.evaluate("() => document.querySelector('#pad').style.height = '10px'")
page.wait_for_timeout(300)
without_bar = page.evaluate(probe)GUTTER tall=15 short=15 still_scrolls=False.
Non-vacuity check, with scrollerGutter() removed from stateStrip's avail: fails at tests/test_render_pages.py:2006, assert 705 == 720, column {'left': 288, 'right': 993, 'width': 705}.
The freshness note stateStrip carried named a hazard the layer has already closed: body is given scrollbar-gutter: stable in the same chrome-style.js rule that makes it the scroller, so the room is reserved whether or not a bar is drawn in it and the difference between the scroller's two boxes holds still for the page's life. A document that stops overflowing reads the same gutter it read while it scrolled — 15 before and 15 after on a Linux runner — so replay retiring a slot, a late settlement and an arriving image are answered where the scroller is declared rather than left as a bounded residual. Record that beside the reading in scrolling.js, at the veto that owes it, and in the runtime guide's sentence about which of the two geometry readings is observed.
|
A third It is the post-#64 shape rather than the pre-#64 one the earlier comment recorded: The failing run's own output
|
The runtime half of this change gives the gutter one reading; the test that drives the page past the floor hand-wrote the same expression in its own reading dict, which is a second spelling that agrees on inspection rather than by construction. Ask /runtime/scrolling.js for it instead. The reading's own gutter key stays: it is what the failure dumps report the short measure against, which is the first thing to know when this assertion goes red.
|
The latest test change makes the production helper both choose the roomy viewport and drive the behavior under test, which leaves a correlated-error blind spot. For example, if scrollerGutter overreads a real 15px gutter as 30px, the test resizes to 1446px and stateStrip subtracts the same wrong 30px to reach its 1416px threshold; every assertion can pass while 1431–1445px viewports remain wrongly cramped and stateRoom is 15px short. Please keep or compare the independent browser measurement already available as reading["gutter"] (for example, assert bar == at_floor["gutter"] before using bar). This should preserve the single production implementation while keeping the test oracle independent. |
|
The |
Asking scrollerGutter for the width the page is driven at made the helper choose the viewport and then be the thing under test, so an error in the helper landed on both sides and cancelled. A gutter overread as 30 puts the page at 1446 and has stateStrip take 30 off it: the floor is met on the nose, every measure passes, and the band from 1431 up is cramped for nothing. Verified — with the overread planted and no guard, the test passes green. Hold the two spellings to each other at the one viewport both are read at. The reading's gutter key becomes a claim about what the gutter is rather than a copy nothing checks, and stays what the failure dumps report a short measure against.
|
You're right, and the blind spot was live rather than theoretical — I reproduced it before fixing it. With Fixed in c8d85d9 the way you asked — That also settles what I said in the reply above about keeping the
The two bug-backsBoth against the merged tree on a CI Linux runner, Chrome for Testing 151.0.7922.173, The blind spot, with the guard removed and The guard, same plant: The dump also shows the consequence directly: And the original gate is still non-vacuous — with |
|
The oracle fix on c8d85d9 closes the blocker: the independent body-box reading is now compared with scrollerGutter() before that helper chooses the roomy viewport. Please refresh the PR description before merge, since its Solution section still says the test reads its own gutter key instead of a fourth expression and does not describe the new production-helper call plus independent cross-check. The Testing section should name the overread bug-back as well. Code verdict is otherwise acceptable; exact-head CI remains the final gate. |
There was a problem hiding this comment.
Reviewing my own PR, so this is a COMMENT rather than an approval. The increment since my last review is the guard @max-sixty asked for, and it does what was asked: the two spellings are held to each other at the one viewport both are read at, before either drives a resize, and the bug-back in the reply above shows the planted overread passing without it and failing at assert 30 == 15 with it. Both earlier threads are resolved. One finding, on prose the increment left behind.
The docstring still carries the tolerance this PR retires. 7f17a60 "Account for stable gutter in sidebar test" introduced assert roomy["column"]["width"] == 720 - roomy["gutter"] and, in the same hunk, rewrote the docstring's third sentence from "both may stand outside a full-width column" to "both may stand outside the ordinary column, less only a live platform's stable scrollbar gutter". This PR deletes the assertion and leaves the sentence standing. It now contradicts the comment eight lines below it in the same function — "neither read subtracts a bar from what it expects ... a measure that fell short of 720 anywhere here would be the fault this floor exists to prevent rather than a tolerance to write down" — and a docstring is where the next author would take the tolerance back from, which is the whole shape this PR exists to close.
What the two assertions now say between them is that at the combined floor of the page's own box both strips stand outside a full 720 column, and a window short of that floor by a bar's width has the veto hand them back — at a 1416 window with a 15px bar, avail is 1401 against --strip-min: 1416, so data-lf-cramped is set, which is why at_floor reads a full column too. That is a stronger sentence than the one 7f17a60 replaced, so a plain revert would undersell it.
I'm pushing the correction rather than leaving it: no author but me will act on it, and it is docstring-only with no behavioral risk. ci.yaml declares no concurrency group, so the test run in flight on c8d85d9 finishes on its own rather than being replaced by the push. Revert it if you'd rather keep the current framing. The description refresh @max-sixty asked for above goes in the same round, wearing the author hat rather than this one.
Nothing else stands. The runtime half is a behavior-identical extraction — pageScroller is document.body, so stateStrip and stateRoom each keep the exact number they computed inline — and I checked the two claims the new comments rest on rather than taking them: scrollbar-gutter: stable is on body in the same :where(html:not(.lf-copy)) rule that gives it overflow-y: auto (runtime/chrome-style.js), and the chrome stylesheet is appended to document.head at module top level, well before the first stateStrip() call, so there is no cold read of 0 before the rule lands.
Where the docstring sentence came from
$ git show 7f17a60 -- tests/test_render_pages.py
- for prose. At the combined floor, both may stand outside a full-width column. A
- script-free copy has to make the same choice from its viewport alone.
+ for prose. At the combined floor, both may stand outside the ordinary column, less
+ only a live platform's stable scrollbar gutter. A script-free copy has to make the
+ same choice from its viewport alone.
@@
- assert roomy["column"]["width"] == 720
+ assert roomy["column"]["width"] == 720 - roomy["gutter"], (
+ f"the two strips cost more than the live page's stable scrollbar gutter: {roomy}"
+ )
One commit, one hunk: the assertion and its sentence form arrived together, so retiring one leaves the other unsupported.
7f17a60 wrote the docstring's "less only a live platform's stable scrollbar gutter" in the same hunk that wrote the assertion it describes. The assertion is gone; the sentence said what the two assertions now say between them is wrong, and a docstring is where a next author would take the tolerance back from.
|
Description refreshed — both asks are in the body now, since the edit itself is silent and you gated merge on it. Solution no longer says the test reads its own gutter key instead of a fourth expression. It describes the actual arrangement: the test asks the served page's module for Testing names the overread bug-back as its second bullet, both directions: the plant passing green with the cross-check removed, and failing at Head is e6d0a63, which added the docstring correction on top of c8d85d9. |
|
Current head 1b0375c now stacks the independently reviewed #66 commit so one branch closes both failures on red main. That combined code is acceptable, but the PR description still presents only the gutter change and no longer matches the five-file diff. Before merge, please document that #66 is intentionally included to break the two-failure CI cycle, summarize its stale navigation-test repair/testing, and state that #66 will be closed as landed through this PR. I will require the exact combined head CI and review checks to pass; the currently cancelled review check will be rerun rather than waived. |
|
Both description asks are already in the body — you made those edits yourself at 03:00:10 and 03:03:57, so I've left the prose alone rather than clobbering it. For the record it now covers all three: the Problem section states that each isolated repair stayed red on the other failure so this branch carries both and gets one exact full-suite gate, and that #66 will be closed as landed through #65; the Solution section's second paragraph summarizes the navigation-test repair as a layout relation rather than duplicated theme arithmetic; and Testing names The part that does need you: the review rerun you asked for already happened, and it was the waiver rather than the rerun. You reran it at 03:00:42 (attempt 2 of run 33034993120); it reported I set the stamp to Worth a look upstream in EvidenceAttempts of run
Attempt 2's steps: The stamp it read: The gate in Reviews on the PR, newest last — none on 1b0375c: |
leaf-agent
left a comment
There was a problem hiding this comment.
Reviewing my own PR, so this is a COMMENT rather than an approval. This is the first review on the combined head 1b0375c — the earlier ones stop at c8d85d9, for the stamp reason recorded in the comment above. Nothing to raise: no finding survived, and both threads from earlier rounds are resolved.
The increment since c8d85d9 is two commits. e6d0a63 is the docstring correction my last review asked for, and it now says the right thing: at a 1416 window with a 15px bar avail is 1401 against --strip-min: 1416, so the veto fires and at_floor reads a full column, while 1431 clears the floor exactly and grants both strips — which is what the two assertions read. 1c3f14c is #66's commit, and no review on this PR had covered it, so I read it as new rather than taking #66's.
What I checked on it, rather than reasoning from the description. INSIDE_ITS_OPTION now reads status.left/status.width off lf-option::before and paddingInlineStart off the option, and both share an origin with x: lf-option is position: relative (lf-option { display: block; position: relative; }), so the absolutely-positioned status rule's containing block is the option's padding box, which is the same edge opt.x + borderLeftWidth lands on. Both groups on ADDRESS_PAGE are unsettled lf-options[choose], so the rule that generates that pseudo (& > lf-option::before { content: ""; position: absolute; inset-inline-start: var(--sp-2); … }) applies to card and row alike — which is what keeps parseFloat(status.left) off auto, and so off NaN.
I ran the three bug-backs independently, and each new term of the chain fails on its own — the band is not carried by one term with two decorations.
Bug-backs and runs
Merged tree at 1b0375c, -n0, headless shell. Clean: tests/test_render_navigation.py -k digits and tests/test_render_pages.py -k opposite_margin both pass.
afterStatus < x, with --lf-address-start moved back into the status rule's room:
AssertionError: c-heater's digit runs 4…23 in a gutter whose status rule ends at 11 and whose words open at 29, so the gutter is holding one of the three in another's room
assert 11 < 4
ends < opens, with --lf-address-col pulled 10px in so the prose opens under the chip:
AssertionError: c-heater's digit runs 15…34 in a gutter whose status rule ends at 11 and whose words open at 30, …
assert 34 < 30
len(seats) == 1, with the row form given inset-inline-start: calc(var(--lf-address-start) + 2px) — a seat still inside the band, so only the cross-form check can see it:
AssertionError: the digits stand at more than one column: {15: ['c-heater', 'c-cable', 'c-hand'], 17: ['r-now', 'r-later']}
assert 2 == 1
The third is the one worth having: it is the only assertion that would have caught a per-form override of the column, which is the shape the docstring says broke before.
The runtime half, re-read against the combined head
scrollerGutter has exactly two production callers and one test reading — grep -rn "offsetWidth - .*clientWidth\|scrollerGutter" over assets/ and tests/ returns leaf.js:1759, leaf.js:1879, the definition, test_render_pages.py:1981 (reading's own key, deliberately a second copy), and the import('/runtime/scrolling.js') at 2019. So the one-reading claim holds where it is made.
The extraction is behavior-identical including evaluation order: panelStrip and trayStrip are commentsEdge.width() / traysEdge.width(), and width() is held(chosen) — a read of documentElement.clientWidth through cap(), with state() the only writer. So moving the gutter read from before those calls to after them in stateRoom puts no mutation between the reads.
lint is green on this head. ci's test job on 1b0375c is still running; I have not seen it terminal, and it remains the merge gate alongside this review.
# Conflicts: # tests/test_render_navigation.py # tests/test_render_pages.py
leaf-agent
left a comment
There was a problem hiding this comment.
Reviewing my own PR, so this is a COMMENT rather than an approval. No findings; posting because a self-authored PR has no empty-body approval to say "this head was examined", and that ambiguity is what bit this PR once already.
The increment since my last review at 1b0375c is your merge commit 2bd593b and nothing else — git log --no-merges 1b0375c..2bd593b --not ac88a1a is empty, so no authored line arrived with it. What the merge did change is the branch's composition: it takes main's tests/render_cases_navigation.py and tests/test_render_navigation.py over #66's, and both files are now byte-identical to ac88a1a. So the two asks in your 03:00 comment — document #66's inclusion, summarize its repair — are void rather than outstanding, and the body already reads that way: it presents the scrollbar-gutter repair alone and says #66 is superseded by #56 and can close unmerged. That matches the four-file diff.
The remaining four files are unchanged from what I reviewed at 1b0375c, where nothing survived. I re-checked the one thing the new base could have disturbed — whether #56 reaches the code this PR moves. It does not: scrolling.js on ac88a1a is pageScroller alone, and a grep of the merged tree finds exactly the readings the one-reading claim allows.
leaf.js:251 import { pageScroller, scrollerGutter } from "./runtime/scrolling.js";
leaf.js:1759 scrollerGutter() - (stateStrip)
leaf.js:1879 scrollerGutter(), (stateRoom)
runtime/scrolling.js:21 the definition
test_render_pages.py:1981 reading's own key (the deliberate second copy)
test_render_pages.py:2019 import('/runtime/scrolling.js')
lint is green on this head. ci's test on 2bd593b is still in flight; I have not seen it terminal, and it remains the merge gate.
|
Checked your #66 rationale against the tree rather than taking it, and it holds: One residual worth knowing, since it isn't in what you enumerated as preserved. That relation is read on That is the form with the history. The navigation test's own docstring says the two forms are exactly what differed — "cards padded clear of their corners, rows with none to spare" — and records the vertical pin already going wrong for rows once, level with a 15px row and then two pixels high at 17px, with the gate green throughout. So the fragile form is the one now unwatched in the axis this whole round was about. Cheapest close is a second read of the existing relation against a row in the options test; no new specimen needed, since What I ran, and one limit on itOn the merged tree ( The limit: this sandbox reads the scroller's gutter as 0, so the margin-resident test is vacuous here. Both mutually exclusive forms pass — I also reached the wrong conclusion first here and want it on the record so it isn't re-derived: I grepped for |
# Conflicts: # tests/test_render_pages.py
…draws in (#121) ## Problem `ci` has been red on `main` since [26feec8](26feec8) "Use the browser root as Leaf's document scroller" — four consecutive runs, the same 13 tests each time, [the run this was dispatched for](https://github.com/max-sixty/leaf/actions/runs/33296140347) included (its commit only edited `TODO.md`). The root scroller took `scrollbar-gutter: stable`, and Chromium honours that by holding a classic bar's width open even where the platform draws overlay bars, which take no room at all. That width comes out of the page while every window-relative coordinate keeps the whole window, so the layout and everything that measures against the window disagree by exactly one bar. Measured here, at a 1200px viewport: the html box is 1185px wide, `documentElement.clientWidth` is 1200, and `innerWidth - documentElement.clientWidth` — the reading the suite and `stateStrip` use to find the bar — is 0. The 15px is reserved and nothing reports it. The three shapes the suite caught: a drawn edge landed a bar's width off the hand that drew it (`the edge did not follow the hand: … 'edge': 590 … 'chosen': '595'`), a covering sheet anchored to the initial containing block's right edge left a blank strip at the window's (`'panel': {'left': -15, … 'right': 305}` on a 320px window, with the toast inside it off screen), and a page with nothing overflowing measured a bar's width narrower than the box it scrolled in (`assert -15 == 0`). <details><summary>The browser behaviour, isolated</summary> Standards mode, this suite's pinned Chromium (151.0.7922.34), 1200×800: | `html` rule | `innerWidth` | `de.clientWidth` | `de.scrollWidth` | `de.getBoundingClientRect().width` | | --- | --- | --- | --- | --- | | `overflow-x: hidden; overflow-y: auto` | 1200 | 1200 | 1200 | 1200 | | `overflow-x: hidden; overflow-y: auto; scrollbar-gutter: stable` | 1200 | 1200 | 1185 | 1185 | | `overflow-x: hidden; overflow-y: scroll` | 1200 | 1200 | 1200 | 1200 | The bars here are overlay ones — a nested `overflow: auto` container with vertical content keeps `clientWidth === offsetWidth`, and the same container with `scrollbar-gutter: stable` loses 15px. So the gutter is reserved for a bar that will never stand in it, and `scrollWidth - clientWidth` — the suite's "does the page scroll sideways" reading — sits at `-15` before anything overflows. </details> ## Solution `overflow-y: scroll` asks for the same standing bar that the `stable` gutter was there to get. It is a no-op where bars overlay, and where they take room it takes exactly the room the bar does — the room `innerWidth - documentElement.clientWidth` already reports, which is what [#64](#64) and [#65](#65) established as the one reading. It also makes true again the sentence `skills/leaf/CLAUDE.md` states about the new scroller, that `documentElement.clientWidth` is already the root scrollport's usable width; that comment in `chrome-layout.js` is reworded to say the standing bar rather than the reserved gutter. Two tests carried a stale premise from before the scroller moved, and neither is loosened here: - the render gate's off-window float is still required to be named, against `<html>` — the scrollport since the move — rather than `<body>`. The gate already reports it that way; the same stale sentence in `render-checks/layout.js` is corrected beside it. - `test_covering_panel_takes_the_page_scroll_with_it` now waits for the glide that centres a quoted passage to be over before it wheels. The glide reaches its destination a frame or more before the browser fires its `scrollend`, and a wheel sent inside that window cancels the animation instead of scrolling, so the reader's notch is spent stopping a glide that had already stopped moving. Measured: at the moment the mark is centred one `scrollend` has fired, at 3442 — the instant scroll's — and the glide's own lands at 3669 afterwards. ## Testing All 14 tests the failed run reported now pass, and each was reproduced first at `-n0` on `main` at `01ec4f4`. Also green: the everyday suite (`uv run pytest tests`, 723 passed), and `test_render_controls.py`, `test_render_pages.py`, `test_render_widgets.py`, `test_render_gate.py`, `test_render_options.py`, `test_render_margin.py`, `test_render_navigation.py` with `--run-nightly` (362 tests). `ruff check`, `ruff format --check`, `prettier --check`, and `node --check` pass on the touched files. ## One failure in that run this does not fix `test_render_gate.py::test_a_reader_arrives_at_what_they_left_rather_than_watching_it_arrive` is intermittent and predates this branch: on `main` with these changes stashed it failed 2 runs in 6, and with them applied 1 in 3 — the same rate, and the arrangement it names differs every time (`the leaves tray standing` here, `the tray panel at the width the reader drew it to` and `the thread panel open` in the two CI runs). <details><summary>What it is, for whoever picks it up</summary> The extra motion is `CSSTransition box-shadow 120ms` on 57–83 `lf-option`s, and it is not hover — `document.querySelectorAll(':hover')` is empty when the finding lands. `theme.css`'s arrival guard is `body:not([data-lf-presented])` and `.lf-ui`, and its own comment names the trade: "`transition` does not inherit, so what an unreached descendant can still play is a transition of its own declaring; that is the trade, and the check that finds one is the suite's". `lf-options[choose] … > lf-option` in the default package theme declares `transition: box-shadow .12s ease, transform .12s ease`, and its `box-shadow` is `var(--lf-ring), var(--lf-lift)` where `--lf-ring` moves when replay writes `[chosen]`. So it is an unreached descendant with a transition of its own, and the coverage that was supposed to find one has. Left out of this PR because it is a separate cause in a separate layer and would be revertable on its own. </details> --- Automated fix for [failed run](https://github.com/max-sixty/leaf/actions/runs/33296140347) ## Rebased onto `d8d1f1e` `main` has since absorbed three of the five files this branch touched, so the diff is now the `chrome-style.js` rule and the `scrollend` wait alone. [7336a14](7336a14) landed the render gate's `<root scrollport>` naming and its own wording of the `layout.js` comment, and [335bf9a](335bf9a) deleted the `chrome-layout.js` sentence reworded above. Everything the **Solution** section says about those three files is now true of `main` rather than of this diff; the CSS change and its reasoning are unchanged. The **Testing** section describes the tree as it stood at `01ec4f4`. Re-measured on `d8d1f1e`, over the 57 tests the latest red run ([33325826683](https://github.com/max-sixty/leaf/actions/runs/33325826683)) reported plus 7336a14's new `test_the_render_gate_measures_sideways_room_at_the_root_scrollport`: 28 red before this branch, 20 after. The nine it fixes include that new gate test, which asserts on a non-overflowing page exactly the `-15` this PR is about. Two of the twenty it leaves red are reached only because it gets past an earlier assertion, and neither is its doing — the thread comment below carries them. ## What the covering lock costs The two covering locks spell themselves `overflow-y: hidden`, which draws no bar, while `scrollbar-gutter: stable` held its room open under `hidden` too. So where the platform draws classic bars, the strip of page still visible beside a covering sheet now widens by a bar's width as the sheet opens and narrows back as it closes — one more rewrap on a window that was already rewrapping. Holding that room across the lock means reserving it on every platform, overlay-bar ones included, which is the `485` box inside a `500` `clientWidth` this PR exists to end, and the locked state is where it cost the most: that is the covering sheet landing at `left: -15` with its toast off screen. So the rewrap stays, and it is recorded beside the lock rules rather than only in the thread. No test can see it — every browser the suite launches carries `--hide-scrollbars`. --------- Co-authored-by: leaf-agent <318509791+leaf-agent@users.noreply.github.com>
Problem
mainhad two independent stale layout assertions:#56 has since repaired the stale option-address assertion on
mainand directly checks the status/address/prose relation for the card form. This branch is therefore back to the scrollbar-gutter repair alone. #66 remains separate: it has been reopened to retain the corresponding row-form and cross-form coverage.Solution
The runtime now has one
scrollerGutterreading besidepageScroller;stateStripandstateRoomboth use it. The browser test asks that production helper for the width it drives, but first cross-checks it against an independent body-box reading. That guard catches a correlated overread before the helper can choose and judge the same viewport. The roomy column is then required to remain the full 720px; the retired gutter tolerance and its prose form are removed.Testing
test_opposite_margin_residents_wait_for_the_room_they_need, the currenttest_a_questions_digits_are_drawn_whole, and Add flowchart node comment anchors #56's direct option-pointer regression: 3 passed together after merging currentmain.mainpass pre-commit.30 == 15.scrollerGutter()from the strip calculation fails at705 == 720.Exact-head full CI and review remain the merge gate.