diff --git a/.claude/skills/page-layout/SKILL.md b/.claude/skills/page-layout/SKILL.md
new file mode 100644
index 0000000000..7d2cadccd2
--- /dev/null
+++ b/.claude/skills/page-layout/SKILL.md
@@ -0,0 +1,44 @@
+---
+name: page-layout
+description: Add or update app page headers and layout using PageHeader and PageLayout. Use when creating a new page, fixing inconsistent headers, or migrating Service Map / Kubernetes / dashboard pages to the shared layout.
+---
+
+# Page layout and headers
+
+Read **`agent_docs/page_layout.md`** before changing any page shell. It is the source of truth for API, examples, and migration steps.
+
+## Quick rules
+
+1. **Default for new pages**: `PageLayout` with `title`, optional `leading` / `actions`, and `content` — only when the sticky bar is **text-only** (no inputs in that bar).
+2. **Sticky bar contains inputs** (source pickers, SQL/search, sliders, time range, Run, etc.): **do not** use `PageHeader` / `PageLayout` **`title`**. The sticky row is for controls only. Put **where you are** in the **`breadcrumbs` prop** (renders **inside the sticky `PageHeader`**, above the toolbar) when the page lives under a hierarchy (e.g. `Dashboards` → `Kubernetes`); otherwise you may omit `breadcrumbs` and rely on `
`, the sidebar active item, and the empty state copy.
+3. **Never** duplicate location: do not set `title="Kubernetes Dashboard"` **and** breadcrumbs that repeat the same page name.
+4. **Never** use `` as the page title in the body.
+5. **Global controls** (time range, Run, Save, sampling) go in `actions`, right-aligned. **Context** pickers go in `leading` (no `title` when those slots are used for inputs). **Breadcrumbs** use the `breadcrumbs` prop so they stay in the sticky header, not in `content`.
+6. **Full-height tools** (maps, large charts): `fillViewport` on `PageLayout`.
+7. **Search / Chart Explorer**: keep bespoke toolbars unless the task is explicitly to redesign them.
+
+## Workflow
+
+1. Read `agent_docs/page_layout.md`.
+2. Open a similar page already on `PageHeader` / `PageLayout` (e.g. `AlertsPage.tsx`, `DBServiceMapPage.tsx`, `KubernetesDashboardPage.tsx`).
+3. Implement using `@/components/PageLayout` or `@/components/PageHeader`.
+4. Preserve or add `data-testid` on the page root for E2E tests.
+5. Run `yarn lint:fix` in the repo root when done.
+6. If the page has E2E coverage, run the relevant spec under `packages/app/tests/e2e/`.
+
+## Imports
+
+```tsx
+import { PageHeader } from '@/components/PageHeader';
+import { PageLayout } from '@/components/PageLayout';
+```
+
+## Reference implementations
+
+| Page | File | Pattern |
+|------|------|---------|
+| List page | `AlertsPage.tsx` | `PageHeader` + `title` + `Container` (no inputs in header) |
+| Tool page with inputs + hierarchy | `KubernetesDashboardPage.tsx`, `ClickhousePage.tsx` | `PageLayout` **without** `title`; **`breadcrumbs`** + `leading` + `actions` in one sticky header |
+| Tool page (top-level) | `DBServiceMapPage.tsx` | `PageLayout` **without** `title`; `leading` / `actions` only; no duplicate breadcrumb unless you add a real hierarchy |
+| Custom toolbar | `SessionsPage.tsx` | `PageLayout` + `header` = custom `PageHeader` `children` (single-row inputs, no `title`) |
+| Custom title | `TeamPage.tsx` | `PageHeader` with `children` only |
diff --git a/agent_docs/README.md b/agent_docs/README.md
index 957afbe811..67af911e0a 100644
--- a/agent_docs/README.md
+++ b/agent_docs/README.md
@@ -16,6 +16,7 @@ Instead of stuffing all instructions into `AGENTS.md` (which goes into every con
- **`tech_stack.md`** - Technology choices, UI component patterns, library usage
- **`development.md`** - Development workflows, testing strategy, common tasks, debugging
- **`code_style.md`** - Code patterns and best practices (read only when actively coding)
+- **`page_layout.md`** - PageHeader, PageLayout, and consistent page chrome (titles, actions, tool pages)
- **`data_viz_colors.md`** - Chart, heatmap, and semantic status colors. Read before adding or changing any color in a chart, sparkline, heatmap, legend, or status pill.
## Usage Pattern
diff --git a/agent_docs/page_layout.md b/agent_docs/page_layout.md
new file mode 100644
index 0000000000..18d7274bb2
--- /dev/null
+++ b/agent_docs/page_layout.md
@@ -0,0 +1,135 @@
+# Page layout and headers
+
+HyperDX app pages share a sticky top header bar and a scrollable content region below it. Use the shared layout primitives so titles, controls, and spacing stay consistent across Search, list pages, and tool pages (Service Map, Kubernetes, Chart Explorer).
+
+## Components
+
+| Component | Path | Use when |
+|-----------|------|----------|
+| `PageHeader` | `packages/app/src/components/PageHeader.tsx` | You only need the header bar (page already has its own content wrapper). |
+| `PageLayout` | `packages/app/src/components/PageLayout.tsx` | You want header + flex content column in one wrapper (recommended for new pages). |
+
+Both live next to `withAppNav` in `packages/app/src/layout.tsx`, which wraps pages with the left nav and scroll container.
+
+## PageHeader API
+
+```tsx
+
+
+{/* Sticky bar has inputs: no `title` — pass breadcrumbs into the header */}
+...}
+ leading={}
+ actions={}
+/>
+
+
+ {/* Custom title row, e.g. editable team name on Team settings */}
+
+```
+
+| Prop | Purpose |
+|------|---------|
+| `title` | Plain page title (`
`). **Use only when the sticky bar has no inputs** (list/settings pages). If the bar has pickers, search, sliders, or Run, omit `title` and use **`breadcrumbs`** (or rely on nav + document title only). |
+| `breadcrumbs` | Location trail **inside the sticky header**, rendered above the toolbar when `leading` / `actions` are set. Use Mantine `Breadcrumbs` (e.g. `Dashboards` → current page). Do not duplicate the same text as `title`. |
+| `leading` | Left cluster: source picker, badges, or other controls. When inputs are present, **do not** pair with `title` for the same page name. |
+| `actions` | Right-aligned cluster: time range, Run/Save, sampling, refresh. |
+| `children` | Full custom header when slots are not enough. Do not combine with `title` / `leading` / `actions` / `breadcrumbs` unless you use the breadcrumbs-only branch. |
+
+Header styling is defined in `PageHeader.module.scss`: sticky top, bottom border, horizontal padding `var(--mantine-spacing-sm)` (same as Search `px="sm"`). Single-row headers keep `min-height: 60px`; stacked header (breadcrumbs + toolbar) grows with content.
+
+## PageLayout API
+
+```tsx
+...}
+ leading={sourceSelect}
+ actions={headerActions}
+ fillViewport
+ content={}
+/>
+```
+
+| Prop | Purpose |
+|------|---------|
+| `title`, `leading`, `actions`, `breadcrumbs`, `children` | Forwarded to `PageHeader` (same rules as above). |
+| `header` | Replace the default `PageHeader` entirely. |
+| `content` | Main page body below the header (required). |
+| `fillViewport` | Set `height: 100vh` on the page root for full-height canvases (maps, charts). |
+| `contentClassName` | Extra class on the content region (e.g. padding). |
+
+## When to use which pattern
+
+### List / settings pages (Alerts, Dashboards, Saved Searches, Team)
+
+- Use `PageHeader` at the top of the page.
+- Put filters, tables, and tabs in a `Container` below the header (see `AlertsPage.tsx`, `DashboardsListPage.tsx`).
+- Simple title-only pages: `` — **only** when the header row has **no** inputs.
+
+### Tool / canvas pages (Service Map, Kubernetes, Clickhouse, Chart Explorer)
+
+- Prefer `PageLayout` with `fillViewport` when the main UI should fill remaining height.
+- Put **global** controls in `actions` (time picker, sampling, Run)—not inside the canvas.
+- Put **context** controls in `leading` (source picker, environment).
+- **If the sticky header row includes any inputs** (selectors, sliders, search, time range, Run): **omit `title`**. Express location with the **`breadcrumbs` prop** on `PageLayout` / `PageHeader` so the trail stays **inside the sticky header** above the toolbar (see `KubernetesDashboardPage.tsx`). Do **not** repeat the same label as both `title` and the last breadcrumb. Top-level tools (e.g. Service Map) may omit `breadcrumbs` if `` and the sidebar active state are enough.
+
+### Search and Chart Explorer (complex query chrome)
+
+These pages use bespoke multi-row toolbars instead of a single title row. That is intentional until those flows are redesigned. Do not force a `title` on them; if you add a header row, use `PageHeader` `children` or a dedicated toolbar component, not ad-hoc `Text size="xl"` in the body.
+
+### Client Sessions (query + list)
+
+Sessions keeps a **single-row** query toolbar (source, where filter, time range, Run). Put the full toolbar in a custom `PageHeader` via `PageLayout` `header` — do not split controls across `title` / `leading` / `actions` / `content`.
+
+## Do / don't
+
+```tsx
+// ❌ Ad-hoc title in page body
+
+ Service Map
+
+
+
+// ✅ Dashboard tool page: breadcrumbs in header, inputs in same sticky block (no `title`)
+...}
+ leading={}
+ actions={}
+ content={<>...>}
+/>
+
+// ❌ Title + breadcrumbs repeating the same page name
+… Kubernetes} />
+
+// ❌ Duplicate padding wrapper around the whole page including title
+
+ Alerts
+ ...
+
+
+// ✅ Header outside padded content
+
+...
+```
+
+## Migrating an existing page
+
+1. Replace `Text size="xl"` title + `Group justify="space-between"` with `PageLayout` or `PageHeader` slots.
+2. Move time picker and primary actions to `actions`.
+3. Move source pickers and badges to `leading`.
+4. If the sticky bar has inputs, **do not** set `title`; pass **`breadcrumbs`** on `PageLayout` when the route has a hierarchy (breadcrumbs render inside the sticky `PageHeader`, not in `content`).
+5. Keep `data-testid` on `PageLayout` / page root for E2E tests.
+6. Run affected Playwright tests (`packages/app/tests/e2e/`).
+
+## Pages using PageHeader / PageLayout today
+
+- Alerts, Dashboards, Saved Searches — `PageHeader` with `title`
+- Team — custom `PageHeader` `children` (editable name)
+- Service Map — `PageLayout` with `fillViewport`, `leading` / `actions` only (no `title`; top-level tool)
+- Kubernetes Dashboard, Clickhouse Dashboard — `PageLayout` with `breadcrumbs`, `leading`, `actions`, `padded` (no `title`)
+- Client Sessions — `PageLayout` with custom `PageHeader` containing the full single-row query toolbar
+
+## Knip
+
+`packages/app` Knip entry roots are `pages/`, `scripts/`, and e2e tests. After you add or move a `PageLayout` import, run `yarn knip` from the repo root (or `yarn knip` in `packages/app` if configured) so new consumers stay wired.
diff --git a/packages/app/src/SessionsPage.tsx b/packages/app/src/SessionsPage.tsx
index 25661456a6..70b4490fdc 100644
--- a/packages/app/src/SessionsPage.tsx
+++ b/packages/app/src/SessionsPage.tsx
@@ -16,7 +16,6 @@ import {
} from '@hyperdx/common-utils/dist/types';
import {
Anchor,
- Box,
Button,
Code,
Flex,
@@ -32,6 +31,8 @@ import {
import { useVirtualizer } from '@tanstack/react-virtual';
import EmptyState from '@/components/EmptyState';
+import { PageHeader } from '@/components/PageHeader';
+import { PageLayout } from '@/components/PageLayout';
import { SourceSelectControlled } from '@/components/SourceSelect';
import { TimePicker } from '@/components/TimePicker';
import { parseTimeQuery, useNewTimeQuery } from '@/timeQuery';
@@ -352,11 +353,7 @@ export default function SessionsPage() {
const targetSession = sessions.find(s => s.sessionId === selectedSession?.id);
return (
-