# GAB-054 · StoryTimelineNarrative — « Frise narrative »

**Archétype / renderer_key :** `text_cta` (cartographie) · **module :** EdTechStoryLearning
**Critère validé :** changer le JSON change la frise sans modifier le HTML. ✅ check.py 12/12.

## Pack (structure officielle par-GAB)
```
GAB-054/
  renderer.html            ← moteur frise narrative (ne pas modifier par instance)
  instance.example.json    ← SOURCE DE VÉRITÉ (contenu réel, à plat)
  schema.contract.json     ← contrat de validation
  README-contract.md       ← ce fichier
```

## Champs requis (instance, à plat)
`gab_id` · `timeline_id` · `events[]{id, icon, date, title, description}`

Optionnels : `title`, `note`.

## Ce qui vient du JSON vs HTML
- **JSON** : chaque événement (icon, date, title, description), état initial `lit_default`, note de bas de frise, titre hero.
- **HTML** : ligne de temps, points animés, accordéon des descriptions, layout, fallback BLOCKED.

## Garde-fous (child_safety)
- **Anti-invention** : 0 contenu pédagogique en dur dans le renderer. Tous les textes viennent de l'instance.
- **BLOCKED** si `events` absent/vide ou `timeline_id` absent.
- **État exclusif** : un seul événement ouvert à la fois ; cliquer le même le ferme (toggle).
- `lit_default:true` : au plus 1 événement initialement ouvert (le premier `true` rencontré).

## QA à vérifier
1. Modifier `events[].description` → rendu change sans toucher au HTML (critère d'or).
2. `events:[]` → BLOCKED propre.
3. `timeline_id` absent → BLOCKED signalé.
4. Clic événement fermé → s'ouvre ; clic événement ouvert → se ferme.
5. Injection `init(ext)` avec instance différente → frise rebuildée.
6. Responsive 375/768/1024 — aucun débordement.
7. Navigation clavier (Tab + Enter/Space) sur chaque événement.

## Source
`INDEX-300-storylearning-GAB-050-054-PLAYABLE.html` (stage `data-tpl="54"`, handler `tlToggle`).

## Références externes (hors-lot)
Aucune dépendance sur d'autres GAB de ce lot. Gabarit autonome.
