diff --git a/docs/solutions/2026-07-17-formater-les-donnees-pour-le-formulaire-mairi-oebs-solution.md b/docs/solutions/2026-07-17-formater-les-donnees-pour-le-formulaire-mairi-oebs-solution.md
new file mode 100644
index 0000000..67a23a4
--- /dev/null
+++ b/docs/solutions/2026-07-17-formater-les-donnees-pour-le-formulaire-mairi-oebs-solution.md
@@ -0,0 +1,161 @@
+# Solution : [T1] Content script : pré-remplir le formulaire mairie (+ config)
+
+## Problème résolu
+
+Au clic sur « Créer sur le site mairie », l'extension déposait l'événement dans
+`storage.local` puis ouvrait un **formulaire vide** : le bénévole devait recopier
+à la main titre, dates, heures, adresse, organisateur, email — exactement le
+geste que l'extension existe pour supprimer. La boucle n'était pas fermée.
+
+Objectif tenu : le formulaire s'ouvre **déjà rempli** de ce que la machine sait,
+**laisse vide** ce qu'elle ignore (jamais de valeur inventée), et **dit** dans un
+bandeau ce qui reste à compléter. Le bénévole relit, complète (image, thème,
+enrichissement) et envoie lui-même — **pas d'auto-submit**.
+
+Ce ticket a aussi **introduit la config** de l'extension.
+
+## Approche choisie
+
+**Deux content scripts classiques partageant un scope + namespace `globalThis`**
+(approche 3 du brainstorm), **+ config à défauts committés et surcharge locale
+optionnelle** (option (a)).
+
+Le vrai risque du ticket n'était pas « poser des valeurs dans un DOM » : c'était
+le **calcul des dates** (DTEND exclusif, dates flottantes de journée entière,
+franchissement de minuit) — invisible sous `TZ=Europe/Paris`, la famille de bugs
+qui avait déjà coûté deux passes au ticket précédent. Il fallait donc que ce
+calcul reste **sous `bun test`**.
+
+Trois alternatives écartées, et pourquoi :
+- **Content script monolithique** : logique de dates intestable (rien à importer
+ en `bun test`) → on coderait à l'aveugle les deux bugs les plus chers.
+- **Module ESM en import dynamique** (`import(runtime.getURL(...))` +
+ `web_accessible_resources`) : s'appuie sur un **bug ouvert MV3 de Firefox**
+ ([bugzilla 1803950](https://bugzilla.mozilla.org/show_bug.cgi?id=1803950)) —
+ parier le risque sur le point le plus fragile de la plateforme.
+- **Formater en amont, content script bête** : réviserait une **couture figée
+ trois jours plus tôt** et coupleraient la page liste au formulaire mairie.
+
+L'approche retenue garde le calcul testable **sans** parier sur un bug plateforme
+**ni** rouvrir une couture fraîche.
+
+## Décisions clés
+
+- **`config.js` sans `import` ni `export`, communique par `globalThis`.** Un
+ fichier sans `import`/`export` est valide **à la fois** comme script classique
+ (content script, background) **et** comme module ESM (`import "./config.js"`
+ depuis `presentation.js` et `bun test`). C'est ce qui permet d'absorber
+ `LIEU_PAR_DEFAUT` (fin d'une duplication) sans build. ⚠️ Le jour où quelqu'un
+ ajoute un `export`, tous les content scripts cassent → acté dans `DECISIONS.md`,
+ avec un commentaire en tête de fichier.
+
+- **Le background lit `config.local.json`, pas le content script.** Un content
+ script ne peut `fetch(runtime.getURL(...))` que si la ressource est en
+ `web_accessible_resources` — ce qui **exposerait l'email à la page de la
+ mairie**. Le background lit sans WAR et transmet via `storage.local`.
+
+- **Config optionnelle, dégradation propre.** Fichier absent (clone frais), HTTP
+ non-ok ou JSON invalide → `{}` → organisateur/email vides **signalés dans le
+ bandeau**, tout le reste rempli. Un `config.local.json` **requis** (énoncé
+ littéral) aurait cassé tout clone frais : rejeté. Le fichier n'étant pas
+ garanti présent, il **ne peut pas** être listé dans le manifest (Firefox refuse
+ d'installer si un fichier `js` manque) : `fetch` + repli est la seule voie.
+
+- **`CLE_EVENEMENT_EN_ATTENTE` dupliquée + test de dérive.** La constante vit dans
+ `transfert.js` (ESM), inatteignable depuis un content script classique.
+ Dupliquée volontairement, avec un test qui garantit l'égalité avec l'originale
+ (plutôt que rouvrir la couture pour convertir `transfert.js`).
+
+- **Honnêteté machine/humain.** Jamais de « 00:00 » sur une journée entière
+ (heures laissées vides), jamais de `lat|lng` inventé sur un lieu hors café,
+ `input_6_35` (accessibilité) laissé intact, honeypots intacts. Tout manque
+ remonte dans `aCompleter` (bandeau).
+
+- **`config.local.json.example` dans `extension/`, pas à la racine.** Le
+ background résout `runtime.getURL("config.local.json")` **dans `extension/`** :
+ l'exemple doit être à côté de sa cible de copie.
+
+## Patterns à réutiliser
+
+- **Fichier « bilingue » ESM/classique** : ne poser `import`/`export` nulle part,
+ publier l'API sur `globalThis.MonNamespace`. Chargeable partout dans une
+ extension MV3 sans build. Le prix : une convention à documenter pour qu'un futur
+ agent ne la « corrige » pas en `export`.
+
+- **Séparer calcul pur (testé) / effet de bord (fin, non testé).**
+ `calculerValeurs(payload, config)` est pur et renvoie un objet **plat, prêt à
+ poser** (`dateDebut: "18/07/2026"`, `heureDebut: ["19","30"] | null`, …) ;
+ `formulaire-mairie.js` ne fait que lire le storage et écrire le DOM. Le bug des
+ heures (voir Pièges) a **validé** ce découpage : confiné au poseur, diagnostiqué
+ en une pile d'appels, corrigé en quelques lignes, cœur testé jamais touché.
+
+- **Merge tolérant pour config éditée à la main** : `fusionner(defauts, locale)`
+ traite `null` / non-objet / tableau comme « pas de surcharge » et ne mute pas
+ les défauts. Une config optionnelle ne doit **jamais** lever.
+
+- **Dates de calendrier sans fuseau.** Journée entière = date flottante
+ (RFC 5545) → lire **uniquement** les composantes locales
+ (`getFullYear/getMonth/getDate`), jamais l'heure, jamais un fuseau. DTEND est
+ **exclusif** → reculer la date de fin d'un jour (en composantes locales, jamais
+ « −24h »). Événement horodaté → tout à `Europe/Paris` via `Intl.DateTimeFormat`,
+ heures zéro-paddées. **Épingler `TZ=Europe/Paris` dans les tests ET rejouer sous
+ plusieurs fuseaux** (le bug de fuseau est invisible sous le fuseau de dev).
+
+- **Poser une valeur dans un champ piloté par un framework** : après `.value`,
+ dispatcher `new Event("input", {bubbles:true})` **puis**
+ `new Event("change", {bubbles:true})` (Gravity Forms écoute `change`). Le
+ dispatch est gratuit — ne pas supposer que `.value` suffit.
+
+- **Isoler chaque effet de bord dans son propre `try/catch`.** Un `try` global
+ autour du remplissage laissait la première exception emporter **tous** les
+ champs suivants. Un `try` **par champ** → le champ fautif part dans `manquants`,
+ les autres sont posés. Ferme la classe entière de panne, pas le cas vu.
+
+- **Rendre tout geste automatique visible.** Un bandeau injecté dit ce qui a été
+ fait et ce qui reste ; storage vide (visite directe) → **rien**. Un
+ préremplissage silencieusement partiel soumis à une mairie qui modère est le
+ pire échec possible.
+
+## Pièges à éviter
+
+- 🔴 **Ne jamais déduire le type d'un champ de son intitulé — le HTML est la seule
+ source de vérité.** L'énoncé du ticket affirmait « Heures = 2 **selects**
+ input_6_38_1/_2 ». Le HTML réel dit ``. `poserSelect`
+ faisait `[...champ.options]` sur un `` → `champ.options` undefined →
+ `TypeError`, qui (avant l'isolation par champ) vidait aussi Tarifs, adresse et
+ coordonnées. Corrigé : heures via `poserTexte` (le zéro-padding `"09"`/`"00"`
+ reste un *valid floating-point number* au sens HTML). `RECHERCHE.md` §2 a été
+ corrigée à la racine (colonne « balise réelle » + avertissement) pour qu'un
+ futur agent ne repaie pas ce bug. C'est la cause racine du seul bug du ticket,
+ et il venait d'un **doc/énoncé faux cru sur parole**.
+
+- **Toucher au `manifest.json` impose Décharger + Charger**, pas « Recharger » ni
+ F5. Piège déjà payé plusieurs fois. Fermer aussi les anciens onglets de la liste
+ (page orpheline = clics inertes).
+
+- **DTEND exclusif** : sans recul d'un jour, la date de fin d'un événement
+ multi-jours est **fausse** — et soumise à une mairie qui modère (personne ne
+ relit une date qui *a l'air* juste). Le bug le plus cher du ticket.
+
+- **Course background ↔ content script** (P3 ouvert, non corrigé) :
+ `chargerConfigLocale()` est async ; au tout premier chargement post-install, si
+ le formulaire s'ouvrait avant la fin du `fetch`, organisateur/email seraient
+ vides à tort. Improbable (la valeur persiste ensuite), noté pour mémoire.
+
+- **`chargerConfigLocale()` s'exécute à chaque réveil de l'event page** et écrit
+ `{}` si le fichier est absent. Toute future page d'options qui écrirait dans
+ `storage.local` verrait ses valeurs **écrasées par `{}`** au prochain réveil.
+ Trancher « qui fait autorité, fichier ou UI » fait partie du futur ticket — ce
+ n'est pas « juste ajouter un écran ».
+
+- **Le poseur DOM n'est pas testé unitairement** (aucun harnais DOM dans le repo ;
+ happy-dom n'émule pas le JS de Gravity Forms / Leaflet). C'est le **test humain**
+ qui tranche le rendu réel (datepicker, marqueur carte, bandeau non écrasé par le
+ CSS de la mairie). Le bug des heures l'a prouvé : seul le DOM réel tranche.
+
+- **Config `cafe` partielle interdite** : `fusionner` est shallow → une locale
+ avec un `cafe` incomplet produirait `"undefined|adresse"`. Le README précise que
+ seuls `organisateur` et `email` sont surchargeables.
+
+## Tags
+tags: [firefox-extension, manifest-v3, content-script, globalthis, esm, gravity-forms, dates, timezone, rfc5545, config, bun-test, tdd, vanilla-js]