/* global React, Icon, WORK, REEL_URL, REEL_WORK, WatchReelButton, CLIENT_LOGOS, slugForWork */
const { useState, useEffect, useRef } = React;

// ===================================================================
// Hero — Marquee variant
// ===================================================================
function HeroMarquee({ onGoto }) {
  return (
    <section className="hero-marquee">
      <div className="blob" />
      <div className="blob blob-2" />
      <div className="marquee-row r1">
        <div className="marquee-track">
          <span>Detail</span><span className="star"><Icon name="star" size={64} /></span>
          <span>driven</span><span>works</span><span className="star"><Icon name="star" size={64} /></span>
          <span>Detail</span><span className="star"><Icon name="star" size={64} /></span>
          <span>driven</span><span>works</span><span className="star"><Icon name="star" size={64} /></span>
        </div>
      </div>
      <div className="marquee-row r2 italic">
        <div className="marquee-track">
          <span>Commercial</span><span>—</span>
          <span>Brand Videos</span><span>—</span>
          <span>Social Content</span><span>—</span>
          <span>Live Events</span><span>—</span>
          <span>Commercial</span><span>—</span>
          <span>Brand Videos</span><span>—</span>
          <span>Social Content</span><span>—</span>
          <span>Live Events</span><span>—</span>
        </div>
      </div>
      <div className="container">
        <div className="hero-meta">
          <p className="lead">A boutique production company in Acton, West London. We make creative <em>corporate</em> and <em>commercial</em> video for brands and agencies.</p>
          <div className="stats">
            <div className="stat"><div className="num">120+</div><div className="lbl">Projects</div></div>
            <div className="stat"><div className="num">8 yrs</div><div className="lbl">In business</div></div>
            <div className="stat"><div className="num">W3</div><div className="lbl">London studio</div></div>
          </div>
          <div className="actions">
            <button className="btn-pri" onClick={() => onGoto('contact')} data-hover="Send"><span>Start a project</span><Icon name="arrow-right" /></button>
            <WatchReelButton className="btn-ghost"/>
          </div>
        </div>
      </div>
    </section>);

}

// ===================================================================
// Hero — Stacked variant
// ===================================================================
function HeroStacked({ onGoto }) {
  return (
    <section className="hero-stacked">
      <div className="blob" style={{ position: 'absolute', width: 1100, height: 1100, borderRadius: '50%', filter: 'blur(120px)', background: 'radial-gradient(circle at 30% 30%, var(--vf-blue) 0%, var(--vf-blue-800) 40%, transparent 70%)', opacity: 0.55, left: -300, top: -200, pointerEvents: 'none' }} />
      <div className="blob blob-2" style={{ position: 'absolute', width: 700, height: 700, borderRadius: '50%', filter: 'blur(120px)', background: 'radial-gradient(circle, var(--vf-blue-500) 0%, var(--vf-blue-900) 60%, transparent 80%)', opacity: 0.4, right: -100, top: 100, pointerEvents: 'none' }} />
      <div className="container">
        <div className="hero-stacked-grid">
          <div>
            <div className="eyebrow"><span className="idx">VF—1.0</span> Boutique film studio · Acton, W3</div>
            <h1 style={{ marginTop: 24 }}>Detail <em>driven</em><br />works.</h1>
            <p>Elevate your brand through powerful visuals and captivating narratives. Commercials, brand videos, social content and live events — made by a small team that handles brief through to grade.</p>
            <div style={{ display: 'flex', gap: 12, marginTop: 36 }}>
              <button className="btn-pri" onClick={() => onGoto('contact')} data-hover="Send"><span>Say hello</span><Icon name="arrow-right" /></button>
              <WatchReelButton className="btn-sec"><Icon name="play-line" /> Watch reel</WatchReelButton>
            </div>
          </div>
          <div className="hero-stack-cards">
            <div className="float-card fc-1" data-hover="Play">
              <div className="thumb" style={{ backgroundImage: 'url(assets/work-isamaya.jpg)' }}>
                <div className="play" />
              </div>
              <div className="meta">Isamaya</div>
              <h4>Spring Lookbook</h4>
            </div>
            <div className="float-card fc-2" data-hover="Play">
              <div className="thumb" style={{ backgroundImage: 'url(assets/work-lent.jpg)' }}>
                <div className="play" />
              </div>
              <div className="meta">Lent</div>
              <h4>I Fly Away</h4>
            </div>
            <div className="float-card fc-3" data-hover="Play">
              <div className="thumb" style={{ backgroundImage: 'url(assets/work-tearfund.jpg)' }}>
                <div className="play" />
              </div>
              <div className="meta">Tearfund</div>
              <h4>Frames</h4>
            </div>
          </div>
        </div>
      </div>
    </section>);

}

// ===================================================================
// Hero — Reel variant (full-bleed BTS image)
// ===================================================================
function HeroReel({ onGoto }) {
  const [reelOpen, setReelOpen] = useState(false);
  return (
    <React.Fragment>
      <section className="hero-reel">
        <div className="reel-bg">
          <iframe
            className="reel-bg-video"
            src="https://iframe.mediadelivery.net/embed/653743/c48a2278-d069-4456-976c-c775bb412d05?autoplay=true&loop=true&muted=true&preload=true&responsive=true"
            allow="autoplay"
            aria-hidden="true"
            title="Valley Films hero reel"
            frameBorder="0"
          />
        </div>
        <div className="reel-overlay" />
        <div className="reel-frame-corners"><span /><span /></div>
        <div className="hero-reel-content">
          <div className="container">
            <div>
              <button
                type="button"
                className="eyebrow eyebrow-reel-link"
                onClick={() => setReelOpen(true)}
                style={{ marginBottom: 24 }}
                aria-label="Play Valley Films reel">
                <span className="idx">VF—1.0</span> Reel
              </button>
              <h1>Detail <em>driven</em> works for brands & agencies.</h1>
            </div>
          </div>
        </div>
      </section>
      {reelOpen && <WorkPlayer work={REEL_WORK} onClose={() => setReelOpen(false)} />}
    </React.Fragment>);

}

function Hero({ variant, onGoto }) {
  if (variant === 'stacked') return <HeroStacked onGoto={onGoto} />;
  if (variant === 'reel') return <HeroReel onGoto={onGoto} />;
  return <HeroMarquee onGoto={onGoto} />;
}

// ===================================================================
// Client strip — repeating logo marquee. Each logo, if it matches a client
// in WORK, is clickable and notifies the parent page to filter the work
// grid to that client's productions. (Was: opened the most recent video.)
// ===================================================================
function ClientStrip({ onSelectClient }) {
  // Case-insensitive match against the logo's display name PLUS any aliases
  // (e.g. an "MWS" logo can declare aliases ['MatchWornShirt', 'Frame the Game B.V.']
  // so it matches a Notion client recorded under the parent legal entity).
  // Exact match wins; otherwise substring either direction.
  // Returns the first matching WORK entry (= most recent in array order).
  const matchVideo = (logo) => {
    const candidates = [logo.name, ...(logo.aliases || [])].map((s) => s.toLowerCase());
    let exact, substr;
    for (const w of WORK) {
      const cname = (w.client || '').toLowerCase();
      if (candidates.includes(cname)) { exact = w; break; }
      if (!substr && candidates.some((c) => cname.includes(c) || c.includes(cname))) substr = w;
    }
    return exact || substr || null;
  };

  const items = CLIENT_LOGOS.map((c) => ({ ...c, video: matchVideo(c) }));

  const renderItem = (c, i) => {
    const interactive = !!c.video;
    const select = () => interactive && onSelectClient && onSelectClient(c.video.client);
    return (
      <span
        key={`${c.name}-${i}`}
        className={`item logo-item${interactive ? ' is-interactive' : ''}`}
        onClick={select}
        role={interactive ? 'button' : undefined}
        tabIndex={interactive ? 0 : undefined}
        aria-label={interactive ? `Filter work by ${c.video.client}` : c.name}
        onKeyDown={interactive ? (e) => { if (e.key === 'Enter' || e.key === ' ') { e.preventDefault(); select(); } } : undefined}
        data-hover={interactive ? 'Filter' : undefined}>
        <img src={c.logo} alt={c.name} loading="lazy" />
      </span>);
  };

  // Render N identical sets so the marquee can translate by exactly
  // 1/N and loop seamlessly. N=4 keeps coverage even on wide displays
  // (3 sets give ~1900px of content; viewports above ~2000px would otherwise
  // show a momentary gap).
  const SETS = 4;

  // rAF-driven marquee. Hand-rolled instead of CSS animation so the speed
  // can be smoothly interpolated when the pointer enters/leaves — a CSS
  // `animation-duration` swap is discrete and produces a visible jump as
  // the browser re-derives current progress for the new timeline. With a
  // JS loop we lerp the current speed toward a target each frame, so the
  // motion just glides into the slower rate without skipping.
  const stripRef = useRef(null);
  const trackRef = useRef(null);
  useEffect(() => {
    const strip = stripRef.current;
    const track = trackRef.current;
    if (!strip || !track) return;

    // Width of one set + the trailing 80px gap. We only ever wrap by this
    // amount, so seams are invisible regardless of how many sets render.
    let setW = 0;
    const measure = () => {
      const set = track.querySelector('.client-strip-set');
      if (set) setW = set.getBoundingClientRect().width;
    };
    measure();
    window.addEventListener('resize', measure);

    // Speed in px/ms. Tuned to roughly match the previous CSS keyframes
    // (50s for one full set on desktop, 32s on mobile). We re-evaluate
    // baseSpeed() inside the loop so a viewport rotation/resize picks up
    // the new cadence without remounting.
    const baseSpeed = () => (window.innerWidth < 760 ? 0.10 : 0.06);
    const HOVER_FACTOR = 0.18; // ~5.5x slower while hovered
    let target = baseSpeed();
    let speed = target;
    let pos = 0;
    let last = performance.now();

    const onEnter = () => { target = baseSpeed() * HOVER_FACTOR; };
    const onLeave = () => { target = baseSpeed(); };
    strip.addEventListener('mouseenter', onEnter);
    strip.addEventListener('mouseleave', onLeave);

    let raf;
    const loop = (t) => {
      const dt = Math.min(48, t - last); // clamp dt so a tab-switch resume doesn't lurch
      last = t;
      // Half-life ~200ms — fast enough to feel responsive, slow enough to feel like a glide.
      const k = 1 - Math.pow(0.001, dt / 1000);
      speed += (target - speed) * k;
      pos -= speed * dt;
      if (setW > 0) {
        // Wrap by exactly one set so the next copy lines up with no jump.
        while (-pos >= setW) pos += setW;
      }
      track.style.transform = `translate3d(${pos}px, 0, 0)`;
      raf = requestAnimationFrame(loop);
    };
    raf = requestAnimationFrame(loop);

    return () => {
      cancelAnimationFrame(raf);
      window.removeEventListener('resize', measure);
      strip.removeEventListener('mouseenter', onEnter);
      strip.removeEventListener('mouseleave', onLeave);
    };
  }, []);

  return (
    <div className="client-strip" ref={stripRef}>
      <div className="client-strip-track" ref={trackRef}>
        {Array.from({ length: SETS }, (_, n) => (
          <span key={n} className="client-strip-set" aria-hidden={n > 0 ? true : undefined}>
            {items.map((c, i) => renderItem(c, n * 1000 + i))}
          </span>
        ))}
      </div>
    </div>);

}

// ===================================================================
// Work index — FilmLaab-style numbered list with hover preview
// ===================================================================
function WorkIndex({ items }) {
  const [hover, setHover] = useState(null);
  const [pos, setPos] = useState({ x: 0, y: 0 });
  return (
    <div className="work-index" onMouseLeave={() => setHover(null)} onMouseMove={(e) => setPos({ x: e.clientX, y: e.clientY })}>
      {items.map((w, i) =>
      <div key={w.id}
      className="work-row"
      onMouseEnter={() => setHover(w)}
      data-hover="Open">
          <div className="idx">W—{String(i + 1).padStart(2, '0')}</div>
          <div className="title">{w.title}</div>
          <div className="client">{w.client}</div>
          <div className="tags">{w.tags.map((t) => <span key={t} className="tag">{t}</span>)}</div>
          <div className="arrow"><Icon name="arrow-up-right" /></div>
        </div>
      )}
      <div className={`work-preview ${hover ? 'is-visible' : ''}`}
      style={{ left: pos.x, top: pos.y, backgroundImage: hover ? `url(${hover.img})` : 'none' }} />
    </div>);

}

// ===================================================================
// Bunny pull-zone hostname for the Valley Films stream library 653743.
// Used to derive hover-preview asset URLs from any embed/play URL. Same
// library = same pull zone = same prefix for every video.
//
// `bunnyMp4Url` returns one of Bunny's MP4-fallback renditions (480p by
// default — sharp enough for a tile-sized hover, ~1–3 MB, only fetched
// when a user lingers on a tile). The full set Bunny serves when "MP4
// fallback" is enabled in the library Encoding settings:
//   play_240p.mp4, play_360p.mp4, play_480p.mp4, play_720p.mp4, play_1080p.mp4
// `bunnyPreviewUrl` is kept around for the touch-device fallback (it
// renders the static animated webp instead of fetching real video).
// ===================================================================
const BUNNY_PULL_ZONE = 'https://vz-c00d983c-d04.b-cdn.net';
function bunnyGuid(url) {
  const m = (url || '').match(/(?:embed|play)\/\d+\/([0-9a-f-]+)/i);
  return m ? m[1] : null;
}
function bunnyPreviewUrl(url) {
  const id = bunnyGuid(url);
  return id ? `${BUNNY_PULL_ZONE}/${id}/preview.webp` : null;
}
function bunnyMp4Url(url, res = '480p') {
  const id = bunnyGuid(url);
  return id ? `${BUNNY_PULL_ZONE}/${id}/play_${res}.mp4` : null;
}

// True on devices with a real mouse (fine pointer + hover). We gate the
// video-on-hover behaviour behind this so iOS / Android / touchscreens
// don't auto-fetch ~MB MP4s when a finger brushes past a tile.
const supportsHoverPreview = () =>
  typeof window !== 'undefined' &&
  window.matchMedia &&
  window.matchMedia('(hover: hover) and (pointer: fine)').matches;

// ===================================================================
// Tile hover preview — replaces the old preview.webp <img>. Mounts a
// muted <video> with preload="none" so nothing loads until the user
// actually hovers. We wait ~120ms before play to filter out the
// micro-hovers as the pointer crosses the grid; otherwise every tile
// in the path would trigger a fetch.
//
// On touch devices (no hover support) we render the static preview.webp
// instead — same fade-in feel, zero video bandwidth on mobile.
// ===================================================================
function TileHoverPreview({ mp4Src, webpSrc, isHovered }) {
  const videoRef = useRef(null);
  const [playReady, setPlayReady] = useState(false);
  // Capture the hover-capability once, on mount — matchMedia changes are
  // exceedingly rare in practice and re-evaluating per render is wasted work.
  const canHover = useRef(supportsHoverPreview()).current;

  useEffect(() => {
    if (!canHover) return;
    const v = videoRef.current;
    if (!v) return;
    if (isHovered) {
      // Brief delay so a fast pass over the grid doesn't fire a play() on
      // every tile in the path. If the user leaves before 120ms is up,
      // we never even start fetching.
      const t = setTimeout(() => {
        // Lazy-attach src on first hover. After that the browser caches
        // the file, so subsequent hovers play instantly.
        if (!v.src && mp4Src) v.src = mp4Src;
        const p = v.play();
        if (p && typeof p.catch === 'function') p.catch(() => { /* autoplay blocked — fine */ });
        setPlayReady(true);
      }, 120);
      return () => clearTimeout(t);
    } else {
      v.pause();
      // Don't reset currentTime — if the user comes back the loop resumes
      // mid-frame, which feels more alive than restarting from 0.
      setPlayReady(false);
    }
  }, [isHovered, mp4Src, canHover]);

  if (!canHover) {
    // Touch / coarse-pointer fallback — keep the lightweight webp.
    return webpSrc ? (
      <img className="tile-bg-preview" src={webpSrc} alt="" aria-hidden="true" loading="lazy" />
    ) : null;
  }

  if (!mp4Src) return null;
  return (
    <video
      ref={videoRef}
      className={`tile-bg-preview tile-bg-video${playReady ? ' is-playing' : ''}`}
      muted
      loop
      playsInline
      preload="none"
      aria-hidden="true"
      tabIndex={-1}
    />);

}

// ===================================================================
// Work grid — alt view. URL-driven: /work/:slug opens the player modal.
// ===================================================================

const WORK_GRID_MAX = 8;
const RANK_WEIGHT = { High: 3, Medium: 2, Low: 1, 'Hidden - Logo': 0, 'Hidden - Entirely': 0 };

// Column-width vocabulary; each pattern sums to 12. Slot constraints:
//   'h' — must accept a horizontal-leaning crop (col-6/8/12). Vertical
//         orientations are excluded so 9:16 source isn't force-stretched.
//   'v' — must be a vertical orientation.
//   'a' — anything fits.
// Order in this list is the picker's default preference when nothing else
// rules. The picker also (a) prefers patterns that exactly use the
// remaining tail and (b) anti-repeats the previous row for visual variety.
const WORK_PATTERNS = [
  { cols: [12],       constraints: ['h']         },
  { cols: [8, 4],     constraints: ['h', 'a']    },
  { cols: [4, 4, 4],  constraints: ['a', 'a', 'a'] },
  { cols: [4, 8],     constraints: ['v', 'h']    },
  { cols: [6, 6],     constraints: ['h', 'h']    },
];

function workOrientation(w) {
  const o = w && w.orientation;
  return o === 'vertical' || o === 'square' ? o : 'horizontal';
}

function fitsSlot(slot, item) {
  const o = workOrientation(item);
  if (slot === 'v') return o === 'vertical';
  if (slot === 'h') return o !== 'vertical';
  return true;
}

function sameCols(a, b) {
  if (!a || !b || a.length !== b.length) return false;
  return a.every((c, i) => c === b[i]);
}

// Cropping penalty: a horizontal item dropped into col-4 (4:5) loses the
// sides of its frame. The packer minimises total crop so a [4,8] pattern
// beats [4,4,4] when only the leading slot is vertical.
function patternCropPenalty(pattern, head) {
  let penalty = 0;
  pattern.cols.forEach((col, idx) => {
    const o = workOrientation(head[idx]);
    if (o === 'horizontal' && col === 4) penalty += 1;
  });
  return penalty;
}

function pickWorkPattern(queue, remaining, lastCols, maxCol) {
  const head = queue.slice(0, 3);
  const viable = WORK_PATTERNS.filter((p) => {
    if (p.cols.length > remaining) return false;
    if (p.cols.some((c) => c > maxCol)) return false;
    return p.constraints.every((c, idx) => fitsSlot(c, head[idx]));
  });
  if (viable.length) {
    const scored = viable.map((p) => ({ p, s: patternCropPenalty(p, head) }));
    const minScore = scored.reduce((m, x) => Math.min(m, x.s), Infinity);
    const best = scored.filter((x) => x.s === minScore).map((x) => x.p);
    // Tail packing: when ≤3 items remain, prefer the pattern that lands them
    // all in one row so the grid never ends with an orphan gap.
    if (remaining <= 3) {
      const exact = best.filter((p) => p.cols.length === remaining);
      if (exact.length) {
        const fresh = exact.filter((p) => !sameCols(p.cols, lastCols));
        return (fresh.length ? fresh : exact)[0];
      }
    }
    const fresh = best.filter((p) => !sameCols(p.cols, lastCols));
    return (fresh.length ? fresh : best)[0];
  }
  // Forced fallback: every remaining item is vertical with no horizontal
  // companion left. Stretching a lone vertical to col-12 is uglier than
  // letting [4,4,4] cover three verticals in narrow slots, so prefer that
  // when it fits; otherwise live with the stretch on the very last row.
  // (Lone-item fallback ignores maxCol — a half-empty row beats no row.)
  const cols = remaining >= 3 ? [4, 4, 4] : remaining === 2 ? [6, 6] : [12];
  return { cols };
}

function packWorkTiles(items, maxCol = 12) {
  if (!items || items.length === 0) return [];
  // Rank-sort, then cap. High items naturally land in earlier rows (which
  // is where the bigger pattern slots — col-12, col-8 — live).
  const ranked = [...items]
    .map((w, i) => ({ w, i, weight: RANK_WEIGHT[w.ranking] ?? 2 }))
    .sort((a, b) => b.weight - a.weight || a.i - b.i)
    .map(({ w }) => w);
  const visible = ranked.slice(0, WORK_GRID_MAX);
  const sized = [];
  let i = 0;
  let lastCols = null;
  while (i < visible.length) {
    const pattern = pickWorkPattern(visible.slice(i), visible.length - i, lastCols, maxCol);
    pattern.cols.forEach((col, j) => sized.push({ w: visible[i + j], col }));
    i += pattern.cols.length;
    lastCols = pattern.cols;
  }
  return sized;
}

function WorkGrid({ items, workSlug, maxCol = 12 }) {
  // Layout rules:
  //   - Cap at 8 visible tiles per filter; lowest-ranked drop first.
  //   - Sort by ranking so High → top, Low → bottom of the visible set.
  //   - Vertical-orientation items are locked to col-4 (4:5) so 9:16 source
  //     content doesn't get force-stretched. Horizontal items can take
  //     col-6/8/12. Square sits in between and the packer treats it as
  //     "any width is fine".
  //   - `maxCol` caps the widest pattern slot (HomePage passes 8 for the
  //     All filter so no single tile dominates the page; other filters keep
  //     the col-12 hero option when they only have one or two items).
  //   - Rows are built greedily from a small pattern vocabulary that always
  //     sums to 12. Anti-repeat keeps adjacent rows visually distinct.
  //   - Mobile (<900px) collapses everything to full-width via existing CSS.
  // The modal lookup uses the full `items` list (not the capped set) so a
  // direct /work/<slug> URL still works for items the cap dropped.
  const sized = packWorkTiles(items, maxCol);
  // Use the full WORK array for slug lookup so hidden items (Hidden - Logo /
  // Hidden - Entirely) are still reachable via a direct /work/<slug> URL even
  // though they're excluded from the filtered `items` passed in.
  const active = workSlug ? WORK.find((w) => slugForWork(w) === workSlug) : null;
  // Closing the modal: pop history. If the user landed direct on /work/foo
  // (no in-app history), fall back to pushing /.
  const closePlayer = () => {
    if (window.history.length > 1) {
      window.history.back();
    } else {
      window.history.pushState(null, '', '/');
      window.dispatchEvent(new PopStateEvent('popstate'));
    }
  };
  return (
    <React.Fragment>
      <div className="work-grid">
        {sized.map(({ w, col }) => (
          <WorkTile key={w.id} w={w} col={col} />
        ))}
      </div>
      {active && <WorkPlayer work={active} onClose={closePlayer} />}
    </React.Fragment>);

}

// ===================================================================
// Single tile — extracted so each one can own its hover state. That
// state drives TileHoverPreview, which lazy-loads the real MP4 from
// Bunny when the pointer settles on a tile (fine-pointer devices only;
// touch devices get the static webp).
// ===================================================================
function WorkTile({ w, col }) {
  const [hovered, setHovered] = useState(false);
  const duration = (w.tags || []).find((t) => /^\d+:\d+$/.test(t));
  const chips = [w.type, duration].filter(Boolean);
  const size = `col-${col}`;
  const href = `/work/${slugForWork(w)}`;
  const mp4Src = bunnyMp4Url(w.videoUrl, '480p');
  const webpSrc = bunnyPreviewUrl(w.videoUrl);

  return (
    <a
      href={href}
      className={`tile ${size}`}
      data-hover="Play"
      aria-label={`${w.client} — ${w.title}`}
      onMouseEnter={() => setHovered(true)}
      onMouseLeave={() => setHovered(false)}
      onFocus={() => setHovered(true)}
      onBlur={() => setHovered(false)}>
      <div className="tile-bg" style={{ backgroundImage: `url(${w.img})` }} />
      <TileHoverPreview mp4Src={mp4Src} webpSrc={webpSrc} isHovered={hovered} />
      <div className="tile-meta-top">
        <span className="num">{w.client}</span>
        <span className="tile-play-btn" aria-hidden="true">
          <svg viewBox="0 0 14 14" width="12" height="12">
            <path d="M3 1.5 L12 7 L3 12.5 Z" fill="currentColor" />
          </svg>
        </span>
      </div>
      <div className="tile-content">
        <div className="chips">
          {chips.map((c) => <span key={`${w.id}-${c}`} className="chip">{c}</span>)}
        </div>
        <h3 className="title">{w.title}</h3>
      </div>
    </a>);

}

// ===================================================================
// Work mini-player — opens on tile click
// ===================================================================
function WorkPlayer({ work, onClose }) {
  const [shared, setShared] = useState(false);
  useEffect(() => {
    const FOCUSABLE = 'a[href], button:not([disabled]), [tabindex]:not([tabindex="-1"]), input, textarea, select';
    const onKey = (e) => {
      if (e.key === 'Escape') { onClose(); return; }
      if (e.key !== 'Tab') return;
      const modal = document.querySelector('.vf-player-shell');
      if (!modal) return;
      const items = [...modal.querySelectorAll(FOCUSABLE)].filter(el => !el.hidden && el.offsetParent !== null);
      if (!items.length) return;
      const first = items[0], last = items[items.length - 1];
      if (e.shiftKey && document.activeElement === first) { e.preventDefault(); last.focus(); }
      else if (!e.shiftKey && document.activeElement === last) { e.preventDefault(); first.focus(); }
    };
    window.addEventListener('keydown', onKey);
    document.body.style.overflow = 'hidden';
    const focusInModal = setTimeout(() => {
      const closeBtn = document.querySelector('.vf-player-shell .player-close, .vf-player-shell button');
      closeBtn && closeBtn.focus();
    }, 0);
    return () => {
      clearTimeout(focusInModal);
      window.removeEventListener('keydown', onKey);
      document.body.style.overflow = '';
    };
  }, [onClose]);
  const duration = (work.tags || []).find((t) => /^\d+:\d+$/.test(t));
  const videoUrl = work.videoUrl || REEL_URL;
  // Normalise Bunny Stream player URLs to their iframe embed equivalent.
  // player.mediadelivery.net/play/{lib}/{guid} → iframe.mediadelivery.net/embed/{lib}/{guid}
  const bunnyMatch = videoUrl.match(/^https?:\/\/(?:player|iframe)\.mediadelivery\.net\/(?:play|embed)\/(\d+)\/([0-9a-f-]+)/i);
  const isBunny = !!bunnyMatch;
  const baseEmbed = isBunny
    ? `https://iframe.mediadelivery.net/embed/${bunnyMatch[1]}/${bunnyMatch[2]}`
    : videoUrl;
  const autoplayParams = isBunny
    ? 'autoplay=true&preload=true'
    : 'autoplay=1&title=0&byline=0&portrait=0';
  const embedSrc = baseEmbed.includes('?') ? baseEmbed : `${baseEmbed}?${autoplayParams}`;
  const onShare = async () => {
    const url = window.location.href;
    try {
      if (navigator.share) {
        await navigator.share({ title: `${work.client} — ${work.title}`, url });
      } else {
        await navigator.clipboard.writeText(url);
        setShared(true);
        setTimeout(() => setShared(false), 1800);
      }
    } catch { /* user-cancelled or unsupported */ }
  };
  return (
    <div className="vf-player" onClick={onClose}>
      <div className="vf-player-shell" onClick={(e) => e.stopPropagation()}>
        <div className="vf-player-head">
          <div className="vf-player-meta">
            <span className="vf-player-type">{work.type}</span>
            {duration && <><span className="vf-player-sep">·</span><span>{duration}</span></>}
          </div>
          <button className="vf-player-close" type="button" onClick={onClose} aria-label="Close" data-hover="Close">
            <svg viewBox="0 0 16 16" width="14" height="14" aria-hidden="true">
              <path d="M3 3 L13 13 M13 3 L3 13" stroke="currentColor" strokeWidth="1.6" strokeLinecap="round" />
            </svg>
          </button>
        </div>
        <div className="vf-player-stage">
          <iframe
            className="vf-player-frame"
            src={embedSrc}
            title={`${work.client} — ${work.title}`}
            allow="autoplay; fullscreen; picture-in-picture"
            allowFullScreen
            frameBorder="0"
          />
        </div>
        <div className="vf-player-foot">
          <div className="vf-player-titles">
            <div className="vf-player-client">{work.client}</div>
            <h2 className="vf-player-title">{work.title}</h2>
            {work.description && <p className="vf-player-desc">{work.description}</p>}
          </div>
          <div className="vf-player-actions">
            <button className="vf-player-action" type="button" data-hover="Share" onClick={onShare}>
              {shared ? 'Link copied' : 'Share'}
            </button>
          </div>
        </div>
      </div>
    </div>);

}

Object.assign(window, { Hero, ClientStrip, WorkIndex, WorkGrid, WorkPlayer });