/* ==========================================================================
   POLARIX — page principale
   Une famille de mise en page par section, jamais deux fois la même :
     héros        partage asymétrique + instrument
     méthode      trajectoire verticale
     réalisations balayage horizontal
     formules     escalier ascendant
     options      champ d'étiquettes + encart
     abonnements  diagramme d'orbites lié à une liste
     contact      clôture centrée
   ========================================================================== */

/* ==========================================================================
   Le voile
   Le ciel est magnifique derrière un titre de six mots ; il est illisible
   derrière un paragraphe. Un voile posé entre le ciel et le contenu règle
   la question sans rien retirer : il est nul au niveau du héros, où le ciel
   doit exister pleinement, il se ferme à mesure qu'on descend vers le
   texte, et il se rouvre un peu tout en bas pour que la page se termine
   dans le ciel où elle a commencé.

   Un dégradé statique, pas une animation liée au défilement : le voile
   porte la lisibilité du texte, il ne peut pas dépendre du support d'une
   fonctionnalité par le navigateur.
   ========================================================================== */

.page::before {
  content: '';
  position: absolute;
  inset: 0;
  z-index: -1;
  pointer-events: none;
  background: rgba(4, 6, 12, 0.9);
}

.page--hero::before {
  background: linear-gradient(180deg,
    rgba(4, 6, 12, 0)    0,
    rgba(4, 6, 12, 0)    58vh,
    rgba(4, 6, 12, 0.9)  120vh,
    rgba(4, 6, 12, 0.9)  84%,
    rgba(4, 6, 12, 0.5)  100%);
}

/* ==========================================================================
   Navigation
   Une seule ligne sur grand écran, 66px de haut. Fond transparent sur le
   héros, verre teinté une fois la sentinelle passée.
   ========================================================================== */

.nav {
  position: fixed;
  top: 0; left: 0; right: 0;
  z-index: var(--z-nav);
  height: var(--nav-h);
  display: flex;
  align-items: center;
  border-bottom: 1px solid transparent;
  transition: background var(--d-mid) var(--ease),
              border-color var(--d-mid) var(--ease),
              backdrop-filter var(--d-mid) var(--ease);
}

.nav.is-stuck {
  background: rgba(4, 6, 12, 0.72);
  border-bottom-color: var(--line-soft);
  backdrop-filter: blur(14px) saturate(140%);
  -webkit-backdrop-filter: blur(14px) saturate(140%);
}

/* Sans transparence disponible, on repasse à un fond plein : la barre
   reste lisible par-dessus le ciel. */
@media (prefers-reduced-transparency: reduce) {
  .nav.is-stuck {
    background: var(--void);
    backdrop-filter: none;
    -webkit-backdrop-filter: none;
  }
}

.nav__inner {
  width: 100%;
  max-width: var(--shell);
  margin-inline: auto;
  padding-inline: var(--gutter);
  display: flex;
  align-items: center;
  gap: var(--s-3);
}

.nav__brand { --px-brand-w: 132px; margin-right: auto; }
.nav__brand-mark { display: none; }

.nav__links {
  display: flex;
  align-items: center;
  gap: clamp(0.9rem, 2vw, 2rem);
  font-family: var(--font-display);
  font-variation-settings: 'wdth' 92;
  font-size: var(--t-xs);
  font-weight: 500;
  letter-spacing: 0.02em;
  text-transform: uppercase;
}

.nav__links a {
  position: relative;
  color: var(--muted);
  padding-block: 0.35rem;
  transition: color var(--d-fast) var(--ease);
}

/* Le filet d'état actif se déploie depuis la gauche. Il sert deux fois :
   au survol, et pour la section en cours de lecture. */
.nav__links a::after {
  content: '';
  position: absolute;
  left: 0; right: 0; bottom: 0;
  height: 1px;
  background: var(--polaris);
  transform: scaleX(0);
  transform-origin: left;
  transition: transform var(--d-mid) var(--ease);
}

.nav__links a:hover,
.nav__links a.is-current { color: var(--paper); }
.nav__links a:hover::after,
.nav__links a.is-current::after { transform: scaleX(1); }

.nav__cta {
  padding: 0.7rem 1.4rem;
  font-size: var(--t-micro);
  font-weight: 600;
  letter-spacing: 0.03em;
}

.nav__toggle {
  display: none;
  width: 44px; height: 44px;
  align-items: center;
  justify-content: center;
  flex-direction: column;
  gap: 6px;
  cursor: pointer;
  border-radius: var(--r-pill);
}
.nav__bar {
  display: block;
  width: 22px; height: 1px;
  background: var(--paper);
  transition: transform var(--d-mid) var(--ease),
              opacity var(--d-fast) var(--ease);
}
.nav__toggle[aria-expanded='true'] .nav__bar:first-child {
  transform: translateY(3.5px) rotate(45deg);
}
.nav__toggle[aria-expanded='true'] .nav__bar:last-child {
  transform: translateY(-3.5px) rotate(-45deg);
}

/* ==========================================================================
   Rail de déclinaison
   Fixe à gauche, à partir de 1200px. Une graduation, cinq crans, et une
   barre qui se remplit avec la progression réelle dans la page.
   La progression est native : animation-timeline: scroll(). Aucun écouteur
   de défilement, aucun calcul en JavaScript. Sans support du navigateur, la
   barre reste vide et les crans continuent de fonctionner.
   ========================================================================== */

.rail {
  display: none;
  position: fixed;
  left: clamp(14px, 2.2vw, 34px);
  top: 50%;
  z-index: var(--z-rail);
  transform: translateY(-50%);
  height: 46vh;
  opacity: 0;
  transition: opacity var(--d-slow) var(--ease);
}
.rail.is-on { opacity: 1; }

@media (min-width: 1200px) { .rail { display: block; } }

/* Un fond dégradé sous le rail. La bande des réalisations est pleine
   largeur : sans ce voile, le nom de la section en cours se poserait
   directement sur une photo. */
.rail::before {
  content: '';
  position: absolute;
  left: -34px;
  top: -8%;
  width: 210px;
  height: 116%;
  background: linear-gradient(90deg, rgba(4, 6, 12, 0.82), transparent);
  pointer-events: none;
}

.rail__scale {
  position: absolute;
  left: 5px; top: 0; bottom: 0;
  width: 1px;
  background: var(--line);
}

.rail__fill {
  position: absolute;
  inset: 0;
  background: linear-gradient(180deg, var(--polaris), rgba(242, 183, 94, 0.35));
  transform: scaleY(0);
  transform-origin: top;
}

@supports (animation-timeline: scroll()) {
  .rail__fill {
    animation: railFill linear both;
    animation-timeline: scroll(root block);
  }
}
@keyframes railFill {
  from { transform: scaleY(0); }
  to   { transform: scaleY(1); }
}

/* Sous mouvement réduit, la barre disparaît plutôt que de rester bloquée
   sur une valeur arbitraire : la règle globale de base.css écrase la durée
   de toutes les animations, ce qui n'a aucun sens pour une animation liée
   au défilement. Les crans continuent d'indiquer la section en cours. */
@media (prefers-reduced-motion: reduce) {
  .rail__fill { display: none; }
}

.rail__marks {
  position: relative;
  height: 100%;
  display: flex;
  flex-direction: column;
  justify-content: space-between;
}

.rail__marks a {
  display: flex;
  align-items: center;
  gap: 0.7rem;
  height: 12px;
}

/* Le cran. Il s'allonge et s'éclaire quand sa section est à l'écran. */
.rail__marks a::before {
  content: '';
  width: 11px; height: 1px;
  background: var(--dim);
  transition: width var(--d-mid) var(--ease),
              background var(--d-mid) var(--ease);
}

/* Le nom sort à l'horizontale, jamais tourné à 90 degrés. */
.rail__marks span {
  font-family: var(--font-tech);
  font-size: var(--t-micro);
  letter-spacing: 0.1em;
  text-transform: uppercase;
  color: var(--muted);
  white-space: nowrap;
  opacity: 0;
  transform: translateX(-6px);
  transition: opacity var(--d-mid) var(--ease),
              transform var(--d-mid) var(--ease);
}

.rail__marks a:hover::before,
.rail__marks a.is-current::before { width: 22px; background: var(--polaris); }
.rail__marks a:hover span { opacity: 1; transform: none; color: var(--paper); }
.rail__marks a.is-current span { opacity: 1; transform: none; color: var(--polaris); }

/* ==========================================================================
   Héros
   Partage asymétrique : la parole à gauche, l'instrument à droite.
   min-height en dvh, jamais vh : la barre d'adresse d'iOS ne doit pas
   faire sauter la mise en page.
   ========================================================================== */

.hero {
  min-height: 100dvh;
  display: flex;
  align-items: center;
  padding-top: calc(var(--nav-h) + var(--s-3));
  padding-bottom: var(--s-4);
}

.hero__inner {
  display: grid;
  grid-template-columns: minmax(0, 1.02fr) minmax(0, 1fr);
  align-items: center;
  gap: clamp(2rem, 5vw, 5rem);
}

.hero__title {
  /* Deux lignes maximum, quoi qu'il arrive. */
  margin-bottom: var(--s-3);
}

/* La ligne accentuée arrive après le titre, avec un décalage : elle se
   pose comme une seconde affirmation, pas comme la suite de la phrase. */
.hero__title b {
  display: inline-block;
  /* dégagement des jambages descendants */
  line-height: 1.06;
  padding-bottom: 0.05em;
}

.hero__lede { margin-bottom: var(--s-3); }

.hero__actions {
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  gap: var(--s-2);
}

/* Entrée : le titre, le chapô puis les actions se posent l'un après
   l'autre. Une seule passe au chargement, jamais rejouée. */
@media (prefers-reduced-motion: no-preference) {
  .js .hero__title,
  .js .hero__lede,
  .js .hero__actions,
  .js .astro {
    opacity: 0;
    animation: heroIn var(--d-slow) var(--ease) forwards;
  }
  .js .hero__title   { animation-delay: 120ms; }
  .js .hero__lede    { animation-delay: 260ms; }
  .js .hero__actions { animation-delay: 380ms; }
  .js .astro         { animation-delay: 200ms; animation-duration: 1.4s; }
}

@keyframes heroIn {
  from { opacity: 0; transform: translateY(20px); }
  to   { opacity: 1; transform: none; }
}

/* --- L'astrolabe -------------------------------------------------------- */

.astro {
  position: relative;
  width: 100%;
  /* L'inclinaison suit le pointeur. Les deux variables sont écrites par
     assets/js/studio.js dans une boucle requestAnimationFrame, jamais dans
     un état de composant : aucun recalcul de mise en page. */
  transform: perspective(1400px)
             rotateX(calc(var(--ax, 0) * 1deg))
             rotateY(calc(var(--ay, 0) * 1deg));
  transform-style: preserve-3d;
  will-change: transform;
}

/* Un voile sombre sous l'instrument. Sans lui, le cadran se noie dans le
   champ d'étoiles : il faut que le ciel s'efface là où l'objet se pose,
   exactement comme le verre fumé d'un viseur. */
.astro::before {
  content: '';
  position: absolute;
  inset: -8%;
  border-radius: 50%;
  background: radial-gradient(circle at 50% 50%,
              rgba(4, 6, 12, 0.72) 0%,
              rgba(4, 6, 12, 0.55) 46%,
              transparent 72%);
  pointer-events: none;
}

.astro__svg { position: relative; width: 100%; height: auto; overflow: visible; }

.astro__ring {
  fill: none;
  stroke: rgba(169, 196, 239, 0.3);
  stroke-width: 1;
}
.astro__ring--outer { stroke-dasharray: 2 9; }
.astro__ring--mid   { stroke: rgba(169, 196, 239, 0.14); }

/* Le disque intérieur porte une teinte : l'étoile du pôle a besoin d'un
   corps sur lequel se poser, sinon elle flotte dans le champ d'étoiles. */
.astro__ring--inner {
  fill: rgba(11, 18, 36, 0.55);
  stroke: rgba(169, 196, 239, 0.24);
}

.astro__crosshair path {
  stroke: rgba(169, 196, 239, 0.16);
  stroke-width: 1;
  fill: none;
}

/* La couronne graduée tourne. Les relèvements, eux, ne bougent pas : c'est
   la seule chose que cet objet a besoin de dire. */
.astro__ticks line {
  stroke: var(--ice);
  stroke-width: 1;
  opacity: 0.45;
}
.astro__ticks line.is-major { opacity: 0.85; }

@media (prefers-reduced-motion: no-preference) {
  .astro__ticks {
    transform-origin: 380px 320px;
    animation: spin 220s linear infinite;
  }
  .astro__orbiter {
    transform-origin: 380px 320px;
    animation: spin 34s linear infinite;
  }
}
@keyframes spin { to { transform: rotate(360deg); } }

.astro__orbiter-star { fill: var(--paper); }

.astro__hit { fill: transparent; }

.astro__star {
  fill: var(--ice);
  transition: fill var(--d-mid) var(--ease),
              transform var(--d-mid) var(--ease-snap);
  transform-box: fill-box;
  transform-origin: center;
}

.astro__label {
  font-family: var(--font-tech);
  font-size: 19px;
  letter-spacing: 0.1em;
  text-transform: uppercase;
  fill: var(--muted);
  transition: fill var(--d-mid) var(--ease);
}

.astro__mark { cursor: pointer; }
.astro__mark:hover .astro__star,
.astro__mark:focus-visible .astro__star { fill: var(--polaris); transform: scale(1.5); }
.astro__mark:hover .astro__label,
.astro__mark:focus-visible .astro__label { fill: var(--paper); }
.astro__mark:focus-visible { outline: none; }
.astro__mark:focus-visible .astro__hit {
  fill: rgba(242, 183, 94, 0.14);
  stroke: var(--polaris);
  stroke-width: 1;
}

/* La rose porte ses propres facettes (jetons --rose-*), elle n'hérite donc
   pas d'une couleur d'encre comme les symboles à une couleur. */
.astro__pole { pointer-events: none; }

/* Sous 900px les repères deviennent des cibles de moins de 44px : ils
   disparaissent — display:none les retire aussi de l'arbre d'accessibilité,
   donc la navigation n'est pas dupliquée pour rien — et le menu prend le
   relais. Le cadran reste, comme objet. */
@media (max-width: 900px) {
  .astro__marks { display: none; }
}

/* ==========================================================================
   Méthode
   ========================================================================== */

.method { padding-block: var(--s-6); }

.method__head {
  display: grid;
  gap: var(--s-3);
  margin-bottom: var(--s-5);
  max-width: 74ch;
}
.method__intro { display: grid; gap: var(--s-2); }

/* --- La trajectoire -------------------------------------------------------
   Un méridien vertical, quatre stations qui l'alternent de part et d'autre.
   Le filet se trace au fil du défilement et chaque station s'allume quand
   on l'atteint : le parcours se lit comme un relevé, pas comme une liste
   d'étapes. La géométrie est celle d'une grille, donc les points tombent
   toujours exactement sur la ligne, quelle que soit la longueur des textes.
   -------------------------------------------------------------------------- */

.course {
  --course-gap: clamp(3rem, 7vw, 6rem);
  position: relative;
}

.course__line {
  position: absolute;
  left: 50%;
  top: 0;
  width: 2px;
  height: 100%;
  transform: translateX(-50%);
  overflow: visible;
}

.course__path {
  stroke: rgba(242, 183, 94, 0.5);
  stroke-width: 1.5;
  stroke-linecap: round;
  /* Sans support des animations liées au défilement, le trait est
     simplement tracé : aucune information n'est perdue. */
  stroke-dasharray: none;
}

@supports (animation-timeline: view()) {
  @media (prefers-reduced-motion: no-preference) {
    .course { view-timeline-name: --course; view-timeline-axis: block; }
    /* La longueur est normalisée à 1 par l'attribut pathLength du tracé.
       La plage « cover » couvre toute la traversée de la fenêtre par la
       section : le trait finit de se tracer au moment où l'on finit de
       lire la dernière station, pas après l'avoir dépassée. */
    .course__path {
      stroke-dasharray: 1;
      stroke-dashoffset: 1;
      animation: draw linear both;
      animation-timeline: --course;
      animation-range: cover 12% cover 76%;
    }
  }
}
@keyframes draw { to { stroke-dashoffset: 0; } }

.course__steps {
  display: grid;
  grid-template-columns: 1fr 1fr;
  column-gap: var(--course-gap);
  row-gap: clamp(2.5rem, 6vw, 5rem);
}

.step { position: relative; }

/* Chaque station occupe sa propre rangée, en alternance : la lecture reste
   strictement linéaire alors que l'oeil zigzague. */
.step:nth-child(1) { grid-area: 1 / 1; text-align: right; }
.step:nth-child(2) { grid-area: 2 / 2; }
.step:nth-child(3) { grid-area: 3 / 1; text-align: right; }
.step:nth-child(4) { grid-area: 4 / 2; }

/* Le filet de rappel qui relie la station au méridien. */
.step::after {
  content: '';
  position: absolute;
  top: 1.15em;
  width: calc(var(--course-gap) / 2 - 9px);
  height: 1px;
  background: var(--line);
}
.step:nth-child(odd)::after  { left: 100%; }
.step:nth-child(even)::after { right: 100%; }

.step__dot {
  position: absolute;
  top: calc(1.15em - 4px);
  width: 9px;
  height: 9px;
  background: var(--void);
  border: 1px solid var(--polaris);
  border-radius: var(--r-pill);
}
.step:nth-child(odd) .step__dot  { left: calc(100% + var(--course-gap) / 2 - 4.5px); }
.step:nth-child(even) .step__dot { right: calc(100% + var(--course-gap) / 2 - 4.5px); }

/* Le point s'allume quand sa station est atteinte : le trajet se ponctue. */
.step__dot::after {
  content: '';
  position: absolute;
  inset: 1px;
  border-radius: var(--r-pill);
  background: var(--polaris);
  transform: scale(0);
  transition: transform var(--d-mid) var(--ease-snap) 240ms;
}
.js .step.is-in .step__dot::after { transform: scale(1); }

.step__n {
  display: block;
  margin-bottom: 0.35rem;
  color: var(--polaris);
}

.step h3 {
  font-family: var(--font-display);
  font-variation-settings: 'wdth' 108;
  font-size: var(--t-lg);
  font-weight: 500;
  letter-spacing: -0.02em;
  margin-bottom: 0.5rem;
}

.step p {
  color: var(--muted);
  font-size: var(--t-sm);
  line-height: 1.7;
  max-width: 42ch;
}
.step:nth-child(odd) p { margin-left: auto; }

.step__when {
  font-family: var(--font-tech);
  font-size: var(--t-micro);
  letter-spacing: 0.1em;
  text-transform: uppercase;
  color: var(--ice);
  margin-bottom: 0.4rem;
}

/* Sous 760px l'alternance n'a plus la place d'exister : le méridien passe à
   gauche et les quatre stations s'alignent dessus, dans le même ordre. */
@media (max-width: 760px) {
  .course { --course-gap: 3rem; padding-left: 1.75rem; }
  .course__line { left: 0; transform: none; }
  .course__steps { grid-template-columns: 1fr; }
  .step:nth-child(1),
  .step:nth-child(2),
  .step:nth-child(3),
  .step:nth-child(4) { grid-area: auto; text-align: left; }
  .step:nth-child(odd) p { margin-left: 0; }
  .step::after,
  .step:nth-child(odd)::after,
  .step:nth-child(even)::after {
    left: auto;
    right: 100%;
    width: 1.75rem;
  }
  .step:nth-child(odd) .step__dot,
  .step:nth-child(even) .step__dot {
    left: auto;
    right: calc(100% + 1.75rem - 4.5px);
  }
}

/* ==========================================================================
   Réalisations — balayage horizontal
   Par défaut : un carrousel horizontal avec accroche, qui fonctionne
   partout, y compris sans JavaScript et sans animations liées au
   défilement. L'épinglage n'est ajouté que si le navigateur sait le faire.
   ========================================================================== */

.work { padding-block: var(--s-6); }

.work__track {
  display: flex;
  gap: clamp(1.5rem, 3vw, 3rem);
  align-items: center;
  padding-inline: var(--gutter);
  overflow-x: auto;
  scroll-snap-type: x mandatory;
  scrollbar-width: thin;
  scrollbar-color: var(--line) transparent;
}
.work__track > * { scroll-snap-align: center; }

.work__lead {
  flex: none;
  width: min(38ch, 74vw);
  display: grid;
  gap: var(--s-2);
  align-content: center;
}

@supports (animation-timeline: view()) {
  @media (min-width: 900px) and (prefers-reduced-motion: no-preference) {
    /* La hauteur du bloc épinglé décide de la longueur du travelling.
       Quatre projets plus l'intitulé : 340vh de course. */
    .work__pin {
      height: 340vh;
      view-timeline-name: --work;
      view-timeline-axis: block;
    }
    .work__stage {
      position: sticky;
      top: 0;
      height: 100dvh;
      display: flex;
      align-items: center;
      overflow: hidden;
    }
    .work__track {
      overflow: visible;
      scroll-snap-type: none;
      width: max-content;
      animation: pan linear both;
      animation-timeline: --work;
      /* « contain » sur un sujet plus haut que la fenêtre couvre très
         exactement la durée de l'épinglage : le travelling commence quand
         la section se cale en haut, et finit quand elle se décroche. */
      animation-range: contain 0% contain 100%;
      will-change: transform;
    }
  }
}

/* Deux translations distinctes plutôt qu'un seul calc() mêlant pourcentage
   et pixels : un calc() mixte dans une transformation n'est pas
   interpolable, le navigateur bascule alors en interpolation discrète et
   la bande saute d'un bloc à mi-course au lieu de glisser.

   Le -100% est la largeur de la bande, le +100vw celle de la fenêtre :
   à l'arrivée, le bord droit de la bande affleure le bord droit de
   l'écran. Le retrait latéral est porté par le padding de la bande, il
   n'y a donc rien à soustraire ici, et pas de vide en fin de course. */
@keyframes pan {
  from { transform: translate3d(0, 0, 0)     translate3d(0, 0, 0); }
  to   { transform: translate3d(-100%, 0, 0) translate3d(100vw, 0, 0); }
}

.piece {
  flex: none;
  width: min(46vw, 620px);
  display: grid;
  gap: var(--s-2);
}
.piece--tall   { width: min(30vw, 420px); }
.piece--square { width: min(34vw, 480px); }

.piece__media {
  position: relative;
  isolation: isolate;
  overflow: hidden;
  border-radius: var(--r-box);
  aspect-ratio: 4 / 3;
  /* Lit bleu profond : n'importe quelle source photo retombe dans la
     palette au lieu de lutter contre elle. */
  background: linear-gradient(160deg, #16264d 0%, #0a1020 55%, #04060c 100%);
}
.piece--tall .piece__media   { aspect-ratio: 3 / 4; }
.piece--square .piece__media { aspect-ratio: 1 / 1; }

.piece__media img {
  width: 100%; height: 100%;
  object-fit: cover;
  filter: grayscale(1) contrast(1.14) brightness(0.9);
  mix-blend-mode: luminosity;
  opacity: 0.6;
  transform: scale(1.02);
  transition: opacity var(--d-slow) var(--ease),
              filter var(--d-slow) var(--ease),
              transform 1.2s var(--ease);
}

/* Au survol, la mise au point se fait : l'image reprend de la densité et
   avance légèrement. C'est le seul retour d'état dont une vignette de
   projet a besoin. */
.piece:hover .piece__media img {
  opacity: 0.86;
  filter: grayscale(0.55) contrast(1.06) brightness(1);
  transform: scale(1.06);
}

/* Filet chaud sur le bord haut, tracé au survol. */
.piece__media::after {
  content: '';
  position: absolute;
  inset: 0;
  border-top: 1px solid var(--polaris);
  transform: scaleX(0);
  transform-origin: left;
  transition: transform var(--d-slow) var(--ease);
  pointer-events: none;
}
.piece:hover .piece__media::after { transform: scaleX(1); }

.piece__caption h3 {
  font-family: var(--font-display);
  font-variation-settings: 'wdth' 108;
  font-size: var(--t-lg);
  font-weight: 500;
  letter-spacing: -0.02em;
}
.piece__caption p {
  color: var(--muted);
  font-size: var(--t-sm);
}

@media (max-width: 900px) {
  .piece,
  .piece--tall,
  .piece--square { width: min(78vw, 420px); }
}

/* ==========================================================================
   Formules — l'escalier
   Trois paliers qui montent, de tailles différentes, sans boîtes : un filet
   en tête suffit à les séparer. Le palier le plus complet porte le filet
   chaud, c'est le seul repère de hiérarchie dont on a besoin.
   ========================================================================== */

.offers { padding-block: var(--s-6); }

.offers__head {
  display: grid;
  gap: var(--s-2);
  max-width: 62ch;
  margin-bottom: var(--s-5);
}

.plans {
  position: relative;
  display: grid;
  grid-template-columns: repeat(3, minmax(0, 1fr));
  gap: clamp(1.5rem, 3vw, 3.5rem);
  align-items: start;
}

/* Pas de diagonale décorative ici : les trois filets de tête sont déjà
   décalés en hauteur par les marges négatives ci-dessous, et c'est eux qui
   dessinent la montée. Un trait de plus ne dirait rien qu'ils ne disent
   déjà, et il traverserait les orbites. */

.plan {
  position: relative;
  padding-top: var(--s-2);
  border-top: 1px solid var(--line);
  transition: border-color var(--d-mid) var(--ease);
}
.plan--mid { margin-top: clamp(-4rem, -4vw, -2rem); }
.plan--top {
  margin-top: clamp(-8rem, -8vw, -4rem);
  border-top-color: var(--line-warm);
}
.plan:hover { border-top-color: var(--polaris); }

.plan__sky { margin-bottom: var(--s-2); }
.plan__sky svg { width: 100%; height: auto; }

.plan__orbit,
.plan__ring {
  fill: none;
  stroke: var(--line);
  stroke-width: 1;
}
.plan__ring { stroke: var(--line-warm); }

/* --- Les globes ----------------------------------------------------------
   L'astre n'est plus un disque plat : c'est une photo de la Terre, masquée
   en cercle et ramenée dans la palette exactement comme les images du
   portfolio (photo en niveaux de gris, fusionnée en luminosité sur un lit
   bleu profond). La photo ne fournit que la matière ; le volume vient du
   dégradé de modelé, et la présence du liseré et de l'atmosphère.

   La progression des trois formules se lit dans la texture autant que dans
   l'appareillage : terre nue, puis terre reliée, puis terre reliée avec
   anneau et deux satellites.
   -------------------------------------------------------------------------- */

.globe { isolation: isolate; }

.globe__bed   { fill: url(#globeBed); }
.globe__shade { fill: url(#globeShade); }

.globe__map {
  filter: grayscale(1) contrast(1.16) brightness(0.92);
  mix-blend-mode: luminosity;
  opacity: 0.92;
}

/* La terre nue n'a pas de points lumineux pour la porter : océans et
   continents tombent tous les deux dans les gris moyens et le disque
   s'aplatit. On écarte les valeurs pour lui rendre du relief. */
.globe__map--plain {
  filter: grayscale(1) contrast(1.5) brightness(0.82);
  opacity: 0.95;
}

/* Le limbe. Un liseré fin suffit à fermer la sphère ; sans lui le disque
   se dissout dans le fond. */
.globe__rim {
  fill: none;
  stroke: rgba(169, 196, 239, 0.5);
  stroke-width: 1;
}
.plan--top .globe__rim { stroke: rgba(242, 183, 94, 0.62); }

.globe__air { fill: url(#globeAir); }
.plan--top .globe__air { fill: url(#globeAirWarm); }

/* Dérive du globe. Un aller-retour lent en fondu d'accélération : la
   planète a l'air de tourner sur elle-même, et comme le mouvement
   s'inverse au lieu de boucler, il n'y a jamais de saut. La photo est
   tracée une fois et demie plus large que son masque, il reste donc
   toujours de la matière sous le cercle. */
@media (prefers-reduced-motion: no-preference) {
  .globe__map {
    animation: globeDrift 38s var(--ease-io) infinite alternate;
    will-change: transform;
  }
}
@keyframes globeDrift {
  from { transform: translate3d(calc(var(--drift, 5px) * -1), 0, 0); }
  to   { transform: translate3d(var(--drift, 5px), 0, 0); }
}

/* Le halo respire, très lentement. Un astre n'est pas un objet mort, mais
   la page ne doit pas clignoter : 7 secondes par cycle. */
.plan__halo {
  fill: none;
  stroke: var(--ice);
  stroke-width: 1;
  opacity: 0.18;
}
.plan--top .plan__halo { stroke: var(--polaris); }

@media (prefers-reduced-motion: no-preference) {
  .plan__halo { animation: breathe 7s var(--ease-io) infinite; transform-box: fill-box; transform-origin: center; }
}
@keyframes breathe {
  0%, 100% { transform: scale(1);    opacity: 0.22; }
  50%      { transform: scale(1.28); opacity: 0; }
}

/* --- Satellites ----------------------------------------------------------
   Ils suivent leur ellipse, tracé par tracé.

   Une rotation ne convenait pas : rotate() fait décrire un cercle, alors
   que l'orbite dessinée est une ellipse. Le satellite s'en échappait donc
   par le haut et par le bas, et se faisait couper net par le viewBox, qui
   est plus bas que le rayon de rotation. Ici la trajectoire EST l'ellipse
   dessinée, aux mêmes rayons : le satellite ne peut plus ni la quitter ni
   déborder du cadre.

   Le chemin porte aussi la position au repos : à offset-distance 0%, le
   satellite est posé sur le bord droit de son orbite. Sous mouvement
   réduit, il s'y arrête simplement.
   -------------------------------------------------------------------------- */

.plan__moon {
  fill: var(--ice);
  offset-rotate: 0deg;
  /* Le rayon est piloté en CSS plutôt que par l'attribut : c'est ce qui
     permet de le faire varier au long de l'orbite. On passe par `r` et non
     par un scale() parce qu'un scale() sur un élément SVG dépend de
     transform-box, qui entrerait en conflit avec la boîte de référence
     d'offset-path. `r` grandit autour du centre du cercle, par définition. */
  r: var(--moon-r);
}
.plan--top .plan__moon { fill: var(--polaris); }

/* Mêmes rayons que les ellipses .plan__orbit correspondantes. */
.plan__moon--b {
  --moon-r: 4.5px;
  offset-path: path("M 176,92 a 76,24 0 1,1 -152,0 a 76,24 0 1,1 152,0");
}
.plan__moon--c1,
.plan__moon--c2 {
  offset-path: path("M 186,92 a 86,28 0 1,1 -172,0 a 86,28 0 1,1 172,0");
}
.plan__moon--c1 { --moon-r: 5px; }
/* Déphasage au repos, pour que les deux ne se superposent pas. */
.plan__moon--c2 { --moon-r: 3.2px; offset-distance: 38%; }

@media (prefers-reduced-motion: no-preference) {
  /* Deux animations sur la même horloge : le déplacement, à vitesse
     constante, et la profondeur. */
  .plan__moon {
    animation: orbit 22s linear infinite,
               moonDepth 22s ease-in-out infinite;
  }
  .plan__moon--c1 { animation-duration: 26s, 26s; }
  /* Même durée que c1, décalée de 38 % de la période : les deux corps
     partagent l'orbite sans jamais se rattraper. */
  .plan__moon--c2 {
    animation-duration: 26s, 26s;
    animation-delay: -9.88s, -9.88s;
  }
}

@keyframes orbit {
  from { offset-distance: 0%; }
  to   { offset-distance: 100%; }
}

/* Profondeur. Le tracé part du bord droit et tourne dans le sens des
   aiguilles : à 25 % le satellite est au point bas, donc devant le globe,
   et il grossit ; à 75 % il est au point haut, donc derrière, et il
   s'amenuise. Faute d'occlusion réelle, c'est ce qui donne le relief. */
@keyframes moonDepth {
  0%, 50%, 100% { r: var(--moon-r);              opacity: 0.85; }
  25%           { r: calc(var(--moon-r) * 1.28); opacity: 1; }
  75%           { r: calc(var(--moon-r) * 0.66); opacity: 0.42; }
}

/* Sans offset-path, le satellite n'aurait aucune position : il se poserait
   dans le coin du cadre. Mieux vaut ne pas le dessiner du tout, l'orbite
   reste lisible sans lui. */
@supports not (offset-path: path("M 0,0 L 1,1")) {
  .plan__moon { display: none; }
}

.plan__rank { display: block; margin-bottom: 0.4rem; }

.plan__name {
  font-family: var(--font-display);
  font-variation-settings: 'wdth' 112;
  font-size: var(--t-xl);
  font-weight: 500;
  letter-spacing: -0.03em;
  line-height: 1;
}
.plan--mid .plan__name { font-size: calc(var(--t-xl) * 1.08); }
.plan--top .plan__name { font-size: calc(var(--t-xl) * 1.16); color: var(--paper); }

.plan__claim {
  color: var(--ice);
  font-size: var(--t-sm);
  margin-top: 0.5rem;
}

.plan__price {
  font-family: var(--font-tech);
  font-size: var(--t-md);
  color: var(--paper);
  margin-block: var(--s-2);
  padding-block: 0.6rem;
  border-block: 1px solid var(--line-soft);
}
.plan--top .plan__price { color: var(--polaris); }

.plan__list {
  display: grid;
  gap: 0.8rem;
  color: var(--muted);
  font-size: var(--t-sm);
  line-height: 1.65;
}
.plan__list li { padding-left: 1.1rem; position: relative; }
.plan__list li::before {
  content: '';
  position: absolute;
  left: 0; top: 0.72em;
  width: 4px; height: 4px;
  background: var(--ice);
  opacity: 0.55;
  transform: rotate(45deg);
}
.plan--top .plan__list li::before { background: var(--polaris); opacity: 0.7; }

.plan__foot {
  margin-top: var(--s-2);
  padding-top: 0.9rem;
  border-top: 1px solid var(--line-soft);
  font-family: var(--font-tech);
  font-size: var(--t-micro);
  letter-spacing: 0.06em;
  color: var(--dim);
}

@media (max-width: 880px) {
  .plans { grid-template-columns: 1fr; gap: var(--s-4); }
  .plans::before { display: none; }
  .plan--mid,
  .plan--top { margin-top: 0; }
  .plan__sky { max-width: 220px; }
  .plan--mid .plan__name,
  .plan--top .plan__name { font-size: var(--t-xl); }
}

/* ==========================================================================
   Options et audit
   ========================================================================== */

.extras { padding-block: var(--s-6); }

.extras__grid {
  display: grid;
  grid-template-columns: minmax(0, 1.35fr) minmax(0, 1fr);
  gap: clamp(2rem, 5vw, 5rem);
  align-items: start;
}

.extras__field h2 { margin-bottom: var(--s-2); }

/* Champ d'étiquettes : on le balaie, on ne le lit pas ligne à ligne. Onze
   options en liste à puces seraient onze lignes à traverser. */
.tags {
  display: flex;
  flex-wrap: wrap;
  gap: 0.6rem;
  margin-block: var(--s-3);
}

.tags li {
  padding: 0.55rem 1.1rem;
  border: 1px solid var(--line);
  border-radius: var(--r-pill);
  font-size: var(--t-xs);
  color: var(--muted);
  transition: border-color var(--d-fast) var(--ease),
              color var(--d-fast) var(--ease),
              transform var(--d-fast) var(--ease);
}
.tags li:hover {
  border-color: var(--line-warm);
  color: var(--paper);
  transform: translateY(-2px);
}

.extras__cta { color: var(--muted); font-size: var(--t-sm); max-width: 46ch; }

.audit {
  padding: clamp(1.75rem, 3vw, 2.75rem);
  border: 1px solid var(--line);
  border-top-color: var(--line-warm);
  border-radius: var(--r-box);
  background: linear-gradient(180deg, rgba(16, 26, 51, 0.55), rgba(7, 11, 22, 0.35));
  display: grid;
  gap: var(--s-2);
}

.audit__price { margin-bottom: 0.2rem; }
.audit__lead {
  font-family: var(--font-tech);
  font-size: var(--t-xs);
  letter-spacing: 0.14em;
  text-transform: uppercase;
  color: var(--ice);
}

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

/* ==========================================================================
   Abonnements — orbites emboîtées
   Le contenu est cumulatif : chaque palier reprend le précédent. Le dessin
   l'est aussi. L'anneau du palier atteint s'allume, et ceux du dessous
   restent allumés avec lui.
   ========================================================================== */

.subs { padding-block: var(--s-6); }

.subs__head {
  display: grid;
  gap: var(--s-2);
  max-width: 62ch;
  margin-bottom: var(--s-5);
}

.subs__grid {
  display: grid;
  grid-template-columns: minmax(0, 0.8fr) minmax(0, 1.2fr);
  gap: clamp(2rem, 6vw, 6rem);
  align-items: start;
}

.subs__diagram {
  position: sticky;
  top: calc(var(--nav-h) + 12vh);
}
.subs__diagram svg { width: 100%; max-width: 380px; margin-inline: auto; }

.subs__orbit {
  fill: none;
  stroke: var(--line);
  stroke-width: 1;
  transition: stroke var(--d-slow) var(--ease),
              stroke-width var(--d-slow) var(--ease);
}
.subs__orbit.is-lit { stroke: var(--polaris); stroke-width: 1.4; }

.subs__runner { transform-origin: 160px 160px; opacity: 0; transition: opacity var(--d-slow) var(--ease); }
.subs__runner.is-lit { opacity: 1; }
.subs__runner circle { fill: var(--polaris); }

@media (prefers-reduced-motion: no-preference) {
  .subs__runner[data-runner='0'] { animation: spin 16s linear infinite; }
  .subs__runner[data-runner='1'] { animation: spin 26s linear infinite; }
  .subs__runner[data-runner='2'] { animation: spin 38s linear infinite; }
}

.subs__core { color: var(--paper); }

.subs__list { display: grid; gap: var(--s-4); }

.sub {
  position: relative;
  padding-left: var(--s-3);
  border-left: 1px solid var(--line);
  transition: border-color var(--d-mid) var(--ease);
}
.sub.is-active { border-left-color: var(--polaris); }

.sub__rank { display: block; margin-bottom: 0.35rem; }

.sub h3 {
  font-family: var(--font-display);
  font-variation-settings: 'wdth' 108;
  font-size: var(--t-lg);
  font-weight: 500;
  letter-spacing: -0.02em;
}

.sub__price {
  font-family: var(--font-tech);
  font-size: var(--t-lg);
  color: var(--polaris);
  margin-block: 0.5rem var(--s-2);
}
.sub__price i {
  font-style: normal;
  font-size: var(--t-xs);
  color: var(--dim);
  letter-spacing: 0.06em;
}

.sub__from {
  font-family: var(--font-tech);
  font-size: var(--t-micro);
  letter-spacing: 0.1em;
  text-transform: uppercase;
  color: var(--ice);
  margin-bottom: 0.6rem;
}

.sub ul { display: grid; gap: 0.7rem; color: var(--muted); font-size: var(--t-sm); line-height: 1.65; }
.sub ul li { padding-left: 1.1rem; position: relative; }
.sub ul li::before {
  content: '';
  position: absolute;
  left: 0; top: 0.72em;
  width: 4px; height: 4px;
  background: var(--ice);
  opacity: 0.55;
  transform: rotate(45deg);
}

@media (max-width: 880px) {
  .subs__grid { grid-template-columns: 1fr; }
  .subs__diagram { position: static; max-width: 260px; margin-inline: auto; }
}

/* ==========================================================================
   Contact — la clôture
   Centré, sans concurrence : c'est le seul endroit de la page où le message
   est lui-même la mise en page.
   ========================================================================== */

.contact {
  padding-block: var(--s-6);
  text-align: center;
}

.contact__inner {
  display: grid;
  justify-items: center;
  gap: var(--s-3);
}

.contact__title b { display: inline-block; line-height: 1.06; padding-bottom: 0.05em; }
.contact__lede { max-width: 44ch; color: var(--muted); }

.contact__actions {
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  justify-content: center;
  gap: var(--s-3);
  margin-top: var(--s-2);
}

.contact__tel { font-family: var(--font-tech); font-size: var(--t-sm); color: var(--muted); }

/* ==========================================================================
   Pied de page
   ========================================================================== */

.site-footer {
  border-top: 1px solid var(--line-soft);
  padding-block: var(--s-4) var(--s-3);
  background: rgba(4, 6, 12, 0.6);
}

.foot__grid {
  display: grid;
  grid-template-columns: minmax(0, 1.4fr) repeat(3, minmax(0, 1fr));
  gap: var(--s-3);
  padding-bottom: var(--s-4);
}

.foot__brand { --px-brand-w: 168px; display: grid; gap: var(--s-2); align-content: start; }
.foot__baseline { color: var(--muted); font-size: var(--t-sm); max-width: 26ch; }

.foot__col h4 { margin-bottom: 0.9rem; }
.foot__col p,
.foot__col li { color: var(--muted); font-size: var(--t-sm); line-height: 1.8; }

.foot__bottom {
  display: flex;
  flex-wrap: wrap;
  gap: var(--s-3);
  padding-top: var(--s-2);
  border-top: 1px solid var(--line-soft);
  font-family: var(--font-tech);
  font-size: var(--t-micro);
  letter-spacing: 0.08em;
  text-transform: uppercase;
  color: var(--dim);
}
.foot__bottom a { transition: color var(--d-fast) var(--ease); }
.foot__bottom a:hover { color: var(--polaris); }
.foot__bottom a:last-child { margin-left: auto; }

@media (max-width: 880px) {
  .foot__grid { grid-template-columns: repeat(2, minmax(0, 1fr)); gap: var(--s-3); }
  .foot__brand { grid-column: 1 / -1; }
  .foot__bottom a:last-child { margin-left: 0; }
}

/* ==========================================================================
   Adaptations
   Toute mise en page à variance élevée retombe en colonne unique sous 768px.
   ========================================================================== */

@media (max-width: 1024px) {
  .hero__inner {
    grid-template-columns: 1fr;
    justify-items: start;
    gap: var(--s-4);
  }
  .astro { max-width: 520px; margin-inline: auto; }
}

@media (max-width: 860px) {
  .nav__links {
    position: fixed;
    inset: var(--nav-h) 0 auto 0;
    flex-direction: column;
    align-items: flex-start;
    gap: 0;
    padding: var(--s-2) var(--gutter) var(--s-3);
    /* Le flou doit être franc : à 24px, ce qui reste visible derrière le
       panneau se lit comme une texture, pas comme du texte fantôme. */
    background: rgba(4, 6, 12, 0.93);
    border-bottom: 1px solid var(--line-soft);
    backdrop-filter: blur(24px) saturate(130%);
    -webkit-backdrop-filter: blur(24px) saturate(130%);
    font-size: var(--t-md);
    transform: translateY(-8px);
    opacity: 0;
    visibility: hidden;
    transition: opacity var(--d-mid) var(--ease),
                transform var(--d-mid) var(--ease),
                visibility 0s linear var(--d-mid);
  }
  .nav__links.is-open {
    opacity: 1;
    visibility: visible;
    transform: none;
    transition-delay: 0s;
  }
  .nav__links a { width: 100%; padding-block: 0.85rem; border-bottom: 1px solid var(--line-soft); }
  .nav__links a::after { display: none; }

  /* Sans transparence disponible, le panneau devient plein : on ne peut
     pas laisser du texte transparaître derrière un menu. */
  @media (prefers-reduced-transparency: reduce) {
    .nav__links {
      background: var(--void);
      backdrop-filter: none;
      -webkit-backdrop-filter: none;
    }
  }
  .nav__toggle { display: flex; }
  .nav__cta { padding: 0.6rem 1.1rem; font-size: var(--t-micro); }
}

@media (max-width: 560px) {
  .nav__brand .px-logo--lockup-h { display: none; }
  /* Sélecteur à deux classes : il doit l'emporter sur `.px-brand .px-logo`
     de brand.css, qui impose la largeur du lockup. */
  .nav__brand .nav__brand-mark { display: block; width: 28px; min-width: 28px; }
  .hero__actions { width: 100%; }
  .hero__actions .btn { flex: 1 1 auto; justify-content: center; }
}
