# 28 — Stories : le format `stories` (stories web) et story.recit.dev

**Statut : proposé · Phase : P11 · Mis à jour : 02/11/2026.**

Ce plan détaille la phase P11 de [06 · Feuille de route](/06-roadmap) : le format `stories` (cartes
verticales plein écran qu'on fait défiler d'un tap), son jumeau JSON, son lecteur, ses exports
(images, film) et le site story.recit.dev. Il part de [08 · Formats](/08-formats) §6, qu'il précise
sans revenir sur les décisions acceptées ; chaque désaccord avec un plan accepté est posé en
question ouverte (§12). Il s'appuie sur le code au 02/11/2026.

Il dépend de [11 · Images](/11-images) (lots IM1 et IM2 logés en P11 : ` ```pixel ` et `textcard`),
de [14 · IA et scénario](/14-ia-scenario) (attributs de scène du scénario, Broc), de
[15 · Export PDF](/15-export-pdf) (pas de PDF pour `stories`, mais la même chaîne de capture), de
[09 · Langues et JSON](/09-langues-et-json), de [10 · Formulaires](/10-formulaires) (sondages), de
[03 · Packaging](/03-packaging) §6 et §8, de [07 · Domaines](/07-domaines) et de
[21 · Tests](/21-tests). Il réutilise la chaîne de capture livrée par le plan 23 (report, P6) et le
bloc d'image du film livré par [24 · Webtoon](/24-webtoon) (P7). Les ponts depuis `report` et
`calendar` attendent les plans 23 et 27.

**Le nom.** Le format `stories` porte le même mot que le modèle `Story` du cœur. `AGENTS.md` le
garde tel quel « on purpose » : le renommer est une décision de produit, pas un nettoyage. Ce plan
ne la prend pas ; il écrit `stories` partout et liste en §12 (question 1) ce qu'un renommage
toucherait et pourquoi il vaudrait mieux le décider avant ST0.

## 1. Pourquoi

- **La forme courte la plus lue sur mobile.** Des cartes `9:16` plein écran, une idée par carte, un
  tap pour avancer : c'est la forme des stories des réseaux et des Web Stories. Récit la sert depuis
  la même source Markdown que le reste.
- **Écrire une fois, publier partout.** Une story s'intègre dans une page de doc ou une landing
  (`StoryEmbed`), s'exporte en une image `1080×1920` par carte pour les réseaux, et devient un film
  vertical par le pont stories ⇄ motion.
- **La fin de la chaîne verticale.** Avec `motion` (P4) et `toon` (P7), `stories` ferme le trio des
  formats verticaux : une seule source donne la story, le webtoon et le film (`14` D6).
- **Le format des cartes de texte et des images en JS.** Le plan 11 a tranché que `textcard` et
  ` ```pixel ` sont des blocs de `motion` et de `stories`, pas un format : P11 est leur premier vrai
  client.

## 2. Ce qui existe déjà et ce qu'on réutilise

Relevé dans le dépôt le 02/11/2026.

| Élément                                               | État                                                                                                                                                                                              | Ce que P11 en fait                                                                                                    |
| ----------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | --------------------------------------------------------------------------------------------------------------------- |
| `packages/formats/stories`                            | stub privé (`@recit/stories` 0.0.0, `scope:format`), `src/index.ts` vide                                                                                                                          | paquet publiable sur le modèle de `packages/formats/deck`                                                             |
| `@recit/core/schema` (`source.ts`)                    | `StoriesSourceSchema = genericSource("stories")`, scènes lâches ; `stories` dans `STORY_FORMATS` ; `packages/core/schema/v1/stories.json` publié                                                  | schéma strict et typé, ajout compatible en `v1` (B1)                                                                  |
| `@recit/core/config` (`validate.ts`)                  | `StoriesConfigSchema` existe déjà, vide de réglages : il ne fait que refuser les anciennes clés de site (`dir`, `routePrefix`…) avec un renvoi vers `publish:`                                    | reçoit les réglages du format ; les refus restent                                                                     |
| `MotionSceneSourceSchema`                             | `type` (bloc), `dur`, `hold`, `props`, `attrs`, `body`, `voiceover`                                                                                                                               | les champs d'une carte en sont un **sur-ensemble** : le pont motion → stories devient fermé et presque gratuit (§5.4) |
| `@recit/motion/source`                                | `storyToFilm` et la table d'inférence fermée (`infer.ts` : un titre seul → `title`, un paragraphe → `statement`, une liste → `list`…) ; `site-stories/vite.ts` l'essaie déjà sur toute story deck | même table pour le pont stories → motion                                                                              |
| `@recit/motion-blocks`                                | `MotionFrame`, `filmTimeline`, `useFilmClock` (`clock.ts`), huit blocs, le cadre commun `blocks/frame.tsx`                                                                                        | une carte `type=stat` est un bloc joué dans la carte, à l'horloge de la carte                                         |
| `@recit/deck`                                         | rendu HTML d'une scène (`slides/SceneContent.tsx`, `HtmlSlide.tsx`), navigation clavier (`navigation-utils.ts`) ; **aucun geste tactile** (pas de swipe)                                          | rendu des cartes Markdown à réutiliser ; gestes à écrire (événements pointeur, sans dépendance)                       |
| Règles de densité (`core/src/rules/anti-overload.ts`) | mots visibles, puces, par scène, avec `RulesConfigSchema`                                                                                                                                         | seuils propres aux cartes                                                                                             |
| `@recit/site-stories`                                 | `plugin.tsx` choisit `MotionViewer` ou `StoryViewer` (deck) selon le format                                                                                                                       | une branche `stories` vers `StoryPlayer`, chargée paresseusement                                                      |
| `@recit/site-kit`                                     | `formats.ts` décrit `stories` ; `SITES` ne connaît pas `story`                                                                                                                                    | entrée `story` (port 3009)                                                                                            |
| La CLI                                                | `recit dev` a un aperçu propre au motion ; `recit export` (deck) sans `--to` ; `recit render` rend un récit motion                                                                                | aperçu stories ; `--to images` avec la chaîne de P6 ; `render` accepte une story `stories`                            |
| Plan 11                                               | `TextCardBlock`, `PixelBlock` (schémas prévus dans `@recit/core/schema`), `TextCard` statique et `PixelImage` dans `@recit/components`, blocs animés dans `@recit/motion-blocks`                  | une carte = une `textcard` (IM2.3), image d'une carte = ` ```pixel ` (IM1)                                            |
| Plan 24                                               | bloc `panel` du film (image dans un plan)                                                                                                                                                         | fond image d'une carte dans le film                                                                                   |

On ne crée pas : de bibliothèque de carrousel, de geste ou de lecteur vidéo ; de second moteur de
texte (`11` D4) ; de stockage pour les sondages ; de schéma de type hors de `@recit/core`.

## 3. Vocabulaire

- Une **scène** de `stories` est une **carte** : « carte » est le mot du lecteur, de l'URL
  (`?card=N`) et de l'interface, comme « slide » pour le deck. Le modèle garde `Scene`.
- Une **story** est le modèle compilé (`Story`), quel que soit le format. Dans la doc, un document
  `format: stories` se dit « une story (format stories) » tant que la question 1 n'est pas tranchée.
- Le rendu motion d'une story est un **film**. Un « deck » ne désigne que les slides.
- `textcard` est un bloc, pas une carte : une carte peut contenir une `textcard` (et c'est le cas
  principal, `11` D6).

À ajouter au glossaire d'`AGENTS.md` par l'intégrateur (ST6) : « carte ».

## 4. Le modèle

### 4.1 Markdown

```md
---
title: Lorem Launch
format: stories
lang: en
aspectRatio: 9:16
autoplay: true
dur: 5
---

--- id=hook background=./bg/01.webp alt="An abstract lorem gradient."

# Lorem ipsum.

--- id=proof dur=7 overlay=bottom

## :value{data=sales field=revenue agg=sum format=currency}

dolor sit amet this quarter

--- id=stat type=stat number=30 unit=s kicker="Lorem"

--- id=quote

:::textcard{variant=quote background=theme}
Lorem ipsum dolor sit amet.

— Lorem Ipsum
:::

--- id=cta background=#0b0b0f

# Try it now

::cta[Get started]{href=https://recit.dev}
```

| Élément                           | Rôle                                                                                                                                                            |
| --------------------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `background`                      | image, vidéo courte (`.mp4`, `.webm`, muette, en boucle), couleur, ou `theme` / `gradient` (mêmes valeurs que la `textcard`, `11` D4). Un seul nom, jamais `bg` |
| `alt`                             | description d'un fond image ou vidéo, localisable ; manquante : erreur en mode strict (`08` §6), avertissement sinon                                            |
| `overlay`                         | dégradé de lisibilité : `none`, `top`, `bottom`, `full`                                                                                                         |
| `dur`                             | secondes à l'écran en lecture automatique ; par défaut le `dur` de la story, ou la durée du bloc motion                                                         |
| `type`, `hold`, attributs de bloc | carte jouée par un bloc de `@recit/motion-blocks` (`stat`, `list`…), mêmes attributs qu'en motion (B4)                                                          |
| corps                             | Markdown habituel (titres, paragraphes, listes, `:value`), blocs transverses dans le corps (` ```pixel `, `:::textcard`, règle B2)                              |
| `::cta[label]{href}`              | bouton d'action en bas de la carte (balayage vers le haut) ; une seule par carte, en dernier                                                                    |
| `:::notes`                        | notes d'auteur, jamais affichées ; voix off dans le film                                                                                                        |
| `beat`, `cast`, `set`, `prompt`…  | attributs réservés du scénario (`14` D4), gardés dans `attrs` jusqu'à P16                                                                                       |

Deux écarts avec `08` §6, posés en question 2 : `08` écrit `duration: 5s` et `aspect: 9:16`. Le cœur
appelle déjà ces réglages `dur` (scène motion, secondes, nombre) et `aspectRatio` (frontmatter du
deck et du film). Ce plan propose les mots existants : un mot par chose, et le pont avec motion
copie les champs sans les traduire.

Règles :

- texte trop long pour une carte : erreur (règles de densité, seuils propres au format, `08` §6) ;
- `::cta` ailleurs qu'en fin de carte, ou deux `::cta` : erreur avec ligne ;
- `type=` inconnu de `@recit/motion` : erreur avec la liste des blocs ; `type=` quand
  `@recit/motion-blocks` n'est pas installé : erreur « installez … » (B1) ;
- fond introuvable : erreur avec ligne ; poids au-delà du budget : avertissement ;
- `aspectRatio` autre que `9:16` : erreur en P11 (question 6).

### 4.2 Le jumeau JSON

Dans `packages/core/src/schema/source.ts`, en zod/mini, types dérivés :

```ts
/** A background: a color, a theme keyword, or an image or video with its description */
export const StoriesBackgroundSchema = z.union([
  z.string(), // "#0b0b0f", "theme", "gradient", "./bg/01.webp"
  z.strictObject({ src: z.string(), alt: z.optional(LocalizedSchema) }),
]);

export const StoriesCtaSchema = z.strictObject({
  label: LocalizedSchema,
  href: z.string().check(z.minLength(1)),
});

export const StoriesSceneSourceSchema = z.strictObject({
  id: sceneFields.id,
  attrs: sceneFields.attrs,
  body: sceneFields.body,
  notes: sceneFields.notes,
  /** A motion block played in the card (same names as MotionSceneSource) */
  type: z.optional(z.string().check(z.minLength(1))),
  props: z.optional(z.record(z.string(), z.unknown())),
  dur: z.optional(z.number().check(z.positive())),
  hold: z.optional(z.number().check(z.gte(0))),
  background: z.optional(StoriesBackgroundSchema),
  overlay: z.optional(z.enum(["none", "top", "bottom", "full"])),
  cta: z.optional(StoriesCtaSchema),
});

export const StoriesSettingsSourceSchema = z.looseObject({
  aspectRatio: z.optional(z.literal("9:16")),
  autoplay: z.optional(z.boolean()),
  dur: z.optional(z.number().check(z.positive())),
  loop: z.optional(z.boolean()),
  theme: z.optional(z.union([ThemeModeSchema, z.string(), ThemeConfigSchema])),
  rules: z.optional(RulesConfigSchema),
  /** Round thumbnail of the story in a rail */
  cover: z.optional(StoriesBackgroundSchema),
  images: z.optional(StoriesImagesOptionsSchema), // ST3
});

export const StoriesSourceSchema = z.strictObject({
  ...storyFields,
  format: z.literal("stories"),
  settings: z.optional(StoriesSettingsSourceSchema),
  scenes: z.array(StoriesSceneSourceSchema),
});
```

- **`background` reprend la forme de l'image du deck** (`DeckImageSchema` :
  `string | { src, alt }`), et la conversion suit celle de `deckScene` : `background=… alt=…` dans
  le Markdown, objet en JSON. Ce plan propose de renommer ce type en `ImageSourceSchema` et de le
  partager (question 7) plutôt que d'en écrire un second.
- `09` §2.3 écrit `background`, `duration`, `cta { label, href }`, `overlay` : seul `duration`
  change (question 2). `cta` est un champ typé, comme `09` le prévoit ; le `::cta` du Markdown y est
  converti. La règle B2 (blocs transverses dans le corps) vaut pour `textcard` et `pixel`, pas pour
  `cta`, qui est propre au format.
- `type`, `props`, `dur`, `hold` ont **exactement** le sens de `MotionSceneSourceSchema` : les deux
  schémas partagent ces champs par un objet commun (`sceneBlockFields`), jamais par copie.
- `StoriesImagesOptionsSchema` va dans `packages/core/src/config/validate.ts` avec les autres
  options d'export ; `StoriesConfigSchema` reçoit les réglages (forme imbriquée de `recit.config`)
  et garde les refus des anciennes clés de site. Pas d'options PDF : `stories` n'a pas de PDF (`15`
  §1, tranché).

### 4.3 Conversion et aller-retour

- `storiesScene` dans `convert.ts` : attributs typés vers leurs champs, `::cta` final vers `cta`,
  `:::notes` vers `notes`, le reste du corps dans `body`. Un bloc ` ```yaml ` seul dans le corps
  d'une carte `type=…` donne `props`, comme en motion.
- Écriture canonique : `id type background alt overlay dur hold`, puis `attrs`, puis le corps, puis
  `::cta` en dernier.
- Propriété testée : `compile(md) == compile(toJson(md))` et `toMd(toJson(md)) == md` sur les
  fixtures et les exemples ; `source-conversion.spec.ts` couvre `examples/stories-*` sans code en
  plus.

### 4.4 Langues

- Une story par langue (`launch.fr.md`, même structure, mêmes `id`) ou un JSON qui porte toutes ses
  langues ; la parité vérifie en plus `cta.href`, `type`, `dur` identiques, et signale un
  `cta.label` ou un `alt` non traduit.
- Les fonds sont partagés (variante `01.fr.webp` possible, `09` §1). Une `textcard` ou un
  ` ```pixel ` se refait avec la langue (`11`).
- `dir` suit la langue : en RTL, les zones de tap et le sens du balayage s'inversent ; les barres de
  progression se remplissent de droite à gauche.

### 4.5 Scénario, personnages, images

- Le plan 14 (D4) écrit ses exemples en `stories` : `cast:` dans le frontmatter, `cast=`, `set=`,
  `pose=`, `prompt=` sur les cartes. En P11 ces attributs restent dans `attrs` ; `@recit/scenario`
  les lit en P16. Une carte avec `prompt=` et sans fond reçoit une **image de remplacement** neutre
  (avertissement, erreur en `--strict`), point d'accroche de `14` D7.
- Les images ne sont jamais générées par Récit. ` ```pixel ` (IM1) et `sprite` (IM4, plus tard)
  dessinent en JS, sans IA.
- **Broc.** Le scénario 2 de `sites/plans/pages/annexes/broc/` a une version `stories`. Elle vit
  dans `examples/scenario-lebrocoli` (règle B7), en P16, après la licence de l'image de référence (©
  vguerand, tous droits réservés). Rien de Broc dans `examples/stories-*`.

## 5. Le rendu

### 5.1 Le lecteur

Composants de `@recit/stories` (`08` §6), anonymes, un `X.module.css` chacun, une story et un `play`
chacun :

| Composant      | Rôle                                                                                                                                   |
| -------------- | -------------------------------------------------------------------------------------------------------------------------------------- |
| `StoryPlayer`  | racine : carte courante, horloge, clavier, gestes, `?card=N` (ou `?card=<id>`), plein écran sur mobile, cadre `9:16` centré sur bureau |
| `StoryCard`    | une carte : fond, voile, contenu (HTML de la scène, ou bloc motion), `StoryCTA`                                                        |
| `ProgressBars` | une barre par carte, remplie par l'horloge ; `role="progressbar"` sur la carte courante                                                |
| `TapZones`     | précédent (premier tiers), suivant (le reste), appui long = pause ; ce sont des boutons nommés, pas des zones muettes                  |
| `StoryCTA`     | lien d'action, balayage vers le haut ou clic                                                                                           |
| `StoryCover`   | vignette ronde d'une story                                                                                                             |
| `StoryRail`    | rangée de vignettes de plusieurs stories ; passer d'une story à l'autre par balayage horizontal                                        |
| `MuteToggle`   | seulement si un fond vidéo a du son (muet par défaut)                                                                                  |
| `ShareButton`  | `navigator.share` quand il existe, sinon copie du lien                                                                                 |
| `PauseButton`  | pause visible, toujours (critère WCAG 2.2.2 : un contenu qui avance seul plus de 5 s doit pouvoir s'arrêter)                           |

- **Contenu d'une carte.** Une carte Markdown se rend par le rendu HTML de scène du deck
  (`SceneContent`) ; si l'importer tire le runtime du deck dans le bundle, la partie commune descend
  dans `@recit/components` (ST1.2 tranche, mesure à l'appui). Une carte `type=…` se rend par
  `MotionFrame`, à l'horloge de la carte : le bloc joue son entrée puis tient.
- **Horloge.** `useFilmClock` de `@recit/motion-blocks` sert aussi aux barres de progression : une
  seule notion du temps pour la carte et son bloc. Pause quand l'onglet est caché
  (`visibilitychange`) ou pendant un appui long.
- **Gestes.** Événements pointeur natifs (tap, appui long, balayages), sans dépendance. Clavier :
  flèches, espace (pause), Échap (fermer dans un rail), Entrée sur le CTA.
- **Mouvement réduit.** Sous `prefers-reduced-motion`, la lecture automatique est coupée par défaut,
  les blocs motion montrent leur état final, une vidéo de fond montre sa première image.
- **Préchargement** de la carte suivante (image, vidéo, police), rien au-delà.
- **Mobile.** `100dvh`, marges de sécurité (`env(safe-area-inset-*)`), aucune barre du navigateur
  sur le texte ; le contenu se met à l'échelle par les unités de conteneur.
- **Style.** Loi du plan 19 : modules CSS compilés par Lightning CSS, `./styles.css` publié, valeurs
  dynamiques (couleur de fond, progression) en variables CSS par `useCssVariable`, `light-dark()`
  pour l'interface ; une carte garde les couleurs de sa story.
- **Embed.** `<StoryEmbed slug="launch" format="stories" />` dans une page, `StoryRail` en MDX ;
  plein écran sous `/present/<slug>`.

### 5.2 Les images (ST3)

- `recit export launch.md --to images` : un PNG `1080×1920` par carte et par langue
  (`--all-locales`), listés dans `exports.json` (variante `kind: "images"` de
  `ExportManifestSchema`).
- La CLI sert le build du lecteur et capture chaque carte par `@recit/paged/runner` : page
  `?card=N&settled=1`, fenêtre `360×640` à l'échelle 3. Les blocs motion sont à leur état final, les
  vidéos à leur première image. La `textcard` statique de `@recit/components` sert ici (IM2.3).
- Options typées (`StoriesImagesOptionsSchema`) : `format` (`png`, `jpeg`, `webp`, encodé dans
  Chromium), `scale`, `cards` (une sélection d'`id`), `maxBytes`.

### 5.3 Pas de PDF

Tranché le 24/10 (`15` Q5) : `recit export --to pdf` sur une story `stories` répond par une erreur
explicite qui renvoie vers `--to images` et `recit render`.

### 5.4 Le film (pont stories ⇄ motion, ST2)

Correspondance fermée, dans les deux sens (`08` §0) :

| Carte                | Plan du film                                                                                                                                                  |
| -------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `type=…` et `props`  | même bloc, mêmes props                                                                                                                                        |
| corps Markdown       | table d'inférence de `@recit/motion` (titre seul → `title`, paragraphe → `statement`, liste → `list`…) ; hors table : erreur avec ligne, jamais une devinette |
| `:::textcard`        | bloc `textcard` (IM2.2)                                                                                                                                       |
| `background` image   | couche de fond du plan : bloc `panel` du plan 24 ou fond du cadre commun (`blocks/frame.tsx`), à trancher en ST2.1                                            |
| `background` couleur | fond du plan                                                                                                                                                  |
| `background` vidéo   | erreur en P11 (question 8)                                                                                                                                    |
| `dur`, `hold`        | `dur`, `hold`                                                                                                                                                 |
| `notes`              | `voiceover`                                                                                                                                                   |
| `cta`                | perdu dans le film (un lien ne se clique pas dans un MP4) : avertissement                                                                                     |

Dans l'autre sens, un récit `format: motion` devient une story en changeant `format:` : chaque plan
devient une carte `type=…` avec son `dur`, et `voiceover` devient `notes`. Le sur-ensemble du §4.2
rend ce sens sans perte.

- `storiesToFilm` vit dans `@recit/stories/motion` (sous-chemin : le lecteur n'importe pas
  `@recit/motion` pour une story sans bloc). `recit render launch.md` reconnaît le format ; pas
  d'option `--as motion` (même proposition que la question 10 du plan 24).
- Les autres ponts de P11 (`08` §0) : **report → stories** (chaque `:::kpi` ou `::chart` → une carte
  ; `11` IM3 précise `:::kpi` → `textcard variant=stat`) et **calendar → stories** (les prochains
  événements → une story « à venir »). Ils dépendent de `@recit/data` (P6) et de `@recit/calendar`
  (P10) et forment le lot ST2b.
- **stories → toon** (`14` D6 : une carte = une case) est « plus tard », hors P11.

## 6. CLI et MCP

| Commande                                | Effet                                                              | Lot |
| --------------------------------------- | ------------------------------------------------------------------ | --- |
| `recit init --format stories`           | story Lorem de quatre cartes                                       | ST1 |
| `recit dev launch.md`                   | aperçu du lecteur, rechargement à chaud (`src/stories/client.tsx`) | ST1 |
| `recit build launch.md`                 | lecteur statique                                                   | ST1 |
| `recit validate [--strict] launch.md`   | schéma, densité, `alt`, `cta`, blocs, parité                       | ST0 |
| `recit convert launch.md --to json\|md` | jumeau, forme canonique                                            | ST0 |
| `recit export launch.md --to images`    | PNG par carte                                                      | ST3 |
| `recit render launch.md`                | film `9:16`                                                        | ST2 |

MCP (`@recit/mcp`), sans appel de modèle : `describe_format` accepte `stories` ; `validate_story`
(générique, proposé par le plan 24) ; ressource `recit://guide/stories` (une idée par carte, jamais
de chiffre inventé : la règle d'or du guide motion vaut ici), reprise par `recit skills export`. Les
outils `build_film` et `apply_ops` restent propres au film.

## 7. Le site story.recit.dev

- `sites/story` : Markdown et config (`createSiteConfig({ site: "story" })`, `styles.css`,
  `vercel.json`), composants dans `@recit/site-kit`, exemples en `devDependencies` (ST-20).
- Accueil : une story jouable (tap, balayage), rail de vignettes ; galerie : lancement, cartes de
  texte (`examples/stories-textcards`, plan 11), « rapport en cartes » et « à venir » quand les
  ponts ST2b existent ; exports : PNG et MP4 par `exports.json`. Aucune doc propre : docs.recit.dev
  › Formats › Stories.
- Port local 3009, e2e 4309, projet Vercel `recit-story`, alias 301 `stories.recit.dev` (`07`). Le
  projet Vercel et le DNS restent à l'utilisateur.
- **Référencement.** Métadonnées Open Graph, `hreflang`, et JSON-LD par le sous-export
  `@recit/core/jsonld` prévu en P10 (`03` §8). Le format Web Stories de Google repose sur AMP
  (`amp-story`) : une sortie AMP serait une seconde vue de la story. Ce plan ne la promet pas
  (question 4) ; l'état du programme est à vérifier au moment de ST4.

## 8. Les exemples

| Dossier                      | Contenu                                                                                             | Langues | Lot            |
| ---------------------------- | --------------------------------------------------------------------------------------------------- | ------- | -------------- |
| `examples/stories-launch`    | six cartes Lorem : titre, chiffre `:value` sur des données Lorem, bloc `stat`, liste, citation, CTA | en, fr  | ST4            |
| `examples/stories-textcards` | les cinq variantes de `textcard` (plan 11)                                                          | en, fr  | IM2.5 (P11)    |
| `examples/stories-character` | kit de personnage Lorem, planche synthétique (plan 11)                                              | en, fr  | IM4, plus tard |

- Lorem Ipsum, aucun nom réel, aucun asset tiers. Fonds en SVG et dégradés écrits par un script de
  l'exemple ; pas de vidéo commitée dans les exemples. La vidéo de fond est testée par une fixture
  courte générée par script dans Chromium (canvas et `MediaRecorder`), dans les tests seulement.
- `launch.md` et son jumeau `launch.json` (commité ou non, l'aller-retour est testé dans les deux
  cas).
- Les sources ne passent jamais par oxfmt ; ligne au `NOTICE.md` dans le même changement.

## 9. Tests

| Niveau           | Ce qui est vérifié                                                                                                                                                                                | Où                                                          |
| ---------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ----------------------------------------------------------- |
| Schéma et jumeau | `storiesScene` et son inverse, `::cta`, `props` en YAML, fond objet ou chaîne ; aller-retour ; erreurs avec ligne ; parité ; sur-ensemble de motion (un récit motion change de format sans perte) | `packages/core/src/__tests__/`, `source-conversion.spec.ts` |
| Modèle et ponts  | densité, `storiesToFilm` (table d'inférence, fonds, durées, `cta` perdu avec avertissement), motion → stories                                                                                     | `packages/formats/stories/src/__tests__/`                   |
| Composants       | une story par composant avec `play` : avancer au tap et au clavier, pause (bouton, appui long), CTA atteignable au clavier, barres de progression lues comme telles, RTL ; axe en erreur          | `X.stories.tsx`, `pnpm test:stories`                        |
| Horloge          | lecture automatique avec une horloge factice (pas d'attente réelle), pause sur onglet caché, mouvement réduit                                                                                     | tests unitaires                                             |
| CLI              | `dev`, `build`, `convert`, `validate`, `export --to images` (nombre, taille `1080×1920`), `render`                                                                                                | `packages/cli/src/__tests__/`                               |
| e2e, site        | story.recit.dev en clair et sombre, mobile et bureau ; tap, clavier, balayage (événements pointeur de Playwright) ; changement de langue ; `?card=N`                                              | `e2e/site-story.spec.ts`, captures `-linux`                 |
| e2e, exports     | PNG des cartes (taille, nombre, deux langues), MP4 du pont (durée)                                                                                                                                | `e2e/stories-export.spec.ts`                                |
| Storybook        | surface `stories` de la galerie                                                                                                                                                                   | `e2e/storybook-stories.spec.ts`                             |

Les noms suivent la convention des specs (`*.spec.ts`, une spec par surface de Storybook) ; le plan
11 (§ tests) écrit encore `e2e/stories.e2e.ts`. Seuils de couverture : une ligne
`packages/formats/stories/**` dans le `vitest.config.ts` racine.

## 10. Lots

Identifiants ST0 à ST6 : ST1 à ST4 reprennent `08` §6, ST0, ST2b, ST5 et ST6 sont nouveaux. IM1 et
IM2 (plan 11) sont logés dans cette phase et se coordonnent avec ST1 à ST3.

```
(question 1) ─► ST0 ─► ST1 ─┬─► ST2 ──┬─► ST4 ─► ST6
                            ├─► ST3 ──┤
                            └─► ST5 ──┘
IM1, IM2 (plan 11) ─► ST2, ST3     P6, P10 ─► ST2b
```

### ST0 — Le jumeau (M)

Dépend de : la question 1 (un renommage se fait ici ou jamais avant la 1.0), et les questions 2
et 7. Fichiers : `packages/core/src/schema/source.ts`, `convert.ts`, `parity.ts`,
`packages/core/src/config/validate.ts` (`StoriesConfigSchema`),
`packages/core/schema/v1/stories.json`.

- [ ] ST0.1 — `StoriesSceneSourceSchema`, `StoriesSettingsSourceSchema`, `StoriesSourceSchema`
      strict ; `sceneBlockFields` partagé avec motion ; `ImageSourceSchema` si la question 7 le
      retient.
- [ ] ST0.2 — `storiesScene` et l'écriture canonique ; erreurs avec ligne.
- [ ] ST0.3 — Parité ; fixtures Lorem ; test « un récit motion change de format sans perte ».
- [ ] ST0.4 — `pnpm -F @recit/core schema:json` ; version plan `prerelease` pour `@recit/core`.

Sortie : aller-retour octet pour octet sur toutes les fixtures ; `examples/motion-hello`, passé en
`format: stories`, valide sans retouche hors `:::notes`.

```bash
pnpm exec vitest run packages/core/src/__tests__/source-conversion.spec.ts
pnpm test:related packages/core/src/schema/convert.ts && pnpm check
```

### ST1 — Le paquet et le lecteur (L)

Dépend de : ST0. Fichiers : `packages/formats/stories/**` (paquet publiable, `./styles.css`),
`packages/cli/src/stories/`, `packages/cli/src/index.ts`,
`packages/integrations/site-stories/src/plugin.tsx` et `vite.ts`, et `packages/components` si ST1.2
y déplace le rendu commun.

- [ ] ST1.1 — `compileStories(story)` : cartes typées, densité, budget.
- [ ] ST1.2 — Rendu d'une carte Markdown : réutilisation de `SceneContent`, mesure du bundle,
      décision écrite dans ce plan.
- [ ] ST1.3 — Composants du §5.1, gestes, horloge, mouvement réduit ; stories avec `play`.
- [ ] ST1.4 — `recit init --format stories`, `recit dev`, `recit build`.
- [ ] ST1.5 — Branche `stories` de `site-stories`, `StoryEmbed format="stories"`, routes par langue.
- [ ] ST1.6 — Version plan `preminor` pour `@recit/stories`, `prerelease` pour `@recit/cli` et
      `@recit/site-stories`.

Sortie : une fixture se lit au tap, au clavier et au balayage dans `recit dev` et dans
`examples/site-embed` ; compteurs de style et de stories à 0 ; aucune nouvelle tolérance dans
`scripts/check-package-boundaries.js`.

```bash
pnpm exec nx run stories:build && pnpm -F @recit/stories typecheck
pnpm test:stories packages/formats/stories && pnpm check
```

### ST2 — Blocs motion et pont stories ⇄ motion (M/L)

Dépend de : ST1, le bloc `panel` du plan 24 (W4), IM2.2 pour `textcard`. Fichiers :
`packages/formats/stories/src/motion/`, `packages/formats/motion-blocks/src/blocks/frame.tsx`
(fond), `packages/formats/motion/src/schema/`, `packages/cli/src/motion/source.ts`.

- [ ] ST2.1 — Fond d'image d'un plan : `panel` ou cadre commun, décision écrite ici.
- [ ] ST2.2 — Cartes `type=…` jouées dans le lecteur.
- [ ] ST2.3 — `storiesToFilm`, `recit render launch.md` ; sens motion → stories.
- [ ] ST2.4 — Avec IM2.3 : une `textcard` comme carte.

Sortie : `recit render examples/stories-launch/launch.md` produit un MP4 `9:16` ; le même fichier
passé en `format: motion` se joue sur motion.recit.dev. Version plan `prerelease` pour
`@recit/motion` et `@recit/motion-blocks`.

### ST2b — Ponts report → stories et calendar → stories (M)

Dépend de : `@recit/report` et `@recit/data` (P6, plan 23), `@recit/calendar` (P10, plan 27).
Correspondances de `08` §0 et `11` IM3, en erreur explicite hors table. Peut glisser après P11 sans
bloquer sa sortie (question 9).

### ST3 — Les images (S/M)

Dépend de : ST1 et `@recit/paged/runner` (P6). Fichiers : `StoriesImagesOptionsSchema`,
`packages/cli/src/commands/export-*.ts`, `ExportManifestSchema`.

- [ ] ST3.1 — Capture par carte (`?card=N&settled=1`, échelle 3), encodage dans Chromium.
- [ ] ST3.2 — `exports.json`, `--all-locales`, `--to pdf` refusé avec son message.

Sortie : `recit export examples/stories-launch/launch.md --to images --all-locales` produit douze
PNG `1080×1920` listés dans `exports.json`.

### ST4 — Exemple, site, doc (M)

Dépend de : ST2, ST3. Fichiers : `examples/stories-launch/**`, `sites/story/**`,
`packages/tools/site-kit/src/sites.ts`, `e2e/ports.ts`, `scripts/e2e-projects.js`,
`e2e/site-story.spec.ts`, `e2e/stories-export.spec.ts`, `e2e/storybook-stories.spec.ts`, page docs
Formats › Stories, `NOTICE.md`.

- [ ] ST4.1 — `examples/stories-launch` (en, fr).
- [ ] ST4.2 — `sites/story`, entrée `story` de `SITES`, métadonnées de partage.
- [ ] ST4.3 — Doc : syntaxe, JSON Schema, ponts.
- [ ] ST4.4 — e2e et captures : `pnpm test:e2e:update:linux e2e/site-story.spec.ts`.

### ST5 — MCP et guide (S)

`describe_format` pour `stories`, `recit://guide/stories`, `validate_story` s'il n'existe pas déjà
(plan 24, W6.1). Sortie : un agent qui lit le guide écrit une story Lorem qui passe
`recit validate --strict`.

### ST6 — Clôture (S)

`pnpm verify`,
`pnpm test:e2e:linux e2e/site-story.spec.ts e2e/stories-export.spec.ts e2e/storybook-stories.spec.ts`,
seuils de couverture, version plans relus, mot « carte » proposé au glossaire d'`AGENTS.md`, note
datée dans `journal.md`.

### Critères de sortie de P11

Ceux de `06` P11 :

- `examples/stories-launch` se lit au tap, au clavier et au balayage ;
- l'export d'images et le pont vers motion (MP4 vertical) fonctionnent ;
- la version `0.7.0-alpha` est publiée (après le go de l'utilisateur).

Proposés par ce plan : story.recit.dev en ligne (`stories.recit.dev` redirige), comme les autres
phases de format ; jumeau de chaque exemple en aller-retour ; lecteur conforme à WCAG 2.2.2 (pause).

## 11. Risques

| Risque                                                                    | Parade                                                                                 |
| ------------------------------------------------------------------------- | -------------------------------------------------------------------------------------- |
| Confusion entre `Story` (modèle) et `stories` (format)                    | glossaire, « une story (format stories) » dans la doc ; question 1 avant ST0           |
| Lecture automatique inaccessible                                          | bouton de pause toujours visible, mouvement réduit, horloge testée                     |
| Le lecteur tire le runtime du deck ou de motion dans le bundle            | ST1.2 mesure ; sous-chemin `./motion` ; blocs motion chargés à la demande              |
| Gestes qui se battent avec le défilement de la page hôte (embed)          | gestes actifs seulement en plein écran ou sur la carte ; e2e sur `examples/site-embed` |
| P6 ou P10 en retard : ponts report et calendar absents                    | ST2b séparé, hors des critères de P11                                                  |
| Vidéos de fond lourdes                                                    | budget de poids, préchargement d'une seule carte, première image sous mouvement réduit |
| Web Stories de Google dépendantes d'AMP                                   | pas de promesse AMP (question 4) ; métadonnées standard                                |
| `type=` ouvre la carte à tous les blocs, y compris `ui-scene` trop chargé | densité appliquée aussi aux props textuelles ; liste blanche de blocs si besoin        |

## 12. Questions ouvertes

1. **Le nom du format.** `stories` partage son mot avec le modèle `Story`. Le renommer est une
   décision de produit (`AGENTS.md`) que ce plan ne prend pas. `AGENTS.md` dit qu'elle figure dans
   `06`, où ne se trouve que la question 11 (sens de story.recit.dev). Un renommage toucherait :
   `STORY_FORMATS` et `StoriesSourceSchema` (`@recit/core`), l'URL
   `recit.dev/schema/v1/stories.json` (figée à la 1.0), la clé `stories:` de `recit.config`
   (aujourd'hui réservée au format, avec les refus de l'ancienne section de site), `@recit/stories`
   et `packages/formats/stories`, `formats.ts` de `site-kit`, l'alias `stories.recit.dev`, l'icône
   `fmt-stories`. Garder `stories`, ou renommer — et dans ce cas, avant ST0 ?
2. **`dur` et `aspectRatio`.** `08` §6 et `09` §2.3 écrivent `duration: 5s` et `aspect: 9:16` ; le
   cœur a déjà `dur` (secondes) et `aspectRatio`. Prendre les mots existants (recommandé) ?
3. **Sens de story.recit.dev** (`06`, question 11, toujours ouverte) : stories web (ce plan), ou
   _user stories_ de backlog ?
4. **AMP et Web Stories.** Faut-il une sortie AMP (`amp-story`) pour l'indexation par Google, ou les
   métadonnées standard suffisent-elles (recommandé pour P11, spike après) ?
5. **Sondages.** `08` §6 prévoit `:::poll` en v2, avec un stockage. Proposition : un sondage est un
   `:::form` du plan 10 (contrat d'API, aucun service hébergé par Récit), pas un bloc nouveau.
   D'accord ?
6. **Seul `9:16` ?** D'autres rapports (`4:5`) en P11, ou plus tard ?
7. **Un seul type d'image.** Renommer `DeckImageSchema` en `ImageSourceSchema` et le partager entre
   l'image du deck, le fond d'une carte et la couverture ?
8. **Fond vidéo dans le film.** Erreur en P11 (proposé), ou incrustation de la vidéo dans le plan
   Remotion ?
9. **Ponts report et calendar.** Dans les critères de P11 (`08` §0 les y range), ou après, selon
   l'avancement de P6 et P10 ?
10. **Lecture automatique par défaut.** `autoplay: true` par défaut (comme les réseaux), ou `false`
    (plus accessible) ?
