/* ── grain.css ──────────────────────────────────────────────────────────────
   The one material layer on an otherwise pure-flat-colour site. Not the same
   category of thing as the banned gradients/glassmorphism/blur-orbs — this is
   closer to what film grain does to a digital photo: it makes flat colour
   read as material rather than a hex value.

   A single fixed overlay, procedural (SVG feTurbulence, no image asset), so
   it costs nothing to ship and scales losslessly. [data-grain] on <html>
   picks the variant; --grain-opacity (tokens.css) is 0 until one is chosen.
   No colour tint yet — see the note at the bottom before adding one back.
   ───────────────────────────────────────────────────────────────────────── */

body::after {
  content: "";
  position: fixed;
  inset: 0;
  z-index: 1000; /* above page content and the sticky nav/buy bar (90),
                    below the dev-only lab bar (9999) */
  pointer-events: none;
  opacity: var(--grain-opacity);
  /* hard-light, not screen/multiply: those branch on the BASE pixel's
     brightness (screen only ever adds light, multiply only ever removes
     it), which varies across a page mixing flat ground with bright UI
     screenshots. hard-light branches on the NOISE pixel instead, so a
     mid-grey-centred noise field darkens dark ground and lightens bright
     ground with the same rule everywhere. */
  mix-blend-mode: hard-light;
  background-repeat: repeat;
  background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='480' height='480' viewBox='0 0 480 480'%3E%3Cfilter id='n' x='0%25' y='0%25' width='100%25' height='100%25' color-interpolation-filters='sRGB'%3E%3CfeTurbulence type='fractalNoise' baseFrequency='0.75' numOctaves='3' stitchTiles='stitch' result='fine'/%3E%3CfeTurbulence type='fractalNoise' baseFrequency='0.02' numOctaves='2' stitchTiles='stitch' result='coarse'/%3E%3CfeComposite in='fine' in2='coarse' operator='arithmetic' k1='0' k2='0.75' k3='0.25' k4='0' result='noise'/%3E%3CfeColorMatrix in='noise' type='saturate' values='0'/%3E%3C/filter%3E%3Crect width='100%25' height='100%25' filter='url(%23n)'/%3E%3C/svg%3E");
}

/* Four things make this noise read as even, neutral grain instead of a
   blotchy colour cast — each one was a real, measured bug during tuning,
   not a style choice, so don't remove any of them without re-checking:

   1. Two feTurbulence layers, not one — a fine pass (0.75, 3 octaves) and
      a coarse one (0.02, 2 octaves) combined via feComposite "arithmetic"
      (a 75/25 weighted average, fine-dominant). This is what makes density
      visibly drift across the page instead of reading as flat, uniform
      "sand". feBlend "overlay" was tried here first and rejected: it isn't
      mean-preserving for two independent noise fields, and biased the
      combined result to ~0.74 average brightness instead of a neutral
      ~0.5 — which happened to still look fine on a dark backdrop but made
      the noise nearly invisible on a light one.

      The coarse layer's weight (originally 40%) and the SVG's own tile
      size (originally 200×200) were both tuned down/up after the first
      pass read as blotchy in an obviously repeating way — a 200px tile
      only fits ~3 cycles of a 0.015-frequency coarse layer, so the exact
      same blotch shapes recurred every 200px, an unmissably periodic
      pattern rather than organic drift. A 480×480 tile at 0.02 baseFrequency
      fits many more unique coarse cycles before repeating, and dropping
      the coarse weight to 25% keeps the fine layer dominant so the
      remaining blotchiness reads as subtle modulation, not the main event.

   2. color-interpolation-filters="sRGB" on the filter — SVG filters
      default to processing in linearRGB, but canvas/display readback is
      sRGB (gamma-encoded), and that round-trip is nonlinear: a field
      centred at 0.5 in linear light reads back as ~0.73 in sRGB. Left at
      the default, the noise measured ~0.74 average with a narrow spread;
      with this attribute, ~0.51 average with roughly double the spread.

   3. feColorMatrix type="saturate" values="0" as the last step —
      feTurbulence generates independent noise per R/G/B channel, which
      hard-light then renders as visible colour speckle (a green/purple
      cast), not grain. Desaturating is a linear per-pixel operation, so it
      commutes with the weighted average in #1 — one pass after combining
      the two layers does the same job as desaturating each layer first,
      so there's only one here, not two.

   4. Explicit width/height/viewBox (480×480) on the <svg>, and the
      filter's own x/y/width/height set to exactly match (0%/0%/100%/100%,
      not the SVG default of -10%/-10%/120%/120%) — without an intrinsic
      size, a background-image SVG is stretched to fill the ENTIRE
      background-positioning area instead of being tiled, so
      background-repeat has nothing to repeat: you get one giant instance
      of the filter's default region, which fades out right at that
      region's boundary. Giving it a real small size turns it into an
      actual repeating tile, and matching the filter region to that exact
      size (rather than the wider default) is what lets stitchTiles="stitch"
      compute seams that line up with the edge CSS actually repeats on —
      mismatched regions were producing visible seams between tiles. */

/* Whisper vs. visible/plugin-tinted: checked directly against real page
   content (not just isolated swatches) at a few opacities — 0.3 reads as
   clearly material without tipping into "textured wallpaper"; going much
   higher starts showing faint per-channel colour fringing that's inherent
   to fractalNoise (it isn't perfectly channel-identical even after
   desaturating the combined field, since the desaturation only forces R=G=B
   on average across the blend, not per source channel before that point). */
[data-grain="whisper"]        { --grain-opacity: 0.15; }
[data-grain="visible"]        { --grain-opacity: 0.3; }
[data-grain="plugin-tinted"]  { --grain-opacity: 0.3; }

/* plugin-tinted only: a second, separate layer washes in the plugin's own
   --p colour under the grain — a live CSS gradient blended in normally,
   not baked into the SVG, so it's unrelated to the four fixes above and
   not affected by them. Kept as its own rule so --p (defined on every
   product page) doesn't leak into "whisper"/"visible" too.

   The wash colour is --p mixed down to --grain-p-alpha (tokens.css,
   per-plugin — 35% if a plugin doesn't set one) rather than plain --p, so
   each plugin's screen-blend strength can be tuned independently of --p
   itself, which other things on the page also use. */
[data-grain="plugin-tinted"] body::after {
  --grain-p-wash: color-mix(in srgb, var(--p, transparent) var(--grain-p-alpha, 35%), transparent);
  background-image:
    url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='480' height='480' viewBox='0 0 480 480'%3E%3Cfilter id='n' x='0%25' y='0%25' width='100%25' height='100%25' color-interpolation-filters='sRGB'%3E%3CfeTurbulence type='fractalNoise' baseFrequency='0.75' numOctaves='3' stitchTiles='stitch' result='fine'/%3E%3CfeTurbulence type='fractalNoise' baseFrequency='0.02' numOctaves='2' stitchTiles='stitch' result='coarse'/%3E%3CfeComposite in='fine' in2='coarse' operator='arithmetic' k1='0' k2='0.75' k3='0.25' k4='0' result='noise'/%3E%3CfeColorMatrix in='noise' type='saturate' values='0'/%3E%3C/filter%3E%3Crect width='100%25' height='100%25' filter='url(%23n)'/%3E%3C/svg%3E"),
    linear-gradient(var(--grain-p-wash), var(--grain-p-wash));
  background-blend-mode: hard-light, screen;
}

/* NOTE for whoever adds a colour tint back in: a "tint toward --ground via
   feFlood + feBlend luminosity" approach was tried and measurably made
   things worse, not better — it pulls the noise's overall brightness
   toward its OWN tint colour (dark tint skews the noise below hard-light's
   useful midpoint against its own near-black backdrop; light tint skews it
   above, against its own near-white backdrop), which is backwards from
   what hard-light needs. Try grain-lab.html's tint checkbox to see this
   directly before reaching for that approach again. */
