/*
Page background. It runs across the whole site and belongs to no single block, so it lives
in its own file rather than in hero.css.

WHAT IT IS. The reference render is a caustic: the bright thread in it is not a drawn line
but the fold of a smoothly warped field of light, which is why it has a hard crest and soft
flanks at the same time. Three rounds of this background were built from gradients and all
three read as blurred smears, because a gradient is a monotone ramp and can only fade away
evenly on both sides. No choice of stops turns one into a thread.

HOW IT IS BUILT INSTEAD.
  1. The source is two families of soft parallel bands of light, plain
     repeating-linear-gradients at an angle to each other (--aurora-weave-*).
  2. The wrapper around them carries an SVG filter: feTurbulence (fractalNoise) feeding
     feDisplacementMap. It warps the bands. Where the warp compresses a band, its long soft
     ramp is squeezed into a thin hard-edged thread; where it stretches one, the band opens
     into a broad faint wash. Crest and flank come out of the same surface — which is the
     caustic relationship itself, not an imitation of it.
  3. The noise stands still and the bands drift underneath it.

Point 3 is the reason for the nesting. The filter is on .page-bg__field and the animation
on .page-bg__weave inside it. A transform on the filtered element itself would move the
finished picture, warp and all, and the effect would slide about as one rigid sheet.
Animating the filter instead — baseFrequency, or the displacement scale — would rebuild the
turbulence field on every frame, which is the most expensive thing this filter can do;
moving the source only re-warps a field that has already been computed. Measured on the
stand at 1920x900, dpr 2: turbulence and displacement together are free, holding the same
120fps as no filter at all, and the whole thing including the blur runs at 100.

Raster is still forbidden here, for the same reason as before: the step between a thread
and the canvas is a few levels per channel, so any image would band, and the site has to
stay light. The grain layer below is what handles the banding instead.

Values were tuned on an isolated stand, design/preview/aurora.html, against the clean
background exported from the design (design/screenshots/aurora-ref-src.jpg): an effect this
faint cannot be judged with content sitting on top of it. The stand stays in the repository
as the place where this background is edited and reviewed on its own.
*/

/*
The canvas lives on body: the aurora only covers the top of the page, and below it the
background must stay flat no matter how long the document is.
*/
body {
  position: relative;
  background: var(--page-bg);
}

/*
Absolute, not fixed: the aurora belongs to the top of the document and has to scroll away
with it. Fixed would pin it to the viewport and it would follow the whole site.

The layer is exactly viewport-wide; the slack for the warp and the drift is pushed inside,
onto .page-bg__weave. Negative left/right here would widen the scrollable area: an absolute
element takes no part in layout, but it does create horizontal scroll, and the document
would grow wider than the screen by the amount of that slack.
*/
.page-bg {
  position: absolute;
  z-index: -1;
  top: 0;
  right: 0;
  left: 0;
  height: var(--aurora-h);
  overflow: hidden;
  pointer-events: none;

  /*
  The bottom edge has to dissolve rather than stop: without the mask there is a flat
  horizontal seam across the full width, exactly where the container ends.
  */
  mask-image: linear-gradient(to bottom, #000 0%, #000 44%, rgb(0 0 0 / 0) 100%);
}

/* The warped surface. Everything inside it is source material for the filter. */
.page-bg__field {
  position: absolute;
  inset: 0;
  filter: url(#page-bg-caustics);
}

.page-bg__weave {
  position: absolute;
  inset: calc(var(--aurora-pad) * -1);
  display: block;
  will-change: transform;
}

/*
The first family carries the canvas colour as well, so the filter has an opaque surface to
warp. With a transparent source the displacement pulls emptiness in from beyond the edge
and the frame ends in torn holes rather than in colour.
*/
.page-bg__weave--1 {
  background:
    repeating-linear-gradient(
      var(--aurora-weave-1-angle),
      rgb(var(--aurora-ink) / 0) 0,
      rgb(var(--aurora-ink) / calc(var(--aurora-weave-1-a) * 0.05)) var(--aurora-halo-in),
      rgb(var(--aurora-ink) / calc(var(--aurora-weave-1-a) * 0.22)) var(--aurora-knee-in),
      rgb(var(--aurora-ink) / var(--aurora-weave-1-a)) var(--aurora-core-in),
      rgb(var(--aurora-ink) / var(--aurora-weave-1-a)) var(--aurora-core-out),
      rgb(var(--aurora-ink) / calc(var(--aurora-weave-1-a) * 0.16)) var(--aurora-knee-out),
      rgb(var(--aurora-ink) / calc(var(--aurora-weave-1-a) * 0.04)) var(--aurora-halo-out),
      rgb(var(--aurora-ink) / 0) var(--aurora-period)
    ),
    var(--page-bg);
  background-position:
    calc(var(--aurora-pad) + var(--aurora-weave-1-phase-x))
    calc(var(--aurora-pad) + var(--aurora-weave-1-phase-y));

  animation: page-bg-drift-1 var(--aurora-dur-1) linear infinite;
}

.page-bg__weave--2 {
  background:
    repeating-linear-gradient(
      var(--aurora-weave-2-angle),
      rgb(var(--aurora-ink) / 0) 0,
      rgb(var(--aurora-ink) / calc(var(--aurora-weave-2-a) * 0.05)) var(--aurora-halo-in),
      rgb(var(--aurora-ink) / calc(var(--aurora-weave-2-a) * 0.22)) var(--aurora-knee-in),
      rgb(var(--aurora-ink) / var(--aurora-weave-2-a)) var(--aurora-core-in),
      rgb(var(--aurora-ink) / var(--aurora-weave-2-a)) var(--aurora-core-out),
      rgb(var(--aurora-ink) / calc(var(--aurora-weave-2-a) * 0.16)) var(--aurora-knee-out),
      rgb(var(--aurora-ink) / calc(var(--aurora-weave-2-a) * 0.04)) var(--aurora-halo-out),
      rgb(var(--aurora-ink) / 0) var(--aurora-weave-2-period)
    );
  background-position:
    calc(var(--aurora-pad) + var(--aurora-weave-2-phase-x))
    calc(var(--aurora-pad) + var(--aurora-weave-2-phase-y));

  animation: page-bg-drift-2 var(--aurora-dur-2) linear infinite;
}

/*
transform moves, not background-position. Sliding the pattern by its own period would be
the tidier way to drift a repeating gradient — a tiling pattern has no edges, so the layer
could stay small — but the browser has to repaint the gradient on every frame to do it. On
the stand that costs 75fps against 120 for the transform. The transform is composited, and
the price for it is the oversized layer that --aurora-pad pays for.

Travel is one full period along the normal of the bands, so the last frame of the loop is
the first one and it can run linear with no turn. The multipliers are that normal: for a
gradient at A degrees it is (sin A, -cos A). 155deg gives (0.423, 0.906) and 42deg gives
(0.669, -0.743). They are written out because a keyframe cannot compute a sine — change an
angle token and these have to change with it.
*/
@keyframes page-bg-drift-1 {
  from {
    transform: translate3d(0, 0, 0);
  }

  to {
    transform: translate3d(
      calc(var(--aurora-period) * 0.423),
      calc(var(--aurora-period) * 0.906),
      0
    );
  }
}

@keyframes page-bg-drift-2 {
  from {
    transform: translate3d(0, 0, 0);
  }

  to {
    transform: translate3d(
      calc(var(--aurora-weave-2-period) * 0.669),
      calc(var(--aurora-weave-2-period) * -0.743),
      0
    );
  }
}

/*
Grain, against banding. It is a separate static layer rather than another primitive in the
filter chain: this way it is rasterised once, instead of being recomputed on every frame of
the drift. It sits outside .page-bg__field for the same reason — inside, the warp would
smear it into streaks and it would stop being noise.
*/
.page-bg__grain {
  position: absolute;
  inset: 0;
  display: block;
  opacity: var(--aurora-grain);
  background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='160' height='160'%3E%3Cfilter id='n'%3E%3CfeTurbulence type='fractalNoise' baseFrequency='0.85' numOctaves='2' stitchTiles='stitch'/%3E%3C/filter%3E%3Crect width='160' height='160' filter='url(%23n)'/%3E%3C/svg%3E");
}

/*
The filter definitions are markup, not paint: they must take no part in layout and must not
be reachable. They are in header.php because filter: url() cannot point into another
document.
*/
.page-bg__defs {
  position: absolute;
  width: 0;
  height: 0;
  overflow: hidden;
}

/*
Below 900 the bands are drawn about twice as fine (see tokens.css), and the warp has to be
scaled with them. Filter primitives take plain numbers, so the second scale is a second
filter rather than a media query over custom properties.
*/
@media (max-width: 900px) {
  .page-bg__field {
    filter: url(#page-bg-caustics-narrow);
  }
}

/* Movement stops, the threads stay: without motion the background must look the same. */
@media (prefers-reduced-motion: reduce) {
  .page-bg__weave {
    transform: none;
    animation: none;
    will-change: auto;
  }
}
