diff --git a/CHANGELOG.md b/CHANGELOG.md
index 646212f2..ad80eb97 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -7,6 +7,22 @@ Versioning : [Semantic Versioning](https://semver.org/lang/fr/)
---
+## [Non publié]
+
+### Sécurité
+
+- **Les données de contenu pouvaient rompre un `` fermait la balise, et tout ce qui suivait devenait du HTML actif.
+
+ Le vecteur large est le JSON-LD : il sérialise `title`, champ **obligatoire** de toute série, et `BareLayout` est le layout servi par défaut quand le site n'en fournit pas. La lightbox exposait `images[].alt`.
+
+ Le modèle de menace d'un générateur statique — l'auteur écrit son propre contenu — ne suffit pas à classer cela bénin. `images.json` (§1.5.1) est produit par des outils : `hyperfocale-exporter` lit les métadonnées des fichiers, `hyperfocale-cms` écrit le frontmatter. Un texte alternatif peut donc arriver d'un champ IPTC sans jamais passer sous les yeux d'un humain.
+
+ Le correctif remplace `<` par `\u003c` avant l'injection. C'est un échappement JSON valide : `JSON.parse` restitue le `<` d'origine, la donnée est intacte, seule la séquence littérale ne peut plus apparaître. Aucun changement d'API, aucun contenu rejeté.
+
+ Cinq tests e2e couvrent les deux sites, sur le HTML réellement produit — le seul niveau où le bug existait.
+
+---
+
## [0.18.0] — 2026-08-20
Les helpers savent lire plusieurs collections dans un même build. Aucun changement cassant.
diff --git a/docs/reports/audit-summary-2026-08-24.md b/docs/reports/audit-summary-2026-08-24.md
new file mode 100644
index 00000000..7327eb84
--- /dev/null
+++ b/docs/reports/audit-summary-2026-08-24.md
@@ -0,0 +1,168 @@
+# Audit consolidé — hyperfocale 0.18.0
+
+> Généré le 2026-08-24 · commit `9f2b285` · mode `blackemperor audit`
+> Audit précédent : [`audit-summary-2026-06-24.md`](audit-summary-2026-06-24.md) (v0.5.0)
+
+## Scores
+
+| Axe | Score | Évolution vs 2026-06-24 |
+|-----|-------|--------------------------|
+| Code / architecture | **8,5/10** | ↗ périmètre triplé, dette non accumulée |
+| Performance | **9/10** | ↗ zéro framework client, JS inline borné |
+| Accessibilité | **8/10** | → base solide, lightbox à vérifier au clavier |
+| Sécurité | **5/10** | ↘ **deux XSS stockés confirmés** (nouveaux composants) |
+| Documentation (`CLAUDE.md`) | **10/10** | ↗ corrigée le jour même |
+
+**Verdict — ne pas publier de nouvelle version avant correction de `#SEC-001`.**
+Le paquet 0.18.0 déjà en ligne porte la faille : les deux sites d'injection existent
+depuis 0.16.0 (`BareLayout`) et bien avant pour la lightbox.
+
+---
+
+## 🚨 Sécurité — 2 findings HAUTS, confirmés par build
+
+### `#SEC-001` · Rupture de `` ferme la balise et tout ce qui suit devient du HTML actif.
+
+| # | Fichier | Champ vecteur | Portée |
+|---|---------|---------------|--------|
+| a | `src/layouts/BareLayout.astro:29` | `title`, `description` de la série (JSON-LD) | **toute route injectée** sur le layout de repli |
+| b | `src/components/SeriesLightbox.astro:74` | `images[].alt` | toute page rendant la lightbox |
+
+Le cas (a) est le plus large : `title` est **obligatoire** sur chaque série, et
+`BareLayout` est le layout servi par défaut quand le site ne passe pas le sien.
+
+**Preuve** — page construite puis `astro build` réel, HTML inspecté :
+
+```
+alt = photo
+```
+
+rendu dans `dist/` :
+
+```html
+
+"}]
+```
+
+Le marqueur se trouve **après** la fermeture du script : il est sorti de la zone JSON.
+Page de test supprimée après vérification.
+
+**Modèle de menace** — le contenu d'un SSG est en principe écrit par l'auteur du site,
+ce qui limiterait l'impact à de l'auto-XSS. Deux raisons de ne pas s'en contenter :
+
+1. `images.json` (§1.5.1) est **produit par des outils** — `hyperfocale-exporter` lit les
+ métadonnées d'images, `hyperfocale-cms` écrit le frontmatter. Un `alt` peut donc venir
+ d'un champ IPTC sans jamais passer sous les yeux d'un humain.
+2. Un CMS multi-utilisateurs rend le frontmatter semi-hostile par construction.
+
+**Correctif** (une ligne par site, sans changement d'API) :
+
+```ts
+const safe = JSON.stringify(data).replace(/` ne peut plus apparaître littéralement. Même traitement pour le
+JSON-LD de `BareLayout`.
+
+**Test de non-régression à ajouter** : une série au `title` contenant ``, buildée
+en e2e, dont le HTML ne doit pas contenir la séquence hors de la balise.
+
+### `#SEC-002` · `embeds[].id` interpolé sans contrainte dans les URL de lecture
+
+`src/components/SeriesEmbeds.astro:29-47` construit l'URL du lecteur par interpolation
+directe, alors que le schéma déclare `id: z.string().optional()` — **aucun format imposé**
+(`src/schema.ts:136`), et `playable` ne vérifie qu'une chose : chaîne non vide.
+
+```ts
+return `https://player.vimeo.com/video/${embed.id}?autoplay=1`;
+```
+
+Un `id` valant `123?autoplay=0&x=` ou `../../autre-chemin` détourne l'URL produite. Le cas
+SoundCloud est le plus fragile — l'id atterrit dans un paramètre `url=` déjà encodé, où un
+`&` casse la structure.
+
+Ce n'est **pas** un XSS : Astro échappe les attributs, et le préfixe `https:///`
+reste en place. L'impact se limite au détournement de l'URL vers une autre ressource du
+même hébergeur.
+
+**Correctif** : `encodeURIComponent(embed.id)` à l'interpolation, ou un `z.string().regex(/^[\w-]+$/)`
+au schéma. La première option est préférable — elle ne rejette aucun contenu que la spec
+tient pour valide, ce qui est la ligne suivie pour les plateformes inconnues.
+
+---
+
+## Code & architecture — 8,5/10
+
+| Indicateur | Valeur |
+|---|---|
+| `src/` TypeScript | 6 fichiers · 1 794 lignes |
+| `src/` Astro | 13 fichiers · 2 086 lignes |
+| Tests | 16 fichiers · 2 482 lignes — **1,38× le TypeScript**, 0,64× tout `src/` |
+| `any` · `@ts-ignore` · `!.` | **0** |
+| `console.*` non gardés | **0** (le seul `console.info` est derrière `HYPERFOCALE_DEBUG_CACHE`) |
+| `TODO` / `FIXME` / `HACK` | **0** |
+| Fichier `src/` non importé | **0** |
+
+Deux fonctions dépassent 90 lignes dans `src/helpers/index.ts` — `parseImageManifest` (93)
+et `getSeriesImages` (92). Les deux traitent les trois formes d'entrée du manifeste ; leur
+longueur vient de la couverture des cas, pas d'un enchevêtrement. Pas d'action recommandée.
+
+## Performance — 9/10
+
+`dist/` publié : **224 Ko**, 32 fichiers. Aucun framework client — le JS envoyé au
+navigateur est inline et borné :
+
+| Composant | JS inline |
+|---|---|
+| `SeriesLightbox` | 5 633 c. |
+| `SeriesFilter` | 2 463 c. |
+| `SeriesEmbeds` | 1 169 c. |
+| `SeriesGallery` | 401 c. |
+| `SeriesMasonry` | 263 c. |
+
+`loading` et `decoding` sont posés sur les images des cinq composants concernés. Les embeds
+sont rendus en façade — l'iframe tierce n'arrive qu'au clic, ce qui évite à la fois le coût
+de chargement et le dépôt de cookies non sollicité.
+
+## Accessibilité — 8/10
+
+Vérifié sur balises multi-lignes, pas au `grep` naïf :
+
+- **7 `` sur 7** portent un `alt` ✅
+- **5 `