diff --git a/.changeset/scroll-lock-keeps-the-page.md b/.changeset/scroll-lock-keeps-the-page.md
new file mode 100644
index 0000000000000..b9a407e820293
--- /dev/null
+++ b/.changeset/scroll-lock-keeps-the-page.md
@@ -0,0 +1,7 @@
+---
+'@astryxdesign/core': patch
+---
+
+[fix] `useScrollLock`, and so Dialog, BottomSheet, Drawer, Lightbox and MobileNav, keeps the page where it is while an overlay is open. The lock used to pin `
` with `position: fixed` at minus the scroll offset and scroll back on release: the page's offset read 0 for the whole lock, and on iOS Safari the jump to the top brought the toolbar back, where it stayed after the overlay closed. Now the page gets `overflow: hidden` (on the root, or on `` when the body's own overflow is the one the viewport uses), and where the primary pointer is coarse (phones and tablets), a one-finger pan that would scroll the page is cancelled, which iOS Safari needs because it scrolls an `overflow: hidden` page under a finger. Pans that a scroller inside the overlay can take, drags on a native range input, pinch-zoom, and one-finger drags on a pinch-zoomed page (they pan the zoomed view) still work. The lock no longer pins ``, and `window.scrollY` keeps its value while the lock holds. The lock cancels those pans itself, leaving a touch a gesture recognizer owns to it, and judges each pan after a small slop so a swipe's first wobble does not fix the wrong axis. It follows the browser's scroll chain, so an overlay rendered inside a scrolling container still holds the page: a modal dialog's pans go to the page, not to that container. It also sets `overscroll-behavior-y: none` on the root and ``, which stops the page's overscroll bounce while an overlay is open. MobileNav now takes the same lock: while it is open, the page's bounce stops and one-finger pans are held as behind any other modal, and closing it while another modal holds the page keeps that modal's scrollbar gutter.
+
+@vjeux
diff --git a/docs/specs/AST-094-touch-presentations/spec.md b/docs/specs/AST-094-touch-presentations/spec.md
index 6c7bc290781bd..4c83a8680e501 100644
--- a/docs/specs/AST-094-touch-presentations/spec.md
+++ b/docs/specs/AST-094-touch-presentations/spec.md
@@ -332,7 +332,10 @@ a measured, sourced difference exists.
four exceptions. While a scroll lock under a modal presentation holds the
page, it MAY cancel the moves of a one-finger touch that no recognizer owns
when that pan would scroll the page; a second finger is never cancelled
- (FR10), and the page keeps pinch-zoom. Once its press is delivered, a
+ (FR10), and the page keeps pinch-zoom. A modal overlay that takes the page
+ scroll lock also stops the page's pull-to-refresh and overscroll bounce
+ while it holds the lock, and releases them with it. Once its press is
+ delivered, a
control that keeps its press through a drag (FR11) owns its touch and MAY
cancel that finger's moves. A scroll-edge pan, such as a sheet's pull
from a scroll area at its edge (FR14), MAY cancel its touch's moves from the
diff --git a/docs/specs/components/BottomSheet/BottomSheetSwitcher.spec.md b/docs/specs/components/BottomSheet/BottomSheetSwitcher.spec.md
index a9eabeb2229e0..d8f14bd6b7e1d 100644
--- a/docs/specs/components/BottomSheet/BottomSheetSwitcher.spec.md
+++ b/docs/specs/components/BottomSheet/BottomSheetSwitcher.spec.md
@@ -146,7 +146,7 @@ Consumer migration instructions belong in consumer docs and release notes.
| FR2 | Each child sheet owns one native dialog. Opening a sheet presents its dialog once; a handoff presents the entering sheet's dialog above the retained one and closes the retained dialog only after its panel has faded. Returning to a sheet whose dialog is still open beneath a newer one re-presents it so the interactive sheet is the topmost layer. | DEC-1 |
| FR3 | During a handoff, the entering sheet is above the retained sheet. A taller retained sheet aligns down to a shorter entering sheet, and the retained sheet fades only after required transform motion completes. Under reduced motion the handoff follows `design:reduced-motion`: no transform motion runs, and the sheets cross-fade (DR2) or change at once (DR3) in the same order. | Current source and transition tests; `design:reduced-motion` DR2, DR3 |
| FR4 | Closing retains the outgoing sheet through its exit, then closes the dialog. A modal flow restores focus to the element captured when that modal flow opened. | Current source and focus tests |
-| FR5 | `hasScrim=true` uses `showModal()`, a native backdrop, and native focus containment, and locks page scroll: the page does not scroll while a modal flow is presented; the lock is held continuously across a handoff, never released and re-acquired between steps; it still holds beneath a `Dialog` opened from a step; it is released after the flow's exit (DEC-1). `hasScrim=false` uses `show()` without a backdrop or scroll lock and leaves the page interactive. In a non-modal flow the sheet dialogs order themselves with local values inside the switcher's isolated flow wrapper (DEC-1); the page-level value that wrapper carries remains a `spec:AST-027` conformance gap. | DEC-1; `spec:AST-027` FR2, FR4, FR8 |
+| FR5 | `hasScrim=true` uses `showModal()`, a native backdrop, and native focus containment, and locks page scroll: the page does not scroll while a modal flow is presented, and pull-to-refresh and the page's bounce stop behind it, because a scroll lock under a modal holds the page (`spec:AST-094` FR19), as behind a UIKit modal presentation; the lock is held continuously across a handoff, never released and re-acquired between steps; it still holds beneath a `Dialog` opened from a step; it is released after the flow's exit (DEC-1). `hasScrim=false` uses `show()` without a backdrop or scroll lock and leaves the page interactive. In a non-modal flow the sheet dialogs order themselves with local values inside the switcher's isolated flow wrapper (DEC-1); the page-level value that wrapper carries remains a `spec:AST-027` conformance gap. | DEC-1; `spec:AST-027` FR2, FR4, FR8 |
| FR6 | The active child purpose governs implicit dismissal: `info` allows Escape, scrim click, and swipe; `form` allows Escape; `required` blocks all three and exposes `alertdialog`. | Current BottomSheet docs and switcher tests |
| FR7 | While visible, the switcher participates in `family:overlay-dismissal`; one Escape or platform close request reaches only the topmost present layer, and descendants receive a deeper logical layer scope. | `family:overlay-dismissal` and the audit regression test |
| FR8 | One backdrop paints the flow's scrim at a time: the retained sheet's dialog keeps it through a handoff and the entering dialog takes it over without a fade when the retained dialog closes. A stale gesture from an outgoing sheet cannot change the flow's scrim, and unmounting a retained sheet cannot keep its dialog open or the flow's modal layer held. | DEC-1; current tests |
@@ -259,7 +259,7 @@ Consumer migration instructions belong in consumer docs and release notes.
| Contract | Verification | Representative states | Mutation or failure expectation | Audit section |
| ---------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ----------------------------------------- |
| FR1–FR4, FR8 | `BottomSheetSwitcher.test.tsx` | closed, first open, handoffs in either completion order, rapid replacement, unmount, close; a handoff under reduced motion on a coarse and a fine pointer | A child becomes interactive at the wrong time, a retained child disappears early, a second backdrop paints, or a sheet dialog's lifecycle breaks. Under reduced motion, a handoff that moves a sheet, or on a coarse pointer does not cross-fade. | `audit:BottomSheetSwitcher/behavior` |
-| FR5–FR7, AR1–AR3 | `BottomSheetSwitcher.test.tsx`; `BottomSheetNativeModal.a11y.browser.spec.ts` | modal/non-modal, info/form/required, nested layer, IME, focus return, scroll lock in engines, non-modal local ordering | Modality, dismissal order, naming, inertness, scroll lock, or focus behavior diverges, or a sheet dialog takes a page-level z-index. | `audit:BottomSheetSwitcher/accessibility` |
+| FR5–FR7, AR1–AR3 | `BottomSheetSwitcher.test.tsx`; `BottomSheetNativeModal.a11y.browser.spec.ts`; `useScrollLock.test.ts` for the page's overscroll | modal/non-modal, info/form/required, nested layer, IME, focus return, scroll lock in engines, non-modal local ordering | Modality, dismissal order, naming, inertness, scroll lock, or focus behavior diverges, or a sheet dialog takes a page-level z-index. | `audit:BottomSheetSwitcher/accessibility` |
| FR9 | `BottomSheetSwitcher.test.tsx`; export checks | ref across open/handoff/close/unmount, callback-ref cleanup, `id` once, DOM/ARIA/data/events, class, style, xstyle | A supported input is dropped or duplicated, an object ref is null while a sheet is mounted or skips a cleanup, or an input overrides component-owned semantics accidentally. | `audit:BottomSheetSwitcher/api` |
| Theming anatomy | `scripts/check-knowledge.mjs`; theming tests | dialog, delegated sheet panel, scrim | A non-painting host gains a target, a delegated panel loses its owner, or the scrim gap disappears without review. | `audit:BottomSheetSwitcher/theming` |
| FR10 | `component:BottomSheet`'s stacking suite (the switcher-as-a-level cases); real-browser dim and recede sampling of a flow stacked over, and under, a standalone sheet | switcher base covered by a standalone sheet, switcher on top of a standalone sheet, a sheet in a step's content, flow closing over a lower sheet | Two backdrops paint, the covered level does not recede, a covered flow takes Escape, or focus lands on `body` when the flow closes over a sheet. | `audit:BottomSheetSwitcher/behavior` |
diff --git a/docs/specs/components/Drawer/Drawer.spec.md b/docs/specs/components/Drawer/Drawer.spec.md
index 7effabb54c2fb..ac12067eb8652 100644
--- a/docs/specs/components/Drawer/Drawer.spec.md
+++ b/docs/specs/components/Drawer/Drawer.spec.md
@@ -163,7 +163,7 @@ Consumer prop syntax and examples remain in `Drawer.doc.mjs`.
| FR2 | The logical edge selected for an open Drawer MUST remain latched through its exit even when caller state changes the live edge prop during close. | Current exit-side test and implementation |
| FR3 | The desktop inline-size budget MUST accept pixel numbers or CSS lengths and remain bounded by the viewport. At or below 640px, page-reveal mode MUST preserve 56px and use `width` as a cap; full-mobile mode MUST override `width` with `100dvw`. | Current docs, styles, and width tests |
| FR4 | Visibility through Drawer-owned behavior MUST follow `isOpen`, from the render after hydration on (FR17). Uncanceled Escape routed by the shared stack and uncanceled modal-backdrop activation request `false` when `purpose` allows (FR15), and a `DrawerHeader` close action requests `false` through the callback it is given; Drawer prevents its handled native `cancel` event from closing the dialog directly. During controlled exit, Drawer keeps its host and content region mounted while rendering the caller's current `children`. | Current prop contract and lifecycle tests |
-| FR5 | At open time, `hasScrim={true}` MUST select native `showModal()` with `aria-modal`, a focus trap, body scroll locking, and a visible backdrop; `hasScrim={false}` MUST select a manual Popover API host that leaves the page behind interactive. Changing `hasScrim` while open is not a supported native-mode transition. | Current source, docs, and initial-mode tests |
+| FR5 | At open time, `hasScrim={true}` MUST select native `showModal()` with `aria-modal`, a focus trap, page scroll locking, and a visible backdrop; under the lock, pull-to-refresh and the page's bounce stop, because a scroll lock under a modal holds the page (`spec:AST-094` FR19), as behind a UIKit modal presentation; `hasScrim={false}` MUST select a manual Popover API host that leaves the page behind interactive. Changing `hasScrim` while open is not a supported native-mode transition. | Current source, docs, and initial-mode tests |
| FR6 | An uncanceled press on the modal backdrop that starts and ends there (`target === currentTarget` for the pointer-down, the pointer-up, and the click, while live `hasScrim` is true; `family:overlay-dismissal` FR10) MUST request close when `purpose` is `info`, and MUST NOT for `form` or `required`; a press that crosses the drawer's edge either way MUST NOT request close. Descendant clicks and dialog-root clicks while live `hasScrim` is false MUST NOT request close, and non-modal presentation MUST NOT install an invisible outside-pointer dismissal plane. | Current click tests; `useBackdropPress.test.tsx` |
| FR7 | Drawer MUST NOT render a close control of its own. A visible close action is caller composition, normally `DrawerHeader` with `onOpenChange` (`component:DrawerHeader` FR3), which requests `false` through that callback without taking open-state ownership. | DEC-1; close-control suite |
| FR8 | Opening MUST present the native host, capture the element focused before opening, and attempt to focus the first rendered `[data-autofocus]` descendant, if any. After a completed controlled close releases the native host, Drawer MUST attempt to restore focus to the captured element. The non-modal host's synthetic `close` event MUST dispatch after that restore attempt — matching the task-queued native `dialog.close()` event — so a consumer close listener keeps the last word on final focus. | Current presence hook and focus tests |
@@ -336,7 +336,7 @@ block is optional during migration, so the map is deferred.
Read from source, with no focused test yet:
- FR1: not reserving layout space or reflowing the page.
-- FR5: body scroll locking for a modal drawer.
+- FR5: page scroll locking for a modal drawer.
- FR9: the computed-duration backstop.
- FR13: logical depth for descendant layers, and platform-close routing.
- FR14: the `dialog.show()` fallback below the Popover API support floor.
diff --git a/docs/specs/components/MobileNav/MobileNav.spec.md b/docs/specs/components/MobileNav/MobileNav.spec.md
index 3041eb1f187b6..5a1d45f9e9813 100644
--- a/docs/specs/components/MobileNav/MobileNav.spec.md
+++ b/docs/specs/components/MobileNav/MobileNav.spec.md
@@ -136,21 +136,21 @@ Consumer migration instructions belong in consumer docs and release notes.
## Behavioral and layout contract
-| ID | Invariant | Basis |
-| ---- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ---------------------------------------------------------------------------------------------- |
-| FR1 | MobileNav renders one native `