diff --git a/.specs/ticker.md b/.specs/ticker.md
new file mode 100644
index 000000000..aa84418a7
--- /dev/null
+++ b/.specs/ticker.md
@@ -0,0 +1,154 @@
+---
+name: ticker
+category: marketing
+structure: monolithic
+status: approved
+spec_version: 1
+checksum: bbd1646868aee1bf69ce86f3b81595ee8b491320cf9c4acfc3872253fd664e73
+created: 2026-09-23
+last_updated: 2026-10-01
+---
+
+# Ticker — Component Spec
+
+## Purpose
+
+An endlessly looping row of brand marks — the trust strip that names the companies running on the platform, and the stack strip that names the frameworks and clouds a workload already uses. The marks come from the package's own registry and are drawn in one ink, so a strip reads as a list rather than as a dozen competing palettes.
+
+## When to use
+
+- Under a hero, to name the companies a product already serves ("Trusted by mission-critical workloads").
+- To state compatibility as a list of framework, cloud and tooling marks ("Your Stack, Your Way").
+- Whenever the set of marks is longer than the row that holds it and no single mark has to be read.
+
+## When NOT to use
+
+- When every mark must be seen → use `logo-wall`, which wraps into a grid and never moves.
+- When the reader chooses how far to go through browsable content → use `carousel`, which is user-driven and never advances on its own.
+- For a mark a reader must act on, such as a partner directory → the strip is decorative motion, so put linked marks in a `logo-wall`.
+- For one mark in its own right (a header lockup, a client story card) → use `brand`.
+
+## Related
+
+- `logo-wall` — the static, wrapping grid of consumer-supplied marks; reach for the strip only when the wall would be too long.
+- `carousel` — the user-driven snap scroller; this one moves on its own and holds no essential content.
+- `brand` — the Azion lockup, for the product's own mark rather than a third party's.
+- `overline` — renders the `label` above the row.
+
+## Best practices
+
+- Name marks from the registry rather than passing artwork: the registry is what keeps one company's mark identical on every surface, and a name with no entry still renders as its own wordmark so a strip is never incomplete.
+- Let `duration` derive itself. The component holds speed constant rather than duration, so an eleven-mark strip and a thirty-mark strip advance at the same rate; pass a number only to slow one deliberately.
+- Keep `label` to the claim the row makes, not to a heading — the strip sits under a hero, and a second heading competes with the one above it.
+- Use `size="small"` only where the strip is the quietest thing on the page, such as the floor of a sign-in column; `medium` is the marketing band and carries its own responsive ladder.
+- Never put a call to action, a price or anything else a reader needs inside a strip. It scrolls away, and under `prefers-reduced-motion` it becomes a wrapped grid instead.
+
+## Usage
+
+```vue
+
+
+
+
+
+```
+
+## Props
+
+| Prop | Type | Default | Required | JSDoc |
+| ----------- | --------------------- | ---------- | -------- | -------------------------------------------------------------------------------------------------------------------------------------------------- |
+| `kind` | `'plain' \| 'band'` | `'plain'` | false | Whether the strip paints its own ground; `band` fills it with the page canvas so a textured hero cannot show through the marks, and the floor still reads continuous with the band above it. |
+| `marks` | `string[]` | `[]` | false | Registry names of the marks rendered in the row, in order; an unregistered name renders as its own typographic wordmark so the row stays complete. |
+| `label` | `string` | `''` | false | Overline above the row, stating the claim the marks make. |
+| `size` | `'small' \| 'medium'` | `'medium'` | false | Mark scale; `medium` is the marketing band and climbs 32/40/48 px by device class, `small` is a flat 24 px for a column. |
+| `duration` | `number` | `0` | false | Seconds for one full pass; left at `0` it is derived from the number of marks so every strip moves at one speed. |
+| `ariaLabel` | `string` | `''` | false | Accessible name for the row of marks, announced instead of an unnamed list. |
+
+## Events
+
+| _none_ | — | — |
+
+## Slots
+
+| _none_ | — | — |
+
+## States
+
+- Visual states: every mark rests at 70% opacity and lifts to full on `hover`
+- `data-size` mirrors the `size` prop and is what drives the mark height, the cell width and the row gap
+- `data-kind` mirrors the `kind` prop; `band` paints the page canvas and the strip's own vertical rhythm so nothing behind it shows through while the fill still matches the band it floors, `plain` leaves it transparent over whatever it sits on
+- `data-duplicate` marks every `aria-hidden` copy after the first — the copies the loop translates into place, and the ones hidden outright under reduced motion
+- Short rows: the row repeats until half the track is at least as wide as the strip, so a strip with few marks never shows an empty run; the pass lengthens with each repeat so the speed stays the same
+- Empty: when `marks` is empty the row renders nothing, so a page with no marks to name shows no empty band
+- Reduced motion: the track stops and wraps into a centred static grid, with the duplicate rows removed, so every mark stays reachable
+
+## Motion & Animations
+
+| Trigger | Animation / Transition | Token (see `.claude/docs/DESIGN.md` § Animations) | Reduced-motion fallback |
+| ----------------- | -------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------ | --------------------------------------------------------------- |
+| always (the loop) | `animate-brand-marquee` | semantic (59s default · linear infinite; the track overrides `animation-duration` from its mark count) | `motion-reduce:animate-none` (the row wraps into a static grid) |
+| hover on a mark | `transition-opacity duration-fast-02 ease-productive-entrance` | inline (matches catalog) | `motion-reduce:transition-none` |
+
+## Tokens
+
+| Region | Token (DESIGN.md) |
+| --------------------------------- | --------------------------------------------------- |
+| typography (fallback wordmark) | `.text-heading-xxs` |
+| ink (mark) | `var(--text-default)` at 70% opacity, full on hover |
+| spacing (label ↔ row) | `var(--spacing-xl)` |
+| spacing (row gap, medium) | `var(--spacing-4)` / `var(--spacing-8)` |
+| spacing (row gap, small) | `var(--spacing-10)`, cells sized to the mark |
+| spacing (reduced-motion grid gap) | `var(--spacing-md)` |
+
+## Theme gaps
+
+| Figma variable | Temporary primitive | Follow-up |
+| -------------- | ------------------- | --------- |
+| _none_ | — | — |
+
+## Accessibility (WCAG 2.1 AA)
+
+- Visible focus: none of its own — no mark is a focusable target, so the strip adds no tab stop and needs no ring. The loop still pauses on `:focus-within`, so anything a consumer places over it stays still while it is reached.
+- Keyboard map: no arrow-key model and no tab stop — this is a list, not a composite widget.
+- ARIA: the row is a `
` of `- ` named by `ariaLabel` when set; every copy after the first carries `aria-hidden="true"` so a screen reader hears each mark once, and each mark is titled with its own name.
+- Contrast ≥4.5:1 (text) / ≥3:1 (large + icons): marks are drawn in `var(--text-default)` at 70%, which WCAG 1.4.11 exempts as a logotype; the typographic fallback is real text and clears the minimum in both themes at that strength.
+- `motion-reduce:animate-none` on the track and `motion-reduce:transition-none` on the mark hover. WCAG 2.2.2 applies — the loop is longer than five seconds, so it pauses on hover and on focus-within, and stops outright under `prefers-reduced-motion`.
+- Touch target ≥40×40 px: marks are not interactive targets; the whole strip is one decorative band.
+
+## Stories (Storybook)
+
+- Default
+- Sizes — composite story rendering both `size` values side-by-side
+- Frameworks — the stack strip rather than the client strip (justified: the two registries are the component's whole reason for existing, and no arg on the Default story shows that a framework mark resolves the same way a client mark does)
+- Fallback — a row mixing registered names with an unregistered one (justified: the typographic wordmark is a rendered state no prop can reach, and it is what lets a page name a company whose artwork has not landed)
+
+## 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 `