/* ============================================================
   Fresh Detailing 95 — scrollytelling.css   "Le Rituel"
   Scroll-LINKED chapter choreography (see assets/js/modules/scrollytelling.js).
   Loaded only on pages that set $pageActs = true (index/prestations/a-propos).

   PERF CONTRACT — the same discipline the aurora background had to be
   rewritten to respect (see effects.css history):
     - only transform / opacity / clip-path may change per frame;
     - NO filter: blur(), NO backdrop-filter, NO mix-blend-mode on anything
       that moves. In particular: never put .glass inside a moving layer —
       it re-blurs its backdrop on every frame;
     - no [data-tilt] / [data-magnetic] on an element that also receives a
       --act-p transform: those modules write inline style.transform, which
       always wins over a stylesheet rule and would silently break the
       choreography;
     - keep each stage under ~20 elements and ~6 animated layers;
     - every animated layer's will-change is gated by .is-near (added/removed
       by scrollytelling.js as the act enters/leaves a viewport margin) —
       never applied unconditionally.

   STACKING — inside a stage, every positioned layer gets an explicit
   POSITIVE z-index. Never mix an explicit z-index:0 with a sibling at
   z-index:auto: they are not painted at the same step, which is the exact
   bug that once made the aurora background paint over the whole page (see
   effects.css). .act__stage carries isolation:isolate so a stage's stacking
   is self-contained.

   STICKY TRAP — the tall min-height MUST sit on .act, the DIRECT parent of
   the sticky .act__stage, and .act must keep overflow:visible. .container
   goes INSIDE the stage, never around it — any ancestor with
   overflow != visible between the sticky element and the viewport disables
   the sticky positioning silently.

   AUTHORING RULE — every --act-p rule is a transform AWAY FROM identity.
   Identity (no --act-p applied) must always equal the designed, legible
   "poster" composition, so a failed JS chunk or prefers-reduced-motion never
   hides content — it just freezes it at its settled state.
   ============================================================ */

/* ---------- Scaffolding: base (no JS / no enhancement) ---------- */
.act {
  position: relative;
  padding-block: var(--space-2xl);
}
.act__grid {
  display: grid;
  grid-template-columns: 1fr;
  gap: var(--space-lg);
  align-items: center;
}
@media (min-width: 900px) {
  .act__grid { grid-template-columns: 1.05fr 1fr; gap: var(--space-xl); }
  /* Alternating layout: media on the left, text on the right */
  .act__grid--reverse { grid-template-columns: 1fr 1.05fr; }
  .act__grid--reverse .act__text { order: 2; }
  .act__grid--reverse .act__media { order: 1; }
}

.act__num {
  font-family: var(--font-display);
  font-size: var(--fs-xl);
  color: var(--c-gold-500);
  line-height: 1;
  display: block;
  margin-bottom: var(--space-2xs);
}
.act__title { font-size: var(--fs-3xl); margin-bottom: var(--space-sm); }
.act__body { max-width: 46ch; margin-bottom: var(--space-md); }

.act__media {
  position: relative;
  overflow: hidden; /* safe here: .act__media is never an ancestor of the sticky stage */
  border-radius: var(--radius-lg);
  aspect-ratio: 4 / 3;
  box-shadow: var(--shadow-md);
  background: var(--c-bg-elevated);
}
.act__img { position: absolute; inset: 0; width: 100%; height: 100%; object-fit: cover; }
.act__img--a { z-index: 1; }
.act__img--b { z-index: 2; }
.act__squeegee { z-index: 3; }

/* ---------- Scaffolding: enhanced (pin + progress-driven motion) ---------- */
@media (prefers-reduced-motion: no-preference) {
  .js .act {
    padding-block: 0;
    min-height: calc(var(--act-h, 300) * 1vh);
    min-height: calc(var(--act-h, 300) * 1svh); /* svh: immune to the mobile URL-bar resize */
  }
  .js .act__stage {
    position: sticky;
    top: 0;
    height: 100vh;
    height: 100svh;
    display: grid;
    align-content: center;
    overflow: hidden;
    isolation: isolate;
    padding-block: calc(var(--header-h) + var(--space-md)) var(--space-lg);
  }
  /* Explicit stacking order inside a stage. The content container MUST be
     positioned with an explicit z-index above the decorative layers —
     a static (z-index:auto) container paints BEFORE positioned siblings and
     would end up underneath the numeral/glow/beads. */
  .js .act__stage > .container { position: relative; z-index: 3; }

  /* Giant decorative chapter numeral behind the content — aria-hidden, never a heading */
  .js .act__num {
    position: absolute;
    z-index: 1;
    inset-block-start: 6%;
    inset-inline-end: 2%;
    margin: 0;
    font-size: clamp(6rem, 20vw, 21rem);
    line-height: 0.78;
    color: transparent;
    -webkit-text-stroke: 1px rgba(232, 200, 119, 0.2);
    pointer-events: none;
  }
  @supports not (-webkit-text-stroke: 1px #000) {
    .js .act__num { color: rgba(232, 200, 119, 0.1); }
  }
  .js .act.is-live .act__num {
    transform: translate3d(calc((var(--act-p, 0) - 0.5) * -8vw), 0, 0);
  }
  @media (min-width: 900px) {
    .js .act.is-live .act__num { transform: translate3d(calc((var(--act-p, 0) - 0.5) * -22vw), 0, 0); }
  }

  .js .act.is-near .act__anim { will-change: transform, opacity; }

  /* Landscape phones / short windows: 100svh leaves too little room for a
     chapter — unpin entirely. scrollytelling.js reads this back via
     getComputedStyle and switches that act to "cross" progress by itself. */
  @media (max-height: 560px) {
    .js .act { min-height: 0; padding-block: var(--space-xl); }
    .js .act__stage { position: static; height: auto; overflow: visible; padding-block: 0; }
    .js .act__num {
      position: static; font-size: var(--fs-xl); color: var(--c-gold-500);
      -webkit-text-stroke: 0; transform: none !important;
    }
  }
}

/* Mobile-first scroll budget per act — shorter on phones (less patience,
   less benefit from a long pin), taller on desktop. */
.act--diagnostic      { --act-h: 220; }
.act--decontamination { --act-h: 240; }
.act--polissage       { --act-h: 220; }
.act--ceramique       { --act-h: 240; }
.act--histoire        { --act-h: 280; }
.act--art             { --act-h: 220; }
.act--valeurs         { --act-h: 200; }
@media (min-width: 900px) {
  .act--diagnostic      { --act-h: 300; }
  .act--decontamination { --act-h: 340; }
  .act--polissage       { --act-h: 300; }
  .act--ceramique       { --act-h: 340; }
  .act--histoire        { --act-h: 400; }
  .act--art             { --act-h: 300; }
  .act--valeurs         { --act-h: 280; }
  .act--pan             { --act-h: 320; }
}

/* ---------- Treatment A — kinetic type mask (every act title) ---------- */
.act__title .line { display: block; }
.act__title .line:nth-child(2) { --d: 0.05; }
.act__title .line:nth-child(3) { --d: 0.1; }

@media (prefers-reduced-motion: no-preference) {
  .js .act__title .line {
    overflow: hidden;
    padding-bottom: 0.08em; /* Bebas descenders/accents would clip otherwise */
    margin-bottom: -0.08em;
  }
  .js .act.is-live .act__title .line > span {
    display: block;
    transform: translate3d(0, calc((1 - clamp(0, (var(--act-p, 0) - var(--d, 0)) / 0.18, 1)) * 105%), 0);
  }

  /* Whole text column exits upward + fades in the last 20% of the act */
  .js .act.is-live .act__text {
    --exit: clamp(0, (var(--act-p, 0) - 0.8) / 0.2, 1);
    transform: translate3d(0, calc(var(--exit) * -12vh), 0);
    opacity: calc(1 - var(--exit));
  }
}

/* ---------- Ouverture — the hero reacts to the very first scroll ----------
   data-act="pass" on .hero: progress 0 at the top of the page, 1 once the
   hero is fully scrolled past. The transform sits on the WRAPPER
   (.hero__content), never on its [data-reveal] children, so the intro
   stagger and the scroll choreography compose instead of fighting. */
@media (prefers-reduced-motion: no-preference) {
  .js .hero.is-live .hero__content {
    transform: translate3d(0, calc(var(--act-p, 0) * -14vh), 0);
    opacity: calc(1 - var(--act-p, 0) * 1.5);
  }
  .js .hero.is-live .hero__media {
    transform: translate3d(0, calc(var(--act-p, 0) * 10vh), 0) scale(calc(1 + var(--act-p, 0) * 0.08));
  }
}

/* ---------- Treatment F — media letterbox open (Acte 01, Diagnostic) ---------- */
.act--diagnostic { --b-open: clamp(0, (var(--act-p, 0) - 0.15) / 0.4, 1); }

@media (prefers-reduced-motion: no-preference) {
  /* No clip-path when not live == fully open == the settled composition. */
  .js .act--diagnostic.is-live .act__media {
    clip-path: inset(calc((1 - var(--b-open)) * 20%) 0 round var(--radius-lg));
  }
}

/* ---------- Treatment B — foam wipe (Acte 02, Décontamination) ---------- */
.act--decontamination { --b-wipe: clamp(0, (var(--act-p, 0) - 0.2) / 0.5, 1); }

.act__squeegee {
  position: absolute; inset: 0; pointer-events: none; opacity: 0;
  background: linear-gradient(90deg,
    rgba(246, 230, 180, 0) 0,
    rgba(246, 230, 180, 0.9) 1.5px,
    rgba(246, 230, 180, 0.35) 3px,
    rgba(246, 230, 180, 0) 7px);
}

@media (prefers-reduced-motion: no-preference) {
  /* Clean plate (b) revealed left→right behind a diagonal squeegee edge.
     -14%..+114% so the edge fully clears both sides of the frame. No
     clip-path at all when not live == fully revealed == the settled state. */
  .js .act--decontamination.is-live .act__img--b {
    --wipe: calc(var(--b-wipe) * 128% - 14%);
    clip-path: polygon(0 0, calc(var(--wipe) + 6%) 0, calc(var(--wipe) - 6%) 100%, 0 100%);
  }
  .js .act--decontamination.is-live .act__squeegee {
    transform: translate3d(calc(var(--b-wipe) * 128% - 14%), 0, 0) skewX(-9deg);
    opacity: calc(min(var(--b-wipe), 1 - var(--b-wipe)) * 8);
  }
}

/* ---------- Treatment C — mirror type (Acte 03, Polissage) ---------- */
.act--polissage { --b-mirror: clamp(0, (var(--act-p, 0) - .25) / .35, 1); --b-shine: clamp(0, (var(--act-p, 0) - .38) / .34, 1); }

.act__mirror {
  display: block;
  transform: scaleY(-1);
  transform-origin: top;
  margin-top: 0.05em;
  opacity: 0.16;
  -webkit-mask-image: linear-gradient(to bottom, rgba(0, 0, 0, 0.85), transparent 60%);
          mask-image: linear-gradient(to bottom, rgba(0, 0, 0, 0.85), transparent 60%);
  pointer-events: none;
}
.act__shine {
  background: linear-gradient(100deg,
    var(--c-chrome-700) 0 34%,
    var(--c-gold-100) 45%, #fffdf6 50%, var(--c-gold-100) 55%,
    var(--c-chrome-700) 66% 100%);
  background-size: 300% 100%;
  background-position: 100% 0;
  -webkit-background-clip: text; background-clip: text;
  color: transparent;
}

@media (prefers-reduced-motion: no-preference) {
  .js .act--polissage.is-live .act__mirror {
    transform: scaleY(-1) translate3d(0, calc((1 - var(--b-mirror)) * -8%), 0);
    opacity: calc(var(--b-mirror) * .2);
  }
  .js .act--polissage.is-live .act__shine {
    background-position: calc(100% - var(--b-shine) * 100%) 0;
  }
  /* Media beat: opacity cross-fade rather than a second wipe (Acte 02 already used that gesture) */
  .js .act--polissage.is-live .act__img--b { opacity: var(--b-mirror); }
}

/* ---------- Treatment D — céramique (beading + big stat) ---------- */
.act--ceramique { --b-set: clamp(0, (var(--act-p, 0) - .3) / .45, 1); }

/* Beads live in their own absolutely-positioned layer so they can drift over
   the whole stage without affecting the grid. Hidden in the static/no-JS
   state — they are pure decoration with no informational value. */
.act__beads { display: none; }
@media (prefers-reduced-motion: no-preference) {
  .js .act--ceramique .act__beads {
    display: block;
    position: absolute; inset: 0; z-index: 2;
    pointer-events: none;
  }
}

.act__bead {
  position: absolute; border-radius: 50%; opacity: 0.5;
  background: radial-gradient(circle at 34% 28%,
    rgba(255, 255, 255, 0.92), rgba(255, 255, 255, 0.18) 46%, rgba(255, 255, 255, 0.05) 72%);
}
.act__bead:nth-child(1) { width: 15px; height: 15px; left: 14%; top: 10%; --fall: 44vh; --lag: 0; }
.act__bead:nth-child(2) { width: 9px; height: 9px; left: 31%; top: 18%; --fall: 52vh; --lag: .06; }
.act__bead:nth-child(3) { width: 22px; height: 22px; left: 48%; top: 8%; --fall: 38vh; --lag: .02; }
.act__bead:nth-child(4) { width: 12px; height: 12px; left: 63%; top: 22%; --fall: 48vh; --lag: .1; }
.act__bead:nth-child(5) { width: 7px; height: 7px; left: 76%; top: 14%; --fall: 56vh; --lag: .04; }
.act__bead:nth-child(6) { width: 18px; height: 18px; left: 87%; top: 26%; --fall: 40vh; --lag: .12; }

.act__glow {
  position: absolute; inset: 0; z-index: 0; pointer-events: none; opacity: 0.55;
  background: radial-gradient(60% 55% at 50% 45%, rgba(232, 200, 119, .22), transparent 70%);
}
.act__stat-row { display: flex; gap: var(--space-lg); margin-block: var(--space-sm) var(--space-md); }
.act__stat { text-align: left; }
.act__stat-num { font-family: var(--font-display); font-size: var(--fs-3xl); line-height: 1; color: var(--c-gold-300); }
.act__stat-label { font-size: var(--fs-xs); color: var(--c-text-faint); text-transform: uppercase; letter-spacing: .06em; }

@media (prefers-reduced-motion: no-preference) {
  .js .act--ceramique.is-live .act__bead {
    --t: clamp(0, (var(--act-p, 0) - var(--lag)) / .55, 1);
    transform: translate3d(0, calc(var(--t) * var(--fall)), 0);
    opacity: calc(min(var(--t) * 5, 1) * (1 - var(--t)) * 1.5);
  }
  .js .act--ceramique.is-live .act__stat { transform: scale(calc(.88 + var(--b-set) * .12)); opacity: calc(.15 + var(--b-set) * .85); }
  .js .act--ceramique.is-live .act__glow { opacity: calc(var(--b-set) * .55); }
}

/* ---------- Treatment G — vertical timeline (À propos, Histoire) ---------- */
.act--histoire .act__stage { display: grid; grid-template-columns: 1fr; }
.act--histoire .act__timeline {
  position: relative;
  display: grid;
  max-width: 640px;
  margin-inline: auto;
  padding-inline: var(--container-pad);
}
.act--histoire .act__rail {
  position: absolute;
  left: calc(var(--container-pad) - 1px);
  top: 6%; bottom: 6%;
  width: 2px;
  background: var(--c-border);
  z-index: 1;
}
.act--histoire .act__rail::after {
  content: "";
  position: absolute; inset: 0;
  background: var(--gradient-gold);
  transform-origin: top;
  transform: scaleY(var(--act-p, 1)); /* not live → full gold rail (settled state) */
}
.act__beat {
  grid-area: 1 / 1; /* CSS grid stacking — every beat occupies the same cell,
                        sane in every fallback state (no absolute positioning
                        needed, no layout collapse if JS never runs). */
  padding-left: calc(var(--space-lg) + var(--space-2xs));
  padding-block: var(--space-md);
}
.act__beat-year { font-family: var(--font-display); color: var(--c-gold-300); font-size: var(--fs-lg); margin-bottom: .3em; }

@media (prefers-reduced-motion: no-preference) {
  .js .act--histoire.is-live .act__beat {
    --t: clamp(0, (var(--act-p, 0) - var(--from, 0)) / .12, 1);
    --u: clamp(0, (var(--act-p, 0) - var(--to, 1)) / .12, 1);
    opacity: calc(var(--t) - var(--u));
    transform: translate3d(0, calc((1 - var(--t)) * 5vh + var(--u) * -5vh), 0) scale(calc(.97 + var(--t) * .03));
  }
}

/* ---------- Treatment H — animated triptych (À propos, Valeurs) ----------
   .value-card reuses .service-card's layout but drops .glass (a moving
   backdrop-filter re-blurs every frame) and data-tilt (tilt.js/magnetic.js
   write inline style.transform, which always wins over the --act-p rule
   below and would silently freeze the fan-out animation). Solid elevated
   background instead — visually near-identical on the near-black page. */
.value-card { background: var(--c-bg-elevated); border: 1px solid var(--c-border); }

@media (prefers-reduced-motion: no-preference) {
@media (prefers-reduced-motion: no-preference) {
  .js .act--valeurs.is-live .value-card {
    --b: clamp(0, (var(--act-p, 0) - .15) / .5, 1);
  }
  .js .act--valeurs.is-live .value-card:nth-child(1) {
    transform: translate3d(calc((1 - var(--b)) * 16%), 0, 0) rotate(calc((1 - var(--b)) * -4deg));
  }
  .js .act--valeurs.is-live .value-card:nth-child(2) {
    transform: translate3d(0, calc((1 - var(--b)) * 4vh), 0) scale(calc(.96 + var(--b) * .04));
  }
  .js .act--valeurs.is-live .value-card:nth-child(3) {
    transform: translate3d(calc((1 - var(--b)) * -16%), 0, 0) rotate(calc((1 - var(--b)) * 4deg));
  }
}

/* ---------- Treatment E — staggered panel reveal (Prestations only) ----------
   First shipped as a horizontal scroll-pan (track wider than its clipping
   viewport, panned via translateX as --act-p advanced). Dropped: at the
   panel width that was actually being rendered (min(38vw,360px) x3), the
   three panels' combined width came in UNDER the real right-column viewport
   width on ordinary desktop screens (confirmed ~1112px of panels inside a
   ~1400px+ viewport at 1440px+ browser widths) — so there was nothing left
   to clip, the "pan" translated the fully-visible row into empty space, and
   the track ended up blank at p=1. A horizontal pan only makes sense when
   the track is reliably wider than its viewport; three short text/image
   cards next to a text column never reliably are. Replaced with a 3-column
   grid (always exactly fills the available width, nothing to overflow) plus
   a staggered fade/rise so scroll still drives *something* per the brief,
   without depending on a width relationship that breaks per-viewport. */
.act--pan .act__panel:nth-child(1) { --d: 0; }
.act--pan .act__panel:nth-child(2) { --d: .12; }
.act--pan .act__panel:nth-child(3) { --d: .24; }

@media (prefers-reduced-motion: no-preference) and (max-width: 899px) {
  /* Nothing animates on .act--pan below 900px (no horizontal pan), so
     pinning it would just be dead scroll — content frozen on screen for no
     reason while the user scrolls past empty space. Let it flow normally
     (both the forced min-height AND the pin must be reset together, or the
     un-pinned stage leaves a tall empty gap below its real content).
     scrollytelling.js reads this back via getComputedStyle and safely
     switches the act to "cross" progress, which nothing here reads anyway. */
  .js .act--pan {
    min-height: 0;
  }
  .js .act--pan .act__stage {
    position: static;
    height: auto;
    overflow: visible;
    padding-block: var(--space-xl);
  }
}

.act--pan .act__stage-head { max-width: 46ch; margin-bottom: var(--space-lg); }
.act--pan .act__panels {
  display: flex;
  flex-direction: column;
  gap: var(--space-md);
}
.act__panel {
  padding: var(--space-md);
  border-radius: var(--radius-md);
  background: var(--c-bg-elevated);
  border: 1px solid var(--c-border);
}
.act__panel-label { font-size: var(--fs-xs); color: var(--c-gold-300); font-weight: 700; letter-spacing: .06em; text-transform: uppercase; margin-bottom: .4em; }
.act__panel img {
  /* width:100%/height:auto is required alongside aspect-ratio: the <img>
     tags carry HTML width/height attributes (600x450, for layout-shift
     prevention pre-CSS-load) which the UA maps to a presentational height
     hint. Without an explicit height:auto here, that hint wins over
     aspect-ratio once max-width constrains the rendered width down to the
     panel's ~310px — the image stayed 450px tall (its raw attribute value)
     instead of the correct ~233px, silently overflowing the pinned stage's
     overflow:hidden box by the difference. */
  width: 100%;
  height: auto;
  border-radius: var(--radius-sm);
  aspect-ratio: 4/3;
  object-fit: cover;
  margin-bottom: var(--space-sm);
}
.act__panel ul { display: flex; flex-direction: column; gap: .5em; }
.act__panel li { display: flex; gap: .5em; font-size: var(--fs-sm); color: var(--c-text-soft); }
.act__panel li svg { width: 16px; height: 16px; flex-shrink: 0; color: var(--c-gold-300); margin-top: .2em; }

@media (min-width: 900px) {
  /* Side-by-side, not stacked: the stage-head (icon/title/meta/CTA) sits in
     a fixed-ish left column, the panel carousel gets its own column on the
     right. Earlier version stacked the head ABOVE the panels inside the
     same 100svh stage — a tall head pushed the panels below the fold and
     the stage's overflow:hidden cut them off ("cards à moitié coupées").
     Side-by-side means each column only needs to fit its own height,
     independently vertically centered. This part is unconditional (not
     gated on reduced-motion) — it's a layout choice, not an animation. */
  .js .act--pan .act__stage {
    display: grid;
    grid-template-columns: minmax(300px, 360px) 1fr;
    align-items: center;
    gap: var(--space-xl);
    padding-inline: var(--container-pad);
    padding-block: calc(var(--header-h) + var(--space-md)) var(--space-md);
  }
  .js .act--pan .act__stage-head { padding-inline: 0; margin-bottom: 0; max-width: none; }

  /* No overflow:hidden here (unlike the earlier pan version) — a 3-up grid
     always exactly fills its own column, so there's nothing to clip and
     therefore no risk of ever clipping live content, reduced-motion or not. */
  .js .act--pan .act__panels {
    display: grid;
    grid-template-columns: repeat(3, 1fr);
    gap: var(--space-md);
  }
}

/* Staggered reveal, not a pan: each panel fades/rises in on its own
   --d-delayed slice of the act's scroll progress (01 first, 03 last),
   purely opacity+transform so it stays off the compositor. Base state
   (no .is-live yet, i.e. before scrollytelling.js has run a frame) is
   untouched full-opacity/no-transform — matches this file's rule that
   every --act-p transform starts from the identity/settled state, so a
   failed JS load or reduced-motion never leaves content half-hidden.
   Deliberately NOT nested inside the min-width:900px block above (unlike
   the grid layout, which is a desktop-only layout choice): below 900px
   .act--pan is unpinned (position:static, see the max-width:899px block
   near the top of this section) but scrollytelling.js still computes
   --act-p in "cross" mode for unpinned acts, so this same reveal plays as
   the stacked cards scroll into view on mobile too — otherwise mobile lost
   all motion on this section entirely when the old horizontal-pan carousel
   was replaced by a static grid. */
@media (prefers-reduced-motion: no-preference) {
  .js .act--pan.is-live .act__panel {
    --panel-reveal: clamp(0, calc((var(--act-p, 0) - var(--d, 0)) / .3), 1);
    opacity: var(--panel-reveal);
    transform: translate3d(0, calc((1 - var(--panel-reveal)) * 28px), 0);
  }
}
