# TakafulHub Design System Foundation

**Phase:** 11A2 — Design Tokens Foundation
**Scope:** This is an additive Tailwind token layer. No existing page or component consumes these tokens yet, so it makes no visual change.

## Architecture and token principles

`tailwind.config.js` is the single source of truth for application-wide visual tokens. Tokens are added through `theme.extend`, preserving Tailwind defaults and all existing utilities. Future components and migrations must use semantic tokens instead of palette-specific or arbitrary values.

- Use semantic role names (`bg-primary`, `text-text-muted`, `border-border`), not raw hue names, for shared UI.
- Tokens must work across public, agent, and admin UI. Do not create page-, role-, or feature-specific palette tokens.
- Public operator pages may continue to provide tenant CSS variables; components should map those variables to semantic roles rather than introduce another global token system.
- New token use is opt-in until a later migration phase. Existing `gray-*`, `slate-*`, arbitrary shadows, and component/page utilities are intentionally unchanged.

## Color system

| Token | Tailwind utility examples | Intended use |
| --- | --- | --- |
| `primary` / `primary-hover` / `primary-active` | `bg-primary`, `hover:bg-primary-hover`, `active:bg-primary-active` | Primary calls to action and active controls. |
| `secondary` | `text-secondary`, `bg-secondary` | Secondary emphasis; typically text or a secondary solid action. |
| `success` | `text-success`, `bg-success` | Confirmed/successful states. |
| `warning` | `text-warning`, `bg-warning` | Caution, pending, or attention-needed states. |
| `danger` | `text-danger`, `bg-danger` | Destructive/error states only. |
| `info` | `text-info`, `bg-info` | Informational states; do not use as the default action color. |
| `background` | `bg-background` | Application/page canvas. |
| `surface` / `surface-muted` | `bg-surface`, `bg-surface-muted` | Cards/dialogs and subdued regions/table headers. |
| `border` | `border-border` | Default control and surface border. |
| `text-primary` / `text-secondary` / `text-muted` | `text-text-primary`, etc. | Main text, supporting text, and metadata/disabled-adjacent copy. |
| `disabled` | `text-disabled`, `bg-disabled` | Disabled-only presentation; preserve native disabled semantics. |
| `focus` | `ring-focus` | The shared focus-ring color. |

### Neutral scale

Slate is the designated neutral scale moving forward. It gives a single scale for surfaces, borders, and text: slate-50/100 for muted surfaces, slate-200/300 for borders, slate-400/500 for muted text, slate-600/700 for secondary text, and slate-800/900/950 for high-emphasis text. Use the semantic tokens above for shared components whenever a role is available; use `slate-*` only where an intentional tonal step is needed. Do not introduce new `gray-*` utilities.

## Typography

Figtree remains the default sans family. Use semantic type utilities below in future components; assign semantic HTML heading elements independently of visual size.

| Token | Utility | Intended use |
| --- | --- | --- |
| Display | `text-display` | Marketing hero/display text; one per view where possible. |
| H1 | `text-h1` | Primary page title. |
| H2 | `text-h2` | Major page section title. |
| H3 | `text-h3` | Card or subordinate section title. |
| Body | `text-body` | Standard readable text. |
| Body Small | `text-body-sm` | Compact UI body text, table content, and help text. |
| Caption | `text-caption` | Metadata and timestamps. |
| Overline | `text-overline` | Short category labels only. |

Weights are `font-regular` (400), `font-medium` (500), `font-semibold` (600), and `font-bold` (700). Use regular for body copy, medium for interactive labels, semibold for UI titles, and bold for display/headline emphasis.

## Spacing, radius, and elevation

### Spacing rhythm

The canonical scale is 4, 8, 12, 16, 20, 24, 32, 40, 48, and 64 px, exposed as `space-1`, `space-2`, `space-3`, `space-4`, `space-5`, `space-6`, `space-8`, `space-10`, `space-12`, and `space-16` respectively. Examples: `p-space-4`, `gap-space-3`, and `mt-space-6`.

Use 4–8 px for tightly related inline items, 12–16 px inside controls and compact cards, 20–24 px for normal card/form groups, 32–40 px between page sections, and 48–64 px for major public-page regions. Avoid arbitrary spacing values in new shared components.

### Radius

Use Tailwind's unchanged `rounded-sm`, `rounded-md`, `rounded-lg`, `rounded-xl`, and `rounded-2xl` scale for compact controls, ordinary controls/buttons, cards, prominent cards/dialogs, and large public marketing surfaces respectively. `rounded-pill` is the new semantic alias for badges, counters, and pill actions. Retaining Tailwind's built-in radius values avoids changing any existing `rounded-*` utility in this foundation phase.

### Shadows

Use `shadow-card` for raised cards, `shadow-dropdown` for menus/popovers, `shadow-modal` for dialogs, and `shadow-floating` for detached persistent/floating controls. Avoid arbitrary shadow values in new work. `shadow-focus` is reserved for focus treatment.

## Motion

Use `duration-fast` (150ms) for hover/focus feedback, `duration-normal` (200ms) for ordinary component transitions, and `duration-slow` (300ms) for dialogs or larger visibility changes. Motion must communicate state, not decorate it. Respect `motion-reduce` utilities; no interaction may depend on animation completion.

## Button

`resources/js/Components/Button.vue` is the shared native-button foundation for future work. It deliberately has no `href` prop: use an Inertia `Link` or anchor for navigation, and use `Button` for in-place actions or form submission. This preserves native semantics and keeps the first shared API small. The legacy `PrimaryButton`, `SecondaryButton`, and `DangerButton` components remain unchanged for now; migration is explicitly deferred.

### API and hierarchy

```vue
<Button variant="primary" size="md" type="submit" :loading="form.processing">
    Save changes
</Button>
```

`type` defaults to `button`; forms must opt into `type="submit"`. Variants are intentionally limited to:

| Variant | Use | Do not use for |
| --- | --- | --- |
| `primary` | One dominant action for a view, form, or action group. | Secondary, cancel, or destructive actions. |
| `secondary` | Neutral alternative actions, including Cancel where a visible action is appropriate. | The most important action in a group. |
| `danger` | Irreversible/destructive actions such as deletion. | General warnings or ordinary negative states. |
| `ghost` | Low-emphasis toolbar, inline, or modal-secondary actions. | A required, primary, or destructive decision. |

There is no success variant because a repeated action use case has not yet been established.

### Sizes and icons

Sizes are `sm` (32 px), `md` (40 px, default), and `lg` (48 px). They set a consistent height, horizontal padding, semantic text utility, and slot gap. Use the named `icon` and `icon-right` slots with regular button text. Supply 16 px icons for `sm`, 20 px icons for `md`, and 20 px icons for `lg` (24 px only when a standalone navigation control needs it).

Set `icon-only` and supply `aria-label` (or the `aria-label` attribute) for icon-only usage. The component emits a development warning if an icon-only button has no accessible name. A separate icon-button component is not needed yet.

### States and accessibility

- `loading` disables interaction, sets `aria-busy="true"`, shows a decorative spinner, and keeps the content area invisible rather than removing it to preserve the button's width.
- `disabled` uses native disabled semantics, a non-interactive cursor, and semantic disabled colors. It is not opacity-only.
- All variants use the shared `focus-visible:shadow-focus` treatment and `duration-normal` color transition.
- Button content can be text, icon plus text, or an accessible icon-only action. Do not use icon-only controls for actions whose meaning cannot be conveyed by a concise accessible label.

## Form controls

The future form-control foundation lives in `resources/js/Components`: `FormField`, `Label`, `Input`, `Select`, `Textarea`, `Checkbox`, `Radio`, and `InputError`. `TextInput` and `InputLabel` remain legacy compatibility primitives and are not migration targets for new work.

### Field anatomy and rhythm

Use `FormField` to assemble a visible label, optional description, native control, and validation error. It provides the control slot with `id`, `required`, `error`, `aria-invalid`, and `aria-describedby` props so the control is explicitly connected to its label/help/error text.

```vue
<FormField id="coupon-code" label="Coupon code" description="Use uppercase letters and numbers." :error="form.errors.code" required>
    <template #default="field">
        <Input v-model="form.code" v-bind="field" autocomplete="off" />
    </template>
</FormField>
```

The vertical order is label → helper text (when useful) → input → error. `FormField` uses a 4 px internal rhythm; use 16 px (`space-4`) between ordinary fields and 24 px (`space-6`) between field groups. Required fields show a visual asterisk and receive the native `required` attribute through the slot. Optional is shown only where its clarification helps.

### Controls and states

| Control | Scope |
| --- | --- |
| `Input` | Native text, email, password, tel, number, search, date/time, and similar native input types. Supports simple `prefix` and `suffix` slots for currency, percentages, and icons. |
| `Select` | Native select with option slot and optional disabled placeholder option. It is not searchable or custom. |
| `Textarea` | Native multiline text with `rows` (default 4), disabled, readonly, and error support. |
| `Checkbox` | Native checkbox retaining the existing `v-model:checked` API, now with semantic state and ARIA props. |
| `Radio` | Native radio for single choice within a named group using `v-model`. |
| `Label` / `InputError` | Semantic label/required marker and non-HTML validation text. `FormField` enables InputError's decorative error icon; legacy direct use remains text-only. |

All controls use a 40 px default control height where a single-line control applies, semantic surface/border/text tokens, a visible focus shadow, native disabled semantics, and an error border when `error` is true. Readonly `Input` and `Textarea` retain readable text on a muted surface; disabled controls use the disabled token and cannot be interacted with. Do not add custom date pickers, masks, rich text, searchable selects, or autocomplete behavior to these primitives.

### Prefixes, suffixes, and validation

Use `Input`'s named slots only for short static context such as `RM`, `%`, or a presentational icon. They are not interactive controls and do not implement masking. Server-side/Inertia errors remain the source of truth: pass an error string to `FormField`, then bind its slot props to the control. `InputError` renders text only; it never accepts raw HTML.

### Accessibility and mobile behavior

- Every field needs a stable `id` and visible `Label`; do not rely on placeholder text as a label.
- `FormField` links helper and error text with `aria-describedby`; controls set `aria-invalid` when an error is present. Native `required`, `disabled`, `readonly`, checkbox, and radio behavior is retained.
- Checkbox and radio controls remain native inputs, preserving browser keyboard behavior and autofill is passed through to native inputs/selects/textareas without overlay interception.
- The 40 px default height is touch-friendly. Pair checkbox/radio controls with a visible `<label for="…">` in the consuming form to provide a comfortable touch target.
- Use the shared Button with `type="submit"` for a future form's submit action. Do not use a button for navigation.

## Data table

`DataTable` is a lightweight semantic table shell, not a data-grid framework. It owns the bordered responsive `overflow-x-auto` container, `table` element, subdued header styling, row dividers, optional row hover, and default/compact cell density. Pages retain their domain-specific `thead`, `tbody`, columns, data formatting, filtering, and action logic through slots.

```vue
<DataTable :empty="!tickets.data.length">
    <template #caption>Support tickets</template>
    <template #head>
        <tr><th scope="col">Subject</th><th scope="col" class="text-right">Action</th></tr>
    </template>
    <tr v-for="ticket in tickets.data" :key="ticket.id">
        <td>{{ ticket.subject }}</td><td class="text-right">…</td>
    </tr>
    <template #empty><EmptyState title="No support tickets yet." /></template>
</DataTable>
```

Use a table when users compare repeated records across consistent columns. Keep it inside `DataTable` so horizontal overflow remains available on narrow screens; this foundation intentionally does not convert tables into mobile cards. Use `density="compact"` only for dense, operational monitoring tables—default density is preferred for most listings. Consumers must use semantic `th`, `td`, and `scope="col"`; action columns normally align right and should contain a restrained number of actions. Future icon-button/menu work can reduce crowded action clusters.

For record collections with substantial per-item context or several independent actions, a responsive Card list is an intentional alternative to `DataTable`, especially in Agent workflows. Use it only when it improves narrow-screen completion; retain `DataTable` for dense comparison tasks and do not create a second table pattern merely to avoid horizontal scrolling.

`DataTable` displays its named `empty` slot below the valid, empty `tbody` rather than placing non-table markup inside table rows. Use `EmptyState` there or outside the table when a page has no listing context. Loading/skeleton treatment is deliberately deferred.

## Pagination

`Pagination` consumes Laravel/Inertia paginator `links` and optional `from`, `to`, and `total` metadata. It preserves backend-generated URLs through Inertia `Link`; it does not slice data, calculate pages, or perform client-side pagination. On desktop it shows the summary, previous/next controls, and page links. On mobile page-number links are hidden while previous/next controls remain.

Current pages expose `aria-current="page"`. Links use the shared visible focus treatment; links without URLs render as non-interactive spans with `aria-disabled="true"`, so disabled pagination cannot navigate. The component safely normalizes known Laravel label entities and strips markup before interpolation. It never uses `v-html`, so no generic raw HTML path is introduced.

## Empty state

`EmptyState` provides a restrained title, optional description, optional decorative `icon` slot, and optional `action` slot. Prefer specific, calm copy that explains the state and offers one useful next action when available—for example, “No support tickets yet” followed by “Create a ticket if you need help.” Do not use oversized illustrations, color alone, or more than one competing action. Icons in the `icon` slot are decorative; meaningful information must remain in the title/description.

## Card

`Card` provides a non-interactive structural surface with semantic border, radius, and controlled padding. It accepts `default` (surface with `shadow-card`), `muted` (subdued surface without elevation), and `elevated` (surface with `shadow-floating`) variants. Padding is `sm` (12 px), `md` (20 px, default), or `lg` (24 px). Use the `header`, default, and `footer` slots for a predictable structure; supplied header/footer slots receive a divider automatically.

```vue
<Card variant="default" padding="md">
    <template #header><SectionHeader title="Recent activity" level="h3" /></template>
    <!-- domain content -->
    <template #footer><!-- restrained secondary action --></template>
</Card>
```

Cards are containers, not links or buttons. Do not add click handlers to make a generic Card interactive; a dedicated interactive-card pattern can be introduced only when there is a real use case.

## Page header

`PageHeader` is for the top-level content heading on an Agent, Admin, or major public application page. It renders one `h1` from `title`, optional supporting `description`, an optional `eyebrow` slot for breadcrumbs/context, optional `meta`, and an `actions` slot. It uses semantic H1 and muted body typography. On small screens, actions sit beneath the heading; from `sm` onward, heading content and actions align left/right without forcing overflow.

```vue
<PageHeader title="Lead assignments" description="Review operational distribution status.">
    <template #eyebrow><a href="…">Operations</a></template>
    <template #actions><Button>Export CSV</Button></template>
</PageHeader>
```

Use one PageHeader per major view. Do not use it for a card title or a minor content grouping.

## Section header

`SectionHeader` provides a quieter internal heading with `title`, optional `description`, an optional `actions` slot, and a constrained `level` of `h2` (default) or `h3`. Use `h2` for major page sections and `h3` for sections nested under an `h2`; preserve semantic heading order rather than selecting a level only for appearance. Actions stack below content on mobile and align right from `sm` onward.

## KPI card

`KpiCard` is a neutral dashboard metric surface built on `Card`. It accepts a pre-formatted `label`, `value`, optional `helper`, decorative `icon` slot, and optional `helper`/`trend` slots. It does not calculate, format, classify, chart, or color a metric: consumers supply currency, percentages, short text, and any semantic status deliberately. KPI cards have no fixed width and belong in responsive page-level grids supplied by consumers.

## Navigation

The navigation foundation is content-agnostic and lives under `resources/js/Components/Navigation`: `Sidebar`, `SidebarNavGroup`, `SidebarNavItem`, `MobileSidebar`, and `Topbar`. It does not inspect user roles, partner status, route names, notifications, or business data. Agent/Admin shells supply their own groups, internal URLs, active flags, permission-filtered items, badges, logos, and footer content.

### Sidebar and navigation items

`Sidebar` is a desktop-only (`lg` and above), sticky shell with brand, scrollable navigation, and footer slots. Its local `v-model:collapsed` state switches between a practical 256 px expanded width and 64 px icon-only width; it never hover-expands. The supplied collapse control has an accessible name and state. Consumers pass the `collapsed` slot prop through to `SidebarNavGroup` and `SidebarNavItem`.

`SidebarNavGroup` supplies an optional restrained group label and can be explicitly made collapsible. Navigation is limited to groups and items—do not build recursive trees. `SidebarNavItem` uses Inertia `Link`, accepts consumer-provided `href`, `label`, `active`, optional number/text `badge`, icon and badge slots, and emits `navigate` after a link click. Active items use semantic primary/surface contrast; inactive items use restrained surface hover. In collapsed state, labels/badges are visually hidden but retained for assistive technology, and the item `title` supplies a lightweight pointer tooltip.

Use 20 px (`h-5 w-5`) navigation icons with a 2 px outline stroke. The icon slot is decorative; the visible/accessible item label remains the navigation name. Navigation badges are intentionally local, compact count indicators—not a replacement for a future status-badge component.

### Mobile drawer and topbar

Below `lg`, use `MobileSidebar` with local `v-model:open` state and connect `Topbar`'s `toggle-navigation` event to it. `MobileSidebar` provides a body-blocking overlay, close button, Escape close, initial drawer focus, scrollable navigation area, and slots for brand/footer/content. Its default slot exposes `close`; bind it to `SidebarNavItem`'s `navigate` event to close after a navigation selection without coupling the foundation to page menus.

`Topbar` is a light content-area header. It provides the mobile navigation trigger, context/breadcrumb slot, general actions slot, notification slot, and user-menu slot. Reuse the existing `Dropdown` and notification implementation inside these slots rather than adding a second dropdown system.

```vue
<Sidebar v-model:collapsed="collapsed">
    <template #brand>…</template>
    <template #default="{ collapsed }">
        <SidebarNavGroup label="…" :collapsed="collapsed">
            <SidebarNavItem href="…" label="…" :active="…" :collapsed="collapsed" />
        </SidebarNavGroup>
    </template>
</Sidebar>
```

Portal content remains consumer-controlled: use full-width layouts for operational tables and constrained widths for reading/forms. Do not make the navigation engine choose content width. Maintain keyboard-visible focus, semantic navigation/dialog landmarks, readable active contrast, and reduced-motion behavior. This foundation intentionally does not include a focus-trap dependency, persisted sidebar preferences, route logic, breadcrumbs, user/notification redesign, or any portal migration.

## Modal and confirmation

`Modal` evolves the existing native `<dialog>` foundation and retains its legacy `show`, `maxWidth`, and `closeable` props for current callers. New work should use `open`/`@update:open`, the `sm`/`md`/`lg`/`xl` `size` prop, `title`, optional `description`, default content, and `footer` slots. Title and description establish `aria-labelledby` and `aria-describedby`; title-less dialogs use the `ariaLabel` fallback.

Normal dialogs dismiss by overlay and Escape. Use `dismissible`, `closeOnOverlay`, and `closeOnEscape` to disable these paths for mandatory or in-flight critical flows. Modal locks body scrolling, uses semantic overlay/modal layers, focuses a `data-autofocus` element (or the first practical control), and restores focus to the opener where practical. Native dialog behavior provides the lightweight focus boundary; no focus-trap dependency is added. Keep Tab order logical, give every dialog action visible focus and explicit button types, and use Escape only when the configured policy permits it.

`ConfirmDialog` is built on `Modal` for explicit confirmation. It accepts `open`, title/description, confirm/cancel labels, `default` or `danger` variant, loading, and dismissal control; it emits `confirm`, `cancel`, and `update:open`. Default confirmation uses primary, destructive confirmation uses danger, and Cancel is secondary. While loading, actions and dismissal are disabled so confirmation cannot double-submit. Consumers own every request, mutation, and business outcome.

```vue
<ConfirmDialog v-model:open="confirmingDeletion" title="Delete account?" description="This action cannot be undone." confirm-label="Delete account" variant="danger" :loading="form.processing" @confirm="deleteAccount" />
```

The existing `Dropdown` remains the shared lightweight menu/popover primitive; no second popover or generic drawer system is needed. Keep its triggers keyboard reachable, use it for short menus, and do not use a dropdown for confirmations or dense rich content. `MobileSidebar` remains navigation-only.

### Agent Portal Shell composition

The authenticated Agent shell composes `Sidebar` (desktop), `MobileSidebar` (below `lg`), `Topbar`, and the existing page header/default slots. The shell owns Agent-specific route activity and permission-filtered item composition; the navigation primitives remain content-agnostic. Keep page content full-width by default so operational tables, Kanban boards, support threads, and history views retain adequate space. Individual Agent pages keep their existing internal layout/padding until their dedicated migration phase.

### Admin Portal Shell composition

The authenticated Admin shell uses the same `Sidebar`, `MobileSidebar`, and `Topbar` primitives, but supplies a distinct Admin-only route composition. It keeps operational content full-width and preserves existing page header/default slots. The Admin shell is selected only from the current authenticated Admin capability; normal Agent and Partner users continue through the separate Agent shell and never receive Admin navigation data.

## Status badge

`StatusBadge` represents a concise, text-bearing semantic state. Its only variants are `neutral`, `info`, `success`, `warning`, and `danger`; sizes are `sm` (default) and `md`. It uses a restrained pill shape and can show a small decorative leading dot with `dot`. The text always carries the state meaning—color and dot are supporting cues only.

Business state mapping remains a consumer decision: approved commonly maps to success, failed/rejected to danger, closed to neutral, and pending to warning or info depending on the context. Do not add business-specific badge variants or encode status mapping inside the component.

## Alert

`Alert` is contextual feedback within the current page, not a persistent product notification or future toast. It provides `info`, `success`, `warning`, and `danger` variants; optional title, description/default slot, decorative icon slot, action slot, and dismiss button. Dismissal emits `close` only—consumers decide whether and how to update their state.

Danger alerts default to assertive `role="alert"`; other variants use polite `role="status"`. Consumers may override the role/live mode for a known semantic need, but should not make every informational banner assertive. Alert content must describe the condition in text rather than relying on color. Existing flash banners and the notification system are not migration targets in this phase.

## Loading state and skeleton

`LoadingState` communicates that a section or content area is still unresolved. It includes a decorative spinner, visible label, optional description, `role="status"`, polite live updates, and `aria-busy="true"`; it does not block the browser or create a global overlay. Shared Button loading remains the correct pattern for a single action control.

`Skeleton` is an assistive-technology-hidden visual placeholder for composition inside a loading region. Use `shape="text"`, `circle`, or `rectangle`, then provide ordinary Tailwind `class`/`style` width and height constraints at the call site. It uses subtle pulse motion and disables animation for reduced-motion users. Build future table rows, cards, and KPI placeholders by composing it rather than adding specialized skeleton components now.

Loading and empty are distinct: use `LoadingState` while data is unresolved and `EmptyState` only after a successful load has no records. Alert is contextual page content; notifications are persistent product events and remain a separate system.

## Responsive system

Existing Tailwind default breakpoints are retained—no breakpoint redesign is part of this phase:

| Prefix | Minimum width | Intended use |
| --- | ---: | --- |
| base | 0 px | Mobile-first default. |
| `sm:` | 640 px | Small tablet / expanded phone layouts. |
| `md:` | 768 px | Tablet layouts. |
| `lg:` | 1024 px | Desktop layout changes. |
| `xl:` | 1280 px | Wide desktop enhancements. |
| `2xl:` | 1536 px | Large-display refinement only. |

Build mobile-first. For data-dense UI, retain horizontal scrolling or deliberately change presentation at a breakpoint; never let a table silently overflow its page.

## Layering

Use named z-index tokens rather than ad hoc numeric values: `z-navigation` (30), `z-dropdown` (40), `z-overlay` (50), `z-modal` (60), `z-toast` (70), and `z-tooltip` (80). The overlay must remain beneath modal content; toast/tooltip placements must not obscure a modal’s required action.

## Focus, icons, and accessibility

### Focus

The standard focus treatment is a visible 3 px indigo ring: `focus-visible:outline-none focus-visible:shadow-focus` (and, where a ring is preferred, `focus-visible:ring-2 focus-visible:ring-focus focus-visible:ring-offset-2`). Apply it to all keyboard-reachable custom controls. Do not remove focus indication without replacement.

### Icons

Do not replace current icons in this phase. Future icon usage should default to 20 px (`h-5 w-5`) within normal controls and 16 px (`h-4 w-4`) for dense/table actions; use 24 px (`h-6 w-6`) for standalone navigation controls. Use a 2 px stroke for outline icons and pair an icon-only control with an accessible name (`aria-label` or visible text). Icons are supporting signals, not the only carrier of critical meaning.

### Accessibility expectations

- Meet WCAG AA contrast at minimum: 4.5:1 for normal text and 3:1 for large text and visual UI boundaries/focus indicators.
- Use native controls first. Associate every input with a visible label or an equivalent accessible name; placeholders are not labels.
- Use semantic headings in order, table headers with appropriate scope, and buttons for actions versus links for navigation.
- Announce validation and status changes with appropriate ARIA/live-region patterns in future shared components; keep error guidance adjacent to the affected field.
- Dialogs must manage focus, expose an accessible name, support Escape only when dismissible, and never trap a user without a valid exit or required completion path.
- Honour reduced-motion preferences with `motion-reduce`; retain readable state changes when motion is disabled.

## Future utility-to-component guidance

These utilities are intentionally not applied during Phase 11A2. They define the recipes later phases should consolidate:

| Pattern | Future recipe |
| --- | --- |
| Primary button | `inline-flex items-center justify-center rounded-md bg-primary px-space-4 py-space-2 text-body-sm font-semibold text-white transition duration-fast hover:bg-primary-hover active:bg-primary-active focus-visible:outline-none focus-visible:shadow-focus disabled:cursor-not-allowed disabled:opacity-50` |
| Card | `rounded-lg border border-border bg-surface p-space-5 shadow-card` |
| Table shell | `overflow-x-auto rounded-lg border border-border bg-surface`; use `bg-surface-muted` headers and a documented cell density. |
| Form field | Label + input/control + optional hint + adjacent error; default vertical gap is `space-1`, fields are separated by `space-4`. |
| Status/alert | Use semantic success/warning/danger/info colors with text, icon, and accessible status messaging—not color alone. |

When a repeated recipe appears in two or more product surfaces, create or extend a shared component rather than copying its utility string. Migration phases must retain existing behavior and verify public, agent, and admin flows separately.
