/* ==========================================================================
   main.css — Style principal du site
   Rôle : mise en page générale et style des composants (header, nav,
   sections, cartes, footer, transitions et modules de page). Les ajustements
   mobiles sont dans responsive.css.
   ========================================================================== */

body {
  font-family: var(--font-primary);
  color: var(--color-text);
  background-color: var(--color-bg);
  transition: opacity 0.3s ease;
  padding-top: 88px;
}

/* Pas de header sur les pages projet et à propos : toute la hauteur leur
   est réservée. */
body[data-page="project"],
body[data-page="about"] {
  padding-top: 0;
}

a:focus-visible {
  outline: 2px solid var(--color-text);
  outline-offset: 2px;
}

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

  /* ::view-transition-*(root) sont des pseudo-éléments à part, pas couverts
     par *::before/*::after ci-dessus : sans cette règle dédiée, le flou de
     transition entre pages continuerait de jouer même ici. */
  ::view-transition-group(root),
  ::view-transition-old(root),
  ::view-transition-new(root) {
    animation-duration: 0.01ms !important;
  }
}

body.is-transitioning {
  opacity: 0.7;
}

/* Le site est multi-pages (pas une SPA) : chaque navigation recharge un
   document entier. document.startViewTransition (API JS) ne crossfade que
   des changements DANS un même document ; pour un vrai fondu/flou entre deux
   pages différentes, il faut cette activation CSS des "Cross-Document View
   Transitions", à poser des deux côtés de la navigation (ici : partout,
   puisque main.css est partagé par toutes les pages). */
@view-transition {
  navigation: auto;
}

::view-transition-old(root) {
  animation: viewFadeOut 0.72s cubic-bezier(0.77, 0, 0.175, 1) both;
}

::view-transition-new(root) {
  animation: viewFadeIn 0.72s cubic-bezier(0.77, 0, 0.175, 1) both;
}

@keyframes viewFadeOut {
  from {
    filter: blur(0);
    opacity: 1;
  }
  to {
    filter: blur(16px);
    opacity: 0;
  }
}

@keyframes viewFadeIn {
  from {
    filter: blur(16px);
    opacity: 0;
  }
  to {
    filter: blur(0);
    opacity: 1;
  }
}

::view-transition-group(root) {
  animation-duration: 0.72s;
  animation-timing-function: cubic-bezier(0.77, 0, 0.175, 1);
}

.container {
  max-width: var(--max-width);
  margin-inline: auto;
  padding-inline: var(--spacing-md);
}

/* Header */
.site-header {
  position: fixed;
  top: 0;
  left: 0;
  right: 0;
  z-index: 1000;
  display: grid;
  grid-template-columns: 1fr auto 1fr;
  align-items: center;
  padding: 1rem 1.5rem;
  background-color: rgba(255, 255, 255, 0.95);
  backdrop-filter: blur(8px);
}

.nav-link,
.contact-link {
  position: relative;
  display: inline-block;
  font-size: 0.8rem;
  letter-spacing: 0.16em;
  text-transform: uppercase;
  color: var(--color-text);
}

.nav-link::after,
.contact-link::after {
  content: "";
  position: absolute;
  left: 0;
  bottom: -2px;
  width: 100%;
  height: 1px;
  background-color: currentColor;
  transform: scaleX(0);
  transform-origin: left;
  transition: transform var(--transition-base);
}

.nav-link:hover::after,
.contact-link:hover::after {
  transform: scaleX(1);
}

.nav-link {
  justify-self: start;
}

.contact-link {
  justify-self: end;
}

.site-logo {
  justify-self: center;
  display: inline-flex;
  align-items: center;
}

.site-logo img {
  height: 3.5rem;
  width: auto;
  transform: translateY(0.5rem);
}

/* Galerie horizontale */
.gallery-section {
  min-height: calc(100vh - 88px);
  padding: 1.5rem 0 4rem;
  display: flex;
  align-items: center;
}

.gallery-container {
  --hover-scale: 2.5;
  display: flex;
  align-items: center;
  gap: 1rem;
  /* Le padding vertical réserve toujours assez de place pour que la photo
     survolée (agrandie de --hover-scale) ne soit jamais rognée par
     l'overflow-y: hidden ci-dessous. */
  padding: calc(3.5rem + (var(--gallery-item-height, 26vh) * (var(--hover-scale) - 1) / 2)) 1.5rem
    calc(2rem + (var(--gallery-item-height, 26vh) * (var(--hover-scale) - 1) / 2));
  overflow-x: auto;
  overflow-y: hidden;
  scrollbar-width: none;
  -ms-overflow-style: none;
  cursor: grab;
  touch-action: pan-y;
}

.gallery-container::-webkit-scrollbar {
  display: none;
}

.gallery-container.is--dragging {
  cursor: grabbing;
  scroll-behavior: auto;
}

.gallery-item {
  position: relative;
  flex: 0 0 auto;
  display: block;
  height: var(--gallery-item-height, 26vh);
  transform: translateX(0) scale(1);
  transition: transform 0.6s cubic-bezier(0.65, 0, 0.35, 1);
  will-change: transform;
}

.gallery-item img {
  display: block;
  height: 100%;
  width: auto;
  object-fit: cover;
  filter: grayscale(0);
  transition: filter 0.5s cubic-bezier(0.65, 0, 0.35, 1), transform 0.6s cubic-bezier(0.65, 0, 0.35, 1);
  -webkit-user-drag: none;
  user-select: none;
  /* Le filter (sur l'image) qui transitionne pendant que le parent est mis à
     l'échelle (sur .gallery-item) peut faire apparaître un liseré fantôme
     pendant l'animation : le navigateur rastérise le filtre sur ses propres
     limites avant que la mise à l'échelle du parent ne s'applique, avec un
     léger désalignement sous-pixel entre les deux. will-change + sa propre
     couche de composition (backface-visibility) alignent le rendu du filtre
     sur les limites réelles, à l'échelle finale.
  */
  will-change: filter, transform;
  -webkit-backface-visibility: hidden;
  backface-visibility: hidden;
}

.gallery-caption {
  position: absolute;
  left: 0;
  bottom: calc(100% + 0.6rem);
  transform-origin: bottom left;
  display: flex;
  flex-direction: column;
  gap: 0;
  opacity: 0;
  pointer-events: none;
  z-index: 3;
  transition: opacity 0.3s ease, transform 0.6s cubic-bezier(0.65, 0, 0.35, 1);
}

.caption-meta,
.caption-title {
  display: block;
  line-height: 1.05;
  font-size: 0.8rem;
  letter-spacing: 0.16em;
  text-transform: uppercase;
  color: var(--color-text);
  /* .gallery-caption est positionnée en absolute avec seulement `left` (pas
     `right`) : sa largeur disponible est alors plafonnée à celle de son
     propre .gallery-item, même si le texte a besoin de plus de place. Sur
     les photos verticales les plus étroites (ou une fenêtre moins haute, qui
     réduit d'autant leur largeur), "JULES DELALUNE" retombait sur 2 lignes,
     ce qui poussait "2026" hors de la zone visible. nowrap force une seule
     ligne, quitte à déborder légèrement de l'item plutôt que de s'empiler. */
  white-space: nowrap;
}

.caption-meta::before {
  content: attr(data-year);
}

.caption-title::before {
  content: attr(data-photographer);
}

.gallery-item:hover .gallery-caption,
.gallery-item.is--focused .gallery-caption,
.gallery-item.is--hover-active .gallery-caption {
  opacity: 1;
}

/* Chaque .gallery-item a son propre contexte d'empilement (transform), donc
   le z-index de sa légende ne rivalise qu'à l'intérieur de cet item. Sur les
   photos les plus étroites, la légende ("JULES DELALUNE") dépasse tout juste
   de la largeur de l'item et se retrouvait recouverte par l'item voisin
   (peint après, donc au-dessus). On fait passer l'item actif lui-même
   au-dessus de ses voisins pendant que sa légende est visible. */
.gallery-item:hover,
.gallery-item.is--focused,
.gallery-item.is--hover-active {
  z-index: 10;
}

.gallery-item.is--focused img {
  filter: grayscale(0);
  transform: scale(1.1);
  transition: filter 0.4s cubic-bezier(0.65, 0, 0.35, 1), transform 0.4s cubic-bezier(0.65, 0, 0.35, 1);
}

.gallery-item:hover img,
.gallery-item.is--hover-active img {
  filter: grayscale(0);
}

.nav-link--text-nav {
  position: fixed;
  right: 1.25rem;
  bottom: 1.25rem;
  z-index: 1100;
  font-size: 0.8rem;
  letter-spacing: 0.18em;
}

/* Dégradés de bord sur la home : par-dessus les photos, sous le header
   (z-index: 1000) et le lien Instagram (z-index: 1100). */
.edge-fade {
  position: fixed;
  top: 88px;
  bottom: 0;
  width: 4vw;
  min-width: 30px;
  max-width: 80px;
  z-index: 500;
  pointer-events: none;
}

.edge-fade--left {
  left: 0;
  background: linear-gradient(90deg, rgba(255, 255, 255, 0.6) 0%, rgba(255, 255, 255, 0) 100%);
}

.edge-fade--right {
  right: 0;
  background: linear-gradient(270deg, rgba(255, 255, 255, 0.6) 0%, rgba(255, 255, 255, 0) 100%);
}

.hero {
  display: flex;
  flex-direction: column;
  align-items: flex-start;
  gap: var(--spacing-md);
  padding: var(--spacing-xl) var(--spacing-lg);
}

.hero h1 {
  font-family: var(--font-heading);
  font-size: var(--font-size-xl);
}

.hero p {
  font-size: var(--font-size-md);
  color: var(--color-text-light);
  max-width: 60ch;
}

.eyebrow {
  font-size: 0.8rem;
  text-transform: uppercase;
  letter-spacing: 0.2em;
  color: var(--color-primary);
}

.btn {
  display: inline-block;
  padding: var(--spacing-sm) var(--spacing-lg);
  border-radius: var(--radius-md);
  background-color: var(--color-primary);
  color: #fff;
  transition: background-color var(--transition-base);
}

.btn:hover {
  background-color: var(--color-primary-dark);
}

/* Révélation de texte */
.js-reveal {
  opacity: 1;
  transform: translateY(0);
  transition: opacity 0.6s cubic-bezier(0.65, 0, 0.35, 1), transform 0.6s cubic-bezier(0.65, 0, 0.35, 1);
  will-change: opacity, transform;
}

.js-reveal.is--hidden {
  opacity: 0;
  transform: translateY(20px);
}

.js-reveal.is--revealed {
  opacity: 1;
  transform: translateY(0);
}

/* Révélation ligne par ligne (voir initLineReveal dans textReveal.js) :
   chaque ligne réellement mesurée est enveloppée dans un masque
   overflow:hidden, et glisse depuis le bas comme un rideau qui se lève. */
.reveal-line {
  display: block;
  overflow: hidden;
}

.reveal-line-inner {
  display: block;
  transform: translateY(100%);
  transition: transform 0.45s cubic-bezier(0.65, 0, 0.35, 1);
}

.js-reveal-lines.is--lines-revealed .reveal-line-inner {
  transform: translateY(0);
}

.intro-target {
  opacity: 1;
  transform: translateY(0);
  transition: opacity 0.6s cubic-bezier(0.65, 0, 0.35, 1), transform 0.6s cubic-bezier(0.65, 0, 0.35, 1);
}

.intro-target.is--intro-hidden {
  opacity: 0;
  transform: translateY(20px);
}

.intro-target.is--revealed {
  opacity: 1;
  transform: translateY(0);
}

.gallery-item img {
  position: relative;
  opacity: 1;
  transform: translate(0, 0);
}

/* Pendant l'intro, toutes les photos sont cachées par défaut : seul le
   sous-ensemble empilé (is--intro-hidden/is--intro-stacked ci-dessous) doit
   être visible, empilé au centre — les autres ne réapparaissent qu'au
   moment du "saut" final (is--visible), masqué par le voile flou. */
body.is-intro-active .gallery-item img {
  opacity: 0;
  filter: grayscale(0);
}

/* Phase 1 (voir initIntroAnimation) : centrée dans le viewport en
   position:fixed — un centrage purement CSS (top/left 50% + translate
   -50%,-50%), indépendant de toute mesure de position naturelle. L'ancienne
   version calculait un décalage en JS à partir de getBoundingClientRect(),
   ce qui dépendait du chargement des images/polices et du scroll initial :
   sur une connexion plus lente que l'environnement de test, certaines
   photos restaient bloquées sur le côté. Le centrage CSS pur ne peut pas se
   tromper. Invisible, aucune transition : état de départ posé avant le
   premier paint. */
body.is-intro-active .gallery-item img.is--intro-hidden {
  position: fixed;
  top: 50%;
  left: 50%;
  transform: translate(-50%, -50%);
  height: 32vh;
  width: auto;
  opacity: 0;
  transition: none;
}

/* Phase 2 : apparition en pile au centre, une image après l'autre selon
   --stack-index (ordre aléatoire), empilées visuellement via --stack-z.
   Timing repris du fichier référence (transition-delay basé sur 0.1s). */
body.is-intro-active .gallery-item img.is--intro-stacked {
  position: fixed;
  top: 50%;
  left: 50%;
  transform: translate(-50%, -50%);
  height: 32vh;
  width: auto;
  opacity: 1;
  transition: opacity 0.2s ease;
  transition-delay: calc(var(--stack-index, 0) * 0.1s);
  z-index: var(--stack-z, 1);
}

/* Aucune dispersion visible : introSequence.js "téléporte" toutes les
   photos vers leur état final pendant que le voile flou est à son opacité
   maximale — voir intro-veil ci-dessous. Spécificité volontairement plus
   élevée que les règles ci-dessus pour reprendre la main sur position et
   taille (sinon la photo resterait en position:fixed, minuscule au centre). */
body.is-intro-active .gallery-item img.is--visible {
  position: relative;
  top: auto;
  left: auto;
  height: 100%;
  width: auto;
  opacity: 1;
  transform: translate(0, 0);
}

/* Pendant l'intro : aucune interaction (survol, clic, drag) sur la galerie —
   l'empilement doit se voir tel quel. */
body.is-intro-active .gallery-container {
  pointer-events: none;
}

/* Header et lien Instagram ("IN") complètement invisibles pendant l'intro
   (pas juste leur texte, via .intro-target) — rien de l'interface finale ne
   doit apparaître tant que la pile de photos est à l'écran. */
body.is-intro-active .site-header,
body.is-intro-active .nav-link--text-nav {
  opacity: 0;
  pointer-events: none;
}

/* La règle équivalente pour .intro-pending (avant que main.js n'ajoute
   is-intro-active) est en <style> INLINE dans le <head> d'index.html, pas
   ici : ce fichier externe peut lui-même mettre du temps à charger sur un
   vrai réseau, ce qui laisserait le même flash réapparaître. */

/* .gallery-item a TOUJOURS un transform actif (même translateX(0) scale(1)
   au repos) ET will-change:transform : les deux, par spec CSS, en font un
   "containing block" pour ses descendants en position:fixed (will-change
   déclenche le même comportement que si le transform était réellement
   appliqué), qui se centreraient alors sur CET item plutôt que sur le vrai
   viewport. On neutralise les deux pendant l'intro pour que le centrage de
   la pile (ci-dessus) soit réellement relatif à l'écran entier. */
body.is-intro-active .gallery-item {
  transform: none;
  will-change: auto;
}

.gallery-item img.is--visible {
  opacity: 1;
  transform: translate(0, 0);
}

.intro-veil {
  position: fixed;
  inset: 0;
  z-index: 1000;
  background: rgba(255, 255, 255, 1);
  backdrop-filter: blur(16px);
  -webkit-backdrop-filter: blur(16px);
  opacity: 0;
  pointer-events: none;
  transition: opacity 0.7s cubic-bezier(0.77, 0, 0.175, 1), backdrop-filter 0.7s cubic-bezier(0.77, 0, 0.175, 1);
}

.intro-veil.is--visible {
  opacity: 1;
  backdrop-filter: blur(16px);
  -webkit-backdrop-filter: blur(16px);
}

/* Sections génériques */
section {
  padding: var(--spacing-xl) var(--spacing-lg);
}

section + section {
  border-top: 1px solid var(--color-border);
}

section h2 {
  font-family: var(--font-heading);
  font-size: var(--font-size-lg);
  margin-bottom: var(--spacing-lg);
}

/* Grille de cartes (projets, compétences...) */
.card-grid {
  display: grid;
  grid-template-columns: repeat(auto-fit, minmax(240px, 1fr));
  gap: var(--spacing-lg);
}

.card,
.project-card {
  padding: var(--spacing-lg);
  background-color: var(--color-bg-alt);
  border-radius: var(--radius-md);
  box-shadow: var(--shadow-sm);
  transition: box-shadow var(--transition-base), transform var(--transition-base);
  opacity: 0;
  transform: translateY(20px);
}

.card.is-visible,
.project-card.is-visible,
.hero.is-visible,
.about-intro.is-visible {
  opacity: 1;
  transform: translateY(0);
}

.card:hover,
.project-card:hover {
  box-shadow: var(--shadow-md);
  transform: translateY(-2px);
}

/* Page À propos — même gabarit que la page projet (pas de header, pleine
   hauteur, BACK au même endroit). */
.about-page {
  height: 100vh;
  /* Même repli mobile que .project-page ci-dessous : voir son commentaire. */
  height: 100dvh;
  padding: 1.5rem 1.5rem 2rem;
  display: flex;
  flex-direction: column;
  align-items: center;
  overflow: hidden;
}

.about-columns {
  flex: 1;
  min-height: 0;
  width: 100%;
  overflow: hidden;
  display: grid;
  /* Le wireframe serre logo/photo/texte les uns contre les autres, avec la
     marge excédentaire repoussée sur les bords extérieurs (grande marge
     avant le logo) plutôt qu'entre les éléments eux-mêmes. */
  grid-template-columns: minmax(180px, 1fr) minmax(200px, 0.6fr) minmax(220px, 1fr);
  grid-template-rows: minmax(0, 1fr);
  gap: 1.25rem;
  align-items: center;
}

.about-column {
  display: flex;
  align-items: center;
  min-height: 0;
  height: 100%;
}

.about-column--left {
  /* Colle le logo contre la photo ; la marge de la colonne se retrouve
     avant lui, côté bord de page (comme dans le wireframe). */
  justify-content: flex-end;
}

.about-column--center {
  justify-content: center;
}

.about-column--right {
  /* align-self positionne la colonne dans la grille ; align-items positionne
     le paragraphe À L'INTÉRIEUR de cette colonne — sans lui, le texte
     restait centré verticalement au lieu de démarrer en haut, près du sommet
     de la photo, comme sur le wireframe. */
  align-self: start;
  align-items: flex-start;
  justify-content: flex-start;
  padding-top: 4.5rem;
}

.about-logo {
  width: min(280px, 100%);
  height: auto;
}

.about-photo {
  /* Le ratio correspond exactement au cadrage fourni (463x1048) : object-fit
     ne recadre donc plus rien, le cadrage voulu (zoom précis sur le visage)
     reste intact, quelle que soit la hauteur disponible. */
  height: 100%;
  width: auto;
  max-width: 100%;
  aspect-ratio: 463 / 1048;
  object-fit: cover;
  object-position: center;
  border-radius: 0.25rem;
  display: block;
}

.about-bio {
  display: flex;
  flex-direction: column;
  gap: 1em;
}

.--text-body {
  font-size: 15px;
  line-height: 1.4;
  max-width: 300px;
  margin: 0;
  color: var(--color-text);
  text-wrap: balance;
}

.about-back {
  align-self: center;
  margin-top: 1.75rem;
}

.grain {
  position: relative;
}

.grain::before {
  content: "";
  position: absolute;
  inset: 0;
  background-image: radial-gradient(rgba(255, 255, 255, 0.08) 0.8px, transparent 0.8px);
  background-size: 6px 6px;
  mix-blend-mode: soft-light;
  pointer-events: none;
}

/* Project viewer */
.project-page {
  height: 100vh;
  /* Sur mobile, 100vh correspond à la hauteur avec la barre d'adresse du
     navigateur masquée : quand elle est visible (chargement de la page),
     le haut du contenu se retrouve poussé hors de l'écran et coupé (le
     "2026" de la légende, notamment). 100dvh suit la hauteur réellement
     visible ; on garde 100vh en repli pour les navigateurs plus anciens. */
  height: 100dvh;
  padding: 1.5rem 1.5rem 2rem;
  display: flex;
  flex-direction: column;
  align-items: center;
  overflow: hidden;
}

.project-stage {
  position: relative;
  display: flex;
  flex-direction: column;
  align-items: center;
  gap: 0.75rem;
  width: 100%;
  flex: 1;
  min-height: 0;
}

.project-caption {
  order: -1;
  display: flex;
  flex-direction: row;
  align-items: baseline;
  gap: 0.5em;
  pointer-events: none;
}

.project-caption .caption-meta::after {
  content: "|";
  margin-left: 0.5em;
}

.project-caption .caption-meta,
.project-caption .caption-title {
  display: block;
  line-height: 1.05;
  font-size: 0.8rem;
  letter-spacing: 0.16em;
  text-transform: uppercase;
  color: var(--color-text);
}

.project-focus {
  position: relative;
  width: 100%;
  flex: 1;
  display: flex;
  align-items: center;
  justify-content: center;
  min-height: 0;
}

.focus-item {
  position: absolute;
  inset: 0;
  display: flex;
  align-items: center;
  justify-content: center;
  opacity: 0;
}

.focus-item.is--active {
  opacity: 1;
}

.focus-item img {
  display: block;
  width: auto;
  max-width: 100%;
  max-height: 100%;
  object-fit: contain;
}

.project-thumbs {
  position: fixed;
  /* left/width/padding-left : le conteneur déborde de 6rem vers la gauche
     (au-delà de la position visuelle réelle, 1.5rem) pour laisser la place
     à l'animation d'entrée des miniatures (glissement depuis la gauche),
     en plus de la marge déjà réservée à droite pour le décalage de
     proximité (jusqu'à 64px). Sinon overflow-y: hidden ci-dessous force le
     navigateur à traiter overflow-x comme "auto" et coupe tout dépassement. */
  left: -4.5rem;
  padding-left: 6rem;
  top: 0;
  bottom: 0;
  width: 16rem;
  display: flex;
  flex-direction: column;
  align-items: flex-start;
  gap: 1rem;
  overflow-y: hidden;
  scrollbar-width: none;
  -ms-overflow-style: none;
  cursor: grab;
}

.project-thumbs.is--dragging {
  cursor: grabbing;
}

.project-thumbs::-webkit-scrollbar {
  display: none;
}

.thumb-item {
  display: block;
  flex-shrink: 0;
  width: 5.5rem;
  height: 13vh;
  overflow: hidden;
  will-change: transform;
  /* Caché hors-écran dès le tout premier paint, avant même que le JS ait pu
     s'exécuter (clonage, mesures...) : sans ça, les miniatures s'affichent
     brièvement à leur position finale au chargement, le temps que
     projectViewer.js les repousse pour jouer l'entrée. */
  transform: translateX(-6rem);
}

/* L'animation d'entrée (glissement depuis la gauche façon référence) est
   pilotée entièrement en JS via le style inline (voir playEntrance dans
   projectViewer.js), pas par des classes CSS — cette règle ci-dessus ne sert
   que d'état de repli avant l'exécution du JS. */

.thumb-item img {
  height: 100%;
  width: 100%;
  object-fit: cover;
  display: block;
  -webkit-user-drag: none;
  user-select: none;
}

/* Ajoutée en JS (voir projectViewer.js) une fois l'orientation réelle de
   l'image connue : les paysages sont montrés en entier (moins zoomés) au
   lieu d'être recadrés serré comme les portraits. */
.thumb-item.thumb-item--landscape {
  height: auto;
}

.thumb-item.thumb-item--landscape img {
  height: auto;
  object-fit: cover;
}

.project-back {
  align-self: center;
  margin-top: 1.75rem;
}
