# Elements: Layout, structure and containers

> Plainkit 0.28.0. Generated by scripts/build-skills.mjs from core/dist/elements/api.json. Do not edit: change the source and run the generator.

How to read these tables: see `elements-index.md`. Each element shows its first example; the others are in `references/full/examples-layout.md` (kept apart so the group files stay short).

## `pk-accordion`

**Accordion** (Containers). A stack of collapsible sections; exclusive mode keeps one open.

**Props**

| Attribute | Type | Default | Description |
|---|---|---|---|
| `exclusive` | boolean | `false` | Opening one item closes the others. |
| `flush` | boolean | `false` | No gaps: one shared frame. |

**Slots**

| Slot | Description |
|---|---|
| (default) | pk-accordion-item children. |

**Writes to host nodes**

| Target | Writes | Why |
|---|---|---|
| the accordion items | `open` | With exclusive, opening one item closes the others. The items raise pk-toggle for it. Not undone on disconnect. |

**Accessibility.** Items are native details and summary: Enter and Space toggle, find-in-page opens matches.

Example: Exclusive

```html
<pk-accordion exclusive>
  <pk-accordion-item heading="Shipping" open>Ships in 2 days.</pk-accordion-item>
  <pk-accordion-item heading="Returns">30 days.</pk-accordion-item>
</pk-accordion>
```

## `pk-accordion-item`

**Accordion item** (Containers). A collapsible section on a native details element.

**Props**

| Attribute | Type | Default | Description |
|---|---|---|---|
| `heading` | string | `""` | Header text. |
| `open` | boolean | `false` | Expanded. |

**Slots**

| Slot | Description |
|---|---|
| (default) | The panel. |
| `heading` | Rich heading. |
| `icon` | Icon before the heading. |
| `actions` | Header controls that do not toggle. |

**Events**

| Event | Detail | Description |
|---|---|---|
| `pk-toggle` | `{ open: bool }` | Opened or closed. |

**CSS parts**

| Part | Description |
|---|---|
| `details` | The details element. |
| `summary` | The header. |
| `heading` | The heading text. |
| `actions` | The actions. |
| `chevron` | The chevron. |
| `panel` | The panel wrapper. |

**CSS custom properties**

| Property | Default | Description |
|---|---|---|
| `--pk-accordion-duration` | `0.2s` | Open/close time. |

**Accessibility.** Native details and summary, 44px header; decorative chevron; no animation under reduced motion.

Example: Open by default

```html
<pk-accordion-item heading="Returns" open>30 days, unopened.</pk-accordion-item>
```

## `pk-app-frame`

**App frame** (Layout & structure). The app frame from data: header (brand, module picker, search, settings), side nav, breadcrumbs, page and footer. It routes nothing; the host navigates.

**Props**

| Attribute | Type | Default | Description |
|---|---|---|---|
| `brand` | string | `""` | The brand text in the header. |
| `brand-href` (property `brandHref`) | string | `""` | Where the brand links (same-site path or http(s)); empty is plain text. |
| `nav-label` (property `navLabel`) | string | `"Main"` | The side nav landmark name. |
| `nav` | json | `[]` | Side nav rows: { id, title, href?, icon?, badge?, children? }[]. Empty draws no nav. |
| `current-id` (property `currentId`) | string | `""` | Id of the current row. |
| `trail` | json | `[]` | Ids of the branches to open (ancestors of the current row). |
| `modules` | json | `[]` | The module picker: { id, title, href }[]; fewer than two draws none. On a phone it is the first row of the drawer. |
| `module-title` (property `moduleTitle`) | string | `""` | The active module, shown on the picker. |
| `crumbs` | json | `[]` | Breadcrumbs: { label, href? }[]; the last is the current page. Empty draws none. |
| `search` | boolean | `false` | Shows the header search. |
| `search-placeholder` (property `searchPlaceholder`) | string | `"Search"` | The search field placeholder and name. |
| `busy` | boolean | `false` | Blocks the page behind a loading overlay. |
| `busy-label` (property `busyLabel`) | string | `""` | Its label. |
| `search-items` (property `searchItems`) | json | `[]` | Search results, pk-app-bar-search items; the host sets them on pk-query. |
| `search-error` (property `searchError`) | string | `""` | A failed search: while set, the header search's results panel shows this message instead of "No results" (pk-app-bar-search error). Set it when the search behind pk-query throws; clear it with the next result. The panel then shows a Retry button: pressing it raises pk-query again with the text (the search's own pk-retry also bubbles out of the frame). |

**Slots**

| Slot | Description |
|---|---|
| (default) | The page, with the skip link target (main#pk-main). |
| `footer` | Footer content. |

**Events**

| Event | Detail | Description |
|---|---|---|
| `pk-query` | `{ query: string }` | The search text changed; set searchItems. |
| `pk-select` | `{ item: object }` | A search result was chosen. |
| `pk-theme-toggle` | `{ }` | Toggle theme was chosen. |

**CSS parts**

| Part | Description |
|---|---|
| `shell` | The pk-app-shell |
| `nav` | The side nav |
| `bar` | The header bar |
| `brand` | The brand link |
| `picker` | The module picker |
| `search` | The header search |
| `settings` | The settings menu |
| `crumbs` | The breadcrumb |
| `module` | The active module title |
| `theme` | The Toggle theme item |
| `footer` | The footer strip content |

**Accessibility.** Landmarks, keyboard and focus come from pk-app-shell, pk-navbar, pk-side-nav and pk-breadcrumb. The skip link and main landmark belong to the page slot (an id cannot cross the shadow boundary).

Example: App frame with a module picker

```html
<pk-app-frame brand="Acme" brand-href="#" module-title="Sales" current-id="open" nav='[{"id":"home","title":"Home","href":"#home","icon":"home"},{"id":"orders","title":"Orders","href":"#orders","badge":3,"children":[{"id":"open","title":"Open","href":"#open"},{"id":"shipped","title":"Shipped","href":"#shipped"}]},{"id":"reports","title":"Reports","href":"#reports"}]' trail='["orders","open"]' modules='[{"id":"sales","title":"Sales","href":"#sales"},{"id":"support","title":"Support","href":"#support"}]' crumbs='[{"label":"Sales","href":"#sales"},{"label":"Orders","href":"#orders"},{"label":"Open"}]' search search-placeholder="Search orders"><pk-page-header heading="Open orders"></pk-page-header><p>The page goes in the default slot.</p><span slot="footer">Acme 2026</span></pk-app-frame>
```

## `pk-app-shell`

**App shell** (Layout & structure). The page frame: nav column, header, one scrolling body and footer. Header and footer stay put; safe-area insets are added on notched phones; a slotted control marked data-nav-toggle opens the side nav drawer, or hides and shows the nav on a wide screen.

**Props**

| Attribute | Type | Default | Description |
|---|---|---|---|
| `nav-open` (property `navOpen`) | boolean | `false` | Drawer state, mirrors the side nav. |
| `nav-hidden` (property `navHidden`) | boolean | `false` | Above the drawer breakpoint (1024px) the side nav is hidden and the body takes the full width; the nav toggle flips it and the nav comes back in its previous expanded or icon-rail state. Remembered under the nav's persist key plus ":nav-hidden". On a phone or tablet the nav is the drawer (navOpen) and this has no effect. |
| `flush` | boolean | `false` | Removes the body padding so a workspace, table or map sits edge to edge (the safe-area insets on a notched phone stay). |
| `contain` | boolean | `false` | Makes the body a named inline-size container (name from --pk-app-shell-container, default page) so @container page (...) answers to the body width with no wrapper element, clips sideways overflow, and lets any single slotted child fill the height (flex 1, min-height 0) so a list page or a fixed region plus a scrolling one needs no :has() hack. Off by default; composes with flush. |
| `back-href` (property `backHref`) | string | `""` | Puts a back link (an arrow) before the title: a same-site path or an http(s) address, usually the parent list. Empty shows no link. Anything else is dropped with a warning. |
| `back-label` (property `backLabel`) | string | `"Back"` | The back link's accessible name (its aria-label), for example "Back to Invoices"; the arrow is the only visible content. |

**Slots**

| Slot | Description |
|---|---|
| `nav` | A pk-side-nav. |
| `header` | Top bar content after the title: page actions, an account menu. A direct child marked data-nav-toggle stays first. |
| (default) | The scrolling body. It is a flex column of definite height: ordinary content keeps its height and the body scrolls, while a page type that fills (sets --pk-fill-shrink: 1 and flex: 1 1 auto, min-height: 0, such as a list page with stickyHeader) takes the height left, so its header row and pager stay pinned and only its rows scroll. A host wrapper element between the body and the page needs the same two properties, or none (display: contents). |
| `footer` | Footer content. |

**Events**

| Event | Detail | Description |
|---|---|---|
| `pk-nav-toggle` | `{ open: boolean, hidden: boolean }` | A data-nav-toggle control was pressed (open is the drawer state, hidden the wide-screen state), or the saved nav visibility was restored. |

**CSS parts**

| Part | Description |
|---|---|
| `main` | Main column |
| `back` | The back link |
| `header` | Header row |
| `body` | Main content |
| `footer` | Footer row |

**CSS custom properties**

| Property | Default | Description |
|---|---|---|
| `--pk-app-shell-body-padding-block-start` | `page padding` | The padding above the first thing in the body (default: the page padding). Set it to 0px to sit a record page header flush under the top bar, or to a smaller step to tighten the band; the sticky page header offset follows. Inline and bottom padding are untouched; flush still removes all of it. |
| `--pk-app-shell-body-padding-block-end` | `page padding` | The padding under the last thing in the body (default: the page padding). Raise it to keep the last row and the pager clear of a floating launcher, for example a pk-tray launcher in the bottom corner: calc(var(--touch-target) + var(--space-6)). Inline and top padding are untouched; flush still removes all of it. |
| `--pk-app-shell-container` | `page` | The container name of the body when contain is set (an identifier, not a length); @container <name> (...) queries the body width. |
| `--pk-fill-shrink` |  | Set on a page type's host (1) so it may shrink to the height left in the body instead of keeping its own height; the default 0 keeps ordinary content at its height so the body scrolls. List pages and filling (fill) master-detail and workspace pages set it. |

**Writes to host nodes**

| Target | Writes | Why |
|---|---|---|
| the pk-side-nav in the shell | `open` | The nav-open state and the hamburger [data-nav-toggle] control the drawer state of the nav (and follow it). Not undone on disconnect. The wide-screen hide is done in the shell's own shadow tree (its nav slot), not on the nav. |

**Accessibility.** Landmarks: header, main content region and footer are exposed; only the body scrolls, so there is never a nested scrollbar; the skip link belongs first in the header slot. The back link is a real link (keyboard reachable, 44px target, focus ring) named by back-label; the arrow is aria-hidden. The page's h1 belongs in the body, usually as pk-page-header's heading, so there is exactly one page heading.

Example: App shell with a sticky footer

```html
<pk-app-shell><pk-side-nav slot="nav" label="Main"><a slot="brand" href="#">App</a><pk-nav-item href="#" current>Home</pk-nav-item><pk-nav-item href="#">Reports</pk-nav-item></pk-side-nav><div slot="header"><pk-button data-nav-toggle aria-label="Open the menu" variant="ghost">Menu</pk-button></div><pk-stack><h2>Home</h2><p>Content scrolls here; the header and footer stay put.</p></pk-stack><span slot="footer">Plainkit</span></pk-app-shell>
```

## `pk-canvas`

**Canvas** (Layout & structure). Also known as: canvas, workspace canvas, pan zoom, artboard. A pan and zoom ground for design tools: slotted content sits in a world you drag, pinch or Ctrl+wheel around, over an optional grid. pointToWorld and worldToPoint convert units, data-surface-selected nodes get a frame, and the page owns the selection.

**Props**

| Attribute | Type | Default | Values | Description |
|---|---|---|---|---|
| `zoom` | number | `1` |  | The scale: 1 is actual size. |
| `pan-x` (property `panX`) | number | `0` |  | Where the world origin sits, px from the left. |
| `pan-y` (property `panY`) | number | `0` |  | Where the world origin sits, px from the top. |
| `grid` | number | `0` |  | The grid spacing in world units (0 none); pointToWorld snaps to it. |
| `auto-grid` (property `autoGrid`) | boolean | `false` |  | An adaptive grid: lines at the 1, 2, 5, 10, 20, 50 ... step that stays 8 to 20 px apart at every zoom, with a stronger line every fifth step or more (colour: --pk-canvas-grid-major). Overrides grid; pointToWorld snaps to the fine step. |
| `unit` | string | `"none"` | `none` `px` `mm` `in` `pt` | The unit of the rulers and the auto grid. Only labels change: the stored world numbers never do. |
| `px-per-unit` (property `pxPerUnit`) | number | `0` |  | World units per display unit: 0 derives it from unit (mm 96/25.4, in 96, pt 96/72, otherwise 1). Set 1 when the world is already in the unit. |
| `rulers` | boolean | `false` |  | Rulers along the top and left with a pointer readout, in unit; hidden on a phone, thickness --pk-canvas-ruler-size (1.5rem). Loaded on first use. |
| `selectable` | boolean | `false` |  | Items that declare a world rect (data-id with data-x, data-y, data-w, data-h, or an SVG child's own x, y, width, height, or a geometry(el) callback property) can be selected: click, Shift or Ctrl click to toggle, a marquee drag on bare ground, Ctrl+A, Escape, and Tab focus. The layer loads on first use. The canvas never writes to the items. |
| `mode` | string | `"select"` | `select` `pan` | What a primary drag on bare ground does in a selectable canvas: select draws a marquee (touch, the middle button and the toolbar still pan), pan drags the view. |
| `selection` | json | `[]` |  | The ids of the selected items (an array, or a JSON attribute). The canvas sets it from a gesture and emits pk-canvas-select; set it to select from code. |
| `readonly` | boolean | `false` |  | View only: pan, zoom and selection still work, but the selection cannot be moved or resized (no pk-canvas-move or pk-canvas-resize, no handles). |
| `snap` | string | `""` |  | What a dragged selection snaps to, a space list: grid, guides, edges, centres (edges and centres of the other items and of the targets property, a JS-only array of extra { x, y, w, h } world rects). Empty: no snapping. The lines that hold it are drawn while dragging. |
| `snap-threshold` (property `snapThreshold`) | number | `6` |  | How near, in screen pixels, a snap line pulls. |
| `guides` | json | `[]` |  | Guide lines [{ axis: 'x' \| 'y', at }] in world units, drawn across a selectable surface and snapped to with snap="guides". |
| `min-zoom` (property `minZoom`) | number | `0.25` |  | The smallest scale. |
| `max-zoom` (property `maxZoom`) | number | `4` |  | The largest scale. |
| `wheel` | string | `"auto"` | `auto` `zoom` `pan` `page` | The plain wheel: auto pans only with focus (else the page scrolls), pan, zoom, or page (never taken). Ctrl or Cmd with the wheel, and pinch, always zoom. |
| `label` | string | `""` |  | The accessible name. Required. |

**Slots**

| Slot | Description |
|---|---|
| (default) | The content. Add data-surface-selected to a descendant to frame it. |

**Events**

| Event | Detail | Description |
|---|---|---|
| `pk-view-change` | `{ x: number, y: number, zoom: number }` | Panned or zoomed. |
| `pk-canvas-pick` | `{ target: Element \| null, additive: boolean }` | A press without movement: the deepest node under it, or null on bare ground; additive with Shift, Ctrl or Cmd. |
| `pk-canvas-select` | `{ ids: string[] }` | The selection changed by a gesture (click, marquee, Ctrl+A, Escape, Tab focus): the new ids. |
| `pk-canvas-key` | `{ key: string, shiftKey: boolean, altKey: boolean, ctrlKey: boolean }` | Delete, Escape, or Shift with an arrow (when there is no selection to move), on the focused surface, for the page to act on. |
| `pk-canvas-move` | `{ moves: { id: string, x: number, y: number }[], done: boolean }` | Cancelable: the selection is asked to move to these world positions. Arrow keys send done true; dragging an already selected item sends done false while it runs and done true at the end (Escape puts it back with done true). The page writes them back to the items' data-x and data-y (or an SVG item's x and y); the canvas never moves items itself. |
| `pk-canvas-resize` | `{ id: string, x: number, y: number, w: number, h: number, done: boolean }` | Cancelable: the one selected item is asked to take this world rect, from a drag of one of its eight handles (done false while it runs, done true at the end; Shift keeps the ratio, Alt the centre, Escape puts it back), or from Ctrl or Cmd with an arrow key (done true: one grid step, Shift ten, bottom-right corner). The page writes it back; the canvas never resizes items itself. |

**Methods**

| Method | Description |
|---|---|
| `pointToWorld(clientX, clientY)` | Returns { x, y } world units for a client point, grid-snapped |
| `worldToPoint(x, y)` | Returns { x, y } in client pixels for a world point |
| `zoomToFit(rect?)` | Fits a world rect { x, y, w, h }, or the slotted content, above the toolbar in the surface |
| `zoomTo(zoom, anchor?)` | Zooms around a surface pixel { x, y } (default the middle) |
| `centerOn(x, y)` | Centres a world point |
| `reset()` | Back to zoom 1 at the origin |

**CSS parts**

| Part | Description |
|---|---|
| `frame` | The focusable ground |
| `world` | The transformed content layer |
| `overlay` | The marks layer |
| `out` | The zoom out button |
| `in` | The zoom in button |
| `reset` | The reset view button |
| `mark` | The frame template |
| `toolbar` | The buttons |
| `level` | The zoom as a percentage |
| `status` | The live region that announces the zoom |
| `selection` | The layer of selection frames (selectable) |
| `marquee` | The rubber band while a drag selects |
| `handle` | A resize handle (eight on a single selected item, not readonly), data-handle nw, ne, se, sw, n, e, s or w |
| `guide` | A guide line, data-axis x (vertical) or y (horizontal) |
| `snap-line` | A line a dragged selection is snapped to, shown while dragging |
| `rulers` | The rulers layer (rulers on) |
| `ruler` | One ruler strip, data-axis x or y |
| `ruler-corner` | The corner with the unit |
| `ruler-readout` | The pointer position in the unit |

**CSS custom properties**

| Property | Default | Description |
|---|---|---|
| `--pk-canvas-bg` | `a page and text mix` | The ground behind the content |
| `--pk-canvas-grid` | `a faint text mix` | The grid line colour |
| `--pk-canvas-width` | `24rem` | The world width at zoom 1 |
| `--pk-canvas-height` | `24rem` | The height of the surface |
| `--pk-canvas-handle-size` | `var(--space-2)` | The drawn size of a resize handle; its hit area stays --touch-target |
| `--pk-canvas-guide` | `var(--color-accent)` | The colour of guides and snap lines |

**Accessibility.** A labelled group and one tab stop. Arrows pan (mirrored in right-to-left), + and - zoom, 0 resets, 1 or Home fits; with a selection (selectable, not readonly) the arrows move it instead: one grid step, Shift ten, Alt one screen pixel, announced as "Moved to x, y". With one item selected, Ctrl (Cmd on a Mac) with an arrow resizes it from its bottom-right corner: one grid step, Shift ten, never below one step, announced as "Resized to W by H". Delete, Escape and Shift+arrow reach the page. Zoom changes are announced; the plain wheel is the page's until focus.

Example: Selectable items on a canvas

```html
<pk-canvas label="Floor plan" selectable auto-grid selection='["table"]'><svg width="360" height="200"><rect data-id="table" x="40" y="40" width="120" height="80" tabindex="0" role="button" aria-label="Table" fill="currentColor" fill-opacity="0.15"></rect><rect data-id="door" x="220" y="60" width="80" height="60" tabindex="0" role="button" aria-label="Door" fill="currentColor" fill-opacity="0.15"></rect></svg></pk-canvas>
```

## `pk-card`

**Card** (Containers). A titled panel with optional media, header actions and footer. Vertical or horizontal (stacking by its own width), flush for tables, and optionally one link.

**Props**

| Attribute | Type | Default | Values | Description |
|---|---|---|---|---|
| `heading` | string | `""` |  | The card title. Without it (and without actions) the header is not shown. |
| `level` | number | `2` |  | The heading level the title exposes to assistive technology (aria-level). |
| `tone` | enum | `"default"` | `default` `error` | error draws the border in the error colour. |
| `orientation` | enum | `"vertical"` | `vertical` `horizontal` | Horizontal puts the media beside the content; it stacks when the card is narrower than 30rem (a container query, not the viewport). |
| `flush` | boolean | `false` |  | No padding around the body, for a table or an edge-to-edge body; the heading row keeps its padding. |
| `href` | string | `""` |  | Makes the whole card one link (a single tab stop named by the heading). |
| `fill` | boolean | `false` |  | Fills a flex parent (a workspace pane): the card takes the remaining height, has no padding, and its body is the scroll region. |

**Slots**

| Slot | Description |
|---|---|
| (default) | The card body. |
| `media` | An image, picture or video at the top (or the start when horizontal). |
| `actions` | Buttons or links at the right of the header. |
| `footer` | Content below the body; hidden when empty. |

**CSS parts**

| Part | Description |
|---|---|
| `card` | The panel. |
| `header` | The header row. |
| `title` | The title. |
| `body` | The body wrapper. |
| `footer` | The footer wrapper. |
| `media` | The media wrapper; hidden when empty. |
| `content` | The padded content column (header, body, footer). |
| `link` | The overlay link when href is set. |

**CSS custom properties**

| Property | Default | Description |
|---|---|---|
| `--pk-card-bg` | `var(--color-panel)` | Panel background. |
| `--pk-card-padding` | `var(--space-4) 1.25rem` | Panel padding. |
| `--pk-card-radius` | `var(--radius-md)` | Corner radius. |
| `--pk-card-media-width` | `40%` | Media width in the horizontal layout. |

**Accessibility.** The title is exposed as a heading whose level you choose with level, so the card fits the page outline wherever it sits. With href the card holds one link named by the heading, so there is a single tab stop and no nested interactive content. The card adds no live region of its own; a page that draws a loading or error state into the body supplies its own announcement.

Example: Titled card with an action

```html
<pk-card heading="Shipping">
  <pk-button slot="actions" size="mini" variant="ghost">Edit</pk-button>
  <p>Ships within two business days.</p>
</pk-card>
```

## `pk-cluster`

**Cluster** (Layout). A wrapping row (or column) of controls with a fixed gap: the layout primitive for button rows, chip lists and toolbars.

**Props**

| Attribute | Type | Default | Values | Description |
|---|---|---|---|---|
| `direction` | enum | `"horizontal"` | `horizontal` `vertical` | Row (wraps at the edge) or column. |
| `gap` | enum | `"sm"` | `none` `xs` `sm` `md` `lg` `xl` | Space between children, on the spacing scale (none 0, xs --space-1, sm --space-2, md --space-4, lg --space-6, xl --space-8). Default sm (the control gap). |
| `align` | enum | `"center"` | `start` `center` `end` `stretch` `baseline` | Cross-axis alignment of the items. |
| `justify` | enum | `"start"` | `start` `center` `end` `between` | Main-axis distribution of the items. |
| `nowrap` | boolean | `false` |  | Keep a horizontal cluster on one line instead of wrapping (wrapping is the default). |

**Slots**

| Slot | Description |
|---|---|
| (default) | The items to lay out. |

**CSS custom properties**

| Property | Default | Description |
|---|---|---|
| `--pk-cluster-gap` | `the gap token` | Overrides the gap prop with any length. |

**Accessibility.** A plain layout box with no role and no focus behaviour; keep the DOM order the reading order.

Example: Wrapping row

```html
<pk-cluster gap="sm"><pk-button>One</pk-button><pk-button variant="ghost">Two</pk-button><pk-badge>Three</pk-badge></pk-cluster>
```

## `pk-container`

**Container** (Containers). A max-width, padded content region, optionally scrolling on its own. Wraps a gallery stage, a code view or a doc-like page instead of a hand-styled div with max-width, padding and overflow.

**Props**

| Attribute | Type | Default | Values | Description |
|---|---|---|---|---|
| `size` | enum | `"md"` | `sm` `md` `lg` `full` | Max width, on the content scale (sm --content-sm, md --content-md, lg --content-lg). full removes the max width. |
| `padding` | enum | `"md"` | `none` `xs` `sm` `md` `lg` `xl` | Padding on the spacing scale (none 0, xs --space-1, sm --space-2, md --space-4, lg --space-6, xl --space-8). |
| `align` | enum | `"center"` | `center` `start` | center margins the region within its parent (the usual page layout); start pins it to the inline start instead. |
| `scroll` | enum | `"none"` | `none` `y` `both` | Makes the region scroll on its own instead of growing with its content: y for vertical overflow, both for vertical and horizontal. Needs a parent with a definite height (or fill) to have anything to scroll within. A scrolling region gets tabindex=0 and role=region, and needs label. |
| `label` | string | `""` |  | The accessible name for the scrolling region; required when scroll is not none (a warning is logged once when it is missing). Unused otherwise. |
| `fill` | boolean | `false` |  | Fills a flex or grid parent with a definite height (a workspace pane, an app shell body) instead of sizing to content, so scroll has a height to scroll within. |

**Slots**

| Slot | Description |
|---|---|
| (default) | The content. |

**CSS parts**

| Part | Description |
|---|---|
| `root` | The max-width, padded region; the scroll container when scroll is set. |

**CSS custom properties**

| Property | Default | Description |
|---|---|---|
| `--pk-container-width` | `the size token` | Overrides the size prop with any length (or none). |
| `--pk-container-pad` | `the padding token` | Overrides the padding prop with any length. |

**Accessibility.** A plain layout box with no role. With scroll set to y or both, the region becomes a keyboard-reachable landmark: tabindex=0, role=region and aria-label from label, so a keyboard or screen reader user can find and scroll it without a pointer.

Example: Doc-like page

```html
<pk-container size="md">
  <h2>Release notes</h2>
  <p>A centred column of prose, the width and padding both on the token scale.</p>
</pk-container>
```

## `pk-cta-band`

**CTA band** (Containers). A full-width call-to-action surface: your own heading, a line of text and one or two actions. Neutral or brand surface, start or centred, optionally edge to edge. Your markup is never changed; before it upgrades (or with JavaScript off) the heading, text and links show as plain blocks.

**Props**

| Attribute | Type | Default | Values | Description |
|---|---|---|---|---|
| `tone` | enum | `"neutral"` | `neutral` `brand` | The surface: neutral is the alternate surface colour, brand the accent fill with white text; primary buttons inside it turn light so they stand out. |
| `align` | enum | `"start"` | `start` `center` | Aligns the text and the actions. |
| `bleed` | boolean | `false` |  | Runs the band edge to edge of the viewport from inside a padded column. Off, it fills its column. |

**Slots**

| Slot | Description |
|---|---|
| `heading` | Your own heading (h2 or the level that fits the page). The element never sets a level or invents one; with none it logs one warning. |
| (default) | The text under the heading. |
| `actions` | A wrapper (a span) of one or two buttons or links. They sit side by side; give each button `block` and on a phone each is full width. |

**CSS custom properties**

| Property | Default | Description |
|---|---|---|
| `--pk-cta-band-bg` | `var(--color-surface-alt) (neutral) or var(--color-accent-fill) (brand)` | The surface colour. |

**Accessibility.** A plain block of your own heading, text and links: no live region and no role, so it is not announced as an alert. The heading level is yours, so the band fits the page outline. Text on the brand surface is white on the accent fill (4.5:1 or better), and its primary buttons take a light panel fill.

Example: Brand, centred

```html
<pk-cta-band tone="brand" align="center">
  <h2 slot="heading">Ready to start?</h2>
  <p>Free for 30 days. No card needed.</p>
  <span slot="actions"><pk-button block href="#signup">Start free</pk-button><pk-button block href="#demo">Book a demo</pk-button></span>
</pk-cta-band>
```

## `pk-dashboard-page`

**Dashboard page** (Page types). A dashboard of widgets, each a pk-card holding a pk-stat, a pk-chart or a pk-table, optionally grouped into tabs and headed sections, with an optional filter bar. Set the load(key) callback as a property (business logic, never config data): it is called with this bound to the page element, once per widget and independently, and resolves to the data merged onto that widget's element (a pk-stat's value/delta/tone/values, a pk-chart's data/caption, or for kind 'table' { columns: [{ key, label, align? }], rows: object[], caption? } drawn in a full-row pk-table captioned by caption or the widget label; no rows shows the card's empty state). A widget in a tab loads the first time its card is within about 200px of the viewport while that tab is shown (an above-the-fold widget at once, a lower one when it scrolls near; cards keep their reserved height, so a late start shifts nothing), never for a tab never opened, never again on a revisit, and a start still waiting when the user leaves the tab is dropped; without IntersectionObserver every widget of the shown tab starts at once. Each widget is its own async boundary through its card's state: loading, the content once it resolves, or an error with Retry on rejection - one slow or rejecting widget never blocks or breaks another. The current filter selections are on this.context (read them inside load); changing a filter reloads every widget that has already loaded.

**Props**

| Attribute | Type | Default | Description |
|---|---|---|---|
| `config` | json | `{}` | { tabs?, tab?, widgets, sections?, filters?, values?, empty? }. pk-page-header title bar keys: badges [{ label, variant? }], actionsOverflow, overflowLabel, homeHref, homeLabel, homeIcon. tab is the tab id to open first; values is { filterKey: value }, the selections the filter bar starts with; a widget may set size ('md' spans two columns and 'lg' the whole row once the section is 48rem wide, 'sm' takes one, and a phone-width section has one column); a load(key) that resolves null hides that widget's card. widgets is [{ key, tab?, label, kind: 'stat'\|'chart'\|'table', empty? }][] - key is passed to load(key), tab is the id of the tab it belongs to (the first tab when missing or unknown), label is the card heading and the pk-stat label or pk-chart caption, kind picks the composed element (default stat; a 'table' card spans the whole row), empty is { heading?, description? } shown in the card when there is no load callback. tabs is [{ id, label }][]; without tabs every widget renders in one ungrouped grid, in array order. sections is [{ heading?, tab?, widgets: string[] }][] naming the keys each groups, scoped to a tab (a tab with no section lists all its widgets). filters is [{ key, type: 'text'\|'email'\|'number'\|'date'\|'select', label, options? }][], the same shape as pk-list-page's, drawn as a bar above the tabs. empty is { heading?, description? } shown in place of everything when widgets is empty. Breaking change: this replaces the earlier config.tiles shape, with no alias. Optional title bar from the shared page shell: heading?: string, breadcrumb?: [{ label, href? }], actions?: [{ key, label, href?, variant?, hint? }] (a hint shows as a tooltip on the button) (an action without href fires pk-action). Nothing is drawn when none is set. |
| `context` | json | `{}` | The current filter selections as a plain object keyed by filter key ({} with nothing selected; an empty selection is dropped). Read it inside load(key); changing a filter reloads every widget that has already loaded. |

**Slots**

| Slot | Description |
|---|---|
| `badges` | Status chips beside the title. |

**Events**

| Event | Detail | Description |
|---|---|---|
| `pk-action` | `{ key }` | A title-bar action (config.actions entry without an href) was clicked. |

**CSS parts**

| Part | Description |
|---|---|
| `header` | The shared title bar: a pk-page-header with the breadcrumb and actions from config. Hidden when config sets none of heading, breadcrumb or actions. |
| `filters` | The filter bar: one pk-input or pk-select per config.filters entry. Hidden when there are no filters. |
| `body` | Holds the pk-tabs strip (a pk-tab and pk-tab-panel per tab) when config.tabs is set, or every section directly when there are no tabs. A section is an optional section-heading around a grid of widget pk-cards. |

**Accessibility.** Tab semantics (tablist, arrow keys, roving tabindex) come from pk-tabs. Each widget's loading and error states are announced by the pk-skeleton and pk-alert the page draws into that widget's pk-card (the card draws none itself), scoped to that widget alone; a resolved widget is a real pk-stat, pk-chart or pk-table with its own accessible name.

Example: Tabs, widgets and a filter (set config and load from script: an inline script cannot run under this site's CSP, so wire it the way pk-tool-page's run is wired)

```html
<pk-dashboard-page id="overview"></pk-dashboard-page>
```

## `pk-design-surface`

**Design surface** (Layout). A scrolling ground that frames a page being edited at full, tablet or phone width, draws selection, drop-target, hidden and empty marks around the page's nodes, and places an action chip beside a node.

**Props**

| Attribute | Type | Default | Values | Description |
|---|---|---|---|---|
| `width` | enum | `"full"` | `full` `tablet` `phone` | The page's width frame: full fills the surface, tablet and phone cap it (768px and 375px, from --pk-design-surface-tablet-w and --pk-design-surface-phone-w) and centre it. |
| `label` | string | `""` |  | The surface's accessible name (it is a focusable role=group); warns once when empty. |
| `selected` | json | `null` |  | Property only: the page node to frame as selected (a solid accent box). The host sets it; the surface never changes it. |
| `drop-target` (property `dropTarget`) | json | `null` |  | Property only: the container an external drag would drop into (a dashed accent box). |
| `chip-for` (property `chipFor`) | json | `null` |  | Property only: the node the action chip (slot chip) follows. null hides the chip. |
| `dragging` | boolean | `false` |  | An external drag is in progress: the chip is hidden. |
| `interactive` | boolean | `false` |  | Lets the page be used: links and controls inside it work. By default the page wrapper is inert (a click selects, it follows nothing). Not named inert because inert on the host would make the whole surface and its chip inert, and a boolean attribute defaults to off. |

**Slots**

| Slot | Description |
|---|---|
| (default) | The page being edited. Nodes at any depth may carry data-surface-hidden (drawn veiled with a dotted frame) or data-surface-empty (a dashed placeholder frame); the host owns those attributes. |
| `chip` | The action chip: real buttons the host owns (Edit, Delete). Placed at the top-right of the chipFor node, above it, inside its top edge when there is no room above, or below it when inside would cover its top-left; clamped to the surface. |
| `empty` | Shown while the default slot is empty (for example the text of a drag-an-element-here hint). |

**Events**

| Event | Detail | Description |
|---|---|---|
| `pk-surface-pick` | `{ target, additive }` | A click or tap on the page, hit-tested to the deepest page node under it (null for bare ground); additive is Shift, Ctrl or Cmd. Selecting is the host's: the surface never changes selected. |
| `pk-surface-hover` | `{ target }` | The pointer moved to another page node (null when it left). At most once per animation frame. |
| `pk-surface-key` | `{ key, shiftKey, altKey, ctrlKey }` | An arrow key, Delete or Escape on the focused surface. Cancelable: a host that handles the key calls preventDefault, which also stops the native scroll. |

**Methods**

| Method | Description |
|---|---|
| `reveal(node)` | Scrolls the ground so a page node shows, with a margin. |
| `nodeAt(x, y)` | The deepest page node under a viewport point, or null. |
| `rectOf(node)` | A node's { left, top, width, height } relative to the page box, scroll included. |
| `relRect(rect, origin)` | A rectangle relative to an origin rectangle; pure. |
| `deepestAt(items, x, y)` | The index of the deepest { depth, rect } containing a point, or -1; pure. |
| `placeChip(node, chip, bounds, gap)` | Where the chip goes, clamped into the bounds; null when the node is outside them; pure. |
| `revealDelta(node, view, pad)` | How far to scroll so a node shows; pure. |

**CSS parts**

| Part | Description |
|---|---|
| `frame` | The scrolling ground (the focus stop). |
| `page` | The framed width box around the default slot. |
| `empty` | Wraps the empty slot. |
| `overlay` | The marks layer (pointer-events none, aria-hidden). |
| `mark-selected` | The selection box. |
| `mark-drop` | The drop-target box. |
| `mark-hidden` | The veil over a node flagged data-surface-hidden. |
| `mark-empty` | The dashed frame on a node flagged data-surface-empty. |
| `chip` | Wraps the chip slot. |

**CSS custom properties**

| Property | Default | Description |
|---|---|---|
| `--pk-design-surface-bg` | `color-mix(in srgb, var(--color-bg) 70%, var(--color-text) 30%)` | The ground behind the page. |
| `--pk-design-surface-pad` | `var(--space-4)` | Padding between the ground's edge and the page. |
| `--pk-design-surface-phone-w` | `var(--surface-phone-w)` | Width of the phone frame. |
| `--pk-design-surface-tablet-w` | `var(--surface-tablet-w)` | Width of the tablet frame. |
| `--pk-design-surface-min-h` | `16rem` | Minimum height of the element. |
| `--pk-design-surface-chip-z` | `2` | Stacking order of the chip. |

**Accessibility.** The surface is a role=group with the label as its name and one tab stop (tabindex 0); arrow keys, Delete and Escape on it are reported as pk-surface-key so a host can give an accessible way to select. The marks are decoration (aria-hidden); the host announces the selection itself. The inert page is not a keyboard trap: Tab leaves the surface, and the chip's buttons are real, focusable buttons the host owns. The focus ring is the shared ring, drawn inside the ground so it is never clipped.

Example: Tablet frame (selected and chipFor are set from script)

```html
<pk-design-surface label="Page canvas" width="tablet">
  <pk-stack gap="md">
    <pk-heading level="3">Order summary</pk-heading>
    <pk-text>Two items, free shipping.</pk-text>
    <pk-button>Place order</pk-button>
  </pk-stack>
  <pk-button slot="chip" size="mini" collapse="phone" icon-name="edit">Edit</pk-button>
  <pk-button slot="chip" size="mini" collapse="phone" icon-name="trash">Delete</pk-button>
</pk-design-surface>
```

## `pk-detail-layout`

**Detail layout** (Layout & structure). A record-detail page's body: main content and a sticky sidebar of summary cards, Below a container width (not the viewport) it collapses to one column; sidebarFirst puts the sidebar above the main content once collapsed, and sidebarTwoUp lays its own cards two-up instead of stacking one-per-row. Elements carrying data-pk-section are grouped into phone tabs once collapsed. Before it upgrades (or with JavaScript off) its slotted content shows as plain markup, not hidden.

**Props**

| Attribute | Type | Default | Values | Description |
|---|---|---|---|---|
| `sidebar-first` (property `sidebarFirst`) | boolean | `false` |  | Once collapsed to one column, the sidebar comes before the main content instead of after. |
| `sidebar-two-up` (property `sidebarTwoUp`) | boolean | `false` |  | Once collapsed to one column, the sidebar's own cards lay out two per row instead of stacking one-per-row. |
| `section` | string | `""` |  | The selected section once the layout has collapsed to one column and its cards name two or more data-pk-section values. Empty selects the first section. |
| `tabs-from` (property `tabsFrom`) | enum | `"collapse"` | `collapse` `phone` `tablet` `always` | Where the data-pk-section tabs apply. collapse: once the layout is one column (container query). phone or tablet: at or below that viewport width. always: in two columns too, sidebar cards of the chosen section beside it. Elsewhere every section shows, with no strip. |
| `hide-tabs` (property `hideTabs`) | boolean | `false` |  | The strip is the host's: the layout draws none and, where tabs-from says the strip applies (below the collapse width by default), shows one data-pk-section at a time from section alone, and still draws Next. Elsewhere every section shows. Supply the strip yourself, for example the docked tabs of a pk-page-header, and set section from its pk-tab-change. |
| `sections-above` (property `sectionsAbove`) | boolean | `false` |  | Also manage data-pk-section elements that are siblings before the layout (for example a full-width summary strip above it), so they show and hide with the chosen section. |
| `tab-param` (property `tabParam`) | string | `""` |  | A query parameter name ("tab") that keeps the chosen section in the address bar, so a reload or a shared link opens on it. (replaceState, no history entry). |
| `hide-next` (property `hideNext`) | boolean | `false` |  | Draws no Next button after the main column. The sections still change from the tab strip (or from section); use it when the strip, on a phone too, is enough. |
| `next-label` (property `nextLabel`) | string | `"Next"` |  | The text before the next section's name on the button after the main column ("Next: Pricing"). |

**Slots**

| Slot | Description |
|---|---|
| (default) | The main content. |
| `sidebar` | Summary/status cards; pk-card fits. Sticky in the two-column layout: it sticks by its top edge, or by its bottom edge when it is taller than the viewport, so every card can be reached. |

**Events**

| Event | Detail | Description |
|---|---|---|
| `pk-section-change` | `{ section, previous }` | The user chose a section, with a tab, the Next button, or by submitting a form whose invalid control was in a hidden section. Cancelable: preventDefault keeps the previous section. |

**CSS parts**

| Part | Description |
|---|---|
| `grid` | The two-column grid |
| `main` | The main content wrapper |
| `sidebar` | The sidebar wrapper |
| `tabs` | The pk-tabs strip of sections (collapsed layout with two or more sections only) |
| `next` | The button to the next section |

**CSS custom properties**

| Property | Default | Description |
|---|---|---|
| `--pk-detail-layout-sidebar` | `20rem` | Sidebar column width in the two-column layout. |
| `--pk-detail-layout-gap` | `var(--space-6)` | Gap between the main column and the sidebar in the two-column layout. |
| `--pk-detail-layout-main-max` | `none` | Maximum width of the main column. The sidebar stays flush right in the content area, so a capped main column does not pull it inward. |
| `--pk-detail-layout-height` | `0px` | Set by the element on the sidebar part: its measured height, used to dock a sidebar taller than the viewport by its bottom edge. Not for authors to set. |

**Writes to host nodes**

| Target | Writes | Why |
|---|---|---|
| elements inside the layout (and, with sectionsAbove, its preceding siblings) that carry data-pk-section | `data-pk-section-hidden` | While tabbed, every element outside the selected section gets data-pk-section-hidden and display: none (CSSOM, no attribute); both are removed when the layout widens, loses its sections or the section is selected. Kept when the element disconnects. |

**Accessibility.** A layout primitive only: no landmark or role of its own. Give the main content and the sidebar their own headings so assistive technology can navigate between them.

Example: Order detail

```html
<pk-detail-layout>
  <pk-page-header heading="Order 1042"></pk-page-header>
  <p>Line items go here.</p>
  <pk-card slot="sidebar"><h3>Status</h3><p>Shipped</p></pk-card>
  <pk-card slot="sidebar"><h3>Customer</h3><p>Ada Lovelace</p></pk-card>
</pk-detail-layout>
```

## `pk-divider`

**Divider** (Layout). A rule between content, horizontal or vertical, with an optional label.

**Props**

| Attribute | Type | Default | Values | Description |
|---|---|---|---|---|
| `vertical` | boolean | `false` |  | A vertical rule; the parent must be a flex row. |
| `label-position` (property `labelPosition`) | enum | `"center"` | `center` `start` | Where the label sits. |
| `decorative` | boolean | `false` |  | Remove the separator role. |

**Slots**

| Slot | Description |
|---|---|
| (default) | The label. |

**CSS parts**

| Part | Description |
|---|---|
| `line` | A rule. |
| `label` | The label. |

**CSS custom properties**

| Property | Default | Description |
|---|---|---|
| `--pk-divider-color` | `var(--color-border)` | Line colour. |
| `--pk-divider-gap` | `var(--space-3)` | Space around the label. |

**Accessibility.** role=separator (with aria-orientation) unless decorative is set.

Example: Labelled

```html
<pk-divider>or</pk-divider>
```

## `pk-doc-page`

**Doc page** (Page types). A page of long-form documents: a filterable side nav of items, an article with a title, summary and body, a table of contents and a previous/next pager. Mounted by the app framework as the `doc` page type (route param `id`, query `anchor`; pk-navigate goes through the router). Set loadItem(id), href(id, anchor?) and, for a search of your own, searchItems(query) as properties (business logic, never config data); a same-page link inside the article scrolls it and fires pk-navigate instead of touching history itself. Static slotted mode (for server-rendered, JavaScript-optional sites): write the page as plain child markup with no config items, a pk-side-nav in slot="nav", a pk-toc in slot="aside" and the article and pk-pager in the default slot, and the page lays them out as written, takes the current item from your aria-current marking and never creates, moves or changes your elements; on a phone it draws its own Menu button (in its shadow tree) that opens your nav as a drawer.

**Props**

| Attribute | Type | Default | Description |
|---|---|---|---|
| `config` | json | `{}` | { items: [{ id, title, summary }], id: currentId\|null, anchor: headingId\|null, search: true, home?: { title, summary, cards?: true }, level?: 1\|2\|3, navLabel?, pagerLabel?, searchLabel?, breadcrumb?: true, embedded?: true, scroller?: selector }. embedded: true is for a host that draws the item nav and the breadcrumb itself (the app shell): neither is drawn here and the search box sits in the article header, under the title. scroller names the host's own scrolling box (a selector) that the table of contents follows instead of the whole page. home.cards shows the home list as a grid of pk-cards (title, summary, link) instead of a plain list; breadcrumb: true draws a pk-breadcrumb above the title (the home title as a link, then the current page); navLabel (default Contents), pagerLabel (default Page navigation) and searchLabel (default Search) name the nav, the pager and the host search box. level is the article title's heading level (default 1 for a page of its own; use 2 when the doc page sits inside a page that already has an h1), read once when the element is built. id null shows the home list (every item, linked); id set loads and shows that item through loadItem(id). anchor scrolls the article to that heading once the item is shown, retried once on the very first load (the elements around the article are still upgrading). |

**Slots**

| Slot | Description |
|---|---|
| `nav` | Static slotted mode: your own pk-side-nav, shown as written (the current item is the one you mark current / aria-current=page); on a phone the element opens it as a drawer from its own Menu button. Dynamic mode: the item nav (a real pk-side-nav the element builds and owns itself, with a pk-nav-item per config.items entry). With searchItems set it also holds a search box (pk-input) and a polite status line (pk-text) above the items; the host's search names which items stay (an item whose id is not in ids gets hidden, shown again when the box is empty). |
| `bar` | The under-1024px bar: the drawer toggle button the element builds itself (dynamic mode; static slotted mode draws its Menu button inside its own shadow tree instead). |
| `aside` | The table of contents: dynamic mode builds a real pk-toc bound to the article body; static slotted mode shows your own pk-toc as written. |
| (default) | The article's own content. Dynamic mode: the title, summary, body and pager the element builds and owns itself. Static slotted mode: your own heading, article body and pk-pager, as written. |

**Events**

| Event | Detail | Description |
|---|---|---|
| `pk-navigate` | `{ id, anchor, replace: false }` | A same-page link inside the article body (the table of contents, a heading's own permalink) was followed. The host rewrites the address (history.pushState) and keeps anchor in the config it sets next; the element never touches history itself. |

**CSS parts**

| Part | Description |
|---|---|
| `shell` | The whole layout: the nav slot beside the main column. |
| `main` | The column beside the nav: the bar (under 1024px), then the page. |
| `page` | The table of contents and the article, laid out side by side above 1280px and stacked at or below it. |
| `bar` | Wraps the bar slot and the Menu button; shown only at or below 1024px. |
| `menu` | Static slotted mode only: the Menu button (a pk-button the element draws in its own shadow tree, shown at or below 1024px) that opens and closes your nav as a drawer. It carries aria-expanded; Escape, a tap outside the drawer or a tap on a nav item closes it and focus returns to the button. |
| `backdrop` | Static slotted mode only: the scrim behind the open drawer; a tap on it closes the drawer. |
| `aside` | Wraps the table of contents slot. |
| `article` | Wraps the default slot: the title, summary, body and pager the element builds. |

**Writes to host nodes**

| Target | Writes | Why |
|---|---|---|
| the nav, toggle button and table of contents (pk-side-nav, pk-button, pk-toc) | `open`, `pressed`, `filterable`, `hidden`, `label`, `placeholder`, `aria-label`, `scroller` | Light-DOM elements this element creates and owns itself (never host-given nodes): its own drawer toggle opens/closes the nav (open, pressed), its own config.search turns the nav's built-in filter on (filterable), and the table of contents is hidden while the shown page has no headings to list (hidden). |

**Accessibility.** Static slotted mode: the Menu button is a real button named Menu with aria-expanded and aria-controls pointing at your nav; opening the drawer moves focus to the first nav item and closing it (Escape, the scrim, a nav item) returns focus to the button. The host search box is named by searchLabel and its status line is aria-live=polite, so a screen reader hears how many items matched and where. The breadcrumb is a real pk-breadcrumb (a labelled navigation landmark) whose last crumb is aria-current=page. The nav is a real pk-side-nav (its own filter box, arrow-key tree navigation, phone drawer); the article's h1 gets focus (tabindex="-1", preventScroll) after a navigation so keyboard and screen reader users start there; the table of contents is a real pk-toc bound to the article body by id.

Example: A small set of documents (set loadItem/href from script: an inline script cannot run under this site's CSP)

```html
<pk-doc-page id="docs-example" config='{"level":2}'></pk-doc-page>
```

## `pk-dock`

**Dock** (Layout). A workspace of panels laid out as a tree of resizable splits and tab groups. Drag a separator or use its arrow keys to resize, choose a tab to switch a group, drag a panel's header or tab onto another group (its center to add as a tab, an edge to split and dock there) or use a group's Move button to do the same without a mouse, Close/reopen a panel from the toolbar's Panels menu, or use a single-panel header's chevron to collapse it to just its title. A collapsed group at a screen edge folds to a narrow rail button instead; activating it opens the panel as a flyout over the content, closing on Escape, an outside click or a blur. The flyout opens with an Expand button ahead of the panel's own content: activating it restores the panel to a normal docked header (a separate action from opening or closing the flyout, and the only way back from a collapsed rail). A toolbar-start slot lets a host add its own menus (File/Edit/View-style) alongside it - the dock draws none of that content itself. A panel can also float: taken out of its group from its own Panel menu's Float item (or a pointer drag once it already floats), it draws as its own window-like overlay inside the dock's own bounds (never over the page), moved and resized by dragging its header/corner grip or, once its frame has focus, by arrow keys (Alt+Arrow resizes, Shift steps bigger); its own Panel menu offers docking it back into the tree at any group and zone instead of the usual Move items. Focusing or grabbing a floater brings it to the front. The layout can be saved and restored across reloads through persistKey. The arrangement is one JSON layout value. Free-running by default (a change applies, then fires pk-layout-change); a host that wants every change to round-trip first sets confirmLayout(arg) as a callback property (business logic, not config data) - arg is { layout: json, reason }, and the settled result (a layout JSON string, or null/undefined to keep the previous one) is what gets applied and reported.

**Props**

| Attribute | Type | Default | Description |
|---|---|---|---|
| `layout` | json | `null` | The arrangement: { version, seq, root, collapsed } where root is a split { id, type: 'split', orientation, size, min, max, a, b } or a tab group { id, type: 'tabs', active, panels }, and collapsed is the list of open panel ids folded to their header. Leave it unset for the default arrangement derived from each panel's data-group. The element validates what it is given (panels that are not declared are dropped, panels the layout lacks are added, bad numbers are repaired). While the user resizes, chooses a tab or collapses a panel the element owns it; after pk-layout-change the host does. Property or a JSON attribute. |
| `label` | string | `""` | Accessible name of a tab group (aria-label), for example the workspace's name. A single-panel group is named by its heading. |
| `resize-label` (property `resizeLabel`) | string | `"Resize panels"` | Accessible name of every separator between two panels (aria-label). |
| `fill` | boolean | `false` | Fill the height the parent gives it (a parent with a definite height, or a flex or grid parent) instead of --pk-dock-height. |
| `persist-key` (property `persistKey`) | string | `""` | Save and restore the layout across reloads, through the SDK's store (js/store.js), under the key pk-dock:<persistKey>. Leave unset to disable persistence. On connect, a saved layout is used as the initial arrangement when the layout prop is not set (stored data wins over the default arrangement); every later pk-layout-change (resize, activate, panels) is saved, but a layout the host sets is not. Give each pk-dock on a page its own value; changing it starts a fresh, separately-saved layout. |

**Slots**

| Slot | Description |
|---|---|
| `<panel id>` | One panel: any element with slot="<id>" (a lowercase id, letters, digits, - and _, up to 40 characters). data-heading is its title (the id when missing) and data-group (left, center, right, bottom) places it in the default arrangement. Optional attributes (read, never written): data-closable="false" removes Close, data-collapsible="false" the collapse toggle, data-open="false" starts it closed, data-float starts it floating. The first data-group="center" panel is the permanent centre (never closed or floated; the phone starts on it). Panels are slotted, never moved, so form and scroll state survive. |
| `empty` | Shown when there is no panel. |
| `toolbar-start` | The start of the toolbar (part toolbar), before the dock's own Panels menu: a place for a host app's own top-level menus (File, Edit, View...). The dock draws none of this; it only gives it a stable spot next to its own controls. The toolbar shows whenever this slot holds something or a panel is closed, and collapses away when both are empty. |

**Events**

| Event | Detail | Description |
|---|---|---|
| `pk-layout-change` | `{ layout: object, reason: string }` | The arrangement changed because of the user or because panels were added or removed: reason is resize (a drag or a splitter's arrow keys moved a separator), activate (a tab was chosen), move (a panel was moved to another group from its Move menu or a pointer drag), collapse (a single-panel header's chevron toggled its body), floater (a floating panel was dragged or its grip resized by pointer), float (a panel was detached to float, from its Panel menu's Float item), dockfloat (a floating panel was docked back into the tree, from its own Panel menu), raise (a floating panel came to the front, from a pointer grab or focus landing anywhere inside it) or panels (the declared panels changed and the layout was repaired). A floater's own frame also has arrow-key move and resize (Alt+Arrow, Shift for a bigger step; reason resize or move, like the splitter's own keys), and a live region (part status) announces every one of these except raise. layout is the new layout JSON. Not raised for a layout the host set, nor for the first render. Not cancelable. |

**Methods**

| Method | Description |
|---|---|
| `resetLayout()` | Back to the default arrangement; fires pk-layout-change (reason reset). |
| `getLayout()` | The drawn layout as a JSON string (the same shape as the layout property), or null before the first draw. |
| `setLayout(json)` | Applies a layout (JSON string or object), validated like the layout property and drawn without a pk-layout-change; returns the repairs made, [] when it was clean. |
| `openPanel(id)` | Reopens a closed panel, announces it and focuses its tab; nothing when it is open. |
| `closePanel(id)` | Closes a panel (it stays a host child, so its state survives; the Panels menu offers it again), announces it and moves focus to the Panels button. Refused for the centre panel and data-closable="false". |
| `floatPanel(id, rect?)` | Floats a docked panel as its Float menu item does (reason float), at rect { x, y, w, h } in dock pixels (kept inside the dock) or a cascade position. Refused for the centre panel and when floating is off. |
| `dockPanel(id, { target, zone })` | Moves a panel as the Move menu does (reason move): zone center (the default) adds it as a tab of group target, left, right, top or bottom docks it beside that group. target is a group id of the layout (getLayout()). |
| `collapsePanel(id)` | Collapses a panel that is alone in its group, as its header toggle does (reason collapse; at a screen edge it becomes a rail button). Nothing for a tab group or data-collapsible="false". |
| `raisePanel(id)` | Brings the floating panel to the front (reason raise); nothing for a docked panel. |

**CSS parts**

| Part | Description |
|---|---|
| `toolbar` | The bar above the tree: slot toolbar-start (a host app's own menus) then the dock's own Panels menu. Collapses away while both are empty. |
| `root` | The frame that holds the rendered tree. |
| `group` | One tab group or single panel, with its header. A floating panel (round 2 item 5 of #432) is also drawn with this part: an absolutely-positioned, window-like overlay inside the dock's own bounds (selectable as pk-dock::part(group)[data-floater]), its own header doubling as the drag-to-move handle and a .floater-resize grip in its corner. The floater frame itself is also focusable (tabIndex 0, aria-roledescription "floating panel"): arrow keys move it, Alt+Arrow resizes, Shift steps bigger, and focusing anything inside it raises it to the front. |
| `header` | The title bar of a single-panel group. |
| `collapse-toggle` | The pk-button (a disclosure button; its own chevron shows the direction) in a single-panel header that collapses or expands its body. Style the box through its --pk-button-* hooks; the inner control is not reachable from outside the dock. Reused (same part and class) for the flyout's own Expand button, which restores the panel to a normal docked header - a different action from the rail button that opened the flyout, which only opens or closes it. |
| `title` | The panel's heading text inside collapse-toggle. |
| `body` | The scrolling area around a panel's content. |
| `rail-button` | The pk-button (a disclosure button, label written vertically) a collapsed edge group folds to instead of a header; opens the panel as a flyout. |
| `flyout` | The floating layer a rail button opens its panel into, positioned next to the button. Its Expand button (part collapse-toggle, issue #636) comes first, ahead of the panel's own content. |
| `empty` | The empty state. |
| `status` | The visually hidden live region that announces a move, a close/reopen, or a floating panel being floated, docked back in, moved or resized. |

**CSS custom properties**

| Property | Default | Description |
|---|---|---|
| `--pk-dock-height` | `28rem` | Height when fill is not set. |
| `--pk-dock-header-height` | `var(--space-8)` | Minimum height of a single-panel header. |
| `--pk-dock-pad` | `var(--space-3)` | Padding around a panel's content. |
| `--pk-dock-rail-width` | `2.75rem` | Width of a collapsed edge group's rail button. |
| `--pk-dock-flyout-width` | `18rem` | Width of the flyout a rail button opens. |

**Accessibility.** Each separator is a pk-splitter: role=separator with aria-valuenow, aria-valuemin, aria-valuemax and its own arrow keys (Left and Right side by side, mirrored in a right-to-left page; Up and Down stacked; Home and End), so every resize has a keyboard path. A group of several panels is a pk-tabs tablist (roving tabindex, arrow keys); a single panel is a labelled section named by its heading. Hidden tab panels are out of the tab order and the accessibility tree. When there is more than one group, its header (or its tab strip, in the trailing slot) carries a Move button listing every other group: Add as tab in it, or Dock left/right/above/below it; choosing one moves the active panel with dockPanel/moveTab and moves focus to it, and the visually hidden status part announces the result. A pointer can instead drag a header or tab onto another group's center (add as tab) or an edge (split and dock there), shown with an accent drop-zone highlight while dragging (the same dockPanel/moveTab operations; the Move menu needs no mouse). A single-panel header's collapse-toggle is a pk-button (Tab to reach it, Enter or Space to activate, no extra key handling needed) whose control carries aria-expanded; a tab group has no collapse. A collapsed group at a screen edge is instead a rail button whose control carries aria-expanded for the flyout it opens; the flyout closes and returns focus to the rail button on Escape, and closes (without moving focus) on an outside pointerdown or when focus leaves it. The flyout opens with a named, keyboard-reachable Expand button ahead of the panel's own content (issue #636) - Tab into the flyout and activate it with Enter or Space to restore the panel to a normal docked header, moving focus to that header's own collapse-toggle.

Example: Toolbox, canvas and properties

```html
<pk-dock label="Editor workspace" resize-label="Resize panels">
  <div slot="tools" data-heading="Toolbox" data-group="left" class="stack"><span>Select</span><span>Rectangle</span><span>Text</span></div>
  <div slot="canvas" data-heading="Canvas" class="stack"><span>The canvas fills the middle.</span><span>Drag a separator, or focus it and press an arrow key.</span></div>
  <div slot="props" data-heading="Properties" data-group="right" class="stack"><span>Width 120</span><span>Height 80</span></div>
</pk-dock>
```

## `pk-fill-column`

**Fill column** (Layout). A column in a parent of fixed height: the fixed slot keeps its height, the rest fills and scrolls.

**Props**

| Attribute | Type | Default | Values | Description |
|---|---|---|---|---|
| `fixed` | string | `""` |  | Fixed region height, a CSS length (12rem). Empty: natural height. |
| `gap` | enum | `"md"` | `none` `xs` `sm` `md` `lg` `xl` | Space between regions. |

**Slots**

| Slot | Description |
|---|---|
| `fixed` | Keeps its height above the rest. Empty: hidden. |
| (default) | Fills the rest and scrolls. |

**CSS parts**

| Part | Description |
|---|---|
| `fixed` | The fixed region. |
| `rest` | The region that fills the rest and scrolls. |

**CSS custom properties**

| Property | Default | Description |
|---|---|---|
| `--pk-fill-column-fixed` | `fixed` | Overrides the fixed prop. |
| `--pk-fill-column-gap` | `gap` | Overrides gap. |

**Accessibility.** A layout box with no role; reading order is fixed region, then rest.

Example: A preview above a scrolling list

```html
<pk-fill-column fixed="8rem"><pk-card slot="fixed" heading="Camera preview">Live preview</pk-card><pk-stack><pk-text>Scanned item 1</pk-text><pk-text>Scanned item 2</pk-text><pk-text>Scanned item 3</pk-text></pk-stack></pk-fill-column>
```

## `pk-grid`

**Grid** (Layout). Equal-width responsive columns: as many columns of at least min as fit, one column on a phone.

**Props**

| Attribute | Type | Default | Values | Description |
|---|---|---|---|---|
| `min` | string | `"16rem"` |  | Minimum column width as a CSS length (for example 12rem or 200px). Columns are equal width and never narrower than this, so the grid drops to one column when the container is narrower than two. |
| `columns` | number | `0` |  | The most columns to use, 0 for no limit. The grid still drops columns when they would be narrower than min. |
| `ratio` | string | `""` |  | Unequal columns as a ratio, for example 2:1 or 1:2:1. Overrides min and columns; on a phone the grid falls back to auto columns so it stacks. |
| `tracks` | string | `""` |  | A column track list as CSS writes it, for a fixed grid such as a 12-column dashboard (repeat(12, minmax(0, 1fr))) or uneven columns (12rem 1fr). Use minmax(0, ...) so wide content cannot blow a track out. Children take columns with data-col-span. Overrides ratio, min and columns; an unparseable value is ignored with one warning. On a phone the grid falls back to auto columns, so it stacks and spans stop applying. |
| `tablet-tracks` (property `tabletTracks`) | string | `""` |  | Column tracks from 641 to 1024px, for example repeat(6, minmax(0, 1fr)) to halve 12 tracks; data-col-span halves too. |
| `stack-below` (property `stackBelow`) | enum | `"phone"` | `phone` `tablet` | Stack at phone (640px) or tablet (1024px) width. |
| `fill` | boolean | `false` |  | Takes the remaining height of its parent (a flex column or a sized block). Off on a phone. |
| `rows` | string | `""` |  | Row tracks as CSS writes them (minmax(0, 1fr) minmax(0, 2fr)); with fill they share the height. Off on a phone. |
| `sticky-first` (property `stickyFirst`) | boolean | `false` |  | The first child stays in view while the page scrolls past the other columns (a section nav beside a long article): sticky at --pk-grid-sticky-top (default --space-4) from the top of the scrolling area, sized to its content. Use with ratio or tracks; on a phone the grid stacks and nothing sticks. |
| `gap` | enum | `"md"` | `none` `xs` `sm` `md` `lg` `xl` | Space between children, on the spacing scale (none 0, xs --space-1, sm --space-2, md --space-4, lg --space-6, xl --space-8). Default md (the card gap). |

**Slots**

| Slot | Description |
|---|---|
| (default) | The items to lay out. Any child can carry data-col-span="2" to "12" (spans that many columns) and data-row-span="2" to "4" (spans that many rows), meant for a grid with tracks; on a pk-* child it is a plain attribute (<PkCard data-col-span="6"> in Blazor). Not applied on a phone. |

**CSS custom properties**

| Property | Default | Description |
|---|---|---|
| `--pk-grid-min` | `16rem` | The column minimum; set by the min prop. |
| `--pk-grid-columns` | `unset (no cap)` | The column cap; set by the columns prop. |
| `--pk-grid-tablet` | `unset` | Set by tablet-tracks. |
| `--pk-grid-rows` | `unset` | Set by rows. |
| `--pk-grid-sticky-top` | `var(--space-4)` | With sticky-first: the distance of the first child from the top of the scrolling area (under a sticky header, set it to the header's height). |
| `--pk-grid-ratio` | `unset (auto-fit columns)` | The explicit column list; set by the ratio prop. |

**Accessibility.** A plain layout box with no role; the DOM order is the reading order, so a single column on a phone keeps the same order.

Example: A 12-column dashboard

```html
<pk-grid tracks="repeat(12, minmax(0, 1fr))"><pk-card heading="Revenue" data-col-span="8">Wide</pk-card><pk-card heading="Users" data-col-span="4">Narrow</pk-card><pk-card heading="Orders" data-col-span="4">A</pk-card><pk-card heading="Refunds" data-col-span="4">B</pk-card><pk-card heading="Churn" data-col-span="4">C</pk-card><pk-card heading="Activity" data-col-span="12">Full width</pk-card></pk-grid>
```

## `pk-heading`

**Heading** (Layout). A real h1 to h6 heading in the shadow tree, with a visual size decoupled from the level: the semantic counterpart of pk-text's look-only h1 to h6 variants, for a strict module that cannot write a raw heading tag.

**Props**

| Attribute | Type | Default | Values | Description |
|---|---|---|---|---|
| `level` | number | `2` |  | The heading level exposed to assistive technology (1 to 6, clamped and rounded; out of range falls back to 2 and warns once). Rendered as a real h1 to h6 in the shadow tree, so it takes its place in the page outline. |
| `variant` | enum | `"inherit"` | `inherit` `h1` `h2` `h3` `h4` `h5` `h6` | The visual size on the --text-h1 to --text-h6 scale, independent of level: a section can be an h3 in the outline but look like an h1, or the reverse. Default inherit: the size matches level. |
| `tone` | enum | `"inherit"` | `inherit` `default` `muted` `accent` `positive` `warning` `critical` | Text colour from the theme, as on pk-text. Default inherit: the colour around it. |
| `weight` | enum | `"inherit"` | `inherit` `regular` `medium` `semibold` `bold` | Font weight. Default inherit: bold, the native heading weight. |
| `truncate` | boolean | `false` |  | Keep it to one line and end with an ellipsis when it does not fit. |

**Slots**

| Slot | Description |
|---|---|
| (default) | The heading text: phrasing content only. |

**CSS parts**

| Part | Description |
|---|---|
| `heading` | The rendered h1 to h6 element. |

**CSS custom properties**

| Property | Default | Description |
|---|---|---|
| `--pk-heading-size` | `var(--text-h2)` | The font size used when variant is inherit; set from level. An explicit variant overrides it. |

**Accessibility.** The shadow tree renders a real h1 to h6 for the level prop, so it keeps the heading role and level in the accessibility tree and the page outline; use one h1 per page and do not skip levels. variant changes only the look: a section that must be an h3 in the outline but look larger sets level="3" variant="h1", never the reverse (a level chosen for the look breaks the outline). For a look-only heading that is not a real heading (a figure label, a number in a card), use pk-text's h1 to h6 variant instead.

Example: Levels

```html
<pk-heading level="1">Billing</pk-heading><pk-heading level="2">Invoices</pk-heading><pk-heading level="3">Payment methods</pk-heading>
```

## `pk-kanban`

**Kanban board** (Layout). A board of pk-kanban-column children, each holding pk-sortable-item cards (usually a pk-card inside). Drag a card's handle to another column or position (pointer or touch), or focus a card and press Alt+Arrow. It never moves its own children: pk-move carries the move and the host applies it.

**Props**

| Attribute | Type | Default | Description |
|---|---|---|---|
| `label` | string | `""` | Accessible name for the board (aria-label). |
| `disabled` | boolean | `false` | No card can be grabbed or moved by the keyboard. |
| `dragging` | boolean | `false` | True while a card is being dragged. A styling hook the element sets itself. |

**Slots**

| Slot | Description |
|---|---|
| (default) | pk-kanban-column children, in board order. |

**Events**

| Event | Detail | Description |
|---|---|---|
| `pk-move` | `{ item, from, to, fromIndex, toIndex }` | A card move was committed: a drag ended over a new column or position, or Alt+Arrow moved the focused card. item is the card's value, from and to are the columns' values, and fromIndex and toIndex are the card's index in its old column and the index it takes in the new one (the same list with the card lifted out, so within one column it is where the card ends up). The host applies it: pk-kanban never moves its own children. Cancelable: preventDefault declines the move, and the live region says it was cancelled. |

**Methods**

| Method | Description |
|---|---|
| `dropTarget(cols, x, y)` | The column and index a pointer position means; pure. |
| `keyDestination(key, col, index, lens, rtl)` | The column and index Alt+Arrow asks for; pure. |

**CSS parts**

| Part | Description |
|---|---|
| `board` | The horizontally scrolling row of columns. |
| `announcer` | The visually-hidden aria-live region that announces a grab, a move or a cancelled move for a screen reader. |

**CSS custom properties**

| Property | Default | Description |
|---|---|---|
| `--pk-kanban-gap` | `var(--space-3)` | Gap between columns. |
| `--pk-kanban-column-width` | `18rem` | Width of a column on a wide screen (a phone shows 85%). |

**Writes to host nodes**

| Target | Writes | Why |
|---|---|---|
| the pk-kanban-column and pk-sortable-item children | `dragging`, `drop-indicator`, `drop-target`, `tabindex` | dragging marks the card being dragged; drop-indicator draws the insertion line against a neighbour and drop-target highlights the column under the pointer (both cleared when the drag ends); tabindex is the roving focus stop across every card of the board. None is undone on disconnect, like pk-sortable. |

**Accessibility.** role=group; each column is a role=list and each card a listitem. One tab stop across the board (roving tabindex): Up and Down move between cards of a column, Left and Right to the neighbouring column (mirrored right to left). Alt+Up and Alt+Down reorder the focused card, Alt+Left and Alt+Right move it to the next column; focus stays on it. A visually-hidden aria-live=polite region announces "Card moved to Done, position 2 of 4." Escape cancels a drag. Pointer dragging scrolls automatically: near a column edge the column scrolls, near the board edge the board scrolls, and near the top or bottom of the window (or of the nearest scrolling ancestor) the page scrolls (a steady half speed under prefers-reduced-motion); keyboard moves are unchanged.

Example: A three-column board

```html
<pk-kanban label="Sprint board">
  <pk-kanban-column value="todo" label="To do">
    <pk-sortable-item value="t1"><pk-card heading="Write the spec" level="3">Two pages.</pk-card></pk-sortable-item>
    <pk-sortable-item value="t2"><pk-card heading="Design the header" level="3">Before Friday.</pk-card></pk-sortable-item>
  </pk-kanban-column>
  <pk-kanban-column value="doing" label="In progress">
    <pk-sortable-item value="d1"><pk-card heading="Build the board" level="3">Pointer, touch and keys.</pk-card></pk-sortable-item>
  </pk-kanban-column>
  <pk-kanban-column value="done" label="Done" empty-text="Nothing shipped yet"></pk-kanban-column>
</pk-kanban>
```

## `pk-kanban-column`

**Kanban column** (Layout). One column of a pk-kanban: a title with a card count, the cards, and a message when it has none. Place it directly inside a pk-kanban.

**Props**

| Attribute | Type | Default | Description |
|---|---|---|---|
| `value` | string | `""` | The identifier pk-kanban reports for this column in a pk-move event's from and to. |
| `label` | string | `""` | The column's title, and its accessible name. Falls back to value. |
| `empty-text` (property `emptyText`) | string | `"No cards"` | Shown in place of the cards while the column has none; it is also the drop target for a card dragged in. |
| `drop-target` (property `dropTarget`) | boolean | `false` | Set by the surrounding pk-kanban while a dragged card is over this column. |

**Slots**

| Slot | Description |
|---|---|
| (default) | pk-sortable-item cards, in order. |

**CSS parts**

| Part | Description |
|---|---|
| `root` | The column's frame. |
| `header` | The title row. |
| `title` | The column title. |
| `count` | The number of cards. |
| `list` | The scrolling region that holds the cards; a long column scrolls here, not the page. |
| `empty` | The empty message. |

**CSS custom properties**

| Property | Default | Description |
|---|---|---|
| `--pk-kanban-column-max-height` | `32rem` | Height at which a long column starts to scroll. |

**Accessibility.** role=list, named by label (or value); its cards are listitems. The count is text beside the title.

Example: Inside pk-kanban

```html
<pk-kanban label="Board">
  <pk-kanban-column value="todo" label="To do">
    <pk-sortable-item value="a">Mix</pk-sortable-item>
  </pk-kanban-column>
</pk-kanban>
```

## `pk-label-sheet`

**Label sheet** (Layout). A grid of fixed physical cells for labels, bin tags and entry cards: set the cell size in mm, in or pt, a gap and the cells per row; each child is one cell and is never split across a printed page. Use it inside pk-print-page, whose size and margin define the sheet and which hides the app chrome. Cells show at true scale on screen.

**Props**

| Attribute | Type | Default | Description |
|---|---|---|---|
| `cell-width` (property `cellWidth`) | string | `"50mm"` | Cell width: a number with mm, cm, in, pt or px. Anything else falls back to 50mm and warns once. |
| `cell-height` (property `cellHeight`) | string | `"25mm"` | Cell height, same units. Falls back to 25mm. |
| `columns` | number | `0` | Cells per row; 0 fits as many as the width allows. |
| `gap` | string | `"0"` | Gap between cells, same units. |
| `offset-x` (property `offsetX`) | string | `"0"` | Sheet origin from the left of its box, for pre-cut stock. |
| `offset-y` (property `offsetY`) | string | `"0"` | Sheet origin from the top of its box, for pre-cut stock. |
| `outline` | boolean | `false` | Dashed cut guides around every cell, on screen and in print. |

**Slots**

| Slot | Description |
|---|---|
| (default) | The cells: one element per label (a pk-card, a div with an SVG barcode). A child larger than its cell is clipped and marked data-clipped. |

**CSS custom properties**

| Property | Default | Description |
|---|---|---|
| `--pk-label-sheet-cell-width` | `cell-width` | Overrides cell-width. |
| `--pk-label-sheet-cell-height` | `cell-height` | Overrides cell-height. |
| `--pk-label-sheet-gap` | `gap` | Overrides gap. |
| `--pk-label-sheet-pad` | `0` | Padding inside every cell. |

**Writes to host nodes**

| Target | Writes | Why |
|---|---|---|
| each cell (the slotted children) | `data-clipped` | A flag only: set while a cell's content is larger than the cell and clipped, so a page or test can find the label that does not fit. Nothing else on a cell is written. |

**Accessibility.** Adds no semantics: the cells keep their own roles and text. Colours and fills print (print-color-adjust: exact).

Example: Bin labels, three to a row

```html
<pk-label-sheet cell-width="50mm" cell-height="25mm" columns="3" gap="2mm" outline>
  <pk-card heading="Bin A1"><p>M6 bolts</p></pk-card>
  <pk-card heading="Bin A2"><p>M8 nuts</p></pk-card>
  <pk-card heading="Bin A3"><p>Washers</p></pk-card>
  <pk-card heading="Bin B1"><p>Springs</p></pk-card>
</pk-label-sheet>
```

## `pk-list`

**List** (Layout). A plain ordered or unordered prose list: bullets, numbers or checkmarks on items the host writes as plain elements, never a raw ul, ol or li.

**Props**

| Attribute | Type | Default | Values | Description |
|---|---|---|---|---|
| `ordered` | boolean | `false` |  | Numbered instead of bulleted, when marker is auto (the default). |
| `marker` | enum | `"auto"` | `auto` `disc` `decimal` `check` `none` | The bullet: auto follows ordered (decimal when set, disc otherwise), disc and decimal force a style regardless of ordered, check draws a checkmark, none removes the marker and its indent (the list role stays; it comes from ARIA, not the native marker). |
| `gap` | enum | `"xs"` | `none` `xs` `sm` `md` `lg` `xl` | Space between items, on the spacing scale (none 0, xs --space-1, sm --space-2, md --space-4, lg --space-6, xl --space-8). Default xs. |
| `dense` | boolean | `false` |  | Tighter rhythm and smaller text. |
| `label` | string | `""` |  | Accessible name of the list. |

**Slots**

| Slot | Description |
|---|---|
| (default) | Items: plain elements (span, div, a) or pk-text inline. Never li; the list role and item count come from ARIA. |

**CSS custom properties**

| Property | Default | Description |
|---|---|---|
| `--pk-list-gap` | `the gap step` | Space between items, overriding gap. |

**Writes to host nodes**

| Target | Writes | Why |
|---|---|---|
| the slotted items | `role` | An item without a role gets role=listitem so the list has the items the host role=list expects; a role the host set is left alone. Not removed. Kept when the element disconnects. |

**Accessibility.** The host is role=list (named by label when set) and each item gets role=listitem unless it already has one. The marker is drawn by the browser's native ::marker on a display: list-item box, so it never carries the meaning alone; with marker=none the list-style is removed but the ARIA role stays, so the list and its item count still reach assistive technology.

Example: Unordered

```html
<pk-list label="Shipping notes"><span>Orders ship within two working days.</span><span>Anything placed after 3 pm counts as the next day.</span><span>Weekend orders ship Monday.</span></pk-list>
```

## `pk-list-page`

**List page** (Page types). A filterable, sortable, paginated list of records: a pk-data-table (the one implementation of the query, load and states: a manual pk-table, a pk-table-filters search/filter panel and a pk-pagination footer) under an optional title bar, with optional toolbar actions. Set the load(query) callback as a property (business logic, never config data) - it receives { page, pageSize, sort, sortDir, search, filters } and returns (or resolves to) { rows, total, filterOptions? } for that page (filterOptions: { filterKey: [{ value, label }] } replaces those select filters' options, e.g. labels with counts); a rejection shows the built-in error state with Retry, and zero rows show the configured empty state. rowHref(row), also a callback property, makes rows clickable and is called instead of the element navigating itself, so the host wires its router.

**Props**

| Attribute | Type | Default | Description |
|---|---|---|---|
| `clickable` | boolean | `false` | Rows raise pk-row-click when clicked (or Enter/Space on the focused row). Without it only rowHref makes rows clickable. |
| `row-href-template` (property `rowHrefTemplate`) | string | `""` | Row links: an address with {id} (and any {field}, URL-encoded), like /orders/{id} or #/orders/{id}. The first cell of each row becomes a real link; a plain click on it or the row goes through the router (pk-navigate). Plain data, so a wrapper can set it (rowHref is a callback). |
| `navigate` | string | `""` | Who follows a row link: router (default, empty), host or none. |
| `config` | json | `{}` | { columns, filters?, actions?, empty?, pageSize? }. pk-page-header title bar keys: badges [{ label, variant? }], actionsOverflow, overflowLabel, homeHref, homeLabel, homeIcon. columns is pk-table's own column definitions ({ key, label, type?, align?, sortable?, hidePhone?, shed?, title? }[]). filters is [{ key, type: 'text'\|'email'\|'number'\|'date'\|'select'\|'multiselect', label, options? }] (a select's options are [{ value, label? }] or plain strings), rendered into the pk-table-filters panel and combined into query.filters as { [key]: text }. actions is [{ label, href?, variant?, icon?, collapse? }], toolbar buttons shown beside the search box; icon is an SDK icon name drawn before the label, and the button folds to the icon alone on a phone (collapse: 'phone' by default with an icon, 'tablet' or 'none'), the label staying its accessible name. empty is { heading?, description? } shown when a page has zero rows. expandable? (boolean), expander? ('none') and expanded? (row ids) are pk-data-table's: rows with a detail-<rowId> child get a toggle and pk-row-expand reports it (expanding is not a row click). selectable? (boolean) shows the checkbox column and the bulk bar, rowKey? names the row field that identifies a row (default id), bulkActions? is [{ id, label, variant?, icon?, collapse? }], the buttons of the bulk bar. pageSize is the initial page size (default 25); search?, filterValues?, sort?, sortDir? and page? seed the first query. Title bar: heading?: string and breadcrumb?: [{ label, href? }]; nothing is drawn when neither is set. toolbarInHeader? puts them in the toolbar row with the search (no pk-page-header: no sticky header or back link). cards? (default true unless selectable): cards below 640px. searchPlaceholder? is the search box text (placeholder and accessible name; searchLabel then is the accessible name only: a placeholder that differs from it). selectPageOnly? (with selectable) selects only the loaded page and never offers "Select all N", so pk-bulk scope is always "page". stickyHeader? (boolean) pins the column header; in a flex column of definite height (a pk-stack[fill], an app-shell body) the rows scroll and the pager and bulk bar stay pinned. |

**Slots**

| Slot | Description |
|---|---|
| `badges` | Status chips beside the title. |
| `actions` | Title bar content. |
| `toolbar` | End of the toolbar. |
| `detail-<rowId>` | Detail content of one row, shown under it when the row is expanded (config.expandable), as pk-data-table's detail slots. |
| `before-list` | Content between the title bar and the list (an alert, a summary card). It keeps its height while the page has room, else shrinks and scrolls in its own box, so the list keeps --pk-list-page-table-min and a tall summary never squeezes the rows to zero; with stickyHeader the column header and pager stay pinned. With no definite body height the page scrolls as a whole. |

**Events**

| Event | Detail | Description |
|---|---|---|
| `pk-select` | `{ selected: string[], scope: 'page' \| 'all', query: { page, pageSize, sort, sortDir, search, filters } }` | The selection changed (config.selectable), passed through from the pk-data-table. For scope 'all' run the bulk action against query on the server, never against the ids. |
| `pk-row-click` | `{ id: string, row: object }` | A row was activated (clickable): id is the rowKey value, row the row object from load(). The data table's own event, passed through. |
| `pk-row-expand` | `{ id: string, index: number, expanded: bool }` | The reader expanded or collapsed a row (config.expandable): expanded is the new state. The table's own event, passed through; not a row click. |
| `pk-bulk` | `{ action: string, selected: string[], scope: 'page' \| 'all', query: object }` | A button of config.bulkActions was pressed: its id and the selection at that moment, as pk-select carries it. |

**Methods**

| Method | Description |
|---|---|
| `refresh()` | Loads the current query again. |

**CSS parts**

| Part | Description |
|---|---|
| `header` | The shared title bar: a pk-page-header with the breadcrumb and actions from config. Hidden when config sets none of them. |
| `table` | The pk-data-table, which owns the query, the load, the states, the search/filter panel and the pager (its own parts: table, filters, pagination, state; part('filters'), part('pagination') and part('state') on this page reach them). |
| `before` | The box around the before-list slot; shrinks and scrolls when the page is short, hidden while empty. |
| `footer` | The table footer bar (the pager sits in it): a top rule and block padding (--pk-table-footer-padding). |
| `pager` | The pk-pagination bar (its nav), exported from the pager. |
| `pager-prev` | The previous-page button. |
| `pager-next` | The next-page button. |
| `bulk` | Holds one button per config.bulkActions entry, in the data table's bulk bar (shown while rows are selected). |
| `actions` | Holds one button per config.actions entry, in the data table's actions slot at the end of the toolbar. |

**CSS custom properties**

| Property | Default | Description |
|---|---|---|
| `--pk-list-page-table-min` | `calc(var(--space-12) * 6)` | The least height the list keeps while a before-list region is slotted. |

**Accessibility.** Every filter field is a labelled real control (pk-input/pk-select, showLabel on); the table, search box and pagination keep their own roles, names and announcements; the loading and error states use pk-skeleton and pk-alert's own roles and announcements.

Example: Orders list (config and load are set from script)

```html
<pk-list-page id="orders"></pk-list-page>
```

## `pk-master-detail-page`

**Master-detail page** (Page types). Static slotted mode (for server-rendered, JavaScript-optional sites): write your own list as a child in slot="list" (a pk-side-nav of links, a plain list) and the record as a child in slot="detail", with no config.list. The page lays them out as written (side by side wide; one pane at a time on a phone: the list, or the record when you wrote record-id), takes the current row from your own markup (aria-current / current), and never creates, moves or changes your elements; load, open, close, mountDetail and config do nothing, and the record keeps its own back link. A list or record is real markup even without JavaScript. Dynamic mode: A list of records and the record the route selected. Wide, they sit side by side (a pk-list-page and the record pane); narrow, one shows at a time and the record has a Back button. The host owns the selection: set recordId from the route (a deep link works), and the element reports what the reader did through callback properties. load(query) feeds the list exactly as pk-list-page's does; open(row) is called when a row is chosen and close() when Back is pressed, so the host changes the route. mountDetail(pane, id) draws the record into the pane and returns a cleanup function or a { destroy() } handle, called when the record changes or the element leaves the page. While mountDetail is pending the built-in loading state shows over the record, a rejection shows the error state with Retry. Focus moves to the record once it is drawn, and back to the row that was open when the selection clears.

**Props**

| Attribute | Type | Default | Description |
|---|---|---|---|
| `config` | json | `{}` | { list?, backLabel?, none?, label? }. pk-page-header title bar keys: badges [{ label, variant? }], actionsOverflow, overflowLabel, homeHref, homeLabel, homeIcon. list is pk-list-page's own config ({ columns, filters?, actions?, empty?, pageSize? }). backLabel is the Back button's text (default 'Back'). none is { heading?, description? } shown in the record pane while nothing is selected. label names the loading state. Optional title bar from the shared page shell: heading?: string, breadcrumb?: [{ label, href? }], actions?: [{ key, label, href?, variant?, hint? }] (a hint shows as a tooltip on the button) (an action without href fires pk-action). Nothing is drawn when none is set. |
| `record-id` (property `recordId`) | string | `""` | The id of the selected record, from the route; empty when none is. Untrusted text: it is only passed to mountDetail. |
| `fill` | boolean | `false` | Take all the room a flex column parent leaves. |

**Slots**

| Slot | Description |
|---|---|
| `badges` | Status chips beside the title. |
| `list` | Static slotted mode: your own list of records (a pk-side-nav, a plain list of links), shown as written; its presence with no config.list selects the mode, decided when the element connects. |
| `detail` | The record (mountDetail not set), shown only while recordId is set. Static slotted mode: your own record markup, shown as written; on a phone it shows instead of the list while record-id is set. |

**Events**

| Event | Detail | Description |
|---|---|---|
| `pk-action` | `{ key }` | A title-bar action (config.actions entry without an href) was clicked. |

**Methods**

| Method | Description |
|---|---|
| `refresh()` | Runs the list's load() again (the Blazor wrapper does after setting load). |

**CSS parts**

| Part | Description |
|---|---|
| `header` | The shared title bar: a pk-page-header with the breadcrumb and actions from config. Hidden when config sets none of them. |
| `layout` | The grid holding both panes. |
| `master` | The list pane. |
| `list` | The pk-list-page. |
| `bar` | Holds the Back button. |
| `detail` | The record pane (focused once its record is drawn). |
| `back` | The Back button; shown only when one pane shows at a time. |
| `none` | The nothing-selected message. |
| `record` | The record's content; mountDetail draws into it. |
| `state` | Shows the loading or error state in place of the record. |

**Accessibility.** The list keeps pk-list-page's table, filter and pagination semantics. The record pane is focusable and takes focus when a record opens, so a keyboard or screen reader user lands on it; Back returns focus to the row that was open. The loading and error states use pk-skeleton and pk-alert's own roles.

Example: Static slotted mode: your own list and record as plain markup, no script

```html
<pk-master-detail-page record-id="2"><pk-side-nav slot="list" label="Orders"><pk-nav-item href="#o1"><a href="#o1">Order 1</a></pk-nav-item><pk-nav-item href="#o2" current><a href="#o2" aria-current="page">Order 2</a></pk-nav-item></pk-side-nav><article slot="detail"><h2>Order 2</h2><p>Shown as written.</p><a href="#o1">Back to the orders</a></article></pk-master-detail-page>
```

## `pk-not-found-page`

**Not found page** (Page types). The "this page doesn't exist" or "you don't have access" screen for a whole page, built on the same shared state markup as pk-states-page's empty state. Works with zero config; add label for a real action button that raises pk-action.

**Props**

| Attribute | Type | Default | Description |
|---|---|---|---|
| `heading` | string | `""` | Heading. Defaults to "Page not found". |
| `description` | string | `""` | Explanation. Defaults to "The page you're looking for doesn't exist or you don't have access to it." |
| `label` | string | `""` | Text for an action button (e.g. "Go home"); no button is drawn when empty. Clicking it raises pk-action - this element never navigates by itself. |

**Slots**

| Slot | Description |
|---|---|
| `title` | The page title: a level 1 pk-heading the built-in page factory (mountTitled) appends, so the app can focus it after navigation. When present, the built-in state heading is not drawn a second time. |
| `drawn` | Internal: the drawn pk-empty-state content and its action button. A light-DOM child this element creates and self-assigns here (never a shadow-root child), so the SDK's on-demand loader can see and upgrade it. |

**Events**

| Event | Detail | Description |
|---|---|---|
| `pk-action` | `none` | The label action button was activated. The host decides what happens (navigate home, go back); this element never navigates by itself. |

**Writes to host nodes**

| Target | Writes | Why |
|---|---|---|
| the drawn child | `slot` | A light-DOM element this element creates and owns itself (never a host-given node), self-assigned to its own drawn slot so the SDK's on-demand loader can see and upgrade it (a shadow-root child would be invisible to it). |

**Accessibility.** Uses pk-empty-state's own role and heading level; the action is a real pk-button in its actions slot.

Example: Zero config

```html
<pk-not-found-page></pk-not-found-page>
```

## `pk-note-page`

**Note page** (Page types). A static informational page: a heading and a block of prose in a card. No data, no callbacks - the page-type building block for a one-off About/Overview/Summary page.

**Props**

| Attribute | Type | Default | Description |
|---|---|---|---|
| `config` | json | `{}` | { heading, body, cardHeading }. heading is the page's h1 text, body is the card's prose, cardHeading is an optional card title (the card has no header without it). |

**CSS parts**

| Part | Description |
|---|---|
| `body` | Wraps the built pk-stack > pk-heading + pk-card content. |

**Accessibility.** The heading is a real pk-heading (h1); the card's body is a plain paragraph.

Example: About page (set config from script: an inline script cannot run under this site's CSP, so wire it the way pk-tool-page's run is wired)

```html
<pk-note-page id="about"></pk-note-page>
```

## `pk-page-header`

**Page header** (Layout & structure). The top of a page, a section or a record: a breadcrumb row, a title bar with actions, and a note line. Without an explicit heading, the crumbs, chips and actions collapse onto a single row by default; spacious opts back into the multi-row breakdown. Stacks by its own width, with the actions full width and touch-sized on a phone.

**Props**

| Attribute | Type | Default | Values | Description |
|---|---|---|---|---|
| `heading` | string | `""` |  | The title. Without it (and without default or actions content) the title bar is not shown. |
| `level` | number | `2` |  | The heading level the title exposes to assistive technology (aria-level). |
| `variant` | enum | `"page"` | `page` `section` `record` | page is the standard page top; section is a titled section inside a page; record is the compact strip of a single-record page (chips left, actions right; on a narrow container the chips move to a row below). Its title, the heading or the last crumb, is one line: a long one ends in an ellipsis with the full text as its tooltip, and the chips and actions keep their width. |
| `spacious` | boolean | `false` |  | Without a heading the crumbs, chips and actions share one row. Set spacious for the multi-row breakdown (crumbs, then a title bar with actions, then the note line), for a long title or many badges. No effect when heading is set. |
| `wrap-title` (property `wrapTitle`) | boolean | `false` |  | Record variant only: on a narrow container (40rem or less, a phone) the title wraps to at most three lines (two on a phone, 30rem or less), then ends in an ellipsis, instead of the single ellipsized line. Wide containers keep one line. Off by default (the one-line title keeps the chips and actions on a steady row); the full title stays the tooltip in the one-line mode. With a slotted pk-heading, leave its truncate off. |
| `sticky` | boolean | `false` |  | Pins the header to the top of the scrolling page body while the content scrolls: in pk-app-shell flush under the app bar (the body is the scroller), in a plain page at the top of the viewport. It is its own stacking context one above in-body sticky strips, so a popover opened from the header draws over them. |
| `sticky-from` (property `stickyFrom`) | enum | `"always"` | `always` `phone` `tablet` | With sticky, the width above which the header stays pinned: always, phone (above the phone breakpoint) or tablet. At and below it the header scrolls away, leaving room above the on-screen keyboard. The opposite sense of tabs-from. |
| `crumbs` | string | `""` |  | The breadcrumb trail as a JSON array of { "label", "href" }, drawn in a pk-breadcrumb above the title (the last crumb is the current page: aria-current, and the page title when no heading is set). An href that is not a same-site path, http(s), mailto, tel or sms address draws a crumb without a link. Use it instead of the breadcrumb slot, not with it. |
| `actions-overflow` (property `actionsOverflow`) | enum | `"never"` | `never` `phone` `tablet` `always` | Collapses the actions slot into one icon button that opens a menu at the named width and below (phone, tablet) or always. Each slotted action becomes a menu item with its label, icon-name, disabled and danger state; choosing one clicks the original, so its handlers, href and events are unchanged, and the title bar stays one row high. Use pk-button or link actions as direct children of the slot; a pk-dropdown or pk-card-menu among them is flattened into its own items (headers kept, selecting one selects the original). A slotted menu with no items adds nothing, and with nothing to choose the overflow button is not drawn. |
| `overflow-label` (property `overflowLabel`) | string | `"Actions"` |  | The accessible name of the overflow button drawn by actionsOverflow. |
| `breadcrumb-label` (property `breadcrumbLabel`) | string | `"Breadcrumb"` |  | The accessible name of the breadcrumb landmark drawn from crumbs. |
| `home-href` (property `homeHref`) | string | `""` |  | An address for a leading home crumb: an icon-only link (named by homeLabel) first in the trail crumbs draws. Empty draws none. |
| `home-label` (property `homeLabel`) | string | `"Home"` |  | The accessible name of the home crumb. |
| `home-icon` (property `homeIcon`) | string | `"dashboard"` |  | The icon of the home crumb (a name of the icon sprite). |
| `tabs-from` (property `tabsFrom`) | enum | `"always"` | `phone` `tablet` `always` | Where the docked tabs strip shows: always, or only at phone or tablet width and below (above it the strip is hidden, so a page that shows every section at that width needs no strip). |
| `tabs` | json | `[]` |  | The docked strip as data: an array of { value, label, count?, visible? }, the same shape as pk-record-form. The element draws a pk-tabs at the foot of the header (a pk-tabs in the tabs slot wins); count is the badge after the label and a tab with visible false is left out. With sticky the strip stays in view with the bar. Listen for pk-tab-change for the chosen value. |
| `back-link` (property `backLink`) | boolean | `false` |  | Draws a back link above the header to the last crumb before the current one that has a usable href: a ghost icon-only button with a chevron, named "Back to <label>". On a header of 30rem or less with no heading (the crumbs are the title) the chevron takes the start of row one and the home and parent crumbs fold away, so the title shares the row with the actions. Off by default because the app shell can draw its own. |

**Slots**

| Slot | Description |
|---|---|
| `title` | A real heading element for the title (a pk-heading) in place of the built-in title text; set heading too so the title bar is drawn. The page shell (showTitleBar, so every built-in page type) slots a pk-heading level 1 here. |
| (default) | Beside the title: status chips, badges or your own heading element. In the record variant on a narrow container they drop to their own row under the title and actions. |
| `breadcrumb` | A pk-breadcrumb (or any trail) above the title; hidden when empty. |
| `actions` | Buttons at the right of the title bar; full width and touch-sized on a narrow container. Give each an icon-name and collapse="phone" so it folds to the icon alone on a phone (its text stays the accessible name). |
| `meta` | A note or metadata line under the title bar; hidden when empty. |
| `tabs` | A tab strip (pk-tabs with tabs only, or your own) docked at the foot of the header, so with sticky it stays in view with the bar; replaces the strip drawn from the tabs array; hidden when empty or when tabs-from says it does not apply at this width. A strip with more tabs than the width holds scrolls inside the header with a fade on the hidden side (pk-tabs overflow="scroll", its default; overflow="menu" lists them in a menu instead), so it never widens the page. |

**Events**

| Event | Detail | Description |
|---|---|---|
| `pk-tab-change` | `{ value, previous, fallback? }` | The docked strip chose a tab. |

**CSS parts**

| Part | Description |
|---|---|
| `overflow` | The dropdown drawn by actionsOverflow. |
| `overflow-button` | The icon button that opens the overflow menu. |
| `header` | The header element. |
| `trail` | The pk-breadcrumb drawn from crumbs. |
| `back` | The back link drawn when backLink is set. |
| `crumbs` | The breadcrumb row. |
| `titlebar` | The row holding the title and the actions. |
| `lead` | The title and the default slot. |
| `title` | The built-in title text (hidden when the title slot is filled). |
| `strip` | The pk-tabs the element draws from the tabs array (the tabs slot's fallback). |
| `actions` | The actions wrapper. |
| `meta` | The note line. |
| `chips` | Wraps the default slot; a plain pass-through except in a narrow record, where it is its own row. |
| `tabs` | The docked tabs row. |

**CSS custom properties**

| Property | Default | Description |
|---|---|---|
| `--pk-page-header-padding` | `var(--space-1) 0` | Padding of the header. |
| `--pk-page-header-title-size` | `var(--text-lg)` | Title font size (the built-in title text; a slotted pk-heading takes its own variant). |
| `--pk-page-header-sticky-top` | `0` | Offset from the top of the scroller when sticky. 0 is flush under the app bar inside pk-app-shell; on a plain page under a fixed bar of your own set it to that bar's height (for example var(--app-header-h)). |

**Accessibility.** The header is a header element, not a banner landmark, so it is safe inside main. With a slotted title (the page shell uses a pk-heading level 1, focusable with tabindex -1 so a route change can move focus to it) the title is that element's own real heading and the built-in text is hidden. Otherwise the title is exposed as a heading whose level you choose with level, so it fits the page outline (use 1 for a page, 2 or 3 for a section). Give the breadcrumb slot a pk-breadcrumb, which is its own labelled navigation landmark; a home crumb is an icon-only link first in that trail and needs an aria-label. Tabs in the tabs slot keep their own tablist semantics. Any variant with no heading attribute renders single-row by default (unless spacious is set), with the last breadcrumb crumb (already aria-current="page") standing in for the title; pk-page-header does not own pk-breadcrumb's slotted content, so it cannot add a heading role to that crumb for you — add role="heading" and the matching aria-level (matching level) on that last crumb yourself, as in the record example below.

Example: Docked tabs from data

```html
<pk-page-header heading="Blue mug" level="2" sticky tabs-from="phone" tabs='[{"value":"details","label":"Details"},{"value":"orders","label":"Orders","count":3},{"value":"audit","label":"Audit","visible":false}]'></pk-page-header>
```

## `pk-page-shell`

**Page shell** (Layout & structure). The frame of one page: a pk-page-header (breadcrumb, title, actions, a docked tab strip) above one content region. A whole-page state (loading, empty, error, forbidden) replaces the content. The spacing between header and content is the shell's own, so a page declares none. Tabs: one content region, and the page swaps what it renders when the tab strip fires pk-tab-change.

**Props**

| Attribute | Type | Default | Values | Description |
|---|---|---|---|---|
| `heading` | string | `""` |  | The page title, drawn by the header. |
| `level` | number | `1` |  | The heading level the title exposes to assistive technology. A page title is 1. |
| `crumbs` | string | `""` |  | The breadcrumb trail as a JSON array of { "label", "href" }, as on pk-page-header. Use it instead of the breadcrumb slot. |
| `back-link` (property `backLink`) | boolean | `false` |  | Draws a back link to the last crumb that has a usable href. |
| `variant` | enum | `"page"` | `page` `section` `record` | As on pk-page-header: record is the compact strip of a single-record page. |
| `wrap-title` (property `wrapTitle`) | boolean | `false` |  | As on pk-page-header (record variant only): on a narrow container the title wraps to at most three lines instead of one ellipsized line. |
| `sticky` | boolean | `false` |  | Pins the header (with its tabs) to the top of the scrolling page body. |
| `sticky-from` (property `stickyFrom`) | enum | `"always"` | `always` `phone` `tablet` | As the header's. |
| `tabs-from` (property `tabsFrom`) | enum | `"always"` | `phone` `tablet` `always` | As the header's. |
| `home-href` (property `homeHref`) | string | `""` |  | As on pk-page-header: an icon-only home crumb first in the trail. |
| `home-label` (property `homeLabel`) | string | `"Home"` |  | As the header's. |
| `home-icon` (property `homeIcon`) | string | `"dashboard"` |  | As the header's. |
| `actions-overflow` (property `actionsOverflow`) | enum | `"never"` | `never` `phone` `tablet` `always` | As on pk-page-header: the actions fold into one menu button. |
| `overflow-label` (property `overflowLabel`) | string | `"Actions"` |  | As the header's. |
| `state` | enum | `"ready"` | `ready` `loading` `empty` `error` `forbidden` | ready shows the content. Any other value shows that whole-page state in its place and hides the content (it stays in the DOM). The header and its tabs stay. |
| `state-heading` (property `stateHeading`) | string | `""` |  | The state's heading (the loading label for loading). Empty uses a default per state. |
| `state-description` (property `stateDescription`) | string | `""` |  | The line under the state's heading (the error message for error). |

**Slots**

| Slot | Description |
|---|---|
| (default) | The page content, one region. Hidden while a whole-page state shows. |
| `breadcrumb` | A pk-breadcrumb above the title, instead of crumbs. |
| `title` | A pk-heading for the title, in place of the built-in text. |
| `badges` | Status chips (pk-badge) beside the title. |
| `actions` | Buttons at the right of the title bar. Give each an icon-name and collapse="phone". |
| `meta` | A note line under the title bar. |
| `tabs` | A pk-tabs strip (tabs only) docked at the foot of the header. |

**Events**

| Event | Detail | Description |
|---|---|---|
| `pk-page-shell-retry` | `none` | Retry was pressed in the error state: load again, then set state back to ready. |

**CSS parts**

| Part | Description |
|---|---|
| `header` | The pk-page-header. |
| `state` | The whole-page state, drawn only while state is not ready. |
| `content` | The region holding the default slot. |

**CSS custom properties**

| Property | Default | Description |
|---|---|---|
| `--pk-page-shell-gap` | `var(--space-4), var(--space-3) on a phone` | The space between the header, the state and the content. |

**Accessibility.** The title is the header's real heading at level (1 for a page). The state region is a status role, so a loading or error state is announced; the tab strip keeps its own tablist semantics.

Example: Header, tabs and content

```html
<pk-page-shell heading="PO 1042" crumbs='[{"label":"Stock","href":"#"},{"label":"Purchase orders","href":"#"},{"label":"PO 1042"}]'>
  <pk-button slot="actions" icon-name="import" collapse="phone">Receive</pk-button>
  <pk-tabs slot="tabs">
    <pk-tab value="lines">Lines</pk-tab>
    <pk-tab value="history">History</pk-tab>
  </pk-tabs>
  <pk-card heading="Lines">Three lines, 412.00 in total.</pk-card>
</pk-page-shell>
```

## `pk-print-page`

**Print page** (Page types). A page whose document is the only thing that prints: a statement, a sheet of labels, a count sheet. On screen it is a paper-like sheet under a toolbar (flat on a phone); in print the toolbar, anything marked data-screen-only and the app shell's header, footer and nav disappear, @page size and margins come from props, and pk-card, pk-field-list, pk-property-grid, pk-field-group and figure stay whole while a pk-table keeps whole rows and repeats its header on every page. While connected the element adopts one constructed stylesheet onto the document (CSP-safe) and removes it when it leaves.

**Props**

| Attribute | Type | Default | Description |
|---|---|---|---|
| `size` | string | `""` | The CSS @page size: a paper name, optionally with an orientation ("A4", "letter landscape") or two lengths. Empty leaves the printer default. Only letters, digits, spaces, dots, percent signs and dashes are accepted. |
| `margin` | string | `"15mm"` | The CSS @page margin (one to four lengths, for example "15mm" or "20mm 15mm"). |

**Slots**

| Slot | Description |
|---|---|
| `toolbar` | Screen-only actions (print, back). Hidden when printing. |
| `title` | The page title: a level 1 pk-heading the built-in page factory (mountTitled) appends, so the app can focus it after navigation. It prints as part of the page. |
| (default) | The document: the only content that prints. Mark any part of it that should not print with the data-screen-only attribute. |

**CSS parts**

| Part | Description |
|---|---|
| `toolbar` | Wraps the toolbar slot; not printed. |
| `sheet` | Wraps the document: a paper-like sheet on screen (a phone gets it flat and full width), bare in print. |

**CSS custom properties**

| Property | Default | Description |
|---|---|---|
| `--pk-print-page-width` |  | Width of the sheet on screen (default 48rem). |

**Accessibility.** The toolbar comes before the document in reading order and keeps its own controls' names; nothing is added to the document's semantics. Print output uses the real DOM, so headings, tables and lists keep their structure.

Example: A statement with a screen-only toolbar

```html
<pk-print-page size="A4" margin="15mm">
  <pk-button slot="toolbar" icon-name="document">Print</pk-button>
  <pk-heading level="1">Statement, October</pk-heading>
  <pk-card heading="Acme Supply"><p>Balance due 1,240.00</p></pk-card>
  <p data-screen-only>Not printed: use the toolbar to print this page.</p>
</pk-print-page>
```

## `pk-record-page`

**Record page** (Page types). One record shown and edited on a page: the fields read as a pk-field-list, or as a validated pk-form (a pk-field-group per section card) in edit mode, beside sidebar cards (pk-detail-layout, which collapses on a phone). Set load(id) and save(values) as properties (business logic, never config data). load runs when config.id is set and shows the built-in loading, not-found and error (with Retry) states; without an id the page is a new record, in edit mode. save runs only after client validation passes; reject with { errors: { field: message } } to mark fields inline, or any other error for a notice above the form. While edits are unsaved, dirty is true, pk-record-dirty fires, and closing or reloading the tab asks first.

**Props**

| Attribute | Type | Default | Values | Description |
|---|---|---|---|---|
| `config` | json | `{}` |  | { id?, fields, values?, sections?, sidebar?, titleField?, heading?, saveLabel?, editable?, deletable?, label?, emptyHeading? }. deletable shows a Delete button of an existing record, in view and edit mode (pk-record-delete); the record page type sets it from its delete callback. fields are the field defs of pk-field-group, [{ key, label?, kind?, hint?, help?, required?, readonly?, disabled?, span?, options?, when?, ... }] (name and type are older spellings of key and kind): kind is a pk-input type (text, email, number, date...), textarea, select, checkbox, switch, range, combobox or custom (its body in edit mode is your child with slot editor-<key>) (options: strings or { value, label }). In edit mode they are one pk-field-group, so every member it knows works here. values is the starting record of a new one (no id). sections and sidebar are [{ heading, fields: [keys], columns? }], one card each (columns, 2 or more, flows that card's fields into that many columns in edit mode, as pk-field-group columns does; a narrow card shows one) (main column, beside it; fields no section or sidebar card names lead the main column under heading). Sidebar cards are editable in edit mode (same form, one Save); a field they name is edited there only. titleField is a field key whose loaded or saved value is the page title (title is the fallback). cancellable shows Cancel on a new record too. The host may set a beforeSave(values) property (a function, or async): it runs once the form is valid and before save, and returning false stops the save and keeps the page in edit mode with the edits (the record page type takes it as config.beforeSave(values, ctx)). id selects the record to load; none means a new record. editable: false hides Edit. Optional title bar from the shared page shell: title?: string (the page heading; config.heading titles the field list), breadcrumb?: [{ label, href? }], actions?: [{ key, label, href?, variant?, hint? }] (a hint shows as a tooltip on the button) (an action without href fires pk-action). Nothing is drawn when none is set. |
| `mode` | enum | `"view"` | `view` `edit` | view reads the record; edit shows the form with Save and Cancel. Edit and a successful save switch it; a new record is always in edit mode. |
| `deletable` | boolean | `false` |  | Shows the Delete button of an existing record, in view and edit mode; pk-record-delete fires when it is pressed. The same as config.deletable (either one shows it). |
| `cancellable` | boolean | `false` |  | Shows Cancel on a new record too The same as config.cancellable. |
| `sticky` | boolean | `false` |  | Pins the title bar to the top of the scrolling page, with Edit, Cancel, Save and Delete in it, so they stay in reach while a long form scrolls. The same as config.sticky (either one pins it). |
| `wrap-title` (property `wrapTitle`) | boolean | `false` |  | Draws the title bar as the compact record strip (pk-page-header variant record) and lets a long title wrap to at most three lines on a narrow container (a phone) instead of one ellipsized line. The same as config.wrapTitle (either one turns it on). |
| `busy` | boolean | `false` |  | The host is working (a delete, a save it runs itself): Edit, Cancel, Save and Delete are disabled and Save shows its busy state, and a submit is ignored, until it is false again. |
| `error` | string | `""` |  | The host's error text, shown as a notice above the form (for example a failed delete); empty clears it. A rejected save shows its own notice in the same place. |
| `dirty` | boolean | `false` |  | The form differs from the last loaded or saved values. Read it; the element sets it. |

**Slots**

| Slot | Description |
|---|---|
| `badges` | Status chips beside the title. |
| `main-before` | Content placed above the field list or form in the main column (a PkCard section, a banner). Shown in view and edit mode. |
| `main-after` | Content placed below the field list or form in the main column (more PkCard sections). Shown in view and edit mode. |
| `side-after` | Content placed after the sidebar cards. |

**Events**

| Event | Detail | Description |
|---|---|---|
| `pk-action` | `{ key }` | A title-bar action (config.actions entry without an href) was clicked. |
| `pk-record-dirty` | `{ dirty }` | The unsaved-changes state changed. The app framework's record page type uses it to confirm before an in-app leave (link, breadcrumb, back/forward). |
| `pk-record-cancel` | `{ id }` | Cancel was pressed; for a new record (id is null) the host decides where to go. |
| `pk-record-change` | `{ key, value, values }` | A field's change was committed in edit mode (a blur or a choice, not each keystroke): key and value are the field, values the current values of every field of the page, in all its cards. Not raised for a change the host makes. Use it to redraw content beside the form that reads the edits (a summary card in main-after, a preview). |
| `pk-record-save` | `{ values }` | save() resolved; values are the saved record. |
| `pk-record-delete` | `{ id }` | The Delete button (shown for an existing record, in view or edit mode, when config.deletable is set) was pressed. The app framework's record page type asks first, calls its delete(id, ctx) and toasts; a host that uses the element alone handles the event. |

**Methods**

| Method | Description |
|---|---|
| `refresh()` | Loads the record again by config.id through load(id); called when the load callback is set after the page connected (the Blazor wrapper does). |
| `requestSave(skipBeforeSave)` | Saves the form the way the Save button does (validation, busy state, dirty reset, switch to view) and resolves true when it was saved, false when it was invalid, busy, not in edit mode or rejected. skipBeforeSave leaves out the beforeSave callback for this run: a host whose beforeSave declined to ask the person calls it once they confirm. |

**CSS parts**

| Part | Description |
|---|---|
| `header` | The shared title bar: a pk-page-header with the breadcrumb and actions from config. Hidden when config sets none of them. |
| `layout` | The pk-detail-layout holding the main column and the sidebar. |
| `column` | The main column: the main-before slot, main, then the main-after slot. |
| `aside` | The sidebar column: side, then the side-after slot. |
| `main` | The field list or the form. |
| `side` | The sidebar cards. |
| `bar` | Wraps the Edit, Cancel and Save buttons. |
| `edit` | The Edit button (view mode). |
| `cancel` | The Cancel button (edit mode of an existing record, or of a new one when cancellable). |
| `save` | The Save button (edit mode); busy while save() is in flight. |
| `delete` | The Delete button (an existing record, view or edit mode, when config.deletable is true); raises pk-record-delete. |
| `notice` | The save error, above the form. |
| `state` | The loading, not-found or load error state, in place of the record. |

**Writes to host nodes**

| Target | Writes | Why |
|---|---|---|
| the bar part (Edit, Cancel, Save, Delete) in its own shadow tree | `slot` | With sticky, the bar assigns itself to the actions slot of the pk-page-shell it sits in (also in its own shadow tree), so the buttons ride in the pinned header; without sticky the attribute is removed. |

**Accessibility.** Fields are labelled pk-field controls; pk-form moves focus to the first invalid field and lists the problems in a summary, and a server error focuses its field. The loading and error states use pk-skeleton and pk-alert's own roles and announcements.

Example: Product record (a new record from config values; load and save are properties set from script, which an inline script cannot do under this site's CSP)

```html
<pk-record-page id="product" config='{"heading":"Product","title":"Trailhead jacket","fields":[{"key":"name","label":"Name","required":true},{"key":"sku","label":"SKU"},{"key":"status","label":"Status","kind":"select","options":["Draft","Active","Retired"]},{"key":"notes","label":"Notes","kind":"textarea"}],"values":{"name":"Trailhead jacket","sku":"TJ-204","status":"Active","notes":"Waterproof shell."},"sidebar":[{"heading":"Publishing","fields":["status"]}]}'></pk-record-page>
```

## `pk-settings-page`

**Settings page** (Page types). A sectioned settings form with a sticky Save/Discard bar that appears once a field differs from its last saved value. Set the save(values) callback as a property (business logic, never config data) - it may reject, which shows the error inline instead of saving. Your own content for a section goes in a light-DOM child with slot="section-<key>" (key = that section's config key, for example slot="section-general"): it shows inside the section's card after its fields, so the section nav and the save bar cover it. An edit in it (a control raising pk-value-change, pk-change or change, or a pk-field-group's pk-field-change) marks the form changed (Discard does not reset your content); the save callback receives only the config fields. A section with `bare: true` draws no card and no heading: its slot content (and fields) sit directly in the page, so content that draws its own pk-cards is not a card inside a card; the nav item, title bar and save bar still cover it.

**Props**

| Attribute | Type | Default | Description |
|---|---|---|---|
| `config` | json | `{}` | { sections: [{ heading, fields: [{ key, type: 'text'\|'email'\|'number'\|'date'\|'textarea'\|'select'\|'switch'\|'range', label, placeholder?, required?, options?, min?, max?, step?, hint? }] }] }. pk-page-header title bar keys: badges [{ label, variant? }], actionsOverflow, overflowLabel, homeHref, homeLabel, homeIcon. A field's hint (or help) draws an info button beside its label that opens a tooltip; a field without one is unchanged. Each section draws as a pk-card (bare?: true draws none and no heading: the section's fields and slot content sit directly in the page and the app's content draws its own cards; the section still has its nav item and tab, its heading is the label, and the save bar covers it); a select's options are [{ value, label? }] or plain strings; styled?: true on a select draws the themed list, the combobox in select mode, as pk-field-group does; type 'combobox' takes mode?: 'select' \| 'autocomplete'. Section nav: nav?: true \| false \| 'auto' (default 'auto': a list of the sections beside the cards from three sections up; false keeps the plain stack, true forces it at one or two), and each section's key? (its anchor, default a slug of its heading). The list is sticky and marks the section in view with aria-current; choosing one scrolls its card to the top and moves focus onto it; on a phone the list is replaced by pk-detail-layout's tab strip, one section at a time. Optional title bar from the shared page shell: heading?: string, breadcrumb?: [{ label, href? }], actions?: [{ key, label, href?, variant?, hint? }] (a hint shows as a tooltip on the button) (an action without href fires pk-action). Nothing is drawn when none is set. |
| `values` | json | `{}` | The current values, keyed by field key (what save() receives and discard() restores to). Set again once loaded values arrive (e.g. from ctx.store) to refresh the fields and take a new baseline. |

**Slots**

| Slot | Description |
|---|---|
| `badges` | Status chips beside the title. |

**Events**

| Event | Detail | Description |
|---|---|---|
| `pk-action` | `{ key }` | A title-bar action (config.actions entry without an href) was clicked. |

**CSS parts**

| Part | Description |
|---|---|
| `header` | The shared title bar: a pk-page-header with the breadcrumb and actions from config. Hidden when config sets none of heading, breadcrumb or actions. |
| `saved` | A success message shown after save(), hidden again once a field changes. |
| `empty` | A placeholder message shown in place of the sections while config.sections is empty. |
| `form` | Wraps the sections and the save bar; submitting it (click Save or Enter) saves. |
| `body` | The grid of the section nav and the cards; carries data-nav while the nav is on. |
| `nav` | The sticky section nav (a nav landmark of pk-nav-item rows); hidden while the nav is off, and on a phone. |
| `layout` | The pk-detail-layout around the cards: on a phone it draws the tab strip of sections. |
| `sections` | Holds one pk-card per config.sections entry. |
| `bar` | The sticky Save/Discard bar; hidden while nothing has changed. |
| `status` | The bar's own message: "Unsaved changes", or a save error. |
| `save` | The Save button. |
| `discard` | The Discard button; restores the last saved values. |

**CSS custom properties**

| Property | Default | Description |
|---|---|---|
| `--pk-settings-page-nav-width` |  | Width of the section nav column beside the cards (default 12rem). |

**Accessibility.** Every field is a labelled real control (pk-input/pk-textarea/pk-select showLabel on, pk-switch's own slotted label, pk-range wrapped in a labelled pk-field); the bar's status uses pk-alert's own role and announcement.

Example: Store settings (set config, values and save from script: an inline script cannot run under this site's CSP)

```html
<pk-settings-page id="store-settings"></pk-settings-page>
```

## `pk-sortable`

**Sortable** (Layout). A pointer, touch and keyboard reorder list. Children are pk-sortable-item: drag a row's handle, or focus a row and press Alt+Up or Alt+Down. Never reorders its own children (the host applies the order from pk-reorder); it can also accept a row dragged in from outside it, for a palette-to-canvas drop.

**Props**

| Attribute | Type | Default | Values | Description |
|---|---|---|---|---|
| `label` | string | `""` |  | Accessible name for the list (aria-label). |
| `orientation` | enum | `"vertical"` | `vertical` `horizontal` | The axis rows lay out and reorder along; Alt+Up/Alt+Down always means earlier/later in the list, whichever axis that is. |
| `disabled` | boolean | `false` |  | No row can be grabbed, dropped on or moved by the keyboard. |
| `accept-external` (property `acceptExternal`) | boolean | `false` |  | Lets a source outside this list drop a new row in, through beginExternalDrag / externalDragOver / endExternalDrag. Off by default, so a plain reorder list never becomes an accidental drop target. |
| `dragging` | boolean | `false` |  | True while a row of this list, or an external drag, is being dragged over it. A styling hook the element sets itself. |

**Slots**

| Slot | Description |
|---|---|
| (default) | pk-sortable-item children, in the current order. |

**Events**

| Event | Detail | Description |
|---|---|---|
| `pk-reorder` | `{ order, item, from, to, external, payload? }` | A reorder was committed: a drag ended over a new position, or Alt+Up/Alt+Down moved the focused row. order is the new list of every row's value, in order (the host applies it: pk-sortable never moves its own children); item, from and to describe the row that moved. For a drop accepted from outside the list (external: true, through endExternalDrag) order, item and from are null, to is the index to insert at, and payload is whatever beginExternalDrag(payload) was called with. Not cancelable: the gesture already happened. |

**Methods**

| Method | Description |
|---|---|
| `moveOrder(order, from, to)` | The new order with one item relocated; pure. |
| `keyMove(key, index, length)` | The index Alt+Up or Alt+Down asks for. |
| `dropIndex(mids, pos)` | The insertion index a pointer position means. |
| `beginExternalDrag(payload)` | Called by a source outside the list, on its own pointerdown, to start tracking a drag it owns. |
| `externalDragOver(x, y)` | Called on that source's pointermove; updates the drop-indicator and returns whether the point is a valid drop (accept-external must be set). |
| `endExternalDrag(commit)` | Called on that source's pointerup (commit) or pointercancel (commit: false); raises pk-reorder and returns the index, or null when nothing was accepted. |

**CSS parts**

| Part | Description |
|---|---|
| `announcer` | The visually-hidden aria-live region that announces a reorder or a drop for a screen reader. |

**CSS custom properties**

| Property | Default | Description |
|---|---|---|
| `--pk-sortable-gap` | `var(--space-1)` | Gap between rows. |

**Writes to host nodes**

| Target | Writes | Why |
|---|---|---|
| the pk-sortable-item children | `dragging`, `drop-indicator`, `tabindex` | dragging marks the row being dragged; drop-indicator marks the neighbour to draw the insertion line against (cleared when the drag ends); tabindex is the roving focus stop. Refreshed on every drag step and every update; none is undone on disconnect (tabindex stays wherever the list last put it, like pk-tree and pk-tabs). |

**Accessibility.** role=list; each pk-sortable-item is role=listitem with aria-roledescription and aria-disabled. One tab stop (roving tabindex, skipping disabled rows); Up, Down, Home and End move it; Alt+Up and Alt+Down move the focused row and keep focus on it (a disabled row cannot be focused or moved). A visually-hidden aria-live=polite region announces a grab, a completed or cancelled reorder, and an accepted external drop.

Example: Reorder a checklist

```html
<pk-sortable label="Steps">
  <pk-sortable-item value="mix">Mix</pk-sortable-item>
  <pk-sortable-item value="bake">Bake</pk-sortable-item>
  <pk-sortable-item value="cool">Cool</pk-sortable-item>
  <pk-sortable-item value="ice">Ice</pk-sortable-item>
</pk-sortable>
```

## `pk-sortable-item`

**Sortable item** (Layout). One row of a pk-sortable: drag its handle (pointer or touch) to reorder, or focus the row and press Alt+Up/Alt+Down. Place it directly inside a pk-sortable.

**Props**

| Attribute | Type | Default | Values | Description |
|---|---|---|---|---|
| `value` | string | `""` |  | The identifier pk-sortable reports for this row in a pk-reorder event's order and item. |
| `disabled` | boolean | `false` |  | Cannot be grabbed, dropped on, or moved by the keyboard. |
| `dragging` | boolean | `false` |  | Set by the surrounding pk-sortable while this row is the one being dragged. |
| `drop-indicator` (property `dropIndicator`) | enum | `"none"` | `none` `before` `after` | Set by the surrounding pk-sortable to draw the insertion line above or below this row while another row (or an external drag) hovers next to it. |

**Slots**

| Slot | Description |
|---|---|
| (default) | The row's content. |

**Events**

| Event | Detail | Description |
|---|---|---|
| `pk-sortable-grab` | `{ x, y }` | The handle was pressed (pointerdown); consumed by the parent pk-sortable to start a drag. Not cancelable. |
| `pk-sortable-drag` | `{ x, y }` | The handle is moving while it holds pointer capture; consumed by the parent pk-sortable to update the drop position. Not cancelable. |
| `pk-sortable-drop` | `{ cancelled }` | The handle released the pointer (or the drag was cancelled); consumed by the parent pk-sortable to finish or abandon the drag. Not cancelable. |

**CSS parts**

| Part | Description |
|---|---|
| `root` | The row: the handle and the content, side by side. |
| `indicator` | The drop-indicator line, shown above or below the row by dropIndicator. |
| `handle` | The grab handle: a 44px touch target on a touch screen, decorative to assistive technology (the row itself carries the semantics). |
| `grip` | The handle's icon. |
| `content` | The row's slotted content. |

**CSS custom properties**

| Property | Default | Description |
|---|---|---|
| `--pk-sortable-item-indicator` | `var(--color-accent)` | Colour of the drop-indicator line. |

**Accessibility.** role=listitem with aria-roledescription="Draggable item" and aria-disabled through ElementInternals. The handle is aria-hidden (decorative, pointer and touch only); the row itself is the tab stop pk-sortable manages (roving tabindex) and the one that responds to Alt+Up and Alt+Down.

Example: Inside pk-sortable

```html
<pk-sortable label="Steps">
  <pk-sortable-item value="a">Mix</pk-sortable-item>
  <pk-sortable-item value="b">Bake</pk-sortable-item>
  <pk-sortable-item value="c">Cool</pk-sortable-item>
</pk-sortable>
```

## `pk-splitter`

**Splitter** (Layout). Two panes with a draggable separator between them: resize by pointer or arrow keys, side by side or stacked, with a minimum and maximum size. Sizes are percentages of the room the panes share.

It is delegates focus to its inner control.

**Props**

| Attribute | Type | Default | Values | Description |
|---|---|---|---|---|
| `orientation` | enum | `"horizontal"` | `horizontal` `vertical` | How the panes lie: horizontal puts them side by side (the separator is a vertical bar), vertical stacks them (the separator is a horizontal bar and it is 16rem tall unless --pk-splitter-height says otherwise). |
| `size` | number | `50` |  | The start pane's share of the room, in percent, kept between min and max. While the user drags or presses a key the element owns it; after pk-resize the host does. |
| `min` | number | `10` |  | Smallest size of the start pane, in percent (aria-valuemin). |
| `max` | number | `90` |  | Largest size of the start pane, in percent (aria-valuemax). |
| `step` | number | `2` |  | Percent moved by one arrow key press. |
| `label` | string | `"Resize panes"` |  | The separator's accessible name (aria-label). |
| `disabled` | boolean | `false` |  | Fixes the size: the separator no longer takes focus or moves. |
| `collapsible` | boolean | `false` |  | Shows a toggle on the separator that collapses the end pane (collapsed at first on a phone). |
| `collapsed` | boolean | `false` |  | The end pane is collapsed (inert, size kept). |
| `toggle-label` (property `toggleLabel`) | string | `""` |  | The toggle's name. |
| `persist-key` (property `persistKey`) | string | `""` |  | Saves size and collapsed in local storage under this key and restores them, unless size or collapsed is set. |

**Slots**

| Slot | Description |
|---|---|
| `start` | The first pane (left, or top when stacked); its size is size percent. |
| `end` | The second pane, which takes the rest. |

**Events**

| Event | Detail | Description |
|---|---|---|
| `input` | `native Event` | The size is changing while a drag is in progress (native, composed). Read the size from the element. |
| `pk-resize` | `{ size: number }` | The user committed a new size: a drag ended, or an arrow, Home or End key moved it. Raised once per key press and once per drag, never for a size the host set; not cancelable. |
| `pk-collapse-change` | `{ collapsed: boolean }` | The user collapsed or restored the end pane. |

**CSS parts**

| Part | Description |
|---|---|
| `root` | The grid that lays out the two panes and the separator. |
| `start` | The first pane wrapper (it scrolls on its own). |
| `handle` | The separator: a role=separator control with its own focus ring and a 44px hit area on a touch screen. |
| `toggle` | The collapse button. |
| `end` | The second pane wrapper (it scrolls on its own). |

**CSS custom properties**

| Property | Default | Description |
|---|---|---|
| `--pk-splitter-handle` | `var(--space-2)` | Thickness of the separator. |
| `--pk-splitter-height` | `auto, or 16rem when stacked` | Height of the splitter (the panes scroll inside it). |
| `--pk-splitter-line` | `var(--color-border)` | Colour of the separator line at rest. |

**Accessibility.** The separator is role=separator, focusable, with aria-orientation, aria-valuenow, aria-valuemin, aria-valuemax and aria-controls (the start pane): the window splitter pattern. Arrow keys along the axis move it by step (Left and Right side by side, mirrored in a right-to-left page; Up and Down stacked), Home and End go to the minimum and maximum. A pointer drag is captured by the separator, so it follows the pointer out of the element and ends cleanly on release or cancel. The line grows and takes the accent colour on hover, drag and keyboard focus, so state is never colour alone.

Example: Side by side

```html
<pk-splitter size="30" min="15" max="70" collapsible persist-key="gallery-splitter" toggle-label="Detail panel" label="Resize the list"><div slot="start" class="stack"><strong>List</strong><span>Drag the bar, or focus it and use the arrow keys.</span></div><div slot="end" class="stack"><strong>Detail</strong><span>Home and End go to the minimum and maximum.</span></div></pk-splitter>
```

## `pk-stack`

**Stack** (Layout). A vertical stack of children with an even gap: the layout primitive for form sections, card bodies and page columns.

**Props**

| Attribute | Type | Default | Values | Description |
|---|---|---|---|---|
| `gap` | enum | `"md"` | `none` `xs` `sm` `md` `lg` `xl` | Space between children, on the spacing scale (none 0, xs --space-1, sm --space-2, md --space-4, lg --space-6, xl --space-8). Default md (the flow space). |
| `align` | enum | `"stretch"` | `stretch` `start` `center` `end` | Cross-axis alignment: stretch fills the width, the others size each child to its content. |
| `dividers` | boolean | `false` |  | Draw a rule between children. |
| `fill` | boolean | `false` |  | Fills a flex or grid parent with a definite height (a workspace pane, an app shell body): the stack takes the full height instead of sizing to its content, so a flex child declared to grow (a table with sticky-header, for example) can own the scrolling instead of the page. |

**Slots**

| Slot | Description |
|---|---|
| (default) | The items to lay out. |

**CSS custom properties**

| Property | Default | Description |
|---|---|---|
| `--pk-stack-gap` | `the gap token` | Overrides the gap prop with any length. |
| `--pk-stack-divider` | `var(--color-border)` | Colour of the dividers. |

**Accessibility.** A plain layout box with no role; the rules from dividers are decorative borders, not separators, so add pk-divider where a separator should be announced.

Example: Even rhythm

```html
<pk-stack gap="md"><p>First paragraph.</p><p>Second paragraph.</p><p>Third paragraph.</p></pk-stack>
```

## `pk-states-page`

**States page** (Page types). The loading, empty, error and forbidden content a page shows in place of its own, one at a time: the page-type building block for "nothing to show yet".

**Props**

| Attribute | Type | Default | Values | Description |
|---|---|---|---|---|
| `state` | enum | `"ready"` | `ready` `loading` `empty` `error` `forbidden` | Which content shows. ready reveals the slotted content and hides the rest; every other value hides the slotted content and draws the matching built-in state (a labelled skeleton for loading, pk-empty-state for empty and forbidden, a danger pk-alert for error). |
| `heading` | string | `""` |  | Heading for empty, error or forbidden. A sensible default is used when empty ("Nothing here yet", "Something went wrong", "Not allowed"). |
| `description` | string | `""` |  | Explanation for empty, error or forbidden. |
| `label` | string | `""` |  | Accessible label for the loading skeleton (defaults to "Loading"). |

**Slots**

| Slot | Description |
|---|---|
| `title` | The page title: a level 1 pk-heading the built-in page factory (mountTitled) appends, so the app can focus it after navigation. When present, the built-in state heading is not drawn a second time. |
| (default) | The page's own content, shown only in the ready state. |
| `drawn` | Internal: the drawn loading/empty/error/forbidden content. A light-DOM child this element creates and self-assigns here (never a shadow-root child), so the SDK's on-demand loader can see and upgrade it. |

**Events**

| Event | Detail | Description |
|---|---|---|
| `pk-retry` | `none` | The error state's Retry action was activated. The host decides what happens (reload the data, set state back to loading); this element never retries by itself. |

**CSS parts**

| Part | Description |
|---|---|
| `content` | Wraps the default slot; hidden outside the ready state. |

**Writes to host nodes**

| Target | Writes | Why |
|---|---|---|
| the drawn child | `slot` | A light-DOM element this element creates and owns itself (never a host-given node), self-assigned to its own drawn slot so the SDK's on-demand loader can see and upgrade it (a shadow-root child would be invisible to it). |

**Accessibility.** The error alert and empty/forbidden states use pk-alert and pk-empty-state's own roles and announcements; nothing here is re-implemented.

Example: Loading, then the real content

```html
<pk-states-page state="loading" label="Loading orders">
  <p>The order list goes here once it loads.</p>
</pk-states-page>
```

## `pk-text`

**Text** (Layout). A themed paragraph, or a run of text inside a line, on the type scale and colour tokens: the text counterpart of pk-stack and pk-cluster. A variant gives it a heading, lead or eyebrow look; real headings stay native h1 to h6.

**Props**

| Attribute | Type | Default | Values | Description |
|---|---|---|---|---|
| `inline` | boolean | `false` |  | A run of text inside a line (like a span) instead of a paragraph block. |
| `variant` | enum | `"normal"` | `normal` `h1` `h2` `h3` `h4` `h5` `h6` `lead` `eyebrow` | The type style: normal (the text around it), a heading look h1 to h6 on the --text-h1 to --text-h6 scale, lead (larger, muted intro text) or eyebrow (small uppercase label). A look only: an h1 to h6 variant is not a heading to assistive technology; use a native h1 to h6 for a real heading. size, tone and weight override the variant. |
| `font` | enum | `"inherit"` | `inherit` `sans` `mono` | Font family: sans (--font-sans) or mono (--font-mono, with tabular figures for ids and amounts). Default inherit. |
| `size` | enum | `"inherit"` | `inherit` `meta` `read` `lg` | Font size on the type scale (meta --text-meta, read --text-read, lg --text-lg). Default inherit: the size around it. |
| `tone` | enum | `"inherit"` | `inherit` `default` `muted` `accent` `positive` `warning` `critical` | Text colour from the theme: default is the body text colour (to undo a muted variant), the rest are the status colours. Default inherit: the colour around it. Never the only signal: say it in the words too. |
| `weight` | enum | `"inherit"` | `inherit` `regular` `medium` `semibold` `bold` | Font weight. Default inherit. |
| `white-space` (property `whiteSpace`) | enum | `"inherit"` | `inherit` `normal` `nowrap` `pre-wrap` | How line breaks and spaces are treated: pre-wrap keeps the text's own line breaks and runs of spaces (notes, raw values), nowrap stays on one line. Default inherit. |
| `truncate` | boolean | `false` |  | Keep it to one line and end with an ellipsis when it does not fit. |

**Slots**

| Slot | Description |
|---|---|
| (default) | The text: phrasing content only (text, links, code, badges). Several paragraphs are several pk-text in a pk-stack. |

**Accessibility.** A block is exposed with the paragraph role; an inline run has no role, like a span. The h1 to h6 variants only change the look and add no heading role or level: a real heading, one that belongs in the page outline and that a screen reader lists, stays a native h1 to h6, which the base styles size on the same --text-h1 to --text-h6 scale. Use a heading variant for text that should look like one without being one (a figure label, a number in a card). A truncated text keeps its full content in the accessibility tree; put the full value in a tooltip or a detail view for sighted readers.

Example: Paragraph

```html
<pk-text>Orders ship within two working days. Anything placed after 3 pm counts as the next day.</pk-text><pk-text size="meta" tone="muted">Last updated 2 June.</pk-text>
```

## `pk-tool-page`

**Tool page** (Page types). A page that takes input, runs one action and shows the outcome: input fields, a Run button, and a stat, table, code block or line of text once it finishes. Set the run(values) callback as a property (business logic, never config data) - it may reject, which shows the built-in error state with Retry.

**Props**

| Attribute | Type | Default | Description |
|---|---|---|---|
| `config` | json | `{}` | Optional title bar from the shared page shell: heading?, breadcrumb?: [{ label, href? }], actions?: [{ key, label, href?, variant?, hint? }] (an action without href fires pk-action), and the title bar keys of pk-page-header: badges [{ label, variant? }], actionsOverflow, overflowLabel, homeHref, homeLabel, homeIcon; nothing is drawn when none is set. { input: [{ key, type: 'text'\|'email'\|'number'\|'date'\|'textarea'\|'select', label, placeholder?, required?, options?, hint? }], outcome: 'stat'\|'table'\|'code'\|'text' }. input builds one labelled control per field (a hint, or help, adds an info button beside the label that opens a tooltip) (a select's options are [{ value, label? }] or plain strings; styled?: true on a select draws the themed list, the combobox in select mode, as pk-field-group does; type 'combobox' takes mode?: 'select' \| 'autocomplete'); outcome picks what the run() result draws as: stat (Object.assign onto pk-stat: value, label, ...), table ({ columns, rows } onto pk-table), code (a string, or { code } onto pk-code-block), text (a string, or { text }). |
| `run-label` (property `runLabel`) | string | `""` | Text of the Run button. Defaults to "Run". |

**Slots**

| Slot | Description |
|---|---|
| (default) | The app's own content. When present it replaces the generated input fields, Run button and outcome (config.input, config.outcome and run are then unused). |
| `title` | The page title: a level 1 pk-heading the built-in page factory (mountTitled) appends from config.heading, so the app can focus it after navigation. |
| `badges` | Status chips beside the title. |
| `actions` | Title bar buttons. |

**Events**

| Event | Detail | Description |
|---|---|---|
| `pk-action` | `{ key }` | A title-bar action (config.actions entry without an href) was clicked. |

**CSS parts**

| Part | Description |
|---|---|
| `header` | The title bar (pk-page-shell): heading, breadcrumb, actions and badges from config; the form sits under it. |
| `custom` | The default slot: the app's own content, which replaces the form and outcome when present. |
| `form` | Wraps the fields and the Run button; submitting it (click or Enter) runs the action. |
| `fields` | Holds one control per config.input entry. |
| `run` | The Run button. |
| `outcome` | Holds the loading/error state while run() is in flight, then the drawn outcome. |

**Accessibility.** Every field is a labelled real control (pk-input/pk-textarea/pk-select, showLabel on); the loading and error states use pk-skeleton and pk-alert's own roles and announcements.

Example: Word count (set config and run from script: an inline script cannot run under this site's CSP, so wire it the way pk-command-palette's items are wired)

```html
<pk-tool-page id="word-count" run-label="Count"></pk-tool-page>
```

## `pk-toolbar`

**Toolbar** (Layout & structure). A row with a lead (title and note) on the left and actions on the right. It wraps by its own width and stacks on a phone, with touch-sized actions.

**Props**

| Attribute | Type | Default | Description |
|---|---|---|---|
| `heading` | string | `""` | The lead title, in strong text. |
| `note` | string | `""` | A muted note beside the title, such as a count. |

**Slots**

| Slot | Description |
|---|---|
| (default) | More lead content after the title and note (filters, chips). |
| `actions` | Buttons at the right; hidden when empty, full width and touch-sized on a narrow container. |

**CSS parts**

| Part | Description |
|---|---|
| `toolbar` | The row. |
| `lead` | The title, note and default slot. |
| `title` | The title. |
| `note` | The note. |
| `actions` | The actions wrapper. |

**CSS custom properties**

| Property | Default | Description |
|---|---|---|
| `--pk-toolbar-margin` | `var(--space-1) 0 var(--space-3) 0` | Outer margin of the row. |

**Accessibility.** A plain group of controls, not an ARIA toolbar (which would need arrow-key roving): every action is a normal tab stop in reading order. The title is text, not a heading; use pk-page-header when the outline needs one.

Example: Title, note and actions

```html
<pk-toolbar heading="Lines" note="12 lines on this order">
  <pk-button slot="actions" size="mini" variant="ghost">Export</pk-button>
  <pk-button slot="actions" size="mini">Add line</pk-button>
</pk-toolbar>
```

## `pk-wizard-page`

**Wizard page** (Page types). A multi-step flow on a page: a pk-stepper for progress, one step's fields at a time as a validated pk-form (or your own content through mountStep), and Back, Next and Submit. Next is blocked until the step's fields are valid and validate(stepId, values) passes; what was typed is kept when you go back; focus moves to the step heading on every step change. An optional Review step lists every answer before Submit. Set validate, submit, load and mountStep as properties (business logic, never config data). Reject or return { errors: { field: message } } to mark fields inline (a submit may name a field of an earlier step: the wizard goes back to it), or any other error for a notice. While answers are unsent, dirty is true, pk-wizard-dirty fires, and closing or reloading the tab asks first.

**Props**

| Attribute | Type | Default | Description |
|---|---|---|---|
| `config` | json | `{}` | { steps, review?, reviewLabel?, submitLabel?, doneHeading?, doneDescription?, label? }. steps is [{ id, label, description?, fields? }]: fields is [{ name, label?, type?, required?, help?, options? }] (type is a pk-input type, 'textarea' or 'select'); a step without fields is drawn by mountStep. review: true adds a last step that lists all answers. Optional title bar from the shared page shell: heading?: string, breadcrumb?: [{ label, href? }], actions?: [{ key, label, href?, variant?, hint? }] (a hint shows as a tooltip on the button) (an action without href fires pk-action). Nothing is drawn when none is set. |
| `dirty` | boolean | `false` | Answers were typed and not yet submitted. Read it; the element sets it. |

**Slots**

| Slot | Description |
|---|---|
| `badges` | Status chips beside the title. |
| `step-<id>` | The step with that id (no fields, no mountStep). The app owns it; its controls are not collected or validated. |

**Events**

| Event | Detail | Description |
|---|---|---|
| `pk-action` | `{ key }` | A title-bar action (config.actions entry without an href) was clicked. |
| `pk-wizard-dirty` | `{ dirty }` | The unsent-answers state changed. An app that owns its router uses it to confirm before leaving. |
| `pk-wizard-submit` | `{ values }` | submit() resolved; values are all the answers. |

**Methods**

| Method | Description |
|---|---|
| `refresh()` | Runs load() again and restarts at the first step; called when the load callback is set after the wizard connected (the Blazor wrapper does). |

**CSS parts**

| Part | Description |
|---|---|
| `header` | The shared title bar: a pk-page-header with the breadcrumb and actions from config. Hidden when config sets none of them. |
| `stepper` | The pk-stepper showing progress; a reached step can be clicked to go back. |
| `card` | The pk-card holding the heading, the step and the buttons. |
| `heading` | The current step's heading; it receives focus on every step change. |
| `panes` | One pane per visited step; the others are hidden, not removed. |
| `bar` | Wraps Back and Next or Submit. |
| `back` | The Back button. |
| `next` | The Next button (Submit on the last step); busy while validate() or submit() runs. |
| `notice` | The step or submit error, above the card. |
| `state` | The loading or load error state before the first step, and the done message after Submit. |

**Writes to host nodes**

| Target | Writes | Why |
|---|---|---|
| the pk-field elements the step forms build in this element's own shadow tree | `error` | A validate or submit that rejects with { errors } sets each named field's error, and the next attempt clears them. The fields are rebuilt with the wizard, so nothing outlives the element. |
| the step panes this element draws in its own shadow tree | `hidden` | Each visited step keeps its pane (so answers survive Back); only the current one is shown. The panes are created by this element and removed with it. |

**Accessibility.** Fields are labelled pk-field controls; pk-form moves focus to the first invalid field and lists the problems in a summary, and a server error focuses its field. Focus moves to the step heading (an h2) on every step change so a keyboard or screen reader user starts at the new step; Enter in a field is Next. pk-stepper announces 'Step n of m' with its state.

Example: Sign-up in three steps with a review (set validate and submit from script: an inline script cannot run under this site's CSP)

```html
<pk-wizard-page id="wizard-example" config='{"review":true,"steps":[{"id":"account","label":"Account","fields":[{"name":"email","label":"Email","type":"email","required":true}]},{"id":"plan","label":"Plan","fields":[{"name":"plan","label":"Plan","type":"select","options":["Free","Team"]}]}]}'></pk-wizard-page>
```

## `pk-workspace`

**Workspace** (Layout & structure). A tool page in panes: a nav pane, a main pane and an optional docked aside, each scrolling on its own. On a phone one pane shows at a time and a tab strip (Contents, Content, Details) switches it.

**Props**

| Attribute | Type | Default | Values | Description |
|---|---|---|---|---|
| `fill` | boolean | `false` |  | Fill the height the parent gives it (a parent with a definite height, or a flex or grid parent) and drop the frame, for a tool that owns the whole page. Without it the workspace is as tall as the viewport minus a reserve. |
| `active-pane` (property `activePane`) | enum | `"main"` | `nav` `main` `aside` | The pane shown on a phone. On a wider screen every pane shows. A pane that does not exist falls back to the main pane. A tab tap raises a cancelable pk-pane-change first and the element changes the pane itself only when it is not cancelled; a host that owns the pane from a route calls preventDefault() and changes the route instead (see the routed-list-detail template). |
| `aside-open` (property `asideOpen`) | boolean | `false` |  | Show the aside: a docked column on a wide screen, a panel over the main pane on a tablet and its own pane on a phone. |
| `nav-label` (property `navLabel`) | string | `"Contents"` |  | The nav pane's accessible name and its tab on a phone. |
| `nav-width` (property `navWidth`) | string | `""` |  | Width of the nav column on a wide screen: sm, md, lg or xl (12, 17.5, 24 or 32rem), or a CSS length. Empty keeps the default (the --pk-workspace-nav-w hook). On a phone the panes show one at a time, so it has no effect there. |
| `main-label` (property `mainLabel`) | string | `"Content"` |  | The main pane's accessible name and its tab on a phone. |
| `aside-label` (property `asideLabel`) | string | `"Details"` |  | The aside's accessible name and its tab on a phone. |

**Slots**

| Slot | Description |
|---|---|
| `nav` | The nav pane: a file tree, a list of sections, filters. Leave it out for a two-pane workspace. |
| (default) | The main pane. |
| `aside` | The docked aside (an inspector or a details panel). It shows when asideOpen is set. |

**Events**

| Event | Detail | Description |
|---|---|---|
| `pk-pane-change` | `{ pane: string, previous: string }` | A tab in the phone strip was chosen. Cancelable: preventDefault keeps the previous pane. |

**CSS parts**

| Part | Description |
|---|---|
| `root` | The frame that lays the panes out. |
| `strip` | The phone tab strip. |
| `tab` | One tab in the strip. |
| `nav` | The nav pane. |
| `main` | The main pane. |
| `aside` | The aside pane. |

**CSS custom properties**

| Property | Default | Description |
|---|---|---|
| `--pk-workspace-height` | `calc(100dvh - 20rem)` | Height when fill is not set. |
| `--pk-workspace-nav-w` | `17.5rem` | Width of the nav column on a wide screen. |
| `--pk-workspace-aside-w` | `var(--inspector-w)` | Width of the aside. |
| `--pk-workspace-pad` | `var(--pad-panel)` | Padding inside each pane (the default of the three below). |
| `--pk-workspace-nav-pad` | `var(--pk-workspace-pad)` | Padding inside the nav pane; 0 for a pane that fills itself. |
| `--pk-workspace-main-pad` | `var(--pk-workspace-pad)` | Padding inside the main pane. |
| `--pk-workspace-aside-pad` | `var(--pk-workspace-pad)` | Padding inside the aside pane. |

**Accessibility.** Each pane is a labelled region (the aside a complementary landmark) and scrolls on its own, so it is reachable by keyboard. On a phone the strip is a tablist with a roving tabindex: Left and Right (or Up and Down), Home and End move between tabs and select them; each pane becomes a tabpanel labelled by its tab. Tabs are 44px high. Hidden panes are display:none, so they are out of the tab order and the accessibility tree.

Example: Nav and main

```html
<pk-workspace nav-label="Sections">
  <div slot="nav" class="stack"><a href="#overview">Overview</a><a href="#channels">Channels</a><a href="#integrations">Integrations</a></div>
  <div class="stack"><h2>Overview</h2><p>The main pane scrolls on its own. Below 640px only the nav or this pane shows, switched by the strip above.</p></div>
</pk-workspace>
```

## `pk-workspace-page`

**Workspace page** (Page types). A page of side-by-side panes (a nav, a main pane and an optional aside) that each scroll on their own, built on pk-workspace: on a phone one pane shows at a time behind a tab strip. Set the mount(panes) callback as a property (business logic, never config data): it receives { nav, main, aside } (the panes that exist) and returns a cleanup function or a { destroy() } handle, which the element calls when it leaves the page. While mount() is pending the built-in loading state shows, and a rejection shows the error state with Retry.

**Props**

| Attribute | Type | Default | Description |
|---|---|---|---|
| `config` | json | `{}` | { panes?, navLabel?, mainLabel?, asideLabel?, asideOpen?, label? }. panes lists the optional panes to create: any of 'nav' and 'aside' (default ['nav']; the main pane always exists). navLabel, mainLabel and asideLabel are the panes' accessible names and phone tabs (pk-workspace's own defaults otherwise). asideOpen is true unless set to false. label names the loading state. Optional title bar from the shared page shell: heading?: string, breadcrumb?: [{ label, href? }], actions?: [{ key, label, href?, variant?, hint? }] (a hint shows as a tooltip on the button) (an action without href fires pk-action). Nothing is drawn when none is set. |
| `fill` | boolean | `false` | Take all the room a flex column parent leaves (the panes then scroll instead of the page). Without it the page has pk-workspace's default height. |

**Slots**

| Slot | Description |
|---|---|
| `badges` | Status chips beside the title. |
| (default) | The main pane (mount() not set). |
| `nav` | The nav pane (mount() not set). |
| `aside` | The aside pane (mount() not set; config.panes lists it). |

**Events**

| Event | Detail | Description |
|---|---|---|
| `pk-action` | `{ key }` | A title-bar action (config.actions entry without an href) was clicked. |

**CSS parts**

| Part | Description |
|---|---|
| `header` | The shared title bar: a pk-page-header with the breadcrumb and actions from config. Hidden when config sets none of them. |
| `frame` | Positions the state overlay so it covers the panes but never the title bar. |
| `workspace` | The pk-workspace that lays out the panes. |
| `nav` | The nav pane's content; mount() draws into it. |
| `main` | The main pane's content; mount() draws into it. |
| `aside` | The aside pane's content; mount() draws into it. |
| `state` | Covers the panes with the loading or error state while mount() is in flight or failed. |

**Accessibility.** pk-workspace names each pane (region, complementary, or a tab panel on a phone, with arrow-key navigation on the tab strip); the loading and error states use pk-skeleton and pk-alert's own roles and announcements.

Example: Files workspace (set config and mount from script: an inline script cannot run under this site's CSP, so wire it the way pk-tool-page's run is wired)

```html
<pk-workspace-page id="files"></pk-workspace-page>
```
