/* The premium motion layer.

   Split out of style.css deliberately. Everything here is decoration on top of a
   page that is already complete and readable without it, so it reads better as its
   own file, it keeps both files inside the 600-line budget, and deleting this one
   <link> turns the whole layer off without touching the base styles.

   Inspiration was a Three.js shader library (warp fields, liquid metal buttons,
   portal fields). None of that is imported: three.js is ~600KB against a page whose
   whole critical path is ~170KB. Everything below is CSS and a few lines of
   pointer maths, and every effect animates transform or opacity so it stays on the
   compositor rather than repainting.

   Every rule is inside prefers-reduced-motion: no-preference. Under reduce, this
   entire stylesheet does nothing. */

@media (prefers-reduced-motion: no-preference) {

  /* --- the shader field ----------------------------------------------------
     Positioning for the WebGL canvas in the hero. It lives here rather than in
     style.css because the canvas only ever exists when motion is allowed:
     field.js returns before unhiding it under reduce. field.js checks these
     rules actually applied before it shows the canvas, so deleting this
     stylesheet turns the field off cleanly instead of leaving a stray 300x150
     canvas in the flow. */

  /* width/height are NOT redundant with inset: canvas is a replaced element with
     an intrinsic 300x150, and inset alone leaves it at that size. */
  .field {
    position: absolute; inset: 0; width: 100%; height: 100%;
    z-index: 0; pointer-events: none;
  }
  .hero, .endcta { position: relative; }
  .hero > .shell, .hero > .demo, .endcta > .shell { position: relative; z-index: 1; }

  /* --- the beam ------------------------------------------------------------
     A conic gradient rotating behind a 1px gap, which is what reads as a lit,
     energised edge. The gradient never changes; only its transform does, so it
     is one composited layer rather than a per-frame repaint. */

  .beam { position: relative; padding: 1px; border: 0 !important; overflow: hidden; }
  .beam::before {
    content: "";
    position: absolute;
    left: 50%; top: 50%;
    width: 145%; aspect-ratio: 1;
    z-index: 0;
    /* The bright core has to be WIDER than the widest edge's angular span, or the
       corners go dark. A conic is angular but the box is not: from the centre of
       a 1120x738 window the long edges each subtend ~113deg and the short ones
       ~67deg. With a 43deg core, pointing it at the top edge left BOTH top
       corners outside the beam, so twice per turn the glow read as a straight bar
       with the corners snipped off. At 122deg the core cannot fit inside a long
       edge, so a corner is always lit. */
    background: conic-gradient(
      from 0turn,
      transparent 0 26%,
      oklch(0.7 0.16 277 / 0.5) 40%,
      oklch(0.82 0.13 300 / 0.9) 52%,
      oklch(0.82 0.13 300 / 0.9) 68%,
      oklch(0.7 0.16 277 / 0.5) 80%,
      transparent 94%);
    animation: beam-spin 9s linear infinite;
  }
  .beam > * { position: relative; z-index: 1; }

  /* Concentric with the outer radius, inset by the beam's 1px padding. */
  .beam > .window-in { border-radius: calc(var(--r-lg) - 1px); overflow: hidden; }
  @keyframes beam-spin {
    from { transform: translate(-50%, -50%) rotate(0turn); }
    to   { transform: translate(-50%, -50%) rotate(1turn); }
  }

  /* The base .lit hairline would sit on top of the beam and dull it. */
  .beam.lit::after { display: none; }

  /* --- pointer spotlight ---------------------------------------------------
     Cards light up where the cursor is. --mx/--my are set by one delegated
     listener, rAF-throttled, so N cards cost one handler rather than N. */

  /* The glow sits above the card's own background but below its content, so it
     lights the surface without washing out the text and costing contrast. */
  .spot { position: relative; }
  .spot > * { position: relative; z-index: 1; }
  .spot::before {
    content: "";
    position: absolute;
    inset: 0;
    z-index: 0;
    border-radius: inherit;
    pointer-events: none;
    opacity: 0;
    transition: opacity 0.35s ease;
    background: radial-gradient(
      260px circle at var(--mx, 50%) var(--my, 50%),
      oklch(0.7 0.16 277 / 0.14), transparent 68%);
  }
  /* Driven by data-lit, which the pointer handler sets, rather than by :hover.
     The handler already knows which card the pointer is in, and one source of
     truth beats CSS and JS disagreeing about it. :focus-within keeps it working
     for anyone arriving by keyboard. */
  .spot[data-lit]::before,
  .spot:focus-within::before { opacity: 1; }

  /* Cards lift slightly on approach. transform only. */
  .spot { transition: transform 0.4s var(--spring); }
  .spot[data-lit], .spot:focus-within { transform: translateY(-3px); }

  /* --- headline sweep ------------------------------------------------------
     One pass of light across the headline as it settles, then it is over.
     background-clip: text, so nothing moves and nothing reflows. */

  /* Guarded, because a headline that fails to paint is the worst possible bug
     here. The gradient is --fg everywhere except a narrow bright band, and both
     end positions keep the (wider-than-the-box) image over the text, so at rest
     the headline is solid --fg. Sliding the image out of the box instead leaves
     background-clip: text with nothing to clip, and the h1 disappears. */
  @supports ((background-clip: text) or (-webkit-background-clip: text)) {
    .in span {
      background-image: linear-gradient(100deg,
        var(--fg) 38%, oklch(0.99 0.02 280) 50%, var(--fg) 62%);
      background-repeat: no-repeat;
      background-size: 300% 100%;
      background-position: 100% 0;
      -webkit-background-clip: text;
      background-clip: text;
      color: transparent;
      animation-name: rise, sweep;
      animation-duration: 0.85s, 1.6s;
      animation-timing-function: var(--enter), cubic-bezier(0.4, 0, 0.2, 1);
      animation-delay: 0s, 0.55s;
      animation-fill-mode: backwards, forwards;
    }
    .in span:nth-child(2) { animation-delay: 0.08s, 0.68s; }
    .in span:nth-child(3) { animation-delay: 0.16s, 0.8s; }
    @keyframes sweep { to { background-position: 0% 0; } }
  }

  /* --- nav scroll progress -------------------------------------------------
     How far down the page you are, on the nav's own bottom edge. Pure CSS on a
     scroll timeline: no listener, no layout read, and it cannot drift. */

  @supports (animation-timeline: scroll()) {
    .nav::after {
      content: "";
      position: absolute;
      left: 0; right: 0; bottom: -1px;
      height: 2px;
      transform-origin: left;
      transform: scaleX(0);
      background: linear-gradient(90deg, var(--accent), oklch(0.82 0.13 300));
      animation: progress linear both;
      animation-timeline: scroll(root block);
    }
    @keyframes progress { to { transform: scaleX(1); } }
  }

  /* --- depth on approach ---------------------------------------------------
     The phone drifts up as it enters. The plan card used to do this too and it
     made no sense: that card is something you interact with, and a surface that
     is still settling while you are trying to click into it fights you. */

  @supports (animation-timeline: view()) {
    @media (width >= 720px) {
      .phone {
        animation: float linear both;
        animation-duration: auto;
        animation-timeline: view();
        animation-range: entry 0% cover 55%;
      }
      @keyframes float {
        from { transform: translateY(26px) scale(0.985); }
        to   { transform: none; }
      }

      /* The hero window rises to meet you: tilted back a few degrees on load
         and flat by the time its top third has scrolled into view. Transform
         only, and on the window rather than the figure, so it does not fight
         the figure's one-shot entrance. The one premium move on the page, and
         it belongs to the one hero object. */
      .window {
        transform-origin: 50% 0;
        animation: settle linear both;
        animation-duration: auto;
        animation-timeline: view();
        animation-range: entry 0% cover 40%;
      }
      @keyframes settle {
        from { transform: perspective(1200px) rotateX(6deg) scale(0.97); }
        to   { transform: none; }
      }
    }
  }
}

/* --- scroll-driven: the board, the phone, the reveals ----------------------
   Moved here out of style.css. It has to come after sections.css, which owns
   .lane and .lanes: from style.css these rules lost to it on source order, and
   the travelling card silently stopped laying out. Ordering is the whole reason
   this block lives in this file rather than with the rest of the hero motion. */

/* Scroll-driven: Chrome/Edge 115+, Safari 26+; Firefox keeps the static diagram.
   Nothing is hidden by default, which is why this tests the FUNCTION not the
   property name. */
@supports (animation-timeline: view()) {
  @media (prefers-reduced-motion: no-preference) {
    /* The timeline is named off the BOARD, not off the rail. Scoped to the rail
       it measured the rail's own box crossing the viewport, which on a phone is
       a fifth of the board: the card finished its journey while four lanes were
       still on screen and then sat in Done. On the board it travels for exactly
       as long as the board is passing, which is the thing being described. */
    .board { view-timeline: --travel block; }

    /* Room for the card to sit IN a lane rather than on top of one. Must clear its
       full height or it covers the lane's copy. Only here: else it is a hole. */
    .lane { padding-bottom: 8.75rem; }

    /* The RAIL is one lane and is what moves. translate percentages resolve
       against the ANIMATED element, so the rail has to be exactly one lane on the
       axis it travels; that is why its size is set, and why the two width cases
       below differ only in which axis they set. */
    .rail {
      /* sections.css hides it: the card only exists where it can travel, which
         is also the reduced-motion and no-scroll-timeline state. */
      display: block;
      position: absolute;
      z-index: 2;
      pointer-events: none;
      /* Longhands, NOT the shorthand: it resets animation-duration to 0s, and a
         scroll-driven animation at 0s never advances, it just sits there. */
      animation-duration: auto;
      /* Linear and continuous. I tried stepping it lane to lane, on the theory
         that a ticket hops rather than slides; on a scroll timeline that reads as
         glitchy, because your scroll speed drives it and the dwells make it stall
         and lurch rather than hop. A constant glide tracks the scroll, which is
         what makes it feel attached to your hand. */
      animation-timing-function: linear;
      animation-fill-mode: both;
      animation-timeline: --travel;
      animation-range: cover 8% cover 92%;
    }

    /* One keyframe, five ranges: each status row owns a fifth of the travel, so
       the card changes state lane by lane. Opacity only. */
    .ts { animation: swap linear both; animation-duration: auto; animation-timeline: --travel; }
    .ts[data-s="todo"] { animation-range: cover 0% cover 26%; }
    .ts[data-s="prog"] { animation-range: cover 24% cover 45%; }
    .ts[data-s="pend"] { animation-range: cover 43% cover 62%; }
    .ts[data-s="rev"]  { animation-range: cover 60% cover 79%; }
    .ts[data-s="done"] { animation-range: cover 77% cover 100%; }
    @keyframes swap { 0% { opacity: 0 } 14%, 86% { opacity: 1 } 100% { opacity: 0 } }

    /* The lane the card is currently in lights up, on the same ranges as the
       status rows above. This is the bit that makes the travel legible: without
       it the card slides past five identical boxes. box-shadow and border-color
       rather than transform, because the point is that the LANE reacts. */
    .lane { animation: lane-lit linear both; animation-duration: auto; animation-timeline: --travel; }
    .lane:nth-child(1) { animation-range: cover 0% cover 26%; }
    .lane:nth-child(2) { animation-range: cover 24% cover 45%; }
    .lane:nth-child(3) { animation-range: cover 43% cover 62%; }
    .lane:nth-child(4) { animation-range: cover 60% cover 79%; }
    .lane:nth-child(5) { animation-range: cover 77% cover 100%; }
    @keyframes lane-lit {
      0%, 100% { border-color: var(--edge); background: var(--surface-1); }
      16%, 84% {
        border-color: color-mix(in oklab, var(--hue) 45%, transparent);
        background: color-mix(in oklab, var(--hue) 7%, var(--surface-1));
      }
    }

    /* The progress bar fills as it crosses. scaleX, not width. */
    .tbar b { width: 100%; transform-origin: left; animation: fill linear both;
              animation-duration: auto; animation-timeline: --travel; animation-range: cover 8% cover 92%; }
    @keyframes fill { from { transform: scaleX(0.06) } to { transform: scaleX(1) } }

    /* Timeline named off the PHONE, not view() on the feed: the feed is taller
       than its frame, so its own view range reads as frozen. Not width-gated: a
       feed scrolling inside a phone frame is if anything more at home on a
       phone, and gating it there was why it did nothing on mobile. */
    .phone { view-timeline: --phone block; }
    .phone-feed {
      animation: feed linear both; animation-duration: auto;
      animation-timeline: --phone; animation-range: cover 12% cover 88%;
    }
    @keyframes feed { from { transform: translateY(0) } to { transform: translateY(-42%) } }
  }

  /* Wide: five lanes side by side, so the card crosses them left to right. */
  @media (prefers-reduced-motion: no-preference) and (width >= 720px) {
    .rail {
      bottom: 0.9rem; left: 0;
      width: calc((100% - 4 * 0.7rem) / 5);
      animation-name: travel-x;
    }
    .tcard { margin-inline: 0.9rem; }

    /* n lanes across is n * 100% of the rail plus n gaps, at any width. transform
       only, so it stays on the compositor. */
    @keyframes travel-x {
      from { transform: translate3d(0, 0, 0); }
      to   { transform: translate3d(calc(400% + 2.8rem), 0, 0); }
    }
  }

  /* Narrow: the lanes stack, so the card travels DOWN them instead. Same story,
     same five ranges, same maths on the other axis. grid-auto-rows: 1fr is what
     makes it work -- the pitch has to be constant for 400% to mean four lanes,
     and lane copy is not all the same length. */
  @media (prefers-reduced-motion: no-preference) and (width < 720px) {
    .lanes { grid-template-columns: 1fr; grid-auto-rows: 1fr; }
    .rail {
      top: 0; left: 0; right: 0;
      height: calc((100% - 4 * 0.7rem) / 5);
      animation-name: travel-y;
    }
    /* The rail is a whole lane tall; the card rides at the bottom of it, which is
       the gap the lane's padding-bottom opened up. */
    .tcard { position: absolute; left: 0.9rem; right: 0.9rem; bottom: 0.9rem; }

    @keyframes travel-y {
      from { transform: translate3d(0, 0, 0); }
      to   { transform: translate3d(0, calc(400% + 2.8rem), 0); }
    }
  }

  /* Everything below is axis-independent and belongs at every width. It used to
     sit inside the >= 720px block, which is why nothing revealed on a phone. */
  @media (prefers-reduced-motion: no-preference) {
    /* --- the pipeline runs itself -------------------------------------------
       Continuous and time-based, like the beam around the video, rather than
       driven by scroll position. Scroll-driven made the diagram a thing you
       operated: it only moved while you moved, and it froze the moment you
       stopped reading. This runs on its own, so the pipeline is alive whenever
       you are looking at it.

       A pulse walks the graph on a loop: trigger, then BOTH subtickets at the
       same delay, then the gate. That the middle two share a delay is the claim
       the section makes -- two nodes with no path between them run at once -- so
       they light together rather than left to right. */

    .nodes rect {
      animation: node-pulse 4s ease-in-out infinite;
    }
    .nodes > g:nth-child(2) rect,
    .nodes > g:nth-child(3) rect { animation-delay: 0.8s; }
    .nodes > g:nth-child(4) rect { animation-delay: 1.6s; }
    @keyframes node-pulse {
      0%, 46%, 100% { fill-opacity: 0.16; stroke-opacity: 0.8; stroke-width: 1; }
      14%           { fill-opacity: 0.46; stroke-opacity: 1;   stroke-width: 1.9; }
    }

    /* Work moving along the edges, always. This is what carries the sense of
       flow between pulses; the nodes are the beats, this is the current. */
    .flow path {
      stroke-dasharray: 2.5 13;
      opacity: 0.85;
      animation: flow 1.5s linear infinite;
    }
    @keyframes flow { to { stroke-dashoffset: -15.5; } }

    /* Sections below the fold rise as they enter. Same guard, same longhands. */
    .reveal {
      animation-name: rise-in;
      animation-duration: auto;
      animation-timing-function: linear;
      animation-fill-mode: both;
      animation-timeline: view();
      animation-range: entry 5% entry 60%;
    }
    @keyframes rise-in {
      from { opacity: 0; transform: translateY(1.6rem); }
      to { opacity: 1; transform: none; }
    }
  }
}
