# 001 — HistoMeteo MVP — Spec Technique v18

> **Source fonctionnelle** : `001-histometeo-mvp.md`
> **Base technique** : `001-histometeo-mvp.tech.v17.md`
> **Feedback intégré** : `feedback-to-architect-001-v17.md` (R52)
> **Demande additionnelle** : Bloc de recherche repliable sur les pages résultat + repositionnement du H2 SEO
> **Date** : 2026-03-13

---

## 0) Contract

- **Source of truth** : cette spec technique (`001-histometeo-mvp.tech.v18.md`)
- **Functional integrity** : AC1–AC17 inchangés. Critères d'acceptation additionnels AC18–AC22.
  - AC18 : Sur les pages résultat (simple, comparaison, ville, mois), le bloc recherche est replié par défaut et affiche un résumé compact
  - AC19 : Le bloc compact affiche la commune sélectionnée, la période et un bouton « Modifier la recherche »
  - AC20 : Le clic sur « Modifier la recherche » déplie le formulaire complet avec une transition fluide ; un second clic le replie
  - AC21 : Sur la page d'accueil (aucune recherche), le formulaire complet est affiché par défaut (pas de bloc compact)
  - AC22 : Le formulaire de recherche n'est jamais dupliqué dans le DOM — même composant, deux états d'affichage
  - AC23 : Le H2 SEO (ex. « Historique météo à Gap du 10 au 12 mars 2026 ») apparaît entre le bloc de navigation ancres (`#results-nav`) et la section « Résumé par jour » (`#daily-summary`) — et non plus en haut de page dans `#seo-intro`
- **Scope** — fichiers modifiables :
  - `public/index.html`
  - `public/style.css`
  - `public/app.js`
  - `docs/specs/001-histometeo-mvp.tech.v18.md`
- **Forbidden changes** :
  - Tout fichier backend : `src/main.py`, `src/config.py`, `src/normals_service.py`, `src/weather_service.py`, `src/cache.py`, `src/commune_service.py`, `src/og_service.py`
  - `requirements.txt`, `Dockerfile`, `pyproject.toml`
  - `README.md`, `.github/`, `public/assets/`
  - `src/assets/weather-icons/`
  - Tests ≤ v17 (93 tests existants) — ne pas modifier, tous doivent continuer à passer
- **Invariants** :
  - INV-1 à INV-24 : tous préservés (cf. v17)
  - INV-7 : pas de `innerHTML` dans `app.js`
  - INV-12 : pas de JS media queries (`matchMedia`, `window.innerWidth` interdits)
  - INV-14 : OG tags server-side uniquement
  - INV-20 : un seul `<h1>` visible par page à tout moment
  - INV-25 _(nouveau)_ : le formulaire de recherche n'est jamais dupliqué — un seul jeu d'inputs dans le DOM, deux états d'affichage (compact / ouvert)
  - INV-26 _(nouveau)_ : le bloc compact est peuplé exclusivement via `textContent` et manipulation DOM standard (conforme à INV-7)
  - INV-27 _(nouveau)_ : la transition compact ↔ ouvert utilise uniquement des animations CSS (pas de `setTimeout` chaîné ni de manipulation JS de `style.height`)
- **Done when** :
  - D11 : Sur toutes les pages résultat (simple, comparaison, ville, mois), le bloc recherche est replié par défaut
  - D12 : Le bloc compact affiche : commune, période (si applicable), bouton « Modifier la recherche »
  - D13 : Le clic sur « Modifier la recherche » déplie le formulaire complet avec animation fluide (≤ 300ms)
  - D14 : Sur la page d'accueil, le formulaire est ouvert par défaut (bloc compact masqué)
  - D15 : Le formulaire déplié est identique au formulaire existant (même composant DOM, pas de duplication)
  - D16 : Le bloc compact est lisible et fonctionnel sur mobile (bouton « Modifier » raccourci)
  - D17 : R52 — fichiers `__pycache__/` retirés de l'index git via `git rm --cached`
  - D18 : Tous les tests passent (93 hérités)
  - D19 : Le H2 SEO est positionné entre `#results-nav` et `#daily-summary` sur les pages simple, mois et ville (pas en mode comparaison — INV-21)

---

## 1) Objectif technique

Introduire un mécanisme de repli du bloc de recherche sur les pages résultat pour maximiser l'espace consacré aux données météo.

Le bloc `#search` existant bascule entre deux états visuels :

- **Mode ouvert** : formulaire complet (état actuel, inchangé)
- **Mode compact** : résumé en une ligne — commune, période, bouton d'expansion

La bascule est purement CSS + manipulation de classes DOM. Aucun élément de formulaire n'est recréé ni déplacé.

Parallèlement :

- Intégrer R52 (retrait des `__pycache__/` de l'index git)
- Repositionner le H2 SEO (ex. « Historique météo à Gap du 10 au 12 mars 2026 ») : actuellement affiché en haut de page dans la section `#seo-intro` (juste sous le bloc de recherche), il doit apparaître plus bas, entre le bloc de navigation ancres (`#results-nav`) et la section « Résumé par jour » (`#daily-summary`). L'objectif est de rapprocher le titre sémantique du contenu qu'il qualifie, et de libérer l'espace haut de page pour le résumé de la période.

---

## 2) Analyse du brief

### Besoins principaux

| #   | Besoin                                                    | Origine               |
| --- | --------------------------------------------------------- | --------------------- |
| B1  | Bloc compact par défaut sur les pages résultat            | Brief UX §2, §4       |
| B2  | Bouton « Modifier la recherche » pour déplier             | Brief UX §3           |
| B3  | Animation fluide d'ouverture/fermeture                    | Brief UX §5           |
| B4  | Adaptation mobile (compact lisible, bouton accessible)    | Brief UX §6           |
| B5  | Accessibilité : bloc identifiable comme zone de recherche | Brief UX §7           |
| B6  | Pas de duplication du formulaire                          | Brief UX §8           |
| B7  | Retrait des `__pycache__/` de l'index git                 | Feedback R52          |
| B8  | H2 SEO repositionné sous la barre de navigation ancres    | Demande additionnelle |

### Contraintes

- Le formulaire existant dans `#search` n'est pas restructuré — on ajoute un bloc compact au-dessus et on enveloppe le contenu existant dans un `<div>` togglable
- La bascule est pilotée par une classe CSS sur `#search` — pas de clonage ni de déplacement de nœuds DOM
- Le pré-remplissage du formulaire (commune + dates) sur les pages résultat est déjà implémenté — le bloc compact lit les données depuis l'état JS existant (`selectedCommune`, `dateStart.value`, `dateEnd.value`)
- L'animation utilise `max-height` + `overflow: hidden` + `transition` CSS — pas de JS pour calculer les hauteurs
- R53 (navigation mois précédent/suivant), R54 (uniformisation résolution commune), R55 (découpage ES modules) sont reportés à une itération future

### Risques

Voir §7.

---

## 3) Design minimal proposé

### 3.1 Modification HTML — `public/index.html`

#### 3.1.1 Restructuration de la section `#search`

**Avant** (structure actuelle) :

```html
<section id="search" class="panel">
  <h2>Recherche</h2>
  <label for="commune-input">Commune</label>
  <!-- ...tout le formulaire... -->
  <button id="search-button" disabled>Voir la météo</button>
</section>
```

**Après** :

```html
<section id="search" class="panel" aria-label="Recherche météo">
  <!-- BLOC COMPACT (masqué par défaut) -->
  <div id="search-compact" class="search-compact hidden">
    <div class="search-compact-info">
      <span class="search-compact-icon" aria-hidden="true">🔎</span>
      <div class="search-compact-details">
        <strong class="search-compact-title">Recherche météo</strong>
        <span id="search-compact-commune"></span>
        <span id="search-compact-period"></span>
      </div>
    </div>
    <button
      type="button"
      id="search-modify-btn"
      class="btn-secondary"
      aria-expanded="false"
      aria-controls="search-form-body"
    >
      Modifier<span class="search-modify-long"> la recherche</span>
    </button>
  </div>

  <!-- FORMULAIRE COMPLET (contenu existant enveloppé) -->
  <div id="search-form-body">
    <h2>Recherche</h2>
    <label for="commune-input">Commune</label>
    <!-- ...tout le contenu existant INCHANGÉ... -->
    <button id="search-button" disabled>Voir la météo</button>
  </div>
</section>
```

**Modifications concrètes** :

1. Ajouter `aria-label="Recherche météo"` sur `<section id="search">`
2. Insérer le bloc `#search-compact` (nouveau) comme premier enfant de `#search`
3. Envelopper tout le contenu existant (de `<h2>Recherche</h2>` à `<button id="search-button">` inclus) dans un `<div id="search-form-body">`
4. Aucun élément existant n'est supprimé, déplacé ou modifié — seulement enveloppé

#### 3.1.2 Repositionnement du H2 SEO

Actuellement, `#seo-intro` contient deux éléments :

```html
<section id="seo-intro" class="panel hidden" aria-live="polite">
  <p id="seo-intro-text" class="seo-intro-text"></p>
  <h2 id="seo-h2" class="seo-h2"></h2>
</section>
```

**Modification** : extraire le `<h2 id="seo-h2">` de `#seo-intro` et le placer comme élément autonome entre `#results-nav` et `#daily-summary` :

```html
<!-- Intro SEO (ne contient plus le H2) -->
<section id="seo-intro" class="panel hidden" aria-live="polite">
  <p id="seo-intro-text" class="seo-intro-text"></p>
</section>

<!-- ... sections intermédiaires inchangées ... -->

<nav
  id="results-nav"
  class="results-nav hidden"
  aria-label="Navigation résultats"
>
  <a href="#daily-summary">Résumé</a>
  <a href="#chart">Graphique</a>
  <a href="#results">Détail horaire</a>
</nav>

<!-- H2 SEO déplacé ici -->
<h2 id="seo-h2" class="seo-h2 hidden"></h2>

<section id="daily-summary" class="panel hidden" aria-live="polite">
  <!-- ...inchangé... -->
</section>
```

**Ordre résultant des blocs visibles sur une page résultat simple** :

1. `#search` (compact)
2. `#page-title-results` (H1)
3. `#seo-intro` (texte d'introduction seul)
4. `#period-summary` (résumé de la période)
5. `#share-block`
6. `#commune-info`
7. `#results-nav` (ancres Résumé / Graphique / Détail horaire)
8. **`#seo-h2`** (H2 SEO — nouvel emplacement)
9. `#daily-summary` (résumé par jour)
10. `#climate-normals`
11. `#chart`
12. `#results` (détail horaire)

**Impact pages Ville et Mois** : le déplacement s'applique identiquement. Le H2 est peuplé via `setSeoIntro()` et `renderSeoIntro()` existants — seul son emplacement DOM change. En mode comparaison, le H2 reste `hidden` (INV-21).

### 3.2 Nouveau CSS — `public/style.css`

#### 3.2.1 Bloc compact

```css
/* ── Search Compact ─────────────────────── */

.search-compact {
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: var(--space-md);
}

.search-compact.hidden {
  display: none;
}

.search-compact-info {
  display: flex;
  align-items: center;
  gap: var(--space-sm);
  min-width: 0;
}

.search-compact-icon {
  font-size: 1.3rem;
  flex-shrink: 0;
}

.search-compact-details {
  display: flex;
  flex-direction: column;
  gap: 2px;
  min-width: 0;
}

.search-compact-title {
  font-size: 0.8rem;
  color: var(--muted);
  text-transform: uppercase;
  letter-spacing: 0.03em;
}

#search-compact-commune {
  font-weight: 600;
  font-size: 1rem;
  color: var(--text);
  white-space: nowrap;
  overflow: hidden;
  text-overflow: ellipsis;
}

#search-compact-period {
  font-size: 0.9rem;
  color: var(--muted);
}
```

#### 3.2.2 Transition du formulaire

```css
#search-form-body {
  overflow: hidden;
  max-height: 2000px;
  opacity: 1;
  transition:
    max-height 0.3s ease-out,
    opacity 0.2s ease-out;
}

#search-form-body.collapsed {
  max-height: 0;
  opacity: 0;
  pointer-events: none;
}
```

> **Note** : `max-height: 2000px` est une borne haute volontairement généreuse. La transition s'exécute en ~300ms quelle que soit la hauteur réelle. `pointer-events: none` empêche l'interaction avec le formulaire invisible pendant qu'il est replié.

#### 3.2.3 Transitions — sens du repli

Lorsque le formulaire se replie (ajout de `.collapsed`) :

- `max-height` passe de la valeur courante à 0 → transition 0.3s
- `opacity` passe de 1 à 0 → transition 0.2s (termine avant la réduction de hauteur)

Lorsque le formulaire s'ouvre (retrait de `.collapsed`) :

- `max-height` passe de 0 à 2000px → transition 0.3s
- `opacity` passe de 0 à 1 → transition 0.2s

#### 3.2.4 Séparateur visuel

```css
#search-compact + #search-form-body:not(.collapsed) {
  border-top: 1px solid var(--border);
  margin-top: var(--space-md);
  padding-top: var(--space-md);
}
```

> Quand le compact est visible ET le formulaire est ouvert, un séparateur visuel sépare les deux zones.

#### 3.2.5 H2 SEO repositionné

```css
#seo-h2 {
  margin: 0 0 var(--space-md);
  padding: 0;
}

#seo-h2.hidden {
  display: none;
}
```

> Le H2 est un élément autonome entre `#results-nav` et `#daily-summary`. La classe `hidden` reste son état par défaut. La marge basse assure l'espacement avec le tableau qui suit.

#### 3.2.6 Mobile

```css
@media (max-width: 600px) {
  .search-compact {
    flex-direction: column;
    align-items: flex-start;
    gap: var(--space-sm);
  }

  .search-compact .btn-secondary {
    align-self: stretch;
    text-align: center;
  }

  .search-modify-long {
    display: none;
  }
}
```

> Sur mobile, le bouton dit simplement « Modifier » (le `<span class="search-modify-long">` est masqué). Le bloc compact se dispose en colonne. Le bouton occupe toute la largeur.

### 3.3 JavaScript — `public/app.js`

#### 3.3.1 Nouvelles références DOM

Au niveau des déclarations existantes (début de fichier, zone des `const`/`let`) :

```javascript
const searchCompact = document.getElementById("search-compact");
const searchCompactCommune = document.getElementById("search-compact-commune");
const searchCompactPeriod = document.getElementById("search-compact-period");
const searchFormBody = document.getElementById("search-form-body");
const searchModifyBtn = document.getElementById("search-modify-btn");
```

#### 3.3.2 Fonction `setSearchCompact(communeLabel, periodLabel)`

Bascule le bloc recherche en mode compact.

```javascript
function setSearchCompact(communeLabel, periodLabel) {
  // Peupler le résumé
  searchCompactCommune.textContent = communeLabel || "";
  searchCompactPeriod.textContent = periodLabel || "";
  if (!periodLabel) {
    searchCompactPeriod.classList.add("hidden");
  } else {
    searchCompactPeriod.classList.remove("hidden");
  }

  // Afficher le compact, replier le formulaire
  searchCompact.classList.remove("hidden");
  searchFormBody.classList.add("collapsed");
  searchModifyBtn.setAttribute("aria-expanded", "false");
}
```

#### 3.3.3 Fonction `setSearchExpanded()`

Bascule le bloc recherche en mode formulaire complet.

```javascript
function setSearchExpanded() {
  // Masquer le compact, déplier le formulaire
  searchCompact.classList.add("hidden");
  searchFormBody.classList.remove("collapsed");
  searchModifyBtn.setAttribute("aria-expanded", "false");
}
```

> **Page d'accueil** : `searchCompact` masqué, `searchFormBody` visible → état initial HTML sans modification.

#### 3.3.4 Fonction `toggleSearchForm()`

Liée au bouton « Modifier la recherche ».

```javascript
function toggleSearchForm() {
  const isCollapsed = searchFormBody.classList.contains("collapsed");
  if (isCollapsed) {
    searchFormBody.classList.remove("collapsed");
    searchModifyBtn.setAttribute("aria-expanded", "true");
  } else {
    searchFormBody.classList.add("collapsed");
    searchModifyBtn.setAttribute("aria-expanded", "false");
  }
}
```

> Le bloc compact reste toujours visible sur les pages résultat — seul le formulaire se déplie/replie en-dessous.

#### 3.3.5 Event listener

Ajouter dans la zone d'initialisation des événements :

```javascript
searchModifyBtn.addEventListener("click", toggleSearchForm);
```

#### 3.3.6 Formatage du libellé de la période

Fonction utilitaire pour le bloc compact :

```javascript
function formatCompactPeriod(startISO, endISO) {
  if (!startISO || !endISO) return "";
  const start = new Date(startISO + "T00:00:00");
  const end = new Date(endISO + "T00:00:00");

  const startYear = start.getFullYear();
  const endYear = end.getFullYear();

  const optionsNoYear = { day: "numeric", month: "long" };
  const optionsWithYear = { day: "numeric", month: "long", year: "numeric" };

  const fmtNoYear = new Intl.DateTimeFormat("fr-FR", optionsNoYear);
  const fmtWithYear = new Intl.DateTimeFormat("fr-FR", optionsWithYear);

  if (startYear === endYear) {
    return fmtNoYear.format(start) + " → " + fmtWithYear.format(end);
  }
  return fmtWithYear.format(start) + " → " + fmtWithYear.format(end);
}
```

Exemples de sortie :

- `"6 mars → 12 mars 2026"`
- `"28 décembre 2025 → 3 janvier 2026"`

#### 3.3.7 Construction du libellé commune

```javascript
function formatCompactCommune(commune) {
  if (!commune) return "";
  const dept = commune.codeDepartement || "";
  return commune.nom + (dept ? " (" + dept + ")" : "");
}
```

Pour le mode comparaison, construire le label avec les deux communes :

```javascript
const label =
  formatCompactCommune(selectedCommune) +
  " vs " +
  formatCompactCommune(selectedCommune2);
```

#### 3.3.8 Intégration dans le flux de chargement des pages

**Dans `loadFromURL()`** — après le `switch` sur `parsed.mode` :

| Mode           | Appel                                                                                                                                                                                              |
| -------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `"search"`     | `setSearchExpanded()` (formulaire ouvert, pas de compact)                                                                                                                                          |
| `"simple"`     | `setSearchCompact(formatCompactCommune(commune), formatCompactPeriod(start, end))` — appelé dans `renderSimpleResults()` après le rendering, quand `selectedCommune` et les dates sont disponibles |
| `"comparison"` | `setSearchCompact(label1 + " vs " + label2, formatCompactPeriod(start, end))` — appelé dans `renderComparisonResults()`                                                                            |
| `"town"`       | `setSearchCompact(formatCompactCommune(commune), null)` — appelé dans `renderTownPage()` (pas de période)                                                                                          |
| `"month"`      | `setSearchCompact(formatCompactCommune(commune), MONTH_NAMES_FR[month] + " " + year)` — appelé dans `renderMonthPage()`                                                                            |

> L'appel se fait **à la fin** de chaque fonction `render*()`, après que le contenu soit affiché — le compact doit refléter les données effectivement chargées.

**Cas d'erreur** : si le chargement échoue (slug invalide, API en erreur), le formulaire reste ouvert (`setSearchExpanded()`) pour permettre une nouvelle recherche.

#### 3.3.9 Adaptation des fonctions `renderSeoIntro()` et `setSeoIntro()`

Ces fonctions peuplent actuellement `seoIntroText` et `seoH2`. Après le déplacement du H2 hors de `#seo-intro` :

- **`seoH2` est maintenant un élément autonome** — il doit être rendu visible/masqué indépendamment de `seoIntroSection`
- Ajouter la gestion de `seoH2.classList.remove("hidden")` dans `renderSeoIntro()` et `setSeoIntro()` après le peuplement du texte
- Ajouter `seoH2.classList.add("hidden")` dans `clearResults()` pour le masquer lors du nettoyage

**Modifications dans `renderSeoIntro()`** :

```javascript
function renderSeoIntro(communeName, startDate, endDate) {
  // ...calculs existants inchangés...
  seoIntroText.textContent = introText;
  seoH2.textContent = h2Text;
  seoIntroSection.classList.remove("hidden");
  seoH2.classList.remove("hidden"); // ← ajout
}
```

**Modifications dans `setSeoIntro()`** :

```javascript
function setSeoIntro(introText, h2Text) {
  seoIntroText.textContent = introText;
  seoH2.textContent = h2Text;
  seoIntroSection.classList.remove("hidden");
  seoH2.classList.remove("hidden"); // ← ajout
}
```

**Modifications dans `clearResults()`** :

Ajouter dans la zone de nettoyage existante :

```javascript
seoH2.textContent = "";
seoH2.classList.add("hidden");
```

> Cela remplace le nettoyage implicite précédent (le H2 était masqué en tant qu'enfant de `#seo-intro.hidden`).

#### 3.3.10 Interaction avec `clearResults()` — bloc compact

`clearResults()` n'est **pas** modifié pour gérer le bloc compact. La raison :

- `clearResults()` est appelé au début de chaque `render*()` pour nettoyer les résultats précédents
- Le mode du bloc recherche est défini **après** le rendering, en fin de `render*()`
- Si `clearResults()` réinitialisait le compact, il y aurait un flash visible (compact → ouvert → compact)

Le bloc compact est géré exclusivement par `setSearchCompact()`, `setSearchExpanded()` et `toggleSearchForm()`.

### 3.4 États visuels — Récapitulatif

| Page         | `#search-compact` | `#search-form-body` | Bouton compact                                 |
| ------------ | ----------------- | ------------------- | ---------------------------------------------- |
| Accueil      | `hidden`          | visible             | —                                              |
| Simple       | visible           | `.collapsed`        | « Modifier la recherche »                      |
| Comparaison  | visible           | `.collapsed`        | « Modifier la recherche »                      |
| Ville        | visible           | `.collapsed`        | « Modifier la recherche »                      |
| Mois         | visible           | `.collapsed`        | « Modifier la recherche »                      |
| Après toggle | visible           | visible             | « Modifier la recherche » (aria-expanded=true) |

### 3.5 Contenu du bloc compact par type de page

| Type de page | Ligne « Commune »               | Ligne « Période »       |
| ------------ | ------------------------------- | ----------------------- |
| Simple       | `Saint-Véran (05)`              | `6 mars → 12 mars 2026` |
| Comparaison  | `Saint-Véran (05) vs Lyon (69)` | `6 mars → 12 mars 2026` |
| Ville        | `Saint-Véran (05)`              | _(masqué)_              |
| Mois         | `Saint-Véran (05)`              | `mars 2026`             |

### 3.6 Intégration R52 — Nettoyage de l'index git

Exécuter une fois :

```bash
git rm --cached -r src/__pycache__/ tests/__pycache__/
git commit -m "chore: remove __pycache__ from git index"
```

Le `.gitignore` contient déjà `__pycache__/` (D9 de v17). Les fichiers `.pyc` ne seront plus trackés.

---

## 4) Plan d'implémentation

| Étape | Description                                                                                                                                                                                                                                                                     | Fichiers            | Testable                                                                                           |
| ----- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ------------------- | -------------------------------------------------------------------------------------------------- |
| E1    | `index.html` : insérer le bloc `#search-compact` (masqué), envelopper le contenu existant dans `<div id="search-form-body">`. Déplacer `<h2 id="seo-h2">` de `#seo-intro` vers une position autonome entre `#results-nav` et `#daily-summary` (avec classe `hidden` par défaut) | `public/index.html` | Visuel — page d'accueil inchangée (compact masqué, formulaire visible)                             |
| E2    | `style.css` : ajouter les styles `.search-compact`, transition `#search-form-body.collapsed`, séparateur, adaptation mobile                                                                                                                                                     | `public/style.css`  | Visuel — ajout de `.collapsed` manuellement dans l'inspecteur pour valider                         |
| E3    | `app.js` : ajouter les DOM refs, `setSearchCompact()`, `setSearchExpanded()`, `toggleSearchForm()`, `formatCompactPeriod()`, `formatCompactCommune()`, event listener. Adapter `renderSeoIntro()`, `setSeoIntro()`, `clearResults()` pour gérer le H2 autonome                  | `public/app.js`     | Fonctionnel — charger `/` → formulaire ouvert ; charger une page résultat → H2 entre nav et résumé |
| E4    | `app.js` : intégrer `setSearchCompact()` dans `renderSimpleResults()`, `renderComparisonResults()`, `renderTownPage()`, `renderMonthPage()` ; `setSearchExpanded()` si erreur ou accueil                                                                                        | `public/app.js`     | Fonctionnel — naviguer sur une page résultat → compact affiché, clic → déplie                      |
| E5    | R52 : `git rm --cached -r src/__pycache__/ tests/__pycache__/`                                                                                                                                                                                                                  | _(git index)_       | `git status` ne montre plus les `.pyc`                                                             |
| E6    | Validation : exécuter `pytest` → 93/93 PASSED. Tests manuels M16–M25.                                                                                                                                                                                                           | —                   | Tous les tests passent                                                                             |

---

## 5) Guide pour le Développeur

### Pièges fréquents

1. **`#search-form-body` enveloppe TOUT le contenu existant** — du `<h2>Recherche</h2>` au `<button id="search-button">` inclus. Ne pas oublier les `<div id="compare-toggle-area">`, `<div id="compare-fields">` et les `<div class="preset-buttons">` qui sont entre les deux. Si un élément n'est pas dans le wrapper, il restera visible quand le formulaire est replié.

2. **Le `max-height: 2000px` n'est pas une valeur magique** — c'est une borne haute. Si le formulaire avec le mode comparaison dépasse 2000px (très improbable), augmenter cette valeur. La transition CSS s'adapte proportionnellement.

3. **Ne pas appeler `setSearchCompact()` avant que les données soient disponibles** — Il faut que `selectedCommune` soit résolu et que les dates soient définies. L'appel se fait à la fin de chaque `render*()`, pas au début.

4. **Le `toggleSearchForm()` ne modifie que la visibilité** — Il ne touche pas au contenu du formulaire (pas de réinitialisation des champs). L'utilisateur retrouve le formulaire pré-rempli quand il déplie.

5. **`clearResults()` ne touche PAS au bloc compact** — C'est intentionnel. Le compact est géré par un circuit séparé (`setSearchCompact` / `setSearchExpanded`). Mélanger les deux provoquerait des flashes visuels.

6. **Le `<span class="search-modify-long">` est un détail CSS** — Sur desktop, le bouton dit « Modifier la recherche ». Sur mobile (≤ 600px), le `<span>` est masqué par CSS → le bouton dit « Modifier ». Pas de JS conditionnel (INV-12).

7. **Le H2 est maintenant autonome dans le DOM** — Après le déplacement hors de `#seo-intro`, le H2 n'est plus masqué automatiquement quand `seoIntroSection` reçoit `hidden`. Il faut explicitement masquer/afficher `seoH2` via sa propre classe `hidden`. Ne pas oublier de le réinitialiser dans `clearResults()`.

8. **L'ancre `#daily-summary` dans `#results-nav` reste correcte** — Le H2 inséré entre `#results-nav` et `#daily-summary` n'affecte pas le scroll vers `#daily-summary` (l'ancre cible l'ID, pas la position relative).

### Zones de dérive

- Ne pas ajouter de `pushState` ni de gestion d'historique pour le toggle du formulaire — c'est un toggle UI local, pas un changement de page.
- Ne pas créer un second formulaire pour le mode compact — le même DOM est partagé (INV-25).
- Ne pas animer le bloc compact lui-même — seul `#search-form-body` est animé (INV-27).
- Ne pas masquer la section `#search` entière sur les pages résultat — le bloc compact doit rester visible.

### Simplifications autorisées

- Les formateurs `Intl.DateTimeFormat` pour les dates du compact peuvent être instanciés dans `formatCompactPeriod()` à chaque appel (pas besoin de les pré-instancier en constantes module-level — la fonction est appelée une seule fois par chargement de page).
- Le séparateur compact ↔ formulaire (`border-top`) peut être omis si le rendu est jugé suffisamment clair sans lui.

### Décisions explicitement interdites

- Ne pas modifier de fichier backend.
- Ne pas modifier les 93 tests existants.
- Ne pas utiliser `innerHTML` (INV-7).
- Ne pas utiliser de media queries JS (INV-12).
- Ne pas dupliquer le formulaire (INV-25).
- Ne pas calculer des hauteurs dynamiques en JS pour l'animation (INV-27).

---

## 6) Stratégie de tests

### Tests existants à préserver

Les 93 tests existants doivent tous passer sans modification. La restructuration HTML (ajout d'un wrapper `<div>` autour du formulaire) ne modifie aucun `id` existant et n'affecte pas les réponses API.

### Nouveaux tests backend

Aucun. Cette itération est purement frontend.

### Tests manuels

| #   | Scénario                                                  | Vérification                                                                                                                                                                                                        |
| --- | --------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| M16 | Page d'accueil `/`                                        | Le formulaire complet est visible. Le bloc compact est masqué. Aucune régression visuelle.                                                                                                                          |
| M17 | Page simple `/meteo/saint-veran-05/2026-03-01/2026-03-07` | Le bloc compact est visible avec « Saint-Véran (05) » et « 1 mars → 7 mars 2026 ». Le formulaire est replié. Les données météo sont immédiatement visibles.                                                         |
| M18 | Clic sur « Modifier la recherche » (page simple)          | Le formulaire complet apparaît avec animation fluide sous le bloc compact. Le formulaire est pré-rempli. `aria-expanded="true"`.                                                                                    |
| M19 | Second clic sur « Modifier la recherche »                 | Le formulaire se replie. `aria-expanded="false"`.                                                                                                                                                                   |
| M20 | Page comparaison                                          | Le bloc compact affiche « {commune1} vs {commune2} » et la période. Formulaire replié.                                                                                                                              |
| M21 | Page Ville `/ville/saint-veran-05`                        | Bloc compact avec « Saint-Véran (05) » sans ligne de période. Données climatiques visibles immédiatement.                                                                                                           |
| M22 | Page Mois `/meteo/saint-veran-05/2026/03`                 | Bloc compact avec « Saint-Véran (05) » et « mars 2026 ». Résumé du mois visible immédiatement.                                                                                                                      |
| M23 | Mobile (≤ 600px) — page simple                            | Bloc compact empilé verticalement. Le bouton dit « Modifier » (texte court). Bouton pleine largeur.                                                                                                                 |
| M24 | Erreur de chargement (slug invalide)                      | Le formulaire reste ouvert (pas de compact). L'utilisateur peut corriger sa recherche.                                                                                                                              |
| M25 | H2 SEO — page simple                                      | Le H2 (« Historique météo à Gap du 10 au 12 mars 2026 ») apparaît entre la barre de navigation (Résumé/Graphique/Détail) et le tableau « Résumé par jour ». Il n'apparaît plus dans le bloc d'introduction en haut. |
| M26 | H2 SEO — page comparaison                                 | Le H2 est masqué (INV-21). Aucun H2 orphelin visible.                                                                                                                                                               |

### Total tests attendu

93 (hérités, tous passent) + 0 nouveaux tests automatisés = **93 tests minimum**

---

## 7) Risques techniques

| #   | Risque                                                                                                                   | Probabilité | Mitigation                                                                                                                                                                                                                                      |
| --- | ------------------------------------------------------------------------------------------------------------------------ | ----------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| R1  | Le wrapper `<div id="search-form-body">` casse des sélecteurs CSS existants qui ciblent des enfants directs de `#search` | Faible      | Vérifier qu'aucun style existant n'utilise `#search > h2`, `#search > label`, etc. Les styles existants ciblent par ID ou classe, pas par parenté directe. Valider visuellement après E1.                                                       |
| R2  | La transition `max-height: 2000px` crée un délai visible quand la hauteur réelle est petite (~400px)                     | Faible      | Avec `0.3s ease-out`, la portion visible de l'animation (0 → 400px) se joue dans les premiers ~60ms. L'effet est quasi-imperceptible. Si jugé trop lent, réduire `max-height` à `1000px`.                                                       |
| R3  | L'ajout de `#search-form-body` casse des sélecteurs JS existants qui cherchent des éléments dans `#search`               | Faible      | Tous les éléments du formulaire gardent leur `id`. Les `document.getElementById()` et `document.querySelector()` existants ne sont pas affectés par l'ajout d'un wrapper parent. Valider en exécutant les 93 tests backend + tests manuels M16. |
