+ {/snippet}
+
+ undefined}
+ onDiscard={() => undefined}
+ />
+
+ {/snippet}
+
diff --git a/packages/ui/README.md b/packages/ui/README.md
index e6927585..554dca0a 100644
--- a/packages/ui/README.md
+++ b/packages/ui/README.md
@@ -122,6 +122,66 @@ Reusable Svelte hooks are located in the `src/hooks/` directory. These are Svelt
Utility functions and helpers are located in `src/tools/`. The main utility is the `cn` function for class name merging.
+### Page layout (`src/base/page/`)
+
+Composable page shell used across dashboard list and settings screens. Import as `import * as Page from '@cio/ui/base/page'`.
+
+| Export | Purpose |
+| ------------------------------ | ------------------------------------------------------------ |
+| `Page.Root` | Flex column wrapper with minimum viewport height |
+| `Page.Header` | Title row; pass `isSticky` to pin the header while scrolling |
+| `Page.HeaderContent` | Title + subtitle column |
+| `Page.Title` / `Page.Subtitle` | Page heading and description |
+| `Page.Action` | Right-aligned header actions |
+| `Page.Body` | Main content area (`child` snippet) |
+| `Page.BodyHeader` | Toolbar inside the body |
+| `Page.SettingsActions` | Save/discard bar for settings forms (floats, then docks) |
+
+**Settings pages:** Place `Page.SettingsActions` as the last child inside `Page.Root`, after `Page.Body`. The bar uses `position: sticky; bottom: 0` so it stays pinned to the viewport bottom while you scroll, then settles into normal flow at the end of the page. Do not put `overflow` on `Page.Root` that would break sticky positioning (horizontal overflow on `Page.Body` is fine). Disable Save and Discard via `hasChanges={false}` when the form is clean. Pass translated `statusLabel`, `discardLabel`, and `saveLabel` props from the dashboard. Use `disabled` to block Save only (Discard still follows `hasChanges`). Use `contentClass` for extra classes on the inner card when needed.
+
+**`Page.Root` can come from a layout.** "Last child of `Page.Root`" is a runtime relationship, not a same-file one. Where a `+layout.svelte` owns the `Page.Root` and renders pages into it via `{@render children?.()}`, the page file itself contains only `Page.Header`, `Page.Body`, and `Page.SettingsActions` — that is correct and already satisfies the rule. Do **not** add a second `Page.Root` in the page: nesting them stacks two `min-h-[calc(100vh-48px)]` containers, drops the layout's width constraints, and shortens the sticky bar's containing block so it stops floating. Pages that rely on a layout-owned `Page.Root`:
+
+- `routes/(app)/org/[slug]/settings/+page.svelte` and `settings/org/+page.svelte`, `settings/customize-lms/+page.svelte`, `settings/notifications/+page.svelte` → `routes/(app)/org/[slug]/settings/+layout.svelte`
+- `routes/(app)/lms/settings/+page.svelte`, `routes/(app)/lms/settings/notifications/+page.svelte` → `routes/(app)/lms/settings/+layout.svelte`
+
+**Never render `Page.SettingsActions` from inside `Page.Body`.** That includes rendering it from a feature component that a route passes into `Page.Body`'s `child` snippet. `Page.Body` sets `overflow-x-hidden`, and CSS computes `overflow-y` to `auto` whenever the other axis is not `visible` — so `Page.Body` is a scroll container. It is sized by its content and never scrolls internally, so a sticky bar inside it has no travel and renders flat at the end of the content. Keep the bar a sibling of `Page.Body` and lift the dirty/saving state up to the route with `bind:this` and `bind:hasUnsavedChanges`, the way `routes/(app)/courses/[id]/settings/+page.svelte` does.
+
+The bar also sits at `z-50`, matching the editor root so it paints above editor content (later in DOM wins the tie) while staying below body-portaled dialogs and sheets. Its shadow appears only while it is pinned away from its flow position, tracked by an `IntersectionObserver` on a 1px sentinel rendered as its next sibling.
+
+| Prop | Description |
+| -------------- | -------------------------------------------------------- |
+| `hasChanges` | Enables Save and Discard when the form is dirty |
+| `loading` | Shows loading state on Save |
+| `disabled` | Disables Save only (e.g. while not initialized) |
+| `contentClass` | Extra classes on the inner card (width, padding, etc.) |
+| `statusLabel` | Left-side status text (e.g. "Unsaved changes") |
+| `discardLabel` | Discard button label |
+| `saveLabel` | Save button label |
+| `onSave` | Save handler |
+| `onDiscard` | Discard handler |
+
+```svelte
+
+ ...
+
+ {#snippet child()}
+
+ {/snippet}
+
+
+
+```
+
+See `Molecules/Page` → **Settings Actions** in Storybook.
+
## Importing `cn`
The `cn` utility function is used throughout the codebase for merging Tailwind CSS classes. It combines `clsx` and `tailwind-merge` to handle class conflicts intelligently.
diff --git a/packages/ui/src/base/page/index.ts b/packages/ui/src/base/page/index.ts
index a97331c4..61d058bf 100644
--- a/packages/ui/src/base/page/index.ts
+++ b/packages/ui/src/base/page/index.ts
@@ -4,6 +4,7 @@ import BodyHeader from './page-body-header.svelte';
import Header from './page-header.svelte';
import HeaderContent from './page-header-content.svelte';
import Root from './page.svelte';
+import SettingsActions from './page-settings-actions.svelte';
import Subtitle from './page-subtitle.svelte';
import Title from './page-title.svelte';
@@ -16,6 +17,7 @@ export {
Subtitle,
Body,
BodyHeader,
+ SettingsActions,
//
Root as Page,
Header as PageHeader,
@@ -24,5 +26,6 @@ export {
Title as PageTitle,
Subtitle as PageSubtitle,
Body as PageBody,
- BodyHeader as PageBodyHeader
+ BodyHeader as PageBodyHeader,
+ SettingsActions as PageSettingsActions
};
diff --git a/packages/ui/src/base/page/page-body.svelte b/packages/ui/src/base/page/page-body.svelte
index dc29e860..d2d8f98a 100644
--- a/packages/ui/src/base/page/page-body.svelte
+++ b/packages/ui/src/base/page/page-body.svelte
@@ -16,7 +16,7 @@