feat(composer): expand ui-system from placeholder to full interview

ui-system skill is no longer a placeholder. Full design-system and
interaction-design interviews with branching, defaults, batched
question clusters, and an 'accept default / skip' protocol so users
move through fast when they have taste and patient when they don't.

Design system interview covers: visual posture, color (with WCAG
contrast verification before writing), typography, spacing, radii,
shadows, density, visual motion, components, theming, and
accessibility tokens (focus ring, hit targets).

Interaction design interview covers: input model, state-surface
declarations per action class, motion posture and triggers,
breakpoints, i18n, four state postures (empty/loading/error/offline),
time formatting, keyboard policy, optimistic-update strategy with
rollback and conflict resolution, gestures, focus management,
notification patterns, selection, and drag-and-drop.

Reference banks for "make it look like Linear/Notion/Stripe/etc."
translate well-known products into starting tokens — user still
confirms each value, no silent invention.

Templates expanded to hold the full output:
- design-system/SKILL.md grew sections for accessibility tokens,
  density, dark-mode mapping, contrast verification, theming variants
- design-system/tokens.ts now exports color (light + dark), typography
  (family/size/weight/line-height/letter-spacing), spacing + density-
  aware padding tokens, radii, shadows, motion (durations + easings),
  z-index scale, breakpoints, hit-target minimums
- design-system/components.md gained anatomy/state/a11y coverage for
  10 primitives plus an "adding a new component" workflow and a
  "patterns to avoid" list (nested cards, color-only meaning, etc.)
- interaction-design/SKILL.md gained sections for optimistic updates,
  conflict resolution, gestures, focus management, notification
  patterns, selection/multi-select, and drag-and-drop

Hard rules: never invent on the user's behalf; <TBD> instead. Verify
contrast before writing color tokens. Always declare prefers-reduced-
motion as honored.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
movq
2026-04-29 21:10:36 -05:00
parent 594fccc80a
commit 92f005895c
5 changed files with 947 additions and 263 deletions

View File

@@ -1,94 +1,140 @@
---
name: <SLUG>-design-system
description: Design system for the <PROJECT NAME> project. Color tokens, typography, spacing, radii, shadows, motion durations, and component vocabulary. Load and apply whenever building UI for this project — generating mocks, writing components, picking colors, configuring Tailwind, styling native screens, building themes, designing CSS, or making any visual decision. The canonical visual reference for this project; if anything visible is being touched, this skill should be in context. Pairs with <SLUG>-interaction-design.
description: Design system for the <PROJECT NAME> project. Color tokens, typography, spacing, radii, shadows, density, motion, accessibility tokens, and component vocabulary. Load and apply whenever building UI for this project — generating mocks, writing components, picking colors, configuring Tailwind, styling native screens, building themes, designing CSS, or making any visual decision. The canonical visual reference for this project; if anything visible is being touched, this skill should be in context. Pairs with <SLUG>-interaction-design.
---
# <PROJECT NAME> design system
> **Status: <draft | reviewed | locked>**
> Generated by Composer's `ui-system` phase. Re-running that phase
> updates this skill (with confirmation).
> updates this skill (with confirmation on overwrite).
## How to use this skill
Whenever you generate or edit UI for this project — a mock, a real
component, a theme, a CSS file — read this SKILL.md and the sidecar
files first, then use the tokens. Do not hardcode values. Do not
introduce new components without proposing them here first.
When generating or editing UI for this project — a mock, a real
component, a theme, a CSS file — read this SKILL.md and `tokens.ts`
first, then use the tokens. Never hardcode visual values. Don't
introduce new components without proposing them in `components.md`.
Sidecar files in this skill directory:
- `tokens.ts` — machine-readable token export. Import in mocks/code.
- `components.md` — component vocabulary with anatomy.
- `components.md` — component vocabulary, anatomy, states.
## Visual posture
<!-- One or two sentences describing the feel. "Quiet, dense, monochromatic with a single brand accent." Or "Playful, generous spacing, expressive type." -->
<!-- One-sentence north star. -->
<TBD: visual posture>
<TBD: visual posture sentence>
## Color
### Brand
- Primary: `<#hex>` — <name>
- (Secondary, if any): `<#hex>` — <name>
- **Primary**: `<#hex>` — <name>
- **Secondary** *(if any)*: `<#hex>` — <name>
### Neutrals
- Tone: <warm | cool | true>
- Scale: 50 / 100 / 200 / 300 / 400 / 500 / 600 / 700 / 800 / 900 / 950
- **Tone**: <warm | cool | true>
- **Scale**: 50 / 100 / 200 / 300 / 400 / 500 / 600 / 700 / 800 / 900 / 950
- See `tokens.ts` for hex values.
### Semantic
| Role | Value |
|---|---|
| Success | `<#hex>` |
| Warning | `<#hex>` |
| Error | `<#hex>` |
| Info | `<#hex>` |
| Role | Light | Dark *(if dark mode)* |
|---|---|---|
| Success | `<#hex>` | `<#hex>` |
| Warning | `<#hex>` | `<#hex>` |
| Error | `<#hex>` | `<#hex>` |
| Info | `<#hex>` | `<#hex>` |
### Dark mode
<TBD: yes / no / system-following + token mapping>
- **Posture**: <yes | no | system-following>
- **Token mapping**: see `tokens.ts` `colorDark` (if applicable)
### Accessibility contrast
- **Target**: WCAG <AA | AAA>
- **Verified pairings** (against `neutral.50` light bg, `neutral.950` dark bg):
- body text: <ratio> — ✅
- UI components: <ratio> — ✅
- <flag any below threshold>
## Typography
- **Display family**: <name>, weights <list>
- **Body family**: <name>, weights <list>
- **Scale base**: <px>, ratio <1.125 | 1.2 | 1.25 | 1.333>
- **Sizes**: xs / sm / base / lg / xl / 2xl / 3xl / 4xl
- **Line-height**: tight for display, normal for body, relaxed for long-form prose
See `tokens.ts` for exact px values.
- **Body family**: <name | "(same as display)">, weights <list>
- **Mono family** *(if used)*: <name>
- **Scale base**: <px>, ratio <1.125 | 1.2 | 1.25 | 1.333 | 1.5>
- **Sizes**: see `tokens.ts` `fontSize` — xs / sm / base / lg / xl / 2xl / 3xl / 4xl
- **Line-height**:
- Tight (display): <value>
- Normal (body): <value>
- Relaxed (long-form): <value>
- **Letter-spacing** *(if customized)*: <values per size class>
## Spacing
- **Base unit**: <4px | 8px>
- **Scale (in base units)**: 0, 0.5, 1, 1.5, 2, 3, 4, 6, 8, 12, 16, 24
- See `tokens.ts` `spacing` for px values.
## Radii
- `sm`, `md`, `lg`, `pill`, `full` — values in `tokens.ts`.
- **Posture**: <sharp | tight | medium | pillowy | mixed>
- `sm` <px> · `md` <px> · `lg` <px> · `pill` 9999px · `full` 9999px
## Shadows
## Shadows & elevation
- `xs / sm / md / lg / xl` — values in `tokens.ts`.
- (Or: "flat — no shadows" if the posture is flat.)
- **Posture**: <flat | subtle | generous>
- Scale: `xs / sm / md / lg / xl` — see `tokens.ts` `shadow`
- (Or "no shadows in this system" if posture is flat — use borders for elevation hierarchy.)
## Density
- **Default**: <comfortable | cozy | compact>
- **Padding tokens**: see `tokens.ts` `padding``tight / default / loose`
- **Component impact**: list components whose default density differs from the global default.
## Visual motion
- **Default duration**: <ms>
- **Default easing**: <named curve or cubic-bezier>
- **Default duration**: <ms> (token: `motion.duration.default`)
- **Default easing**: <named curve> (token: `motion.easing.standard`)
- **Reduced-motion fallback**: linear, instant where appropriate
Behavioral motion (when things animate, what does and doesn't) lives
in the interaction-design skill.
Behavioral motion (when things animate, what triggers them) is in the
**interaction-design** skill, not here.
## Accessibility tokens
- **Focus ring**: <style — solid 2px brand at 60% opacity / etc.>
- **Focus ring color token**: `color.focus`
- **Focus offset**: <px>
- **Hit-target minimum**: 44×44px (mobile) / 32×32px (desktop pointer)
- **Contrast targets**: WCAG <AA | AAA> — body 4.5:1, UI 3:1
## Components
See `components.md` in this directory for the full vocabulary and
anatomy. The TL;DR list:
See `components.md` in this directory for the full vocabulary, anatomy,
and state coverage. The TL;DR list of primitives:
- Button (primary / secondary / ghost / destructive / link)
- Input (text / textarea / select / checkbox / radio / toggle)
- Card / Surface
- (project-specific components from `ia.md`)
- **Button** (primary / secondary / ghost / destructive / link)
- **Input** (text / textarea / select / checkbox / radio / toggle)
- **Card / Surface**
- **Badge / Tag**
- **Avatar**
- **Tooltip**
- **Modal / Drawer / Popover** *(behavioral patterns in interaction-design skill)*
- **Tabs**
- **Menu / Dropdown**
- **Toast / Notification**
Plus project-specific components from `ia.md`: see `components.md`.
## Theming variants
- **Multi-brand / tenant theming**: <yes | no — typically no for v1>
- **Theme-switching mechanism** *(if applicable)*: <CSS variables / data-theme attr / etc.>
## TBD on return
- <TBD: anything the placeholder pass couldn't answer>
<!-- Any phases that were skipped or had unanswered questions in the
ui-system interview. This is the worklist for the next pass. -->
- <TBD: ...>

View File

@@ -1,52 +1,174 @@
# Component vocabulary
The named components for <PROJECT NAME>. Match names from `ia.md`
where applicable. Do not introduce new components without proposing
them here first.
The named components for <PROJECT NAME>. Component names match
`ia.md` where applicable. **Do not introduce new components without
proposing them here first.**
This file is loaded automatically by the design-system skill — agents
generating UI should be aware of every component in this list and
prefer them to ad-hoc one-offs.
This file is a sidecar of the design-system skill — agents generating
UI should be aware of every component in this list and prefer them to
ad-hoc one-offs. New patterns belong here, not in mock files.
## How to read this
For each component:
- **Variants** — distinct visual flavors with different semantic meaning
- **Sizes** — usually `sm / md / lg`; pick the same scale across components
- **States** — every visible state the component must handle
- **Anatomy** — the named slots / regions the component is composed of
- **Tokens used** — which design-system tokens this component reads
- **A11y notes** — load-bearing accessibility rules for this component
## Primitives
### Button
- **Variants**: primary | secondary | ghost | destructive | link
- **Sizes**: sm | md | lg
- **States**: default | hover | active | focus | disabled | loading
- **Anatomy**: optional leading icon · label · optional trailing icon
- **Motion**: <TBD>
- **Variants**: `primary` | `secondary` | `ghost` | `destructive` | `link`
- **Sizes**: `sm` | `md` | `lg`
- **States**: default | hover | active | focus | focus-visible | disabled | loading
- **Anatomy**: optional leading icon · label · optional trailing icon · optional loading spinner
- **Tokens used**: `color.brand.primary`, `color.semantic.error` (destructive), `color.focus`, `padding.default`, `radius.md`, `motion.duration.fast`, `fontSize.sm/base/lg`, `fontWeight.medium`
- **A11y**:
- Hit target ≥ `hitTarget.mobile` for `md+` sizes
- `aria-busy` while loading; preserve label
- `aria-disabled` rather than `disabled` if the action is permission-gated rather than form-invalid
- `link` variant uses `<a>`, not `<button>`
### Input
- **Variants**: text | textarea | select | checkbox | radio | toggle
- **Sizes**: sm | md | lg
- **States**: default | focus | error | disabled | readonly
- **Anatomy**: label · field · helper text · error message
- **Validation pattern**: <TBD — see interaction-design skill>
- **Variants**: `text` | `textarea` | `select` | `checkbox` | `radio` | `toggle` | `password` | `number` | `date`
- **Sizes**: `sm` | `md` | `lg`
- **States**: default | focus | filled | disabled | readonly | error | success *(rare)*
- **Anatomy**: label · *(optional)* description · field · *(optional)* helper text · *(error or hint)* message
- **Tokens used**: `color.neutral.*`, `color.semantic.error`, `color.focus`, `padding.default`, `radius.sm`, `fontSize.base`
- **A11y**:
- Label is **always** present (visible or `aria-label`)
- Error message is `aria-describedby`-linked to the field
- `aria-invalid="true"` while error
- Required fields marked in two ways (asterisk + `required` attribute)
- `aria-describedby` for helper text
### Card / Surface
- **Variants**: <flat | bordered | elevated — pick or list>
- **Anatomy**: <header? · body · footer?>
- **Variants**: `flat` | `bordered` | `elevated` *(per design-system shadow posture)*
- **States**: default | hover *(if interactive)* | focus *(if interactive)* | disabled
- **Anatomy**: optional header · body · optional footer · optional media region
- **Tokens used**: `color.neutral.*`, `radius.lg`, `shadow.sm` (elevated), `padding.loose`
- **A11y**:
- If clickable, the entire card is one `<button>` or `<a>` — not nested interactives
- If contains interactives, card itself is not clickable
### Badge / Tag
- **Variants**: `neutral` | `success` | `warning` | `error` | `info` | `brand`
- **Sizes**: `sm` | `md`
- **States**: default | *(removable)* with close button
- **Anatomy**: optional dot · label · optional close button
- **Tokens used**: `color.semantic.*`, `radius.pill`, `fontSize.xs`, `fontWeight.medium`
### Avatar
- **Variants**: `image` | `initials` | `placeholder`
- **Sizes**: `xs` | `sm` | `md` | `lg` | `xl`
- **States**: default | with status indicator
- **Anatomy**: image / initials · *(optional)* status dot
- **Tokens used**: `radius.full`, `color.neutral.*` (fallback bg), `fontSize.*` (initials sizing)
- **A11y**: `alt` text required for image variant; meaningful or empty `alt=""` for purely decorative
### Tooltip
- **Variants**: `default` | `error` | `info`
- **States**: hidden | shown
- **Anatomy**: trigger · floating panel · optional arrow
- **Tokens used**: `color.neutral.900` bg, `color.neutral.50` text, `radius.sm`, `shadow.md`, `motion.duration.fast`
- **A11y**:
- `role="tooltip"`, `aria-describedby` on trigger
- Show on hover AND focus (keyboard parity)
- Dismissible with `Esc`
- Never the only conveyance of critical information
### Modal / Drawer / Popover
These are **behavioral patterns** — see the interaction-design skill
for *when* to use which. The visual styling is here.
- **Modal**:
- Anatomy: backdrop · panel · optional header · body · optional footer · close button
- Tokens: `shadow.xl`, `radius.lg`, `motion.duration.default`, `zIndex.modal`
- Always traps focus; restores on close
- **Drawer**:
- Anatomy: backdrop · panel (side-anchored) · header · body · footer · close button
- Tokens: `shadow.xl`, `motion.duration.default`, `zIndex.drawer`
- **Popover**:
- Anatomy: trigger · floating panel · optional arrow
- Tokens: `shadow.md`, `radius.md`, `zIndex.popover`
### Tabs
- **Variants**: `underline` | `pills` | `enclosed`
- **States**: active | inactive | hover | focus | disabled
- **Anatomy**: tab list · tab · panel
- **A11y**:
- `role="tablist"`, `role="tab"`, `role="tabpanel"`
- Arrow-key navigation between tabs (per WAI-ARIA Authoring Practices)
- Tab `aria-selected` and `tabindex` managed appropriately
### Menu / Dropdown
- **Variants**: `default` | `with-icons` | `with-shortcuts` | `submenu`
- **States**: closed | open
- **Anatomy**: trigger · floating panel · menu items · separators
- **Tokens used**: `shadow.md`, `radius.md`, `padding.tight`, `zIndex.dropdown`
- **A11y**:
- `role="menu"`, `role="menuitem"`
- Arrow-key navigation; `Esc` closes
- Type-ahead is encouraged
- Focus returns to trigger on close
### Toast / Notification
- **Variants**: `default` | `success` | `warning` | `error` | `info`
- **Anatomy**: optional icon · title · optional description · optional action button · close button
- **Tokens used**: `color.semantic.*`, `radius.md`, `shadow.lg`, `motion.duration.default`, `zIndex.toast`
- **A11y**:
- `role="status"` for non-critical, `role="alert"` for critical
- Dismissible by user; don't auto-dismiss critical errors
- Stack region is `aria-live="polite"` (or `assertive` for errors)
## Project-specific components
<!--
Pulled from ia.md. One block each, with anatomy and any state-specific
notes that should always apply when this component is rendered.
notes that should always apply when this component is rendered. Match
the format of the primitives above.
-->
### <Component name>
- **Purpose**: <one line>
- **Anatomy**: <slots / regions>
- **Variants**: <list>
- **Sizes**: <list>
- **States**: <list>
- **Notes**: <anything load-bearing>
- **Anatomy**: <slots / regions>
- **Tokens used**: <list>
- **A11y**: <load-bearing rules>
- **Notes**: <anything else>
## Patterns to avoid
<!--
Things that come up often and should be vetoed. Saves the agent from
re-inventing them.
-->
Things that come up often and should be vetoed by default. Saves the
agent from re-inventing them.
- <TBD: e.g., "no nested cards", "no inline icons in body text", etc.>
- **Nested cards** — flatten or use sections; visual hierarchy comes from spacing and dividers, not stacking surfaces
- **Inline icons in body text** — icons in body text rarely scale or align well; use them in headers, buttons, badges
- **Modal-from-modal** — if a modal needs to launch another modal, the first modal is doing too much
- **Tooltip as the only conveyance** — tooltips are supplementary; the primary information must be available without hover
- **Color-only meaning** — every color-coded distinction needs a redundant signal (icon, label, position)
- **Custom focus rings that don't meet contrast** — use the system focus-ring token
- **Animation that violates `prefers-reduced-motion`** — always honor the user preference
- **Click targets below 44px on touch devices**
## Adding a new component
1. Propose it here first with the same shape as the others (variants, states, anatomy, tokens, a11y).
2. Add it to `tokens.ts` if it needs new tokens.
3. Add a mock that uses it before writing real-code implementation.
4. Council review (Maya for a11y, Marge for end-user) before locking in.

View File

@@ -1,13 +1,20 @@
// Design tokens for <PROJECT NAME>.
// Generated by Composer's ui-system phase.
// Single source of truth for colors, type, spacing, radii, shadows, motion.
// Import this in mocks and (eventually) in real UI code; do not hardcode.
// Single source of truth colors, type, spacing, radii, shadows,
// density, motion, accessibility. Import this in mocks AND in real
// UI code; do not hardcode visual values.
// =============================================================================
// COLOR
// =============================================================================
export const color = {
brand: {
primary: "<TBD: #hex>",
primary: "<TBD: #hex>",
// secondary: "<TBD>",
},
// Neutral scale, tone: <warm | cool | true>
neutral: {
50: "<TBD>",
100: "<TBD>",
@@ -21,81 +28,189 @@ export const color = {
900: "<TBD>",
950: "<TBD>",
},
semantic: {
success: "<TBD>",
warning: "<TBD>",
error: "<TBD>",
info: "<TBD>",
},
// dark mode tokens (if applicable):
// dark: { ... }
// Accessibility tokens
focus: "<TBD>", // focus-ring color
selection: "<TBD>", // text selection background
// Text on each neutral background, pre-verified for contrast
textOnLight: "<TBD>", // body text on neutral.50neutral.200 — must be ≥ 4.5:1
textOnDark: "<TBD>", // body text on neutral.800neutral.950 — must be ≥ 4.5:1
textMuted: "<TBD>", // secondary text — must be ≥ 4.5:1 for body, 3:1 for UI
} as const;
// Dark-mode overrides (only filled in if dark mode = yes/system).
// When dark mode is active, prefer these over `color.*`.
export const colorDark = {
brand: { primary: "<TBD>" },
neutral: { 50: "<TBD>", 100: "<TBD>", /* … */ 950: "<TBD>" },
semantic: { success: "<TBD>", warning: "<TBD>", error: "<TBD>", info: "<TBD>" },
} as const;
// =============================================================================
// TYPOGRAPHY
// =============================================================================
export const fontFamily = {
display: "<TBD: name>, system-ui, sans-serif",
body: "<TBD: name>, system-ui, sans-serif",
mono: "ui-monospace, SFMono-Regular, monospace",
display: '"<TBD: name>", system-ui, -apple-system, sans-serif',
body: '"<TBD: name>", system-ui, -apple-system, sans-serif',
mono: 'ui-monospace, "SF Mono", Menlo, Consolas, monospace',
} as const;
// Modular scale — base × ratio
export const fontSize = {
xs: "12px",
sm: "14px",
base:"16px",
lg: "18px",
xl: "20px",
"2xl":"24px",
"3xl":"30px",
"4xl":"36px",
xs: "12px",
sm: "14px",
base: "16px", // <TBD: confirm base>
lg: "18px",
xl: "20px",
"2xl": "24px",
"3xl": "30px",
"4xl": "36px",
"5xl": "48px",
} as const;
export const fontWeight = {
regular: 400,
medium: 500,
semibold: 600,
bold: 700,
} as const;
export const lineHeight = {
tight: 1.2,
normal: 1.5,
relaxed: 1.7,
tight: 1.2, // display
normal: 1.5, // body
relaxed: 1.7, // long-form prose
} as const;
// Spacing scale in base units. Multiply by base (4 or 8) for px.
export const spacingBase = 4; // <TBD: 4 or 8>
export const spacing = {
0: 0,
0.5: 0.5 * spacingBase,
1: 1 * spacingBase,
1.5: 1.5 * spacingBase,
2: 2 * spacingBase,
3: 3 * spacingBase,
4: 4 * spacingBase,
6: 6 * spacingBase,
8: 8 * spacingBase,
12: 12 * spacingBase,
16: 16 * spacingBase,
24: 24 * spacingBase,
export const letterSpacing = {
tight: "-0.02em",
normal: "0",
wide: "0.04em",
} as const;
// =============================================================================
// SPACING
// =============================================================================
export const spacingBase = 4; // <TBD: 4 or 8>
export const spacing = {
0: 0,
0.5: 0.5 * spacingBase,
1: 1 * spacingBase,
1.5: 1.5 * spacingBase,
2: 2 * spacingBase,
3: 3 * spacingBase,
4: 4 * spacingBase,
6: 6 * spacingBase,
8: 8 * spacingBase,
12: 12 * spacingBase,
16: 16 * spacingBase,
24: 24 * spacingBase,
} as const;
// Padding tokens by density posture — components reference these
// rather than picking spacing values directly. Switch the global
// density by re-pointing `density` (or per-component override).
export const padding = {
tight: { x: spacing[2], y: spacing[1] },
default: { x: spacing[3], y: spacing[2] },
loose: { x: spacing[4], y: spacing[3] },
} as const;
export const density = "default" as keyof typeof padding; // <TBD: "tight" | "default" | "loose">
// =============================================================================
// RADII
// =============================================================================
export const radius = {
sm: "<TBD: px>",
md: "<TBD: px>",
lg: "<TBD: px>",
none: "0",
sm: "<TBD>",
md: "<TBD>",
lg: "<TBD>",
pill: "9999px",
full: "9999px",
} as const;
// =============================================================================
// SHADOWS
// =============================================================================
export const shadow = {
xs: "<TBD>",
sm: "<TBD>",
md: "<TBD>",
lg: "<TBD>",
xl: "<TBD>",
none: "none",
xs: "<TBD>",
sm: "<TBD>",
md: "<TBD>",
lg: "<TBD>",
xl: "<TBD>",
} as const;
// =============================================================================
// MOTION
// =============================================================================
export const motion = {
duration: {
instant: 0,
fast: 150,
default: 250,
slow: 400,
slower: 600,
},
easing: {
standard: "cubic-bezier(0.2, 0, 0, 1)",
accel: "cubic-bezier(0.4, 0, 1, 1)",
decel: "cubic-bezier(0, 0, 0.2, 1)",
bounce: "cubic-bezier(0.34, 1.56, 0.64, 1)",
},
} as const;
// =============================================================================
// Z-INDEX
// =============================================================================
export const zIndex = {
base: 0,
raised: 10,
dropdown: 100,
sticky: 200,
drawer: 300,
modal: 400,
popover: 500,
toast: 600,
tooltip: 700,
max: 9999,
} as const;
// =============================================================================
// BREAKPOINTS
// =============================================================================
// Note: breakpoint values live here for visual-token coordination, but the
// authoritative *interpretation* (mobile-first, primary target, etc.) is in
// the interaction-design skill.
export const breakpoint = {
sm: "640px",
md: "768px",
lg: "1024px",
xl: "1280px",
"2xl": "1536px",
} as const;
// =============================================================================
// HIT TARGETS (a11y)
// =============================================================================
export const hitTarget = {
mobile: 44, // px — WCAG 2.2 minimum
desktop: 32, // px — pointer-input minimum
} as const;