Fix the dashboard on mobile
doingEvery page overflows horizontally at phone width. Collapse the nav into a real menu, stop the card grid blowing out, and make wide content scroll inside itself.
Task graph
Ordered the way the dispatcher would work it. Each row shows the loop's own verdict —
this is the same computation scripts/next.mjs runs, not a second opinion.
- dm-005 Tap targets and spacing pass across every page todo
5 acceptance criteria
- No interactive element has a smaller dimension under 44px at 375px — audit reports zero violations
- Decision card option buttons remain comfortably tappable and keep their full-width layout
- Chips, .more-link, .q-proj and .trow-lane are legible without zooming
- Nothing on desktop grows: a >=1080px screenshot is unchanged from before this task
- mobile-audit clean across every route at all three widths
The sweep-up task, deliberately last: it depends on the three structural fixes because measuring tap targets in a layout that still overflows produces noise.
The measured starting point is nav links at 20px tall. Expect the same problem in chips,
.more-link, and the small monospace metadata rows, which were all sized for a desktop cursor.The desktop-unchanged criterion is the guard rail. The easy way to hit 44px everywhere is to inflate padding globally, which would wreck the dense information layout that makes this dashboard scannable on a laptop. Scope the growth to the mobile breakpoint.
- dm-006 Gate the build on the mobile audit todoneeds dm-005
4 acceptance criteria
- npm run audit:mobile runs the harness against a fresh build
- The dispatcher's cycle finish step fails when the audit fails, so a regression cannot be pushed silently
- The gate adds under 30s to a cycle
- Documented in README so the next person knows why the build refuses
Without this, mobile drifts back. Every page in this repo is generated from data that changes daily — a new project with a long name, a doc title nobody anticipated — and the layouts that overflow today were all fine when written.
Wire it into
scripts/cycle.mjs’s finish step alongsidenpm run build, so the loop cannot push a regression it introduced itself.Lane 1: it is CI plumbing. If the gate is wrong it is loosened in a follow-up, and a gate that is too strict fails loudly rather than shipping something broken.
- dm-001 Mobile audit harness — measure overflow, fail on regression done
5 acceptance criteria · 1 run
- scripts/mobile-audit.mjs builds the site, serves dist, and measures scrollWidth - clientWidth on every route at 320, 375 and 414px
- Routes are discovered from the build output, not hardcoded — a new page is audited without editing the script
- Names the offending elements (tag, class, width, computed overflow-x), not just the page
- Also reports interactive elements whose smaller dimension is under 44px
- Exits non-zero when any page overflows; exits 0 and prints a one-line summary when clean
This comes first because every other task in this mission needs a checkable definition of done, and “looks fine on my phone” is not one. A verifier working from a diff cannot see a layout. It can read an exit code.
Chromium is already present at
/opt/pw-browsers/chromium-1194/chrome-linux/chromewithPLAYWRIGHT_BROWSERS_PATH=/opt/pw-browsers— do not download a browser.Reuse the approach that produced the numbers in the mission spec: walk
body *, collect elements whose bounding rect crosses the viewport edge, and skip any element whose parent already overflows so the report names the actual cause rather than every ancestor containing it.Lane 1: it is a script in
scripts/, it changes no rendered output, and a bad version is deleted rather than reverted.Log
- 2026-07-25 — claimed, attempt 1/3.
- 2026-07-25 — verified pass. Auto-merged (lane 1).
- ok 2026-07-25 attempt 1 $1.10 Audit harness: auto-discovers routes from dist, measures overflow at 320/375/414, names offenders, checks 44px targets. Found 66 failing page/width combos.
- dm-002 Collapse the top nav into a mobile menu with a close control doneneeds dm-001
8 acceptance criteria · 1 run
- Below 720px the six nav links are replaced by a single menu button; above it the current inline row is unchanged
- The panel has a visible close (×) control reachable without scrolling
- Escape closes the panel; focus returns to the button that opened it
- Focus is trapped inside the panel while it is open
- The menu button and every link inside the panel are at least 44x44px
- aria-expanded on the button and aria-label on the close control both reflect state
- With JavaScript disabled the links remain reachable — the panel must not hide them behind a control that cannot open
- mobile-audit reports zero overflow attributable to .topnav at all three widths
The single biggest cause:
.topnavis a six-item flex row with no wrapping and no mobile treatment, measuring 329px wide with its right edge at 439px inside a 375px viewport. It is why almost every page overflows by exactly the same amount.Touch it in
src/layouts/Base.astroandglobal.cssonly. Every page shares this layout, so this one task fixes the 64px baseline overflow everywhere at once — which is also why it is worth getting the focus handling right rather than shipping a checkbox hack.The no-JS criterion is not hypothetical politeness: this dashboard sits behind Cloudflare Access, and an auth interstitial that runs before the page scripts should never be able to strand the nav.
Out of scope: changing which links exist, or their order.
Log
- 2026-07-25 — claimed, attempt 1/3.
- 2026-07-25 — verified pass. Parked for approval (lane 2).
- 2026-07-26 — approved and merged.
- ok 2026-07-25 attempt 1 $2.40 Nav collapses to a native <details> menu below 720px with a full-screen panel, close x, Escape, focus trap and backdrop tap. Works with JS disabled.
- dm-003 Stop the card grid blowing out its track on / doneneeds dm-001
5 acceptance criteria · 1 run
- .card.pcard width tracks the viewport at 320, 375 and 414px instead of staying at 595px
- Long document titles still truncate with an ellipsis rather than wrapping to three lines
- Fix is the min-width:0 cascade on the grid and flex chain, not a fixed width or a max-width on .card
- Two-column layout at >=820px is unchanged
- mobile-audit reports zero overflow on / at all three widths
.card.pcardmeasures 595px at every tested viewport — 320, 375 and 414 all produce the identical number. A constant width under a1frcolumn means content is setting the track’s min-content size.Grid and flex items default to
min-width: auto, which refuses to shrink below their content. One non-wrapping descendant props the entire column open, and thewhite-space: nowrapon.md-title/.md-metais the likely origin.The criterion names the fix deliberately: capping
.cardwith amax-widthwould make the symptom disappear at these three widths while leaving the real cause in place for the next component that hits it. Putmin-width: 0where the chain actually breaks.Runs in parallel with dm-002 and dm-004 — different files, no shared surface.
Log
- 2026-07-25 — claimed, attempt 1/3.
- 2026-07-25 — verified pass. Parked for approval (lane 2).
- 2026-07-26 — approved and merged.
- ok 2026-07-25 attempt 1 $1.20 min-width:0 cascade through the card/grid/flex chain. .card.pcard now tracks the viewport instead of pinning at 595px.
- dm-004 Wide content scrolls inside its own container doneneeds dm-001
5 acceptance criteria · 1 run
- Markdown tables in .doc scroll horizontally inside their own box; the page does not
- The /graph triple lists and .prov provenance paths stay inside the viewport at 320px
- Code blocks and inline <code> with long unbreakable strings do not widen the page
- A scrollable region is discoverable — it is visually apparent that there is more to the right
- mobile-audit reports zero overflow on /graph and on /p/insuladent/google-ads-web-plan at all three widths
Two measured cases: a markdown table rendering 437px wide on the Insuladent plan page, and the serialised graph triples on
/graph, which are long unbreakable strings like(2026-07-05-week) --[references]--> (cloudflare-deploy)plus a file path.The rule is that wide content scrolls inside its own box and never drags the page with it.
overflow-x: autoon a wrapper, notoverflow: hiddenon an ancestor — hiding it means the table’s right-hand columns become unreachable, which is worse than the scroll.The discoverability criterion matters on a phone: a scroll container with no visual affordance reads as truncated data rather than scrollable data.
Runs in parallel with dm-002 and dm-003.
Log
- 2026-07-25 — claimed, attempt 1/3.
- 2026-07-25 — verified pass. Parked for approval (lane 2).
- 2026-07-26 — approved and merged.
- ok 2026-07-25 attempt 1 $0.90 Tables scroll inside themselves, triples wrap, pre/code contained. Audit clean on /graph and doc pages.
The spec
Fix the dashboard on mobile
The dash is described as mobile-first and currently is not usable on a phone. Every page scrolls sideways.
Measured, not guessed
Audited with headless Chromium at 320 / 375 / 414 px across twelve
representative pages. Horizontal overflow (scrollWidth - clientWidth):
| Page | 320px | 375px | 414px |
|---|---|---|---|
/ | 291px | 236px | 197px |
| every other page | 119px | 64px | 64px |
/p/insuladent/google-ads-web-plan | 133px | 78px | — |
Three distinct causes:
1. The top nav, on every page. .topnav is a six-item flex row with no
wrapping and no mobile treatment. It measures 329px wide and its right edge lands
at 439px inside a 375px viewport. This is the overlay running off the screen, and
it is why the number is identical on almost every page. Nav links are also 20px
tall — less than half the 44px minimum tap target.
2. The card grid on /. .card.pcard renders 595px wide regardless of
viewport — 320, 375 and 414 all produce the same 595. A constant width under a
1fr grid column means content is setting the track’s min-content size: grid and
flex items default to min-width: auto, so one non-wrapping descendant props the
whole column open. The nowrap/ellipsis rules on .md-title and .md-meta are the
likely culprits, but the fix is the min-width: 0 cascade, not the symptom.
3. Wide content with no scroll container. Markdown tables in doc pages render
437px wide. The /graph triple list overflows its container by a pixel at 320px
because the serialised triples are long unbreakable strings.
Goal
No page scrolls horizontally at 320px or wider. The nav is a real menu on phones, openable and closable. Wide content scrolls inside its own box rather than dragging the page with it.
Scope
- A repeatable audit that measures overflow, so “fixed” is checkable rather than a matter of opinion.
- The nav becomes a hamburger below the desktop breakpoint: opens a panel, closes with an explicit × control, closes on Escape, and returns focus where it came from.
- The card grid stops blowing out its track.
- Tables, triple lists and code blocks scroll inside
overflow-x: autocontainers. - Tap targets reach 44px.
Non-goals
- No redesign. Same light theme, same
#ff5a1faccent, same 8px spacing scale, same components. This is a layout repair, not a visual refresh. - No CSS framework, no JS framework. The site has zero runtime dependencies and keeps them.
- No new breakpoints beyond the three already in
global.css(700 / 720 / 820) unless a task’s criteria genuinely require one — three is already one too many and consolidating them is not this mission’s job. - Not touching desktop layout. If a desktop screenshot changes, that is a regression.
Constraints
- Astro 4, static output. Any menu JS is vanilla, in the layout, and must survive a page navigation — every page is a full load, so no persistent app state to rely on.
- The menu must work with JS disabled to the extent of not trapping the user: if the panel cannot open, the links stay reachable.
npm run buildmust pass.
Acceptance (mission-level)
- The audit script reports zero horizontal overflow on every page at 320, 375 and 414 px.
- The nav menu opens and closes by tap, closes on Escape, and its close control is visible without scrolling.
- No interactive element is under 44px in its smaller dimension.
- Desktop (≥1080px) renders unchanged — verified by screenshot comparison, not by eye.
The line
Everything here is reversible CSS and markup in a private dashboard behind Cloudflare Access. The audit harness and the CI gate are lane 1. The visible layout changes are lane 2 — not because they are dangerous, but because “does this look right on a phone” is a judgement the verifier cannot make from a diff, and the screenshots are worth a glance before they land.