/* --imm-touch-size pilote toutes les cibles tactiles (.touch-target,
   .touch-target-label, zoom Leaflet, etc.) : 44px par défaut (mobile/
   tablette, recommandation WCAG/Apple HIG), ramené à une taille compacte
   au-dessus du breakpoint navbar-expand-lg où la souris n'a pas besoin de
   cette marge — un seul endroit à ajuster plutôt qu'une media query par
   composant. */
:root {
    --imm-overlay-bg: rgba(255, 255, 255, 0.8);
    --imm-overlay-radius: 5px;
    --imm-touch-size: 44px;
}

@media (min-width: 992px) {
    :root {
        --imm-touch-size: 26px;
    }
}

html, body {
    min-height: 100%;
    margin: 0;
    /* Fond noir plutôt que le blanc par défaut du navigateur : sur Firefox,
       l'arrondi sous-pixel d'un flex-column (header/carte/footer en 100dvh)
       peut laisser une fine ligne d'un pixel entre les enfants flex, qui
       laisse alors apparaître le fond de <html> — invisible si ce fond est
       déjà noir comme le header/footer plutôt que blanc.
       min-height (pas height) : sur une page statique dont le contenu
       dépasse la hauteur de la fenêtre (.imm-static-page, ex: /sources
       avec une longue liste), html doit pouvoir grandir avec body plutôt
       que rester bloqué à 100vh — sinon son fond noir apparaît sous le
       contenu qui déborde. */
    background-color: #000;
}

/* Page carte : header + carte + footer en colonne flex sur la hauteur de
   viewport réelle (dvh plutôt que vh, pour tenir compte des barres d'adresse
   mobiles qui rétrécissent la fenêtre visible), comme le layout flex du
   legacy (index_v2.php:50-91 : nav / main.h-100 / footer). */
.imm-map-page {
    display: flex;
    flex-direction: column;
    height: 100vh;
    height: 100dvh;
}

.imm-map-page > main {
    flex: 1 1 auto;
    min-height: 0;
    display: flex;
    flex-direction: column;
}

.imm-map-page #map-wrapper {
    position: relative;
    flex: 1 1 auto;
    min-height: 0;
}

.imm-map-page #map {
    width: 100%;
    height: 100%;
    /* Couleur de fond posée dynamiquement en JS (voir applyMapBackground()
       dans map.js) selon le niveau chargé — #eae8d3 par défaut (couleur
       v2), #303030 sur la page d'accueil. Valeur ici juste avant le premier
       rendu JS pour éviter un flash blanc au chargement. */
    background: #eae8d3;
    transition: filter 0.2s ease;
}

/* Pendant le chargement d'un changement d'année/niveau (navigateToLevel en
   AJAX, voir map.js), le plan précédent reste visible mais grisé plutôt que
   de disparaître d'un coup — évite l'écran vide le temps de la requête. */
.imm-map-page #map.imm-map--loading {
    filter: grayscale(1) brightness(0.85);
    pointer-events: none;
}

/* Indicateur de chargement centré sur la carte, affiché pendant loadLevel()
   (voir showLoader()/hideLoader() dans map.js). Badge à fond clair + liseré
   noir 1px (plutôt que du texte blanc à ombre portée, l'ancien style) : le
   fond derrière ce loader varie selon le niveau affiché (#eae8d3 par
   défaut, mais bg_color est personnalisable par niveau, #303030 fixe sur la
   page d'accueil) — un badge à fond propre reste lisible quel que soit le
   fond en dessous, contrairement à du texte blanc qui perdrait en
   contraste sur un fond clair. */
.imm-map-loader {
    position: absolute;
    z-index: 1500;
    top: 50%;
    left: 50%;
    transform: translate(-50%, -50%);
    display: flex;
    flex-direction: column;
    align-items: center;
    gap: 0.5rem;
    background: rgba(255, 255, 255, 0.92);
    border: 1px solid #000;
    border-radius: var(--imm-overlay-radius);
    padding: 1rem 1.5rem;
    color: #1a1a1a;
    box-shadow: 0 0 15px rgba(0, 0, 0, 0.2);
    pointer-events: none;
}

.imm-map-loader__logo {
    width: 56px;
    height: 56px;
    /* Le SVG source est noir (fill par défaut) ; ce filtre le fait
       démarrer gris clair, puis imm-loader-pulse fait osciller son
       assombrissement pour boucler gris clair ↔ noir pendant le
       chargement — toujours visible sur le fond clair fixe du badge. */
    filter: brightness(0) saturate(100%) invert(85%);
    animation: imm-loader-pulse 1.1s ease-in-out infinite;
}

@keyframes imm-loader-pulse {
    0%, 100% { filter: brightness(0) saturate(100%) invert(85%); }
    50% { filter: brightness(0) saturate(100%) invert(30%); }
}

/* Utilisateurs sensibles au mouvement : pas de pulsation, logo fixe (gris
   intermédiaire, toujours visible sur le badge clair). */
@media (prefers-reduced-motion: reduce) {
    .imm-map-loader__logo {
        animation: none;
        filter: brightness(0) saturate(100%) invert(55%);
    }
}

/* Panneaux flottants génériques (sélecteur année/niveau, légende, bouton aide).
   max-height + overflow-y : depuis l'ajout du header/footer, la carte
   n'occupe plus toute la hauteur de viewport, donc un panneau au contenu
   long (accordéon "Résidents" déplié avec beaucoup de types) doit défiler
   dans ses propres limites plutôt que déborder sur les autres panneaux ou
   sous le footer. */
.map-overlay {
    position: absolute;
    z-index: 1000;
    background: var(--imm-overlay-bg);
    border-radius: var(--imm-overlay-radius);
    padding: 6px 8px;
    font: 14px/16px Arial, Helvetica, sans-serif;
    text-align: left;
    box-shadow: 0 0 15px rgba(0, 0, 0, 0.2);
    max-width: min(90vw, 320px);
    max-height: calc(100% - 1.5rem);
    overflow-y: auto;
}

.map-overlay--top-left { top: 0.75rem; left: 0.75rem; }
.map-overlay--top-right { top: 0.75rem; right: 0.75rem; }
.map-overlay--bottom-left { bottom: 0.75rem; left: 0.75rem; }
.map-overlay--bottom-right { bottom: 0.75rem; right: 0.75rem; }

#source-panel[hidden] {
    display: none;
}

/* Ligne compacte du panneau source sur mobile ("Année • Niveau" + icône
   "voir sur le plan") : remplace le détail complet (#source-panel-full, qui
   inclut en plus le nom du lieu — omis ici, l'utilisateur sait déjà où il
   est), déplacé dans une modale au clic sur l'icône (voir map.js). Ne
   réapparaît qu'au-dessus de 768px (voir media query plus bas). max-width
   contraint le texte seul (pas la ligne entière, sinon le bouton avec sa
   cible tactile 44px déborderait du panneau parent lui-même limité à
   min(90vw, 320px) par .map-overlay). */
#source-panel-compact {
    display: flex;
    align-items: center;
    gap: 0.25rem;
    font-size: 0.75rem;
}

#source-panel-compact-text {
    overflow: hidden;
    text-overflow: ellipsis;
    white-space: nowrap;
    max-width: 160px;
}

/* min-width/min-height désactivés malgré .touch-target (44px, norme
   tactile) : sur une seule ligne de contenu, ça gonflait la hauteur du
   panneau largement au-delà de ce qu'il faut pour le texte + l'icône 22px
   — même compromis délibéré que les boutons de zoom Leaflet (voir plus
   haut dans ce fichier). */
#source-panel-info-btn {
    flex-shrink: 0;
    background: transparent;
    border: none;
    padding: 0;
    min-width: 0;
    min-height: 0;
}

.imm-panel-info-icon {
    width: 22px;
    height: 22px;
    flex-shrink: 0;
    fill: #777;
}

@media (min-width: 768px) {
    #source-panel-compact {
        display: none;
    }
}

/* Sélecteur descendant (#source-panel #source-panel-full), pas #source-panel-full
   seul : une fois déplacé dans la modale par le clic sur l'icône info (voir
   map.js), il ne doit plus être masqué, sinon la modale s'ouvre vide. */
@media (max-width: 767.98px) {
    #source-panel #source-panel-full {
        display: none;
    }
}

/* Panneau lieux : aligné à droite (demande explicite, cohérent avec sa
   position en bas à droite de l'écran), toujours affiché tant qu'il y a des
   polygones — desktop comme mobile. Rempli au survol sur desktop, au clic
   sur un polygone sur mobile (pas de survol possible au tactile — voir
   updatePlacesHoverPanel()/showPlacesPanelForClick() dans map.js), avec un
   texte d'invite différent selon le contexte (placesHoverDefaultText()). */
#places-hover-panel {
    text-align: right;
}

/* Police réduite sur mobile (demande explicite) : le contenu variable
   (nom de lieu potentiellement long) tient mieux dans les 320px max du
   panneau (voir .map-overlay) sans déborder. */
@media (max-width: 767.98px) {
    #places-hover-panel {
        font-size: 12px;
    }
}

/* Cibles tactiles : au moins 44x44px, recommandation WCAG/Apple HIG.
   Corrige les boutons de zoom Leaflet (~26px) et les checkboxes de filtre
   trop petites du legacy. */
.touch-target,
.touch-target-group label,
.touch-target-group input[type="checkbox"],
.touch-target-group input[type="radio"] {
    min-width: var(--imm-touch-size);
    min-height: var(--imm-touch-size);
    display: inline-flex;
    align-items: center;
    justify-content: center;
}

/* Les items de menu déroulant restent alignés à gauche (comportement
   standard d'un menu) malgré la cible tactile agrandie de .touch-target,
   qui centre sinon leur contenu. */
.dropdown-menu .touch-target {
    justify-content: flex-start;
}

/* Boutons de zoom Leaflet volontairement exclus de --imm-touch-size : taille
   identique desktop/mobile (26px), demande explicite malgré la norme
   tactile 44px (boutons jugés trop imposants en mobile). */
.leaflet-touch .leaflet-bar a {
    width: 26px !important;
    height: 26px !important;
    line-height: 26px !important;
}

/* Icône du type de résident, accolée à sa case à cocher dans l'accordéon
   "Résidents" du sélecteur — remplace la légende séparée du bas-gauche. */
.imm-type-filter-icon {
    display: inline-block;
    width: 14px;
    height: 14px;
    flex-shrink: 0;
    object-fit: contain;
}

/* Panneau d'info du plan et fiche résident : toujours un panneau latéral
   (offcanvas-end, posé en JS sur mobile comme sur desktop — voir
   applyResponsivePanelMode() dans map.js), glissé entre le header et le
   footer plutôt que sur 100% de la hauteur de viewport (Bootstrap positionne
   un offcanvas en fixed top:0/bottom:0 par défaut) — hauteurs mesurées en JS
   (updatePanelVerticalBounds()) car elles varient selon le contenu du
   header/footer. !important nécessaire pour battre
   .offcanvas{top:0;bottom:0!important} de Bootstrap. Seule la largeur change
   selon l'écran : 100% sur mobile (même contenu/style qu'en vue PC, pas un
   bottom-sheet séparé), min(420px, 90vw) au-dessus de 768px. */
#level-info-panel.offcanvas-end,
#hist-object-panel.offcanvas-end {
    top: var(--imm-header-height, 0) !important;
    bottom: var(--imm-footer-height, 0) !important;
    height: auto !important;
}

@media (max-width: 767.98px) {
    #level-info-panel,
    #hist-object-panel {
        width: 100%;
    }
}

@media (min-width: 768px) {
    #level-info-panel,
    #hist-object-panel {
        width: min(420px, 90vw);
    }
}

/* Panneaux "Ce plan" et fiche résident (offcanvas) : fond gris foncé
   (couleur définie par l'utilisateur, distincte du noir pur
   ajaxGetTitle()/ajaxGetBodyPart() du legacy) et texte blanc, accent beige
   (.text-imm du legacy, css/styles.css:11-13) — même habillage pour les
   deux, qui partageaient déjà ce style en v2 (class/app.php:428-482).
   !important nécessaire pour battre .offcanvas{
   background-color:transparent!important} de Bootstrap (bootstrap.min.css),
   plus spécifique que la seule classe. */
#level-info-panel.imm-offcanvas-dark,
#hist-object-panel.imm-offcanvas-dark {
    background-color: #373a41 !important;
    color: #fff;
}

.imm-text-accent {
    color: #D6C2A0;
}

/* Lightbox vidéo Vespera (voir map.php, map.js::openVesperaLightbox()) :
   90%×90% de l'espace RESTANT entre le header et le footer du site (les
   mêmes --imm-header-height/--imm-footer-height posées par
   updatePanelVerticalBounds() pour #level-info-panel/#hist-object-panel,
   pas 90% du viewport complet) — titre fixe en en-tête, le lecteur vidéo
   occupe tout l'espace restant du corps de la modale. Même habillage sombre
   que les panneaux offcanvas (imm-offcanvas-dark) pour rester cohérent avec
   le reste du site plutôt que le blanc Bootstrap par défaut. */
.imm-vespera-lightbox-dialog {
    width: 90vw;
    height: calc((100vh - var(--imm-header-height, 0px) - var(--imm-footer-height, 0px)) * 0.9);
    max-width: none;
    margin: calc((100vh - var(--imm-header-height, 0px) - var(--imm-footer-height, 0px)) * 0.05 + var(--imm-header-height, 0px)) auto 0;
}

.imm-vespera-lightbox-content {
    height: 100%;
}

.imm-vespera-lightbox-content .modal-title {
    color: #8a6d3b;
}

.imm-vespera-lightbox-body {
    padding: 0;
    display: flex;
}

.imm-vespera-lightbox-body iframe {
    width: 100%;
    height: 100%;
    border: 0;
}

/* Lien sur fond clair (panneau source, etc.) : même famille dorée que
   l'accent .imm-text-accent (#D6C2A0), mais assombrie pour rester lisible
   sur fond clair — #D6C2A0 est pensé pour un fond noir (header/offcanvas,
   contraste 1.7:1 sur blanc) et serait illisible ici. #8a6d3b conserve la
   teinte tout en atteignant 4.85:1 sur blanc (seuil WCAG AA : 4.5:1).
   Remplace le bleu Bootstrap par défaut (--bs-link-color), hors charte. */
.imm-link-accent,
.imm-link-accent:visited {
    color: #8a6d3b;
}

.imm-link-accent:hover,
.imm-link-accent:focus {
    color: #6b5230;
}

/* Lien "Vidéo" de la rubrique Vespera (voir map.js) — couleur dédiée,
   distincte de .imm-link-accent qui sert aux autres liens du panneau
   détail (Wikipedia, CRCV, Bossu). */
.imm-link-vespera,
.imm-link-vespera:visited {
    color: #ccc296;
}

.imm-link-vespera:hover,
.imm-link-vespera:focus {
    color: #a89f78;
}

/* Liens du contenu (article) des pages statiques du menu "Projet"
   (description, équipe, sources, mentions légales, cour) : même famille
   dorée que .imm-link-accent, pour remplacer le bleu Bootstrap par défaut
   sans poser la classe sur chaque <a> de ces pages. Restreint à l'article
   (pas .imm-static-page directement) pour ne pas affecter les liens du
   header/footer inclus dans la même page, qui ont déjà leur propre style. */
.imm-static-page article a,
.imm-static-page article a:visited {
    color: #8a6d3b;
}

.imm-static-page article a:hover,
.imm-static-page article a:focus {
    color: #6b5230;
}

/* Header : titre à gauche, croix de fermeture à droite (space-between
   plutôt que le centrage par défaut de .offcanvas-header), sur le même
   fond que le reste du panneau. */
#level-info-panel .offcanvas-header,
#hist-object-panel .offcanvas-header {
    justify-content: space-between;
    align-items: flex-start;
}

/* .offcanvas-body est display:flex par défaut (Bootstrap) : header + image
   + Source/Titre/Note (imm-offcanvas-fixed) restent fixes, seules les
   sections "Résidents recensés"/"Lieux recensées" défilent en dessous
   (imm-offcanvas-scroll), sous le filet blanc généré en JS. Même principe
   pour la fiche résident (hist-object-body-wrapper) : portrait + légende
   fixes, notices/marqueurs défilants. */
#level-info-body-wrapper,
#hist-object-body-wrapper {
    flex-direction: column;
    padding: 0;
    height: 100%;
    min-height: 0;
}

.imm-offcanvas-fixed {
    flex: 0 0 auto;
    padding: 0 1rem;
}

.imm-offcanvas-scroll {
    flex: 1 1 auto;
    min-height: 0;
    overflow-y: auto;
    padding: 0 1rem 1rem;
}

.imm-offcanvas-title-block {
    text-align: left;
}

.imm-offcanvas-year {
    color: #D6C2A0;
    font-size: 1.2rem;
    margin: 0;
}

.imm-offcanvas-level {
    color: #fff;
    font-size: 1rem;
    margin: 0;
}

/* Titre (nom du résident) de la fiche résident : reproduit le style v2
   (ajaxGetTitle(), class/app.php:428-440 — h3.text-white text-end, en
   pratique rendu en 28px/500/#D6C2A0 par le thème Bootstrap du legacy),
   plutôt que la taille de sous-titre héritée de .imm-offcanvas-level. */
.imm-hist-object-title {
    color: #D6C2A0;
    font-size: 28px;
    font-weight: 500;
    line-height: 1.2;
}

/* Image du plan collée aux bords du panneau (pas de bordure ni de marge),
   contrairement à ajaxGetTitle() qui l'entourait d'un filet noir — demande
   explicite de coller l'image au cadre plutôt que de reproduire ce filet.
   Marge négative pour compenser le padding horizontal de
   imm-offcanvas-fixed. */
.imm-offcanvas-image {
    display: block;
    width: calc(100% + 2rem);
    max-width: none;
    height: auto;
    margin: 0 -1rem 1rem;
}

/* Portrait Wikidata d'un résident : deux cas selon que les dimensions
   réelles sont connues (voir WikidataService::fetchImageDimensions(),
   attributs width/height HTML natifs posés par renderHistObjectFixed()) :
   - .imm-hist-object-image--sized : le navigateur calcule le ratio depuis
     width/height dès le chargement (pas de recadrage, pas de saut de
     layout) — cas normal une fois un résident rafraîchi ;
   - .imm-hist-object-image (repli, résident pas encore rafraîchi) :
     hauteur fixe + object-fit:cover, pour un rendu stable même sans
     connaître le ratio à l'avance. */
.imm-hist-object-image--sized {
    max-height: 420px;
    object-fit: contain;
}

.imm-hist-object-image {
    height: 320px;
    object-fit: cover;
    object-position: center top;
}

/* Trait fin séparant "Source"/"Titre"/"Note" (zone fixe) des sections
   "Résidents recensés"/"Lieux recensées" (zone défilante) — généré en JS
   dans le HTML de updateLevelInfoPanel(), juste avant le début de
   imm-offcanvas-scroll. Reproduit le <hr> Bootstrap par défaut du v2
   (border-bottom: 1px solid #fff, opacity: 0.25, margin: 1rem 0). */
.imm-offcanvas-hr {
    border: none;
    border-bottom: 1px solid #fff;
    opacity: 0.25;
    margin: 1rem 0;
}

.imm-body-part-title {
    width: 100%;
    text-align: center;
    font-weight: bold;
    color: #fff;
    font-variant: small-caps;
}

.imm-body-part-content {
    margin: 0.5rem 0 1rem;
    color: #fff;
    text-align: justify;
}

/* Variante « note » (texte de l'offcanvas centré en petits caractères) :
   remplace l'ancien style="" inline (CSP style-src 'self'). Placée après la
   règle de base pour l'emporter à spécificité égale. */
.imm-body-part-content.imm-body-part-content--note {
    font-size: small;
    text-align: center;
}

.imm-body-part-list {
    list-style-type: circle;
    margin: 0;
    padding-left: 1.25rem;
}

/* Petites majuscules pour un nom de famille (équipe, listes d'étudiants). */
.imm-small-caps {
    font-variant-caps: small-caps;
}

/* Tous les titres de section (h2) du contenu (article) des pages statiques
   du menu "Projet" (description, équipe, sources, mentions légales, cour) :
   petites majuscules pour les distinguer visuellement du h1 de page.
   Restreint à l'article pour ne pas affecter le h2 de la modale "Citer ce
   projet" incluse via le footer sur ces mêmes pages. */
.imm-static-page article h2 {
    font-variant-caps: small-caps;
}

/* Reproduit le <hr> Bootstrap par défaut du v2 (border-bottom: 1px solid
   #fff, opacity: 0.25, margin: 1rem 0). */
.imm-body-part-hr {
    border: none;
    border-bottom: 1px solid #fff;
    opacity: 0.25;
    margin: 1rem 0;
}

/* Icône "carte" (lien vers un marqueur/polygone) : couleur fixe plutôt que
   currentColor, pour rester identique partout où elle apparaît (fiche
   résident et panneau "Ce plan") indépendamment de la couleur du lien
   parent. Taille doublée par rapport au viewBox d'origine (8x8, trop petit
   pour être une cible tactile confortable). */
.imm-oi-map {
    width: 16px;
    height: 16px;
    fill: #ccc39b;
    vertical-align: middle;
    margin-left: 3px;
}

/* Bulle "lieu" de la page d'accueil : reproduit le style du legacy
   (bulle noire + triangle pointant vers la coordonnée exacte du lieu),
   pages/map.php:93-119. Cible tactile agrandie par rapport au legacy. */
.imm-place-bubble {
    position: relative;
    cursor: pointer;
}

.imm-place-bubble__label {
    position: absolute;
    left: 0;
    top: 0;
    transform: translate(-50%, -100%);
    background: #000;
    color: #fff;
    padding: 0.5rem 0.75rem;
    border-radius: 4px;
    font-size: 1rem;
    font-weight: normal;
    white-space: nowrap;
    box-shadow: 0 2px 4px rgba(0, 0, 0, 0.3);
    min-height: var(--imm-touch-size);
    display: flex;
    align-items: center;
}

/* Labels plus compacts sur mobile (page d'accueil) : moins de place prise à
   l'écran, ce qui aide aussi fitMapToVisibleElements() (map.js) à tenir tous
   les labels dans le viewport avec un zoom moins réduit. min-height
   explicitement réduit ici : sinon --imm-touch-size (44px, norme tactile)
   domine sur le padding resserré et la hauteur ne bouge pas malgré la
   police plus petite — même compromis délibéré que les boutons de zoom
   Leaflet (voir .leaflet-touch .leaflet-bar a), le label n'étant pas une
   cible tactile au sens strict (c'est le marqueur en dessous qui l'est). */
@media (max-width: 767.98px) {
    .imm-place-bubble__label {
        font-size: 0.8rem;
        padding: 0.35rem 0.5rem;
        min-height: 0;
    }
}

.imm-place-bubble__pointer {
    position: absolute;
    left: 0;
    top: 0;
    transform: translateX(-50%);
    width: 0;
    height: 0;
    border-left: 7px solid transparent;
    border-right: 7px solid transparent;
    border-top: 8px solid #000;
}

/* Sélecteur année/niveau/résidents : accordéons dépliables (legacy
   pages/map.php:636-673), avec cibles tactiles agrandies sur les labels.
   Couleur/taille/alignement reproduisent .info h4 du legacy (css/styles.css). */
.imm-accordion__title {
    cursor: pointer;
    color: #777;
    font-size: 18px;
    margin: 0 0 5px;
    min-height: var(--imm-touch-size);
    display: flex;
    align-items: center;
    justify-content: flex-start;
}

/* Le titre d'accordéon contient un vrai <button> (clavier, aria-expanded) :
   on neutralise son habillage natif pour garder le rendu du titre. */
.imm-accordion__toggle {
    all: unset;
    box-sizing: border-box;
    display: flex;
    align-items: center;
    gap: 0.25rem;
    width: 100%;
    min-height: inherit;
    cursor: pointer;
}

.imm-accordion__toggle:focus-visible {
    outline: 2px solid currentColor;
    outline-offset: 2px;
}

.touch-target-label {
    display: flex;
    align-items: center;
    gap: 0.375rem;
    min-height: var(--imm-touch-size);
    cursor: pointer;
}

.touch-target-label input {
    width: 14px;
    height: 14px;
}

/* Au-dessus du breakpoint navbar-expand-lg (souris/desktop), les lignes du
   sélecteur retrouvent leur hauteur naturelle de texte, sans la marge
   tactile — comme les lignes compactes du legacy (pages/map.php:431,
   labels sans hauteur minimale). Reste à --imm-touch-size (44px) en
   dessous, où le doigt a besoin de cette marge. */
@media (min-width: 992px) {
    #level-selector .touch-target-label {
        min-height: 0;
    }
}

/* Sur mobile, le panneau années/niveaux est trop encombrant avec la marge
   tactile pleine de 44px par ligne (--imm-touch-size) : compromis délibéré
   à ~32px plutôt que 0 pur, pour rester cliquable au doigt tout en
   resserrant nettement l'espacement vertical. Même logique que le
   compromis déjà fait sur les boutons de zoom Leaflet (voir plus haut). */
@media (max-width: 767.98px) {
    #level-selector .imm-accordion__title {
        font-size: 15px;
        min-height: 32px;
        margin: 0;
    }

    #level-selector .touch-target-label {
        min-height: 32px;
        gap: 0.25rem;
        font-size: 0.85rem;
    }

    #level-selector .imm-accordion__body {
        margin-bottom: 0.15rem;
    }

    #level-selector hr.my-2 {
        margin: 0.25rem 0 !important;
    }
}

/* Titre des panneaux flottants façon .info du legacy (panneau "Lieux" au
   survol notamment) : reproduit .info h4 de css/styles.css:38-42
   (color: #777), au lieu du noir par défaut de Bootstrap. */
.imm-info-title {
    color: #777;
    margin: 0 0 5px;
}

/* ==================== Header / footer publics ====================
   Reproduit l'identité visuelle du legacy (pages/nav.php, pages/footer.php) :
   bandeau noir, logo + nom + sous-titre, menus déroulants imbriqués. Sur
   mobile, la navbar Bootstrap bascule en menu hamburger repliable
   (comportement standard navbar-expand-lg, déjà responsive) — on ajoute
   uniquement des cibles tactiles ≥44px et un espacement adapté au doigt
   dans le menu déplié. */

/* Bandeau du mode preview admin (voir components/header.php) — couleur
   d'alerte franche pour ne jamais être confondu avec le reste du site,
   toujours au-dessus de la navbar dans l'ordre visuel. */
.imm-preview-banner {
    background-color: #b8860b;
    color: #fff;
    display: flex;
    flex-wrap: wrap;
    align-items: center;
    justify-content: center;
    gap: 0.75rem;
    padding: 0.4rem 1rem;
    font-size: 0.9rem;
    text-align: center;
}

.imm-navbar {
    background-color: #000;
    flex: 0 0 auto;
    /* Sans z-index explicite, la navbar (et ses menus déroulants
       Bootstrap, eux-mêmes en z-index:1000) se retrouve à égalité avec
       .map-overlay et les panneaux Leaflet (z-index:1000 également) —
       l'ordre DOM tranche alors en défaveur du header, déclaré avant
       #map-wrapper, et ses sous-menus apparaissent sous les panneaux de la
       carte. position: relative nécessaire pour que z-index s'applique. */
    position: relative;
    z-index: 2000;
}

/* Séparateur fin entre le header/footer et le contenu, inspiré du legacy
   (index_v2.php:74-76 et :83-85 : bande de 5px avec un filet noir d'un
   côté), réduit à 3px. Un élément dédié plutôt qu'une bordure sur le
   header/footer lui-même, comme en v2. */
.imm-hr-strip {
    flex: 0 0 auto;
    height: 4px;
    background: #fff;
    border-bottom: 1px solid #000;
}

.imm-hr-strip--footer {
    border-bottom: none;
    border-top: 1px solid #000;
}

.imm-navbar-brand {
    display: flex;
    align-items: center;
    gap: 0.5rem;
}

.imm-navbar-logo {
    filter: invert(88%) sepia(25%) saturate(396%) hue-rotate(343deg) brightness(89%) contrast(86%);
    flex: 0 0 auto;
}

.imm-navbar-brand-text {
    display: flex;
    flex-direction: column;
    justify-content: center;
    line-height: 1.05;
}

.imm-navbar-title {
    display: block;
    font-weight: 400;
    text-transform: uppercase;
    color: #fff;
    letter-spacing: 0.2em;
    font-size: 18px;
}

.imm-navbar-subtitle {
    display: block;
    margin-top: 2px;
    color: rgba(255, 255, 255, .75);
    font-size: .75rem;
}

/* Support des sous-menus imbriqués (.dropend) au clic comme au survol */
.imm-navbar .dropdown-menu {
    overflow: visible;
}

.imm-navbar .dropdown-menu .dropend {
    position: relative;
}

.imm-navbar .dropdown-menu .dropend > .dropdown-menu {
    top: 0;
    left: 100%;
    margin-left: .1rem;
}

@media (max-width: 991.98px) {
    /* Sous menu ouvert en cascade repoussé dans le flux plutôt qu'en overlay
       absolu, illisible une fois le menu principal replié en colonne. */
    .imm-navbar .dropdown-menu .dropend > .dropdown-menu {
        position: static;
        margin-left: 1rem;
    }

    .imm-navbar-search {
        margin-top: 0.5rem;
    }
}

/* Au-dessus du breakpoint navbar-expand-lg, le menu est affiché en ligne
   horizontale (usage souris/desktop) : la contrainte de cible tactile
   44x44px sur les liens du premier niveau (utile une fois le menu replié
   en colonne sur mobile) gonfle inutilement la hauteur de la barre par
   rapport au legacy. Elle reste appliquée dans les menus déroulants
   (dropdown-item), qui peuvent être ouverts au tactile même en desktop. */
@media (min-width: 992px) {
    .imm-navbar > .container-fluid > .navbar-collapse > .navbar-nav > .nav-item > .nav-link.touch-target,
    .imm-navbar-brand.touch-target {
        min-height: 0;
        min-width: 0;
    }
}

.imm-navbar-search {
    gap: 0.5rem;
}

.imm-navbar-search input[type="search"] {
    min-width: 0;
    width: 160px;
    max-width: 40vw;
}

@media (max-width: 991.98px) {
    .imm-navbar-search input[type="search"] {
        width: auto;
        max-width: none;
        flex: 1 1 auto;
    }
}

.imm-footer {
    flex: 0 0 auto;
}

.imm-footer-bar {
    text-align: center;
    background: #000;
    color: #fff;
    padding: 0 1rem;
    font-size: 0.8rem;
}

.imm-footer-link {
    color: #fff;
    text-decoration: underline;
}

/* Pages statiques (projet, équipe, sources, mentions légales, cour, aide,
   annuaires) : header + contenu + footer en colonne, le contenu prend
   l'espace disponible pour que le footer reste en bas même sur une page
   courte. */
.imm-static-page {
    display: flex;
    flex-direction: column;
    min-height: 100vh;
    min-height: 100dvh;
    margin: 0;
    /* Contrairement à .imm-map-page (fond noir), les pages de contenu texte
       ont un fond blanc — annule le background-color: #000 posé sur body
       par défaut (voir plus haut) pour éviter un flash de noir sous le
       contenu si l'article ne remplit pas toute la hauteur de page. */
    background-color: #fff;
}

.imm-static-page > article {
    flex: 1 1 auto;
}

/* Largeur des pages du menu "Projet" (description, équipe, sources,
   mentions légales, cour) — une seule classe plutôt qu'un style inline
   dupliqué dans les 5 vues project/*.php. */
.imm-static-article {
    max-width: 1100px;
}

/* Le contenu CMS des pages statiques (App\Models\Page::contentHtml, édité
   depuis le back-office) passe par App\Core\eHtml(), qui retire tout
   attribut class des balises h2/h3/p/ul/ol/li — ces règles reproduisent
   visuellement les utilitaires Bootstrap (.h5, .h6, mt-4, m-0...) qu'on ne
   peut plus mettre en ligne dans le HTML stocké en base. Portée à
   .imm-static-page article pour couvrir aussi help.php (pas de classe
   .imm-static-article sur son <article>). */
.imm-static-page article h2 {
    font-size: 1.25rem; /* équivalent Bootstrap .h5 */
    margin-top: 1.5rem;
    margin-bottom: 1rem;
}

.imm-static-page article h3 {
    font-size: 1rem; /* équivalent Bootstrap .h6 */
    margin-top: 1rem;
}

.imm-static-page article ul,
.imm-static-page article ol {
    margin-top: 0.25rem;
    margin-bottom: 1rem;
}

/* Annuaires "Résidents" et "Espaces" (directory.php, table DataTables
   partagée par les deux pages) : police compacte pour un tableau dense,
   colonne Mentions alignée à gauche (par défaut DataTables centre le
   contenu des dernières colonnes sur certains layouts). Les deux pages
   partagent le même template et doivent donc rester visuellement
   identiques. */
/* .dataTables_wrapper (pas juste #directory-table) : couvre aussi les
   contrôles générés par DataTables hors de la balise <table> elle-même
   (barre "Rechercher", "Affichage de X à Y sur Z entrées", pagination). */
#directory-table_wrapper {
    font-size: 12px;
}

#directory-table td.imm-directory-mentions {
    text-align: left;
}

/* Planchers de largeur (pas des %, voir directory.js) mesurés dans le
   contexte réel de la cellule (police 12px, padding DataTables 8px/10px
   inclus) sur les 272 résidents/785 espaces. Opendata : 134px nécessaires
   pour que les 4 icônes (Wikidata/Wikipédia/CRCV/Bossu) tiennent toujours
   sur une seule ligne — 140px avec une petite marge. Mentions : la mention
   la plus longue mesure jusqu'à 322px avec padding, mais posé volontairement
   en dessous (280px) pour garder la colonne compacte — les libellés les
   plus longs peuvent occasionnellement revenir à la ligne, compromis
   accepté au profit de la densité du tableau. */
#directory-table th.imm-directory-opendata,
#directory-table td.imm-directory-opendata {
    min-width: 140px;
}

#directory-table th.imm-directory-mentions,
#directory-table td.imm-directory-mentions {
    min-width: 280px;
}

/* Pictogramme "carte" (voir directory.js, OI_MAP_SVG) placé avant chaque
   mention : teinte foncée cohérente avec le texte sur fond blanc (contraire
   de .imm-oi-map, doré clair pensé pour l'offcanvas sombre de la carte), et
   marge à droite plutôt qu'à gauche puisqu'il précède le texte ici. */
#directory-table .imm-oi-map {
    fill: #495057;
    margin-left: 0;
    margin-right: 4px;
}

/* Logo de type (résident) ou icône associée, affiché avant le nom dans la
   colonne fusionnée Résident/Espace. */
.imm-directory-name-icon {
    height: 16px;
    width: auto;
    vertical-align: middle;
    margin-right: 6px;
}

/* Colonne "Opendata" (résidents) : icônes Wikidata/Wikipédia/CRCV/Bossu
   regroupées dans une seule cellule plutôt que 4 colonnes séparées, pour
   gagner en largeur au profit de Mentions. */
.imm-directory-opendata img {
    margin-right: 4px;
}

/* ==========================================================================
   Utilitaires — remplacent les anciens attributs style="" (CSP :
   style-src 'self', sans 'unsafe-inline'). Aucun style ne doit plus être
   écrit en attribut dans les vues ni dans le HTML construit par le JS ;
   les valeurs dynamiques passent par CSSOM (el.style.xxx = ...).
   ========================================================================== */

/* font-size: small (mot-clé, 13px) ≠ .small de Bootstrap (0.875em). */
.imm-text-small {
    font-size: small;
}

.imm-maxw-900 {
    max-width: 900px;
}

/* Logos Opendata (Wikidata/Wikipédia/CRCV/Bossu) de l'annuaire. */
.imm-logo-24 {
    width: 24px;
}

/* Petites icônes insérées dans le texte des panneaux de la carte. */
.imm-inline-icon-20 {
    width: 20px;
    height: 20px;
    vertical-align: middle;
}

.imm-inline-icon-28x20 {
    width: 28px;
    height: 20px;
    vertical-align: middle;
}

/* Lien d'évitement : masqué (Bootstrap .visually-hidden-focusable) jusqu'au
   focus clavier, puis affiché au-dessus de tout. */
.imm-skip-link:focus {
    position: fixed;
    top: 0.5rem;
    left: 0.5rem;
    z-index: 5000;
    padding: 0.5rem 1rem;
    background: #fff;
    color: #000;
    border: 2px solid #000;
    border-radius: 4px;
}
