diff --git a/devlog/_plan/260829_gui_dashboard_slop/000_baseline_and_roadmap.md b/devlog/_plan/260829_gui_dashboard_slop/000_baseline_and_roadmap.md
new file mode 100644
index 0000000000..0d58135639
--- /dev/null
+++ b/devlog/_plan/260829_gui_dashboard_slop/000_baseline_and_roadmap.md
@@ -0,0 +1,124 @@
+# 000 — Dashboard sidecar pair alignment and GUI polish
+
+Reported: the dashboard at `#dashboard` "슬롭이 났다" — components misaligned
+horizontally and vertically, and wrong under dynamic viewports.
+
+This unit fixes what is *measured*, not what is asserted. Every defect below has
+a numeric baseline captured with a CDP geometry harness that overrides the
+viewport (`Emulation.setDeviceMetricsOverride`, dpr 2) and reads live
+`getBoundingClientRect` values, so a claim can be re-checked rather than
+re-argued.
+
+## Harness and why it is trustworthy
+
+Two harness bugs were found and fixed *before* any defect was accepted, because
+each one silently manufactured agreement:
+
+1. **Viewport lag.** The first sweep measured a cell before the emulated
+ viewport had actually resized, so rows reported the *previous* width
+ (`ru/w1100` measured `vw=1440`). Fixed with a settle loop that requires
+ `innerWidth === target` before measuring, and the verdict tool now fails on
+ any unsettled cell.
+2. **Locale never applied.** `Page.navigate` to a URL differing only in its
+ hash does not reload the document, so all four locales measured byte-identical
+ geometry — an invalid multi-locale claim that *looked* like passing evidence.
+ Fixed with an explicit `Page.reload` plus a settle condition on
+ `document.documentElement.lang`. The probe now records the rendered hint
+ length per card, and the verdict tool **fails** when every locale reports the
+ same signature. Post-fix signatures: `en=66,81 ko=30,41 ru=82,114 fr=88,111`.
+
+The second bug is the important one: without it, this unit would have "verified"
+the locale dimension while never rendering a non-English string.
+
+## Defect 1 — the sidecar pair loses its shared control row (TOP PRIORITY)
+
+`.dash-sidecar-row-card` (web search) and `.dash-vision-sidecar-card` are
+documented in the stylesheet as a *matched pair* whose first `Select` must land
+on one line. Measured `ctrlYDelta` (vertical offset between the two control
+rows):
+
+| locale | 1440 | 1100 | 1024 |
+|--------|------|------|------|
+| en | 0.1 | 0.1 | **27.4** |
+| ko | 0.4 | 0.4 | **27.8** |
+| ru | 0.1 | **7.1** | **22.6** |
+| fr | 0.1 | **2.3** | **22.6** |
+
+### Root cause
+
+Both cards are `flex-wrap: wrap` with `align-items: center`, and the grid
+stretches them to equal height. Below ~`36rem` of *card* width the container
+query gives copy and controls `flex-basis: 100%`, so each card becomes two
+wrapped flex lines. The two cards then have **equal outer height but different
+content height** — vision's control column is taller (select row + advanced
+disclosure). Flexbox distributes the leftover space of each card independently
+and `align-items: center` centres each line within its own leftover, so the
+shorter card's control row sinks by half the difference. Nothing ties one card's
+second line to the other's.
+
+The shipped mitigation is a hard-coded reserved band:
+
+```css
+.dash-sidecar-row-card .dash-sidecar-copy { min-height: 3.9375rem; }
+```
+
+`3.9375rem` = 63px = "21px title + 3px hint margin + two 19.5px hint lines".
+That number only holds while *both* hints wrap to at most two lines. It is the
+third attempt at this alignment recorded in the file — after wrapping only one
+card, then `align-items: flex-start` on one card — each adding a magic number
+instead of removing the cause. The ru/fr breakage at 1100px is the band failing
+exactly as predicted: longer hints take a third line, overflow the band, and the
+pair desynchronises at a width where English still looks fine.
+
+### Fix direction
+
+Align the rows *structurally* so no number has to be maintained: the pair shares
+one row grid, and each card's copy row and control row are placed into shared
+tracks. Then alignment holds for any hint length in any locale, and the band can
+be deleted rather than re-tuned. The existing comment correctly warns that
+`container-type` layout containment blocks `subgrid` from reading parent
+tracks, so the container query must not sit on a subgrid participant.
+
+## Defect 2 — phantom zero-width grid track
+
+At vw ≥ 1440, `.dash-sidecar-grid` and `.dash-overview-tools` compute
+`grid-template-columns: 555px 555px 0px`. `repeat(auto-fit, minmax(min(100%, 21rem), 1fr))`
+emits a third, zero-width track. Trailing gap measures 0 today, so nothing
+visibly shifts — but the track is real, and it becomes a phantom gap the moment a
+third card is added to either grid.
+
+## Defect 3 — static viewport units in scroll surfaces
+
+`gui/src/styles.css:2003` caps `.logs-table-wrap` with
+`max-height: calc(100vh - 260px)`. Static `vh` resolves against the *large*
+viewport, ignoring mobile browser chrome, while the rest of the shell already
+uses `100dvh` (styles.css:244, 247, 411, 412, 2198). The log table is therefore
+sized for a viewport the user cannot see. `styles.css:755` and `1222` cap toast
+width with `calc(100vw - Npx)`, which ignores classic scrollbar width.
+
+The probe measures this behaviourally — comparing each scroll container's
+computed cap against `visualViewport.height` — rather than grepping for the
+unit, so the assertion survives a refactor.
+
+## Work phases
+
+| phase | doc | deliverable |
+|-------|-----|-------------|
+| wp0 | this unit | measured baseline + roadmap |
+| wp1 | `010` | sidecar pair structural alignment (top priority) |
+| wp2 | `020` | phantom auto-fit track |
+| wp3 | `030` | dynamic viewport units |
+
+Acceptance for every implementation phase: `ctrlYDelta ≤ 1px` and
+`heightDelta ≤ 1px` while paired, no horizontal overflow, no zero-width track,
+no scroll cap exceeding the visual viewport, across
+`1440/1100/1024/900/430` × `en/ko/ru/fr`, with locale signatures proven
+distinct.
+
+## Constraints
+
+- The local suite is not run here (user instruction). Gates run remotely via
+ `ssh lidge` + `ocx-run`; pushes use `--no-verify` only after those gates.
+- Delivery is a stacked PR chain onto `dev`, each PR carrying screenshots
+ (`enforce-target` requires a screenshot for GUI PRs).
+
diff --git a/devlog/_plan/260829_gui_dashboard_slop/010_sidecar_pair_alignment.md b/devlog/_plan/260829_gui_dashboard_slop/010_sidecar_pair_alignment.md
new file mode 100644
index 0000000000..d5c32e0dcf
--- /dev/null
+++ b/devlog/_plan/260829_gui_dashboard_slop/010_sidecar_pair_alignment.md
@@ -0,0 +1,79 @@
+# 010 — Sidecar pair: structural row alignment (wp1, TOP PRIORITY)
+
+Removes the magic reserved band and makes the two sidecar cards share real row
+tracks, so their control rows align for any hint length in any locale.
+
+## Current shape
+
+```text
+.dash-sidecar-grid grid, auto-fit 2 tracks, align-items: stretch
+ └─ .panel.dash-delegation-summary.dash-sidecar-row-card
+ ├─ .dash-sidecar-copy (title + hint)
+ └─ .dash-delegation-controls (selects / switch / disclosure)
+```
+
+Each card is its own flex container (`flex-wrap: wrap`, `align-items: center`)
+and, below `36rem` of card width, both children take `flex-basis: 100%` — two
+wrapped lines whose position depends only on that card's own leftover space.
+
+## Change
+
+Make each card a two-row grid and let both cards inherit the *same* two rows from
+the pair grid:
+
+1. `.dash-sidecar-grid` gains `grid-template-rows: auto auto` so there are named
+ parent rows to inherit.
+2. `.dash-sidecar-row-card` becomes `display: grid` with
+ `grid-template-rows: subgrid` spanning both rows, so copy lands in row 1 and
+ controls in row 2 **in both cards**. Row 1 is sized by the taller of the two
+ copy blocks, automatically — which is exactly what the 63px band was
+ hand-computing.
+3. Delete `.dash-sidecar-row-card .dash-sidecar-copy { min-height: 3.9375rem }`
+ and the `min-height: 3.6875rem` band on `.dash-delegation-controls`. They are
+ the numbers being replaced.
+4. Move `container-type: inline-size` **off** the subgrid participant. Layout
+ containment blocks a subgrid from reading parent tracks (the stylesheet already
+ warns about this). The container is re-established on a wrapper so the existing
+ `@container sidecar-card` rules keep working unchanged.
+
+## Wrapper
+
+Subgrid requires the card to be a grid *item* of the pair grid, but the card also
+has to be the container query root's child. Structure becomes:
+
+```text
+.dash-sidecar-grid (grid, 2 rows)
+ └─ .dash-sidecar-cell (container-type: inline-size, display: grid, rows: subgrid, span 2)
+ └─ .dash-sidecar-row-card (display: grid, rows: subgrid, span 2)
+```
+
+The cell carries the container query; the card carries the visible panel styling.
+Both pass the rows through, so row 1 and row 2 are shared across the pair.
+
+Requires one JSX change in `dashboard-overview-sections.tsx`: wrap each of the
+two existing card `div`s in `
`.
+
+## Stacked state
+
+When the container query stacks a card (card narrower than `22rem`), the two
+cards are in *different* grid columns of a single-column grid — i.e. different
+rows of the pair — so cross-card alignment is meaningless and must not be
+asserted. The verdict tool already treats `sameRow: false` as `STACKED` and
+skips the delta check.
+
+## Fallback
+
+`grid-template-rows: subgrid` is supported in Chrome 117+, Safari 16+, Firefox
+71+. Guard with `@supports (grid-template-rows: subgrid)`; without support the
+cards keep the current flex row behaviour, which is the shipped status quo rather
+than a regression. The deleted bands are restored inside the negative branch so
+unsupported browsers keep today's approximation.
+
+## Acceptance
+
+- `ctrlYDelta ≤ 1px` and `heightDelta ≤ 1px` at every PAIRED cell across
+ `1440/1100/1024` × `en/ko/ru/fr` (baseline: up to 27.8px).
+- No `min-height` band remains on `.dash-sidecar-copy`.
+- Locale hint signatures distinct, no unsettled cells.
+- A focused GUI test asserts the subgrid contract so a future edit that reverts
+ to the band fails.
diff --git a/devlog/_plan/260829_gui_dashboard_slop/011_audit_correction_align_content.md b/devlog/_plan/260829_gui_dashboard_slop/011_audit_correction_align_content.md
new file mode 100644
index 0000000000..198ff217da
--- /dev/null
+++ b/devlog/_plan/260829_gui_dashboard_slop/011_audit_correction_align_content.md
@@ -0,0 +1,115 @@
+# 011 — Audit correction: subgrid was the wrong fix
+
+The `010` plan was written before the regime sweep existed. The sweep contradicts
+its central assumption, so `010` is superseded by this document. Recorded rather
+than silently edited, because the wrong assumption is the interesting part.
+
+## What `010` assumed
+
+That the cards render as a horizontal row at desktop width (copy LEFT, controls
+RIGHT) and only stack when narrow — so a two-row subgrid was needed to align the
+"second line" of each card.
+
+## What the measurement shows
+
+Per-card internal layout, at full reload per width, sidebar state recorded:
+
+| vw | mainInner | cardW | columns | inside the card | ctrlYDelta |
+|------|-----------|-------|---------|-----------------|------------|
+| 1600 | 1128 | 556 | 2 | STACKED | 0 |
+| 1440 | 1126 | 555 | 2 | STACKED | 0 |
+| 1280 | 966 | 475 | 2 | STACKED | 0 |
+| 1100 | 786 | 385 | 2 | STACKED | -2.3 |
+| 1024 | 782 | 347 | 2 | STACKED | **22.5** |
+| 1010 | 768 | 340 | 2 | STACKED | **22.5** |
+| 1000 | 758 | 686 | 1 | ROW | n/a (single column) |
+| 992 | 750 | 678 | 1 | ROW | n/a |
+| 980 | 738 | 666 | 1 | STACKED | n/a |
+| 768 | 526 | 454 | 1 | STACKED | n/a |
+| **760** | **750** | **349** | **2** | STACKED | **22.5** |
+| 740 | 730 | 339 | 2 | STACKED | **22.5** |
+| 720 | 710 | 674 | 1 | ROW | n/a |
+
+Two facts kill `010`:
+
+1. **The cards are ALREADY stacked internally at every two-column width.** The
+ `36rem` container query fires whenever the pair is side by side, because a
+ two-up card is at most ~556px = 34.75rem < 36rem. Copy and controls are already
+ on separate lines; there is no row to preserve and nothing for a two-row
+ subgrid to add. The horizontal row only appears when the grid collapses to ONE
+ column (cardW ≈ 674-686px > 36rem), and in that state the cards are stacked
+ vertically as a pair, so cross-card alignment is meaningless.
+2. **A JSX wrapper would have been added for nothing**, and moving
+ `container-type` off the card would have silently killed the existing
+ `@container sidecar-card` rules — the exact "reads correct in review but does
+ nothing" failure the stylesheet already warns about.
+
+## The real cause of the 22.5px offset
+
+Both cards are stretched to equal height by the grid, and each is
+`flex-wrap: wrap` + `align-items: center`. Two wrapped lines, equal outer
+height, **different content height** (vision's control column is taller: select
+row + 12px gap + the advanced disclosure). Flexbox gives each card its own
+leftover space, and `align-items: center` centres each line inside its own
+leftover. The card with less content has more leftover, so its control row sinks
+by roughly half the difference. Nothing couples the two cards.
+
+The 63px copy band mitigates this only while both hints wrap to the same number of
+lines. At `ru`/`fr`, the vision hint takes a third line at 1100px, which is why
+ru/fr break at a width where en/ko still measure clean.
+
+## The fix
+
+Pack the wrapped lines from the top of each card instead of centring them in
+leftover space:
+
+```css
+.dash-sidecar-row-card { align-content: start; }
+```
+
+`align-content` is the correct property for a **multi-line** flex container — it
+distributes the *lines*, which is exactly what is misdistributed here.
+`align-items` (already `center` from `.dash-delegation-summary`) aligns items
+*within* a line and must stay, so the single-line desktop row keeps its vertical
+centring.
+
+The stylesheet notes that `align-content` "has no effect on one line" — true, and
+it is why `align-content` alone was rejected for the *control group*. But the
+target here is the CARD, which genuinely has two lines in exactly the regime that
+misaligns. In the one-column regime the card is a single line, where
+`align-content: start` is inert and the row is unaffected. That is the property
+doing precisely one job in precisely one regime.
+
+With lines packed from the top, both control rows sit at
+`padding-top + copyRowHeight`. Equal copy row height across the pair is then the
+only remaining requirement, and it is what the `min-height` band already
+provides — but now the band only needs to cover the *tallest actual* copy, and
+alignment no longer depends on the two hints matching. The band is therefore
+replaced by a locale-proof mechanism: the copy row's height is equalised by the
+same `align-content` packing plus a shared floor expressed in line units
+(`3lh`), not a pixel count derived from one locale's wrap count.
+
+## Consequences for the existing test
+
+`gui/tests/sidecar-layout.test.ts` currently asserts the magic band *as the
+contract*:
+
+- "both cards reserve the same copy band" requires `min-height >= 3.9rem` on the
+ copy block;
+- "both control groups reserve the same band and pack from its top" requires
+ `min-height` and `align-items: flex-start` on the control group.
+
+Those assertions encode the mitigation, not the requirement, so they must be
+rewritten to assert the *cause* being removed (lines pack from the start; no
+pixel-derived band is load-bearing). This is the file's stated purpose — "make the
+specific CSS shape that caused the bug impossible to reintroduce" — applied to the
+actual cause.
+
+## Additional defect found by the sweep (new)
+
+**The 760px two-column regression.** At `max-width: 760px` the sidebar leaves the
+flow (`position: fixed`, off-canvas at `x=-280`), so `.main-inner` JUMPS from
+526px to 750px. The sidecar grid re-splits into two columns at 349px each and the
+22.5px misalignment returns — on tablet widths, below the width where it was last
+believed fixed. Any fix must be verified at 760/740, not only at desktop widths.
+
diff --git a/devlog/_plan/260829_gui_dashboard_slop/012_shipped_fix_and_subgrid_postmortem.md b/devlog/_plan/260829_gui_dashboard_slop/012_shipped_fix_and_subgrid_postmortem.md
new file mode 100644
index 0000000000..e921ac6795
--- /dev/null
+++ b/devlog/_plan/260829_gui_dashboard_slop/012_shipped_fix_and_subgrid_postmortem.md
@@ -0,0 +1,83 @@
+# 012 — What actually shipped, and why subgrid could not
+
+`010` proposed subgrid; `011` corrected its premise but still recommended shared
+row tracks. Both were wrong about the mechanism. This is the record of what the
+experiments showed, kept because the failed attempts are the reason the shipped
+fix is one line.
+
+## The fix as understood at this point
+
+```css
+.dash-sidecar-row-card { align-content: start; }
+```
+
+> **Superseded by `013`.** This declaration ships and is load-bearing, but it is
+> only half of the fix. The measurement below was taken with the old
+> `3.9375rem` copy band still in place, which hid the ru/fr case where the two
+> copy rows are unequal. See `013` for the shipped pair and the removal tests.
+
+Measured: worst paired offset **0.0px** (was 27.8px) across `en/ko/ru/fr/ja/zh/de/tr`
+at 1024 and 1100, plus the regime boundaries 1600/1440/1010/760/1000/992/430 on
+the two longest-hint locales. Card heights and the one-column horizontal row are
+unchanged.
+
+## Why the copy band was never the cause
+
+The band (`min-height: 3.9375rem`) looked like the culprit and the plan called for
+deleting it. The decisive experiment says otherwise: with the band in place and
+`align-content` still at its default, the two copy blocks measured **equal**
+(63/63) while the control rows were still **27.4px apart**. Equal copy height is
+therefore necessary but not sufficient — the mis-distributed thing is the wrapped
+**lines**, not the copy.
+
+So *a* copy-row floor stays: something has to equalise the copy row that
+`align-content: start` then packs against, and deleting the floor outright would
+have re-broken the pair while the new rule kept measuring 0.0px at the locales
+that happen to wrap identically.
+
+What this document gets wrong is which floor. It concludes the `3.9375rem` band
+itself is load-bearing; `013` shows the band is a two-line pixel assumption that
+fails at ru/fr, and ships `min-height: 3lh` on the hint in its place. The band is
+**not** in the shipped stylesheet.
+
+## Why subgrid is unavailable here
+
+Shared row tracks are the textbook fix, and the independent auditor recommended
+them. They cannot work in this tree:
+
+| attempt | result |
+|---------|--------|
+| card as subgrid, `container-type` on the card | never applied; computed `display` stayed `flex` |
+| `container-type` moved to `.dash-sidecar-grid` | card's computed `grid-template-rows` = `none`; rows collapsed to 19px; cards 54px tall; controls overflowing 43-80px |
+| `container-type` on `.dash-overview-stack` | same collapse |
+| `min-content` / `max-content` / `auto` row sizing | no effect; the rejection is of `subgrid` itself, not the track sizing |
+| isolated clone with no container ancestor | worked perfectly, delta 0 — which is what identified containment as the cause |
+
+Chrome rejects a child's `grid-template-rows: subgrid` when an ancestor
+establishes layout containment via `container-type: inline-size`. This surface has
+two such containers (`.dash-sidecar-grid` and the per-card `sidecar-card` used by
+the existing narrow-card queries), so there is no position for the container that
+does not also block the subgrid. Removing the queries to make room would trade a
+27px offset for the wrong-axis bug they were introduced to fix.
+
+## The measurement lesson
+
+The subgrid collapse **passed the alignment gate**: `ctrlYDelta` read 0.0px while
+cards rendered 54px instead of 215px, because both cards were broken *identically*.
+A relative metric cannot see a symmetric failure. The gate now also asserts
+absolute card height and that no child overflows its panel, which is what caught
+it.
+
+## Deferred, per the audit
+
+Auditor blockers 6 and 7 are accepted and remove work from `020`/`030` rather than
+adding it:
+
+- The `0px` third track is **normal** `auto-fit` behaviour for a collapsed empty
+ track, not a defect. Replacing `auto-fit` with a fixed two-up would change
+ future three-card behaviour for no present gain. `020` is withdrawn.
+- `dvw` does not subtract a classic scrollbar, so a `vw` → `dvw` swap would not
+ have fixed the toast. The containing-block rewrite was then dropped too: the
+ divergence could not be reproduced here (`innerWidth == clientWidth`). What
+ shipped from `030` is the `.logs-table-wrap` `vh` → `dvh` change plus a
+ reproduced toast **specificity** fix; see `030` for both.
diff --git a/devlog/_plan/260829_gui_dashboard_slop/013_final_shipped_and_measurement_lessons.md b/devlog/_plan/260829_gui_dashboard_slop/013_final_shipped_and_measurement_lessons.md
new file mode 100644
index 0000000000..bd306ad387
--- /dev/null
+++ b/devlog/_plan/260829_gui_dashboard_slop/013_final_shipped_and_measurement_lessons.md
@@ -0,0 +1,111 @@
+# 013 — Final: what shipped and what the measurement taught
+
+Supersedes the mechanism proposed in `010`/`011`/`012`. Those documents are kept
+because the failed attempts are why the shipped fix is exactly these two
+declarations and not a third.
+
+## Shipped
+
+```css
+.dash-sidecar-row-card { align-content: start; }
+.dash-sidecar-row-card .dash-sidecar-copy .setting-hint { min-height: 3lh; }
+```
+
+The second replaces `min-height: 3.9375rem` on the copy block.
+
+**Both are load-bearing, and each was confirmed by removing it from the shipped
+stylesheet and re-measuring the rendered page.** They fix two independent halves
+of the same symptom, which is why neither alone is enough:
+
+| shipped CSS under test | worst paired offset | what breaks |
+|------------------------|--------------------|-------------|
+| both declarations | **0.0px** | nothing |
+| `3lh` only (`align-content` back to its `stretch` default) | **27.8px** | equal copy rows, but each card spreads its own leftover space across its own wrapped lines |
+| `align-content: start` only (old `3.9375rem` band restored) | **19.5px** | lines pack from the top, but the two copy rows are unequal at ru/fr (63px vs 82.5px) |
+
+`align-content: start` fixes the *distribution* of the wrapped flex lines;
+`3lh` fixes the *height of the copy row* those lines pack against. Removing
+either one re-opens the defect, so a future maintainer must treat both as part
+of the fix.
+
+## The defect, stated exactly
+
+The pair's control rows sat **19.5px** apart — one line box — at every two-up width
+from 1600 down to 740, in **ru** and **fr** only. Six other locales measured 0px.
+
+The old band was 63px, documented as "21px title + 3px hint margin + two 19.5px
+hint lines". It encodes a two-line assumption. The ru/fr vision hint wraps to
+**three** lines at a two-up card (82.5px of copy against 63px), so the band stopped
+describing the taller card and each card's control line followed its own copy.
+
+`3lh` states the real constraint — reserve three line boxes of the hint's own
+line-height — so the shorter hint reserves the same three lines, and a font or
+line-height change cannot invalidate the number.
+
+| locale | hint lines (web search / vision) | before | after |
+|--------|----------------------------------|--------|-------|
+| en | 2 / 2 | 0px | 0px |
+| ko | 1 / 2 | 0px | 0px |
+| ja | 2 / 2 | 0px | 0px |
+| zh | 1 / 1 | 0px | 0px |
+| de | 2 / 2 | 0px | 0px |
+| tr | 2 / 2 | 0px | 0px |
+| **ru** | **2 / 3** | **19.5px** | **0px** |
+| **fr** | **2 / 3** | **19.5px** | **0px** |
+
+## Why not subgrid
+
+Shared row tracks are the textbook fix and the independent auditor recommended
+them. They are unavailable here, and the evidence is unambiguous:
+
+| attempt | measured result |
+|---------|-----------------|
+| card as subgrid, `container-type` on the card | never applied; computed `display` stayed `flex` |
+| `container-type` moved to `.dash-sidecar-grid` | card's computed `grid-template-rows` = `none`; tracks 19px; cards 54px tall; controls overflowing 43-80px |
+| `container-type` on `.dash-overview-stack` | same collapse |
+| `auto` / `min-content` / `max-content` rows | no effect — the rejection is of `subgrid`, not the sizing |
+| isolated clone, no container ancestor | worked, delta 0 — which is what identified containment as the cause |
+
+Chrome rejects a child's `grid-template-rows: subgrid` when an ancestor
+establishes layout containment via `container-type`. This surface has two such
+containers (`.dash-sidecar-grid` and the per-card `sidecar-card` that the existing
+narrow-card queries depend on), so there is no placement that does not block it.
+
+## Two measurement failures worth keeping
+
+Both produced confident, wrong "all clear" results. The harness now defends
+against each.
+
+**1. A symmetric break passes a relative gate.** The subgrid collapse reported
+`ctrlYDelta = 0.0px` while cards rendered 54px instead of 215px, because both
+cards were broken identically. Alignment deltas cannot see that. The gate now also
+asserts absolute card height, child-vs-panel overflow, and hint truncation.
+
+**2. A leftover probe stylesheet fakes a pass.** An earlier round reported "ALL
+OK" for `align-content: start` across 30 cells. The number was real; the page was
+not the shipped page — an injected experiment sheet from a previous probe was still
+attached. The harness now strips every probe sheet before measuring, counts what
+remains, and **fails** if the count is not what the run expects.
+
+The second one is why `align-content: start` was briefly believed to be the
+*whole* fix. Re-measured on a clean page it leaves the full 19.5px at ru/fr,
+because packing lines from the top does nothing about copy rows that are unequal
+to begin with. That is a correction of its sufficiency, not of its necessity — it
+ships, and the removal test above shows the pair drifts 27.8px without it.
+
+## Deferred, per the audit
+
+- `020` **withdrawn.** The `0px` third track is normal `auto-fit` behaviour for a
+ collapsed empty track, not a defect. Replacing `auto-fit` with a fixed two-up
+ would change future three-card behaviour for no present gain.
+- `030` **reduced.** `dvw` does not subtract a classic scrollbar, so a unit swap
+ would not have fixed the toast, and the containing-block rewrite was dropped as
+ unreproducible on this surface (`innerWidth == clientWidth`, gap 0). Shipped
+ instead: `.logs-table-wrap`'s `vh` → `dvh`, and a toast `max-width` that was
+ losing the cascade to a later equal-specificity `.notice` rule.
+
+## Evidence
+
+- Harness: `.tmp/uiux/measure.ts` (scratch, not committed)
+- Screenshots with control-row guides: before `-19.5px` / after `0px` at ru and fr, 1024
+- Regression: `gui/tests/sidecar-layout.test.ts`, red on the previous CSS (2 fail), green on this one (8 pass)
diff --git a/devlog/_plan/260829_gui_dashboard_slop/020_phantom_grid_track.md b/devlog/_plan/260829_gui_dashboard_slop/020_phantom_grid_track.md
new file mode 100644
index 0000000000..63190005b4
--- /dev/null
+++ b/devlog/_plan/260829_gui_dashboard_slop/020_phantom_grid_track.md
@@ -0,0 +1,43 @@
+# 020 — Phantom zero-width auto-fit track (wp2)
+
+## Defect
+
+At vw ≥ 1440 both dashboard grids compute a third, zero-width column:
+
+```css
+grid-template-columns: 555px 555px 0px
+```
+
+from `repeat(auto-fit, minmax(min(100%, 21rem), 1fr))`.
+
+`auto-fit` collapses empty tracks but still *generates* one here because
+`min(100%, 21rem)` lets the hypothetical third track floor at 0 once the
+container is wide enough to nominally fit it. With only two children the track
+collapses to 0 and the trailing gap measures 0, so nothing shifts today. It
+becomes a real phantom gap the moment a third card is added.
+
+## Change
+
+Both grids hold a *known* number of cards, so express that instead of asking
+`auto-fit` to guess:
+
+```css
+grid-template-columns: repeat(auto-fit, minmax(min(100%, 21rem), 1fr));
+```
+
+becomes an explicit two-up that collapses to one column by container width:
+
+```css
+grid-template-columns: 1fr; /* narrow: stack */
+@container / min-width: two-up → 1fr 1fr /* wide: matched pair */
+```
+
+Applies to `.dash-sidecar-grid` and `.dash-overview-tools`. The wrap width stays
+`21rem` per card so the responsive behaviour is unchanged — verified by the same
+sweep, which must keep reporting STACKED at 900/430 and PAIRED at 1024+.
+
+## Acceptance
+
+- No `0px` track in either grid's computed columns at any swept width.
+- The PAIRED/STACKED pattern per width matches the baseline exactly (no
+ behavioural change, only the phantom track removed).
diff --git a/devlog/_plan/260829_gui_dashboard_slop/030_dynamic_viewport_units.md b/devlog/_plan/260829_gui_dashboard_slop/030_dynamic_viewport_units.md
new file mode 100644
index 0000000000..e7dfd01185
--- /dev/null
+++ b/devlog/_plan/260829_gui_dashboard_slop/030_dynamic_viewport_units.md
@@ -0,0 +1,60 @@
+# 030 — Dynamic viewport units in scroll surfaces (wp3)
+
+## Defect
+
+`gui/src/styles.css:2003`:
+
+```css
+.logs-table-wrap { max-height: calc(100vh - 260px); }
+```
+
+`vh` is the *large* viewport: it ignores mobile browser chrome, so the log table
+is capped for a viewport taller than the one the user can see, pushing the last
+rows under the browser UI. The rest of the shell already moved to `100dvh`
+(styles.css:244, 247, 411, 412, 2198), so this line is an outlier, not a
+convention.
+
+`styles.css:755` and `1222` cap toast width with `calc(100vw - Npx)`. Per CSS
+Values and Units 4, `100vw` includes the classic scrollbar gutter, so a
+scrollbar-reserving platform can in principle render a cap wider than the visible
+area.
+
+A separate, *reproduced* toast defect turned up while measuring that one: the
+cap on `.action-toast` never applied at all. Every toast also carries `.notice`,
+and `.notice { max-width: var(--prose-measure) }` is declared later in the same
+file at equal specificity, so source order won and the toast resolved to 70ch
+(542px) instead of its design width.
+
+## Change
+
+- `.logs-table-wrap` → `max-height: calc(100dvh - 260px)`.
+- Add `.action-toast.notice { max-width: min(480px, calc(100vw - 48px)) }` — two
+ classes so it beats the later `.notice` rule. Both halves of the cap are
+ restated: dropping the viewport term let the toast reach the screen edge at
+ 430px (measured `left = 0`, losing the 24px inset the right side keeps).
+- `styles.css:2003` is the only static `vh` in a scroll surface; the `12vh`
+ padding on the toast wrapper is decorative offset, not a size cap, and stays.
+
+### Not changed: the `vw` → containing-block rewrite
+
+The scrollbar-divergence rewrite was reverted before commit because it could not
+be reproduced on this surface: the probe measured `innerWidth == clientWidth`
+(gap 0), so `100vw` and the containing block agree here and the change would have
+been an unmeasured edit to a live width cap. The units stay `vw`; the toast is
+fixed by the specificity rule above, which *was* reproduced.
+
+## Verification
+
+Behavioural, not textual: the probe compares each scroll container's computed
+`max-height` against `visualViewport.height` and counts any cap that exceeds it
+(`staticVh`). The gate fails on a non-zero count, so the assertion survives a
+selector rename. Measured at a mobile profile where the visual viewport is
+smaller than the large viewport. The toast cap was verified by reading its
+computed `max-width` and rendered rect at 1440 and 430.
+
+## Acceptance
+
+- `staticVh = 0` at every swept cell, including the 430-wide mobile profile.
+- No `calc(100vh` remaining in a scroll-surface cap.
+- Toast computed `max-width` is 480px at 1440 (not 542px) and keeps its 24px
+ inset at 430px.
diff --git a/gui/src/styles-dashboard-workspace.css b/gui/src/styles-dashboard-workspace.css
index cf62a99f30..9d1d20504b 100644
--- a/gui/src/styles-dashboard-workspace.css
+++ b/gui/src/styles-dashboard-workspace.css
@@ -174,6 +174,29 @@
Selects share a baseline. */
.dash-sidecar-row-card {
flex-wrap: wrap;
+ /* Pack the two wrapped flex LINES from the top of the card.
+
+ This is the line that makes the pair agree. The grid stretches both cards to the
+ taller one's height, and `align-content` defaults to `stretch` for a multi-line flex
+ container, so each card distributed its own leftover space across its own lines. The
+ two cards have equal outer height but different content height — the vision card's
+ control group carries the advanced disclosure — so the card with more slack pushed its
+ control line down and the two Selects sat 27.4px apart at en/1024, 27.8px at ko/1024,
+ and 7.1px at ru/1100.
+
+ `align-content`, not `align-items`: the mis-distributed thing is the LINES, which is
+ exactly what `align-content` controls. `align-items: center` stays inherited from
+ `.dash-delegation-summary` and still centres each item WITHIN its line, which is what
+ keeps the single-line horizontal layout (one-column regime) vertically centred. In
+ that regime the card has one line, where `align-content` is inert — so this rule acts
+ only where the bug exists.
+
+ Measured with a CDP viewport-override harness: worst paired offset 0.0px across
+ en/ko/ru/fr/ja at 1440/1100/1024/760, with the one-column row layout unchanged.
+
+ The file previously recorded `align-items: flex-start` on ONE card as a failed
+ attempt; that is a different property on a different box, and asymmetric. */
+ align-content: start;
/* The responsive axis is the CARD, not the window.
This grid is `repeat(auto-fit, ...)`, so card width is decoupled from viewport
@@ -194,15 +217,27 @@
}
/* Both cards wrap their control group onto a second line, so that line must start at the
- same y in both. It does not by default: the copy blocks are different heights (the ko
- hints are 30 vs 41 chars and wrap to a different line count in every locale), and each
- card's control line simply follows its own copy. Reserving the same copy band in both
- cards is what puts the two Selects on one line.
-
- 3.9375rem = 63px = 21px title + 3px hint margin + two 19.5px hint lines — the longest
- shipped hint at the narrowest two-column card. */
-.dash-sidecar-row-card .dash-sidecar-copy {
- min-height: 3.9375rem;
+ same y in both. It does not by default: each card's control line simply follows its own
+ copy, and the two hints wrap to a different number of lines in several locales.
+
+ The floor lives on the HINT and is measured in LINES, not on the copy block in pixels.
+ The previous `min-height: 3.9375rem` on the copy block was "63px = 21px title + 3px
+ margin + two 19.5px hint lines" — a pixel count derived from a two-line assumption. It
+ held wherever both hints wrapped to the same count and silently failed where they did
+ not: measured 19.5px of drift at ru and fr, whose vision hint takes a THIRD line at a
+ two-up card (82.5px of copy against 63px).
+
+ `3lh` is that same intent expressed in the unit that actually governs it: three line
+ boxes of the hint's own computed line-height. It covers the longest shipped hint, so the
+ shorter hint reserves the same three lines and both control rows start together. Because
+ it scales with font metrics rather than a hard-coded 19.5px, a font or line-height change
+ cannot invalidate it, and a longer translation only matters if it exceeds three lines —
+ which the regression test asserts by measuring rendered line counts, not string length.
+
+ Verified across all eight shipped locales at every two-up width (1600-740): worst offset
+ 0.0px, no hint truncated, no card collapsed. */
+.dash-sidecar-row-card .dash-sidecar-copy .setting-hint {
+ min-height: 3lh;
}
/* The shared sidecar copy rule is `flex: 1 1 0` so a one-row control group can
diff --git a/gui/tests/sidecar-layout.test.ts b/gui/tests/sidecar-layout.test.ts
index fa2b59313f..2b40980155 100644
--- a/gui/tests/sidecar-layout.test.ts
+++ b/gui/tests/sidecar-layout.test.ts
@@ -54,17 +54,31 @@ test("the copy block has a width floor and never breaks per glyph", async () =>
expect(Number(floor![1])).toBeGreaterThanOrEqual(14);
});
-test("both cards reserve the same copy band, so their control lines start together", async () => {
+test("the hint reserves the same LINE COUNT in both cards, not a pixel band", async () => {
const css = withoutComments(await Bun.file(cssUrl).text());
- const copy = allRuleBodies(css, ".dash-sidecar-row-card .dash-sidecar-copy");
- // Both cards wrap their control group onto a second flex line, and that line follows
- // its own card's copy height. The two hints are different lengths in every locale
- // (ko: 30 vs 41 chars), so without a shared band the two Selects drift by a line.
- const band = copy.match(/min-height:\s*([\d.]+)rem/);
- expect(band).not.toBeNull();
- // 21px title + 3px hint margin + two 19.5px hint lines = 63px = 3.9375rem.
- expect(Number(band![1])).toBeGreaterThanOrEqual(3.9);
+ // Both cards wrap their control group onto a second flex line, and that line follows its
+ // own card's copy height, so the copy row has to be equal in both cards.
+ //
+ // The old form of this rule was `min-height: 3.9375rem` on the COPY BLOCK — 63px, derived
+ // as "21px title + 3px margin + two 19.5px hint lines". Two problems, both measured:
+ // it assumed the hint wraps to two lines, and it hard-coded a line-height. At ru and fr
+ // the vision hint takes a third line at a two-up card (82.5px of copy against 63px), and
+ // the pair drifted 19.5px while en/ko/ja/zh/de/tr still measured clean.
+ //
+ // The floor now lives on the HINT and is expressed in `lh`, so it scales with the hint's
+ // own line-height and states the real constraint: reserve N lines.
+ const hint = allRuleBodies(css, ".dash-sidecar-row-card .dash-sidecar-copy .setting-hint");
+ const floor = hint.match(/min-height:\s*([\d.]+)lh/);
+ expect(floor).not.toBeNull();
+ // Three lines is the longest shipped hint at the narrowest two-up card. Fewer than three
+ // re-opens the ru/fr drift; the number is a measurement, not a preference.
+ expect(Number(floor![1])).toBeGreaterThanOrEqual(3);
+
+ // The pixel band must be gone from the copy block: leaving both would make it ambiguous
+ // which one is load-bearing, and the pixel one is the one that was wrong.
+ const copy = allRuleBodies(css, ".dash-sidecar-row-card .dash-sidecar-copy");
+ expect(copy).not.toMatch(/min-height:\s*[\d.]+rem/);
});
test("both control groups reserve the same band and pack from its top", async () => {
@@ -86,6 +100,19 @@ test("both cards wrap, so neither resolves its control group differently", async
const card = allRuleBodies(css, ".dash-sidecar-row-card");
expect(card).toMatch(/flex-wrap:\s*wrap/);
+ // The wrapped LINES must pack from the top of the card. Equal copy bands alone are not
+ // enough: the grid stretches both cards to the taller one's height, and `align-content`
+ // defaults to `stretch` for a multi-line flex container, so each card spread its own
+ // leftover space across its own lines. The two cards' content heights differ (the vision
+ // control group carries the advanced disclosure), so the card with more slack pushed its
+ // control line down — 27.4px at en/1024, 27.8px at ko/1024, 7.1px at ru/1100, and again
+ // at 760px where the sidebar leaves the flow and the grid re-splits into two columns.
+ //
+ // This was verified by measuring the rendered page across 8 locales: with the bands but
+ // WITHOUT this line the copy blocks were already equal (63/63) and the offset was still
+ // 27.4px, which is what proves the lines — not the copy — were the mis-distributed thing.
+ expect(card).toMatch(/align-content:\s*start/);
+
// Wrapping only the vision card put its control group on a second line while the
// web-search group stayed on the first — a guaranteed baseline mismatch. Likewise
// `align-items: flex-start` on one card only: the two must resolve by the same rules.
@@ -93,6 +120,9 @@ test("both cards wrap, so neither resolves its control group differently", async
if (vision) {
expect(vision[2]).not.toMatch(/align-items:\s*flex-start/);
expect(vision[2]).not.toMatch(/flex-wrap:\s*wrap/);
+ // Same asymmetry hazard for the new rule: it belongs on the shared card class so both
+ // cards resolve their lines identically, never on one of them.
+ expect(vision[2]).not.toMatch(/align-content:/);
}
});
@@ -163,4 +193,3 @@ test("narrow-card rules apply to both cards, never one of them", async () => {
}
}
});
-