html,body{height:100%;overflow:hidden;background:var(--bg);
  transition:background 1s cubic-bezier(.32,.72,0,1)}
body{font-family:'Source Sans 3',-apple-system,BlinkMacSystemFont,'Segoe UI',system-ui,sans-serif;
  -webkit-font-smoothing:antialiased;color:var(--tx)}
canvas{display:block;position:relative;z-index:2}

#videobg{position:fixed;inset:0;z-index:0;overflow:hidden;background:#dcd3bd;
  opacity:0;transition:opacity 1.4s ease}
body.light #videobg{opacity:1}
/* The clips are 256px wide and get blown up to fill the screen. Scaled slightly
   past the edges so no seam shows, and the browser's own smoothing plus the
   glass above does the rest — at this blur the upscale is invisible. */
#videobg video{position:absolute;top:50%;left:50%;min-width:100%;min-height:100%;
  width:auto;height:auto;transform:translate(-50%,-50%) scale(1.06);object-fit:cover;
  opacity:0;transition:opacity 2.6s ease;will-change:opacity}
#videobg video.on{opacity:1}

/* ---------- the frosted glass ----------
   A full sheet over the footage, not a tint. `backdrop-filter` blurs everything
   painted beneath it, and the only thing beneath is #videobg — the WebGL canvas
   sits at z-index 2, above this, so the graph itself stays perfectly sharp.

   Three layers do three jobs: the blur destroys all detail and leaves moving
   colour, the saturate stops that colour going muddy once it is smeared, and
   the warm translucent wash on top ties whatever the clip happens to be into
   the paper palette so the text above stays readable over any of the 42. */
/* The first version of this was a cream wash at .68-.84 opacity, with
   saturate(1.9) and a brightness lift on top -- three things all pushing the
   same way, which turned every clip into the same yellow haze. Frosted glass is
   supposed to obscure the shape of what is behind it, not its colour. So: a far
   thinner wash, weighted into a vignette so the edges stay calm and the middle
   stays alive, less blur so movement is still legible as movement, and no
   brightness push at all. */
#scrim{position:fixed;inset:0;z-index:1;opacity:0;pointer-events:none;
  transition:opacity 1.4s ease;
  background:linear-gradient(180deg,rgba(250,246,236,.38) 0%,rgba(250,246,236,.17) 30%,
              rgba(248,243,232,.19) 70%,rgba(224,212,187,.48) 100%),
              radial-gradient(116% 94% at 50% 44%,rgba(255,253,247,.04) 0%,
              rgba(92,78,54,.26) 100%);
  -webkit-backdrop-filter:blur(15px) saturate(1.5) contrast(1.05);
  backdrop-filter:blur(15px) saturate(1.5) contrast(1.05)}
body.light #scrim{opacity:1}

#grain{position:fixed;inset:-60px;pointer-events:none;z-index:3;opacity:var(--grain);
  mix-blend-mode:multiply;transition:opacity 1s ease}
#grain::before,#grain::after{content:'';position:absolute;inset:0;
  background-image:url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='180' height='180'%3E%3Cfilter id='n'%3E%3CfeTurbulence type='fractalNoise' baseFrequency='.82' numOctaves='4'/%3E%3C/filter%3E%3Crect width='180' height='180' filter='url(%23n)' opacity='.42'/%3E%3C/svg%3E")}
#grain::before{animation:grain-drift-a 19s linear infinite}
#grain::after{opacity:.6;animation:grain-drift-b 27s linear infinite reverse}
@keyframes grain-drift-a{
  0%{transform:translate(0,0)} 25%{transform:translate(-3%,2%)}
  50%{transform:translate(2%,-3%)} 75%{transform:translate(3%,3%)}
  100%{transform:translate(0,0)}}
@keyframes grain-drift-b{
  0%{transform:translate(0,0)} 33%{transform:translate(4%,-2%)}
  66%{transform:translate(-2%,-4%)} 100%{transform:translate(0,0)}}

#labels{position:fixed;inset:0;pointer-events:none;z-index:5}
/* opacity:0 to start. A label is positioned by a transform that loop.js writes
   on every frame, so between being created and the first frame running it has
   no transform at all -- which puts all 355 of them stacked at the top-left
   corner at full opacity. That was the smear of text in the corner on boot.
   The loop sets opacity on every label every frame regardless, so starting at
   zero costs nothing and there is no frame where an unpositioned label shows. */
/* font-size is fixed here and never touched again; loop.js varies apparent size
   with a scale() in the transform instead, so text is rasterised once and then
   composited rather than re-laid-out on every frame. See the note there. */
.lab{position:absolute;white-space:nowrap;font-family:'Source Sans 3',system-ui,sans-serif;
  color:var(--lab);text-shadow:var(--lab-shadow);letter-spacing:-.004em;opacity:0;
  font-size:15.5px;transform-origin:50% 50%;
  transition:opacity .4s cubic-bezier(.32,.72,0,1);will-change:transform,opacity}
