html {
    scroll-behavior: smooth;
}
body {
    font-family: "Inter", sans-serif;
    color: #211315;
}

/* ---------- Scrollbars natives - thème projet ----------
   Style appliqué globalement à la fenêtre + à tout conteneur scrollable
   (overflow:auto/scroll). Les couleurs reprennent les tokens du design :
     - thumb au repos  : surface-border (#D9D9D9), discret sur fond clair
     - thumb au hover  : ink-500 (#757575), plus contrasté pour l'interaction
     - thumb actif     : brand (rgb(var(--brand))), retour visuel cohérent au drag
     - track           : transparent (le fond du conteneur transparaît)

   Les règles plus spécifiques en aval (`.scroll-snap::-webkit-scrollbar`
   masquée pour les carrousels, `.media-carousel-track` idem, surcharge
   à 6px sur `.multiselect-options`) l'emportent par spécificité de
   classe et restent intactes. Ne rien ajouter ici sans vérifier qu'on
   ne casse pas ces overrides.

   Note WebKit : `background-clip: padding-box` + `border: 2px solid transparent`
   crée un padding visuel autour du thumb (10px de piste, 6px de thumb effectif)
   pour ne pas coller aux bords - pattern classique des scrollbars macOS-like. */
html {
    scrollbar-width: thin;                  /* Firefox : barre fine */
    scrollbar-color: #D9D9D9 transparent;   /* Firefox : thumb / track */
}
::-webkit-scrollbar {
    width: 10px;
    height: 10px;
}
::-webkit-scrollbar-track {
    background: transparent;
}
::-webkit-scrollbar-thumb {
    background: #D9D9D9;
    border: 2px solid transparent;
    background-clip: padding-box;
    border-radius: 999px;
    transition: background-color 0.15s ease;
}
::-webkit-scrollbar-thumb:hover {
    background: #757575;
    background-clip: padding-box;
}
::-webkit-scrollbar-thumb:active {
    background: rgb(var(--brand));
    background-clip: padding-box;
}
::-webkit-scrollbar-corner {
    background: transparent;
}
/* Variante "fine" sur les panels overlay (multiselect/autocomplete/datepicker)
   et toute zone interne où la 10px ferait trop lourd. Spécificité de classe
   l'emporte sur les règles globales ci-dessus. */
.multiselect-panel ::-webkit-scrollbar,
.datepicker-panel ::-webkit-scrollbar {
    width: 6px;
    height: 6px;
}

/* Header : transition de repli au scroll vers le bas */
#site-header,
#list-filters-bar,
#provider-anchor-bar {
    transition: transform 0.3s ease;
    will-change: transform;
}
#site-header.is-hidden {
    transform: translateY(-100%);
}
/* Header jaune quand on est tout en haut - posé par subs.js sur public/subs.php
   uniquement (classe jamais ajoutée ailleurs → règle inerte sur les autres pages).
   Sélecteur id+classe : l'emporte sur l'utilitaire `.bg-white` du header (header.php)
   sans !important. La transition bg/bordure surcharge celle (transform only) ci-dessus
   pour #site-header ; #list-filters-bar conserve la sienne. */
#site-header {
    transition: transform 0.3s ease, background-color 0.25s ease, border-color 0.25s ease;
}
#site-header.subs-header-yellow {
    background-color: #F4F4A8;   /* = accent (jaune unique) */
    border-bottom-color: transparent;
}
/* Barres sticky top-[85px] sous le header (filtres de list.php, ancres de provider.php).
   Quand le header se replie, on translate la barre de -85px (hauteur du header) pour
   qu'elle remonte se coller au top et qu'il n'y ait pas de trou visuel à la place du header. */
#list-filters-bar.header-collapsed,
#provider-anchor-bar.header-collapsed {
    transform: translateY(-85px);
}
/* Nav d'ancres de la barre provider : scrollable horizontalement si elle déborde
   (systématique en mobile, possible en md serré) mais sans scrollbar visible - la
   10px globale ferait un trait disgracieux en plein milieu de la barre. PAS de
   `.scroll-snap` ici : cette classe embarque tout le système carrousel (drag souris,
   snap mandatory) qui avalerait les clics sur les liens. */
#provider-anchor-bar nav {
    -ms-overflow-style: none;
    scrollbar-width: none;
}
#provider-anchor-bar nav::-webkit-scrollbar {
    display: none;
}

/* Barre de filtres sticky de subs.php - même fonctionnement que #list-filters-bar :
   fixée sous le header (top: 85px, z-40 < header z-50) → jamais recouverte quand le header
   réapparaît. Quand le header se replie (`#site-header.is-hidden`), elle remonte se coller
   au top via `:has()` (même approche que les sidebars du projet). L'apparition/disparition
   au scroll (sous la barre de filtres principale) est pilotée par `.is-visible` dans subs.js.
   IMPORTANT : on anime `top` (et NON `transform`) pour la remontée - un `transform` créerait
   un containing block qui casserait le positionnement position:fixed des dropdowns
   multiselect/autocomplete contenus dans la barre. */
#subs-sticky-bar {
    position: fixed;
    top: 85px;
    left: 0;
    right: 0;
    z-index: 40;
    opacity: 0;
    pointer-events: none;
    transition: top 0.3s ease, opacity 0.25s ease;
}
#subs-sticky-bar.is-visible {
    opacity: 1;
    pointer-events: auto;
}
body:has(#site-header.is-hidden) #subs-sticky-bar {
    top: 0;
}
@media (prefers-reduced-motion: reduce) {
    #subs-sticky-bar {
        transition: none;
    }
}
/* Bug d'empilement mobile : un panel multiselect/autocomplete ouvert DANS la barre sticky
   reste piégé dans son contexte d'empilement (z-index:40), alors que l'overlay mobile
   (#multiselect-overlay) est rattaché au <body> à z-55 → le voile passe par-dessus le
   bottom-sheet. Tant qu'un panel y est ouvert (`.multiselect-panel` sans `.hidden`, chrome
   partagée par le multiselect ET l'autocomplete), on rehausse la barre au-dessus du voile
   pour que son panel (z-60) ressorte par-dessus. */
#subs-sticky-bar:has(.multiselect-panel:not(.hidden)) {
    z-index: 60;
}
/* Cas inverse (group.php) : le filtre « région » vit inline dans le flux - pas
   dans la modale plein écran comme list.php (où `body.filters-modal-open` rehausse
   le panel à z-95). Son bottom-sheet mobile ouvert reste donc à z-60, SOUS la barre
   comparateur (#list-mobile-bar, z-70) qui flotte alors par-dessus le panel et son
   voile (#multiselect-overlay, z-55). Tant qu'un panel est ouvert, on fait repasser
   la barre sous le voile. Sans effet sur list.php / user-list-booking.php : leurs
   multiselects vivent dans une modale plein écran qui masque déjà la barre. */
body:has(.multiselect-panel:not(.hidden)) #list-mobile-bar {
    z-index: 50;
}
/* Bannière pub sticky dans la sidebar gauche (list.php, desktop ≥ lg).
   top par défaut = header(85) + filterbar(76) + air(15) = 176px. Quand le header se replie
   (`#list-filters-bar.header-collapsed`), la filterbar remonte se coller au top → on baisse
   le top de la pub à filterbar(76) + air(15) = 91px pour qu'elle suive. Transition alignée
   sur les 0.3s ease du header pour éviter un saut visuel. */
@media (min-width: 1024px) {
    #sidebar-ad {
        top: 176px;
        transition: top 0.3s ease;
    }
    body:has(#list-filters-bar.header-collapsed) #sidebar-ad {
        top: 91px;
    }
}
/* provider.php - Sidebar droite (desktop ≥ lg) sticky top-[160px]
   (= header 85 + barre d'ancres 60 + air 15 ; barre = py 10×2 + boutons 40).
   Quand le header se replie (`#site-header.is-hidden`), la barre d'ancres remonte se coller
   au top → on baisse le top de la sidebar à barre(60) + air(15) = 75px pour qu'elle suive.
   Transition alignée sur les 0.3s ease du header pour éviter un saut visuel. */
@media (min-width: 1024px) {
    #provider-sidebar {
        transition: top 0.3s ease;
    }
    body:has(#site-header.is-hidden) #provider-sidebar {
        top: 75px;
    }
}
/* Bug d'empilement desktop : `position: sticky` crée un contexte d'empilement à lui seul
   (même sans z-index). Le panel d'un multiselect / datepicker ouvert dans la sidebar
   (position:fixed, z-60) y reste donc piégé et se peint au rang de la sidebar (z auto = 0)
   dans le contexte de la page : les overlays de la mosaïque galerie (« Afficher la galerie »,
   boutons vidéo / 360°, z-[2]) passaient par-dessus le calendrier quand celui-ci s'ouvrait
   vers le haut. Même remède que #subs-sticky-bar : tant qu'un panel y est ouvert, on rehausse
   la sidebar au niveau du panel (60), au-dessus du header (z-50) et de la barre d'ancres
   (z-40) que le calendrier peut aussi chevaucher. Sans effet sous lg (sidebar `hidden`). */
#provider-sidebar:has(.multiselect-panel:not(.hidden)) {
    z-index: 60;
}
/* blog-list.php - Sidebar blog (desktop ≥ lg) : le bloc "Événements à venir" → "À ne pas
   rater" est sticky top-[100px] (= header 85 + air 15). Même logique que #provider-sidebar :
   quand le header se replie (`#site-header.is-hidden`), on baisse le top à 15px pour qu'il
   suive et reste collé en haut. Transition alignée sur les 0.3s ease du header. */
@media (min-width: 1024px) {
    #blog-sidebar-sticky {
        transition: top 0.3s ease;
    }
    body:has(#site-header.is-hidden) #blog-sidebar-sticky {
        top: 15px;
    }
}
@media (prefers-reduced-motion: reduce) {
    #site-header,
    #list-filters-bar,
    #provider-anchor-bar,
    #sidebar-ad,
    #provider-sidebar,
    #blog-sidebar-sticky {
        transition: none;
    }
}

/* list.php - Toggle Liste/Grille des cards lieu.
   Le markup natif (`flex flex-col gap-[15px]` + chaque card en `flex-col md:flex-row` avec
   média à 310px à gauche) est la vue Liste par défaut. Quand le toggle pose `data-view="grid"`
   sur `#list-cards` (cf. section 16 de main.js), on bascule en grille 3 colonnes ≥ md et on
   réinitialise les cards en empilement vertical (rendu identique au mobile).
   Spécificité (1 id + 1 attr + 1 class = 0,1,2,1) bat les utilitaires Tailwind `md:*` (0,0,1,0),
   donc pas besoin de `!important`. Pas d'override < md : la vue mobile reste inchangée. */

/* Ombre sur chaque card de résultat à TOUTES les tailles (mobile inclus). Déclarée hors
   media-query pour couvrir le mobile ; les bascules list/grille ≥ md ne re-déclarent donc
   plus l'ombre. (Scopé à `#list-cards` pour ne pas toucher la card du popup Leaflet.) */
#list-cards .list-card {
    box-shadow: 0 2px 12px rgba(0, 0, 0, 0.08);
    /* Rendu paresseux : hors écran, la card sort entièrement du style/layout/paint
       (et ses animations sont suspendues) - indispensable quand l'infinite scroll
       accumule des centaines de cards dans le DOM. `auto` mémorise la taille
       réellement rendue ; 300px n'est que l'estimation de hauteur donnée à la
       scrollbar avant le premier rendu (entre ~240px en vue liste desktop et
       ~430px en vue grille/mobile). Même scope que l'ombre : la card du popup
       Leaflet reste rendue normalement. */
    content-visibility: auto;
    contain-intrinsic-size: auto 300px;
}
@media (min-width: 768px) {
    /* Mode Liste - card blanche définie (fond + ombre + coins arrondis, comme la
       grille). Fond blanc + padding UNIQUEMENT sur le contenu : la photo reste en plein
       cadre, collée aux bords gauche/haut/bas de la card (jamais encadrée de blanc) et
       remplit toute la hauteur (align-items:stretch via height:auto) ; ses coins arrondis
       sont gérés par l'overflow-hidden de la card, donc media-frame remis à border-radius 0.
       Gap entre cards à 18px (le `gap-[15px]` du markup convient au mobile mais semblait
       serré en desktop avec les cards horizontales). */
    #list-cards[data-view="list"] {
        gap: 18px;
    }
    #list-cards[data-view="list"] .list-card {
        background-color: #fff;
        border-radius: 15px;
    }
    #list-cards[data-view="list"] .list-card-media {
        padding: 0;
        height: auto;
    }
    #list-cards[data-view="list"] .list-card-media-frame {
        border-radius: 0;
    }
    #list-cards[data-view="list"] .list-card-content {
        padding: 20px;
    }

    /* Mode Grille - 3 colonnes, cards verticales (rendu identique au mobile). */
    #list-cards[data-view="grid"] {
        display: grid;
        grid-template-columns: repeat(3, minmax(0, 1fr));
        gap: 20px;
    }
    #list-cards[data-view="grid"] .list-card {
        flex-direction: column;
        border-radius: 15px;
    }
    #list-cards[data-view="grid"] .list-card-media {
        width: 100%;
        height: 230px;
        padding: 0;
    }
    #list-cards[data-view="grid"] .list-card-media-frame {
        border-radius: 12px 12px 0 0;
    }
    #list-cards[data-view="grid"] .list-card-content {
        padding: 15px;
    }
    /* Séparateur « sans budget renseigné » (list.php, priorisation budget) :
       pleine largeur de la grille - en vue Liste (flex colonne), rien à faire. */
    #list-cards[data-view="grid"] [data-budget-separator] {
        grid-column: 1 / -1;
    }
}

/* CTA de card lieu (render_venue_card) - libellé par défaut = prix (« Dès X € ») ou
   « Contacter » si pas de prix ; au survol de TOUTE la card (`.list-card:hover`), il
   devient « Découvrir → ». Les deux libellés sont empilés dans la MÊME cellule de grille :
   la largeur du bouton = le plus large des deux, donc aucun saut de largeur au survol
   (simple cross-fade d'opacité). Le `:focus-visible` sur le bouton garde le swap au clavier.
   NB : `.venue-cta` impose le display (grid) - le `<a>` ne porte donc pas d'`inline-flex`.
   Mobile (< md) : pas de survol - le CTA affiche « Découvrir → » en permanence et le prix
   est rendu en clair à gauche de la ligne (render_venue_card / buildPopupHtml main.js §16,
   place libérée par le comparateur desktop only). Le libellé prix reste dans l'arbre
   d'accessibilité (pattern sr-only, pas de display:none) : le lien garde son nom
   (« Dès X € » / « Contacter ») et le bouton se dimensionne sur « Découvrir → » seul. */
.venue-cta {
    display: inline-grid;
    place-items: center;
}
.venue-cta > .venue-cta-rest,
.venue-cta > .venue-cta-hover {
    grid-area: 1 / 1;
    display: inline-flex;
    align-items: center;
    gap: 6px;
    transition: opacity 0.15s ease;
}
.venue-cta > .venue-cta-hover {
    opacity: 0;
}
.list-card:hover .venue-cta > .venue-cta-rest,
.venue-cta:focus-visible > .venue-cta-rest {
    opacity: 0;
}
.list-card:hover .venue-cta > .venue-cta-hover,
.venue-cta:focus-visible > .venue-cta-hover {
    opacity: 1;
}
@media (max-width: 767px) {
    .venue-cta > .venue-cta-rest {
        position: absolute;
        width: 1px;
        height: 1px;
        overflow: hidden;
        clip-path: inset(50%);
        white-space: nowrap;
    }
    .venue-cta > .venue-cta-hover {
        opacity: 1;
    }
}

/* Chip type de prestataire (déplacé de middle/user/common.css - composant devenu global).
   Tag neutre (icône + libellé : Lieu, DJ, Traiteur…) rendu par
   render_provider_type_chip() ou en JS (favourite.js) à côté du nom de
   l'établissement - cartes de l'inbox, en-tête de user-booking.php, cards
   favoris et cards « Découvrez notre sélection » (render_home_suggestions).
   Gris volontairement discret : il qualifie sans concurrencer les badges
   colorés (statut, promo). */
.provider-chip {
    display: inline-flex;
    align-items: center;
    gap: 5px;
    height: 22px;
    padding: 0 9px;
    border-radius: 9999px;
    background: #F5F5F5;        /* surface-light */
    color: #757575;             /* ink-500 */
    font-size: 11px;
    font-weight: 600;
    line-height: 1;
    white-space: nowrap;
}
.provider-chip i { font-size: 10px; }
/* Variante fond blanc : pour les cards posées sur une section bg-surface-light
   (ex. carrousel cross-trade de provider.php), où le gris #F5F5F5 du chip se
   fondrait dans le fond. */
.provider-chip--white {
    background: #fff;
}

/* Masque la scrollbar tout en gardant le scroll (overflow simple, sans carrousel).
   Utilisé p.ex. sur la barre de catégories du blog (public/blog-list.php). */
.no-scrollbar {
    -ms-overflow-style: none; /* IE/Edge legacy */
    scrollbar-width: none;    /* Firefox */
}
.no-scrollbar::-webkit-scrollbar {
    display: none;            /* Chrome/Safari/WebKit */
}

/* Drag-to-scroll souris (cf. main.js §4c) : curseur grab + pas de sélection de
   texte pendant le glissement. Le scroll tactile/trackpad reste natif. */
[data-drag-scroll] {
    cursor: grab;
    user-select: none;
    -webkit-user-select: none;
}
[data-drag-scroll].is-grabbing {
    cursor: grabbing;
}

/* Cache les scrollbars sur les carrousels */
.scroll-snap {
    scroll-snap-type: x mandatory;
    -ms-overflow-style: none;
    scrollbar-width: none;
    cursor: grab;
    touch-action: pan-y;
    user-select: none;
    -webkit-user-select: none;
}
.scroll-snap::-webkit-scrollbar {
    display: none;
}
.scroll-snap > * {
    scroll-snap-align: start;
}
.scroll-snap.is-dragging {
    cursor: grabbing;
    scroll-snap-type: none;
}
.scroll-snap.is-dragging a,
.scroll-snap.is-dragging img {
    pointer-events: none;
}
/* Pendant l'animation de snap JS, on désactive le scroll-snap natif pour éviter les conflits */
.scroll-snap.is-snapping {
    scroll-snap-type: none;
}
/* Desktop : cards CENTRÉES quand elles ne remplissent pas la piste (défaut de
   TOUS les carrousels .scroll-snap), alignement standard dès que ça déborde - le
   mot-clé `safe` bascule en flex-start en débordement, sans quoi le début du
   carrousel passerait à scrollLeft négatif et deviendrait inatteignable au
   scroll (cf. .photo-viewer-thumbs, même pattern).
   Posée en :where() donc à spécificité 0 : n'importe quel utilitaire Tailwind
   `justify-*` du markup la bat malgré l'ordre des feuilles (styles.css après
   tailwind.css) - c'est ainsi qu'une piste s'exclut du centrage avec un simple
   `md:justify-start` / `lg:justify-start`. Exclues à ce jour : celles qui
   basculent en COLONNE sur desktop (#subs-packs, #subs-boost, #sidebar-sponso,
   la piste des labels de fiche) et celles qui doivent rester alignées sur le
   contenu qui les précède (#reviews-photos-carousel et
   #reviews-modal-photos-carousel = miniatures des photos d'avis,
   #actions-carousel = actions du dashboard internaute) et TOUTES les pistes de
   la fiche publique provider.php (#promos-carousel, #accommodations-carousel,
   #menus-carousel, #access-carousel, #rse-carousel, #prestataires, #similaires -
   décision du 2026-09-10 : sur la fiche, une piste incomplète reste calée à
   gauche sous son titre, comme les miniatures d'avis). .comparator-track, elle,
   reste collée à gauche par sa propre règle (alignement sur sa colonne de
   libellés).
   Mobile exclu : la piste y est full-bleed (marges négatives + padding de 15px
   calibrés sur le .container-x), l'alignement à gauche reste le bon. */
@media (min-width: 768px) {
    :where(.scroll-snap) {
        justify-content: safe center;
    }
}

/* Mobile : carrousels en pleine largeur (collés aux bords de l'écran)
   tout en gardant une marge gauche/droite sur la première / dernière carte.
   On annule les 15px de padding du .container-x parent via des marges négatives,
   et on réinjecte ce 15px en padding + scroll-padding pour le snap. */
@media (max-width: 767px) {
    .scroll-snap {
        margin-left: -15px;
        margin-right: -15px;
        padding-left: 15px;
        padding-right: 15px;
        scroll-padding-left: 15px;
        scroll-padding-right: 15px;
    }
}

/* Hero gradient overlay */
.hero-overlay {
    background: linear-gradient(
        180deg,
        rgba(0, 0, 0, 0.15) 0%,
        rgba(0, 0, 0, 0.35) 50%,
        rgba(0, 0, 0, 0.65) 100%
    );
}

/* Animations subtiles */
@keyframes fadeInUp {
    from {
        opacity: 0;
        transform: translateY(20px);
    }
    to {
        opacity: 1;
        transform: translateY(0);
    }
}
.fade-in-up {
    animation: fadeInUp 0.7s ease-out both;
}

/* Ligne d'étoiles */
.star {
    color: #ffb800;
}

/* Bouton principal */
.btn-primary {
    background: rgb(var(--brand));
    color: #fff;
    font-weight: 700;
    padding: 14px 28px;
    border-radius: 12px;
}
.btn-primary:hover {
    background: rgb(var(--brand-dark));
}

/* Bouton secondaire / contour */
.btn-ghost {
    background: #f2f3f5;
    color: #0a0a0a;
    font-weight: 500;
    padding: 14px 28px;
    border-radius: 12px;
}
.btn-ghost:hover {
    background: #e6e7ea;
}

/* Carte "cliquable en entier" : le <a> est sur le titre, mais s'étire sur toute la carte */
.card-link {
    position: static;
    transition: color 0.15s ease;
}
.card-link::after {
    content: "";
    position: absolute;
    inset: 0;
    z-index: 1;
}
.card-link:hover {
    color: rgb(var(--brand));
}
/* Les contrôles dans la carte (favori, badge) doivent rester cliquables au-dessus du lien fantôme.
   On ne fixe pas `position` ici : chaque contrôle définit déjà son positionnement (absolute) via Tailwind. */
.card-overlay {
    z-index: 2;
}

/* Bouton favori actif (cf. main.js §25 : ajoute `.is-faved` quand la fiche est dans
   le localStorage `1001salles:favourites`). Le signet passe en plein (fa-solid, swap JS)
   et en couleur brand. Spécificité (0,2,0) > `.text-ink-900` (0,1,0) pour gagner sur la
   couleur de base du bouton. */
[data-fav-toggle].is-faved {
    color: rgb(var(--brand));
}

/* Lazy-loading visuel : fond gris + spinner centré tant que l'image n'est pas chargée */
.img-loading {
    background: #ededed;
}
.img-loading::after {
    content: "";
    position: absolute;
    top: 50%;
    left: 50%;
    width: 28px;
    height: 28px;
    margin: -14px 0 0 -14px;
    border: 3px solid #d4d4d4;
    border-top-color: #757575;
    border-radius: 50%;
    animation: img-spin 0.8s linear infinite;
    pointer-events: none;
    z-index: 0;
}
.img-loading.is-loaded::after {
    display: none;
}
/* Spinner activé uniquement à l'approche du viewport : main.js §1 pose .is-spinning
   via un IntersectionObserver partagé. Sans ce garde, chaque image lazy jamais
   téléchargée (slides cachées des carrousels, cards hors écran) garderait une
   animation infinie active - à ~200 spinners, le main thread sature. */
.img-loading:not(.is-spinning)::after {
    display: none;
}
@keyframes img-spin {
    to {
        transform: rotate(360deg);
    }
}

/* Placeholder quand l'image échoue à charger. Également réutilisé TEL QUEL
   (span statique .img-loading.is-loaded.has-error) par render_image_placeholder()
   (functions.php) pour les vignettes SANS photo : les deux cas doivent rendre
   exactement pareil - ne pas diverger. */
.img-loading.has-error > img {
    opacity: 0;
}
.img-loading.has-error::before {
    content: "\f03e"; /* fa-image */
    /* Le projet embarque Font Awesome 6 Pro (cf. private/views/header.php), pas Free.
       Utiliser le mauvais nom de famille rendait le caractère privé U+F03E en "tofu". */
    font-family: "Font Awesome 6 Pro";
    font-weight: 900;
    position: absolute;
    top: 50%;
    left: 50%;
    transform: translate(-50%, -50%);
    color: #b0b0b0;
    font-size: 32px;
    pointer-events: none;
    z-index: 0;
}

/* Petit avatar (render_user_avatar, avatar de l'inbox .booking-card-avatar,
   vignette d'en-tête de conversation user-booking.php) : le spinner générique
   (28px) et l'icône d'erreur (32px) déborderaient d'un cadre de 40-52px -
   ramenés à l'échelle du composant. */
.avatar-circle.img-loading::after {
    width: 14px;
    height: 14px;
    margin: -7px 0 0 -7px;
    border-width: 2px;
}
.avatar-circle.img-loading.has-error::before {
    font-size: 16px;
}

/* Encart pub (render_ad) dont l'image est en erreur - fichier absent du serveur
   média : l'<img> cassée n'a quasiment pas de hauteur (le lien est en h-auto),
   le placeholder générique ci-dessus s'écrasait en barre plate. On impose un
   ratio de bannière au lien pour que le bloc gris + icône garde une taille
   décente. initImageLoading (main.js §1) pose .has-error sur le parent
   positionné de l'<img> = le <a> [data-ad-click] lui-même (render_ad émet
   l'<img> en enfant direct du lien ; le chemin <picture> n'existe que si le
   sibling .webp local existe - donc jamais en erreur). Concerne toutes les
   pages servies par render_ad : provider.php (sidebar + galerie overlay),
   blog-list, blog-single, list.php (sidebar desktop + bannière mobile). */
[data-ad-click].has-error {
    aspect-ratio: 3 / 2;
}

/* ============================================================
   Tooltip global (.app-tooltip - main.js §28)
   ------------------------------------------------------------
   Bulle unique injectée dans <body>, affichée au survol/focus des boutons et
   liens portant un `title` (transféré en data-tooltip par le JS - remplace le
   tooltip natif des boutons d'action à icône seule : Partager, favoris, croix
   de retrait…). position:fixed posée par le JS ; flèche centrée sur la CIBLE
   via --tooltip-arrow-x (la bulle peut être clampée au bord du viewport).
   z-index au-dessus des modales/viewers (z-80) et des barres flottantes (z-70) :
   un bouton à tooltip peut vivre dans une modale ouverte. */
.app-tooltip {
    position: fixed;
    z-index: 140;
    max-width: 260px;
    padding: 7px 11px;
    border-radius: 8px;
    background: #0A0A0A;      /* ink-900 */
    color: #fff;
    font-size: 12px;
    font-weight: 500;
    line-height: 1.35;
    pointer-events: none;     /* jamais un obstacle au curseur */
    opacity: 0;
    transform: translateY(3px);
    transition: opacity 120ms ease, transform 120ms ease;
}
.app-tooltip.is-visible {
    opacity: 1;
    transform: translateY(0);
}
.app-tooltip::after {
    content: "";
    position: absolute;
    top: 100%;
    left: var(--tooltip-arrow-x, 50%);
    margin-left: -5px;
    border: 5px solid transparent;
    border-top-color: #0A0A0A;
}
/* Bulle placée SOUS la cible (pas de place au-dessus) : flèche retournée. */
.app-tooltip.is-below {
    transform: translateY(-3px);
}
.app-tooltip.is-below.is-visible {
    transform: translateY(0);
}
.app-tooltip.is-below::after {
    top: auto;
    bottom: 100%;
    border-top-color: transparent;
    border-bottom-color: #0A0A0A;
}

/* Carte "suggestion" (régions / types de lieu / villes) - carré sur mobile et desktop */
.suggest-card {
    position: relative;
    isolation: isolate;
    flex-shrink: 0;
    display: flex;
    flex-direction: column;
    justify-content: flex-end;
    align-items: flex-start;
    width: 240px;
    height: 240px;
    padding: 12px;
    border-radius: 20px;
    overflow: hidden;
    scroll-snap-align: start;
}
/* Desktop : 5 cartes pleines + aperçu de la 6ème dans .container-x (1220px inner, gap 20px).
   5 × 210 + 5 × 20 = 1150 → reste ~70px pour amorcer la 6ème carte. */
@media (min-width: 1024px) {
    .suggest-card {
        width: 210px;
        height: 210px;
    }
}
.suggest-card-img {
    position: absolute;
    inset: 0;
    width: 100%;
    height: 100%;
    object-fit: cover;
    z-index: -1;
}
/* Badge = lien réel (SEO) dans un <h3> ; le ::after étend la zone cliquable à toute la carte.
   display: block requis : dans le <h3>, le lien n'est plus un flex item (max-width/ellipsis). */
.suggest-card-badge {
    display: block;
    position: static;
    max-width: 100%;
    background: #F4F4A8;   /* = accent (jaune unique) */
    color: #0a0a0a;
    font-size: 13px;
    font-weight: 700;
    padding: 4px 12px;
    border-radius: 8px;
    box-shadow: 0 1px 2px rgba(0, 0, 0, 0.08);
    white-space: nowrap;
    overflow: hidden;
    text-overflow: ellipsis;
    text-decoration: none;
    transition: background-color 0.18s ease, box-shadow 0.18s ease;
}
.suggest-card:hover .suggest-card-badge,
.suggest-card-badge:hover,
.suggest-card-badge:focus-visible {
    background: #D7D794;   /* = accent-hover (jaune unique assombri) */
    box-shadow: 0 3px 8px rgba(0, 0, 0, 0.14);
}
.suggest-card-badge::after {
    content: "";
    position: absolute;
    inset: 0;
    z-index: 1;
}
/* Variante multi-lignes (option badge_wrap de render_suggest_carousel) : badges
   qui portent une PHRASE et non un libellé court - cards « À chaque besoin, une
   solution » de la homepage (« Arbre de Noël dans une salle de spectacle »…),
   que l'ellipsis sur une ligne de la card 210px amputerait. 3 lignes max, puis
   ellipsis. Réservée aux carrousels qui en font la demande : les libellés
   courts (régions, villes, types de lieu) gardent la ligne unique. */
.suggest-card-badge--wrap {
    display: -webkit-box;
    -webkit-box-orient: vertical;
    -webkit-line-clamp: 3;
    line-clamp: 3;
    white-space: normal;
    line-height: 17px;
    padding: 6px 12px;
}
/* Description optionnelle (champ desc de render_suggest_carousel - cards
   « type de lieu » de la homepage) : texte blanc sous le badge, lisible sur
   photo ET sur le placeholder gris grâce au dégradé sombre bas de card.
   Le scrim suit l'img dans le DOM : même z-index négatif = peint au-dessus
   d'elle, sous le contenu statique (badge + texte). */
.suggest-card-scrim {
    position: absolute;
    left: 0;
    right: 0;
    bottom: 0;
    height: 62%;
    background: linear-gradient(to top, rgba(0, 0, 0, 0.62) 0%, rgba(0, 0, 0, 0.30) 55%, rgba(0, 0, 0, 0) 100%);
    z-index: -1;
    pointer-events: none;
}
.suggest-card-desc {
    margin-top: 6px;
    color: #fff;
    font-size: 12px;
    line-height: 1.35;
    font-weight: 500;
    text-shadow: 0 1px 2px rgba(0, 0, 0, 0.35);
    display: -webkit-box;
    -webkit-box-orient: vertical;
    -webkit-line-clamp: 3;
    line-clamp: 3;
    overflow: hidden;
}

/* Bouton arrondi navigation carrousel - masqué en mobile */
.nav-btn {
    width: 44px;
    height: 44px;
    border-radius: 9999px;
    background: #fff;
    box-shadow: 0 2px 10px rgba(0, 0, 0, 0.12);
    display: none;
    align-items: center;
    justify-content: center;
    color: #0a0a0a;
    transition: opacity 0.2s ease, box-shadow 0.2s ease;
}
@media (min-width: 768px) {
    .nav-btn {
        display: inline-flex;
    }
}
.nav-btn:not(:disabled):hover {
    box-shadow: 0 4px 16px rgba(0, 0, 0, 0.18);
}
.nav-btn:disabled {
    opacity: 0;
    pointer-events: none;
}

/* ---------- Media carousel (intra-card) ----------
   Carrousel d'images à l'intérieur d'une vignette de card. UX :
   - Desktop : flèches prev/next visibles au hover du carrousel, masquées
     quand on est au bord (attribut [hidden] posé par main.js section 13).
     Pas de dots affichés.
   - Mobile : pas de flèches, dots affichés en bas centre, swipe tactile
     natif via scroll-snap mandatory.

   Le wrapper parent direct DOIT être `position: relative` et `overflow: hidden`
   avec les dimensions souhaitées de la vignette : le .media-carousel s'y ancre
   en inset:0.

   Empilement (important) : la .media-carousel-track est en z-index:2 pour passer
   AU-DESSUS du .card-link::after (z-index:1) - c'est ce qui permet au swipe tactile
   natif de fonctionner même quand la photo est couverte par la zone cliquable de
   la card. Un click handler JS forward le tap simple vers le .card-link pour
   préserver la navigation.

   À ajouter via le helper PHP render_media_carousel() - voir private/functions.php. */
.media-carousel {
    position: absolute;
    inset: 0;
    z-index: 2;
}
.media-carousel-track {
    position: absolute;
    inset: 0;
    display: flex;
    overflow-x: auto;
    scroll-snap-type: x mandatory;
    overscroll-behavior-x: contain;
    -ms-overflow-style: none;
    scrollbar-width: none;
    /* pan-y : permet au scroll vertical de la page de passer quand le doigt
       commence sur la track ; le swipe horizontal est ensuite intercepté en JS
       (preventDefault sur touchmove non-passif) pour contrôler le snap. */
    touch-action: pan-y;
    /* Le tap sur la track forwarde au .card-link parent (cf. main.js section 13),
       donc on garde le curseur "main" pour signaler la nature cliquable, comme sur
       le reste de la card. Les boutons flèches override avec leur cursor:pointer natif. */
    cursor: pointer;
}
.media-carousel-track::-webkit-scrollbar {
    display: none;
}
/* Pendant le swipe JS et l'animation de snap, on désactive le scroll-snap natif
   pour éviter que le navigateur ne saute sur une slide pendant qu'on contrôle
   scrollLeft programmatiquement (sinon : conflit visible = micro-saccades). */
.media-carousel-track.is-dragging,
.media-carousel-track.is-snapping {
    scroll-snap-type: none;
}
.media-carousel-slide {
    /* position:relative pour servir de containing block au spinner .img-loading
       que la section 1 de main.js applique sur le parent de chaque <img>. */
    position: relative;
    flex: 0 0 100%;
    width: 100%;
    height: 100%;
    scroll-snap-align: start;
}
.media-carousel-slide img {
    -webkit-user-drag: none;
    user-select: none;
    -webkit-user-select: none;
}

/* Flèches prev/next - masquées par défaut, visibles au hover du carrousel.
   Posées au-dessus de la track (ordre DOM → empilement naturel). */
.media-carousel-nav {
    position: absolute;
    top: 50%;
    transform: translateY(-50%);
    width: 30px;
    height: 30px;
    border-radius: 9999px;
    background: rgba(255, 255, 255, 0.95);
    box-shadow: 0 2px 8px rgba(0, 0, 0, 0.18);
    display: flex;
    align-items: center;
    justify-content: center;
    color: #0a0a0a;
    opacity: 0;
    pointer-events: none;
    transition: opacity 0.18s ease, background-color 0.18s ease;
}
.media-carousel-nav--prev { left: 10px; }
.media-carousel-nav--next { right: 10px; }
.media-carousel-nav[hidden] {
    /* Indispensable : sinon le pseudo-bouton resterait dans le flux et le hover
       afficherait une opacité 1 sur un bouton "désactivé" (cf. fin de carrousel). */
    display: none !important;
}
.media-carousel-nav:hover {
    background: #fff;
}
/* Hover uniquement sur les périphériques qui ont un vrai survol (souris / trackpad).
   Sur tactile, le hover sticky ferait apparaître les flèches après le moindre tap. */
@media (hover: hover) and (pointer: fine) {
    .media-carousel:hover .media-carousel-nav:not([hidden]) {
        opacity: 1;
        pointer-events: auto;
    }
}

/* Dots de pagination - mobile uniquement (sur desktop, la nav se fait aux flèches).
   Au-delà de 7 photos, fenêtre glissante façon Instagram : la strip interne translateX
   pour garder le dot d'ancrage centré dans une fenêtre clipée de 7 dots, et les dots
   aux extrémités de la fenêtre rétrécissent (.is-edge-sm puis .is-edge-xs). Ces classes
   sont posées par main.js §13. Quand l'état actif tombe sur un edge, .is-active prime
   et le dot reste à pleine taille (cf. selecteur combiné plus bas). */
.media-carousel-dots {
    position: absolute;
    bottom: 12px;
    left: 50%;
    transform: translateX(-50%);
    pointer-events: none;
    overflow: hidden;
    /* 1px de chaque côté pour laisser respirer le `transform: scale(1.2)` du dot actif
       (qui étend 0.5px hors de sa box de 5px) quand il tombe au bord de la fenêtre -
       sinon le clip de `overflow: hidden` rogne le dot actif aux extrémités (idx = 0
       ou dernière photo). Padding box → l'`overflow` clippe à 67px au lieu de 65px. */
    padding: 0 1px;
}
/* Outer width 67px = content area 65px + padding 2px. Tailwind Preflight applique
   `box-sizing: border-box` partout, donc width inclut le padding - il faut bien 67px
   en outer pour que le content soit à 7 dots × 5 + 6 gaps × 5 = 65px. La strip interne
   (display:flex) garde sa largeur naturelle et est clipée par le parent dès count > 7. */
.media-carousel-dots.has-sliding-window {
    width: 67px;
}
.media-carousel-dots-strip {
    display: flex;
    align-items: center;
    gap: 5px;
    transition: transform 0.2s ease;
    will-change: transform;
}
.media-carousel-dot {
    flex: 0 0 auto;
    width: 5px;
    height: 5px;
    border-radius: 9999px;
    background: rgba(255, 255, 255, 0.7);
    transition: transform 0.2s ease, background-color 0.2s ease;
}
.media-carousel-dot.is-active {
    background: #fff;
    transform: scale(1.2);
}
/* Rétrécissement progressif aux bords de la fenêtre (uniquement côté qui a du contenu
   "au-delà" - JS gère le fadeLeft/fadeRight pour ne pas rétrécir le côté ancré). */
.media-carousel-dot.is-edge-sm {
    transform: scale(0.7);
}
.media-carousel-dot.is-edge-xs {
    transform: scale(0.4);
}
/* L'actif prime sur les classes de bord : si l'index courant est au bord de la fenêtre
   (galerie aux extrémités), le dot doit rester à pleine taille pour rester lisible. */
.media-carousel-dot.is-active.is-edge-sm,
.media-carousel-dot.is-active.is-edge-xs {
    transform: scale(1.2);
}
@media (hover: hover) and (pointer: fine) {
    .media-carousel-dots {
        display: none;
    }
}

/* Titre du hero sombre de la homepage : segment en dégradé de marque
   (rose → violet privé, bleu plein PRO - variables surchargées par pro.css). */
.hero-grad-text {
    background: linear-gradient(
        90deg,
        rgb(var(--hero-grad-from)),
        rgb(var(--hero-grad-to))
    );
    -webkit-background-clip: text;
    background-clip: text;
    color: transparent;
}

/* Barre de recherche compacte du hero sombre (render_venue_search, homepage +
   404) : placeholders Figma #757575 à 50% - medium 16px desktop, regular
   13px mobile (les tailles vivent dans le markup, seul le poids varie ici). */
.venue-search-bar .multiselect-value:not(.has-selection),
.venue-search-bar input::placeholder {
    color: rgb(117 117 117 / 0.5);
    font-weight: 500;
}
/* Pas de ring brand à l'ouverture du multiselect dans la barre (le liseré
   générique .multiselect-trigger[aria-expanded="true"] détonne sur les champs
   sans fond / barres blanches autonomes - il reste pour les multiselect
   standard de render_multiselect). */
.venue-search-bar .multiselect-trigger[aria-expanded="true"] {
    box-shadow: none;
}
@media (max-width: 767px) {
    .venue-search-bar .multiselect-value:not(.has-selection),
    .venue-search-bar input::placeholder {
        font-weight: 400;
    }
}

/* Side menu mobile : transitions + hauteur */
.mobile-drawer {
    transition: transform 0.3s ease-out;
    /* `dvh` suit la zone réellement visible : sans ça, `100vh` ignore la barre
       d'URL du navigateur mobile et le bas du menu (boutons CTA) passe dessous. */
    height: 100vh;
    height: 100dvh;
}
.mobile-overlay {
    transition: opacity 0.3s ease;
}

/* Container : aligné sur le header (max 1280, padding 15 mobile / 30 desktop) */
.container-x {
    max-width: 1280px;
    margin-left: auto;
    margin-right: auto;
    padding-left: 15px;
    padding-right: 15px;
}
@media (min-width: 768px) {
    .container-x {
        padding-left: 30px;
        padding-right: 30px;
    }
}

/* Bandeau labels - défilement infini lent en pleine largeur */
.marquee {
    overflow: hidden;
    width: 100%;
    -webkit-mask-image: linear-gradient(
        to right,
        transparent,
        #000 8%,
        #000 92%,
        transparent
    );
    mask-image: linear-gradient(
        to right,
        transparent,
        #000 8%,
        #000 92%,
        transparent
    );
}
.marquee-track {
    display: flex;
    flex-wrap: nowrap;
    width: max-content;
    animation: marquee 60s linear infinite;
    will-change: transform;
}
.marquee:hover .marquee-track {
    animation-play-state: paused;
}
@keyframes marquee {
    from {
        transform: translateX(0);
    }
    to {
        transform: translateX(-50%);
    }
}
@media (prefers-reduced-motion: reduce) {
    .marquee-track {
        animation: none;
    }
}
/* Variante « défilant sur mobile seulement » (réassurance du hero homepage) :
   dès md la rangée devient statique - plus de masque ni d'animation, et la
   série dupliquée pour la boucle ([data-marquee-dup]) est retirée. */
@media (min-width: 768px) {
    .marquee-md-static {
        -webkit-mask-image: none;
        mask-image: none;
    }
    .marquee-md-static .marquee-track {
        animation: none;
        width: auto;
    }
    .marquee-md-static [data-marquee-dup] {
        display: none;
    }
}

/* Boîte de logo partenaire (ruban .marquee de la home / abonnements / groupes,
   vignette des cards de /nos-partenaires) : la boîte porte une taille FIXE et
   l'<img> s'y contient (max-h-full / max-w-full + object-contain), pour que des
   visuels de ratios quelconques gardent un rythme régulier.
   render_media_picture() insère un <picture> entre les deux quand un sibling
   .webp existe - or un max-height/max-width en % ne se résout PAS sur un parent
   à dimension auto : le logo échapperait à sa boîte sur les seules images ayant
   une variante WebP. `display: contents` efface ce <picture> de la mise en page
   (la sélection de source, elle, reste faite par le parser) : l'<img> redevient
   enfant direct de la boîte dimensionnée. Ne pas remplacer par un max-h-[…px]
   en dur - la boîte change de taille au breakpoint. */
.partner-logo picture {
    display: contents;
}

/* ===========================================================
   Multiselect custom (champ Événement)
   Desktop : dropdown ancré au champ
   Mobile  : bottom-sheet plein largeur, animé depuis le bas
   =========================================================== */

.multiselect-trigger {
    cursor: pointer;
}
.multiselect-trigger[aria-expanded="true"] {
    box-shadow: 0 0 0 2px rgb(var(--brand) / 0.3);
}
.multiselect[data-open] [data-chevron] {
    transform: translateY(-50%) rotate(180deg);
}
.multiselect-value:not(.has-selection) {
    color: #757575;
}
.multiselect-value.has-selection {
    color: #211315;
    font-weight: 500;
}

/* Panneau - desktop par défaut (dropdown ancré au trigger via JS en position fixed
   pour échapper à tout ancêtre `overflow: hidden`). Les coordonnées top/left/width
   sont définies par le JS en style inline à l'ouverture et lors du scroll/resize.
   Le panel est lui-même le flex container : son layout est prédictible quelle
   que soit la hauteur (max-height fixée en JS desktop, 85vh fixe en mobile). */
.multiselect-panel {
    position: fixed;
    z-index: 60;
    background: #fff;
    border-radius: 12px;
    box-shadow: 0 12px 32px rgba(0, 0, 0, 0.18), 0 2px 8px rgba(0, 0, 0, 0.08);
    overflow: hidden;
    display: flex;
    flex-direction: column;
    animation: multiselect-fade-in 0.18s ease-out;
}
.multiselect-panel.hidden {
    display: none;
}
@keyframes multiselect-fade-in {
    from {
        opacity: 0;
        transform: translateY(-4px);
    }
    to {
        opacity: 1;
        transform: translateY(0);
    }
}

/* En-tête (titre + bouton fermer) - masqué desktop */
.multiselect-header {
    display: none;
    padding: 16px 16px 8px;
    border-bottom: 1px solid #f2f3f5;
}
.multiselect-header-row {
    display: flex;
    align-items: center;
    justify-content: space-between;
}
.multiselect-title {
    font-size: 17px;
    font-weight: 700;
    color: #0a0a0a;
}
.multiselect-close {
    width: 36px;
    height: 36px;
    border-radius: 999px;
    display: inline-flex;
    align-items: center;
    justify-content: center;
    color: #211315;
    transition: background-color 0.15s ease;
}
.multiselect-close:hover {
    background: #f2f3f5;
}

/* Recherche en haut du panneau */
.multiselect-search {
    position: relative;
    display: flex;
    align-items: center;
    gap: 8px;
    padding: 12px 12px 8px;
}
.multiselect-search > i {
    position: absolute;
    left: 22px;
    top: 50%;
    transform: translateY(-50%);
    margin-top: 2px; /* compense le padding asymétrique 12/8 du conteneur pour centrer sur l'input */
    pointer-events: none;
}
.multiselect-search input {
    flex: 1;
    /* min-width: 0 force l'input à respecter `flex: 1` (sinon `min-width: auto`
       - la valeur par défaut des items flex - bloque le shrink à la taille
       intrinsèque du champ, soit ~150-200px, et l'input déborde du panel
       quand le trigger est étroit). max-width: 100% en ceinture-bretelles. */
    min-width: 0;
    max-width: 100%;
    height: 40px;
    padding: 0 36px 0 34px;
    font-size: 14px;
    color: #211315;
    background: #f2f3f5;
    border: 1px solid transparent;
    border-radius: 10px;
    outline: none;
    transition: border-color 0.15s ease, background-color 0.15s ease;
}
.multiselect-search input::placeholder {
    color: #757575;
}
.multiselect-search input:focus {
    background: #fff;
    border-color: rgb(var(--brand));
}
.multiselect-search-clear {
    position: absolute;
    right: 18px;
    top: 50%;
    transform: translateY(-50%);
    margin-top: 2px; /* compense le padding asymétrique 12/8 du conteneur pour centrer sur l'input */
    width: 22px;
    height: 22px;
    border-radius: 999px;
    background: #d9d9d9;
    color: #fff;
    display: inline-flex;
    align-items: center;
    justify-content: center;
    transition: background-color 0.15s ease;
}
.multiselect-search-clear:hover {
    background: #757575;
}
.multiselect-search-clear[hidden] {
    display: none;
}

/* Liste des options */
.multiselect-options {
    list-style: none;
    padding: 4px 6px 8px;
    margin: 0;
    overflow-y: auto;
    flex: 1 1 auto;
    min-height: 0; /* permet à flex-1 d'effectivement rétrécir et faire scroller */
    -webkit-overflow-scrolling: touch;
    overscroll-behavior: contain;
}
.multiselect-options::-webkit-scrollbar {
    width: 6px;
}
.multiselect-options::-webkit-scrollbar-thumb {
    background: #d9d9d9;
    border-radius: 999px;
}
.multiselect-options li.is-hidden {
    display: none;
}
/* Optgroup header dans un multiselect - élément décoratif non sélectionnable
   (généré par render_multiselect quand une option est un sous-tableau). */
.multiselect-group-header {
    padding: 10px 12px 4px;
    font-size: 11px;
    font-weight: 700;
    text-transform: uppercase;
    letter-spacing: 0.04em;
    color: #757575;
    pointer-events: none;
    user-select: none;
}
/* Options à l'intérieur d'un optgroup : indentation pour la hiérarchie visuelle.
   Classe posée explicitement par render_multiselect (préfèrable aux sélecteurs
   siblings complexes - robustesse si la liste contient plusieurs groups + options
   hors groupes mélangés). */
.multiselect-option--grouped {
    padding-left: 22px;
}
.multiselect-option {
    position: relative; /* ancre l'input masqué (abspos) dans la rangée - cf. .filters-check */
    display: flex;
    align-items: center;
    gap: 12px;
    padding: 10px 12px;
    border-radius: 8px;
    font-size: 15px;
    color: #211315;
    cursor: pointer;
    transition: background-color 0.12s ease;
    user-select: none;
}
.multiselect-option:hover {
    background: #f7f7f8;
}
/* Option INDISPONIBLE : valeur déjà utilisée ailleurs (une ligne existante la
   porte déjà), posée par le JS qui `disabled` la checkbox - le clic sur le
   label n'a donc plus d'effet, le composant n'écoutant que le `change` natif.
   La classe ne fait que le rendre lisible : grisé, sans survol, curseur barré. */
.multiselect-option.is-disabled {
    color: #c9c9cf;
    cursor: not-allowed;
}
.multiselect-option.is-disabled:hover {
    background: transparent;
}
.multiselect-option.is-disabled .multiselect-check {
    opacity: 0.4;
}
.multiselect-option input[type="checkbox"] {
    position: absolute;
    opacity: 0;
    width: 0;
    height: 0;
    pointer-events: none;
}
.multiselect-check {
    flex-shrink: 0;
    width: 20px;
    height: 20px;
    border-radius: 6px;
    border: 1.5px solid #d9d9d9;
    background: #fff;
    display: inline-flex;
    align-items: center;
    justify-content: center;
    color: #fff;
    transition: background-color 0.15s ease, border-color 0.15s ease;
}
.multiselect-check::after {
    content: "\f00c"; /* fa-check */
    /* Le projet embarque Font Awesome 6 Pro (cf. private/views/header.php :
       solid.min.css déclare `font-family: "Font Awesome 6 Pro"`). Cibler "Free"
       affichait un tofu (U+F00C non résolu car la famille Free n'est pas chargée). */
    font-family: "Font Awesome 6 Pro";
    font-weight: 900;
    font-size: 11px;
    opacity: 0;
    transition: opacity 0.12s ease;
}
.multiselect-option input:checked + .multiselect-check {
    background: rgb(var(--brand));
    border-color: rgb(var(--brand));
}
.multiselect-option input:checked + .multiselect-check::after {
    opacity: 1;
}
.multiselect-option input:focus-visible + .multiselect-check {
    box-shadow: 0 0 0 3px rgb(var(--brand) / 0.25);
}
.multiselect-option .multiselect-label {
    flex: 1;
    min-width: 0;
    line-height: 1.3;
}

/* ----------- Mode mono-sélection (data-mode="single") -----------
   Visuel radio (cercle au lieu de carré coché) + footer caché
   (la sélection ferme le panel immédiatement, pas besoin de bouton Valider). */
.multiselect[data-mode="single"] .multiselect-check {
    border-radius: 999px;
}
.multiselect[data-mode="single"] .multiselect-check::after {
    content: "";
    width: 8px;
    height: 8px;
    border-radius: 999px;
    background: #fff;
    font-size: 0;
}
.multiselect[data-mode="single"] .multiselect-footer {
    display: none;
}

/* ----------- Autocomplete (data-autocomplete) -----------
   Réutilise toute la chrome `.multiselect-*` (panel, header, options, empty).
   Spécifique : chaque option est un <button> plein largeur (pas de checkbox)
   + barre de recherche interne visible uniquement sur mobile (sur desktop
   c'est l'input principal qui sert de recherche). */
.autocomplete-option {
    display: block;
    width: 100%;
    text-align: left;
    padding: 10px 12px;
    border-radius: 8px;
    font-size: 15px;
    color: #211315;
    background: transparent;
    transition: background-color 0.12s ease, color 0.12s ease;
    cursor: pointer;
    line-height: 1.3;
}
.autocomplete-option:hover,
.autocomplete-option.is-active {
    background: #f7f7f8;
}
.autocomplete-option.is-selected {
    color: rgb(var(--brand));
    font-weight: 600;
}
/* Mise en exergue du fragment recherché */
.autocomplete-option mark {
    background: transparent;
    color: inherit;
    font-weight: 700;
}
/* Label + sous-texte (meta) - rendu utilisé par initAutocomplete quand l'item AJAX
   fournit un champ `meta` (ex. région pour les villes). */
.autocomplete-option-label {
    display: block;
    line-height: 1.25;
}
.autocomplete-option-meta {
    display: block;
    font-size: 12px;
    color: #757575;
    font-weight: 400;
    margin-top: 1px;
}
.autocomplete-option.is-selected .autocomplete-option-meta {
    color: rgb(var(--brand) / 0.7);
}
/* La barre de recherche interne ne sert qu'à l'overlay mobile fullscreen */
.autocomplete-search-mobile {
    display: none;
}
/* Bouton retour (flèche) intégré dans la top bar mobile fullscreen - masqué desktop */
.autocomplete-back {
    display: none;
    flex-shrink: 0;
    width: 36px;
    height: 36px;
    border-radius: 999px;
    align-items: center;
    justify-content: center;
    color: #211315;
    transition: background-color 0.15s ease;
}
.autocomplete-back:hover {
    background: #f2f3f5;
}
@media (max-width: 767px) {
    .autocomplete-search-mobile {
        display: flex;
    }
    .autocomplete-back {
        display: inline-flex;
    }
    .autocomplete-option {
        padding: 14px 12px;
        font-size: 16px;
    }
}

/* États messages : prompt (invite à saisir) / empty / loading / error.
   Tous styles communs : centrés, gris. `min-height` assure que le panel a une
   vraie zone à centrer verticalement (sinon le contenu colle au haut). */
.multiselect-empty,
.multiselect-loading,
.multiselect-prompt {
    padding: 16px;
    text-align: center;
    font-size: 14px;
    color: #757575;
    display: flex;
    align-items: center;
    justify-content: center;
    gap: 8px;
}
/* Restaure l'effet de l'attribut [hidden] : sans cette règle, le `display: flex`
   ci-dessus a une spécificité supérieure à la règle UA `*[hidden]{display:none}`,
   et tous les états restent visibles simultanément. Idem pour `.multiselect-options`
   (qu'on cache via [hidden] quand aucune option n'est visible). */
.multiselect-empty[hidden],
.multiselect-loading[hidden],
.multiselect-prompt[hidden],
.multiselect-options[hidden] {
    display: none;
}
/* Icône loupe du prompt : masquée sur desktop (texte seul, plus épuré),
   ré-affichée en grand au-dessus du texte sur mobile (voir media query). */
.multiselect-prompt > i {
    display: none;
}
/* Spinner - sur desktop il est inline (petit, à gauche du texte) ; sur mobile
   fullscreen, on l'agrandit et on l'empile au-dessus du texte (voir media query). */
.multiselect-spinner {
    display: inline-block;
    width: 14px;
    height: 14px;
    border: 2px solid #d9d9d9;
    border-top-color: rgb(var(--brand));
    border-radius: 50%;
    animation: img-spin 0.8s linear infinite;
    flex-shrink: 0;
}

/* Footer : compteur + actions */
.multiselect-footer {
    display: flex;
    align-items: center;
    justify-content: space-between;
    gap: 12px;
    padding: 10px 12px;
    border-top: 1px solid #f2f3f5;
    background: #fff;
}
.multiselect-reset {
    font-size: 14px;
    font-weight: 500;
    color: #757575;
    padding: 8px 10px;
    border-radius: 8px;
    transition: color 0.15s ease, background-color 0.15s ease;
}
.multiselect-reset:hover {
    color: #211315;
    background: #f2f3f5;
}
.multiselect-reset:disabled {
    opacity: 0.45;
    pointer-events: none;
}
.multiselect-validate {
    flex: 0 0 auto;
    min-width: 120px;
    height: 40px;
    padding: 0 18px;
    background: rgb(var(--brand));
    color: #fff;
    font-weight: 700;
    font-size: 14px;
    border-radius: 10px;
    transition: background-color 0.15s ease;
}
.multiselect-validate:hover {
    background: rgb(var(--brand-dark));
}

/* ----------- Mobile : bottom-sheet plein largeur -----------
   `--kb-inset` = encart du clavier virtuel, posé/retiré en style inline par
   createKeyboardInsetWatcher (main.js §9bis) tant qu'un panel est ouvert : le
   sheet remonte au-dessus du clavier (bottom) et sa hauteur est plafonnée à la
   zone réellement visible - sinon le clavier recouvre le bas (footer Valider,
   dernières options) sans que le scroll interne puisse les révéler. */
@media (max-width: 767px) {
    /* !important pour écraser les styles inline (top/left/width) placés par le JS desktop */
    .multiselect-panel {
        position: fixed !important;
        top: auto !important;
        left: 0 !important;
        right: 0 !important;
        bottom: var(--kb-inset, 0px) !important;
        width: auto !important;
        max-height: 85vh;
        max-height: min(85vh, calc(100dvh - var(--kb-inset, 0px)));
        border-radius: 20px 20px 0 0;
        box-shadow: 0 -8px 32px rgba(0, 0, 0, 0.24);
        animation: multiselect-slide-up 0.25s cubic-bezier(0.22, 1, 0.36, 1);
        z-index: 60;
    }
    .multiselect-header {
        display: block;
    }
    .multiselect-options {
        padding-bottom: 16px;
    }
    .multiselect-footer {
        padding: 12px 16px calc(12px + env(safe-area-inset-bottom, 0px));
        box-shadow: 0 -1px 0 #f2f3f5;
    }
    .multiselect-option {
        padding: 14px 12px;
        font-size: 16px;
    }
    .multiselect-check {
        width: 24px;
        height: 24px;
    }
    .multiselect-check::after {
        font-size: 13px;
    }
    .multiselect-search input {
        height: 44px;
        font-size: 16px; /* évite le zoom iOS au focus */
    }
}
@keyframes multiselect-slide-up {
    from {
        transform: translateY(100%);
    }
    to {
        transform: translateY(0);
    }
}

/* ----------- Mobile : autocomplete en overlay FULLSCREEN -----------
   À la différence du multiselect (bottom-sheet 85vh, border-radius en haut),
   l'autocomplete prend 100% de la page : top bar avec champ texte + flèche
   retour, zone résultats occupant le reste du viewport. Cela donne plus de
   place pour la liste et évite que le clavier mobile chevauche les options. */
@media (max-width: 767px) {
    /* `--kb-inset` (encart clavier, cf. createKeyboardInsetWatcher main.js §9bis) :
       la hauteur se rétrécit à la zone visible au-dessus du clavier, sinon le bas
       de la liste est recouvert et inatteignable au scroll (top: 0 + height posés
       → le bottom hérité du bloc générique est ignoré, bloc sur-contraint). */
    [data-autocomplete] .multiselect-panel {
        top: 0 !important;
        left: 0 !important;
        right: 0 !important;
        bottom: 0 !important;
        width: 100% !important;
        height: calc(100vh - var(--kb-inset, 0px)) !important;
        height: calc(100dvh - var(--kb-inset, 0px)) !important;
        max-height: none !important;
        border-radius: 0 !important;
        box-shadow: none;
        animation: autocomplete-fullscreen-in 0.18s ease-out;
    }
    /* En fullscreen, le header (grip + titre) est masqué : la top bar EST la search row. */
    [data-autocomplete] .multiselect-header {
        display: none;
    }
    /* Top bar : flèche retour + input + clear, séparée des résultats par un trait. */
    [data-autocomplete] .autocomplete-search-mobile {
        padding: 10px 12px;
        border-bottom: 1px solid #f2f3f5;
        gap: 8px;
    }
    /* L'icône loupe est superflue sur mobile (la flèche retour suffit comme repère visuel). */
    [data-autocomplete] .autocomplete-search-mobile > i.fa-magnifying-glass {
        display: none;
    }
    [data-autocomplete] .autocomplete-search-mobile input {
        padding-left: 14px; /* plus de loupe → on retire le décalage gauche prévu pour elle */
        padding-right: 14px; /* clear n'est plus absolute → plus besoin de réserver 36px à droite */
    }
    /* Sur mobile, le clear devient un flex item normal pour être centré verticalement
       par `align-items: center` du parent (le `top: 50%` absolu peut dériver quand la
       flèche retour étire la hauteur du conteneur). */
    [data-autocomplete] .autocomplete-search-mobile .multiselect-search-clear {
        position: static;
        transform: none;
        flex-shrink: 0;
    }
    /* Zone résultats : occupe tout l'espace, padding latéral généreux. */
    [data-autocomplete] .multiselect-options {
        padding: 8px 12px calc(16px + env(safe-area-inset-bottom, 0px));
    }
    /* États centrés verticalement dans la zone résultats (le panel est flex column,
       on prend toute la hauteur restante via flex: 1). */
    [data-autocomplete] .multiselect-empty,
    [data-autocomplete] .multiselect-loading,
    [data-autocomplete] .multiselect-prompt {
        flex: 1 1 auto;
        flex-direction: column;
        padding: 48px 32px;
        font-size: 15px;
        line-height: 1.45;
        gap: 14px;
    }
    /* Spinner mobile : version grosse au-dessus du texte */
    [data-autocomplete] .multiselect-spinner {
        width: 36px;
        height: 36px;
        border-width: 3px;
    }
    /* Icône du prompt en grand au-dessus du texte (réactivée vs desktop où elle est masquée) */
    [data-autocomplete] .multiselect-prompt > i {
        display: block;
        font-size: 32px;
        color: #d9d9d9;
        margin-bottom: 4px;
    }
}
@keyframes autocomplete-fullscreen-in {
    from {
        opacity: 0;
        transform: translateY(6px);
    }
    to {
        opacity: 1;
        transform: translateY(0);
    }
}

/* Overlay assombri (mobile uniquement) */
.multiselect-overlay {
    position: fixed;
    inset: 0;
    background: rgba(10, 10, 10, 0.45);
    z-index: 55;
    opacity: 0;
    pointer-events: none;
    transition: opacity 0.25s ease;
}
.multiselect-overlay.is-visible {
    opacity: 1;
    pointer-events: auto;
}
@media (min-width: 768px) {
    .multiselect-overlay {
        display: none;
    }
}

@media (prefers-reduced-motion: reduce) {
    .multiselect-panel,
    .multiselect-overlay {
        animation: none;
        transition: none;
    }
}

/* ===========================================================
   Datepicker custom (data-datepicker)
   Réutilise la chrome `.multiselect-panel` (positionné en fixed par le JS,
   bottom-sheet sur mobile). Styles spécifiques à la grille calendrier et à
   la vue mois/année ci-dessous.
   =========================================================== */

.datepicker-panel {
    /* Largeur minimum desktop pour que la grille 7×6 reste lisible même quand
       le trigger est étroit. Le positioner JS posera width=trigger.width,
       on garantit un plancher pour les petits champs. */
    min-width: 320px;
    /* La chrome partagée `.multiselect-panel` est en `overflow: hidden` - ce qui
       convient à une liste d'options, qui porte son propre scroll interne. Le
       calendrier, lui, n'en a aucun : dès que le positionneur bornait la hauteur
       (trigger près du bas du viewport), la dernière semaine était COUPÉE et
       inatteignable. Le seuil de bascule du datepicker est monté à 380 px côté
       JS ; ce scroll est la ceinture de sécurité pour les petites fenêtres où
       aucun des deux côtés ne peut afficher le mois entier. */
    overflow-y: auto;
}

.datepicker-view.hidden {
    display: none;
}

/* Barre de navigation : prev / titre cliquable / next */
.datepicker-nav {
    display: flex;
    align-items: center;
    justify-content: space-between;
    gap: 4px;
    padding: 12px 12px 6px;
}

.datepicker-nav-btn {
    flex-shrink: 0;
    width: 32px;
    height: 32px;
    border-radius: 999px;
    display: inline-flex;
    align-items: center;
    justify-content: center;
    color: #211315;
    background: transparent;
    transition: background-color 0.15s ease, color 0.15s ease;
}

.datepicker-nav-btn:hover:not(:disabled) {
    background: #f2f3f5;
}

.datepicker-nav-btn:disabled {
    opacity: 0.3;
    pointer-events: none;
}

.datepicker-month-trigger {
    flex: 1;
    min-width: 0;
    display: inline-flex;
    align-items: center;
    justify-content: center;
    gap: 6px;
    height: 32px;
    padding: 0 12px;
    border-radius: 8px;
    font-size: 14px;
    font-weight: 600;
    color: #211315;
    background: transparent;
    transition: background-color 0.15s ease;
}

.datepicker-month-trigger:hover {
    background: #f2f3f5;
}

.datepicker-month-trigger > span {
    overflow: hidden;
    text-overflow: ellipsis;
    white-space: nowrap;
}

/* Ligne des libellés de jours (L M M J V S D) */
.datepicker-weekdays {
    display: grid;
    grid-template-columns: repeat(7, 1fr);
    padding: 4px 12px 2px;
    text-align: center;
    font-size: 11px;
    font-weight: 600;
    color: #757575;
    text-transform: uppercase;
    letter-spacing: 0.02em;
}

.datepicker-weekdays > span {
    padding: 6px 0;
}

/* Grille des jours du mois - 7×6 = 42 cellules */
.datepicker-grid {
    display: grid;
    grid-template-columns: repeat(7, 1fr);
    gap: 2px;
    padding: 0 12px 8px;
}

.datepicker-day {
    aspect-ratio: 1;
    min-height: 36px;
    display: flex;
    align-items: center;
    justify-content: center;
    border-radius: 8px;
    font-size: 14px;
    color: #211315;
    background: transparent;
    cursor: pointer;
    transition: background-color 0.12s ease, color 0.12s ease;
    position: relative;
}

.datepicker-day:hover:not(.is-disabled):not(.is-selected) {
    background: #f7f7f8;
}

/* Jours du mois précédent / suivant : gris pâle, cliquables (font basculer le mois) */
.datepicker-day.is-outside {
    color: #c9c9c9;
}

/* Aujourd'hui : cercle bordé brand (réservé aux jours non-sélectionnés ;
   si aujourd'hui est aussi sélectionné, .is-selected prend le dessus) */
.datepicker-day.is-today:not(.is-selected) {
    color: rgb(var(--brand));
    font-weight: 700;
    box-shadow: inset 0 0 0 1.5px rgb(var(--brand));
}

/* Sélection : fond plein brand */
.datepicker-day.is-selected {
    background: rgb(var(--brand));
    color: #fff;
    font-weight: 700;
}

.datepicker-day.is-selected:hover {
    background: rgb(var(--brand-dark));
}

/* Hors plage min/max : très pâle, non-cliquable */
.datepicker-day.is-disabled {
    color: #d9d9d9;
    cursor: not-allowed;
    background: transparent;
}

.datepicker-day:focus-visible {
    outline: none;
    box-shadow: 0 0 0 2px rgb(var(--brand) / 0.35);
}

/* Vue "mois/année" : grille 4×3 des 12 mois */
.datepicker-months-grid {
    display: grid;
    grid-template-columns: repeat(3, 1fr);
    gap: 6px;
    padding: 8px 12px 12px;
}

.datepicker-month-cell {
    height: 48px;
    border-radius: 10px;
    font-size: 14px;
    font-weight: 500;
    color: #211315;
    background: #f7f7f8;
    transition: background-color 0.12s ease, color 0.12s ease;
    cursor: pointer;
}

.datepicker-month-cell:hover:not(.is-disabled):not(.is-selected) {
    background: #e8e9ec;
}

.datepicker-month-cell.is-current:not(.is-selected) {
    box-shadow: inset 0 0 0 1.5px rgb(var(--brand));
    color: rgb(var(--brand));
}

.datepicker-month-cell.is-selected {
    background: rgb(var(--brand));
    color: #fff;
    font-weight: 700;
}

.datepicker-month-cell.is-disabled {
    opacity: 0.35;
    cursor: not-allowed;
}

/* Footer : actions Effacer / Aujourd'hui */
.datepicker-footer {
    display: flex;
    align-items: center;
    justify-content: space-between;
    gap: 12px;
    padding: 10px 12px;
    border-top: 1px solid #f2f3f5;
    background: #fff;
}

.datepicker-action {
    flex: 0 0 auto;
    height: 38px;
    padding: 0 16px;
    border-radius: 8px;
    font-size: 14px;
    font-weight: 500;
    transition: background-color 0.15s ease, color 0.15s ease;
}

.datepicker-action--secondary {
    color: #757575;
    background: transparent;
}

.datepicker-action--secondary:hover {
    color: #211315;
    background: #f2f3f5;
}

.datepicker-action--primary {
    color: rgb(var(--brand));
    background: rgb(var(--brand) / 0.08);
    font-weight: 600;
}

.datepicker-action--primary:hover {
    background: rgb(var(--brand) / 0.16);
}

/* ----------- Mobile : datepicker en bottom-sheet (90vh) -----------
   Les overrides de .multiselect-panel mobile (position fixed bottom 0 etc.)
   s'appliquent déjà via la classe .multiselect-panel. On adapte juste la
   taille des cellules pour qu'elles soient confortablement tappables. */
@media (max-width: 767px) {
    .datepicker-panel {
        min-width: 0;
        max-height: 90vh !important;
    }
    .datepicker-nav {
        padding: 14px 16px 8px;
    }
    .datepicker-nav-btn {
        width: 40px;
        height: 40px;
    }
    .datepicker-month-trigger {
        height: 40px;
        font-size: 16px;
    }
    .datepicker-weekdays {
        padding: 6px 12px 4px;
        font-size: 12px;
    }
    .datepicker-grid {
        gap: 4px;
        padding: 0 12px 12px;
    }
    .datepicker-day {
        min-height: 44px;
        font-size: 16px;
        border-radius: 10px;
    }
    .datepicker-months-grid {
        gap: 8px;
        padding: 12px 16px 16px;
    }
    .datepicker-month-cell {
        height: 56px;
        font-size: 15px;
    }
    .datepicker-footer {
        padding: 12px 16px calc(12px + env(safe-area-inset-bottom, 0px));
        box-shadow: 0 -1px 0 #f2f3f5;
    }
    .datepicker-action {
        height: 44px;
        font-size: 15px;
    }
}

/* ===========================================================
   SOS - Hero badge (étiquette jaune "Plus de 500 événements...")
   Posée en absolu sur le bas de la mosaïque desktop ;
   inline en flux normal sur mobile via .sos-hero-badge--mobile.
   =========================================================== */
.sos-hero-badge {
    display: inline-flex;
    align-items: center;
    background: #F4F4A8;   /* = accent (jaune unique) */
    color: #0a0a0a;
    font-size: 15px;
    font-weight: 500;
    padding: 7px 15px;
    border-radius: 8px;
    box-shadow: 0 2px 15px rgba(0, 0, 0, 0.10);
    white-space: nowrap;
}
.sos-hero-mosaic .sos-hero-badge {
    position: absolute;
    top: -18px;
    left: -15px;
    transform: rotate(-4deg);
    transform-origin: center;
    display: none;
}
@media (min-width: 1024px) {
    .sos-hero-mosaic .sos-hero-badge {
        display: inline-flex;
    }
}
.sos-hero-badge--mobile {
    font-size: 14px;
    padding: 6px 13px;
}
@media (min-width: 1024px) {
    .sos-hero-badge--mobile {
        display: none;
    }
}

/* ===========================================================
   Floating labels (form-floating)
   ----------------------------------------------------------
   Opt-in : appliquer la classe `form-floating` sur un ancêtre
   (typiquement le <form> de sos.php). Hors `.form-floating`,
   les classes `field-wrap` et `field-label` sont neutres et
   le rendu reste identique au look classique label-au-dessus.

   Hooks de détection de l'état "rempli" (CSS pur, sans JS) :
   - input natif / textarea / autocomplete  → :not(:placeholder-shown)
     (les champs doivent avoir un attribut placeholder, ne serait-ce
      qu'un espace, pour que :placeholder-shown s'évalue correctement)
   - champ auto-rempli par le navigateur → :-webkit-autofill / :autofill
     (au chargement, l'autofill Chrome pose une valeur "preview" qui ne
      déclenche PAS :placeholder-shown tant que l'utilisateur n'a pas
      interagi avec la page → sans ce hook, le label resterait couché
      sur la valeur préremplie - cas vu sur l'email de login.php)
   - multiselect  → .multiselect-value.has-selection (ajoutée par main.js)
   - datepicker   → [data-datepicker-display].has-selection (idem)

   Cascade : on utilise :where() pour neutraliser la spécificité de la
   liste flotté (qui contient des :has() coûteux en classes), afin que
   la règle "focus → couleur brand" l'emporte sur la couleur du flotté
   sans avoir à surenchérir artificiellement.
   =========================================================== */

.form-floating .field-wrap {
    position: relative;
    gap: 0;
}

/* Empêche les cellules d'une rangée de s'étirer à la hauteur de leur voisine
   (cas typique : message d'erreur `.field-error` - EN FLUX - qui rallonge un
   wrap → sans ça, la cellule voisine s'étire aussi et casse l'alignement des
   champs de la rangée).
   On le pose sur la grille parent plutôt que sur `.field-wrap` directement,
   car `align-self: start` sur un flex item de flex-col (cas du textarea
   `.field-wrap` enfant direct du `<form class="form-floating ... flex-col">`)
   bloque le width à 100% et empêche le champ de prendre toute la largeur. */
.form-floating .grid > .field-wrap {
    align-self: start;
}

.form-floating .field-label {
    position: absolute;
    /* Ancre FIXE = mi-hauteur des champs floating (56px, cf. règle plus bas) :
       un top en % se recentrerait sur le wrap entier quand le .field-error
       EN FLUX le rallonge, décalant le label hors de l'input. */
    top: 28px;
    left: 12px;
    transform: translateY(-50%);
    z-index: 1;
    margin: 0;
    padding: 0 4px;
    background: #fff;
    color: #757575;
    font-size: 14px;
    font-weight: 400;
    /* line-height >= 1.3 pour ne pas couper les descendantes (g, j, p, q, y)
       avec overflow: hidden (utilisé pour text-overflow: ellipsis sur les
       libellés longs). */
    line-height: 1.3;
    pointer-events: none;
    transition: top 180ms ease, font-size 180ms ease,
                font-weight 180ms ease, color 180ms ease;
    max-width: calc(100% - 28px);
    white-space: nowrap;
    overflow: hidden;
    text-overflow: ellipsis;
}

/* Hauteur 56px pour les champs principaux (vs 46px du style classique).
   Cibles précises : trigger custom + input direct enfant du wrap (ou de
   son sous-wrap `.relative` quand il y a une icône). On évite ainsi de
   toucher l'input de recherche `[data-multiselect-search]` qui vit dans
   le panel. */
.form-floating .field-wrap > .multiselect > .multiselect-trigger,
.form-floating .field-wrap > .multiselect > input[data-autocomplete-input],
.form-floating .field-wrap > input:not([type="hidden"]):not([type="checkbox"]),
.form-floating .field-wrap > .relative > input:not([type="hidden"]):not([type="checkbox"]) {
    height: 56px;
}

.form-floating .field-wrap > textarea {
    padding-top: 26px;
}

/* Placeholder masqué au repos, révélé au focus. Le label fait l'invite au
   repos ; le placeholder sert d'exemple quand on tape (ex. "ex. 80"). */
.form-floating .field-wrap > input::placeholder,
.form-floating .field-wrap > .relative > input::placeholder,
.form-floating .field-wrap > .multiselect > input[data-autocomplete-input]::placeholder,
.form-floating .field-wrap > textarea::placeholder {
    color: transparent;
    transition: color 150ms ease;
}
.form-floating .field-wrap:focus-within > input::placeholder,
.form-floating .field-wrap:focus-within > .relative > input::placeholder,
.form-floating .field-wrap:focus-within > .multiselect > input[data-autocomplete-input]::placeholder,
.form-floating .field-wrap:focus-within > textarea::placeholder {
    color: #757575;
}

/* Cas textarea : au repos, label ancré en haut (pas centré verticalement).
   Placé AVANT la règle "flotté" pour que l'ordre de cascade favorise le
   flotté quand son sélecteur match (spécificités égales 0,3,0 via :where). */
.form-floating .field-wrap:where(:has(> textarea)) .field-label {
    top: 22px;
}

/* État flotté : focus OU rempli OU panneau ouvert. Le label monte en haut
   du champ (translateY(-50%) chevauche la bordure → effet "notch" propre,
   le fond blanc du label masque la portion de bordure derrière le texte).
   Le hook `:has(> .multiselect[data-open])` couvre multiselect, autocomplete
   et datepicker (qui posent tous `data-open` sur leur wrapper `.multiselect`
   pendant l'ouverture du panneau) : sans ça, après sélection en mode single,
   l'attribut `inert` posé sur le panel déplace le focus hors du wrap → un
   flash possible du label avant que `:has(.has-selection)` ne reprenne la
   main (l'invalidation de `:has()` est décalée d'une frame sur certains
   moteurs). Garder le hook actif tant que le panneau est ouvert élimine
   tout gap pendant la transition focus-within → has-selection.
   Hooks autofill : une pseudo-classe PAR item :has() - :has() est
   non-tolérant (un argument inconnu invaliderait le :has() entier),
   alors que la liste de :where() est tolérante : un navigateur qui ne
   connaît pas :-webkit-autofill ou :autofill ne perd que cet item. */
.form-floating .field-wrap:where(
    :focus-within,
    :has(> input:not([type="hidden"]):not(:placeholder-shown)),
    :has(> .relative > input:not([type="hidden"]):not(:placeholder-shown)),
    :has(> .multiselect > input[data-autocomplete-input]:not(:placeholder-shown)),
    :has(> textarea:not(:placeholder-shown)),
    :has(.multiselect-value.has-selection),
    :has([data-datepicker-display].has-selection),
    :has(> .multiselect[data-open]),
    :has(> input:-webkit-autofill),
    :has(> .relative > input:-webkit-autofill),
    :has(> .multiselect > input[data-autocomplete-input]:-webkit-autofill),
    :has(> input:autofill),
    :has(> .relative > input:autofill),
    :has(> .multiselect > input[data-autocomplete-input]:autofill)
) .field-label {
    top: 0;
    font-size: 11px;
    font-weight: 600;
    color: #211315;
}

/* Au focus : label flotté en couleur brand. Spécificité 0,4,0 - bat la règle
   flotté (0,3,0) grâce à :focus-within (vraie pseudo-classe vs :where()). */
.form-floating .field-wrap:focus-within .field-label {
    color: rgb(var(--brand));
}

/* Triggers custom (multiselect / datepicker) : le span de valeur affiche
   normalement un placeholder textuel ("Type d'événement *") tant que rien
   n'est sélectionné - en mode floating, ça ferait doublon avec le label.
   On le masque tant que .has-selection n'est pas posée par main.js, le
   label flottant assurant seul l'invite. */
.form-floating .field-wrap .multiselect-value:not(.has-selection),
.form-floating .field-wrap [data-datepicker-display]:not(.has-selection) {
    visibility: hidden;
}

/* Champ désactivé : aligne le fond du label floating sur celui de l'input
   désactivé (#F5F5F5 = bg-surface-light). Sans ça, le label garde son fond
   blanc et un rectangle clair apparaît par-dessus le fond gris du champ. */
.form-floating .field-wrap:has(> input:disabled) .field-label,
.form-floating .field-wrap:has(> .relative > input:disabled) .field-label {
    background: #F5F5F5;
}

@media (prefers-reduced-motion: reduce) {
    .form-floating .field-label,
    .form-floating .field-wrap input::placeholder,
    .form-floating .field-wrap textarea::placeholder {
        transition: none;
    }
}

/* ===========================================================
   États d'erreur de formulaire
   ----------------------------------------------------------
   La classe `.has-error` posée sur un `.field-wrap` affiche le
   message `.field-error` placé à l'intérieur et passe la bordure
   du champ en rouge. Au focus, le JS retire `.has-error` du wrap
   pour revenir à l'état normal (cf. section "Erreurs de
   formulaire" dans main.js).

   Couvre les inputs natifs, textarea, et les triggers des
   composants custom (multiselect / autocomplete / datepicker).
   =========================================================== */
/* Le message d'erreur est EN FLUX, sous le champ : il pousse naturellement le
   contenu suivant quelle que soit sa longueur (les messages serveur - newsletter,
   futurs endpoints - peuvent faire plusieurs lignes ; l'ancien positionnement
   absolu ne réservait qu'une ligne et débordait au-delà). Deux garde-fous
   rendent ça sûr : le label floating est ancré à mi-hauteur FIXE du champ
   (top: 28px - pas en % du wrap), et les cellules de grid sont en
   `align-self: start` (cf. règles plus haut) pour que la rangée reste alignée. */
.field-error {
    margin-top: 6px;
    display: none;
    color: #DC2626;
    font-size: 12px;
    line-height: 1.4;
}
.field-wrap.has-error .field-error {
    display: block;
}
/* Variante consentement : le "wrap" est le <label data-required-consent>
   (pas un .field-wrap). Même convention pour les messages serveur injectés
   par le multistep (applyServerErrors, main.js §14) : le label passe en
   flex-wrap pour que le message occupe sa propre ligne sous la case. */
[data-required-consent] {
    flex-wrap: wrap;
}
/* Contrepartie de ce flex-wrap : le texte du consentement doit rester SUR la
   ligne de la case. Sans base flexible, sa largeur max-content (une phrase
   longue - « Je comprends que 1001 Salles repose sur le modèle de
   l'abonnement... » de l'inscription prestataire) déborde de la ligne et
   l'item entier bascule sous la case, cadré à gauche. `flex-basis: 0` le
   laisse se replier normalement ; seul le `.field-error` (width: 100 %) prend
   sa propre ligne - c'est pour lui que le flex-wrap est là. */
[data-required-consent] > span {
    flex: 1 1 0%;
    min-width: 0;
}
[data-required-consent].has-error .field-error {
    display: block;
    width: 100%;
}
.field-wrap.has-error > input:not([type="hidden"]):not([type="checkbox"]),
.field-wrap.has-error > textarea,
.field-wrap.has-error > .relative > input:not([type="hidden"]),
.field-wrap.has-error .multiselect-trigger,
.field-wrap.has-error input[data-autocomplete-input],
.field-wrap.has-error [data-datepicker-display] {
    border-color: #DC2626;
}

/* Mode floating : label en rouge quand le wrap est en erreur.
   Spécificité 0,3,0 - placé APRÈS la cascade floating pour gagner
   à spécificité égale. Le focus (qui retire `.has-error` via JS)
   restaure naturellement la couleur brand. */
.form-floating .field-wrap.has-error .field-label {
    color: #DC2626;
}

/* ===========================================================
   Neutralisation du thème autofill (Chrome / Edge / Safari)
   ----------------------------------------------------------
   Chrome applique un fond bleu (#E8F0FE) et une couleur de texte
   bleutée aux champs auto-remplis, non surchargeables via
   `background-color` / `color` standards. Parades nécessaires :
   - `-webkit-box-shadow: inset` épais → masque le fond bleu
     par-dessus (le navigateur peint le fond, mais l'ombre interne
     occupe tout le champ et le recouvre).
   - `-webkit-text-fill-color` → réécrit la couleur du texte
     (la prop `color` est ignorée tant que l'autofill est actif).
   - `transition: background-color 5000s` → neutralise l'animation
     de transition que Chrome déclenche au remplissage.
   On cible aussi :hover/:focus/:active pour couvrir tous les états.
   =========================================================== */
input:-webkit-autofill,
input:-webkit-autofill:hover,
input:-webkit-autofill:focus,
input:-webkit-autofill:active,
textarea:-webkit-autofill,
textarea:-webkit-autofill:hover,
textarea:-webkit-autofill:focus,
textarea:-webkit-autofill:active,
select:-webkit-autofill,
select:-webkit-autofill:hover,
select:-webkit-autofill:focus {
    box-shadow: 0 0 0 1000px #fff inset !important;
    -webkit-text-fill-color: #0a0a0a !important;
    caret-color: #0a0a0a;
    transition: background-color 5000s ease-in-out 0s,
                color 5000s ease-in-out 0s;
}

/* Firefox : moins agressif (juste un léger jaunâtre) mais on
   neutralise pour rester cohérent multi-navigateur. */
input:autofill,
textarea:autofill,
select:autofill {
    background-color: #fff !important;
    color: #0a0a0a !important;
}

/* ===========================================================
   Bloc SEO (rich content bas de page)
   ----------------------------------------------------------
   Couleurs spécifiques au bloc éditorial SEO en bas des pages
   (paragraphes en gris foncé, titres en bleu nuit). Hors du
   design system principal - encapsulé ici pour ne pas polluer
   les classes utilitaires avec des couleurs ponctuelles.
   =========================================================== */
.seo-content {
    color: #4B4646;
}
.seo-content h3 {
    color: #051D34;
}

/* ===========================================================
   Prose riche des articles de blog (blog-single.php)
   ----------------------------------------------------------
   Le back-office stocke du HTML libre (titres, listes, tableaux,
   citations, hr…) restitué brut dans les sections d'article. Ces
   règles redonnent une mise en forme éditoriale à ces balises SANS
   classes. Scopé sur .prose-rich (posé par render_article_section
   sur chaque wrapper de contenu) et PAS sur .seo-content : les
   autres enfants de la prose (accordéon FAQ, encarts CTA) ne
   doivent pas hériter de ces styles.
   Couleurs : titres #051D34 (= prose-heading), bordures #E5E7EB
   (= gray-200 déjà utilisé par les cards), accents en
   rgb(var(--brand)) (rose privé / bleu PRO). */
.prose-rich p {
    margin: 0 0 14px;
}
.prose-rich > :first-child {
    margin-top: 0;
}
.prose-rich > :last-child {
    margin-bottom: 0;
}

/* L'éditeur legacy sème des font-size (12pt…) et font-family (Cambria, serif…
   - collages Word) inline qui désaccordent la typo d'un bloc à l'autre : on
   les neutralise - seul recours face à un style inline. Taille et police
   viennent du conteneur (.seo-content 15px, police du site). */
.prose-rich [style*="font-size"] {
    font-size: inherit !important;
}
.prose-rich [style*="font-family"] {
    font-family: inherit !important;
}

/* Titres embarqués (sans classes - ceux rendus par PHP ont leurs utilitaires).
   scroll-margin : les h2 du contenu portent des ancres (sommaire, liens). */
.prose-rich h2 {
    font-size: 24px;
    line-height: 30px;
    font-weight: 700;
    color: #051D34;
    margin: 28px 0 12px;
    scroll-margin-top: 100px;
}
.prose-rich h3 {
    font-size: 19px;
    line-height: 26px;
    font-weight: 700;
    color: #051D34;
    margin: 22px 0 10px;
    scroll-margin-top: 100px;
}
.prose-rich h4 {
    font-size: 16px;
    line-height: 24px;
    font-weight: 700;
    color: #051D34;
    margin: 18px 0 8px;
}

/* Variante compacte (intro SEO de list.php) : le h1 de la page ne fait que
   24px (20px mobile), les titres embarqués doivent passer dessous - h2 18px
   (aligné sur le h2 « résultats » de la page), échelle et marges resserrées. */
.prose-rich--compact h2 {
    font-size: 18px;
    line-height: 24px;
    margin: 20px 0 10px;
}
.prose-rich--compact h3 {
    font-size: 16px;
    line-height: 22px;
    margin: 16px 0 8px;
}
.prose-rich--compact h4 {
    font-size: 15px;
    line-height: 22px;
    margin: 14px 0 6px;
}

/* Listes (le preflight Tailwind retire puces et retraits). */
.prose-rich ul,
.prose-rich ol {
    margin: 0 0 16px;
    padding-left: 24px;
}
.prose-rich ul {
    list-style: disc;
}
.prose-rich ol {
    list-style: decimal;
}
.prose-rich li {
    margin: 6px 0;
    padding-left: 2px;
}
.prose-rich li::marker {
    color: rgb(var(--brand));
    font-weight: 700;
}
.prose-rich ul ul,
.prose-rich ol ol,
.prose-rich ul ol,
.prose-rich ol ul {
    margin: 6px 0 0;
}
.prose-rich ul ul {
    list-style: circle;
}

/* Tableaux (les inline styles de l'éditeur - border-collapse, border=1 -
   convergent avec ces règles). Zébrage léger + en-tête brand-tint (token
   prévu pour les fonds d'en-tête de tableau). */
.prose-rich table {
    width: 100%;
    border-collapse: collapse;
    border: 1px solid #E5E7EB;
    margin: 8px 0 20px;
    font-size: 14px;
    line-height: 1.5;
}
/* Conteneur défilable posé autour de chaque <table> par blog_section_html()
   (blog-single.php) : le tableau reste pleine largeur, et s'il excède sa
   colonne (mobile), c'est ce conteneur qui scrolle - jamais la page. Non
   scopé à .prose-rich : le contenu des réponses FAQ passe par le même helper. */
.table-scroll {
    overflow-x: auto;
    -webkit-overflow-scrolling: touch;
}
.prose-rich .table-scroll {
    margin: 8px 0 20px;
}
.prose-rich .table-scroll table {
    margin: 0;
}
.prose-rich th,
.prose-rich td {
    border: 1px solid #E5E7EB;
    padding: 10px 14px;
    text-align: left;
    vertical-align: top;
}
.prose-rich thead th {
    background: rgb(var(--brand-tint));
    color: #051D34;
    font-weight: 700;
    white-space: nowrap;
}
.prose-rich tbody tr:nth-child(even) td {
    background: #F8F9FB; /* = surface-subtle */
}
/* Variante éditeur SANS <thead> (première ligne = <td> gras) : la 1re ligne
   fait office d'en-tête - même fond brand-tint que les thead th. Navigateur
   sans :has() : dégradé propre (ligne simplement blanche). */
.prose-rich table:not(:has(thead)) tr:first-child td {
    background: rgb(var(--brand-tint));
    color: #051D34;
    font-weight: 700;
}
/* Les <p> que l'éditeur insère DANS les cellules ne portent pas le rythme de
   la prose (leurs 14px de marge basse rendaient les lignes trop hautes). */
.prose-rich th p,
.prose-rich td p {
    margin: 0;
}
/* Titres égarés DANS des cellules (contenus legacy) : rendus comme du texte
   de cellule - le gras d'en-tête vient des règles thead / 1re ligne. */
.prose-rich th h1, .prose-rich th h2, .prose-rich th h3, .prose-rich th h4,
.prose-rich td h1, .prose-rich td h2, .prose-rich td h3, .prose-rich td h4 {
    font-size: inherit;
    line-height: inherit;
    font-weight: inherit;
    color: inherit;
    margin: 0;
}
.prose-rich th p + p,
.prose-rich td p + p {
    margin-top: 6px;
}
/* L'éditeur pose aussi des border-color inline (#000000) sur table/td :
   bordures noires. Seul un !important ramène au gris du design. */
.prose-rich table,
.prose-rich th,
.prose-rich td {
    border-color: #E5E7EB !important;
}
/* Mobile : en-têtes autorisés à passer à la ligne - sinon le nowrap force un
   tableau plus large que l'écran (et donc un scroll) même quand ça tiendrait. */
@media (max-width: 767px) {
    .prose-rich thead th {
        white-space: normal;
    }
}

/* Citations / encadrés conseil. */
.prose-rich blockquote {
    margin: 8px 0 20px;
    padding: 16px 20px;
    background: #F5F5F5; /* = surface-light */
    border-left: 3px solid rgb(var(--brand));
    border-radius: 0 12px 12px 0;
}
.prose-rich blockquote p {
    margin-bottom: 10px;
}
.prose-rich blockquote > :last-child {
    margin-bottom: 0;
}

/* Séparateurs de l'éditeur. Avec le collapsing des marges entre voisins
   (le max l'emporte : 20 vs les 14 du <p> précédent, 22 vs 0 du suivant),
   l'espacement rendu est ~20px au-dessus / 22px en dessous - équilibré. */
.prose-rich hr {
    border: 0;
    border-top: 1px solid #E5E7EB;
    margin: 20px 0 22px;
}

/* Liens éditoriaux - SAUF les liens-boutons insérés par l'éditeur (classes
   btn-primary/btn-* du legacy) : .prose-rich a est plus spécifique que
   .btn-primary et repeindrait leur texte en brand sur fond brand (invisible). */
.prose-rich a:not([class*="btn"]) {
    color: rgb(var(--brand));
    font-weight: 500;
    text-decoration: underline;
    text-underline-offset: 2px;
    transition: color 0.15s ease;
}
.prose-rich a:not([class*="btn"]):hover {
    color: rgb(var(--brand-dark));
}
.prose-rich a[class*="btn"] {
    display: inline-block;
    text-decoration: none;
}

/* Images insérées dans le corps (rares - la plupart passent par les
   sections image) : jamais plus larges que la colonne. */
.prose-rich img {
    max-width: 100%;
    height: auto;
    border-radius: 12px;
}

/* ===========================================================
   Formulaire multi-étapes (sos.php)
   ----------------------------------------------------------
   Le composant [data-multistep] enveloppe un <form> contenant
   plusieurs <fieldset class="multistep-step" data-multistep-step="N">.
   Un seul step est affiché à la fois (sélectionné par [data-active]).
   Le stepper visuel (dots + connecteurs) reflète l'étape courante.
   La barre d'actions en bas pilote Retour / Suivant / Effacer / Submit.
   Logique JS : section 14 de main.js (data-multistep).
   =========================================================== */

/* Marge de défilement pour le scrollIntoView() de fin d'étape (section 14
   de main.js). Sans ça, `block: 'start'` aligne le haut du form sur le top
   du viewport - invisible derrière le header sticky (85px) - et l'œil n'a
   aucun point d'ancrage visuel. 85 (header) + 30 (respiration demandée) = 115.
   Même marge sur les `.field-wrap` à l'intérieur d'un multistep : `validateStep`
   scrolle vers le 1er wrap en erreur (du haut vers le bas dans le DOM), il
   doit lui aussi atterrir SOUS le header. */
[data-multistep],
[data-multistep] .field-wrap,
[data-multistep-scroll-target] {
    scroll-margin-top: 115px;
}

/* Conteneur form : un fieldset par étape, masqué par défaut.
   `border: 0; padding: 0; margin: 0` neutralise le rendu natif des <fieldset>. */
.multistep-step {
    border: 0;
    padding: 0;
    margin: 0;
    min-width: 0;
    display: none;
}
.multistep-step[data-active] {
    display: block;
}
/* Animation d'entrée discrète au changement d'étape.
   ATTENTION : on n'anime QUE `opacity`, jamais `transform`. Toute valeur de
   transform != none sur un ancêtre crée un containing block pour les enfants
   en `position: fixed` - ce qui décalerait visuellement les panels des
   composants multiselect/autocomplete/datepicker ancrés au viewport (cf.
   commentaire en haut de la section 9 dans main.js et "Piège connu" dans
   CLAUDE.md). On laisse aussi `fill-mode` à sa valeur par défaut (`none`) :
   pas de besoin de conserver l'état final, l'opacity finale (= 1) correspond
   à l'état CSS naturel. */
.multistep-step[data-active] {
    animation: multistep-fade 220ms ease-out;
}
@keyframes multistep-fade {
    from { opacity: 0; }
    to   { opacity: 1; }
}
@media (prefers-reduced-motion: reduce) {
    .multistep-step[data-active] { animation: none; }
}

/* Titre du formulaire - slot unique au-dessus du stepper, mis à jour par le JS
   selon l'étape active (data-multistep-step-title sur le fieldset courant).
   Le titre est extrait du <fieldset>/<legend> pour pouvoir être placé AU-DESSUS
   du stepper visuel (le <legend> était forcément dans son <fieldset>). */
.multistep-form-title {
    display: block;
    width: 100%;
    padding: 0;
    margin: 0;
    font-size: 20px;
    line-height: 1.3;
    font-weight: 700;
    color: #211315;
    text-align: center;
}
@media (min-width: 768px) {
    .multistep-form-title {
        font-size: 22px;
    }
}

/* ---------- Stepper (dots + connecteurs) ----------
   Aligné horizontalement, occupe la largeur du form, espacement adaptatif
   via flex. Les dots sont des cercles 14px ; les connecteurs sont des barres
   de 2px qui s'étirent (flex:1) entre deux dots. */
.multistep-progress {
    display: flex;
    align-items: center;
    gap: 0;
    margin: 0 4px 5px;
}
.multistep-dot {
    flex: 0 0 auto;
    width: 14px;
    height: 14px;
    border-radius: 50%;
    background: #E5E7EB;
    border: 0;
    padding: 0;
    cursor: default;
    transition: background 200ms ease, box-shadow 200ms ease, transform 200ms ease;
}
.multistep-dot.is-done {
    background: rgb(var(--brand));
    cursor: pointer;
}
.multistep-dot.is-done:hover {
    box-shadow: 0 0 0 4px rgb(var(--brand) / 0.18);
}
.multistep-dot.is-done:focus-visible {
    outline: none;
    box-shadow: 0 0 0 4px rgb(var(--brand) / 0.35);
}
.multistep-dot.is-current {
    background: rgb(var(--brand));
    box-shadow: 0 0 0 4px rgb(var(--brand) / 0.18);
}
.multistep-line {
    flex: 1 1 auto;
    height: 2px;
    background: #E5E7EB;
    transition: background 200ms ease;
    margin: 0 6px;
}
.multistep-line.is-done {
    background: rgb(var(--brand));
}

/* ---------- Barre d'actions ----------
   Sticky en bas du form au visuel : disposée en 2 colonnes (Effacer à gauche,
   Retour/Suivant/Submit à droite). Sur mobile : "Effacer" reste en lien discret
   sous le bloc principal pour ne pas concurrencer les actions primaires. */
.multistep-actions {
    display: flex;
    align-items: center;
    justify-content: space-between;
    gap: 15px;
    padding-top: 25px;
    border-top: 1px solid #EFEFEF;
}
.multistep-actions-main {
    display: flex;
    align-items: center;
    gap: 10px;
    flex: 1;
    justify-content: flex-end;
}
.multistep-actions .btn-primary {
    padding: 12px 28px;
}
/* Bouton "Retour" : style fantôme (gris clair). Maintenu compact pour laisser
   le bouton primaire dominer visuellement.
   `:not([hidden])` : l'attribut HTML `hidden` du user-agent (specificity 0,1,0)
   serait écrasé par `.multistep-back` (même specificity, mais charge après dans
   styles.css). On contourne en ne posant `display` que quand le bouton est visible. */
.multistep-back:not([hidden]) {
    display: inline-flex;
    align-items: center;
    gap: 8px;
    padding: 12px 18px;
    border-radius: 12px;
    background: #F2F3F5;
    color: #211315;
    font-weight: 600;
    font-size: 14px;
    transition: background 150ms ease;
}
.multistep-back:hover {
    background: #E6E7EA;
}
/* Bouton "Effacer" : lien discret, à gauche. Toujours visible (permet de tout
   réinitialiser à n'importe quelle étape). */
.multistep-clear {
    display: inline-flex;
    align-items: center;
    gap: 6px;
    font-size: 13px;
    color: #757575;
    font-weight: 500;
    padding: 8px 4px;
    transition: color 150ms ease;
}
.multistep-clear:hover {
    color: rgb(var(--brand));
}
@media (max-width: 559px) {
    /* Mobile compact : pile verticalement, "Effacer" en bas en lien discret */
    .multistep-actions {
        flex-direction: column-reverse;
        align-items: stretch;
        gap: 10px;
    }
    .multistep-actions-main {
        flex-direction: row;
        width: 100%;
    }
    .multistep-actions-main .btn-primary {
        flex: 1;
        justify-content: center;
    }
    .multistep-clear {
        align-self: center;
    }
}

/* ---------- Encart de connexion ----------
   Bandeau en tête d'étape des formulaires publics (booking.php, sos.php) :
   « Connecté en tant que... » ou « Vous avez déjà un compte ? ».
   Fond bleu pâle des tokens `info` du design system (ton informatif -
   surtout pas la couleur de marque, qui se lirait comme une alerte) ;
   seul le lien d'action reste en brand. */
.login-hint {
    display: flex;
    align-items: center;
    justify-content: center;
    gap: 8px;
    font-size: 14px;
    font-weight: 500;
    color: #0A0A0A;
    background: #EAF1FB;        /* token info */
    border: 1px solid #E4ECF7;  /* token info-light */
    padding: 12px 16px;
    margin: 0 0 20px;
    text-align: center;
}
.login-hint i {
    font-size: 14px;
    color: #051D34; /* token prose-heading - navy des blocs info (cf. « À lire ensuite ») */
}
.login-hint a {
    color: rgb(var(--brand));
    font-weight: 700;
    text-decoration: underline;
    text-underline-offset: 2px;
}
.login-hint a:hover { color: rgb(var(--brand-dark)); }
/* En enfant direct d'un multistep (sos.php : bandeau placé hors des fieldsets
   pour rester visible à chaque étape), le `gap` du form gère l'espacement -
   on neutralise la marge propre. Et comme enterSuccessState (main.js §14) ne
   masque que steps/actions/progress/titre, on masque le bandeau sur l'écran
   de succès en CSS (même pattern :has() que booking.css pour son heading). */
[data-multistep] > .login-hint { margin-bottom: 0; }
[data-multistep]:has([data-multistep-success]:not(.hidden)) > .login-hint { display: none; }

/* ===========================================================
   App dialog (alert / confirm custom)
   ----------------------------------------------------------
   Modal centré, design aligné sur le site (rounded-xl2, shadow-card,
   bouton primaire rose). Remplace `alert()` / `confirm()` natifs.
   API : voir `appDialog()` dans main.js (section 15).
   Markup injecté une seule fois dans <body> au 1er appel.
   =========================================================== */
.app-dialog-overlay {
    position: fixed;
    inset: 0;
    background: rgba(0, 0, 0, 0.45);
    display: flex;
    align-items: center;
    justify-content: center;
    padding: 20px;
    z-index: 1000;
    opacity: 0;
    transition: opacity 180ms ease;
}
.app-dialog-overlay.is-visible {
    opacity: 1;
}
.app-dialog-overlay[hidden] {
    display: none !important;
}
.app-dialog {
    background: #fff;
    border-radius: 20px;
    box-shadow: 0 20px 60px rgba(0, 0, 0, 0.25);
    width: 100%;
    max-width: 420px;
    padding: 30px 28px 24px;
    display: flex;
    flex-direction: column;
    align-items: center;
    gap: 14px;
    text-align: center;
    transform: scale(0.96);
    transition: transform 180ms ease;
}
.app-dialog-overlay.is-visible .app-dialog {
    transform: scale(1);
}
@media (prefers-reduced-motion: reduce) {
    .app-dialog-overlay,
    .app-dialog {
        transition: none;
    }
}
/* Icône optionnelle (cercle de couleur selon le type). 64px haut, ronde,
   icône Font Awesome blanche au centre. */
.app-dialog-icon {
    width: 64px;
    height: 64px;
    border-radius: 50%;
    display: inline-flex;
    align-items: center;
    justify-content: center;
    font-size: 26px;
    color: #fff;
    flex-shrink: 0;
    margin-bottom: 4px;
}
.app-dialog-icon--warning  { background: #FFB020; box-shadow: 0 8px 24px rgba(255, 176, 32, 0.25); }
.app-dialog-icon--danger   { background: #DC2626; box-shadow: 0 8px 24px rgba(220, 38, 38, 0.25); }
.app-dialog-icon--error    { background: #DC2626; box-shadow: 0 8px 24px rgba(220, 38, 38, 0.25); }
.app-dialog-icon--success  { background: #16A34A; box-shadow: 0 8px 24px rgba(22, 163, 74, 0.25); }
.app-dialog-icon--info     { background: rgb(var(--brand)); box-shadow: 0 8px 24px rgb(var(--brand) / 0.25); }
.app-dialog-icon--question { background: rgb(var(--brand)); box-shadow: 0 8px 24px rgb(var(--brand) / 0.25); }

.app-dialog-title {
    font-size: 18px;
    line-height: 1.3;
    font-weight: 700;
    color: #211315;
    margin: 0;
}
.app-dialog-message {
    font-size: 14px;
    line-height: 1.5;
    color: #757575;
    margin: 0;
}
.app-dialog-actions {
    display: flex;
    gap: 10px;
    width: 100%;
    margin-top: 6px;
}
.app-dialog-actions > button {
    flex: 1;
    padding: 12px 18px;
    border-radius: 12px;
    font-weight: 600;
    font-size: 14px;
    transition: background 150ms ease;
    line-height: 1.2;
}
.app-dialog-cancel {
    background: #F2F3F5;
    color: #211315;
}
.app-dialog-cancel:hover {
    background: #E6E7EA;
}
.app-dialog-confirm {
    background: rgb(var(--brand));
    color: #fff;
}
.app-dialog-confirm:hover {
    background: rgb(var(--brand-dark));
}
/* Variante "danger" pour les actions destructives (Effacer, Supprimer, etc.) */
.app-dialog-confirm.is-danger {
    background: #DC2626;
}
.app-dialog-confirm.is-danger:hover {
    background: #B91C1C;
}
/* Mode 3 boutons (option extraLabel) : trois libellés longs ne tiennent pas en
   ligne - pile verticale, column-reverse pour garder le bouton primaire en tête
   (ordre DOM : cancel, extra, confirm → affiché : confirm, extra, cancel). */
.app-dialog-actions.is-triple {
    flex-direction: column-reverse;
}
/* Mode alert (1 seul bouton) : on ne stretch pas le bouton sur toute la largeur,
   on le centre. */
.app-dialog-actions.is-single {
    justify-content: center;
}
.app-dialog-actions.is-single > button {
    flex: 0 0 auto;
    min-width: 140px;
}
/* Bloque le scroll body quand le dialog est ouvert (posée par le JS) */
body.app-dialog-open {
    overflow: hidden;
}

/* ---------- Écran de succès (après submit) ----------
   Remplace le contenu du form (steps + actions) une fois envoyé.
   `:not(.hidden)` : même problème que `.multistep-back` ci-dessus - la classe
   utilitaire `.hidden` de Tailwind (`display: none`) charge AVANT styles.css,
   donc à specificity égale `.multistep-success` l'emporterait. On évite ça en
   ne posant `display: flex` que quand `.hidden` n'est pas présente. Le JS
   bascule via `successEl.classList.remove('hidden')` au submit. */
.multistep-success:not(.hidden) {
    display: flex;
    flex-direction: column;
    align-items: center;
    gap: 14px;
    padding: 10px 0 5px;
}
.multistep-success-icon {
    width: 60px;
    height: 60px;
    border-radius: 50%;
    background: rgb(var(--brand));
    color: #fff;
    display: inline-flex;
    align-items: center;
    justify-content: center;
    font-size: 24px;
    box-shadow: 0 8px 24px rgb(var(--brand) / 0.25);
}

/* ---------- Vue Carte (Leaflet) - page list.php ----------
   L'overlay est positionné en `fixed inset-0 z-[60]` côté Tailwind. Ici on stylise la
   chrome Leaflet (popup, close button, attribution) pour qu'elle colle au design system.
   Le contenu du popup lui-même (`.list-card ...`) réutilise strictement le markup de la
   card mobile de la liste - pas de classes propres au popup, juste un override de la
   chrome Leaflet qui contient la card (radius, padding 0, largeur 310). */
.leaflet-container {
    font-family: 'Inter', system-ui, sans-serif;
}
.venue-popup .leaflet-popup-content-wrapper {
    border-radius: 15px;
    box-shadow: 0 8px 32px rgba(0, 0, 0, 0.18);
    padding: 0;
    overflow: hidden;
    background: #fff;
}
.venue-popup .leaflet-popup-content {
    margin: 0;
    width: 310px !important;
    line-height: inherit;
}
.venue-popup .leaflet-popup-tip {
    box-shadow: 0 8px 32px rgba(0, 0, 0, 0.18);
}
/* La card a déjà son propre rounded[15px] - on neutralise le `padding` du
   wrapper et on laisse la card être bord-à-bord. */
.venue-popup .list-card {
    margin: 0;
}
/* Leaflet pose `.leaflet-container a { color: #0078A8 }` (spec 0,1,1) qui écrase
   les utilitaires Tailwind `.text-X` (spec 0,1,0) sur tous les <a> du popup. On
   rétablit explicitement les couleurs ciblées (titre + CTA brand) avec une spec
   supérieure (.venue-popup .list-card .CLASSE = 0,3,0 > .leaflet-container a 0,1,1),
   sans !important. */
.venue-popup .list-card .card-link {
    color: #0A0A0A; /* = ink-900 */
}
.venue-popup .list-card .card-link:hover {
    color: rgb(var(--brand));
}
.venue-popup .list-card a.bg-brand,
.venue-popup .list-card a.bg-brand:hover {
    color: #fff;
}
/* Leaflet pose `.leaflet-popup-content p { margin: 1.3em 0 }` (spec 0,1,1) sur tous
   les <p> du popup, ce qui rajoutait ~17px de margin top/bottom autour du paragraphe
   de description. On annule pour laisser le `gap-[6px]` du parent flex piloter seul
   l'espacement entre éléments. */
.venue-popup .list-card p {
    margin: 0;
}
/* Bouton de fermeture Leaflet : Leaflet pose un caractère `×` HTML brut dans un <a>
   et lui applique `.leaflet-container a.leaflet-popup-close-button { width: 24px;
   height: 24px; font: 16px/24px Tahoma }` - spécificité (0,2,1). Pour overrider sans
   !important partout, on préfixe avec la même chaîne (0,2,1) ; à spec égale, styles.css
   chargé après leaflet.css gagne. Taille 34×34 pour matcher exactement le bouton favori
   `.w-[34px] h-[34px]` ; centrage flex car la métrique du caractère `×` tombe au-dessus
   de la baseline et un simple line-height ne le cale pas verticalement. */
.leaflet-container a.leaflet-popup-close-button {
    display: flex;
    align-items: center;
    justify-content: center;
    width: 34px;
    height: 34px;
    line-height: 1;
    border-radius: 50%;
    background: #fff;
    box-shadow: 0 2px 10px rgba(0, 0, 0, 0.20);
    top: 9px;
    right: 10px;
    color: #0A0A0A;
    font-size: 14px; /* taille de l'icône FA, idem que .fa-bookmark du bouton favori */
    padding: 0;
    transition: color .15s;
    z-index: 4;
}
.leaflet-container a.leaflet-popup-close-button:hover,
.leaflet-container a.leaflet-popup-close-button:focus {
    color: rgb(var(--brand));
    background: #fff;
}
/* Attribution OSM : on garde le texte par défaut mais on adoucit le fond pour qu'il
   se fonde dans le design (les contrôles Leaflet sont par défaut blanc-sur-gris). */
.leaflet-control-attribution {
    font-size: 10px !important;
    background: rgba(255, 255, 255, 0.85) !important;
    color: #757575 !important;
}
.leaflet-control-attribution a {
    color: #0A0A0A !important;
}

/* Leaflet.markercluster - override du thème par défaut (vert/jaune/orange) vers le rose
   brand. Les classes `.marker-cluster-small|medium|large` sont posées automatiquement
   par le plugin selon le nombre de points groupés. On garde le double cercle (halo + cœur)
   du thème par défaut mais en deux teintes de rose : halo translucide + cœur opaque. */
.marker-cluster-small,
.marker-cluster-medium,
.marker-cluster-large {
    background-color: rgb(var(--brand) / 0.30);
}
.marker-cluster-small div,
.marker-cluster-medium div,
.marker-cluster-large div {
    background-color: rgb(var(--brand) / 0.85);
    color: #fff;
    font-weight: 700;
    font-family: 'Inter', system-ui, sans-serif !important;
}
.marker-cluster span {
    color: #fff;
}

/* ===========================================================
   Phone input (intl-tel-input v25)
   ----------------------------------------------------------
   Helpers PHP : render_phone() - voir CLAUDE.md > "Phone input".
   La lib enveloppe l'input dans un `.iti` au moment de l'init JS, ce qui
   (a) casse la cascade form-floating (sélecteurs en `> input` ou `> .relative > input`),
   (b) ferait conflit visuel avec un label flotté (le drapeau prend la place).
   Décision : sur les wraps [data-phone-input], on désactive le floating et on
   garde un label statique au-dessus. Pour matcher le reste du design, on
   force .iti à prendre 100% de largeur (par défaut inline-block) et on raccorde
   les états d'erreur (.has-error) à l'input visible nouvellement réimbriqué.
   =========================================================== */
[data-phone-input] .iti {
    display: block;
    width: 100%;
}
/* Forcer la hauteur de l'input à 46px même quand intl-tel-input pose son padding
   dynamique (la cascade Tailwind h-[46px] reste prioritaire, mais on sécurise). */
[data-phone-input] [data-phone-tel] {
    height: 46px;
}
/* En mode floating, les autres champs (input, multiselect, autocomplete, datepicker)
   passent à 56px. On aligne le phone pour rester sur la même rangée visuelle. */
.form-floating .field-wrap[data-phone-input] [data-phone-tel] {
    height: 56px;
}
/* État d'erreur : la règle générique .field-wrap.has-error > input ne matche plus
   après que .iti ait wrappé l'input → bordure rouge pilotée explicitement. */
.field-wrap.has-error [data-phone-tel] {
    border-color: #DC2626;
}
/* Neutralisation du mode floating sur le phone-input.
   Le `:not(.sr-only)` est indispensable : sans lui, l'override gagne en spécificité
   (0,4,0) sur la classe `.sr-only` (0,1,0) posée par render_phone(hide_label=true)
   → le label réapparaît à l'écran. On laisse donc passer le mode sr-only intact.
   NB : l'espace label ↔ champ vient du margin-bottom du label statique, PAS d'un
   `gap` sur le wrap : un gap s'appliquerait aussi entre le champ et le
   `.field-error` (qui a déjà son margin-top: 6px), décalant le message d'erreur
   6px plus bas que celui des champs voisins - surtout visible quand le label est
   en sr-only (sos.php) où le wrap n'a alors AUCUN espace à réserver. */
.form-floating .field-wrap[data-phone-input] .field-label:not(.sr-only) {
    position: static;
    transform: none;
    background: transparent;
    color: #211315;
    font-size: 13px;
    font-weight: 600;
    padding: 0;
    margin: 0 0 6px;
    line-height: 1.4;
    max-width: none;
    overflow: visible;
    white-space: normal;
    text-overflow: clip;
    transition: none;
}
/* Au focus, garder le ring brand cohérent avec les autres champs. */
.field-wrap[data-phone-input] [data-phone-tel]:focus {
    outline: none;
}

/* List page mobile bottom bar (#list-mobile-bar).
   Pattern par défaut : `position: sticky; bottom: 0` posé via Tailwind dans le markup
   (cf. list.php). La barre est donc collée au bas du viewport tant que sa position
   naturelle (à la fin de la première <section>, juste après le container-x et la pub
   sponsorisée mobile) reste sous le viewport, puis se dépose naturellement à cette
   position quand l'utilisateur scrolle suffisamment.
   ⚠️ La bar DOIT être un enfant direct du <section> (PAS du `.flex-1` colonne droite).
   Sticky est borné par les limites verticales du parent : si le parent commence trop bas
   dans la page (cas du flex-1 dont le top est sous l'aside "À la Une" mobile), le sticky
   se "casse" dès que l'utilisateur scrolle au-dessus de ce top - la bar reste calée en
   haut du parent au lieu du bas du viewport. Hébergé au niveau <section>, le parent
   commence sous le header (sticky top) et descend jusqu'à la fin de la liste, ce qui
   donne assez de course verticale au sticky même panel comparateur ouvert.
   - État "sticky" (par défaut) : box-shadow vers le haut (séparation visuelle du contenu
     qui défile derrière).
   - État "rested" (`.is-rested` posée par main.js section 16b via IntersectionObserver
     sur le sentinel #list-mobile-bar-sentinel) : shadow retirée et border-bottom ajoutée
     pour transformer la bar en simple bloc inline encadré, qui se confond avec le flow.
   Override : quand l'overlay map plein écran est ouvert (#map-view-overlay :not([hidden])),
   le scroll body est bloqué (overflow:hidden, cf. main.js section 16 openMap), ce qui fige
   la position sticky à l'emplacement courant. Si à ce moment-là la position naturelle de la
   bar est déjà visible (utilisateur ayant scrollé en bas avant d'ouvrir la map), la bar
   serait au milieu/haut de la map et non en bas du viewport. On la force donc en
   `position: fixed` ancrée en bas du viewport pour garantir sa visibilité par-dessus la map.
   `:has()` est supporté par tous les navigateurs cibles (Chrome 105+, Safari 15.4+, FF 121+). */
#list-mobile-bar {
    box-shadow: 0 -4px 20px rgba(0, 0, 0, 0.08);
}
/* État rested : la bar est posée dans le flux et DÉFILE avec le contenu - elle peut
   donc croiser le header sticky (z-50) et la barre d'ancres provider (z-40) en haut
   du viewport. Son z-[70] (nécessaire en sticky pour passer au-dessus de l'overlay
   map z-60 de list.php) la ferait passer PAR-DESSUS : on l'abaisse à 30 tant qu'elle
   est posée (au-dessus du contenu de page ≤ z-10, sous header/barres/overlays).
   Spécificité 0,2,0 > l'utilitaire .z-\[70\] (0,1,0), pas de !important nécessaire. */
#list-mobile-bar.is-rested {
    box-shadow: none;
    border-bottom: 1px solid #D9D9D9;
    z-index: 30;
}
/* Provider page mobile bottom bar (#provider-mobile-bar) - même pattern sticky/rested
   (z-index 30 inclus, cf. commentaire ci-dessus) que #list-mobile-bar, observer câblé
   dans provider.js §4. Pas d'override map-overlay ici (la page provider n'a pas de vue
   carte plein écran). */
#provider-mobile-bar {
    box-shadow: 0 -4px 20px rgba(0, 0, 0, 0.08);
}
#provider-mobile-bar.is-rested {
    box-shadow: none;
    border-bottom: 1px solid #D9D9D9;
    z-index: 30;
}
/* Swap des libellés du bouton comparateur desktop de #provider-anchor-bar :
   "Comparer ce lieu" ↔ "Retirer ce lieu", basculé via aria-pressed posé par
   main.js §18 syncCard à chaque ajout/retrait au comparateur. */
#provider-anchor-bar [data-compare-toggle]:not([aria-pressed="true"]) [data-pdbar-label-remove],
#provider-anchor-bar [data-compare-toggle][aria-pressed="true"] [data-pdbar-label-add] {
    display: none;
}
/* Panneau « autres sections » de #provider-anchor-bar (menu de débordement des
   ancres, provider.js §10) : les <li> déplacés y gardent leurs classes de pill
   inline-flex - on force la pleine largeur pour des lignes de menu cliquables. */
[data-anchor-more-list] li > a {
    display: flex;
    width: 100%;
}
body:has(#map-view-overlay:not([hidden])) #list-mobile-bar {
    position: fixed;
    left: 0;
    right: 0;
    bottom: 0;
    margin-left: 0;
    margin-right: 0;
    /* Quand la map est ouverte, la bar est forcée en position:fixed pour rester visible
       par-dessus l'overlay map. Dans cet état, le sentinel n'est plus pertinent (le scroll
       body est bloqué), donc on force aussi l'état "sticky" (shadow visible) pour le
       contraste avec l'overlay map qui occupe tout le viewport. Le z-index est re-forcé
       à 70 : si `.is-rested` est posée au moment de l'ouverture (scroll figé en bas de
       liste), son z-index:30 ferait passer la bar SOUS l'overlay map (z-60). */
    z-index: 70;
    box-shadow: 0 -4px 20px rgba(0, 0, 0, 0.18) !important;
    border-bottom: 0 !important;
}
/* Bouton "Carte" de la bottom bar (data-list-bottom-bar-map) : passe en rose brand
   quand l'overlay map est ouvert, pour signaler que la vue Carte est active (et que
   re-cliquer sur le bouton la fermera). Spécificité 0,1,2 (body:has + attr) > Tailwind
   text-ink-900 (0,1,0), donc override propre sans !important. */
body:has(#map-view-overlay:not([hidden])) [data-list-bottom-bar-map] {
    color: rgb(var(--brand));
}

/* Comparateur desktop flottant (#list-desktop-bar : list.php + group.php ;
   #provider-desktop-bar : provider.php - IDs mutuellement exclusifs, jamais dans
   le DOM en même temps).
   Le conteneur est une boîte transparente fixée en bas à droite du viewport
   (position/offsets via Tailwind dans le markup) : seule la pill trigger
   "Comparateur X/5" reste en flux (le CTA desktop "Comparer ce lieu" de
   provider vit dans #provider-anchor-bar, pas dans le widget).
   Le panel [data-comparator-panel] est sorti du flux ici (absolute, ancré
   au-dessus des pills, aligné à droite) ; son ouverture/fermeture est un pop
   opacity/translate piloté par la classe .is-open (posée par main.js §18).
   NB : depuis le retrait du comparateur sur mobile, ce panel n'existe plus que
   sur desktop (#*-desktop-bar) - plus d'animation grid-rows « mobile » à
   surcharger (cf. CLAUDE.md). Ces règles sont donc auto-suffisantes.
   - visibility:hidden à l'état fermé conserve la sémantique a11y du pattern
     global (contenu non focusable / invisible aux lecteurs d'écran) ; au repli
     elle est retardée (delay 200ms) pour rester visible pendant la transition.
   - Pas d'ombre sur le conteneur : elle est portée par les pills et le panel
     eux-mêmes (shadow-float Tailwind) - une ombre sur la boîte transparente
     peindrait un halo rectangulaire autour des pills.
   - Quand l'overlay map est ouvert (#map-view-overlay:not([hidden])), le widget
     reste visible par-dessus la map (z-70 > z-60, géré par le markup). */
@media (min-width: 768px) {
    #list-desktop-bar [data-comparator-panel],
    #provider-desktop-bar [data-comparator-panel] {
        display: block;
        position: absolute;
        bottom: calc(100% + 12px);
        right: 0;
        visibility: hidden;
        opacity: 0;
        transform: translateY(8px);
        transition: opacity 200ms ease, transform 200ms ease, visibility 0s 200ms;
    }
    #list-desktop-bar [data-comparator-panel].is-open,
    #provider-desktop-bar [data-comparator-panel].is-open {
        visibility: visible;
        opacity: 1;
        transform: translateY(0);
        transition: opacity 200ms ease, transform 200ms ease;
    }
}
@media (prefers-reduced-motion: reduce) {
    #list-desktop-bar [data-comparator-panel],
    #provider-desktop-bar [data-comparator-panel] {
        transition: none;
    }
}
/* Padding posé sur le <footer> (pas sur le body) pour que le widget comparateur
   flottant (fixed bottom-right) ne recouvre pas le bas du contenu du footer.
   Mettre le padding sur body créerait un espace blanc visible sous le footer
   (puisque le body est blanc et le footer sombre). Étendre le footer lui-même
   garde son fond sombre jusqu'au bas du viewport. */
@media (min-width: 768px) {
    body:has(#list-desktop-bar) > footer,
    body:has(#provider-desktop-bar) > footer {
        padding-bottom: 60px;
    }
}
/* Trigger comparator : la flèche tourne 180° (↑ → ↓) quand le panel est ouvert,
   pour indiquer que cliquer va fermer. aria-expanded est posée par main.js sec.18.
   Cible mobile ET desktop ; sur mobile la flèche n'existe pas (icône fa-clone), donc
   le sélecteur ne matche rien et la règle est inerte. */
[data-comparator-toggle][aria-expanded="true"] [data-comparator-arrow] {
    transform: rotate(180deg);
}

/* NB : l'ancienne animation grid-template-rows des panneaux comparateur MOBILES
   (toutes pages) a été retirée avec le comparateur mobile (cf. CLAUDE.md). Les
   seuls [data-comparator-panel] restants vivent dans les widgets desktop flottants
   (#*-desktop-bar), entièrement stylés par le bloc "Comparateur desktop flottant"
   ci-dessus (pop opacity/translate, @media min-width:768px). Plus de baseline
   "mobile" à conserver ici. */

/* ============================================================
   Modal Filtres (list.php)
   ------------------------------------------------------------
   Composant dédié à la page Liste (vs. .app-dialog qui est un alert/confirm
   générique). Architecture :
   - .filters-modal             : conteneur racine fixed plein écran (z-index 80,
                                  au-dessus du header z-50 et des bottom bars z-70).
   - .filters-modal-overlay     : voile sombre sous le dialog, ferme au clic (geste JS).
   - .filters-modal-dialog      : la carte blanche. Desktop : centrée, max 720px, rounded-xl2,
                                  shadow. Mobile (max-width 767) : occupe tout l'écran.
   - flex column avec body scrollable (header/footer sticky par flex-shrink:0).

   Empilement avec les composants custom (multiselect/autocomplete/datepicker) :
   leurs panels sont en position:fixed à z-index 60, leur overlay mobile à z=55.
   Quand la modal est ouverte, on rehausse ces z-index via body.filters-modal-open
   pour qu'ils s'ouvrent au-dessus de la modal (sinon le dropdown serait masqué).

   Animation : opacity uniquement (PAS de transform sur le dialog) - un `transform`
   créerait un containing block qui ferait dériver les panels position:fixed des
   multiselect/autocomplete enfants. Voir avertissement dans CLAUDE.md §"Select custom".
   ============================================================ */
.filters-modal {
    position: fixed;
    /* IMPORTANT : ne pas utiliser `inset: 0`. Avec top:0 + bottom:0 posés
       ensemble, le navigateur ancre les DEUX bords au layout viewport (qui
       sur mobile inclut la zone derrière la barre d'URL) et la `height` est
       ignorée - résultat : la modal déborde en haut ET en bas tant que la
       barre d'URL est visible. On laisse `bottom: auto` et on fixe une hauteur
       en `dvh` (dynamic viewport height) qui suit la zone réellement visible. */
    top: 0;
    left: 0;
    right: 0;
    bottom: auto;
    height: 100vh;
    height: 100dvh;
    z-index: 80;
    display: flex;
    align-items: center;
    justify-content: center;
}
.filters-modal[hidden] {
    display: none !important;
}
/* Badge compteur du trigger mobile (list.php) : il est en `inline-flex` (0,1,0)
   → l'attribut [hidden] posé par main.js ne suffit pas (la règle auteur bat la
   règle UA / le preflight, cf. le même rattrapage sur .multiselect-options).
   Sans ça, le badge « 0 » resterait collé à l'icône filtres. Les autres cibles
   du même toggle ([data-filters-trigger-dot], [data-filters-trigger-count-wrap]
   et le compteur inline du pill desktop) n'ont pas de classe de display et
   restent masquées par l'attribut seul. */
[data-filters-trigger-count][hidden] { display: none; }
.filters-modal-overlay {
    position: absolute;
    inset: 0;
    background: rgba(0, 0, 0, 0.45);
    opacity: 0;
    transition: opacity 200ms ease;
}
.filters-modal[data-open] .filters-modal-overlay {
    opacity: 1;
}
/* Empilement : si une modal data-open en suit une autre dans le DOM (cas typique :
   modal "Numéros de téléphone" ouverte par-dessus une "Offre spéciale"), le voile
   de la modal du dessous est masqué pour éviter le cumul d'opacité (sinon le fond
   devient quasiment noir). Seul le voile de la modal la plus haute dans la pile
   (la dernière `[data-open]` du DOM) reste visible. `:has()` est supporté par tous
   les navigateurs modernes (Chrome 105+, Safari 15.4+, Firefox 121+). */
.filters-modal[data-open]:has(~ .filters-modal[data-open]) .filters-modal-overlay {
    opacity: 0;
}
.filters-modal-dialog {
    position: relative;
    background: #fff;
    width: 100%;
    max-width: 720px;
    max-height: calc(100vh - 60px);
    max-height: calc(100dvh - 60px);
    border-radius: 20px;
    box-shadow: 0 20px 60px rgba(0, 0, 0, 0.25);
    display: flex;
    flex-direction: column;
    /* `clip` et NON `hidden` : hidden laisse le dialog scrollable
       programmatiquement (scroll-into-view au focus d'un input masqué en
       position:absolute, ex. .filters-check) → le contenu se décale et
       l'utilisateur n'a aucun moyen de le ramener (pas de scrollbar). clip
       rogne pareil (coins arrondis) mais l'élément n'est plus une scroll box. */
    overflow: clip;
    margin: 30px;
    opacity: 0;
    transition: opacity 200ms ease;
}
.filters-modal[data-open] .filters-modal-dialog {
    opacity: 1;
}
@media (prefers-reduced-motion: reduce) {
    .filters-modal-overlay,
    .filters-modal-dialog {
        transition: none;
    }
}

/* Mobile : modal plein écran (pas de marges, pas de coins arrondis).
   Hauteur en `dvh` pour s'adapter à la barre d'URL du navigateur mobile
   (sans ça, la modal déborderait en haut/bas quand la barre d'URL est visible). */
@media (max-width: 767px) {
    .filters-modal-dialog {
        margin: 0;
        max-width: 100%;
        max-height: 100vh;
        max-height: 100dvh;
        height: 100vh;
        height: 100dvh;
        border-radius: 0;
        box-shadow: none;
    }
}

/* État de chargement de la modale dynamique (#dynamic-modal). Posé par provider.js
   §2 entre le clic du trigger et l'arrivée de la réponse AJAX : le dialog est
   ouvert immédiatement avec un spinner centré pour donner un feedback visuel à
   l'utilisateur (sinon réseau lent = page figée et l'utilisateur reclique).
   On override le flex-direction column par défaut le temps du chargement pour
   centrer le spinner ; le innerHTML est vidé par le JS donc rien d'autre à
   masquer. Hauteur minimum pour éviter un dialog "ratatiné" pendant le fetch. */
.filters-modal-dialog[data-loading] {
    align-items: center;
    justify-content: center;
    min-height: 220px;
}
.filters-modal-dialog[data-loading]::before {
    content: '';
    width: 36px;
    height: 36px;
    border: 3px solid #E5E7EB;
    border-top-color: rgb(var(--brand));
    border-radius: 50%;
    animation: filters-modal-spin 0.8s linear infinite;
}
@keyframes filters-modal-spin {
    to { transform: rotate(360deg); }
}
@media (prefers-reduced-motion: reduce) {
    .filters-modal-dialog[data-loading]::before {
        animation: none;
    }
}

/* Header sticky (titre + bouton X). Bordure basse subtile pour séparer du contenu. */
.filters-modal-header {
    flex-shrink: 0;
    display: flex;
    align-items: center;
    justify-content: space-between;
    padding: 20px 24px;
    border-bottom: 1px solid #E5E7EB;
    background: #fff;
}
.filters-modal-title {
    font-size: 17px;
    line-height: 24px;
    font-weight: 700;
    color: #211315;
    margin: 0;
}
/* Le bouton de fermeture utilise les classes Tailwind directement dans le markup
   (même style que `[data-comparator-toggle]` aria-label="Fermer" en bas de page) :
   cercle 36px bg-surface-light, hover bg-surface-border. Pas de règle CSS dédiée. */

/* Body scrollable (sections accordéon).
   overscroll-behavior: contain pour ne pas faire scroller le body derrière sur mobile. */
.filters-modal-body {
    flex: 1 1 auto;
    overflow-y: auto;
    overscroll-behavior: contain;
    padding: 8px 24px;
}

/* Une section = titre cliquable (trigger accordéon) + panel.
   Bordure basse pour séparer les sections. La dernière section n'a pas de bordure
   (sinon doublon avec le footer). */
.filters-section {
    border-bottom: 1px solid #E5E7EB;
    padding: 12px 0;
}
.filters-section:last-child {
    border-bottom: none;
}
.filters-section-trigger {
    width: 100%;
    display: flex;
    align-items: center;
    justify-content: space-between;
    padding: 10px 0;
    font-size: 15px;
    line-height: 20px;
    font-weight: 600;
    color: #211315;
    transition: color 150ms ease;
}
.filters-section-trigger:hover {
    color: rgb(var(--brand));
}
/* Wrapper du titre : permet d'aligner le titre + le badge dynamique côté gauche
   du trigger, le chevron restant collé à droite via justify-between du parent. */
.filters-section-title {
    display: inline-flex;
    align-items: center;
    gap: 10px;
    min-width: 0;
}
/* Badge de section : cercle rose avec compteur blanc à droite du titre.
   `hidden` (HTML attribute) au repos, retiré par main.js (section 19) dès que
   count > 0. min-width permet d'afficher 2 chiffres (ex. 10+) sans déformation. */
.filters-section-badge {
    display: inline-flex;
    align-items: center;
    justify-content: center;
    min-width: 20px;
    height: 20px;
    padding: 0 6px;
    border-radius: 999px;
    background: rgb(var(--brand));
    color: #fff;
    font-size: 11px;
    line-height: 1;
    font-weight: 700;
    flex-shrink: 0;
}
.filters-section-badge[hidden] {
    display: none;
}
/* Le panel est révélé par défaut (aria-expanded="true" au load). La classe `hidden`
   utilitaire de Tailwind est posée/retirée par le pattern accordéon (section 6b de main.js)
   pour replier. Padding-top léger pour aérer le titre du contenu. */
.filters-section-panel {
    padding: 12px 0 6px;
}
/* Liste verticale de champs dans la section "Votre recherche". */
.filters-fields {
    display: flex;
    flex-direction: column;
    gap: 12px;
}
/* Grille budget : deux inputs "min" / "max" côte à côte sur desktop,
   empilés sur très petit écran pour éviter de comprimer les champs. */
.filters-budget-grid {
    display: grid;
    grid-template-columns: 1fr 1fr;
    gap: 12px;
}
@media (max-width: 480px) {
    .filters-budget-grid {
        grid-template-columns: 1fr;
    }
}

/* Footer sticky (actions). Bordure haute pour le détacher du body scrollable. */
.filters-modal-footer {
    flex-shrink: 0;
    display: flex;
    align-items: center;
    justify-content: space-between;
    gap: 16px;
    padding: 16px 24px;
    border-top: 1px solid #E5E7EB;
    background: #fff;
}
.filters-modal-clear {
    font-size: 14px;
    line-height: 20px;
    font-weight: 600;
    color: #211315;
    text-decoration: underline;
    text-underline-offset: 4px;
    transition: color 150ms ease;
}
.filters-modal-clear:hover {
    color: rgb(var(--brand));
}
.filters-modal-submit {
    height: 48px;
    padding: 0 22px;
    border-radius: 12px;
    font-size: 14px;
    font-weight: 600;
    background: rgb(var(--brand));
    color: #fff;
    transition: background 150ms ease;
    display: inline-flex;
    align-items: center;
    justify-content: center;
    gap: 8px;
    line-height: 1;
}
.filters-modal-submit:hover {
    background: rgb(var(--brand-dark));
}
/* Mobile : footer reste sur une seule ligne (Effacer à gauche, Afficher à droite).
   On réduit juste le padding latéral et la taille du texte du bouton "Effacer" pour
   tenir sans wrap sur les petits écrans. */
@media (max-width: 767px) {
    .filters-modal-footer {
        gap: 12px;
        padding: 14px 16px calc(14px + env(safe-area-inset-bottom));
    }
    .filters-modal-clear {
        font-size: 13px;
        white-space: nowrap;
    }
    .filters-modal-submit {
        padding: 0 16px;
        white-space: nowrap;
    }
    .filters-modal-body {
        padding: 4px 16px;
    }
    .filters-modal-header {
        padding: 16px;
    }
}

/* Bloque le scroll body quand la modal est ouverte (posée par main.js).
   Rehausse aussi les z-index des panels custom (multiselect/autocomplete/datepicker)
   pour qu'ils s'ouvrent au-dessus de la modal (z=80) - sinon le dropdown serait masqué.
   Le panel par défaut est à z=60, l'overlay mobile à z=55 (cf. .multiselect-panel /
   .multiselect-overlay plus haut). */
body.filters-modal-open {
    overflow: hidden;
}
body.filters-modal-open .multiselect-panel {
    z-index: 95;
}
body.filters-modal-open .multiselect-overlay {
    z-index: 90;
}

/* ============================================================
   Photo viewer plein écran (provider.php > gallery-viewer)
   ------------------------------------------------------------
   Variante "fullscreen sombre" de la .filters-modal - réutilise la racine fixed
   z-80 + overlay + transition opacity (cf. provider.js §2 qui pose data-open),
   mais remplace la chrome blanche .filters-modal-dialog par un layout dédié
   stage + miniatures + légende. Le voile par défaut est rehaussé à 95% noir
   (vs 45% pour les modales contenu) pour un vrai mode immersif.

   Markup généré par render_photo_viewer() (cf. private/functions.php) :
   <div class="filters-modal photo-viewer">
     <div class="filters-modal-overlay"></div>            ← rendu rgba(0,0,0,.95)
     <div class="photo-viewer-dialog">
       <header.photo-viewer-header> ✕   compteur </header>
       <div.photo-viewer-stage> ◀  <img>  ▶ </div>
       <p.photo-viewer-caption>légende</p>
       <div.photo-viewer-thumbs>[thumbs]</div>
     </div>
   </div>

   Pilotage interactif (zoom, swipe, navigation, miniatures) dans provider.js §6.
============================================================ */
.photo-viewer .filters-modal-overlay {
    background: #000;
}
/* Override de `.img-loading` (main.js §1) - pose un fond gris #ededed sur le parent
   positionné de chaque <img> pendant le chargement. Dans le viewer on veut un fond
   100% noir partout (mode immersif), et un spinner blanc plus lisible sur fond sombre. */
.photo-viewer .img-loading {
    background: #000;
}
.photo-viewer .img-loading::after {
    border-color: rgba(255, 255, 255, 0.2);
    border-top-color: #fff;
}
/* Image du stage en échec (has-error rétabli par les listeners persistants de
   provider.js §6 à chaque swap de src) : l'icône fa-image du placeholder
   générique (.img-loading.has-error::before) est agrandie - 32px se perdrait
   dans le stage plein écran. */
.photo-viewer [data-pv-stage].has-error::before {
    font-size: 56px;
    color: #4a4a4a;
}
.photo-viewer-dialog {
    position: relative;
    z-index: 1;
    width: 100%;
    height: 100vh;
    height: 100dvh;
    /* Fond noir solide aussi sur le dialog : l'overlay est techniquement derrière
       (sibling en absolute), mais le dialog couvre tout le viewport et bloque la
       vue. Sans ce fond, on verrait à travers le dialog transparent. Combiné à
       l'overlay #000, on a 100% noir partout sauf la photo elle-même. */
    background: #000;
    display: grid;
    /* Lignes : header (auto) | stage (1fr, prend tout l'espace) | légende (auto) |
       miniatures (auto). Évite que la légende ou les miniatures écrasent le stage. */
    grid-template-rows: auto 1fr auto auto;
    opacity: 0;
    transition: opacity 200ms ease;
}
.photo-viewer[data-open] .photo-viewer-dialog {
    opacity: 1;
}
@media (prefers-reduced-motion: reduce) {
    .photo-viewer-dialog {
        transition: none;
    }
}
.photo-viewer-header {
    display: flex;
    align-items: center;
    justify-content: space-between;
    gap: 12px;
    padding: 16px 20px;
}
/* Boutons icône du header (fermer) - cercle sombre semi-transparent pour
   rester lisible par-dessus n'importe quelle photo claire ou foncée. */
.photo-viewer-iconbtn {
    width: 40px;
    height: 40px;
    border-radius: 9999px;
    background: rgba(255, 255, 255, 0.12);
    color: #fff;
    display: inline-flex;
    align-items: center;
    justify-content: center;
    transition: background 150ms ease;
    backdrop-filter: blur(8px);
    -webkit-backdrop-filter: blur(8px);
}
.photo-viewer-iconbtn:hover {
    background: rgba(255, 255, 255, 0.2);
}
.photo-viewer-counter {
    color: #fff;
    font-size: 14px;
    font-weight: 500;
    background: rgba(255, 255, 255, 0.12);
    backdrop-filter: blur(8px);
    -webkit-backdrop-filter: blur(8px);
    padding: 8px 14px;
    border-radius: 9999px;
}
/* Stage : conteneur de la grande image + flèches absolues. position:relative pour
   ancrer les flèches ; overflow:hidden pour clipper le zoom (image agrandie ne
   doit pas déborder sur le header / la légende). */
.photo-viewer-stage {
    position: relative;
    overflow: hidden;
    display: flex;
    align-items: center;
    justify-content: center;
    padding: 0 60px;
    /* touch-action: pan-y pour laisser le scroll vertical passer si l'utilisateur
       swipe vers le bas, tout en laissant nos handlers JS gérer le pan horizontal. */
    touch-action: pan-y;
}
.photo-viewer-image {
    max-width: 100%;
    max-height: 100%;
    object-fit: contain;
    user-select: none;
    -webkit-user-drag: none;
    transition: transform 200ms ease;
    cursor: zoom-in;
}
.photo-viewer-image.is-zoomed {
    cursor: grab;
    /* Pas de transition pendant le drag pour rester réactif au curseur. */
    transition: none;
}
.photo-viewer-image.is-zoomed:active {
    cursor: grabbing;
}
/* Flèches nav prev/next - alignées sur le style de .media-carousel-nav mais en
   variante "sombre" (cercle blanc semi-transparent sur fond sombre). */
.photo-viewer-nav {
    position: absolute;
    top: 50%;
    transform: translateY(-50%);
    width: 44px;
    height: 44px;
    border-radius: 9999px;
    background: rgba(255, 255, 255, 0.12);
    color: #fff;
    display: inline-flex;
    align-items: center;
    justify-content: center;
    transition: background 150ms ease;
    backdrop-filter: blur(8px);
    -webkit-backdrop-filter: blur(8px);
    z-index: 2;
}
.photo-viewer-nav:hover {
    background: rgba(255, 255, 255, 0.22);
}
.photo-viewer-nav--prev { left: 16px; }
.photo-viewer-nav--next { right: 16px; }
.photo-viewer-nav[hidden] { display: none; }

/* Légende - texte blanc centré, max-width pour ne pas s'étaler sur ultra-large.
   min-height (= line-height) pour que l'absence de légende ne fasse pas sauter
   la mise en page. */
.photo-viewer-caption {
    color: #fff;
    font-size: 14px;
    line-height: 20px;
    text-align: center;
    padding: 12px 24px;
    min-height: 20px;
    opacity: 0.9;
}
.photo-viewer-caption:empty {
    /* On garde min-height : grid-template-rows: auto sur la ligne légende sinon
       collapse à 0 et la transition entre photos avec/sans légende devient saccadée. */
    min-height: 20px;
}

/* Bande de miniatures. Scroll horizontal silencieux, chaque thumb en 80×60 avec
   ring rose quand active. Curseur `grab` au repos pour signaler le drag possible
   (handler JS dans provider.js §6 - on n'utilise pas le `.scroll-snap` global car
   son drag ignore les <button>, or nos miniatures sont des <button>). */
.photo-viewer-thumbs {
    display: flex;
    gap: 8px;
    padding: 16px 24px 24px;
    overflow-x: auto;
    overflow-y: hidden;
    /* `safe center` : centré tant que le contenu tient dans le container, mais
       bascule en flex-start dès qu'il déborde - sans ça, le premier item passe
       à scrollLeft négatif et devient inatteignable au scroll (bug classique
       de `center` + overflow). Supporté Chrome 93+, Firefox 63+, Safari 11+. */
    justify-content: safe center;
    scrollbar-width: none;
    cursor: grab;
    user-select: none;
    -webkit-user-select: none;
}
.photo-viewer-thumbs::-webkit-scrollbar {
    display: none;
}
.photo-viewer-thumbs.is-grabbing {
    cursor: grabbing;
}
/* Pendant le drag, on neutralise les pointer-events sur les miniatures pour ne pas
   intercepter le mousemove ; le click qui suit est bloqué côté JS (cf. provider.js §6). */
.photo-viewer-thumbs.is-grabbing .photo-viewer-thumb {
    pointer-events: none;
}
.photo-viewer-thumb {
    position: relative; /* containing block pour le spinner .img-loading appliqué par main.js §1 sur le parent de l'<img> */
    flex: 0 0 auto;
    width: 80px;
    height: 60px;
    border-radius: 8px;
    overflow: hidden;
    background: #1a1a1a;
    opacity: 0.5;
    transition: opacity 150ms ease, box-shadow 150ms ease, transform 150ms ease;
    cursor: pointer;
}
.photo-viewer-thumb img {
    width: 100%;
    height: 100%;
    object-fit: cover;
    pointer-events: none;
}
/* Miniature en échec de chargement (classe posée par provider.js §6) : l'<img>
   cassée est masquée, une icône fa-image prend place - même technique que le
   placeholder générique .img-loading.has-error (main.js §1). */
.photo-viewer-thumb.has-error img {
    opacity: 0;
}
.photo-viewer-thumb.has-error::before {
    content: "\f03e"; /* fa-image */
    font-family: "Font Awesome 6 Pro";
    font-weight: 900;
    position: absolute;
    top: 50%;
    left: 50%;
    transform: translate(-50%, -50%);
    color: #4a4a4a;
    font-size: 16px;
    pointer-events: none;
}
.photo-viewer-thumb:hover {
    opacity: 0.85;
}
.photo-viewer-thumb.is-active {
    opacity: 1;
    box-shadow: 0 0 0 2px rgb(var(--brand));
}

/* ----- Video viewer ----- (variante du photo viewer pour vidéos YouTube)
   L'iframe remplit 100% du stage. YouTube gère son letterboxing 16/9 en interne ;
   sur fond #000 les bandes sont invisibles.

   Pourquoi position:absolute + inset:0 sur le wrap ?
     Le stage hérite `display:flex; align-items:center; justify-content:center` de
     `.photo-viewer-stage`. Un flex item avec `width:100%; height:100%` ne se
     dimensionne pas toujours comme attendu (la `width` est respectée mais le
     flex peut shrink, et `align-items:center` annule le stretch cross-axis).
     `position:absolute; inset:0` court-circuite tout ça : le wrap occupe
     littéralement les 4 bords du stage (qui est `position:relative`). */
.video-viewer-stage {
    padding: 0;
}
.video-viewer-iframe-wrap {
    position: absolute;
    inset: 0;
    background: #000;
}
.video-viewer-iframe-wrap iframe {
    width: 100%;
    height: 100%;
    display: block;
    border: 0;
}
/* Miniatures vidéo : overlay sombre + icône play centrée pour distinguer des
   miniatures photo. L'overlay reste visible quand la miniature est active
   (renvoie l'idée "vidéo en cours de visionnage"). */
.video-viewer-thumb {
    position: relative;
}
.video-viewer-thumb::before {
    content: "";
    position: absolute;
    inset: 0;
    background: rgba(0, 0, 0, 0.35);
    pointer-events: none;
    transition: background 150ms ease;
}
.video-viewer-thumb:hover::before {
    background: rgba(0, 0, 0, 0.2);
}
.video-viewer-thumb-play {
    position: absolute;
    top: 50%;
    left: 50%;
    transform: translate(-50%, -50%);
    color: #fff;
    font-size: 14px;
    pointer-events: none;
    text-shadow: 0 1px 3px rgba(0, 0, 0, 0.5);
}
/* Miniature vidéo en échec (thumb YouTube introuvable - has-error posé par
   main.js §1, thumbs statiques scannées au boot) : le ::before du thumb vidéo
   est DÉJÀ pris par le voile sombre ci-dessus, et la règle générique
   .img-loading.has-error::before (spécificité 0,2,0) l'écraserait en glyphe
   fa-image déformé (top/left 50% + right/bottom hérités du inset:0). On
   restaure le voile à spécificité supérieure : l'<img> cassée est masquée par
   la règle générique (`> img { opacity: 0 }`) et le picto play (span séparé,
   toujours centré) sert de placeholder - une tuile sombre + play lit toujours
   « vidéo ». */
.video-viewer-thumb.img-loading.has-error::before {
    content: "";
    top: 0;
    left: 0;
    transform: none;
    background: rgba(0, 0, 0, 0.35);
}

/* Mobile : padding réduits, flèches plus petites et près du bord, miniatures
   conservées en format compact (56×42, scrollable horizontalement) - la bande
   reste lisible et permet le jump direct sans avoir à swiper N photos.
   Le video-viewer-thumb-play passe en plus petit pour matcher la mini-thumb. */
@media (max-width: 767px) {
    .photo-viewer-header {
        padding: 12px 14px;
        padding-top: max(12px, env(safe-area-inset-top));
    }
    /* Mobile : pas de padding latéral - l'image principale colle aux bords du viewport
       (UX immersive façon Instagram/Photos). Les flèches restent lisibles car elles ont
       un fond noir semi-transparent + backdrop-blur. */
    .photo-viewer-stage {
        padding: 0;
    }
    .photo-viewer-nav {
        width: 38px;
        height: 38px;
    }
    .photo-viewer-nav--prev { left: 8px; }
    .photo-viewer-nav--next { right: 8px; }
    .photo-viewer-caption {
        padding: 8px 16px;
    }
    .photo-viewer-thumbs {
        gap: 6px;
        padding: 8px 12px;
        padding-bottom: max(10px, env(safe-area-inset-bottom));
        /* Pas d'override de justify-content : on hérite du `safe center` desktop
           - centré tant que ça tient (ex. 3 vidéos), bascule en flex-start dès
           que ça déborde (ex. 20 photos), sans bug de scrollLeft négatif. */
    }
    .photo-viewer-thumb {
        width: 56px;
        height: 42px;
        border-radius: 6px;
    }
    .video-viewer-thumb-play {
        font-size: 11px;
    }
}

/* ============================================================
   Galerie overlay plein écran (provider.php - écran intermédiaire)
   ------------------------------------------------------------
   Variante "fullscreen claire" de la .filters-modal - pendant lumineux du
   .photo-viewer (variante sombre). Écran type Airbnb/Mariages.net qui
   s'intercale ENTRE la fiche et les visionneuses : tabs Photos/Vidéos/
   Visite virtuelle + mosaïque scrollable + sidebar devis desktop.

   Markup généré par render_gallery_overlay() (cf. private/functions.php) :
   <div class="filters-modal gallery-overlay">
     <div class="filters-modal-overlay"></div>
     <div class="gallery-overlay-dialog">
       <header>  tabs + actions (Tailwind dans le markup)  </header>
       <div.gallery-overlay-body>  retour + sidebar + panneaux  </div>
       <div>  bottom bar mobile (Tailwind dans le markup)  </div>
     </div>
   </div>

   Seul le chrome (plein écran, grid rows, transition [data-open]) vit ici -
   tout l'habillage interne est en utilitaires Tailwind dans le helper PHP.
   IMPORTANT empilement : l'overlay doit être rendu AVANT les visionneuses
   photo/vidéo dans le DOM (z-index identiques → l'ordre DOM décide qui est
   peint au-dessus quand deux modales sont ouvertes en même temps).
   Pilotage (tabs, ouverture ciblée, iframe lazy) dans provider.js §6b.
============================================================ */
.gallery-overlay-dialog {
    position: relative;
    z-index: 1;
    width: 100%;
    /* min-width 0 : le dialog est un flex item de .filters-modal (min-width:auto par
       défaut) - sans ça, une min-content interne large (rangée d'onglets) pourrait
       l'élargir au-delà du viewport sur mobile. */
    min-width: 0;
    height: 100vh;
    height: 100dvh;
    background: #fff;
    display: grid;
    /* Colonne contrainte minmax(0, 1fr) : empêche les items (header avec rangée
       d'onglets, bottom bar) d'élargir la colonne au-delà du dialog - leur
       min-content déborderait sinon (le tablist scrolle en interne à la place). */
    grid-template-columns: minmax(0, 1fr);
    /* Lignes : header tabs (auto) | body scrollable (1fr) | bottom bar mobile
       (auto - vide en desktop, la div est cachée par md:hidden). */
    grid-template-rows: auto 1fr auto;
    opacity: 0;
    transition: opacity 200ms ease;
}
.gallery-overlay[data-open] .gallery-overlay-dialog {
    opacity: 1;
}
@media (prefers-reduced-motion: reduce) {
    .gallery-overlay-dialog {
        transition: none;
    }
}
/* Body scrollable : c'est ICI que scrolle la mosaïque (le body de la page est
   verrouillé par le système de modales). overscroll-behavior pour ne pas faire
   défiler la fiche derrière une fois arrivé en butée. */
.gallery-overlay-body {
    overflow-y: auto;
    overscroll-behavior: contain;
    /* Réserve la largeur de la scrollbar des DEUX côtés : le container-x du body
       reste centré sur le même axe que celui du header (qui ne scrolle pas). Sans
       ça, la scrollbar (10px) décale le contenu de ~5px vers la gauche et les
       onglets ne s'alignent plus avec la card/mosaïque en dessous. Sur mobile les
       scrollbars sont en overlay (largeur 0) → aucun espace réservé. */
    scrollbar-gutter: stable both-edges;
}
/* La croix de fermeture est absolue au bord droit du viewport (hors container-x).
   On ne réserve sa place dans le padding droit du container QUE sur les écrans où
   le container touche les bords (max-width 1280 + 2×56px de zone croix = 1392px) :
   les actions passeraient dessous sinon. Au-delà, la croix vit dans la marge
   extérieure du container et les actions restent alignées pile sur le bord droit
   du contenu (mosaïque) en dessous. 36px de croix + 20px d'offset + 20px d'air. */
@media (min-width: 768px) and (max-width: 1392px) {
    .gallery-overlay-header > .container-x {
        padding-right: 76px;
    }
}
/* Rangée d'onglets scrollable (déborde en mobile) sans scrollbar visible - même
   approche que la nav de #provider-anchor-bar (la scrollbar globale de 10px
   ferait un trait disgracieux en plein milieu du header). */
.gallery-overlay [role="tablist"] {
    -ms-overflow-style: none;
    scrollbar-width: none;
}
.gallery-overlay [role="tablist"]::-webkit-scrollbar {
    display: none;
}

/* ---------- Champ "Nombre d'invités" ----------
   Aligné sur le pattern de sos.php (input number nu, icône user-group à droite,
   pas de boutons stepper). Le placeholder "ex. 80" sert d'aide à la saisie quand
   le champ est focusé en mode floating. Seul style dédié : masquage des spinners
   natifs (apparence cross-browser non uniforme).
   ---------------------------------------------- */
[data-filters-stepper-input]::-webkit-outer-spin-button,
[data-filters-stepper-input]::-webkit-inner-spin-button {
    -webkit-appearance: none;
    margin: 0;
}
[data-filters-stepper-input][type="number"] {
    -moz-appearance: textfield;
    appearance: textfield;
}

/* ---------- Slider "Distance maximum" ----------
   Input range stylisé. Le remplissage du track (rose jusqu'au thumb) utilise une
   variable CSS `--filters-range-progress` (0% → 100%) mise à jour par main.js à
   chaque `input`. linear-gradient construit le track rose | gris à partir de %.
   ----------------------------------------------- */
.filters-range {
    display: flex;
    flex-direction: column;
    gap: 8px;
    padding-top: 6px;
}
.filters-range-header {
    display: flex;
    align-items: center;
    justify-content: space-between;
    gap: 12px;
}
.filters-range-label {
    font-size: 14px;
    line-height: 20px;
    color: #211315;
    font-weight: 500;
}
.filters-range-value {
    font-size: 14px;
    line-height: 20px;
    color: #211315;
    font-weight: 700;
}
.filters-range-bounds {
    display: flex;
    justify-content: space-between;
    font-size: 12px;
    line-height: 16px;
    color: #757575;
}
/* Reset apparence native du <input type="range"> sur tous les navigateurs.
   Le track est dessiné par linear-gradient, le thumb par ::-webkit-slider-thumb
   et ::-moz-range-thumb. */
.filters-range-input {
    -webkit-appearance: none;
    appearance: none;
    width: 100%;
    height: 6px;
    background: linear-gradient(to right,
        rgb(var(--brand)) 0%,
        rgb(var(--brand)) var(--filters-range-progress, 0%),
        #E5E7EB var(--filters-range-progress, 0%),
        #E5E7EB 100%);
    border-radius: 999px;
    outline: none;
    cursor: pointer;
    margin: 8px 0;
}
.filters-range-input:focus-visible {
    box-shadow: 0 0 0 3px rgb(var(--brand) / 0.20);
}
.filters-range-input::-webkit-slider-thumb {
    -webkit-appearance: none;
    appearance: none;
    width: 22px;
    height: 22px;
    border-radius: 50%;
    background: rgb(var(--brand));
    border: 3px solid #fff;
    box-shadow: 0 2px 8px rgb(var(--brand) / 0.35);
    cursor: pointer;
    transition: transform 100ms ease;
}
.filters-range-input::-webkit-slider-thumb:hover {
    transform: scale(1.08);
}
.filters-range-input::-moz-range-thumb {
    width: 22px;
    height: 22px;
    border-radius: 50%;
    background: rgb(var(--brand));
    border: 3px solid #fff;
    box-shadow: 0 2px 8px rgb(var(--brand) / 0.35);
    cursor: pointer;
}
.filters-range-input::-moz-range-track {
    background: transparent;
}

/* ---------- Radios verticaux (modal filtres - section Restauration) ----------
   Liste de radios en colonne, avec un puce circulaire rose qui reprend le style
   `.multiselect-check` mode single (cercle vide → cercle rose plein quand checked).
   ---------------------------------------------------------- */
.filters-radio-group {
    display: flex;
    flex-direction: column;
    gap: 4px;
    margin-top: 14px;
}
.filters-radio {
    position: relative; /* ancre l'input radio masqué - cf. .filters-check */
    display: flex;
    align-items: center;
    gap: 12px;
    padding: 8px 0;
    font-size: 14px;
    line-height: 20px;
    color: #211315;
    cursor: pointer;
    user-select: none;
}
.filters-radio input[type="radio"] {
    position: absolute;
    opacity: 0;
    width: 0;
    height: 0;
    pointer-events: none;
}
.filters-radio-bullet {
    flex-shrink: 0;
    width: 18px;
    height: 18px;
    border-radius: 999px;
    border: 1.5px solid #d9d9d9;
    background: #fff;
    display: inline-flex;
    align-items: center;
    justify-content: center;
    transition: background-color 0.15s ease, border-color 0.15s ease;
}
.filters-radio-bullet::after {
    content: "";
    width: 8px;
    height: 8px;
    border-radius: 999px;
    background: #fff;
    opacity: 0;
    transition: opacity 0.12s ease;
}
.filters-radio input:checked + .filters-radio-bullet {
    background: rgb(var(--brand));
    border-color: rgb(var(--brand));
}
.filters-radio input:checked + .filters-radio-bullet::after {
    opacity: 1;
}
.filters-radio input:focus-visible + .filters-radio-bullet {
    box-shadow: 0 0 0 3px rgb(var(--brand) / 0.25);
}
/* Sous-groupe conditionnel (Restauration > Oui → choix traiteur), révélé/masqué
   par main.js §19 via [data-filters-show-when] - même mécanique que les sections
   conditionnelles, appliquée à un bloc interne. Présenté en ENCART gris clair
   arrondi (et non en simple indentation) pour marquer le niveau hiérarchique
   sous les pills Oui/Non. */
.filters-subradios {
    padding: 2px 14px;
    background: #F5F5F5;       /* surface-light */
    border-radius: 12px;
}
.filters-subradios[hidden] {
    display: none;
}
/* Apparition en fondu + léger glissement au passage sur "Oui". Le transform ne
   vit que le temps de l'animation et l'encart ne contient AUCUN composant à
   panel position:fixed (uniquement des radios) - pas de piège containing block
   (cf. avertissement .filters-modal-dialog). */
@keyframes filters-subradios-reveal {
    from { opacity: 0; transform: translateY(-4px); }
    to   { opacity: 1; transform: none; }
}
.filters-subradios:not([hidden]) {
    animation: filters-subradios-reveal 0.18s ease;
}
@media (prefers-reduced-motion: reduce) {
    .filters-subradios:not([hidden]) {
        animation: none;
    }
}

/* Rangée de sous-option : icône dans une pastille blanche + libellé + puce radio
   à droite (alignée sur les check-lists .filters-check, case à droite). Posée en
   modificateur de .filters-radio pour hériter des états de .filters-radio-bullet
   (checked/focus-visible) sans dupliquer la cascade. Rangées ≥ 48px : zone
   tactile confortable en mobile. */
.filters-radio.filters-subchoice {
    gap: 12px;
    padding: 10px 0;
    min-height: 54px;
}
.filters-radio.filters-subchoice + .filters-radio.filters-subchoice {
    border-top: 1px solid #E5E7EB;
}
.filters-subchoice-icon {
    flex-shrink: 0;
    width: 34px;
    height: 34px;
    border-radius: 10px;
    background: #fff;
    box-shadow: 0 1px 2px rgba(0, 0, 0, 0.06);
    display: inline-flex;
    align-items: center;
    justify-content: center;
    font-size: 14px;
    color: #757575;            /* ink-500 */
    transition: color 0.15s ease;
}
.filters-subchoice-label {
    flex: 1;
    min-width: 0;
}
.filters-subchoice:has(input:checked) .filters-subchoice-icon {
    color: rgb(var(--brand));
}

/* ---------- Check-lists inline (modal filtres) ----------
   Rangée label (gauche) + case à cocher (droite) posée à plat dans le panel de
   section - visuel de la case calqué sur .multiselect-check. Sections Types de
   lieu, Environnement, Loisirs & activités, Accès, Label. */
.filters-check-group {
    display: flex;
    flex-direction: column;
}
.filters-check {
    /* relative OBLIGATOIRE : ancre l'input masqué (position:absolute ci-dessous)
       dans la rangée. Sans ça, son containing block remonte au
       .filters-modal-dialog (relative) → au clic, le focus-scroll natif vers
       l'input « mal placé » scrollait le dialog entier (cf. .filters-pill qui
       a toujours été relative et n'a jamais eu le bug). */
    position: relative;
    display: flex;
    align-items: center;
    justify-content: space-between;
    gap: 12px;
    padding: 9px 0;
    font-size: 15px;
    line-height: 20px;
    color: #211315;
    cursor: pointer;
    user-select: none;
}
.filters-check-label {
    flex: 1;
    min-width: 0;
}
.filters-check input[type="checkbox"] {
    position: absolute;
    opacity: 0;
    width: 0;
    height: 0;
    pointer-events: none;
}
.filters-check-box {
    flex-shrink: 0;
    width: 22px;
    height: 22px;
    border-radius: 6px;
    border: 1.5px solid #d9d9d9;
    background: #fff;
    display: inline-flex;
    align-items: center;
    justify-content: center;
    color: #fff;
    transition: background-color 0.15s ease, border-color 0.15s ease;
}
.filters-check-box::after {
    content: "\f00c"; /* fa-check - police Pro embarquée (cf. .multiselect-check) */
    font-family: "Font Awesome 6 Pro";
    font-weight: 900;
    font-size: 12px;
    opacity: 0;
    transition: opacity 0.12s ease;
}
.filters-check:hover .filters-check-box {
    border-color: rgb(var(--brand));
}
.filters-check input:checked ~ .filters-check-box {
    background: rgb(var(--brand));
    border-color: rgb(var(--brand));
}
.filters-check input:checked ~ .filters-check-box::after {
    opacity: 1;
}
.filters-check input:focus-visible ~ .filters-check-box {
    box-shadow: 0 0 0 3px rgb(var(--brand) / 0.25);
}

/* ---------- Pills de filtre (choix unique décochable) ----------
   Checkboxes cachées stylées en pills - l'exclusivité par groupe est gérée par
   main.js §19 ([data-filters-single]). Style aligné sur .filters-popover-pill. */
.filters-pills {
    display: flex;
    flex-wrap: wrap;
    gap: 10px;
}
/* Variante segmentée : les pills se partagent la rangée à parts égales
   (Restauration > Oui/Non). Reste sur une seule ligne à toutes les largeurs. */
.filters-pills--split {
    flex-wrap: nowrap;
}
.filters-pills--split > .filters-pill {
    flex: 1 1 0;
    min-width: 0;
}
.filters-pill {
    position: relative;
    display: inline-flex;
    align-items: center;
    justify-content: center;
    min-height: 46px;
    padding: 12px 18px;
    border: 1.5px solid #D9D9D9;
    border-radius: 10px;
    background: #fff;
    color: #0A0A0A;
    font-size: 14px;
    font-weight: 600;
    line-height: 1.2;
    text-align: center;
    cursor: pointer;
    user-select: none;
    transition: border-color 0.15s ease, background-color 0.15s ease, color 0.15s ease;
}
/* Input masqué - checkbox (pills décochables) OU radio natif (Oui/Non
   Restauration : exclusivité native, mécanique radio de main.js §19). */
.filters-pill input {
    position: absolute;
    opacity: 0;
    width: 0;
    height: 0;
    pointer-events: none;
}
.filters-pill:hover {
    border-color: rgb(var(--brand));
}
.filters-pill:has(input:checked) {
    border-color: rgb(var(--brand));
    color: rgb(var(--brand));
    background: rgb(var(--brand) / 0.1);
}
.filters-pill:has(input:focus-visible) {
    box-shadow: 0 0 0 3px rgb(var(--brand) / 0.25);
}

/* ---------- Ligne switch (Disposition > Événement dansant) ----------
   Réutilise l'interrupteur 100 % CSS .switch/.switch-input/.switch-knob
   (cf. render_switch) encadré des libellés Non / Oui. */
.filters-switch-row {
    display: flex;
    align-items: center;
    justify-content: space-between;
    gap: 16px;
    padding: 8px 0;
    font-size: 15px;
    line-height: 20px;
    color: #211315;
    cursor: pointer;
    user-select: none;
}
.filters-switch-toggle {
    position: relative; /* ancre le .switch-input masqué - cf. .filters-check */
    display: inline-flex;
    align-items: center;
    gap: 10px;
    flex-shrink: 0;
    font-size: 14px;
}

/* ---------- Chips de catégories (Loisirs & activités) ----------
   Checkboxes cachées stylées en chips icône + libellé, grille 2 colonnes.
   Cocher une chip révèle son groupe de checkboxes [data-filters-cat-group]
   (main.js §19). */
.filters-chips {
    display: grid;
    grid-template-columns: repeat(2, minmax(0, 1fr));
    gap: 10px;
}
.filters-chip {
    position: relative;
    display: inline-flex;
    align-items: center;
    justify-content: center;
    gap: 10px;
    min-height: 48px;
    padding: 10px 12px;
    border: 1.5px solid #D9D9D9;
    border-radius: 12px;
    background: #fff;
    color: #0A0A0A;
    font-size: 14px;
    font-weight: 600;
    line-height: 1.2;
    cursor: pointer;
    user-select: none;
    transition: border-color 0.15s ease, background-color 0.15s ease, color 0.15s ease;
}
.filters-chip i {
    font-size: 16px;
    flex-shrink: 0;
}
.filters-chip input[type="checkbox"] {
    position: absolute;
    opacity: 0;
    width: 0;
    height: 0;
    pointer-events: none;
}
.filters-chip:hover {
    border-color: rgb(var(--brand));
}
.filters-chip:has(input:checked) {
    border-color: rgb(var(--brand));
    color: rgb(var(--brand));
    background: rgb(var(--brand) / 0.1);
}
.filters-chip:has(input:focus-visible) {
    box-shadow: 0 0 0 3px rgb(var(--brand) / 0.25);
}
.filters-cat-group {
    margin-top: 4px;
}
.filters-cat-group[hidden] {
    display: none;
}
.filters-cat-group-title {
    font-size: 14px;
    font-weight: 700;
    color: #211315;
    margin: 0 0 2px;
}

/* ---------- Popovers de la barre de filtres (page list.php) ----------
   Trois popovers partagent ce squelette : Budget (5 pills), Nombre d'invités
   (input number), Hébergement (slider + radios + Effacer). Chacun est ancré
   sous son bouton trigger dans #list-filters-bar via un parent `position: relative`.
   Pas un composant générique - raccourcis dédiés vers les sections respectives
   de la modal des filtres (sections 20/21/22 de main.js pour la sync).
   - position: absolute dans le parent relative ; la barre #list-filters-bar
     n'a pas d'overflow → le popover déborde librement vers le bas.
   - Flèche : carré pivoté 45° centré sur l'icône du trigger (≈ 22px depuis le
     bord gauche du bouton, padding-left 20px + demi-icône). Les 3 triggers ont
     une icône cadrée pareil → la même offset gauche fonctionne pour les 3. */
.filters-popover {
    position: absolute;
    top: calc(100% + 12px);
    left: 0;
    z-index: 45;
    width: 360px;
    max-width: calc(100vw - 30px);
    background: #fff;
    border-radius: 16px;
    box-shadow: 0 8px 30px rgba(0, 0, 0, 0.12);
    padding: 20px;
    display: flex;
    flex-direction: column;
    gap: 14px;
}
.filters-popover[hidden] {
    display: none;
}
.filters-popover::before {
    content: "";
    position: absolute;
    top: -6px;
    left: 22px;
    width: 14px;
    height: 14px;
    background: #fff;
    transform: rotate(45deg);
    box-shadow: -2px -2px 6px rgba(0, 0, 0, 0.03);
}
.filters-popover-title {
    font-size: 16px;
    font-weight: 700;
    color: #0A0A0A;
    margin: 0;
    line-height: 1.2;
}
.filters-popover-options {
    display: flex;
    flex-wrap: wrap;
    gap: 10px;
}
.filters-popover-pill {
    height: 40px;
    padding: 0 14px;
    border: 1.5px solid #D9D9D9;
    border-radius: 10px;
    background: #fff;
    color: #0A0A0A;
    font-size: 13px;
    font-weight: 600;
    line-height: 1;
    white-space: nowrap;
    cursor: pointer;
    transition: border-color 0.15s ease, background-color 0.15s ease, color 0.15s ease;
}
.filters-popover-pill:hover {
    border-color: rgb(var(--brand));
}
.filters-popover-pill[aria-pressed="true"] {
    border-color: rgb(var(--brand));
    color: rgb(var(--brand));
    background: rgb(var(--brand) / 0.1);
}
.filters-popover-pill:focus-visible {
    outline: none;
    box-shadow: 0 0 0 3px rgb(var(--brand) / 0.25);
}
/* Section Hébergement (popover ET modal) : sépare visuellement le slider couchages
   du groupe radio distance par une fine ligne, et resserre l'espacement par
   rapport au défaut (qui s'additionnait au margin-top: 14px global du
   .filters-radio-group → écart d'origine ≈ 28px, trop aéré). On annule ce
   margin-top et on s'appuie sur padding-top + border-top du radio group.
   Les deux contextes (popover + section modal) partagent le même rendu. */
.filters-popover--hebergement {
    gap: 10px;
}
.filters-popover--hebergement .filters-radio-group,
[data-filters-section="hebergement"] .filters-radio-group {
    margin-top: 0;
    padding-top: 10px;
    border-top: 1px solid #E5E5E5;
}
[data-filters-section="hebergement"] .filters-fields {
    gap: 10px;
}

/* Triggers actifs : bordure + texte/icône brand quand au moins une valeur de la
   section correspondante est appliquée. Le label des triggers Budget/Invités est
   remplacé par la valeur sélectionnée (cf. main.js sections 20/21) ; le trigger
   Hébergement garde son label "Hébergement" (combiner slider + radios serait
   ambigu en un mot). */
[data-budget-trigger].is-active,
[data-guests-trigger].is-active,
[data-hebergement-trigger].is-active {
    border-color: rgb(var(--brand));
    color: rgb(var(--brand));
}
[data-budget-trigger].is-active i,
[data-guests-trigger].is-active i,
[data-hebergement-trigger].is-active i {
    color: rgb(var(--brand));
}

/* ---------- Configuration des espaces (provider.php) ----------
   Table comparative responsive avec un markup UNIQUE.
   - Desktop ≥ md : layout table natif (thead visible, valeurs centrées dans
     chaque cellule). L'icône et le label embarqués dans chaque <td> sont
     masqués ; seul le span `.config-cell-value` reste visible.
   - Mobile < md : la table est transformée en cards par CSS pur. Le `thead`
     est masqué et chaque <tr> devient une card autonome en grid 2 colonnes
     (le th[scope=row] occupe les 2 colonnes pour le header photo+nom, les
     <td> se répartissent ensuite à raison d'un par colonne). Les <td> à
     valeur '-' / null reçoivent `.is-empty` et sont masqués pour alléger
     la card (le desktop reste exhaustif pour la comparaison).
   ============================================================ */

/* Desktop : on cache picto + label dans chaque cellule.
   Le <div class="config-cell-meta"> reste un block enfant du <td>, mais ne
   contient que `.config-cell-value` une fois le label masqué - le `text-align:
   center` du td (classe Tailwind `.text-center`) centre la valeur comme avant. */
.config-table .config-cell-icon,
.config-table .config-cell-label {
    display: none;
}

@media (max-width: 767px) {
    /* Sortir du mode "table" : chaque sous-élément redevient un block. */
    .config-table,
    .config-table thead,
    .config-table tbody,
    .config-table tr,
    .config-table th,
    .config-table td {
        display: block;
    }
    /* En-têtes de colonnes inutiles ici : chaque cellule embarque son
       propre picto + label (cf. .config-cell-icon / .config-cell-label). */
    .config-table thead {
        display: none;
    }
    /* Le wrapper de la table et la table elle-même deviennent transparents - le
       rendu "card" vit sur chaque <tr>. Sans ça, on aurait une chrome blanche
       imbriquée dans une chrome blanche (effet boîte dans une boîte). */
    .config-table-wrap {
        background: transparent;
        box-shadow: none;
        border-radius: 0;
        overflow: visible;
    }
    .config-table {
        background: transparent;
        box-shadow: none;
        border-radius: 0;
    }
    /* Carrousel : le tbody devient le scroll container, chaque tr un slide.
       - Full-bleed via marges négatives -15px (compense le padding 15px du
         .container-x parent) pour que les bords du scroll touchent le viewport
         et que la card suivante "peek" aux bords.
       - scroll-snap-type natif + swipe tactile, pas de JS.
       - Pas de scrollbar visible (cohérent avec les autres carrousels du projet). */
    .config-table tbody {
        display: flex;
        gap: 15px;
        overflow-x: auto;
        overflow-y: hidden;
        scroll-snap-type: x mandatory;
        scroll-padding-left: 15px;
        margin-left: -15px;
        margin-right: -15px;
        padding: 0 15px;
        scrollbar-width: none;
        -ms-overflow-style: none;
    }
    .config-table tbody::-webkit-scrollbar {
        display: none;
    }
    /* Chaque ligne = une card slide.
       Grid 2 cols : le th[scope=row] s'étend sur 2 cols (header photo+nom),
       les <td> qui suivent prennent 1 col chacun.
       Largeur fixe : `calc(100vw - 60px)` laisse ~30px à droite pour le peek
       de la card suivante ; `max-width: 320px` borne la taille sur grand mobile
       pour éviter qu'elle ne s'étire trop.
       Bordure 1px (au lieu de shadow-card) : rendu plus sobre, conforme à la demande. */
    .config-table tbody tr {
        display: grid;
        grid-template-columns: 1fr 1fr;
        gap: 14px 16px;
        align-content: start;
        background: #ffffff;
        border: 1px solid #D9D9D9;
        border-radius: 20px;
        padding: 20px;
        flex-shrink: 0;
        width: calc(100vw - 60px);
        max-width: 320px;
        scroll-snap-align: start;
        /* align-self: stretch (default des flex items) → toutes les cards
           s'étirent à la hauteur de la plus haute pour un alignement propre. */
    }
    /* Header de ligne (photo + nom) : pleine largeur, séparateur en bas.
       Position static obligatoire : sinon le `sticky left-0` posé par les
       classes Tailwind reste actif et fige la cellule à gauche du scroll. */
    .config-table th[scope="row"].config-row-head {
        grid-column: 1 / -1;
        padding: 0 0 16px 0;
        border-top: 0;
        border-bottom: 1px solid #D9D9D9;
        position: static;
        background: transparent;
    }
    .config-table .config-row-head-inner {
        min-width: 0;
    }
    .config-table .config-room-thumb {
        width: 56px;
        height: 56px;
        border-radius: 10px;
    }
    .config-table .config-room-name {
        font-size: 16px;
        font-weight: 700;
        white-space: normal;
    }
    /* Cellules de stats : icône à gauche, label + valeur empilés à droite.
       `align-items: center` aligne le picto sur la mi-hauteur du bloc texte
       (label + valeur) plutôt qu'en haut - visuellement plus équilibré. */
    .config-table td.config-cell {
        padding: 0;
        text-align: left;
        white-space: normal;
        border-top: 0;
        display: flex;
        align-items: center;
        gap: 10px;
    }
    /* Toutes les configurations restent visibles en mobile (y compris celles à
       '-'/null) : on garde la grille complète pour que chaque card affiche le
       même set de pictos, ce qui permet de comparer les espaces d'un coup d'œil
       en swipant le carrousel. */
    .config-table .config-cell-icon {
        display: inline-block;
        color: rgb(var(--brand));
        font-size: 16px;
        width: 20px;
        text-align: center;
        flex-shrink: 0;
    }
    .config-table .config-cell-meta {
        display: flex;
        flex-direction: column;
        gap: 2px;
        min-width: 0;
    }
    .config-table .config-cell-label {
        display: block;
        font-size: 12px;
        color: #757575;
        line-height: 1.2;
    }
    .config-table .config-cell-value {
        font-size: 14px;
        font-weight: 500;
        line-height: 1.2;
        color: #0A0A0A;
    }
}

/* ============================================================
   Accordion (composant [data-accordion] - voir COMPONENTS.md)
   Markup : .accordion > .accordion-item > .accordion-trigger
            + .accordion-panel > .accordion-panel-inner.
   Animation hauteur via grid-template-rows (0fr → 1fr) plutôt
   que max-height - pas besoin de connaître la hauteur réelle
   du contenu, et la transition reste smooth quel que soit le
   contenu (paragraphe, liste, image…).
   Pilotage JS : main.js §23 (initAccordion).
============================================================ */
.accordion {
    display: flex;
    flex-direction: column;
    gap: 10px;
}
.accordion-item {
    background: #fff;
    border: 1px solid #D9D9D9;
    border-radius: 10px;
    overflow: hidden;
}
.accordion-trigger {
    width: 100%;
    display: flex;
    align-items: center;
    justify-content: space-between;
    gap: 16px;
    padding: 18px 22px;
    background: transparent;
    border: 0;
    cursor: pointer;
    text-align: left;
    color: #0A0A0A;
}
.accordion-trigger:focus-visible {
    outline: 2px solid rgb(var(--brand));
    outline-offset: -2px;
}
.accordion-title {
    flex: 1;
    min-width: 0;
    font-size: 15px;
    font-weight: 700;
    line-height: 1.4;
    color: inherit;
}
.accordion-chevron {
    flex-shrink: 0;
    font-size: 14px;
    color: inherit;
    transition: transform 220ms ease;
}
.accordion-item.is-open .accordion-chevron {
    transform: rotate(180deg);
}
/* Animation : grid 0fr → 1fr. L'enfant .accordion-panel-inner doit avoir
   overflow:hidden + min-height:0 pour que l'animation soit clean (sinon
   le contenu déborde avant que la transition se termine). */
.accordion-panel {
    display: grid;
    grid-template-rows: 0fr;
    transition: grid-template-rows 280ms ease;
}
.accordion-item.is-open .accordion-panel {
    grid-template-rows: 1fr;
}
.accordion-panel-inner {
    overflow: hidden;
    min-height: 0;
    padding: 0 22px;
    color: #757575;
    font-size: 14px;
    line-height: 1.65;
}
.accordion-panel-inner > * {
    margin: 0;
    padding-bottom: 20px;
}
@media (min-width: 768px) {
    .accordion-trigger {
        padding: 20px 26px;
    }
    .accordion-title {
        font-size: 16px;
    }
    .accordion-panel-inner {
        padding: 0 26px;
        font-size: 15px;
    }
    .accordion-panel-inner > * {
        padding-bottom: 22px;
    }
}

/* ------------------------------------------------------------
   Variante "plain" du composant accordion (.accordion--plain)
   Réutilise le moteur [data-accordion] (main.js §23) + l'animation
   grid du .accordion-panel, mais sans chrome card : le déclencheur
   est rendu en simple lien souligné (chevron masqué). Pour une
   divulgation isolée type « Plus d'informations » (provider.php,
   sous la tarification). Même approche que .settings-card (middle/user/common.css) :
   spécificité 0,2,0 (.accordion--plain .accordion-*) → l'emporte sur
   les règles de base 0,1,0. */
.accordion--plain {
    gap: 0;
}
.accordion--plain .accordion-item {
    background: transparent;
    border: 0;
    border-radius: 0;
    overflow: visible;
}
.accordion--plain .accordion-trigger {
    width: auto;
    padding: 0;
    justify-content: flex-start;
    gap: 0;
    color: #757575;
    transition: color 160ms ease;
}
.accordion--plain .accordion-trigger:hover {
    color: rgb(var(--brand));
}
.accordion--plain .accordion-title {
    flex: 0 1 auto;
    font-size: 14px;
    font-weight: 400;
    text-decoration: underline;
    text-underline-offset: 2px;
}
.accordion--plain .accordion-chevron {
    display: none;
}
/* Bascule du libellé ouvert/fermé (opt-in) : poser deux spans dans le titre via
   l'option `title_html` du helper - [data-acc-label-closed] + [data-acc-label-open].
   Le libellé "ouvert" n'apparaît que sur l'item .is-open. */
[data-acc-label-open] {
    display: none;
}
.accordion-item.is-open [data-acc-label-closed] {
    display: none;
}
.accordion-item.is-open [data-acc-label-open] {
    display: inline;
}
.accordion--plain .accordion-panel-inner {
    padding: 12px 0 0;
}
.accordion--plain .accordion-panel-inner > * {
    padding-bottom: 0;
}

/* ------------------------------------------------------------
   Variante "card" du composant accordion (.accordion--card)
   Même moteur [data-accordion], chrome des FAQ marketing : la
   bordure grise cède la place à une ombre douce, le rayon passe à
   18px, le titre gagne en présence. Le chevron du helper est
   remplacé par un +/− de marque - l'icône <i> est masquée et les
   deux traits sont dessinés en ::before/::after du déclencheur (le
   trait vertical disparaît à l'ouverture : + → −).
   Même spécificité (0,2,0) que .accordion--plain → l'emporte sur
   les règles de base à ordre de source.
   ------------------------------------------------------------ */
.accordion--card {
    gap: 12px;
}
.accordion--card .accordion-item {
    border: 0;
    border-radius: 18px;
    box-shadow: 0 2px 12px rgb(43 32 30 / 0.05);
}
.accordion--card .accordion-title {
    font-size: 15px;
    font-weight: 600;
    color: #1A1A29;
}
.accordion--card .accordion-panel-inner {
    color: #6B5D59;
}
.accordion--card .accordion-chevron {
    display: none;
}
/* Le déclencheur porte le +/− : réserve de place à droite (le titre est en
   flex:1) puis deux traits centrés dans ce carré de 16px. */
.accordion--card .accordion-trigger {
    position: relative;
    padding-right: 46px;
}
.accordion--card .accordion-trigger::before,
.accordion--card .accordion-trigger::after {
    content: "";
    position: absolute;
    right: 24px;
    top: 50%;
    width: 16px;
    height: 2px;
    margin-top: -1px;
    border-radius: 1px;
    background: rgb(var(--brand));
    transition: transform 220ms ease, opacity 220ms ease;
}
.accordion--card .accordion-trigger::after {
    transform: rotate(90deg);
}
.accordion--card .accordion-item.is-open .accordion-trigger::after {
    transform: rotate(0deg);
    opacity: 0;
}
@media (min-width: 768px) {
    .accordion--card .accordion-title {
        font-size: 16px;
    }
}

/* ============================================================
   Comparateur de lieux (comparator.php)
   Disposition « colonne de critères fixe + carrousel de cartes » :
   à gauche les libellés (fixes), à droite un .scroll-snap (moteur
   de carrousel du projet : flèches + drag + swipe + snap). Les
   lignes des cartes et de la colonne de critères sont alignées via
   syncRowHeights() (JS). Construit par comparator.js.
   ============================================================ */
.comparator-layout {
    display: flex;
    align-items: flex-start;
}
/* Colonne FIXE des libellés (hors carrousel, toujours visible). */
.comparator-labels {
    flex: 0 0 auto;
    width: 150px;
    background: #fff;
    border-right: 1px solid #ededed;
}
/* Zone du carrousel (référent positionnel des flèches). */
.comparator-carousel-area {
    position: relative;
    flex: 1 1 0;
    min-width: 0;
}
/* Piste scrollable. La classe .scroll-snap (cf. plus haut + main.js) ajoute
   scroll-snap, cursor grab et le masquage de la scrollbar. */
.comparator-track {
    display: flex;
    overflow-x: auto;
    -webkit-overflow-scrolling: touch;
    /* Jamais centrée (contrairement au défaut .scroll-snap desktop) : les
       colonnes doivent rester collées à gauche pour s'aligner ligne à ligne
       sur la colonne de libellés qui les précède. */
    justify-content: flex-start;
}
/* Une colonne = une carte (lieu ou slot libre), enfant direct du .scroll-snap
   (le snap itère les enfants directs). */
.comparator-col {
    flex: 0 0 auto;
    width: 230px;
}

/* Lignes : base commune à la colonne de critères et aux cartes. La hauteur est
   imposée en JS (syncRowHeights) pour aligner les lignes entre colonnes. */
.comparator-row {
    display: flex;
    align-items: center;
    padding: 16px 18px;
    border-bottom: 1px solid #ededed;
}
.comparator-labels .comparator-row {
    font-size: 14px;
    font-weight: 400;
    color: #757575;
}
.comparator-col .comparator-row {
    font-size: 15px;
    font-weight: 700;
    color: #0a0a0a;
}
/* En-tête (carte lieu / boîte pointillée) et pied (actions) : flux bloc pour que
   le contenu prenne toute la largeur et se cale en haut. */
.comparator-row-head,
.comparator-row-actions {
    display: block;
}
.comparator-row-actions {
    border-bottom: none;
}

/* Icône d'un critère : cachée par défaut ; révélée (et le texte masqué) en mode
   compact mobile, quand la piste est scrollée (cf. .is-compact). */
.comparator-label-icon {
    display: none;
    font-size: 16px;
}

/* Mode compact (mobile uniquement) : dès que l'on scrolle vers la droite, la
   colonne de critères se réduit à ses icônes pour rendre de la place aux cartes.
   Sur desktop, la classe .is-compact est posée mais ces règles ne s'appliquent
   pas (la colonne garde ses libellés texte). */
@media (max-width: 767px) {
    .comparator-labels {
        transition: width 0.2s ease;
    }
    .is-compact .comparator-labels {
        width: 52px;
    }
    .is-compact .comparator-labels .comparator-row {
        justify-content: center;
        padding-left: 0;
        padding-right: 0;
    }
    .is-compact .comparator-label-text {
        display: none;
    }
    .is-compact .comparator-label-icon {
        display: inline-block;
    }
}

/* Flèches prev/next du carrousel (réutilise .nav-btn : 44×44, masquées mobile). */
.comparator-nav {
    position: absolute;
    top: 70px;
    z-index: 5;
}
.comparator-nav-prev {
    left: -16px;
}
.comparator-nav-next {
    right: -16px;
}

/* Mobile : neutralise le full-bleed (-15px) que la règle générique .scroll-snap
   applique aux carrousels enfants de .container-x - ici la piste vit à droite de
   la colonne de critères, pas en pleine largeur. */
@media (max-width: 767px) {
    .comparator-track.scroll-snap {
        margin-left: 0;
        margin-right: 0;
        padding-left: 0;
        padding-right: 0;
        scroll-padding-left: 0;
        scroll-padding-right: 0;
    }
}

/* En-tête de carte lieu : photo + nom + type + croix. */
.comparator-venue-card {
    position: relative; /* ancre le .card-link::after qui rend tout le bloc cliquable */
    display: flex;
    flex-direction: column;
    gap: 8px;
}
.comparator-venue-media {
    position: relative;
    width: 100%;
    aspect-ratio: 16 / 11;
    border-radius: 16px;
    overflow: hidden;
    background: #f5f5f5;
}
.comparator-venue-media img {
    position: absolute;
    inset: 0;
    width: 100%;
    height: 100%;
    object-fit: cover;
}
.comparator-remove {
    position: absolute;
    top: 8px;
    right: 8px;
    width: 32px;
    height: 32px;
    border-radius: 9999px;
    background: rgba(255, 255, 255, 0.95);
    display: inline-flex;
    align-items: center;
    justify-content: center;
    color: #0a0a0a;
    box-shadow: 0 2px 8px rgba(0, 0, 0, 0.15);
    transition: color 0.15s ease;
}
.comparator-remove:hover {
    color: rgb(var(--brand));
}
.comparator-venue-name {
    font-size: 15px;
    font-weight: 700;
    color: #0a0a0a;
    line-height: 1.3;
}
.comparator-venue-type {
    font-size: 13px;
    font-weight: 400;
    color: #757575;
}

/* Slot libre : carte pointillée « Ajouter un lieu » dans l'en-tête de la colonne
   vide (le reste de la colonne = lignes vides, alignées par syncRowHeights). */
.comparator-empty-slot-card {
    display: flex;
    flex-direction: column;
    align-items: center;
    justify-content: center;
    gap: 10px;
    width: 100%;
    aspect-ratio: 16 / 11;
    border: 2px dashed #d9d9d9;
    border-radius: 16px;
    color: #757575;
    transition: border-color 0.15s ease, color 0.15s ease;
}
.comparator-empty-slot-card:hover {
    border-color: rgb(var(--brand));
    color: rgb(var(--brand));
}
.comparator-empty-slot-plus {
    width: 40px;
    height: 40px;
    border-radius: 9999px;
    background: #f5f5f5;
    display: inline-flex;
    align-items: center;
    justify-content: center;
    font-size: 16px;
    transition: background 0.15s ease;
}
.comparator-empty-slot-card:hover .comparator-empty-slot-plus {
    background: rgb(var(--brand) / 0.1);
}
.comparator-empty-slot-label {
    font-size: 13px;
    font-weight: 600;
}
/* Valeurs + mise en avant de la meilleure (highlight brand). */
.comparator-val {
    display: inline-block;
}
.comparator-row.is-best {
    background: rgb(var(--brand) / 0.06);
}
.comparator-row.is-best .comparator-val {
    color: rgb(var(--brand));
}
.comparator-dash {
    color: #b0b0b0;
    font-weight: 400;
}

/* Cellule « Capacité salle max » : lignes Banquet/Cocktail/… */
.comparator-cap-cell {
    font-weight: 400;
}
.comparator-cap-line {
    display: flex;
    gap: 6px;
    font-size: 14px;
    line-height: 1.7;
}
.comparator-cap-label {
    color: #757575;
    font-weight: 400;
}
.comparator-cap-val {
    color: #0a0a0a;
    font-weight: 700;
}
.comparator-cap-val.is-best {
    color: rgb(var(--brand));
}

/* Pied : actions par lieu (Découvrir / devis / favori). */
.comparator-actions {
    display: flex;
    flex-direction: column;
    gap: 10px;
}
.comparator-cta-primary {
    display: inline-flex;
    align-items: center;
    justify-content: center;
    height: 44px;
    border-radius: 12px;
    background: rgb(var(--brand));
    color: #fff;
    font-size: 14px;
    font-weight: 700;
    transition: background 0.15s ease;
}
.comparator-cta-primary:hover {
    background: rgb(var(--brand-dark));
}
.comparator-actions-secondary {
    display: flex;
    gap: 8px;
}
.comparator-cta-ghost {
    flex: 1 1 auto;
    display: inline-flex;
    align-items: center;
    justify-content: center;
    height: 40px;
    border-radius: 10px;
    background: #f2f3f5;
    color: #0a0a0a;
    font-size: 13px;
    font-weight: 600;
    transition: background 0.15s ease;
}
.comparator-cta-ghost:hover {
    background: #e6e7ea;
}
.comparator-fav {
    flex: 0 0 auto;
    width: 40px;
    height: 40px;
    border-radius: 10px;
    border: 1px solid #d9d9d9;
    background: #fff;
    color: #0a0a0a;
    display: inline-flex;
    align-items: center;
    justify-content: center;
    transition: color 0.15s ease, border-color 0.15s ease;
}
.comparator-fav:hover,
.comparator-fav[aria-pressed="true"] {
    color: rgb(var(--brand));
    border-color: rgb(var(--brand));
}

/* État vide. */
.comparator-empty {
    text-align: center;
    padding: 60px 20px;
    display: flex;
    flex-direction: column;
    align-items: center;
    gap: 12px;
}
.comparator-empty-icon {
    width: 64px;
    height: 64px;
    border-radius: 9999px;
    background: #f5f5f5;
    color: #757575;
    display: inline-flex;
    align-items: center;
    justify-content: center;
    font-size: 26px;
}
.comparator-empty-title {
    font-size: 18px;
    font-weight: 700;
    color: #0a0a0a;
}
.comparator-empty-text {
    font-size: 14px;
    color: #757575;
    max-width: 380px;
    line-height: 1.5;
}
.comparator-empty-cta {
    margin-top: 6px;
    display: inline-flex;
    align-items: center;
    justify-content: center;
    height: 46px;
    padding: 0 24px;
    border-radius: 12px;
    background: rgb(var(--brand));
    color: #fff;
    font-size: 14px;
    font-weight: 700;
    transition: background 0.15s ease;
}
.comparator-empty-cta:hover {
    background: rgb(var(--brand-dark));
}

/* En-tête de page : bouton « Tout effacer ». */
.comparator-clear-all {
    display: inline-flex;
    align-items: center;
    gap: 8px;
    height: 40px;
    padding: 0 16px;
    border-radius: 10px;
    background: #f5f5f5;
    color: #0a0a0a;
    font-size: 14px;
    font-weight: 600;
    white-space: nowrap;
    transition: background 0.15s ease;
}
.comparator-clear-all:hover {
    background: #ececec;
}
/* L'attribut [hidden] doit l'emporter sur les utilitaires de display posés sur
   ces éléments (le bandeau a `flex`, le bouton `inline-flex`) : sans ça, masquer
   via JS (el.hidden = true) resterait sans effet visuel. Sélecteurs à deux
   attributs/classe → spécificité (0,2,0), supérieure à `.flex` (0,1,0). */
[data-comparator-header][hidden],
.comparator-clear-all[hidden] {
    display: none;
}
/* Deux instances de « Tout effacer » : sur mobile sur la ligne du retour (à
   droite), sur desktop sur la ligne du titre. On affiche celle qui correspond au
   breakpoint. Règles placées APRÈS .comparator-clear-all (même spécificité) pour
   l'emporter sur son `display: inline-flex`. */
.comparator-clear-all--desktop {
    display: none;
}
@media (min-width: 768px) {
    .comparator-clear-all--mobile {
        display: none;
    }
    .comparator-clear-all--desktop {
        display: inline-flex;
    }
}

@media (min-width: 768px) {
    .comparator-labels {
        width: 200px;
    }
    .comparator-col {
        width: 260px;
    }
}
/* Desktop : on affiche 4 cartes pleines + le DÉBUT de la 5e (~60px de peek) pour
   signaler qu'il reste des lieux à droite. La largeur est relative à la piste
   visible (100% = largeur du .comparator-track) → peek constant quel que soit
   l'écran ; il y a toujours 5 colonnes (lieux + slots libres jusqu'à MAX). */
@media (min-width: 1024px) {
    .comparator-col {
        width: calc((100% - 60px) / 4);
    }
}

/* ===========================================================
   Switch on/off + ligne de préférence - render_switch()
   ----------------------------------------------------------
   Interrupteur 100 % CSS : l'état visible est piloté par la
   checkbox native masquée (`.switch-input:checked + .switch`),
   aucun JS requis. Le `<label>` parent (`.switch-row`) rend
   toute la ligne cliquable. Dimensions calées sur le toggle
   privé/pro de subs.php (track 44×24, pastille 20, course 20px).
   =========================================================== */
.switch-row {
    position: relative; /* ancre le .switch-input masqué - cf. .filters-check */
    display: flex;
    align-items: center;
    justify-content: space-between;
    gap: 16px;
    padding: 16px 2px;
    cursor: pointer;
}
.switch-row-text {
    display: flex;
    flex-direction: column;
    gap: 4px;
    min-width: 0;
}
.switch-row-title {
    font-size: 15px;
    font-weight: 600;
    color: #0a0a0a;
    line-height: 1.3;
}
.switch-row-desc {
    font-size: 13px;
    color: #757575;
    line-height: 1.4;
}
/* Checkbox réelle : retirée du flux (n'influence pas le flex) mais conservée
   comme sibling direct AVANT `.switch` pour piloter l'état via `+`. */
.switch-input {
    position: absolute;
    width: 1px;
    height: 1px;
    margin: 0;
    opacity: 0;
    pointer-events: none;
}
.switch {
    position: relative;
    flex-shrink: 0;
    width: 44px;
    height: 24px;
    border-radius: 9999px;
    background: #d9d9d9;
    transition: background-color .2s ease;
}
.switch-knob {
    position: absolute;
    top: 2px;
    left: 2px;
    width: 20px;
    height: 20px;
    border-radius: 9999px;
    background: #fff;
    box-shadow: 0 1px 2px rgba(0, 0, 0, .25);
    transition: transform .2s ease;
}
.switch-input:checked + .switch {
    background: rgb(var(--brand));        /* brand */
}
.switch-input:checked + .switch .switch-knob {
    transform: translateX(20px);
}
.switch-input:focus-visible + .switch {
    outline: 2px solid rgb(var(--brand) / .45);
    outline-offset: 2px;
}
@media (prefers-reduced-motion: reduce) {
    .switch,
    .switch-knob {
        transition: none;
    }
}

/* ============================================================
   Rangée des logos du groupe - hero de la page À propos (about.php)
   Une seule ligne scrollable horizontalement (desktop comme mobile)
   quand elle déborde, sans scrollbar visible. Pas de `.scroll-snap` :
   sa règle mobile générique (-15px de full-bleed calée sur
   .container-x) décalerait la rangée, le hero ayant ses propres
   paddings (20/80px, réinjectés dans le markup).
   ============================================================ */
.about-logos {
    -ms-overflow-style: none;
    scrollbar-width: none;
}
.about-logos::-webkit-scrollbar {
    display: none;
}

/* ============================================================
   Ruban de photos de la page À propos (about.php)
   Reprise du rendu v2020 (about-caterer.php, .about .list-picture) :
   bande bord à bord du viewport, swipable desktop et mobile via le
   moteur .scroll-snap. Cette variante neutralise UNIQUEMENT le
   full-bleed mobile générique de .scroll-snap (marges -15px calées
   sur .container-x - même besoin que .comparator-track) : la bande
   vit hors container, déjà collée aux bords.
   ============================================================ */
@media (max-width: 767px) {
    .about-strip.scroll-snap {
        margin-left: 0;
        margin-right: 0;
        padding-left: 0;
        padding-right: 0;
        scroll-padding-left: 0;
        scroll-padding-right: 0;
    }
}

/* ============================================================
   Bouton d'action en attente - [data-btn-loading]
   Posé par window.appBtnLoading / appBtnRun (utils.js) le temps
   d'un appel AJAX : l'icône du bouton devient un spinner et le
   bouton est neutralisé. `pointer-events` sert aux <a> (les
   téléchargements), que l'attribut `disabled` ne couvre pas.
   L'attribut est data-btn-loading et non data-loading, déjà pris
   par .filters-modal-dialog (modales de contenu AJAX).
   ============================================================ */
[data-btn-loading="1"] {
    pointer-events: none;
    cursor: progress;
    opacity: 0.6;
}

/* ============================================================
   Page Abonnements (/nos-abonnements) - cards d'offre
   Maquette Figma « Référencez-vous ! » : boutons de card (plein
   rose pour Gold, teinté 8 % pour Essai / Présence), switch du
   « Pack Privé + Pro » et bandeau « Le plus populaire ».
   Les trois passent par les variables de marque (--brand,
   --hero-grad-*) : le site PRO les recolore sans une ligne de plus.
   ============================================================ */
.subs-btn-primary,
.subs-btn-soft {
    display: flex;
    align-items: center;
    justify-content: center;
    width: 100%;
    height: 46px;
    border-radius: 14px;
    font-size: 15px;
    font-weight: 500;
    text-align: center;
    transition: background-color 0.15s ease;
}
.subs-btn-primary {
    background: rgb(var(--brand));
    color: #fff;
}
.subs-btn-primary:hover {
    background: rgb(var(--brand-dark));
}
.subs-btn-soft {
    background: rgb(var(--brand) / 0.08);
    color: rgb(var(--brand));
}
.subs-btn-soft:hover {
    background: rgb(var(--brand) / 0.16);
}

/* Switch du « Pack Privé + Pro » (39x20, pastille 14, course 19px) : OFF gris,
   ON dégradé de marque (mêmes variables que .hero-grad-text). L'état est posé
   par subs.js §5 via `.is-on` - pas de checkbox ici, contrairement à `.switch`
   de render_switch() : le déclencheur est un <button role="switch"> porteur de
   l'aria-checked, la piste n'étant qu'un décor (aria-hidden). */
.subs-pack-switch {
    position: relative;
    display: block;
    width: 39px;
    height: 20px;
    padding: 3px;
    border-radius: 40px;
    background: #E1E4ED;
    transition: background 0.2s ease;
}
.subs-pack-switch.is-on {
    background: linear-gradient(90deg, rgb(var(--hero-grad-from)), rgb(var(--hero-grad-to)));
}
.subs-pack-knob {
    display: block;
    width: 14px;
    height: 14px;
    border-radius: 999px;
    background: #fff;
    transition: transform 0.2s ease;
}
.subs-pack-switch.is-on .subs-pack-knob {
    transform: translateX(19px);
}
@media (prefers-reduced-motion: reduce) {
    .subs-pack-switch,
    .subs-pack-knob {
        transition: none;
    }
}

/* Contenus RÉVÉLÉS par ce switch (avantages de la card Gold, bandeau du tableau
   des positions) : subs.js §5 les pilote par l'attribut [hidden], mais ils
   portent la classe Tailwind `flex` - or `.flex { display: flex }` et le
   `[hidden]:where(...) { display: none }` du preflight ont la MÊME spécificité
   (0,1,0) et les utilitaires sont déclarés après : le masquage ne s'applique
   jamais et le bandeau « Pack Privé + Pro » s'affiche switch OFF. On rétablit
   [hidden] explicitement, comme .fav-grid[hidden] (favourite.css) ou
   .chat-icon-btn[hidden] (chat.css). */
[data-subs-pack-item][hidden] {
    display: none;
}

/* Bandeau « Le plus populaire » de la card Gold : la maquette le dessine comme
   une bordure basse de 30px (border-b-30). On le rend en bande absolue à
   l'intérieur de la card (qui porte overflow-hidden + le padding bas
   correspondant), pour qu'il épouse le rayon de 20px sans hack de bordure. */
.subs-popular-ribbon {
    position: absolute;
    left: 0;
    right: 0;
    bottom: 0;
    display: flex;
    align-items: center;
    justify-content: center;
    height: 30px;
    background: linear-gradient(90deg, rgb(var(--hero-grad-from)), rgb(var(--hero-grad-to)));
    color: #fff;
    font-size: 12px;
    font-weight: 600;
    letter-spacing: -0.06px;
}
