/* Beat 04 — Tension */

/* Shorter. Five viewports was two movements' worth of runway for a beat whose second
   movement now runs on its own clock — the marquee does not need scroll to move. */
.beat-tension { height: 260vh; }

.beat-tension__stage {
  position: sticky;
  top: 0;
  height: 100vh;
  overflow: hidden;
  background: var(--c-aperture);
}

.tension__standoff {
  position: absolute;
  inset: 0;
  z-index: 10;
  display: grid;
  place-items: center;
  /* No opacity transition: tension.js writes this element's opacity every scrub frame.
     A CSS transition on top of a per-frame write makes it lag scroll and rubber-band
     instead of tracking it. */
}

.tension__wide {
  position: absolute;
  width: min(62vw, 1000px);
  aspect-ratio: 16 / 9;
  border-radius: var(--portalith-radius);
  transform:
    translateY(calc(var(--parallax, 0) + var(--my, 0) * 0.9vh))
    translateX(calc(var(--mx, 0) * 1.1vw))
    scale(var(--p-scale, 1));
  overflow: hidden;
  /* The horizontal plate lets go first, so the vertical is left alone. */
  opacity: calc(1 - var(--blend, 0));
  transition: transform 0.45s var(--ease-out);
}

/* The 9:16 grows out of the 16:9 and sits inside it — picture in picture, but with the
   inset carrying a different aspect ratio, which is where the tension comes from. */
.tension__tall {
  position: absolute;
  width: min(19vw, 300px);
  aspect-ratio: 9 / 16;
  border-radius: var(--portalith-radius);
  /* It leaves downward and fades, rather than sliding off to one side — sideways read as
     the card being dismissed, and it never found a slot to land in anyway.
     --mx/--my are the hover parallax, written by tension.js. */
  transform:
    translateY(calc(var(--parallax, 0) + var(--blend, 0) * 26vh + var(--my, 0) * -1.6vh))
    translateX(calc(var(--mx, 0) * -2.2vw))
    scale(var(--p-scale, 1));
  opacity: calc(1 - var(--blend, 0));
  overflow: hidden;
  box-shadow: 0 24px 60px rgb(0 0 0 / calc(0.45 * (1 - var(--blend, 0))));
  transition: transform 0.45s var(--ease-out);
}

/* The plate underneath moves the other way and less far — opposed rates are what make a
   two-layer hover read as depth rather than as one object sliding. */
.tension__standoff:hover .tension__wide,
.tension__standoff:hover .tension__tall { transition-duration: 0.18s; }

.tension__wide img, .tension__tall img { width: 100%; height: 100%; object-fit: cover; }

.tension__copy {
  position: absolute;
  right: var(--gutter);
  bottom: 12vh;
  max-width: 30ch;
  color: var(--c-ground);
  font-size: 1rem;
  line-height: 1.5;
}

.tension__field {
  position: absolute;
  inset: 0;
  z-index: 5;
  display: grid;
  grid-template-columns: repeat(4, 1fr);
  /* A little air. Not the old gutter — just enough that the wall reads as tiles rather
     than as one printed sheet. */
  /* The gutter lives INSIDE each column now, as padding — see the note on .field__col. */
  gap: 0;
  padding: 0;
}

/* Each column clips its own track, so a wrapping marquee never shows an end. This is also
   what guarantees no blank space: the column is exactly the stage's height and the track
   inside it is always at least twice that.

   The horizontal gutter is padding HERE rather than a gap on the grid, and that is what
   lets a held tile grow. The column's overflow: hidden clips anything that reaches its
   edge — with the tiles running full-bleed to that edge, a hovered tile scaling up had its
   rounded corners sliced into square ones and its caption scrim cut off at the boundary.
   Padding gives the pop somewhere to go. Keep it wider than half the pop's growth. */
.field__col {
  position: relative;
  overflow: hidden;
  height: 100%;
  padding: 0 0.9vw;
}

.field__track {
  display: flex;
  flex-direction: column;
  will-change: transform;
}

/* grid-column-start is written by field.js as an integer — see the note there. */
/* No transform on the tile at all now — the track carries the movement, so a tile is free
   to transition its depth channels without ever fighting a per-frame write. This is also
   what fixed the hover glitch: holding one tile rewrote every other tile's --p-scale, and
   with that landing straight on transform the whole wall jumped in a single frame. */
.field__tile {
  margin: 0 0 1.8vw;
  width: 100%;
  aspect-ratio: 9 / 16;
  position: relative;
  flex: 0 0 auto;
  cursor: pointer;
  border-radius: var(--portalith-radius);
  overflow: hidden;
  transition:
    filter 0.35s var(--ease-out),
    opacity 0.35s var(--ease-out),
    transform 0.3s var(--ease-out),
    box-shadow 0.3s var(--ease-out);
}

/* The pop. Safe to put on the tile's transform because the marquee moves the TRACK — the
   tile itself owns no per-frame transform to fight with. */
.field__tile.is-held {
  transform: scale(1.05);
  z-index: 2;
  box-shadow: 0 26px 60px rgb(0 0 0 / 0.55);
}

.field__tile img { width: 100%; height: 100%; object-fit: cover; }

/* Inside the tile, bottom left, on its own scrim. It used to hang below the tile — which
   worked until the tile got the Portalith's radius and `overflow: hidden` with it, and
   clipped the caption out of existence. */
.field__caption {
  position: absolute;
  inset: auto 0 0 0;
  padding: 0.9rem 0.9rem 0.8rem;
  color: var(--c-ground);
  font-size: 0.75rem;
  line-height: 1.35;
  text-align: left;
  opacity: 0;
  transition: opacity 0.25s var(--ease-out);
  background: linear-gradient(to top, rgb(17 17 17 / 0.88), rgb(17 17 17 / 0));
}
.field__caption b { display: block; }
.field__caption span { display: block; color: var(--c-portalith); }

.field__tile.is-held .field__caption { opacity: 1; }

/* Motion off: a uniform static grid with captions always visible, so nothing depends on a
   hover the viewer may never perform. */
.tension__field.is-static .field__caption { opacity: 1; }
.tension__field.is-static .field__track { transform: none !important; }

/* Reduced motion has no scrub. Tension is the one beat with two movements that BOTH earn
   their place — the standoff states the principle in words, the field carries the work —
   so instead of collapsing to a single screen they stack and flow: standoff first, field
   beneath it.

   This is the one beat that uses `height: auto`, and it may only do so because the two
   children below are switched to flowing. Absolutely positioned children contribute no
   height, which is what flattens a stage to its padding — see the other beats, which
   collapse to 100vh precisely because their children stay absolute. */
:root[data-motion="reduced"] .beat-tension { height: auto; }
:root[data-motion="reduced"] .beat-tension__stage {
  position: relative;
  height: auto;
  display: flex;
  flex-direction: column;
  gap: clamp(2rem, 6vh, 5rem);
  padding: clamp(3rem, 10vh, 8rem) 0;
}

:root[data-motion="reduced"] .tension__standoff {
  position: relative;
  inset: auto;
  height: 70vh;
  opacity: 1;
  /* The plates are the statement here; the field below carries the interaction, so the
     standoff must not sit over it swallowing pointer events. */
  pointer-events: none;
}

:root[data-motion="reduced"] .tension__field {
  position: relative;
  inset: auto;
  padding: 0 var(--gutter);
  /* Flowing exposes all four rows, and .field__caption hangs 3.2rem below its own tile
     (inset: auto 0 -3.2rem 0). The base 4vh row-gap isn't enough clearance for that overhang
     — measured an ~11.9px collision with the row beneath at 4vh. Widen the row gap only
     (column gap stays var(--gutter)) with a fixed margin well past that measurement. */
  row-gap: calc(4vh + 2rem);
}
