/* Carte des chantiers — /search/?view=map
   Design system : tokens --vefa-* (tokens.css chargé globalement par base.html).
   Composants : pins par phase, clusters brandés, popup card, légende,
   « Rechercher dans cette zone », compteur, contrôles nav, POI. */

/* =====================================================
   Container + états
   ===================================================== */
#search-programs-map {
    position: relative;
    background: #eef1f4;
}

#search-programs-map.is-loading::after {
    content: '';
    position: absolute;
    inset: 0;
    z-index: 800;
    background: rgba(255, 255, 255, 0.45);
    pointer-events: none;
}

/* =====================================================
   Marqueurs (pin teardrop coloré par phase)
   ===================================================== */
.map-pin-el { cursor: pointer; }

/* Tokens raccordés au set commun tokens.css — même valeur, source unique */
.map-pin {
    --pin-color: var(--vefa-primary);
    width: 30px;
    height: 30px;
    background: var(--pin-color);
    border: 2px solid #fff;
    border-radius: 50% 50% 50% 0;
    transform: rotate(-45deg);
    box-shadow: 0 3px 8px rgba(15, 23, 42, 0.35);
    display: flex;
    align-items: center;
    justify-content: center;
    transition: transform 0.15s ease, box-shadow 0.15s ease;
}

.map-pin i {
    transform: rotate(45deg);
    color: #fff;
    font-size: 12px;
    line-height: 1;
}

.map-pin-el:hover .map-pin {
    transform: rotate(-45deg) scale(1.15);
    box-shadow: 0 5px 14px rgba(15, 23, 42, 0.45);
}

/* Anti-collision des pilules (Lot 3b) : le pin survolé passe au-dessus de
   ses voisins pour que sa pilule masquée (.map-pin__label--collided,
   cf. plus bas) réapparaisse lisible et entière. */
.map-pin-el:hover {
    z-index: 5;
}

/* Halo « communauté active » (followers ou questions > 0) */
.map-pin--active::after {
    content: '';
    position: absolute;
    inset: -7px;
    border-radius: 50% 50% 50% 0;
    border: 2px solid var(--pin-color);
    opacity: 0.55;
    animation: map-pin-pulse 2.2s ease-out infinite;
    pointer-events: none;
}

@keyframes map-pin-pulse {
    0% { transform: scale(0.85); opacity: 0.55; }
    70% { transform: scale(1.18); opacity: 0; }
    100% { transform: scale(1.18); opacity: 0; }
}

@media (prefers-reduced-motion: reduce) {
    .map-pin--active::after { animation: none; opacity: 0.4; }
}

/* =====================================================
   Clusters donut — composition par phase de chantier
   ⚠️ .map-donut est l'ÉLÉMENT RACINE du marqueur MapLibre : le moteur le
   positionne via un transform inline à chaque frame. Ne JAMAIS poser de
   transform/transition/position sur la racine (les marqueurs « glisseraient »
   derrière la carte au zoom) — les effets visuels vont sur le <svg> enfant.
   ===================================================== */
.map-donut { cursor: pointer; }

.map-donut svg {
    display: block;
    filter: drop-shadow(0 3px 8px rgba(15, 23, 42, 0.28));
    transition: transform 0.15s ease;
}

.map-donut:hover svg { transform: scale(1.08); }

.map-donut text {
    font-weight: 700;
    font-size: 12px;
    fill: var(--vefa-text, #1f2d3d);
    font-family: inherit;
}

/* Pilule nominative au zoom quartier (data-zoomband posé par le JS).
   ⚠️ .map-pin-el est la racine du marqueur : PAS de position/transform ici —
   MapLibre lui pose position:absolute (classe .maplibregl-marker) + transform
   inline, et c'est ce position:absolute qui ancre le label ci-dessous. */
.map-pin__label {
    display: none;
    position: absolute;
    left: 34px;
    top: 1px;
    background: rgba(255, 255, 255, 0.96);
    color: var(--vefa-text, #1f2d3d);
    font-size: 0.74rem;
    font-weight: 600;
    padding: 4px 10px;
    border-radius: 999px;
    white-space: nowrap;
    max-width: 180px;
    overflow: hidden;
    text-overflow: ellipsis;
    box-shadow: 0 2px 8px rgba(15, 23, 42, 0.22);
    pointer-events: none;
}

[data-zoomband="near"] .map-pin__label { display: block; }

/* Pilule en collision avec une voisine déjà gardée (JS resolveLabelCollisions,
   programs_map.js) : visibility, pas display, pour ne pas décaler le pin.
   Le survol la fait réapparaître au-dessus des autres (cf. .map-pin-el:hover
   plus haut pour le z-index). */
.map-pin__label--collided { visibility: hidden; }

.map-pin-el:hover .map-pin__label--collided { visibility: visible; }

/* =====================================================
   Permis déposés — pins distincts (contour pointillé, teinte à part de la
   palette des phases) : ce sont des PROJETS, pas des chantiers.
   ⚠️ .map-permit-el est la RACINE du marqueur MapLibre (transform inline à
   chaque frame) : jamais de transform/transition/position ici — l'effet va
   sur l'enfant .map-permit-pin (même règle que .map-pin / .poi-badge).
   ===================================================== */
.map-permit-el { cursor: pointer; }

.map-permit-pin {
    width: 34px;
    height: 34px;
    border-radius: 50%;
    /* Fond OPAQUE : un fond translucide disparaissait sur l'imagerie
       satellite (feuillages/toits sombres) — le disque blanc + halo
       reste lisible sur plan ET satellite. */
    background: #fff;
    border: 2px dashed #6d28d9;
    color: #6d28d9;
    display: flex;
    align-items: center;
    justify-content: center;
    font-size: 15px;
    box-shadow: 0 0 0 3px rgba(255, 255, 255, 0.55), 0 4px 10px rgba(15, 23, 42, 0.4);
    transition: transform 0.15s ease;
}

.map-permit-el:hover .map-permit-pin {
    transform: scale(1.15);
    background: #f3eeff;
}

.map-legend__poi--permits.is-active {
    background: #6d28d9;
    border-color: #6d28d9;
    color: #fff;
}

.map-popup__chip--permit { background: #6d28d9; }

.map-popup__media--permit {
    color: #6d28d9;
    background: linear-gradient(135deg, #ede9fe, #ddd6fe);
}

/* =====================================================
   Popup card
   ===================================================== */
.map-popup-anchor .maplibregl-popup-content {
    border-radius: 14px;
    padding: 0;
    overflow: hidden;
    box-shadow: 0 12px 32px rgba(15, 23, 42, 0.18);
    line-height: 1.4;
}

.map-popup-anchor .maplibregl-popup-close-button {
    z-index: 5;
    font-size: 18px;
    width: 26px;
    height: 26px;
    border-radius: 50%;
    background: rgba(255, 255, 255, 0.85);
    color: var(--vefa-text, #1f2d3d);
    top: 6px;
    right: 6px;
}

.map-popup { width: 280px; max-width: 100%; }

.map-popup__media {
    position: relative;
    height: 130px;
    background: linear-gradient(135deg, #dfe7ef, #c8d4e0);
    overflow: hidden;
}

.map-popup__media img {
    width: 100%;
    height: 100%;
    object-fit: cover;
    display: block;
}

.map-popup__media--empty {
    display: flex;
    align-items: center;
    justify-content: center;
    color: #8aa0b5;
    font-size: 2rem;
}

.map-popup__chips {
    position: absolute;
    top: 8px;
    left: 8px;
    right: 8px;
    display: flex;
    flex-wrap: wrap;
    gap: 4px;
}

.map-popup__chip {
    --chip-color: var(--vefa-primary);
    background: var(--chip-color);
    color: #fff;
    font-size: 0.68rem;
    font-weight: 600;
    padding: 3px 8px;
    border-radius: 999px;
    box-shadow: 0 1px 4px rgba(15, 23, 42, 0.3);
    white-space: nowrap;
}

.map-popup__chip--new { background: #198754; }
.map-popup__chip--soon { background: #b57e00; }

.map-popup__body { padding: 12px 14px 14px; }

.map-popup__title {
    font-size: 1rem;
    font-weight: 700;
    color: var(--vefa-text, #1f2d3d);
    margin: 0 0 6px;
}

.map-popup__meta {
    font-size: 0.8rem;
    color: #64748b;
    margin-bottom: 3px;
}

.map-popup__meta i { width: 14px; text-align: center; margin-right: 2px; }
.map-popup__sep { opacity: 0.5; }

.map-popup__community {
    display: flex;
    gap: 12px;
    font-size: 0.8rem;
    color: #475569;
    background: #f6f8fa;
    border-radius: 8px;
    padding: 6px 10px;
    margin: 8px 0 10px;
}

.map-popup__community i { color: var(--vefa-accent, #ff4000); margin-right: 3px; }

.map-popup__actions { display: flex; gap: 6px; }

.map-popup__btn {
    flex: 1;
    display: inline-flex;
    align-items: center;
    justify-content: center;
    gap: 5px;
    border-radius: 8px;
    font-size: 0.82rem;
    font-weight: 600;
    padding: 7px 10px;
    border: 0;
    cursor: pointer;
    text-decoration: none;
    transition: transform 0.15s ease, box-shadow 0.15s ease, background 0.15s ease;
    white-space: nowrap;
}

.map-popup__btn--primary {
    background: var(--vefa-accent, #ff4000);
    color: #fff !important;
}

.map-popup__btn--primary:hover {
    background: #e63a00;
    color: #fff;
    transform: translateY(-1px);
    box-shadow: 0 4px 10px rgba(255, 64, 0, 0.3);
}

.map-popup__btn--ghost {
    background: #fff;
    color: var(--vefa-text, #1f2d3d) !important;
    border: 1px solid #d7dee6;
}

.map-popup__btn--ghost:hover {
    border-color: var(--vefa-accent, #ff4000);
    color: var(--vefa-accent, #ff4000) !important;
}

.map-popup__btn--ghost.is-following {
    background: #198754;
    border-color: #198754;
    color: #fff !important;
}

/* =====================================================
   Légende (contrôle Leaflet bas-gauche)
   ===================================================== */
.map-legend {
    background: rgba(255, 255, 255, 0.96);
    border-radius: 12px;
    box-shadow: 0 4px 16px rgba(15, 23, 42, 0.16);
    overflow: hidden;
    max-width: 220px;
    font-family: inherit;
}

.map-legend__head {
    display: flex;
    align-items: center;
    gap: 6px;
    width: 100%;
    background: transparent;
    border: 0;
    padding: 8px 12px;
    font-size: 0.8rem;
    font-weight: 700;
    color: var(--vefa-text, #1f2d3d);
    cursor: pointer;
}

.map-legend__caret { margin-left: auto; font-size: 0.7rem; opacity: 0.5; transition: transform 0.2s ease; }
.map-legend__head[aria-expanded="false"] .map-legend__caret { transform: rotate(-90deg); }

.map-legend__body {
    padding: 0 10px 10px;
    /* Plafonné + scroll interne : dépliée, la légende ne doit jamais
       envahir la carte ni recouvrir d'autres contrôles. */
    max-height: 300px;
    overflow-y: auto;
    overscroll-behavior: contain;
}
.map-legend__body.is-collapsed { display: none; }

.map-legend__label {
    font-size: 0.68rem;
    text-transform: uppercase;
    letter-spacing: 0.04em;
    color: #94a3b8;
    margin: 8px 0 4px;
    font-weight: 700;
}

.map-legend__label small {
    text-transform: none;
    letter-spacing: 0;
    font-weight: 400;
}

.map-legend__phase {
    display: flex;
    align-items: center;
    gap: 7px;
    width: 100%;
    background: transparent;
    border: 1px solid transparent;
    border-radius: 7px;
    padding: 4px 6px;
    font-size: 0.78rem;
    color: var(--vefa-text, #1f2d3d);
    cursor: pointer;
    text-align: left;
    transition: background 0.15s ease, opacity 0.15s ease;
}

.map-legend__phase:hover { background: #f1f5f9; }
.map-legend__phase.is-inactive { opacity: 0.35; }
.map-legend__phase.is-inactive .map-legend__dot { background: #cbd5e1 !important; }

.map-legend__dot {
    display: inline-block;
    width: 12px;
    height: 12px;
    border-radius: 50%;
    border: 2px solid #fff;
    box-shadow: 0 0 0 1px rgba(15, 23, 42, 0.15);
    flex-shrink: 0;
}

.map-legend__row {
    display: flex;
    align-items: center;
    gap: 7px;
    font-size: 0.75rem;
    color: #475569;
    padding: 3px 6px;
}

.map-legend__glyph {
    width: 16px;
    text-align: center;
    color: #64748b;
    font-size: 0.7rem;
    flex-shrink: 0;
}

.map-legend__glyph--pulse {
    width: 10px;
    height: 10px;
    margin: 0 3px;
    border-radius: 50%;
    border: 2px solid var(--vefa-accent, #ff4000);
    opacity: 0.6;
}

/* Chips POI (« Autour des chantiers ») */
.map-legend__pois {
    display: flex;
    flex-wrap: wrap;
    gap: 5px;
    padding: 2px 4px 4px;
}

.map-legend__poi {
    display: inline-flex;
    align-items: center;
    gap: 5px;
    background: #f1f5f9;
    border: 1px solid #e2e8f0;
    border-radius: 999px;
    padding: 4px 10px;
    font-size: 0.72rem;
    font-weight: 600;
    color: #475569;
    cursor: pointer;
    transition: background 0.15s ease, color 0.15s ease, border-color 0.15s ease;
}

.map-legend__poi:hover { border-color: var(--vefa-accent, #ff4000); }

.map-legend__poi.is-active {
    background: var(--vefa-accent, #ff4000);
    border-color: var(--vefa-accent, #ff4000);
    color: #fff;
}

/* =====================================================
   « Rechercher dans cette zone » + toast + compteur
   ⚠️ top décalé à 68px (au lieu de 12px) : dégage la barre de recherche
   flottante .map-float-search, elle aussi centrée en haut de la carte
   (cf. section « Recherche flottante » plus bas).
   ===================================================== */
.map-zone-search {
    position: absolute;
    top: 68px;
    left: 50%;
    transform: translateX(-50%);
    z-index: 900;
    display: inline-flex;
    align-items: center;
    gap: 6px;
    background: #fff;
    color: var(--vefa-text, #1f2d3d);
    border: 0;
    border-radius: 999px;
    padding: 8px 16px;
    font-size: 0.85rem;
    font-weight: 600;
    box-shadow: 0 4px 16px rgba(15, 23, 42, 0.22);
    cursor: pointer;
    transition: transform 0.15s ease, box-shadow 0.15s ease;
}

.map-zone-search:hover {
    transform: translateX(-50%) translateY(-1px);
    box-shadow: 0 6px 20px rgba(15, 23, 42, 0.28);
    color: var(--vefa-accent, #ff4000);
}

.map-zone-search i { color: var(--vefa-accent, #ff4000); }

.map-toast {
    position: absolute;
    top: 68px; /* dégage la barre de recherche flottante, cf. .map-float-search */
    left: 50%;
    transform: translateX(-50%);
    z-index: 950;
    background: rgba(31, 45, 61, 0.92);
    color: #fff;
    border-radius: 999px;
    padding: 7px 14px;
    font-size: 0.8rem;
    white-space: nowrap;
    box-shadow: 0 4px 14px rgba(15, 23, 42, 0.3);
    animation: map-toast-in 0.25s ease;
    pointer-events: none;
}

@keyframes map-toast-in {
    from { opacity: 0; transform: translateX(-50%) translateY(-6px); }
    to { opacity: 1; transform: translateX(-50%) translateY(0); }
}

/* Indicateur « Centrage sur {nom}… » (Lot 2, arrivée depuis la fiche via
   ?program=<slug>&from=fiche) : couvre le délai entre la caméra initiale déjà
   pointée sur le programme (cf. initialCenter/initialZoom dans
   programs_map.js) et la fin du vol de focusOnProgram(). Même top 68px que
   .map-zone-search/.map-toast juste au-dessus (dégage la recherche flottante) ;
   fond translucide + flou + radius + ombre repris tels quels de .map-ambiance
   plus bas pour rester cohérent avec les autres chips de la carte.
   pointer-events: none : purement informatif, jamais un obstacle au clic. */
.map-focus-loading {
    position: absolute;
    top: 68px;
    left: 50%;
    transform: translateX(-50%);
    z-index: 950;
    display: inline-flex;
    align-items: center;
    gap: 8px;
    background: rgba(255, 255, 255, 0.94);
    backdrop-filter: blur(4px);
    color: var(--vefa-text, #1f2d3d);
    border-radius: 16px;
    padding: 8px 14px;
    font-size: 0.85rem;
    font-weight: 600;
    white-space: nowrap;
    box-shadow: 0 4px 16px rgba(15, 23, 42, 0.18);
    pointer-events: none;
    transition: opacity 0.2s ease;
}

.map-focus-loading i { color: var(--vefa-accent, #ff4000); }

/* Posée par hideFocusLoading() (programs_map.js) : fondu avant retrait du
   nœud (250 ms plus tard), jamais de disparition sèche. */
.map-focus-loading.is-hidden { opacity: 0; }

/* Variante interactive (hint 3D, sous-lot politique 2D/3D) : contrairement
   au toast informatif ci-dessus, celle-ci porte des boutons cliquables donc
   pointer-events doit rester actif. Même position/racine (pas de Marker). */
.map-toast--3d-hint {
    pointer-events: auto;
    display: flex;
    align-items: center;
    gap: 8px;
    padding: 6px 8px 6px 14px;
}

.map-toast__action {
    background: transparent;
    border: 0;
    color: #fff;
    font-size: 0.8rem;
    font-weight: 600;
    white-space: nowrap;
    cursor: pointer;
    padding: 2px 4px;
}

.map-toast__action i { color: var(--vefa-accent, #ff4000); margin-right: 2px; }

.map-toast__close {
    background: rgba(255, 255, 255, 0.14);
    border: 0;
    color: #fff;
    width: 22px;
    height: 22px;
    border-radius: 50%;
    display: flex;
    align-items: center;
    justify-content: center;
    font-size: 0.7rem;
    cursor: pointer;
    flex-shrink: 0;
}

.map-toast__close:hover { background: rgba(255, 255, 255, 0.26); }

.map-toast--3d-hint:focus-within,
.map-toast__action:focus-visible,
.map-toast__close:focus-visible {
    outline: 2px solid #fff;
    outline-offset: 2px;
}

/* Variante « zéro résultat dans la zone » (Lot B2, sortie de secours quand
   la recherche de zone ne ramène rien) : texte + bouton d'action + croix,
   même patron que .map-toast--3d-hint ci-dessus, mais le texte combiné
   (message + libellé du bouton) dépasserait largement un viewport mobile
   étroit en nowrap hérité de .map-toast. flex-wrap + une largeur bornée à
   l'écran laissent le contenu passer sur 2 lignes plutôt que déborder ;
   sur desktop, où la place ne manque jamais, ces règles restent inertes
   (une seule ligne, comme les autres variantes de toast). */
.map-toast--zone-empty {
    flex-wrap: wrap;
    justify-content: center;
    max-width: calc(100vw - 20px);
}

.map-toast--zone-empty .map-toast__text {
    white-space: normal;
}

/* Sortie de secours : ce bouton est la SEULE issue visible quand une
   recherche de zone ne ramène rien (sur mobile la pastille « Zone affichée »
   du compteur est masquee, cf. le bloc Lot A plus bas). Le style plat de
   `.map-toast__action`, suffisant pour une suggestion, ne se lit pas comme
   un bouton : on lui donne ici une vraie affordance (fond plein, cible
   tactile 44px), constatee necessaire en QA visuelle. */
.map-toast--zone-empty .map-toast__action {
    background: var(--vefa-accent, #ff4000);
    color: var(--vefa-text-on-primary, #fff);
    border-radius: 999px;
    padding: 0 16px;
    min-height: 44px;
    font-weight: 700;
}

.map-toast--zone-empty .map-toast__action:hover {
    background: var(--vefa-accent-strong, var(--vefa-accent, #ff4000));
    filter: brightness(0.94);
}

/* Compteur dynamique « N chantiers dans cette vue » — pill flottant
   bas-droite (plein écran : plus de bande sous la carte qui créerait du
   scroll). Zone libre : légende bas-gauche, pilule soleil bas-centre. */
.search-map-counter {
    position: absolute;
    bottom: 14px;
    right: 14px;
    z-index: 860;
    display: inline-flex;
    flex-wrap: wrap;
    align-items: center;
    gap: 8px;
    padding: 7px 13px;
    font-size: 0.8rem;
    color: #475569;
    background: rgba(255, 255, 255, 0.94);
    border-radius: 999px;
    box-shadow: 0 3px 12px rgba(15, 23, 42, 0.16);
    backdrop-filter: blur(4px);
}
.search-map-counter:empty { display: none; }

.search-map-counter i { color: var(--vefa-accent, #ff4000); }
.search-map-counter strong { color: var(--vefa-text, #1f2d3d); }

.map-counter-muted { color: #94a3b8; }
.map-counter-muted i { color: #94a3b8; }

.map-counter-link {
    background: transparent;
    border: 0;
    padding: 0;
    color: var(--vefa-primary, #3498db);
    font-size: 0.8rem;
    cursor: pointer;
    text-decoration: underline;
}

.map-zone-chip {
    display: inline-flex;
    align-items: center;
    gap: 6px;
    background: rgba(255, 64, 0, 0.08);
    color: var(--vefa-accent, #ff4000);
    border: 1px solid rgba(255, 64, 0, 0.25);
    border-radius: 999px;
    padding: 4px 10px;
    font-size: 0.78rem;
    font-weight: 600;
    cursor: pointer;
    transition: background 0.15s ease;
}

.map-zone-chip:hover { background: rgba(255, 64, 0, 0.14); }
.map-zone-chip__x { opacity: 0.7; }
.map-zone-chip i { color: var(--vefa-accent, #ff4000); }

/* =====================================================
   Contrôles (Autour de moi / France / 3D / Satellite) + MapLibre
   ===================================================== */
.map-ctl { overflow: hidden; }

.map-ctl-btn {
    /* !important sur les dimensions : maplibre-gl.css pose déjà
       `.maplibregl-ctrl-group > button { width/height: 29px }` avec une
       spécificité (classe + élément) supérieure à notre simple classe —
       sans !important nos tailles (34px desktop / 44px tactile) seraient
       silencieusement ignorées malgré l'ordre de chargement des feuilles. */
    display: flex !important;
    align-items: center;
    justify-content: center;
    width: 34px !important;
    height: 34px !important;
    background: #fff;
    border: 0;
    border-bottom: 1px solid #e2e8f0;
    color: var(--vefa-text, #1f2d3d);
    font-size: 13px;
    cursor: pointer;
    transition: color 0.15s ease, background 0.15s ease;
}

.map-ctl-btn:last-child { border-bottom: 0; }

.map-ctl-btn:hover {
    background: #f8fafc;
    color: var(--vefa-accent, #ff4000);
}

.map-ctl-btn.is-active {
    background: var(--vefa-accent, #ff4000);
    color: #fff;
}

/* Bouton 3D : libellé texte explicite (pas juste une icône cube) — état
   enfoncé porté par .is-active (fond accent), cf. toggle3d(). */
.map-ctl-btn__label {
    font-weight: 800;
    font-size: 11px;
    letter-spacing: 0.02em;
    line-height: 1;
}

#search-programs-map .maplibregl-ctrl-group {
    border-radius: 10px;
    box-shadow: 0 3px 12px rgba(15, 23, 42, 0.18);
    overflow: hidden;
}

/* Focus clavier visible sur tous les contrôles de la carte (accessibilité) —
   jamais d'outline:none sans remplacement. */
.map-ctl-btn:focus-visible,
.map-legend__phase:focus-visible,
.map-legend__poi:focus-visible,
.map-legend__head:focus-visible,
.map-layers-toggle:focus-visible,
.map-zone-search:focus-visible,
.map-zone-chip:focus-visible,
.map-counter-link:focus-visible,
.map-ambiance__season-btn:focus-visible,
.map-ambiance__sun-slider:focus-visible,
.map-ambiance__preset-btn:focus-visible,
.map-float-search__submit:focus-visible,
#search-programs-map .maplibregl-ctrl-zoom-in:focus-visible,
#search-programs-map .maplibregl-ctrl-zoom-out:focus-visible,
#search-programs-map .maplibregl-ctrl-compass:focus-visible {
    outline: 2px solid var(--vefa-primary, #3498db);
    outline-offset: 2px;
    z-index: 2;
    position: relative;
}

/* Focus de la recherche flottante : l'anneau se dessine sur la PILULE
   (:focus-within, arrondi 999px suivi), jamais sur l'<input> rectangulaire à
   l'intérieur — l'ancien outline interne dessinait un rectangle bleu au
   milieu de la pilule blanche (« barres bleues », retour utilisateur
   2026-08-11). L'indication de focus reste visible (a11y) : liseré primaire
   ajouté à l'ombre portée existante. */
.map-float-search__input:focus-visible { outline: none; }

.map-float-search__input-wrap:focus-within {
    box-shadow: 0 4px 16px rgba(15, 23, 42, 0.22),
                0 0 0 2px var(--vefa-primary, #3498db);
}

/* Cibles tactiles ≥44px (WCAG 2.5.5) — nos boutons (.map-ctl-btn) ET les
   contrôles de navigation MapLibre natifs (zoom in/out/boussole), qui font
   29×29px par défaut. Espacement vertical ≥8px entre boutons : on éclate le
   groupement visuel « pilule unique » au profit de boutons distincts avec
   leur propre ombre/coin arrondi (le regroupement compact n'a de sens qu'au
   doigt fin de la souris). */
@media (pointer: coarse) {
    .map-ctl-btn,
    #search-programs-map .maplibregl-ctrl-zoom-in,
    #search-programs-map .maplibregl-ctrl-zoom-out,
    #search-programs-map .maplibregl-ctrl-compass {
        width: 44px !important;
        height: 44px !important;
        font-size: 16px;
    }

    .map-ctl-btn__label { font-size: 13px; }

    #search-programs-map .maplibregl-ctrl-group {
        overflow: visible;
        box-shadow: none;
        background: transparent;
        border-radius: 0;
    }

    .map-ctl-btn,
    #search-programs-map .maplibregl-ctrl-zoom-in,
    #search-programs-map .maplibregl-ctrl-zoom-out,
    #search-programs-map .maplibregl-ctrl-compass {
        border-radius: 10px;
        box-shadow: 0 2px 8px rgba(15, 23, 42, 0.22);
    }

    .map-ctl-btn:not(:last-child) {
        margin-bottom: 8px;
        border-bottom: 0;
    }

    #search-programs-map .maplibregl-ctrl-zoom-in,
    #search-programs-map .maplibregl-ctrl-zoom-out {
        margin-bottom: 8px;
    }
}

/* Cibles tactiles ≥44px (WCAG 2.5.5), 2e vague (audit externe 2026-08-25) :
   le bloc ci-dessus ne couvre que .map-ctl-btn et les contrôles MapLibre
   natifs. Restaient sous 44px : le submit de la recherche flottante
   (38×38px) et les boutons de la topbar (« Filtres » ~35×31px). Gate
   `pointer: coarse` SEUL insuffisant : un petit écran peut être utilisé
   (ou audité, comme ici) sans pointeur tactile détecté, or WCAG 2.5.5 vise
   la taille de cible quel que soit le pointeur déclaré, d'où le double
   gate ci-dessous, factorisé en une seule media query combinée (liste de
   sélecteurs non dupliquée).
   Bloc VOLONTAIREMENT SÉPARÉ du `@media (pointer: coarse)` ci-dessus : ce
   dernier contient des règles qui n'ont de sens qu'au doigt (éclatement de
   la pilule MapLibre en boutons distincts avec ombre propre, `width`/
   `height` FIXES en 44px), les faire fuiter sous 767px en pointeur fin
   casserait cet éclatement sans raison. min-width/min-height (jamais
   width/height) : ces 3 boutons portent un libellé texte, une taille fixe
   le tronquerait.
   Pas de !important nécessaire : `.map-float-search__submit` a bien un
   `width/height: 38px` explicite dans sa règle de base, mais le modèle de
   boîte CSS calcule la largeur/hauteur RENDUE comme le plus grand des deux
   entre la valeur spécifiée et min-width/min-height (min-width gagne
   toujours sur un width plus petit, indépendamment de l'ordre des règles).
   `.map-topbar__btn`/`.map-topbar__toggle-btn` n'ont eux aucun width/height
   déclaré ailleurs dans ce fichier (grep vérifié) : min-width ne fait ici
   que relever leur plancher, la desktop >=768px en pointeur fin (hors ce
   double gate) reste donc inchangée dans les deux cas. */
@media (pointer: coarse), (max-width: 767px) {
    .map-float-search__submit,
    .map-topbar__btn,
    .map-topbar__toggle-btn {
        min-width: 44px;
        min-height: 44px;
    }
}

.map-me-dot {
    width: 16px;
    height: 16px;
    border-radius: 50%;
    background: var(--vefa-primary, #3498db);
    border: 3px solid #fff;
    box-shadow: 0 0 0 2px rgba(52, 152, 219, 0.35);
}

/* =====================================================
   POI — badges colorés par catégorie
   ⚠️ .poi-badge est un ENFANT du marqueur (wrapper sans style créé par le
   JS) : la racine reste vierge de transform/transition, sinon les badges
   glissent au zoom (MapLibre positionne la racine par transform inline).
   ===================================================== */
.poi-badge {
    position: absolute;
    left: 0;
    top: 0;
    transform: translate(-50%, -50%);
    width: 26px;
    height: 26px;
    display: flex;
    align-items: center;
    justify-content: center;
    border-radius: 50%;
    color: #fff;
    font-size: 11px;
    border: 2px solid rgba(255, 255, 255, 0.95);
    box-shadow: 0 2px 6px rgba(15, 23, 42, 0.28);
    transition: transform 0.12s ease, opacity 0.15s ease;
    /* Atténuation (Lot 3b) : les badges POI ne doivent plus rivaliser
       visuellement avec les pins programme au zoom quartier. Pleine
       opacité au survol pour rester identifiable au clic. */
    opacity: 0.78;
}

.poi-badge:hover { transform: translate(-50%, -50%) scale(1.18); opacity: 1; }

.poi-badge--transport { background: #0d6efd; }
.poi-badge--school { background: #198754; }
.poi-badge--health { background: #dc3545; }
.poi-badge--shop { background: #b57e00; }

.map-poi-loading {
    position: absolute;
    bottom: 12px;
    right: 12px;
    z-index: 900;
    display: inline-flex;
    align-items: center;
    gap: 6px;
    background: rgba(255, 255, 255, 0.95);
    color: #475569;
    border-radius: 999px;
    padding: 5px 12px;
    font-size: 0.75rem;
    font-weight: 600;
    box-shadow: 0 3px 10px rgba(15, 23, 42, 0.18);
    pointer-events: none;
}

.map-poi-loading i { color: var(--vefa-accent, #ff4000); }

/* Empile au-dessus de l'indicateur POI si les deux chargent en même temps. */
.map-permits-loading { bottom: 50px; }

/* Infobulle des POI du diorama 3D. Les pastilles y sont rendues DANS la
   scène three (pas de DOM) — elles n'ont donc pas de `title=` natif comme
   les badges 2D : c'est ce div, positionné par programs_map.js à partir du
   hit-test VefaPhoto3D.poiAt(). Couleurs = palette 3D des familles
   (POI_FAMILY_COLORS), pas celle des badges 2D. */
.map-poi-tip {
    position: absolute;
    z-index: 950;
    pointer-events: none;      /* ne doit jamais voler le survol au canvas */
    max-width: 240px;
    display: flex;
    /* ⚠️ INDISPENSABLE : `.map-poi-tip__lines` porte `flex: 1 0 100%` pour
       passer sous le nom. Sans `wrap`, cette base de 100 % ne crée aucune
       ligne — elle réclame toute la largeur sur la ligne unique. Le `__kind`
       étant en `nowrap`, le nom devient le seul élément compressible et tombe
       à un caractère de large : il s'affiche alors en colonne, une lettre par
       ligne (constaté sur « Hérold - Mairie de Courbevoie » desservi par
       trois lignes de bus). */
    flex-wrap: wrap;
    align-items: baseline;
    gap: 7px;
    background: rgba(255, 253, 248, 0.97);
    color: #2b2722;
    border-radius: 10px;
    padding: 6px 10px;
    font-size: 0.78rem;
    line-height: 1.3;
    box-shadow: 0 4px 14px rgba(15, 23, 42, 0.22);
    border-left: 3px solid var(--map-poi-tip-accent, #7a71d8);
}

.map-poi-tip i {
    color: var(--map-poi-tip-accent, #7a71d8);
    font-size: 0.8em;
    flex: 0 0 auto;
}

.map-poi-tip__name {
    font-weight: 600;
    /* `break-word` et non `anywhere` : les deux coupent un mot trop long, mais
       `anywhere` abaisse aussi la largeur minimale intrinsèque à un caractère,
       ce qui autorise le flex à écraser le nom en colonne de lettres dès que
       la place manque. `break-word` préserve cette largeur minimale — le nom
       garde donc une taille lisible même serré. */
    overflow-wrap: break-word;
    /* Le nom occupe la place restante de la première ligne, à côté de l'icône
       et devant le type. */
    flex: 1 1 auto;
    min-width: 0;
}

.map-poi-tip__kind {
    color: #6b6459;
    white-space: nowrap;
}

/* Lignes desservant un arrêt — pastille à la couleur officielle du réseau,
   servie par notre API depuis la base (jamais par le repli Overpass, qui ne
   connaît pas les lignes). La bulle passe donc en multi-lignes. */
.map-poi-tip__lines {
    display: flex;
    flex-wrap: wrap;
    gap: 3px;
    flex: 1 0 100%;      /* toujours sur sa propre ligne, sous le nom */
    margin-top: 3px;
}

.map-poi-tip__line {
    display: inline-block;
    min-width: 18px;
    padding: 1px 5px;
    border-radius: 4px;
    /* Repli quand OSM ne donne pas de couleur, ou en donne une invalide :
       le JS ne pose `background` qu'après validation stricte du code hexa. */
    background: #5b5650;
    color: #fff;
    font-size: 0.72rem;
    font-weight: 700;
    line-height: 1.45;
    text-align: center;
    white-space: nowrap;
}

.map-poi-tip__line--more {
    background: transparent;
    color: #6b6459;
    font-weight: 600;
}

.map-poi-tip[data-poi-family="transport"] { --map-poi-tip-accent: #7a71d8; }
.map-poi-tip[data-poi-family="school"] { --map-poi-tip-accent: #e79b23; }
.map-poi-tip[data-poi-family="healthcare"] { --map-poi-tip-accent: #20a888; }
.map-poi-tip[data-poi-family="shopping"] { --map-poi-tip-accent: #e08256; }

/* =====================================================
   Pilule « Ensoleillement » — bas-centre, visible seulement 3D actif +
   satellite inactif (les ambiances Neutre/Doré/Nuit ont été retirées
   2026-07 : le soleil réel est le seul mode lumière). Rangée saisons +
   grand curseur d'heure — changer l'heure doit être immédiat.
   ===================================================== */
.map-ambiance {
    position: absolute;
    bottom: 14px;
    left: 50%;
    transform: translateX(-50%);
    z-index: 900;
    display: flex;
    flex-direction: column;
    align-items: stretch;
    gap: 6px;
    background: rgba(255, 255, 255, 0.94);
    backdrop-filter: blur(4px);
    border-radius: 16px;
    padding: 8px 10px;
    box-shadow: 0 4px 16px rgba(15, 23, 42, 0.18);
    transition: opacity 0.2s ease;
}

/* Une popup programme ou permis est ouverte (cf. onProgramPopupOpened dans
   programs_map.js) : elle peut se déployer en moitié basse de l'écran, là où
   vit cette pilule. On l'efface plutôt que de la laisser recouvrir les
   boutons de la popup. Préfixe #search-programs-map obligatoire (convention
   du fichier) pour battre les spécificités des couches MapLibre. */
#search-programs-map.is-popup-open .map-ambiance {
    opacity: 0;
    pointer-events: none;
}

.map-ambiance__seasons {
    display: flex;
    align-items: center;
    justify-content: center;
    gap: 5px;
}

.map-ambiance__season-btn {
    border: 0;
    background: #f1f5f9;
    color: var(--vefa-text, #1f2d3d);
    font-size: 0.74rem;
    font-weight: 600;
    padding: 5px 11px;
    border-radius: 999px;
    cursor: pointer;
    white-space: nowrap;
    transition: background 0.15s ease, color 0.15s ease;
}

.map-ambiance__season-btn:hover { background: #e2e8f0; }

.map-ambiance__season-btn.is-active {
    background: linear-gradient(135deg, #f59e0b, var(--vefa-accent, #ff4000));
    color: #fff;
}

.map-ambiance__suntimes {
    margin-left: 6px;
    font-size: 0.7rem;
    font-weight: 600;
    color: #64748b;
    white-space: nowrap;
    font-variant-numeric: tabular-nums;
}

.map-ambiance__sun {
    display: flex;
    align-items: center;
    gap: 10px;
    padding: 0 2px;
}

.map-ambiance__sun-icon {
    color: #f59e0b;
    font-size: 0.95rem;
    flex: 0 0 auto;
}

/* Grand curseur : c'est LE gest principal du mode — large et facile à
   attraper (retour utilisateur 2026-07). */
.map-ambiance__sun-slider {
    flex: 1 1 auto;
    min-width: 240px;
    height: 22px;
    accent-color: var(--vefa-accent, #ff4000);
    cursor: pointer;
}

.map-ambiance__sun-time {
    flex: 0 0 auto;
    font-size: 0.92rem;
    font-weight: 700;
    color: var(--vefa-text, #1f2d3d);
    min-width: 3em;
    text-align: right;
    font-variant-numeric: tabular-nums;
}

/* Presets tactiles Midi/Soirée/Nuit (Lot C, 2026-08-17) — remplacent le
   curseur continu sur pointeur grossier (bascule dans @media (pointer:
   coarse) plus bas) : mêmes chips visuellement que les saisons ci-dessus,
   pour rester cohérent. Masqués par défaut (souris) — le grand curseur reste
   LE geste principal au pointeur fin, inchangé. */
.map-ambiance__sun-presets {
    display: none;
    align-items: center;
    justify-content: center;
    gap: 5px;
}

.map-ambiance__preset-btn {
    border: 0;
    background: var(--vefa-bg-muted, #f1f5f9);
    color: var(--vefa-text, #1f2d3d);
    font-size: 0.74rem;
    font-weight: 600;
    padding: 5px 11px;
    border-radius: 999px;
    cursor: pointer;
    white-space: nowrap;
    transition: background 0.15s ease, color 0.15s ease;
}

.map-ambiance__preset-btn:hover { background: var(--vefa-border, #e2e8f0); }

.map-ambiance__preset-btn.is-active {
    background: linear-gradient(135deg, var(--vefa-warning, #f59e0b), var(--vefa-accent, #ff4000));
    color: var(--vefa-text-on-primary, #fff);
}

/* Chip « Maintenant » de la rangée desktop du curseur (Lot lisibilité,
   2026-08), même gabarit pilule que .map-ambiance__preset-btn ci-dessus
   (cohérent avec le reste de la rangée .map-ambiance__sun) ; la variante
   mobile (.map-ambiance__now-btn--mobile) est stylée plus bas, alignée sur
   .map-ambiance__hour-btn. Pas d'état is-active : c'est une action, pas un
   réglage à refléter. */
.map-ambiance__sun .map-ambiance__now-btn {
    flex: 0 0 auto;
    border: 0;
    background: var(--vefa-bg-muted, #f1f5f9);
    color: var(--vefa-text, #1f2d3d);
    font-size: 0.74rem;
    font-weight: 600;
    padding: 5px 11px;
    border-radius: 999px;
    cursor: pointer;
    white-space: nowrap;
    transition: background 0.15s ease, color 0.15s ease;
}

.map-ambiance__sun .map-ambiance__now-btn:hover { background: var(--vefa-border, #e2e8f0); }

/* Bascule slider <-> presets (Lot C, 2026-08-17) — le grand curseur continu
   est malaisé à draguer au doigt (retour utilisateur ayant motivé le
   masquage temporaire de la pilule entière en Lot A.2) : au pointeur
   grossier on bascule vers les 3 chips Midi/Soirée/Nuit ci-dessus, seule
   différence avec le pointeur fin (desktop, inchangé). ⚠️ Ce bloc DOIT rester
   déclaré APRÈS les règles de base .map-ambiance__sun / .map-ambiance__sun-
   presets ci-dessus : à spécificité égale (une classe), la cascade CSS
   départage par ORDRE DE SOURCE, indépendamment de la présence d'un @media —
   posé plus haut (à côté de l'autre bloc `@media (pointer: coarse)` du
   fichier), ce override aurait été silencieusement écrasé par la règle de
   base définie plus bas (bug constaté en QA : `getComputedStyle` renvoyait
   `display: flex` sur le slider malgré `pointer: coarse` matché). */
@media (pointer: coarse) {
    .map-ambiance__sun { display: none; }
    .map-ambiance__sun-presets { display: flex; }
}

/* Rangée unique mobile (2026-08-18) — remplace, sous 768px (cf. bloc
   @media (max-width:767px) plus bas), les 2 rangées ci-dessus (saisons +
   soleil/presets) par UN chip saison cyclique + 5 pas horaires : leur
   empilement faisait dépasser la pilule et chevauchait la pilule zone/la
   légende repliée (retour utilisateur PROD). Masquée par défaut ici
   (desktop ET tablette/laptop tactile ≥768px, cf. bloc pointer:coarse
   ci-dessus — INCHANGÉ pour ces gabarits, la rangée unique ne les concerne
   pas). 2026-08-19 — sous 768px, elle n'est plus affichée en permanence :
   seule la pastille FAB (.map-ambiance__fab ci-dessous) est visible au
   repos, la rangée n'apparaît que dépliée (cf. règle
   `.map-ambiance.is-expanded .map-ambiance__mobile-row` dans le bloc
   @media plus bas). */
.map-ambiance__mobile-row {
    display: none;
    align-items: center;
    justify-content: center;
    gap: 4px;
}

/* Pastille FAB (icône soleil) — repliée par défaut sur mobile, remplace
   visuellement les 4 rangées ci-dessus (cf. .map-ambiance__fab dans le
   bloc @media (max-width:767px) plus bas pour la vraie mise en forme :
   cercle 48×48 bas-droite). Masquée ici par défaut (desktop ET
   tablette/laptop tactile ≥768px) — le nœud existe toujours dans le DOM
   (créé une seule fois, cf. programs_map.js) mais ne doit jamais apparaître
   hors mobile. */
.map-ambiance__fab {
    display: none;
}

.map-ambiance__season-cycle,
.map-ambiance__hour-btn,
.map-ambiance__now-btn--mobile {
    flex: 0 0 auto;
    min-height: 44px;
    border: 0;
    border-radius: 10px;
    padding: 4px 6px;
    background: var(--vefa-bg-muted, #f1f5f9);
    color: var(--vefa-text, #1f2d3d);
    font-size: 0.66rem;
    font-weight: 600;
    line-height: 1.15;
    white-space: nowrap;
    cursor: pointer;
    transition: background 0.15s ease, color 0.15s ease;
}

.map-ambiance__season-cycle { background: var(--vefa-bg-soft, #f8fafc); }

.map-ambiance__hour-btn.is-active {
    background: linear-gradient(135deg, var(--vefa-warning, #f59e0b), var(--vefa-accent, #ff4000));
    color: var(--vefa-text-on-primary, #fff);
}

/* =====================================================
   Ergonomie plein écran de la vue carte (2026-07) — la carte domine l'écran :
   hero + sidebar filtres masqués côté template (program_search_results.html,
   `{% if view_mode != 'map' %}`), le conteneur monte en hauteur et une barre
   de recherche compacte flotte au-dessus du canvas. Ce fichier n'est chargé
   QUE pour la vue carte (extra_css conditionnel) : pas besoin de scoper ces
   règles sous une classe supplémentaire.
   ===================================================== */
/* Plein écran : la page carte annule le padding container-fluid/py-4 et la
   carte remplit toute la hauteur sous l'en-tête de site (94px desktop). Toute la
   chrome (recherche, Filtres, toggle, chips, compteur) flotte SUR le canvas
   — plus aucun empilement vertical qui rognerait la carte. */
.search-page--map {
    padding: 0 !important;
    max-width: none;
}

.search-map-container {
    position: relative; /* ancre les overlays flottants en absolute */
    /* Fond neutre proche du fond de carte positron : les tuiles pas encore
       peintes se fondent au lieu d'afficher un gris franc pendant un
       changement de ville (couplé à fadeDuration:0 côté JS). */
    background: #e8eaed;
    /* Plus d'écart posé ici : la respiration entre la carte de l'en-tête et le
       canvas vient désormais du `margin-bottom` de `.vc-header`, qui la garantit
       sur TOUTES les pages (delta 4 de site_header.css). En garder un ici en
       plus ferait doublon — et sortirait du compte réservé ci-dessous. */
}

#search-programs-map {
    /* 112px = empreinte VERTICALE de l'en-tête de site : 76px de hauteur
       + 18px de marge haute + 18px de marge basse (la carte flottante ne
       touche ni le bord de la fenêtre ni le canvas). L'ancienne navbar en
       occupait 70 ; garder cette valeur laissait la carte déborder de 24px
       sous la fenêtre, mesuré.
       ⚠️ Couplage silencieux avec `site_header.css` : changer la hauteur ou une
       marge de l'en-tête sans toucher ici ne casse rien de visible, la carte
       cesse juste d'être en plein écran. Verrouillé par
       `test_la_vue_carte_reserve_lempreinte_reelle_de_len_tete`. */
    height: calc(100dvh - 112px);
    min-height: 480px;
    background: #e8eaed;
}

/* --- Plein écran (API Fullscreen) ---------------------------------------
   La hauteur normale réserve 94px pour l'en-tête de site ; en plein écran celui-ci
   n'existe plus, la carte doit donc prendre toute la place. `min-height` est
   remis à 0 : 480px déborderaient sur un écran bas (portable 13" en paysage,
   écran externe 768px), et le débordement ferait apparaître une barre de
   défilement DANS le plein écran.

   ⚠️ Les deux sélecteurs sont VOLONTAIREMENT dupliqués au lieu d'être
   groupés : un sélecteur inconnu du navigateur invalide TOUTE la règle qui le
   contient, donc `:fullscreen, :-webkit-full-screen { … }` ne s'appliquerait
   nulle part. C'est le piège classique de cette pseudo-classe. */
.search-map-container:fullscreen {
    width: 100%;
    height: 100%;
    margin: 0; /* la respiration carte/en-tête n'a pas de sens sans en-tête */
    background: #e8eaed;
}

.search-map-container:fullscreen #search-programs-map {
    height: 100%;
    min-height: 0;
}

.search-map-container:-webkit-full-screen {
    width: 100%;
    height: 100%;
    margin: 0;
    background: #e8eaed;
}

.search-map-container:-webkit-full-screen #search-programs-map {
    height: 100%;
    min-height: 0;
}

/* Contrôles MapLibre haut-DROITE (geoloc/3D/satellite) décalés sous la barre
   d'outils flottante. Le haut-gauche (zoom) garde sa place — la barre est à
   droite uniquement. */
.maplibregl-ctrl-top-right { top: 58px; }

/* --------- Barre d'outils flottante (haut-droite) --------- */
.map-topbar {
    position: absolute;
    top: 12px;
    right: 12px;
    z-index: 880;
    display: flex;
    align-items: center;
    gap: 8px;
}

.map-topbar__btn,
.map-topbar__toggle {
    background: #fff;
    border-radius: 999px;
    box-shadow: 0 4px 16px rgba(15, 23, 42, 0.2);
}

.map-topbar__btn {
    display: inline-flex;
    align-items: center;
    gap: 6px;
    border: 0;
    padding: 9px 14px;
    font-size: 0.82rem;
    font-weight: 600;
    color: var(--vefa-text, #1f2d3d);
    cursor: pointer;
}

.map-topbar__btn i { color: var(--vefa-accent, #ff4000); }
.map-topbar__btn:hover { background: #f1f5f9; }

.map-topbar__toggle {
    display: inline-flex;
    padding: 3px;
    gap: 2px;
}

.map-topbar__toggle-btn {
    display: inline-flex;
    align-items: center;
    gap: 5px;
    border-radius: 999px;
    padding: 6px 12px;
    font-size: 0.82rem;
    font-weight: 600;
    color: var(--vefa-text, #1f2d3d);
    text-decoration: none;
    cursor: pointer;
}

.map-topbar__toggle-btn:hover { background: #f1f5f9; }

.map-topbar__toggle-btn.is-active {
    background: var(--vefa-primary, #3498db);
    color: #fff;
    cursor: default;
}

/* --------- Chips de filtres actifs (haut-gauche, sous le zoom) --------- */
.map-active-filters {
    position: absolute;
    top: 128px;     /* sous le groupe de contrôles zoom (haut-gauche) */
    left: 12px;
    z-index: 875;
    display: flex;
    flex-wrap: wrap;
    gap: 6px;
    max-width: min(46vw, 420px);
}

.map-chip {
    display: inline-flex;
    align-items: center;
    gap: 5px;
    background: #fff;
    border-radius: 999px;
    padding: 5px 11px;
    font-size: 0.76rem;
    font-weight: 600;
    color: var(--vefa-text, #1f2d3d);
    text-decoration: none;
    box-shadow: 0 3px 12px rgba(15, 23, 42, 0.16);
}

.map-chip i { font-size: 0.68rem; color: #94a3b8; }
.map-chip:hover { color: var(--vefa-accent, #ff4000); }
.map-chip:hover i { color: var(--vefa-accent, #ff4000); }

.map-chip--clear {
    background: rgba(255, 255, 255, 0.75);
    color: #64748b;
    font-weight: 500;
}

/* Recherche flottante — ancrée en haut-centre DE la carte (position absolute
   au-dessus du canvas), sous les toasts/chip de zone (dégagés à 68px, cf.
   .map-zone-search/.map-toast) mais au-dessus du rendu de la carte. Même
   `<form id="quick-search-form">` que la vue liste (juste un mécanisme
   différent : GET classique, cf. commentaire du partial programs_map_inline.html) —
   pas de conflit d'id, les deux branches sont mutuellement exclusives. */
.map-float-search {
    position: absolute;
    top: 12px;
    left: 50%;
    transform: translateX(-50%);
    z-index: 870;
    width: 340px;
    max-width: calc(100% - 24px);
}

.map-float-search__input-wrap {
    position: relative;
    display: flex;
    align-items: center;
    background: #fff;
    border-radius: 999px;
    box-shadow: 0 4px 16px rgba(15, 23, 42, 0.22);
    overflow: hidden;
}

.map-float-search__input {
    flex: 1 1 auto;
    min-width: 0;
    border: 0;
    outline: none;
    background: transparent;
    padding: 11px 4px 11px 16px;
    font-size: 0.88rem;
    color: var(--vefa-text, #1f2d3d);
}

.map-float-search__input::placeholder { color: #94a3b8; }

.map-float-search__submit {
    flex: 0 0 auto;
    width: 38px;
    height: 38px;
    margin: 4px;
    border: 0;
    border-radius: 50%;
    background: var(--vefa-accent, #ff4000);
    color: #fff;
    display: flex;
    align-items: center;
    justify-content: center;
    cursor: pointer;
    transition: background 0.15s ease;
}

.map-float-search__submit:hover { background: #e63a00; }

/* =====================================================
   Responsive
   ===================================================== */
@media (max-width: 767px) {
    .map-popup { width: 240px; }
    .map-popup__media { height: 110px; }
    /* 220px (au lieu de 180px) : la légende mobile porte désormais aussi le
       compteur fusionné (Lot A item 4, cf. .map-legend .search-map-counter
       plus bas) — 180px faisait tronquer « N chantiers affichés ». */
    .map-legend { max-width: 220px; }
    /* Repositionnée en bas d'écran, centrée (Lot A item 3) : la pile de
       contrôles du haut-droit est devenue un seul bouton « Couches »
       (.map-layers-toggle plus bas), mais la pilule reste descendue pour
       ne jamais chevaucher la barre d'outils / les chips de filtres.
       140px : mesuré (QA) au-dessus de la légende mobile dans ses DEUX états
       les plus hauts — compteur+chip « Zone affichée » (123px) et
       compteur+note « non localisés » (113px), jamais les deux à la fois
       (renderCounter() les affiche en exclusion mutuelle) — + marge. */
    /* 2026-08-19 — 3e itération retour utilisateur : « ça flotte toujours,
       le compteur ET la pilule zone doivent disparaître ». La pilule zone
       est désormais entièrement masquée sur mobile (`display:none`) au lieu
       d'être repositionnée une fois de plus — elle campait toujours au
       milieu du regard quel que soit son ancrage. Les propriétés ci-dessous
       (héritées des itérations précédentes) deviennent inertes mais restent
       en place ; le bouton JS (zoneBtn/resetZone, cf. programs_map.js) n'est
       pas retiré, juste rendu invisible — desktop (règle de base, hors ce
       bloc @media) strictement inchangé. */
    .map-zone-search {
        display: none;
        top: 64px;
        font-size: 0.78rem;
        padding: 7px 12px;
        white-space: nowrap;
    }

    /* 2026-08-19 — la pilule ambiance devient une pastille FAB ronde 48×48
       ancrée bas-droite (au lieu de flotter bas-centre au-dessus de la
       légende) : right:10 / bottom:56, au-dessus du bouton attribution
       MapLibre ⓘ (bottom-right natif, ~24px de haut + son offset ~10px de
       marge basse ⇒ 56 dégage une marge confortable, mesuré en QA). Un tap
       sur .map-ambiance__fab bascule .is-expanded (cf. règle juste après et
       programs_map.js) : la pilule redevient alors la rangée habituelle,
       ancrée depuis ce MÊME coin (right:10 inchangé ⇒ elle s'étend vers la
       gauche, jamais vers le centre de l'écran). */
    .map-ambiance {
        bottom: 56px;
        left: auto;
        right: 10px;
        transform: none;
        width: 48px;
        height: 48px;
        max-width: 48px;
        padding: 0;
        border-radius: 50%;
        align-items: center;
        justify-content: center;
        overflow: hidden;
        background: var(--vefa-bg, #fff);
        backdrop-filter: none;
        box-shadow: 0 3px 12px rgba(15, 23, 42, 0.18);
    }

    /* Dépliée : redevient la pilule arrondie classique (mêmes teintes que
       la règle de base desktop), taille reprise par le contenu (la rangée
       mobile ci-dessous) — ~310px de large mesurés à 390px de viewport. */
    .map-ambiance.is-expanded {
        width: auto;
        height: auto;
        max-width: calc(100vw - 20px);
        padding: 8px 10px;
        border-radius: 16px;
        overflow: visible;
        background: rgba(255, 255, 255, 0.94);
        backdrop-filter: blur(4px);
        box-shadow: 0 4px 16px rgba(15, 23, 42, 0.18);
    }

    /* 2026-08-18 — la pilule ambiance mobile bascule sur la rangée unique
       (chip saison cyclique + 5 pas horaires, cf. .map-ambiance__mobile-row
       plus haut) : les 3 rangées desktop (saisons / soleil-slider / presets)
       sortent du flux, cette rangée les remplace — hauteur de la pilule
       ~divisée par 2 (l'ancien empilement chevauchait la pilule zone/la
       légende repliée, retour utilisateur). ≥768px : aucune de ces 2 règles
       ne s'applique (bloc @media 767px), comportement STRICTEMENT inchangé
       (slider fin desktop + presets pointer:coarse tablette/laptop tactile,
       cf. règles de base plus haut). */
    .map-ambiance__seasons,
    .map-ambiance__sun,
    .map-ambiance__sun-presets {
        display: none;
    }

    /* Pastille visible seulement à l'état replié (repos) — cf.
       programs_map.js pour le toggle au tap. */
    .map-ambiance__fab {
        display: flex;
        width: 48px;
        height: 48px;
        flex: 0 0 auto;
        align-items: center;
        justify-content: center;
        border: 0;
        background: transparent;
        color: var(--vefa-warning, #f59e0b);
        font-size: 1.05rem;
        cursor: pointer;
    }

    .map-ambiance.is-expanded .map-ambiance__fab { display: none; }

    /* Rangée masquée au repos (seule la pastille ci-dessus est visible) —
       n'apparaît que dépliée (2026-08-19, remplace l'ancien
       `display:flex` inconditionnel de la même règle sous ce breakpoint). */
    .map-ambiance.is-expanded .map-ambiance__mobile-row { display: flex; }

    /* Mobile : 143px = en-tête (68px + 0px de marge haute + 10px de marge
       basse sous 560px, cf. site_header.css) + barre du bas (65px). Historique
       des valeurs : 125 = ancienne navbar 60 + 65 ; 153 = carte flottante,
       avant que l'en-tête ne passe pleine largeur collé en haut (2026-08-17),
       ce qui a rendu ses 10px de marge haute à la carte. */
    #search-programs-map { height: calc(100dvh - 143px); min-height: 380px; }
    .search-map-container, #search-programs-map { background: #e8eaed; }

    /* Barre d'outils compacte : icônes seules (les libellés prennent trop de
       place à côté de la recherche flottante). */
    /* right:140px = largeur intrinsèque de la topbar (~117px : Filtres 35
       + gap 6 + toggle 76) + son offset right:10 + un écart de sécurité.
       À 92px, le champ passait SOUS le bouton Filtres et son submit orange
       dépassait en croissant derrière (constaté en prod sur iPhone). */
    .map-float-search { width: auto; top: 10px; left: 12px; right: 140px; transform: none; max-width: none; }
    .map-topbar { top: 10px; right: 10px; gap: 6px; }
    .map-topbar__label { display: none; }
    .map-topbar__btn { padding: 9px 11px; }
    .map-topbar__toggle-btn { padding: 7px 10px; }
    .map-active-filters { top: 120px; max-width: calc(100vw - 24px); }

    /* =====================================================
       Lot A (2026-08) — désencombrement de la carte mobile
       ===================================================== */

    /* --- Item 4 : compteur fusionné dans l'en-tête de la légende repliée.
       #map-counter est DÉPLACÉ (JS, legendControl.onAdd, élément déplacé —
       pas cloné) dans le bloc légende quand elle démarre repliée (mobile) :
       ce n'est donc plus une pilule flottante autonome (ancienne règle
       `bottom: 92px; right: 10px` supprimée, devenue sans objet) — la
       position absolue générale de .search-map-counter est neutralisée
       ici pour s'intégrer à la colonne de la légende. Règle DOM-
       conditionnelle (dépend du déplacement JS, pas seulement du viewport)
       mais rangée avec le reste du Lot A pour rester groupée.
       2026-08-19 — 3e itération retour utilisateur : masqué par défaut
       (pilule repliée, `display:none`) — l'info reste utile mais ne doit
       plus flotter en permanence à côté du libellé « Légende & couches ».
       Redevient visible uniquement dans la sheet ouverte (règle
       `.map-legend.is-sheet-open .search-map-counter` juste après) — pur
       CSS, renderCounter() continue de cibler #map-counter par id sans
       changement de cycle (il est réinjecté avec son innerHTML à chaque
       appel, jamais recréé). */
    .map-legend .search-map-counter {
        position: static;
        display: none;
        width: 100%;
        background: transparent;
        box-shadow: none;
        backdrop-filter: none;
        border-top: 1px solid var(--vefa-border, #e2e8f0);
        border-radius: 0;
        margin-top: 6px;
        padding: 8px 12px 4px;
    }

    /* Sheet ouverte (cf. .map-legend.is-sheet-open plus bas, Lot B) : le
       compteur redevient visible — c'est de l'info utile DANS la sheet,
       seul l'état replié le masque. */
    .map-legend.is-sheet-open .search-map-counter {
        display: flex;
    }

    /* --- Item 1 : panneau « Couches » — bouton 44×44 + panneau ancré à
       droite, construits par navControl.onAdd() dans programs_map.js quand
       window.innerWidth < 768. Le groupe de contrôles déplacé à l'intérieur
       (.map-ctl) garde son style desktop existant (icônes, fond blanc,
       coins arrondis via #search-programs-map .maplibregl-ctrl-group) —
       seul son emplacement change, replié dans le panneau au lieu de
       flotter en haut-droite. --- */
    .map-layers-mobile { position: relative; }

    .map-layers-toggle {
        display: flex;
        align-items: center;
        justify-content: center;
        width: 44px;
        height: 44px;
        border: 0;
        border-radius: 10px;
        background: var(--vefa-bg, #fff);
        color: var(--vefa-text, #1f2d3d);
        font-size: 16px;
        box-shadow: 0 3px 12px rgba(15, 23, 42, 0.18);
        cursor: pointer;
        transition: color 0.15s ease, background 0.15s ease;
    }

    /* Préfixe #search-programs-map obligatoire sur les états hover/ouvert :
       maplibre-gl.css pose `.maplibregl-ctrl button:not(:disabled):hover
       { background-color: rgba(0,0,0,.05) }` (spécificité 0,3,1) qui écrase
       un fond posé par simple classe — et au doigt le :hover COLLE après le
       tap : bouton ouvert = icône blanche sur gris quasi transparent,
       invisible sur carte claire (attrapé en contre-QA). Même parade que
       les cibles 44px de la media query pointer:coarse plus haut. */
    #search-programs-map .map-layers-toggle:hover {
        background: var(--vefa-bg-soft, #f8fafc);
        color: var(--vefa-accent, #ff4000);
    }

    #search-programs-map .map-layers-toggle[aria-expanded="true"],
    #search-programs-map .map-layers-toggle[aria-expanded="true"]:hover {
        background: var(--vefa-accent, #ff4000);
        color: var(--vefa-text-on-dark, #fff);
    }

    .map-layers-panel {
        display: none;
        position: absolute;
        top: calc(100% + 8px);
        right: 0;
        z-index: 950;
        width: 220px;
        max-width: calc(100vw - 24px);
        max-height: calc(100dvh - 220px);
        overflow-y: auto;
        overscroll-behavior: contain;
        background: rgba(255, 255, 255, 0.98);
        border-radius: 12px;
        box-shadow: 0 8px 28px rgba(15, 23, 42, 0.26);
        padding: 10px;
    }

    /* Ouvert = colonne (groupe de contrôles au-dessus, section POI en
       dessous, séparées par la bordure de .map-layers-panel__section
       suivante). Fermé : display:none ci-dessus, jamais de flash au
       chargement (contrairement à l'attribut `hidden`, qui se ferait
       silencieusement écraser par un `display` posé en base de classe). */
    .map-layers-panel.is-open { display: flex; flex-direction: column; gap: 10px; }

    /* Le groupe de contrôles déplacé (.map-ctl / .maplibregl-ctrl-group)
       porte encore la classe `maplibregl-ctrl` — maplibre-gl.css lui pose
       `float:right; margin:10px 10px 0 0` pensé pour flotter en coin de
       carte, plus du tout pertinent une fois glissé dans une colonne du
       panneau (le groupe se collait à droite avec un vide à gauche).
       Lot A.2, item 3(a) : grille horizontale (flex row + wrap) plutôt
       qu'une colonne empilée — la hauteur du panneau est ~divisée par 2.
       `overflow: visible` : la règle de base .map-ctl (overflow:hidden,
       pensée pour la pilule verticale desktop) clipperait sinon les ombres
       des boutons qui passent à la ligne. */
    .map-layers-panel .maplibregl-ctrl-group {
        float: none;
        margin: 0;
        width: 100%;
        display: flex;
        flex-wrap: wrap;
        gap: 8px;
        overflow: visible;
    }

    /* Les marges verticales de 8px posées par la media query pointer:coarse
       (plus haut dans ce fichier, pensée pour un empilement colonne) sont
       neutralisées ici : le `gap` ci-dessus les remplace, dans les deux
       dimensions, pour un layout en grille. Tailles 44px !important
       CONSERVÉES (non touchées) — seul l'espacement change. Sélecteur à 3
       classes : spécificité (0,3,0) > (0,2,0) de
       `.map-ctl-btn:not(:last-child)`, l'override est donc garanti quel
       que soit l'ordre des règles dans ce fichier. */
    .map-layers-panel .maplibregl-ctrl-group .map-ctl-btn {
        margin-bottom: 0;
    }

    .map-layers-panel__section:empty { display: none; }

    .map-layers-panel__section + .map-layers-panel__section {
        border-top: 1px solid var(--vefa-border, #e2e8f0);
        padding-top: 8px;
    }

    /* --- Item 5 : auto-masquage des overlays secondaires pendant un
       pan/zoom (cf. movestart/moveend dans programs_map.js). Recherche
       flottante + barre d'outils du haut ne sont jamais concernées —
       elles restent les seuls repères visibles pendant le geste. --- */
    .map-legend,
    .map-active-filters,
    .map-zone-search,
    .map-layers-mobile,
    .map-ambiance {
        transition: opacity 0.18s ease;
    }

    .map-overlay--fading {
        opacity: 0;
        pointer-events: none;
    }

    /* =====================================================
       Lot A.2 (2026-08) — finitions anti-superposition
       ===================================================== */

    /* --- Item 1 : les boutons +/- du zoom sont retirés du DOM côté JS
       (showZoom:false au constructeur du NavigationControl, pas juste
       masqués ici) — le pincement suffit au doigt. La boussole reste
       construite (showCompass:true) mais démarre masquée ; la classe
       .is-tilted, posée en JS sur les événements rotate/pitch de la carte
       (cf. updateCompassVisibility dans programs_map.js), la révèle
       uniquement quand la carte est inclinée ou tournée. Desktop : aucune
       règle ici, comportement MapLibre natif inchangé. */
    #search-programs-map .maplibregl-ctrl-compass {
        display: none;
    }

    #search-programs-map .maplibregl-ctrl-compass.is-tilted {
        display: flex;
    }

    /* ⚠️ Piège débogué en QA : `.maplibregl-ctrl-top-left` démarre au même
       top:10px (relatif au conteneur carte) que `.map-float-search`, qui
       s'étale large sur mobile (left:12px / right:92px, cf. règle mobile
       plus haut) — les deux se superposent. Invisible avant ce lot (le
       zoom+/- masquait discrètement le 1er bouton du groupe, zoom-out et
       boussole restaient lisibles en dessous) ; devenu bloquant maintenant
       que la boussole EST ce 1er (et seul) bouton du groupe — sans ce
       décalage, .is-tilted la rendrait « visible » en display mais sous la
       barre de recherche. 68px = même valeur que .map-zone-search/.map-toast
       plus haut dans ce fichier (dégage la recherche flottante) ; 68+44 (une
       boussole) = 112px, sous les chips (top:120px) : aucune collision
       introduite. Desktop non concerné (règle mobile uniquement). */
    .maplibregl-ctrl-top-left {
        top: 68px;
    }

    /* --- Item 3(c) / Lot B : masqués tant que l'UNE des deux surfaces
       mobiles (sheet légende OU panneau Couches) est ouverte — classe
       togglée en JS par setMobileOverlaysHiddenForSurface (partagée entre
       legendControl et navControl.onAdd, cf. programs_map.js). Étendu au
       Lot B (2026-08-18) de la seule .map-active-filters à la pilule zone,
       la pilule ambiance et le bouton de l'AUTRE surface (`.map-legend` —
       pilule repliée — quand le panneau Couches s'ouvre ; `.map-layers-
       mobile` quand la sheet légende s'ouvre). La transition d'opacité
       vient déjà de la règle Item 5 ci-dessus (mêmes sélecteurs listés,
       plus .map-ambiance) — classe DÉDIÉE, volontairement distincte de
       .map-overlay--fading : les deux mécanismes sont indépendants, un
       geste de pan pendant qu'une surface est ouverte ne doit pas
       réafficher ces overlays au relâchement (setOverlaysFading ne connaît
       que MOBILE_FADE_SELECTOR, pas l'état des surfaces Lot B). */
    .map-active-filters.is-panel-hidden,
    .map-zone-search.is-panel-hidden,
    .map-ambiance.is-panel-hidden,
    .map-layers-mobile.is-panel-hidden,
    .map-legend.is-panel-hidden {
        opacity: 0;
        pointer-events: none;
    }

    /* =====================================================
       Lot B (2026-08-18) — bottom-sheet mobile (légende & couches)
       Retour utilisateur PROD : la légende dépliée flottait par-dessus la
       carte et chevauchait chips/pilule zone/pilule ambiance. Elle devient
       ici une vraie bottom-sheet ancrée au bas du conteneur carte quand
       dépliée ; repliée elle reste la pilule compacte du Lot A (inchangée).
       DOM : wrap (.map-legend) est déplacé — jamais cloné — entre le coin
       MapLibre natif (pilule) et #search-programs-map directement (sheet),
       cf. openLegend/closeLegend dans programs_map.js. Desktop : aucune de
       ces règles ne s'applique (bloc @media 767px), et `.map-legend__headrow`
       / `.map-legend__close` n'existent même pas dans le DOM desktop (markup
       conditionnel côté JS) — zéro risque de fuite.
       ===================================================== */

    /* En-tête mobile : bouton titre (flex:1, tap ≥44px) + bouton fermeture
       dédié côte à côte. N'existe QUE sur mobile (markup JS conditionnel) —
       pas besoin de gate supplémentaire ici, mais rangé dans ce bloc @media
       par cohérence avec le reste du fichier. */
    .map-legend__headrow {
        display: flex;
        align-items: center;
    }

    .map-legend__headrow .map-legend__head {
        flex: 1 1 auto;
        width: auto;
        min-width: 0;
    }

    /* Cible tactile ≥44px (WCAG 2.5.5) — la pilule/en-tête sert de bouton
       principal d'ouverture/fermeture sur mobile ; le padding existant
       (8px 12px + line-height du 0.8rem) ne l'atteignait pas (~31px).
       Desktop non concerné (règle dans ce bloc @media uniquement). */
    .map-legend__head {
        min-height: 44px;
    }

    /* Bouton de fermeture dédié (Item « en plus du tap sur l'en-tête ») —
       masqué par défaut (pilule repliée) : affiché uniquement quand la
       sheet est ouverte, cf. règle .is-sheet-open plus bas. Pas de `hidden`
       JS ici : le display:none de base suffit, aucune classe supplémentaire
       à gérer côté script. */
    .map-legend__close {
        display: none;
        flex: 0 0 auto;
        width: 44px;
        height: 44px;
        align-items: center;
        justify-content: center;
        background: transparent;
        border: 0;
        border-radius: 10px;
        color: var(--vefa-text, #1f2d3d);
        font-size: 16px;
        cursor: pointer;
    }

    /* Piège (a) de ce chantier : `.map-legend` porte la classe
       `maplibregl-ctrl`, donc `.maplibregl-ctrl button:not(:disabled):hover
       {background-color:rgb(0 0 0/5%)}` de maplibre-gl.css (spécificité
       0,3,1) écraserait un simple `.map-legend__close:hover` — préfixe
       #search-programs-map obligatoire (même parade que .map-layers-toggle
       plus haut dans ce fichier). */
    #search-programs-map .map-legend__close:hover {
        background: var(--vefa-bg-soft, #f8fafc);
        color: var(--vefa-accent, #ff4000);
    }

    /* --- Bottom-sheet : la légende dépliée sort du coin MapLibre (JS) pour
       devenir un enfant direct de #search-programs-map, seul moyen fiable
       de couvrir toute la largeur du conteneur carte — les coins MapLibre
       (.maplibregl-ctrl-bottom-left) sont en position:absolute avec une
       largeur calée sur leur contenu (shrink-to-fit), pas sur la carte :
       y positionner une sheet pleine largeur en CSS pur (sans déplacement
       DOM) donnerait une largeur de 0 ou incohérente selon les navigateurs.
       #search-programs-map est déjà position:relative (règle de tout en
       haut de ce fichier) : left/right/bottom:0 s'y ancrent donc
       correctement, exactement comme .map-zone-search/.map-ambiance qui y
       sont déjà des enfants directs. */
    .map-legend.is-sheet-open {
        position: absolute;
        left: 0;
        right: 0;
        bottom: 0;
        top: auto;
        z-index: 950; /* même palier que .map-layers-panel : mutuellement exclusifs, jamais visibles ensemble */
        display: flex;
        flex-direction: column;
        width: auto;
        max-width: none;
        max-height: 55%;
        border-radius: 16px 16px 0 0;
        box-shadow: 0 -6px 24px rgba(15, 23, 42, 0.22);
    }

    .map-legend.is-sheet-open .map-legend__headrow {
        flex: 0 0 auto;
    }

    .map-legend.is-sheet-open .map-legend__close {
        display: flex;
    }

    /* Corps scrollable : remplace le plafond fixe 300px (pensé pour la
       pilule desktop/repliée) par un flex-fill de l'espace restant sous
       l'en-tête, dans la limite du max-height:55% posé sur le conteneur
       ci-dessus. min-height:0 est INDISPENSABLE — sans lui un enfant flex
       ne peut pas rétrécir sous son contenu intrinsèque, et le scroll
       interne ne se déclenche jamais (piège flexbox classique). */
    .map-legend.is-sheet-open .map-legend__body {
        flex: 1 1 auto;
        min-height: 0;
        max-height: none;
        overflow-y: auto;
        padding-bottom: calc(10px + env(safe-area-inset-bottom, 0px));
    }
}
