diff --git a/.specs/frame-box.md b/.specs/frame-box.md
new file mode 100644
index 000000000..204d3a3f2
--- /dev/null
+++ b/.specs/frame-box.md
@@ -0,0 +1,150 @@
+---
+name: frame-box
+category: layout
+structure: monolithic
+status: approved
+spec_version: 1
+checksum: 05c784a5f4f1bc876b5bbe5794d8946d7dc8e50b5842a37c64b0019d67ceed3a
+style_seam: true
+created: 2026-08-11
+last_updated: 2026-08-12
+---
+
+# Frame Box — Component Spec
+
+## Purpose
+
+The registration frame: a hairline box with a small square set inside each corner and an optional linear hatch texture behind its content. Every side and every corner is addressable on its own, so frames stack and abut without doubling a rule or a mark. It is the container every framed page section is built from — a section title, the gap between two sections, a cell of a divider grid — so a page's rules and corner marks come from one component instead of from per-screen borders.
+
+## When to use
+
+- Wrapping a page section, band, or grid cell that should read as a framed module.
+- Building a page whose vertical rhythm is drawn by rules rather than by margins: stack frames and let `flush` collapse each junction to a single hairline.
+- Adding the corner registration marks (and optionally the hatch texture) to an area that already has its rules drawn by a surrounding grid — set `borders="none"` and keep `marks`.
+
+## When NOT to use
+
+- For a card-shaped surface with padding, a header and a footer → use `card-box` instead.
+- For a single separating line between two blocks → use `divider` instead.
+- For the empty framed band that separates two sections → use `section-gap`, which is this frame configured for that one job.
+
+## Related
+
+- `section-gap` — an empty `frame-box` that holds the air between two sections.
+- `section-title` — a `frame-box` around a centered section header.
+- `card-box` — a padded surface with header/content/footer regions; a card, not a frame.
+- `divider` — one hairline with optional centered label; no corners, no content.
+
+## Best practices
+
+- Stack or abut frames with `flush` so a shared edge reads as one hairline instead of two. It subtracts the named sides from this frame's own set, so it works vertically (`flush`, i.e. `top`) and horizontally (`flush="left"`) with the same mechanic.
+- Use `borders` rather than overriding the border utilities from the call site: `y` for a full-bleed band that keeps only its top and bottom rules, `none` for a cell of a `gap-px` divider grid whose edges are already drawn by the grid's seams.
+- Keep `marks` on: the corner squares are the frame's identity, and they cost nothing when the rules are handed over to a grid. On a shared edge, pair `flush` with the opposite `marks` keyword (`flush` + `marks="bottom"`, `flush="left"` + `marks="right"`) so the junction carries one rule and one mark per corner.
+- Reach for `hatch` sparingly — one hatched frame per view reads as texture; several read as noise.
+
+## Usage
+
+```vue
+
+
+
+
+ Framed content
+
+
+```
+
+## Props
+
+| Prop | Type | Default | Required | JSDoc |
+|---|---|---|---|---|
+| `borders` | `'all' \| 'none' \| 'x' \| 'y' \| Side \| Side[]` | `'all'` | false | Which of the frame's own rules to draw. Takes a keyword, one side, or a list of sides. |
+| `marks` | `'all' \| 'none' \| Edge \| Corner \| Corner[]` | `'all'` | false | Which corner registration squares to draw. Takes a keyword (`all`, `none`, `top`, `bottom`, `left`, `right`), one corner, or a list of corners. |
+| `hatch` | `boolean` | `false` | false | Show the linear hatch texture behind the content, faded toward the edges. Reserved for `section-gap`, whose identity it is. |
+| `flush` | `boolean \| Side \| Side[]` | `false` | false | Which sides a neighbouring frame already draws, so this one does not draw them again. `true` is shorthand for `top`; use `left` for a horizontal row, or a list for a grid cell. |
+
+## Events
+
+| _none_ | — | — |
+
+## Slots
+
+| Slot | Scope | Notes |
+|---|---|---|
+| `default` | — | Frame content; rendered above the hatch texture and stretched to the frame height. |
+
+## States
+
+- Visual states: `default`
+- `data-borders` carries the RESOLVED, space-separated side list (`flush` already subtracted), or `none`
+- `data-marks` carries the RESOLVED, space-separated corner list, or `none`
+- `data-hatch` present when the hatch texture is drawn
+- `data-flush` carries the space-separated list of sides a neighbour draws; absent when none do
+
+## Motion & Animations
+
+_none_
+
+## Tokens
+
+| Region | Token (DESIGN.md) |
+|---|---|
+| frame rule | `var(--border-default)` |
+| corner mark | `var(--border-default)` |
+| hatch line | `var(--border-default)` |
+| hatch pitch | `var(--spacing-lg)` |
+
+## Theme gaps
+
+
+
+| Figma variable | Temporary primitive | Follow-up |
+|---|---|---|
+| opaque neutral rule | `var(--border-default)` / `var(--border-muted)` as they resolve today | The neutral border tokens are semi-transparent, so a frame rule has no fixed identity — it paints one colour on `--bg-canvas` and a brighter one on `--bg-surface-overlay` — and the hatch inherits that faintness. Making both opaque steps of the surface palette is [#889](https://github.com/aziontech/webkit/pull/889); the frame needs no change when it lands. |
+
+## Accessibility (WCAG 2.1 AA)
+
+- Visible focus: not applicable — the frame is a container and holds no interactive surface of its own; focusable content composed into it keeps its own `focus-visible:ring-2 focus-visible:ring-(--ring-color)` ring.
+- Keyboard map: none — the frame is not focusable and traps nothing; `Tab` order is decided entirely by the slotted content.
+- ARIA: the hatch texture and the four corner squares are decorative and carry `aria-hidden="true"`; the root adds no role, so the slotted content's semantics reach the accessibility tree unchanged.
+- Contrast ≥4.5:1 (text) / ≥3:1 (large + icons) — the rules and marks are non-informational decoration, and the slotted content owns its own contrast.
+- `motion-reduce:transition-none motion-reduce:transform-none` — not applicable, the component is static.
+- Touch target ≥40×40 px — not applicable, no interactive control.
+
+## Stories (Storybook)
+
+- Default
+- Borders — composite story rendering every `borders` value side-by-side (justified: the difference between `all`/`x`/`y`/`none` is only legible when the four are compared in one frame)
+- Marks — composite story rendering the `marks` keywords, a single corner and an explicit list side-by-side (justified: which corners are drawn is the axis a stack depends on, and the forms are only readable in comparison)
+- Hatch — the `hatch` texture on (mutually-exclusive boolean state of the `hatch` prop)
+- Flush — a vertical stack and a horizontal row, each with the second frame flushed against the first (justified: a shared edge is invisible on a single frame, and the story is what proves `flush` works on both axes)
+
+## Constraints — DO NOT
+
+
+
+- Do not add props beyond the Props table above. If you need a prop that is not listed, emit `BLOCKED: missing prop ` and stop — do not invent.
+- Do not add events beyond the Events table above. Same rule for slots and sub-components.
+- Do not invent imports. Every `@aziontech/webkit/*` path must exist in `packages/webkit/package.json#exports`. Every relative import must resolve to a real file. Every npm package must be installed.
+- Do not use HEX/RGB/HSL colors, Tailwind palette names (e.g. `bg-blue-500`), raw typography classes (e.g. `text-sm`), `any`, `@ts-ignore`, or `class` inside `defineProps`.
+- Do not install or import positioning/animation libraries (`@floating-ui/*`, `popper.js`, `tippy.js`, `gsap`, `framer-motion`, `motion`, `@vueuse/motion`, `@formkit/auto-animate`, drag-drop runtimes, scroll virtualization libs). Use CSS + Vue primitives (``, ``). See `.claude/rules/dependencies.md`.
+- Do not improvise animations. Every `animate-*` / `transition-*` class must come from `packages/theme/src/tokens/semantic/animations.js`; every motion-bearing class pairs with `motion-reduce:*` on the same class string; no component-local `@keyframes`.
+- Do not create class presets in JavaScript (`const kindClasses = {...}`, `const sharedClasses = [...]`, `const sizeClasses = {...}`, `const rootClasses = computed(...)`). Variants live on `data-*` attributes consumed by Tailwind `data-[attr=value]:`. All utilities live inline on the root element's `class` attribute. No `