/* ============================================================================
   Tropiku · scroll choreography

   Everything here is driven by two things the page publishes itself:
     --p   the cut's progress through the viewport, 0 as it enters, 1 as it leaves
     .in   set once, when the element has entered and its media has decoded

   Nothing re-hides on the way back up: content that disappears when you scroll
   up is a defect, not an effect.
   ============================================================================ */

/* ---------- lines of type rise into place, one after the other ----------
   The line now arrives from further down and slightly wider, so the width
   axis itself is part of the entrance: the words tighten as they land. */
.ln{ display:block; overflow:hidden; }

/* El título de #c09 abre su recorte por arriba. La altura de línea de los
   títulos es menor que el cuerpo de la letra (medido: 23.31px de línea contra
   23.79px de fuente en móvil), así que `overflow:hidden` corta la tinta que
   sobresale ~2px arriba — justo donde vive el acento de una mayúscula. Sin
   esto "PRÁCTICA" se lee "PRACTICA". Y como el corte cae en una fracción de
   pixel, el acento aparecía y desaparecía entre render y render: es lo que
   hacía inestable la captura m-11 del verificador.
   `clip-path` con inset negativo arriba deja pasar esa tinta y sigue cortando
   abajo, que es por donde entran las letras deslizándose. No toca padding ni
   margen: la maquetación no se mueve.
   Se aplica solo aquí a propósito. El mismo recorte también corta el halo de
   neón de todos los títulos, y abrirlo en `.ln` movía 83 de las 140 capturas
   de la página — un cambio de aspecto general que no toca decidir desde esta
   sección. El acento de NINGÚN en #c04 sigue cortado por eso. */
#c09 .ln{ overflow:visible; clip-path:inset(-0.12em 0 0 0); }
.ln > i{
  display:block; font-style:inherit;
  transform:translate(calc(var(--from-x,0px) * 0.5), 112%) rotate(1.2deg);
  opacity:0;
  transition:
    transform 900ms var(--sc-ease-out) var(--ln-d,0ms),
    opacity   620ms var(--sc-ease-out) var(--ln-d,0ms);
}
.in .ln > i{ transform:none; opacity:1; }

/* ---------- everything else arrives under the same clock ----------
   Not always from below. A page where every element rises the same way reads
   as one gesture repeated, and the eye stops noticing it. Each cut declares
   --from-x / --from-y, so content enters from the side, from below, or from a
   diagonal, and the direction changes as you travel down the page. */
[data-rise]{
  opacity:0;
  transform:translate(var(--from-x,0px), var(--from-y,26px));
  transition:opacity 620ms var(--sc-ease-out) var(--ln-d,0ms),
             transform 760ms var(--sc-ease-out) var(--ln-d,0ms);
}
.in [data-rise]{ opacity:1; transform:none; }

/* Progressive enhancement, never a visibility dependency. The document keeps
   .no-js until motion.js finishes its synchronous setup. If scripts are
   disabled, blocked, or throw during startup, this wins over the animation
   defaults across every viewport (desktop included). `motion-pending` avoids
   a first-paint flash for a healthy deferred script; its timeout releases the
   readable fallback when that script never becomes ready. */
html.no-js:not(.motion-pending) :is(.ln > i, [data-rise]){
  opacity:1;
  transform:none;
  transition:none;
}
html.no-js:not(.motion-pending) .ficha tr{ --row:1; }
html.no-js:not(.motion-pending) .ficha td{
  clip-path:none;
  animation:none;
}
html.no-js:not(.motion-pending) .ficha td::after{
  animation:none;
  opacity:0;
}
html.no-js:not(.motion-pending) .ficha :is(th,td)::before{ transition:none; }

/* ---------- inside a parked cut, the content is DEALT OUT by scroll --------
   Entering all at once and then standing still is what makes the page feel
   like a stack of slides going past. Here each item is tied to --hold-p, the
   cut's own progress through its pause, so the reader is the one uncovering
   the section: keep scrolling and the next line arrives, from the side.

   --hold-p falls back to 1, never 0: if the loop never runs the content is
   simply all there. Content that depends on JS to become visible is the same
   trap the ficha values fell into. */
.cut.has-pin.in [data-rise]{
  --appear:clamp(0, calc((var(--hold-p, 1) - var(--seq, 0) * var(--seq-step, 0.085)) * 3.4), 1);
  opacity:var(--appear);
  transform:translate(
    calc((1 - var(--appear)) * var(--from-x, 0px) * 1.6),
    calc((1 - var(--appear)) * var(--from-y, 0px)));
  transition:none;   /* scroll IS the clock here; a duration would fight it */
}
/* the heading rides the same lateral drift, a beat ahead of its copy */
.cut.has-pin.in .copy{
  transform:translate(calc(var(--out) * -46px + (1 - clamp(0, calc(var(--hold-p, 1) * 3), 1)) * var(--from-x, 0px) * 0.5), 0);
}

/* The rhythm of directions. Cuts 1-14 in order: no two neighbours share an
   entrance, and the sides alternate so the page rocks left-right instead of
   marching straight down. */
.cut:nth-of-type(3n+1){ --from-x:-70px; --from-y:0px; }   /* in from the left  */
.cut:nth-of-type(3n+2){ --from-x: 70px; --from-y:0px; }   /* in from the right */
.cut:nth-of-type(3n+3){ --from-x:  0px; --from-y:34px; }  /* up from below     */

/* the media leans the opposite way to whatever its copy does, so the two
   halves of a cut converge instead of sliding together */
.cut .art{ --art-x:calc(var(--from-x,0px) * -1.5); }
.art img, .band img:not(.ms-sheet){ will-change:transform, clip-path; }

/* ---------- and every cut now has a DEPARTURE, not only an arrival --------
   --out is published per cut by motion.js: 0 while the cut owns the screen,
   1 by the time it has left through the top. Because it is a pure function of
   scroll position, going back up runs it in reverse and nothing is ever lost.
   The copy lifts and thins; the media lags slightly behind it, which is what
   makes the two read as separate planes rather than one sheet. */
.cut{ --out:0; }
.cut .copy{
  opacity:calc(1 - var(--out) * 0.5);
  transform:translateY(calc(var(--out) * -46px));
}
/* The hero is the only copy plane that has to survive a complete document
   round-trip beneath the fixed glass bar and multiple Music ON blend layers.
   Keep it promoted without asking every chapter to consume a GPU layer. */
#c01 .copy{
  transform:translate3d(0, calc(var(--out) * -46px), 0);
  will-change:transform,opacity;
  backface-visibility:hidden;
}
.cut .art, .cut .band{
  opacity:calc(1 - var(--out) * 0.4);
}
/* the accent word holds on a beat longer than the line it sits in — the last
   thing you see leaving a cut is the word the cut was about */
.cut h2 em, .cut h3 em{
  opacity:calc(1 - var(--out) * 0.3);
}

/* ---------- media arrives as a wipe, edge to edge ----------
   The wipe now runs ACROSS the frame rather than down it, and alternates
   direction cut to cut so the page does not develop a tic. It waits for the
   bitmap to decode, so a slow image is never a flash of nothing then a bang. */
.art, .band{ overflow:hidden; }
.art img, .band img:not(.ms-sheet){
  clip-path:inset(0 100% 0 0);
  transform:scale(1.08);
  transition:clip-path 1200ms var(--sc-ease-out),
             transform 1500ms var(--sc-ease-out);
}
/* Every second cut wipes the other way. The :not(.in) is load-bearing: without
   it this rule outranks the reveal below it and the image never opens at all —
   it cost six cuts their photograph. A direction is a STARTING state, so it
   has to stop applying the moment the cut has entered. */
.cut:nth-of-type(even):not(.in) .art img,
.cut:nth-of-type(even):not(.in) .band img:not(.ms-sheet){ clip-path:inset(0 0 0 100%); }
.in .art img, .in .band img:not(.ms-sheet){ clip-path:inset(0 0 0 0); transform:scale(1); }

/* ---------- parallax: the art travels against the copy ---------- */
.art, .band{ --par:0px; }
.art img, .band img:not(.ms-sheet){ will-change:transform, clip-path; }
.in .art img{ transform:translate3d(0, var(--par), 0) scale(1.02); }
.in .band img:not(.ms-sheet){ transform:translate3d(0, var(--par), 0) scale(1.06); }

/* ---------- the neon seam between cuts ---------- */
.cut + .cut::before{
  content:""; position:absolute; inset:0 0 auto 0; height:1px; z-index:8;
  background:linear-gradient(90deg,
    transparent 0%, var(--a) 18%, var(--a2) 52%, transparent 92%);
  transform:scaleX(var(--seam,0)); transform-origin:0 50%;
  transition:transform 1200ms var(--sc-ease-out);
  opacity:.9;
}
.cut.in + .cut::before, .cut.in::before{ --seam:1; }

/* ---------- the label's neon rule ---------- */
.lbl{ display:flex; align-items:center; gap:.7rem; }
.lbl::before{
  content:""; height:1px; width:0; background:var(--a);
  box-shadow:0 0 6px var(--a);
  transition:width 900ms var(--sc-ease-out) var(--ln-d,0ms);
}
.in .lbl::before{ width:2.2rem; }

/* ---------- ficha rows draw their rule as they arrive ---------- */
.ficha tr{ --row:0; }
.ficha th, .ficha td{ border-top:0; position:relative; }
.ficha th::before{
  content:""; position:absolute; left:0; top:0; height:1px; width:100%;
  background:var(--h); transform:scaleX(var(--row)); transform-origin:0 50%;
  transition:transform 700ms var(--sc-ease-out) var(--row-d,0ms);
}
.ficha td::before{
  content:""; position:absolute; left:0; top:0; height:1px; width:100%;
  background:var(--h); transform:scaleX(var(--row)); transform-origin:0 50%;
  transition:transform 700ms var(--sc-ease-out) var(--row-d,0ms);
}
.in .ficha tr{ --row:1; }

/* ---------- the data types itself out ----------
   The page's whole argument is that it MEASURES. Numbers that fade in are
   decoration; numbers that type out read as a readout being taken.

   Done with a stepped clip rather than by rewriting the text, so the string
   is never actually broken: screen readers get the whole value at once, and
   copy-paste keeps working. steps() is what makes it tick instead of slide. */
/* The resting state is VISIBLE. Hiding the value in CSS and relying on a class
   to reveal it means any failure — the observer never firing, a fast scroll
   past, JS erroring earlier on the page — leaves the data permanently blank.
   The animation supplies its own hidden start, so the effect is identical and
   the failure mode is "no animation" instead of "no content". */
.ficha td{ clip-path:none; }
.in .ficha td{
  animation:tick 620ms steps(18,end) var(--row-d,0ms) both;
}
@keyframes tick{ from{ clip-path:inset(0 100% 0 0); } to{ clip-path:inset(0 0 0 0); } }

/* the caret that runs ahead of the value while it is being typed */
.ficha td{ position:relative; }
.ficha td::after{
  content:""; position:absolute; right:-.42em; top:.12em; bottom:.12em; width:.09em;
  background:var(--a2); opacity:0;
}
.in .ficha td::after{ animation:caret 620ms steps(2,end) var(--row-d,0ms); }
@keyframes caret{ 0%,90%{ opacity:.9; } 100%{ opacity:0; } }

/* the accent word inside a heading picks up the second neon */
.cut h2 em, .cut h3 em, .hero h1 em{
  font-style:italic; color:var(--a2);
  text-shadow:0 0 18px color-mix(in srgb, var(--a2) 42%, transparent);
}

@media (prefers-reduced-motion:reduce){
  /* the pan, the bubble and the glow all sit at rest */
  .cut .band, .cut .art{ --pan:0.5 !important; --blow:0 !important; }
  #c15 .band{ transform:none !important; }
  .cut{ --glow:0 !important; }
  /* no exit either: --out is ignored, so nothing moves or thins on the way past */
  .cut .copy, .cut .art, .cut .band, .cut h2 em, .cut h3 em{
    opacity:1; transform:none; }
  .cut{ --from-x:0px !important; --from-y:0px !important; }
  .cut.has-pin.in [data-rise]{ --appear:1 !important; opacity:1; transform:none; }
  .cut.has-pin.in .copy{ transform:none; }
  .ln > i, [data-rise]{ transform:none; opacity:1; transition:opacity 240ms linear; }
  .in .ln > i, .in [data-rise]{ opacity:1; }
  .art img, .band img:not(.ms-sheet){ clip-path:none; transform:none; transition:opacity 240ms linear; }
  .in .art img, .in .band img:not(.ms-sheet){ transform:none; }
  .cut + .cut::before{ transform:scaleX(1); transition:none; }
  .lbl::before{ width:2.2rem; transition:none; }
  .ficha tr{ --row:1; }
  /* no typing, no caret: the values are simply there */
  .ficha td, .in .ficha td{ clip-path:none; animation:none; }
  .ficha td::after, .in .ficha td::after{ animation:none; opacity:0; }
  .ficha th::before, .ficha td::before{ transition:none; }
}

/* ---------- audited mobile repairs: scoped, desktop untouched ---------- */
@media (max-width:820px){
  /* c02: the whole pin is taller than a phone because it carries both the
     reading copy and the figure. Pinning that box cannot work: it has no
     runway. The copy fits, so it owns the reading stage; the figure follows
     after that stage releases. */
  #c02{ --from-x:0px; --hold:70svh; }
  /* A reading stage must never wait on the desktop line entrance. Fast scroll
     can otherwise leave the heading half-raised while its lead is already in. */
  #c02 .ln > i{ transform:none; opacity:1; transition:none; }
  #c02 .ficha{ table-layout:fixed; }
  #c02 .ficha th{ width:32%; }
  #c02 .ficha td, #c02 .ficha td .hi{ min-width:0; overflow-wrap:anywhere; }
  #c02 .ficha td, #c02.in .ficha td{ clip-path:none; animation:none; }
  #c02 .ficha td::after, #c02.in .ficha td::after{ animation:none; opacity:0; }

  /* Reading belongs to a stage, never to the whole mobile pin. The desktop
     grids dissolve their .mobile-read-stage wrappers; phones give each one its
     own runway, then release the following photograph instead of covering it.
     #c09 va aparte: desde que abre con el reel a pantalla completa su texto
     dejó de ser hijo directo de la sección y vive en .hold-stage, igual que
     el de #c12. Sin esta ruta extra el teléfono perdería toda la etapa de
     lectura de esa sección sin que nada falle a la vista. */
  :is(#c02,#c03,#c04,#c05,#c06,#c11) > .pin,
  /* El `display:block` que apila esto en el teléfono NO puede vivir aquí:
     tendría la misma especificidad que el `display:grid` de escritorio y este
     archivo se carga antes que los estilos del propio index.html, así que
     perdería sin que nada falle a la vista. Vive junto a la regla de
     escritorio, en index.html. */
  #c09 .hold-stage > .pin{
    position:relative; top:auto; min-height:0; padding-top:0;
  }
  :is(#c02,#c03,#c04,#c05,#c06) > .pin > .mobile-read-stage,
  #c11 .method__stages > .mobile-read-stage,
  #c09 .hold-stage > .pin > .mobile-read-stage,
  #c15 .close__grid > .mobile-read-stage{
    --mobile-dwell:44svh;
    display:block; position:relative; overflow:clip;
    min-height:calc(100svh - var(--bar-h) + var(--mobile-dwell));
    isolation:isolate;
  }
  #c02 > .pin > .mobile-read-stage{ --mobile-dwell:70svh; }
  :is(#c02,#c03,#c04,#c05,#c06) > .pin > .mobile-read-stage > .copy,
  #c11 .method__stages > .mobile-read-stage > .copy,
  #c09 .hold-stage > .pin > .mobile-read-stage > .copy,
  #c15 .close__grid > .mobile-read-stage > :is(.copy,.form){
    position:sticky; top:var(--bar-h);
    min-height:calc(100svh - var(--bar-h)); box-sizing:border-box;
    isolation:isolate; z-index:5;
  }
  /* The new functional/body-aware reading cuts use the same visible-bottom
     reserve as prices and locations. The runway lives on the stage; this only
     keeps copy out of the final 24px plus the observed scroll drift. */
  :is(#c03,#c04,#c05) > .pin > .mobile-read-stage > .copy,
  #c11 .method__stages > .mobile-read-stage > .copy{
    --mobile-read-safety:3.75rem;
    min-height:calc(100svh - var(--bar-h) - var(--mobile-read-safety));
    display:flex; flex-direction:column; justify-content:center;
  }
  :is(#c02,#c04,#c06,#c09,#c11,#c15) .mobile-read-stage{
    background-image:
      url("assets/web/papel.webp"),
      radial-gradient(120% 70% at 50% -8%, rgba(255,255,255,.05) 0%, rgba(255,255,255,0) 62%),
      linear-gradient(172deg,#201B14 0%,#161310 58%,#1C1712 100%);
    background-size:768px 768px, auto, auto;
    background-position:var(--papel-pos, 0 0), center, center;
    background-repeat:repeat, no-repeat, no-repeat;
    background-blend-mode:soft-light, normal, normal;
  }
  :is(#c03,#c05) .mobile-read-stage{ background:linear-gradient(172deg,#101218 0%,#08090C 58%,#0B0C11 100%); }
  /* El stop tiene que ser legible aunque el observer de la coreografía llegue
     tarde: estas etapas ya controlan su propio scroll, así que no dependen de
     que la sección reciba .in para revelar copy, ticks o precios. */
  :is(#c02,#c03,#c04,#c05,#c06,#c09,#c10,#c11,#c12,#c15) .mobile-read-stage .copy,
  :is(#c02,#c03,#c04,#c05,#c06,#c09,#c10,#c11,#c12,#c15) .mobile-read-stage .form,
  :is(#c02,#c03,#c04,#c05,#c06,#c09,#c10,#c11,#c12,#c15) .mobile-read-stage [data-rise],
  :is(#c02,#c03,#c04,#c05,#c06,#c09,#c10,#c11,#c12,#c15) .mobile-read-stage .ln > i{
    opacity:1; transform:none; transition:none;
  }
  :is(#c02,#c03,#c04,#c05,#c06,#c09,#c10,#c11,#c12,#c15) .mobile-read-stage .ficha td,
  :is(#c02,#c03,#c04,#c05,#c06,#c09,#c10,#c11,#c12,#c15) .mobile-read-stage.in .ficha td{
    clip-path:none; animation:none;
  }
  :is(#c02,#c03,#c04,#c05,#c06,#c09,#c10,#c11,#c12,#c15) .mobile-read-stage .ficha td::after,
  :is(#c02,#c03,#c04,#c05,#c06,#c09,#c10,#c11,#c12,#c15) .mobile-read-stage.in .ficha td::after{
    animation:none; opacity:0;
  }
  #c05 .ticks li{ opacity:1; transform:none; }
  #c15 .close__grid{ display:block; }
  #c15 .close__grid > .mobile-read-stage > .form{ width:100%; max-width:27rem; }

  /* c10 y el texto de c12 también se leen por etapas. Un solo pin alto no
     funciona: c10 contiene dos ofertas y c12 dos bloques de contenido; el
     segundo entraba mientras el primero todavía era legible. Cada stage toma
     un runway de 44svh y su copy queda estacionada debajo de la barra. */
  #c10{ --from-x:0px; min-height:0; }
  #c10 > .pin{ position:relative; top:auto; min-height:0; display:block; }
  #c10 .mobile-read-stage,
  #c12 .hold-stage .mobile-read-stage{
    --mobile-dwell:44svh;
    display:block; position:relative; overflow:clip;
    min-height:calc(100svh - var(--bar-h) + var(--mobile-dwell));
    isolation:isolate;
  }
  #c10 .mobile-read-stage > .copy,
  #c12 .hold-stage .mobile-read-stage > .copy{
    position:sticky; top:var(--bar-h);
    /* Reserva 24px de lectura y el drift máximo observado (≈35px) en Chrome
       Headless Shell sin motion. El runway sigue en la etapa, no aquí. */
    --mobile-read-safety:3.75rem;
    min-height:calc(100svh - var(--bar-h) - var(--mobile-read-safety)); box-sizing:border-box;
    display:flex; flex-direction:column; justify-content:center;
    isolation:isolate; z-index:5;
  }
  /* La primera etapa de precios cabe por nueve píxeles en la vista de 581px
     de ancho: esta respiración sigue siendo generosa, pero no deja la última
     regla de la tabla bajo el borde inferior. */
  #c10 .mobile-read-stage > .copy{ padding-block:clamp(1rem,3vw,1.25rem); }
  #c10 .price-plan{ margin-top:var(--sc-2); }

  /* Estas reglas base viven dentro del <style> del documento, después de
     motion.css, con la misma especificidad que la ruta desktop. !important es
     deliberado y acotado al móvil: sin él el pin de video gana por orden de
     carga y vuelve a recortar las etapas de lectura de c09 y c12. */
  #c09 .hold-stage,
  #c12 .hold-stage{ height:auto !important; }
  #c09 .hold-stage > .pin,
  #c12 .hold-stage > .pin{
    position:relative !important; top:auto !important;
    min-height:0 !important; height:auto !important;
    overflow:visible !important; display:block !important;
  }

  /* c15: preserve the full 16:9 composition rather than travelling through a portrait crop. */
  #c15 .pin-stage, #c15 .pin{ position:relative; top:auto; height:auto; min-height:0; }
  #c15 .band{ position:relative; inset:auto; height:auto; aspect-ratio:16 / 9; }
  #c15 .band img{ width:100%; height:100%; min-height:0; object-fit:contain; object-position:center; transform:none !important; }

  /* FAQ is also reading, not a long accidental scroll. The heading and each
     coherent question group get their own release, including the expanded
     biohacking answer. Desktop keeps one continuous answer column below. */
  #c14{ --from-x:0px; min-height:0; }
  #c14 > .pin{ position:relative; top:auto; min-height:0; display:block; }
  #c14 .faq__stages{ display:block; }
  #c14 .mobile-read-stage{
    --mobile-dwell:44svh;
    display:block; position:relative; overflow:clip;
    min-height:calc(100svh - var(--bar-h) + var(--mobile-dwell));
    isolation:isolate;
    background:linear-gradient(172deg,#101218 0%,#08090C 58%,#0B0C11 100%);
  }
  #c14 .mobile-read-stage > .copy{
    --mobile-read-safety:3.75rem;
    position:sticky; top:var(--bar-h);
    min-height:calc(100svh - var(--bar-h) - var(--mobile-read-safety));
    box-sizing:border-box; display:flex; flex-direction:column; justify-content:center;
    isolation:isolate; z-index:5;
  }
  #c14 .faq__stage-copy{ display:flex; justify-content:flex-start;
    padding-block:clamp(1rem,3vw,1.25rem); }
  #c14 .mobile-read-stage .copy,
  #c14 .mobile-read-stage [data-rise],
  #c14 .mobile-read-stage .ln > i{
    opacity:1; transform:none; transition:none;
  }
}

/* These wrappers exist only to split phone reading. Their contents return to
   the original single FAQ column above the mobile breakpoint. */
.faq__stages{ display:flex; flex-direction:column; gap:0; }
.faq__stages .faq__stage-copy{ display:contents; padding:0; }
.method__stages{ position:relative; z-index:4; padding:clamp(1.5rem,4.5vw,3.6rem);
  display:flex; flex-direction:column; justify-content:center; }
.method__stages .copy{ display:contents; padding:0; }

@media (max-width:820px){
  #c11 .method__stages{ display:block; padding:0; }
  #c11 .method__stages > .mobile-read-stage > .copy{
    padding:clamp(1.5rem,4.5vw,3.6rem);
  }
}

@media (prefers-reduced-motion:reduce){
  .btn, .bar__cta{ transition:none; }
}
