/* =====================================================================
   PRTL — Lucien Parent-Santarelli
   Feuille de style unique.

   Organisation :
     01. Tokens de design   ← 95% de tes réglages se font ici
     02. Reset & base
     03. Header + navigation
     04. Bouton losange
     05. Indicateur de scroll
     06. Hero
     07. Section Works
     08. Footer
     09. Responsive
     10. Accessibilité / motion
   ===================================================================== */


/* ============================ 01. TOKENS ============================ */
:root {
    /* --- Couleurs --- */
    --paper: #ffffff; /* fond */
    --ink: #101010; /* texte principal */
    --ink-soft: #4a4a4a; /* texte secondaire */
    --ink-faint: #6b6b6b; /* texte tertiaire (contraste AA respecté) */
    --rule: #101010; /* filets verticaux */
    --line: #e2e2e2; /* séparateurs */
    --rail-track: #d6d6d6; /* fond de la barre de scroll */
    --rail-thumb: #101010; /* curseur de la barre de scroll */
  --rail-w:       2px;     /* épaisseur du trait au repos */
  --rail-w-hover: 6px;     /* épaisseur au survol et pendant la saisie */
  --rail-hit:     18px;    /* largeur de la zone de préhension invisible */
    /* --- Typographie ---
     `futura-pt` est le nom que donne Adobe Fonts à la famille : il doit
     correspondre exactement à celui affiché dans ton projet web.
     Jost, derrière, sert de filet de secours — c'est une géométrique
     très proche, donc une panne d'Adobe ne déformerait pas la mise en
     page. Une seule ligne à changer pour basculer sur autre chose. */
    --font: "futura-pt", "Futura PT", "Jost", "Futura", system-ui, sans-serif;
    /* Les plafonds sont volontairement hauts : sur un moniteur large, une
     valeur basse fige le site à sa taille « portable » et le design se
     retrouve perdu au milieu du vide. */
    --fs-nav: clamp(0.66rem, 0.60rem + 0.22vw, 1.05rem);
    --fs-logo: clamp(0.72rem, 0.64rem + 0.26vw, 1.20rem);
    /* Le texte d'intro suit la largeur de l'écran (0.98vw) au lieu de rester
     figé : c'est ce qui permet aux deux phrases de tenir chacune sur une
     ligne, comme dans le mockup. Les bornes évitent l'illisible et le géant. */
    --fs-intro: clamp(0.86rem, 1.01vw, 1.88rem);
    --intro-width: 58vw; /* élargi pour absorber le corps relevé */
    --indent: 2.6em; /* décalage des blocs Interests / Currently */
    /* Suit la même échelle que l'intro : dans le mockup, l'intro et les
     bullets font quasiment la même taille. */
    --fs-body: clamp(0.88rem, 0.99vw, 1.83rem);
    --fs-title: clamp(1.75rem, 1.20rem + 2.40vw, 3.60rem);
    --track-nav: 0.24em; /* interlettrage nav */
    --track-logo: 0.36em; /* interlettrage logo */
    /* --- Rythme spatial --- */
    --gutter: clamp(22px, 5.4vw, 160px); /* marge latérale du contenu */
    --header-h: clamp(64px, 9vh, 128px);
    --header-fade: 210%; /* hauteur du dégradé du header, en % de --header-h */
  --footer-fade: 150%;   /* même principe, en bas de page */

  /* Curseur personnalisé */
  --cursor-size:  15px;
  --cursor-grow:  1.5;   /* facteur d'agrandissement sur un élément cliquable */

  /* Titre à lettres flottantes (page Piñata).
     Garde --float-amp très bas : au-delà de 0.1em le titre cesse de
     flotter et se met à sautiller. */
  --float-amp:     -0.055em;  /* négatif = vers le haut */
  --float-speed:   3.6s;
  --float-stagger: 0.13s;     /* décalage entre deux lettres */

  /* Révélation foil sur les titres */
  --foil-size:   clamp(120px, 14vw, 340px);  /* diamètre de la zone révélée */
  --foil-speed:  9s;                         /* dérive des couleurs */

  /* Page About — marges relevées sur le mockup.
     Le contenu est volontairement plus rentré que le header (8,4% contre
     5,4%), et la marge droite plus courte pour que la pointe du losange
     approche le bord de page. */
  --about-left:  clamp(24px, 8.4vw, 250px);
  --about-right: clamp(16px, 2.4vw, 70px);
  --about-title-gap: -0.1em;   /* écart titre → intro (négatif = resserré) */

  /* Descente de TOUT le bloc sous le titre : citation, paragraphes,
     filet vertical et bloc Experience. En rem plutôt qu'en em — le
     réglage reste prévisible, là où un em du titre vaudrait déjà 17px. */
  --about-body-offset: 0.9rem;

  /* Cadre losange de la page About.
     À 100%, il remplit exactement sa colonne de grille — c'est elle qui
     le dimensionne, plus une valeur en vw à resynchroniser à la main. */
  --frame-size:   100%;
  --frame-inset:  16px;              /* décalage de la photo (voir note) */
  --frame-pos:    50% 38%;           /* cadrage : remonte sur le visage */
  --frame-scale:  1;                 /* zoom dans le losange */

  /* Déplacement de la photo À L'INTÉRIEUR du losange, qui lui ne bouge
     pas. Négatif = vers la gauche / vers le haut.

     ⚠ Décaler découvre le bord opposé. Une valeur de N% demande donc un
     --frame-scale d'au moins 1 + 2×N/100, sans quoi le fond noir
     apparaîtrait sur un côté. Pour -6%, compte 1.12. */
  --frame-shift-x: 0%;
  --frame-shift-y: 0%;
  --frame-sat:    1.7;               /* densité des couleurs du liseré */
  --frame-bright: 0.88;

  /* Couleur d'accent, pour les passages saillants de la page About. */
  --accent: #c8102e;

  /* --- Pages projet --- */
  --phero-scrim:  0.55;              /* assombrissement du fond */
  --phero-pos:    center;            /* cadrage du fond */
  --phero-tool-h: clamp(30px, 3.2vw, 54px);  /* hauteur des logos d'outils */
  --phero-width:  54rem;             /* largeur du bloc de texte du hero */
  --btn-ring:     2px;               /* épaisseur du contour des boutons */
  --pembed-ring:  3px;               /* liseré autour d'un document intégré */
  --gallery-ring: 3px;               /* liseré des images de mosaïque */
    /* --- Hero : cadrage du média ---
     Ces 4 valeurs contrôlent la position du rectangle vidéo.
     Le mockup correspond à : top 14%, bottom 8%, right gutter+34px, left 30%. */
    --media-top: 5%;
    --media-bottom: 8%;
    --media-right: calc(var(--gutter) + 34px);
    --media-left: 10%;
    /* --- Hero : forme du fondu ---
     Le dégradé est ancré sur le coin inférieur droit : c'est le seul
     point pleinement opaque, et l'image s'efface en s'en éloignant.
     --fade-size est le rayon de l'ellipse, en % du bloc média. À 100%,
     l'arc passe exactement par les coins bas-gauche et haut-droit ;
     au-delà, la zone visible s'élargit. */
    --fade-x: 100%;
    --fade-y: 100%;
    --fade-size: 110%;
    /* Opacité d'ensemble du média du hero, fondu compris.
     1 = tel quel, 0.6 = image nettement plus discrète.
     À ne pas confondre avec les paliers du fondu (voir .hero__media) :
     ceci agit sur TOUT le bloc, y compris sa zone la plus nette. */
    --media-opacity: .6;
    /* --- Grille Works --- */
    --work-row: clamp(170px, 18vw, 420px); /* hauteur d'une rangée */
    --work-gap: clamp(12px, 1.5vw, 40px); /* écart entre les blocs */
    --work-pad: clamp(16px, 1.7vw, 48px); /* marge intérieure */
    --work-logo: clamp(44px, 4.6vw, 110px); /* côté de la plaque logo */
    --work-placeholder: #d9d9d9; /* gris avant les images */
  --work-text:        #ffffff; /* texte des blocs, sur fond image */
    /* Balayage diagonal de la vidéo au survol */
    --wipe: 620ms;
    --wipe-ease: cubic-bezier(0.42, 0, 0.18, 1);
    /* Voiles des blocs Works.
     light = état repos (calque blanc, texte noir)
     dark  = état survol (calque noir, texte blanc)
     Surchargeables bloc par bloc : style="--scrim-idle: 0.85" */
  --scrim-idle:   0.78;   /* force des bandeaux au repos */
  --scrim-hover:  0.55;   /* force au survol : l'image s'ouvre */
  --scrim-top:    38%;    /* hauteur du bandeau haut, en % du bloc */
  --scrim-bottom: 52%;    /* hauteur du bandeau bas */
    /* Cadrage des visuels dans les blocs. Valeurs possibles : center,
     top, bottom, left, right, ou un couple « 50% 80% ».
     Surchargeable bloc par bloc : style="--work-image-pos: bottom" */
    --work-image-pos: 80% 80%;
    /* Zoom du visuel dans le bloc. 1 = cadrage normal, 1.25 = +25%.
     Le zoom s'effectue autour du point défini par --work-image-pos :
     le bord que tu as calé reste donc calé.
     Reste au-dessus de 1 — en dessous, l'image rétrécit et laisse
     apparaître le gris de remplissage sur les côtés. */
    --work-image-scale: 1;

  /* Flou de l'image au repos, levé au survol. Surchargeable bloc par
     bloc : style="--work-blur: 10px" */
  --work-blur: 0px;

  /* Inclinaison 3D des blocs au survol.
     --tilt-max au-delà de 6deg, l'effet cesse d'être subtil et le texte
     commence à se déformer visiblement. */
  --tilt-max:    1deg;     /* angle maximal, aux coins */
  --tilt-depth:  900px;    /* profondeur : plus bas = perspective plus forte */
  --tilt-follow: 130ms;    /* réactivité du suivi pendant le survol */
  --tilt-return: 550ms;    /* retour à plat en sortie */

  /* Liseré prismatique au survol.
     Les quatre réglages sont surchargeables bloc par bloc :
     style="--prism-w: 5px; --prism-bright: 0.7" */
  --prism-w:      5px;     /* épaisseur du trait */
  --prism-speed:  8s;    /* durée d'un cycle complet */
  --prism-sat:    3;       /* saturation : 1 = pastel, 2.5 = franc */
  --prism-bright: 0.6;       /* luminosité : 1 = clair, 0.6 = sombre */
  --prism-glow:   0px;     /* halo diffus autour du trait, 0 = aucun */

  /* --- Pavage des dégradés irisés ---

     LA cause des coupures nettes que tu voyais.

     Un `repeating-linear-gradient` produit une image de la taille de la
     zone de fond, puis cette image est répétée. Comme la taille de
     l'élément n'est presque jamais un multiple exact du motif, la
     jonction entre deux copies tombe au milieu d'une couleur : d'où une
     cassure diagonale franche, qui se déplace pendant l'animation.

     La parade : imposer une tuile dont la diagonale vaut EXACTEMENT un
     motif. Pour un dégradé à 135°, avancer d'une tuile fait progresser
     de côté ÷ √2 le long de l'axe. Il faut donc côté = motif × √2 :

         360 × 1,41421 = 509px

     Les tuiles se raccordent alors parfaitement, et un déplacement d'une
     tuile en diagonale reboucle sans coupure. */
  --foil-period: 360px;
  --foil-tile:   509px;

  /* Renfort de saturation suivant le curseur */
  --prism-glow-r:      240px;  /* rayon du renfort sur les blocs */
  --prism-glow-sat:    4.5;    /* saturation du renfort */
  --prism-glow-bright: 0.95;
  --frame-glow-r:      260px;  /* rayon du renfort sur le cadre About */
  --rule-glow:         200px;  /* rayon d'allumage des filets verticaux */
    --ease: cubic-bezier(0.22, 0.61, 0.36, 1);
}


/* ========================= 02. RESET & BASE ========================= */
*, *::before, *::after { box-sizing: border-box; }

html {
  scroll-behavior: smooth;
  scroll-padding-top: var(--header-h);
  scrollbar-width: thin;
  scrollbar-color: var(--rail-track) transparent;
}

/* Sur desktop, l'indicateur custom remplace la barre native : on masque
   cette dernière pour ne pas en afficher deux, comme dans le mockup.
   Molette, trackpad et clavier fonctionnent normalement ; seul le
   glisser-déposer de la barre disparaît. Supprime ce bloc pour la rendre. */
@media (min-width: 861px) {
  html { scrollbar-width: none; }
  html::-webkit-scrollbar { display: none; }
}

body {
  margin: 0;
  background: var(--paper);
  color: var(--ink);
  font-family: var(--font);
  font-size: var(--fs-body);
  font-weight: 400;
  line-height: 1.55;
  -webkit-font-smoothing: antialiased;
  text-rendering: optimizeLegibility;
  overflow-x: hidden;
}

h1, h2, h3, p, ul, figure { margin: 0; }
ul { padding: 0; list-style: none; }
img, video { display: block; max-width: 100%; }

a {
  color: inherit;
  text-decoration: none;
  transition: opacity 0.25s var(--ease), color 0.25s var(--ease);
}

:focus-visible {
  outline: 2px solid var(--ink);
  outline-offset: 4px;
}

.skip-link {
  position: absolute;
  left: -9999px;
  top: 0;
  z-index: 200;
  background: var(--ink);
  color: var(--paper);
  padding: 0.8em 1.4em;
  font-size: var(--fs-nav);
  letter-spacing: var(--track-nav);
  text-transform: uppercase;
}
.skip-link:focus { left: 0; }

/* Filet vertical à gauche d'un bloc de texte (comme sur le mockup). */
.ruled {
  position: relative;
  padding-left: 1.15em;
}
.ruled::before {
  content: "";
  position: absolute;
  left: 0;
  top: 0.18em;
  bottom: 0.18em;
  width: 2px;
  background: var(--rule);
}

/* Doublure irisée du filet, révélée autour du curseur.

   Elle se superpose exactement au filet noir : là où le masque radial
   est opaque, on voit les couleurs ; ailleurs, le noir reste.

   L'atténuation horizontale est gratuite : le cercle est centré sur le
   curseur, qui se trouve à des centaines de pixels à droite du filet.
   Plus tu t'en éloignes, moins le cercle le recouvre. Le filet s'allume
   donc là où tu passes, et s'éteint quand tu t'en vas — sans qu'aucune
   distance ne soit calculée en JavaScript.

   Le dégradé est vertical (180deg) et non diagonal : sur un trait de
   2px de large, une diagonale ne montrerait qu'une seule couleur. */
.ruled::after {
  content: "";
  position: absolute;
  left: 0;
  top: 0.18em;
  bottom: 0.18em;
  width: 2px;
  pointer-events: none;

  /* Dégradé vertical : la tuile fait exactement un motif de haut, donc
     elle se répète sans coupure. Pas de √2 ici, l'axe est vertical. */
  background-image: repeating-linear-gradient(
    180deg,
    #ff0080   0px,
    #ff8c00  34px,
    #ffe600  68px,
    #00e676 102px,
    #00b0ff 136px,
    #7c4dff 170px,
    #ff0080 204px);
  background-size: 100% 204px;

  -webkit-mask-image: radial-gradient(
    circle var(--rule-glow) at var(--foil-x, -999px) var(--foil-y, -999px),
    #000 0%, rgba(0, 0, 0, 0.55) 45%, transparent 100%);
          mask-image: radial-gradient(
    circle var(--rule-glow) at var(--foil-x, -999px) var(--foil-y, -999px),
    #000 0%, rgba(0, 0, 0, 0.55) 45%, transparent 100%);

  animation: rule-drift 5s linear infinite;
}

@keyframes rule-drift {
  from { background-position: 0     0; }
  to   { background-position: 0 204px; }
}


/* ============================ 03. HEADER ============================ */
.site-header {
  position: fixed;
  inset: 0 0 auto 0;
  z-index: 100;
  height: var(--header-h);
  padding-inline: var(--gutter);

  /* Trois zones : logo à gauche, nav au centre, losange à droite.
     Les deux colonnes 1fr encadrent la colonne auto, ce qui centre la
     nav sur la PAGE et non entre ses deux voisins — donc son centrage
     ne bouge pas si le logo ou le losange change de largeur. */
  display: grid;
  grid-template-columns: 1fr auto 1fr;
  align-items: center;
  gap: clamp(0.6rem, 2vw, 2rem);

  background: transparent;
}

.site-header .logo    { justify-self: start; }
.site-header .nav     { justify-self: center; }
.site-header .diamond { justify-self: end; }

/* Variante des pages projet : le header n'est pas posé PAR-DESSUS le
   hero, il vit dans le flux, juste après lui. `sticky` le laisse
   défiler jusqu'en haut de l'écran puis l'y accroche — d'où l'absence
   de saut au moment où il s'y colle, contrairement à un basculement
   vers `fixed` en JavaScript.

   Son voile dégradé reste utile ici : il estompe le contenu qui passe
   dessous au lieu de le trancher net. */
.site-header--flow {
  position: sticky;
  inset: auto;
  top: 0;
}

/* Voile de lisibilité du header.
   Ce n'est pas un aplat : c'est un dégradé qui descend plus bas que le
   header lui-même et s'éteint progressivement. Résultat, la nav se
   détache sans qu'aucune ligne de séparation n'apparaisse.
   Règle la hauteur avec --header-fade : plus la valeur est grande, plus
   la transition est douce. */
.site-header::before {
  content: "";
  position: absolute;
  inset: 0 0 auto 0;
  height: var(--header-fade);
  z-index: -1;
  pointer-events: none;
  background: linear-gradient(
    to bottom,
    rgba(255, 255, 255, 0.94) 0%,
    rgba(255, 255, 255, 0.90) 34%,
    rgba(255, 255, 255, 0.62) 58%,
    rgba(255, 255, 255, 0.26) 79%,
    rgba(255, 255, 255, 0)   100%);
  opacity: 0.8;
  transition: opacity 0.4s var(--ease);
}

/* Posé par le JS après quelques pixels de scroll : on densifie un peu
   le voile, sans jamais créer d'arête. */
.site-header.is-stuck::before { opacity: 1; }

.logo {
  font-size: var(--fs-logo);
  font-weight: 500;
  letter-spacing: var(--track-logo);
  text-transform: uppercase;
  white-space: nowrap;
}
.logo:hover { opacity: 0.55; }

.nav {
  display: flex;
  align-items: center;
  gap: clamp(1.1rem, 3vw, 3.4rem);
}

.nav__link {
  position: relative;
  font-size: var(--fs-nav);
  font-weight: 500;
  letter-spacing: var(--track-nav);
  text-transform: uppercase;
  white-space: nowrap;
  padding-block: 0.5em;
}

/* --- Roulement vertical au survol ---

   Le mot glisse vers le haut, sa copie arrive par le bas, comme un
   compteur mécanique. Le JS construit les deux exemplaires au
   chargement : le HTML ne contient toujours qu'un seul mot, et la copie
   est masquée aux lecteurs d'écran pour ne pas être lue deux fois.

   Le masque est un élément à part et non le lien lui-même : le lien
   porte un padding vertical, et y mettre overflow:hidden laisserait
   dépasser le haut de la copie dans cette marge. */
.nav__mask {
  display: block;
  overflow: hidden;
}

.nav__roll {
  display: block;
  position: relative;
  transition: transform 0.42s var(--ease);
}

.nav__word { display: block; }

.nav__word--copy {
  position: absolute;
  inset: 100% 0 auto 0;   /* juste sous le mot d'origine */
}

/* Le mot qui arrive par le bas est irisé : le survol ne fait pas que
   déplacer le texte, il le transforme. Pas de suivi de curseur ici —
   sur un mot de nav, le roulement suffit à porter l'effet. */
@supports ((-webkit-background-clip: text) or (background-clip: text)) {
  .nav__word--copy {
    background-image: repeating-linear-gradient(
      135deg,
      #ff0080   0px,
      #ff8c00  60px,
      #ffe600 120px,
      #00e676 180px,
      #00b0ff 240px,
      #7c4dff 300px,
      #ff0080 360px);
    background-size: var(--foil-tile) var(--foil-tile);

    -webkit-background-clip: text;
            background-clip: text;
    -webkit-text-fill-color: transparent;
            color: transparent;

    animation: foil-drift var(--foil-speed) linear infinite;
  }
}

.nav__link:hover .nav__roll,
.nav__link:focus-visible .nav__roll { transform: translateY(-100%); }

/* Soulignement qui se déploie depuis la gauche. */
.nav__link::after {
  content: "";
  position: absolute;
  left: 0;
  bottom: 0.1em;
  width: 100%;
  height: 1px;
  background: currentColor;
  transform: scaleX(0);
  transform-origin: left;
  transition: transform 0.35s var(--ease);
}
.nav__link:hover::after,
.nav__link:focus-visible::after { transform: scaleX(1); }


/* ========================= 04. BOUTON LOSANGE ======================= */
.diamond {
  position: relative;
  flex: none;
  display: grid;
  place-items: center;
  width: clamp(34px, 3.4vw, 46px);
  aspect-ratio: 1;
}

/* Le carré pivoté à 45°. La rotation vient de la classe
   `diamond--spin`, posée sur le <a class="diamond"> du HTML : retire-la
   pour figer le losange. La durée se règle ci-dessous. */
/* Noir au repos. */
.diamond__shape {
  position: absolute;
  inset: 8%;
  background: var(--ink);
  transform: rotate(45deg);
  transition: transform 0.5s var(--ease);
}

.diamond--spin .diamond__shape {
  animation: diamond-spin 24s linear infinite;
}

/* Le foil est porté par un calque à part, et non par le fond du losange :
   une image de fond ne se transitionne pas, le passage au survol serait
   brutal. En fondant l'opacité d'un calque, on obtient un dégradé propre.

   Ce calque a sa PROPRE animation : la rotation du losange peut donc se
   figer au survol pendant que les couleurs, elles, continuent de dériver.
   Un seul `animation-play-state` sur l'élément parent n'aurait pas permis
   de dissocier les deux. */
.diamond__shape::after {
  content: "";
  position: absolute;
  inset: 0;

  background-image: repeating-linear-gradient(
    135deg,
    #ff0080   0px,
    #ff8c00  60px,
    #ffe600 120px,
    #00e676 180px,
    #00b0ff 240px,
    #7c4dff 300px,
    #ff0080 360px);
  background-size: var(--foil-tile) var(--foil-tile);

  opacity: 0;
  transition: opacity 0.35s var(--ease);
  animation: foil-drift var(--foil-speed) linear infinite;
}

.diamond:hover .diamond__shape::after,
.diamond:focus-visible .diamond__shape::after { opacity: 1; }

.diamond--spin .diamond__shape {
  animation: diamond-spin 24s linear infinite;
}

/* Le point d'interrogation reste droit et lisible. */
.diamond__mark {
  position: relative;
  z-index: 1;
  color: var(--paper);
  font-size: clamp(0.7rem, 1.1vw, 0.92rem);
  font-weight: 600;
  line-height: 1;
  transform: translateY(-1px);
  pointer-events: none;
}

/* Le grossissement est porté par le lien, pas par le carré.
   Une animation l'emporte toujours sur une déclaration `transform`
   ordinaire : mettre le scale sur .diamond__shape n'aurait eu aucun
   effet tant que la rotation tourne. */
.diamond {
  transition: transform 0.35s var(--ease);
}

.diamond:hover,
.diamond:focus-visible { transform: scale(1.14); }

/* La rotation se fige au survol, pour que le losange redevienne lisible
   au moment où on s'y intéresse. */
.diamond:hover .diamond__shape,
.diamond:focus-visible .diamond__shape { animation-play-state: paused; }

@keyframes diamond-spin {
  from { transform: rotate(45deg); }
  to   { transform: rotate(405deg); }
}


/* ===================== 05. INDICATEUR DE SCROLL ==================== */
/* La barre est saisissable. Elle est donc plus large qu'elle n'en a
   l'air : --rail-hit définit une zone de préhension transparente, et le
   trait visible (--rail-w) est dessiné centré dedans. Sans ça, il
   faudrait viser 2 pixels à la souris. */
.scrollrail {
  position: fixed;
  right: calc(var(--gutter) * 0.55 - var(--rail-hit) / 2 + 1px);
  top: 18vh;
  bottom: 9vh;
  z-index: 90;
  width: var(--rail-hit);
  background: none;
  touch-action: none;
}

/* La piste visible. */
.scrollrail::before {
  content: "";
  position: absolute;
  top: 0;
  bottom: 0;
  left: calc(50% - var(--rail-w) / 2);
  width: var(--rail-w);
  background: var(--rail-track);
  transition: width 0.2s var(--ease), left 0.2s var(--ease);
}

/* Épaississement au survol et pendant la saisie. */
.scrollrail:hover,
.scrollrail.is-dragging { --rail-w: var(--rail-w-hover); }

/* Hauteur = proportion visible de la page, position = avancement du scroll.
   Les deux sont calculées en JS. */
/* Centré via `left` et non via un translateX : le JS pilote déjà
   `transform` pour la position verticale, et les deux se marcheraient
   dessus. */
.scrollrail__thumb {
  position: absolute;
  top: 0;
  left: calc(50% - var(--rail-w) / 2);
  width: var(--rail-w);
  height: 30%;
  background: var(--rail-thumb);
  transition: width 0.2s var(--ease), left 0.2s var(--ease);
  will-change: transform, height;
}


/* ============================= 06. HERO ============================ */
.hero {
  position: relative;
  min-height: 100svh;
  display: grid;
  align-content: center;
  padding: calc(var(--header-h) + 2rem) var(--gutter) 8vh;
  overflow: hidden;
}

/* --- Le média --- */
.hero__media {
  position: absolute;
  top: var(--media-top);
  right: var(--media-right);
  bottom: var(--media-bottom);
  left: var(--media-left);
  z-index: 0;
  pointer-events: none;
  opacity: var(--media-opacity);

  /* Masque unique, purement radial, ancré sur le coin inférieur droit.
     Seul ce coin est pleinement visible ; l'image se dissout dans tous
     les autres, sur une frontière courbe et non sur une diagonale nette.
     Les paliers d'opacité sont resserrés dans la seconde moitié : c'est
     ce qui rend l'effacement progressif plutôt que net. */
  -webkit-mask-image:
    radial-gradient(var(--fade-size) var(--fade-size)
      at var(--fade-x) var(--fade-y),
      #000                    0%,
      rgba(0, 0, 0, 0.97)    28%,
      rgba(0, 0, 0, 0.80)    45%,
      rgba(0, 0, 0, 0.45)    62%,
      rgba(0, 0, 0, 0.15)    76%,
      transparent            88%);
  mask-image:
    radial-gradient(var(--fade-size) var(--fade-size)
      at var(--fade-x) var(--fade-y),
      #000                    0%,
      rgba(0, 0, 0, 0.97)    28%,
      rgba(0, 0, 0, 0.80)    45%,
      rgba(0, 0, 0, 0.45)    62%,
      rgba(0, 0, 0, 0.15)    76%,
      transparent            88%);
  -webkit-mask-repeat: no-repeat;
  mask-repeat: no-repeat;

  will-change: transform;
}

.hero__video {
  width: 100%;
  height: 100%;
  object-fit: cover;
  object-position: center 40%;
}

/* Substitut affiché tant que assets/hero.mp4 n'existe pas.
   Le JS retire ce bloc dès que la vidéo se charge vraiment. */
.hero__placeholder {
  position: absolute;
  inset: 0;
  background:
    radial-gradient(70% 60% at 62% 30%, #f5e3d0 0%, transparent 70%),
    radial-gradient(80% 70% at 78% 78%, #cfd6ea 0%, transparent 72%),
    linear-gradient(150deg, #eef1f6 0%, #e6dfe0 55%, #d9dbe6 100%);
}
.has-video .hero__placeholder { display: none; }

/* --- Le texte --- */
.hero__content {
  position: relative;
  z-index: 1;
  max-width: var(--intro-width);
  display: grid;
  gap: clamp(1.6rem, 3.2vh, 2.6rem);
}

.hero__intro {
  font-size: var(--fs-intro);
  line-height: 1.5;
}

/* Une phrase par ligne, à partir du moment où la largeur le permet.
   En dessous de 1280px les 100 caractères d'une phrase ne peuvent pas
   tenir sans devenir illisibles : on laisse alors le texte couler. */
.hero__line { display: inline; }

@media (min-width: 1280px) {
  .hero__line { display: block; }
}

/* Dans le mockup, les blocs Interests et Currently sont décalés vers la
   droite par rapport au paragraphe d'intro. */
.hero__interests,
.hero__current { margin-left: var(--indent); }

.hero__label {
  margin-bottom: 0.4em;
}

.interests {
  display: grid;
  gap: 0.1em;
}
.interests li {
  position: relative;
  padding-left: 1.2em;
  line-height: 1.35;
  font-weight: 600;
}
.interests li::before {
  content: "•";
  position: absolute;
  left: 0.15em;
  font-weight: 400;
}

.hero__current strong { font-weight: 600; }


/* =========================== 07. WORKS =============================

   Grille « bento » 12 colonnes. Chaque bloc déclare sa largeur avec
   une classe de format ; la hauteur des rangées est fixe pour que les
   blocs s'alignent quelle que soit la longueur des textes.
   ================================================================== */
.works {
  padding: clamp(4rem, 12vh, 9rem) var(--gutter) clamp(4rem, 10vh, 8rem);
}

.works__grid {
  display: grid;
  grid-template-columns: repeat(12, 1fr);
  /* 1ʳᵉ rangée : hauteur libre, dictée par le titre.
     Rangées suivantes : hauteur fixe, pour que les blocs s'alignent. */
  grid-template-rows: auto;
  grid-auto-rows: var(--work-row);
  grid-auto-flow: row dense;   /* rebouche les trous automatiquement */
  gap: var(--work-gap);
}

/* --- Le grand titre de section ---
   Le placement en grille est scopé aux enfants directs de .works__grid,
   pour que la classe reste réutilisable ailleurs (page About). */
.works__grid > .works__title {
  grid-column: span 7;
  align-self: center;
}

.works__title {
  padding-bottom: clamp(0.4rem, 2vh, 1.4rem);
  font-size: clamp(3rem, 12vw, 17rem);
  font-weight: 400;
  line-height: 0.9;
  letter-spacing: -0.005em;
  text-transform: uppercase;
}

/* Bloc d'introduction occupant une cellule de la grille, à la place du
   grand titre : titre + texte, sur la page Piñata. */
.works__intro {
  grid-column: span 5;
  align-self: center;
  display: grid;
  gap: 0.9em;
  padding-bottom: clamp(0.4rem, 2vh, 1.4rem);
}

.works__intro .works__title {
  font-size: clamp(2.5rem, 7.9vw, 10rem);
  margin-bottom: 0.15em;
}

.works__intro p {
  font-weight: 700;
  max-width: 44ch;
}

/* --- Formats de blocs --- */
.work            { grid-column: span 12; }   /* défaut */
.work--narrow    { grid-column: span 5; }
.work--half      { grid-column: span 6; }
.work--wide      { grid-column: span 7; }
.work--full      { grid-column: span 12; }
.work--tall      { grid-row: span 2; }

/* --- Apparition au scroll ---

   L'état caché n'est appliqué QUE si <html> porte la classe .js-reveal,
   posée par un script en tête de page. Sans JS, ou si l'utilisateur a
   réduit les animations, les blocs sont visibles d'emblée : on ne prend
   jamais le risque d'un contenu invisible.

   Le décalage entre blocs vient de --reveal-i, un simple index posé par
   le JS sur chaque bloc. */
.js-reveal .work {
  opacity: 0;
  transform: translateY(30px);
}

.js-reveal .work.is-revealed {
  opacity: 1;
  transform: none;
  transition: opacity 0.7s var(--ease),
              transform 0.7s var(--ease);
  transition-delay: calc(var(--reveal-i, 0) * 90ms);
}

/* --- Le bloc ---
   Important : c'est .work qui capte le survol, et .work ne bouge JAMAIS.
   Ce sont ses deux couches internes qui rétrécissent. Si on animait
   .work directement, ses bords s'éloigneraient du curseur près des
   arêtes et l'effet se mettrait à clignoter. */
.work {
  position: relative;
  overflow: hidden;
  background: none;
}

/* --- Inclinaison 3D + rétrécissement ---

   Le bloc s'incline pour « faire face » au curseur : souris en haut à
   droite, le coin haut-droit bascule vers l'arrière comme un miroir
   qu'on oriente. Le JS ne calcule que deux angles et les publie dans
   --tilt-x / --tilt-y ; tout le rendu est ici.

   L'inclinaison est portée par les COUCHES INTERNES, jamais par .work
   lui-même — même raison que pour le rétrécissement : .work est la zone
   de survol, et la faire pivoter éloignerait ses bords du curseur, ce
   qui déclencherait des entrées/sorties en boucle près des arêtes.

   Les trois couches partagent exactement la même formule, la même
   taille et la même origine : elles restent donc rigoureusement
   coplanaires, sans avoir besoin d'un parent en preserve-3d. */
.work {
  --tilt-x: 0deg;
  --tilt-y: 0deg;
  --tilt-scale: 1;
}

.work__media,
.work__link {
  transform: perspective(var(--tilt-depth))
             rotateX(var(--tilt-x))
             rotateY(var(--tilt-y))
             scale(var(--tilt-scale));
  transition: transform var(--tilt-return) var(--ease);
}

/* Pendant le survol, le suivi doit être serré. Au retour, une durée
   longue donne le mouvement de détente. D'où deux vitesses. */
.work.is-tilting .work__media,
.work.is-tilting .work__link { transition-duration: var(--tilt-follow); }

/* Le liseré transitionne opacité PUIS transform : deux valeurs, pour ne
   pas accélérer aussi son apparition. */
.work.is-tilting::after { transition-duration: 0.35s, var(--tilt-follow); }

/* Demandé : le bloc rétrécit légèrement au survol. */
.work:hover,
.work:focus-within { --tilt-scale: 0.978; }

/* --- Couche média : image idle + vidéo au survol --- */
.work__media {
  position: absolute;
  inset: 0;
  z-index: 0;
  background: var(--work-placeholder);   /* gris tant qu'il n'y a pas d'image */

  /* Indispensable dès qu'on zoome un visuel : sans ça, l'image agrandie
     déborderait du média et annulerait visuellement le rétrécissement
     du bloc au survol. */
  overflow: hidden;
}

.work__image,
.work__video {
  position: absolute;
  inset: 0;
  width: 100%;
  height: 100%;
  object-fit: cover;
}

/* Le point d'origine du zoom suit le cadrage : si tu cales l'image en
   bas, elle grossit depuis le bas et ne se décale pas. */
.work__image {
  object-position: var(--work-image-pos);
  transform: scale(var(--work-image-scale));
  transform-origin: var(--work-image-pos);
}

/* La vidéo se cadre séparément — leurs formats sources diffèrent
   souvent. Par défaut elle suit l'image, donc rien ne change tant que
   tu ne déclares pas --work-video-pos. */
.work__video {
  object-position: var(--work-video-pos, var(--work-image-pos));
  transform: scale(var(--work-video-scale, var(--work-image-scale)));
  transform-origin: var(--work-video-pos, var(--work-image-pos));
}

/* --- Flou au repos, netteté au survol ---

   L'image déborde volontairement de son cadre. Un `blur` échantillonne
   au-delà des bords de l'élément : sans ce débord, le pourtour du bloc
   se dégraderait en un halo laissant transparaître le gris du fond.
   En agrandissant l'image, ce halo tombe hors champ et le débordement
   masqué de .work__media le coupe net.

   Largeur et hauteur explicites, et pas seulement `inset` : sur une
   image — un élément remplacé — `inset` seul ne dimensionne pas.

   La vidéo, elle, reste toujours nette : elle n'apparaît qu'au survol,
   au moment précis où le flou se lève. */
.work__image {
  inset:  calc(var(--work-blur) * -3);
  width:  calc(100% + var(--work-blur) * 6);
  height: calc(100% + var(--work-blur) * 6);

  filter: blur(var(--work-blur));
  transition: filter var(--wipe) var(--wipe-ease);
}

.work:hover        .work__image,
.work:focus-within .work__image { filter: blur(0); }

/* ==================== LE BALAYAGE DIAGONAL =========================

   La vidéo se dévoile le long d'une diagonale. Elle porte un masque en
   dégradé coupé net, de 220% de la taille du bloc, et c'est sa POSITION
   que l'on anime — pas sa forme :

     position 100% 100% → quart bas-droit du masque : côté caché
     position  50%  50% → la coupe passe exactement par les coins
                          bas-gauche et haut-droit : le triangle
     position   0%   0% → quart haut-gauche : entièrement révélé

   Animer une position plutôt qu'une forme, c'est ce qui permet à la
   transition de s'inverser proprement en cours de route quand la souris
   ressort avant la fin.
   ================================================================== */

.work__video {
  -webkit-mask-image: linear-gradient(to bottom right,
                        #000 49.2%, transparent 50.8%);
          mask-image: linear-gradient(to bottom right,
                        #000 49.2%, transparent 50.8%);
  -webkit-mask-size: 220% 220%;
          mask-size: 220% 220%;
  -webkit-mask-repeat: no-repeat;
          mask-repeat: no-repeat;
  -webkit-mask-position: 100% 100%;
          mask-position: 100% 100%;

  /* L'opacité ne sert QU'À une chose : ne pas dévoiler un élément vidéo
     encore vide au tout premier survol. Une fois la classe .is-ready
     posée, elle ne repart plus — c'est le masque, et lui seul, qui fait
     apparaître et disparaître la vidéo.

     Sinon le fondu de sortie (250ms) devançait le balayage inverse
     (620ms) : la vidéo avait disparu avant que la diagonale ait fini sa
     course, et la sortie se lisait comme un simple fondu. */
  opacity: 0;
  transition: opacity 250ms var(--ease),
              transform 0.45s var(--ease),
              -webkit-mask-position var(--wipe) var(--wipe-ease),
                      mask-position var(--wipe) var(--wipe-ease);
}

.work.is-ready .work__video { opacity: 1; }

.work:hover        .work__video,
.work:focus-within .work__video {
  -webkit-mask-position: 0% 0%;
          mask-position: 0% 0%;
}

/* ==================== LES BANDEAUX DE LISIBILITÉ ===================

   Deux dégradés noirs, confinés au haut et au bas du bloc. Le cœur de
   l'illustration n'est jamais touché : il garde sa saturation pleine.

   Pourquoi du noir et non du blanc : un voile blanc désature, tire tout
   vers le blanc et lit comme une impression ratée. Un voile noir
   comprime les hautes lumières mais conserve les teintes — l'œil
   l'interprète comme une ambiance, pas comme un défaut. C'est la seule
   combinaison qui tienne sur n'importe quelle image, quelle qu'elle
   soit. D'où le texte blanc dans les deux états.

   Au survol les bandeaux s'allègent : le survol récompense par PLUS
   d'image, pas par moins.
   ================================================================== */
.work__scrim {
  position: absolute;
  inset: 0;

  /* La hauteur de chaque bandeau est pilotée par background-size,
     ce qui permet de la régler en pourcentage du bloc. */
  background:
    linear-gradient(to bottom,
      rgba(0, 0, 0, 0.92)  0%,
      rgba(0, 0, 0, 0.55) 45%,
      rgba(0, 0, 0, 0)   100%) top / 100% var(--scrim-top) no-repeat,
    linear-gradient(to top,
      rgba(0, 0, 0, 0.95)  0%,
      rgba(0, 0, 0, 0.60) 45%,
      rgba(0, 0, 0, 0)   100%) bottom / 100% var(--scrim-bottom) no-repeat;

  opacity: var(--scrim-idle);
  transition: opacity var(--wipe) var(--wipe-ease);
}

.work:hover        .work__scrim,
.work:focus-within .work__scrim { opacity: var(--scrim-hover); }

/* --- Contenu --- */
.work__link {
  position: relative;
  z-index: 1;
  display: flex;
  flex-direction: column;
  justify-content: space-between;
  gap: 1rem;
  height: 100%;
  padding: var(--work-pad);
}

.work__top {
  display: flex;
  align-items: flex-start;
  justify-content: space-between;
  gap: 1rem;
}

.work__company,
.work__project {
  font-size: clamp(1rem, 1.5vw, 2.4rem);
  font-weight: 700;
  line-height: 1.1;
  letter-spacing: 0.005em;
  text-transform: uppercase;
  text-wrap: balance;
}

.work__project { margin-bottom: 0.15em; }

.work__desc {
  font-size: clamp(0.88rem, 1.02vw, 1.62rem);
  line-height: 1.35;
  max-width: 52ch;
}

/* --- Logo ---
   Plus de plaque noire par défaut : le bandeau sombre assure déjà le
   contraste, et une plaque noire posée dessus s'y noierait. Fournis un
   logo blanc sur fond transparent, il se pose directement. */
.work__logo {
  flex: none;
  display: grid;
  place-items: center;
  height: var(--work-logo);
  max-width: calc(var(--work-logo) * 2.4);
}
.work__logo img {
  width: auto;
  height: 100%;
  object-fit: contain;
}

/* Plaque noire, si tu la veux quand même sur un bloc précis. */
.work__logo--plate {
  width: var(--work-logo);
  aspect-ratio: 1;
  background: var(--ink);
}
.work__logo--plate img { width: 62%; height: 62%; }

/* ==================== LE LISERÉ PRISMATIQUE ========================

   Un dégradé ne peut pas s'appliquer à une bordure directement. La
   technique : un pseudo-élément rempli du dégradé, dont on ne garde que
   l'anneau extérieur grâce à deux masques superposés —

     couche 1 : la zone INTÉRIEURE seule (content-box)
     couche 2 : la surface ENTIÈRE (padding-box)
     `exclude` ne conserve que ce qui appartient à l'une SANS l'autre,
     c'est-à-dire l'anneau d'épaisseur --prism-w.

   Les couleurs défilent en diagonale, dans le même sens que le balayage
   de la vidéo, pour que les deux gestes se répondent.

   Le tout est sous @supports : sur un navigateur sans mask-composite,
   le pseudo-élément n'existe simplement pas. Sans ce garde-fou, il
   s'afficherait en aplat et recouvrirait toute la carte.
   ================================================================== */
@supports (mask-composite: exclude) or (-webkit-mask-composite: xor) {

  .work::after {
    content: "";
    position: absolute;
    inset: 0;
    z-index: 3;              /* au-dessus du texte (1) */
    pointer-events: none;
    padding: var(--prism-w);

    /* Teintes pastel plutôt qu'arc-en-ciel saturé : sur un site en noir
       et blanc, l'effet lit comme un reflet irisé sur du blanc — ce que
       tu décrivais — au lieu d'un gadget multicolore.
       Le motif fait 280px et la dernière couleur reprend la première :
       c'est ce qui permet la boucle sans raccord. */
    background-image: repeating-linear-gradient(
      135deg,
      #ffd9e2   0px,
      #ffe9c4  60px,
      #eaffd4 120px,
      #ccfff2 180px,
      #d2e6ff 240px,
      #e4d6ff 300px,
      #ffd9e2 360px);
    background-size: var(--foil-tile) var(--foil-tile);

    -webkit-mask:
      linear-gradient(#000 0 0) content-box,
      linear-gradient(#000 0 0);
    -webkit-mask-composite: xor;          /* Safari, Chrome ancien */
            mask:
      linear-gradient(#000 0 0) content-box,
      linear-gradient(#000 0 0);
            mask-composite: exclude;      /* standard */

    /* Un filtre plutôt que sept couleurs à réécrire : tu ajustes
       l'intensité du liseré avec deux nombres, sans toucher au dégradé.
       Le drop-shadow s'applique APRÈS le masque, donc le halo épouse
       l'anneau et non le rectangle. */
    filter: saturate(var(--prism-sat))
            brightness(var(--prism-bright))
            drop-shadow(0 0 var(--prism-glow) rgba(255, 255, 255, 0.55));

    /* Même formule que les autres couches : le liseré s'incline et
       rétrécit avec le bloc, sinon il resterait un cadre plat et
       immobile autour d'un contenu qui bouge. */
    transform: perspective(var(--tilt-depth))
               rotateX(var(--tilt-x))
               rotateY(var(--tilt-y))
               scale(var(--tilt-scale));

    opacity: 0;
    transition: opacity 0.35s var(--ease),
                transform var(--tilt-return) var(--ease);
  }

  .work:hover::after,
  .work:focus-within::after {
    opacity: 1;
    animation: foil-drift var(--prism-speed) linear infinite;
  }

}

/* --- Renfort de saturation autour du curseur ---

   Un second anneau, nettement plus saturé, superposé au premier et
   découpé par un cercle qui suit le curseur. Le liseré paraît donc
   s'aviver au passage de la souris.

   Trois couches de masque et deux opérateurs :
     couche 3 (bas)  la surface entière
     couche 2        la zone intérieure, en `exclude`   → l'anneau
     couche 1 (haut) le cercle du curseur, en `intersect` → l'arc

   Bloc @supports séparé et sans préfixe : la composition à trois
   couches demande le `mask-composite` standard, là où l'anneau simple
   plus haut sait encore se contenter de la syntaxe WebKit historique. */
@supports (mask-composite: exclude) {

  .work::before {
    content: "";
    position: absolute;
    inset: 0;
    z-index: 4;             /* au-dessus du liseré de base (3) */
    pointer-events: none;
    padding: var(--prism-w);

    background-image: repeating-linear-gradient(
      135deg,
      #ffd9e2   0px,
      #ffe9c4  60px,
      #eaffd4 120px,
      #ccfff2 180px,
      #d2e6ff 240px,
      #e4d6ff 300px,
      #ffd9e2 360px);
    background-size: var(--foil-tile) var(--foil-tile);

    filter: saturate(var(--prism-glow-sat)) brightness(var(--prism-glow-bright));

    mask:
      radial-gradient(circle var(--prism-glow-r)
        at var(--foil-x, -999px) var(--foil-y, -999px),
        #000 0%, rgba(0, 0, 0, 0.55) 50%, transparent 100%),
      linear-gradient(#000 0 0) content-box,
      linear-gradient(#000 0 0);
    mask-composite: intersect, exclude;

    transform: perspective(var(--tilt-depth))
               rotateX(var(--tilt-x))
               rotateY(var(--tilt-y))
               scale(var(--tilt-scale));

    opacity: 0;
    transition: opacity 0.35s var(--ease),
                transform var(--tilt-return) var(--ease);
  }

  .work:hover::before,
  .work:focus-within::before {
    opacity: 1;
    animation: foil-drift var(--prism-speed) linear infinite;
  }

  .work.is-tilting::before { transition-duration: 0.35s, var(--tilt-follow); }
}

/* Le texte des blocs est blanc dans les deux états.
   C'est ce qui permet aux bandeaux d'être sombres, donc aux
   illustrations de garder leur saturation. Voir le commentaire des
   bandeaux plus haut.

   --work-text et non --paper : un bloc est posé sur une image, jamais
   sur le fond de la page. Son texte doit donc rester blanc même sur une
   page au thème sombre, où --paper devient noir. */
.work { color: var(--work-text); }


/* ==================== 07b. CURSEUR PERSONNALISÉ ====================

   Le curseur natif n'est masqué QUE si le JS a réussi à créer le
   remplaçant (classe .has-cursor posée par main.js). Sans ce garde-fou,
   un échec du script laisserait l'utilisateur sans aucun curseur.

   mix-blend-mode: difference — l'anneau est blanc, mais s'affiche noir
   sur fond clair et blanc sur fond sombre. Un seul élément reste donc
   visible partout, y compris sur un bloc Works assombri.
   ================================================================== */
.has-cursor,
.has-cursor * { cursor: none; }

.cursor {
  position: fixed;
  top: 0;
  left: 0;
  z-index: 9999;
  pointer-events: none;
  mix-blend-mode: difference;
  will-change: transform;
}

.cursor__ring {
  position: absolute;
  top: 0;
  left: 0;
  width: var(--cursor-size);
  height: var(--cursor-size);
  border: 1.5px solid #fff;
  border-radius: 50%;
  transform: translate(-50%, -50%);
  transition: transform 0.28s var(--ease),
              background-color 0.28s var(--ease),
              border-color 0.28s var(--ease);
}

/* Au-dessus d'un élément cliquable : l'anneau grossit et se remplit. */
.cursor.is-active .cursor__ring {
  background: #fff;
  transform: translate(-50%, -50%) scale(var(--cursor-grow));
}


/* ======================= 07c. TITRES FOIL ==========================

   Une copie du titre, remplie d'un dégradé irisé, est superposée à
   l'original et découpée par un cercle qui suit le curseur.

   `background-clip: text` confine le dégradé aux lettres ; le masque
   radial ne laisse voir que la zone autour du pointeur. Le texte de la
   copie vient de data-foil, que le JS remplit tout seul à partir du
   contenu — rien à dupliquer dans le HTML.

   Ici le dégradé est SATURÉ, contrairement au liseré des blocs : sur
   des lettres noires posées sur du blanc, des pastels ne se verraient
   pas. Chaque effet a sa palette.
   ================================================================== */
/* Plus de copie du texte : deux couches de FOND découpées par les
   lettres elles-mêmes.

     couche du bas   le dégradé irisé
     couche du haut  un disque transparent en son centre, encre opaque
                     au-delà — c'est lui qui suit le curseur

   Là où le disque est troué, on voit le dégradé ; ailleurs, l'encre.

   Cette version remplace l'ancienne, qui dupliquait le texte via
   `content: attr(data-foil)`. Deux gains : le JS n'a plus rien à
   recopier, et surtout **l'effet fonctionne sur un texte qui passe à la
   ligne** — impossible avec une copie, qui ne peut contenir de <br>.

   Le @supports est indispensable : sans `background-clip: text`, un
   texte en `color: transparent` serait purement et simplement invisible. */
@supports ((-webkit-background-clip: text) or (background-clip: text))
      and (color: color-mix(in srgb, red, blue)) {

  .foil {
    /* Couleur du texte HORS du disque, c'est-à-dire son état normal.
       C'est ce qui permet d'appliquer l'effet partout sans écraser les
       couleurs existantes : noir sur fond clair, blanc dans les blocs
       Works, rouge sur les passages en accent de la page About. */
    --foil-base: var(--ink);

    /* Voile résiduel au CENTRE du disque : 0 laisse le dégradé pur,
       0.5 le montre à travers un demi-voile de la couleur de repos.
       C'est ce qui permet d'atténuer l'effet là où il est trop vif,
       sans rien changer aux couleurs du dégradé lui-même. */
    --foil-veil: 0;

    background-image:
      radial-gradient(circle var(--foil-size)
        at var(--foil-x, -999px) var(--foil-y, -999px),
        color-mix(in srgb, var(--foil-base) calc(var(--foil-veil) * 100%), transparent) 0%,
        color-mix(in srgb,
          var(--foil-base) calc(35% + var(--foil-veil) * 45%),
          transparent) 46%,
        var(--foil-base) 78%),
      repeating-linear-gradient(
        135deg,
        #ff0080   0px,
        #ff8c00  60px,
        #ffe600 120px,
        #00e676 180px,
        #00b0ff 240px,
        #7c4dff 300px,
        #ff0080 360px);

    background-size: auto, var(--foil-tile) var(--foil-tile);
    background-repeat: no-repeat, repeat;

    -webkit-background-clip: text;
            background-clip: text;
    -webkit-text-fill-color: transparent;
            color: transparent;

    animation: foil-text-drift var(--foil-speed) linear infinite;
  }

  /* --- Les textes en gras du site ---

     Le JS pose la classe `foil` sur chacun d'eux au chargement ; il ne
     reste ici qu'à leur donner la bonne couleur de repos.

     Sur un mot court, le rayon par défaut (prévu pour un titre de
     150px de haut) engloberait tout le mot dès que le curseur approche.
     `foil--inline` le resserre pour que l'effet balaie réellement les
     lettres. */
  .foil--inline { --foil-size: clamp(80px, 8vw, 190px); }

  /* Les passages en accent gardent leur rouge au repos : l'irisation
     s'ajoute à la couleur, elle ne la remplace pas.
     Le voile la retient à mi-chemin — le foil s'y lit plus sourd et
     plus chaud, au lieu d'effacer le rouge. */
  .about__body strong {
    --foil-base: var(--accent);
    --foil-veil: 0.5;
  }

  /* Dans les blocs Works le texte est blanc sur un bandeau sombre.
     Sans cette ligne, il repasserait en noir et deviendrait illisible. */
  .work__company,
  .work__project { --foil-base: var(--work-text); }
}

/* ===================== TITRE À LETTRES FLOTTANTES ==================

   Chaque lettre est un span portant son index dans --i. Cet index sert
   à DEUX choses : décaler son entrée dans le mouvement, et décaler sa
   position dans le dégradé — d'où une couleur différente par lettre.

   Deux animations cohabitent sur le même élément parce qu'elles
   touchent des propriétés différentes : l'une le fond, l'autre la
   transformation. Elles ne se marchent pas dessus.

   Le foil est ici permanent et ne suit pas le curseur : il faudrait
   des coordonnées par lettre, et sur un titre déjà animé ce serait du
   bruit ajouté au bruit.
   ================================================================== */
.foil-letter {
  display: inline-block;   /* indispensable : une inline ne se transforme pas */
  animation: letter-drift var(--foil-speed) linear infinite,
             letter-float var(--float-speed) ease-in-out infinite;
  animation-delay: 0s, calc(var(--i) * var(--float-stagger));
}

@supports ((-webkit-background-clip: text) or (background-clip: text)) {
  .foil-letter {
    background-image: repeating-linear-gradient(
      135deg,
      #ff0080   0px,
      #ff8c00  60px,
      #ffe600 120px,
      #00e676 180px,
      #00b0ff 240px,
      #7c4dff 300px,
      #ff0080 360px);
    background-size: var(--foil-tile) var(--foil-tile);

    -webkit-background-clip: text;
            background-clip: text;
    -webkit-text-fill-color: transparent;
            color: transparent;
  }
}

/* Le décalage par lettre est porté par les keyframes et non par une
   déclaration à part : l'animation écraserait sinon la position. */
@keyframes letter-drift {
  from { background-position: calc(var(--i) * -70px)          0; }
  to   { background-position: calc(var(--i) * -70px + 509px) 509px; }
}

@keyframes letter-float {
  0%, 100% { transform: translateY(0); }
  50%      { transform: translateY(var(--float-amp)); }
}

/* Une tuile complète en diagonale : la boucle est invisible. */
@keyframes foil-drift {
  from { background-position:   0   0; }
  to   { background-position: 509px 509px; }
}

/* Idem, mais sur la seconde couche seulement — la première, le disque
   du curseur, ne doit pas bouger. */
@keyframes foil-text-drift {
  from { background-position: 0 0,   0     0; }
  to   { background-position: 0 0, 509px 509px; }
}


/* ======================= 07c-bis. PAGES PROJET =====================

   Première partie : un hero plein écran.

   Trois rangées — outils en haut, contenu au centre, rôle en bas — pour
   que le bloc central reste optiquement centré quel que soit le nombre
   de lignes de texte.
   ================================================================== */
.phero {
  position: relative;
  min-height: 100svh;
  display: grid;
  grid-template-rows: auto 1fr auto;
  align-items: center;
  justify-items: center;
  gap: 1.5rem;
  /* Aucune réserve pour le header : sur une page projet il n'est pas
     posé par-dessus, il arrive après le hero. */
  padding: clamp(2rem, 6vh, 4rem) var(--gutter) clamp(1.5rem, 4vh, 3rem);
  overflow: hidden;
  text-align: center;
  color: var(--paper);
}

/* --- Fond --- */
.phero__media {
  position: absolute;
  inset: 0;
  z-index: 0;
  background: var(--ink);
}

.phero__image,
.phero__video {
  position: absolute;
  top: 0;
  left: 0;
  width: 100%;
  height: 100%;
  object-fit: cover;
  object-position: var(--phero-pos);
}

/* La vidéo, si elle existe, se pose simplement sur l'image. */
.phero__video { z-index: 1; }

.phero__scrim {
  position: absolute;
  inset: 0;
  z-index: 2;
  background: #000;
  opacity: var(--phero-scrim);
}

/* --- Contenu --- */
.phero__tools,
.phero__content,
.phero__role { position: relative; z-index: 3; }

/* Les logos d'outils s'accumulent horizontalement, centrés. */
.phero__tools {
  display: flex;
  align-items: center;
  justify-content: center;
  flex-wrap: wrap;
  gap: clamp(1rem, 2.4vw, 2.4rem);
}
.phero__tools img {
  height: var(--phero-tool-h);
  width: auto;
  object-fit: contain;
}

.phero__content {
  display: grid;
  justify-items: center;
  gap: clamp(0.5rem, 1.4vh, 1rem);
  max-width: var(--phero-width);
}

/* Pas de `text-wrap: balance` : la répartition automatique casse la
   ligne trop tôt sur un titre à deux-points. Les coupures se posent à
   la main avec un <br> dans le HTML. */
.phero__title {
  font-size: clamp(2rem, 4.4vw, 6rem);
  font-weight: 700;
  line-height: 1.02;
  letter-spacing: 0.04em;
  text-transform: uppercase;
}

.phero__team {
  font-size: clamp(0.62rem, 0.82vw, 1rem);
  letter-spacing: 0.22em;
  text-transform: uppercase;
  opacity: 0.72;
}

/* Un lien dans cette ligne doit rester repérable sans casser la
   discrétion de la mention : soulignement fin, et pleine opacité au
   survol plutôt qu'un changement de couleur. */
.phero__team a,
.phero__role a {
  text-decoration: underline;
  text-underline-offset: 0.25em;
  text-decoration-thickness: 1px;
  transition: opacity 0.3s var(--ease);
}

.phero__team:hover,
.phero__role:hover { opacity: 1; }

.phero__intro {
  font-weight: 700;
  max-width: 40rem;
  margin-top: 0.4em;
}

/* Rangée de boutons. Un seul, il est centré ; plusieurs, ils s'alignent
   et passent à la ligne sur écran étroit plutôt que de déborder.
   La marge est reprise par la rangée, sinon elle s'ajouterait à celle
   que porte déjà chaque bouton. */
.phero__actions {
  display: flex;
  flex-wrap: wrap;
  justify-content: center;
  gap: clamp(0.6rem, 1.4vw, 1.1rem);
  margin-top: 0.8em;
}

.phero__actions .btn { margin-top: 0; }

.phero__role {
  font-size: clamp(0.62rem, 0.78vw, 0.95rem);
  letter-spacing: 0.22em;
  text-transform: uppercase;
  opacity: 0.8;
}


/* =========================== 07c-ter. BOUTON =======================

   Pas de `border` : le contour est un pseudo-élément, comme les liserés
   des blocs. C'est ce qui permet de passer d'un trait blanc à un trait
   irisé en fondu — une bordure, elle, aurait sauté d'un coup.
   ================================================================== */
.btn {
  position: relative;
  display: inline-flex;
  align-items: center;
  justify-content: center;
  min-width: 12rem;
  padding: 0.95em 2.2em;
  margin-top: 0.8em;

  font-size: clamp(0.8rem, 1vw, 1.15rem);
  font-weight: 700;
  color: var(--paper);
  background: transparent;

  transition: background-color 0.35s var(--ease),
              color 0.35s var(--ease);
}

.btn:hover,
.btn:focus-visible {
  background: var(--paper);
  color: var(--ink);
}

/* Contour blanc au repos. */
.btn::before {
  content: "";
  position: absolute;
  inset: 0;
  border: var(--btn-ring) solid rgba(255, 255, 255, 0.85);
  transition: opacity 0.35s var(--ease);
}

.btn:hover::before,
.btn:focus-visible::before { opacity: 0; }

/* Contour irisé au survol. */
@supports (mask-composite: exclude) or (-webkit-mask-composite: xor) {
  .btn::after {
    content: "";
    position: absolute;
    inset: 0;
    pointer-events: none;
    padding: var(--btn-ring);

    background-image: repeating-linear-gradient(
      135deg,
      #ff0080   0px,
      #ff8c00  60px,
      #ffe600 120px,
      #00e676 180px,
      #00b0ff 240px,
      #7c4dff 300px,
      #ff0080 360px);
    background-size: var(--foil-tile) var(--foil-tile);

    -webkit-mask:
      linear-gradient(#000 0 0) content-box,
      linear-gradient(#000 0 0);
    -webkit-mask-composite: xor;
            mask:
      linear-gradient(#000 0 0) content-box,
      linear-gradient(#000 0 0);
            mask-composite: exclude;

    opacity: 0;
    transition: opacity 0.35s var(--ease);
  }

  .btn:hover::after,
  .btn:focus-visible::after {
    opacity: 1;
    animation: foil-drift var(--foil-speed) linear infinite;
  }
}

/* Bouton sans destination — « Coming soon ». Il garde son apparence
   mais n'est ni cliquable ni focusable. */
.btn--soon { cursor: default; }


/* ==================== 07c-quater. CORPS DE PROJET ================== */
.pbody {
  padding: clamp(4rem, 12vh, 9rem) var(--gutter) clamp(1rem, 4vh, 3rem);
}

.pbody__title {
  font-size: clamp(1.8rem, 4vw, 4.6rem);
  font-weight: 700;
  line-height: 1.05;
  letter-spacing: 0.03em;
  text-transform: uppercase;
  margin-bottom: clamp(1.5rem, 4vh, 3rem);
}

/* Texte descriptif : paragraphes et listes à puces cohabitent. */
.pbody__text {
  display: grid;
  gap: 1.1em;
  max-width: min(100%, 80ch);
  color: var(--ink-soft);
  font-weight: 700;
}

/* Les intitulés en gras ressortent en noir sur le gris du texte. */
.pbody__text strong { color: var(--ink); }

.pbody__text ul,
.pbody__list {
  display: grid;
  gap: 1em;
  margin-top: 0.4em;
}

.pbody__text li,
.pbody__list li {
  position: relative;
  padding-left: 1.4em;
}

.pbody__text li::before,
.pbody__list li::before {
  content: "•";
  position: absolute;
  left: 0.25em;
  font-weight: 400;
}

/* Liste hors du bloc de texte : elle reprend ses couleurs et son gras. */
.pbody__list {
  max-width: min(100%, 80ch);
  margin-left: var(--indent);
  color: var(--ink-soft);
  font-weight: 700;
}
.pbody__list strong { color: var(--ink); }

/* Variante à deux niveaux : l'intitulé prend sa propre ligne.
   À utiliser quand la description dépasse une ligne — en ligne, un
   intitulé se noie dans le paragraphe qui le suit. */
.pbody__list--stacked strong {
  display: block;
  margin-bottom: 0.2em;
}

/* Rangée de liens sociaux sous un bloc de texte.
   Les liens portent la classe `nav__link` : ils héritent du roulement,
   du soulignement et du foil de la navigation, sans une ligne de plus. */
.pbody__social {
  display: flex;
  flex-wrap: wrap;
  align-items: baseline;
  gap: clamp(1.2rem, 3vw, 2.6rem);
  margin-top: clamp(2rem, 5vh, 3.5rem);
  font-size: var(--fs-nav);
  letter-spacing: var(--track-nav);
  text-transform: uppercase;
}

/* Sous-titre de section, entre deux blocs de texte. */
.pbody__sub {
  font-size: clamp(1.1rem, 1.9vw, 2rem);
  font-weight: 700;
  margin-top: clamp(2rem, 6vh, 4rem);
  margin-bottom: 0.6em;
}


/* --- Intégration d'un document externe (Google Slides, Drive, Figma…) ---

   Le rapport 16/9 est porté par le conteneur, pas par l'iframe : une
   iframe n'a pas de taille intrinsèque, elle prendrait une hauteur
   arbitraire de 150px. En la calant sur un parent au bon rapport, le
   document reste proportionné à toutes les largeurs d'écran. */
.pembed {
  position: relative;
  width: min(100%, 68rem);
  aspect-ratio: var(--pembed-ratio, 16 / 9);
  margin-inline: auto;
  margin-block: clamp(2rem, 5vh, 3.5rem) clamp(1.5rem, 4vh, 3rem);
  background: var(--work-placeholder);
}

.pembed iframe {
  position: absolute;
  inset: 0;
  width: 100%;
  height: 100%;
  border: 0;
}

/* Capture d'écran présentée comme un document : même largeur, même
   liseré. La hauteur suit l'image, il n'y a donc pas de rapport à
   imposer — contrairement à l'iframe. */
.pshot {
  position: relative;
  width: min(100%, 68rem);
  margin-inline: auto;
  margin-block: clamp(2rem, 5vh, 3.5rem) clamp(1.5rem, 4vh, 3rem);
}

.pshot img {
  display: block;
  width: 100%;
  height: auto;
}

/* Légende sous une capture. */
.pshot figcaption {
  margin-top: 0.9em;
  font-size: var(--fs-nav);
  letter-spacing: var(--track-nav);
  text-transform: uppercase;
  color: var(--ink-faint);
  text-align: center;
}

/* Liseré prismatique permanent — pas au survol, contrairement aux blocs :
   ici il encadre un document, il doit donc être là en permanence.

   `pointer-events: none` est indispensable : sans lui, le liseré
   couvrirait les bords de l'iframe et volerait les clics destinés aux
   flèches de navigation de Google. */
@supports (mask-composite: exclude) or (-webkit-mask-composite: xor) {
  .pembed::after,
  .pshot::after {
    content: "";
    position: absolute;
    inset: calc(var(--pembed-ring) * -1);
    z-index: 1;
    pointer-events: none;
    padding: var(--pembed-ring);

    background-image: repeating-linear-gradient(
      135deg,
      #ff0080   0px,
      #ff8c00  60px,
      #ffe600 120px,
      #00e676 180px,
      #00b0ff 240px,
      #7c4dff 300px,
      #ff0080 360px);
    background-size: var(--foil-tile) var(--foil-tile);

    -webkit-mask:
      linear-gradient(#000 0 0) content-box,
      linear-gradient(#000 0 0);
    -webkit-mask-composite: xor;
            mask:
      linear-gradient(#000 0 0) content-box,
      linear-gradient(#000 0 0);
            mask-composite: exclude;

    animation: foil-drift var(--foil-speed) linear infinite;
  }
}

/* Bouton centré sous un contenu. */
.pbody__actions {
  display: flex;
  justify-content: center;
}


/* ==================== 07c-quinquies. MOSAÏQUE ======================

   Grille d'images sur deux colonnes. Contrairement aux blocs Works,
   les hauteurs ne sont PAS imposées : chaque image garde son rapport.

   C'est volontaire. Ces pages sont des mises en page — les rogner
   détruirait précisément ce qu'elles montrent. Les doubles pages
   prennent donc toute la largeur, les pages simples se font face.
   ================================================================== */
.gallery {
  display: grid;
  grid-template-columns: repeat(2, minmax(0, 1fr));
  gap: var(--work-gap);
  align-items: start;
  margin-top: clamp(2rem, 5vh, 3.5rem);
}

.gallery__item {
  position: relative;
  display: block;
  overflow: hidden;
  background: var(--work-placeholder);
}

/* Double page : toute la largeur. */
.gallery__item--full { grid-column: 1 / -1; }

.gallery__item img {
  display: block;
  width: 100%;
  height: auto;
  transition: transform 0.5s var(--ease);
}

/* Zoom léger, et non retrait comme sur les blocs Works : ici la boîte
   épouse l'image, la rétrécir laisserait apparaître le fond sur les
   bords. */
.gallery__item:hover img,
.gallery__item:focus-visible img { transform: scale(1.03); }

/* Liseré prismatique au survol, comme les blocs. */
@supports (mask-composite: exclude) or (-webkit-mask-composite: xor) {
  .gallery__item::after {
    content: "";
    position: absolute;
    inset: 0;
    pointer-events: none;
    padding: var(--gallery-ring);

    background-image: repeating-linear-gradient(
      135deg,
      #ff0080   0px,
      #ff8c00  60px,
      #ffe600 120px,
      #00e676 180px,
      #00b0ff 240px,
      #7c4dff 300px,
      #ff0080 360px);
    background-size: var(--foil-tile) var(--foil-tile);

    -webkit-mask:
      linear-gradient(#000 0 0) content-box,
      linear-gradient(#000 0 0);
    -webkit-mask-composite: xor;
            mask:
      linear-gradient(#000 0 0) content-box,
      linear-gradient(#000 0 0);
            mask-composite: exclude;

    opacity: 0;
    transition: opacity 0.35s var(--ease);
  }

  .gallery__item:hover::after,
  .gallery__item:focus-visible::after {
    opacity: 1;
    animation: foil-drift var(--foil-speed) linear infinite;
  }
}

@media (max-width: 700px) {
  .gallery { grid-template-columns: 1fr; }
}

/* Les boutons du corps de page sont sur fond clair : contour et texte
   passent en encre, et le survol inverse en noir. */
.btn--dark { color: var(--ink); }
.btn--dark::before { border-color: rgba(16, 16, 16, 0.8); }
.btn--dark:hover,
.btn--dark:focus-visible {
  background: var(--ink);
  color: var(--paper);
}

/* --- Section « autres projets » --- */
.works--others { padding-top: clamp(2rem, 8vh, 6rem); }

.works__title--others {
  display: block;
  font-size: clamp(1.8rem, 5vw, 6rem);
  margin-bottom: clamp(1.5rem, 4vh, 3rem);
}

/* La grille n'a plus de titre en premier enfant : toutes ses rangées
   doivent donc avoir la hauteur fixe, sans quoi un bloc à double
   hauteur s'effondrerait sur la première. */
.works__grid--flat { grid-template-rows: var(--work-row); }


/* ========================== 07d. PAGE ABOUT ======================== */
.about {
  padding: calc(var(--header-h) + 6vh)
           var(--about-right)
           clamp(3rem, 8vh, 6rem)
           var(--about-left);
}

/* Proportions calées sur le mockup : la colonne de texte occupe environ
   53% de la largeur d'écran, le losange 32%, séparés par 4,2%.
   Les fr reproduisent ce rapport une fois la gouttière retirée. */
.about__grid {
  display: grid;
  grid-template-columns: minmax(0, 1.65fr) minmax(0, 1fr);
  gap: clamp(1.5rem, 4.2vw, 5rem);
  align-items: center;
}

/* Portrait masqué. Un seul mot à retirer du HTML pour tout rétablir.
   La colonne de droite disparaît en même temps que le cadre : la
   masquer seule aurait laissé un grand vide à droite du texte. */
.about__grid--solo { grid-template-columns: minmax(0, 1fr); }
.about__grid--solo .foil-frame { display: none; }

/* Mesure volontairement longue : dans le mockup, « My first introduction
   to game design was with paper prototypes… » tient sur UNE ligne, soit
   plus de 90 caractères. Un 62ch typographiquement orthodoxe cassait
   toutes les lignes au mauvais endroit. La colonne de grille reste le
   vrai garde-fou ; le ch ne sert qu'à borner les très grands écrans. */
/* La marge est portée par ce bloc, pas par la citation seule : le filet
   vertical appartient à ce conteneur, il descend donc avec le texte au
   lieu de rester accroché en haut. */
.about__body {
  display: grid;
  gap: 1.1em;
  max-width: min(100%, 94ch);
  margin-top: var(--about-body-offset);
}

/* Resserrage sous le titre.

   Un titre de 12vw en line-height 0.9 traîne beaucoup d'air sous sa
   ligne de base : l'espace réservé aux jambages descendants est là même
   si « ABOUT » n'en a aucun. On supprime le padding et on remonte d'un
   dixième de cadratin — exprimé en em, donc proportionnel au titre.
   Rends --about-title-gap plus négatif pour resserrer davantage. */
.about__col .works__title {
  padding-bottom: 0;
  margin-bottom: var(--about-title-gap);
}

/* Figma : 25px pour la citation, 20px pour le corps → rapport 1,25.
   Exprimé en `em`, il reste juste quelle que soit la largeur d'écran,
   là où une valeur en px se figerait.

   font-weight 700 = Futura PT Heavy dans ton kit Adobe. Si le rendu te
   paraît trop léger, remplace par font-family: "futura-pt-bold" — c'est
   une famille séparée, également présente dans le kit. */
.about__lead {
  font-size: 1.25em;
  font-weight: 700;
  line-height: 1.35;
  margin-bottom: 0.55em;
}

/* Les passages saillants, en accent. Le gras vient de <strong>. */
.about__body strong { color: var(--accent); }

/* Dans le mockup, « Experience : » s'aligne sur le texte des
   paragraphes — pas sur le filet, et sans décalage supplémentaire.
   1.15em correspond exactement au retrait imposé par la classe `ruled`. */
.about__extra {
  margin-left: 1.15em;
  margin-top: 1.6em;
}

/* --- Le cadre losange à liseré foil ---

   Deux losanges superposés, découpés par clip-path : celui du dessous
   porte le dégradé, celui du dessus la photo, légèrement plus petit.
   L'anneau visible est la différence entre les deux.

   Pourquoi pas la technique des blocs (masques + mask-composite) : elle
   produit un anneau RECTANGULAIRE. Sur une forme en losange, seul le
   découpage géométrique donne un liseré qui suit les quatre arêtes.

   ⚠ --frame-inset est un décalage VERTICAL et horizontal. Sur une arête
   à 45°, l'épaisseur perpendiculaire vaut environ 0,71 × cette valeur.
   Pour un trait de 8px à l'œil, mets donc ~11px. */
/* Calé à droite et non centré dans sa colonne : dans le mockup, la
   pointe droite du losange frôle le bord de page.

   Le `68vh` dans la largeur n'est pas une erreur : le cadre est carré
   (aspect-ratio: 1), donc sa largeur détermine aussi sa hauteur. Sans
   ce plafond, une valeur en vw pouvait produire un carré plus haut que
   l'écran — le losange débordait alors sous le header et écrasait la
   page. C'est le garde-fou qui manquait. */
.foil-frame {
  position: relative;
  width: min(100%, var(--frame-size), 68vh);
  aspect-ratio: 1;
  margin-left: auto;
  margin-right: 0;
}

.foil-frame__edge,
.foil-frame__glow,
.foil-frame__crop {
  position: absolute;
  clip-path: polygon(50% 0%, 100% 50%, 50% 100%, 0% 50%);
}

/* Renfort de saturation autour du curseur, même principe que sur les
   blocs — mais ici aucun masque composé n'est nécessaire : le losange
   vient du clip-path, le masque radial est donc libre.
   Ce calque se glisse entre le liseré et la photo, qui le recouvre en
   son centre : seul l'arc proche du curseur reste visible. */
.foil-frame__glow {
  inset: 0;
  pointer-events: none;

  background-image: repeating-linear-gradient(
    135deg,
    #ff0080   0px,
    #ff8c00  60px,
    #ffe600 120px,
    #00e676 180px,
    #00b0ff 240px,
    #7c4dff 300px,
    #ff0080 360px);
  background-size: var(--foil-tile) var(--foil-tile);

  -webkit-mask-image: radial-gradient(
    circle var(--frame-glow-r) at var(--foil-x, -999px) var(--foil-y, -999px),
    #000 0%, rgba(0, 0, 0, 0.5) 50%, transparent 100%);
          mask-image: radial-gradient(
    circle var(--frame-glow-r) at var(--foil-x, -999px) var(--foil-y, -999px),
    #000 0%, rgba(0, 0, 0, 0.5) 50%, transparent 100%);

  animation: foil-drift var(--foil-speed) linear infinite;
}

.foil-frame__edge {
  inset: 0;

  /* Motif de 360px, dernière couleur = première : la boucle de
     `foil-drift` (255px ≈ 360 / √2) est donc invisible. */
  background-image: repeating-linear-gradient(
    135deg,
    #ffd9e2   0px,
    #ffe9c4  60px,
    #eaffd4 120px,
    #ccfff2 180px,
    #d2e6ff 240px,
    #e4d6ff 300px,
    #ffd9e2 360px);
  background-size: var(--foil-tile) var(--foil-tile);

  /* Les pastels des blocs sont posés sur un voile sombre ; ici le cadre
     est sur du blanc, il faut donc les densifier pour qu'ils existent. */
  filter: saturate(var(--frame-sat)) brightness(var(--frame-bright));

  animation: foil-drift var(--foil-speed) linear infinite;
}

/* Le fond noir n'est visible que si la photo manque ou échoue : on voit
   alors un losange noir cerclé de foil, au lieu d'un aplat de dégradé
   qui donne l'impression que le cadre est cassé. */
/* Le losange de découpe. Il porte la forme et ne bouge JAMAIS.

   Cette séparation entre la découpe et la photo est indispensable : une
   transformation s'applique après la découpe, donc déplacer ou agrandir
   l'image directement emmènerait son losange avec elle. Le cadre se
   décalerait et mordrait le liseré.

   ⚠ Largeur et hauteur EXPLICITES, et non `inset` seul. Sur un élément
   remplacé — image, vidéo, iframe — la spécification donne la priorité à
   la taille intrinsèque du fichier et ignore `right` et `bottom`. */
.foil-frame__crop {
  top:  var(--frame-inset);
  left: var(--frame-inset);
  width:  calc(100% - 2 * var(--frame-inset));
  height: calc(100% - 2 * var(--frame-inset));
  overflow: hidden;
  background: var(--ink);
}

/* La photo, libre de bouger à l'intérieur de la découpe. */
.foil-frame__photo {
  position: absolute;
  inset: 0;
  width: 100%;
  height: 100%;
  color: var(--paper);
  object-fit: cover;
  object-position: var(--frame-pos);

  /* Le zoom part du point de cadrage : recadrer et zoomer se combinent
     sans se décaler. Le déplacement, lui, agit sur la photo seule. */
  transform: translate(var(--frame-shift-x), var(--frame-shift-y))
             scale(var(--frame-scale));
  transform-origin: var(--frame-pos);
}

/* --- Responsive About --- */
@media (max-width: 900px) {
  .about__grid {
    grid-template-columns: 1fr;
    gap: clamp(2rem, 6vh, 3.5rem);
  }
  /* La photo passe sous le texte, et se limite pour ne pas écraser la page. */
  .foil-frame { width: min(100%, 22rem); order: 2; }
}


/* ==================== 07e. TRANSITION DE PAGE ======================

   Le même balayage diagonal que les blocs Works, mais d'une page à
   l'autre. Aucun JavaScript : l'API View Transitions fait le travail.

   Le navigateur photographie l'ancienne page, charge la nouvelle, puis
   anime le passage. La page sortante reste immobile dessous pendant que
   l'entrante se dévoile par-dessus — exactement la mécanique des blocs.

   Pourquoi des keyframes et non une simple transition : entre deux
   `clip-path`, le navigateur interpole les sommets bêtement, et un
   triangle qui grandit vers un rectangle produit un zoom carré. Les
   étapes intermédiaires forcent le tracé à suivre la diagonale.

   Le polygone garde toujours 5 sommets, certains confondus : c'est ce
   qui permet à la forme de passer de triangle à pentagone sans saut.

   ⚠ Ne fonctionne pas en ouvrant les fichiers en `file://`. Utilise
   Live Server (voir README) ou teste une fois en ligne.
   ================================================================== */
@view-transition { navigation: auto; }

::view-transition-old(root) {
  animation: none;
  opacity: 1;
  z-index: 0;
}

/* On anime une POSITION DE MASQUE, pas un clip-path.

   Ma première version interpolait un polygone à cinq sommets. La
   géométrie était juste, mais un `clip-path` sur une capture plein
   écran oblige le navigateur à recalculer et repeindre toute la surface
   à chaque image, sur le fil principal — d'où les à-coups.

   Un masque qui glisse ne redessine rien : c'est exactement la
   mécanique déjà utilisée pour les blocs Works, et pour la même raison. */
::view-transition-new(root) {
  animation: page-wipe var(--wipe) var(--wipe-ease) both;
  z-index: 1;

  -webkit-mask-image: linear-gradient(to bottom right,
                        #000 49.2%, transparent 50.8%);
          mask-image: linear-gradient(to bottom right,
                        #000 49.2%, transparent 50.8%);
  -webkit-mask-size: 220% 220%;
          mask-size: 220% 220%;
  -webkit-mask-repeat: no-repeat;
          mask-repeat: no-repeat;
}

@keyframes page-wipe {
  from {
    -webkit-mask-position: 100% 100%;
            mask-position: 100% 100%;
  }
  to {
    -webkit-mask-position: 0% 0%;
            mask-position: 0% 0%;
  }
}

/* Les pseudo-éléments de transition ne sont pas atteints par le bloc
   « animations réduites » plus bas : il leur faut leur propre règle. */
@media (prefers-reduced-motion: reduce) {
  ::view-transition-group(*),
  ::view-transition-old(*),
  ::view-transition-new(*) { animation: none !important; }
}


/* ======================= 07f. THÈME SOMBRE =========================

   Utilisé par la page Piñata. Plutôt que de réécrire chaque règle, on
   redéfinit la PALETTE : tout ce qui utilise --ink ou --paper s'inverse
   de lui-même. Header, filets, titres foil, barre de scroll, footer.

   Deux exceptions, volontaires :
     --work-text        le texte des blocs reste blanc, il est posé sur
                        une image et non sur le fond de la page
     --work-placeholder le gris des blocs sans visuel reste clair
   ================================================================== */
.theme-dark {
  --paper:      #0e0e0e;
  --ink:        #ffffff;
  --ink-soft:   #b6b6b6;
  --ink-faint:  #8c8c8c;
  --rule:       #ffffff;
  --line:       #2b2b2b;
  --rail-track: #3a3a3a;
  --rail-thumb: #ffffff;

  background: var(--paper);
  color: var(--ink);
}

/* Les voiles du header et du footer sont écrits en dur en blanc : la
   palette ne peut pas les retourner, il faut les redéclarer. */
.theme-dark .site-header::before {
  background: linear-gradient(
    to bottom,
    rgba(14, 14, 14, 0.96) 0%,
    rgba(14, 14, 14, 0.92) 34%,
    rgba(14, 14, 14, 0.64) 58%,
    rgba(14, 14, 14, 0.28) 79%,
    rgba(14, 14, 14, 0)   100%);
}

.theme-dark .site-footer::before {
  background: linear-gradient(
    to top,
    rgba(14, 14, 14, 0.96) 0%,
    rgba(14, 14, 14, 0.92) 34%,
    rgba(14, 14, 14, 0.64) 58%,
    rgba(14, 14, 14, 0.28) 79%,
    rgba(14, 14, 14, 0)   100%);
}

/* Losange irisé en permanence : il signale qu'on est déjà sur la page
   mystère. Le « X » passe en sombre — sur un dégradé saturé, c'est lui
   qui reste lisible partout, contrairement au blanc qui disparaît dans
   les jaunes. */
.diamond--live .diamond__shape::after { opacity: 1; }
.diamond--live .diamond__mark { color: #101010; }


/* ============================ 08. FOOTER =========================== */
/* Comme le header : aucun trait de séparation.
   Le détachement se fait par le vide, pas par une ligne — d'où le
   padding haut nettement plus généreux que le padding bas. */
.site-footer {
  position: relative;
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: 1.5rem;
  flex-wrap: wrap;
  padding: clamp(3.5rem, 9vh, 7rem) var(--gutter) clamp(2rem, 5vh, 3.5rem);
  font-size: var(--fs-nav);
  letter-spacing: var(--track-nav);
  text-transform: uppercase;
}

/* Voile dégradé, symétrique de celui du header : invisible sur le fond
   blanc actuel, mais déjà en place si tu poses un jour un visuel
   derrière le footer. Réglable avec --footer-fade. */
.site-footer::before {
  content: "";
  position: absolute;
  inset: auto 0 0 0;
  height: var(--footer-fade);
  z-index: -1;
  pointer-events: none;
  background: linear-gradient(
    to top,
    rgba(255, 255, 255, 0.94) 0%,
    rgba(255, 255, 255, 0.90) 34%,
    rgba(255, 255, 255, 0.62) 58%,
    rgba(255, 255, 255, 0.26) 79%,
    rgba(255, 255, 255, 0)   100%);
  opacity: 0.8;
}

/* Les liens portent la classe `nav__link` : ils héritent du roulement,
   du soulignement et du foil. `align-items: baseline` évite que les
   masques de roulement se décalent entre eux. */
.site-footer__links {
  display: flex;
  align-items: baseline;
  flex-wrap: wrap;
  gap: clamp(1.2rem, 3vw, 2.6rem);
}


/* ========================== 09. RESPONSIVE ========================= */

/* --- Tablette paysage / petit portable --- */
@media (max-width: 1080px) {
  :root {
    --media-left: 22%;
    --media-top: 18%;
    --intro-width: 60vw;   /* le texte reprend de la largeur */
  }
}

/* --- Tablette portrait et en dessous : le média passe en fond ---
   La vidéo occupe tout l'écran derrière le texte, avec un fondu
   nettement plus marqué pour préserver la lisibilité. */
@media (max-width: 860px) {
  .hero {
    align-content: end;
    padding-bottom: clamp(4rem, 14vh, 9rem);
  }

  .hero__content {
    max-width: 34rem;
    width: 100%;
  }

  .hero__media {
    top: 0;
    right: 0;
    bottom: 0;
    left: 0;

    -webkit-mask-image:
      radial-gradient(120% 80% at 70% 30%,
        #000 0%, rgba(0,0,0,0.72) 45%, rgba(0,0,0,0.30) 72%, transparent 100%);
    mask-image:
      radial-gradient(120% 80% at 70% 30%,
        #000 0%, rgba(0,0,0,0.72) 45%, rgba(0,0,0,0.30) 72%, transparent 100%);
  }

  /* Voile blanc : remonte le contraste du texte par-dessus l'image. */
  .hero__media::after {
    content: "";
    position: absolute;
    inset: 0;
    background: linear-gradient(
      to top,
      rgba(255,255,255,0.90) 0%,
      rgba(255,255,255,0.62) 32%,
      rgba(255,255,255,0.10) 70%,
      rgba(255,255,255,0) 100%);
  }

  /* La barre custom perd son sens sur tactile : on rend la native. */
  .scrollrail { display: none; }
  html { scrollbar-width: auto; }
}

/* --- Works : sous 900px, le bento devient une colonne unique.
   Choix délibéré : l'ordre que tu donnes aux projets dans le HTML est
   alors strictement respecté, et aucun trou ne peut apparaître dans la
   grille. Le bloc --tall garde sa double hauteur pour conserver du
   rythme dans la pile. --- */
@media (max-width: 900px) {
  .works__title,
  .works__intro,
  .work--narrow,
  .work--half,
  .work--wide,
  .work--full   { grid-column: span 12; }
}

@media (max-width: 620px) {
  :root { --work-row: clamp(150px, 42vw, 230px); }
}

/* --- Mobile --- */
@media (max-width: 560px) {
  :root {
    --track-nav: 0.14em;
    --track-logo: 0.24em;
  }
  .nav { gap: 0.95rem; }
  .site-footer { flex-direction: column; align-items: flex-start; gap: 1.2rem; }
}

/* --- Très petits écrans (iPhone SE) --- */
@media (max-width: 380px) {
  :root { --fs-nav: 0.62rem; }
  .nav { gap: 0.7rem; }
}

/* --- Écrans très larges ---
   Seule la grille Works est bornée, et généreusement : au-delà, une
   rangée de blocs devient une bande trop étirée pour être lisible.
   Le hero, lui, reste pleine largeur — c'est le principe de la compo. */
@media (min-width: 1800px) {
  .works__grid {
    max-width: 2100px;
    margin-inline: auto;
  }
}


/* ==================== 10. ACCESSIBILITÉ / MOTION =================== */

/* Respect du réglage système « réduire les animations ». */
@media (prefers-reduced-motion: reduce) {
  html { scroll-behavior: auto; }

  *, *::before, *::after {
    animation-duration: 0.001ms !important;
    animation-iteration-count: 1 !important;
    transition-duration: 0.001ms !important;
  }

  .diamond__shape { animation: none; transform: rotate(45deg); }
  .hero__media { transform: none !important; }
}

/* Impression : on retire le décor. */
@media print {
  .site-header, .scrollrail, .hero__media { display: none; }
  .hero { min-height: auto; padding-top: 2rem; }
}
