/* ============================================================
   ARTAWEB -- Components
   Buttons, Navigation, Language Switcher, Dark Mode Toggle, Cards
   ============================================================ */

/* ----------------------------------------------------------
   Buttons
   ---------------------------------------------------------- */
.btn {
    display: inline-flex;
    align-items: center;
    gap: var(--space-sm);
    font-family: var(--font-heading);
    font-size: var(--text-sm);
    font-weight: 500;
    letter-spacing: 0.05em;
    text-transform: uppercase;
    /* Le rembourrage HORIZONTAL vient de la Charte (« Taille des boutons »).
       Le vertical, lui, reste fixe : c'est la hauteur de frappe, et la réduire
       rendrait la cible moins sûre au doigt. */
    padding: 0.85em var(--btn-pad-x, 2em);
    border-radius: var(--radius-sm);
    transition: all var(--transition-base);
    text-decoration: none;
    cursor: pointer;
    border: none;
}

.btn--primary {
    background-color: var(--color-accent);
    color: var(--color-bg);
}

.btn--primary:hover {
    background-color: var(--color-text);
    color: var(--color-bg);
}

.btn--outline {
    background: transparent;
    border: 1px solid var(--color-accent);
    color: var(--color-accent);
}

.btn--outline:hover {
    background-color: var(--color-accent);
    color: var(--color-bg);
}

.btn--ghost {
    background: transparent;
    color: var(--color-accent);
    padding: 0.5em 1em;
}

.btn--ghost:hover {
    color: var(--color-text);
}

/* LE STYLE « LIEN SIMPLE » N'AVAIT AUCUNE RÈGLE. renderBtn() le connaît depuis
   toujours — `$style === 'link'` y pose `.btn-link` au lieu de `.btn btn--…`,
   précisément pour qu'un lien souligné ne ressemble pas à un bouton — mais la
   classe n'était déclarée dans aucune feuille. L'exploitant qui choisissait
   « Lien simple » dans un composant Bouton, un CTA ou une carte obtenait donc du
   texte nu : ni couleur d'accent, ni soulignement, ni rien qui dise que c'est
   cliquable. Une option offerte à l'écran et sans effet visible.
   Le blog rendait déjà ce style, sous son propre nom (`.blog-links__link`,
   css/blog.css) : on reprend le même dessin, pour que « Lien simple » ait la
   même allure partout. */
/* ==========================================================================
   BIBLIOTHÈQUE D'ICÔNES — composant de vitrine (`icon-library`, réservé au dev)
   Grille compacte, groupée, repliable en <details> natif : aucun JavaScript,
   accessible au clavier, et imprimée ouverte.
   ========================================================================== */
/* Les deux mesures du composant, définies ICI et pas en repli d'un `var()` :
   ce sont des variables LOCALES, pas des jetons de thème. Sans cette
   déclaration, la taille « petite » (celle par défaut) n'en avait aucune —
   `minmax(var(--iconlib-cell), 1fr)` devenait invalide, la règle entière
   tombait, et la grille s'effondrait sur UNE colonne. Mesuré : 651 px de large
   pour une seule colonne, et un groupe de douze icônes haut de 781 px. */
.block-iconlib {
    --iconlib-size: 26px;
    --iconlib-cell: 68px;
}

.block-iconlib__group {
    /* Teintes dérivées de `currentColor`, jamais des jetons de thème : ce
       composant peut être posé dans une `.section--dark`, qui impose son propre
       texte blanc. Un `--color-border` de thème clair y disparaîtrait, et un
       `--color-text` y rendrait les icônes invisibles — c'est ce qui est arrivé
       au premier essai. Le composant HÉRITE, il ne décide pas. */
    border-top: 1px solid color-mix(in srgb, currentColor 18%, transparent);
    padding: var(--space-sm) 0;
}
.block-iconlib__group:last-child {
    border-bottom: 1px solid color-mix(in srgb, currentColor 18%, transparent);
}

.block-iconlib__title {
    cursor: pointer;
    font-family: var(--font-heading);
    font-size: var(--text-sm);
    font-weight: 600;
    letter-spacing: 0.04em;
    text-transform: uppercase;
    display: flex;
    align-items: center;
    gap: var(--space-sm);
    list-style: none;
}
/* La flèche par défaut de <summary> diffère d'un navigateur à l'autre ; on la
   remplace par un chevron qui pivote, identique partout. */
.block-iconlib__title::-webkit-details-marker { display: none; }
.block-iconlib__title::before {
    content: "";
    width: 0.45em;
    height: 0.45em;
    border-right: 2px solid currentColor;
    border-bottom: 2px solid currentColor;
    transform: rotate(-45deg);
    transition: transform var(--transition-base);
    flex: none;
}
.block-iconlib__group[open] > .block-iconlib__title::before { transform: rotate(45deg); }

.block-iconlib__count {
    font-size: var(--text-xs);
    font-weight: 500;
    opacity: 0.6;
    letter-spacing: 0;
}

/* `auto-fill` plutôt qu'un nombre de colonnes : la grille se densifie seule du
   téléphone au grand écran, sans point de rupture à tenir à jour. */
.block-iconlib__grid {
    list-style: none;
    margin: var(--space-sm) 0 0;
    padding: 0;
    display: grid;
    grid-template-columns: repeat(auto-fill, minmax(var(--iconlib-cell), 1fr));
    gap: var(--space-xs);
}
.block-iconlib__item {
    display: flex;
    flex-direction: column;
    align-items: center;
    gap: 0.35em;
    padding: 0.7em 0.3em;
    border-radius: var(--radius-sm);
    text-align: center;
    transition: background var(--transition-base);
}
.block-iconlib__item:hover { background: color-mix(in srgb, currentColor 8%, transparent); }
.block-iconlib__svg {
    width: var(--iconlib-size);
    height: var(--iconlib-size);
    /* Aucune couleur ici : le tracé suit `currentColor`, donc le texte de la
       section qui l'accueille. */
}
.block-iconlib__name {
    font-size: var(--text-xs);
    line-height: 1.25;
    opacity: 0.7;
    overflow-wrap: anywhere;
}

.block-iconlib--md { --iconlib-size: 32px; --iconlib-cell: 82px; }
.block-iconlib--lg { --iconlib-size: 40px; --iconlib-cell: 100px; }
/* Avec les noms affichés, il faut de la largeur : un libellé de deux mots ne
   doit pas se couper en quatre lignes. */
.block-iconlib--named { --iconlib-cell: 92px; }
.block-iconlib--named.block-iconlib--md { --iconlib-cell: 104px; }
.block-iconlib--named.block-iconlib--lg { --iconlib-cell: 120px; }

/* Icône facultative d'un bouton (renderBtn, option `icon`).
   `.btn` est déjà inline-flex avec un gap : il ne manquait que la taille du
   glyphe. En `em`, elle suit la taille du texte — un bouton agrandi garde ses
   proportions. `flex:none` l'empêche d'être comprimée par un libellé long. */
.btn__icon {
    width: 1.15em;
    height: 1.15em;
    flex: none;
}
/* Le style « link » n'est pas un bouton : il n'hérite donc pas du flex de
   `.btn`, et il faut le lui donner ici pour aligner l'icône sur le texte. */
.btn-link--with-icon {
    display: inline-flex;
    align-items: center;
    gap: 0.45em;
}

.btn-link {
    color: var(--color-accent);
    font-weight: 600;
    text-decoration: none;
    border-bottom: 1px solid currentColor;
}

.btn-link:hover {
    color: var(--color-text);
}

/* Bouton COMPACT. Utilisé par l'agenda (billetterie, inscription, itinéraire,
   filtres) : sections/event-single.php, sections/events-list.php et le composant
   « voir tout » d'includes/events.php.
   Il n'existait jusqu'ici que sous `body.admin-body` (css/admin.css) : côté
   public la classe ne correspondait à AUCUNE règle, et ces boutons se
   rendaient au gabarit plein. */
.btn--sm {
    font-size: var(--text-xs);
    padding: 0.55em 1.25em;
    gap: var(--space-xs);
    /* Cible tactile conforme (WCAG 2.5.8) malgré le gabarit compact. */
    min-height: 32px;
}

/* « Ne pas autoriser les renvois à la ligne » : le libellé ne se coupe plus. À n'activer
   que sur un libellé court — sur un écran étroit, un texte long qui refuse de
   se couper déborde, et un débordement est pire qu'un retour à la ligne. */
.btn--nowrap, .btn-link--nowrap { white-space: nowrap; }

.btn:focus-visible {
    outline: 2px solid var(--color-accent);
    outline-offset: 3px;
}

/* ----------------------------------------------------------
   Site Header / Navigation
   ---------------------------------------------------------- */
.site-header {
    position: fixed;
    top: 0;
    left: 0;
    right: 0;
    z-index: 100;
    /* AUCUN padding ici — il appartient aux LIGNES.
       Tant qu'il était posé sur l'en-tête, une ligne à fond coloré (la barre de
       service) commençait quelques pixels sous le haut de l'écran et laissait
       une bande transparente au-dessus d'elle. */
    transition: background-color var(--transition-base), box-shadow var(--transition-base);
}

.site-header.is-scrolled {
    background-color: var(--color-bg);
    box-shadow: var(--shadow-sm);
}

/* ----------------------------------------------------------
   Header — moteur de zones
   L'en-tête est composé dans l'admin : des lignes, contenant des colonnes,
   contenant des éléments. Voir includes/header-layout.php.
   ---------------------------------------------------------- */

/* Une ligne. `data-h` porte sa hauteur : c'est elle qui donne son épaisseur à
   l'en-tête, et elle est reprise côté serveur pour calculer --header-h.
   Le padding vertical est ici, et non sur l'en-tête : c'est ce qui permet à une
   ligne à fond coloré de toucher le haut de l'écran. */
.site-header__row { display: block; padding-block: var(--space-md); }
.site-header__row[data-h="compact"] { padding-block: var(--space-xs); }
.site-header__row[data-h="large"]   { padding-block: var(--space-lg); }
.site-header__row[data-h="compact"] .site-header__inner,
.site-header__row[data-h="compact"] .site-header__cols { min-height: 28px; }
.site-header__row[data-h="normal"] .site-header__inner,
.site-header__row[data-h="normal"] .site-header__cols { min-height: 56px; }
.site-header__row[data-h="large"] .site-header__inner,
.site-header__row[data-h="large"] .site-header__cols { min-height: 80px; }

/* Fonds de ligne. Une ligne opaque au-dessus d'un hero photo doit reprendre
   les couleurs de texte du thème : sans quoi le blanc forcé de
   `body.has-photo-hero` rendrait son contenu illisible. */
.site-header__row--surface { background-color: var(--color-surface); }

/* Fond « accent » : le texte du thème n'y est PAS lisible.
   Mesuré sur la palette de ce site : 2,97:1 entre la couleur de texte et
   l'accent, là où WCAG AA demande 4,5:1. Le blanc atteint 5,09:1. Une ligne à
   fond accent bascule donc tout son contenu en blanc — liens, icônes et
   séparateurs compris, sans quoi seuls certains mots resteraient lisibles. */
.site-header__row--accent { background-color: var(--color-accent); }
.site-header__row--accent,
.site-header__row--accent .main-nav__link,
.site-header__row--accent .lang-switcher__link,
.site-header__row--accent .dark-toggle,
.site-header__row--accent .site-header__text,
.site-header__row--accent .site-header__contact-link,
.site-header__row--accent .site-header__social-link { color: #fff; }
.site-header__row--accent .lang-switcher__link:not(.is-active) { color: rgba(255, 255, 255, 0.75); }
.site-header__row--accent .lang-switcher__sep { color: rgba(255, 255, 255, 0.4); }
body.has-photo-hero .site-header:not(.is-scrolled):not(.site-header--static) .site-header__row--surface .main-nav__link,
body.has-photo-hero .site-header:not(.is-scrolled):not(.site-header--static) .site-header__row--surface .lang-switcher__link,
body.has-photo-hero .site-header:not(.is-scrolled):not(.site-header--static) .site-header__row--surface .dark-toggle {
    color: var(--color-text);
}

/* Ligne pleine largeur : pas de conteneur, mais on garde une respiration
   latérale pour que le contenu ne colle pas au bord de l'écran. */
.site-header__full {
    width: 100%;
    padding-inline: var(--space-md);
}

/* Une seule colonne : conteneur flex historique (logo à gauche, reste à
   droite). C'est le rendu de l'en-tête d'origine, préservé tel quel. */
.site-header__inner {
    display: flex;
    align-items: center;
    justify-content: space-between;
}

/* Plusieurs colonnes : grille pilotée par --cols, posée en propriété
   personnalisée et non en grid-template-columns direct — c'est ce qui permet
   aux media queries de reprendre la main sans !important. Même mécanique que
   `.section-cols` pour le contenu (sections.css). */
.site-header__cols {
    display: grid;
    grid-template-columns: var(--cols, 1fr);
    align-items: center;
    gap: var(--space-md);
}

/* Une colonne. Les éléments s'y suivent horizontalement ; la colonne du milieu
   se centre, la dernière se colle à droite — c'est ce qu'on attend d'un
   en-tête, sans avoir à le régler à la main. */
.site-header__col {
    display: flex;
    align-items: center;
    gap: var(--space-md);
    min-width: 0;
}
/* Alignement du contenu dans la colonne, réglé dans l'éditeur d'en-tête.
   Ces classes remplacent les règles de position qui étaient déduites du rang de
   la colonne : la place des éléments se décide maintenant, elle ne se devine
   plus. Les valeurs par défaut reproduisent l'ancien comportement — voir
   headerRowAligns(). */
.site-header__col--left    { justify-content: flex-start; }
.site-header__col--center  { justify-content: center; }
.site-header__col--right   { justify-content: flex-end; }
.site-header__col--between { justify-content: space-between; }

/* Enveloppe d'un élément resté dans la barre. Neutre par défaut : elle ne sert
   qu'à porter les classes de comportement mobile. */
.site-header__item { display: flex; align-items: center; }

/* En-tête non collant : il défile avec la page. Le contenu n'a alors plus
   besoin d'être dégagé. */
.site-header--static { position: static; }
body:has(.site-header--static) { --header-h: 0px; }

/* EN-TÊTE « INTÉGRÉ » — il reste fixe et suit le défilement, mais ne recouvre
   rien : la première bande commence SOUS lui. Deux conséquences, et il faut les
   deux.

   1. Il lui faut son propre fond dès le chargement. `is-scrolled` ne le lui
      donne qu'une fois la page défilée ; sans cette règle, on verrait le haut
      de la première bande au travers.
   2. La première bande se décale par une MARGE, pas par un rembourrage. Le
      rembourrage laisse le FOND de la section remonter derrière l'en-tête —
      c'est exactement ce qu'on veut en mode superposé, et exactement ce qu'on
      ne veut pas ici. */
.site-header--integre { background-color: var(--color-bg); }
body.header-integre #main-content > .section:first-child {
    margin-top: var(--header-h, 88px);
    padding-top: calc(var(--space-xl, 3rem) * var(--sp-top, 1));
}

/* ----------------------------------------------------------
   Header — éléments
   Ce que produisent les réglages de chaque élément dans l'éditeur d'en-tête.
   Toute option ajoutée à headerItemTypes() a sa règle ICI, sans quoi le
   réglage s'affiche dans l'admin sans rien changer sur le site.
   ---------------------------------------------------------- */


/* Le menu n'a PAS de réglage d'alignement propre : c'est l'alignement de la
   COLONNE qui le place, comme tous les autres éléments. Deux réglages pour la
   même chose auraient surtout produit des combinaisons qui se contredisent. */

.site-header__logo img {
    height: 70px;
    width: auto;
    /* Thème clair : logo couleur affiché tel quel (pas d'inversion). */
    filter: none;
    transition: filter var(--transition-base), height var(--transition-base);
}

/* LES TROIS CRANS VIENNENT APRÈS LA TAILLE PAR DÉFAUT, ET C'EST TOUT L'ENJEU.
   ============================================================
   Ils étaient écrits AVANT. Or `.site-header__logo--small img` et
   `.site-header__logo img` ont la MÊME spécificité (une classe, un type) : à
   égalité, c'est la dernière déclarée qui gagne. La règle par défaut, placée
   ensuite, écrasait donc les deux crans — « Petit » et « Grand » ne changeaient
   rien pour un logo IMAGE, et l'exploitant qui cherchait à réduire son logo
   voyait le réglage rester sans effet.

   Trouvé le 2026-09-18 en posant le cran « Petit » sur artamedia.net : la classe
   arrivait bien dans le HTML, et le logo mesurait toujours 338 × 70 px. Les
   variantes `is-scrolled`, elles, fonctionnaient — leur sélecteur porte une
   classe de plus, donc l'emportait. Un même réglage marchait au défilement et
   pas au repos : c'est ce genre d'incohérence qui fait douter de l'écran plutôt
   que du code.

   La hauteur au défilement reste proportionnelle au cran. */
.site-header__logo--small img { height: 48px; }
.site-header__logo--large img { height: 92px; }
.site-header.is-scrolled .site-header__logo--small img { height: 40px; }
.site-header.is-scrolled .site-header__logo--large img { height: 68px; }

.site-header.is-scrolled .site-header__logo img {
    height: 56px;
    filter: none;
}

/* LE LOGO NE PREND PAS L'ÉCRAN D'UN TÉLÉPHONE.
   ============================================================
   La hauteur du logo était la même partout : 70 px (ou le cran choisi, ou la
   hauteur exacte réglée dans l'éditeur d'en-tête). Sur un écran de bureau
   c'est juste ; sur un téléphone, non — et d'autant moins que le logo est
   LARGE. Mesuré sur artamedia.net, dont le logo fait 4,8 pour 1 : 300 × 70 px
   sur un écran de 375, soit 80 % de la largeur, et 59 px seulement pour le
   bouton de menu. Signalé par Patrick : « le logo est trop gros, surtout sur
   mobile ».

   LA LARGEUR EST LA VRAIE CONTRAINTE, PAS LA HAUTEUR. Un logo carré à 70 px
   occupe 19 % d'un écran de 375 : il n'a aucun besoin de rétrécir. Le même
   réglage appliqué à un logo en bandeau en occupe 80 %. On borne donc les
   DEUX, avec `width`/`height` en `auto` pour que le navigateur inscrive
   l'image dans la boîte SANS l'écraser : un logo étroit garde sa hauteur, un
   logo large perd de la hauteur jusqu'à tenir.

   Les crans `--small` et `--large` gardent leur proportion, et une hauteur
   exacte réglée dans l'éditeur est plafonnée comme le reste : sur un
   téléphone, l'en-tête ne peut pas offrir davantage, quel que soit le
   souhait exprimé pour le bureau. */
@media (max-width: 899.98px) {
    .site-header__logo img,
    .site-header.is-scrolled .site-header__logo img,
    .site-header__logo[style*="--hd-logo-h"] img {
        height: auto;
        width: auto;
        max-height: 56px;
        max-width: 46vw;
    }
    .site-header__logo--small img { max-height: 44px; }
    .site-header__logo--large img { max-height: 68px; }
}

@media (max-width: 599.98px) {
    .site-header__logo img,
    .site-header.is-scrolled .site-header__logo img,
    .site-header__logo[style*="--hd-logo-h"] img {
        max-height: 44px;
        max-width: 52vw;
    }
    .site-header__logo--small img { max-height: 36px; }
    .site-header__logo--large img { max-height: 52px; }
}

/* En dark mode, l'en-tête scrollé reste foncé : le logo (s'il n'a pas de
   variante sombre dédiée) doit donc rester blanc même une fois scrollé. */
[data-theme="dark"] .site-header.is-scrolled .site-header__logo:not(:has(.site-header__logo-img--dark-only)) img {
    filter: brightness(0) invert(1);
}

/* Variantes light/dark du logo (si une version sombre est fournie via la Charte) */
.site-header__logo-img--dark-only { display: none; }

/* Quand une version sombre est fournie : on n'utilise plus le filter hack */
.site-header__logo:has(.site-header__logo-img--dark-only) .site-header__logo-img {
    filter: none;
}

/* Thème clair : on affiche toujours la version couleur (light-only).
   La version blanche (dark-only) n'apparaît qu'en dark mode. */
[data-theme="dark"] .site-header__logo-img--dark-only { display: block; }
[data-theme="dark"] .site-header__logo-img--light-only { display: none; }

/* ----- Logo TEXTE (alternative à l'image) + slogan -----
   Police / couleur / taille pilotées par la Charte (variables --logo-*).
   Le repli texte (nom du site) évite tout logo cassé quand aucune image n'est
   fournie. Le slogan s'affiche sous le logo (image OU texte). */
.site-header__logo--text  { display: inline-flex; align-items: center; }
/* LE SLOGAN SE CALE SOUS LE LOGO, et le réglage vient de la Charte.
   ⚠️  LE REPLI DE CHAQUE RÈGLE EST SON PROPRE DÉFAUT D'AVANT — `flex-start`
   ici, `stretch` au pied. C'est ce qui rend le réglage invisible tant que
   personne n'y touche : la variable n'est même pas émise dans ce cas
   (renderDesignCss), et chaque bloc garde l'alignement qu'il avait. */
.site-header__logo--stack { display: inline-flex; flex-direction: column; align-items: var(--logo-slogan-align, flex-start); justify-content: center; gap: 1px; }
.site-header__logo-text {
    font-family: var(--logo-text-font, var(--font-display));
    color: var(--logo-text-color, var(--color-accent));
    font-size: var(--logo-text-size, 1.7rem);
    font-weight: 700;
    line-height: 1.02;
    letter-spacing: -0.01em;
    white-space: nowrap;
    transition: font-size var(--transition-base);
}
.site-header.is-scrolled .site-header__logo-text { font-size: calc(var(--logo-text-size, 1.7rem) * 0.86); }
.site-header__logo--small .site-header__logo-text { font-size: calc(var(--logo-text-size, 1.7rem) * 0.8); }
.site-header__logo--large .site-header__logo-text { font-size: calc(var(--logo-text-size, 1.7rem) * 1.25); }
.site-header__slogan {
    font-family: var(--logo-slogan-font, var(--font-body));
    color: var(--logo-slogan-color, var(--color-muted));
    font-size: var(--logo-slogan-size, 0.8rem);
    font-weight: 400;
    line-height: 1.1;
    letter-spacing: 0.01em;
    white-space: nowrap;
}
/* Au-dessus d'un hero photo (en-tête translucide, non scrollé) : logo texte + slogan en blanc. */
/* Un hero photo passe l'en-tête en blanc : il flotte AU-DESSUS de la photo.
   Encore faut-il qu'il flotte. Un en-tête NON collant (`--static`) est une
   barre ordinaire, posée sur le fond de la page — blanc sur blanc. Le site le
   plus simple qui soit, en-tête fixe décoché et photo en tête de page, perdait
   ainsi son logo et son menu, sans qu'aucune erreur ne le signale.
   D'où le `:not(.site-header--static)` de toutes les règles qui suivent. */
body.has-photo-hero .site-header:not(.is-scrolled):not(.site-header--static) .site-header__logo-text,
body.has-photo-hero .site-header:not(.is-scrolled):not(.site-header--static) .site-header__slogan { color: #fff; }

/* Pied de page : logo texte + slogan (mêmes variables de Charte que l'en-tête). */
.site-footer__brand { display: inline-flex; flex-direction: column; gap: 2px; align-items: var(--logo-slogan-align, stretch); }
.site-footer__brand--text { align-items: var(--logo-slogan-align, flex-start); }
.site-footer__logo-text {
    font-family: var(--logo-text-font, var(--font-display));
    color: var(--logo-text-color, var(--color-accent));
    font-size: var(--logo-text-size, 1.7rem);
    font-weight: 700;
    line-height: 1.05;
    letter-spacing: -0.01em;
}
.site-footer__slogan {
    font-family: var(--logo-slogan-font, var(--font-body));
    color: var(--logo-slogan-color, var(--color-muted));
    font-size: var(--logo-slogan-size, 0.8rem);
    line-height: 1.1;
}

/* ----- En-tête AU-DESSUS d'un hero photo/slider (non scrollé) : tout en blanc -----
   Reproduit l'en-tête transparent de l'ancien site par-dessus la photo. */
body.has-photo-hero .site-header:not(.is-scrolled):not(.site-header--static) .main-nav__link,
body.has-photo-hero .site-header:not(.is-scrolled):not(.site-header--static) .dark-toggle { color: #fff; }
body.has-photo-hero .site-header:not(.is-scrolled):not(.site-header--static) .lang-switcher__link { color: rgba(255, 255, 255, 0.7); }
body.has-photo-hero .site-header:not(.is-scrolled):not(.site-header--static) .lang-switcher__link.is-active,
body.has-photo-hero .site-header:not(.is-scrolled):not(.site-header--static) .lang-switcher__link:hover { color: #fff; }
body.has-photo-hero .site-header:not(.is-scrolled):not(.site-header--static) .lang-switcher__sep { color: rgba(255, 255, 255, 0.35); }
body.has-photo-hero .site-header:not(.is-scrolled):not(.site-header--static) .menu-toggle__bar { background-color: #fff; }

/* ⚠️ EXCEPTION : le VOLET OUVERT n'est plus au-dessus de la photo.
   Les règles ci-dessus peignent en blanc ce qui se détache sur le hero. Mais
   sur téléphone, `#main-nav` est un panneau coulissant avec son PROPRE fond
   opaque (--color-surface) : une fois ouvert, ses liens ne sont plus sur la
   photo, ils sont sur du blanc. Résultat mesuré avant correction : liens
   rgb(255,255,255) sur panneau rgb(255,255,255) — contraste 1:1, menu
   totalement invisible.
   Le bouton burger avait déjà son correctif (« .main-nav.is-open ~ .menu-toggle »,
   plus bas) : c'est le contenu du volet qui avait été oublié. */
body.has-photo-hero .site-header:not(.is-scrolled):not(.site-header--static) .main-nav.is-open .main-nav__link,
body.has-photo-hero .site-header:not(.is-scrolled):not(.site-header--static) .main-nav.is-open .main-nav__sub-link,
body.has-photo-hero .site-header:not(.is-scrolled):not(.site-header--static) .main-nav.is-open .dark-toggle {
    color: var(--color-text);
}
/* La page courante et le survol retrouvent l'accent, comme partout ailleurs :
   la règle « tout en blanc » les écrasait aussi. */
body.has-photo-hero .site-header:not(.is-scrolled):not(.site-header--static) .main-nav.is-open .main-nav__link:hover,
body.has-photo-hero .site-header:not(.is-scrolled):not(.site-header--static) .main-nav.is-open .main-nav__link.is-active,
body.has-photo-hero .site-header:not(.is-scrolled):not(.site-header--static) .main-nav.is-open .main-nav__sub-link:hover,
body.has-photo-hero .site-header:not(.is-scrolled):not(.site-header--static) .main-nav.is-open .main-nav__sub-link.is-active {
    color: var(--color-accent);
}
/* Le sélecteur de langue, s'il a été posé dans le volet. */
body.has-photo-hero .site-header:not(.is-scrolled):not(.site-header--static) .main-nav.is-open .lang-switcher__link {
    color: var(--color-muted);
}
body.has-photo-hero .site-header:not(.is-scrolled):not(.site-header--static) .main-nav.is-open .lang-switcher__link.is-active,
body.has-photo-hero .site-header:not(.is-scrolled):not(.site-header--static) .main-nav.is-open .lang-switcher__link:hover {
    color: var(--color-accent);
}
body.has-photo-hero .site-header:not(.is-scrolled):not(.site-header--static) .main-nav.is-open .lang-switcher__sep {
    color: var(--color-border);
}
/* Logo blanc par-dessus le hero (non scrollé), logo couleur au scroll. */
body.has-photo-hero .site-header:not(.is-scrolled):not(.site-header--static) .site-header__logo-img--light-only { display: none; }
body.has-photo-hero .site-header:not(.is-scrolled):not(.site-header--static) .site-header__logo-img--dark-only { display: block; }

/* Main nav */
.main-nav {
    display: flex;
    align-items: center;
    gap: var(--space-lg);
}

.main-nav__list {
    display: flex;
    align-items: center;
    gap: var(--space-md);
}

.main-nav__link {
    /* Police et taille viennent de la Charte depuis le 2026-09-11.
       --menu-scale est un COEFFICIENT (1 par défaut), pas une taille : le menu
       en a trois selon le contexte — celle-ci, celle des sous-menus, et la
       valeur fluide du volet mobile — et chacune garde sa base propre. Sans
       réglage, le calcul redonne 0,78 rem : le pixel près. */
    font-family: var(--menu-font, var(--font-heading));
    font-size: calc(0.78rem * var(--menu-scale, 1));
    font-weight: 500;
    text-transform: uppercase;
    letter-spacing: 0.08em;
    /* Thème clair : texte foncé par défaut (header au-dessus d'un hero clair). */
    color: var(--color-text);
    padding: var(--space-xs) var(--space-sm);
    position: relative;
    transition: color var(--transition-fast);
    /* `inline-block` N'EST PAS COSMÉTIQUE : c'est ce qui empêche le lien de se
       couper en deux.
       ============================================================
       Le soulignement du menu est un `::after` en `position: absolute` — donc
       BLOQUIFIÉ par la spécification, quel que soit son `display` écrit. Un
       enfant de niveau bloc dans un parent `inline` coupe la boîte de ce parent
       en deux fragments : le lien mesurait 53,6 px de haut pour un texte de
       15 px, réparti sur DEUX lignes dont la seconde était vide.

       Conséquence visible, signalée par Patrick le 2026-09-18 : « le menu est
       décalé vers le haut ». La rangée centrait une boîte deux fois trop
       haute, et le texte se posait dans sa moitié supérieure — 13,6 px
       au-dessus de l'axe où tombent le logo et le sélecteur de langue.

       Trouvé en mesurant `getClientRects()` sur le lien : deux rectangles,
       21,5→44,5 et 52,1→75,1. Un seul suffisait à le dire.

       Une déclaration répare quatre choses à la fois — mesuré :
         · l'écart à l'axe        : −13,6 px → +1,1 px
         · la hauteur de l'en-tête : 93 px → 88 px (la ligne fantôme part avec)
         · le soulignement         : il revient sous le texte (top 54,6 → 30,2)
         · le sous-menu            : il s'ouvre 17 px plus près de son parent

       Et le rembourrage vertical se met enfin à compter : en `inline`, il ne
       participait pas à la ligne. La zone cliquable passe de 23 à 29 px de
       haut, ce qui n'est pas un détail sur un écran tactile. */
    display: inline-block;
}

/* SOULIGNEMENT DU MENU — trois états, réglés dans la Charte (`design_nav_rule`),
   sur le modèle exact des étiquettes de section. Deux jetons suffisent — la
   largeur au repos et la largeur active — parce que l'animation N'EST QUE le
   passage de l'une à l'autre. « Fixe » les met à égalité, « aucun » les met à
   zéro : dans les deux cas il ne reste rien à animer. */
.main-nav__link::after {
    content: '';
    position: absolute;
    bottom: -2px;
    left: calc(50% - var(--nav-rule-from, 0%) / 2);
    width: var(--nav-rule-from, 0);
    height: 1px;
    background-color: var(--color-accent);
    transition: width var(--transition-base), left var(--transition-base);
}

.main-nav__link:hover,
.main-nav__link.is-active {
    color: var(--color-accent);
}

.main-nav__link:hover::after,
.main-nav__link.is-active::after {
    width: var(--nav-rule-to, 100%);
    left: calc(50% - var(--nav-rule-to, 100%) / 2);
}

/* ----- Sous-menus (pages enfants) ----- */
.main-nav__item {
    position: relative;
}

.main-nav__sub {
    display: flex;
    flex-direction: column;
    gap: 2px;
    list-style: none;
    margin: 0;
    padding: var(--space-xs) 0;
}
/* Chaque entrée de sous-menu ancre le déroulant qu'elle peut porter.
   `.main-nav__item` le posait déjà pour le 1er niveau ; les <li> plus bas
   n'avaient aucune classe avant le passage à trois niveaux. */
.main-nav__item--sub { position: relative; }

/* Desktop : sous-menu flottant, affiché au survol / focus clavier.
   901px, comme le burger et le menu latéral : les trois seuils étaient
   auparavant à 769, 768 et 901, si bien qu'entre 769 et 900 le mode « menu
   latéral » retombait sans le dire sur un menu horizontal. */
@media (min-width: 900px) {
    .main-nav__sub {
        position: absolute;
        top: 100%;
        left: 0;
        min-width: 220px;
        background-color: var(--color-bg);
        box-shadow: var(--shadow-lg);
        border-radius: var(--radius-sm, 4px);
        padding: var(--space-sm) 0;
        opacity: 0;
        visibility: hidden;
        transform: translateY(6px);
        transition: opacity var(--transition-fast), transform var(--transition-fast), visibility var(--transition-fast);
        z-index: 95;
    }

    /* Le combinateur ENFANT est indispensable depuis le 3e niveau : en
       descendant (`.main-nav__item--has-sub:hover .main-nav__sub`), survoler
       une entrée de 1er niveau ouvrait d'un coup TOUS les déroulants qu'elle
       contient, petits-enfants compris. */
    .main-nav__item--has-sub:hover > .main-nav__sub,
    .main-nav__item--has-sub:focus-within > .main-nav__sub {
        opacity: 1;
        visibility: visible;
        transform: translateY(0);
    }

    /* 3e niveau : en CASCADE sur le côté, et non sous son parent — un
       déroulant qui pousserait vers le bas recouvrirait ses propres frères. */
    .main-nav__sub .main-nav__sub {
        top: 0;
        left: 100%;
        margin-left: 2px;
        transform: translateX(-6px);
    }
    .main-nav__item--has-sub:hover > .main-nav__sub .main-nav__sub,
    .main-nav__sub .main-nav__item--has-sub:hover > .main-nav__sub,
    .main-nav__sub .main-nav__item--has-sub:focus-within > .main-nav__sub {
        transform: translateX(0);
    }

    /* Une entrée qui en cache d'autres le dit : un chevron vers la droite. */
    .main-nav__item--sub.main-nav__item--has-sub > .main-nav__sub-link::after {
        content: '›';
        float: right;
        margin-left: var(--space-sm);
        opacity: .6;
    }
}

/* Téléphone : tout est dans le flux du panneau coulissant. Le 3e niveau se
   distingue par un retrait, pas par une position — il n'y a pas de survol. */
@media (max-width: 899.98px) {
    .main-nav__sub .main-nav__sub {
        padding-left: var(--space-md);
    }
}

.main-nav__sub-link {
    display: block;
    font-family: var(--menu-font, var(--font-heading));
    /* 0.8 -> 0.875rem : à 0,8 rem sans capitales ni interlettrage marqué, une
       entrée de sous-menu passait sous le premier niveau (0,78 rem, mais en
       CAPITALES, donc optiquement plus grande). Un sous-menu est moins affirmé
       qu'un menu, pas plus petit à lire.
       LE RAPPORT EST FIGÉ, PAS LA TAILLE (2026-09-11). La Charte pilote
       désormais la taille du menu ; 0,874 rem est ce qui rendait les deux
       niveaux optiquement égaux, et ce rapport vaut à toutes les tailles. Un
       sous-menu resté à 0,875 rem en dur serait passé SOUS son parent dès le
       cran « Grand ». Sans réglage : 0,874 rem, l'ancienne valeur à deux
       centièmes de pixel près. */
    font-size: calc(0.874rem * var(--menu-scale, 1));
    letter-spacing: 0.04em;
    color: var(--color-text);
    padding: var(--space-xs) var(--space-md);
    white-space: nowrap;
    transition: color var(--transition-fast), background-color var(--transition-fast);
}

.main-nav__sub-link:hover,
.main-nav__sub-link.is-active {
    color: var(--color-accent);
    /* --color-bg-alt n'a jamais été défini : le repli s'appliquait toujours.
       On utilise le token réel de la Charte, avec le même rendu en secours. */
    background-color: var(--color-surface, rgba(0, 0, 0, 0.04));
}

/* ----------------------------------------------------------
   Formulaires (bloc « form »)
   ---------------------------------------------------------- */
.block-form { max-width: 1000px; }

.mp-form {
    display: flex;
    flex-wrap: wrap;
    gap: var(--space-md);
}

/* Honeypot anti-spam : hors écran (pas display:none, pour piéger les robots). */
.mp-form__hp {
    position: absolute !important;
    left: -9999px !important;
    width: 1px;
    height: 1px;
    overflow: hidden;
}

.mp-form__field {
    flex: 1 1 100%;
    display: flex;
    flex-direction: column;
    gap: 0.35rem;
}
/* Largeurs de champ : 100% / 75% / 50% / 25% (on soustrait la gouttière pour
   que toutes les combinaisons tiennent sur une ligne). */
.mp-form__field--w75              { flex: 1 1 calc(75% - var(--space-md)); }
.mp-form__field--w50,
.mp-form__field--half             { flex: 1 1 calc(50% - var(--space-md)); }
.mp-form__field--w25              { flex: 1 1 calc(25% - var(--space-md)); }
@media (max-width: 599.98px) {
    /* Mobile : tous les champs en pleine largeur. */
    .mp-form__field,
    .mp-form__field--w75,
    .mp-form__field--w50,
    .mp-form__field--half,
    .mp-form__field--w25 { flex: 1 1 100%; }
}

/* ----------------------------------------------------------------
   LIGNE DE COLONNES
   ----------------------------------------------------------------
   Ce que la ligne apporte, que le `flex-wrap` ci-dessus ne donne pas :

   1. UN SAUT DE LIGNE GARANTI. En flex-wrap, rien ne borne une rangée : un
      champ à 50 % suivi de deux champs à 25 % remplit la même ligne, qu'on
      l'ait voulu ou non.
   2. DES LARGEURS EXACTES. `.mp-form__field` porte `flex-grow: 1` : un champ à
      25 % seul sur sa ligne s'étire à 100 %, et si un champ conditionnel de la
      rangée est masqué, ses voisins s'étalent pour prendre la place. Une piste
      de grille, elle, reste là où on l'a mise.

   Les ratios arrivent par --cols (« 1fr 3fr »), posée sur l'élément : le
   responsive vit ICI, pas dans le HTML. Même mécanique que les lignes de
   section (sections/_generic.php). */
.mp-form__row {
    flex: 1 1 100%;
    display: grid;
    grid-template-columns: var(--cols, 1fr);
    gap: var(--space-md);
    align-items: start;
}
/* Dans une ligne, la grille décide seule : on neutralise le flex du champ. */
.mp-form__row > .mp-form__field { flex: initial; }

/* ----------------------------------------------------------------
   ÉTAPES
   ----------------------------------------------------------------
   Sans JavaScript, une étape est un simple bloc pleine largeur : les pages
   s'affichent les unes sous les autres et le formulaire fonctionne. La barre de
   progression et les boutons sont posés par js/forms.js — ils n'existent donc
   pas ici quand il n'est pas là. */
.mp-form__step {
    flex: 1 1 100%;
    display: flex;
    flex-wrap: wrap;
    gap: var(--space-md);
    min-width: 0;
}
.mp-form__step[hidden] { display: none; }

.mp-form__steps {
    flex: 1 1 100%;
    display: flex;
    flex-wrap: wrap;
    gap: var(--space-sm) var(--space-md);
    margin: 0 0 var(--space-sm);
    padding: 0;
    list-style: none;
    counter-reset: etape;
}
.mp-form__steps-item {
    display: flex;
    align-items: center;
    gap: 0.5rem;
    font-size: var(--text-sm);
    color: var(--color-muted);
}
/* La pastille numérotée vient d'un compteur CSS : la numérotation suit l'ordre
   réel des étapes, sans que le JavaScript ait à l'écrire. */
.mp-form__steps-item::before {
    counter-increment: etape;
    content: counter(etape);
    display: grid;
    place-items: center;
    width: 1.6rem;
    height: 1.6rem;
    border-radius: 50%;
    border: 1px solid var(--color-border);
    font-size: var(--text-xs);
    font-variant-numeric: tabular-nums;
}
.mp-form__steps-item.is-current { color: var(--color-text); font-weight: 600; }
.mp-form__steps-item.is-current::before {
    background: var(--color-accent);
    border-color: var(--color-accent);
    color: var(--color-bg);
}
.mp-form__steps-item.is-done::before {
    /* Une étape franchie : la coche remplace le numéro, qui n'a plus d'usage. */
    content: '\2713';
    border-color: var(--color-accent);
    color: var(--color-accent);
}

.mp-form__stepnav {
    flex: 1 1 100%;
    display: flex;
    gap: var(--space-sm);
    align-items: center;
}
/* `[hidden]` ne suffit pas sur un bouton : la classe `.btn` pose un `display`
   explicite, qui l'emporte sur la règle par défaut du navigateur. Sans ces deux
   lignes, « Envoyer » restait visible dès la première page — et invitait à
   envoyer un formulaire que le serveur allait refuser. */
.mp-form__stepbtn[hidden],
.mp-form__submit[hidden] { display: none; }
/* « Précédent » est un retour en arrière : il se pousse à gauche, et « Suivant »
   reste du côté de la progression. */
.mp-form__stepnav .mp-form__stepbtn + .mp-form__stepbtn { margin-left: auto; }

/* ----------------------------------------------------------------
   GROUPE TITRÉ
   ----------------------------------------------------------------
   Un <fieldset> pour de vrai : le lecteur d'écran annonce la légende avant
   chaque champ du groupe, ce qu'aucun titre posé à côté ne fait. Les styles
   par défaut du navigateur (bordure gravée, marge latérale) sont remis à plat. */
.mp-form__fieldset {
    flex: 1 1 100%;
    display: flex;
    flex-wrap: wrap;
    gap: var(--space-md);
    min-width: 0;          /* sans quoi un <fieldset> refuse de rétrécir */
    margin: 0;
    padding: var(--space-md);
    border: 1px solid var(--color-border);
    border-radius: var(--radius-md, 10px);
    background: var(--color-surface);
}
.mp-form__legend {
    padding: 0 var(--space-xs);
    font-family: var(--font-heading);
    font-size: var(--text-base);
    font-weight: 600;
    color: var(--color-heading, var(--color-text));
}
.mp-form__fieldset[hidden] { display: none; }

/* ----------------------------------------------------------------
   POSITION DES LIBELLÉS
   ----------------------------------------------------------------
   Réglée pour tout le formulaire. Les deux mesures vivent en propriétés
   personnalisées : l'écart libellé/champ est ainsi le MÊME partout, quel que
   soit le type de champ, au lieu d'être répété dans chaque règle. */
.mp-form { --form-label-gap: 0.35rem; --form-label-width: 10rem; }
.mp-form__field { gap: var(--form-label-gap); }

/* À gauche : deux colonnes, le libellé aligné sur la première ligne du champ. */
.mp-form--labels-left .mp-form__field {
    display: grid;
    grid-template-columns: var(--form-label-width) minmax(0, 1fr);
    gap: var(--form-label-gap) var(--space-md);
    align-items: baseline;
}
/* Aide et erreur passent sous le champ, dans la colonne du champ. */
.mp-form--labels-left .mp-form__hint,
.mp-form--labels-left .mp-form__error { grid-column: 2; }
/* La case à cocher unique n'a pas de libellé séparé : son texte EST à côté
   d'elle. On la laisse occuper les deux colonnes. */
.mp-form--labels-left .mp-form__field > .mp-form__check { grid-column: 1 / -1; }

/* DANS UNE LIGNE À PLUSIEURS COLONNES, le libellé repasse au-dessus.
   Mesuré : dans une colonne à 25 %, un libellé de 10 rem ne laisse qu'une
   quarantaine de pixels au champ. « À gauche » vaut pour un champ qui dispose
   de la largeur du formulaire, pas pour un quart de celle-ci. Une ligne à une
   seule colonne, elle, garde le libellé à gauche : la place y est. */
.mp-form--labels-left .mp-form__row--multi .mp-form__field {
    grid-template-columns: minmax(0, 1fr);
    align-items: stretch;
}
.mp-form--labels-left .mp-form__row--multi .mp-form__hint,
.mp-form--labels-left .mp-form__row--multi .mp-form__error,
.mp-form--labels-left .mp-form__row--multi .mp-form__field > .mp-form__check { grid-column: 1; }

@media (max-width: 599.98px) {
    /* Sous 600 px, deux colonnes ne tiennent plus : le libellé repasse
       au-dessus, et une ligne de colonnes s'empile. */
    .mp-form--labels-left .mp-form__field {
        grid-template-columns: 1fr;
        align-items: stretch;
    }
    .mp-form--labels-left .mp-form__hint,
    .mp-form--labels-left .mp-form__error,
    .mp-form--labels-left .mp-form__field > .mp-form__check { grid-column: 1; }
    .mp-form__row { grid-template-columns: 1fr; }
}

.mp-form__label,
.mp-form .mp-form__label {
    font-family: var(--font-heading);
    font-size: var(--text-sm);
    font-weight: 500;
    color: var(--color-text);
}
.mp-form__req { color: var(--color-accent); }

.mp-form__input {
    width: 100%;
    padding: 0.7em 0.9em;
    border: 1px solid var(--color-border);
    border-radius: var(--radius-sm, 6px);
    background: var(--color-bg);
    color: var(--color-text);
    font: inherit;
    transition: border-color var(--transition-fast), box-shadow var(--transition-fast);
}
.mp-form__input:focus {
    outline: none;
    border-color: var(--color-accent);
    box-shadow: 0 0 0 3px color-mix(in srgb, var(--color-accent) 22%, transparent);
}
textarea.mp-form__input { resize: vertical; min-height: 6rem; }

.mp-form__group {
    flex: 1 1 100%;
    margin: var(--space-md) 0 0;
    padding-bottom: 0.4rem;
    border-bottom: 1px solid var(--color-border);
    font-size: var(--text-sm);
    text-transform: uppercase;
    letter-spacing: 0.08em;
    color: var(--color-accent);
}

.mp-form__checks,
.mp-form__radios {
    display: flex;
    flex-wrap: wrap;
    gap: 0.5rem 1.2rem;
}
.mp-form__field[hidden] { display: none; }
.mp-form__check {
    display: inline-flex;
    align-items: baseline;
    gap: 0.5rem;
    font-size: var(--text-sm);
    cursor: pointer;
}

.mp-form__note {
    flex: 1 1 100%;
    font-size: var(--text-sm);
    color: var(--color-muted);
    background: var(--color-surface);
    padding: var(--space-sm) var(--space-md);
    border-radius: var(--radius-sm, 6px);
}
.mp-form__hint { font-size: var(--text-xs); color: var(--color-muted); }
.mp-form__error { font-size: var(--text-xs); color: #c0392b; }
.mp-form__field.has-error .mp-form__input { border-color: #c0392b; }

.mp-form__submit { flex: 0 0 auto; margin-top: var(--space-sm); }

.mp-form__success,
.mp-form__alert {
    flex: 1 1 100%;
    padding: var(--space-md);
    border-radius: var(--radius-sm, 6px);
    font-weight: 500;
}
.mp-form__success {
    background: color-mix(in srgb, var(--color-accent) 15%, var(--color-bg));
    border: 1px solid var(--color-accent);
    color: var(--color-text);
}
.mp-form__alert {
    background: #fdecea;
    border: 1px solid #e6b0aa;
    color: #922b21;
}

/* Header controls (lang + dark mode) */
.header-controls {
    display: flex;
    align-items: center;
    gap: var(--space-md);
}

/* ----------------------------------------------------------
   Menu latéral (gauche / droite) — ordinateur uniquement.
   Sur mobile (< 901px), le menu reste en haut (toggle existant).
   ---------------------------------------------------------- */
@media (min-width: 900px) {
    [data-menu-position="left"] .site-header,
    [data-menu-position="right"] .site-header {
        top: 0;
        bottom: 0;
        right: auto;
        width: 220px;
        background-color: var(--color-bg);
        box-shadow: var(--shadow-sm);
        padding: var(--space-lg) var(--space-md);
    }
    [data-menu-position="right"] .site-header { left: auto; right: 0; }

    [data-menu-position="left"] .site-header__inner,
    [data-menu-position="right"] .site-header__inner {
        flex-direction: column;
        align-items: flex-start;
        justify-content: flex-start;
        gap: var(--space-xl);
        height: 100%;
    }
    /* La barre latérale a un fond solide → logo et texte foncés en permanence */
    [data-menu-position="left"] .site-header__logo img,
    [data-menu-position="right"] .site-header__logo img { filter: none; height: 44px; }

    [data-menu-position="left"] .main-nav,
    [data-menu-position="right"] .main-nav {
        flex-direction: column;
        align-items: flex-start;
        gap: var(--space-lg);
        width: 100%;
        margin-block: auto;
    }
    [data-menu-position="left"] .main-nav__list,
    [data-menu-position="right"] .main-nav__list {
        flex-direction: column;
        align-items: flex-start;
        gap: var(--space-sm);
    }
    [data-menu-position="left"] .main-nav__link,
    [data-menu-position="right"] .main-nav__link { color: var(--color-text); }

    /* Décale le contenu pour laisser place à la barre */
    [data-menu-position="left"] #main-content,
    [data-menu-position="left"] .site-footer { margin-left: 220px; }
    [data-menu-position="right"] #main-content,
    [data-menu-position="right"] .site-footer { margin-right: 220px; }
}

/* ----------------------------------------------------------
   Mobile Menu Toggle
   ---------------------------------------------------------- */
.menu-toggle {
    display: none;
    flex-direction: column;
    gap: 5px;
    padding: var(--space-sm);
    cursor: pointer;
    z-index: 110;
}

.menu-toggle__bar {
    display: block;
    width: 24px;
    height: 2px;
    /* La couleur du TEXTE par défaut, et non le blanc.
       Le blanc supposait un en-tête flottant au-dessus d'un hero sombre — un
       cas particulier, pas le cas général. Sur un en-tête ordinaire, posé sur
       le fond de la page, le bouton d'ouverture du menu était invisible : sur
       téléphone, le site n'avait plus de menu du tout. Le blanc reste, mais
       là où il se justifie : la règle `has-photo-hero` plus haut, qui vérifie
       que l'en-tête flotte VRAIMENT. */
    background-color: var(--color-text);
    transition: transform var(--transition-base), opacity var(--transition-fast), background-color var(--transition-fast);
}

.site-header.is-scrolled .menu-toggle__bar,
.main-nav.is-open ~ .menu-toggle .menu-toggle__bar {
    background-color: var(--color-text);
}

.menu-toggle.is-open .menu-toggle__bar:nth-child(1) {
    transform: translateY(7px) rotate(45deg);
}

.menu-toggle.is-open .menu-toggle__bar:nth-child(2) {
    opacity: 0;
}

.menu-toggle.is-open .menu-toggle__bar:nth-child(3) {
    transform: translateY(-7px) rotate(-45deg);
}

/* Bascule vers le menu déroulant. Le seuil est passé de 768 à 900px pour
   rejoindre celui du menu latéral : sur une tablette en portrait, le menu
   horizontal débordait déjà sur deux lignes. */
@media (max-width: 899.98px) {
    .menu-toggle {
        display: flex;
    }

    /* LE BURGER FERME TOUJOURS LA MARCHE.
       Il est écrit à la place du premier élément destiné au volet — donc AVANT
       un bouton d'action posé après le menu, ce qui est l'ordre voulu sur
       ordinateur. Sur téléphone il doit être le dernier de la barre : c'est le
       coin que le pouce atteint, et la place que tout le monde lui connaît.
       `order` le déplace ici seulement, sans toucher au balisage.

       L'ordre du DOM ne bouge pas : le burger reste collé, dans le document, au
       volet qu'il commande — ce qui compte pour un lecteur d'écran. Seul le
       parcours au clavier passe par lui avant le bouton d'action affiché à sa
       gauche : un arrêt de tabulation en décalage, contre une barre lisible. */
    .menu-toggle { order: 99; }

    /* « RÉPARTI » NE DOIT PAS DÉPENDRE DU NOMBRE D'ÉLÉMENTS.
       `space-between` partage l'espace libre entre TOUS les éléments. À deux —
       logo et burger — cela donne bien un à chaque bout. Au troisième (un
       bouton d'action, un numéro de téléphone), l'espace se coupe en deux et le
       burger tombe au milieu : la place du burger dépendait donc du nombre de
       voisins, sans que rien ne l'explique ni ne permette de le rattraper.

       Une marge automatique sur le premier élément absorbe tout l'espace libre
       AVANT que `justify-content` n'ait quoi que ce soit à répartir. Le premier
       reste à gauche, tout le reste se groupe à droite — quel qu'en soit le
       nombre.

       Portée volontairement étroite : seulement les conteneurs réglés sur
       « Réparti » ET qui portent le burger. Un alignement « Centré », « Gauche »
       ou « Droite » choisi dans l'éditeur d'en-tête reste intact, et le premier
       élément d'un conteneur sans burger aussi. Le sélecteur vaut pour les deux
       formes de conteneur : `.site-header__inner` (une seule colonne) porte la
       même classe d'alignement que `.site-header__col`.

       `:not(.menu-toggle)` n'est pas une précaution en l'air : dans une colonne
       qui ne contient PAS le logo — la deuxième ligne du modèle « Logo centré »,
       par exemple — le premier élément du DOM est justement le burger, et lui
       poser cette marge le renverrait vers la gauche. */
    .site-header__col--between:has(> .menu-toggle) > *:first-child:not(.menu-toggle) {
        margin-right: auto;
    }

    /* L'écart entre éléments groupés. `.site-header__col` en a un depuis
       toujours ; `.site-header__inner` — le conteneur d'une ligne à colonne
       unique — n'en avait pas besoin, parce que `space-between` écartait tout
       le monde. Une fois les éléments groupés à droite, ils se touchent :
       bouton d'action et burger collés, deux cibles tactiles sans séparation.
       Le repli à 16 px vaut ce que la barre utilisait déjà ailleurs. */
    .site-header__inner { gap: var(--space-md, 1rem); }

    /* LE VOLET DOIT POUVOIR DÉFILER, ET TROIS DÉTAILS S'Y LIGUENT.
       Une page à long sous-menu — Fonctionnalités et Composants en ont douze
       chacune — dépasse la hauteur de l'écran, et le volet coupait sans recours.

       1. `justify-content: flex-start`, et non `center`. Ce n'est pas un choix
          d'esthétique : un conteneur flex centré dont le contenu DÉPASSE
          déborde des DEUX côtés, et la partie sortie par le haut reste
          inatteignable — le défilement ne remonte pas avant le début de la
          boîte. Centrer et pouvoir défiler s'excluent.
       2. `overflow-y: auto`, évidemment — mais sans le point 1 il n'aurait
          rien réglé.
       3. `100dvh` plutôt que `100vh` : sur téléphone, `vh` compte la hauteur
          de l'écran barre d'adresse RÉTRACTÉE. Le bas du volet passait donc
          sous la barre du navigateur. `vh` reste en repli pour les moteurs
          qui ignorent `dvh`. */
    .main-nav {
        position: fixed;
        top: 0;
        right: -100%;
        /* HORS DE PORTÉE DU CLAVIER, pas seulement hors de l'écran (2026-09-10).
           -----------------------------------------------------------------
           `right: -100%` pousse le volet dehors, mais il reste dans l'ordre de
           tabulation ET dans l'arbre d'accessibilité. Mesuré par l'audit
           indépendant : à 390 px, ouvrir le menu, presser Échap, puis Tab —
           le focus part sur « Accueil » à x = 532 px, hors écran. Le visiteur
           au clavier traverse des liens qu'il ne voit pas, et le lecteur
           d'écran les annonce.

           `visibility: hidden` retire les deux d'un coup. Le délai de
           transition la fait basculer APRÈS le glissement, sinon le volet
           disparaîtrait d'un coup au lieu de sortir ; à l'ouverture,
           `.is-open` remet la transition à zéro pour qu'il soit atteignable
           immédiatement.

           EN CSS ET NON EN JS, délibérément : le volet ne s'ouvre QUE par le
           JS. Si celui-ci ne se charge pas, un attribut `inert` posé par JS ne
           serait jamais posé non plus — et l'on retomberait exactement sur le
           défaut qu'on corrige. La règle vit dans la requête média du
           téléphone : sur grand écran, la barre de navigation est visible en
           permanence et n'est pas concernée. */
        visibility: hidden;
        width: 80%;
        max-width: 360px;
        height: 100vh;
        height: 100dvh;
        background-color: var(--color-bg);
        flex-direction: column;
        align-items: flex-start;
        justify-content: flex-start;
        overflow-y: auto;
        overscroll-behavior: contain;
        padding: var(--space-xl);
        gap: var(--space-lg);
        transition: right var(--transition-base),
                    visibility 0s linear var(--transition-base);
        box-shadow: var(--shadow-lg);
    }

    .main-nav.is-open {
        right: 0;
        visibility: visible;
        transition: right var(--transition-base), visibility 0s;
    }

    .main-nav__list {
        flex-direction: column;
        align-items: flex-start;
        gap: var(--space-lg);
    }

    .main-nav__link {
        /* LE VOLET GARDE SA TAILLE FLUIDE et suit quand même le réglage : la
           base reste --text-lg, que le coefficient module. Une taille absolue
           venue de la Charte aurait figé ici une valeur pensée pour un menu
           horizontal, dans un volet tactile où la cible doit rester grande. */
        font-size: calc(var(--text-lg) * var(--menu-scale, 1));
    }

    /* Langues et jour/nuit remontent EN TÊTE du volet. Ils sont rendus après
       le menu — c'est l'ordre voulu sur ordinateur, où ils suivent la barre de
       navigation — mais dans un volet qui défile, ce qui suit un long menu
       n'est atteint qu'après l'avoir traversé. `order` le corrige ici seulement,
       sans toucher au balisage ni au rendu sur grand écran.

       Les deux voyagent ensemble : le moteur les écrit dans un même
       `.header-controls` (renderHeaderPanel), et les séparer en CSS casserait
       la mise en page du groupe. */
    .main-nav > .header-controls {
        order: -1;
        flex-direction: column;
        align-items: flex-start;
        gap: var(--space-md);
    }
    .header-controls {
        flex-direction: column;
        align-items: flex-start;
        gap: var(--space-md);
    }
}

/* Mobile overlay */
.nav-overlay {
    display: none;
    position: fixed;
    inset: 0;
    background-color: rgba(26, 26, 26, 0.5);
    z-index: 90;
    opacity: 0;
    transition: opacity var(--transition-base);
}

.nav-overlay.is-visible {
    display: block;
    opacity: 1;
}

/* ----------------------------------------------------------
   Language Switcher
   ---------------------------------------------------------- */
.lang-switcher {
    display: flex;
    align-items: center;
    gap: 2px;
    font-family: var(--font-heading);
    font-size: var(--text-xs);
    font-weight: 500;
    text-transform: uppercase;
    letter-spacing: 0.05em;
}

.lang-switcher__link {
    /* Thème clair : couleur atténuée par défaut. */
    color: var(--color-muted);
    padding: var(--space-xs);
    transition: color var(--transition-fast);
}

.lang-switcher__link.is-active {
    color: var(--color-accent);
}

.lang-switcher__link:hover {
    color: var(--color-text);
}

.lang-switcher__sep {
    color: var(--color-border);
    user-select: none;
}

/* Scrolled state */
.site-header.is-scrolled .lang-switcher__link {
    color: var(--color-muted);
}

.site-header.is-scrolled .lang-switcher__link.is-active {
    color: var(--color-accent);
}

.site-header.is-scrolled .lang-switcher__link:hover {
    color: var(--color-text);
}

.site-header.is-scrolled .lang-switcher__sep {
    color: var(--color-border);
}

/* ----------------------------------------------------------
   Dark Mode Toggle
   ---------------------------------------------------------- */
.dark-toggle {
    display: flex;
    align-items: center;
    justify-content: center;
    width: 36px;
    height: 36px;
    border-radius: 50%;
    /* Thème clair : icône foncée par défaut. */
    color: var(--color-text);
    transition: background-color var(--transition-fast), color var(--transition-fast);
    cursor: pointer;
}

.dark-toggle:hover {
    background-color: var(--color-surface);
}

/* Scrolled state */
.site-header.is-scrolled .dark-toggle {
    color: var(--color-text);
}

.site-header.is-scrolled .dark-toggle:hover {
    background-color: var(--color-surface);
}

.dark-toggle:focus-visible {
    outline: 2px solid var(--color-accent);
    outline-offset: 2px;
}

.dark-toggle__icon {
    width: 18px;
    height: 18px;
}

/* Sun icon hidden in light mode, moon hidden in dark mode */
.dark-toggle__sun  { display: none; }
.dark-toggle__moon { display: block; }

[data-theme="dark"] .dark-toggle__sun  { display: block; }
[data-theme="dark"] .dark-toggle__moon { display: none; }

/* ----------------------------------------------------------
   Cards
   ---------------------------------------------------------- */
.card {
    background-color: var(--color-surface);
    border-radius: var(--radius-md);
    overflow: hidden;
    transition: box-shadow var(--transition-base), transform var(--transition-base);
}

.card:hover {
    box-shadow: var(--shadow-md);
    transform: translateY(-2px);
}

.card__image {
    aspect-ratio: 4 / 3;
    overflow: hidden;
}

.card__image img {
    width: 100%;
    height: 100%;
    object-fit: cover;
    transition: transform var(--transition-slow);
}

.card:hover .card__image img {
    transform: scale(1.03);
}

.card__body {
    padding: var(--space-lg);
}

.card__title {
    font-family: var(--font-heading);
    font-size: var(--text-lg);
    font-weight: 500;
    margin-bottom: var(--space-sm);
}

/* ----------------------------------------------------------
   Separator / Divider
   ---------------------------------------------------------- */
.divider {
    width: 60px;
    height: 1px;
    background-color: var(--color-accent);
    margin-inline: auto;
    margin-block: var(--space-lg);
}

.divider--left {
    margin-inline: 0;
}

/* ----------------------------------------------------------
   Tag / Badge
   ---------------------------------------------------------- */
.tag {
    display: inline-block;
    font-family: var(--font-heading);
    font-size: var(--text-xs);
    font-weight: 500;
    text-transform: uppercase;
    letter-spacing: 0.08em;
    color: var(--color-accent);
    border: 1px solid var(--color-accent);
    padding: 0.25em 0.75em;
    border-radius: var(--radius-sm);
}

/* ============================================================
   BLOCKS — Bibliothèque de composants éditoriaux
   Rendus par includes/blocks.php (switch sur block_type)
   ============================================================ */

/* --- Image simple --- */
/* `--block-rhythm` vient de la Charte (Mise en page → Rythme vertical). Repli
   `--space-lg` : la valeur qui était en dur, pour qu'une feuille en cache ne
   change rien. Les autres composants « lourds » suivent le même pas plus bas. */
.block-image {
    margin: var(--block-rhythm, var(--space-lg)) 0;
}
/* ⚠️  `max-width`, PAS `width`. Avec `width: 100%`, une image plus PETITE que
   sa colonne était AGRANDIE : un logo de 260 px s'affichait à 460, une médaille
   de 200 px à 260 — floue, et d'autant plus visible sur un visuel de
   certification. Les paliers ci-dessous (`--sm`, `--md`, `--lg`) ne faisaient
   que plafonner, ils n'empêchaient rien : choisir « petite » pouvait donc
   GROSSIR l'image, ce que personne n'a jamais voulu dire.
   Une image plus grande que sa colonne, elle, est toujours ramenée à 100 %. */
.block-image img {
    max-width: 100%;
    width: auto;
    height: auto;
    border-radius: var(--radius-sm);
}

/* L'exception : « Pleine largeur (bord à bord) » demande explicitement que
   l'image occupe toute la fenêtre — là, on l'étire. */
.block-image--bleed img {
    width: 100%;
}
/* Cadre (option « Cadre de l'image ») : coins arrondis + ombre + fin liseré.
   Le liseré s'éclaircit sur fond sombre, où l'ombre ne se voit pas. */
.block-image--frame img {
    border-radius: 12px;
    box-shadow: 0 20px 45px rgba(0, 0, 0, 0.22);
    outline: 1px solid rgba(0, 0, 0, 0.06);
}
.section--dark .block-image--frame img {
    outline-color: rgba(255, 255, 255, 0.12);
}
/* Débordement (option « Pleine largeur (bord à bord) ») : l'image sort de la
   colonne pour occuper toute la largeur de la fenêtre. Le calcul 50% - 50vw se
   fait par rapport au conteneur centré, donc l'image se recentre sur la fenêtre. */
.block-image--bleed {
    width: 100vw;
    max-width: 100vw;
    margin-left: calc(50% - 50vw);
    margin-right: calc(50% - 50vw);
    border-radius: 0;
    overflow: hidden;
}
.block-image--bleed img {
    border-radius: 0;
}
/* En tête de la 1re section, une image pleine largeur se colle juste SOUS
   l'en-tête du SITE. Deux cas, et il a fallu les séparer.

   ⚠️ CETTE MARGE NÉGATIVE N'ÉTAIT JUSTE QUE SI UN FILET PRÉCÉDAIT L'IMAGE.
   Elle annule la marge basse de l'en-tête de SECTION (le filet décoratif, rendu
   dans un `.container` avant la ligne) : --space-xl d'un côté, --space-xl de
   l'autre, somme nulle, image collée. Mais ce filet est une CASE À COCHER.
   Décochée, il n'y a plus rien à annuler — et la marge négative tirait l'image
   de 64px SOUS l'en-tête fixe, où elle disparaissait.

   Mesuré le 2026-09-15, les deux sites côte à côte : la vitrine en ligne (filet
   décoché) montrait un écart de -64px, l'install locale (filet coché) un écart
   de +1px. Même version, même CSS, même composant : seul le réglage différait.
   Ce qui ressemblait à un défaut de déploiement était un défaut de règle.

   On part donc de zéro — l'image colle, point — et on n'annule QUE s'il y a
   quelque chose à annuler. */
#main-content > .section:first-child .section__generic-body > .block-image--bleed:first-child,
#main-content > .section:first-child .section__generic-body > .block-fx:first-child > .block-image--bleed {
    margin-top: 0;
    margin-bottom: var(--space-xl, 3rem);
}
#main-content > .section:first-child:has(> .container) .section__generic-body > .block-image--bleed:first-child,
#main-content > .section:first-child:has(> .container) .section__generic-body > .block-fx:first-child > .block-image--bleed {
    margin-top: calc(-1 * var(--space-xl, 3rem));
}

/* ⚠️ CE RETRAIT NE SUFFISAIT PAS, et il visait à côté (mesuré le 2026-09-13).
   La marge négative ci-dessus REMONTE bien de --space-xl — mais ce qu'elle
   annule, c'est le RYTHME PROPRE du composant image (`.block-image` porte
   `margin: var(--block-rhythm)`), pas l'espace que la section réserve en plus
   de l'en-tête. Les deux valant --space-xl, l'une mangeait l'autre et le
   compte semblait juste. Il ne l'était pas : il restait un bandeau de la
   hauteur d'un --space-xl entre l'en-tête et la photo — 51px mesurés sur la
   vitrine, et le bandeau GRANDISSAIT dès qu'on corrigeait --header-h.

   On retire donc le terme à la source : quand le contenu de la première
   section COMMENCE par une image pleine largeur, la section ne réserve que la
   hauteur de l'en-tête. La marge négative ci-dessus continue, elle, d'annuler
   le rythme du composant. Résultat : l'image touche l'en-tête.

   La condition porte sur l'image, PAS sur `body.has-photo-hero` : cette
   classe-là est vraie dès que la première bande est sombre, donc aussi sur des
   pages qui commencent par un <h1> (Fonctionnalités, Modules, Télécharger).
   S'y accrocher aurait collé ces titres contre l'en-tête. */
#main-content > .section:first-child:has(.section__generic-body > .block-image--bleed:first-child),
#main-content > .section:first-child:has(.section__generic-body > .block-fx:first-child > .block-image--bleed) {
    padding-top: var(--header-h, 88px);
}

/* --- Citation --- */
.block-quote {
    margin: var(--space-xl) auto;
    max-width: 720px;
    text-align: center;
    padding: var(--space-lg) var(--space-md);
    position: relative;
}
.block-quote blockquote {
    font-family: var(--font-body);
    font-size: calc(var(--text-lg) * 1.15);
    font-style: italic;
    line-height: 1.5;
    color: var(--color-text);
    margin: 0;
    padding: 0;
    quotes: "“" "”";
}
.block-quote blockquote::before {
    content: open-quote;
    display: block;
    font-size: 3em;
    line-height: 0.5;
    color: var(--color-accent);
    margin-bottom: var(--space-sm);
}
.block-quote cite {
    display: block;
    margin-top: var(--space-sm);
    font-family: var(--font-heading);
    font-style: normal;
    font-size: var(--text-xs);
    text-transform: uppercase;
    letter-spacing: 0.1em;
    color: var(--color-muted);
}

/* --- Bouton (wrapper) --- */
.block-button-wrap {
    margin: var(--space-lg) 0;
}

/* --- CTA --- */
.block-cta {
    background: var(--color-surface);
    border: 1px solid var(--color-border);
    border-radius: var(--radius-md);
    padding: var(--space-xl);
    margin: var(--space-xl) 0;
    text-align: center;
}
.block-cta__title {
    font-size: var(--text-xl);
    margin-bottom: var(--space-sm);
}
.block-cta__text {
    color: var(--color-muted);
    margin-bottom: var(--space-lg);
    max-width: 560px;
    margin-inline: auto;
}

/* --- Divider --- */
.block-divider {
    width: 80px;
    height: 1px;
    background: var(--color-accent);
    border: 0;
    margin: var(--space-xl) auto;
}

/* --- Menu (composant de navigation) ---------------------------------------
   Une liste, pas un cadre : posé dans une colonne latérale, le composant doit
   longer le contenu sans se donner l'air d'une carte. Le repère visuel est le
   filet de gauche, qui s'épaissit sous l'entrée courante — même vocabulaire
   que le rail de l'aide, sans lui emprunter une ligne (l'aide vit dans
   l'administration, qui n'a pas la charte du site).
   Aucune règle de `position: sticky` ici : le collant est un réglage de la
   colonne latérale (migration 0024), pas une propriété du composant. Le poser
   ici le tuerait d'ailleurs en silence dès qu'on déposerait le menu dans une
   ligne ordinaire — `.section` porte `overflow: hidden`. */
.block-menu { font-size: var(--text-sm, .9375rem); }

.block-menu__title {
    margin: 0 0 var(--space-sm);
    font-weight: 600;
    color: var(--color-heading);
}

.block-menu__list {
    margin: 0;
    padding: 0;
    list-style: none;
}

/* Les niveaux imbriqués se décalent et prennent un filet à eux. Le premier
   niveau n'en a pas : sans contenu à sa gauche, un filet flotterait. */
.block-menu__list--n2,
.block-menu__list--n3 {
    margin-top: 2px;
    padding-left: var(--space-sm);
    border-left: 1px solid var(--color-border);
}

.block-menu__link {
    display: block;
    padding: 6px 0 6px var(--space-sm);
    /* Le filet de l'entrée est TRANSPARENT au repos et non absent : sans lui,
       l'entrée se décalerait de 2 px en devenant active. */
    border-left: 2px solid transparent;
    margin-left: -1px;
    color: var(--color-text);
    text-decoration: none;
    line-height: 1.35;
    transition: color .12s ease, border-color .12s ease;
}

.block-menu__link:hover,
.block-menu__link:focus-visible {
    color: var(--color-accent);
    border-left-color: var(--color-border);
}

/* `is-active` est posée par le scrollspy (js/main.js) pour les ancres, et par
   le serveur (aria-current) pour les pages. Les deux mènent au même dessin. */
.block-menu__link.is-active,
.block-menu__link[aria-current="page"] {
    color: var(--color-accent);
    border-left-color: var(--color-accent);
    font-weight: 600;
}

/* --- Spacer --- */
.block-spacer {
    display: block;
    width: 100%;
}

/* --- Vidéo --- */
.block-video {
    margin: var(--block-rhythm, var(--space-xl)) 0;
}
.block-video__frame {
    position: relative;
    width: 100%;
    aspect-ratio: 16 / 9;
    overflow: hidden;
    border-radius: var(--radius-md);
    background: #000;
}
.block-video__frame iframe {
    position: absolute;
    inset: 0;
    width: 100%;
    height: 100%;
    border: 0;
}
/* Façade : ce que l'on voit tant que la vidéo n'a pas été demandée. Le cadre
   .block-video__frame fournit déjà le format 16/9 et le fond noir. */
.block-video__facade {
    position: absolute;
    inset: 0;
    width: 100%;
    height: 100%;
    padding: 0;
    border: 0;
    cursor: pointer;
    display: grid;
    place-items: center;
    /* Sans vignette, un aplat sobre plutôt qu'un trou noir : on doit voir qu'il
       y a quelque chose à cliquer, même quand l'image d'aperçu manque. */
    background: linear-gradient(160deg, var(--color-surface-alt, #1c1c1c), #000);
}
.block-video__poster {
    position: absolute;
    inset: 0;
    width: 100%;
    height: 100%;
    object-fit: cover;
}
.block-video__play {
    position: relative;
    width: 4.5rem;
    height: 4.5rem;
    border-radius: 50%;
    background: rgba(0, 0, 0, 0.55);
    border: 2px solid #fff;
    transition: transform 0.18s ease, background-color 0.18s ease;
}
.block-video__play::before {
    content: "";
    position: absolute;
    top: 50%;
    left: 54%;
    transform: translate(-50%, -50%);
    border-style: solid;
    border-width: 0.85rem 0 0.85rem 1.35rem;
    border-color: transparent transparent transparent #fff;
}
.block-video__facade:hover .block-video__play,
.block-video__facade:focus-visible .block-video__play {
    transform: scale(1.08);
    background: var(--color-primary, #2f6fed);
}
/* La mention du tiers est une information, pas une décoration : elle reste
   lisible sur une vignette claire comme sur un aplat sombre. */
.block-video__note {
    position: absolute;
    left: 0;
    right: 0;
    bottom: 0;
    padding: 0.55rem 0.9rem;
    background: rgba(0, 0, 0, 0.62);
    color: #fff;
    font-size: var(--text-xs);
    line-height: 1.4;
    text-align: center;
}
.block-video__fallback {
    position: absolute;
    inset: 0;
    display: grid;
    place-items: center;
    color: #fff;
    text-decoration: underline;
}
@media (prefers-reduced-motion: reduce) {
    .block-video__play { transition: none; }
    .block-video__facade:hover .block-video__play,
    .block-video__facade:focus-visible .block-video__play { transform: none; }
}

/* Un lecteur de son n'a pas de scène : il occupe la largeur et se contente de
   sa propre hauteur. Sans cette règle il héritait du cadre 16/9 de la vidéo. */
.block-video--audio audio {
    width: 100%;
    display: block;
}

.block-video video {
    width: 100%;
    height: auto;
    border-radius: var(--radius-md);
    display: block;
}
.block-video figcaption {
    margin-top: var(--space-sm);
    text-align: center;
    font-size: var(--text-xs);
    color: var(--color-muted);
    letter-spacing: 0.05em;
}

/* --- Gallery --- */
/* ----------------------------------------------------------------------
   GRILLE DE COMPOSANTS — LA FORMULE UNIQUE

   Quatre mécanismes cohabitaient pour dire « N colonnes » : des variables CSS
   (--gallery-cols, --stats-cols, --iconlist-cols), des classes en dur
   (block-card-grid--3cols, blog-list--cols-3), la fonction zoneRowGrid() et sa
   copie non migrée dans _generic.php. Plus dix-sept copies de
   `grid-template-columns: 1fr` dans des media queries, à l'identique.

   La meilleure écriture du dépôt était déjà là, dans events.css : N colonnes
   AVEC UN PLANCHER, sans une seule media query. Si N colonnes ne tiennent pas au
   plancher demandé, la grille en met moins — d'elle-même, à toutes les largeurs
   intermédiaires, sans qu'on ait à deviner un seuil.

     --cols     nombre de colonnes voulu sur grand écran
     --col-min  largeur en dessous de laquelle une colonne n'a plus de sens

   `max()` prend le plus grand des deux : la part idéale (100 % moins les gouttières,
   divisé par N) ou le plancher. `auto-fill` place ensuite autant de colonnes qu'il
   en tient. C'est tout, et cela remplace les media queries de quatre composants.
   ---------------------------------------------------------------------- */

.block-cols {
    display: grid;
    gap: var(--col-gap, var(--space-lg));
    grid-template-columns: repeat(auto-fill, minmax(
        max(var(--col-min, 240px),
            calc((100% - (var(--cols, 3) - 1) * var(--col-gap, var(--space-lg))) / var(--cols, 3))), 1fr));
}

.block-gallery {
    --col-gap: var(--space-sm);
    --col-min: 150px;
    margin: var(--block-rhythm, var(--space-lg)) 0;
}
@media (max-width: 640px) {
    /* Sur mobile : 2 colonnes max, quel que soit le réglage. */
    .block-gallery { grid-template-columns: repeat(2, 1fr); }
}
.block-gallery__item {
    margin: 0;
    aspect-ratio: 1;
    overflow: hidden;
    border-radius: var(--radius-sm);
    background: var(--color-surface);
}
.block-gallery__item img {
    width: 100%;
    height: 100%;
    object-fit: cover;
    transition: transform var(--transition-slow);
}
.block-gallery__item:hover img {
    transform: scale(1.04);
}

/* --- Card grid --- */
.block-card-grid {
    --col-min: 260px;
    margin: var(--block-rhythm, var(--space-lg)) 0;
}
/* Trois classes en dur et deux media queries remplacées par une variable : la
   formule de .block-cols dégrade d'elle-même, et à toutes les largeurs, pas
   seulement aux deux seuils qu'on avait devinés. */
.block-card-grid--2cols { --cols: 2; }
.block-card-grid--3cols { --cols: 3; }
.block-card-grid--4cols { --cols: 4; }

.block-card {
    background: var(--color-surface);
    border: 1px solid var(--color-border);
    border-radius: var(--radius-md);
    box-shadow: var(--shadow-sm);
    overflow: hidden;
    text-decoration: none;
    color: var(--color-text);
    display: flex;
    flex-direction: column;
    transition: box-shadow var(--transition-base), transform var(--transition-base), border-color var(--transition-base);
}
a.block-card:hover {
    box-shadow: var(--shadow-md);
    border-color: var(--color-accent);
    transform: translateY(-2px);
}
.block-card__image {
    /* Image (icône ou photo) centrée dans la colonne.
       - plus petite que la colonne -> taille réelle
       - plus grande -> contrainte à la largeur de la colonne */
    display: flex;
    justify-content: center;
    align-items: center;
    padding: var(--space-md);
}
.block-card__image img {
    max-width: 100%;
    width: auto;
    height: auto;
    object-fit: contain;
}
.block-card__body {
    padding: var(--space-md) var(--space-lg);
    flex: 1;
}
.block-card__title {
    font-size: var(--text-lg);
    margin-bottom: var(--space-xs);
}
.block-card__text {
    font-size: var(--text-sm);
    color: var(--color-muted);
    line-height: 1.5;
    margin: 0;
}

/* --- Stats --- */
.block-stats {
    /* 140 et non 160 : mesuré à 380 px, deux chiffres tiennent en 158 px chacun
       et le plancher de 160 les faisait basculer à UNE colonne — un chiffre clé
       est court, il n'a pas besoin de toute la largeur. */
    --col-min: 140px;
    --cols: 4;
    margin: var(--space-xl) 0;
    text-align: center;
}
.block-stats__item {
    display: flex;
    flex-direction: column;
    gap: var(--space-xs);
}
.block-stats__value {
    font-family: var(--font-heading);
    font-size: clamp(2rem, 4vw, 3.5rem);
    font-weight: 600;
    color: var(--color-accent);
    line-height: 1;
}
/* Compteur animé (Mise en forme → Animation). Des chiffres de largeurs
   différentes feraient trembler la mise en page à chaque image : les chiffres
   tabulaires occupent tous la même largeur, le bloc reste immobile. */
.block-stats--anim .block-stats__value {
    font-variant-numeric: tabular-nums;
    font-feature-settings: "tnum";
}
.block-stats__label {
    font-size: var(--text-xs);
    text-transform: uppercase;
    letter-spacing: 0.08em;
    color: var(--color-muted);
}

/* --- Horaires (bloc « hours ») --- */
.block-hours {
    margin: var(--space-lg) 0;
    max-width: 34rem;
    display: flex;
    flex-direction: column;
}
.block-hours__row {
    display: grid;
    grid-template-columns: 1fr auto;               /* jour | plage 1 */
    align-items: baseline;
    gap: var(--space-md) var(--space-lg);
    padding: 0.7rem 0.25rem;
    /* Bordure et teinte dérivées de la couleur du texte → s'adapte à un fond
       clair OU foncé (le composant peut vivre dans n'importe quelle section). */
    border-bottom: 1px solid;
    border-color: color-mix(in srgb, currentColor 18%, transparent);
}
.block-hours--two .block-hours__row {
    grid-template-columns: 1fr auto auto;          /* jour | plage 1 | plage 2 */
}
.block-hours__row:last-child { border-bottom: 0; }
.block-hours__day {
    font-weight: 600;                              /* couleur héritée de la section */
}
.block-hours__slot {
    opacity: 0.8;                                  /* léger contraste, relatif au fond */
    font-variant-numeric: tabular-nums;
    white-space: nowrap;
    text-align: right;
}
@media (max-width: 480px) {
    /* Sur mobile : jour au-dessus, plages en dessous alignées à gauche. */
    .block-hours__row,
    .block-hours--two .block-hours__row {
        grid-template-columns: 1fr;
        gap: 0.15rem;
        padding: 0.6rem 0.25rem;
    }
    .block-hours__slot { text-align: left; }
}

/* --- Icon list --- */
.block-icon-list {
    --col-min: 240px;
    --cols: 3;
    padding: 0;
    margin: var(--block-rhythm, var(--space-lg)) 0;
}

/* ÉCART ENTRE LES ENTRÉES (réglage du composant). `row-gap` seulement, et non
   `gap` : l'écart entre COLONNES d'une liste à deux ou trois colonnes n'a rien
   à voir avec celui qui sépare deux entrées, et le resserrer collerait les
   colonnes. Le socle .block-cols garde donc la main sur l'horizontale. */
.block-icon-list--serre { row-gap: 0.6rem; }
.block-icon-list--large { row-gap: 3rem; }
.block-icon-list__item {
    display: flex;
    gap: var(--space-md);
    align-items: flex-start;
}
.block-icon-list__icon {
    font-size: 1.75em;
    line-height: 1;
    flex-shrink: 0;
    color: var(--color-accent);
}
.block-icon-list__body {
    display: flex;
    flex-direction: column;
    gap: var(--space-xs);
}
.block-icon-list__body strong {
    font-family: var(--font-heading);
    font-weight: 500;
    font-size: var(--text-base);
}
.block-icon-list__body span {
    font-size: var(--text-sm);
    color: var(--color-muted);
    line-height: 1.5;
}
/* Icône choisie dans la bibliothèque (SVG) : héritée en couleur d'accent. */
.block-icon-list__svg {
    display: block;
    color: var(--color-accent);
}
/* Badge facultatif accolé au titre (ex. « bientôt »). */
.block-icon-list__badge {
    display: inline-block;
    vertical-align: middle;
    font-family: var(--font-body);
    font-size: 0.62em;
    font-weight: 700;
    text-transform: uppercase;
    letter-spacing: 0.05em;
    line-height: 1;
    padding: 0.28em 0.6em;
    border-radius: 99px;
    background: var(--color-accent);
    color: #fff;
    margin-left: 0.4em;
}

/* --- Accordion --- */
.block-accordion {
    margin: var(--block-rhythm, var(--space-lg)) 0;
    border-top: 1px solid var(--color-border);
}
.block-accordion__item {
    border-bottom: 1px solid var(--color-border);
}
.block-accordion__q {
    cursor: pointer;
    padding: var(--space-md) 0;
    font-family: var(--font-heading);
    font-weight: 500;
    font-size: var(--text-base);
    list-style: none;
    position: relative;
    padding-right: 2em;
    transition: color var(--transition-fast);
}
.block-accordion__q::-webkit-details-marker { display: none; }
.block-accordion__q::after {
    content: "+";
    position: absolute;
    right: 0;
    top: 50%;
    transform: translateY(-50%);
    font-size: 1.4em;
    color: var(--color-accent);
    transition: transform var(--transition-fast);
}
.block-accordion__item[open] .block-accordion__q::after {
    content: "−";
}
.block-accordion__q:hover {
    color: var(--color-accent);
}
.block-accordion__a {
    padding: 0 0 var(--space-md);
    color: var(--color-muted);
    line-height: 1.6;
}

/* --- Button block alignment --- */
.btn-pdf { margin: var(--space-md) 0; }

/* --- Downloads (kit logo) --- */
/* ------------------------------------------------------------------
   GALERIE EN MODE « FICHIERS À TÉLÉCHARGER »
   Ces règles viennent du composant « Téléchargements », retiré le 2026-09-08
   et absorbé par la Galerie (migration 0015). Elles ne s'appliquent QUE si
   l'exploitant a activé une option — bouton, informations ou fond d'aperçu ;
   une galerie ordinaire ne les voit jamais.
   ------------------------------------------------------------------ */
.block-gallery-groups {
    margin: var(--block-rhythm, var(--space-lg)) 0;
    display: flex;
    flex-direction: column;
    gap: var(--space-xl, 3rem);
}
.block-gallery__group-title {
    margin: 0 0 var(--space-md);
    padding-bottom: var(--space-xs, 0.5rem);
    border-bottom: 1px solid var(--color-border);
    font-size: 1.05rem;
    letter-spacing: 0.02em;
    color: var(--color-muted);
}
.block-gallery__item--file {
    margin: 0;
    display: flex;
    flex-direction: column;
    background: var(--color-surface);
    border: 1px solid var(--color-border);
    border-radius: var(--radius-md);
    box-shadow: var(--shadow-sm);
    overflow: hidden;
    transition: box-shadow var(--transition-base), transform var(--transition-base);
}
.block-gallery__item--file:hover {
    box-shadow: var(--shadow-md);
    transform: translateY(-2px);
}
.block-gallery__preview {
    display: flex;
    align-items: center;
    justify-content: center;
    padding: var(--space-md);
    min-height: 130px;
}
/* Damier discret : rend visible la transparence des PNG. */
.block-gallery__preview--light {
    background-color: #fff;
    background-image:
        linear-gradient(45deg, rgba(0,0,0,0.05) 25%, transparent 25%, transparent 75%, rgba(0,0,0,0.05) 75%),
        linear-gradient(45deg, rgba(0,0,0,0.05) 25%, transparent 25%, transparent 75%, rgba(0,0,0,0.05) 75%);
    background-size: 18px 18px;
    background-position: 0 0, 9px 9px;
}
/* Encre blanche : fond sombre, sans quoi le fichier est invisible. */
.block-gallery__preview--dark {
    background-color: #1f1f1f;
    background-image:
        linear-gradient(45deg, rgba(255,255,255,0.06) 25%, transparent 25%, transparent 75%, rgba(255,255,255,0.06) 75%),
        linear-gradient(45deg, rgba(255,255,255,0.06) 25%, transparent 25%, transparent 75%, rgba(255,255,255,0.06) 75%);
    background-size: 18px 18px;
    background-position: 0 0, 9px 9px;
}
.block-gallery__preview img {
    max-width: 100%;
    max-height: 110px;
    height: auto;
    object-fit: contain;
}
.block-gallery__caption {
    padding: var(--space-sm) var(--space-md) 0;
    font-weight: 600;
    font-size: 0.95rem;
    color: var(--color-text);
}
/* Format · dimensions · poids : une information de service, discrète. */
.block-gallery__info {
    margin: 2px 0 0;
    padding: 0 var(--space-md);
    font-size: 0.85rem;
    color: var(--color-muted);
}
.block-gallery__btn {
    margin: var(--space-sm) var(--space-md) var(--space-md);
    align-self: flex-start;
}

/* ============================================================
   MISE EN FORME PAR BLOC
   Conteneur .block-fx (align / largeur / espacement) + modificateurs
   par composant. Piloté par blocks.settings — voir includes/block-types.php.
   Le conteneur n'existe dans le DOM que si un réglage transversal est actif :
   sans réglage, le rendu est identique à l'existant.
   ============================================================ */

/* --- Alignement --- */
.block-fx--align-left   { text-align: left; }
/* ----------------------------------------------------------
   TRAITEMENT DES IMAGES D'UN COMPOSANT (réglage `img_filter`)
   ----------------------------------------------------------
   Une seule paire de règles pour les cinq composants qui affichent des images
   (galerie, grille de cartes, image, articles du blog, projets) : la classe est
   posée par blockFxWrapperAttrs(), et le filtre descend sur les `img`.

   ⚠️  LE SURVOL EST RÉSERVÉ AUX POINTEURS. `(hover: hover) and (pointer: fine)`
   écarte les écrans tactiles, où un « survol » se produirait au toucher — et
   resterait collé après le doigt, sans moyen d'en sortir.

   ET SUR CES ÉCRANS-LÀ, LES IMAGES RESTENT EN COULEUR. Un gris que rien ne peut
   révéler n'est plus un effet, c'est une perte : le visiteur au doigt n'aurait
   jamais vu les images telles qu'elles sont. Qui veut du gris PARTOUT choisit
   « Noir et blanc » — c'est à cela que sert la première option.
   ---------------------------------------------------------- */
.block-fx--img-gris img {
    filter: grayscale(1);
}

/* LOGOS. Même gris que ci-dessus en thème clair ; en thème SOMBRE, les logos
   noirs — Samsung, Sonos, Synology… — disparaîtraient purement et simplement.
   `brightness(0)` les aplatit en noir puis `invert(1)` les rend blancs : une
   silhouette claire, lisible, la même pour toute la série. La transparence des
   PNG est préservée ; un logo posé sur un fond opaque deviendrait en revanche
   un rectangle blanc — c'est la limite, et elle tient au fichier, pas à la règle.

   Le survol rend la couleur en clair ; en sombre il ne fait que gagner en
   présence : un logo noir retrouvant son noir sur un fond sombre s'effacerait
   au moment même où l'utilisateur le vise. */
.block-fx--img-logos img {
    filter: grayscale(1);
    opacity: 0.65;
    transition: opacity var(--transition-base, 0.25s ease),
                filter var(--transition-base, 0.25s ease);
}
[data-theme="dark"] .block-fx--img-logos img {
    filter: brightness(0) invert(1);
    opacity: 0.7;
}

@media (hover: hover) and (pointer: fine) {
    .block-fx--img-logos a:hover img,
    .block-fx--img-logos a:focus-visible img,
    .block-fx--img-logos img:hover {
        filter: none;
        opacity: 1;
    }
    [data-theme="dark"] .block-fx--img-logos a:hover img,
    [data-theme="dark"] .block-fx--img-logos a:focus-visible img,
    [data-theme="dark"] .block-fx--img-logos img:hover {
        filter: brightness(0) invert(1);
        opacity: 1;
    }
}

@media (hover: hover) and (pointer: fine) {
    .block-fx--img-gris-survol img {
        filter: grayscale(1);
        transition: filter var(--transition-base, 0.25s ease);
    }
    /* La couleur revient sur l'image visée — qu'elle soit dans un lien (galerie,
       carte, article) ou nue (composant Image). `:focus-visible` la rend aussi
       au parcours clavier : sans lui, l'effet n'existerait qu'à la souris. */
    .block-fx--img-gris-survol a:hover img,
    .block-fx--img-gris-survol a:focus-visible img,
    .block-fx--img-gris-survol img:hover {
        filter: none;
    }
}

.block-fx--align-center { text-align: center; }
.block-fx--align-right  { text-align: right; }

/* --- Largeur de lecture (centrée) --- */
.block-fx--w-narrow { max-width: 640px; }
.block-fx--w-medium { max-width: 820px; }
.block-fx--w-wide   { max-width: 1040px; }
.block-fx--w-narrow,
.block-fx--w-medium,
.block-fx--w-wide { margin-inline: auto; }


/* --- Image alignée ---
   DEUX BOÎTES À DÉPLACER, PAS UNE. L'enveloppe `.block-image` d'abord — utile
   quand une taille (sm/md/lg) la contraint. Mais SANS taille, l'enveloppe
   occupe toute la colonne et son `margin: auto` ne déplace rien : c'est alors
   l'IMAGE qui doit se centrer dans l'enveloppe.

   Or `text-align` ne peut pas le faire. L'image est en `display: block` (reset
   du socle) : une boîte de bloc ignore l'alignement de texte de son parent et
   se colle au bord d'attaque. Il lui faut ses propres marges automatiques.

   Le défaut ne se voyait pas sur grand écran, où les images de presse
   remplissent leur tiers de colonne. Il apparaissait dès que les colonnes
   s'empilent : mesuré à 880 px, une image de 560 px dans un bloc de 816 px
   restait collée à gauche, à 128 px de l'axe — alors que le composant était
   bien réglé sur « centré ». */
.block-fx--align-left   > .block-image { margin-right: auto; margin-left: 0; }
.block-fx--align-center > .block-image { margin-inline: auto; }
.block-fx--align-right  > .block-image { margin-left: auto; margin-right: 0; }

.block-fx--align-left   > .block-image img { margin-right: auto; margin-left: 0; }
.block-fx--align-center > .block-image img { margin-inline: auto; }
.block-fx--align-right  > .block-image img { margin-left: auto; margin-right: 0; }

/* ==========================================================================
   LÉGENDE D'UNE IMAGE — quatre positions, un seul jeu de règles
   ==========================================================================
   Partagé par le composant Image (`.block-image`) et la Galerie
   (`.block-gallery__item`). Sans classe, la légende reste SOUS le média :
   c'est le comportement de toujours, et aucun contenu existant ne bouge.

   POSÉE SUR L'IMAGE, la figure doit se rétrécir à la largeur de l'image —
   sans quoi la bande dépasserait de chaque côté d'une image plus étroite que
   sa colonne. `fit-content` s'en charge, et les marges automatiques de
   l'alignement continuent de la placer.
   ========================================================================== */
.block-cap--band,
.block-cap--band-light,
.block-cap--center,
.block-cap--hover {
    position: relative;
    width: fit-content;
    max-width: 100%;
    overflow: hidden;
}
/* Une VIDÉO garde sa largeur : son cadre est déjà dimensionné par le format
   choisi (16/9, 4/3…), et le rétrécir au contenu le ferait retomber sur la
   largeur intrinsèque de l'affiche. */
.block-video.block-cap--band,
.block-video.block-cap--band-light,
.block-video.block-cap--center,
.block-video.block-cap--hover { width: auto; }

/* Pleine largeur demandée : le débordement l'emporte sur le rétrécissement. */
.block-image--bleed.block-cap--band,
.block-image--bleed.block-cap--band-light,
.block-image--bleed.block-cap--center,
.block-image--bleed.block-cap--hover { width: 100%; }

.block-cap--band > .block-image__caption,
.block-cap--band-light > .block-image__caption,
.block-cap--band .block-gallery__caption,
.block-cap--band-light .block-gallery__caption,
.block-video.block-cap--band > .block-image__caption,
.block-video.block-cap--band-light > .block-image__caption {
    position: absolute;
    left: 0;
    right: 0;
    bottom: 0;
    margin: 0;
    padding: var(--space-sm) var(--space-md);
    font-size: 0.95rem;
    line-height: 1.4;
}
.block-cap--band > .block-image__caption,
.block-cap--band .block-gallery__caption {
    background: rgba(17, 17, 17, 0.9);
    color: #fff;
}
.block-cap--band-light > .block-image__caption,
.block-cap--band-light .block-gallery__caption {
    background: rgba(255, 255, 255, 0.9);
    color: #111;
}
/* Les liens d'une légende posée sur l'image héritent de son encre : l'accent
   du site n'est pas garanti lisible sur un aplat noir à 90 %. */
.block-cap--band .block-image__caption a,
.block-cap--band-light .block-image__caption a { color: inherit; }

/* AU CENTRE, EN GRAND — une image d'illustration qui porte son titre. */
.block-cap--center > .block-image__caption,
.block-cap--center .block-gallery__caption {
    position: absolute;
    inset: 0;
    margin: 0;
    display: flex;
    align-items: center;
    justify-content: center;
    text-align: center;
    padding: var(--space-md);
    background: rgba(17, 17, 17, 0.45);
    color: #fff;
    font-family: var(--font-heading);
    font-size: clamp(1.25rem, 0.9rem + 1.6vw, 2rem);
    line-height: 1.2;
}
.block-cap--center .block-image__caption a { color: inherit; }

/* AU SURVOL — la bande sombre, révélée. ⚠️ Un écran tactile n'a pas de survol :
   la légende y serait définitivement invisible. On la laisse donc VISIBLE dès
   qu'aucun dispositif ne sait survoler, plutôt que de la perdre. Le libellé du
   réglage annonce déjà la limite ; la CSS en limite les dégâts. */
.block-cap--hover > .block-image__caption,
.block-cap--hover .block-gallery__caption {
    position: absolute;
    left: 0;
    right: 0;
    bottom: 0;
    margin: 0;
    padding: var(--space-sm) var(--space-md);
    background: rgba(17, 17, 17, 0.9);
    color: #fff;
    font-size: 0.95rem;
    opacity: 0;
    transform: translateY(100%);
    transition: opacity var(--transition-base), transform var(--transition-base);
}
.block-cap--hover:hover > .block-image__caption,
.block-cap--hover:focus-within > .block-image__caption,
.block-cap--hover:hover .block-gallery__caption {
    opacity: 1;
    transform: none;
}
@media (hover: none) {
    .block-cap--hover > .block-image__caption,
    .block-cap--hover .block-gallery__caption { opacity: 1; transform: none; }
}

/* Légende SOUS l'image (défaut) : même style que celle de la Galerie. */
.block-image__caption {
    padding: var(--space-sm) 0 0;
    font-size: 0.95rem;
    color: var(--color-text);
}
/* UNE LÉGENDE EST COMPACTE. Ses paragraphes héritaient de la marge du corps —
   16 px —, pensée pour un texte courant, pas pour deux lignes serrées dans une
   bande de 40 px de haut. Le blanc y était plus grand que l'interligne, et les
   deux lignes ne se lisaient plus comme un bloc.
   Relevé par Patrick, qui a d'abord cru à deux composants distincts. */
.block-image__caption p { margin-bottom: 0.2em; }
.block-image__caption p:last-child { margin-bottom: 0; }
.block-image__link { display: block; }

/* LIEN ÉTENDU À TOUTE LA FIGURE, LÉGENDE COMPRISE.
   L'ancre n'entoure que l'image — un <figcaption> doit rester enfant direct de
   sa <figure> — mais sa zone cliquable, elle, couvre tout. Une nappe absolue
   plutôt qu'un balisage tordu.

   ⚠️ LES LIENS DE LA LÉGENDE PASSENT DEVANT. Sans ce `z-index`, la nappe les
   recouvrirait et le clic irait toujours au lien de l'image : l'exploitant
   verrait deux liens et n'en obtiendrait qu'un. */
.block-image--lien { position: relative; }
.block-image--lien .block-image__link::after {
    content: '';
    position: absolute;
    inset: 0;
}
/* ⚠️ LA NAPPE NE SUFFIT PAS, et l'essai au clic l'a montré. En mode bande ou
   au centre, le <figcaption> est LUI AUSSI positionné, et il suit l'ancre dans
   le document : il peint donc PAR-DESSUS la nappe, et cliquer la légende ne
   suivait pas le lien. Mesuré avec elementFromPoint() au centre de la bande —
   il rendait le FIGCAPTION, pas l'ancre.

   La légende laisse donc passer le clic. Ses propres liens, eux, le reprennent
   et restent cliquables : c'est ce couple `none` / `auto` qui permet à deux
   destinations de cohabiter sur une même image. */
.block-image--lien .block-image__caption { pointer-events: none; }
.block-image--lien .block-image__caption a {
    position: relative;
    z-index: 1;
    pointer-events: auto;
}
/* La figure entière se signale comme cliquable, pas seulement la photo. */
.block-image--lien { cursor: pointer; }

/* --- Image : tailles --- */
.block-image--sm { max-width: 260px; }
.block-image--md { max-width: 460px; }
.block-image--lg { max-width: 720px; }

/* --- Citation : styles --- */
.block-quote--plain {
    text-align: left;
    max-width: none;
    padding-inline: 0 0;
    padding-left: var(--space-lg);
    border-left: 3px solid var(--color-accent);
    margin-inline: 0;
}
.block-quote--plain blockquote::before { display: none; }
.block-quote--boxed {
    background: var(--color-surface);
    border: 1px solid var(--color-border);
    border-radius: var(--radius-md);
}

/* --- Call-to-action : fonds --- */
.block-cta--accent {
    background: var(--color-surface); /* repli si color-mix non supporté */
    background: color-mix(in srgb, var(--color-accent) 12%, var(--color-surface));
    border-color: var(--color-accent);
    border-color: color-mix(in srgb, var(--color-accent) 30%, var(--color-border));
}
.block-cta--plain {
    background: transparent;
    border-color: transparent;
    padding-inline: 0;
}

/* Colonnes imposées par le réglage « Mise en forme » du composant. Deux blocs
   @media identiques, à six lignes d'écart, ont disparu : la formule partagée
   descend le nombre de colonnes toute seule quand la place manque. */
.block-icon-list--cols { --cols: var(--iconlist-cols, 2); }
.block-stats--cols     { --cols: var(--stats-cols, 3); }

/* --- Fil d'ariane (élément global, rendu par includes/pages.php) ---
   Rendu DANS #main-content : il hérite ainsi du décalage du menu latéral
   (voir [data-menu-position]) au lieu de passer dessous. */
.breadcrumb-bar {
    padding-block: var(--space-md, 1.5rem);
    font-size: 0.875rem;
    color: var(--color-muted);
}

/* En premier enfant de #main-content, c'est LUI qui dégage la hauteur de
   l'en-tête fixe (--header-h) : la règle `.section:first-child` de layout.css ne
   s'applique alors plus à la première section, qui reprend son espacement
   normal. Sur une page qui ouvre par un visuel plein cadre, le fil est rendu
   APRÈS ce visuel : il n'est plus :first-child et n'a rien à dégager. */
#main-content > .breadcrumb-bar:first-child {
    padding-top: calc(var(--header-h, 88px) + var(--space-lg, 2rem));
}
/* Variante « barre » : le dégagement passe en MARGE, sinon la bande teintée
   ferait un bandeau de la hauteur de l'en-tête. */
#main-content > .breadcrumb-bar--boxed:first-child {
    margin-top: calc(var(--header-h, 88px) + var(--space-lg, 2rem));
    padding-top: var(--space-md, 1rem);
}
/* La section qui suit n'a plus besoin de son grand espacement de tête. */
.breadcrumb-bar + .section { padding-top: var(--space-xl, 3rem); }

.breadcrumb-bar--boxed {
    background-color: var(--color-surface);
    border-bottom: 1px solid var(--color-border);
}

.breadcrumb-bar .container {
    display: flex;
    align-items: center;
    flex-wrap: wrap;
    gap: var(--space-xs, 0.5rem) var(--space-sm, 0.75rem);
}

.breadcrumb__list {
    display: flex;
    flex-wrap: wrap;
    align-items: center;
    gap: var(--space-xs, 0.5rem);
    margin: 0;
    padding: 0;
    list-style: none;
}

.breadcrumb__item {
    display: inline-flex;
    align-items: center;
    gap: var(--space-xs, 0.5rem);
}

/* Séparateur purement décoratif (aria-hidden côté HTML). */
.breadcrumb__sep {
    color: var(--color-muted);
    opacity: 0.6;
}

.breadcrumb__link {
    color: inherit;
    text-decoration: none;
    border-bottom: 1px solid transparent;
    transition: color var(--transition-base), border-color var(--transition-base);
}
.breadcrumb__link:hover,
.breadcrumb__link:focus-visible {
    color: var(--color-link, var(--color-accent));
    border-bottom-color: currentColor;
}

/* Page courante : non cliquable, légèrement appuyée. */
.breadcrumb__current {
    color: var(--color-text);
    font-weight: 600;
}

.breadcrumb__home {
    display: inline-flex;
    align-items: center;
}
.breadcrumb__home svg { display: block; }

@media (max-width: 640px) {
    .breadcrumb-bar { font-size: 0.8125rem; }
}

/* Bouton d'action, coordonnées, réseaux et texte libre de l'en-tête.
   Volontairement discrets : ces éléments cohabitent avec le menu dans une
   bande de quelques dizaines de pixels. */
.site-header__cta { white-space: nowrap; }

.site-header__contact,
.site-header__social {
    display: flex;
    align-items: center;
    gap: var(--space-sm);
}
.site-header__contact-link,
.site-header__social-link {
    display: inline-flex;
    align-items: center;
    font-size: var(--text-xs);
    color: var(--color-text);
    text-decoration: none;
    transition: color var(--transition-fast);
}
.site-header__contact-link:hover,
.site-header__social-link:hover { color: var(--color-accent); }
.site-header__social-link svg { width: 18px; height: 18px; }

.site-header__text {
    font-size: var(--text-sm);
    color: var(--color-muted);
    line-height: 1.3;
}
.site-header__text--small { font-size: var(--text-xs); }

/* Au-dessus d'un hero photo, l'en-tête transparent passe tout en blanc :
   ces éléments doivent suivre, sinon ils disparaissent sur l'image. */
body.has-photo-hero .site-header:not(.is-scrolled):not(.site-header--static) .site-header__contact-link,
body.has-photo-hero .site-header:not(.is-scrolled):not(.site-header--static) .site-header__social-link,
body.has-photo-hero .site-header:not(.is-scrolled):not(.site-header--static) .site-header__text { color: #fff; }

/* ----------------------------------------------------------
   Header — comportement des éléments sur téléphone
   Chaque élément porte l'un de ces trois états, choisi dans l'éditeur
   d'en-tête. Voir headerItemMobileClass() (includes/header-layout.php).
   ---------------------------------------------------------- */
@media (max-width: 899.98px) {
    /* « Masqué sur téléphone » */
    .site-header .mp-hd-hidden { display: none; }

    /* « Dans la barre » : l'élément reste visible à côté du bouton de menu.
       Il ne doit ni s'étirer ni pousser ce bouton hors de l'écran — un texte
       libre un peu long, ou un bouton d'action au libellé bavard, le feraient
       sans ces deux garde-fous. */
    .site-header .mp-hd-bar {
        flex: 0 1 auto;
        min-width: 0;
    }
    .site-header .mp-hd-bar > * {
        overflow: hidden;
        text-overflow: ellipsis;
        white-space: nowrap;
    }

    /* « Dans le menu déroulant », pour un élément qui n'a pas pu être écrit
       DANS le volet — il est dans une autre colonne, et le volet est en
       position fixe : aucune règle CSS ne peut l'y faire entrer. C'est
       main.js qui l'y déplace. Tant qu'il ne l'a pas fait (script désactivé,
       page en cours de chargement), l'élément reste visible dans la barre :
       masqué, il deviendrait inaccessible, ce qui est bien pire que déplacé. */
    .site-header .mp-hd-panel { max-width: 100%; }

    /* Sur téléphone, une ligne redevient une BARRE : logo à gauche, bouton de
       menu à droite. Empiler les colonnes les unes sous les autres mettrait le
       logo et le burger sur deux étages, et l'en-tête mangerait le tiers de
       l'écran. La grille cède donc la place à une simple rangée — ce qui reste
       visible y tient largement, puisque le reste est parti dans le volet. */
    .site-header__cols {
        display: flex;
        align-items: center;
        justify-content: space-between;
        gap: var(--space-sm);
        min-height: 0;
    }
    .site-header__col { flex: 0 1 auto; justify-content: flex-start; }
    /* La dernière colonne pousse le bouton de menu contre le bord droit. */
    .site-header__cols > .site-header__col:last-child { margin-left: auto; }
    /* Une colonne vidée par le passage en mobile ne doit pas occuper d'espace. */
    .site-header__col:empty { display: none; }

    /* Une ligne dont il ne reste rien de visible ne doit pas occuper de place
       ni afficher son fond — sinon une barre de service entièrement masquée
       laisserait un bandeau vide en haut de chaque page. */
    .site-header__row:not(:has(.site-header__item:not(.mp-hd-hidden))):not(:has(.main-nav)) {
        display: none;
    }
}

/* Éléments déplacés dans le volet par main.js : ils s'y empilent comme le
   reste, sans reprendre la disposition horizontale qu'ils avaient en barre. */
.main-nav > .site-header__item {
    width: 100%;
    justify-content: flex-start;
}

/* ============================================================
   Bandeau d'information « cookies »
   ------------------------------------------------------------
   Sobre et discret : le site ne dépose qu'un cookie technique, le bandeau
   informe, il ne bloque pas la lecture (pas de voile, pas de fenêtre modale).
   Réglé dans l'admin (Réglages → Confidentialité) ; affiché par
   js/cookie-notice.js, jamais avant d'avoir vérifié qu'il n'a pas déjà été
   fermé — d'où l'état masqué par défaut.
   ============================================================ */
.cookie-notice {
    position: fixed;
    z-index: 900;                 /* au-dessus du contenu, sous l'intro (1000+) */
    background: var(--color-surface);
    color: var(--color-text);
    border: 1px solid var(--color-border);
    border-radius: var(--radius-md);
    box-shadow: var(--shadow-lg);
    /* État d'entrée : le retrait est repris par .is-visible ci-dessous. */
    opacity: 0;
    transform: translateY(1rem);
    transition: opacity var(--transition-base), transform var(--transition-base);
}
.cookie-notice.is-visible {
    opacity: 1;
    transform: translateY(0);
}

/* Variante « carte flottante » (défaut) : en bas à gauche, largeur limitée. */
.cookie-notice--card {
    left: var(--space-md);
    bottom: var(--space-md);
    max-width: 26rem;
}

/* Variante « barre » : pleine largeur, collée au bas de la fenêtre. */
.cookie-notice--bar {
    left: 0;
    right: 0;
    bottom: 0;
    max-width: none;
    border-width: 1px 0 0;
    border-radius: 0;
}

.cookie-notice__inner {
    display: flex;
    align-items: center;
    gap: var(--space-md);
    padding: var(--space-md);
}
/* En barre, le contenu suit la largeur de lecture du site (--site-width,
   pilotée par la Charte graphique — même variable que .container). */
.cookie-notice--bar .cookie-notice__inner {
    max-width: var(--site-width, 1200px);
    margin: 0 auto;
    padding: var(--space-md) var(--space-lg);
}
/* En carte, tout s'empile : la largeur ne permet pas le bouton à côté. */
.cookie-notice--card .cookie-notice__inner {
    flex-direction: column;
    align-items: flex-start;
}

.cookie-notice__text { flex: 1 1 auto; }

.cookie-notice__title {
    font-family: var(--font-heading);
    font-size: var(--text-base);
    font-weight: 600;
    margin: 0 0 0.25em;
}
.cookie-notice__body {
    font-size: var(--text-sm);
    line-height: 1.5;
    color: var(--color-muted);
    margin: 0;
}
/* Titre et message sortent d'un éditeur riche : leur contenu peut arriver
   enveloppé dans des <p> (ou porter des listes). On neutralise les marges
   extérieures pour que le bandeau garde exactement la même allure qu'avec du
   texte nu, et on laisse un petit interligne ENTRE paragraphes. */
.cookie-notice__title > :first-child,
.cookie-notice__body  > :first-child { margin-top: 0; }
.cookie-notice__title > :last-child,
.cookie-notice__body  > :last-child  { margin-bottom: 0; }
.cookie-notice__body p { margin: 0 0 0.5em; }
.cookie-notice__body ul,
.cookie-notice__body ol { margin: 0 0 0.5em; padding-left: 1.25em; }
/* Un lien saisi DANS le message reste discret : c'est le lien « en savoir
   plus » qui porte l'appel à l'action. */
.cookie-notice__body a { color: inherit; text-decoration: underline; }

.cookie-notice__link {
    color: var(--color-link, var(--color-accent));
    text-decoration: underline;
    white-space: nowrap;
}

.cookie-notice__accept {
    flex: 0 0 auto;
    padding: 0.7em 1.6em;
    font-size: var(--text-xs);
}

@media (max-width: 640px) {
    /* Sur téléphone, la carte occupe la largeur disponible : une carte étroite
       dans un coin y devient illisible. */
    .cookie-notice--card {
        left: var(--space-sm);
        right: var(--space-sm);
        bottom: var(--space-sm);
        max-width: none;
    }
    .cookie-notice--bar .cookie-notice__inner { padding: var(--space-md); }
    .cookie-notice__inner { flex-direction: column; align-items: flex-start; }
    .cookie-notice__accept { width: 100%; justify-content: center; }
}

/* Animation d'entrée retirée pour qui la refuse (préférence système). */
@media (prefers-reduced-motion: reduce) {
    .cookie-notice { transition: none; transform: none; }
}


/* ==========================================================================
   TABLEAUX DE CONTENU
   ==========================================================================

   POURQUOI DANS LE CŒUR
   allowedHtmlTags() autorise <table>, <thead>, <tr>, <th scope>, <td colspan>
   et <caption> dans TOUT contenu enrichi. N'importe quel site Artaweb peut
   donc produire un tableau depuis un composant Texte — et jusqu'ici il sortait
   sans bordure ni marge. Ce n'était pas un besoin d'un site en particulier,
   c'était un trou du cœur.

   CE QUI EST VISÉ, ET CE QUI NE L'EST PAS
   `table:not([class])` ne touche QUE les tableaux écrits dans du contenu, qui
   n'ont jamais de classe. Les tableaux du CMS portent la leur et gardent leur
   habillage : .events-cal__table (calendrier) et .admin-table (back-office,
   qui vit de toute façon dans admin.css, jamais chargée ici). `.block-table`
   est réservée au futur composant « Tableau » : il héritera de tout ceci sans
   qu'une ligne soit à réécrire.

   TOUT VIENT DE LA CHARTE
   Aucune couleur en dur : la teinte des lignes paires alterne --color-bg et
   --color-surface, deux jetons qui diffèrent légèrement dans les deux thèmes.
   Le tableau suit donc la palette du client et bascule seul en mode sombre,
   sans une seule règle @media prefers-color-scheme.

   DÉBORDEMENT SANS CONTENEUR
   Un tableau écrit dans le WYSIWYG n'a pas de div autour de lui : il doit donc
   savoir déborder tout seul. `display: block` + `overflow-x: auto` sur le
   <table> lui-même le rend défilant sans casser la mise en page interne des
   lignes. C'est le corps de la page qui ne doit JAMAIS défiler latéralement.
   ---------------------------------------------------------------------- */

/* ----------------------------------------------------------------------
   FORMAT DES VIGNETTES — réglage partagé Galerie / Grille de cartes

   La Galerie imposait le carré ; la Grille de cartes ne cadrait pas du tout.
   Le même vocabulaire que le composant « À l'affiche » de l'agenda, pour que
   l'exploitant n'ait pas deux grammaires à retenir. Par défaut (aucune classe),
   chaque composant garde ce qu'il faisait avant.
   ---------------------------------------------------------------------- */

.block-ratio--free  .block-gallery__item,
.block-ratio--free  .block-card__image { aspect-ratio: auto; }
.block-ratio--4-3   .block-gallery__item,
.block-ratio--4-3   .block-card__image { aspect-ratio: 4 / 3; }
.block-ratio--16-9  .block-gallery__item,
.block-ratio--16-9  .block-card__image { aspect-ratio: 16 / 9; }
.block-ratio--3-4   .block-gallery__item,
.block-ratio--3-4   .block-card__image { aspect-ratio: 3 / 4; }
.block-ratio--2-3   .block-gallery__item,
.block-ratio--2-3   .block-card__image { aspect-ratio: 2 / 3; }

.block-ratio--free .block-gallery__item img,
.block-ratio--free .block-card__image img { height: auto; object-fit: contain; }
[class*="block-ratio--"] .block-card__image { overflow: hidden; }
[class*="block-ratio--"] .block-card__image img { width: 100%; height: 100%; object-fit: cover; }

/* ----------------------------------------------------------------------
   SÉPARATEUR — largeur et épaisseur réglables
   ---------------------------------------------------------------------- */

.block-divider--half   { width: 50%; }
.block-divider--full   { width: 100%; }
/* ÉPAISSEUR — sur `height`, pas sur la bordure. Le trait n'est pas une bordure :
   `.block-divider` pose `border: 0` et dessine le filet avec `height: 1px` et un
   fond. `border-top-width` ne pouvait donc rien épaissir, et le menu
   « Épaisseur » tournait à vide, sans que rien ne le dise. */
.block-divider--medium { height: 2px; }
.block-divider--thick  { height: 4px; }
/* ALIGNEMENT — l'option `align` s'applique par une classe d'enveloppe qui pose
   `text-align`. Sur un filet en `margin: … auto`, `text-align` ne peut rien : la
   marge automatique le recentre quoi qu'il arrive. Il faut donc agir sur la
   marge, exactement comme le fait déjà `.block-image` (voir plus bas). */
.block-fx--align-left   > .block-divider { margin-inline: 0 auto; }
.block-fx--align-center > .block-divider { margin-inline: auto; }
.block-fx--align-right  > .block-divider { margin-inline: auto 0; }

/* ----------------------------------------------------------------------
   LISTE À ICÔNES — lien facultatif par élément
   ---------------------------------------------------------------------- */

.block-icon-list__link {
    color: inherit;
    text-decoration: none;
    border-bottom: 1px solid var(--color-accent);
}
.block-icon-list__link:hover,
.block-icon-list__link:focus-visible { color: var(--color-accent); }

/* ----------------------------------------------------------------------
   FOCUS DES COMPOSANTS DE CONTENU

   Sur les seize règles :focus-visible du site, aucune ne visait un élément
   interactif de composant : l'accordéon (un <summary>) et la carte cliquable
   retombaient sur l'anneau du navigateur, que rien n'accorde à la charte.
   (Les commandes de carrousel sont traitées avec le composant, plus bas.)
   ---------------------------------------------------------------------- */

.block-accordion__q:focus-visible,
a.block-card:focus-visible {
    outline: 2px solid var(--color-accent);
    outline-offset: 2px;
    border-radius: var(--radius-sm);
}

/* ----------------------------------------------------------------------
   GRILLE DE CARTES — pied aligné

   Même raison que pour les cartes du blog : à hauteurs égales et titres
   inégaux, le corps des cartes flottait. Le corps prend la place restante,
   ce qui aligne ce qui suit sur toute la rangée.
   ---------------------------------------------------------------------- */

.block-card__body {
    display: flex;
    flex-direction: column;
    flex: 1 1 auto;
}

/* ----------------------------------------------------------------------
   CARROUSEL — composant partagé

   Le Slider et les Témoignages avaient chacun leur jeu de règles, jumelles à la
   couleur et à 1 px près (points de 12 px d'un côté, 11 de l'autre). Elles sont
   ici, une fois. Chaque composant ne garde que ce qui le distingue vraiment : la
   hauteur du hero, la couleur de l'encre sur photo, la place des commandes.

   CIBLES TACTILES. Les points faisaient 11 à 12 px, alors que l'agenda s'impose
   44 px partout. Le bouton mesure donc 44 × 44 — la pastille visible, elle, reste
   petite : c'est un ::after centré. On agrandit la zone tactile sans alourdir le
   dessin.

   FOCUS. Aucune règle :focus-visible du site ne visait un élément de composant ; ces
   boutons retombaient sur l'anneau du navigateur, sur une photo, sans garantie de
   contraste. `currentColor` suit l'encre du composant — blanche sur le slider, accent
   sur les témoignages.
   ---------------------------------------------------------------------- */

.carousel { position: relative; }

.carousel__track { display: grid; }

.carousel__item {
    grid-area: 1 / 1;
    opacity: 0;
    visibility: hidden;
    transition: opacity .6s ease, visibility .6s;
}
.carousel__item.is-active { opacity: 1; visibility: visible; }

/* GALERIE EN CARROUSEL. Le socle `.carousel` empile ses éléments dans une
   grille 1×1 : la hauteur vient donc de la plus haute image. Deux photos de
   produit au même format s'accordent d'elles-mêmes ; si elles diffèrent, le
   cadre prend la plus haute et les autres s'y centrent, plutôt que de faire
   sauter la page à chaque changement de slide. */
.block-gallery-carousel__item figure { margin: 0; }
.block-gallery-carousel__item .block-gallery__img,
.block-gallery-carousel__item img {
    width: 100%;
    height: 100%;
    object-fit: contain;
}

.carousel__controls {
    display: flex;
    align-items: center;
    justify-content: center;
    gap: .25rem;
}

.carousel__dots {
    display: flex;
    align-items: center;
    justify-content: center;
    gap: 0;
}

/* Bouton de 44 px, pastille de 12 px. */
.carousel__dot {
    position: relative;
    width: 44px;
    height: 44px;
    padding: 0;
    border: 0;
    background: transparent;
    cursor: pointer;
    border-radius: 50%;
}
.carousel__dot::after {
    content: "";
    position: absolute;
    inset: 50% auto auto 50%;
    width: 12px;
    height: 12px;
    margin: -6px 0 0 -6px;
    border: 2px solid currentColor;
    border-radius: 50%;
    background: transparent;
    transition: background-color var(--transition-fast), transform var(--transition-fast);
}

.carousel__nav,
.carousel__pause {
    display: inline-flex;
    align-items: center;
    justify-content: center;
    width: 44px;
    height: 44px;
    padding: 0;
    border: 0;
    background: transparent;
    color: inherit;
    cursor: pointer;
    border-radius: 50%;
    opacity: .75;
    transition: opacity var(--transition-fast);
}
.carousel__nav:hover,
.carousel__pause:hover { opacity: 1; }
.carousel__nav { font-size: 1.75rem; line-height: 1; }

/* Le bouton pause dessine ses deux barres, puis un triangle une fois arrêté :
   pas d'image, donc rien à recolorer ni à charger. */
.carousel__pause > span {
    display: block;
    width: 12px;
    height: 12px;
    border-left: 3px solid currentColor;
    border-right: 3px solid currentColor;
}
.carousel__pause[aria-pressed="true"] > span {
    width: 0;
    height: 0;
    border: 6px solid transparent;
    border-left: 10px solid currentColor;
    border-right: 0;
}

.carousel__dot:focus-visible,
.carousel__nav:focus-visible,
.carousel__pause:focus-visible,
.carousel:focus-visible {
    outline: 2px solid currentColor;
    outline-offset: 2px;
    border-radius: var(--radius-sm);
    opacity: 1;
}

/* ----------------------------------------------------------------------
   ÉTAT VIDE D'UN COMPOSANT DE LISTE

   Les quatre composants qui listent des fiches (Articles du blog, Prochaines dates,
   À l'affiche, Calendrier) partagent une politique : quand il n'y a rien à
   montrer, ils le DISENT au lieu de disparaître (cf. renderListingEmpty).

   La règle vit ici et non dans events.css : celle-ci n'est chargée que si la
   page contient un composant d'agenda, et l'état vide sert aussi au blog. Même
   habillage que `.events__empty`, qui reste en place pour les gabarits de page.
   ---------------------------------------------------------------------- */

.listing-empty {
    margin: 0;
    padding: var(--space-lg);
    color: var(--color-muted);
    background: var(--color-surface);
    border-radius: var(--radius-md);
    text-align: center;
}

/* ----------------------------------------------------------------------
   LISTES ÉCRITES DANS UN COMPOSANT

   base.css remet `ul, ol { list-style: none }` à zéro pour tout le site — il le
   faut, la quasi-totalité des listes du CMS sont des menus, des grilles ou des
   barres de méta. Trois endroits rétablissaient ensuite des puces : l'éditeur
   WYSIWYG (wysiwyg.css), le corps d'article (blog.css) et le bandeau cookies.
   Le composant « Texte » ne l'avait jamais été : l'éditeur MONTRAIT des puces que le
   visiteur ne voyait pas, alors que la description du composant promet « listes ».

   Même sélection que le tableau juste en dessous, et pour la même raison : une
   liste écrite dans le WYSIWYG n'a pas de classe, alors que toutes les listes
   fabriquées par un composant en portent une (events-list, blog-list,
   block-icon-list, site-footer__list…). `:not([class])` les distingue sans
   énumérer personne.
   ---------------------------------------------------------------------- */

ul:not([class]),
ol:not([class]) {
    padding-left: 1.5em;
    margin: 0 0 var(--space-md);
}
ul:not([class]) { list-style: disc; }
ol:not([class]) { list-style: decimal; }
ul:not([class]) ul:not([class]) { list-style: circle; }
ul:not([class]) li,
ol:not([class]) li { margin-bottom: 0.35em; }
/* Une liste imbriquée ne redouble pas la marge du bas. */
ul:not([class]) ul:not([class]),
ul:not([class]) ol:not([class]),
ol:not([class]) ul:not([class]),
ol:not([class]) ol:not([class]) { margin-bottom: 0; }

/* ----------------------------------------------------------------------
   MOTS INSÉCABLES

   Mesuré : un mot sans césure possible (référence, mot composé allemand, adresse
   sans tiret) dans un titre de carte donnait `scrollWidth 549` pour
   `clientWidth 245` — 304 px de texte hors de la boîte. La page, elle, ne
   débordait pas : `.section { overflow: hidden }` masquait le symptôme, et la fin
   du mot était simplement EFFACÉE de l'écran, sans un signe.

   `anywhere` et non `break-word` : seul le premier autorise le navigateur à
   tenir compte de la coupure dans le calcul de la largeur minimale du contenu —
   ce qui est précisément ce qui manque en grille.

   `min-width: 0` sur les enfants de grille : sans lui, `min-width: auto` fait
   grossir la PISTE, et un seul mot long casse la rangée entière, pas sa carte.
   ---------------------------------------------------------------------- */

/* `anywhere` chez les ENFANTS DE GRILLE, et eux seuls : c'est le seul mot-clé qui
   fasse compter la coupure dans la largeur minimale du contenu — donc le seul qui
   empêche un mot long de faire grossir la PISTE et de casser toute la rangée. */
.block-card__title,
.block-card__text,
.block-icon-list__body,
.block-cta__title,
.block-cta__text,
.block-stats__label { overflow-wrap: anywhere; }

/* `break-word` partout ailleurs. La différence n'est pas cosmétique : `anywhere`
   autorise le navigateur à réduire une colonne jusqu'à la largeur d'UNE LETTRE, et
   le tableau des horaires y coupait « Lundi — jeudi » en « Lund / i — / jeudi »
   (constaté à 760 px). `break-word` ne coupe qu'en dernier recours, sans toucher au
   calcul de la largeur minimale : la colonne garde la mesure de son plus long mot. */
.block-accordion__q,
.block-accordion__a,
.block-quote blockquote,
.block-quote cite,
.block-hours__day,
.block-hours__slot { overflow-wrap: break-word; }

.block-card,
.block-icon-list__item,
.block-icon-list__body,
.block-stats__item { min-width: 0; }

.block-table,
table:not([class]) {
    display: block;
    overflow-x: auto;
    -webkit-overflow-scrolling: touch;
    max-width: 100%;
    width: 100%;
    border-collapse: collapse;
    margin: var(--space-lg) 0;
    font-size: 0.95rem;
    line-height: 1.45;
}

.block-table caption,
table:not([class]) caption {
    caption-side: top;
    text-align: left;
    font-family: var(--font-heading);
    font-weight: 600;
    color: var(--color-heading);
    padding: 0 0 var(--space-sm);
}

.block-table th,
.block-table td,
table:not([class]) th,
table:not([class]) td {
    padding: 0.7rem var(--space-md);
    text-align: left;
    vertical-align: top;
    border: 0;
}

/* En-tête de COLONNE — discret, en capitales, posé sur un filet net. */
.block-table thead th,
table:not([class]) thead th {
    font-family: var(--font-heading);
    font-size: 0.78rem;
    font-weight: 600;
    letter-spacing: 0.07em;
    text-transform: uppercase;
    color: var(--color-muted);
    white-space: nowrap;
    border-bottom: 2px solid var(--color-border);
    background: var(--color-bg);
}

/* Zébrures — la « légère teinte » : deux jetons de la charte qui ne diffèrent
   que d'un souffle, en clair comme en sombre. Pas de filet entre les lignes :
   l'alternance suffit à guider l'œil, et la page reste épurée. */
.block-table tbody tr:nth-child(odd),
table:not([class]) tbody tr:nth-child(odd)  { background: var(--color-bg); }
.block-table tbody tr:nth-child(even),
table:not([class]) tbody tr:nth-child(even) { background: var(--color-surface); }

/* En-tête de LIGNE — première colonne. Un peu plus affirmée que les cellules,
   sans crier. Collée à gauche pour rester lisible pendant le défilement
   horizontal : sans elle, on ne sait plus de quelle ligne on lit la valeur. */
.block-table th[scope="row"],
table:not([class]) th[scope="row"] {
    font-weight: 600;
    color: var(--color-heading);
    position: sticky;
    left: 0;
    background: inherit;
    min-width: 12rem;
}

/* Pied de tableau : un filet qui referme, rien de plus. */
.block-table tfoot td,
.block-table tfoot th,
table:not([class]) tfoot td,
table:not([class]) tfoot th {
    border-top: 2px solid var(--color-border);
    font-weight: 600;
}

@media (max-width: 640px) {
    .block-table,
    table:not([class]) { font-size: 0.88rem; }

    .block-table th,
    .block-table td,
    table:not([class]) th,
    table:not([class]) td { padding: 0.6rem 0.7rem; }

    .block-table th[scope="row"],
    table:not([class]) th[scope="row"] { min-width: 10rem; }
}

/* ============================================================================
   BOUTON « HAUT DE PAGE »
   Réglé dans l'admin (Réglages → Navigation), affiché par défaut.

   POURQUOI IL NE MASQUE PAS LE PIED DE PAGE
     Un bouton simplement collé en bas à droite recouvre le pied dès qu'on
     l'atteint — c'est-à-dire exactement au moment où l'on descend y lire une
     adresse ou cliquer un lien. `js/back-to-top.js` mesure la part visible du
     pied et la reporte dans `--btt-offset` : le bouton remonte au-dessus au
     lieu de passer par-dessus.
   ========================================================================== */
.back-to-top {
    position: fixed;
    right: 1.25rem;
    bottom: calc(1.25rem + var(--btt-offset, 0px));
    z-index: 95;                 /* sous l'en-tête collant (100) et le bandeau cookies (1000) */
    display: flex;
    align-items: center;
    justify-content: center;
    width: 2.75rem;
    height: 2.75rem;
    padding: 0;
    border: 1px solid var(--color-border);
    border-radius: var(--radius-md);
    background: var(--color-surface);
    color: var(--color-heading);
    box-shadow: var(--shadow-md);
    cursor: pointer;
    /* Absent du flux tant qu'on n'a pas défilé : `visibility` retire aussi le
       bouton de l'ordre de tabulation, ce qu'une simple opacité ne fait pas. */
    opacity: 0;
    visibility: hidden;
    transform: translateY(0.5rem);
    transition: opacity .2s ease, transform .2s ease, visibility .2s;
}
.back-to-top.is-visible {
    opacity: 1;
    visibility: visible;
    transform: translateY(0);
}
.back-to-top:hover,
.back-to-top:focus-visible {
    background: var(--color-accent);
    color: var(--color-bg);
    border-color: var(--color-accent);
}
.back-to-top svg { width: 1.15rem; height: 1.15rem; display: block; }

@media (max-width: 640px) {
    .back-to-top { right: .85rem; bottom: calc(.85rem + var(--btt-offset, 0px)); }
}
@media (prefers-reduced-motion: reduce) {
    .back-to-top { transition: opacity .2s ease, visibility .2s; transform: none; }
    .back-to-top.is-visible { transform: none; }
}

/* ----------------------------------------------------------------
   CHAMP CALCULÉ
   ----------------------------------------------------------------
   Un RÉSULTAT, pas une saisie : d'où <output> plutôt qu'un champ grisé, que
   le visiteur essaierait de remplir. La valeur n'est d'ailleurs jamais
   envoyée — le serveur la recalcule. */
.mp-form__calc {
    display: block;
    padding: var(--space-xs) 0;
    font-family: var(--font-heading);
    font-size: var(--text-lg);
    font-weight: 600;
    font-variant-numeric: tabular-nums;
    color: var(--color-heading, var(--color-text));
}

/* ----------------------------------------------------------
   Composant Carte
   ----------------------------------------------------------
   Le fond OpenStreetMap est servi par le site (tuiles.php) : rien ici
   n'appelle un domaine tiers, pas même une image de pastille — les épingles
   sont des cercles CSS, colorés par la Charte ou par le KML.
   ---------------------------------------------------------- */
.block-map {
    margin: var(--block-rhythm, var(--space-xl)) 0;
}

.block-map__canvas {
    width: 100%;
    height: var(--map-h, 500px);
    border-radius: var(--radius-md);
    overflow: hidden;
    background: var(--color-surface);
    box-shadow: var(--shadow-sm);
}

/* Épingle : un disque bordé de blanc, lisible sur une tuile claire comme sur
   une forêt. La couleur arrive en --pin, posée par le rendu PHP. */
.map-pin {
    display: block;
    width: 18px;
    height: 18px;
    border-radius: 50%;
    background: var(--pin, var(--color-accent));
    border: 3px solid #fff;
    box-shadow: 0 1px 4px rgba(0, 0, 0, 0.45);
}
.map-pin-wrap { background: none; border: 0; }

/* FORMES DES POINTS — réglage « Forme des points » du composant Carte
   (2026-09-11). Le rond ci-dessus reste le défaut : aucune classe, aucun
   changement pour les cartes déjà posées.
   Toutes gardent le liseré blanc et l'ombre portée, qui sont ce qui détache le
   point d'un fond de carte chargé : sans eux, un point foncé disparaît dans une
   forêt et un point clair dans un lac. */

/* Épingle : un carré aux trois coins arrondis, tourné d'un quart de tour — le
   coin resté vif devient la pointe, et c'est LUI qui désigne le lieu. Son ancre
   est calculée dans js/map.js ; les deux doivent rester d'accord. */
.map-pin--pin {
    border-radius: 50% 50% 50% 0;
    transform: rotate(-45deg);
}

/* Carré : angles à peine adoucis. Un angle parfaitement vif accroche mal l'œil
   à 18 px et bave au rendu. */
.map-pin--square { border-radius: 2px; }

/* Losange : le même carré, sur la pointe. Il se centre sur le lieu comme un
   rond — ce n'est pas une épingle, il ne désigne rien du doigt. */
.map-pin--diamond {
    border-radius: 2px;
    transform: rotate(45deg);
}

.map-pop__desc {
    margin-top: 0.35rem;
    font-size: var(--text-sm);
    line-height: 1.45;
}

.block-map__credit {
    margin: 0.5rem 0 0;
    font-size: var(--text-xs);
    color: var(--color-muted);
}

/* Alternative textuelle : repliée, mais bien présente dans le document — une
   carte ne dit rien à un lecteur d'écran, et WCAG 2.2 ne s'en contente pas. */
.block-map__alt {
    margin-top: 0.6rem;
    font-size: var(--text-sm);
}
.block-map__alt summary {
    cursor: pointer;
    color: var(--color-link);
}
.block-map__alt ul {
    margin: 0.6rem 0 0;
    padding-left: 1.2rem;
    line-height: 1.6;
}

.block-map__legend {
    margin-top: 0.7rem;
    font-size: var(--text-sm);
    color: var(--color-text);
}

/* ---- Mode Google : façade, comme la vidéo ---- */
.block-map__frame {
    position: relative;
    width: 100%;
    height: var(--map-h, 500px);
    overflow: hidden;
    border-radius: var(--radius-md);
    background: var(--color-surface);
}
.block-map__facade {
    position: absolute;
    inset: 0;
    width: 100%;
    height: 100%;
    padding: 0;
    border: 0;
    cursor: pointer;
    display: grid;
    place-content: center;
    gap: 0.8rem;
    background: var(--color-surface);
    color: var(--color-text);
}
.block-map__pinicon {
    justify-self: center;
    width: 34px;
    height: 34px;
    border-radius: 50% 50% 50% 0;
    transform: rotate(-45deg);
    background: var(--color-accent);
    box-shadow: var(--shadow-md);
    transition: transform 0.15s ease;
}
.block-map__facade:hover .block-map__pinicon,
.block-map__facade:focus-visible .block-map__pinicon {
    transform: rotate(-45deg) scale(1.1);
}
.block-map__note {
    position: absolute;
    left: 0;
    right: 0;
    bottom: 0;
    padding: 0.55rem 0.9rem;
    background: rgba(0, 0, 0, 0.62);
    color: #fff;
    font-size: var(--text-xs);
    text-align: center;
}
.block-map__iframe {
    width: 100%;
    height: var(--map-h, 500px);
    border: 0;
    border-radius: var(--radius-md);
}
.block-map__fallback {
    display: inline-block;
    padding: var(--space-md);
}

@media (prefers-reduced-motion: reduce) {
    .block-map__facade:hover .block-map__pinicon,
    .block-map__facade:focus-visible .block-map__pinicon { transform: rotate(-45deg); }
}

/* ==========================================================================
   Composant « Widget externe »
   --------------------------------------------------------------------------
   Même façade que la vidéo et que la carte Google, donc mêmes règles : un
   rectangle sobre aux couleurs de la Charte, un pictogramme, et la mention du
   service en bas. Rien n'est chargé avant le clic — c'est le CSS d'un bouton,
   pas d'un cadre.

   LA HAUTEUR VIT DANS UNE VARIABLE. Le widget peut l'ajuster lui-même après le
   clic (js/embed-facade.js écrit alors un `style="height:…"` sur le cadre, qui
   passe devant) ; avant, c'est la valeur réglée dans l'administration qui
   s'applique — au bouton comme au cadre, pour que rien ne saute au clic.
   ========================================================================== */

.block-embed {
    margin: var(--space-xl) 0;
}
.block-embed__frame {
    position: relative;
    width: 100%;
    height: var(--embed-h, 600px);
    overflow: hidden;
    border-radius: var(--radius-md);
    background: var(--color-surface);
}
.block-embed__facade {
    position: absolute;
    inset: 0;
    width: 100%;
    height: 100%;
    padding: 0;
    border: 0;
    cursor: pointer;
    display: grid;
    place-content: center;
    gap: 0.8rem;
    background: var(--color-surface);
    color: var(--color-text);
}
/* Un carré de calendrier : deux anneaux en haut, une page en dessous. Dessiné
   en CSS — une icône de plus à télécharger pour un bouton qui disparaît au
   premier clic ne se justifie pas. */
.block-embed__icon {
    justify-self: center;
    position: relative;
    width: 38px;
    height: 34px;
    border: 3px solid var(--color-accent);
    border-radius: 5px;
    border-top-width: 10px;
    transition: transform 0.15s ease;
}
.block-embed__icon::before,
.block-embed__icon::after {
    content: '';
    position: absolute;
    top: -14px;
    width: 4px;
    height: 8px;
    border-radius: 2px;
    background: var(--color-accent);
}
.block-embed__icon::before { left: 7px; }
.block-embed__icon::after  { right: 7px; }
.block-embed__facade:hover .block-embed__icon,
.block-embed__facade:focus-visible .block-embed__icon {
    transform: scale(1.1);
}
.block-embed__note {
    position: absolute;
    left: 0;
    right: 0;
    bottom: 0;
    padding: 0.55rem 0.9rem;
    background: rgba(0, 0, 0, 0.62);
    color: #fff;
    font-size: var(--text-xs);
    text-align: center;
}
.block-embed__iframe {
    width: 100%;
    height: var(--embed-h, 600px);
    border: 0;
    border-radius: var(--radius-md);
}
.block-embed__fallback {
    display: inline-block;
    padding: var(--space-md);
}
.block-embed__legend {
    margin-top: 0.7rem;
    font-size: var(--text-sm);
    color: var(--color-text);
}

@media (prefers-reduced-motion: reduce) {
    .block-embed__facade:hover .block-embed__icon,
    .block-embed__facade:focus-visible .block-embed__icon { transform: none; }
}

/* ==========================================================================
   Composant « Tableau »
   --------------------------------------------------------------------------
   Un tableau de données collé depuis un tableur. Les couleurs viennent de la
   Charte : ce bloc n'invente aucune teinte.

   L'ENVELOPPE DE DÉFILEMENT N'EST PAS FACULTATIVE. Un tableau à cinq colonnes
   ne rentre pas dans un téléphone. Sans elle, il élargit la PAGE — et c'est
   toute la mise en page qui se met à glisser latéralement, y compris le texte
   qui l'entoure. Avec elle, le tableau seul défile, dans son cadre.
   ========================================================================== */

.block-table-scroll {
    overflow-x: auto;
    -webkit-overflow-scrolling: touch;
    margin: var(--space-md) 0;
}

.block-table {
    width: 100%;
    min-width: 30rem;   /* en deçà, les colonnes se compriment au point d'être illisibles */
    border-collapse: collapse;
    font-size: var(--text-sm);
    line-height: 1.45;
}

.block-table caption {
    caption-side: bottom;
    padding-top: var(--space-sm);
    color: var(--color-muted);
    font-size: var(--text-sm);
    text-align: left;
}

.block-table th,
.block-table td {
    padding: 0.6em 0.9em;
    border-bottom: 1px solid var(--color-border);
    text-align: left;
    vertical-align: top;
}

.block-table thead th {
    border-bottom-width: 2px;
    font-weight: 600;
    color: var(--color-heading);
    white-space: nowrap;   /* un intitulé de colonne coupé en deux ne se lit plus */
}

/* Première colonne en en-tête : elle nomme la ligne, elle ne la remplit pas. */
.block-table tbody th[scope="row"] {
    font-weight: 600;
    color: var(--color-heading);
}

.block-table tbody tr:last-child th,
.block-table tbody tr:last-child td {
    border-bottom: none;
}

/* Les chiffres s'alignent sur leur virgule dès qu'ils se suivent en colonne :
   sans cela, « 120.– » et « 1 210.– » se lisent comme deux ordres de grandeur
   voisins. */
.block-table td {
    font-variant-numeric: tabular-nums;
}

/* ---------------------------------------------------------------------------
   Composant « Onglets »
   ---------------------------------------------------------------------------
   DEUX ÉTATS, et le premier n'est pas un accident.

   Sans JavaScript, le bloc reste ce que le serveur a rendu : une suite de
   volets titrés, lisibles à la file. Aucune règle ci-dessous ne les masque —
   c'est `js/tabs.js` qui pose `.is-tabbed` APRÈS avoir construit la barre de
   languettes, et c'est seulement à partir de là que les titres s'effacent et
   qu'un seul volet reste visible. Une feuille de style qui cacherait les volets
   d'emblée rendrait la page illisible le jour où le script ne tourne pas.
   --------------------------------------------------------------------------- */

.block-tabs {
    margin: var(--space-lg) 0;
}

/* La barre n'existe pas dans le HTML : elle est fabriquée par le script. */
.block-tabs__list {
    display: flex;
    flex-wrap: wrap;
    gap: var(--space-xs);
    border-bottom: 1px solid var(--color-border);
    margin-bottom: var(--space-lg);
}

.block-tabs__tab {
    appearance: none;
    background: none;
    border: 0;
    border-bottom: 2px solid transparent;
    margin-bottom: -1px;               /* la languette active mord sur le filet */
    padding: var(--space-sm) var(--space-md);
    font-family: var(--font-heading);
    font-size: var(--text-base);
    font-weight: 600;
    line-height: 1.3;
    color: var(--color-muted);
    cursor: pointer;
    transition: color .2s, border-color .2s;
}

.block-tabs__tab:hover { color: var(--color-text); }

.block-tabs__tab[aria-selected="true"] {
    color: var(--color-accent);
    border-bottom-color: var(--color-accent);
}

/* Le focus doit rester VISIBLE : la barre se parcourt aux flèches, et sans ce
   contour on ne saurait jamais où l'on est. */
.block-tabs__tab:focus-visible {
    outline: 2px solid var(--color-accent);
    outline-offset: 2px;
    border-radius: 3px;
}

/* Un volet reçoit le focus (tabindex 0) pour être atteignable au clavier ; on
   ne lui met pas de contour permanent pour autant. */
.block-tabs__panel:focus { outline: none; }
.block-tabs__panel:focus-visible {
    outline: 2px solid var(--color-accent);
    outline-offset: 4px;
}

/* Sans le script : chaque volet garde son titre et suit le précédent. */
.block-tabs__panel + .block-tabs__panel { margin-top: var(--space-xl); }
.block-tabs__panel-title {
    font-family: var(--font-heading);
    font-size: var(--text-lg);
    margin: 0 0 var(--space-sm);
}

/* Avec le script : la barre porte les titres, les volets s'effacent. */
.block-tabs.is-tabbed .block-tabs__panel-title { display: none; }
.block-tabs.is-tabbed .block-tabs__panel + .block-tabs__panel { margin-top: 0; }

@media (max-width: 600px) {
    /* Sur un petit écran, une barre de six languettes devient illisible : elle
       défile horizontalement plutôt que de se casser sur quatre lignes. */
    .block-tabs__list {
        flex-wrap: nowrap;
        overflow-x: auto;
        scrollbar-width: thin;
    }
    .block-tabs__tab { white-space: nowrap; }
}

/* ---------------------------------------------------------------------------
   Composant « Projets » (portfolio-projects)
   ---------------------------------------------------------------------------
   Il rend la MÊME liste que la rubrique — même HTML, mêmes classes
   `.portfolio-item*`, servies par css/portfolio.css. Ne sont ici que
   l'enveloppe et le lien « voir tous », qui n'existent pas dans la rubrique.

   ⚠️ css/portfolio.css n'est chargée QUE sur les pages du module. Une page
   ordinaire qui porte ce composant doit donc la charger aussi : c'est
   index.php qui s'en occupe, à la détection des composants.
   --------------------------------------------------------------------------- */

.block-portfolio { margin: var(--space-lg) 0; }

.block-portfolio__title {
    font-family: var(--font-heading);
    margin: 0 0 var(--space-lg);
}

.block-portfolio__all {
    margin-top: var(--space-lg);
    text-align: center;
}

/* ===========================================================================
   LISTES DE CONTENU — les quatre mises en forme partagées
   (rendu : includes/content-types.php, renderContentList)

   CE QUI EST ICI : la STRUCTURE. Quels éléments se voient, comment ils se
   placent, quelle grille. Le Blog et le Portfolio passent tous deux par ces
   classes.

   CE QUI N'Y EST PAS : la PEAU. Aucune typographie, aucune couleur, aucune
   proportion d'image — sinon ces règles écraseraient celles que le Blog a
   déjà (css/blog.css) et ses listes changeraient d'aspect. Chaque module pose
   sa peau derrière SA classe de liste : `.blog-list` pour l'un,
   `.portfolio-list` pour l'autre.

   ⚠️ LA BOÎTE DE L'ENTRÉE N'EST PAS TOUJOURS LA MÊME. Une entrée sans bouton
   « Lire la suite » est enveloppée dans un seul lien (`.content-item__link`) ;
   une entrée qui en porte un ne peut pas l'être — deux liens ne s'imbriquent
   pas. La boîte à mettre en grille est donc le lien s'il existe, l'entrée
   sinon. D'où les couples de sélecteurs `:not(:has(> .content-item__link))` /
   `> .content-item__link` qui reviennent ci-dessous.
   ========================================================================= */

.content-list { list-style: none; margin: 0; padding: 0; }

.content-item__link {
    display: block;
    color: inherit;
    text-decoration: none;
}

.content-item__media { display: block; overflow: hidden; }
.content-item__media img { width: 100%; height: auto; display: block; }

/* Nombre de colonnes : on nourrit les variables du socle .block-cols, plutôt
   que de redéclarer une grille. Une seule mécanique de colonnes dans le CMS. */
.content-list--cards { --col-min: 260px; }
.content-list--cols-1 { --cols: 1; }
.content-list--cols-2 { --cols: 2; }
.content-list--cols-3 { --cols: 3; }
.content-list--cols-4 { --cols: 4; }
.content-list--cols-5 { --cols: 5; }

/* ---------- « Liens seuls » : une ligne par entrée ------------------------- */
.content-list--links {
    display: block;
    border-top: 1px solid var(--color-border);
}
.content-list--links .content-item { border-bottom: 1px solid var(--color-border); }
.content-list--links .content-item__link { padding: var(--space-sm) 0; }
.content-list--links .content-item__body {
    display: flex;
    flex-wrap: wrap;
    gap: var(--space-xs) var(--space-md);
    align-items: baseline;
    justify-content: space-between;
}
.content-list--links .content-item__meta { margin: 0; }

/* ---------- « Vignette à gauche » : une colonne, vignette de côté ---------- */
/* UNE seule colonne, toujours : sans cela `.block-cols` — que les deux modules
   posent sur la liste — en étalerait trois de front, et une « ligne » cesserait
   d'en être une. */
.content-list--rows {
    display: grid;
    grid-template-columns: minmax(0, 1fr);
    gap: var(--space-lg);
}
.content-list--rows .content-item:not(:has(> .content-item__link)),
.content-list--rows .content-item > .content-item__link {
    display: grid;
    grid-template-columns: minmax(0, 140px) minmax(0, 1fr);
    gap: var(--space-md);
    align-items: start;
}
.content-list--rows .content-item--img-right:not(:has(> .content-item__link)),
.content-list--rows .content-item--img-right > .content-item__link {
    grid-template-columns: minmax(0, 1fr) minmax(0, 140px);
}
.content-list--rows .content-item--img-right .content-item__media { order: 2; }
/* Sans vignette, la colonne réservée ne doit pas rester vide. */
.content-list--rows .content-item--noimg:not(:has(> .content-item__link)),
.content-list--rows .content-item--noimg > .content-item__link {
    grid-template-columns: minmax(0, 1fr);
}
@media (max-width: 480px) {
    .content-list--rows .content-item:not(:has(> .content-item__link)),
    .content-list--rows .content-item > .content-item__link { grid-template-columns: minmax(0, 1fr); }
    .content-list--rows .content-item--img-right .content-item__media { order: 0; }
}

/* ---------- « Cartes » : la vignette peut se poser de côté ----------------- */
.content-list--cards .content-item--img-left:not(:has(> .content-item__link)),
.content-list--cards .content-item--img-left > .content-item__link,
.content-list--cards .content-item--img-right:not(:has(> .content-item__link)),
.content-list--cards .content-item--img-right > .content-item__link {
    display: grid;
    grid-template-columns: minmax(0, 1fr) minmax(0, 1fr);
    gap: var(--space-md);
    align-items: start;
}
.content-list--cards .content-item--img-right .content-item__media { order: 2; }

/* Titre POSÉ SUR l'image. Le repère est le LIEN quand il existe, l'entrée
   sinon : le corps est le FRÈRE de la vignette, pas son enfant, et se calerait
   autrement sur un ancêtre lointain — il s'étalait sur toute la largeur de la
   grille et débordait sous la carte. */
.content-list--title-over .content-item:not(:has(> .content-item__link)),
.content-list--title-over .content-item > .content-item__link { position: relative; display: block; }
.content-list--title-over .content-item__body {
    position: absolute;
    inset: auto 0 0 0;
    padding: var(--space-xl) var(--space-md) var(--space-md);
    background: linear-gradient(to top, rgb(0 0 0 / .74), rgb(0 0 0 / 0));
    color: #fff;
}
/* Sur l'image, on ne garde que la méta et le titre : l'extrait et le supplément
   recouvraient les deux tiers de la vignette, alors que ce mode existe pour
   montrer la PHOTO.
   ⚠️ TROIS CLASSES, VOLONTAIREMENT. Une peau de module (`.portfolio-list .x`,
   `.blog-list .x`) pèse deux classes et est chargée APRÈS ce fichier : à
   égalité, elle gagnerait, et le texte se remettrait à recouvrir la photo. */
.content-list--title-over .content-item .content-item__excerpt,
.content-list--title-over .content-item .content-item__facts,
.content-list--title-over .content-item .content-item__more { display: none; }
.content-list--title-over .content-item .content-item__meta { color: rgb(255 255 255 / .82); }
.content-list--title-over .content-item .content-item__title { color: #fff; }

/* ---------- « Briques » : maçonnerie, nom au survol ------------------------ */
/* MAÇONNERIE EN COLONNES CSS, sans une ligne de JavaScript. `grid` ne sait pas
   encore faire de vraie maçonnerie dans les navigateurs livrés ; `columns` le
   fait depuis toujours.
   ⚠️ SON DÉFAUT, ASSUMÉ : l'ordre se lit de HAUT EN BAS, colonne par colonne,
   et non de gauche à droite. */
.content-list--bricks {
    columns: var(--cols, 3) 260px;
    column-gap: var(--space-md);
    display: block;
}
.content-list--bricks .content-item {
    break-inside: avoid;
    margin: 0 0 var(--space-md);
    display: block;
}
.content-list--bricks .content-item:not(:has(> .content-item__link)),
.content-list--bricks .content-item > .content-item__link { position: relative; display: block; }
.content-list--bricks .content-item__body {
    position: absolute;
    inset: 0;
    display: flex;
    flex-direction: column;
    justify-content: flex-end;
    gap: var(--space-2xs, .25rem);
    padding: var(--space-md);
    background: linear-gradient(to top, rgb(0 0 0 / .72), rgb(0 0 0 / .1));
    color: #fff;
}
.content-list--bricks .content-item .content-item__meta { color: rgb(255 255 255 / .8); margin: 0; }
.content-list--bricks .content-item .content-item__title { color: #fff; }

/* Le nom est TOUJOURS dans le HTML : on ne le masque que là où un survol
   existe. Il reste donc lu au clavier, sur mobile et par un lecteur d'écran. */
@media (hover: hover) {
    .content-list--bricks .content-item__body { opacity: 0; transition: opacity .25s; }
    .content-list--bricks .content-item:hover .content-item__body,
    .content-list--bricks .content-item:focus-within .content-item__body { opacity: 1; }
}

/* Brique SANS image : le corps ne doit pas se poser sur une vignette absente,
   sinon il sortirait du flux et se superposerait à sa voisine. */
.content-list--bricks .content-item--noimg .content-item__body,
.content-list--title-over .content-item--noimg .content-item__body {
    position: static;
    opacity: 1;
    padding: var(--space-md);
    background: var(--color-surface);
    color: inherit;
}

@media (prefers-reduced-motion: reduce) {
    .content-list--bricks .content-item__body { transition: none; }
}

/* ---------- « Carrousel » : une entrée à la fois -------------------------- */
/* Le balisage, les commandes et le JS viennent du carrousel générique
   (.carousel, js/slider.js), qui sert déjà le Slider et les Témoignages. Ne
   restent ici que les quelques règles propres à une entrée de contenu. */
.content-carousel { margin-block: var(--space-md); }

.content-carousel__slide { padding-bottom: var(--space-xl); }

/* Une carte de carrousel n'est pas serrée par une grille : on lui laisse la
   largeur d'un texte lisible, image comprise. */
.content-carousel__slide .content-item {
    display: grid;
    gap: var(--space-md);
    max-width: 72ch;
    margin-inline: auto;
}

/* Les commandes se posent SOUS la piste, au centre — le carrousel de contenu
   n'a pas de fond sombre où les superposer, contrairement au Slider. */
.content-carousel > .carousel__controls {
    position: static;
    display: flex;
    justify-content: center;
    align-items: center;
    gap: var(--space-sm);
    margin-top: var(--space-sm);
}

/* ---------- Cadrage des vignettes (contentRatios) ------------------------- */
/* Choisi dans le composant, il l'emporte sur le cadrage que le module donne par
   défaut aux siennes — d'où le `:not([class*="content-list--ratio-"])` qui garde
   les peaux de module (blog, portfolio, agenda) de se battre avec lui. */
.content-list--ratio-free  .content-item__media { aspect-ratio: auto; }
.content-list--ratio-3-2   .content-item__media { aspect-ratio: 3 / 2; }
.content-list--ratio-16-9  .content-item__media { aspect-ratio: 16 / 9; }
.content-list--ratio-1-1   .content-item__media { aspect-ratio: 1 / 1; }
.content-list--ratio-3-4   .content-item__media { aspect-ratio: 3 / 4; }
.content-list--ratio-2-3   .content-item__media { aspect-ratio: 2 / 3; }

.content-list[class*="content-list--ratio-"]:not(.content-list--ratio-free) .content-item__media img {
    height: 100%;
    object-fit: cover;
}

/* Format de la boîte d'une vidéo (« Mise en forme » → Format).
   Une vidéo AUTO-HÉBERGÉE garde ses proportions natives, et un tournage au
   téléphone est vertical : à pleine largeur de colonne il fait 635 px de haut
   là où ses voisines en font 200, et la grille explose. Le format contraint la
   boîte ; `contain` garantit que RIEN n'est recadré — la vidéo se centre, au
   prix de bandes latérales, ce qui est le bon compromis pour un document.
   « Hauteurs libres » (block-ratio--free) laisse les proportions natives. */
.block-video[class*="block-ratio--"]:not(.block-ratio--free) video,
.block-video[class*="block-ratio--"]:not(.block-ratio--free) .block-video__frame {
    width: 100%;
    height: 100%;
    object-fit: contain;
}
.block-video.block-ratio--4-3  { aspect-ratio: 4 / 3; }
.block-video.block-ratio--16-9 { aspect-ratio: 16 / 9; }
.block-video.block-ratio--3-4  { aspect-ratio: 3 / 4; }
.block-video.block-ratio--2-3  { aspect-ratio: 2 / 3; }
.block-video.block-ratio--free video { height: auto; }

/* --------------------------------------------------------------------------
   Grille de cartes — visuel « Icône sur pastille ronde »
   --------------------------------------------------------------------------
   L'autre usage d'une grille de cartes : le visuel n'est pas une vignette à
   recadrer mais un PICTOGRAMME détouré, posé sur une pastille de la couleur
   d'accent, à cheval sur le bord haut de la carte.

   Le motif était écrit à la main dans la couche site d'au moins un site,
   accroché au slug d'une section — donc cassé au premier renommage, sans que
   rien ne l'explique. Il n'a rien de propre à un site : c'est une carte de
   fonctionnalité, et tout le monde en fait.

   `overflow: visible` sur la carte est indispensable — sans lui, la moitié
   haute de la pastille serait rognée par le cadre. Le `margin-top` de la carte
   réserve la place de ce débordement, sinon la pastille chevaucherait la carte
   précédente.
   -------------------------------------------------------------------------- */
.block-card-grid--badge .block-card {
    overflow: visible;
    margin-top: 44px;
}
.block-card-grid--badge .block-card__image {
    width: 88px;
    height: 88px;
    margin: -44px auto var(--space-md);
    background: var(--color-accent);
    border-radius: 50%;
    display: flex;
    align-items: center;
    justify-content: center;
    box-shadow: 0 4px 14px rgba(0, 0, 0, 0.12);
    aspect-ratio: auto;
    overflow: visible;
}
.block-card-grid--badge .block-card__image img,
.block-card-grid--badge .block-card__icon {
    width: 48px;
    height: 48px;
    object-fit: contain;
}
/* Le glyphe posé sur la pastille ne peut pas hériter de `currentColor` : la
   pastille est peinte en --color-accent, un tracé de la couleur du texte de la
   carte s'y confondrait. On reprend la convention de `.btn--primary`, seul
   autre endroit du CMS où l'on écrit sur un fond d'accent. */
.block-card-grid--badge .block-card__icon {
    color: var(--color-bg);
}

/* --- Cartes « icône seule » : le même dessin, sans le rond ---
   Pour les sites où la pastille de 88 px est trop démonstrative. L'icône reste
   à sa place, en haut de la carte, et suit la couleur d'accent du site. */
.block-card-grid--icon .block-card__image--icon {
    padding: var(--space-lg) var(--space-md) 0;
    background: none;
}
.block-card-grid--icon .block-card__icon {
    width: 44px;
    height: 44px;
    color: var(--color-accent);
}

/* --- Cartes « logo » : une marque, entière, sur un fond où elle se voit ---
   ---------------------------------------------------------------------------
   Un logo arrive presque toujours en PNG à fond TRANSPARENT, et son texte est
   noir. Sur un site en mode sombre, il disparaît — constaté le 2026-09-16 sur
   mon-producteur.ch, dont les dix-sept logos de partenaires devenaient illisibles
   à la bascule : texte noir sur un fond de page rgb(20,23,15), sans fond de
   carte, sans filtre.

   Le remède n'est PAS `filter: invert()`. Il rendrait le texte lisible en
   dénaturant la marque : le point rouge de Regiogarantie virerait au cyan, la
   croix suisse de Vaud+ aussi. On ne retouche pas les couleurs d'une marque —
   on lui rend le fond clair sur lequel elle a été dessinée.

   `--card-logo-bg` est en blanc cassé plutôt qu'en blanc pur : sur une page
   claire, un blanc pur découperait une dalle plus lumineuse que le fond. Un
   site peut le redéfinir dans `css/site.css` — certaines chartes de partenaires
   exigent un fond précis.
   -------------------------------------------------------------------------- */
.block-card-grid--logo {
    --card-logo-bg: #FBFBF9;
}
.block-card-grid--logo .block-card__image {
    background: var(--card-logo-bg);
    /* De l'air autour : une marque a sa zone de protection, et un logo collé
       aux bords de sa dalle se lit comme une erreur de montage. */
    padding: var(--space-md);
    /* Pas de recadrage : `contain` est déjà la règle de `.block-card__image img`,
       on garantit seulement que le ratio imposé plus haut ne rogne rien. */
    aspect-ratio: auto;
    min-height: 120px;
}
.block-card-grid--logo .block-card__image img {
    max-height: 90px;
    object-fit: contain;
}

/* Hors des deux modes ci-dessus, une icône posée sur une carte reste sobre :
   elle occupe la place de l'image sans en prendre la hauteur. */
.block-card__image--icon {
    aspect-ratio: auto;
}
.block-card__icon {
    width: 40px;
    height: 40px;
    flex: none;
}

/* Hauteurs EXACTES de l'en-tête (réglages « Hauteur exacte »).
   Les crans — 44 / 88 / 120 px de bande, 48 / 70 / 92 px de logo — couvrent la
   plupart des sites ; un site reconstruit tombe presque toujours entre deux, et
   devait alors reprendre ces hauteurs dans sa couche site.
   Les deux variables ne sont posées QUE si une valeur exacte est saisie : sans
   elles, les règles ci-dessus gardent la main, et rien ne bouge. */
.site-header__row[style*="--hd-row-h"] {
    padding-block: 0;
}
.site-header__row[style*="--hd-row-h"] .site-header__inner,
.site-header__row[style*="--hd-row-h"] .site-header__cols {
    min-height: var(--hd-row-h);
}
.site-header__logo[style*="--hd-logo-h"] img {
    height: var(--hd-logo-h);
}

/* ===========================================================================
   GRAPHIQUE DU COMPOSANT « TABLEAU ET GRAPHIQUE »
   ===========================================================================
   Le SVG est engendré par le serveur (includes/chart.php) ; tout ce qui est
   COULEUR et ÉPAISSEUR vit ici, pour que la charte du site s'y applique sans
   qu'on retouche une ligne de PHP. C'est aussi ce qui fait qu'un graphique
   suit le mode sombre : les variables changent, le dessin non.

   Six teintes, dérivées des deux couleurs de la charte. `color-mix` clarifie ou
   assombrit plutôt que d'inventer des valeurs en dur, qui jureraient avec la
   palette du client dès qu'il la change. Un site peut redéfinir les six dans
   `css/site.css` sans toucher au cœur. */
.block-chart {
    --chart-1: var(--color-accent);
    --chart-2: var(--color-secondary);
    --chart-3: color-mix(in srgb, var(--color-accent) 55%, var(--color-bg));
    --chart-4: color-mix(in srgb, var(--color-secondary) 55%, var(--color-bg));
    --chart-5: color-mix(in srgb, var(--color-accent) 70%, var(--color-text));
    --chart-6: color-mix(in srgb, var(--color-secondary) 70%, var(--color-text));
    margin: 0 0 var(--space-md, 1.5rem);
}
.block-chart svg {
    display: block;
    width: 100%;
    height: auto;
    /* La hauteur du viewBox est de 320 ; sans plafond, le SVG grandit avec la
       largeur de la colonne et un graphique de six valeurs occupe un écran. */
    max-height: 420px;
    overflow: visible;
}

/* Le repère. Les graduations s'effacent derrière les données — c'est la règle
   de tout graphique lisible : la grille se devine, elle ne se lit pas. */
.block-chart__grid  { stroke: var(--color-border); stroke-width: 1; opacity: .55; }
.block-chart__zero  { stroke: var(--color-text); stroke-width: 1; opacity: .35; }
.block-chart__tick,
.block-chart__label {
    font-family: var(--font-body);
    font-size: 11px;
    fill: var(--color-muted);
}
.block-chart__part {
    font-family: var(--font-heading);
    font-size: 12px;
    font-weight: 600;
    fill: var(--color-bg);
}

/* Barres, courbes, parts : la même série, la même couleur, d'un type à l'autre. */
.block-chart__bar   { transition: none; }
.block-chart__line  { fill: none; stroke-width: 2.5; stroke-linejoin: round; stroke-linecap: round; }
.block-chart__slice { stroke: var(--color-bg); stroke-width: 1.5; }

.block-chart--c1.block-chart__bar, .block-chart--c1.block-chart__slice, .block-chart--c1.block-chart__dot { fill: var(--chart-1); }
.block-chart--c2.block-chart__bar, .block-chart--c2.block-chart__slice, .block-chart--c2.block-chart__dot { fill: var(--chart-2); }
.block-chart--c3.block-chart__bar, .block-chart--c3.block-chart__slice, .block-chart--c3.block-chart__dot { fill: var(--chart-3); }
.block-chart--c4.block-chart__bar, .block-chart--c4.block-chart__slice, .block-chart--c4.block-chart__dot { fill: var(--chart-4); }
.block-chart--c5.block-chart__bar, .block-chart--c5.block-chart__slice, .block-chart--c5.block-chart__dot { fill: var(--chart-5); }
.block-chart--c6.block-chart__bar, .block-chart--c6.block-chart__slice, .block-chart--c6.block-chart__dot { fill: var(--chart-6); }
.block-chart--c1.block-chart__line { stroke: var(--chart-1); }
.block-chart--c2.block-chart__line { stroke: var(--chart-2); }
.block-chart--c3.block-chart__line { stroke: var(--chart-3); }
.block-chart--c4.block-chart__line { stroke: var(--chart-4); }
.block-chart--c5.block-chart__line { stroke: var(--chart-5); }
.block-chart--c6.block-chart__line { stroke: var(--chart-6); }

/* La légende. En ligne et repliable : sur un téléphone, six noms de séries
   côte à côte sortiraient de l'écran. */
.block-chart__legend {
    display: flex;
    flex-wrap: wrap;
    gap: var(--space-xs, .5rem) var(--space-md, 1.5rem);
    margin: var(--space-xs, .5rem) 0 0;
    padding: 0;
    list-style: none;
    font-size: var(--text-sm);
    color: var(--color-muted);
}
.block-chart__legend li { display: flex; align-items: center; gap: .45em; }
.block-chart__key {
    flex: 0 0 auto;
    width: .8em;
    height: .8em;
    border-radius: 2px;
    background: var(--color-border);
}
.block-chart__key.block-chart--c1 { background: var(--chart-1); }
.block-chart__key.block-chart--c2 { background: var(--chart-2); }
.block-chart__key.block-chart--c3 { background: var(--chart-3); }
.block-chart__key.block-chart--c4 { background: var(--chart-4); }
.block-chart__key.block-chart--c5 { background: var(--chart-5); }
.block-chart__key.block-chart--c6 { background: var(--chart-6); }
