/* ==========================================================================
   Portfolio Cuisinier — feuille de style du site public
   Socle de la refonte graphique (TODO 11.1), porté de public/assets/poc/poc.css.

   Un seul fichier, aucune ressource externe : polices système, SVG inline,
   aucun @import distant. Ce qui est écrit ici est ce qui est servi.

   Organisation en couches explicites. L'ordre de déclaration ci-dessous EST
   l'ordre de priorité : `mouvement` passe après `composants`, donc aucune
   règle n'a besoin de `!important` pour l'emporter — c'est ce qui referme la
   dette D.2 par construction. Le fichier n'en contient plus aucun.

   Transition achevée (lots 11.7 et 11.9) : les 8 gabarits de public/pages/
   ont été réécrits dans le vocabulaire cible (français : .entete, .chapitre,
   .carte, .bouton…), et les blocs « compat, retiré en 11.9 » portant le
   vocabulaire hérité (.header, .card, .btn, .gate, .lightbox, .hero en
   flux…) ont été retirés (dette D.6). Seule `.ingredients` en a été extraite
   plutôt que supprimée : `public/pages/recette.html` l'emploie encore — voir
   le commentaire à côté de sa règle, dans `@layer pages`. Même chose pour
   `.placeholder`, produite par `core.js` et `components.js` comme repli
   d'image, extraite de l'ancienne section « Carte encadrée », dans
   `@layer composants`.
   ========================================================================== */

@layer reset, jetons, base, mise-en-page, composants, pages, mouvement, utilitaires;

/* ==========================================================================
   @layer reset — réinitialisation
   ========================================================================== */

@layer reset {
  *,
  *::before,
  *::after {
    box-sizing: border-box;
  }

  * {
    margin: 0;
  }

  html {
    -webkit-text-size-adjust: 100%;
    scroll-behavior: smooth;
  }

  /* Garde-fou : aucune barre de défilement horizontale, jamais. `clip` — et
     non `hidden` — parce qu'il ne crée pas de conteneur de défilement : le
     `position: sticky` et l'aimantation verticale continuent de fonctionner. */
  html,
  body {
    overflow-x: clip;
  }

  img,
  picture,
  svg,
  video {
    display: block;
    max-width: 100%;
  }

  /* Photos et vidéos du cuisinier : pas de glisser-déposer vers le bureau, pas
     de sélection, pas de menu « enregistrer l'image » à l'appui long sur
     mobile. Un frein, pas une protection : voir core.js (protegerMedias). */
  img,
  video {
    -webkit-user-drag: none;
    user-select: none;
    -webkit-user-select: none;
    -webkit-touch-callout: none;
  }

  input,
  button,
  textarea,
  select {
    font: inherit;
    color: inherit;
  }

  button {
    background: none;
    border: none;
    cursor: pointer;
  }

  ul,
  ol {
    list-style: none;
    padding: 0;
  }

  /* Le navigateur gère Échap, le piège de focus et le fond assombri : on ne
     lui retire que son cadre et ses limites de taille. */
  dialog {
    border: none;
    padding: 0;
    color: inherit;
    max-width: none;
    max-height: none;
  }
}

/* ==========================================================================
   @layer jetons — @property, couleurs light-dark(), échelle fluide, mouvement
   ========================================================================== */

@layer jetons {
  /* Propriétés personnalisées typées : ce qui les rend animables et
     interpolables, donc utilisables dans une transition ou un calc(). */

  /* Progression d'apparition d'un chapitre : 0 = caché, 1 = en place. Une
     seule valeur pilote l'opacité ET le déplacement du repli d'apparition. */
  @property --reveal {
    syntax: '<number>';
    inherits: true;
    initial-value: 1;
  }

  /* Rang d'un mot dans le titre du hero — sert à échelonner les délais. */
  @property --i {
    syntax: '<number>';
    inherits: false;
    initial-value: 0;
  }

  :root {
    /* Permet d'animer une hauteur vers `auto` (menu mobile). */
    interpolate-size: allow-keywords;

    /* ------------------------------------------------------------------
       Les DEUX SEULES teintes réglables depuis l'admin (Réglages → Palette).
       Le serveur les surcharge par un <style> injecté dans <head>
       (src/routes/pages.js, marqueur {{THEME_STYLE}}) : ce <style> n'est
       rangé dans aucune couche, il l'emporte donc naturellement sur ce bloc.
       Ne jamais renommer ces deux variables, ni figer une couleur qui devrait
       les suivre : tout le reste en dérive par color-mix()/light-dark().
       ------------------------------------------------------------------ */
    --theme-accent: #8a4a2c;
    --theme-sand: #ddc6a3;

    /* Un seul jeu de valeurs pour les deux thèmes : `light-dark()` remplace
       le bloc @media (prefers-color-scheme: dark) dupliqué, qui a disparu.
       Un composant qui aurait besoin d'une couleur propre au thème sombre
       manque une variable — il n'écrit jamais de règle de thème à lui. */
    /* Thème clair imposé : la charte est blanc + sable + verveine, quel que
       soit le réglage du navigateur du visiteur. Pour rendre le thème sombre
       (réglage système), repasser à `light dark` : rien d'autre à toucher. */
    color-scheme: light;

    --fond: light-dark(#fbf7f1, #14110d);
    --fond-alt: light-dark(
      color-mix(in srgb, var(--theme-sand) 34%, #fbf7f1),
      color-mix(in srgb, var(--theme-sand) 10%, #14110d)
    );
    --surface: light-dark(#fffefb, #1d1813);
    --encre: light-dark(#1c1711, #efe7db);
    --encre-douce: light-dark(#6b5c4c, #a89c8b);
    --filet: light-dark(
      color-mix(in srgb, var(--theme-sand) 55%, #fbf7f1),
      color-mix(in srgb, var(--theme-sand) 18%, #14110d)
    );
    --accent: light-dark(var(--theme-accent), color-mix(in oklab, var(--theme-accent) 58%, white));
    --accent-encre: light-dark(#fff8f1, #14110d);
    --accent-doux: light-dark(
      color-mix(in srgb, var(--theme-accent) 11%, white),
      color-mix(in srgb, var(--theme-accent) 22%, #14110d)
    );
    --ombre: light-dark(rgb(32 24 14 / 10%), rgb(0 0 0 / 45%));

    /* Posé sur une photo : reste clair quel que soit le thème. */
    --sur-photo: #f7f2ea;
    --sur-photo-doux: rgb(247 242 234 / 72%);

    /* Typographie — piles système, aucune police téléchargée. */
    --serif: ui-serif, Georgia, 'Iowan Old Style', 'Palatino Linotype', 'Times New Roman', serif;
    --sans: ui-sans-serif, system-ui, -apple-system, 'Segoe UI', Roboto, 'Helvetica Neue', Arial,
      sans-serif;

    /* Échelle typographique fluide, interpolée entre 320 et 1280 px — écrite
       à la main chez nous, aucune dépendance. Les deux paliers hauts
       continuent de grandir au-delà de 1280 px : sur un grand écran le titre
       doit occuper l'espace, pas flotter dans un coin. */
    --pas--1: clamp(0.83rem, 0.81rem + 0.11vw, 0.9rem);
    --pas-0: clamp(1rem, 0.96rem + 0.2vw, 1.125rem);
    --pas-1: clamp(1.2rem, 1.13rem + 0.35vw, 1.41rem);
    --pas-2: clamp(1.44rem, 1.32rem + 0.6vw, 1.76rem);
    --pas-3: clamp(1.73rem, 1.54rem + 0.94vw, 2.2rem);
    --pas-4: clamp(2.07rem, 1.79rem + 1.42vw, 2.75rem);
    --pas-5: clamp(2.49rem, 1.7rem + 3.4vw, 4.75rem);
    --pas-6: clamp(2.6rem, 1.7rem + 4vw, 5.25rem);

    /* Espacement — les noms --s1…--s9 sont conservés (aucun composant à
       retoucher), les paliers hauts deviennent fluides. */
    --s1: 4px;
    --s2: 8px;
    --s3: 12px;
    --s4: 16px;
    --s5: 24px;
    --s6: 32px;
    --s7: clamp(2rem, 1.6rem + 2vw, 3rem);
    --s8: clamp(2.75rem, 2rem + 3.75vw, 4.5rem);

    /* Plus de conteneur centré : les sections occupent toute la largeur de
       l'écran, seule cette marge respire avec lui. Ce qui doit rester lisible
       est borné en `ch` (le paragraphe, le titre), jamais le bloc qui le
       contient — d'où --mesure, et non une largeur de colonne. */
    --marge: clamp(1rem, 0.4rem + 3vw, 4.5rem);
    --mesure: 54ch;

    /* Interlignes — un paragraphe de dix lignes doit respirer, sinon il se lit
       comme un mur. Le corps courant à 1.62 ; les paragraphes d'introduction,
       plus gros donc plus larges à l'œil, un cran au-dessus. Ces deux valeurs
       aèrent tout le site d'un coup : ne pas écrire d'interligne en dur dans
       un composant de texte long. */
    --interligne: 1.62;
    --interligne-ample: 1.68;

    --rayon: 4px;
    --rayon-media: 8px;

    /* En-tête plus présent, et qui grandit avec l'écran. */
    --hauteur-entete: clamp(96px, 66px + 4.2vw, 140px);
    --sceau: clamp(54px, 40px + 2.4vw, 80px);

    /* Mouvement — durées et courbes. `--ressort` produit un léger dépassement
       sans une ligne de JS. */
    --court: 150ms;
    --moyen: 320ms;
    --long: 700ms;
    --sortie: cubic-bezier(0.22, 0.75, 0.24, 1);
    --ressort: linear(0, 0.22 3.5%, 0.68 12%, 0.96 22%, 1.04 32%, 1);

  }
}

/* ==========================================================================
   @layer base — éléments HTML nus
   ========================================================================== */

@layer base {
  body {
    background: var(--fond);
    color: var(--encre);
    font-family: var(--sans);
    font-size: var(--pas-0);
    line-height: var(--interligne);
    -webkit-font-smoothing: antialiased;
    /* Un mot très long (adresse, URL) se coupe au lieu de déborder. */
    overflow-wrap: break-word;
    /* Colonne flex : ce sur quoi s'appuie `.gabarit { flex: 1 }` plus bas
       pour pousser le pied de page en bas des pages courtes. */
    min-height: 100svh;
    display: flex;
    flex-direction: column;
  }

  h1,
  h2,
  h3,
  h4 {
    font-family: var(--serif);
    font-weight: 600;
    line-height: 1.02;
    letter-spacing: -0.025em;
    text-wrap: balance;
  }

  /* Tailles par défaut : les gabarits pas encore réécrits utilisent des
     titres nus. Les composants cibles les surchargent par une classe. */
  h1 {
    font-size: var(--pas-6);
    font-weight: 700;
    letter-spacing: -0.03em;
  }

  h2 {
    font-size: var(--pas-4);
    font-weight: 700;
    letter-spacing: -0.02em;
  }

  h3 {
    font-size: var(--pas-1);
    line-height: 1.15;
    letter-spacing: -0.01em;
  }

  h4 {
    font-size: var(--pas-0);
  }

  p {
    text-wrap: pretty;
  }

  a {
    color: var(--accent);
    text-decoration-thickness: 1px;
    text-underline-offset: 3px;
  }

  :focus-visible {
    outline: 2px solid var(--accent);
    outline-offset: 3px;
    border-radius: 2px;
  }

  ::selection {
    background: var(--accent-doux);
    color: var(--encre);
  }
}

/* ==========================================================================
   @layer mise-en-page — pleine largeur, saignées, rails, anatomie de chapitre
   ========================================================================== */

@layer mise-en-page {
  /* --- Racines de page ------------------------------------------------- */

  .gabarit {
    /* `body` est une colonne flex : c'est ce qui pousse le pied de page en
       bas des pages courtes. */
    flex: 1;
  }

  /* Page interne : pleine largeur, seule --marge respire. */
  .page {
    display: grid;
    align-content: start;
    gap: clamp(1rem, 3vh, var(--s7));
    padding-block: calc(var(--hauteur-entete) + clamp(0.5rem, 1.5vh, 1.25rem)) var(--s8);
    padding-inline: var(--marge);
    container: gabarit / inline-size;
  }

  .dedans {
    width: 100%;
    padding-inline: var(--marge);
  }

  .section--tight {
    padding-block: var(--s7);
  }

  .section--alt {
    background: var(--fond-alt);
  }

  .stack > * + * {
    margin-top: var(--s4);
  }

  .actions {
    display: flex;
    flex-wrap: wrap;
    gap: var(--s3);
  }

  /* --- Saignée ---------------------------------------------------------
     Déborde des marges du bloc parent sans jamais recourir à `100vw` : on
     annule simplement la marge courante. Aucune barre de défilement
     horizontale possible, aucune correction de largeur de scrollbar.
     --------------------------------------------------------------------- */

  .bleed {
    margin-inline: calc(var(--marge) * -1);
  }

  .bleed--debut {
    margin-inline-start: calc(var(--marge) * -1);
    margin-inline-end: 0;
  }

  .bleed--fin {
    margin-inline-start: 0;
    margin-inline-end: calc(var(--marge) * -1);
  }

  /* Une image à saignée n'est jamais étirée : 3/2, comme partout ailleurs.
     `max-height` plafonne la hauteur (calculée depuis la largeur pleine page
     par le ratio 3/2) : sans lui, une page pleine largeur sur un très grand
     écran donne une bande qui déborde de l'écran (portrait de À propos) ou
     qui écrase le titre (image d'ouverture d'une recette). Même mesure que
     `.visionneuse__image` un peu plus loin, en un peu plus retenu : une image
     de bandeau reste présente, jamais dominante. */
  .bleed > img,
  .bleed > video {
    width: 100%;
    aspect-ratio: 3 / 2;
    object-fit: cover;
    max-height: 60svh;
  }

  /* --- Rail ------------------------------------------------------------
     Collection horizontale à aimantation : une carte pleine au pouce sur
     téléphone, plusieurs sur grand écran. C'est ce qui permet de montrer
     cinq recettes sans empiler cinq blocs de texte.
     --------------------------------------------------------------------- */

  .rail {
    display: grid;
    grid-auto-flow: column;
    grid-auto-columns: clamp(15rem, 74vw, 26rem);
    gap: var(--s5);
    overflow-x: auto;
    overscroll-behavior-inline: contain;
    /* `proximity`, jamais `mandatory` : on doit pouvoir s'arrêter entre deux
       cartes sans que le navigateur reprenne la main. */
    scroll-snap-type: x proximity;
    scroll-padding-inline: var(--marge);
    padding-block-end: var(--s3);
    scrollbar-width: thin;
    scrollbar-color: var(--filet) transparent;
  }

  .rail > * {
    scroll-snap-align: start;
    min-width: 0;
  }

  /* Rail à saignée : les cartes repartent de la marge, la dernière peut
     glisser jusqu'au bord. */
  .rail.bleed {
    padding-inline: var(--marge);
  }

  /* Au-delà d'une certaine largeur de conteneur, la collection n'a plus
     besoin de défiler : elle tient en grille. */
  @container gabarit (width >= 60rem) {
    .rail--grille {
      grid-auto-flow: row;
      grid-template-columns: repeat(3, minmax(0, 1fr));
      overflow-x: visible;
      scroll-snap-type: none;
    }
  }

  /* --- Anatomie d'un chapitre -------------------------------------------
     Trois rangées : tête / scène / sortie — `auto / minmax(0,1fr) / auto`.
     La scène prend la place restante, donc un chapitre tient toujours
     exactement dans un écran, à n'importe quelle taille, sans jamais rogner
     son contenu. Une rangée vide ne coûte rien (auto = 0).
     --------------------------------------------------------------------- */

  .chapitre {
    position: relative;
    min-height: 100svh;
    display: grid;
    grid-template-rows: auto minmax(0, 1fr) auto;
    gap: var(--s5);
    padding-block: calc(var(--hauteur-entete) + var(--s8)) var(--s8);
    padding-inline: var(--marge);
    scroll-snap-align: start;
  }

  /* Volontairement PAS de `container-type` sur `.chapitre` : il occupe de
     toute façon la largeur de l'écran, les deux points de rupture du design
     system (40rem / 52rem) y sont exactement équivalents. Les container
     queries servent aux composants réellement réemployés dans des largeurs
     différentes (`carte`, `formulaire`, `gabarit`). */

  /* Placement par défaut : tout enfant est « scène » (rangée du milieu) sauf
     la tête et la sortie. Un chapitre qui n'a qu'un bloc de contenu le voit
     donc occuper toute la place restante, sans balise supplémentaire — et
     rien ne peut retomber par erreur dans la rangée de tête. */
  .chapitre > * {
    grid-row: 2;
    min-width: 0;
  }

  .chapitre > .chapitre__tete {
    grid-row: 1;
  }

  .chapitre > .chapitre__sortie,
  .chapitre > .pied {
    grid-row: 3;
  }

  /* Les calques de fond du hero couvrent toute la section, rembourrage compris.
     PIÈGE : un enfant positionné en absolu prend sa ZONE DE GRILLE comme bloc
     conteneur dès qu'on lui donne une position de grille définie. La règle
     ci-dessus l'enfermait dans la rangée 2 : `inset: 0` ne faisait plus que
     quelques dizaines de pixels et la photo du hero disparaissait. On rend
     donc la position indéfinie — le bloc conteneur redevient la boîte de
     rembourrage de la section. Étendre sur toutes les rangées ne suffirait
     pas : la grille s'arrête au rembourrage. Le POC n'avait pas le problème,
     il n'a qu'une rangée et aucune règle de placement des enfants. */
  .chapitre > .hero__media,
  .chapitre > .hero__voile {
    grid-row: auto;
  }

  .chapitre__scene,
  .chapitre__bloc {
    min-height: 0;
  }

  /* Filet de séparation entre deux chapitres consécutifs. */
  .chapitre + .chapitre::before {
    content: '';
    position: absolute;
    inset: 0 0 auto 0;
    height: 1px;
    background: var(--filet);
  }

  /* L'aimantation ne s'active que sur un écran assez haut, et jamais contre
     l'avis du visiteur. */
  @media (height >= 40rem) and (prefers-reduced-motion: no-preference) {
    html {
      scroll-snap-type: y proximity;
    }
  }

  /* Écran court (paysage sur téléphone, petit portable) : la contrainte se
     relâche au lieu de comprimer, et l'aimantation se coupe. */
  @media (height < 40rem) {
    .chapitre {
      /* La hauteur redevient celle du contenu : les rangées explicites
         subsistent, mais `minmax(0, 1fr)` ne s'étire plus. */
      min-height: 0;
      padding-block: var(--s7);
      scroll-snap-align: none;
    }
  }

  /* Téléphone et écrans tactiles : aucune aimantation verticale. Au doigt,
     l'écran qui « tire » vers le début de chaque chapitre casse l'élan du
     défilement et donne une navigation saccadée. Le défilement reste libre ;
     seuls les rails horizontaux (cartes à balayer) gardent leur aimantation. */
  @media (max-width: 48rem), (pointer: coarse) {
    html {
      scroll-snap-type: none;
      scroll-behavior: auto;
    }

    .chapitre {
      scroll-snap-align: none;
    }
  }

  /* --- Lien d'évitement -------------------------------------------------- */

  .lien-evitement,
  .skip-link {
    position: absolute;
    left: var(--s4);
    top: -100px;
    z-index: 100;
    background: var(--accent);
    color: var(--accent-encre);
    padding: var(--s3) var(--s4);
    border-radius: var(--rayon);
    text-decoration: none;
    transition: top var(--court) var(--sortie);
  }

  .lien-evitement:focus,
  .skip-link:focus {
    top: var(--s3);
  }
}

/* ==========================================================================
   @layer composants — briques réutilisables, adaptées par container queries
   ========================================================================== */

@layer composants {
  /* ======================================================================
     En-tête et navigation — vocabulaire cible
     ====================================================================== */

  .entete {
    position: fixed;
    inset: 0 0 auto 0;
    z-index: 40;
    color: var(--encre);
    background-color: color-mix(in srgb, var(--fond) 88%, transparent);
    border-bottom: 1px solid var(--filet);
    backdrop-filter: blur(12px);
  }

  .entete__dedans {
    display: flex;
    align-items: center;
    justify-content: space-between;
    gap: var(--s4);
    min-height: var(--hauteur-entete);
    padding-inline: var(--marge);
  }

  .marque {
    display: flex;
    align-items: center;
    gap: var(--s4);
    min-width: 0;
    /* 42 px sans cette ligne : deux pixels sous la cible tactile. */
    min-block-size: 44px;
    color: inherit;
    text-decoration: none;
  }

  .marque__sceau {
    width: var(--sceau);
    height: var(--sceau);
    flex: none;
    display: grid;
    place-items: center;
    border: 1px solid currentcolor;
    border-radius: 50%;
    font-family: var(--serif);
    font-size: clamp(0.75rem, 0.65rem + 0.3vw, 0.9375rem);
    opacity: 0.8;
    overflow: hidden;
  }

  /* Logo envoye depuis l'admin (Reglages > Identite) : remplace les initiales,
     meme sceau circulaire. */
  .marque__sceau img {
    width: 100%;
    height: 100%;
    object-fit: cover;
  }

  /* Monogramme DC (logo valide) : plus de sceau circulaire, le dessin tient
     lui-meme lieu de pastille. Masque + currentcolor : meme couleur que le
     texte, clair sur la photo du hero puis encre au defilement. */
  .marque__sceau:has(.marque__logo) {
    width: auto;
    height: clamp(72px, 52px + 2.8vw, 100px);
    aspect-ratio: 1 / 1;
    border: 0;
    border-radius: 0;
    opacity: 1;
    overflow: visible;
  }

  .marque__logo {
    display: block;
    width: 100%;
    height: 100%;
    background-color: currentcolor;
    mask: url('/assets/images/logo-dc.png') center / contain no-repeat;
  }

  .marque__nom {
    display: block;
    font-family: var(--serif);
    font-size: clamp(1.5rem, 1.2rem + 1.1vw, 2.3rem);
    line-height: 1.1;
    letter-spacing: -0.015em;
  }

  /* Le métier, pas un sous-titre décoratif : c'est ce que le visiteur lit
     juste après le nom. L'emphase passe par la taille et la graisse, pas par
     l'accent — l'en-tête est transparent sur la photo du hero, une couleur
     fixe y perdrait son contraste dans un des deux états. */
  /* Agrandi et remonté à pleine opacité (demande client) : ce libellé passait
     inaperçu à côté du nom. Toujours pas d'accent (voir commentaire ci-dessus,
     inchangé), l'emphase reste sur la taille et la graisse. */
  .marque__role {
    display: block;
    margin-top: 4px;
    font-size: clamp(0.9rem, 0.78rem + 0.5vw, 1.2rem);
    font-weight: 600;
    text-transform: uppercase;
    letter-spacing: 0.1em;
    opacity: 1;
  }

  @media (width < 25rem) {
    .marque__role {
      font-size: 0.75rem;
      letter-spacing: 0.05em;
    }
  }

  .menu {
    display: none;
    align-items: center;
    /* La gouttière se resserre avant que les libellés ne se touchent. */
    gap: clamp(var(--s4), 0.4rem + 1.6vw, var(--s7));
  }

  .menu__lien {
    position: relative;
    color: inherit;
    opacity: 0.6;
    text-decoration: none;
    font-size: clamp(1.05rem, 0.92rem + 0.45vw, 1.4rem);
    padding-block: var(--s3);
    transition: opacity var(--moyen);
  }

  /* L'état actif est porté par `aria-current`, posé par le serveur ou le
     suivi de défilement : un seul attribut sert le lecteur d'écran ET le
     style. Aucune classe d'état en double. */
  .menu__lien:hover,
  .menu__lien[aria-current] {
    opacity: 1;
  }

  .menu__lien::after {
    content: '';
    position: absolute;
    left: 0;
    right: 0;
    bottom: 0;
    height: 1px;
    background: currentcolor;
    transform: scaleX(0);
    transform-origin: left;
    transition: transform var(--moyen) var(--sortie);
  }

  .menu__lien:hover::after,
  .menu__lien[aria-current]::after {
    transform: scaleX(1);
  }

  .bascule {
    width: 44px;
    height: 44px;
    display: grid;
    place-items: center;
    color: inherit;
    margin-right: calc(var(--s2) * -1);
  }

  .bascule__trait,
  .bascule__trait::before,
  .bascule__trait::after {
    display: block;
    width: 20px;
    height: 1.5px;
    background: currentcolor;
    transition: transform var(--moyen) var(--sortie), opacity var(--court);
  }

  .bascule__trait::before,
  .bascule__trait::after {
    content: '';
    position: relative;
  }

  .bascule__trait::before {
    top: -6px;
  }

  .bascule__trait::after {
    top: 4.5px;
  }

  .bascule[aria-expanded='true'] .bascule__trait {
    background: transparent;
  }

  .bascule[aria-expanded='true'] .bascule__trait::before {
    transform: translateY(6px) rotate(45deg);
  }

  .bascule[aria-expanded='true'] .bascule__trait::after {
    transform: translateY(-4.5px) rotate(-45deg);
  }

  /* `visibility: hidden` en plus de la hauteur nulle : sans elle, les liens
     restent atteignables au clavier alors qu'ils sont invisibles. L'état ne
     vit toujours que dans `aria-expanded`, lu par `:has()` — on ne
     réintroduit pas de seconde source de vérité. */
  .menu-mobile {
    display: grid;
    padding-inline: var(--marge);
    overflow: hidden;
    block-size: 0;
    visibility: hidden;
    transition:
      block-size var(--moyen) var(--sortie),
      visibility var(--moyen);
  }

  /* Menu ouvert : l'en-tête devient opaque, sinon les liens se liraient
     par-dessus la photo du hero. L'état est lu sur le bouton via `:has()`,
     pas posé par une classe. */
  .entete:has(.bascule[aria-expanded='true']) {
    background-color: var(--fond);
  }

  .entete:has(.bascule[aria-expanded='true']) .menu-mobile {
    block-size: auto;
    padding-block-end: var(--s5);
    visibility: visible;
  }

  .menu-mobile .menu__lien {
    padding: var(--s4) 0;
    font-size: 1.25rem;
    font-family: var(--serif);
    letter-spacing: -0.01em;
    border-bottom: 1px solid var(--filet);
    opacity: 0.85;
  }

  /* Page d'accueil : pas de burger. Les pages y sont déjà données par les
     tuiles-photos sous le hero (.raccourcis) ; le burger ne reste que sur les
     pages intérieures. */
  body:has(#chapitre-accueil) .bascule,
  body:has(#chapitre-accueil) .menu-mobile {
    display: none;
  }

  /* Bandeau agrandi : le menu complet ne tient qu'à partir de 72 rem, en
     dessous il reste le bouton burger. */
  @media (width >= 72rem) {
    .menu {
      display: flex;
    }

    .menu__lien {
      white-space: nowrap;
    }

    .bascule,
    .menu-mobile {
      display: none;
    }
  }

  /* ======================================================================
     Pied de page
     ====================================================================== */

  .pied {
    display: flex;
    flex-wrap: wrap;
    align-items: center;
    justify-content: space-between;
    gap: var(--s3);
    padding-top: var(--s5);
    border-top: 1px solid var(--filet);
    font-size: 0.8125rem;
    color: var(--encre-douce);
  }

  /* Le même pied, mais en bas de document plutôt qu'en sortie de chapitre :
     il porte alors les marges de page. */
  .pied--site {
    margin-top: var(--s8);
    padding-inline: var(--marge);
    padding-block: var(--s5) var(--s7);
  }

  /* Les liens du pied n'occupaient que la hauteur de leur texte, soit 17 px.
     Le rembourrage vertical les amène à la cible tactile sans allonger le pied
     de façon déraisonnable : ils restent sur une ligne. */
  .pied a {
    display: inline-flex;
    align-items: center;
    min-block-size: 44px;
    padding-inline: var(--s2);
    color: inherit;
    text-decoration: none;
  }

  .pied a:hover {
    color: var(--encre);
  }

  /* Accès discret à l'administration : aucun indice visuel, aucune adresse
     dans le HTML. Le serveur ne révèle le chemin qu'après authentification. */
  .pied__sceau,
  .footer__mark {
    cursor: default;
    user-select: none;
    -webkit-user-select: none;
  }

  /* ======================================================================
     Boutons et liens
     ====================================================================== */

  .bouton,
  .btn {
    display: inline-flex;
    align-items: center;
    justify-content: center;
    gap: var(--s2);
    min-height: 46px;
    padding: var(--s3) var(--s6);
    background: var(--accent);
    color: var(--accent-encre);
    border: 1px solid var(--accent);
    border-radius: var(--rayon);
    font-size: 0.9375rem;
    text-decoration: none;
    transition: transform var(--court) var(--sortie), opacity var(--court);
  }

  .bouton:hover,
  .btn:hover {
    opacity: 0.9;
    transform: translateY(-1px);
  }

  .bouton:disabled,
  .btn:disabled {
    opacity: 0.5;
    transform: none;
    cursor: not-allowed;
  }

  /* Appel à l'action de l'en-tête : présent en permanence, comme sur les
     sites de chefs privés — c'est la seule chose qu'on attend du visiteur. */
  .bouton--compact {
    /* Plus resserré que le bouton plein, mais pas sous la cible tactile : ce
       bouton n'apparaît qu'au-delà de 52 rem, une largeur qu'atteignent aussi
       les tablettes et les portables tactiles. */
    min-height: 44px;
    padding: var(--s2) var(--s4);
    font-size: 0.875rem;
  }

  .entete__action {
    display: none;
  }

  @media (width >= 52rem) {
    .entete__action {
      display: inline-flex;
      /* Plus massif que le compact : c'est l'unique appel à l'action. */
      min-height: 52px;
      padding: var(--s3) var(--s6);
      font-size: 1rem;
    }
  }

  .bouton--fantome,
  .btn--ghost {
    background: transparent;
    color: inherit;
    border-color: color-mix(in srgb, currentcolor 38%, transparent);
  }

  .bouton--fantome:hover,
  .btn--ghost:hover {
    border-color: currentcolor;
    opacity: 1;
  }

  /* Lien de sortie de chapitre : c'est lui qui mène à la page de détail. */
  /* Un cadre fin autour du mot, puis une flèche haute et grasse, discrète : le
     lien se lit comme un bouton sans prendre la place d'un appel à l'action.
     Au survol, le cadre se remplit d'accent et la flèche avance. */
  .lien-suite {
    display: inline-flex;
    align-items: center;
    gap: var(--s3);
    min-height: 44px;
    width: fit-content;
    padding: var(--s2) var(--s4) var(--s2) var(--s5);
    border: 1.5px solid currentcolor;
    border-radius: var(--rayon);
    color: inherit;
    text-decoration: none;
    font-size: 0.9375rem;
    letter-spacing: 0.01em;
    transition: background-color var(--moyen), border-color var(--moyen),
      color var(--moyen);
  }

  .lien-suite__fleche {
    flex: none;
    display: block;
    width: 1.1em;
    height: 1.5em;
    transition: transform var(--moyen) var(--sortie);
  }

  .lien-suite__fleche svg {
    display: block;
    width: 100%;
    height: 100%;
    fill: none;
    stroke: currentcolor;
    stroke-width: 2.6;
    stroke-linecap: round;
    stroke-linejoin: round;
  }

  .lien-suite:hover {
    background: var(--accent);
    border-color: var(--accent);
    color: var(--accent-encre);
  }

  .lien-suite:hover .lien-suite__fleche {
    transform: translateX(4px);
  }

  /* Liens de suite des chapitres de l'accueil (« Galerie », « Prestations »,
     « Qui je suis ») : la place ne manque pas, ils grandissent — plus de texte,
     un cadre plus large, une cible plus facile à viser. */
  .chapitre .lien-suite {
    gap: var(--s4);
    min-height: 56px;
    padding-inline: var(--s6) var(--s5);
    font-size: clamp(1.125rem, 1rem + 0.5vw, 1.5rem);
  }

  .retour {
    display: inline-flex;
    align-items: center;
    gap: var(--s2);
    min-height: 44px;
    color: var(--encre-douce);
    text-decoration: none;
    font-size: 0.875rem;
  }

  .retour:hover {
    color: var(--accent);
  }

  /* ======================================================================
     Textes
     ====================================================================== */

  .surtitre,
  .overline {
    font-size: 0.6875rem;
    text-transform: uppercase;
    letter-spacing: 0.18em;
    color: var(--encre-douce);
  }

  .phrase,
  .lead {
    font-size: var(--pas-1);
    line-height: var(--interligne-ample);
    color: var(--encre-douce);
    max-width: var(--mesure);
  }

  .prose {
    max-width: var(--mesure);
  }

  .prose > * + * {
    margin-top: var(--s5);
  }

  /* Les retours à la ligne saisis dans l'admin comptent : un champ éditable
     qui ne contient que du texte (aucun élément enfant, donc aucune
     indentation de gabarit à craindre) les affiche tels quels. */
  [data-champ]:not(:has(*)) {
    white-space: pre-line;
  }

  /* ======================================================================
     Localisation — un chef à domicile se choisit d'abord sur sa zone
     ====================================================================== */

  .lieu {
    display: inline-flex;
    align-items: center;
    gap: var(--s2);
    font-weight: 500;
    color: var(--encre);
  }

  .lieu svg {
    width: 1.05em;
    height: 1.05em;
    flex: none;
    opacity: 0.8;
  }

  /* En bloc, hors du sur-titre : taille de lecture, marqué à l'accent. */
  .lieu--bloc {
    display: flex;
    width: fit-content;
    font-size: 0.9375rem;
    font-weight: 400;
    text-transform: none;
    letter-spacing: 0;
    padding: var(--s3) var(--s4);
    border-left: 2px solid var(--accent);
    background: var(--accent-doux);
    border-radius: 0 var(--rayon) var(--rayon) 0;
  }

  .lieu--bloc svg {
    color: var(--accent);
    opacity: 1;
  }

  /* ======================================================================
     Carte pleine — image en fond, voile, texte superposé
     ====================================================================== */

  .carte {
    position: relative;
    display: block;
    width: 100%;
    height: 100%;
    padding: 0;
    overflow: hidden;
    border-radius: var(--rayon-media);
    background: var(--fond-alt);
    text-align: left;
    text-decoration: none;
    color: var(--sur-photo);
    isolation: isolate;
  }

  /* `.placeholder` est traité comme l'image : components.js pose l'un ou
     l'autre au même endroit selon qu'une photo est renseignée. */
  .carte img,
  .carte > .placeholder {
    position: absolute;
    inset: 0;
    width: 100%;
    height: 100%;
    object-fit: contain;
  }

  .carte__voile {
    position: absolute;
    inset: 0;
    background: linear-gradient(to top, rgb(8 5 3 / 82%) 0%, rgb(8 5 3 / 20%) 46%, transparent 74%);
  }

  .carte__texte {
    position: absolute;
    inset: auto 0 0 0;
    padding: var(--s5);
    display: grid;
    gap: var(--s1);
  }

  .carte__surtitre {
    font-size: 0.625rem;
    text-transform: uppercase;
    letter-spacing: 0.18em;
    color: var(--sur-photo-doux);
  }

  .carte__titre {
    font-family: var(--serif);
    font-size: var(--pas-1);
    line-height: 1.1;
    letter-spacing: -0.01em;
  }

  /* Tuile de galerie : même carte, mais paysage et légendée d'un simple
     libellé — ce n'est pas un titre de contenu. Produite par
     `galleryTile()` sur un <button>, d'où l'absence de bordure de bouton. */
  .carte--vignette {
    aspect-ratio: 3 / 2;
    height: auto;
  }

  .carte--vignette .carte__texte {
    padding: var(--s5) var(--s4) var(--s4);
  }

  /* Pastille de lecture — une vignette de galerie qui porte une vidéo. La
     mosaïque n'affiche que des photos : sans ce repère, rien ne distingue un
     élément qu'on peut lire d'un élément qu'on ne fait qu'agrandir. */
  .vignette__lecture {
    position: absolute;
    inset: auto var(--s3) var(--s3) auto;
    display: grid;
    place-items: center;
    width: 40px;
    height: 40px;
    border-radius: 50%;
    /* Fond neutre plutôt que la couleur d'accent : le repère doit se lire sur
       n'importe quelle photo sans devenir un appel à l'action. */
    background: rgb(0 0 0 / 0.55);
    backdrop-filter: blur(4px);
    color: #fff;
    /* Le triangle est optiquement décentré s'il est parfaitement centré. */
    padding-left: 2px;
  }

  .carte--vignette .carte__titre {
    font-family: var(--sans);
    font-size: 0.8125rem;
    line-height: 1.35;
    letter-spacing: 0;
  }

  /* ======================================================================
     Carte à légende sous l'image (série de trois)
     ====================================================================== */

  .serie {
    display: grid;
    gap: var(--s6);
    grid-template-columns: 1fr;
  }

  @media (width >= 52rem) {
    .serie {
      grid-template-columns: repeat(3, minmax(0, 1fr));
      gap: clamp(var(--s5), 2.4vw, 3.25rem);
    }
  }

  .serie__carte {
    display: grid;
    gap: var(--s4);
    align-content: start;
    text-decoration: none;
    color: inherit;
  }

  /* Photo entière : le cadre prend la hauteur de la photo, sans recadrage ni
     zoom au survol (un zoom rognerait les bords). */
  .serie__media {
    overflow: hidden;
    border-radius: var(--rayon-media);
    background: var(--fond-alt);
  }

  .serie__media img {
    display: block;
    width: 100%;
    height: auto;
    object-fit: contain;
  }

  .serie__titre {
    font-family: var(--serif);
    font-size: var(--pas-1);
    line-height: 1.15;
    letter-spacing: -0.01em;
  }

  .serie__resume {
    font-size: 0.9375rem;
    color: var(--encre-douce);
    max-width: 34ch;
  }

  .serie__meta {
    font-size: 0.8125rem;
    color: var(--encre-douce);
  }

  /* Aplat de remplacement quand aucune photo n'est renseignée : un fond
     travaillé (deux dégradés superposés, aucune image) plutôt qu'un
     rectangle plat. Sortie de l'ancienne section « Carte encadrée — compat,
     retiré en 11.9 » : le reste de ce bloc (`.card`, `.card__*`) a bien été
     retiré, mais `.placeholder` reste produite par `core.js` (`media()`) et
     `components.js` (`visuel()`) comme repli d'image — elle doit survivre. */
  .placeholder {
    width: 100%;
    height: 100%;
    display: grid;
    place-items: center;
    background-color: var(--fond-alt);
    background-image: radial-gradient(
        120% 90% at 15% 0%,
        color-mix(in srgb, var(--accent) 18%, transparent),
        transparent 60%
      ),
      linear-gradient(150deg, transparent 40%, color-mix(in srgb, var(--theme-sand) 45%, transparent));
    color: var(--encre-douce);
    font-family: var(--serif);
    font-size: var(--pas-2);
    letter-spacing: 0.04em;
  }

  /* ======================================================================
     Formulaires
     ====================================================================== */

  .champ,
  .field {
    display: grid;
    gap: var(--s2);
  }

  .champ__libelle,
  .field__label {
    font-size: 0.875rem;
    color: var(--encre-douce);
  }

  .field__required {
    color: var(--accent);
  }

  /* L'astérisque des champs obligatoires est dérivé de l'attribut `required`
     lui-même : impossible de le désynchroniser du balisage, et rien à écrire
     dans le gabarit. Le lecteur d'écran, lui, annonce déjà `required`. */
  .champ:has([required]) .champ__libelle::after {
    content: ' *';
    color: var(--accent);
  }

  .saisie,
  .input,
  .textarea,
  .select {
    width: 100%;
    min-height: 44px;
    padding: var(--s3);
    background: var(--surface);
    color: var(--encre);
    border: 1px solid var(--filet);
    border-radius: var(--rayon);
    transition: border-color var(--court), box-shadow var(--court);
  }

  .saisie:hover,
  .input:hover,
  .textarea:hover,
  .select:hover {
    border-color: var(--encre-douce);
  }

  /* Pas de `outline: none` : l'anneau :focus-visible global doit rester
     visible au clavier, le filet et l'ombre ne font que le renforcer. */
  .saisie:focus,
  .input:focus,
  .textarea:focus,
  .select:focus {
    border-color: var(--accent);
    box-shadow: 0 0 0 3px var(--accent-doux);
  }

  .saisie--zone,
  .textarea {
    min-height: 9rem;
    resize: vertical;
  }

  .formulaire,
  .form {
    display: grid;
    gap: var(--s4);
    max-width: 34rem;
    container: formulaire / inline-size;
  }

  .form__row {
    display: grid;
    gap: var(--s4);
  }

  @container formulaire (width >= 30rem) {
    .form__row {
      grid-template-columns: 1fr 1fr;
    }
  }

  /* Piège à robots : invisible pour l'humain, y compris au clavier. */
  .honeypot {
    position: absolute;
    left: -9999px;
    width: 1px;
    height: 1px;
    overflow: hidden;
  }

  .formulaire__etat,
  .form__status {
    padding: var(--s3) var(--s4);
    border: 1px solid var(--filet);
    border-radius: var(--rayon);
    font-size: 0.9375rem;
  }

  /* L'état est porté par un attribut, pas par une classe. */
  /* Attribut et valeurs en français, comme `data-champ` et `data-ville` :
     `contact.js` pose `data-etat="succes"` ou `"erreur"`. Ces sélecteurs
     étaient écrits en anglais — ils ne rencontraient donc jamais le JS, et le
     message d'envoi restait sans couleur, succès comme échec. */
  .formulaire__etat[data-etat='succes'],
  .form__status[data-etat='succes'] {
    border-color: var(--accent);
    background: var(--accent-doux);
    color: var(--encre);
  }

  .formulaire__etat[data-etat='erreur'],
  .form__status[data-etat='erreur'] {
    border-color: var(--accent);
    color: var(--accent);
  }

  .formulaire__etat:empty,
  .form__status:empty {
    display: none;
  }

  /* État de validation lu directement sur le champ, une fois le visiteur
     passé dessus (`:user-invalid`, jamais `:invalid` qui marquerait un
     formulaire vierge). Le signal ne repose pas sur la seule couleur : le
     message natif du navigateur reste, on ne fait que renforcer le filet. */
  :is(.saisie, .input, .textarea, .select):user-invalid {
    border-color: var(--accent);
  }

  .champ:has(:user-invalid) .champ__libelle,
  .field:has(:user-invalid) .field__label {
    color: var(--accent);
  }

  /* ======================================================================
     Filtres — pastilles et étiquettes
     ====================================================================== */

  .pastilles,
  .chips,
  .tag-list {
    display: flex;
    flex-wrap: wrap;
    gap: var(--s2);
  }

  .pastille,
  .chip {
    display: inline-flex;
    align-items: center;
    justify-content: center;
    min-height: 44px;
    padding: var(--s2) var(--s4);
    border: 1px solid var(--filet);
    border-radius: 999px;
    background: var(--surface);
    color: var(--encre-douce);
    font-size: 0.875rem;
    transition: color var(--court), border-color var(--court), background-color var(--court);
  }

  .pastille:hover,
  .chip:hover {
    border-color: var(--accent);
    color: var(--accent);
  }

  .pastille[aria-pressed='true'],
  .chip[aria-pressed='true'] {
    background: var(--accent);
    border-color: var(--accent);
    color: var(--accent-encre);
  }

  .tag {
    display: inline-block;
    padding: 2px var(--s3);
    border: 1px solid var(--filet);
    border-radius: 999px;
    background: var(--fond);
    color: var(--encre-douce);
    font-size: 0.75rem;
  }

  /* ======================================================================
     Citation
     ====================================================================== */

  .citation,
  .quote {
    font-family: var(--serif);
    font-style: italic;
    font-size: var(--pas-3);
    line-height: 1.25;
    letter-spacing: -0.015em;
    max-width: 24ch;
    color: inherit;
  }

  .citation::before,
  .quote::before {
    content: '«';
    color: var(--accent);
    margin-right: var(--s2);
  }

  .citation::after,
  .quote::after {
    content: '»';
    color: var(--accent);
    margin-left: var(--s2);
  }

  .quote__author {
    display: block;
    margin-top: var(--s4);
    font-family: var(--sans);
    font-style: normal;
    font-size: 0.8125rem;
    text-transform: uppercase;
    letter-spacing: 0.12em;
    color: var(--encre-douce);
  }

  /* ======================================================================
     Liste d'offres — le tarif aligné à droite, filet de séparation
     ====================================================================== */

  .offres {
    display: grid;
    margin-block: var(--s2);
  }

  .offres li {
    border-top: 1px solid var(--filet);
  }

  .offres li:last-child {
    border-bottom: 1px solid var(--filet);
  }

  /* Toute la ligne se clique ; la flèche, haute et grasse comme celle des
     liens de chapitre, avance au survol. */
  .offres__lien {
    display: flex;
    align-items: center;
    justify-content: space-between;
    gap: var(--s3);
    min-height: 56px;
    padding-block: var(--s3);
    color: inherit;
    text-decoration: none;
  }

  .offres__nom {
    font-family: var(--serif);
    font-size: var(--pas-1);
    letter-spacing: -0.01em;
  }

  .offres__fleche {
    flex: none;
    display: block;
    width: 1.1rem;
    height: 1.5rem;
    transition: transform var(--moyen) var(--sortie), color var(--moyen);
  }

  .offres__fleche svg {
    display: block;
    width: 100%;
    height: 100%;
    fill: none;
    stroke: currentcolor;
    stroke-width: 2.6;
    stroke-linecap: round;
    stroke-linejoin: round;
  }

  .offres__lien:hover .offres__fleche {
    transform: translateX(4px);
    color: var(--accent);
  }

  /* ======================================================================
     Listes marquées
     ====================================================================== */

  .checklist {
    display: grid;
    gap: var(--s2);
    margin-top: var(--s4);
  }

  .checklist li {
    display: grid;
    grid-template-columns: auto 1fr;
    gap: var(--s3);
    font-size: 0.9375rem;
    color: var(--encre-douce);
  }

  .checklist li::before {
    content: '';
    width: 6px;
    height: 6px;
    margin-top: 0.62em;
    border-radius: 50%;
    border: 1px solid var(--accent);
  }

  /* ======================================================================
     État vide
     ====================================================================== */

  .empty {
    padding: var(--s8) var(--s5);
    text-align: center;
    border: 1px dashed var(--filet);
    border-radius: var(--rayon-media);
    color: var(--encre-douce);
  }

  .empty__title {
    margin-bottom: var(--s2);
    font-family: var(--serif);
    font-size: var(--pas-1);
    color: var(--encre);
  }

  /* ======================================================================
     Boîtes de dialogue natives

     `<dialog>` fournit Échap, le piège de focus et le fond assombri : les
     deux pièges de focus écrits à la main disparaissent (lots 11.x). Les
     règles génériques sont ici pour que toute future boîte en hérite.
     ====================================================================== */

  dialog::backdrop {
    background: rgb(8 5 3 / 76%);
    backdrop-filter: blur(4px);
  }

  @supports (transition-behavior: allow-discrete) {
    dialog {
      opacity: 0;
      transition: opacity var(--moyen) var(--sortie), display var(--moyen) allow-discrete,
        overlay var(--moyen) allow-discrete;
    }

    dialog[open] {
      opacity: 1;
    }

    @starting-style {
      dialog[open] {
        opacity: 0;
      }
    }
  }
}

/* ==========================================================================
   @layer pages — accueil, recettes, fiche, galerie, à-propos, prestations,
                  contact, 404
   ========================================================================== */

@layer pages {
  /* ======================================================================
     Accueil — chapitres
     ====================================================================== */

  /* Un chapitre désactivé depuis l'admin est simplement absent : il ne génère
     pas de boîte, donc ne consomme pas de numéro. */
  .chapitre[hidden] {
    display: none;
  }

  .chapitre__intro {
    display: grid;
    gap: var(--s3);
    max-width: 44ch;
  }

  .chapitre__titre {
    font-size: var(--pas-5);
  }

  /* Tête de chapitre pleine largeur : titre à gauche, lien de sortie à
     droite ; empilés sous 34rem de conteneur. */
  .chapitre__tete {
    display: flex;
    flex-wrap: wrap;
    align-items: flex-end;
    justify-content: space-between;
    gap: var(--s5) var(--s7);
  }

  @media (width < 40rem) {
    .chapitre__tete {
      flex-direction: column;
      align-items: flex-start;
      gap: var(--s3);
    }
  }

  /* « Les coulisses » : titre et texte prennent la place laissée libre à côté
     du lien de sortie, au lieu de rester dans une colonne de 44 caractères. Le
     texte garde un plafond de lecture ; sous 40 rem, la tête est empilée et le
     bloc occupe la largeur disponible. */
  #chapitre-galerie .chapitre__intro {
    flex: 1 1 24rem;
    max-width: none;
  }

  #chapitre-galerie .chapitre__titre {
    text-wrap: balance;
  }

  #chapitre-galerie .phrase {
    max-width: min(100%, 110ch);
  }

  /* Tête empilée : le basis de 24 rem ne doit pas devenir une hauteur. */
  @media (width < 40rem) {
    #chapitre-galerie .chapitre__intro {
      flex: none;
    }
  }

  /* --- Hero ------------------------------------------------------------- */

  .chapitre--hero {
    grid-template-rows: minmax(0, 1fr) auto;
    align-content: end;
    isolation: isolate;
    color: var(--sur-photo);
    /* Le texte est collé en bas : sur une fenêtre peu haute, sans cette marge
       le haut du titre passait sous la barre de navigation fixe. La section
       grandit plutôt que de cacher la première ligne. */
    padding-block: calc(var(--hauteur-entete) + var(--s5)) var(--s8);
  }

  .chapitre--hero::before {
    content: none;
  }

  .chapitre--hero .hero__bloc {
    grid-row: 1;
  }

  .chapitre--hero .hero__repere {
    grid-row: 2;
  }

  .hero__media {
    position: absolute;
    inset: 0;
    z-index: -2;
    overflow: hidden;
    background: var(--fond-alt);
  }

  .hero__media img,
  .hero__media video {
    width: 100%;
    height: 100%;
    object-fit: cover;
    transform: scale(1.08);
  }

  /* Voile en dégradé, pas un aplat translucide : le titre reste lisible sans
     éteindre la photo. */
  .hero__voile {
    position: absolute;
    inset: 0;
    z-index: -1;
    background: linear-gradient(
      to top,
      rgb(10 7 4 / 86%) 0%,
      rgb(10 7 4 / 58%) 32%,
      rgb(10 7 4 / 12%) 64%,
      rgb(10 7 4 / 38%) 100%
    );
  }

  /* --- Hero vidéo sur écran en portrait ---------------------------------
     Une vidéo 16:9 en plein cadre sur un téléphone n'en montre qu'un tiers :
     on la rend ENTIÈRE, en pleine largeur, sous l'en-tête, et le reste de
     l'écran prend une version floue et assombrie de sa propre image — la
     même ambiance, sans bandes noires ni second décodage. Le texte passe
     dessous. L'image fixe (sans vidéo, ou mouvement réduit) reste en plein
     cadre : seule la vidéo est concernée. */
  @media (orientation: portrait) {
    .chapitre--hero:has(.hero__media video) {
      padding-block-start: calc(var(--hauteur-entete) + 100vw / var(--hero-ratio, 1.7778) + var(--s6));
    }

    .hero__media:has(video)::before {
      content: '';
      position: absolute;
      inset: -10%;
      background: var(--hero-poster, none) center / cover no-repeat;
      filter: blur(36px) saturate(1.15) brightness(0.55);
    }

    /* Spécificité volontairement plus haute que la parallaxe du hero plein cadre
       (.chapitre--hero .hero__media video), déclarée plus bas. */
    .chapitre--hero .hero__media:has(video) video {
      position: absolute;
      inset-inline: 0;
      inset-block-start: var(--hauteur-entete);
      width: 100%;
      height: auto;
      aspect-ratio: var(--hero-ratio, 1.7778);
      object-fit: contain;
      transform: none;
      animation: none;
      box-shadow: 0 0.75rem 2.5rem rgb(0 0 0 / 45%);
    }
  }

  .hero__bloc {
    align-self: end;
    display: grid;
    gap: var(--s5);
    justify-items: start;
  }

  .hero__titre {
    font-size: clamp(2.2rem, 1rem + 5.2vw, 6.4rem);
    font-weight: 700;
    /* Un peu plus que 0.94 : l'accent de « à » (ligne du bas) ne doit pas
       toucher le jambage du « p » de « privé » (ligne du haut). */
    line-height: 1.06;
    letter-spacing: -0.035em;
    max-width: 13ch;
  }

  .hero__titre span {
    display: block;
  }

  /* Le serif italique ET les majuscules espacées se cumulaient : illisible.
     On garde une seule des deux idées — la capitale espacée, en sans. */
  .hero__amorce {
    display: flex;
    flex-wrap: wrap;
    align-items: center;
    gap: var(--s2) var(--s4);
    font-size: 0.75rem;
    text-transform: uppercase;
    letter-spacing: 0.18em;
    color: var(--sur-photo-doux);
  }

  /* Second bloc de titre, sous le grand : c'est lui qui dit l'offre. */
  .hero__seconde {
    font-family: var(--serif);
    font-size: var(--pas-1);
    line-height: 1.3;
    letter-spacing: -0.015em;
    color: var(--sur-photo);
    max-width: 30ch;
  }

  /* Son libellé est en `visuellement-cache` : sans dimensions minimales, il ne
     restait que la flèche de 16 px, intapable au doigt. */
  .hero__repere {
    justify-self: center;
    display: inline-flex;
    flex-direction: column;
    align-items: center;
    justify-content: center;
    min-inline-size: 44px;
    min-block-size: 44px;
    gap: var(--s2);
    color: var(--sur-photo-doux);
    text-decoration: none;
    font-size: 0.625rem;
    text-transform: uppercase;
    letter-spacing: 0.2em;
  }

  .chapitre--hero .surtitre,
  .chapitre--hero .overline {
    color: var(--sur-photo-doux);
  }

  .chapitre--hero .lieu {
    color: var(--sur-photo);
  }

  /* --- Composition texte / image ----------------------------------------
     Deux gabarits, deux seulement : `.chapitre__bloc` (texte serré face à UNE
     image paysage) et `.serie` (une rangée de cartes sous un titre). Toutes
     les images sont en 3/2 et ne sont jamais étirées sur la hauteur du
     chapitre — c'est ce qui produisait des bandes verticales.
     ---------------------------------------------------------------------- */

  .chapitre__bloc {
    display: grid;
    grid-template-rows: auto auto;
    gap: var(--s5);
    align-content: center;
  }

  @media (width >= 52rem) {
    /* Deux moitiés exactes : le texte à gauche, l'image à droite — et
       l'inverse un chapitre sur deux. */
    .chapitre__bloc {
      grid-template-rows: auto;
      grid-template-columns: minmax(0, 1fr) minmax(0, 1fr);
      gap: clamp(var(--s7), 4vw, 6rem);
      align-items: center;
    }

    /* Image à gauche un chapitre sur deux. On échange les colonnes plutôt que
       d'utiliser `order` : celle-ci déplace l'élément mais pas la largeur de
       la colonne, et l'image se retrouvait comprimée. */
    .chapitre--inverse .bloc__media {
      grid-column: 1;
      grid-row: 1;
    }

    .chapitre--inverse .bloc__texte {
      grid-column: 2;
      grid-row: 1;
    }
  }

  .bloc__texte {
    display: grid;
    gap: var(--s5);
    align-content: center;
    max-width: 46ch;
  }

  .bloc__texte .phrase {
    max-width: 40ch;
  }

  /* Le cadre n'impose aucune proportion : il épouse la photo. (Sur l'accueil,
     cadrerSelonPhoto() lui pose la proportion de la photo en ligne.) */
  .bloc__media {
    overflow: hidden;
    border-radius: var(--rayon-media);
    background: var(--fond-alt);
  }

  /* Photo entière : cadrerSelonPhoto() donne au cadre la proportion de la
     photo, `contain` empêche tout recadrage. La vidéo, elle, remplit son cadre. */
  .bloc__media img {
    display: block;
    width: auto;
    max-width: 100%;
    height: auto;
    max-height: min(72svh, 44rem);
    margin-inline: auto;
  }

  /* Grands écrans : le plafond de hauteur monte, la photo grandit avec la
     place disponible (toujours entière). */
  @media (width >= 90rem) {
    .bloc__media img {
      max-height: min(80svh, 60rem);
    }
    object-fit: contain;
  }

  .bloc__media video {
    width: 100%;
    height: 100%;
    object-fit: cover;
  }

  /* Passé le seuil des deux colonnes, les bornes ci-dessus — pensées pour la
     colonne unique du téléphone — laisseraient le texte en bande étroite au
     milieu d'une demi-page vide. Ce bloc doit rester APRÈS `.bloc__texte` et
     `.bloc__media` : à spécificité égale, c'est l'ordre du fichier qui
     tranche, une media query n'y change rien.

     Le gabarit d'origine ne suivait pas la taille de l'écran : le texte
     plafonnait à `--pas-1` et à 40ch pendant que la page s'élargissait. Ici
     l'image remplit exactement sa moitié et le texte grandit avec l'écran —
     corps fluide, mesure plus large — sans dépasser une longueur de ligne
     lisible. */

  @media (width >= 52rem) {
    .bloc__texte {
      max-width: none;
    }

    /* Le paragraphe, lui, reste borné : une ligne de 1100 px ne se lit pas.
       66ch au lieu de 40ch, et un corps qui suit la largeur de l'écran au
       lieu de plafonner à 1.41rem — sur un grand moniteur, prendre plus de
       place se joue d'abord sur la taille du texte. */
    .bloc__texte .phrase {
      max-width: 66ch;
      font-size: clamp(1.2rem, 0.95rem + 0.7vw, 1.6rem);
    }

    /* L'image occupe toute sa moitié. `max-height` la retient sur un écran
       très large, où le rapport 3/2 la ferait dépasser la hauteur du
       chapitre ; le recadrage est absorbé par `object-fit: cover`, jamais par
       une déformation. */
    .bloc__media {
      width: 100%;
      height: auto;
    }
  }

  .chapitre--serie {
    grid-template-rows: auto minmax(0, 1fr);
    align-content: center;
    gap: var(--s8);
  }

  .chapitre--contact {
    background: var(--fond-alt);
  }

  .contact__scene {
    display: grid;
    gap: var(--s5);
    align-content: center;
    justify-items: start;
  }

  .contact__coord {
    display: grid;
    gap: var(--s3);
  }

  .contact__coord a {
    display: inline-flex;
    align-items: center;
    /* 43 px sans cette ligne. Un numéro de téléphone se compose au doigt :
       c'est le lien qui doit le plus tenir la cible tactile. */
    min-block-size: 44px;
    font-family: var(--serif);
    font-size: var(--pas-3);
    color: var(--encre);
    text-decoration: none;
    letter-spacing: -0.02em;
  }

  .contact__coord a:hover {
    color: var(--accent);
  }

  /* Chapitre Contact de l'accueil : un numéro et surtout une adresse e-mail à
     --pas-3 en serif débordaient de leur colonne (« denischefprive@outlook »
     coupé contre la photo). Taille réduite, sans empattements — plus sobre pour
     une adresse — et repli de ligne possible en dernier recours. */
  .chapitre--contact .contact__coord a {
    font-family: var(--sans);
    font-weight: 500;
    font-size: clamp(1.0625rem, 0.98rem + 0.45vw, 1.375rem);
    letter-spacing: 0;
    min-width: 0;
    overflow-wrap: anywhere;
  }

  .chapitre--contact .insta {
    margin-top: var(--s2);
  }

  /* Téléphone — la hauteur est la ressource rare : on resserre les marges et
     on retire ce qui ne porte pas d'information, pour que la photo garde la
     place. La troisième carte d'une série attend sur la page de détail. */
  @media (width < 40rem) {
    .chapitre {
      padding-block: calc(var(--hauteur-entete) + var(--s5)) var(--s5);
      gap: var(--s4);
    }

    .chapitre__bloc,
    .chapitre--serie {
      gap: var(--s5);
    }

    .hero__bloc {
      gap: var(--s4);
    }

    /* Contact : les coordonnées à gauche, la photo à droite, comme sur grand
       écran, plutôt que la photo en dessous qui rallonge la page. Colonne de
       texte un peu plus large que la photo ; adresse et numéro peuvent passer
       à la ligne plutôt que de déborder. */
    .chapitre--contact .chapitre__bloc {
      grid-template-rows: auto;
      grid-template-columns: minmax(0, 1.45fr) minmax(0, 1fr);
      align-items: center;
      gap: var(--s4);
    }

    .chapitre--contact .bloc__texte {
      gap: var(--s3);
      min-width: 0;
    }

    .chapitre--contact .chapitre__titre {
      font-size: clamp(1.75rem, 1.2rem + 3vw, 2.5rem);
    }

    .chapitre--contact .contact__coord a {
      font-size: 0.9375rem;
      min-block-size: 40px;
    }

    /* Sur téléphone, la série devient un rail à aimantation plutôt que de
       masquer sa troisième carte : l'accueil ne montre que trois entrées
       épinglées, en cacher une reviendrait à perdre un tiers du contenu
       choisi depuis l'admin. C'est aussi ce que demande le POC — une carte
       plein pouce au doigt. */
    .serie {
      /* Sans ce reset, la piste explicite `1fr` de la règle de base (au-dessus,
         hors media query) reste active pour la première carte : elle seule
         hérite d'une largeur de colonne différente des suivantes (posées par
         grid-auto-columns), ce qui désaligne son image et la fait chevaucher
         la carte suivante lors de l'aimantation. */
      grid-template-columns: none;
      grid-auto-flow: column;
      grid-auto-columns: clamp(15rem, 74vw, 26rem);
      overflow-x: auto;
      overscroll-behavior-inline: contain;
      scroll-snap-type: x proximity;
      scroll-padding-inline: var(--marge);
      padding-block-end: var(--s3);
      scrollbar-width: thin;
      scrollbar-color: var(--filet) transparent;
    }

    .serie > * {
      scroll-snap-align: start;
      min-width: 0;
    }

    .pied {
      flex-direction: column;
      align-items: flex-start;
      gap: var(--s2);
    }
  }

  /* ======================================================================
     Ouverture éditoriale des pages internes

     Remplace le bandeau plein cadre en couleur d'accent : sur-titre, très
     grand titre, filet. C'est aussi ce qui supprime le dernier `100vw` du
     fichier (l'ancien `.page-head::before`).
     ====================================================================== */

  .entete-page,
  .page-head {
    display: grid;
    gap: var(--s4);
    justify-items: start;
  }

  .entete-page__titre {
    font-size: var(--pas-6);
  }

  .entete-page__filet {
    display: block;
    width: 4rem;
    height: 2px;
    margin-block: var(--s2);
    background: var(--accent);
  }

  .page-head h1 {
    color: var(--encre);
  }

  .page-head__intro {
    max-width: 56ch;
    color: var(--encre-douce);
    font-size: var(--pas-1);
    line-height: var(--interligne-ample);
  }

  /* Badge d'appartenance (À propos) : logo fixe + mention réglable. Un simple
     filet, pas un aplat — l'accent des boutons est déjà bien présent sur la
     page, ce badge doit rester discret, une mention plutôt qu'un CTA. */
  .badge-membre {
    display: inline-flex;
    flex-wrap: wrap;
    align-items: center;
    gap: var(--s3) var(--s4);
    margin-top: var(--s3);
    padding: var(--s3) var(--s5);
    border: 1px solid color-mix(in srgb, var(--accent) 35%, var(--filet));
    /* Arrondi fixe plutôt qu'une pastille : sur une colonne étroite le texte
       passe sous le logo, et une pastille de 999px ferait une forme en boudin. */
    border-radius: 1.5rem;
    font-size: clamp(1rem, 0.92rem + 0.4vw, 1.3125rem);
    font-weight: 500;
    color: var(--encre);
  }

  .badge-membre img {
    height: clamp(2.5rem, 2.2rem + 1.2vw, 3.5rem);
    width: auto;
  }

  /* Le badge n'affiche que le logo, qui écrit déjà le nom de l'Académie : la
     mention réglable reste dans la page pour les lecteurs d'écran
     (.visuellement-cache) et vide cette mention retire toujours le badge. */
  .badge-membre {
    flex-wrap: nowrap;
    padding: var(--s2) var(--s4);
    border-radius: 999px;
  }

  .badge-membre img {
    height: clamp(1.75rem, 1.5rem + 1.2vw, 2.75rem);
  }

  /* Mon parcours : le badge ne touche jamais la photo. Empilés (téléphone), le
     texte finit sur le badge et la photo à saignée démarre juste après : sans
     cette marge, rien ne les sépare. Côte à côte, elle s'ajoute à la gouttière. */
  .apropos-ouverture > .entete-page {
    padding-block-end: var(--s6);
  }

  /* ======================================================================
     Recettes — liste
     ====================================================================== */

  .filtres,
  .filters {
    display: flex;
    flex-wrap: wrap;
    align-items: center;
    justify-content: space-between;
    gap: var(--s4);
  }

  .filters {
    padding-block: var(--s5);
  }

  .filtres__recherche,
  .filters__search {
    flex: 1 1 15rem;
    max-width: 22rem;
  }

  /* Filtres + résultats sous un seul élément de grille : un `position:
     sticky` posé sur un enfant direct de `.page` serait enfermé dans sa zone
     de grille — donc dans sa propre hauteur — et ne collerait jamais. */
  .catalogue {
    display: grid;
    align-content: start;
    gap: var(--s5);
  }

  /* Collants sous l'en-tête : la recherche et les catégories restent sous la
     main en parcourant la grille. Le fond doit être opaque au défilement,
     sinon les cartes se lisent au travers. */
  .filtres--collants {
    position: sticky;
    top: var(--hauteur-entete);
    z-index: 20;
    padding-block: var(--s4);
    background: color-mix(in srgb, var(--fond) 92%, transparent);
    backdrop-filter: blur(10px);
  }

  /* Compteur, grille et état vide : un seul bloc, pour que la gouttière de
     `.page` ne s'insère pas entre le compteur et sa liste. */
  .resultats {
    display: grid;
    gap: var(--s4);
  }

  /* Deux colonnes dès le téléphone — une carte pleine largeur obligerait à
     défiler pour voir trois recettes — et jusqu'à six sur grand écran. */
  .grille {
    display: grid;
    gap: var(--s3);
    grid-template-columns: repeat(auto-fill, minmax(clamp(9.5rem, 22vw, 18rem), 1fr));
  }

  /* Seule exception au 3/2 généralisé : un cadrage plus vertical permet de
     tenir plusieurs colonnes denses sans écraser les images. */
  .grille .carte {
    aspect-ratio: 3 / 4;
    height: auto;
  }

  .grid--wide {
    grid-template-columns: repeat(auto-fill, minmax(min(100%, 340px), 1fr));
  }

  /* ======================================================================
     Fiche recette
     ====================================================================== */

  /* `.page` fait tout le gros œuvre ; la fiche resserre seulement sa
     gouttière : retour, ouverture éditoriale et image forment un seul geste. */
  .fiche {
    gap: var(--s6);
  }

  /* La fiche est une vraie page navigable (`<article class="page fiche">`),
     pas la modale du POC : elle n'a donc ni largeur propre, ni fond, ni
     défilement interne — `.page` fait tout cela. On ne décrit ici que sa
     composition. Le `view-transition-name` de l'image d'ouverture n'est pas
     écrit en CSS : il est posé par la CSSOM depuis `pages/recette.js`, à
     partir du slug, pour correspondre à la carte cliquée. */
  .fiche__media {
    overflow: hidden;
    background: var(--fond-alt);
  }

  /* `aspect-ratio` sur l'image, pas sur la figure (même motif que .portrait
     ci-dessous) : c'est ce qui laisse `.bleed > img` (max-height: 60svh, voir
     plus haut) réellement raccourcir l'image sur un très grand écran. Un
     aspect-ratio posé sur la figure, lui, ne se serait pas laissé plafonner —
     elle aurait gardé sa hauteur d'origine, vide sous l'image raccourcie. */
  .fiche__media img {
    display: block;
    width: 100%;
    height: auto;
    object-fit: contain;
  }

  .fiche__faits {
    display: flex;
    flex-wrap: wrap;
    gap: var(--s5) var(--s7);
    padding-block: var(--s4);
    border-block: 1px solid var(--filet);
  }

  /* Accepte les deux structures : `<div class="fiche__fait"><dt><dd>` et une
     suite de `<dt>/<dd>` posée directement dans le `<dl>`. */
  .fiche__fait {
    display: grid;
    gap: var(--s1);
  }

  .fiche__faits dt {
    font-size: 0.625rem;
    text-transform: uppercase;
    letter-spacing: 0.16em;
    color: var(--encre-douce);
  }

  .fiche__faits dd {
    margin: 0;
    font-family: var(--serif);
    font-size: var(--pas-1);
  }

  .fiche__grille {
    display: grid;
    gap: var(--s7);
    align-items: start;
  }

  @container gabarit (width >= 52rem) {
    .fiche__grille {
      grid-template-columns: minmax(0, 1fr) 20rem;
      gap: var(--s8);
    }
  }

  .fiche__corps {
    display: grid;
    gap: var(--s5);
    align-content: start;
    max-width: 68ch;
  }

  .fiche__corps h2 {
    font-size: var(--pas-3);
  }

  /* Colonne d'ingrédients : collante dès qu'il y a deux colonnes, pour rester
     lisible pendant qu'on descend dans les étapes. */
  .fiche__aparte {
    display: grid;
    gap: var(--s4);
    align-content: start;
    padding: var(--s5);
    background: var(--surface);
    border: 1px solid var(--filet);
    border-radius: var(--rayon-media);
  }

  .fiche__aparte h2 {
    font-size: var(--pas-2);
  }

  @container gabarit (width >= 52rem) {
    .fiche__aparte {
      position: sticky;
      top: calc(var(--hauteur-entete) + var(--s5));
    }
  }

  .fiche__suite {
    display: grid;
    gap: var(--s5);
    padding-top: var(--s7);
    border-top: 1px solid var(--filet);
  }

  /* Étapes numérotées par compteur CSS : le numéro n'est jamais dans le HTML,
     retirer une étape depuis l'admin ne laisse pas de trou. */
  .etapes {
    counter-reset: etape;
    display: grid;
    gap: var(--s5);
  }

  .etapes li {
    counter-increment: etape;
    display: grid;
    grid-template-columns: 2rem 1fr;
    gap: var(--s4);
  }

  .etapes li::before {
    content: counter(etape);
    font-family: var(--serif);
    font-size: var(--pas-1);
    color: var(--accent);
    line-height: 1.5;
    border-top: 1px solid var(--accent);
    padding-top: var(--s2);
  }

  .astuce {
    padding: var(--s4) var(--s5);
    border-left: 2px solid var(--accent);
    background: var(--accent-doux);
    border-radius: 0 var(--rayon) var(--rayon) 0;
  }

  .astuce__libelle {
    margin-bottom: var(--s2);
    font-size: 0.6875rem;
    text-transform: uppercase;
    letter-spacing: 0.12em;
    color: var(--accent);
  }

  /* Mots-clés : la règle porte sur le `<li>` pour ne rien exiger du script
     qui les fabrique. */
  .etiquettes {
    display: flex;
    flex-wrap: wrap;
    gap: var(--s2);
  }

  .etiquettes li,
  .etiquette {
    display: inline-block;
    padding: 2px var(--s3);
    border: 1px solid var(--filet);
    border-radius: 999px;
    background: var(--fond);
    color: var(--encre-douce);
    font-size: 0.75rem;
  }

  /* Colonne d'ingrédients de la fiche recette. Seule survivante de l'ancienne
     section « Recette — page complète (compat, retiré en 11.9) » : ses
     voisines `.recipe`, `.recipe__*`, `.fact`, `.steps`, `.aside-block`,
     `.tip` et `.breadcrumb` ont bien été retirées, mais `.ingredients` reste
     produite par `public/pages/recette.html` (`<ul class="ingredients"
     id="ingredients">`) et remplie par `public/assets/js/pages/recette.js` —
     elle doit rester active. */
  .ingredients {
    display: grid;
    gap: var(--s3);
  }

  .ingredients li {
    display: grid;
    grid-template-columns: auto 1fr;
    gap: var(--s3);
    padding-bottom: var(--s3);
    border-bottom: 1px solid var(--filet);
    font-size: 0.9375rem;
  }

  .ingredients li::before {
    content: '';
    width: 5px;
    height: 5px;
    margin-top: 0.7em;
    border-radius: 50%;
    background: var(--accent);
  }

  /* ======================================================================
     Galerie

     Mosaïque plutôt qu'une grille de carrés : motif déterministe, une tuile
     sur cinq occupe deux colonnes. Toutes les tuiles sont en 3/2 et posées
     en haut de leur rangée (`align-items: start`) : aucune n'est étirée.
     ====================================================================== */

  /* Mosaïque : une grille de tuiles dont une sur cinq, plus grande, occupe deux
     colonnes et deux rangées. RÈGLE : une photo n'est jamais recadrée ni
     déformée, toujours entière — la tuile prend la forme de la photo (hauteur
     automatique), pas l'inverse. Les photos du site étant verticales, une
     grande tuile (deux colonnes) a exactement la hauteur de deux petites
     rangées. `align-items: start` évite d'étirer une tuile dans sa rangée. */
  .mosaique {
    display: grid;
    gap: var(--s3);
    grid-auto-flow: dense;
    align-items: start;
    grid-template-columns: repeat(2, minmax(0, 1fr));
  }

  .mosaique > .carte--vignette {
    width: 100%;
    height: auto;
    aspect-ratio: auto;
  }

  .mosaique .carte--vignette img {
    position: static;
    display: block;
    width: 100%;
    height: auto;
    object-fit: contain;
  }

  /* Téléphone : deux colonnes, la grande tuile prend toute la largeur. */
  .mosaique > :nth-child(5n + 1) {
    grid-column: span 2;
  }

  /* Les légendes sont des noms de plats : limitées à quatre lignes (le nom
     complet reste dans l'aria-label et dans la visionneuse). Masquées dans les
     petites tuiles du téléphone. */
  .mosaique .carte__titre {
    display: -webkit-box;
    -webkit-box-orient: vertical;
    -webkit-line-clamp: 4;
    line-clamp: 4;
    overflow: hidden;
  }

  @container gabarit (width < 34rem) {
    .mosaique .carte__texte {
      display: none;
    }
  }

  /* À partir d'un conteneur de 34 rem : autant de colonnes de 15 rem que la
     largeur en loge ; la grande tuile occupe aussi deux rangées. */
  @container gabarit (width >= 34rem) {
    .mosaique {
      grid-template-columns: repeat(auto-fill, minmax(min(100%, 15rem), 1fr));
    }

    .mosaique > :nth-child(5n + 1) {
      grid-column: span 2;
      grid-row: span 2;
    }
  }

  /* Grands écrans : des tuiles plus larges, donc des photos plus grandes. Le
     seuil suit la largeur du conteneur, pas celle de la fenêtre. */
  @container gabarit (width >= 70rem) {
    .mosaique {
      grid-template-columns: repeat(auto-fill, minmax(min(100%, 20rem), 1fr));
    }
  }

  @container gabarit (width >= 100rem) {
    .mosaique {
      grid-template-columns: repeat(auto-fill, minmax(min(100%, 26rem), 1fr));
    }
  }

  /* Portrait à saignée : déborde des marges de la page en annulant --marge,
     jamais par un 100vw. Cadré en 3/2 comme toutes les images du site.

     Le ratio est porté par l'image, pas par le cadre : en pleine largeur, le
     plafond `max-height: 60svh` de `.bleed > img` rogne la photo, et un cadre
     qui garderait son propre 3/2 laisserait sous elle une bande de fond vide
     de la hauteur perdue (plus de 1000 px sur un grand écran). Le <figure> se
     contente donc d'encadrer ce que l'image mesure vraiment. */
  .portrait {
    overflow: hidden;
    background: var(--fond-alt);
  }

  .portrait img {
    display: block;
    width: 100%;
    height: auto;
    object-fit: contain;
  }

  /* À propos : texte à gauche, portrait à DROITE. La photo reste entière et
     prend la largeur de sa colonne ; `!important` écrase les bornes posées en
     ligne par cadrerSelonPhoto() (plafond de hauteur, largeur automatique),
     pensées pour le portrait pleine largeur du téléphone. */
  @media (width >= 52rem) {
    .apropos-ouverture {
      display: grid;
      grid-template-columns: repeat(2, minmax(0, 1fr));
      column-gap: var(--s8);
      align-items: start;
    }

    /* Photo centrée dans sa moitié de page, pas collée au bord droit. */
    .apropos-ouverture .portrait.bleed {
      justify-self: center;
      width: 100%;
      max-width: 24rem;
      margin-inline: 0;
      border-radius: var(--rayon-media);
    }

    .apropos-ouverture .portrait img {
      width: 100% !important;
      max-height: none !important;
      aspect-ratio: auto !important;
      margin-inline: 0 !important;
    }
  }

  /* --- Visionneuse en <dialog> natif ------------------------------------
     Échap, piège de focus, fond assombri et retour du focus sont gérés par
     le navigateur. L'état tient dans l'attribut `open` : aucune classe
     d'état, aucun `hidden` doublé.
     ---------------------------------------------------------------------- */

  /* `display` n'est posé QUE sur l'état ouvert : une règle `display: grid`
     sur la boîte fermée l'emporterait sur `dialog:not([open])` du navigateur
     et la visionneuse resterait affichée en permanence. */
  .visionneuse[open] {
    display: grid;
    place-items: center;
  }

  .visionneuse {
    position: fixed;
    inset: 0;
    width: 100%;
    height: 100%;
    margin: 0;
    padding: var(--s5);
    background: transparent;
    color: var(--sur-photo);
  }

  .visionneuse::backdrop {
    background: rgb(10 8 6 / 94%);
  }

  .visionneuse__figure {
    display: grid;
    gap: var(--s4);
    justify-items: center;
    max-width: min(100%, 1100px);
    max-height: 100%;
  }

  .visionneuse__image {
    max-height: 78svh;
    width: auto;
    object-fit: contain;
    border-radius: var(--rayon-media);
  }

  .visionneuse__pied {
    display: grid;
    gap: var(--s1);
    text-align: center;
  }

  .visionneuse__legende {
    font-size: 0.9375rem;
  }

  .visionneuse__compteur {
    font-size: 0.8125rem;
    color: var(--sur-photo-doux);
  }

  .visionneuse__bouton {
    position: absolute;
    width: 48px;
    height: 48px;
    display: grid;
    place-items: center;
    border: 1px solid rgb(255 255 255 / 25%);
    border-radius: 50%;
    background: rgb(255 255 255 / 6%);
    color: #fff;
    font-size: 1.25rem;
    transition: background-color var(--court);
  }

  .visionneuse__bouton:hover {
    background: rgb(255 255 255 / 16%);
  }

  .visionneuse__bouton--fermer {
    top: var(--s4);
    right: var(--s4);
  }

  .visionneuse__bouton--precedent {
    left: var(--s4);
    top: 50%;
    transform: translateY(-50%);
  }

  .visionneuse__bouton--suivant {
    right: var(--s4);
    top: 50%;
    transform: translateY(-50%);
  }

  /* ======================================================================
     À propos
     ====================================================================== */

  .about {
    display: grid;
    gap: var(--s7);
    padding-block: var(--s7);
  }

  @container gabarit (width >= 46rem) {
    .about {
      grid-template-columns: minmax(0, 1.1fr) minmax(0, 1fr);
      gap: var(--s8);
      align-items: start;
    }
  }

  /* Portrait en 3/2 comme toutes les autres images : le 4/5 du système
     précédent disparaît. */
  .about__portrait {
    aspect-ratio: 3 / 2;
    border-radius: var(--rayon-media);
    overflow: hidden;
    background: var(--fond-alt);
  }

  .about__portrait img {
    width: 100%;
    height: auto;
    object-fit: contain;
  }

  /* Parcours en menu : chaque étape est une ligne de carte — titre, année en
     italique — qui se déplie d'un clic sur son texte, comme une ligne d'accord.
     Sans JS ni ouverture, les titres et les années restent lisibles. Les
     étapes sans texte ne sont pas des boutons. */
  /* Cadre des cartes de menu : double filet, titre en capitales espacées, feuille. */
  #apropos-parcours {
    --menu-filet: color-mix(in oklab, var(--accent) 55%, transparent);
    position: relative;
    width: min(100%, 44rem);
    margin: var(--s7) auto 0;
    padding: clamp(1.5rem, 3vw, 3rem) clamp(1rem, 3vw, 3rem);
    border: 1.2px solid var(--menu-filet);
    background: var(--surface);
    text-align: center;
  }

  #apropos-parcours::before {
    content: '';
    position: absolute;
    inset: 6px;
    border: 0.5px solid var(--menu-filet);
    pointer-events: none;
  }

  #apropos-parcours {
    background-image: radial-gradient(ellipse at 50% 0%, color-mix(in oklab, var(--surface) 70%, white) 0%, transparent 70%);
  }

  .menu-parcours__pied {
    font-size: var(--pas--1);
    letter-spacing: 0.3em;
    text-transform: uppercase;
    color: var(--encre-douce);
  }

  /* Filet à losange, sous le titre et avant la mention finale. */
  .menu-filet {
    display: flex;
    align-items: center;
    gap: var(--s4);
    justify-content: center;
    width: 70%;
    margin: 0 auto;
    color: var(--menu-filet);
  }

  .menu-filet::before,
  .menu-filet::after {
    content: '';
    flex: 1;
    height: 1px;
    background: currentColor;
  }

  .menu-filet i {
    width: 7px;
    height: 7px;
    rotate: 45deg;
    background: var(--accent);
  }

  .menu-parcours__pied {
    margin: var(--s5) 0 0;
  }

  .menu-parcours {
    width: min(100%, 40rem);
    margin: var(--s3) auto 0;
    text-align: center;
  }

  .menu-etape + .menu-etape {
    border-top: 1px solid var(--filet);
  }

  .menu-etape__tete {
    display: grid;
    gap: var(--s1);
    justify-items: center;
    width: 100%;
    padding: var(--s5) var(--s2) var(--s4);
    border: 0;
    background: none;
    color: inherit;
    font: inherit;
  }

  button.menu-etape__tete {
    cursor: pointer;
  }

  .menu-etape__titre {
    font-family: var(--serif);
    font-size: var(--pas-2);
    line-height: 1.1;
    text-wrap: balance;
    transition: letter-spacing 0.5s cubic-bezier(0.2, 0.8, 0.2, 1), color 0.3s;
  }

  .menu-etape__annee {
    font-family: var(--serif);
    font-style: italic;
    font-size: var(--pas-0);
    color: var(--encre-douce);
  }

  @media (hover: hover) {
    button.menu-etape__tete:hover .menu-etape__titre {
      letter-spacing: 0.02em;
      color: var(--accent);
    }
  }

  button.menu-etape__tete:focus-visible {
    outline: 2px solid var(--accent);
    outline-offset: -2px;
  }

  .menu-etape__corps {
    display: grid;
    grid-template-rows: 0fr;
    transition: grid-template-rows 0.6s cubic-bezier(0.2, 0.8, 0.2, 1);
  }

  .menu-etape[data-ouvert='true'] .menu-etape__corps {
    grid-template-rows: 1fr;
  }

  .menu-etape__corps > div {
    overflow: hidden;
    opacity: 0;
    padding-inline: var(--s2);
    transition: opacity 0.4s;
  }

  .menu-etape[data-ouvert='true'] .menu-etape__corps > div {
    opacity: 1;
    padding-bottom: var(--s5);
    transition-delay: 0.25s;
  }

  .menu-etape__texte {
    max-width: 46ch;
    margin: var(--s2) auto 0;
    line-height: 1.6;
    text-wrap: pretty;
  }

  @media (prefers-reduced-motion: reduce) {
    .menu-etape__titre,
    .menu-etape__corps,
    .menu-etape__corps > div {
      transition-duration: 0.01s;
      transition-delay: 0s;
    }
  }

  /* ======================================================================
     Prestations — blocs alternés

     `.bloc` réemploie la composition texte / image des chapitres
     (`.bloc__texte`, `.bloc__media`, définis plus haut) : une prestation
     s'affiche comme un chapitre, sans en être un. L'alternance échange les
     colonnes plutôt que d'utiliser `order`, qui déplacerait l'élément sans
     redimensionner sa colonne.
     ====================================================================== */

  .blocs {
    display: grid;
  }

  .bloc {
    display: grid;
    gap: var(--s5);
    padding-block: var(--s7);
    border-bottom: 1px solid var(--filet);
    align-items: center;
  }

  .bloc:last-child {
    border-bottom: none;
  }

  @container gabarit (width >= 46rem) {
    /* Colonnes inégales : 11.9 demande des images plus grandes qu'avant.
       À parts égales elles ne gagnaient rien. La colonne large suit
       l'image, donc l'alternance inverse aussi le rapport. */
    .bloc {
      grid-template-columns: minmax(0, 1.35fr) minmax(0, 1fr);
      gap: clamp(var(--s7), 5vw, 6rem);
    }

    .bloc:nth-child(even) {
      grid-template-columns: minmax(0, 1fr) minmax(0, 1.35fr);
    }

    .bloc:nth-child(even) .bloc__media {
      grid-column: 2;
      grid-row: 1;
    }

    .bloc:nth-child(even) .bloc__texte {
      grid-column: 1;
      grid-row: 1;
    }
  }

  .bloc__titre {
    font-size: var(--pas-3);
  }

  .bloc__prix[hidden],
  .menu-carte__prix[hidden] {
    display: none;
  }

  .bloc__prix {
    display: inline-block;
    font-family: var(--serif);
    font-size: var(--pas-1);
    color: var(--accent);
  }

  /* ======================================================================
     Menus
     ====================================================================== */

  /* Les cartes se rangent en rangées centrées : 1, 2, 3 menus ou plus, la
     dernière rangée incomplète reste au milieu de l'écran. Elles occupent la
     hauteur restante sous l'en-tête : tout se voit dès l'arrivée, sans
     défiler, et cadre comme texte suivent la taille de la fenêtre (unités vh). */
  .menus {
    display: flex;
    flex-wrap: wrap;
    justify-content: center;
    gap: clamp(0.75rem, 2.5vw, 2rem);
    padding-block: 0;
    min-height: calc(100svh - var(--hauteur-entete) - 11rem);
  }

  /* Carte de menu « grande maison » : double filet, tout centré, titre en
     capitales très espacées, plats en italique séparés par une feuille.
     Le filet intérieur est un pseudo-élément ; la feuille est un clip-path
     (aucune image, donc rien à autoriser côté CSP). */
  .menu-carte {
    --menu-filet: color-mix(in oklab, var(--accent) 55%, transparent);
    position: relative;
    display: flex;
    flex: 1 1 min(100%, 30rem);
    max-width: 54rem;
    flex-direction: column;
    align-items: center;
    gap: clamp(0.5rem, 2vh, 1.75rem);
    padding: clamp(1.25rem, 2.8vh, 3.5rem) clamp(1.25rem, 2.5vw, 3.5rem);
    border: 1.2px solid var(--menu-filet);
    background: var(--surface);
    text-align: center;
  }

  .menu-carte::before {
    content: '';
    position: absolute;
    inset: 6px;
    border: 0.5px solid var(--menu-filet);
    pointer-events: none;
  }

  .menu-carte__media {
    width: 100%;
    overflow: hidden;
    background: var(--fond-alt);
  }

  .menu-carte__media img {
    display: block;
    width: 100%;
    height: auto;
    object-fit: contain;
  }

  /* Titre d'abord, sous-titre en italique juste dessous, puis la barre
     (filets + feuille) un peu plus large que celles entre les plats. */
  .menu-carte__texte {
    position: relative;
    display: grid;
    justify-items: center;
    gap: clamp(0.25rem, 1vh, 0.75rem);
  }

  .menu-carte__texte .surtitre {
    order: 2;
    font-family: var(--serif);
    font-size: clamp(0.9375rem, 2vh, 1.4rem);
    font-style: italic;
    text-transform: none;
    letter-spacing: 0;
    color: var(--encre-douce);
  }

  .menu-carte__titre {
    order: 1;
    padding-left: 0.5em; /* compense l'espacement après la dernière lettre */
    font-family: var(--serif);
    font-size: clamp(1.2rem, 2.8vh, 2.1rem);
    font-weight: 500;
    letter-spacing: 0.5em;
    text-transform: uppercase;
    color: var(--encre);
  }

  .menu-carte__texte::after,
  .menu-carte__plat + .menu-carte__plat::after {
    content: '';
    display: block;
    width: 0.5625rem;
    height: 0.8125rem;
    background: var(--accent);
    clip-path: path('M4.5 0C7.5 3 7.5 7.5 4.5 13C1.5 7.5 1.5 3 4.5 0Z');
  }

  .menu-carte__texte::after {
    order: 3;
    margin-top: var(--s4);
  }

  .menu-carte__texte::before {
    content: '';
    position: absolute;
    bottom: 6px;
    left: 50%;
    width: 10rem;
    height: 1px;
    translate: -50% 0;
    background: linear-gradient(
      to right,
      var(--menu-filet) 0 3.5rem,
      transparent 3.5rem calc(100% - 3.5rem),
      var(--menu-filet) calc(100% - 3.5rem)
    );
  }

  /* Liste des plats : chaque plat occupe une cellule de marge égale au-dessus
     et au-dessous ; le séparateur (feuille + filets) est posé sur la frontière
     entre deux cellules, donc le texte est centré entre deux barres. */
  .menu-carte__plats {
    flex: 1;
    display: flex;
    flex-direction: column;
    align-items: center;
    width: 100%;
    gap: 0;
  }

  .menu-carte__plat {
    position: relative;
    flex: 1 1 auto;
    display: flex;
    flex-direction: column;
    align-items: center;
    justify-content: center;
    gap: var(--s1);
    width: 100%;
    max-width: 66ch;
    padding-block: clamp(0.6rem, 2.1vh, 1.8rem);
  }

  .menu-carte__plat + .menu-carte__plat::before {
    content: '';
    position: absolute;
    top: 0;
    left: 50%;
    width: 7rem;
    height: 1px;
    translate: -50% -50%;
    background: linear-gradient(
      to right,
      var(--menu-filet) 0 2.25rem,
      transparent 2.25rem calc(100% - 2.25rem),
      var(--menu-filet) calc(100% - 2.25rem)
    );
  }

  .menu-carte__plat + .menu-carte__plat::after {
    position: absolute;
    top: 0;
    left: 50%;
    translate: -50% -50%;
    width: 0.375rem;
    height: 0.5625rem;
    clip-path: path('M3 0C5 2 5 5 3 9C1 5 1 2 3 0Z');
  }

  .menu-carte__categorie {
    font-size: clamp(0.8125rem, 1.7vh, 1.25rem);
    font-style: italic;
    color: var(--accent);
  }

  /* `pre-line` : les retours à la ligne saisis dans l'admin (nom du plat,
     garniture, technique) restent significatifs. */
  .menu-carte__description {
    font-family: var(--serif);
    font-size: clamp(0.95rem, 1.95vh, 1.5rem);
    font-style: italic;
    line-height: 1.3;
    color: var(--encre);
    white-space: pre-line;
  }

  /* Pied : tarif, mention et bouton, sous un filet qui s'estompe. */
  .menu-carte__pied {
    margin-top: auto; /* le pied reste en bas de cadre, quelle que soit la longueur */
    display: grid;
    justify-items: center;
    gap: var(--s3);
    width: 100%;
    padding-top: var(--s5);
    border-top: 1px solid var(--menu-filet);
    border-image: linear-gradient(to right, transparent, var(--menu-filet) 25%, var(--menu-filet) 75%, transparent) 1;
  }

  .menu-carte__prix {
    font-family: var(--serif);
    font-size: 1rem;
    color: var(--accent);
  }

  .menu-carte__note {
    max-width: 44ch;
    font-family: var(--serif);
    font-size: clamp(0.875rem, 1.7vh, 1.25rem);
    font-style: italic;
    color: var(--encre-douce);
  }

  /* Bouton discret : un lien à filet, pas un aplat, pour ne pas écraser la carte. */
  .menu-carte__pied .actions {
    margin-top: var(--s2);
  }

  .menu-carte__pied .bouton {
    min-height: 0;
    padding: 0.6em 1.5em;
    border: 1px solid var(--menu-filet);
    background: transparent;
    font-size: clamp(0.6875rem, 1.5vh, 0.95rem);
    font-weight: 400;
    letter-spacing: 0.22em;
    text-transform: uppercase;
    color: var(--accent);
  }

  .menu-carte__pied .bouton:hover,
  .menu-carte__pied .bouton:focus-visible {
    background: var(--accent);
    color: var(--accent-encre);
  }

  /* Ouverture compacte, commune à toutes les pages internes : le titre à gauche,
     la phrase d'accroche centrée sur l'écran entre deux petites feuilles, un
     filet dessous. Le contenu est visible dès l'arrivée, sans défiler. Les
     pages dont l'ouverture est prise dans une composition à deux colonnes
     (À propos, Contact) gardent leur disposition. */
  .page > .entete-page {
    grid-template-columns: 1fr minmax(0, 62rem) 1fr;
    align-items: end;
    column-gap: var(--s7);
    row-gap: var(--s2);
    padding-bottom: var(--s2);
    border-bottom: 1px solid var(--filet);
  }

  .page > .entete-page > :is(.surtitre, .erreur__code) {
    grid-column: 1 / -1;
  }

  .page > .entete-page .entete-page__titre {
    grid-column: 1;
    font-size: var(--pas-4);
  }

  .page > .entete-page .entete-page__filet {
    display: none;
  }

  .page > .entete-page .phrase {
    grid-column: 2;
    grid-row: 2;
    display: flex;
    align-items: center;
    justify-content: center;
    gap: var(--s4);
    align-self: center;
    justify-self: center;
    width: fit-content;
    max-width: 100%;
    font-family: var(--serif);
    font-size: clamp(1.0625rem, 2.3vh, 1.4rem);
    font-style: italic;
    line-height: 1.3;
    text-align: center;
    color: var(--encre);
  }

  .page > .entete-page .phrase:empty {
    display: none;
  }

  .page > .entete-page .phrase::before,
  .page > .entete-page .phrase::after {
    content: '';
    flex: none;
    width: 0.5625rem;
    height: 0.8125rem;
    background: var(--accent);
    clip-path: path('M4.5 0C7.5 3 7.5 7.5 4.5 13C1.5 7.5 1.5 3 4.5 0Z');
  }

  /* Sous 80 rem, la phrase passe sous le titre ; sous 48 rem, sans feuilles. */
  @media (max-width: 80rem) {
    .page > .entete-page {
      grid-template-columns: 1fr;
    }

    .page > .entete-page .phrase {
      grid-column: 1;
      grid-row: auto;
      margin-top: var(--s2);
    }
  }

  @media (max-width: 48rem) {
    .page > .entete-page .phrase {
      justify-self: start;
      text-align: left;
    }

    .page > .entete-page .phrase::before,
    .page > .entete-page .phrase::after {
      display: none;
    }
  }

  /* Page Menus : la grille de cartes colle au bas de l'écran. */
  .page--menus {
    padding-block-end: var(--s5);
  }

  /* Appel à l'action de fin de page : le seul aplat de couleur des pages
     internes, le bandeau plein cadre restant réservé à l'accueil. */
  .appel {
    display: grid;
    justify-items: start;
    gap: var(--s4);
    padding: var(--s7);
    background: var(--fond-alt);
    border-radius: var(--rayon-media);
  }

  /* Un `display` d'auteur bat le `[hidden]` du navigateur : sans cette règle,
     l'appel apparaîtrait vide avant que le script ne le remplisse. */
  .appel[hidden] {
    display: none;
  }

  .appel h2 {
    font-size: var(--pas-3);
  }

  /* ======================================================================
     Contact
     ====================================================================== */

  .contact {
    display: grid;
    gap: var(--s7);
    padding-block: var(--s7);
  }

  /* Dès 52 rem : titre et formulaire à gauche, carte-photo à droite. La carte
     part de la hauteur du titre et reste visible (sticky) pendant qu'on remplit
     le formulaire ; elle prend la taille de sa photo, plafonnée à ce qui tient
     dans l'écran : la photo est vue en entier dès l'arrivée. */
  @container gabarit (width >= 52rem) {
    .contact-ensemble {
      display: grid;
      grid-template-columns: minmax(0, 34rem) minmax(0, 1fr);
      column-gap: var(--s8);
      align-items: start;
    }

    .contact-ensemble > .contact__aside {
      position: sticky;
      top: calc(var(--hauteur-entete) + var(--s4));
      margin-top: 0;
      width: fit-content;
      max-width: 100%;
      justify-self: center;
    }

    .contact-ensemble .contact__portrait img {
      width: auto !important;
      max-width: 100%;
      height: auto !important;
      max-height: calc(100svh - var(--hauteur-entete) - var(--s8) - var(--s4)) !important;
      aspect-ratio: auto !important;
      margin: 0 !important;
    }
  }

  @container gabarit (width >= 52rem) {
    .contact {
      /* `minmax(0, 34rem)` — la mesure du formulaire lui-meme (voir .formulaire
         plus haut), pas `1fr` : sur une page pleine largeur, une colonne `1fr`
         etire la piste bien au-dela du formulaire (borne a 34rem), ce qui
         plaque les coordonnees tout au bord droit avec un grand vide entre les
         deux. Les deux colonnes se compactent l'une contre l'autre. */
      grid-template-columns: minmax(0, 34rem);
      align-items: start;
    }
  }

  /* Colonne de droite : une carte-photo. La photo y est ENTIÈRE (jamais
     recadrée : la carte prend sa hauteur) et les coordonnées sont posées par-
     dessus, en bas, sur un dégradé sombre qui les rend lisibles quelle que
     soit la photo. Les deux enfants partagent la même cellule de grille.
     Sous 52rem, `.contact` est en une seule piste : l'aside vient après le
     formulaire, jamais avant. Sans photo, il ne reste que les coordonnées,
     sur le fond de page. */
  .contact__aside {
    position: relative;
    isolation: isolate;
    display: grid;
    align-content: start;
    max-width: 40rem;
  }

  .contact__aside:has(.contact__portrait) {
    overflow: hidden;
    border-radius: var(--rayon-media);
    background: #0d0a07;
    color: var(--sur-photo);
  }

  .contact__portrait {
    grid-area: 1 / 1;
    margin: 0;
  }

  .contact__portrait img {
    display: block;
    width: 100%;
    height: auto;
    object-fit: contain;
  }

  .contact__details {
    grid-area: 1 / 1;
    display: grid;
    gap: var(--s3);
  }

  /* Sur la photo : en bas, sur un dégradé qui part de la transparence pour que
     le haut (le visage) reste dégagé. */
  .contact__aside:has(.contact__portrait) .contact__details {
    align-self: end;
    padding: 6rem var(--s5) var(--s5);
    background: linear-gradient(
      to top,
      rgb(10 7 4 / 94%) 0%,
      rgb(10 7 4 / 86%) 58%,
      rgb(10 7 4 / 0%) 100%
    );
  }

  .contact__aside:has(.contact__portrait) .contact__coord a {
    color: var(--sur-photo);
  }

  .contact__aside:has(.contact__portrait) .contact__coord a:hover {
    color: #fff;
  }

  .contact__aside:has(.contact__portrait) .bouton--fantome {
    color: var(--sur-photo);
    border-color: rgb(247 242 234 / 55%);
    background: rgb(247 242 234 / 8%);
  }

  /* `.contact__coord a` vaut `--pas-3` par defaut (voir plus haut, pensé pour
     le grand chapitre de cloture `.chapitre--contact` de l'accueil) : dans
     cette colonne de 18rem à côté d'un formulaire compact, ce même corps de
     texte débordait et paraissait disproportionné. Resserré ici seulement —
     le chapitre de l'accueil garde sa taille. */
  .contact__details .contact__coord a {
    font-size: var(--pas-1);
  }

  /* Accès Instagram : pictogramme + @pseudo, qui ouvre une fenêtre avec le code
     QR en grand (voir « Fenêtre Instagram » ci-dessous). */
  .insta {
    display: inline-flex;
    align-items: center;
    gap: var(--s3);
    width: fit-content;
    min-height: 48px;
    padding: 0 var(--s4) 0 var(--s3);
    border: 1px solid currentcolor;
    border-radius: 999px;
    background: transparent;
    color: inherit;
    font: inherit;
    font-weight: 600;
    cursor: pointer;
    transition: background-color var(--moyen), color var(--moyen), border-color var(--moyen);
  }

  .insta:hover {
    background: var(--accent);
    border-color: var(--accent);
    color: var(--accent-encre);
  }

  .insta__logo {
    width: 24px;
    height: 24px;
    flex: none;
  }

  /* Sur la carte-photo : clair sur le dégradé sombre. */
  .contact__aside:has(.contact__portrait) .insta {
    color: var(--sur-photo);
    border-color: rgb(247 242 234 / 60%);
    background: rgb(247 242 234 / 10%);
  }

  .contact__aside:has(.contact__portrait) .insta:hover {
    background: var(--accent);
    border-color: var(--accent);
    color: var(--accent-encre);
  }

  /* --- Fenêtre Instagram ------------------------------------------------
     <dialog> natif : Échap, piège de focus, fond assombri et retour du focus
     sont gérés par le navigateur. Le code QR est grand (jusqu'à 26 rem) sur un
     fond blanc : c'est ce qui le rend lisible de loin et sous un mauvais angle. */
  .insta-popup {
    position: fixed;
    inset: 0;
    margin: auto;
    width: min(92vw, 36rem);
    max-height: 96svh;
    overflow: auto;
    padding: var(--s6) var(--s6) var(--s5);
    border: 0;
    border-radius: 22px;
    background: var(--fond);
    color: var(--encre);
    box-shadow: 0 2rem 5rem rgb(10 7 4 / 45%);
    text-align: center;
  }

  .insta-popup[open] {
    display: grid;
    gap: var(--s4);
    justify-items: center;
  }

  .insta-popup::backdrop {
    background: rgb(10 7 4 / 62%);
  }

  .insta-popup__fermer {
    position: absolute;
    top: var(--s3);
    right: var(--s3);
    width: 44px;
    height: 44px;
    border: 0;
    border-radius: 50%;
    background: var(--fond-alt);
    color: inherit;
    font: inherit;
    font-size: 1.1rem;
    cursor: pointer;
  }

  .insta-popup__tete {
    display: flex;
    align-items: center;
    gap: var(--s3);
    text-align: left;
    color: var(--accent);
  }

  .insta-popup__tete .insta__logo {
    width: 40px;
    height: 40px;
  }

  .insta-popup__tete div {
    color: var(--encre);
  }

  .insta-popup__surtitre {
    margin: 0;
    font-size: 0.75rem;
    text-transform: uppercase;
    letter-spacing: 0.18em;
    color: var(--encre-douce);
  }

  .insta-popup h2 {
    margin: 0;
    font-family: var(--serif);
    font-size: clamp(1.4rem, 1.2rem + 1vw, 1.9rem);
    line-height: 1.1;
  }

  .insta-popup__qr {
    /* Le plus grand possible, sans dépasser la hauteur de l'écran. */
    width: min(100%, 30rem, 56svh);
    padding: var(--s3);
    border-radius: 18px;
    background: #fff;
    box-shadow: 0 0.5rem 1.75rem rgb(10 7 4 / 16%);
    line-height: 0;
  }

  .insta-popup__qr svg {
    display: block;
    width: 100%;
    height: auto;
  }

  .insta-popup__aide {
    margin: 0;
    color: var(--encre-douce);
    font-size: 0.9375rem;
  }

  .insta-popup__ouvrir {
    width: 100%;
    max-width: 26rem;
    justify-content: center;
    min-height: 52px;
    font-size: 1.0625rem;
  }

  .contact__item .overline,
  .contact__item .surtitre {
    display: inline-block;
    font-family: var(--serif);
    font-size: 0.875rem;
    text-transform: none;
    letter-spacing: 0.01em;
    padding-bottom: var(--s1);
    border-bottom: 1px solid var(--accent);
    color: var(--encre-douce);
  }

  .contact__item a {
    color: var(--encre);
    text-decoration: none;
  }

  .contact__item a:hover {
    color: var(--accent);
    text-decoration: underline;
  }

  /* ======================================================================
     Mentions légales — rubriques « intitulé / valeur » et texte au fil

     Tout le reste vient de classes existantes : `.page` pour la mise en page,
     `.entete-page` pour l'ouverture, `.prose` pour la mesure de lecture et le
     rythme vertical, `.surtitre` pour les intitulés. Ne restent ici que la
     liste de définitions, qu'aucun composant ne décrivait, et la conservation
     des retours à la ligne.
     ====================================================================== */

  .mentions {
    display: grid;
    gap: var(--s5);
  }

  .mentions__ligne {
    display: grid;
    gap: var(--s1);
  }

  /* `pre-line` : une adresse ou une clause de médiation se saisit sur
     plusieurs lignes dans l'administration, et se lit de même ici — même
     convention que `.menu-carte__description`. */
  .mentions__ligne dd,
  .mentions__texte {
    white-space: pre-line;
  }

  /* ======================================================================
     Accès administration — <dialog> natif

     Le protocole ne change pas : aucune adresse d'administration n'apparaît
     dans le HTML public, le serveur ne la renvoie qu'après authentification.
     Le passage en <dialog> ne fait que confier Échap, le piège de focus et le
     fond assombri au navigateur.
     ====================================================================== */

  .acces {
    width: min(100% - 2rem, 24rem);
    margin: auto;
    background: var(--surface);
    border: 1px solid var(--filet);
    border-radius: var(--rayon-media);
    box-shadow: 0 20px 50px var(--ombre);
  }

  .acces__panneau {
    display: grid;
    gap: var(--s4);
    padding: var(--s6);
  }

  .acces__titre {
    font-family: var(--serif);
    font-size: var(--pas-1);
  }

  .acces__erreur {
    font-size: 0.875rem;
    color: var(--accent);
  }

  .acces__erreur:empty {
    display: none;
  }

  .acces__actions {
    justify-content: flex-end;
  }

  /* ======================================================================
     Page 404 — même ouverture éditoriale que les autres pages internes,
     le code d'erreur tenant lieu de sur-titre.
     ====================================================================== */

  .erreur {
    min-height: 70svh;
    align-content: center;
  }

  .erreur__code {
    font-family: var(--serif);
    font-weight: 700;
    font-size: clamp(3rem, 2rem + 5vw, 5rem);
    line-height: 1;
    letter-spacing: -0.02em;
    color: var(--accent);
  }

  /* ======================================================================
     Articles — liste, cartes et page de lecture

     Le contenu est écrit par le serveur (src/posts.js) : ces règles ne
     décrivent que sa mise en forme. Les photos ne sont jamais recadrées
     (`object-fit: contain`), seulement plafonnées en hauteur. Le texte de
     lecture tient à 65 caractères de large.
     ====================================================================== */

  .articles-liste {
    display: grid;
    gap: var(--s6) var(--s5);
    grid-template-columns: repeat(auto-fill, minmax(min(100%, 20rem), 1fr));
    align-items: start;
  }

  /* Le titre est le seul lien de la carte : son ::after l'étend à toute la
     carte, sans doubler le lien pour un lecteur d'écran. */
  .article-carte {
    position: relative;
    display: grid;
    gap: var(--s3);
    align-content: start;
    padding-top: var(--s4);
    border-top: 1px solid var(--filet);
  }

  .article-carte__media {
    overflow: hidden;
    border-radius: var(--rayon-media);
    background: var(--fond-alt);
  }

  .article-carte__media img {
    display: block;
    width: 100%;
    height: auto;
    max-height: 22rem;
    object-fit: contain;
  }

  .article-carte__texte {
    display: grid;
    gap: var(--s2);
  }

  .article-carte__meta,
  .article__meta {
    display: flex;
    flex-wrap: wrap;
    align-items: center;
    gap: var(--s3);
    font-size: 0.75rem;
    color: var(--encre-douce);
  }

  .article-carte__titre {
    font-size: var(--pas-2);
    line-height: 1.1;
  }

  .article-carte__titre a {
    color: var(--encre);
    text-decoration: none;
  }

  .article-carte__titre a::after {
    content: '';
    position: absolute;
    inset: 0;
  }

  .article-carte:hover .article-carte__titre a {
    color: var(--accent);
  }

  .article-carte__resume {
    max-width: 60ch;
    color: var(--encre-douce);
  }

  /* --- Fil d'Ariane ---------------------------------------------------- */

  .fil-ariane ol {
    display: flex;
    flex-wrap: wrap;
    align-items: center;
    gap: var(--s1) var(--s2);
    font-size: 0.875rem;
    color: var(--encre-douce);
  }

  .fil-ariane li + li::before {
    content: '›';
    margin-right: var(--s2);
  }

  .fil-ariane a {
    display: inline-flex;
    align-items: center;
    min-height: 44px;
    color: inherit;
    text-decoration: none;
  }

  .fil-ariane a:hover {
    color: var(--accent);
  }

  .fil-ariane [aria-current='page'] {
    color: var(--encre);
  }

  /* --- Page de lecture ------------------------------------------------- */

  .article {
    --lecture: 65ch;
    display: grid;
    gap: var(--s6);
  }

  .article__entete {
    display: grid;
    gap: var(--s3);
    justify-items: start;
    max-width: 48rem;
  }

  .article__titre {
    font-size: var(--pas-5);
  }

  .article__resume {
    max-width: var(--lecture);
  }

  .article__media {
    max-width: 56rem;
    overflow: hidden;
    border-radius: var(--rayon-media);
    background: var(--fond-alt);
  }

  .article__media img {
    display: block;
    width: 100%;
    height: auto;
    max-height: 70svh;
    object-fit: contain;
  }

  .article__corps {
    max-width: var(--lecture);
    line-height: var(--interligne-ample);
  }

  .article__corps > * + * {
    margin-top: var(--s4);
  }

  .article__corps > h2 {
    margin-top: var(--s7);
    font-size: var(--pas-3);
  }

  .article__corps > h3 {
    margin-top: var(--s6);
    font-size: var(--pas-2);
  }

  .article__corps > :is(h2, h3) + * {
    margin-top: var(--s3);
  }

  .article__corps ul {
    padding-left: var(--s5);
    list-style: disc;
  }

  .article__corps li + li {
    margin-top: var(--s2);
  }

  .article__corps li::marker {
    color: var(--accent);
  }

  .article__suite {
    display: grid;
    gap: var(--s5);
    padding-top: var(--s6);
    border-top: 1px solid var(--filet);
  }

  .article__suite h2 {
    font-size: var(--pas-3);
  }

}

/* ==========================================================================
   @layer mouvement — facultatif, jamais nécessaire à la lecture

   Cette couche passe après `composants` et `pages` : elle l'emporte par
   l'ordre de cascade, sans un seul `!important`. Tout est enfermé dans
   `prefers-reduced-motion: no-preference` et, quand la fonctionnalité n'est
   pas universelle, dans un `@supports` avec un rendu statique correct.
   ========================================================================== */

@layer mouvement {
  /* Fondu croisé entre deux pages, sans SPA : c'est ce qui supprime le flash
     blanc. Déclaré au premier niveau — un navigateur qui ne connaît pas
     cette règle l'ignore, et la navigation reste classique. Le respect de
     `prefers-reduced-motion` est assuré plus bas, sur les pseudo-éléments. */
  @view-transition {
    navigation: auto;
  }

  ::view-transition-old(root),
  ::view-transition-new(root) {
    animation-duration: 300ms;
  }

  /* En-tête immobile pendant la transition. Un `view-transition-name` doit
     être unique dans le document : la règle ne vise QUE `.entete`, jamais
     l'en-tête hérité, sinon une page servant les deux romprait la
     transition entière. */
  .entete {
    view-transition-name: entete;
  }

  /* Pied de page immobile lui aussi. Même précaution d'unicité : seul le
     gabarit commun sert un `.pied--site`, l'accueil n'en contient pas. */
  .pied--site {
    view-transition-name: pied;
  }

  /* En-tête transparent sur le hero de l'accueil, puis opaque au défilement.
     Piloté par `scroll()` et `:has()` : aucun écouteur de défilement, aucune
     classe posée par JS — c'est précisément ce que la refonte supprime.
     Sans le support, l'en-tête reste opaque partout : dégradé propre. */
  @supports (animation-timeline: scroll()) {
    @media (prefers-reduced-motion: no-preference) {
      body:has(.chapitre--hero) .entete {
        color: var(--sur-photo);
        background-color: transparent;
        border-block-end-color: transparent;
        animation: opacifier linear both;
        animation-timeline: scroll(root block);
        animation-range: 0 60svh;
      }
    }
  }

  /* Même fondu pour les navigateurs sans `animation-timeline` (Safari avant
     la 26, Firefox) : `enteteDeRepli()` de motion.js pose `--defil` (0 → 1) et
     la classe `repli-entete` ; le CSS interpole les couleurs. Le sélecteur est
     plus spécifique que celui du repli opaque, et le bloc « téléphone » plus
     bas le surcharge pour finir en plein. */
  @supports not (animation-timeline: scroll()) {
    @media (prefers-reduced-motion: no-preference) {
      :root.repli-entete body:has(.chapitre--hero) .entete {
        color: color-mix(in srgb, var(--encre) calc(var(--defil, 0) * 100%), var(--sur-photo));
        background-color: color-mix(in srgb, var(--fond) calc(var(--defil, 0) * 88%), transparent);
        border-block-end-color: color-mix(in srgb, var(--filet) calc(var(--defil, 0) * 100%), transparent);
      }
    }
  }

  @keyframes opacifier-plein {
    to {
      color: var(--encre);
      background-color: var(--fond);
      border-block-end-color: var(--filet);
    }
  }

  @keyframes opacifier {
    to {
      color: var(--encre);
      background-color: color-mix(in srgb, var(--fond) 88%, transparent);
      border-block-end-color: var(--filet);
    }
  }

  @media (prefers-reduced-motion: no-preference) {
    /* --- Entrée du hero : l'amorce, puis les mots du titre, puis les
           actions. `--i` porte le rang du mot, posé sur chaque <span>. --- */
    .hero__amorce,
    .hero__titre span,
    .hero__bloc .actions {
      animation: monter 800ms var(--sortie) both;
    }

    .hero__titre span {
      animation-delay: calc(120ms + var(--i) * 85ms);
    }

    .hero__bloc .actions {
      animation-delay: 480ms;
    }

    .hero__repere {
      animation: respirer 2.6s ease-in-out 1.4s infinite;
    }

    /* --- Apparitions et parallaxe pilotées par le défilement, sans JS --- */
    @supports (animation-timeline: view()) {
      .chapitre__tete,
      .chapitre__scene,
      .chapitre__bloc {
        animation: monter linear both;
        animation-timeline: view();
        animation-range: entry 10% entry 85%;
      }

      /* Le hero est déjà à l'écran au chargement : il ne s'anime pas deux
         fois. */
      .chapitre--hero .chapitre__tete,
      .chapitre--hero .chapitre__scene,
      .chapitre--hero .chapitre__bloc {
        animation: none;
      }

      /* Parallaxe réservée au hero plein cadre : le hero hérité affiche son
         média en flux, il ne doit pas glisser. */
      .chapitre--hero .hero__media img,
      .chapitre--hero .hero__media video {
        animation: parallaxe linear both;
        animation-timeline: scroll(root block);
        animation-range: 0 100svh;
      }

      /* Hero vidéo en portrait : la vidéo s'affiche entière sous l'en-tête
         (voir « Hero vidéo sur écran en portrait », couche pages) — ni zoom
         ni glissement. */
      @media (orientation: portrait) {
        .chapitre--hero .hero__media video {
          animation: none;
          transform: none;
        }
      }

      .hero__bloc {
        animation: estomper linear both;
        animation-timeline: scroll(root block);
        animation-range: 0 72svh;
      }

      /* Barre de progression de lecture des étapes d'une recette. Elle
         n'existe QUE si le navigateur sait la piloter : sans
         `animation-timeline`, elle n'a ni hauteur ni fond et n'occupe rien —
         d'où sa mise en forme ici et non dans la couche `pages`. */
      .lecture {
        position: sticky;
        top: calc(var(--hauteur-entete) + var(--s3));
        z-index: 5;
        height: 2px;
        border-radius: 999px;
        background: var(--accent);
        transform-origin: left;
        animation: progresser linear both;
        animation-timeline: scroll(root block);
      }
    }

    /* Repli sans `animation-timeline` : `--reveal` est une propriété typée,
       donc interpolable — une seule valeur pilote l'opacité et le décalage.
       La classe sur <html> n'est posée que par motion.js : sans JS, rien
       n'est jamais masqué. */
    @supports not (animation-timeline: view()) {
      :root.repli-apparition .chapitre {
        --reveal: 0;
        transition: --reveal var(--long) var(--sortie);
      }

      :root.repli-apparition .chapitre.est-vu {
        --reveal: 1;
      }

      :root.repli-apparition .chapitre :is(.chapitre__tete, .chapitre__scene, .chapitre__bloc) {
        opacity: var(--reveal);
        transform: translateY(calc((1 - var(--reveal)) * 18px));
      }
    }
  }

  @keyframes monter {
    from {
      opacity: 0;
      transform: translateY(20px);
    }

    to {
      opacity: 1;
      transform: none;
    }
  }

  @keyframes parallaxe {
    from {
      transform: scale(1.08) translateY(0);
    }

    to {
      transform: scale(1.08) translateY(7%);
    }
  }

  @keyframes estomper {
    from {
      opacity: 1;
      transform: none;
    }

    to {
      opacity: 0;
      transform: translateY(-26px);
    }
  }

  @keyframes respirer {
    0%,
    100% {
      transform: translateY(0);
    }

    50% {
      transform: translateY(5px);
    }
  }

  @keyframes progresser {
    from {
      transform: scaleX(0);
    }

    to {
      transform: scaleX(1);
    }
  }
}

/* ==========================================================================
   @layer utilitaires — dernière couche déclarée, donc la plus prioritaire
   ========================================================================== */

@layer utilitaires {
  .visuellement-cache,
  .visually-hidden {
    position: absolute;
    width: 1px;
    height: 1px;
    padding: 0;
    margin: -1px;
    overflow: hidden;
    clip-path: inset(50%);
    white-space: nowrap;
  }

  .discret,
  .text-muted {
    color: var(--encre-douce);
  }

  .mt-4 {
    margin-top: var(--s4);
  }

  .mt-5 {
    margin-top: var(--s5);
  }

  .mt-6 {
    margin-top: var(--s6);
  }

  .no-scroll {
    overflow: hidden;
  }

  /* L'attribut `hidden` cache, point. Placé dans la dernière couche, il
     l'emporte sur n'importe quel `display` posé par un composant : c'est ce
     qui évite d'avoir à doubler l'attribut d'une classe d'état (`.form`,
     `.chips`, `.tag-list` sont en `display: grid/flex` et échappaient
     jusqu'ici à la règle du navigateur). */
  [hidden] {
    display: none;
  }

  /* ----------------------------------------------------------------------
     Téléphone et écrans tactiles — défilement allégé.

     Ce qui coûte à un téléphone à chaque image du défilement : le flou
     d'arrière-plan (surtout au-dessus d'une vidéo en lecture), les animations
     liées à la position de défilement (apparition de chaque chapitre, fondu du
     hero, couleur de l'en-tête) qui repeignent la page. On les remplace par des
     états fixes : le contenu est identique, seul l'effet disparaît.
     ---------------------------------------------------------------------- */
  @media (max-width: 48rem), (pointer: coarse) {
    *,
    *::before,
    *::after {
      backdrop-filter: none;
    }

    .chapitre__tete,
    .chapitre__scene,
    .chapitre__bloc,
    .hero__bloc,
    .chapitre--hero .hero__media img,
    .chapitre--hero .hero__media video {
      animation: none;
    }

    /* Sans flou d'arrière-plan, un en-tête translucide laisse voir ce qui passe
       dessous : on le rend plein. */
    .entete {
      background-color: var(--fond);
    }

    /* En-tête de l'accueil : sombre par défaut (repli sans animation liée au
       défilement), puis — comme sur ordinateur — transparent sur le hero et
       fondu vers le fond de page au défilement, logo compris. Sans flou
       d'arrière-plan sur téléphone, l'état final est PLEIN (--fond) plutôt que
       translucide : ce qui défile dessous ne transparaît pas. */
    body:has(.chapitre--hero) .entete {
      animation: none;
      color: var(--sur-photo);
      background-color: rgb(10 7 4 / 95%);
      border-block-end-color: transparent;
    }

    @supports (animation-timeline: scroll()) {
      @media (prefers-reduced-motion: no-preference) {
        body:has(.chapitre--hero) .entete {
          background-color: transparent;
          animation: opacifier-plein linear both;
          animation-timeline: scroll(root block);
          animation-range: 0 60svh;
        }
      }
    }

    /* Repli sans `animation-timeline` : même fondu, fini en plein. */
    @supports not (animation-timeline: scroll()) {
      @media (prefers-reduced-motion: no-preference) {
        :root.repli-entete body:has(.chapitre--hero) .entete {
          background-color: color-mix(in srgb, var(--fond) calc(var(--defil, 0) * 100%), transparent);
        }
      }
    }
  }

  /* Éléments propres au téléphone : cachés par défaut, montrés dans le bloc
     « Téléphone » ci-dessous. Sur grand écran la galerie de l'accueil garde ses
     trois vignettes (le serveur en envoie six). */
  .raccourcis,
  .offres__photo {
    display: none;
  }

  #galerie-serie .serie__carte:nth-child(n + 4) {
    display: none;
  }

  /* ----------------------------------------------------------------------
     Téléphone — affichage resserré : peu de texte, des photos.

     Sur un écran étroit, chaque bloc ne garde que son premier texte. L'accueil
     devient un sommaire (titres, photos, boutons) ; les pages internes gardent
     leur titre et leur premier paragraphe, sans les détails secondaires. Le
     contenu reste dans les pages (rien n'est supprimé des données) : seul
     l'affichage change, et un écran plus large le montre en entier.
     ---------------------------------------------------------------------- */
  @media (max-width: 48rem) {
    /* Moins de titres : l'accroche du hero redit l'en-tête, les légendes de la
       galerie redisent la photo, « Les coulisses » redit le lien « Galerie ». */
    .hero__amorce,
    .serie__titre,
    #galerie-titre {
      display: none;
    }

    /* Téléphone : l'image parle d'elle-même, le titre du hero disparaît de
       l'écran. Il reste dans le document (titre de page pour les lecteurs
       d'écran et les moteurs de recherche), seulement retiré de la vue. */
    /* Le menu du bas donne déjà les pages : le lien du hero ne sert plus. */
    .hero__bloc .actions {
      display: none;
    }

    .hero__titre {
      position: absolute;
      width: 1px;
      height: 1px;
      margin: -1px;
      padding: 0;
      overflow: hidden;
      clip-path: inset(50%);
      white-space: nowrap;
    }

    /* Tuiles-photos vers les pages, sous le hero. */
    .raccourcis {
      display: grid;
      grid-template-columns: repeat(2, minmax(0, 1fr));
      align-items: start;
      gap: var(--s3);
      /* Le fond clair remonte au-dessus des tuiles : il les encadre, et touche
         la vidéo sans bande entre les deux. */
      padding: var(--s5) var(--marge) 0;
    }

    .raccourcis__tuile {
      position: relative;
      display: block;
      overflow: hidden;
      border-radius: 14px;
      background: var(--fond-alt);
      color: var(--sur-photo);
      text-decoration: none;
    }

    .raccourcis__tuile img {
      display: block;
      width: 100%;
      height: auto;
      object-fit: contain;
    }

    .raccourcis__tuile::after {
      content: '';
      position: absolute;
      inset: 0;
      background: linear-gradient(to top, rgb(10 7 4 / 78%) 0%, rgb(10 7 4 / 0%) 62%);
    }

    .raccourcis__tuile span {
      position: absolute;
      z-index: 1;
      inset: auto var(--s3) var(--s3) var(--s3);
      font-family: var(--serif);
      font-size: var(--pas-1);
      font-weight: 600;
      line-height: 1.1;
    }

    .raccourcis__tuile span::after {
      content: ' →';
    }

    /* Les tuiles ci-dessus mènent déjà aux pages : les chapitres qui les
       redisent (À propos, Prestations, Galerie) sont retirés sur téléphone.
       Le chapitre Contact reste, avec le téléphone, l'e-mail et le bouton. */
    #chapitre-a-propos,
    #chapitre-prestations,
    #chapitre-galerie {
      display: none;
    }

    /* Galerie de l'accueil : six photos en grille de deux colonnes, sans
       légende, plutôt qu'un rail à balayer. */
    #galerie-serie {
      grid-auto-flow: row;
      grid-template-columns: repeat(2, minmax(0, 1fr));
      grid-auto-columns: auto;
      overflow: visible;
      align-items: start;
      gap: var(--s3);
      padding-block-end: 0;
    }

    #galerie-serie .serie__carte:nth-child(n + 4) {
      display: block;
    }

    /* Liste des prestations : une miniature devant chaque ligne. */
    .offres__lien {
      flex-wrap: nowrap;
      gap: var(--s3);
    }

    .offres__photo {
      display: block;
      flex: none;
      width: 3.5rem;
      height: auto;
      object-fit: contain;
      border-radius: 8px;
    }

    .offres__nom {
      flex: 1;
      min-width: 0;
    }

    /* Accueil : le hero garde son accroche et son titre, pas le paragraphe ;
       les chapitres gardent leur titre, pas leur résumé ni leur adresse. */
    .hero__seconde,
    .hero__amorce .lieu,
    .chapitre:not(.chapitre--hero) .phrase,
    .chapitre .lieu {
      display: none;
    }

    /* Hero vidéo : la hauteur de son contenu (vidéo, titre, boutons) plutôt
       que tout l'écran — sans son paragraphe il ne resterait qu'un grand vide
       sombre, et la suite de la page n'arriverait que plus bas. */
    .chapitre--hero:has(.hero__media video) {
      min-height: 0;
      padding-block-end: 0;
    }

    /* La vidéo entière s'arrête où le hero s'arrête : plus de bande sombre
       (le flou de l'image) ni d'ombre portée sous elle, que le titre masqué
       ne remplit plus. */
    .chapitre--hero:has(.hero__media video) {
      padding-block-start: calc(var(--hauteur-entete) + 100vw / var(--hero-ratio, 1.7778));
    }

    .chapitre--hero .hero__media:has(video) video {
      box-shadow: none;
    }

    .chapitre--hero:has(.hero__media video) .hero__repere {
      display: none;
    }

    /* Chapitres de l'accueil : la hauteur de leur contenu, pas un écran
       entier chacun — avec moins de texte, la hauteur imposée ne laisserait
       que du vide entre deux photos. */
    .chapitre:not(.chapitre--hero) {
      min-height: 0;
      padding-block: var(--s7);
    }

    /* Pages internes : points forts des prestations. Le titre, la date et la
       description principale restent. */
    .bloc .checklist {
      display: none;
    }
  }

  /* ----------------------------------------------------------------------
     Écrans hauts et larges (tablette ou moniteur en portrait, 1620 × 2160…).
     Les règles « téléphone » ne s'appliquent pas (plus de 48 rem de large) et
     le style bureau, pensé pour un écran en largeur, y paraît minuscule : tout
     est dimensionné en rem, donc on grandit la racine avec la largeur — textes,
     boutons et marges suivent ensemble. La vidéo, elle, reste entière (règle
     « Hero vidéo sur écran en portrait ») et le hero prend la hauteur de son
     contenu au lieu d'un écran de 2 000 px qui ne laisserait que du vide.
     ---------------------------------------------------------------------- */
  @media (orientation: portrait) and (min-width: 48rem) {
    html {
      font-size: clamp(16px, 0.9rem + 1.1vw, 26px);
    }

    .chapitre--hero:has(.hero__media video) {
      min-height: 0;
      padding-block-end: var(--s7);
    }

    .chapitre--hero:has(.hero__media video) .hero__repere {
      display: none;
    }

    .hero__seconde {
      max-width: 42ch;
    }

    /* L'en-tête a une hauteur en px : sa marque, son menu et son bouton ne
       doivent pas passer à la ligne quand la racine grandit. */
    .marque__role,
    .entete__action,
    .menu__lien {
      white-space: nowrap;
    }

    .marque__role {
      font-size: 0.72em;
      letter-spacing: 0.1em;
    }

    .entete__dedans {
      gap: var(--s3);
    }
  }

  /* ----------------------------------------------------------------------
     Mouvement réduit — neutralisation générale.

     Aucun `!important` : cette couche est la dernière déclarée, elle prime
     donc sur `mouvement`, `pages` et `composants` quelle que soit leur
     spécificité. C'est le filet de sécurité ; la vraie protection est en
     amont, chaque animation étant enfermée dans
     `@media (prefers-reduced-motion: no-preference)`.
     ---------------------------------------------------------------------- */
  @media (prefers-reduced-motion: reduce) {
    html {
      scroll-behavior: auto;
      scroll-snap-type: none;
    }

    *,
    *::before,
    *::after {
      animation-duration: 1ms;
      animation-delay: 0ms;
      animation-iteration-count: 1;
      transition-duration: 1ms;
      transition-delay: 0ms;
      scroll-behavior: auto;
    }

    /* Y compris les transitions de document : la page change d'un coup. */
    ::view-transition-group(*),
    ::view-transition-old(*),
    ::view-transition-new(*) {
      animation: none;
    }
  }

  @media print {
    .entete,
    .pied,
    .filtres,
    .visionneuse,
    .acces,
    .bascule,
    .hero__repere {
      display: none;
    }

    body {
      background: #fff;
      color: #000;
      display: block;
    }

    .chapitre {
      min-height: 0;
      padding-block: var(--s5);
    }
  }
}
