/* 2so-fx: grain. A fine monochrome film grain over footage, for the
   cinematic grayscale look (brand kit: images are grayscale, high contrast,
   dark). CSS only: add class="fx-grain" to the positioned element holding
   the footage (relative, absolute or fixed; the class doesn't change it).

   Original 2SO effect (not a port). The noise is an SVG turbulence tile
   rendered once by the browser; a stepped transform shifts it about eight
   times a second, which the compositor handles without repainting. The
   host gets overflow: hidden.

   Tune with custom properties on the host:
     --fx-grain-opacity (default .07)  --fx-grain-size (default 160px)
     --fx-grain-speed (default .9s)    --fx-grain-blend (default overlay)
     --fx-grain-z (default 1) */

.fx-grain {
  overflow: hidden;
}

.fx-grain::after {
  content: "";
  position: absolute;
  inset: -32px;
  z-index: var(--fx-grain-z, 1);
  pointer-events: none;
  background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='160' height='160'%3E%3Cfilter id='g'%3E%3CfeTurbulence type='fractalNoise' baseFrequency='.85' numOctaves='3' stitchTiles='stitch'/%3E%3CfeColorMatrix type='saturate' values='0'/%3E%3C/filter%3E%3Crect width='100%25' height='100%25' filter='url(%23g)'/%3E%3C/svg%3E");
  background-size: var(--fx-grain-size, 160px);
  opacity: var(--fx-grain-opacity, .07);
  mix-blend-mode: var(--fx-grain-blend, overlay);
  animation: fx-grain-shift var(--fx-grain-speed, .9s) steps(1, end) infinite;
}

/* Seven jumps a cycle, each inside the 32px overscan, so no edge shows.
   The overscan is a fixed length, not a percentage: a percentage inset moves
   the overlay's top edge whenever the host's height changes (a hero growing
   as the next show loads), and the browser counts that as a layout shift. */
@keyframes fx-grain-shift {
  0% { transform: translate(0, 0); }
  14% { transform: translate(-12px, 9px); }
  28% { transform: translate(9px, -15px); }
  42% { transform: translate(-18px, -6px); }
  57% { transform: translate(15px, 12px); }
  71% { transform: translate(-6px, 18px); }
  85% { transform: translate(18px, -9px); }
  100% { transform: translate(0, 0); }
}

/* Reduced motion: the texture stays, the flicker stops. */
@media (prefers-reduced-motion: reduce) {
  .fx-grain::after { animation: none; }
}

/* Forced colours, reduced data and print: no grain at all. */
@media (forced-colors: active), (prefers-reduced-data: reduce), print {
  .fx-grain::after { display: none; }
}
