# Textal design system

The interface is one product rendered through three independent preferences. A **color theme**
decides the palette, a **UI style** decides the interface's character, and the **mode** chooses light or
dark. Every theme works with every style in both modes (7 × 5 × 2 = 70 combinations), and the
Appearance settings page renders a live miniature of each choice.

Source of truth, in this order: `resources/css/themes.css` (palettes), `resources/css/styles.css`
(styles and semantic mapping), `resources/css/app.css` (Tailwind bridge, type and surface roles),
`resources/js/lib/appearance.ts` (option metadata, legacy migration). This document explains them.

## Architecture

```
<html class="dark"            ← mode: light | dark (system resolves to one of them)
      data-theme="aurora"     ← color theme: palette roles --t-*
      data-style="minimal">   ← UI style: semantic mapping + --ui-* presentation tokens
```

1. **Theme** (`themes.css`) defines color *roles* only: `--t-canvas`, `--t-canvas-2`, `--t-surface`,
   `--t-raised`, `--t-line`, `--t-line-2`, `--t-text`, `--t-text-2`, `--t-text-3`, the accent set
   (`--t-accent`, `-hover`, `-active`, `-fg`), the command-rail set (`--t-rail*`), ambient glows,
   a shadow tint and a second chart hue. Each theme has a light and a dark block.
2. **Mode** primitives (`--m-*`, status colors) live at the top of `themes.css`.
3. **Style** (`styles.css`) maps roles onto the semantic tokens the app uses (`--background`,
   `--card`, `--sidebar`, `--primary`…) and owns presentation: type faces and scale, radii, control
   heights, density, elevation, shell composition.

The semantic mapping is re-declared on every element carrying `data-theme`, `data-style` or
`data-mode`, so a scoped subtree (the appearance previews, theme swatches) re-themes itself.
Previews must set all three attributes.

Tailwind reads everything through `@theme inline`, so utilities such as `bg-card`, `rounded-card`
or `shadow-popover` resolve per scope at runtime.

## Color themes

| Theme | Character | Light accent | Dark accent |
|---|---|---|---|
| Aurora (default) | indigo and violet over cool neutrals | `#5b5bd6` | `#9391f8` |
| Obsidian | graphite neutrals, ink accent | `#18181b` | `#f4f4f5` |
| Pearl | porcelain and silver, steel-cyan accent | `#0f6b87` | `#67c6e0` |
| Midnight | navy-tinted slate, cobalt accent | `#2f5bea` | `#7aa2ff` |
| Emerald | green-grey, deep emerald accent | `#047857` | `#34d399` |
| Ember | warm sand, burnt-orange accent | `#c2410c` | `#fb923c` |
| Rose | blush neutrals, crimson-rose accent | `#cc2952` | `#fb7192` |

Rules every palette follows (enforced by `resources/js/lib/themes.test.ts` for all 14 palettes):
body and secondary text ≥ 7:1 on every ground, meta text ≥ 4.5:1, the accent ≥ 4.5:1 both as a
button fill (against its label) and as link text, rail text ≥ 7:1 / 4.5:1, status colors ≥ 4.5:1 on
surfaces and on their own tint. Dark palettes use a light accent with a dark label, so `text-primary`
stays legible.

Status colors are shared: success, warning, danger, info, defined per mode. Their backgrounds are
translucent tints (`color-mix` with transparent) so they sit correctly on any surface.

## UI styles

| | Minimal | Executive | Glass | Soft | Editorial |
|---|---|---|---|---|---|
| Shell | flush, hairline sidebar on canvas | deep command rail, grey canvas, white panels | translucent sidebar and header over an ambient field | inset workspace sheet on a tinted ground | flush on warm paper, fine page rule |
| Type | Inter, 13px UI | IBM Plex Sans, 14px | Geist, 14px | Figtree, 14px | Schibsted Grotesk UI, Source Serif 4 display |
| Controls | 32px, 6px radius | 36px, 5px | 36px, 9px | 38px, 10px | 36px, 3px |
| Cards | 10px, hairline, near-flat | 8px, border + crisp shadow | 14px, frosted, top highlight | 16px, borderless tinted wells | 4px, hairline, flat |
| Labels | normal case | uppercase, tracked | normal case | normal case | small caps |
| Badges | pill | rectangular | pill | pill | rectangular |

A style may restyle the sidebar as its own surface (Executive's rail). Generic tokens are
re-scoped inside `[data-sidebar='sidebar']` from inherited `--ui-island-*` values, which always
resolve from the nearest theming scope.

Glass applies `backdrop-filter` through ancestor selectors in `styles.css`; nothing else uses it.
Never add `backdrop-filter` to a surface generically: any value creates a containing block for
fixed descendants.

## Semantic tokens (use these in markup)

Colors: `background`, `foreground`, `foreground-secondary`, `muted-foreground`, `surface`,
`surface-secondary` (translucent hover/head tint), `surface-tertiary`, `card`, `popover`, `primary`
(+ `-hover`, `-active`, `-foreground`, `-subtle`), `secondary` (+ `-hover`), `muted`, `accent`,
`border`, `border-strong`, `input`, `ring`, `success|warning|danger|info` (+ `-background`),
`destructive`, `sidebar*`, `overlay`, `chart-1…5`.

Radii: `rounded-control`, `rounded-item`, `rounded-menu`, `rounded-card`, `rounded-dialog`,
`rounded-badge`; `rounded-xs…2xl` also follow the style.

Elevation: `shadow-control`, `shadow-card`, `shadow-popover`, `shadow-dialog` (`shadow-xs…xl`
map onto these).

Density (arbitrary-value utilities): `h-(--ui-control-h)`, `p-(--ui-card-pad)`,
`h-(--ui-row-h)`, `space-y-(--ui-section-gap)`, `h-(--ui-header-h)`, `h-(--ui-nav-h)`.

`dark:` is not used on token classes; tokens flip themselves. No raw hex in components.

## Roles

Type roles (in `app.css`, named `type-*` so tailwind-merge never mistakes them for colors):

- `type-title`: page titles (`PageHeader`, `Heading`, auth). Style picks face, size and weight.
- `font-heading`: section and card headings; add `font-semibold`.
- `type-figure`: metric values; display face, tabular lining numerals.
- `type-overline`: group labels, table headers, small section labels.

Surface roles for markup that cannot use a primitive:

- `panel`: card treatment (radius, border, background, shadow) for hand-built sections.
- `floating`: popover treatment for hand-built dropdowns and hover cards.
- `field`: input treatment for native `<select>` and `<input>` elements.

`cn()` (`lib/utils.ts`) is extended so `rounded-full` overrides `rounded-control`, `shadow-none`
overrides `shadow-card`, and so on.

## Components

Primitives in `components/ui` consume the tokens: Button (`default`, `secondary`, `outline`, `ghost`,
`destructive`, `link`; sizes follow `--ui-control-h*`), Badge (adds `neutral`, `primary`, `success`,
`warning`, `danger`, `info`), Card, Input, Textarea, Select, Dialog, Sheet (floats inset in Soft and
Glass), DropdownMenu, Popover, Tooltip, Switch, Checkbox, Skeleton, Alert, Sonner, Sidebar,
both DataTables.

Composites: `PageHeader` (title, description, `meta` and `actions` slots; Editorial draws a rule
under it), `EmptyState` (signal-ring icon, optional `bare` inside other panels, `description` slot),
`ErrorState`, `MetricCard` (`bare` for cells in a grouped strip, `tone="danger"`), `StatusBadge`,
`SetupChecklist`, `TextDivider`.

### Signal language

Status is never color alone. `StatusBadge` pairs a tinted badge, the label, and a leading signal
dot. States still in flight (queued, pending, processing, sending, provisioning, ringing,
in progress) pulse with `signal-live`, a slow ring that respects reduced motion. Empty states echo
the motif with faint concentric rings behind the icon. The caller-ID pill in the header carries a
steady success dot.

## Shell

`AppSidebar` (brand row at header height, onboarding progress ring, grouped nav with overline
labels, nested pages keep their section active), `AppHeader` (breadcrumbs, credits, caller ID,
`AppearanceMenu`, account menu; on mobile a title replaces breadcrumbs), `PageContainer`
(style-driven padding and section gap), `SetupBanner` (hidden on the dashboard and on the step's
own page). The `flush` layout (inbox) is bounded to the viewport and scrolls inside its panes.

## Personalization

- `AppearanceMenu` (header and auth pages): mode segmented control, theme swatches, style list,
  link to the full page. All three are keyboard-operable radio groups.
- `pages/settings/Appearance.vue`: radio-card groups for mode, theme and style. Every card is an
  `AppearancePreview`, a scaled miniature of the real shell drawn with the real tokens and
  primitives for that combination, so previews cannot drift from the app.
- Changes apply instantly through a View Transitions cross-fade (skipped under reduced motion).

Persistence: `appearance`, `ui_theme`, `ui_style` in localStorage and cookies (cookies are
excluded from encryption). `HandleAppearance` resolves cookies server-side through
`App\Enums\ColorTheme::resolve()` and `App\Enums\UiStyle::resolve()`; the root Blade template
renders `data-theme`, `data-style` and `.dark` before first paint.

Legacy values are migrated, never dropped: `ui_style` claymorphism → Soft, neumorphism → Minimal,
glassmorphism → Glass, skeuomorphism → Executive; old `design_system` palettes map to the theme
with the nearest accent hue (violets → Aurora, rose/coral → Rose, skyline → Midnight, greens and
teals → Emerald, warm palettes → Ember, neutral and editorial → Obsidian). A legacy UI type without
a saved palette maps to that material's palette.

## Motion

150–220 ms with `ease-ui` (`cubic-bezier(0.2, 0, 0, 1)`). Motion conveys state: menus fade and
scale from their origin, sheets slide, theme changes cross-fade, live statuses pulse. Global
`prefers-reduced-motion` handling collapses all of it.

## Accessibility

Contrast is tested per palette (above). Focus is a solid 2px ring with offset on buttons and a
border plus 3px halo on fields. Inputs keep 16px text on mobile. Selection groups are radio groups
with arrow-key navigation. Previews are `inert` and hidden from assistive technology.

## Extending

- **New theme:** add light and dark `--t-*` blocks to `themes.css`, add it to `COLOR_THEMES` and
  `COLOR_THEME_OPTIONS` (`lib/appearance.ts`), `App\Enums\ColorTheme`, the list in
  `resources/views/app.blade.php`, then run `npx vitest run resources/js/lib/themes.test.ts`.
- **New style:** add a `[data-style='…']` block overriding only what differs from the base block,
  then register it in the same four places as a theme. Check it in the Appearance page previews in
  both modes.
- **New screen:** compose `PageHeader`, primitives and roles. If you reach for a raw color, radius
  or shadow, a token or role is missing; add it here instead.
