# Architecture CSS — comment une couleur arrive à l'écran

Ce document explique le **mécanisme** : dans quel ordre les feuilles se chargent, comment une valeur voyage jusqu'au composant, qui a le droit de la remplacer et à quel moment. Pour la table exhaustive de chaque variable, voir [CASCADE.md](CASCADE.md), qui est générée depuis les feuilles. Pour les garde-fous qui imposent tout ceci, voir [DOCUMENTATION.md](DOCUMENTATION.md).

## Le trajet d'une valeur

Une seule règle gouverne le système : **chaque maillon ne connaît que le suivant**.

```
#4f46e5                la valeur brute, écrite une seule fois
   ↓                   (primitives.css)
--base-primary-600     la primitive : un nom pour une valeur, sans intention
   ↓                   (« indigo 600 », pas « la couleur des boutons »)
--action-primary       le token sémantique : une intention, sans valeur
   ↓                   (defaults.css — « la couleur d'action principale »)
background             la propriété, dans components.css
   ↓                   (qui ne sait pas de quelle couleur il s'agit)
<button>               l'élément, qui ne sait rien du tout
```

L'intérêt n'est pas la beauté du schéma : c'est que **chaque flèche peut être redirigée sans toucher aux autres**. Un thème remplace la deuxième flèche — `--action-primary` pointe alors vers `--base-solaire-coral-700` — et les 281 règles de `components.css` suivent sans être modifiées d'une ligne.

### Pourquoi deux niveaux de nommage

La tentation est d'écrire `background: var(--base-primary-600)` directement. Le système l'interdit, et une règle de lint le vérifie. La raison tient en une phrase : **une primitive décrit une couleur, un token sémantique décrit un usage**. Un thème qui redéfinit « la couleur d'action » sait ce qu'il fait ; un thème qui redéfinit « indigo 600 » casse tout ce qui utilisait cet indigo pour autre chose.

C'est ce qui permet à Solaire de dire « mon action principale est un corail profond » sans avoir à savoir qu'un `<button>`, une case cochée, un curseur de `range` et un `<progress>` s'en servent.

## Comment les noms sont construits

Le nom dit à quelle couche appartient une variable — pas seulement ce qu'elle vaut.

| Couche | Forme | Exemples |
|---|---|---|
| **Primitive** | `--base-<famille>-<cran>` | `--base-neutral-900` · `--base-space-4` · `--base-radius-lg` |
| **Primitive de thème** | `--base-<thème>-<palette>-<cran>` | `--base-solaire-coral-700` · `--base-luxe-gold-400` |
| **Token sémantique** | `--<rôle>-<précision>[-<état>]` | `--action-primary-hover` · `--status-success-background` |

Trois conséquences pratiques :

**On voit l'erreur.** Écrire `background: var(--base-primary-600)` dans un composant se repère à l'œil nu : le préfixe `--base-` n'a rien à y faire. Auparavant, `var(--c-pri-600)` et `var(--act-pri)` se ressemblaient trop pour qu'un relecteur tique.

**La règle de lint devient infaillible.** Elle tenait en douze motifs — un par famille — qu'il fallait penser à étendre à chaque ajout ; une primitive non listée devenait silencieusement autorisée dans les composants. Elle tient maintenant en un seul : `/^--base-/`.

**Les thèmes ne peuvent plus se marcher dessus.** Le nom du thème est dans celui de la primitive, donc `--base-solaire-gradient` et `--base-luxe-gradient` coexistent sans risque. C'est précisément le bug vécu à la construction de v6, quand quatre thèmes définissaient chacun un `--cc-gradient` et que le dernier chargé gagnait pour tout le monde.

### Aucun mot tronqué

Un nom se lit à voix haute sans décodage. `--c-n-900` demandait de savoir que `c` vaut *color* et `n` *neutral* ; `--act-pri-h` que `h` vaut *hover* ; `--st-ok-bg` que `bg` vaut *background* et `ok` *success*. Ces trois-là s'écrivent désormais `--base-neutral-900`, `--action-primary-hover` et `--status-success-background`.

Deux endroits gardent des noms courts, et c'est délibéré :

- **les hooks de style inline** (`--bg`, `--shadow`, `--radius`, `--w`) — ce sont des paramètres d'instance passés en HTML, pas des tokens du système ; les allonger alourdirait chaque attribut `style` des pages générées ;
- **les variables locales à un composant** (`--toggle` dans l'interrupteur, `--header-height` posée en JavaScript), dont la portée tient en quelques lignes.

### La hiérarchie se nomme, elle ne se numérote pas

`--txt-2` et `--txt-3` ne disaient pas ce qu'ils désignaient — ni lequel était le plus discret. Ils s'appellent `--text-secondary` et `--text-tertiary`. Même principe pour les états : `-h` et `-a` sont devenus `-hover` et `-active`, et rien n'oblige plus à deviner si `-a` signifiait *active* ou *alternate*.

## Les fichiers, dans l'ordre où ils se chargent

L'ordre n'est pas décoratif : c'est lui qui décide qui l'emporte à spécificité égale.

| # | Fichier | Contient | Change quand… | Omissible ? |
|---|---|---|---|---|
| 1 | `styles/primitives.css` | les valeurs brutes, nommées | on enrichit le vocabulaire | non |
| 2 | `styles/defaults.css` | les 6 familles par défaut | on change une décision par défaut | non |
| 3 | `styles/components.css` | éléments HTML, composants, motifs | on ajoute un composant | non |
| 4 | `styles/layout.css` | classes de disposition (`.flex`, `.gap-4`) | on ajoute une classe mécanique | oui |
| 5 | `styles/themes/theme-*.css` | une identité, en surcharge | une marque évolue | oui, chacun |

**Un fichier, une nature, une raison de changer.** C'est ce qui permet de savoir sans réfléchir où va une modification — et de constater qu'une modification qui toucherait deux fichiers touche probablement deux choses.

`defaults.css` porte ce que le catalogue appelle le thème « Moderne ». Il ne vit pas dans `themes/` pour une raison de fond : il est **obligatoire** et scopé `:root`, là où les thèmes sont **optionnels** et scopés `[data-theme-style]`. Un thème absent ne casse rien — les tokens qu'il aurait redéfinis gardent leur valeur par défaut ; `defaults.css` absent, plus rien ne tient.

Un sixième fichier existe, hors de `styles/` : **`catalogue.css`**, qui habille les pages de démonstration. Il n'appartient pas au design system et n'a aucune raison d'être chargé par un projet qui le consomme. C'est aussi pourquoi il s'autorise ce que `components.css` s'interdit.

### Ce que chaque fichier n'a pas le droit de faire

| Fichier | Interdit | Pourquoi |
|---|---|---|
| `primitives.css` | *(rien — c'est le seul endroit où une valeur s'écrit littéralement)* | |
| `defaults.css` | une couleur littérale | un token qui porte sa valeur en dur n'est plus surchargeable par un thème |
| `components.css` | référencer une primitive (`--c-*`, `--base-font-*`, `--base-space-*`…) | il deviendrait sensible à la palette, donc non thémable |
| `components.css` | écrire une couleur littérale | même raison, en pire : le défaut n'apparaît qu'au changement de thème |
| `components.css` | un sélecteur `[data-theme]` | il « saurait » quel thème est actif, ce qui inverse la dépendance |
| `themes/*.css` | une couleur littérale hors de sa section 0 | même raison que `defaults.css` |
| `layout.css` | un token sémantique (`--space-*`, `--layout-*`) | `.gap-4` doit valoir la même distance physique quel que soit le thème |

Ces interdits sont vérifiés par `npm run lint:css` — six règles au total, dont celle sur `light-dark()` décrite plus bas. Ce ne sont pas des conventions : ce sont des erreurs de build.

Depuis que le chrome du catalogue vit dans `catalogue.css`, **`components.css` ne contient plus aucune zone exemptée** sinon une exception d'une ligne, documentée sur place (les variantes décoratives de l'avatar).

## Les six familles

Les tokens sémantiques sont rangés en six familles, chacune surchargeable indépendamment. Un thème peut ne redéfinir que la typographie et hériter du reste.

| Famille | Préfixes | Ce qu'elle décide |
|---|---|---|
| Couleurs | `--surface-*` `--txt-*` `--bdr-*` `--act-*` `--st-*` `--brand-*` | surfaces, texte, bordures, actions, statuts |
| Espaces | `--space-*` | rythme vertical et horizontal, paddings de composants |
| Layout | `--layout-*` | largeurs, rayons, tailles de contrôle, z-index |
| Ombres | `--shadow-*` | élévation, halos, anneau de focus |
| Animations | `--motion-*` | vitesses, ressorts, amplitudes |
| Typographie | `--font-*` `--weight-*` `--leading-*` `--tracking-*` `--size-*` | la « voix » : police, graisse, interlignage |

Le décompte exact de ce que chaque thème redéfinit se lit dans [CASCADE.md](CASCADE.md), section 4.

## Clair et sombre : une seule déclaration

Chaque token qui dépend du mode s'écrit **une fois**, les deux valeurs portées par `light-dark()` :

```css
:root {
  color-scheme: light dark;                          /* suit la préférence système */
  --text: light-dark(var(--base-neutral-900), var(--base-neutral-50));
}
[data-theme="light"] { color-scheme: light; }        /* fige le mode… */
[data-theme="dark"]  { color-scheme: dark; }         /* …et tout bascule avec lui */
```

`data-theme` ne pose aucune couleur : il **fige `color-scheme`**, et c'est ce `color-scheme` que chaque `light-dark()` consulte pour choisir sa branche. D'où trois conséquences :

- **sans `data-theme`, le mode suit la préférence système**, sans JavaScript, pour les six thèmes ;
- le script anti-FOUC ne sert plus qu'à restituer un choix explicite avant le premier rendu ;
- il n'existe pas de valeur `data-theme="auto"` : « auto » se dit en **n'écrivant pas** l'attribut.

> **Piège.** `light-dark()` n'accepte que des **couleurs**. Y enfermer une pile d'ombres entière produit une déclaration invalide — la propriété est ignorée en silence, l'ombre disparaît sans un mot. Sur une ombre, la fonction se place donc sur la couleur (`0 2px 8px light-dark(…, …)`) ou sur une primitive de teinte (`--base-shadow-tint-*`). La règle `design-system/light-dark-colors-only` refuse la mauvaise forme, y compris quand les longueurs sont cachées derrière des `var()`.

## Ce à quoi un composant s'adapte

Un composant réagit à **la largeur de son conteneur**, pas à celle de l'écran. La différence n'est pas théorique : une grille `.cols-3` placée dans une carte de 280 px affichait trois colonnes de 86 px sur un écran de 1280 px, et une table dans une colonne étroite débordait en silence. Le viewport était large : les `@media` ne se déclenchaient jamais.

```css
/* le bloc se déclare conteneur… */
.card { container-type: inline-size; }

/* …et son contenu l'interroge */
@container (max-width: 768px) {
  :is(.cols-2, .cols-3, .cols-4) { grid-template-columns: 1fr; }
}
```

Trois blocs sont conteneurs d'office : **`<main>`** (la racine du contenu, garantie par la règle `single-main`), **`.card`** et **`.demo`**. Pour tout autre bloc, l'utilitaire **`.container`** le déclare explicitement.

### Pourquoi chaque `@container` est doublé d'un `@media`

Une requête de conteneur sans conteneur ancêtre **ne se déclenche jamais** — silencieusement. Du contenu placé hors de `<main>` (dans un `<footer>`, par exemple) perdrait donc toute adaptation. Le repli `@media`, au même seuil, couvre ce cas : le comportement sur petit écran reste identique, avec ou sans conteneur déclaré.

### Ce qui reste au viewport, à raison

Le header de page est pleine largeur : son conteneur *est* le viewport, et sa media query est donc le bon outil. Une requête de conteneur y serait une complication sans objet.

### Un piège écarté

`container-type: inline-size` applique un confinement, ce qui pose deux questions qu'il faut trancher avant d'en semer partout :

- **il fige la largeur intrinsèque** : à ne poser que sur un bloc dont la largeur vient du parent, jamais sur un élément dimensionné par son contenu ;
- **il ne crée pas de bloc conteneur pour les descendants `position: fixed`** — contrairement à `contain: layout` ou à `transform`. Vérifié dans le navigateur avant adoption : `.skip-link` et `.toast-region` restent ancrés au viewport.

## Qui gagne, et pourquoi

Trois mécanismes se combinent, dans cet ordre de priorité croissante.

**1. L'ordre de chargement.** À spécificité égale, la dernière déclaration l'emporte. C'est pourquoi les thèmes se chargent après `defaults.css`.

**2. La spécificité.** Un thème cible `[data-theme-style="solaire"]` (0-1-0), plus spécifique que le `:root` (0-1-0 aussi, mais chargé avant). Aucun `!important` n'est nécessaire nulle part dans le système.

**3. La spécificité *nulle* des composants.** Les boutons sont déclarés avec `:where(button, .btn)`, dont la spécificité est **zéro par construction**. N'importe quelle règle applicative peut donc personnaliser un bouton sans lutter.

Cette troisième décision a un prix, qu'il faut connaître : **tout sélecteur contextuel l'emporte sur elle**. `nav a` (0-0-2) battait ainsi `:where(button, .btn)`, si bien qu'un `<a class="btn">` placé dans un `<nav>` perdait son padding. Le correctif n'est pas d'augmenter la spécificité des boutons — ce serait renoncer au bénéfice — mais de restreindre la règle contextuelle : `nav a:not(.btn)`.

## Ce que fait un thème, concrètement

Un thème ne contient **aucun composant**. Il redéfinit des maillons :

```css
/* Sa section 0 : ses propres primitives, préfixe unique */
:root {
  --base-solaire-coral-500: #f9682c;
  --base-solaire-coral-700: #b93d13;
}

/* Ses sections 1-6 : les tokens sémantiques repointés */
[data-theme-style="solaire"] {
  --action-primary: light-dark(var(--base-solaire-coral-700), var(--base-solaire-coral-500));
}
```

> **Piège vécu.** Les quatre premiers thèmes nommaient tous leur dégradé `--cc-gradient`, dans un `:root` non scopé. Les fichiers étant chargés ensemble, **le dernier chargé gagnait pour tout le monde** : chaque thème affichait le dégradé d'un autre, sans que rien ne plante. D'où la règle : préfixe unique par thème dans la section 0.

## Les deux échappatoires, et leurs limites

**Les hooks de valeur ponctuelle.** Plutôt que des dizaines de classes, quatre variables inline sont acceptées : `style="--bg: …"`, `--shadow`, `--radius`, `--w`. Ce sont les seules propriétés inline que le garde-fou HTML laisse passer.

**Les classes utilitaires.** `.flex`, `.gap-4`, `.cols-auto` lisent l'échelle **brute** (`--base-space-*`), jamais les tokens sémantiques : un `.gap-4` doit représenter la même distance physique quel que soit le thème actif. C'est le miroir exact de l'interdit qui pèse sur `components.css`.

## Vérifier son travail

```bash
npm test               # les quatre garde-fous + les tests de l'outillage
npm run docs:cascade   # régénère CASCADE.md après un changement de tokens
```

L'ordre de chargement dans une page :

```html
<link rel="stylesheet" href="styles/primitives.css">
<link rel="stylesheet" href="styles/defaults.css">
<link rel="stylesheet" href="styles/components.css">
<link rel="stylesheet" href="styles/layout.css">
<link rel="stylesheet" href="styles/themes/theme-solaire.css"> <!-- au choix -->
```

Un token ajouté ou renommé demande trois gestes : le déclarer dans `defaults.css`, le décrire dans [TOKENS.md](TOKENS.md), et régénérer [CASCADE.md](CASCADE.md). Les deux derniers sont vérifiés par `npm run lint:docs`.
