/* rail-kit motion — the shared animation layer.
 *
 * Promoted from danielzea-site, where these lived as local components
 * (`foco.css`, `brands.css`, `velo.css`, `entrada.css`) and could not be used
 * by any other brand. Nothing here names a colour or a typeface; every value
 * reads from a token with a neutral fallback, the same contract as rail.css.
 *
 * Import it separately from rail.css — a site that wants the cards and not
 * the motion should not pay for the motion:
 *
 *   import "@/vendor/rail-kit/rail.css";
 *   import "@/vendor/rail-kit/motion.css";
 *
 * EVERYTHING here sits inside `prefers-reduced-motion: no-preference`. There
 * is no reduce block switching things off, because nothing is switched on
 * without the query in the first place — which is the stronger version of the
 * same promise: a visitor who asked for less movement never gets the styles
 * at all, not even for one frame.
 */

/* ---------------------------------------------------------------------------
 * SPOTLIGHT — a light travelling around the edge of the card in focus.
 *
 * ON THE ANGLE: a plain custom property is a string, and a string cannot be
 * animated. Typed with @property as an <angle> it can, and the compositor
 * does it. Where @property does not exist the cone is painted still, which is
 * a fine floor: the ring still marks the card.
 *
 * ON THE FLOOR COLOUR, which is the part that took danielzea three tries: the
 * cone's floor was at 6% white — black, in practice. With the ring dark
 * everywhere except the tip of light, all you could see was that tip, and in
 * a still screenshot the tip is always in ONE place, so the card looked like
 * it had a glow on one border even though the light was going round. The
 * floor is the accent now and the white tip travels over it: all four sides
 * read at all times, and you can still see which one the light is passing.
 *
 * `linear`, not `ease-in-out`: a light running around a border does not
 * accelerate and brake once per lap. That reads as a heartbeat.
 * ------------------------------------------------------------------------- */

@property --rail-spotlight-angle {
  syntax: "<angle>";
  initial-value: 0deg;
  inherits: false;
}

[data-rail-spotlight] {
  position: relative;
}

[data-rail-spotlight]::before {
  content: "";
  position: absolute;
  inset: 0;
  z-index: 3;
  border-radius: inherit;
  padding: var(--rail-spotlight-width, 1.5px);
  background-image: conic-gradient(
    from var(--rail-spotlight-angle) at 50% 50%,
    color-mix(in srgb, var(--rail-spotlight-color, var(--_accent, currentColor)) 34%, transparent) 0deg,
    color-mix(in srgb, var(--rail-spotlight-light, #fff) 30%, transparent) 30deg,
    color-mix(in srgb, var(--rail-spotlight-light, #fff) 55%, transparent) 62deg,
    var(--rail-spotlight-light, #fff) 88deg,
    var(--rail-spotlight-color, var(--_accent, currentColor)) 104deg,
    color-mix(in srgb, var(--rail-spotlight-color, var(--_accent, currentColor)) 82%, transparent) 132deg,
    color-mix(in srgb, var(--rail-spotlight-light, #fff) 34%, transparent) 176deg,
    color-mix(in srgb, var(--rail-spotlight-color, var(--_accent, currentColor)) 30%, transparent) 250deg,
    color-mix(in srgb, var(--rail-spotlight-color, var(--_accent, currentColor)) 34%, transparent) 360deg
  );
  -webkit-mask: linear-gradient(#000 0 0) content-box, linear-gradient(#000 0 0);
  mask: linear-gradient(#000 0 0) content-box, linear-gradient(#000 0 0);
  -webkit-mask-composite: xor;
  mask-composite: exclude;
  pointer-events: none;
}

@media (prefers-reduced-motion: no-preference) {
  [data-rail-spotlight]::before {
    animation: rail-spotlight var(--rail-spotlight-duration, 5.4s) linear
      var(--rail-spotlight-iterations, 3);
  }
}

@keyframes rail-spotlight {
  to { --rail-spotlight-angle: 360deg; }
}

/* ---------------------------------------------------------------------------
 * SHIMMER — a sweep across a surface, for the one suggested card.
 *
 * It animates `background-position`, not the gradient. A gradient recomputed
 * every frame repaints the whole layer; a position shifted over a canvas
 * bigger than the box is moved by the compositor. That is what the 240% is
 * for: there has to be canvas to travel across.
 * ------------------------------------------------------------------------- */

[data-rail-shimmer] {
  background-image: linear-gradient(
    115deg,
    transparent 30%,
    color-mix(in srgb, var(--rail-shimmer, currentColor) 22%, transparent) 48%,
    transparent 66%
  );
  background-size: 240% 240%;
}

@media (prefers-reduced-motion: no-preference) {
  [data-rail-shimmer] {
    animation: rail-shimmer var(--rail-shimmer-duration, 5.4s) ease-in-out
      var(--rail-shimmer-iterations, 3);
  }
}

@keyframes rail-shimmer {
  0% { background-position: 0% 0%; }
  50% { background-position: 100% 100%; }
  100% { background-position: 0% 0%; }
}

/* ---------------------------------------------------------------------------
 * REVEAL — a block arrives as it enters the viewport.
 *
 * The kit only ships the two states. WHEN `is-in` goes on is the site's
 * business, because it is the site that owns the IntersectionObserver, the
 * routing and the hydration timing.
 *
 * The `.rail-anim` gate matters: without it, a visitor whose JavaScript never
 * runs gets a page of permanently invisible blocks. The site adds `rail-anim`
 * to <html> from script, so no-JS falls back to everything visible — which is
 * the correct failure, not a blank page.
 * ------------------------------------------------------------------------- */

@media (prefers-reduced-motion: no-preference) {
  .rail-anim .rail-reveal {
    opacity: 0;
    transform: translateY(var(--rail-reveal-distance, 22px));
    transition:
      opacity var(--rail-reveal-duration, 0.9s) ease,
      transform var(--rail-reveal-duration, 0.9s) cubic-bezier(0.2, 0.7, 0.2, 1);
  }

  .rail-anim .rail-reveal.is-in {
    opacity: 1;
    transform: none;
  }
}

/* ---------------------------------------------------------------------------
 * TILT — a surface that obeys the pointer, so a card reads as an object in the
 * hand rather than a picture of one.
 *
 * Promoted from danielzea-site, where it was written twice in three days: once
 * for a digital business card and again, from scratch, for a pricing card in
 * another stylesheet. The second time was the tell — the brief was «make it
 * tactile», and the answer was a component, not a page.
 *
 * WHAT THE KIT SHIPS AND WHAT IT DOES NOT. The kit ships the states and the
 * geometry; the SITE writes the two angles, because only the site knows which
 * element is being pointed at and how it wants to sample the pointer. This is
 * the same split as REVEAL above and for the same reason: the observer, the
 * routing and the hydration timing are the site's business. The contract is
 * four custom properties and two classes:
 *
 *   on pointermove, inside ONE requestAnimationFrame per frame:
 *     --rail-tilt-x / --rail-tilt-y   the two angles, in deg
 *     --rail-tilt-lx / --rail-tilt-ly the sheen's position, in %
 *   on enter/leave:  .is-tilting      on press:  .is-pressed
 *
 * ON THE CEILING, which is a design decision and not a detail: the angles are
 * the site's to cap, and the honest cap depends on what the card carries. The
 * card that IS the gesture — a contact card someone opens from a QR — can
 * afford five degrees. A card with a PRICE on it cannot: a tariff that sways
 * reads as a banner, and that costs credit exactly where credit is being
 * asked for. Three degrees there. A kit that hard-coded one number would have
 * been wrong for one of the two.
 *
 * ON THE DEPTH. The tilt alone reads as a skewed rectangle. What makes it read
 * as an object with thickness is a few children coming FORWARD in Z while it
 * leans — the badge, the headline figure, the button. That is
 * `[data-rail-tilt-lift]`, and it needs `preserve-3d` on the card, which is
 * why the card sets it unconditionally and not only while tilting: flipping
 * `transform-style` mid-gesture re-rasterises every layer.
 *
 * ON TOUCH. The tilt is pointer-only by contract — a finger COVERS the card it
 * is tilting, so the effect is work a phone pays for in battery and nobody
 * sees. The press is not: without a pointer it is the only answer the surface
 * can give, so `.is-pressed` is the part that must survive on a phone.
 * ------------------------------------------------------------------------- */

/* The scene. Perspective belongs to the PARENT: set on the card itself, each
   card gets its own vanishing point and a row of them leans like a row of
   separate objects instead of one surface seen from one place. */
[data-rail-tilt-scene] {
  perspective: var(--rail-tilt-perspective, 1600px);
}

[data-rail-tilt] {
  position: relative;
  transform-style: preserve-3d;
}

/* The specular sheen, painted by the card's own background so the component
   costs no extra element and cannot fight a border or a ring drawn on ::before
   or ::after — both of which are already spoken for in this file. */
[data-rail-tilt-sheen] {
  background-image: radial-gradient(
    var(--rail-tilt-sheen-size, 40% 50%) at var(--rail-tilt-lx, 50%) var(--rail-tilt-ly, 0%),
    color-mix(in srgb, var(--rail-tilt-sheen-light, #fff) 8%, transparent) 0%,
    color-mix(in srgb, var(--rail-tilt-sheen-color, var(--_accent, currentColor)) 8%, transparent) 40%,
    transparent 74%
  );
  background-repeat: no-repeat;
  background-size: 100% 100%;
}

@media (prefers-reduced-motion: no-preference) {
  [data-rail-tilt] {
    transform: rotateX(var(--rail-tilt-x, 0deg)) rotateY(var(--rail-tilt-y, 0deg));
    /* Slow on the way back to flat, quick while the pointer is on it: a
       surface that lags the finger does not feel like a surface. */
    transition:
      transform var(--rail-tilt-return, 0.5s) cubic-bezier(0.2, 0.7, 0.2, 1),
      box-shadow var(--rail-tilt-return, 0.5s) ease;
  }

  [data-rail-tilt].is-tilting {
    transition:
      transform var(--rail-tilt-follow, 0.12s) linear,
      box-shadow var(--rail-tilt-return, 0.5s) ease;
  }

  [data-rail-tilt-lift] {
    transition: transform var(--rail-tilt-return, 0.5s) cubic-bezier(0.2, 0.7, 0.2, 1);
  }

  [data-rail-tilt].is-tilting [data-rail-tilt-lift] {
    transform: translateZ(var(--rail-tilt-lift, 20px));
  }

  /* The press keeps the current angles: dropping them here makes the card
     snap flat under the finger, which reads as the gesture being rejected. */
  [data-rail-tilt].is-pressed {
    transform: scale(var(--rail-tilt-press, 0.99))
      rotateX(var(--rail-tilt-x, 0deg)) rotateY(var(--rail-tilt-y, 0deg));
    transition: transform var(--rail-tilt-press-duration, 0.18s)
      cubic-bezier(0.2, 0.7, 0.2, 1);
  }
}

/* ---------------------------------------------------------------------------
 * VEIL — the plane that covers a page change, with the brand's mark on it.
 *
 * THE HELD BEAT AT THE START IS NOT PADDING. Without it the mark appears and
 * is already leaving: there is no time to read it, and then there was no
 * point putting it there. 0.22s still is enough to register it; the rest is
 * the dissolve.
 *
 * 0.8s in total and not 1.05: a long dissolve reads as the page being slow. A
 * curtain can afford time because the movement entertains; a fade cannot.
 *
 * The mark grows 3.5% as it leaves. That is what makes it read as dissolving
 * into light rather than as somebody turning its brightness down.
 *
 * Both live on <html>, not on an element in the document, for the same
 * reason: they have to exist BEFORE the new page's first frame, and a script
 * at the end of <body> arrives too late.
 * ------------------------------------------------------------------------- */

@media (prefers-reduced-motion: no-preference) {
  html.rail-veil::after {
    content: "";
    position: fixed;
    inset: 0;
    z-index: 90;
    background: var(--rail-veil-bg, Canvas);
    pointer-events: none;
    animation: rail-veil-dissolve var(--rail-veil-duration, 0.8s) ease-out forwards;
  }

  /* The mark is the site's own: pass it as a string in --rail-veil-mark, or
     leave it out and only the plane fades. */
  html.rail-veil::before {
    content: var(--rail-veil-mark, "");
    position: fixed;
    inset: 0;
    margin: auto;
    z-index: 91;
    width: max-content;
    height: max-content;
    pointer-events: none;
    font-family: var(--rail-veil-mark-font, inherit);
    font-size: var(--rail-veil-mark-size, clamp(21px, 2.6vw, 27px));
    /* The tracking leaves a gap after the last letter, so a rule under the
       mark sits off to the right. A matching text-indent splits it evenly. */
    letter-spacing: var(--rail-veil-mark-tracking, 0.3em);
    text-indent: var(--rail-veil-mark-tracking, 0.3em);
    line-height: 1;
    color: var(--rail-veil-mark-color, CanvasText);
    animation: rail-veil-mark var(--rail-veil-duration, 0.8s) ease-out forwards;
  }
}

@keyframes rail-veil-dissolve {
  0%, 28% { opacity: 1; }
  100% { opacity: 0; visibility: hidden; }
}

@keyframes rail-veil-mark {
  0%, 28% { opacity: 1; transform: scale(1); }
  100% { opacity: 0; transform: scale(1.035); visibility: hidden; }
}

/* ---------------------------------------------------------------------------
   EL BRILLO. `[data-rail-glow]`.
 *
 * Un halo alrededor de una pieza que importa: la carta que acabas de ganar, la
 * jugada que cambio el marcador.
 *
 * VA EN `box-shadow`, NO en un pseudo-elemento. `::before` ya es del
 * spotlight y `::after` del canto del vidrio: un tercer inquilino no gana "a
 * medias", se mezclan propiedad por propiedad y sale algo que no se parece a
 * ninguno de los tres. Con box-shadow el brillo convive con cualquiera.
 *
 * NO RECORTA. Un halo dibujado por fuera muere dentro de un `overflow:hidden`,
 * asi que se pone en el elemento que ENVUELVE a la tarjeta, nunca en la
 * superficie recortada.
 *
 * EL LATIDO ES OPCIONAL Y NUNCA ES EL DATO. `[data-rail-glow="pulse"]` respira;
 * con motion reducido el halo se queda quieto pero SIGUE AHI. Apagar el
 * movimiento no puede apagar la informacion, y si el halo fuera el unico modo
 * de saber que algo es especial, ya estaria mal antes de llegar aqui.
 * --------------------------------------------------------------------------- */
[data-rail-glow] {
  --_glow-color: var(--rail-glow-color, currentColor);
  --_glow-spread: var(--rail-glow-spread, 22px);
  box-shadow:
    0 0 var(--_glow-spread) calc(var(--_glow-spread) / -3) var(--_glow-color),
    0 0 calc(var(--_glow-spread) * 2.2) calc(var(--_glow-spread) / -2) var(--_glow-color);
}

[data-rail-glow="soft"] {
  --_glow-spread: var(--rail-glow-spread-soft, 14px);
}

[data-rail-glow="strong"] {
  --_glow-spread: var(--rail-glow-spread-strong, 34px);
}

@media (prefers-reduced-motion: no-preference) {
  [data-rail-glow="pulse"] {
    animation: rail-glow-breathe var(--rail-glow-beat, 2.6s) ease-in-out infinite;
  }
}

@keyframes rail-glow-breathe {
  0%, 100% { box-shadow: 0 0 var(--_glow-spread) calc(var(--_glow-spread) / -3) var(--_glow-color); }
  50% {
    box-shadow:
      0 0 calc(var(--_glow-spread) * 1.6) calc(var(--_glow-spread) / -4) var(--_glow-color),
      0 0 calc(var(--_glow-spread) * 3) calc(var(--_glow-spread) / -2) var(--_glow-color);
  }
}

/* ---------------------------------------------------------------------------
 * DECISION EXITS — a card leaving in the direction you decided.
 *
 * Promoted from the Christian Castiblanco postcard deck, where they were local
 * and could not be used by another brand. See docs/MAGAZINE-LANE.md.
 *
 * TWO EXITS, NOT ONE. A deck driven by both a button and a drag needs two, and
 * the reason is the starting position. A button press starts at rest, so it can
 * afford to hold still for a beat while the stamp registers — that hold is what
 * tells you which way you chose. A drag has already travelled; holding it at
 * neutral means teleporting the card back under the finger that just moved it,
 * waiting, and only then leaving. That shipped, and it read as a bug.
 *
 * The drag variants therefore start from --rail-drag-x / --rail-drag-angle, the
 * live offset, and never return to zero. Set them while dragging and do NOT
 * clear them before adding the exit class — the animation reads them on the
 * first frame.
 */
@media (prefers-reduced-motion: no-preference) {
  .rail-decide { transition: transform .28s ease, opacity .28s ease; }

  /* Dragging: no transition, the pointer is the clock. */
  .rail-decide.is-dragging {
    transform: translateX(var(--rail-drag-x, 0px)) rotate(var(--rail-drag-angle, 0deg));
    transition: none;
  }

  /* Tap path: hold, then go. The hold is the point. */
  .rail-decide.is-keeping { animation: rail-decide-keep .9s ease-in-out both; }
  .rail-decide.is-passing { animation: rail-decide-pass .9s ease-in-out both; }

  /* Drag path: continue, never return. */
  .rail-decide.from-drag.is-keeping { animation: rail-decide-keep-drag .42s cubic-bezier(.32,.72,.4,1) both; }
  .rail-decide.from-drag.is-passing { animation: rail-decide-pass-drag .42s cubic-bezier(.32,.72,.4,1) both; }

  @keyframes rail-decide-keep {
    0%, 40% { transform: rotate(var(--rail-decide-rest, -1deg)); opacity: 1; }
    100%    { transform: translateX(var(--rail-decide-travel, 150px)) rotate(13deg); opacity: 0; }
  }
  @keyframes rail-decide-pass {
    0%, 40% { transform: rotate(var(--rail-decide-rest, -1deg)); opacity: 1; }
    100%    { transform: translateX(calc(var(--rail-decide-travel, 150px) * -1)) rotate(-13deg); opacity: 0; }
  }
  @keyframes rail-decide-keep-drag {
    from { transform: translateX(var(--rail-drag-x, 0px)) rotate(var(--rail-drag-angle, 0deg)); opacity: 1; }
    to   { transform: translateX(var(--rail-decide-fling, 260px)) rotate(13deg); opacity: 0; }
  }
  @keyframes rail-decide-pass-drag {
    from { transform: translateX(var(--rail-drag-x, 0px)) rotate(var(--rail-drag-angle, 0deg)); opacity: 1; }
    to   { transform: translateX(calc(var(--rail-decide-fling, 260px) * -1)) rotate(-13deg); opacity: 0; }
  }
}

/* ---------------------------------------------------------------------------
 * VINYL — rotation bound to media state, never to a timer.
 *
 * The disc turns because the audio element says it is playing. Drive it from
 * the `play`, `pause` and `ended` events and let the animation-play-state do
 * the work; a setInterval drifts away from the sound within a minute and the
 * drift is the thing people notice without being able to name it.
 *
 *   audio.addEventListener('play',  () => deck.dataset.spinning = 'true');
 *   audio.addEventListener('pause', () => delete deck.dataset.spinning);
 */
@media (prefers-reduced-motion: no-preference) {
  .rail-vinyl { animation: rail-vinyl 3.4s linear infinite; animation-play-state: paused; }
  [data-spinning] .rail-vinyl, .rail-vinyl[data-spinning] { animation-play-state: running; }
  @keyframes rail-vinyl { to { transform: rotate(360deg); } }

  /* The arm swings in and stays; it is a position, not a loop. */
  .rail-tonearm { transition: transform .5s cubic-bezier(.2,.8,.2,1); transform: rotate(var(--rail-tonearm-rest, -16deg)); transform-origin: 80% 16%; }
  [data-spinning] .rail-tonearm, .rail-tonearm[data-spinning] { transform: rotate(var(--rail-tonearm-play, 0deg)); }
}

/* ---------------------------------------------------------------------------
 * GLINT — a finite pass across an action that just became relevant.
 *
 * Three passes, then it stops. Finite is the whole point: a shimmer that loops
 * is read as a loading state, and a visitor waits for it instead of pressing it.
 */
@media (prefers-reduced-motion: no-preference) {
  .rail-glint { position: relative; overflow: hidden; }
  .rail-glint::after {
    content: ""; position: absolute; inset: 0; pointer-events: none;
    background: linear-gradient(110deg, transparent 30%, var(--rail-glint-sheen, rgb(255 255 255 / .22)) 45%, transparent 65%);
    transform: translateX(-100%);
    animation: rail-glint 1.15s ease-out 3;
  }
  @keyframes rail-glint { to { transform: translateX(100%); } }
}
