/* ============================================================
   process3d.css — additive depth for the "How we work" path.

   Pure CSS 3D, no WebGL. Layers on top of styles.css and only
   takes effect when ALL of these are true:
     · viewport >= 721px
     · body.has-motion  (main.js adds it only when motion is OK)
     · .p3d-ready       (process3d.js adds it after it measures)
   Remove any one of them and the section falls straight back to
   the original flat design. Nothing here is required for the
   page to work.

   Custom properties written by process3d.js:
     --p3d-rx / --p3d-ry  on .path-3d   → scene tilt
     --p3d-heat           on .path      → 0..1 line glow intensity
     --p3d-enter          on a station  → 0..1 entrance progress
   ============================================================ */

/* The wrapper process3d.js injects around .path's children.
   `display: contents` by default means a wrapper that is present
   but not yet activated (or left over for a frame while tearing
   down to mobile) generates no box at all — children keep
   resolving against .path exactly as they did before. */
.path-3d { display: contents; }


@media (min-width: 721px) {

  /* ---------- 1. the stage ---------- */

  body.has-motion .path {
    perspective: 1400px;
    perspective-origin: 50% 42%;
    --p3d-heat: 0;
    /* opacity of a station number before that station enters */
    --p3d-num-idle: .42;
  }

  /* inset:0 reproduces .path's padding box, so every absolutely
     positioned child (svg, stations, blobs) keeps its old origin. */
  body.has-motion .path > .path-3d {
    display: block;
    position: absolute;
    inset: 0;
    transform-style: preserve-3d;
    transform: rotateX(var(--p3d-rx, 0deg)) rotateY(var(--p3d-ry, 0deg));
    will-change: transform;
  }

  /* The list has to be preserve-3d or it flattens the stations' translateZ.
     But preserve-3d also makes it the containing block for its absolutely
     positioned children, and the <ol> itself is 0px tall — which would
     resolve every station's `top: var(--sy)` against 0 and stack them all
     at the top. inset:0 gives it .path's box back, so --sx/--sy resolve
     against exactly the same rectangle as before. */
  body.has-motion .path .path-stations {
    position: absolute;
    inset: 0;
    transform-style: preserve-3d;
  }


  /* ---------- 2. stations on alternating depth planes ---------- */

  body.has-motion .path .path-station {
    transform-style: preserve-3d;
    --p3d-enter: 0;
    /* odd stations (01, 03, 05) come forward */
    --p3d-z: 70px;
    /* Perspective magnifies by P/(P-z), so cancel it with (1 - z/P),
       P = 1400px. Depth reads as depth, type stays the same size. */
    --p3d-s: .95;
  }
  body.has-motion .path .path-station:nth-child(even) {
    /* even stations (02, 04) sit back */
    --p3d-z: -55px;
    --p3d-s: 1.0393;
  }

  /* Re-declares the base translate from styles.css because transform is a
     single property — the depth is appended, never replacing it. JS only
     ever writes custom properties (--p3d-enter, --p3d-dy), so it composes
     with this rule instead of clobbering it.

     --p3d-dy is measured by JS: a plane at depth z is projected away from
     the perspective origin by d * z/P, which would drag each station's
     hairline connector off its node by up to ~48px at the far ends of a
     2150px-tall path. Translating by -d * z/P cancels that exactly, so the
     scene is pixel-identical to the flat design at rest and the depth
     reveals itself in the tilt parallax and the entrance. */
  body.has-motion .p3d-ready .path .path-station-right {
    transform:
      translateY(-50%)
      translate(0, var(--p3d-dy, 0px))
      translateZ(calc(var(--p3d-z) + (1 - var(--p3d-enter)) * -120px))
      scale(var(--p3d-s));
    will-change: transform;
  }
  body.has-motion .p3d-ready .path .path-station-left {
    transform:
      translate(-100%, -50%)
      translate(0, var(--p3d-dy, 0px))
      translateZ(calc(var(--p3d-z) + (1 - var(--p3d-enter)) * -120px))
      scale(var(--p3d-s));
    will-change: transform;
  }


  /* ---------- 3. layers inside a station ---------- */

  /* The number floats ~40px in front of its own h3/p, so a 3deg tilt
     shears it visibly against the copy. Scale compensated as above. */
  body.has-motion .p3d-ready .path .path-station-num {
    transform: translate(0, var(--p3d-dy-num, 0px)) translateZ(40px) scale(.9714);
    opacity: calc(
      var(--p3d-num-idle) + (1 - var(--p3d-num-idle)) * var(--p3d-enter)
    );
  }

  /* extruded cherry stack, lit consistently from the top-left */
  body.has-motion .p3d-ready .path .path-station.is-active .path-station-num {
    text-shadow:
      1px 1px 0 rgba(194, 37, 50, .28),
      2px 2px 0 rgba(194, 37, 50, .20),
      3px 3px 0 rgba(194, 37, 50, .14),
      4px 4px 0 rgba(194, 37, 50, .085),
      5px 5px 0 rgba(194, 37, 50, .045),
      7px 8px 14px rgba(142, 27, 36, .18);
    transition: text-shadow .6s ease, color .5s ease;
  }


  /* ---------- 4. the line plane, just behind the stations ---------- */

  body.has-motion .path .path-svg {
    transition: transform .7s cubic-bezier(.22, .61, .36, 1);
  }
  body.has-motion .p3d-ready .path .path-svg {
    transform: translate(0, var(--p3d-dy-svg, 0px)) translateZ(-20px) scale(1.0143);
  }

  /* Soft inner-shadow feel: a tight dark seat under the hairline plus a
     wide ivory bloom around it, which reads as a groove in the paper. */
  body.has-motion .path .path-line-bg {
    stroke-opacity: .9;
    filter:
      drop-shadow(0 1px 1px rgba(33, 27, 22, .10))
      drop-shadow(0 0 5px rgba(246, 241, 231, .95));
  }

  /* --p3d-heat rises with scroll velocity, i.e. with how fast the
     travelling dot is moving, so the drawn line glows hottest right
     where the dot is working. */
  body.has-motion .path .path-line {
    filter: drop-shadow(
      0 0 calc(3px + 8px * var(--p3d-heat, 0))
      rgba(194, 37, 50, calc(.30 + .45 * var(--p3d-heat, 0)))
    );
  }
  body.has-motion .path .path-line-hot {
    filter:
      drop-shadow(0 0 7px rgba(216, 64, 78, .85))
      drop-shadow(
        0 0 calc(6px + 14px * var(--p3d-heat, 0))
        rgba(216, 64, 78, calc(.25 + .50 * var(--p3d-heat, 0)))
      );
  }


  /* ---------- 5. the travelling dot ---------- */

  body.has-motion .path .path-dot {
    filter: drop-shadow(0 0 6px rgba(194, 37, 50, .55));
  }

  /* fill-box + centre origin so the halo scales around the circle
     itself rather than around the SVG user-space origin (0,0).
     No `filter` here on purpose: that would override the element's
     filter="url(#dotGlow)" attribute and kill the gaussian blur. */
  body.has-motion .path .path-dot-glow {
    transform-box: fill-box;
    transform-origin: 50% 50%;
    animation: p3d-dot-pulse 2.4s ease-in-out infinite alternate;
  }
  @keyframes p3d-dot-pulse {
    from { transform: scale(1);    opacity: .35; }
    to   { transform: scale(1.35); opacity: .12; }
  }


  /* ---------- 6. background blobs, parallaxed by depth ---------- */

  /* Each blob carries its own translateZ inside the keyframes, so the
     drift and the depth stay in one transform. Scales are pre-multiplied
     by (1 - z/P) so the far blob still covers the same area. */
  body.has-motion .p3d-ready .path .glow-blob-a {
    animation: p3d-blob-a 26s ease-in-out infinite alternate;
  }
  body.has-motion .p3d-ready .path .glow-blob-b {
    animation: p3d-blob-b 17s ease-in-out infinite alternate;
    animation-delay: -6s;
  }
  @keyframes p3d-blob-a {
    from { transform: translate3d(0, -4%, -260px) scale(1.186); }
    to   { transform: translate3d(7%, 8%, -260px) scale(1.40); }
  }
  @keyframes p3d-blob-b {
    from { transform: translate3d(0, -2%, -120px) scale(1.086); }
    to   { transform: translate3d(-5%, 6%, -120px) scale(1.28); }
  }
}


/* ============================================================
   Reduced motion — neutralise everything above.
   Selectors mirror the ones in the block above and this rule set
   comes last, so it wins on source order without !important.
   ============================================================ */
/* Scoped to the same width range the 3D rules live in — otherwise these
   neutralisers outrank the mobile vertical-list layout in styles.css. */
@media (min-width: 721px) and (prefers-reduced-motion: reduce) {

  body.has-motion .path {
    perspective: none;
    perspective-origin: 50% 50%;
  }
  body.has-motion .path > .path-3d {
    display: contents;
    transform: none;
    will-change: auto;
  }
  body.has-motion .path .path-stations {
    position: static;
    inset: auto;
    transform-style: flat;
  }
  body.has-motion .p3d-ready .path .path-station-right {
    transform: translateY(-50%);
    will-change: auto;
  }
  body.has-motion .p3d-ready .path .path-station-left {
    transform: translate(-100%, -50%);
    will-change: auto;
  }
  body.has-motion .p3d-ready .path .path-station-num {
    transform: none;
    opacity: 1;
  }
  body.has-motion .p3d-ready .path .path-station.is-active .path-station-num {
    text-shadow: none;
    transition: none;
  }
  body.has-motion .path .path-svg,
  body.has-motion .p3d-ready .path .path-svg {
    transform: none;
    transition: none;
  }
  body.has-motion .path .path-line-bg,
  body.has-motion .path .path-line,
  body.has-motion .path .path-dot {
    filter: none;
  }
  body.has-motion .path .path-line-hot {
    filter: drop-shadow(0 0 7px rgba(216, 64, 78, .85));
  }
  body.has-motion .path .path-dot-glow {
    animation: none;
    transform: none;
    opacity: .35;
  }
  body.has-motion .p3d-ready .path .glow-blob-a,
  body.has-motion .p3d-ready .path .glow-blob-b {
    animation: none;
    transform: none;
  }
}
