diff --git a/apps/website/src/app/global.css b/apps/website/src/app/global.css
index 455f1f8fa..208b05d53 100644
--- a/apps/website/src/app/global.css
+++ b/apps/website/src/app/global.css
@@ -21,6 +21,8 @@
html {
overflow-x: hidden;
+ /* Anchor jumps clear the fixed nav (--nav-h defined in styles/chrome.css). */
+ scroll-padding-top: calc(var(--nav-h) + 16px);
}
body {
@@ -29,7 +31,15 @@ body {
font-family: var(--font-inter);
-webkit-font-smoothing: antialiased;
-moz-osx-font-smoothing: grayscale;
- overflow-x: hidden;
+ /* overflow-x: CLIP, never hidden. `hidden` makes a scroll container
+ * and every position:sticky descendant pins against it — which never
+ * scrolls — so the docs rails were dead site-wide. `clip` clips identically
+ * but creates no scroll container, so sticky works AND the mobile layout
+ * viewport stays pinned to the device width (without any body guard,
+ * min-content overflow forces phones to zoom out — measured vw 421 on a
+ * 320 device). Real overflow sources are still fixed at the source; this
+ * is the belt, not the fix. */
+ overflow-x: clip;
}
/* Code blocks scroll internally; never expand the layout viewport. */
diff --git a/apps/website/src/styles/chrome.css b/apps/website/src/styles/chrome.css
index b92cbe396..afaa16276 100644
--- a/apps/website/src/styles/chrome.css
+++ b/apps/website/src/styles/chrome.css
@@ -14,6 +14,21 @@
* 'var(--font-garamond,"EB Garamond",Georgia,serif)') are intentional.
*/
+/* One nav height. The fixed nav measures 58px (px-6 py-4) and 81px at the md
+ * breakpoint (md:px-8 md:py-5, 1px border included). Everything that offsets
+ * against the nav — the docs shell's top padding, both sticky rails, the
+ * mobile overlay, and html's scroll-padding for anchor jumps — reads this one
+ * variable instead of hardcoding its own guess (the old hardcoded 80px left
+ * 22px of dead space on phones; anchors landed 81px under the nav). */
+:root {
+ --nav-h: 58px;
+}
+@media (min-width: 768px) {
+ :root {
+ --nav-h: 81px;
+ }
+}
+
/* Footer */
.footer-root {
background: var(--color-surface);
@@ -132,7 +147,7 @@
margin: -12px;
}
.nav-mobile-overlay {
- top: 57px;
+ top: calc(var(--nav-h) - 1px);
z-index: 9999;
background: var(--color-surface);
border-top: 1px solid var(--color-border);
diff --git a/apps/website/src/styles/docs.css b/apps/website/src/styles/docs.css
index 413b6868a..4bad69ac6 100644
--- a/apps/website/src/styles/docs.css
+++ b/apps/website/src/styles/docs.css
@@ -457,6 +457,10 @@
}
.mdx-step-content {
flex: 1;
+ /* flex:1 leaves min-width:auto; the item refuses to shrink below its
+ * content's min-content width and blew the quickstart pages 512px past a
+ * 320px viewport (findings §2). */
+ min-width: 0;
padding-bottom: 8px;
}
.mdx-step-title {
@@ -701,9 +705,13 @@
.docs-sidebar {
border-right: 1px solid var(--color-border);
background: var(--color-surface);
- min-height: calc(100vh - 5rem);
position: sticky;
- top: 5rem;
+ top: var(--nav-h);
+ /* Without align-self the flex row stretches the aside to the article's full
+ * height (measured 10,030px), so its overflow-y:auto never engaged. */
+ align-self: flex-start;
+ min-height: calc(100vh - var(--nav-h));
+ max-height: calc(100vh - var(--nav-h));
}
.docs-sidebar-lib-trigger {
background: var(--color-surface);
@@ -890,9 +898,9 @@
/* DocsTOC */
.docs-toc {
position: sticky;
- top: 5rem;
+ top: var(--nav-h);
align-self: flex-start;
- max-height: calc(100vh - 6rem);
+ max-height: calc(100vh - var(--nav-h) - 1rem);
overflow-y: auto;
}
.docs-toc-label {
diff --git a/apps/website/src/styles/landing.css b/apps/website/src/styles/landing.css
index ac3360c28..65c007af5 100644
--- a/apps/website/src/styles/landing.css
+++ b/apps/website/src/styles/landing.css
@@ -90,7 +90,9 @@
@keyframes blink { to { visibility: hidden; } }
@media (max-width: 900px) {
.hero-grid {
- grid-template-columns: 1fr !important;
+ /* minmax(0,…): bare 1fr keeps an auto minimum, so one wide child pushed
+ * the stacked column past the viewport (findings §2). */
+ grid-template-columns: minmax(0, 1fr) !important;
gap: 40px !important;
}
}
@@ -190,7 +192,7 @@
}
.feature-block-card-row {
display: grid;
- grid-template-columns: repeat(auto-fit, minmax(140px, 1fr));
+ grid-template-columns: repeat(auto-fit, minmax(min(140px, 100%), 1fr));
gap: 8px;
margin-bottom: 24px;
}
@@ -219,7 +221,9 @@
}
@media (max-width: 900px) {
.feature-block-grid {
- grid-template-columns: 1fr !important;
+ /* minmax(0,…): bare 1fr keeps an auto minimum, so one wide child pushed
+ * the stacked column past the viewport (findings §2). */
+ grid-template-columns: minmax(0, 1fr) !important;
gap: 32px !important;
}
.feature-block-grid > div {
@@ -277,6 +281,9 @@
}
.why-row__body {
flex: 1;
+ /* min-width:auto refused to shrink past the nowrap primitive chip and
+ * pushed the row 52px past a 320px viewport (findings §2). */
+ min-width: 0;
display: flex;
gap: 16px;
align-items: baseline;
@@ -421,8 +428,27 @@
.wp-cover-wrap {
display: flex;
justify-content: center;
+ /* Grid items have min-width:auto too — without this the wrap sat at its
+ * content's 392px inside a 280px track. */
+ min-width: 0;
+}
+/* The frame is shrink-to-fit, so the cover's `100%` was indefinite and its
+ * aspect-ratio kept deriving a fixed ~392px width from content height. A
+ * definite width (capped by the frame's own inline maxWidth) lets the cover
+ * track the viewport. Scoped to the stacked breakpoint: at desktop the grid
+ * track is wider than the card, and an unconditional 100% would grow it. */
+@media (max-width: 640px) {
+ .wp-cover-wrap > [data-ui="browser-frame"] {
+ width: 100%;
+ }
}
.wp-cover {
+ /* aspect-ratio alone derives width from the content's height, which makes
+ * the cover a fixed ~392px regardless of container — the audit's 392px
+ * card forcing phones to clip. Pin the width; aspect-ratio then drives
+ * height, and content taller than the ratio still stretches it (min-height
+ * auto). */
+ width: min(100%, 392px);
aspect-ratio: 8.5 / 11;
background: linear-gradient(135deg, #fafbfc 0%, #eaf3ff 100%);
padding: 48px 36px;
@@ -462,7 +488,9 @@
}
@media (max-width: 900px) {
.wp-grid {
- grid-template-columns: 1fr !important;
+ /* minmax(0,…): bare 1fr keeps an auto minimum, so one wide child pushed
+ * the stacked column past the viewport (findings §2). */
+ grid-template-columns: minmax(0, 1fr) !important;
gap: 40px !important;
}
}
@@ -569,7 +597,9 @@
}
@media (max-width: 900px) {
.pilot-block-grid {
- grid-template-columns: 1fr !important;
+ /* minmax(0,…): bare 1fr keeps an auto minimum, so one wide child pushed
+ * the stacked column past the viewport (findings §2). */
+ grid-template-columns: minmax(0, 1fr) !important;
gap: 40px !important;
}
}
@@ -662,7 +692,7 @@
}
.ecosystem-row-tiles {
display: grid;
- grid-template-columns: repeat(auto-fit, minmax(150px, 1fr));
+ grid-template-columns: repeat(auto-fit, minmax(min(150px, 100%), 1fr));
gap: 10px;
}
[data-ui="ecosystem-tile"]:hover {
@@ -672,7 +702,9 @@
}
@media (max-width: 760px) {
.ecosystem-row {
- grid-template-columns: 1fr !important;
+ /* minmax(0,…): bare 1fr keeps an auto minimum, so one wide child pushed
+ * the stacked column past the viewport (findings §2). */
+ grid-template-columns: minmax(0, 1fr) !important;
gap: 8px !important;
}
.ecosystem-row > div:first-child {
@@ -712,7 +744,7 @@
}
.promises-grid {
display: grid;
- grid-template-columns: repeat(auto-fit, minmax(200px, 1fr));
+ grid-template-columns: repeat(auto-fit, minmax(min(200px, 100%), 1fr));
gap: 16px;
max-width: 1100px;
margin: 0 auto;
@@ -752,7 +784,7 @@
}
.recent-articles-grid {
display: grid;
- grid-template-columns: repeat(auto-fill, minmax(300px, 1fr));
+ grid-template-columns: repeat(auto-fill, minmax(min(300px, 100%), 1fr));
gap: 16px;
}
.recent-articles-footer {
@@ -829,7 +861,7 @@
/* BackendsGrid — components/landing/ag-ui/BackendsGrid.tsx */
.backends-grid {
display: grid;
- grid-template-columns: repeat(auto-fit, minmax(140px, 1fr));
+ grid-template-columns: repeat(auto-fit, minmax(min(140px, 100%), 1fr));
gap: 10px;
padding: 16px;
background: var(--color-surface-tinted);
diff --git a/apps/website/src/styles/pages.css b/apps/website/src/styles/pages.css
index d988f24c7..c61d1053d 100644
--- a/apps/website/src/styles/pages.css
+++ b/apps/website/src/styles/pages.css
@@ -364,7 +364,7 @@
* not this migration. */
.docs-shell-page {
background: var(--color-canvas);
- padding-top: 80px;
+ padding-top: var(--nav-h);
}
.docs-shell-body {
background: var(--color-surface);
@@ -410,6 +410,9 @@
margin-top: 32px;
}
.about-link {
+ /* The full repo URL is one unbreakable token; without this it pushed the
+ * page 9px past a 320px viewport (findings §2). */
+ overflow-wrap: anywhere;
color: var(--color-accent);
}
@@ -1139,7 +1142,7 @@
}
.blog-index-grid {
display: grid;
- grid-template-columns: repeat(auto-fill, minmax(300px, 1fr));
+ grid-template-columns: repeat(auto-fill, minmax(min(300px, 100%), 1fr));
gap: 16px;
}
diff --git a/apps/website/src/styles/ui.css b/apps/website/src/styles/ui.css
index 334b08d9d..b12630fd8 100644
--- a/apps/website/src/styles/ui.css
+++ b/apps/website/src/styles/ui.css
@@ -314,6 +314,13 @@
}
[data-ui="browser-frame-url"] {
flex: 1;
+ /* An unbreakable mono URL is the frame's min-content; without these three
+ * it propagated a ~390px floor up to the page and forced phones to zoom out
+ * (findings §2). Real browser chrome ellipsizes — so do we. */
+ min-width: 0;
+ overflow: hidden;
+ text-overflow: ellipsis;
+ white-space: nowrap;
text-align: center;
font-family: "JetBrains Mono", monospace;
font-size: 11px;
diff --git a/docs/superpowers/plans/2026-08-30-docs-polish-arc.md b/docs/superpowers/plans/2026-08-30-docs-polish-arc.md
new file mode 100644
index 000000000..c7f273350
--- /dev/null
+++ b/docs/superpowers/plans/2026-08-30-docs-polish-arc.md
@@ -0,0 +1,56 @@
+# Docs Polish Arc Implementation Plan (Project 3)
+
+> **For agentic workers:** This plan is executed by the controller directly with
+> live browser measurement — the implement-measure-adjust loop each fix needs.
+> Steps use checkbox (`- [ ]`) syntax for tracking.
+
+**Goal:** Fix every defect in `docs/superpowers/audits/2026-08-29-docs-visual-review-findings.md` — the review that started this whole arc. Unlike Projects 1–2, these PRs **deliberately change what readers see**; each change is bounded by a measured before-state from the audit and a measured after-assertion here.
+
+**Prerequisites (all merged):** #845 (tokens), #848–#858 (substrate). Every fix below is now a stylesheet edit.
+
+**Verification stance:** the migration's "signature-identical to prod" gate inverts — for each defect the audit's measured *broken* value must flip to the specified *fixed* value, and surfaces a PR does not target must stay signature-identical to prod at 1280 (blast-radius check).
+
+---
+
+## PR 1 — structural: overflow truth, sticky rails, one nav height
+
+Findings §1, §2, §5, §6(partial).
+
+- [ ] **`--nav-h`** custom property on `:root` in `chrome.css`: `58px`, `81px` at `min-width: 768px` (measured nav heights). Consumers: docs shell `padding-top` (was hardcoded 80px → 22px mobile dead space), sidebar/TOC sticky `top` + `max-height`, mobile overlay `top` (`calc(var(--nav-h) - 1px)`, was 57px), and `html { scroll-padding-top: calc(var(--nav-h) + 16px) }` — which fixes every deep link landing under the nav (§5) without touching a heading.
+- [ ] **Revive sticky** (§1): delete `overflow-x: hidden` from `body` (it makes `` a scroll container; sticky descendants pin against it and never stick) and the `overflow-x-hidden` utility from the docs shell div. `html`'s guard **stays** — verified it propagates to the viewport without breaking sticky. Sidebar additionally needs `align-self: flex-start` + `max-height: calc(100vh - var(--nav-h))` so its own `overflow-y: auto` finally engages (it was 10,030px tall).
+- [ ] **Fix the real overflow sources the guards were masking** (§2), then prove no page scrolls horizontally at 320/375/768 *with the guard gone*:
+ - `
` body: `min-width: 0` on `.mdx-step-body` (flex item refused to shrink; +512px on quickstarts at 320).
+ - landing `.why-row__body` / hero demo box / whitepaper card: responsive max-widths.
+ - blog index cards (`width: 300px` fixed) → fluid with a 300px cap.
+ - about page GitHub URL → `overflow-wrap: anywhere`.
+ - anything else the 320px probe finds — the probe is the authority, not this list.
+- [ ] **Assertions:** at `scrollY=3000` on a docs page, sidebar and TOC `getBoundingClientRect().top === nav-h`; hash-jump to a mid-page heading lands its top ≥ nav bottom; docs shell top padding equals actual nav height at 375 and 1280; `documentElement.scrollWidth === innerWidth` on all 15 probe pages at 320 AND 375; non-targeted pages signature-identical to prod at 1280.
+
+## PR 2 — details: breadcrumb, tables, rails, mdx
+
+Findings §3, §4, §6(rail), §9.
+
+- [ ] **Breadcrumb** (§3): typography moves to the `` (13px), `align-items: center`, separators become `li + li::before` (or equivalent) so they can never diverge from link size again; wrapped-row gap. Assertion: separator and link share font-size and vertical center on every crumb.
+- [ ] **Tables** (§4): `min-width` on tables inside `.docs-table-scroll` so the scroller scrolls (at 375 the props table rendered `agent` as three lines in a 49px column, rows 227px tall); edge-fade affordance; `tabindex="0"` + `role="region"` + aria-label on the wrapper (keyboard reachability); same wrapper treatment for `ApiDocRenderer`/`ApiRefTable` tables. Inline `code` chips get `word-break: normal` so `@threadplane/langgraph` stops splitting into two pills.
+- [ ] **One horizontal rail** (§6): breadcrumb/header wrapper, article, API block, and prev/next all share one padding scale (the 8px misalignment at <640px).
+- [ ] **`` connector**: `:last-child` kills the dangling line below the final step.
+- [ ] **Dead CSS** (§9): delete the `.shiki` and `[data-rehype-pretty-code-title]` rules (zero matching elements site-wide; documented in the audit).
+- [ ] **Assertions:** table wrapper `scrollWidth > clientWidth` at 375 with single-line `agent` cell; breadcrumb metrics; rail lefts equal at 375; docs pages signature vs prod shows deltas ONLY on the targeted elements.
+
+## PR 3 — a11y + interaction
+
+Findings §6(anchors/targets), §7.
+
+- [ ] Shared `:focus-visible` ring (`var(--shadow-focus)`) on interactive docs chrome.
+- [ ] **DocsSearch**: `role="dialog"` + `aria-modal`, focus trap + restore, `listbox`/`option` semantics, selected item scrolled into view, and a **mobile entry point** (the ⌘K trigger lives in the desktop-only sidebar).
+- [ ] **Tabs/TabGroup**: `role="tablist"`/`tab`/`tabpanel`, arrow keys, horizontal scroll on narrow screens.
+- [ ] **PageActions**: menu keyboard nav + item hover/focus styles.
+- [ ] Heading anchors get a mobile-visible affordance (currently `display: none` below 768px — no way to deep-link from a phone).
+- [ ] Touch targets ≥44px (PageActions 32px, code-copy 28px) via hit-area padding, not visual growth.
+- [ ] `prefers-reduced-motion` guards `scroll-behavior: smooth`.
+- [ ] **Assertions:** axe-style manual checks per control; targets measured ≥44; keyboard walkthrough of search + tabs + menu.
+
+## Deferred, explicitly
+
+- **Font unification onto next/font** (findings §10): a real sitewide visual change; needs its own decision.
+- **Toast width on small screens**; **`AnnouncementToast` focus management** — candidates for a later pass.