17 (motir-core) Soft playful OVERFLOWS the left nav rail at both widths — its 40px `--height-control` is 1px wider than the 56px collapsed rail's content box, and pushes the expanded rail past the fold 68–76px earlier than the default style
Type · implementation defect (a raw layout constant that does not track the shape token it exists to hold, plus a single-axis overflow declaration). Not a planning mistake — no card asked for a rail width that ignores --height-control, and the shell already documents the CSS rule the rail misses. No lesson owed.
Parent · parentless at the ROOT. Reported out-of-band by Yue from manual dogfooding (data-style="soft-playful" on /plans), so there is no finding story to carry a blocked_by edge into and no in-flight card whose epic could take it. Same placement and same shape as MOTIR-3522, which was a named style exposing a shell defect the style's own card never touched.
Discovered in · manual dogfooding, 2026-09-02. Two screenshots: the expanded 240px rail and the collapsed 56px rail, both drawing a full-height classic scrollbar; the collapsed one also draws a horizontal bar under the last icon.
Root cause / fix
Reproduced, not read
Rendered headlessly against the REAL token layer — packages/design-system/theme.css compiled through @tailwindcss/postcss, the exact class strings components/ui/Sidebar.tsx emits, at the shell geometry components/ui/AppLayout.tsx builds (a h-14 top bar, then md:grid-cols-[240px_1fr] / md:grid-cols-[56px_1fr]). Measured on origin/main @ f8fb84d80f934fd1e5b8bb5a8ef99125d907dc24. There are two defects and they are independent.
Defect A — the collapsed 56px rail is NARROWER than the control it holds. Viewport-independent.
AppLayout.tsx:117 states the rail column as a raw literal:
collapsed ? 'md:grid-cols-[56px_1fr]' : 'md:grid-cols-[240px_1fr]',
The <nav> inside it is px-2 with a border-r, so its content box is 56 − 16 − 1 = 39px. Every collapsed row is Sidebar.tsx:131 / :143:
'mx-auto flex h-(--height-control) w-(--height-control) …'
--height-control is the style axis's own density token, and two of the eleven styles set it above 39px:
| style | --height-control | fits the 39px box? |
|---|---|---|
| swiss-minimal-flat · cybercore-y2k | 34px | ✅ |
| warm-editorial (the default) · neo-brutalism | 36px | ✅ |
| glassmorphism · aurora · neumorphism · hand-drawn-indie · retrofuturism | 38px | ⚠️ 1px of margin |
| soft-playful · 3d-immersive | 40px | ❌ overflows by 1px |
Measured under soft-playful, collapsed, at BOTH 1280×1074 and 1280×800: scrollWidth 40 into a clientWidth of 39 — hScroll: true. Under warm-editorial, 36 into 39 — hScroll: false. The 56px is the defect: it is a layout constant sized for a 36px control and nothing re-derives it when the style axis moves the control. motir-core/CLAUDE.md's shape rule is the standing statement of exactly this ("NEVER a fixed raw utility … so density flips too"); the rail is the one place a layout width is load-bearing on a control token, which is why the layout-spacing exemption does not cover it.
Defect B — the rail's scroller states ONE axis, so overflow-x computes to auto and the 1px of A becomes a second scrollbar
Sidebar.tsx:317:
<div className="flex min-h-0 flex-1 flex-col gap-3 overflow-y-auto">
Measured getComputedStyle: overflow-y: auto, overflow-x: auto — CSS Overflow 3 computes visible to auto whenever the other axis is non-visible. So A does not clip, it draws a horizontal bar in a 56px rail. The shell one file away already knows this — AppLayout.tsx's <main> carries a six-line comment stating the rule verbatim ("Left implicit, overflow-x does not stay visible: CSS Overflow 3 computes it to auto … so the shell's one scroller would acquire a horizontal bar nobody chose") and sets both axes deliberately. The rail's scroller was never given the same treatment.
The measurement behind the report's own words — the style moves the FOLD by 68–76px
Viewport height at and below which the expanded 240px rail starts to scroll, by rail configuration (the bottom section is 5 or 6 rows depending on workspaceTierRevealed; the primary section gains a 13th row when canResume):
| rail configuration | warm-editorial (default) | soft-playful |
|---|---|---|
| 12 primary + 5 bottom | 790px | 858px |
| 12 primary + 6 bottom | 828px | 900px |
| 13 primary + 6 bottom | 866px | 942px |
soft-playful raises --height-control 36 → 40px and --spacing-control-y 6 → 8px, which is 68–76px of extra rail across 17–19 rows. 1280×800 sits inside that band: the default style fits with room to spare and soft-playful does not — which is precisely "soft/playful theme gives the left nav bar a scroll bar". The scrolling itself is legitimate (a rail taller than the fold must scroll); what is not is that it draws the browser's classic scrollbar, which on the reporter's platform is ~15px and steals that width from a 56px rail — nothing anywhere in theme.css or globals.css styles a scrollbar at all (grep -n 'scrollbar\|::-webkit' over both: zero rules).
Fix direction
- A — the collapsed rail must be derived from
--height-control, not from56. Either widen the column to acalc()over the token (--height-control+ thepx-2gutters + the border) or bound the row to the rail it sits in (w-fullcapped at--height-control). Whichever is chosen must hold for every style, not for 40px: five styles sit at 38px with a single pixel of margin, so a fix that only clears today's two is the same defect one style later. - B — state
overflow-xexplicitly onSidebar.tsx's scroller, with a comment citing the same CSS Overflow 3 ruleAppLayout.tsx's<main>already cites.hiddenis the right value here (unlike<main>, where wide content must stay reachable — a nav row has nothing to reveal horizontally). - The bar itself — give the rail's scroller a thin, token-coloured scrollbar (
scrollbar-width: thin+scrollbar-colorfrom--el-*) so an overflowing rail costs ~6px rather than ~15px of a 56px column, and looks like the product rather than the OS.
Out of scope: re-deciding soft-playful's density, or the rail's row count. This card makes the rail correct at whatever density the style sets.
Acceptance criteria
- Under every registered
data-style, the collapsed rail's row fits its content box: rendered at the real compiled token layer at 1280×1074 and 1280×800, the rail's scroll container reportsscrollWidth <= clientWidth. Asserted over the style REGISTRY (STYLE_REGISTRY), not over a hard-coded list, so a new style with a taller control fails the test rather than shipping the defect. getComputedStyleoncomponents/ui/Sidebar.tsx's scroll container returnsoverflow-x: hidden(stated, not inherited), and the declaration carries a comment naming the CSS Overflow 3 computation, ascomponents/ui/AppLayout.tsx's<main>does.- The collapsed rail width is no longer a raw
56pxindependent of--height-control: changing--height-controlin a[data-style]block changes the rendered rail width, demonstrated by a test that reads both. - When the expanded rail DOES overflow (measure at 1280×760, where the default style also scrolls), its scrollbar is the thin token-coloured one, not the platform default — assert the
scrollbar-width/scrollbar-colordeclarations resolve on the scroller. - The default
warm-editorialrail renders unchanged at 1280×900 (no scroll, same row geometry) — the fix is not allowed to re-shape the style nobody reported. pnpm lint,pnpm typecheckand the theme suite are green.
Context refs
motir-core/components/ui/AppLayout.tsx:117— the rawmd:grid-cols-[56px_1fr]; and its<main>block, which states the CSS Overflow 3 rule and sets both axes (the model to follow).motir-core/components/ui/Sidebar.tsx:317— the single-axisoverflow-y-autoscroller;:131,:143,:194— theh-(--height-control) w-(--height-control)collapsed rows.motir-core/packages/design-system/theme.css— the@theme--height-control: 36pxand the eleven[data-style]blocks that move it (soft-playful and 3d-immersive to 40px).motir-core/packages/design-system/src/theme/styles.ts:435—DEFAULT_STYLE_ID = 'warm-editorial', the style the rail was sized against.motir-core/app/(authed)/_components/SidebarNav.tsx— the 12-or-13 primary rows and the 5-or-6 bottom rows the measurement counts.motir-core/tests/e2e/shell-viewport-floor.spec.ts·tests/theme/shellViewportUnits.test.ts— the shell's existing scroll invariants. Neither measures the RAIL, which is why this shipped.- MOTIR-2335 — the open story for the own-box padding/height escapes.
relates_to, NOT a blocker: that story's 447-literal population is a control's OWN box, and this is a layout column that must track one. No edge owed. - MOTIR-3522 — the sibling defect in the same class (a named style exposing shell code that keys on the wrong thing), and the parentless-at-root precedent.
- MOTIR-3208 — the card that made the shell own the only scroller and wrote the
<main>comment this fix mirrors.
Resolution
Open.