# Themes & Branding (DB-driven, Tailwind v4) — Canonical Reference

> **Reference for `application`, `ui-components`, `apex` skills** — the COMPLETE token map any generated UI must respect. SmartStack v3.46+ is theme-driven: every color, radius, shadow, and font in a generated component MUST come from one of the CSS variables below. Hardcoded Tailwind colors break dark mode and per-tenant branding.

## When This Reference Applies

- The client wants several themes (light/dark + custom)
- The client wants per-tenant branding (logo, favicon, colors)
- You generate UI code that needs to consume theme tokens
- The user asks "how do I add a custom theme", "how do I change the logo per tenant", "what about Tailwind colors"

---

## TL;DR — 5 canonical rules

1. **Always use CSS variables for color/border/radius/shadow.** `bg-[var(--bg-card)]` — not `bg-white`.
2. **Never use `dark:` prefix.** SmartStack toggles a `.dark` class on `<html>`; CSS vars adapt automatically.
3. **Status badges use the 4 status token sets** (`--success-*`, `--warning-*`, `--error-*`, `--info-*`).
4. **Action buttons use the accent tokens** (`--color-accent-50..950` or `--accent-*`).
5. **Reusable component classes go in `@layer components`** (one definition, not duplicated in every component).

---

## Section 1 — CSS Variables (canonical token map)

All variables are defined on `:root` (light defaults) and overridden by `.dark` for dark mode. They are exported by `@atlashub/smartstack/theme.css` (auto-imported when you import the SDK).

### 1.1 Backgrounds

| Variable | Light | Dark | Use case |
|---|---|---|---|
| `--bg-app` | `#fafafa` | `#161618` | App root background, `<body>` |
| `--bg-primary` | `#ffffff` | `#1c1c1f` | Main containers, cards, page bodies |
| `--bg-secondary` | `#f4f4f5` | `#232326` | Secondary zones, hover surfaces |
| `--bg-tertiary` | `#e4e4e7` | `#2a2a2e` | Tertiary layers, distinct from secondary |
| `--bg-card` | `#ffffff` | `#1c1c1f` | Explicit card wrappers |
| `--bg-hover` | `#f4f4f5` | `#27272a` | Hover state on surfaces |
| `--bg-active` | `#e4e4e7` | `#3f3f46` | Active / selected state |

### 1.2 Text

| Variable | Light | Dark | Use case |
|---|---|---|---|
| `--text-primary` | `#18181b` | `#e4e4e7` | Body text, headings |
| `--text-secondary` | `#52525b` | `#a1a1aa` | Subtitles, helper text |
| `--text-tertiary` | `#71717a` | `#71717a` | Hints, metadata |
| `--text-muted` | `#a1a1aa` | `#52525b` | Very muted text |
| `--text-inverse` | `#fafafa` | `#e4e4e7` | Text on dark surface (fallback) |

### 1.3 Borders

| Variable | Light | Dark | Use case |
|---|---|---|---|
| `--border-color` | `#e4e4e7` | `#3f3f46` | Standard border |
| `--border-subtle` | `#f4f4f5` | `#27272a` | Very light divider |
| `--border-strong` | `#d4d4d8` | `#52525b` | Strong divider |
| `--border-width` | `1px` | `1px` | Border thickness (overridable per theme) |

### 1.4 Status (4 sets — canonical for badges/banners/toasts)

| Variable | Light | Dark | Use case |
|---|---|---|---|
| `--success-bg` | `#f0fdfa` | `rgba(20,184,166,0.15)` | Success badge / banner background |
| `--success-text` | `#0f766e` | `#5eead4` | Success label text |
| `--success-border` | `#99f6e4` | `rgba(20,184,166,0.3)` | Success border |
| `--success-dot` | `#14b8a6` | `#14b8a6` | Status dot (always vivid) |
| `--warning-bg` | `#fffbeb` | `rgba(245,158,11,0.15)` | Warning bg |
| `--warning-text` | `#b45309` | `#fbbf24` | Warning text |
| `--warning-border` | `#fde68a` | `rgba(245,158,11,0.3)` | Warning border |
| `--warning-dot` | `#f59e0b` | `#f59e0b` | Warning dot |
| `--error-bg` | `#fff1f2` | `rgba(244,63,94,0.15)` | Error bg |
| `--error-text` | `#be123c` | `#fb7185` | Error text |
| `--error-border` | `#fecdd3` | `rgba(244,63,94,0.3)` | Error border |
| `--error-dot` | `#f43f5e` | `#f43f5e` | Error dot |
| `--info-bg` | `#f0f9ff` | `rgba(14,165,233,0.15)` | Info bg |
| `--info-text` | `#0369a1` | `#7dd3fc` | Info text |
| `--info-border` | `#bae6fd` | `rgba(14,165,233,0.3)` | Info border |
| `--info-dot` | `#0ea5e9` | `#0ea5e9` | Info dot |

### 1.5 Accent (dynamic — set by ThemeContext from `selectedPreset.accentColorKey`)

| Variable | Default light (indigo) | Default dark (indigo) | Use case |
|---|---|---|---|
| `--color-accent-50` | `#eef2ff` | `#1e1b4b` | Lightest accent (badge bg) |
| `--color-accent-100` | `#e0e7ff` | `#312e81` | |
| `--color-accent-200` | `#c7d2fe` | `#3730a3` | |
| `--color-accent-300` | `#a5b4fc` | `#4338ca` | |
| `--color-accent-400` | `#818cf8` | `#4f46e5` | |
| `--color-accent-500` | `#6366f1` | `#6366f1` | Primary accent (icons, links) |
| `--color-accent-600` | `#4f46e5` | `#818cf8` | Action button bg |
| `--color-accent-700` | `#4338ca` | `#a5b4fc` | Hover for action button |
| `--color-accent-800` | `#3730a3` | `#c7d2fe` | |
| `--color-accent-900` | `#312e81` | `#e0e7ff` | |
| `--color-accent-950` | `#1e1b4b` | `#eef2ff` | Darkest accent |
| `--accent-bg` | `#eef2ff` | `rgba(99,102,241,0.15)` | Accent badge / hover background (semantic alias) |
| `--accent-text` | `#4338ca` | `#a5b4fc` | Accent label text |
| `--accent-border` | `#c7d2fe` | `rgba(99,102,241,0.3)` | Accent border |

### 1.6 Border-radius (overridable per theme)

| Variable | Default | Use case |
|---|---|---|
| `--radius-card` | `12px` | Card corners |
| `--radius-button` | `8px` | Button corners |
| `--radius-badge` | `8px` | Badge / chip corners |
| `--radius-input` | `8px` | Input field corners |
| `--radius-modal` | `16px` | Modal corners |
| `--radius-menu-item` | `8px` | Sidebar / menu item corners |

### 1.7 Item palette (depends on `selectedPreset.itemPaletteKey`)

| Variable | neutral light | neutral dark | Use case |
|---|---|---|---|
| `--item-color-light` | `#ffffff` | `#1c1c1f` | Item lightest tint |
| `--item-color-medium` | `#f4f4f5` | `#27272a` | Item medium tint |
| `--item-color-dark` | `#e4e4e7` | `#3f3f46` | Item darkest tint |
| `--item-color-border` | `#d4d4d8` | `#52525b` | Item border |

### 1.8 Typography & shadows

| Variable | Default | Use case |
|---|---|---|
| `--font-sans` | `'Inter', system-ui, -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif` | Default font stack |
| `--shadow-sm` | `0 1px 2px 0 rgba(0,0,0,0.05)` | Subtle elevation |
| `--shadow-md` | `0 4px 6px -1px rgba(0,0,0,0.1)` | Medium elevation |
| `--shadow-lg` | `0 10px 15px -3px rgba(0,0,0,0.1)` | Large elevation |
| `--shadow-xl` | `0 20px 25px -5px rgba(0,0,0,0.1)` | Extra-large elevation |

### 1.9 Tooltip variants

| Variable | Light | Dark | Use case |
|---|---|---|---|
| `--tooltip-info-bg` | `#eff6ff` | `#1a2c3b` | Info tooltip bg |
| `--tooltip-success-bg` | `#f0fdf4` | `#1a3329` | Success tooltip bg |
| `--tooltip-warning-bg` | `#fefce8` | `#3b2f1a` | Warning tooltip bg |
| `--tooltip-error-bg` | `#fef2f2` | `#3b1c1e` | Error tooltip bg |

---

## Section 2 — TypeScript tokens

### 2.1 `ACCENT_COLORS` (13 palettes)

Defined in `web/smartstack-web/src/config/themePresets.ts`. Each palette exposes 11 shades (50, 100, 200, 300, 400, 500, 600, 700, 800, 900, 950).

**Neutrals:** `slate`, `gray`, `zinc`, `stone`
**Vibrants:** `indigo` (default primary), `violet`, `blue`, `cyan`, `teal`, `emerald`, `amber`, `orange`, `rose`, `pink`

```ts
ACCENT_COLORS: Record<string, { name: string; shades: ColorShades }>
ColorShades = { 50, 100, 200, 300, 400, 500, 600, 700, 800, 900, 950 }
```

The user-selected key is stored in `UiPreset.accentColorKey` and applied at runtime by `ThemeContext.applyAccent()`, which writes the 11 shades to `--color-accent-50..950`.

### 2.2 `ITEM_PALETTES` (6 palettes)

| Key | Label FR | Use case |
|---|---|---|
| `neutral` | Neutre | Default — CRUD tables, cards |
| `ocean` | Océan | Soft blues |
| `forest` | Forêt | Natural greens |
| `sunset` | Coucher de soleil | Warm oranges/amber |
| `lavender` | Lavande | Elegant purples |
| `rose` | Rose | Soft pinks |

Each palette ships `light` and `dark` color sets `{ light, medium, dark, border }`.

### 2.3 `BORDER_RADIUS_VALUES` (6 presets)

| Key | px | Label FR |
|---|---|---|
| `none` | `0px` | Carré |
| `small` | `4px` | Léger |
| `medium` | `8px` | Moyen (default for buttons/badges) |
| `large` | `12px` | Arrondi (default for cards) |
| `xlarge` | `16px` | Très arrondi (default for modals) |
| `xxlarge` | `24px` | Pill |

### 2.4 `UiTheme` JSON shape (backend → frontend contract)

```ts
interface UiTheme {
  backgroundsJson: string;         // { app, primary, secondary, tertiary, card, hover, active }
  textColorsJson: string;          // { primary, secondary, tertiary, muted, inverse }
  borderColorsJson: string;        // { default, subtle, strong, width: number }
  statusColorsJson: string;        // { success/warning/error/info: { bg, text, border, dot } }
  defaultBorderRadiusJson: string; // { card, button, badge, input, modal, menuItem }
  defaultAccentColorKey: string;   // e.g. "indigo" | hex like "#ff00aa"
  defaultItemPaletteKey: string;   // "neutral" | "ocean" | ...
}
interface UiPreset {
  accentColorKey: string;          // overrides theme.defaultAccentColorKey
  itemPaletteKey: string | null;   // overrides theme.defaultItemPaletteKey
  borderRadiusJson: string | null; // overrides theme.defaultBorderRadiusJson
  backgroundOverrideJson: string | null;
}
```

---

## Section 3 — Tailwind v4 setup (mandatory in client projects)

### 3.1 `package.json`

```json
{
  "dependencies": {
    "tailwindcss": "^4.1.18",
    "@tailwindcss/vite": "^4.1.18"
  }
}
```

### 3.2 `vite.config.ts`

```ts
import tailwindcss from '@tailwindcss/vite';
import react from '@vitejs/plugin-react';
import { defineConfig } from 'vite';

export default defineConfig({
  plugins: [react(), tailwindcss()],
});
```

### 3.3 `index.css` (minimum)

```css
@import "tailwindcss";
@source "../node_modules/@atlashub/smartstack";
@custom-variant dark (&:where(.dark, .dark *));

@theme {
  --color-primary-50: #eef2ff;
  --color-primary-100: #e0e7ff;
  /* ... 9 shades total ... */
  --color-primary-950: #1e1b4b;

  --color-accent-50: #f5f3ff;
  /* ... 9 shades ... */
  --color-accent-950: #2e1065;

  --font-sans: 'Inter', system-ui, -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
}

/* Then optionally @layer components { .card { ... } } for shared classes */
```

> Tailwind v4 has **no `tailwind.config.ts`** — tokens go in `@theme {}` blocks inside CSS.

### 3.4 Dark mode

```css
:root { /* light defaults already declared */ }

.dark {
  color-scheme: dark;
  --bg-app: #161618;
  --text-primary: #e4e4e7;
  /* ... overrides for all variables ... */
}
```

Toggle from JS:

```ts
document.documentElement.classList.toggle('dark', wantDark);
```

`ThemeContext` does this automatically based on the luminance of `--bg-app` — never write the toggle yourself in components.

---

## Section 4 — className conventions (constated patterns)

### Pattern A — Tailwind utility + CSS var (most common)

```tsx
<p className="text-[var(--text-secondary)]">{subtitle}</p>
<div className="bg-[var(--bg-primary)]/80 backdrop-blur-lg border-b border-[var(--border-color)]" />
<button className="bg-[var(--color-accent-600)] hover:bg-[var(--color-accent-700)] text-white px-4 py-2 rounded-[var(--radius-button)]">Save</button>
```

### Pattern B — Custom @layer components class

```css
@layer components {
  .card {
    background: var(--bg-card);
    border: 1px solid var(--border-color);
    border-radius: var(--radius-card);
    box-shadow: var(--shadow-sm);
  }
  .badge-success {
    background: var(--success-bg);
    color: var(--success-text);
    border: 1px solid var(--success-border);
    border-radius: var(--radius-badge);
    padding: 0.125rem 0.5rem;
    font-size: 0.75rem;
  }
}
```

```tsx
<div className="card">...</div>
<span className="badge-success">OK</span>
```

### Pattern C — Inline style for dynamic gradients

```tsx
const accentGradient: React.CSSProperties = {
  background: 'linear-gradient(to bottom right, var(--color-accent-500), var(--color-accent-600))',
};
<div style={accentGradient}>{initials}</div>
```

### Pattern D — Variant mapping (modal/banner color)

```tsx
const VARIANT_CONFIG = {
  danger:  { iconColor: 'text-[var(--error-text)]',   buttonColor: 'bg-[var(--error-text)] hover:opacity-90',         iconBg: 'bg-[var(--error-bg)]' },
  warning: { iconColor: 'text-[var(--warning-text)]', buttonColor: 'bg-[var(--warning-text)] hover:opacity-90',       iconBg: 'bg-[var(--warning-bg)]' },
  info:    { iconColor: 'text-[var(--info-text)]',    buttonColor: 'bg-[var(--color-accent-600)] hover:bg-[var(--color-accent-700)]', iconBg: 'bg-[var(--info-bg)]' },
};
```

---

## Section 5 — Migration map (Tailwind hardcoded → CSS var)

Use this table when fixing a generated file that violates the theme.

| Hardcoded Tailwind | SmartStack CSS var |
|---|---|
| `bg-white` | `bg-[var(--bg-card)]` |
| `bg-gray-50` | `bg-[var(--bg-primary)]` |
| `bg-gray-100` | `bg-[var(--bg-secondary)]` |
| `bg-gray-200` | `bg-[var(--bg-tertiary)]` |
| `text-gray-900` | `text-[var(--text-primary)]` |
| `text-gray-700` | `text-[var(--text-secondary)]` |
| `text-gray-500` | `text-[var(--text-tertiary)]` |
| `text-gray-400` | `text-[var(--text-muted)]` |
| `border-gray-200` | `border-[var(--border-color)]` |
| `border-gray-100` | `border-[var(--border-subtle)]` |
| `bg-blue-500/10` (info badge) | `bg-[var(--info-bg)]` |
| `text-blue-600` (info text) | `text-[var(--info-text)]` |
| `bg-blue-600` (action button) | `bg-[var(--color-accent-600)]` |
| `text-blue-600` (link) | `text-[var(--color-accent-600)]` |
| `bg-red-500/10` | `bg-[var(--error-bg)]` |
| `text-red-500` / `text-red-600` | `text-[var(--error-text)]` |
| `border-red-500/30` | `border-[var(--error-border)]` |
| `bg-yellow-500/10` / `bg-amber-500/10` | `bg-[var(--warning-bg)]` |
| `text-yellow-600` / `text-amber-600` | `text-[var(--warning-text)]` |
| `bg-green-500/10` | `bg-[var(--success-bg)]` |
| `text-green-600` | `text-[var(--success-text)]` |
| `bg-purple-500/10` | `bg-[var(--accent-bg)]` |
| `text-purple-600` | `text-[var(--accent-text)]` |
| `dark:bg-gray-900` | (remove — `.dark` class on root drives `--bg-app`) |
| `dark:text-white` | (remove — CSS vars adapt automatically) |

### Status badge recipe (canonical, theme-aware)

```tsx
// success / warning / error / info / accent / neutral — pick one set
<span className="px-2 py-0.5 rounded text-xs font-medium border bg-[var(--success-bg)] text-[var(--success-text)] border-[var(--success-border)]">
  Approved
</span>
```

### HTTP method badge recipe (REST conventions)

| Method | Token set |
|---|---|
| `GET` | `--info-*` |
| `POST` | `--success-*` |
| `PUT` | `--warning-*` |
| `PATCH` | `--accent-*` |
| `DELETE` | `--error-*` |

---

## Section 6 — Branding assets (DB-driven)

### Architecture (since v3.45)

`BrandingAsset` entity stores uploads (logo, favicon) per scope:

```csharp
class BrandingAsset : BaseEntity, IAuditableEntity, IOptionalTenantEntity
{
    public BrandingAssetType AssetType { get; }    // Logo=0, LogoDark=1, Favicon=2
    public BrandingAssetScope Scope { get; }       // Platform=0, Tenant=1
    public Guid? TenantId { get; }
    public string StorageFileName { get; }         // resolved via IFileStorageService
    public string ContentType { get; }
    public long FileSizeBytes { get; }
    public string OriginalFileName { get; }
    public int? Width { get; }
    public int? Height { get; }
}
```

`UiTheme` may reference assets via `LogoAssetId`, `LogoDarkAssetId`, `FaviconAssetId` (theme-level overrides).

### Built-in seeds (immutable)

- `UiThemeSeedData.DarkThemeId` = `f1e8a3d2-4b7c-4e5f-9a2c-6d1b8e4f3c7a`
- `UiThemeSeedData.LightThemeId` = `c2d9b4e5-3a8f-4c6d-b1e7-5f2c9a8b3d4e`

These IDs MUST be referenced by const, never re-generated.

### API endpoints

| Endpoint | Purpose |
|---|---|
| `GET /api/ui/themes` | List effective themes for current user |
| `GET /api/ui/themes/{id}` | Get one theme |
| `POST /api/ui/themes` | Create custom theme (admin) |
| `GET /api/ui/branding/{type}` | Resolve asset for current tenant + assetType |
| `POST /api/ui/branding` | Upload asset (admin) |

The frontend `ThemeContext` calls these at startup and after a tenant switch.

### Frontend consumption

```tsx
import { useTheme } from '@atlashub/smartstack';

function MyComponent() {
  const { mode, selectedTheme, toggleMode, setTheme } = useTheme();
  // selectedTheme.BackgroundsJson is parsed and applied to :root by ThemeSync.
  // Components only consume var(--xxx) and never read the JSON directly.
}
```

---

## Section 7 — DO / DON'T

| ✅ DO | ❌ DON'T |
|---|---|
| Read theme via `useTheme()` | Read `localStorage` for theme directly |
| Use CSS vars (`var(--bg-card)`, `var(--text-primary)`, …) | Hardcode `bg-blue-500`, `text-gray-900`, `bg-white` |
| Toggle dark mode via `useTheme().toggleMode()` | Use `dark:` Tailwind prefix |
| Reference `DarkThemeId`/`LightThemeId` as constants | Re-generate the built-in theme GUIDs |
| Upload branding via `/api/ui/branding` | Replace logo file directly on disk |
| Use `BrandingAssetScope.Tenant` for per-tenant logos | Hardcode logo path in `App.tsx` |
| Put reusable component classes in `@layer components` | Duplicate the same className across 10 components |
| Use status tokens (`--success-*`, …) for badges | Pick a random Tailwind shade per badge |
| Rely on `IFileStorageService` for asset URLs | Hardcode `/uploads/logo.png` paths |

---

## Section 8 — Validation hooks

Generated UI MUST pass `apex/references/checks/frontend-checks.sh` (POST-CHECK C9 / C9b / C9c) which now BLOCK on:

- `bg|text|border-(red|blue|green|gray|white|black|slate|zinc|neutral|stone|amber|yellow|orange|purple|indigo|violet|cyan|pink|emerald|rose|sky|teal|lime|fuchsia)-\d+` in `.tsx`
- `dark:` prefix on any utility class
- Hex colors `#xxxxxx` inside `className=[…]`

Before declaring a generation complete, run:

```bash
bash templates/skills/apex/references/checks/frontend-checks.sh
```

Expected: `0 BLOCKING`. If C9 / C9b / C9c trigger, use the migration table in Section 5 to fix.

---

## Reference source files (read-only — do NOT copy verbatim)

- `D:\01 - projets\SmartStack.app\features\IA-Workflow\web\smartstack-web\src\index.css` (CSS vars)
- `D:\01 - projets\SmartStack.app\features\IA-Workflow\web\smartstack-web\src\base.css` (custom @layer classes)
- `D:\01 - projets\SmartStack.app\features\IA-Workflow\web\smartstack-web\src\config\themePresets.ts` (ACCENT_COLORS, ITEM_PALETTES, BORDER_RADIUS)
- `D:\01 - projets\SmartStack.app\features\IA-Workflow\web\smartstack-web\src\contexts\ThemeContext.tsx` (apply* functions)
- `D:\01 - projets\SmartStack.app\features\IA-Workflow\src\SmartStack.Domain\Platform\Administration\UiConfiguration\UiTheme.cs`
- `D:\01 - projets\SmartStack.app\features\IA-Workflow\src\SmartStack.Domain\Platform\Administration\UiConfiguration\BrandingAsset.cs`

See also [smartstack-provider.md](smartstack-provider.md) for `ThemeProvider` wrapping order, [contexts-cheatsheet.md](contexts-cheatsheet.md) for `useTheme()` API.
