/* ==========================================================================
   vivaocean-theme / scene
   --------------------------------------------------------------------------
   The water. This is the layer the repository exists for, and it is kept in
   its own directory beside shell/ for the same reason the shell is not
   filed under components/: it is not a garnish on the theme, it is half of
   what you are taking.

   WHAT IS HERE. A depth gradient painted on the body, a set of sunbeams,
   and three schools of fish at three depths on three clocks. That is the
   whole scene. There is no photograph, no canvas, no WebGL, and no
   JavaScript: every moving thing below is a CSS animation on an inline SVG
   shape, and the file you are reading is the entire implementation.

   There is not one color literal in this file. Every color, every duration
   and every geometry value is a var() reference to tokens.css. The bare
   numbers that remain are three kinds and nothing else: the 100% loop
   distance (structural, explained at the keyframes), the gradient stop
   offsets, and the 1023px breakpoint in one media query, which cannot be a
   custom property because media query conditions do not read them.

   THE GROUND AXIS. Per class decision 14, the ground is a slot and the
   mechanism is an attribute on the body that only the script ever touches:

     body[data-scene="calm"]   a two-stop gradient and nothing else. This is
                               also what you get with no attribute at all,
                               so the no-JS document carries it by plain CSS
     body[data-scene="ocean"]  four stops of depth, the sunbeams, and the
                               schools

   The attribute is named data-scene rather than the pilot's data-wallpaper
   because nothing here is a wallpaper. A wallpaper is behind the work; this
   is the work.

   Two variants, not three, per class decision 16. The enhanced default is
   ocean, which is the opposite of the pilot's choice and is allowed to be:
   the pilot's scene is a still life and its quiet field loses little, while
   a VivaOcean that opens on a flat gradient has not shown you the product.

   CONTRAST BY CONSTRUCTION, per class decision 15. The scene may paint only
   with these tokens: the four water stops, --vo-ray at --vo-ray-step, and
   the two fish fills. The lightest paint that set can produce is a sunbeam
   core falling across the deep fish fill, and ground ink on it measures
   4.94:1. That is the worst text-over-water pairing anywhere in the theme
   and it clears AA.

   The resolved value of that worst-case blend is quoted in tokens.css and
   deliberately not repeated here. It was, once, and the two copies had
   already disagreed by the time an audit grepped for colour literals
   outside the token file: the number is a consequence of three tokens, so
   it belongs beside them and nowhere else.

   Two construction rules keep the ceiling true rather than approximately
   true:

     1. NO TWO BEAMS OVERLAP. Beams are built to composite WITHIN
        themselves, three steps to a soft edge, and the arithmetic that
        produces 0.1426 assumes exactly three. Two beams crossing would make
        it six and put a patch of water lighter than anything measured onto
        the screen. The five beams are laid out with a gap between every
        adjacent pair at both the top and the bottom of the frame, and all
        fifteen shapes live in one group, so they sway together and their
        spacing is fixed by construction rather than by luck. If you add a
        sixth, check its gaps at both ends.
     2. NEITHER FISH FILL IS LIGHTER THAN THE WATER ABOVE IT. The shallow
        schools are darker than the water they swim in and the deep school
        is lighter than the water it swims in, so a fish can never be the
        lightest thing on screen, with or without a beam across it.

   No shipped variant needs a scrim. The class guarantee for a variant that
   wants lighter water stands: a token-driven scrim between scene and
   content, with every ratio re-measured through it.

   Requires tokens.css. Load it first:

     <link rel="stylesheet" href="tokens/tokens.css" />
     <link rel="stylesheet" href="scene/scene.css" />
   ========================================================================== */

/* --------------------------------------------------------------------------
   THE WATER ITSELF
   The body is the ocean. The gradient is on the body rather than on a fixed
   layer so that the no-JS document, which has no scene markup showing at
   all, is still underwater.
   -------------------------------------------------------------------------- */
.vo-water {
  margin: 0;
  min-height: 100vh;
  min-height: 100dvh;

  /* The calm variant, and the default with no attribute at all. Two stops:
     lit at the top, dark at the bottom, nothing in between claiming to be
     depth. */
  background-color: var(--vo-water-abyss);
  background-image: linear-gradient(
    180deg,
    var(--vo-water-surface),
    var(--vo-water-abyss)
  );
  /* Deliberately NOT background-attachment: fixed. Live, the body is
     viewport-height and does not scroll, so the gradient is the screen. With
     no script the page scrolls and the gradient stretches over the whole
     document instead, which means reading the page top to bottom is a
     descent. That is a better answer than pinning it, and it costs nothing:
     a fixed attachment on a long scrolling document is a repaint on every
     frame of the scroll, for a picture that would then never change. */

  color: var(--vo-ground-ink);
  font-family: var(--vo-font-sans);
  font-size: var(--vo-text-body);
  line-height: var(--vo-leading-body);
}

/* Four stops, and the interval between them is the illusion. Light falls
   off fast in water, so the top two sit closer together than the bottom
   two; an evenly spaced ramp reads as a swatch. */
body[data-scene="ocean"] {
  background-image: linear-gradient(
    180deg,
    var(--vo-water-surface) 0%,
    var(--vo-water-mid) 30%,
    var(--vo-water-deep) 62%,
    var(--vo-water-abyss) 100%
  );
}

/* --------------------------------------------------------------------------
   THE SCENE LAYER
   Inline SVG in the page markup, because an SVG loaded through an image or
   a data URI is an isolated document that cannot read custom properties.
   Inlined, every paint rule below reaches it, and a re-skin of tokens.css
   re-skins the ocean along with everything else.

   Fixed and pointer-transparent: the water is behind the site, never in
   front of it, and it never eats a click.
   -------------------------------------------------------------------------- */
.vo-scene {
  display: none;
  position: fixed;
  inset: 0;
  z-index: var(--vo-z-scene);
  pointer-events: none;
  overflow: hidden;
}

.vo-scene > svg {
  width: 100%;
  height: 100%;
  display: block;
}

body[data-scene="ocean"] .vo-scene {
  display: block;
}

/* --------------------------------------------------------------------------
   THE SUNBEAMS
   Five beams leaning down and to the left, because the light in this scene
   comes from up and to the right and stays there.

   SOFTNESS IS BUILT, NOT BLURRED, in both directions, and neither one uses
   a filter. A filter on a moving element is re-rasterised every frame,
   which is precisely the cost this scene exists to avoid.

     Down the beam, softness is a gradient. One vertical fade in user space,
     shared by all five so they dim on the same schedule rather than each
     over its own height. The gradient is styled here rather than in the
     markup because stop-color and stop-opacity are real CSS properties,
     which is the one mechanism that lets an SVG gradient read a token.

     Across the beam, softness is three shapes. The first draft used one
     quadrilateral per beam and the render was unambiguous: a hard left and
     right edge makes a stripe, and five stripes make a barcode, not
     weather. Each beam is now three quads on a shared centreline at full,
     62% and 30% of its width, each at --vo-ray-step. They composite to
     0.1426 down the core and step down twice to nothing at the edges.

   The three steps are why the step token is not the opacity token. The
   contrast contract is quoted against the composite, and both numbers are
   in tokens.css beside the value.
   -------------------------------------------------------------------------- */
.vo-ray-in {
  stop-color: var(--vo-ray);
  stop-opacity: var(--vo-ray-step);
}

.vo-ray-out {
  stop-color: var(--vo-ray);
  stop-opacity: 0;
}

.vo-ray-shape {
  fill: url(#vo-ray-fade);
}

/* The sway. One group, so the five beams keep their spacing and can never
   drift into each other. Alternating rather than looping, because light
   through water leans one way and then the other; it does not march. */
.vo-rays {
  transform-box: view-box;
  transform-origin: 50% 0;
  animation: vo-ray-sway var(--vo-ray-shift) ease-in-out infinite alternate;
}

@keyframes vo-ray-sway {
  from {
    transform: translateX(calc(-1 * var(--vo-ray-sway)));
  }
  to {
    transform: translateX(var(--vo-ray-sway));
  }
}

/* --------------------------------------------------------------------------
   THE SCHOOLS
   Three, at three depths, on three clocks, in two directions. The spread
   between the clocks is what produces the parallax; if the three durations
   were close together the schools would read as one layer with holes in it.

   THE LOOP DISTANCE IS EXACTLY 100% AND IS NOT A DESIGN VALUE. Each school
   group in the markup holds TWO copies of its shoal, spaced one view-box
   width apart. Translating the group by exactly one view-box width lands
   the second copy where the first one was, so the end of the cycle is
   pixel-identical to its start and the loop has no seam. Any other distance
   puts a jump in it. This is why there is no --vo-school-travel token: the
   number is a consequence of the markup, not a choice, and a token would
   invite someone to change one without the other.

   transform-box: view-box makes the percentage resolve against the view box
   rather than each group's own bounding box, which is what makes "one view
   box width" expressible at all.

   AT REST, WHICH IS THE POINT. Both keyframe sets are written so that
   translateX(0) is an end of the cycle, and every shoal is placed in the
   markup at the position it should occupy in a still picture. That makes
   the reduced-motion state (durations zeroed in tokens.css) a composed
   photograph rather than a paused frame, and it is the state every capture
   in this repository is taken in.
   -------------------------------------------------------------------------- */
/* NO will-change HERE, AND THAT IS A MEASUREMENT RATHER THAN AN OVERSIGHT.

   The obvious thing to write on an element you are about to animate forever
   is will-change: transform, and it was here until the numbers came back.
   Over ten seconds of idle at 1280, with the hint: 596 layouts, 596 style
   recalcs, 0.379s of main-thread task time. Without it: 600, 600, 0.353s.
   Identical inside the noise, because Chromium does not promote an animated
   SVG group to its own compositor layer the way it promotes an HTML element,
   and the hint cannot buy a promotion the engine is not offering.

   So it is a layer hint that produces no layer, and it is not free: class
   decision 36 records that a will-change anywhere on the path is one of the
   five properties that breaks position: fixed for a descendant, which makes
   an ornamental one on the scene a trap set for whoever adds the next fixed
   thing. Deleted on both counts.

   What that leaves is a main-thread animation, which is worth saying plainly
   rather than calling this scene GPU-cheap. What it costs is in the README,
   with the control that makes the number mean something. */
.vo-school {
  transform-box: view-box;
  transform-origin: 0 0;
}

.vo-school-far {
  fill: var(--vo-fish-shallow);
  animation: vo-swim-right var(--vo-drift-far) linear infinite;
}

/* The mid school takes the LIGHT fill even though it is the middle depth,
   and the render is why. Its first draft used the shallow fill and it
   vanished: at y=548 the water is already between the mid and deep stops,
   which is within a few points of the shallow fish fill itself, so seven
   fish were drawn and none of them existed. Which fill a school takes is
   decided by the water at ITS depth, not by its rank in the stack. */
.vo-school-mid {
  fill: var(--vo-fish-deep);
  animation: vo-swim-left var(--vo-drift-mid) linear infinite;
}

.vo-school-near {
  fill: var(--vo-fish-deep);
  animation: vo-swim-right var(--vo-drift-near) linear infinite;
}

/* FACING BELONGS INSIDE THE ANIMATED ELEMENT, NEVER ON IT.

   Two of these keyframe sets travel right and one travels left, and the fish
   glyph faces left, so the right-travelling shoals have to be mirrored. The
   mirror is on a group INSIDE each shoal, in the markup, and it must stay
   there.

   The trap, which is worth stating because it fails quietly: a scaleX(-1)
   written on .vo-school instead would be composed with the keyframe
   transform below, so it would reverse the TRAVEL as well as the facing. The
   fish would then point the right way relative to their own motion and the
   whole school would swim the wrong way across the frame, which looks
   plausible in isolation and is wrong against the parallax of the other two
   layers. One bug becomes two that cancel on inspection and not on screen.

   The scene shipped every school swimming tail-first once already, so this
   is not a hypothetical. */
@keyframes vo-swim-right {
  from {
    transform: translateX(-100%);
  }
  to {
    transform: translateX(0);
  }
}

@keyframes vo-swim-left {
  from {
    transform: translateX(0);
  }
  to {
    transform: translateX(-100%);
  }
}

/* --------------------------------------------------------------------------
   PREFERENCES
   Reduced motion is handled entirely in tokens.css, which zeroes the four
   scene durations. There is no override here, and that is the design: a
   zero-duration animation leaves every shape at its authored position, and
   the authored positions are the still life.

   More contrast is handled here, because it is a change of composition
   rather than of timing. It forces the calm two-stop field regardless of
   what the picker says and removes the scene layer entirely, so nothing at
   all is drawn between the reader and the words. Placed after the ocean
   rules so it wins on source order at equal specificity.
   -------------------------------------------------------------------------- */
@media (prefers-contrast: more) {
  body[data-scene] {
    background-image: linear-gradient(
      180deg,
      var(--vo-water-surface),
      var(--vo-water-abyss)
    );
  }

  body[data-scene] .vo-scene {
    display: none;
  }
}

/* --------------------------------------------------------------------------
   NARROW SCREENS
   The scene stays, and stays animated. It is the one part of this shell
   that does not transform at the breakpoint, because a phone-sized ocean is
   still an ocean; what changes is how much of it the window leaves visible,
   and that is the shell's business rather than the scene's.

   One thing does change. Below the breakpoint the window takes the full
   width and the visible water is reduced to the band above it, so the only
   school a phone ever sees is the shallow one. Slowing the near school here
   keeps the deep water calm behind the menu panel when it is open, and it is
   the only place in the scene where a duration is not the token's.

   The band exists because of a measurement rather than a preference; the
   note at --vo-window-band in tokens.css has it.
   -------------------------------------------------------------------------- */
@media (max-width: 1023px) {
  .vo-school-near {
    animation-duration: var(--vo-drift-far);
  }
}
