/* ReliefAE — famille unique du site (UI, slider, grille, About). */
@font-face {
  font-family: "ReliefAE";
  src: url("fonts/ReliefAE-Regular.ttf") format("truetype");
  font-style: normal;
  font-weight: 400;
  font-display: swap;
}
@font-face {
  font-family: "ReliefAE";
  src: url("fonts/ReliefAE-Italic.ttf") format("truetype");
  font-style: italic;
  font-weight: 400;
  font-display: swap;
}
@font-face {
  font-family: "ReliefAE";
  src: url("fonts/ReliefAE-SemiBold.ttf") format("truetype");
  font-style: normal;
  font-weight: 600;
  font-display: swap;
}
@font-face {
  font-family: "ReliefAE";
  src: url("fonts/ReliefAE-SemiBoldItalic.ttf") format("truetype");
  font-style: italic;
  font-weight: 600;
  font-display: swap;
}
@font-face {
  font-family: "ReliefAE";
  src: url("fonts/ReliefAE-Bold.ttf") format("truetype");
  font-style: normal;
  font-weight: 700;
  font-display: swap;
}
@font-face {
  font-family: "ReliefAE";
  src: url("fonts/ReliefAE-BoldItalic.ttf") format("truetype");
  font-style: italic;
  font-weight: 700;
  font-display: swap;
}

:root {
  --ink: #f4f1ea;
  --muted: rgba(244, 241, 234, .45);
  --hairline: rgba(244, 241, 234, .22);
  --black: #050505;
  --pad: clamp(18px, 1.5vw, 28px);
  /* Nom de variable conservé ("--mono" / "--druk") pour ne pas retoucher
     toutes les utilisations : les deux pointent vers ReliefAE. */
  --mono: "ReliefAE", "Helvetica Neue", Helvetica, Arial, sans-serif;
  --sans: "ReliefAE", "Helvetica Neue", Helvetica, Arial, sans-serif;
  --druk: "ReliefAE", "Helvetica Neue", Helvetica, Arial, sans-serif;
  /* Réglages du texte About pilotables depuis l'admin WordPress (menu
     Projets Auræ → Typographie) — voir aurae_portfolio_typography_css() qui
     réécrit ces valeurs par-dessus, dans un <style> ajouté après ce fichier.
     Les valeurs ci-dessous sont juste les valeurs par défaut du prototype
     autonome (hors WordPress). */
  --about-text-scale: 1;
  --about-scale-x: .79;
  --about-letter-spacing: 0em;
  --about-word-spacing: 0em;
  --text: clamp(13px, calc(.82vw + 2px), 15px);
  /* Tous les éléments cliquables (boutons/liens : Slider/Grid, filtres,
     Info/Mute/Fermer, titres de projet, cartes de la grid…) affichent leur
     texte 2px plus grand que le reste du site (--text), sur ordi comme sur
     tel — demandé pour les rendre plus faciles à lire/repérer comme
     interactifs. Le reste du texte (légendes, descriptions...) garde
     --text tel quel. */
  --text-clickable: calc(var(--text) + 2px);
  /* Masque de l'extrait home : 1.85:1 (Flat), bords latéraux légèrement
     bombés — calé sur la forme fournie. */
  --anamorphic: 1.85 / 1;
  --ease: cubic-bezier(.22, 1, .36, 1);
}

* { box-sizing: border-box; }
/* Empêche le drag natif des images/liens : sans ça, tout geste de
   souris/trackpad qui démarre sur une photo ou un lien (typiquement en
   scrollant "à la main" dans la galerie ou le slider, qui reposent tous les
   deux sur du scroll natif au clic-glissé) peut déclencher le drag HTML
   natif du navigateur. Sur Safari en particulier, ça affiche une vignette
   fantôme de l'élément qui suit le curseur — visuellement, on dirait que le
   curseur "se dédouble" et se fait "attirer" par les photos/liens. */
img, a, video {
  -webkit-user-drag: none;
  user-drag: none;
  -webkit-touch-callout: none;
}
/* Idem pour la sélection de texte : un clic-glissé rapide sur du texte
   (titres du slider, légendes, descriptions) peut déclencher une sélection
   au lieu du scroll natif, ce qui crée le même genre d'artefact visuel. */
.project-list, .project-gallery, .grid-card, .gallery-slide {
  -webkit-user-select: none;
  user-select: none;
}
/* Le thème WordPress fixe lui aussi une taille de police de base sur html/body
   (18px) qui gagnait le conflit puisque son CSS charge après le nôtre — d'où
   tout le site en trop gros malgré les autres correctifs. Verrouillé ici. */
/* Empêche Safari iOS d'agrandir automatiquement le texte dans les colonnes
   étroites ("text size adjust" / boosting) — cause fréquente d'un texte qui
   déborde et se fait couper sur mobile alors qu'il tient très bien à la
   même largeur en desktop/dans les outils de test. */
html { background: var(--black) !important; color: var(--ink) !important; font-size: 16px !important; -webkit-text-size-adjust: 100% !important; text-size-adjust: 100% !important; }
body {
  margin: 0 !important;
  background: var(--black) !important;
  color: var(--ink) !important;
  font-family: var(--mono) !important;
  font-size: var(--text) !important;
  font-synthesis: none;
  line-height: 1.4 !important;
  text-transform: uppercase !important;
}
/* Forcé en !important ET scopé à .site (spécificité plus élevée) : le thème
   WordPress applique ses propres styles globaux aux balises <button>/<a>
   (couleur, taille de police) qui sinon écrasent les nôtres. font-size est
   volontairement +2px (--text-clickable) plutôt que inherit : tous les
   éléments cliquables (texte de bouton/lien) doivent être un peu plus
   grands que le reste du site. */
button, a,
.site button, .site a {
  color: inherit !important;
  font-family: inherit !important;
  font-size: var(--text-clickable) !important;
  font-weight: inherit !important;
  line-height: inherit !important;
  letter-spacing: inherit !important;
  text-transform: inherit !important;
  outline: none;
  -webkit-tap-highlight-color: transparent;
}
button { border: 0; }
button::-moz-focus-inner { border: 0; }
a { text-decoration: none; }

.skip-link { position: fixed !important; z-index: 100; left: 1rem; top: 1rem; padding: .7rem; background: var(--ink); color: var(--black) !important; transform: translateY(-180%); }
.skip-link:focus { transform: translateY(0); }
/* Cascade progressive 100vh → 100svh → 100dvh → var(--app-height) : sur
   mobile, la barre d'adresse apparaît/disparaît en scrollant, ce qui change
   la hauteur "visuelle" réelle de l'écran en cours de route. 100svh (petite
   valeur garantie) laissait une bande noire en bas dès que la barre se
   masquait et que l'écran s'agrandissait au-delà de cette valeur figée.
   100dvh est censé suivre ce changement en direct, mais son comportement
   réel (fréquence de mise à jour, valeur utilisée avant la 1ère interaction)
   diffère selon les moteurs — la bande persistait sur Chrome/Brave alors que
   100dvh l'avait corrigée sur Safari. --app-height (posée en JS dans app.js
   via window.innerHeight/visualViewport, recalculée à chaque resize) ne
   dépend d'aucune implémentation dvh : elle mesure l'espace réellement
   disponible, ce qui la rend fiable sur tous les navigateurs. Elle est
   placée en dernier pour prendre le dessus dès qu'elle existe ; tant que le
   JS n'a pas tourné (tout premier rendu), la cascade dvh sert de repli. */
.site { min-height: 100vh; min-height: 100svh; min-height: 100dvh; min-height: var(--app-height, 100dvh); background: var(--black); overflow: hidden; text-transform: none; }
/* position/width forcés + scopés à .site (spécificité plus élevée) : le thème
   force "position: relative; width: 100%" sur <header>, ce qui annulait notre
   ancrage fixe et poussait le bloc hors du cadre à droite. */
.topbar, .site .topbar { position: fixed !important; z-index: 30; inset: var(--pad) 0 auto; width: 100% !important; display: flex; justify-content: center; align-items: center; gap: clamp(12px, 1.4vw, 22px); pointer-events: none; text-transform: none; }
.topbar > * { pointer-events: auto; }
.brand { display: block; width: clamp(92px, 8.8vw, 140px); }
.topbar .brand { width: 130px; }
.brand-logo { display: block; width: 100%; height: auto; }
.topbar .info-toggle, .topbar .write-toggle, .topbar .contact-link { opacity: .82; white-space: nowrap; font-size: calc(var(--text-clickable) + 3px) !important; }
.text-button, .filters button, .view-switcher button { padding: 0; background: transparent; cursor: pointer; opacity: .7; transition: opacity .25s ease; }
.text-button:hover, .text-button:focus-visible, .filters button:hover, .view-switcher button:hover { opacity: 1; }

.slider-view { position: relative; min-height: 100vh; min-height: 100svh; min-height: 100dvh; min-height: var(--app-height, 100dvh); isolation: isolate; text-transform: none; }
/* Masque 1.85:1 : côtés bombés, haut et bas creusés (anamorphique). */
.stage {
  position: absolute;
  z-index: 0;
  inset: auto;
  top: 50%;
  left: 50%;
  width: min(66vw, calc((var(--app-height, 100dvh) - 10rem) * 1.85));
  max-width: calc(100vw - 20rem);
  height: auto;
  max-height: calc(var(--app-height, 100dvh) - 10rem);
  aspect-ratio: var(--anamorphic);
  overflow: hidden;
  background: var(--black);
  transform: translate(-50%, -50%);
  clip-path: polygon(
    0.70% 0.00%,
    5.63% 0.13%,
    10.56% 0.25%,
    15.49% 0.36%,
    20.42% 0.47%,
    25.35% 0.57%,
    30.28% 0.65%,
    35.21% 0.71%,
    40.14% 0.76%,
    45.07% 0.79%,
    50.00% 0.80%,
    54.93% 0.79%,
    59.86% 0.76%,
    64.79% 0.71%,
    69.72% 0.65%,
    74.65% 0.57%,
    79.58% 0.47%,
    84.51% 0.36%,
    89.44% 0.25%,
    94.37% 0.13%,
    99.30% 0.00%,
    99.41% 5.00%,
    99.52% 10.00%,
    99.62% 15.00%,
    99.71% 20.00%,
    99.79% 25.00%,
    99.87% 30.00%,
    99.92% 35.00%,
    99.97% 40.00%,
    99.99% 45.00%,
    100.00% 50.00%,
    99.99% 55.00%,
    99.97% 60.00%,
    99.92% 65.00%,
    99.87% 70.00%,
    99.79% 75.00%,
    99.71% 80.00%,
    99.62% 85.00%,
    99.52% 90.00%,
    99.41% 95.00%,
    99.30% 100.00%,
    94.37% 99.87%,
    89.44% 99.75%,
    84.51% 99.64%,
    79.58% 99.53%,
    74.65% 99.43%,
    69.72% 99.35%,
    64.79% 99.29%,
    59.86% 99.24%,
    54.93% 99.21%,
    50.00% 99.20%,
    45.07% 99.21%,
    40.14% 99.24%,
    35.21% 99.29%,
    30.28% 99.35%,
    25.35% 99.43%,
    20.42% 99.53%,
    15.49% 99.64%,
    10.56% 99.75%,
    5.63% 99.87%,
    0.70% 100.00%,
    0.59% 95.00%,
    0.48% 90.00%,
    0.38% 85.00%,
    0.29% 80.00%,
    0.21% 75.00%,
    0.13% 70.00%,
    0.08% 65.00%,
    0.03% 60.00%,
    0.01% 55.00%,
    0.00% 50.00%,
    0.01% 45.00%,
    0.03% 40.00%,
    0.08% 35.00%,
    0.13% 30.00%,
    0.21% 25.00%,
    0.29% 20.00%,
    0.38% 15.00%,
    0.48% 10.00%,
    0.59% 5.00%
  );
}
.stage-layer { position: absolute; inset: -2%; background-position: center; background-size: cover; transition: opacity .55s var(--ease), transform 1.1s var(--ease), filter .55s ease; transform: scale(1.055); filter: saturate(.94) brightness(.96); will-change: opacity, transform; }
.stage-layer.is-instant { transition: none; }
.stage-layer.is-visible { transform: scale(1); }
.stage-layer.is-current { filter: none; }
.stage-layer-a { opacity: 1; }
.stage-layer-b { opacity: 0; }
.stage-video { position: absolute; inset: 0; width: 100%; height: 100%; object-fit: cover; object-position: center; pointer-events: none; background: transparent; opacity: 0; }
/* Safari iOS dessine un bouton Play natif sur toute <video> en pause ou
   dont l'autoplay inline a échoué (Low Power Mode). Ces covers n'ont pas
   de contrôles : on masque l'overlay, le poster / la première frame restent. */
.stage-video::-webkit-media-controls,
.stage-video::-webkit-media-controls-start-playback-button,
.stage-video::-webkit-media-controls-overlay-play-button,
.grid-card-video::-webkit-media-controls,
.grid-card-video::-webkit-media-controls-start-playback-button,
.grid-card-video::-webkit-media-controls-overlay-play-button,
.project-modal-video::-webkit-media-controls-start-playback-button,
.project-modal-video::-webkit-media-controls-overlay-play-button {
  display: none !important;
  -webkit-appearance: none;
  opacity: 0;
  pointer-events: none;
  width: 0;
  height: 0;
}
.stage::after { content: none; }
/* Le SVG de bruit n'a qu'un viewBox (pas de width/height), donc sans taille
   explicite ici, il n'a pas de dimension intrinsèque : le navigateur
   l'étirait en UNE seule tuile géante sur tout l'élément (background-size
   auto par défaut) au lieu de répéter un petit motif — d'où un grain "trop
   gros et pixellisé" plutôt qu'un fin grain de film. background-size fixe
   la taille réelle de la tuile (répétée ensuite normalement). */
.stage-grain, .project-media-grain { position: absolute; inset: 0; z-index: 2; opacity: .13; mix-blend-mode: soft-light; pointer-events: none; background-image: url("data:image/svg+xml,%3Csvg viewBox='0 0 180 180' xmlns='http://www.w3.org/2000/svg'%3E%3Cfilter id='n'%3E%3CfeTurbulence type='fractalNoise' baseFrequency='.95' numOctaves='3' stitchTiles='stitch'/%3E%3C/filter%3E%3Crect width='100%25' height='100%25' filter='url(%23n)' opacity='.8'/%3E%3C/svg%3E"); background-size: 180px 180px; background-repeat: repeat; }
/* Sur un projet photo (galerie ou image unique, cf. .is-photo-media posée par
   renderProjectMedia() dans app.js), le grain est complètement retiré du fond
   de la modale — demandé explicitement, il ne reste que sur les vidéos. */
.project-modal.is-photo-media .project-media-grain { display: none; }

/* .project-list est maintenant le vrai conteneur de scroll natif (comme
   .project-gallery pour la galerie photo) : plein écran (inset:0) pour que
   la molette/le doigt fonctionne où qu'on scrolle dans le slider (pas
   seulement au-dessus des titres) — c'est exactement le problème qui avait
   fait abandonner le scroll natif ici la première fois (.project-list était
   une petite boîte, voisine de .stage, donc hors de sa zone rien ne
   défilait). Le rendu reste visuellement identique (colonne étroite à
   gauche) grâce à la largeur fixée sur chaque <li> + à un masque qui ne
   laisse apparaître qu'une bande autour du centre vertical. Boucle infinie :
   même technique que la galerie (titres répétés en plusieurs jeux, recalage
   silencieux de scrollTop) — mais ici uniquement à l'arrêt du scroll, voir
   handleSliderRest() dans app.js. */
/* Pas de mask-image ici : chaque titre ne fait que 50px (contre ~58vh pour
   une photo de la galerie), donc les 3 jeux triplés tiennent largement
   ensemble dans la fenêtre visible — un mask sur le conteneur ne masquerait
   pas "les autres jeux" puisqu'ils sont dans la même fenêtre. À la place,
   updateSliderActive() (app.js) calcule à chaque scroll l'opacité de
   CHAQUE <li> selon sa distance au centre — mêmes valeurs que le fade du
   mask précédent, mais fiable quel que soit le nombre de jeux visibles. */
/* background quasi invisible (1% de noir) volontaire : Chrome n'inclut pas
   les zones "vides" (sans rien peint dedans) d'un conteneur scrollable dans
   sa détection de survol pour la molette — vérifié en conditions réelles,
   sans ce fond la molette ne fonctionnait que juste au-dessus des titres
   eux-mêmes, pas sur le reste de l'écran (le bug qu'on cherche justement à
   éviter). Avec ce fond quasi transparent, tout le slider redevient
   scrollable où qu'on scrolle. */
/* Aimantage natif (scroll-snap) : un geste ne peut plus s'arrêter entre deux
   titres, il vient toujours caler un titre pile sur l'axe central (celui des
   crédits, à 50%). C'est le navigateur qui gère la fin de course, donc on
   garde l'inertie réelle du trackpad/du doigt — mais l'état actif ne bascule
   plus au pixel près comme quand on pouvait s'immobiliser à mi-chemin. Le
   recalage de la boucle infinie ne se fait plus qu'à l'arrêt (scrollend, voir
   handleSliderRest dans app.js) et toujours d'un multiple exact de la hauteur
   d'un jeu, donc il retombe sur un point d'aimantage valide : pas de conflit
   avec le snap, contrairement à la galerie photo. */
.project-list { position: absolute; inset: 0; overflow-y: auto; overflow-x: hidden; scrollbar-width: none; -ms-overflow-style: none; background: rgba(0, 0, 0, .01); scroll-snap-type: y mandatory; }
.project-list.is-over-stage { cursor: pointer; }
.project-list::-webkit-scrollbar { display: none; }
.project-track { margin: 0; padding: 50vh 0 50vh var(--pad); list-style: none; display: flex; flex-direction: column; align-items: flex-start; }
.project-list li { height: 30px; width: min(280px, 22vw); flex: 0 0 auto; scroll-snap-align: center; }
/* font-weight n'est PAS dans la transition : l'animer refait la mise en page
   et le rendu du texte à chaque changement d'actif (le titre change de
   largeur), ce qui se voyait en saccade pendant un défilement rapide. */
.project-list button { display: block; width: calc(100% - .35rem); min-height: 30px; padding: 0; text-align: left; background: transparent; color: var(--muted) !important; font-weight: 400 !important; cursor: pointer; opacity: 1; transition: color .3s ease, transform .55s var(--ease); }
.project-list button:hover, .project-list button:focus-visible { color: rgba(244, 241, 234, .72) !important; transform: translateX(.35rem); }
.project-list button.is-active { color: var(--ink) !important; font-weight: 600 !important; transform: translateX(.35rem); }
.project-list .project-title { display: block; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
.credits { position: absolute; z-index: 2; right: var(--pad); top: 50%; width: auto; max-width: min(240px, 22vw); margin: 0; display: grid; grid-template-columns: auto auto; column-gap: .35rem; row-gap: .15rem; transform: translateY(-50%); pointer-events: none; text-transform: none; }
.credits div { display: contents; }
.credits dt { color: var(--muted); font-style: italic; font-weight: 400; }
.credits dd { margin: 0; font-style: normal; font-weight: 400; }

.view-switcher { position: fixed !important; z-index: 30; left: 50%; bottom: var(--pad); display: flex; gap: .55rem; opacity: 1; transform: translateX(-50%); text-transform: none; transition: opacity .35s ease, transform .5s var(--ease); }
.view-switcher button { opacity: .4; font-size: calc(var(--text-clickable) + 3px) !important; }
.view-switcher button[aria-pressed="true"] { opacity: 1; }
.site[data-view="grid"] .view-switcher,
.site:has(.info-panel.is-open) .view-switcher { opacity: 0; visibility: hidden; pointer-events: none; }

/* La grille défile sous le footer fixe (Gallery, Who we are). */
.grid-view { display: none; flex-direction: column; min-height: 0; padding: clamp(140px, 16vh, 200px) var(--pad) 3rem; background: var(--black); }
.site[data-view="grid"],
.site:has(.info-panel.is-open) { display: flex; flex-direction: column; min-height: 100vh; min-height: 100svh; min-height: 100dvh; min-height: var(--app-height, 100dvh); overflow: visible; }
.site:has(.info-panel.is-open) #portfolio { display: none; }
.site[data-view="grid"] .slider-view { display: none; }
.site[data-view="grid"] .grid-view { display: flex; flex: 1 0 auto; }
.filters { display: flex; gap: .85rem; margin-bottom: clamp(34px, 3vw, 50px); }
.filters button { opacity: .35; }
.filters button[aria-pressed="true"] { opacity: 1; }
.project-grid { flex: 1 0 auto; display: grid; grid-template-columns: repeat(3, minmax(0, 1fr)); gap: clamp(20px, 2.2vw, 32px) clamp(5px, .3vw, 8px); align-content: start; }
.grid-card { position: relative; display: grid; grid-template-rows: auto auto; min-width: 0; padding: 0; overflow: hidden; text-align: left; cursor: pointer; background: var(--black); color: var(--ink); opacity: 0; transform: translateY(38px); animation: gridReveal .9s var(--ease) forwards; animation-delay: calc(100ms + var(--i, 0) * 65ms); }
/* --image n'est posée qu'au chargement paresseux de la carte (voir
   loadGridCardMedia() dans app.js) — tant que ce n'est pas encore fait, la
   variable n'existe pas du tout. Sans repli explicite ("none"), var(--image)
   invalide alors TOUTE la déclaration background (pas juste cette couche),
   ce qui ferait disparaître aussi --tone : la carte serait vide/transparente
   au lieu d'afficher sa couleur de repli en attendant la vraie photo. */
.grid-card-media { position: relative; display: block; aspect-ratio: 1.65 / 1; overflow: hidden; background: var(--image, none), var(--tone); background-size: var(--fit, cover), cover; background-position: var(--focal, center); transform: scale(1.015); transition: transform .9s var(--ease), filter .55s ease; }
.grid-card:hover .grid-card-media, .grid-card:focus-visible .grid-card-media { transform: scale(1.055); filter: saturate(.78); }
.grid-card-video { position: absolute; inset: 0; width: 100%; height: 100%; object-fit: var(--fit, cover); object-position: var(--focal, center); pointer-events: none; }
.grid-card-copy { display: flex; justify-content: space-between; align-items: start; gap: 1rem; padding: 13px 2px 0; opacity: 1; transform: none; }
.grid-card-copy .grid-card-category { color: var(--muted); transition: color .3s ease; }
.grid-card:hover .grid-card-copy .grid-card-category, .grid-card:focus-visible .grid-card-copy .grid-card-category { color: var(--ink); }
.grid-card[hidden] { display: none; }

.site-footer {
  display: none;
  position: relative;
  z-index: 2;
  width: 100%;
  margin-top: auto;
  align-items: center;
  gap: clamp(1.1rem, 2vw, 1.8rem);
  padding: 1.15rem var(--pad) 1.25rem;
  background: var(--black);
  text-transform: none;
}
.site-footer::before {
  content: "";
  position: absolute;
  top: 0;
  left: var(--pad);
  right: var(--pad);
  height: 1px;
  background: var(--hairline);
}
.site[data-view="grid"] .site-footer,
.site:has(.info-panel.is-open) .site-footer { display: flex; }
.site:has(.project-modal.is-open) .site-footer { display: none; }
.site-footer-logo { width: 88px; flex: 0 0 auto; }
.site-footer a.site-footer-logo,
.site-footer a.site-footer-logo:hover,
.site-footer a.site-footer-logo:focus-visible { opacity: 1; }
.site-footer-nav, .site-footer-social { display: flex; align-items: center; gap: clamp(.85rem, 1.6vw, 1.35rem); }
.site-footer-social { margin-left: auto; }
.site-footer button, .site-footer a { opacity: .82; white-space: nowrap; background: transparent; font-size: calc(var(--text-clickable) + 1px) !important; }
.site-footer button:hover, .site-footer a:hover,
.site-footer button:focus-visible, .site-footer a:focus-visible { opacity: 1; }
.site-footer [data-view-button][aria-pressed="true"],
.site-footer .info-toggle[aria-expanded="true"] { opacity: 1; }
.site:has(.info-panel.is-open) .site-footer [data-view-button][aria-pressed="true"] { opacity: .82; }
.site:has(.info-panel.is-open) .site-footer .info-toggle { opacity: 1; }
.site:has(#write-panel.is-open) .site-footer .info-toggle { opacity: .82; }
.site:has(#write-panel.is-open) .site-footer .write-toggle { opacity: 1; }

/* ReliefAE partout dans le panneau About, y compris .info-statement p
   (--druk pointe aussi vers ReliefAE). */
.site:has(.info-panel.is-open) .topbar { visibility: hidden; }
.info-panel { position: fixed !important; z-index: 60; inset: 0; display: grid; grid-template-rows: auto minmax(0, 1fr); grid-template-columns: minmax(0, 1fr); padding: var(--pad); overflow: hidden; background: var(--black); color: var(--ink); font-family: var(--mono); visibility: hidden; opacity: 0; clip-path: inset(0 0 100% 0); transition: clip-path .8s var(--ease), opacity .45s ease, visibility 0s linear .8s; text-transform: none; }
.info-panel.is-open { position: relative !important; inset: auto; display: flex; flex-direction: column; width: 100%; height: auto; flex: 1 0 auto; overflow: visible; visibility: visible; opacity: 1; clip-path: inset(0); transition-delay: 0s; }
.info-panel:focus { outline: none; }
.info-head { display: flex; justify-content: center; align-items: center; gap: clamp(12px, 1.4vw, 22px); min-width: 0; }
.info-head .brand { width: 130px; }
.info-head .info-toggle, .info-head .write-toggle, .info-head .contact-link { opacity: .82; white-space: nowrap; font-size: calc(var(--text-clickable) + 3px) !important; }
.info-head .info-toggle[aria-expanded="true"],
.info-head .write-toggle[aria-expanded="true"] { opacity: 1; }
.info-content { align-self: stretch; justify-self: stretch; min-width: 0; min-height: 0; overflow: visible; width: 100%; max-width: none; margin: 0; padding: clamp(56px, 8vh, 104px) 0 clamp(48px, 7vh, 88px); }
.about-intro { display: grid; grid-template-columns: minmax(0, .96fr) minmax(0, 1.04fr); gap: clamp(28px, 4.2vw, 72px); align-items: center; }
.about-intro:not(:has(.about-portrait)) { grid-template-columns: minmax(0, min(38rem, 100%)); justify-content: center; }
.about-portrait { margin: 0; }
.about-portrait img { display: block; width: 100%; height: auto; }
.about-copy, .about-body { min-width: 0; max-width: 100%; }
.about-headline { margin: 0 0 1.35rem; max-width: min(42em, calc(100vw - 4.75rem)); font-weight: 700; font-size: clamp(17px, 1.35vw, 21px); line-height: 1.35; text-transform: none; }
.about-body p { margin: 0 0 1.05rem; max-width: min(42em, calc(100vw - 4.75rem)); font-size: clamp(15px, 1.15vw, 18px); line-height: 1.55; text-transform: none; }
.about-body p:last-child { margin-bottom: 0; }
.about-rule { margin: clamp(40px, 6vh, 72px) 0 clamp(36px, 5vh, 60px); border: 0; border-top: 1px solid var(--hairline); }
.about-faces { text-align: center; }
.about-faces h2 { margin: 0; font-size: clamp(18px, 1.4vw, 23px); font-weight: 700; line-height: 1.2; text-transform: none; }
.about-faces-lead { margin: .4rem 0 0; font-style: italic; font-weight: 400; font-size: clamp(15px, 1.15vw, 18px); line-height: 1.4; text-transform: none; }
.about-people { display: grid; grid-template-columns: repeat(3, minmax(0, 1fr)); gap: clamp(16px, 2.4vw, 32px); max-width: 860px; margin: clamp(28px, 4.5vh, 52px) auto 0; padding: 0; list-style: none; }
.about-people figure { margin: 0; }
.about-people img { display: block; width: 100%; aspect-ratio: 1; object-fit: cover; }
.about-people figcaption { margin-top: .9rem; }
.about-people strong, .about-people span { display: block; font-size: clamp(14px, 1.05vw, 17px); line-height: 1.35; text-transform: none; }
.about-people strong { font-weight: 700; }
.about-people span { margin-top: .15rem; font-weight: 400; }

.write-layout { display: grid; grid-template-columns: minmax(0, 1.15fr) minmax(0, .72fr); gap: clamp(48px, 8vw, 140px); align-items: start; width: min(980px, 100%); margin: 0 auto; }
.write-panel.is-open .info-content {
  flex: 1 0 auto;
  display: flex;
  flex-direction: column;
  justify-content: center;
  padding-top: clamp(24px, 4vh, 48px);
  padding-bottom: clamp(24px, 4vh, 48px);
}
.write-form h2 { margin: 0 0 1.75rem; font-size: clamp(18px, 1.4vw, 22px); font-weight: 700; line-height: 1.3; text-transform: none; }
.write-form label { display: block; margin: 0 0 1.45rem; font-size: clamp(16px, 1.2vw, 18px); font-weight: 400; text-transform: none; }
.write-form input, .write-form textarea { display: block; width: 100%; margin-top: .9rem; padding: .35rem 0 .55rem; border: 0; border-bottom: 1px solid rgba(244, 241, 234, .55); border-radius: 0; background: transparent; color: inherit; font: inherit; font-size: inherit; line-height: 1.4; text-transform: none; resize: vertical; appearance: none; }
.write-form textarea { min-height: 2.6rem; resize: none; }
.write-form input:focus, .write-form textarea:focus { outline: none; border-bottom-color: var(--ink); }
.write-form button[type="submit"] { margin-top: .4rem; padding: 0; background: transparent; cursor: pointer; font-size: clamp(16px, 1.2vw, 18px) !important; text-transform: none; }
.write-form button[type="submit"]:disabled { opacity: .45; }
.write-note { margin: 1rem 0 0; text-transform: none; }
.write-honeypot { position: absolute; width: 1px; height: 1px; overflow: hidden; clip-path: inset(50%); }
.write-aside p { margin: 0 0 1.7rem; font-size: clamp(16px, 1.2vw, 18px); line-height: 1.45; text-transform: none; }
.write-aside strong, .write-aside a, .write-aside span { display: block; font-weight: 400; text-transform: none; }
.write-aside strong { margin-bottom: .45rem; font-weight: 700; }
.write-social { display: flex; flex-direction: column; align-items: flex-start; gap: .4rem; margin-top: 2.4rem; font-size: clamp(16px, 1.2vw, 18px); line-height: 1.45; }
.write-social a, .write-social span { font-size: inherit !important; font-weight: 400; text-transform: none; }

.project-modal { position: fixed !important; z-index: 50; inset: 0; color: var(--ink); background: var(--black); visibility: hidden; opacity: 0; pointer-events: none; clip-path: inset(100% 0 0); transition: clip-path .85s var(--ease), opacity .4s ease, visibility 0s linear .85s; }
.project-modal.is-open { visibility: visible; opacity: 1; pointer-events: auto; clip-path: inset(0); transition-delay: 0s; }
/* Les projets vidéo/photo (média unique) s'affichent "dans leur format"
   (contain), centrés, avec un fond noir plein qui comble les marges — comme
   la référence envoyée par le client — plutôt qu'en plein cadre recadré
   (cover). La couleur de fond du letterboxing est noire pour ces cas
   (--modal-media-bg), alors que la galerie photo (plusieurs images, pas de
   --modal-image ici) garde son fond dégradé (--modal-tone) derrière le
   carrousel, comme avant. */
.project-media { position: absolute; inset: 0; background-color: var(--modal-media-bg, var(--modal-tone, #222)); background-image: var(--modal-image, none); background-repeat: no-repeat; background-position: var(--modal-position, center); background-size: var(--modal-fit, contain); transform: scale(1.08); transition: transform 1.25s var(--ease), background-color .4s ease, inset .4s var(--ease); }
.project-modal.is-open .project-media { transform: scale(1); }
/* Vidéo/photo unique (pas la galerie) : la référence envoyée par le client
   montre un média nettement plus petit que l'écran, encadré de noir sur ses
   4 côtés — pas juste letterboxé sur un seul axe comme le ferait "contain"
   seul. On réduit donc la boîte elle-même (via `inset`), qui reste centrée ;
   Info/Close/Prev/Next/la légende restent épinglés aux bords de l'écran,
   inchangés, puisqu'ils sont positionnés par rapport à .project-modal et non
   à .project-media. La galerie photo (plusieurs images) garde son cadre
   plein écran d'origine, non concernée par ce resserrement. */
.project-modal.is-media-framed .project-media { inset: 13% 15%; }
/* Vidéo : le cadre 13%/15% a le ratio de l'écran, pas celui de la source
   (Njørden 4:3, WAP 5:4, le reste 16:9), et object-fit:cover recadrait la
   vidéo pour le remplir. La boîte prend donc le ratio réel (--modal-aspect,
   posé en JS depuis videoWidth/videoHeight) et reste la plus grande qui tienne
   dans cette même marge. Le noir autour est le fond de la modale. */
.project-modal.is-media-framed.is-video-media .project-media {
  inset: auto;
  top: 50%;
  left: 50%;
  width: min(70vw, calc(74dvh * var(--modal-aspect, 16 / 9)));
  max-width: 70vw;
  height: auto;
  max-height: 74dvh;
  aspect-ratio: var(--modal-aspect, 16 / 9);
  transform: translate(-50%, -50%) scale(1.08);
}
.project-modal.is-open.is-media-framed.is-video-media .project-media {
  transform: translate(-50%, -50%) scale(1);
}
/* Tant que le vrai ratio n'est pas connu, le cadre resterait en 16:9 puis
   sauterait (souvent au premier chargement, avant que les métadonnées
   soient en cache). On le garde invisible jusqu'à ce que --modal-aspect
   soit le ratio réel. */
.project-modal.is-media-framed.is-video-media:not(.has-aspect) .project-media {
  visibility: hidden;
}
.project-modal.is-media-framed.is-video-media .project-modal-video {
  object-fit: contain;
}
/* pointer-events:none est essentiel ici : ce dégradé décoratif (::after)
   est généré après .project-media-content dans l'ordre de peinture, donc il
   recouvrait entièrement la galerie photo/vidéo en dessous et interceptait
   tout scroll/clic avant qu'il n'atteigne les vraies photos — c'était la
   cause du blocage du scroll dans les projets photo. */
.project-media::after { content: ""; position: absolute; inset: 0; z-index: 2; pointer-events: none; background: linear-gradient(0deg, rgba(0,0,0,.35), transparent 40%); }
.project-media-content { position: absolute; inset: 0; z-index: 1; }
.project-modal-video { position: absolute; inset: 0; z-index: 1; width: 100%; height: 100%; object-fit: var(--modal-fit, contain); object-position: var(--modal-position, center); pointer-events: none; background: transparent; }

/* Galerie photo (projets à plusieurs photos) : carrousel circulaire "à plat"
   (pas 3D) qui tourne indéfiniment dans les deux sens — mais piloté par le
   VRAI scroll natif du navigateur (.project-gallery est un vrai conteneur
   overflow-y:auto), pas simulé en JS : c'est ce qui donne l'inertie/la
   fluidité natives du trackpad/de la molette/du doigt. buildGallery() dans
   app.js se contente d'écouter le scroll pour (1) mettre à jour la classe
   .is-current sur la photo la plus proche du centre — animée par une
   transition CSS ci-dessous — et (2) faire boucler à l'infini : les photos
   sont triplées dans le DOM, et dès qu'on s'approche du 1er ou du 3ᵉ jeu on
   recale scrollTop silencieusement d'un jeu (contenu identique = invisible).
   Jamais de scroll-snap ici : ça entre en conflit avec ce recalage. */
/* Même fond quasi invisible que .project-list, et pour la même raison :
   dans le padding/les espaces entre photos, il n'y a rien de peint, et
   Chrome peut alors ignorer la molette à cet endroit précis du conteneur
   scrollable. */
.project-gallery { position: absolute; inset: 0; overflow-y: auto; overflow-x: hidden; scrollbar-width: none; -ms-overflow-style: none; background: rgba(0, 0, 0, .01); }
.project-gallery::-webkit-scrollbar { display: none; }
.gallery-track { display: flex; flex-direction: column; align-items: center; gap: clamp(28px, 4.5vw, 64px); padding: 50vh 0; }
.gallery-slide { flex: 0 0 auto; height: 58vh; width: 100%; display: flex; align-items: center; justify-content: center; opacity: .32; transform: scale(.86); transition: opacity .45s ease, transform .45s cubic-bezier(.22, .61, .36, 1); }
.gallery-slide.is-current { opacity: 1; transform: scale(1); }
.gallery-slide img { max-width: 100%; max-height: 100%; width: auto; height: auto; object-fit: contain; }
.gallery-counter { position: absolute; z-index: 2; right: clamp(20px, 3vw, 40px); bottom: clamp(90px, 9vw, 130px); font-size: .72rem; letter-spacing: .08em; opacity: .75; pointer-events: none; }
.modal-top { position: absolute; z-index: 3; inset: var(--pad) var(--pad) auto; display: flex; justify-content: space-between; }
.modal-mute[hidden],
.modal-fullscreen[hidden] { display: none; }
.modal-fullscreen {
  position: absolute;
  z-index: 3;
  left: 50%;
  top: 0;
  display: none;
  padding: .35rem .55rem;
  transform: translate(-50%, .65rem);
  background: rgba(0, 0, 0, .35);
  cursor: pointer;
  white-space: nowrap;
}
.modal-nav { position: absolute; z-index: 3; top: 50%; padding: .3rem .45rem; background: rgba(0,0,0,.35); cursor: pointer; transform: translateY(-50%); }
.modal-prev { left: var(--pad); }
.modal-next { right: var(--pad); }
.modal-caption { position: absolute; z-index: 3; bottom: var(--pad); left: 50%; display: flex; gap: .7rem; transform: translateX(-50%); white-space: nowrap; opacity: 1; visibility: visible; transition: opacity .3s ease, visibility 0s linear 0s; }
/* La légende (titre | client) partage la même bande verticale que la
   description quand le panneau Info est ouvert : sur une description
   longue (2 lignes), le texte peut s'étendre jusqu'au centre où se trouve
   la légende et venir se superposer avec elle. On masque simplement la
   légende pendant que le détail est affiché (l'info titre/client y est de
   toute façon redondante avec le Client affiché dans le bloc détail). */
.project-modal.is-info-open .modal-caption { opacity: 0; visibility: hidden; transition: opacity .3s ease, visibility 0s linear .3s; }
.project-modal.is-info-open .modal-top { justify-content: flex-start; }
.project-modal.is-info-open .modal-info,
.project-modal.is-info-open .modal-mute,
.project-modal.is-info-open .modal-fullscreen,
.project-modal.is-info-open .modal-prev,
.project-modal.is-info-open .modal-next { display: none; }
.modal-details { position: absolute; z-index: 2; inset: 0; display: grid; grid-template-columns: 1fr 320px; align-items: end; gap: 3rem; padding: clamp(90px, 12vw, 170px) var(--pad) var(--pad); background: #330000; opacity: 0; visibility: hidden; transition: opacity .35s ease, visibility 0s linear .35s; }
.modal-details.is-open { opacity: 1; visibility: visible; transition-delay: 0s; }
.modal-details > p { max-width: 58ch; margin: 0; font-size: clamp(20px, 2.8vw, 44px); line-height: 1.08; letter-spacing: -.035em; text-transform: none; }
.modal-details dl { margin: 0; font-size: clamp(17px, 1.35vw, 21px); line-height: 1.35; }
.modal-details dl div { display: flex; justify-content: space-between; padding: .4rem 0; border-bottom: 1px solid var(--hairline); }
.modal-details dd { margin: 0; }

/* Transition slider ↔ grid : un fondu au noir avec un très léger zoom sur le
   contenu, plutôt qu'un rideau géométrique qui tranche l'écran. Courbe
   ease-in-out symétrique (au lieu d'un ease-out appliqué sur tout le
   cycle aller-retour, qui rendait le fondu inégal/pas smooth) ; le flou a
   été retiré — un filter animé en continu accroche facilement sur certains
   écrans/navigateurs et donnait un rendu moins fluide que prévu. */
.view-transition { position: fixed !important; z-index: 100; inset: 0; background: var(--black); pointer-events: none; opacity: 0; }
.site.is-view-transitioning { overflow: hidden !important; }
.site.is-view-transitioning .view-transition { animation: viewFade .62s cubic-bezier(.45, 0, .55, 1) both; }
main#portfolio { will-change: opacity, transform; }
.site.is-view-transitioning main#portfolio { animation: viewFocusPull .62s cubic-bezier(.45, 0, .55, 1) both; }

@keyframes listReveal { from { opacity: 0; transform: translateX(-18px); } to { opacity: 1; transform: none; } }
@keyframes gridReveal { from { opacity: 0; transform: translateY(44px); } to { opacity: 1; transform: none; } }
@keyframes infoReveal { to { opacity: 1; transform: none; } }
@keyframes viewFade {
  0% { opacity: 0; }
  45%, 55% { opacity: 1; }
  100% { opacity: 0; }
}
@keyframes viewFocusPull {
  0% { opacity: 1; transform: scale(1); }
  45%, 55% { opacity: .25; transform: scale(1.02); }
  100% { opacity: 1; transform: scale(1); }
}
@media (max-width: 1100px) {
  .project-modal.is-media-framed.is-video-media .project-media {
    width: min(
      calc(100% - var(--pad) * 2),
      calc((var(--app-height, 100dvh) - 6.5rem) * var(--modal-aspect, 16 / 9))
    );
    max-width: calc(100% - var(--pad) * 2);
    max-height: calc(var(--app-height, 100dvh) - 6.5rem);
  }
}

@media (max-width: 820px) {
  .stage {
    inset: 0;
    top: 0;
    left: 0;
    width: 100%;
    max-width: none;
    height: 100%;
    max-height: none;
    aspect-ratio: auto;
    transform: none;
    clip-path: none;
  }
  .project-list li { width: calc(52% - var(--pad)); height: 34px; }
  .project-list button { min-height: 34px; font-size: 16px !important; }
  .credits { display: none; }
  .grid-view { padding: 125px var(--pad) 2.5rem; }
  .project-grid { grid-template-columns: 1fr; }
  .grid-card-copy { padding: 11px 2px 0; }
  .site[data-view="grid"] .site-footer,
  .site:has(.info-panel.is-open) .site-footer {
    display: grid;
    grid-template-columns: auto 1fr;
    grid-template-areas: "logo social";
    align-items: center;
    column-gap: 1rem;
    row-gap: 0;
    padding: 1rem var(--pad) 1.05rem;
  }
  .site-footer-logo { grid-area: logo; width: 72px; }
  .site-footer-social {
    grid-area: social;
    margin-left: 0;
    flex: 0 0 auto;
    max-width: none;
    width: auto;
    justify-self: end;
    justify-content: flex-end;
    gap: 1.15rem;
  }
  .site-footer-nav { display: none; }
  .site-footer button,
  .site-footer a { padding-block: .55rem; }
  .site-footer a.site-footer-logo { padding-block: 0; }
  .project-modal.is-video-media:not(.is-info-open) .modal-fullscreen:not([hidden]) { display: inline-block; }
  .modal-mute:not([hidden]) {
    position: absolute;
    left: 50%;
    top: 0;
    transform: translateX(-50%);
  }
  .modal-caption {
    left: 50%;
    right: auto;
    transform: translateX(-50%);
    max-width: calc(100% - var(--pad) * 2);
    justify-content: center;
    text-align: center;
    white-space: normal;
  }
  .info-head { gap: 10px; justify-content: space-between; position: relative; }
  .info-head .brand { width: 92px; position: absolute; left: 50%; transform: translateX(-50%); }
  .info-head .info-toggle, .info-head .write-toggle, .info-head .contact-link { font-size: var(--text-clickable) !important; }
  .topbar, .site .topbar {
    justify-content: space-between;
    padding-inline: var(--pad);
    gap: 0;
  }
  .topbar .brand {
    width: 92px;
    position: absolute;
    left: 50%;
    transform: translateX(-50%);
  }
  .topbar .info-toggle, .topbar .write-toggle, .topbar .contact-link { font-size: var(--text-clickable) !important; }
  .write-layout { grid-template-columns: 1fr; gap: 2.75rem; }
  .info-content { padding: 56px 0 28px; }
  .write-panel.is-open .info-content { padding-top: 36px; padding-bottom: 28px; }
  .about-intro,
  .about-intro:not(:has(.about-portrait)) { grid-template-columns: minmax(0, 1fr); width: 100%; max-width: 100%; gap: 28px; justify-content: stretch; }
  .about-copy, .about-body { width: 100%; max-width: 100%; }
  .about-headline,
  .about-body p { max-width: none !important; overflow-wrap: anywhere; }
  .about-people { gap: 14px; }
  .modal-details { grid-template-columns: 1fr; }
}

@media (max-width: 540px) {
  .about-people { grid-template-columns: 1fr; max-width: 280px; }
  .project-modal.is-media-framed.is-video-media .project-media {
    width: min(
      calc(100% - 4.75rem),
      calc((var(--app-height, 100dvh) - 6.5rem) * var(--modal-aspect, 16 / 9))
    );
    max-width: calc(100% - 4.75rem);
  }
}

/* Zone de tap élargie (44px min, repère Apple/WCAG) sur les petits boutons
   texte (Slider/Grid, filtres, Info/Mute/Close de la modale, about…) : leur
   visuel garde sa taille/police d'origine (juste le texte, padding:0), mais
   au doigt on loupait régulièrement le bouton car la zone cliquable réelle
   ne dépassait pas les quelques pixels du texte lui-même. Un ::before
   invisible, positionné en absolu (donc hors du flux, sans toucher au
   layout flex existant), étend juste la zone cliquable autour du texte.
   Ciblé sur (pointer: coarse) uniquement : le rendu desktop (souris/trackpad,
   déjà précis) reste identique au pixel près. */
@media (pointer: coarse) {
  .text-button, .filters button, .view-switcher button { position: relative; }
  .text-button::before, .filters button::before, .view-switcher button::before, .modal-nav::before {
    content: "";
    position: absolute;
    top: 50%;
    left: 50%;
    width: max(100%, 44px);
    height: max(100%, 44px);
    transform: translate(-50%, -50%);
  }
}

@media (prefers-reduced-motion: reduce) {
  *, *::before, *::after { scroll-behavior: auto !important; transition-duration: .01ms !important; animation-duration: .01ms !important; animation-iteration-count: 1 !important; }
}

/* Custom cursor — plain white circle by default, larger plain circle on
   ordinary clickable hover (nav/buttons), and the official "Viewed by
   Auræ" round badge (assets/cursor-badge.svg) spinning continuously when
   hovering a project (slider list item or grid card) — same size as the
   standard hover circle, and mix-blend-mode: difference stays on so it
   inverts to a negative wherever it overlaps text/images, like the rest
   of the cursor. */
@media (hover: hover) and (pointer: fine) {
  /* Le curseur custom est désactivé sur Safari côté JS (has-custom-cursor
     n'y est jamais posée) : le pointeur système de Safari reste donc
     visible partout sur le site, comme sur la plupart des autres sites.
     Voir le commentaire dans app.js pour le pourquoi. */
  html.has-custom-cursor, html.has-custom-cursor * { cursor: none; }
  .cursor-dot {
    position: fixed !important; z-index: 200; top: 0; left: 0;
    width: 10px; height: 10px; margin: -5px 0 0 -5px;
    border-radius: 50%; background: var(--ink);
    mix-blend-mode: difference; pointer-events: none;
    display: grid; place-items: center;
    transform: translate3d(var(--x, -50px), var(--y, -50px), 0) scale(1);
    transition: transform .18s var(--ease), width .3s var(--ease), height .3s var(--ease), margin .3s var(--ease);
  }
  .cursor-dot.is-hover, .cursor-dot.is-hover-project { width: 54px; height: 54px; margin: -27px 0 0 -27px; }
  .cursor-dot.is-hover-project { background: transparent; }
  .cursor-badge { display: block; width: 100%; height: 100%; opacity: 0; transition: opacity .25s ease; }
  .cursor-dot.is-hover-project .cursor-badge { opacity: 1; animation: cursorSpin 7s linear infinite; }
  @keyframes cursorSpin { from { transform: rotate(0deg); } to { transform: rotate(360deg); } }
}
