/* ==========================================================================
   Astuce L-Bar — motion

   Two layers, because that is how a broadcast graphic actually reads:

     1. the BASE — a plate is pushed into place and opens as it travels;
     2. the COPY — the text rises out of the plate two frames behind it.

   Each layer moves in TWO properties at once, on TWO different curves:

     clip-path  reveals   — monotonic, front-loaded, never passes its edge
     transform  travels   — overshoots its mark and settles back

   Splitting them is the whole point. A reveal alone has no mass: it is a
   progress bar. A travel alone is a slide. Together, on curves that disagree
   slightly, the plate reads as having been pushed and the copy as having been
   carried. They are separate `animation`s on the same element precisely so
   they can carry separate timing functions.

   The plate travels DOWN into place while the copy rises UP out of it. The
   opposed vectors are what produce depth: the base arrives, the text reacts.

   The whole layer is opt-in. Without `data-motion="on"` on the root the
   package renders exactly as the static templates do, which is what you want
   for stills, for frame grabs and for design review.
   ========================================================================== */

/* --------------------------------------------------------------------------
   Axis
   The base opens down the Y by default: an L-Bar builds itself top-down, the
   way the eye enters the frame. `data-motion-axis="x"` puts it back on the
   horizontal, where reveal and push both follow the reading edge — right in
   RTL, left in LTR — the same logic the layout uses for its offsets.
   -------------------------------------------------------------------------- */

[dir="ltr"] {
  --wipe-closed-x: inset(0 100% 0 0);
  --wipe-closed-far-x: inset(0 0 0 100%);
  --push-from-x: translateX(calc(var(--push-distance) * -1));
}
[dir="rtl"] {
  --wipe-closed-x: inset(0 0 0 100%);
  --wipe-closed-far-x: inset(0 100% 0 0);
  --push-from-x: translateX(var(--push-distance));
}

.lbar-canvas {
  --wipe-closed-y: inset(0 0 100% 0);       /* collapsed to the near edge */
  --wipe-closed-far-y: inset(100% 0 0 0);   /* collapsed to the far edge  */
  --wipe-closed: var(--wipe-closed-y);
  --wipe-closed-far: var(--wipe-closed-far-y);
  --wipe-open: inset(0 0 0 0);
  --push-from: translateY(calc(var(--push-distance) * -1));
}
.lbar-canvas[data-motion-axis="x"] {
  --wipe-closed: var(--wipe-closed-x);
  --wipe-closed-far: var(--wipe-closed-far-x);
  --push-from: var(--push-from-x);
}

/* --------------------------------------------------------------------------
   Layer 1 — the base
   Regions cascade in reading order, and the gaps TIGHTEN as the build goes on
   — three frames, then two, then one. An even cascade is a metronome; a
   closing one reads as a graphic assembling itself. The video window leads:
   the picture is the reason the L-Bar exists.
   -------------------------------------------------------------------------- */

@keyframes lbar-plate-open {
  from { clip-path: var(--wipe-closed); }
  to   { clip-path: var(--wipe-open); }
}
@keyframes lbar-plate-push {
  from { transform: var(--push-from); }
  to   { transform: translate(0); }
}

[data-motion="on"] .lbar-region:not(.lbar-sweep) {
  animation:
    lbar-plate-open var(--dur-plate) var(--ease-plate) both,
    lbar-plate-push var(--dur-push)  var(--ease-push)  both;
  /* One value cycles across both animations in the list.
     The plate starts `--sweep-lead` after the solid's leading edge, so its
     reveal always runs inside the covered band. */
  animation-delay: calc(var(--reveal-delay, 0ms) + var(--sweep-lead));
  /* Promoted for the duration only: a permanent layer on seven 1920-wide
     regions is memory a graphics engine would rather keep. Holding it here
     also means the copy repaints inside a layer that already exists. */
  will-change: clip-path, transform;
}
[data-motion="on"] .lbar-region--stage   { --reveal-delay: 0ms; }
[data-motion="on"] .lbar-region--logo    { --reveal-delay: calc(var(--motion-frame) * 3); }
[data-motion="on"] .lbar-region--panel   { --reveal-delay: calc(var(--motion-frame) * 5); }
[data-motion="on"] .lbar-region--rail    { --reveal-delay: calc(var(--motion-frame) * 7); }
[data-motion="on"] .lbar-region--program { --reveal-delay: calc(var(--motion-frame) * 8); }
[data-motion="on"] .lbar-region--clock   { --reveal-delay: calc(var(--motion-frame) * 9); }
[data-motion="on"] .lbar-region--news    { --reveal-delay: calc(var(--motion-frame) * 10); }

/* --------------------------------------------------------------------------
   Layer 0 — the backdrop
   The ground establishes the shot, so it starts at zero and runs longer than
   anything that lands on it. It pulls BACK out of a blur — overscaled and
   soft, easing to 1:1 and sharp — which reads as the ground settling into
   place rather than growing into frame.

   Blur is expensive, so this is the one layer that gets it, and the promotion
   is dropped the moment the entrance settles.
   -------------------------------------------------------------------------- */

@keyframes lbar-backdrop-in {
  from {
    transform: scale(var(--backdrop-zoom));
    filter: blur(var(--backdrop-blur));
    opacity: 0;
  }
  to {
    transform: scale(1);
    filter: blur(0);
    opacity: 1;
  }
}

[data-motion="on"]::before {
  animation: lbar-backdrop-in var(--dur-backdrop) var(--ease-plate) both;
  will-change: transform, filter, opacity;
}
/* Landed: hold. This is also what lets a replay restart it — `reveal` drops
   the settled class, the animation-name changes from none back to the
   keyframes, and a changed name starts a new animation. Without this the
   backdrop simply never played again. */
[data-motion="on"].lbar-settled::before { animation: none; will-change: auto; }

/* --------------------------------------------------------------------------
   Layer 1a — the sweep
   A solid plate the size of the box, passing THROUGH it: it covers, the
   content is revealed behind it, and it uncovers to leave the content
   standing. Not a bar with a tail — a bar reads as decoration travelling
   past, whereas a plate reads as the graphic being laid down.

   It needs two independent edges moving on the same curve at different
   times, and `clip-path` is one property, so it takes two elements:

     .lbar-sweep          the LEADING edge — opens, exactly like a plate
     .lbar-sweep::before  the TRAILING edge — closes, four frames behind

   The plate's own reveal runs between them, two frames back. So at every
   moment, top to bottom: trailing edge, then the plate's reveal edge, then
   the leading edge. The solid always sits over ground that is not ready yet
   and never uncovers ground that is.

   The colour is the graphic's own — near-black under the dark package, white
   under the bright — so the solid resolves into the plate it leaves behind
   instead of flashing as a separate event.

   It cannot live inside the region: the region is what `clip-path` is
   cutting, so a pseudo-element of it would be cut away exactly where the
   solid needs to be. motion.js drops one sibling per region into the canvas
   carrying the region's own grid classes, so it inherits the measured
   geometry from `base.css` — nothing is measured in JavaScript.
   -------------------------------------------------------------------------- */

.lbar-sweep {
  pointer-events: none;
  z-index: 5;
}
.lbar-sweep::before {
  content: "";
  position: absolute;
  inset: 0;
  background: var(--sweep-color);
}

@keyframes lbar-sweep-lead  {
  from { clip-path: var(--wipe-closed); }
  to   { clip-path: var(--wipe-open); }
}
@keyframes lbar-sweep-trail {
  from { clip-path: var(--wipe-open); }
  to   { clip-path: var(--wipe-closed-far); }
}

[data-motion="on"] .lbar-sweep {
  animation: lbar-sweep-lead var(--dur-plate) var(--ease-plate) both;
  animation-delay: var(--reveal-delay, 0ms);
  will-change: clip-path;
}
[data-motion="on"] .lbar-sweep::before {
  animation: lbar-sweep-trail var(--dur-plate) var(--ease-plate) both;
  animation-delay: calc(var(--reveal-delay, 0ms) + var(--sweep-lead) * 2);
  will-change: clip-path;
}

/* --------------------------------------------------------------------------
   Layer 2 — the copy
   Every text leaf already carries `.lbar-t`, so the copy animates without a
   single wrapper being added to the DOM.

   The mask travels with the glyphs rather than standing still — a fixed mask
   would need an extra element around every line, and over 0.42em the
   difference is not visible. Because the distance is in `em`, the gesture
   survives a font swap.

   Left, right and bottom insets are negative so a descender or an overhang is
   never shaved; only the top inset animates.
   -------------------------------------------------------------------------- */

@keyframes lbar-copy-open {
  from { clip-path: inset(100% -20% -20% -20%); }
  to   { clip-path: inset(0    -20% -20% -20%); }
}
@keyframes lbar-copy-rise {
  from { transform: translateY(var(--rise-distance)); }
  to   { transform: translateY(0); }
}

/* `--reveal-delay` and `--row-index` are custom properties, so a line inherits
   its region's start and its row's place without a rule of its own.

   The second group is the furniture that belongs to a line rather than to the
   plate — the direction badge, the sparkline, the day chart, the news tick,
   the programme thumbnail. Left on the plate they arrive before the copy does
   and read as markers floating beside nothing, so they rise with their row. */
[data-motion="on"] .lbar-t,
[data-motion="on"] lbar-badge,
[data-motion="on"] .qc__spark,
[data-motion="on"] .nr__tick,
[data-motion="on"] .mc__chart,
[data-motion="on"] .pcard__thumb {
  animation:
    lbar-copy-open var(--dur-copy) var(--ease-copy) both,
    lbar-copy-rise var(--dur-rise) var(--ease-rise) both;
  animation-delay: calc(
    var(--reveal-delay, 0ms) +
    var(--sweep-lead) +
    var(--stagger-text) +
    var(--row-index, 0) * var(--stagger-row)
  );
}

/* Once the entrance has played the promotion is dead weight — motion.js drops
   this class when the last animation ends. */
[data-motion="on"].lbar-settled .lbar-region { will-change: auto; }

/* --------------------------------------------------------------------------
   Landed
   Once the graphic is up, nothing is animating, and saying so matters.

   Leaving the entrance rules applied is not free: a swap replaces the
   animation on one element for the length of the swap, and the moment
   `data-swap` comes off, `animation-name` reverts to the entrance pair. A
   changed animation-name starts a NEW animation — so the value that had just
   wiped in would vanish for the length of the entrance delay and wipe in a
   second time. Read on air as the text blinking two or three times per update.

   So once settled the copy holds, and the only exception is an element that is
   mid-swap. The swap's own final state is the element's natural one, so
   dropping the animation when it ends changes nothing on screen.
   -------------------------------------------------------------------------- */

[data-motion="on"].lbar-settled .lbar-region,
[data-motion="on"].lbar-settled .lbar-t:not([data-swap]),
[data-motion="on"].lbar-settled lbar-badge:not([data-swap]),
[data-motion="on"].lbar-settled .qc__spark:not([data-swap]),
[data-motion="on"].lbar-settled .nr__tick:not([data-swap]),
[data-motion="on"].lbar-settled .mc__chart:not([data-swap]),
[data-motion="on"].lbar-settled .pcard__thumb:not([data-swap]) {
  animation: none;
}

/* --------------------------------------------------------------------------
   Exit — one thing at a time.

   The copy of a region clears, three frames later that region's plate closes
   behind it, and the ground goes once the last plate has started. Regions
   leave in reverse build order, so the graphic unbuilds the way it was built.

   The sweep does not run on the way out. It is an arrival gesture — a solid
   laying the graphic down — and on the exit it was a third moving object in
   the same window as the falling copy and the closing plate. Three overlapping
   things per region read as a mess rather than as a departure, which is what
   this looked like before.

   Nothing overshoots: an overshoot is the sound of something landing, and
   nothing is landing here.
   -------------------------------------------------------------------------- */

@keyframes lbar-plate-close {
  from { clip-path: var(--wipe-open); transform: translate(0); }
  to   { clip-path: var(--wipe-closed); transform: var(--push-from); }
}
@keyframes lbar-copy-fall {
  from { clip-path: inset(0    -20% -20% -20%); transform: translateY(0); }
  to   { clip-path: inset(100% -20% -20% -20%); transform: translateY(calc(var(--rise-distance) * -1)); }
}

[data-motion="on"][data-state="out"] .lbar-t,
[data-motion="on"][data-state="out"] lbar-badge,
[data-motion="on"][data-state="out"] lbar-chevrons,
[data-motion="on"][data-state="out"] .qc__spark,
[data-motion="on"][data-state="out"] .nr__tick,
[data-motion="on"][data-state="out"] .mc__chart,
[data-motion="on"][data-state="out"] .pcard__thumb {
  animation: lbar-copy-fall var(--dur-swap) var(--ease-swap) both;
  animation-delay: calc(var(--motion-frame) * 10 - var(--reveal-delay, 0ms));
}
[data-motion="on"][data-state="out"] .lbar-region:not(.lbar-sweep) {
  animation: lbar-plate-close var(--dur-plate-out) var(--ease-swap) both;
  animation-delay: calc(
    var(--motion-frame) * 10 - var(--reveal-delay, 0ms) + var(--exit-lead)
  );
}
/* motion.js clears the sweeps before an exit, but if one ever survives it
   would still match the arrival rule and lay itself down over a departing
   graphic. Belt and braces. */
[data-motion="on"][data-state="out"] .lbar-sweep,
[data-motion="on"][data-state="out"] .lbar-sweep::before {
  animation: none;
}

/* The ground goes last, once the final plate has started to close. */
[data-motion="on"][data-state="out"]::before {
  animation: lbar-backdrop-in var(--dur-backdrop) var(--ease-swap) reverse both;
  animation-delay: calc(var(--motion-frame) * 10 + var(--exit-lead));
}

/* --------------------------------------------------------------------------
   Data change
   The same rise at six frames: the old value leaves upward and the new one
   comes up behind it. motion.js sets `data-swap` on the element whose text
   changed, waits for the out, writes, and plays the in.
   -------------------------------------------------------------------------- */

[data-motion="on"] [data-swap="out"] {
  animation: lbar-copy-fall var(--dur-swap) var(--ease-swap) both;
  animation-delay: 0ms;
}
[data-motion="on"] [data-swap="in"] {
  animation: lbar-copy-swap-in var(--dur-swap) var(--ease-swap) both;
  animation-delay: 0ms;
}
@keyframes lbar-copy-swap-in {
  from { clip-path: inset(100% -20% -20% -20%); transform: translateY(var(--rise-distance)); }
  to   { clip-path: inset(0    -20% -20% -20%); transform: translateY(0); }
}

/* --------------------------------------------------------------------------
   The direction marker
   The one element that keeps moving after the graphic has landed.

   ARRIVING. The dots come in per chevron, apex first and out along the arms.
   `--chev-order` counts from the TRAILING chevron, and because `down` is the
   whole glyph flipped, the trailing chevron carries the high index in both
   directions — so a single number sequences the travel and the CSS never has
   to know which way the arrow points.

   Nothing here loops. A pulsing marker was tried and cut: resting the coloured
   chevrons dim so a highlight could travel through them left them faint for
   most of every cycle, while the flat grey one — which had nothing to travel —
   sat at full strength, so the rail read brightest where the least was
   happening. The graphic lands and holds.
   -------------------------------------------------------------------------- */

.chevrons__glyph circle {
  transform-box: fill-box;
  transform-origin: center;
  /* Where along the arm a dot sits in the running order. The default runs
     apex outward — the marker opens from its centre and spreads to the edges,
     which is what a loser does. */
  --dot-order: var(--dot-step, 0);
}
/* A gainer runs the other way: the arms fill from their outer ends inward and
   the tip is the last thing to land, so the marker converges on the point it
   is making. `--arm-count` comes off the svg, which knows how long the arm is. */
[data-motion="on"] .chevrons.is-up .chevrons__glyph circle {
  --dot-order: calc(var(--arm-count, 4) - var(--dot-step, 0));
}

@keyframes lbar-dot-in {
  from { opacity: 0; transform: scale(0.35); }
  to   { opacity: 1; transform: scale(1); }
}
[data-motion="on"] .chevrons__glyph circle {
  animation: lbar-dot-in var(--dur-chev-in) var(--ease-copy) both;
  animation-delay: calc(
    var(--reveal-delay, 0ms) +
    var(--sweep-lead) +
    var(--stagger-text) +
    var(--row-index, 0) * var(--stagger-row) +
    var(--chev-order, 0) * var(--stagger-chev) +
    var(--dot-order, 0) * var(--stagger-dot)
  );
}

/* Once landed the dots hold, and on the way out the marker leaves with its
   row — so neither class change may be taken as a cue to arrive again. */
[data-motion="on"].lbar-settled .chevrons__glyph circle,
[data-motion="on"][data-state="out"] .chevrons__glyph circle { animation: none; }

/* --------------------------------------------------------------------------
   Accessibility. A viewer who has asked for reduced motion still needs the
   graphic, so it arrives — it just arrives already in place.
   -------------------------------------------------------------------------- */

@media (prefers-reduced-motion: reduce) {
  [data-motion="on"] * {
    animation-duration: 1ms !important;
    animation-delay: 0ms !important;
  }
}
