diff --git a/.specs/progress-bar.md b/.specs/progress-bar.md
new file mode 100644
index 000000000..238f2bf81
--- /dev/null
+++ b/.specs/progress-bar.md
@@ -0,0 +1,126 @@
+---
+name: progress-bar
+category: feedback
+structure: monolithic
+status: implemented
+spec_version: 1
+checksum: 1a7bc9bdc211ad0591a48082474922afe6e508a9b620c45ac30fe79ca782c6d7
+figma:
+ url: https://www.figma.com/design/t97pXRs7xME3SJDs5iZ5RF/Webkit?node-id=479-870
+ node_id: 479:870
+created: 2026-06-26
+last_updated: 2026-06-26
+---
+
+# Progress Bar — Component Spec
+
+## Purpose
+
+Communicates the progress of an ongoing task as a horizontal bar. Use it for a
+determinate value within a range (`value` / `max`) or, when progress can't be
+measured, as an indeterminate loading indicator. Unlike `skeleton` (placeholder
+geometry) or `status-indicator` (discrete state), it expresses a continuous
+quantity.
+
+## Usage
+
+```vue
+
+
+
+
+
+```
+
+## Props
+
+| Prop | Type | Default | Required | JSDoc |
+|---|---|---|---|---|
+| `value` | `number` | `0` | no | Current progress, relative to `max`. |
+| `max` | `number` | `100` | no | Upper bound; percentage = `value / max * 100`. |
+| `shape` | `'rounded' \| 'flat'` | `'rounded'` | no | Track and fill corner-radius variant. |
+| `size` | `'small' \| 'medium' \| 'large'` | `'medium'` | no | Bar height token. |
+| `indeterminate` | `boolean` | `false` | no | Loading state; animates a sliding segment and ignores `value`. |
+
+## Events
+
+| _none_ | — | — |
+
+## Slots
+
+| _none_ | — | — |
+
+## States
+
+- Visual states: `default`, `indeterminate`
+- `data-shape` mirrors the `shape` prop: `rounded` | `flat`
+- `data-size` mirrors the `size` prop: `small` | `medium` | `large`
+- `data-indeterminate` is present while `indeterminate` is `true`
+
+## Motion & Animations
+
+| Trigger | Animation / Transition | Token (see `.claude/docs/DESIGN.md` § Animations) | Reduced-motion fallback |
+|---|---|---|---|
+| `value` change (determinate) | `transition-[width] duration-moderate-02 ease-productive-entrance` (width morph) | duration/curve aliases from `animate.js` (240ms · productive-entrance) | `motion-reduce:transition-none` |
+| `indeterminate` (loading sweep) | `animate-progress-indeterminate` + `animate-progress-indeterminate-short` (two offset bars sweeping across the track via `inset-inline-start`/`inset-inline-end`) | semantic (2.1s · infinite) | `motion-reduce:animate-none` (static fill) |
+
+## Tokens
+
+| Region | Token (DESIGN.md) |
+|---|---|
+| fill | `var(--primary)` |
+| shape (rounded) | `var(--shape-elements)` |
+| shape (flat) | `var(--shape-flat)` |
+
+
+
+## Theme gaps
+
+| Figma variable | Temporary primitive | Follow-up |
+|---|---|---|
+| `--bg-surface-raised` (track) | `var(--bg-surface-raised)` | `TODO: document --bg-surface-raised in .claude/docs/DESIGN.md § Colors` |
+
+## Accessibility (WCAG 2.1 AA)
+
+- Non-interactive: the bar is not focusable and takes no keyboard input.
+- ARIA: root uses `role="progressbar"`. Determinate sets `aria-valuemin="0"`, `aria-valuemax=""`, `aria-valuenow=""`; indeterminate omits `aria-valuenow` and sets `aria-busy="true"`.
+- Contrast ≥3:1 between the fill (`var(--primary)`) and the track (`var(--bg-surface-raised)`).
+- `motion-reduce:transition-none` on the determinate width morph and `motion-reduce:animate-none` on the indeterminate animation (it falls back to a static fill under reduced motion).
+
+## Stories (Storybook)
+
+Composite stories render every variant of an axis side-by-side in a single frame;
+the `Indeterminate` story is the state story for the `indeterminate` boolean.
+
+- Default
+- Shapes — composite story rendering `rounded` and `flat` side-by-side (the component's variant axis; stands in for `Types`).
+- Sizes — composite story rendering `small`, `medium`, `large` side-by-side.
+- Indeterminate — state story for the `indeterminate` prop.
+- Simulation — interactive story: a button advances `value` in steps so the reviewer can see the fill's width transition animate on each `value` change. Justified because this motion (the determinate `transition-[width]`) only manifests on a live value change and cannot be shown by a static frame.
+
+## 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 `