/* LES POLICES — 3 fichiers dans fonts/, 112 Ko. woff2 seul suffit :
   tous les navigateurs en service le lisent.

   font-display: swap est la ligne qui compte. Sans elle le navigateur
   MASQUE le texte tant que la police n'est pas arrivée ; avec elle il
   l'affiche aussitôt dans la police de secours puis échange. */

/* Inter est VARIABLE : un fichier contient toutes les graisses, d'où
   la plage « 400 500 » et non une valeur unique. */
@font-face {
  font-family: "Inter";
  src: url("../fonts/inter-variable.woff2") format("woff2");
  font-weight: 400 500;
  font-style: normal;
  font-display: swap;
}

@font-face {
  font-family: "Playfair Display";
  src: url("../fonts/playfair-500.woff2") format("woff2");
  font-weight: 500;
  font-style: normal;
  font-display: swap;
}

/* L'italique est un fichier à part : dans une sérif ce n'est pas la
   droite penchée, c'est un dessin entièrement différent. */
@font-face {
  font-family: "Playfair Display";
  src: url("../fonts/playfair-500-italic.woff2") format("woff2");
  font-weight: 500;
  font-style: italic;
  font-display: swap;
}

/* L'italique en graisse 400, plus fine que la 500 : pour la ligne
   « Photographie · Vidéo » sous le logo, sur ordinateur. Un fichier de
   plus (22 Ko), demandé par Benjamin après avoir vu la 500 trop
   épaisse. Le navigateur ne le télécharge que si un élément l'utilise. */
@font-face {
  font-family: "Playfair Display";
  src: url("../fonts/playfair-400-italic.woff2") format("woff2");
  font-weight: 400;
  font-style: italic;
  font-display: swap;
}


/* ============================================================
   PORTFOLIO — BENJAMIN CODONER
   Feuille de style principale.

   Les VARIABLES d'abord : écrites une fois, réutilisées partout. Le
   jour où tu changes le doré, tu modifies UNE ligne.
   ============================================================ */


/* :root = la balise <html>. Une variable déclarée ici est visible par
   toute la page. Deux tirets obligatoires : --nom, relu par var(--nom). */

:root {

  /* ---------- Couleurs. Fond sombre, accent doré. ---------- */

  /* Pas du noir pur : du texte clair dessus vibre et fatigue, et une
     photo posée sur du noir pur s'y fond, on perd ses limites. */
  --couleur-fond: #121110;

  /* À peine plus clair, pour détacher un bloc sans tracer de bordure. */
  --couleur-fond-clair: #1c1a18;

  /* Blanc cassé chaud, pas #ffffff : on évite le contraste maximal. */
  --couleur-texte: #ece7e1;

  /* Légendes, pied de page. Plus gris = moins important, et lisible
     quand même. Ne pas descendre plus bas. */
  --couleur-texte-doux: #a29a92;

  /* UNE seule couleur d'accent sur tout le site : c'est ce qui fait
     qu'un site a l'air tenu plutôt que bricolé. */
  --couleur-accent: #d9a441;

  /* La même en plus clair : le survol éclaircit, il ne change pas de
     couleur. */
  --couleur-accent-clair: #f0bf63;

  /* Très discret : une bordure trop visible découpe la page au lieu de
     l'organiser. */
  --couleur-bordure: #2e2b28;

  /* Un orangé sourd plutôt qu'un rouge vif pour les erreurs du
     formulaire : il faut qu'on le remarque, pas qu'on croie à une panne. */
  --couleur-erreur: #e0885a;

  /* LES VOILES. Des couleurs transparentes posées par-dessus autre
     chose. Elles étaient écrites en dur dans leurs règles ; en variables,
     le thème clair peut les changer d'un seul endroit. */

  /* Le voile doré léger des boutons ronds, de l'anneau du curseur et du
     bouton d'envoi : l'accent à 12 %. */
  --voile-accent: rgba(217, 164, 65, 0.12);

  /* Sur téléphone, entre les photos du carrousel et le texte de
     l'accueil. 0.71 : voir #accueil::before pour le calcul. */
  --voile-accueil: rgba(18, 17, 16, 0.71);

  /* Derrière la visionneuse des albums, sur la page floutée. */
  --voile-visionneuse: rgba(18, 17, 16, 0.86);

  /* Le logo est un fichier SVG dont le doré est écrit dedans : le CSS
     ne peut pas changer sa couleur, seulement le filtrer. En sombre,
     aucun filtre. */
  --filtre-logo: none;


  /* ---------- Typographie ----------

     Une « pile » : le navigateur essaie la première, puis la suivante.
     Ce qui suit Inter et Playfair est le filet de sécurité, le temps du
     téléchargement.

     Deux rôles. La SÉRIF aux titres : ses empattements et son contraste
     viennent de la gravure, c'est la typographie des livres, elle
     apporte l'autorité. La SANS-SERIF au texte courant, volontairement
     neutre — deux polices de caractère fort se disputeraient l'attention. */
  --police: "Inter", system-ui, -apple-system, "Segoe UI", Roboto, sans-serif;
  --police-titre: "Playfair Display", Georgia, "Times New Roman", serif;

  /* Tailles en rem. 1rem = la taille réglée dans le navigateur du
     visiteur, 16px par défaut. Si quelqu'un l'a augmentée parce qu'il
     voit mal, tout le site grandit avec ; des px écraseraient ce réglage. */

  --texte-s: 1rem;              /* pied de page, mentions */
  --texte-base: 1.1875rem;      /* 19px, le corps de texte */
  --texte-m: 1.375rem;          /* 22px, l'accroche de l'accueil */

  /* clamp(minimum, idéal, maximum). La valeur du milieu est en vw, donc
     le titre grandit avec l'écran sans jamais sortir des bornes. Une
     ligne qui remplace deux ou trois media queries. */
  --texte-l: clamp(2rem, 3.6vw, 2.875rem);

  /* Le <h1>, nettement agrandi en même temps qu'on adoptait la sérif :
     une sérif à fort contraste ne se révèle qu'en grand. Et l'écart
     entre le plus grand et le plus petit texte est en soi un signal —
     tout à la même taille fait document, un titre qui écrase fait composé. */
  --texte-xl: clamp(2.75rem, 6.5vw, 5rem);

  /* Étiquettes en capitales espacées. Minuscules exprès : leur présence
     tient à l'espacement des lettres, pas à la taille. */
  --texte-xs: 0.8125rem; /* 13px */

  /* Interlignes SANS unité : c'est un multiplicateur, donc l'écart suit
     chaque taille automatiquement. 1.6 pour le texte, de l'air pour que
     l'œil retrouve la ligne suivante. */
  --interligne-texte: 1.6;

  /* 1.15 pour les titres : trop d'écart et les lignes se désolidarisent,
     on ne lit plus un titre mais deux morceaux de phrase. */
  --interligne-titre: 1.15;


  /* ---------- Espacements ----------
     Une échelle plutôt que 13px ici et 27px là au feeling : c'est ce qui
     donne son rythme à la page. */

  --espace-xs: 0.5rem;   /*  8px — entre une image et sa légende */
  --espace-s:  1rem;     /* 16px — entre deux paragraphes */
  --espace-m:  1.5rem;   /* 24px — marges intérieures d'un bloc */
  --espace-l:  3rem;     /* 48px — entre un titre et son contenu */
  --espace-xl: 6rem;     /* 96px — entre deux sections. Généreux exprès :
                            le vide est ce qui sépare les sections,
                            pas besoin de lignes de séparation. */


  /* ---------- Largeurs ---------- */

  /* Au-delà, le contenu cesse de s'étirer et reste centré : sur un 27
     pouces, une galerie pleine largeur devient impossible à parcourir. */
  --largeur-max: 72rem;   /* ≈ 1152px */

  /* Un bloc de texte, plus étroit : au-delà de 65-75 caractères par
     ligne, l'œil se perd en revenant à la ligne. */
  --largeur-texte: 38rem; /* ≈ 608px */

  /* L'en-tête est FORCÉ à cette hauteur (min-height plus bas), ce qui
     permet à l'accueil d'occuper le reste au pixel près avec
     calc(100svh - cette valeur). Sinon la colonne de photos dépasse du
     bas de la hauteur du bandeau — ça a toujours l'air d'un accident. */
  --hauteur-entete: 5.5rem; /* ≈ 88px */

  /* L'espace du carrousel : entre les deux colonnes ET entre les photos
     d'une même colonne. Une seule variable pour les deux, de sorte que
     l'écart horizontal et l'écart vertical soient égaux par
     construction — on ne peut pas régler l'un sans l'autre. */
  --carrousel-ecart: 0.375rem; /* 6px */


  /* ---------- Animation ----------
     200ms partout pour les transitions : assez pour percevoir le
     mouvement, assez court pour que le site ne traîne pas. */
  --duree-transition: 200ms;

  /* Long, pour les mouvements qu'on doit REGARDER. C'est la lenteur qui
     sépare un site qui s'agite d'un site sûr de lui. */
  --duree-ample: 900ms;

  /* La courbe donne son caractère au mouvement, bien plus que la durée :
     départ très rapide, freinage très long. L'élément semble lancé puis
     se pose, au lieu de s'arrêter net.

     À comparer au « linear » du carrousel, où l'on veut au contraire une
     régularité parfaite, sans début ni fin perceptibles. */
  --courbe: cubic-bezier(0.16, 1, 0.3, 1);


  /* Pas une variable : color-scheme prévient le navigateur que la page
     est sombre. Il adapte ce que le CSS ne contrôle pas — barre de
     défilement, menus déroulants. Sans elle, barre blanche sur site noir. */
  color-scheme: dark;

}


/* ============================================================
   LE THÈME CLAIR

   Il ne redéfinit QUE des variables : toutes les règles du site
   lisent leurs couleurs dans var(--couleur-…), elles basculent donc
   toutes seules. Pour retoucher le thème clair, c'est ici et nulle part
   ailleurs.

   C'EST LE THÈME DE BASE. Il s'applique tant que <html> ne porte pas
   data-theme="sombre" : à tout le monde par défaut, visiteurs sans
   JavaScript compris. Le sombre ne s'active que si le visiteur le
   choisit avec le bouton ; js/theme.js pose alors l'attribut et retient
   le choix. D'où le :not() : « partout, sauf si sombre a été choisi ».
   Les variables de :root, plus haut, ne servent donc qu'au thème sombre.

   LES CONTRASTES, mesurés sur le fond crème (minimum exigé : 4,5:1
   pour du texte, 3:1 pour un graphisme) :
     texte 13,4:1 — texte doux 5,7:1 — accent 5,3:1 — survol 6,2:1
     logo assombri 4,1:1 — erreur 5,2:1

   LE DORÉ. Celui du site, #d9a441, ne fait que 2:1 sur un fond clair :
   du texte doré y devient illisible. Le thème clair prend donc un doré
   plus profond, presque bronze. Et le survol, qui ÉCLAIRCIT en sombre,
   ASSOMBRIT ici : dans les deux cas il renforce le contraste. La
   variable garde son nom, --couleur-accent-clair, mais lisez-la comme
   « l'accent au survol ».
   ============================================================ */

:root:not([data-theme="sombre"]) {
  /* UN CRÈME, ni blanc ni ivoire. Le premier fond clair, #f5f1ea,
     était un ivoire presque blanc qui éblouissait. Celui-ci renvoie
     14 % de lumière en moins et tire franchement vers le jaune chaud :
     un papier ancien plutôt qu'une feuille d'imprimante. C'est aussi
     le pendant du noir pas tout à fait noir du thème sombre.

     Rendre le fond moins lumineux réduit tous les contrastes : le
     texte doux, le bronze et l'orangé d'erreur ont donc été foncés
     d'un cran en même temps, pour rester au-dessus des minimums. */
  --couleur-fond: #ebe1cc;

  /* À peine plus FONCÉ que le fond, cette fois : c'est l'écart qui
     détache un bloc, pas le sens. Sert aussi aux cadres des photos
     manquantes et au fond des champs du formulaire. */
  --couleur-fond-clair: #e0d4bb;

  --couleur-texte: #1c1a18;
  --couleur-texte-doux: #5c544b;
  --couleur-accent: #7a520c;
  --couleur-accent-clair: #6b4a0c;
  --couleur-bordure: #cfc2a7;
  --couleur-erreur: #9c3f1a;

  --voile-accent: rgba(122, 82, 12, 0.10);

  /* 0.87 et non 0.71 : un voile crème plus léger laisserait passer le
     noir des photos, et le logo bronze tomberait sous les 3:1 exigés
     pour un graphisme. À 0.87 il tient 3:1 même sur du noir pur, et le
     texte principal 10:1. */
  --voile-accueil: rgba(235, 225, 204, 0.87);
  --voile-visionneuse: rgba(235, 225, 204, 0.88);

  /* brightness(0.62) multiplie chaque composante du doré par 0,62 :
     #d9a441 devient #866528, un bronze qui tient 4,1:1 sur le crème.
     Un filtre sur une petite image ne coûte rien, contrairement à un
     masque sur une grande surface. */
  --filtre-logo: brightness(0.62);

  color-scheme: light;
}

/* LE FONDU ENTRE LES THÈMES. js/theme.js photographie la page avant et
   après le changement ; ces deux règles règlent la durée du fondu
   enchaîné entre les deux images. --duree-ample et --courbe : le même
   mouvement lent et sûr que le reste du site. */
::view-transition-old(root),
::view-transition-new(root) {
  animation-duration: var(--duree-ample);
  animation-timing-function: var(--courbe);
}


/* ============================================================
   REMISE À ZÉRO

   Le navigateur applique ses propres styles avant ton CSS, et ils ne
   sont pas les mêmes partout. On en neutralise deux, les plus gênants —
   pas plus : une remise à zéro trop large oblige à tout réécrire.
   ============================================================ */


/* Règle 1 — box-sizing décide de ce que veut dire « largeur ».

   Par défaut, width: 300px avec 20px de marge intérieure de chaque côté
   occupe 340px : la marge s'ajoute PAR-DESSUS. Tu demandes 50 %, tu
   obtiens plus que la moitié, et deux blocs ne tiennent plus sur la ligne.

   Avec border-box, 300px veut dire 300px à l'écran, marges et bordure
   comprises. C'est ce à quoi on s'attend, et ça rend tout prévisible.

   ::before et ::after sont inclus pour qu'ils n'échappent pas à la règle
   le jour où tu en ajoutes un. */

*,
*::before,
*::after {
  box-sizing: border-box;
}


/* Règle 2 — les marges par défaut. Le problème n'est pas qu'il y ait de
   l'espace, c'est qu'on ne l'a pas choisi : chaque élément a sa valeur,
   et elles s'additionnent ou se chevauchent de façon peu prévisible. On
   repart de zéro pour les replacer avec l'échelle --espace-*.

   Liste explicite plutôt que * { margin: 0 } : elle correspond à ce qui
   existe dans index.html. Si tu ajoutes un <h3>, ajoute-le ici. */

body,
h1,
h2,
p,
figure,
ul {
  margin: 0;
}


/* ============================================================
   STYLES DE BASE — le bloc qui branche enfin les variables.

   Ce qu'on pose sur <body> descend dans tout ce qu'il contient : la
   police et la couleur s'écrivent une fois ici au lieu d'être répétées
   sur chaque paragraphe. C'est l'héritage du CSS.
   ============================================================ */

body {
  background-color: var(--couleur-fond);
  color: var(--couleur-texte);
  font-family: var(--police);
  font-size: var(--texte-base);
  line-height: var(--interligne-texte);

  /* Sur Mac, du texte clair sur fond sombre apparaît plus gras qu'il ne
     devrait : les lettres bavent. Seul endroit où c'est nécessaire. */
  -webkit-font-smoothing: antialiased;

  /* Sans ça, ton adresse e-mail est un « mot » de 28 caractères qu'aucun
     navigateur n'a le droit de couper : il la laisse dépasser et la page
     glisse latéralement sur téléphone. break-word ne coupe QUE les mots
     qui ne tiennent pas, le texte courant n'est pas touché. */
  overflow-wrap: break-word;
}


/* Les images. Ces règles valent pour TOUTES celles du site. */

img {
  /* « Jamais plus large que le bloc qui te contient » : une photo de
     1600px se réduit seule sur un téléphone de 390px, au lieu de
     déborder et de créer une barre de défilement horizontale. */
  max-width: 100%;

  /* Indispensable en duo avec la ligne du dessus : le HTML fixe un width
     et un height, et si la largeur se réduit pendant que la hauteur
     reste bloquée, l'image s'écrase. auto la laisse suivre la largeur. */
  height: auto;

  /* Une image est traitée comme une lettre dans une ligne de texte, ce
     qui laisse 3 ou 4px sous elle — la place des jambages du p et du g.
     En bloc, ce fantôme disparaît. */
  display: block;

  /* Les deux lignes suivantes transforment une image MANQUANTE en cadre
     propre plutôt qu'en icône cassée : width et height réservent déjà
     les proportions, on obtient un rectangle gris à la taille exacte de
     la future photo. Une vraie maquette, pas un accident.

     color: transparent masque le texte de remplacement à l'écran
     seulement — l'attribut alt reste dans le HTML et les lecteurs
     d'écran le lisent. On cache la rustine, jamais l'information. */
  background-color: var(--couleur-fond-clair);
  color: transparent;

  /* Sert de repère au pseudo-élément ci-dessous. Sans décalage,
     position: relative ne change rien à la mise en page. */
  position: relative;
}

/* Recouvre l'icône d'image cassée, que color: transparent n'atteint pas
   puisque c'est une image et non du texte.

   L'astuce : une image est un élément « remplacé », son contenu vient du
   fichier. Tant qu'elle s'affiche, le navigateur ignore purement et
   simplement ce ::after — il ne le dessine que si l'image a échoué.
   Cette règle ne touche donc jamais tes vraies photos. */
img::after {
  content: "";
  position: absolute;
  inset: 0;
  background-color: var(--couleur-fond-clair);
}


/* Les liens. */

a {
  color: var(--couleur-accent);

  /* On garde le soulignement — le retirer rend les liens difficiles à
     repérer — mais décalé de 3px pour qu'il ne traverse plus le bas des
     lettres. */
  text-underline-offset: 3px;

  /* La transition s'écrit sur l'état NORMAL et non sur :hover : ainsi
     elle joue dans les deux sens. Sur :hover, le retour serait brutal. */
  transition: color var(--duree-transition);
}

a:hover {
  color: var(--couleur-accent-clair);
}

/* :focus-visible et non :focus : le navigateur réserve alors ce contour
   au clavier, un clic de souris ne le déclenche pas.
   Ne jamais le supprimer sans le remplacer — sans lui, naviguer au
   clavier revient à chercher son curseur dans le noir. */
a:focus-visible {
  outline: 2px solid var(--couleur-accent-clair);
  outline-offset: 3px;
}


/* Les titres. Taille et interligne seulement : les marges sont posées
   section par section avec l'échelle --espace-*. */

h1,
h2 {
  font-family: var(--police-titre);
  line-height: var(--interligne-titre);

  /* L'espacement par défaut est calculé pour du texte courant : à grande
     taille un titre paraît lâche. Valeur minuscule, on la sent sans la
     voir. C'est l'inverse des capitales de la nav, qu'on écarte — même
     règle au fond : plus le texte est gros, moins il lui faut d'air. */
  letter-spacing: -0.015em;

  /* 500 et non 700 : une sérif à fort contraste n'a pas besoin de
     graisse, c'est le dessin qui porte. L'alourdir écraserait justement
     les déliés qui font sa valeur. */
  font-weight: 500;
}

h1 {
  font-size: var(--texte-xl);
}

h2 {
  font-size: var(--texte-l);
}


/* La largeur du contenu : en-tête, sections et pied de page s'étirent
   jusqu'à --largeur-max puis se centrent. #accueil fera exception et va
   d'un bord à l'autre — traité dans son propre bloc. */

header,
main > section,
footer {
  max-width: var(--largeur-max);

  /* auto des deux côtés : le navigateur partage l'espace restant en
     parts égales, donc le bloc se centre. */
  margin-inline: auto;

  /* La marge INTÉRIEURE ne centre rien : elle empêche les lettres de
     toucher la tranche du téléphone. */
  padding-inline: var(--espace-m);
}

/* « + » = la section qui en suit immédiatement une autre : la première
   n'est donc pas concernée et on évite un vide en haut de page. C'est ce
   vide généreux qui sépare les sections, pas des traits. */
main > section + section {
  margin-block-start: var(--espace-xl);
}


/* L'en-tête. Volontairement PAS collant : un bandeau sticky mange 60 à
   70px en permanence sur téléphone et se pose par-dessus les photos dès
   qu'on défile. Sur un site de photographe, c'est l'élément qui ne se
   justifie pas. */

header {
  display: flex;
  align-items: center;

  /* Navigation CENTRÉE, et seule : le logo n'est pas dans l'en-tête,
     il est le grand titre de l'accueil juste en dessous. Collée à
     droite, la nav flottait sans appartenir à rien. Centrée elle
     devient l'axe de la page et s'accorde au logo. Règle générale : un
     élément isolé doit s'aligner sur quelque chose, et quand il n'y a
     rien, le centre est le seul point non arbitraire. */
  justify-content: center;

  /* Hauteur imposée plutôt que dépendante du contenu : c'est ce qui
     permet à l'accueil de calculer ce qui lui reste. */
  min-height: var(--hauteur-entete);
}

header nav ul {
  /* Liste pour le SENS — un lecteur d'écran annonce « liste de 3
     éléments » — mais sans raison d'en montrer les puces. */
  list-style: none;

  /* Le navigateur décale les listes pour loger les puces qu'on vient de
     retirer. C'est un padding, pas une margin : la remise à zéro plus
     haut ne l'a donc pas enlevé. */
  padding: 0;

  display: flex;
  flex-wrap: wrap;
  gap: var(--espace-xs);
}

/* Zones cliquables généreuses : le texte d'un lien fait ~20px, un doigt
   vise 44px. Le padding agrandit la zone sensible sans grossir le texte,
   et inline-block est nécessaire — sur un lien resté en ligne, le padding
   vertical s'affiche sans agrandir la zone cliquable. */
header nav a {
  display: inline-block;
  padding: var(--espace-xs) var(--espace-s);

  /* La nav est une commande, pas du texte à lire. Le soulignement revient
     au survol, quand on vise le lien. */
  text-decoration: none;

  /* En petites capitales espacées, un mot cesse d'être lu comme une
     phrase et devient une étiquette. L'espacement est la clé : en
     capitales les lettres se collent et le mot devient un bloc. On
     n'espace jamais des minuscules, toujours des capitales. */
  text-transform: uppercase;
  letter-spacing: 0.18em;
  font-size: var(--texte-xs);

  /* Gris doux et non doré : trois liens dorés en haut de page attiraient
     l'œil avant les photos. Une navigation doit être trouvable, pas
     remarquable. Le doré est gardé pour le survol, où il fait réponse. */
  color: var(--couleur-texte-doux);

  /* Repère pour le trait ci-dessous. */
  position: relative;
}

/* Trait dessiné à la main plutôt que le soulignement du navigateur, qui
   reste collé sous le texte et ne peut pas être animé. Large de zéro,
   étiré au survol depuis son centre. scaleX plutôt que width : une
   transformation ne fait pas recalculer la mise en page. */
header nav a::after {
  content: "";
  position: absolute;
  left: var(--espace-s);
  right: var(--espace-s);
  bottom: 0.35rem;
  height: 1px;
  background-color: var(--couleur-accent);

  transform: scaleX(0);
  transition: transform var(--duree-ample) var(--courbe);
}

header nav a:hover,
header nav a:focus-visible {
  color: var(--couleur-accent);
}

header nav a:hover::after,
header nav a:focus-visible::after {
  transform: scaleX(1);
}


/* ============================================================
   L'ACCUEIL — l'écran coupé en deux : texte à gauche, colonne de
   photos qui défile à droite.
   ============================================================ */

#accueil {
  /* ANNULENT la règle de largeur écrite plus haut : l'accueil est la
     seule section qui va d'un bord à l'autre. Sans ça, la colonne de
     photos s'arrêterait à 1152px et flotterait au milieu du vide. */
  max-width: none;
  padding-inline: 0;

  /* height et NON min-height. Un minimum n'est qu'un plancher : la
     section resterait libre de grandir, et comme la colonne de droite
     contient une piste de près de 19 000px, la rangée s'étirait d'autant
     — le texte de gauche, centré dedans, se retrouvait très loin sous
     l'écran. Une hauteur ferme referme le piège.

     svh et non vh : sur téléphone 100vh vaut la hauteur SANS la barre
     d'adresse, donc dépasse tant qu'elle est visible. svh prend la plus
     petite, celle où la barre est là. */
  height: calc(100svh - var(--hauteur-entete));

  display: grid;

  /* MOITIÉ-MOITIÉ : le bord du carrousel tombe pile au centre de la
     page, sur le même axe que la navigation centrée au-dessus. Il a été
     un temps en 1fr 1.15fr, pour élargir les photos de 15 % ; le bord
     se retrouvait alors 50 px à gauche du centre, décalé par rapport à
     la navigation, et ça se voyait. */
  grid-template-columns: 1fr 1fr;

  /* Sans cette ligne, la rangée serait dimensionnée « au contenu » et
     reprendrait les 19 000px de la piste malgré le height ci-dessus. */
  grid-template-rows: 100%;

  /* Les deux colonnes font toute la hauteur ; le texte sera recentré
     individuellement juste en dessous. */
  align-items: stretch;

  /* Ce qui transforme la colonne en fenêtre. Sans ça, la piste dépasse
     et crée une immense barre de défilement. */
  overflow: hidden;
}


/* ---------- Colonne de gauche : le texte. ---------- */

.accueil-texte {
  /* Recentre CE bloc seul, sans toucher à la colonne voisine qui doit
     rester étirée. */
  align-self: center;

  /* La marge intérieure que la section n'a plus : jamais moins de 24px,
     jamais plus de 96px, et entre les deux elle suit la fenêtre. */
  padding-inline: clamp(var(--espace-m), 5vw, var(--espace-xl));

  /* Centré, ce qui n'a de sens que depuis qu'il ne reste qu'une phrase
     sous le nom : à trois lignes inégales un texte centré produit des
     bords en dents de scie. C'est le retrait du paragraphe qui a rendu
     ce centrage possible, pas l'inverse. */
  text-align: center;
}

/* 6px sous un titre de 80px : c'est ce qui soude les deux lignes en un
   bloc. L'œil groupe ce qui est proche avant même de lire, il suffit
   donc de resserrer ici et d'éloigner la suite — voir la marge sous la
   signature.

   En rem et non en em : en em ce serait relatif aux 80px du titre, donc
   32px, ce qui ruinerait l'effet sur grand écran. Un espace de
   séparation ne doit pas grandir avec le texte qu'il sépare. */
.accueil-texte h1 {
  margin-block-end: 0.375rem;
}

/* Le grand titre est le logo. 26rem de large au plus, ou toute la
   largeur de la colonne si elle est plus étroite ; la hauteur suit.
   min() prend la plus petite des deux valeurs.

   background-color: transparent ANNULE le fond gris que toute <img> du
   site reçoit pour signaler une photo manquante : le SVG est
   transparent, il s'afficherait sinon dans un rectangle gris. */
.accueil-texte h1 img {
  display: block;
  width: min(100%, 26rem);
  height: auto;
  margin-inline: auto;
  background-color: transparent;

  /* Aucun en sombre ; en clair, assombrit le doré du fichier pour qu'il
     tienne sur le fond crème. Voir --filtre-logo. */
  filter: var(--filtre-logo);
}

/* La signature reprend les capitales espacées de la navigation et des
   numéros de section : ces trois éléments désignent plutôt qu'ils ne
   racontent, et un site cohérent est un site où ce qui joue le même rôle
   se ressemble.

   En blanc cassé, la couleur du texte courant, sous le logo doré : le
   doré reste réservé au logo, et la ligne se lit comme une légende
   plutôt que comme une seconde enseigne. Le gris doux d'avant était
   choisi pour un titre en lettres claires ; sous un logo doré, il
   s'éteignait. */
.accueil-signature {
  /* Sérif italique, en bas de casse : l'italique d'une sérif est un
     dessin à part, hérité de la plume, fin et élégant en minuscules.
     En capitales espacées, la version précédente, il perdrait ça.

     Graisse 400, la plus fine embarquée, sur ordinateur : le fond est
     un aplat sombre, les déliés fins y restent nets. Sur téléphone la
     ligne repose sur le carrousel voilé, et la 500 y tient mieux — voir
     le bloc téléphone de l'accueil. */
  font-family: var(--police-titre);
  font-style: italic;
  font-weight: 400;
  font-size: var(--texte-m);
  letter-spacing: 0.02em;
  color: var(--couleur-texte);

  /* L'autre moitié du procédé : 6px au-dessus, 48px en dessous. Le
     rapport de un à huit ne laisse aucune ambiguïté sur ce qui va
     ensemble. Resserrer sans éloigner n'aurait donné que trois lignes
     un peu plus tassées. */
  margin-block-end: var(--espace-l);
}

/* Sérif italique : le geste typographique le plus marqué du site.
   L'italique d'une sérif éditoriale est un dessin entièrement différent,
   hérité de l'écriture à la plume — d'où son élégance manuscrite.
   Ici et nulle part ailleurs : un effet qui se répète cesse d'en être un. */
.accroche {
  font-family: var(--police-titre);
  font-style: italic;
  font-weight: 500;
  font-size: var(--texte-m);
  letter-spacing: 0;

  /* max-width limite la longueur de ligne, margin-inline: auto recentre
     le bloc rétréci. Les deux vont ensemble : sans la marge auto, le
     bloc resterait collé à gauche. */
  max-width: var(--largeur-texte);
  margin-inline: auto;
}


/* ---------- Colonne de droite : la fenêtre du carrousel. ---------- */

.carrousel {
  overflow: hidden;

  /* Un élément de grille refuse par défaut de devenir plus petit que son
     contenu, ce qui suffirait à rouvrir le piège des 19 000px.
     min-height: 0 lève cette protection, ici indésirable. */
  min-height: 0;

  /* Repère pour le calque de fondu du thème sombre, plus bas. */
  position: relative;

  /* DEUX COLONNES DE PHOTOS, côte à côte et de même largeur. Chacune
     est un carrousel complet : sa propre piste, ses propres copies.

     L'écart entre elles est minuscule : les photos se touchent à la
     verticale, elles doivent presque se toucher à l'horizontale aussi.
     Assez pour que l'œil sépare les deux flux qui vont en sens
     contraire, pas assez pour ouvrir un couloir de vide au milieu. */
  display: grid;
  column-gap: var(--carrousel-ecart);

  /* UNE colonne par défaut, DEUX seulement une fois le JavaScript passé.

     C'est js/carrousel.js qui fabrique la seconde colonne et pose la
     classe « prete ». Sans lui — script bloqué, connexion coupée — le
     HTML ne contient qu'une liste : ouvrir deux pistes de grille
     afficherait une colonne vide à droite. On part donc d'une colonne,
     et on n'en ouvre une seconde que lorsqu'il y a de quoi la remplir. */
  grid-template-columns: 1fr;
}

.carrousel.prete {
  grid-template-columns: 1fr 1fr;
}

/* Chaque colonne est une fenêtre à part entière. Le overflow du parent
   suffirait à couper le bas, mais le déclarer ici aussi rend chaque
   colonne autonome — et c'est indispensable sous mouvement réduit, où
   la piste devient défilable à la main et a besoin d'un cadre à sa
   propre hauteur. */
.carrousel-colonne {
  overflow: hidden;
  min-height: 0;
}

/* LES FONDUS DES BORDS, EN THÈME SOMBRE SEULEMENT.

   Deux dégradés peints par-dessus les photos : de haut en bas, elles
   entrent et sortent en fondu plutôt que tranchées net par le bord ; et
   depuis la gauche, le raccord entre texte et photos s'efface, la
   colonne semble émerger de l'ombre. Le fondu gauche est court (14 %)
   contre 12 / 88 % pour le vertical : il adoucit, il ne mange pas la
   photo.

   AUCUN FONDU EN THÈME CLAIR : essayé sur les quatre bords, puis en
   haut et en bas seulement, et écarté les deux fois le 10 octobre
   2026. Sur le crème, les photos sombres se diluaient dans une brume
   beige. Elles y restent tranchées net.

   PEINTS ET NON MASQUÉS. Un mask-image oblige le navigateur à
   re-rasteriser la zone masquée à chaque recomposition. Mesuré dans
   Brave en rendu logiciel, souris en mouvement : 59 fps avec masque,
   73 avec ces dégradés peints vers --couleur-fond. Le rendu est
   identique tant que le fond derrière la colonne est un aplat. */
:root[data-theme="sombre"] .carrousel::after {
  content: "";
  position: absolute;
  inset: 0;

  /* Ne doit jamais intercepter un clic : il couvre toutes les photos. */
  pointer-events: none;
  z-index: 1;

  background-image:
    linear-gradient(to bottom, var(--couleur-fond), transparent 12%, transparent 88%, var(--couleur-fond)),
    linear-gradient(to right, var(--couleur-fond), transparent 14%);
}


/* La piste : le ruban qui contient les deux copies. C'est elle qu'on
   déplace, pas les images. */

/* Le défilement n'existe qu'une fois le carrousel « prêt », c'est-à-dire
   une fois que js/carrousel.js a fabriqué les copies. Avant, la piste
   ne contient qu'une liste : la déplacer de -50 % la ferait sauter au
   milieu de la liste à chaque tour. Sans JavaScript, elle reste donc
   immobile — ce qui est la bonne dégradation. */
/* La durée vient de js/carrousel.js, qui la calcule d'après la hauteur
   des photos pour que la vitesse reste la même quel que soit leur
   nombre (environ 96 s avec les 18 photos). 50s n'est qu'une valeur de
   secours, si la variable manquait. */
.carrousel.prete .carrousel-piste {
  animation: defilement-carrousel var(--carrousel-duree, 50s) linear infinite;
}

/* La colonne de droite joue la MÊME animation à l'envers.

   reverse lit les images-clés de la fin vers le début : la piste part
   de -50 % et revient vers 0, donc descend. On ne réécrit ni la durée,
   ni la courbe, ni les images-clés — un seul mot, et la symétrie est
   garantie par construction plutôt que recopiée à la main.

   La boucle reste sans couture par le même argument qu'à l'endroit :
   au retour brutal de 0 vers -50 %, la première copie occupe pile la
   position que la seconde vient de quitter. */
.carrousel.prete .carrousel-colonne--inverse .carrousel-piste {
  animation-direction: reverse;
}

/* On déplace de la MOITIÉ de sa hauteur. Comme elle contient exactement
   deux copies identiques, la moitié vaut une copie : à la fin, la
   seconde occupe pile la position de départ de la première, et le retour
   brutal à zéro est invisible.

   linear et non ease : une accélération et un freinage se verraient à
   chaque tour. Un défilement continu doit être d'une régularité parfaite.

   Volontairement lent — c'est ce qui sépare un portfolio d'un bandeau
   publicitaire. Une photo carrée met environ 4,5 secondes à passer
   (voir js/carrousel.js, SECONDES_PAR_LARGEUR). */
@keyframes defilement-carrousel {
  from { transform: translateY(0); }
  to   { transform: translateY(-50%); }
}


/* Les deux listes. Aucun espacement entre les photos : elles forment un
   ruban continu. Choix esthétique, mais avec un effet de bord utile —
   sans espace, pas d'espace à compter faux.

   POUR REMETTRE DE L'AIR UN JOUR : une margin-block-end sur CHAQUE
   image, la dernière comprise, et surtout pas un gap. Douze images
   séparées par gap font onze espaces et non douze : la piste mesurerait
   une demi-espace de moins que le double d'une copie, translateY(-50%)
   ne tomberait plus juste, et l'image sursauterait à chaque tour.

   display: flex évite un piège voisin : dans un bloc normal, la marge de
   la dernière image s'échapperait hors de la liste (fusion des marges)
   et fausserait la hauteur. Un conteneur flex ne fusionne jamais. */

.carrousel-liste {
  list-style: none;
  padding: 0;
  display: flex;
  flex-direction: column;
}

/* L'espace sous chaque photo, la dernière comprise — et surtout PAS un
   gap sur la liste. Dix photos séparées par gap, ça fait neuf espaces ;
   la piste mesurerait alors un demi-espace de moins que le double d'une
   copie, translateY(-50%) ne tomberait plus juste, et l'image
   sursauterait à chaque tour. Avec une marge sous CHAQUE photo, une
   copie vaut exactement n photos + n espaces : la moitié de la piste
   tombe au pixel près.

   C'est la même variable que l'écart entre les colonnes : les deux
   sont égaux par construction. */
.carrousel-liste li {
  margin-block-end: var(--carrousel-ecart);
}

.carrousel-liste img {
  width: 100%;
}

/* Le lien enveloppe l'image : en bloc, il épouse exactement ses
   dimensions. En ligne, il laisserait dépasser quelques pixels. */
.carrousel-liste a {
  display: block;
}

/* Le contour de focus est dessiné VERS L'INTÉRIEUR (offset négatif).
   Avec l'offset positif habituel, il serait tracé en dehors de l'image
   et le overflow: hidden de la colonne le couperait : on ne verrait
   plus où on se trouve au clavier. */
.carrousel-liste a:focus-visible {
  outline: 2px solid var(--couleur-accent-clair);
  outline-offset: -4px;
}


/* Mettre le défilement en pause. Une animation qui démarre seule et ne
   s'arrête jamais doit pouvoir être stoppée : c'est une règle
   d'accessibilité, pas un confort.

   :focus-within = « un élément à l'intérieur a le focus ». Il couvre le
   clavier, et c'est pour ça que les images sont des liens : sans eux,
   Tab n'entrerait jamais dans la colonne.

   :hover a été RETIRÉ volontairement. Le carrousel occupe toute la
   moitié droite : la souris le traverse en permanence et le défilement
   se figeait à chaque fois. Une pause déclenchée sans le vouloir n'est
   pas une pause, c'est une panne. Deux moyens d'arrêt subsistent, ce qui
   rend ce retrait acceptable : le clavier ci-dessous et le réglage
   « réduire les animations », juste après.

   animation-play-state met en pause SUR PLACE, sans revenir au début. */

.carrousel:focus-within .carrousel-piste {
  animation-play-state: paused;
}


/* Mouvement réduit. Troubles vestibulaires, migraines, épilepsie : ce
   réglage est un besoin médical, pas une préférence esthétique.

   On ne se contente pas d'arrêter l'animation : la colonne devient
   défilable à la main, et la copie décorative disparaît — sinon la
   personne verrait ses photos deux fois de suite en défilant. */

@media (prefers-reduced-motion: reduce) {

  /* .carrousel.prete .carrousel-piste, et non .carrousel-piste seul :
     c'est sous cette double classe que le défilement est déclaré. Une
     règle moins spécifique perdrait l'arbitrage et le carrousel
     continuerait de tourner malgré le réglage — ce qui est exactement
     arrivé lors de l'ajout de la classe « prete ». */
  .carrousel.prete .carrousel-piste {
    animation: none;
  }

  .carrousel-liste[aria-hidden="true"] {
    display: none;
  }

  /* La colonne de droite disparaît et la gauche prend toute la largeur.
     Sans mouvement, deux colonnes des mêmes photos n'ont plus aucun
     sens : on retombe sur une seule colonne, défilable à la main, qui
     montre chaque photo une fois. */
  .carrousel-colonne--inverse {
    display: none;
  }

  /* .carrousel.prete, et pas .carrousel : la règle qui ouvre deux
     colonnes porte cette double classe, il faut la même pour la
     contredire. Sans ça, on garderait deux pistes de grille avec la
     droite masquée — une colonne de vide. */
  .carrousel.prete {
    grid-template-columns: 1fr;
  }

  /* C'est la PISTE qui défile à la main, pas la colonne — et ce détail
     compte. Avec le défilement sur .carrousel, le calque
     de fondu (.carrousel::after) défilait avec le contenu : au bout de
     quelques centaines de pixels, les photos redevenaient tranchées net
     et le fondu se retrouvait au milieu. Le défilement est donc placé un
     cran plus à l'intérieur : la colonne ne bouge pas, le calque reste
     collé à ses bords. */
  .carrousel-piste {
    height: 100%;
    overflow-y: auto;
  }

}


/* L'accueil sur téléphone. Deux colonnes ne tiennent pas sur 390px : on
   change de principe plutôt que de rétrécir. Le carrousel quitte la
   grille, passe DERRIÈRE le texte en occupant tout l'écran, et un voile
   sombre s'intercale pour que le nom reste lisible.

   45rem ≈ 720px : on ne raisonne pas en « taille de téléphone » mais en
   largeur à partir de laquelle la mise en page cesse de fonctionner.
   C'est la seule mesure qui ne se périme pas. */

@media (max-width: 45rem) {

  #accueil {
    /* Une seule colonne : le carrousel va sortir de la grille. */
    grid-template-columns: 1fr;

    /* Un élément en absolute se cale sur son ancêtre positionné le plus
       proche. Sans cette ligne, le carrousel se calerait sur la page
       entière au lieu de la section. */
    position: relative;

    /* Retour à min-height, sans danger ici : le carrousel sort de la
       grille et ne peut plus étirer la rangée. L'intérêt, c'est que si
       la nav passait sur deux lignes, une hauteur figée rognerait le bas
       de la section — là, la mise en page cède proprement. */
    height: auto;
    min-height: calc(100svh - var(--hauteur-entete));
  }

  /* Les trois liens mesurent 328px : sur 390px il manque 2px et la nav
     passe sur deux lignes, ce qui fait grandir l'en-tête et pousse
     l'accueil sous la ligne de flottaison. On réduit l'horizontale
     seulement — la verticale donne au doigt sa cible confortable. */
  header nav a {
    padding-inline: var(--espace-xs);
  }

  header nav ul {
    gap: 0;
  }

  /* Sous le logo, la ligne en italique repasse en graisse 500 : ici
     elle est posée sur les photos du carrousel derrière le voile, et
     les déliés de la 400 s'y perdraient. */
  .accueil-signature {
    font-weight: 500;
  }


  .carrousel {
    /* inset: 0 = haut/droite/bas/gauche à zéro : l'élément s'étire
       jusqu'aux quatre bords de la section. */
    position: absolute;
    inset: 0;
    z-index: 0;
  }

  /* Le voile, en ::before : un élément purement décoratif n'a rien à
     faire dans le HTML. content: "" est obligatoire même vide, sans lui
     le pseudo-élément n'est pas créé.

     0.71 est LE nombre à régler si le texte paraît trop ou trop peu
     lisible. Plus haut, les photos disparaissent ; plus bas, on les voit
     mieux mais le nom se bat avec elles.

     NE PAS DESCENDRE SOUS 0.63 : c'est le seuil calculé pour le pire cas,
     une photo entièrement blanche derrière le texte, où le contraste
     tombe pile sur le minimum exigé de 4.5:1. */
  #accueil::before {
    content: "";
    position: absolute;
    inset: 0;
    z-index: 1;
    background-color: var(--voile-accueil);
  }

  /* Le texte repasse au-dessus du voile. position: relative sans
     décalage semble inutile mais ne l'est pas : z-index n'a aucun effet
     sur un élément non positionné. */
  .accueil-texte {
    position: relative;
    z-index: 2;

    /* Le texte occupe toute la largeur : marge intérieure normale plutôt
       que celle, généreuse, prévue pour la colonne de gauche. */
    padding-inline: var(--espace-m);
  }

}


/* ============================================================
   LA GALERIE — le seul endroit où l'on s'arrête vraiment sur une
   image. L'accueil fait apercevoir, ici on regarde.
   ============================================================ */

/* L'espace entre un titre de section et son contenu, pour les trois
   sections qui suivent. */
main > section h2 {
  margin-block-end: var(--espace-l);
}

/* Le numéro au-dessus de chaque titre. attr(data-numero) lit l'attribut
   du même nom dans le HTML — le seul moyen, en CSS pur, d'afficher un
   texte qui change d'un élément à l'autre.

   Procédé emprunté aux magazines : il transforme trois sections
   juxtaposées en sommaire, et le lecteur comprend qu'il parcourt un
   ensemble fini plutôt qu'une page sans fin.

   Retour à la sans-serif et aux capitales espacées : ce petit texte joue
   le même rôle que la navigation, il doit lui ressembler. */
main > section h2::before {
  content: attr(data-numero);
  display: block;

  font-family: var(--police);
  font-size: var(--texte-xs);
  font-weight: 500;
  letter-spacing: 0.22em;
  color: var(--couleur-accent);

  /* Annule l'interligne serré et le resserrement des lettres hérités
     du h2, qui sont calculés pour du très grand texte. */
  line-height: 1;
  margin-block-end: var(--espace-s);
}


/* columns et NON CSS Grid, choix dicté par les photos, qui mélangent
   portraits et paysages. Dans une grille, toutes les cases d'une rangée
   prennent la hauteur de la plus haute : un paysage à côté d'un portrait
   se retrouve avec un grand vide sous lui. columns remplit la première
   colonne puis la seconde, comme un journal — chaque photo garde sa
   hauteur et se cale sous la précédente.

   Contrepartie : l'ordre de lecture descend la colonne de gauche avant
   de passer à droite. Sans importance ici, on regarde, on ne lit pas. */

.galerie {
  columns: 2;
  column-gap: var(--espace-m);
}

.galerie figure {
  /* Interdit au navigateur de couper une figure pour la répartir sur
     deux colonnes. Sans cette ligne, une photo peut se retrouver
     séparée de sa légende, voire coupée en plein milieu. */
  break-inside: avoid;

  /* L'espace sous chaque figure. column-gap ne gère que l'espace
     HORIZONTAL entre les colonnes ; le vertical se pose ici. */
  margin-block-end: var(--espace-m);
}

.galerie img {
  /* Toute la largeur de la colonne ; la hauteur suit seule grâce au
     height: auto des styles de base. On ne recadre rien. */
  width: 100%;
}

/* Les légendes accompagnent l'image, elles ne la concurrencent pas :
   plus petites et plus grises que le texte courant. */
.galerie figcaption {
  margin-block-start: var(--espace-xs);
  font-size: var(--texte-s);
  color: var(--couleur-texte-doux);
}


/* ============================================================
   À PROPOS
   ============================================================ */

#a-propos {
  display: grid;

  /* Deux colonnes : la photo, puis le texte.

     Le texte est plafonné à --largeur-texte, sa longueur de ligne
     confortable, et la photo prend ce qui reste, avec 24rem de plancher
     pour ne jamais devenir un timbre. Sur un écran large, ça lui donne
     28rem. Sur un écran plus étroit, c'est le texte qui cède d'abord,
     jusqu'à ce que la photo touche son plancher. Sans ce plancher, un
     texte qui a un maximum mais pas de minimum la grignoterait jusqu'à
     rien. */
  grid-template-columns: minmax(24rem, 1fr) minmax(0, var(--largeur-texte));
  gap: var(--espace-l);

  /* Les colonnes partent du haut plutôt que d'être centrées : le
     portrait s'aligne sur la première ligne du texte. */
  align-items: start;
}

/* Le titre traverse les deux colonnes. 1 / -1 veut dire « de la
   première ligne de colonne à la dernière », quel que soit leur
   nombre : la règle reste vraie si tu passes à trois colonnes.

   Sa marge inférieure est annulée : dans une grille, c'est le gap qui
   gère l'espacement, et les deux s'additionneraient. */
#a-propos h2 {
  grid-column: 1 / -1;
  margin-block-end: 0;
}

/* On assigne explicitement sa colonne à chacun. Laissée libre, la
   grille range ses enfants un par un dans la case suivante : le
   portrait prenait bien la colonne 1, mais le bloc de texte pouvait
   retomber en dessous plutôt qu'à côté. */
#a-propos img {
  grid-column: 1;
  width: 100%;
}

#a-propos .a-propos-texte {
  grid-column: 2;
  max-width: var(--largeur-texte);
}

#a-propos p + p {
  margin-block-start: var(--espace-s);
}

/* Le matériel, en retrait : plus petit, plus gris, en italique. Une
   information pour les pros, qui ne doit pas peser autant que le reste.

   L'italique vient de la sérif, pas d'Inter : fonts/ n'a pas de fichier
   italique pour Inter, et le navigateur fabriquerait alors un faux
   italique en penchant les lettres droites — ça se voit. Playfair a le
   sien, dessiné à part. Le gris doux garde un contraste de 6,8:1 sur le
   fond, au-dessus des 4,5 exigés pour du petit texte. */
.a-propos-materiel {
  font-family: var(--police-titre);
  font-style: italic;
  font-weight: 500;
  font-size: var(--texte-s);
  color: var(--couleur-texte-doux);
}

/* SUR GRAND ÉCRAN, LA PHOTO PREND LA HAUTEUR DU TEXTE.

   Le texte est plus haut que la photo : environ 340px contre 300px
   pour un 3:2 dans sa colonne. height: 100% étire la photo à la hauteur
   de sa ligne de grille, donc du texte, et object-fit: cover la fait
   remplir ce cadre SANS la déformer : elle est agrandie jusqu'à couvrir
   la hauteur, et ce qui dépasse en largeur est rogné. object-position
   choisit ce qu'on garde : le bord gauche, chevelure et oreille. Ce qui
   part est de l'air à droite, 12 à 16 % de la largeur. Le visage et son
   placement au tiers restent.

   Seulement à partir de 72rem, la largeur maximale du site. En dessous,
   la colonne du texte rétrécit, le texte s'allonge, et l'étirer à cette
   hauteur couperait le visage : la photo garde alors son format et ne
   s'aligne que sur le haut du texte. */
@media (min-width: 72rem) {
  #a-propos img {
    height: 100%;
    object-fit: cover;
    object-position: left top;
  }
}

/* L'ALIGNEMENT AU PIXEL. Une ligne de texte a de l'air au-dessus des
   capitales et au-dessous de la ligne de base : la moitié de
   l'interligne, environ 8px ici. Sans rien faire, le texte paraît
   rentré de 8px par rapport aux bords de la photo. text-box retire cet
   air : au premier paragraphe, le haut des capitales touche le bord
   haut de la photo ; au dernier, la ligne de base touche le bord bas.

   Un navigateur qui ne connaît pas encore cette propriété l'ignore :
   le texte reste rentré de 8px. Pas cassé, juste moins net. */
.a-propos-texte > :first-child {
  text-box: trim-start cap alphabetic;
}

.a-propos-texte > :last-child {
  text-box: trim-end cap alphabetic;
}


/* ============================================================
   CONTACT

   Le but de toute la page. Cette section doit être la plus facile à
   utiliser, pas la plus jolie.
   ============================================================ */

.contact-intro {
  font-family: var(--police-titre);
  font-style: italic;
  /* 500 dit explicitement : depuis que l'italique existe aussi en 400,
     un texte sans graisse hériterait de celle du corps (400) et
     changerait de dessin sans qu'on l'ait voulu. */
  font-weight: 500;
  font-size: var(--texte-m);
  margin-block-end: var(--espace-l);
}


/* ------------------------------------------------------------
   Le formulaire.

   Il est volontairement étroit : un champ de saisie large de
   1100px est intimidant, et on ne sait pas où regarder. La largeur
   de confort du texte s'applique aussi aux champs qu'on remplit.
   ------------------------------------------------------------ */

.formulaire {
  max-width: var(--largeur-texte);
}

.champ {
  /* Le label au-dessus du champ, pas à côté. C'est ce qui tient sur
     un téléphone, et ce qui évite que l'œil fasse des allers-retours
     horizontaux entre l'étiquette et la case. */
  display: flex;
  flex-direction: column;
  gap: var(--espace-xs);
}

.champ + .champ {
  margin-block-start: var(--espace-m);
}

.champ label {
  font-size: var(--texte-xs);
  letter-spacing: 0.18em;
  text-transform: uppercase;
  color: var(--couleur-texte-doux);
}

/* Un champ de saisie n'hérite JAMAIS de la police de la page : il garde
   celle du navigateur. D'où font: inherit, la ligne la plus importante
   de ce bloc — sans elle, du Arial 13px au milieu d'un site en Inter 19. */
.champ input,
.champ textarea {
  font: inherit;
  color: var(--couleur-texte);

  /* Un fond à peine plus clair que la page plutôt qu'un cadre :
     le champ se signale par sa matière, pas par un trait. */
  background-color: var(--couleur-fond-clair);
  border: 1px solid var(--couleur-bordure);
  border-radius: 0;

  padding: var(--espace-s);
  width: 100%;

  transition:
    border-color var(--duree-ample) var(--courbe),
    background-color var(--duree-ample) var(--courbe);
}

.champ textarea {
  /* Interdit le redimensionnement horizontal, qui casserait la mise
     en page, tout en laissant l'agrandissement vertical — utile à qui
     écrit un long message. */
  resize: vertical;
  min-height: 9rem;
}

.champ input:focus,
.champ textarea:focus {
  /* On remplace le contour par défaut plutôt que de le supprimer :
     la bordure dorée dit clairement où l'on se trouve, au clavier
     comme à la souris. */
  outline: none;
  border-color: var(--couleur-accent);
  background-color: var(--couleur-fond);
}


/* Le piège à robots. Il doit être invisible SANS utiliser
   display: none ni visibility: hidden — les robots les plus évolués
   reconnaissent ces deux propriétés et savent alors qu'il s'agit
   d'un piège. On le sort donc de l'écran par la géométrie. */
.piege {
  position: absolute;
  left: -9999px;
  width: 1px;
  height: 1px;
  overflow: hidden;
}


/* Le message de confirmation ou d'erreur. Vide au départ, il ne
   prend aucune place tant qu'il ne contient rien. */
.formulaire-etat {
  margin-block-start: var(--espace-m);
  font-size: var(--texte-s);
  min-height: 1.5em;
}

.formulaire-etat.succes {
  color: var(--couleur-accent-clair);
}

.formulaire-etat.erreur {
  /* Un orangé sourd plutôt qu'un rouge vif : voir --couleur-erreur. */
  color: var(--couleur-erreur);
}

.formulaire-etat.attente {
  color: var(--couleur-texte-doux);
}


/* Le bouton désactivé pendant l'envoi. On le montre clairement :
   un bouton qui ne réagit plus sans raison visible donne
   l'impression que le site est cassé. */
.cta:disabled {
  opacity: 0.45;
  cursor: not-allowed;
}


/* Le recours affiché uniquement si JavaScript est absent. Personne ne
   le voit en temps normal — d'où l'absence de traitement particulier,
   il reprend simplement l'apparence du texte courant. */
.contact-recours {
  margin-block-start: var(--espace-l);
  max-width: var(--largeur-texte);
  color: var(--couleur-texte-doux);
}


/* ============================================================
   PIED DE PAGE
   ============================================================ */

footer {
  margin-block-start: var(--espace-xl);
  padding-block: var(--espace-l);

  /* Un trait très discret pour détacher le pied de page. C'est le seul
     endroit du site où une bordure se justifie : partout ailleurs,
     c'est le vide qui sépare. */
  border-block-start: 1px solid var(--couleur-bordure);

  font-size: var(--texte-s);
  color: var(--couleur-texte-doux);
}

footer p + p {
  margin-block-start: var(--espace-xs);
}


/* ============================================================
   L'ARRIVÉE SUR LA PAGE

   Le contenu ne s'affiche pas, il se pose. C'est le DÉCALAGE entre les
   éléments qui produit l'effet : ensemble ce serait un simple fondu,
   en cascade on croit à une intention.
   ============================================================ */

@keyframes arrivee {
  from {
    opacity: 0;
    /* La montée de 1.5rem est ce qui distingue une apparition d'un
       fondu : l'élément vient se mettre en place, il n'était pas déjà là
       en attendant d'être révélé. */
    transform: translateY(1.5rem);
  }
  to {
    opacity: 1;
    transform: none;
  }
}

/* La colonne de photos, elle, ne monte pas : elle se révèle sur place,
   plus lentement que le texte. Une masse de cette taille qui se
   déplacerait paraîtrait lourde. */
@keyframes revelation {
  from { opacity: 0; }
  to   { opacity: 1; }
}

/* « both » est le mot important : l'animation s'applique AVANT son
   démarrage autant qu'après sa fin. Sans lui, un élément retardé de
   400ms reste visible pendant ces 400ms puis disparaît d'un coup pour
   réapparaître en fondu — l'inverse exact de l'effet cherché.

   120ms entre chaque élément. Au-delà, l'attente devient perceptible. */
.accueil-texte h1 {
  animation: arrivee var(--duree-ample) var(--courbe) both;
}

.accueil-signature {
  animation: arrivee var(--duree-ample) var(--courbe) 120ms both;
}

.accroche {
  animation: arrivee var(--duree-ample) var(--courbe) 240ms both;
}

.carrousel {
  animation: revelation 1600ms var(--courbe) 200ms both;
}


/* ============================================================
   LES SURVOLS

   Une image qui ne réagit pas au passage de la souris donne
   l'impression d'une page morte. Une image qui réagit trop donne
   l'impression d'une vitrine de magasin. On vise entre les deux :
   un mouvement qu'on remarque à peine mais qu'on ressent.
   ============================================================ */

/* LE CADRE QUI RETIENT LE ZOOM, et il entoure l'image SEULE.

   C'était le <figure> qui jouait ce rôle, et c'était faux. Une figure
   contient l'image ET sa légende : elle mesurait 573,6 px là où l'image
   en fait 540. Le zoom, lui, est bien centré — mesuré, l'image passe de
   [0 ; 540] à [-10,8 ; 550,8], soit 10,8 px de chaque côté. Mais seuls
   les 10,8 px du HAUT dépassaient de la figure et se faisaient couper ;
   ceux du bas restaient dedans et grandissaient dans l'espace de la
   légende. D'où le contour noir des photos, recadré en haut et toujours
   visible en bas.

   Une <div> sans valeur sémantique est justifiée ici : elle ne sert
   qu'à délimiter une zone de recadrage. */
.galerie .cadre {
  overflow: hidden;
}

.galerie img,
.carrousel-liste img {
  /* La transition est écrite sur l'état normal, jamais sur :hover :
     elle joue ainsi dans les deux sens, à l'arrivée de la souris
     comme à son départ. Sur :hover, le retour serait brutal. */
  transition:
    transform var(--duree-ample) var(--courbe),
    filter var(--duree-ample) var(--courbe);
}

/* 1.04, c'est 4% d'agrandissement. C'est très peu, et c'est voulu :
   au-delà, on voit la photo bouger au lieu de la voir s'animer. */
.galerie figure:hover img {
  transform: scale(1.04);
  filter: brightness(1.06);
}

/* Dans le carrousel, pas de zoom : la piste est déjà en mouvement,
   un second déplacement par-dessus serait illisible. Seule la
   luminosité répond. */
.carrousel-liste a:hover img,
.carrousel-liste a:focus-visible img {
  filter: brightness(1.12);
}

/* Les légendes n'apparaissent qu'au survol de leur photo. Elles
   restent disponibles au clavier grâce à :focus-within. */
.galerie figcaption {
  transition: color var(--duree-ample) var(--courbe);
}

.galerie figure:hover figcaption {
  color: var(--couleur-texte);
}


/* ============================================================
   LES ALBUMS ET LA VISIONNEUSE

   Une vignette de la galerie est la couverture d'un album. Le clic
   ouvre la visionneuse : un voile sombre sur toute la page, et les
   photos de l'album alignées sur une BANDE horizontale — celle du
   milieu en entier, ses voisines qui dépassent sur les bords. La
   molette fait avancer la bande d'une photo, le doigt la fait glisser.
   C'est js/albums.js qui remplit et ouvre ; ici, seulement l'apparence.
   ============================================================ */

/* La couverture est un lien qui contient l'image. display: block : un
   lien est « en ligne » par défaut, et un lien en ligne autour d'une
   image laisse sous elle quelques pixels réservés aux jambages des
   lettres, un liseré de fond qui n'a rien à faire là. */
.album-couverture {
  display: block;
}

/* Le contour de focus est tracé vers l'intérieur : .cadre coupe tout
   ce qui dépasse (c'est son rôle, pour le zoom), et un contour dessiné
   dehors serait invisible. */
.album-couverture:focus-visible {
  outline: 2px solid var(--couleur-accent-clair);
  outline-offset: -3px;
}

/* Le nombre de photos, ajouté à la légende par le script. Le point
   médian est dessiné ici plutôt qu'écrit dans le script : c'est de la
   ponctuation décorative, elle relève de la mise en forme. */
.album-nombre::before {
  content: " · ";
}

/* ---------- La fenêtre. ---------- */

.visionneuse {
  /* Toute la fenêtre du navigateur, par-dessus la page. Les réglages
     par défaut d'un <dialog> — centré, à la taille de son contenu, avec
     une bordure — sont tous remis à zéro. */
  position: fixed;
  inset: 0;
  width: 100%;
  height: 100%;
  max-width: none;
  max-height: none;
  margin: 0;
  border: 0;

  /* En haut, la place de la croix ; en bas, un peu d'air sous la
     légende. Pas de marge sur les côtés : la bande va jusqu'aux bords. */
  padding-block: var(--espace-xl) var(--espace-m);
  padding-inline: 0;

  /* Le voile : le fond du site, très sombre mais pas opaque. On devine
     encore la page derrière, assez pour savoir qu'on ne l'a pas
     quittée. 0.86 et non 0.94 depuis le flou : une page floutée peut
     transparaître davantage sans distraire. */
  background-color: var(--voile-visionneuse);
  color: var(--couleur-texte);

  /* LE FLOU. backdrop-filter s'applique à ce qui est dessiné DERRIÈRE
     l'élément, ici la page. Combiné au voile, la page devient une
     lueur diffuse : la galerie est encore là, mais plus lisible.

     Ce que ça coûte : le navigateur recalcule ce flou à chaque image
     où quelque chose bouge, devant ou derrière. Sur une carte
     graphique c'est bon marché ; en rendu logiciel, ça peut faire
     saccader la bande pendant qu'elle glisse. C'est pour ça que le
     carrousel de l'accueil est mis en pause derrière (voir plus bas) :
     sans mouvement derrière, le flou n'est refait que quand la bande
     bouge. Si ça saccade malgré tout, c'est cette seule ligne à
     retirer. Le fondu d'ouverture emporte le flou avec lui : à opacité
     nulle, on ne voit ni le voile ni le flou, puis les deux montent
     ensemble. */
  backdrop-filter: blur(16px);

  /* Au-dessus du bouton de retour en haut (98), SOUS le curseur (100 et
     101). C'est tout l'intérêt de ne pas utiliser showModal() : voir
     js/albums.js. */
  z-index: 99;

  /* Deux rangées : la bande, qui prend toute la hauteur qui reste, puis
     la légende. minmax(0, 1fr) et non 1fr seul : une rangée 1fr refuse
     de descendre sous la taille de son contenu, et une photo haute
     pousserait la légende hors de l'écran. */
  display: grid;
  grid-template-rows: minmax(0, 1fr) auto;
  row-gap: var(--espace-s);

  /* Apparition en fondu. Un <dialog> fermé est en display: none, et
     display ne s'anime pas : allow-discrete demande au navigateur de
     garder l'élément affiché le temps que l'opacité finisse de
     descendre. @starting-style, plus bas, donne le point de départ à
     l'ouverture ; sans lui l'élément apparaîtrait déjà opaque. Un
     navigateur qui ne connaît pas ces deux mots ouvre et ferme d'un
     coup. */
  opacity: 1;
  transition:
    opacity var(--duree-ample) var(--courbe),
    display var(--duree-ample) allow-discrete;
}

/* Le display: grid écrit juste au-dessus prendrait le pas sur le
   display: none que le navigateur applique aux <dialog> fermés : on le
   redit ici. */
.visionneuse:not([open]) {
  display: none;
  opacity: 0;
}

@starting-style {
  .visionneuse[open] {
    opacity: 0;
  }
}

/* Pendant que la visionneuse est ouverte, la page derrière ne défile
   plus et le bouton de retour en haut s'efface. :has() sélectionne le
   body qui CONTIENT une visionneuse ouverte — une condition sur un
   descendant, ce que le CSS ne savait pas faire avant. */
body:has(.visionneuse[open]) {
  overflow: hidden;
}

body:has(.visionneuse[open]) .retour-haut,
body:has(.visionneuse[open]) .bascule-theme {
  opacity: 0;
  visibility: hidden;
}

/* Le carrousel s'arrête pendant que la visionneuse est ouverte. On ne
   le voit plus, et chacun de ses mouvements obligerait à recalculer le
   flou (voir backdrop-filter plus haut). Pas animation: none, qui le
   ferait sauter au début : paused le fige, il repart d'où il était. */
body:has(.visionneuse[open]) .carrousel-piste {
  animation-play-state: paused;
}

/* ---------- La bande. ---------- */

.visionneuse-scene {
  list-style: none;
  margin: 0;

  /* Le script mesure la position de chaque photo depuis la bande
     (offsetLeft) : il faut pour ça que la bande soit « positionnée ». */
  position: relative;

  /* Les photos à la queue leu leu, centrées en hauteur. */
  display: flex;
  align-items: center;
  gap: var(--espace-l);

  /* Toute la rangée. min-height: 0 lève l'interdiction, pour un
     élément de grille, d'être plus petit que son contenu. */
  height: 100%;
  min-height: 0;

  /* La moitié de l'écran de marge intérieure de chaque côté : c'est ce
     qui permet à la première et à la dernière photo de venir au centre
     de l'écran, comme les autres. Sans ça la première resterait collée
     au bord gauche. */
  padding-inline: 50vw;

  /* La bande défile à l'horizontale, jamais à la verticale. */
  overflow-x: auto;
  overflow-y: hidden;

  /* scroll-snap : quand le défilement s'arrête, le navigateur le
     termine lui-même sur une photo centrée, jamais entre deux.
     mandatory : toujours, même après un tout petit geste. */
  scroll-snap-type: x mandatory;

  /* Arrivé au bout de la bande, le geste ne se propage pas à la page. */
  overscroll-behavior-x: contain;

  /* Pas de barre de défilement : la photo suivante qui dépasse sur le
     bord, et le compteur, suffisent à dire qu'il y a une suite. */
  scrollbar-width: none;
}

/* La même chose pour les navigateurs qui ne connaissent pas encore
   scrollbar-width (Safari). */
.visionneuse-scene::-webkit-scrollbar {
  display: none;
}

.visionneuse-vue {
  /* flex: 0 0 auto : ni rétrécie ni étirée, la largeur de son contenu. */
  flex: 0 0 auto;
  height: 100%;

  /* flex et non grid pour centrer la photo en hauteur : dans une
     grille, la rangée se dimensionne sur son contenu, et le
     max-height: 100% de la photo n'aurait rien de fixe pour se
     calculer — elle débordait de la bande. Ici la hauteur de la <li>
     est connue, et le pourcentage s'y rapporte. */
  display: flex;
  align-items: center;

  /* Le point où le défilement s'arrête : le centre de la photo au
     centre de l'écran. */
  scroll-snap-align: center;
}

.visionneuse-vue img {
  display: block;
  width: auto;
  height: auto;

  /* Aussi haute que la bande le permet, jamais plus large que l'écran
     moins deux marges. La proportion est conservée : la largeur suit. */
  max-height: 100%;
  max-width: calc(100vw - 4rem);
}

/* ---------- La légende : le nom de l'album et le compteur. ---------- */

.visionneuse-legende {
  margin: 0;
  display: flex;
  justify-content: center;
  gap: var(--espace-s);
  font-size: var(--texte-s);
  color: var(--couleur-texte-doux);
}

/* Album d'une seule photo : pas de compteur. visibility plutôt que
   display, pour que la légende garde sa hauteur. */
.visionneuse.seule .visionneuse-compteur {
  visibility: hidden;
}

/* La croix, dans le coin, sortie de la grille par position: absolute.
   z-index : toute <img> du site est en position: relative (pour le
   cadre gris des photos manquantes), et un élément positionné se
   dessine par-dessus ceux écrits avant lui dans le HTML ; une photo
   haute recouvrirait la croix. */
.visionneuse-fermer {
  position: absolute;
  top: var(--espace-m);
  right: var(--espace-m);
  z-index: 1;
}

/* ---------- L'indice de molette. ----------

   Une petite souris en haut au centre, dans la marge au-dessus des
   photos, à la hauteur de la croix. Sa molette (un point) descend et
   s'efface, puis recommence : le geste qu'on attend du visiteur.

   CACHÉ PAR DÉFAUT, montré plus bas aux seuls pointeurs précis, comme
   le curseur personnalisé : sur téléphone, on glisse au doigt, et une
   souris n'y voudrait rien dire. */
.visionneuse-indice {
  display: none;
  position: absolute;
  top: var(--espace-m);
  left: 50%;
  translate: -50% 0;

  /* La hauteur de la croix (3rem), le dessin centré dedans : les deux
     sont ainsi sur la même ligne. */
  height: 3rem;
  align-items: center;

  color: var(--couleur-accent);

  /* Un clic à cet endroit traverse le dessin et tombe sur le voile,
     qui ferme : comme partout ailleurs autour des photos. */
  pointer-events: none;

  /* Le fondu de sortie, quand la bande a bougé. Lent, comme le reste
     du site. */
  transition: opacity var(--duree-ample) var(--courbe);
}

.visionneuse-indice svg {
  display: block;
  width: 1.25rem;
  height: 1.875rem;
}

/* LE MOUVEMENT DE LA MOLETTE. 1,6 seconde par tour : à 2 secondes il
   paraissait un peu mou ; plus vite, un indice qui s'agite attire
   l'œil loin des photos.

   transform-box et transform-origin : le point doit s'étirer autour de
   SON centre. Sans ces deux lignes, un élément de SVG se transforme
   depuis le coin haut gauche du dessin entier, et en s'étirant le
   point partirait en biais vers le bas de la souris. */
.indice-molette {
  transform-box: fill-box;
  transform-origin: center;
  animation: molette 1.6s ease-in-out infinite;
}

/* Le point apparaît en haut, rond. Il démarre doucement, accélère, et
   c'est là qu'il s'étire : plus haut que large, comme une goutte
   entraînée par sa vitesse (scale(0.82, 1.45) : 82 % de sa largeur,
   145 % de sa hauteur). Il freine en arrivant, redevient rond à
   l'arrêt, puis s'efface. Ensuite, une courte pause invisible avant de
   recommencer : un mouvement continu s'ignore, un mouvement qui
   reprend se remarque.

   L'étirement suit la vitesse parce que chaque moitié de la descente
   a sa propre courbe : ease-in jusqu'au milieu (le point accélère,
   l'étirement grandit), ease-out ensuite (le point freine, il se
   rétracte). animation-timing-function, écrite DANS une étape, règle
   le trajet de cette étape jusqu'à la suivante.

   7px : dans ce SVG, une unité du dessin vaut un pixel (20 × 30 unités
   affichées en 20 × 30 px). Le point part de 8 et s'arrête à 15, au
   milieu de la souris. */
@keyframes molette {
  0%   { transform: translateY(0)     scale(1);          opacity: 0; }
  10%  { transform: translateY(0)     scale(1);          opacity: 1; }
  20%  { transform: translateY(0)     scale(1);          opacity: 1;
         animation-timing-function: ease-in; }
  40%  { transform: translateY(3.5px) scale(0.82, 1.45); opacity: 1;
         animation-timing-function: ease-out; }
  60%  { transform: translateY(7px)   scale(1);          opacity: 1; }
  80%  { transform: translateY(7px)   scale(1);          opacity: 0; }
  100% { transform: translateY(7px)   scale(1);          opacity: 0; }
}

/* Une fois la bande partie de la première photo, l'indice a fait son
   travail : il s'efface. La molette s'arrête aussi, là où elle est
   (paused et non none, qui la ferait sauter pendant le fondu) : une
   animation qui tourne derrière le flou de la visionneuse oblige le
   navigateur à le recalculer à chaque image (voir backdrop-filter).
   Album d'une seule photo : rien à faire défiler, pas d'indice. */
.visionneuse.defilee .visionneuse-indice,
.visionneuse.seule .visionneuse-indice {
  opacity: 0;
}

.visionneuse.defilee .indice-molette,
.visionneuse.seule .indice-molette {
  animation-play-state: paused;
}

/* Les pointeurs précis : souris, pavé tactile. Même condition que pour
   le curseur personnalisé. */
@media (hover: hover) and (pointer: fine) {

  .visionneuse-indice {
    display: flex;
  }

}

/* Sur téléphone, les photos se serrent. 4,5rem de moins que l'écran et
   non 2 : la marge ainsi libérée, plus l'espace entre deux photos,
   laisse dépasser une vingtaine de pixels de la suivante sur le bord
   droit. C'est ce bord qui dit qu'on peut glisser ; sans lui, seul le
   compteur le disait. */
@media (max-width: 45rem) {

  .visionneuse-scene {
    gap: var(--espace-s);
  }

  .visionneuse-vue img {
    max-width: calc(100vw - 4.5rem);
  }

}

@media (prefers-reduced-motion: reduce) {

  .visionneuse,
  .visionneuse-indice {
    transition: none;
  }

  /* L'indice reste affiché, immobile : la souris et sa molette se
     comprennent sans le mouvement. On coupe le mouvement, jamais
     l'affichage. */
  .indice-molette {
    animation: none;
  }

}


/* ============================================================
   LE CURSEUR — un anneau doré qui suit la souris avec un léger
   retard et s'élargit au-dessus de ce qui est cliquable.
   js/curseur.js le déplace et lui pose des classes ; toute
   l'apparence est ici.
   ============================================================ */

/* MASQUÉS PAR DÉFAUT, rallumés plus bas pour les pointeurs précis. Ce
   sens-là compte : on part de « rien » et on ajoute pour la souris.
   L'inverse laisserait passer tous les appareils non prévus — tablette
   à stylet, portable tactile, navigateur qui répond mal. Le doute
   profite à l'absence de curseur, jamais à sa présence. */
.curseur,
.curseur-point {
  display: none;
}

.curseur {
  /* fixed : positionné par rapport à la fenêtre, il ne défile pas. */
  position: fixed;

  /* Le point d'origine. Le JavaScript ne touche qu'à translate(), bien
     plus fluide que top et left : une transformation n'oblige pas à
     recalculer la mise en page. */
  top: 0;
  left: 0;

  width: 2rem;
  height: 2rem;
  border: 1px solid var(--couleur-accent);
  border-radius: 50%;

  /* Le fond doré est l'état NORMAL et disparaît au survol — l'inverse
     de l'usage, et c'est mieux ici : au repos l'anneau a du corps et se
     repère sur n'importe quelle photo ; au-dessus d'un élément cliquable
     il se vide et laisse voir. L'effet s'efface quand l'image compte. */
  background-color: var(--voile-accent);

  /* Sans ça, l'anneau intercepterait tous les clics : il est en
     permanence sous le pointeur. */
  pointer-events: none;

  z-index: 100;

  /* Masqué tant que le JavaScript n'a pas reçu un premier mouvement,
     sinon l'anneau apparaîtrait dans le coin au chargement. */
  opacity: 0;

  /* Son propre calque, au lieu de repeindre à chaque image tout ce qui
     est dessous — ici les photos du carrousel. */
  will-change: transform;

  /* INDISPENSABLE depuis que le grossissement passe par scale(). Par
     défaut transform-origin vaut le centre, et le navigateur enrobe la
     transformation : décalage vers le centre, application, décalage
     inverse. Sans effet sur un déplacement, mais combiné au scale() ça
     décale l'anneau de 8px au survol — mesuré : centre à 992 pour une
     souris à 1000. 0 0 remet l'origine au coin, et le recentrage par
     translate(-50%, -50%) fait son travail à toutes les tailles. */
  transform-origin: 0 0;

  /* PLUS DE TRANSITION SUR width NI height : c'était la cause du
     ralentissement, animer une largeur fait recalculer la mise en page
     entière à chaque image. Mesuré sur 120 mouvements : 911 avec, 9 sans.
     L'agrandissement passe par scale() dans js/curseur.js.

     background-color reste : une couleur ne repeint que l'anneau. */
  transition:
    opacity var(--duree-transition),
    background-color var(--duree-ample) var(--courbe);
}

.curseur.actif {
  opacity: 1;
}

/* Au-dessus d'un lien ou d'une photo : le retour visuel qui dit « ici,
   il se passe quelque chose ».

   L'agrandissement n'est plus décrit ici — la taille passe de 2rem à
   3rem par un scale(1.5) posé par le JavaScript, constante
   GROSSISSEMENT dans js/curseur.js.

   3rem et non 4.5rem, décidé plus tôt : à 4.5rem l'anneau devenait une
   tache qui masquait la photo regardée, l'inverse du but. Un retour
   visuel doit se remarquer, pas s'imposer. */
.curseur.survol {
  /* Le fond s'efface : l'anneau devient un simple contour, et on voit
     à travers. */
  background-color: transparent;
}


/* Le point doré : un SECOND élément et non un pseudo-élément de
   l'anneau, et c'est toute l'astuce — dessiné dedans, il hériterait de
   son retard. Séparé, il colle à la souris pendant que l'anneau garde le
   sien. L'œil voit une réponse instantanée et, par-dessus, un mouvement
   souple : le retard devient un effet au lieu d'une lenteur. */

.curseur-point {
  position: fixed;
  top: 0;
  left: 0;

  width: 6px;
  height: 6px;
  border-radius: 50%;
  background-color: var(--couleur-accent);

  pointer-events: none;

  /* Au-dessus de l'anneau, qui est à 100. */
  z-index: 101;

  opacity: 0;

  /* Même raison que pour l'anneau : son propre calque, pour ne pas
     forcer le repeint de ce qu'il survole à chaque image. */
  will-change: transform;

  /* AUCUNE transition sur la position : la moindre transition sur
     transform réintroduirait le retard qu'on cherche à supprimer. Seule
     l'opacité est animée, au moment de l'apparition. */
  transition: opacity var(--duree-transition);
}

.curseur-point.actif {
  opacity: 1;
}

/* hover: hover → l'appareil sait survoler. pointer: fine → le pointeur
   est précis, pas un doigt. Sur téléphone les deux sont fausses, ce bloc
   est ignoré, les éléments restent en display: none et le doigt retrouve
   un site normal. */
@media (hover: hover) and (pointer: fine) {

  .curseur,
  .curseur-point {
    display: block;
  }

  /* LE VRAI CURSEUR EST MASQUÉ PARTOUT. body { cursor: none } ne suffit
     pas, c'est le piège : chaque type d'élément a son curseur par
     défaut, qui l'emporte sur celui hérité du body — main sur un lien,
     barre en I sur un champ. Sans le sélecteur universel, le vrai
     curseur réapparaît exactement là où on regarde.

     Le prix à payer : la barre en I est le seul indice qui dit « ce
     texte est sélectionnable ». Il le reste, il ne l'annonce plus.

     SAUF SI LA MACHINE NE SUIT PAS. js/curseur.js pose alors la classe
     .curseur-lent sur le body, et ces deux sélecteurs cessent purement
     et simplement de correspondre. On ne remet pas « cursor: auto »
     par-dessus — ce serait pire, ça écraserait la main des liens et la
     barre en I des champs par une flèche uniforme. En empêchant la
     règle de s'appliquer, chaque élément retrouve exactement le
     curseur que le navigateur lui donne. */
  body:not(.curseur-lent),
  body:not(.curseur-lent) * {
    cursor: none;
  }

  /* Le point doré disparaît, l'anneau reste. Le point n'existait que
     pour donner la sensation de réponse immédiate ; la flèche du
     système fait ce travail bien mieux, et les deux superposés au même
     endroit ne feraient que se gêner. L'anneau, lui, garde tout son
     sens : il suit derrière, et c'est maintenant un effet assumé
     plutôt qu'un retard subi. */
  body.curseur-lent .curseur-point {
    display: none;
  }

}


/* ============================================================
   LES PAGES DE TEXTE — mentions légales, confidentialité, 404.

   Une seule colonne étroite : un texte de loi sur toute la largeur
   d'un 27 pouces est illisible, l'œil se perd en revenant à la ligne.
   On reprend la largeur de confort définie en haut du fichier.
   ============================================================ */

.page-texte {
  max-width: var(--largeur-texte);
  margin-inline: auto;
  padding-inline: var(--espace-m);
  padding-block: var(--espace-xl);
}

.page-texte h1 {
  /* Plus petit que le nom de l'accueil : c'est un titre de document,
     pas une enseigne. */
  font-size: var(--texte-l);
  margin-block-end: var(--espace-l);
}

.page-texte h2 {
  font-size: var(--texte-m);
  margin-block-start: var(--espace-l);
  margin-block-end: var(--espace-s);
}

.page-texte p + p,
.page-texte h2 + p {
  margin-block-start: var(--espace-s);
}

/* Le résumé de la page de confidentialité : l'essentiel pour qui ne
   lira pas la suite, c'est-à-dire la majorité des gens. */
.page-texte .resume {
  font-family: var(--police-titre);
  font-style: italic;
  font-weight: 500; /* même raison que .contact-intro */
  font-size: var(--texte-m);
  color: var(--couleur-texte);
  margin-block-end: var(--espace-l);
}


/* ---------- La page d'erreur. ---------- */

.page-erreur {
  /* Centrée verticalement dans ce qui reste sous l'en-tête : une
     page d'erreur avec un grand vide sous elle a l'air cassée. */
  min-height: calc(100svh - var(--hauteur-entete) - 12rem);
  display: flex;
  flex-direction: column;
  justify-content: center;
}

.code-erreur {
  font-size: var(--texte-xs);
  letter-spacing: 0.22em;
  text-transform: uppercase;
  color: var(--couleur-accent);
  margin-block-end: var(--espace-s);
}

.cta-ligne {
  margin-block-start: var(--espace-l);
}


/* ============================================================
   L'APPEL À L'ACTION — un seul bouton sur tout le site, délibéré.

   Un visiteur ne retient pas cinq actions possibles, il en retient
   une. Si tout a l'apparence d'un bouton, rien n'est prioritaire et
   il choisit de ne rien faire. Tous les autres liens restent des
   liens : c'est cette retenue qui donne au bouton son poids.
   ============================================================ */

.cta {
  display: inline-block;

  /* CES TROIS LIGNES NEUTRALISENT L'HABILLAGE DU NAVIGATEUR, parce que
     cette classe sert à la fois sur un <a> et sur un <button>. Un <a>
     n'a aucune apparence propre ; un <button> arrive avec un costume
     d'usine — fond gris, bordure en relief, et SA police, qu'il n'hérite
     jamais de la page. D'où un bouton de formulaire en Arial sur fond
     gris au milieu d'un site en Inter.

     appearance: none coupe en plus les coins arrondis et dégradés d'iOS,
     qu'aucune autre propriété n'atteint. */
  appearance: none;
  background-color: transparent;
  font-family: var(--police);

  /* Une bordure fine plutôt qu'un aplat : un bouton plein et doré sur
     fond sombre crie, un contour propose. Ici on propose. */
  border: 1px solid var(--couleur-accent);
  padding: var(--espace-s) var(--espace-l);

  color: var(--couleur-accent);
  text-decoration: none;

  /* Les capitales espacées le rattachent à la navigation et aux numéros
     de section : la famille des éléments qui commandent. */
  font-size: var(--texte-xs);
  font-weight: 500;
  letter-spacing: 0.18em;
  text-transform: uppercase;

  transition:
    background-color var(--duree-ample) var(--courbe),
    color var(--duree-ample) var(--courbe),
    border-color var(--duree-ample) var(--courbe);
}

/* Au survol, le bouton ne s'inverse plus. Il se remplissait de doré
   avec un texte sombre : franc mais brutal, le seul élément qui criait
   sur un site tenu par la retenue. Trois réglages discrets suffisent —
   voile léger, contour et texte éclaircis.

   Le voile reprend la valeur du fond de l'anneau, 0.12 : deux éléments
   qui disent « ici, tu peux agir » doivent le dire pareil. */
.cta:hover,
.cta:focus-visible {
  background-color: var(--voile-accent);
  border-color: var(--couleur-accent-clair);
  color: var(--couleur-accent-clair);
  text-decoration: none;
}


/* ============================================================
   LE BOUTON DE RETOUR EN HAUT — il reprend l'allure de l'anneau du
   curseur : même rond, même contour doré, même voile à 0.12. Les deux
   flottent au-dessus de la page sans appartenir au contenu, ils
   doivent se reconnaître comme appartenant à la même couche.
   ============================================================ */

/* LE BOUTON ROND, partagé : le retour en haut, et dans la visionneuse
   les deux flèches et la croix. Un seul dessin pour les quatre : un
   cercle, un contour doré, un voile doré léger, une icône au centre. */
.bouton-rond {
  width: 3rem;
  height: 3rem;
  border-radius: 50%;

  border: 1px solid var(--couleur-accent);
  background-color: var(--voile-accent);
  color: var(--couleur-accent);

  display: grid;
  place-items: center;

  transition:
    background-color var(--duree-ample) var(--courbe),
    border-color var(--duree-ample) var(--courbe),
    color var(--duree-ample) var(--courbe);
}

.bouton-rond svg {
  width: 1.125rem;
  height: 1.125rem;
  display: block;
}

/* Au survol, le voile s'efface et le contour s'éclaircit — exactement
   comme l'anneau du curseur et le bouton d'envoi. Une seule façon de
   répondre sur tout le site. */
.bouton-rond:hover,
.bouton-rond:focus-visible {
  background-color: transparent;
  border-color: var(--couleur-accent-clair);
  color: var(--couleur-accent-clair);
}

.bouton-rond:focus-visible {
  outline: 2px solid var(--couleur-accent-clair);
  outline-offset: 3px;
}

/* Ce qui est propre au retour en haut : sa place, et son apparition. */
.retour-haut {
  /* fixed : il reste à sa place pendant qu'on défile. */
  position: fixed;
  left: var(--espace-l);
  bottom: var(--espace-l);

  /* Sous le curseur, qui occupe 100 et 101, et sous la visionneuse
     (99). 98 avait été choisi pour passer sous le voile de grain,
     retiré depuis (voir plus bas) ; la valeur reste bonne, elle place
     le bouton au-dessus du contenu. */
  z-index: 98;

  /* MASQUÉ PAR DÉFAUT. Le JavaScript ajoute .visible après un écran
     et demi de défilement. Il est aussi retiré du parcours clavier
     tant qu'il est invisible : sans quoi la touche Tab s'arrêterait
     sur un bouton que personne ne voit. */
  opacity: 0;
  visibility: hidden;
  transform: translateY(0.75rem);

  /* La liste complète est redite : une transition écrite ici remplace
     celle du bouton rond, elle ne s'y ajoute pas. */
  transition:
    opacity var(--duree-ample) var(--courbe),
    visibility var(--duree-ample) var(--courbe),
    transform var(--duree-ample) var(--courbe),
    background-color var(--duree-ample) var(--courbe),
    border-color var(--duree-ample) var(--courbe),
    color var(--duree-ample) var(--courbe);
}

.retour-haut.visible {
  opacity: 1;
  visibility: visible;
  transform: none;
}

/* LES DEUX BOUTONS FIXES, retour en haut et thème, flottent au-dessus
   des photos. Le voile doré de .bouton-rond est presque transparent :
   sur une photo de la même teinte que leur contour, ils disparaissaient
   (bronze sur photo sombre en clair, doré sur photo claire en sombre).
   Ils reçoivent donc un fond plein, la couleur du site, avec le voile
   doré peint par-dessus en dégradé uni. Au survol, on garde le fond et
   on retire seulement le voile, comme les autres boutons ronds. */
.retour-haut,
.bascule-theme {
  background-color: var(--couleur-fond);
  background-image: linear-gradient(var(--voile-accent), var(--voile-accent));
}

.retour-haut:hover,
.retour-haut:focus-visible,
.bascule-theme:hover,
.bascule-theme:focus-visible {
  background-color: var(--couleur-fond);
  background-image: none;
}

/* LE BOUTON DE THÈME. En bas à droite, le pendant du retour en haut en
   bas à gauche : même dessin, même hauteur, les deux encadrent l'écran.
   Lui reste toujours visible, puisqu'on peut vouloir changer de thème
   dès l'arrivée. */
.bascule-theme {
  position: fixed;
  right: var(--espace-l);
  bottom: var(--espace-l);
  z-index: 98;
}

/* Caché tant que js/theme.js ne l'a pas activé : sans JavaScript il ne
   pourrait rien faire. La règle est nécessaire parce que display: grid,
   posé par .bouton-rond, l'emporte sur le hidden du HTML. */
.bascule-theme[hidden] {
  display: none;
}

/* Deux icônes dans le bouton, une seule visible : celle du thème vers
   lequel on va. Le soleil en sombre, la lune en clair. */
.bascule-theme .icone-lune {
  display: none;
}

:root:not([data-theme="sombre"]) .bascule-theme .icone-soleil {
  display: none;
}

:root:not([data-theme="sombre"]) .bascule-theme .icone-lune {
  display: block;
}

/* Sur téléphone, il se rapproche du bord : l'écran est étroit et
   chaque centimètre compte. */
@media (max-width: 45rem) {

  .retour-haut {
    left: var(--espace-m);
    bottom: var(--espace-m);
  }

  .bascule-theme {
    right: var(--espace-m);
    bottom: var(--espace-m);
  }

}


/* ============================================================
   LE GRAIN — RETIRÉ, ET POURQUOI

   Il y avait ici un body::after : un voile de bruit à 3,5 % d'opacité,
   fixed et inset: 0, donc couvrant l'écran entier par-dessus la page.
   Supprimé pour une raison mesurée, pas par goût.

   Un calque plein écran semi-transparent oblige le navigateur à
   re-mélanger toute sa surface dès que quelque chose bouge dessous. Or
   le curseur personnalisé bouge en permanence, et sans accélération
   matérielle c'est le processeur qui fait ce mélange — sur 2560×1440,
   il n'y arrive pas.

   Mesuré dans Brave en rendu logiciel, trois relevés :
       avec le grain   21.9 / 22.3 / 17.8 ms  (~45 fps)
       sans le grain   14.8 / 15.5 / 15.5 ms  (~65 fps)
   Et surtout les images sous 30 fps passent de 17 à 4.

   DEUX FAUSSES PISTES, pour ne pas les reprendre : carreler le bruit en
   tuile de 180px ne change rien (22.4 / 23.2 / 21.1), et le promouvoir
   en calque propre non plus. Le coût n'est pas de FABRIQUER le bruit,
   calculé une fois puis mis en cache, mais de mélanger la couche à
   chaque image.

   S'IL DOIT REVENIR : jamais en voile plein écran au-dessus de tout,
   mais en background-image sur les sections, sous le contenu.
   ============================================================ */


/* ============================================================
   LA RÉVÉLATION AU DÉFILEMENT

   Ces deux règles ne s'appliquent QUE si le JavaScript a posé la
   classe. Sans lui, le contenu reste visible. On ne cache jamais du
   contenu en pariant qu'un script viendra le révéler — c'est la faute
   classique de ce procédé, et elle rend des sites entiers vides.
   ============================================================ */

.a-reveler {
  opacity: 0;

  /* 3rem et non 2, passé en même temps que la durée s'allongeait : un
     mouvement lent sur la même distance paraît mou, sur une distance
     plus longue il paraît ample. C'est la vitesse perçue qui compte. */
  transform: translateY(3rem);

  /* 1800ms. Le freinage très long de la courbe fait parcourir
     l'essentiel du trajet dans le premier tiers du temps : l'élément
     n'a pas l'air lent, il a l'air de se poser. */
  transition:
    opacity 1800ms var(--courbe),
    transform 1800ms var(--courbe);
}

.a-reveler.revelee {
  opacity: 1;
  transform: none;
}


/* ============================================================
   MOUVEMENT RÉDUIT — LA SUITE

   Tout ce qui précède doit pouvoir être coupé. On complète donc la
   règle écrite au bloc 3.
   ============================================================ */

@media (prefers-reduced-motion: reduce) {

  /* Les apparitions sont supprimées, pas accélérées : le contenu est
     simplement là, tout de suite. */
  .accueil-texte h1,
  .accueil-signature,
  .accroche,
  .carrousel {
    animation: none;
  }

  /* Le zoom au survol disparaît. La luminosité, elle, est conservée :
     ce n'est pas un mouvement, c'est un changement de couleur, et il
     reste utile pour savoir où on se trouve. */
  .galerie figure:hover img {
    transform: none;
  }

  /* Le curseur RESTE VISIBLE : remplacer la flèche par un anneau est un
     choix d'apparence, pas une animation. Ce réglage supprime le
     mouvement ajouté, pas les éléments. Seule la traîne disparaît — on
     ne garde que la transition d'opacité, et le déplacement est
     neutralisé côté JavaScript par la constante SOUPLESSE.

     Il y avait ici body { cursor: auto }, qui ne servait à rien : body
     ne désigne que l'élément body, jamais son contenu, et body * plus
     haut l'emportait. Le vrai curseur restait masqué pendant que
     l'anneau était supprimé — plus aucun curseur nulle part. */
  .curseur {
    transition: opacity var(--duree-transition);
  }

}


/* ---------- Le reste de la page sur téléphone. ---------- */

@media (max-width: 45rem) {

  /* Deux photos côte à côte sur 390px, on ne voit plus rien. */
  .galerie {
    columns: 1;
  }

  /* Le portrait passe au-dessus du texte. */
  #a-propos {
    grid-template-columns: 1fr;
    gap: var(--espace-m);
  }

  /* ANNULE l'assignation en colonne 2 du grand écran : demander la
     colonne 2 dans une grille qui n'en a qu'une en fabriquerait une
     seconde hors de l'écran, et tout le texte partirait à droite. */
  #a-propos img,
  #a-propos .a-propos-texte {
    grid-column: 1;
  }

  /* Le portrait n'est plus bridé par sa colonne : on lui remet une
     limite directement, sinon il occuperait toute la largeur d'une
     tablette. Sur un téléphone, l'écran est plus étroit que 24rem :
     c'est alors width: 100% qui décide. */
  #a-propos img {
    max-width: 24rem;
  }

}
