// App v5 — same architecture as v3/v4, new section order:
// Hero → Invitation → Pillars → Card Draw → Story (bio) → Why the Raven → Bridge → Gatherings → Contact → Footer

const { useEffect: useEffectA5, useState: useStateA5, useCallback: useCallbackA5 } = React;

const THEME_KEY_V5 = 'up_v5_theme';

function AppV5() {
  // Default twilight; load from localStorage if user has changed it
  const [theme, setTheme] = useStateA5(() => {
    try {
      const stored = localStorage.getItem(THEME_KEY_V5);
      return stored === 'parchment' ? 'parchment' : 'twilight';
    } catch {
      return 'twilight';
    }
  });

  useEffectA5(() => {
    document.body.dataset.theme = theme;
    try { localStorage.setItem(THEME_KEY_V5, theme); } catch {}
  }, [theme]);

  const toggleTheme = useCallbackA5(() => {
    setTheme(t => t === 'twilight' ? 'parchment' : 'twilight');
  }, []);

  // Fade-in observer
  useEffectA5(() => {
    document.documentElement.setAttribute('data-fadein-ready', '1');
    const els = document.querySelectorAll('.fade-in');
    els.forEach(el => {
      const r = el.getBoundingClientRect();
      if (r.top < window.innerHeight && r.bottom > 0) {
        el.classList.add('visible');
      }
    });
    const io = new IntersectionObserver((entries) => {
      entries.forEach(e => {
        if (e.isIntersecting) {
          e.target.classList.add('visible');
          io.unobserve(e.target);
        }
      });
    }, { threshold: 0, rootMargin: '0px 0px -5% 0px' });
    els.forEach(el => {
      if (!el.classList.contains('visible')) io.observe(el);
    });
    const safety = setTimeout(() => {
      document.querySelectorAll('.fade-in:not(.visible)').forEach(el => el.classList.add('visible'));
    }, 3000);
    return () => { io.disconnect(); clearTimeout(safety); };
  }, []);

  // Smooth scroll on in-page anchor clicks
  useEffectA5(() => {
    const onClick = (e) => {
      const a = e.target.closest('a[href^="#"]');
      if (!a) return;
      const id = a.getAttribute('href').slice(1);
      if (!id) return;
      const target = document.getElementById(id);
      if (!target) return;
      e.preventDefault();
      const top = target.getBoundingClientRect().top + window.scrollY - 80;
      window.scrollTo({ top, behavior: 'smooth' });
      try { history.replaceState(null, '', '#' + id); } catch {}
    };
    document.addEventListener('click', onClick);
    return () => document.removeEventListener('click', onClick);
  }, []);

  // ===========================================================================
  // Perching raven (v7) — sprite-driven state machine
  // ---------------------------------------------------------------------------
  // Six animation states, each backed by real artwork frames:
  //   PERCHED        — bird is sitting on a section title (idle)
  //   PERCHED_IDLE   — bird occasionally turns its head (headturn sheet)
  //   TAKEOFF        — perched → airborne (takeoff sheet, 4 frames, plays once)
  //   CRUISING       — flying along a path (interleaved flap + downflap, loops)
  //   BRAKING        — approaching perch, slowing down (brake sheet 0-1)
  //   LANDING        — final descent to perch (brake sheet 2-3)
  //
  // Position is still rAF-driven with smoothing (kept from v6 — that part
  // worked well). What changed:
  //   - The wing animation is no longer a CSS scale-Y squash — it's real
  //     hand-drawn frames swapped on .raven-frame.src
  //   - Direction-aware: when flying leftward, the .raven-flip layer
  //     applies scaleX(-1) (mirroring the right-facing frames)
  //   - Headturn idle: when perched and stationary >IDLE_HEADTURN_MS, plays
  //     one head-turn cycle (front → over-shoulder → back to front)
  // ===========================================================================
  useEffectA5(() => {
    const raven = document.getElementById('perchingRaven');
    if (!raven) return;

    // Respect reduced motion — hide the raven entirely for those users
    const prefersReducedMotion = window.matchMedia('(prefers-reduced-motion: reduce)').matches;
    if (prefersReducedMotion) {
      raven.style.display = 'none';
      return;
    }

    const tilt = raven.querySelector('.raven-tilt');
    const bob = raven.querySelector('.raven-bob');
    const flip = raven.querySelector('.raven-flip');
    const frameImg = raven.querySelector('.raven-frame');

    // ---- Tunables --------------------------------------------------------
    const RAVEN_W = 140;           // matches CSS
    const EDGE_PAD = 16;
    const PERCH_OFFSET_X = 18;     // perch slightly to the right of title
    const PERCH_OFFSET_Y = -78;    // perch above the title baseline
    const POS_SMOOTH = 0.075;      // position lerp factor (per frame at ~60fps)
    const TILT_SMOOTH = 0.10;      // banking lerp
    const MAX_TILT_DEG = 18;       // bird banks up to this much
    const TILT_PER_VX = 0.50;      // deg per px/frame of horizontal velocity
    const TILT_PER_VY = 0.08;      // a bit of nose-up/down with vertical speed
    const IDLE_MS = 600;           // ms of no-scroll before we lock onto perch
    const LAND_DIST = 12;          // px from perch target to count as landed
    const BRAKE_DIST = 80;         // px from perch target to enter BRAKING state
    const CRUISE_SPEED = 1.2;      // px/frame above which we're "actively flying"
    const GLIDE_SPEED = 0.6;       // px/frame below which we glide (slow flap)
    const DRIFT_AMP_X = 14;        // sideways drift amplitude (px) while cruising
    const DRIFT_AMP_Y = 6;         // vertical drift amplitude (px)
    const BOB_AMP = 5;             // body bob amplitude (px) during flight

    // Sprite-cycle timing — calm pacing for a contemplative site
    const FLAP_FRAME_MS = 90;      // 90ms × 8 frames = 720ms full flap cycle
    const TAKEOFF_FRAME_MS = 110;  // 4 frames × 110ms = 440ms takeoff
    const BRAKE_FRAME_MS = 120;    // 4 frames × 120ms = 480ms brake/land
    const GLIDE_FRAME_MS = 180;    // slower frame cycle when gliding
    const HEADTURN_FRAME_MS = 280; // slow, deliberate head turn
    const IDLE_HEADTURN_MIN = 4500; // min ms between head turns
    const IDLE_HEADTURN_MAX = 9000; // max ms between head turns

    // ---- Frame sets ------------------------------------------------------
    // All frames are right-facing in source. The .raven-flip layer mirrors
    // them when the bird is moving left.
    // Eight-frame flap cycle interleaves "flap" (upper-arc) with "downflap"
    // (power downstroke) for a complete wing-beat arc.
    const FRAMES = {
      // Upper-arc + downstroke woven together for a real beat cycle.
      // Order picked to read as: wings high → mid → power-down → low → recovery → up.
      flap: [
        'assets/raven-frames/flap-1.png',     // wings high (top of upstroke)
        'assets/raven-frames/flap-2.png',     // wings mid-high (transitioning down)
        'assets/raven-frames/downflap-0.png', // wings sweeping down
        'assets/raven-frames/downflap-1.png', // wings mid-down (power)
        'assets/raven-frames/downflap-2.png', // wings fully extended down (bottom)
        'assets/raven-frames/downflap-3.png', // wings back+down (end of power stroke)
        'assets/raven-frames/flap-0.png',     // wings cupped (recovery, coming back up)
        'assets/raven-frames/flap-3.png',     // wings up-back (returning to top)
      ],
      // Takeoff: perched → crouch → wings up → wings climbing
      takeoff: [
        'assets/raven-frames/takeoff-0.png',
        'assets/raven-frames/takeoff-1.png',
        'assets/raven-frames/takeoff-2.png',
        'assets/raven-frames/takeoff-3.png',
      ],
      // Brake + land: wings up → cupped forward → folding → perched
      brake: [
        'assets/raven-frames/brake-0.png',
        'assets/raven-frames/brake-1.png',
        'assets/raven-frames/brake-2.png',
        'assets/raven-frames/brake-3.png',
      ],
      // Idle perched — uses brake-3 (clean perched silhouette)
      perched: ['assets/raven-frames/brake-3.png'],
      // Head turn: front → 3/4 → profile → over-shoulder
      headturn: [
        'assets/raven-frames/headturn-0.png',
        'assets/raven-frames/headturn-1.png',
        'assets/raven-frames/headturn-2.png',
        'assets/raven-frames/headturn-3.png',
      ],
    };

    // Preload all frames so the first sprite swap doesn't flash an empty img
    const preloaded = [];
    Object.values(FRAMES).flat().forEach(src => {
      const i = new Image();
      i.src = src;
      preloaded.push(i);
    });

    // ---- States ----------------------------------------------------------
    // Finite state machine for the raven's behavior.
    const STATE = {
      OFFSCREEN: 'offscreen',
      TAKEOFF: 'takeoff',
      CRUISING: 'cruising',
      BRAKING: 'braking',
      LANDING: 'landing',
      PERCHED: 'perched',
      HEADTURN: 'headturn',
    };
    let state = STATE.OFFSCREEN;
    let stateStart = 0;            // ms timestamp when current state began
    let frameIdx = 0;              // index into current state's frame set
    let lastFrameSwap = 0;         // ms timestamp of last frame change

    // Helper: switch state, reset counters
    function setState(newState, now) {
      state = newState;
      stateStart = now;
      frameIdx = 0;
      lastFrameSwap = now;
    }

    // Helper: advance frame index based on current state's timing.
    // Returns one of:
    //   'wait'      — not enough time has passed; nothing changed
    //   'advanced'  — frame index moved to a new value within the set
    //   'looped'    — wrapped from end back to start (loop sets only)
    //   'finished'  — non-loop sets: just left the final frame in place,
    //                  signalling the one-shot animation is complete.
    //                  Caller should transition to the next state.
    function advanceFrame(now, set, intervalMs, loop) {
      if (now - lastFrameSwap < intervalMs) return 'wait';
      lastFrameSwap = now;
      // If we're already at the last frame of a non-looping set, this is
      // the moment to declare "finished" — without resetting the timer
      // further. Clamp frameIdx, return 'finished'.
      if (!loop && frameIdx >= set.length - 1) {
        frameIdx = set.length - 1;
        return 'finished';
      }
      frameIdx++;
      if (frameIdx >= set.length) {
        if (loop) { frameIdx = 0; return 'looped'; }
        frameIdx = set.length - 1;
      }
      return 'advanced';
    }

    // Helper: paint the current frame to the img element
    let lastSrc = '';
    function paintFrame(src) {
      if (src !== lastSrc) {
        frameImg.src = src;
        lastSrc = src;
      }
    }

    // ---- Position state --------------------------------------------------
    // current rendered position
    let x = -200, y = -200;
    // target position the raven wants to be at
    let tx = -200, ty = -200;
    // last frame position for velocity
    let lastX = x, lastY = y;
    // smoothed velocity
    let vx = 0, vy = 0;
    // heavily-smoothed horizontal velocity used for direction decisions
    // (raw vx oscillates with drift sine waves; we don't want the bird to
    // flip back and forth from drift, only from real motion)
    let vxSmooth = 0;
    // current tilt (deg) and smoothed target
    let tilt_cur = 0;
    // facing direction: 1 = right, -1 = left. Smoothed via flipBlend.
    // Start facing left because the bird seeds offscreen-right and flies
    // leftward into view (negative vx). Without this, the first frames would
    // show her facing right while moving left — backwards.
    let facing = -1;
    let flipBlend = -1;

    let visible = false;
    let landedTarget = null;       // element we currently perch on
    let lastScrollAt = 0;
    let lastScrollY = window.scrollY;
    let nextHeadturnAt = 0;        // when to play next idle headturn
    let rafId = 0;
    let t0 = performance.now();

    // ---- Helpers ---------------------------------------------------------
    const clamp = (v, a, b) => Math.max(a, Math.min(b, v));

    // Find the section title nearest a given viewport-Y anchor (default
    // 35% down the viewport).
    //
    // Returns { el, x, y } — ALWAYS returns a valid perch:
    //   - If a title is in (or near) the viewport, perch beside it.
    //   - Otherwise, fall back to a "virtual perch" at the right edge of the
    //     viewport at ~35% down — so the bird gracefully glides to a hover
    //     instead of cruising forever in the gap between distant sections.
    const findNearestTitle = (anchorY) => {
      const titles = Array.from(document.querySelectorAll('[data-section-title]'));
      const anchor = anchorY ?? window.innerHeight * 0.35;

      // Pass 1: prefer titles inside (or near) the viewport.
      let best = null;
      let bestDist = Infinity;
      titles.forEach(t => {
        const r = t.getBoundingClientRect();
        if (r.bottom < -300 || r.top > window.innerHeight + 200) return;
        const center = r.top + r.height / 2;
        const dist = Math.abs(center - anchor);
        if (dist < bestDist) { bestDist = dist; best = t; }
      });

      if (best) {
        const r = best.getBoundingClientRect();
        const perchX = clamp(
          r.right + PERCH_OFFSET_X,
          EDGE_PAD,
          window.innerWidth - RAVEN_W - EDGE_PAD
        );
        const perchY = clamp(r.top + PERCH_OFFSET_Y, 56, window.innerHeight - RAVEN_W);
        return { el: best, x: perchX, y: perchY };
      }

      // Pass 2: no title in viewport — perch at a virtual "hover" point.
      // This keeps the bird from flapping forever when the user has stopped
      // between distant section titles (e.g. mid-bio).
      const virtualX = clamp(
        window.innerWidth - RAVEN_W - EDGE_PAD * 2,
        EDGE_PAD,
        window.innerWidth - RAVEN_W - EDGE_PAD
      );
      const virtualY = clamp(anchor - RAVEN_W * 0.4, 80, window.innerHeight - RAVEN_W);
      return { el: null, x: virtualX, y: virtualY };
    };

    // Compute a flight target while scrolling — keeps the raven roughly at
    // the same vertical position in the viewport so it reads as "flying
    // alongside" the content rather than chasing a far-away element.
    const computeFlightTarget = () => {
      const anchorY = clamp(window.innerHeight * 0.32, 120, window.innerHeight - 200);
      // Sit toward the right edge of the viewport while flying
      const flyX = window.innerWidth - RAVEN_W - EDGE_PAD * 2;
      // Lean into wind: scroll down (positive dy) → raven a touch left
      const dy = window.scrollY - lastScrollY;
      const lean = clamp(dy * 1.2, -80, 80);
      return { x: flyX - lean, y: anchorY };
    };

    // Add organic drift (two-frequency sine perpendicular to motion).
    // The drift uses asymmetric Y amplitude (more vertical wave than
    // horizontal) on purpose: large horizontal drift would cause the bird
    // to flip direction back and forth on her own. Keep X drift gentle.
    const addDrift = (target, tMs) => {
      const tSec = tMs / 1000;
      const dx = Math.sin(tSec * 0.7) * (DRIFT_AMP_X * 0.5) + Math.sin(tSec * 1.9 + 1.3) * (DRIFT_AMP_X * 0.2);
      const dy = Math.cos(tSec * 0.9 + 0.6) * DRIFT_AMP_Y + Math.sin(tSec * 2.3) * (DRIFT_AMP_Y * 0.5);
      return { x: target.x + dx, y: target.y + dy };
    };

    // ---- Main loop -------------------------------------------------------
    const tick = (now) => {
      const tMs = now - t0;
      const idleFor = now - lastScrollAt;

      // ----- Decide position target -----
      // While user is scrolling: chase a "flight anchor" (right-ish of viewport,
      // ~30% down). After idle, lock onto nearest section title.
      let perch = null;
      let wantPerch = false;
      if (idleFor > IDLE_MS) {
        perch = findNearestTitle();
        if (perch) {
          tx = perch.x;
          ty = perch.y;
          landedTarget = perch.el;
          wantPerch = true;
        }
      }
      if (!wantPerch) {
        const flight = computeFlightTarget();
        const drifted = addDrift(flight, tMs);
        tx = drifted.x;
        ty = drifted.y;
        landedTarget = null;
      }

      // ----- Integrate position with smoothing -----
      x += (tx - x) * POS_SMOOTH;
      y += (ty - y) * POS_SMOOTH;
      vx = x - lastX;
      vy = y - lastY;
      lastX = x;
      lastY = y;

      const speed = Math.hypot(vx, vy);
      const distToTarget = Math.hypot(tx - x, ty - y);

      // ----- Direction (facing) -----
      // The bird should flip orientation ONLY when it's actually traveling in
      // a sustained direction — not when drift sine waves wiggle vx around 0,
      // and not when she's perching or perched. Three guards:
      //   1. Smooth vx with a slow low-pass so brief sign flips don't count
      //   2. Threshold + hysteresis: require >1.2 px/frame to flip; require
      //      >1.6 px/frame in the OPPOSITE direction to flip BACK
      //   3. Don't change facing at all when not actively cruising
      vxSmooth += (vx - vxSmooth) * 0.12;
      const isActivelyFlying = (state === STATE.CRUISING || state === STATE.TAKEOFF);
      if (isActivelyFlying) {
        const FLIP_THRESHOLD = 1.2;
        const FLIP_HYSTERESIS = 1.6;  // need this much to reverse current direction
        if (facing === 1 && vxSmooth < -FLIP_HYSTERESIS) {
          facing = -1;
        } else if (facing === -1 && vxSmooth > FLIP_HYSTERESIS) {
          facing = 1;
        } else if (Math.abs(vxSmooth) > FLIP_THRESHOLD && Math.sign(vxSmooth) !== facing) {
          // First-time or after a long pause — easier to commit to a direction
          facing = vxSmooth > 0 ? 1 : -1;
        }
      }
      // Smoothly blend the flip so direction changes don't snap (the actual
      // mirror is binary, but we ease the scaleX so the wing motion doesn't
      // cut mid-frame).
      flipBlend += (facing - flipBlend) * 0.16;
      const flipX = flipBlend < 0 ? -1 : 1;
      // Subtle x-axis compression at the moment of crossing zero — reads as
      // "turning toward camera" rather than instant flip.
      const flipScale = 0.25 + 0.75 * Math.abs(flipBlend);
      if (flip) flip.style.transform = `scaleX(${(flipX * flipScale).toFixed(3)})`;

      // ----- Visibility -----
      if (!visible) {
        visible = true;
        raven.classList.add('visible');
      }

      // ----- State machine -----
      // Goal: drive `state` based on (a) where we are vs perch target,
      // (b) how fast we're moving, (c) how long we've been still.
      //
      // Transitions:
      //   OFFSCREEN  → CRUISING                 (after entry)
      //   CRUISING   → BRAKING                  when wantPerch && dist < BRAKE_DIST
      //   BRAKING    → LANDING                  when dist < LAND_DIST * 2
      //   LANDING    → PERCHED                  when landing animation finishes
      //   PERCHED    → HEADTURN                 after a random idle interval
      //   HEADTURN   → PERCHED                  when head turn finishes
      //   PERCHED    → TAKEOFF                  when user scrolls (wantPerch false)
      //   TAKEOFF    → CRUISING                 when takeoff animation finishes

      const inFlight = (state === STATE.CRUISING || state === STATE.TAKEOFF ||
                        state === STATE.BRAKING || state === STATE.LANDING);

      switch (state) {
        case STATE.OFFSCREEN: {
          // Once we have a real position target, start cruising.
          if (visible && distToTarget < window.innerWidth * 0.7) {
            setState(STATE.CRUISING, now);
          }
          break;
        }

        case STATE.CRUISING: {
          // Cycle the flap loop
          const slow = speed < GLIDE_SPEED;
          const interval = slow ? GLIDE_FRAME_MS : FLAP_FRAME_MS;
          advanceFrame(now, FRAMES.flap, interval, true);
          paintFrame(FRAMES.flap[frameIdx]);
          // Approaching a perch target → BRAKING
          if (wantPerch && distToTarget < BRAKE_DIST) {
            setState(STATE.BRAKING, now);
          }
          break;
        }

        case STATE.BRAKING: {
          // Use brake-0 and brake-1 (wings up cupped, then forward braking).
          // We just want to advance to and hold brake-1 while approaching.
          const brakingFrames = [FRAMES.brake[0], FRAMES.brake[1]];
          advanceFrame(now, brakingFrames, BRAKE_FRAME_MS, false);
          paintFrame(brakingFrames[frameIdx]);
          // No longer wants to perch (user resumed scroll) → back to cruising
          if (!wantPerch) {
            setState(STATE.CRUISING, now);
            break;
          }
          // Close enough → LANDING
          if (distToTarget < LAND_DIST * 2) {
            setState(STATE.LANDING, now);
          }
          break;
        }

        case STATE.LANDING: {
          // brake-2 (wings folding) → brake-3 (perched). Plays once.
          const landingFrames = [FRAMES.brake[2], FRAMES.brake[3]];
          const r = advanceFrame(now, landingFrames, BRAKE_FRAME_MS + 20, false);
          paintFrame(landingFrames[frameIdx]);
          if (r === 'finished') {
            setState(STATE.PERCHED, now);
            // schedule first headturn
            nextHeadturnAt = now + IDLE_HEADTURN_MIN +
                              Math.random() * (IDLE_HEADTURN_MAX - IDLE_HEADTURN_MIN);
          }
          // User resumed scrolling mid-landing → bail to takeoff
          if (!wantPerch) {
            setState(STATE.TAKEOFF, now);
          }
          break;
        }

        case STATE.PERCHED: {
          paintFrame(FRAMES.perched[0]);
          // User scrolls → take off
          if (!wantPerch) {
            setState(STATE.TAKEOFF, now);
            break;
          }
          // Time for a head turn?
          if (now >= nextHeadturnAt) {
            setState(STATE.HEADTURN, now);
          }
          break;
        }

        case STATE.HEADTURN: {
          // Play headturn forward then back: front → 3/4 → profile →
          // over-shoulder → profile → 3/4 → front. Built as an indirect
          // sequence over FRAMES.headturn so we can reuse advanceFrame.
          // pingpong indices: 0,1,2,3,2,1,0  (length 7)
          const seq = [0, 1, 2, 3, 2, 1, 0];
          // Build a "virtual" frame set by indexing through seq; we treat
          // it as a non-looping sequence of length 7.
          const r = advanceFrame(now, seq, HEADTURN_FRAME_MS, false);
          paintFrame(FRAMES.headturn[seq[frameIdx]]);
          if (r === 'finished') {
            setState(STATE.PERCHED, now);
            nextHeadturnAt = now + IDLE_HEADTURN_MIN +
                              Math.random() * (IDLE_HEADTURN_MAX - IDLE_HEADTURN_MIN);
            break;
          }
          // User scrolls → cancel headturn, take off
          if (!wantPerch) {
            setState(STATE.TAKEOFF, now);
          }
          break;
        }

        case STATE.TAKEOFF: {
          // Plays once. When advanceFrame returns 'finished', we've held
          // the final takeoff frame long enough → transition to CRUISING.
          const r = advanceFrame(now, FRAMES.takeoff, TAKEOFF_FRAME_MS, false);
          paintFrame(FRAMES.takeoff[frameIdx]);
          if (r === 'finished') {
            setState(STATE.CRUISING, now);
          }
          break;
        }
      }

      // ----- Tilt (banking) -----
      // No tilt while perched/heading. Bank while cruising/braking proportional
      // to velocity. Easing always smooths.
      const tiltTarget = inFlight
        ? clamp(vx * TILT_PER_VX + vy * TILT_PER_VY, -MAX_TILT_DEG, MAX_TILT_DEG)
        : 0;
      tilt_cur += (tiltTarget - tilt_cur) * TILT_SMOOTH;

      // ----- Bob (body wave) — only during flight -----
      const bobAmp = inFlight ? BOB_AMP : 0;
      const bobY = Math.sin(tMs / 240) * bobAmp;

      // ----- Apply transforms -----
      raven.style.transform = `translate3d(${x.toFixed(2)}px, ${y.toFixed(2)}px, 0)`;
      if (tilt) tilt.style.transform = `rotate(${tilt_cur.toFixed(2)}deg)`;
      if (bob) bob.style.setProperty('--bob', `${bobY.toFixed(2)}px`);

      rafId = requestAnimationFrame(tick);
    };

    // ---- Scroll listener: just records "scroll happened" + delta ---------
    const onScroll = () => {
      lastScrollAt = performance.now();
      // record scroll delta for "lean into wind" effect
      // (lastScrollY is read inside computeFlightTarget; update AFTER use)
      // We update it on next animation frame so the velocity has a frame to read
      requestAnimationFrame(() => { lastScrollY = window.scrollY; });
    };

    const onResize = () => {
      // re-clamp target on resize
      lastScrollAt = performance.now();
    };

    // Seed: start raven somewhere offscreen-right so its entrance is a fly-in
    x = window.innerWidth + 60;
    y = window.innerHeight * 0.20;
    lastX = x; lastY = y;
    raven.style.transform = `translate3d(${x}px, ${y}px, 0)`;

    // Begin
    lastScrollAt = performance.now() - IDLE_MS - 100; // start by seeking a perch
    window.addEventListener('scroll', onScroll, { passive: true });
    window.addEventListener('resize', onResize);
    rafId = requestAnimationFrame(tick);

    return () => {
      cancelAnimationFrame(rafId);
      window.removeEventListener('scroll', onScroll);
      window.removeEventListener('resize', onResize);
    };
  }, []);

  return (
    <>
      <NavV5 theme={theme} toggleTheme={toggleTheme} />
      <HeroV5 />
      <InvitationV5 />
      <SigilDividerV5 />
      <PillarsV5 />
      <div id="draw"></div>
      <section style={{ paddingTop: 80, paddingBottom: 80 }}>
        <CardDrawV4 />
      </section>
      <SigilDividerV5 />
      <StoryV5 />
      <WhyTheRavenV5 />
      <BridgeV5 />
      <GatheringsV5 />
      <ContactV5 />
      <FooterV5 />
    </>
  );
}

const rootEl = document.getElementById('root');
const root = ReactDOM.createRoot(rootEl);
root.render(<AppV5 />);
