/* One cached noise tile, moved in discrete steps instead of recalculating a filter.
   Native dialogs are in the browser's top layer and get their own overlay. */
:root { --grain-opacity: .18; }
body::after,
dialog::after {
  content: "";
  position: fixed;
  inset: -128px;
  z-index: 100;
  pointer-events: none;
  user-select: none;
  background: url('./assets/grain.svg') repeat;
  background-size: 180px 180px;
  opacity: var(--grain-opacity);
}
body:has(dialog[open])::after { visibility: hidden; }
@media (prefers-reduced-motion: no-preference) {
  body::after,
  dialog[open]::after {
    animation: grain-shift .64s steps(1, end) infinite;
    will-change: transform;
  }
}
@keyframes grain-shift {
  0%, 100% { transform: translate(0, 0); }
  12.5% { transform: translate(-36px, 58px); }
  25% { transform: translate(74px, -26px); }
  37.5% { transform: translate(-92px, -74px); }
  50% { transform: translate(28px, 96px); }
  62.5% { transform: translate(-64px, 12px); }
  75% { transform: translate(96px, -54px); }
  87.5% { transform: translate(12px, 42px); }
}
@media (prefers-reduced-motion: reduce) {
  body::after,
  dialog::after { animation: none; will-change: auto; }
}
