// Direction 3 — "Cinematic Reel" (TWEAKABLE EDITION)
// All visual options driven by a context object so the Tweaks panel can drive everything.

(function () {
  const { useState, useEffect, useRef, useMemo, useContext, createContext } = React;
  // Preview mode (admin opens ?preview=1 to view drafts as they'll look live).
  const A2M_PREVIEW = (() => { try { return new URLSearchParams(window.location.search).get('preview') === '1'; } catch (e) { return false; } })();
  // `let` (not const) so the host page can swap in Firestore data before the
  // first render via window.A2M_applyData(). Components read these at render
  // time, so reassigning before ReactDOM.render reflects the live data.
  let CASES = window.A2M_CASES;
  let FILTERS = window.A2M_FILTERS;
  const CaseArt = window.CaseArt;

  // Called by index.html after fetching cases from Firestore (falls back to the
  // bundled lib/*.js data if the fetch fails or returns nothing).
  window.A2M_applyData = function (cases) {
    if (!Array.isArray(cases) || !cases.length) return;
    CASES = cases;
    FILTERS = {
      sectors: [...new Set(cases.map(c => c.sectorTag).filter(Boolean))],
      channels: [...new Set(cases.flatMap(c => c.channels || []))],
      formats: [...new Set(cases.flatMap(c => c.formats || []))],
    };
  };

  const D3Ctx = createContext({});
  const useD3 = () => useContext(D3Ctx);

  const BG_PRESETS = {
    black:    { bg: '#000000', fg: '#F4F1EB', soft: '#F4F1EB18', dim: '#F4F1EB' },
    dark:     { bg: '#0A0A0A', fg: '#F4F1EB', soft: '#F4F1EB18', dim: '#F4F1EB' },
    charcoal: { bg: '#1B1B1A', fg: '#F4F1EB', soft: '#F4F1EB20', dim: '#F4F1EB' },
    midnight: { bg: '#0B1424', fg: '#E8ECF7', soft: '#E8ECF722', dim: '#E8ECF7' },
    cream:    { bg: '#F4F1EB', fg: '#0A0A0A', soft: '#0A0A0A18', dim: '#0A0A0A' },
  };

  // ── Cursor with trail + follower ────────────────────────
  function Cursor({ scrollRef }) {
    const { accent, palette } = useD3();
    const dotRef = useRef(null);
    const ringRef = useRef(null);
    const pos = useRef({ x: 0, y: 0 });
    const ring = useRef({ x: 0, y: 0 });
    const [label, setLabel] = useState(null);

    useEffect(() => {
      const el = scrollRef.current; if (!el) return;
      const onMove = (e) => {
        pos.current = { x: e.clientX, y: e.clientY };
        if (dotRef.current) dotRef.current.style.transform = `translate(${e.clientX}px, ${e.clientY}px)`;
        const target = e.target.closest('[data-d3-cursor]');
        setLabel(target ? target.getAttribute('data-d3-cursor') : null);
      };
      el.addEventListener('mousemove', onMove);
      let raf;
      const tick = () => {
        ring.current.x += (pos.current.x - ring.current.x) * 0.12;
        ring.current.y += (pos.current.y - ring.current.y) * 0.12;
        if (ringRef.current) ringRef.current.style.transform = `translate(${ring.current.x}px, ${ring.current.y}px)`;
        raf = requestAnimationFrame(tick);
      };
      raf = requestAnimationFrame(tick);
      return () => { el.removeEventListener('mousemove', onMove); cancelAnimationFrame(raf); };
    }, []);

    return (
      <>
        <div ref={ringRef} style={{ position: 'fixed', left: 0, top: 0, zIndex: 99, pointerEvents: 'none' }}>
          <div style={{
            position: 'absolute', left: -40, top: -40,
            width: label ? 90 : 44, height: label ? 90 : 44, borderRadius: 999,
            border: `1px solid ${accent}`, background: label ? accent : 'transparent',
            color: '#0A0A0A', display: 'flex', alignItems: 'center', justifyContent: 'center',
            fontSize: 11, fontWeight: 700, letterSpacing: '0.14em', textTransform: 'uppercase',
            transition: 'width .35s cubic-bezier(.2,.8,.2,1), height .35s cubic-bezier(.2,.8,.2,1), background .25s',
          }}>{label}</div>
        </div>
        <div ref={dotRef} style={{ position: 'fixed', left: 0, top: 0, zIndex: 100, pointerEvents: 'none' }}>
          <div style={{ position: 'absolute', left: -3, top: -3, width: 6, height: 6, borderRadius: 999, background: palette.fg, mixBlendMode: 'difference' }} />
        </div>
      </>
    );
  }

  function AmbientOrb() {
    const { accent, glow2, showOrbs, orbIntensity } = useD3();
    if (!showOrbs) return null;
    const alpha = Math.round(orbIntensity * 0.55 * 255).toString(16).padStart(2, '0');
    const alpha2 = Math.round(orbIntensity * 0.45 * 255).toString(16).padStart(2, '0');
    return (
      <div style={{ position: 'absolute', inset: 0, pointerEvents: 'none', overflow: 'hidden', zIndex: 0 }}>
        <div style={{
          position: 'absolute', top: '20%', left: '70%', width: '45vw', height: '45vw',
          borderRadius: '50%', background: `radial-gradient(circle, ${accent}${alpha}, transparent 65%)`,
          filter: 'blur(60px)', animation: 'd3-orb 14s ease-in-out infinite alternate',
        }} />
        <div style={{
          position: 'absolute', top: '55%', left: '10%', width: '35vw', height: '35vw',
          borderRadius: '50%', background: `radial-gradient(circle, ${glow2}${alpha2}, transparent 65%)`,
          filter: 'blur(70px)', animation: 'd3-orb2 18s ease-in-out infinite alternate',
        }} />
      </div>
    );
  }

  function KineticText({ text, size, weight = 500, delay = 0 }) {
    const { showKinetic } = useD3();
    const ref = useRef(null);
    const [on, setOn] = useState(!showKinetic);
    useEffect(() => {
      if (!showKinetic) { setOn(true); return; }
      const io = new IntersectionObserver(([e]) => e.isIntersecting && setOn(true), { threshold: 0.2 });
      if (ref.current) io.observe(ref.current);
      return () => io.disconnect();
    }, [showKinetic, text]);
    return (
      <div ref={ref} style={{ display: 'block', lineHeight: 1.0, fontWeight: weight, fontSize: size, letterSpacing: '-0.04em', fontFamily: 'var(--d3-sans)' }}>
        {String(text).split(' ').map((word, wi) => (
          <span key={wi} style={{ display: 'inline-block', marginRight: '0.28em', overflow: 'hidden', verticalAlign: 'top', paddingBottom: '0.18em' }}>
            <span style={{
              display: 'inline-block',
              transform: on ? 'translateY(0) rotate(0)' : 'translateY(110%) rotate(4deg)',
              transition: showKinetic ? `transform .9s cubic-bezier(.2,.8,.2,1) ${delay + wi * 70}ms` : 'none',
            }}>{word}</span>
          </span>
        ))}
      </div>
    );
  }

  function Magnetic({ children, strength = 0.3, style = {}, ...rest }) {
    const { showMagnetic } = useD3();
    const ref = useRef(null);
    const [pos, setPos] = useState({ x: 0, y: 0 });
    if (!showMagnetic) return <div style={{ display: 'inline-block', ...style }} {...rest}>{children}</div>;
    return (
      <div
        ref={ref}
        onMouseMove={(e) => {
          const r = ref.current.getBoundingClientRect();
          setPos({ x: (e.clientX - r.left - r.width / 2) * strength, y: (e.clientY - r.top - r.height / 2) * strength });
        }}
        onMouseLeave={() => setPos({ x: 0, y: 0 })}
        style={{ display: 'inline-block', transform: `translate(${pos.x}px, ${pos.y}px)`, transition: 'transform .35s cubic-bezier(.2,.8,.2,1)', ...style }}
        {...rest}
      >{children}</div>
    );
  }

  function resolveVariant(i, artStyle) {
    if (artStyle === 'auto') return ['split','gradient','big-metric','stripes','block'][i % 5];
    return artStyle;
  }

  // ── Intro reel panels (editorial, non-tiled) ────────────
  function IntroFounder() {
    const { accent, palette, glow2, isMobile } = useD3();
    return (
      <div style={{ flex: isMobile ? '0 0 auto' : '0 0 96vw', width: isMobile ? '100%' : undefined, height: isMobile ? 'auto' : '100%', padding: isMobile ? 'clamp(48px, 8vh, 72px) 7vw' : 'clamp(84px, 11vh, 120px) 5vw clamp(70px, 9vh, 100px)', display: 'grid', gridTemplateColumns: isMobile ? '1fr' : 'minmax(0, 1.25fr) minmax(0, 1fr)', alignItems: isMobile ? 'start' : 'center', gap: isMobile ? 'clamp(28px, 5vh, 44px)' : 'clamp(40px, 5vw, 96px)', position: 'relative', boxSizing: 'border-box', overflow: isMobile ? 'visible' : 'hidden' }}>
        <div style={{ position: 'relative', zIndex: 2 }}>
          <div style={{ fontSize: 11, fontWeight: 600, letterSpacing: '0.22em', textTransform: 'uppercase', color: accent, marginBottom: 18, display: 'flex', alignItems: 'center', gap: 10 }}>
            <span style={{ width: 22, height: 1, background: accent }} />
            01 · Message from our CEO
          </div>
          <div style={{ fontFamily: 'var(--d3-sans)', fontWeight: 500, fontSize: 'clamp(26px, 2.9vw, 44px)', letterSpacing: '-0.02em', lineHeight: 1.05, marginBottom: 14 }}>
            A note from <span style={{ color: accent }}>Tony Laskar</span>.
          </div>
          <div style={{ width: 72, height: 2, background: accent, marginBottom: 26 }} />
          <div style={{ display: 'flex', flexDirection: 'column', gap: 'clamp(12px, 1.5vh, 18px)', fontSize: 'clamp(14px, 1vw, 17px)', lineHeight: 1.7, fontWeight: 300, opacity: 0.85, maxWidth: 600 }}>
            <p style={{ margin: 0 }}><span style={{ color: accent, fontWeight: 500 }}>“</span>Thank you for taking the time to learn more about Audience2Media. At our core is pioneering data-driven technology, award-winning expertise and an audience targeting platform that's unrivalled on performance — seamlessly combining programmatic and influencer marketing to connect brands with the right people at local and global scale.</p>
            <p style={{ margin: 0 }}>What truly sets us apart is our team. We're dedicated to delivering outstanding results and giving our clients the competitive edge they deserve — and I'm excited to keep innovating alongside you into the next decade.<span style={{ color: accent, fontWeight: 500 }}>”</span></p>
          </div>
          <div style={{ marginTop: 28, display: 'flex', alignItems: 'center', gap: 14 }}>
            <div style={{ width: 44, height: 44, borderRadius: 999, background: `linear-gradient(135deg, ${accent}, ${glow2})`, opacity: 0.85, flexShrink: 0 }} />
            <div>
              <div style={{ fontFamily: 'var(--d3-sans)', fontWeight: 500, fontSize: 16, letterSpacing: '-0.01em' }}>Tony Laskar</div>
              <div style={{ fontSize: 10, opacity: 0.55, letterSpacing: '0.15em', textTransform: 'uppercase', marginTop: 4 }}>Founder & Chief Executive Officer</div>
            </div>
          </div>
        </div>
        <div style={{ position: 'relative' }}>
          <KineticText text={'The audience targeting company.'} size={isMobile ? 'clamp(34px, 9vw, 52px)' : 'clamp(38px, 4.6vw, 82px)'} weight={500} />
        </div>
      </div>
    );
  }

  function IntroWhoWeAre() {
    const { accent, palette } = useD3();
    const lines = [
      { a: 'We are', b: 'Audience2Media' },
      { a: 'A team of', b: '128 specialists' },
      { a: 'Working across', b: '12 markets' },
      { a: 'Since', b: '2009' },
    ];
    return (
      <div style={{ flex: '0 0 70vw', padding: '0 5vw', display: 'flex', flexDirection: 'column', justifyContent: 'center', position: 'relative' }}>
        <div style={{ fontSize: 11, fontWeight: 600, letterSpacing: '0.22em', textTransform: 'uppercase', color: accent, marginBottom: 50, display: 'flex', alignItems: 'center', gap: 10 }}>
          <span style={{ width: 22, height: 1, background: accent }} />
          02 · Who we are
        </div>
        <div style={{ display: 'flex', flexDirection: 'column', gap: 'clamp(8px, 1vh, 16px)' }}>
          {lines.map((l, i) => (
            <div key={i} style={{ display: 'grid', gridTemplateColumns: '0.7fr 2fr', alignItems: 'baseline', gap: 'clamp(20px, 3vw, 48px)', borderBottom: `1px solid ${palette.soft}`, paddingBottom: 'clamp(8px, 1.2vh, 18px)' }}>
              <div style={{ fontSize: 13, letterSpacing: '0.12em', textTransform: 'uppercase', opacity: 0.55, fontWeight: 500 }}>{l.a}</div>
              <div style={{ fontFamily: 'var(--d3-sans)', fontWeight: 500, fontSize: 'clamp(40px, 5.4vw, 92px)', lineHeight: 1, letterSpacing: '-0.03em', color: i === 0 ? accent : 'inherit' }}>{l.b}</div>
            </div>
          ))}
        </div>
        <div style={{ marginTop: 40, fontSize: 13, opacity: 0.55, maxWidth: 460, lineHeight: 1.55, fontWeight: 300 }}>
          Independent. Audience-first. Built around a single instinct — find the people who matter, meet them at the moment that matters.
        </div>
      </div>
    );
  }

  function IntroFramework() {
    const { accent, palette, isMobile } = useD3();
    const pillars = [
      { n: 'I', t: 'Insights', sub: 'We understand your audience.', d: 'Decoding the real signals and behaviours that shape how people engage with brands.' },
      { n: 'II', t: 'Strategy', sub: 'We create demand.', d: 'Turning attention into measurable growth. Every message backed by data and aligned to the outcome that matters.' },
      { n: 'III', t: 'Activation', sub: 'We connect to the right people.', items: [
        { l: 'Online', d: 'Interest-based programmatic targeting across social, digital OOH and the open web.' },
        { l: 'Offline', d: 'High-impact OOH, print, radio and TV at scale.' },
        { l: 'Influencer', d: 'Data-driven creator story-telling.' },
      ]},
    ];
    return (
      <div style={{ flex: isMobile ? '0 0 auto' : '0 0 92vw', width: isMobile ? '100%' : undefined, boxSizing: 'border-box', height: isMobile ? 'auto' : '100%', padding: isMobile ? 'clamp(40px, 7vh, 64px) 7vw' : 'clamp(88px, 12vh, 124px) 5vw clamp(74px, 10vh, 104px)', display: 'flex', flexDirection: 'column', justifyContent: 'center', position: 'relative', overflow: isMobile ? 'visible' : 'hidden' }}>
        <div style={{ display: 'flex', justifyContent: 'space-between', alignItems: 'flex-end', marginBottom: 'clamp(20px, 3.5vh, 44px)', gap: 24, flexWrap: 'wrap' }}>
          <div>
            <div style={{ fontSize: 11, fontWeight: 600, letterSpacing: '0.22em', textTransform: 'uppercase', color: accent, marginBottom: 16, display: 'flex', alignItems: 'center', gap: 10 }}>
              <span style={{ width: 22, height: 1, background: accent }} />
              02 · The framework
            </div>
            <KineticText text="Our framework that delivers." size={isMobile ? 'clamp(30px, 8vw, 46px)' : 'clamp(30px, 4vw, 64px)'} weight={500} />
          </div>
        </div>
        <div style={{ display: 'grid', gridTemplateColumns: isMobile ? '1fr' : 'repeat(3, 1fr)', gap: 0, borderTop: `1px solid ${palette.soft}` }}>
          {pillars.map((p, i) => (
            <div key={i} style={{ padding: isMobile ? 'clamp(22px, 3vh, 30px) 0' : 'clamp(18px, 2.6vh, 32px) 28px clamp(14px, 2vh, 28px) 0', borderRight: !isMobile && i < pillars.length - 1 ? `1px solid ${palette.soft}` : 'none', borderBottom: isMobile && i < pillars.length - 1 ? `1px solid ${palette.soft}` : 'none', display: 'flex', flexDirection: 'column', justifyContent: 'flex-start', gap: isMobile ? 'clamp(10px, 2vh, 18px)' : 'clamp(16px, 2.4vh, 32px)' }}>
              <div style={{ fontFamily: 'var(--d3-sans)', fontWeight: 500, fontSize: 13, letterSpacing: '0.18em', color: accent, marginLeft: isMobile ? 0 : 28 }}>{p.n}</div>
              <div style={{ marginLeft: isMobile ? 0 : 28 }}>
                <div style={{ fontFamily: 'var(--d3-sans)', fontWeight: 500, fontSize: 'clamp(24px, 2.6vw, 42px)', letterSpacing: '-0.02em', marginBottom: 8 }}>{p.t}</div>
                <div style={{ fontSize: 13, lineHeight: 1.4, marginBottom: 14, color: accent, fontWeight: 600, letterSpacing: '0.01em' }}>{p.sub}</div>
                {p.d && (
                  <div style={{ fontSize: 13, opacity: 0.6, lineHeight: 1.5, maxWidth: '28ch', fontWeight: 300 }}>{p.d}</div>
                )}
                {p.items && (
                  <div style={{ marginTop: 4, display: 'flex', flexDirection: 'column', gap: 'clamp(8px, 1.4vh, 14px)' }}>
                    {p.items.map((it, j) => (
                      <div key={j} style={{ display: 'grid', gridTemplateColumns: '0.5fr 1fr', alignItems: 'baseline', gap: 16, paddingTop: 'clamp(8px, 1.2vh, 12px)', borderTop: `1px solid ${palette.soft}` }}>
                        <div style={{ fontSize: 10, fontWeight: 700, letterSpacing: '0.16em', textTransform: 'uppercase', color: palette.fg }}>{it.l}</div>
                        <div style={{ fontSize: 12, opacity: 0.6, lineHeight: 1.45, fontWeight: 300 }}>{it.d}</div>
                      </div>
                    ))}
                  </div>
                )}
              </div>
            </div>
          ))}
        </div>
      </div>
    );
  }

  function IntroBrands() {
    const { accent, palette, isMobile } = useD3();
    // Brand logos come from Firestore (window.A2M_BRANDS). If none are loaded,
    // fall back to the original scrolling text marquee.
    const brands = (window.A2M_BRANDS || []).filter(b => b && b.logoUrl);
    const fallbackRows = [
      { dir: 'left', weight: 400, items: ['Ted Baker', 'Vodafone', 'Aston Martin', 'Google', 'Alibaba', 'Warner Bros', 'TUI', 'Macmillan', 'Sony Music', 'Canon'] },
      { dir: 'right', weight: 700, items: ['Tesco', 'Barclays', 'Dyson', 'Nestlé', 'HSBC', 'Toyota', 'Disney+', 'O₂', 'Burger King', 'British Airways'] },
      { dir: 'left', weight: 400, items: ['NordVPN', 'L’Oréal', 'Serpentine', 'Wallace Collection', 'Brooklands Museum', 'AEON Bank', '4FINGERS', 'MUBI', 'Yves Saint Laurent', 'Nando’s'] },
    ];

    // Split the logos across up to 3 marquee rows for a fuller wall.
    const rowCount = brands.length >= 12 ? 3 : (brands.length >= 6 ? 2 : 1);
    const logoRows = Array.from({ length: rowCount }, (_, r) => brands.filter((_, i) => i % rowCount === r));

    return (
      <div style={{ flex: isMobile ? '0 0 auto' : '0 0 92vw', minWidth: 0, maxWidth: isMobile ? '100%' : '92vw', width: isMobile ? '100%' : undefined, height: isMobile ? 'auto' : '100%', boxSizing: 'border-box', padding: isMobile ? 'clamp(44px, 7vh, 68px) 7vw clamp(70px, 10vh, 96px)' : 'clamp(80px, 10vh, 116px) 5vw clamp(100px, 13vh, 140px)', display: 'flex', flexDirection: 'column', justifyContent: 'center', position: 'relative', overflow: 'hidden' }}>
        <div style={{ marginBottom: 40, maxWidth: 720 }}>
          <div style={{ fontSize: 11, fontWeight: 600, letterSpacing: '0.22em', textTransform: 'uppercase', color: accent, marginBottom: 22, display: 'flex', alignItems: 'center', gap: 10 }}>
            <span style={{ width: 22, height: 1, background: accent }} />
            03 · Trusted by global brands
          </div>
          <KineticText text="Trusted by global brands." size={isMobile ? 'clamp(34px, 9vw, 52px)' : 'clamp(40px, 5.2vw, 88px)'} weight={500} />
          <div style={{ marginTop: 22, fontSize: 14, opacity: 0.65, lineHeight: 1.55, maxWidth: 540, fontWeight: 300 }}>
            Capturing the perfect audience for your brand — delivered with speed and performance.
          </div>
        </div>

        {brands.length > 0 ? (
          <div style={{ borderTop: `1px solid ${palette.soft}`, borderBottom: `1px solid ${palette.soft}`, width: '100%', overflow: 'hidden' }}>
            {logoRows.map((row, ri) => (
              <div key={ri} style={{ overflow: 'hidden', width: '100%', borderBottom: ri < logoRows.length - 1 ? `1px solid ${palette.soft}` : 'none', padding: 'clamp(8px, 1.4vh, 15px) 0' }}>
                <div style={{ display: 'inline-flex', whiteSpace: 'nowrap', animation: `d3-marquee-${ri % 2 === 0 ? 'left' : 'right'} ${26 + ri * 6}s linear infinite` }}>
                  {[...row, ...row, ...row].map((b, i) => (
                    <span key={i} style={{ display: 'inline-flex', alignItems: 'center', paddingRight: 'clamp(28px, 4vw, 64px)' }}>
                      <span style={{ height: 'clamp(34px, 4.4vh, 54px)', display: 'inline-flex', alignItems: 'center', justifyContent: 'center' }}>
                        <img src={b.logoUrl} alt={b.name || ''} style={{ maxHeight: '100%', maxWidth: 'clamp(96px, 10vw, 168px)', objectFit: 'contain', display: 'block', filter: 'brightness(0) invert(1)', opacity: 0.85 }} loading="lazy" />
                      </span>
                    </span>
                  ))}
                </div>
              </div>
            ))}
          </div>
        ) : (
          <div style={{ borderTop: `1px solid ${palette.soft}`, borderBottom: `1px solid ${palette.soft}`, width: '100%', overflow: 'hidden' }}>
            {fallbackRows.map((row, ri) => (
              <div key={ri} style={{ overflow: 'hidden', width: '100%', borderBottom: ri < fallbackRows.length - 1 ? `1px solid ${palette.soft}` : 'none', padding: 'clamp(10px, 1.8vh, 18px) 0' }}>
                <div style={{ display: 'inline-flex', whiteSpace: 'nowrap', animation: `d3-marquee-${row.dir} ${22 + ri * 6}s linear infinite` }}>
                  {[...row.items, ...row.items, ...row.items].map((b, i) => (
                    <span key={i} style={{ display: 'inline-flex', alignItems: 'center', gap: 'clamp(14px, 2.4vw, 30px)', paddingRight: 'clamp(22px, 3vw, 40px)', fontFamily: 'var(--d3-sans)', fontWeight: row.weight, fontSize: 'clamp(18px, 2.4vw, 34px)', letterSpacing: '-0.02em', opacity: i % 5 === 0 ? 1 : 0.55 }}>
                      <span style={{ display: 'inline-block', width: 4, height: 4, borderRadius: 999, background: accent, flexShrink: 0 }} />
                      {b}
                    </span>
                  ))}
                </div>
              </div>
            ))}
          </div>
        )}
      </div>
    );
  }

  function IntroOffering() {
    const { accent, palette, glow2 } = useD3();
    return (
      <div style={{ flex: '0 0 90vw', padding: '0 5vw', display: 'grid', gridTemplateColumns: '1.1fr 1fr', alignItems: 'center', gap: '5vw', position: 'relative' }}>
        <div style={{ position: 'relative', zIndex: 2 }}>
          <div style={{ fontSize: 11, fontWeight: 600, letterSpacing: '0.22em', textTransform: 'uppercase', color: accent, marginBottom: 28, display: 'flex', alignItems: 'center', gap: 10 }}>
            <span style={{ width: 22, height: 1, background: accent }} />
            04 · Our unique market offering
          </div>
          <KineticText text="A 360° performance ecosystem." size="clamp(40px, 5.6vw, 96px)" weight={500} />
          <div style={{ marginTop: 32, fontSize: 15, opacity: 0.72, lineHeight: 1.6, maxWidth: 560, fontWeight: 300 }}>
            Fuelled by real-time insights, engineered through intelligent strategy, executed with precision activation. A2M integrates proprietary data, AI and human expertise to unify every media channel — influencer marketing included — into one transparent system at global scale.
          </div>
          <div style={{ marginTop: 36, display: 'flex', gap: 'clamp(18px, 3vw, 36px)', flexWrap: 'wrap' }}>
            {['Smarter decisions', 'Faster optimisation', 'Measurable ROI'].map((t, i) => (
              <div key={i} style={{ display: 'flex', alignItems: 'center', gap: 10, fontSize: 12, letterSpacing: '0.14em', textTransform: 'uppercase', fontWeight: 600 }}>
                <span style={{ display: 'inline-block', width: 6, height: 6, borderRadius: 999, background: accent }} />
                {t}
              </div>
            ))}
          </div>
        </div>
        <div style={{ position: 'relative', height: 'clamp(280px, 52vh, 520px)' }}>
          <svg viewBox="0 0 420 340" style={{ width: '100%', height: '100%' }} preserveAspectRatio="xMidYMid meet">
            <defs>
              <linearGradient id="beamA" x1="0" y1="1" x2="0" y2="0">
                <stop offset="0%" stopColor={palette.bg} stopOpacity="1" />
                <stop offset="35%" stopColor={accent} stopOpacity="0.85" />
                <stop offset="100%" stopColor={accent} stopOpacity="0.05" />
              </linearGradient>
              <linearGradient id="beamB" x1="0" y1="1" x2="0" y2="0">
                <stop offset="0%" stopColor={palette.bg} stopOpacity="1" />
                <stop offset="35%" stopColor={glow2} stopOpacity="0.85" />
                <stop offset="100%" stopColor={glow2} stopOpacity="0.05" />
              </linearGradient>
            </defs>
            {Array.from({ length: 28 }).map((_, i) => {
              const x = 20 + (i / 27) * 380;
              const seed = Math.sin(i * 0.83) * 0.5 + 0.5;
              const h = 90 + seed * 180 + (i % 4) * 14;
              const isAccent = i % 2 === 0;
              return (
                <g key={i}>
                  <rect x={x - 0.6} y={335 - h} width={1.2} height={h} fill={`url(#${isAccent ? 'beamA' : 'beamB'})`} opacity={0.9} />
                  {Array.from({ length: Math.max(3, Math.round(h / 28)) }).map((_, j) => (
                    <circle key={j} cx={x} cy={335 - h + j * (h / 6)} r={1.4} fill={isAccent ? accent : glow2} opacity={Math.max(0.15, 1 - j * 0.16)} />
                  ))}
                </g>
              );
            })}
            <line x1="0" y1="335" x2="420" y2="335" stroke={palette.fg} strokeOpacity="0.2" strokeWidth="1" />
            <line x1="0" y1="338" x2="420" y2="338" stroke={palette.fg} strokeOpacity="0.06" strokeWidth="2" />
          </svg>
        </div>
      </div>
    );
  }

  function IntroNumbers() {
    const { accent, palette } = useD3();
    const stats = [
      { v: '63', l: 'Campaigns shipped', sub: 'in 2025' },
      { v: '12', l: 'Markets', sub: 'across 4 regions' },
      { v: '4.7B', l: 'Audience reached', sub: 'verified impressions' },
      { v: '38%', l: 'Avg lift', sub: 'on the brief KPI' },
    ];
    return (
      <div style={{ flex: '0 0 85vw', padding: '0 5vw', display: 'flex', flexDirection: 'column', justifyContent: 'center', position: 'relative' }}>
        <div style={{ fontSize: 11, fontWeight: 600, letterSpacing: '0.22em', textTransform: 'uppercase', color: accent, marginBottom: 40, display: 'flex', alignItems: 'center', gap: 10 }}>
          <span style={{ width: 22, height: 1, background: accent }} />
          05 · By the numbers
        </div>
        <div style={{ display: 'grid', gridTemplateColumns: 'repeat(4, 1fr)', gap: 'clamp(20px, 3vw, 56px)', alignItems: 'end' }}>
          {stats.map((s, i) => (
            <div key={i} style={{ display: 'flex', flexDirection: 'column', gap: 16, borderTop: `1px solid ${palette.soft}`, paddingTop: 28, position: 'relative' }}>
              <div style={{ position: 'absolute', top: -1, left: 0, width: 36, height: 2, background: accent }} />
              <div style={{ fontFamily: 'var(--d3-sans)', fontWeight: 500, fontSize: 'clamp(72px, 10vw, 180px)', lineHeight: 0.88, letterSpacing: '-0.04em' }}>{s.v}</div>
              <div>
                <div style={{ fontSize: 13, fontWeight: 600, letterSpacing: '0.05em' }}>{s.l}</div>
                <div style={{ fontSize: 11, opacity: 0.5, marginTop: 4, letterSpacing: '0.08em', textTransform: 'uppercase' }}>{s.sub}</div>
              </div>
            </div>
          ))}
        </div>
        <div style={{ marginTop: 60, display: 'flex', alignItems: 'center', gap: 16, fontSize: 12, letterSpacing: '0.14em', textTransform: 'uppercase', fontWeight: 600, opacity: 0.7 }}>
          <span style={{ display: 'inline-block', width: 30, height: 1, background: accent }} />
          Now — the work ↓
        </div>
      </div>
    );
  }

  function Reel({ onOpen }) {
    const { accent, palette, panelWidth, showParallax, autoDrift, artStyle, tileRadius, introHeadline, introKicker, introBody, skipIntro, isMobile } = useD3();
    const ref = useRef(null);
    const [progress, setProgress] = useState(0);
    const [hoverIdx, setHoverIdx] = useState(null);
    const [mouse, setMouse] = useState({ x: 0.5, y: 0.5 });
    // Mobile: the Skip-intro pill is position:fixed, so hide it once the user has
    // scrolled into the Case Studies grid (otherwise it floats over everything).
    const [showSkipMobile, setShowSkipMobile] = useState(true);
    useEffect(() => {
      if (!isMobile) return;
      const scroller = document.querySelector('.site-viewport > div');
      if (!scroller) return;
      const onScroll = () => {
        const grid = document.getElementById('d3-grid');
        const threshold = grid ? grid.offsetTop - window.innerHeight * 0.5 : window.innerHeight * 3;
        setShowSkipMobile(scroller.scrollTop < threshold);
      };
      onScroll();
      scroller.addEventListener('scroll', onScroll, { passive: true });
      return () => scroller.removeEventListener('scroll', onScroll);
    }, [isMobile]);

    useEffect(() => {
      const el = ref.current; if (!el) return;
      // On mobile the reel stacks vertically and scrolls with the page — skip all
      // the horizontal wheel/drag/snap hijacking.
      if (isMobile) return;
      let snapping = false;
      let scrollEndTimer = null;

      let wheelLocked = false;
      let wheelUnlockTimer = null;

      const getSnapPoints = () => Array.from(el.children)
        .filter(c => c.offsetWidth > window.innerWidth * 0.6)
        .map(p => p.offsetLeft);

      const glideTo = (target, duration = 600) => {
        snapping = true;
        const start = el.scrollLeft;
        const delta = target - start;
        const max = el.scrollWidth - el.clientWidth;
        if (Math.abs(delta) < 2) { snapping = false; return; }
        const t0 = performance.now();
        const step = (now) => {
          if (!snapping) return;
          const t = Math.min((now - t0) / duration, 1);
          const eased = 1 - Math.pow(1 - t, 3);
          el.scrollLeft = start + delta * eased;
          if (max > 0) setProgress(el.scrollLeft / max);
          if (t < 1) requestAnimationFrame(step);
          else snapping = false;
        };
        requestAnimationFrame(step);
      };

      const stepPanel = (dir) => {
        const pts = getSnapPoints();
        if (!pts.length) return;
        const cur = el.scrollLeft;
        let idx = 0, best = Infinity;
        pts.forEach((p, i) => { const d = Math.abs(p - cur); if (d < best) { best = d; idx = i; } });
        const target = Math.max(0, Math.min(pts.length - 1, idx + dir));
        glideTo(pts[target]);
      };

      // One wheel gesture advances exactly one panel.
      const onWheel = (e) => {
        if (Math.abs(e.deltaY) <= Math.abs(e.deltaX)) return;
        const max = el.scrollWidth - el.clientWidth;
        const dir = e.deltaY > 0 ? 1 : -1;
        const atStart = el.scrollLeft <= 2;
        const atEnd = el.scrollLeft >= max - 2;
        // At the ends, let the wheel bubble so the page can scroll past the reel.
        if ((atEnd && dir > 0) || (atStart && dir < 0)) return;
        e.preventDefault();
        if (wheelLocked || snapping) return;
        wheelLocked = true;
        stepPanel(dir);
        if (wheelUnlockTimer) clearTimeout(wheelUnlockTimer);
        wheelUnlockTimer = setTimeout(() => { wheelLocked = false; }, 720);
      };
      el.addEventListener('wheel', onWheel, { passive: false });

      // After a drag, settle to the nearest panel.
      const onScroll = () => {
        if (snapping || wheelLocked) return;
        if (scrollEndTimer) clearTimeout(scrollEndTimer);
        scrollEndTimer = setTimeout(() => {
          scrollEndTimer = null;
          const pts = getSnapPoints();
          const cur = el.scrollLeft;
          let nearest = null, best = Infinity;
          for (const sp of pts) { const d = Math.abs(sp - cur); if (d < best) { best = d; nearest = sp; } }
          if (nearest !== null && best > 4) glideTo(nearest);
        }, 140);
      };
      el.addEventListener('scroll', onScroll, { passive: true });

      // Click + drag horizontal scrolling (mouse).
      let isDragging = false, dragStartX = 0, dragStartScroll = 0, dragMoved = false;
      const onMouseDown = (e) => {
        if (e.button !== 0) return;
        // ignore clicks on interactive children (buttons, links, tiles)
        if (e.target.closest('button, a, [data-d3-cursor="Open"]')) return;
        isDragging = true; dragMoved = false; dragStartX = e.clientX; dragStartScroll = el.scrollLeft;
        snapping = false;
        if (scrollEndTimer) { clearTimeout(scrollEndTimer); scrollEndTimer = null; }
        el.style.cursor = 'grabbing';
      };
      const onMouseMove = (e) => {
        if (!isDragging) return;
        const dx = e.clientX - dragStartX;
        if (Math.abs(dx) > 3) dragMoved = true;
        el.scrollLeft = dragStartScroll - dx;
        const max = el.scrollWidth - el.clientWidth;
        setProgress(el.scrollLeft / max);
        if (dragMoved) e.preventDefault();
      };
      const onMouseUp = () => {
        if (!isDragging) return;
        isDragging = false;
        el.style.cursor = '';
      };
      const onClickCapture = (e) => {
        // Suppress click events that fire after a drag so tiles don't open on drag-release.
        if (dragMoved) { e.stopPropagation(); e.preventDefault(); dragMoved = false; }
      };
      el.addEventListener('mousedown', onMouseDown);
      window.addEventListener('mousemove', onMouseMove);
      window.addEventListener('mouseup', onMouseUp);
      el.addEventListener('click', onClickCapture, true);
      el.style.cursor = 'grab';

      let raf, last = performance.now(), idle = 0, lastScroll = el.scrollLeft;
      const tick = (now) => {
        const dt = now - last; last = now;
        if (el.scrollLeft === lastScroll) idle += dt; else idle = 0;
        lastScroll = el.scrollLeft;
        if (false && !snapping && autoDrift > 0 && idle > 1500) {
          el.scrollLeft += 0.3 * autoDrift;
          const max = el.scrollWidth - el.clientWidth;
          if (el.scrollLeft >= max - 2) el.scrollLeft = 0;
          setProgress(el.scrollLeft / max);
        }
        raf = requestAnimationFrame(tick);
      };
      raf = requestAnimationFrame(tick);
      return () => {
        el.removeEventListener('wheel', onWheel);
        el.removeEventListener('scroll', onScroll);
        cancelAnimationFrame(raf);
        if (scrollEndTimer) clearTimeout(scrollEndTimer);
      };
    }, [autoDrift, isMobile]);

    return (
      <section style={{ position: 'relative', height: isMobile ? 'auto' : '100vh', overflow: isMobile ? 'visible' : 'hidden', background: palette.bg, color: palette.fg }}
        onMouseMove={(e) => setMouse({ x: e.clientX / window.innerWidth, y: e.clientY / window.innerHeight })}>
        <AmbientOrb />
        <div ref={ref} className="d3-reel" style={{ display: 'flex', flexDirection: isMobile ? 'column' : 'row', height: isMobile ? 'auto' : '100%', overflowX: isMobile ? 'visible' : 'auto', scrollBehavior: 'auto', scrollbarWidth: 'none', position: 'relative', zIndex: 1 }}>
          <div style={{ flex: isMobile ? '0 0 auto' : '0 0 100vw', width: isMobile ? '100%' : undefined, minHeight: isMobile ? '78vh' : undefined, padding: isMobile ? 'clamp(110px, 18vh, 160px) 7vw clamp(40px, 8vh, 70px)' : '0 6vw', display: 'flex', flexDirection: 'column', justifyContent: 'center', position: 'relative', boxSizing: 'border-box' }}>
            <div style={{ fontSize: 12, fontWeight: 600, letterSpacing: '0.2em', textTransform: 'uppercase', marginBottom: 28, color: accent, display: 'flex', alignItems: 'center', gap: 12 }}>
              <span style={{ width: 8, height: 8, borderRadius: 999, background: accent, animation: 'd3-pulse 1.6s ease-in-out infinite' }} />
              {introKicker}
            </div>
            <KineticText text={introHeadline} size={isMobile ? 'clamp(48px, 15vw, 88px)' : 'clamp(60px, 9.5vw, 180px)'} weight={500} />
          </div>

          {!isMobile && <div style={{ flex: '0 0 6vw' }} />}
          <IntroFounder />
          {!isMobile && <div style={{ flex: '0 0 2vw' }} />}
          <IntroFramework />
          {!isMobile && <div style={{ flex: '0 0 2vw' }} />}
          <IntroBrands />
          {!isMobile && <div style={{ flex: '0 0 6vw' }} />}
        </div>

        {!isMobile && (
        <div style={{ position: 'absolute', bottom: 30, left: '6vw', right: '6vw', height: 1, background: palette.soft, zIndex: 2 }}>
          <div style={{ height: '100%', background: accent, width: `${progress * 100}%`, transition: 'width .1s' }} />
        </div>
        )}
        {!isMobile && (
        <div style={{ position: 'absolute', bottom: 40, right: '6vw', fontSize: 11, fontWeight: 600, letterSpacing: '0.15em', textTransform: 'uppercase', opacity: 0.7, zIndex: 2 }}>
          Scroll ↔ <span style={{ color: accent }}>{String(Math.round(progress * 100)).padStart(2, '0')}%</span>
        </div>
        )}

        <button
          onClick={skipIntro}
          aria-label="Skip intro and jump straight to the work"
          style={{
            position: isMobile ? 'fixed' : 'absolute',
            bottom: isMobile ? 20 : 60,
            left: isMobile ? '50%' : '6vw',
            transform: isMobile ? 'translateX(-50%)' : 'none',
            zIndex: isMobile ? 50 : 3,
            display: 'inline-flex', alignItems: 'center', gap: 10,
            padding: '10px 18px 10px 20px',
            background: isMobile ? palette.bg : 'transparent',
            border: `1px solid ${accent}55`,
            color: palette.fg,
            borderRadius: 999,
            boxShadow: isMobile ? '0 8px 30px rgba(0,0,0,0.5)' : 'none',
            opacity: isMobile && !showSkipMobile ? 0 : 1,
            pointerEvents: isMobile && !showSkipMobile ? 'none' : 'auto',
            fontFamily: 'var(--d3-sans)', fontSize: 11, fontWeight: 600,
            letterSpacing: '0.18em', textTransform: 'uppercase',
            cursor: 'pointer',
            transition: 'background .25s ease, border-color .25s ease, color .25s ease, transform .25s ease, opacity .3s ease',
          }}
          onMouseEnter={(e) => { e.currentTarget.style.background = accent; e.currentTarget.style.borderColor = accent; e.currentTarget.style.color = '#0A0A0A'; e.currentTarget.style.transform = 'translateY(-1px)'; }}
          onMouseLeave={(e) => { e.currentTarget.style.background = 'transparent'; e.currentTarget.style.borderColor = `${accent}55`; e.currentTarget.style.color = palette.fg; e.currentTarget.style.transform = 'translateY(0)'; }}
        >
          Skip intro
          <span style={{ display: 'inline-block', width: 18, height: 1, background: 'currentColor' }} />
          <span style={{ fontSize: 13, lineHeight: 1, transform: 'translateY(-1px)' }}>↓</span>
        </button>
      </section>
    );
  }

  function GridTile({ c, i, onOpen }) {
    const { artStyle, tileRadius, showGloss, accent } = useD3();
    const tileRef = useRef(null);
    const [hovered, setHovered] = useState(false);
    const [mouse, setMouse] = useState({ x: 0.5, y: 0.5 });
    const [revealed, setRevealed] = useState(false);

    useEffect(() => {
      const io = new IntersectionObserver(([e]) => e.isIntersecting && setRevealed(true), { threshold: 0.15 });
      if (tileRef.current) io.observe(tileRef.current);
      return () => io.disconnect();
    }, []);

    const tiltX = (mouse.y - 0.5) * 8;
    const tiltY = (mouse.x - 0.5) * -8;

    return (
      <div
        ref={tileRef}
        onClick={() => onOpen(c.id)}
        onMouseEnter={() => setHovered(true)}
        onMouseLeave={() => { setHovered(false); setMouse({ x: 0.5, y: 0.5 }); }}
        onMouseMove={(e) => {
          const r = tileRef.current.getBoundingClientRect();
          setMouse({ x: (e.clientX - r.left) / r.width, y: (e.clientY - r.top) / r.height });
        }}
        data-d3-cursor="Open"
        style={{
          cursor: 'pointer', perspective: 1200,
          opacity: revealed ? 1 : 0,
          transform: revealed ? 'translateY(0)' : 'translateY(40px)',
          transition: `opacity .9s cubic-bezier(.2,.8,.2,1) ${(i % 4) * 80}ms, transform .9s cubic-bezier(.2,.8,.2,1) ${(i % 4) * 80}ms`,
        }}
      >
        <div style={{
          aspectRatio: '1', overflow: 'hidden', position: 'relative', borderRadius: tileRadius,
          transform: hovered ? `rotateX(${tiltX}deg) rotateY(${tiltY}deg) scale(0.97)` : 'rotateX(0) rotateY(0) scale(1)',
          transition: 'transform .5s cubic-bezier(.2,.8,.2,1)',
          transformStyle: 'preserve-3d',
        }}>
          <div style={{
            position: 'absolute', inset: -8,
            transform: hovered ? `scale(1.12) translate(${(mouse.x - 0.5) * -30}px, ${(mouse.y - 0.5) * -30}px)` : 'scale(1)',
            transition: 'transform .8s cubic-bezier(.2,.8,.2,1)',
            filter: hovered ? 'none' : 'saturate(0.88)',
          }}>
            {c.heroImage ? (
              <img src={c.heroImage} alt={c.client} loading="lazy" style={{ width: '100%', height: '100%', objectFit: 'cover', objectPosition: 'center', display: 'block', background: c.color }} />
            ) : (
              <CaseArt caseItem={c} variant={resolveVariant(i, artStyle)} />
            )}
          </div>
          {showGloss && (
            <div style={{
              position: 'absolute', inset: 0, pointerEvents: 'none',
              background: `linear-gradient(115deg, transparent 40%, #ffffff22 50%, transparent 60%)`,
              transform: hovered ? 'translateX(100%)' : 'translateX(-100%)',
              transition: 'transform 1.1s cubic-bezier(.2,.8,.2,1)',
            }} />
          )}
          <div style={{
            position: 'absolute', left: 0, right: 0, bottom: 0,
            padding: '36px 20px 20px',
            background: 'linear-gradient(180deg, transparent 0%, rgba(10,10,10,0.65) 45%, rgba(10,10,10,0.95) 100%)',
            color: '#F4F1EB',
            transform: hovered ? 'translateY(0)' : 'translateY(101%)',
            transition: 'transform .45s cubic-bezier(.2,.8,.2,1)',
            pointerEvents: 'none',
          }}>
            <div style={{ fontSize: 10, fontWeight: 600, letterSpacing: '0.16em', textTransform: 'uppercase', opacity: 0.75, marginBottom: 6, color: accent }}>
              {c.sectorTag}
            </div>
            <div style={{ fontFamily: 'var(--d3-sans)', fontWeight: 500, fontSize: 'clamp(20px, 1.6vw, 28px)', letterSpacing: '-0.02em', lineHeight: 1.1 }}>
              {c.client}
            </div>
          </div>
        </div>
        <div style={{ marginTop: 14, display: 'flex', justifyContent: 'space-between', alignItems: 'flex-start', overflow: 'hidden' }}>
          <div style={{ overflow: 'hidden' }}>
            <div style={{
              fontFamily: 'var(--d3-sans)', fontWeight: 500, fontSize: 20, letterSpacing: '-0.02em',
              transform: hovered ? 'translateY(-100%)' : 'translateY(0)',
              transition: 'transform .5s cubic-bezier(.2,.8,.2,1)',
            }}>{c.client}</div>
            <div style={{
              fontFamily: 'var(--d3-sans)', fontWeight: 500, fontSize: 20, letterSpacing: '-0.02em', color: accent,
              transform: hovered ? 'translateY(-100%)' : 'translateY(0)',
              transition: 'transform .5s cubic-bezier(.2,.8,.2,1)',
            }}>View case study →</div>
            <div style={{ fontSize: 13, opacity: 0.6, marginTop: 2 }}>{c.shortTitle}</div>
          </div>
        </div>
      </div>
    );
  }

  // Floating filter panel — every category visible, wrapped onto centered rows.
  // Keeps the premium floating-pill surface; the active chip fills solid.
  function FilterBar({ chips, value, onChange, bg, fg }) {
    const isDark = bg !== '#F4F1EB';
    const surface = isDark ? 'rgba(255,255,255,0.05)' : '#FFFFFF';
    const borderCol = isDark ? 'rgba(255,255,255,0.10)' : 'rgba(0,0,0,0.05)';
    const shadow = isDark ? '0 14px 44px rgba(0,0,0,0.55)' : '0 14px 44px rgba(0,0,0,0.10)';
    const hoverBg = isDark ? 'rgba(255,255,255,0.08)' : 'rgba(10,10,10,0.05)';
    return (
      <div style={{ display: 'flex', justifyContent: 'center', marginBottom: 44 }}>
        <div style={{
          maxWidth: 940, width: '100%',
          background: surface, border: `1px solid ${borderCol}`, borderRadius: 24,
          boxShadow: shadow, padding: '10px 12px',
          display: 'flex', flexWrap: 'wrap', justifyContent: 'center', gap: 4,
        }}>
          {chips.map((f) => {
            const active = f === value;
            return (
              <button key={f} onClick={() => onChange(f)} data-d3-cursor="Filter"
                onMouseEnter={(e) => { if (!active) e.currentTarget.style.background = hoverBg; }}
                onMouseLeave={(e) => { if (!active) e.currentTarget.style.background = 'transparent'; }}
                style={{
                  flex: '0 0 auto', whiteSpace: 'nowrap',
                  padding: '9px 16px', border: 0,
                  background: active ? fg : 'transparent',
                  color: active ? bg : fg, opacity: active ? 1 : 0.72,
                  cursor: 'pointer', fontSize: 13, fontWeight: 600, letterSpacing: '0.01em',
                  borderRadius: 999, fontFamily: 'inherit',
                  transition: 'background .25s ease, color .25s ease, opacity .2s ease',
                }}>{f}</button>
            );
          })}
        </div>
      </div>
    );
  }

  function GridView({ onOpen }) {
    const { accent, gridBgInvert, palette } = useD3();
    const [filter, setFilter] = useState('All');
    const filterChips = ['All', ...FILTERS.sectors, ...FILTERS.channels.slice(0, 5)];
    const filtered = filter === 'All' ? CASES : CASES.filter(c => c.sectorTag === filter || c.channels.includes(filter));

    const sectionBg = gridBgInvert ? (palette.bg === '#F4F1EB' ? '#0A0A0A' : '#F4F1EB') : palette.bg;
    const sectionFg = gridBgInvert ? (palette.fg === '#F4F1EB' ? '#0A0A0A' : '#F4F1EB') : palette.fg;

    return (
      <section id="d3-grid" style={{ background: sectionBg, color: sectionFg, padding: 'clamp(96px, 12vh, 160px) 4vw 8vh', position: 'relative', overflow: 'hidden' }}>
        <div style={{ display: 'flex', justifyContent: 'space-between', alignItems: 'flex-end', flexWrap: 'wrap', gap: 24, marginBottom: 40 }}>
          <KineticText text="Case Studies" size="clamp(40px, 6vw, 96px)" weight={500} />
          <div style={{ fontSize: 12, fontWeight: 600, letterSpacing: '0.15em', textTransform: 'uppercase' }}>
            <span style={{ color: accent }}>●</span> {filtered.length} results
          </div>
        </div>
        <FilterBar chips={filterChips} value={filter} onChange={setFilter} bg={sectionBg} fg={sectionFg} />
        <div style={{ display: 'grid', gridTemplateColumns: 'repeat(auto-fill, minmax(300px, 1fr))', gap: 16 }}>
          {filtered.map((c, i) => <GridTile key={c.id} c={c} i={i} onOpen={onOpen} />)}
        </div>
      </section>
    );
  }

  // ── Manifesto: "The Build" — looping 14s mosaic sequence ─────────────
  // 10 tiles in a 12×6 CSS Grid mosaic: 6 real influencer clips + 4
  // Krypton HTML/CSS tiles (dashboard panel, big metric, UK+MY heatmap,
  // live-scan ticker). Tiles populate one-by-one with corner-reticle
  // lock-on, hold "alive", fade to black, resolve to A2M end slate. Loop.

  // Corner reticles (drawn around any tile during "lock-on")
  function Reticles({ color, on }) {
    const s = (extra) => ({
      position: 'absolute', width: 14, height: 14,
      transition: `opacity .35s ease, transform .35s cubic-bezier(.2,.8,.2,1)`,
      opacity: on ? 0.9 : 0,
      transform: on ? 'scale(1)' : 'scale(0.6)',
      ...extra,
    });
    return (
      <>
        <div style={s({ top: 6, left: 6, borderTop: `1px solid ${color}`, borderLeft: `1px solid ${color}` })} />
        <div style={s({ top: 6, right: 6, borderTop: `1px solid ${color}`, borderRight: `1px solid ${color}` })} />
        <div style={s({ bottom: 6, left: 6, borderBottom: `1px solid ${color}`, borderLeft: `1px solid ${color}` })} />
        <div style={s({ bottom: 6, right: 6, borderBottom: `1px solid ${color}`, borderRight: `1px solid ${color}` })} />
      </>
    );
  }

  function ClipTile({ src, client, region, hero, revealed, contentVisible, accent, krypton, paletteFg }) {
    const ringColor = hero ? accent : krypton;
    return (
      <div style={{
        position: 'relative', borderRadius: 6, overflow: 'hidden',
        border: `1px solid ${ringColor}${revealed ? '70' : '00'}`,
        boxShadow: revealed ? `0 ${hero ? 24 : 10}px ${hero ? 80 : 36}px ${ringColor}${hero ? '40' : '22'}` : 'none',
        background: '#0A0A0A',
        transition: 'border-color .5s ease, box-shadow .5s ease',
        height: '100%', width: '100%',
      }}>
        <video src={src} autoPlay muted loop playsInline preload="auto"
          style={{
            width: '100%', height: '100%', objectFit: 'cover', display: 'block',
            opacity: contentVisible ? (hero ? 1 : 0.92) : 0,
            transform: contentVisible ? 'scale(1)' : 'scale(1.06)',
            transition: 'opacity .7s cubic-bezier(.2,.8,.2,1), transform .9s cubic-bezier(.2,.8,.2,1)',
          }} />
        <Reticles color={ringColor} on={revealed} />
        <div style={{
          position: 'absolute', left: 10, bottom: 10, right: 10,
          fontSize: hero ? 10 : 8.5,
          letterSpacing: '0.2em', textTransform: 'uppercase', fontWeight: 600,
          color: paletteFg, opacity: contentVisible ? 0.95 : 0,
          transition: 'opacity .5s ease .2s',
          display: 'flex', alignItems: 'center', gap: 6, flexWrap: 'wrap',
        }}>
          <span style={{ width: hero ? 14 : 8, height: 1, background: ringColor, flexShrink: 0 }} />
          <span style={{ color: ringColor }}>{region}</span>
          {hero && <span style={{ marginLeft: 4, fontFamily: 'var(--d3-sans)', fontSize: 13, letterSpacing: '-0.01em', fontWeight: 500, textTransform: 'none', color: paletteFg }}>{client}</span>}
        </div>
      </div>
    );
  }

  // Faux Krypton dashboard tile: header, animated cohort bars, lock-on indicator, IP counter.
  function KryptonDashTile({ revealed, contentVisible, krypton, accent, paletteFg, alive }) {
    const cohorts = [
      { l: 'Engaged 30+ Female', v: 72 },
      { l: 'Travel · Affluent',   v: 58 },
      { l: 'Auto Intenders',      v: 41 },
      { l: 'Parent · UK',         v: 28 },
    ];
    const [ipCount, setIpCount] = useState(218492);
    useEffect(() => {
      if (!alive) return;
      const id = setInterval(() => setIpCount(n => n + Math.floor(7 + Math.random() * 13)), 220);
      return () => clearInterval(id);
    }, [alive]);
    return (
      <div style={{
        position: 'relative', borderRadius: 6, overflow: 'hidden',
        border: `1px solid ${krypton}${revealed ? '70' : '00'}`,
        boxShadow: revealed ? `0 16px 60px ${krypton}30` : 'none',
        background: `linear-gradient(180deg, ${krypton}0F 0%, #0A0A0A 60%)`,
        transition: 'border-color .5s ease, box-shadow .5s ease',
        height: '100%', width: '100%',
        padding: 'clamp(16px, 1.6vw, 24px)',
        display: 'flex', flexDirection: 'column', gap: 'clamp(8px, 1vh, 18px)',
        fontFamily: 'var(--d3-sans)',
        opacity: contentVisible ? 1 : 0,
        transition: 'opacity .7s ease, border-color .5s ease, box-shadow .5s ease',
      }}>
        <Reticles color={krypton} on={revealed} />
        <div style={{ display: 'flex', alignItems: 'center', justifyContent: 'space-between', flexShrink: 0 }}>
          <div style={{ fontSize: 10, letterSpacing: '0.22em', fontWeight: 700, textTransform: 'uppercase', color: krypton, display: 'flex', alignItems: 'center', gap: 8 }}>
            <span style={{ width: 6, height: 6, borderRadius: 999, background: krypton, animation: 'd3-pulse 1.6s ease-in-out infinite' }} />
            Krypton · Behavioural Engine
          </div>
          <div style={{ fontSize: 9, letterSpacing: '0.18em', textTransform: 'uppercase', opacity: 0.5 }}>v3.2</div>
        </div>
        <div style={{ fontSize: 'clamp(12px, 0.95vw, 14px)', opacity: 0.55, letterSpacing: '0.04em' }}>Audience cohort scan</div>
        <div style={{ display: 'flex', flexDirection: 'column', gap: 'clamp(6px, 0.9vh, 12px)', flex: 1, justifyContent: 'center' }}>
          {cohorts.map((c, i) => (
            <div key={c.l}>
              <div style={{ display: 'flex', justifyContent: 'space-between', alignItems: 'baseline', marginBottom: 4 }}>
                <span style={{ fontSize: 11, letterSpacing: '0.05em', opacity: 0.85 }}>{c.l}</span>
                <span style={{ fontSize: 11, fontFamily: 'var(--d3-mono, "JetBrains Mono", monospace)', color: krypton, fontWeight: 600 }}>{c.v}%</span>
              </div>
              <div style={{ height: 3, background: `${krypton}1A`, borderRadius: 2, overflow: 'hidden' }}>
                <div style={{
                  height: '100%', width: alive ? `${c.v}%` : '0%',
                  background: `linear-gradient(90deg, ${krypton}, ${accent})`,
                  transition: `width 1.4s cubic-bezier(.2,.8,.2,1) ${300 + i * 120}ms`,
                }} />
              </div>
            </div>
          ))}
        </div>
        <div style={{ display: 'flex', justifyContent: 'space-between', alignItems: 'flex-end', gap: 8, marginTop: 'auto', flexShrink: 0 }}>
          <div>
            <div style={{ fontSize: 9, letterSpacing: '0.2em', textTransform: 'uppercase', opacity: 0.55, marginBottom: 2 }}>Lock-on signal</div>
            <div style={{ fontSize: 'clamp(18px, 1.8vw, 24px)', fontFamily: 'var(--d3-mono, "JetBrains Mono", monospace)', color: accent, fontWeight: 600, letterSpacing: '-0.02em' }}>
              {ipCount.toLocaleString()} <span style={{ fontSize: 11, opacity: 0.6, marginLeft: 4 }}>unique IPs</span>
            </div>
          </div>
          <div style={{ display: 'inline-flex', alignItems: 'center', gap: 6, padding: '5px 9px', border: `1px solid ${accent}66`, borderRadius: 999, fontSize: 9, letterSpacing: '0.18em', textTransform: 'uppercase', color: accent, fontWeight: 600 }}>
            <span style={{ width: 6, height: 6, borderRadius: 999, background: accent, animation: 'd3-pulse 1.2s ease-in-out infinite' }} />
            Live
          </div>
        </div>
      </div>
    );
  }

  // Big rotating metric tile
  function KryptonMetricTile({ revealed, contentVisible, krypton, accent, alive }) {
    const metrics = [
      { value: '1.1B',  label: 'Ads served',        sub: 'Giffgaff · 2024' },
      { value: '332×',  label: 'ROAS uplift',       sub: 'Wallace Collection' },
      { value: '4.7B',  label: 'Audience reached',  sub: '2024 · 12 markets' },
      { value: '62%',   label: 'CPS below target',  sub: 'Krypton · behavioural' },
    ];
    const [idx, setIdx] = useState(0);
    useEffect(() => {
      if (!alive) return;
      const id = setInterval(() => setIdx(i => (i + 1) % metrics.length), 2800);
      return () => clearInterval(id);
    }, [alive]);
    const m = metrics[idx];
    return (
      <div style={{
        position: 'relative', borderRadius: 6, overflow: 'hidden',
        border: `1px solid ${accent}${revealed ? '66' : '00'}`,
        boxShadow: revealed ? `0 14px 60px ${accent}30` : 'none',
        background: '#0A0A0A',
        transition: 'border-color .5s ease, box-shadow .5s ease, opacity .7s ease',
        height: '100%', width: '100%',
        padding: 'clamp(14px, 1.4vw, 22px)',
        display: 'flex', flexDirection: 'column', justifyContent: 'space-between',
        opacity: contentVisible ? 1 : 0,
      }}>
        <Reticles color={accent} on={revealed} />
        <div style={{ fontSize: 9, letterSpacing: '0.22em', textTransform: 'uppercase', fontWeight: 700, color: krypton, display: 'flex', alignItems: 'center', gap: 8 }}>
          <span style={{ width: 14, height: 1, background: krypton }} />
          Krypton · Live
        </div>
        <div key={m.value} style={{
          fontFamily: 'var(--d3-sans)', fontWeight: 500,
          fontSize: 'clamp(46px, 5vw, 84px)', lineHeight: 0.88, letterSpacing: '-0.04em',
          color: accent,
          animation: 'd3-metric-in .55s cubic-bezier(.2,.8,.2,1)',
        }}>
          {m.value}
        </div>
        <div>
          <div style={{ fontSize: 11, fontWeight: 600, letterSpacing: '0.04em', marginBottom: 2 }}>{m.label}</div>
          <div style={{ fontSize: 9, letterSpacing: '0.18em', textTransform: 'uppercase', opacity: 0.55 }}>{m.sub}</div>
        </div>
      </div>
    );
  }

  // UK + Malaysia heatmap with pulsing dots
  function KryptonHeatmapTile({ revealed, contentVisible, krypton, accent, alive }) {
    // Hand-picked dot positions inside the tile (% coords) — clustered around UK + MY silhouettes
    const dots = [
      // UK cluster
      { x: 22, y: 18, r: 2.4 }, { x: 28, y: 14, r: 1.6 }, { x: 24, y: 22, r: 3.2 },
      { x: 18, y: 28, r: 1.4 }, { x: 30, y: 24, r: 1.8 }, { x: 26, y: 30, r: 2.0 },
      // MY cluster
      { x: 72, y: 62, r: 2.8 }, { x: 78, y: 60, r: 1.8 }, { x: 70, y: 68, r: 2.2 },
      { x: 76, y: 70, r: 1.4 }, { x: 80, y: 64, r: 1.6 }, { x: 74, y: 56, r: 1.2 },
    ];
    return (
      <div style={{
        position: 'relative', borderRadius: 6, overflow: 'hidden',
        border: `1px solid ${krypton}${revealed ? '60' : '00'}`,
        boxShadow: revealed ? `0 12px 50px ${krypton}25` : 'none',
        background: `radial-gradient(ellipse at 30% 30%, ${krypton}10, #0A0A0A 50%)`,
        transition: 'border-color .5s ease, box-shadow .5s ease, opacity .7s ease',
        height: '100%', width: '100%',
        padding: 'clamp(14px, 1.4vw, 22px)',
        display: 'flex', flexDirection: 'column', justifyContent: 'space-between',
        opacity: contentVisible ? 1 : 0,
      }}>
        <Reticles color={krypton} on={revealed} />
        <div style={{ fontSize: 9, letterSpacing: '0.22em', textTransform: 'uppercase', fontWeight: 700, color: krypton }}>
          <span style={{ display: 'inline-block', width: 14, height: 1, background: krypton, marginRight: 8, verticalAlign: 'middle' }} />
          Active markets
        </div>
        <div style={{ position: 'absolute', inset: 0, pointerEvents: 'none' }}>
          {/* Thin lat/long grid */}
          <svg viewBox="0 0 100 100" preserveAspectRatio="none" style={{ position: 'absolute', inset: 0, width: '100%', height: '100%', opacity: 0.18 }}>
            {[15, 30, 45, 60, 75].map(y => <line key={`h${y}`} x1="0" y1={y} x2="100" y2={y} stroke={krypton} strokeWidth="0.15" />)}
            {[15, 30, 45, 60, 75].map(x => <line key={`v${x}`} x1={x} y1="0" x2={x} y2="100" stroke={krypton} strokeWidth="0.15" />)}
          </svg>
          {/* Connecting arc UK → MY */}
          <svg viewBox="0 0 100 100" preserveAspectRatio="none" style={{ position: 'absolute', inset: 0, width: '100%', height: '100%' }}>
            <path d="M 24 22 Q 50 -10 74 62" fill="none" stroke={accent} strokeWidth="0.3" strokeOpacity={alive ? 0.8 : 0} strokeDasharray="1 1.5" style={{ transition: 'stroke-opacity 1.2s ease' }} />
          </svg>
          {/* Dots */}
          {dots.map((d, i) => (
            <div key={i} style={{
              position: 'absolute', left: `${d.x}%`, top: `${d.y}%`,
              width: `${d.r * 2}px`, height: `${d.r * 2}px`, borderRadius: '50%',
              background: krypton, transform: 'translate(-50%, -50%)',
              boxShadow: `0 0 ${d.r * 4}px ${krypton}`,
              opacity: alive ? 0.9 : 0,
              animation: alive ? `d3-pulse ${1.4 + (i % 3) * 0.4}s ease-in-out infinite ${i * 0.12}s` : 'none',
              transition: 'opacity .8s ease',
            }} />
          ))}
        </div>
        <div style={{ display: 'flex', alignItems: 'baseline', gap: 10, position: 'relative', zIndex: 2 }}>
          <div style={{ fontFamily: 'var(--d3-sans)', fontWeight: 500, fontSize: 'clamp(28px, 2.6vw, 42px)', letterSpacing: '-0.03em', color: accent, lineHeight: 1 }}>12</div>
          <div style={{ fontSize: 10, letterSpacing: '0.18em', textTransform: 'uppercase', opacity: 0.7, fontWeight: 600 }}>Markets · UK · MY · APAC</div>
        </div>
      </div>
    );
  }

  // Live scan ticker — horizontal scrolling targeting events
  function KryptonScanTile({ revealed, contentVisible, krypton, accent, alive }) {
    const events = [
      'BEHAVIOURAL SIGNAL ▸ CULTURE / ARTS 45+',
      'AUDIENCE MATCH ▸ 87% CONFIDENCE',
      'IP COHORT ▸ UK · 138M scanned',
      'LOCK-ON ▸ travel · luxury · male 50+',
      'REAL-TIME BID ▸ premium publishers · WIN',
      'CONTEXTUAL ▸ home improvement · BVOD',
      'INFLUENCER MATCH ▸ MY · beauty 25-34',
      'CONVERSION ▸ CPA -82% vs target',
    ];
    return (
      <div style={{
        position: 'relative', borderRadius: 6, overflow: 'hidden',
        border: `1px solid ${krypton}${revealed ? '55' : '00'}`,
        boxShadow: revealed ? `0 10px 40px ${krypton}20` : 'none',
        background: `linear-gradient(90deg, #0A0A0A 0%, ${krypton}08 50%, #0A0A0A 100%)`,
        transition: 'border-color .5s ease, box-shadow .5s ease, opacity .7s ease',
        height: '100%', width: '100%',
        padding: 'clamp(14px, 1.4vw, 22px)',
        display: 'flex', flexDirection: 'column', justifyContent: 'space-between',
        opacity: contentVisible ? 1 : 0,
      }}>
        <Reticles color={krypton} on={revealed} />
        <div style={{ display: 'flex', justifyContent: 'space-between', alignItems: 'center' }}>
          <div style={{ fontSize: 9, letterSpacing: '0.22em', textTransform: 'uppercase', fontWeight: 700, color: krypton, display: 'flex', alignItems: 'center', gap: 8 }}>
            <span style={{ width: 6, height: 6, borderRadius: 999, background: accent, animation: 'd3-pulse 1.2s ease-in-out infinite' }} />
            Live scan
          </div>
          <div style={{ fontSize: 9, letterSpacing: '0.18em', textTransform: 'uppercase', opacity: 0.5, fontFamily: 'var(--d3-mono, "JetBrains Mono", monospace)' }}>
            T+{Math.floor(Math.random() * 99) + 1}ms
          </div>
        </div>
        <div style={{ overflow: 'hidden', position: 'relative', height: 'clamp(18px, 2vw, 26px)', display: 'flex', alignItems: 'center' }}>
          <div style={{
            display: 'inline-flex', whiteSpace: 'nowrap',
            animation: alive ? 'd3-marquee-left 22s linear infinite' : 'none',
            fontFamily: 'var(--d3-mono, "JetBrains Mono", monospace)',
            fontSize: 'clamp(11px, 0.95vw, 13px)', letterSpacing: '0.06em', color: krypton,
            fontWeight: 500,
          }}>
            {[...events, ...events, ...events].map((e, i) => (
              <span key={i} style={{ display: 'inline-flex', alignItems: 'center', gap: 10, paddingRight: 24 }}>
                <span style={{ width: 5, height: 5, background: accent, borderRadius: 999 }} />
                {e}
              </span>
            ))}
          </div>
        </div>
        <div style={{ display: 'flex', alignItems: 'center', justifyContent: 'space-between', fontSize: 9, letterSpacing: '0.18em', textTransform: 'uppercase' }}>
          <span style={{ opacity: 0.55 }}>87% confidence</span>
          <span style={{ color: accent, fontWeight: 700 }}>▸ LOCK</span>
        </div>
      </div>
    );
  }

  function Manifesto({ scrollRef }) {
    const { accent, palette, seenIntro, replayIntro } = useD3();
    const ref = useRef(null);
    const [visible, setVisible] = useState(false);
    // Phase machine (plays ONCE, no loop):
    //   intro → building → alive → fading → outro → held
    // From 'held', the next scroll-down collapses the section so the grid
    // rises up. Scrolling back up after collapse re-expands to a static slate.
    const [phase, setPhase] = useState('intro');
    const [revealedCount, setRevealedCount] = useState(0);
    const [collapsed, setCollapsed] = useState(false);
    const krypton = '#1A9CA0';

    // ── "The Wall" — 30 scatter clips + 2 Krypton punch-through tiles ────
    // Positions are % offsets from the section center for each clip card.
    // Reveal order = array order (top-rows fill first, scattered jitter).
    // Krypton tiles sit at the END so they punch in LAST, on top.
    // 5 rough rows × 6 cols = 30 positions. Clips are larger and packed
    // tighter so they OVERLAP each other organically — no more separate tiles.
    // Per-clip rotation/scale + jittered positions for designer-pinboard feel.
    const clipSpecs = [
      // Row 1 (y ~18)
      { src: 'video/cases/warner-bros.mp4',       client: 'Warner Bros.',  region: 'UK',   x:  8, y: 18, rot: -3,   scale: 0.95, z:  4 },
      { src: 'video/cases/vodafone.mp4',          client: 'Vodafone',      region: 'UK',   x: 22, y: 15, rot:  2,   scale: 1.05, z:  9 },
      { src: 'video/cases/voxi.mp4',              client: 'VOXI',          region: 'UK',   x: 36, y: 20, rot: -1.5, scale: 0.9,  z:  3 },
      { src: 'video/cases/tenby.mp4',             client: 'Tenby Schools', region: 'MY',   x: 50, y: 16, rot:  3,   scale: 1.0,  z: 11 },
      { src: 'video/cases/nestle.mp4',            client: 'Nestlé',        region: 'MY',   x: 64, y: 19, rot: -2,   scale: 0.95, z:  6 },
      { src: 'video/cases/darlie.mp4',            client: 'Darlie',        region: 'MY',   x: 78, y: 15, rot:  1.5, scale: 1.0,  z:  8 },
      { src: 'video/cases/warner-bros-fern.mp4',  client: 'Warner Bros.',  region: 'UK',   x: 92, y: 19, rot: -3,   scale: 0.9,  z:  3 },

      // Row 2 (y ~36, offset x for overlap into row 1)
      { src: 'video/cases/vodafone-video.mp4',    client: 'Vodafone',      region: 'UK',   x: 15, y: 36, rot:  2.5, scale: 0.95, z:  7 },
      { src: 'video/cases/vodafone-yt.mp4',       client: 'Vodafone',      region: 'UK',   x: 29, y: 33, rot: -2,   scale: 1.0,  z: 10 },
      { src: 'video/cases/zoek-1.mp4',            client: 'Zoek',          region: 'UK',   x: 43, y: 37, rot:  1.5, scale: 0.9,  z:  5 },
      { src: 'video/cases/serpentine.mp4',        client: 'Serpentine',    region: 'UK',   x: 57, y: 34, rot: -3,   scale: 1.05, z: 12 },
      { src: 'video/cases/card-factory.mp4',      client: 'Card Factory',  region: 'UK',   x: 71, y: 37, rot:  2,   scale: 0.95, z:  6 },
      { src: 'video/cases/nasty.mp4',             client: 'NASTY',         region: 'UK',   x: 85, y: 33, rot: -1.5, scale: 1.0,  z:  9 },

      // Row 3 (y ~53)
      { src: 'video/cases/tenby-2.mp4',           client: 'Tenby Schools', region: 'MY',   x:  6, y: 53, rot: -2,   scale: 1.0,  z:  8 },
      { src: 'video/cases/acmar.mp4',             client: 'ACMAR School',  region: 'MY',   x: 20, y: 50, rot:  1.5, scale: 0.95, z: 11 },
      { src: 'video/cases/acmar-2.mp4',           client: 'ACMAR School',  region: 'MY',   x: 34, y: 54, rot: -3,   scale: 0.9,  z:  4 },
      { src: 'video/cases/alcon.mp4',             client: 'Alcon',         region: 'MY',   x: 48, y: 51, rot:  3,   scale: 1.05, z: 13 },
      { src: 'video/cases/alcon-2.mp4',           client: 'Alcon',         region: 'MY',   x: 62, y: 55, rot: -2,   scale: 0.95, z:  7 },
      { src: 'video/cases/derma-angel.mp4',       client: 'Derma Angel',   region: 'MY',   x: 76, y: 50, rot:  2,   scale: 1.0,  z: 10 },
      { src: 'video/cases/derma-angel-2.mp4',     client: 'Derma Angel',   region: 'MY',   x: 90, y: 54, rot: -1.5, scale: 0.9,  z:  3 },

      // Row 4 (y ~70, offset)
      { src: 'video/cases/darlie-2.mp4',          client: 'Darlie',        region: 'MY',   x: 12, y: 70, rot:  3,   scale: 0.9,  z:  5 },
      { src: 'video/cases/nestle-2.mp4',          client: 'Nestlé',        region: 'MY',   x: 26, y: 68, rot: -2,   scale: 1.0,  z:  9 },
      { src: 'video/cases/nestle-3.mp4',          client: 'Nestlé',        region: 'MY',   x: 40, y: 71, rot:  1.5, scale: 1.05, z: 12 },
      { src: 'video/cases/farm-fresh.mp4',        client: 'Farm Fresh',    region: 'MY',   x: 54, y: 68, rot: -3,   scale: 0.95, z:  6 },
      { src: 'video/cases/boh.mp4',               client: 'BOH',           region: 'MY',   x: 68, y: 72, rot:  2,   scale: 1.0,  z: 11 },
      { src: 'video/cases/boh-2.mp4',             client: 'BOH',           region: 'MY',   x: 82, y: 68, rot: -1.5, scale: 0.95, z:  7 },

      // Row 5 (y ~83) — only 4 clips to leave breathing room near bottom
      { src: 'video/cases/kewpie.mp4',            client: 'Kewpie',        region: 'MY',   x: 18, y: 84, rot: -2,   scale: 1.0,  z:  8 },
      { src: 'video/cases/aeon-bank.mp4',         client: 'AEON Bank',     region: 'MY',   x: 38, y: 86, rot:  3,   scale: 1.05, z: 10 },
      { src: 'video/cases/zoek-2.mp4',            client: 'Zoek',          region: 'UK',   x: 58, y: 84, rot: -3,   scale: 0.95, z:  6 },
      { src: 'video/cases/visa.mp4',              client: 'VISA',          region: 'MENA', x: 78, y: 86, rot:  2,   scale: 1.0,  z:  9 },
    ];

    // Sequence timings (ms) — plays once, ends on 'held'
    const T_INTRO_END   =  1800; // intro slate holds before the wall builds
    const T_TILE_GAP    =   150; // 30 clips × 150 = 4500ms
    const T_BUILD_END   = T_INTRO_END + clipSpecs.length * T_TILE_GAP + 600; // ~6900
    const T_ALIVE_END   = T_BUILD_END + 3500; // ~10400
    const T_FADE_END    = T_ALIVE_END + 1800; // ~12200
    const T_OUTRO_END   = T_FADE_END  + 1800; // ~14000 → phase 'held'

    useEffect(() => {
      // rootMargin pre-triggers visibility ~50vh before section enters viewport
      // so the 30 videos get a head start on download.
      const io = new IntersectionObserver(([e]) => e.isIntersecting && setVisible(true), { threshold: 0, rootMargin: '0px 0px 400px 0px' });
      if (ref.current) io.observe(ref.current);
      return () => io.disconnect();
    }, []);

    // Run the sequence once when the section first becomes visible.
    useEffect(() => {
      if (!visible) return;
      const timers = [];
      setPhase('intro');
      setRevealedCount(0);
      timers.push(setTimeout(() => setPhase('building'), T_INTRO_END));
      // Reveal 30 clips one by one with corner-reticle lock-on
      for (let i = 0; i < clipSpecs.length; i++) {
        timers.push(setTimeout(() => setRevealedCount(i + 1), T_INTRO_END + i * T_TILE_GAP));
      }
      timers.push(setTimeout(() => setPhase('alive'),  T_BUILD_END));
      timers.push(setTimeout(() => setPhase('fading'), T_ALIVE_END));
      timers.push(setTimeout(() => setPhase('outro'),  T_FADE_END));
      timers.push(setTimeout(() => setPhase('held'),   T_OUTRO_END));
      return () => timers.forEach(clearTimeout);
    }, [visible]);

    // From 'held': the next scroll-DOWN gesture collapses the section so the
    // grid below rises up. Only fires while the section is actually in view.
    useEffect(() => {
      if (collapsed || phase !== 'held') return;
      const el = scrollRef && scrollRef.current;
      if (!el) return;
      const onWheel = (e) => {
        if (e.deltaY <= 0) return;
        const r = ref.current.getBoundingClientRect();
        const inView = r.top < window.innerHeight * 0.4 && r.bottom > window.innerHeight * 0.6;
        if (!inView) return;
        e.preventDefault();
        el.scrollTop = ref.current.offsetTop; // pin to section top for a clean collapse
        setCollapsed(true);
      };
      el.addEventListener('wheel', onWheel, { passive: false });
      return () => el.removeEventListener('wheel', onWheel);
    }, [phase, collapsed]);

    // Once collapsed: scrolling back UP to the section boundary re-expands it
    // to a static slate (no replay of the sequence).
    useEffect(() => {
      if (!collapsed) return;
      const el = scrollRef && scrollRef.current;
      if (!el) return;
      const onWheel = (e) => {
        if (e.deltaY >= 0) return;
        const r = ref.current.getBoundingClientRect();
        if (r.top > -40) {
          e.preventDefault();
          setCollapsed(false);
          setPhase('static');
          requestAnimationFrame(() => { if (ref.current) el.scrollTop = ref.current.offsetTop; });
        }
      };
      el.addEventListener('wheel', onWheel, { passive: false });
      return () => el.removeEventListener('wheel', onWheel);
    }, [collapsed]);

    // Slate is shown at the start (intro), the end (outro/held), and when
    // re-expanded (static). The wall shows only while it's building/alive/fading.
    const slateVisible = phase === 'intro' || phase === 'outro' || phase === 'held' || phase === 'static';
    const wallVisible  = phase === 'building' || phase === 'alive' || phase === 'fading';

    // Helper: shared wrapper for absolutely-positioned wall tiles.
    // Clip cards use a vh-based height so they fit the section vertically
    // regardless of viewport width. Width is derived via aspect-ratio.
    const wrapStyle = (spec, revealed) => ({
      position: 'absolute',
      left: `${spec.x}%`, top: `${spec.y}%`,
      // Krypton tiles (have explicit `width`) use width-based sizing.
      // Clip tiles use height-based sizing (capped to ~16vh) so they fit 5 rows.
      ...(spec.width
        ? { width: spec.width, aspectRatio: spec.aspectRatio }
        : { height: 'clamp(180px, 24vh, 280px)', aspectRatio: '9 / 16' }),
      transformOrigin: 'center center',
      transform: revealed
        ? `translate(-50%, -50%) rotate(${spec.rot || 0}deg) scale(${spec.scale || 1})`
        : `translate(-50%, -50%) rotate(${(spec.rot || 0) * 0.4}deg) scale(${(spec.scale || 1) * 0.82})`,
      opacity: revealed ? 1 : 0,
      transition: 'opacity .7s cubic-bezier(.2,.8,.2,1), transform .9s cubic-bezier(.2,.8,.2,1)',
      zIndex: spec.z || 5,
      pointerEvents: 'none',
    });

    return (
      <section ref={ref} style={{
        background: palette.bg, color: palette.fg,
        height: collapsed ? '0px' : '100vh',
        position: 'relative', overflow: 'hidden',
        overflowAnchor: 'none',
        transition: 'height 0.85s cubic-bezier(.6,.04,.2,1)',
      }}>
        <AmbientOrb />

        {/* WALL — 30 scattered clip cards.
            Lazy-mount: nothing renders (and no video downloads start) until the
            section enters the viewport (`visible` flips via IntersectionObserver).
            Unmounts when collapsed to free the 30 video decoders. */}
        <div style={{
          position: 'absolute', inset: 0, zIndex: 1,
          opacity: wallVisible ? 1 : 0,
          transition: 'opacity 1.2s cubic-bezier(.2,.8,.2,1)',
        }}>
          {visible && !collapsed && clipSpecs.map((c, i) => {
            const revealed = revealedCount > i;
            return (
              <div key={c.src} style={wrapStyle(c, revealed)}>
                <ClipTile src={c.src} client={c.client} region={c.region} hero={false}
                  revealed={revealed} contentVisible={revealed}
                  accent={accent} krypton={krypton} paletteFg={palette.fg} />
              </div>
            );
          })}
        </div>

        {/* Subtle full-section Krypton scan line — thin teal sweep top→bottom every ~10s during alive */}
        <div aria-hidden="true" style={{
          position: 'absolute', inset: 0, pointerEvents: 'none', zIndex: 2, overflow: 'hidden',
          opacity: (phase === 'alive' || phase === 'fading') ? 1 : 0,
          transition: 'opacity 1s ease',
        }}>
          <div style={{
            position: 'absolute', left: 0, right: 0, height: 1,
            background: `linear-gradient(90deg, transparent 0%, ${krypton}90 50%, transparent 100%)`,
            boxShadow: `0 0 24px ${krypton}55`,
            animation: 'd3-scanline 10s linear infinite',
          }} />
        </div>

        {/* Eyebrow tag — top-left, present except during slate */}
        <div style={{
          position: 'absolute', top: 'clamp(24px, 3vh, 40px)', left: 'clamp(20px, 4vw, 56px)', zIndex: 4,
          fontSize: 11, fontWeight: 600, letterSpacing: '0.22em', textTransform: 'uppercase',
          color: accent, display: 'flex', alignItems: 'center', gap: 10,
          opacity: visible && wallVisible ? 0.9 : 0,
          transition: 'opacity .8s ease',
        }}>
          <span style={{ width: 22, height: 1, background: accent }} />
          Behavioural targeting · <span style={{ color: krypton }}>Krypton</span> · 12 markets
        </div>

        {/* END SLATE — black plate with A2M logo + wordmark + tagline */}
        <div aria-hidden={!slateVisible} style={{
          position: 'absolute', inset: 0, zIndex: 5,
          background: '#0A0A0A',
          display: 'flex', flexDirection: 'column', alignItems: 'center', justifyContent: 'center', gap: 'clamp(20px, 2.4vh, 36px)',
          opacity: slateVisible ? 1 : 0,
          pointerEvents: slateVisible ? 'auto' : 'none',
          transition: `opacity ${slateVisible ? 0.9 : 0.6}s cubic-bezier(.2,.8,.2,1)`,
        }}>
          <img src="assets/logo.png" alt="Audience2Media" style={{
            width: 'clamp(110px, 9vw, 150px)', height: 'auto', display: 'block',
            opacity: slateVisible ? 1 : 0,
            transform: slateVisible ? 'translateY(0)' : 'translateY(14px)',
            transition: 'opacity .9s cubic-bezier(.2,.8,.2,1) .15s, transform .9s cubic-bezier(.2,.8,.2,1) .15s',
          }} />
          <div style={{
            fontFamily: 'var(--d3-sans)', fontWeight: 500,
            fontSize: 'clamp(36px, 5.4vw, 84px)', letterSpacing: '-0.04em', lineHeight: 1,
            color: palette.fg, textAlign: 'center',
            opacity: slateVisible ? 1 : 0,
            transform: slateVisible ? 'translateY(0)' : 'translateY(18px)',
            transition: 'opacity .9s cubic-bezier(.2,.8,.2,1) .35s, transform .9s cubic-bezier(.2,.8,.2,1) .35s',
          }}>Audience2Media</div>
          <div style={{
            width: 'clamp(36px, 4vw, 60px)', height: 1, background: accent,
            opacity: slateVisible ? 0.9 : 0,
            transition: 'opacity .9s ease .55s',
          }} />
          <div style={{
            fontSize: 'clamp(13px, 1.05vw, 16px)', letterSpacing: '0.04em',
            color: palette.fg, opacity: slateVisible ? 0.75 : 0,
            transform: slateVisible ? 'translateY(0)' : 'translateY(12px)',
            transition: 'opacity .9s ease .75s, transform .9s ease .75s',
            fontFamily: 'var(--d3-sans)', fontWeight: 400,
          }}>Brands, meet your audience.</div>
        </div>

        {/* Replay intro pill — preserved, top-right, hidden during slate */}
        {seenIntro && (
          <button
            onClick={replayIntro}
            aria-label="Replay the intro reel"
            style={{
              position: 'absolute', top: 'clamp(24px, 3vh, 36px)', right: 'clamp(20px, 4vw, 56px)', zIndex: 4,
              display: 'inline-flex', alignItems: 'center', gap: 8,
              padding: '8px 14px',
              background: 'transparent',
              border: `1px solid ${palette.fg}30`,
              color: palette.fg,
              borderRadius: 999,
              fontFamily: 'var(--d3-sans)', fontSize: 10, fontWeight: 600,
              letterSpacing: '0.18em', textTransform: 'uppercase',
              cursor: 'pointer',
              opacity: slateVisible ? 0 : 0.7,
              transition: 'opacity .4s ease, border-color .2s ease',
            }}
            onMouseEnter={(e) => { e.currentTarget.style.opacity = 1; e.currentTarget.style.borderColor = accent; }}
            onMouseLeave={(e) => { e.currentTarget.style.opacity = slateVisible ? 0 : 0.7; e.currentTarget.style.borderColor = `${palette.fg}30`; }}
          >
            <span style={{ fontSize: 12, lineHeight: 1 }}>↺</span>
            Replay intro
          </button>
        )}
      </section>
    );
  }

  function Detail({ id, onBack, scrollRef }) {
    const { accent, palette, tileRadius, isMobile } = useD3();
    const c = CASES.find(x => x.id === id) || CASES[0];
    const [t, setT] = useState(0);
    const [lightboxIndex, setLightboxIndex] = useState(null);
    useEffect(() => {
      const el = scrollRef.current; if (!el) return;
      el.scrollTo({ top: 0 });
      const onScroll = () => setT(el.scrollTop);
      el.addEventListener('scroll', onScroll, { passive: true });
      return () => el.removeEventListener('scroll', onScroll);
    }, [id]);

    // Pick the 3 numbers for the Highlights band. Use curated topHighlights if present, else first 3 metrics.
    const highlights = (c.topHighlights && c.topHighlights.length) ? c.topHighlights.slice(0, 3) : (c.metrics || []).slice(0, 3);
    const headlineText = c.headline || c.title || c.shortTitle || '';
    const objective = c.objective || '';
    const solution = c.solution || '';
    const campaignSuccess = c.campaignSuccess || '';
    const hasStructuredStory = objective || solution || campaignSuccess;
    const influencers = c.influencers || [];
    const gallery = c.gallery || [];
    const nextCase = CASES[(CASES.findIndex(x => x.id === id) + 1) % CASES.length];

    const openLightbox = (i) => setLightboxIndex(i);
    const closeLightbox = () => setLightboxIndex(null);
    const prevLightbox = () => setLightboxIndex((i) => (i == null ? null : (i - 1 + gallery.length) % gallery.length));
    const nextLightbox = () => setLightboxIndex((i) => (i == null ? null : (i + 1) % gallery.length));

    // Scale hero title font-size to client name length so long titles
    // (CHARTERHOUSE, Korea Tourism Organization, etc.) don't get cut by the hero image.
    const titleWords = (c.client || '').trim().split(/\s+/).filter(Boolean);
    const longestWord = titleWords.reduce((m, w) => Math.max(m, w.length), 0);
    const titleLen = (c.client || '').length;
    // Size by whichever is the tighter constraint: the longest single word (it
    // can't wrap) or the overall length. Keeps long one-word names like
    // CHARTERHOUSE on one line instead of breaking mid-word.
    const sizeBasis = Math.max(longestWord, Math.ceil(titleLen * 0.55));
    const titleFontSize =
      sizeBasis <= 7  ? 'clamp(52px, 8vw, 150px)' :
      sizeBasis <= 9  ? 'clamp(46px, 7vw, 124px)' :
      sizeBasis <= 11 ? 'clamp(40px, 5.8vw, 104px)' :
      sizeBasis <= 13 ? 'clamp(34px, 5vw, 84px)' :
      sizeBasis <= 16 ? 'clamp(30px, 4.2vw, 72px)' :
                        'clamp(26px, 3.6vw, 60px)';

    return (
      <div style={{ background: palette.bg, color: palette.fg, minHeight: '100%' }}>
        {/* ── 1. HERO BAND — title left, hero image right ─────────────── */}
        <section style={{ position: 'relative', display: 'grid', gridTemplateColumns: isMobile ? '1fr' : 'minmax(0, 1.15fr) minmax(0, 1fr)', gap: 0, minHeight: isMobile ? 'auto' : '88vh', borderBottom: `1px solid ${palette.soft}` }}>
          <div style={{ padding: isMobile ? 'clamp(96px, 16vh, 130px) 7vw clamp(28px, 4vh, 40px)' : 'clamp(120px, 14vh, 180px) clamp(28px, 5vw, 80px) clamp(60px, 8vh, 120px)', display: 'flex', flexDirection: 'column', justifyContent: 'center', minWidth: 0, order: isMobile ? 1 : 0 }}>
            <div style={{ fontSize: 11, fontWeight: 600, letterSpacing: '0.22em', textTransform: 'uppercase', color: accent, marginBottom: 26, display: 'flex', alignItems: 'center', gap: 12 }}>
              <span style={{ display: 'inline-block', width: 22, height: 1, background: accent }} />
              {c.sectorTag} · {c.region || '—'}
            </div>
            <div style={{ fontFamily: 'var(--d3-sans)', fontWeight: 600, fontSize: titleFontSize, lineHeight: 0.95, letterSpacing: '-0.04em', textTransform: 'uppercase', marginBottom: 24, overflowWrap: 'break-word' }}>
              {c.client}
            </div>
            <div style={{ fontFamily: 'var(--d3-sans)', fontWeight: 400, fontSize: 'clamp(18px, 2.2vw, 30px)', lineHeight: 1.25, letterSpacing: '-0.01em', opacity: 0.85, maxWidth: '28ch' }}>
              {headlineText}
            </div>
          </div>

          <div style={{ position: 'relative', minHeight: isMobile ? 'clamp(260px, 44vh, 380px)' : 'clamp(320px, 60vh, 720px)', background: c.color || palette.bg, overflow: 'hidden', order: isMobile ? 2 : 0 }}>
            {c.heroImage ? (
              <img src={c.heroImage} alt={c.client} style={{ width: '100%', height: '100%', objectFit: 'cover', objectPosition: 'center', display: 'block', transform: `scale(${1.02 + t * 0.0002}) translateY(${-t * 0.06}px)`, transition: 'transform .05s' }} />
            ) : (
              <CaseArt caseItem={c} variant="split" />
            )}
          </div>
        </section>

        {/* ── 2. HIGHLIGHTS BAND — black with accent metrics ──────────── */}
        {highlights.length > 0 && (
          <section style={{ background: '#000', color: '#F4F1EB', padding: isMobile ? 'clamp(36px, 6vh, 56px) 6vw' : 'clamp(40px, 6vh, 80px) clamp(24px, 5vw, 80px)', display: 'grid', gridTemplateColumns: `repeat(${Math.min(3, highlights.length)}, 1fr)`, gap: isMobile ? 'clamp(10px, 3vw, 18px)' : 'clamp(24px, 5vw, 80px)', borderTop: `2px solid ${accent}`, borderBottom: `2px solid ${accent}` }}>
            {highlights.map((m, i) => {
              const [on, setOn] = useState(false);
              const r = useRef(null);
              useEffect(() => {
                const io = new IntersectionObserver(([e]) => e.isIntersecting && setOn(true), { threshold: 0.3 });
                if (r.current) io.observe(r.current);
                return () => io.disconnect();
              }, []);
              return (
                <div ref={r} key={i} style={{ overflow: 'hidden', textAlign: 'center', position: 'relative' }}>
                  <div style={{ fontFamily: 'var(--d3-sans)', fontWeight: 700, fontSize: isMobile ? 'clamp(26px, 7.5vw, 40px)' : 'clamp(40px, 6vw, 84px)', lineHeight: 0.95, letterSpacing: '-0.03em', color: accent, transform: on ? 'translateY(0)' : 'translateY(110%)', transition: `transform .9s cubic-bezier(.2,.8,.2,1) ${i * 110}ms` }}>{m.value}</div>
                  <div style={{ fontSize: isMobile ? 9 : 11, letterSpacing: '0.14em', textTransform: 'uppercase', opacity: on ? 0.7 : 0, marginTop: isMobile ? 8 : 12, fontWeight: 600, transition: `opacity .6s ease ${i * 110 + 300}ms` }}>{m.label}</div>
                </div>
              );
            })}
          </section>
        )}

        {/* ── 3. STORY SECTION — Objective / Solution / Campaign Success ─ */}
        <section style={{ padding: isMobile ? 'clamp(48px, 8vh, 72px) 7vw' : 'clamp(60px, 12vh, 160px) clamp(28px, 5vw, 80px)', display: 'grid', gridTemplateColumns: isMobile ? '1fr' : 'minmax(220px, 0.42fr) minmax(0, 1fr)', gap: isMobile ? 'clamp(20px, 4vh, 32px)' : 'clamp(28px, 5vw, 80px)' }}>
          <div style={{ position: isMobile ? 'static' : 'sticky', top: 100, alignSelf: 'flex-start' }}>
            <SectionTitle>Story</SectionTitle>
          </div>
          <div style={{ display: 'flex', flexDirection: 'column', gap: 'clamp(32px, 5vh, 56px)' }}>
            {hasStructuredStory ? (
              <>
                {objective && <StoryBlock title="Objective" body={objective} />}
                {solution && <StoryBlock title="Solution" body={solution} />}
                {campaignSuccess && <StoryBlock title="Campaign success" body={campaignSuccess} />}
              </>
            ) : (
              <StoryBlock title="The story" body={c.summary} />
            )}
          </div>
        </section>

        {/* ── 4. INFLUENCERS & BEHAVIOURAL APPROACH (conditional) ────── */}
        {(influencers.length > 0 || (c.behaviourCategories && c.behaviourCategories.length >= 3) || c.graph) && (
          <section style={{ padding: 'clamp(60px, 10vh, 140px) clamp(28px, 5vw, 80px)', borderTop: `1px solid ${palette.soft}`, display: 'flex', flexDirection: 'column', gap: 'clamp(48px, 8vh, 96px)' }}>

            {influencers.length > 0 && (
              <div style={{ display: 'grid', gridTemplateColumns: isMobile ? '1fr' : 'minmax(220px, 0.42fr) minmax(0, 1fr)', gap: isMobile ? 'clamp(20px, 4vh, 32px)' : 'clamp(28px, 5vw, 80px)', alignItems: 'flex-start' }}>
                <div style={{ position: isMobile ? 'static' : 'sticky', top: 100 }}>
                  <SectionTitle subtitle={'Via our proprietary platform "IMPACT" — matching the right creators to the right audience at the moment of intent.'}>
                    Influencers
                  </SectionTitle>
                </div>
                <div style={{ display: 'grid', gridTemplateColumns: 'repeat(auto-fit, minmax(180px, 1fr))', gap: 12 }}>
                  {influencers.map((inf, i) => <InfluencerCard key={i} inf={inf} accent={accent} palette={palette} caseColor={c.color} caseAlt={c.altColor} index={i} />)}
                </div>
              </div>
            )}

            {c.behaviourCategories && c.behaviourCategories.length >= 3 && (
              <div style={{ display: 'grid', gridTemplateColumns: isMobile ? '1fr' : 'minmax(0, 1fr) minmax(0, 0.85fr)', gap: 'clamp(32px, 5vw, 80px)', alignItems: 'center' }}>
                <SectionTitle subtitle="Krypton decoded real-time signals across high-index segments — placing the brand where genuine intent already lived.">
                  Behavioural Approach
                </SectionTitle>
                <BehaviourRadar categories={c.behaviourCategories} accent={accent} fg={palette.fg} />
              </div>
            )}

            {c.graph && (
              <div style={{ aspectRatio: '16/9', borderRadius: tileRadius, overflow: 'hidden', background: palette.soft, maxWidth: 900, margin: '0 auto', width: '100%' }}>
                <img src={c.graph} alt="Performance chart" style={{ width: '100%', height: '100%', objectFit: 'contain' }} />
              </div>
            )}
          </section>
        )}

        {/* ── 5. ASSETS GALLERY ───────────────────────────────────────── */}
        {gallery.length > 0 && (
          <section style={{ padding: 'clamp(60px, 10vh, 140px) clamp(28px, 5vw, 80px)', borderTop: `1px solid ${palette.soft}` }}>
            <div style={{ marginBottom: 40 }}>
              <SectionTitle>Assets</SectionTitle>
            </div>
            <div style={{ columnWidth: 'clamp(260px, 26vw, 380px)', columnGap: 14 }}>
              {gallery.map((item, i) => (
                <div
                  key={i}
                  onClick={() => openLightbox(i)}
                  data-d3-cursor="View"
                  style={{
                    breakInside: 'avoid',
                    WebkitColumnBreakInside: 'avoid',
                    pageBreakInside: 'avoid',
                    marginBottom: 14,
                    borderRadius: tileRadius,
                    overflow: 'hidden',
                    background: c.color || palette.soft,
                    cursor: 'pointer',
                    position: 'relative',
                    display: 'block',
                    transition: 'transform .35s cubic-bezier(.2,.8,.2,1), box-shadow .35s ease',
                  }}
                  onMouseEnter={(e) => {
                    e.currentTarget.style.transform = 'translateY(-3px)';
                    e.currentTarget.style.boxShadow = `0 24px 60px -20px ${accent}40`;
                    const hint = e.currentTarget.querySelector('[data-zoom-hint]');
                    if (hint) hint.style.opacity = '1';
                  }}
                  onMouseLeave={(e) => {
                    e.currentTarget.style.transform = 'translateY(0)';
                    e.currentTarget.style.boxShadow = 'none';
                    const hint = e.currentTarget.querySelector('[data-zoom-hint]');
                    if (hint) hint.style.opacity = '0';
                  }}
                >
                  {item.type === 'video' ? (
                    <video src={item.dataUrl || item.url} style={{ width: '100%', height: 'auto', display: 'block' }} muted autoPlay loop playsInline />
                  ) : (
                    <img src={item.dataUrl || item.url} alt={item.caption || ''} style={{ width: '100%', height: 'auto', display: 'block' }} loading="lazy" />
                  )}
                  <div
                    data-zoom-hint
                    style={{
                      position: 'absolute',
                      top: 12,
                      right: 12,
                      width: 38,
                      height: 38,
                      borderRadius: 999,
                      background: 'rgba(0,0,0,0.55)',
                      backdropFilter: 'blur(8px)',
                      WebkitBackdropFilter: 'blur(8px)',
                      display: 'flex',
                      alignItems: 'center',
                      justifyContent: 'center',
                      opacity: 0,
                      transition: 'opacity .3s ease',
                      pointerEvents: 'none',
                    }}
                  >
                    {item.type === 'video' ? (
                      <svg width="14" height="14" viewBox="0 0 24 24" fill="#FFFFFF"><path d="M8 5v14l11-7z" /></svg>
                    ) : (
                      <svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="#FFFFFF" strokeWidth="2.4" strokeLinecap="round" strokeLinejoin="round"><circle cx="11" cy="11" r="7" /><line x1="20" y1="20" x2="16.65" y2="16.65" /><line x1="11" y1="8" x2="11" y2="14" /><line x1="8" y1="11" x2="14" y2="11" /></svg>
                    )}
                  </div>
                </div>
              ))}
            </div>
          </section>
        )}

        {/* ── 6. CONTACT FOOTER ───────────────────────────────────────── */}
        <section style={{ background: '#000', color: '#F4F1EB', padding: 'clamp(80px, 14vh, 180px) clamp(28px, 5vw, 80px)', borderTop: `2px solid ${accent}` }}>
          <div style={{ fontSize: 11, fontWeight: 600, letterSpacing: '0.22em', textTransform: 'uppercase', color: accent, marginBottom: 30, display: 'flex', alignItems: 'center', gap: 12 }}>
            <span style={{ display: 'inline-block', width: 22, height: 1, background: accent }} />
            Contact information
          </div>
          <div style={{ display: 'grid', gridTemplateColumns: isMobile ? '1fr' : 'minmax(0, 1.05fr) minmax(0, 1fr)', gap: isMobile ? 'clamp(36px, 6vh, 56px)' : 'clamp(40px, 6vw, 96px)', alignItems: 'flex-start' }}>
            <div>
              <div style={{ fontFamily: 'var(--d3-sans)', fontWeight: 500, fontSize: isMobile ? 'clamp(38px, 11vw, 60px)' : 'clamp(40px, 6vw, 96px)', lineHeight: 1, letterSpacing: '-0.03em', marginBottom: 40 }}>
                Let’s build something.
              </div>
              <div style={{ display: 'grid', gridTemplateColumns: 'repeat(auto-fit, minmax(180px, 1fr))', gap: 'clamp(20px, 2.5vw, 36px)' }}>
                <div>
                  <div style={{ fontSize: 10, fontWeight: 700, letterSpacing: '0.18em', textTransform: 'uppercase', color: accent, opacity: 0.7, marginBottom: 8 }}>Email</div>
                  <a href="mailto:info@audience2media.com" style={{ fontSize: 16, fontWeight: 500, color: '#F4F1EB', textDecoration: 'none' }}>info@audience2media.com</a>
                </div>
                <div>
                  <div style={{ fontSize: 10, fontWeight: 700, letterSpacing: '0.18em', textTransform: 'uppercase', color: accent, opacity: 0.7, marginBottom: 8 }}>Website</div>
                  <a href="https://www.audience2media.com" target="_blank" rel="noreferrer" style={{ fontSize: 16, fontWeight: 500, color: '#F4F1EB', textDecoration: 'none' }}>audience2media.com</a>
                </div>
                <div>
                  <div style={{ fontSize: 10, fontWeight: 700, letterSpacing: '0.18em', textTransform: 'uppercase', color: accent, opacity: 0.7, marginBottom: 8 }}>Follow</div>
                  <div style={{ display: 'flex', gap: 14, fontSize: 14, flexWrap: 'wrap' }}>
                    <a href="https://www.instagram.com/audience2media_global/" data-social="instagram" target="_blank" rel="noreferrer" style={{ color: '#F4F1EB', textDecoration: 'none', borderBottom: '1px solid rgba(244,241,235,0.18)', paddingBottom: 2, transition: 'border-color .2s ease, color .2s ease' }} onMouseEnter={(e) => { e.currentTarget.style.borderBottomColor = accent; e.currentTarget.style.color = accent; }} onMouseLeave={(e) => { e.currentTarget.style.borderBottomColor = 'rgba(244,241,235,0.18)'; e.currentTarget.style.color = '#F4F1EB'; }}>Instagram</a>
                    <a href="https://www.linkedin.com/company/audience2media/" data-social="linkedin" target="_blank" rel="noreferrer" style={{ color: '#F4F1EB', textDecoration: 'none', borderBottom: '1px solid rgba(244,241,235,0.18)', paddingBottom: 2, transition: 'border-color .2s ease, color .2s ease' }} onMouseEnter={(e) => { e.currentTarget.style.borderBottomColor = accent; e.currentTarget.style.color = accent; }} onMouseLeave={(e) => { e.currentTarget.style.borderBottomColor = 'rgba(244,241,235,0.18)'; e.currentTarget.style.color = '#F4F1EB'; }}>LinkedIn</a>
                  </div>
                </div>
              </div>
            </div>
            <div>
              <EnquiryForm />
            </div>
          </div>
        </section>

        {/* ── Next case ──────────────────────────────────────────────── */}
        <Magnetic strength={0.1}>
        <div onClick={() => onBack(nextCase.id)} data-d3-cursor="Next" style={{ padding: isMobile ? 'clamp(48px, 8vh, 72px) 7vw' : 'clamp(60px, 10vh, 140px) clamp(28px, 5vw, 80px)', borderTop: `1px solid ${palette.soft}`, cursor: 'pointer', display: 'grid', gridTemplateColumns: isMobile ? '1fr' : '1fr 1fr', gap: isMobile ? 24 : 40, alignItems: 'center', background: palette.bg, color: palette.fg }}>
          <div>
            <div style={{ fontSize: 11, fontWeight: 600, letterSpacing: '0.22em', textTransform: 'uppercase', color: accent }}>Next →</div>
            <div style={{ fontFamily: 'var(--d3-sans)', fontWeight: 500, fontSize: 'clamp(36px, 5.5vw, 88px)', marginTop: 14, letterSpacing: '-0.03em' }}>
              {nextCase.client}
            </div>
          </div>
          <div style={{ aspectRatio: '16/10', borderRadius: tileRadius, overflow: 'hidden', background: nextCase.color }}>
            {nextCase.heroImage ? <img src={nextCase.heroImage} alt={nextCase.client} style={{ width: '100%', height: '100%', objectFit: 'cover' }} /> : <CaseArt caseItem={nextCase} variant="gradient" />}
          </div>
        </div>
        </Magnetic>

        <Lightbox
          items={gallery}
          index={lightboxIndex}
          onClose={closeLightbox}
          onPrev={prevLightbox}
          onNext={nextLightbox}
        />
      </div>
    );
  }

  function InfluencerCard({ inf, accent, palette, caseColor, caseAlt, index }) {
    // Avatar colour palette — drawn from the case's brand colour first, with a
    // small rotating set as fallback so a row of 5 cards doesn't look identical.
    const PALETTE = [caseColor || accent, caseAlt || '#E2231A', '#FFD500', accent, '#4687FF'];
    const tint = inf.color || PALETTE[index % PALETTE.length] || accent;
    const category = String(inf.category || inf.platform || '').toUpperCase();
    const reach = inf.reach || '';
    const sep = category && reach ? ' · ' : '';
    return (
      <div style={{
        background: '#0E0E0E',
        border: `1px solid ${palette.fg}1A`,
        borderRadius: 14,
        display: 'flex',
        flexDirection: 'column',
        aspectRatio: '0.95',
        position: 'relative',
        overflow: 'hidden',
      }}>
        <div style={{ flex: 1, width: '100%', overflow: 'hidden', position: 'relative', background: '#000' }}>
          {inf.avatar ? (
            <img src={inf.avatar} alt={inf.handle || inf.name} style={{ width: '100%', height: '100%', objectFit: 'cover', display: 'block' }} />
          ) : (
            <div style={{
              position: 'absolute', inset: 0,
              background: `radial-gradient(circle at 35% 30%, ${tint}FF 0%, ${tint}AA 45%, ${tint}33 100%)`,
            }} />
          )}
        </div>
        <div style={{ padding: '18px 22px 20px' }}>
          <div style={{ fontFamily: 'var(--d3-sans)', fontWeight: 500, fontSize: 17, letterSpacing: '-0.01em', marginBottom: 6 }}>
            {inf.handle || inf.name || '—'}
          </div>
          <div style={{ fontSize: 10, fontWeight: 700, letterSpacing: '0.16em', textTransform: 'uppercase', opacity: 0.55, color: palette.fg }}>
            {category}{sep}{reach}
          </div>
        </div>
      </div>
    );
  }

  function BehaviourRadar({ categories, accent, fg }) {
    if (!categories || categories.length < 3) return null;
    const n = categories.length;
    const size = 360;
    const pad = 108;
    const cx = size / 2;
    const cy = size / 2;
    const maxR = size / 2 - pad;
    const ringCount = 5;
    const angleFor = (i) => -Math.PI / 2 + (i / n) * 2 * Math.PI;
    const point = (i, r) => ({ x: cx + Math.cos(angleFor(i)) * r, y: cy + Math.sin(angleFor(i)) * r });

    // Ring grid
    const rings = [];
    for (let r = 1; r <= ringCount; r++) {
      const radius = (r / ringCount) * maxR;
      const pts = Array.from({ length: n }, (_, i) => point(i, radius));
      rings.push(<polygon key={`ring-${r}`} points={pts.map(p => `${p.x},${p.y}`).join(' ')} fill="none" stroke={fg} strokeOpacity={r === ringCount ? 0.25 : 0.12} strokeWidth="1" />);
    }
    // Spokes
    const spokes = categories.map((_, i) => {
      const outer = point(i, maxR);
      return <line key={`spoke-${i}`} x1={cx} y1={cy} x2={outer.x} y2={outer.y} stroke={fg} strokeOpacity="0.12" strokeWidth="1" />;
    });
    // Value polygon
    const valuePts = categories.map((cat, i) => {
      const v = Math.max(0.05, Math.min(1, Number(cat.value) || 0));
      return point(i, maxR * v);
    });
    const valueStr = valuePts.map(p => `${p.x.toFixed(1)},${p.y.toFixed(1)}`).join(' ');
    // Value dots
    const dots = valuePts.map((p, i) => (
      <circle key={`dot-${i}`} cx={p.x} cy={p.y} r="4" fill={accent} />
    ));
    // Labels
    const labels = categories.map((cat, i) => {
      const labelR = maxR + 22;
      const p = point(i, labelR);
      const ang = angleFor(i);
      const anchor = Math.abs(Math.cos(ang)) < 0.25 ? 'middle' : (Math.cos(ang) > 0 ? 'start' : 'end');
      return (
        <text key={`label-${i}`} x={p.x} y={p.y} fill={fg} fillOpacity="0.85" fontSize="12" fontWeight="500" textAnchor={anchor} dominantBaseline="middle" style={{ fontFamily: 'var(--d3-sans)' }}>{cat.label}</text>
      );
    });

    return (
      <div style={{ width: '100%', maxWidth: 480, marginLeft: 'auto', marginRight: 'auto' }}>
        <svg viewBox={`0 0 ${size} ${size}`} style={{ width: '100%', height: 'auto', display: 'block', overflow: 'visible' }} role="img" aria-label="Behavioural approach radar chart">
          {rings}
          {spokes}
          <polygon points={valueStr} fill={accent} fillOpacity="0.18" stroke={accent} strokeWidth="2" strokeLinejoin="round" />
          {dots}
          {labels}
        </svg>
      </div>
    );
  }

  function StoryBlock({ title, body }) {
    const { accent, palette } = useD3();
    const ref = useRef(null);
    const [visible, setVisible] = useState(false);
    useEffect(() => {
      const io = new IntersectionObserver(([e]) => e.isIntersecting && setVisible(true), { threshold: 0.2 });
      if (ref.current) io.observe(ref.current);
      return () => io.disconnect();
    }, []);
    return (
      <div ref={ref} style={{ opacity: visible ? 1 : 0, transform: visible ? 'translateY(0)' : 'translateY(20px)', transition: 'opacity .7s ease, transform .7s cubic-bezier(.2,.8,.2,1)' }}>
        <div style={{ fontSize: 12, fontWeight: 700, letterSpacing: '0.18em', textTransform: 'uppercase', color: accent, marginBottom: 14 }}>{title}</div>
        <div style={{ fontFamily: 'var(--d3-sans)', fontSize: 'clamp(20px, 2.4vw, 32px)', fontWeight: 400, lineHeight: 1.4, letterSpacing: '-0.01em', maxWidth: '62ch' }}>{body}</div>
      </div>
    );
  }

  // ─────────────────────────────────────────────────────────────
  // EnquiryForm — Name / Company / Email / Request.
  // Currently stubs the submit (logs to console). Wire to Firebase
  // Function / Formspree / mailto when backend swap happens.
  // ─────────────────────────────────────────────────────────────
  function EnquiryForm() {
    const { accent, isMobile } = useD3();
    const [form, setForm] = useState({ firstName: '', lastName: '', company: '', email: '', request: '' });
    const [submitting, setSubmitting] = useState(false);
    const [submitted, setSubmitted] = useState(false);
    const [error, setError] = useState('');
    const [attempted, setAttempted] = useState(false);
    const honeypot = useRef('');

    const upd = (k) => (e) => setForm((f) => ({ ...f, [k]: e.target.value }));

    const emailOk = (v) => /^[^@\s]+@[^@\s]+\.[^@\s]+$/.test(String(v).trim());
    const errors = {
      firstName: !form.firstName.trim(),
      lastName: !form.lastName.trim(),
      company: !form.company.trim(),
      email: !emailOk(form.email),
    };
    const hasErrors = errors.firstName || errors.lastName || errors.company || errors.email;

    const onSubmit = async (e) => {
      e.preventDefault();
      if (submitting) return;
      setAttempted(true);
      setError('');
      if (hasErrors) { setError('Please add your first name, last name, company and a valid email address.'); return; }
      if (honeypot.current) { setSubmitted(true); return; } // bot trap — silently drop
      setSubmitting(true);
      try {
        if (!window.firebase || !window.firebase.firestore) throw new Error('Firestore unavailable');
        await window.firebase.firestore().collection('enquiries').add({
          firstName: form.firstName.trim(),
          lastName: form.lastName.trim(),
          name: (form.firstName.trim() + ' ' + form.lastName.trim()).trim(),
          company: form.company.trim(),
          email: form.email.trim(),
          message: form.request.trim(),
          source: 'ourwork.audience2media.com',
          page: (() => { try { return window.location.href; } catch (e) { return ''; } })(),
          read: false,
          createdAt: Date.now(),
        });
        setSubmitting(false);
        setSubmitted(true);
        setForm({ firstName: '', lastName: '', company: '', email: '', request: '' });
        setAttempted(false);
        setTimeout(() => setSubmitted(false), 6000);
      } catch (err) {
        setSubmitting(false);
        setError('Sorry — that didn’t send. Please email info@audience2media.com directly.');
      }
    };

    const baseField = {
      width: '100%',
      background: 'transparent',
      border: 'none',
      borderBottom: '1px solid rgba(244,241,235,0.18)',
      color: '#F4F1EB',
      fontFamily: 'inherit',
      fontSize: 15,
      fontWeight: 400,
      padding: '14px 0 12px',
      outline: 'none',
      transition: 'border-color .25s ease',
    };

    return (
      <form onSubmit={onSubmit} style={{ display: 'flex', flexDirection: 'column', gap: 4 }}>
        <div style={{ fontSize: 10, fontWeight: 700, letterSpacing: '0.22em', textTransform: 'uppercase', color: accent, opacity: 0.85, marginBottom: 22 }}>
          Send us an enquiry
        </div>
        <div style={{ display: 'grid', gridTemplateColumns: isMobile ? '1fr' : 'repeat(2, minmax(0, 1fr))', gap: isMobile ? 0 : 'clamp(16px, 2vw, 28px)' }}>
          <input
            type="text" name="firstName" id="a2m-firstName" autoComplete="given-name"
            placeholder="First name *" aria-label="First name" aria-required="true"
            value={form.firstName}
            onChange={upd('firstName')}
            style={{ ...baseField, borderBottomColor: attempted && errors.firstName ? '#ff6b6b' : 'rgba(244,241,235,0.18)' }}
            onFocus={(e) => { e.target.style.borderBottomColor = accent; }}
            onBlur={(e) => { e.target.style.borderBottomColor = attempted && errors.firstName ? '#ff6b6b' : 'rgba(244,241,235,0.18)'; }}
          />
          <input
            type="text" name="lastName" id="a2m-lastName" autoComplete="family-name"
            placeholder="Last name *" aria-label="Last name" aria-required="true"
            value={form.lastName}
            onChange={upd('lastName')}
            style={{ ...baseField, borderBottomColor: attempted && errors.lastName ? '#ff6b6b' : 'rgba(244,241,235,0.18)' }}
            onFocus={(e) => { e.target.style.borderBottomColor = accent; }}
            onBlur={(e) => { e.target.style.borderBottomColor = attempted && errors.lastName ? '#ff6b6b' : 'rgba(244,241,235,0.18)'; }}
          />
        </div>
        <input
          type="text" name="company" id="a2m-company" autoComplete="organization"
          placeholder="Company *" aria-label="Company" aria-required="true"
          value={form.company}
          onChange={upd('company')}
          style={{ ...baseField, marginTop: 8, borderBottomColor: attempted && errors.company ? '#ff6b6b' : 'rgba(244,241,235,0.18)' }}
          onFocus={(e) => { e.target.style.borderBottomColor = accent; }}
          onBlur={(e) => { e.target.style.borderBottomColor = attempted && errors.company ? '#ff6b6b' : 'rgba(244,241,235,0.18)'; }}
        />
        <input
          type="email" name="email" id="a2m-email" autoComplete="email"
          placeholder="Email *" aria-label="Email" aria-required="true"
          value={form.email}
          onChange={upd('email')}
          style={{ ...baseField, marginTop: 8, borderBottomColor: attempted && errors.email ? '#ff6b6b' : 'rgba(244,241,235,0.18)' }}
          onFocus={(e) => { e.target.style.borderBottomColor = accent; }}
          onBlur={(e) => { e.target.style.borderBottomColor = attempted && errors.email ? '#ff6b6b' : 'rgba(244,241,235,0.18)'; }}
        />
        <textarea
          placeholder="How can we help?"
          rows={4} name="message" id="a2m-message" aria-label="How can we help?"
          value={form.request}
          onChange={upd('request')}
          style={{ ...baseField, marginTop: 8, resize: 'vertical', minHeight: 96 }}
          onFocus={(e) => { e.target.style.borderBottomColor = accent; }}
          onBlur={(e) => { e.target.style.borderBottomColor = 'rgba(244,241,235,0.18)'; }}
        />
        <input
          type="text" tabIndex={-1} autoComplete="off" aria-hidden="true"
          onChange={(e) => { honeypot.current = e.target.value; }}
          style={{ position: 'absolute', left: '-9999px', width: 1, height: 1, opacity: 0 }}
        />
        <div style={{ display: 'flex', alignItems: 'center', gap: 16, marginTop: 22, flexWrap: 'wrap' }}>
          <button
            type="submit"
            disabled={submitting}
            style={{
              background: accent,
              color: '#0A0A0A',
              border: 'none',
              padding: '14px 26px',
              fontFamily: 'inherit',
              fontWeight: 700,
              fontSize: 13,
              letterSpacing: '0.12em',
              textTransform: 'uppercase',
              cursor: submitting ? 'wait' : 'pointer',
              opacity: submitting ? 0.55 : 1,
              transition: 'transform .2s ease, opacity .2s ease',
              display: 'inline-flex',
              alignItems: 'center',
              gap: 8,
            }}
            onMouseEnter={(e) => { if (!submitting) e.currentTarget.style.transform = 'translateY(-2px)'; }}
            onMouseLeave={(e) => { e.currentTarget.style.transform = 'translateY(0)'; }}
          >
            {submitting ? 'Sending…' : 'Send enquiry →'}
          </button>
          {submitted && (
            <span style={{ fontSize: 12, fontWeight: 600, letterSpacing: '0.06em', color: accent }}>
              Thanks — we'll be in touch.
            </span>
          )}
          {error && (
            <span style={{ fontSize: 12, fontWeight: 600, letterSpacing: '0.02em', color: '#ff6b6b', maxWidth: 360, lineHeight: 1.4 }}>
              {error}
            </span>
          )}
        </div>
      </form>
    );
  }

  // ─────────────────────────────────────────────────────────────
  // Lightbox — full-screen modal for Assets gallery items.
  // Esc / overlay click closes, ← → arrows navigate.
  // ─────────────────────────────────────────────────────────────
  function Lightbox({ items, index, onClose, onPrev, onNext }) {
    useEffect(() => {
      if (index == null) return;
      const onKey = (e) => {
        if (e.key === 'Escape') onClose();
        else if (e.key === 'ArrowLeft') onPrev();
        else if (e.key === 'ArrowRight') onNext();
      };
      document.addEventListener('keydown', onKey);
      const prevOverflow = document.body.style.overflow;
      document.body.style.overflow = 'hidden';
      return () => {
        document.removeEventListener('keydown', onKey);
        document.body.style.overflow = prevOverflow;
      };
    }, [index, onClose, onPrev, onNext]);

    if (index == null || !items || !items[index]) return null;
    const item = items[index];
    const multi = items.length > 1;

    const btn = {
      position: 'absolute',
      width: 48,
      height: 48,
      borderRadius: 999,
      border: 'none',
      background: 'rgba(255,255,255,0.08)',
      color: '#F4F1EB',
      cursor: 'pointer',
      display: 'flex',
      alignItems: 'center',
      justifyContent: 'center',
      fontSize: 22,
      backdropFilter: 'blur(10px)',
      WebkitBackdropFilter: 'blur(10px)',
      transition: 'background .2s ease',
    };

    return (
      <div
        onClick={onClose}
        style={{
          position: 'fixed',
          inset: 0,
          zIndex: 1000,
          background: 'rgba(0,0,0,0.94)',
          backdropFilter: 'blur(10px)',
          WebkitBackdropFilter: 'blur(10px)',
          display: 'flex',
          alignItems: 'center',
          justifyContent: 'center',
          padding: 'clamp(20px, 5vw, 80px)',
          animation: 'd3-lb-fade 0.25s ease-out',
        }}
      >
        <button
          onClick={(e) => { e.stopPropagation(); onClose(); }}
          aria-label="Close"
          style={{ ...btn, top: 24, right: 24 }}
          onMouseEnter={(e) => { e.currentTarget.style.background = 'rgba(255,255,255,0.18)'; }}
          onMouseLeave={(e) => { e.currentTarget.style.background = 'rgba(255,255,255,0.08)'; }}
        >
          <svg width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="#F4F1EB" strokeWidth="2.4" strokeLinecap="round"><line x1="6" y1="6" x2="18" y2="18" /><line x1="6" y1="18" x2="18" y2="6" /></svg>
        </button>

        {multi && (
          <>
            <button
              onClick={(e) => { e.stopPropagation(); onPrev(); }}
              aria-label="Previous"
              style={{ ...btn, left: 'clamp(12px, 2vw, 28px)', top: '50%', transform: 'translateY(-50%)' }}
              onMouseEnter={(e) => { e.currentTarget.style.background = 'rgba(255,255,255,0.18)'; }}
              onMouseLeave={(e) => { e.currentTarget.style.background = 'rgba(255,255,255,0.08)'; }}
            >
              <svg width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="#F4F1EB" strokeWidth="2.4" strokeLinecap="round" strokeLinejoin="round"><polyline points="15 6 9 12 15 18" /></svg>
            </button>
            <button
              onClick={(e) => { e.stopPropagation(); onNext(); }}
              aria-label="Next"
              style={{ ...btn, right: 'clamp(12px, 2vw, 28px)', top: '50%', transform: 'translateY(-50%)' }}
              onMouseEnter={(e) => { e.currentTarget.style.background = 'rgba(255,255,255,0.18)'; }}
              onMouseLeave={(e) => { e.currentTarget.style.background = 'rgba(255,255,255,0.08)'; }}
            >
              <svg width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="#F4F1EB" strokeWidth="2.4" strokeLinecap="round" strokeLinejoin="round"><polyline points="9 6 15 12 9 18" /></svg>
            </button>
          </>
        )}

        <div
          onClick={(e) => e.stopPropagation()}
          style={{
            maxWidth: 'min(1200px, 92vw)',
            maxHeight: '88vh',
            display: 'flex',
            flexDirection: 'column',
            alignItems: 'center',
            gap: 14,
          }}
        >
          {item.type === 'video' ? (
            <video
              key={index}
              src={item.dataUrl || item.url}
              controls
              autoPlay
              playsInline
              style={{ maxWidth: '92vw', maxHeight: '80vh', display: 'block', borderRadius: 4 }}
            />
          ) : (
            <img
              key={index}
              src={item.dataUrl || item.url}
              alt={item.caption || ''}
              style={{ maxWidth: '92vw', maxHeight: '80vh', objectFit: 'contain', display: 'block', borderRadius: 4 }}
            />
          )}
          {item.caption && (
            <div style={{ color: '#F4F1EB', fontSize: 13, opacity: 0.7, textAlign: 'center', maxWidth: '60ch' }}>
              {item.caption}
            </div>
          )}
          {multi && (
            <div style={{ color: '#F4F1EB', fontSize: 11, opacity: 0.5, letterSpacing: '0.16em', textTransform: 'uppercase', fontWeight: 600 }}>
              {index + 1} / {items.length}
            </div>
          )}
        </div>
      </div>
    );
  }

  // ─────────────────────────────────────────────────────────────
  // SectionTitle — unified Detail-page section heading.
  // White Manrope Bold inside a tight orange box, with optional subtitle.
  // ─────────────────────────────────────────────────────────────
  function SectionTitle({ children, subtitle, subtitleMaxWidth = 480 }) {
    const { accent } = useD3();
    return (
      <div>
        <span style={{
          display: 'inline-block',
          background: accent,
          color: '#FFFFFF',
          fontFamily: '"Manrope", var(--d3-sans), sans-serif',
          fontWeight: 800,
          fontSize: 'clamp(26px, 3.4vw, 50px)',
          lineHeight: 1.08,
          letterSpacing: '-0.015em',
          padding: '0.08em 0.32em 0.14em',
        }}>
          {children}
        </span>
        {subtitle && (
          <div style={{
            marginTop: 18,
            fontSize: 14,
            opacity: 0.72,
            fontWeight: 300,
            lineHeight: 1.55,
            maxWidth: subtitleMaxWidth,
            letterSpacing: '0.01em',
          }}>
            {subtitle}
          </div>
        )}
      </div>
    );
  }

  function Nav({ onHome }) {
    const { palette, invertLogo, compactNav, view, onBackToGrid, isMobile } = useD3();
    const [hovered, setHovered] = useState(false);
    const isCompact = compactNav && !hovered;
    const isDetail = view && view.page === 'detail';
    // On a phone the detail nav (back + logo + brand + Contact) is too wide for
    // the pill, so collapse the brand wordmark, divider and Contact there.
    const collapseBrand = isCompact || (isMobile && isDetail);
    return (
      <nav
        onMouseEnter={() => setHovered(true)}
        onMouseLeave={() => setHovered(false)}
        style={{
        position: 'fixed',
        top: 16,
        left: 16,
        zIndex: 50,
        display: 'inline-flex',
        alignItems: 'center',
        gap: isCompact ? 0 : 18,
        padding: isCompact ? '10px 14px' : '10px 18px',
        background: 'rgba(15, 15, 15, 0.72)',
        backdropFilter: 'blur(24px) saturate(180%)',
        WebkitBackdropFilter: 'blur(24px) saturate(180%)',
        borderRadius: 999,
        border: '1px solid rgba(244, 241, 235, 0.10)',
        color: '#F4F1EB',
        boxShadow: '0 12px 36px rgba(0, 0, 0, 0.35), 0 2px 8px rgba(0, 0, 0, 0.2)',
        maxWidth: 'calc(100vw - 32px)',
        transition: 'gap .35s cubic-bezier(.2,.8,.2,1), padding .35s cubic-bezier(.2,.8,.2,1)',
      }}>
        {isDetail && (
          <>
            <button onClick={onBackToGrid} data-d3-cursor="Back" aria-label="Back to case studies" style={{
              display: 'inline-flex', alignItems: 'center', gap: 6,
              background: 'transparent', border: 0, color: 'inherit', cursor: 'pointer',
              fontFamily: 'var(--d3-sans)', fontWeight: 500, fontSize: 12, letterSpacing: '0.01em',
              padding: '4px 8px', borderRadius: 999,
              transition: 'background .2s ease',
            }}
            onMouseEnter={(e) => e.currentTarget.style.background = 'rgba(244,241,235,0.08)'}
            onMouseLeave={(e) => e.currentTarget.style.background = 'transparent'}
            >
              <span style={{ fontSize: 14, lineHeight: 1 }}>←</span>
              <span style={{ whiteSpace: 'nowrap' }}>All case studies</span>
            </button>
          </>
        )}
        <div onClick={onHome} style={{ display: 'flex', alignItems: 'center', gap: 9, cursor: 'pointer', fontFamily: 'var(--d3-sans)', fontWeight: 500, fontSize: 14, letterSpacing: '-0.01em' }} data-d3-cursor="Home" aria-label="Home">
          <img src="assets/logo-white.svg" style={{ height: 22, transition: 'transform .35s cubic-bezier(.2,.8,.2,1)' }} alt="Audience2Media" />
          <span style={{
            display: 'inline-block',
            maxWidth: collapseBrand ? 0 : 200,
            opacity: collapseBrand ? 0 : 1,
            overflow: 'hidden',
            whiteSpace: 'nowrap',
            transition: 'max-width .35s cubic-bezier(.2,.8,.2,1), opacity .25s ease',
          }}>Audience2Media</span>
        </div>
        <span style={{
          width: collapseBrand ? 0 : 1,
          height: 18,
          background: 'rgba(244, 241, 235, 0.18)',
          opacity: collapseBrand ? 0 : 1,
          transition: 'width .35s cubic-bezier(.2,.8,.2,1), opacity .2s ease',
        }} />
        <div style={{
          display: 'inline-flex',
          maxWidth: collapseBrand ? 0 : 120,
          opacity: collapseBrand ? 0 : 1,
          overflow: 'hidden',
          transition: 'max-width .35s cubic-bezier(.2,.8,.2,1), opacity .25s ease',
        }}>
          <Magnetic>
            <a href="https://www.audience2media.com/lets-talk" target="_blank" rel="noreferrer" data-d3-cursor="—" style={{ cursor: 'pointer', fontSize: 12, fontWeight: 500, letterSpacing: '0.01em', opacity: 0.82, textDecoration: 'none', color: 'inherit', whiteSpace: 'nowrap' }}>Contact</a>
          </Magnetic>
        </div>
      </nav>
    );
  }

  window.DirectionThree = function DirectionThree(opts = {}) {
    const config = {
      font: 'neue',
      accent: '#F39325',
      glow2: '#4687FF',
      bgPreset: 'dark',
      panelWidth: 60,
      tileRadius: 0,
      artStyle: 'auto',
      autoDrift: 1,
      orbIntensity: 1,
      showCursor: true,
      showOrbs: true,
      showParallax: true,
      showKinetic: true,
      showMagnetic: true,
      showGloss: true,
      showManifesto: true,
      showGrid: true,
      showFooter: true,
      gridBgInvert: true,
      invertLogo: true,
      introKicker: 'Selected work · 2019 / 2025',
      introHeadline: 'Scroll through our work.',
      introBody: '63 campaigns. 12 markets. Horizontal scroll, or drag →',
      manifestoCopy: 'We build audience. Across sectors, channels and continents — always with the same instinct for the moment that matters.',
      ctaHeadline: "Let's build something.",
      ...opts,
    };
    const palette = BG_PRESETS[config.bgPreset] || BG_PRESETS.dark;
    if (config.bgPreset === 'cream') config.invertLogo = false;

    const fontMap = {
      neue: '"Inter", -apple-system, sans-serif',
      grotesk: '"Space Grotesk", sans-serif',
      mono: '"JetBrains Mono", monospace',
      archivo: '"Archivo Black", sans-serif',
      tight: '"Pangram Sans", "Inter", sans-serif',
    };

    const [view, setView] = useState({ page: 'home', id: null });
    const scrollRef = useRef(null);
    const gridScrollRef = useRef(0); // remembers grid scroll position when opening a detail
    const pushedAny = useRef(false); // true once we've pushed a detail entry into browser history
    const [seenIntro, setSeenIntro] = useState(() => {
      try { return localStorage.getItem('a2m_seen_intro') === '1'; } catch (e) { return false; }
    });
    // Phone-width flag. Drives a vertical, single-column mobile layout. False on
    // desktop, so every `isMobile ? … : …` below resolves to the original value —
    // the desktop/monitor layout is guaranteed unchanged.
    const [isMobile, setIsMobile] = useState(() => {
      try { return window.matchMedia('(max-width: 768px)').matches; } catch (e) { return false; }
    });
    useEffect(() => {
      let mq;
      try { mq = window.matchMedia('(max-width: 768px)'); } catch (e) { return; }
      const onChange = () => setIsMobile(mq.matches);
      onChange();
      if (mq.addEventListener) mq.addEventListener('change', onChange); else mq.addListener(onChange);
      return () => { if (mq.removeEventListener) mq.removeEventListener('change', onChange); else mq.removeListener(onChange); };
    }, []);
    useEffect(() => {
      // Detail pages always open at the top. Home scroll is managed by the
      // nav handlers (onHome → top, onBackToGrid → restore grid position);
      // a fresh page load is naturally at scrollTop 0.
      if (view.page === 'detail') scrollRef.current?.scrollTo({ top: 0 });
    }, [view.page, view.id]);

    // Push a detail entry into browser history so the browser back button
    // returns to the grid — identical behaviour to the "All case studies" button.
    const pushDetail = (id) => {
      try {
        history.pushState({ page: 'detail', id }, '', '#case/' + encodeURIComponent(id));
        pushedAny.current = true;
      } catch (e) {}
    };

    // Open a case-study detail, remembering where the user was in the grid.
    const openDetail = (id) => {
      if (scrollRef.current) gridScrollRef.current = scrollRef.current.scrollTop;
      setView({ page: 'detail', id });
      pushDetail(id);
    };

    // Open another case from within a detail page (the Next-case CTA).
    const openNextCase = (id) => {
      setView({ page: 'detail', id });
      pushDetail(id);
    };

    // Browser-history integration: deep-link support + back/forward buttons.
    useEffect(() => {
      const parseHash = () => {
        const m = (window.location.hash || '').match(/^#case\/(.+)$/);
        return m ? decodeURIComponent(m[1]) : null;
      };
      // Sync the initial history entry with the current URL (supports reload / shared links).
      const initId = parseHash();
      if (initId && CASES.find(c => c.id === initId)) {
        setView({ page: 'detail', id: initId });
        try { history.replaceState({ page: 'detail', id: initId }, '', '#case/' + encodeURIComponent(initId)); } catch (e) {}
      } else {
        try { history.replaceState({ page: 'home', id: null }, '', window.location.pathname + window.location.search); } catch (e) {}
      }
      const onPop = (e) => {
        const hashId = parseHash();
        const st = (e.state && e.state.page) ? e.state : { page: hashId ? 'detail' : 'home', id: hashId };
        if (st.page === 'detail' && st.id && CASES.find(c => c.id === st.id)) {
          setView({ page: 'detail', id: st.id });
          requestAnimationFrame(() => scrollRef.current?.scrollTo({ top: 0 }));
        } else {
          setView({ page: 'home', id: null });
          setTimeout(() => scrollRef.current?.scrollTo({ top: gridScrollRef.current, behavior: 'auto' }), 60);
        }
      };
      window.addEventListener('popstate', onPop);
      return () => window.removeEventListener('popstate', onPop);
    }, []);

    const animateScroll = (toTop, duration = 700) => {
      const el = scrollRef.current; if (!el) return;
      const start = el.scrollTop;
      const delta = toTop - start;
      if (Math.abs(delta) < 2) { el.scrollTop = toTop; return; }
      const t0 = performance.now();
      const step = (now) => {
        const t = Math.min((now - t0) / duration, 1);
        const eased = 1 - Math.pow(1 - t, 3); // easeOutCubic
        el.scrollTop = start + delta * eased;
        if (t < 1) requestAnimationFrame(step);
      };
      requestAnimationFrame(step);
    };
    const skipIntro = () => {
      try { localStorage.setItem('a2m_seen_intro', '1'); } catch (e) {}
      setSeenIntro(true);
      // On mobile the intro flows vertically and is taller than one screen, so
      // jump to the Case Studies grid rather than a single viewport height.
      const grid = document.getElementById('d3-grid');
      animateScroll(isMobile && grid ? grid.offsetTop : window.innerHeight);
    };
    const replayIntro = () => {
      try { localStorage.removeItem('a2m_seen_intro'); } catch (e) {}
      setSeenIntro(false);
      const reel = document.querySelector('.d3-reel');
      if (reel) reel.scrollLeft = 0;
      animateScroll(0, 800);
    };

    // Compact the nav pill once the user has scrolled past the intro/hero band.
    // Also track whether to show the "back to top of case studies" pill.
    const [compactNav, setCompactNav] = useState(false);
    const [showBackTop, setShowBackTop] = useState(false);
    useEffect(() => {
      const el = scrollRef.current; if (!el) return;
      const onScroll = () => {
        setCompactNav(el.scrollTop > window.innerHeight * 0.7);
        const grid = document.getElementById('d3-grid');
        setShowBackTop(!!grid && el.scrollTop > grid.offsetTop + window.innerHeight * 0.6);
      };
      onScroll();
      el.addEventListener('scroll', onScroll, { passive: true });
      return () => el.removeEventListener('scroll', onScroll);
    }, [view.page, view.id]);

    // Smooth-scroll back to the top of the Case Studies section (not the page top).
    const scrollToGridTop = () => {
      const grid = document.getElementById('d3-grid');
      if (grid) animateScroll(grid.offsetTop, 600);
    };

    // From a case-study detail page, return to where the user was browsing the
    // grid. Routed through browser history so the on-page "All case studies"
    // button and the browser back button behave identically. Falls back to a
    // direct view change for deep-links (where there's no in-app entry to pop).
    const onBackToGrid = () => {
      if (pushedAny.current && history.state && history.state.page === 'detail') {
        history.back(); // popstate handler restores the grid + scroll position
        return;
      }
      setView({ page: 'home', id: null });
      try { history.replaceState({ page: 'home', id: null }, '', window.location.pathname + window.location.search); } catch (e) {}
      setTimeout(() => {
        if (scrollRef.current) scrollRef.current.scrollTo({ top: gridScrollRef.current, behavior: 'auto' });
      }, 60);
    };

    const ctx = { ...config, palette, seenIntro, skipIntro, replayIntro, compactNav, view, onBackToGrid, isMobile };

    return (
      <D3Ctx.Provider value={ctx}>
        <div ref={scrollRef} style={{
          '--d3-sans': fontMap[config.font] || fontMap.neue,
          width: '100%', height: '100%', overflow: 'auto',
          fontFamily: 'var(--d3-sans)',
          background: palette.bg, color: palette.fg,
          cursor: 'auto',
        }}>
          <style>{`
            @keyframes d3-mq { from { transform: translateX(0) } to { transform: translateX(-33.333%) } }
            @keyframes d3-orb { 0%{transform:translate(0,0) scale(1)} 100%{transform:translate(-10vw,10vh) scale(1.3)} }
            @keyframes d3-orb2 { 0%{transform:translate(0,0) scale(1)} 100%{transform:translate(10vw,-8vh) scale(1.2)} }
            @keyframes d3-pulse { 0%,100%{transform:scale(1);opacity:1} 50%{transform:scale(1.8);opacity:.4} }
            @keyframes d3-line { 0%,100%{width:20px;opacity:.5} 50%{width:60px;opacity:1} }
            @keyframes d3-marquee-left { from { transform: translateX(0) } to { transform: translateX(-33.333%) } }
            @keyframes d3-marquee-right { from { transform: translateX(-33.333%) } to { transform: translateX(0) } }
            @keyframes d3-metric-in { 0% { opacity: 0; transform: translateY(8px) } 100% { opacity: 1; transform: translateY(0) } }
            @keyframes d3-scanline { 0% { top: -2px; opacity: 0 } 8% { opacity: 1 } 92% { opacity: 1 } 100% { top: 100%; opacity: 0 } }
            @keyframes d3-lb-fade { from { opacity: 0 } to { opacity: 1 } }
            .d3-reel::-webkit-scrollbar { display: none; }
          `}</style>
          {config.showCursor && <Cursor scrollRef={scrollRef} />}
          <Nav onHome={() => { setView({ page: 'home' }); try { history.replaceState({ page: 'home', id: null }, '', window.location.pathname + window.location.search); } catch (e) {} requestAnimationFrame(() => scrollRef.current?.scrollTo({ top: 0 })); }} />
          {A2M_PREVIEW && (
            <div style={{ position: 'fixed', bottom: 18, left: '50%', transform: 'translateX(-50%)', zIndex: 9999, background: config.accent, color: '#0A0A0A', fontSize: 11.5, fontWeight: 700, letterSpacing: '0.07em', textTransform: 'uppercase', padding: '8px 16px', borderRadius: 999, boxShadow: '0 8px 30px rgba(0,0,0,0.45)', pointerEvents: 'none', whiteSpace: 'nowrap' }}>
              Preview mode · drafts visible · not the live site
            </div>
          )}
          {view.page === 'home' && (
            <>
              <Reel onOpen={openDetail} />
              {config.showManifesto && <Manifesto scrollRef={scrollRef} />}
              {config.showGrid && <GridView onOpen={openDetail} />}
              {config.showFooter && (
                <footer style={{ padding: '12vh 6vw 5vh', background: palette.bg, color: palette.fg, borderTop: `1px solid ${palette.soft}`, position: 'relative', overflow: 'hidden' }}>
                  <AmbientOrb />
                  <div style={{ position: 'relative', zIndex: 1 }}>
                    <KineticText text={config.ctaHeadline} size={isMobile ? 'clamp(40px, 12vw, 64px)' : 'clamp(48px, 8vw, 150px)'} weight={500} />
                    <div style={{ display: 'grid', gridTemplateColumns: isMobile ? '1fr' : 'minmax(0, 1fr) minmax(0, 1fr)', gap: isMobile ? 'clamp(36px, 6vh, 56px)' : 'clamp(36px, 6vw, 96px)', marginTop: 'clamp(40px, 6vh, 72px)', alignItems: 'flex-start' }}>
                      <div style={{ display: 'grid', gridTemplateColumns: 'repeat(auto-fit, minmax(170px, 1fr))', gap: 'clamp(20px, 2.5vw, 36px)' }}>
                        <div>
                          <div style={{ fontSize: 10, fontWeight: 700, letterSpacing: '0.18em', textTransform: 'uppercase', color: config.accent, opacity: 0.7, marginBottom: 8 }}>Email</div>
                          <a href="mailto:info@audience2media.com" style={{ fontSize: 16, fontWeight: 500, color: palette.fg, textDecoration: 'none' }}>info@audience2media.com</a>
                        </div>
                        <div>
                          <div style={{ fontSize: 10, fontWeight: 700, letterSpacing: '0.18em', textTransform: 'uppercase', color: config.accent, opacity: 0.7, marginBottom: 8 }}>Website</div>
                          <a href="https://www.audience2media.com" target="_blank" rel="noreferrer" style={{ fontSize: 16, fontWeight: 500, color: palette.fg, textDecoration: 'none' }}>audience2media.com</a>
                        </div>
                        <div>
                          <div style={{ fontSize: 10, fontWeight: 700, letterSpacing: '0.18em', textTransform: 'uppercase', color: config.accent, opacity: 0.7, marginBottom: 8 }}>Follow</div>
                          <div style={{ display: 'flex', gap: 18, fontSize: 16, fontWeight: 500, flexWrap: 'wrap' }}>
                            <a href="https://www.instagram.com/audience2media_global/" target="_blank" rel="noreferrer" style={{ color: palette.fg, textDecoration: 'none' }}>Instagram</a>
                            <a href="https://www.linkedin.com/company/audience2media/" target="_blank" rel="noreferrer" style={{ color: palette.fg, textDecoration: 'none' }}>LinkedIn</a>
                          </div>
                        </div>
                      </div>
                      <div><EnquiryForm /></div>
                    </div>
                    <div style={{ display: 'flex', justifyContent: 'space-between', marginTop: 'clamp(40px, 6vh, 72px)', flexWrap: 'wrap', gap: 24, fontSize: 13, opacity: 0.65 }}>
                      <div>© 2026 Audience2Media</div>
                    </div>
                  </div>
                </footer>
              )}
            </>
          )}
          {view.page === 'detail' && (
            <Detail id={view.id} scrollRef={scrollRef} onBack={(maybeId) => typeof maybeId === 'string' ? openNextCase(maybeId) : onBackToGrid()} />
          )}

          {/* Floating "back to top of Case Studies" pill — bottom-right, only on home once scrolled into the grid */}
          {view.page === 'home' && (
            <button
              onClick={scrollToGridTop}
              aria-label="Back to top of case studies"
              style={{
                position: 'fixed', bottom: 'clamp(20px, 4vh, 40px)', right: 'clamp(20px, 4vw, 40px)', zIndex: 60,
                width: 50, height: 50, borderRadius: 999,
                display: 'flex', alignItems: 'center', justifyContent: 'center',
                background: palette.bg, color: palette.fg,
                border: `1px solid ${config.accent}`,
                cursor: 'pointer', fontSize: 20, lineHeight: 1,
                opacity: showBackTop ? 1 : 0,
                transform: showBackTop ? 'translateY(0)' : 'translateY(14px)',
                pointerEvents: showBackTop ? 'auto' : 'none',
                transition: 'opacity .35s ease, transform .35s cubic-bezier(.2,.8,.2,1), background .2s ease, color .2s ease',
                boxShadow: `0 10px 36px rgba(0,0,0,0.45)`,
              }}
              onMouseEnter={(e) => { e.currentTarget.style.background = config.accent; e.currentTarget.style.color = '#0A0A0A'; }}
              onMouseLeave={(e) => { e.currentTarget.style.background = palette.bg; e.currentTarget.style.color = palette.fg; }}
            >↑</button>
          )}
        </div>
      </D3Ctx.Provider>
    );
  };
})();
