/* =========================================================================
   By Zigzag — Composants réutilisables (boutons, header, footer, switch)
   ========================================================================= */

/* ---- Boutons ---- */
.btn {
    display: inline-flex;
    align-items: center;
    justify-content: center;
    gap: 0.5rem;
    padding: 0.7rem 1.4rem;
    border-radius: var(--radius);
    border: 1px solid transparent;
    /* ⚠️ CES DEUX LIGNES ONT ÉTÉ PAYÉES SUR UN iPhone (26 août 2026), et elles
       ferment une FAMILLE, pas un cas. Alexandre : « le fond du bouton annuler
       est bleu ».

       `.btn` ne posait AUCUN `background`. Un bouton dont le modificateur n'en
       fournit pas non plus — `.btn--danger`, qui ne définit qu'une couleur de
       texte — laissait donc passer le CHROME NATIF du navigateur : gris
       `rgb(240,240,240)` sur Chrome de bureau, BLEU sur iOS. Mesuré en console,
       `appearance: auto` à l'appui.

       ⚠️ C'EST INVISIBLE À TOUT CE QUI LIT LE HTML : la classe est bien là, elle
       existe bien dans la feuille, et elle ne fournit simplement pas ce qu'on
       croit (`CLAUDE.md` §5.1). Il a fallu un vrai appareil pour que ça se voie
       comme un défaut, et une mesure du `backgroundColor` pour le nommer.

       Les modificateurs qui posent un fond (`--primary`) le font APRÈS dans ce
       fichier : ils l'emportent, à spécificité égale. */
    background: transparent;
    -webkit-appearance: none;
    appearance: none;
    font-family: var(--police-titre);
    font-weight: 600;
    letter-spacing: 1px;
    text-transform: uppercase;
    font-size: 0.9rem;
    cursor: pointer;
    transition: transform 0.12s ease, background-color 0.18s ease, color 0.18s ease, border-color 0.18s ease;
}

.btn:hover {
    transform: translateY(-1px);
}

.btn--primary {
    background-color: var(--c-accent-texte);
    color: var(--c-accent-contrast);
}

.btn--primary:hover {
    background-color: var(--c-accent-hover);
    color: var(--c-accent-contrast);
}

/* Le primaire INVERSÉ — mêmes couleurs, rôles échangés : le doré passe au texte et au contour, le fond
   reste transparent (celui de `.btn`) ; au survol, il redevient le primaire. Pour une seconde sortie
   posée à côté d'un primaire, qu'on DISTINGUE sans la rétrograder (Alexandre, 25 septembre 2026 : la
   confirmation de réservation). */
.btn--inverse {
    color: var(--c-accent-texte);
    border-color: var(--c-accent-texte);
}

.btn--inverse:hover {
    background-color: var(--c-accent-texte);
    color: var(--c-accent-contrast);
}

.btn--ghost {
    background-color: transparent;
    color: var(--c-text);
    border-color: var(--c-border);
}

.btn--ghost:hover {
    border-color: var(--c-accent-texte);
    color: var(--c-accent-texte);
}

.btn--lg {
    padding: 0.95rem 1.9rem;
    font-size: 1rem;
}

/* Bouton compact (contrôles de tableaux denses). */
.btn--sm {
    padding: 0.35rem 0.7rem;
    font-size: 0.78rem;
    letter-spacing: 0.5px;
}

.btn.is-disabled {
    opacity: 0.5;
    cursor: not-allowed;
    pointer-events: none;
}

/* Bouton NATIVEMENT désactivé (espace client : « Déplacer »/« Annuler » quand
   le moteur a fermé l'action). Il reste VISIBLE — l'explication vit à côté,
   pas dans un title — et garde son relief de bouton pour qu'on comprenne que
   l'action existe, mais pas maintenant. Pas de pointer-events:none ici : le
   navigateur bloque déjà le clic, et le curseur « interdit » informe. */
.btn:disabled {
    opacity: 0.45;
    cursor: not-allowed;
}

.btn:disabled:hover {
    transform: none;
    border-color: var(--c-border);
    color: inherit;
}

/* ---- Header sticky ---- */
/* ⚠️⚠️ CE FOND DÉRIVE, IL NE RECOPIE PAS — 10 septembre 2026.
   Il valait `rgba(18, 18, 18, 0.92)`, c'est-à-dire le `#121212` du thème SOMBRE
   écrit en dur. Le texte du menu, lui, suivait le thème : sur `clair` il devenait
   `#241f17` et se peignait sur une barre restée quasi noire — **1,07:1**, mesuré
   au point unique du moteur, quand la même barre dérivée rend 16,36:1.
   La barre encadre TOUTES les pages : le site entier devenait inutilisable, et
   *aucun test de ce dépôt ne mesure un pixel*.

   ⚠️ `in srgb` EST LA MOITIÉ DE LA PROMESSE, comme pour `--hero-overlay` :
   `color-mix()` interpole dans l'espace qu'on lui NOMME, et le défaut de CSS
   (`oklab`) rend des valeurs différentes de celles que le moteur calcule.

   ⚠️ SUR LE THÈME SOMBRE, LA VALEUR EST IDENTIQUE À CELLE D'AVANT — c'est ce qui
   rend la conversion sûre : `--c-bg` y vaut `#121212`, donc le site de ZigZag ne
   bouge pas d'un pixel. */
.site-header {
    position: sticky;
    top: 0;
    z-index: 50;
    background-color: color-mix(in srgb, var(--c-bg) 92%, transparent);
    backdrop-filter: blur(8px);
    border-bottom: 1px solid var(--c-border);
}

.site-header__inner {
    display: flex;
    align-items: center;
    justify-content: space-between;
    gap: 1rem;
    min-height: var(--header-h);
    /* ⚠️ PAS DE `padding-block` ICI — ESSAYE ET MESURE LE 1er SEPTEMBRE 2026.
       L'intention etait de donner de l'air aux rangees quand la barre passe a
       la ligne (320 px et 640 px, ou les boutons touchent le fond peint).
       Mais `box-sizing: border-box` fait entrer le padding DANS `min-height`,
       et le logo occupe deja 72 px des 80 px de `--header-h` : 0,4 rem en haut
       et en bas faisaient passer l'en-tete de 80 a 86 px A TOUTES LES LARGEURS,
       desktop compris. Deux cas de repli ne valent pas une regression partout. */
    /* UNE SEULE LIGNE, quelle que soit la largeur. Sans ce `nowrap`, la barre
       se réorganisait dès qu'elle manquait de place : l'en-tête passait de
       72 px à 96 px de haut et le tout perdait son alignement. Ce qui ne tient
       pas est RETIRÉ par les media queries plus bas, jamais renvoyé à la ligne. */
    flex-wrap: nowrap;
}

/* ═══ LA BARRE PEUT DÉPASSER LA COLONNE DE TEXTE, ET ELLE LE DOIT ═══
   Le lot signalait « Bonjour Alex… » tronqué « alors que la place existe ».
   La place existe en effet — mais PAS là où on la cherchait, et le compte est
   sans appel. Mesuré à 1440 px, personnel connecté :

     .site-header__inner ... 1100 px (le plafond de .container), 1060 utiles
     logo ....................  61 px
     nav (5 liens) ........... 470 px
     actions (4 boutons) ..... 497 px
     2 gouttières de 16 px ...  32 px
                               ────────
                               1060 px — pile le budget, à zéro près

   La salutation est le SEUL enfant qui accepte de se comprimer (c'est écrit
   plus bas, et c'est voulu : un bouton comprimé coupe son texte). Elle absorbe
   donc tout le déficit à elle seule et tombe à 12 px — la largeur des points
   de suspension, rien d'autre. Il lui manque exactement 92 px.

   ⚠️ ET CE N'EST PAS UNE AFFAIRE DE GRAND ÉCRAN : le résultat est IDENTIQUE à
   1101, 1200, 1440 et 1920 px, parce que `.container` plafonne à 1100 px et
   ne grandit jamais. À 1920 px, 820 px de fenêtre restaient inutilisés pendant
   que le prénom était réduit à trois points. C'est ça, le défaut.

   La barre n'est pas une colonne de texte : son fond va déjà d'un bord à
   l'autre. On lui rend donc la largeur de la fenêtre au-delà de 1100 px, dans
   la limite de 1400 px — assez pour les 92 px manquants (+300 px utiles à
   1440), sans laisser le logo partir se coller au bord sur un très grand
   écran. Le CONTENU des pages, lui, garde ses 1100 px : rien d'autre ne bouge.

   Les points de suspension restent en place (voir .site-header__user) : ils
   cessent d'être la règle et redeviennent ce qu'ils auraient dû rester, un
   filet pour un prénom vraiment long.

   ⚠️ LE `max(1100px, …)` N'EST PAS DÉCORATIF. Écrit `min(100% - 2.5rem,
   1400px)` tout court, il RÉTRÉCIT la barre à 1061 px sur une fenêtre de
   1101 px — juste sous le plafond qu'il était censé lever — et la salutation
   y passait de 12 px à 0. Mesuré, et corrigé : le plancher garantit que cette
   règle ne peut qu'ajouter de la place, jamais en retirer. */
@media (min-width: 1101px) {
    .site-header__inner {
        max-width: max(1100px, min(100% - 2.5rem, 1400px));
    }
}

.brand {
    display: flex;
    align-items: center;
    gap: 0.65rem;
    line-height: 1.05;
    color: var(--c-text);
}

.brand__text {
    display: flex;
    flex-direction: column;
}

/* Logo image du header — seul et agrandi depuis le lot 2 (le bloc texte
   ne s'affiche qu'en repli, sans logo uploadé). */
/* ═══ LA LARGEUR DU LOGO EST BORNEE — ELLE NE L'ETAIT PAR RIEN ═══
   `height` est fixe et `width: auto` : la largeur rendue est donc dictee par le
   RAPPORT DE FORME DE L'IMAGE QUE LE GERANT TELEVERSE. Le `max-width: 100%` de
   la regle `img` d'app.css ne borne rien ici — 100 % d'un conteneur flexible,
   c'est-a-dire de la place que le logo prend deja.

   ⚠️ C'EST UNE LARGEUR QUI VIENT DES DONNEES, PAS DU CODE : elle ne se voit
   sur aucun poste de dev, et elle change d'un salon a l'autre.

   ⚠️ ET VOICI OU CE PLAFOND SERT VRAIMENT — mesure, pas intention, parce que
   la premiere justification ecrite ici etait FAUSSE et aurait fait autorite :
     · logo 2,5:1 (celui du banc) ... aucun effet, la barre tient sans lui ;
     · logo 4:1 ..................... aucun effet MESURABLE non plus : hauteur
       et nombre de rangees identiques avec et sans. `.brand` a `flex-shrink: 1`,
       il cede donc de lui-meme, et le `wrap` de la barre d'actions absorbe le
       reste ;
     · logo 10:1 .................... LA il mord, et fort : sans plafond la page
       deborde de 131 a 221 px selon la largeur ; avec, de 0.
   Autrement dit ce n'est PAS lui qui corrige le defaut signale (c'est le `wrap`
   de `.site-header__actions`) : c'est la borne du cas extreme, celui d'un logo
   tres allonge qu'aucun test de ce depot ne peut voir. Ne pas lui preter le
   merite du correctif, et ne pas le retirer en le croyant decoratif.

   Le plafond est en `vw` et non en pixels : il suit l'ecran au lieu d'etre un
   nombre juste sur l'appareil ou on l'a calcule. `object-fit: contain` garde le
   rapport de forme quand le plafond mord — sans lui, `height` fixe et largeur
   contrainte ecraseraient l'image.

   ⚠️ NE PAS REMPLACER PAR UNE VALEUR EN PIXELS : ce serait retomber dans le
   budget de pixels tenu a la main, qui a echoue trois fois sur cette barre. */
.brand__logo {
    display: block;
    height: 72px;
    width: auto;
    max-width: 34vw;
    object-fit: contain;
    border-radius: 4px;
    flex-shrink: 0;
}

/* Monogramme placeholder du logo (header, login, admin). */
.logo-mark {
    display: inline-flex;
    align-items: center;
    justify-content: center;
    width: 56px;
    height: 56px;
    border: 2px solid var(--c-accent-interface);
    border-radius: 50%;
    font-family: var(--police-titre);
    font-size: 1.4rem;
    font-weight: 700;
    letter-spacing: 0.05em;
    color: var(--c-accent-texte);
    flex-shrink: 0;
}

.logo-mark--sm {
    width: 38px;
    height: 38px;
    font-size: 0.95rem;
}

.brand__name {
    font-family: var(--police-titre);
    font-weight: 700;
    font-size: 1.35rem;
    letter-spacing: 1px;
    text-transform: uppercase;
}

.brand__tag {
    font-size: 0.68rem;
    letter-spacing: 2px;
    text-transform: uppercase;
    color: var(--c-accent-texte);
}

.main-nav {
    display: flex;
    gap: 1.1rem;
    /* La nav ne se comprime pas au point de casser ses libellés : « Le
       concept » et « Notre adresse » se cassaient en deux lignes bien avant
       que la place manque vraiment. */
    flex: 0 0 auto;
}

.main-nav a {
    color: var(--c-text);
    font-size: 0.92rem;
    font-weight: 500;
    white-space: nowrap;

    /* ═══ DEUX DE CES LIBELLÉS SONT ÉCRITS PAR LE GÉRANT ═══
       Depuis le 15/08/2026, « concept » et « réalisations » affichent
       `textes.surtitreConcept` et `textes.surtitreRealisations`. Le plafond de
       100 caractères a été fixé côté moteur pour des <h2>, à un moment où
       personne ne prévoyait qu'un MENU les consommerait : un surtitre de 100
       caractères, avec `white-space: nowrap` et une nav qui ne se comprime pas,
       pousse la barre d'actions hors de l'en-tête.

       LA COUPURE EST ICI, ET NULLE PART AILLEURS. Pas de champ en base, pas de
       contrainte moteur, pas de `|slice` dans le gabarit : le <h2> de la
       section reste ENTIER là où il compte, et seule l'entrée de menu s'arrête.
       Une seule source, un seul texte, deux rendus.

       ET LE TEXTE COMPLET RESTE DANS LE DOM — c'est l'avantage d'une coupure
       CSS sur une troncature en PHP : un lecteur d'écran lit le libellé entier,
       une recherche navigateur le trouve, et rien n'est perdu à la copie.

       16ch : « Notre travail » (13) et « À propos » (8), les deux défauts,
       passent au large ; un libellé raisonnable de gérant aussi. Au-delà, les
       points de suspension font leur travail. */
    max-width: 16ch;
    overflow: hidden;
    text-overflow: ellipsis;
}

.main-nav a:hover {
    color: var(--c-accent-texte);
}

.site-header__actions {
    display: flex;
    align-items: center;
    gap: 0.6rem;
    /* Le bloc peut se comprimer, mais UN SEUL de ses enfants cède : la
       salutation (voir .site-header__user). Les boutons, eux, gardent leur
       taille — un bouton comprimé ne rétrécit pas son texte, il le coupe. */
    flex: 0 1 auto;
    min-width: 0;

    /* ⚠️ CES TROIS LIGNES SONT LE CORRECTIF DU DÉBORDEMENT — voir le pavé
       juste sous ce bloc pour la mesure et le motif. Elles vivent ICI, dans le
       bloc du sélecteur, et non dans un second bloc plus bas : une feuille qui
       répète un sélecteur laisse croire à chaque lecteur qu'il a vu la règle
       entière alors qu'il n'en a vu qu'un morceau. */
    flex-wrap: wrap;
    justify-content: flex-end;
    row-gap: 0.4rem;
}

.site-header__actions .btn,
.site-header__logout {
    flex-shrink: 0;
}

/* ═══ CE QUI NE TIENT PAS PASSE A LA LIGNE — ET NE SORT PLUS DE L'ECRAN ═══
   (1er septembre 2026, signalement d'Alexandre : « le bouton de deconnexion
   depasse du telephone ».)

   LE MECANISME, MESURE, et il rend inevitable ce qui arrivait :
   `.site-header__actions` a le droit de retrecir (`flex: 0 1 auto`), mais
   AUCUN de ses enfants ne l'a (`flex-shrink: 0`, juste au-dessus, et c'est
   voulu — un bouton comprime coupe son texte). Le conteneur se reduisait donc
   a 228 px pendant que son contenu en reclamait 353, et rien ne retenait la
   difference : « Deconnexion » sortait jusqu'a 105 px hors de l'ecran, et
   TOUTE la page se mettait a defiler de cote.

   ⚠️ POURQUOI LE RABOTAGE DE PADDING A ECHOUE TROIS FOIS — c'est la lecon, pas
   l'anecdote. Les correctifs precedents ont resserre a 1040 px, puis a 560, puis
   a 380, chacun mesure et juste A SA LARGEUR. Mesure du 1er septembre sur douze
   largeurs : ca debordait a 320, 360, 390, 412, 430, 480 ET 640 px — 640 allant
   plus mal que 560. Le deficit reel est de 81 a 125 px : aucun padding ne le
   rend. Un budget de pixels tenu a la main est juste le jour ou on le calcule,
   et faux des qu'on ajoute un bouton ou qu'on regarde entre deux points de
   rupture.

   ⚠️ LA PROPRIETE GARDEE ICI N'EST PAS « CA TIENT A 390 PX », C'EST « CA NE
   PEUT PAS DEPASSER ». Avec `flex-wrap: wrap`, un bouton qui ne tient pas
   descend d'une ligne : la barre GRANDIT au lieu de DEBORDER. C'est vrai a
   toute largeur, avec n'importe quel libelle, et ca le restera le jour ou
   quelqu'un ajoutera une cinquieme entree — ce que le reglage fin, lui, ne
   promet pas.

   Le reste (libelles courts, logo plafonne, plus bas) ne sert plus qu'a ce que
   la barre tienne JOLIMENT sur une ligne dans les cas courants. C'est du
   confort pose sur une structure sure, jamais l'inverse.

   ⚠️ LES DECLARATIONS ELLES-MEMES SONT DANS LE BLOC `.site-header__actions`
   PLUS HAUT — ce commentaire porte le motif, pas la regle. Gardees par
   `tests/Gabarits/GardeBarreNeDeborddePasTest`, qui verifie qu'elles n'ont pas
   disparu. Cette garde LIT LA FEUILLE : elle ne mesure aucun pixel et ne peut
   donc pas dire que la barre tient a 390 px — seul un vrai telephone le dit. */

/* UN LIBELLÉ NE SE COUPE JAMAIS. « Mes rendez-vous » se cassait en trois
   lignes dès que la barre manquait de place : le bouton devenait un pavé,
   l'en-tête enflait de 72 à 96 px et l'ensemble perdait son alignement.
   La règle valait déjà sous 560 px ; elle vaut désormais partout.

   Et des boutons PLUS COMPACTS que ceux des pages : la barre en aligne
   jusqu'à cinq (« Espace salon » en plus pour le personnel) dans les 1060 px
   utiles du .container. Au gabarit des pages, l'ensemble réclamait 130 px de
   plus que la place disponible — et les derniers boutons se faisaient
   simplement couper, sans que rien ne le dise. */
.site-header__actions .btn {
    white-space: nowrap;
    padding: 0.5rem 0.9rem;
    font-size: 0.82rem;
    letter-spacing: 0.5px;
}

/* Salutation du client connecté + formulaire de déconnexion (T2).
   C'EST LA SEULE ENTRÉE DE LARGEUR IMPRÉVISIBLE de la barre : elle porte un
   prénom, et un prénom peut être long (le repli sur la partie locale d'une
   adresse donnait « Bonjour Alexandrebergeron12 »). Elle est donc la seule à
   céder quand la place manque — abrégée par des points de suspension plutôt
   que de pousser les boutons hors de l'écran. */
.site-header__user {
    color: var(--c-text-muted);
    font-size: 0.9rem;
    white-space: nowrap;
    flex: 0 1 auto;
    min-width: 0;
    overflow: hidden;
    text-overflow: ellipsis;
}

.site-header__logout {
    margin: 0;
}

/* ---- Footer ---- */
/* ⚠️⚠️ MÊME FAMILLE QUE LA BARRE, ET IL FALLAIT LE CHERCHER — 10 septembre 2026.
   Il valait `#0d0d0d` en dur, pendant que sa couleur de texte suivait le thème :
   sur `clair`, `--c-text-muted` (#6d6559) se peignait sur un pied resté noir —
   **3,38:1**, sous le seuil de 4,5, contre 5,04:1 une fois dérivé.
   *Une garde écrite en même temps qu'un correctif regarde ce que le correctif a
   touché, jamais ce qu'il a oublié* : la barre avait été signalée, le pied non —
   et ce sont les deux éléments qui encadrent toutes les pages du site.

   ⚠️⚠️ CE SITE-LÀ CHANGE LE RENDU D'AUJOURD'HUI, ET C'EST LE SEUL DES QUATRE.
   `#0d0d0d` est cinq niveaux plus sombre que `--c-bg` (#121212) sur le thème
   sombre ; le pied passe donc de 13 à 18 sur 255. Ce qui sépare le pied du corps
   n'est pas cet écart — c'est le `border-top` juste en dessous, et il ne bouge
   pas. Écrire `color-mix(… 72%, #000)` rendrait bien `#0d0d0d` à l'octet près,
   mais peindrait un pied GRIS MOYEN sur un site clair : *on ne recopie pas une
   valeur pour préserver un écart qu'aucun œil ne voit.* */
.site-footer {
    background-color: var(--c-bg);
    border-top: 1px solid var(--c-border);
    padding: 3rem 0 1.5rem;
    color: var(--c-text-muted);
}

.site-footer__inner {
    display: grid;
    grid-template-columns: repeat(3, 1fr);
    gap: 2rem;
}

.site-footer__brand {
    font-family: var(--police-titre);
    font-size: 1.3rem;
    color: var(--c-text);
    text-transform: uppercase;
    letter-spacing: 1px;
    margin-bottom: 0.25rem;
}

.site-footer__col a {
    display: block;
    color: var(--c-text-muted);
    margin-bottom: 0.4rem;
}

.site-footer__col a:hover {
    color: var(--c-accent-texte);
}

.site-footer__bottom {
    margin-top: 2rem;
    padding-top: 1.25rem;
    border-top: 1px solid var(--c-border);
    text-align: center;
    font-size: 0.85rem;
}

/* ---- Responsive header / footer ---- */
@media (max-width: 860px) {
    .main-nav {
        display: none; /* nav simplifiée en mobile pour la démo phase 1 */
    }
    .site-footer__inner {
        grid-template-columns: 1fr;
        gap: 1.5rem;
        text-align: center;
    }
}

/* BARRE D'ACTIONS QUI DÉBORDAIT, sur DEUX plages distinctes — le défaut ne se
   limitait pas au mobile. Une fois le client CONNECTÉ, la barre compte deux
   entrées de plus (« Bonjour X » et « Déconnexion ») et réclame 616 px :
     - à 390 px, elle demandait 486 px pour 331 px utiles ;
     - entre 862 px et ~1030 px, la nav principale vient de réapparaître
       (324 px) et le total dépasse encore la largeur disponible.
   Dans les deux cas TOUTE la page héritait d'un défilement horizontal.

   On retire les entrées qui font DOUBLE EMPLOI, plutôt que de rétrécir le
   reste jusqu'à l'illisible ou de repousser la nav :
     - « Bonjour X » : la page le redit en titre, juste dessous.
   Elle revient au-delà, où la place existe réellement.

   ⚠️ « ACCUEIL » A ÉTÉ RETIRÉ DU GABARIT LE 7 SEPTEMBRE 2026 (lot B-1) — pour
   la raison même qui le masquait ici : le logo, à gauche, EST déjà ce lien.
   Le sélecteur qui le cachait est parti avec lui ; le laisser aurait fait une
   règle CSS sans élément, que rien ne signale.

   ⚠️ SEUIL PORTÉ DE 1040 À 1100 PX (lot d'usage, mesuré). Le calcul ci-dessus
   ignorait « Espace salon », qui n'existait pas encore : pour un membre du
   personnel connecté, la barre réclame 1021 px là où 1040 px de fenêtre n'en
   laissent que 1001 utiles (.container = 1100 px moins ses marges). À 1101 px
   de fenêtre, les 1061 px utiles suffisent — d'où ce seuil, qui n'est pas une
   valeur ronde mais la mesure. */
@media (max-width: 1100px) {
    .site-header__user {
        display: none;
    }

    /* ═══ LA NAV CÈDE PLUS TÔT QUAND ON EST CONNECTÉ ═══
       Mesuré : entre 861 px (réapparition de la nav) et 1040 px, un membre du
       personnel connecté aligne quatre boutons — « Espace salon », « Mes
       rendez-vous », « Déconnexion », « Réserver » — et il manque jusqu'à
       143 px. Les entrées ci-dessus sont déjà retirées, il ne reste que la nav.
       Elle disparaît donc dès 1100 px pour qui est connecté, au lieu de 860 px,
       exactement comme elle le fait déjà pour tout le monde en dessous.
       Ses cinq liens sont des ancres de la page d'accueil, où le logo mène. */
    .site-header--connecte .main-nav {
        display: none;
    }
}

/* Libellé long par défaut ; le court n'existe que sous 560 px (voir plus bas).
   Jamais les deux à la fois : `display: none` les retire du rendu ET de la
   lecture d'écran. */
.btn__court { display: none; }

@media (max-width: 560px) {
    .btn__long { display: none; }
    .btn__court { display: inline; }

    .brand__tag { display: none; }
    .site-header__actions .btn--primary { display: none; }
    /* Logo réduit pour laisser la place aux actions (header 72px, cf. themes.css). */
    .brand__logo { height: 56px; }

    .site-header__actions {
        gap: 0.5rem;
    }

    /* Libellés sur UNE ligne : à la taille normale, « Mes rendez-vous » se
       cassait en trois lignes et étirait la hauteur de l'en-tête. */
    .site-header__actions .btn {
        padding: 0.45rem 0.75rem;
        font-size: 0.78rem;
        letter-spacing: 0.5px;
        white-space: nowrap;
    }
}

/* ═══ LE RESSERREMENT VAUT SUR TOUTE LA BANDE TELEPHONE, PLUS SEULEMENT SOUS
   380 PX ═══ (1er septembre 2026)

   Il était borné à 380 px — la largeur d'un iPhone SE, mesurée à l'époque où
   « Déconnexion » s'y faisait couper. Depuis que la barre passe à la ligne au
   lieu de déborder, cette borne ne coupe plus rien : elle fabrique une MARCHE.

   Mesuré avant ce changement, personnel connecté : header de 73 px à 360 px de
   large, et de 120 px à 390 px. Le même écran, 30 px plus large, et un bandeau
   collant qui enfle de moitié — parce qu'à 381 px les libellés reprennent leur
   taille pleine et passent de deux rangées à trois.

   La borne à 380 px n'était donc pas la frontière d'un défaut, c'était la
   largeur de l'appareil qu'on avait sous la main. On l'aligne sur 560 px, qui
   est la vraie frontière du produit — celle où les libellés courts prennent le
   relais et où « Réserver » se retire. Une bande, un comportement.

   ⚠️ NE PAS LA REDESCENDRE POUR « RENDRE DE LA PLACE » AUX GRANDS TELEPHONES :
   c'est exactement ce qui a produit la marche. */
@media (max-width: 560px) {
    .brand__logo { height: 44px; }

    .site-header__actions {
        gap: 0.35rem;
    }

    .site-header__actions .btn {
        padding: 0.4rem 0.5rem;
        font-size: 0.72rem;
        letter-spacing: 0;
    }
}

/* ---- Messages flash ---- */
.flash {
    margin: 1.25rem 0 0;
    padding: 0.75rem 1rem;
    border-radius: var(--radius);
    border: 1px solid var(--c-border);
    background-color: var(--c-bg-elev);
    font-size: 0.95rem;
}

/* Un message suivi d'un bloc garde son souffle : `.flash` bat la marge de `p`
   (1rem en bas), et le formulaire qui suit touchait le cadre (connexion,
   13 septembre 2026). Pas sur le dernier enfant — un conteneur n'a pas à
   gagner un vide sous son message. */
.flash:not(:last-child) {
    margin-bottom: 1.25rem;
}

.flash--warning {
    border-color: rgba(201, 161, 74, 0.6);
    color: var(--c-accent-hover, var(--c-accent));
}

.flash--success {
    border-color: rgba(90, 160, 90, 0.6);
    color: #8fce8f;
}

/* AVERTISSEMENT ≠ REFUS (lots O10/O12).
   Le moteur renvoie des `avertissements[]` sur des opérations qui ont RÉUSSI :
   « ce créneau chevauche un rendez-vous », « hors des horaires d'ouverture »,
   « ce rendez-vous est déjà honoré, son prix pratiqué passe de X € à Y € ». Le board
   les rendait avec l'étiquette 'warning', celle de ses refus — un constat sur
   une écriture aboutie avait donc exactement l'aspect d'un échec, et le gérant
   ne pouvait pas savoir si son rendez-vous existait.
   D'où une étiquette et un style à part : cadre neutre, pas de teinte d'alerte,
   et un préfixe « À savoir » qui dit en un mot que rien n'a échoué. */
.flash--avertissement {
    border-color: var(--c-border);
    border-left: 3px solid var(--c-accent-interface);
    color: var(--c-text-muted);
}

.flash--avertissement::before {
    content: "À savoir — ";
    font-weight: 600;
    color: var(--c-accent-texte);
}

/* =========================================================================
   LES ANNONCES — le composant UNIQUE des messages du sac à flashes.
   Voir templates/_annonces.html.twig pour le pourquoi et le relevé complet.

   ⚠️ `.flash` CI-DESSUS N'EST PAS REMPLACÉ. Il sert les messages d'ÉTAT DE
   PAGE, qui doivent rester DANS le flux, à l'endroit qu'ils commentent
   (« Accès refusé », « le moteur ne répond pas »). Les 74 usages de `.flash`
   dans 34 gabarits sont ceux-là. `.annonce` ne traite que le TRANSITOIRE.

   FLOTTANT, DONC NE POUSSE RIEN. C'est la demande : le message n'ouvre plus
   d'espace vide entre l'en-tête et la page d'accueil, et ne colle plus au
   titre de l'agenda. Aucun écran n'a plus à lui ménager une place.
   ========================================================================= */
.annonces {
    position: fixed;
    /* ═══ EN BAS, ET SUR TOUS LES ÉCRANS ═══
       Le premier essai posait la pile SOUS l'en-tête (`top: --header-h`). Elle
       recouvrait alors la navigation du board : mesuré, la vitrine empile
       81 px d'en-tête quand le board en empile 168 (barre 0–70 + navigation
       70–168), et `--header-h` ne connaît que la première. Il aurait fallu une
       valeur par gabarit — donc deux traitements pour le même besoin, ce que ce
       composant existe précisément pour supprimer.

       En bas, la question ne se pose plus : aucun en-tête à contourner, aucune
       hauteur à tenir à jour, et le même réglage du téléphone à l'écran large.
       C'est aussi là que le pouce arrive sur un téléphone. */
    bottom: 1rem;
    right: 1rem;
    /* Au-dessus de tout : de l'en-tête (50), des modales de l'agenda (100) et
       de son popover de déplacement (120). Une confirmation qui arrive pendant
       qu'une modale est ouverte doit se voir. */
    z-index: 200;
    display: flex;
    flex-direction: column;
    gap: 0.5rem;
    width: min(26rem, calc(100% - 2rem));
    /* La pile ne capte pas le pointeur : on peut cliquer la page AUTOUR des
       messages, y compris dans la colonne qu'ils occupent. Chaque annonce le
       reprend pour elle-même (voir .annonce). */
    pointer-events: none;
}

.annonce {
    pointer-events: auto;
    display: flex;
    align-items: flex-start;
    gap: 0.75rem;
    padding: 0.75rem 1rem;
    border-radius: var(--radius);
    border: 1px solid var(--c-border);
    /* Opaque, et non pas la surface translucide du reste : ce bloc se pose
       par-dessus du contenu quelconque, il doit rester lisible dessus. */
    background-color: var(--c-bg-elev);
    box-shadow: 0 6px 24px rgba(0, 0, 0, 0.45);
    font-size: 0.95rem;
}

.annonce__texte {
    margin: 0;
    flex: 1 1 auto;
    /* Un message long ne doit ni déborder ni couper un mot en deux. */
    min-width: 0;
    overflow-wrap: break-word;
}

/* La croix n'existe que si le JS peut la servir (voir annonces.js). */
.annonce__fermer {
    display: none;
    flex-shrink: 0;
    padding: 0 0.25rem;
    border: 0;
    background: none;
    color: inherit;
    font-size: 1.25rem;
    line-height: 1;
    cursor: pointer;
    opacity: 0.7;
}

.annonces--vivantes .annonce__fermer {
    display: block;
}

.annonce__fermer:hover,
.annonce__fermer:focus-visible {
    opacity: 1;
}

/* Les teintes sont celles de `.flash`, à l'identique : un refus n'a pas à
   changer de couleur parce qu'il a changé de place. */
.annonce--warning {
    border-color: rgba(201, 161, 74, 0.6);
    color: var(--c-accent-hover, var(--c-accent));
}

.annonce--success {
    border-color: rgba(90, 160, 90, 0.6);
    color: #8fce8f;
}

.annonce--avertissement {
    border-left: 3px solid var(--c-accent-interface);
    color: var(--c-text-muted);
}

/* Le préfixe est REPRIS TEL QUEL de .flash--avertissement : c'est lui qui dit
   en un mot que rien n'a échoué. Il vit sur le TEXTE et non sur la boîte, qui
   est désormais un conteneur flex (un ::before y deviendrait un enfant flex,
   posé à côté du texte au lieu de le précéder). */
.annonce--avertissement .annonce__texte::before {
    content: "À savoir — ";
    font-weight: 600;
    color: var(--c-accent-texte);
}

/* Entrée et sortie. Discrètes : ce n'est pas un événement, c'est une réponse
   à ce qu'on vient de faire. */
@media (prefers-reduced-motion: no-preference) {
    .annonce {
        animation: annonce-entree 200ms ease-out;
    }

    .annonce--partie {
        transition: opacity 220ms ease, transform 220ms ease;
        opacity: 0;
        transform: translateX(1rem);
    }
}

/* Le mouvement rend certaines personnes malades : sans animation, la sortie
   reste instantanée et le message disparaît quand même. */
@media (prefers-reduced-motion: reduce) {
    .annonce--partie {
        opacity: 0;
    }
}

@keyframes annonce-entree {
    from {
        opacity: 0;
        transform: translateX(1rem);
    }
}

/* TÉLÉPHONE : pleine largeur, collé aux bords. Une colonne de 26 rem sur un
   écran de 390 px laisserait une gouttière inutile d'un côté et rognerait le
   texte de l'autre. */
@media (max-width: 560px) {
    .annonces {
        /* Pleine largeur : une colonne de 26 rem sur un écran de 390 px
           laisserait une gouttière inutile d'un côté et rognerait le texte de
           l'autre. Seuls les bords changent — la pile est déjà en bas. */
        bottom: 0.75rem;
        left: 0.5rem;
        right: 0.5rem;
        width: auto;
    }
}

/* Variante destructrice publique (déconnexion, annulation) — même teinte
   que .btn--danger de l'admin. */
.btn--danger {
    color: #d66;
}

.btn--danger:hover {
    border-color: #d66;
    color: #d66;
}

/* Action destructrice PRINCIPALE (confirmer l'annulation d'un rendez-vous) :
   le rouge doit être le FOND, pas le texte. Cumulé à .btn--primary, qui pose
   un fond doré, le `color: #d66` ci-dessus donnait du rouge sur or —
   illisible. Rouge repris de .badge--annulee, pour que « annulé » ait la même
   couleur partout. */
.btn--primary.btn--danger,
.btn--primary.btn--danger:hover {
    background-color: #b02226;
    border-color: #b02226;
    color: #fff;
}

.btn--primary.btn--danger:hover {
    background-color: #8e1b1e;
    border-color: #8e1b1e;
}

/* Enveloppe de tableau : défilement horizontal propre sur petits écrans
   (le tableau scrolle dans son cadre au lieu de faire déborder la page).

   `position: relative` N'EST PAS décoratif : sans lui, l'enveloppe ne
   découpait PAS ses descendants en position absolue, et la page entière
   héritait d'un défilement horizontal.

   Le cas réel : le <th> de la colonne d'action de « Mes rendez-vous » porte
   un `<span class="visually-hidden">Avis</span>` ; `.visually-hidden` est en
   `position: absolute` (app.css). Un élément absolu n'est découpé par un
   `overflow` ancêtre QUE si cet ancêtre est son bloc conteneur. L'enveloppe
   étant en `position: static`, le bloc conteneur du span était le bloc
   conteneur INITIAL : le span échappait au découpage et se posait à sa
   position statique — c'est-à-dire dans le tableau NON défilé, large de
   481 px. À 390 px de viewport il atterrissait à x = 414, et le document
   mesurait 415 px. D'où 25 px de défilement parasite à 390 px, 95 px à
   320 px, sur l'écran le plus consulté au téléphone.

   Rendre l'enveloppe positionnée la remet dans son rôle : tout ce qu'elle
   contient, y compris les éléments absolus, reste découpé par ELLE. */
.table-scroll {
    position: relative;
    overflow-x: auto;
}

/* Zone d'action destructrice (droit à l'effacement RGPD sur « Mes données »). */
.zone-danger {
    margin-top: 2.5rem;
    padding: 1.25rem 1.5rem;
    border: 1px solid rgba(221, 102, 102, 0.4);
    border-radius: 8px;
    background-color: rgba(221, 102, 102, 0.05);
}

.zone-danger__titre {
    font-size: 1.1rem;
    color: #d66;
    margin: 0 0 0.5rem;
}

.zone-danger p {
    color: var(--c-text-muted);
    font-size: 0.9rem;
    margin: 0 0 1rem;
}

/* =========================================================================
   Œil afficher/masquer un mot de passe (JS: assets/password_toggle.js)
   Le wrapper .pw-toggle est injecté autour de chaque <input type=password>.
   ========================================================================= */
.pw-toggle {
    position: relative;
    display: block;
}

.pw-toggle__btn {
    position: absolute;
    top: 0;
    right: 0;
    height: 100%;
    width: 2.75rem;
    display: flex;
    align-items: center;
    justify-content: center;
    padding: 0;
    background: none;
    border: none;
    color: var(--c-text-muted);
    cursor: pointer;
    transition: color 0.18s ease;
}

.pw-toggle__btn:hover,
.pw-toggle__btn[aria-pressed="true"] {
    color: var(--c-accent-texte);
}

.pw-toggle__btn:focus-visible {
    outline: 2px solid var(--c-accent);
    outline-offset: -2px;
    border-radius: var(--radius);
}

/* =========================================================================
   Message de politique d'annulation (espace client + tunnel de réservation).
   ========================================================================= */
.policy-note {
    display: flex;
    align-items: flex-start;
    gap: 0.6rem;
    margin: 1rem 0;
    padding: 0.7rem 0.9rem;
    background: rgba(201, 161, 74, 0.08);
    border: 1px solid rgba(201, 161, 74, 0.35);
    border-left: 3px solid var(--c-accent-interface);
    border-radius: var(--radius);
    color: var(--c-text);
    /* ⚠️ RÉDUIT LE 5 SEPTEMBRE 2026 (demande de ZigZag : « les messages
       d'info jaunes en plus petit »). Le changement est GLOBAL, pas borné
       au tunnel : `.policy-note` porte aussi la règle d'annulation dans
       l'espace client et sur l'écran d'annulation, et n'en rétrécir qu'un
       ferait diverger le même encart d'un écran à l'autre — le défaut
       coûte plus cher que le gain. */
    font-size: 0.82rem;
    line-height: 1.45;
}

.policy-note__icon {
    flex-shrink: 0;
    width: 1.15rem;
    height: 1.15rem;
    display: inline-flex;
    align-items: center;
    justify-content: center;
    border-radius: 50%;
    background: var(--c-accent);
    color: var(--c-accent-contrast);
    font-family: var(--police-titre);
    font-weight: 700;
    font-size: 0.85rem;
    line-height: 1;
}

/* --- Champ « code à 6 chiffres » (C2) ----------------------------------- */
/* Partagé par la page client (/verification-email, habillage .client-form du
   site) et la page staff (/invitation, habillage .login-box du board) : les
   deux chargent components.css, et un code se saisit de la même façon des
   deux côtés. Chiffres espacés et centrés — on relit un code caractère par
   caractère, en le comparant à celui affiché dans l'e-mail. */
.champ-code {
    text-align: center;
    letter-spacing: 0.4em;
    /* Le letter-spacing ajoute un vide APRÈS le dernier chiffre : cette
       indentation le compense pour que la saisie reste optiquement centrée. */
    text-indent: 0.4em;
    font-size: 1.3rem;
    font-family: var(--police-titre);
}

/* Le placeholder « 123456 » doit s'aligner sur la saisie qui le remplacera
   (même espacement), sans se faire passer pour un code déjà entré. */
.champ-code::placeholder {
    letter-spacing: 0.4em;
    opacity: 0.35;
}

/* ═══ AUCUN CHAMP DE SAISIE SOUS 16 px AU TÉLÉPHONE ═══ (lot 27)

   ⚠️ CE N'EST PAS UNE RÈGLE DE LISIBILITÉ, C'EST LA CORRECTION D'UN DÉFAUT
   D'AFFICHAGE. iOS Safari ZOOME de lui-même dès qu'un `<input>`, `<select>` ou
   `<textarea>` reçoit le focus avec une police sous 16 px — et il ne dézoome
   jamais complètement ensuite : l'écran reste décalé, l'en-tête sort du cadre.
   C'est ce que l'usage a remonté comme « le zoom casse l'affichage ».

   ⚠️ ET CE N'EST PAS LE ZOOM PINCÉ QU'IL FAUT COUPER. `user-scalable=no` et
   `maximum-scale=1` sont IGNORÉS par iOS Safari depuis iOS 10 : les poser ne
   ferait rien là où le problème se produit. Et là où ça marche, ça empêche
   quelqu'un d'agrandir un texte qu'il ne lit pas — un coût réel pour un effet
   nul. On ne le coupe pas. On traite la CAUSE, qui est la taille de police.

   ⚠️ ICI ET PAS DANS `admin.css`. Cette feuille est la seule chargée par les
   DEUX bases (site public et board) : le prochain champ écrit hérite donc de
   la règle sans que son auteur ait à la connaître. La poser dans `admin.css`
   aurait corrigé les instances mesurées et laissé le tunnel public dehors —
   « on corrige l'instance qu'on regarde, pas le défaut ».

   ⚠️ ET UN PLANCHER NE SUFFIT PAS : LA SPÉCIFICITÉ DÉCIDE. Trois règles plus
   précises le battaient en silence ; elles sont passées en
   `@media (min-width: 761px)` dans `admin.css`. C'est
   `GardeChampsSeizePixelsTest` qui l'a mesuré — pas la relecture.

   CE QUI EST EXCLU, ET POURQUOI : case à cocher, bouton radio, sélecteur de
   couleur et champ de fichier N'OUVRENT AUCUN CLAVIER, donc ne déclenchent
   aucun zoom. Les inclure changerait leur mise en page pour rien : ce sont les
   faux positifs connus du balayage, à ne pas « corriger ».

   BORNE : 760 px, comme le reste des règles tactiles du produit. Au-dessus, il
   n'y a pas de doigt, et une police de champ plus fine y est un choix de
   densité légitime. */
@media (max-width: 760px) {
    input:not([type="checkbox"]):not([type="radio"]):not([type="color"]):not([type="file"]),
    select,
    textarea {
        /* En px, DÉLIBÉRÉMENT : le seuil d'iOS est un seuil en PIXELS. Écrit en
           `rem`, il suivrait la taille de police racine et repasserait sous
           16 px chez quelqu'un qui l'a réduite — le défaut reviendrait pour
           ceux-là seuls, c'est-à-dire précisément ceux qui ont besoin qu'on n'y
           touche pas. */
        font-size: 16px;
    }
}

/* ══════════════════════════════════════════════════════════════════════════
   LE GUIDE « AJOUTER À L'ÉCRAN D'ACCUEIL » — S-2, 26 août 2026.

   ⚠️ CACHÉ PAR CETTE CLASSE, JAMAIS PAR L'ATTRIBUT `hidden`. Ce dépôt a payé
   le piège en production : `hidden` n'est qu'un `display: none` de la feuille
   du NAVIGATEUR, que la moindre déclaration d'auteur emporte — et aucun test
   lisant le HTML ne peut le voir, puisque l'attribut y est bien.
   ══════════════════════════════════════════════════════════════════════════ */

.installer-app {
    display: none;
}

/* Posée par le JavaScript, et par lui seul : sans lui on ne sait pas sur quelle
   plateforme on est, donc on n'a rien à expliquer. */
.installer-app.is-visible {
    display: block;
    position: fixed;
    left: 0.75rem;
    right: 0.75rem;
    bottom: 0.75rem;
    z-index: 60;
}

.installer-app__corps {
    display: flex;
    align-items: flex-start;
    gap: 0.9rem;
    padding: 0.9rem 1rem;
    border: 1px solid var(--c-border);
    border-radius: var(--radius);
    /* Opaque : le message passe par-dessus du contenu, il doit rester lisible
       quel que soit ce qu'il recouvre. */
    background: var(--c-bg);
    box-shadow: 0 8px 28px rgba(0, 0, 0, 0.45);
}

.installer-app__texte {
    flex: 1 1 auto;
    margin: 0;
    color: var(--c-text-muted);
    font-size: 0.9rem;
    line-height: 1.55;
}

/* ═══ LA VARIANTE VIDEO DU TUTO *(lot CMS etape 5, 10 septembre 2026)* ═══
   Elle prend la place du texte, dans la meme rangee : `flex` identique, pour
   que le bouton « Masquer » ne bouge pas d un pixel selon le reglage du gerant.

   ⚠️ 9/16 PAR CONSTRUCTION, ET SANS REGLAGE : un tuto d installation se filme
   sur le telephone qu il explique. Proposer un choix de format ici offrirait
   une option dont la bonne reponse est toujours la meme.

   ⚠️ LA HAUTEUR EST BORNEE A L ECRAN, PAS SEULEMENT A UNE LARGEUR : ce popup
   est en SURIMPRESSION en bas de page. Sans `min()`, une video verticale de
   280 px de large ferait 498 px de haut et mangerait la moitie d un telephone.
   `svh` et pas `vh` -- meme correctif que la hauteur plein ecran du 6
   septembre : `100vh` mesure l ecran SANS la barre du navigateur. */
/* ⚠️⚠️ LA BORNE EN `svh` S'APPLIQUE A LA HAUTEUR, DONC ELLE PASSE PAR 9/16 — hotfix du
   13 septembre 2026 (ZigZag : « le popup telephone avec une video est trop grand »).
   Jusque-la `min(220px, 28svh)` bornait la LARGEUR a 28 % de l ecran : la hauteur
   montait a 50 % sans que personne ne l ecrive. `calc(Nsvh * 9 / 16)` en largeur donne
   une hauteur d au plus N svh — c est la seule forme qui borne ce que le commentaire
   ci-dessus promet. */
.installer-app__video {
    flex: 1 1 auto;
    max-width: min(200px, calc(40svh * 9 / 16));
    margin: 0 auto;
}

/* ⚠️ LE CADRE 9/16 EST UN ENFANT DEPUIS LE 13 SEPTEMBRE 2026 : l element marque
   porte aussi le titre du gerant au-dessus (retour de recette), et un titre ne
   peut pas vivre dans une boite a `aspect-ratio` + `overflow: hidden` -- il y
   serait rogne. Les largeurs restent sur `.installer-app__video`, le cadre
   prend 100 % et fixe la forme. */
.installer-app__cadre {
    aspect-ratio: 9 / 16;
    overflow: hidden;
    border-radius: var(--radius);
}

.installer-app__titre {
    margin: 0 0 0.6rem;
    font-weight: 600;
    font-size: 1rem;
    line-height: 1.3;
    text-align: center;
}

/* ⚠️⚠️ AVEC UNE VIDEO, LE POPUP CHANGE DE FORME — retour de recette du 12
   septembre 2026 : « je voudrais que ce soit centre et que ca prenne plus de
   place sur la page ». Le corps passe en colonne, la video est centree et
   monte a 340 px / 62 svh (la moitie haute d un telephone, la fleche et la
   barre de Safari restent visibles dessous), et la croix se pose dans le coin
   au lieu de prendre une colonne a droite. La variante TEXTE ne bouge pas
   d un pixel : `:has()` ne s applique qu au corps qui porte une video. */
/* ⚠️⚠️ HOTFIX DU 13 SEPTEMBRE 2026 — ZigZag : « trop grand, et la croix doit etre mieux
   cliquable ». `min(340px, 62svh)` bornait la LARGEUR : une video de 340 px de large
   faisait 604 px de haut, soit plus que l ecran utile d un telephone — et la croix, dans
   le coin, flottait juste au-dessus du lecteur. La video tient maintenant dans la MOITIE
   de l ecran en hauteur (au plus 240 px de large), et la croix est une pastille visible
   de 48 px, degagee du lecteur par la marge haute du corps.
   Mesure par le calcul, pas sur appareil (le poste de dev n ouvre pas Safari iOS) :
   iPhone 390 x 844, ~660 px de hauteur utile → video 186 x 330 au lieu de 340 x 604. */
.installer-app__corps:has(.installer-app__video) {
    position: relative;
    flex-direction: column;
    align-items: center;
    padding: 3.75rem 1rem 1rem;
}

.installer-app__corps:has(.installer-app__video) .installer-app__video {
    max-width: min(240px, calc(50svh * 9 / 16));
    width: 100%;
}

.installer-app__corps:has(.installer-app__video) .installer-app__fermer {
    position: absolute;
    top: 0.5rem;
    right: 0.5rem;
    z-index: 1;
    border: 1px solid var(--c-border);
    border-radius: 50%;
    background: var(--c-bg-elev);
    color: var(--c-text);
    font-size: 1.9rem;
}

.installer-app__cadre iframe {
    width: 100%;
    height: 100%;
    border: 0;
    display: block;
}

.installer-app__texte strong {
    display: block;
    margin-bottom: 0.15rem;
    color: var(--c-text);
}

/* Le geste à faire, détaché du reste de la phrase. */
.installer-app__geste {
    display: inline-block;
    padding: 0.05rem 0.35rem;
    border-radius: 4px;
    background: rgba(201, 161, 74, 0.14);
    color: var(--c-accent-texte);
    font-weight: 600;
    white-space: nowrap;
}

.installer-app__icone {
    display: inline-flex;
    vertical-align: -0.2em;
    color: var(--c-accent-texte);
}

/* ⚠️ 44 px de côté : c'est la cible tactile minimale du lot 27, éprouvée sur un
   vrai téléphone. En dessous, on rate le bouton et on croit que le message ne
   se ferme pas. */
/* ⚠️ UNE CROIX DISCRETE, 44 px DE ZONE QUAND MEME (12 septembre 2026) : le
   glyphe est petit, la cible ne l est pas. En dessous de 44 px on rate le
   bouton et on croit que le message ne se ferme pas — mesure sur appareil. */
.installer-app__fermer {
    flex: 0 0 auto;
    display: inline-flex;
    align-items: center;
    justify-content: center;
    /* 48 px depuis le hotfix du 13 septembre 2026 (« la croix doit etre mieux cliquable ») :
       44 est un plancher, pas une cible. `touch-action` retire le delai du double-toucher. */
    min-width: 48px;
    min-height: 48px;
    touch-action: manipulation;
    padding: 0;
    border: 0;
    border-radius: var(--radius);
    background: transparent;
    color: var(--c-text-muted);
    font-size: 1.5rem;
    line-height: 1;
    cursor: pointer;
}

.installer-app__fermer:hover,
.installer-app__fermer:focus-visible {
    color: var(--c-text);
}

/* Le tracé suit `font-size` du bouton (1.5rem, 1.9rem sur la vidéo) et sa couleur.
   `display: block` retire l'espace de ligne de base d'un élément en ligne : c'est
   lui qui décentrait verticalement le caractère « × » d'avant. */
.installer-app__croix {
    display: block;
    width: 0.7em;
    height: 0.7em;
    fill: none;
    stroke: currentColor;
    stroke-width: 2.5;
    stroke-linecap: round;
}

/* La flèche, purement décorative : le texte dit déjà où regarder, parce qu'une
   flèche ne se lit pas à voix haute. Son orientation est posée par le JS. */
.installer-app__fleche {
    display: block;
    width: 0;
    height: 0;
    margin: 0 auto;
    border-left: 9px solid transparent;
    border-right: 9px solid transparent;
}

/* iOS : la barre de Safari est EN BAS — la flèche descend vers elle. */
.installer-app__fleche--bas {
    margin-top: -1px;
    border-top: 10px solid var(--c-border);
}

/* Android : le menu ⋮ est EN HAUT À DROITE — la flèche monte, et se décale à
   droite pour le désigner. */
.installer-app__fleche--haut {
    order: -1;
    margin: 0 1.5rem 0 auto;
    border-bottom: 10px solid var(--c-border);
}

.installer-app.is-visible:has(.installer-app__fleche--haut) {
    display: flex;
    flex-direction: column;
}

/* Le contenu de la page se réserve la place du message tant qu'il est là :
   sans cette marge, il recouvre la fin de la page au lieu de la laisser lire.
   Posée sur `body` par le JS, retirée à la fermeture. */
body.a-installer-app {
    padding-bottom: 6.5rem;
}

/* ═══════════ LE CHAMP « TÉLÉPHONE + PAYS » (lot T-1, 27 août 2026) ═══════════
   Un sélecteur de pays collé à un champ de numéro, sur une seule ligne. Il vit
   ICI et pas dans admin.css : le même champ sert le board ET les écrans clients
   (inscription, ma fiche), et `components.css` est chargé par les deux bases.

   ⚠️ LE SÉLECTEUR NE DOIT PAS MANGER LA LIGNE. Il porte « +33 France », donc un
   texte plus long que ce qu'on veut lui donner : d'où une largeur fixe et un
   `text-overflow`. Le numéro, lui, prend tout le reste — c'est lui qu'on lit et
   qu'on corrige. */
.champ-tel {
    display: flex;
    align-items: stretch;
    gap: 0.4rem;
    width: 100%;
}

/* ═══ ⚠️ L'APPARENCE VIT ICI, ET ELLE MANQUAIT — T-1, seconde passe ═══
   *« Je suis tombé sur des input qui sont carrément pas stylisées du tout »*
   (Alexandre, 27 août 2026). C'était exact, et MESURÉ en console dans une
   pop-up de l'agenda : fond `rgb(255,255,255)`, texte noir, Arial 13.33 px,
   bordure `2px inset` — le champ NATIF du navigateur, au milieu d'un board dont
   les autres champs sont en `#121212`, Inter 14.4 px, rayon 10 px.

   ⚠️ LA CAUSE N'ÉTAIT PAS UN OUBLI DE CLASSE, MAIS UNE FRONTIÈRE ENTRE FEUILLES.
   `.form-control` — l'apparence des champs du board — vit dans `admin.css`, que
   les écrans CLIENTS ne chargent pas. Ce champ, lui, sert les deux (inscription,
   ma fiche, board). Lui poser `form-control` l'aurait habillé sur le board et
   laissé nu chez la cliente ; ces règles-ci, dans `components.css`, valent des
   deux côtés.

   ⚠️ ET ELLES NE RECOUVRENT RIEN. Leur spécificité (0,1,0) passe SOUS celle des
   hôtes qui habillent déjà leurs champs (`.admin-form input:not(…)`,
   `.tunnel-form input[type="tel"]`) : là où un habillage existe, il gagne et
   rien ne change ; là où il n'y en avait aucun — les pop-ups de l'agenda —, ces
   règles comblent le trou. C'est le contraire d'un `!important` : on remplit,
   on n'écrase pas. */
.champ-tel__pays,
.champ-tel__numero {
    padding: 0.55rem 0.75rem;
    background: var(--c-bg);
    border: 1px solid var(--c-border);
    border-radius: var(--radius);
    color: var(--c-text);
    font: inherit;
    /* Le chevron natif du <select> reste lisible sur fond sombre. */
    color-scheme: dark;
}

.champ-tel__pays:focus,
.champ-tel__numero:focus {
    outline: 2px solid var(--c-accent);
    outline-offset: -1px;
}

.champ-tel__numero::placeholder {
    color: var(--c-text-muted);
}

.champ-tel .is-invalid {
    border-color: #e88;
    outline-color: #e88;
}

.champ-tel__pays {
    flex: 0 0 8.5rem;
    max-width: 8.5rem;
    min-width: 0;
    text-overflow: ellipsis;
}

.champ-tel__numero {
    flex: 1 1 auto;
    min-width: 0;
}

/* L'exemple sous le champ : il passe SOUS les deux, d'où la base en `wrap`. */
.champ-tel {
    flex-wrap: wrap;
}

.champ-tel__exemple {
    flex: 1 1 100%;
    margin: 0.3rem 0 0;
    font-size: 0.8rem;
    color: var(--c-text-muted);
}

/* Sans liste de pays — API muette ou moteur antérieur au lot —, le champ est
   seul et reprend toute la largeur, sans trou là où le sélecteur serait. */
.champ-tel--sans-pays .champ-tel__numero {
    flex: 1 1 100%;
}

/* ⚠️ SOUS 30 rem, LES DEUX PASSENT L'UN AU-DESSUS DE L'AUTRE. Côte à côte sur
   un téléphone, le numéro tombait sous 10 caractères visibles — on tape à
   l'aveugle un champ dont dépend l'arrivée du SMS. Le sélecteur reprend alors
   toute la largeur : il est plus facile à toucher, et il n'est manipulé qu'une
   fois. */
@media (max-width: 30rem) {
    /* `flex-wrap: wrap` est desormais dans la base — l'exemple sous le champ en
       a besoin a toute largeur. Le laisser ici aussi ferait croire qu'il ne vaut
       que sous 30 rem. Ce qui reste propre au telephone, c'est la largeur du
       selecteur. */
    .champ-tel__pays {
        flex: 1 1 100%;
        max-width: 100%;
    }
}

/* ═══ LES SORTIES DE LA PAGE D'ERREUR (item 69, 3 septembre 2026) ═══

   Une rangée de boutons, et rien de plus. Elle a sa propre classe plutôt que
   d'emprunter `admin-head__actions` ou `rdv-actions` : ces classes-là sont
   dimensionnées pour leur contexte (une ligne de tableau, un en-tête de board),
   et ce dépôt a déjà mesuré ce que coûte un emprunt — un repère de 12 px
   recopié dans un cadre de 9 rem donne un point perdu au milieu du vide, avec
   la bonne classe dans le HTML et 1887 tests verts.

   ⚠️ ELLE EXISTE PARCE QU'UNE CLASSE SANS RÈGLE NE FAIT RIEN, EN SILENCE.
   Trois classes de la vue Semaine ont vécu écrites dans le gabarit, calculées
   par le service et gardées par un test, sans qu'aucune règle CSS ne leur
   corresponde. Rien ne pouvait le dire — ni le HTML servi, ni les tests. */
.erreur__sorties {
    display: flex;
    flex-wrap: wrap;
    gap: 0.75rem;
    margin-top: 2rem;
}

/* ── Un établissement au pied d'un site d'enseigne (V1c) ──
   ⚠️ REPLIABLE EN NATIF, sans une ligne de JavaScript — l'idiome du dépôt
   (voir `.creneau-day` dans sections.css). Le marqueur natif est masqué et
   remplacé par le chevron doré, comme là-bas : deux triangles de styles
   différents dans le même site se remarquent.

   ⚠️ ET LE CONTENU RESTE DANS LE HTML SERVI, replié ou non. C'est la raison
   d'être du choix : un pied de page d'enseigne porte une fiche
   adresse-téléphone par ville, et c'est ce qu'un moteur de recherche vient y
   lire. Masquer en JavaScript le lui retirerait. */
.site-footer__lieu {
    margin-bottom: 0.75rem;
}

.site-footer__lieu:last-child {
    margin-bottom: 0;
}

.site-footer__lieu p {
    margin: 0.15rem 0;
}

.site-footer__lieu-titre {
    display: flex;
    align-items: center;
    gap: 0.5rem;
    cursor: pointer;
    list-style: none; /* masque le triangle natif (WebKit ci-dessous) */
    user-select: none;
    font-family: var(--police-titre);
    font-size: 0.82rem;
    letter-spacing: 0.12em;
    text-transform: uppercase;
    color: var(--c-accent-texte);
}

.site-footer__lieu-titre::-webkit-details-marker {
    display: none;
}

/* Chevron doré, pivote à l'ouverture — même geste que `.creneau-day`. */
.site-footer__lieu-titre::after {
    content: "";
    flex-shrink: 0;
    width: 0.4rem;
    height: 0.4rem;
    border-right: 2px solid var(--c-accent-interface);
    border-bottom: 2px solid var(--c-accent-interface);
    transform: rotate(45deg);
    transition: transform 0.2s ease;
}

.site-footer__lieu[open] .site-footer__lieu-titre::after {
    transform: rotate(-135deg);
}

.site-footer__lieu-titre:focus-visible {
    outline: 2px solid var(--c-accent);
    outline-offset: 2px;
}

/* ═══ LE DÉROULANT D'ÉTABLISSEMENT DE LA BARRE (lot V3) ═══
   Rendu seulement quand une personne administre plusieurs établissements —
   donc jamais sur un mono-salon, c'est-à-dire jamais sur le parc actuel.

   ⚠️ REPLI NATIF (`<details>`), comme le pied de page d'enseigne et les
   créneaux du tunnel : il marche sans JavaScript. Un menu de NAVIGATION qui
   dépendrait d'un asset non recompilé enfermerait le gérant sur un seul
   établissement, sans le moindre message. */
.choix-etab {
    position: relative;
    display: inline-block;
}

/* Le bouton reprend `.btn .btn--ghost` : il DOIT ressembler aux autres entrées
   de la barre, dont il prend la place. */
.choix-etab__bouton {
    display: inline-flex;
    align-items: center;
    gap: 0.4rem;
    cursor: pointer;
    list-style: none; /* masque le triangle natif (WebKit ci-dessous) */
    user-select: none;
}

.choix-etab__bouton::-webkit-details-marker {
    display: none;
}

/* Chevron doré, pivote à l'ouverture — même geste que `.creneau-day` et
   `.site-footer__lieu`. Trois triangles de styles différents dans un même
   site se remarquent. */
.choix-etab__bouton::after {
    content: "";
    flex-shrink: 0;
    width: 0.4rem;
    height: 0.4rem;
    border-right: 2px solid currentColor;
    border-bottom: 2px solid currentColor;
    transform: rotate(45deg) translate(-1px, -1px);
    transition: transform 0.2s ease;
}

.choix-etab[open] .choix-etab__bouton::after {
    transform: rotate(-135deg) translate(-1px, -1px);
}

/* ⚠️ LE PANNEAU EST EN `position: absolute` : sans ça, il POUSSERAIT la barre
   et ferait sauter toute la page à l'ouverture. */
.choix-etab__liste {
    position: absolute;
    top: calc(100% + 0.4rem);
    right: 0;
    z-index: 60;
    min-width: 11rem;
    margin: 0;
    padding: 0.35rem;
    list-style: none;
    background: var(--c-bg-elev);
    border: 1px solid var(--c-border);
    border-radius: var(--radius);
    box-shadow: var(--shadow);
}

.choix-etab__lien {
    display: block;
    padding: 0.5rem 0.7rem;
    border-radius: calc(var(--radius) / 2);
    color: var(--c-text);
    text-decoration: none;
    white-space: nowrap;
    font-size: 0.92rem;
}

.choix-etab__lien:hover,
.choix-etab__lien:focus-visible {
    background: var(--c-bg);
    color: var(--c-accent-texte);
}

/* ⚠️⚠️ « MON COMPTE » — lots B-8 et B-9, 8 septembre 2026.
   Il EMPRUNTE `.choix-etab` : c'est le même déroulant, dans la même barre, et
   *un second module aurait échappé à toutes les gardes du premier*. Cette classe
   n'existe que pour lui donner un nom vrai — « choix d'établissement » ne décrit
   pas un menu de compte — et pour porter ce qui lui est propre. */
.menu-compte {
    margin-left: auto;
}

/* ⚠️ LA DÉCONNEXION EST UN `<button>` DANS UN `<form>`, PAS UN LIEN, et il faut
   qu'elle en ait l'air : *visiter un lien ne doit pas déconnecter*, donc c'est
   un POST avec son jeton. Ces trois lignes suppriment l'apparence de bouton que
   le navigateur lui donne, sans toucher à ce qu'elle EST.

   ⚠️ `width: 100%` ET `text-align: left` : sans eux, l'entrée serait centrée et
   plus étroite que ses voisines — la seule du menu à ne pas s'aligner, ce qui se
   voit tout de suite et donne l'air d'un défaut. */
.menu-compte__quitter {
    width: 100%;
    text-align: left;
    background: none;
    border: 0;
    font: inherit;
    cursor: pointer;
}

/* L'établissement qu'on regarde déjà : marqué, mais toujours cliquable — s'en
   servir pour revenir en haut du board est un geste légitime. */
.choix-etab__lien.is-courant {
    color: var(--c-accent-texte);
    font-weight: 700;
}

.choix-etab__separateur {
    height: 1px;
    margin: 0.35rem 0.2rem;
    background: var(--c-border);
}

.choix-etab__lien--marque {
    color: var(--c-text-muted);
    font-style: italic;
}

/* ═══ ⚠️⚠️ SUR TÉLÉPHONE, « ESPACE SALON » PASSE DANS « MON COMPTE » — 15 septembre 2026 ═══
   Recette d'Alexandre : un admin de plusieurs établissements alignait trois déroulants (Espace salon, Agenda, Mon
   compte) et la barre passait sur deux lignes. Son choix : « Agenda » reste, « Espace salon » rejoint le menu du
   compte. Les DEUX bascules sont ici, sous la même borne, et doivent le rester : retirer la première ferait
   apparaître « Espace salon » deux fois, retirer la seconde le ferait disparaître du téléphone.
   ⚠️ SÉLECTEURS À DEUX CLASSES : `.choix-etab` (inline-block) et les `li` de la liste redéclarent `display` avec une
   seule classe, et la règle écrite la dernière gagnerait par accident. */
.choix-etab__liste .menu-compte__telephone {
    display: none;
}

@media (max-width: 560px) {
    .site-header__actions .choix-etab--hors-telephone {
        display: none;
    }

    .choix-etab__liste .menu-compte__telephone {
        display: list-item;
    }
}

/* ═══ G3 — LA BANNIÈRE DE CONSENTEMENT ET LES VIDÉOS EN ATTENTE (15 septembre 2026) ═══
   Trois boutons de MÊME POIDS (décision D2) : tous en `btn--ghost`, aucun mis en avant. La bannière est fixée en bas,
   au-dessus du contenu, et ne masque jamais la page entière : on peut lire le site avant de choisir. */
.consentement {
    position: fixed;
    left: 1rem;
    right: 1rem;
    bottom: 1rem;
    z-index: 80;
    max-width: 56rem;
    margin: 0 auto;
    padding: 1rem 1.25rem;
    display: flex;
    flex-wrap: wrap;
    gap: 0.75rem 1.5rem;
    align-items: center;
    justify-content: space-between;
    background: var(--c-bg-elev);
    color: var(--c-text);
    border: 1px solid var(--c-border);
    border-radius: var(--radius);
    box-shadow: var(--shadow);
}

.consentement__texte {
    flex: 1 1 22rem;
    font-size: 0.9rem;
}

.consentement__texte p {
    margin: 0;
}

.consentement__titre {
    font-weight: 700;
    margin-bottom: 0.25rem !important;
}

.consentement__formulaire {
    display: flex;
    flex-wrap: wrap;
    gap: 0.5rem;
    align-items: flex-start;
}

.consentement__choix > summary {
    list-style: none;
    cursor: pointer;
}

.consentement__choix > summary::-webkit-details-marker {
    display: none;
}

.consentement__cases {
    display: grid;
    gap: 0.6rem;
    margin-top: 0.75rem;
    max-width: 30rem;
}

.consentement__case {
    display: flex;
    gap: 0.5rem;
    align-items: flex-start;
    font-size: 0.88rem;
}

.consentement__etat {
    margin: 1rem 0;
}

/* Une vidéo qui attend l'accord : rien de YouTube, une phrase et un bouton. Dans une section d'accueil elle remplit
   le bloc à ratio fixe ; dans le tuto elle prend sa hauteur. */
.video-attente {
    display: flex;
    flex-direction: column;
    align-items: center;
    justify-content: center;
    gap: 0.75rem;
    padding: 1.25rem;
    text-align: center;
    background: var(--c-bg-elev);
    border-radius: var(--radius);
}

.video-accueil > .video-attente {
    position: absolute;
    inset: 0;
}

.video-attente__phrase {
    margin: 0;
    max-width: 28rem;
    font-size: 0.9rem;
}