/* A1M ambient background layer — the single source of truth, linked by every page.
   C1: this file exists because build/site-v4.css (inlined on 72 generated pages) and
   assets/css/v4.css (linked by 15) are two divergent stylesheets that already share
   every token name. Duplicating the layer into both is the exact defect that produced
   that divergence, so it lives here once and both generators link it.

   Colours read the shipped token set with literal fallbacks, so the layer still renders
   correctly on any page whose token block is incomplete. Light and dark come free:
   html[data-theme="dark"] reassigns --navy and --accent, and these vars follow.

   Zero JavaScript. Both drifting layers animate transform ONLY — never
   background-position, never background-size, never top/left — so each is a single
   composited layer and the drift costs no repaint. */

.amb{
  position:fixed;
  inset:0;
  z-index:-1;
  pointer-events:none;
  overflow:hidden;              /* clips the two oversized blobs to the viewport box */
  /* 3% grain, inline feTurbulence, no image request. The 3% is baked into the SVG's
     own rect opacity rather than a CSS opacity on .amb, because opacity here would
     also dim the two pseudo-element blobs. Static by construction — nothing about
     this layer animates. */
  background-image:url("data:image/svg+xml,%3Csvg%20xmlns='http://www.w3.org/2000/svg'%20width='140'%20height='140'%3E%3Cfilter%20id='g'%3E%3CfeTurbulence%20type='fractalNoise'%20baseFrequency='.82'%20numOctaves='3'%20stitchTiles='stitch'/%3E%3C/filter%3E%3Crect%20width='140'%20height='140'%20filter='url(%23g)'%20opacity='.03'/%3E%3C/svg%3E");
  background-repeat:repeat;
  background-size:140px 140px;
}

/* The two blobs. Pseudo-elements rather than child nodes so the markup every generator
   emits is one empty div and nothing can drift between them. */
.amb::before,
.amb::after{
  content:"";
  position:absolute;
  top:50%;
  left:50%;
  width:120vmax;
  height:120vmax;
  margin:-60vmax 0 0 -60vmax;
  border-radius:50%;
  will-change:transform;
}

/* --navy blob. 5% alpha, 45s loop.
   Ending on the bare `transparent` keyword, not on a color-mix. The usual worry is that
   `transparent` means transparent BLACK and darkens the ramp -- it does not, because CSS
   gradient interpolation is PREMULTIPLIED. Measured rather than assumed: the midpoint of
   this gradient over white samples rgb(149,158,203) with `transparent`, with an explicit
   rgba(43,61,152,0), and with color-mix(in srgb,#2b3d98 0%,transparent) -- three
   byte-identical readings, and color-mix's computed value resolves to color(srgb 0 0 0/0)
   anyway. A second declaration would have been a dead line carrying a false claim. */
.amb::before{
  background:radial-gradient(circle closest-side,var(--navy,#2b3d98),transparent);
  opacity:.05;
  animation:ambDriftA 45s ease-in-out infinite;
}

/* --accent blob. 4% alpha, 70s loop, counter-rotating against the navy one. */
.amb::after{
  background:radial-gradient(circle closest-side,var(--accent,#cf7008),transparent);
  opacity:.04;
  animation:ambDriftB 70s ease-in-out infinite;
}

/* transform ONLY. Every keyframe below is a single translate3d+scale — nothing here
   touches a paint-triggering property. */
@keyframes ambDriftA{
  0%  {transform:translate3d(-14%,-9%,0) scale(1)}
  50% {transform:translate3d(13%,10%,0) scale(1.14)}
  100%{transform:translate3d(-14%,-9%,0) scale(1)}
}
@keyframes ambDriftB{
  0%  {transform:translate3d(15%,11%,0) scale(1.12)}
  50% {transform:translate3d(-12%,-10%,0) scale(1)}
  100%{transform:translate3d(15%,11%,0) scale(1.12)}
}

/* Fully static. animation:none leaves each blob at its untransformed centre, and the
   grain never moved in the first place. will-change is dropped too — there is no
   longer anything to promote a layer for. */
@media (prefers-reduced-motion:reduce){
  .amb::before,
  .amb::after{
    animation:none;
    will-change:auto;
  }
}
