/* =====================================================================
   Core / loader.css
   ---------------------------------------------------------------------
   The one loading state on this site. Every page uses it — the home
   page, the interior pages, and the full-screen labs pieces — so
   arriving anywhere reads the same way.

   THE IDEA — the waterline, turned into a loop. The whole word VAL
   stands still in the middle of the dark, every stroke dim, and a single
   lift of light travels through it in the order the word is drawn:
   v-left, v-right, a-left, a-right, l-stem, l-foot. Each stroke rises to
   full as the light reaches it and settles back as it passes. Then a
   beat of stillness, and it comes round again.

   NOTHING MOVES. No spread, no scale, no rotation, nothing drawing
   itself on — the only property that changes is opacity, on six
   elements. That is the whole animation, and it is deliberate twice
   over: it is the same event the home page's sea is built around, light
   crossing a surface and the surface reading back; and it is the only
   shape of animation that costs a page nothing, because opacity on a
   composited layer never touches layout or paint. This plane is
   sometimes up while a shader is compiling behind it.

   THE PROGRESS RULE IS GONE, and with it the last thing on this plane
   that was not the word. It was honest — it tracked real work through
   VALLoader.track() — but it was a second object competing with the
   letters, and it answered a question nobody was asking: on a static
   site with no build step, the wait is a second, and a visitor watching
   a bar fill for a second is a visitor being shown a bar.

   What took its job is the thing that was behind the plane all along.
   core/glass.js holds the sheet SEALED — a thick lens, closed over the
   page — until this plane starts to clear, and surfaces it as the plane
   goes. So the background of the loading state is the page transition
   itself, mid-gesture, and the plane over it is a scrim rather than a
   wall: see the alpha in #vl-loader below.

   WHERE THE WORD COMES FROM. Six elements, each the whole word's box,
   each cut to one stroke by a clip-path in core/brand-mark.css — the
   same technique and the same file as the mark in the corner, and the
   same drawing as assets/brand-wordmark.svg. No path data lives here.

   Self-contained otherwise, on purpose. This file names its own --vl-*
   tokens instead of borrowing site.css's or index.html's, because the
   whole point is that the loader is one thing everywhere rather than a
   copy per page — it must not shift when the surrounding stylesheet
   does. The values are the shared palette: --vl-void is site.css's
   --void-0, --vl-warm is the home shader's light source.
===================================================================== */

/* ==== CONFIG ==========================================================
   THE LOOP'S SHAPE AND ITS PACE. Edit here and nowhere else — core/
   loader.js reads every --load-* value below off this
   element at run time rather than carrying its own copies, so there is
   one place the loop is timed and no pair of numbers to keep in step.

   Tune it so the word looks like it is BREATHING, not blinking: the
   whole cycle wants to be watchable three times over without becoming
   irritating. Slower stagger and a longer hold read calmer; a shorter
   stroke reads more like a pulse.
====================================================================== */
:root{
  --vl-void:   #04060a;
  --vl-warm:   #fff3e6;
  --vl-signal: #e2e6ea;

  /* >>> THE WORD'S SIZE. THIS IS THE ONE TOKEN TO TURN. <<<

     Its WIDTH, and nothing else is stated: the word is 2.6746 : 1, so
     aspect-ratio takes the height from --word-ratio
     (core/brand-mark.css), and the gap down to the status line is a
     sixth of this. Change this line and the whole plane rescales.

     Was clamp(150px, 26vw, 330px) — 330px wide and 123px tall on a
     laptop, which is a quarter of the screen's width and reads as a
     splash screen rather than as a page arriving. 176px is 66px tall and
     about a ninth of a 1600px screen: unmistakably the word, comfortably
     held by the frame.

     SENSIBLE RANGE for the ceiling is roughly 130px to 210px. Below
     about 120 the word stops carrying and reads as incidental; above
     about 240 it starts to dominate the plane again. The floor and the
     vw term keep the same proportion down to a phone — 104px on a 390px
     screen — and want changing only if the ceiling moves a long way. */
  --vl-word-w: clamp(104px, 12vw, 176px);

  /* HOW DIM A STROKE IS WHEN THE LIGHT IS NOT ON IT, and how bright it
     is when the light is. The word must still read at rest — this is
     one word breathing, not six lights blinking on a dark field. */
  --load-rest:       .30;
  --load-lit:        1;

  /* BETWEEN ONE STROKE AND THE NEXT. Six strokes, so the light takes
     five of these to cross the word. */
  --load-stagger:    .16s;

  /* ONE STROKE RISING TO FULL AND SETTLING BACK. Split inside
     core/loader.js so the rise takes the larger share and the settle
     runs at about 65% of it — the site's own exit rule, applied to a
     stroke instead of to a panel. */
  --load-stroke-dur: .5s;

  /* THE STILLNESS AFTER THE LAST STROKE, before the pass comes round
     again. This is what stops the loop reading as a spinner: the word
     is allowed to simply be there for a moment. */
  --load-hold:       .7s;

  /* WHEN THE PAGE IS READY BEFORE THE LIGHT HAS FINISHED CROSSING.
     The plane never leaves mid-stroke — but it does not have to finish
     at walking pace either. On a warm cache a page is ready in about
     40ms and the loop's first pass takes 1300, so the visitor sat
     through 1.3 seconds of animation for 40ms of loading. The rest of
     the pass is now PLAYED FASTER instead of waited out: the light still
     crosses the whole word and every stroke still settles, it just gets
     there in --load-exit rather than in whatever was left.

     --load-exit      how long the hurried finish is allowed to take.
     --load-rate-max  and the fastest it may ever be played, so that a
                      page ready at the very start of a pass does not
                      produce a blur. Whichever of the two is reached
                      first decides; a pass with less than --load-exit
                      left simply plays out at its own pace.

     Unitless, and read by core/loader.js with the rest of the CONFIG. */
  --load-exit:      260ms;
  --load-rate-max:  6;

  --vl-ease:    cubic-bezier(.15,.72,.2,1);
  /* THE PLANE'S FADE ON DISMISS. .34 against the opening's .52 is the
     site's exit rule — about 65% of the entrance — and core/loader.js
     reads this rather than carrying a copy of it. */
  --vl-dur-out: .34s;

  /* HOW MUCH OF THE SHEET COMES THROUGH, on the pages that have one.
     0 would be a window, 1 a wall. Lower to see more of the seal
     opening behind the word; raise it if the word stops reading. */
  --vl-scrim:   .55;
}
/* ==== end CONFIG ====================================================== */

/* The plane itself is only a frame for the word — it paints nothing. The
   ground is .vl-plane below, so the two can be at different opacities:
   the ground steps back to let the sheet through, the word does not. */
#vl-loader{
  position:fixed;inset:0;z-index:200;
  display:grid;place-items:center;
  opacity:1;
  transition:opacity var(--vl-dur-out) var(--vl-ease);
}

/* THE GROUND. The same radial the interior pages' veil uses — the sea's
   colour at distance, so the loader, the transition and the water are
   one continuous ground.

   Opaque by default, and only steps back to --vl-scrim once
   `body.has-glass` says there is a live sheet behind it. That
   distinction is doing real work rather than being cautious:

     · on a page with the sheet, what is behind this is the seal in the
       middle of opening (core/glass.js holds it shut until the plane
       starts to clear), which is the whole point of the loading state;
     · on a page without one — the labs pieces, or any machine where
       WebGL failed — what is behind it is the page, and Undercurrent in
       particular brings this plane BACK mid-session to cover a camera
       and a model loading. There it has to be a wall. */
.vl-plane{
  position:absolute;inset:0;
  background:radial-gradient(130% 100% at 50% 60%,#0b111a 0%,#05080c 46%,var(--vl-void) 100%);
  transition:opacity var(--vl-dur-out) var(--vl-ease);
}
body.has-glass .vl-plane{opacity:var(--vl-scrim)}
#vl-loader.is-out{opacity:0}
#vl-loader.is-done{display:none}

.vl-core{
  position:relative;   /* over .vl-plane */
  display:flex;flex-direction:column;align-items:center;
  gap:calc(var(--vl-word-w) / 6);
  transition:opacity var(--vl-dur-out) var(--vl-ease),
             transform var(--vl-dur-out) var(--vl-ease);
}
#vl-loader.is-out .vl-core{opacity:0;transform:translateY(-6px)}

/* ---------- the word ---------------------------------------------------
   Six elements stacked in one box, each the full width of the word and
   each showing only its own stroke. Full-size rather than cut to fit for
   the same reason the mark's two arms are: a clip-path's percentages
   resolve against the element's own box, so every polygon in
   core/brand-mark.css is stated against one box and one box only.

   --word-ratio gives the height, so --vl-word-w above is the only size
   this file states.                                                    */
.vl-word{
  position:relative;display:block;
  width:var(--vl-word-w);
  aspect-ratio:var(--word-ratio);
  color:var(--vl-warm);
}
.vl-word i{
  position:absolute;inset:0;
  background:currentColor;
  opacity:var(--load-rest);
  /* The lift itself is not here. core/loader.js drives these six with
     the Web Animations API, off the --load-* values above: the
     timing is data, the browser composites it, and the loader can tell
     to the millisecond when a pass has finished so it never leaves
     mid-stroke. What IS here is where each stroke rests. */
}
.vl-word .wm-v-left  {clip-path:var(--word-v-left)}
.vl-word .wm-v-right {clip-path:var(--word-v-right)}
.vl-word .wm-a-left  {clip-path:var(--word-a-left)}
.vl-word .wm-a-right {clip-path:var(--word-a-right)}
.vl-word .wm-l-stem  {clip-path:var(--word-l-stem)}
.vl-word .wm-l-foot  {clip-path:var(--word-l-foot)}

/* ---------- status line ------------------------------------------------
   Empty and hidden unless a page has something the visitor actually
   needs to know — the labs pieces use it to say which of the camera and
   the tracking model they are still waiting on.

   Taken out of the flow entirely while empty, not just made invisible:
   a hidden-but-present line still takes its height and the flex gap
   above it, which pushed the word off the centre of the screen on every
   page that has nothing to say.                                        */
.vl-status:empty{display:none}
.vl-status{
  margin:0;min-height:1em;
  font:500 10px/1 'Syne', ui-sans-serif, system-ui, -apple-system, 'Segoe UI', sans-serif;
  letter-spacing:.26em;text-transform:uppercase;
  color:rgba(226,238,246,.42);text-align:center;
  opacity:0;transition:opacity var(--vl-dur-out) var(--vl-ease);
}
.vl-status:not(:empty){opacity:1}

/* ---------- reduced motion ---------------------------------------------
   THE FINISHED WORD, AND IT HOLDS. No loop, no opening, no stagger —
   every stroke at full, which is what the loop is on its way to and
   from anyway. core/loader.js does not start the animations at all
   under this setting, so there is nothing running to stop; the rule
   below is what makes the resting state the lit one.

   The sheet behind the plane does not seal at all under this setting
   (see core/glass.js), so what is left is a word on a dark ground,
   which is the honest minimum this state can be.                       */
@media (prefers-reduced-motion: reduce){
  .vl-word i{opacity:var(--load-lit)}
  #vl-loader,.vl-core{transition-duration:.2s}
  #vl-loader.is-out .vl-core{transform:none}
}
