/* ============================================================================
   FICHIER GÉNÉRÉ — NE PAS MODIFIER À LA MAIN.
   ----------------------------------------------------------------------------
   Produit par « npm run pages », qui met bout à bout, dans cet ordre :
     assets/css/tokens.css
     assets/css/base.css
     assets/css/style.css
   Ces trois fichiers-là restent la vérité : on les édite, on relance
   npm run pages, et on versionne le résultat. Une feuille au lieu de trois,
   ce sont deux requêtes bloquantes en moins avant le premier affichage.
   ============================================================================ */

/* ————— assets/css/tokens.css ————— */
/* ============================================================================
   OPAQ — site de NS Production — tokens.css
   ----------------------------------------------------------------------------
   DEUX directions de design, UN seul HTML. Le HTML et le JS sont identiques
   d'une direction à l'autre : seules ces variables et style.css changent.

     html[data-theme="planche"]  → A « Planche contact » (docs/design-directions.md §4)
     html[data-theme="salle"]    → B « Salle sombre »    (docs/design-directions.md §3)

   Les valeurs des deux palettes et des deux jeux de polices sont RECOPIÉES du
   document de direction, pas réinventées. Les noms sont des rôles (bg, ink,
   signal…), jamais des teintes : c'est ce qui permet de permuter les deux
   habillages sans toucher une ligne de balisage.
   base.css n'est jamais modifié ; tout passe par ici et par style.css.

   Aperçu de l'autre direction sans toucher au HTML : index.html?theme=salle
   ============================================================================ */

/* ============================================================================
   LES POLICES, SERVIES PAR CE SITE
   ----------------------------------------------------------------------------
   Aucune requête vers Google : les fichiers sont dans assets/fonts/ et partent
   du même domaine que la page (docs/securite.md, §RGPD). Ce sont les polices
   de la direction MONTRÉE, « Salle sombre » ; celles du thème Planche —
   Newsreader et Schibsted Grotesk — ne sont pas servies : le jour où on montre
   ce thème-là, il faudra les télécharger dans assets/fonts/ et les déclarer
   ici, à la suite.

   TROIS FAMILLES, TROIS RÔLES, ET PAS UNE DE PLUS.
     Instrument Serif  les TITRES (--font-display). Un serif de labeur
                       contemporain, dessiné pour les grands corps : c'est lui
                       qui donne son ton aux trois thèmes de l'accueil et à
                       toutes les têtes de page. Une seule graisse, 400, et son
                       italique — la famille n'en a pas d'autre, et c'est très
                       bien ainsi : un titre de ce corps n'a pas besoin d'être
                       gras.
     Archivo           TOUT LE RESTE : le texte courant, les libellés en
                       capitales, les numéros. Elle est variable, 400 à 700.
     Poppins 300       UNE PHRASE, et une seule : le slogan de l'ouverture
                       (--font-slogan, dans les invariants plus bas). Ce n'est
                       pas un troisième alphabet de page, c'est la LETTRE DE LA
                       CARTE DE VISITE de Noé — voir le bloc @font-face qui la
                       déclare, et assets/fonts/README.md.

   JetBrains Mono a QUITTÉ le site. Elle portait les libellés et les numéros,
   et sa largeur fixe les étirait : à côté d'un titre serif, ce mono-là faisait
   un troisième alphabet dans la page. Les libellés reviennent donc à Archivo,
   en capitales espacées de 0.12em, à 12-13 px — c'est la lettre du texte
   courant, ramassée et espacée, et la page n'a plus que deux voix.

   Une famille = UN fichier, sauf l'italique du serif, qui est toujours un
   second dessin. Sous-ensemble LATIN seul — la plage exacte que servait
   Google. Le site est en français : le latin étendu, le grec, le cyrillique et
   le vietnamien ne servaient à personne. font-display: swap — le texte
   s'affiche tout de suite dans la police du système, et bascule à l'arrivée du
   fichier.

   Graisses relevées dans base.css et style.css, thème salle, et elles seules :
     Instrument Serif  400 — et rien d'autre : partout où le socle demandait du
                       600 ou du 700 sur un titre, style.css le ramène à 400,
                       faute de quoi le navigateur fabriquerait un faux gras.
     Archivo           400 texte courant et méta · 500 numéros et navigation ·
                       600 libellés capitales, bouton, libellé de champ ·
                       700 mot d'accès
     Poppins           300, et style.css l'excepte nommément de la règle qui
                       ramène les titres à 400.
   ============================================================================ */
@font-face {
  font-family: 'Archivo';
  font-style: normal;
  font-weight: 400 700;
  font-stretch: 100%;
  font-display: swap;
  src: url('../fonts/archivo-variable.woff2') format('woff2');
  unicode-range: U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6, U+02DA, U+02DC, U+0304, U+0308, U+0329, U+2000-206F, U+20AC, U+2122, U+2191, U+2193, U+2212, U+2215, U+FEFF, U+FFFD;
}
@font-face {
  font-family: 'Instrument Serif';
  font-style: normal;
  font-weight: 400;
  font-display: swap;
  src: url('../fonts/instrument-serif-400.woff2') format('woff2');
  unicode-range: U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6, U+02DA, U+02DC, U+0304, U+0308, U+0329, U+2000-206F, U+20AC, U+2122, U+2191, U+2193, U+2212, U+2215, U+FEFF, U+FFFD;
}
/* L'italique est un SECOND DESSIN, pas une inclinaison de la romaine : sans
   lui, le navigateur pencherait les lettres droites et le résultat se voit.
   Il n'est PAS préchargé — rien ne l'emploie encore dans la page ; il est là
   pour que le jour où un mot doit être mis en italique, la fonte existe. */
@font-face {
  font-family: 'Instrument Serif';
  font-style: italic;
  font-weight: 400;
  font-display: swap;
  src: url('../fonts/instrument-serif-400-italic.woff2') format('woff2');
  unicode-range: U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6, U+02DA, U+02DC, U+0304, U+0308, U+0329, U+2000-206F, U+20AC, U+2122, U+2191, U+2193, U+2212, U+2215, U+FEFF, U+FFFD;
}

/* LA TROISIÈME FONTE, ET ELLE NE SERT QU'À UNE PHRASE : le slogan de
   l'ouverture de l'accueil. Ce n'est pas un troisième alphabet de page, c'est
   la LETTRE DE LA MARQUE — celle de la petite phrase imprimée sur la carte de
   visite de NS Visual, sous le monogramme. Le client a demandé que le site
   ouvre sur cette phrase-là ; l'écrire dans une autre lettre que sa carte,
   c'était la citer de travers.
   Poppins, et pas Montserrat : les deux sont des géométriques à « a » d'un
   seul étage, mais relevées sur la carte redressée et mises à la longueur de
   la phrase imprimée, ce sont les proportions de Poppins qui tombent dessus —
   même hauteur d'x (celle de Montserrat est nettement plus haute et rend la
   ligne plus grasse à longueur égale), mêmes bols circulaires, même rythme de
   mots d'un bout de la phrase à l'autre.
   UNE SEULE FACE, 300. La famille en compte neuf ; le site n'en sert qu'une,
   parce qu'une seule phrase l'emploie. Le fichier « latin » suffit à cette
   phrase-là (à et é sont dans U+0000-00FF) ; le latin étendu est déclaré
   derrière lui pour que le jour où le slogan change, aucune lettre ne manque —
   son unicode-range fait qu'il n'est JAMAIS téléchargé tant que rien ne
   l'appelle. Seul le « latin » est préchargé. */
@font-face {
  font-family: 'Poppins';
  font-style: normal;
  font-weight: 300;
  font-display: swap;
  src: url('../fonts/poppins-300.woff2') format('woff2');
  unicode-range: U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6, U+02DA, U+02DC, U+0304, U+0308, U+0329, U+2000-206F, U+20AC, U+2122, U+2191, U+2193, U+2212, U+2215, U+FEFF, U+FFFD;
}
@font-face {
  font-family: 'Poppins';
  font-style: normal;
  font-weight: 300;
  font-display: swap;
  src: url('../fonts/poppins-300-latin-ext.woff2') format('woff2');
  unicode-range: U+0100-02BA, U+02BD-02C5, U+02C7-02CC, U+02CE-02D7, U+02DD-02FF, U+0304, U+0308, U+0329, U+1D00-1DBF, U+1E00-1E9F, U+1EF2-1EFF, U+2020, U+20A0-20AB, U+20AD-20C0, U+2113, U+2C60-2C7F, U+A720-A7FF;
}

/* --- Invariants : rythme, échelle, espaces (identiques aux deux directions) - */
:root {
  --lh-display: 1.06;

  /* LA LETTRE DE LA MARQUE, commune aux deux directions — et c'est voulu : ce
     n'est pas un choix d'habillage, c'est la lettre de la carte de visite de
     Noé. Elle ne sert QU'AU SLOGAN de l'ouverture (le h1 de .ecran), seul sur
     l'image depuis le lot 11 : les titres restent au --font-display de la
     direction, le texte au --font-text. */
  --font-slogan: 'Poppins', system-ui, sans-serif;

  /* LE VOILE des panneaux de l'accueil — les trois thèmes et le devis. C'est
     un APLAT posé sur la photo de fond, jamais un dégradé et jamais un filtre :
     il baisse l'image d'un ton, il ne la retouche pas (la colorimétrie de Noé
     est sa signature). --voile est son opacité au repos, --voile-survol celle
     qu'il prend quand le pointeur entre dans le panneau mais pas dans le cadre
     (style.css, « le cadre qui s'efface »).
     Les valeurs ci-dessous sont celles d'avant le lot 12 ; la direction Salle
     les remplace plus bas. */
  --voile-teinte: #0A0A0B;
  --voile: 0.5;
  --voile-survol: 0.15;

  --step-1: clamp(1.05rem, 1rem + 0.25vw, 1.2rem);     /* h4                */
  --step-2: clamp(1.3rem, 1.15rem + 0.7vw, 1.75rem);   /* h3                */
  --step-3: clamp(1.6rem, 1.35rem + 1.2vw, 2.4rem);    /* h2 de section     */
  --step-5: clamp(3rem, 2rem + 5vw, 6rem);             /* display géant     */

  --container: 1180px;
  --space-1: 0.5rem;  --space-2: 1rem;   --space-3: 1.5rem; --space-4: 2rem;
  --space-5: 3rem;    --space-6: 4rem;   --space-7: 6rem;   --space-8: 8rem;
  --section-y: clamp(var(--space-5), 7vw, var(--space-7));

  --dur-fast: 0.2s;
  --color-success: #1E7F4F;
  --color-error:   #B42318;
}

/* ============================================================================
   A — « PLANCHE CONTACT »   (design-directions.md §4)
   Papier neutre, grille dense de vignettes numérotées, filets 1 px, aucune
   ombre, aucun arrondi. Le signal #B8442A est le feutre de sélection : trois
   emplois par page au maximum.
   ============================================================================ */
html[data-theme="planche"] {
  /* Couleurs — §4, recopiées telles quelles */
  --color-bg:         #F2F2F1;
  --color-surface:    #FFFFFF;
  /* La surface des cartes de tarifs.html (lot 16) : sur le papier, il n'y a
     rien de plus clair que le blanc — c'est la surface elle-même. */
  --color-surface-carte: #FFFFFF;
  /* Le mode papier de tarifs.html (lot 17) : sur le papier, les cartes sont
     déjà du papier — elles gardent la surface et l'encre de la direction. */
  --papier-fond:  #FFFFFF;
  --papier-encre: #141414;
  --papier-muet:  #6E6B66;
  --papier-filet: rgba(20, 20, 20, 0.15);
  --papier-champ: #FFFFFF;
  --color-ink:        #141414;
  --color-muted:      #6E6B66;
  --color-line:       rgba(20, 20, 20, 0.16);
  --color-accent:     #141414;
  --color-accent-ink: #F2F2F1;
  --color-dark:       #141414;
  --color-dark-ink:   #F2F2F1;
  --color-signal:     #B8442A;

  /* Typographie — §4 */
  --font-display: 'Schibsted Grotesk', system-ui, sans-serif;
  --font-text:    'Newsreader', Georgia, serif;
  --font-label:   'Schibsted Grotesk', system-ui, sans-serif;  /* rôle : libellés */
  --tracking-display: -0.015em;
  --tracking-label:    0.1em;
  --step-4: clamp(2.75rem, 2rem + 4.2vw, 5.5rem);   /* le nom est l'accroche */
  --text-base: clamp(1.0625rem, 1rem + 0.3vw, 1.15rem);
  --lh-text: 1.62;

  /* Mise en page — §4 */
  --gutter: clamp(1.25rem, 4vw, 3.5rem);
  --radius: 0; --radius-sm: 0; --radius-pill: 0;
  --shadow: none;
  --nav-height: 62px;

  /* Mouvement — §4 : rien sur la grille, une seule transition (la visionneuse) */
  --dur:  0.35s;
  --ease: cubic-bezier(.2, .7, .2, 1);
  --reveal-y: 0px;
  --reveal-dur: var(--dur);
  --reveal-pas: 0s;      /* la grille du papier n'apparaît pas : rien à décaler */

  /* Rôles propres à la galerie */
  --planche-max: 1440px;              /* la planche est plus large que le texte */
  --planche-pad: var(--gutter);       /* elle reste dans la marge de papier     */
  --planche-gap-x: clamp(0.5rem, 1.6vw, 1.1rem);
  --planche-gap-y: clamp(1.5rem, 3vw, 2.5rem);
  --cadre-filet: 1px;                 /* le filet autour de chaque tirage       */
}

/* ============================================================================
   B — « SALLE SOMBRE »   (design-directions.md §3)
   Noir, aucun cadre et aucun fond autour de l'image, titres en serif, libellés
   et numéros en Archivo capitales. Fondu 0.4 s à l'entrée, rien d'autre. Aucune
   lueur, aucun verre dépoli, aucun dégradé, aucune ombre colorée.
   Seule la bande d'ambiance de l'accueil reste bord à bord : sur du noir elle
   ne se distingue plus par sa couleur, c'est sa largeur qui la signale.
   ============================================================================ */
html[data-theme="salle"] {
  /* Couleurs — §3, recopiées telles quelles, à une exception mesurée près (le
     gris secondaire, voir ci-dessous). */
  --color-bg:         #0A0A0B;
  --color-surface:    #141416;
  /* LA SURFACE DES CARTES DE TARIFS.HTML (lot 16) : un cran au-dessus de
     --color-surface, dans la même famille de gris, pour que les cartes et la
     carte du formulaire se détachent du noir de la page sans devenir grises.
     Le site reste sombre ; ce sont les photos des cartes qui apportent la
     lumière. Valeur réglée sur captures parmi cinq, de #141416 à #252426
     (README, « La page Tarifs »). */
  --color-surface-carte: #1C1B1D;
  /* LE MODE PAPIER DE TARIFS.HTML (lot 17, classe tarifs--papier sur <body>) :
     les cartes et la carte du formulaire passent en blanc cassé sur la page
     sombre. Aucune couleur nouvelle pour la carte et son encre — ce sont
     l'encre et le fond de la direction, inversés. Deux valeurs seulement
     s'ajoutent, et toutes deux sont mesurées :
       --papier-muet  le gris secondaire sur le papier : #5E5C59 tient 5,9:1
                      sur #F2F0EC et 6,7:1 sur le blanc des champs (4,5 exigé) ;
       --papier-champ le blanc des champs du formulaire.
     Les filets sont l'encre à 15 %. */
  --papier-fond:  var(--color-ink);
  --papier-encre: var(--color-bg);
  --papier-muet:  #5E5C59;
  --papier-filet: rgba(10, 10, 11, 0.15);
  --papier-champ: #FFFFFF;
  --color-ink:        #F2F0EC;
  /* §3 donne #8A8A8F. Mesuré sur le fond réel, il tient à 5,76:1 — au-dessus
     de la barre — mais les légendes sont en mono de 12 px, capitales et
     interlettrées : à ce corps-là 5,76 se devine plus qu'il ne se lit. Relevé
     à 7,61:1, soit moins de la moitié du contraste de l'encre (17,39:1) : le
     gris reste franchement secondaire, il devient seulement lisible. */
  --color-muted:      #A0A0A6;
  --color-line:       rgba(242, 240, 236, 0.14);
  /* PLUS D'ACCENT DE COULEUR — lot 12, demande du client après la
     présentation du 10 septembre. Le teal sourd #3A6F66 du §3 est retiré du
     site montré : il tenait trois rôles (le fond d'un bouton plein, l'anneau
     de focus, le survol d'un lien) et, dans une page où chaque panneau est une
     photo, il faisait une quatrième couleur qui n'était à personne. Les deux
     rôles sont repris par l'encre et le fond de la palette : un bouton devient
     un pavé blanc à texte noir, un anneau de focus devient blanc — plus
     visible sur du noir que le teal ne l'était — et un survol monte du gris à
     l'encre. Aucune valeur nouvelle : ce sont --color-ink et --color-bg.
     La direction A garde SON signal (#B8442A) : elle n'est pas montrée, et on
     ne mélange pas les deux directions. */
  --color-accent:     #F2F0EC;
  --color-accent-ink: #0A0A0B;
  --color-dark:       #000000;
  --color-dark-ink:   #F2F0EC;
  --color-signal:     #F2F0EC;

  /* Typographie — §3, révisée au lot 8 : le titre passe au SERIF.
     Le §3 donnait Archivo pour tout, et JetBrains Mono pour la signature des
     libellés. L'accueil s'ouvre maintenant sur un slogan posé plein cadre :
     un grotesque de labeur y disait « interface », là où il faut « auteur ».
     Instrument Serif ne sert QUE les titres ; Archivo garde le texte et
     reprend les libellés, que le mono étirait. */
  --font-display: 'Instrument Serif', Georgia, 'Times New Roman', serif;
  --font-text:    'Archivo', system-ui, sans-serif;
  /* Le rôle est resté « mono » dans les feuilles ; la fonte, elle, ne l'est
     plus. C'est le même alphabet que le texte, en capitales espacées : ces
     deux variables ne se distinguent plus que par ce qu'elles nomment. */
  --font-mono:    'Archivo', system-ui, sans-serif;
  --font-label:   'Archivo', system-ui, sans-serif;
  /* Un serif de titre ne se resserre pas comme un grotesque : à -0.02em ses
     empattements se touchaient dans les grands corps. */
  --tracking-display: -0.01em;
  --tracking-label:    0.12em;
  /* Même échelle de titre que sur le papier : le nom est l'accroche dans les
     deux directions, il n'y a aucune raison qu'il soit plus petit ici. */
  --step-4: clamp(2.75rem, 2rem + 4.2vw, 5.5rem);
  --text-base: clamp(0.95rem, 0.92rem + 0.2vw, 1.05rem);
  --lh-text: 1.55;

  /* Mise en page — §3 */
  --gutter: clamp(1.25rem, 4vw, 3.5rem);
  --radius: 0; --radius-sm: 0; --radius-pill: 0;
  --shadow: none;
  --nav-height: 76px;

  /* Mouvement — §3.
     --dur est la durée des trois fondus de la direction (le mur, la barre, la
     visionneuse) et ne bouge pas. L'APPARITION D'UNE PIÈCE a la sienne : à
     0.4 s et 10 px, le mouvement se devinait plus qu'il ne se voyait, et la
     page semblait seulement clignoter en défilant. 0.6 s et 16 px, c'est la
     même sobriété mais elle se lit ; --reveal-pas est l'écart d'une pièce à
     l'autre DANS UNE MÊME RANGÉE (galerie.js, poseDecalages). */
  --dur:  0.4s;
  --ease: cubic-bezier(.2, .7, .2, 1);
  --reveal-y: 16px;
  --reveal-dur: 0.6s;
  --reveal-pas: 0.06s;
  /* L'écart entre les trois nœuds d'un THÈME de l'accueil — titre, ligne,
     cadre. Plus large que celui des pièces d'une rangée (60 ms) : là il
     s'agissait de désolidariser quatre images de même taille, ici de faire
     lire trois blocs de poids très différents, dans l'ordre. */
  --theme-pas: 0.08s;

  /* Rôles propres à la galerie. Le mur de la salle est bâti comme celui de la
     planche — même largeur utile, même marge de page — avec DEUX écarts
     assumés, décidés parce que ses fonds bleus clairs se perdent sur du noir :
     l'image doit être plus grande (trois colonnes au lieu de quatre, voir
     style.css) et le mur plus serré (espacements réduits d'un tiers). */
  --planche-max: 1440px;
  --planche-pad: var(--gutter);
  --planche-gap-x: clamp(0.35rem, 1.05vw, 0.75rem);
  --planche-gap-y: clamp(1rem, 2vw, 1.65rem);
  --cadre-filet: 0px;

  /* La hauteur de rangée du mur. Au-dessus de 768 px, le mur n'est plus une
     grille de colonnes mais une suite de RANGÉES JUSTIFIÉES : chaque rangée a
     une hauteur commune, et la largeur d'une pièce découle de son format
     (2:3, 3:2, 9:16, 16:9). --mur-h est la hauteur visée ; le nombre de pièces
     par rangée s'en déduit, et la rangée s'étire ensuite jusqu'aux deux marges.
     C'est ce qui supprime les trous sous les pièces horizontales.
     Valeur de départ, pas hauteur imposée : la rangée réelle s'en écarte de ce
     qu'il faut pour tomber juste. */
  --mur-h: 300px;

  /* LE VOILE MONTE DE 0.5 À 0.72 — lot 12, demande du client. D'un panneau à
     l'autre les trois photos de fond n'ont pas la même dominante : le sport
     est presque noir, la mode est une image claire (des stores et une main en
     pleine lumière), le portrait est un fond de studio franchement bleu. À
     0.5, la page changeait donc de couleur à chaque écran — c'est le reproche
     qui a été fait, « les couleurs sont trop mélangées ».
     Relevé sur la bande de fond qui dépasse du cadre, à 1440, en luminance sur
     255 et en écart des canaux (la couleur) :
                          sport      mode       portrait
       voile 0.5          L27 c1     L75 c3     L50 c43
       voile 0.65         L23 c1     L55 c2     L38 c30
       voile 0.72         L19 c1     L46 c1     L32 c24     ← retenu
       voile 0.8          L17 c1     L36 c1     L26 c18
     0.65 laissait la mode deux fois plus claire que le sport et le portrait
     encore bleu ; 0.8 éteignait le sport, qui n'était plus qu'un aplat. À
     0.72, les trois tombent dans le même ton sombre et on devine encore la
     photo — le sujet, le cadrage, les masses — mais plus sa couleur.
     C'est justement ce que rend le survol, qui redescend le voile à 0.15 : la
     couleur est ce qu'on va CHERCHER, elle n'est plus ce qu'on subit. La
     teinte, elle, est le fond de page lui-même : un voile d'une autre couleur
     serait un filtre, et la colorimétrie de Noé est sa signature. */
  --voile-teinte: var(--color-bg);
  --voile: 0.72;
}
@media (min-width: 1200px) {
  html[data-theme="salle"] { --mur-h: 360px; }
}
/* 420 px seulement à partir de 1400. Entre 1200 et 1399, cette hauteur-là ne
   laissait plus tenir que deux pièces par rangée : la rangée s'étirait alors
   bien au-delà de la hauteur visée, et le mur perdait son grain. À 360, il en
   tient trois d'un bout à l'autre du palier. */
@media (min-width: 1400px) {
  html[data-theme="salle"] { --mur-h: 420px; }
}

/* ————— assets/css/base.css ————— */
/* ============================================================================
   OPAQ — socle v1.0 — base.css
   ----------------------------------------------------------------------------
   Fondations communes à tous les sites : reset, typographie, conteneur, grille,
   navigation, boutons, cartes, formulaires, apparitions, accessibilité.
   Ce fichier ne porte AUCUNE décision esthétique : tout passe par tokens.css.
   Un site ajoute son propre style.css APRÈS (composants métier, sections).
   ============================================================================ */

/* --- Reset minimal ------------------------------------------------------- */
*, *::before, *::after { box-sizing: border-box; }
* { margin: 0; }
html { -webkit-text-size-adjust: 100%; scroll-padding-top: calc(var(--nav-height) + 1rem); }
html:focus-within { scroll-behavior: smooth; }
body {
  font-family: var(--font-text);
  font-size: var(--text-base);
  line-height: var(--lh-text);
  color: var(--color-ink);
  background: var(--color-bg);
  -webkit-font-smoothing: antialiased;
  text-rendering: optimizeLegibility;
  min-height: 100dvh;
}
img, picture, video, svg { display: block; max-width: 100%; height: auto; }
input, button, textarea, select { font: inherit; color: inherit; }
button { cursor: pointer; background: none; border: 0; }
a { color: inherit; text-decoration-thickness: 1px; text-underline-offset: 0.18em; }
ul[role="list"], ol[role="list"] { list-style: none; padding: 0; }

/* --- Typographie ---------------------------------------------------------- */
h1, h2, h3, h4, .display {
  font-family: var(--font-display);
  line-height: var(--lh-display);
  letter-spacing: var(--tracking-display);
  font-weight: 600;
  text-wrap: balance;
}
h1, .h1 { font-size: var(--step-4); }
h2, .h2 { font-size: var(--step-3); }
h3, .h3 { font-size: var(--step-2); }
h4, .h4 { font-size: var(--step-1); }
.display { font-size: var(--step-5); }
p, li { max-width: 65ch; text-wrap: pretty; }
.lead { font-size: calc(var(--text-base) * 1.2); color: var(--color-muted); }
.muted { color: var(--color-muted); }
.label {
  font-size: 0.78rem; font-weight: 600; letter-spacing: var(--tracking-label);
  text-transform: uppercase; color: var(--color-muted);
}
.label--accent { color: var(--color-accent); }

/* --- Conteneur, sections, grilles ---------------------------------------- */
.container { width: min(100% - 2 * var(--gutter), var(--container)); margin-inline: auto; }
.container--narrow { --container: 800px; }
.section { padding-block: var(--section-y); }
.section--tight { padding-block: calc(var(--section-y) * 0.6); }
.section--dark { background: var(--color-dark); color: var(--color-dark-ink); }
.section--dark .muted, .section--dark .label { color: color-mix(in srgb, var(--color-dark-ink) 70%, transparent); }
.section__head { margin-bottom: var(--space-5); }
.section__head > * + * { margin-top: var(--space-2); }
.grid { display: grid; gap: var(--space-4); }
.grid--2 { grid-template-columns: repeat(auto-fit, minmax(min(100%, 18rem), 1fr)); }
.grid--3 { grid-template-columns: repeat(auto-fit, minmax(min(100%, 15rem), 1fr)); }
.grid--4 { grid-template-columns: repeat(auto-fit, minmax(min(100%, 11rem), 1fr)); }
.split { display: grid; gap: var(--space-5); align-items: center; grid-template-columns: minmax(0, 1fr); }
@media (min-width: 900px) { .split { grid-template-columns: minmax(0, 1fr) minmax(0, 1fr); } .split--reverse > :first-child { order: 2; } }
.grid > *, .split > * { min-width: 0; }
.stack > * + * { margin-top: var(--space-3); }
.cluster { display: flex; flex-wrap: wrap; gap: var(--space-2); align-items: center; }

/* --- Navigation ----------------------------------------------------------- */
.nav {
  position: sticky; top: 0; z-index: 50;
  height: var(--nav-height);
  display: flex; align-items: center;
  background: color-mix(in srgb, var(--color-bg) 85%, transparent);
  backdrop-filter: blur(12px); -webkit-backdrop-filter: blur(12px);
  border-bottom: 1px solid transparent;
  transition: border-color var(--dur-fast), background var(--dur-fast);
}
.nav.is-scrolled { border-bottom-color: var(--color-line); }
.nav__inner { display: flex; align-items: center; justify-content: space-between; gap: var(--space-4); }
.nav__brand { font-family: var(--font-display); font-weight: 700; font-size: 1.2rem; text-decoration: none; letter-spacing: -0.01em; }
.nav__brand img { height: 36px; width: auto; }
.nav__links { display: flex; gap: var(--space-4); align-items: center; }
.nav__links a { text-decoration: none; font-weight: 500; opacity: 0.85; }
.nav__links a:hover, .nav__links a[aria-current="page"] { opacity: 1; }
.nav__toggle { display: none; width: 44px; height: 44px; align-items: center; justify-content: center; border-radius: var(--radius-sm); }
.nav__toggle span { display: block; width: 22px; height: 2px; background: currentColor; position: relative; }
.nav__toggle span::before, .nav__toggle span::after { content: ''; position: absolute; left: 0; width: 22px; height: 2px; background: currentColor; transition: transform var(--dur-fast); }
.nav__toggle span::before { top: -7px; } .nav__toggle span::after { top: 7px; }
@media (max-width: 899px) {
  .nav__toggle { display: inline-flex; }
  .nav__links {
    position: fixed; inset: var(--nav-height) 0 auto 0;
    flex-direction: column; align-items: flex-start; gap: var(--space-3);
    padding: var(--space-4) var(--gutter) var(--space-5);
    background: var(--color-bg); border-bottom: 1px solid var(--color-line);
    transform: translateY(-8px); opacity: 0; pointer-events: none;
    transition: opacity var(--dur-fast), transform var(--dur-fast);
  }
  .nav.is-open .nav__links { transform: none; opacity: 1; pointer-events: auto; }
  .nav.is-open .nav__toggle span { background: transparent; }
  .nav.is-open .nav__toggle span::before { transform: translateY(7px) rotate(45deg); }
  .nav.is-open .nav__toggle span::after { transform: translateY(-7px) rotate(-45deg); }
}

/* --- Boutons ---------------------------------------------------------------- */
.btn {
  display: inline-flex; align-items: center; justify-content: center; gap: 0.5em;
  padding: 0.85em 1.6em; border-radius: var(--radius-pill);
  font-weight: 600; text-decoration: none; line-height: 1;
  background: var(--color-accent); color: var(--color-accent-ink);
  border: 1px solid var(--color-accent);
  transition: transform var(--dur-fast) var(--ease), opacity var(--dur-fast), background var(--dur-fast);
}
.btn:hover { transform: translateY(-1px); opacity: 0.92; }
.btn:active { transform: translateY(0); }
.btn--ghost { background: transparent; color: inherit; border-color: var(--color-line); }
.btn--ghost:hover { border-color: currentColor; opacity: 1; }
.btn--small { padding: 0.6em 1.1em; font-size: 0.9rem; }
.btn[disabled], .btn[aria-disabled="true"] { opacity: 0.5; pointer-events: none; }

/* --- Cartes ----------------------------------------------------------------- */
.card {
  background: var(--color-surface); border: 1px solid var(--color-line);
  border-radius: var(--radius); padding: var(--space-4);
  transition: transform var(--dur-fast) var(--ease), box-shadow var(--dur-fast);
}
.card:hover { transform: translateY(-3px); box-shadow: var(--shadow); }
.card > * + * { margin-top: var(--space-2); }
.card__media { border-radius: calc(var(--radius) - 4px); overflow: hidden; aspect-ratio: 4 / 3; margin: calc(-1 * var(--space-4)) calc(-1 * var(--space-4)) var(--space-3); }
.card__media img { width: 100%; height: 100%; object-fit: cover; }

/* --- Hero ------------------------------------------------------------------- */
.hero { padding-block: clamp(var(--space-6), 12vw, var(--space-8)); }
.hero__inner > * + * { margin-top: var(--space-3); }
.hero__actions { margin-top: var(--space-4); }
.hero--cover { position: relative; min-height: min(88dvh, 820px); display: grid; align-items: end; color: var(--color-dark-ink); isolation: isolate; }
.hero--cover img.hero__bg { position: absolute; inset: 0; width: 100%; height: 100%; object-fit: cover; z-index: -2; }
.hero--cover::after { content: ''; position: absolute; inset: 0; z-index: -1; background: linear-gradient(to top, rgba(0,0,0,0.72), rgba(0,0,0,0.15) 60%, rgba(0,0,0,0.1)); }

/* --- Chiffres clés, témoignages, FAQ (composants fréquents) ---------------- */
.stats { display: grid; grid-template-columns: repeat(auto-fit, minmax(10rem, 1fr)); gap: var(--space-4); border-block: 1px solid var(--color-line); padding-block: var(--space-4); }
.stat__value { font-family: var(--font-display); font-size: var(--step-3); line-height: 1; font-weight: 600; }
.quote { font-family: var(--font-display); font-size: var(--step-2); line-height: 1.3; font-weight: 400; }
.quote__by { margin-top: var(--space-2); font-size: 0.9rem; color: var(--color-muted); }
.faq details { border-top: 1px solid var(--color-line); padding: var(--space-2) 0; }
.faq details:last-child { border-bottom: 1px solid var(--color-line); }
.faq summary { cursor: pointer; font-weight: 600; list-style: none; display: flex; justify-content: space-between; gap: 1rem; padding-block: 0.4rem; }
.faq summary::-webkit-details-marker { display: none; }
.faq summary::after { content: '+'; font-weight: 400; }
.faq details[open] summary::after { content: '–'; }

/* --- Formulaires -------------------------------------------------------------- */
.form { display: grid; gap: var(--space-3); }
.form__row { display: grid; gap: var(--space-3); grid-template-columns: 1fr; }
@media (min-width: 640px) { .form__row { grid-template-columns: 1fr 1fr; } }
.field { display: grid; gap: 0.4rem; }
.field label { font-weight: 600; font-size: 0.92rem; }
.field input, .field textarea, .field select {
  width: 100%; padding: 0.8em 1em; border-radius: var(--radius-sm);
  border: 1px solid var(--color-line); background: var(--color-surface);
}
.field input:focus-visible, .field textarea:focus-visible, .field select:focus-visible { outline: 2px solid var(--color-accent); outline-offset: 1px; border-color: transparent; }
.field textarea { min-height: 8rem; resize: vertical; }
.form__hint { font-size: 0.85rem; color: var(--color-muted); }
.form__status { padding: var(--space-2) var(--space-3); border-radius: var(--radius-sm); font-weight: 500; }
.form__status--ok { background: color-mix(in srgb, var(--color-success) 12%, transparent); color: var(--color-success); }
.form__status--error { background: color-mix(in srgb, var(--color-error) 12%, transparent); color: var(--color-error); }
.form .btn { justify-self: start; }
.hp { position: absolute; left: -9999px; width: 1px; height: 1px; overflow: hidden; } /* honeypot */

/* --- Pied de page ----------------------------------------------------------------- */
.footer { border-top: 1px solid var(--color-line); padding-block: var(--space-6) var(--space-4); font-size: 0.95rem; }
.footer__grid { display: grid; gap: var(--space-5); grid-template-columns: repeat(auto-fit, minmax(min(100%, 14rem), 1fr)); }
.footer__bottom { margin-top: var(--space-5); padding-top: var(--space-3); border-top: 1px solid var(--color-line); display: flex; flex-wrap: wrap; justify-content: space-between; gap: var(--space-2); color: var(--color-muted); font-size: 0.85rem; }
.footer a { text-decoration: none; }
.footer__grid strong, .footer__grid .stack > a { display: block; }
.footer__grid .stack > * + * { margin-top: var(--space-1); }
.footer a:hover { text-decoration: underline; }

/* --- Apparitions au défilement ----------------------------------------------------- */
.reveal { opacity: 0; transform: translateY(var(--reveal-y)); transition: opacity var(--dur) var(--ease), transform var(--dur) var(--ease); }
.reveal.is-visible { opacity: 1; transform: none; }
.reveal[data-delay="1"] { transition-delay: 0.1s; } .reveal[data-delay="2"] { transition-delay: 0.2s; } .reveal[data-delay="3"] { transition-delay: 0.3s; }
.no-js .reveal { opacity: 1; transform: none; }

/* --- Accessibilité & utilitaires ----------------------------------------------------- */
.sr-only { position: absolute; width: 1px; height: 1px; padding: 0; margin: -1px; overflow: hidden; clip: rect(0 0 0 0); white-space: nowrap; border: 0; }
.skip-link { position: absolute; left: 1rem; top: -100%; z-index: 100; padding: 0.6em 1em; background: var(--color-ink); color: var(--color-bg); border-radius: var(--radius-sm); }
.skip-link:focus { top: 1rem; }
:focus-visible { outline: 2px solid var(--color-accent); outline-offset: 3px; }
.text-center { text-align: center; }
.mt-2 { margin-top: var(--space-2); } .mt-4 { margin-top: var(--space-4); } .mt-6 { margin-top: var(--space-6); }

@media (prefers-reduced-motion: reduce) {
  html:focus-within { scroll-behavior: auto; }
  *, *::before, *::after { animation-duration: 0.01ms !important; animation-iteration-count: 1 !important; transition-duration: 0.01ms !important; }
  .reveal { opacity: 1; transform: none; }
}
@media print {
  .nav, .footer, .btn, .skip-link { display: none !important; }
  body { background: #fff; color: #000; }
}

/* ————— assets/css/style.css ————— */
/* ============================================================================
   OPAQ — site de NS Production — style.css
   ----------------------------------------------------------------------------
   La mise en page. Chargé après tokens.css et base.css.

   UN SEUL BALISAGE, DEUX RENDUS. Le HTML produit par assets/js/galerie.js est
   rigoureusement identique dans les deux directions ; ce fichier seul décide
   de ce qu'on en fait :

     § A  html[data-theme="planche"] — planche contact : mur en lignes, les
          numéros se suivant de gauche à droite ; photo à sa hauteur naturelle,
          vidéo dans un cadre 16:9 noir, numéro de vue en tête de légende, les
          « sélection » en tête de mur et au feutre.
     § B  html[data-theme="salle"]   — salle sombre : MÊME mur, MÊME liste de
          vidéos, MÊMES numéros de vue, sur du noir et en JetBrains Mono ; trois
          écarts assumés seulement (le mur en rangées justifiées plutôt qu'en
          colonnes, mur serré d'un tiers, accueil ouvert en plein cadre),
          signalés « ÉCART » au § B.

   Les différences de format (2:3, 3:2, 9:16, 16:9) sont portées par les
   attributs data-type / data-format des pièces. Dans le MUR de la salle, ces
   formats donnent aussi leur proportion aux cadres : les ratios déclarés sont
   ceux des fichiers eux-mêmes, si bien qu'aucune image n'est recadrée — le
   cadre a exactement la forme de la photo qu'il contient. Partout ailleurs,
   aucune proportion n'est imposée. Aucun filtre sur une photo, nulle part.
   ============================================================================ */


/* ==========================================================================
   0. Ce que le socle propose et que les DEUX directions refusent
   ========================================================================== */

/* LE DÉFILEMENT DOUX, ET SANS UNE LIGNE DE JAVASCRIPT (la CSP interdirait de
   toute façon un gestionnaire inline). Le socle ne l'allume que sur
   html:focus-within, c'est-à-dire une fois que quelque chose a le focus : au
   premier clic sur une ancre, la page sautait. On l'allume donc sur html, et
   on l'éteint franchement sous mouvement réduit. */
html { scroll-behavior: smooth; }
@media (prefers-reduced-motion: reduce) {
  html { scroll-behavior: auto; }
}

/* LE PREMIER THÈME SE POSE EN HAUT DE L'ÉCRAN, sans laisser voir une bande de
   l'ouverture. Le socle réserve la hauteur de la navigation au-dessus de toute
   ancre (scroll-padding-top sur html) : c'est juste partout ailleurs, où la
   barre est opaque et masquerait le haut de la cible. Ici la barre est POSÉE
   SUR le panneau, exactement comme elle l'est sur l'ouverture — il n'y a rien
   à réserver. Un scroll-margin négatif de la même valeur annule ce
   rembourrage : les deux s'additionnent, et leur somme fait zéro. */
#travail { scroll-margin-top: calc(-1 * (var(--nav-height) + 1rem)); }
.nav {
  background: var(--color-bg);
  backdrop-filter: none;
  -webkit-backdrop-filter: none;
  border-bottom: 1px solid var(--color-line);
  /* Le passage à is-scrolled, DANS LES DEUX SENS : sur l'accueil, la barre est
     posée sur l'image tant que la page n'a pas défilé, puis reprend son fond et
     son filet. Sans transition, ce fond apparaissait d'un coup au 25e pixel de
     défilement — un claquement noir en haut de l'écran. 0.35 s, la durée d'un
     changement d'état qu'on veut voir sans l'attendre.
     Le socle transitionne « background », un raccourci qui embarque l'image de
     fond ; ici seule la COULEUR change, on la nomme. */
  transition: background-color 0.35s var(--ease), border-color 0.35s var(--ease);
}
.nav.is-scrolled { border-bottom-color: var(--color-line); }
.card, .btn, .card:hover { box-shadow: none; }
:focus-visible { outline: 2px solid var(--color-signal); outline-offset: 3px; }
[hidden] { display: none !important; }
.label { font-family: var(--font-label); }
body.est-bloque { overflow: hidden; }


/* ==========================================================================
   1. Structure commune (elle tient debout dans les deux directions)
   ========================================================================== */

/* --- Le logo, partout ---------------------------------------------------- */
/* Deux encres posées côte à côte dans le DOM, une seule visible : c'est la
   direction active qui choisit, pas le JavaScript. Le balisage reste donc
   rigoureusement le même d'une direction à l'autre. */
.logo-duo { display: block; }
html[data-theme="planche"] .logo--sombre { display: none; }
html[data-theme="salle"]   .logo--clair  { display: none; }

/* --- Navigation ---------------------------------------------------------- */
.nav__brand { font-family: var(--font-display); text-decoration: none; display: flex; align-items: center; }
/* En dessous de 80 px de haut, la baseline « NS PRODUCTION » du logo complet
   n'est plus lisible : la navigation porte donc le MONOGRAMME seul, servi
   depuis le dépôt en 384 x 400. Les attributs width/height réservent la place,
   seule la hauteur est imposée ici — et il faut passer devant la règle
   `.nav__brand img` du socle, d'où le sélecteur en deux classes. */
.nav__brand .nav__logo { height: 34px; width: auto; }
.nav__links a[aria-current="page"] { text-decoration: underline; text-underline-offset: 0.35em; }

/* Les deux réseaux ferment la ligne. Le dessin fait 20 px ; la CIBLE en fait
   44, comme le bouton du menu — c'est le minimum tenable au doigt, et il ne
   se voit pas. Une icône sans adresse reste affichée, à demi-encre : elle dit
   qu'il y aura là un compte, pas qu'il y en a un (voir lienReseau()). */
.nav__reseaux { display: flex; align-items: center; }
.nav__reseau {
  display: inline-flex; align-items: center; justify-content: center;
  width: 44px; height: 44px;
  text-decoration: none;
}
.nav__reseau .icone { display: block; width: 20px; height: 20px; }
.nav__reseau[aria-disabled="true"] { opacity: 0.5; cursor: default; }

/* --- LES CIBLES, AU DOIGT -------------------------------------------------
   44 px, en hauteur comme en largeur, pour tout ce qui se TOUCHE. Mesuré au
   lot 8 sur les trois largeurs de téléphone : le monogramme faisait 33 x 34,
   les liens du tiroir du menu 19 px de haut, ceux du pied 24 — on visait, on
   ratait, on retombait sur la page qu'on venait de quitter.
   Ce n'est pas le corps du texte qui grandit : c'est la BOÎTE autour de lui.
   Le mot garde sa taille, il gagne de l'air au-dessus et au-dessous.
   Restent en dehors, et c'est volontaire, les liens EN LIGNE DANS UNE PHRASE
   du bas de pied (« Mentions légales », « Confidentialité », « OPAQ ») : les
   grossir couperait la ligne en trois blocs, et une cible de 24 px dans un
   texte suivi est ce que la règle elle-même exempte (WCAG 2.2, 2.5.8). */
.skip-link { display: inline-flex; align-items: center; min-height: 44px; }
.nav__brand { min-width: 44px; min-height: 44px; }
@media (max-width: 899px) {
  .nav__links > a { display: flex; align-items: center; min-height: 44px; }
}
@media (max-width: 699px) {
  .footer__grid .stack > a,
  .footer__grid .stack .coord__valeur a { display: flex; align-items: center; min-height: 44px; }
}

/* --- Écran d'ouverture (accueil) ----------------------------------------- */
/* UN seul départ, et c'est un média. La page ne s'ouvre plus sur trois choses
   empilées — un titre géant, un logo, puis une bande vidéo — mais sur une
   image plein cadre que la navigation traverse.

   AUCUN texte n'est posé dessus : c'est la règle de la direction Salle, et
   c'est aussi ce qui dispense de tout voile ou dégradé pour rendre un texte
   lisible. Le nom est dans la navigation, sous la forme du monogramme ; le
   reste est dans la tête d'une ligne, juste en dessous.

   La hauteur est 100svh — la hauteur *petite* du viewport, celle qui ne
   change pas quand la barre d'adresse d'un téléphone se rétracte : sans elle
   l'écran d'ouverture sursaute au premier défilement. Le plancher de 560 px
   protège le cas du paysage sur un téléphone.

   Le média est une IMAGE dans le HTML livré, dans les deux directions. La
   vidéo est ajoutée par active(), et seulement sur un grand écran : voir
   assets/js/galerie.js, activeEcran(). */
.ecran {
  position: relative;
  display: block;
  height: 100svh;
  min-height: 560px;
  overflow: hidden;
  background: var(--color-dark);
  /* La navigation est collante et reste dans le flux : sans cette remontée,
     l'écran commencerait sous elle et le premier écran ferait
     100svh + la hauteur de la navigation. */
  margin-top: calc(-1 * var(--nav-height));
}
.ecran__media { display: block; width: 100%; height: 100%; }
.ecran__img, .ecran__video { width: 100%; height: 100%; object-fit: cover; }
/* La vidéo est posée SUR le poster, et ne se montre qu'une fois prête : on ne
   voit jamais de cadre noir à sa place. Fondu 0.4 s, le seul mouvement de la
   direction. */
.ecran__video {
  position: absolute; inset: 0;
  opacity: 0;
  transition: opacity var(--dur) var(--ease);
}
.ecran__video.est-vue { opacity: 1; }
@media (prefers-reduced-motion: reduce) {
  .ecran__video { transition: none; }
}

/* Le SLOGAN, posé sur l'image, seul depuis le lot 11 : le bouton « Voir le
   travail » qui descendait au premier thème est parti à la demande du client.
   Centré dans l'écran entier : le bloc couvre les 100svh du média, et la
   navigation lui passe par-dessus sans le pousser.
   Aucun voile, aucun dégradé sous le texte — la direction les interdit et la
   photo n'est pas un fond. (Le fondu du bas de l'écran, lot 20, n'est pas
   sous le texte : il ne couvre que le bas, vers le panneau Sport, et il est
   dessous.) Ce qui rend le texte lisible sur une zone claire,
   c'est une OMBRE PORTÉE sur les lettres elles-mêmes, la même réponse que
   celle de la navigation quand elle est posée sur l'image. */
.ecran__texte {
  position: absolute;
  inset: 0;
  /* AU-DESSUS DE LA VIDÉO, et il faut le dire. La vidéo d'ambiance n'est pas
     dans le HTML livré : active() la crée après le chargement et l'ajoute à la
     fin de .ecran — donc APRÈS ce bloc-ci. Deux éléments positionnés sans
     z-index sont empilés dans l'ordre du document : sans cette ligne, la vidéo
     recouvrait le slogan dès qu'elle arrivait. Le HTML ne peut pas régler
     cela — l'ordre y est déjà le bon avant que la vidéo n'existe.
     2 et non plus 1 depuis le lot 20 : l'aplat qui fond le bas de l'ouverture
     vers le panneau Sport (.ecran::after, z-index 1) passe entre la vidéo et
     le texte, jamais par-dessus le texte. */
  z-index: 2;
  display: grid;
  place-items: center;
}
.ecran__inner {
  display: grid;
  justify-items: center;
  gap: var(--space-4);
  text-align: center;
}
.ecran__titre {
  /* LA LETTRE DE LA CARTE DE VISITE, et elle ne sert qu'ici. Poppins 300 :
     c'est la petite phrase imprimée sous le monogramme NS, et c'est la même
     phrase. Voir tokens.css, --font-slogan. Le serif de la direction reprend
     la main partout ailleurs.
     300 et pas 400 : sur la carte la phrase est fine, et l'ombre portée
     ci-dessous suffit à la tenir sur l'image — c'est vérifié sur les captures
     à 390 et à 1440, poster compris.
     SON PROPRE CORPS, et plus --step-3/--step-4. L'échelle de titre du site
     est faite pour un serif à grande hauteur d'x ; une géométrique légère y
     paraît énorme, et surtout, à 88 px cette phrase-ci tenait sur deux lignes
     à 1440. Le palier haut (3.4rem, 54,4 px) la met sur UNE ligne à 1440 —
     1064 px dans un conteneur de 1180 —, le palier bas (1.8rem, 28,8 px) sur
     DEUX à 390. Interlettrage 0.02em : relevé sur la carte redressée, où les
     mots tombent un peu plus à droite que Poppins par défaut. */
  font-family: var(--font-slogan);
  font-weight: 300;
  font-size: clamp(1.8rem, 1.05rem + 3.2vw, 3.4rem);
  letter-spacing: 0.02em;
  /* --lh-display (1.06) est l'interligne d'un serif de titre ; les hampes et
     les jambages de Poppins sont longs, et deux lignes s'y touchaient. */
  line-height: 1.15;
  color: #FFFFFF;
  /* DEUX OMBRES, et c'est ce qu'il faut : la courte (1 px, 2 px de diffusion)
     détache la lettre du pixel qu'elle touche — c'est elle qui la sauve sur un
     mur clair ; la longue (12 px) creuse un halo sous le mot et l'assoit sur
     l'image. Une seule des deux laissait toujours un cas illisible. */
  text-shadow: 0 1px 2px rgba(0, 0, 0, 0.55), 0 2px 12px rgba(0, 0, 0, 0.55);
}
/* --- Les trois thèmes de l'accueil --------------------------------------- */
/* Une section par thème, pleine largeur, sur son image de fond. Le voile est
   un APLAT et non un dégradé : il baisse la photo, il ne la « fond » pas dans
   le noir — c'est ce qui permet d'écrire dessus sans que l'image devienne une
   texture. Son opacité et sa teinte sont dans tokens.css (--voile,
   --voile-teinte) : c'est là qu'on règle jusqu'où la photo se devine, et c'est
   là que le lot 12 l'a montée de 0.5 à 0.72 pour que les trois panneaux et le
   devis lisent dans le même ton. */
.travail { display: block; }
/* CHAQUE THÈME EST UN ÉCRAN. 100svh — la hauteur « petite » du viewport, celle
   qui ne bouge pas quand la barre d'adresse d'un téléphone se rétracte — avec
   un plancher de 640 px pour le paysage sur téléphone. C'est un minimum et non
   une hauteur imposée : quand le cadre est plus haut que l'écran (le portrait
   sur un téléphone), le panneau grandit avec lui au lieu de déborder.
   Les panneaux se suivent BORD À BORD : plus de trait entre eux, c'est le
   changement de photo qui dit qu'on a tourné la page. */
.theme {
  position: relative;
  isolation: isolate;
  display: grid;
  align-items: center;
  min-height: max(100svh, 640px);
  padding-block: var(--space-6);
  overflow: hidden;
}
.theme__fond {
  position: absolute; inset: 0; z-index: -1;
  background: var(--color-dark);
}
.theme__img { width: 100%; height: 100%; object-fit: cover; }
.theme__fond::after {
  content: '';
  position: absolute; inset: 0;
  /* Un aplat de la couleur de fond, et c'est son OPACITÉ qui porte le réglage :
     le survol n'a plus alors qu'une valeur à changer. Aucun filter, aucun
     mix-blend-mode : la colorimétrie de Noé n'est pas retouchée, elle est
     couverte. */
  background: var(--voile-teinte);
  opacity: var(--voile);
}

/* LE CADRE EST L'ÉLÉMENT PRINCIPAL DU PANNEAU, et sa largeur le dit : celle du
   mur (--planche-max, 1440) au lieu de celle d'un texte (--container, 1180),
   et des marges de 5vw au lieu de la marge de page. À 1440 il occupe 1296 px
   des 1440, soit neuf dixièmes de l'écran ; la photo de fond n'en est plus que
   la bordure. Le plancher de 1.25rem garde une marge tenable sous 400 px, où
   5vw ne feraient plus que 19 px. */
.theme__inner {
  width: min(100% - 2 * max(5vw, 1.25rem), var(--planche-max));
  margin-inline: auto;
}
.theme__tete { display: grid; gap: var(--space-2); justify-items: start; }
.theme__titre { font-size: var(--step-3); }
.theme__ligne { max-width: 48ch; }

/* LE CADRE : le seul bloc opaque de la page, et c'est voulu — il détache du
   fond ce sur quoi on peut cliquer. Une colonne sur un téléphone (la vidéo,
   puis l'aperçu), deux colonnes égales au-delà. */
.theme__cadre {
  background: var(--color-surface);
  border: 1px solid var(--color-line);
  padding: var(--space-4);
  display: grid;
  gap: var(--space-4);
  align-items: center;
}
/* L'en-tête tient toute la largeur du cadre, au-dessus des deux colonnes. Sur
   un téléphone il n'y a qu'une colonne : il est simplement le premier bloc. */
.theme__tete { grid-column: 1 / -1; }
/* Les deux colonnes ont la même construction : ce qu'on regarde, puis la
   barre qui le nomme. C'est ce qui les fait finir ensemble. */
.theme__media, .apercu { min-width: 0; display: grid; grid-template-rows: auto auto; }
.theme__media, .apercu { align-content: start; }

/* L'APERÇU-BOUTON : quatre vignettes et un libellé, dans un seul lien.
   Les cadres portent la proportion des fichiers (§ B) et se suivent en 2 x 2,
   à six pixels l'un de l'autre — l'écart d'une planche-contact, pas d'une
   grille. Coins droits, comme tout le reste du site. */
.apercu { display: grid; gap: 0; text-decoration: none; color: inherit; }
.apercu__mosaique {
  display: grid;
  grid-template-columns: repeat(2, minmax(0, 1fr));
  gap: 6px;
  align-items: start;
}
/* DEUX COLONNES, ET UNE HORIZONTALE LES PREND TOUTES LES DEUX — la règle du
   mur sous 768 px, reprise ici telle quelle. C'est le FORMAT de chaque pièce
   qui dessine, pas un réglage : quatre verticales font une mosaïque 2 x 2,
   deux verticales suivies d'une horizontale font deux vignettes puis une
   bande. Le reste — l'écart de six pixels, les coins droits, la proportion du
   fichier sur chaque cadre — ne change pas d'une forme à l'autre. */
.mosaique__vue[data-format="horizontal"] { grid-column: span 2; }
/* « 1+1 » — UNE verticale puis une horizontale (thème 01 depuis le lot 11).
   La verticale n'a pas de voisine : elle prend les deux colonnes, comme la
   bande sous elle. Chaque vignette garde la proportion de son fichier, rien
   n'est recadré. Vaut à toutes les largeurs : empilée sur un téléphone, la
   verticale est donc aussi large que la façade au-dessus d'elle. */
.theme__cadre[data-apercu="1+1"] .mosaique__vue { grid-column: 1 / -1; }
.mosaique__vue { position: relative; display: block; overflow: hidden; }
.mosaique__img { width: 100%; height: 100%; object-fit: cover; }

/* LA BOUCLE (lot 15). Une vidéo muette qu'active() pose PAR-DESSUS la
   couverture d'une pièce du cadre Sport — dans la façade (.piece__cadre) ou
   dans une vignette de l'aperçu (.mosaique__vue), deux hôtes déjà en position
   relative. Elle remplit exactement son hôte : le fichier a la proportion de
   la couverture, rien n'est recadré. Invisible tant qu'elle n'est pas prête,
   elle se révèle en 0.4 s — le fondu de la vidéo d'ouverture, pas un de plus.
   Elle ne prend aucun pointeur : le clic reste à ce qu'il y a dessous. Le
   repère de lecture d'une façade (::after du cadre) reste au-dessus d'elle,
   puisqu'il vient après elle dans le cadre. */
.boucle {
  position: absolute; inset: 0;
  width: 100%; height: 100%;
  object-fit: cover;
  opacity: 0;
  transition: opacity var(--dur) var(--ease);
  pointer-events: none;
}
.boucle.est-vue { opacity: 1; }
@media (prefers-reduced-motion: reduce) {
  .boucle { transition: none; }
}

/* LA BARRE, ET SES DEUX EXEMPLAIRES. Le ton du cadre, un filet en tête, un
   libellé à gauche, un signe à droite — et rien d'autre. Elle ne se distingue
   du cadre que par ce filet, ce qui est exactement ce qu'on veut : elle
   appartient à ce qu'elle termine, elle ne lui fait pas concurrence.
   48 px de haut : la cible du pouce, et surtout la MÊME hauteur des deux
   côtés — c'est elle qui met les deux colonnes du cadre à niveau. */
.barre {
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: var(--space-3);
  width: 100%;
  min-height: 48px;
  padding: var(--space-2) var(--space-3);
  margin-top: 6px;                 /* le même écart qu'entre deux vignettes */
  background: var(--color-surface);
  border-top: 1px solid var(--color-line);
  color: var(--color-ink);
}
/* Le libellé de gauche ne pousse jamais le signe hors du bandeau : il se
   coupe. Un titre de vidéo peut être long, la barre ne doit pas grandir. */
.barre__libelle {
  min-width: 0;
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
}
.barre__action { flex: none; display: inline-flex; align-items: center; gap: 0.5em; }
.barre__signe {
  flex: none;
  line-height: 1;
  transition: transform var(--dur) var(--ease);
}
/* La barre du média est une commande : elle se donne la main du curseur. */
.theme__barre[role="button"] { cursor: pointer; }

@media (min-width: 768px) {
  .theme__titre { font-size: var(--step-4); }
  .theme__cadre { grid-template-columns: repeat(2, minmax(0, 1fr)); }
  /* QUARANTE-CINQ / CINQUANTE-CINQ, et ce n'est pas un goût : c'est le seul
     couple qui met les deux colonnes à la même hauteur quand l'aperçu est en
     « 2 + 1 ». Une grande verticale 9:16 large de 0,45 fait 0,80 de haut ;
     l'aperçu, lui, empile deux vignettes 9:16 de (0,55 − écart)/2 et une bande
     16:9 de 0,55, soit 0,798. Mesuré à 1440 : 960 px contre 958,6 — un pixel
     et demi d'écart sur près d'un mètre de cadre.
     En « fr » et non en pour cent : les pour cent ignorent l'écart entre
     colonnes et débordent de sa largeur. */
  .theme__cadre[data-apercu="2+1"] { grid-template-columns: 45fr 55fr; }
  /* Le sens alterne : sur un thème pair, le média passe à droite (order), donc
     c'est la SECONDE colonne qui doit faire 45. */
  .theme:nth-of-type(even) .theme__cadre[data-apercu="2+1"] { grid-template-columns: 55fr 45fr; }
  /* « 1+1 » — LA FAÇADE VERTICALE EN GRAND (lot 11). D'un côté une façade
     9:16 large de a, de l'autre une verticale 9:16 puis une bande 16:9, toutes
     deux larges de b, six pixels entre elles ; les deux barres en dessous sont
     jumelles, elles s'annulent. Même hauteur des deux côtés :
         a × 16/9  =  b × 16/9  +  6  +  b × 9/16
     soit 256 a = 337 b + 864. Avec a + b = S, la largeur utile (le cadre moins
     son écart de colonnes) :
         b = S × 256/593 − 864/593 px      a = le reste
     C'est le « 57 / 43 » du dessin, au pixel près : 56,83 / 43,17, moins
     1,457 px — sans cette correction, les six pixels entre les deux vignettes
     laissaient l'aperçu six pixels plus bas que la façade.
     En calc et non en fr : l'écart de 6 px est une constante, une proportion
     seule ne peut pas l'absorber. Les pour cent d'une piste de grille se
     rapportent à la boîte de contenu du cadre, d'où le retrait de l'écart. */
  .theme__cadre[data-apercu="1+1"] {
    grid-template-columns: minmax(0, 1fr) calc((100% - var(--space-4)) * 256 / 593 - 864px / 593);
  }
  .theme:nth-of-type(even) .theme__cadre[data-apercu="1+1"] {
    grid-template-columns: calc((100% - var(--space-4)) * 256 / 593 - 864px / 593) minmax(0, 1fr);
  }
  /* LE SENS ALTERNE : la vidéo à gauche pour un thème impair, à droite pour un
     pair. Rien ne change dans le HTML — les deux colonnes y sont toujours
     écrites dans le même ordre, seule leur place bascule. Trois sections qui
     se répètent à l'identique se lisent comme un tableau ; alternées, elles se
     lisent comme trois pages. */
  .theme:nth-of-type(even) .theme__media { order: 2; }
}

/* « 1+2 » — LA PAYSAGE EN TÊTE, PUIS DEUX VERTICALES (lot 19, thème Sport).
   Le cadre n'a pas de façade : l'aperçu en est le seul contenu, sous l'en-tête,
   et il prend toute la largeur à toutes les tailles.
   Sous 1024 px, rien à ajouter : c'est la mosaïque ordinaire — la paysage
   prend les deux colonnes, les deux verticales partagent la rangée suivante,
   donc à la même hauteur l'une que l'autre. */
.theme__cadre[data-apercu="1+2"] { grid-template-columns: minmax(0, 1fr); }

/* À PARTIR DE 1024 PX, UNE SEULE RANGÉE, UNE DIMENSION VERROUILLÉE : la
   hauteur. Les trois cadres gardent la proportion de leur fichier (16:9, 9:16,
   9:16) ; pour qu'ils tombent à la même hauteur h, leurs largeurs doivent être
   dans le rapport 16/9 : 9/16 : 9/16, soit 256 : 81 : 81 — des « fr », qui se
   partagent la place une fois retirés les deux écarts de 6 px.
   La rangée entière est donc large de h × 418/144 + 12 px, et h est le plus
   petit de deux :
     — ce que permet la LARGEUR du cadre : (largeur − 12 px) × 72/209 ;
     — ce que permet la HAUTEUR de l'écran : 100svh moins tout ce que le
       panneau empile autour de la rangée (--rangee-reserve, tokens ci-dessous),
       pour que l'en-tête, la rangée et le bouton tiennent dans l'écran, sans
       défiler, à 1440 × 900 comme à 1366 × 768.
   Quand c'est la hauteur qui décide, la rangée est plus étroite que le cadre :
   elle se centre, et sa barre (le bouton) la suit, puisqu'elle est de la même
   largeur que l'aperçu. Un plancher (--rangee-min) empêche un écran très bas
   de réduire les images à des timbres : le panneau grandit alors, comme il le
   fait déjà sur téléphone.
   « 100cqi » est la largeur de contenu du cadre, d'où container-type. */
@media (min-width: 1024px) {
  .theme__cadre[data-apercu="1+2"] {
    container-type: inline-size;
    --rangee-min: 240px;
    /* 2 × --space-6 (le panneau) + 2 × --space-4 et 2 px de bordure (le
       cadre) + l'en-tête + --space-4 (l'écart) + la barre et son écart de
       6 px. L'en-tête et la barre sont MESURÉS sur la page rendue : 175 px et
       59 px à partir de 1366 (l'en-tête ne fait que 161 px à 1024, où le titre
       est plus petit — la réserve y est donc large de 14 px, ce qui ne coûte
       que quelques pixels de rangée). Soit 466 px en tout. Un titre ou une
       ligne réécrits sur deux lignes demanderaient de refaire la mesure. */
    --rangee-reserve: calc(2 * var(--space-6) + 2 * var(--space-4) + 2px + 175px + var(--space-4) + 65px);
  }
  .theme__cadre[data-apercu="1+2"] .apercu {
    --rangee-h: max(var(--rangee-min), min((100cqi - 12px) * 72 / 209, 100svh - var(--rangee-reserve)));
    width: calc(var(--rangee-h) * 209 / 72 + 12px);
    max-width: 100%;
    justify-self: center;
  }
  .theme__cadre[data-apercu="1+2"] .apercu__mosaique { grid-template-columns: 256fr 81fr 81fr; }
  .theme__cadre[data-apercu="1+2"] .mosaique__vue[data-format="horizontal"] { grid-column: auto; }
}

/* --- LE CADRE QUI S'EFFACE, ORDINATEUR SEULEMENT ------------------------
   Le panneau est bâti sur une photo, et on ne la voyait plus : le voile d'un
   côté, le cadre opaque qui couvre neuf dixièmes de la largeur de l'autre.
   Elle n'était plus un fond, elle était une bordure. Depuis le lot 12 le voile
   est plus épais encore (0.72) : ce mouvement-ci est donc le SEUL endroit où
   la couleur des photos de fond se donne, et c'est bien ce qu'on veut.

   Alors quand le pointeur entre dans le panneau MAIS PAS dans le cadre, le
   cadre s'efface presque (0.15) et le voile s'allège : la photo se montre en
   entier, le temps qu'on la regarde. Dès que le pointeur revient sur le cadre
   — ou qu'un de ses éléments prend le focus au clavier — tout revient.

   Trois précautions, et elles font tout :
     — OPACITÉ SEULEMENT. Jamais visibility, jamais pointer-events : à 0.15 le
       cadre est toujours là, toujours cliquable, et le pointeur qui le
       traverse le rallume. Un cadre qu'on aurait rendu inerte serait un piège.
     — ORDINATEUR SEULEMENT, par (hover: hover) and (pointer: fine). Sur un
       écran tactile, « survoler » n'existe pas : le doigt qui effleure
       laisserait le cadre effacé derrière lui.
     — :focus-within pour le clavier, qui ne survole rien : tabuler dans le
       cadre le rallume, sans quoi on remplirait un cadre invisible.
   Le voile, lui, demande :has() — c'est la seule façon de dire « le panneau
   est survolé, mais pas son cadre ». Là où :has() manque, cette règle-là est
   ignorée et l'autre tient : le cadre s'efface quand même, le voile reste à
   --voile. C'est un repli, pas une panne. */
@media (hover: hover) and (pointer: fine) {
  .theme__cadre {
    transition: opacity 0.45s var(--ease);
  }
  .theme:hover .theme__cadre { opacity: 0.15; }
  .theme .theme__cadre:hover,
  .theme .theme__cadre:focus-within { opacity: 1; }

  /* Le voile : c'est son OPACITÉ qui bouge, de --voile (0.72) à --voile-survol
     (0.15). La couleur ne change pas — seule la quantité de noir posée dessus.
     C'est ce geste-ci qui rend la photo : au repos les trois panneaux sont dans
     le même ton, et la couleur de chacun est ce qu'on va chercher. */
  .theme__fond::after { transition: opacity 0.45s var(--ease); }
  .theme:hover:not(:has(.theme__cadre:hover)):not(:has(.theme__cadre:focus-within)) .theme__fond::after {
    opacity: var(--voile-survol);
  }

  /* Mouvement réduit : le même effet, mais sans le fondu. Ce n'est pas une
     animation d'agrément — c'est ce qui donne accès à l'image. */
  @media (prefers-reduced-motion: reduce) {
    .theme__cadre, .theme__fond::after { transition: none; }
  }
}

/* --- Le panneau de devis, qui ferme l'accueil ---------------------------- */
/* Il ferme la page SUR UNE IMAGE, comme les trois thèmes l'ouvrent : même
   fabrique de fond, même voile plat. Sans image déclarée
   (accueil.devis_fond), il reste l'aplat de fond de page qu'il était. */
.panneau {
  position: relative;
  isolation: isolate;
  background: var(--color-bg);
  padding-block: var(--section-y);
}
.panneau__fond { position: absolute; inset: 0; z-index: -1; background: var(--color-dark); }
/* LE VISAGE EN ENTIER. Le panneau est large et bas, la couverture est un 16:9 :
   « cover » centré coupait le haut du cadre, donc le crâne. La tête occupe le
   quart supérieur de l'image — on remonte donc le point d'ancrage à 25 %, ce
   qui fait entrer tout le visage dans la fenêtre visible, à 1440 comme à 390.
   Aucun recadrage du fichier : c'est le même, regardé plus haut. */
.panneau__img { width: 100%; height: 100%; object-fit: cover; object-position: center 25%; }
.panneau__fond::after {
  content: '';
  position: absolute; inset: 0;
  /* LE MÊME VOILE QUE LES TROIS THÈMES, à la variable près : le devis ferme la
     page sur une image, il doit fermer dans le ton où les trois ont lu. */
  background: var(--voile-teinte);
  opacity: var(--voile);
}
.panneau__inner { display: grid; gap: var(--space-3); justify-items: start; }
.panneau__titre { font-size: var(--step-3); }
.panneau__intro { color: var(--color-muted); max-width: 56ch; }
.panneau__bouton { width: 100%; min-height: 48px; margin-top: var(--space-2); }
@media (min-width: 700px) {
  .panneau__bouton { width: auto; }
}

/* --- Les jonctions entre panneaux (lot 19, refaites au lot 20) ------------
   Ouverture → Sport, Sport → Mode, Mode → Portrait, Portrait → devis. Les
   deux images ne se touchent jamais sur une ligne nette. Le filet tracé au
   défilement du lot 19 est parti : le fondu suffit. Tout est ici, en CSS —
   identique sans JavaScript et sous mouvement réduit.

   LA FORME D'UNE JONCTION, de haut en bas :
     — l'image du dessus s'éteint vers le noir du voile sur --fondu-long
       (20svh), selon une courbe douce : onze arrêts sur une « smoothstep »
       (3t² − 2t³), qui part lentement et arrive lentement — on ne voit ni
       où le dégradé commence ni où il finit ;
     — une BANDE PLEINE de la couleur du voile, 2 × --fondu-bande (4svh) :
       les deux images ne sont jamais visibles au même endroit, même en
       transparence ;
     — l'image du dessous s'allume sur --fondu-long, selon la même courbe.
   En svh et non en vh : la hauteur petite du viewport, qui ne saute pas quand
   la barre d'adresse d'un téléphone se replie.

   COMMENT. Sur les fonds des panneaux, un MASQUE sur le bloc du fond (l'image
   et son voile ensemble), jamais un filtre sur la photo — sa colorimétrie
   n'est pas touchée, elle est seulement moins couvrante au bord. Ce qui
   apparaît dessous est le fond de page, la teinte même du voile (tokens.css,
   --voile-teinte). Une seule image de masque porte le haut ET le bas ; un
   bord qui ne se fond pas a ses deux longueurs à zéro, et tous ses arrêts
   tombent au bord. Sur l'OUVERTURE, où il y a une vidéo, pas de masque : un
   aplat de la couleur du voile posé PAR-DESSUS la vidéo, et c'est sur cet
   aplat qu'est le masque (voir .ecran::after plus bas). Le slogan est
   au-dessus de l'aplat, et bien plus haut que lui. */
:root {
  --fondu-long: 20svh;
  --fondu-bande: 2svh;
}
.travail .theme__fond,
.travail + .panneau .panneau__fond {
  --fh: 0px;  /* longueur du fondu du haut */
  --bh: 0px;  /* demi-bande pleine du haut */
  --fb: 0px;  /* longueur du fondu du bas  */
  --bb: 0px;  /* demi-bande pleine du bas  */
  --masque-fondu: linear-gradient(to bottom,
    rgba(0, 0, 0, 0) 0,
    rgba(0, 0, 0, 0) var(--bh),
    rgba(0, 0, 0, 0.028) calc(var(--bh) + var(--fh) * 0.1),
    rgba(0, 0, 0, 0.104) calc(var(--bh) + var(--fh) * 0.2),
    rgba(0, 0, 0, 0.216) calc(var(--bh) + var(--fh) * 0.3),
    rgba(0, 0, 0, 0.352) calc(var(--bh) + var(--fh) * 0.4),
    rgba(0, 0, 0, 0.5)   calc(var(--bh) + var(--fh) * 0.5),
    rgba(0, 0, 0, 0.648) calc(var(--bh) + var(--fh) * 0.6),
    rgba(0, 0, 0, 0.784) calc(var(--bh) + var(--fh) * 0.7),
    rgba(0, 0, 0, 0.896) calc(var(--bh) + var(--fh) * 0.8),
    rgba(0, 0, 0, 0.972) calc(var(--bh) + var(--fh) * 0.9),
    rgba(0, 0, 0, 1)     calc(var(--bh) + var(--fh)),
    rgba(0, 0, 0, 1)     calc(100% - var(--bb) - var(--fb)),
    rgba(0, 0, 0, 0.972) calc(100% - var(--bb) - var(--fb) * 0.9),
    rgba(0, 0, 0, 0.896) calc(100% - var(--bb) - var(--fb) * 0.8),
    rgba(0, 0, 0, 0.784) calc(100% - var(--bb) - var(--fb) * 0.7),
    rgba(0, 0, 0, 0.648) calc(100% - var(--bb) - var(--fb) * 0.6),
    rgba(0, 0, 0, 0.5)   calc(100% - var(--bb) - var(--fb) * 0.5),
    rgba(0, 0, 0, 0.352) calc(100% - var(--bb) - var(--fb) * 0.4),
    rgba(0, 0, 0, 0.216) calc(100% - var(--bb) - var(--fb) * 0.3),
    rgba(0, 0, 0, 0.104) calc(100% - var(--bb) - var(--fb) * 0.2),
    rgba(0, 0, 0, 0.028) calc(100% - var(--bb) - var(--fb) * 0.1),
    rgba(0, 0, 0, 0)     calc(100% - var(--bb)),
    rgba(0, 0, 0, 0)     100%);
  -webkit-mask-image: var(--masque-fondu);
          mask-image: var(--masque-fondu);
}
/* Le haut : tout thème qui suit un autre thème, le premier thème s'il suit
   l'ouverture, et le panneau de devis. */
.travail .theme + .theme .theme__fond,
.ecran + .travail .theme:first-child .theme__fond,
.travail + .panneau .panneau__fond { --fh: var(--fondu-long); --bh: var(--fondu-bande); }
/* Le bas : tout thème suivi d'un thème, et le dernier s'il est suivi du
   panneau de devis (:has ; sans :has, ce bord-là reste net, rien de cassé). */
.travail .theme:has(+ .theme) .theme__fond,
.travail:has(+ .panneau) .theme:last-child .theme__fond { --fb: var(--fondu-long); --bb: var(--fondu-bande); }

/* LE BAS DE L'OUVERTURE. Un aplat de la couleur du voile, haut de
   --fondu-long + --fondu-bande, posé sur le bas de l'écran, AU-DESSUS du
   poster et de la vidéo (z-index 1) et SOUS le slogan (z-index 2, voir
   .ecran__texte). La même courbe que les fonds, lue de bas en haut : plein
   sur la demi-bande, puis transparent au bout du fondu. Aucun filtre, aucun
   masque sur la vidéo elle-même : le masque est sur l'aplat, qui ne bouge
   pas. Il ne prend aucun pointeur. */
.ecran::after {
  content: '';
  position: absolute;
  inset: auto 0 0 0;
  z-index: 1;
  height: calc(var(--fondu-long) + var(--fondu-bande));
  background: var(--voile-teinte);
  pointer-events: none;
  --masque-ouverture: linear-gradient(to top,
    rgba(0, 0, 0, 1)     0,
    rgba(0, 0, 0, 1)     var(--fondu-bande),
    rgba(0, 0, 0, 0.972) calc(var(--fondu-bande) + var(--fondu-long) * 0.1),
    rgba(0, 0, 0, 0.896) calc(var(--fondu-bande) + var(--fondu-long) * 0.2),
    rgba(0, 0, 0, 0.784) calc(var(--fondu-bande) + var(--fondu-long) * 0.3),
    rgba(0, 0, 0, 0.648) calc(var(--fondu-bande) + var(--fondu-long) * 0.4),
    rgba(0, 0, 0, 0.5)   calc(var(--fondu-bande) + var(--fondu-long) * 0.5),
    rgba(0, 0, 0, 0.352) calc(var(--fondu-bande) + var(--fondu-long) * 0.6),
    rgba(0, 0, 0, 0.216) calc(var(--fondu-bande) + var(--fondu-long) * 0.7),
    rgba(0, 0, 0, 0.104) calc(var(--fondu-bande) + var(--fondu-long) * 0.8),
    rgba(0, 0, 0, 0.028) calc(var(--fondu-bande) + var(--fondu-long) * 0.9),
    rgba(0, 0, 0, 0)     100%);
  -webkit-mask-image: var(--masque-ouverture);
          mask-image: var(--masque-ouverture);
}

/* --- Tête de page : nom / titre de page ---------------------------------- */
.tete { padding-block: var(--section-y) 0; }
.tete__inner > * + * { margin-top: var(--space-2); }
.tete__accroche { font-size: calc(var(--text-base) * 1.25); max-width: 34ch; }
.tete__sous { color: var(--color-muted); max-width: 46ch; }

/* --- Bloc générique ------------------------------------------------------ */
.bloc { padding-block: var(--section-y); }
.bloc__tete { margin-bottom: var(--space-4); }
.bloc__tete > * + * { margin-top: var(--space-2); }

/* --- Hôte de la planche : c'est lui qui décide « marge » ou « bord à bord » */
.planche-hote {
  width: min(100% - 2 * var(--planche-pad), var(--planche-max));
  margin-inline: auto;
}

/* --- Filtres ------------------------------------------------------------- */
.filtres { display: flex; flex-wrap: wrap; gap: var(--space-2) var(--space-3); align-items: baseline; }
.filtre {
  font-family: var(--font-label);
  font-size: 0.76rem;
  letter-spacing: var(--tracking-label);
  text-transform: uppercase;
  color: var(--color-muted);
  padding: 0.35em 0;
  border-bottom: 1px solid transparent;
}
.filtre:hover { color: var(--color-ink); }
.filtre[aria-pressed="true"] { color: var(--color-ink); border-bottom-color: var(--color-ink); }
.filtres__compte {
  font-family: var(--font-label); font-size: 0.72rem;
  letter-spacing: var(--tracking-label); text-transform: uppercase;
  color: var(--color-muted);
}

/* --- Une pièce (photo ou vidéo) ------------------------------------------ */
.piece { margin: 0; min-width: 0; }
.piece__ouvrir {
  display: block; width: 100%; padding: 0; border: 0;
  background: none; text-align: inherit; color: inherit;
}
.piece__facade { position: relative; display: block; }
.piece__cadre {
  position: relative;
  display: block;
  background: transparent;
  border: var(--cadre-filet) solid var(--color-line);
}
/* Par défaut, aucune proportion imposée sur un cadre : l'image garde sa
   hauteur naturelle et c'est elle qui donne au cadre la sienne. Deux
   exceptions, toutes deux au § B :
     — le lecteur YouTube, qui n'a pas d'image pour lui donner sa hauteur et
       reprend donc le format de sa pièce ;
     — le MUR de la salle, où le cadre reçoit la proportion déclarée par
       data-type / data-format. Ce n'est pas un recadrage : ces proportions
       SONT celles des fichiers (22 photos en 2:3, 2 en 3:2, vidéos en 16:9 ou
       9:16), donc l'image remplit son cadre sans qu'un pixel soit coupé. C'est
       la condition pour que des rangées à hauteur commune tombent juste.

   Aucun filter, aucun mix-blend-mode, aucune opacité au repos : sa
   colorimétrie est sa signature. */
.piece__img { width: 100%; height: 100%; object-fit: contain; }

/* État « le fichier n'est pas encore sur Cloudinary » : le cadre garde sa
   place pour que la planche se lise quand même. Disparaît dès que l'image
   existe. */
.piece__absente {
  position: absolute; inset: 0;
  display: none; place-items: center;
  font-family: var(--font-label); font-size: 0.68rem;
  letter-spacing: var(--tracking-label); text-transform: uppercase;
  color: var(--color-muted);
}
.est-absente > .piece__img { visibility: hidden; }
.est-absente > .piece__absente { display: grid; }

/* --- La couverture qui lit (façade vidéo, lot 20) ------------------------
   Quand la pièce a un identifiant YouTube, la couverture ENTIÈRE est la
   commande (.piece__lecture : un lien vers YouTube dans le document livré, un
   bouton une fois active() passé) — on clique n'importe où sur l'image. Au
   centre, un BOUTON ROND : 64 px sur ordinateur, 56 sur téléphone, un
   triangle blanc sur un disque noir translucide, aucune couleur d'accent.
   Commun aux deux directions : le bouton sous l'image, qu'il fallait aller
   chercher, est parti. Au survol, le disque fonce ; au clavier, un anneau
   d'encre entoure la couverture. Le disque n'est ni un voile ni du verre
   dépoli : aucun flou, rien derrière lui n'est retouché. */
.piece__lecture {
  position: relative;
  display: block;
  color: inherit;
  text-decoration: none;
  cursor: pointer;
}
.piece__lecture:focus-visible { outline: 2px solid var(--color-signal); outline-offset: 3px; }
.piece__rond {
  position: absolute;
  top: 50%; left: 50%;
  width: 56px; height: 56px;
  margin: -28px 0 0 -28px;
  border-radius: 50%;
  background: rgba(0, 0, 0, 0.55);
  pointer-events: none;
  transition: background-color var(--dur-fast) var(--ease);
}
.piece__rond::before {
  content: '';
  position: absolute;
  /* Le triangle est décalé d'un douzième vers la droite : centré sur sa boîte,
     un triangle paraît trop à gauche — son centre de gravité est au tiers. */
  top: 50%; left: 54%;
  width: 18px; height: 21px;
  margin: -10.5px 0 0 -9px;
  background: #FFFFFF;
  clip-path: polygon(0 0, 100% 50%, 0 100%);
}
@media (min-width: 768px) {
  .piece__rond { width: 64px; height: 64px; margin: -32px 0 0 -32px; }
  .piece__rond::before { width: 20px; height: 24px; margin: -12px 0 0 -10px; }
}
.piece__lecture:hover .piece__rond,
.piece__lecture:focus-visible .piece__rond { background: rgba(0, 0, 0, 0.75); }

/* « SUR YOUTUBE ↗ » (lot 25). Trois vidéos ne s'intègrent pas — YouTube
   l'interdit — : leur couverture ouvre l'onglet YouTube au lieu de poser un
   lecteur, et elle le dit AVANT le clic, sous le bouton rond. Un libellé du
   site (Archivo capitale, interlettré), blanc, SANS cartouche ni voile : c'est
   une ombre portée courte qui le tient sur l'image, la réponse déjà retenue
   pour la navigation posée sur le poster et pour le triangle de lecture. Il ne
   prend aucun pointeur : c'est la couverture entière qui est le lien. */
.piece__mention {
  position: absolute;
  top: calc(50% + 40px);
  left: 0;
  right: 0;
  text-align: center;
  font-family: var(--font-label);
  font-size: 0.72rem;
  font-weight: 600;
  letter-spacing: var(--tracking-label);
  text-transform: uppercase;
  color: #FFFFFF;
  text-shadow: 0 1px 2px rgba(0, 0, 0, 0.6);
  pointer-events: none;
}
@media (min-width: 768px) {
  .piece__mention { top: calc(50% + 46px); }
}
@media (prefers-reduced-motion: reduce) {
  .piece__rond { transition: none; }
}
.piece__lecteur { display: block; width: 100%; height: 100%; border: 0; }

/* Le repère de lecture : un triangle plein posé en bas à gauche de la
   couverture. Il n'est pas là pour décorer — sans lui, la page Vidéos se lit
   comme une page de photos. Il disparaît dès que le lecteur est lancé.
   L'ombre portée est sur le triangle SEUL (un pseudo-élément) : elle le garde
   lisible sur une couverture claire. Aucun filtre ne touche l'image. */
.piece--video .piece__cadre::after {
  content: '';
  position: absolute;
  left: 0.7rem;
  bottom: 0.7rem;
  width: 0.7rem;
  height: 0.85rem;
  background: #FFFFFF;
  clip-path: polygon(0 0, 100% 50%, 0 100%);
  filter: drop-shadow(0 1px 2px rgba(0, 0, 0, 0.5));
  pointer-events: none;
}
.piece__facade.est-lue .piece__cadre::after { content: none; }
/* Une couverture qui lit a son bouton rond au centre : le petit triangle du
   coin ferait doublon. Il reste sur les vidéos sans identifiant YouTube. */
.piece__lecture .piece__cadre::after { content: none; }

/* --- Légende ------------------------------------------------------------- */
.piece__legende { display: grid; gap: 0.1rem; }
.piece__num { font-variant-numeric: tabular-nums; }

/* --- Coordonnées (pied de page) ------------------------------------------ */
/* Les lignes de coordonnées de contact.html sont parties avec la page (lot
   13) ; reste la valeur, que le pied de page emploie. */
.coord__valeur { font-size: calc(var(--text-base) * 1.05); }

/* --- Demande de devis (tarifs) ------------------------------------------- */
/* Le formulaire tient dans la largeur d'un texte et pas dans celle du mur :
   un champ large de 1180 px ne se remplit pas, il s'arpente. */
.devis { max-width: 46rem; }
.devis > * + * { margin-top: var(--space-3); }
.devis__intro { color: var(--color-muted); max-width: 56ch; }
/* Le libellé d'un champ porte le rôle « label » du site. Le socle donne à
   « .field label » un corps à lui, plus spécifique que .label ; on lui rend
   la main. */
.devis .field label.label { font-size: 0.78rem; }
/* « (FACULTATIF) », VOISIN DU LIBELLÉ (lot 15). La mention a quitté le
   <label> — Netlify titre la ligne du mail d'après lui — mais pas la ligne :
   le champ passe à deux colonnes, le libellé à sa largeur, la mention juste
   après lui, et le champ de saisie dessous, sur les deux. Même corps, même
   graisse, mêmes capitales : elle porte la classe .label, comme lui. L'espace
   qui les séparait dans le texte est rendu par une espace insécable en tête
   de la mention — une espace ordinaire en début de boîte serait avalée —, et
   elle prend l'interlettrage des capitales comme l'autre le prenait. */
.devis .field--mention { grid-template-columns: auto 1fr; column-gap: 0; }
.devis .field--mention > :not(.label) { grid-column: 1 / -1; }
.field__mention::before { content: '\00A0'; }
/* Un champ de saisie ne PREND PAS la police de la page : le navigateur lui
   impose la sienne. Sans cette ligne, le formulaire est le seul endroit du
   site à ne pas être en Archivo. */
.devis .field input, .devis .field textarea { font: inherit; color: var(--color-ink); }
.devis .field textarea { min-height: 9rem; }
/* Le titre du formulaire est un titre de SECTION de la page Tarifs : il a le
   corps des titres de catégorie au-dessus de lui. */
.devis__titre { font-size: var(--step-3); }
/* Il reçoit le focus quand la barre fixe y descend (tabindex="-1") : un
   focus donné par programme n'a pas à tracer d'anneau. */
.devis__titre:focus { outline: none; }

/* SANS JAVASCRIPT SEULEMENT (lot 26). Les cases et la date ne sont plus des
   champs du formulaire : c'est le script qui les fait voyager, dans la phrase.
   Sans lui, seul le message part — on demande donc d'y mettre aussi la date et
   les prestations. La ligne est dans le document livré et galerie.js la cache
   en retirant « no-js » de <html> : rien n'apparaît après coup. */
.form__sansjs { display: none; }
html.no-js .form__sansjs {
  display: block;
  color: var(--color-muted);
  font-size: 0.875rem;
  line-height: 1.45;
}

/* --- Les prestations, dans le formulaire --------------------------------- */
/* Un fieldset garde du navigateur un cadre, une marge et une largeur minimale
   au contenu (min-inline-size) qui le fait déborder d'un écran étroit : on
   retire les trois. */
.choix, .choix__groupe { margin: 0; padding: 0; border: 0; min-inline-size: 0; }
.choix { display: grid; gap: var(--space-3); }
.choix__legende { padding: 0; margin-bottom: var(--space-2); }
.choix__categorie { padding: 0; margin-bottom: var(--space-2); color: var(--color-muted); font-size: 0.875rem; }
.choix__options { display: grid; gap: var(--space-2); }
@media (min-width: 640px) {
  .choix__options { grid-template-columns: repeat(2, minmax(0, 1fr)); }
}
/* UNE LIGNE PAR CASE, ET TOUTE LA LIGNE SE TOUCHE : 48 px de haut, la case
   dans son libellé. L'ÉTAT SE LIT D'UN COUP D'ŒIL — non cochée, un filet et
   le mot en gris ; cochée, le filet et le mot passent à l'encre et la ligne
   prend le fond des cartes. Aucune couleur nouvelle. */
.choix__option {
  display: flex;
  align-items: center;
  gap: var(--space-2);
  min-height: 48px;
  padding: 0.5rem var(--space-3);
  border: 1px solid var(--color-line);
  color: var(--color-muted);
  cursor: pointer;
  line-height: 1.3;
}
.choix__option input {
  flex: none;
  width: 1.125rem;
  height: 1.125rem;
  margin: 0;
  accent-color: var(--color-ink);
  cursor: pointer;
}
.choix__option:has(input:checked) {
  border-color: var(--color-ink);
  background: var(--color-surface);
  color: var(--color-ink);
}
@media (hover: hover) {
  .choix__option:hover { color: var(--color-ink); }
}

/* --- La page Tarifs ------------------------------------------------------- */
/* Une section par catégorie, sous la tête de page. Leurs en-têtes sont ceux
   des cadres de l'accueil : le numéro en libellé, le titre en serif. */
.tarifs__categorie { padding-block: var(--section-y) 0; }
.tarifs__tete { display: grid; gap: var(--space-2); justify-items: start; margin-bottom: var(--space-4); }
.tarifs__titre { font-size: var(--step-3); }
@media (min-width: 768px) {
  .tarifs__titre, .devis__titre { font-size: var(--step-4); }
}

/* LA GRILLE : une carte par rangée sous 768 px, deux à partir de 768, trois
   à partir de 1200. L'air entre deux rangées est la marge basse des cartes.
   align-items: start (lot 17) — une carte n'est JAMAIS étirée à la hauteur de
   sa voisine : ouvrir le détail d'une carte ne fait grandir qu'elle. */
.tarifs__grille {
  display: grid;
  grid-template-columns: minmax(0, 1fr);
  column-gap: var(--space-3);
  row-gap: 0;
  align-items: start;
}
@media (min-width: 768px) {
  .tarifs__grille { grid-template-columns: repeat(2, minmax(0, 1fr)); }
}
/* Les formules de la carte large gardent leur grille du lot 17 : trois à partir
   de 1200 px. */
@media (min-width: 1200px) {
  .carte--large .tarifs__grille { grid-template-columns: repeat(3, minmax(0, 1fr)); }
}
/* LA PLACE D'UNE CARTE (lot 21) — l'enveloppe extérieure, construite par
   rend() : c'est elle qui est dans la grille. Elle porte le décalage vertical
   (la marge ci-dessous), l'apparition, et la profondeur au défilement
   (activeTarifs) ; la carte qu'elle contient porte le flottement. */
.tarifs__place { min-width: 0; }
.tarifs__place--large { grid-column: 1 / -1; }

/* --- LE MUR DE TIRAGES ÉPINGLÉS, À PARTIR DE 1024 PX (lot 21) -------------
   Les cartes ne partent plus toutes de la même ligne. Elles gardent leur ordre
   de lecture et une grille de colonnes :
     — TROIS colonnes (les packs photo : 298 px par carte à 1024, 377 à 1440) ;
     — DEUX, en 2 × 2, pour une grille de QUATRE cartes (les packs vidéo). À
       1440 le conteneur fait 1180 px : quatre colonnes n'y laisseraient que
       277 px par carte, sous le plancher de 280, et une accroche de quatre
       lignes pour un titre ; en 2 × 2 chaque carte fait 578 px et tout se lit
       d'un coup d'œil (captures du lot 21). galerie.js annonce les photos de
       ces cartes à la même largeur (TAILLES.carte_demi).
   LE DÉCALAGE est une MARGE, pas une position : la rangée grandit avec sa
   carte la plus basse, la section avec ses rangées, et quand un détail
   s'ouvre rien ne chevauche rien. Six valeurs entre 0 et 80 px, dans l'ordre
   des cartes, choisies pour que deux voisines — dans une rangée de trois, de
   quatre, ou dans un 2 × 2, côte à côte comme l'une sous l'autre — ne soient
   jamais au même niveau et ne dessinent ni escalier (trois de suite qui
   montent ou descendent) ni vague (qui alternent). La carte large n'est pas
   décalée.
   Le rembourrage haut de la grille laisse la place à la profondeur : une
   carte peut remonter de 40 px au plus, elle ne vient jamais sur le titre de
   sa section. */
@media (min-width: 1024px) {
  .tarifs__categorie > .container > .tarifs__grille {
    grid-template-columns: repeat(3, minmax(0, 1fr));
    padding-top: 40px;
  }
  .tarifs__categorie > .container > .tarifs__grille:has(> .tarifs__place:nth-child(4):last-child) {
    grid-template-columns: repeat(2, minmax(0, 1fr));
  }
  .tarifs__place:nth-child(6n+1) { margin-top: 24px; }
  .tarifs__place:nth-child(6n+2) { margin-top: 72px; }
  .tarifs__place:nth-child(6n+3) { margin-top: 40px; }
  .tarifs__place:nth-child(6n+4) { margin-top: 0; }
  .tarifs__place:nth-child(6n+5) { margin-top: 64px; }
  .tarifs__place:nth-child(6n)   { margin-top: 16px; }
  .tarifs__place.tarifs__place--large { margin-top: 0; }
  /* Une ombre très légère détache le papier du fond sombre. Aucune lueur,
     aucun verre, aucune bordure colorée. */
  .tarifs--papier .tarifs__place > .carte { box-shadow: 0 18px 36px -24px rgba(0, 0, 0, 0.7); }
  .tarifs--papier .tarifs__place > .carte.est-choisie {
    box-shadow: 0 0 0 1px var(--papier-encre), 0 18px 36px -24px rgba(0, 0, 0, 0.7);
  }
}

/* L'APPARITION DES CARTES (lot 21), la mécanique des pièces et des thèmes :
   la place monte de --reveal-y et se découvre quand elle entre dans l'écran
   (observe, dans activeTarifs), et rejoue en y revenant. Les cartes d'une même
   rangée partent l'une après l'autre, 90 ms d'écart (data-decalage, mesuré
   par galerie.js). C'est la propriété « translate » qui monte, et non
   « transform » : la profondeur au défilement tient transform, les deux
   s'additionnent sans se contrarier. Sans JavaScript (no-js), rien n'est
   caché ; sous mouvement réduit, active() les montre d'emblée et la
   transition est coupée. */
html[data-theme="salle"]:not(.no-js) .tarifs__place {
  opacity: 0;
  translate: 0 var(--reveal-y);
  transition: opacity var(--reveal-dur) var(--ease), translate var(--reveal-dur) var(--ease);
}
html[data-theme="salle"]:not(.no-js) .tarifs__place.is-vu { opacity: 1; translate: none; }
html[data-theme="salle"] .tarifs__place[data-decalage="1"] { transition-delay: 90ms; }
html[data-theme="salle"] .tarifs__place[data-decalage="2"] { transition-delay: 180ms; }
html[data-theme="salle"] .tarifs__place[data-decalage="3"] { transition-delay: 270ms; }
@media (prefers-reduced-motion: reduce) {
  html[data-theme="salle"]:not(.no-js) .tarifs__place { opacity: 1; translate: none; transition: none; }
}

/* LA CARTE COMPACTE (lot 16). Le client a aimé une page de réservation où
   chaque prestation tient dans une petite carte à photo : ses six parties —
   photo, titre, accroche, ligne prix — durée, détail dépliable, barre — se
   suivent en colonne.
   PLUS DE LIGNES PARTAGÉES (lot 17). Au lot 16 les parties étaient rangées sur
   les lignes de la grille (subgrid) : les cartes fermées s'alignaient, mais un
   détail OUVERT agrandissait sa ligne pour toute la rangée, et les voisines
   s'étiraient d'autant — un subgrid s'étire toujours sur ses lignes, rien ne
   l'en empêche. Chaque carte a donc repris sa propre colonne, la grille ne
   les étire plus (align-items: start), et l'alignement des cartes FERMÉES est
   tenu par deux RÉSERVES de hauteur, plus bas : l'accroche garde la place de
   sa plus longue voisine, la ligne prix — durée celle de deux lignes. Photo,
   titre, « Voir le détail » et barre ont la même hauteur d'une carte à
   l'autre : tout tombe au même niveau, et seule la carte qu'on ouvre grandit.
   L'espace entre les parties est porté par chaque partie.
   SA SURFACE est --color-surface-carte (tokens.css) : un cran plus claire que
   le fond des champs, pour que la carte se détache du noir de la page sans
   devenir grise. C'est la photo qui apporte la lumière. */
.carte {
  --carte-pad: var(--space-3);
  display: flex;
  flex-direction: column;
  margin-bottom: var(--space-3);
  background: var(--color-surface-carte);
  border: 1px solid var(--color-line);
  min-width: 0;
}
.carte > :not(.carte__barre):not(.carte__photo) { min-width: 0; margin-inline: var(--carte-pad); }
.carte > .carte__titre { padding-top: var(--space-3); }
/* Une carte sans photo (une formule) : le titre reprend l'air du bord. */
.carte > .carte__titre:first-child { padding-top: var(--carte-pad); }
.carte > .carte__accroche { padding-top: 0.5rem; }
.carte > .carte__prix { padding-top: var(--space-3); }
.carte > .carte__detail { margin-top: var(--space-3); }
.carte > .carte__barre { margin-top: var(--space-2); }

/* LES DEUX RÉSERVES, là où plusieurs cartes partagent une rangée (768 px et
   plus), et pour les cartes des catégories seulement — les formules de la
   carte large ont la leur, plus bas.
   L'ACCROCHE garde la place de deux lignes, trois dans les deux bandes de
   largeur où la plus longue en prend trois. Mesuré au balayage de 768 à 2400
   au pas de 10 (accroches du lot 13) : trois lignes de 768 à 807 px et de
   1200 à 1217 px, deux partout ailleurs ; les bandes sont élargies de vingt
   pixels par prudence. Une accroche réécrite plus longue peut demander de
   refaire la mesure : elle ne casse rien, elle décale seulement sa carte.
   LA LIGNE PRIX — DURÉE garde la place de deux lignes — le montant, puis la
   durée passée dessous —, ce qu'elle occupe dès que la carte est trop étroite
   pour les tenir sur une seule. C'est la somme exacte de ses hauteurs : le
   corps du montant (interligne 1), l'écart entre les deux lignes, la ligne de
   la durée (interligne 1.2). */
/* LOT 21 : la carte est dans sa place (.tarifs__place). La bande des
   1200-1237 px est partie avec la grille de trois colonnes qu'elle mesurait :
   sur ordinateur les cartes sont maintenant décalées, et l'alignement d'une
   rangée n'a plus à tomber au pixel. */
@media (min-width: 768px) {
  .tarifs__grille > .tarifs__place > .carte:not(.carte--large) > .carte__accroche {
    min-height: calc(2lh + 0.5rem);
  }
  .tarifs__grille > .tarifs__place > .carte:not(.carte--large) > .carte__prix {
    min-height: calc(var(--step-3) + 0.35rem + 0.8125rem * 1.2 + var(--space-3));
    align-content: flex-start;
  }
}
@media (min-width: 768px) and (max-width: 827px) {
  .tarifs__grille > .tarifs__place > .carte:not(.carte--large) > .carte__accroche {
    min-height: calc(3lh + 0.5rem);
  }
}

/* LA PHOTO. Le cadre impose sa proportion — 3:2, le bandeau de la carte
   large 3:1 — et l'image le remplit : c'est le SEUL recadrage du site, et il
   ne touche que l'affichage (voir galerie.js, photoCarte). Bord à bord dans
   la carte, sous son filet. Le point focal descend du milieu au quart haut
   quand les données le demandent. */
.carte__photo { aspect-ratio: 3 / 2; overflow: hidden; background: var(--color-dark); }
/* Le bandeau de la carte large : 3:2 comme les autres sous 1024 px (lot 21),
   3:1 au-delà — le <picture> sert le fichier recadré à la même forme. */
@media (min-width: 1024px) {
  .carte__photo--bandeau { aspect-ratio: 3 / 1; }
}
.carte__image { display: block; width: 100%; height: 100%; }
.carte__img { display: block; width: 100%; height: 100%; object-fit: cover; object-position: 50% 50%; }
.carte__photo[data-focal="haut"] .carte__img { object-position: 50% 25%; }

.carte__titre { font-size: var(--step-2); }
.carte__accroche { color: var(--color-muted); font-size: 0.9375rem; line-height: 1.45; }

/* LA LIGNE PRIX — DURÉE. Le montant est le plus grand corps de la carte, en
   serif ; « à partir de » le précède en petit libellé, et la durée le suit,
   un cran plus petite, à l'encre, tiret compris. Tout est calé sur la même
   ligne de base ; si la carte est trop étroite, la durée passe dessous. */
.carte__prix {
  display: flex;
  flex-wrap: wrap;
  align-items: baseline;
  column-gap: 0.5em;
  row-gap: 0.35rem;
  line-height: 1.2;
}
.carte__prefixe {
  font-family: var(--font-label);
  font-size: 0.75rem;
  font-weight: 600;
  letter-spacing: var(--tracking-label);
  text-transform: uppercase;
  color: var(--color-muted);
}
.carte__montant {
  font-family: var(--font-display);
  font-size: var(--step-3);
  line-height: 1;
  letter-spacing: var(--tracking-display);
  white-space: nowrap;
}
/* LA DURÉE, en libellé — mais SANS capitales : « 1h30 » et « 2h » en
   capitales deviendraient « 1H30 » et « 2H », et le symbole de l'heure
   s'écrit en minuscule. */
.carte__duree {
  font-family: var(--font-label);
  font-size: 0.8125rem;
  font-weight: 600;
  letter-spacing: 0.02em;
  color: var(--color-ink);
}

/* LE DÉTAIL DÉPLIABLE. Un <details> natif : il s'ouvre au clic, à Entrée et
   à Espace sans une ligne de JavaScript, et le navigateur annonce son état.
   Le résumé est un libellé sous un filet, le mot à gauche et le signe à
   droite (+ fermé, − ouvert) — le dessin des barres, à l'échelle d'un
   libellé, pour qu'il ne se confonde pas avec la barre d'ajout juste
   dessous. Le marqueur du navigateur est retiré. */
.carte__detail { border-top: 1px solid var(--color-line); }
.carte__resume {
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: var(--space-2);
  min-height: 44px;
  cursor: pointer;
  list-style: none;
  font-family: var(--font-label);
  font-size: 0.75rem;
  font-weight: 600;
  letter-spacing: var(--tracking-label);
  text-transform: uppercase;
  color: var(--color-muted);
}
.carte__resume::-webkit-details-marker { display: none; }
.carte__resume::marker { content: ''; }
.carte__resume-signe::before { content: '+'; font-size: 1rem; line-height: 1; }
.carte__detail[open] .carte__resume-signe::before { content: '−'; }
.carte__resume:focus-visible { outline: 2px solid var(--color-signal); outline-offset: 2px; }
@media (hover: hover) {
  .carte__resume:hover { color: var(--color-ink); }
}
/* L'air sous le contenu déplié est un bloc et non un rembourrage : l'animation
   du lot 21 enroule le déplié jusqu'à 0 px, et un rembourrage lui laisserait
   une hauteur minimale — le dernier cran de la fermeture sauterait. Pendant
   l'animation (est-anime), ce qui dépasse est coupé. */
.carte__deplie::after { content: ''; display: block; height: var(--space-2); }
.carte__detail.est-anime > .carte__deplie { overflow: hidden; }

/* Ce qui est inclus : une ligne par élément, un filet fin au-dessus de
   chacune. */
.carte__inclus li {
  padding-block: 0.5rem;
  border-top: 1px solid var(--color-line);
  font-size: 0.9375rem;
  line-height: 1.4;
}
/* Le pied : supplément et variantes, en gris et un cran plus petit, sous un
   filet. */
.carte__pied {
  margin-top: var(--space-2);
  padding-top: var(--space-2);
  border-top: 1px solid var(--color-line);
  color: var(--color-muted);
  font-size: 0.875rem;
  line-height: 1.45;
}
.carte__pied > * + * { margin-top: 0.35rem; }
.carte__variantes li + li { margin-top: 0.2rem; }

/* LA BARRE, jumelle de celles de l'accueil (.barre) : même hauteur, même
   filet, même ton. Elle touche les bords de la carte. */
/* Le mot de la barre part du même bord que le texte de la carte. */
.carte__barre { padding-inline: var(--carte-pad); text-decoration: none; cursor: pointer; }
.carte__barre:focus-visible { outline-offset: -2px; }
@media (hover: hover) {
  .carte__barre:hover { border-top-color: var(--color-ink); }
}
/* SÉLECTIONNÉE : un filet à l'encre sur tout le pourtour. La bordure existe
   déjà, seule sa couleur change — rien ne bouge d'un pixel. */
.carte.est-choisie { border-color: var(--color-ink); }

/* LA CARTE LARGE — le pack international. Elle tient toute la rangée et ne
   partage pas les lignes de ses voisines : elle n'en a pas. Sa photo est un
   BANDEAU bord à bord, en tête ; tout le reste garde la marge de la carte. */
/* LOT 21 — LA MÊME ANATOMIE QU'UNE CARTE COMPACTE : photo, tête, « Voir le
   détail », barre bord à bord. Volets et formules sont dans le détail, sous
   la suite de l'accroche. */
.carte--large {
  grid-row: auto;
  display: block;
  padding: 0;
}
@media (min-width: 768px) {
  .carte--large { --carte-pad: var(--space-4); }
}
.carte.carte--large > :not(.carte__photo):not(.carte__barre) { margin-inline: var(--carte-pad); }
.carte--large > .carte__tete { padding-top: var(--carte-pad); }
.carte--large > .carte__barre { display: flex; }
.carte__suite { color: var(--color-muted); font-size: 0.9375rem; line-height: 1.45; max-width: 62ch; padding-top: var(--space-2); }
.carte__tete { display: grid; gap: var(--space-2); }
.carte__tete .carte__accroche { max-width: 62ch; }
.carte__tete .carte__prix { margin-top: var(--space-2); }
@media (min-width: 768px) {
  /* Le prix passe à droite du titre, calé sur le bas de l'accroche. */
  .carte__tete { grid-template-columns: minmax(0, 1fr) auto; column-gap: var(--space-5); }
  .carte__tete > :not(.carte__prix) { grid-column: 1; }
  .carte__tete .carte__prix { grid-column: 2; grid-row: 1 / span 2; align-self: end; justify-content: flex-end; margin-top: 0; }
}
.carte__bloc { margin-top: var(--space-5); }
.carte__intertitre { margin-bottom: var(--space-3); }
/* LES VOLETS côte à côte à partir de 768 px, empilés dessous. */
.carte__volets { display: grid; gap: var(--space-4); }
@media (min-width: 768px) {
  .carte__volets { grid-template-columns: repeat(3, minmax(0, 1fr)); }
}
.volet__titre { margin-bottom: var(--space-2); color: var(--color-ink); }
/* Les sous-cartes sont des cartes compactes : le même dessin, un étage plus
   bas, photo facultative. La dernière rangée n'a pas besoin de sa marge — le
   bord de la carte large est juste en dessous.
   ELLES NE PARTAGENT PAS LEURS LIGNES. Les trois formules ne se ressemblent
   pas : deux ont un prix et pas d'accroche, la troisième une accroche de
   quatre lignes et pas de prix. Alignées, les deux premières ouvraient un
   trou entre leur titre et leur prix. Chacune se range donc pour elle-même,
   et seules les barres ferment ensemble, poussées en bas.
   LEUR RÉSERVE (lot 17). La grille ne les étire plus : pour que les barres
   ferment ensemble quand les trois formules partagent une rangée (1200 px et
   plus), chacune garde la hauteur de la plus haute fermée — Essentiel et
   Premium, 288 à 291 px mesurés de 1200 à 2400 ; Sur mesure en fait 233.
   Entre 768 et 1199 elles vont par deux, et les deux premières sont de même
   facture : rien à réserver. */
.carte--large .tarifs__grille { margin-bottom: calc(-1 * var(--space-3)); }
.carte--large .carte {
  display: flex;
  flex-direction: column;
  grid-row: auto;
}
@media (min-width: 1200px) {
  .carte--large .carte { min-height: 18.25rem; }
}
/* Les calages de la grille partagée (start, end) n'ont plus de sens en
   colonne : ils y réduiraient chaque partie à la largeur de son texte. */
.carte--large .carte > * { align-self: stretch; }
.carte--large .carte > .carte__barre { margin-top: auto; }
.carte--large .carte > :nth-last-child(2) { margin-bottom: var(--space-3); }

/* La mention des prix, sous les grilles. */
.tarifs__mention { padding-top: var(--space-3); }
.tarifs__note { color: var(--color-muted); font-size: 0.875rem; }
.tarifs__demande { margin-top: var(--section-y); border-top: 1px solid var(--color-line); }

/* --- Les fonds de photo de tarifs.html (lot 16) --------------------------
   La tête de page et la section de demande sont posées sur une photo sous
   voile : le même objet que les panneaux de l'accueil — l'image en cover sur
   toute la surface, l'aplat --voile par-dessus (tokens.css), aucun filtre,
   aucun dégradé, aucun texte sur la photo elle-même. Le texte est sur le
   voile, comme le titre du panneau de devis de l'accueil. */
.fond { position: absolute; inset: 0; z-index: -1; background: var(--color-dark); }
.fond__img { display: block; width: 100%; height: 100%; object-fit: cover; object-position: 50% 50%; }
.fond[data-focal="haut"] .fond__img { object-position: 50% 25%; }
.fond::after {
  content: '';
  position: absolute; inset: 0;
  background: var(--voile-teinte);
  opacity: var(--voile);
}

/* LA TÊTE SUR PHOTO : près des deux tiers de l'écran (62svh, plancher
   420 px — lot 17 ; 50svh et 360 px au lot 16, où l'image paraissait coupée),
   le texte calé en bas, et un titre à l'échelle d'une ouverture de section.
   Les deux directions posent chacune leur marge de tête (html[data-theme]
   .tete) : la règle les nomme toutes les deux pour passer devant sans
   dépendre de l'ordre de la feuille. */
.tete--fond {
  position: relative;
  isolation: isolate;
  display: grid;
  align-items: end;
  min-height: max(62svh, 420px);
}
html[data-theme] .tete.tete--fond { padding-block: var(--space-6) var(--space-5); }
html[data-theme] .tete--fond .tete__nom { font-size: var(--step-4); }
html[data-theme] .tete--fond .tete__sous { color: var(--color-ink); }

/* LA DEMANDE SUR PHOTO : bord à bord, sans filet — c'est la photo qui dit
   qu'on a changé de partie —, et le formulaire dans SA CARTE, à la surface
   des cartes de prix, pour qu'il se lise sur l'image. */
.tarifs__demande--fond { position: relative; isolation: isolate; border-top: 0; }
.tarifs__demande .devis {
  background: var(--color-surface-carte);
  border: 1px solid var(--color-line);
  padding: var(--space-4) var(--space-3);
}
@media (min-width: 768px) {
  .tarifs__demande .devis { padding: var(--space-5); }
}

/* --- LES CARTES « PAPIER » (lot 17) ----------------------------------------
   Les cartes de prestations et la carte du formulaire passent sur une surface
   blanc cassé, à l'encre noire, sur la page qui reste sombre : les titres de
   section, la ligne de consigne, la mention des prix et les deux fonds sous
   voile ne bougent pas.
   TOUT TIENT DANS UNE CLASSE, tarifs--papier, posée sur le <body> de
   _src/tarifs.html. La retirer rend le lot 16 à l'identique : aucune règle
   ci-dessous ne s'applique sans elle.
   COMMENT : la carte REDÉFINIT pour elle-même les rôles de couleur du thème —
   l'encre, le gris secondaire, le filet, la surface d'un champ, le signal du
   focus, l'accent du bouton. Tout ce qu'elle contient les lit déjà : le prix,
   l'accroche, les filets de la liste, le détail, les champs, l'anneau de
   focus suivent d'eux-mêmes, sans une règle de plus. Restent écrites ici les
   seules choses qui ne sont pas une simple inversion : la barre noire, le
   « Voir le détail » et le préfixe du prix en noir plutôt qu'en gris, le
   filet de 2 px de la carte choisie, la bordure noire des champs et des cases,
   et le rendu clair des cases à cocher.
   Contrastes mesurés sur la page (README, « La page Tarifs ») : l'encre fait
   17:1 sur le papier, le gris secondaire 5,9:1, et 6,7:1 sur le blanc d'un
   champ. Les cibles de 44 et 48 px ne changent pas. */
.tarifs--papier .carte,
.tarifs--papier .tarifs__demande .devis {
  --color-ink: var(--papier-encre);
  --color-muted: var(--papier-muet);
  --color-line: var(--papier-filet);
  --color-surface: var(--papier-champ);
  --color-surface-carte: var(--papier-fond);
  --color-signal: var(--papier-encre);
  --color-accent: var(--papier-encre);
  --color-accent-ink: var(--papier-fond);
  color: var(--papier-encre);
}
/* « Voir le détail » et le prix, préfixe compris, en noir. */
.tarifs--papier .carte__resume,
.tarifs--papier .carte__prefixe { color: var(--papier-encre); }
/* LA BARRE INVERSÉE : noire, texte blanc cassé, dans les deux états — « Ajouter
   à ma demande + » comme « Sélectionnée ✓ ». Son anneau de focus est clair et
   posé DEDANS : un anneau noir sur une barre noire ne se verrait pas. */
.tarifs--papier .carte__barre {
  background: var(--papier-encre);
  color: var(--papier-fond);
  border-top-color: var(--papier-encre);
}
.tarifs--papier .carte__barre:focus-visible { outline: 2px solid var(--papier-fond); outline-offset: -4px; }
/* CHOISIE : le filet de la carte passe à l'encre et s'épaissit à 2 px — la
   bordure d'1 px plus une ombre pleine d'1 px autour, pour que rien ne bouge
   d'un pixel. */
.tarifs--papier .carte.est-choisie { box-shadow: 0 0 0 1px var(--papier-encre); }
/* LES CHAMPS : sur blanc (la surface redéfinie), bordure noire fine. Les cases
   aussi, et une case cochée prend un filet de 2 px, comme une carte choisie. */
.tarifs--papier .devis .field input,
.tarifs--papier .devis .field textarea { border-color: var(--papier-encre); }
.tarifs--papier .devis .choix__option {
  background: var(--papier-champ);
  border-color: var(--papier-encre);
}
.tarifs--papier .devis .choix__option:has(input:checked) { box-shadow: inset 0 0 0 1px var(--papier-encre); }
/* Les cases sont dessinées par le navigateur : sur le papier, en clair. */
.tarifs--papier .tarifs__demande .choix { color-scheme: light; }

/* --- La barre fixe de la demande ------------------------------------------
   Construite par active(), montrée dès qu'une prestation est cochée, effacée
   tant que le formulaire est à l'écran. Posée en bas, sur le fond des cartes,
   un filet en tête : c'est une barre de plus, à l'échelle de la page. Elle
   APPARAÎT SANS FONDU — les fondus de la direction sont comptés, et celui-ci
   n'en fait pas partie. */
.envoi {
  position: fixed;
  inset: auto 0 0 0;
  z-index: 40;
  background: var(--color-surface);
  border-top: 1px solid var(--color-line);
  padding-bottom: env(safe-area-inset-bottom, 0px);
}
.envoi__barre {
  width: min(100% - 2 * var(--gutter), var(--container));
  min-height: 56px;
  margin: 0 auto;
  padding-inline: 0;
  border-top: 0;
  background: transparent;
  text-decoration: none;
  font-size: 0.9375rem;
}
.envoi__barre .barre__action { font-weight: 600; white-space: nowrap; }
/* Le compte ne se coupe JAMAIS en points de suspension, contrairement au
   libellé d'une barre d'accueil : c'est lui l'information. Sur un téléphone
   il descend d'un cran et l'écart se resserre, ce qui le tient sur une ligne
   à 390 px ; plus étroit encore, il passe à la ligne. */
.envoi__barre .barre__libelle { white-space: normal; overflow: visible; text-overflow: clip; }
@media (max-width: 479.98px) {
  .envoi__barre { gap: var(--space-2); font-size: 0.875rem; }
}
.envoi__barre:focus-visible { outline-offset: -2px; }
@media (hover: hover) {
  .envoi__barre:hover .barre__signe { transform: translateX(4px); }
}
/* Tant que la barre est là, le défilement au clavier et aux ancres garde de
   quoi ne pas cacher sous elle l'élément qu'on vient d'atteindre. */
html:has(.envoi:not([hidden])) { scroll-padding-bottom: 5rem; }
@media print {
  .envoi { display: none !important; }
}

/* --- Pied de page --------------------------------------------------------- */
/* Une coordonnée encore en PLACEHOLDER est un <span>, pas un <a> : le socle ne
   met en bloc que les <a>. */
.footer__grid .stack > * { display: block; }
/* Même raison qu'en navigation : à 40 px de haut, seul le monogramme tient. */
.pied__logo { height: 40px; width: auto; }

/* --- Valeur encore absente de data/site.json ------------------------------ */
.a-completer {
  text-decoration: underline dotted;
  text-decoration-color: var(--color-signal);
  text-underline-offset: 0.25em;
}

/* --- Sans JavaScript ------------------------------------------------------ */
.noscript { display: block; padding: var(--space-5) var(--gutter); border-bottom: 1px solid var(--color-line); }
.noscript p + p { margin-top: var(--space-2); }

/* --- Message d'erreur de chargement --------------------------------------- */
.panne { padding: var(--section-y) var(--gutter); }


/* ==========================================================================
   2. Visionneuse plein écran (maison, aucune bibliothèque)
   ========================================================================== */
.visionneuse {
  position: fixed; inset: 0; z-index: 100;
  display: grid; grid-template-rows: auto minmax(0, 1fr) auto;
  gap: var(--space-2);
  padding: var(--space-2) var(--gutter) var(--space-3);
  background: var(--color-bg);
  opacity: 0;
  /* DEUX DURÉES, ET C'EST VOULU : 0.35 s à l'ouverture, 0.25 s à la fermeture.
     Une visionneuse qui s'ouvre est un geste — on veut le voir ; une
     visionneuse qui se ferme est un retour — on veut qu'il soit fait. La durée
     d'ouverture est donc portée par l'état ouvert lui-même : la classe posée,
     c'est 0.35 s ; la classe retirée, l'élément retrouve les 0.25 s de sa
     règle de base. Rien à faire en JavaScript.
     Le minuteur de fermeVisionneuse() (280 ms) couvre la fermeture. */
  transition: opacity 0.25s var(--ease);
}
.visionneuse.est-ouverte { opacity: 1; transition-duration: 0.35s; }
.visionneuse__barre, .visionneuse__pied { display: flex; align-items: center; justify-content: space-between; gap: var(--space-3); }
.visionneuse__compte, .visionneuse__bouton {
  font-family: var(--font-label); font-size: 0.74rem;
  letter-spacing: var(--tracking-label); text-transform: uppercase;
  color: var(--color-muted);
}
.visionneuse__bouton { padding: 0.6em 0; transition: color var(--dur-fast); }
.visionneuse__bouton:hover { color: var(--color-signal); }
/* L'échelle porte sur la SCÈNE, pas sur la visionneuse entière : mettre à
   l'échelle un panneau opaque plein écran en découvrirait les bords.
   0.9 à 1 — un dixième. À deux pour cent, l'image ne venait pas vers soi, elle
   frémissait : on ne voyait pas d'où elle sortait, seulement qu'elle était là.
   Un dixième donne le mouvement qu'on attend d'un agrandissement, et reste
   très en deçà d'un effet. */
.visionneuse__scene {
  display: grid; grid-template-rows: minmax(0, 1fr) auto;
  place-items: center; min-height: 0; margin: 0; gap: var(--space-2);
  transform: scale(0.9);
  transition: transform 0.25s var(--ease);
}
.visionneuse.est-ouverte .visionneuse__scene { transform: none; transition-duration: 0.35s; }
@media (prefers-reduced-motion: reduce) {
  .visionneuse, .visionneuse__scene { transition: none; }
  .visionneuse__scene { transform: none; }
}
.visionneuse__img {
  max-width: 100%; max-height: 100%;
  width: auto; height: auto;
  object-fit: contain;
}
.visionneuse__legende {
  display: flex; flex-wrap: wrap; gap: 0.4rem var(--space-3); align-items: baseline;
  font-family: var(--font-label); font-size: 0.74rem;
  letter-spacing: var(--tracking-label); text-transform: uppercase;
  color: var(--color-muted);
}
.visionneuse__titre { color: var(--color-ink); }
.visionneuse__num { font-variant-numeric: tabular-nums; }
.visionneuse__nav { display: flex; gap: var(--space-4); }


/* ==========================================================================
   A — html[data-theme="planche"]   « PLANCHE CONTACT »
   Papier, mur dense de tirages numérotés, aucune animation sur le mur : il
   doit se lire d'un seul coup d'oeil.
   ========================================================================== */

html[data-theme="planche"] .nav { height: var(--nav-height); }
html[data-theme="planche"] .nav__brand { font-weight: 700; font-size: 1.05rem; letter-spacing: 0.01em; }
html[data-theme="planche"] .nav__links a { font-family: var(--font-display); font-size: 0.86rem; font-weight: 500; opacity: 1; }
html[data-theme="planche"] .nav__links a:hover { color: var(--color-signal); }

/* Hero « modèle 4 » : le nom, une ligne, et la planche commence tout de suite. */
html[data-theme="planche"] .tete { padding-block: clamp(2rem, 5vw, 3.5rem) 0; }
html[data-theme="planche"] .tete__nom { font-weight: 700; }
html[data-theme="planche"] .tete__accroche { font-family: var(--font-text); }
html[data-theme="planche"] .bandeau { padding-block: clamp(2rem, 5vw, 3.5rem); margin-top: var(--section-y); }

/* Le mur se remplit LIGNE PAR LIGNE : 001, 002, 003, 004 de gauche à droite,
   puis on passe à la ligne suivante. Les numéros sont l'élément fort de la
   page, ils doivent se suivre.
   La ligne prend donc la hauteur de sa pièce la plus haute, et les pièces se
   calent en haut de leur ligne (align-items: start). Une horizontale posée à
   côté de verticales laisse du papier sous sa légende : c'est le prix de
   l'ordre, et rien ne vient le combler. */
html[data-theme="planche"] .planche {
  display: grid;
  grid-template-columns: repeat(2, minmax(0, 1fr));
  gap: var(--planche-gap-x);
  align-items: start;
}
@media (min-width: 768px)  { html[data-theme="planche"] .planche { grid-template-columns: repeat(3, minmax(0, 1fr)); } }
@media (min-width: 1200px) { html[data-theme="planche"] .planche { grid-template-columns: repeat(4, minmax(0, 1fr)); } }

/* Le tirage est posé sur le papier : ni filet, ni fond, ni proportion imposée.
   L'image occupe toute la largeur de sa colonne et garde sa hauteur naturelle.
   Les attributs width/height du <img> réservent la place : aucun saut de page. */
html[data-theme="planche"] .planche .piece .piece__cadre {
  aspect-ratio: auto;
  border: 0;
  background: none;
}
html[data-theme="planche"] .planche .piece .piece__img {
  width: 100%; height: auto; object-fit: fill;
}
/* Un fichier encore absent de Cloudinary garde son filet, pour qu'on voie le
   trou pendant l'atelier (sa place est déjà réservée par les width/height du
   <img> resté en visibility:hidden). */
html[data-theme="planche"] .planche .piece .piece__cadre.est-absente { border: 1px solid var(--color-line); }

/* Une couverture vidéo se comporte comme un tirage : son ratio réel, aucune
   bande, aucun letterbox. Seul le lecteur YouTube, qui n'a pas d'image pour
   lui donner sa hauteur, reprend le format de la pièce. */
html[data-theme="planche"] .planche .piece .piece__cadre--lecteur { aspect-ratio: 16 / 9; }
html[data-theme="planche"] .planche .piece[data-format="vertical"] .piece__cadre--lecteur { aspect-ratio: 9 / 16; }

/* ------------------------------------------------------------------------
   videos.html : une LISTE, pas un mur.
   Sept vidéos de deux formats opposés ne tiennent pas dans une grille sans
   creuser des trous. Ici une vidéo = une ligne, sur douze colonnes : le média
   d'un côté, sa légende de l'autre, alignée en haut. Chaque ligne est
   autonome, donc aucun trou n'est possible.
   ------------------------------------------------------------------------ */
html[data-theme="planche"] [data-page="videos"] .planche {
  grid-template-columns: minmax(0, 1fr);
  gap: clamp(var(--space-5), 6vw, var(--space-7));
}
/* Sous 768 px la ligne s'empile : le média pleine largeur, la légende dessous
   — c'est déjà le comportement d'une colonne unique. */
@media (min-width: 768px) {
  html[data-theme="planche"] [data-page="videos"] .piece {
    display: grid;
    grid-template-columns: repeat(12, minmax(0, 1fr));
    column-gap: var(--planche-gap-x);
    align-items: start;
  }
  /* Une horizontale est large : elle prend huit colonnes, la légende quatre.
     Une verticale est haute et étroite : quatre colonnes lui suffisent, et la
     légende récupère les huit autres. */
  html[data-theme="planche"] [data-page="videos"] .piece[data-format="horizontal"] .piece__facade  { grid-column: span 8; }
  html[data-theme="planche"] [data-page="videos"] .piece[data-format="horizontal"] .piece__legende { grid-column: span 4; }
  html[data-theme="planche"] [data-page="videos"] .piece[data-format="vertical"]   .piece__facade  { grid-column: span 4; }
  html[data-theme="planche"] [data-page="videos"] .piece[data-format="vertical"]   .piece__legende { grid-column: span 8; }
  /* Le filet de la légende s'aligne alors sur le haut du média, pas 8 px plus bas. */
  html[data-theme="planche"] [data-page="videos"] .piece__legende { margin-top: 0; }
}

/* Légende sous le tirage. Le NUMÉRO DE VUE est la signature d'une planche
   contact : c'est lui qu'on doit voir en premier en balayant la page. Il passe
   donc en gros, en chiffres tabulaires et à l'encre ; le titre et la catégorie
   restent petits et discrets en dessous. */
html[data-theme="planche"] .piece__legende {
  margin-top: 0.5rem; padding-top: 0.4rem;
  border-top: 1px solid var(--color-line);
  font-family: var(--font-display);
  font-size: 0.72rem; line-height: 1.35;
}
html[data-theme="planche"] .piece__num {
  font-size: 1.5rem; line-height: 1.05; font-weight: 500;
  letter-spacing: var(--tracking-display);
  font-variant-numeric: tabular-nums;
  color: var(--color-ink);
  margin-bottom: 0.2rem;
}
html[data-theme="planche"] .piece__titre { font-weight: 500; }
html[data-theme="planche"] .piece__meta { color: var(--color-muted); }
html[data-theme="planche"] .piece__ouvrir:hover .piece__titre,
html[data-theme="planche"] .piece__ouvrir:focus-visible .piece__titre { color: var(--color-signal); }

/* Le trait de feutre : dans un mur dense, une pièce plus grande casse tout. Une
   « sélection » garde donc exactement la taille des autres. Elle est seulement
   placée en tête de mur (pageAccueil / pagePhotos / pageVideos) et marquée
   au-dessus de sa légende — filet de 2 px et numéro au signal. */
html[data-theme="planche"] .piece[data-selection="true"] .piece__legende {
  border-top: 2px solid var(--color-signal);
}
html[data-theme="planche"] .piece[data-selection="true"] .piece__num { color: var(--color-signal); }

/* Aucun mouvement sur la grille (design-directions.md, direction Planche). */
html[data-theme="planche"] .piece { opacity: 1; transform: none; }

html[data-theme="planche"] .footer { font-family: var(--font-display); }


/* ==========================================================================
   B — html[data-theme="salle"]   « SALLE SOMBRE »
   Noir, aucun cadre et aucun fond autour de l'image, TITRES EN SERIF
   (Instrument Serif, une seule graisse), libellés et numéros en Archivo
   capitales, aucune couleur d'accent (lot 12). Fondu 0.4 s à l'entrée et rien d'autre. Aucune lueur, aucun verre dépoli, aucun dégradé, aucune
   ombre colorée : c'est ce refus-là qui sépare cette direction du lot des
   sites noirs de vidéastes.

   Le mur est bâti EXACTEMENT comme celui de la planche — même grille à lignes
   automatiques, mêmes numéros qui se suivent de gauche à droite, même liste
   sur videos.html, même triangle de lecture, mêmes filtres, même visionneuse.
   (Ce mur-là est celui de photos.html : depuis le lot 8, l'accueil n'en a plus.)
   TROIS ÉCARTS seulement, assumés, et signalés « ÉCART » ci-dessous : ses
   photos ont des fonds bleus clairs qui se perdent sur du noir, il leur faut
   donc plus de place et moins de vide autour.
   ========================================================================== */

/* UNE SEULE GRAISSE. Instrument Serif n'existe qu'en 400 ; partout où le socle
   demande 600 ou 700 sur un titre, le navigateur fabriquerait sinon un faux
   gras — des lettres épaissies au trait, aux empattements écrasés, qui se
   voient tout de suite à 88 px. On lui rend donc son unique graisse.
   Le texte courant, lui, garde l'Archivo variable et toutes les siennes. */
html[data-theme="salle"] h1,
html[data-theme="salle"] h2,
html[data-theme="salle"] h3,
html[data-theme="salle"] h4,
html[data-theme="salle"] .h1,
html[data-theme="salle"] .h2,
html[data-theme="salle"] .h3,
html[data-theme="salle"] .h4,
html[data-theme="salle"] .display { font-weight: 400; }
/* SAUF LE SLOGAN. Il est le seul titre de la page qui ne soit pas en
   Instrument Serif : c'est la lettre de la carte de visite, en Poppins 300, et
   la règle ci-dessus le ramenait à 400. Le fichier servi étant un 300, on ne
   voyait pas la différence — le navigateur prend la face la plus proche — mais
   la feuille demandait une graisse que le site ne sert pas, et le jour où une
   seconde face de Poppins arrive, le slogan s'épaissirait tout seul. */
html[data-theme="salle"] .ecran__titre { font-weight: 300; }

html[data-theme="salle"] .nav { height: var(--nav-height); }
html[data-theme="salle"] .nav__brand { font-weight: 600; font-size: 0.95rem; letter-spacing: 0.12em; text-transform: uppercase; }
html[data-theme="salle"] .nav__links a { font-family: var(--font-label); font-size: 0.78rem; font-weight: 500; letter-spacing: var(--tracking-label); text-transform: uppercase; opacity: 1; color: var(--color-muted); }
html[data-theme="salle"] .nav__links a:hover,
html[data-theme="salle"] .nav__links a[aria-current="page"] { color: var(--color-ink); }

/* Tête de page : le texte d'abord, comme sur le papier. Jamais de grande typo
   posée par-dessus une image (direction Salle, à éviter absolument). */
html[data-theme="salle"] .tete { padding-block: clamp(2.5rem, 7vw, 5rem) 0; }
/* Le titre d'une page intérieure n'est pas un titre d'ouverture : « Photos »
   à 64 px annonçait un événement, alors qu'il ne fait qu'étiqueter une planche
   qui, elle, doit dominer. Il descend à l'échelle d'un titre de section
   (--step-3, 26 à 38 px). L'accueil garde sa propre règle plus bas. */
html[data-theme="salle"] .tete__nom { font-weight: 400; font-size: var(--step-3); }
html[data-theme="salle"] .tete__role { color: var(--color-muted); }

/* ÉCART 3 — l'accueil s'ouvre en plein cadre. Sur le papier, la bande
   d'ambiance se signalait par sa couleur ; ici tout est déjà noir, donc c'est
   sa TAILLE : le média prend l'écran entier, la navigation passe par-dessus
   sans fond ni filet, et le texte n'arrive qu'en dessous. C'est le seul
   élément du site qui déborde de la marge de page.

   Tant que la page n'a pas défilé, la navigation est transparente : elle est
   posée sur l'image, pas devant elle. Au premier défilement, activeEcran()
   n'y est pour rien — c'est activeNav() qui pose is-scrolled — et elle reprend
   le fond de la page et son filet. */
html[data-theme="salle"] body:has(main[data-page="accueil"]) .nav {
  background: transparent;
  border-bottom-color: transparent;
}
html[data-theme="salle"] body:has(main[data-page="accueil"]) .nav.is-scrolled,
html[data-theme="salle"] body:has(main[data-page="accueil"]) .nav.is-open {
  background: var(--color-bg);
}
html[data-theme="salle"] body:has(main[data-page="accueil"]) .nav.is-scrolled {
  border-bottom-color: var(--color-line);
}

/* Tant qu'elle est posée sur l'image, la navigation n'a aucun fond pour la
   porter : sur une zone claire du poster, du gris sur du blanc ne se lit plus.
   Une ombre portée courte, noire, sans flou décoratif — 1 px de décalage,
   2 px de diffusion — suffit à détacher les lettres et le monogramme sans
   poser de voile sur la photo, ce que la direction Salle interdit. Elle
   disparaît dès que la navigation reprend son fond (is-scrolled / is-open) :
   après défilement, rien ne change. */
/* L'ombre est déclarée dans les DEUX états, et c'est son ALPHA qui va de 0.6 à
   0 : « text-shadow: none » ne s'interpole pas avec une ombre, la barre aurait
   perdu son ombre d'un coup pendant que son fond, lui, arrivait en fondu. Même
   durée que le fond, 0.35 s : les trois changements — fond, filet, ombre — sont
   un seul geste. */
html[data-theme="salle"] body:has(main[data-page="accueil"]) .nav .nav__links a {
  text-shadow: 0 1px 2px rgba(0, 0, 0, 0.6);
  transition: color var(--dur-fast), text-shadow 0.35s var(--ease);
}
html[data-theme="salle"] body:has(main[data-page="accueil"]) .nav .nav__logo,
html[data-theme="salle"] body:has(main[data-page="accueil"]) .nav .icone {
  filter: drop-shadow(0 1px 2px rgba(0, 0, 0, 0.6));
  transition: filter 0.35s var(--ease);
}
html[data-theme="salle"] body:has(main[data-page="accueil"]) .nav.is-scrolled .nav__links a,
html[data-theme="salle"] body:has(main[data-page="accueil"]) .nav.is-open .nav__links a {
  text-shadow: 0 1px 2px rgba(0, 0, 0, 0);
}
html[data-theme="salle"] body:has(main[data-page="accueil"]) .nav.is-scrolled .nav__logo,
html[data-theme="salle"] body:has(main[data-page="accueil"]) .nav.is-open .nav__logo,
html[data-theme="salle"] body:has(main[data-page="accueil"]) .nav.is-scrolled .icone,
html[data-theme="salle"] body:has(main[data-page="accueil"]) .nav.is-open .icone {
  filter: drop-shadow(0 1px 2px rgba(0, 0, 0, 0));
}
@media (prefers-reduced-motion: reduce) {
  html[data-theme="salle"] body:has(main[data-page="accueil"]) .nav .nav__links a,
  html[data-theme="salle"] body:has(main[data-page="accueil"]) .nav .nav__logo,
  html[data-theme="salle"] body:has(main[data-page="accueil"]) .nav .icone { transition: none; }
}

/* L'ACCUEIL N'A PLUS DE TÊTE DE PAGE. Le nom, le métier et l'accroche
   tenaient sur deux lignes sous l'ouverture ; l'accroche est maintenant le
   titre POSÉ SUR l'image (.ecran__titre), le nom est le monogramme de la
   navigation, et le métier ne se répète plus. Les règles qui habillaient cette
   tête-là sont parties avec elle : .tete ne sert plus qu'aux pages
   intérieures, où elle n'a pas bougé. */

/* Le mur se remplit LIGNE PAR LIGNE : 001, 002, 003 de gauche à droite, puis
   la ligne suivante. Les numéros sont un élément fort de la page dans les deux
   directions, ils doivent se suivre.
   La ligne prend la hauteur de sa pièce la plus haute et les pièces se calent
   en haut de leur ligne. Une horizontale posée à côté de verticales laisse du
   noir sous sa légende : c'est le prix de l'ordre, et rien ne vient le
   combler. */
html[data-theme="salle"] .planche {
  display: grid;
  grid-template-columns: repeat(2, minmax(0, 1fr));
  gap: var(--planche-gap-y) var(--planche-gap-x);
  align-items: start;
}
/* ÉCART 1 — trois colonnes au maximum là où le papier en tient quatre : le
   palier des 1200 px n'existe pas ici. Sur du noir, une image plus petite ne
   tient pas ; il lui faut environ un tiers de largeur en plus.
   ÉCART 2 — les espacements du mur sont réduits d'environ un tiers
   (--planche-gap-x / -y, voir tokens.css) : c'est l'image qui doit dominer,
   pas le noir entre les images. */
@media (min-width: 768px) { html[data-theme="salle"] .planche { grid-template-columns: repeat(3, minmax(0, 1fr)); } }

/* L'image est posée sur le noir : ni filet, ni fond. Sur videos.html, elle
   occupe toute la largeur de sa colonne et garde sa hauteur naturelle ; les
   attributs width/height du <img> réservent la place, aucun saut de page. */
html[data-theme="salle"] .planche .piece .piece__cadre {
  aspect-ratio: auto;
  border: 0;
  background: none;
}
html[data-theme="salle"] .planche .piece .piece__img {
  width: 100%; height: auto; object-fit: fill;
}

/* ------------------------------------------------------------------------
   LE MUR — photos.html, et lui seul depuis le lot 8.
   L'accueil avait le même : c'était le mur de la sélection, et il a laissé la
   place aux trois thèmes. videos.html garde sa liste.

   Le problème : une grille de colonnes alignée en haut creuse un trou sous
   chaque pièce horizontale, parce que la ligne prend la hauteur de la plus
   haute et que rien ne vient combler le reste. Sur l'accueil, deux trous de
   400 px ; sur photos.html, un.

   La réponse : des RANGÉES À HAUTEUR COMMUNE, justifiées d'une marge à
   l'autre, dans l'ordre de lecture — 001, 002, 003 de gauche à droite, comme
   avant. Chaque pièce déclare son ratio (--r), et deux nombres en découlent :
     flex-basis = --mur-h × --r   la largeur qu'elle aurait à la hauteur visée
     flex-grow  = --r × 100       sa part de l'espace restant de la rangée
   Comme la base ET la croissance sont toutes deux proportionnelles au ratio,
   toutes les pièces d'une rangée sont mises à la MÊME hauteur, quelle que soit
   la manière dont la rangée s'étire. C'est l'algorithme d'une planche contact
   de laboratoire, et il ne laisse aucun trou.

   Le ::after final absorbe l'espace restant de la DERNIÈRE rangée : sans lui,
   trois pièces esseulées s'étireraient sur toute la largeur.
   ------------------------------------------------------------------------ */

/* LE RATIO — ce sont les proportions des FICHIERS, jamais un recadrage.
   Il est déclaré sur tout élément qui porte data-type et data-format, et non
   plus sur les seules .piece : les vignettes de la mosaïque de l'accueil les
   portent aussi, et elles ont besoin de la même chose — la forme du fichier.
   Il sert donc, selon l'endroit :
     photos.html   proportion du cadre, largeur de base et part de croissance
                   des rangées justifiées (voir plus bas) ;
     accueil       proportion du cadre d'un média de thème et de chacune des
                   quatre vignettes de son aperçu. */
html[data-theme="salle"] [data-page="accueil"] [data-type="photo"][data-format="vertical"],
html[data-theme="salle"] [data-page="photos"]  [data-type="photo"][data-format="vertical"]   { --r: 2 / 3; }
html[data-theme="salle"] [data-page="accueil"] [data-type="photo"][data-format="horizontal"],
html[data-theme="salle"] [data-page="photos"]  [data-type="photo"][data-format="horizontal"] { --r: 3 / 2; }
html[data-theme="salle"] [data-page="accueil"] [data-type="video"][data-format="vertical"],
html[data-theme="salle"] [data-page="photos"]  [data-type="video"][data-format="vertical"]   { --r: 9 / 16; }
html[data-theme="salle"] [data-page="accueil"] [data-type="video"][data-format="horizontal"],
html[data-theme="salle"] [data-page="photos"]  [data-type="video"][data-format="horizontal"] { --r: 16 / 9; }

/* Le cadre prend la proportion de la pièce et l'image le remplit. « cover »
   ne coupe rien ici : le cadre a exactement la forme du fichier. */
html[data-theme="salle"] [data-page="photos"] .planche .piece .piece__cadre { aspect-ratio: var(--r); }
html[data-theme="salle"] [data-page="photos"] .planche .piece .piece__img {
  width: 100%; height: 100%; object-fit: cover;
}

/* Sous 768 px : deux colonnes, et une horizontale les prend toutes les deux.
   Aucun trou possible — les verticales ont toutes la même proportion, et une
   horizontale n'a plus de voisine. */
html[data-theme="salle"] [data-page="photos"] .planche .piece[data-format="horizontal"] { grid-column: span 2; }

@media (min-width: 768px) {
  html[data-theme="salle"] [data-page="photos"] .planche {
    display: flex;
    flex-wrap: wrap;
    align-items: flex-start;
    gap: var(--planche-gap-y) var(--planche-gap-x);
  }
  html[data-theme="salle"] [data-page="photos"] .planche > .piece {
    flex-grow: calc(var(--r) * 100);
    flex-shrink: 1;
    flex-basis: calc(var(--mur-h) * var(--r));
  }
  /* La dernière rangée garde sa taille au lieu de s'étirer. */
  html[data-theme="salle"] [data-page="photos"] .planche::after {
    content: '';
    flex-grow: 999999;
    flex-shrink: 0;
    flex-basis: 0;
    min-width: 0;
  }
}
/* Un fichier encore absent de Cloudinary garde son filet, pour qu'on voie le
   trou pendant l'atelier. */
html[data-theme="salle"] .planche .piece .piece__cadre.est-absente { border: 1px solid var(--color-line); }

/* Seul le lecteur YouTube, qui n'a pas d'image pour lui donner sa hauteur,
   reprend le format de la pièce. */
html[data-theme="salle"] .planche .piece .piece__cadre--lecteur { aspect-ratio: 16 / 9; }
html[data-theme="salle"] .planche .piece[data-format="vertical"] .piece__cadre--lecteur { aspect-ratio: 9 / 16; }

/* ------------------------------------------------------------------------
   videos.html : une LISTE, pas un mur — même structure que sur le papier.
   Sept vidéos de deux formats opposés ne tiennent pas dans une grille sans
   creuser des trous. Une vidéo = une ligne, sur douze colonnes : le média d'un
   côté, sa légende de l'autre, alignée en haut.
   ------------------------------------------------------------------------ */
html[data-theme="salle"] [data-page="videos"] .planche {
  grid-template-columns: minmax(0, 1fr);
  /* ÉCART 2, ici aussi : la liste est serrée d'un tiers par rapport au papier. */
  gap: clamp(var(--space-4), 4vw, var(--space-6));
}
@media (min-width: 768px) {
  html[data-theme="salle"] [data-page="videos"] .piece {
    display: grid;
    grid-template-columns: repeat(12, minmax(0, 1fr));
    column-gap: var(--planche-gap-x);
    align-items: start;
  }
  /* Une horizontale est large : huit colonnes, la légende quatre. Une
     verticale est haute et étroite : quatre colonnes lui suffisent, et la
     légende récupère les huit autres. */
  html[data-theme="salle"] [data-page="videos"] .piece[data-format="horizontal"] .piece__facade  { grid-column: span 8; }
  html[data-theme="salle"] [data-page="videos"] .piece[data-format="horizontal"] .piece__legende { grid-column: span 4; }
  html[data-theme="salle"] [data-page="videos"] .piece[data-format="vertical"]   .piece__facade  { grid-column: span 4; }
  html[data-theme="salle"] [data-page="videos"] .piece[data-format="vertical"]   .piece__legende { grid-column: span 8; }
  html[data-theme="salle"] [data-page="videos"] .piece__legende { margin-top: 0; }

  /* LA LISTE ALTERNE : média à gauche, puis média à droite, et ainsi de suite.
     Sept vidéos toutes cadrées du même côté se lisaient comme un tableau ;
     alternées, l'œil redescend en zigzag et chaque ligne redevient une pièce.
     C'est un « order » sur la façade — le HTML ne bouge pas, et sous 768 px la
     liste reste empilée, donc la règle ne s'y applique pas.

     Deux écritures, et la seconde n'est pas un doublon : « nth-child(even) »
     compte TOUTES les pièces, y compris celles qu'un filtre vient de masquer,
     et deux médias pouvaient se retrouver du même côté. « of :not([hidden]) »
     ne compte que les visibles, ce qui est le seul comptage qui ait un sens à
     l'œil. Un navigateur qui ne connaît pas cette syntaxe ignore la règle et
     garde la première : il alterne, simplement sans tenir compte des filtres. */
  html[data-theme="salle"] [data-page="videos"] .piece:nth-child(even) .piece__facade { order: 2; }
  html[data-theme="salle"] [data-page="videos"] .piece:nth-child(odd of :not([hidden])) .piece__facade { order: 0; }
  html[data-theme="salle"] [data-page="videos"] .piece:nth-child(even of :not([hidden])) .piece__facade { order: 2; }
}

/* Légende sous l'image, jamais dessus. Le NUMÉRO DE VUE pèse ici exactement ce
   qu'il pèse sur le papier — même corps, même graisse, même encre. Il était
   écrit en JetBrains Mono, la signature de la direction ; depuis le lot 8 c'est
   Archivo, comme tous les libellés, et la signature est passée au TITRE, qui
   est en serif. Le titre d'une pièce reprend d'ailleurs cette police-là, en bas
   de casse, pour ne pas se confondre avec les libellés qui l'entourent. */
html[data-theme="salle"] .piece__legende {
  display: grid;
  gap: 0.1rem;
  margin-top: 0.5rem;
  padding-top: 0.4rem;
  padding-inline: 0;
  border-top: 1px solid var(--color-line);
  font-family: var(--font-mono);
  /* 0.68rem donnait 10,88 px : du mono capitale interlettré à ce corps-là ne se
     lit pas, il se devine. 12 px est le plancher tenable pour cette fonte. */
  font-size: 0.75rem; line-height: 1.5;
  letter-spacing: var(--tracking-label);
  text-transform: uppercase;
  color: var(--color-muted);
}
/* Le numéro est au repos en gris et passe à l'encre au survol de sa pièce.
   C'est le SEUL geste du mur sous la souris : pas de zoom, pas de voile, pas de
   recadrage — le mur ne bouge pas quand on le traverse, et ce léger
   raffermissement suffit à dire quelle vue est sous le curseur. Depuis que la
   légende d'une photo se réduit à ce numéro, c'est aussi le seul endroit où le
   mur pouvait répondre. */
html[data-theme="salle"] .piece__num {
  font-family: var(--font-mono);
  font-size: 1.5rem; line-height: 1.05; font-weight: 500;
  /* L'interlettrage de 0.14em des libellés étire trop un nombre de ce corps :
     on en garde l'esprit, pas la valeur de légende. */
  letter-spacing: 0.06em;
  font-variant-numeric: tabular-nums;
  color: var(--color-muted);
  margin-bottom: 0.2rem;
  transition: color var(--dur-fast) var(--ease);
}
/* Au clavier, la même réponse, sur tous les appareils. */
html[data-theme="salle"] .piece:focus-within .piece__num { color: var(--color-ink); }
/* Un écran tactile n'a pas de survol : la règle ne s'y applique pas, sinon le
   numéro resterait allumé après le premier effleurement. */
@media (hover: hover) {
  html[data-theme="salle"] .piece:hover .piece__num { color: var(--color-ink); }
}
@media (prefers-reduced-motion: reduce) {
  html[data-theme="salle"] .piece__num { transition: none; }
}
/* Le titre d'une pièce reste dans la police des TITRES — c'est ce qui le
   sépare des libellés qui l'entourent. Elle est maintenant serif : à 0.85rem,
   ses déliés disparaissaient sur le noir. Un cran plus haut, et l'interlignage
   avec lui. */
html[data-theme="salle"] .piece__titre {
  font-family: var(--font-display); font-weight: 400;
  font-size: 1rem; line-height: 1.3; letter-spacing: 0;
  text-transform: none;
  color: var(--color-ink);
}
html[data-theme="salle"] .piece__meta { color: var(--color-muted); }
/* Sous 768 px le mur passe à deux colonnes : « PORTRAIT · STUDIO · 2025 » ne
   tenait plus sur une ligne et se cassait après un séparateur, ce qui donnait
   une légende à deux hauteurs selon les pièces. Un cran de corps et un cran
   d'interlettrage en moins la font rentrer, et la ligne reste insécable au
   milieu d'un mot — la coupure, s'il en faut une, tombe sur une espace de
   séparateur. Le titre et le numéro ne bougent pas. */
@media (max-width: 767.98px) {
  html[data-theme="salle"] .piece__meta {
    font-size: 0.62rem;
    letter-spacing: 0.08em;
  }
}
/* Les filtres et le compteur sont des COMMANDES, pas des légendes : à 12 px ils
   se perdaient dans le haut de la page et il fallait viser pour cliquer. Un cran
   au-dessus de la légende de pièce, et une cible verticale de 40 px environ. Le
   filet reste collé au mot — c'est lui qui dit lequel est actif — donc le
   rembourrage est vertical seulement. */
/* Les filtres et le compteur sont des libellés du site : Archivo capitale,
   0.12em, 12-13 px. C'est le REMBOURRAGE, et non le corps, qui leur donne
   leur cible de 40 px — Archivo est plus ramassée que le mono qu'elle
   remplace, et à 0.88rem ces mots-là faisaient une seconde ligne de titres. */
html[data-theme="salle"] .filtre {
  font-size: 0.8rem;
  padding: 0.75em 0;
}
html[data-theme="salle"] .filtres__compte { font-size: 0.78rem; }
/* Le repère de sélection. Comme sur le papier, une sélection n'est pas plus
   grande : elle est seulement en tête de mur et marquée au-dessus de sa
   légende. Le filet était teal jusqu'au lot 12 ; il est maintenant à l'encre —
   --color-signal, qui ne porte plus de couleur dans cette direction. */
html[data-theme="salle"] .piece[data-selection="true"] .piece__legende {
  border-top: 2px solid var(--color-signal);
}
/* Le numéro NE porte plus le teal de la sélection : c'est maintenant lui qui
   répond au survol (gris → encre), et une sélection déjà teale n'aurait rien eu
   à répondre — sur l'accueil, où les huit pièces sont des sélections, le mur
   serait resté muet sous la souris. Le filet de 2 px au-dessus de la légende
   reste le repère, comme il l'est sur le papier. */

/* Façade vidéo : depuis le lot 20, la commande de lecture est la couverture
   elle-même, avec son bouton rond au centre (§ 1, « La couverture qui lit »),
   à la demande du client — le bouton posé sous l'image était à aller
   chercher. Un disque noir translucide sans flou n'est pas du verre dépoli. */

/* L'IMAGE entre en fondu quand elle est DÉCODÉE — c'est activeCadres() qui
   pose est-chargee sur son cadre, à l'événement load. Ce fondu-là n'est pas
   celui de la pièce juste en dessous : au premier écran les deux se
   superposent, mais plus bas dans la page la pièce est vue bien avant que son
   image en loading="lazy" n'arrive, et c'est ce fondu-ci qui évite le
   surgissement. Aucun flou, aucune image d'attente, aucun carré de couleur
   moyenne : sa colorimétrie est sa signature, on ne montre pas une
   approximation de ses photos.
   La règle est conditionnée à :not(.no-js) — c'est galerie.js qui retire cette
   classe, et sans lui les images resteraient invisibles pour toujours. Elle ne
   vaut que dans le MUR de photos.html : la liste de videos.html et les cadres
   de thème de l'accueil gardent le seul fondu de pièce. */
html[data-theme="salle"]:not(.no-js) [data-page="photos"] .planche .piece__img {
  opacity: 0;
  transition: opacity var(--dur) var(--ease);
}
html[data-theme="salle"] [data-page="photos"] .planche .piece__cadre.est-chargee .piece__img {
  opacity: 1;
}
@media (prefers-reduced-motion: reduce) {
  html[data-theme="salle"] [data-page="photos"] .planche .piece__img { opacity: 1; transition: none; }
}

/* Le seul mouvement de la pièce : elle monte de --reveal-y et se découvre en
   entrant dans l'écran, en --reveal-dur (0.6 s).

   ET LES PIÈCES D'UNE RANGÉE NE PARTENT PAS ENSEMBLE. Elles entrent dans
   l'écran au même instant — c'est ce qu'est une rangée — et apparaissaient
   donc d'un bloc : on voyait la bande, pas le mouvement. --reveal-pas (60 ms)
   les décale de gauche à droite, dans l'ordre de lecture. Le rang d'une pièce
   dans sa rangée est MESURÉ par galerie.js (poseDecalages) et écrit en
   data-decalage : la rangée est une donnée de mise en page, elle dépend de la
   largeur de la fenêtre, et le HTML livré ne peut pas la connaître.
   Sept paliers, comme dans galerie.js : au-delà, deux pièces partent ensemble
   en bout de rangée — mieux vaut cela qu'une rangée étalée sur une seconde.
   Pas de style inline pour porter le rang : la CSP l'interdit. */
html[data-theme="salle"] .piece {
  opacity: 0; transform: translateY(var(--reveal-y));
  transition: opacity var(--reveal-dur) var(--ease), transform var(--reveal-dur) var(--ease);
}
html[data-theme="salle"] .piece.is-vu { opacity: 1; transform: none; }
html[data-theme="salle"] .piece[data-decalage="1"] { transition-delay: calc(1 * var(--reveal-pas)); }
html[data-theme="salle"] .piece[data-decalage="2"] { transition-delay: calc(2 * var(--reveal-pas)); }
html[data-theme="salle"] .piece[data-decalage="3"] { transition-delay: calc(3 * var(--reveal-pas)); }
html[data-theme="salle"] .piece[data-decalage="4"] { transition-delay: calc(4 * var(--reveal-pas)); }
html[data-theme="salle"] .piece[data-decalage="5"] { transition-delay: calc(5 * var(--reveal-pas)); }
html[data-theme="salle"] .piece[data-decalage="6"] { transition-delay: calc(6 * var(--reveal-pas)); }
html[data-theme="salle"] .piece[data-decalage="7"] { transition-delay: calc(7 * var(--reveal-pas)); }

/* --- Demande de devis --------------------------------------------------- */
/* AUCUNE COULEUR NOUVELLE. Le fond d'un champ est --color-surface et son
   filet --color-line : ce sont ceux que le socle pose déjà, et ce sont les
   valeurs du thème — il n'y a rien à redire ici. Ne reste que le FOCUS : le
   socle efface le filet du champ pour ne laisser que le liseré, ce qui ouvre
   un blanc entre les deux sur du noir. On garde le filet et le liseré
   par-dessus, tous deux à --color-signal — l'encre depuis le lot 12, le teal
   avant lui. Le bouton est le .btn du socle, tel quel : --color-accent étant
   passé à l'encre, c'est un pavé blanc à texte noir. */
html[data-theme="salle"] .devis .field input:focus-visible,
html[data-theme="salle"] .devis .field textarea:focus-visible {
  outline: 2px solid var(--color-signal);
  outline-offset: 1px;
  border-color: var(--color-signal);
}
@media (hover: hover) {
  html[data-theme="salle"] .devis .field input:hover,
  html[data-theme="salle"] .devis .field textarea:hover { border-color: var(--color-ink); }
}
/* Les cases à cocher sont dessinées par le navigateur, qui les peint en blanc
   s'il ne sait pas la page sombre : dix carrés blancs sur du noir. On le lui
   dit, pour ces cases-là seulement. Ce n'est pas une couleur, c'est un
   réglage de rendu — le papier de la direction Planche garde les siennes. */
html[data-theme="salle"] .choix { color-scheme: dark; }

/* ------------------------------------------------------------------------
   L'ACCUEIL DE LA SALLE — l'ouverture écrite et les trois thèmes.

   C'est ici que la direction consent son quatrième écart : ON ÉCRIT SUR LES
   IMAGES. Le §3 l'écartait, le client l'a demandé (référence BJ Productions),
   et la page y gagne — une ouverture muette ne disait pas ce que Noé fait, et
   trois thèmes sans titre ne sont que trois photos de plus.
   L'écart s'arrête là : aucun voile dégradé, aucune lueur, aucun verre dépoli.
   Ce qui rend le texte lisible sur une zone claire, c'est une OMBRE PORTÉE
   COURTE sur les lettres — exactement la réponse déjà retenue pour la barre de
   navigation quand elle est posée sur l'image — et, pour les thèmes seulement,
   un aplat de noir à 50 % qui baisse la photo d'un demi-ton.
   ------------------------------------------------------------------------ */

/* Le slogan est un titre d'ouverture : l'échelle --step-4 et l'interlignage
   serré des titres. Blanc franc et non l'encre du thème (#F2F0EC) : sur une
   photo, un blanc cassé prend la couleur de ce qu'il y a dessous. */
html[data-theme="salle"] .ecran__titre { line-height: var(--lh-display); }

/* L'EN-TÊTE EST DANS LE CADRE : plus d'ombre portée, plus de libellé relevé à
   l'encre. Ces deux-là ne servaient qu'à tenir sur une photo ; sur le fond
   opaque du cadre, le libellé reprend le gris de tous les autres libellés du
   site et le titre se pose simplement dessus. C'est une chose de moins qui
   ressemble à un habillage. */
html[data-theme="salle"] .theme__ligne { font-size: calc(var(--text-base) * 1.15); }

/* Le cadre d'un média de thème et celui d'une vignette prennent la proportion
   du FICHIER (--r, déclaré plus haut avec ceux du mur) : « cover » n'y coupe
   rien, le cadre a exactement la forme de l'image qu'il porte. */
html[data-theme="salle"] .theme__media .piece__cadre { aspect-ratio: var(--r); }
html[data-theme="salle"] .theme__media .piece__img { width: 100%; height: 100%; object-fit: cover; }
html[data-theme="salle"] .mosaique__vue { aspect-ratio: var(--r); }

/* Dans un cadre de thème, la pièce ne fait plus son entrée pour son compte :
   c'est le cadre entier qui monte, et elle est dedans. Deux fondus l'un sur
   l'autre auraient traîné. */
html[data-theme="salle"] .theme .piece { opacity: 1; transform: none; transition: none; }

/* L'ENTRÉE D'UN THÈME, EN DEUX TEMPS : le cadre et son en-tête, puis les deux
   colonnes 80 ms plus tard. Même geste que celui d'une pièce du mur — 16 px de
   montée, 0.6 s — et mêmes durées qu'avant ; c'est la cascade qui a changé de
   découpage, parce que la structure a changé : le titre était sur la photo et
   arrivait avant le cadre, il est maintenant DANS le cadre et arrive avec lui.
   Le panneau s'annonce d'abord (le bloc opaque et ce qu'il dit), puis se
   remplit. C'est active() (observeThemes) qui pose is-vu ; les retards sont
   ici, comme ceux du mur. */
html[data-theme="salle"] .theme__inner,
html[data-theme="salle"] .theme__media,
html[data-theme="salle"] .apercu {
  opacity: 0;
  transform: translateY(var(--reveal-y));
  transition: opacity var(--reveal-dur) var(--ease), transform var(--reveal-dur) var(--ease);
}
html[data-theme="salle"] .theme__media,
html[data-theme="salle"] .apercu { transition-delay: var(--theme-pas); }
html[data-theme="salle"] .theme.is-vu .theme__inner,
html[data-theme="salle"] .theme.is-vu .theme__media,
html[data-theme="salle"] .theme.is-vu .apercu { opacity: 1; transform: none; }

/* LE SURVOL DE L'APERÇU, et lui seul dans toute la page à toucher l'opacité
   d'une image. La règle du site est qu'aucune photo n'est retouchée en CSS —
   ni filtre, ni fondu au repos : sa colorimétrie est sa signature. Ces
   quatre-là ne sont pas des photos exposées mais l'INTÉRIEUR D'UN BOUTON, à
   400 px de large : les tenir à 0.85 au repos, c'est dire qu'elles attendent
   le clic, et les rendre pleines au survol, c'est la réponse du bouton.
   Sur un écran tactile la règle ne s'applique pas — sans curseur, elles
   resteraient ternes pour toujours.
   Le libellé, lui, ne s'inverse plus : c'est la FLÈCHE qui répond, en se
   décalant de quatre pixels vers la droite — le geste du lien qu'on suit. Un
   pavé d'accent qui devenait un pavé blanc était une réponse de bouton ; ici
   ce n'est pas un bouton, c'est une porte.
   La règle porte sur la VIGNETTE, et non plus sur son image, depuis le lot 15 :
   une vignette peut contenir une boucle posée par-dessus l'image, et les deux
   doivent passer de 0.85 à 1 ensemble. Pour une vignette sans boucle, le rendu
   est le même au pixel près — la vignette n'a ni fond ni bordure. */
@media (hover: hover) {
  html[data-theme="salle"] .apercu .mosaique__vue {
    opacity: 0.85;
    transition: opacity 0.3s var(--ease);
  }
  html[data-theme="salle"] .apercu:hover .mosaique__vue,
  html[data-theme="salle"] .apercu:focus-visible .mosaique__vue { opacity: 1; }
  html[data-theme="salle"] .apercu:hover .barre__signe,
  html[data-theme="salle"] .apercu:focus-visible .barre__signe { transform: translateX(4px); }
  /* La barre du média répond de la même façon : c'est la même barre, et une
     jumelle qui ne bougerait pas passerait pour un décor. */
  html[data-theme="salle"] .theme__barre[role="button"]:hover .barre__signe,
  html[data-theme="salle"] .theme__barre[role="button"]:focus-visible .barre__signe { transform: translateX(4px); }
}

@media (prefers-reduced-motion: reduce) {
  html[data-theme="salle"] .theme__inner,
  html[data-theme="salle"] .theme__media,
  html[data-theme="salle"] .apercu {
    opacity: 1; transform: none; transition: none; transition-delay: 0s;
  }
  html[data-theme="salle"] .apercu .mosaique__vue { transition: none; }
  html[data-theme="salle"] .barre__signe { transition: none; }
}

html[data-theme="salle"] .footer { font-family: var(--font-text); }

/* Le décalage part avec le mouvement. La seconde règle n'est pas un doublon :
   les retards sont posés par un sélecteur d'attribut, et une règle sans
   attribut leur passerait dessous quelle que soit sa place. On remet donc le
   retard à zéro à ÉGALE spécificité, plus bas dans la cascade. */
@media (prefers-reduced-motion: reduce) {
  html[data-theme="salle"] .piece { opacity: 1; transform: none; }
  html[data-theme="salle"] .piece[data-decalage] { transition-delay: 0s; }
}
