@font-face {
  font-family: "Geist";
  src: url("/assets/fonts/geist-400.woff2") format("woff2");
  font-style: normal;
  font-weight: 400;
  font-display: swap;
}

@font-face {
  font-family: "Geist";
  src: url("/assets/fonts/geist-500.woff2") format("woff2");
  font-style: normal;
  font-weight: 500;
  font-display: swap;
}

@font-face {
  font-family: "Geist";
  src: url("/assets/fonts/geist-600.woff2") format("woff2");
  font-style: normal;
  font-weight: 600;
  font-display: swap;
}

@font-face {
  font-family: "Geist";
  src: url("/assets/fonts/geist-700.woff2") format("woff2");
  font-style: normal;
  font-weight: 700;
  font-display: swap;
}

:root {
  color-scheme: light;
  font-family: "Geist", system-ui, sans-serif;
  color: oklch(0.19 0.008 250);
  background: oklch(0.962 0.004 250);
  font-synthesis: none;
  text-rendering: optimizeLegibility;
  -webkit-font-smoothing: antialiased;

  --background: oklch(0.962 0.004 250);
  /* La surface ne part pas du blanc pur : sans marge vers le haut, la lumière
     qui la balaie n'a aucune amplitude et le satin ne se voit pas. */
  --surface-top: oklch(0.985 0.002 250);
  --surface-bottom: oklch(0.968 0.003 250);
  --text: oklch(0.19 0.008 250);
  --text-soft: oklch(0.37 0.009 250);
  /* Calé sur la mesure, pas sur l'œil : à 0,515 le pied de page tombait à
     4,05:1 sur le fond de la page, et le pied de carte à 4,29:1 pendant
     l'inclinaison. Les deux sont du texte courant, donc le seuil est 4,5:1. */
  --muted: oklch(0.47 0.011 250);
  --line: oklch(0.2 0.01 250 / 9%);
  --wash: oklch(0.2 0.01 250 / 4.5%);
  --focus: oklch(0.19 0.008 250);

  --ease: cubic-bezier(0.22, 1, 0.36, 1);

  /* Encres de la gravure : trois valeurs seulement, pour que le regard trie
     immédiatement le contexte (sourd), la matière lue (moyen) et ce que
     l'application signale (clair). */
  --engrave-dim: oklch(0.66 0.008 250 / 40%);
  --engrave-base: oklch(0.82 0.006 250 / 64%);
  --engrave-signal: oklch(0.99 0 0 / 96%);
}

* {
  box-sizing: border-box;
}

html {
  min-width: 320px;
  background: var(--background);
}

body {
  min-height: 100vh;
  margin: 0;
  overflow-x: hidden;
  /* Aucun fond ici : html porte la couleur, et un fond de body recouvrirait
     la couche de lumière, qui est en z-index négatif. */
}

h1,
h2,
p {
  margin-top: 0;
}

.sr-only {
  position: absolute;
  width: 1px;
  height: 1px;
  overflow: hidden;
  margin: -1px;
  padding: 0;
  border: 0;
  clip: rect(0 0 0 0);
  clip-path: inset(50%);
  white-space: nowrap;
}

svg {
  display: block;
  fill: none;
  stroke: currentColor;
  stroke-linecap: round;
  stroke-linejoin: round;
  stroke-width: 1.7;
}

/* Lumière d'ambiance : une source haute, derrière le logo, et une assise plus
   sombre en bas qui détache la silhouette du panneau. Neutre, jamais teintée. */
.strata-bg {
  position: fixed;
  z-index: -1;
  inset: 0;
  background:
    radial-gradient(
      1000px 520px at 50% -18%,
      oklch(1 0 0 / 82%),
      transparent 56%
    ),
    radial-gradient(
      1500px 900px at 50% 112%,
      oklch(0.885 0.008 250 / 62%),
      transparent 60%
    ),
    var(--background);
}

/* Grain de dithering : le fond est un dégradé très plat, donc un candidat
   au banding sur les dalles bureautiques. Le bruit casse la quantification.
   Réglage tenu : on ne doit pas voir le grain, seulement un dégradé propre. */
.strata-bg::after {
  position: absolute;
  inset: 0;
  background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='128' height='128'%3E%3Cfilter id='n'%3E%3CfeTurbulence type='fractalNoise' baseFrequency='0.8' numOctaves='1' stitchTiles='stitch'/%3E%3CfeColorMatrix type='saturate' values='0'/%3E%3C/filter%3E%3Crect width='128' height='128' filter='url(%23n)'/%3E%3C/svg%3E");
  background-size: 128px 128px;
  content: "";
  opacity: 0.022;
}

/* Un grain reste à un pixel physique quel que soit le rapport d'écran. */
@media (min-resolution: 2x) {
  .strata-bg::after {
    background-size: 64px 64px;
  }
}

.site-container {
  display: flex;
  width: min(1000px, calc(100% - 48px));
  min-height: 100vh;
  flex-direction: column;
  /* La composition se pose au centre : aucune carte étirée pour remplir.
     « safe » garde le haut atteignable quand la fenêtre est plus courte. */
  justify-content: center;
  justify-content: safe center;
  margin: 0 auto;
  padding: 24px 0;
}

/* ---------------------------------------------------------------- Logo -- */

/* Le logo signe la page, il ne la domine plus : ce sont les deux objets
   produits qui portent l'échelle. */
.logo-header {
  display: flex;
  justify-content: center;
  margin-bottom: 20px;
}

.logo-link {
  position: relative;
  display: block;
  border-radius: 12px;
  line-height: 0;
}

/* L'ombre portée naît sous le logo au survol, pendant qu'il se soulève : c'est
   ce qui rend le mouvement lisible comme une élévation et non comme un
   glissement. Elle n'existe pas au repos, sinon le logo flotterait en
   permanence. */
.logo-link::before {
  position: absolute;
  inset: 24% 12% 8%;
  border-radius: 50%;
  background: oklch(0.2 0.008 250 / 20%);
  content: "";
  filter: blur(26px);
  opacity: 0;
  transform: scale(0.7);
  transition:
    opacity 460ms var(--ease),
    transform 460ms var(--ease);
}

.logo-link:hover::before {
  opacity: 1;
  transform: scale(1.04);
}

.logo-link img {
  display: block;
  width: 132px;
  height: 132px;
  object-fit: contain;
  /* Le dessin n'est pas centré dans son PNG : son encre porte à gauche et vers
     le haut. Sans compensation, tout l'axe vertical de la page penche, puisque
     le logo est ce qui le fixe. Valeurs en pourcentage de la boîte, donc
     valables aussi à 128px sur mobile. */
  translate: 2.26% 2.62%;
  filter: drop-shadow(0 3px 6px oklch(0.2 0.01 250 / 10%));
  transition:
    filter 460ms var(--ease),
    translate 460ms var(--ease);
}

.logo-link:hover img {
  filter: drop-shadow(0 8px 14px oklch(0.2 0.01 250 / 10%));
  translate: 2.26% calc(2.62% - 5px);
}

/* ---------------------------------------------------------- Carte satin -- */

.satin-card {
  position: relative;
  display: flex;
  overflow: hidden;
  flex-direction: column;
  border: 1px solid var(--line);
  border-radius: 26px;
  padding: 40px 48px 28px;
  background: linear-gradient(
    177deg,
    var(--surface-top),
    var(--surface-bottom)
  );
  /* L'ombre se déplace à l'opposé de l'inclinaison : la carte a une masse
     posée sur un sol, pas une image qui pivote. */
  box-shadow:
    0 1px 2px oklch(0.2 0.01 250 / 4%),
    var(--shadow-x, 0px) calc(10px + var(--shadow-y, 0px)) 22px -10px
      oklch(0.2 0.01 250 / 10%),
    calc(var(--shadow-x, 0px) * 2) calc(44px + var(--shadow-y, 0px) * 2) 72px
      -34px oklch(0.2 0.01 250 / 20%),
    inset 0 1px 0 oklch(1 0 0 / 90%);
  transform: perspective(1200px) rotateX(var(--tilt-x, 0deg))
    rotateY(var(--tilt-y, 0deg));
}

@media (hover: hover) and (pointer: fine) {
  .satin-card {
    will-change: transform;
  }
}

/* Réflexion anisotrope : une surface satinée ne réfléchit pas en disque mais
   en bande étroite, qui balaie beaucoup pour un petit changement d'angle.
   C'est ce qui distingue le satin du halo de projecteur. */
.satin-card::after {
  position: absolute;
  z-index: 0;
  inset: 0;
  background:
    /* Grain d'un pixel par-dessus les dégradés : sans lui, l'arc du lobe sombre
       laisse une arête visible en aplat clair. Opacité cuite dans le SVG, pour
       ne pas diluer les dégradés avec l'opacité du pseudo-élément. */
    url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='128' height='128'%3E%3Cfilter id='n'%3E%3CfeTurbulence type='fractalNoise' baseFrequency='0.8' numOctaves='1' stitchTiles='stitch'/%3E%3CfeColorMatrix type='saturate' values='0'/%3E%3C/filter%3E%3Crect width='128' height='128' filter='url(%23n)' opacity='0.03'/%3E%3C/svg%3E")
      0 0 / 128px 128px repeat,
    radial-gradient(
      1100px 220px at calc(50% + var(--sheen-x, 0px))
        calc(38% + var(--sheen-y, 0%)),
      oklch(1 0 0 / var(--sheen-alpha, 30%)),
      oklch(1 0 0 / 0%) 60%
    ),
    /* Le lobe sombre, à l'opposé, et c'est LUI qui porte l'effet. Sur une
       surface à 0,97 de clarté, un reflet blanc n'a que 3 % de marge vers le
       haut quand une ombre en a 8 % vers le bas : ce qu'on voit glisser sur du
       satin clair, c'est l'ombre, pas le reflet. Mesuré à l'écran, pas déduit
       de la feuille de style. */
    radial-gradient(
      1300px 800px at calc(50% - var(--sheen-x, 0px) * 1.3)
        calc(112% - var(--sheen-y, 0%)),
      oklch(0.5 0.014 250 / var(--shade-alpha, 9%)),
      oklch(0.5 0.014 250 / 0%) 70%
    );
  content: "";
  /* La matière existe avant qu'on touche la souris : c'est une surface,
     pas un effet de survol. */
  pointer-events: none;
}

/* Arête spéculaire : sur un objet réel, c'est le bord qui trahit l'inclinaison
   avant la face. Un liseré d'un pixel, blanc là où la lumière frappe, éteint
   ailleurs, et qui glisse avec la même bande que le satin. */
.satin-card::before {
  position: absolute;
  z-index: 2;
  inset: 0;
  border-radius: inherit;
  padding: 1px;
  background:
    radial-gradient(
      600px 320px at calc(50% + var(--sheen-x, 0px) * 1.4)
        calc(10% + var(--sheen-y, 0%)),
      oklch(1 0 0 / 100%),
      oklch(1 0 0 / 0%) 72%
    ),
    /* Le bord qui s'éloigne perd sa lumière et se referme d'un trait sombre :
       c'est ce contraste haut/bas de l'arête qui fait lire l'angle, avant même
       que la face n'ait bougé. */
    radial-gradient(
      700px 360px at calc(50% - var(--sheen-x, 0px) * 1.4)
        calc(116% - var(--sheen-y, 0%)),
      oklch(0.42 0.012 250 / 26%),
      oklch(0.42 0.012 250 / 0%) 76%
    );
  content: "";
  mask:
    linear-gradient(#000 0 0) content-box,
    linear-gradient(#000 0 0);
  mask-composite: exclude;
  pointer-events: none;
}

.satin-card[data-tilting="true"] {
  box-shadow:
    0 1px 2px oklch(0.2 0.01 250 / 4%),
    var(--shadow-x, 0px) calc(14px + var(--shadow-y, 0px)) 28px -12px
      oklch(0.2 0.01 250 / 12%),
    calc(var(--shadow-x, 0px) * 2) calc(54px + var(--shadow-y, 0px) * 2) 84px
      -36px oklch(0.2 0.01 250 / 24%),
    inset 0 1px 0 oklch(1 0 0 / 92%);
}

.product-grid,
.installation-row,
.card-footer {
  position: relative;
  z-index: 1;
}

/* ------------------------------------------------------------- En-tête -- */

.card-header {
  display: flex;
  flex-direction: column;
  align-items: center;
  text-align: center;
}

.card-header h1 {
  max-width: 22ch;
  margin-bottom: 12px;
  color: var(--text);
  font-size: clamp(2rem, 1.35rem + 1.7vw, 2.6rem);
  font-weight: 700;
  letter-spacing: -0.032em;
  line-height: 1.08;
  text-wrap: balance;
}

.card-header > p {
  max-width: 46ch;
  margin-bottom: 0;
  color: var(--muted);
  font-size: 1.0625rem;
  line-height: 1.55;
  text-wrap: balance;
}

/* ------------------------------------------------------------ Produits -- */

.product-grid {
  display: grid;
  grid-template-columns: repeat(2, minmax(0, 1fr));
  grid-template-rows: repeat(7, auto);
  margin: 2px 0 26px;
  /* Deux mondes distincts dans une même composition : sans cet écart, les deux
     plaques se lisaient comme un seul bandeau coupé en deux. */
  column-gap: clamp(28px, 6vw, 68px);
  /* Une seule fuyante pour les deux objets : ils sont photographiés ensemble,
     sur la même table, pas collés côte à côte. */
  perspective: 1500px;
  perspective-origin: 50% 40%;
}

.product {
  display: grid;
  min-width: 0;
  grid-row: span 7;
  grid-template-rows: subgrid;
  justify-items: center;
  text-align: center;
}

/* --------------------------------------------------------- Plaque gravée --

   Chaque application reçoit un objet, pas une ligne de texte. La plaque est
   sombre pour deux raisons : c'est l'encre déjà utilisée par la marque et par
   les boutons, et un trait clair gravé dedans se lit de loin, ce qu'aucun
   diagramme posé sur fond pâle ne fait. */

.plate {
  position: relative;
  overflow: hidden;
  width: 100%;
  aspect-ratio: 380 / 180;
  margin-bottom: clamp(18px, 2vw, 26px);
  border-radius: 16px;
  background: radial-gradient(
    130% 150% at 50% -30%,
    oklch(0.335 0.012 250),
    oklch(0.205 0.008 250) 56%,
    oklch(0.148 0.006 250)
  );
  /* L'objet est posé sur la carte, pas imprimé dessus : arête haute éclairée,
     assise sombre, et trois ombres de portées croissantes, dont la plus longue
     suit le décalage que l'inclinaison donne déjà au socle de la carte. */
  box-shadow:
    inset 0 1px 0 oklch(1 0 0 / 15%),
    inset 0 0 0 1px oklch(1 0 0 / 6%),
    0 1px 2px oklch(0.2 0.01 250 / 20%),
    calc(var(--shadow-x, 0px) * 0.45)
      calc((12px + var(--shadow-y, 0px) * 0.45) * var(--shadow-lift, 1)) 20px
      -10px oklch(0.2 0.01 250 / 30%),
    calc(var(--shadow-x, 0px) * 0.9)
      calc((34px + var(--shadow-y, 0px) * 0.9) * var(--shadow-lift, 1)) 56px
      -26px oklch(0.2 0.01 250 / 42%),
    calc(var(--shadow-x, 0px) * 1.4)
      calc((78px + var(--shadow-y, 0px) * 1.4) * var(--shadow-lift, 1)) 96px
      -60px oklch(0.2 0.01 250 / 46%);
  transform: translateY(var(--lift, 0px)) rotateY(var(--face, 0deg));
  transition:
    transform 380ms var(--ease),
    box-shadow 380ms var(--ease);
}

/* Les deux plaques se présentent l'une vers l'autre : deux objets posés sur la
   même table, pas deux vignettes collées côte à côte. */
.product:first-child .plate {
  --face: 5deg;
}

.product:last-child .plate {
  --face: -5deg;
}

/* Viser un bouton soulève l'objet correspondant. C'est le seul rapport qui
   restait à établir : lequel des deux part au téléchargement. */
.product:has(.download-button:hover) .plate,
.product:has(.download-button:focus-visible) .plate {
  --lift: -5px;
  --shadow-lift: 1.5;
}

/* Réflexion de la plaque : sur une face sombre, la bande spéculaire est le
   seul indice de matière. Elle lit les mêmes variables que le satin de la
   carte, donc un seul geste de souris anime les deux surfaces. */
.plate::after {
  position: absolute;
  z-index: 2;
  inset: 0;
  background:
    url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='128' height='128'%3E%3Cfilter id='n'%3E%3CfeTurbulence type='fractalNoise' baseFrequency='0.8' numOctaves='1' stitchTiles='stitch'/%3E%3CfeColorMatrix type='saturate' values='0'/%3E%3C/filter%3E%3Crect width='128' height='128' filter='url(%23n)' opacity='0.055'/%3E%3C/svg%3E")
      0 0 / 128px 128px repeat,
    radial-gradient(
      320px 200px at calc(50% + var(--sheen-x, 0px) * 0.8)
        calc(12% + var(--sheen-y, 0%)),
      oklch(1 0 0 / 13%),
      oklch(1 0 0 / 0%) 68%
    );
  content: "";
  pointer-events: none;
}

/* ------------------------------------------------- Contenu de la plaque --

   Ce que l'application a sous les yeux, pas un schéma de son fonctionnement.
   Le transporteur reconnaît ses propres pièces : une facture reçue, une
   commande partie, un écart en euros, une liste de commandes du jour.

   Tout est dimensionné en unités de conteneur (cqw), donc l'illustration
   grandit et rétrécit d'un bloc comme une image, au lieu de se recomposer.
   Les valeurs qui portent le sens gardent un plancher en pixels : au format
   téléphone, elles restent lisibles pendant que le reste devient de la
   matière, exactement comme sur une vraie capture vue de loin. */

.plate {
  container-type: inline-size;
}

.scene {
  position: relative;
  z-index: 1;
  display: flex;
  height: 100%;
  align-items: center;
  padding: 6.5cqw;
  gap: 4cqw;
}

/* --- La pièce de dossier, l'objet commun aux deux illustrations --- */

.doc {
  flex: 1;
  border-radius: 2.6cqw;
  background: linear-gradient(
    180deg,
    oklch(0.985 0.002 250),
    oklch(0.945 0.003 250)
  );
  box-shadow:
    0 0.3cqw 0.6cqw oklch(0.14 0.006 250 / 34%),
    0 2.2cqw 4cqw -1.4cqw oklch(0.14 0.006 250 / 46%);
  padding: 3.2cqw 3.4cqw;
}

.doc-head {
  display: flex;
  align-items: baseline;
  justify-content: space-between;
  margin: 0 0 2.2cqw;
  border-bottom: 1px solid oklch(0.2 0.01 250 / 11%);
  padding-bottom: 1.8cqw;
  gap: 2cqw;
}

.doc-source {
  color: var(--text);
  font-size: max(10px, 2.35cqw);
  font-weight: 600;
  letter-spacing: -0.01em;
  white-space: nowrap;
}

.doc-ref {
  color: var(--muted);
  font-size: max(9px, 2.05cqw);
  font-weight: 500;
  font-variant-numeric: tabular-nums;
  white-space: nowrap;
}

.doc-route {
  margin: 0 0 1.4cqw;
  color: var(--muted);
  font-size: max(9px, 2.05cqw);
  font-weight: 500;
  white-space: nowrap;
}

/* Le montant est ce que le transporteur cherche en premier. Il est donc la
   plus grosse valeur de la pièce, et en chiffres à chasse fixe pour que les
   deux colonnes s'alignent au centime. */
.doc-amount {
  margin: 0;
  color: var(--text);
  font-size: max(15px, 3.9cqw);
  font-weight: 600;
  font-variant-numeric: tabular-nums;
  letter-spacing: -0.02em;
}

/* --- Contrôle de facturation : deux pièces, un écart --- */

/* Deux pièces côte à côte, la conclusion dessous et centrée : c'est l'ordre
   dans lequel un humain fait la comparaison, et ça évite que l'écart ait l'air
   d'appartenir à l'une des deux colonnes. */
.scene-control {
  display: grid;
  align-content: center;
  justify-items: center;
  gap: 3.2cqw;
  grid-template-columns: 1fr 1fr;
}

.scene-control .doc {
  width: 100%;
  min-width: 0;
}

.scene-context,
.gap-chip {
  grid-column: 1 / -1;
}

.scene-context {
  margin: 0;
  color: oklch(0.78 0.005 250);
  font-size: max(9px, 2.1cqw);
  font-weight: 500;
  letter-spacing: 0.002em;
  white-space: nowrap;
}

/* Le montant facturé est celui qui pose problème : il est le seul à ne pas
   être aligné sur le reste du dossier. */
.doc-invoice .doc-amount {
  color: oklch(0.19 0.008 250);
}

.doc-order .doc-amount {
  color: var(--text-soft);
}

/* L'écart est la conclusion de la comparaison, donc le point le plus lumineux
   de la plaque : c'est la seule chose que l'application a produite. */
.gap-chip {
  display: inline-flex;
  width: auto;
  align-items: baseline;
  margin: 0;
  border-radius: 999px;
  background: oklch(0.99 0.002 250);
  box-shadow:
    0 0.3cqw 0.7cqw oklch(0.14 0.006 250 / 40%),
    0 1.6cqw 3cqw -1cqw oklch(0.14 0.006 250 / 46%);
  padding: 1.5cqw 3.4cqw;
  gap: 1.6cqw;
}

.gap-label {
  color: var(--muted);
  font-size: max(9px, 2.05cqw);
  font-weight: 600;
}

.gap-value {
  color: var(--text);
  font-size: max(12px, 3cqw);
  font-weight: 700;
  font-variant-numeric: tabular-nums;
  letter-spacing: -0.02em;
}

/* --- Commandes : les canaux d'arrivée, puis la liste tenue --- */

.scene-orders {
  gap: 0;
}

.inbox {
  display: flex;
  flex: 0 0 auto;
  flex-direction: column;
  margin: 0;
  padding: 0;
  gap: 2.6cqw;
  list-style: none;
}

.inbox li {
  display: flex;
  align-items: center;
  color: oklch(0.86 0.004 250);
  font-size: max(9px, 2.15cqw);
  font-weight: 500;
  gap: 1.6cqw;
  white-space: nowrap;
}

.inbox svg {
  width: max(11px, 2.9cqw);
  height: max(11px, 2.9cqw);
  flex: none;
  fill: none;
  stroke: oklch(0.72 0.006 250);
  stroke-linecap: round;
  stroke-linejoin: round;
  stroke-width: 1.7;
}

/* Les quatre canaux se rejoignent en un point : c'est tout le propos de
   l'application, une seule file en sortie quelle que soit l'entrée. */
.funnel {
  width: 9cqw;
  height: 78%;
  flex: none;
  margin: 0 0.6cqw;
}

.funnel path {
  fill: none;
  stroke: oklch(0.62 0.008 250);
  stroke-width: 1.4;
  vector-effect: non-scaling-stroke;
}

.doc-list {
  flex: 1 1 auto;
  min-width: 0;
}

.doc-list ul {
  margin: 0;
  padding: 0;
  list-style: none;
}

.doc-list li {
  display: grid;
  align-items: baseline;
  padding: 1.35cqw 0;
  gap: 2cqw;
  grid-template-columns: auto 1fr auto;
}

.doc-list li + li {
  border-top: 1px solid oklch(0.2 0.01 250 / 8%);
}

.row-ref {
  color: var(--text);
  font-size: max(9.5px, 2.3cqw);
  font-weight: 600;
  font-variant-numeric: tabular-nums;
  letter-spacing: -0.01em;
}

.row-city {
  color: var(--muted);
  font-size: max(9px, 2.1cqw);
  font-weight: 500;
}

/* Deux états seulement, et ils se distinguent par le poids, pas par une
   couleur : la page reste monochrome, et « à valider » est ce qui appelle
   l'opérateur. */
.row-state {
  border-radius: 999px;
  background: oklch(0.2 0.01 250 / 7%);
  padding: 0.7cqw 1.7cqw;
  color: var(--text-soft);
  font-size: max(8.5px, 1.95cqw);
  font-weight: 600;
  white-space: nowrap;
}

.row-state.is-check {
  background: var(--text);
  color: oklch(0.985 0.002 250);
}

/* ------------------------------------------------ Entrée des documents --

   Le dossier se pose dans l'ordre où il se constitue : les pièces d'abord,
   la conclusion ensuite. L'état final est l'état naturel du dessin, donc un
   rendu sans animation affiche déjà l'illustration complète. */

@keyframes doc-in {
  from {
    opacity: 0;
    translate: 0 2.5cqw;
  }
}

@keyframes trace-in {
  from {
    stroke-dashoffset: 90;
  }
}

.scene-control .doc {
  animation: doc-in 620ms var(--ease) backwards;
  animation-delay: calc(120ms + var(--i) * 130ms);
}

/* L'écart ne peut pas apparaître avant les deux pièces qu'il compare. */
.gap-chip {
  animation: doc-in 560ms var(--ease) backwards;
  animation-delay: 620ms;
}

.inbox li {
  animation: doc-in 560ms var(--ease) backwards;
  animation-delay: calc(100ms + var(--i) * 90ms);
}

.funnel path {
  stroke-dasharray: 90;
  animation: trace-in 700ms cubic-bezier(0.16, 1, 0.3, 1) backwards;
  animation-delay: calc(420ms + var(--i) * 70ms);
}

.doc-list {
  animation: doc-in 640ms var(--ease) backwards;
  animation-delay: 300ms;
}

.doc-list li {
  animation: doc-in 520ms var(--ease) backwards;
  animation-delay: calc(760ms + var(--i) * 110ms);
}

/* ------------------------------------------------------------ Libellés -- */

.product-name {
  margin-bottom: 5px;
  color: var(--text);
  font-size: 1.75rem;
  font-weight: 600;
  letter-spacing: -0.026em;
  line-height: 1.15;
}

.product-role {
  margin-bottom: 0;
  color: var(--text-soft);
  font-size: 1.0625rem;
  font-weight: 500;
  letter-spacing: -0.012em;
  line-height: 1.35;
}

.product-description {
  max-width: 32ch;
  margin: 12px 0 0;
  color: var(--muted);
  font-size: 0.875rem;
  line-height: 1.45;
  text-wrap: balance;
}

.product-more-link {
  display: inline-flex;
  min-height: 44px;
  align-items: center;
  justify-content: center;
  margin-top: 2px;
  border-radius: 8px;
  padding: 0 10px;
  color: var(--muted);
  font-size: 0.875rem;
  font-weight: 500;
  text-decoration-color: color-mix(in oklch, currentColor 42%, transparent);
  text-decoration-thickness: 1px;
  text-underline-offset: 4px;
  transition:
    background-color 200ms var(--ease),
    color 200ms var(--ease),
    text-decoration-color 200ms var(--ease);
}

.product-more-link:hover {
  background: var(--wash);
  color: var(--text);
  text-decoration-color: currentColor;
}

.release-meta {
  display: flex;
  /* Hauteur figée : la confirmation de clic réutilise cette ligne sans
     déplacer d'un pixel ce qu'il y a en dessous. */
  min-height: 1.05rem;
  align-items: center;
  justify-content: center;
  gap: 8px;
  margin: 18px 0 16px;
  color: var(--muted);
  font-size: 0.8125rem;
  font-variant-numeric: tabular-nums;
  letter-spacing: -0.005em;
}

.release-dot {
  width: 3px;
  height: 3px;
  border-radius: 50%;
  background: currentColor;
  opacity: 0.5;
}

/* Après le clic, la même ligne confirme le départ. Elle monte de quatre pixels
   en apparaissant : le texte a changé parce qu'il s'est passé quelque chose,
   pas parce qu'il a été réécrit en silence. */
@keyframes confirm-in {
  from {
    opacity: 0;
    translate: 0 4px;
  }
}

.release-meta[data-confirming] {
  animation: confirm-in 420ms var(--ease);
  color: var(--text-soft);
}

/* La flèche part par le bas et revient par le haut. Le saut de 40 % à 41 % se
   fait à opacité nulle : on ne voit jamais la remontée, seulement un départ
   puis un retour. */
@keyframes button-send {
  40% {
    opacity: 0;
    translate: 0 22px;
  }

  41% {
    opacity: 0;
    translate: 0 -22px;
  }

  100% {
    opacity: 1;
    translate: 0 0;
  }
}

.download-button[data-sending] svg {
  animation: button-send 780ms var(--ease);
}

.download-button {
  position: relative;
  display: inline-flex;
  overflow: hidden;
  width: min(100%, 17.5rem);
  min-height: 48px;
  align-items: center;
  justify-content: center;
  gap: 10px;
  align-self: end;
  border-radius: 11px;
  padding: 12px 22px;
  background: var(--text);
  box-shadow:
    0 1px 2px oklch(0.2 0.01 250 / 22%),
    0 8px 18px -8px oklch(0.2 0.01 250 / 40%);
  color: oklch(0.985 0.002 250);
  font-size: 0.9375rem;
  font-weight: 600;
  /* Chasse positive : à cette taille, du texte clair sur fond sombre déborde
     optiquement et se resserre. Le crénage négatif des titres l'abîmerait. */
  letter-spacing: 0.004em;
  text-decoration: none;
  transition:
    background-color 200ms var(--ease),
    box-shadow 200ms var(--ease),
    translate 200ms var(--ease);
}

/* Le seul endroit de la page où la lumière est vraiment visible. Le satin de la
   carte est une surface claire et mate : la physique interdit d'y voir un
   reflet. Le bouton est sombre et laqué, donc il en porte un, et il suit la
   souris via --sheen-x, hérité de la carte. Mesuré : 90,6 % des pixels du
   bouton changent entre deux positions de souris, contre 2,6 % sur le satin. */
.download-button::before {
  position: absolute;
  inset: 0;
  border-radius: inherit;
  background: radial-gradient(
    240px 90px at calc(50% + var(--sheen-x, 0px) * 0.55) -30%,
    oklch(1 0 0 / 26%),
    oklch(1 0 0 / 0%) 70%
  );
  content: "";
  pointer-events: none;
}

/* L'arête supérieure, là où la lumière rasante accroche une surface bombée. */
.download-button::after {
  position: absolute;
  z-index: 1;
  inset: 0;
  border-radius: inherit;
  box-shadow: inset 0 1px 0 oklch(1 0 0 / 16%);
  content: "";
  pointer-events: none;
}

.download-button > span,
.download-button svg {
  position: relative;
  z-index: 2;
}

.download-button svg {
  width: 18px;
  height: 18px;
  /* Même correction que la chasse : un trait clair sur fond sombre s'amincit. */
  stroke-width: 2.1;
  transition: translate 280ms var(--ease);
}

/* Le bouton se décolle de la carte au survol et son ombre s'allonge d'autant.
   Effet d'origine du dépôt, retiré en route. */
.download-button:hover {
  background: oklch(0.27 0.009 250);
  box-shadow:
    0 1px 2px oklch(0.2 0.01 250 / 24%),
    0 14px 24px -10px oklch(0.2 0.01 250 / 46%);
  translate: 0 -2px;
}

/* La flèche descend de deux pixels au survol : elle dit ce que le bouton fait,
   c'est la seule raison de la faire bouger. */
.download-button:hover svg {
  translate: 0 2px;
}

/* À l'appui il retombe sur la carte : deux pixels de course depuis le survol,
   et l'ombre se raccourcit d'autant. */
.download-button:active {
  box-shadow:
    0 1px 2px oklch(0.2 0.01 250 / 26%),
    0 4px 10px -6px oklch(0.2 0.01 250 / 40%);
  translate: 0 0;
  transition-duration: 90ms;
}

/* -------------------------------------------------------- Installation -- */

.installation-row {
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: 24px;
  /* Séparateur gravé, pas posé : trait sombre puis arête claire dessous,
     comme un filet gaufré dans du papier épais. */
  border-top: 1px solid var(--line);
  box-shadow: inset 0 1px 0 oklch(1 0 0 / 90%);
  padding: 18px 0;
}

.installation-row h2 {
  margin-bottom: 0;
  color: var(--text);
  font-size: 1rem;
  font-weight: 600;
  letter-spacing: -0.018em;
}

.video-link {
  display: inline-flex;
  min-height: 44px;
  align-items: center;
  gap: 10px;
  /* Le retrait compense le rembourrage : le texte reste sur la marge. */
  margin-right: -8px;
  border-radius: 8px;
  padding: 8px;
  /* Second rôle de la ligne : le titre de la section reste l'ancre, ce lien
     l'accompagne. Même graisse que les deux titres et ils se disputeraient
     l'attention à un mètre de l'écran. */
  color: var(--text-soft);
  font-size: 0.9062rem;
  font-weight: 500;
  letter-spacing: -0.006em;
  text-decoration: none;
  transition:
    background-color 200ms var(--ease),
    color 200ms var(--ease);
}

.video-link svg {
  width: 21px;
  height: 21px;
  color: var(--muted);
  transition: color 200ms var(--ease);
}

.video-link svg path {
  fill: currentColor;
  stroke-width: 1.2;
}

.video-link-time {
  color: var(--muted);
  font-size: 0.8125rem;
  font-weight: 500;
  font-variant-numeric: tabular-nums;
}

.video-link:hover {
  background: var(--wash);
  color: var(--text);
}

.video-link:hover svg {
  color: var(--text);
}

/* -------------------------------------------------------------- Pieds -- */

.card-footer {
  display: flex;
  justify-content: center;
  border-top: 1px solid var(--line);
  padding-top: 18px;
  text-align: center;
}

.card-footer p {
  margin-bottom: 0;
  color: var(--muted);
  font-size: 0.8125rem;
  font-weight: 500;
  letter-spacing: -0.004em;
  /* Sur une seule colonne, la ligne se coupe : elle doit se couper aux points
     médians, jamais au milieu de « Mises à jour ». */
  text-wrap: balance;
}

/* Le point médian appartient à la mention qui le précède : sur une colonne, la
   ligne se coupe après lui, jamais devant lui ni dans « Mises à jour ». */
.card-footer span {
  white-space: nowrap;
}

.card-footer span:not(:last-child)::after {
  content: " ·";
}

.site-footer {
  padding: 20px 0 24px;
  color: var(--muted);
  font-size: 0.75rem;
  text-align: center;
}

/* Anneau de focus à l'encre de la marque : 18:1 contre la carte, là où le
   bleu système plafonnait à 4:1 et jurait avec une palette sans couleur.
   Le décalage laisse passer le blanc, donc l'anneau reste lisible même
   posé sur un bouton noir. */
.logo-link:focus-visible,
.download-button:focus-visible,
.product-more-link:focus-visible,
.video-link:focus-visible,
.back-link:focus-visible {
  outline: 2px solid var(--focus);
  outline-offset: 3px;
}

/* -------------------------------------------------- Page installation -- */

.installation-container {
  max-width: 920px;
}

.installation-card {
  padding-top: clamp(26px, 2.6vw, 34px);
}

.back-link {
  position: relative;
  z-index: 1;
  display: inline-flex;
  width: fit-content;
  min-height: 44px;
  align-self: flex-start;
  align-items: center;
  gap: 8px;
  margin: 0 0 12px;
  border-radius: 8px;
  padding: 8px 12px;
  color: var(--muted);
  font-size: 0.875rem;
  font-weight: 500;
  text-decoration: none;
  transition:
    background-color 200ms var(--ease),
    color 200ms var(--ease);
}

.back-link svg {
  width: 18px;
  height: 18px;
}

.back-link:hover {
  background: var(--wash);
  color: var(--text);
}

.installation-heading {
  margin: 0 0 28px;
}

.installation-heading h1 {
  font-size: clamp(1.9rem, 1.3rem + 1.5vw, 2.4rem);
}

/* Même objet que les plaques de la page d'accueil : même rayon, même famille
   d'ombres. Les deux pages tiennent alors d'un seul système. */
.video-frame {
  position: relative;
  z-index: 1;
  overflow: hidden;
  border-radius: 16px;
  background: oklch(0.16 0.006 250);
  box-shadow:
    inset 0 0 0 1px oklch(1 0 0 / 6%),
    0 1px 2px oklch(0.2 0.01 250 / 20%),
    var(--shadow-x, 0px) calc(12px + var(--shadow-y, 0px) * 0.45) 20px -10px
      oklch(0.2 0.01 250 / 26%),
    calc(var(--shadow-x, 0px) * 0.9) calc(34px + var(--shadow-y, 0px) * 0.9)
      56px -26px oklch(0.2 0.01 250 / 38%),
    calc(var(--shadow-x, 0px) * 1.4) calc(78px + var(--shadow-y, 0px) * 1.4)
      96px -60px oklch(0.2 0.01 250 / 42%);
}

.video-frame video {
  display: block;
  width: 100%;
  aspect-ratio: 16 / 9;
  background: oklch(0.12 0.004 250);
}

.after-video {
  position: relative;
  z-index: 1;
  max-width: 58ch;
  margin: 24px auto 0;
  color: var(--muted);
  font-size: 0.9062rem;
  line-height: 1.6;
  text-align: center;
}

/* ------------------------------------------------------------ Mobile --- */

@media (max-width: 760px) {
  .site-container {
    width: min(100% - 24px, 560px);
    padding-top: 14px;
  }

  .logo-header {
    margin-bottom: 14px;
  }

  .logo-link img {
    width: 128px;
    height: 128px;
  }

  .satin-card {
    border-radius: 22px;
    padding: 28px 20px 22px;
    transform: none !important;
  }

  .card-header h1 {
    font-size: 1.95rem;
    letter-spacing: -0.028em;
  }

  .card-header > p {
    font-size: 1rem;
  }

  .product-grid {
    display: block;
    margin: 0 0 24px;
  }

  .product {
    display: flex;
    flex-direction: column;
  }

  .product + .product {
    margin-top: 30px;
    border-top: 1px solid var(--line);
    padding-top: 30px;
  }

  /* Une colonne : chaque objet est regardé de face, l'un après l'autre.
     La mise en scène à deux n'a plus d'objet, donc l'angle non plus. */
  .plate {
    --face: 0deg;

    margin-bottom: 18px;
    border-radius: 14px;
  }

  .product-description {
    max-width: 40ch;
  }

  .product-more-link {
    width: fit-content;
    align-self: center;
  }

  .release-meta {
    margin: 16px 0 16px;
    font-size: 0.875rem;
  }

  .download-button {
    width: 100%;
    max-width: none;
    min-height: 50px;
  }

  .installation-row {
    align-items: flex-start;
    flex-direction: column;
    gap: 14px;
    padding: 24px 0;
  }

  .video-link {
    width: calc(100% + 16px);
    margin-left: -8px;
  }

  .video-link-time {
    margin-left: auto;
  }

  .card-footer {
    padding-top: 20px;
  }

  .installation-heading {
    margin-bottom: 24px;
  }

  .back-link {
    margin-bottom: 8px;
  }

  .video-frame {
    border-radius: 14px;
  }
}

/* ---------------------------------------------------- Mouvement réduit -- */

@media (prefers-reduced-motion: reduce) {
  *,
  *::before,
  *::after {
    animation: none !important;
    transition-duration: 0.01ms !important;
  }

  /* Le satin et l'arête restent : ils ne bougent plus, mais la matière n'est
     pas du mouvement et la retirer laisserait une surface plate. */
  .satin-card {
    transform: none !important;
  }
}
