+```
+
+- **`padded` defaults to `false`.** An edge-to-edge stack of modules each own their padding; adding
+ column padding would double it and pull the modules off the frame. Pass `padded` only for a plain
+ prose column with no modules.
+- **`bordered`** can be turned off for a column that sits inside another frame.
+
+---
+
+## 3. The module rule — `SectionModule`
+
+[`SectionModule.vue`](../../apps/webkit-sample/src/shared/ui/layout/SectionModule.vue)
+
+The "lego brick". A `
` with a header row divided from its body by a hairline, and divided
+from the module above by a hairline. Stacked in a `SectionContainer`, the modules read as bricks in
+one continuous frame.
+
+```html
+
+```
+
+| Prop | When to change it |
+| --- | --- |
+| `divided` | `false` on the **first** module in a column — its top edge is the hero's `border-b`. |
+| `padded` | `false` when the body is an edge-to-edge `CardGrid`, so the grid's rules meet the frame with no gutter. |
+| `#header` | Replaces the default title row entirely. |
+| `#actions` | Trailing CTAs inside the default header row. |
+
+---
+
+## 4. The hairline box grid — `CardGrid variant="divider"`
+
+[`CardGrid.vue`](../../apps/webkit-sample/src/shared/ui/layout/CardGrid.vue)
+
+The signature industrial module: a grid whose internal rules are **gaps**, not borders.
+
+```html
+
+```
+
+`gap-px` lets the wrapper's background colour show through as 1px internal rules. **Children must
+fill their own background** (`bg-[var(--bg-canvas)]`) or the whole cell goes border-coloured.
+
+The divider variant carries **no perimeter border** — that is what lets it sit flush inside a
+`SectionContainer` whose `border-x` already owns the outer edges. Set `dividerColor="muted"` for
+rules one step back.
+
+The `gap` variant is the other mode: self-contained cards with their own border, radius and
+background, separated by real gutters.
+
+**Cells are square.** `ComponentGridCell` uses `rounded-[var(--shape-flat)]` — the interactive
+component showcase grid is deliberately unrounded, with a dashed `--primary` ring and a corner label
+pinned at `top-0 left-0` on hover/focus-within.
+([`ComponentGridCell.vue:33-49`](../../apps/webkit-sample/src/hub/components/ComponentGridCell.vue#L33-L49))
+
+---
+
+## 5. The registration frame — `FrameBox`
+
+[`FrameBox.vue`](../../apps/webkit-sample/src/shared/ui/layout/FrameBox.vue)
+
+The most literal piece of the industrial language: a thin bordered box with **crosshair registration
+marks** straddling each corner, plus an optional vertical hatch texture. Used for the final CTA
+block.
+
+- **Marks** (`marks`, default `true`) — an 11px crosshair per corner, centered *on* the corner with a
+ half-size translate so it straddles the border line. Two 1px spans in `--border-default`.
+- **Hatch** (`hatch`, default `false`) — vertical rules every `--spacing-lg`, drawn as a
+ `repeating-linear-gradient` in `--border-muted` at `opacity-40`, faded at the edges with
+ `mask-[radial-gradient(ellipse_at_center,black,transparent_85%)]`.
+
+```html
+
+```
+
+---
+
+## Vocabulary
+
+The token layer everything above resolves to.
+
+### Weights
+
+| Token | Value | Role |
+| --- | --- | --- |
+| `--border-width-default` (= `--border-1`) | `0.8px` | Component-level hairline (opt-in, see [drift](#known-issues)). |
+| `--border-2` | `2px` | Accent bar — painted as a **filled element**, never a border. |
+
+The page layer uses the bare `border` / `border-x` / `border-t` utilities throughout, which is
+Tailwind's **1px**.
+
+### Colors
+
+| Token | Light | Dark | Role |
+| --- | --- | --- | --- |
+| `--border-default` | `#00000014` | `#FFFFFF1A` | The frame: hero rule, column rules, module dividers, grid gaps. |
+| `--border-muted` | `#00000014` | `#FFFFFF0D` | One step back: rules *inside* a module, capability strips, `FrameBox` perimeter + hatch. |
+| `--border-strong` | `#000000` | `#FFFFFF` | Pressed / active edge. |
+| `--border-selected` | `#F3652B` | `#F3652B` | Selection (Azion orange, both modes). |
+
+> **`default` and `muted` are identical in light mode** (`#00000014`); only dark mode separates them.
+> Pick by role and verify in dark — light mode cannot show you the mistake.
+
+### Radii
+
+`--shape-flat` `0` (grid cells, bands) · `--shape-elements` `6px` (inputs, chips) ·
+`--shape-button` `6px` · `--shape-card` `8px` — **the ceiling**. Nothing structural is pill-shaped.
+
+### Column widths
+
+Both containers key into [`container.js`](../../packages/theme/src/tokens/primitives/shape/container.js)
+— a geometric scale anchored at `3xs` 256px and `7xl` 1620px, every neighbour ~+16.6% apart.
+
+---
+
+## Rules of the language
+
+- **One edge between two things.** Give it to one side; the other draws nothing.
+- **First module in a column: `:divided="false"`.**
+- **Module body `:padded="false"` when it holds an edge-to-edge grid.**
+- **`variant="divider"` children must fill their own background.**
+- **Heroes are full-bleed; columns are capped.** Never put `border-x` on a hero or `border-b` on a
+ column.
+- **No shadow anywhere in this language.** Shadows mean *floating* — overlays, popovers, drawers.
+- **No radius above `--shape-card` (8px); grid cells are `--shape-flat`.**
+- **Texture is masked, never raw.** Every ASCII field / hatch carries a radial mask and an opacity
+ below 1, so it never competes with copy.
+- **`motion-reduce:` on every transition.** `ComponentGridCell` does this in its scoped block; inline
+ utilities use `motion-reduce:transition-none`.
+
+---
+
+## Known issues
+
+Found while documenting; **not fixed** — each changes rendered output.
+
+1. **`FrameBox` is used but never imported** —
+ [`AzionHome.vue:550`](../../apps/webkit-sample/src/site/components/AzionHome.vue#L550) wraps the
+ final CTA in ``, but the script block imports only `Button`, `CardBox`,
+ `CardPricing`, `CodeBlock`, `PlatformIllustrations`, `PlatformShowcase`, and
+ `main.js` registers nothing globally. The route is live (`/site/home` → `LandingAzion` →
+ `AzionHome`), so Vue logs *"Failed to resolve component: FrameBox"* and the CTA renders with **no
+ frame, no crosshairs, no hatch**. One-line fix:
+ `import FrameBox from './foundations/components/layout/FrameBox.vue'`.
+
+2. **`maxWidth` keys don't mean the same width in the two containers.** `Hero`'s map is
+ shifted two steps down from `SectionContainer`'s — `max-width="7xl"` gives `--container-5xl`
+ (1192px) in the banner but `--container-7xl` (1620px) in the column; `"6xl"` gives 1024px vs
+ 1388px. `SectionContainer`'s docstring says *"match the banner above it"*, which reads as "pass
+ the same key" — and passing the same key does **not** align their content edges. (The banner also
+ adds `px-[var(--spacing-xl)]`, so some inset is intended; the two-step shift is systematic across
+ all five keys.)
+
+3. **Two hairline weights coexist.** The page layer uses bare `border` (1px); 39 webkit components
+ request `border-[length:var(--border-width-default)]` (0.8px). The theme sets no
+ `--default-border-width`, so the token is opt-in and the two sit side by side on the same screen.
+
+4. **`PageHeader` mixes raw values into a token system** — `mb-12`, `mb-3`, `mt-4`, `mt-6`, and
+ `max-w-[620px]` instead of spacing/container tokens.
+
+Items 2–4 need a visual-baseline regen; item 1 does not.
diff --git a/.claude/hooks/component-categories.mjs b/.claude/hooks/component-categories.mjs
index 2e1e73a22..ae47b77e0 100644
--- a/.claude/hooks/component-categories.mjs
+++ b/.claude/hooks/component-categories.mjs
@@ -7,6 +7,7 @@ export const COMPONENT_CATEGORIES = [
'feedback',
'inputs',
'layout',
+ 'marketing',
'navigation',
'overlay',
'templates',
diff --git a/.claude/hooks/validate-references.mjs b/.claude/hooks/validate-references.mjs
index 264708494..6a3fe9607 100644
--- a/.claude/hooks/validate-references.mjs
+++ b/.claude/hooks/validate-references.mjs
@@ -66,9 +66,15 @@ function resolveAziontechWorkspace(specifier) {
return existsSync(join(ROOT, 'packages', wsName, 'package.json'))
}
-function resolveRelativeImport(specifier, fromFile) {
- const baseDir = dirname(resolve(fromFile))
- const target = resolve(baseDir, specifier)
+const VITE_ALIASES = {
+ '@shared': 'apps/webkit-sample/src/shared',
+ '@site': 'apps/webkit-sample/src/site',
+ '@hub': 'apps/webkit-sample/src/hub',
+ '@console': 'apps/webkit-sample/src/console',
+ '@preview': 'apps/webkit-sample/src/preview'
+}
+
+function resolveFileTarget(target) {
const candidates = [
target,
`${target}.vue`,
@@ -95,6 +101,17 @@ function resolveRelativeImport(specifier, fromFile) {
})
}
+function resolveRelativeImport(specifier, fromFile) {
+ return resolveFileTarget(resolve(dirname(resolve(fromFile)), specifier))
+}
+
+function resolveViteAlias(specifier) {
+ const [head, ...rest] = specifier.split('/')
+ const base = VITE_ALIASES[head]
+ if (!base) return false
+ return resolveFileTarget(join(ROOT, base, ...rest))
+}
+
function resolveNodeModule(specifier) {
const parts = specifier.split('/')
const pkgName = specifier.startsWith('@') ? `${parts[0]}/${parts[1]}` : parts[0]
@@ -104,7 +121,8 @@ function resolveNodeModule(specifier) {
'packages/theme/node_modules',
'packages/icons/node_modules',
'apps/storybook/node_modules',
- 'apps/icons-gallery/node_modules'
+ 'apps/icons-gallery/node_modules',
+ 'apps/webkit-sample/node_modules'
]
return dirs.some((d) => existsSync(join(ROOT, d, pkgName)))
}
@@ -139,7 +157,7 @@ async function main() {
let ok = false
let reason = ''
- if (spec.startsWith('@aziontech/webkit')) {
+ if (spec === '@aziontech/webkit' || spec.startsWith('@aziontech/webkit/')) {
ok = resolveAziontechWebkit(spec)
reason = 'no matching entry in packages/webkit/package.json#exports'
} else if (spec.startsWith('@aziontech/')) {
@@ -148,6 +166,9 @@ async function main() {
} else if (spec.startsWith('.') || spec.startsWith('/')) {
ok = resolveRelativeImport(spec, filePath)
reason = 'relative path does not resolve to any file'
+ } else if (Object.hasOwn(VITE_ALIASES, spec.split('/')[0])) {
+ ok = resolveViteAlias(spec)
+ reason = 'vite alias does not resolve to any file'
} else {
ok = resolveNodeModule(spec)
reason = 'package not installed in any node_modules'
diff --git a/.specs/_schema.json b/.specs/_schema.json
index d52d0a7eb..c5a402e3a 100644
--- a/.specs/_schema.json
+++ b/.specs/_schema.json
@@ -31,6 +31,7 @@
"feedback",
"inputs",
"layout",
+ "marketing",
"navigation",
"overlay",
"templates",
diff --git a/.specs/_template.md b/.specs/_template.md
index 61984b230..08f81013a 100644
--- a/.specs/_template.md
+++ b/.specs/_template.md
@@ -1,6 +1,6 @@
---
name:
-category:
+category:
structure:
status: draft
spec_version: 1