// Act IV — THE VALIDATION
// God-mode redesign 2026-04-28 (revision 2): beat-swap, 4 panels.
// Added the actual primary-research process photos Svetlana captured at
// Drexel/TJU + the CLO3D end-product reveal of the pink playsuit.
//
//   b0  Archives — TJU + Drexel partner cards
//   b1  Method  — 3 steps WITH the actual process photos
//   b2  End Product — physical garment → CLO3D digital twin
//   b3  Outcome punctum + bridge
//
// Closes rubric Cat 3 — primary research informed product design.
// Globals are VAL_-prefixed (was the source of the 2026-04-27 namespace bug).

const VAL_PARTNERS = [
  {
    name: 'Thomas Jefferson University',
    where: 'Philadelphia · University Archives',
    note: 'Site-visits and hands-on garment work. Measurement, documentation, construction analysis under archive supervision.',
  },
  {
    name: 'Drexel University · Fox Historic Costume Collection',
    where: 'Philadelphia · Fox Collection',
    note: 'Curator-led access to a historic costume collection. The reconstruction workflow tested here became the VintaVerse pipeline.',
  },
];

// Method steps now carry their actual process photographs taken on-site at
// Drexel/TJU during Svetlana's primary-research visits.
const VAL_METHOD = [
  {
    n: '01',
    title: 'Measure',
    body: 'Precise measurements taken in person under curator supervision. Cotton gloves, no tools that damage fiber.',
    photo: 'assets/validation-measure.webp',
    photoAlt: 'Hands in blue archival gloves measuring a pink rayon playsuit at the archive',
  },
  {
    n: '02',
    title: 'Reconstruct',
    body: 'Garment documented flat with construction notes. Ribbon lengths, pleat depths, button geometry — all logged for the digital build.',
    photo: 'assets/validation-laidout.webp',
    photoAlt: 'The pink playsuit laid out flat with measuring tape across the bodice',
  },
  {
    n: '03',
    title: 'Validate',
    body: 'Curator signs off on the reconstruction. Technical accuracy locked. Institutional credibility earned.',
    photo: 'assets/pink-playsuit-clo3d.webp',
    photoAlt: 'CLO3D digital twin reconstruction of the pink playsuit, validated by archive curator',
  },
];

function ValidationOpenPanel({ vis }) {
  const eased = 1 - Math.pow(1 - vis, 3);
  return (
    <div style={{
      position: 'absolute', inset: 0,
      opacity: eased, transform: `translateY(${(1 - eased) * 24}px)`,
      transition: 'opacity 0.7s ease, transform 0.7s ease',
      pointerEvents: vis > 0.05 ? 'auto' : 'none',
      display: 'flex', alignItems: 'center', justifyContent: 'center',
      padding: '0 80px',
    }}>
      <div style={{ width: '100%', maxWidth: 1480 }}>
        <div style={{ display: 'flex', alignItems: 'baseline', gap: 16, marginBottom: 18 }}>
          <span className="label-text" style={{ color: 'var(--sage)', letterSpacing: '0.32em' }}>The Validation · primary research</span>
          <div style={{ flex: 1, height: 1, background: 'rgba(143,166,143,0.22)' }} />
          <span className="label-text" style={{ color: 'var(--accent-key)' }}>
            ◆ curator-validated · 2025–2026
          </span>
        </div>
        <h3 className="font-display" style={{
          fontSize: 'clamp(48px, 5.6vw, 92px)',
          lineHeight: 0.98, letterSpacing: '-0.02em',
          color: 'var(--parchment)', margin: '0 0 36px', textTransform: 'uppercase',
        }}>
          We tested this <span style={{ color: 'var(--accent-key)', fontStyle: 'italic' }}>in real archives</span>.
        </h3>
        <div style={{ display: 'grid', gridTemplateColumns: '1fr 1fr', gap: 22 }}>
          {VAL_PARTNERS.map((p) => (
            <div key={p.name} style={{
              padding: '28px 28px',
              background: 'rgba(7,50,66,0.55)',
              border: '1px solid rgba(114,145,232,0.22)',
              borderLeft: '3px solid var(--sage)',
            }}>
              <div className="label-text" style={{ color: 'var(--sage)' }}>{p.where}</div>
              <h4 className="font-display" style={{
                margin: '12px 0 0', fontSize: 26, color: 'var(--parchment)',
                lineHeight: 1.15, letterSpacing: '-0.005em', textTransform: 'uppercase',
              }}>{p.name}</h4>
              <p className="font-editorial" style={{
                margin: '14px 0 0', fontSize: 15, color: 'var(--parchment-dim)',
                lineHeight: 1.55, fontStyle: 'italic',
              }}>{p.note}</p>
            </div>
          ))}
        </div>
        <p className="font-editorial" style={{
          marginTop: 36, fontSize: 17, lineHeight: 1.55,
          color: 'var(--parchment)', fontStyle: 'italic', maxWidth: 1100,
          textWrap: 'pretty',
        }}>
          Two collections. Curator-supervised. Real garments measured in person, reconstructed in 3D, taught back to students.
        </p>
      </div>
    </div>
  );
}

function ValidationMethodPanel({ vis }) {
  const eased = 1 - Math.pow(1 - vis, 3);
  const ARCHIVE_GRADE = 'sepia(0.06) saturate(0.95) contrast(1.04) brightness(0.99)';
  return (
    <div style={{
      position: 'absolute', inset: 0,
      opacity: eased, transform: `translateY(${(1 - eased) * 24}px)`,
      transition: 'opacity 0.7s ease, transform 0.7s ease',
      pointerEvents: vis > 0.05 ? 'auto' : 'none',
      display: 'flex', alignItems: 'center', justifyContent: 'center',
      padding: '0 80px',
    }}>
      <div style={{ width: '100%', maxWidth: 1480 }}>
        <div style={{ display: 'flex', alignItems: 'baseline', gap: 16, marginBottom: 18 }}>
          <span className="label-text" style={{ color: 'var(--sage)', letterSpacing: '0.32em' }}>Method · the workflow that came out of the field</span>
          <div style={{ flex: 1, height: 1, background: 'rgba(143,166,143,0.22)' }} />
          <span className="label-text" style={{ color: 'var(--taupe)' }}>Drexel · TJU · 2025–2026</span>
        </div>
        <h3 className="font-display" style={{
          fontSize: 'clamp(40px, 4.4vw, 64px)',
          lineHeight: 0.98, letterSpacing: '-0.02em',
          color: 'var(--parchment)', margin: '0 0 32px', textTransform: 'uppercase',
        }}>
          Measure. Reconstruct. <span style={{ color: 'var(--gold)', fontStyle: 'italic' }}>Validate.</span>
        </h3>
        <div style={{ display: 'grid', gridTemplateColumns: 'repeat(3, 1fr)', gap: 22 }}>
          {VAL_METHOD.map((m) => (
            <div key={m.n} style={{
              position: 'relative',
              background: 'rgba(7,50,66,0.55)',
              border: '1px solid rgba(255,255,255,0.08)',
              borderTop: '2px solid var(--accent-key)',
              overflow: 'hidden',
            }}>
              {/* Process photo from the field */}
              <div style={{
                position: 'relative',
                aspectRatio: '4 / 3',
                background: 'rgba(0,0,0,0.25)',
                overflow: 'hidden',
              }}>
                <img
                  src={m.photo}
                  alt={m.photoAlt}
                  style={{
                    width: '100%', height: '100%',
                    objectFit: 'cover',
                    objectPosition: 'center',
                    filter: ARCHIVE_GRADE,
                  }}
                />
                {/* Step badge over the photo */}
                <div style={{
                  position: 'absolute', top: 14, left: 14,
                  padding: '6px 12px',
                  background: 'rgba(0,24,37,0.78)',
                  border: '1px solid var(--accent-key)',
                  backdropFilter: 'blur(6px)',
                }}>
                  <span className="label-text" style={{
                    color: 'var(--accent-key)', letterSpacing: '0.32em',
                    fontSize: 10,
                  }}>STEP · {m.n}</span>
                </div>
              </div>
              {/* Caption */}
              <div style={{ padding: '22px 24px 22px' }}>
                <h4 className="font-display" style={{
                  margin: 0, fontSize: 28, color: 'var(--parchment)',
                  lineHeight: 1, letterSpacing: '-0.01em', textTransform: 'uppercase',
                }}>{m.title}</h4>
                <p className="font-editorial" style={{
                  margin: '14px 0 0', fontSize: 14, color: 'var(--parchment-dim)',
                  lineHeight: 1.55,
                }}>{m.body}</p>
              </div>
            </div>
          ))}
        </div>
        <p className="font-editorial" style={{
          marginTop: 22, fontSize: 13, color: 'var(--taupe)',
          fontStyle: 'italic', textAlign: 'right',
        }}>
          Photos · Drexel Fox Historic Costume Collection · Shalumova field study
        </p>
      </div>
    </div>
  );
}

// Live 3D viewer for the pink-playsuit GLB. We tried Google's <model-viewer>
// custom element first but React 18 + babel-standalone wasn't registering
// boolean attributes reliably and the element silently failed. Direct Three.js
// works around all of that.
//
// Static fallback (added 2026-04-29): Svet hit "three context failed" once
// during a rehearsal walk. WebGL context loss is intermittent — driver state,
// tab visibility, GPU memory pressure can all kill a context. If init fails,
// the GLB load errors, or the context goes lost mid-flight, we swap to the
// static CLO3D PNG (assets/pink-playsuit-clo3d.webp) so the slide always
// shows the digital twin. A small note appears so it's clear this is the
// fallback, with a button to retry the live viewer.
function Playsuit3DViewer() {
  const containerRef = window.useRef(null);
  const [ready, setReady] = window.useState(!!window.__threeReady);
  const [errMsg, setErrMsg] = window.useState(null);
  const [retryNonce, setRetryNonce] = window.useState(0);

  window.useEffect(() => {
    if (ready) return;
    const onReady = () => setReady(true);
    window.addEventListener('three-ready', onReady);
    return () => window.removeEventListener('three-ready', onReady);
  }, [ready]);

  window.useEffect(() => {
    if (!ready) return;
    if (errMsg) return;  // already in fallback mode — don't try again until retry
    const container = containerRef.current;
    if (!container) return;
    const THREE = window.THREE;
    const GLTFLoader = window.GLTFLoader;
    const OrbitControls = window.OrbitControls;
    const MeshoptDecoder = window.MeshoptDecoder;
    if (!THREE || !GLTFLoader) {
      setErrMsg('Three.js not loaded');
      return;
    }

    let w = container.clientWidth;
    let h = container.clientHeight;
    if (w < 50 || h < 50) {
      // figure may not have laid out yet — wait one frame
      requestAnimationFrame(() => {
        w = container.clientWidth;
        h = container.clientHeight;
      });
    }

    const scene = new THREE.Scene();
    const camera = new THREE.PerspectiveCamera(28, w / h, 0.05, 100);
    camera.position.set(0, 0.95, 3.4);

    // WebGLRenderer creation can throw if WebGL is unavailable, blocked,
    // or the GPU is in a bad state. Guard so the deck never crashes.
    let renderer;
    try {
      renderer = new THREE.WebGLRenderer({
        antialias: true,
        alpha: true,
        failIfMajorPerformanceCaveat: false,  // allow software fallback
        powerPreference: 'default',
      });
    } catch (e) {
      console.error('WebGLRenderer init failed', e);
      setErrMsg('WebGL unavailable — showing static render');
      return;
    }
    renderer.setSize(w, h);
    renderer.setPixelRatio(Math.min(window.devicePixelRatio, 2));
    renderer.outputColorSpace = THREE.SRGBColorSpace;
    renderer.toneMapping = THREE.ACESFilmicToneMapping;
    renderer.toneMappingExposure = 1.05;
    renderer.setClearColor(0x000000, 0);
    container.appendChild(renderer.domElement);

    // If the browser drops the context (tab background, GPU pressure, driver
    // crash), swap to the static fallback. preventDefault tells the browser
    // we're handling it ourselves.
    const onContextLost = (e) => {
      e.preventDefault();
      console.warn('WebGL context lost — switching to static fallback');
      setErrMsg('WebGL context lost — showing static render');
    };
    renderer.domElement.addEventListener('webglcontextlost', onContextLost, false);

    // Lighting — three-point setup so the white-ish dress reads with depth
    scene.add(new THREE.AmbientLight(0xffffff, 0.55));
    const key = new THREE.DirectionalLight(0xffffff, 1.4);
    key.position.set(2.4, 4.0, 3.0);
    scene.add(key);
    const fill = new THREE.DirectionalLight(0xc0d4ff, 0.55);
    fill.position.set(-3.0, 2.0, 2.0);
    scene.add(fill);
    const rim = new THREE.DirectionalLight(0xffe2b0, 0.4);
    rim.position.set(0, 1.5, -3.0);
    scene.add(rim);

    // Model
    const loader = new GLTFLoader();
    if (MeshoptDecoder) loader.setMeshoptDecoder(MeshoptDecoder);

    let model = null;
    let raf = 0;
    let disposed = false;

    loader.load(
      'assets/pink-playsuit-1930.glb',
      (gltf) => {
        if (disposed) return;
        model = gltf.scene;
        // Center horizontally + vertically; place model so its feet sit at y=0
        const box = new THREE.Box3().setFromObject(model);
        const size = box.getSize(new THREE.Vector3());
        const center = box.getCenter(new THREE.Vector3());
        model.position.x -= center.x;
        model.position.z -= center.z;
        model.position.y -= box.min.y;
        // Aim camera at the figure's mid-height
        camera.lookAt(0, size.y * 0.5, 0);
        // Scale so the figure roughly fits a 1.7m frame regardless of source
        const targetH = 1.7;
        if (size.y > 0) {
          const k = targetH / size.y;
          model.scale.setScalar(k);
        }
        scene.add(model);
      },
      undefined,
      (err) => {
        if (disposed) return;
        console.error('GLB load error', err);
        setErrMsg('Could not load 3D model');
      }
    );

    // Orbit controls — auto rotate, soft damp
    const controls = new OrbitControls(camera, renderer.domElement);
    controls.enableDamping = true;
    controls.dampingFactor = 0.06;
    controls.autoRotate = true;
    controls.autoRotateSpeed = 0.85; // ~5°/sec; slow + cinematic
    controls.target.set(0, 0.85, 0);
    controls.minDistance = 1.6;
    controls.maxDistance = 6.0;
    controls.enablePan = false;
    controls.update();

    const tick = () => {
      controls.update();
      renderer.render(scene, camera);
      raf = requestAnimationFrame(tick);
    };
    tick();

    const onResize = () => {
      const nw = container.clientWidth;
      const nh = container.clientHeight;
      if (nw < 50 || nh < 50) return;
      renderer.setSize(nw, nh);
      camera.aspect = nw / nh;
      camera.updateProjectionMatrix();
    };
    window.addEventListener('resize', onResize);
    const ro = new ResizeObserver(onResize);
    ro.observe(container);

    return () => {
      disposed = true;
      cancelAnimationFrame(raf);
      window.removeEventListener('resize', onResize);
      ro.disconnect();
      controls.dispose();
      renderer.domElement.removeEventListener('webglcontextlost', onContextLost);
      // forceContextLoss helps free the WebGL context immediately on unmount
      try { renderer.forceContextLoss(); } catch {}
      renderer.dispose();
      if (renderer.domElement.parentNode === container) {
        container.removeChild(renderer.domElement);
      }
    };
  }, [ready, retryNonce, errMsg]);

  // Fallback render: static CLO3D PNG. Same aspect / position as the live
  // viewer would occupy, so the layout doesn't shift. Includes a quiet
  // retry affordance — Svet can tap to attempt the live viewer again.
  if (errMsg) {
    return (
      <div style={{ position: 'absolute', inset: 0 }}>
        <img
          src="assets/pink-playsuit-clo3d.webp"
          alt="CLO3D digital twin reconstruction of the pink playsuit (static fallback)"
          style={{
            width: '100%', height: '100%',
            objectFit: 'contain', padding: '6%',
            filter: 'sepia(0.04) saturate(0.92) contrast(1.06) brightness(1.02)',
          }}
        />
        <button
          onClick={() => { setErrMsg(null); setRetryNonce(n => n + 1); }}
          aria-label="Retry live 3D viewer"
          title="Retry live 3D viewer"
          style={{
            position: 'absolute', top: 14, right: 14,
            padding: '6px 12px',
            background: 'rgba(0,24,37,0.6)',
            border: '1px solid rgba(143,166,143,0.4)',
            color: 'var(--sage)',
            fontFamily: 'var(--font-ui)', fontSize: 9,
            letterSpacing: '0.22em', textTransform: 'uppercase',
            cursor: 'pointer',
            backdropFilter: 'blur(6px)',
          }}
        >Retry 3D</button>
      </div>
    );
  }

  return (
    <div ref={containerRef} style={{
      position: 'absolute', inset: 0,
      cursor: 'grab',
    }} />
  );
}

// Panel 2 — End Product. The physical garment from the archive →
// CLO3D digital twin. The pink playsuit is the SAME object Svetlana
// measured in the photos on the previous panel. This is the proof.
function ValidationProductPanel({ vis }) {
  const eased = 1 - Math.pow(1 - vis, 3);
  const PHYSICAL_GRADE = 'sepia(0.06) saturate(0.95) contrast(1.04) brightness(0.99)';
  const TWIN_GRADE = 'sepia(0.04) saturate(0.92) contrast(1.06) brightness(1.02)';
  return (
    <div style={{
      position: 'absolute', inset: 0,
      opacity: eased, transform: `translateY(${(1 - eased) * 24}px)`,
      transition: 'opacity 0.7s ease, transform 0.7s ease',
      pointerEvents: vis > 0.05 ? 'auto' : 'none',
      display: 'flex', alignItems: 'center', justifyContent: 'center',
      padding: '0 80px',
    }}>
      <div style={{ width: '100%', maxWidth: 1480 }}>
        <div style={{ display: 'flex', alignItems: 'baseline', gap: 16, marginBottom: 18 }}>
          <span className="label-text" style={{ color: 'var(--sage)', letterSpacing: '0.32em' }}>The end product · physical → digital</span>
          <div style={{ flex: 1, height: 1, background: 'rgba(143,166,143,0.22)' }} />
          <span className="label-text" style={{ color: 'var(--accent-key)' }}>
            ◆ same garment · two states
          </span>
        </div>
        <h3 className="font-display" style={{
          fontSize: 'clamp(40px, 4.4vw, 64px)',
          lineHeight: 0.98, letterSpacing: '-0.02em',
          color: 'var(--parchment)', margin: '0 0 28px', textTransform: 'uppercase',
        }}>
          From the archive, <span style={{ color: 'var(--gold)', fontStyle: 'italic' }}>to the platform.</span>
        </h3>
        {/* Diptych — physical archive photo + CLO3D digital twin */}
        <div style={{
          display: 'grid', gridTemplateColumns: '1fr auto 1fr',
          gap: 24, alignItems: 'center',
        }}>
          {/* Physical garment, from the field */}
          <figure style={{
            margin: 0, position: 'relative',
            aspectRatio: '4 / 5',
            background: 'rgba(0,0,0,0.25)',
            border: '1px solid rgba(143,166,143,0.18)',
            overflow: 'hidden',
          }}>
            <img
              src="assets/validation-fullview.webp"
              alt="Physical pink playsuit at the Drexel archive"
              style={{
                width: '100%', height: '100%',
                objectFit: 'cover', objectPosition: 'center',
                filter: PHYSICAL_GRADE,
              }}
            />
            <figcaption style={{
              position: 'absolute', bottom: 0, left: 0, right: 0,
              padding: '14px 18px',
              background: 'linear-gradient(0deg, rgba(0,24,37,0.92), rgba(0,24,37,0))',
            }}>
              <span className="label-text" style={{
                color: 'var(--parchment)', letterSpacing: '0.32em', fontSize: 10,
              }}>Physical · Drexel Fox Collection</span>
              <div className="font-editorial" style={{
                fontSize: 14, color: 'var(--parchment-dim)',
                marginTop: 4, fontStyle: 'italic',
              }}>
                Pink rayon playsuit · c. 1930s
              </div>
            </figcaption>
          </figure>
          {/* Arrow */}
          <div style={{ textAlign: 'center', padding: '0 16px' }}>
            <div style={{
              fontFamily: 'var(--font-display)', fontSize: 56,
              color: 'var(--gold)', fontStyle: 'italic', lineHeight: 1,
            }}>→</div>
            <div className="label-text" style={{
              color: 'var(--gold)', letterSpacing: '0.32em', marginTop: 14,
            }}>CLO3D</div>
            <div className="font-editorial" style={{
              fontSize: 12, color: 'var(--parchment-dim)',
              fontStyle: 'italic', marginTop: 6, maxWidth: 140,
            }}>
              Curator-supervised<br />digital reconstruction
            </div>
          </div>
          {/* CLO3D digital twin — LIVE Three.js viewer with auto-rotate */}
          <figure style={{
            margin: 0, position: 'relative',
            aspectRatio: '4 / 5',
            background: 'linear-gradient(180deg, rgba(114,145,232,0.16) 0%, rgba(0,24,37,0.96) 95%)',
            border: '1px solid rgba(201,168,76,0.32)',
            overflow: 'hidden',
          }}>
            <Playsuit3DViewer />
            <figcaption style={{
              position: 'absolute', bottom: 0, left: 0, right: 0,
              padding: '14px 18px',
              background: 'linear-gradient(0deg, rgba(0,24,37,0.92), rgba(0,24,37,0))',
              zIndex: 2,
              pointerEvents: 'none',
            }}>
              <span className="label-text" style={{
                color: 'var(--gold)', letterSpacing: '0.32em', fontSize: 10,
              }}>Digital twin · CLO3D · live 3D · 2026</span>
              <div className="font-editorial" style={{
                fontSize: 14, color: 'var(--parchment-dim)',
                marginTop: 4, fontStyle: 'italic',
              }}>
                Inspectable · citable · teachable
              </div>
            </figcaption>
          </figure>
        </div>
        <p className="font-editorial" style={{
          marginTop: 28, fontSize: 16, lineHeight: 1.55,
          color: 'var(--parchment)', fontStyle: 'italic', textAlign: 'center',
          maxWidth: 920, margin: '28px auto 0', textWrap: 'pretty',
        }}>
          The same garment, archived to spec — now accessible to a student in
          Nebraska or a researcher in Kyiv without leaving the curator&rsquo;s authority behind.
        </p>
      </div>
    </div>
  );
}

function ValidationOutcomePanel({ vis }) {
  const eased = 1 - Math.pow(1 - vis, 3);
  return (
    <div style={{
      position: 'absolute', inset: 0,
      opacity: eased, transform: `translateY(${(1 - eased) * 24}px)`,
      transition: 'opacity 0.7s ease, transform 0.7s ease',
      pointerEvents: vis > 0.05 ? 'auto' : 'none',
      display: 'flex', alignItems: 'center', justifyContent: 'center',
      padding: '0 80px',
    }}>
      <div style={{ width: '100%', maxWidth: 1100, textAlign: 'center' }}>
        <span className="label-text" style={{
          color: 'var(--accent-key)', fontSize: 11, letterSpacing: '0.36em',
        }}>The result</span>
        <div className="font-display" style={{
          fontSize: 'clamp(56px, 6vw, 96px)',
          lineHeight: 0.98, letterSpacing: '-0.02em',
          margin: '20px 0 32px',
          color: 'var(--parchment)',
        }}>
          <span style={{ color: 'var(--accent-key)' }}>Higher engagement.</span><br />
          <span style={{ fontStyle: 'italic' }}>Deeper understanding.</span>
        </div>
        <p className="font-editorial" style={{
          fontSize: 17, lineHeight: 1.6,
          color: 'var(--parchment)', maxWidth: 820, margin: '0 auto',
          fontStyle: 'italic', textWrap: 'pretty',
        }}>
          Learning outcomes were measured comparing 3D interaction to physical archive access.
          Students showed higher engagement and deeper understanding when the archive came to them
          as an inspectable digital twin — without losing the curator&rsquo;s authority.
        </p>
        <div style={{ marginTop: 48 }}>
          <span className="label-text" style={{ color: 'var(--accent-key)' }}>Next · the customer</span>
          <div className="font-display" style={{
            fontSize: 28, color: 'var(--parchment)',
            marginTop: 6, letterSpacing: '-0.01em', fontStyle: 'italic',
          }}>
            Emma · Dr. Grey →
          </div>
        </div>
      </div>
    </div>
  );
}

function ActValidationInner({ index, total, act }) {
  const p0 = useBeatProgress(0);
  const p1 = useBeatProgress(1);
  const p2 = useBeatProgress(2);
  const p3 = useBeatProgress(3);

  const visOpen    = panelVis(p0, p1);
  const visMethod  = panelVis(p1, p2);
  const visProduct = panelVis(p2, p3);
  const visOutcome = p3;

  return (
    <ActFrame act={act} index={index} total={total} bg="var(--petrol-deep)" bleed>
      <div style={{ position: 'absolute', inset: 0, zIndex: 4 }}>
        <ValidationOpenPanel vis={visOpen} />
        <ValidationMethodPanel vis={visMethod} />
        <ValidationProductPanel vis={visProduct} />
        <ValidationOutcomePanel vis={visOutcome} />
      </div>
    </ActFrame>
  );
}

function ActValidation({ index, total, act }) {
  return (
    <ScrollBeats beats={4} bg="var(--petrol-deep)">
      <ActValidationInner index={index} total={total} act={act} />
    </ScrollBeats>
  );
}

window.ActValidation = ActValidation;
