# 001 — HistoMétéo MVP — Spécification Technique v13

> **Itération** : v13 — intègre le feedback Reviewer v12 (R38–R40, C1 `README.md` hors scope confirmé non bloquant) + nouvelles demandes UX (pictogrammes agrandis, colonnes Conditions repositionnées, renommages colonnes, bloc commune remonté et reformaté, renommage/ajout boutons preset, pictogramme dominant dans le résumé de la période).

---

## 0) Contract

- **Source of truth** : ce document (`001-histometeo-mvp.tech.v13.md`)
- **Functional integrity** : aucun critère d'acceptation de `001-histometeo-mvp.md` ne peut être modifié, ignoré ou réinterprété. Les AC1–AC11 restent la référence fonctionnelle absolue.
- **Scope** : fichiers autorisés à modifier :
  - `public/index.html` — restructuration du bloc résultats (`#commune-info` remonté après `#period-summary`), modification des en-têtes du tableau résumé par jour, renommage des boutons preset, ajout des nouveaux boutons preset historiques
  - `public/app.js` — modification de `buildSummaryRows()` (colonne Conditions repositionnée en 2e, icône agrandie, description en tooltip), modification de `buildHourlyTable()` (idem), modification de `formatDaySummaryInline()` (icône agrandie), modification de `renderCommuneInfo()` (layout compact en lignes), modification de `renderPeriodSummary()` / `buildPeriodSummaryParagraph()` (ajout pictogramme dominant), ajout logique preset historique (`applyHistoricalPreset`), renommage des labels preset
  - `public/style.css` — styles pour pictogrammes agrandis dans les tableaux (`.weather-icon-lg`), styles pour l'icône dans les en-têtes de jour (`.weather-icon-day-header`), styles pour le pictogramme du résumé de période (`.period-summary-icon`), styles pour le bloc commune compact (`.commune-info-compact`), intégration R38 (`#date-message` dans le fieldset), intégration R39 (déduplication `#search-button`), intégration R40 (`.preset-buttons button` dans règle partagée)
- **Forbidden changes** :
  - `src/` — **aucun fichier backend modifié** (zéro changement Python)
  - `src/weather_service.py`, `src/cache.py`, `src/commune_service.py`, `src/config.py`, `src/normals_service.py`, `src/main.py` — aucune modification
  - `tests/` — aucune modification
  - `docs/` — aucune modification des specs existantes (hors ce fichier)
  - `.github/` — aucune modification
  - `Dockerfile`, `pyproject.toml`, `requirements.txt` — pas de modification
  - `public/assets/` — pas de modification
- **Invariants** (tous hérités et préservés) :
  - INV-1 : Aucune donnée utilisateur stockée (ni serveur, ni client)
  - INV-2 : Heures en fuseau `Europe/Paris`
  - INV-3 : Période maximale 31 jours
  - INV-4 : Aucune clé API
  - INV-5 : Interface en français avec accents (`HistoMétéo`)
  - INV-6b : Page unique, URL reflète l'état via des routes propres (path segments)
  - INV-7 : Aucune injection HTML — `textContent`, `createElement`, `replaceChildren()` uniquement, jamais `innerHTML`
  - INV-8 : Le flux recherche simple fonctionne indépendamment du mode comparaison
  - INV-9 : Les normales climatiques sont un enrichissement progressif — leur indisponibilité ne bloque pas l'affichage des données météo ni la navigation
  - INV-10 : Les informations commune sont un enrichissement progressif — leur absence partielle n'empêche pas l'affichage du bloc
  - INV-11 : Le texte contextuel saisonnier et le bloc climat mensuel sont des enrichissements progressifs liés aux normales — leur absence (échec normales) ne dégrade rien
  - INV-12 : Le comportement responsive est 100% CSS. Aucune détection de user-agent, aucun JS conditionnel basé sur la taille d'écran. Les media queries CSS sont la seule source de vérité pour l'adaptation au viewport.
  - INV-13 : Le format interne des dates reste `YYYY-MM-DD`. Toute saisie utilisateur est convertie vers ce format avant utilisation par la logique métier. L'affichage utilisateur est en `JJ/MM/AAAA`.
- **Done when** :
  - Les 11 critères d'acceptation originaux (AC1–AC11) restent vérifiables
  - Toutes les fonctionnalités v2–v12 restent opérationnelles
  - **Tableau « Résumé par jour »** : colonne Conditions en 2e position (après Date), sans titre de colonne, pictogramme 2× plus gros, description en infobulle (`title`), colonnes renommées « Min (°C) » / « Max (°C) »
  - **Tableau « Détail horaire »** : colonne Conditions en 2e position (après Heure), sans titre, pictogramme 2× plus gros, description en infobulle
  - **En-têtes de jour** (détail horaire) : pictogramme 2× plus gros
  - **Bloc « Informations sur la commune »** : remonté juste après « Résumé de la période » dans le DOM, layout compact (Commune / Département / Région sur une ligne, Altitude sur la suivante, Population / Superficie / Densité sur la suivante)
  - **Boutons preset renommés** : « 3 derniers jours », « 7 derniers jours », « 15 derniers jours », « 30 derniers jours »
  - **Nouveaux boutons preset historiques** : « Il y a 1 an », « Il y a 5 ans », « Il y a 10 ans » — chacun positionne la période sur un seul jour (le même jour calendaire, N années en arrière)
  - **Résumé de la période** : un pictogramme de condition dominante est affiché sur la même ligne que le titre « Résumé de la période », cohérent avec le texte de résumé et la valeur `dominantIcon` des agrégats
  - R38 intégré : `#date-message` repositionné à l'intérieur du `<fieldset>`
  - R39 intégré : déduplication `min-height` sur `#search-button`
  - R40 intégré : `.preset-buttons button` couvert par la règle tactile partagée
  - Tous les tests backend existants (61/61) passent sans modification
  - Le site reste utilisable à 360px (iPhone SE) et identique visuellement sur desktop ≥ 768px

---

## 1) Objectif technique

Améliorer la lisibilité des données météo en repositionnant et en agrandissant les pictogrammes de conditions dans les tableaux et les en-têtes de jour. Réorganiser le bloc d'informations commune en position plus visible avec un layout compact. Enrichir les boutons de sélection rapide avec des raccourcis historiques (1, 5 et 10 ans en arrière). Ajouter un indicateur visuel de condition dominante dans le résumé de la période. Intégrer les recommandations R38–R40 du feedback v12.

---

## 2) Analyse du brief

### Besoins principaux

| #   | Besoin                                                                      | Source       | Complexité | Impact           |
| --- | --------------------------------------------------------------------------- | ------------ | ---------- | ---------------- |
| D10 | Colonne Conditions en 2e position dans « Résumé par jour »                  | Demande UX   | Faible     | Lisibilité       |
| D11 | Colonne Conditions sans titre, picto 2× plus gros, description en infobulle | Demande UX   | Faible     | Clarté visuelle  |
| D12 | Renommer « Temp. min/max » en « Min / Max » dans résumé par jour            | Demande UX   | Trivial    | Concision        |
| D13 | Idem D10/D11 pour le tableau « Détail horaire »                             | Demande UX   | Faible     | Cohérence        |
| D14 | Pictogramme 2× plus gros dans les en-têtes de jour                          | Demande UX   | Trivial    | Lisibilité       |
| D15 | Bloc commune remonté après le résumé de la période                          | Demande UX   | Faible     | Visibilité       |
| D16 | Layout commune compact : 3 lignes séparées par « / »                        | Demande UX   | Faible     | Compacité        |
| D17 | Renommage boutons preset (N jours → N derniers jours)                       | Demande UX   | Trivial    | Clarté           |
| D18 | Nouveaux boutons preset historiques (1 an, 5 ans, 10 ans)                   | Demande UX   | Faible     | Nouvelle feature |
| D19 | Pictogramme condition dominante dans le titre du résumé de période          | Demande UX   | Faible     | Lisibilité       |
| R38 | `#date-message` à l'intérieur du `<fieldset>`                               | Feedback v12 | Trivial    | Accessibilité    |
| R39 | Déduplication `min-height` sur `#search-button`                             | Feedback v12 | Trivial    | Maintenabilité   |
| R40 | `.preset-buttons button` dans la règle tactile partagée                     | Feedback v12 | Trivial    | Maintenabilité   |

### Contraintes

- **Aucune nouvelle dépendance** — tout reste en JS vanilla.
- **Aucun changement backend** — le périmètre est exclusivement frontend.
- **INV-7 absolu** — tout le DOM est construit via `createElement` / `textContent`. Zéro `innerHTML`. Les infobulles utilisent l'attribut `title` natif du navigateur.
- **INV-12 respecté** — aucun JS conditionnel responsive.
- **Les pictogrammes sont des emojis Unicode** — fournis par le backend via `WMO_DESCRIPTIONS`. L'agrandissement se fait uniquement via CSS (`font-size`) appliqué à un `<span>` dédié.
- **`computeAggregates()` fournit déjà `dominantIcon` et `dominantDesc`** — ces valeurs sont directement exploitables pour le pictogramme du résumé de période.
- **Les boutons preset historiques doivent respecter INV-3** — chaque bouton positionne une période d'un seul jour (start = end), ce qui respecte la limite de 31 jours.

### Risques

| #   | Risque                                                                                          | Probabilité | Impact | Mitigation                                                                                                                                    |
| --- | ----------------------------------------------------------------------------------------------- | ----------- | ------ | --------------------------------------------------------------------------------------------------------------------------------------------- |
| 1   | Les emojis rendus à 2× la taille par défaut ne s'alignent pas bien dans les cellules de tableau | Moyenne     | Faible | Utiliser `vertical-align: middle` et `line-height: 1` sur le `<span>` contenant l'emoji. Tester sur Chrome, Firefox, Safari.                  |
| 2   | Les boutons preset historiques dépassent la largeur sur mobile (9 boutons au total)             | Moyenne     | Faible | Les boutons sont en `flex-wrap: wrap` — ils passeront naturellement à la ligne suivante. Vérifier à 360px qu'aucun débordement ne se produit. |
| 3   | La date « il y a N ans » peut tomber un 29/02 sur une année non bissextile                      | Faible      | Moyen  | Gérer en JS : si la date cible n'existe pas (29/02 en année non bissextile), décaler au 28/02.                                                |

---

## 3) Design minimal proposé

### 3.1) Colonne Conditions repositionnée — Tableau « Résumé par jour »

**Actuellement** (`createDailySummaryTable` / `buildSummaryRows`) :

```
| Date | Temp. min (°C) | Temp. max (°C) | Pluie (mm) | Humidité moy. (%) | Vent moy. (km/h) | Conditions |
```

Chaque cellule Conditions contient en texte : `☀️ Ciel dégagé`.

**Cible** :

```
| Date | (pas de titre) | Min (°C) | Max (°C) | Pluie (mm) | Humidité moy. (%) | Vent moy. (km/h) |
```

La 2e colonne affiche uniquement le pictogramme, agrandi (2× `font-size`), dans un `<span class="weather-icon-lg">`. La description est en attribut `title` sur la cellule `<td>` (infobulle native au survol).

**Modifications** :

1. **En-têtes** (`public/index.html`) — l'ordre des `<th>` devient :

   ```
   Date | (th vide) | Min (°C) | Max (°C) | Pluie (mm) | Humidité moy. (%) | Vent moy. (km/h)
   ```

   Le 2e `<th>` est vide (pas de texte, juste `<th></th>`).

2. **Corps** (`buildSummaryRows` dans `app.js`) — l'ordre des cellules suit les en-têtes. La cellule Conditions est construite ainsi :

   ```js
   const conditionTd = document.createElement("td");
   conditionTd.title = summary.description || "";
   const iconSpan = document.createElement("span");
   iconSpan.className = "weather-icon-lg";
   iconSpan.textContent = summary.icon || "—";
   iconSpan.setAttribute(
     "aria-label",
     summary.description || "Conditions non disponibles",
   );
   conditionTd.appendChild(iconSpan);
   ```

   Les autres cellules sont des `<td>` avec `textContent` comme actuellement.

3. **CSS** :

   ```css
   .weather-icon-lg {
     font-size: 1.6em;
     line-height: 1;
     vertical-align: middle;
   }
   ```

   La `min-width` du tableau `#daily-summary table` peut être ajustée à la baisse vu que la colonne Conditions ne contient plus de texte.

### 3.2) Colonne Conditions repositionnée — Tableau « Détail horaire »

Même logique que §3.1 mais appliquée à `buildHourlyTable()`.

**Actuellement** :

```
| Heure | Temp. (°C) | Précip. (mm) | Humidité (%) | Vent (km/h) | Conditions |
```

**Cible** :

```
| Heure | (pas de titre) | Temp. (°C) | Précip. (mm) | Humidité (%) | Vent (km/h) |
```

**Modifications** :

1. **En-têtes** (dans `buildHourlyTable`) — l'ordre des `<th>` devient :

   ```
   Heure | (th vide) | Temp. (°C) | Précip. (mm) | Humidité (%) | Vent (km/h)
   ```

2. **Corps** — la cellule Conditions est construite identiquement à §3.1, avec un `<span class="weather-icon-lg">` et un `title` sur le `<td>`.

3. **La `min-width` du tableau horaire** n'est pas définie explicitement (pas de `#results table { min-width: ... }`) — rien à changer.

### 3.3) Pictogramme agrandi dans les en-têtes de jour (détail horaire)

**Actuellement** (`formatDaySummaryInline`) :

```
☀️ 5°C -> 15°C · 2.5 mm
```

Le pictogramme est au même `font-size` que le reste du texte.

**Cible** :

Le pictogramme est dans un `<span class="weather-icon-day-header">` avec un `font-size` doublé.

**Modification** :

La fonction `formatDaySummaryInline` retourne actuellement une simple string. Pour insérer un `<span>` avec une classe CSS, il faut la transformer en fonction qui produit un **DocumentFragment** ou des éléments DOM, ou bien séparer l'icône du texte et construire le DOM dans `renderDayGroups`.

**Approche retenue** : dans `renderDayGroups`, construire le contenu de `day-summary` avec des éléments DOM au lieu d'une string.

```js
// Au lieu de :
// infoSpan.textContent = formatDaySummaryInline(summary);

// Faire :
const iconSpan = document.createElement("span");
iconSpan.className = "weather-icon-day-header";
iconSpan.textContent = summary?.icon || "";

const textSpan = document.createElement("span");
const tMin = summary?.temp_min !== null ? `${summary.temp_min}°C` : "—";
const tMax = summary?.temp_max !== null ? `${summary.temp_max}°C` : "—";
const precipitation =
  summary?.precipitation_sum !== null ? `${summary.precipitation_sum} mm` : "—";
textSpan.textContent = ` ${tMin} → ${tMax} · ${precipitation}`;

infoSpan.appendChild(iconSpan);
infoSpan.appendChild(textSpan);
```

**CSS** :

```css
.weather-icon-day-header {
  font-size: 1.6em;
  line-height: 1;
  vertical-align: middle;
}
```

### 3.4) Bloc « Informations sur la commune » remonté et compact

**Actuellement** :

- Position dans le DOM : après `#climate-month`, avant `#period-links` (avant-dernier bloc)
- Layout : `<ul>` avec un `<li>` par info (Commune, Département, Région, Population, Superficie, Densité, Altitude), chaque `<li>` sur sa propre ligne.

**Cible** :

- Position dans le DOM : immédiatement après `#period-summary`, avant `#comparison-summary`
- Layout compact sur 3 lignes :
  - Ligne 1 : `{Commune} / {Département} ({code}) / {Région}`
  - Ligne 2 : `Altitude : {altitude} m`
  - Ligne 3 : `{population} habitants / {superficie} km² / {densité} hab/km²`

**Modifications DOM** (`public/index.html`) :

Déplacer le `<section id="commune-info" ...>` de sa position actuelle (après `#climate-month`) vers juste après `</section><!-- #period-summary -->`.

Nouvel ordre dans le DOM :

```
#seo-question
#period-summary
#commune-info          ← remonté ici
#comparison-summary
#results-nav
#daily-summary
#climate-normals
#chart
#results
#climate-month
#period-links
#info
```

**Modifications JS** (`renderCommuneInfo`) :

Refactorer la fonction pour produire le layout compact. Remplacer la `<ul>/<li>` par des `<p>` :

```js
function renderCommuneInfo(commune, elevation) {
  communeInfoSection.replaceChildren();

  const title = document.createElement("h2");
  title.textContent = "Informations sur la commune";

  const container = document.createElement("div");
  container.className = "commune-info-compact";

  // Ligne 1 : Commune / Département / Région
  const parts1 = [];
  if (commune?.nom) parts1.push(commune.nom);
  if (commune?.departement_nom && commune?.departement)
    parts1.push(`${commune.departement_nom} (${commune.departement})`);
  if (commune?.region_nom) parts1.push(commune.region_nom);

  if (parts1.length === 0) {
    communeInfoSection.classList.add("hidden");
    return;
  }

  const line1 = document.createElement("p");
  line1.textContent = parts1.join(" / ");

  // Ligne 2 : Altitude
  const line2 = document.createElement("p");
  if (typeof elevation === "number") {
    line2.textContent = `Altitude : ${noDecimalFr.format(Math.round(elevation))} m`;
  }

  // Ligne 3 : Population / Superficie / Densité
  const parts3 = [];
  if (typeof commune?.population === "number")
    parts3.push(`${numberFr.format(commune.population)} habitants`);
  if (typeof commune?.surface_km2 === "number")
    parts3.push(`${oneDecimalFr.format(commune.surface_km2)} km²`);
  if (
    typeof commune?.population === "number" &&
    typeof commune?.surface_km2 === "number" &&
    commune.surface_km2 > 0
  ) {
    const density = commune.population / commune.surface_km2;
    parts3.push(`${noDecimalFr.format(density)} hab/km²`);
  }
  const line3 = document.createElement("p");
  if (parts3.length) line3.textContent = parts3.join(" / ");

  container.appendChild(line1);
  if (line2.textContent) container.appendChild(line2);
  if (line3.textContent) container.appendChild(line3);

  communeInfoSection.replaceChildren(title, container);
  communeInfoSection.classList.remove("hidden");
}
```

**CSS** :

```css
.commune-info-compact {
  display: flex;
  flex-direction: column;
  gap: 0.25rem;
}

.commune-info-compact p {
  margin: 0;
  color: var(--muted);
}

.commune-info-compact p:first-child {
  font-weight: 600;
  color: var(--text);
}
```

**Note** : les anciens styles `.commune-info-list`, `.commune-info-list li`, `.info-label` ne sont plus utilisés. Ils peuvent rester dans le CSS (pas de nettoyage obligatoire — dead code mineur, pas de régression).

### 3.5) Renommage des boutons preset existants

**Actuellement** (`public/index.html`) :

```html
<button type="button" data-days="0" disabled>Hier</button>
<button type="button" data-days="3" disabled>3 jours</button>
<button type="button" data-days="7" disabled>7 jours</button>
<button type="button" data-days="15" disabled>15 jours</button>
<button type="button" data-days="30" disabled>30 jours</button>
```

**Cible** :

```html
<button type="button" data-days="0" disabled>Hier</button>
<button type="button" data-days="3" disabled>3 derniers jours</button>
<button type="button" data-days="7" disabled>7 derniers jours</button>
<button type="button" data-days="15" disabled>15 derniers jours</button>
<button type="button" data-days="30" disabled>30 derniers jours</button>
```

« Hier » reste inchangé.

### 3.6) Nouveaux boutons preset historiques

**HTML** — ajoutés après les boutons existants dans `#preset-buttons` :

```html
<button type="button" data-years-ago="1" disabled>Il y a 1 an</button>
<button type="button" data-years-ago="5" disabled>Il y a 5 ans</button>
<button type="button" data-years-ago="10" disabled>Il y a 10 ans</button>
```

**Attribut `data-years-ago`** : un attribut distinct de `data-days` pour différencier la logique.

**Logique JS** — nouvelle fonction `applyHistoricalPreset(yearsAgo)` :

```js
function applyHistoricalPreset(yearsAgo) {
  const today = new Date();
  const targetYear = today.getFullYear() - yearsAgo;
  const targetMonth = today.getMonth(); // 0-indexed
  const targetDay = today.getDate();

  // Gérer le 29/02 en année non bissextile
  let targetDate = new Date(targetYear, targetMonth, targetDay);
  if (targetDate.getMonth() !== targetMonth) {
    // Le jour n'existe pas (ex: 29/02 en année non bissextile) → 28/02
    targetDate = new Date(targetYear, targetMonth + 1, 0); // dernier jour du mois
  }

  const isoDate = formatIsoDateLocal(targetDate);
  dateStart.value = isoDate;
  dateEnd.value = isoDate;
  dateStartController.syncHiddenToText();
  dateEndController.syncHiddenToText();

  isDateRangeValid();
  updateSearchButtonState();
}
```

**Points d'intégration** :

- Les nouveaux boutons sont sélectionnés via `document.querySelectorAll("#preset-buttons button[data-years-ago]")`.
- Un event listener similaire à celui des boutons `data-days` est ajouté :

```js
document
  .querySelectorAll("#preset-buttons button[data-years-ago]")
  .forEach((button) => {
    button.addEventListener("click", () => {
      if (button.disabled) return;
      const yearsAgo = Number.parseInt(button.dataset.yearsAgo, 10);
      applyHistoricalPreset(yearsAgo);
      clearPresetActiveState();
      button.classList.add("active");
    });
  });
```

- `clearPresetActiveState()` doit couvrir **tous** les boutons preset (existants + nouveaux). Il utilise déjà `presetButtons` qui est sélectionné via `#preset-buttons button` — les nouveaux boutons seront automatiquement inclus si la sélection se fait après l'ajout au DOM.
- L'activation/désactivation doit aussi couvrir les nouveaux boutons (même logique que les existants : désactivés tant qu'aucune commune n'est sélectionnée).

**Validation** : la date cible doit être dans la plage valide (≥ 01/01/1940, ≤ hier). Si la date « il y a 10 ans » tombe avant 1940, le bouton calculé positionnera la date mais la validation existante (`isDateRangeValid`) affichera le message d'erreur approprié. Aucune logique de validation supplémentaire n'est nécessaire.

### 3.7) Pictogramme de condition dominante dans le résumé de la période

**Actuellement** (`renderPeriodSummary`) :

Le titre est le `<h2>Résumé de la période</h2>` statique dans le HTML. Le contenu est injecté dans `#period-summary-body`.

**Cible** :

Le `<h2>` contient le titre **et** un pictogramme de condition dominante, de la même taille que les autres pictogrammes de la page (taille de base, pas 2×).

```
Résumé de la période  ☀️
```

**Modification** :

Dans `renderPeriodSummary()`, après avoir rendu le body, mettre à jour le `<h2>` :

```js
function renderPeriodSummary(agg, communeName, startDate, endDate) {
  // Mise à jour du titre avec le pictogramme dominant
  const titleEl = periodSummarySection.querySelector("h2");
  titleEl.replaceChildren(); // vider le h2
  const titleText = document.createTextNode("Résumé de la période ");
  titleEl.appendChild(titleText);

  if (agg.dominantIcon && agg.dominantIcon !== "❓") {
    const iconSpan = document.createElement("span");
    iconSpan.className = "period-summary-icon";
    iconSpan.textContent = agg.dominantIcon;
    iconSpan.title = agg.dominantDesc || "";
    iconSpan.setAttribute(
      "aria-label",
      `Conditions dominantes : ${agg.dominantDesc || ""}`,
    );
    titleEl.appendChild(iconSpan);
  }

  periodSummaryBody.replaceChildren(
    buildPeriodSummaryParagraph(agg, communeName, startDate, endDate),
  );
  periodSummarySection.classList.remove("hidden");
}
```

**CSS** :

```css
.period-summary-icon {
  font-size: 1em;
  vertical-align: middle;
  margin-left: 0.3rem;
}
```

La taille du pictogramme est `1em` (taille de base du `<h2>`), ce qui le rend cohérent avec les titres. Pas de doublement ici — le pictogramme dans le titre est déjà visuellement plus grand car le `<h2>` a un `font-size` supérieur au body.

### 3.8) Intégration R38 — `#date-message` dans le fieldset

**Actuellement** : le `<p id="date-message">` est placé après `</fieldset>`.

**Modification** : déplacer `<p id="date-message" class="field-message" aria-live="polite"></p>` à l'intérieur du `<fieldset class="date-fieldset">`, juste avant `</fieldset>`. Cela améliore le lien sémantique entre le message d'erreur et le groupe de champs de date.

### 3.9) Intégration R39 — Déduplication `#search-button`

**Actuellement** : `#search-button` déclare `min-height: 44px` dans son bloc propre, et est déjà couvert par la règle partagée tactile.

**Modification** : supprimer `min-height: 44px` du bloc `#search-button`.

### 3.10) Intégration R40 — `.preset-buttons button` dans la règle tactile partagée

**Actuellement** : `.preset-buttons button` déclare indépendamment `min-height: 44px; display: flex; align-items: center; justify-content: center;`.

**Modification** :

1. Ajouter `.preset-buttons button` au sélecteur de la règle tactile partagée (à côté de `.btn-secondary`, `.btn-cancel`, `.btn-period-link`, `.results-nav a`, `.btn-calendar`).
2. Supprimer `min-height`, `display`, `align-items`, `justify-content` du bloc `.preset-buttons button`. Conserver les propriétés spécifiques (`padding`, `font-size`, `border-radius`, `background`, `border`, `color`).
3. **Note** : les boutons preset utilisent `display: flex` (non `inline-flex` comme la règle partagée). Évaluer si `inline-flex` convient — dans ce contexte (enfants d'un parent `flex-wrap`), `inline-flex` fonctionne de la même manière. Sinon, conserver `display: flex` dans le bloc individuel.

---

## 4) Plan d'implémentation

### Étape 1 — HTML : restructuration DOM et boutons preset

**Fichier** : `public/index.html`

1. **Déplacer** `<section id="commune-info" ...>` de sa position actuelle (après `#climate-month`) vers juste après la fermeture de `<section id="period-summary"...>`.
2. **Renommer** les en-têtes du tableau `#daily-summary` :
   - Insérer un `<th></th>` vide en 2e position (après « Date »)
   - Renommer `Temp. min (°C)` → `Min (°C)`
   - Renommer `Temp. max (°C)` → `Max (°C)`
   - Supprimer le `<th>Conditions</th>` en dernière position
3. **Renommer** les labels des boutons preset : `3 jours` → `3 derniers jours`, etc.
4. **Ajouter** les 3 boutons `data-years-ago` (1, 5, 10) dans `#preset-buttons`.
5. **Déplacer** `<p id="date-message" ...>` à l'intérieur du `<fieldset>` (R38).

**Vérifiable** : le HTML est valide, le tableau a 7 colonnes avec les bons en-têtes, les boutons preset sont 8 au total (Hier + 4 durées + 3 historiques), `#commune-info` est positionné après `#period-summary`.

### Étape 2 — JS : tableaux avec Conditions en 2e position

**Fichier** : `public/app.js`

1. **Modifier `buildSummaryRows()`** :
   - La cellule Conditions (actuellement dernière) devient la 2e cellule.
   - Elle contient un `<span class="weather-icon-lg">` avec l'emoji et un attribut `title` sur le `<td>` avec la description.
   - Les autres cellules suivent dans l'ordre : Min, Max, Pluie, Humidité, Vent.

2. **Modifier `buildHourlyTable()`** :
   - Les en-têtes deviennent : `Heure`, `(vide)`, `Temp. (°C)`, `Précip. (mm)`, `Humidité (%)`, `Vent (km/h)`.
   - La cellule Conditions (actuellement dernière) devient la 2e cellule, même construction que dans le résumé.

3. **Ajouter un helper** pour créer la cellule Conditions de manière DRY :

   ```js
   function createConditionCell(icon, description) {
     const td = document.createElement("td");
     td.title = description || "";
     const iconSpan = document.createElement("span");
     iconSpan.className = "weather-icon-lg";
     iconSpan.textContent = icon || "—";
     iconSpan.setAttribute(
       "aria-label",
       description || "Conditions non disponibles",
     );
     td.appendChild(iconSpan);
     return td;
   }
   ```

**Vérifiable** : le pictogramme est en 2e colonne dans les deux tableaux. Le survol affiche la description en infobulle. Le pictogramme est visuellement plus gros.

### Étape 3 — JS : en-têtes de jour avec pictogramme agrandi

**Fichier** : `public/app.js`

1. **Modifier `renderDayGroups()`** : au lieu d'appeler `formatDaySummaryInline(summary)` et faire `infoSpan.textContent = ...`, construire le contenu de `infoSpan` via des éléments DOM :
   - `<span class="weather-icon-day-header">` pour l'emoji
   - `<span>` pour le texte restant (températures + précipitations)

2. **La fonction `formatDaySummaryInline`** peut être conservée pour les cas où seule une string est nécessaire, mais n'est plus appelée dans `renderDayGroups`.

**Vérifiable** : les en-têtes de jour affichent un pictogramme visuellement plus gros que le texte, suivi des infos de température et précipitations.

### Étape 4 — JS : bloc commune compact + résumé de période avec pictogramme

**Fichier** : `public/app.js`

1. **Refactorer `renderCommuneInfo()`** selon §3.4 — layout compact en 3 lignes avec `<p>` et séparateurs `/`.
2. **Modifier `renderPeriodSummary()`** selon §3.7 — ajouter le pictogramme dominant dans le `<h2>`.

**Vérifiable** : le bloc commune est compact (3 lignes max). Le titre du résumé de période affiche le bon emoji (cohérent avec `dominantDesc`).

### Étape 5 — JS : boutons preset historiques + renommage

**Fichier** : `public/app.js`

1. **Ajouter** la fonction `applyHistoricalPreset(yearsAgo)` décrite en §3.6.
2. **Ajouter** les event listeners pour les boutons `[data-years-ago]`.
3. **S'assurer** que `clearPresetActiveState()` couvre les nouveaux boutons.
4. **S'assurer** que l'activation/désactivation des boutons (quand une commune est sélectionnée) inclut les nouveaux boutons `[data-years-ago]`.

**Vérifiable** : clic sur « Il y a 1 an » remplit les dates avec le jour correspondant à il y a 1 an exactement. Clic sur « Il y a 10 ans » pareil. Le 29/02 est géré.

### Étape 6 — CSS : styles pictogrammes, commune compact, R39, R40

**Fichier** : `public/style.css`

1. **Ajouter** `.weather-icon-lg` (picto 2× en tableau).
2. **Ajouter** `.weather-icon-day-header` (picto 2× dans en-têtes de jour).
3. **Ajouter** `.period-summary-icon` (picto dans titre résumé).
4. **Ajouter** `.commune-info-compact` + ses `p`.
5. **R39** : supprimer `min-height: 44px` du bloc `#search-button`.
6. **R40** : ajouter `.preset-buttons button` au sélecteur de la règle tactile partagée. Supprimer les propriétés dupliquées du bloc individuel.
7. **Ajuster** `#daily-summary table { min-width: ... }` si nécessaire (la colonne Conditions ne contient plus de texte, le tableau peut être un peu plus étroit).

**Vérifiable** : les pictogrammes sont visuellement 2× plus gros dans les tableaux et en-têtes de jour. Le bloc commune est compact. Pas de régression visuelle.

### Étape 7 — Validation finale

1. Vérifier visuellement à 360px (Chrome DevTools → iPhone SE) : tous les tableaux lisibles, boutons preset sur 2-3 lignes sans débordement, bloc commune compact, infobulle des conditions visible au tap long.
2. Vérifier visuellement à 1024px : alignement des colonnes, pictogrammes bien alignés verticalement, infobulle visible au hover.
3. Tester les 3 boutons historiques : « Il y a 1 an », « Il y a 5 ans », « Il y a 10 ans » — dates correctes, gestion du 29/02.
4. Tester le résumé de période : le pictogramme affiché correspond à `dominantIcon` des agrégats.
5. Tester le bloc commune : 3 lignes, séparateurs `/`, toutes les infos présentes.
6. Exécuter `pytest tests/ -v` — 61/61 tests doivent passer.
7. Grep `innerHTML` dans `app.js` → 0 occurrence (INV-7 préservé).
8. Grep `window.innerWidth|matchMedia|userAgent` dans `app.js` → 0 occurrence (INV-12 préservé).

---

## 5) Guide pour le Développeur

### Pièges fréquents

1. **La cellule Conditions dans les tableaux utilise `title` pour l'infobulle** — l'attribut `title` natif du `<td>` est la méthode la plus simple et cross-browser pour afficher une infobulle au survol. Sur mobile, la description n'est visible qu'en appui long — c'est un compromis acceptable et explicite. Ne PAS implémenter de tooltip custom en JS.

2. **Le pictogramme 2× dans les tableaux est un `font-size: 1.6em`** — pas un `transform: scale(2)`. L'utilisation de `em` garantit le scaling relatif au contexte. Vérifier que le `line-height: 1` et le `vertical-align: middle` assurent un bon alignement dans les cellules.

3. **`buildSummaryRows()` construit les cellules par `textContent` dans une boucle** — la cellule Conditions ne peut plus être une simple string. Elle doit être construite comme un élément DOM. Séparer la construction de cette cellule des autres.

4. **Les boutons `data-years-ago` et `data-days` coexistent** — le code doit différencier les deux types de presets. Les sélecteurs `[data-years-ago]` et `[data-days]` sont mutuellement exclusifs.

5. **`formatDaySummaryInline` retournait une string** — après la modification, le contenu de `day-summary` est construit via des éléments DOM. Ne pas tenter de concaténer des `<span>` via `textContent`.

6. **Déplacement de `#commune-info` dans le DOM** — le JS actuel sélectionne `communeInfoSection` via `document.getElementById("commune-info")`. Ce sélecteur continuera de fonctionner quel que soit l'emplacement dans le DOM. Aucune modification JS n'est nécessaire pour le déplacement.

7. **`computeAggregates` fournit déjà `dominantIcon` et `dominantDesc`** — ces valeurs sont calculées par comptage de fréquence des conditions journalières. Ne pas recalculer cette logique dans `renderPeriodSummary`.

8. **`clearPresetActiveState()` enlève la classe `active` de tous les boutons dans `#preset-buttons`** — vérifier que la sélection `presetButtons` inclut bien les nouveaux boutons `[data-years-ago]` (elle le fera si `presetButtons` est défini avec `#preset-buttons button` sans filtre `[data-days]`).

### Zones de dérive

- **Ne pas implémenter de tooltip custom en JS** — l'attribut `title` natif suffit pour la v13. Un tooltip accessible (ARIA) serait une amélioration future.
- **Ne pas modifier `computeAggregates`** — l'icône dominante est déjà calculée correctement.
- **Ne pas modifier le backend** — tout le travail est exclusivement frontend.
- **Ne pas ajouter de logique pour vérifier si la date historique est dans la plage valide** — `isDateRangeValid()` le fait déjà.
- **Ne pas créer un composant séparé pour les presets historiques** — ils s'intègrent dans le même `#preset-buttons`.

### Simplifications autorisées

- Si l'alignement vertical du pictogramme 2× pose problème sur un navigateur spécifique, ajuster le `vertical-align` ou le `line-height` est autorisé.
- La `min-width` du tableau `#daily-summary table` peut être réduite de `860px` à une valeur inférieure si nécessaire.

### Décisions explicitement interdites

- **Interdit** : utiliser `innerHTML` pour construire les cellules Conditions.
- **Interdit** : utiliser `!important` dans les nouvelles règles CSS.
- **Interdit** : modifier `computeAggregates()`, `isDateRangeValid()`, `updateSearchButtonState()`, ou `performSearch()`.
- **Interdit** : utiliser des images (SVG, PNG) pour remplacer les emojis — les emojis restent la source des pictogrammes.
- **Interdit** : ajouter une dépendance (Tooltip.js, Tippy.js).
- **Interdit** : supprimer les classes `.anomaly-warm`, `.anomaly-cold`, `.anomaly-wet`, `.anomaly-dry`, `.comparison-winner`.

---

## 6) Stratégie de tests

### Tests automatisés existants

Les 61 tests backend (`pytest tests/ -v`) doivent passer à l'identique. Aucun test n'est ajouté, modifié ou supprimé — il n'y a pas de changement backend.

### Tests manuels requis

| #     | Scénario                                                    | Device simulé | Résultat attendu                                                                                                                                                            |
| ----- | ----------------------------------------------------------- | ------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| TM-25 | Tableau « Résumé par jour » : vérifier l'ordre des colonnes | 1024px        | Colonnes : Date, (picto), Min (°C), Max (°C), Pluie (mm), Humidité moy. (%), Vent moy. (km/h). La colonne picto n'a pas de titre.                                           |
| TM-26 | Tableau « Résumé par jour » : survol du pictogramme         | 1024px        | L'infobulle native affiche la description (ex: « Ciel dégagé »).                                                                                                            |
| TM-27 | Tableau « Résumé par jour » : taille du pictogramme         | 1024px        | Le pictogramme est visuellement ~2× plus gros que le texte des autres colonnes.                                                                                             |
| TM-28 | Tableau « Détail horaire » : vérifier l'ordre des colonnes  | 1024px        | Colonnes : Heure, (picto), Temp. (°C), Précip. (mm), Humidité (%), Vent (km/h). La colonne picto n'a pas de titre.                                                          |
| TM-29 | Tableau « Détail horaire » : survol du pictogramme          | 1024px        | L'infobulle native affiche la description.                                                                                                                                  |
| TM-30 | En-tête de jour : taille du pictogramme                     | 1024px        | Le pictogramme dans l'en-tête de jour (à droite) est ~2× plus gros que le texte des températures.                                                                           |
| TM-31 | Bloc commune : position et layout                           | 1024px        | Le bloc « Informations sur la commune » apparaît juste après « Résumé de la période ». Layout : Commune / Dept / Région sur ligne 1, Altitude sur 2, Pop/Sup/Densité sur 3. |
| TM-32 | Bloc commune : données partielles                           | 1024px        | Si certaines infos manquent, les lignes correspondantes ne s'affichent pas. Pas de « / » traînant.                                                                          |
| TM-33 | Boutons preset renommés                                     | 360px         | Les boutons affichent « Hier », « 3 derniers jours », « 7 derniers jours », « 15 derniers jours », « 30 derniers jours ».                                                   |
| TM-34 | Bouton « Il y a 1 an »                                      | 1024px        | Clic → les deux champs date se remplissent avec la même date = aujourd'hui − 1 an. La validation s'enchaîne.                                                                |
| TM-35 | Bouton « Il y a 5 ans »                                     | 1024px        | Clic → les deux champs date = aujourd'hui − 5 ans. Idem.                                                                                                                    |
| TM-36 | Bouton « Il y a 10 ans »                                    | 1024px        | Clic → les deux champs date = aujourd'hui − 10 ans. Si < 01/01/1940, le message de validation s'affiche.                                                                    |
| TM-37 | Bouton historique un 29/02 en année bissextile              | 1024px        | Si on est le 29/02/2028 et on clique « Il y a 1 an », la date cible est le 28/02/2027 (décalage automatique).                                                               |
| TM-38 | Résumé de période : pictogramme dominant                    | 1024px        | Le titre « Résumé de la période » affiche un emoji à droite du texte, correspondant à la condition dominante. Le survol affiche la description.                             |
| TM-39 | Responsive 360px : boutons preset et tableau                | 360px         | Les 8 boutons preset s'affichent sur 2-3 lignes sans débordement horizontal. Le tableau scrolle horizontalement si nécessaire.                                              |
| TM-40 | Mode comparaison : non régression                           | 1024px        | Le mode comparaison fonctionne toujours. Le bloc commune est masqué en mode comparaison (comportement existant préservé).                                                   |

### Edge cases critiques

- **Pictogramme absent** (`icon` = `null` ou `""`) — la cellule Conditions affiche « — ».
- **Description absente** (`description` = `null`) — l'attribut `title` est vide, pas d'infobulle.
- **`dominantIcon` = `"❓"` (aucune condition exploitable)** — le pictogramme n'est pas ajouté au titre du résumé de période.
- **Commune sans population ni superficie** — les lignes 2 et 3 du bloc compact ne s'affichent pas.
- **Date « il y a 10 ans » un 29/02** — la date est décalée au 28/02.
- **Date « il y a 10 ans » avant 1940** — la validation existante affiche le message d'erreur.

---

## 7) Risques techniques

| #   | Risque                                                            | Probabilité | Impact | Mitigation                                                                                                                  |
| --- | ----------------------------------------------------------------- | ----------- | ------ | --------------------------------------------------------------------------------------------------------------------------- |
| 1   | Emojis 2× la taille default mal alignés dans les cellules tableau | Moyenne     | Faible | `vertical-align: middle` et `line-height: 1` sur le `<span>`. Tester Chrome, Firefox, Safari.                               |
| 2   | 8 boutons preset débordent sur mobile (360px)                     | Moyenne     | Faible | `flex-wrap: wrap` existant gère le passage à la ligne. Vérifier visuellement à 360px.                                       |
| 3   | Date « il y a N ans » un 29/02 en année non bissextile            | Faible      | Moyen  | Vérifier via `getMonth()` après construction de `new Date()` — si le mois a changé, utiliser le dernier jour du mois cible. |
