# Design System Audit — Xpeditis
> **Statut : CHARTE VERROUILLÉE (IMMUTABLE).**
> Ce document est la source de vérité de l'identité visuelle Xpeditis.
> Toute refonte UI doit produire un rendu dont **chaque couleur, police, rayon et token** est présent dans ce document.
> Aucune valeur de ce document ne peut être modifiée dans le cadre d'une refonte UI.
**Date de l'audit :** 2026-08-12
**Périmètre :** `apps/frontend` (126 fichiers `.tsx`, ~38 000 lignes, 70 routes)
**Branche :** `ui_update`
---
## 0. Source de vérité actuelle
Il existe **quatre sources concurrentes**, ce qui est le problème structurel n°1 du projet :
| # | Source | Fichier | Autorité réelle | Statut |
|---|--------|---------|-----------------|--------|
| 1 | **Tailwind theme** | `apps/frontend/tailwind.config.ts` | ✅ **Source de vérité effective** | À conserver tel quel |
| 2 | Variables CSS shadcn | `apps/frontend/app/globals.css` (`:root`, `.dark`) | ⚠️ Partiellement contredite par (1) | Conflit documenté §1.3 |
| 3 | Classes `@layer components` | `apps/frontend/app/globals.css` | ⚠️ Quasi inutilisées dans l'app | Legacy |
| 4 | Doc marketing | `apps/frontend/DESIGN_SYSTEM.md` | ❌ Descriptive, non appliquée | Documentaire |
**Décision d'audit :** la source de vérité retenue est **`tailwind.config.ts`**, complétée par les variables CSS de `globals.css` pour les tokens `--border`, `--input`, `--muted`, `--radius`, `--destructive`.
> ⚠️ **Constat majeur :** l'application n'applique pas sa propre charte. On compte **816 occurrences de `blue-*` Tailwind (bleu générique, hors charte)** contre ~700 occurrences de tokens de marque, et **2 485 occurrences de `gray-*`** au lieu de l'échelle `neutral` navy définie dans le thème. Voir §23.
---
## 1. Color palette
### 1.1 Couleurs de marque — IMMUTABLES
Définies dans `tailwind.config.ts` → `theme.extend.colors`.
| Token Tailwind | Valeur HEX | Usage documenté | Occurrences |
|----------------|-----------|-----------------|-------------|
| `brand-navy` | `#10183A` | Couleur primaire, titres, surfaces sombres, sidebar admin | 123 `text-`, 46 `bg-` |
| `brand-turquoise` | `#34CCCD` | Accent, CTA, focus, liens | 137 `bg-`, 134 `text-` |
| `brand-green` | `#067224` | Succès, validation | 19 `bg-`, 11 `text-` |
| `brand-gray` | `#F2F2F2` | Fond de section clair | 8 `bg-` |
### 1.2 Alias sémantiques de marque — IMMUTABLES
| Token | Valeur | Note |
|-------|--------|------|
| `primary.DEFAULT` | `#10183A` | Override explicite du bleu shadcn |
| `primary.foreground` | `#FFFFFF` | |
| `primary.navy` | `#10183A` | Alias redondant |
| `accent.DEFAULT` | `#34CCCD` | Override explicite du gris shadcn |
| `accent.foreground` | `#FFFFFF` | |
| `accent.turquoise` | `#34CCCD` | Alias redondant |
| `success.DEFAULT` | `#067224` | |
| `success.light` | `#08A131` | |
| `success.dark` | `#044F19` | |
### 1.3 ⚠️ Conflit tokens Tailwind ↔ variables CSS
`globals.css` définit encore la palette shadcn par défaut, **contredite** par `tailwind.config.ts` :
| Token | Valeur CSS `:root` (globals.css) | Valeur Tailwind (config) | Qui gagne |
|-------|----------------------------------|--------------------------|-----------|
| `--primary` | `221.2 83.2% 53.3%` (bleu #3B82F6) | `#10183A` | **Tailwind** (`bg-primary` = navy) |
| `--accent` | `210 40% 96.1%` (gris quasi-blanc) | `#34CCCD` | **Tailwind** (`bg-accent` = turquoise) |
| `--ring` | `221.2 83.2% 53.3%` (bleu) | *(non overridé)* | **CSS** → ❗ **anneaux de focus bleus, hors charte** |
**Conséquence :** `focus-visible:ring-ring` de `Button` et `Input` produit un halo **bleu #3B82F6** sur toute l'application.
**Statut :** défaut d'implémentation, pas un choix de charte. `--ring` doit pointer vers le turquoise de marque — c'est une **restauration** de la charte, pas une modification.
### 1.4 Échelle neutre (dérivée du navy) — IMMUTABLE
`tailwind.config.ts` → `colors.neutral` :
| Token | HEX |
|-------|-----|
| `neutral-50` | `#F8F9FC` |
| `neutral-100` | `#EDEEF5` |
| `neutral-200` | `#DADBEB` |
| `neutral-300` | `#B0B6DA` |
| `neutral-400` | `#8590C9` |
| `neutral-500` | `#5A6BB8` |
| `neutral-600` | `#3A4A97` |
| `neutral-700` | `#2C3978` |
| `neutral-800` | `#1E2859` |
| `neutral-900` | `#10183A` |
> Échelle **froide, bleutée**, dérivée du navy. Actuellement quasi inutilisée (92 occurrences) au profit de `gray-*` Tailwind (2 485 occurrences), qui est une échelle **neutre-froide différente** (`#F9FAFB → #111827`).
### 1.5 Variables CSS shadcn conservées (mode clair)
`globals.css` `:root` :
| Variable | HSL | Équivalent |
|----------|-----|-----------|
| `--background` | `0 0% 100%` | `#FFFFFF` |
| `--foreground` | `222.2 84% 4.9%` | `#020817` |
| `--card` / `--popover` | `0 0% 100%` | `#FFFFFF` |
| `--card-foreground` / `--popover-foreground` | `222.2 84% 4.9%` | `#020817` |
| `--secondary` | `210 40% 96.1%` | `#F1F5F9` |
| `--secondary-foreground` | `222.2 47.4% 11.2%` | `#0F172A` |
| `--muted` | `210 40% 96.1%` | `#F1F5F9` |
| `--muted-foreground` | `215.4 16.3% 46.9%` | `#64748B` |
| `--destructive` | `0 84.2% 60.2%` | `#EF4444` |
| `--destructive-foreground` | `210 40% 98%` | `#F8FAFC` |
| `--border` | `214.3 31.8% 91.4%` | `#E2E8F0` |
| `--input` | `214.3 31.8% 91.4%` | `#E2E8F0` |
### 1.6 Couleurs hardcodées relevées
**Alignées charte (à conserver, idéalement tokenisées) :**
| HEX | Occurrences | Emplacements |
|-----|-------------|--------------|
| `#34CCCD` | 50 | `globals.css` (prose, tiptap), maps, charts, SVG |
| `#10183A` | 23 | `globals.css` (prose), charts, SVG |
| `#067224` | 3 | badges, charts |
| `#F2F2F2` | 1 | section landing |
**Hors charte (dérives) :**
| HEX | Occ. | Emplacement | Nature |
|-----|------|-------------|--------|
| `#1A2550`, `#1A2A5E` | 7 | dégradés navy | Nuances navy inventées |
| `#0E9999`, `#2BB8B9`, `#2AB8B9` | 4 | hovers turquoise | Nuances turquoise inventées |
| `#3B82F6`, `#2563EB`, `#1E3A8A` | 4 | charts Recharts, `Bar fill` | **Bleu générique** |
| `#10B981`, `#EF4444`, `#F59E0B` | 3 | `dashboard/page.tsx` — Pie chart status | Palette Tailwind brute |
| `#8884D8` | 1 | `dashboard/page.tsx` — `Pie fill` par défaut | **Violet par défaut Recharts (oubli)** |
| `#0D1117`, `#161B22` | 2 | `CodeBlock.tsx` | Thème GitHub Dark |
| `#FFD700`, `#FF6600`, `#FF00FF`, `#E30613`, `#006633`, `#003A70`, `#002B5C`, `#00243D`, `#E65100` | 9 | couleurs de compagnies maritimes / partenaires | **Légitime** (identités tierces) |
| `#F3F4F6`, `#E5E7EB`, `#ADB5BD`, `#F0F0F0` | 4 | prose, tiptap, grilles charts | Gris utilitaires |
### 1.7 Familles Tailwind utilisées hors charte
| Famille | Occurrences | Verdict |
|---------|-------------|---------|
| `gray-*` | 2 485 | Neutre de fait de l'app — **doublon** de `neutral-*` |
| `blue-*` | 816 | ❗ **Accent parallèle hors charte** (voir §23) |
| `red-*` | 368 | Destructif — acceptable, à tokeniser |
| `green-*` | 246 | Succès — doublon de `brand-green`/`success` |
| `amber-*` / `yellow-*` | 189 | Avertissement — acceptable, à tokeniser |
| `orange-*` | 66 | Doublon d'`amber` |
| `purple-*` / `violet-*` | 43 | ❗ Sans justification (KPI volume, badge Platinium) |
| `emerald-*` | 15 | Doublon de `green` |
| `slate-*` / `neutral-*` / `zinc-*` | 104 | Doublon de `gray` |
| `indigo-*`, `cyan-*`, `sky-*`, `teal-*`, `rose-*` | 24 | Bruit |
---
## 2. Typography
**Chargement :** `next/font/google` dans `apps/frontend/src/lib/fonts.ts`, `display: 'swap'`, `preload: true`, subset `latin`. Variables CSS injectées sur `` dans `app/[locale]/layout.tsx`.
| Rôle | Police | Variable CSS | Alias Tailwind | Poids chargés |
|------|--------|--------------|----------------|---------------|
| Titres | **Manrope** | `--font-manrope` | `font-manrope`, `font-heading` | 200–800 |
| Corps / UI | **Montserrat** | `--font-montserrat` | `font-montserrat`, `font-body`, `font-sans` (défaut) | 100–900 |
- `body` porte `font-body` (Montserrat) — `app/[locale]/layout.tsx` + `globals.css`.
- `h1`–`h6` portent `font-heading` (Manrope) via `@layer base` dans `globals.css`.
- `.prose h1..h4` forcent Manrope (`globals.css`).
**IMMUTABLE :** aucune autre famille ne doit être introduite. Aucune police système, aucune substitution.
---
## 3. Font weights
| Contexte | Poids | Défini dans |
|----------|-------|-------------|
| `display-lg` | 800 | `tailwind.config.ts` |
| `display-md`, `display-sm`, `h1` | 700 | `tailwind.config.ts` |
| `h2`, `h3`, `h4` | 600 | `tailwind.config.ts` |
| `h5`, `h6` | 500 | `tailwind.config.ts` |
| `body-*` | 400 | `tailwind.config.ts` |
| `label-*` | 600 | `tailwind.config.ts` |
| Boutons (`Button`) | 500 (`font-medium`) | `src/components/ui/button.tsx` |
| Boutons (`.btn-*` CSS) | 600 (`font-semibold`) | `globals.css` — ⚠️ incohérent avec ci-dessus |
| Badge | 600 (`font-semibold`) | `src/components/ui/badge.tsx` |
| CardTitle | 600 | `src/components/ui/card.tsx` |
| TableHead | 500 | `src/components/ui/table.tsx` |
---
## 4. Font sizes
Échelle complète définie dans `tailwind.config.ts` → `theme.extend.fontSize`. **IMMUTABLE.**
### Display
| Token | Taille | Line-height | Letter-spacing | Weight |
|-------|--------|-------------|----------------|--------|
| `text-display-lg` | `4.5rem` / 72px | 1.1 | −0.02em | 800 |
| `text-display-md` | `3.75rem` / 60px | 1.15 | −0.02em | 700 |
| `text-display-sm` | `3rem` / 48px | 1.2 | −0.01em | 700 |
### Headings
| Token | Taille | Line-height | Weight |
|-------|--------|-------------|--------|
| `text-h1` | `2.5rem` / 40px | 1.25 | 700 |
| `text-h2` | `2rem` / 32px | 1.3 | 600 |
| `text-h3` | `1.5rem` / 24px | 1.35 | 600 |
| `text-h4` | `1.25rem` / 20px | 1.4 | 600 |
| `text-h5` | `1.125rem` / 18px | 1.45 | 500 |
| `text-h6` | `1rem` / 16px | 1.5 | 500 |
### Body
| Token | Taille | Line-height | Weight |
|-------|--------|-------------|--------|
| `text-body-lg` | `1.125rem` / 18px | 1.6 | 400 |
| `text-body` | `1rem` / 16px | 1.6 | 400 |
| `text-body-sm` | `0.875rem` / 14px | 1.55 | 400 |
| `text-body-xs` | `0.75rem` / 12px | 1.5 | 400 |
### Labels
| Token | Taille | Line-height | Weight | Letter-spacing |
|-------|--------|-------------|--------|----------------|
| `text-label-lg` | `0.875rem` / 14px | 1.4 | 600 | 0.05em |
| `text-label` | `0.75rem` / 12px | 1.4 | 600 | 0.05em |
| `text-label-sm` | `0.6875rem` / 11px | 1.4 | 600 | 0.05em |
> ⚠️ **Constat :** cette échelle est **quasi inutilisée dans l'app produit**. Les pages utilisent l'échelle Tailwind par défaut (`text-xs`, `text-sm`, `text-base`, `text-lg`, `text-2xl`, `text-3xl`). Les deux échelles divergent (ex. `text-h1` = 40px vs `text-3xl` = 30px).
### Échelle Tailwind par défaut réellement utilisée
`text-[10px]`, `text-xs` (12), `text-sm` (14), `text-base` (16), `text-lg` (18), `text-xl` (20), `text-2xl` (24), `text-3xl` (30), `text-4xl` (36), `text-5xl` (48), `text-6xl` (60).
---
## 5. Line heights
| Source | Valeurs |
|--------|---------|
| Tokens `fontSize` (config) | 1.1 / 1.15 / 1.2 / 1.25 / 1.3 / 1.35 / 1.4 / 1.45 / 1.5 / 1.55 / 1.6 |
| Utilitaires employés dans l'app | `leading-none`, `leading-tight`, `leading-relaxed`, `leading-tight` sur `[10px]` |
Aucune échelle `lineHeight` custom n'est déclarée dans `tailwind.config.ts` — seules les valeurs couplées aux `fontSize` existent.
---
## 6. Spacing system
**Échelle Tailwind par défaut (base 4px)** — aucune extension `spacing` dans la config.
Valeurs réellement employées, par fréquence :
| Rang | Classe | Occ. |
|------|--------|------|
| 1 | `py-2` | 315 |
| 2 | `px-6` | 308 |
| 3 | `px-4` | 295 |
| 4 | `mb-4` | 213 |
| 5 | `py-3` | 202 |
| 6 | `mt-1` / `mb-2` | 174 / 168 |
| 7 | `px-3` | 168 |
| 8 | `p-4` | 149 |
| 9 | `mb-6` | 147 |
| 10 | `py-4` | 125 |
| 11 | `gap-4` / `gap-2` / `gap-3` | 118 / 115 / 68 |
| 12 | `p-6` / `p-3` / `p-8` | 90 / 77 / 59 |
| 13 | `space-y-2` / `space-y-6` / `space-y-4` | 71 / 68 / 52 |
**Container Tailwind** (`theme.container`) : `center: true`, `padding: 2rem`, `screens.2xl: 1400px`.
**Rythme de padding constaté :** `p-4` (mobile/compact) et `p-6` (desktop/card) dominants — c'est le rythme de fait.
---
## 7. Border radius
**Token racine :** `--radius: 0.5rem` (8px) — `globals.css`. **IMMUTABLE.**
| Token Tailwind | Calcul | Valeur |
|----------------|--------|--------|
| `rounded-sm` | `calc(var(--radius) - 4px)` | **4px** |
| `rounded-md` | `calc(var(--radius) - 2px)` | **6px** |
| `rounded-lg` | `var(--radius)` | **8px** |
Les autres rayons proviennent de l'échelle Tailwind par défaut (`rounded` 4px, `rounded-xl` 12px, `rounded-2xl` 16px, `rounded-3xl` 24px, `rounded-full` 9999px).
**Usage réel :**
| Classe | Occ. | Rayon |
|--------|------|-------|
| `rounded-lg` | 452 | 8px — **rayon standard de l'app** |
| `rounded-full` | 241 | pilules, avatars, badges |
| `rounded-md` | 188 | 6px — contrôles (Button, Input) |
| `rounded-xl` | 95 | 12px |
| `rounded` | 83 | 4px |
| `rounded-2xl` | 62 | 16px |
| `rounded-3xl` | 10 | 24px |
**Rayons canoniques retenus (IMMUTABLES) :** `rounded-md` (contrôles) · `rounded-lg` (cartes, conteneurs) · `rounded-full` (pilules/avatars).
`rounded-xl`, `2xl`, `3xl` sont des dérives ponctuelles (landing, modales) — non canoniques.
---
## 8. Shadows
Aucune extension `boxShadow` dans `tailwind.config.ts` → **échelle Tailwind par défaut**.
| Classe | Occ. | Valeur CSS |
|--------|------|-----------|
| `shadow-sm` | 123 | `0 1px 2px 0 rgb(0 0 0 / 0.05)` |
| `shadow-lg` | 76 | `0 10px 15px -3px rgb(0 0 0 / 0.1), 0 4px 6px -4px …` |
| `shadow` | 39 | `0 1px 3px 0 rgb(0 0 0 / 0.1), 0 1px 2px -1px …` |
| `shadow-xl` | 36 | `0 20px 25px -5px rgb(0 0 0 / 0.1), …` |
| `shadow-md` | 28 | `0 4px 6px -1px rgb(0 0 0 / 0.1), …` |
| `shadow-2xl` | 23 | `0 25px 50px -12px rgb(0 0 0 / 0.25)` |
| `shadow-brand` | 2 | ❗ **classe inexistante** — aucun token `shadow-brand` déclaré. Rend un no-op. |
**Élévation canonique constatée :** `shadow-sm` pour les cartes (+ `hover:shadow-md`), `shadow-lg` pour sidebar/dropdowns, `shadow-xl`/`2xl` pour modales.
---
## 9. Borders
| Élément | Valeur | Source |
|---------|--------|--------|
| Couleur par défaut globale | `hsl(var(--border))` = `#E2E8F0` | `globals.css` — règle `* { @apply border-border }` |
| Bordure des inputs | `hsl(var(--input))` = `#E2E8F0` | `globals.css` + `ui/input.tsx` |
| Épaisseur standard | `1px` (`border`) | — |
| Bouton outline (CSS legacy) | `2px` (`border-2 border-primary`) | `globals.css` `.btn-outline` |
| Input legacy CSS | `border-neutral-300` = `#B0B6DA` | `globals.css` `.input` |
| Bordures de cartes en pages | `border-gray-200` = `#E5E7EB` | ⚠️ hardcodé, doublon de `--border` |
| Séparateurs internes | `border-gray-100` = `#F3F4F6` / `divide-gray-100` | ⚠️ hardcodé |
| Sidebar admin (sur navy) | `border-white/10` | `admin/layout.tsx` |
---
## 10. Icons
| Aspect | Valeur |
|--------|--------|
| Bibliothèque | **`lucide-react` v0.294.0** — unique et exclusive |
| Tailles employées | `w-3 h-3` (12), `w-4 h-4` (16, dominante), `w-5 h-5` (20, nav), `w-6 h-6` (24), `h-3 w-3` |
| Épaisseur | `strokeWidth` par défaut (2), sauf SVG inline hamburger/close (`strokeWidth={2}` explicite) |
| Couleur | Hérite de `currentColor` |
| Exceptions | SVG inline manuscrits pour le menu burger et la fermeture de sidebar (`dashboard/layout.tsx`, `admin/layout.tsx`) — **doublons de `Menu`/`X` de lucide** |
**Icônes de marque / logos :**
| Asset | Chemin | Usage |
|-------|--------|-------|
| Logo noir | `public/assets/logos/logo-black.svg` | Sidebar dashboard, favicon, OG image |
| Logo blanc (SVG) | `public/assets/logos/logo-white.svg` | Sidebar admin, footer navy |
| Logo blanc (PNG) | `public/assets/logos/logo-white.png` | Fallback |
| Logos partenaires | `public/assets/logos/partner/*.png` (7 fichiers, noms avec espaces) | Landing — bandeau partenaires |
| Images | `background-login.png`, `background-section-1-landingpage.png` | Auth, landing |
| Vidéos | `demo-xpeditis.mp4` / `.webm` | Modale démo |
**IMMUTABLE :** logos, fichiers de marque, et le choix de `lucide-react`.
---
## 11. Buttons
Trois systèmes coexistent — **c'est une incohérence, pas une charte** :
### 11.1 `src/components/ui/button.tsx` (composant React)
Adoption : **17 usages dans 8 fichiers sur 126**.
Base : `inline-flex items-center justify-center rounded-md text-sm font-medium transition-colors focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 disabled:opacity-50 disabled:pointer-events-none`
| Variant | Classes |
|---------|---------|
| `default` | `bg-primary text-primary-foreground hover:bg-primary/90` → navy |
| `destructive` | `bg-destructive text-destructive-foreground hover:bg-destructive/90` → rouge |
| `outline` | `border border-input bg-background hover:bg-accent hover:text-accent-foreground` → hover **turquoise plein** |
| `secondary` | `bg-secondary text-secondary-foreground hover:bg-secondary/80` |
| `ghost` | `hover:bg-accent hover:text-accent-foreground` → hover **turquoise plein** |
| `link` | `text-primary underline-offset-4 hover:underline` |
| Size | Classes | Hauteur |
|------|---------|---------|
| `default` | `h-10 px-4 py-2` | 40px |
| `sm` | `h-9 rounded-md px-3` | 36px |
| `lg` | `h-11 rounded-md px-8` | 44px |
| `icon` | `h-10 w-10` | 40×40 |
> ⚠️ `hover:bg-accent` sur `ghost`/`outline` produit un aplat **turquoise #34CCCD** au survol (car `accent` est overridé) — visuellement agressif et non intentionnel.
> ⚠️ Pas de `cn()` / `tailwind-merge` : la concaténation de string empêche l'override propre des classes.
> ⚠️ Pas de `asChild` : `