/* The 3D layer, and the small amount of page motion that goes with it.

   styles.css is untouched: everything here is additive and every rule that
   changes the page is behind body.has3d (WebGL is running) or body.cards3d (the
   CSS card layer is running). Delete this file and the page is the site as
   shipped.

   Layering, which is the whole idea:
     #rd3d-back    z-index -1   over the body background, under every word
     page content  z-index auto
     #rd3d-front   z-index  4   over the content, under the nav
     .nav                  90
     .sheet               100                                                 */

.rd3d {
  position: fixed;
  inset: 0;
  width: 100%;
  height: 100%;
  display: block;
  pointer-events: none;
  contain: strict;
}

/* Until WebGL is confirmed there is nothing to show, so nothing is reserved.
   This is why a browser that cannot run the scene gets the original page rather
   than the original page with holes in it. */
.stage3d { display: none; }
body.has3d .stage3d { display: block; }

/* ------------------------------------------------------------------ the stack */

/* Copy on one side, the deck opening on the other. The split only exists when
   the canvas does: without WebGL there is no stack, and a two column grid with
   an empty right hand cell is worse than the page as it shipped. */
.stack-split { display: grid; gap: clamp(22px, 3vw, 56px); }

@media (min-width: 981px) {
  body.has3d .stack-split {
    grid-template-columns: minmax(0, 0.86fr) minmax(0, 1.14fr);
    align-items: center;
  }
  body.has3d .stack-split .stack-stage { margin: 0; }
}

.stack-stage {
  position: relative;
  /* The deck is sized by whichever of the box's two sides binds first, and
     beside the copy that is always the width: at 1440 the box was 614 wide and
     630 tall, and only the 614 was doing anything. The other 130px was empty
     paper under the slabs, which is the same "you cannot see what is coming
     next" note the phone got, one screen further up the page. Anything at or
     above 482px leaves the deck at exactly the size it already is. */
  height: 56vh;
  min-height: 480px;
  max-height: 600px;
  margin: 18px 0 6px;
}

/* The floor the stack sits on. It has to paint below the canvas, so it is a
   negative z-index child of a section that is not itself a stacking context,
   which puts it in the root stacking context underneath -1. */
.stack-stage::before {
  content: "";
  position: absolute;
  z-index: -2;
  left: 50%;
  top: 8%;
  width: min(1180px, 116%);
  height: 92%;
  transform: translateX(-50%);
  border-radius: 50% 50% 42% 42% / 34% 34% 66% 66%;
  background:
    radial-gradient(ellipse 62% 46% at 50% 62%, rgba(124, 26, 252, 0.13), rgba(124, 26, 252, 0) 70%),
    radial-gradient(ellipse 78% 60% at 50% 42%, var(--paper-3), rgba(242, 239, 250, 0) 72%);
  pointer-events: none;
}

/* A thin horizon, so the slabs look like they are standing on something. */
.stack-stage::after {
  content: "";
  position: absolute;
  z-index: -2;
  left: 50%;
  bottom: 12%;
  width: min(820px, 88%);
  height: 1px;
  transform: translateX(-50%);
  background: linear-gradient(90deg, transparent, var(--line-2) 22%, var(--line-2) 78%, transparent);
  pointer-events: none;
}

/* The flat diagram underneath says the same six things the stack says, and
   both on screen at once is one argument made twice. It stays in the document,
   because a reader on a screen reader is not looking at the canvas and the
   canvas carries no text.

   Only where the two are actually both on screen. The split is the reason this
   works: at 981px and up the claim stands next to the deck and the deck is the
   diagram. Below that the split collapses, the deck loses the copy it was
   opening beside, and the labels on its faces are a few millimetres tall, so
   hiding this left the section on a phone with a picture and no words. It is
   the words. Below the split it comes back, and the stack goes back to being
   what it is at that size: the illustration above them. */
@media (min-width: 981px) {
  body.has3d .engine-flat {
    position: absolute;
    width: 1px;
    height: 1px;
    margin: 0;
    padding: 0;
    overflow: hidden;
    clip-path: inset(50%);
    white-space: nowrap;
  }
}

/* -------------------------------------------------------------- the 3D cards */

/* The site's own cards, given a third axis. One shared perspective per grid
   rather than one per card, so a row reads as a row of physical panels seen
   from one place instead of three unrelated tilts.

   overflow on .card is hidden in styles.css, and any overflow other than
   visible forces transform-style back to flat, which would silently kill every
   translateZ below. So it is turned off here and the radial glow, which was the
   reason for the clip, gets the radius back instead. */
/* 1800px is a very long lens. The row curve below turns the outer columns by
   six degrees and stands them 46px back, and at 1800 that is a two per cent
   change in width: arithmetically present, invisible on a screen. This is the
   number that decides whether any of it can be seen. */
body.cards3d .grid-svc,
body.cards3d .grid-3 {
  perspective: 1150px;
  perspective-origin: 50% 42%;
}

body.cards3d .grid-svc .card,
body.cards3d .grid-3 .card {
  transform-style: preserve-3d;
  overflow: visible;
  backface-visibility: hidden;
  /* transform is driven a frame at a time from script, so it must not also be
     transitioned here or every frame would be chasing the last one. */
  transition: box-shadow 0.4s var(--ease), border-color 0.4s var(--ease);
}
body.cards3d .grid-svc .card::after,
body.cards3d .grid-3 .card::after { border-radius: inherit; }

/* What a reader actually reads as depth. The tilt on its own is a picture of a
   card at an angle; the shadow underneath it moving as it rises is the part
   that says the card left the paper.

   It is a pseudo element with a fixed shadow and a moving opacity, not a
   box-shadow on the card recomputed every frame. A box-shadow that changes
   repaints the card, and repainting a card repaints the type on it, which is
   the flicker this whole pass is here to remove. Opacity is composited: the
   card is never touched. translateZ puts it behind the card in the same 3D
   space rather than relying on a z-index that preserve-3d ignores.

   Everywhere except the phone, and not because a phone would not benefit:
   below 560px the same pseudo element is the dot on the rail that threads the
   six cards together, and that is the more useful of the two things one
   ::before can be. 801 was the wrong edge for that swap. The grid collapses to
   one column at 940, the rail starts at 560, so between them sat a band of
   widths with a single column of tilting cards and nothing underneath them:
   the tilt without the shadow, which is the half of the effect that does not
   read. 561 is where the dot stops needing the pseudo element. */
@media (min-width: 561px) {
  body.cards3d .grid-svc .card::before,
  body.cards3d .grid-3 .card::before {
    content: "";
    position: absolute;
    inset: 10% 7% -1% 7%;
    border-radius: inherit;
    pointer-events: none;
    transform: translateZ(-6px);
    box-shadow: 0 26px 50px -22px rgba(26, 10, 58, 0.55);
    opacity: var(--zl, 0);
  }
}

/* The parent reveal already slides the whole grid up. The cards now arrive one
   at a time on their own, so the group move goes and the fade stays. */
body.cards3d .grid-svc.reveal,
body.cards3d .grid-3.reveal { transform: none; }

/* What actually says depth. The tilt on its own is a picture of a card
   rotating; the parallax between the icon, the heading and the link as it
   rotates is the part the eye reads as thickness. */
body.cards3d .grid-svc .card .svc-icon { transform: translateZ(38px); }
body.cards3d .grid-svc .card h3,
body.cards3d .grid-3 .card h3 { transform: translateZ(22px); }
body.cards3d .grid-svc .card p,
body.cards3d .grid-3 .card p { transform: translateZ(9px); }
body.cards3d .card .go { transform: translateZ(30px); }

/* ------------------------------------------------------------- page movement */

/* The reveal already on the site, given a small amount of depth. Four degrees is
   deliberately under the threshold where a reader notices a rotation and over
   the one where they notice its absence. */
body.has3d .reveal {
  transform: translate3d(0, 26px, 0) rotateX(4deg);
  transform-origin: 50% 0;
  transition: opacity 0.8s var(--ease), transform 0.9s var(--ease);
}
body.has3d .reveal.in { transform: none; }

/* Cards outside the 3D set still lift, they just do it flat. */
body.has3d .card {
  transition: transform 0.5s var(--ease), border-color 0.35s var(--ease),
              box-shadow 0.5s var(--ease);
}
body.has3d a.card:hover {
  transform: translate3d(0, -5px, 0) rotateX(1.6deg);
  box-shadow: 0 26px 60px -34px rgba(20, 16, 28, 0.42);
}

/* The ticker gets a shallow perspective so the band reads as a belt turning
   through the page rather than as text sliding sideways. */
body.has3d .ticker { perspective: 900px; }
body.has3d .ticker-track { transform: rotateX(7deg); transform-origin: 50% 50%; }

/* --------------------------------------------------------------- small screens */

/* One column, so the page has no side margins left to park anything in and the
   cards have no row to arrive as. The stack and the footer mark stay, because
   they are the two things that are the point. */
@media (max-width: 800px) {
  /* 62vh was a stage sized for a deck that has copy beside it. With the copy
     back underneath, the deck is a header illustration and the box only has to
     hold it. The deck is sized off the screen width here, not off this box, so
     the box can be cut to what the slabs actually paint: 255px at 390 wide,
     plus a little air. Anything taller than that is empty paper, which is what
     reads as the section trailing off into nothing. */
  .stack-stage { height: 34vh; min-height: 260px; max-height: 300px; margin: 6px 0 0; }
  .stack-stage::before { top: 2%; height: 96%; }
  .stack-split { gap: 18px; }
  body.has3d .engine-flat { margin-top: 34px !important; }

  body.has3d .ticker-track { transform: none; }

  /* One column, so the row curve zeroes itself and what is left is the drum:
     the card in the middle of the screen stands proud and faces the reader, the
     ones on their way out turn away from them. A shorter lens than the desktop
     one, because the column is narrower and the same degrees have less width to
     act on. */
  body.cards3d .grid-svc,
  body.cards3d .grid-3 { perspective: 820px; perspective-origin: 50% 50%; }

  /* And the card moves as one panel, not as four parts of one. Every child's
     translateZ is measured from the grid's perspective origin, and the grid on
     a phone is a single very tall column, so a card near the top or the bottom
     of it has its icon, its heading and its link pushed by visibly different
     amounts. The parallax that reads as thickness across a wide three column
     grid reads as a card coming apart in a narrow one. */
  body.cards3d .grid-svc .card .svc-icon,
  body.cards3d .grid-svc .card h3,
  body.cards3d .grid-3 .card h3,
  body.cards3d .grid-svc .card p,
  body.cards3d .grid-3 .card p,
  body.cards3d .card .go { transform: none; }

  /* A phone scrolls a screen in a flick, and a block that takes nine tenths of
     a second to arrive is still arriving when the reader is already looking at
     where it should be. That wait is the white space at the bottom of the
     screen: not padding, which is 54px here, but the next thing not being drawn
     yet. Shorter, and a shorter distance to travel. */
  body.has3d .reveal {
    transform: translate3d(0, 14px, 0);
    transition: opacity 0.42s var(--ease), transform 0.48s var(--ease);
  }
  body.has3d .reveal.d1 { transition-delay: 0.04s; }
  body.has3d .reveal.d2 { transition-delay: 0.08s; }
  body.has3d .reveal.d3 { transition-delay: 0.12s; }
}

/* -------------------------------------------------------------------- opt out */

@media (prefers-reduced-motion: reduce) {
  body.has3d .reveal { transform: none; transition: opacity 0.4s linear; }
  body.has3d .ticker-track { transform: none; }
  body.has3d a.card:hover { transform: none; }
  body.cards3d .grid-svc,
  body.cards3d .grid-3 { perspective: none; }
  body.cards3d .card .svc-icon,
  body.cards3d .card h3,
  body.cards3d .card p,
  body.cards3d .card .go { transform: none; }
}

/* ------------------------------------------------------------- tinted bands */

/* A band with a colour paints over the back canvas and the slabs vanish, so the
   one section that hosts 3D paints its tint from a negative z-index child
   instead. The section is position:relative with z-index auto, which is not a
   stacking context, so that child lands in the root stacking context below the
   back canvas at -1 rather than inside the section.

   This only works for something the back canvas is allowed to sit in front of.
   The footer is opaque ink by design, so the mark that floats over it goes on
   the front canvas instead. */
body.has3d .tint3d { background: transparent; position: relative; }
body.has3d .tint3d::before {
  content: "";
  position: absolute;
  inset: 0;
  z-index: -3;
  background: var(--paper-2);
  pointer-events: none;
}

/* ------------------------------------------------- a line the reader fills in

   The stylesheet draws both of these lines as a switch: a class arrives, a 0.7s
   transition runs, the line is full. Nobody sees it. On a phone the flick that
   brings the section on screen is still moving when the animation has finished,
   which is the whole of "the next step goes too quickly".

   scene.js turns them into a read-out instead: the fill follows a line two
   thirds down the viewport and takes exactly as long as the reader takes. These
   rules are what it needs in order to take over. Everything is gated on
   .threaded and .railed, classes only the script adds, so with no JavaScript and
   under prefers-reduced-motion the stylesheet's own behaviour is untouched. */

/* The script writes scaleY every frame. A transition on top of that is a frame
   of lag on every one of them. */
.flow.threaded .fline { transition: none; }

/* .in still arrives, from the site's own observer, and it lights the dot the
   moment the step is barely on screen. That is the early lighting the fill is
   supposed to replace, so for a threaded flow it goes back to being nothing.
   The specificity here is deliberate: .in has to lose to .lit, and the ink
   panel's own pair has to lose to both. */
.flow.threaded .flow-step.in .fdot {
  background: #fff;
  border-color: var(--line);
  box-shadow: none;
}
.flow.threaded .fdot {
  transition: background 0.3s var(--ease), border-color 0.3s var(--ease);
}
.flow.threaded .flow-step .fdot.lit,
.flow.threaded .flow-step.in .fdot.lit {
  background: var(--brand);
  border-color: var(--brand);
  box-shadow: 0 0 0 6px var(--brand-soft);
}
.panel-ink .flow.threaded .flow-step.in .fdot {
  background: var(--ink);
  border-color: rgba(255, 255, 255, 0.25);
  box-shadow: none;
}
.panel-ink .flow.threaded .flow-step .fdot.lit,
.panel-ink .flow.threaded .flow-step.in .fdot.lit {
  background: #B58CFF;
  border-color: #B58CFF;
  box-shadow: 0 0 0 6px rgba(181, 140, 255, 0.16);
}

/* The pulse. It ends on exactly the resting value of the dot it belongs to, so
   there is no snap back when the animation hands the property over. */
@keyframes rd-ping {
  0% { box-shadow: 0 0 0 0 rgba(124, 26, 252, 0.5); }
  70% { box-shadow: 0 0 0 12px rgba(124, 26, 252, 0); }
  100% { box-shadow: 0 0 0 6px var(--brand-soft); }
}
@keyframes rd-ping-ink {
  0% { box-shadow: 0 0 0 0 rgba(181, 140, 255, 0.55); }
  70% { box-shadow: 0 0 0 12px rgba(181, 140, 255, 0); }
  100% { box-shadow: 0 0 0 6px rgba(181, 140, 255, 0.16); }
}
@keyframes rd-ping-rail {
  0% { box-shadow: 0 0 0 0 rgba(124, 26, 252, 0.5); }
  70% { box-shadow: 0 0 0 10px rgba(124, 26, 252, 0); }
  100% { box-shadow: 0 0 0 4px rgba(124, 26, 252, 0.16); }
}
.flow.threaded .fdot.ping { animation: rd-ping 0.6s var(--ease); }
.panel-ink .flow.threaded .fdot.ping { animation: rd-ping-ink 0.6s var(--ease); }
.grid-svc.railed .sr-dot.ping { animation: rd-ping-rail 0.6s var(--ease); }

/* The rail itself lives above 560 as an empty absolute box that paints nothing,
   because a phone turned sideways in the middle of a session should not have to
   rebuild it. */
.svc-rail { display: none; }

@media (max-width: 560px) {
  /* preserve-3d and a hidden backface each force the card onto its own
     composited layer, and a layer is rasterised once, at the size it had when
     it was made. Bring it forward with translateZ and every letter on it is a
     bitmap being magnified: the blur, and it is worst in the middle of the
     screen, where the bow is at its maximum and the card is being read. Neither
     property buys anything here, because every child of the card is already
     flat on a phone. scene.js cuts the bow from 40px to 18 at the same time. */
  body.cards3d .grid-svc .card,
  body.cards3d .grid-3 .card {
    transform-style: flat;
    backface-visibility: visible;
  }

  /* Once the script has put real dots on the grid, the ones hanging off the
     cards go. A card is the one thing here that moves in three dimensions, so a
     dot parented to it swings off the line every time the drum turns, and that
     is the dots moving with the animation while the line does not. */
  body.cards3d .grid-svc.railed .card::before { display: none; }

  /* And the rail stops being the line and becomes the space the line will fill,
     the same division of labour the flow already has between .fbg and .fline. */
  .grid-svc.railed::before {
    background: var(--line);
    -webkit-mask-image: none;
    mask-image: none;
    top: var(--rail-top, 0);
    bottom: auto;
    height: var(--rail-h, 100%);
  }

  .svc-rail {
    display: block;
    position: absolute;
    inset: 0;
    pointer-events: none;
  }
  .sr-fill {
    position: absolute;
    left: 7px;
    width: 2px;
    border-radius: 2px;
    background: var(--brand);
    transform: scaleY(0);
    transform-origin: 50% 0;
  }
  /* box-sizing is border-box site wide, so a 10px dot is 10px across with its
     ring inside it. left 3 puts its middle on 8, which is the middle of the
     2px line at left 7. The dot that shipped was 2px off it. */
  .sr-dot {
    position: absolute;
    left: 3px;
    width: 10px;
    height: 10px;
    border-radius: 50%;
    background: var(--paper);
    border: 2px solid var(--line);
    transition: background 0.3s var(--ease), border-color 0.3s var(--ease);
  }
  .sr-dot.lit {
    background: var(--brand);
    border-color: var(--brand);
    box-shadow: 0 0 0 4px rgba(124, 26, 252, 0.16);
  }
}
