/*
 * Fullscreen league stories.
 *
 * Constrained to the shell width rather than the window: on a phone that is
 * genuinely full-bleed, and on desktop it keeps the mini app inside its own
 * frame instead of taking over the browser.
 *
 * Three transform layers, each owning exactly one job, because they need
 * different origins and different timing:
 *
 *   __stage                     the shared-element morph. Origin parked on the
 *                               tapped avatar.
 *   __card                      the dismiss drag. Origin its own centre, which
 *                               is where a card has to shrink towards.
 *   .story-pane / .story-card   the chain drag, as a 3D hinge. Origin on the
 *                               card's own leading or trailing edge.
 *
 * Collapsing any two of these into one element means one transform-origin
 * serving two gestures, and the dismiss would shrink towards the avatar
 * instead of towards itself.
 *
 * The perspective sits on .story-pane and not on __stage, because `perspective`
 * only reaches an element's *direct* children: declared on the stage it would
 * foreshorten __card — the dismiss layer — and never arrive at the cards two
 * levels further down. Every pane is inset:0 over the same deck, so they all
 * resolve the same perspective origin and the cards hinge in one shared space.
 */

.story-viewer {
  /*
   * Dwell per item. Not a motion token — --duration-max caps *animation*, and
   * this is reading time, which is content pacing. It lives here because it is
   * the story viewer's own pacing and nothing else in the app has an opinion.
   */
  --story-duration: 4200ms;

  /*
   * Hinge geometry. The two stacks are a pair of doors on a shared jamb: the
   * outgoing card swings shut on its trailing edge while the incoming one
   * opens on its leading edge, both driven by the single --progress the drag
   * writes onto the deck.
   *
   * Geometry, not motion — every duration and easing still comes from
   * tokens.css. These live here for the same reason --story-duration does:
   * they are this component's own shape and nothing else has an opinion.
   *
   * The perspective was picked against the real card, ~360px wide inside the
   * gutters, with the swing frozen at its midpoint. 1000px puts the headline
   * on a visibly steep slant — the last word of "Preview story 1" rides a good
   * deal higher than the first — and fans the corners out hard. 1500px flattens
   * the taper until the fold reads as a crease rather than a card standing at
   * an angle. 1200px keeps the depth and leaves the copy square enough to read
   * mid-drag, which is the whole point of a gesture the thumb is still on.
   */
  --hinge-perspective: 1200px;
  --hinge-angle: 70deg;
  --hinge-scale: 0.92;
  /*
   * Opacity lost at full swing, and the gain that compresses the fade into the
   * last quarter of it — so the card only dims once it is already near
   * edge-on, rather than greying out through the whole drag.
   */
  --hinge-fade: 0.4;
  --hinge-fade-gain: 4;

  position: fixed;
  inset: 0;
  z-index: 100;
  max-width: var(--app-max-width);
  margin-inline: auto;
}

/*
 * Translucent, so the strip stays visible behind the card. That is what makes
 * the dismiss legible: as the card shrinks away, the avatar it is heading back
 * into is already on screen. --dim rises with the drag.
 */
.story-viewer__backdrop {
  position: absolute;
  inset: 0;
  background: var(--surface-inverse);
  opacity: 0;
  transition: opacity var(--duration) var(--easing);
}

.story-viewer[data-open="true"] .story-viewer__backdrop {
  opacity: var(--dim, 0.62);
}

/* --- Morph layer ---------------------------------------------------------- */

.story-viewer__stage {
  position: absolute;
  inset: 0;
  opacity: 0;
  transform: scale(var(--morph-scale, 0.1));
  transform-origin: 50% 40%;
  /* The stage owns every gesture; nothing inside competes for the finger. */
  touch-action: none;
  will-change: transform, opacity;
  transition:
    transform var(--spring-duration) var(--spring),
    opacity var(--duration) var(--easing);
}

.story-viewer[data-open="true"] .story-viewer__stage {
  opacity: 1;
  transform: scale(1);
}

/* --- Drag layer ----------------------------------------------------------- */

/*
 * The visible card. It carries the background and the radius, so the morph and
 * the dismiss share one rounding: 50% while collapsed into the avatar,
 * --drag-radius while being pulled down.
 *
 * user-select / touch-callout are load-bearing here, not hygiene. Left on, the
 * browser starts a native text selection on the headline and then a drag of
 * that selection, which fires pointercancel and silently kills the swipe — on
 * the second gesture only. On iOS the callout menu would likewise outrank the
 * press-and-hold that pauses the story.
 */
.story-viewer__card {
  position: absolute;
  inset: 0;
  display: flex;
  flex-direction: column;
  gap: var(--space-3);
  padding: calc(var(--space-3) + var(--safe-top)) 0
    calc(var(--space-5) + max(var(--safe-bottom), var(--tg-chrome-inset, 0px)));
  background: var(--surface-inverse);
  color: var(--text-inverse);
  border-radius: 50%;
  overflow: hidden;
  user-select: none;
  -webkit-user-select: none;
  -webkit-touch-callout: none;
  transform: translateY(var(--drag-y, 0px)) scale(var(--drag-scale, 1));
  transform-origin: center;
  will-change: transform;
  transition:
    transform var(--spring-duration) var(--spring),
    border-radius var(--spring-duration) var(--easing);
}

.story-viewer[data-open="true"] .story-viewer__card {
  border-radius: var(--drag-radius, 0px);
}

/*
 * While the finger is down everything tracks it 1:1. No transition, or the card
 * lags the thumb by a whole spring.
 */
.story-viewer[data-dragging="true"] .story-viewer__card,
.story-viewer[data-dragging="true"] .story-viewer__backdrop,
.story-viewer[data-dragging="true"] .story-deck,
.story-viewer[data-dragging="true"] .story-card {
  transition: none;
}

/*
 * Re-indexing after a committed chain: the panes jump from their committed
 * positions back to prev/current/next, and that bookkeeping must not be
 * something the user can see.
 */
.story-viewer[data-settling="true"] .story-deck,
.story-viewer[data-settling="true"] .story-card {
  transition: none;
}

/* --- Chrome --------------------------------------------------------------- */

/*
 * Fixed above the deck: the progress bar swaps to the incoming league the
 * instant the commit threshold is crossed, so it must not be riding on the
 * panes that are still mid-slide.
 */
.story-chrome {
  flex: none;
  display: flex;
  flex-direction: column;
  gap: var(--space-3);
  padding-inline: var(--space-4);
}

.story-progress {
  display: flex;
  gap: 3px;
}

/* A class beats the UA [hidden] rule, so display has to be turned off by hand. */
.story-progress[hidden] {
  display: none;
}

.story-progress__segment {
  flex: 1;
  height: 2px;
  border-radius: var(--radius-full);
  background: rgb(var(--text-inverse-rgb) / 0.28);
  overflow: hidden;
}

/*
 * scaleX rather than width: the bar animates for four seconds straight, and it
 * is the one element on screen that must not cost a layout pass per frame.
 */
.story-progress__fill {
  display: block;
  height: 100%;
  background: var(--text-inverse);
  transform: scaleX(0);
  transform-origin: left center;
}

.story-progress__segment[data-state="seen"] .story-progress__fill {
  transform: scaleX(1);
}

/*
 * The dwell timer itself. StoryViewer.js advances on this animation's end, so
 * the bar and the clock are the same object and cannot drift apart — and
 * pausing the bar is what lets a cancelled dismiss resume mid-item rather than
 * restart the story.
 */
.story-progress__segment[data-state="active"] .story-progress__fill {
  animation: story-progress var(--story-duration) linear forwards;
}

.story-viewer[data-paused="true"] .story-progress__fill {
  animation-play-state: paused;
}

@keyframes story-progress {
  to {
    transform: scaleX(1);
  }
}

.story-head {
  display: flex;
  align-items: center;
  gap: var(--space-2);
}

.story-head__crest {
  display: grid;
  place-items: center;
  width: 28px;
  height: 28px;
  background: rgb(var(--text-inverse-rgb) / 0.16);
  border-radius: var(--radius-full);
  font-size: var(--text-2xs);
  font-weight: var(--weight-semibold);
}

.story-head__league {
  font-size: var(--text-sm);
  font-weight: var(--weight-semibold);
}

.story-head__counter {
  flex: 1;
  color: rgb(var(--text-inverse-rgb) / 0.6);
  font-size: var(--text-xs);
}

.story-head__close {
  display: grid;
  place-items: center;
  width: 32px;
  height: 32px;
  border-radius: var(--radius-full);
  color: inherit;
}

/* --- Deck ----------------------------------------------------------------- */

/*
 * --cross-y is the damped remainder of a vertical wobble during a horizontal
 * drag, and --dx the same remainder the other way round during a dismiss.
 * Deliberately tiny: enough that a diagonal finger is acknowledged, never
 * enough to read as the gesture the drag did not lock to.
 */
.story-deck {
  position: relative;
  flex: 1;
  transform: translate(var(--dx, 0px), var(--cross-y, 0px));
  transition: transform var(--spring-duration) var(--spring);
}

/*
 * The hinge's frame. Every pane is inset:0 over the deck — the cards are a
 * stack, not a filmstrip, and are revealed by rotating rather than by sliding
 * out of the way.
 *
 * transform-style is left at its default `flat`. preserve-3d would hand the
 * depth ordering to the geometry, which sounds right until the two cards cross
 * at the midpoint: coplanar faces get split and sorted per fragment, and that
 * is exactly the z-fighting Telegram's WebView is worst at. Flat keeps the
 * ordering in z-index, where a single crossing point can own it.
 */
.story-pane {
  position: absolute;
  inset: 0;
  z-index: 1;
  display: flex;
  padding-inline: var(--space-4);
  perspective: var(--hinge-perspective);
}

/*
 * Rest. The current card lies flat and the neighbours are already fully swung
 * open on the edge they will come in from — a pair of closed doors either side.
 * At full swing they foreshorten to about a third of their width and sit behind
 * the current card, so nothing of them shows until the drag starts opening one.
 */
.story-pane--current {
  z-index: 2;
  --hinge-t: 0;
}

.story-pane--next {
  --hinge-origin: left;
  --hinge-sign: 1;
  --hinge-t: 1;
}

.story-pane--prev {
  --hinge-origin: right;
  --hinge-sign: -1;
  --hinge-t: 1;
}

/*
 * Under the finger. --progress is the drag's own 0..1, written once onto the
 * deck; each pane reads it as its own distance from flat, so one number drives
 * both halves of the hinge. The outgoing card leaves flat as progress rises,
 * the incoming one arrives at flat as it does.
 *
 * --dir is set at the start of the drag and never cleared. At rest a stale
 * value is inert — the current card sits at t: 0, where neither the origin nor
 * the sign can be seen — and leaving it in place is what lets a cancelled drag
 * spring back around the same edge it swung out on, rather than switching
 * hinge mid-flight.
 */
.story-viewer[data-dir="forward"] .story-pane--current {
  --hinge-origin: right;
  --hinge-sign: -1;
  --hinge-t: var(--progress, 0);
}

.story-viewer[data-dir="forward"] .story-pane--next {
  --hinge-t: calc(1 - var(--progress, 0));
}

.story-viewer[data-dir="back"] .story-pane--current {
  --hinge-origin: left;
  --hinge-sign: 1;
  --hinge-t: var(--progress, 0);
}

.story-viewer[data-dir="back"] .story-pane--prev {
  --hinge-t: calc(1 - var(--progress, 0));
}

/*
 * Depth ordering. Cover flow reads correctly only while the card nearer the
 * viewer paints last, and which card that is changes at the crossing — so the
 * swap happens there, at the midpoint, where the two are mirror images of each
 * other and it cannot be seen. JS owns the crossing; this is only its result.
 */
.story-viewer[data-front="incoming"] .story-pane--current {
  z-index: 1;
}

.story-viewer[data-front="incoming"] .story-pane--next,
.story-viewer[data-front="incoming"] .story-pane--prev {
  z-index: 2;
}

/*
 * Committed. --hinge-t is pinned to its end value rather than left reading
 * --progress, because the inline --progress the drag writes onto the deck
 * would outrank anything this rule could inherit. Same specificity as the
 * --dir rules above, so source order is what carries it — these must stay
 * below them.
 */
.story-viewer[data-committing="forward"] .story-pane--current,
.story-viewer[data-committing="back"] .story-pane--current {
  --hinge-t: 1;
}

.story-viewer[data-committing="forward"] .story-pane--next,
.story-viewer[data-committing="back"] .story-pane--prev {
  --hinge-t: 0;
}

/* --- Card content --------------------------------------------------------- */

/*
 * The hinged leaf. It carries the whole chain transform: rotation about the
 * edge it is hinged on, and a scale away from the viewer as it swings.
 *
 * --hinge-sign folds the two directions into one rule. Going forward, the
 * outgoing card hinges on its right edge at -1 and the incoming one on its
 * left at +1; going back, the pair swap. The sign is the only thing that
 * changes, so the transform below never has to know which way the finger went.
 *
 * The background is opaque on purpose, and layered rather than picked: the
 * tint is the same alpha step of --text-inverse-rgb the crest and the progress
 * track already use, but composited over --surface-inverse instead of over
 * whatever happens to be behind it. A translucent card would show the other
 * card through itself at every angle of the swing, which reads as a bug rather
 * than as depth.
 */
.story-card {
  flex: 1;
  display: flex;
  flex-direction: column;
  justify-content: flex-end;
  gap: var(--space-2);
  padding: var(--space-5);
  background:
    linear-gradient(
      rgb(var(--text-inverse-rgb) / 0.06),
      rgb(var(--text-inverse-rgb) / 0.06)
    ),
    var(--surface-inverse);
  border: 1px solid rgb(var(--text-inverse-rgb) / 0.12);
  border-radius: var(--radius-lg);
  box-shadow: var(--shadow-floating);
  transform-origin: var(--hinge-origin, center) center;
  transform: rotateY(calc(var(--hinge-sign, 0) * var(--hinge-angle) * var(--hinge-t, 0)))
    scale(calc(1 - (1 - var(--hinge-scale)) * var(--hinge-t, 0)));
  /*
   * The fade, remapped so it only bites over the last quarter of the swing —
   * anything before that lands above 1 and is clamped away.
   */
  --hinge-fade-t: calc(var(--hinge-t, 0) * var(--hinge-fade-gain) - (var(--hinge-fade-gain) - 1));
  opacity: clamp(
    calc(1 - var(--hinge-fade)),
    calc(1 - var(--hinge-fade) * var(--hinge-fade-t)),
    1
  );
  will-change: transform, opacity;
  transition:
    transform var(--spring-duration) var(--spring),
    opacity var(--duration) var(--easing);
}

/*
 * The flight home. Ease, never the spring: --spring overshoots by design, and
 * a rotation that overshoots 0deg swings the arriving card visibly past flat
 * and back — a wobble on a door that has already shut. --duration-slow is the
 * app's non-spring length at the same 320ms, so a committed chain still lands
 * in step with everything else that travels this far.
 *
 * A cancelled drag keeps the spring above. That one is a snap back to rest,
 * which is exactly what an overshoot is for.
 */
.story-viewer[data-committing] .story-card {
  transition:
    transform var(--duration-slow) var(--easing),
    opacity var(--duration-slow) var(--easing);
}

/*
 * A tap is a cut, not a glide. The copy has already been swapped by the time
 * this runs; it only softens the edge of the swap. --duration is the app's fade
 * token — anything springy here would make prev/next feel slower than the drag
 * it sits alongside.
 */
.story-pane[data-cut] .story-card {
  animation: story-cut var(--duration) var(--easing);
}

@keyframes story-cut {
  from {
    opacity: 0;
  }
}

.story-card__eyebrow {
  align-self: flex-start;
  padding: 2px var(--space-2);
  background: var(--text-inverse);
  border-radius: var(--radius-full);
  color: var(--surface-inverse);
  font-size: var(--text-2xs);
  font-weight: var(--weight-semibold);
  text-transform: uppercase;
  letter-spacing: 0.06em;
}

.story-card__headline {
  font-size: var(--text-lg);
  font-weight: var(--weight-semibold);
  line-height: 1.25;
}

.story-card__body {
  color: rgb(var(--text-inverse-rgb) / 0.72);
  font-size: var(--text-sm);
}

/* The page behind must not scroll under the overlay. */
body[data-story-open] {
  overflow: hidden;
}

@media (prefers-reduced-motion: reduce) {
  .story-pane[data-cut] .story-card {
    animation: none;
  }

  /*
   * The morph collapses to a cross-fade. Neutralising the scale rather than
   * the whole transform matters: --drag-y shares that property, and
   * drag-to-dismiss still has to track the finger here.
   */
  .story-viewer__stage {
    --morph-scale: 1;
  }

  /*
   * The hinge is finger-tracked, so zeroing the durations does not reach it —
   * a 70deg rotation would still play out under the thumb, which is the shape
   * of motion this media query exists to remove. Flattened back to the
   * filmstrip it replaced, reusing the same two variables: --hinge-sign
   * already encodes which way each card travels, and --hinge-t how far.
   */
  .story-viewer {
    --hinge-angle: 0deg;
    --hinge-scale: 1;
    --hinge-fade: 0;
  }

  .story-card {
    transform: translateX(calc(var(--hinge-sign, 0) * 100% * var(--hinge-t, 0)));
  }
}
