/* ============================================================
   ITG background - page ground + the .grid-bg module

   NOTE: this file is deliberately pure ASCII. A PowerShell one-liner doing
   `Get-Content -Raw | Set-Content -Encoding utf8` to bump the cache version read it as ANSI and
   wrote mojibake over every box-drawing character and em dash (2026-08-17). Keep it ASCII and
   that class of accident cannot happen again.
   ============================================================ */
.bd-base{position:fixed;inset:0;z-index:0;pointer-events:none;background:#04060a;}

/* ============================================================
   .grid-bg - THE MODULE. Drop it on any dark area, on any page:

     <div class="grid-bg"></div>

   inside a position:relative container. That is the whole API.

   TWO baked, seamless 1120x1120 PNGs, repeated by the browser:
     ::before  assets/grid-mesh.png    the WHITE mesh (Nico 2026-08-24; was lime), everywhere, no holes
     ::after   assets/grid-clouds.png  ink-coloured blotches that HIDE the mesh

   Painting opaque #04060a over the mesh is identical to punching holes in it, because everything
   behind .grid-bg is the flat page ground (.bd-base / html,body). Splitting the two is what lets
   the blotches move on their own: an overlay can be driven with `transform`, which the compositor
   handles with zero repaint. (Before 2026-08-17 both were flattened into one grid-bg.png, so the
   blotches were part of the artwork and could only move with the parallax, i.e. they read as
   frozen. The canvas engine that predated the tile could animate, but only by repainting forever.)

   THREE independent motions, none of which fight each other, because each drives a DIFFERENT
   property (individual transform properties compose: translate -> scale -> transform):
     translate   PARALLAX - scroll-driven, on BOTH layers, so the whole background lags the page
     transform   DRIFT    - time-based orbit, clouds only: the blotches flow across the mesh
     scale       BREATHE  - time-based pulse, clouds only: keeps the flow from reading as a
                            rigid sheet sliding past

   The tile is 1120px = 28 x 40px cells, so mesh lines align across repeats, and the noise
   lattice wraps, so there are no seams.

   Regenerate the tiles (different seed, coverage, mesh size, colour):
     python _tools/make-grid-layers.py

   IMPORTANT - the module NEVER overflows its container.
   Parallax and drift both need artwork past the container edges, but that bleed lives on the
   ::before / ::after layers and .grid-bg clips it. Bleeding the outer box instead broke the page
   twice over (2026-08-14): the host wrapper carries inline `overflow-x:hidden`, and per CSS spec a
   non-visible value on one axis promotes the other from `visible` to `auto`, so the wrapper
   silently became a second scroll container, showing a second scrollbar AND stealing the mouse
   wheel from the page.
   ============================================================ */
:root{
  /* -- THE PARALLAX KNOB ---------------------------------------------------------------------
     How far the grid lags the page over a full scroll. This single value drives BOTH the
     keyframe distance and the bleed, which must always match: if the shift were larger than
     the bleed, the end of the scroll would expose an empty strip above the artwork.
       120px  barely there
       180px  subtle (previous default)
       320px  clearly reads as depth
       500px  strong
       800px  dramatic; the mesh visibly slides while you scroll                             */
  --grid-parallax:500px;

  /* -- THE DRIFT KNOBS -----------------------------------------------------------------------
     The blotches orbit slowly, so the patches of visible mesh keep re-forming. Radius drives
     both the travel and the extra bleed, same contract as the parallax.

     READ THIS BEFORE TUNING: the drift is subtle by design and these knobs cannot change that.
     The mesh's brightest pixel is rgb(24,26,30) over ink rgb(4,6,10), a ~20/255 swing (white bake 2026-08-24), which is a
     hard ceiling on anything the clouds can modulate - the screen changes by a mean of 0.18/255
     over 5 seconds no matter how fast the orbit runs. Raising the mesh alpha WOULD make the
     motion read, and was tried and rejected on 2026-08-17 ("el grid esta mas marcado, me gustaba
     como estaba antes"). The resting look is locked; see _tools/make-grid-layers.py.

       --grid-drift          orbit radius. 120px = a slow breath, 280px = clearly flowing,
                             560px (half a tile) = the old canvas engine's amplitude.
       --grid-drift-seconds  one full orbit. Higher = slower. Speed is 2*pi*radius/seconds, so
                             this alone changes pace without changing how far the blotches
                             travel; raise the radius instead if you want more ground covered.
                             At radius 260px:  78s = 21px/s   55s = 30px/s   43s = 38px/s
                             33s = 50px/s   26s = 63px/s     (43s matches the old canvas engine).
       --grid-breathe        peak scale of the pulse. MUST stay >= 1 or the layer shrinks off
                             its own bleed and a full-mesh band appears at the edges.
       --grid-breathe-seconds  keep it a non-integer ratio of the orbit, or the two resync and
                             the motion starts to read as a loop.                            */
  --grid-drift:260px;
  --grid-drift-seconds:26s;
  --grid-breathe:1.04;
  --grid-breathe-seconds:11s;
}
.grid-bg{
  position:absolute;inset:0;z-index:0;pointer-events:none;
  overflow:hidden;                 /* clips the bleeding layers below - do not remove */
}
.grid-bg::before,
.grid-bg::after{
  content:'';position:absolute;left:0;right:0;
  /* slack for the parallax shift, clipped by the parent */
  top:calc(-1 * var(--grid-parallax));
  bottom:calc(-1 * var(--grid-parallax));
  background-repeat:repeat;
  background-position:0 0;
}
/* the ?v on the tiles is not optional: re-baking writes the same filenames, and without it the
   browser keeps serving the cached artwork and a re-bake looks like it did nothing */
.grid-bg::before{background-image:url('assets/grid-mesh.png?v=4');}

.grid-bg::after{
  background-image:url('assets/grid-clouds.png?v=4');
  /* the clouds also orbit, so they need the radius on top of the parallax slack, on every
     side, since the orbit is horizontal as well as vertical. The +32px is not decoration: at
     exactly radius the layer lands flush with the container edge at the extremes of the orbit,
     and a single subpixel of rounding there exposes a bright line of unmasked mesh. */
  --_drift-bleed:calc(var(--grid-drift) + 32px);
  top:calc(-1 * (var(--grid-parallax) + var(--_drift-bleed)));
  bottom:calc(-1 * (var(--grid-parallax) + var(--_drift-bleed)));
  left:calc(-1 * var(--_drift-bleed));
  right:calc(-1 * var(--_drift-bleed));
  --_drift-diag:calc(var(--grid-drift) * 0.7071);   /* 45 deg stops on the orbit */
  animation-name:itgGridDrift, itgGridBreathe;
  animation-duration:var(--grid-drift-seconds), var(--grid-breathe-seconds);
  animation-timing-function:linear, ease-in-out;
  animation-iteration-count:infinite, infinite;
  will-change:transform;
}

/* -- DRIFT -------------------------------------------------------------------------------------
   A closed circular orbit, so it loops seamlessly and never drifts away. Eight stops keep the
   path round enough that the speed reads as constant; a four-stop version traces a diamond and
   you can see it change direction at the corners. */
@keyframes itgGridDrift{
  0%   { transform:translate3d(0, calc(-1 * var(--grid-drift)), 0); }
  12.5%{ transform:translate3d(var(--_drift-diag), calc(-1 * var(--_drift-diag)), 0); }
  25%  { transform:translate3d(var(--grid-drift), 0, 0); }
  37.5%{ transform:translate3d(var(--_drift-diag), var(--_drift-diag), 0); }
  50%  { transform:translate3d(0, var(--grid-drift), 0); }
  62.5%{ transform:translate3d(calc(-1 * var(--_drift-diag)), var(--_drift-diag), 0); }
  75%  { transform:translate3d(calc(-1 * var(--grid-drift)), 0, 0); }
  87.5%{ transform:translate3d(calc(-1 * var(--_drift-diag)), calc(-1 * var(--_drift-diag)), 0); }
  100% { transform:translate3d(0, calc(-1 * var(--grid-drift)), 0); }
}
/* -- BREATHE -----------------------------------------------------------------------------------
   On a different period from the orbit, so the two never resync. Without it the blotches read as
   one rigid sheet being panned; with it they appear to swell and dissolve as they travel. */
@keyframes itgGridBreathe{
  0%,100%{ scale:1; }
  50%    { scale:var(--grid-breathe); }
}

/* -- PARALLAX ----------------------------------------------------------------------------------
   The whole background, mesh AND blotches, lags the page as you scroll, so it reads as sitting
   further back and slipping behind the content. Native scroll-driven animation: the compositor
   runs it, there is no scroll listener and no repaint, so it stays as cheap as a static tile.
   It drives `translate`, NOT `transform`, precisely so the clouds' drift (`transform`) and pulse
   (`scale`) ride on top of it instead of overwriting it.
   Browsers without scroll-timeline support simply get the background without parallax - the drift
   still runs, and there is no fallback JS. */
@supports (animation-timeline: scroll()) {
  @keyframes itgGridParallax{
    from{ translate:0 0; }
    to  { translate:0 var(--grid-parallax); }
  }
  .grid-bg::before{
    /* longhand on purpose: the `animation` shorthand resets duration to 0s, and a scroll-driven
       animation needs `auto` to map onto the full scroll range - with 0s it never advances. */
    animation-name:itgGridParallax;
    animation-duration:auto;
    animation-timing-function:linear;
    animation-fill-mode:both;
    animation-timeline:scroll(root block);
    will-change:translate;
  }
  .grid-bg::after{
    /* the full list has to be restated: animation-name is a single property, so naming only the
       parallax here would drop the drift and the breathe */
    animation-name:itgGridParallax, itgGridDrift, itgGridBreathe;
    animation-duration:auto, var(--grid-drift-seconds), var(--grid-breathe-seconds);
    animation-timing-function:linear, linear, ease-in-out;
    animation-iteration-count:1, infinite, infinite;
    animation-fill-mode:both, none, none;
    animation-timeline:scroll(root block), auto, auto;
    will-change:translate, transform, scale;
  }
  /* a fixed tile is already detached from the scroll; parallax on top would fight it */
  .grid-bg--fixed::before,
  .grid-bg--fixed::after{animation-name:none;}
}

/* Opt-in variants ---------------------------------------------------------------------------
   --fixed   both layers stay put while the page scrolls: no parallax, no drift. Use sparingly,
             this is what reads as the background tracking the reader. It keeps `background-
             attachment:fixed`, which cannot coexist with a transform on the same box (the
             transform would become the background's containing block), so this variant is
             deliberately motionless.
   --faint / --strong   dial the mesh without regenerating the assets. */
.grid-bg--fixed::before,
.grid-bg--fixed::after{background-attachment:fixed;animation-name:none;}
.grid-bg--faint{opacity:.55;}
/* brightness on .grid-bg would also lift the near-black clouds above the page ground and turn
   them into visible grey patches - the filter belongs on the mesh layer only */
.grid-bg--strong::before{filter:brightness(1.6);}

@media (prefers-reduced-motion: reduce){
  .grid-bg::before,
  .grid-bg::after{animation-name:none;}
}
