/* ==========================================================================
   Background Pattern
   Everything needed for the scrollable rectangle background lives here.
   The actual pattern image is generated in script.js (see PATTERN_CONFIG)
   and applied directly to <body>, so it scrolls with the page content
   like any normal background.
   ========================================================================== */

:root {
  --bg-color: #ffffff;
}

* {
  margin: 0;
  box-sizing: border-box;
}

body {
  background-color: var(--bg-color);
  /* background-image / background-size are set from script.js */
  background-repeat: repeat;
}

/* Background Frame Flashes (per 18)background.txt, script.js
   initBackgroundFrames) — slides 1 and 7b only. Sparse video-frame tiles
   sit ON TOP of everything else in the slide, including .source-wrap (6)
   and .source-line (5), not just the text. The overlay div itself is
   pointer-events:none (just a clipping box); only actual visible tiles are
   clickable, so a click reaches script.js's clear-and-restart handler
   (via bubbling) exactly where a tile is currently covering something, and
   passes through to the citation link/button everywhere else. */
.bg-frame-overlay {
  position: absolute;
  z-index: 7;
  inset: 0;
  overflow: hidden;
  pointer-events: none;
}

.bg-frame-tile {
  position: absolute;
  display: block;
  object-fit: cover;
  opacity: 1;
  pointer-events: auto;
  cursor: pointer;
  transition: opacity 0.6s ease;
}

.bg-frame-tile-pending {
  opacity: 0;
  pointer-events: none; /* not yet visible -- shouldn't block clicks to whatever's beneath it */
}

/* ==========================================================================
   Volume toggle (top right corner, fixed, per 19)volumebutton.txt)
   Site always loads muted (autoplay requirement -- see script.js
   initVolumeToggle), so it starts as a white/hollow circle; a click flips
   every <video>'s .muted at once and fills the circle solid black to show
   sound is on. 50px @ 1512px Figma frame, converted to vw the same way as
   the type scale below (--volume-btn-size).
   ========================================================================== */

.volume-toggle {
  position: fixed;
  top: 2vw;
  right: 2vw;
  width: var(--volume-btn-size);
  height: var(--volume-btn-size);
  border-radius: 50%;
  border: 1px solid #000;
  background-color: #fff;
  padding: 0;
  cursor: pointer;
  z-index: 20; /* above every other layer in the deck (max elsewhere is 10) */
}

.volume-toggle.is-unmuted {
  background-color: #000;
}

/* ==========================================================================
   Scroll Snap
   Each .slide locks into place as you scroll past it, one at a time.
   Snapping is set up here on <html> (the element that actually scrolls);
   the per-slide side is declared on .slide in the Slides section below.
   ========================================================================== */

html {
  scroll-snap-type: y mandatory;
}

/* ==========================================================================
   Typography
   Fonts and the type scale from content.txt. Sizes are given as px values
   from a 1512px-wide Figma frame, converted to vw so they scale with the
   viewport (px / 1512 * 100vw), uncapped in both directions.
   ========================================================================== */

@font-face {
  font-family: 'Array';
  src: url('fonts/Array-Regular.otf') format('opentype');
  font-weight: 400;
  font-style: normal;
  font-display: swap;
}

@font-face {
  font-family: 'Bitcount Prop Single';
  src: url('fonts/BitcountPropSingle-VariableFont_CRSV,ELSH,ELXP,slnt,wght.ttf') format('truetype');
  font-weight: 400;
  font-style: normal;

  font-display: swap;
}

@font-face {
  font-family: 'Baby Blocks';
  src: url('fonts/baby%20blocks.ttf') format('truetype');
  font-weight: 400;
  font-style: normal;
  font-display: swap;
}

:root {
  /* type scale, derived from content.txt px values @ 1512px Figma frame */
  --type-scale: 0.8; /* overall multiplier — adjust this to size all text up/down together */
  --fs-h1: calc(46.96vw * var(--type-scale)); /* 710px */
  --fs-h2: calc(19.18vw * var(--type-scale)); /* 290px */
  --fs-h3: calc(7.94vw * var(--type-scale));  /* 120px */
  --fs-p1: calc(4.23vw * var(--type-scale));  /* 64px */
  --fs-p2: calc(2.65vw * var(--type-scale));  /* 40px */
  --fs-source: calc(0.99vw * var(--type-scale)); /* 15px, per 9)sources.txt */
  --fs-warning: calc(2.12vw * var(--type-scale)); /* 32px, per 11)contentwarning.txt */
  --fs-source-num-9b: calc(1.98vw * var(--type-scale)); /* 30px, per 13)slide9b.txt */
  --fs-petition: calc(2.12vw * var(--type-scale)); /* 32px, per 14)link_petition.txt (same size as --fs-warning) */
  --petition-arrow-w: calc(33.66vw * var(--type-scale)); /* 509px, arrow.svg's own native width, per 14)link_petition.txt */
  --volume-btn-size: calc(3.31vw * var(--type-scale)); /* 50px, per 19)volumebutton.txt */
}

.h1, .h2, .h3 {
  font-family: 'Array', sans-serif;
  line-height: 0.85;
  margin: 0;
}

.p1, .p2 {
  font-family: 'Bitcount Prop Single', sans-serif;
  line-height: 0.85;
  margin: 0;
}

.h1 { font-size: var(--fs-h1); }
.h2 { font-size: var(--fs-h2); }
.h3 { font-size: var(--fs-h3); }
.p1 { font-size: var(--fs-p1); }
.p2 { font-size: var(--fs-p2); }

/* ==========================================================================
   Slides
   Layout/positioning is placeholder-simple for now — fine-tuning is a
   later step. This just stacks each slide's text, centered.
   ========================================================================== */

.slide {
  position: relative;
  z-index: 0; /* establishes a stacking context so .bg-frame-overlay's z-index is scoped to this slide (vs. .source-wrap etc.), not the whole document */
  min-height: 100vh;
  display: flex;
  align-items: center;
  justify-content: center;
  scroll-snap-align: start;
  scroll-snap-stop: always;
}

.slide-content {
  display: flex;
  flex-direction: column;
  align-items: center;
  gap: 0.5em;
  text-align: center;
  padding: 2rem;
}

.slide-columns {
  flex-direction: row;
  align-items: center;
  justify-content: space-between;
  text-align: left;
  width: 100%;
}

.slide-columns .col {
  flex: 1;
}

.col-left {
  display: flex;
  flex-direction: column;
  gap: 0.5em;
}

/* ==========================================================================
   Content position fine-tuning (per content_fixes.txt)
   .slide centers .slide-content by default; these modifiers move that
   anchor point per slide. Position (top/bottom/left/right) is set on the
   .slide itself; text/box alignment ("linksbündig") is set on .slide-content
   so it doesn't fight the .slide-columns row layout used on slides 4/5.
   ========================================================================== */

.pos-top-left {
  align-items: flex-start;
  justify-content: flex-start;
}

.pos-bottom-right {
  align-items: flex-end;
  justify-content: flex-end;
}

/* top-left like .pos-top-left, but pulled in further from the edge —
   slide 9 reads "oben links, aber nicht ganz" (top left, but not quite) */
.pos-top-left-soft {
  align-items: flex-start;
  justify-content: flex-start;
}
.pos-top-left-soft .slide-content {
  padding: 6rem 8rem;
}

.align-left {
  align-items: flex-start;
  text-align: left;
}

/* horizontal-only: box sits at the left edge, vertical centering untouched
   (used where a slide is "links" but not "oben"/"unten") */
.pos-left {
  justify-content: flex-start;
}

/* slide 7b: "mittig (bisschen weiter unten)" — vertically centered like any
   default slide, just nudged down a bit rather than sitting dead-center */
#slide-7b .slide-content {
  margin-top: 8vh;
}

/* slide 8: intro / "$2.5-$4.5" / closing paragraph all sit on the left edge;
   "BILLION" alone is nudged right ("rechts") on its own line instead of
   sharing that left edge */
#slide-8 .group-shift {
  margin-left: 40vw;
}

/* Rotated "!" / "?" marks (slides 2 and 6, per references/2.pdf & 6.pdf).
   The outer element is positioned normally (unrotated) via top/left — only
   the glyph inside is rotated about its own center, so top/left can be
   tuned directly against the reference mockups without doing rotation
   math by hand.
   NOTE: content.txt/the brief say "-90 degrees", but rotate(-90deg) puts
   the glyph's dot/tail on the far side and its bar/hook nearest the text —
   the opposite of both reference PDFs, which show the small dot first
   (nearest) and the long bar/hook trailing after it. rotate(90deg) is
   what actually matches the references; flip back if that was intentional. */
.rotated-mark {
  position: absolute;
  pointer-events: none;
}
.rotated-mark span {
  display: block;
  margin: 0;
  transform: rotate(90deg);
  transform-origin: center;
  white-space: nowrap;
}

#slide-2 .rotated-mark {
  top: 14vh;
  left: 78vw;
}

#slide-6 .rotated-mark {
  top: 44vh;
  left: 69vw;
}

/* slide 7: manual <br>s define the intended line rhythm, but the last
   segment ("and subscriptions...opinion.") is too long to fit one line at
   this type size regardless of box width — cap the box so it still reads
   as "unten rechts" instead of spanning the full viewport */
#slide-7 .slide-content {
  max-width: 75vw;
}

/* ==========================================================================
   Zoom stage (slides 3-5)
   One shared box, fixed to the same screen position for the whole slide
   3-5 sequence. Three layers stack on top of each other (video above the
   100-grid above the 764-grid) and each layer beneath is always fully
   visible at full size — script.js (initZoomStage) shrinks the *top*
   layer(s) toward the top-left corner as you scroll, until it's exactly
   the size of one cell in the grid beneath it, revealing that whole grid
   like pulling a smaller nested layer back to uncover a bigger one.
   ========================================================================== */

.zoom-stage {
  position: fixed;
  top: 2rem;
  right: 2rem;
  bottom: 2rem;
  aspect-ratio: 560 / 994; /* same as the source videos' 560:994 frame */
  z-index: 5;
  pointer-events: none; /* purely decorative, never blocks scrolling/clicks */
  /* hidden by default; script.js sets this to 1 once scroll position is
     actually within slide 3-5 — this fallback keeps it from flashing
     visible on slide 1/2 before that first runs (or if it doesn't) */
  opacity: 0;
}

.zoom-layer {
  position: absolute;
  inset: 0;
  width: 100%;
  height: 100%;
  transform-origin: top left; /* shrinks toward the corner, not the center */
  /* transform is set per scroll-frame from script.js — no CSS transition
     here, so it tracks scroll position exactly instead of animating on a
     timer */
}

#zoom-video-layer { z-index: 3; }
#zoom-grid-4 {
  z-index: 2;
  /* grid-5 sits directly behind grid-4 in the same box — without an opaque
     backdrop here, grid-4's 2px gaps would reveal grid-5's (unrelated,
     outside the shared top-left block) images instead of a neutral gap */
  background-color: var(--bg-color);
}
#zoom-grid-5 { z-index: 1; }

.slide-video {
  height: 100%;
  width: auto;
  display: block;
  object-fit: cover;
}

/* Playback of .slide-video is started/stopped from script.js
   (initZoomStage) based on the zoom-stage scroll progress. */

.frame-grid {
  /* --grid-cols/--grid-rows set inline per instance (slide 4: 10x10,
     slide 5: 28x28) so this one ruleset covers any grid size/shape */
  display: grid;
  grid-template-columns: repeat(var(--grid-cols), 1fr);
  grid-template-rows: repeat(var(--grid-rows), 1fr);
  width: 100%;
  height: 100%;
  gap: 2px; /* lets the background pattern show through between tiles */
  overflow: hidden; /* safety net: never let grid rows push past the box below */
}

.frame-grid img {
  width: 100%;
  height: 100%;
  display: block;
  object-fit: cover;
  /* grid items default to min-height/width:auto, sized off the image's own
     intrinsic pixels — at 764 tiny cells that's bigger than the 1fr track,
     which blows the grid rows out past their box. This resets that floor
     so 1fr tracks actually shrink to fit. */
  min-width: 0;
  min-height: 0;
}

/* ==========================================================================
   Source citations (per 9)sources.txt)
   Each .source-wrap is a black button (number + label) with an optional
   reveal line of text underneath it on hover/focus. Placement is fixed by
   hand per slide (keyed by data-key) rather than computed, chosen to sit in
   each slide's empty space without overlapping existing content. The
   connecting line to each slide's biggest text element (or, on slide 3,
   the video's top-left corner) is drawn by script.js (initSources), which
   measures actual rendered positions and redraws on resize since the type
   scale is viewport-relative.
   ========================================================================== */

.source-wrap {
  position: absolute;
  display: flex;
  flex-direction: column;
  align-items: flex-start;
  gap: 0.4em;
  z-index: 6; /* above the zoom-stage (5) so boxes stay clickable where they'd otherwise sit underneath it */
}

.source-box {
  display: inline-flex;
  align-items: baseline;
  gap: 0.35em;
  background: #000;
  color: #fff;
  text-decoration: none;
  line-height: 0.85;
  padding: 0.3em 0.5em;
}

.source-box .source-num {
  font-family: 'Baby Blocks', sans-serif;
  font-size: var(--fs-source);
}

.source-box .source-label {
  font-family: 'Bitcount Prop Single', sans-serif;
  font-size: var(--fs-source);
}

.source-hover {
  font-family: 'Bitcount Prop Single', sans-serif;
  font-size: var(--fs-source);
  line-height: 0.85;
  color: #000;
  max-width: 18vw;
  margin: 0;
  opacity: 0;
  pointer-events: none;
  transition: opacity 0.15s ease;
}

.source-wrap:hover .source-hover,
.source-wrap:focus-within .source-hover {
  opacity: 1;
}

.source-line {
  position: absolute;
  inset: 0;
  width: 100%;
  height: 100%;
  /* clip to this slide's own box — slide 3's anchor is the fixed
     #zoom-stage, so once slide 3 scrolls out of view its line's anchor
     end drifts outside this box; without clipping it still gets painted
     wherever that lands (e.g. over slide 2/4/5) instead of disappearing
     with the rest of slide 3 */
  overflow: hidden;
  pointer-events: none;
  z-index: 5;
}

.source-line line {
  stroke: #000;
  stroke-width: 1px;
}

.source-wrap[data-key="s1"]  { bottom: 8vh;  right: 8vw; }
.source-wrap[data-key="s3"]  { bottom: 10vh; left: 8vw; }
.source-wrap[data-key="s4a"] { bottom: 10vh; left: 6vw; }
.source-wrap[data-key="s4b"] { bottom: 10vh; left: 30vw; }
.source-wrap[data-key="s5a"] { bottom: 14vh; left: 6vw; }
.source-wrap[data-key="s5b"] { bottom: 28vh; left: 32vw; }
.source-wrap[data-key="s7"]  { top: 8vh;  left: 8vw; }
.source-wrap[data-key="s7b"] { top: 10vh; right: 8vw; }
.source-wrap[data-key="s8"]  { bottom: 6vh; right: 10vw; }

/* ==========================================================================
   Content warnings (per 11)contentwarning.txt, slides 2 and 6)
   Each .warning-box is the inverse of .source-box: white fill, black 1px
   stroke, black text. Unlike sources (hand-placed per data-key), these are
   placed randomly by script.js (initContentWarnings) — left/top are set
   inline in vw/vh so the layout, computed once per page load, keeps its
   proportions across resizes without needing to be re-randomized. The
   connecting line to the slide's biggest text element (the rotated "!"/"?",
   which at 710px outsizes the "content warning"/"what do they consume"
   heading's 290px) is drawn the same way as .source-line.

   Cursor avoidance: script.js (initContentWarnings' updateAvoidance) sets
   a transform offset on top of the left/top rest position when the cursor
   comes within range, then clears it again once the cursor moves away —
   this transition is what makes both directions read as a "dodge" instead
   of a snap.
   ========================================================================== */

.warning-wrap {
  position: absolute;
  z-index: 6;
  transition: transform 0.2s ease-out;
}

.warning-box {
  display: inline-block;
  background: #fff;
  color: #000;
  border: 1px solid #000;
  padding: 0.3em 0.5em;
  font-family: 'Bitcount Prop Single', sans-serif;
  font-size: var(--fs-warning);
  line-height: 0.85;
  white-space: nowrap;
}

.warning-line {
  position: absolute;
  inset: 0;
  width: 100%;
  height: 100%;
  overflow: hidden;
  pointer-events: none;
  z-index: 5;
}

.warning-line line {
  stroke: #000;
  stroke-width: 1px;
}

/* ==========================================================================
   Hashtags (slide 3b, per 12)hashtags.txt)
   .hashtag-box is the same black-text/white-fill/black-stroke pill as
   .warning-box, sized the same way (--fs-warning). Unlike the warnings,
   these aren't placed randomly: script.js (initHashtags) appends them in
   document order into this flex-wrap row, then hides whichever trailing
   ones don't fit within the slide's visible height, so the cutoff always
   lands on a whole box instead of clipping one via overflow.
   ========================================================================== */

.hashtag-list {
  display: flex;
  flex-wrap: wrap;
  gap: 0.6vw;
  overflow: hidden;
}

.hashtag-box {
  display: inline-block;
  background: #fff;
  color: #000;
  border: 1px solid #000;
  padding: 0.3em 0.5em;
  font-family: 'Bitcount Prop Single', sans-serif;
  font-size: var(--fs-warning);
  line-height: 0.85;
  white-space: nowrap;
}

/* ==========================================================================
   Sources list (slide 9b, per 13)slide9b.txt)
   The full bibliography for every source cited across the deck (same
   numbering as the corner .source-box citations elsewhere), plus source 8
   (the petition), which is only cited here. Same black-box/white-text
   convention as .source-box, but stacked as a list: the reference number
   sits at 30px (vs. 15px for the corner citations) beside the source name,
   with the URL wrapping onto its own line underneath. Entries are appended
   in document order by script.js (initSourcesList), same approach as
   .hashtag-list above.
   ========================================================================== */

.sources-list {
  display: flex;
  flex-direction: column;
  align-items: flex-start;
  gap: 0.6em;
}

.sources-list-item {
  display: inline-flex;
  align-items: baseline;
  gap: 0.5em;
  background: #000;
  color: #fff;
  text-decoration: none;
  padding: 0.3em 0.6em;
}

.sources-list-num {
  font-family: 'Baby Blocks', sans-serif;
  font-size: var(--fs-source-num-9b);
  line-height: 0.85;
}

.sources-list-text {
  display: flex;
  flex-direction: column;
}

.sources-list-name,
.sources-list-url {
  font-family: 'Bitcount Prop Single', sans-serif;
  font-size: var(--fs-source);
  line-height: 0.85;
}

.sources-heading {
  position: absolute;
  bottom: 6vh;
  right: 6vw;
  margin: 0;
}

/* ==========================================================================
   Petition link (slide 9, per 14)link_petition.txt)
   arrow.svg *is* the button here -- no black box like .source-box
   elsewhere. The number/label sit on the arrow's shaft (the rectangular
   part before its point), left-aligned and vertically centered on it;
   converting the arrow's own pixel width the same way as every --fs-*
   size above keeps it proportional to the 32pt text the shaft was
   originally sized to fit, whatever --type-scale ends up being.
   Placement (left/top, in vw/vh) and drift (transform) are both driven by
   script.js (initPetitionLink): left/top is a one-time random rest spot
   in the slide's bottom-right whitespace, transform is the live, eased
   pull toward the cursor layered on top of it.
   ========================================================================== */

.petition-wrap {
  position: absolute;
  z-index: 6;
}

.petition-link {
  position: relative;
  display: block;
  width: var(--petition-arrow-w);
}

.petition-arrow {
  display: block;
  width: 100%;
  height: auto;
  pointer-events: none;
}

.petition-text {
  position: absolute;
  left: 0;
  top: 29.03%;   /* shaft spans y:[38.313, 95.106] of arrow.svg's 132-tall viewBox */
  bottom: 27.95%;
  display: flex;
  align-items: center;
  gap: 0.35em;
  padding-left: 0.5em;
  white-space: nowrap;
}

.petition-num {
  font-family: 'Baby Blocks', sans-serif;
  font-size: var(--fs-petition);
  color: #fff;
  line-height: 0.85;
}

.petition-label {
  font-family: 'Bitcount Prop Single', sans-serif;
  font-size: var(--fs-petition);
  color: #fff;
  line-height: 0.85;
}

/* connects the "Sign a petition..." heading to the arrow's top-left
   corner -- same drawing approach as .source-line, redrawn continuously
   while the arrow drifts (see script.js's frame()) */
.petition-line {
  position: absolute;
  inset: 0;
  width: 100%;
  height: 100%;
  overflow: hidden;
  pointer-events: none;
  z-index: 5;
}

.petition-line line {
  stroke: #000;
  stroke-width: 1px;
}

/* ==========================================================================
   Slide 9 pop-up videos (per 15)videos.txt)
   Six clips, one fixed per slot, staggered in 3s apart on first arriving at
   slide 9. Each plays once, then hides and respawns (same clip, new random
   spot, new random size, after a random delay) -- either on its own
   ("ended") or early if clicked. Width is set inline per appearance by
   script.js (initSlideVideos), randomized between 330-500px of the 1512px
   Figma frame -- no fixed --video-pop-w custom property here.
   ========================================================================== */

.video-pop-wrap {
  position: absolute;
  z-index: 6;
}

.video-pop {
  display: block;
  height: auto;
  cursor: pointer;
}

/* shared with the content warning glitch below */
@keyframes glitch-flicker {
  0%   { opacity: 1; }
  20%  { opacity: 0.3; }
  35%  { opacity: 1; }
  55%  { opacity: 0.25; }
  70%  { opacity: 1; }
  85%  { opacity: 0.5; }
  100% { opacity: 1; }
}

/* ==========================================================================
   Content warning full-slide image glitch (slide 2, per
   17)contentwarning_glitch.txt) -- a base/r/b channel-split ghosting
   approach (see script.js's buildGlitchFilterDefs), but the wrap covers
   the entire slide (not a small random-positioned box) and there is only
   ever one instance, since a full-slide flash on top of another full-slide
   flash would be pointless.
   Hidden (display: none) except during its brief .is-flashing window.
   ========================================================================== */

.cw-glitch-wrap {
  position: absolute;
  inset: 0;
  z-index: 10; /* above every other slide-2 layer (max elsewhere is 6) */
  overflow: hidden;
  pointer-events: none; /* a glitch, not a button -- never blocks clicks */
  display: none;
}

.cw-glitch-wrap.is-flashing {
  display: block;
  animation:
    glitch-flicker var(--glitch-duration, 300ms) steps(1, end),
    cw-glitch-jitter var(--glitch-duration, 300ms) linear;
}

.cw-glitch-layer {
  position: absolute;
  inset: 0;
  width: 100%;
  height: 100%;
  object-fit: cover;
}

/* shared with .bg9b-glitch-layer below (slide 9b) -- same channel-split
   look, reused rather than redefined */
.cw-glitch-layer.glitch-r,
.cw-glitch-layer.glitch-b,
.bg9b-glitch-layer.glitch-r,
.bg9b-glitch-layer.glitch-b {
  mix-blend-mode: screen;
  opacity: 0.55;
}

.cw-glitch-layer.glitch-r,
.bg9b-glitch-layer.glitch-r { filter: url(#glitchChannelR); transform: translateX(-6px); }
.cw-glitch-layer.glitch-b,
.bg9b-glitch-layer.glitch-b { filter: url(#glitchChannelB); transform: translateX(6px); }

/* slightly larger swing than glitch-jitter (video glitches) plus a touch of
   scale jitter, since this covers the full viewport and per
   17)contentwarning_glitch.txt should read as "small position/scale jitter" */
@keyframes cw-glitch-jitter {
  0%   { translate: 0 0;      scale: 1; }
  20%  { translate: -6px 4px; scale: 1.015; }
  40%  { translate: 5px -4px; scale: 0.99; }
  60%  { translate: -4px 3px; scale: 1.02; }
  80%  { translate: 6px -3px; scale: 0.995; }
  100% { translate: 0 0;      scale: 1; }
}

/* ==========================================================================
   Background glitch (slide 9b, per 20)background_glitches.txt)
   Same base/r/b channel-split approach as the slide-2 content warning
   glitch above (reuses .glitch-r/.glitch-b's SVG filters and the
   glitch-flicker/cw-glitch-jitter keyframes), but this wrap is a true
   BACKGROUND: z-index -1 keeps it beneath .slide-content (the sources
   list/heading) instead of above it, and it never returns to
   display:none -- script.js (initBackground9bGlitch) flashes it a couple
   times then adds .is-settled, which drops the channel-split layers and
   leaves the plain jpg sitting there for good. Once settled it fully
   covers the page's rectangle-grid background (background.txt) for the
   rest of this slide.
   ========================================================================== */

.bg9b-glitch-wrap {
  position: absolute;
  inset: 0;
  z-index: -1; /* behind .slide-content, unlike .cw-glitch-wrap which flashes above it */
  overflow: hidden;
  pointer-events: none;
  display: none;
}

.bg9b-glitch-wrap.is-flashing {
  display: block;
  animation:
    glitch-flicker var(--glitch-duration, 300ms) steps(1, end),
    cw-glitch-jitter var(--glitch-duration, 300ms) linear;
}

.bg9b-glitch-wrap.is-settled {
  display: block;
  opacity: 1;
}

/* once settled it's a plain, stable image -- only the undistorted base
   layer should remain, not the channel-split ghosting */
.bg9b-glitch-wrap.is-settled .glitch-r,
.bg9b-glitch-wrap.is-settled .glitch-b {
  display: none;
}

.bg9b-glitch-layer {
  position: absolute;
  inset: 0;
  width: 100%;
  height: 100%;
  object-fit: cover;
}
