# Theme System — Lvntr Starter Kit

This document covers:

- The `AppShell` / `AdminLayout` composition model and how to build a new layout
- The two-layer theme model: runtime kit themes (`main`, `aura`) and build-time custom slot-overrides
- The build-time resolver and full-replacement + fallback model for custom themes
- `VITE_SK_THEME` activation for custom themes
- Step-by-step recipe for adding a custom theme or overriding a single component's styles

---

## Layout architecture

### AppShell — structural shell

`resources/js/layouts/AppShell.vue` is the structural backbone of the admin panel. It owns:

- The `.admin-layout` root and `.admin-main` / `.admin-content` regions
- Sidebar collapse and mobile-open state via `useSidebar` (single owner — do not import `useSidebar` again in child components)
- Responsive class modifiers (`admin-main--expanded`, `admin-main--collapsed`, `admin-main--mobile`) on `.admin-main`

`AppShell` exposes five named slots:

| Slot | Receives (scoped) | Purpose |
|---|---|---|
| `#sidebar` | `{ collapsed, mobileOpen, isMobile, closeMobile }` | Sidebar region |
| `#header` | `{ collapsed, isMobile, toggle }` | Top header bar |
| `default` | — | Page content (`.admin-content` wrapper) |
| `#footer` | — | Footer strip |
| `#overlays` | — | Global overlays (dialogs, toasts, confirm) |

`AppShell` carries no page props, no flash-to-toast bridge, and no `<Head>`. It is a pure structural wrapper.

### AdminLayout — thin composition

`resources/js/layouts/AdminLayout.vue` wraps `AppShell` and fills every region with the standard admin components:

- `#sidebar` → `AdminSidebar`
- `#header` → `AdminHeader` (forwards dark-mode toggle)
- default → `AdminPageHeader` + `<slot />` (page content) + `<slot name="page-actions" />`
- `#footer` → `AdminFooter`
- `#overlays` → `ConfirmDialogComponent`, `ToastComponent`, `AppDialog`, `ImageLightbox`

It also handles the flash-to-toast bridge (`router.on('finish', …)`) and `<Head :title="title" />`.

**External contract (unchanged):**

```vue
<AdminLayout title="Users" subtitle="Manage users" :back-url="route('admin.users.index')">
    <template #page-actions>
        <Button label="New User" />
    </template>

    <!-- page content -->
</AdminLayout>
```

Props: `title?: string`, `subtitle?: string`, `backUrl?: string | boolean`.
Slots: `default` (page body), `page-actions` (rendered inside `AdminPageHeader`'s `#actions` slot).

All existing pages continue to import `@/layouts/AdminLayout.vue` — the public contract is identical.

### Building a new layout

To create a layout that shares the shell structure but arranges regions differently, compose `AppShell` directly:

```vue
<!-- resources/js/layouts/MinimalLayout.vue -->
<script setup lang="ts">
    import AppShell from '@/layouts/AppShell.vue';
    import MinimalSidebar from '@/layouts/components/MinimalSidebar.vue';
    import MinimalHeader from '@/layouts/components/MinimalHeader.vue';
</script>

<template>
    <AppShell>
        <template #sidebar="{ collapsed, mobileOpen, isMobile, closeMobile }">
            <MinimalSidebar
                :collapsed="collapsed"
                :mobile-open="mobileOpen"
                :is-mobile="isMobile"
                @close-mobile="closeMobile"
            />
        </template>
        <template #header="{ collapsed, isMobile, toggle }">
            <MinimalHeader :collapsed="collapsed" :is-mobile="isMobile" @toggle-sidebar="toggle" />
        </template>
        <slot />
    </AppShell>
</template>
```

Pages using the new layout import it by path — `AdminLayout` pages are unaffected.

---

## Two style layers

The theme system has two independent, complementary layers.

| Layer | Themes | How it activates | Resolver | Artifact |
|---|---|---|---|---|
| **Runtime kit themes** | `main`, `aura` | `appearance.theme` DB setting (Settings → Appearance) — takes effect instantly, no rebuild | `useTheme` composable sets `<html data-sk-theme>` at runtime | None — CSS always bundled |
| **Build-time custom themes** | Consumer-created `theme/<custom>/` directories | `VITE_SK_THEME=custom` in `.env` + `npm run build` | `sk-theme-build.mjs` (vendor-resident) writes `_active.css` | Generated file (`_active.css`), gitignored |

Additionally, the **PrimeVue preset layer** is keyed off `VITE_SK_THEME` at build time:

| Layer | What it covers | Resolver | Artifact |
|---|---|---|---|
| **PrimeVue preset** | Primary palette, surface colours, border radius, component tokens (`--p-*` variables) | alias `customResolver` in `vite.config.ts` (`resolveActivePreset`) | None — pure JS module resolution |

### Runtime kit themes — `main` and `aura`

Both `main` and `aura` are **always bundled** in every build. Switching between them requires no rebuild — the `useTheme` composable reads `appearance.theme` from the shared Inertia props and writes `<html data-sk-theme="aura">` (or removes the attribute for `main`) on mount and on every prop change.

- **`main`** — the default. No `data-sk-theme` attribute on `<html>`; all base rules apply.
- **`aura`** — activates when `data-sk-theme="aura"` is set on `<html>`. All aura rules are scoped to `html[data-sk-theme='aura']`, so they are inert until the attribute is present.

Switch from the admin **Settings → Appearance** panel; the change takes effect immediately and persists admin-wide. There is no per-user override.

### Build-time custom themes

Custom themes live in `resources/css/theme/<name>/` directories and use the full-replacement + fallback slot model — see [CSS theme system](#css-theme-system) below. They require `VITE_SK_THEME=<name>` in `.env` and a `npm run build` / `npm run dev` to become active. When a custom theme is selected in Settings → Appearance, a "rebuild required" hint is shown in the UI.

The two layers are independent: you can have `VITE_SK_THEME=custom` (build-time custom base) while the runtime attribute switches between `main` and `aura` visual styles. A `tokens.css` override typically reads the `--p-*` tokens the preset emits — see [Dependency chain](#dependency-chain-tokens-and-preset) below.

---

## CSS theme system

### Directory structure

```
resources/css/
├── theme/
│   ├── theme.css              # Entry: imports _active.css then theme-runtime/aura/aura.css
│   ├── _active.css            # GENERATED — do not edit; gitignored
│   ├── main/                  # Built-in base theme (source of truth for all slots)
│   │   ├── tokens.css         # CSS custom properties: layout dims, colours, shadows
│   │   ├── fonts.css          # Web font declarations
│   │   ├── _base.scss         # Base reset / typography
│   │   ├── layout/
│   │   │   ├── shell.css        # .admin-layout, .admin-main, Vue transitions
│   │   │   ├── sidebar.css      # .admin-sidebar*, .admin-overlay
│   │   │   ├── header.css       # .admin-header*
│   │   │   ├── page-header.css  # .admin-page-header*
│   │   │   └── footer.css       # .admin-footer*
│   │   ├── components/
│   │   │   ├── card.css
│   │   │   ├── button.css
│   │   │   ├── confirm.css
│   │   │   ├── datatable.css
│   │   │   ├── dialog.css
│   │   │   ├── editor.css
│   │   │   ├── formbuilder.css
│   │   │   ├── menus.css
│   │   │   ├── message.css
│   │   │   ├── navigation.css
│   │   │   ├── primevue.css
│   │   │   ├── tabs.css
│   │   │   ├── tag.css
│   │   │   └── toast.css
│   │   ├── _auth.scss         # Auth layout styles
│   │   └── utilities.css      # Tailwind utility overrides
│   └── custom/                # Your override theme — not shipped; you create it
│       └── (create as needed — see recipe below)
└── theme-runtime/
    └── aura/                  # Runtime kit theme — always bundled, scoped to html[data-sk-theme='aura']
        ├── aura.css           # Index: imports the four files below in order
        ├── tokens.css         # Frame/panel/sidebar tokens (scoped + dark + sidebar variants)
        └── layout/
            ├── shell.css        # Brand-colored frame + inset rounded panel
            ├── sidebar.css      # Static sidebar on the frame, version chip, mobile drawer
            └── header.css       # 70px panel header, solid dev/debug tags
```

> The kit no longer ships an empty `custom/` directory. If you want to override any slot, create the directory yourself first (see recipes below).

> `theme-runtime/aura/` is not a build-time slot directory. It is not read by the resolver (`sk-theme-build.mjs`) and setting `VITE_SK_THEME=aura` has no effect — `aura` activates at runtime via the `data-sk-theme` attribute only.

### Built-in `aura` theme

`aura` is the kit's second built-in theme. It restyles the admin shell as an **inset, rounded panel inside a brand-colored frame**: the sidebar sits directly on the frame (white active pill in the colored style), and the header/content/footer share the panel surface. It overrides only four visual areas — `tokens.css`, `layout/shell.css`, `layout/sidebar.css`, `layout/header.css` — everything else (navigation, footer, page-header, all components) falls back to `main` and is repainted purely by the tokens.

The frame color follows the active PrimeVue primary (`--p-primary-800`), so the existing accent picker recolors the whole frame; the sidebar style switch (`colored` / `light`) and dark mode are fully supported.

**Activating `aura`:** open admin **Settings → Appearance** and select the Aura theme card. The change applies instantly — no build step required. The setting is admin-wide (stored in DB as `appearance.theme`).

`aura`'s CSS lives in `resources/css/theme-runtime/aura/` and is **always bundled** alongside `main`. All its rules are scoped to `html[data-sk-theme='aura']` so they are completely inert until the runtime attribute is present. The `useTheme` composable (called from `AdminLayout`) writes and removes this attribute in response to the Inertia shared `appearance.theme` prop.

> Do not set `VITE_SK_THEME=aura` — that variable controls the build-time custom-theme resolver only, and `aura` is not a slot-based theme. Setting it has no effect on `aura`'s runtime behavior and may produce unexpected results from the resolver.

### How the resolver works

`sk-theme-build.mjs` is a kit-managed build script shipped inside the package (vendor-resident at `vendor/lvntr/laravel-starter-kit/resources/js/theme/sk-theme-build.mjs`). You do not own or edit this file — only theme/slot/preset files in your `resources/` directory are yours to customise. The script is invoked by the `skTheme()` Vite plugin automatically during `dev` and `build`, and it writes `theme/_active.css`. The model is **full-replacement + fallback**:

1. `main/` is the canonical slot list and import order.
2. For each slot, the resolver loads `<active>/<slot>` **if the file exists**; otherwise it loads `main/<slot>`.
3. The result is a single `_active.css` with one `@import` per slot in the correct cascade order.

If `custom/` does not exist at all, the build still completes — every slot falls through to `main`.

Import order (preserved from the original cascade):

```
tokens → fonts → _base → layout/* → components/* → _auth → utilities
```

Every layer is a slot. A custom theme that ships only `components/datatable.css` overrides just the datatable. Everything else falls through to `main`. A file in `custom/` whose path does not match any `main/` slot is ignored — the resolver iterates `main`'s slot list, not the custom directory.

Note: `_base.scss` and `_auth.scss` are `.scss` files — their extension is preserved. The resolver is extension-aware; Sass compilation is handled by the same pipeline as before.

### `VITE_SK_THEME` activation

`VITE_SK_THEME` controls the **build-time custom theme resolver only**. It has no effect on the runtime kit themes (`main`, `aura`) — those are always bundled and activated via the `data-sk-theme` attribute at runtime.

Set the active custom theme in `.env`:

```dotenv
VITE_SK_THEME=custom
```

The default is `main`. If the variable is absent or blank, `main` is used — i.e. every slot resolves to `main/` and no custom files are loaded. Run `npm run dev` or `npm run build` — the `skTheme()` Vite plugin guarantees the resolver runs and regenerates `_active.css` before Vite processes any assets. This approach is safe under `ignore-scripts=true` because it does not rely on npm lifecycle hooks.

To preview the resolved manifest without a full build, use the `theme:build` npm script (which runs the vendor script directly):

```bash
npm run theme:build
# [sk-theme-build] → resources/css/theme/_active.css (25 slots, 1 override)
```

Or invoke the vendor script directly:

```bash
node vendor/lvntr/laravel-starter-kit/resources/js/theme/sk-theme-build.mjs
```

Open `resources/css/theme/_active.css` to see exactly which file each slot resolved to. Overridden slots are annotated with `/* override */`.

### `_active.css` — generated artifact

`_active.css` is a **build artifact**:

- Listed in `.gitignore` — never commit it.
- Not hash-tracked by `sk:update` — it is regenerated on every `npm run dev` / `npm run build`.
- Always present before Vite starts because the `skTheme()` plugin invokes the resolver before Vite processes assets.

---

## Custom theme recipe

### Override a single component

To restyle the datatable without touching anything else:

1. Create the custom directory and copy the slot you want to override:

   ```bash
   mkdir -p resources/css/theme/custom/components
   cp resources/css/theme/main/components/datatable.css \
      resources/css/theme/custom/components/datatable.css
   ```

2. Edit `custom/components/datatable.css`. Keep the same class names — the Vue components target them.

3. Set `VITE_SK_THEME=custom` in `.env`.

4. Run `npm run dev`. The `skTheme()` plugin regenerates `_active.css` before Vite starts; `custom/components/datatable.css` replaces `main/components/datatable.css` in the import list. All other slots remain from `main`.

Verify the resolved manifest:

```
@import './main/tokens.css';
@import './main/fonts.css';
@import './main/_base.scss';
@import './main/layout/footer.css';
…
@import './custom/components/datatable.css'; /* override */
@import './main/components/dialog.css';
…
@import './main/_auth.scss';
@import './main/utilities.css';
```

### Override the token set

To change layout dimensions, colours, or shadows globally, override `tokens.css`:

```bash
mkdir -p resources/css/theme/custom
cp resources/css/theme/main/tokens.css \
   resources/css/theme/custom/tokens.css
```

Edit the custom properties. All layout regions and components read from these tokens, so a token change propagates everywhere without touching individual component files.

### Override a layout region

```bash
mkdir -p resources/css/theme/custom/layout
cp resources/css/theme/main/layout/sidebar.css \
   resources/css/theme/custom/layout/sidebar.css
```

Edit as needed. Only the sidebar slot is replaced; the rest of the layout comes from `main`.

### Override fonts or utilities

The font declarations and Tailwind utility overrides are now slots like any other. To swap in your own fonts without touching layout or components:

```bash
mkdir -p resources/css/theme/custom
cp resources/css/theme/main/fonts.css \
   resources/css/theme/custom/fonts.css
```

Edit the `@font-face` declarations in `custom/fonts.css`. On the next build, `_active.css` will use your font file instead of the `main` version; all other slots remain from `main`.

Similarly, to override Tailwind utility overrides:

```bash
mkdir -p resources/css/theme/custom
cp resources/css/theme/main/utilities.css \
   resources/css/theme/custom/utilities.css
```

Because `utilities.css` is unlayered and emitted last in the cascade, it wins over every layered rule — the same precedence behaviour as before. Edit freely.

### Override auth styles

```bash
mkdir -p resources/css/theme/custom
cp resources/css/theme/main/_auth.scss \
   resources/css/theme/custom/_auth.scss
```

Edit `custom/_auth.scss`. The `.scss` extension is required — the resolver and Sass compiler are extension-aware.

### Complete slot reference

| Slot | File | Notes |
|---|---|---|
| tokens | `tokens.css` | CSS custom properties (light + dark) |
| fonts | `fonts.css` | `@font-face` declarations |
| base | `_base.scss` | Reset / typography; `.scss` extension required |
| layout/footer | `layout/footer.css` | `.admin-footer*` |
| layout/header | `layout/header.css` | `.admin-header*` |
| layout/page-header | `layout/page-header.css` | `.admin-page-header*` |
| layout/shell | `layout/shell.css` | `.admin-layout`, `.admin-main`, Vue transitions |
| layout/sidebar | `layout/sidebar.css` | `.admin-sidebar*`, `.admin-overlay` |
| components/button | `components/button.css` | Button severity palette (Tailwind colors) |
| components/card | `components/card.css` | |
| components/confirm | `components/confirm.css` | |
| components/datatable | `components/datatable.css` | |
| components/dialog | `components/dialog.css` | |
| components/editor | `components/editor.css` | |
| components/formbuilder | `components/formbuilder.css` | |
| components/menus | `components/menus.css` | |
| components/navigation | `components/navigation.css` | |
| components/page-loader | `components/page-loader.css` | Full-screen page-switch loading overlay |
| components/primevue | `components/primevue.css` | |
| components/tabs | `components/tabs.css` | |
| components/tag | `components/tag.css` | |
| components/toast | `components/toast.css` | |
| auth | `_auth.scss` | Auth layout styles; `.scss` extension required |
| utilities | `utilities.css` | Tailwind utility overrides; unlayered, emitted last |

### Notes

- A custom theme replaces slots **whole-file** — there is no cascading diff. Copy the `main` file as a starting point.
- Custom themes cannot add new slots. A file in `custom/` that has no matching path in `main/` is never imported.
- `VITE_SK_THEME=main` (the default) produces a build byte-identical to the stock panel — no custom files are loaded.
- The kit does not ship a `custom/` directory. Create it yourself before adding overrides; if it does not exist, every slot resolves to `main`.
- Remove `custom/` files you no longer need; the resolver falls back to `main` automatically.
- Slots with a `.scss` extension (`_base.scss`, `_auth.scss`) must keep that extension in `custom/` too.

---

## PrimeVue preset layer

### How it works

`resources/js/theme/preset.ts` is the base PrimeVue styled-mode preset — it defines the primary palette, surface colours, border radius, and per-component tokens. `app.ts` imports it as `@/theme/preset`. The import path never changes.

The alias `customResolver` in `vite.config.ts` — the `resolveActivePreset` helper exported from the kit's vendor-resident `vite-plugin-sk-theme.mjs` — intercepts the `@/theme/preset` specifier at build time:

- If `VITE_SK_THEME` is not `main` **and** `resources/js/theme/<active>/preset.ts` exists → resolves to that override file.
- Otherwise → resolves to the base `resources/js/theme/preset.ts`.

The base file stays where it has always been — the kit never moves it because it is the most commonly customised file in a consumer project.

### Directory layout

```
resources/js/theme/
├── preset.ts              # Base preset (consumer-customizable; stays here)
└── custom/                # Not shipped — you create this when needed
    └── preset.ts          # (you create this — see recipe below)
```

The `custom/` directory is not shipped by the kit. The `custom` theme's preset is absent by default, so the base preset is used and the PrimeVue look is byte-identical to the stock panel. Create the directory only when you need a theme-specific preset override.

### Give a custom theme its own PrimeVue palette

1. Create `resources/js/theme/custom/preset.ts` (create the `custom/` directory first if it does not exist). The simplest approach is to import the base preset and override only the palette:

   ```bash
   mkdir -p resources/js/theme/custom
   ```

   ```ts
   import { definePreset } from '@primevue/themes';
   import Material from '@primevue/themes/material';
   import AppPreset from '../preset';

   export default definePreset(Material, {
       ...AppPreset,
       semantic: {
           ...AppPreset.semantic,
           primary: {
               50:  '{emerald.50}',
               100: '{emerald.100}',
               200: '{emerald.200}',
               300: '{emerald.300}',
               400: '{emerald.400}',
               500: '{emerald.500}',
               600: '{emerald.600}',
               700: '{emerald.700}',
               800: '{emerald.800}',
               900: '{emerald.900}',
               950: '{emerald.950}',
           },
       },
   });
   ```

2. Set `VITE_SK_THEME=custom` in `.env`.

3. Run `npm run dev` or `npm run build`. The plugin resolves `@/theme/preset` to your override; with no override file present it falls back to the base.

### Notes

- Export the preset object as the **default export** — this is what `app.ts` passes to PrimeVue's `preset` option.
- The override applies only when `VITE_SK_THEME=custom` and `resources/js/theme/custom/preset.ts` exists. Any other value falls back to the base.
- The kit does not ship a `custom/` directory under `resources/js/theme/`. Create it yourself when you need a theme-specific preset.
- There is no generated artifact. This is pure module resolution — no `_active` file to gitignore, no npm chain to maintain.

### Dependency chain — tokens and preset

`resources/css/theme/main/tokens.css` (and any `custom/tokens.css` override) defines the `--admin-*` custom properties the layout and components use. These are **live references** to the `--p-*` variables the PrimeVue preset emits at runtime:

```css
/* tokens.css — admin roles point at PrimeVue's preset output */
--admin-sidebar-bg: var(--p-surface-900);
--admin-sidebar-item-active-bg: var(--p-primary-color);
```

Because they are live `var()` references, changing the primary/surface palette in your preset override **flows through automatically** — the `--admin-*` roles re-resolve to the new `--p-*` values; there is nothing to keep "in sync".

Override `tokens.css` only if you want to re-map which PrimeVue token an admin role points at (e.g. make the sidebar read a different surface step):

```bash
mkdir -p resources/css/theme/custom
cp resources/css/theme/main/tokens.css \
   resources/css/theme/custom/tokens.css
```

Then edit the `--admin-*` properties to reference your chosen `--p-*` tokens.

---

## Accent color system

The admin panel supports a runtime accent color that repaints the PrimeVue primary palette and the sidebar surface without any rebuild. The system has two layers: an **admin global default** (set in Settings → Appearance) and an optional **per-user override** (set from the header popover). When a user has no personal override, the admin global default applies; when the admin global default is also `'default'`, the kit primary color is used (blue under `main`, indigo under `aura`).

### How it works

`useAccentColor` (vendor-resident composable, `resources/js/composables/useAccentColor.ts`) drives the accent. On `onMounted` and on watch it calls:

1. **`updatePrimaryPalette(palette)`** — PrimeVue's runtime palette swap. Replaces the active `--p-primary-*` CSS custom properties with the chosen Tailwind v4 oklch scale; buttons, links, focus rings, active states, and every `--p-primary-color` reference update instantly with no rebuild.
2. **`data-sk-accent` on `<html>`** — a marker used by `tokens.css` to apply the deep accent tint to the sidebar surface in light mode. In dark mode, the sidebar always stays the neutral dark surface; only active items and buttons carry the accent color. The marker is absent when the accent is `'default'`.

The sidebar surface treatment is independent of the accent: the `sidebarStyle` value (`'colored'` | `'light'`) controls a `data-sk-sidebar` marker on `<html>`. When `data-sk-sidebar="light"` is present, the sidebar shows a white/light surface with dark text; when absent (the `'colored'` default), the deep accent tint (or neutral dark in dark mode) applies.

### Available colors

`ACCENT_COLORS` lists all 26 selectable names: the 22 standard Tailwind v4 palettes (`slate`, `gray`, `zinc`, `neutral`, `stone`, `red`, `orange`, `amber`, `yellow`, `lime`, `green`, `emerald`, `teal`, `cyan`, `sky`, `blue`, `indigo`, `violet`, `purple`, `fuchsia`, `pink`, `rose`) plus four custom muted tones (`taupe`, `mauve`, `mist`, `olive`). The special value `'default'` means "use the admin global default" (or fall back to the kit primary if the global is also `'default'`).

All palette values are Tailwind v4 oklch scales inlined in the composable — they are not read from Tailwind's CSS variables at runtime, because Tailwind tree-shakes unused `--color-*` variables.

### Persistence

Both accent choice and sidebar style are persisted in `localStorage`:

| Key | Default | Meaning |
|---|---|---|
| `admin-accent-color` | `'default'` | Per-user accent choice; `writeDefaults: false` (seed value is not written until the user makes an explicit pick) |
| `admin-sidebar-style` | `'colored'` | Per-user sidebar surface treatment |

The `writeDefaults: false` option means `localStorage` is empty for a first-time user, so the admin global default is always live until the user explicitly picks a color. One-time legacy storage cleanup (`migrateLegacyAppearanceStorage`) removes any stale `'default'` / `false` / `'colored'` seeds written by older builds that used eager `writeDefaults: true`.

### Admin global default vs per-user override

The `appearance` shared prop (provided by `HandleInertiaRequests` on every Inertia response) carries:

| Field | Type | Description |
|---|---|---|
| `accent_color` | `string` | Admin global default accent name, or `'default'` |
| `sidebar_style` | `'colored' \| 'light'` | Admin global default sidebar style |
| `dark_mode_default` | `boolean` | Admin global dark mode default |
| `theme` | `string` | Active runtime theme (`'main'` or `'aura'`) |

`useAppearanceDefaults` reads this prop. `useAccentColor` consumes `defaultAccent` and `defaultSidebarStyle` from it to seed initial values when the user has made no personal choice.

Configure the global default from **Settings → Appearance → Default Color**. The admin-side picker shows a live preview: selecting a swatch calls `applyAccent(color, { followGlobal: false })`, which treats `'default'` as the kit primary (because the admin is defining the default and must see what kit-blue looks like). On leaving the tab without saving, the user's own session accent is reinstated.

The header popover available to every admin user shows the same color grid plus a "Default" swatch. Picking a color calls `setAccent(color)` which persists to `localStorage`; the watch on `accent` then calls `applyAccent` automatically. Choosing "Default" clears the personal override so the admin global default applies again.

### Theme interaction

- **`main` default accent:** blue (`{blue.x}` token references, resolved against the Material preset).
- **`aura` default accent:** indigo (Tailwind v4 oklch scale). When the runtime theme switches between `main` and `aura`, `useAccentColor` re-applies the palette automatically (a `watch` on `appearance.theme` triggers `applyAccent`) so a `'default'` accent picks the correct signature color for the active theme.
- The aura frame color (`--p-primary-800`) follows the active primary, so the accent picker recolors the entire frame under `aura`.

### Page-loader accent

`SkPageLoader` — the full-screen animated overlay shown during Inertia page switches — uses `--p-primary-color` for brand elements (rays, blobs, bouncing dots, wave-peak letter color). Because `updatePrimaryPalette` updates `--p-primary-color` at runtime, the page-loader automatically uses whatever accent is active. The overlay background is theme-driven:

- **`main`:** `--admin-sidebar-bg` (the dark sidebar surface).
- **`aura`:** `--p-surface-900` (light) / `--p-surface-950` (dark) — a neutral dark surface, because `aura`'s `--admin-sidebar-bg` is transparent.

Styles live in the `components/page-loader.css` theme slot (overrideable via a custom theme). The overlay is SSR-safe and honors `prefers-reduced-motion`. It is **opt-in** — mount `<SkPageLoader/>` in your `AdminLayout` overlays slot to enable it (see [ui-components.md](https://starter-kit.lvntr.dev/docs/ui-components) — SkPageLoader).

---

## Dark mode

CSS custom properties for dark mode are declared inside `.dark { … }` blocks in `main/tokens.css` (and in the per-region layout files where layout-specific dark overrides exist). If you override `tokens.css`, copy those `.dark` blocks too.

Dark mode is toggled by `useDarkMode` — it adds / removes the `dark` class on `<html>`. No build step or separate CSS file is needed.

The `aura` theme's dark-mode rules are declared as `html.dark[data-sk-theme='aura'] { … }` blocks inside `theme-runtime/aura/tokens.css`. Both attributes are independent — dark mode and the theme switch compose correctly in all four combinations (`light/dark` × `main/aura`).

---

## Related docs

- `docs/install.md` — first-time setup
- `docs/update.md` — `sk:update` and hash-tracked stubs
- `docs/UPGRADE.md` — migration guide for existing projects
