Type · code (UI layout defect) · Parent · root sibling (discovered during MOTIR-2219, whose story 11.5 and epic MOTIR-1850 are both done) · Discovered in · manual dogfooding — minting a PAT for the cli-v0.2.0 anonymous-install verification (MOTIR-2219 step 4).
Settings → Account → API tokens → Create opens the modal, and there is no Create token button and no Cancel button. Nothing scrolls: the panel has no scrollbar, so the footer cannot be reached by any means. The form is unusable — a PAT cannot be minted from the UI.
origin/main @ 0d5279da)Modal's panel caps its own height and clips:
packages/design-system/src/components/ui/Modal.tsx:45
'flex max-h-[90vh] flex-col overflow-hidden'
The scroll recipe that makes that survivable lives in Modal.Body (Modal.tsx:238-256) — flex min-h-0 flex-1 flex-col overflow-y-auto. Its own doc comment names this exact failure: "instead of the dialog growing off-screen (e.g. the create modal's expandable Explanation pushing the Create button out of view)."
CreateTokenModal never uses Modal.Body (zero occurrences in the file). Its whole form phase is one bare <form className="flex flex-col gap-4"> (app/(authed)/settings/account/_components/CreateTokenModal.tsx:301) with Modal.Footer as its last child (:419-431). A flex item defaults to min-height: auto, so the form refuses to shrink below its content height; the panel clips the overflow; and because no ancestor sets overflow-y: auto, no scrollbar appears anywhere. The footer — Cancel and Create token — is simply painted outside the clip box.
Same class as the sticky-under-a-clipping-ancestor trap: the element is in the DOM and passes every unit assertion, but is unreachable on screen.
This is the tallest form in the app. It is deliberately widened to max-w-[42rem] so all six permission scopes show at once — "width, not scroll" (:261-265) — which trades width for a lot of height. It then grows further, conditionally:
multiOrg (account spans ≥2 organizations) adds a full-width label + Combobox row — :322-338multiWorkspace (≥2 workspaces in the selected org) swaps the flat readonly field for a Combobox — :343-357:409-417Every test in tests/e2e/api-tokens.spec.ts runs against a fresh signup — one org, one workspace — at Playwright's default 1280×720. That is the shortest variant of the form, and it fits. The multi-org / multi-workspace variant, which is what a real dogfooding account renders, has no coverage at any viewport, and no test constrains viewport height.
Adopt the canonical pattern already used two files over in app/(authed)/_components/CreateIssueModal.tsx:222-227 — the form becomes the flex column, the fields go in Modal.Body, the footer stays a sibling INSIDE the form so type="submit" keeps working:
<form className="flex min-h-0 flex-1 flex-col" onSubmit={…}>
<Modal.Body className="gap-4">…fields…</Modal.Body>
<Modal.Footer className="shrink-0">…</Modal.Footer>
</form>
MOTIR-462 (bug-sprint-report-modal-clipped-burndown) was the same defect — "SprintReport bypasses Modal.Body, so the burndown section is clipped off the bottom with no scroll affordance" — fixed at ONE site with no sweep of the others. A sweep of the remaining <Modal> call sites that never wrap their fields in Modal.Body found ~40 files; most are short confirm dialogs that fit, but BoardConfigEditor, WorkflowEditor, WidgetConfigModal and AutomationSettings are tall enough to be worth measuring. That sweep is NOT in this card's scope — it wants its own card.
Modal.Body) while the footer stays pinned; the panel itself never grows past 90vh.onSubmit — the primary button keeps type="submit" and stays inside the <form>.tests/e2e/api-tokens.spec.ts cases still pass unchanged.FIXED — PR #1960, squash-merged as e67536bc (2026-08-08), full CI green.
The fields moved into Modal.Body (which owns the flex min-h-0 flex-1 overflow-y-auto scroll recipe) and the footer is pinned beside it with shrink-0, both still inside the <form> so type="submit" keeps working — the CreateIssueModal pattern named in the fix direction above.
The regression test breaks both blind axes at once, because either alone still passes: a ≥2-org account created server-side (a single-org account has no UI path to org #2), at a 1280×700 viewport, asserted with toBeInViewport() rather than toBeVisible() — the clipped button has a bounding box and answers every role query, which is precisely why unit and E2E both stayed green while the surface was unusable. Verified failing before the fix ("viewport ratio 0") and passing after; the four existing cases are untouched.
The class-level sweep is MOTIR-2491.