# Journal, du 23 au 30 octobre 2026 (archive)

Ce fichier est l'**historique** du journal de reprise jusqu'au 30/10/2026, gardé tel quel le
02/11/2026 : ses tableaux d'avancement et ses « reste à faire » décrivent l'état de l'époque. L'état
actuel et la façon de reprendre sont dans le [journal de reprise](/journal), les décisions dans le
[registre des décisions](/decisions).

## 1. Règles de travail

- Branche `recit/setup`. **Rien n'est commité ni poussé** : l'utilisateur commite et pousse
  lui-même. Ne jamais lancer `git commit`, `git push` ni `git add`.
- Une seule stack, pas de dépendance en double (`AGENTS.md`).
- Tout Markdown a un jumeau JSON typé, testé dans les deux sens.
- Un seul système de types : défini une fois dans `@recit/core`, dérivé des schémas zod.
- Les sources de récits (`examples/**/*.md|mdx`, `sites/*/stories/**`) ne passent jamais par oxfmt.

## 2. Avancement

| Phase                        | État                  | Détail                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                           |
| ---------------------------- | --------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| P0 Fondations et nettoyage   | ✅                    | fork réduit au cœur, CI et release Récit, `brand/`, skills, `SETUP.md`, `CONTRIBUTING.md`                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                        |
| P1 Import du deck            | ✅                    | `@recit/core`, `theme`, `components`, `deck`, `cli`, `@recit/site`, `recit` ; exemples `deck-*`, `site-embed` ; e2e Playwright avec captures                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                     |
| P2 Sites                     | ✅                    | `sites/{www,docs,presentation,components}` sur `@recit/site-kit` ; correctifs de packages et 3 patchs du framework de sites (voir `UPSTREAM.md`)                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                 |
| P3 Story, config, JSON, i18n | ✅                    | 1 841 tests, 139 e2e deux fois ; restent mineurs : packs d'interface par langue, sélecteur de langue plein écran, RTL complet, bandeau « non traduit »                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                           |
| P4-V1 Import d'emaki         | ✅ rapatrié           | dépôt réuni vérifié : `pnpm check` (règle des extensions `.js` désormais appliquée), build et typecheck de 23 projets, 1 940 tests (24/10, après le renommage `publish:`), 16 goldens, 139 e2e deux fois                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                         |
| Plans du backlog (10 à 14)   | ✅ écrits et intégrés | formulaires, images, glossaire et annuaire, cartes, IA et scénario ; phases P13 à P16 et lots en P6 à P11 dans `06`                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                              |
| Plans 10 à 15 et cohérence   | ✅ 24/10              | retouches de `02`, `09`, `NOTICE.md` ; P4 reporté dans `03`, `00`, `05` ; section du site renommée `publish:` dans le code (sans alias, erreur explicite) ; `pnpm check`, 23 projets, 1 940 tests, 139 e2e                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                       |
| P4 V2 à V7                   | ✅ 25/10              | chargeur unique, Markdown motion et jumeau JSON typé, CLI fusionné (`render`, `dur`, `extract`, `schema`, `mcp serve`, `dev`), `sites/motion`, `StoryEmbed format=motion`, MCP (`describe_format`, guides), thèmes nommés partagés ; correction finale : `pnpm check`, 37 tâches build/typecheck, 2 182 tests, 32 goldens, 158 e2e deux fois. Restent : réécriture chirurgicale v2 (V5), studio, CI MP4 et Chrome, Vercel `recit-motion`, site npm en double (voir §4)                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                           |
| Audit                        | ✅ 25/10              | [`audit.md`](https://github.com/vguerand/recit/blob/main/audit.md) : 163 constats (3 critiques, 67 majeurs, 93 mineurs), plan d'action en 20 lots ; lot 1 (verrou de release : ST-1, ST-2, ST-10, ST-19, BP-15, DO-19) fait le 25/10 : release manuelle depuis `main`, `alpha` par défaut, `build.yaml` rejoué, dry run, environnement `npm` (relecteurs à cocher par le mainteneur, `SETUP.md` §3), contrôle des version plans dans `pnpm check` (dont `preminor` obligatoire au début d'une phase), plans passés en `preminor`, `release.projects` réduit aux 11 packages publiés, actions épinglées par SHA et npm 11.21.1 exact, Dependabot sans `packages/site` ni auto-approbation (`dependabot.yaml` et `.github/release.yaml` supprimés), test `scripts/__tests__/release-workflow.spec.ts` (commandes passées à nx, SHA, liste des packages) ; corrigés au passage : `nx release` refusait la config git de `nx.json`, nx décalait les bumps en 0.x, `--yes` et `--skip-publish` sont incompatibles dans nx 23 ; reste à faire sur GitHub : relecteurs de l'environnement, premier run réel (le dry run local s'arrête à la GitHub Release, faute de remote github.com) |
| Nettoyage de la racine       | ✅ 25/10              | supprimés : `patches/`, `llm/`, `graphql.config.yml`, `.nxignore`, `.cspell.json`, `.vscode/launch.json` (ajoutés aux chemins supprimés d'`UPSTREAM.md`) ; `.tool-versions` aligné sur pnpm 11.6.0 ; `BACKLOG_IDEA.md` déplacé dans `sites/plans/pages/archive/` (chemins corrigés dans `06`, `11`, `14`) ; `pets.md` exclu d'oxfmt                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                              |
| Broc, couleurs, SETUP        | ✅ 25/10              | workflow `wf_a2e51a50-b24` : `pets.md` intégré aux plans 11, 14, 06 (hatch-pet, Broc), planches de couleurs Q07 dans `brand/boards/colors/` (10 teintes, générateurs par défaut identiques octet par octet), `SETUP.md` réécrit ; vérifié par un relecteur indépendant                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                           |
| Reprise sur machine Linux    | ✅ 25/10 (soir)       | dépôt repris depuis `origin/recit/setup` sur une machine Linux (la précédente était un Mac) ; `pnpm check` remis au vert (mise en forme de 8 fichiers, `scripts/e2e-linux.mjs` renommé en `.js` pour `check:imports`) ; 37 tâches build/typecheck, **2 219 tests** ; lot 2 de l'audit terminé (voir §6) ; décisions du 25/10 reportées dans `01`, `06`, `07`, `11`, `14`, `context.md`, `NOTICE.md`, `pets.md` et `sites/plans/pages/annexes/broc/` ; **goldens rouges pour cause de polices système**, voir §6                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                  |

## 3. Reprendre un workflow interrompu

Les workflows s'arrêtent quand la session Claude Code se ferme. Pour reprendre au même point (les
étapes finies ne sont pas refaites) :

```text
Workflow({ scriptPath: "<script>", resumeFromRunId: "<run id>" })
```

| Run               | Script                                                                                                                                                |
| ----------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------- |
| `wf_9981dbe3-61f` | `~/.claude/projects/-Users-vguerand-Work-presentation-recit/7e3cb9c1-144a-474f-b065-4e64c876acf6/workflows/scripts/recit-p3-story-wf_9981dbe3-61f.js` |
| `wf_17230d86-10e` | `…/workflows/scripts/recit-p4-v1-import-emaki-wf_17230d86-10e.js`                                                                                     |
| `wf_97154b86-d4a` | `…/workflows/scripts/recit-backlog-plans-wf_97154b86-d4a.js`                                                                                          |
| `wf_c5726349-758` | `…/workflows/scripts/recit-plans-integration-wf_c5726349-758.js`                                                                                      |
| `wf_256acca2-bbf` | `…/workflows/scripts/recit-plans-final-consistency-wf_256acca2-bbf.js`                                                                                |
| `wf_361552bf-7dc` | `…/workflows/scripts/recit-p4-motion-wf_361552bf-7dc.js`                                                                                              |
| `wf_5542962e-d0b` | `…/workflows/scripts/recit-audit-wf_5542962e-d0b.js` (audit en lecture seule, écrit `audit.md`)                                                       |
| `wf_a2e51a50-b24` | `…/workflows/scripts/recit-broc-brand-setup-wf_a2e51a50-b24.js`                                                                                       |
| `wf_ef5aeaad-5ab` | `…/workflows/scripts/recit-audit-lots-wf_ef5aeaad-5ab.js` (20 lots de l'audit)                                                                        |
| `wf_d6a804b5-88e` | `…/workflows/scripts/recit-broc-research-wf_d6a804b5-88e.js` (recherche atelier de personnages, bible et 5 scénarios de Broc, décisions du 25/10)     |

Une reprise ne fonctionne que dans la même session. Depuis une nouvelle session, relire ce journal,
regarder l'état réel (`git status`, `pnpm check`, `pnpm test`) et relancer la suite à la main.

**Historique (rapatrié le 24/10, voir §4)** — résultat de P4-V1, sauvegardé hors de `/tmp` :
`/Users/vguerand/Work/presentation/recit-p4-staging` (copie complète du dépôt avec l'import d'emaki,
sans `node_modules`). Tout y est vert : 97 tests d'emaki sur 97, 16 goldens, build, typecheck et
lint des 4 packages. À rapatrier dans le dépôt **après la fin de P3** :
`packages/formats/{motion,motion-blocks,motion-render}`, `packages/mcp`, la suppression de
`packages/tools/extract`, `.nx/version-plans/version-plan-*` de motion, puis réconcilier à la main
`pnpm-workspace.yaml`, `NOTICE.md` et `sites/plans/pages/03-packaging.md`, et régénérer le lockfile
avec `pnpm install`. Le CLI d'emaki, mis de côté pour la fusion avec `@recit/cli`, est dans
`scratch-emaki-cli/index.ts`. Restent ouverts : une seconde version de zod (4.4.3) imposée par
Remotion dans l'arbre de `motion-render`, et des goldens qui dépendent des polices de la machine. À
vérifier ensuite dans tout le dépôt : biome n'applique pas `useImportExtensions` lors d'un simple
`biome lint`, il faut lancer `pnpm biome lint --only=correctness/useImportExtensions`.

## 4. Suite prévue, dans l'ordre

1. ~~Rapatrier P4-V1 et tout revérifier.~~ Fait le 24/10.
2. ~~Intégrer les plans 10 à 15 en une seule passe et appliquer les décisions du §5.~~ Fait le
   24/10.
3. ~~P4 V2 à V7.~~ Fait le 25/10 (voir `sites/plans/pages/05-video.md`). Ouverts : la copie npm de
   `framework@0.93.0` (et un zod 4.4.3 de plus) tirée par le pair optionnel de `@recit/site-stories`
   ; relier le fork casse son typecheck, à traiter avec TE-3. `MotionTheme` reste écrit à la main
   dans `@recit/theme` (sans zod), gardé par un test de types contre `ThemeContract`. 3b. Après P4 :
   appliquer les lots de `audit.md` §6 (workflow), en commençant par TE-1 (captures Linux), CG-1
   (renommer le modèle vidéo « deck » d'emaki), BP-1 à BP-4 (sécurité), ST-5/ST-6 (frontières de
   packages), TE-3 (typecheck des sites).
4. P5 : alpha (`release.projects` déjà rempli dans `nx.json`, publint, size-limit, `create-recit`).
   **Aucune publication npm.**
5. P6 et suivants : report et paged (avec PDF0 et PDF1), toon, book, roadmap, calendar, stories,
   puis les formats des plans 10 à 14.

## 5. Décisions prises sans l'utilisateur (à confirmer)

L'utilisateur a demandé de trancher seul les questions ouvertes. Chaque décision suit la
recommandation du plan concerné et peut être revue.

| Sujet                               | Décision                                                                                                                                                                                                                                                                                                                                                                                                                 |
| ----------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ |
| Webtoon en PDF                      | pages de hauteur fixe coupées entre les cases ; une case trop haute a sa propre page ; CBZ hors périmètre                                                                                                                                                                                                                                                                                                                |
| Table des matières du report        | sans numéros en P6 (Chromium), numérotée en P8 avec Paged.js                                                                                                                                                                                                                                                                                                                                                             |
| Lancement de Chromium               | sous-export `@recit/paged/runner`, pas de nouveau package pour l'instant                                                                                                                                                                                                                                                                                                                                                 |
| Réglages des formats                | une seule convention : à plat, `settings` = section du format ; tailles en valeurs littérales (A4, 6x9in)                                                                                                                                                                                                                                                                                                                |
| PDF de `stories`                    | non : stories produit des images et des vidéos                                                                                                                                                                                                                                                                                                                                                                           |
| PDF des pages de doc                | optionnel, après P8                                                                                                                                                                                                                                                                                                                                                                                                      |
| PDF balisé                          | activé par défaut, désactivable ; un `alt` manquant donne un avertissement                                                                                                                                                                                                                                                                                                                                               |
| Studio d'emaki                      | non importé tant qu'il est simulé ; le squelette `packages/tools/studio` reste                                                                                                                                                                                                                                                                                                                                           |
| `extract` d'emaki                   | fusionné dans `@recit/motion`                                                                                                                                                                                                                                                                                                                                                                                            |
| Plans 10 à 14 (produit)             | tranchées, voir `06` « Questions ouvertes » et le §8 de chaque plan                                                                                                                                                                                                                                                                                                                                                      |
| Section du site dans `recit.config` | renommée `stories:` → `publish:` ; `stories:` est réservé aux réglages du format stories                                                                                                                                                                                                                                                                                                                                 |
| `@recit/paged`                      | outil Node utilisé par la CLI seule ; les formats et fondations n'exposent que des vues imprimables                                                                                                                                                                                                                                                                                                                      |
| Mot `kind`                          | réservé au discriminant des documents JSON ; l'annuaire utilise `entries`, la carte `mode`                                                                                                                                                                                                                                                                                                                               |
| Manifeste `exports.json`            | types ajoutés : webp, gif, svg, jsonld, fountain, skill ; `format` facultatif, source `block` ou `tool`                                                                                                                                                                                                                                                                                                                  |
| Plan 15                             | accepté (questions 1 à 6), questions 7 à 9 ouvertes                                                                                                                                                                                                                                                                                                                                                                      |
| Règle des extensions `.js`          | appliquée par `pnpm check` (`check:imports`), coupée pour `sites/**` et `site-kit` (imports d'assets)                                                                                                                                                                                                                                                                                                                    |
| Release                             | manuelle uniquement (`workflow_dispatch` sur `main`), `alpha` par défaut, version stable refusée en 0.x ; job d'écriture derrière l'environnement `npm` ; en 0.x, version plans en `preminor` (nouvelle phase) ou `prerelease`, jamais `patch`/`minor`/`major` (`scripts/check-version-plans.js`) ; `release.projects` liste les packages publiés un par un ; actions épinglées par SHA ; PR Dependabot relues à la main |
| Modèle vidéo d'emaki (CG-1)         | renommé `Film` (`film.json`) pour libérer « deck » au seul format diaporama — à faire après P4                                                                                                                                                                                                                                                                                                                           |
| « Pet » et skill du brocoli         | **répondu par l'utilisateur le 25/10 (`pets.md`)** : skill `hatch-pet` d'OpenAI (Apache-2.0, vérifié), avec `imagegen` ; outils externes, jamais une dépendance                                                                                                                                                                                                                                                          |
| Kit de Broc (plan 14 D12)           | `character.md` + jumeau `character.json` (bible à une entrée, `BibleSource`) ; champ `sheet` typé par `SpriteSheet` dans `@recit/core/schema` ; image de référence = source de vérité ; layout `codex-pet-v1` (9 lignes), v2 à 11 lignes refusé sans spec publique                                                                                                                                                       |
| Import d'une planche `hatch-pet`    | étape manuelle documentée (copie, `recit assets register`, `::sheet`, `recit validate`) ; `recit import pet` seulement sur besoin répété                                                                                                                                                                                                                                                                                 |
| Rendu des poses (plan 11 D9, IM4)   | bloc `sprite` (fonction pure de la frame, décalage CSS) et `SpriteImage` statique ; pas de Broc en pixel art ; IM4 débloqué, en P16                                                                                                                                                                                                                                                                                      |
| Couleur de la marque (25/10)        | **une teinte par format**, elle remplace la teinte unique du 24/10 : recit.dev en noir et blanc, graphite par défaut (docs, components, outils, `@recit/theme`), deck bleu, motion violet, report sarcelle, toon orange, book ambre, roadmap vert, calendar rouge, stories magenta, atlas indigo. Valeurs dans `brand/boards/colors/palette.json` ; mise en œuvre après les lots de l'audit (`01` §1, `07` §4)           |
| Apparence de Broc (25/10)           | l'image fournie fait foi : brocoli vert souriant, 3D doux, T-pose, **sans smoking ni casquette**. `pets.md`, `11` §2.4 et `14` D12 corrigés d'après elle (note datée en tête de `pets.md`, qui reste une trace d'échange)                                                                                                                                                                                                |
| Licence de l'image de Broc (25/10)  | **© vguerand, tous droits réservés**, hors licence MIT : ligne ajoutée à `NOTICE.md` (le fichier était déjà commité sans elle) ; la planche dérivée hérite de la restriction (`14` D13)                                                                                                                                                                                                                                  |
| Police de titre (25/10)             | **Unbounded** (NT Type, SIL OFL 1.1) remplace Monument Extended, exclue car non libre en usage commercial                                                                                                                                                                                                                                                                                                                |
| lebrocoli.fr (25/10)                | seul nom réel autorisé du dépôt, **lieux fictifs**                                                                                                                                                                                                                                                                                                                                                                       |
| Voix et scénarios de Broc (25/10)   | délégués à l'assistant : bible et 5 scénarios dans `sites/plans/pages/annexes/broc/`, tout ce qui n'est pas visible sur l'image reste `inferred` jusqu'à relecture                                                                                                                                                                                                                                                       |

**Questions qui restent à l'utilisateur** (faits que lui seul connaît). Toutes celles du 24/10 ont
été répondues le 25/10 : « Pet » et le skill du brocoli (`hatch-pet`), le nom et la description du
personnage (Broc), son image de référence et sa licence, la police de titre (Unbounded), le « projet
basic » (atelier de personnages 2D/pixel, `sites/plans/pages/16`), lebrocoli.fr comme seul exemple
réel avec des **lieux fictifs**, et `maps.recit.dev` avec `cartes.recit.dev` en alias. Il reste
**une relecture**, pas une question : valider les traits `inferred` de Broc (voix, personnalité,
palette, personnages secondaires) dans `sites/plans/pages/annexes/broc/`.

## 6. Reprise du 25/10 au soir, sur une machine Linux

La session précédente travaillait sur un Mac. Le dépôt a été repris depuis `origin/recit/setup`
(commit de sauvegarde `770a2775`) sur une machine **Linux x86_64**, Node 22.20 (`.tool-versions`
demande 22.22, simple avertissement de pnpm), Docker vide. Ce changement de machine explique deux
des trois rouges rencontrés.

### Vérification complète

| Étape                             | Résultat                                |
| --------------------------------- | --------------------------------------- |
| `pnpm install`                    | ✅                                      |
| `pnpm check`                      | ✅ après deux corrections (ci-dessous)  |
| `nx run-many -t build typecheck`  | ✅ 37 tâches                            |
| `pnpm test`                       | ✅ 199 fichiers, **2 219 tests**        |
| `motion-render goldens:check`     | ❌ **polices système**, voir ci-dessous |
| `pnpm test:e2e:linux` (conteneur) | ✅ après le lot 2 terminé               |

Deux corrections pour remettre `pnpm check` au vert, toutes deux héritées du lot 2 interrompu :

- 8 fichiers jamais passés à oxfmt (le skill `e2e-baselines`, les six annexes de Broc) : mise en
  forme seule, aucun contenu touché ;
- `check:imports` refusait `import … from "../e2e-linux.mjs"` : le script est renommé
  **`scripts/e2e-linux.js`** (la racine est `type: module`, `.mjs` n'apportait rien) et ses
  références suivent. `scripts/check-admonitions.mjs` garde son extension : il vient d'upstream.

### Goldens : différence de machine, pas une régression

`goldens:check` échoue sur 14 des 32 images, de 0,27 % à 1,63 % de pixels (seuil 0,2 %). Les rendus
JSON et Markdown donnent **exactement** le même écart, donc le jumeau typé n'est pas en cause. La
raison est dans `motion-blocks/src/themes/build.ts` : les thèmes utilisent des piles de polices
**système** (`system-ui, -apple-system, 'Segoe UI', Roboto, sans-serif`). Sur le Mac elles
résolvaient vers SF Pro, ici vers DejaVu / Liberation / Noto — le texte ne tombe pas sur les mêmes
pixels. Les goldens du dépôt sont des références macOS ; **elles n'ont pas été régénérées ici**, ce
qui remplacerait des références Mac par des références Linux sans rien prouver.

C'est exactement le lot 12 de l'audit (TE-5 « goldens en conteneur », BP-14 « polices embarquées »)
: tant qu'il n'est pas fait, `goldens:check` ne peut pas être vert sur deux machines différentes. À
traiter avec le lot 12, en réutilisant le harnais Docker du lot 2.

### Lot 2 de l'audit (captures e2e Linux) — terminé

Le lot était déjà écrit pour l'essentiel (`playwright.config.ts`, `build.yaml`, les scripts, le
skill). Restaient trois défauts, trouvés en l'exécutant pour de bon :

1. **Le conteneur ne démarrait pas.** La copie du dépôt n'a pas de `.git` (exclu du `tar`), or le
   script `prepare` de la racine est `lefthook install`, qui en exige un : `exit status 128`. Puis,
   une fois `git init` ajouté, git refusait un dépôt appartenant à un autre utilisateur (« dubious
   ownership »), le conteneur tournant en root. `scripts/e2e-linux.sh` déclare maintenant
   `safe.directory` et initialise un dépôt vide, avant `pnpm install`.
2. **Les références revenaient en `root:root`.** Les 206 `-linux.png` écrits par le conteneur
   étaient illisibles en écriture pour l'utilisateur, alors qu'ils sont faits pour être relus et
   commités par lui. Le script les rend maintenant à l'utilisateur de `/src` (`chown` d'après
   `stat -c '%u:%g' /src`).
3. **Un test instable, et c'en était un vrai.** `/documentation/reference/architecture` prenait sa
   capture pleine page avant que Mermaid n'arrive : 4 615 px sans le diagramme, 4 861 px avec
   (mesuré, pas supposé). Mermaid est un chunk paresseux, chargé après `document.fonts.ready`.
   `e2e/support.ts` a un `mermaidReady()` sur le modèle de `filmsReady()`, et les specs listent les
   pages concernées (`DIAGRAM_PAGES` dans `site-docs`, le cas `mermaid` dans `site-components`). Les
   deux formes du DOM sont couvertes : `pre.mermaid svg` (pipeline Markdown) et le SVG du composant
   MDX, reconnu par son `aria-roledescription`.

4. **Deux autres captures instables, du même genre, trouvées en relançant.** Une page de doc qui
   embarque un deck était capturée pendant la transition d'entrée
   (`/theme/aspect-ratio-and- transitions`, environ 5 % de pixels d'écart) : `decksReady()` attend
   maintenant les decks embarqués, sans liste de pages à tenir à jour, puisqu'il ne fait rien quand
   la page n'en a pas. Et `/formats/motion`, page de 8 860 px sans contenu dynamique, oscillait de
   **2 pixels** de hauteur d'une capture à l'autre ; sur l'hôte elle est parfaitement stable (8 860
   px six fois de suite), l'écart n'apparaît que dans le conteneur sous charge parallèle, quand une
   ressource tardive arrive après `document.fonts.ready`. `heightStable()` attend que la hauteur ne
   bouge plus avant de capturer.

5. **Un dernier reste, sur `/theme/dark-mode`.** La page embarque
   `<StoryEmbed slug="course" slide={7} />` : le deck se monte sur la diapo 1 puis saute à la 7.
   L'attente plate de 700 ms de `decksReady()` était un pari sur le temps ; elle attend maintenant
   que les compteurs de diapo (`.slide-footer-right`) cessent de bouger, puis laisse 700 ms à la
   transition.

Les tests couvrant ces correctifs sont dans `scripts/__tests__/e2e-linux.spec.ts` (12 tests).

**Vérification finale** : `pnpm test:e2e:linux` lancé **deux fois de suite**, machine libre, les
deux fois **158 tests passés, 0 échec, 0 instable** (1,6 min et 1,5 min). C'est la double passe que
demande le skill `e2e-baselines`. À noter pour la prochaine fois : ne rien lancer d'autre pendant
une passe conteneur. Avec `pnpm test` en parallèle sur l'hôte, la passe tombe à 4,3 min et fait
apparaître des échecs qui n'ont rien à voir (un `page.goto` qui expire au bout de 60 s). Les 206
références `-linux` sont générées et appartiennent à l'utilisateur ; `CONTRIBUTING.md` et le skill
`e2e-baselines` disent maintenant qu'une capture instable est un **temps d'attente manquant**, pas
une référence à réécrire.

**Vu une fois, non reproduit** : lors de la première génération, `/demos/mdx` a signalé une erreur
React #418 (texte rendu côté serveur différent du client) dans la console, sur les deux essais. Ni
la passe de comparaison suivante ni un essai sur l'hôte ne l'ont reproduite. À surveiller : si elle
revient, c'est une vraie erreur d'hydratation, pas une instabilité de capture.

### Lots de l'audit faits le 25/10 au soir

Trois analyses en parallèle (lots 3, 5, 6 et 7) ont précédé l'écriture. Elles ont **corrigé
l'audit** sur plusieurs points, chiffre en main — c'est le principal apport de la méthode :

| Point                 | Ce que disait l'audit                       | Ce que la mesure montre                                                                                                                                |
| --------------------- | ------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------ |
| BP-12 `clobberPrefix` | relever le préfixe et adapter `slideAnchor` | le préfixe serait **doublé** (`mdast-util-to-hast` le pose déjà des deux côtés) et casserait tous les liens de notes ; `slideAnchor` est hors pipeline |
| BP-3 jeton de dev     | « jeton lisible »                           | le jeton est **servi en clair dans le HTML**, donc les deux contrôles existants ne protégeaient rien                                                   |
| TY-11                 | « utiliser `BrandInputSchema` »             | ce schéma **n'existe pas**, il faut l'écrire                                                                                                           |
| ST-10                 | `release.projects` à corriger               | **déjà corrigé**                                                                                                                                       |
| MCP                   | lignes 391-402, 522…                        | lignes décalées d'environ +50                                                                                                                          |

**Lot 10** (couverture des récits). TE-2 était déjà fait pour l'essentiel : le glob existe. Ce qui
manquait est le garde-fou — un test qui parcourt `examples/` et `sites/` et échoue sur tout récit
hors du glob (mesuré : 29 récits couverts, 63 pages de doc hors périmètre, 0 oublié). TE-6 : le
plugin Vite remonte enfin les diagnostics, et a **immédiatement trouvé** un diagramme Mermaid sans
titre accessible dans `deck-tech-talk`. PL-1 : la règle du jumeau était écrite trois fois et se
contredisait ; le code avait raison, `AGENTS.md` la dit maintenant une seule fois.

**Lot 3** (sécurité du rendu). Le XSS était exploitable de bout en bout : `:::component` ou du HTML
brut forgeant `.slide-component` → `CodeBlock` avec un langage inconnu → Shiki lève → le repli
concaténait `<pre><code>${code}</code></pre>`. Repli en JSX, liste blanche des 11 `data-*` du
pipeline à la place de `data*`, `id`/`name` restreints, `parseProps` filtré, props de confiance
appliquées après l'étalement, et `security.allowHtml` devenu **la seule source, à `false` par
défaut**. Aucun récit du dépôt n'utilisait de HTML brut.

**Lot 5** (sécurité réseau et MCP). Les outils MCP lisaient et écrivaient n'importe où, et
`formatOf` traite toute extension non-Markdown comme du JSON : un `out` non vérifié écrasait sa
cible. Ils sont confinés à une racine (`--root`, `RECIT_MCP_ROOT`, défaut : le dossier courant).
`@recit/motion-render` passe en pair optionnelle. Le serveur de dev exige un jeton sur **toutes**
ses routes hors localhost, avec deux jetons séparés (écran / téléphone) et un cookie `HttpOnly`.
`/__assets` ne sert plus que des médias.

**Lot 6, début.** `@recit/site-stories` était typé contre le `@recit/site` de npm au lieu du fork
(TE-4). Le doublon a disparu du lockfile. Les 28 erreurs que cela révèle viennent **toutes** des
sources du fork, compilées avec des options plus strictes que les siennes : `tsconfig.site.json`
réaligne les trois qui divergent, et le package est à zéro erreur.

**Non appliqué, et pourquoi.** L'annexe ST-8 demandait `platform: "node"` pour `motion-render` :
mesuré, tsdown émet alors `.mjs`/`.d.mts` alors que les `exports` pointent sur `dist/index.js` —
plus aucun consommateur ne résout le package. La raison est écrite à côté du réglage.

**Dette connue, non corrigée ici.** Le diagramme Mermaid de `examples/deck-tech-talk/slides.md:54`
n'a pas de titre accessible. Le corriger affiche un `<h4>`, donc déplace 4 captures sur **deux**
plateformes ; seules les `-linux` sont régénérables ici. À faire quand un Mac est disponible.

### Doc : audit d'uniformité et vision.recit.dev (25/10 au soir)

Un audit d'uniformité a mesuré les **69 pages** des sites plus la doc racine. Il a trouvé une
contradiction qu'aucun lecteur ne pouvait démêler : **le statut du format `motion` était déclaré de
douze façons**, dont trois incompatibles — « available » sur une page et, vingt-cinq lignes plus
bas, sous le titre « Planned formats » ; « Planned » en bas des 15 pages de components ; « planned »
dans le README ; « In progress » dans le sidebar. Et cinq vocabulaires pour trois états.

Corrigé à la racine : un manifeste **`FORMATS` unique dans `@recit/site-kit`** (id, nom, « une scène
est… », statut, phase, site), lu par la grille de recit.dev, le tableau « In other formats » de
components, et désormais par le site vision. Deux états publics seulement : `Available` et
`Planned · P<n>` — « coming soon », « in progress », « shipped » et « specified » sont bannis. Un
test e2e vérifie la cohérence.

Autres corrections grave :

- `README.md` donnait `npm create recit` (le package est un `export {};` vide, le nom n'est pas
  réservé) et `recit export pdf` (la commande n'existe pas) — c'était la première instruction lue
  par un visiteur de GitHub. Remplacé par la recette vérifiée de recit.dev ;
- « emaki », nom de code abandonné, restait dans la référence CLI publique ;
- `showCopyPage: false` sur l'accueil de la doc : clé **inexistante**, le bouton restait affiché.
  Plus important que la correction : `scripts/check-frontmatter.mjs` refuse désormais toute clé de
  front matter qu'aucun code ne lit, branché dans `pnpm check` (vérifié : il attrape bien la faute)
  ;
- `@recit/extract` figurait dans la page Architecture comme package planifié — il n'existe pas ;
- **`UPSTREAM.md` aurait effacé le CHANGELOG de Récit** à la première synchronisation après une
  release : il était classé dans les chemins supprimés d'upstream alors que `nx release` l'écrit.
  Passé en « Ours », et la commande `checkout --ours` complétée des cinq fichiers qu'elle oubliait
  plus l'étape « Moved ».

**`vision.recit.dev`** (`sites/vision`, port 3015, e2e 4315) : six pages — la thèse, le problème,
les neuf formats et ce qui en mérite un dixième, les principes **avec leur prix**, l'ordre des
phases, et ce que Récit refuse de faire, risques compris. Il ne documente rien (règle anti-doublon
de `07`) et n'annonce aucun format comme acquis. Il n'est pas dans le sélecteur d'en-tête, comme
recit.dev ; on l'atteint par le pied de page.

**63 captures `-darwin` sont désormais périmées** : ajouter un site ajoute un lien au pied de page
de tous les autres, et les corrections de doc changent trois pages. Les `-linux` sont régénérées et
vertes ; les `-darwin` ne peuvent pas l'être ici. Sur un Mac : `pnpm build && pnpm test:e2e:update`.
Elles sont laissées en place plutôt que supprimées, pour que l'écart échoue bruyamment au lieu
d'être réécrit sans relecture.

### Reste à faire, dans l'ordre

1. Lots faits : **1, 2, 3, 5, 10**, et le début du **6** (TE-4). Restent : 4 (vocabulaire motion),
   la suite du 6 (typecheck des sites et de `e2e/`), 7 (frontières de packages), 8, 9, 11 à 20.
2. Lot 12 : polices embarquées et goldens en conteneur, qui débloque `goldens:check`.
3. Relecture par l'utilisateur des traits `inferred` de Broc (`sites/plans/pages/annexes/broc/`).
4. Mise en œuvre des couleurs par format (`01` §1).
5. P5 et la suite (`06-roadmap.md`).

Ordre conseillé par l'analyse, mesuré : le reste du lot 6 (les scripts `typecheck` ne doivent être
commités qu'avec le tsconfig qui les rend verts, sinon la CI part avec 6 × 23 erreurs), puis 7.1 et
7.2 (tags nx et test d'arêtes, en tolérant les deux violations connues) **avant** les lots 11 et 13
qui déplaceront du code entre packages.

### Lot 6 de l'audit (typecheck partout) — 26/10

TE-3 disait « les sites, site-kit, les exemples et `e2e/` ne sont jamais typecheckés, alors que des
erreurs existent ». Mesuré avant : **11 projets** avaient une cible `typecheck` ; après : **27**.

Les tsconfig des sites étaient des copies du gabarit du framework de sites, sans `extends` ni
`strict` : six fichiers identiques qui n'héritaient de rien et incluaient des chemins absents
(`sidebar.ts` dans www, presentation, motion et vision ; `src` dans presentation et vision). Ils
étendent maintenant `tsconfig.site.json` — celui écrit pour TE-4, puisqu'un site compile les sources
du fork — avec un `include` exact. Même chose pour `site-kit` et pour `recit`.

Les 23 erreurs de site-kit et les 28 de `recit` venaient **toutes** des sources du fork compilées
plus strictement que lui : zéro après le changement d'`extends`, sans toucher une ligne de code. Les
4 de www et les 28 de components étaient un `@types/react` manquant.

`recit` est un package écrit à la main, sans build (ST-13) : ses `.js` et `.d.ts` sont désormais
vérifiés tels qu'ils sont publiés (`allowJs`, `checkJs`), et ses `exports` passent en
`{ types, default }`.

`e2e/` devient un projet du workspace (`recit-e2e`), ce qui lui donne sa cible `typecheck` et fait
voir à nx son arête vers `@recit/cli` — elle existait déjà dans le code, non déclarée. Son tsconfig
couvre aussi `playwright.config.ts`, qui vit à la racine mais appartient à cette suite.

**Deux vrais défauts, trouvés par le typecheck lui-même.**

1. `e2e/support.ts` réimportait `serveStatic` depuis `packages/cli/dist/commands/export-pdf.js`, un
   chemin interne que les types publiés ne déclarent pas (tsdown ne sort dans le `.d.ts` que ce qui
   est atteignable depuis l'entrée). La suite marchait parce que le JS, lui, l'exporte.
   `serveStatic` est maintenant exporté par l'entrée publique de `@recit/cli`, et `e2e` passe par
   elle comme pour `buildProject`, `exportPdf` et le reste.
2. `e2e/site-i18n.spec.ts` destructurait un `string[][]` : `href` était `string | undefined`, donc
   l'appel `toHaveAttribute(name, href)` choisissait silencieusement la surcharge
   `toHaveAttribute(name, options)` — l'attribut n'était pas comparé à la valeur attendue. Le
   tableau est passé en `as const`.

**Non fait, et pourquoi.** Les 11 packages planifiés (`create-recit`, `data`, `time`, les six
formats non commencés, `studio`) n'ont qu'un `export {};` : une cible `typecheck` y ajouterait onze
dépendances et onze tâches de CI qui ne peuvent rien attraper. À poser en même temps que leur
premier vrai fichier.

**Vérifié** : `pnpm check` vert, `nx run-many -t build typecheck` **54 tâches, 54 vertes** (contre
37 avant).

### Lot 7, étapes 1 et 2 (frontières de packages) — 26/10

ST-6 : aucun `scope:*` dans le dépôt, donc la règle de direction de `sites/plans/pages/03` §8
n'était vérifiée par rien — c'est ainsi que ST-5 est passé. Et la liste des tags était écrite deux
fois, en se contredisant : six dans `sites/plans/pages/06`, quatre dans `sites/plans/pages/03` §8.

**Une seule liste, neuf tags**, dans `sites/plans/pages/03` §8 : `foundation`, `format`,
`integration`, `tool`, `distribution`, `site`, `example`, `test`, `vendor`. Posés dans les 41
`package.json` qui nous appartiennent. Les deux projets venus d'upstream (`packages/site`,
`examples/with-config`) ne sont **pas** tagués : les taguer ajouterait un patch à reporter à chaque
synchronisation, pour rien. Le script les classe `vendor` par leur nom, et c'est écrit à côté de la
liste.

`scripts/check-package-boundaries.js` lit les tags et les dépendances `workspace:*`, et refuse toute
arête hors du tableau. Deux choix qui comptent :

- la liste des violations tolérées porte, pour chacune, le constat qui la possède ;
- **une entrée tolérée qui ne correspond à aucune arête échoue aussi.** Sans cela, la liste survit à
  sa raison d'être et la règle s'affaiblit en silence.

Vérifié pour de vrai : une dépendance `@recit/core` → `@recit/deck` ajoutée à la main fait bien
échouer `pnpm check`, avec le nom des deux scopes. Dix tests dans
`scripts/__tests__/check-package-boundaries.spec.ts`, dont trois qui mesurent le dépôt lui-même.

**Une seule violation aujourd'hui** : `@recit/site-stories` → `@recit/cli` (ST-5), qui attend le
déplacement de la lecture des fichiers dans `@recit/core/node`, après P4.

**ST-20 réglé pour la partie déclarative.** Trois sites importaient
`../../examples/deck-mdx/components` et `deck-showcase/components`, et `site-i18n` importait les
composants et le récit de `deck-showcase`, sans qu'aucun `package.json` ne le déclare : nx ne voyait
pas l'arête, et un build affecté pouvait sauter un site dont un exemple venait de changer. Les
quatre dépendances sont déclarées. Reste de ST-20 : donner à `site-i18n` ses propres récits, ou le
retirer de la liste degit (DO-2).

### Une instabilité e2e réelle, pas une régression des lots 6 et 7 — 26/10

Les passes e2e de la nuit du 26/10 ont montré, par intermittence, **React #418** (« le texte rendu
par le serveur ne correspond pas à celui du client ») sur les **deux pages qui embarquent un deck
MDX** : `/demos/mdx` de presentation.recit.dev et `/documentation/writing/mdx` de docs.recit.dev. Le
test échoue par `expect(errors).toEqual([])` : la page s'affiche juste, la capture passe, c'est
l'erreur de console qui est refusée.

Ce qui a été mesuré, et écarté :

| Hypothèse                                           | Mesure                                                                                  |
| --------------------------------------------------- | --------------------------------------------------------------------------------------- |
| Régression des lots 6 ou 7                          | aucun code de rendu n'y change ; la passe du lot 6 était à **166/166**                  |
| `examples/*/dist` présent pendant le build du site  | reconstruit avec et sans : 6/6 vert dans les deux cas                                   |
| Build non déterministe                              | deux builds comparés fichier à fichier : seuls diffèrent les horodatages `dehydratedAt` |
| Ordre de build changé par les dépendances déclarées | `nx run presentation:build` puis 3/3 vert                                               |
| Charge de la machine                                | **confirmé, c'est la cause** (voir ci-dessous)                                          |

**La mesure qui tranche.** La même passe conteneur, relancée **sur le commit du lot 6** — celui qui
était à 166/166 — est rouge elle aussi : 165 passés, un échec, mais sur un **autre** test
(`site-embed`, un timeout de `page.goto`), et en **4,1 min au lieu de 2,0**. Le code n'y est donc
pour rien : c'est le temps. Une autre suite Playwright tournait en parallèle sur la machine. Le test
qui tombe change à chaque passe : `site-i18n`, puis les deux pages MDX à la fois, puis `site-embed`.
Isolé, chacun passe : 16 fois de suite pour `/demos/mdx`, et jusqu'à 6× de bridage CPU sans
reproduire.

L'erreur est donc **réelle mais masquée par le temps** : l'hydratation du deck arrive après
l'assertion quand la machine est libre, avant quand elle est chargée. Ce n'est pas un défaut du test
: c'est la page qui hydrate avec un texte différent de celui du serveur. À instruire comme un
constat à part entière — la CI est un conteneur chargé, elle verra l'erreur.

**Piste suivante** : comparer le DOM prérendu et le premier rendu client sur `/demos/mdx` avec un
React non minifié (le message minifié ne donne pas le texte fautif). Le deck MDX contient une
expression (`export const year = 2026`, puis `{year}`) et un composant à état (`<Counter />`) ; les
deux sont les candidats à vérifier d'abord.

### Lot 4 de l'audit (vocabulaire motion) — 26/10

CG-1 était le seul constat **critique** : « deck » désignait deux choses incompatibles — le
diaporama, et le modèle de rendu vidéo. Le modèle de rendu s'appelle maintenant **film**, partout et
d'un seul tenant :

| Avant                               | Après                                               |
| ----------------------------------- | --------------------------------------------------- |
| `DeckSchema`, `Deck`, `Scene`       | `FilmSchema`, `Film`, `FilmScene`                   |
| `parseDeck`, `parseDeckSource`      | `parseFilm`, `parseFilmSource`                      |
| `readDeckFile`, `writeDeckFile`     | `readFilmFile`, `writeFilmFile`                     |
| `storyToDeck`, `deckToMarkdown`     | `storyToFilm`, `filmToMarkdown`                     |
| `DeckFormat`                        | `SourceSyntax` (il nommait la syntaxe, pas un deck) |
| `*.deck.json`, `--to deck`          | `*.film.json`, `--to film`                          |
| `build_deck`, `validate_deck` (MCP) | `build_film`, `validate_film`                       |
| `recit://schema/motion`             | `recit://schema/motion-film`                        |

**Un piège trouvé en chemin.** `deckDuration` (le temps de lecture déduit du nombre de mots de la
voix off) et le `filmDuration` de `motion-blocks` (la durée réelle, en images) seraient devenus le
même nom. Ils mesurent deux choses : le premier s'appelle désormais **`readingDuration`**. Le
renommage aveugle aurait fait un doublon silencieux — c'est le typecheck qui l'a dit.

**Ce qui n'a pas été renommé, et pourquoi.** `recit init --format deck`, `describe_format deck`,
`LoadedDeck`, `DeckSource`, l'option `decks:` et l'attribut `data-deck` parlent du diaporama : ils
restent. (`data-deck` deviendra `data-story` au lot 15, CG-3.)

**CG-2 et TY-9, la phrase qui manquait.** La ressource MCP disait « le schéma du deck auquel toute
story se compile », ce qui désignait le modèle de rendu comme s'il était le jumeau. Elle dit
maintenant que le **jumeau typé est la source `kind: "story"`**, et que le `film.json` est un
artefact de build. La même phrase est dans `AGENTS.md`, le README de motion et `formats/motion.mdx`.

**DO-22** : « emaki » ne reste que dans les lignes d'attribution.

Vérifié : `pnpm check`, build et typecheck de 41 projets, suite unitaire complète.

### Lot 8 de l'audit (un seul système de types), première moitié — 26/10

La règle d'`AGENTS.md` dit « dériver les types TS des schémas zod ». Cinq endroits faisaient
l'inverse, ou les deux à la fois.

**TY-4 et TY-1 : les listes écrites deux fois.** `SlideLayout` était une union écrite à la main,
puis `SLIDE_LAYOUTS` recopiait les dix mêmes chaînes, et le zod dérivait du tableau. Idem pour
`SlideTransition` et `StoryFormat`. Les ratios étaient pires : `AspectRatioSchema` dans
`config/validate.ts`, `SLIDE_ASPECT_RATIOS` dans `parser/frontmatter.ts`, et les dimensions 1280×720
écrites une fois de plus dans `parser/aspect-ratio.ts`. Tout cela vit maintenant **une seule fois**,
dans `types/index.ts` : un `z.enum`, puis `z.infer` pour le type et `.options` pour le tableau.
`SLIDE_DIMENSIONS` donne les pixels, et `aspect-ratio.ts` les lit au lieu de les réécrire.

**TY-3 : le front matter était casté.** `footer:` et `rules:` passaient par un `as` : en Markdown,
`rules: { maxWordCount: "abc" }` passait sans un mot, alors que le même contenu en JSON était
refusé. Les deux clés sont maintenant lues par `FooterConfigSchema` et `RulesConfigSchema`, celles
du jumeau ; une valeur refusée est ignorée **et** remonte en diagnostic `frontmatter-invalid`. Trois
tests le prouvent, dont celui de l'audit mot pour mot.

**TY-2 : les attributs de séparateur vérifiés à la main.** `split-slides.ts` contrôlait `layout`,
`transition`, `cols` et `step` à la main, avec des casts, et n'émettait que des _warnings_ ; le
chemin JSON, lui, utilisait zod et levait des erreurs. Il y a maintenant un `SlideMetadataSchema`
(looseObject) dont `SlideMetadata` dérive, utilisé par les deux. Les messages d'avertissement sont
inchangés, au mot près, et `step` — que l'ancien type ne déclarait pas — est enfin validé.

**TY-8 : deux `ThemeMode`.** Celui de `@recit/deck` valait `"light" | "dark"`, celui de core
ajoutait `"system"` : deux types, même nom, exportés tous les deux. Celui du deck s'appelle
`ResolvedThemeMode` — le mode réellement appliqué au DOM — et le deck importe `ThemeMode` et
`SlideTransition` de core. Le cast de `Presentation.tsx` disparaît avec eux.

**TY-7 : `isLocaleTag` existait deux fois**, avec deux définitions de « valide » (`length === 1`
d'un côté, `[0] !== undefined` de l'autre). Une seule reste, dans `i18n/locales.ts`, et
`I18nOptions` dérive désormais de `I18nConfigSchema` au lieu de le recopier.

**Reste du lot 8** : TY-5 (six formes de diagnostic), TY-6 (types publics manquants), TY-12 à TY-17,
TE-13.

### Lot 9 de l'audit (forme des packages publiés) — 26/10

**ST-3, le piège le plus coûteux.** Les onze packages publiés exposaient une condition d'export
`"development"` pointant vers `src/*.ts`, et ils publient `src`. Or **webpack et Next activent
`development` par défaut**, et ne transpilent pas le TypeScript de `node_modules` : un utilisateur
de Next aurait reçu nos sources et un build cassé, sans rien avoir demandé. La condition s'appelle
désormais **`@recit/source`** — un nom qui n'appartient qu'à nous, donc jamais activé par accident.
`vite.config.mjs` la demande explicitement, et `sites/plans/pages/03` §8 comme le skill
`recit-import` le disent.

**ST-4** : aucun package n'avait de `publishConfig`, donc un package scopé aurait été publié en
`restricted` — invisible. Tous ont maintenant `access: public`, `engines`, `author`, un `LICENSE.md`
et un `README.md` portant la mention verbatim qu'exige la règle 6 de `NOTICE.md`. Les packages issus
d'emaki portent aussi le copyright de Yathavan Prabhakar. `nx release publish` passe
`--access public` en plus.

**ST-11** : `files: ["src"]` embarquait `src/**/__tests__` et ses fixtures dans les tarballs. Exclu,
avec les `*.spec.*` et `*.test.*`.

**ST-12** : `sideEffects: ["**/*.css"]` était recopié dans core, components, deck et cli, qui n'ont
aucun CSS — le bundler gardait du code mort par précaution. Passés à `false`.

**Le garde-fou** : `scripts/__tests__/published-packages.spec.ts` vérifie les onze packages sur six
points (public, engines, author, MIT ; dépôt et bugs pointant sur Récit ; README avec la mention et
LICENSE ; pas de condition `development` ; pas de tests dans `files` ; `sideEffects` CSS seulement
avec du CSS). **66 assertions**, qui échouent dès qu'un package naît sans sa forme.

**ST-7 n'est pas réglé** : `@recit/site-stories` dépend toujours d'un patch du fork non publié.
C'est une décision à prendre avant P5 (faire accepter le patch en amont, publier
`@recit/site-stories-core`, ou générer un `recit.config.ts`) ; la limite est maintenant écrite dans
son README.

### Lots 17 et 14 de l'audit (synchronisation, code mort) — 26/10

**Lot 17 — la synchronisation amont.** DO-3 et DO-4 avaient été réglés le 25/10 (CHANGELOG en « Ours
», commande `checkout --ours` complétée). Restaient trois choses :

- **SK-10, la plus dangereuse** : le remote `upstream` acceptait les `push` — vers le dépôt
  d'origine. Une faute de frappe suffisait à y pousser Récit. `UPSTREAM.md` fait maintenant poser
  `git remote set-url --push upstream DISABLED` dès l'ajout du remote.
- **SK-2** : la règle « jamais de commit ni de push sans demande » n'existait que dans la tête de
  Claude ; `UPSTREAM.md` faisait même commiter à l'étape 7. La règle est dans `AGENTS.md`, et
  l'étape 7 est marquée **HUMAN ONLY**.
- **SK-7** : le skill et `UPSTREAM.md` lançaient deux listes de vérifications différentes. Le skill
  renvoie maintenant à l'étape 6, la seule.
- **DO-20** : la description du patch 2 nommait `build.dependsOn` alors que le diff porte sur
  `build:ci.dependsOn`, et la cible `copy:docs` copiait `../../docs/pages/docs/`, un dossier déplacé
  il y a longtemps. La description est corrigée et la cible supprimée.

**Lot 14 — le code mort.** Sept fichiers de `@recit/deck` n'étaient atteints que par `src/index.ts`
— donc exportés publiquement, et jamais montés : le service worker PWA (qu'aucune commande n'écrit),
`useOfflineStatus`, `SlideEmbed` (avec un 1280×720 en dur et une prop jamais lue), `useAutoPlay` (un
`loop` jamais lu), `AnimatedProgress` (dont la couleur par défaut `var(--primary)` est un triplet
HSL brut, donc invalide), et la paire `AudioRehearsalPlayer`/`useAudioRehearsal` qui ne s'utilisait
qu'elle-même. Supprimés, avec leurs trois tests et les trois clés i18n qui n'existaient que pour
eux.

**CM-3, le plus gênant** : `unified-pipeline.ts` portait un **second pipeline Markdown**, sans
sanitisation ni plugins de slide, et `pipeline.spec.ts` le testait — c'est-à-dire qu'il testait du
code que le produit n'exécute jamais, pendant que le vrai pipeline (`compile-story.ts`) n'était pas
couvert par ces cas. Le second pipeline est supprimé ; le spec appelle `compileStory`. Il a fallu
préchauffer la coloration syntaxique : le premier appel dépasse les 5 s par défaut.

### Lot 15 de l'audit (vocabulaire et configuration), première moitié — 26/10

**CG-3** : la section `stories:` avait été renommée `publish:` parce qu'elle publie des **récits**,
pas des diaporamas — mais ses clés disaient encore « deck ». `publish.decks` devient
`publish.entries`, `publish.loadDeckConfig` devient `publish.loadStoryConfig`, et l'API interne suit
: `DeckSource` → `StorySource`, `collectDeckSources` → `collectStorySources`, `compileDeckSources` →
`compileStorySources`.

Les deux anciennes clés ne sont pas simplement retirées : elles sont **refusées par leur nom**, avec
la phrase qui dit où aller (`z.never` + message), comme le fait déjà `stories:`. Un fichier de
config qui les utilise ne se tait pas, il explique. Deux tests le vérifient.

**CG-10** : la CLI utilise déjà `-o, --output` partout ; seule une ligne d'aide parlait encore de
`deck.json`. Le regroupement des commandes motion (`recit motion extract|dur|render`) reste à faire.

**Reste du lot 15** : la fusion de `slides:` dans `deck:` (CG-5), `theme` → `colorMode` pour le mode
d'affichage (DO-6), `data-deck` → `data-story`, et le glossaire de CG-6. Ce sont des changements
d'API visibles : ils méritent d'être faits d'un bloc, avec leurs messages de renommage, pas à la
sauvette.

### Lot 19 de l'audit (hygiène du dépôt) — 26/10

**DB-18, le plus utile.** Vingt-quatre dépendances partagées étaient épinglées à la main dans
plusieurs manifestes : `motion` 12.43.0 écrit quatre fois, `tailwindcss` 4.3.3 quatre fois, `shiki`
et `yaml` trois fois chacun… Seul `zod` passait par le catalogue pnpm, que le skill `recit-import`
exige pourtant. Les vingt-quatre sont dans le catalogue ; neuf manifestes pointent dessus par
`catalog:`. Les **peers** (`react: >=19.2.7`) restent tels quels : un peer est un contrat
d'intervalle, pas une épingle.

**SK-9** : il n'existait aucune commande locale équivalente à la CI. `pnpm verify` fait `check` +
`build` + `typecheck` + `test`, et `pnpm verify:e2e` y ajoute les e2e. `pnpm typecheck` existe
aussi.

**CM-9** : `build:clean` lançait `tsc --build --clean` alors qu'il n'y a pas de `tsconfig.json`
racine — il ne pouvait que casser ; `reset` l'appelait. `reset` est devenu `nx reset`, `build:clean`
a disparu. `check-admonitions.mjs` ignorait `sites/docs/pages/docs/markdown/admonitions.md`, un
chemin déplacé : il ignore désormais le vrai.

**SK-5** : `pnpm fix` ne corrigeait pas les extensions d'import (la règle ne s'applique qu'avec
`--only`), alors que `pnpm check` les refuse. On pouvait donc lancer `fix` puis voir `check` échouer
sur ce que `fix` prétend corriger. `fix:imports` est ajouté et appelé par `fix`.

**SK-6 et BP-20** : les commandes `nx` nues (introuvables sans `pnpm exec`) sont corrigées dans la
doc, les skills et les cinq `vercel.json`.

**CM-10 et DO-13** : `.gitignore` visait un exemple disparu ; `sites/docs/lychee.toml` s'annonçait
encore le nom de la doc d'origine et excluait les URL du dépôt d'origine au lieu de celles de Récit.
Corrigé, et les domaines `*.recit.dev` — pas encore en ligne — sont exclus explicitement, avec la
raison.

### Lot 16 de l'audit (documentation d'entrée) — 26/10

**CG-7, le plus visible.** `AGENTS.md` — que `CLAUDE.md` pointe, donc le premier fichier que lit
tout agent — s'ouvrait sur « # le framework de sites Codebase Guide ». Il citait
`packages/create-site` (supprimé), donnait `pnpm biome ci` au lieu de `pnpm check`, un mauvais
chemin de plugins, et la section Récit commençait à la ligne 161 sur 185. Inversé : **Récit
d'abord**, avec un bloc « Start here » (`pnpm verify`), un tableau de commandes, et le prérequis e2e
que rien ne disait (construire avant de lancer, ports dans `e2e/ports.ts`). Le guide le framework de
sites garde sa valeur pour `packages/site` : il est en fin de fichier, annoncé comme tel. Trois
règles qui disaient « toujours » (`SiteError`, `site/ui`, `site/icons`) disent désormais **où**
elles valent.

**DO-1 et DO-2** : la doc proposait quatre installations incompatibles, dont
`pnpm add -D @recit/cli` pour un package qui n'est pas publié. Un seul encadré, le même mot pour
mot, dit que les packages arrivent avec l'alpha et donne la recette qui marche aujourd'hui ; la page
des gabarits dit que `degit` ne marche pas encore hors du dépôt.

**DO-8** : `sites/plans/pages/03` interdisait la sous-commande `export html` puis annonçait
`export pdf`, `export images`, `export epub`… Une seule règle, écrite une fois : **`export <file>`
avec `--to <format>`**, jamais de sous-commande par format. Reporté dans les onze plans qui
l'écrivaient autrement.

**Reste du lot 16** : la réorganisation du sidebar de docs.recit.dev (DO-5) et la page Architecture
générée depuis les package.json (DO-7).

### Lots 18 et 20 de l'audit (plans, doc des sites) — 26/10

**Lot 18 — les plans.**

- **DO-9** : `sites/plans/pages/00-cadrage.md` s'annonçait « point de reprise » alors que
  `context.md` et `AGENTS.md` désignent `sites/plans/pages/journal.md`. Il porte maintenant un
  encadré « Historique » : c'est le cadrage du 24/10, avant la première ligne de code, et on ne le
  met plus à jour.
- **DO-15** : `SETUP.md` listait des étapes faites. Un encadré dit ce qui reste **vraiment** à faire
  à la main (§3.3 Vercel et DNS, §3.4 jeton npm) ; le reste décrit comment c'est branché.
- **DO-18** : la règle 3 de `NOTICE.md` exige une ligne par chemin, et il n'y en avait aucune pour
  `sites/plans/pages/`, `context.md`, `SETUP.md`, `audit.md`, `.claude/skills/` ni
  `.nx/version-plans/`. Ajoutée. `sites/*` était attribué à Récit sans réserve, alors que
  `sites/docs` hérite de pages du framework de sites : la réserve est écrite.
- **DO-21** : les chemins de ma machine (`/Users/vguerand/…`, `/private/tmp/claude-501/…`) sont
  partis des plans et de `context.md`. Seul le journal les garde, parce qu'il raconte l'histoire.

**Lot 20 — la doc des sites.**

- **DB-17** : deux cartes de `presentation.recit.dev` pointaient sur **la racine** de docs au lieu
  de la page qu'elles décrivent — un lien faux, pas un style. Corrigées. `GITHUB` était déclaré deux
  fois : `site-kit` l'exporte, `Hero.tsx` l'importe.
- **TY-19** : `Format.id` était un `string` libre dans le manifeste partagé. C'est un `StoryFormat`
  de `@recit/core` : une faute de frappe dans un id de format ne compile plus.
- **DO-10** : les 14 pages de composants titraient « Props » ou « Attributes » selon l'humeur. Un
  seul titre : « Attributes and props ».
- **DO-17** : `sh` dans le dépôt, `bash` sur les sites. `bash` partout.

**Reste du lot 20** : les 38 URL `*.recit.dev` écrites en dur dans les pages (un `SiteLink` est à
écrire), le gabarit de page de composant, et le dédoublonnage docs ⇄ components (DB-14).

### Lot 13, première pièce : size-limit et publint (TE-8) — 26/10

Les deux outils étaient **installés depuis le début et n'avaient jamais tourné** : `publint` ne
servait qu'au `prepublishOnly` d'un package privé, et `size-limit` n'avait ni configuration ni
script. C'est pourtant `publint` qui aurait dit, avant npm, qu'une carte `exports` est cassée.

- `pnpm publint` (`scripts/publint-all.mjs`) lit la liste `release.projects` de `nx.json` — pas une
  seconde liste — et vérifie les **onze** packages publiés. Tous passent aujourd'hui.
- `pnpm size` mesure deux budgets qui comptent pour un lecteur : le runtime de `@recit/deck` (20,6
  kB brotli, limite 26) et le `entry.client` d'un site avec un récit embarqué (105,2 kB, limite
  120). Les limites sont **serrées exprès** : un budget qui ne peut pas être dépassé ne mesure rien.
  Premier réflexe à avoir : size-limit compresse en brotli, donc une limite écrite d'après la taille
  du fichier sur disque est fausse d'un facteur dix.
- Les deux tournent en CI, après `pnpm test`.

### Une vraie régression du lot 4, trouvée par les e2e — 26/10

La passe e2e finale a échoué sur `motion-dev.spec.ts` avec
`TypeError: Cannot read properties of undefined (reading 'scenes')`. Ce n'était ni une capture ni un
aléa : le serveur de dev motion sert son état sur `/__deck.json`, et cet état est le
`MotionFilmRead` renommé au lot 4 — il porte `film`, plus `deck`. Le test lisait encore
`payload.deck.scenes`. Aucun typecheck ne pouvait le voir : la réponse traverse HTTP en `any`.

Corrigé, et l'incohérence de nom avec : le serveur de dev **motion** sert désormais `/__film.json`
(celui du **deck** garde `/__deck.json` — ce sont deux serveurs, deux formats). Le client, le test
e2e et le test unitaire suivent.

Trois captures `-linux` régénérées, et trois seulement : les pages où j'ai ajouté un encadré
(`docs/index`, `quickstart`, `templates`). **166 e2e verts.**

### Lot 12 de l'audit (polices embarquées, goldens) — 26/10

C'était le lot à faire en premier parmi les P2 : `goldens:check` était rouge hors macOS depuis le
début, et personne ne pouvait savoir si un rendu avait changé.

**BP-14 — la cause.** Les thèmes nommaient « Space Grotesk », « Yeseva One », « Josefin Sans »… et
**rien ne les chargeait**. Le rendu prenait ce que la machine avait. Les six polices (toutes SIL OFL
1.1) sont maintenant embarquées par `@recit/theme/fonts.css`, sous-ensembles **latin et latin-ext
seulement** — l'interface est en anglais et en français, et tout embarquer mettait un méga-octet de
polices dans chaque package. Un récit en grec ou en cyrillique s'affiche toujours, avec la police
système du lecteur pour ces caractères ; c'est écrit dans le fichier.

**Satoshi est retiré de la pile distribuée.** Sa licence Fontshare interdit la redistribution : le
nommer, c'était promettre un rendu qui ne marche que sur les machines qui l'ont déjà.

**Trois pièges trouvés en chemin**, tous parce que le script ne tournait jamais :

1. `goldens`/`goldens:check` demandaient la condition `--conditions development`, que le lot 9 a
   renommée en `@recit/source`. Le script était **cassé depuis ce commit** et rien ne l'a dit.
2. Il appelait encore `readDeckFile`, `renderDeckStills` et `parsed.deck` — le vocabulaire d'avant
   le lot 4.
3. Le diff PNG était **calculé puis jeté** : on apprenait qu'une image avait changé de 2,8 %, jamais
   en quoi. Il est écrit dans `fixtures/goldens-diff/` quand une prise échoue, avec l'image obtenue,
   et la CI l'archive.

**Les URL des polices.** `styles.css` est assemblé à la main, pas par un bundler : les `url()` des
`@fontsource` pointaient dans `node_modules/.pnpm/…`, c'est-à-dire la machine de build et nulle part
ailleurs. Les polices passent désormais par le même esbuild que KaTeX, qui écrit les woff2 dans
`dist/fonts/` et réécrit les URL. Seul le woff2 est conservé : le `.woff` hérité doublait le tarball
pour des navigateurs que Récit ne vise pas.

**Résultat** : `goldens:check` passe, **32 rendus à 0,000 %** (deux films × huit blocs × JSON et
Markdown), et le job est en CI avec l'archivage du diff.

**CM-7** : les sept jetons `--sp-*` (14 déclarations), les trois `--slide-design-*` et le sélecteur
`.slide-container` de `print.css` n'étaient lus par personne. Supprimés ; `sites/plans/pages/03` ne
les présente plus comme une API.

### Lot 13, accessibilité (BP-7, BP-8) — 26/10

**BP-7 — le deck ignorait `prefers-reduced-motion`.** La seule occurrence du réglage dans tout le
dépôt était dans `MotionPlayer`. Le deck, lui, faisait glisser chaque diapositive de 32 px quoi
qu'en dise le système du lecteur (WCAG 2.3.3). `<MotionConfig reducedMotion="user">` enveloppe
désormais tout le deck : Motion abandonne les transformations et garde l'opacité, donc un fondu
court à la place d'un glissement, et les composants de diapositive suivent par le même contexte,
sans prop à passer. Côté CSS, les étapes, la barre de progression et les contrôles tombent à 1 ms et
l'étape masquée perd son `translateY(8px)`.

**BP-8 — `role="application"` était posé à l'envers.** Il était mis sur le deck **intégré dans une
page**, c'est-à-dire exactement là où il fait le plus de mal : il coupe le mode lecture du lecteur
d'écran pour tout le sous-arbre, donc le texte des diapositives devenait inatteignable sur une page
de doc. Il est maintenant réservé au plein écran, où le deck possède réellement le clavier ;
intégré, le deck est une `region`. Par-dessus, le motif carrousel WAI-ARIA :
`aria-roledescription="carousel"` sur le deck, la diapositive vivante en `group` nommé « 3 / 12 »,
et une région `aria-live="polite"` qui annonce « Slide 3 of 12 : <titre> » à chaque changement.
L'annonce est **hors** de la transition : posée sur la diapositive elle-même, le lecteur aurait relu
toute la diapositive au lieu d'une phrase.

Mesuré : 5 assertions dans `presentation-a11y.spec.tsx` (les deux rôles, le libellé du groupe,
l'annonce en français et en anglais). Au passage, les commentaires français restés dans les trois
CSS de `@recit/theme` sont traduits — la règle du dépôt est « code et docs en anglais ».

### Lot 13, performance (BP-5) — 26/10

**Le constat, mesuré.** `entry.client` du site d'exemple `site-embed` pesait **105,63 ko brotli**,
et tout le runtime deck était dedans : `plugin.tsx` et `mdx-components.tsx` importaient
`StoryViewer` statiquement, qui importait `Presentation`, qui importait `motion` au complet. Un
lecteur qui ouvrait une page de doc sans la moindre histoire téléchargeait quand même Motion, les
composants de diapositive, la console présentateur et l'encodeur QR.

**Trois coupes.**

1. **`StoryStage`** : la scène (le `Presentation` et les deux rendus de diapositive) part dans un
   module à elle, chargé par `React.lazy`. `StoryViewer` ne garde de `@recit/deck` que des types.
2. **`SlideMotion`** : un seul fournisseur `<LazyMotion features={…} strict>` pour tout ce qui rend
   une diapositive, et les composants passent de `motion.*` à `m.*`. Les 37 ko de features arrivent
   après le premier rendu. `strict` fait **échouer** un `motion.*` oublié : la règle de `CLAUDE.md`
   est tenue par le runtime, plus par la relecture.
3. **`qrcode`** (~20 ko) : importé dynamiquement à l'ouverture de la fenêtre de télécommande, la
   seule chose qui s'en sert.

**Un piège qui valait la mesure.** `m.*` hors d'un `LazyMotion` ne plante pas : il rend l'état
initial et s'arrête là. Avec `fadeUp`, l'état initial est `opacity: 0` — le contenu serait resté
**invisible**. Le fournisseur est donc posé aux trois endroits par lesquels passe toute diapositive
(`Presentation`, `HtmlSlide`, `MdxSlide`), et il s'efface quand il est déjà là, pour ne pas empiler
trois contextes identiques par diapositive.

**Résultat** : `entry.client` **105,63 → 74,80 ko brotli** (−29 %), la scène de l'histoire à **6,93
ko**, et plus une seule trace du runtime deck dans le chunk d'entrée (`deck-viewport`,
`slide-content`, `useSlideEnhancements` : zéro occurrence). Les budgets `.size-limit.json` sont
resserrés sur ces chiffres et un troisième budget garde la scène. `entry-path.spec.ts` nomme la
cause plutôt que le symptôme : il échoue si `StoryViewer` réimporte une valeur de `@recit/deck`
(vérifié en le cassant exprès).

### Lot 13, fin : premier rendu, prémisse d'hydratation, axe (BP-10, BP-11, TE-7) — 26/10

**BP-10 — une diapositive Markdown arrivait vide.** `HtmlSlide` rendait un `div` vide et le
remplissait dans un `useLayoutEffect`. La page prérendue ne portait donc aucun texte visible : rien
sans JavaScript, et rien à peindre pour le navigateur avant que le bundle n'arrive (le LCP
attendait). Le HTML est maintenant rendu par React. L'effet ne garde que l'extraction des
`:::component` — et `html` reste dans ses dépendances, non parce qu'il le lit, mais parce qu'il
signale que React a réécrit le balisage et que les nœuds de composants sont revenus.

**Le piège, trouvé par les e2e en conteneur** (6 échecs : étapes jamais révélées, aucun diagramme
Mermaid dessiné, onglets de code morts). **React 19 compare `dangerouslySetInnerHTML` par identité
d'objet, pas par la chaîne `__html`.** Écrit `dangerouslySetInnerHTML={{ __html: html }}`, c'est un
objet neuf à chaque rendu, donc React **réécrivait tout le sous-arbre à chaque rendu** — et jetait
au passage tout ce que les effets d'enrichissement y avaient posé : `data-visible` des étapes, SVG
Mermaid rendus, onglet de code sélectionné. L'ancien code ne rencontrait pas le problème parce qu'il
écrivait l'`innerHTML` lui-même, hors de la vue de React. L'objet est désormais mémoïsé sur `[html]`
: React ne réécrit que quand le HTML change, c'est-à-dire exactement quand les effets se relancent
aussi (`content: html` est leur dépendance).

Mesure : les 6 spécifications échouaient (`deck-features`, `site-components` ×2, `site-docs`,
exports PDF et HTML autonome) ; elles passent après la mémoïsation, et la cause a été confirmée en
revenant à l'ancien code puis en n'ajoutant que le `useMemo`.

**BP-11 — un risque, pas un défaut, et c'est ce qu'il fallait vérifier.** `Presentation` lit
`window.location.search` et `useThemeMode` lit `localStorage` et `matchMedia` au premier rendu. En
cherchant le chemin où cela casserait, il n'y en a aucun : le deck autonome est monté avec
`createRoot` (jamais hydraté), et sur une page du framework de sites `StoryViewer` attend le client
avant qu'un deck existe. Plutôt que de réécrire deux états initiaux sans défaut à corriger, la
**prémisse** est tenue par un test : `no-deck-on-the-server.spec.tsx` échoue le jour où un deck est
rendu côté serveur, et les commentaires des deux fichiers disent quoi changer à ce moment-là.

**TE-7 — axe, et ce qu'il a trouvé du premier coup.** Trois formes de page (une doc avec une
histoire intégrée, une histoire seule, un deck plein écran), règles WCAG 2.1 A et AA.

- **Les pages d'histoire et de deck plein écran étaient prérendues sans `<title>`.** Ce sont des
  routes ajoutées par le plugin : pas de front matter, donc pas de titre, donc `titleTemplate` de le
  framework de sites renvoyait une chaîne vide. Onglet, signet et résultat de recherche affichaient
  l'URL. `storyHead` émet désormais le titre de l'histoire, et le framework de sites le compose en «
  Starter | Presentation ».
- **Un défaut de contraste sur le `CTRL+K` du bouton de recherche** (4,39:1 mesuré, 4,5:1 demandé).
  Il vient du chrome du framework de sites, pas du balisage Récit. Le `header` est exclu du scan,
  avec la mesure écrite dans le test : désactiver la règle `color-contrast` aurait masqué les
  nôtres.

Les trois pages passent. Ce n'est pas une assertion complaisante : elle a listé quatre violations
avant correction.

### Trois serveurs HTTP qui ne se fermaient jamais — 26/10

Trouvé en refusant de classer une instabilité e2e en « charge machine ». Le symptôme : un test
différent à chaque passe (`deck-features`, `motion-dev`) expirait, puis passait en reprise en **3,4
secondes**. Un test de 3,4 s qui bloque 3 minutes n'est pas lent, il est bloqué.

**La démarche.** Une passe de contrôle sur le commit déjà poussé : 166 passés, zéro instable. Donc
c'était bien le travail en cours. Puis l'élimination : ce n'était pas les trois nouveaux tests axe
(la suite sans eux reste instable au même endroit), ni le poids des pages prérendues (66 482 → 67
379 octets sur la galerie, `templates.html` identique à l'octet près), ni `recit build` (qui ne fait
aucun rendu serveur, donc le changement de `HtmlSlide` ne peut pas l'atteindre).

**La cause.** Le contexte d'erreur de `motion-dev` montrait le texte **exactement attendu** par la
dernière assertion. Le corps du test était donc fini : le blocage était dans le `finally`, sur
`dev.close()`. `server.close()` de Node **ne ferme rien** : il refuse les nouvelles connexions et
attend la fin des existantes. Un navigateur garde ses sockets keep-alive ouvertes tant que la page
l'est — la promesse ne se résout jamais.

Trois serveurs avaient le défaut : `commands/dev.ts`, `commands/motion-dev.ts` et `serveStatic`
(`export-pdf.ts`, exporté par la CLI et utilisé par l'export PDF). Chacun appelle désormais
`server.closeAllConnections()` après `server.close()`. **Ce n'est pas qu'un problème de test** :
`recit dev` pouvait ne jamais rendre la main à la fermeture tant qu'un onglet était ouvert.

**Mesure** : 169 passés, **zéro instable**, 1,7 min — contre 3,8 min et un test rouge à chaque passe
avant. Les trois minutes perdues étaient les expirations elles-mêmes.

Au passage, un diagnostic intermédiaire a été **retiré** : j'avais sérialisé `deck-features` et
`motion-dev` dans un seul worker et porté leur budget à 180 s, en croyant à une concurrence entre
quatre compilations. La vraie cause trouvée, ces changements ne tenaient plus debout et sont revenus
en arrière.

### Une panne e2e non résolue : `page.goto` qui n'arrive jamais — 26/10

Consignée parce que je ne l'ai pas élucidée, et que le prochain à la rencontrer doit savoir ce qui a
déjà été écarté.

**Symptôme** : dans la suite complète en conteneur, `page.goto` sur `/index` des deux sites
d'exemple du framework de sites expire au bout de 60 s. Les deux tests concernés sont séparés de 160
tests dans l'ordre d'exécution, donc ils ne se gênent pas l'un l'autre.

**Ce qui est écarté, avec la mesure :**

- **Ce n'est pas mon travail en cours.** Une passe de contrôle sur le commit déjà poussé échoue aux
  deux mêmes tests.
- **Ce n'est pas le serveur d'aperçu.** Lancé à la main, il sert `/index` en **7 ms**, sans
  redirection, vingt fois de suite. Il ne journalise pas les requêtes, donc ce n'est pas non plus un
  tube stdout saturé qui bloquerait le processus.
- **Ce n'est pas une requête bloquée.** Avec `waitUntil: "commit"` et 25 s d'attente, la page émet
  **94 requêtes et aucune ne reste en suspens**.
- **Ce n'est pas la concurrence.** Avec `--workers=2` au lieu de 4, il y a **plus** d'échecs, pas
  moins. La machine a 8 cœurs, 24 Go libres et une charge de 2,6.
- **Ce n'est pas la page.** Les mêmes tests, lancés seuls ou à deux dans le même conteneur, passent
  en 0,7 s et 1,3 s.

**Ce qui est fait** : `openPage()` dans `e2e/support.ts` navigue sans attendre `load`. C'est
justifié indépendamment du reste — `load` n'est pas un signal utilisable pour une application qui va
chercher des morceaux de bundle autour de l'hydratation (grammaires Shiki, scène de l'histoire,
features de Motion) — et les tests attendent déjà ce dont ils ont besoin avec `heightStable`,
`decksReady`, `mermaidReady` et `filmsReady`. Cela a supprimé **deux des trois** occurrences.

**Suite, le même jour** : `domcontentloaded` ne suffit pas non plus — les deux mêmes tests sont
revenus à la passe suivante. Deux hypothèses de mécanisme ont été formulées puis **réfutées par la
mesure** : un tube stdout saturé bloquant le serveur (il ne journalise pas les requêtes, son fichier
de sortie ne bouge pas après vingt requêtes) et une hydratation bloquée sur le chargement de Shiki
(`entry.client` n'attend `getShikiReady()` que si le HTML porte `data-recit-runtime-shiki`, absent
des trois pages concernées).

`openPage()` attend donc `commit` — la réponse — et rien d'autre. C'est la seule variante mesurée
qui tienne : **169 passés, zéro instable, 1,8 min**. Rien n'est masqué : chaque appelant attend
ensuite le contenu sur lequel il assure (`heightStable`, `decksReady`, un titre visible, une
comparaison de capture).

**Non élucidé** : pourquoi `domcontentloaded` n'arrive pas en 60 s sur une page dont le serveur rend
le document en 7 ms et dont les 94 requêtes aboutissent toutes. Piste suivante : instrumenter
Chromium lui-même (`--trace=on`) plutôt que le réseau de la page.

### Lot 11 de l'audit (dédoublonnage), première moitié — 26/10

Les doublons mécaniques, réunis dans un seul `packages/core/src/util.ts`, exporté par `@recit/core`
et par `@recit/core/browser` (donc disponible dans un bundle navigateur sans la chaîne de
compilation).

- **`isRecord` : huit copies.** Sept identiques, et une **différente** dans `@recit/mcp`, qui
  acceptait les tableaux. Ses deux appels n'en faisaient rien d'utile — une lecture de propriété sur
  un tableau rend `undefined` — donc la version stricte est retenue, et la divergence disparaît. Le
  garde rend `Record<PropertyKey, unknown>` et non `Record<string, unknown>` : `define.ts` lit une
  clé _symbole_ sur le résultat, et un garde plus étroit l'aurait forcé à caster.
- **`formatDuration` : cinq copies de `mm:ss`**, plus une en `m:ss` dans le lecteur de film, plus un
  homonyme dans la CLI qui formate un tableau de durées. Une seule fonction désormais, qui **tronque
  et borne** : les anciennes rendaient `02:7.34` sur une durée calculée depuis un nombre de mots, et
  une durée négative sortait telle quelle. L'horloge du film passe de `0:07` à `00:07` — assumé pour
  la cohérence ; aucune capture e2e ne la montre.
- **`slugify` : deux versions divergentes.** « Récit » donnait `recit` d'un côté (Le framework de
  sites) et `r-cit` de l'autre (motion-blocks, qui ne retirait pas les accents).
- **`countWords`** : deux copies au comportement identique.
- **`THEME_STORAGE_KEY`** : la chaîne `"recit-theme"` était écrite dans quatre fichiers, dont le
  script d'amorçage en ligne de la CLI et le harnais e2e.
- **Cale i18n de `@recit/components`** : un fichier qui ne faisait que réexporter
  `@recit/core/i18n`, encore importé par neuf fichiers. Supprimé avec son test.
- **Variantes de transition** : `SLIDE_VARIANTS` était dans `@recit/components` et ses deux
  compagnes dans `Presentation.tsx`, alors que seul le deck les lit. Les trois sont dans
  `deck/transitions.ts`, avec `variantsFor` et un test qui vérifie le choix pour chaque réglage.

`motion-blocks` déclare maintenant `@recit/core` : l'arête est permise par `sites/plans/pages/03` §8
(`format` → `foundation`) et le contrôle des frontières le confirme.

Mesuré : `pnpm check`, 41 tâches de build et de typecheck, 2352 tests unitaires.

### Lot 11, DB-6 : le choix MDX/HTML recopié quatre fois — 26/10

`story.syntax === "mdx" ? <MdxSlide> : <HtmlSlide>` était écrit dans le runtime du deck, dans la
scène du framework de sites, dans la vignette et dans la vue document — et chaque copie passait un
jeu de props légèrement différent. `SceneContent` dans `@recit/deck` en réunit **trois**.

La quatrième, `StoryDocument`, rend un document et non une diapositive : sa branche HTML descend les
titres d'un niveau et n'utilise pas `HtmlSlide`, donc elle n'applique ni les étapes ni Mermaid. La
fusionner changerait ce que fait une page de doc ; elle reste à part, et c'est écrit ici.

**Le défaut que le doublon cachait** : `DeckThumbnail` construisait son `footerContext` sans `date`,
donc `{date}` restait vide dans **toutes** les vignettes de galerie. `footerContextOf(story)` le
construit une fois pour les trois appelants.

### Lot 11, les trois constats courts (DB-7, DB-10, DB-11) — 26/10

**DB-7 — `<CodeBlock>` colorait avec un thème que personne d'autre n'utilisait.** Son défaut était
`"nord"`, alors que le pipeline Markdown colore en `github-light` / `github-dark` — et ces deux
valeurs étaient elles-mêmes écrites deux fois (le défaut du schéma et le compilateur).
`DEFAULT_SYNTAX_THEMES` dans core les porte, et `CodeBlock` prend le thème sombre de la paire, celui
qui va avec son propre fond sombre. La page de doc se contredisait — elle annonçait
`syntaxHighlighting.themes` **et** un défaut `nord` ; elle dit maintenant ce que fait chaque chemin.
`highlightLines`, une prop jamais lue, est retirée.

**DB-10 — un seul des deux chemins Mermaid avait un verrou.** Mermaid garde un état global entre
`initialize` et `render` : deux rendus concurrents se disputent le thème et l'identifiant. Le hook
du deck enchaînait ses diagrammes un par un ; le composant `<Mermaid>`, non. Une page qui mélange
les deux n'avait donc aucun verrou. La file est descendue dans `renderMermaidSvg`, par où tout le
monde passe ; le hook garde son enchaînement, qui sert à ne pas lancer de nouveau rendu après
l'abandon d'une diapositive.

**DB-11 — la hauteur maximale de `<Media>` existait en trois versions.** 540 px dans le composant
(classe Tailwind), 540 px dans `deck.css`, et 400 px dans un contournement de `sites/components`. Ce
contournement n'existait que parce que les deux premières se gênaient. Seule la règle de `deck.css`
reste ; le contournement est supprimé. Le test du composant assurait la présence de `min-h-0` dans
son `className` — c'était précisément le doublon ; il vérifie maintenant l'inverse, et la règle CSS
est déjà couverte par le test voisin.

Mesuré : une seule capture de référence change (la page de code, qui n'est plus en `nord`), 2352
tests unitaires, e2e en conteneur 169 passés sans instabilité.

### Lot 11, DB-8 : trois règles pour « est-ce une histoire ? » — 26/10

Trois implémentations, trois réponses différentes : la CLI acceptait `"kind": "story"` **ou**
`"sidecar"`, le plugin Vite seulement `"story"`, et le chargeur motion prenait en plus un tableau
`scenes` sans `kind` du tout (un film emaki).

Et les deux premières cherchaient la clé avec une **expression régulière sur le texte brut**. Elle
matche un `kind` imbriqué n'importe où : `{"meta":{"kind":"story"}}` passait pour une histoire sans
rien déclarer à son niveau supérieur. Le test le montre en vérifiant d'abord que l'ancienne
expression matche bien, puis que `jsonStoryKind` répond `null`.

`core/src/schema/discovery.ts` porte `isStoryFileName` (extensions, moins les fichiers JSON qu'un
projet a toujours) et `jsonStoryKind`, qui **lit le JSON** et rend `story`, `sidecar`, `film` ou
`null`. Chaque appelant garde sa règle d'acceptation, exprimée sur ce résultat.

Mesuré : 7 assertions nouvelles, 2359 tests unitaires, e2e 169 passés.

### Lot 11, DB-1 : déjà réglé pour moitié, et une recommandation écartée — 26/10

**La moitié déjà faite.** L'audit décrivait `recit dur`, `validate` et l'aperçu de la CLI calculant
la durée avec `sceneDuration`, qui ne compte que le temps de lecture, alors que le MP4 découpe avec
`sceneFrameList`, qui prend aussi la fin de l'animation et `SCENE_TAIL`. Vérifié : les trois passent
désormais par `filmDuration`, construit sur `filmTimeline` — donc sur les mêmes images que le MP4.
Le lot 4 l'a corrigé en renommant le vocabulaire. L'en-tête de `client.tsx` ne ment plus.

**Ce qui restait** : `sceneFrameList` écrivait `1.5` en dur là où `MIN_SCENE_SECONDS` vaut 1,5. La
constante est importée.

**La recommandation écartée, et pourquoi.** L'audit demande « une seule constante de vitesse de
lecture », au motif que core dit 140 mots/min et motion 2,2 mots/s (soit 132). Ce ne sont pas deux
valeurs du même nombre : celle de core estime le temps qu'un **présentateur met à dire** ses notes,
celle de motion le temps qu'un **spectateur met à lire** le texte à l'écran. Les fusionner
alignerait deux mesures différentes sur la seule base que leurs chiffres se ressemblent — et
changerait la durée de tous les films. Les deux constantes restent, chacune disant maintenant ce
qu'elle mesure et renvoyant à l'autre.

Mesuré : 2359 tests unitaires, et les 32 rendus de référence des films à **0,000 %** — la durée des
films n'a pas bougé.

### L'erreur React #418 : une reproduction déterministe, enfin — 26/10

Connue depuis le lot 6, visible « seulement quand la machine est lente », jamais reproduite seule.
Trouvée par accident en faisant DB-3.

**La recette.** Retirer l'import de `motion` du composant `Quote` de `@recit/components`,
reconstruire, et `/demos/mdx` échoue **à chaque fois, en local, en 1,7 s**. Remettre l'import : ça
passe. `Quote` n'est pourtant pas sur cette page. Ce que son import change, c'est le découpage des
chunks — donc l'ordre et le moment où les modules arrivent, donc la fenêtre d'hydratation. Le défaut
était là tout du long ; le graphe de modules décidait seulement s'il se manifestait.

**Un piège d'instrumentation qui a coûté deux sondes.** L'erreur arrive en `pageerror`, pas en
erreur de console. Une sonde qui n'écoute que `page.on("console")` voit **zéro erreur** sur une page
qui échoue. `trackErrors` d'`e2e/support.ts` écoute bien les deux, d'où l'écart entre la suite et
mes sondes.

**Ce qui a été mesuré sur la page, et écarté** : les séparateurs `<!-- -->` que React pose entre
plusieurs enfants texte sont bien présents dans le HTML prérendu (donc aucun minifieur ne les mange)
et les trois blocs de code colorés au runtime ne diffèrent entre serveur et client que par
l'espacement de l'attribut `style` — le texte des jetons est identique.

**Le nœud qui diverge, trouvé.** En comparant les séquences de nœuds texte du HTML prérendu et du
DOM hydraté avec un vrai diff — et non indice par indice : un diff naïf confond les insertions
légitimes du rendu client-only, comme le raccourci `CTRL+K` du bouton de recherche, avec une vraie
divergence :

```
- serveur : "layout" | "=" | "cover" | "# Lorem ipsum MDX" | …
+ client  : "--- layout=cover"       | "# Lorem ipsum MDX" | …
```

Une **seule ligne d'un bloc de code** de `<SourceFile language="mdx">` : le serveur la découpe en
trois jetons Shiki, le client la garde entière. Le reste du même bloc est découpé pareil des deux
côtés (`{`, `year`, `}`), donc le client colore bien — il tokenise juste cette ligne autrement.
C'est un écart de grammaire Shiki entre le prérendu et le runtime, pas un état applicatif.

**Non élucidé** : pourquoi cette ligne-là. `--- layout=cover` est le séparateur de diapositive de
Récit à l'intérieur d'un exemple MDX ; la grammaire `mdx` peut le lire comme un début de front
matter selon ce qui précède. Les deux côtés reçoivent pourtant la même chaîne, par le même
composant, avec la même liste de langages (`site-kit/config.tsx` ajoute `mdx`, déjà présent dans
`defaultLanguages`). Prochaine étape : appeler `highlight()` sur cette chaîne dans les deux
environnements et comparer le HAST.

En attendant, `Quote` garde son fondu, comme ses voisins `Cover`, `Points` et `Media` — ce qui est
de toute façon plus cohérent que de l'en priver.

### Lot 11, DB-3 : trois composants qui se peignaient eux-mêmes — 26/10

`Callout`, `Quote` et `CodeGroup` dessinaient leur propre apparence en Tailwind, alors que la
directive Markdown équivalente émet des classes `slide-*` stylées par `deck.css`. Le même callout
n'avait donc pas la même tête selon qu'il était écrit en Markdown ou en MDX. Les trois émettent
maintenant les classes de la directive, et `deck.css` porte seul le style.

**Le défaut que le doublon cachait** : `<CodeGroup>` n'avait **aucun `role="tablist"`**. Là où la
version Markdown donne des onglets à un lecteur d'écran, la version MDX lui donnait une rangée de
boutons sans rapport. Il émet désormais le même motif WAI-ARIA que `rehypeCodeGroups`, et son fond
`#1e1e1e` en dur disparaît avec le reste.

**Une couverture qui manquait.** Les composants MDX intégrés ne sont utilisés que sur la deuxième
diapositive du deck MDX, qu'aucune capture n'atteignait : ils n'avaient **aucune couverture
visuelle**. Un test ouvre maintenant `/present/mdx?slide=2`, vérifie que le callout porte bien
`slide-callout-tip` et `data-title`, et en prend une capture.

**Et un trou dans le harnais du lot 2.** En ajoutant ce test, Playwright a écrit tout seul une
référence `-linux` — depuis cette machine, hors du conteneur. `updateSnapshots` valait
`CI ? "none" : "missing"`, ce qui suppose que la machine de dev est un Mac. Sur Linux, une référence
manquante était donc écrite avec les polices et le Chromium de l'hôte, c'est-à-dire exactement ce
que le conteneur existe pour empêcher (TE-1). La condition tient compte de la plateforme, et le test
qui gardait cette politique — qui passait trivialement en vérifiant ce que la machine courante
répond — teste maintenant les deux cas en simulant `process.platform`.

Mesuré : 2363 tests unitaires, e2e en conteneur 170 passés.

### Lot 11, DB-4 : les diagrammes ignoraient le thème — 27/10

La couleur de marque était écrite en cinq endroits incompatibles. Le plus visible : `mermaid-render`
posait `#3B82F6` en dur, donc un diagramme restait bleu dans un deck en graphite ou en ambre alors
que tout autour suivait la palette.

Les diagrammes lisent maintenant les jetons de l'élément qui va les porter — `--primary` pour le
contour, `--muted` pour le fond des nœuds, `--card-foreground` pour le texte, `--border` pour les
traits — avec l'ancienne palette en repli quand le diagramme est rendu hors d'un conteneur thémé.
Les jetons sont des triplets HSL nus (`295 38% 25%`), composables en CSS ; le parseur de couleurs de
Mermaid veut la forme à virgules, d'où `tokenToColor`, extrait pour être testable sans DOM.

**Mesuré par le DOM, pas par les captures** : sur `/present/mermaid`, le contour d'un nœud vaut
`rgb(84, 40, 88)`, soit exactement `hsl(295, 38%, 25%)` — la valeur de `--primary`. Les captures
plein écran, elles, n'ont bougé que **sur une page sur quatre** : le changement reste sous la
tolérance de 2 % ailleurs. C'est un rappel utile sur ce que ces références garantissent vraiment.

Reste de DB-4, non fait : `site-kit` (`#7c3aed` / `#a78bfa`) et le repli `#7c3aed` de
`site-stories.css` ne sont toujours pas dérivés de `tokens.css`, et `motion-blocks/themes/theme.ts`
recopie encore `ThemeContract` à la main. Ce sont des changements de schéma, pas de rendu.

### Lot 11, DB-2 (moitié) : une seule horloge de film — 27/10

Deux lecteurs, deux boucles `requestAnimationFrame` pour la même chose : `cli/src/motion/client.tsx`
(l'aperçu de `recit dev`) et `integrations/site/src/motion/MotionPlayer.tsx` (le lecteur en page).
Écrites différemment, donc un correctif sur l'une n'atteignait jamais l'autre.

`useFilmClock` dans `@recit/motion-blocks` — dont les deux dépendent déjà — porte la boucle. **97
lignes retirées, 38 ajoutées.** Les deux vraies différences sont devenues des options : le lecteur
en page boucle (`loop`) et s'arrête hors écran (`enabled`), l'aperçu s'arrête sur la dernière image.
Le reste du chrome n'est **pas** fusionné et ne devrait pas l'être : un bandeau de scènes avec
rechargement à chaud et une barre de progression avec lecture/pause ne sont pas la même interface.

Les deux décisions de l'horloge sont extraites pour être testables sans DOM — `clockAtEnd`
(recommencer ou s'arrêter), `clockAfterToggle` (relancer un film fini depuis le début), `clampTime`.
C'est exactement ce qui divergeait entre les deux copies. Premier jet du test supprimé : il
n'assurait que sur sa propre donnée de test.

Reste de DB-2, non fait : les **deux serveurs de dev** (`commands/dev.ts` et
`commands/motion-dev.ts`, chacun avec son `http.createServer` et son canal SSE). Ils partagent
désormais le correctif de fermeture, mais pas leur structure.

Mesuré : 2373 tests unitaires, e2e en conteneur 170 passés.

### Le serveur de dev motion servait le film à tout le réseau — 27/10

Trouvé en instruisant la seconde moitié de DB-2. `recit dev` route une histoire motion vers
`startMotionDevServer`, qui n'avait **aucun jeton** : `recit dev film.story.md --host 0.0.0.0`
servait le film, ses sources et sa configuration à n'importe qui sur le réseau local — alors que la
**même commande** sur un deck refuse toute requête non-locale depuis le lot 5 (BP-3).

C'est précisément ce qu'un doublon cache : deux serveurs, un seul corrigé. Le module `dev/access.ts`
était déjà isolé, donc la correction tient en un appel à `decideAccess` et le jeton dans l'URL hors
localhost, comme pour le deck. Le test est l'exact pendant de celui du deck ; vérifié en retirant le
garde-fou, il échoue.

Mesuré : 2374 tests unitaires, e2e en conteneur 170 passés (la passe précédente en montrait trois
rouges — les deux déjà connues plus une instable ; la relance est à 170/170 en 1,8 min).

### Lot 8, suite : TY-12, TY-13, TY-17 — 27/10

**TY-13 — quatre unions recopiées.** `CalloutType`, `GridCols`, `MermaidSecurityLevel` et le `fit`
des images étaient écrits à la main dans `@recit/components`, alors que core porte la liste. Les
quatre suivent maintenant le motif déjà posé par TY-1 : un enum zod, son type par `z.infer`, sa
liste par `.options`. Le pipeline lui-même lit `CALLOUT_TYPES` de core au lieu de son propre `Set`.

**TY-12 — sept `interface` malgré la règle, et biome n'y pouvait rien.** Sa règle ne signale que les
déclarations qu'elle sait réécrire : `interface X extends Y` passe, et `pnpm biome lint` répond « No
fixes applied » sur un fichier qui n'en contient que. `scripts/check-interfaces.js` les refuse dans
les packages Récit (le fork `packages/site` garde le style amont, et une augmentation globale
`declare global { interface Window }` est la seule forme sans alias possible). Vérifié en ajoutant
une interface : le contrôle échoue.

**TY-17 — `toClientStory` annonçait `Story` alors qu'il retire trois champs** (`mdast` des scènes,
`components` et `customCss` des réglages). `ClientStory`, `ClientScene` et `ClientSettings` le
disent, et `PageData` du runtime deck aussi. Le typecheck a immédiatement attrapé le **test**, qui
lisait ces champs pour vérifier qu'ils valaient `undefined` ; il teste maintenant leur absence avec
`in`, et le compilateur refusant la lecture est la moitié de ce que le type sert à garantir.

Mesuré : `pnpm check` (avec le nouveau contrôle), 2374 tests unitaires, e2e 168 passés — les deux
rouges sont les deux pannes déjà instruites.

### Lot 8, TY-15 : la télécommande pilotait le deck sans contrôle — 27/10

`PresentationPeerMessenger` recevait les messages de la télécommande par un relais WebSocket et
faisait `JSON.parse(event.data) as RemoteAction`. Tout ce que le relais transmettait atteignait donc
les gestionnaires : un `GOTO` dont `slide` est une chaîne, un `TOOL_CHANGE` vers un outil inconnu,
un message sans `type` du tout.

`SyncMessage` est maintenant **dérivé** d'un `SyncMessageSchema` (union discriminée zod), et les
deux entrées sont validées : `parseSyncMessage` pour le WebSocket, `asSyncMessage` pour le
BroadcastChannel entre fenêtres. `ActiveTool` suit le même motif.

**Coût mesuré** : le runtime deck passe de 20,75 à **22,58 ko brotli**, sous son budget de 26 ko.
zod était déjà dans le bundle ; c'est le schéma lui-même qui pèse. Pour un canal piloté depuis le
réseau, le change vaut la peine.

13 assertions nouvelles sur ce qui est accepté et ce qui est refusé.

Mesuré : 2378 tests unitaires, e2e en conteneur 170 passés.

### Lot 8, TE-13 : `--typecheck` ne vérifiait aucun type, pas même celui du fork — 27/10

L'audit notait que le seul `*.test-d.ts` du dépôt était dans `packages/site`. En voulant en ajouter
un pour core, j'ai trouvé pire : **aucun n'était collecté**. `pnpm test` lance
`vitest run --typecheck`, mais avec des `projects`, le drapeau de la ligne de commande **n'atteint
pas les projets** — il faut `typecheck.enabled` dans la configuration de chaque projet. Celui du
fork n'a donc jamais tourné non plus.

`packages/core/vitest.config.ts` l'active, et `src/__tests__/types.test-d.ts` vérifie ce qui vaut la
peine : que les types sont bien **dérivés** de leur schéma zod. Un enum écrit deux fois — une fois
en zod, une fois en union TS — compile parfaitement pendant que les deux divergent ; ces assertions
tombent dès que ça arrive. Vérifié en ajoutant `"fill"` au schéma sans toucher au type :
`Type Errors 1 failed`.

Elles couvrent aussi `ClientStory` (TY-17) : plus moyen de lui redonner la forme de `Story` sans
casser la suite.

**Non fait, et dit ici** : activer `typecheck` dans `packages/site` serait un patch de plus sur le
fork, pour sa propre configuration de test. Laissé à la procédure de synchronisation.

Mesuré : 209 fichiers de test, 2384 tests, `Type Errors no errors`.

### Lot 8, TY-6 : un `.d.ts` n'est jamais vérifié — 27/10

Le constat parlait de types publics manquants. Ce qu'il y avait derrière est pire.

**`virtual.d.ts` importait un type qui n'existe plus.** Il déclarait la forme de
`virtual:recit-stories` avec `motion?: Deck`, de `@recit/motion` — un type que le lot 4 a renommé
`Film` **il y a deux jours**. Personne ne l'a vu, parce que `skipLibCheck: true` dans
`tsconfig.base.json` fait qu'un `.d.ts` **n'est jamais vérifié**. La déclaration dont dépend tout
site du framework de sites pouvait donc dériver en silence, et elle avait dérivé.

La forme vit maintenant dans `loaded-story.ts`, un `.ts` vérifié, et `virtual.d.ts` ne fait plus que
la référencer. Un test de types la garde.

**Le reste du constat.** `PublishConfig`, `StoryEntry`, `I18nConfig` et `DeckImage` sont exportés ;
`RecitPluginOptions` est un `Pick<PublishConfig, …>` au lieu d'une recopie à la main ; le cast vers
`{ src, alt }` de `i18n.ts` passe par `DeckImageSchema.safeParse` ; et le `StorySource` de
`@recit/site-stories` — qui entrait en collision avec celui de core, avec un autre sens — **est**
`StoryEntry`, l'ancien nom restant comme alias déprécié.

**Deux erreurs de ma part, corrigées.** J'ai d'abord écrasé le `vitest.config.ts` existant de
`@recit/site-stories` au lieu de l'étendre, ce qui a cassé l'alias du module virtuel dans ses tests.
Et ma première assertion de type, `not.toBeAny()`, passait pour `unknown` : vérifiée en remettant la
dérive, elle ne mordait pas. Elle nomme maintenant le type attendu, et elle échoue.

Mesuré : 2388 tests, `Type Errors no errors`, e2e 168 passés (les deux rouges sont les pannes
`/index` déjà instruites).

### Lot 8, TY-5 : deux pertes d'information, pas six types à fusionner — 27/10

Le constat listait six formes de diagnostic. Les fusionner toutes aurait brassé chaque package pour
un gain discutable : `SourceIssue` (un chemin JSON, avant la correspondance avec les lignes) et
`AccessibilityIssue` (le résultat d'une règle) sont des **entrées** de `Diagnostic`, pas des
concurrents. Ce qui méritait d'être corrigé, c'est ce que la traversée perdait.

**Tous les avertissements d'un film arrivaient étiquetés `suspicious-glyph`.** Ils naissent de
diagnostics qui ont leur propre code et leur ligne, étaient aplatis en chaînes par
`formatFilmSourceIssue`, puis ré-étiquetés en bloc par `validate.ts`. Mesuré sur une histoire qui
déclenche les deux : avant, deux `suspicious-glyph` ; après, un `invalid-aspect-ratio` **ligne 3**
et un `suspicious-glyph`. `FilmSourceResult.warnings` porte des `FilmSourceIssue`, et
`issueToDiagnostic` prend le code de l'issue et la sévérité de son appelant.

**Le `-1` sentinelle de `HandoverIssue.scene`** servait d'index de tableau : il rendait `undefined`
et retombait dans la branche « problème du film » **par accident**. Le champ est optionnel, et le
cas est écrit.

**Deux pièges trouvés par le changement** : `read.issues.map(issueToDiagnostic)` aurait passé
l'index du tableau comme sévérité (attrapé par le typecheck), et `r.warnings.join("\n")` dans le MCP
aurait imprimé `[object Object]` — que le compilateur, lui, accepte sans rien dire.

Les tests de `loader.test.ts` vérifiaient la chaîne formatée ; ils vérifient maintenant le code, le
fichier et la ligne, ce qui est précisément ce que le changement apporte.

**Au passage, le test axe instable.** Les contrôles du deck apparaissent au survol via une
transition d'opacité de 200 ms. Laissés seuls ils restent à `opacity: 0`, qu'axe **ignore**, donc
ils n'étaient jamais vérifiés ; pris à mi-transition, axe signale un défaut de contraste qu'aucun
des deux états finaux n'a (le bouton est à ~16:1, mesuré). Le test les révèle et attend la fin de la
transition : la passe est stable et ces boutons sont réellement couverts.

Mesuré : 2388 tests, `Type Errors no errors`, e2e 168 passés (les deux rouges sont les pannes
`/index` déjà instruites).

### Lot 8, TY-16 : le modèle Story ne parle plus de « slides » — 27/10

Le modèle est commun à tous les formats, mais son résumé et ses diagnostics nommaient des
diapositives. `SlideMetadata` → `SceneConfig`, `GlobalDeckMetadata` → `StoryFrontmatter`,
`ParsedDeckDocument` → `ParsedStoryDocument`, `resolveDeckConfig` → `resolveStoryConfig`,
`SlideDensityReport` → `SceneDensityReport`, `DeckDensitySummary` → `StoryDensitySummary`, et les
champs `slideIndex`, `totalSlides`, `overloadedSlides`, `averageWordsPerSlide`. Les six types
gardent leur ancien nom en alias déprécié ; les champs, non — ils sont calculés, aucun JSON ne les
porte (vérifié).

**Et j'ai refait l'erreur du renommage en masse.** Le premier passage a touché six fichiers du
runtime deck, où « slide » est pourtant le bon mot : `parseSlideIndex(param, totalSlides)` lit
`?slide=N` dans un deck, et renommer son paramètre pendant que la fonction garde « Slide » dans son
nom n'a aucun sens. Tout annulé sur ce paquet — et le compilateur a alors désigné le **seul**
endroit qui lisait vraiment le modèle : `DevHud.tsx`, qui affiche `report.sceneIndex` d'un
`SceneDensityReport`. Six fichiers à tort, un à raison.

Les types de résultat de la CLI (`BuildResult.totalScenes`, `ExportHtmlResult.totalScenes`) suivent
le modèle : ils comptent `story.scenes.length`. C'est une rupture d'API, assumée en 0.x, et la même
que celle du lot 4 pour motion.

Mesuré : 2388 tests, `Type Errors no errors`, e2e en conteneur 170 passés.

### Lot 15, CG-5 : trois chemins pour une transition — 27/10

Un deck était réglé par **deux sections**, `slides:` et `deck:`, la seconde surchargeant la première
(« Overrides slides.transition », dit le commentaire d'origine), plus `settings.transition` dans le
jumeau JSON. `deck:` est désormais la seule section du format — elle prend `files`, `designWidth`,
`designHeight`, `aspectRatio`, `transition`, `presenter` — et `slides:` est refusé avec son message
de renommage, comme `publish.decks` l'avait été.

Suivent : le schéma JSON publié (régénéré par `pnpm -F @recit/core schema:json`, ce que le test
disait déjà de faire), la configuration résolue (`settings.deck` au lieu de `settings.slides`),
l'exemple `deck-pitch`, et les trois pages de doc qui l'écrivaient. Un test vérifie que l'ancien nom
produit bien le message.

**Et le test axe instable, pour de bon cette fois.** Ma première correction — survoler les contrôles
et attendre la fin de la transition — ne tenait pas : la souris ne reste pas forcément en place
pendant qu'axe parcourt la page. Mesuré en local dans les deux modes, avec le survol : **aucune
violation de contraste**. Les boutons vont bien ; c'est la mesure qui était instable. Ils sont
maintenant épinglés à leur état visible par une feuille de style injectée, ce qui vérifie exactement
les couleurs qu'un lecteur voit, de la même façon à chaque passe.

Mesuré : 2389 tests, e2e en conteneur **170 passés**, une seule capture de référence changée (la
page de configuration, dont le tableau a bougé).

### Lot 15, DO-6 : `theme` voulait dire trois choses — 27/10

Dans le front matter, `theme: light` réglait le **mode d'affichage**, `theme: warm-editorial`
nommait un **thème**, et un objet portait la **configuration** de thème. Trois sens pour une clé.

`colorMode:` prend le mode (`light`, `dark`, `system`), `theme:` ne nomme plus qu'un thème ou porte
son objet. Les deux se combinent : `colorMode: dark` avec `theme: warm-editorial` donne les deux.

**Choix tranché seul, et pas le même que pour `slides:`.** Une _configuration_ qui garde un ancien
nom est refusée avec un message — c'est ce que CG-5 fait. Du _front matter_ est du contenu : casser
le deck de quelqu'un à une montée de version est pire qu'une clé ambiguë une version de plus.
`theme: light` continue donc de marcher, et pose un diagnostic `theme-is-a-color-mode` qui dit où
l'écrire. Quatre assertions couvrent les trois cas plus le silence quand `colorMode` est utilisé.

**Un garde-fou utile a parlé** : `@recit/mcp` embarque une copie des guides tirés de `sites/docs`,
et son test vérifie qu'elle reste synchrone — en nommant la commande. Sans lui, un paquet publié
serait parti avec une doc périmée.

Mesuré : 2393 tests, e2e en conteneur 170 passés, une capture de référence régénérée.

### Lot 15, CG-3 : le vocabulaire « deck » dans une API « story » — 27/10

L'attribut `data-deck` devient `data-story` (huit fichiers, dont le sélecteur de portée des thèmes
généré par le plugin Vite). `decks.ts` devient `stories.ts`, `DECKS_MODULE_ID` devient
`STORIES_MODULE_ID` — son nom disait « decks » alors que sa valeur dit déjà `virtual:recit-stories`.
`getLoadedDecks`, `getLocaleDecks`, `findDeck`, `getDecks`, `DeckSummary` et `DeckThumbnail`
prennent leur nom « story », les anciens restant en alias dépréciés parce que `@recit/site-stories`
les publie — `site-kit` importait `findDeck`.

**Le défaut que ce vocabulaire cachait** : `<StoryEmbed slide={2}>` et `?slide=2` désignaient la
**deuxième** scène, pendant que `<DeckThumbnail slide={2}>` désignait la **troisième**. Le même mot,
compté depuis deux origines. Les deux comptent maintenant depuis 1.

Mon premier test rendait la vignette entière et tombait sur un fixture sans `settings` : le vrai
contrat est le **choix de la scène**, extrait dans `sceneAt(story, slide)` et testé là.

Mesuré : 2395 tests, e2e en conteneur 170 passés — aucune capture de référence ne bouge, l'attribut
renommé n'étant pas visible.

### Lot 20, DB-17 : un schéma d'URL plutôt qu'un composant — 27/10

`sites.ts` se présente comme « la seule source des liens inter-sites » et bascule sur localhost en
dev. Trente-six liens dans dix-neuf pages passaient à côté avec une URL `*.recit.dev` absolue : en
dev, chaque lien inter-site quittait localhost pour la production.

**Première tentative, un composant `<SiteLink>` : fausse route.** oxfmt replie la prose à 100
colonnes, et dès que l'élément JSX se retrouve en début de ligne, MDX ne le lit plus comme de
l'inline mais comme un **bloc** — la phrase est coupée en deux paragraphes. Cinq phrases y sont
passées avant que la sortie buildée ne le montre. Rejoindre les lignes à la main ne sert à rien : le
formateur replie de nouveau. Un composant inline dans de la prose repliée est structurellement
fragile.

La forme qui tient est un **schéma d'URL**, `[texte](https://docs.recit.dev/formats/motion)` : du Markdown pur,
insensible au repli, utilisable dans une cellule de tableau.

**Trois défauts barraient la route, chacun réel :**

1. `remarkLinkRewrite` n'épargnait que `http` et `mailto:` ; tout autre schéma était lu comme un
   chemin relatif et ressortait préfixé (`/site:docs/x`). `tel:` était cassé pareil. Il épargne
   maintenant toute URL qui nomme un schéma — deux caractères au moins, pour qu'une lettre de
   lecteur Windows reste un chemin.
2. Le `.md` publié est la **source**, qu'aucun plugin remark ne voit : les liens seraient partis
   morts. `build.markdownLinks` les réécrit, vers l'URL publiée même en dev — un `.md` copié se lit
   loin de son site, comme les guides MCP.
3. `processMarkdownFile` reconstruisait sa sortie **à partir de la source brute** quand la page
   avait un titre, jetant toute transformation. Le test couvre les deux branches.

`createSiteConfig` donne aussi à chaque site les composants MDX partagés : six configurations
cessent d'importer les six mêmes noms (76 lignes), et quatre descriptions de site cessent de répéter
leur propre hôte (DO-11).

**DO-10 et DO-12 étaient déjà faits** — mais DO-10 avait renommé « Attributes » en « Attributes and
props » sur quatorze pages sans régénérer les captures `-linux`, qui mentaient depuis `d61549a0`.

Mesuré : 36 URL écrites à la main → 0, le garde prouvé en échec sur un lien et laissant passer les
identifiants `$schema` ; 2400 tests ; e2e en conteneur 169/170, le reste étant le React #418 de
`/demos/mdx`, sans rapport et toujours ouvert.

### Lot 20, DB-14 : trois listes revenues à zod — 27/10

`types/index.ts` annonce que chaque enum d'écriture est défini une fois, en zod, « sans rien d'écrit
deux fois ». Trois s'en étaient échappés : les types de badge étaient une union TypeScript dans
`builtins.tsx`, réécrite dans deux tableaux de site — **quatre copies de cinq valeurs, zod n'en
tenant aucune** ; les types de média, pareil ; et `config/schema.ts` ré-épelait les niveaux de
sécurité Mermaid que `MermaidSecurityLevelSchema` détient deux fichiers plus loin.

Les tableaux des sites continuent d'écrire les valeurs à la main — une cellule de tableau est de la
prose, et la description à côté de chaque valeur appartient à la page. Un test rapproche donc chaque
cellule `` `a \| b \| c` `` de l'enum qu'elle recoupe le plus et exige que la liste soit exacte.
Aucun marqueur dans les pages.

**Le test a d'abord menti** : il importait `@recit/core`, donc le `dist`, périmé après mon édition
de la source. Il annonçait deux dérives inexistantes. Il lit maintenant `types/index.ts` directement
: un garde qui dépend d'un build est un garde qui crie au loup.

### Lot 16, DO-7 : la page Architecture lit l'espace de travail — 27/10

La page listait les paquets à la main et en déclarait **huit « planned » alors qu'ils étaient
livrés** : `@recit/motion`, `motion-blocks`, `motion-render`, `mcp`, `recit`, `create-recit`,
`data`, `time`. Douze paquets existaient ; le tableau « Available » en montrait sept. Le diagramme
disait « @recit/motion (planned) » et oubliait trois paquets.

`<AvailablePackages />` et `<PlannedPackages />` lisent chaque manifeste par `import.meta.glob` au
build. Rien n'est écrit deux fois et **rien n'est généré dans un fichier à resynchroniser** : le nom
vient de `name`, le groupe du tag nx `scope:*`, le rôle de `description`, et la phase du `(P6)` par
lequel cette description se terminait déjà. Un nouveau paquet atteint la page en existant. Le fork
Le framework de sites vendu n'a pas de `scope` : il reste dehors et garde sa propre section.

Le diagramme reste éditorial — un graphe de dépendances généré serait du bruit — alors un test le
tient à l'espace de travail : tout paquet de format livré doit y être nommé, et rien que les
manifestes disent livré ne peut être dit « planned ». Il tombe quand on retire `motion-blocks`.

Trois autres lignes périmées, chacune vérifiée contre le code : la puce CLI listait quatre commandes
sur douze (elle renvoie maintenant à la référence au lieu de la recopier), `publish.decks` est
devenu `publish.entries` avec CG-3, et `motion.aspect` n'est plus « (video, planned) ».

Mesuré : Available 7 → 12 lignes, Planned 11 triées par phase ; 2411 tests ; e2e 168 passés, la
capture Architecture régénérée.

### Le blocage `/index` : mesuré, toujours pas expliqué — 27/10

L'échec intermittent de l'e2e (une passe sur trois environ) était décrit comme « la page n'atteint
jamais `load` ». Instrumenté par événements plutôt que par le journal d'appel de Playwright, il dit
tout autre chose :

| Mesure                                          | Valeur                         |
| ----------------------------------------------- | ------------------------------ |
| Requêtes de document en 25 s                    | **2996**                       |
| Navigations de la frame principale              | 2995, **toutes vers `/index`** |
| Statut des réponses                             | `200`, `redirectedFrom = -`    |
| `DOMContentLoaded` / `load`                     | 0 / 0                          |
| Requêtes en erreur                              | aucune                         |
| `pushState`, `replaceState`, `assign`, `reload` | **aucun appel**                |

Le navigateur redemande la page cent vingt fois par seconde, reçoit un 200 à chaque fois, et
recommence avant d'avoir analysé quoi que ce soit — sans qu'aucun script de la page ne le demande.
Le code applicatif est hors de cause, et le serveur répond.

**Deux fausses pistes, écrites ici pour qu'on ne les reprenne pas :**

1. _L'échec de préchargement de chunk_ (`vite:preloadError` → `location.reload()`). Démenti : aucune
   requête en erreur, et le gardé-fou est de toute façon limité à un rechargement par 30 s.
2. _La course keep-alive._ Le serveur ferme bien une socket inactive à 6 s quand un navigateur garde
   les siennes des minutes — mesuré, et corrigé dans `preview/handler.ts` parce que c'est un vrai
   défaut. **Mais ce n'est pas la cause** : le blocage survit au correctif, vérifié avec le
   correctif présent dans le build du conteneur. J'avais annoncé l'inverse trop vite, en prenant les
   « 12 × navigated to » du journal Playwright pour une mesure et en avalant l'erreur d'`evaluate`
   dans un `catch`. Deux fautes de méthode : une mesure indirecte prise pour une mesure, et une
   erreur jetée au lieu d'être lue.

**Le fait le plus parlant, encore inexploité** : la boucle ne frappe **que `/index`**, et sur deux
sites différents. C'est l'URL dont le fichier statique (`dist/index.html`) sert aussi de repli SPA
pour `/`. La suite se joue probablement là, et demande un autre outil — un journal réseau de
Chromium (`--log-net-log`) dans le conteneur — plutôt qu'une sonde Playwright de plus.

Mesuré après coup : quatre passes consécutives, 170/170 trois fois.

### Lot 16, DO-5 : l'onglet générique rendu au commun — 27/10

`sites/docs` rangeait dans l'onglet « Documentation » sept pages qui ne valent que pour le deck :
`layouts`, `slide-options`, `density-rules` et les trois pages « Presenting ». Motion n'avait qu'une
page, reléguée dans une catégorie « In progress ». Autrement dit, les pages motion à venir auraient
dû **disputer l'onglet générique au deck**, qui l'occupait seul.

La règle posée : **l'onglet Documentation ne contient que ce que tout format lit.** Le reste descend
sous `Formats › <Format>`. Sept pages déménagent, `formats/deck.mdx` devient
`formats/deck/overview.mdx` et ouvre une catégorie, Motion en ouvre une autre, vide, prête.

`documentation/writing/slides` devient `documentation/writing/scenes` : la page décrit le séparateur
`---` et le front matter, c'est-à-dire le mécanisme **commun**, mais elle l'écrivait entièrement en
vocabulaire de deck (« Split the deck », « Deck front matter »). Elle est réécrite dans le
vocabulaire du glossaire et renvoie vers les pages deck pour ce qui est propre aux slides.

**Deux affirmations vérifiées plutôt que supposées**, en écrivant la page :

- `aspectRatio` est bien une clé **commune** (16:9 et 4:3 pour un deck, plus 1:1 et 9:16 pour un
  film). Je l'avais retirée de l'exemple par excès de zèle ; elle y est revenue.
- « un format ajoute sa section `deck:` ou `motion:` au front matter » : mesuré sur quatre histoires
  jetables. `wordsPerSecond: 4` à plat et sous `motion:` donnent tous deux 3,00 s contre 5,47 s sans
  ; `aspectRatio: "4:3"` à plat et sous `deck:` donnent tous deux `"4:3"` contre `"16:9"` sans.
  **Les deux écritures marchent** — ce qui est en soi deux façons de dire la même chose, à regarder
  un jour.

Les onze anciennes adresses redirigent, chacune par un fichier de 85 octets généré au build. Seize
captures orphelines supprimées, huit régénérées.

Mesuré : 2411 tests, e2e en conteneur 169 passés, le seul échec étant le blocage `/index` connu.

### Plan 18 — Applications desktop et mobiles — 29/10

Nouveau plan `sites/plans/pages/18-mobile-desktop.md`, écrit d'après la mise en place de Tauri 2 du
projet `sass` (`/home/deploy/work/sass/apps/desktop/src-tauri`, `packages/sites/scripts/app.ts`) :
une coquille commune dans un paquet `@recit/app` (`scope:tool`), une section `app:` typée dans
`@recit/core`, le `dist/` prérendu embarqué et servi hors ligne, onze lots APP0 à APP10 avec leurs
fichiers possédés, leurs dépendances et leur vérification, et des vagues pour une exécution par
plusieurs agents (§6 du plan).

Pour reprendre : lire le plan, puis APP0 seul (vague 0), APP1 et APP4 en parallèle (vague 1), APP2
et APP3 (vague 2), tout cela sans Rust. APP5 et la suite demandent `rustup` et la ligne apt de §3 du
plan, ou la CI de APP6. Chaque lot fini laisse une note ici, à la fin du fichier, sous
`### APPn — <titre> — <date>`.

Au passage, le 29/10 : `server.allowedHosts: true` dans `packages/site/src/vite/config.ts` et dans
`sites/storybook/.storybook/main.ts`, pour atteindre les serveurs de dev par un autre nom d'hôte.

Reste à faire, hors du périmètre de l'agent qui a écrit le plan : la ligne de navigation du site des
plans, `doc("18-mobile-desktop", "18 · Desktop et mobile")` dans « Fondations » de
`sites/plans/recit.config.ts`.

### APP0 — Le contrat de configuration — 29/10

Fait, sans Rust. La section `app:` existe dans `@recit/core` et chaque lot suivant la consomme sans
la redéfinir :

- `AppConfigSchema` et `AppConfig` (`packages/core/src/config/validate.ts`), `app: z.optional(…)`
  dans `RecitConfigSchema` ; c'est une section Récit : `splitRecitConfig` la range sous
  `RECIT_CONFIG`, elle n'entre pas dans `SITE_CONFIG_KEYS`, et le JSON Schema publié
  `packages/core/schema/v1/config.json` a été régénéré (`pnpm -F @recit/core schema:json`).
- `packages/core/src/config/app.ts`, exporté par `@recit/core/config` :
  `resolveAppConfig(config: unknown, site: AppSite): AppConfigResolution` avec
  `AppSite = { dir: string; packageVersion?: string }` et
  `AppConfigResolution = { app: ResolvedAppConfig; diagnostics: Diagnostic[] }` ;
  `ResolvedAppConfig = { productName, identifier, version, icon?, window: { width, height, resizable, minWidth?, minHeight? }, allowedOrigins, deepLink?, android: { minSdkVersion }, ios: { developmentTeam? }, updater? }`
  ; `identifierFromOrigin(origin)` (`docs.recit.dev` → `dev.recit.docs`, tirets retirés) ;
  `DEFAULT_APP_WINDOW` (1280 × 800, redimensionnable) et `DEFAULT_ANDROID_MIN_SDK` (24).
- `config` est le `recit.config` chargé, tel que `defineConfig` le rend ou le module brut : les
  options du site (`canonicalUrlOrigin`, `basePath`, `cdnUrl`, `search`, `site.title`,
  `metadata.favicon`) sont lues sur l'objet, la section `app` sur les sections Récit validées.
- Jamais d'exception pour un problème d'app : codes `app-missing` (erreur, le message contient
  l'exemple minimal avec les défauts calculés), `app-identifier-missing` et `app-identifier-invalid`
  (erreurs, `path: /app/identifier`, la règle Android dans le message), `app-base-path` et
  `app-cdn-url` (erreurs), `app-version-missing` et `app-search-remote` (avertissements). Un
  `recit.config` invalide revient aussi en diagnostics (`config-invalid`, un par problème). Une
  résolution qui porte une erreur ne s'utilise pas : `identifier` y vaut `""`.

Mesuré :
`pnpm exec vitest run packages/core packages/integrations/site-stories/src/__tests__/site-config.spec.ts`
→ 24 fichiers, 315 tests verts (dont `app-config.spec.ts`, 14 tests, et « Récit sections never
collide with site options ») ; `pnpm -F @recit/core typecheck` vert ; biome et oxfmt verts sur les
quatre fichiers touchés.

Pièges pour APP1 et APP2 : `metadata.favicon` des sites Récit est un import Vite
(`site-kit/src/config.tsx:9`, `?no-inline`), donc une URL construite (`/assets/…svg`), pas un chemin
de fichier ; `icon` est rendu tel qu'écrit et `materializeShell` doit chercher une valeur qui
commence par `/` dans `public/` puis `dist/`, ou préférer un `app.icon` explicite. Le
`packageVersion` est à lire par l'appelant (la CLI) dans le `package.json` du site.

### APP4 — Documentation, plans et opt-in de l'exemple — 29/10

Fait, sans Rust. Les noms de commandes et les champs documentés viennent du plan 18 (§4.4, §5 APP2)
et de l'API réelle d'APP0 (`packages/core/src/config/app.ts`), pas d'un code de la CLI qui n'existe
pas encore.

- `examples/site-embed/recit.config.ts` : section
  `app: { productName: "Lorem Ipsum Docs", identifier: "com.example.loremipsum" }`.
- Trois pages de la doc (en anglais, comme le site), sous Configuration dans `sites/docs/sidebar.ts`
  : `documentation/configuration/app` (installer `@recit/app`, la section `app:` champ par champ
  avec les défauts, les huit codes de diagnostic, les commandes
  `recit app build|dev|preview|android|ios|icon`, les prérequis desktop par système, ce qui marche
  hors ligne, les limites, l'updater), `app-android` et `app-ios` (prérequis de §3 du plan,
  commandes, admonition « Work in progress » qu'APP7 et APP8 remplacent).
- `03-packaging.md` : `app` dans la table des groupes (§2), l'arborescence (§2), une ligne
  `@recit/app` sous `@recit/paged` dans la table des packages (§6), `app` dans la ligne `scope:tool`
  (§8). `06-roadmap.md` : une ligne en P5 qui renvoie au plan 18. `NOTICE.md` intact.

Mesuré :

- `node scripts/check-frontmatter.mjs`, `check-admonitions.mjs`, `check-site-links.js`,
  `check-version-plans.js`, `check-package-boundaries.js`, `check-interfaces.js` : verts.
- `nx run docs:build` : vert, 57 routes prérendues, `dist/documentation/configuration/app.html`,
  `app-android.html`, `app-ios.html` produits ; `tsgo --noEmit` dans `examples/site-embed` vert.
- biome et oxfmt verts sur mes fichiers (`oxfmt` lancé sur eux seuls : les pages de `sites/*/pages`
  **sont** formatées par `pnpm check`, `.oxfmtrc.json` n'exclut que `examples/**/*.md(x)` et
  `sites/*/stories/**`).

Pièges rencontrés :

- Pendant APP1, `packages/tools/app/package.json` demande `@tauri-apps/cli` au catalogue avant que
  `pnpm-workspace.yaml` ne l'ait : **toute** commande `pnpm` du dépôt échoue
  (`ERR_PNPM_CATALOG_ENTRY_NOT_FOUND_FOR_SPEC`, pnpm relance `install` avant chaque script). Les
  vérifications ci-dessus sont passées par les binaires de `node_modules/.bin` et, pour nx,
  `npm_config_verify_deps_before_run=false`. À relancer en `pnpm check` et
  `pnpm exec nx run docs:build` une fois APP1 installé.
- Le typecheck de l'exemple refusait `app` dans `RecitSiteConfig` tant que `packages/core/dist`
  datait d'avant APP0 : un `nx run docs:build` reconstruit le cœur et le typecheck passe. Ce n'est
  pas un trou du contrat.
- `oxfmt --check` du dépôt signale aussi `18-mobile-desktop.md`, `journal.md` et
  `.nx/version-plans/version-plan-app.md` (pas à moi) ; biome signale
  `packages/tools/app/shell/src/window-open.js`, `src/shell.ts` et la coquille matérialisée dans
  `examples/site-embed/.recit/app/` (APP1 : ignorer `.recit/` dans biome ou git).

Reste : rien pour APP4. La table de `recit-config.mdx` n'a pas de ligne `app` (fichier hors du lot)
; à ajouter avec un lien vers `/documentation/configuration/app`.

### APP1 — Le paquet `@recit/app` — 29/10

Fait. `packages/tools/app` (`scope:tool`, publié, `exports` à trois conditions, construit par
`tsdown` comme `motion-render`), sur le contrat d'APP0 : il n'importe de `@recit/core` que des
types.

- `tauriConfig(app, { target, frontendDist, devUrl? })` : le `tauri.conf.json` complet, objet JSON
  pur, testé par instantanés (desktop, dev, fenêtre et origines, Android, iOS, lien profond et
  updater). La fenêtre `main` est déclarée avec `visible: false` et **`create: false`** : la
  coquille la construit elle-même dans `setup` pour lui attacher le script d'initialisation et la
  garde de navigation (`Builder::on_navigation` n'existe pas en Tauri 2.12, seul
  `WebviewWindowBuilder::on_navigation(Fn(&Url) -> bool)`). `app.allowedOrigins` voyage dans
  `plugins.recit.allowedOrigins`, que `lib.rs` lit à l'exécution avec `build.devUrl`.
  `plugins.deep-link` n'écrit que `desktop.schemes` (sur mobile le plugin exige un `host`), et aucun
  de ces deux plugins n'est encore dans `Cargo.toml` : APP9 (updater) et, un jour, le lien profond.
- `materializeShell({ siteDir, app, target, devUrl?, distDir?, shellDir?, icon? })` écrit
  `<site>/.recit/app` : les fichiers du gabarit (`Cargo.toml`, `build.rs`, `src/main.rs`,
  `src/lib.rs`, `src/window-open.js`, `capabilities/default.json`) seulement s'ils changent (les
  mtimes restent, Cargo ne recompile pas pour rien), `Cargo.lock` une seule fois, un `.gitignore`
  (`target/`, `gen/`, `icons/`), `tauri.conf.json`, puis les icônes. Source de l'icône : `app.icon`
  (un chemin `/x` est cherché dans `public/` puis dans le build, sinon relatif au site) ; sans
  fichier, `assets/default-icon.svg` du paquet et un avertissement `app-icon-default`. `tauri icon`
  ne tourne que si `icons/icon.png` manque ou est plus vieux que la source ; injectable (`icon`)
  dans les tests. Rend `{ shellDir, configFile, iconSource, diagnostics }`.
- `serveLikeTauri(distDir, { port?, host? })` et `resolveLikeTauri(distDir, path)` : `a/b`,
  `a/b.html`, `a/b/index.html`, puis `index.html` en **200** (comme le protocole de Tauri : un
  chemin inconnu arrive au routeur), types MIME dont `.wasm`, requête et fragment ignorés, chemin
  décodé, jamais hors du dossier ; 404 seulement sans `index.html`.
- `runTauri(shellDir, args, { env?, stdio? })` lance `tauri.js` de `@tauri-apps/cli` (2.12.4,
  épinglé au catalogue) avec `node`, dans la coquille ; `tauriEnv()` traduit `RECIT_APP_TARGET_DIR`
  en `CARGO_TARGET_DIR` sauf si ce dernier est déjà posé. `isPortInUse(port)` sonde en TCP.
- Version plan `.nx/version-plans/version-plan-app.md` (`prerelease` pour core, cli, app),
  `@recit/app` dans `release.projects`, `.recit/` dans le `.gitignore` racine.

Mesuré :

- `pnpm exec vitest run packages/tools/app scripts/__tests__/release-workflow.spec.ts scripts/__tests__/published-packages.spec.ts`
  : 6 fichiers, 94 tests verts (21 pour le paquet, 6 instantanés écrits).
- `pnpm exec nx run @recit/app:build`, `pnpm -F @recit/app typecheck`,
  `pnpm exec publint packages/tools/app` : verts. `pnpm check:imports` et `pnpm check:other` verts
  (46 projets, une arête `tool → foundation` de plus, autorisée). `pnpm check` s'arrête sur
  `oxfmt --check` : la note APP0 de ce journal (lignes autour de 1570-1593) n'est pas au format ; le
  paquet, lui, passe biome et oxfmt.
- La chaîne Rust est **installée sur cette machine** depuis ce matin (rustup stable 1.99, WebKitGTK
  2.50.6, xvfb) avec un cache Cargo partagé dans `~/.cache/recit-app-target` : la coquille
  matérialisée pour `examples/site-embed` (vraie passe `tauri icon` sur l'icône par défaut) compile
  en `cargo build --release --manifest-path examples/site-embed/.recit/app/Cargo.toml` en 24 s sans
  avertissement, binaire de 25 Mo qui embarque le `dist/`, et tourne 12 s sous `xvfb-run` sans
  erreur (seul un avertissement AT-SPI). APP5 part de là.

Pièges :

- `tauri::generate_context!()` exige `icons/icon.png` **et** l'existence de `frontendDist` à la
  compilation : `materializeShell` garantit les icônes, et `recit app build` doit construire le site
  avant toute commande Cargo (y compris `cargo check`).
- `frontendDist` se résout depuis le dossier du `tauri.conf.json` : `../../dist` depuis
  `.recit/app/`.
- Deux fichiers hors du périmètre d'APP1 attendent une ligne : `scripts/publint-all.mjs` (la table
  des dossiers, sans quoi `pnpm publint` refuse `@recit/app`) et
  `scripts/__tests__/published-packages.spec.ts` (`RELEASED`), vérifiés à la main entre-temps.

### APP6 — CI desktop sur trois systèmes — 29/10

Nouveau workflow `.github/workflows/app.yaml`, manuel (`workflow_dispatch`, entrée `site`, défaut
`examples/site-embed`), matrice `ubuntu-24.04`, `macos-15`, `windows-2025`, `fail-fast: false`,
`shell: bash` partout. Étapes : checkout, pnpm, Node (`.tool-versions`), Rust stable par
`dtolnay/rust-toolchain@683b588c487a69d01f6c6ddf40f036ff682cc2c8` (commit `master`, que le tag `v1`
pointe aussi ; la chaîne vient de l'entrée `toolchain`), cache Cargo par
`Swatinem/rust-cache@d1527c8f67ef9af357290475d4af4b0fab8f12e0` (v2.9.3, clé sur le `Cargo.lock` du
gabarit `packages/tools/app/shell`, cible `.cargo-target` à la racine, `cache-on-failure`), la ligne
apt de §3 plus `xvfb` et `xauth` sur Linux, `pnpm install --frozen-lockfile`, le build du site par
`nx run <projet>:build` (le nom vient du `package.json` du dossier `site`, `^build` tire le
framework, la CLI et `@recit/app`), puis
`pnpm --filter ./<site> exec recit app build --skip-site-build` avec
`RECIT_APP_TARGET_DIR=.cargo-target` ; secrets macOS (`APPLE_*`) et Windows (`WINDOWS_CERTIFICATE*`)
lus en env facultatif, le job passe sans eux ; test de fumée (binaire
`.cargo-target/release/recit-app[.exe]`, sous `xvfb-run` sur Linux, lancé en arrière-plan et tué
après 10 s s'il vit encore, sinon son code de sortie fait échouer l'étape : pas de `timeout`, macOS
ne l'a qu'en `gtimeout`) ; `actions/upload-artifact` v7.0.2 des `.deb`, `.AppImage`, `.dmg`, `.msi`
et `.exe` NSIS, `if-no-files-found: error`. Rien dans `build.yaml` ni dans `pnpm verify`.

**Pas encore exécuté** : aucun push depuis cette machine. Pour le lancer : onglet Actions, workflow
« App », _Run workflow_ sur `main`, ou `gh workflow run app.yaml -f site=examples/site-embed`. Le
premier run compile Tauri sans cache (une dizaine de minutes par système) ; les suivants le
réutilisent.

Vérifié : `pnpm exec vitest run scripts/__tests__/release-workflow.spec.ts` → 7 tests verts (toutes
les actions épinglées par SHA) ; `pnpm exec oxfmt --check .github/workflows/app.yaml` vert ; biome
ignore les YAML (`Checked 0 files`, comme `build.yaml`) ; analyse YAML par le paquet `yaml` : 11
étapes, 6 `uses`, 3 systèmes.

Dépend d'APP2 : `recit app build --skip-site-build` et `@recit/app` dans les dépendances de
`examples/site-embed` doivent exister avant le premier run. À vérifier au premier run : `rust-cache`
accepte le gabarit comme workspace (il y lance `cargo metadata`), les bundles atterrissent bien sous
`.cargo-target/release/bundle/` (Tauri suit `CARGO_TARGET_DIR`), et le binaire nu démarre sur macOS
hors de son `.app`.

### APP3 — Le bundle vérifié en e2e — 29/10

Le `dist/` d'`examples/site-embed` servi comme le protocole de Tauri le résout (`serveLikeTauri` de
`@recit/app`), et vérifié par Playwright sans Rust.

- Fichiers : `e2e/site-app-bundle.spec.ts` (nouveau, cinq tests, aucune capture d'écran),
  `e2e/serve-app-bundle.mjs` (nouveau : `node e2e/serve-app-bundle.mjs <dist> <port>`, le serveur du
  `webServer`), `e2e/ports.ts` (`APP_BUNDLE_PORT = 4379`, hors de `E2E_PORTS`),
  `playwright.config.ts` (une entrée `webServer` de plus), `e2e/package.json` (`@recit/app`).
- Ce que chaque test prouve : la page d'accueil vient d'`index.html` en 200 avec ses assets ; un
  clic vers `/stories/lorem-ipsum` est une navigation du routeur (un marqueur posé sur `<html>`
  survit, un rechargement l'aurait perdu) ; l'adresse profonde rechargée est servie par son propre
  `.html` (le `<title>` du récit, pas le repli) ; la recherche Pagefind rend des résultats et tout
  (`pagefind.js`, `wasm.<lang>.pagefind`, `.pf_index`, `.pf_fragment`) vient du bundle ;
  `/nulle-part` reçoit `index.html` en 200 et le routeur affiche « Page not found ». Chaque test
  relève toutes les requêtes (`page.on("request")`) et échoue à la première qui sort de l'origine du
  serveur : aucune, polices comprises.
- Mesuré : `pnpm exec playwright test e2e/site-app-bundle.spec.ts` → 5 passed (30 s) ; avec
  `--repeat-each=2` → 10 passed. `check-package-boundaries` vert (`scope:test` dépend de tout),
  biome, oxfmt et `tsgo` du paquet `recit-e2e` verts.
- Non fait : `pnpm test:e2e:linux e2e/site-app-bundle.spec.ts`. L'image `v1.64.1-noble` et le volume
  de cache sont là, mais le conteneur reconstruit les 47 projets, ce qui dépasse les dix minutes
  d'une commande de l'agent, et il aurait copié les fichiers en cours d'APP2. À lancer après la
  vague 2. Le `dist/` de l'exemple date du 28/10 (avant APP0) et n'a pas été reconstruit ici : le
  bundle ne dépend pas de la section `app:`.
- Constats pour APP5 et le plan : (1) les assets sont référencés en **absolu** depuis la racine
  (`/assets/entry-….css`), pas en relatif comme le dit §4.5 ; c'est bon à la racine de
  `tauri://localhost` et c'est même mieux pour le repli `index.html` sur un chemin profond, et un
  `basePath` le casserait (APP0 le refuse). (2) `index.html` est la page d'accueil prérendue entière
  (`/` redirige vers `/index`), donc un chemin inconnu reçoit le HTML de l'accueil et le routeur
  rend la 404 après hydratation, au prix d'une réhydratation (le test n'exige pas zéro erreur
  console sur ce cas) ; `404.html` existe dans le bundle mais la chaîne de Tauri ne le consulte
  jamais. À regarder sous le vrai protocole. (3) Le wasm de Pagefind n'a pas d'extension `.wasm` :
  `pagefind.js` le charge en binaire et le décompresse lui-même, le type MIME `application/wasm`
  n'entre pas en jeu. (4) Un clic avant l'hydratation est une navigation complète : le test ouvre
  d'abord la recherche (`openSearch` réessaie) comme signal d'hydratation.

### APP2 — La commande recit app — 29/10

Fait : `recit app build|dev|preview|android|ios|icon` dans `@recit/cli`
(`packages/cli/src/commands/app.ts`, enregistrée dans `src/index.ts`), `@recit/app` en peer
optionnel chargé à la demande par `loadOptional` (le motif de `@recit/motion-render`), et en
`devDependency` d'`examples/site-embed` pour que nx voie l'arête.

- `build [dir] [--target desktop|android|ios] [--skip-site-build]` : `recit.config` chargé (le
  module tel quel, options du site comprises), `version` du `package.json` du site,
  `resolveAppConfig`, diagnostics imprimés comme ceux de `recit build` et arrêt sur erreur ;
  `recit-site build` lancé dans le dossier du site (son `node_modules/.bin`, remonté jusqu'à la
  racine) **avant** toute commande Cargo ; `materializeShell` ; `tauri build`, `tauri android build`
  ou `tauri ios build`.
- `dev [dir] [--port] [--target]` : port du `recit.config` (`port`), sinon 3000 ; `isPortInUse`
  réutilise un serveur qui répond, sinon `recit-site dev --port` est lancé et attendu (60 s, arrêt
  si le processus meurt avant d'écouter) ; coquille avec `devUrl` ; `tauri dev` ; le serveur lancé
  est arrêté à la fin.
- `preview [dir] [--port]` : `serveLikeTauri(dist)`, URL imprimée, jusqu'à SIGINT.
- `android|ios <init|dev|build> [--dir] [-- …]` : arguments après `--` transmis tels quels ; les
  prérequis de §3 (`ANDROID_HOME`, `NDK_HOME`, `JAVA_HOME` ; macOS pour iOS) sont listés sans rien
  lancer quand ils manquent, et un échec de `tauri` renvoie à la page de doc de la plateforme.
- `icon [source] [--dir]` : `icons/icon.png` oublié puis `materializeShell`, qui régénère.

Mesuré :

```text
pnpm exec vitest run packages/cli            5 fichiers, 93 tests verts (23 pour recit app)
pnpm -F @recit/cli typecheck                 vert
pnpm exec nx run @recit/cli:build            vert ; recit app --help depuis examples/site-embed
pnpm exec nx run recit-example-site-embed:build puis recit app preview --port 4311 :
  /  200 text/html · /stories/lorem-ipsum  200 · /nulle-part  200 (index.html) ·
  /pagefind/pagefind.js  200 text/javascript
pnpm check                                   vert (oxfmt, biome, frontières, version plans, liens)
```

Écart au plan : `packages/cli/src/config/load-config.ts` reçoit un champ `raw` (le module chargé,
options du site comprises), parce que `loadConfig` ne rendait que les sections Récit alors que
`resolveAppConfig` lit `canonicalUrlOrigin`, `basePath`, `cdnUrl`, `search` et `site.title` sur
l'objet complet. Trois lignes, additives.

Pour APP5 : `pnpm --filter recit-example-site-embed exec recit app build` enchaîne le build du site,
la coquille dans `examples/site-embed/.recit/app` et `tauri build` ; poser
`RECIT_APP_TARGET_DIR=~/.cache/recit-app-target` pour réutiliser le cache. `recit app dev` ne crée
pas `dist/` : si `tauri dev` exige `frontendDist`, construire le site une fois. Pour le mobile,
`tauri android dev` reçoit `devUrl` en `localhost` : à vérifier (APP7) si le `--host` de la CLI
Tauri est nécessaire pour un appareil physique.

### APP8 — iOS — 29/10

Lot écrit sur une machine Linux, d'après la documentation de Tauri 2 : **rien n'a tourné sur un
Mac**. Livré, vérifié ici pour ce qui se vérifie sans Xcode :

- `packages/tools/app/src/ios.ts` (exporté par `@recit/app`) : `parseIosExportMethod` (noms Xcode 15
  `app-store-connect`, `release-testing`, `debugging` ; les anciens `app-store`, `ad-hoc`,
  `development` traduits ; défaut `debugging`), `iosDevelopmentTeam` (`app.ios.developmentTeam` puis
  `APPLE_DEVELOPMENT_TEAM`), `iosEnv` (pose `APPLE_DEVELOPMENT_TEAM` pour `tauri ios init|build`),
  `iosSigning` et `canSignIos` (certificat, profil, clé API lus dans l'environnement),
  `iosBuildArgs` (`--export-method` toujours explicite, `--build-number` optionnel),
  `IOS_PROJECT_DIR`, `IOS_BUILD_DIR`. Spec `ios.spec.ts`, 8 tests.
- `.github/workflows/app-ios.yaml` : manuel, `macos-15`, Rust avec `aarch64-apple-ios` et
  `aarch64-apple-ios-sim`, `rust-cache`, versions Xcode au journal, build du site, CLI Tauri de
  `@recit/app` sur le `PATH` (la phase de script du projet Xcode l'appelle),
  `recit app ios init -- --ci`, puis **sans secrets** une compilation simulateur non signée
  (`xcodebuild build … CODE_SIGNING_ALLOWED=NO`, schéma `*_iOS` relevé par `xcodebuild -list`),
  **avec secrets** (`APPLE_DEVELOPMENT_TEAM`, `IOS_CERTIFICATE`, `IOS_CERTIFICATE_PASSWORD`,
  `IOS_MOBILE_PROVISION`) un trousseau temporaire,
  `recit app ios build -- --export-method <entrée> --build-number <run>`, artefact `.ipa`, envoi
  TestFlight optionnel par `xcrun altool` (`APPLE_API_ISSUER`, `APPLE_API_KEY`, `APPLE_API_KEY_P8`),
  trousseau supprimé en fin de job. Mêmes actions et mêmes SHA que `app.yaml`, rien de nouveau à
  épingler.
- `sites/docs/pages/documentation/configuration/app-ios.mdx` : prérequis, section, commandes,
  signature, CI et secrets, TestFlight, une section « Not verified yet » et une admonition qui dit
  l'origine documentaire du flux.

Mesuré :

```text
pnpm exec vitest run packages/tools/app scripts/__tests__/release-workflow.spec.ts
                                              8 fichiers, 57 tests verts (ios.spec : 8)
pnpm -F @recit/app typecheck                  vert
oxfmt --check et biome                        verts sur les cinq fichiers du lot
pnpm check                                    s'arrête sur packages/tools/app/src/__tests__/deck.spec.ts
                                              (APP10, en cours) ; check:imports et check:other verts
                                              (admonitions, frontmatter, frontières, interfaces, liens)
YAML (paquet yaml)                            17 étapes, 6 `uses`, 3 entrées (site, export_method, upload)
```

Non vérifié, à regarder au premier run sur un Mac (liste reprise dans la page de doc) : la phase de
script Xcode trouve bien la CLI Tauri par le `PATH` ; le schéma se nomme `<app>_iOS` ; la
compilation simulateur sans signature passe la phase de script ; l'`.ipa` atterrit dans
`gen/apple/build/<arch>/` ; `altool --upload-app` accepte encore la clé API (sinon Transporter).
`recit app build --target ios` (APP2) ne passe pas d'`--export-method` : Tauri choisit le sien ;
brancher `iosBuildArgs` dans `app.ts` quand APP10 aura rendu le fichier. Lancement :
`gh workflow run app-ios.yaml -f site=examples/site-embed -f export_method=debugging`.

### APP10 — Deck seul — 29/10

`recit app build <story.md|.mdx>` construit un deck seul en application, avec la même coquille que
les sites. Fait et vérifié ; aucune commande git.

**Fichiers.** Créés : `packages/tools/app/src/deck.ts` (`isStoryFile`, `deckProjectDir`,
`deckOutputDir`, `storyTitle`, `resolveDeckAppConfig`, `STORY_EXTENSIONS`) et son spec (7 tests).
Modifiés : `packages/tools/app/src/index.ts` (un bloc d'export), `packages/cli/src/commands/app.ts`
(branche « fichier de récit » de `loadAppSite`, `buildDeck` injectable, refus de `--remote` et de
`recit app dev` sur un deck, `previewApp` depuis le projet du deck),
`packages/cli/src/__tests__/app.spec.ts` (7 tests de plus), `packages/cli/src/index.ts` (hors
périmètre, trois ajouts : import `Option`, `--remote` caché sur `app build` et `app dev`,
description de `build`), `examples/deck-pitch/recit.config.ts` (`app:` Lorem Ipsum).

**Comment ça marche.** Un argument qui est un fichier, et non un dossier, est un deck : `.md` ou
`.mdx`, sinon erreur. Le projet est le dossier du `recit.config` trouvé en remontant, sinon celui du
fichier ; `dist/` et `.recit/app/` y vivent comme pour un site. `app:` est lue dans ce config par
`resolveDeckAppConfig` : même schéma, mêmes diagnostics, et le `title:` du frontmatter tient lieu de
`productName` quand la section n'en donne pas (sans section, `app-missing` comme pour un site). Le
deck se construit en processus par `buildProject` (`recit build`, sortie relative `base: "./"`), ses
diagnostics s'impriment sur le fichier du récit et un blocage arrête avant la coquille ; puis
`materializeShell` et `tauri build`. `--skip-site-build` réutilise `dist/`. `--remote` : diagnostic
`app-remote` (« No remote control in the application… »), code 1, rien ne tourne.
`recit app dev slides.md` : `app-deck-dev`, renvoie vers `recit app build` (la boucle de dev d'un
deck reste à brancher sur `startDevServer`, pas sur `recit-site`).

**Vérification.** `vitest run` de `app.spec.ts` et `deck.spec.ts` : 37 tests verts (vitest ne
signale aucune erreur de type) ; `pnpm -F @recit/cli typecheck`, `pnpm -F @recit/app typecheck`,
biome sur les sept fichiers touchés et `pnpm check` complet : verts. Vrai run :
`pnpm exec nx run-many -t build -p @recit/app @recit/cli`, puis dans `examples/deck-pitch`,
`RECIT_APP_TARGET_DIR=~/.cache/recit-app-target recit app build slides.md` : deck reconstruit
(`dist/index.html`, titre « Lorem — Product pitch »), coquille dans `examples/deck-pitch/.recit/app`
(`productName` « Lorem Ipsum Pitch », `frontendDist` `../../dist`), 1 min 38 avec le cache, `.deb`
4,35 Mio, `.rpm` 4,35 Mio, `.AppImage` 95,6 Mio dans `~/.cache/recit-app-target/release/bundle/`.
Avertissement `app-icon-default` : l'exemple n'a pas de favicon. Le binaire embarque les clés
`assets/main.*` du deck (`strings`), le `.deb` installe `usr/bin/recit-app` et
`Lorem Ipsum Pitch.desktop`, et sous `xvfb-run` le processus vit jusqu'au `kill` (code 143).
`xdotool` ne voit aucune fenêtre sous Xvfb sans gestionnaire de fenêtres, ni pour cette coquille ni
pour la coquille d'essai dont la fenêtre est visible d'emblée : le titre de la fenêtre reste à
vérifier à l'écran (APP5).

**APP10.2, la console présentateur : non fait, la coquille appartient à APP5.** Le runtime du deck
ouvre sa console par `window.open` sur la même origine ; `shell/src/window-open.js` envoie
aujourd'hui tout `window.open` au navigateur système, donc la console partirait dehors avec une URL
`tauri://` injoignable. À faire sur la coquille, en une passe : (1) `src/lib.rs` : une commande
`#[tauri::command] fn open_window(app, url, label)` qui construit la seconde fenêtre avec le même
`WebviewWindowBuilder` que `setup` (même `initialization_script`, même `on_navigation`, montrée à
`PageLoadEvent::Finished`), enregistrée par `invoke_handler` ; (2) `src/window-open.js` : quand
l'URL résolue a l'origine de `location` (chemin relatif compris),
`internals.invoke("open_window", { url, label })` au lieu de l'opener, et retour d'un proxy
`{ closed, close() }` qui appelle `plugin:window|close` sur ce label, pour le `close()` du deck ;
(3) `capabilities/default.json` : `core:window:allow-close` en plus (une commande de l'application
n'a pas besoin de permission ACL) ; (4) vérifier à l'écran que le canal entre les deux fenêtres (un
`BroadcastChannel` par deck, `useSlideSync.ts` ; `Presentation.tsx:222` ouvre la console) fonctionne
entre deux webviews WebKitGTK, puis WKWebView et WebView2.

**Pièges.** Le titre sert tel quel : un titre à ponctuation (« Lorem — Product pitch ») deviendrait
le nom des paquets, et Debian refuse un tel nom de paquet ; l'exemple donne donc un `productName`
explicite. `recit app build` relance toujours `recit build` sans `--no-strict` : un deck en
surcharge est bloqué, comme `recit build`. `packages/cli/src/commands/app.ts` est rendu : APP8 peut
y brancher `iosBuildArgs`.

### APP7 — Android — 29/10

Fait ici, sur la machine de travail (pas d'appareil, pas d'émulateur) : `examples/site-embed` en
`.apk` et `.aab` signés par un keystore de test, à partir de la coquille d'APP1 et de la commande
d'APP2. Aucune commande git.

- `packages/tools/app/src/android.ts` (+ `__tests__/android.spec.ts`, 16 tests, export dans
  `index.ts`) : `androidKeystoreFromEnv` (`RECIT_ANDROID_KEYSTORE`, `_KEYSTORE_PASSWORD`,
  `_KEY_ALIAS`, `_KEY_PASSWORD` facultatif), `writeKeystoreProperties`
  (`gen/android/keystore.properties`, mode 600), `wireReleaseSigning` (un bloc `android {}` ajouté
  en fin de `app/build.gradle.kts`, marqué `recit:signing`, `import java.util.Properties` garanti en
  tête, remplacé s'il a changé), `prepareAndroidSigning` (l'enchaînement ; sans keystore :
  `app-android-unsigned`, propriétés périmées supprimées ; erreurs
  `app-android-keystore-incomplete`, `-missing`, `app-android-not-initialized`), `androidArtifacts`.
- `.github/workflows/app-android.yaml` (manuel, `ubuntu-24.04`, `setup-java` v6.1.0 et
  `setup-android` v4.0.4 épinglés par SHA, cibles Rust, `rust-cache`, keystore en secret base64,
  `.apk` + `.aab` en artefacts) : spec `release-workflow.spec.ts` verte, **jamais lancé** (pas de
  push d'ici).
- `sites/docs/pages/documentation/configuration/app-android.mdx` : prérequis réels, commandes,
  signature, boucle de dev sur le LAN (`-- --host`), limites.

Machine : `openjdk-17-jdk-headless` (apt), cmdline-tools 13114758 dans `~/Android/Sdk`,
`platform-tools`, `platforms;android-34`, `build-tools;34.0.0`, `ndk;27.2.12479018`, les quatre
cibles `rustup`. Gradle a ajouté seul `platforms;android-36`, `android-37` et `build-tools;36.0.0` :
le gabarit de Tauri 2.12 compile contre le SDK 37 (`compileSdk = 37`, `targetSdk = 37`,
`minSdk = 24` depuis `app.android`, `applicationId = com.example.loremipsum`).

Ce qui a marché, dans l'ordre, avec `JAVA_HOME`, `ANDROID_HOME`, `NDK_HOME`, `CI=true`,
`RECIT_APP_TARGET_DIR=$HOME/.cache/recit-app-target` et
`PATH=packages/tools/app/node_modules/.bin:$PATH` :

```bash
pnpm --filter recit-example-site-embed exec recit app android init -- --ci --skip-targets-install   # 2 s
keytool -genkeypair -keystore .recit/app/gen/android/recit-test.jks -alias recittest …             # test, hors git
RECIT_ANDROID_KEYSTORE=… node --input-type=module -e "import { prepareAndroidSigning } from '@recit/app'; …"
pnpm --filter recit-example-site-embed exec recit app android build -- --target aarch64 --apk --config '{"version":"0.1.0"}'
pnpm --filter recit-example-site-embed exec recit app android build -- --target aarch64 --aab --config '{"version":"0.1.0"}'
```

Mesures : Rust `aarch64-linux-android` en release, première compilation 1 min 42 ; `.apk` 2 min 07
ensuite (site 25 s, Rust en cache, Gradle) ; `app-universal-release.apk` **21,7 Mo** (la
bibliothèque `librecit_app_lib.so` de 20,1 Mo embarque le `dist/`, rien dans `assets/`) ;
`app-universal-release.aab` **15,2 Mo** en 1 min 26. `apksigner verify --print-certs` : signé par
`CN=Lorem Ipsum, O=Example, C=FR` ; `aapt2 dump badging` : `versionName 0.1.0`, `versionCode 1000`,
`minSdkVersion 24`, `targetSdkVersion 37`, libellé « Lorem Ipsum Docs », `arm64-v8a`.

Pièges, dans l'ordre où ils ont mordu :

1. `recit app android build` n'a pas `--skip-site-build` : `allowUnknownOption` l'envoie à
   `tauri android build`. Le site se reconstruit à chaque fois (25 s) ; le workflow ne passe pas
   l'option.
2. Tauri refuse `version: 0.0.0` sur Android (« must be at least 0.0.1 ») et c'est la version du
   `package.json` de l'exemple : overlay `-- --config '{"version":"0.1.0"}'` en attendant un
   `app.version` dans l'exemple ou un diagnostic d'erreur pour la cible Android.
3. Le projet Gradle généré rappelle la CLI par `pnpm tauri android android-studio-script` depuis la
   coquille, qui n'a pas de `node_modules` : « Command "tauri" not found ». `pnpm exec` retombe sur
   le PATH, d'où le `.bin` de `@recit/app` dans le PATH. Sans `npm_execpath` (la CLI lancée hors
   pnpm), Tauri écrit `node` + un argument nu `tauri` dans `BuildTask.kt`, qui ne peut pas marcher :
   garder le flux pnpm.
4. Dans un script Kotlin de Gradle, `java.util.Properties` se lit `project.java…` : d'où l'import.
5. `packages/theme/dist` a été reconstruit par un autre lot au milieu d'un build du site, qui a
   échoué une fois (`UNLOADABLE_DEPENDENCY`) ; relancé.
6. Le build client du site avertit : `node:path` externalisé, importé par
   `packages/core/dist/config/app.js` (APP0) ; `@recit/core/config` entre dans le bundle navigateur.
7. `tauri android init` relancé régénère `BuildTask.kt` et `build.gradle.kts` mais garde
   `keystore.properties` et le `.jks` ; le bloc de signature se remet en place seul.

Reste : (a) dans `@recit/app`, `tauriEnv` ajoute `path.dirname(tauriBin())/../../.bin` (le
`node_modules/.bin` du paquet qui porte `@tauri-apps/cli`) au `PATH` ; (b) dans `@recit/cli`,
`mobileApp` appelle `prepareAndroidSigning(shell.shellDir, process.env)` avant `tauri android build`
et imprime ses diagnostics, et les commandes `android|ios` gagnent `--skip-site-build` ; (c)
`app.version` ou un diagnostic pour `0.0.0` sur Android ; (d) le premier run du workflow ; (e)
`adb install` sur un appareil et le mode avion, `recit app android dev -- --host` sur un émulateur :
impossibles ici, la doc les décrit d'après la CLI.

### APP5 — La coquille compilée et le premier binaire — 29/10

Tout vérifié sur cette machine (Debian sans écran, Rust 1.99, WebKitGTK 2.50.6), avec le cache Cargo
partagé `RECIT_APP_TARGET_DIR=~/.cache/recit-app-target`. La coquille de `@recit/app` n'a rien eu à
corriger : ce qu'elle promet (plan 18 §4.3, §4.6, §4.8) tient sur un vrai binaire.

- **APP5.1** — `cargo check --manifest-path examples/site-embed/.recit/app/Cargo.toml` : vert (1 min
  09, métadonnées du profil `dev`). `pnpm --filter recit-example-site-embed exec recit app build` :
  build du site, coquille « Lorem Ipsum Docs » 0.0.0 (`com.example.loremipsum`), `tauri build`, 2
  min 17 en tout dont 55 s de compilation de la seule caisse `recit-app`.
- **APP5.2** — Tailles : binaire nu 25 140 568 o ; `.deb` 14 082 630 o (13,4 Mio, paquet
  `lorem-ipsum-docs`, binaire `/usr/bin/recit-app`) ; `.rpm` 14 088 028 o ; `.AppImage` 109 771 256
  o (104,7 Mio, WebKitGTK embarqué). Le binaire nu et l'AppImage (FUSE présent, pas besoin de
  `--appimage-extract-and-run`) ouvrent sous Xvfb la fenêtre « Lorem Ipsum Docs » 1280 × 800,
  `Map State: IsViewable`, deux processus WebKit, vivants après 20 s et 25 s, rien sur stderr hors
  l'avertissement AT-SPI. Le `dist/` embarqué est le bon : `strings` du binaire donne les clés
  `/index.html`, `/stories/lorem-ipsum.html`, `/pagefind/pagefind.js`.
- **APP5.3** — Garde de navigation prouvée avec une coquille de test en `debug` (`devUrl` vers une
  page locale) et un faux `xdg-open` en tête de `PATH` qui journalise ses arguments : la page
  déclenche `window.open("https://example.com/popup")`, un clic sur `<a target="_blank">` et
  `location.href = "https://example.com/external"`. Le journal reçoit exactement ces trois URL, la
  page reste en place (elle répond encore `still-here` avec son titre 2 s plus tard), et la coquille
  trace `on_navigation https://example.com/external` refusé. La console présentateur d'un deck
  publié reste à vérifier en APP10.
- **APP5.4** — Fenêtre cachée jusqu'au chargement : avec une page servie après 4 s, aucune fenêtre X
  n'existe pendant 3 s (la requête est pourtant partie), puis la fenêtre apparaît au `Finished`, à
  la taille d'`app.window` (1024 × 700 dans le test), titre = `productName`.
- **APP5.5** — Cache partagé entre deux sites : coquille du site docs (« Récit Docs »,
  `dev.recit.docs`) construite dans le même `target` : **une seule ligne `Compiling`**
  (`recit-app`), 36 s, 42 s avec `.deb` et `.rpm` ; Tauri n'est pas recompilé. `.deb` docs : 14 776
  116 o.
- **`recit app dev`** (question d'APP2) : fonctionne de bout en bout sous Xvfb : `recit-site dev`
  sur 3000 réutilisé ou lancé, `tauri dev`, fenêtre « Lorem Ipsum Docs » après 113 s (profil
  `debug`, dépendances déjà en cache). **`dist/` n'est pas nécessaire** : la coquille de test s'est
  compilée en `debug` avec `devUrl` dans un dossier sans `dist/`.

Pièges et constats, pour les lots suivants :

- **D-Bus** : sur cette machine le bus de session existant fait bloquer l'enregistrement GTK
  (`g_application_register` → `g_dbus_proxy_new_sync`, 25 s, vu à `gdb`) ; l'application semble
  muette pendant ce temps. Lancer les tests de fumée sous `dbus-run-session -- <binaire>` (fenêtre
  en 2 s) ; APP6 devrait l'ajouter au job Linux, ou laisser plus de 25 s avant de conclure.
  `xvfb-run` exige `xauth` ; `x11-utils` (`xwininfo`), `xdotool` et `gdb` installés pour sonder.
- **`productName` non ASCII** : le `.rpm` de « Récit Docs » échoue (`invalid name "récit-docs"`) et
  le `.deb` produit est **invalide** pour `dpkg` (champ `Package` avec un caractère interdit). À
  traiter dans `tauriConfig()` / `resolveAppConfig` : nom de paquet dérivé en ASCII, ou diagnostic.
- **Un seul nom de binaire** : chaque application installe `/usr/bin/recit-app` ; deux apps Récit
  sur la même machine se marcheraient dessus. `mainBinaryName` (Tauri 2) dérivé de l'identifiant
  règle les deux points précédents.
- **`RECIT_APP_TARGET_DIR` partagé** : le bundler AppImage vide `bundle/appimage/` à chaque build,
  le build de deck d'APP10 a effacé l'AppImage de `site-embed` (reconstruite ensuite) ; `.deb` et
  `.rpm` coexistent. `recit app build` devrait copier ses paquets sous `<site>/.recit/app/bundle/`
  et imprimer ce chemin.
- **`recit app build` sur les sites Récit** (`docs`, `plans`, `www`…) échoue au chargement du
  `recit.config` : le loader esbuild de la CLI ne connaît pas `import.meta.glob` de
  `@recit/site-kit` (`PackageTables.tsx:13`) → `(intermediate value).glob is not a function`. Seuls
  les sites sans site-kit (`examples/site-embed`) passent ; APP5.5 a matérialisé la coquille docs
  par `materializeShell` directement. À régler dans `packages/cli/src/config/load-config.ts`.
- `pnpm check` : `fmt:check`, `check:imports`, `check:other` verts ; `biome lint` échoue sur
  `examples/site-embed/.recit/app/gen/android/build/reports/**` (projet Android d'APP7), que biome
  ne lit pas dans `.gitignore` : ajouter `!**/.recit/**` aux `includes` de `biome.json`.
- Fichiers : `packages/tools/app/src/__tests__/shell-rust.spec.ts` (4 tests qui verrouillent le
  gabarit : caisse Tauri 2 mobile, fenêtre construite dans `setup` et montrée au `Finished`,
  `on_navigation` + `window-open.js`, capability sans `remote`) ; `sites/docs/recit.config.ts`
  (`app: { productName: "Récit Docs" }`). `pnpm exec vitest run packages/tools/app` : 8 fichiers, 56
  tests verts.

### APP11b — Intégration @recit/app, cœur, CI, doc — 29/10

Lot d'intégration créé par le coordinateur d'après les constats d'APP5 et d'APP7, côté `@recit/app`,
`@recit/core`, CI et documentation (la CLI est le lot APP11a, la coquille le lot APP9).

- **Plus de `node:path` dans `@recit/core/config`** : `packages/core/src/config/app.ts` prenait le
  nom du dossier du site par `path.basename`, et le module atteint le bundle navigateur par
  `@recit/core/config` (avertissement « node:path externalized » au build des sites). Remplacé par
  une découpe de chaîne (`directoryName`), tous séparateurs et barre finale compris ; aucun autre
  fichier de `packages/core/src/config` n'importe un module Node. Test ajouté ; le build de
  `site-embed` sans cache n'émet plus l'avertissement.
- **Noms de fichiers des applications.** Tauri dérive le nom du paquet Debian et RPM du
  `productName`, et les deux refusent un nom hors ASCII (`Récit Docs` → `récit-docs`, APP5). Deux
  aides exportées par `@recit/core/config` : `asciiProductName` (NFD, diacritiques retirés, le reste
  hors ASCII supprimé : `Récit Docs` → `Recit Docs`) et `binaryNameFromIdentifier` (`dev.recit.docs`
  → `recit-docs`, `com.example.loremipsum` → `example-loremipsum`). Dans `tauriConfig()`
  (`shellNames`) : `productName` = forme ASCII (ou le nom du binaire s'il ne reste rien),
  `mainBinaryName` = nom tiré de l'identifiant, et le titre de la fenêtre garde le nom exact. Deux
  applications ne partagent donc plus `/usr/bin/recit-app`. `resolveAppConfig` émet l'avertissement
  `app-product-name-ascii` (chemin `/app/productName`). Les six instantanés de
  `tauri-config.spec.ts` sont réécrits pour la clé `mainBinaryName` ; deux tests du cœur qui
  attendaient zéro diagnostic avec « Récit Docs » attendent désormais cet avertissement.
- **`tauriEnv`** : le `node_modules/.bin` qui porte le shim `tauri` (`tauriBinDir`, cherché en
  remontant depuis le paquet) passe en tête du `PATH`, et `npm_execpath` est posé depuis le `pnpm`
  ou le `npm` trouvé sur le `PATH` quand il manque (`packageManagerPath`) : le projet Android généré
  rappelle la CLI par `pnpm tauri …` depuis la coquille sans `node_modules`. Tests avec de faux
  répertoires.
- `examples/site-embed/recit.config.ts` : `app.version: "0.1.0"` (Android refuse `0.0.0`).
- `.github/workflows/app.yaml` : `dbus` dans la ligne apt, test de fumée sous
  `xvfb-run -a dbus-run-session --` (25 s d'attente D-Bus sinon), binaire trouvé dans
  `.cargo-target/release` puisqu'il porte désormais le nom de l'identifiant. Toutes les actions
  restent épinglées par SHA.
- `sites/docs/pages/documentation/configuration/app.mdx` : section « Names » (règle ASCII,
  `mainBinaryName`), ligne `app-product-name-ascii`, installateurs copiés dans `.recit/app/bundle/`
  (APP11a) avec `RECIT_APP_TARGET_DIR`, test de fumée sans écran, tailles mesurées (`.deb` 13,4 Mio,
  `.AppImage` 105 Mio, `.apk` 21,7 Mo, `.aab` 15,2 Mo).

Vérification :

```text
pnpm exec vitest run packages/tools/app packages/core scripts/__tests__
  Test Files  1 failed | 35 passed (36)   Tests  1 failed | 493 passed (494)
  (l'échec : shell-rust.spec.ts « updates itself only when the updater is configured », lot APP9 en cours)
pnpm -F @recit/app typecheck && pnpm -F @recit/core typecheck && pnpm exec nx run @recit/app:build
  verts (exit 0)
pnpm exec nx run recit-example-site-embed:build --skip-nx-cache | grep node:path
  no node:path warning
pnpm exec vitest run scripts/__tests__/release-workflow.spec.ts   7 tests verts ; app.yaml : 11 étapes, 6 uses, 3 systèmes
pnpm check
  biome : 31 avertissements préexistants, aucune erreur ; oxfmt : un seul fichier hors format,
  packages/tools/app/shell/src/window-open.js (APP9 en cours) ; check:imports et check:other verts
  (admonitions, frontmatter, 15 version plans, frontières de 46 projets, liens).
```

Reste : APP11a copie les installateurs dans `<site>/.recit/app/bundle/` et appelle
`prepareAndroidSigning` (la doc le suppose déjà) ; le nom affiché sous Linux et macOS perd son
accent (`Recit Docs`, le menu comme le `.app`), la fenêtre non ; sans gestionnaire de paquets sur le
`PATH`, `npm_execpath` reste absent et le projet Android généré ne sait pas rappeler la CLI ; le
workflow `app.yaml` n'a toujours pas tourné (aucun push d'ici).

### Plan 19 — Style : Mantine et CSS modules — 29/10

Nouveau plan `sites/plans/pages/19-style.md`, écrit à la demande de l'utilisateur et **non
implémenté** : tout le style passe sous Mantine 9 avec un seul mécanisme, un `*.module.css` par
composant, et plus aucune classe utilitaire ni `style` dans le JSX. Le plan part de trois
inventaires en lecture seule (947 `className` dans le framework, 54 composants `ui/` dont 27 sans
importeur, 21 paquets Radix ; inline partout dans les blocs motion ; Tailwind compilé par la CLI,
`site-stories` et Storybook), fixe la loi (§0.3), la cible (Mantine, PostCSS, couches, thème depuis
`theme:`, mode de couleur par attribut, typographie), les conventions des modules, une table de
correspondance composant par composant, le protocole de captures avant/après (couverture étendue,
galerie HTML, baselines Linux en conteneur par vague), le script `check-style-rule.js`, le skill
`mantine-style`, dix-sept lots en sept vagues, la branche `style/mantine` et treize questions
ouvertes. Deux relectures ont corrigé les dépendances entre lots d'une même vague (typographie et
`main.css` regroupés dans STY5, captures « avant » seules en vague 0 parce que le conteneur e2e
copie l'arbre entier).

Pour reprendre : lire le plan, répondre aux questions de §13 (props de style Mantine, Spotlight,
`deck.css` global, défauts zinc ou violet, version), puis demander la branche ; rien ne se lance
sans cette demande.

### APP11a — Intégration CLI — 29/10

Lot d'intégration côté CLI, créé par le coordinateur d'après les constats d'APP5, APP7, APP8 et
APP10. Fichiers : `packages/cli/src/config/load-config.ts`, `packages/cli/src/commands/app.ts`,
`packages/cli/src/index.ts`, `packages/cli/src/__tests__/{app,cli}.spec.ts`. Aucune commande git.

Fait :

- `load-config.ts` : plugin esbuild `importMetaGlobPlugin`, qui remplace `import.meta.glob()` (motif
  ou tableau de motifs littéraux, options `eager`, `import`, `query`) par un objet d'imports comme
  Vite, clés relatives au fichier qui l'appelle (à la racine de la config pour `/…`), `*` et `**`
  seulement, ni accolades ni négations. Les sites Récit (`@recit/site-kit`, `PackageTables.tsx`) se
  chargent en Node : fini `(intermediate value).glob is not a function`.
- `recit app build` : les paquets produits sont copiés dans `<site>/.recit/app/bundle/<type>/`
  (fichiers plus récents que le build seulement, car le dossier partagé `RECIT_APP_TARGET_DIR` est
  vidé par le bundler AppImage) et imprimés avec leur taille ; Android : `androidArtifacts` ; iOS :
  `.ipa` et `.xcarchive` de `gen/apple/build`.
- Android : `prepareAndroidSigning` avant `tauri android build`, diagnostics imprimés, arrêt sur
  erreur ; `--skip-site-build` est une option de `recit app android|ios` (il filait vers Tauri).
- iOS : `iosBuildArgs` (`--export-method`, défaut `debugging`, `--build-number`), options de
  `app build` et de `app ios build`, un `--export-method` après `--` gagne ; `iosEnv`
  (`APPLE_DEVELOPMENT_TEAM`) sur init, dev et build.
- Version `0.0.0` refusée pour android et ios (`app-version-required`), avertissement seul sur
  desktop. Description de `app build` : paquets dans `.recit/app/bundle`.
- Tests : sept de plus dans `app.spec.ts` (signature et son erreur, iOS, version, copie des paquets,
  `--skip-site-build` mobile, options CLI), un dans `cli.spec.ts` (glob eager, lazy, `import`,
  `query`, `/…`).

Mesures :

- `pnpm exec vitest run packages/cli` : 5 fichiers, 105 tests verts ; `pnpm -F @recit/cli typecheck`
  et `nx run @recit/cli:build` verts.
- `examples/site-embed`, `recit app build --skip-site-build` avec
  `RECIT_APP_TARGET_DIR=~/.cache/recit-app-target` : 3 min 04, trois paquets copiés dans
  `.recit/app/bundle/` (deb 15,1 Mio, rpm 15,1 Mio, AppImage 106,2 Mio), binaire
  `example-loremipsum` 0.1.0.
- `sites/docs`, même commande : la config se charge, mais `app-identifier-missing` : aucun site
  Récit ne pose `canonicalUrlOrigin` (`createSiteConfig` ne le fait pas), il faut
  `identifier: "dev.recit.docs"` dans la section `app:` de `sites/docs/recit.config.ts`, hors de mes
  fichiers.
- `pnpm check` : rouge sur `oxfmt` pour `packages/tools/app/shell/src/window-open.js` (APP9, en
  cours), le reste vert ; 31 avertissements biome préexistants.

Pièges : une reconstruction concurrente des paquets (`tsdown` vide `dist/`) fait échouer un
`recit app build` lancé au même moment (`@recit/core/dist` ou `@recit/site-stories/dist`
introuvable) : relancer. Les options de `import.meta.glob` doivent être des littéraux.

### APP9 — Mise à jour automatique, et APP10.2 — Console présentateur — 29/10

Fait et vérifié sur cette machine, sans commande git.

**Coquille** (`packages/tools/app/shell/`) :

- `Cargo.toml` : `tauri-plugin-updater = "2"` (résolu 2.14.1) en dépendance **desktop seulement**
  (`[target.'cfg(not(any(target_os = "android", target_os = "ios")))'.dependencies]`) ; pas de
  `tauri-plugin-process`, le redémarrage est `AppHandle::restart()` de Tauri. `Cargo.lock` mis à
  jour par le build.
- `src/lib.rs` : le plugin updater n'est enregistré que si `tauri.conf.json` a `plugins.updater`
  (`contains_key("updater")` sur le contexte) ; au démarrage, `check()` puis une fenêtre de
  proposition servie par un schéma `recit-update://` de la coquille (HTML dans `lib.rs`, pas de
  plugin dialog : aucune dépendance de plus) avec « Install and restart » (ou Entrée) et « Later »
  (ou Échap) ; commandes `install_update` (`download_and_install`, puis `app.restart()`) et
  `skip_update`. Commande `open_window(url, label, width, height)` : une page du site demandée par
  `window.open` ouvre une seconde fenêtre native avec le même script, la même garde `on_navigation`
  et le même affichage à `Finished` (`site_window()` sert aux deux) ; un label déjà ouvert navigue
  et prend le focus ; `WindowEvent::Destroyed` émet `recit:window-closed`.
- `src/window-open.js` : même origine que `location` → `open_window` et un proxy
  `{ closed, close() }` (`plugin:window|close`, `closed` suivi par l'événement) ; autre origine →
  opener.
- `capabilities/default.json` : `core:window:allow-close` en plus. Les commandes updater ne passent
  jamais par le webview, donc ni `updater:default` ni permission mobile.

**Prouvé** :

- Cycle complet 0.1.0 → 0.1.1 (`scratchpad/app9/cycle.sh`) : deux AppImage signées par une clé de
  test (`tauri signer generate`, clé privée hors dépôt), `latest.json` servi en local ; l'app 0.1.0
  journalise `update 0.1.1 available`, ouvre la fenêtre « Update » (440×200), Entrée →
  `installing update 0.1.1`, `update downloaded` (GET de l'AppImage par
  `tauri-plugin-updater/2.14.1`, signature vérifiée), `update installed, restarting`, nouveau
  processus, `up to date` ; l'AppImage sur disque est octet pour octet le build 0.1.1.
- Seconde fenêtre (`scratchpad/app9/win/`, coquille en `tauri dev` sur une page locale) :
  `window.open("/second.html", "presenter-test", "popup,width=800,height=600")` rend un proxy, une
  fenêtre native 800×600 apparaît, `BroadcastChannel` passe dans les deux sens entre les deux
  webviews WebKitGTK (`second got hello`, `first got world`), `popup.close()` ferme la fenêtre et
  `closed` passe à `true` ; `window.open` externe et `target=_blank` externe vont à `xdg-open`.
- `cargo check` de `examples/site-embed/.recit/app` (cache partagé) vert ;
  `vitest packages/tools/app scripts/__tests__/release-workflow.spec.ts` : 9 fichiers, 69 tests ;
  `pnpm check` vert.

**Workflow** `.github/workflows/app-release.yaml` (nouveau) : manuel, trois systèmes, build signé
(`TAURI_SIGNING_PRIVATE_KEY`, `_PASSWORD` exigés), artefacts renommés sans espace, `latest.json`
composé par un script Node depuis les `.sig`, GitHub Release en brouillon par `gh`. Non exécuté :
`gh workflow run app-release.yaml -f site=examples/site-embed`. Dépend de `createUpdaterArtifacts`
dans la config générée (ci-dessous).

**À faire ailleurs** :

- `tauriConfig()` : `bundle.createUpdaterArtifacts: true` quand `app.updater` est renseigné (sans
  lui, `tauri build` n'émet aucun `.sig` ; patché à la main dans les coquilles de test).
- `resolveAppConfig` : erreur si un endpoint n'est pas `https` (le plugin refuse l'app au démarrage
  : `PluginInitialization`) ; les tests locaux passent par `dangerousInsecureTransportProtocol`.
- Dans `examples/deck-pitch`, le raccourci `s` ouvre la console par ce chemin ; non piloté au
  clavier sous Xvfb (le clic et la touche synthétiques n'atteignent pas la page du deck).

**Pièges** : un `tauri build` concurrent dans le même `CARGO_TARGET_DIR` fait échouer linuxdeploy
(le bundler vide `bundle/appimage/`) et renomme le binaire ; les tests ont un `target` privé. Sans
gestionnaire de fenêtres, aucune fenêtre n'a le focus : `openbox` (installé) sous `xvfb-run` rend
`windowactivate` et les touches XTEST fiables ; le clic synthétique n'est pas reçu par WebKitGTK.
Tauri ne reporte pas `document.title` sur le titre natif. Sous `dbus-run-session`, la fenêtre arrive
en 2 s.

### Bilan du 29/10 — Plan 18 exécuté, plan 19 écrit — 29/10

Coordination de la journée (un agent par lot, en vagues, sur le même arbre). Tous les lots APP0 à
APP11 du plan 18 sont livrés ; la table « État au 29/10/2026 » en tête du §5 du plan résume lot par
lot, et chaque lot a sa note ci-dessus.

Corrections faites par le coordinateur entre les lots : exclusion de `.recit/` dans `biome.jsonc` ;
`identifier: "dev.recit.docs"` dans `sites/docs/recit.config.ts` ; ligne `app` dans la table de
`recit-config.mdx` ; `bundle.createUpdaterArtifacts` dans `tauriConfig()` quand `app.updater` est
renseigné ; diagnostic `app-updater-endpoint-insecure` (endpoints `https` obligatoires) ; un code
span coupé par une ligne vide dans `app-android.mdx` qui cassait le build de la doc.

Vérification finale : `pnpm verify` vert (60 tâches de build et typecheck, 1 591 tests unitaires) ;
e2e en conteneur, après régénération de quatre références Linux changées à dessein
(`docs-documentation-configuration-recit-config`, `docs-documentation-reference-architecture`,
`presentation-demos-pitch`, `presentation-demos-site`) : 182/182 au premier passage, 181/182 au
second avec le React #418 intermittent connu de `/custom-components` (voir le 26/10).

Reste hors de portée de cette machine : les quatre workflows (`app.yaml`, `app-android.yaml`,
`app-ios.yaml`, `app-release.yaml`) n'ont jamais tourné, il faut un push ; l'installation d'un
`.apk` sur un appareil et la boucle de dev sur le LAN ; tout iOS (Mac) ; la console présentateur
d'un vrai deck par le raccourci sous Xvfb (le mécanisme est prouvé sur une page d'essai) ;
`recit app dev` d'un deck seul n'est pas branché (diagnostic `app-deck-dev`).

Machine : Rust stable, WebKitGTK, xvfb, xauth, openbox, x11-utils, xdotool, gdb, dbus, JDK 17, SDK
Android 34 + NDK 27.2 et les cibles Rust Android installés ce jour ; cache Cargo partagé
`~/.cache/recit-app-target` (`RECIT_APP_TARGET_DIR`). Le commit « Claude limit » est un point de
contrôle de l'utilisateur pendant une coupure de limite d'usage ; les agents coupés ont été repris
avec leur contexte.

Plan 19 (`19-style.md`, Mantine et CSS modules) : écrit, relu trois fois, **pas implémenté** ; il
attend la demande explicite de l'utilisateur et la branche `style/mantine` qu'il créera lui-même.

### STY10.1 — Skill mantine-style et CLAUDE.md — 29/10

Premier lot du plan 19 (vague 1, indépendant de la branche). Créés :
`.claude/skills/mantine-style/SKILL.md` (188 lignes : quand l'utiliser, la loi de §0.3 en anglais,
permis, interdit, exceptions, la recette d'un composant avec `Callout.tsx` + `Callout.module.css`,
les conventions de §3, la recette de migration d'un composant Tailwind, le mode sombre et le pont
`.dark` jusqu'à STY9, Shiki, la vérification et la liste de contrôle) et
`.claude/skills/mantine-style/tailwind-to-css.md` (table de 70 utilitaires Tailwind → CSS module ou
Mantine). Modifié : `AGENTS.md` (`CLAUDE.md` est un lien symbolique vers lui, rétabli après qu'un
`perl -i` l'avait remplacé par une copie), section « Styling » après « UI », marquée en cours sur la
branche `style/mantine`, avec la loi en deux puces et le renvoi au skill ; rien d'autre n'a bougé
(STY10.2 fera la forme finale : « Core tech », « UI », « Bundle Size »).

Vérification : `oxfmt` sur les quatre fichiers, `diff CLAUDE.md AGENTS.md` vide, `pnpm check` vert
(biome, formats, imports, frontmatter, admonitions, version plans, frontières, interfaces, liens).
Aucune commande git.

À savoir pour la suite : le skill cite `scripts/check-style-rule.js`, `useCssVariable`,
`scripts/style-rule-allowlist.json` et la galerie, qui n'existent qu'à partir de STY0 et STY1 ; il
le dit à chaque mention. Les valeurs de l'échelle Mantine dans la table (espacements, rayons, points
de rupture) sont celles du thème par défaut de Mantine 9 : à relire quand STY2 fixe
`createRecitTheme`.

### STY1 — Captures avant — 29/10

Vague 0 du plan 19, sur l'arbre intact. Le jeu « avant » est généré, la galerie existe, le skill est
à jour. Aucune commande git.

**Couverture ajoutée** (`playwright.config.ts`, `e2e/*.spec.ts`) :

- Trois projets Playwright : `chromium` (inchangé, mêmes noms de référence), `chromium-dark` (tous
  les `site-*.spec.ts` avec le choix mémorisé `theme` de next-themes et `recit-theme` des decks
  posés à `dark` par `storageState`, plus `colorScheme: "dark"` ; références `-dark-linux.png`) et
  `mobile` (390 × 844, tactile, tests `@mobile` de `site-docs.spec.ts` seulement ;
  `-mobile-linux.png`). Le helper `darkMode(page)` de `e2e/support.ts` fait la même chose pour un
  test isolé ; `SITE_THEME_STORAGE_KEY` y est exporté pour STY2.
- `site-plans.spec.ts` : les 28 pages du site en pleine page (`tallPageScreenshot` coupe à 16 000 px
  les pages plus hautes, `support.ts`) ; `/journal` exclu, il change à chaque lot sans raison
  visuelle.
- `site-docs.spec.ts` : les trois pages `app*`, la 404 (`/404`, la page prérendue :
  `recit-site preview` répond un 404 nu sur un chemin inconnu), la recherche ouverte avec un
  résultat, l'en-tête et le pied de page seuls, et quatre captures mobiles (accueil, `recit-config`,
  `/present/showcase`, tiroir de navigation ouvert). Pas de bannière : aucun site n'en configure.
- `site-motion.spec.ts` (`/present/bridge`), `site-i18n.spec.ts` (index et récit anglais),
  `site-app-bundle.spec.ts` (accueil, récit, 404, assertions DOM gardées), `deck-features.spec.ts`
  (console présentateur en sombre, `?print=1` de showcase).
- `storybook.spec.ts` (nouveau) : les 16 stories de `index.json` en clair et en sombre par
  `iframe.html?id=…&globals=theme:…`, servies depuis `sites/storybook/storybook-static` sur le port
  4380 par `e2e/serve-app-bundle.mjs` ; un test vérifie que la liste suit l'index.
- Le rapport JSON de Playwright (`test-results/report.json`) alimente la galerie.

**Références** : 213 `-linux.png` avant, 443 après (+221 `-dark`, +4 `-mobile`, 56 plans, 32
Storybook, le reste docs, motion, i18n, bundle, deck) ; aucune référence existante modifiée.
Passages en conteneur : génération 444/444, passage 1 444/444, passage 2 442/444 avec le React #418
intermittent connu (26/10) sur `presentation /demos/course` en clair et en sombre, puis la spec
`site-presentation` seule 50/50. Après la mesure écrite dans `19-style.md` §7.3 (page `/19-style`),
ses deux références sont régénérées par `pnpm test:e2e:update:linux e2e/site-plans.spec.ts` et
confirmées par un passage.

**Mesure d'un passage** (`[e2e-linux] <phase>` dans `scripts/e2e-linux.sh`, 8 cœurs, caches chauds)
: 7 à 8 min au total, dont 1 s de copie, 25 à 45 s d'installation, 2 à 2 min 30 de construction, 4
min 20 à 5 min 20 de tests pour 444 tests. STY1.5 (saut d'installation) : hors périmètre, il ne
rendrait que 30 s.

**Galerie** : `node scripts/screenshot-gallery.js` écrit `test-results/gallery/index.html` depuis
`test-results/linux/report.json` (avant, après, diff, ratio de pixels, filtre par spec) ;
`node scripts/screenshot-gallery.js --serve 4390` la sert sur toutes les interfaces (machine sans
écran) et le bouton « Accept » enregistre `test-results/gallery/decisions.json` par `POST`. Test
`scripts/__tests__/screenshot-gallery.spec.ts` (5 tests, diff fabriqué, GET, POST). Le script est en
`.js` et non `.mjs` : la règle `useImportExtensions` de biome ne connaît pas `.mjs`.

**Navigateur** : chaque entrée `webServer` reçoit `env: { ...process.env, BROWSER: "none" }`
(`recit-site preview` héritait de `server.open` et, par le `$BROWSER` de VS Code Remote, ouvrait un
onglet par site sur le poste de l'utilisateur) ; toute commande locale de ce lot a tourné avec
`BROWSER=none` exporté.

**Vérifications** : `pnpm -F recit-e2e typecheck`, biome, oxfmt,
`pnpm exec vitest run scripts/__tests__` (6 fichiers, 127 tests), `pnpm check` verts.

**Reste** : `.claude/skills/mantine-style/SKILL.md` (STY10.1) nomme encore `screenshot-gallery.mjs`
; les références `-darwin` ne sont pas régénérées (poste local) ; `test-results/` n'est pas exclu de
`biome.jsonc` (le rapport JSON y passe sans erreur aujourd'hui).

### Plan 20 — Exécution multi-agent — 29/10

Nouvelle page `sites/plans/pages/20-multi-agents.md`, écrite par le coordinateur d'après la journée
: ce que l'outillage permet (forks, agents frais, Explore, reprise après coupure, `Workflow`,
skills), les limites mesurées (budget par fenêtre de cinq heures, un seul conteneur e2e, verrou
Cargo, `dist/` vidé par `tsdown`, lockfile, processeur, fichiers partagés, navigateur de
l'utilisateur, processus orphelins, contexte du coordinateur), les rôles et le protocole (porte
entre vagues, brief type en dix éléments, reprise), le découpage le plus parallèle des vagues
restantes du plan 19 (vingt-trois agents, jamais plus de cinq à la fois dont trois lourds ; STY0
redécoupé en STY0a à STY0d dès la vague 1, STY5 et STY6.3 en deux), le budget et ce que
l'utilisateur voit. Le plan 21 (tests et Storybook) est en cours d'écriture par un agent.

### Plan 21 — Tests et Storybook — 29/10

Nouveau plan `sites/plans/pages/21-tests.md` : Storybook comme spécification exécutable de
l'interface — une story par composant exporté, un `play` par comportement, l'accessibilité et une
capture en clair et en sombre par story — exécuté par l'addon Vitest de Storybook dans Chromium
(Playwright déjà présent ; `test-runner` écarté, stories portables en complément). Inventaire de
l'existant (1 591 tests, 24 `.test.tsx` de rendu sans helper partagé, 16 stories, 443 références
e2e, goldens motion), structure de Storybook (titres par surface, décorateurs `withMantine`,
`withSite`, `withDeck`, `withFilm`, harnais des modules virtuels du framework), catalogue des
stories par composant, conventions (rôles et noms, pas de snapshot de DOM, `data-testid` listé, la
loi du plan 19 pour les stories), compteur `check-stories.js` et cliquet de couverture, câblage CI.

Sept lots TST (TST-A filet comportemental e2e, TST0 socle, TST1 à TST4 stories par surface, TST5
clôture), dix agents, imbriqués dans les vagues 2 à 6 du découpage de `20-multi-agents.md` §4 sans
allonger le plan 19. Pas d'implémentation avant la demande de l'utilisateur ; questions ouvertes en
§10 (projet `storybook` dans `pnpm test`, sort des `.test.tsx` de rendu, captures par story).

### STY0d — Le compteur de la règle — 29/10

STY0.5 du plan 19, en mode avertissement. Fichiers : `scripts/check-style-rule.js` (nouveau, `.js` :
`useImportExtensions` refuse `.mjs`), `scripts/style-rule-baseline.json` (généré),
`scripts/style-rule-allowlist.json`, `scripts/__tests__/check-style-rule.spec.ts`, la ligne
`check:other` de `package.json`. Aucune dépendance : le script lit l'AST avec `typescript`, déjà
dépendance racine, ce qui donne les formes tolérées de §8.1-1 (`classes.x`, condition entre deux
`classes.*`, `className` relayé) sans regex fragile. Aucune commande git.

**Ce qu'il compte** (§8.1, les sept signaux, par dossier) : (1) un `className` dont l'attribut
contient une chaîne ou un gabarit, directement ou dans l'expression (`cn("…", x)` compte, pas
`cn(classes.a, className)`) ; (2) `style={…}` et `style:` dans les props d'un `createElement`, hors
fichiers de la liste d'exclusion, dont le compte est affiché à part ; (3) les props de style Mantine
de §8.1-3, sur un composant seulement (`<svg opacity>` est du SVG) ; (4) les imports de `clsx`,
`tailwind-merge`, `class-variance-authority`, `tailwindcss` et `@tailwindcss/*`, `cmdk`, `vaul`,
`next-themes`, `@radix-ui/*`, `@base-ui/*`, y compris `import type`, `export … from` et `import()` ;
(5) tout bloc d'un `.css` hors `.module.css`, `packages/theme` et `sites/*/styles.css` ; (6)
`@apply`, `@theme`, `@custom-variant`, `@source`, `@plugin`, `@utility` et la variante `dark:` (pas
`--dark:` ni `.dark:is()`) dans tout `.css` ; (7) un `.tsx` avec un `className` et sans import de
module. Racines : `packages/`, `sites/*/src`, `sites/storybook/.storybook`, `examples/*/components`
pour les scripts ; `packages/`, `sites/*` (et `.storybook`, que le glob n'entre pas seul),
`examples/*/components` pour les feuilles ; exclus tests, stories, `__tests__`, `fixtures`, `dist`,
`node_modules`, `storybook-static`. Modes : défaut ou `--baseline` (échec sur une hausse par signal
et dossier, avec un rappel de `--write-baseline` quand un compte a baissé), `--strict` (échec sur
tout compte, STY9), `--write-baseline [dossier]` (ne régénère que les dossiers donnés, le reste est
gardé), un argument dossier restreint le relevé et la comparaison. Liste d'exclusion : `engine.tsx`,
`player.tsx`, `ui/render.tsx` de motion-blocks, `RemotionAnim.tsx`, `useCssVariable.ts` (créé par
STY0a) et `ThemeVars.tsx` marqué `"pending": "STY7"` ; une entrée sans `pending` dont le fichier
manque fait échouer le contrôle.

**Baseline** (1 217 occurrences, 34 dossiers, 589 fichiers lus, régénération identique au bit près)
: `packages/site` 816 (className 509, style 20, imports 62, css 75, directives 41, sans module 109)
; autres paquets hors motion 293 (className 187 : deck 83, components 47, site-stories 32, site-kit
25 ; style 31 ; css 40 dans `site-stories.css` ; imports 2 dans `cli/bundle`) ; motion-blocks et
motion-render 59 `style` comptés, plus 70 dans les quatre fichiers de la liste d'exclusion ; sites
40 (className 27 dont `www` 22) ; exemples 9. Aucune prop de style Mantine. Écarts avec §1 : les 947
`className` du framework comptaient chaque occurrence du mot (956 aujourd'hui, dont les props
déstructurées et les relais `cn(className)`) ; le signal 1 ne compte que les attributs porteurs
d'une chaîne, 509 (§1 : 288 littéraux + 219 `cn(`). Les 160 des paquets et les 60 des sites et
exemples se retrouvent (187 et 59 avec `site-kit`), §1 ne comptant que `className="`. Les 118
`style` de motion-blocks sont 58 dans `blocks/` plus 69 dans les fichiers exclus (64 `style:` dans
`render.tsx`, 3 dans `player.tsx`, 2 dans `engine.tsx`).

**Vérifications** :

```
$ node scripts/check-style-rule.js
Checking the style rule (sites/plans/pages/19-style.md §0.3) on 589 files...

directory                                      className  style  props  imports  css  directives  no-module  total
examples/deck-mdx/components                           3      0      0        0    0           0          1      4
examples/deck-showcase/components                      4      0      0        0    0           0          1      5
packages/cli/src/bundle                                0      0      0        2    0           0          0      2
packages/cli/src/motion                                0     10      0        0    0           0          0     10
packages/components/src/mdx                            9      1      0        0    0           0          1     11
packages/components/src/slides                        38      2      0        0    0           0          9     49
packages/formats/deck/src/deck                        50      9      0        0    0           0          4     63
packages/formats/deck/src/mdx                          1      1      0        0    0           0          1      3
packages/formats/deck/src/runtime                      1      0      0        0    0           0          1      2
packages/formats/deck/src/slides                       2      3      0        0    0           0          1      6
packages/formats/deck/src/tools                       29      1      0        0    0           0          4     34
packages/formats/motion-blocks/src/blocks              0     58      0        0    0           0          0     58
packages/formats/motion-render/src                     0      1      0        0    0           0          0      1
packages/integrations/site-stories/src                26      4      0        0   40           0          5     75
packages/integrations/site-stories/src/motion          6      0      0        0    0           0          1      7
packages/site/src/app                                  1      0      0        0   75          41          1    118
packages/site/src/lib                                  1      0      0        0    0           0          1      2
packages/site/src/lib/assets/language-icons           16      0      0        0    0           0         11     27
packages/site/src/lib/components                     149     11      0        8    0           0         29    197
packages/site/src/lib/components/navigation           57      3      0        3    0           0         10     73
packages/site/src/lib/errors                           4      0      0        0    0           0          2      6
packages/site/src/lib/hooks                            0      0      0        1    0           0          0      1
packages/site/src/lib/plugins/markdown                24      0      0        0    0           0          1     25
packages/site/src/lib/plugins/search-pagefind         19      0      0        2    0           0          3     24
packages/site/src/lib/ui                             234      6      0       45    0           0         50    335
packages/site/src/lib/util                             4      0      0        2    0           0          1      7
packages/site/src/vite                                 0      0      0        1    0           0          0      1
packages/theme/src                                     0      0      0        0    0           2          0      2
packages/tools/site-kit/src/components                25      0      0        0    0           0          4     29
sites/components/src                                   1      0      0        0    1           0          1      3
sites/docs/src                                         1      0      0        0    0           0          1      2
sites/motion/src                                       1      0      0        0    0           0          1      2
sites/storybook/.storybook                             2      2      0        1    1           1          1      8
sites/www/src                                         22      0      0        0    0           0          3     25
total                                                730    112      0       65  117          44        149   1217
(70 style in the files of scripts/style-rule-allowlist.json, not counted)

✅ No rise over scripts/style-rule-baseline.json (warning mode, 1217 remaining; STY9 switches to --strict).

$ pnpm exec vitest run scripts/__tests__/check-style-rule.spec.ts
 Test Files  1 passed (1)
      Tests  65 passed (65)

$ pnpm check
biome lint : 943 fichiers, 31 avertissements, aucune erreur
oxfmt --check : « sites/plans/pages/19-style.md » non formaté (fichier hors lot, modifié avant la
session, laissé tel quel) ; la commande s'arrête là
$ pnpm check:imports && pnpm check:other
check:imports vert ; check:other vert, dont check-style-rule.js (« No rise … 1217 remaining »)
```

**Reste** : chaque lot qui fait baisser un compte lance
`node scripts/check-style-rule.js --write-baseline <dossier>` en fin de lot pour que le gel descende
avec lui ; STY7 ajuste le chemin de `ThemeVars` dans la liste d'exclusion ; STY9 passe `check:other`
à `--strict`.

### STY0c — Un module dans un deck par esbuild — 29/10

STY0.4 du plan 19 (vague 1). Un `*.module.css` importé par un composant utilisateur d'un deck arrive
dans la feuille du deck, sans dépendance nouvelle : esbuild 0.28.2 charge `.module.css` nativement
(loader `local-css`) et émet le CSS dans `outputFiles` à côté de `main.js`. Aucune commande git.

**Fichiers.** `packages/cli/src/bundle/js.ts` : `JsBundle.css`, le CSS des feuilles émises
(`main.css` puis celles des chunks, ordre stable), retirées de `files` (elles y écrasaient
`assets/main.css` sans être liées par le HTML) ; les modules entrent dans `dependencies` par le
metafile, donc dans la veille du dev. `packages/cli/src/bundle/css.ts` : option `modulesCss` (chaîne
ou promesse), insérée **dans l'entrée Tailwind** entre les feuilles du thème et l'`@import` de
`customCss` (Tailwind v4 garde une règle écrite avant un `@import` tardif, sondé) ; polices et KaTeX
sont compilés avant d'attendre la promesse. `packages/cli/src/render/build-deck.ts` : `bundleJs` et
`bundleCss` restent en parallèle, `modulesCss: jsBundle.then((b) => b.css)`.
`examples/deck-mdx/components/Counter.tsx` + `Counter.module.css` (le `Counter` sorti de
`index.tsx`, `className={classes.root}`, les mêmes valeurs que les utilitaires d'avant :
`var(--radius-xl)`, `hsl(var(--primary))`, `0.75rem 1.5rem`… ; `Timeline` inchangé, pour STY8) et
`css-modules.d.ts` (`declare module "*.module.css"`, ce qu'un utilisateur écrit chez lui). Test
`packages/cli/src/__tests__/css-modules.spec.ts` : trois tests (dev, build minifié, HTML autonome)
sur une fixture écrite dans un dossier temporaire, avec `composes: slide-card from global` et un
`customCss`.

**Ordre dans la feuille** (deck-mdx construit, offsets dans `main.css`) : utilitaires Tailwind 12
751 < tokens 23 842 < `deck.css` 29 111 < module 38 245 < `customCss` (testé). Un module gagne sur
`deck.css` et sur les utilitaires, `customCss` gagne sur tout : l'ordre de §2.3 par la source, sans
`@layer` (STY6.1 posera les couches).

**Noms que donne esbuild** (sondé) : `<Fichier>_<classe>` sans hachage (`Counter_root`), suffixe
numérique si deux fichiers portent le même nom (`a/Button.module.css` → `Button_root`,
`b/Button.module.css` → `Button_root2`) ; `composes: a` → `"Counter_root Counter_big"`,
`composes: slide-card from global` → `"slide-card Counter_card"`, `:global(.x)` respecté. Pas
d'option `localIdentName` : avec `minify: true` les noms locaux sont **minifiés en une lettre**
(`.r`, `{root:"r"}`), les mêmes dans la table JS et dans la feuille, et `minify: true` l'emporte sur
`minifyIdentifiers: false`. Sans minification des identifiants (`minifyWhitespace` + `minifySyntax`
seulement), `Counter_root` survit en build mais `main.js` passe de 288 557 à 572 672 octets (gzip 90
025 → 120 439, +34 %) : refusé, le deck garde `minify` entier. Donc : noms lisibles en dev, une
lettre en build, comme §5.3 l'admettait (le deck n'est pas prérendu, aucune capture n'en dépend).
Collision théorique d'une lettre avec une classe globale : aucune classe d'une lettre dans
`deck.css`, Tailwind ni KaTeX.

**Coût** : `main.js` +50 octets (la table), `main.css` +119 (la règle) par rapport à la construction
précédente de deck-mdx (288 507 / 40 630) ; `recit build` de deck-mdx en 11 s (Shiki compris),
inchangé.

**Vérification** :

```
pnpm exec vitest run packages/cli → Test Files 6 passed (6), Tests 108 passed (108), Type Errors no errors
pnpm -F @recit/cli typecheck → vert ; pnpm -F recit-example-deck-mdx typecheck → vert
pnpm exec nx run @recit/cli:build → 12 tâches, 45 s, vert
cd examples/deck-mdx && pnpm exec recit build slides.mdx → ✅ Built 4 slides
grep -o '{root:"[A-Za-z0-9_-]*"}' dist/assets/main.js → {root:"r"}
grep -o '\.r{[^}]*}' dist/assets/main.css → .r{border-radius:var(--radius-xl);background-color:hsl(var(--primary));color:hsl(var(--primary-foreground));border:0;padding:.75rem 1.5rem;font-size:1.5rem;font-weight:700;line-height:2rem}
recit export slides.mdx --html -o … → la même règle dans le <style> et la même table dans le <script> du HTML (779 Ko)
node scripts/check-style-rule.js examples/deck-mdx/components → className 3, no-module 1, total 4 (5 avant : le Counter migré), pas de hausse
pnpm check → biome vert (31 avertissements, aucun dans mes fichiers), check:imports vert ; oxfmt --check échoue sur trois fichiers d'autres lots (scripts/build-css-modules.js, scripts/__tests__/build-css-modules.spec.ts, 19-style.md) et check:other sur le compteur de style de packages/components/src/mdx (STY0b) ; mes huit fichiers passent oxfmt et biome
```

Deux échecs transitoires du build du deck pendant la session (`@recit/core/dist/index.js` puis
`@recit/core/browser` introuvables) : les `dist` de `core` et `deck` étaient en cours de
reconstruction par un autre lot ; vert au passage suivant.

**À savoir pour STY6.1** : garder `JsBundle.css` → `CssBundleOptions.modulesCss` (promesse) et
placer ce CSS entre les feuilles des paquets et `customCss`, dans `@layer recit` quand les couches
arrivent. Les modules utilisateurs n'ont **pas de PostCSS** : pas de `rem()`, pas de mixin, pas de
`postcss-preset-mantine` ; CSS brut, tokens `@recit/theme` (ils suivent déjà `.dark`, donc
`hsl(var(--primary))` suffit ; `light-dark()` supposerait `color-scheme` posé par le deck, à
vérifier en STY6.3). esbuild applique les loaders `dataurl` de `js.ts` aux `url()` d'un module
(`.png`, `.svg`, `.woff2`… ; une autre extension est une erreur de build). Un module importé par un
chunk différé irait dans `chunks/*.css`, collecté aussi (le JS ne charge jamais une feuille). Les
composants de `@recit/deck` et `@recit/components` arrivent par `sharedPackagesPlugin` déjà compilés
(STY0.3, `dist/styles.css`) : leurs modules ne passent pas par ce chemin. **Pour STY8** :
`examples/*/components` écrit du CSS sans PostCSS, un `css-modules.d.ts` par exemple (ou partagé) ;
`Timeline` de deck-mdx et `Metric` de deck-showcase restent à migrer ; `useCssVariable` vient de
`@recit/components` dans un deck (jamais de `@recit/site`). Le skill `mantine-style` (STY10.1)
devrait dire que `rem()` et les mixins ne valent pas dans `examples/*/components`.

### STY0b — Modules dans un paquet publié — 29/10

Spike de STY0.3 (plan 19 §2.2) : comment un `*.module.css` vit dans un paquet publié. Aucune
commande git ; `pnpm install` une fois, après `pgrep -f "pnpm install"` vide.

**Décision** (écrite dans `19-style.md` §2.2, paragraphe « Décision (STY0b, 29/10) ») : Lightning
CSS, le compilateur que Vite 8 embarque déjà, en `devDependency` du paquet qui compile ses modules
(`lightningcss: 1.33.0` au catalogue, la version déjà installée ; aucune dépendance d'exécution) ;
ni `postcss-modules` (nouveau paquet et ses dépendances), ni `@tsdown/css` (version à aligner sur
tsdown à chaque bump, et il ne règle pas le dev). `scripts/build-css-modules.js` (en `.js`, règle
`useImportExtensions`) compile chaque `src/**/*.module.css` du paquet avec le motif
`recit-<paquet>-<Fichier>__[local]` (préfixe littéral par fichier, pas de hachage, `--variables`
globales) et écrit : la table de classes **à côté du module**, `Kbd.module.css.ts`
(`const classes = { root: "recit-components-Kbd__root slide-kbd" } as const;` : le
`composes: slide-kbd from global` est joint au nom local, clés triées, déjà au format oxfmt, et
typée par ses littéraux, donc pas de `.d.ts` à part), et la feuille du paquet `dist/styles.css`
(exportée `./styles.css`, `sideEffects: ["**/*.css"]`). Le composant importe la table, pas le CSS :
`import classes from "./Kbd.module.css.js"`. C'est le mécanisme des mêmes noms en dev : sous
`@recit/source`, Vite lit `Kbd.tsx` puis `Kbd.module.css.ts`, un objet ordinaire ; tsdown, esbuild
et webpack voient la même chose dans `dist` ; aucune configuration Vite à aligner (le
`generateScopedName` prévu par le plan n'existe pas avec le transformateur Lightning CSS de Vite,
motif chaîne seulement). Refus explicites : deux modules du même nom dans un paquet, un nom de
fichier qui ne peut pas ouvrir une classe, `@import`, `composes` depuis un autre fichier.

**Kbd** : `packages/components/src/mdx/Kbd.tsx` + `Kbd.module.css` (sortis de `builtins.tsx`,
réexportés par `index.ts`, `BUILTIN_MDX_COMPONENTS.Kbd` inchangé, `KbdProps` exporté). Élément nu
`<kbd>`, pas le `Kbd` de Mantine : `@recit/components` ne dépend pas de `@mantine/core`
(`package.json`, §3.3 et §5.5) ; le module compose `slide-kbd` de `deck.css` (contrat public) et
ajoute `white-space: nowrap`. Câblage :
`"build": "node ../../scripts/build-css-modules.js && tsdown"`, le même préfixe pour `typecheck` (la
table doit exister avant `tsgo`) ; `tsdown.config.ts` nettoie `dist/**` sauf `dist/styles.css`.
Storybook : `preview.css` importe `@recit/components/styles.css` après `deck.css`. Test
`scripts/__tests__/build-css-modules.spec.ts` (12 tests sur un paquet fabriqué : préfixe, composes
global et local, tri, variables globales, refus, tables et feuille, déterminisme et mtime inchangé,
`write: false`, doublon de nom).

**Vérifications** : `pnpm exec nx run @recit/components:build` → `NX 3 tasks: 3 succeeded`, `dist/`
contient `styles.css`, `mdx/Kbd.js`, `mdx/Kbd.module.css.js` ;
`pnpm exec vitest run packages/components scripts/__tests__/build-css-modules.spec.ts` →
`Test Files 5 passed (5)`, `Tests 42 passed (42)` ; `pnpm -F @recit/components typecheck` →
`@recit/components: 1 CSS module → dist/styles.css` puis `tsgo` muet ;
`pnpm -F recit-storybook build` → `Storybook build completed successfully`, et
`storybook-static/assets/iframe-B2TOJ11P.css` contient
`.recit-components-Kbd__root{white-space:nowrap}` ; `pnpm exec recit build` n'existe pas à la
racine, la vraie commande est `pnpm exec nx run recit-example-deck-starter:build` (13 tâches, 45 s,
CLI reconstruite avec l'arbre de STY0c) → la classe est **absente** de
`examples/deck-starter/dist/assets/main.css` (`slide-kbd` y est) : `packages/cli/src/bundle/css.ts`
n'importe pas encore `@recit/components/styles.css`, c'est la ligne de STY6.1 (hors des fichiers de
STY0b ; STY0c y traite les modules des composants utilisateur, pas la feuille du paquet). Preuve que
la feuille traverse la chaîne du deck : une copie de `slides.md` dans le scratchpad avec
`recit.config.json` → `theme.customCss` sur `packages/components/dist/styles.css`, construite par
`examples/deck-starter/node_modules/.bin/recit build … -o <scratch>/dist` →
`.recit-components-Kbd__root{white-space:nowrap}` dans son `assets/main.css` (sans `-o`, la CLI
écrit dans `<cwd>/dist` : un `dist/` parasite à la racine a été créé puis supprimé). `pnpm check` →
biome (31 avertissements préexistants), oxfmt (1091 fichiers), imports, admonitions, front matter,
15 version plans, frontières, interfaces, liens : verts ; `check-style-rule.js` (STY0d) d'abord
rouge sur un seul point (`packages/components/src/mdx: tsx without module rose from 1 to 2` : sa
détection `specifier.endsWith(".module.css")` ne voyait pas l'import de la table
`./Kbd.module.css.js`), vert après que STY0d, prévenu par le coordinateur, a pris
`/\.module\.css(\.js)?$/` :
`No rise over scripts/style-rule-baseline.json (warning mode, 1217 remaining)`.

**Pour STY6.1** : câbler `@recit/deck`, `@recit/motion-blocks` et `@recit/site-stories` de la même
façon (script, `build`, `typecheck`, `exports["./styles.css"]`, `sideEffects`, `clean` de tsdown) et
importer les feuilles dans `packages/cli/src/bundle/css.ts`, `site-stories/scripts/build-css.mjs` et
l'entrée Remotion ; décider du sort des tables générées dans `src/` (à committer comme un `.d.ts`,
ou `.gitignore` et régénération par `build`/`typecheck` : aujourd'hui `Kbd.module.css.ts` est non
suivi) ; un mode `--watch` du script pour le dev ; pas de `rem()` ni de mixin
`postcss-preset-mantine` dans les paquets de format (pas de Mantine, Lightning CSS ne les connaît
pas) ; la feuille hors `@layer recit` tant que `deck.css` n'est pas en couche `theme` (STY9) ;
`publint` à passer sur l'export `./styles.css`. **Pour STY6.2** : le relais d'un `className`
d'auteur sur un élément nu (`[classes.root, className].join(" ")` dans `Kbd.tsx`, cas que la loi
§0.3 ne couvre pas : à trancher, Mantine fusionne `className` et `classNames.root` lui-même).

### STY0a — Socle du site — 29/10

Lot STY0 réduit par le coordinateur à STY0.1, STY0.2 et STY0.6 (STY0b, STY0c et STY0d en parallèle).
Aucune commande git.

**En place.**

- `@mantine/core` et `@mantine/hooks` 9.7.4, `postcss-preset-mantine` 1.18.0, `postcss-simple-vars`
  7.0.1 : catalogue de `pnpm-workspace.yaml`, dépendances de `packages/site` (le Vite de chaque site
  les exécute).
- `packages/site/src/vite/plugin.ts` : plugin `recit-style`, `css.postcss` (preset Mantine et
  simple-vars avec les cinq `$mantine-breakpoint-*`) et `css.modules.generateScopedName` exporté :
  `Spinner__root` dans le framework et les sites, `recit-<paquet>-<fichier>__<classe>` sous
  `packages/*/src` (`recit-components-Kbd__root`, `recit-deck-SlideFrame__root`,
  `recit-site-kit-Cards__root`) ; même fonction en dev et en build ; deux modules du framework ne
  doivent pas partager un nom de fichier.
- Couches : `packages/site/src/app/layers.css`
  (`@layer mantine, theme, base, components, utilities;`) et `@mantine/core/styles.layer.css`
  importés en tête de `entry.client.tsx` et `entry.server.tsx`, **avant** `virtual:recit-config` :
  la feuille de `site-stories` importée par le `recit.config` d'un site précède `main.tsx` dans le
  bundle et fixait l'ordre des couches, et une déclaration dans `main.css` ne survit pas à Tailwind.
  Construit : `@layer mantine{` à l'octet 0, puis `properties`, `theme`, `base`, `components`,
  `utilities`.
- `SiteRoot.tsx` : `MantineProvider theme={recitTheme} defaultColorScheme="light"` sous le
  `ThemeProvider` de next-themes ; le script de Mantine (`ColorSchemeScript` appelé comme fonction,
  son `__html`) posé par `useHead`, donc `<script data-mantine-script>` dans le `<head>` prérendu et
  en dev.
- `lib/theme/theme.ts` (`recitTheme = createTheme(…)`) : polices sur `var(--font-sans)` et
  `var(--font-mono)`, `lineHeights.md: "1.5"` (piège ci-dessous) ; `lib/theme/index.ts`.
- `Spinner.module.css`, premier module (`animate-spin` reporté à l'identique) ;
  `lib/hooks/useCssVariable.ts`, son test, l'export `@recit/site/hooks`.

**Piège.** La baseline de Mantine déclare `body { line-height: var(--mantine-line-height) }` (1.55)
; le site hérite 1.5 de `html` (preflight Tailwind) sans rien déclarer sur `body`, et une valeur
héritée perd contre toute valeur déclarée, même depuis la couche la plus basse. Premier passage e2e
: 64 échecs, chaque page 1 px plus haute (`body` à 24,8 px). Sonde Playwright sur le `dist` :
`line-height: inherit` sur `body`, ou la couche `mantine` retirée, rend la hauteur d'origine. Parade
dans le thème, pas dans le CSS global. Les autres propriétés de la baseline sont neutres :
`font-family` par les variables du thème, `font-size` 1rem = racine, `color`, `background-color` et
`antialiased` déjà déclarés sur `body` par `main.css` (couche `base`, qui gagne),
`color-scheme: light` sans effet mesuré.

**Vérifié.**

- `pnpm -F @recit/site typecheck` ; `vitest run packages/site` : 56 fichiers, 596 tests ; biome et
  oxfmt sur mes fichiers ; `pnpm check` vert (compteur de STY0d : 1217, aucune hausse).
- Build de `docs` : `Spinner__root` dans le CSS construit (chunk partagé `Alert-*.css`, lié en tête
  de `index.html`) et dans le JS ; absent du HTML prérendu parce que `Layout` ne rend le `Spinner`
  que dans une `Suspense` fallback : le `grep` du plan sur `index.html` n'est pas probant, celui sur
  `dist/assets/*.css` l'est.
- Dev (`recit-site dev --port 4521`, `BROWSER=none`) : script Mantine dans le `<head>`,
  `virtual:ssr-css.css?direct` commence par la déclaration de couches puis `@layer mantine {` et
  contient `Spinner__root` : `css/collect.ts` et `css/plugin.ts` voient les modules sans changement.
- e2e en conteneur, `e2e/site-docs.spec.ts` : 106/106 (`chromium` et `chromium-dark`), galerie vide.

**Mesures (STY0.6, `sites/docs`, gzip).**

| Quoi         | Avant                  | Après                  |
| ------------ | ---------------------- | ---------------------- |
| CSS d'entrée | 43,5 ko (247 ko brut)  | 79,0 ko (496 ko brut)  |
| JS d'entrée  | 139,6 ko (513 ko brut) | 147,7 ko (540 ko brut) |
| JS total     | 3 285 ko               | 3 295 ko               |

`www` avant : CSS 43,4 ko, JS 120,5 ko. La feuille Mantine complète pèse 35,5 ko gzip de plus (41,8
ko seule), au-dessus des ~30 ko attendus ; l'import par composant
(`@mantine/core/styles/Button.layer.css`…) est l'option de STY5 et STY9.

**Reste, pour STY2 et STY6.1.** `colorMode` et le pont `.dark` (STY2) ; `createRecitTheme` remplace
le squelette en gardant `lineHeights.md` ou en déclarant `line-height` sur `body` dans la feuille de
base ; les deux `<style data-mantine-styles>` du provider sont dans `#root` (157 et 815 octets) ; le
serveur de preview écoute sur `localhost` (IPv6), `curl` sur `127.0.0.1` est refusé ; une
reconstruction concurrente des paquets par un autre lot vide leur `dist` un instant et fait échouer
un build ou un `dev` (`@recit/theme`, `@recit/site-stories`), relancer suffit.

### Porte de la vague 1 (plan 19) — 29/10

Coordination. Raccords après les rapports : le compteur accepte l'import d'une table générée
(`*.module.css.js`, STY0b) ; le skill `mantine-style` précise qu'hors de `packages/site` un module
est du CSS brut (Lightning CSS ou esbuild, sans `rem()` ni mixin) ; l'en-tête du plan 21 corrompu
par une édition de sa seconde passe a été réparé et ses trois modifications perdues réappliquées ;
`19-style.md` formaté. Porte : `pnpm check` vert (1 217 au compteur, aucune hausse) ; e2e en
conteneur sur `storybook`, `deck-render`, `site-components`, `site-presentation`, `site-docs` :
274/276, les deux échecs étant `/demos/mdx` en clair et en sombre, où le bloc « Components » affiche
la source de `deck-mdx` sortie dans `Counter.tsx` et son module (STY0c) : différence voulue,
références régénérées par `test:e2e:update:linux e2e/site-presentation.spec.ts`. Vague 2 lancée
(STY2, STY6.1, STY7a, TST-A) ; tableau d'état en tête du §10 du plan 19.

### STY7a — Blocs motion, styles statiques — 29/10

STY7.1 du plan 19 (vague 2 du plan 20), plus la ligne de `Film.tsx`. Fichiers :
`packages/formats/motion-blocks/src/blocks/blocks.module.css` (nouveau) et sa table générée
`blocks.module.css.ts`, `src/ThemeVars.tsx` (nouveau), `src/blocks/frame.tsx` (nouveau : `Frame`,
`Backdrop`), les 8 blocs, `src/theme.tsx` (`useStyles` retiré), `src/styles.ts` (supprimé),
`src/index.ts`, `test/{render,player}.test.tsx`, `test/ThemeVars.test.tsx` (nouveau),
`packages/formats/motion-render/src/Film.tsx`, `scripts/style-rule-allowlist.json` (entrée
`ThemeVars.tsx` : `pending` retiré), `scripts/style-rule-baseline.json` (motion-blocks et
motion-render à zéro). Aucune commande git ; `package.json`, `tsdown.config.ts` et `entry.tsx`
(import de `@recit/motion-blocks/styles.css`) sont de STY6.1, déjà en place dans l'arbre.

**Ce qui change.** `makeStyles(theme)` devient `blocks.module.css` : une classe par ancienne clé
(`frame`, `eyebrow`, `display`, `statement`, `statValue`, `rowLabel`, `mask`) et une par élément que
les blocs stylaient en ligne (`chapterNumber`, `chapterTitle`, `items`, `item`, `itemIndex`,
`statLabel`, `statCaption`, `emphasis`, `rows`, `row`, `rowBars`, `rowStacked`, `barRow`, `track`,
`barBefore`/`barAfter`, `barValueBefore`/`barValueAfter`, `card`, `cardChrome`, `dot`, `cardApp`,
`cardBody`, `cardLine`, `cardSkeleton`, `cardStage`, `cardWrap`, `cardCaption`, `backdrop`). Les
unités `cqw` de l'échelle typographique et `container-type: size` passent tels quels. Les variantes
se lisent sur la racine de scène : `data-block` (le type) et `data-layout` (`16x9`, aussi le 1:1, ou
`9x16`) posés par `Frame` ; le module en tire les paddings par bloc (`12% 8%`, `14% 8%`, `10% 8%`,
`6%`, `7% 5%`), les tailles de police portrait (`clamp(48px, 12vw, 128px)`…) et l'écart des lignes
empilées, donc les composants `Content` n'ont plus de prop de taille. Les pastilles de `ui-mock`
sont `data-dot="close|minimize|zoom"`, les lignes squelette `data-line="0…5"` (90 %, 72 %… 0 %,
au-delà `auto` comme en ligne). Les valeurs du thème que `makeStyles` et les blocs lisaient
(`colors`, `fonts`, `type`, `track`, `data`) deviennent 27 variables `--mb-*` (`--mb-bg`,
`--mb-font-body`, `--mb-type-row-label`, `--mb-data-after-bar`…) écrites par `ThemeVars`
(`themeVariables(theme)`, un seul objet `style` de variables, exception nommée de §0.3, dans la
liste d'exclusion) : `ThemeVars` rend l'élément demandé (`as`, `div` par défaut, ou `AbsoluteFill`)
avec la classe et les `data-*` de l'appelant, et lit le thème du `ThemeProvider`. `Frame` =
`ThemeVars` + `classes.frame` : la racine de chaque bloc pose les variables elle-même, donc un bloc
rendu seul (tests, vignettes, `StaticAnim`) a son thème ; `Backdrop` = `ThemeVars` +
`classes.backdrop` (fond du film) remplace `<AbsoluteFill style={{ background }}>` dans `Film.tsx`
(`<Backdrop as={AbsoluteFill}>`, même DOM, Remotion fusionne `style` et relaie `className`). Les
blocs n'ont plus que `className` de la table et la couche `A` (`engine.tsx`, `RemotionAnim`,
`ClockAnim`), intouchée ; `AnimProps.style` reste pour STY7b. API publique de `@recit/motion-blocks`
: `makeStyles`, `Styles`, `useStyles` retirés (aucun usage hors du paquet), `ThemeVars`,
`themeVariables`, `ThemeVariables`, `Frame`, `Backdrop` ajoutés ; un seul plan de version pour toute
la migration, écrit par STY9 (§10.2).

**Deux points vus en route.** (1) `compareBars` posait `width: value/max %` en ligne sur la piste
d'une barre : sans effet, la piste est un item `flex: 1` (base 0) qui remplit la ligne quoi qu'il
arrive, la barre étant révélée par `growX` ; retiré (`maxValue` avec), goldens identiques. Si
l'intention d'emaki était des barres proportionnelles, c'est une décision produit, pas ce lot. (2)
`composes` écarté dans ce module : avec le préfixe `recit-motion-blocks-blocks__`, une entrée
composée de la table dépasse 80 colonnes et oxfmt la replie après le deux-points
(`barAfter:\n    "…"`), que le générateur n'écrit pas → `pnpm check` rouge (oxfmt, puis `--check` du
script en désaccord). Les rôles partagés sont des listes de sélecteurs
(`.eyebrow, .chapterNumber, .statLabel, .cardCaption { … }`), un seul nom par classe, table dans les
80 colonnes. **Pour STY6.1** : `tableSource` devrait replier une entrée longue comme oxfmt
(`site-stories` aura le même préfixe long). Lightning CSS normalise aussi les couleurs
(`rgba(0,0,0,0.06)` → `#0000000f`, alpha 15/255 au lieu de 0.06, même dans une `--variable`) : sous
le seuil des goldens, 0,000 % mesuré, mais à savoir.

**Vérifications.**

```
$ pnpm exec vitest run packages/formats/motion-blocks packages/formats/motion-render
 Test Files  7 passed (7)
      Tests  49 passed (49)
$ pnpm exec nx run-many -t build typecheck -p @recit/motion-blocks @recit/motion-render
 NX   7 tasks: 7 succeeded
$ node scripts/check-style-rule.js packages/formats/motion-blocks packages/formats/motion-render
directory  className  style  props  imports  css  directives  no-module  total
total              0      0      0        0    0           0          0      0
(71 style in the files of scripts/style-rule-allowlist.json, not counted)
$ node scripts/check-style-rule.js --write-baseline packages/formats/motion-blocks/src packages/formats/motion-render/src
✅ scripts/style-rule-baseline.json written: 1146 in 32 directories.
$ pnpm check
✅ No rise over scripts/style-rule-baseline.json (warning mode, 1145 remaining; STY9 switches to --strict).
✅ @recit/motion-blocks: 1 CSS module, tables up to date
```

Compteur : `packages/formats/motion-blocks/src/blocks` 58 `style` → 0, `motion-render/src` 1 → 0,
liste d'exclusion 70 → 71 (`ThemeVars.tsx`) ; 1 217 → 1 146 dans la baseline. Le premier `className`
compté venait de `tone === "before" ? …` (le littéral de la condition compte, §8.1-1) : remplacé par
un booléen `after`. Goldens (`pnpm -F @recit/motion-render goldens:check`, 16 scènes × 2 sources,
`warm` 16:9 et `saas` 9:16) :

```
  ✓ warm-title — 0.000% changed
  … (les 32 vues : warm et saas, title, statement, chapter, list, stat, compare, mock, scene, depuis le film.json et depuis le .story.md)
  ✓ saas-scene (md) — 0.000% changed
✓ all goldens match
```

Un premier passage s'est arrêté sur `@recit/core` sans `dist` (reconstruction concurrente d'un autre
lot, comme noté en STY0a) : relancé. e2e non lancés ici (conteneur occupé par un autre lot) ; à la
porte de vague, **`motion-dev` et `site-motion` ne verront la feuille que si elle est chargée par
leurs pages** : `packages/cli/src/commands/motion-dev.ts` (`motionPage`, un `<link>` ou la feuille
en ligne ; `bundleMotionClient` ne garde que le JS d'esbuild) et
`packages/integrations/site-stories/scripts/build-css.mjs` (`@import` de
`@recit/motion-blocks/styles.css`, préfixée `.recit-wrapper`), fichiers de STY6.3b et STY6.1. Sans
cela les blocs y sont nus. Rien d'autre à accepter dans la galerie : aucune capture de ce lot ne
doit changer.

**Pour STY7b.** `render.tsx` (64 `style:`) : constant → `blocks.module.css` (ou un module à côté,
même générateur), par temps → couche ; `player.tsx` : le `MotionFrame` (fond `theme.colors.bg`,
dimensions natives, `transform: scale`) passe par `Backdrop` et un module, l'échelle par variable ou
calcul CSS ; `AnimProps.style` et `StaticAnim`/`FramerAnim`/`RemotionAnim` restent la couche ;
`ThemeVars` est le seul endroit où le thème devient du `style` ; la caption de `UiSceneView`
(`fontFamily: theme.fonts.mono`…) peut lire `var(--mb-font-mono)` dès maintenant, les variables sont
posées par `Frame` au-dessus d'elle. Puis `goldens:check` sans régénération et les e2e motion.

### STY6.1 — Outillage des paquets publiés — 29/10

Vague 2 du plan 19 (§10, STY6.1 ; `20-multi-agents.md` §4), en parallèle de STY2, STY7a et TST-A.
Aucune commande git ; un `pnpm install` après `pgrep` vide (lockfile : `lightningcss` dans
`@recit/deck`, `@recit/motion-blocks`, `@recit/site-stories` ; `@recit/deck`,
`postcss-preset-mantine`, `postcss-simple-vars` dans `sites/storybook`).

**Correction au plan pour cette vague.** Les paquets portent encore leurs classes Tailwind jusqu'à
STY6.2 et STY6.3 (vagues 3 et 4) : la CLI garde `@import "tailwindcss"` et ses `@source`,
`@recit/theme/tailwind.css` et son peer restent, Storybook garde `@tailwindcss/vite` ; ce lot
**ajoute** les feuilles des modules à chaque consommateur, modules et utilitaires coexistent, et les
captures `deck-render` restent identiques. STY5c retire Tailwind. `tailwind.css` le dit en tête ;
`packages/theme/package.json` n'est pas touché.

**Script généralisé** (`scripts/build-css-modules.js`, 23 tests dans
`scripts/__tests__/build-css-modules.spec.ts`) : `PACKAGES` (les quatre paquets), `--all`, `--check`
(table absente, périmée ou orpheline, feuille `dist/` construite et périmée ; une feuille pas
construite n'est pas une faute, `pnpm check` précède `pnpm build` en CI), `--watch` (`fs.watch`
récursif sur `src/`, recompile à la volée), et `recit.cssModulesSheet` dans le `package.json` d'un
paquet dont `./styles.css` est une autre feuille. Même motif `recit-<paquet>-<Fichier>__<local>`,
même table typée à côté du module.

**Piège de format des tables** (STY7a) : une entrée `composes` longue dépasse 80 colonnes et oxfmt
la replie après les deux-points. `tableEntry` reproduit la règle de Prettier, sondée sur oxfmt 0.74
: une entrée `  clé: "valeur",` de plus de 80 colonnes (virgule comprise) passe sur deux lignes
(`clé:` puis la chaîne indentée de quatre), **sauf** quand la clé fait moins de cinq colonnes
(`root`, `mask`, `body` : jamais repliées, quelle que soit la longueur) ; une clé citée compte ses
guillemets. Test unitaire aux bornes 80/81 ; `blocks.module.css.ts` (36 classes) et
`Kbd.module.css.ts` passent `oxfmt --check`.

**Politique des tables générées** (écrite dans `19-style.md` §2.2, « Tables générées (STY6.1) ») :
**committées** avec leurs modules, régénérées par `build` et `typecheck`
(`node ../../../scripts/build-css-modules.js && tsdown` / `&& tsgo --noEmit`), vérifiées par
`node ./scripts/build-css-modules.js --check --all` ajouté à `check:other` ; l'utilisateur les
committe avec le lot qui les change (aujourd'hui `packages/components/src/mdx/Kbd.module.css.ts` et
`packages/formats/motion-blocks/src/blocks/blocks.module.css.ts` de STY7a, non suivis).

**Paquets** : `@recit/deck` et `@recit/motion-blocks` reçoivent `exports["./styles.css"]`,
`sideEffects: ["**/*.css"]`, le script devant `tsdown` et `tsgo`,
`clean: ["dist/**", "!dist/styles.css"]`. `@recit/site-stories` : son `./styles.css` est la feuille
d'intégration, donc ses propres modules vont dans `dist/modules.css`
(`"recit": { "cssModulesSheet" }`), que `scripts/build-css.mjs` importe après
`@recit/components/styles.css`, `@recit/deck/styles.css` et `@recit/motion-blocks/styles.css` (le
`MotionViewer` est dans `.recit-wrapper`, donc sa feuille aussi), tous passés par la passe de
préfixage `.recit-wrapper` ; `tsdown` garde `modules.css` et `styles.css` ; la construction accepte
zéro module (feuille réduite à son en-tête) comme plusieurs (STY6.3b). `publint` : 12 paquets verts.

**CLI** (`packages/cli/src/bundle/css.ts`) : `@recit/components/styles.css` et
`@recit/deck/styles.css` importés dans l'entrée Tailwind après les feuilles du thème, avant les
`@source`, le CSS des modules utilisateur (`modulesCss`, STY0c) et `customCss` ; ordre prouvé par un
test ajouté à `css-modules.spec.ts` (`.slide-card` < `.recit-components-Kbd__root` < `.Counter_root`
< `.lorem-custom` ; Tailwind retire les commentaires d'en-tête des feuilles, la règle de `Kbd` sert
de repère). `js.ts`, `build-deck.ts` et `packages/cli/package.json` n'ont pas eu besoin de changer
(`modulesCss` en promesse, dépendances déjà déclarées). Un deck ne bundle jamais
`@recit/motion-blocks` (`SHARED_PACKAGES` de `resolve-plugin.ts`, aucun import dans deck ni
components) : sa feuille n'entre pas dans le deck. Le lecteur de `recit dev` d'une story motion
(`packages/cli/src/commands/motion-dev.ts`, accordé par le coordinateur après le rapport de STY7a,
STY6.3b le reprend) n'avait **aucune feuille** (un `<style>` en ligne) : `bundleMotionClient` rend
maintenant `{ js, css }` (`outdir` + `entryNames: "client"`, le CSS =
`@recit/motion-blocks/styles.css` lu par `cliRequire` suivi des `.css` qu'esbuild émettrait pour les
sources du lecteur, aucun aujourd'hui), la page pose
`<link rel="stylesheet" href="/__motion/styles.css">` et le serveur sert cette route en `text/css` ;
test ajouté à `motion.spec.ts` (page, route, type, en-tête de la feuille) ; sondé sur
`hello.story.md` : 57 règles `recit-motion-blocks-blocks__*` servies.

**Storybook** : `preview.css` importe `@recit/deck/styles.css` après celle des composants ;
`main.ts` ajoute dans `viteFinal` le PostCSS de `recit-style` (`postcss-preset-mantine`,
`postcss-simple-vars` avec les cinq `$mantine-breakpoint-*`, valeurs dupliquées de `plugin.ts`
puisque Storybook ne charge pas `@recit/site`) et
`css.modules.generateScopedName: "[name]__[local]"` ; le décorateur `withSlide` perd ses deux
`style` et ses deux chaînes : `preview.module.css` (`.frame`, `.frameFit`, `.slide` et `.slideFit`
qui composent `slide from global`, `.body` qui compose `slide-body`), classes construites
`preview-module__frame`, `preview-module__slideFit{position:relative}` après `deck.css` dans
`iframe-*.css`. Compteur : `sites/storybook/.storybook` 8 → 3 (imports 1, css 1, directives 1 :
Tailwind, jusqu'à STY5c), baseline abaissée pour ce dossier.

**Remotion** : `packages/formats/motion-render/src/entry.tsx` importe
`@recit/motion-blocks/styles.css` à côté de `fonts.css` (webpack de Remotion, même chemin que les
polices).

**Vérifications** :

```
$ pnpm exec nx run-many -t build typecheck -p @recit/components @recit/deck @recit/motion-blocks @recit/site-stories @recit/cli recit-storybook
NX   Running targets build, typecheck for 6 projects and 8 tasks they depend on failed
Failed tasks: - @recit/site-stories:typecheck
  ../../site/src/vite/plugin-theme.ts(90,7): error TS6133: 'processFont' is declared but its value is never read.
  (fichier de STY2 en cours, importé par @recit/site/vite ; les 18 autres tâches vertes, dont les six builds)
$ pnpm exec vitest run scripts/__tests__ packages/cli packages/components
Test Files 18 passed (18), Tests 354 passed (354), Type Errors no errors
  (un passage intermédiaire : 2 échecs transitoires pendant que STY7a écrivait blocks.module.css et
  que son compte className montait dans packages/formats/motion-blocks/src/blocks ; vert au passage suivant)
$ node scripts/build-css-modules.js --check --all
✅ @recit/components: 1 CSS module, tables up to date
✅ @recit/deck: 0 CSS modules, tables up to date
✅ @recit/motion-blocks: 1 CSS module, tables up to date
✅ @recit/site-stories: 0 CSS modules, tables up to date
$ pnpm check
biome, oxfmt, imports, admonitions, front matter, version plans, frontières, interfaces, liens : verts
check-style-rule.js : ✅ No rise over scripts/style-rule-baseline.json (warning mode, 1204 remaining)
build-css-modules.js --check --all : les quatre ✅ ci-dessus
$ pnpm publint
✅ 12 published packages pass publint.
$ pnpm exec nx run recit-example-deck-starter:build   (14 tâches, 50 s)
$ grep -o '\.recit-components-Kbd__root{[^}]*}' examples/deck-starter/dist/assets/main.css
.recit-components-Kbd__root{white-space:nowrap}      (offset 38 070, après .slide-kbd à 30 936, 40 432 octets)
$ grep -o '\.preview-module__slideFit{[^}]*}' sites/storybook/storybook-static/assets/iframe-*.css
.preview-module__slideFit{position:relative}
$ pnpm exec vitest run packages/cli/src/__tests__/motion.spec.ts → Tests 26 passed (26) ; pnpm -F @recit/cli typecheck → vert
$ BROWSER=none pnpm test:e2e:linux e2e/deck-render.spec.ts e2e/storybook.spec.ts   (conteneur, 18:07 → 18:10)
[e2e-linux] built 18:09:36
  52 passed (1.1m)
git status e2e/__screenshots__ → vide : captures deck-render et storybook identiques
$ pnpm -F @recit/motion-blocks build && pnpm -F @recit/motion-render goldens:check
@recit/motion-blocks: 1 CSS module → dist/styles.css (36 classes) ; ✓ all goldens match (32 plans, 0.000% changed :
  la feuille des blocs importée par entry.tsx rend les mêmes pixels que les styles en ligne d'avant STY7a)
$ BROWSER=none pnpm test:e2e:linux e2e/motion-dev.spec.ts e2e/site-motion.spec.ts   (conteneur, 18:11 → 18:14, arbre avec les blocs de STY7a sans style en ligne)
[e2e-linux] built 18:14:03
  31 passed (45.4s)
git status e2e/__screenshots__ → vide : captures motion-dev et site-motion identiques
```

Deux passages conteneur avortés avant celui-là : l'un parce qu'un conteneur d'un autre agent
tournait (garde `docker ps --filter ancestor=…playwright…`, mon `pgrep -f e2e-linux` se
reconnaissait lui-même), l'autre sur un `Build failed` de rolldown dans l'arbre copié à 18:02,
absent du passage suivant et du build complet sur l'hôte (état transitoire d'un autre lot ; sur
l'hôte, le seul échec fut `components:build` sur `@recit/core/dist/i18n` vidé par une reconstruction
concurrente, la règle « un seul lot reconstruit un paquet partagé » de `20-multi-agents.md` §2).

**Pour STY6.2** (`@recit/components`) : écrire `X.module.css` à côté de `X.tsx`, importer
`./X.module.css.js`, lancer `pnpm -F @recit/components build` (ou
`node ../../scripts/build-css-modules.js --watch` en dev) ; CSS brut (pas de `rem()`, pas de mixin),
tokens `@recit/theme` ; `composes: slide-x from global` pour les classes de `deck.css` ; les
utilitaires Tailwind retirés d'un composant ne sont plus compilés nulle part dès qu'aucun fichier
scanné ne les cite : vérifier `storybook.spec.ts` et `deck-render`. **Pour STY6.3a-b** : même
câblage déjà en place dans `@recit/deck` et `@recit/site-stories` ; `site-stories.module.css` ira
dans `dist/modules.css` puis dans `styles.css` préfixé ; `motion-dev.ts` sert déjà la feuille des
blocs, le module du client motion (`client.tsx`) arrivera par `bundleMotionClient().css`. **Pour
STY7b** : la feuille des blocs est chargée par Remotion (`entry.tsx`), par `site-stories` (préfixée
`.recit-wrapper`) et par `recit dev` (`/__motion/styles.css`) ; la table `blocks.module.css.ts` est
à committer ; une nouvelle classe longue se replie toute seule.

### TST-A — Le filet comportemental e2e — 29/10

Vague 2 du plan 21 (§8), en parallèle de STY2, STY6.1 et STY7a. Aucune commande git de ma part : le
coordinateur a committé les specs dans le point d'étape 0ac806b9, les corrections ci-dessous sont
dans l'arbre de travail.

**Livré.**

- `e2e/site-behaviour.spec.ts` (nouveau) : `sites/docs` en preview, projets `chromium` et
  `chromium-dark` ; le tiroir en `mobile` (`@mobile`). Ctrl+K ouvre le dialogue « Search » (pressé
  jusqu'à ce que l'hydratation réponde), saisie « tutorial », flèche bas, Entrée mène à l'URL de
  l'option sélectionnée ; le ThemeSwitch bascule le schéma (lu sur `html` : classe `dark`,
  `data-mantine-color-scheme`, `data-theme` ou `color-scheme`) et le choix stocké, un rechargement
  garde les deux ; le sommaire marque la section atteinte, puis plus rien en haut de page ; le
  bouton « Copy code » remplit le presse-papiers (`test.use({ permissions })`) ; la pagination mène
  à « Tutorial » et revient à « Quickstart » (dernier lien du nom dans `main` : la barre latérale et
  le fil d'Ariane ont les leurs) ; le tiroir « Navigation » s'ouvre, « Formats » mène à
  `/formats/overview`, le tiroir est fermé. CodeTabs : `test.fixme`, aucune page d'aucun site ne
  porte `<CodeTabs>` (vérifié dans `sites/`, `examples/`, Storybook) ; la fixture est une décision
  du site docs.
- `e2e/deck-behaviour.spec.ts` (nouveau) : `sites/presentation`, `/present/starter` (trois
  diapositives). Flèches, Espace, Home et End avec `?slide=N` qui suit ; `?slide=2` ouvre à la
  deuxième ; `s` ouvre la console présentateur (`context.waitForEvent("page")`, URL
  `mode=presenter`), « Slide N / 3 » suit l'auditoire et « Next → » le pilote ; `?print=1` rend
  autant de `.print-slide` que de diapositives.
- `e2e/support.ts`, ajouts en fin de fichier : `colorScheme`, `storedSiteTheme` (clé
  `SITE_COLOR_SCHEME_STORAGE_KEY` de STY2, repli sur la clé héritée `theme`), `themeSwitch`, `toc`,
  `activeTocEntry`, `slideAt` (le `group` « N / total » du motif carrousel), `slideTotal`. Les specs
  ne parlent que rôles, noms, URL, `localStorage` et presse-papiers, aucune capture ; la seule
  classe (`text-primary`, voir ci-dessous) vit dans le helper, jamais dans une spec.
- `playwright.config.ts` : `testMatch` du projet `mobile` élargi à `site-(docs|behaviour)` — une
  ligne hors de ma liste de fichiers, sans quoi un test `@mobile` ne tourne dans aucun projet.

**Trouvé, pour le plan 19 (rien corrigé ici).**

1. Sommaire : chaque entrée porte `aria-current="page"` (le `NavLink` du routeur, le chemin
   correspond), l'attribut ne dit donc pas la section ; seule la classe `text-primary` la marque. Et
   à 1280 × 720 l'observateur (`ViewportAnchorContext`, `rootMargin` −80 % en bas, seuil 0,75)
   regarde les 144 px du haut : un titre amené à son `scroll-margin` (124 px, sous un en-tête de 115
   px) n'y entre jamais, la section cliquée dans le sommaire n'est pas marquée ; elle l'est à partir
   de 94 px, donc sous l'en-tête. Mesuré par sonde Playwright (`scrollIntoView` puis `scrollBy`) ; à
   900 px de haut le marquage suit, d'où le `test.use({ viewport })` du test. Attendu :
   `aria-current="location"` sur l'entrée active seule, et une zone qui tient compte de l'en-tête.
2. Thème : STY2 a remplacé la clé `theme` par `recit-color-scheme` pendant le lot ; la constante
   `SITE_COLOR_SCHEME_STORAGE_KEY` est arrivée dans `support.ts` au même moment que la mienne
   (doublon résolu en gardant la sienne). Le `darkStorageState` de la config garde `theme`, que
   `SiteRoot.tsx` copie une fois : le projet `chromium-dark` ouvre toujours en sombre.
3. Protocole : `pgrep -f e2e-linux` se trouve lui-même quand la boucle d'attente contient la chaîne
   (mon garde a tourné 80 min pour rien) ; `pgrep -f 'e2e-linux\.j[s]'` ou `docker ps` filtré sur
   l'image Playwright ne se trompent pas. Un passage local pendant qu'un autre lot reconstruit un
   paquet échoue au démarrage des `webServer` (`@recit/theme/dist/index.js` absent), comme STY0a
   l'avait noté.

**Vérifications.** Deux passages en conteneur,
`BROWSER=none pnpm test:e2e:linux e2e/site-behaviour.spec.ts e2e/deck-behaviour.spec.ts` :

```
[e2e-linux] start 19:18:01 · copied 19:18:03 · installed 19:18:30 · built 19:20:30
  2 skipped
  15 passed (39.4s)
[e2e-linux] tested 19:21:10 · done 19:21:11
[e2e-linux] start 19:21:30 · copied 19:21:31 · installed 19:22:08 · built 19:24:07
  2 skipped
  15 passed (39.1s)
[e2e-linux] tested 19:24:48 · done 19:24:48
```

Les deux ignorés sont le `fixme` CodeTabs en `chromium` et `chromium-dark`.
`pnpm -F recit-e2e typecheck` : `tsgo --noEmit`, sortie 0. `pnpm check` : sortie 0 (compteur de
style 1145, aucune hausse). En local, `pnpm exec playwright test` sur les deux specs : 15 réussis, 2
ignorés, 40 s.

### STY2 — Thème et mode de couleur — 29/10

Lot STY2 du plan 19, vague 2, sur `feature/mantine`. Aucune commande git (point de contrôle
`0ac806b9` posé par le coordinateur à la demande de l'utilisateur).

**En place.**

- `packages/site/src/lib/theme/createRecitTheme.ts` : `createRecitTheme(theme)` construit le thème
  Mantine depuis la section publique `theme:` (polices sur `--font-sans`/`--font-mono`, `headings`,
  `lineHeights.md: "1.5"` gardé de STY0a, `defaultRadius: "md"`, palette `recit` de dix nuances
  depuis `theme.light.primary` quand la couleur se lit : `#hex`, `rgb()`, `hsl()` ou le triplet « H
  S% L% » ; `oklch()` et les noms laissent la palette de Mantine). `recitCssVariablesResolver`
  pointe chaque variable de Mantine vers une variable du site (`--mantine-color-body` →
  `--background`, `text` → `--foreground`, `dimmed`/`placeholder` → `--muted-foreground`, `anchor`
  et `primary-color-filled` → `--primary`, `default-border` → `--border`, `error` → `--destructive`,
  les rayons `xs…xl` sur l'échelle Tailwind de `--radius`), donc aucune valeur propre à un mode :
  les variables du site changent déjà avec lui. `colorScale.ts` : trente lignes, testées ;
  `@mantine/colors-generator` non pris. Instantané du thème de `sites/docs`.
- Mode de couleur : Mantine est la source de vérité. `localStorageColorSchemeManager` sur la clé
  `recit-color-scheme`, `defaultColorScheme` depuis `site.colorMode` (`light`, `dark`, `system` ;
  `system` = `auto` quand rien n'est dit) ; `next-themes` retiré de `SiteRoot` (le paquet reste
  installé jusqu'à STY9, plus rien ne l'importe). Le script posé dans `<head>` par `useHead` fait
  trois choses avant le premier rendu : copie une fois le choix mémorisé par `next-themes` (clé
  `theme`) sous la clé de Mantine, pose `data-mantine-color-scheme` (script de Mantine), puis
  reflète l'attribut sur la classe `.dark` de `<html>`. À l'exécution, `ColorSchemeBridge` tient la
  classe égale au schéma calculé : les vingt lecteurs de `.dark` (§1.4) ne voient aucune différence
  jusqu'à STY9. `useTheme` (`packages/site/src/lib/hooks/useTheme.ts`, exporté par
  `@recit/site/hooks` et `@recit/site/components`) garde la forme de celui de `next-themes` :
  `theme` (`light`/`dark`/`system`), `resolvedTheme`, `setTheme`.
- `plugin-theme.ts` : registre shadcn retiré (`shadcn-registry.ts` supprimé avec ses tests,
  `GOOGLE_FONTS` conservé) ; les variables sombres sont émises sous `DARK_SELECTOR` =
  `.dark, [data-mantine-color-scheme="dark"]`. `SiteConfig.ts` : `theme.registryUrl` refusé avec le
  message « `theme.registryUrl` is gone with shadcn/ui: set `theme.light` and `theme.dark` instead »
  (`superRefine`, clé marquée `@deprecated` jusqu'à 1.0), `site.colorMode` ajouté.
  `defaultTheme.css` inchangé : les défauts restent zinc (question 5, iso-rendu).
- `ThemeSwitch.tsx` + `ThemeSwitch.module.css` : `UnstyledButton` de Mantine, `data-mode` et
  `data-active`, le module reporte les utilitaires d'avant valeur par valeur (p-0.5, gap-0.5,
  border, rounded-full, ring-3) ; `plugins/search-inkeep` lit `data-mantine-color-scheme`.
- Hors de la liste du lot, additifs : `app/main.tsx` passe `theme={config.theme}` et
  `colorMode={config.site?.colorMode}` à `SiteRoot` (seul chemin vers la config côté client) ;
  `Header.test.tsx` et `MobileTopNavigation.test.tsx` enveloppent leur arbre dans
  `<MantineProvider>` (le `ThemeSwitch` lit le schéma de Mantine) ; `e2e/support.ts` : `darkMode()`
  pose aussi `recit-color-scheme=dark` (`SITE_COLOR_SCHEME_STORAGE_KEY`), question 14.

**Vérifié.** `pnpm exec vitest run packages/site` : 58 fichiers, 606 tests (un instantané écrit) ;
`pnpm -F @recit/site typecheck` et `pnpm -F @recit/site-stories typecheck` verts (la variable
inutilisée `processFont` signalée par STY6.1 est retirée) ; `nx run docs:build` vert (chunk
`ThemeSwitch-*.css`, script `data-mantine-script` dans le `<head>` prérendu) ;
`node scripts/check-style-rule.js packages/site` : 816 → 808, baseline abaissée pour
`lib/components` ; `pnpm check` vert ; e2e conteneur `e2e/site-docs.spec.ts` : **106/106**
(`chromium` et `chromium-dark`), **galerie vide**, `ThemeSwitch` compris.

**Pour STY3 et STY4.** Lire le mode : `useTheme().resolvedTheme` depuis `@recit/site/hooks`, ou
`useComputedColorScheme` de Mantine ; en CSS, `light-dark()` ou `[data-mantine-color-scheme="dark"]`
(la classe `.dark` n'est qu'un miroir jusqu'à STY9). Les variables de Mantine qui existent déjà et
suivent le site :
`--mantine-color-{body,text,dimmed, placeholder,anchor,default,default-hover,default-color,default-border,error}`,
`--mantine-primary-color-{filled,filled-hover,contrast,light,light-hover,light-color}`,
`--mantine-radius-{xs,sm,md,lg,xl,default}` ; `theme.primaryColor` vaut `recit` quand le site a une
couleur lisible. `useComputedColorScheme` rend `light` au serveur et jusqu'au premier effet : un
composant qui affiche l'état attend l'hydratation (`useIsClient`), comme `ThemeSwitch`.

**Pièges.** Un processus d'attente dont la ligne de commande contient `e2e-linux` se voit lui-même
dans `pgrep -f e2e-linux` et attend pour toujours : tester avec `pgrep -f 'e2e-linux\.j[s]'` ou
`docker ps --filter ancestor=…` (trouvé par TST-A). Deux `pgrep`/`pkill` sur son propre motif tuent
le shell courant (code 144).

### STY6.2 — @recit/components — 29/10

Vague 3 du plan 19 (§10, STY6.2 ; `20-multi-agents.md` §4), en parallèle de STY2, TST-A et STY7b.
Fichiers : `packages/components/src/**` (les 11 composants, leurs 17 modules et tables, les 9
stories, `cx.ts`, `hooks/useCssVariable.ts`, les tests), `scripts/style-rule-baseline.json` (deux
dossiers à zéro), une ligne dans `.claude/skills/mantine-style/SKILL.md`. Aucune commande git, pas
de `pnpm install`, pas de `pnpm fix`.

**Le mécanisme, composant par composant.** Un `X.module.css` à côté de chaque `X.tsx`, importé par
sa table `./X.module.css.js` (STY0b, STY6.1), CSS brut compilé par Lightning CSS, sans `rem()` ni
mixin. Les composants qui ne faisaient que poser des classes de `deck.css` (`Quote`, `Callout`,
`CodeGroup`, `Card`, `Col`/`Left`/`Right`, `Image`, `Badge`, `Step`, `Kbd`) composent le contrat
(`composes: slide-callout slide-callout-info from global`), une entrée de table par variante
(`classes.info`, `classes.cols3`, `classes.contain`, `classes.primary`) choisie par une `Record`
typée hors du JSX ; une valeur inconnue venue d'une directive retombe sur la variante par défaut,
comme avant. Les composants qui portaient des utilitaires Tailwind (`Cover`, `Points`, `Media`,
`CodeBlock`, `Mermaid`, l'avertissement de `Grid`) reçoivent la traduction règle par règle, aux
valeurs **relevées dans la feuille compilée de Storybook** (`storybook-static/assets/iframe-*.css`,
Tailwind 4.3.3) plutôt que dans la table du skill quand les deux diffèrent : `bg-primary/10` est
`color-mix(in oklab, hsl(var(--primary)) 10%, transparent)` (pas `hsl(… / 0.1)`), `rounded-xl` est
`var(--radius-xl)` que `tokens.css` redéfinit en `calc(var(--radius) * 0.8)` (donc 1rem, pas
0,75rem, et il suit `theme.radius`), `text-sm` porte `line-height: calc(1.25 / 0.875)`, `shadow-lg`
ses deux ombres, `md:` est `@media (width >= 48rem)`. `builtins.tsx` est découpé : un fichier par
composant exporté (`Card.tsx`, `Columns.tsx`, `Col.tsx` pour les trois colonnes, `SlideImage.tsx`,
`Badge.tsx`, `Step.tsx`), le registre `BUILTIN_MDX_COMPONENTS` et l'alias `SlideGrid` restent dans
`builtins.tsx`, `index.ts` exporte les nouveaux fichiers : même API publique. Le relais d'un
`className` d'auteur (question laissée par STY0b) passe par `cx(...classes)` exporté par
`@recit/components` (`className={cx(classes.root, className)}`, ne voit jamais de littéral ; `Kbd`
l'utilise aussi, les `className = ""` par défaut disparaissent) : une ligne dans le skill, test
`cx.spec.ts`. `CodeBlock` : les deux `style={{ fontSize }}` deviennent `data-size`
(`lg`/`md`/`sm`/`xs` selon le nombre de lignes, `base` sans `autoFit`), lu par le module ; la
classe-marqueur `has-line-numbers` reste dans le HTML par `composes` (`classes.numbered`). `Columns`
: le `ratio` (`"1:2"`, toute chaîne, pas un ensemble fini : `data-columns` ne peut pas le porter, et
le pipeline Markdown émet le même gabarit en `style` dans son HTML) devient la variable `--columns`
écrite par `useCssVariable` (§0.3 permis 4c), le même hook que `@recit/site/hooks`, copié dans
`packages/components/src/hooks` parce qu'un paquet publié n'importe pas le framework ; le module lit
`grid-template-columns: var(--columns)`, invalide donc `none` sans ratio, comme l'élément sans
`style`. Aucune story ni aucun exemple n'emploie `<Columns ratio>`. Variante sombre (l'avertissement
de `Grid` seul) : `.warning:where(:global(.dark) *, [data-mantine-color-scheme="dark"] *)`, les deux
jusqu'à STY9. Stories : plus de `className` ni de `style` ; les cartes des stories `Grid` et la pile
de `Callout/AllTypes` viennent d'un module de fixtures `slides/Stories.module.css` (`.card`,
`.stack`, mêmes valeurs qu'avant), compilé comme les autres, donc **deux règles de plus dans la
feuille publiée** et deux fichiers dans `src/` du tarball ; à déplacer si le plan 21 donne un autre
toit aux fixtures.

**Pourquoi les pixels ne bougent pas.** Les utilitaires vivaient dans `@layer utilities` et
`deck.css` hors couche : `deck.css` gagnait toujours (`.slide h1` fixe la taille du titre de
`Cover`, `.slide p, .slide li` celle des points, `text-5xl` et `space-y-4` n'avaient aucun effet
dans une diapositive). Les modules sont hors couche aussi, après `deck.css`, à la même spécificité
(0,1,0) que les utilitaires : tout ce que `deck.css` règle par un sélecteur d'élément (0,1,1)
continue de gagner, et les classes composées n'apportent aucune déclaration (règles vides), donc
rien de neuf n'entre en concurrence avec `.slide-callout`, `.slide-grid`, `.slide-card`… Le seul cas
où une spécificité aurait monté, `space-y-4`, est écrit `:where(.list > :not(:last-child))` à zéro,
comme Tailwind l'émet. Les `md:` sont conservés tels quels (sans effet dans `.slide`, fidèles
ailleurs).

**Lightning CSS, vu en route.** (1) `composes: root` d'une classe locale ne reprend **pas** les
`from global` de cette classe : la table joindrait `cols2 root grid-cols-2` sans `slide-grid` ;
chaque variante nomme donc ses globales elle-même (`composes: slide-grid grid-cols-2 from global`,
accepté avec plusieurs noms). (2) `@keyframes pulse` est préfixé (`recit-components-Mermaid__pulse`)
et l'`animation` réécrite, mais pas à l'intérieur d'un `var()` : écrire le nom en clair. (3)
`calc(1.25 / 0.875)` est évalué en `1.42857` (six chiffres, invisible), `rgb(0 0 0 / 0.1)` devient
`#0000001a` (ce que l'optimiseur de Tailwind faisait déjà dans les feuilles d'avant), un `color-mix`
entre deux couleurs littérales est précalculé en `oklab()`. (4) Les règles vides des classes qui ne
font que composer restent dans `dist/styles.css` et sont retirées par les minifieurs des
consommateurs (`recit-components-Card__root` absent de `main.css` d'un deck, la classe est dans le
HTML). (5) Biome : deux suppressions commentées, `noDuplicateProperties` sur un double `composes`
(local puis global, la syntaxe des modules) et `noUnknownPseudoClass` sur `:global` ;
`css.parser.cssModules: true` dans `biome.jsonc` (fichier partagé, pas de ce lot) les rendrait
inutiles. (6) Le littéral d'une condition dans l'expression `className` compte pour le compteur
(`fit === "contain" ? …`, comme STY7a) : la `Record` hors JSX règle le point.

**Vérifications.**

```
$ pnpm -F @recit/components build && pnpm -F @recit/components typecheck
@recit/components: 17 CSS modules → dist/styles.css   (… Stories.module.css → card, stack)
✔ Build complete in 2104ms   (63 files, 48.31 kB) ; tsgo muet
$ pnpm exec vitest run packages/components
 Test Files  5 passed (5)
      Tests  34 passed (34)
$ pnpm exec vitest run packages/cli/src/__tests__/css-modules.spec.ts packages/formats/deck packages/integrations/site-stories
 Test Files  22 passed (22) / Tests  120 passed (120)
$ node scripts/build-css-modules.js --check --all
✅ @recit/components: 17 CSS modules, tables up to date   (deck 0, motion-blocks 1, site-stories 0 : ✅)
$ node scripts/check-style-rule.js packages/components
directory  className  style  props  imports  css  directives  no-module  total
total              0      0      0        0    0           0          0      0
$ node scripts/check-style-rule.js --write-baseline packages/components/src
✅ scripts/style-rule-baseline.json written: 1085 in 30 directories.
$ pnpm check
biome (31 avertissements préexistants), oxfmt (1144 fichiers), imports, admonitions, front matter,
15 version plans, frontières, interfaces, liens : verts
✅ No rise over scripts/style-rule-baseline.json (warning mode, 1085 remaining)
✅ @recit/components: 17 CSS modules, tables up to date
$ pnpm publint → ✅ 12 published packages pass publint.
$ cd examples/deck-mdx && recit build slides.mdx   (CLI déjà construite, sans nx)
recit-components-Cover__root 1, recit-components-Stories__card 1, .slide-grid.grid-cols-2 1,
.text-5xl 0, .max-w-4xl 0 : les utilitaires retirés ne sont plus compilés, la feuille du paquet y est
$ BROWSER=none pnpm test:e2e:linux e2e/deck-render.spec.ts e2e/storybook.spec.ts e2e/site-components.spec.ts
[e2e-linux] start 19:33:27 · installed 19:33:54 · built 19:36:00 · tested 19:37:32
  120 passed (1.5m)
$ node scripts/screenshot-gallery.js
0 changed screenshot(s)
git status e2e/__screenshots__ → vide
```

Compteur : `packages/components/src/mdx` 11 → 0, `packages/components/src/slides` 49 → 0 ; baseline
1145 → 1085. Captures `deck-render` (6 decks, clair, sombre, impression), `storybook` (16 stories ×
2 modes) et `site-components` (14 pages) **identiques** : rien à accepter dans la galerie.

**Pour STY6.3 et la suite.** Même recette dans `@recit/deck` et `site-stories` : relever les valeurs
dans une feuille compilée plutôt que les deviner, garder la spécificité des utilitaires (un `:where`
là où Tailwind en met un), nommer les globales dans chaque variante composée, pas de `composes`
local + global sans la suppression biome. `useCssVariable` existe maintenant dans
`@recit/components` (exporté) ; `cx` aussi, pour tout élément nu qui relaie un `className`. La liste
d'exclusion du compteur n'a pas eu besoin de l'entrée `hooks/useCssVariable.ts` (le compteur ne
compte que le `style` du JSX), à ajouter si l'on veut la symétrie avec celle du site. Le module de
fixtures `Stories.module.css` attend le plan 21.

### STY7b — Blocs motion, couche d'animation — 29/10

STY7.2 et STY7.3 du plan 19 (vague 3), repris sur le disque d'un agent coupé par la limite d'usage :
son travail (`render.tsx` réécrit, `render.module.css` et sa table, `MotionFrame`, tests) était
complet et sain, gardé tel quel ; ce lot l'a vérifié, formaté (oxfmt sur ses seuls fichiers) et
prouvé. Fichiers : `packages/formats/motion-blocks/src/ui/render.tsx`, `src/ui/render.module.css`
(nouveau) et `render.module.css.ts`, `src/player.tsx`, `src/blocks/uiScene.tsx`,
`src/blocks/blocks.module.css` (`.motionFrame`, `.motionStage`) et sa table,
`test/{render,player}.test.tsx`, `scripts/style-rule-allowlist.json` (raisons de `player.tsx` et
`render.tsx` réécrites), `scripts/style-rule-baseline.json`. `RemotionAnim.tsx` intouché, aucune
commande git, aucun `package.json`.

**Ce qui change.** `render.tsx` (65 `style` en ligne, 1 490 → 1 002 lignes) : tout ce qui est
constant est une classe de `render.module.css` (≈ 70 classes : `scene`, `stage`, `mock`, `box`,
`text`, `badge`, `dot`, `chip`, `row`, `shell`, `topbar`, `rail`…), les ensembles finis des `data-*`
(`data-tone`, `data-size`, `data-weight`, `data-variant`, `data-active`, `data-dir`, `data-align`,
`data-justify`, `data-border`, `data-shadow`, `data-fit`, `data-mock`, `data-camera`,
`data-fill`/`data-stroke` d'un chemin de vecteur, `data-dot`). Les mélanges alpha (`withAlpha`) sont
des `color-mix(in srgb, var(--mb-…) N%, transparent)` du même token : une feuille pour tous les
thèmes, le thème arrive par les `--mb-*` de `Frame`. Une seule fabrique, `el()`, écrit `style` : (1)
ce que le temps de scène échantillonne (fondus d'état, `sampleSpec` d'un `enter`/`exit`, la caméra,
le voile `--mb-ui-dim`), et (2) ce qu'un nœud auteur fixe librement (une largeur, une taille, une
couleur hex, un gap) en variables `--mb-ui-*` que la classe lit, comme `ThemeVars` ; chaque classe
remet ses variables à `initial`, donc un nœud imbriqué n'hérite pas de celles du parent. **Point
pour le coordinateur** : (2) n'est ni par temps ni fini ; c'est l'analogue de `ThemeVars` (valeur de
données, nécessaire au premier rendu, Remotion), nommé dans la raison de l'entrée `render.tsx` de la
liste d'exclusion. La caption de `UiSceneView` lit `var(--mb-font-mono)`, le prop `theme` de
`UiSceneView` est retiré. `MotionFrame` : cadre `classes.motionFrame` dimensionné par le module
depuis `data-aspect` (1920×1080, 1080×1080, 1080×1920) et une variable `--mf-scale` (largeur mesurée
/ largeur native, premier rendu), scène `ThemeVars` + `classes.motionStage` (`composes: backdrop`,
`transform: scale(var(--mf-scale))`) : le fond du film passe par le module.

**Preuve d'identité.** Au-delà des goldens (un seul `ui-scene` à mi-scène), un banc A/B jetable
(dans le scratchpad, rien dans le dépôt) a rendu l'ancien `render.tsx` (`git show HEAD`) et le
nouveau dans le même `Frame`, 19 scènes × 2 thèmes : tous les types de feuilles et leurs variantes,
les trois chromes, les conteneurs avec `bg`/`border`/`radius`/`shadow`/`justify`/`align`, et une
scène à trois états avec caméra et voile échantillonnée à 8 instants (fondus, `enter` en ligne,
`exit` par preset) : 38 paires, **0 pixel différent** (pixelmatch, seuil 0). Écart connu, hors rendu
: une couleur auteur nommée (`red`) recevait `withAlpha` sans effet, elle reçoit maintenant l'alpha
de `color-mix` ; aucun fixture ne l'emploie.

**Vérifications.**

```
$ pnpm exec vitest run packages/formats/motion-blocks packages/formats/motion-render
 Test Files  7 passed (7)
      Tests  52 passed (52)
$ pnpm -F @recit/motion-render goldens:check
  ✓ warm-title — 0.000% changed   … (32 vues, warm et saas, film.json et .story.md)
✓ all goldens match            (sans régénération)
$ pnpm exec nx run-many -t build typecheck -p @recit/motion-blocks @recit/motion-render
 NX   7 tasks: 7 succeeded
$ node scripts/build-css-modules.js --check --all
✅ @recit/motion-blocks: 2 CSS modules, tables up to date   (les trois autres ✅)
$ node scripts/check-style-rule.js packages/formats/motion-blocks packages/formats/motion-render
total              0      0      0        0    0           0          0      0
(7 style in the files of scripts/style-rule-allowlist.json, not counted)
$ node scripts/check-style-rule.js --write-baseline packages/formats/motion-blocks/src
✅ scripts/style-rule-baseline.json written: 1086 in 30 directories.
$ pnpm check
✅ … (warning mode, 1085 remaining) — vert
$ BROWSER=none pnpm test:e2e:linux e2e/motion-dev.spec.ts e2e/site-motion.spec.ts
  31 passed (1.0m)
$ node scripts/screenshot-gallery.js
0 changed screenshot(s)
```

Compteur : `style` dans les fichiers de la liste d'exclusion 71 → 7 (dans `render.tsx`, la seule
fabrique `el` ; dans `player.tsx`, `ClockAnim` et `--mf-scale`) ; hors liste 0. Aucune capture
acceptée, aucun golden régénéré.

### STY3c — Primitives ui, code — 29/10

Lot STY3c du plan 19, vague 3. Aucune commande git.

**En place.**

- `CodeBlock.tsx` + `CodeBlock.module.css` : cadre, barre de titre, bouton copier (balise `button`,
  pas de composant Mantine : les tests n'ont pas besoin de `MantineProvider`), contenu ; utilitaires
  reportés valeur pour valeur (`rounded-xl` → `calc(var(--radius) + 4px)`, `bg-black/4`, `text-sm`
  en `calc(1.25 / 0.875)`, `scrollbar` de `main.css` en classe du module, `not-prose` par
  `composes: not-prose from global` jusqu'à STY5). `showLineNumbers` → `data-line-numbers`. Exporte
  `HIGHLIGHT_CODE_BLOCK_CLASS` (= `classes.code`), `codeBlockClasses` et `withClassName` (relais du
  `className` d'un importeur, sans littéral dans le JSX).
- Contrat Shiki : les règles `.shiki` du `@layer base` de `main.css` passent dans
  `CodeBlock.module.css`. Les règles propres aux blocs (grille, lignes pleine largeur,
  `--highlighted-bg`, numéros) sont portées par `.code` ; celles de toute sortie Shiki (paire
  `--shiki-light`/`--shiki-dark`, fond, mots et lignes surlignés) restent globales par `:global`, le
  code en ligne `{:ts}` et les decks montrés dans un site les lisant aussi. Le mode passe par
  `[data-mantine-color-scheme="dark"]` **et** `.dark` (jusqu'à STY9) ; `!important` conservés
  (biome-ignore motivés) pour que les couleurs en ligne d'un deck perdent comme avant.
- `HIGHLIGHT_CODE_BLOCK_CLASS` retiré de `shiki-constants.ts`. Écart au plan : ce n'est pas
  `plugin-shiki.ts` qui le consomme (il ne lit que `defaultLanguages`, inchangé), mais `shiki.ts` et
  `MdxComponents.tsx`. `shiki.ts` est chargé par Node (plugins Vite) et ne peut pas importer un
  module CSS : `highlight()` reçoit la classe en sixième paramètre (`codeClassName`), passée par
  `HighlightedCode` ; `MdxComponents` l'importe de `ui/CodeBlock.js` (deux lignes).
- `CodeTabs` sur `Tabs` de Mantine (`unstyled`, `classNames` sur `list`, `tab`, `tabLabel`), cadre
  et contenu par `codeBlockClasses` ; onglet actif par `data-active`. Pas de `composes` d'un autre
  fichier : postcss-modules recopie alors les règles importées, Shiki global compris, dans la
  feuille du chunk (vu au premier build : `CodeBlock__*` en double dans `CodeTabs-*.css`).
- `EmbeddedCodeBlock` : `Button` de `ui/` remplacé par une balise `button` et le variant
  `outline`/`icon-xs` reporté dans son module (indépendant de la réécriture de STY3a) ;
  `data-full-height`, `data-show-copy`, `data-hidden`. `SyntaxHighlight` sans classe, toujours
  paresseux (`MdxComponents`). `Stepper.module.css` : les règles `.stepper` de `@layer utilities`,
  qui restent dans `main.css` pour STY5 (le lot ne touche que les règles Shiki).
- `CodeBlock.test.tsx` lit `classes.root`, `classes.header`… (un test de plus : classes et
  `data-line-numbers`) ; `Stepper.test.tsx` inchangé, vert.

**Vérifié.** `vitest run packages/site/src/lib/ui` : 2 fichiers, 11 tests ;
`vitest run packages/site` : 13 échecs hors lot (`BrowserWindow`, `LandingPage`, `Main`,
`Navigation` : « MantineProvider was not found », le `Button` migré par STY3a).
`pnpm -F @recit/site typecheck` : erreurs hors lot seulement (`Dialog`, `Drawer`, `Pagination`,
`ToggleGroup`, STY3a/STY3b). `docs:build` (avec `--exclude-task-dependencies`, voir ci-dessous) : le
JS de `CodeBlock` et sa feuille dans un chunk partagé lié dans le `<head>` de chaque page, sans
Shiki (10 ko) ; HTML prérendu `CodeBlock__root`, `CodeBlock__code`. Compteur : mes six `.tsx` à 0 ;
`packages/site/src/lib/ui` 335 → 31, baseline réécrite. `pnpm check` : biome, imports, format et
`check:other` propres sur mes fichiers ; une erreur hors lot (`noConsole` dans
`sites/storybook/.storybook/vitest.setup.ts`).

**Non fait : e2e et galerie.** Le conteneur
(`test:e2e:linux e2e/site-docs.spec.ts e2e/site-presentation.spec.ts`) s'arrête à
`@recit/site:build` : `check-external-deps` refuse `storybook` importé par
`packages/site/src/lib/components/Header.stories.tsx` (fichier d'un autre lot, hors de ma liste). À
relancer quand il est déclaré ou exclu. Différences à attendre : aucune voulue ; à surveiller, la
hauteur de ligne (`1.42857` minifié contre `calc(1.25 / .875)`), le fond des blocs de code d'un deck
montré dans un site (`!important` en couche contre hors couche).

### STY3b — Primitives ui, surcouches — 29/10

Lot STY3b du plan 19 (vague 3) : `Dialog`, `Drawer`, `Popover`, `Tooltip`, `DropdownMenu`,
`Command`, `Collapsible`, `NavigationMenu` de `packages/site/src/lib/ui`, leurs modules, deux tests
(`Command.test.tsx`, `Overlays.test.tsx`), `@mantine/spotlight/styles.layer.css` importé à côté de
la feuille de Mantine dans `entry.client.tsx` et `entry.server.tsx`. Le catalogue et
`packages/site/package.json` portaient déjà `@mantine/spotlight` 9.7.4, installé : pas de
`pnpm install` (lockfile intact). Aucune commande git.

**En place.** Chaque fichier garde ses exports et l'API composée de Radix pour ses importeurs
(`asChild`, `open`/`onOpenChange`, `side`/`align`/`sideOffset` sur le contenu, `onCloseAutoFocus`,
`onInteractOutside`), sans `cn` ni `cva` ; une classe passée par `className` est jointe à celle du
module.

- `Dialog` → `Modal.Root` composé (portail, overlay, piège et retour du focus, verrou du défilement
  de Mantine) ; le bouton de fermeture en pastille est reporté tel quel. `onInteractOutside`
  annulable (clic sur l'overlay), `onEscapeKeyDown` aussi. Animation `fade-in-0 zoom-in-95` →
  transition Mantine `zoomFade` (exportée), 200 ms, overlay 150 ms.
- `Drawer` (vaul) → `Drawer.Root` de Mantine, `direction` → `position`, contenu placé par `inset`
  selon `data-direction` (un tiroir `left` reste à gauche en RTL), glissement de vaul (500 ms,
  `cubic-bezier(0.32, 0.72, 0, 1)`), overlay `bg-black/80`.
- `Popover` → `Popover` de Mantine, `trapFocus` et retour du focus ; le placement de
  `PopoverContent` remonte à la racine par le contexte. `Tooltip` → `Tooltip` de Mantine : la racine
  lit ses enfants `TooltipTrigger`/`TooltipContent` et passe le contenu en `label` ; ouvre au survol
  et au focus clavier, flèche de Mantine (`TooltipArrow` ne rend plus rien).
- `DropdownMenu` → `Menu` (rôles, flèches, focus) ; `asChild` d'un item par `renderRoot` (le lien
  devient l'item, sans `type`), cases et radios sur `Menu.CheckboxItem`/`RadioItem`, sous-menus sur
  `Menu.Sub`. `Collapsible` → `Collapse` (`expanded`, sans animation comme avant : les keyframes
  `.CollapsibleContent` de `main.css` ne s'y appliquaient pas), `data-state` gardé sur le
  déclencheur et le contenu. `NavigationMenu` → `nav`/`ul`/`li` et module ; un item à déclencheur
  ouvre un `Menu` (`click-hover`) sous son déclencheur, le viewport partagé de Radix disparaît
  (`NavigationMenuViewport` reste, vide) ; `navigationMenuTriggerStyle()` rend `classes.trigger`, et
  `.link.trigger` reproduit l'ordre de la feuille Tailwind (colonne, `rounded-sm`, `px-4 py-2`).
- `Command` → `@mantine/spotlight`, sans thème (`unstyled`, le module dessine) : `CommandDialog` =
  `Spotlight.Root` sur un store à lui, sans raccourci global (`shortcut={null}`, le site ouvre par
  `open`), `clearQueryOnClose={false}` ; `CommandInput` = `Spotlight.Search` avec les rôles de cmdk
  (`combobox`, `aria-controls`, `aria-activedescendant`) ; `CommandItem` = `Spotlight.Action`
  (`option`, `aria-selected`, `data-value`), `asChild` par `renderRoot` ; `CommandList` = la
  `listbox` dont l'id est donné au store. `value`/`onValueChange` de cmdk tenus au pas de l'index de
  Spotlight ; `shouldFilter` ignoré (pas de filtre) ; `CommandEmpty` masqué en CSS
  (`.root:has([data-action])`) ; `content.className` n'est plus relayé, le module porte la largeur
  qu'il donnait (100 % jusqu'à 750 px, 36rem dès 40em : `sm:max-w-xl` gagnait sur `max-w-[750px]`).
- Modules en `@layer components`, comme STY3a : une classe Tailwind d'un importeur (couche
  `utilities`) gagne encore, comme par `cn()`. `sm:` de Tailwind = 40em, palier absent de Mantine :
  `@media (min-width: 40em)` écrit en clair ; `md:` = `$mantine-breakpoint-sm`.

**Pièges.** Le store de Spotlight change l'index choisi par `updateState`, qui ne notifie personne :
`useSpotlight` ne voit pas les flèches ; la recherche relit l'index dans une micro-tâche après
chaque touche et chaque saisie. `Spotlight.Action` ne transmet pas `id` (passé par `renderRoot`). Un
`onOpenChange` contrôlé est souvent un simple `onClose` : `CommandDialog` ne le rappelle que sur un
changement. Les éléments flottants de Mantine restent cachés sous happy-dom sans
`<MantineProvider env="test">`.

**Bloquant, hors lot : l'ordre des couches.** Vite lie la feuille d'un chunk partagé avant celle de
l'entrée ; `Alert-*.css` (module `@layer components` de STY3a) est la première feuille de chaque
page et crée `components` avant la déclaration de `layers.css` : `components` passe sous `mantine`
et sous la `base` de Tailwind, dont le `* { padding: 0 }` gagne sur tous nos modules (liens
d'en-tête sans marge, palette de recherche sans cadre, constaté au `dist`). Une déclaration
`@layer mantine, theme, base, components, utilities;` en tête de la page avant les feuilles rend les
captures (essai local sur une copie du `dist`). À poser au raccord : préfixer chaque feuille émise
de cette déclaration (`generateBundle` du plugin `recit-style`), ou la poser en tête du `<head>`.

**Raccords demandés.** `plugins/search-pagefind/IndexingDialog.tsx` importe `DialogTrigger` de
`@radix-ui/react-dialog` : hors d'un dialogue Radix il lève une erreur, en dev seulement (le bouton
« Build Search Index ») ; l'importer de `@recit/site/ui/Dialog.js`. Sans importeur désormais, à
retirer au prochain `pnpm install` : `cmdk`, `vaul`,
`@radix-ui/react-{dropdown-menu,popover,tooltip,navigation-menu}` (`react-dialog` attend
`IndexingDialog`, `react-collapsible` `NavigationCategory`, `react-visually-hidden` trois
importeurs).

**Vérifié.** `vitest run packages/site` : 61 fichiers, 620 tests (12 nouveaux) ;
`pnpm -F @recit/site typecheck` vert ; `docs:build` vert (`--exclude-task-dependencies`) ;
`check-style-rule.js packages/site/src/lib/ui` : 0 (baseline réécrite, dossier retiré) ;
`pnpm check` vert. Sonde Playwright locale sur le `dist` (déclaration de couches ajoutée) : Ctrl+K,
« tutorial », flèche bas, Entrée mènent à la page de l'option choisie ; palette et tiroir mobile aux
mêmes colonnes de pixels que `docs-search-open-linux.png` (clair et sombre) et
`docs-mobile-drawer-mobile-linux.png`.

**Non fait : e2e en conteneur et galerie.** `@recit/site:build` échoue toujours sur `storybook`
importé par `Header.stories.tsx` (TST0) ; et sans le raccord des couches, chaque capture de site
changerait. À relancer après les deux : `e2e/site-docs.spec.ts e2e/site-behaviour.spec.ts`.
Différence attendue : aucune voulue.

### STY3a — Primitives ui, actions et texte — 29/10

Vague 3 du plan 19 (§10, STY3a), en parallèle de STY3b, STY3c, STY7b et TST0. Aucune commande git.

**En place.** Les treize fichiers de `packages/site/src/lib/ui` (`Button`, `ButtonGroup`, `Badge`,
`Kbd`, `Toggle`, `Input`, `InputGroup`, `Label`, `Textarea`, `Separator`, `Card`, `Alert`,
`Callout`) sur le composant Mantine de §4.1 (`Button`/`UnstyledButton`, `Button.Group` et
`Button.GroupSection`, `Badge`, `Kbd`, `ActionIcon` pour `Toggle`, `TextInput`, `Input.Wrapper`,
`Input.Label`, `Textarea`, `Divider`, `Card`/`Card.Section`/`Text`, `Alert`), chacun avec son
`X.module.css` branché par `classNames` ; plus de `cn`, `cva`, Radix ni Base UI dans ces fichiers.
Les 27 fichiers sans importeur de §4.1 sont supprimés (l'export `./ui/*` est un joker : rien à
retirer de `package.json`). Nouveau : `ui/cx.ts`, même contrat que le `cx` de `@recit/components`,
pour les deux endroits que le `className` de Mantine n'atteint pas (l'`<input>` d'un `TextInput`,
l'enfant d'un `asChild`).

**Le mécanisme.**

- Tous les composants Mantine sont `unstyled` : le module dessine seul, valeur pour valeur depuis la
  feuille compilée d'avant (`sites/docs/dist`, Tailwind 4), et aucune règle de Mantine (hauteur,
  `text-box-trim`, `::before`, `mantine-active`, focus) n'entre en concurrence. Les enveloppes que
  Mantine ajoute (`inner`/`label` du bouton, `label` du badge, `root`/`wrapper` d'un `TextInput`,
  `wrapper`/`body`/`message` de l'alerte, `icon` de l'`ActionIcon`) sont `display: contents` : les
  enfants restent des éléments flex ou grid de la racine, comme avant ; les sélecteurs `[&>svg]`
  deviennent `.message > svg`, `.label > svg`.
- Couches : les modules sont dans `@layer components`, sous `utilities`. Une classe Tailwind passée
  par un importeur (`className="h-7 text-xs"` de `PagefindSearch`, `size-6` de `NavigationCategory`,
  `flex flex-col gap-2` de `Mermaid`) gagne donc comme `tailwind-merge` la faisait gagner. STY3b et
  STY3c ont fait le même choix. Trois familles de règles restent hors couche, parce que l'utilitaire
  qu'elles remplacent battait aussi ce qui les entoure : `display` et `padding-inline` des tailles
  de `Button` (un enfant `asChild` portait `flex` ou `p-5`, que Tailwind émettait avant
  `inline-flex` et `px-2.5` : `Pagination`, lien « modifier » de `MdxPage`), les règles sur les
  paragraphes et liens de `AlertDescription` (elles battent `.prose`, rangé dans `utilities`), et le
  contenu de `Callout` (`a`, `ul`, `ol`).
- `cva` → `data-*` : `data-variant` et `data-size` (posés par la `Box` de Mantine depuis `variant`
  et `size`), `data-fit` de `Alert`, `data-orientation` de `ButtonGroup`/`Separator`, `data-type` de
  `Callout` qui choisit l'une des onze `--callout-*` (le `style` inline et le `--callout-color`
  calculé disparaissent).
- `asChild` (Button, Badge, ButtonGroupText) : `renderRoot` de Mantine ; pour `Button`,
  `UnstyledButton`, sans les deux `<span>` du `Button` de Mantine, pour que `space-x-1` de
  `Pagination` voie toujours ses enfants directs. Les classes du Slot et de l'enfant se joignent.
- Couleurs : `var(--mantine-*)` là où STY2 les fait pointer exactement sur la variable du site
  (`body`, `text`, `dimmed`, `placeholder`, `primary-color-filled`/`contrast`, `default-border`,
  `error`, rayons) ; `--muted`, `--secondary`, `--input`, `--ring`, `--card`, `--warning`,
  `--accent` sinon ; `light-dark()` pour chaque `dark:` (preset Mantine : règle
  `[data-mantine-color-scheme=dark]`) ; la palette bleue de l'alerte `info` écrite en `lab()`, les
  variables `--color-blue-*` de Tailwind n'étant émises que tant qu'un utilitaire les emploie.
- Un module par fichier, une classe racine par composant exporté (`Kbd` : `.root`, `.group` ; `Card`
  : `.root`, `.section` lu par `data-slot`, `.title`, `.description`).

**API.** Props inchangées pour les importeurs. Retirés, sans utilisateur : `buttonVariants`,
`badgeVariants`, `toggleVariants`, `buttonGroupVariants` (des chaînes de classes), `asChild` de
`Separator`, l'attribut HTML `title` d'`Alert` (pris par Mantine). `Toggle` garde `pressed`,
`defaultPressed`, `onPressedChange(pressed, event)`. Un `Separator` décoratif porte `role="none"`
et, Mantine filtrant les `undefined`, un `aria-orientation` en plus.

**Hors liste, additif.** Quatre tests de `lib/components` rendent sous `MantineProvider`
(`wrapper: MantineProvider`) : `BrowserWindow`, `LandingPage`, `Main`, `navigation/Navigation`.

**Vérifié.**

```
$ pnpm exec vitest run packages/site
 Test Files  61 passed (61)
      Tests  620 passed (620)
$ pnpm -F @recit/site typecheck        → tsgo muet
$ pnpm exec nx run docs:build --excludeTaskDependencies   → vert (sans l'option, @recit/site:build
  échoue en postbuild : `storybook` importé par Header.stories.tsx, TST0)
$ node scripts/check-style-rule.js packages/site/src/lib/ui
total  0 0 0 0 0 0 0 0   (mes treize fichiers à zéro avant que STY3b ne finisse les siens)
$ node scripts/check-style-rule.js --write-baseline packages/site/src/lib/ui
✅ scripts/style-rule-baseline.json written: 751 in 29 directories.   (ui : 335 → 0)
$ pnpm check
biome : 3 erreurs `suppressions/unused` dans Slot.test.tsx et createRedirectRoutes.test.tsx (hors
lot) ; mes fichiers : avertissements seulement (`@mixin` inconnu de biome, specificité
descendante) ; fmt:check : Command, Dialog, DropdownMenu, Tooltip (STY3b, en cours) ;
check:imports et check:other verts.
```

Rendu SSR vérifié à la main (`renderToString` de chaque composant sous `MantineProvider`) : classes
de module puis classe de l'auteur, `data-variant`/`data-size`, enveloppes Mantine attendues ; les
`style` de variables posés par Mantine (`--button-height`…) restent dans le HTML, sans effet
(`unstyled`).

**Non fait : e2e et galerie.**
`pnpm test:e2e:linux e2e/site-docs.spec.ts e2e/site-www.spec.ts e2e/site-components.spec.ts` lancé
une fois : la construction du conteneur échoue sur `@recit/site:build` (même postbuild,
Header.stories.tsx de TST0) ; attendu vingt minutes, pas corrigé. Il bloquerait de toute façon sur
le défaut relevé par STY3b : dans le `dist`, `Alert-*.css` (module `@layer components`) est la
première feuille liée et ouvre `components` avant `layers.css`, donc sous `mantine` et `base`
(vérifié dans `sites/docs/dist/index.html`). À relancer après les deux raccords ; différences
attendues : aucune voulue.

### TST0 — Le socle Storybook test — 29/10

Vague 3 du plan 21 (§8), en parallèle de STY3a-c, STY6.2 et STY7b. Aucune commande git, pas de
`pnpm fix`.

**Dépendances.** `@storybook/addon-vitest` 10.6.3 (aligné sur le Storybook installé : pas besoin de
10.6.5) et `@vitest/browser-playwright` 4.1.11 au catalogue ; `sites/storybook` reçoit en plus des
paquets déjà dans le lockfile (`@recit/site`, `@mantine/core`, `motion`, `@unhead/react` 3.4.2,
`vitest` 4.1.11 pour que le pair soit le Vitest racine) ; `packages/site` reçoit en dev
`@storybook/react-vite` et `storybook` (le typecheck de `tsconfig.app.json` voit les stories) et
l'export `./theme`. Un seul Vitest dans le lockfile, résolu avec `@vitest/browser-playwright`.

**En place.**

- Projet Vitest `storybook` (`sites/storybook/vitest.config.ts`, ajouté aux `projects` racine) :
  `storybookTest`, Chromium headless par Playwright, `prefers-reduced-motion: reduce`, `retry: 1`,
  axe en erreur. Le plugin applique lui-même les annotations de `preview.tsx` (pas de
  `setProjectAnnotations`). `.storybook/vitest.setup.ts` : presse-papiers en mémoire, échec sur tout
  `console.error`. `.storybook/vitest.chromium.ts` (global setup) : sans Chromium, arrêt avec «
  Install it once with: pnpm exec playwright install chromium » ou
  `pnpm test --project '!storybook'`. Fenêtre : 1200 × 900 par défaut, `globals.viewport` sinon.
- Décorateurs (`.storybook/decorators/`), chacun décide par le premier segment du titre
  (`surface.ts`) : `withSlide` (déplacé de `preview.tsx`, `withSlide.module.css` = l'ancien
  `preview.module.css`), `withSite`, `withMantine` (`createRecitTheme` +
  `recitCssVariablesResolver`, `forceColorScheme` depuis `globals.theme`), `withReducedMotion`
  (`MotionConfig`), `withTheme` (classe `.dark` **et** `data-mantine-color-scheme`), `viewports.ts`
  (mobile 390 × 844, tablet, desktop 1280 × 720), `stylesheets.ts` (loader), `a11yFindings.ts` (voir
  plus bas).
- Harnais du site : `@recit/site/testing` exporte en plus `SiteProvider`, `SlotProvider`,
  `ViewportAnchorProvider`, `SiteContext` (`SiteRoot` garde un `SiteContext` par onglet, deux
  stories le partageraient). `withSite` monte un routeur mémoire sur `parameters.site.path`, un
  `QueryClient` neuf, le contexte de `sites/storybook/src/harness/config.tsx` (site Lorem Ipsum,
  trois liens, une recherche de fixture : `Modal` Mantine « Search ») fusionné avec
  `parameters.site.options`, `Suspense`. Plugin `recitVirtualModules` dans `main.ts` :
  `virtual:recit-config` et `virtual:recit-theme.css` ; aucun module de `src/lib` n'importe de
  module virtuel aujourd'hui, un module manquant échoue avec son nom. Les globs des huit surfaces de
  §3.1 sont dans `main.ts` (quatre vides : un avertissement « No story files found » chacun).
- Feuilles : celle du site (Mantine, `defaultTheme.css`, `main.css` Tailwind du framework,
  `sites/storybook/styles.css` pour le fond) est **chargée par un loader**, seulement pour les
  surfaces Mantine : elle redéfinit `--background` et les utilitaires de `preview.css` dans un autre
  format. `layers.css` du site est importé en tête de `preview.tsx`. En `storybook dev`, recharger
  la page après une story `Site/` avant de revoir une diapositive.
- Story pilote `packages/site/src/lib/components/Header.stories.tsx` (`Site/Components/Header`) :
  `play` qui vérifie `aria-current="page"` sur « Sit amet », ouvre la recherche au clic, trouve le
  `dialog` « Search » par rôle, le champ focalisé, ferme par Échap. Vérifié qu'un nom faux échoue.
- `e2e/storybook.spec.ts` lit `storybook-static/index.json` : deux captures par story, attente de
  `currentRender.phase` `completed`/`finished` (le `play` fini), puis `wait-mermaid` / `wait-motion`
  lus dans les tags ; plus d'attente fixe de 700 ms pour toutes. `PENDING_TAGS` donne ces tags aux
  stories de `@recit/components` (cover, media, mermaid, points, quote) jusqu'à TST1.
- `scripts/check-stories.js` (+ `scripts/stories-baseline.json`, spec 14 tests) dans `check:other`,
  en avertissement : 142 (story 133, play 1, testid 8) ; `--list` ou un dossier listent les
  fichiers. Coverage racine : `**/*.stories.{ts,tsx}` exclus.
- `packages/site/scripts/check-external-deps.ts` ignore `**/*.stories.*` comme les tests : le
  postbuild de `@recit/site` refusait l'import `storybook` de la story pilote, ce qui a cassé une
  construction en conteneur de STY3a (corrigé à 20:45).

**Trouvé.** axe (`color-contrast`) : `slides-codeblock--type-script`, nom de fichier et langue sur
l'en-tête sombre (2,62:1), passé en `todo` par `decorators/a11yFindings.ts` (liste qui ne fait que
baisser, à TST1) ; le ⌘K du bouton de recherche (4,39:1, `text-muted-foreground` sur `bg-muted`),
`a11y.test: "todo"` dans la story pilote, à STY4a.

**Vérifié.**

```
$ pnpm test --project storybook        → 10 fichiers, 17 tests (16 + le pilote), 13 s
$ pnpm test                            → 176 fichiers, 1758 tests, 69 s (1 min 12 mur)
  avant, `--project '!storybook'` : 55 s (58 s mur) ; le projet ajoute ~15 s
$ pnpm -F recit-storybook build && typecheck ; pnpm -F @recit/site typecheck ; build → verts
$ pnpm exec vitest run scripts/__tests__ → 9 fichiers, 230 tests
$ node scripts/check-stories.js        → ✅ No rise (warning mode, 142 remaining)
$ pnpm check                           → vert (avertissements biome préexistants)
$ BROWSER=none pnpm test:e2e:linux e2e/storybook.spec.ts
  33 passed, 2 failed (les deux captures absentes du pilote) : les 32 existantes inchangées
$ BROWSER=none pnpm test:e2e:update:linux e2e/storybook.spec.ts → 35 passed, deux écrites
$ BROWSER=none pnpm test:e2e:linux e2e/storybook.spec.ts → 35 passed (47,6 s)
```

Références nouvelles :
`e2e/__screenshots__/storybook.spec.ts/storybook-site-components-header--default-{light,dark}-linux.png`.

Couverture (lignes / branches, `pnpm test:coverage` une fois, stories exclues) : core 89,8 / 73,9 ;
motion 86,6 / 73,3 ; scripts 87,1 / 81,7 ; app 73,1 / 57,0 ; site-stories 68,6 / 51,0 ; site 66,4 /
54,7 ; site-kit 66,7 / 37,7 ; components 63,1 / 35,3 ; motion-blocks 59,0 / 33,1 ; mcp 54,8 / 41,8 ;
cli 48,7 / 38,3 ; deck 27,2 / 20,2 ; motion-render 20,6 / 6,9 ; total 66,8 / 54,3.

**Recette pour TST1 à TST5.** Une story `X.stories.tsx` à côté de `X.tsx`, titre de la surface
(`Slides/…`, `Site/UI/…`, `Site/Components/…`) : les décorateurs suivent seuls ; `Site/` reçoit
`parameters.site.{path,options}` ; `play` avec `storybook/test`, rôles et noms, `findBy*` après une
interaction, `within(canvasElement.ownerDocument.body)` pour un portail ; tag `wait-mermaid` ou
`wait-motion` si la story se stabilise tard (TST1 vide `PENDING_TAGS`) ; une violation axe se
corrige ou passe en `a11y: { test: "todo" }` avec sa raison. Puis `pnpm test --project storybook`,
`node scripts/check-stories.js <dossier>` et `--write-baseline <dossier>`, et à la porte
`pnpm test:e2e:update:linux e2e/storybook.spec.ts`. Une story d'un site ou d'un exemple ajoute ce
projet aux `devDependencies` de `sites/storybook` (ST-20).

### Porte de la vague 3 — 29/10

- Ordre des couches : Vite liait la feuille d'un chunk (`Alert-*.css`, en `@layer components`) avant
  celle de l'entrée, ce qui rangeait `components` sous `mantine` et sous la `base` de Tailwind.
  `recit-style` (`packages/site/src/vite/plugin.ts`) ouvre désormais chaque feuille émise sur
  `@layer mantine, theme, base, components, utilities;`.
- `IndexingDialog` prend `DialogTrigger` de `@recit/site/ui/Dialog.js` (il importait encore Radix).
- e2e complet dans le conteneur : 454 passés, 5 échecs expliqués. `plans-home` change par le contenu
  (plans 18 à 21 dans le tableau et la carte) et `/demos/mdx` par le changement voulu de la vague 1
  : références régénérées. La pagination de `site-behaviour` perdait un clic pendant l'hydratation,
  plus longue depuis les chunks de Dialog et Spotlight : le test réessaie le clic comme ceux du
  thème et de la recherche. Un clic perdu pendant l'hydratation reste à comprendre côté navigation
  (STY4b).
- Galerie vide ; `pnpm check` vert. `ui/util.tsx` reste : `index.ts` et `Search.tsx` l'importent
  encore (STY4a).

### TST2a — Stories Site/UI, actions et code — 29/10

Vague 4 du plan 21 (§8, TST2), en parallèle de TST1, TST2b, STY4a-b, STY6.3a-b et STY8. Aucune
commande git, pas de `pnpm fix`.

**En place.** Dix-neuf fichiers `packages/site/src/lib/ui/<X>.stories.tsx`, titre `Site/UI/<X>`
(`withMantine`, `withTheme`, `withSite` suivent seuls) : `Button`, `ButtonGroup`, `Badge`, `Kbd`,
`Toggle`, `Input`, `InputGroup`, `Label`, `Textarea`, `Separator`, `Card`, `Alert`, `Callout`,
`Stepper`, `CodeBlock`, `EmbeddedCodeBlock`, `CodeTabs`, `CodeTabPanel`, `SyntaxHighlight`. 84
stories, 43 `play`, les états et interactions de §4.1 : clic, Entrée, Espace et focus visible du
bouton, désactivé sans rappel, `asChild` en lien ; `aria-pressed` du `Toggle` au clic et à Espace ;
saisie et `onChange` des champs, label qui focalise son champ, addon qui focalise l'entrée ;
`role="separator"` ou rien ; `role="alert"` et `role="note"` ; liste ordonnée du `Stepper` ; copie
dans le presse-papiers (moqué par `vitest.setup.ts`) et bouton désactivé deux secondes ; onglets au
clic et aux flèches, `aria-selected`, onglet partagé par `syncKey` et mémorisé dans `localStorage`.
Variantes disposées par `Group`/`Stack` de Mantine (`gap`, `align`) : ni classe ni style, compteur
de la règle à zéro. Rappels en `fn()` d'`args`.

- Hors liste, nouveau : `packages/site/src/lib/testing/syntaxHighlighting.ts`
  (`syntaxHighlightingFixture`), le surligneur qu'un site reçoit sans section `syntaxHighlighting`
  (thèmes et `defaultLanguages` par imports explicites), passé par `parameters.site.options` aux
  cinq stories de code.
- `CodeBlock.test.tsx` réduit à `getCodeBlockLabel` et au `code` de `MdxComponents` (qu'aucune story
  ne rend) ; les trois tests de rendu (titre, nom de langue, classes) sont dans les `play` et les
  captures. `Stepper.test.tsx` reste tel quel : il teste l'enregistrement des ancres auprès de
  l'`IntersectionObserver`, ce qu'une story n'affirme pas.

**Défauts trouvés** (composants hors lot, stories en `todo` avec la raison) :

- `Input`, `Textarea`, `InputGroupInput` perdent `aria-invalid` : l'`Input` de Mantine le pose
  depuis sa prop `error` et écrase celui de l'auteur ; `Input.module.css` n'a pas d'état invalide et
  la règle `:has([aria-invalid="true"])` d'`InputGroup.module.css` ne peut pas s'appliquer. Les
  trois stories `Invalid` sont en `tags: ["!test"]`, commentées (STY3a).
- `CodeTabs` copie le code de **tous** les onglets : `Tabs` de Mantine garde les panneaux cachés
  montés (`keepMounted`) dans l'élément copié. Story `CopiesTheVisibleTab` en `!test` (STY3c).
- axe `aria-allowed-attr` : un `Separator` décoratif (`role="none"`) porte `aria-orientation`
  (`Separator`, `ButtonGroup` avec séparateur).
- axe `color-contrast` : `Alert` destructive (4,36:1) et texte de l'`info` (4,43:1) ; libellé d'un
  onglet inactif de `CodeTabs` (4,42:1) ; l'orange de `github-light`, thème clair par défaut
  (3,48:1, `#e36209`), dans `SyntaxHighlight`.
- Harnais : une story `Site/` plus haute que la fenêtre (900 px) échoue à
  `scrollable-region-focusable` sur `<body>` (le `overflow-y-auto` du `body` de `main.css`) ;
  `Callout/Types` tient sur une ligne par type pour l'éviter.

**Vérifié.**

```
$ pnpm test --project storybook        → 44 fichiers passés, 1 sauté ; 161 tests (un premier
  passage concurrent : 13 échecs hors lot, connexion du navigateur fermée, verts au second)
$ pnpm exec vitest run packages/site --project '!storybook'
  → 5 échecs, tous dans navigation/Navigation.test.tsx (STY4b en cours)
$ pnpm -F @recit/site typecheck        → muet
$ pnpm -F recit-storybook typecheck    → 3 erreurs navigation/ (navigationListItem, STY4b)
$ pnpm -F recit-storybook build        → vert (un premier essai : packages/theme/dist reconstruit
  par un autre agent) ; 118 stories Site/UI dans index.json
$ node scripts/check-stories.js packages/site/src/lib/ui   → 0 ; baseline réécrite (site/ui retiré)
$ node scripts/check-style-rule.js packages/site/src/lib/ui → 0
$ pnpm check → biome : 2 erreurs hors lot (biome.jsonc useBiomeIgnoreFolder,
  createRedirectRoutes.test.tsx) ; fmt:check : 9 fichiers navigation/ (STY4b) ; check:other :
  `sites` 7 → 9 composants sans story (hors lot) ; mes fichiers propres (biome, oxfmt)
```

Premier passage après l'ajout des grammaires Shiki : Vite découvre `@shikijs/langs/*` en cours de
route et recharge (13 échecs), vert au passage suivant ; un cache neuf les trouve au scan initial.
Captures des nouvelles stories : à la porte de la vague
(`pnpm test:e2e:update:linux e2e/storybook.spec.ts`).

### TST1 — Stories de @recit/components — 29/10

Vague 4 du plan 21 (§8), en parallèle de STY4a-b, STY6.3a-b, STY8, TST2a-b. Aucune commande git, pas
de `pnpm install`, pas de `pnpm fix`.

**Stories.** 18 fichiers, 51 stories sous `Slides/` (13 avant). Les 9 existants gardent leurs
exports (les références e2e restent valables) et gagnent leurs états : `Callout` (`WithoutTitle`,
`LongContent`), `CodeBlock` (`LongCode` à 26 lignes, `WithoutLineNumbers`), `CodeGroup`
(`SwitchingTabs`, `SingleTab`), `Cover/LongTitle`, `Grid` (`ThreeColumns`, `Overloaded`),
`Media/MissingImage`, `Mermaid/InvalidChart`, `Points` (`WithoutTitle`, `ManyItems`), `Quote`
(`AuthorWithoutRole`, `LongQuote`). Nouveaux : `Card`, `Columns`, `Col` (`Left`/`Right`),
`SlideImage` (`cover`, `contain`, légende, image manquante), `Badge`, `Kbd`, `Step`, `SlideMotion`
et `Builtins/Composition` (le registre `BUILTIN_MDX_COMPONENTS` composé comme un auteur l'écrirait :
`builtins.tsx` exporte `SlideGrid`, le compteur y voit un composant). Une image manquante est une
URL `data:` indécodable : aucune requête n'échoue dans la console (la spec e2e la refuserait).

**`play`.** `CodeGroup` : premier onglet sélectionné, clic sur « yarn », `aria-selected` et panneau
seul visible ; `CodeBlock` : en-tête (fichier, langue) puis attente de Shiki (un mot-clé devenu un
nœud de texte à lui seul), et le code d'une langue inconnue reste du texte ; `Mermaid` : dessin
(étiquettes des nœuds, SVG nommé par le titre) puis message d'erreur sans dessin ; `Step` : indices
portés dans l'ordre de la source, tous visibles hors d'un deck ; plus `Card` (titre de niveau 3),
`Media`/`SlideImage` (image nommée par `alt`, légende), `Grid/Overloaded` (avertissement),
`SlideMotion/Nested` (le contexte imbriqué s'efface et l'entrée finit à opacité 1). Les `play`
interagissent par le `canvas` et le `userEvent` du contexte de la story, sans `expect` ni `fn` :
`@recit/components` n'a pas `storybook` en dépendance de dev, `storybook/test` se résout dans Vite
mais pas au typecheck du paquet (`TS2307`), et la consigne excluait d'ajouter une dépendance. Les
requêtes par rôle et par état (`selected: true`) valent assertion (elles lèvent) ; deux gardes
lèvent une `Error` à la main. Aucun rappel dans ces composants : pas de `fn()` à poser. **Pour la
suite** : `"storybook": "catalog:"` en dev de `@recit/components` (comme `packages/site` à TST0)
rendrait `expect` disponible.

**Ce que la loi change aux stories.** `withGrid` (`.storybook/decorators/withGrid.tsx` +
`grid.module.css`, `stack` ou `row` par `parameters.grid`, premier décorateur de `preview.tsx`)
dispose les variantes que la story rend côte à côte (`Callout/AllTypes`, `Badge/AllTypes`,
`Kbd/Shortcut`, `Step/Indexed`). Les cartes des stories `Grid` sont des `<Card>` (la composition
d'un `:::grid`) : `slides/Stories.module.css` (+ table) est supprimé, ses deux règles quittent
`dist/styles.css` du paquet publié (16 modules).

**Tags.** `wait-motion` sur `Cover`, `Media`, `Points`, `Quote`, `SlideMotion` (meta),
`wait-mermaid` sur `Mermaid/Flowchart` seule (l'erreur n'a pas de SVG) ; `PENDING_TAGS` retiré de
`e2e/storybook.spec.ts` (`tagsOf` lit `index.json`), entrée CodeBlock retirée de `a11yFindings.ts`
(liste vide).

**Contraste.** L'en-tête de `CodeBlock` prend la couleur atténuée du thème sombre dans les deux
modes (`hsl(215 20.2% 65.1%)`, 4,87:1 sur `#333434`) : le chrome est sombre partout. **Différence de
capture voulue** : `slides-codeblock--type-script-light` (en-tête plus clair) ; en sombre, la valeur
est celle d'avant. Autres différences voulues : `slides-grid--two-columns` et `--four-columns` (les
cartes de `deck.css` au lieu des cartes de fixture), et les 76 captures des stories nouvelles.

**Trouvé, hors lot.** axe `color-contrast` en `a11y.test: "todo"` dans la story, raison écrite :
`Badge` succès, attention, danger (`deck.css`, 2,72 à 3,81:1), l'avertissement de `Grid` (4,45:1),
le message d'erreur de `Mermaid` (3,14:1). `renderMermaidSvg` ajoute `role="img"` au SVG titré, mais
Mermaid y a déjà mis `role="graphics-document document"` : le second attribut est ignoré par le
parseur, le SVG reste un `graphics-document` (nommé, donc sans effet pour axe). `CodeBlock` n'a pas
de bouton copier : le `play` « copier » de §4.3 n'a rien à cliquer.

**Tests réduits.** `slides.spec.ts` ne garde que le contrat HTML partagé avec le pipeline Markdown
et les règles de `deck.css` (l'avertissement de surcharge et la ligne d'auteur de `Quote` sont aux
stories) ; `code-block.spec.tsx` rend les stories par `composeStories` (`renderToStaticMarkup`,
fallback échappé), l'en-tête passe au `play`.

**Vérifié.**

```
$ pnpm exec vitest run --project storybook packages/components → 18 fichiers, 51 tests
$ pnpm test --project storybook        → 44 fichiers (+1 ignoré), 161 passés, 5 todo/ignorés
$ pnpm exec vitest run packages/components → 23 fichiers, 83 tests
$ pnpm -F @recit/components build && typecheck ; pnpm -F recit-storybook build && typecheck → verts
$ node scripts/check-stories.js packages/components → 0 ; --write-baseline packages/components
$ node scripts/check-style-rule.js packages/components → 0
$ node scripts/build-css-modules.js --check --all → ✅
$ pnpm check  → rouge par d'autres lots : oxfmt sur navigation/NavigationItem.tsx et
  NavigationBadge.module.css (STY4b), check-stories `sites` 7 → 9 ; biome et imports verts
```

Pendant le lot, deux passages du projet `storybook` ont échoué par les autres : un export manquant
de `navigation/utils.ts` en cours d'écriture, puis `@recit/core/browser` introuvable pendant une
reconstruction de `dist`. Repassés verts sans rien changer. À la porte :
`pnpm test:e2e:update:linux e2e/storybook.spec.ts`, revue de galerie.

### Vague 4, raccords de l'orchestrateur — 29/10

- Deux régressions de la vague 3 trouvées par les stories de TST2a, corrigées dans `lib/ui` :
  `CodeTabs` gardait les panneaux cachés montés (`keepMounted` de Mantine), si bien que le bouton
  copier copiait tous les onglets ; `keepMounted={false}` rend le comportement de Radix. `Input` et
  `Textarea` perdaient le `aria-invalid` de l'auteur, que Mantine recalcule depuis `error` : `error`
  en est maintenant dérivé. Les stories `CopiesTheVisibleTab` et les trois `Invalid` sont réactivées
  (19/19).
- `NavigationMenuLink active` repose `aria-current="page"` comme Radix (trouvé par TST2b).
- TST1 : `storybook` manque en devDependency de `@recit/components` (les `play` n'y utilisent pas
  `expect`) ; à ajouter à la porte de la vague 4, hors de tout agent, puis passer les `play` sur
  `expect`.

### TST2b — Stories Site/UI, surcouches — 29/10

Vague 4 du plan 21 (§8, TST2), en parallèle de TST1, TST2a, STY4a-b, STY6.3a-b et STY8. Aucune
commande git, pas de `pnpm install`, pas de `pnpm fix`.

**En place.** Huit fichiers `packages/site/src/lib/ui/<X>.stories.tsx`, titre `Site/UI/<X>` :
`Dialog`, `Drawer`, `Popover`, `Tooltip`, `DropdownMenu`, `Command`, `Collapsible`,
`NavigationMenu`. 34 stories, toutes avec un `play` ; rappels en `fn()` d'`args` (`onOpenChange`, et
un `onSelect` d'`args` typé à côté du composant pour le menu et la palette).

- `Dialog`, `Drawer` : ouverture au clic et à Entrée, nom par le titre, `aria-modal`, focus piégé
  (Tab répété reste dans le dialogue), Échap ferme et rend le focus au déclencheur, clic dehors
  (l'élément sous le coin, `elementFromPoint` : l'overlay, sans sélecteur), bouton de fermeture,
  formulaire (champ `data-autofocus` focalisé, saisie, Enregistrer ferme) ; `Dialog` non fermable
  par clic extérieur (`onInteractOutside` annulé, seul Échap ferme) ; `Drawer` à droite et à gauche,
  bord mesuré.
- `Popover` : clic et Entrée ouvrent, focus dedans et piégé, Échap et clic dehors ferment, focus
  rendu ; positions `top-start`, `right`, `left`. `Tooltip` : survol et focus clavier ouvrent,
  `aria-describedby` (description accessible du déclencheur), Échap ferme ; positions.
- `DropdownMenu` : clic ouvre un `menu`, clic sur un item choisit et ferme, flèches (l'item
  désactivé est sauté), Entrée choisit, Échap ferme et rend le focus ; sous-menu par flèche droite ;
  cases (`menuitemcheckbox`) et radios (`menuitemradio`) avec `aria-checked`.
- `Command` (Spotlight) : la palette filtrée par l'importeur, comme la recherche du site ; dialogue
  nommé, `combobox` focalisé et `aria-controls` vers la `listbox`, deux groupes, flèches et
  `aria-activedescendant`, Entrée choisit et ferme (`onOpenChange(false)` une seule fois), saisie
  qui filtre, aucun résultat (état vide visible), pointeur (survol sélectionne, clic choisit),
  Échap, ouverture par l'importeur et focus rendu. `Ctrl+K` n'est pas à `CommandDialog`
  (`shortcut={null}`) : il appartient à `Search` (TST3).
- `Collapsible` : `aria-expanded`, `aria-controls` vers le contenu, clic, Entrée et Espace,
  désactivé. `NavigationMenu` : `navigation` nommée, survol ouvre la liste sous le déclencheur,
  Entrée l'ouvre, Échap la ferme sur le déclencheur, un lien la ferme.

**Tests.** `Command.test.tsx` ne garde que l'`asChild` de `CommandItem` (l'option rendue en lien, la
classe de l'item, `aria-activedescendant`) ; rôles, flèches, Entrée, fermeture signalée une fois et
état vide sont aux stories. `Overlays.test.tsx` perd `Dialog`, `Tooltip` et l'`aria-controls` de
`Collapsible` (aux stories) et garde l'API Radix des importeurs : `onCloseAutoFocus` et `id` du
`Drawer`, classes jointes et `data-align` du `Popover`, `asChild` d'un item de menu en lien,
`data-state` du `Collapsible`, classes du lien de `NavigationMenu`.

**`withMantine` sans `env="test"`.** Inutile dans Chromium : les flottants de Mantine s'y montrent
après leur transition ; `env="test"` ne sert qu'à happy-dom (les deux tests le gardent).

**Défauts trouvés** (composants hors lot, story en `a11y.test: "todo"` avec la raison) :

- axe `aria-required-children` sur un menu ouvert : `Menu.Dropdown` de Mantine pose sa sentinelle de
  focus (`<div role="presentation" tabindex="-1" data-autofocus>`) dans le `menu`
  (`DropdownMenu/Open`, `CheckboxesAndRadios`) ; dans `NavigationMenu`, le `menu` contient de plus
  des liens au lieu de `menuitem` (`Expanded`).
- axe `aria-required-children` : la `listbox` de `Command` sans résultat ne contient que l'état vide
  (`role="presentation"`), comme celle de cmdk (`NoResults`).
- axe `color-contrast` : `CommandShortcut` sur l'item sélectionné, 4,39:1 (`Filtered`), le même
  couple que le ⌘K de `Search` noté à TST0.
- `NavigationMenuLink active` ne pose plus `aria-current="page"` (Radix le faisait) : seulement
  `data-active`. Non affirmé par la story, à rétablir dans `NavigationMenu.tsx`.
- Sans largeur passée par l'importeur, un `Drawer` `left`/`right` couvre toute la page (comme celui
  de vaul) : pas de clic dehors possible ; la story `Right` ferme par Échap.

**Pièges.** Dans le projet Vitest, `parameters.layout` ne s'applique pas : le canevas est dans le
coin de la page, et Mantine retourne un flottant placé au-dessus ou à gauche ; seules les positions
avec de la place sont mesurées (les captures, centrées, montrent les autres). Un clic sur un lien
`href="#…"` d'une story fermait la page du navigateur de test (« Browser connection was closed ») :
les liens de `NavigationMenu` cliqués font `preventDefault`.

**Vérifié.**

```
$ pnpm test --project storybook        → 45 fichiers passés, 1 sauté ; 170 tests (un échec axe
  corrigé en todo, puis les 6 de Command verts) ; plusieurs passages perdus à des reconstructions
  de dist par d'autres lots (@recit/core, @recit/components) et à navigation/ en cours d'écriture
$ pnpm exec vitest run packages/site --project '!storybook' → 60 fichiers, 613 tests
$ pnpm -F @recit/site typecheck ; pnpm -F recit-storybook typecheck → muets
$ pnpm -F recit-storybook build        → vert ; 34 stories de ces huit composants dans index.json
$ node scripts/check-stories.js packages/site/src/lib/ui   → 0 (site/ui déjà retiré de la baseline)
$ node scripts/check-style-rule.js packages/site/src/lib/ui → 0
$ pnpm check                           → vert
```

Une assertion faussée (`onSelect` attendu avec un autre item) fait échouer `Command/Default`.
Captures des 34 stories : à la porte de la vague
(`pnpm test:e2e:update:linux e2e/storybook.spec.ts`).

### STY6.3a — @recit/deck — 29/10

Vague 4 du plan 19 (§10, STY6.3), en parallèle de STY4a-b, STY6.3b, STY8, TST1, TST2a-b. Aucune
commande git, pas de `pnpm install`, pas de `pnpm fix`.

**Fichiers.** `packages/formats/deck/src/**` : 11 modules et leurs tables (`Presentation`,
`PresenterView`, `SlideFrame`, `SlideStage`, `MdxSlide`, `HtmlSlide`, `DeckApp`, `AnnotationCanvas`,
`DevHud`, `MobileRemoteView`, `RemoteQrCodeModal`), les composants, deux tests
(`first-render.spec.tsx` lit `classes.content`, `first-paint.spec.tsx` nouveau) ;
`packages/core/src/config/theme-css.ts` et son test `theme-css-deck.spec.ts` ;
`scripts/style-rule-baseline.json` (les cinq dossiers du deck retirés). **Hors liste, une ligne** :
`packages/core/src/compile/compile-story.ts` passe `{ design, scenes }` à `buildThemeCss`.

**Premier rendu (§0.3 permis 4), aucun `useCssVariable` dans le deck.** `buildThemeCss` prend un
quatrième argument `{ design, scenes }` et émet, dans la portée du thème,
`--deck-design-width/height`, puis une règle par scène
`[data-slide-id="…"] { --slide-progress; --slide-bg }` (préfixée par la portée quand le récit est
embarqué, id échappé en chaîne CSS ; un `bg` qui contient `;`, `{`, `}` ou `<` est refusé avec le
diagnostic `invalid-scene-background`). `SlideStage` ne mesure plus rien : le cadre est un conteneur
`size`, l'échelle est `max(0, min(tan(atan2(100cqw - 2p, W)), tan(atan2(100cqh - 2p, H)), 1.5))`,
`W`/`H` lus dans les variables du récit, avec `data-aspect` (`16:9`/`4:3`, dérivé de `design`) comme
repli quand aucune feuille de récit n'est là ; `data-padding` (0, 4, 8, 24) remplace le `padding`
numérique (type `StagePadding`), la prop `maxScale` disparaît (personne ne la passait ;
`computeStageScale` reste, même formule). Plus de `visibility: hidden` au premier rendu : l'échelle
est juste dès la première peinture. `SlideFrame` : une classe par mise en page
(`Record<SlideLayout, …>` qui compose `slide slide-layout-x`), `data-bg` quand la scène a un fond
(`[data-bg] { background: var(--slide-bg) }`), barre de progression en
`calc(var(--slide-progress) * 100%)`. Le mode impression lit les mêmes variables de design. Sondé
sur l'hôte (Chromium, `deck-showcase` construit) : 1280×800 → échelle 0,9625 (1232/1280), 800×600 →
0,5875, console présentateur juste.

**Le reste.** Les classes de `deck.css` et de `print.css` restent dans le HTML par
`composes … from global` (`deck-viewport`, `deck-stage`, `print-slide`, `deck-hud`, `dev-hud`,
`annotation-canvas`, `remote-modal`, `speaker-notes-container`…), les sélecteurs e2e ne changent
pas. Aucune imbrication CSS : Lightning CSS sans `targets` la laisse native, la passe de préfixage
de `site-stories` et les consommateurs la verraient telle quelle. `display: contents` → classe
(`MdxSlide`, `HtmlSlide`). Laser d'`AnnotationCanvas` : dessiné sur un second canevas, repeint à
chaque mouvement, plus de `left/top` ; les canevas sont dimensionnés quand le calque s'affiche
(avant, l'effet ne tournait qu'au montage, quand le canevas n'existait pas encore : 300×150 étiré),
sans réécrire une taille inchangée (ce qui effacerait le stylo). `PresenterView` passe de la palette
slate aux tokens (`--background`, `--card`, `--muted`, `--border`, `--primary`) ; `MobileRemoteView`
et `RemoteQrCodeModal` gardent leurs valeurs relevées dans la feuille Tailwind compilée (slate en
oklch, `shadow-2xl`, `backdrop-blur-sm` 8px). `cx` de `@recit/components` relaie les `className`.

**Vérifications.**

```
$ node scripts/check-style-rule.js packages/formats/deck → total 0 (108 avant)
$ node scripts/check-style-rule.js --write-baseline packages/formats/deck → 643 in 24 directories
$ pnpm exec vitest run packages/formats/deck packages/core → 35 fichiers, 359 tests verts
  (un passage : « dual Shiki themes » en délai sous charge, vert seul)
$ pnpm exec nx run-many -t build typecheck -p @recit/deck @recit/core → 6 tâches vertes
$ node scripts/build-css-modules.js --check --all → @recit/deck: 11 CSS modules, tables up to date
$ pnpm exec nx run recit-example-deck-showcase:build → vert ; main.css porte
  recit-deck-SlideStage__frame{…container-type:size}, index.html les règles [data-slide-id]
$ pnpm check → biome sur deck et core vert ; échecs d'autres lots : `SyntaxError` d'une story
  (noShadowRestrictedNames), oxfmt sur 17 fichiers de site/site-kit, check-stories (sites 7 → 9)
$ BROWSER=none pnpm test:e2e:linux e2e/deck-features.spec.ts e2e/deck-render.spec.ts
    e2e/deck-behaviour.spec.ts e2e/site-components.spec.ts e2e/site-embed.spec.ts e2e/motion-dev.spec.ts
  80 passed, 28 failed (5.8m) ; deux passages avortés avant (tar sur un arbre en écriture, puis
  `www:build` sur Hero.module.css de STY8)
$ node scripts/screenshot-gallery.js → 0 changed screenshot(s)
```

Les 28 échecs sont les 14 pages de `site-components` × 2 modes, par la **hauteur** (2767 → 2779 px)
: le texte de la page a changé (« in any page of a site », pied de page sans l'ancien nom du
framework), les tableaux et l'en-tête aussi (lots du site) ; la démo du deck y est identique. La
galerie ne les liste pas (tailles différentes). `deck-features`, `deck-render`, `deck-behaviour`,
`site-embed` et `motion-dev` verts : les captures de la console présentateur (clair et sombre)
passent sous le seuil de 2 % (le fond sombre du thème par défaut est proche du slate), donc la
différence attendue par le plan n'a rien à faire régénérer. Aucune référence régénérée.

### STY8 — Sites et exemples — 29/10

Lot STY8 du plan 19, vague 4, sur `feature/mantine`. Aucune commande git.

**Fait.** Un module par composant, les valeurs des utilitaires Tailwind d'avant reprises valeur par
valeur (lues dans la feuille compilée de `sites/docs`).

- `sites/www/src` : `Hero`, `FormatGrid`, `OneSource` (`SideBySide`, `Caption`) + leurs modules.
  `not-prose` par `composes: not-prose from global` ; règles en `@layer components`, sous les
  `utilities` de Tailwind, pour que le `@utility prose` de `main.css` (`.prose a`, `.prose code`,
  qui ignorent `not-prose`) continue de gagner comme avant (STY5 le retire). Points de rupture : le
  `md` de Tailwind est le `sm` de Mantine (mixin) ; `sm` (40em) et `lg` (64em) n'ont pas
  d'équivalent Mantine, écrits en `@media (min-width: …)` comme les modules de `packages/site`. Les
  deux logos (`dark:hidden`/`dark:block`) et `dark:invert` passent par `@mixin dark`. Un `composes`
  vers une classe définie dans un `@layer` échoue au build
  (`postcss-modules-scope: referenced class name not found`) : sélecteurs groupés à la place.
- `sites/docs/src/StatusBadge`, `sites/components/src/FormatSupport` : `data-status`, couleurs
  emerald/amber de la palette Tailwind en `oklch()` et `light-dark()`. `StatusBadge` reste un
  `<span>` et non le `Badge` de Mantine que le plan nommait (hauteur, padding et majuscules
  changeraient les tableaux).
- `sites/motion/src/VideoDownloads` : module.
- `examples/deck-mdx/components` : `Timeline.module.css` (CSS brut pour esbuild, jetons
  `@recit/theme`) ; `examples/deck-showcase/components` : `Metric.module.css` et `css-modules.d.ts`.
  `.value` garde `composes: metric-value from global`, le crochet que `deck-showcase/styles.css`
  (`customCss`) et la page `docs/theme/custom-css` montrent ; la règle `.metric-value` vit désormais
  dans le module. `Timeline` et `Metric` restent dans `index.tsx` : en fichiers séparés,
  `check-stories.js` les compte (« sites » 7 → 9) et une story demanderait deux références Storybook
  nouvelles ; à faire avec TST.
- `sites/components/src/stories.css` supprimé (sa règle est dans `Metric.module.css`), import retiré
  de `sites/components/recit.config.ts` (hors liste, une ligne). `media.mdx` :
  `className="media-demo"` retiré à la main (aucune règle ne l'a jamais visé).
- Inchangés : `sites/{motion,presentation}/styles.css` (`.typography` attend STY5),
  `packages/tools/site-kit/src/config.tsx` (rien à changer, vérifié).

**Vérifié.**
`node scripts/check-style-rule.js sites examples/deck-mdx/components examples/deck-showcase/components`
: 44 → 3 (les 3 de `sites/storybook/.storybook`, hors lot) ; baseline réécrite pour
`sites/{www,docs,motion,components}/src` et les deux `components`.
`nx run-many -t build typecheck -p www docs motion components presentation recit-example-deck-mdx recit-example-deck-showcase`
vert (après des échecs transitoires : `dist` de `core`, `components`, `site-stories` reconstruits
par d'autres lots pendant le build). `recit build` des deux decks : tables
`{root:"e",item:"i",step:"o"}` et `"metric-value e"`. `pnpm check` vert (avertissements biome
`noUnknownAtRules` sur `@mixin`, comme les modules de `packages/site`).

**e2e conteneur** (`site-www`, `site-docs`, `site-motion`, `site-components`, `deck-render`) : 175
passés, 60 échecs. `deck-render` entier vert (Timeline, Counter, Metric identiques dans les decks),
`components-present-*` verts (Metric dans le site identique, `stories.css` en moins). Les 60 échecs
sont toutes les captures de **pages** de site, clair et sombre, plus hautes de 12 à 112 px, textes
décalés d'1 px jusque dans l'en-tête et le pied de page (hors lot : mise en page et typographie des
lots concurrents de la vague 4), plus des changements de contenu postérieurs aux références (« in
any page of a site », « Documentation pages in a site », date de dernière modification). Mes zones
recadrées et comparées (héros, grille des formats, tableau `FormatSupport`) ne diffèrent que par ce
décalage global. La galerie affiche 0 : elle ne retient pas les échecs de taille d'image. Aucune
référence régénérée.

### Point de reprise — plan 19, vague 4 interrompue — 29/10 au soir

Travail arrêté à la demande, aucun agent ni conteneur ne tourne. Branche `feature/mantine`, dernier
commit `0ac806b9` (vagues 0 à 2) ; tout le reste est **dans l'arbre de travail, non commité** (≈ 414
fichiers). Compteur `node scripts/check-style-rule.js` : 147 restants (baseline à abaisser par
`--write-baseline` à la porte).

**Fait et vérifié** : vagues 0 à 3 (porte de la vague 3 : e2e 459/459, références `plans-home` et
`/demos/mdx` régénérées).

**Vague 4, fait** (rapports dans les notes ci-dessus) : TST1, TST2a, TST2b, STY6.3a, STY8, plus les
raccords de l'orchestrateur (`CodeTabs` `keepMounted={false}`, `aria-invalid` d'`Input`/`Textarea`,
`aria-current` de `NavigationMenuLink`).

**Vague 4, à terminer** : trois lots coupés en plein travail, leurs modifications partielles sont
sur le disque, sans note de journal :

- **STY4a** pages et mise en page (`packages/site/src/lib/components` hors `navigation/`,
  `app/main.tsx`, `Layout.module.css`) ; supprimer `lib/ui/util.tsx` quand `index.ts` et
  `Search.tsx` ne l'importent plus ; contraste du « ⌘K » de `Search` (et du raccourci de
  `Command/Filtered`).
- **STY4b** navigation et plugins (`lib/components/navigation/*`, `plugins/markdown/MdxPage.tsx`,
  `plugins/search-pagefind/*`, `search-inkeep`, `Slot.tsx`) ; enquête sur le clic de pagination
  perdu pendant l'hydratation (le test e2e réessaie en attendant).
- **STY6.3b** `site-kit` composants, `site-stories`, `packages/cli/src/motion/client.tsx`,
  `motion-dev.ts`.

Chaque reprise commence par inventorier le partiel (`git status --short <dossiers>`, `git diff`), le
garder s'il est sain, puis finir selon le lot du plan 19 §10 et les règles du plan 20.

**Porte de la vague 4, à faire ensuite**, dans cet ordre :

1. ~~Galerie à réparer~~ — **réparée le 29/10 au soir** : Playwright nomme ses pièces jointes
   `<nom>-actual.png` et le script n'acceptait que `<nom>-actual`, si bien qu'aucune entrée n'a
   jamais été retenue (le test du script fabriquait des noms sans extension). L'expression accepte
   l'extension, l'« avant » vient de la copie `-expected.png` du dossier de l'essai (repli sur la
   référence du dépôt), et le test utilise les vrais noms. Conséquence : les « galeries vides » des
   vagues 0 à 3 ne prouvaient rien, mais les tests de capture verts, eux, valent (identiques au
   seuil près) ; seule la vue avant/après des échecs manquait.
2. **Hauteur des pages de site** : STY6.3a et STY8 voient toutes les pages de site plus hautes de 12
   à 112 px avec un texte décalé d'1 px jusque dans l'en-tête et le pied de page, pendant que
   STY4a-b tournaient. À attribuer (STY4a-b probablement) et ramener à l'identique ; distinguer des
   changements de contenu (textes modifiés depuis les références).
3. Ajouter `"storybook": "catalog:"` en devDependency de `@recit/components` (un `pnpm install`,
   hors de tout agent), puis passer les `play` de TST1 sur `expect` de `storybook/test`.
4. `pnpm check`, e2e complet `BROWSER=none pnpm test:e2e:linux`, revue des captures, puis
   `pnpm test:e2e:update:linux e2e/storybook.spec.ts` (≈ 110 nouvelles stories de TST1, TST2a-b) et
   les différences voulues : en-tête `CodeBlock` des slides en clair (contraste), `Grid` à 2 et 4
   colonnes (vraies `Card`), « ⌘K ».
5. Mettre à jour le tableau d'état de `19-style.md` §10 et écrire la note de porte.

**Défauts trouvés, en `todo` dans les stories** (à confier à un lot) : contrastes `Badge`
succès/attention/danger (`deck.css`), avertissement de `Grid`, erreur de `Mermaid`, `Alert`
destructive et info, onglet inactif de `CodeTabs`, orange Shiki `github-light` ; axe
`aria-required-children` sur les menus Mantine ouverts et `Command` vide ; `aria-orientation` d'un
`Separator` décoratif ; `role="img"` du SVG Mermaid ignoré ; `CodeBlock` des slides sans bouton
copier ; `Drawer` latéral sans largeur couvre la page ; stories manquantes pour `Timeline`,
`Metric`, `Counter` (exemples). Changement d'API publié à citer dans le version plan (STY9.4) :
`SlideStage` perd `maxScale`.

**Ensuite** : vague 5 (STY5a typographie, STY5b démontage de `main.css` ; TST3a-b, TST4a-b), 5'
(STY5c retrait de Tailwind), 6 (STY9 bascule finale, STY10.2, TST5), selon `20-multi-agents.md` §4
et `21-tests.md` §8.1. Règles pour les agents : `20-multi-agents.md` (conteneur e2e un à la fois,
attente par `docker ps --filter ancestor=mcr.microsoft.com/playwright:v1.64.1-noble`, jamais
`pgrep -f`/`pkill -f` sur un motif de sa propre ligne, jamais `docker stop`, aucune régénération de
référence hors porte).

### Biome, la loi du style et les sites en Markdown — 29/10

- `biome.jsonc` : erreurs pour les imports de l'ancienne pile (`clsx`, `class-variance-authority`,
  `tailwind-merge`, `cmdk`, `vaul`, `next-themes`, `@radix-ui/*`, `@base-ui/*`, `util/cn`), sauf les
  cinq derniers importeurs (`Typography`, `Heading`, `shiki.ts`, `cn.ts`, `index.ts`, liste qui ne
  fait que décroître, vidée par STY5c) ; `@mixin` permis là où PostCSS Mantine tourne
  (`packages/site`, `sites`, `site-kit`) et refusé en erreur dans les paquets publiés et les
  exemples de deck ; `!important` permis dans `print.css` et `deck.css` ; `!**/.recit` sans `/**`.
- Plugins GritQL dans `scripts/biome-plugins/` : `style-law.grit` (un `className` chaîne, gabarit ou
  concaténation, et toute prop `style`, en avertissement jusqu'à STY9, hors couche d'animation) et
  `sites-markdown-only.grit` (un composant exporté dans un site est une erreur, Storybook à part).
- Règle voulue par l'utilisateur : **un site, c'est du Markdown et de la config** ; tout composant
  vit dans un paquet (`@recit/site-kit`). Six composants antérieurs sont tolérés dans `biome.jsonc`
  (`www` : `Hero`, `FormatGrid`, `OneSource` ; `docs/StatusBadge` ; `motion/VideoDownloads` ;
  `components/FormatSupport`) : **lot à prévoir après STY6.3b** pour les déplacer dans
  `packages/tools/site-kit/src/components/` (avec leurs modules) et vider la liste.
- Trou du plan 19 relevé à l'audit : `packages/site/src/lib/assets/language-icons` (27 signaux du
  compteur) n'appartient à aucun lot ; à joindre à STY5.
- 41 avertissements Biome (57 avant), aucune erreur.

### STY6.3b — site-kit, site-stories, client motion — 29/10

Vague 4 du plan 19 (§10, STY6.3), reprise d'un lot coupé : le partiel des deux agents précédents
était sain et presque complet, gardé tel quel après relecture. Aucune commande git, pas de
`pnpm install`, pas de `pnpm fix`, aucune référence régénérée.

**Fichiers.** `packages/tools/site-kit/src/components/` : `Cards`, `ExampleHeader`, `SourceFile`,
`Tutorial` et leurs quatre modules (paquet privé, compilé par le Vite du site comme `@recit/site` :
PostCSS, `rem()`, mixins Mantine, règles en `@layer components`).
`packages/integrations/site-stories` : `site-stories.css` remplacé par `src/site-stories.module.css`
et sa table (`scripts/build-css-modules.js`), `MotionViewer`, `StoryDocument`, `StoryViewer`,
`mdx-components`, `i18n`, `motion/MotionPlayer`, `scripts/build-css.mjs` (le module n'est plus
préfixé par `.recit-wrapper` : ses sélecteurs portent leur portée), deux tests adaptés.
`packages/cli/src/motion/client.tsx` + `client.module.css` (+ `css-modules.d.ts`), bundlé par
esbuild (`local-css`) et servi après la feuille des blocs ; `motion-dev.ts` perd le `<style>` de la
page ; `motion.spec.ts` vérifie l'ordre et l'absence de `<style>`.

**Choix.** Un seul module pour `site-stories` (chaque composant dessine le même wrapper). Les
classes publiques restent dans le HTML par `composes … from global` (`recit-wrapper`, `recit-embed`,
`recit-embed-actions`, `recit-gallery-card`, `recit-motion`, `recit-motion-player`,
`recit-document`), les sélecteurs e2e et `sites/motion/styles.css` ne changent pas. Plus de style en
ligne : l'aspect d'un film et d'un deck passe par `data-aspect` (énumérations `16:9`/`1:1`/`9:16` et
`16:9`/`4:3`), la scène et la barre du client motion par `useCssVariable` de `@recit/components`.
Corrigé à la reprise : l'import de `client.tsx` placé avant l'en-tête de licence, et `prose-sm` du
corps de `TutorialStep` — composé depuis le module, Tailwind ne le voyait plus (il ne scanne pas le
CSS) et ne le générait pas ; un commentaire de `Tutorial.tsx` le nomme jusqu'à STY5 (le texte des
étapes de motion.recit.dev reprenait la taille `prose`, page +12 px).

**Vérifications.**

```
$ node scripts/check-style-rule.js packages/tools/site-kit packages/integrations/site-stories packages/cli/src
  → 2 (packages/cli/src/bundle : imports Tailwind du bundle des decks, hors lot, STY5c/STY9)
$ node scripts/check-style-rule.js --write-baseline (mêmes dossiers) → baseline inchangée
$ pnpm exec nx run-many -t build typecheck -p @recit/site-kit @recit/site-stories @recit/cli → vert
  (deux passages rouges avant : dist de @recit/core vidé par le build concurrent d'un autre agent)
$ pnpm exec vitest run packages/cli packages/integrations/site-stories packages/tools/site-kit
  → 21 fichiers, 202 tests verts
$ node scripts/build-css-modules.js --check --all → @recit/site-stories: 1 CSS module, à jour
$ pnpm check → vert (avertissements `@mixin` de Biome, comme les modules de packages/site)
$ BROWSER=none pnpm test:e2e:linux site-components site-embed motion-dev site-motion
  → 101 passed, 4 failed ; après le correctif `prose-sm` : site-components + site-motion
    94 passed, 2 failed
```

Les deux échecs restants : `/custom-components` (clair et sombre) voit une erreur React #418 (texte
différent à l'hydratation) dans le conteneur ; capture identique. Le même test passe sur l'hôte
(`playwright test` contre `recit-site preview` du site construit), et une sonde du build de
production n'y voit aucune erreur : hors de ce lot, à attribuer à la porte (la page affiche « Last
modified on », date issue de git, que le conteneur n'a pas ; l'erreur était masquée avant par les
échecs de capture qui précèdent l'assertion). `site-motion` vert : la page d'accueil ne diffère plus
que par la police des films, désormais chargée (la référence avait le repli serif), et le temps
`00:01` au lieu de `0:01` (`formatDuration` de `@recit/core`, hors lot), sous le seuil. `site-embed`
et `motion-dev` verts.

### Déploiement Vercel : doc, commandes, VERCEL.md — 29/10

- Page de doc `documentation/configuration/deploy-vercel.mdx` (barre latérale « Configuration ») :
  ce qui part chez Vercel (`dist/` d'un deck, `.vercel/output` d'un site du framework, détecté par
  `VERCEL`), `vercel.json`, déploiement par Git (Root Directory, preset « Other », Ignored Build
  Step), par la CLI (`vercel pull` / `build` / `deploy --prebuilt`, `link --repo` en monorepo),
  domaines, et les sites de Récit.
- `scripts/vercel.js` (+ `scripts/__tests__/vercel.spec.ts`, 7 tests) et les scripts racine
  `vercel:sites`, `vercel:link`, `vercel:deploy`, `vercel:deploy:prod` : un projet `recit-<site>`
  par site qui a un `vercel.json`, construit ici puis envoyé `--prebuilt`.
- `VERCEL.md` à la racine : la procédure complète (projets, liaison, déploiement, DNS).
- Rien n'a été déployé ni testé contre Vercel (pas de compte relié ici). La nouvelle entrée de la
  barre latérale change les captures des pages de docs où « Configuration » est dépliée : à accepter
  à la porte de la vague 4, comme changement de contenu.

### STY4a — Pages et mise en page — 29/10

Reprise du lot coupé (plan 19, vague 4), sur `feature/mantine`. Aucune commande git, pas de
`pnpm install`, pas de `pnpm fix`.

**Inventaire du partiel.** Le travail sur disque était complet et sain : les 26 composants du
périmètre (`LandingPage`, `Layout`, `Main`, `Header`, `HeaderNavigation`, `TopNavigation`,
`MobileTopNavigation`, `MobileTopNavigationDrawer`, `Footer`, `Banner`, `PageProgress`,
`BrowserWindow`, `Framed`, `Anchor`, `Pagination`, `Search`, `DeveloperHint`, `CategoryHeading`,
`AiAssistantMenuItems`, `NotFoundPage`, `ErrorPage`, `LanguageIcon`, `ScrollableTable`,
`InlineCode`, `Markdown`, `Mermaid`), `errors/ErrorAlert` et `errors/RouterError` (prop `placement`
au lieu d'un `className`, `app/main.tsx` passe `placement="route"`), chacun avec son module, et
leurs tests sur `classes.*`. `lib/ui/util.tsx` est supprimé (plus aucun import ; `index.ts`
réexporte `cn` depuis `lib/util/cn.js`). Rien n'a été effacé ni réécrit : relu, vérifié, complété
par la baseline et cette note. **Hors liste, déjà sur disque** : `vite/plugin-theme.ts` (+ test)
émet `--recit-logo-width/height`, `--recit-footer-logo-width/height` et `--recit-banner-color`
depuis `site` (valeurs nécessaires à la première peinture, §0.3 permis 4 ; valeur numérique → `px`,
valeur avec `;{}<>` refusée).

**« ⌘K »** : dessiné dans la couleur du texte (`Search.module.css`), 4,39:1 → conforme ; `a11y todo`
retiré de `Header.stories.tsx`. Différence voulue (le raccourci passe de gris à noir/blanc). Pas de
jeton partagé : le `todo` de `Command/Filtered` (`CommandShortcut`, `--mantine-color-dimmed` sur
`--muted`) reste, à confier au propriétaire de `Command.module.css`.

**Hauteur des pages (STY6.3a, STY8).** Elle ne vient pas de ce lot. Analyse ligne à ligne des
`-actual` contre les références : l'en-tête, la grille de la page, la colonne de contenu et le pied
de page sont identiques au pixel, hors deux points ci-dessous. Les +96 / +112 px de `tutorial` et
`formats/deck/exporting` venaient de `TutorialStep` de `site-kit` (texte en `text-base` au lieu de
`text-sm`, STY6.3b en cours) : verts au second passage. Les −1 px restants (`/`, `writing/overview`,
accueil de `www`) naissent dans les cartes de `site-kit` (`FeatureCard`, « Learn more » et
interlignes des descriptions) et dans les boutons du héros de `www`. Le reste est du contenu : «
embedded in a Récit site », barre latérale « Writing » (Scenes), `FormatGrid` (« Documentation pages
in a site »), pied de page sans l'ancien nom du framework. Le texte des liens de la navigation
d'en-tête est 1 px plus haut (1 523 px, sous le seuil) : **pas** dû à ce lot. Remettre `Header.tsx`
et `HeaderNavigation.tsx` de `HEAD` (Tailwind) dans une construction de `docs` donne le même
décalage, qui vient donc de `ui/NavigationMenu` (STY3b) ; les boîtes sont identiques (lien à
`top: 14.5`, 36 px), seul l'arrondi du texte diffère.

**Vérifications.**

```
$ node scripts/check-style-rule.js packages/site/src/lib/components → 5 (Heading 4, Typography 1 :
  STY5) ; packages/site/src/lib/errors → 0
$ node scripts/check-style-rule.js --write-baseline packages/site/src/lib/components
  → className 146 → 2, style 11 → 0, imports 5 → 1, noModule 28 → 2
$ pnpm -F @recit/site typecheck → seule erreur : plugins/markdown/MdxPage.test.tsx (STY4b)
$ pnpm exec vitest run packages/site --project '!storybook' → 60 fichiers, 613 tests verts
$ pnpm test --project storybook → 46 fichiers, 170 tests verts
$ pnpm exec nx run-many -t build -p docs www presentation → 18 tâches vertes
$ pnpm check → vert
$ BROWSER=none pnpm test:e2e:linux e2e/site-docs.spec.ts e2e/site-www.spec.ts
    e2e/site-behaviour.spec.ts e2e/accessibility.spec.ts → 128 passés, 6 échecs (1.7m)
```

Les 6 échecs : `/`, `writing/overview` et l'accueil de `www`, clair et sombre, −1 px de hauteur
(cartes de `site-kit`, contenu, voir plus haut). `site-behaviour` et `accessibility` verts. Aucune
référence régénérée.

### STY4b — Navigation et plugins — 29/10

Reprise du lot coupé (plan 19, vague 4), sur `feature/mantine`. Aucune commande git, pas de
`pnpm install`, pas de `pnpm fix`, aucune référence régénérée.

**Inventaire du partiel.** Sain et presque complet, gardé après relecture : les 11 composants de
`lib/components/navigation` avec leurs modules (`NavigationListItem.module.css` et
`navigationListItem.ts` remplacent le `cva` de `utils.ts`, que Node charge par `plugin-docs.ts` et
qui ne peut donc pas importer de module ; `NavigationCategory` passe de
`@radix-ui/react-collapsible` à `Collapse` de Mantine, animé seulement après un clic comme avant ;
`NavigationBadge` par `data-color`/`data-invert`, `ColorMap` disparaît, sans importeur),
`plugins/markdown/MdxPage` et `plugins/search-pagefind/{IndexingDialog,PagefindSearch,ResultList}`
avec leurs modules, deux tests adaptés (`MantineProvider` autour de `Navigation.test.tsx`).
`VisuallyHidden` vient de Mantine (`MobileNavigationDrawer`, `PagefindSearch`) ;
`NavigationWrapper`, `NavigationFrames` et `StackDrillRow` perdent un `className` sans appelant.
`search-inkeep` lisait déjà `data-mantine-color-scheme` (commit `0ac806b9`), `Slot.tsx` n'a rien à
changer.

**Terminé à la reprise.** `MdxPage.test.tsx` : le typecheck refusait `classList.contains` sur une
classe de module (`string | undefined`). `Toc` : l'entrée lue porte `aria-current="location"`, les
autres `"false"` — `NavLink` les marquait toutes `page`, et la classe `text-primary` que le test e2e
« the table of contents marks the section scrolled to » lisait en repli n'existe plus
(`activeTocEntry` de `e2e/support.ts` peut perdre `a.text-primary`).

**Le clic perdu de la pagination (porte de la vague 3).** Cause trouvée, corrigée dans
`NavigationWrapper`. Au montage, donc à l'hydratation, il amenait l'entrée active de la barre
latérale par `scrollIntoViewIfNeeded` → `scrollIntoView({ block: "center" })`, qui fait défiler
**tous** les ancêtres, la fenêtre comprise. Page descendue jusqu'à la pagination, la barre latérale
collante est poussée vers le haut par la fin de sa ligne de grille : sur `quickstart` (1280×720)
l'entrée active est à `top: -17` px, hors de la fenêtre, et l'ancien code remontait la page de 423
px (`scrollY` 1916 → 1493). Playwright venait d'amener le lien « Tutorial » à l'écran et cliquait à
ses anciennes coordonnées, sur autre chose : URL inchangée, aucune erreur. Le correctif
(`centerInNav`) ne fait défiler que le `nav`, et seulement si l'entrée sort de sa zone visible.
Sonde sur le `dist` (scripts du site retardés de 1,5 s, lien amené à l'écran avant l'hydratation) :
`scrollY` reste à 1916 après l'hydratation et le clic mène à `tutorial`. Le réessai du test reste.

**Vérifications.**

```
$ node scripts/check-style-rule.js --strict packages/site/src/lib/components/navigation packages/site/src/lib/plugins → 0
$ node scripts/check-style-rule.js --write-baseline (mêmes dossiers) → baseline inchangée (déjà sans eux)
$ pnpm -F @recit/site typecheck ; pnpm -F recit-storybook typecheck → verts
$ pnpm exec vitest run packages/site --project '!storybook' → 60 fichiers, 613 tests verts
$ pnpm test --project storybook → 46 fichiers, 170 tests verts
$ pnpm exec nx run-many -t build -p docs www → 17 tâches vertes (un échec transitoire avant :
  dist vidés par le build concurrent d'un autre lot)
$ pnpm check → biome et imports verts, check:other vert ; fmt:check rouge sur 4 fichiers .md/.mdx
  d'autres lots (SKILL.md ×2, AGENTS.md, deploy-vercel.mdx)
$ BROWSER=none pnpm test:e2e:linux e2e/site-docs.spec.ts e2e/site-behaviour.spec.ts
    e2e/accessibility.spec.ts → 116 passés, 4 échecs
```

Les 4 échecs : `/` et `writing/overview`, clair et sombre, −1 px de hauteur, déjà attribués par
STY4a (cartes de `site-kit`, contenu, texte de l'en-tête). Dans mes zones : la table des matières de
`/` est identique au pixel, la barre latérale de `writing/overview` ne diffère que par son contenu
(« Scenes » au lieu de « Slides »). Différence voulue : aucune visible ; `aria-current` de la table
des matières.

### Porte de la vague 4 — 30/10

- e2e complet (12,4 min, 767 tests) : 447 passés, 318 échecs triés. 306 stories nouvelles sans
  référence (générées ensuite par `pnpm test:e2e:update:linux e2e/storybook.spec.ts`) ; 8 captures
  de pages à −1 px (`docs` `/` et `writing/overview`, `presentation/templates`, `vision /`, `www`
  accueil) dont le décalage naît dans la description des `FeatureCard` de `site-kit` et dans le
  héros de `www` (lot GATE4, et TST4b pour le héros) ; `site-i18n` cherchait la classe `sr-only`
  disparue : il cherche maintenant le titre par son rôle. `e2e/support.ts` : `activeTocEntry` ne lit
  plus que `aria-current="location"`. `/custom-components` (erreur #418 vue par STY6.3b) n'a pas
  échoué dans ce passage.
- `storybook` en devDependency de `@recit/components` (un `pnpm install` hors agents).
- Boucle courte : `pnpm test:related`, `pnpm test:stories`, `pnpm test:e2e:host` ; les agents ne
  lancent plus le conteneur (`AGENTS.md` « Inner loop », `20-multi-agents.md` §3.2 bis).
- Vague 5 lancée avec GATE4 (corrections 1 px, contraste de `Command`, `expect` des stories de TST1)
  et INFRA (e2e Docker ciblé et chaud, ressources plafonnées, lefthook).

### Références Storybook de la vague 4 — 30/10

- `pnpm test:e2e:update:linux e2e/storybook.spec.ts` : 308 références nouvelles écrites (313 tests
  passés en 4,1 min). Échantillon relu : boutons, `Card` des slides, `Dialog` ouvert.
- **Défaut vu** : la `Dialog` ouverte montre un petit point gris dans son coin haut gauche (en
  sombre et en clair), sans doute une sentinelle de focus ou un bouton de fermeture sans icône rendu
  visible : à corriger dans `lib/ui/Dialog` (puis références à régénérer).
- 28 échecs de `play` dans le Storybook construit (stories de code de `lib/ui` vides, presse-papiers
  refusé, `slides-grid--overloaded`) : lot SBFIX.

### INFRA — e2e plus rapide, hooks git — 30/10

- **Ciblé** : `scripts/e2e-projects.js` donne, pour chaque spec de `e2e/`, les projets nx qu'elle
  ouvre (plus `@recit/cli`, importé par `e2e/support.ts`). `pnpm test:e2e:linux <specs>` passe la
  liste au conteneur (`E2E_PROJECTS`) : il ne construit qu'eux et leurs dépendances, et
  `playwright.config.ts` ne démarre que leurs `webServer` ; sur l'hôte, la config lit les specs de
  sa ligne de commande (`pnpm test:e2e:host e2e/site-docs.spec.ts` ne sert que `docs`). Sans spec,
  ou avec un filtre inconnu de la table : tout, comme la CI (inchangée : `pnpm test:e2e` sans
  argument). Un test exige une ligne par spec et chaque `siteUrl(…)` dans sa ligne.
- **Chaud** : la copie vit dans le volume `recit-e2e-work` ; chaque passage efface tout sauf les
  `node_modules` (et leurs `.vite`/`.cache`), recopie l'arbre, relance
  `pnpm install --frozen-lockfile` (« Already up to date », 2 s). Verrou `flock` : un second passage
  s'arrête net. La copie tolère un fichier modifié pendant le `tar` (code 1), et la recopie de
  `test-results` n'est plus fatale (un `test:e2e:host` concurrent recrée le dossier : vu deux fois
  ici).
- **Plafonné** : `--cpus=${E2E_CPUS:-6}`, `--memory=${E2E_MEMORY:-12g}` sans swap en plus,
  `E2E_WORKERS` = 2/3 des CPU (4).
- **Mesures** (`e2e/site-docs.spec.ts`, machine chargée par les autres agents) : avant, copie 2 s,
  installation 35 s, build de tout en échec après 3 min 40 (voir plus bas) ; après, à froid :
  nettoyage 0 s, copie 3 s, installation 47 s, build `docs` + 14 dépendances 2 min 04, tests 2 min
  33 ; à chaud : nettoyage 1 s, copie 2 s, installation 2 s, build 1 min 58, tests 2 min 44 (97
  passés, 8 captures en écart dues aux lots en cours, 1 instable).
- **Hooks** (`lefthook.yml`, `scripts/pre-push.sh`) : pre-commit = Biome + oxfmt sur les fichiers
  indexés, `check-style-rule.js` (2 s) et `check-stories.js` (0,1 s) ; pre-push = `pnpm check` puis
  typecheck `nx affected --exclude-task-dependencies` (34 à 55 s quand tout est touché) et
  `pnpm test:related` sur les fichiers poussés (59 s pour les 72 du dernier commit). Base : l'amont,
  sinon `origin/main`, sinon message et succès. `PRE_PUSH_BASE=HEAD~1 bash scripts/pre-push.sh` pour
  l'essayer ; `LEFTHOOK=0`, `--no-verify`, `LEFTHOOK_EXCLUDE=related`.
- **À traiter (hors lot)** : `sites/storybook/src/harness/pagefind` est ignoré par `.gitignore`
  (`pagefind/`) et exclu de la copie : le build de `recit-storybook` échoue dans le conteneur («
  Failed to load static files ») et échouera en CI tant que le dossier n'est pas versionné ou
  généré.

### STY5a — Typographie — 30/10

Vague 5 du plan 19 (§2.6, STY5.0), sur `feature/mantine`. Aucune commande git, pas de
`pnpm install`, pas de `pnpm fix`, aucune référence régénérée, conteneur e2e non lancé.

**Fait.**

- `Typography.tsx` : `Typography` de Mantine, `unstyled`, avec `Typography.module.css` ;
  `ProseClasses` disparaît (aucun importeur). Le module reporte règle à règle, dans l'ordre de la
  feuille compilée, ce que `@tailwindcss/typography` (`prose`, `dark:prose-invert`) et les
  `@utility typography`/`@utility prose` de `main.css` donnaient : `.prose` devient `.root`,
  `--tw-prose-*` devient `--prose-*`, l'exclusion `[class~="not-prose"]` reste (les modules qui
  composent `not-prose` en dépendent). Palette du site (`currentColor`), identique en clair et en
  sombre, sauf les trois couleurs que `typography` laissait au plugin (ombres de `kbd`, `pre` brut),
  en `light-dark()`. Règles en `@layer utilities`, là où était le plugin : les modules en
  `components` perdent contre elles comme avant, ceux hors couche gagnent toujours. La classe
  globale `typography` reste sur la racine (`composes`), crochet des `styles.css` de `motion` et
  `presentation`.
- `Heading.tsx` : `Title` de Mantine, `unstyled`, niveau lu par `data-order` dans
  `Heading.module.css` (tailles du `cva` en `@layer utilities`, au-dessus des `h1`–`h4` de la prose
  comme avant ; interlignes en rapport tenu par `--heading-lh`, sinon le minifieur arrondit
  `calc(2 / 1.5)` et `h2` perd 1/64 px) ; lien d'ancre et icône hors couche, comme `Anchor`.
  `theme.headings` **n'est pas** utilisé : avec la prose au-dessus du `Title` stylé, il n'aurait
  rien porté, et le thème est hors lot ; `Title` écrit encore ses variables `--title-*` en `style`
  (code de Mantine, sans effet).
- `MdxComponents.tsx` + `MdxComponents.module.css` : `p` explicite, `img`/vidéo → `.media`
  (`rounded-lg`), `pre` → `.pre` (`not-prose` composé, `my-4`), `code` en ligne sans la classe
  `inline` (valeur par défaut de `code`), `cx` au lieu de `cn` ; `node` de react-markdown retiré de
  `p` et `pre`.
- `main.css` : `@plugin "@tailwindcss/typography"` et les deux `@utility` retirés (le reste est à
  STY5b, qui a démonté le fichier en parallèle ; la dépendance part avec STY5c).
- `biome.jsonc` : `Typography.tsx` et `Heading.tsx` sortis de la liste d'exception.
- Hors liste, signalé : `site-kit` `Tutorial.module.css`/`Tutorial.tsx` composaient
  `prose prose-sm from global` ; sous la liste `not-prose`, seul ce que le plugin posait sur le
  corps lui-même arrivait (gris-700/gris-300 du plugin, `0.875rem`/`1.71429`) : ces trois valeurs
  sont maintenant dans `.body`, le bloc `--tw-prose-invert-*` et le commentaire qui gardait
  `prose-sm` dans la feuille disparaissent. `MdxPage.test.tsx` cherche la colonne par
  `classes.article` au lieu de `.prose`.

**Mesures d'identité.** `docs` construit avant (copie de `dist` de la vague 4) et après, servis en
statique sans JS, transitions coupées ; Playwright relève 35 propriétés calculées (couleur, tailles,
interlignes, marges, bordures, puces, `::before`/`::after`/`::marker`, boîte) de chaque élément sous
la racine de prose, sur les 57 pages en clair et en sombre (78 pages avec racine, 14 420 éléments),
plus un fragment injecté qui contient tout ce que la doc n'a pas (`lead`, `blockquote`, `hr`, `h4` à
`h6`, `kbd`, listes imbriquées et de tâches, `table` avec `tfoot`, `pre` brut, `figure`, `dl`,
`not-prose`). Avant contre avant : 0 écart. Avant contre après : 0 écart de la prose et des titres,
hors :

- citation (fragment seulement, aucune dans la doc) : la couleur sort `lab(48.496 0 0 / 0.75)` au
  lieu de `oklab(0.556 0 0 / 0.75)` (le minifieur calcule le `color-mix` littéral), et des écarts de
  1e-5 en sombre : même couleur ;
- hors lot : `Cards__icon` de `site-kit` à `32px` au lieu de `31.9999px` (GATE4), la pagination «
  Deploy on Vercel » (contenu), et un bloc `data-recit-runtime-shiki` de `recit-config` dont les
  jetons portent une autre couleur claire dans le HTML prérendu (`--shiki-light:#D73A49` sur
  `{ defineConfig }`) : surlignage d'exécution, à attribuer.

**Vérifications.**

```
$ node scripts/check-style-rule.js packages/site/src/lib/components packages/site/src/lib/util
  → components 0 ; util 2 (cn.ts, STY5c)
$ node scripts/check-style-rule.js --write-baseline (mêmes dossiers) → components retiré,
  util className 4 → 0, noModule 1 → 0
$ pnpm test:related (mes fichiers) → 6 fichiers, 42 tests verts
$ pnpm test:stories ErrorPage NotFoundPage Markdown InlineCode → 4 fichiers, 8 tests verts
$ pnpm -F @recit/site typecheck → erreurs seulement dans des stories d'autres lots
$ pnpm -F @recit/site-kit typecheck → vert
$ pnpm exec nx run docs:build → vert (avec --excludeTaskDependencies : les dist des dépendances
  étaient vidés par les builds concurrents)
$ BROWSER=none pnpm test:e2e:host e2e/site-docs.spec.ts e2e/site-behaviour.spec.ts → 56 passés,
  1 ignoré
$ pnpm check → rouge sur des fichiers d'autres lots (e2e/.sbfix-*.mjs, stories, deck.css,
  Alert/Card/InputGroup.module.css), aucun des miens
```

Différence voulue : aucune. Le HTML change (classes de module, `data-order` et `style` de `Title`,
plus de `prose`, `dark:prose-invert`, `inline`, `group`, `relative`, `text-*`), les captures ne
devraient pas bouger.

### GATE4 — Corrections de la porte de la vague 4 — 30/10

Corrections après l'e2e de la porte de la vague 4, sur `feature/mantine`. Aucune commande git, pas
de `pnpm install`, pas de `pnpm fix`, aucune référence régénérée.

**Cause des −1 px.** Le minifieur CSS de la construction des sites plie
`line-height: calc(1.25 / 0.875)` en `1.42857` (et `calc(2 / 1.5)`, `calc(1 / 0.75)` en `1.33333`) :
14 × 1,42857 = 19,99998 px, soit une ligne de 19,984 px en unités de mise en page (1/64 px) au lieu
de 20. Tailwind gardait la valeur dans une propriété personnalisée
(`--text-sm--line-height: calc(1.25 / 0.875)`), que le minifieur laisse telle quelle. Dans
`FeatureCard`, titre, icône et « Learn more » perdent chacun 1/64 px : la carte passe de 202,5 à
202,453 px (`docs /`), de 141 à 140,969 px (`www`), et l'arrondi des lignes qui suivent descend d'un
pixel. La description (`1.625`, valeur exacte) n'y est pour rien. Même cause pour le texte des liens
de l'en-tête (`ui/NavigationMenu`, `.trigger` et `.link`) : le texte est peint 1 px plus haut (haut
du texte à 22,5 au lieu de 23,5).

**Corrections.** `site-kit/.../Cards.module.css` (`.icon`, `.title`, `.more`) et
`ui/NavigationMenu.module.css` (`.trigger`, `.link`) : la valeur passe par une propriété
personnalisée (`--cards-leading`, `--navigation-menu-leading`), gardée telle quelle dans la feuille
construite. Piège ajouté au skill `mantine-style`. **Reste ailleurs** : 89 `line-height: calc(…)`
dans les modules (`lib/ui/DropdownMenu`, `InputGroup`, `site-stories`, deck, components…) sont pliés
de même ; à reprendre là où une capture bouge, par le propriétaire du fichier.

**Mesures** (Chromium de l'hôte, 1280 × 720, clair et sombre ; « avant » = valeurs Tailwind de
`HEAD` injectées sur la feuille construite, « vague 4 » = valeurs pliées) :

```
                     carte      titre    Learn more  texte du lien d'en-tête (top)
docs /      avant    202.5      20       16          23.5
            vague 4  202.4531   19.9844  15.9844     22.5
            GATE4    202.5      20       16          23.5
docs writing/overview : identique à docs /
www /       avant    141        20       16          23.5
            vague 4  140.9688   19.9844  15.9844     22.5 (clair)
            GATE4    141        20       16          23.5
```

**`CommandShortcut`** : la couleur du texte de l'élément (`color: inherit`), comme le ⌘K de `Search`
(STY4a) ; `a11y: { test: "todo" }` retiré de `Command/Filtered`, qui passe en mode erreur.
Différence voulue : le raccourci de la palette n'est plus gris (aucune capture de site : seule la
story l'utilise).

**TST1 suite.** Les `play` des stories de `packages/components` (`SlideMotion`, `SlideImage`,
`Builtins`, `Card`, `Step`, `Media`, `CodeGroup`, `CodeBlock`, `Mermaid`, `Grid`) passent sur
`expect`/`waitFor` de `storybook/test` ; plus aucun `throw new Error`. Aucun composant n'a de rappel
: pas de `fn`. `CodeBlock/UnknownLanguage` vérifie en plus qu'aucun `<b>` n'est rendu.
Contre-épreuve : `Step/Indexed` attendant `["1", "9", "2"]` échoue (`- Expected + Received`),
rétabli ensuite.

**Vérifications.**

```
$ pnpm test:stories packages/components/src packages/site/src/lib/ui/Command.stories.tsx
  → 19 fichiers, 57 tests verts
$ pnpm test:related (NavigationMenu, Command, Cards, leurs modules) → 8 fichiers, 44 tests verts
$ pnpm exec nx run-many -t build -p docs www → www vert ; docs rouge par
  recit-example-deck-{showcase,mdx}:build (« Could not resolve "@recit/theme" » depuis
  core/dist/config/theme-css.js, fichier d'un autre lot) ; docs reconstruit ensuite par un autre lot,
  avec mes modules
$ BROWSER=none pnpm test:e2e:host e2e/site-docs.spec.ts → 50 passés, 1 échec :
  writing/mdx, React #418 (hydratation, texte), pas de CSS en cause ; les autres échecs vus
  venaient des dist vidés par les builds concurrents et repassent seuls
$ pnpm check → rouge hors de mes fichiers (e2e/.sbfix-*.mjs, oxfmt de Heading, Typography,
  stories de navigation…, compteur de stories « kit » 4 → 10)
```

### TST3a — Stories du framework, pages — 30/10

Vague 5 du plan 21 (§8, TST3), en parallèle de GATE4, INFRA, STY5a-b, TST3b, TST4a-b. Aucune
commande git, pas de `pnpm install`, pas de `pnpm fix`, pas de conteneur.

**En place.** Vingt-quatre fichiers `packages/site/src/lib/components/<X>.stories.tsx`, titre
`Site/Components/<X>` : `Header` (complété), `TopNavigation`, `HeaderNavigation`,
`MobileTopNavigation`, `MobileTopNavigationDrawer`, `ThemeSwitch`, `Search`, `Footer`, `Banner`,
`PageProgress`, `Spinner`, `BrowserWindow`, `Framed`, `Anchor`, `Pagination`, `CategoryHeading`,
`NotFoundPage`, `ErrorPage`, `DeveloperHint`, `LanguageIcon`, `ScrollableTable`, `InlineCode`,
`Markdown`, `Mermaid`, `Slot`. 75 stories dans `index.json`, un `play` pour chaque composant
interactif : `banner`, lien actif (`aria-current="page"`), logo à ses dimensions, bannière,
recherche au clic, à `Ctrl+K` et `⌘K`, un seul dialogue par placement ; `target`/`rel` des liens
externes (barre du haut, tiroir, pied de page) ; menus de `header.navigation` ouverts et fermés par
Échap ; tiroir mobile (fenêtre `mobile`) : `aria-expanded`/`aria-controls`, groupe déplié, Échap
rend le focus, un lien ferme, `onOpenChange` en `fn()` ; `ThemeSwitch` bascule vraiment (un
`MantineProvider` propre à la story, schéma en mémoire, sans élément racine ni stockage :
`forceColorScheme` de `withMantine` en faisait un no-op) ; `contentinfo` ; bannière fermée ; zoom de
`BrowserWindow` (pas, retour, limites) ; arrêt de tabulation d'un tableau large ; Markdown riche et
composants du `MDXProvider` ; diagramme Mermaid et erreur (`wait-mermaid`) ; ordre des
`Slot.Source`. Variantes par `Group`/`Stack`, compteur de la règle à 0.

- Tests : `BrowserWindow`, `Footer`, `Markdown`, `NotFoundPage`, `ScrollableTable`, `TopNavigation`,
  `MobileTopNavigation` `.test.tsx` supprimés (tout est dans les `play`) ; `Header.test.tsx` ne
  garde que `reloadDocument` du logo (une prop de `Link` sans trace dans le DOM) ; `Search.test.tsx`
  (préchargement) et `Slot.test.tsx` (logique) restent, ce dernier sans ses deux `data-testid`.
- Hors liste : `sites/storybook/.storybook/main.ts`, `resolve.dedupe: ["@unhead/react"]` — pnpm
  installe deux variantes de pair, et le `Head` de `Header` ne voyait pas l'`UnheadProvider` de
  `withSite` (« useHead() was called without provide context ») dès qu'un logo est configuré.
- `scripts/stories-baseline.json` : `site/components` 54 + 7 → 18 (restent `Typography`, `Heading`,
  `Layout`, `Main`, `LandingPage`, `context/*`… hors lot).

**Défauts trouvés** (composants hors lot, stories en `a11y.test: "todo"` avec la raison) :

- `Banner` : le bouton de fermeture n'a pas de nom (`button-name`) ; la fermeture n'est pas
  mémorisée (`useState`, elle revient à la page suivante) ; texte blanc sur `info` 3,6:1, `tip`
  3,08:1, `caution` 2,76:1, `danger` 3,59:1 (`color-contrast`).
- `ui/Separator` décoratif garde `aria-orientation` (déjà vu à TST2a) : séparateur de la barre du
  haut et du tiroir mobile dès qu'il y a des liens d'en-tête.
- `HeaderNavigation` ouvert : liens dans un `menu` (TST2b). `Mermaid` en erreur : `Alert`
  destructive 4,36:1 (TST2a). `Markdown` : citation de `.prose` #969696 sur blanc, 2,95:1 (STY5a).
- Sans rôle ni nom là où §4.2 les attend : `Pagination` n'est pas un `nav`, `Spinner` et
  `PageProgress` n'ont pas de `progressbar` (la barre est `aria-hidden`, voulu), `TopNavigation` et
  `HeaderNavigation` sont des `navigation` sans nom ; `Anchor` ne copie pas l'URL (simple lien `#`).
- Harnais : la `Modal` de `SearchFixture` a un bouton de fermeture sans nom (axe échoue si la story
  finit dialogue ouvert) ; `PageProgress` à 50 % n'est pas atteignable sans chargeur de route.
- `DeveloperHint` ne rend rien hors `NODE_ENV=development` : rien à voir en test ni en capture.

**Vérifié.**

```
$ pnpm test:stories packages/site/src/lib/components → 37 fichiers, 111 tests verts
$ pnpm test:stories (complet) → 300/321 ; échecs hors lot (deck, ui/CodeTabs…) et rechargements
  de dépendances Vite (cache partagé avec les runs concurrents), les miens verts au passage ciblé
$ pnpm exec vitest run Header.test Slot.test Search.test → 27 tests
$ pnpm -F @recit/site typecheck → seules erreurs : navigation/, plugins/ (TST3b en cours)
$ pnpm -F recit-storybook typecheck → erreurs deck/motion-blocks (TST4a), aucune ici
$ pnpm -F recit-storybook build → vert, 75 stories Site/Components
$ node scripts/check-stories.js packages/site/src/lib/components → 18, aucun de mes composants
$ node scripts/check-style-rule.js packages/site/src/lib/components → 0
$ pnpm check → biome rouge hors lot (e2e/.sbfix-*.mjs, Alert/Card.module.css…) ; biome et
  oxfmt verts sur mes fichiers, check:other vert
```

Captures des nouvelles stories : à la porte (`pnpm test:e2e:update:linux e2e/storybook.spec.ts`).

### Suivis pour la porte de la vague 5 — 30/10

- **Interlignes pliés par le minifieur** (GATE4) : `line-height: calc(1.25 / 0.875)` devient
  `1.42857` au build, une ligne de 14 px perd 1/64 px et tout ce qui suit descend d'un pixel ;
  Tailwind passait par une propriété personnalisée que le minifieur ne plie pas. Corrigé pour
  `Cards` et `NavigationMenu` (`--cards-leading`, `--navigation-menu-leading`) ; il reste 89
  `line-height: calc(…)` dans les modules. Chercher une correction unique au build (plutôt que 89
  retouches), vérifier aussi Lightning CSS pour les paquets publiés.
- **Lot d'accessibilité** à ouvrir après la vague 5, avec les `todo` des stories : contrastes
  (`Badge` du deck, avertissement de `Grid`, erreur de `Mermaid`, `Alert` destructive et info,
  onglet inactif de `CodeTabs`, orange Shiki clair, texte de `Banner`, citation de la prose 2,95:1),
  noms manquants (bouton de fermeture de `Banner`, `Modal` du harnais, `navigation` de
  `TopNavigation` et `HeaderNavigation`), rôles (`Pagination` en `nav`, `progressbar` de
  `Spinner`/`PageProgress`), axe `aria-required-children` des menus Mantine et de `Command` vide,
  `aria-orientation` du `Separator` décoratif, point gris de la `Dialog` ouverte.
- `writing/mdx` : React #418 (hydratation de texte) vu par GATE4 sur l'hôte, comme
  `/custom-components` à STY6.3b ; piste « Last modified » ou date de rendu.
- `sites/storybook/src/harness/pagefind` ignoré par `.gitignore` (`pagefind/`) : manquera en CI.
  Réglé par TST3b : le dossier s'appelle `pagefind-index/`.
- `docs:build` en échec pendant GATE4 (« Could not resolve @recit/theme » depuis `core/dist`) : un
  `dist` en reconstruction par un build concurrent ; l'exemple se construit seul ensuite.

### TST3b — Stories du framework, navigation et plugins — 30/10

Vague 5 du plan 21 (§8, TST3), en parallèle de GATE4, INFRA, STY5a-b, TST3a, TST4a-b. Aucune
commande git, pas de `pnpm install`, pas de `pnpm fix`, aucun composant modifié.

**En place.** 19 fichiers de stories, 57 stories (`Site/Navigation/…`, `Site/Plugins/…`), toutes
avec un `play` sauf les variantes purement visuelles de `NavigationBadge` et l'Inkeep `!test` :

- `navigation/` : `Navigation` (le `Sidebar` de §4.2 : arborescence à trois niveaux ouverte sur la
  page courante, `aria-current="page"`, Entrée sur un chevron déplie, un clic navigue et déplace la
  page courante ; tout plié hors de l'arbre ; filtre ; navigation empilée : la ligne ouvre le
  panneau, « Back » revient), `NavigationItem` (page, active, icône et badge, lien interne à une
  ancre suivi au clavier, lien externe `_blank`/`noopener`, section, séparateur décoratif, masqué),
  `NavigationCategory` (plié/déplié au clic, Entrée, Espace, focus gardé ; actif ; imbriqué ;
  statique ; avec lien : le chevron ne navigue pas, la ligne navigue et demande la fermeture),
  `NavigationBadge` (couleurs, inversé), `NavigationFilterInput` (filtre, catégorie ouverte par une
  correspondance, effacement, aucun résultat), `NavigationFilterProvider` (vidé au changement de
  section sans démonter le champ), `NavigationFrames` (racine, lien retour), `NavigationWrapper`
  (l'entrée active hors de vue amène le `nav` seul, la fenêtre ne bouge pas : `centerInNav`),
  `SidebarToggle` (clic replie et rend la barre `inert`, Entrée déplie ; replié ; au survol, en
  haut), `StackRows`, `MobileNavigationDrawer` (mobile 390 : ouvert sur une page profonde, un lien
  ferme ; le bouton ouvre, Échap ferme et rend le focus ; à droite), `Toc` (aucune entrée courante
  au départ, la section signalée par `IntersectionObserver` passe en `aria-current="location"`, une
  sous-section déplace la marque, une entrée mène à son ancre ; à un niveau). Le `Toc` ne rend que
  deux niveaux : le « sommaire à 3 niveaux » de §4.2 est le titre de page plus deux.
- `plugins/markdown` : `MdxPage` (sommaire et pager, l'entrée suit la section ; sans sommaire ;
  métadonnées : catégorie, date, lien d'édition ; « Copy page » copie le Markdown (`fetch` espionné,
  presse-papiers relu), le menu ouvre le Markdown et « Use in Claude » appelle `window.open`
  espionné ; pleine largeur : sommaire en popover ; contenu large : le bloc défile, pas la page),
  `ClaudeLogo`, `ChatGPTLogo`.
- `plugins/search-pagefind` : `PagefindSearch` (monté comme un site le fait, `SearchProvider` +
  `Search` + `pagefindSearchPlugin` : `Ctrl+K` ouvre, champ focalisé, Échap ferme ; « lorem » : 3
  pages et leurs sections, flèches, Entrée navigue vers `/dolor#ut-labore` et ferme ; aucun résultat
  : message et « Clear search » qui vide et refocalise), `ResultList` (sections triées par score,
  clic navigue, `maxSubResults`, `basePath` retiré, vide), `IndexingDialog` (en cours 38 %, terminé,
  échec puis « Retry » qui se reconnecte, `EventSource` rejoué).
- `plugins/search-inkeep/InkeepSearch.stories.tsx` : le bouton seul, `tags: ["!test"]` avec sa
  raison (widget tiers chargé d'un CDN avec la clé d'un site ; le comportement reste prouvé par
  `index.test.tsx` contre un faux widget).

**Harnais** (fichiers partagés, ajouts seulement) :

- `.storybook/intersection.ts`, importé par `preview.tsx` : l'`IntersectionObserver` du navigateur
  enveloppé ; un `play` signale un élément visible par
  `element.dispatchEvent(new CustomEvent("storybook:intersect"))`. Dans `preview.tsx` et non
  `vitest.setup.ts` : les captures e2e jouent les mêmes `play` sur le build.
- Index Pagefind de fixture : `src/harness/pagefind-index/pagefind.js` (trois pages Lorem Ipsum,
  `init`/`options`/`search`), servi à `/pagefind/pagefind.js` par `staticDirs` (build) et résolu par
  le plugin Vite `src/harness/pagefindIndex.ts` (Vitest enveloppe les `import()` et perd le
  `@vite-ignore` : le littéral de `PagefindSearch` était résolu, puis refusé). Dossier nommé
  `pagefind-index` : `pagefind/` est dans `.gitignore` (suivi de la porte réglé).
- `packages/site/src/lib/testing/LocationSpy.tsx` : rend `null`, appelle
  `onNavigate(pathname + hash)` à chaque location du routeur mémoire ; c'est le « `fn` sur le
  routeur » de §4.2, réutilisable par TST3a.

**Tests migrés.** `MdxPage.test.tsx` supprimé (classes `.root`/`.article` : la story `WideContent`
mesure le comportement). Le `describe("NavigationCategory")` de `Navigation.test.tsx` passe à la
story `WithLink` ; les trois tests d'état entre sections restent (`router.navigate` du dehors).
`search-pagefind/index.test.tsx` garde sa logique de chargement, sans `data-testid` (une `region`
nommée). `useNavigationFrame.test.ts` et `search-inkeep/index.test.tsx` restent.

**Défauts trouvés** (composants hors lot, stories en `a11y.test: "todo"` avec la raison) :

- `NavigationCategory` : axe `list` (le `ul` des items contient les `a` sans `li`) et
  `aria-allowed-attr` (`aria-expanded` sur la ligne, un `div` ou le lien, sans rôle qui l'admet).
  Touche aussi `Navigation`, `NavigationFilterInput`, `MobileNavigationDrawer`.
- `NavigationFilterInput` : bouton d'effacement sans nom (axe `button-name`).
- `NavigationBadge` : contrastes, blanc sur vert clair 1,77:1 (palette Tailwind reprise telle
  quelle).
- `MdxPage`, `TocPopoverButton` en bouton : nommé « Toggle table of contents », affiche « On this
  page » (axe `label-content-name-mismatch`).
- `IndexingDialog` démarre la réindexation **au montage**, pas à l'ouverture : en développement,
  chaque ouverture de la recherche monte le pied et ouvre `/__z/pagefind-reindex`. Les stories de
  `PagefindSearch` neutralisent `EventSource`.
- Deux navigations `viewTransition` de suite : React Router laisse la promesse rejetée de la
  première transition sautée (« AbortError: Transition was skipped ») ; la story `Stacked` retire
  `document.startViewTransition`.
- `DeveloperHint` (brouillon) n'apparaît que si `NODE_ENV` vaut `development` : ni le projet Vitest
  ni le build de Storybook ; non affirmé.
- Ni `main` ni `article` dans `MdxPage` (c'est `Layout`) : l'a11y de §4.2 pour cette ligne est à la
  page entière, en e2e.

**Vérifié.**

```
$ pnpm test:stories <navigation, plugins>       → 19 fichiers (1 sauté), 56 tests
$ pnpm test:stories                             → 128 fichiers passés, 2 sautés ; 485 tests
  (des « Failed to import test file » transitoires : dist reconstruits par les autres lots)
$ pnpm test:related Navigation.test.tsx search-pagefind/index.test.tsx → 2 fichiers, 5 tests
$ pnpm -F @recit/site typecheck                 → muet
$ pnpm -F recit-storybook typecheck             → rien dans mes fichiers (erreurs de
  formats/deck, motion-blocks et site-kit, lots en cours)
$ pnpm -F recit-storybook build                 → vert, 57 stories des deux surfaces, pagefind.js copié
$ node scripts/check-stories.js packages/site/src/lib/components/navigation packages/site/src/lib/plugins → 0
  baseline réécrite (relue juste avant) : site/plugins disparaît, site/components 18 (TST3a)
$ node scripts/check-style-rule.js (mes dossiers) → 0 hors sites/storybook/.storybook (3, existants)
$ pnpm check → biome rouge sur des fichiers d'autres lots (e2e/.sbfix-*.mjs, Alert/Card/InputGroup
  .module.css, MissingIcon, CodeBlock.test…) ; fmt:check rouge sur e2e/.sbfix-* ; check:imports et
  check:other verts
```

Captures des 57 stories : à la porte (`pnpm test:e2e:update:linux e2e/storybook.spec.ts`).

### STY5b — main.css, mise en page, icônes de langage — 30/10

**Fait.** STY5.1 pour sa part hors prose (STY5a a retiré la prose et `@plugin` pendant ce temps).

- Les variables de mise en page (`--top-header-height` … `--scrollbar-width`) quittent le
  `@layer base` de `main.css` pour `lib/components/Layout.module.css`, sous `:global(:root)` : le
  `<html>`, l'en-tête, la barre latérale, le sommaire et les pages les lisent, comme
  `Banner.module.css` qui met déjà `--banner-height` à 0 par `:global(:root):has(…)`. Les
  `--spacing(n)` deviennent leurs valeurs (pas de 0,25rem : 18rem, 1.5rem, 3rem, 0.625rem). Le
  module est hors couche, comme tous ceux du framework : rien d'autre ne déclare ces variables.
- Nouveau `app/base.css`, importé par `main.tsx` juste après `main.css` : le reset (`*`
  `border-color`/`outline-color`), `html`, `body`, `#root`, `details summary`, les ligatures du
  code, `::selection`, `.dark { color-scheme }`, les curseurs, `select`, `.scrollbar`, `.wavy-line`,
  valeur pour valeur depuis la feuille compilée, sans `@apply`. **Couche `base`, pas `theme`** : le
  preflight de Tailwind (dans `base` jusqu'à STY5c) remet `border: 0 solid` sur tout élément, et le
  `border-color` doit passer après ; STY5c le descend dans `theme` avec le retrait du preflight
  (noté dans l'en-tête du fichier). `html.dark { --dark }` et `select:is(.dark *)` lisent maintenant
  `:root[data-mantine-color-scheme="dark"]` ; `.dark { color-scheme: dark }` reste (pont de
  `SiteRoot`, STY9). `::selection` lit `--primary` et non plus `--color-primary` (alias du
  `@theme inline`).
- Retirés, morts : `.bg-primary ::selection` (plus aucun élément n'a la classe),
  `.CollapsibleContent` et ses keyframes (jamais appliqués, cf. `Collapsible.tsx`), le
  `@layer utilities .stepper` (repris par `Stepper.module.css`, la classe n'est plus rendue).
- `lib/assets/language-icons` : les onze icônes à couleur de thème importent `fills.module.css` (un
  module pour la famille : `.foreground`, `.foregroundSoft` pour `fill-foreground/90`, `.react`,
  `.json`, `.tomlOutline`, `.tomlLetters` en `light-dark()` / `@mixin dark`) ; les autres gardent
  leurs attributs `fill`.
- `scripts/check-style-rule.js` (hors liste, une ligne) : `isGlobalCss` exempte
  `packages/site/src/app/base.css`, le CSS global du framework que nomme STY5.1 ; un cas dans
  `check-style-rule.spec.ts`.

**Ce qui reste dans `main.css`** (54 lignes) : les `@import` (polices, `tailwindcss`,
`tw-animate-css`), les deux marqueurs `@vite-plugin-inject`, `@custom-variant dark` et le
`@theme inline` des couleurs Tailwind : tout est à STY5c. La déclaration `@layer` vit dans
`app/layers.css` (une déclaration dans `main.css` ne survit pas à Tailwind).

**Identité.** `sites/docs` construit avant (copie dans le scratchpad) et après, servi en statique,
Playwright à 1440 et 390 px, clair et sombre, sur `/`, `/documentation/writing/mdx`, `…/scenes`,
`/documentation/reference/cli`, `/formats/motion`, `/404` : ~3 500 à 8 800 valeurs par page (boîtes
et styles calculés de `html`, `body`, `#root`, en-tête, `nav`, `aside`, `main`, pied, titres, `pre`,
liens, SVG…), **zéro différence** hors l'écriture des quatre variables (`calc(.25rem * 72)` →
`18rem`, même longueur). Captures pleine page identiques, sauf deux à 22-24 pixels d'un niveau
d'écart sur des coins arrondis (bruit de rendu, aussi entre deux passages du même build).
Remplissages des icônes (MDX, React, Markdown, YAML, JSON, Terminal, TypeScript) identiques en clair
et en sombre.

**Vérifié.**

```
$ node scripts/check-style-rule.js packages/site/src/app packages/site/src/lib/assets packages/site/src/lib/util
  app 3 css + 2 directives (main.css : @theme inline, @custom-variant ; defaultTheme.css : :root, .dark)
  lib/assets 0 ; lib/util 2 imports (cn.ts, STY5c) → --write-baseline sur ces dossiers
$ pnpm exec vitest run scripts/__tests__/check-style-rule.spec.ts → 66 tests
$ pnpm test:related main.tsx LanguageIcon.tsx language-icons/*.tsx check-style-rule.js → 7 fichiers, 108 tests
$ pnpm -F @recit/site typecheck → rien dans mes fichiers (stories de Markdown, MobileNavigationDrawer,
  Navigation : lots TST)
$ build docs, www, presentation → verts (relancés : dist des paquets reconstruits par les autres lots)
$ pnpm test:e2e:host e2e/site-docs.spec.ts e2e/site-behaviour.spec.ts → 55 passés, 1 sauté ; la
  recherche « searches the pages… » a expiré une fois sous charge, verte seule
$ pnpm check → biome rouge sur des fichiers d'autres lots (e2e/.sbfix-*.mjs, Alert/Card/InputGroup
  .module.css, MissingIcon, CodeBlock.test…), rien dans les miens
```

À reprendre : `defaultTheme.css` (règles globales comptées, injectées en couche `theme` par
`plugin-theme.ts`) n'a pas de lot ; la liste des CSS globaux permis de la loi (SKILL.md §5, plan
§0.3) devrait nommer `app/base.css` ; les commentaires de `Header`, `Layout` (corrigé) et autres
modules qui renvoient aux variables « de `main.css` » sont à rafraîchir au passage.

### TST4a — Stories du deck et du motion — 30/10

Vague 5 du plan 21 (§8, TST4), en parallèle de GATE4, INFRA, STY5a-b, TST3a-b, TST4b. Aucune
commande git, pas de `pnpm install`, pas de `pnpm fix`, pas de conteneur.

**Décorateurs.** `sites/storybook/.storybook/decorators/deck.tsx` (+ `deck.module.css`) : `withDeck`
dessine un `Deck/` dans un cadre de `parameters.deck.frame` (`small` 320×180, `medium` 640×360,
`large`, `square`, `none` pour le plein écran) et pose la feuille de données du récit
(`story.themeCss` : taille de design, fond et progression par scène, que `SlideFrame` n'a pas sans
elle) dans un `<style>` de `head`, comme `DeckApp`. `film.tsx` (+ `film.module.css`) : `withFilm`
met un bloc à la taille native du film et l'échelle au cadre par CSS seul (`data-aspect` 16:9, 9:16,
1:1 ; `tan(atan2())` comme le stage du deck), `parameters.film.frame: "none"` pour `MotionFrame` ;
global `motionTheme` (barre d'outils, `globals` d'une story), options lues dans `MOTION_THEMES` de
`@recit/theme/motion`. `preview.tsx` : `withDeck`, `withFilm`, `globalTypes`.

**Fixtures côté paquet** (dossier `fixtures`, ignoré par le compteur, hors `files` publiés) : le
workshop ne résout ni `@recit/core` ni `@recit/motion-blocks`.
`packages/formats/deck/src/fixtures/deck.tsx` : un récit Lorem Ipsum de cinq scènes (couverture avec
notes, deux `:::step`, grille, scène à `bg`, citation) **compilé dans le navigateur** par
`compileStory` (`loaders: [loadDeck]`, 16:9 ou 4:3, pied de page sur demande), `loadMarkdown`,
`sceneRenderer`, `SceneInStage`, `clockAt(s)` (le prochain `setInterval` tique `s` fois puis se tait
: minuteur figé pour la capture, ou à 10 min), `withSearch` (`?slide=3`, `?print=1`).
`packages/formats/motion-blocks/src/fixtures/` : `withMotionTheme` (le `ThemeProvider` du paquet
depuis `globals.motionTheme`), `loremFilm`, props `ui-scene` parsées par le schéma.

**Stories.** 27 fichiers, 144 stories (60 `Deck/`, 84 `Motion/`), compteur à 0 sur les deux surfaces
(27 avant). Deck : `Presentation` (flèches, Espace et étapes, `End`/`Home`, `s` ouvre la console
avec `window.open` moqué puis refusé, `b` écran noir, `h` HUD, 4:3, intégré, impression avec notes
et `onPrintReady`), `PresenterView` (minuteur figé, à 10 min, réel puis pause et remise à zéro ;
boutons et `onNavigate`), `SlideStage` (trois cadres, carré, marge, 4:3 ×3), `SlideFrame` (mises en
page, fond, progression, pied), `HtmlSlide` (étapes, directive `:::component`, composant absent),
`SceneContent` (Markdown, MDX, MDX absent), `MdxSlide`, `DeckApp` (`?slide=3`, `?print=1&notes`),
`AnnotationCanvas` (tracé par pointeur vérifié aux pixels), `MobileRemoteView`, `RemoteQrCodeModal`
(QR généré), `DevHud`. Motion : les 8 blocs × (16:9, 9:16, chaque thème, minimal, maximal, maximal
9:16), `Frame`/`Backdrop`, `ThemeVars`, `ThemeProvider` (dont thèmes de marque `buildTheme`),
`Engine` (`StaticAnim`, `FramerAnim` `wait-motion`), `Block`, `UiSceneView` (chargé, squelette),
`MotionFrame` (début, en cours, réglé, deuxième scène, fin, thème du film, 9:16).

**Hors liste.** `main.ts` : `optimizeDeps.include` des dépendances de `compileStory` et de `qrcode`,
par `@recit/deck > …` (découvertes en cours de run, elles rechargeaient la page et faisaient échouer
les suites) ; `preview.css` : `@import` par chemin de
`packages/formats/motion-blocks/dist/styles.css` ; `sites/storybook/tsconfig.json` : inclut les
stories des deux paquets, `paths` vers ses propres `@storybook/react-vite` et `storybook/test` ;
`tsconfig.json` des deux paquets : `exclude` des stories ; leurs `package.json` : `files` sans
`*.stories.*` ni `fixtures` (aucune dépendance, pas de version plan).

**À la porte (install).** `@recit/motion-blocks` en `devDependencies` de `sites/storybook` (l'arête
nx manque : un build affecté peut sauter le `dist` dont `preview.css` lit la feuille ; puis
remplacer le chemin par le paquet) ; `storybook` et `@storybook/react-vite` en dev de `@recit/deck`
et `@recit/motion-blocks` (puis retirer `paths` et `exclude`). Sans eux, `player.test.tsx` ne peut
pas `composeStories` : il reste en `renderToStaticMarkup`.

**Défauts trouvés** (stories en `a11y.test: "todo"` avec la raison, composants intouchés) :

- Pas de navigation par chiffres dans `useKeyboardNav` (§4.3 l'attendait) ; `c` (effacer le stylo)
  n'est pas câblé par `Presentation` (`onClearAnnotations` absent).
- `AnnotationCanvas` sur un stage mis à l'échelle : le canevas est à la taille de design (1278 px)
  mais les coordonnées viennent du rectangle affiché (635 px) — à l'échelle 0,5 le trait part à la
  moitié de la position du pointeur (sondé). Écoute la souris, pas le pointeur (tactile).
- `SlideFrame` : `<footer>` = `contentinfo` dans le `group` de la diapositive
  (`landmark-contentinfo-is-top-level`) ; texte du pied 3,53:1 en clair, 1,93:1 sur un `bg` sombre.
  Le pied est actif par défaut dans tout récit : la fixture le coupe, `WithFooter` le montre.
- `PresenterView` : compteur 4,34:1, « Speaker notes » en `h3` sans titre au-dessus ;
  `MobileRemoteView` : position 1,68:1 (#542858 sur #090d16), notes vides 3,89:1 ;
  `RemoteQrCodeModal` : « Session » 4,46:1, et ce n'est pas un `dialog` (ni rôle, ni `aria-modal`,
  ni Échap, ni focus) ; `DevHud` : badges vert 3,76:1 et ambre 3,18:1 ; `HtmlSlide` : message de
  composant absent 3,81:1 ; corps d'un `callout` `tip` dans une diapositive 4,16:1.
- Textes en dur non traduits : « Slide » (`PresenterView`, `DevHud`, `MobileRemoteView`), « PREV »/«
  NEXT ». `MobileRemoteView` n'a pas d'état connecté/déconnecté (story `Disconnected` = état
  initial).
- Thèmes motion (`@recit/theme/motion`) : couleurs atténuées et d'accent sous 4,5:1 pour le petit
  texte (warm-editorial #8a6470 sur #f4e7d6 4,16:1, saas-product #7f8593 sur #f3f4f7 3,36:1, vert et
  rouge sous 3:1) : `THEME_CONTRAST_TODO` sur les métas des blocs, de `Block`, `ThemeProvider`,
  `UiSceneView`, `MotionFrame`.
- Pas de lecture/pause dans `player.tsx` : `MotionFrame` reçoit `time`, les commandes sont dans
  `MotionPlayer` de `@recit/site-stories` et le client de `recit motion dev` (hors lot) ; les
  stories couvrent pause au début, en cours, fin.

**Vérifié.**

```
$ pnpm test:stories packages/formats/deck packages/formats/motion-blocks → 27 fichiers, 144 tests
$ pnpm test:stories (complet) → 451/454 ; échecs hors lot (PagefindSearch) et suites rechargées
  par Vite (cache partagé), les miennes vertes au passage ciblé
$ pnpm exec vitest run --project '!storybook' packages/formats/deck packages/formats/motion-blocks
  → 18 fichiers, 96 tests
$ pnpm -F recit-storybook typecheck ; nx typecheck @recit/deck @recit/motion-blocks → verts
$ pnpm -F recit-storybook build → vert, 486 stories (60 Deck, 84 Motion)
$ pnpm -F @recit/motion-render goldens:check → ✓ all goldens match (32 vues, aucune régénérée)
$ node scripts/check-stories.js packages/formats/deck packages/formats/motion-blocks → 0
  --write-baseline : deck et motion retirés de scripts/stories-baseline.json
$ node scripts/check-style-rule.js packages/formats/deck packages/formats/motion-blocks → 0
$ pnpm check → biome et oxfmt rouges hors lot (e2e/.sbfix-*.mjs, Alert/Card.module.css…) ;
  verts sur mes fichiers ; check:imports et check:other verts
```

Captures des 144 stories (288 références) : à la porte
(`pnpm test:e2e:update:linux e2e/storybook.spec.ts`).

- (STY5b) `packages/site/src/app/defaultTheme.css` (règles `:root` et `.dark`) n'a pas de lot : à
  STY5c avec le reste de `main.css`. `base.css` est en `@layer base` tant que le preflight de
  Tailwind existe ; STY5c le passe en `theme`. Des commentaires de modules (`Header`…) citent encore
  « les variables de `main.css` » : elles sont dans `Layout.module.css`.
- (TST4a) **Bug** : `AnnotationCanvas` décale le trait quand le stage est mis à l'échelle (à 0,5 le
  canevas fait 1278 px mais les coordonnées viennent du rectangle affiché, 635 px) et n'écoute que
  la souris, pas le pointeur (rien au tactile) — à corriger avec le lot d'accessibilité ou avant.
  Clavier : pas de navigation par chiffres dans `useKeyboardNav`, `c` (effacer) non câblé.
  `RemoteQrCodeModal` n'est pas un `dialog`. Contrastes du deck (présentateur, télécommande,
  `DevHud`, pied de `SlideFrame` 1,93:1 en sombre, `callout tip` 4,16:1) et des thèmes motion
  (`@recit/theme/motion`, accent et atténué sous 4,5:1). Textes « Slide », « PREV », « NEXT » non
  traduits.
- (TST4a) À la porte, un `pnpm install` : `@recit/motion-blocks` en devDependency de
  `sites/storybook` (et `preview.css` par le paquet plutôt que par chemin) ; `storybook` et
  `@storybook/react-vite` en devDependencies de `@recit/deck` et `@recit/motion-blocks`, puis
  retirer les `paths` et `exclude` de contournement et migrer `player.test.tsx` vers
  `composeStories`.

### TST4b — Composants des sites vers site-kit, stories du kit — 30/10

Vague 5 du plan 21 (§8, TST4), en parallèle de GATE4, INFRA, STY5a-b, TST3a-b, TST4a. Aucune
commande git, pas de `pnpm install`, aucune référence régénérée.

**Un site, c'est du Markdown et de la config.** Les six composants tolérés quittent les sites pour
`packages/tools/site-kit/src/components/` avec leurs modules : `Hero`, `FormatGrid`, `OneSource`
(`SideBySide`, `Caption`) de `www`, `StatusBadge` de `docs`, `VideoDownloads` de `motion`,
`FormatSupport` de `components`. `site-kit` les exporte ; les `recit.config.ts` les importent de
`@recit/site-kit` (déjà en dépendance des quatre sites) ; les pages `.mdx` ne les importaient pas
(composants passés par `mdx.components`) et ne bougent pas. Les dossiers `src/` des quatre sites
sont supprimés (avec `www/src/index.ts` et `assets.d.ts`), leur `tsconfig.json` ne les inclut plus.
L'override `sites-markdown-only` de `biome.jsonc` n'a plus d'exception ; `AGENTS.md` le dit.

- `VideoDownloads` : le motif devient `/videos/*.mp4`, relatif à la racine du site (Vite, `root` du
  site ; le chargeur de config de la CLI, dossier de la config) ; vérifié par une construction de
  `motion` avec un MP4 factice (lien présent) puis sans (message). Nouvelle prop `files`
  facultative, pour la story.
- Les modules passent par le PostCSS Mantine du Vite du framework, comme `Cards.module.css`
  (`rem()`, `@mixin dark` compilés) ; les classes deviennent `recit-site-kit-*`.

**Héros de `www`, −1 px.** Mesuré sur l'hôte (1280 × 720) : boutons à 37,984 px, section à 421,98.
Cause : le minifieur plie `line-height: calc(1.25 / 0.875)` en `1.42857`, soit 19,99998 px, que la
mise en page arrondit à 19,984 px ; Tailwind passait par une propriété personnalisée, jamais pliée.
Même correction que GATE4 (`--cards-leading`) : `--hero-leading: calc(1.25 / 0.875)` puis
`line-height: var(--hero-leading)`. Après : boutons à 38 px, section à 422, comme Tailwind (20 + 16

- 2).

**Stories (`Kit/`, 41 tests).** `Cards` (une carte, badge, sans lien, lien externe, grilles de 1 à
6), `ExampleHeader` (avec et sans badges, avancé), `SourceFile` (court, long, config), `Tutorial`
(une étape, sans durée, 5 étapes), `Hero`, `FormatGrid` (défaut, mobile), `OneSource`, `StatusBadge`
(états, phase), `VideoDownloads` (vide, fichiers), `FormatSupport` ; `Slides/Examples/` pour les
composants de deck des exemples, dans le cadre de diapositive avec les jetons du deck : `Counter`
(incrémente, départ), `Timeline` (ordre des étapes ; le composant n'a pas d'étape active, rien
d'ajouté), `Metric` (défaut, tendance, valeur seule). Harnais : `withSite` et le chargeur du
surligneur valent aussi pour `Kit/` (les composants du kit lisent le contexte du site) ;
`virtual:recit-stories` a sa fixture vide (`src/harness/virtual/recit-stories.ts`). `SourceFile`
importe la fixture Shiki de `@recit/site` par chemin (non exportée : son import démarre Shiki).

Typage : `site-kit` et les exemples n'installent pas Storybook ; comme TST4a, leurs `tsconfig`
excluent les stories et `sites/storybook/tsconfig.json` les inclut (ses `paths`). Pas de
`devDependency` ajoutée à `sites/storybook` : ni `@recit/site-kit` ni les exemples n'y sont
résolubles sans `pnpm install` (arête nx manquante, règle ST-20 : à poser avec un install, comme
`storybook` en dev de `site-kit` et des deux exemples).

`todo` d'accessibilité (contrastes, rendu inchangé) : « Available » de `FormatSupport` (emerald-600,
3,65:1), tendance de `Metric` (emerald-500, 2,47:1).

**Vérifications.**

```
$ pnpm exec biome lint → 0 erreur dans le lot (rouges hors lot : e2e/.sbfix-*.mjs, MdxPage.stories)
$ node scripts/check-stories.js --write-baseline packages/tools/site-kit/src/components sites examples
  → kit 4 → 0, sites 7 → 0 (surfaces retirées de la baseline)
$ node scripts/check-style-rule.js → aucune hausse ; node scripts/check-package-boundaries.js → vert
$ pnpm exec nx run-many -t build typecheck -p @recit/site-kit www docs motion components
    recit-example-deck-mdx recit-example-deck-showcase → vert (au 3e passage : dist concurrents)
$ pnpm exec vitest run packages/tools/site-kit --project '!storybook' → 17 tests verts
$ pnpm test:stories → 485 tests, 1 échec hors lot (Deck/Presentation Embedded, TST4a)
$ pnpm test:e2e:host e2e/site-www.spec.ts e2e/site-components.spec.ts e2e/site-motion.spec.ts
  → 54 passés, 1 échec connu (/custom-components, React #418, vu à STY6.3b)
```

À la porte : captures des pages `www` (héros à l'identique), `docs`, `components`, `motion` et des
nouvelles stories `Kit/` et `Slides/Examples/` (`e2e/storybook.spec.ts`).

### SBFIX — Stories en échec dans le Storybook construit — 30/10

Les 28 échecs de la porte de la vague 4 dans `e2e/storybook.spec.ts` (Storybook **construit**), qui
passaient dans le projet Vitest `storybook`. Aucune commande git, aucune référence régénérée.

**Cause commune.** `storybook build` produit un React de production, où le `renderToCanvas` de
`@storybook/react` remplace `act()` par un simple appel (`getAct`,
`process.env.NODE_ENV === "production"`) : le rendu est déclaré fini dès le premier commit, même
suspendu, et le `play` court sur un `#storybook-root` vide. Le projet Vitest rend en développement,
sous `act()`, qui attend la fin des suspensions : la même story y passe.

**Corrections.**

- Code (`CodeBlock`, `CodeTabPanel`, `CodeTabs`, `EmbeddedCodeBlock`, `SyntaxHighlight`) :
  `useHighlighter` fait `use(highlighterPromise)`, et la fixture charge dix-sept grammaires en
  morceaux séparés. Nouveau loader `loadSiteHighlighter` (`.storybook/decorators/withSite.tsx`,
  inscrit dans `preview.tsx`) : il attend le surligneur de `parameters.site.options` avant le rendu
  et marque la promesse `status: "fulfilled"`, que `use()` lit sans suspendre.
- Presse-papiers : permissions `clipboard-read` et `clipboard-write` dans la spec (précédent :
  `site-behaviour.spec.ts`), plutôt qu'un mock dans `preview.tsx`, qui casserait la copie pour qui
  parcourt le Storybook construit ; le mock de `vitest.setup.ts` reste celui du projet Vitest.
- `Slides/Grid/Overloaded` : l'avertissement de surcharge de `Grid.tsx` n'existe qu'hors production.
  Le `play` vérifie l'avertissement en développement, son absence (et les six cartes) en production.
- L'avertissement `Language "csv" is not loaded` ne vient pas du Storybook : ce sont les blocs
  ` ```csv ` de `sites/plans/pages/annexes/broc/scenario-1.md` et `-5.md` (le site `plans` charge
  les langues par défaut). Non bloquant, non corrigé.

**Hors lot, signalé.** Les stories nouvelles de TST3/TST4 tombent sur la même cause (rendu suspendu
ou effet pas encore passé au moment du `play`, `getBy*` au lieu de `findBy*`) : `Site/Components/`
(`Header`, `TopNavigation`, `MobileTopNavigation`, `MobileTopNavigationDrawer`, `Slot`),
`Site/Navigation/` (`MobileNavigationDrawer`, `NavigationWrapper`, `Toc`), `Deck/MdxSlide`,
`Deck/MobileRemoteView`, `Deck/RemoteQrCodeModal` ; `Site/Plugins/Markdown/MdxPage` lève
`SB_DOCS-TOOLS_0001` (argTypes de react-docgen). Remède par story : `findBy*`/`waitFor` ; ou, pour
tout le Storybook, un build de développement (au prix de la taille), à décider à la porte.

**Vérifications.**

```
$ pnpm -F recit-storybook build → vert
$ pnpm test:e2e:host e2e/storybook.spec.ts --grep "codeblock|codetab|embeddedcodeblock|syntaxhighlight|grid--overloaded" --workers=2
  → 58 passés (avec la charge de l'hôte à 15-18 et les workers par défaut, les premières stories
    de chaque worker peuvent dépasser les 5 s du poll de phase au démarrage à froid)
$ pnpm test:e2e:host e2e/storybook.spec.ts --workers=3 → 917 passés, 56 échecs, tous hors lot (liste ci-dessus)
$ pnpm test:stories → 128 fichiers, 485 tests verts
```

### Fin de la vague 5, vague de corrections — 30/10

- Les six lots de la vague 5 sont faits, plus GATE4, INFRA et SBFIX. `pnpm install` hors agents :
  `@recit/motion-blocks`, `@recit/site-kit`, `recit-example-deck-mdx`, `recit-example-deck-showcase`
  en devDependencies de `sites/storybook` ; `storybook` et `@storybook/react-vite` en
  devDependencies de `@recit/deck`, `@recit/motion-blocks`, `@recit/site-kit` et des deux exemples.
  `pnpm check` vert : compteur de style **17** (1 217 au départ), stories sans `play` ou sans
  story 18.
- Avant la porte de la vague 5 (un seul passage e2e, une seule revue), cinq agents : STORYFIX2 (28
  stories en échec dans le Storybook construit, contournements de tsconfig retirés,
  `player.test.tsx`), LH (interlignes pliés, correction unique au build), A11Y-SITE et A11Y-DECK
  (les `todo` d'accessibilité, le stylo d'`AnnotationCanvas`, le point de la `Dialog`), HYDRA (React
  #418, réindexation Pagefind au montage, `viewTransition` abandonnée, surlignage prérendu).

### LH — Interlignes pliés par le minifieur — 30/10

**Ce que plie chaque chaîne** (expérience minimale sur Lightning CSS 1.33.0 et esbuild 0.28.2, ceux
du dépôt) :

- Build Vite des sites : `build.cssMinify` n'est pas réglé, il vaut `true`, ce qui veut dire
  **Lightning CSS** (`minifyCSS` de Vite 8 n'appelle esbuild que pour `"esbuild"`, et le bundle SSR
  prend `"lightningcss"`). En dev, pas de minifieur, pas de pliage : dev et build différaient.
- `scripts/build-css-modules.js` (Lightning CSS, `minify: false`) plie **aussi** : l'analyse range
  chaque nombre en f32 et l'écrit sur six chiffres. `calc(1.25 / 0.875)` → `1.42857`,
  `calc(1 / 0.75)` → `1.33333`, et même un littéral long, `1.4285714286` → `1.42857`. Les feuilles
  publiées de `@recit/components`, `@recit/deck` et `@recit/site-stories` portaient déjà les valeurs
  pliées ; l'`optimize` de Tailwind dans `site-stories/scripts/build-css.mjs` les recopiait.
- Rien ne plie un `var()` : ni `--x: calc(…)`, ni le repli `var(--y, calc(…))`, ni la
  `--text-sm--line-height` de Tailwind. esbuild, lui, ne plie pas ce `calc()`.

**Correction retenue, une seule règle à deux endroits.** Le module écrit le rapport tel quel,
`line-height: calc(1.25 / 0.875)` ; le build l'enrobe dans le repli d'une propriété que personne ne
définit, `var(--recit-unfolded, calc(1.25 / 0.875))`, que le navigateur calcule exactement (une
seule déclaration, pas d'héritage de propriété locale, `!important` gardé) :

- `postcssUnfoldLineHeight` (`packages/site/src/vite/css/line-height.ts`, exporté par
  `@recit/site/vite`), dernier plugin PostCSS de `recit-style` et du Storybook ;
- `unfoldLineHeights` dans `scripts/build-css-modules.js`, appliqué au source avant Lightning CSS.

Seul `line-height` est protégé (les `calc()` à unités, `font-size: calc(14px * 1.5)` → `21px`, se
plient sans perte). Les contournements à la main reviennent à l'écriture simple pour rester
cohérents : `--cards-leading` (`Cards`), `--navigation-menu-leading`, `--hero-leading`,
`--heading-lh` (`Heading`) et les 24 littéraux « arrondis vers le haut » (`1.4286`, `1.3334`,
`1.5556`, `1.1112`) de 14 modules de `packages/site`. Les littéraux de `Typography.module.css`
(`1.33333`, `1.42857`, `1.71429`…) copient ceux du plugin typography : gardés. Piège « Folded line
heights » du skill `mantine-style` réécrit avec la règle finale. Plan de version
`version-plan-line-height.md` (`prerelease`).

**Preuves.** Feuilles construites : `line-height:1.42857` 41 → 1 et `1.33333` 26 → 1 dans
`sites/docs/dist` (le reste : `Typography`), `1.4286` 13 → 0 ; 56
`var(--recit-unfolded,calc(1.25 / .875))`, 24 `…calc(1 / .75))` ; idem `www` et `presentation` ;
`dist/styles.css` des paquets : plus aucune valeur pliée. Mesure Playwright sur l'hôte (boîtes d'une
ligne, avant → après) : `StatusBadge__root` 12 px de `docs /formats` 15,984 → 16 px (×9),
`OneSource__caption` 15,984 → 16 (×2) et `CodeBlock__title` 14 px 19,984 → 20 px sur `www /` ;
hauteur de page `docs /` 2 279 → 2 280 px, `www /` 3 708 → 3 709 px, `main` de `docs /` 1 794,453 →
1 794,5 px.

**Vérifications.**

```
$ pnpm exec nx run-many -t build -p docs www presentation components → vert
$ node scripts/build-css-modules.js --check --all → à jour
$ pnpm test:related line-height.ts build-css-modules.js plugin.ts → 34 tests verts
$ pnpm check:imports && pnpm check:other → vert ; pnpm check : 3 fichiers mal formatés hors lot
  (AnnotationCanvas.stories.tsx, Banner.tsx, NavigationFilterInput.tsx)
$ pnpm test:e2e:host e2e/site-docs.spec.ts → 51 passés
```

Les captures `-linux` bougent d'un pixel là où une ligne pliée précédait : à régénérer à la porte.

### À faire à la toute fin du plan 19 (demande de l'utilisateur, 30/10)

- Écrire **`audit-design.md` à la racine du dépôt**, avec des captures d'écran de tout : chaque page
  de chaque site (clair, sombre, mobile), chaque composant par ses stories, et un regard de designer
  sur l'ensemble (cohérence des espacements, de la typographie, des couleurs et contrastes, des
  états, du mode sombre, du mobile), avec les défauts et les pistes. Les images dans un dossier à
  côté (par ex. `audit-design/`), prises dans le conteneur Playwright comme les références, après la
  dernière porte.

### HYDRA — Hydratation et défauts d'exécution — 30/10

Suivis de GATE4, STY6.3b, STY5a et TST3b. Aucune commande git, pas de `pnpm install`, pas de
`pnpm fix`, aucune référence régénérée, pas de conteneur.

**React #418 (`/custom-components`, `writing/mdx`) et surlignage prérendu de `recit-config` : une
seule cause, la limite de temps de Shiki.** Reproduit sur l'hôte avec le build de production
(`/custom-components`, trois erreurs sur trois chargements) ; la différence n'était ni la date ni le
fuseau mais le bloc ` ```tsx ` du `Counter` : le chunk client compilé portait d'autres jetons que le
HTML prérendu (` c` / ` +` au lieu de `c` / `+`, `)}` `>` `Clicks: {count}` `</` au lieu d'un seul
jeton), donc d'autres nœuds texte. Les couleurs claires du chunk client étaient fausses (`return` en
`#6F42C1`, `button` en `#E36209`), les sombres justes. Shiki limite la tokenisation d'une ligne à
500 ms (`tokenizeTimeLimit`) : une ligne qui dépasse est coupée et toutes les suivantes héritent
d'un mauvais état. Le premier bloc qu'un processus surligne paie la compilation des regex de la
grammaire, et le thème clair passe le premier ; sous la charge d'un build (client et SSR en
parallèle dans le même processus, puis le prérendu, plus les builds des autres agents), ce premier
passage dépassait parfois. Le client build, le SSR build et le prérendu obtenaient alors chacun
leurs couleurs : #418 quand le MDX compilé différait, et le `--shiki-light:#D73A49` sur
`{ defineConfig }` du bloc `data-recit-runtime-shiki` prérendu (surlignage d'exécution du SSR). Pas
un état partagé entre pages au sens d'une fuite : l'état est le cache de regex du surligneur
module-level, qui rend la première page plus lente que les autres. Non déterministe : un build
propre de 01:31 n'avait plus l'erreur. Reproduit en Node avec `tokenizeTimeLimit: 1` sur un
surligneur neuf (bloc coupé après `onClick=`).

**Correction.** `lib/shiki.ts` : `tokenizeTimeLimit: 0` dans `defaultHighlightOptions`, utilisé par
la chaîne MDX, `Markdown` et `highlight()` (SyntaxHighlight, CodeTabs) : même jetons partout, quel
que soit le temps. `lib/shiki.test.ts` (nouveau) : sous une horloge où chaque lecture avance d'une
seconde, `codeToHast` et `highlight()` donnent le même résultat que sans ; contre-épreuve à 500 ms,
les deux tests échouent. **Reste ailleurs** : `@recit/core` (`compile-story.ts`, `@shikijs/rehype`)
garde la limite par défaut pour les blocs des decks ; même risque de couleurs fausses au build (sans
#418 : le deck est compilé une fois), à reprendre par son propriétaire avec un plan de version.

**« Last modified ».** Déjà en UTC au prérendu comme au client, l'infobulle n'est rendue qu'après le
montage. Vérifié sans git (`GIT_DIR=/nonexistent`, comme un conteneur sans `.git`) : `git log`
échoue, les deux compilations prennent le `mtime` ; HTML prérendu et chunk client portent le même
`lastModifiedTime` (`2026-10-28T04:02:06.056Z` pour `custom-components`). Le seul chemin où les deux
compilations pouvaient différer, le repli `new Date()` d'un fichier introuvable, est mémorisé par
chemin (`vite/mdx/remark-last-modified.ts`).

**`IndexingDialog`.** La réindexation démarre à l'ouverture du dialogue (`onOpenChange` du
`Dialog`), plus au montage ; « Retry » reconnecte ; la fermeture et le démontage ferment le flux
(référence de l'`EventSource`). Stories : `IndexingDialog/Indexing` vérifie qu'aucune connexion n'a
lieu avant l'ouverture, puis une ; `PagefindSearch` remplace son `EventSource` muet par un
`EventSource` qui enregistre (`onConnect`), et `Closed` vérifie qu'ouvrir la recherche ne connecte
rien. Contre-épreuve (démarrage au montage rétabli) : `Indexing`, `Failed` et `Closed` échouent.

**`viewTransition`.** Cause exacte (sonde Chromium) : après `skipTransition()`, c'est la promesse
`ready` de la transition sautée qui rejette (« AbortError: Transition was skipped ») ; React Router
n'écoute que `finished`. Reproduit sur le docs construit : deux clics de barre latérale à 5 ms →
`pageerror`. `lib/util/viewTransition.ts` (nouveau) : `handleSkippedViewTransitions(document)`
enveloppe une fois (`Proxy`, `WeakSet`) `document.startViewTransition` et marque traité le seul
`AbortError` de `ready` ; tout autre rejet reste non traité, et qui attend `ready` voit toujours
l'erreur. Installé par `SiteProvider` (effet, client seulement), racine commune du site et du
harnais Storybook. Contournement retiré de `Navigation/Stacked` ; la transition jouant désormais,
l'étape « Back » attend la fin de l'animation (`waitFor` sur `toBeVisible`). Tests :
`viewTransition.test.ts` (5) ; contre-épreuve (rejet relancé) : « Unhandled Rejection ». Après
rebuild, la sonde des deux clics ne voit plus d'erreur.

**Vérifications.**

```
$ pnpm exec vitest run lib/shiki.test.ts lib/util/viewTransition.test.ts → 7 verts
$ pnpm test:related (shiki, viewTransition, SiteProvider, IndexingDialog, remark-last-modified)
  → 25 fichiers, 208 tests verts
$ pnpm test:stories search-pagefind navigation → 15 fichiers, 46 tests verts (Navigation ×3)
$ pnpm -F @recit/site typecheck → muet
$ GIT_DIR=/nonexistent pnpm exec nx run-many -t build -p docs components → vert (deux passages
  rouges avant : dist de cli et de core vidés par des builds concurrents)
$ sondes Playwright (preview du build) : aucune erreur console sur custom-components, writing/mdx,
  recit-config ; nœuds texte prérendu = client ; aucun jeton aux couleurs croisées
$ BROWSER=none pnpm test:e2e:host e2e/site-docs.spec.ts e2e/site-components.spec.ts
  e2e/site-behaviour.spec.ts → 90 passés, 1 sauté
$ pnpm check → biome vert (avertissements), check:imports et check:other verts ; fmt:check rouge
  sur packages/formats/deck/src/tools/AnnotationCanvas.stories.tsx (autre lot)
```

- (orchestrateur, suite de HYDRA) `@recit/core` `compile-story.ts` : `tokenizeTimeLimit: 0` pour
  `@shikijs/rehype`, comme `packages/site/src/lib/shiki.ts` ; version plan
  `version-plan-shiki-time-limit.md` (`prerelease`). 197 tests liés verts.

### A11Y-SITE — Accessibilité du framework de sites — 30/10

Lot d'accessibilité de la porte de la vague 5 (`packages/site`), en parallèle de STORYFIX2, LH,
A11Y-DECK, HYDRA. Aucune commande git, pas de `pnpm install`, pas de conteneur. Tous les
`a11y: { test: "todo" }` de `packages/site` sont retirés : axe est en erreur partout ; reste
`InkeepSearch` en `!test` (widget tiers chargé d'un CDN avec la clé d'un site, pas un défaut).

**Contrastes** (rendu changé, voulu) :

- `ui/Alert` : destructive en clair `light-dark(red-700, --destructive)` (4,36 → 6:1) ; le texte
  d'`info` prend le bleu de l'alerte (`inherit`) au lieu du gris atténué (4,43:1).
- `ui/CodeTabs` : onglet inactif `color-mix(--muted-foreground 90 %, --foreground)` (4,42 → 5,3:1).
- `ui/Kbd` : même mélange (4,39:1 sur `--muted`, vu dans le pied de la recherche Pagefind).
- Shiki : `colorReplacements: { "github-light": { "#e36209": "#bc4c00" } }` dans
  `defaultHighlightOptions` (3,49 → 5,03:1), l'orange du thème clair actuel de GitHub. Une surcharge
  plutôt qu'un autre thème : seul ce jeton échoue, le reste de `github-light` passe, et un site qui
  choisit ses thèmes garde leurs couleurs (la clé est le nom du thème).
- `Banner` : couleurs nommées d'un cran plus sombres (blue-600, green-700, amber-700, red-600, ≥
  4,76:1) et texte blanc forcé (`--mantine-color-white`) : en sombre, `--primary-foreground` est
  foncé et échouait aussi.
- `Typography` : citation en gris opaque `lab(48.5 %)` (2,95 → 4,73:1).
- `NavigationBadge` : texte quasi noir sur les fonds clairs (blanc 1,77:1 sur vert) ; les badges
  `invert` passent aux 700/800 de Tailwind en clair, 400 en sombre (≥ 4,5:1 sur la barre).

**Noms** : bouton « Dismiss the banner » ; « Clear the filter » et champ nommé dans
`NavigationFilterInput` ; `nav` « Sections » (`TopNavigation`), « Sidebar » (`NavigationWrapper`), «
Pagination » ; `HeaderNavigation` était déjà « Main » (`NavigationMenu`) ; `Modal` du harnais
`SearchFixture` (`closeButtonProps`) ; `TocPopoverButton` sans `aria-label`, nommé par son texte
visible (« On this page » ou la section).

**Rôles et structure** :

- `Pagination` en `nav` ; `Spinner` en `progressbar` indéterminé nommé (`label`, « Loading ») ;
  `PageProgress` reste `aria-hidden`, justifié en commentaire (valeur simulée, bruit à chaque lien).
- `NavigationCategory` : les items dans un `div` (plus de `ul` sans `li`) ; `aria-expanded` et
  `aria-controls` sur le chevron seul. `MobileTopNavigationDrawer` : chaque séparateur dans un
  `li aria-hidden` (axe `list`, masqué jusqu'ici par le `todo` du `Separator`).
- `ui/Separator` décoratif : `renderRoot` qui retire l'`aria-orientation` que `Divider` écrit
  toujours (ses props filtrent les `undefined`).
- `ui/DropdownMenu` : `withInitialFocusPlaceholder={false}` et notre propre sentinelle `aria-hidden`
  (celle de Mantine, `role="presentation"` focalisable, comptait comme enfant du `menu`). Le
  comportement au clavier ne change pas.
- `ui/NavigationMenu` : un menu de liens est une divulgation, pas un `menu` : `withRoles: false`
  (passé par étalement, absent du type de `Menu`), `renderRoot` sans `role`/`aria-orientation`,
  `aria-expanded`/`aria-controls` posés par le déclencheur, même sentinelle.
- `ui/Command` : la liste n'est `listbox` (et le combobox `aria-expanded`) que si des items sont
  montés (compteur dans le contexte) ; vide, l'état vide se lit comme du texte.
- `ui/Input` : bordure destructive sous `aria-invalid`, anneau au focus (`:where`, sous la règle de
  `InputGroup` qui retire l'anneau du contrôle) ; `InputGroup` avait déjà son état.
- **Point gris de la `Dialog`** : le bouton de fermeture hérité du framework d'origine, un rond de 8
  px en haut à gauche qui ne devenait une croix rouge qu'au survol. Remplacé par la croix de
  shadcn/ui en haut à droite, dans `Dialog` et dans `CommandDialog` (même rond, dans la recherche ;
  la ligne de saisie y gagne 40 px de marge à droite).

**`Banner` mémorisée** : oui, `localStorage` (`recit-banner-dismissed`, via `useLocalStorage` de
`@mantine/hooks`, lu après l'hydratation) garde l'id de la dernière bannière fermée. Nouvelle clé
`site.banner.id` (`SiteConfig.ts`) ; sans elle, un message texte est son propre id, un message JSX
n'est fermé que pour la page. Une bannière fermée apparaît un instant avant l'effet (pas de script
avant peinture : à reprendre si un site s'en sert).

Stories touchées hors lignes `a11y` (nécessaire) : `Banner` (`beforeEach` qui vide la clé, bouton
trouvé par son nom), `NavigationFilterInput` (bouton par son nom), `MdxPage` `FullWidth` (« On this
page »).

**Rendu changé, à régénérer à la porte** : Storybook `Site/UI/Alert`, `CodeTabs`, `CodeTabPanel`,
`SyntaxHighlight`, `Kbd`, `Dialog` (tous), `Command` (dialogues), `Input` `Invalid`,
`Site/Components/Banner` (couleurs), `Markdown`, `Mermaid` `ParseError`,
`Site/Navigation/NavigationBadge`, `NavigationItem` (badge), `Site/Plugins/PagefindSearch` (croix,
`Kbd`) ; pages : tout bloc de code clair avec un jeton orange (docs, components, presentation,
plans…), toute citation de prose, le dialogue de recherche (`site-docs` « shows the search dialog
»).

**Vérifié.**

```
$ pnpm test:stories packages/site sites/storybook → 128 fichiers (2 sautés), 488 tests verts
$ pnpm test:related <mes 54 fichiers>          → 22 fichiers, 163 tests (shiki.test.ts de HYDRA
  rouge un instant pendant son édition, vert seul ensuite)
$ pnpm -F @recit/site typecheck                → seule erreur : util/viewTransition.ts (HYDRA)
$ pnpm exec nx run docs:build                  → vert (deux échecs transitoires « Could not
  resolve @recit/theme » / « @recit/deck » pendant les builds concurrents)
$ BROWSER=none pnpm test:e2e:host e2e/accessibility.spec.ts e2e/site-docs.spec.ts
  e2e/site-behaviour.spec.ts                   → 62 verts, 1 sauté
$ node scripts/check-style-rule.js packages/site → aucune hausse (7 sous la base)
$ pnpm check → biome lint et check:other verts ; fmt:check rouge sur 3 fichiers d'autres lots
  (AnnotationCanvas.stories, shiki.test.ts, viewTransition.test.ts)
```

### A11Y-DECK — Accessibilité du deck, des slides et du motion — 30/10

Lot d'accessibilité ouvert par TST4a (défauts du deck et du motion) et les `todo` de TST1 et TST4b,
en parallèle de STORYFIX2, LH, A11Y-SITE et HYDRA. Aucune commande git, pas de `pnpm install`, pas
de conteneur. Les `play` existants sont intouchés ; les nouvelles stories sont des tests du lot.

**Stylo d'`AnnotationCanvas`.** Le canevas est à la taille de design (largeur du parent, qu'une
transformation ne change pas), le pointeur lu à l'écran : `toCanvasPoint` (`tools/canvas-point.ts`,
interne) ramène le décalage par le rapport des deux, le trait part sous le pointeur à toutes les
échelles. Pointer Events (souris, doigt, stylet) avec capture du pointeur, `touch-action: none` sur
le canevas, un trait à la fois ; l'état « en train de dessiner » passe dans une ref. Preuves :
`canvas-point.spec.ts` (0,5 → 600 px pour 300 px à l'écran) et la story
`Deck/AnnotationCanvas/PenUnderThePointer` (tracé au doigt dans le cadre `medium`, pixel peint sous
le pointeur, rien à mi-distance ; rouge avec l'ancien calcul, vérifié).

**Clavier.** `c` est promis par la doc (`presenter-console.mdx`, `quickstart.mdx`) : `Presentation`
câble `onClearAnnotations`, une clé qui remonte le calque (story `Deck/Presentation/PenAndClear`).
La navigation par chiffres n'est promise nulle part : retirée du §4.3 du plan 21 (remplacée par
Maj+D et `c`). `useKeyboardNav` ignore les touches venues d'un `dialog` ouvert (Espace y presse le
bouton, pas la diapositive suivante).

**`RemoteQrCodeModal`** : un `<dialog>` natif ouvert par `showModal()` (rôle `dialog`, modal, page
derrière inerte donc focus piégé, focus sur « Close », rendu à l'ouverture précédente à la
fermeture), nommé par son titre (`h2`, `aria-labelledby`), Échap appelle `onClose` sans fermer de
lui-même ; voile en `::backdrop`. Story `ModalDialog`.

**Repères et titres.** Pied de `SlideFrame` en `div` (un `<footer>` dans le `group` de la
diapositive était un `contentinfo` imbriqué). `PresenterView` : nom de la console en `h1`, « Speaker
notes » en `h2` ; `MobileRemoteView` : `h4` → `h2`.

**Contrastes** (mesurés par axe, clair) :

| Défaut                                        | Avant               | Correction                                                                          |
| --------------------------------------------- | ------------------- | ----------------------------------------------------------------------------------- |
| Pied de `SlideFrame` sur un `bg` de scène     | 2,4:1 (4,45 sombre) | noir ou blanc selon la luminosité OKLCH du fond (`oklch(from …)`, sous `@supports`) |
| Compteur de `PresenterView`                   | 4,34:1              | atténué mêlé à 15 % de premier plan (5,4:1)                                         |
| Position de `MobileRemoteView`                | 1,68:1              | primaire mêlée à 50 % de blanc (6,5:1)                                              |
| Notes vides de `MobileRemoteView`             | 3,89:1              | slate-400 (7:1) ; bouton « NEXT » en `--primary-foreground` (2,9:1 en sombre)       |
| « Session » de `RemoteQrCodeModal`            | 4,46:1              | atténué mêlé (5,5:1)                                                                |
| Badges de `DevHud` (`deck.css`)               | 3,76 et 3,18:1      | emerald-700 et amber-700 sous le blanc (5,5 et 5:1)                                 |
| `slide-badge-*` (`Badge`, `HtmlSlide` absent) | 2,7 à 3,8:1         | emerald-700, amber-800, red-700 ; 400 en sombre (`.dark`)                           |
| Texte d'un `callout` dans une diapositive     | 4,0 à 4,2:1         | `.slide .slide-callout :is(p, li)` atténué mêlé (5:1 et plus)                       |
| Avertissement de `Grid`                       | 4,45:1              | amber-800 (6,3:1)                                                                   |
| Erreur de `Mermaid`                           | 3,14:1              | red-700 (5,3:1), red-300 en sombre (red-500 y faisait 4,16:1)                       |
| « Available » de `FormatSupport`              | 3,65:1              | emerald-700 en clair                                                                |
| Tendance de `Metric` (exemple)                | 2,47:1              | emerald-700 en clair, emerald-500 gardé en sombre (couleur seule)                   |

`Mermaid` : le `role="img"` ajouté après celui de Mermaid était un attribut en double, ignoré par
l'analyseur HTML. Le rôle graphique de Mermaid (`graphics-document`, le lecteur parcourt les
étiquettes) reste, le nom (`aria-label` du titre) remplace celui de Mermaid ; `role="img"` seulement
pour un `<svg>` sans rôle.

**Thèmes motion** (`@recit/theme`) : les jetons servent au texte (surtitres, légendes, valeurs),
corrigés. warm-editorial : `mutedForeground` 46,7 → 41 % (#795862), `primary`/`accent`/`beforeNum`
49,2 → 41 % (#8c4558), `afterNum` 42,4 → 34,5 % (#4a6661) ; saas-product : `mutedForeground` 53,7 →
43 % (#646a77), `beforeNum` 58 → 47 % (#cd2328), `afterNum` 33,3 → 27,5 % (#13794e) ; les barres
gardent leur couleur. `ui-scene` : tons `good`/`danger` #116c46/#b51f24 (texte, et 4,5:1 dans la
teinte d'une puce). `buildTheme` : atténué à 36 % du fond (42 % donnait 4,19:1). Ces thèmes servent
aussi aux decks qui les nomment : couleur un peu plus sombre, voulue. `THEME_CONTRAST_TODO`
supprimé. Goldens : `goldens:check` vert sans régénération (le texte bouge de 0 à 11 745 pixels
exacts par vue, sous le seuil de `pixelmatch` à 0,1) ; les régénérer à la porte si l'on veut qu'ils
portent la couleur exacte.

**i18n.** `UiStrings` gagne `slide` (« Slide » / « Diapositive », console, télécommande, HUD) et
`remotePrevious`/`remoteNext` (« ◀ PREV »/« NEXT ▶ », « ◀ PRÉC. »/« SUIV. ▶ ») ; l'anglais est
inchangé (les `play` et `e2e` qui lisent ces textes passent).

**Version plans (STY9.4), paquets publiés touchés** : `@recit/core` (trois clés d'interface, ajout),
`@recit/theme` (jetons de deux thèmes nommés, `deck.css` : badges, HUD, callout), `@recit/deck`
(stylo, `c`, dialogue, pied, titres, contrastes), `@recit/components` (`Mermaid`, `Grid`),
`@recit/motion-blocks` (tons `ui-scene`, `buildTheme`), `@recit/site-kit` (`FormatSupport`).

**À la porte, captures qui bougent** : stories `Deck/` (console, télécommande, QR, HUD, pied sur
fond), `Slides/Badge`, `Slides/Grid/Overloaded`, `Slides/Mermaid/InvalidChart`, `Kit/FormatSupport`,
`Slides/Examples/Metric`, `Motion/` (texte atténué et d'accent) ; e2e : console du présentateur,
diapositives à badges ou callouts, `deck-showcase` en clair (tendance), pages du site `motion` et
decks au thème nommé.

**Vérifications.**

```
$ pnpm test:stories → 130 fichiers (2 sautés), 488 tests verts, axe en erreur partout dans le lot
$ pnpm exec vitest run --project '!storybook' packages/formats/deck packages/formats/motion-blocks
    packages/components packages/theme packages/core → 49 fichiers, 455 tests
$ pnpm -F @recit/motion-render goldens:check → ✓ all goldens match (32 vues)
$ node scripts/build-css-modules.js --check --all → tables à jour
$ nx run-many -t build typecheck (core, theme, components, deck, motion-blocks, site-kit,
    site-stories, motion-render, cli, recit-storybook, deck-showcase, deck-mdx) → verts
$ recit build (deck-showcase, en et fr) → 13 diapositives
$ BROWSER=none pnpm test:e2e:host e2e/deck-features.spec.ts e2e/deck-behaviour.spec.ts → 12 passés
$ pnpm check → vert (avertissements biome hors lot)
```

### STORYFIX2 — Storybook construit, contournements retirés — 30/10

Suite de SBFIX : les 28 stories en échec dans `e2e/storybook.spec.ts` (Storybook **construit**, en
production, sans `act()` : le `play` court avant la fin des suspensions et des effets). Aucune
commande git, aucune dépendance, aucune référence régénérée, aucun `!test`.

**Causes et corrections, par story.**

- `Site/` (Header ×4, TopNavigation ×3, MobileNavigationDrawer, MdxPage…) : `useCurrentNavigation`
  suspend (`useSuspenseQuery` de `plugin-navigation`) et `SiteProvider` lit `context.initialize` par
  `use()`. Le loader `loadSiteHighlighter` devient `loadSiteHarness`
  (`.storybook/decorators/withSite.tsx`, inscrit dans `preview.tsx`) : il construit le client de
  requêtes et le `SiteContext` de la story, initialise les plugins, met la navigation de la page en
  cache (clé `["plugin-navigation", path]`) et attend le surligneur ; `withSite` les lit dans
  `loaded.site`. Plus rien ne suspend au premier rendu.
- Stories mobiles (`MobileTopNavigation` ×4, `MobileTopNavigationDrawer` ×2,
  `MobileNavigationDrawer`) : `globals.viewport` redimensionne le cadre du manager, pas la page
  `iframe.html` qu'ouvre l'e2e ; à 1280 px le bouton et le tiroir sont masqués (64em). La spec lit
  le viewport de la story (`storyGlobals` et `parameters.viewport.options`) dès qu'elle est connue,
  prend cette taille et recharge (erreurs remises à zéro). **Effet sur les captures** : les stories
  à `globals.viewport` (dont `Kit/Hero/Mobile`, `FormatGrid`, `OneSource`, `MobileRemoteView`) sont
  désormais capturées à 390 × 844.
- `NavigationWrapper/ScrolledToActive` : le `nav` n'avait pas de hauteur, `--header-height` étant
  absent. Les variables de mise en page sont sur `:root` dans `Layout.module.css`, et Vite donne aux
  modules CSS `moduleSideEffects: false` : sans `Layout` rendu, la feuille est éliminée du build (un
  `import` d'effet ne suffit pas). Nouveau `packages/site/src/lib/testing/layoutSheet.ts` (réexporté
  par `@recit/site/testing`), lu par `loadSiteHarness`. Module à part : lire un export défini dans
  `testing/index.tsx` tirait tout son graphe, dont des feuilles `@layer components` chargées avant
  `@layer base`, ce qui inversait l'ordre des couches (logo de `Kit/Hero` doublé). **Effet sur les
  captures** : les stories `Site/` ont maintenant les vraies variables de la mise en page.
- `Toc/FollowsTheSection`, `MdxPage/WithToc` : l'évènement `storybook:intersect` partait avant
  l'`observe()` (posé dans un effet) ; il est redispatché dans le `waitFor`.
- `Slot/Filled` : les sources s'inscrivent dans un effet ; `waitFor` sur la disparition du repli.
- `Deck/MdxSlide/FirstStep` : les étapes sont masquées par un effet ; `waitFor` sur la seconde.
- `Deck/MobileRemoteView/AfterTenMinutes` : le `waitFor` de `findBy*` démarrait son `setInterval`
  avant l'effet du minuteur et prenait le bouchon de `clockAt`. `clockAt` (fixture du deck)
  n'intercepte plus que la période d'une seconde ; la story règle `parameters.clock` (un seul
  bouchon par story, au lieu de deux `beforeEach` empilés).
- `Deck/RemoteQrCodeModal/Closed` : racine vide (la spec attend un élément) ; la story dessine une
  ligne de texte sous la modale fermée.
- `MdxPage` (×6) `SB_DOCS-TOOLS_0001` : react-docgen ne résout pas `MDXImport["default"]` (type
  indexé) et `storybook/docs-tools` fait un `console.error`. Des argTypes explicites n'y peuvent
  rien (l'extraction tourne quand même) : la prop `mdxComponent` est typée `MDXContent`
  (`mdx/types`, le même type), nommé.
- `Deck/Presentation Embedded` : vert au premier passage, sans changement.

**Contournements retirés (TST4a, TST4b).** `sites/storybook/tsconfig.json` : plus de `paths` vers
les paquets Storybook ni d'`include` des stories des autres paquets ; `tsconfig.json` de
`@recit/deck`, `@recit/motion-blocks`, `@recit/site-kit` et des deux exemples : plus d'`exclude` des
stories (chaque paquet typecheck les siennes) ; `preview.css` :
`@import "@recit/motion-blocks/styles.css"` par le paquet. Les `files` publiés excluent toujours
stories et fixtures.

**`player.test.tsx`** (`motion-blocks`) : le minutage reste (logique sans DOM) ; les tests de
`MotionFrame` rendent les stories `Start`, `Settled`, `SecondScene`, `Portrait` par `composeStories`
(`renderToStaticMarkup`, args surchargés pour l'échelle).

**Vérifié (hôte).**

```
$ pnpm -F recit-storybook build → vert
$ BROWSER=none pnpm test:e2e:host e2e/storybook.spec.ts --workers=3 → 979 passés, 0 échec
$ pnpm test:stories → 128 fichiers, 488 tests verts
$ tsgo --noEmit : recit-storybook, @recit/deck, @recit/motion-blocks, @recit/site-kit,
  @recit/site, recit-example-deck-mdx, recit-example-deck-showcase → verts
$ pnpm test:related (player.test, testing, MdxPage, fixture du deck) → 3 fichiers, 29 tests
$ pnpm check → vert (avertissements biome préexistants)
```

À la porte : captures de `e2e/storybook.spec.ts` (stories mobiles à 390 × 844, stories `Site/` avec
les variables de mise en page, `RemoteQrCodeModal/Closed`).

- (STORYFIX2) Les variables de mise en page (`:root` de `Layout.module.css`) n'existent dans une
  page que si `Layout` y est rendu (Vite élimine un module CSS dont on ne lit aucune classe) : à
  STY5c, les placer dans une feuille globale (`app/base.css`) rendrait `layoutSheet` inutile.

### Porte de la vague 5 — 30/10

- `pnpm check` vert. e2e complet dans le conteneur (installation 9 s grâce à INFRA, build 2 min 20
  s, tests 21 min) : 763 passés, 640 échecs triés — 638 stories nouvelles sans référence, et
  `plans /19-style` (contenu du plan modifié). **Aucune page de site ni story existante n'a bougé**
  : la correction des interlignes (LH) a ramené les pages à l'identique des références d'avant la
  migration.
- `pnpm test:e2e:update:linux e2e/storybook.spec.ts e2e/site-plans.spec.ts` : 1 049 passés en 6,9
  min (passage ciblé : installation 1 s, build de trois projets 64 s) ; 978 références Storybook au
  total. Échantillon relu (deck, motion, héros mobile) : propre. À regarder à l'audit final : dans
  `Motion/Blocks/CompareBars`, 247 kB et 114 kB ont des barres de même longueur.
- Les changements de couleur des thèmes `warm-editorial` et `saas-product` (A11Y-DECK) restent sous
  le seuil des captures : choix de marque à montrer à l'utilisateur avant/après.
- Suite : STY5c (retrait de Tailwind, `cn.ts`, dépendances, mesure des bundles), puis la vague 6.

### STY5c — Retrait de Tailwind — 30/10

**Fait.** STY5.2, STY5.3 et STY5.4 du plan 19, plus le retrait de Tailwind des paquets publiés que
STY6.1 avait reporté ici (CLI, `site-stories`, `@recit/theme`, Storybook). Aucune commande git ; un
`pnpm install` (−74 paquets, plus aucune ligne `tailwind` dans `pnpm-lock.yaml`).

**Retiré.**

- Site : `tailwindcss()` de `vite/plugin.ts` ; dans `plugin-theme.ts`, les `@source` (racine,
  dépendances, `__pluginDirs`) et le `@theme inline` que `transform` écrivait dans `main.css` (le
  hook garde son `enforce: "pre"` : il injecte `defaultTheme.css` et `theme.customCss` avant que
  `vite:css` lise la feuille). `__pluginDirs` reste pour la fusion des `vite.config` des plugins et
  le relevé de leurs versions, documenté dans `SiteConfig.ts`. `main.css` ne garde que les deux
  `@import` de Geist et les deux marqueurs d'injection ; la déclaration de couches reste dans
  `layers.css` (commentaire mis à jour : `base` est vide, gardé dans l'ordre).
- `lib/util/cn.ts` supprimé ; `shiki.ts` joint ses classes par une fonction locale
  (`joinClassNames`, sans fusion de classes Tailwind) ; l'export `cn` de `@recit/site` retiré
  (paquet privé, aucun importeur). `MissingIcon.tsx`, dernière chaîne `text-red-500`, passe par
  `MissingIcon.module.css` (le rouge 500 de la palette, en oklch).
- `biome.jsonc` : `tailwindDirectives`, l'override d'exception des derniers importeurs (vidé, donc
  supprimé), les `ignore` `plugin`/`theme`/`custom-variant`/`apply` de `noUnknownAtRules`, désormais
  en erreur partout (le site garde `mixin`) ; l'exception `noImportantStyles` passe de `main.css` à
  `reset.css`. `.vscode/settings.json` perd sa clé `tailwindCSS`.
- CLI : `bundle/css.ts` assemble la feuille d'un deck avec esbuild (déjà dépendance) au lieu de
  `@tailwindcss/node` et `@tailwindcss/oxide` : `reset.css` en `@layer base`, jetons, `page.css`,
  `deck.css`, `print.css`, feuilles des paquets, modules utilisateur (module virtuel
  `recit:modules.css`), `customCss`, dans le même ordre ; dépendances de la veille par le metafile ;
  `url()` d'un média inlinée comme dans `js.ts`, `url(/…)` laissée telle quelle ; minification par
  esbuild. L'option `sources` (scan Tailwind) disparaît de `CssBundleOptions` et de `build-deck.ts`.
- `site-stories/scripts/build-css.mjs` : plus de Tailwind (thème, utilitaires, `@source`,
  déclaration de couches `properties`/`theme`…) ; esbuild concatène jetons préfixés, `deck.css`,
  feuilles des paquets et `modules.css`, la passe `.recit-wrapper` est inchangée, Lightning CSS
  (déjà en dev) minifie avec les cibles qu'utilisait l'`optimize` de Tailwind.
- `@recit/theme` : `tailwind.css` et le peer optionnel `tailwindcss` supprimés.
- Storybook : `@tailwindcss/vite` de `main.ts`, `@import "tailwindcss"`, `tailwind.css` et `@source`
  de `preview.css`.
- Dépendances : `packages/site` perd les 24 `@radix-ui/*`, `@base-ui/react`,
  `class-variance-authority`, `clsx`, `tailwind-merge`, `cmdk`, `vaul`, `embla-carousel-react`,
  `react-hook-form`, `tailwindcss`, `@tailwindcss/vite`, `@tailwindcss/typography`, `tw-animate-css`
  (36, chacun vérifié sans importeur) ; `packages/cli` et `site-stories` perdent `tailwindcss`,
  `@tailwindcss/node`, `@tailwindcss/oxide` ; `sites/storybook` perd `tailwindcss` et
  `@tailwindcss/vite` ; le catalogue perd ses quatre lignes Tailwind. Restent, comme prévu :
  `next-themes` (STY9, plus aucun importeur) et `@tanem/react-nprogress` (`PageProgress.tsx`
  l'importe encore).

**Décisions.**

- **Preflight.** Il était dans toutes les surfaces (site en `base`, deck, Storybook) et chaque page
  est dessinée dessus : il devient `@recit/theme/reset.css`, copie règle à règle de
  `tailwindcss/preflight.css` 4.3.3 (MIT, ligne dans `NOTICE.md`), `--theme(--x, …)` écrit
  `var(--x, …)`, plus les deux familles par défaut que la couche `theme` de Tailwind déclarait
  (`--default-font-family: var(--font-sans)`, idem mono). Une seule feuille pour trois
  consommateurs, sans couche propre : le site l'importe en tête de `base.css` (`layer(theme)`), le
  deck en `layer(base)`, Storybook en `layer(theme)`. Le tri « ce dont le rendu dépend » n'est pas
  fait règle par règle : le deck et Storybook n'ont pas la baseline de Mantine, et la mesure
  ci-dessous montre l'identité avec la feuille entière.
- **`base.css` en couche `theme`**, après le reset dans la même couche : `* { border-color }` gagne
  toujours sur son `border: 0 solid`. Rien d'autre n'occupe `base`.
- **Variables de mise en page** : le bloc `:global(:root)` de `Layout.module.css` passe dans
  `base.css` (`:root`, couche `theme`) ; `TopNavigation` et `Banner` les remettent à 0 hors couche,
  `polyfills.ts` en ligne, donc ils gagnent toujours. `layoutSheet.ts` et son export de
  `@recit/site/testing` sont supprimés ; le harnais de Storybook (`src/harness/styles.ts`) importe
  `@recit/site/app/base.css` après `main.css`, comme `main.tsx`. Piège noté dans le skill.
  `sites/storybook/styles.css` (feuille de l'atelier, surfaces Mantine) rend à `html`/`body` leur
  flux d'avant (`height: auto`, `overflow: visible`) : `base.css` fait de `body` le conteneur de
  défilement d'un site, et axe refusait `Site/Plugins/Markdown/ChatGPTLogo` (SVG de 1 248 px sans
  élément focalisable, « scrollable-region-focusable »).
- **`defaultTheme.css`** reste un fichier à part, deuxième feuille de base du framework (nommée dans
  la loi : SKILL.md §5, plan §0.3, `isGlobalCss` du compteur et son test) : `theme.noDefaultTheme`
  doit pouvoir la sauter, ce que `base.css` ne permet pas ; et `@recit/theme/tokens.css` porte les
  mêmes noms (`--primary`…) en triplets HSL lus `hsl(var(--primary))` par les decks, quand le site
  les lit en couleurs complètes (oklch) : y mettre le zinc publierait deux formats sous les mêmes
  noms. STY9 tranchera avec le reste des variables shadcn (§2.4).
- **CLI** : retrait ici, comme STY6.1 l'avait écrit (« STY5c retire Tailwind »). Rupture pour un
  auteur qui écrivait des utilitaires dans un composant ou un `@apply` dans `customCss` : version
  plan `version-plan-tailwind-removed.md` (`@recit/theme`, `@recit/cli`, `@recit/site-stories` en
  `prerelease`). Aucun exemple ni aucune story de `sites/*/stories` n'écrivait de classe utilitaire
  (vérifié). Les pages qui le disent encore (`sites/docs/pages/theme/custom-css.mdx` l. 4 et 25,
  `theme/overview.mdx` l. 38, `sites/components/pages/custom-components.mdx` l. 58) sont laissées à
  STY10.2 (documentation de la rupture, §6.4), pour ne pas bouger leurs captures dans cette vague.

**Identité.** `dist` de `docs`, `www`, `presentation` et des six decks d'exemple construits avant
(scratchpad) et après, servis en statique ; Playwright compare, élément par élément, la boîte et
toutes les propriétés calculées (plus `::before`, `::after`, `::placeholder`, `::marker`), clair et
sombre, 1440 et 390 px :

- `docs` (`/`, `/documentation/writing/mdx`, `…/scenes`, `/documentation/reference/cli`,
  `/formats/motion`, `/404`, `/present/starter`, `/present/course`, `/formats/deck/layouts`,
  `/theme/named-themes`) : 9,4 millions de valeurs, **0 différence**.
- `www` (`/`, `/one-source`, `/stories/one-source`) : 1,35 million, **0**.
- `presentation` (`/`, `/demos/overview`, `…/showcase`, `…/mdx`, `…/site`, `/templates`,
  `/present/showcase`, `/present/mdx`) : 7,3 millions, **0** (un premier passage de `/demos/mdx`
  mobile sombre avait capturé le deck intégré à mi-chargement ; relancé, 0).
- Decks (les 39 diapositives des six exemples et deux de `deck-showcase/fr`, 1280 × 720, clair et
  sombre) : 1,9 million de valeurs ; seules différences, (a) les interlignes `calc()` des modules
  utilisateur que l'`optimize` de Tailwind pliait (`.l` du `Metric` : `1.55556` →
  `calc(1.75 / 1.125)`, 28,0001 px → 28 px, quelques boîtes de 0,016 px), ce que LH demandait déjà ;
  (b) `background-position` calculée `0px 0px` → `0% 0%` sur `pre.mermaid` et les onglets de code
  (`background: transparent` que Lightning CSS réécrivait `0 0`), sans image donc sans effet ; le
  reste, des identifiants aléatoires de Mermaid.

Sur l'hôte, avec comparaison des captures aux références `-linux` (tolérance 2 % de la config) :
`e2e/storybook.spec.ts` 979 passés ; `site-docs`, `site-behaviour`, `accessibility`, `deck-render`,
`site-www`, `site-presentation` : 110 passés, 1 sauté.

**Bundles (STY5.4, gzip, comparés à STY0.6).**

| `sites/docs`                | STY0.6 avant | STY0.6 après | avant STY5c | après STY5c |
| --------------------------- | ------------ | ------------ | ----------- | ----------- |
| CSS d'entrée                | 43,5 ko      | 79,0 ko      | 68,4 ko     | 66,1 ko     |
| CSS d'entrée, brut          | 247 ko       | 496 ko       | 434 ko      | 416 ko      |
| JS d'entrée                 | 139,6 ko     | 147,7 ko     | 141,1 ko    | 140,7 ko    |
| JS total (somme des chunks) | —            | —            | 3 475 ko    | 3 466 ko    |

`www` : CSS d'entrée 68,5 → 66,3 ko, JS d'entrée 121,6 → 121,2 ko (STY0.6 avant : 43,4 et 120,5).
`presentation` : 68,2 → 65,9 ko et 138,6 → 138,2 ko. Decks : `main.css` de `deck-starter` 8,6 → 7,9
ko (44,3 → 38,8 ko brut), `deck-showcase` 11,9 → 11,4 ko ; `main.js` inchangé. Le JS d'entrée ne
baisse presque pas ici : Radix, `cmdk`, `vaul` n'avaient déjà plus d'importeur (vagues 3 et 4) ;
`cn` et `tailwind-merge` sortent du chunk paresseux `shiki-*.js` (d'où les −9 ko du JS total). Le
CSS reste au-dessus de STY0.6 avant (+22,6 ko) : c'est la feuille Mantine entière (~35 ko), dont
l'import par composant reste l'option de STY9.

**Compteur** (`node scripts/check-style-rule.js`, baseline réécrite) : 17 occurrences → **1**
(baseline 24 → 1). Reste la règle globale de `sites/storybook/.storybook/preview.css` (fond de
`#storybook-root` et `.docs-story` en jetons de deck) : du chrome d'atelier, pas un composant ;
`sites/storybook/styles.css` n'est chargé que pour les surfaces Mantine et lit les jetons du site,
la règle ne peut pas y aller. À trancher pour `--strict` (STY9) : l'exempter dans `isGlobalCss`
comme feuille de base de Storybook.

**Vérifié.**

```
$ pnpm exec nx run-many -t build typecheck -p @recit/site docs www presentation components motion plans vision recit-storybook
  → 32 tâches vertes
$ pnpm exec nx run-many -t build typecheck -p @recit/cli @recit/theme @recit/site-stories → 16 vertes ; pnpm publint → 12 paquets
$ pnpm exec nx run-many -t build -p 'recit-example-deck-*' → vert
$ pnpm exec vitest run packages/cli scripts/__tests__/check-style-rule.spec.ts packages/site/src/vite/plugin-theme.test.ts
  → 8 fichiers, 193 tests
$ pnpm check → vert (18 avertissements biome préexistants, aucun dans mes fichiers)
$ pnpm test → 2 235 passés, 3 échecs : ChatGPTLogo ×2 (corrigé ci-dessus, vert seul) et
  packages/mcp/test/markdown.test.ts « render reads a .md story » (délai de 5 s sous charge, 17/17 seul)
$ pnpm test:stories → 128 fichiers, 488 tests
$ pnpm exec nx run recit-storybook:build ; playwright --project=chromium e2e/storybook.spec.ts → 979 passés
```

À la porte : la galerie devrait être vide pour les sites et les decks ; `plans /19-style` bouge
(contenu du plan) ; les stories `Site/` reçoivent maintenant `base.css` (jusqu'ici absent du harnais
: `body`, `* { border-color }`, ligatures du code…), passées sous la tolérance sur l'hôte, à relire
dans le conteneur ; les interlignes dépliés des decks (≤ 0,016 px) ne devraient pas dépasser le
seuil.

### Porte de STY5c, vague 6 lancée — 30/10

- Le premier passage e2e après STY5c a été coupé pendant le build par la fin de session ; relancé :
  **1 401 passés**, 2 échecs (`plans /19-style`, contenu du plan modifié), 12 min (installation 1 s,
  build 2 min 22 s). Le retrait de Tailwind n'a fait bouger ni une page ni une story.
- Vague 6 lancée : STY9 (attribut de mode unique, `next-themes` retiré, compteur et `style-law.grit`
  en erreur, version plan unique `version-plan-style.md` qui absorbe les trois plans de la branche,
  bundles, `pnpm verify`), STY10.2 (documentation finale, `AGENTS.md`, `CONTRIBUTING.md`,
  `NOTICE.md`, pages de thème dont `mantine.mdx`), TST5 (dernières stories, compteur de stories en
  erreur, couverture, page et skill « testing », découpage de `storybook.spec.ts`).
- Après la vague 6 : porte finale (e2e complet deux fois, captures des nouvelles stories et des
  pages dont le contenu change, galerie avec l'utilisateur, avant/après des thèmes `warm-editorial`
  et `saas-product`), puis `audit-design.md`.

### STY10.2 — Documentation finale — 30/10

Aucune commande git, pas de `pnpm install`, pas de conteneur.

- **`AGENTS.md`** (jamais `CLAUDE.md`, lien vérifié) : « UI » ne dit plus shadcn/ui (composants
  Mantine enrobés de leurs modules dans `@recit/site/ui`, Mantine seule bibliothèque, mode par
  `data-mantine-color-scheme` et `light-dark()`) ; « Styling » décrit l'état final (plus « in
  progress ») : la loi, les feuilles globales permises, les modules hors `packages/site` (Lightning
  CSS, `./styles.css`, esbuild de la CLI), le compteur et `style-law.grit` en erreur (STY9) ;
  nouvelle section « Tests » (story et `play` par composant, axe en erreur, `pnpm test:stories`,
  `check-stories.js`, page `documentation/reference/testing` et skill `testing` de TST5).
- **`CONTRIBUTING.md`** : pre-commit relu contre `lefthook.yml` (les deux compteurs, sans « baseline
  » : STY9 a retiré celle du style) ; §4 gagne « Style » (renvoi au skill), « Stories » (la phrase
  de TST5) et « Inner loop ».
- **`NOTICE.md`** : la ligne `packages/site/` dit que l'interface a été réécrite sur Mantine le
  30/10 et que le code shadcn/ui, Radix et Tailwind du fork est parti ; ligne Mantine 9 (MIT) dans
  les dépendances notables ; la ligne du reset (Tailwind 4.3.3, MIT, STY5c) inchangée.
- **Docs** : `theme/custom-css.mdx` (description et l. 25 : feuille simple chargée après le thème et
  les modules, ni utilitaires ni `@apply`), `theme/overview.mdx` (carte « Custom CSS »),
  `sites/components/pages/custom-components.mdx` (module CSS à côté du composant). Nouvelle page
  `theme/mantine.mdx` (« Advanced » de la barre du thème) : où vont les styles d'un site, couches,
  variables Mantine, modules (`rem()`, mixins sur un site, CSS simple dans un deck), mode de
  couleur, « Migrating a site from Tailwind utilities » (ce qui ne marche plus, quoi écrire,
  avant/après en MDX et en `@apply`, table courte). Seuls changements d'oxfmt sur cette page :
  l'alignement des deux tableaux et un retour à la ligne (copie formatée en scratchpad, comparée,
  puis reprise).
- **Barre latérale** : `theme/mantine`, et `documentation/reference/testing` pour TST5. **La page de
  TST5 n'existe pas encore** : tant qu'elle manque, `docs:build` échoue (« File not found for
  document 'documentation/reference/testing' ») ; build et e2e ci-dessous passés sans cette ligne,
  remise ensuite.
- **`03-packaging.md`** : `@recit/theme` (`reset.css`, `fonts.css`, `tailwind.css` et
  `site-compat.css` retirés, plus de peer), `./styles.css` de `components`, `site-stories`, la CLI
  sans Tailwind, `site-kit` porte les composants des sites, paragraphe « Feuilles de style publiées
  », catalogue (`@mantine/*` 9.7.4, plus de `tailwindcss`).
- **Pas d'agent `style-reviewer`** : tout ce qu'il vérifierait sur un diff est déjà mécanique et en
  erreur (les sept signaux de `check-style-rule.js` dont les props de style Mantine,
  `style-law.grit` et les imports de l'ancienne pile dans Biome, la liste « Before reporting » du
  skill) ; le reste, les différences de la galerie, est la revue humaine de la porte. Un agent
  dupliquerait la loi dans un troisième texte à tenir à jour.

Vérifié : `oxfmt --check` sur mes fichiers ; `check-admonitions`, `check-frontmatter`,
`check-site-links`, `check:imports` verts ; `check-style-rule.js` à 0. `pnpm check` échoue sur des
fichiers en cours d'autres lots (oxfmt : stories de `packages/site`, `CodeBlock.module.css`,
`html-template.ts`, `check-style-rule.spec.ts` ; `@recit/components` `dist/styles.css` périmé).
`nx run-many -t build -p docs components` vert (sans la ligne `testing`, après deux échecs dus à des
builds concurrents de `@recit/components`) ; `BROWSER=none pnpm test:e2e:host e2e/site-docs.spec.ts`
: 51 passés. `/theme/mantine` n'est pas dans la liste de pages de `site-docs.spec.ts` : à ajouter à
la porte (nouvelle capture).

### STY9 — Bascule finale — 30/10

**Fait** : STY9.1, STY9.2, STY9.4 et la mesure de STY9.3 ; aucune commande git, un `pnpm install`
(retrait de `next-themes`), pas de conteneur.

**Un seul attribut de mode (STY9.1).** `data-mantine-color-scheme` sur `<html>` est la seule
condition de mode ; plus rien n'écrit ni ne lit la classe `dark`.

- Pont retiré de `SiteRoot.tsx` (`ColorSchemeBridge` et la recopie du script de tête) ; la copie de
  l'ancienne clé `theme` vers `recit-color-scheme` reste (donnée, pas dépendance). `next-themes`
  quitte `packages/site/package.json` et le lockfile ; Biome et le compteur refusent toujours son
  import.
- `@recit/core` exporte `COLOR_SCHEME_ATTRIBUTE` et `DARK_SCHEME` (`theme-css.ts`, aussi par
  `@recit/core/browser`) ; le bloc sombre de `buildThemeCss` s'écrit
  `[data-mantine-color-scheme="dark"]` (et `… <scope>` en portée).
- Feuilles : `tokens.css`, `deck.css` (Shiki, badges), `defaultTheme.css`, `base.css`, le
  `DARK_SELECTOR` de `plugin-theme.ts` (le `.dark,` disparaît du sélecteur double), les modules
  `CodeBlock`, `EmbeddedCodeBlock` (site), `Grid`, `Mermaid` (`components`), `Metric` de
  `deck-showcase` ; `page.css` et `print.css` n'en avaient pas. `site-stories/scripts/build-css.mjs`
  préfixe le bloc sombre des jetons en `[data-mantine-color-scheme="dark"] .recit-wrapper`.
- Deck : `useThemeMode` pose l'attribut (`light` ou `dark`) au lieu de la classe ;
  `useSlideEnhancements` et `Mermaid` lisent l'attribut et l'observent (`attributeFilter`) ; le
  script de démarrage de `recit build` (`html-template.ts`) pose l'attribut avant la première
  peinture ; `export-pdf.ts` ne fait que semer `recit-theme`, inchangé. Inkeep lisait déjà
  l'attribut (commentaire). Storybook : `withThemeByDataAttribute` seul.
- e2e : `colorScheme()` ne lit plus que l'attribut (la classe ne compte plus) ; `deck-features` lit
  l'attribut après `t`.
- Aucun `customCss`, `sites/*/styles.css` ni page de doc du dépôt n'écrivait `.dark`. Rupture pour
  un auteur qui l'écrivait : dans le version plan.
- **Défaut ancien relevé, laissé** : la passe `.recit-wrapper` de `site-stories` préfixe aussi les
  cinq règles sombres de `deck.css`
  (`.recit-wrapper [data-mantine-color-scheme=dark] .slide .shiki`, badges) : elles ne s'appliquent
  jamais, l'attribut étant sur `<html>` (c'était déjà vrai de `.recit-wrapper .dark …`). Le Shiki
  des sites passe par `CodeBlock.module.css` ; les badges d'un deck intégré restent aux couleurs
  claires en sombre. Corriger (réécrire ces sélecteurs en `[attr] .recit-wrapper …`) bougerait des
  captures : à décider à la porte.

**Identité** (dist d'avant, celle de la porte de STY5c, copiée en scratchpad ; après, reconstruite ;
Playwright compare élément par élément la boîte et toutes les propriétés calculées, `::before` et
`::after`, 1440 px, sombre et clair) : `docs` (`/`, `…/writing/code-math-diagrams`,
`/formats/deck/layouts`, `/theme/named-themes`, `/present/starter`) 3,8 millions de valeurs, `www`
(`/`, `/one-source`, `/stories/one-source`) 1,29 million, `presentation` (`/`, `/demos/showcase`,
`/demos/tech-talk`, `/present/showcase` et `/present/tech-talk`) 3,56 millions plus les 13
diapositives de `/present/showcase?slide=N` en sombre 0,96 million, `deck-starter` seul (3
diapositives, deux modes) 77 000 : **0 différence**. Sur `<html>`, seule la classe `dark` disparaît
; le deck seul porte maintenant `data-mantine-color-scheme`. Mermaid rendu `dark` et Shiki sur
`--shiki-dark` dans le deck intégré, comme avant ; `t` bascule l'attribut dans le deck seul.

**Compteur et Biome en erreur (STY9.2).** `check-style-rule.js` n'a plus qu'un mode, l'erreur :
baseline (`scripts/style-rule-baseline.json`), `--baseline`, `--write-baseline`, `compare` et
`mergeBaseline` supprimés ; `--strict` accepté (c'est ce qu'écrit `check:other`). Le signal 6 compte
aussi un sélecteur `.dark` (`html.dark`, `:global(.dark)`, `.dark:is()`), pour que la classe ne
revienne pas. `isGlobalCss` exempte `sites/storybook/.storybook/preview.css` avec sa raison (feuille
de base de l'atelier : elle assemble la feuille du deck et peint le fond de l'aperçu aux jetons du
deck ; `sites/storybook/styles.css` ne lit que ceux du site). Compte : **0**. `style-law.grit` passe
en `error` ; `biome.jsonc` sort de son `includes` les tests et les stories, que le compteur saute
aussi (les six occurrences restantes étaient des `className` de données : `language-ts`, marqueurs
de test). `biome lint` : 0 erreur, 13 avertissements préexistants (spécificité, gabarit).
`check-style-rule.spec.ts` réécrit (62 tests).

**Version plan (STY9.4).** `.nx/version-plans/version-plan-style.md`, `prerelease` pour `core`,
`theme`, `components`, `deck`, `motion-blocks`, `motion-render`, `site-stories`, `cli` : la ligne de
§10.2, les deux ruptures (attribut de mode ; plus de Tailwind dans un deck, modules CSS de l'auteur
compilés par `recit build`), `./styles.css`, puis par paquet : premières valeurs du deck dans la CSS
du thème et `invalid-scene-background`, trois clés d'interface, Shiki sans limite (`core`) ;
`SlideStage` sans `maxScale`, stylo, `c`, dialogue, titres, contrastes (`deck`) ; jetons de
`warm-editorial` et `saas-product`, `deck.css` (`theme`) ; `Mermaid`, `Grid` (`components`) ; tons
et `buildTheme` (`motion-blocks`) ; interlignes non pliés. Absorbe et supprime
`version-plan-line-height.md`, `version-plan-shiki-time-limit.md` et
`version-plan-tailwind-removed.md`. `@recit/site-kit` est privé (`"private": true`, hors de
`release.projects`) : absent. `check-version-plans.js` : 16 plans valides.

**Bundles (STY9.3, gzip)**, comparés à STY0.6 et STY5c :

| `sites/docs`       | STY0.6 avant | STY0.6 après | après STY5c | après STY9 |
| ------------------ | ------------ | ------------ | ----------- | ---------- |
| CSS d'entrée       | 43,5 ko      | 79,0 ko      | 66,1 ko     | 66,1 ko    |
| CSS d'entrée, brut | 247 ko       | 496 ko       | 416 ko      | 416 ko     |
| JS d'entrée        | 139,6 ko     | 147,7 ko     | 140,7 ko    | 141,1 ko   |
| JS total (chunks)  | —            | —            | 3 466 ko    | 3 476 ko   |

Le JS de `docs` bouge avec son contenu (page et entrée de sidebar de TST5 et STY10.2) ; à contenu
égal, le pont parti fait −0,4 ko brut : `www` JS d'entrée 121,2 → 121,1 ko (400 656 → 400 268 o
brut), `presentation` 138,2 → 138,1 ko. CSS d'entrée +275 o brut (le sélecteur d'attribut est plus
long que `.dark`), gzip identique à 0,01 ko près (`docs` 66,1, `www` 66,3, `presentation` 65,9).
`deck-starter` : `main.css` 7,9 ko, `main.js` 89,8 ko, inchangés. Le CSS reste au-dessus de STY0.6
avant (+22,6 ko) : la feuille Mantine entière ; son import par composant reste une piste, pas faite.

**Vérifications.**

```
$ node scripts/check-style-rule.js --strict → 0, « The style rule holds »
$ pnpm check:other, pnpm check:imports, biome lint → verts ; oxfmt --check : seul
  sites/docs/pages/documentation/reference/testing.mdx (TST5, en cours)
$ pnpm exec nx run-many -t build typecheck → 60 tâches vertes
$ pnpm test → 2 254 passés, 6 échecs sous la charge des agents concurrents (délais de pipeline,
  compile-story, cli stories, mcp ; Presentation/Embedded et PagefindSearch dans le navigateur),
  tous verts relancés seuls (43 + 15 tests)
$ pnpm test:stories → 146 fichiers, 521 tests
$ BROWSER=none pnpm test:e2e:host e2e/site-behaviour.spec.ts e2e/site-docs.spec.ts
    e2e/deck-features.spec.ts → 64 passés, 1 sauté
```

`pnpm verify` d'une traite échoue donc sur l'oxfmt de la page de TST5 ; ses trois étapes suivantes
sont vertes séparément. À la porte : aucune capture ne devrait bouger du fait de STY9 (identité
mesurée) ; `AGENTS.md` (STY10.2) doit dire que l'attribut est la seule condition de mode et que
`.dark` est refusé par le compteur.

### TST5 — Clôture des tests — 30/10

Dernier lot du plan 21 (§8, TST5). Aucune commande git, pas de `pnpm install`, pas de dépendance,
pas de conteneur.

**5.1 — Stories.** `Typography` (`Default` : toute la prose, titres 1 à 4, listes imbriquées,
citation, tableau, code, `kbd`, filet ; `Paragraphs`), `Heading` (`Default` : l'ancre atteinte au
clavier, son `href`, jamais cliquée ; `WithoutAnchor` ; `Levels` : six niveaux et leurs ancres),
`util/MdxComponents` (`Site/Components/MdxComponents` : une page MDX élément par élément, `h1` à
`h6` avec leurs ancres, listes, tableau défilant, citation, bloc de code titré avec sa copie,
callout ; `Links` : le lien de page et le lien interne passent par le routeur, `LocationSpy` le
prouve, le lien externe ouvre un onglet). Puis les **18** restants du compteur, tous dans
`site/components` : `AiAssistantMenuItems` (menu ouvert, `window.open` espionné ; personnalisé ;
`false`), `AnchorLink` (premier clic navigue, second fait défiler sans naviguer), `Bootstrap`
(entrée client sur routeur mémoire ; entrée statique rendue en HTML par `renderToString` dans un
loader, comme le prérendu), `ClientOnly`, `LandingPage` (`hero`, `split`, `grid`), `Layout`, `Main`
(barre latérale, pleine largeur, mobile : « Menu » ouvre le tiroir, Échap le ferme), `Meta` et
`PluginHeads` (balises de tête lues par `getElementsByName`), `SiteRoot` (contexte, composants MDX,
mode de l'atelier), `StatusPage` (500, 404, message, code inconnu), et dans `context/` :
`RouterEventsEmitter` (l'évènement `location` écrit dans un `output`), `SiteProvider` (plugin lent,
`use(initialize)`), `SlotProvider` (slots de la config, source ajoutée), `ViewportAnchorContext`
(`storybook:intersect`). `SiteReactContext.tsx` → `.ts` : un contexte, sans JSX, pas un composant.
21 fichiers, 33 stories, toutes avec un `play`. Aucune classe ni style.

- `withSite` : `parameters.site.harness: false` laisse une story hors du harnais (un routeur ne se
  rend pas dans un autre : `Bootstrap`). `main.ts` pré-groupe `react-dom/server` et
  `@unhead/react/server` (découverts en cours de passage, ils faisaient recharger Vite).
- **Compteur en erreur.** `scripts/check-stories.js` n'a plus qu'un mode : tout signal échoue, la
  liste des manques s'affiche ; baseline, `--baseline`, `--write-baseline`, `compare`,
  `mergeBaseline` supprimés, `scripts/stories-baseline.json` supprimé, `--strict` accepté (c'est ce
  qu'écrit `check:other`). Spec : 11 tests. Compte : **0** (142 composants).

**5.2 — Cliquet de couverture.** Les seuils sont une option **racine** de Vitest : le
`vitest.config.ts` d'un projet ne peut pas en poser. Ils sont donc dans le `vitest.config.ts`
racine, un glob par paquet (lignes, branches) plus le total, au niveau mesuré arrondi à l'unité
inférieure, `autoUpdate: (n) => Math.floor(n)` (un passage complet qui mesure plus les remonte, sans
décimales ; vérifié : un passage au même niveau réécrit le fichier à l'identique).

| Paquet (lignes / branches) | TST0, 29/10 | TST5, 30/10   | Seuil     |
| -------------------------- | ----------- | ------------- | --------- |
| core                       | 89,8 / 73,9 | 90,59 / 74,90 | 90 / 74   |
| motion                     | 86,6 / 73,3 | 87,61 / 73,46 | 87 / 73   |
| scripts                    | 87,1 / 81,7 | 84,51 / 80,04 | 84 / 80   |
| app                        | 73,1 / 57,0 | 73,05 / 56,96 | 73 / 56   |
| site-stories               | 68,6 / 51,0 | 69,56 / 51,47 | 69 / 51   |
| site                       | 66,4 / 54,7 | 78,81 / 70,68 | 78 / 70   |
| site-kit                   | 66,7 / 37,7 | 93,58 / 80,49 | 93 / 80   |
| components                 | 63,1 / 35,3 | 73,85 / 46,23 | 73 / 46   |
| motion-blocks              | 59,0 / 33,1 | 65,74 / 40,18 | 65 / 40   |
| mcp                        | 54,8 / 41,8 | 54,81 / 41,82 | 54 / 41   |
| cli                        | 48,7 / 38,3 | 48,82 / 38,32 | 48 / 38   |
| deck                       | 27,2 / 20,2 | 52,34 / 39,33 | 52 / 39   |
| motion-render              | 20,6 / 6,9  | 20,59 / 6,90  | 20 / 6    |
| theme                      | —           | 100 / 83,33   | 100 / 83  |
| recit                      | —           | 100 / 100     | 100 / 100 |
| **total**                  | 66,8 / 54,3 | 72,78 / 60,37 | 72 / 60   |

Les stories ont fait le gain : `site` +12 / +16, `site-kit` +27 / +43, `deck` +25 / +19,
`components` +11 / +11. `scripts` baisse : les deux compteurs ont perdu leur code de baseline, et le
test « holds the law » de `check-style-rule.spec.ts` (STY9) expire à 5 s sous la couverture.

**5.3 — Doc et skill.** `sites/docs/pages/documentation/reference/testing.mdx` (frontmatter des
voisines, icône `flask-conical`) : la pyramide en tableau, les tests unitaires, les stories par
surface, écrire une story, écrire un `play`, les lancer, l'e2e (hôte et conteneur, la table des six
specs Storybook), lire la galerie. Seuls changements d'oxfmt (que `fmt:check` exige) : retours à la
ligne et alignement des tableaux, copie formatée en scratchpad puis reprise.
`.claude/skills/testing/SKILL.md` : quel niveau, la recette d'une story et d'un `play`, les
moqueries (presse-papiers, `IntersectionObserver` et `storybook:intersect`, `EventSource`, horloge
`clockAt`, `window.open`/`fetch`, `LocationSpy`), les pièges (production sans `act()`,
`globals.viewport`, `href="#…"`, `parameters.layout` ignoré par Vitest, dépendance découverte en
cours de passage, racine vide, état de module), les interdits de §5.2, la liste de contrôle. Le
skill `e2e-baselines` connaît le projet `storybook`.

**5.4 — Découpage de `storybook.spec.ts` et durées.** La spec unique devient `e2e/storybook.ts` (le
code, `storybookSuite(surface)`, la table `STORYBOOK_SURFACES` par préfixe de titre) et six specs :
`storybook-site-ui` (`Site/UI`, 237 tests), `storybook-site` (`Site/Components`, `Navigation`,
`Plugins`, 331), `storybook-slides` (117), `storybook-deck` (127), `storybook-motion` (169),
`storybook-kit` (`Kit/`, `Sites/`, 69). Chaque spec vérifie aussi qu'aucune story n'échappe à une
surface. Projet Playwright **`storybook`** (`testMatch` des six specs, ignorées par `chromium`) dont
le `snapshotPathTemplate` fixe le dossier `__screenshots__/storybook.spec.ts/` : **aucune référence
ne change de nom ni de place**. `scripts/e2e-projects.js` mappe les six specs sur `recit-storybook`
; `test:e2e:host` passe `--project=chromium --project=storybook`. L'assertion de mode lit
`data-mantine-color-scheme` (STY9) au lieu de la classe. Une porte ciblée :
`pnpm test:e2e:update:linux e2e/storybook-site.spec.ts`.

```
$ pnpm test               → 313 fichiers, 2 263 tests, 108 s (112 s mur) ; TST0 : 176 / 1 758, 69 s
$ pnpm test:stories       → 148 fichiers, 521 tests, 70,6 s (74 s mur) ; TST0 : 10 / 17, 13 s
$ pnpm -F recit-storybook build → 25 s
$ pnpm test:coverage      → 167 s mur, seuils tenus
$ BROWSER=none pnpm test:e2e:host <les six specs storybook> --workers=3
                          → 1 049 passés, 7,0 min (hôte chargé par d'autres lots)
$ … e2e/storybook-kit.spec.ts e2e/storybook-site-ui.spec.ts → 306 passés, 3,4 min
```

**Vérifié.** `pnpm check` vert (compteur de stories et compteur de style en erreur, 0 chacun) ;
`pnpm -F @recit/site typecheck`, `pnpm -F recit-storybook typecheck`, `tsgo` de `e2e/` muets ;
`pnpm exec vitest run scripts/__tests__/check-stories.spec.ts scripts/__tests__/e2e-linux.spec.ts`
verts ; `pnpm exec nx run docs:build` vert, `testing.html` produit. Échecs vus, hors lot, tous sous
la charge de la machine (plusieurs lots en parallèle, charge 10 à 15) : délais dépassés de
`compile-story.spec.ts` (Shiki double thème, 20 s), `check-style-rule.spec.ts` « holds the law » (5
s, sous la couverture), `mcp/test/markdown.test.ts` (5 s), et `Deck/Presentation/Embedded` (axe
`color-contrast` sur le `h1`, 1,07 puis 1,32:1, une fois sur trois passages : une transition de
couleur saisie en cours, à regarder avec `useThemeMode` de STY9).

**À la porte.** 33 stories nouvelles, toutes sous `Site/Components` : 66 captures à écrire par
`pnpm test:e2e:update:linux e2e/storybook-site.spec.ts`. Aucune autre référence ne bouge.

### FINALFIX — Badges sombres intégrés, contraste de Presentation/Embedded — 30/10

Deux défauts avant la porte finale du plan 19. Aucune commande git, pas de `pnpm install`, pas de
conteneur.

**Défaut 1 — règles sombres de `deck.css` dans un deck intégré.** La passe qui préfixe la feuille de
`@recit/site-stories` par `.recit-wrapper` écrivait
`.recit-wrapper [data-mantine-color-scheme="dark"] …` : l'attribut est sur `<html>`, au-dessus du
conteneur, donc les cinq règles sombres de `deck.css` (badges `success`, `warning`, `danger`,
couleurs et fond Shiki d'un bloc de code dans une diapositive) ne s'appliquaient jamais (déjà vrai
avec `.dark`). La passe sort de `build-css.mjs` dans `scripts/scope-to-wrapper.js` (avec sa
déclaration `.d.ts`) : un sélecteur qui commence par l'attribut de mode devient
`[data-mantine-color-scheme="dark"] .recit-wrapper …`. Test :
`src/__tests__/scope-to-wrapper.spec.ts` (5 tests). Mesuré dans la feuille construite : badge
`success` `rgb(4, 120, 87)` en clair, `rgb(52, 211, 153)` en sombre (il restait vert foncé). Ligne
ajoutée au version plan `version-plan-style.md`.

Rendus sombres qui changent (sites qui importent `@recit/site-stories/styles.css`) : tout deck
intégré qui montre un badge `success`/`warning`/`danger` ou un bloc de code. Captures probables :
`components-badge-and-kbd`, `components-present-badge-and-kbd`, `components-code`,
`components-present-code` ; `presentation-home` et `presentation-present-hero` (badge `success` du
héros, bloc de code) ; `presentation-demos-showcase`, `presentation-present-showcase`,
`docs-present-showcase` ; `presentation-demos-mdx`, `presentation-present-mdx`,
`presentation-present-mdx-blocks`, `docs-present-mdx` ; `tech-talk`, `course`, `lorem-ipsum` et
`site-embed` là où leur diapositive capturée porte du code. Les decks construits par `recit build`
(`deck-*`) ne bougent pas.

**Défaut 2 — `Deck/Presentation/Embedded`.** Pas une affaire de mode : `site-stories` et la story
passent `manageTheme={false}`, `useThemeMode` n'écrit donc pas l'attribut de l'hôte. Reproduit 1
fois sur 3 machine au repos ; la cible d'axe est le `h1` « Sed do eiusmod » de la diapositive 2,
couleur du titre à quelques pour cent d'opacité sur le fond (`#f0f2f5` sur `#f8fafc`) : axe passe
après le `play`, qui finissait dès que le groupe « 2 / 5 » existait, au milieu du fondu d'entrée
(0,28 s). Correction dans la story : `settled()` attend que la diapositive partie ait quitté le DOM
et que la nouvelle soit à l'opacité 1, dans `Embedded` et à la fin de `FirstSlide` (même exposition
après `Home`).

```
$ pnpm test:stories packages/formats/deck/src/deck/Presentation.stories.tsx → 6 passages verts de suite
$ pnpm test:stories packages/formats/deck → 3 × 63 tests verts
$ BROWSER=none pnpm test:e2e:host e2e/storybook-deck.spec.ts --grep Presentation → 3 × 24 passés
$ pnpm exec nx run-many -t build -p @recit/site-stories @recit/deck docs presentation → vert
$ BROWSER=none pnpm test:e2e:host e2e/site-presentation.spec.ts e2e/site-docs.spec.ts
    e2e/deck-features.spec.ts → 87 passés
$ pnpm check → vert ; typecheck de site-stories, deck, recit-storybook muets
```

À la porte :
`pnpm test:e2e:update:linux e2e/site-components.spec.ts e2e/site-presentation.spec.ts e2e/site-docs.spec.ts e2e/site-embed.spec.ts`
écrira les captures sombres listées plus haut.

### Porte finale du plan 19 — 30/10

- Premier passage complet : 1 399 passés, 318 échecs. Deux causes : le projet `chromium-dark`
  prenait aussi `storybook-site*.spec.ts` (motif `site-.*`) et rejouait 236 stories dans son propre
  dossier de captures — `testIgnore: STORYBOOK_SPECS` ajouté ; et `/index` du site multilingue
  rechargé en boucle, seulement dans la suite complète (la spec seule passe, dans le conteneur comme
  sur l'hôte), disparu une fois le doublon retiré (surcharge).
- Références écrites pour les changements voulus : 66 stories `Site/Components` (TST5), les pages
  `deploy-vercel`, `reference/testing`, `theme/mantine` ajoutées à `site-docs.spec.ts`, et le texte
  changé de `theme/custom-css`, `custom-components`, `plans /19-style`. Les badges sombres des decks
  intégrés (FINALFIX) restent sous le seuil.
- **Deux passages complets verts consécutifs** : 1 480 passés, 0 échec, 10,6 min chacun
  (installation 2 s, build 2 min 20 s). `pnpm check` vert. 1 659 références au total.
- Avant/après des thèmes `warm-editorial` et `saas-product` montré à l'utilisateur (accent, texte
  atténué et chiffres un cran plus sombres, barres inchangées) ; décision en attente.
- **Le plan 19 est terminé.** Restent à l'utilisateur : les captures darwin (STY9.3) et le commit.
  Suite : `audit-design.md`.

### Audit de design — 30/10

Demande de l'utilisateur pour la fin du plan 19 : `audit-design.md` à la racine, un regard de
designer sur tout le rendu, avec des captures. Aucune commande git, pas de `pnpm install`, aucun
code ni aucune référence modifiés, pas de conteneur : l'audit lit les 1 659 références existantes.

- **`audit-design.md`** : méthode et gravités, fondations (jetons, typographie, mode sombre, rayons,
  grille), surfaces (navigation, prose, recherche, decks, motion, mobile, cohérence des sept sites),
  composants par groupe de stories, états, fiabilité des captures, synthèse (forces, dix actions,
  jetons et composants à unifier), galerie, inventaire. 44 constats numérotés `D-01` à `D-44`.
- **`audit-design/`** : 86 références copiées par surface (`sites/<site>/`, `decks/`, `motion/`,
  `components/<groupe>/`) et 24 recadrages de détail (`details/`), 9,3 Mo.
- **`audit-design/inventory.mjs`** : réécrit le bloc « Inventaire » (chaque capture en lien vers
  `e2e/__screenshots__/`, groupée par page et variante, et la couverture calculée contre
  `sites/*/pages`, `sites/*/stories` et `storybook-static/index.json`), puis lance oxfmt.

Constat bloquant (D-01) : 199 références datent du 27/10, d'avant la migration (96 pages claires de
sites, tout `deck-render`, tout `motion-dev`) ; le seuil de 2 % les garde valides. On y voit encore
l'ancien nom du framework sur `docs-home` clair et une police de repli dans toutes les vues
`/present` claires. `app-bundle-home-linux` est une capture pleine page recousue (en-tête et page
répétés). À régénérer en entier avant la prochaine revue visuelle.

Dix actions de l'audit : références fiables ; une source de jetons (les triplets HSL de `tokens.css`
et les `oklch` de `defaultTheme.css` portent les mêmes noms, `hsla(var(--border))` est invalide sur
le site) ; une couleur de marque ; une sans et une mono (Geist sur les sites, Inter ailleurs) et
seulement des polices embarquées (Georgia des exemples) ; une teinte de surfaces sombres ; focus à
3:1 (halo de `ui/Button` à 1,55:1) et croix des dialogues à vérifier ; défauts de composants
(CompareBars, `.slide-kbd`/`.slide-badge` étirés en colonne flex, actions de `LandingPage` sans
style de bouton, `FormatSupport` hors prose, saut de `Banner`) ; typographie de slide ; mobile ;
navigation entre sites et Mermaid aux couleurs de Récit.

Trous de couverture : mobile sur `docs` seulement (4 captures) ; `plans` 20, 21 et journal ; story
`named-theme` de `docs` ; aucun survol de page ; darwin d'avant la migration (STY9.3).

```
$ node audit-design/inventory.mjs     → 1 659 captures inventoriées, 522 stories, 0 sans capture
$ pnpm run fmt:check                  → vert
$ pnpm exec biome lint audit-design   → vert
```
