/* Centre de zone : la pastille numérotée de la liste (`.zone-liste-numero`),
   à l'identique — disque plein de la couleur de la zone (posée en JS via
   `el.style.background`), chiffre blanc, liseré blanc, ombre. L'anneau d'avant
   ne portait aucun numéro : sur trois zones, la carte ne disait plus laquelle
   était laquelle, et il fallait revenir à la liste pour l'apprendre. Les
   points de vente restent distincts par leur glyphe (« TB ») et leur taille. */
.zone-marqueur { width: 22px; height: 22px; border-radius: 50%; background: var(--trust-blue); border-style: solid; border-width: 2px; border-color: var(--white); box-shadow: var(--shadow-md); display: flex; align-items: center; justify-content: center; }
.zone-marqueur-numero { color: var(--white); font-family: 'JetBrains Mono', monospace; font-size: 11px; font-weight: 700; line-height: 1; pointer-events: none; }
/* Points de vente : le même disque, en plus petit et avec un glyphe au lieu
   d'un numéro — tout le reste (couleur de zone posée en JS, liseré blanc,
   ombre, centrage) vient de `.zone-marqueur` ci-dessus et n'est plus
   redéclaré. Le glyphe (`.zone-marqueur-glyphe`) est décoratif : le nom réel
   du site est porté par `title` et par la bulle MapLibre (`setPopup`). */
.zone-marqueur--site { width: 18px; height: 18px; }
.zone-marqueur-glyphe { color: var(--white); font-family: 'JetBrains Mono', monospace; font-size: 8px; font-weight: 700; line-height: 1; letter-spacing: -.02em; pointer-events: none; }

.zone-canvas { position: relative; height: 220px; margin-bottom: var(--sp-2); }
.zone-tableau--replie { display: none; }
/* Cible tactile de 44 px (adapt, finding A6) : le bouton mesurait 97 × 26.
   `inline-flex` + `min-height` agrandissent la zone cliquable sans grossir le
   glyphe — le texte garde sa taille, c'est la boîte qui grandit. */
.zone-tableau-bouton { display: inline-flex; align-items: center; min-height: 44px; font-size: .85rem; color: var(--accent-text); background: none; border: 0; padding: var(--sp-1) 0; cursor: pointer; }
/* `style.css` pose `a { color: inherit; text-decoration: none }` sur tout le
   site : sans cette règle, les liens des notes du panneau (« Hypothèses et
   limites » sur Immobilier, « Le guide de l'API » sur Potentiel, « le plan
   Discovery » sur API) se lisent comme du texte gris. L'espace client n'a pas
   ce reset et rend ces liens visibles — c'est ce rendu-là qu'on reprend.
   `--accent-text` et non `--trust-blue` : la note est déjà en `--text-muted`
   sur un fond très clair, et le jeton sémantique est le seul mesuré. */
.zone-panneau .demo-result-note a { color: var(--accent-text); text-decoration: underline; }
.zone-tableau-bouton-chevron { display: inline-block; margin-right: var(--sp-1); font-size: .75rem; line-height: 1; }
.zone-tableau-bouton:focus-visible { outline: 2px solid var(--trust-blue); outline-offset: 2px; border-radius: var(--r-sm); }

/* ── Démo application « zone de chalandise » — mise en page (spec § 3) ── */
/* `--nav-h` : le nav du site est `position: fixed` (base.html) — chaque
   page racine compense (`.hero`, `blog.css` `.blog-hero-section`, `demo.css`
   `.demo-page`) ; `.app-page` ne le faisait pas, le bandeau/h1 passait sous
   le nav à toute largeur (fix round 2, finding 2). */
.app-page { display: flex; flex-direction: column; padding-top: calc(var(--nav-h) + var(--sp-6)); }
.app-bandeau { padding: var(--sp-4) var(--sp-4) var(--sp-3); max-width: 1200px; margin: 0 auto; width: 100%; }
.app-bandeau-titre { font-size: 1.35rem; margin: var(--sp-2) 0 var(--sp-1); }
.app-bandeau-sous-titre { color: var(--text-muted); margin: 0; font-size: .95rem; }

/* `.zone-scene` enveloppe `#zone-carte` (frère précédent) et `#zone-app` :
   seul ancêtre positionné commun, indispensable à `.zone-carte { position:
   absolute }` sur ordinateur (fix round 2, finding 1 — voir
   zone_de_chalandise.html pour le détail du bug). Téléphone : simple pile en
   flux, la carte occupe ≈ 45 % de la hauteur puis les panneaux suivent. */
.zone-scene { display: flex; flex-direction: column; gap: var(--sp-3); padding: 0 var(--sp-4) var(--sp-6); }
/* `minmax(0, 1fr)` et non `1fr` (adapt, finding A1) : la taille minimale
   automatique d'un élément de grille vaut sa taille min-content, et celle des
   panneaux était dictée par `.zone-tuiles` / `.zone-graphiques` / les tableaux
   — 535 px de large dans une colonne de 343 px, soit 176 px de débordement
   horizontal de la PAGE sur tout téléphone (mesuré : `scrollWidth` 551 à
   375 px de viewport). `minmax(0, …)` ici, `min-width: 0` sur chaque conteneur
   imbriqué : les deux sont nécessaires, la chaîne casse au premier maillon
   manquant. */
.zone-app { display: grid; grid-template-rows: auto auto; grid-template-columns: minmax(0, 1fr); gap: var(--sp-3); }
.zone-carte { position: relative; min-height: 45vh; border-radius: var(--r-lg); overflow: hidden; background: var(--ice-blue); border: 1px solid var(--mist); }
.zone-carte-repli { position: absolute; inset: 0; display: grid; place-items: center; padding: var(--sp-6); text-align: center; color: var(--text-muted); background: var(--ice-blue); }
.zone-carte-repli--erreur[hidden] { display: none; }
/* Attribution : un `<details>` et non plus un `<p>` (adapt, finding A2). Le
   texte complet — crédit ODbL, fournisseur de tuiles, sources INSEE/IGN —
   tenait sur six lignes au téléphone et couvrait un tiers de la carte. Le
   résumé porte le crédit court, toujours visible (c'est lui qui satisfait
   l'ODbL) ; le texte complet reste dans le DOM et s'ouvre d'une touche. Sur
   ordinateur `x-init` ouvre le bloc au montage et le résumé s'efface : on
   retrouve exactement l'ancienne ligne unique. Sans JavaScript, tout reste
   atteignable — le `<details>` est simplement fermé. */
.zone-carte-attribution { position: absolute; right: var(--sp-2); bottom: var(--sp-1); max-width: calc(100% - var(--sp-4)); margin: 0; font-size: .8rem; color: var(--text-muted); background: rgba(255,255,255,.75); padding: 0 var(--sp-1); border-radius: var(--r-sm); z-index: 2; }
[data-theme="dark"] .zone-carte-attribution, :root:not([data-theme="light"]) .zone-carte-attribution { background: rgba(11,22,40,.75); }
/* Cible tactile de 44 px sur le déclencheur, comme partout dans
   l'application ; le chevron `::after` remplace le marqueur natif, mal placé
   dans une ligne en flex (même motif que `.zone-fonction-titre`). */
/* `text-overflow` n'a aucun effet sur un conteneur flex : le texte du résumé
   vit donc dans son propre <span>, seul élément à rétrécir (`min-width: 0`)
   et à porter la troncature, le chevron restant `flex: none` à côté de lui.
   Mesuré à 375 px avant ce lot : `scrollWidth` 293 pour `clientWidth` 265, le
   texte coupé net et le chevron rogné (fix final, finding F8). */
.zone-carte-attribution-resume { display: flex; align-items: center; justify-content: flex-end; gap: var(--sp-1); min-width: 0; min-height: 44px; padding: 0 var(--sp-1); cursor: pointer; list-style: none; }
.zone-carte-attribution-texte { min-width: 0; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
.zone-carte-attribution-resume::-webkit-details-marker { display: none; }
.zone-carte-attribution-resume::after { content: "▸"; font-size: .75rem; flex: none; }
.zone-carte-attribution[open] .zone-carte-attribution-resume::after { content: "▾"; }
.zone-carte-attribution-resume:focus-visible { outline: 2px solid var(--trust-blue); outline-offset: 2px; border-radius: var(--r-sm); }
.zone-carte-attribution-detail { margin: 0 0 var(--sp-1); padding: 0 var(--sp-1); }
.zone-carte-attribution-detail a { color: var(--accent-text); }

.zone-liste, .zone-panneau, .zone-sature { min-width: 0; background: var(--white); border: 1px solid var(--mist); border-radius: var(--r-lg); padding: var(--sp-4); box-shadow: var(--shadow-md); }
:root:not([data-theme="light"]) .zone-liste, :root:not([data-theme="light"]) .zone-panneau, :root:not([data-theme="light"]) .zone-sature { background: var(--ocean-deep); border-color: var(--glass-border); }
/* Bandeau de saturation : il se pose au-dessus de l'exemple servi par le
   cache, il ne le remplace pas (spec § 8.4, fix final, finding C2). */
.zone-sature { border-color: var(--amber-warn); }
.zone-sature p { margin: var(--sp-2) 0 0; font-size: .9rem; }
/* Libellés compacts des sélecteurs durée/mode : ils remplacent des
   `aria-label` par de vrais `<label for>` (fix final, ledger) — visibles,
   donc utiles à tout le monde, pas seulement aux lecteurs d'écran. */
.zone-liste-form-libelle { font-size: .8rem; color: var(--text-muted); }
/* Barre de repli, à toutes les largeurs depuis ce lot (spec § 3 pour le
   téléphone, retour fondateur pour l'étendre à l'ordinateur — plus de
   `!important` forçant l'ouverture au-delà de 1024px, voir le @media plus
   bas). Amélioration progressive : sans JavaScript, `.zone-liste-corps` n'a
   aucun style inline (pas de `x-cloak`) donc reste affiché, et la barre
   n'est pas rendue du tout (`hidden` posé par le serveur, levé par l'`x-init`
   de l'<aside>) — un chevron qui ne replie rien vaut moins que pas de
   chevron. */
.zone-liste-barre { display: flex; align-items: center; justify-content: space-between; gap: var(--sp-2); width: 100%; min-height: 44px; margin: 0 0 var(--sp-2); padding: var(--sp-2) var(--sp-1); background: none; border: 0; border-bottom: 1px solid var(--mist); color: inherit; font: inherit; font-weight: 600; text-align: left; cursor: pointer; }
.zone-liste-barre:focus-visible { outline: 2px solid var(--trust-blue); outline-offset: 2px; }
/* `display: flex` ci-dessus l'emporterait sur l'attribut `hidden` : sans
   cette règle, le bouton rendu `hidden` par le serveur (révélé par l'`x-init`
   de l'<aside>, `_liste.html`) resterait visible et inerte sans JavaScript. */
.zone-liste-barre[hidden] { display: none; }
/* Premier rendu sans décalage (CLS, Lighthouse 2026-09-18) : le script inline
   du <head> de zone_de_chalandise.html pose `data-zone-liste` sur <html>
   avant la première peinture, avec l'état que l'`x-init` de l'<aside> va
   recalculer. Tant que l'attribut est là — jusqu'à ce que cet `x-init` le
   retire —, la barre se voit et le corps replié ne se voit pas, exactement
   comme après hydratation : plus rien ne bouge quand Alpine arrive. Les
   sélecteurs `html[data-zone-liste="repliee"] .zone-liste` plus bas donnent
   le même premier rendu à l'apparence repliée (carte flottante, barre du
   pouce). Sans JavaScript l'attribut n'existe pas : liste ouverte, barre
   absente, comme avant. */
html[data-zone-liste] .zone-liste-barre[hidden] { display: flex; }
html[data-zone-liste="repliee"] .zone-liste-corps { display: none; }
/* Deux habillages du même bouton (voir le commentaire dans `_liste.html`) :
   le texte « N zones · Ajouter » est l'habillage téléphone (par défaut,
   sous 1024px) ; le duo titre/compte est réservé à l'ordinateur. */
.zone-liste-barre-entete { display: none; align-items: baseline; gap: var(--sp-2); min-width: 0; }
.zone-liste-barre-compte { font-weight: 400; color: var(--text-muted); font-size: .85rem; }
.zone-liste-barre-chevron { display: inline-flex; flex: none; transition: transform .15s ease; }
.zone-liste-barre-chevron svg { display: block; }
.zone-liste-barre-chevron--replie { transform: rotate(-90deg); }
/* Les trois contrôles (mode, heure de pointe, durée) empilés — chacun prend
   toute la largeur, lisible à 375px comme sur un panneau flottant desktop. */
.zone-liste-form-ligne { display: flex; flex-direction: column; gap: var(--sp-3); margin: var(--sp-3) 0; }

/* ── Mode : contrôle segmenté (radios enveloppés dans leur label) ──────
   Même famille que `.demo-portee-radio`/`.demo-portee-btn` (démo Mes lieux),
   en version « enveloppée » : ici l'icône et le texte vivent dans le
   <label> qui contient le radio (pas un frère), donc l'état sélectionné et
   le focus se lisent avec `:has()` plutôt qu'avec le combinateur `+`. */
.zone-mode-fieldset { border: 0; margin: 0; padding: 0; min-width: 0; }
.zone-mode-segments { display: flex; gap: var(--sp-1); margin-top: var(--sp-1); }
.zone-mode-segment {
  flex: 1 1 0;
  display: flex; flex-direction: column; align-items: center; justify-content: center;
  gap: var(--sp-1);
  min-height: 44px;
  padding: var(--sp-2) var(--sp-1);
  border-radius: var(--r-md);
  border: 1px solid var(--glass-border);
  color: var(--text-muted);
  text-align: center;
  cursor: pointer;
  transition: background .15s, color .15s, border-color .15s;
}
[data-theme="light"] .zone-mode-segment { border-color: var(--mist); }
/* Le radio reste dans le flux — jamais `display: none`, qui le sortirait de
   la tabulation — mais devient invisible ; c'est son `<label>` (ancêtre)
   qui porte l'apparence, via `:has()`. */
.zone-mode-radio { position: absolute; opacity: 0; pointer-events: none; width: 1px; height: 1px; }
.zone-mode-segment:has(.zone-mode-radio:checked) {
  background: var(--btn-primary-bg);
  border-color: var(--btn-primary-bg);
  color: var(--white);
}
/* Le focus se voit sur le segment, puisque le radio est invisible. */
.zone-mode-segment:has(.zone-mode-radio:focus-visible) { outline: 2px solid var(--trust-blue); outline-offset: 2px; }
.zone-mode-icone svg { display: block; }
.zone-mode-texte { font-size: .75rem; line-height: 1.15; }
@media (prefers-reduced-motion: reduce) { .zone-mode-segment { transition: none; } }

/* ── Heure de pointe : case réelle redessinée en interrupteur ────────── */
.zone-mode-pointe { display: flex; align-items: center; gap: var(--sp-2); min-height: 44px; font-size: .85rem; color: var(--text-muted); cursor: pointer; }
.zone-mode-pointe:has(.zone-mode-pointe-case:disabled) { opacity: .55; cursor: not-allowed; }
/* Case native, agrandie et repeinte en piste + galet — reste une vraie
   checkbox (soumise sans JavaScript), seule son apparence change.
   `.zone-switch` est le même dessin sans la case : un <span> décoratif que
   porte la bascule des points de vente (`.zone-liste-sites`, un lien, l'état
   vivant dans l'URL). Une seule grammaire de bascule dans le panneau au lieu
   de trois — les glyphes ☑/☐ du lien sont partis avec ce lot. Les valeurs
   ci-dessous (40 × 24, galet de 18, course de 16) sont la géométrie du
   dessin, pas du rythme d'espacement : elles restent en pixels. */
.zone-mode-pointe-case, .zone-switch {
  position: relative;
  flex: none;
  width: 40px; height: 24px;
  margin: 0;
  border-radius: 999px;
  border: 1px solid var(--glass-border);
  background: rgba(255, 255, 255, .12);
  transition: background .15s, border-color .15s;
}
.zone-mode-pointe-case { appearance: none; -webkit-appearance: none; cursor: pointer; }
/* `:not(.zone-switch--on)` : sans lui, `[data-theme="light"] .zone-switch`
   (deux sélecteurs) l'emporterait sur `.zone-switch--on` (un seul) et
   l'interrupteur resterait gris une fois allumé, en thème clair seulement.
   La case, elle, se distingue par `:checked` à spécificité égale et plus
   bas dans la feuille — rien à changer de ce côté. */
[data-theme="light"] .zone-mode-pointe-case,
[data-theme="light"] .zone-switch:not(.zone-switch--on) { border-color: var(--mist); background: var(--mist); }
.zone-mode-pointe-case::before, .zone-switch::before {
  content: "";
  position: absolute;
  top: 2px; left: 2px;
  width: 18px; height: 18px;
  border-radius: 50%;
  background: var(--white);
  box-shadow: var(--shadow-sm);
  transition: transform .15s;
}
.zone-mode-pointe-case:checked, .zone-switch--on { background: var(--btn-primary-bg); border-color: var(--btn-primary-bg); }
.zone-mode-pointe-case:checked::before, .zone-switch--on::before { transform: translateX(16px); }
.zone-mode-pointe-case:focus-visible { outline: 2px solid var(--trust-blue); outline-offset: 2px; }
.zone-mode-pointe-case:disabled { cursor: not-allowed; }
@media (prefers-reduced-motion: reduce) {
  .zone-mode-pointe-case, .zone-mode-pointe-case::before,
  .zone-switch, .zone-switch::before { transition: none; }
}

/* ── Durée : curseur plutôt qu'un menu déroulant ──────────────────────
   `<output>` affiche la valeur vive à côté du libellé ; le rendu serveur
   la pose déjà à « 10 min » (valeur par défaut de l'input), Alpine ne fait
   que la garder à jour ensuite — fonctionne identiquement sans JavaScript. */
.zone-liste-duree-entete { display: flex; justify-content: space-between; align-items: baseline; gap: var(--sp-2); }
.zone-liste-duree-valeur { font-size: .85rem; font-variant-numeric: tabular-nums; color: var(--text-muted); }
.zone-liste-duree-range {
  appearance: none; -webkit-appearance: none;
  width: 100%;
  height: 44px; /* cible tactile, le trait visuel reste fin (piste ci-dessous) */
  margin: 0;
  background: transparent;
  cursor: pointer;
}
.zone-liste-duree-range::-webkit-slider-runnable-track { height: 6px; border-radius: 999px; background: rgba(255, 255, 255, .12); }
.zone-liste-duree-range::-moz-range-track { height: 6px; border-radius: 999px; background: rgba(255, 255, 255, .12); }
[data-theme="light"] .zone-liste-duree-range::-webkit-slider-runnable-track { background: var(--mist); }
[data-theme="light"] .zone-liste-duree-range::-moz-range-track { background: var(--mist); }
.zone-liste-duree-range::-webkit-slider-thumb {
  appearance: none; -webkit-appearance: none;
  width: 24px; height: 24px;
  margin-top: -9px; /* centre le galet de 24px sur la piste de 6px */
  border-radius: 50%;
  background: var(--btn-primary-bg);
  border: 2px solid var(--white);
  box-shadow: var(--shadow-sm);
  cursor: pointer;
}
.zone-liste-duree-range::-moz-range-thumb {
  width: 24px; height: 24px;
  border-radius: 50%;
  background: var(--btn-primary-bg);
  border: 2px solid var(--white);
  box-shadow: var(--shadow-sm);
  cursor: pointer;
}
.zone-liste-duree-range:focus-visible { outline: 2px solid var(--trust-blue); outline-offset: 2px; }
.zone-liste-items { list-style: none; padding: 0; margin: var(--sp-3) 0; display: grid; gap: var(--sp-2); }
/* Grille à trois colonnes (icône | textes | actions, spec fondateur) : la
   colonne du milieu porte tout le contenu qui s'empile (adresse sur deux
   lignes, chiffres, notes, erreur, détail des sites) dans un simple flux
   vertical (`.zone-liste-texte`), pas des sous-lignes de grille. */
.zone-liste-item { display: grid; grid-template-columns: auto 1fr auto; align-items: start; gap: var(--sp-2); padding: var(--sp-2); border-radius: var(--r-md); border: 1px solid transparent; }
.zone-liste-item--active { border-color: var(--zone-couleur); background: color-mix(in srgb, var(--zone-couleur) 10%, transparent); }
/* Zone masquée (bouton œil) : atténuée dans la liste, jamais retirée — ses
   chiffres restent lisibles au clic, seule la carte l'omet. */
.zone-liste-item--masquee { opacity: .55; }
.zone-liste-item-lien { display: block; min-width: 0; color: inherit; text-decoration: none; }
/* Cellule « Zone » du tableau des prix de l'onglet Comparer : la pastille de
   couleur et le nom sur la même ligne de base, la portée en dessous et en
   gris. Mêmes règles que l'espace client (`zones.css`). */
.zone-tableau-zone { display: inline-flex; align-items: baseline; gap: var(--sp-2); }
.zone-tableau-secondaire { display: block; color: var(--text-muted); }
.zone-liste-pastille { width: 12px; height: 12px; border-radius: 50%; background: var(--zone-couleur); flex: none; }
.zone-liste-libelle { overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
/* Icône de mode, en lieu et place de la pastille de couleur (spec
   fondateur) : `currentColor` la teinte via la même variable `--zone-couleur`
   que la pastille et la bordure active ci-dessus — pas un token de palette
   brut, donc rien à contourner pour le scanner a11y (`tests/test_a11y_markup.py`
   ne liste que des tokens nommés, jamais les variables `--zone-*`). L'infobulle
   (« 10 min en voiture ») est en CSS pur depuis `data-info`, doublée d'un
   `title` et d'un texte masqué visuellement pour les lecteurs d'écran — un
   `tabindex="0"` rend le déclencheur `:focus-within` atteignable au clavier. */
.zone-liste-icone-colonne { display: flex; flex-direction: column; align-items: center; gap: var(--sp-1); }
.zone-liste-icone {
  position: relative;
  display: inline-flex; align-items: center; justify-content: center;
  width: 28px; height: 28px; margin-top: 2px; flex: none; /* 2px : alignement optique sur la première ligne, pas du rythme d'espacement */
  color: var(--zone-couleur);
  cursor: default;
}
/* Cible tactile de 44 px sans grossir le glyphe (adapt, finding A6) : le
   déclencheur d'infobulle mesurait 28 × 28 alors qu'il porte un `tabindex`.
   La boîte est étendue par un `::before` centré — la grille de la ligne ne
   bouge pas, et 44 px de large s'arrêtent exactement au bord du lien de la
   zone (28 px d'icône + 2 × 8 px de gouttière). */
.zone-liste-icone::before { content: ""; position: absolute; top: 50%; left: 50%; width: 44px; height: 44px; transform: translate(-50%, -50%); }
.zone-liste-icone svg { display: block; position: relative; }
.zone-liste-item--masquee .zone-liste-icone { opacity: .6; }
/* Pastille numérotée sous l'icône de mode (spec fondateur, liste trop
   dense) : un remplissage de la couleur de la zone avec le numéro en blanc
   — un fond, pas du texte de couleur de palette brute, donc hors de la
   portée de `test_aucune_couleur_de_palette_brute_en_texte`. */
.zone-liste-numero {
  display: inline-flex; align-items: center; justify-content: center;
  width: 18px; height: 18px; flex: none;
  border-radius: 50%;
  background: var(--zone-couleur);
  color: var(--white);
  font-family: 'JetBrains Mono', monospace;
  font-weight: 700;
  font-size: 11px;
  line-height: 1;
}
.zone-liste-item--masquee .zone-liste-numero { opacity: .6; }
.zone-liste-icone:hover::after, .zone-liste-icone:focus-visible::after, .zone-liste-icone:focus-within::after {
  content: attr(data-info);
  position: absolute; top: 100%; left: 0; margin-top: var(--sp-1);
  padding: var(--sp-1) var(--sp-2);
  background: var(--charcoal); color: var(--white);
  font-size: .75rem; white-space: nowrap; border-radius: var(--r-sm);
  box-shadow: var(--shadow-md);
  z-index: 5; pointer-events: none;
}
.zone-liste-icone:focus-visible { outline: 2px solid var(--trust-blue); outline-offset: 2px; border-radius: var(--r-sm); }

.zone-liste-texte { display: flex; flex-direction: column; gap: var(--sp-1); min-width: 0; }
.zone-liste-ligne { display: block; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
.zone-liste-ligne--commune { color: var(--text-muted); font-size: .85rem; }
.zone-liste-chiffre { font-family: 'JetBrains Mono', monospace; font-variant-numeric: tabular-nums; color: var(--text-muted); font-size: .78rem; margin: 0; }
.zone-liste-note { font-size: .8rem; color: var(--text-muted); }
/* Cible tactile de 44 px comme `.zone-panneau-bascule` : la croix de retrait
   faisait ~19 px de côté (fix final, finding I12). Même cible pour le
   bouton masquer/afficher, empilé au-dessus dans `.zone-liste-actions`. */
.zone-liste-actions { display: flex; flex-direction: column; gap: var(--sp-1); }
.zone-liste-masquer { color: var(--text-muted); text-decoration: none; display: inline-flex; align-items: center; justify-content: center; min-width: 44px; min-height: 44px; border-radius: var(--r-sm); }
.zone-liste-masquer--actif { color: var(--accent-text); }
.zone-liste-masquer:focus-visible { outline: 2px solid var(--trust-blue); outline-offset: 2px; }
.zone-liste-retirer { color: var(--danger-text); text-decoration: none; font-size: 1.2rem; line-height: 1; display: inline-flex; align-items: center; justify-content: center; min-width: 44px; min-height: 44px; }
.zone-liste-retirer:focus-visible { outline: 2px solid var(--trust-blue); outline-offset: 2px; }
.zone-liste-avertissement { color: var(--danger-text); font-size: .85rem; }
.zone-liste-sites { display: flex; align-items: center; gap: var(--sp-2); min-height: 44px; margin-top: var(--sp-3); padding: var(--sp-1) 0; border-radius: var(--r-sm); color: inherit; text-decoration: none; }
.zone-liste-sites-texte { min-width: 0; }
.zone-liste-sites:focus-visible { outline: 2px solid var(--trust-blue); outline-offset: 2px; }
/* Détail dépliable des noms de sites sous le compteur (spec fondateur) :
   `<details>` reste utilisable sans JS ; plus de `grid-column` à étendre,
   c'est un enfant de flux normal de `.zone-liste-texte` désormais. */
.zone-liste-sites-detail > summary { cursor: pointer; }
.zone-liste-sites-noms { list-style: none; margin: var(--sp-1) 0 0; padding: 0 0 0 var(--sp-3); display: grid; gap: var(--sp-1); font-size: .78rem; }
.zone-liste-sites-duree { color: var(--text-muted); }
.zone-liste-quota { font-size: .8rem; color: var(--text-muted); margin: var(--sp-2) 0 0; }

.zone-panneau-entete { display: flex; justify-content: space-between; align-items: baseline; gap: var(--sp-2); }
.zone-panneau-titre { font-size: 1.1rem; margin: 0; }
/* Bascule ouvrir/fermer (remplace l'ancienne croix `.zone-panneau-fermer` —
   même famille que `.zone-liste-barre-chevron` : texte visible + chevron
   `aria-hidden`, jamais un `aria-label` seul. Cible tactile de 44 px comme
   partout dans l'application (CLAUDE.md « Tap targets »). */
.zone-panneau-bascule { display: inline-flex; align-items: center; gap: var(--sp-1); min-height: 44px; padding: var(--sp-1) var(--sp-2); border-radius: var(--r-sm); color: var(--text-muted); text-decoration: none; font-size: .85rem; font-weight: 600; }
.zone-panneau-bascule:focus-visible { outline: 2px solid var(--trust-blue); outline-offset: 2px; }
.zone-panneau-bascule-chevron { display: inline-flex; flex: none; transition: transform .15s ease; }
.zone-panneau-bascule-chevron svg { display: block; }
.zone-panneau-bascule-chevron--replie { transform: rotate(-90deg); }
@media (prefers-reduced-motion: reduce) { .zone-panneau-bascule-chevron { transition: none; } }
.zone-panneau-bandeau { background: var(--ice-blue); color: var(--charcoal); border-radius: var(--r-md); padding: var(--sp-2) var(--sp-3); font-size: .85rem; }
.zone-panneau-corps, .zone-onglet-corps { min-width: 0; }
/* Les sept onglets défilent DANS leur barre (`overflow-x: auto`), jamais en
   poussant la page : sans `min-width: 0` la barre prenait la largeur de ses
   sept liens et emportait le panneau avec elle (finding A1). */
.zone-onglets { display: flex; gap: var(--sp-1); min-width: 0; overflow-x: auto; scroll-snap-type: x proximity; margin: var(--sp-3) 0; border-bottom: 1px solid var(--mist); }
/* Le fondu de bord est le seul signe qu'il reste des onglets hors du cadre :
   `overflow-x: auto` n'affiche aucune barre de défilement sur macOS ni sur
   téléphone, et le dernier onglet (« API ») se coupait en plein milieu
   (scrollWidth 603 pour clientWidth 589 dans le panneau à 36 %) sans que rien
   ne le dise. La classe est posée par `zone-app.js` **seulement** quand il
   reste quelque chose à faire défiler, et retirée en fin de course : un fondu
   permanent sur une barre qui ne défile pas mangerait le dernier onglet pour
   rien. Le gabarit compact ci-dessous (8 px de gouttière au lieu de 12) fait
   entrer les sept onglets dans le panneau, le fondu ne servant plus alors
   qu'au téléphone.

   Les deux bords, et non plus le seul bord droit (fix final, finding F4) : en
   fin de course « Population » avait entièrement disparu à gauche et
   « Ménages » était coupé en plein mot, sans que rien ne dise qu'il restait
   quelque chose de ce côté-là. Trois règles plutôt que deux classes qui se
   marcheraient dessus — `mask-image` ne se cumule pas, la dernière déclarée
   gagnerait. */
.zone-onglets--fondu-droite { -webkit-mask-image: linear-gradient(to right, black calc(100% - 32px), transparent); mask-image: linear-gradient(to right, black calc(100% - 32px), transparent); }
.zone-onglets--fondu-gauche { -webkit-mask-image: linear-gradient(to left, black calc(100% - 32px), transparent); mask-image: linear-gradient(to left, black calc(100% - 32px), transparent); }
.zone-onglets--fondu-gauche.zone-onglets--fondu-droite { -webkit-mask-image: linear-gradient(to right, transparent, black 32px, black calc(100% - 32px), transparent); mask-image: linear-gradient(to right, transparent, black 32px, black calc(100% - 32px), transparent); }
.zone-onglet { padding: var(--sp-2); scroll-snap-align: start; white-space: nowrap; color: var(--text-muted); text-decoration: none; border-bottom: 2px solid transparent; min-height: 44px; display: inline-flex; align-items: center; }
.zone-onglet--actif { color: var(--accent-text); border-bottom-color: currentColor; }
/* L'anneau global (`style.css`, `:where(a, button…)`) s'arrêterait au bord du
   lien, donc sous le fondu de la barre défilante : `outline-offset` négatif
   pour qu'il reste entièrement dans l'onglet. */
.zone-onglet:focus-visible { outline: 2px solid var(--trust-blue); outline-offset: -2px; border-radius: var(--r-sm); }
/* 120 px et non 140 : à 375 px de viewport la colonne utile fait ~311 px, et
   `minmax(140px, …)` y tenait deux tuiles de justesse — mais imposait 140 px
   de taille min-content à la grille parente dès que le panneau était plus
   étroit (finding A1). */
.zone-tuiles { display: grid; grid-template-columns: repeat(auto-fit, minmax(120px, 1fr)); gap: var(--sp-3); margin-bottom: var(--sp-4); min-width: 0; }
.zone-tuile { display: grid; gap: var(--sp-1); padding: var(--sp-3); border-radius: var(--r-md); background: var(--cloud); }
:root:not([data-theme="light"]) .zone-tuile { background: var(--glass-bg); }
.zone-tuile-libelle { font-size: .8rem; color: var(--text-muted); }
.zone-tuile-valeur { font-size: 1.6rem; font-weight: 700; font-variant-numeric: tabular-nums; }
.zone-tuile-unite { font-size: .75rem; color: var(--text-muted); }
/* `auto-fit` plutôt qu'un `1fr 1fr` fixe : un seul graphique (CSP) remplit
   la colonne au lieu de s'écraser dans une moitié de grille vide, deux
   graphiques (Population, Logement) se partagent la largeur quand elle le
   permet, et tout empile sous 260px de large (téléphone). */
.zone-graphiques { display: grid; grid-template-columns: repeat(auto-fit, minmax(200px, 1fr)); gap: var(--sp-4); min-width: 0; }
.zone-graphique { margin: 0; min-width: 0; }
/* Onze tranches d'âge : la cellule à demi-largeur (auto-fit, minmax(260px))
   coupait une étiquette sur deux malgré `autoSkip: false` faute de place —
   pleine largeur de la grille, le doughnut de pauvreté suit dessous dans le
   flux normal (demande fondateur, fix final ledger 2026-09-17). */
.zone-graphique--large { grid-column: 1 / -1; }
.zone-graphique-titre { font-size: .95rem; margin: 0 0 var(--sp-2); }
/* Un tableau ne se comprime pas sous sa largeur min-content : il défile donc
   dans SA boîte (`overflow-x: auto`), jamais en élargissant la page (finding
   A1). `zone-graphiques.js` insère le canvas et le bouton « Voir les chiffres »
   via `table.parentNode` — cette boîte est donc leur parent, ce qui ne change
   rien à leur rendu (le canvas se dimensionne sur la largeur disponible). */
.zone-tableau-boite { min-width: 0; max-width: 100%; overflow-x: auto; }
.zone-tableau { width: 100%; border-collapse: collapse; font-size: .85rem; }
.zone-tableau caption { text-align: left; margin-bottom: var(--sp-2); }
.zone-tableau th, .zone-tableau td { text-align: left; padding: var(--sp-1) var(--sp-2); border-bottom: 1px solid var(--mist); }
/* Alignement porté explicitement par la même classe sur le `<th>` et ses
   `<td>` (macro `tableau`, `_onglet_macros.html`, et le tableau écrit à la
   main des chevauchements) — remplace l'ancien `td:not(:first-child)` qui
   n'alignait que les cellules : l'en-tête restait à gauche pendant que sa
   colonne passait à droite, désaccordant la ligne de lecture. */
.zone-tableau-numerique { text-align: right; font-variant-numeric: tabular-nums; }
.zone-panneau-pied { margin-top: var(--sp-4); font-size: .75rem; color: var(--text-muted); }

/* ── Onglet Potentiel : la nomenclature INSEE comme sélecteur ─────────────
   13 fonctions dépliables (`<details>` natifs), 151 postes en radios d'un
   même groupe. Fermées par défaut, la liste tient donc en 14 lignes ; le
   panneau d'indicateurs scrolle déjà (`.zone-panneau { overflow: auto }`
   au-delà de 1024px, la page en dessous), rien à borner ici. */
/* Deux portes d'entrée au-dessus de la nomenclature : six secteurs fréquents
   en pastilles (des liens, donc l'état reste dans l'URL et le geste marche
   sans JavaScript) et un filtre texte révélé par Alpine. Sans elles, trouver
   « Boulangerie » parmi 151 postes demandait de déplier au jugé. */
.zone-secteur-boite { margin-top: var(--sp-4); }
.zone-secteur-raccourcis { display: flex; flex-wrap: wrap; align-items: center; gap: var(--sp-2); }
.zone-secteur-raccourcis-titre { flex: none; font-size: .8rem; color: var(--text-muted); }
.zone-secteur-raccourci { display: inline-flex; align-items: center; min-height: 44px; padding: var(--sp-1) var(--sp-3); border: 1px solid var(--mist); border-radius: var(--r-pill); font-size: .8rem; color: var(--text-muted); text-decoration: none; }
.zone-secteur-raccourci:hover { color: var(--accent-text); border-color: currentColor; }
.zone-secteur-raccourci:focus-visible { outline: 2px solid var(--trust-blue); outline-offset: 2px; }
.zone-secteur-raccourci--choisi { background: var(--ice-blue); border-color: var(--ice-blue); color: var(--charcoal); }
.zone-secteur-filtre { display: grid; gap: var(--sp-1); margin-top: var(--sp-3); }
/* Ligne d'en-tête de la liste : « 2,40 % » et « 0,020 » sur la même ligne ne
   se lisaient que par déduction. Mêmes colonnes que `.zone-poste`, en plus
   petit et en couleur sourdine — une légende, pas une ligne de plus. */
.zone-nomenclature-entete { display: flex; flex-wrap: wrap; align-items: baseline; gap: var(--sp-2); padding: var(--sp-1) var(--sp-2); font-size: .75rem; color: var(--text-muted); border-bottom: 1px solid var(--mist); }
.zone-nomenclature-entete-poste { flex: 1 1 auto; }
.zone-nomenclature-entete-part, .zone-nomenclature-entete-coef { flex: none; text-align: right; }
.zone-secteur { display: grid; gap: var(--sp-2); margin-top: var(--sp-3); }
.zone-nomenclature { border: 1px solid var(--mist); border-radius: var(--r-md); margin: 0; padding: var(--sp-1); }
.zone-nomenclature legend { padding: 0 var(--sp-1); }
.zone-fonction { border-top: 1px solid var(--mist); }
.zone-fonction:first-of-type { border-top: 0; }
/* Cible tactile de 44px comme partout dans l'application. `list-style: none`
   + `::marker` vide : le chevron par défaut de Safari/Chrome se place mal
   dans une ligne en flex ; on redessine le nôtre avec `::before`. */
.zone-fonction-titre {
  display: flex; align-items: center; gap: var(--sp-2);
  min-height: 44px; padding: var(--sp-2); cursor: pointer;
  font-size: .85rem; font-weight: 600; list-style: none;
}
.zone-fonction-titre::-webkit-details-marker { display: none; }
.zone-fonction-titre::before { content: "▸"; color: var(--text-muted); font-size: .75rem; flex: none; }
.zone-fonction[open] > .zone-fonction-titre::before { content: "▾"; }
.zone-fonction-libelle { flex: 1 1 auto; }
.zone-fonction-titre:focus-visible { outline: 2px solid var(--trust-blue); outline-offset: -2px; }
/* Le coefficient et le code en JetBrains Mono : ce sont des identifiants et
   des valeurs techniques (charte 2026). */
.zone-coef { font-family: 'JetBrains Mono', monospace; font-size: .78rem; font-variant-numeric: tabular-nums; flex: none; }
.zone-poste-code { font-family: 'JetBrains Mono', monospace; font-size: .75rem; color: var(--text-muted); flex: none; min-width: 4.2em; }
.zone-poste-part { font-family: 'JetBrains Mono', monospace; font-size: .75rem; color: var(--text-muted); font-variant-numeric: tabular-nums; flex: none; }
.zone-poste {
  display: flex; align-items: center; gap: var(--sp-2);
  min-height: 44px; padding: var(--sp-1) var(--sp-2);
  border-radius: var(--r-sm); cursor: pointer; font-size: .8rem;
}
.zone-poste:hover { background: var(--cloud); }
:root:not([data-theme="light"]) .zone-poste:hover { background: var(--glass-bg); }
.zone-poste-libelle { flex: 1 1 auto; }
.zone-poste--fonction { font-weight: 600; }
/* Le retrait dit la profondeur de la nomenclature (CP011 puis CP0111), ce
   que le guide public fait avec des espaces insécables. */
.zone-poste--n4 { padding-left: var(--sp-6); }
.zone-poste--aucun { font-weight: 600; }
.zone-poste--choisi { background: var(--ice-blue); color: var(--charcoal); }
.zone-poste--choisi .zone-poste-code, .zone-poste--choisi .zone-poste-part { color: var(--charcoal); }
.zone-poste input:focus-visible { outline: 2px solid var(--trust-blue); outline-offset: 2px; }

/* Ordinateur : la carte en fond de la région, les panneaux flottent dessus,
   et le panneau de droite rétrécit la carte au lieu de la couvrir. */
@media (min-width: 1024px) {
  /* `.zone-scene` est l'unique ancêtre positionné de `#zone-carte` ET
     `#zone-app` (fix round 2, finding 1) — la hauteur qui était posée sur
     `.zone-app` vit ici. */
  .zone-scene { position: relative; display: block; height: calc(100vh - var(--nav-h) - 96px); min-height: 560px; padding: 0; }
  /* Sélecteur d'ID, pas `.zone-carte` : MapLibre pose la classe
     `maplibregl-map` directement sur le conteneur qu'on lui passe (ce même
     `#zone-carte`, pas un enfant) dès `monter()`, et `maplibre-gl.css`
     (chargé après `demo-app.css`) porte `.maplibregl-map { position:
     relative }` — à spécificité de classe égale, la feuille chargée en
     dernier gagne et annulait silencieusement le `position: absolute`
     ci-dessous une fois la carte montée (fix round 2, finding 1 bis,
     découvert en vérifiant la région au clavier/inspecteur, pas seulement à
     la lecture du HTML statique). Un sélecteur d'ID passe devant quel que
     soit l'ordre des feuilles. */
  #zone-carte { position: absolute; inset: 0; border-radius: 0; border: 0; transition: right .3s ease; }
  #zone-carte:has(~ #zone-app.zone-app--panneau-ouvert) { right: 36%; }
  /* `#zone-app` devient la « couche » qui porte les panneaux flottants :
     plein cadre au-dessus de la carte, mais sans intercepter les clics/le
     drag de la carte sur les zones qu'aucun panneau ne couvre — seuls ses
     enfants positionnés (liste, panneau) redeviennent cliquables. */
  .zone-app { position: absolute; inset: 0; display: block; pointer-events: none; }
  .zone-liste { position: absolute; top: var(--sp-4); left: var(--sp-4); width: 360px; max-height: calc(100% - var(--sp-8)); overflow: auto; z-index: 3; pointer-events: auto; }
  /* Repliée sur ordinateur (nouveau ce lot) : la carte flottante se
     rétrécit à la largeur de son contenu — le bouton titre/compte/chevron —
     plutôt que de garder les 360px de la liste ouverte, pour libérer la
     carte derrière elle. `overflow: visible` : plus de corps scrollable une
     fois qu'il n'y a plus de corps affiché. */
  .zone-liste--repliee { width: auto; max-width: 260px; overflow: visible; }
  /* Même apparence dès le premier rendu, avant qu'Alpine ne pose la classe
     (voir `html[data-zone-liste]` plus haut) ; règle séparée à dessein, les
     tests lisent chaque bloc par son sélecteur exact. */
  html[data-zone-liste="repliee"] .zone-liste:not(.zone-liste--repliee) { width: auto; max-width: 260px; overflow: visible; }
  /* À droite de la liste, jamais dessus ni sous le panneau d'indicateurs. */
  .zone-sature { position: absolute; top: var(--sp-4); left: calc(360px + var(--sp-4) * 2); width: min(420px, 30%); z-index: 4; pointer-events: auto; }
  .zone-panneau { position: absolute; top: 0; right: 0; bottom: 0; width: 36%; overflow: auto; border-radius: 0; border-width: 0 0 0 1px; z-index: 3; pointer-events: auto; }
  /* Repliée (ce lot, retour fondateur) : redevient une carte flottante au
     lieu du panneau plein-hauteur — même traitement que
     `.zone-liste--repliee` ci-dessus — épinglée en haut à droite pour ne
     jamais recouvrir la liste des zones, en haut à gauche. La carte
     retrouve toute sa largeur pendant ce temps : `_app.html` ne pose
     `zone-app--panneau-ouvert` (qui rétrécit `#zone-carte` ci-dessus) que
     quand le panneau est ouvert. */
  .zone-panneau--replie { top: var(--sp-4); right: var(--sp-4); bottom: auto; left: auto; width: auto; max-width: 360px; overflow: visible; border-radius: var(--r-lg); border-width: 1px; }
  /* Sur ordinateur, la barre troque l'habillage téléphone (« N zones ·
     Ajouter ») pour le duo titre « Zones » + compte — c'est cet habillage
     qui fait office d'en-tête du panneau au-delà de 1024px. */
  .zone-liste-barre-texte-telephone { display: none; }
  .zone-liste-barre-entete { display: flex; }
  /* Ouvert par `x-init` au montage : le résumé compact n'a plus rien à dire,
     la ligne complète reprend sa place d'avant (finding A2). */
  .zone-carte-attribution[open] > .zone-carte-attribution-resume { display: none; }
  .zone-carte-attribution-detail { white-space: normal; } /* la ligne complète se replie sur deux lignes plutôt que de déborder de sa boîte (2 088 px mesurés pour 872 en nowrap) */
  /* L'invitation à créer un compte se posait au pied du corps du panneau —
     94 % du défilement sur ordinateur (680 px visibles pour 1 322 px de
     contenu) : personne ne la voyait sans aller la chercher (fix final,
     finding F3). Elle reste à sa place dans le flux, mais colle au bas du
     panneau, qui est ici le conteneur de défilement (`overflow: auto`
     ci-dessus). Fond opaque et bordure haute : le contenu passe dessous, il
     ne transparaît pas. Au téléphone elle reste en flux — le conteneur de
     défilement y est la page, un `sticky` s'y collerait au bas de l'écran,
     juste sous la barre des zones. */
  .zone-compte {
    position: sticky; bottom: 0; z-index: 1;
    margin: var(--sp-4) calc(-1 * var(--sp-4)) 0;
    padding: var(--sp-3) var(--sp-4);
    background: var(--white);
  }
  :root:not([data-theme="light"]) .zone-compte { background: var(--ocean-deep); }
  /* La bulle du widget de support (`.td-sup__trigger`) est une pastille de
     254 px, fixée en bas à droite : elle couvrait la dernière ligne du
     panneau (finding A4), réparée alors par une réserve de 5rem en bas du
     corps. Cette réserve n'a plus de sens maintenant que l'invitation est
     collée au bas (finding F3) — elle la décollerait — et une gouttière
     droite de 280 px écraserait le bloc. Comme au téléphone (finding F6),
     c'est donc la bulle qui passe à gauche, au-dessus de la carte, où rien
     ne se dispute ce coin : les commandes de zoom de MapLibre et
     l'attribution sont toutes deux au bord droit de la carte. Portée à
     cette page seule, le widget étant injecté à la racine du <body>.

     À gauche, mais pas contre le bord : la liste des zones occupe les 360 px
     de gauche sur toute la hauteur, la bulle s'y posait dessus. Elle se
     range juste à sa droite, au-dessus de la carte, seul endroit de la
     région qu'aucun panneau ne revendique — la carte y est un fond, et ses
     commandes de zoom comme son attribution sont à son bord droit. */
  body:has(.app-page) .td-sup__trigger { left: calc(360px + var(--sp-4) * 2); right: auto; }
}

/* ── Téléphone et tablette (adapt) ───────────────────────────────────────── */
@media (max-width: 1023px) {
  /* Repliée, la liste des zones devient la barre d'action du pouce (finding
     A5) : « N zones · Ajouter » reste à une touche alors que la carte occupe
     45 vh et que le panneau d'indicateurs défile sous elle. Dépliée, aucune
     classe n'est posée et elle redéfile normalement — on ne colle jamais un
     formulaire entier au bas de l'écran. Amélioration progressive : sans
     JavaScript, `.zone-liste--repliee` n'existe pas, la liste reste en flux.

     `fixed` et non `sticky` : `.zone-liste` est un élément de grille, et le
     rectangle dans lequel un `sticky` peut glisser est sa **zone de grille**,
     c'est-à-dire sa seule rangée — mesuré, la barre défilait hors de l'écran
     comme avant. La rendre collante demanderait de défaire la grille de
     `.zone-app`, donc le `minmax(0, 1fr)` qui empêche le débordement
     horizontal (finding A1) : `fixed` + la hauteur rendue à la scène
     ci-dessous coûtent moins cher. */
  .zone-liste--repliee {
    position: fixed;
    left: var(--sp-4); right: var(--sp-4); bottom: 0;
    z-index: 5;
    border-radius: var(--r-lg) var(--r-lg) 0 0;
    padding-bottom: max(var(--sp-4), env(safe-area-inset-bottom));
  }
  /* La barre sort du flux : la scène lui rend sa hauteur, sinon la fin du
     panneau d'indicateurs se lit sous elle. `:has()` parce que la classe est
     posée par Alpine sur l'aside, jamais sur un ancêtre — même famille de
     sélecteur que `#zone-carte:has(~ #zone-app…)` plus bas. */
  .zone-scene:has(.zone-liste--repliee) { padding-bottom: calc(var(--sp-6) + 96px); }
  /* Premier rendu replié (avant la classe posée par Alpine) : mêmes règles
     que les deux blocs ci-dessus, en règles séparées — les tests mobile
     lisent chaque bloc par son sélecteur exact. */
  html[data-zone-liste="repliee"] .zone-liste:not(.zone-liste--repliee) {
    position: fixed;
    left: var(--sp-4); right: var(--sp-4); bottom: 0;
    z-index: 5;
    border-radius: var(--r-lg) var(--r-lg) 0 0;
    padding-bottom: max(var(--sp-4), env(safe-area-inset-bottom));
  }
  html[data-zone-liste="repliee"] .zone-scene { padding-bottom: calc(var(--sp-6) + 96px); }
  /* La bulle du widget de support se pose au-dessus de cette barre plutôt que
     dessus (finding A4). Portée à cette page seule par `body:has(.app-page)` —
     le widget est injecté à la racine du `<body>`, hors de `.app-page`, et sa
     feuille est partagée par tout le site. */
  /* Et à GAUCHE (fix final, finding F6) : remontée de 96 px, elle se posait
     sur le bord droit du panneau — l'onglet « API », la bascule de repli, la
     troisième tuile. Le bord gauche n'a rien à perdre à cette hauteur : la
     barre des zones est pleine largeur mais 96 px plus bas, et la carte
     s'arrête au-dessus. */
  body:has(.app-page) .td-sup__trigger { bottom: calc(1rem + 96px); left: 1rem; right: auto; }
  /* Le zoom de MapLibre est posé en `bottom-right` (`zone-app.js`) : le
     déclencheur d'attribution passe à gauche pour que ses 44 px de cible ne
     se disputent pas le coin avec les boutons + / −, et s'arrête avant eux. */
  .zone-carte-attribution { left: var(--sp-2); right: auto; max-width: calc(100% - var(--sp-2) - 60px); }
  .zone-carte-attribution-resume { justify-content: flex-start; }
}

/* ── Gouttières des sections sous la région application (finding A3) ──────
   `.demo-page` (demo.css), utilisée par les autres démos, porte
   `var(--sp-4)` de gouttière ; `.app-page` n'a qu'un `padding-top`, et
   `.demo-container` n'en a aucune — « Comprendre la réponse » et la FAQ
   touchaient le bord de l'écran. La région application garde son plein
   cadre : la carte y est un fond, pas un bloc de texte. */
.app-page .demo-container { padding: 0 var(--sp-4) var(--sp-12); }
/* Longueur de ligne de lecture : un paragraphe de 267 caractères sur une
   seule colonne pleine largeur ne se lit pas. */
.app-page .demo-explain-body { max-width: 72ch; }

/* Les deux blocs de code de l'onglet API défilent dans leur boîte plutôt que
   d'élargir la page — `scrollWidth` mesuré à 999 px pour 375 px de viewport
   avant ce lot (finding A1). */
.zone-onglet-corps pre { max-width: 100%; overflow-x: auto; }

/* ── Chargement, erreur de fond de carte, squelette (harden, H1/H2) ──────── */
/* Placeholder de chargement : même gabarit que les autres replis, un dégradé
   calme plutôt qu'un rotateur. Il ne vit que tant que `#zone-carte` porte
   `zone-carte--chargement` — posée par le serveur, retirée par `zone-app.js`
   au premier `style.load` (ou par le minuteur de 20 s, qui montre alors le
   bandeau d'erreur ci-dessous). */
.zone-carte-chargement { background: linear-gradient(120deg, var(--ice-blue) 0%, var(--cloud) 50%, var(--ice-blue) 100%); background-size: 200% 100%; animation: zone-chargement 1.8s ease-in-out infinite; }
:root:not([data-theme="light"]) .zone-carte-chargement { background: linear-gradient(120deg, var(--ocean-blue) 0%, var(--ocean-deep) 50%, var(--ocean-blue) 100%); background-size: 200% 100%; }
.zone-carte:not(.zone-carte--chargement) .zone-carte-chargement { display: none; }
@keyframes zone-chargement { 0% { background-position: 200% 0; } 100% { background-position: -200% 0; } }

/* Bandeau d'échec du fond de carte : discret, en haut, il ne recouvre pas les
   contours — la carte peut très bien dessiner les zones sans ses tuiles. */
.zone-carte-avis { position: absolute; top: var(--sp-2); left: var(--sp-2); right: var(--sp-2); z-index: 3; margin: 0; padding: var(--sp-2) var(--sp-3); font-size: .8rem; color: var(--text-muted); background: var(--white); border: 1px solid var(--mist); border-radius: var(--r-md); box-shadow: var(--shadow-sm); }
:root:not([data-theme="light"]) .zone-carte-avis { background: var(--ocean-deep); border-color: var(--glass-border); }
.zone-carte-avis[hidden] { display: none; }

/* Squelette du panneau pendant un échange HTMX : `hx-indicator` (posé sur
   `#zone-app`, donc hérité par tous les déclencheurs) marque ce corps de la
   classe `htmx-request`. Opacité réduite + balayage sur les tuiles, jamais de
   rotateur — même langage que `.htmx-request .demo-result` des autres démos. */
.zone-panneau-corps.htmx-request { opacity: .55; }
.zone-panneau-corps.htmx-request .zone-tuile { position: relative; overflow: hidden; }
.zone-panneau-corps.htmx-request .zone-tuile::after { content: ""; position: absolute; inset: 0; background: linear-gradient(90deg, transparent 0%, rgba(45, 123, 229, .16) 50%, transparent 100%); background-size: 200% 100%; animation: zone-chargement 1.2s ease-in-out infinite; pointer-events: none; }

/* Note « Zone retirée … Annuler » (H4) : au-dessus du formulaire d'ajout,
   effacée par le geste suivant. La croix garde la cible tactile de 44 px des
   autres actions de la liste. */
.zone-liste-annuler { display: flex; align-items: center; gap: var(--sp-2); margin: 0 0 var(--sp-3); padding: var(--sp-2) var(--sp-3); font-size: .8rem; color: var(--text-muted); background: var(--cloud); border-radius: var(--r-md); }
:root:not([data-theme="light"]) .zone-liste-annuler { background: var(--glass-bg); }
/* « Annuler » est l'action de la note : cible de 44 px comme le reste des
   commandes de la liste, et anneau de focus explicite. */
.zone-liste-annuler a { display: inline-flex; align-items: center; min-height: 44px; color: var(--accent-text); font-weight: 600; }
.zone-liste-annuler a:focus-visible { outline: 2px solid var(--trust-blue); outline-offset: 2px; border-radius: var(--r-sm); }
.zone-liste-annuler-fermer { margin-left: auto; display: inline-flex; align-items: center; justify-content: center; min-width: 44px; min-height: 44px; margin-top: calc(-1 * var(--sp-2)); margin-bottom: calc(-1 * var(--sp-2)); text-decoration: none; font-size: 1.1rem; }

/* Bandeau d'erreur posé au-dessus des chiffres survivants (H3) : même boîte
   que le bandeau de saturation, dont il reprend la classe. */
.zone-erreur-bandeau { margin-bottom: var(--sp-3); }

@media (prefers-reduced-motion: reduce) {
  .zone-carte-chargement,
  .zone-panneau-corps.htmx-request .zone-tuile::after { animation-duration: 3.2s; }
}

/* ── Gloses au point de lecture (clarify, finding C5) ─────────────────────
   Une ligne sous le titre d'onglet ou sous la tuile qu'elle qualifie : CSP,
   indice d'affinité, exclusif, potentiel théorique, IRIS. Le bloc
   « Comprendre » les explique aussi, mais il est tout en bas de la page —
   personne ne descend l'y chercher au moment où le sigle se lit. Jeton de
   texte sémantique, jamais `--slate-500` (CLAUDE.md « Accessibility »). */
.zone-glose { margin: var(--sp-1) 0 var(--sp-3); font-size: .8rem; line-height: 1.45; color: var(--text-muted); }

/* ── « Copier le lien » (clarify, finding C3) ─────────────────────────────
   Rendu `hidden` par le serveur, révélé par `zone-app.js` : sans
   presse-papiers il n'y a pas de bouton, c'est le <noscript> du gabarit qui
   parle. Cible tactile de 44 px comme partout dans l'application. */
.zone-copier { display: flex; align-items: center; flex-wrap: wrap; gap: var(--sp-2); margin-bottom: var(--sp-2); }
.zone-copier[hidden] { display: none; }
.zone-copier-bouton {
  display: inline-flex; align-items: center;
  min-height: 44px; padding: 0 var(--sp-2);
  background: none; border: 0; border-radius: var(--r-sm);
  font: inherit; font-size: .85rem; font-weight: 600;
  color: var(--accent-text); text-decoration: underline; cursor: pointer;
}
.zone-copier-bouton:focus-visible { outline: 2px solid var(--trust-blue); outline-offset: 2px; }
.zone-copier-etat { font-size: .8rem; color: var(--success-text); }

/* ── Invitation à créer un compte, au pied du panneau (clarify, C1) ───────
   Une seule instance par page : ce bloc vit dans le corps du panneau, donc
   il disparaît avec lui quand il est replié. Jamais flottant, jamais une
   modale. Le bouton passe en primaire sur l'onglet API seulement, où
   l'intention du visiteur est déjà technique. */
.zone-compte { margin-top: var(--sp-4); padding-top: var(--sp-3); border-top: 1px solid var(--mist); display: flex; flex-wrap: wrap; align-items: center; gap: var(--sp-2); }
:root:not([data-theme="light"]) .zone-compte { border-top-color: var(--glass-border); }
.zone-compte-texte { flex: 1 1 100%; margin: 0; font-size: .85rem; color: var(--text-muted); }
.zone-compte-plans { display: inline-flex; align-items: center; min-height: 44px; font-size: .8rem; color: var(--text-muted); }
/* Panneau replié : une ligne, pas un bloc (finding F3). Même cible tactile
   que partout, le lien seul porte l'accent. */
.zone-compte-compact { margin: var(--sp-2) 0 0; font-size: .85rem; }
.zone-compte-compact a { display: inline-flex; align-items: center; min-height: 44px; color: var(--accent-text); }

/* ── Infobulles des commandes de zone (clarify, finding C6) ───────────────
   L'œil et la croix n'avaient qu'un `aria-label` : un premier visiteur voyant
   la liste ne savait pas ce qu'ils font avant de cliquer. Même mécanisme CSS
   que `.zone-liste-icone` (`data-info` en `::after`), ancré à droite — ces
   deux commandes sont contre le bord du panneau. */
.zone-liste-masquer, .zone-liste-retirer { position: relative; }
.zone-liste-masquer:hover::after, .zone-liste-masquer:focus-visible::after,
.zone-liste-retirer:hover::after, .zone-liste-retirer:focus-visible::after {
  content: attr(data-info);
  position: absolute; top: 100%; right: 0; margin-top: calc(-1 * var(--sp-2));
  padding: var(--sp-1) var(--sp-2);
  background: var(--charcoal); color: var(--white);
  font-size: .75rem; font-weight: 400; white-space: nowrap; border-radius: var(--r-sm);
  box-shadow: var(--shadow-md);
  z-index: 5; pointer-events: none;
}
