/* ============================================================
   MOODLAB — Moodboard Canvas + Link Capture Studio
   · Board cards accept ANY image file (PNG/JPEG/GIF/WebP/AVIF/SVG)
     — original bytes kept, so GIF/WebP animation is preserved
   · Link tab: paste YouTube / Vimeo / Instagram links → capture
     stills or in/out clips into the project media database (IndexedDB)
   · Real exports: animated GIF (own encoder), WebM video, WebP/PNG stills
   ============================================================ */

/* ---------- project media database (IndexedDB) ---------- */
function mlDB() {
  return new Promise((res, rej) => {
    const dbName = window.PROJ_ID && window.PROJ_ID !== 'crimson' ? 'silverlines-media-' + window.PROJ_ID : 'silverlines-media';
    const r = indexedDB.open(dbName, 1);
    r.onupgradeneeded = () => r.result.createObjectStore('media', { keyPath: 'id' });
    r.onsuccess = () => res(r.result);
    r.onerror = () => rej(r.error);
  });
}
async function mlAll() {
  const db = await mlDB();
  return new Promise((res, rej) => {
    const q = db.transaction('media').objectStore('media').getAll();
    q.onsuccess = () => res(q.result || []); q.onerror = () => rej(q.error);
  });
}
async function mlPut(item) {
  const db = await mlDB();
  return new Promise((res, rej) => {
    const t = db.transaction('media', 'readwrite');
    t.objectStore('media').put(item);
    t.oncomplete = () => res(); t.onerror = () => rej(t.error);
  });
}
async function mlDel(id) {
  const db = await mlDB();
  return new Promise((res, rej) => {
    const t = db.transaction('media', 'readwrite');
    t.objectStore('media').delete(id);
    t.oncomplete = () => res(); t.onerror = () => rej(t.error);
  });
}

/* ---------- download + image helpers ---------- */
function dlBlob(blob, name) {
  const a = document.createElement('a');
  a.href = URL.createObjectURL(blob); a.download = name; a.click();
  setTimeout(() => URL.revokeObjectURL(a.href), 5000);
}
function dlDataURL(durl, name) {
  const a = document.createElement('a'); a.href = durl; a.download = name; a.click();
}
function loadImg(src) {
  return new Promise((res, rej) => { const i = new Image(); i.onload = () => res(i); i.onerror = rej; i.src = src; });
}
async function stillBlob(src, type) {
  const img = await loadImg(src);
  const c = document.createElement('canvas');
  c.width = img.naturalWidth || 480; c.height = img.naturalHeight || 270;
  c.getContext('2d').drawImage(img, 0, 0);
  return new Promise(r => c.toBlob(r, type, 0.92));
}

/* ---------- real animated-GIF encoder (256-color, looping) ---------- */
function encodeGIF(imageDatas, w, h, fps) {
  const delay = Math.max(2, Math.round(100 / fps));
  const bytes = [];
  const push = (...a) => { for (const x of a) bytes.push(x & 255); };
  const pushArr = arr => { for (let i = 0; i < arr.length; i++) bytes.push(arr[i] & 255); };
  const pushStr = s => { for (let i = 0; i < s.length; i++) push(s.charCodeAt(i)); };
  pushStr('GIF89a');
  push(w & 255, w >> 8, h & 255, h >> 8, 0xF7, 0, 0);
  /* global palette: 6×6×6 cube + 40 grays */
  for (let r = 0; r < 6; r++) for (let g = 0; g < 6; g++) for (let b = 0; b < 6; b++) push(r * 51, g * 51, b * 51);
  for (let i = 0; i < 40; i++) { const v = Math.round(i * 255 / 39); push(v, v, v); }
  /* loop forever */
  push(0x21, 0xFF, 0x0B); pushStr('NETSCAPE2.0'); push(3, 1, 0, 0, 0);
  for (const id of imageDatas) {
    push(0x21, 0xF9, 4, 4, delay & 255, delay >> 8, 0, 0);
    push(0x2C, 0, 0, 0, 0, w & 255, w >> 8, h & 255, h >> 8, 0);
    const d = id.data, n = w * h;
    /* 9-bit "uncompressed" LZW stream with periodic CLEAR */
    push(8);
    let cur = 0, bits = 0, sub = [];
    const emit = code => {
      cur |= code << bits; bits += 9;
      while (bits >= 8) {
        sub.push(cur & 255); cur >>= 8; bits -= 8;
        if (sub.length === 255) { push(255); pushArr(sub); sub = []; }
      }
    };
    emit(256);
    for (let i = 0; i < n; i++) {
      const p = i * 4, gray = Math.abs(d[p] - d[p + 1]) < 12 && Math.abs(d[p + 1] - d[p + 2]) < 12;
      let ix;
      if (gray) ix = 216 + Math.min(39, Math.round((d[p] + d[p + 1] + d[p + 2]) / 3 * 39 / 255));
      else ix = Math.round(d[p] / 51) * 36 + Math.round(d[p + 1] / 51) * 6 + Math.round(d[p + 2] / 51);
      emit(ix);
      if ((i + 1) % 240 === 0) emit(256);
    }
    emit(257);
    if (bits > 0) sub.push(cur & 255);
    if (sub.length) { push(sub.length); pushArr(sub); }
    push(0);
  }
  push(0x3B);
  return new Blob([new Uint8Array(bytes)], { type: 'image/gif' });
}
async function exportClipGIF(frames, name) {
  const imgs = await Promise.all(frames.map(loadImg));
  const w = imgs[0].naturalWidth, h = imgs[0].naturalHeight;
  const c = document.createElement('canvas'); c.width = w; c.height = h;
  const ctx = c.getContext('2d', { willReadFrequently: true });
  const datas = imgs.map(img => { ctx.drawImage(img, 0, 0); return ctx.getImageData(0, 0, w, h); });
  dlBlob(encodeGIF(datas, w, h, 10), name);
}
async function exportClipWebM(frames, name) {
  const imgs = await Promise.all(frames.map(loadImg));
  const w = imgs[0].naturalWidth, h = imgs[0].naturalHeight;
  const c = document.createElement('canvas'); c.width = w; c.height = h;
  const ctx = c.getContext('2d');
  ctx.drawImage(imgs[0], 0, 0);
  const stream = c.captureStream(10);
  const rec = new MediaRecorder(stream, { mimeType: 'video/webm' });
  const chunks = []; rec.ondataavailable = e => e.data.size && chunks.push(e.data);
  const stopped = new Promise(r => { rec.onstop = r; });
  rec.start();
  let i = 0;
  await new Promise(res => {
    const t = setInterval(() => {
      ctx.drawImage(imgs[i % imgs.length], 0, 0); i++;
      if (i >= imgs.length * 2) { clearInterval(t); res(); }
    }, 100);
  });
  rec.stop(); await stopped;
  dlBlob(new Blob(chunks, { type: 'video/webm' }), name);
}

/* ---------- clip loop player (gif-style in-board playback) ---------- */
function ClipPlayer({ frames, fps = 10 }) {
  const [i, setI] = useState(0);
  useEffect(() => {
    const t = setInterval(() => setI(x => (x + 1) % frames.length), 1000 / fps);
    return () => clearInterval(t);
  }, [frames, fps]);
  return <img src={frames[i]} alt="" style={{ width: '100%', display: 'block' }} draggable="false" />;
}

/* ---------- one board card ---------- */
function MoodCard({ item, mobile, onUpdate, onRemove }) {
  const [drag, setDrag] = useState(false);
  const inputRef = useRef(null);
  const slotH = mobile ? Math.round((item.h || 150) * 0.7) : (item.h || 150);
  /* blob-stored media (uploads) get an object URL; dataURL items pass through */
  const src = useMemo(() => (item.blob ? URL.createObjectURL(item.blob) : item.src), [item.id, item.src, item.blob]);
  useEffect(() => () => { if (item.blob && src) URL.revokeObjectURL(src); }, [src]);

  const ingest = file => {
    if (!file || !/^image\//.test(file.type)) { window.toast('Drop an image file — PNG, JPEG, GIF, WebP, AVIF, SVG', 'ImageOff'); return; }
    const rd = new FileReader();
    rd.onload = () => {
      onUpdate({ ...item, kind: 'image', src: rd.result, name: file.name, fmt: (file.type.split('/')[1] || 'img').replace('+xml', '') });
      window.toast(`${file.name} pinned to the board`, 'ImagePlus');
    };
    rd.readAsDataURL(file);
  };
  const exportItem = async which => {
    try {
      if (item.kind === 'image' || item.kind === 'video') {
        if (item.blob) dlBlob(item.blob, item.name || 'media');
        else dlDataURL(item.src, item.name || 'reference');
      }
      else if (item.kind === 'still') {
        const type = which === 'png' ? 'image/png' : 'image/webp';
        dlBlob(await stillBlob(item.src, type), (item.label || 'still').replace(/[^\w-]+/g, '_') + '.' + which);
      } else if (item.kind === 'clip') {
        window.toast(which === 'gif' ? 'Encoding GIF…' : 'Encoding WebM…', 'Loader');
        const nm = (item.label || 'clip').replace(/[^\w-]+/g, '_');
        if (which === 'gif') await exportClipGIF(item.frames, nm + '.gif');
        else await exportClipWebM(item.frames, nm + '.webm');
      }
      window.toast('Export downloaded', 'FileDown');
    } catch (e) { window.toast('Export failed in this browser', 'TriangleAlert'); }
  };

  /* empty slot — universal drop target */
  if (item.kind === 'slot') {
    return (
      <div
        onDragOver={e => { e.preventDefault(); setDrag(true); }}
        onDragLeave={() => setDrag(false)}
        onDrop={e => { e.preventDefault(); setDrag(false); ingest(e.dataTransfer.files[0]); }}
        onClick={() => inputRef.current && inputRef.current.click()}
        className={cx('scan group/card relative cursor-pointer overflow-hidden rounded-xl border transition-colors',
          drag ? 'border-proflow' : 'border-line hover:border-ink-600')}
        style={{ height: slotH, background: item.tone }}>
        <div className="absolute inset-0 bg-gradient-to-t from-black/55 to-transparent"></div>
        <span className="absolute bottom-2.5 left-2.5 z-10 rounded-md bg-black/55 px-2 py-1 font-mono text-[10px] tracking-wide text-zinc-300 backdrop-blur-sm">{item.label}</span>
        <span className={cx('absolute inset-0 flex items-center justify-center text-[11px] font-600 transition-opacity',
          drag ? 'bg-proflow/10 text-proflow-soft opacity-100' : 'text-zinc-400 opacity-0 group-hover/card:opacity-100')}>
          <span className="flex items-center gap-1.5 rounded-md bg-black/55 px-2.5 py-1.5 backdrop-blur-sm"><Icon name="ImagePlus" size={13} /> Drop or browse</span>
        </span>
        <button onClick={e => { e.stopPropagation(); onRemove(); }} title="Remove card"
          className="absolute right-2 top-2 z-10 flex h-6 w-6 items-center justify-center rounded-md bg-black/55 text-zinc-400 opacity-0 backdrop-blur-sm transition-opacity hover:text-white group-hover/card:opacity-100">
          <Icon name="X" size={12} />
        </button>
        <input ref={inputRef} type="file" accept="image/*" className="hidden" onChange={e => ingest(e.target.files[0])} />
      </div>
    );
  }

  /* filled — image / video / still / clip */
  return (
    <div className="group/card relative overflow-hidden rounded-xl border border-line bg-ink-900">
      {item.kind === 'clip'
        ? <ClipPlayer frames={item.frames} fps={item.fps || 10} />
        : item.kind === 'video'
          ? <video src={src} loop muted autoPlay playsInline style={{ width: '100%', display: 'block' }}></video>
          : <img src={src} alt={item.label || item.name || ''} style={{ width: '100%', display: 'block' }} draggable="false" />}
      <span className="absolute left-2 top-2 rounded-md bg-black/60 px-1.5 py-0.5 font-mono text-[9px] font-600 uppercase tracking-[0.08em] text-zinc-200 backdrop-blur-sm">
        {item.kind === 'clip' ? (item.fmt === 'webp' ? 'clip · webp' : 'clip · gif') : item.kind === 'video' ? 'film · ' + (item.fmt || 'video') : (item.fmt || 'img')}
      </span>
      <div className="absolute inset-x-0 bottom-0 flex items-end justify-between gap-2 bg-gradient-to-t from-black/75 to-transparent p-2 opacity-0 transition-opacity group-hover/card:opacity-100">
        <span className="min-w-0 truncate font-mono text-[9.5px] text-zinc-300">{item.label || item.name}</span>
        <div className="flex shrink-0 gap-1">
          {(item.kind === 'image' || item.kind === 'video') && (
            <button onClick={() => exportItem('orig')} title="Download original file"
              className="flex h-6 items-center gap-1 rounded-md bg-black/60 px-1.5 text-[9.5px] font-600 text-zinc-200 backdrop-blur-sm hover:text-white"><Icon name="FileDown" size={11} />File</button>
          )}
          {item.kind === 'still' && ['webp', 'png'].map(f => (
            <button key={f} onClick={() => exportItem(f)} title={'Export ' + f.toUpperCase()}
              className="flex h-6 items-center gap-1 rounded-md bg-black/60 px-1.5 text-[9.5px] font-600 uppercase text-zinc-200 backdrop-blur-sm hover:text-white">{f}</button>
          ))}
          {item.kind === 'clip' && ['gif', 'webm'].map(f => (
            <button key={f} onClick={() => exportItem(f)} title={'Export ' + f.toUpperCase()}
              className="flex h-6 items-center gap-1 rounded-md bg-black/60 px-1.5 text-[9.5px] font-600 uppercase text-zinc-200 backdrop-blur-sm hover:text-white">{f}</button>
          ))}
          <button onClick={onRemove} title="Remove"
            className="flex h-6 w-6 items-center justify-center rounded-md bg-black/60 text-zinc-300 backdrop-blur-sm hover:text-[#FF4D6D]"><Icon name="Trash2" size={11} /></button>
        </div>
      </div>
    </div>
  );
}

/* ============================================================
   LINK CAPTURE STUDIO
   ============================================================ */
function mlDetect(url) {
  if (/youtu\.be|youtube\.com/i.test(url)) return { name: 'YouTube', icon: 'Youtube', color: '#F43F3E' };
  if (/vimeo\.com/i.test(url)) return { name: 'Vimeo', icon: 'Clapperboard', color: '#F9A06E' };
  if (/instagram\.com/i.test(url)) return { name: 'Instagram', icon: 'Instagram', color: '#FF5D92' };
  return { name: 'Web link', icon: 'Globe', color: '#C9CDD5' };
}
const ML_TC = s => `${String(Math.floor(s / 60)).padStart(2, '0')}:${String(Math.floor(s % 60)).padStart(2, '0')}`;
const ML_SUNSET = ['#EFDD58', '#F57F45', '#BD2555', '#5B0952', '#FF1F6B'];

/* deterministic synthetic frame — same fn drives preview AND capture */
function mlDraw(ctx, w, h, t, seed, meta, url) {
  const rnd = k => { const x = Math.sin(seed * 12.9898 + k * 78.233) * 43758.5453; return x - Math.floor(x); };
  ctx.fillStyle = '#0B0E1A'; ctx.fillRect(0, 0, w, h);
  for (let k = 0; k < 4; k++) {
    const sp = 0.18 + rnd(k) * 0.3;
    const x = w * (0.5 + 0.42 * Math.sin(t * sp + rnd(k + 9) * 6.28));
    const y = h * (0.5 + 0.36 * Math.cos(t * sp * 0.8 + rnd(k + 4) * 6.28));
    const r = h * (0.35 + rnd(k + 7) * 0.4);
    const g = ctx.createRadialGradient(x, y, 0, x, y, r);
    g.addColorStop(0, ML_SUNSET[(k + Math.floor(seed)) % ML_SUNSET.length] + '66');
    g.addColorStop(1, 'transparent');
    ctx.fillStyle = g; ctx.fillRect(0, 0, w, h);
  }
  /* sweeping light */
  const sx = ((t * 0.12) % 1.4 - 0.2) * w;
  const lg = ctx.createLinearGradient(sx, 0, sx + w * 0.25, h);
  lg.addColorStop(0, 'transparent'); lg.addColorStop(0.5, 'rgba(238,241,248,0.08)'); lg.addColorStop(1, 'transparent');
  ctx.fillStyle = lg; ctx.fillRect(0, 0, w, h);
  /* letterbox + meta */
  ctx.fillStyle = 'rgba(4,6,12,0.85)';
  ctx.fillRect(0, 0, w, h * 0.085); ctx.fillRect(0, h * 0.915, w, h * 0.085);
  ctx.fillStyle = 'rgba(238,241,248,0.9)';
  ctx.font = `700 ${Math.round(h * 0.052)}px Barlow, sans-serif`;
  ctx.fillText(meta.name.toUpperCase() + ' · LINKED MEDIA', w * 0.03, h * 0.062);
  ctx.fillStyle = 'rgba(238,241,248,0.55)';
  ctx.font = `${Math.round(h * 0.045)}px "JetBrains Mono", monospace`;
  const u = url.length > 52 ? url.slice(0, 52) + '…' : url;
  ctx.fillText(u, w * 0.03, h * 0.972);
  ctx.textAlign = 'right';
  ctx.fillStyle = 'rgba(238,241,248,0.85)';
  ctx.fillText(ML_TC(t), w * 0.97, h * 0.972);
  ctx.fillStyle = 'rgba(238,241,248,0.35)';
  ctx.font = `${Math.round(h * 0.038)}px "JetBrains Mono", monospace`;
  ctx.fillText('SIMULATED PREVIEW', w * 0.97, h * 0.062);
  ctx.textAlign = 'left';
}

function CaptureStudio({ mobile, onSave }) {
  const DUR = 24;
  const [url, setUrl] = useState('');
  const [meta, setMeta] = useState(null);
  const [loading, setLoading] = useState(false);
  const [playing, setPlaying] = useState(false);
  const [t, setT] = useState(0);
  const [inT, setInT] = useState(2);
  const [outT, setOutT] = useState(5);
  const [fmt, setFmt] = useState('gif');
  const [busy, setBusy] = useState(false);
  const cvs = useRef(null);
  const tRef = useRef(0);
  const seed = useMemo(() => { let s = 0; for (let i = 0; i < url.length; i++) s = (s * 31 + url.charCodeAt(i)) % 997; return s + 1; }, [url]);

  const draw = tt => { const c = cvs.current; if (!c) return; mlDraw(c.getContext('2d'), c.width, c.height, tt, seed, meta || { name: '' }, url); };
  useEffect(() => { if (meta) draw(tRef.current); });
  useEffect(() => {
    if (!playing || !meta) return;
    const iv = setInterval(() => {
      tRef.current = (tRef.current + 0.05) % DUR;
      draw(tRef.current);
      setT(tRef.current);
    }, 50);
    return () => clearInterval(iv);
  }, [playing, meta, seed]);

  const load = () => {
    if (!/^https?:\/\/|\w+\.\w+/.test(url.trim())) { window.toast('Paste a YouTube, Vimeo or Instagram link', 'Link'); return; }
    setLoading(true); setMeta(null);
    setTimeout(() => {
      setLoading(false);
      setMeta(mlDetect(url));
      tRef.current = 0; setT(0); setPlaying(true);
      window.toast('Media resolved — scrub and capture', 'MonitorPlay');
    }, 1100);
  };
  const seek = v => { tRef.current = v; setT(v); draw(v); };
  const renderAt = tt => {
    const c = document.createElement('canvas'); c.width = 480; c.height = 270;
    mlDraw(c.getContext('2d'), 480, 270, tt, seed, meta, url);
    return c;
  };
  const capFrame = () => {
    const src = renderAt(tRef.current).toDataURL('image/jpeg', 0.88);
    onSave({ id: 'cap-' + Date.now(), kind: 'still', src, label: `${meta.name} still · ${ML_TC(tRef.current)}`, from: url });
    window.toast('Frame captured to the moodboard', 'Camera');
  };
  const capClip = async () => {
    const a = Math.min(inT, outT), b = Math.max(inT, outT);
    const len = Math.min(4, Math.max(0.5, b - a));
    setBusy(true);
    await new Promise(r => setTimeout(r, 30));
    const fps = 10, n = Math.round(len * fps), frames = [];
    for (let i = 0; i < n; i++) frames.push(renderAt(a + i / fps).toDataURL('image/jpeg', 0.72));
    onSave({ id: 'cap-' + Date.now(), kind: 'clip', frames, fps, fmt, label: `${meta.name} clip · ${len.toFixed(1)}s`, from: url });
    setBusy(false);
    window.toast(`${len.toFixed(1)}s ${fmt.toUpperCase()} clip saved to the moodboard`, 'Film');
  };

  return (
    <div>
      {/* url row */}
      <div className="flex items-center gap-2">
        <div className="flex min-w-0 flex-1 items-center gap-2 rounded-xl border border-line bg-ink-900 px-3 py-2.5">
          <Icon name="Link" size={14} className="shrink-0 text-zinc-500" />
          <input value={url} onChange={e => setUrl(e.target.value)} onKeyDown={e => e.key === 'Enter' && load()}
            placeholder="Paste a YouTube, Vimeo or Instagram link…"
            className="min-w-0 flex-1 bg-transparent text-[13px] text-zinc-200 outline-none placeholder:text-zinc-600" />
        </div>
        <button onClick={load} className="flex shrink-0 items-center gap-2 rounded-xl bg-cta px-3.5 py-2.5 text-[12.5px] font-700 text-cta-ink hover:bg-cta-hover">
          <Icon name="MonitorPlay" size={14} /> Load
        </button>
      </div>
      {/* sample links */}
      {!meta && !loading && (
        <div className="mt-2.5 flex flex-wrap items-center gap-1.5">
          <span className="text-[11px] text-zinc-600">Try:</span>
          {[['youtube.com/watch?v=aerial-dawn', 'Youtube'], ['vimeo.com/823014. ref-flares', 'Clapperboard'], ['instagram.com/p/flightsuit-test', 'Instagram']].map(([u, ic]) => (
            <button key={u} onClick={() => setUrl('https://' + u.replace(/\s/g, ''))}
              className="flex items-center gap-1.5 rounded-full border border-line px-2.5 py-1 text-[11px] font-600 text-zinc-400 transition-colors hover:border-ink-600 hover:text-zinc-200">
              <Icon name={ic} size={11} />{u.split('/')[0]}
            </button>
          ))}
        </div>
      )}
      {loading && (
        <div className="scan-anim mt-3 flex items-center gap-2 rounded-xl border border-line bg-ink-900 px-3 py-3 text-[12.5px] text-zinc-400">
          <Icon name="Loader" size={14} className="text-proflow-soft" /> Resolving media from {mlDetect(url).name}…
        </div>
      )}

      {meta && (
        <div className="fade-up mt-3">
          {/* player */}
          <div className="relative overflow-hidden rounded-xl border border-line bg-black">
            <canvas ref={cvs} width="480" height="270" style={{ width: '100%', display: 'block' }}
              onClick={() => setPlaying(p => !p)} className="cursor-pointer"></canvas>
            <span className="absolute left-2.5 top-2.5 flex items-center gap-1.5 rounded-md bg-black/60 px-2 py-1 text-[10px] font-700 backdrop-blur-sm" style={{ color: meta.color }}>
              <Icon name={meta.icon} size={12} />{meta.name}
            </span>
          </div>
          {/* transport */}
          <div className="mt-2.5 flex items-center gap-2.5">
            <button onClick={() => setPlaying(p => !p)}
              className="flex h-9 w-9 shrink-0 items-center justify-center rounded-lg bg-cta text-cta-ink hover:bg-cta-hover">
              <Icon name={playing ? 'Pause' : 'Play'} size={15} />
            </button>
            <span className="w-[44px] shrink-0 font-mono text-[11px] text-zinc-300">{ML_TC(t)}</span>
            <div className="relative min-w-0 flex-1">
              <input type="range" min="0" max={DUR} step="0.05" value={t} onChange={e => seek(+e.target.value)}
                className="w-full accent-proflow" />
              <div className="pointer-events-none absolute left-0 right-0 top-[-3px] h-1 overflow-visible">
                <div className="absolute h-1 rounded-full bg-proflow/50"
                  style={{ left: (Math.min(inT, outT) / DUR * 100) + '%', width: (Math.abs(outT - inT) / DUR * 100) + '%' }}></div>
              </div>
            </div>
            <span className="w-[44px] shrink-0 text-right font-mono text-[11px] text-zinc-500">{ML_TC(DUR)}</span>
          </div>
          {/* in / out + actions */}
          <div className={cx('mt-3 grid gap-2', mobile ? 'grid-cols-1' : 'grid-cols-2')}>
            <div className="flex items-center gap-2 rounded-xl border border-line bg-ink-900 p-2">
              <button onClick={() => setInT(tRef.current)} className="flex items-center gap-1.5 rounded-lg border border-line px-2.5 py-2 text-[11.5px] font-600 text-zinc-300 hover:border-ink-600">
                <Icon name="ArrowRightToLine" size={12} /> In {ML_TC(inT)}
              </button>
              <button onClick={() => setOutT(tRef.current)} className="flex items-center gap-1.5 rounded-lg border border-line px-2.5 py-2 text-[11.5px] font-600 text-zinc-300 hover:border-ink-600">
                <Icon name="ArrowLeftToLine" size={12} /> Out {ML_TC(outT)}
              </button>
              <span className="ml-auto pr-1 font-mono text-[11px] text-proflow-soft">{Math.min(4, Math.max(0.5, Math.abs(outT - inT))).toFixed(1)}s</span>
            </div>
            <div className="flex items-center justify-end gap-2">
              <div className="flex items-center rounded-lg border border-line bg-ink-900 p-0.5">
                {['gif', 'webp'].map(f => (
                  <button key={f} onClick={() => setFmt(f)}
                    className={cx('rounded-[7px] px-2.5 py-1.5 font-display text-[11px] uppercase transition-colors',
                      fmt === f ? 'bg-ink-700 text-head' : 'text-zinc-500 hover:text-zinc-300')}>{f}</button>
                ))}
              </div>
              <button onClick={capClip} disabled={busy}
                className="flex items-center gap-1.5 rounded-xl bg-cta px-3 py-2.5 text-[12px] font-700 text-cta-ink hover:bg-cta-hover disabled:opacity-50">
                <Icon name="Film" size={13} /> {busy ? 'Cutting…' : 'Cut clip'}
              </button>
              <button onClick={capFrame}
                className="flex items-center gap-1.5 rounded-xl border border-line px-3 py-2.5 text-[12px] font-600 text-zinc-300 hover:border-ink-600">
                <Icon name="Camera" size={13} /> Frame
              </button>
            </div>
          </div>
          <p className="mt-2.5 text-[11px] leading-relaxed text-zinc-600">
            The preview is simulated in this prototype — the capture pipeline is real: frames and clips land on the board,
            save to the project media database, and export as actual GIF / WebM / WebP / PNG files.
          </p>
        </div>
      )}
    </div>
  );
}

/* ============================================================
   MOODLAB — the board panel (capture lives in Tools → Link Capture)
   ============================================================ */
/* ---- treatment structure: references curated by craft ---- */
const MOOD_SECTIONS = [
  { id:'tone',     no:'01', title:'Tone & Atmosphere', kicker:'The feeling', note:'Restraint over spectacle. Crimson dawn, long shadows, the held breath before flight.' },
  { id:'light',    no:'02', title:'Light',             kicker:'How it’s lit', note:'Hard backlight and a low, raking sun. Negative fill carves the steel; practicals do the talking.' },
  { id:'lens',     no:'03', title:'Lens & Texture',    kicker:'The glass',   note:'Anamorphic, open gate. Gentle horizontal flares, fine grain, the romance of older glass.' },
  { id:'design',   no:'04', title:'Wardrobe & Design', kicker:'In frame',    note:'Brushed steel, worn leather, instrument brass. Every surface authentic to the cockpit.' },
  { id:'location', no:'05', title:'Location',          kicker:'The world',   note:'Hangar 7 at first light — wide tarmac, deep perspective, weather as a character.' },
];
const MOOD_SEED = [
  { section:'tone',     label:'Aerial · crimson dawn', h:240, tone:'#2a2f44' },
  { section:'tone',     label:'Held breath · pre-flight', h:168, tone:'#3a2f44' },
  { section:'tone',     label:'Long shadow', h:150, tone:'#4a2a2f' },
  { section:'light',    label:'Hard backlight', h:188, tone:'#443a2a' },
  { section:'light',    label:'Low sun rake', h:150, tone:'#34402f' },
  { section:'light',    label:'Practical glow', h:150, tone:'#3a3320' },
  { section:'lens',     label:'Anamorphic flare', h:200, tone:'#3a2f44' },
  { section:'lens',     label:'Fine grain', h:140, tone:'#33333a' },
  { section:'lens',     label:'Shallow rack focus', h:158, tone:'#2a3340' },
  { section:'design',   label:'Flight suit · hero', h:240, tone:'#2f3a44' },
  { section:'design',   label:'Vintage altimeter', h:150, tone:'#2a3a3f' },
  { section:'design',   label:'Brushed steel', h:170, tone:'#33333a' },
  { section:'location', label:'Hangar 7 · first light', h:200, tone:'#2a2f44' },
  { section:'location', label:'Wide tarmac', h:150, tone:'#34402f' },
];
const MOOD_PALETTE_DEFAULT = [
  { name:'Crimson Dawn', hex:'#BD2555' },
  { name:'Ember', hex:'#F57F45' },
  { name:'Gold Hour', hex:'#EFC65B' },
  { name:'Brushed Steel', hex:'#8A95A5' },
  { name:'Hangar Shadow', hex:'#2A2F3F' },
  { name:'Ink Night', hex:'#0B0E16' },
];
const MOOD_PK = (window.PROJ_ID || 'crimson');
const mlGet = (k, d) => window.SLStore.get('silverlines.mood.' + MOOD_PK + '.' + k, d);
const mlSet = (k, v) => { window.SLStore.set('silverlines.mood.' + MOOD_PK + '.' + k, v); };

/* ---- color & grade palette: editable swatches ---- */
function PaletteStrip() {
  const [pal, setPal] = useState(() => mlGet('palette', MOOD_PALETTE_DEFAULT));
  const save = next => { setPal(next); mlSet('palette', next); };
  const setSwatch = (i, p) => save(pal.map((s, k) => k === i ? { ...s, ...p } : s));
  const del = i => save(pal.filter((_, k) => k !== i));
  const add = () => save([...pal, { name: 'New tone', hex: '#6B7280' }]);
  return (
    <div className="overflow-hidden rounded-2xl border border-line bg-ink-900">
      <div className="flex items-center justify-between border-b border-line px-4 py-3">
        <div className="flex items-center gap-2">
          <span className="font-mono text-[10px] uppercase tracking-[0.18em] text-zinc-500">Color &amp; Grade</span>
          <span className="font-display text-[13px] italic text-head">The palette this film lives in</span>
        </div>
        <button onClick={add} className="flex items-center gap-1 rounded-lg border border-line px-2 py-1 text-[11px] font-600 text-zinc-400 transition-colors hover:border-ink-600 hover:text-zinc-200"><Icon name="Plus" size={12} /> Tone</button>
      </div>
      {/* gradient ribbon */}
      <div className="h-2 w-full" style={{ background: `linear-gradient(90deg, ${pal.map(s => s.hex).join(', ')})` }}></div>
      <div className="grid" style={{ gridTemplateColumns: `repeat(${pal.length}, minmax(0,1fr))` }}>
        {pal.map((s, i) => (
          <div key={i} className="group/sw relative">
            <label className="block h-24 cursor-pointer" style={{ background: s.hex }} title="Click to change the color">
              <input type="color" value={s.hex} onChange={e => setSwatch(i, { hex: e.target.value })} className="sr-only" />
            </label>
            <button onClick={() => del(i)} title="Remove tone"
              className="absolute right-1.5 top-1.5 flex h-5 w-5 items-center justify-center rounded-md bg-black/45 text-white/80 opacity-0 backdrop-blur-sm transition-opacity hover:bg-black/70 group-hover/sw:opacity-100"><Icon name="X" size={11} /></button>
            <div className="border-t border-line px-2.5 py-2">
              <Edit value={s.name} onChange={v => setSwatch(i, { name: v })} className="text-[11.5px] font-600 text-zinc-200" />
              <div className="font-mono text-[10px] uppercase tracking-wide text-zinc-500">{s.hex}</div>
            </div>
          </div>
        ))}
      </div>
    </div>
  );
}

/* ---- one curated section of references — folds shut until filled ---- */
function MoodSection({ sec, items, mobile, wide, onUpsert, onRemove, onAdd }) {
  const [note, setNote] = useState(() => mlGet('note.' + sec.id, sec.note));
  const saveNote = v => { setNote(v); mlSet('note.' + sec.id, v); };
  const filled = items.filter(i => i.kind !== 'slot').length;
  const total = items.length;
  const [open, setOpen] = useState(filled > 0);
  const prev = useRef(filled);
  useEffect(() => { if (filled > prev.current) setOpen(true); prev.current = filled; }, [filled]);

  const addToSection = () => { setOpen(true); onAdd(sec.id); };

  return (
    <section className="border-t border-line">
      {/* foldable header */}
      <button onClick={() => setOpen(o => !o)} className="group/sec flex w-full items-center gap-4 py-5 text-left">
        <Icon name="ChevronRight" size={18}
          className={cx('shrink-0 text-zinc-600 transition-all duration-200 group-hover/sec:text-electric-soft', open && 'rotate-90')} />
        <span className="font-display text-[30px] font-800 italic leading-none text-ink-600 transition-colors group-hover/sec:text-zinc-500" style={{ WebkitTextStroke: '1px rgb(var(--line))' }}>{sec.no}</span>
        <div className="min-w-0">
          <div className="font-mono text-[10px] uppercase tracking-[0.18em] text-zinc-600 transition-colors group-hover/sec:text-electric-soft">{sec.kicker}</div>
          <h3 className="font-display text-[20px] font-700 text-zinc-400 transition-colors group-hover/sec:text-head">{sec.title}</h3>
        </div>
        <span className={cx('ml-auto shrink-0 rounded-full border px-2.5 py-1 font-mono text-[10px] transition-colors',
          filled > 0 ? 'border-electric/40 bg-electric/10 text-electric-soft' : 'border-line text-zinc-600 group-hover/sec:text-zinc-400')}>
          {filled > 0 ? `${filled} pinned` : open ? 'empty' : `${total} slot${total === 1 ? '' : 's'} · add`}
        </span>
      </button>

      {/* foldable body */}
      {open && (
        <div className="fade-up pb-7">
          <div className="mb-4 flex items-start gap-2 text-[12.5px] italic leading-relaxed text-zinc-400 md:max-w-[560px]">
            <span className="mt-0.5 select-none font-display text-[20px] not-italic leading-none text-ink-600">“</span>
            <Edit value={note} onChange={saveNote} multiline className="text-[12.5px] italic text-zinc-400" />
          </div>
          <div className={cx('gap-3', mobile ? 'columns-2' : wide ? 'columns-3' : 'columns-2')} style={{ columnGap: '12px' }}>
            {items.map(it => (
              <div key={it.id} className="mb-3 break-inside-avoid">
                <MoodCard item={it} mobile={mobile} onUpdate={onUpsert} onRemove={() => onRemove(it.id)} />
              </div>
            ))}
            <button onClick={addToSection}
              className="mb-3 flex h-[84px] w-full break-inside-avoid items-center justify-center gap-2 rounded-xl border border-dashed border-ink-600 text-[12px] font-600 text-zinc-500 transition-colors hover:border-electric/50 hover:text-electric-soft">
              <Icon name="Plus" size={15} /> Pin reference
            </button>
          </div>
        </div>
      )}
    </section>
  );
}

function MoodLab({ mobile, wide }) {
  const { setModule } = useContext(AccessContext);
  const [items, setItems] = useState(null);
  const [statement, setStatement] = useState(() => mlGet('statement',
    'One pilot. One dawn. The pinnacle of private flight — told with restraint, shot like a love letter to the machine.'));
  const rootRef = useRef(null);
  const [full, setFull] = useState(false);

  useEffect(() => {
    (async () => {
      try {
        let list = await mlAll();
        /* one-time migration to the curated, sectioned structure */
        if (!mlGet('seededV2', false)) {
          /* drop obsolete empty placeholder slots from the pre-section board */
          for (const it of list) {
            if ((/^seed-/.test(it.id) || /^pin-/.test(it.id)) && it.kind === 'slot') await mlDel(it.id);
          }
          list = list.filter(it => !((/^seed-/.test(it.id) || /^pin-/.test(it.id)) && it.kind === 'slot'));
          /* lay down each craft section's starter slots */
          const base = list.length ? Math.max(...list.map(x => x.ord || 0)) + 1 : 0;
          const seeds = MOOD_SEED.map((m, i) => ({ id: 'mood-' + m.section + '-' + i, ord: base + i, kind: 'slot', section: m.section, label: m.label, h: m.h, tone: m.tone }));
          for (const it of seeds) await mlPut(it);
          list = [...list, ...seeds];
          mlSet('seededV2', true);
        }
        setItems(list.sort((a, b) => (a.ord || 0) - (b.ord || 0)));
      } catch (e) { setItems([]); }
    })();
  }, []);
  useEffect(() => {
    const h = () => setFull(!!document.fullscreenElement);
    document.addEventListener('fullscreenchange', h);
    return () => document.removeEventListener('fullscreenchange', h);
  }, []);

  const upsert = it => {
    mlPut(it).catch(() => window.toast('Could not save to the media database', 'TriangleAlert'));
    setItems(l => {
      const i = (l || []).findIndex(x => x.id === it.id);
      if (i < 0) return [...(l || []), it];
      const c = [...l]; c[i] = it; return c;
    });
  };
  const remove = id => { mlDel(id).catch(() => {}); setItems(l => l.filter(x => x.id !== id)); };
  const nextOrd = () => (items && items.length ? Math.max(...items.map(x => x.ord || 0)) + 1 : 0);
  const addSlot = section => upsert({ id: 'pin-' + Date.now(), ord: nextOrd(), section, label: 'New reference', h: 150, tone: '#232040', kind: 'slot' });
  const saveStatement = v => { setStatement(v); mlSet('statement', v); };
  const present = () => {
    const el = rootRef.current; if (!el) return;
    if (document.fullscreenElement) document.exitFullscreen();
    else if (el.requestFullscreen) el.requestFullscreen();
    else window.toast('Fullscreen not available in this browser', 'Monitor');
  };

  const credit = role => (PEOPLE.find(p => p.role === role) || {});
  const pinned = items ? items.filter(i => i.kind !== 'slot').length : 0;
  const loose = items ? items.filter(i => !i.section) : [];

  return (
    <div ref={rootRef} className={cx('mood-root', full && 'is-full')}>
      {/* ===== treatment masthead ===== */}
      <div className="relative overflow-hidden rounded-2xl border border-line bg-gradient-to-br from-ink-900 to-ink-850">
        <div className="pointer-events-none absolute inset-0 opacity-[0.5]"
          style={{ background: 'radial-gradient(120% 120% at 85% -10%, rgba(189,37,85,0.28), transparent 55%), radial-gradient(90% 90% at 10% 120%, rgba(245,127,69,0.18), transparent 55%)' }}></div>
        <div className="relative p-6 md:p-8">
          <div className="flex flex-wrap items-center gap-3">
            <span className="font-mono text-[10px] uppercase tracking-[0.22em] text-electric-soft">Visual Treatment</span>
            <span className="h-1 w-1 rounded-full bg-zinc-600"></span>
            <span className="font-mono text-[10px] uppercase tracking-[0.18em] text-zinc-500">{PROJECT.code} · {PROJECT.client}</span>
          </div>
          <h2 className="mt-3 font-display text-[40px] font-800 italic leading-[0.95] text-head md:text-[56px]">{PROJECT.name}</h2>
          <div className="mt-4 max-w-[680px] border-l-2 border-electric/50 pl-4 font-display text-[16px] italic leading-relaxed text-zinc-300 md:text-[18px]">
            <Edit value={statement} onChange={saveStatement} multiline className="font-display text-[16px] italic text-zinc-300 md:text-[18px]" />
          </div>
          {/* credits + actions */}
          <div className="mt-6 flex flex-wrap items-end justify-between gap-4">
            <div className="flex flex-wrap gap-x-8 gap-y-3">
              {[['Director', credit('Director')], ['Cinematography', credit('Director of Photography')], ['Produced by', credit('Producer')], ['For', { name: PROJECT.client }]].map(([role, p]) => (
                <div key={role} className="flex items-center gap-2.5">
                  {p.name && p.color ? <Avatar name={p.name} color={p.color} size={30} ring /> : <span className="flex h-[30px] w-[30px] items-center justify-center rounded-full border border-line text-zinc-500"><Icon name="Building2" size={14} /></span>}
                  <div>
                    <div className="font-mono text-[9px] uppercase tracking-[0.16em] text-zinc-500">{role}</div>
                    <div className="text-[12.5px] font-600 text-zinc-100">{p.name || '—'}</div>
                  </div>
                </div>
              ))}
            </div>
            <div className="flex items-center gap-2">
              <Chip tone="neutral">{pinned} references</Chip>
              <button onClick={() => setModule && setModule('linkcapture')}
                className="flex items-center gap-1.5 rounded-lg border border-line bg-ink-900/70 px-3 py-2 font-display text-[12px] text-zinc-300 transition-colors hover:border-ink-600 hover:text-head">
                <Icon name="MonitorPlay" size={13} /> Capture
              </button>
              <button onClick={present}
                className="flex items-center gap-1.5 rounded-lg bg-cta px-3 py-2 font-display text-[12px] font-700 text-cta-ink hover:bg-cta-hover">
                <Icon name={full ? 'Minimize2' : 'Presentation'} size={13} /> {full ? 'Exit' : 'Present'}
              </button>
            </div>
          </div>
        </div>
      </div>

      {/* ===== color & grade ===== */}
      <div className="mt-5"><PaletteStrip /></div>

      {/* ===== curated sections ===== */}
      {items === null
        ? <div className="py-16 text-center text-[12px] text-zinc-600">Loading references…</div>
        : <div className="mt-9 space-y-8">
            {MOOD_SECTIONS.map(sec => (
              <MoodSection key={sec.id} sec={sec} mobile={mobile} wide={wide}
                items={items.filter(i => i.section === sec.id)}
                onUpsert={upsert} onRemove={remove} onAdd={addSlot} />
            ))}

            {loose.length > 0 && (
              <section className="border-t border-line pt-7">
                <div className="mb-4 flex items-start gap-4">
                  <span className="font-display text-[34px] font-800 italic leading-none text-ink-600" style={{ WebkitTextStroke: '1px rgb(var(--line))' }}>+</span>
                  <div>
                    <div className="font-mono text-[10px] uppercase tracking-[0.18em] text-electric-soft">From the project</div>
                    <h3 className="font-display text-[20px] font-700 text-head">Pinned &amp; Captured</h3>
                    <p className="mt-0.5 text-[12px] text-zinc-500">Imports and link captures land here — drag into a section as you curate.</p>
                  </div>
                </div>
                <div className={cx('gap-3', mobile ? 'columns-2' : wide ? 'columns-4' : 'columns-3')} style={{ columnGap: '12px' }}>
                  {loose.map(it => (
                    <div key={it.id} className="mb-3 break-inside-avoid">
                      <MoodCard item={it} mobile={mobile} onUpdate={upsert} onRemove={() => remove(it.id)} />
                    </div>
                  ))}
                </div>
              </section>
            )}

            {/* footer credit */}
            <div className="flex flex-col items-center justify-between gap-2 border-t border-line pt-6 text-center sm:flex-row sm:text-left">
              <p className="text-[11px] leading-relaxed text-zinc-600">Cards accept any image — PNG, JPEG, GIF, WebP, AVIF, SVG. Animated formats keep their motion. Captures from Tools → Link Capture appear above.</p>
              <p className="shrink-0 font-mono text-[10px] uppercase tracking-[0.16em] text-zinc-600">Prepared on Silverlines · {PROJECT.code}</p>
            </div>
          </div>}
    </div>
  );
}
window.MoodLab = MoodLab;

/* ============================================================
   TOOLS — LINK CAPTURE page (future tools dock here too)
   ============================================================ */
function LinkCaptureModule({ mobile }) {
  const [all, setAll] = useState([]);
  const [dragDB, setDragDB] = useState(false);
  const upRef = useRef(null);
  const refresh = async () => {
    try { setAll((await mlAll()).sort((a, b) => (b.ord || 0) - (a.ord || 0))); } catch (e) {}
  };
  useEffect(() => { refresh(); }, []);
  const nextOrd = list => (list.length ? Math.max(...list.map(x => x.ord || 0)) + 1 : 0);
  const save = async it => {
    try {
      const cur = await mlAll().catch(() => []);
      await mlPut({ ...it, ord: nextOrd(cur) });
    } catch (e) {}
    refresh();
  };
  const remove = id => { mlDel(id).catch(() => {}); setAll(l => l.filter(x => x.id !== id)); };
  const ingestFiles = async files => {
    const list = [...files]; let added = 0;
    let cur = await mlAll().catch(() => []);
    let ord = nextOrd(cur);
    for (const f of list) {
      const isImg = /^image\//.test(f.type), isVid = /^video\//.test(f.type);
      if (!isImg && !isVid) { window.toast(`${f.name} skipped — not an image or film`, 'FileX'); continue; }
      if (f.size > 120 * 1024 * 1024) { window.toast(`${f.name} skipped — over 120 MB`, 'FileX'); continue; }
      await mlPut({
        id: 'up-' + Date.now() + '-' + Math.random().toString(36).slice(2, 6),
        ord: ord++, kind: isVid ? 'video' : 'image', blob: f, name: f.name,
        fmt: (f.type.split('/')[1] || '').replace('+xml', '').replace('quicktime', 'mov'),
        label: f.name,
      }).catch(() => window.toast('Could not save to the media database', 'TriangleAlert'));
      added++;
    }
    if (added) {
      window.toast(`${added} file${added === 1 ? '' : 's'} imported to project media`, 'DatabaseZap');
      window.notify && window.notify(`${added} file${added === 1 ? '' : 's'} imported to project media`, 'Database', 'linkcapture');
    }
    refresh();
  };
  const captures = all.filter(i => i.kind === 'still' || i.kind === 'clip');
  const media = all.filter(i => i.kind !== 'slot');
  return (
    <div className="fade-up">
      <SectionTitle kicker="Tools" icon="MonitorPlay" title="Link Capture"
        action={<Chip tone="proflow"><Icon name="Film" size={12} /> Saves to project media</Chip>} />
      <div className={cx('grid items-start gap-5', mobile ? 'grid-cols-1' : 'grid-cols-12')}>
        <Panel className={cx(mobile ? '' : 'col-span-7')}>
          <h3 className="mb-3 font-display text-[15px] text-head">Source</h3>
          <CaptureStudio mobile={mobile} onSave={save} />
        </Panel>
        <Panel className={cx(mobile ? '' : 'col-span-5')}>
          <div className="mb-3 flex items-center gap-2">
            <h3 className="font-display text-[15px] text-head">Captured media</h3>
            <Chip tone="neutral">{captures.length}</Chip>
          </div>
          {captures.length === 0
            ? <div className="scan flex h-[160px] items-center justify-center rounded-xl border border-line text-[12px] text-zinc-500">Frames and clips you capture appear here</div>
            : <div className="columns-2 gap-3" style={{ columnGap: '12px' }}>
                {captures.map(it => (
                  <div key={it.id} className="mb-3 break-inside-avoid">
                    <MoodCard item={it} mobile={mobile} onUpdate={v => mlPut(v).catch(() => {})} onRemove={() => remove(it.id)} />
                  </div>
                ))}
              </div>}

          {/* project media database */}
          <div className="mt-5 border-t border-line pt-4"
            onDragOver={e => { e.preventDefault(); setDragDB(true); }}
            onDragLeave={() => setDragDB(false)}
            onDrop={e => { e.preventDefault(); setDragDB(false); ingestFiles(e.dataTransfer.files); }}>
            <div className="mb-3 flex flex-wrap items-center justify-between gap-2">
              <div className="flex items-center gap-2">
                <Icon name="Database" size={15} className="text-proflow-soft" />
                <h3 className="font-display text-[15px] text-head">Project Media Database</h3>
                <Chip tone="neutral">{media.length} items</Chip>
              </div>
              <button onClick={() => upRef.current && upRef.current.click()}
                className="flex items-center gap-1.5 rounded-xl bg-cta px-3 py-2 text-[12px] font-700 text-cta-ink hover:bg-cta-hover">
                <Icon name="Upload" size={13} /> Upload media
              </button>
              <input ref={upRef} type="file" multiple accept="image/*,video/*" className="hidden"
                onChange={e => { ingestFiles(e.target.files); e.target.value = ''; }} />
            </div>
            {media.length === 0
              ? <div className={cx('scan flex h-[120px] items-center justify-center rounded-xl border text-[12px] text-zinc-500 transition-colors',
                  dragDB ? 'border-proflow bg-proflow/5' : 'border-line')}>Drop images, GIFs or films — or use Upload</div>
              : <div className={cx('gap-3 rounded-xl transition-colors', mobile ? 'columns-2' : 'columns-3', dragDB && 'outline outline-2 outline-proflow/60')} style={{ columnGap: '12px' }}>
                  {media.map(it => (
                    <div key={it.id} className="mb-3 break-inside-avoid">
                      <MoodCard item={it} mobile={mobile} onUpdate={v => mlPut(v).catch(() => {})} onRemove={() => remove(it.id)} />
                    </div>
                  ))}
                </div>}
            <p className="mt-1 text-[11px] text-zinc-600">Every image, GIF, film, frame and clip in the project — imports land on the Development moodboard too.</p>
          </div>
        </Panel>
      </div>
    </div>
  );
}
window.LinkCaptureModule = LinkCaptureModule;
