/* ===========================================================================
   iRaven App Store Studio — per-app Storage / asset library.
   Every uploaded file, every AI-generated asset and every export is kept in
   one place, per app, grouped by source (Uploads · AI · Exports). Blobs live
   in IndexedDB (shared store, reused from the video engine); the lightweight
   index lives in the project state. A media resolver turns storage refs into
   displayable object URLs, with a sync cache + prewarm so exports stay fast.

   A media value anywhere in the app is one of:
     • a string            → asset path ('assets/…') or a data: URL  (legacy)
     • {kind:'image'|'video', key, name}                            (storage)
   Audio refs are {key, name} (matches preview.audio).
   =========================================================================== */

/* ---------- object-URL cache + resolver ---------- */
const _urlCache = {};
const _warming = {};
const _warmListeners = new Set();
function _notifyWarm() { _warmListeners.forEach(fn => { try { fn(); } catch (e) {} }); }

async function mediaWarmKey(key) {
  if (!key) return null;
  if (_urlCache[key]) return _urlCache[key];
  if (_warming[key]) return _warming[key];
  _warming[key] = (async () => {
    try {
      const blob = await window.vidMediaGet(key);
      if (blob) { const u = URL.createObjectURL(blob); _urlCache[key] = u; _notifyWarm(); return u; }
    } catch (e) {}
    return null;
  })();
  return _warming[key];
}
function _collectKeys(values, out) {
  for (const v of values || []) {
    if (!v) continue;
    if (typeof v === 'object' && v.key) out.push(v.key);
  }
}
async function mediaPrewarm(values) {
  const keys = [];
  _collectKeys(values, keys);
  await Promise.all(keys.map(mediaWarmKey));
}
/* synchronous: returns a usable src now, or null (and kicks off a warm) */
function mediaSrcOf(value) {
  if (!value) return null;
  if (typeof value === 'string') return window.resolveAsset(value);
  if (value.key) { if (_urlCache[value.key]) return _urlCache[value.key]; mediaWarmKey(value.key); return null; }
  return null;
}
function isVideoMedia(v) {
  if (!v) return false;
  if (typeof v === 'object') return v.kind === 'video';
  return window.isVideoPath(v);
}
/* React hook: prewarm a set of refs and re-render once they resolve */
function useMediaWarm(values) {
  const [, tick] = React.useState(0);
  const sig = JSON.stringify((values || []).map(v => (v && typeof v === 'object' ? v.key : v) || null));
  React.useEffect(() => {
    let live = true;
    const fn = () => { if (live) tick(x => x + 1); };
    _warmListeners.add(fn);
    mediaPrewarm(values);
    return () => { live = false; _warmListeners.delete(fn); };
  }, [sig]);
}

/* ---------- asset records + writes ---------- */
function assetUid(kind) { return (kind || 'a')[0] + Date.now().toString(36) + Math.random().toString(36).slice(2, 8); }

function _imgDims(blob) {
  return new Promise((res) => {
    const url = URL.createObjectURL(blob);
    const im = new Image();
    im.onload = () => { res({ w: im.naturalWidth, h: im.naturalHeight }); URL.revokeObjectURL(url); };
    im.onerror = () => { res({}); URL.revokeObjectURL(url); };
    im.src = url;
  });
}
function _vidMeta(blob) {
  return new Promise((res) => {
    const url = URL.createObjectURL(blob);
    const el = document.createElement('video');
    el.preload = 'metadata'; el.muted = true;
    el.onloadedmetadata = () => { res({ w: el.videoWidth, h: el.videoHeight, dur: isFinite(el.duration) ? el.duration : null }); URL.revokeObjectURL(url); };
    el.onerror = () => { res({}); URL.revokeObjectURL(url); };
    el.src = url;
  });
}
function _audMeta(blob) {
  return new Promise((res) => {
    const url = URL.createObjectURL(blob);
    const el = document.createElement('audio');
    el.preload = 'metadata';
    el.onloadedmetadata = () => { res({ dur: isFinite(el.duration) ? el.duration : null }); URL.revokeObjectURL(url); };
    el.onerror = () => { res({}); URL.revokeObjectURL(url); };
    el.src = url;
  });
}

/* Re-index any media REFERENCED by the project (scene media, slide images,
   soundtracks) that lost its Storage record — recovering it from IDB/backend
   so everything in use always shows in Storage, grouped by type. Returns the
   recovered records (also merged into state via setAssets). */
async function reconcileAssets(state, setAssets) {
  const have = new Set(((state && state.assets) || []).map(a => a.key).filter(Boolean));
  const seen = new Set();
  const refs = [];
  const consider = (m, fallbackKind) => {
    if (m && typeof m === 'object' && m.key && !have.has(m.key) && !seen.has(m.key)) {
      seen.add(m.key);
      refs.push({ key: m.key, name: m.name, kind: m.kind || fallbackKind });
    }
  };
  for (const sl of (state && state.slides) || []) { consider(sl.image, 'image'); consider(sl.image2, 'image'); }
  for (const v of (state && state.videos) || []) {
    for (const sc of v.scenes || []) consider(sc.media, isVideoMedia(sc.media) ? 'video' : 'image');
    for (const m of v.images || []) consider(m, 'video');           // legacy slot model
    if (v.audio && v.audio.key) consider({ ...v.audio, kind: 'audio' }, 'audio');
  }
  if (!refs.length) return [];
  const recovered = [];
  for (const ref of refs) {
    try {
      const blob = await window.vidMediaGet(ref.key);
      if (!blob) continue;                                          // truly missing — leave the ref alone
      const kind = ref.kind || (blob.type.startsWith('video') ? 'video' : blob.type.startsWith('audio') ? 'audio' : 'image');
      let w, h, dur = null;
      if (kind === 'image') { const d = await _imgDims(blob); w = d.w; h = d.h; }
      else if (kind === 'video') { const d = await _vidMeta(blob); w = d.w; h = d.h; dur = d.dur; }
      else if (kind === 'audio') { const d = await _audMeta(blob); dur = d.dur; }
      recovered.push({ id: 'a' + ref.key, key: ref.key, kind,
        name: ref.name || ('recovered.' + (kind === 'image' ? 'png' : kind === 'video' ? 'mp4' : 'mp3')),
        source: 'ai', sub: 'recovered', mime: blob.type, size: blob.size, w, h, dur, createdAt: Date.now() });
      if (!_urlCache[ref.key]) _urlCache[ref.key] = URL.createObjectURL(blob);
    } catch (e) {}
  }
  if (recovered.length && setAssets) {
    setAssets(arr => { const known = new Set((arr || []).map(a => a.key));
      return [...recovered.filter(r => !known.has(r.key)), ...(arr || [])]; });
    _notifyWarm();
  }
  return recovered;
}

/* store a blob (or data URL) into IDB + the project asset index. Returns the asset. */
async function storeAsset(setAssets, opts) {
  let blob = opts.blob;
  if (!blob && opts.dataUrl) blob = await (await fetch(opts.dataUrl)).blob();
  if (!blob) throw new Error('no blob to store');
  const kind = opts.kind || (blob.type.startsWith('video') ? 'video' : blob.type.startsWith('audio') ? 'audio' : 'image');
  const key = assetUid(kind);
  await window.vidMediaPut(key, blob);
  let w, h, dur = opts.dur || null;
  if (kind === 'image') { const d = await _imgDims(blob); w = d.w; h = d.h; }
  else if (kind === 'video') { const d = await _vidMeta(blob); w = d.w; h = d.h; if (d.dur) dur = d.dur; }
  const asset = {
    id: 'a' + key, key, kind,
    name: opts.name || (kind === 'image' ? 'image.png' : kind === 'video' ? 'clip.mp4' : 'audio.mp3'),
    source: opts.source || 'upload', sub: opts.sub || '',
    mime: blob.type, size: blob.size, w, h, dur, createdAt: Date.now(),
  };
  if (setAssets) setAssets(arr => [asset, ...(arr || []).filter(a => a.key !== key)]);
  /* make it immediately displayable */
  _urlCache[key] = URL.createObjectURL(blob); _notifyWarm();
  return asset;
}

function deleteAsset(setAssets, asset) {
  if (asset.key) { try { window.vidMediaDel(asset.key); } catch (e) {} }
  if (setAssets) setAssets(arr => (arr || []).filter(a => a.id !== asset.id));
}

/* turn an asset into the media ref used by slides / scenes / soundtrack */
function assetToRef(asset) {
  if (!asset) return null;
  if (asset.path && !asset.key) return asset.path;         // bundled stock asset → path string
  if (asset.kind === 'audio') return { key: asset.key, name: asset.name };
  return { kind: asset.kind, key: asset.key, name: asset.name };
}

/* ---------- bundled stock library (shipped Socc360 assets) ----------
   These live as files in assets/ — they appear in Storage as path-backed
   records (no IDB blob) so they can be reused anywhere like any other asset. */
const BUNDLED_LIBRARY = [
  { path: 'assets/shot-live-home.png', kind: 'image', name: 'Live — Home' },
  { path: 'assets/shot-overview.png', kind: 'image', name: 'Overview' },
  { path: 'assets/shot-timeline.png', kind: 'image', name: 'Match timeline' },
  { path: 'assets/shot-stats.png', kind: 'image', name: 'Stats' },
  { path: 'assets/shot-lineups.png', kind: 'image', name: 'Lineups' },
  { path: 'assets/shot-live-lineups.png', kind: 'image', name: 'Live — Lineups' },
  { path: 'assets/shot-signals.png', kind: 'image', name: 'Signals' },
  { path: 'assets/shot-team.png', kind: 'image', name: 'Team' },
  { path: 'assets/home-screenshot.png', kind: 'image', name: 'Home' },
  { path: 'assets/rec-1.mp4', kind: 'video', name: 'Match clip 1' },
  { path: 'assets/rec-2.mp4', kind: 'video', name: 'Match clip 2' },
  { path: 'assets/rec-3.mp4', kind: 'video', name: 'Match clip 3' },
];
function bundledAssetRecords() {
  return BUNDLED_LIBRARY.map(b => ({
    id: 'bundled-' + b.path, path: b.path, kind: b.kind, name: b.name,
    source: 'upload', sub: 'bundled', createdAt: 0,
  }));
}

/* ---------- thumbnails ---------- */
function MediaThumb({ value, kind, style, rounded }) {
  useMediaWarm([value]);
  const src = mediaSrcOf(value);
  const base = Object.assign({ width: '100%', height: '100%', objectFit: 'cover', display: 'block',
    borderRadius: rounded ? 10 : 0, background: '#05070f' }, style);
  const k = kind || (isVideoMedia(value) ? 'video' : 'image');
  if (k === 'audio') {
    return (
      <div style={{ ...base, display: 'flex', alignItems: 'center', justifyContent: 'center',
        background: 'linear-gradient(160deg,#16224a,#0a1124)', color: '#9dbcff', fontSize: 22 }}>♪</div>
    );
  }
  if (!src) {
    return <div style={{ ...base, display: 'flex', alignItems: 'center', justifyContent: 'center',
      color: 'rgba(255,255,255,0.3)', fontFamily: "'JetBrains Mono',monospace", fontSize: 11 }}>…</div>;
  }
  if (k === 'video') {
    return (
      <div style={{ position: 'relative', width: '100%', height: '100%' }}>
        <video src={src} muted playsInline preload="metadata" style={base} />
        <span style={{ position: 'absolute', left: '50%', top: '50%', transform: 'translate(-50%,-50%)',
          width: 30, height: 30, borderRadius: '50%', background: 'rgba(3,5,14,0.6)', color: '#fff',
          display: 'flex', alignItems: 'center', justifyContent: 'center', fontSize: 13 }}>▶</span>
      </div>
    );
  }
  return <img src={src} crossOrigin="anonymous" style={base} />;
}

/* ---------- storage library panel (full view) ---------- */
const STORE_TABS = [
  { id: 'all', label: 'All' },
  { id: 'upload', label: 'Uploads' },
  { id: 'ai', label: 'AI' },
  { id: 'export', label: 'Exports' },
];
const KIND_TABS = [
  { id: 'all', label: 'All', icon: '▦' },
  { id: 'image', label: 'Photos', icon: '◭' },
  { id: 'video', label: 'Videos', icon: '▶' },
  { id: 'audio', label: 'Music', icon: '♪' },
];

function fmtSize(n) { if (!n) return ''; return n > 1e6 ? (n / 1e6).toFixed(1) + ' MB' : Math.round(n / 1e3) + ' KB'; }
function fmtDur(d) { if (!d) return ''; const s = Math.round(d); return Math.floor(s / 60) + ':' + String(s % 60).padStart(2, '0'); }

function AssetCard({ asset, onPick, onDelete, onDownload, onRename, picking }) {
  const [editing, setEditing] = useState(false);
  const aspect = asset.kind === 'audio' ? '3 / 1' : (asset.w && asset.h ? `${asset.w} / ${asset.h}` : '9 / 16');
  return (
    <div style={{ borderRadius: 14, overflow: 'hidden', border: '1px solid rgba(255,255,255,0.10)',
      background: 'rgba(255,255,255,0.03)', display: 'flex', flexDirection: 'column' }}>
      <div style={{ position: 'relative', aspectRatio: aspect, maxHeight: 190, background: '#05070f',
        cursor: picking ? 'pointer' : 'default' }}
        onClick={picking ? () => onPick(asset) : undefined}>
        <MediaThumb value={assetToRef(asset)} kind={asset.kind} />
        <span style={{ position: 'absolute', top: 6, left: 6, fontFamily: "'JetBrains Mono',monospace", fontSize: 9.5,
          fontWeight: 700, color: '#fff', background: 'rgba(0,0,0,0.6)', borderRadius: 6, padding: '1px 6px',
          textTransform: 'uppercase', letterSpacing: '0.06em' }}>
          {asset.source === 'ai' ? '✦ AI' : asset.source === 'export' ? 'EXPORT' : asset.kind}
        </span>
        {picking ? (
          <span style={{ position: 'absolute', inset: 0, display: 'flex', alignItems: 'flex-end', justifyContent: 'center',
            padding: 8, background: 'linear-gradient(0deg,rgba(3,5,14,0.7),transparent 55%)', opacity: 0,
            transition: 'opacity .15s', fontWeight: 600, fontSize: 12.5, color: '#fff' }}
            onMouseEnter={e => e.currentTarget.style.opacity = 1} onMouseLeave={e => e.currentTarget.style.opacity = 0}>
            Use this
          </span>
        ) : null}
      </div>
      <div style={{ padding: '8px 10px', display: 'flex', flexDirection: 'column', gap: 4 }}>
        {editing ? (
          <input autoFocus defaultValue={asset.name} onBlur={e => { onRename && onRename(asset, e.target.value.trim() || asset.name); setEditing(false); }}
            onKeyDown={e => { if (e.key === 'Enter') e.target.blur(); }}
            style={{ width: '100%', boxSizing: 'border-box', padding: '4px 6px', borderRadius: 7, fontSize: 12,
              background: 'rgba(255,255,255,0.06)', border: '1px solid rgba(255,255,255,0.14)', color: '#fff', outline: 'none' }} />
        ) : (
          <span onClick={() => onRename && setEditing(true)} title={onRename ? 'Rename' : ''} style={{ fontSize: 12, color: 'rgba(255,255,255,0.82)',
            whiteSpace: 'nowrap', overflow: 'hidden', textOverflow: 'ellipsis', cursor: onRename ? 'text' : 'default', fontFamily: BODY }}>
            {asset.name}
          </span>
        )}
        <div style={{ display: 'flex', alignItems: 'center', justifyContent: 'space-between' }}>
          <span style={{ fontFamily: "'JetBrains Mono',monospace", fontSize: 10, color: 'rgba(255,255,255,0.38)' }}>
            {[asset.dur ? fmtDur(asset.dur) : (asset.w ? asset.w + '×' + asset.h : ''), fmtSize(asset.size)].filter(Boolean).join(' · ')}
          </span>
          <div style={{ display: 'flex', gap: 6 }}>
            {onDownload ? (
              <button title="Download" onClick={() => onDownload(asset)} style={{ border: 'none', background: 'transparent',
                color: 'rgba(255,255,255,0.55)', cursor: 'pointer', fontSize: 13, padding: 0 }}>↓</button>
            ) : null}
            {onDelete ? (
              <button title="Delete" onClick={() => onDelete(asset)} style={{ border: 'none', background: 'transparent',
                color: 'rgba(255,128,137,0.8)', cursor: 'pointer', fontSize: 13, padding: 0 }}>✕</button>
            ) : null}
          </div>
        </div>
      </div>
    </div>
  );
}

function downloadAsset(asset) {
  if (asset.path && !asset.key) {
    const a = document.createElement('a'); a.href = window.resolveAsset(asset.path);
    a.download = asset.name || asset.path.split('/').pop(); a.click(); return;
  }
  mediaWarmKey(asset.key).then(url => {
    if (!url) return;
    const a = document.createElement('a'); a.href = url;
    a.download = asset.name || (asset.kind + (asset.kind === 'image' ? '.png' : asset.kind === 'video' ? '.mp4' : '.mp3'));
    a.click();
  });
}

function StoragePanel({ assets, setAssets, onClose, onUpload, onGenerate }) {
  const [kind, setKind] = useState('all');
  const [src, setSrc] = useState('all');
  const all = assets || [];
  const byKind = (k) => k === 'all' ? all : all.filter(a => a.kind === k);
  const list = byKind(kind).filter(a => src === 'all' || a.source === src);
  const fileRef = useRef(null);
  return (
    <div onClick={onClose} style={{ position: 'fixed', inset: 0, zIndex: 1250, background: 'rgba(3,5,14,0.82)',
      display: 'flex', alignItems: 'center', justifyContent: 'center', fontFamily: BODY }}>
      <div onClick={e => e.stopPropagation()} style={{ width: 'min(980px, 95vw)', height: 'min(86vh, 780px)',
        borderRadius: 20, background: '#0a0f1c', border: '1px solid rgba(255,255,255,0.12)', display: 'flex',
        flexDirection: 'column', overflow: 'hidden' }}>
        <div style={{ display: 'flex', alignItems: 'center', justifyContent: 'space-between', padding: '18px 22px 14px',
          borderBottom: '1px solid rgba(255,255,255,0.08)' }}>
          <div style={{ display: 'flex', alignItems: 'center', gap: 16 }}>
            <span style={{ fontFamily: DISPLAY, fontWeight: 600, fontSize: 18, color: '#fff' }}>Storage</span>
            <div style={{ display: 'flex', gap: 4, padding: 3, borderRadius: 11, background: 'rgba(255,255,255,0.05)',
              border: '1px solid rgba(255,255,255,0.08)' }}>
              {KIND_TABS.map(t => (
                <button key={t.id} onClick={() => setKind(t.id)} style={{ padding: '6px 14px', borderRadius: 8,
                  border: 'none', cursor: 'pointer', fontFamily: BODY, fontSize: 13, fontWeight: 600,
                  background: kind === t.id ? 'rgba(47,109,246,0.2)' : 'transparent',
                  color: kind === t.id ? '#9dbcff' : 'rgba(255,255,255,0.55)' }}>
                  <span style={{ marginRight: 6, opacity: 0.8 }}>{t.icon}</span>{t.label}
                  <span style={{ opacity: 0.5, fontSize: 11, marginLeft: 5 }}>{byKind(t.id).length}</span>
                </button>
              ))}
            </div>
          </div>
          <div style={{ display: 'flex', gap: 8, alignItems: 'center' }}>
            <button style={btnStyle({ padding: '8px 14px' })} onClick={() => fileRef.current.click()}>↥ Upload</button>
            {onGenerate ? <button style={btnStyle({ padding: '8px 14px',
              background: 'linear-gradient(180deg,#3b78ff,#1f54d6)', border: '1px solid #3b78ff' })}
              onClick={() => onGenerate()}>✦ Generate</button> : null}
            <button style={btnStyle({ padding: '8px 12px' })} onClick={onClose}>✕</button>
          </div>
          <input ref={fileRef} type="file" multiple accept="image/*,video/*,audio/*" style={{ display: 'none' }}
            onChange={e => { const fs = [...e.target.files]; e.target.value = ''; fs.forEach(f => onUpload && onUpload(f)); }} />
        </div>
        {/* source sub-filter */}
        <div style={{ display: 'flex', gap: 6, padding: '12px 22px 0', flexWrap: 'wrap' }}>
          {STORE_TABS.map(t => (
            <button key={t.id} onClick={() => setSrc(t.id)} style={{ padding: '4px 12px', borderRadius: 999,
              border: '1px solid ' + (src === t.id ? 'rgba(47,109,246,0.5)' : 'rgba(255,255,255,0.10)'),
              cursor: 'pointer', fontFamily: BODY, fontSize: 12, fontWeight: 600,
              background: src === t.id ? 'rgba(47,109,246,0.16)' : 'transparent',
              color: src === t.id ? '#bcd2ff' : 'rgba(255,255,255,0.5)' }}>
              {t.id === 'upload' ? 'Uploaded' : t.id === 'ai' ? '✦ AI-made' : t.id === 'export' ? 'Exports' : 'All sources'}
              <span style={{ opacity: 0.5, marginLeft: 5 }}>{t.id === 'all' ? byKind(kind).length : byKind(kind).filter(a => a.source === t.id).length}</span>
            </button>
          ))}
        </div>
        <div style={{ flex: 1, overflowY: 'auto', padding: 22 }}>
          {list.length === 0 ? (
            <div style={{ height: '100%', display: 'flex', flexDirection: 'column', alignItems: 'center',
              justifyContent: 'center', gap: 10, color: 'rgba(255,255,255,0.4)' }}>
              <div style={{ fontSize: 30 }}>{(KIND_TABS.find(k => k.id === kind) || {}).icon || '🗂'}</div>
              <div style={{ fontSize: 14 }}>No {kind === 'all' ? 'items' : (KIND_TABS.find(k => k.id === kind) || { label: '' }).label.toLowerCase()} here yet.</div>
              <div style={{ fontSize: 12.5, maxWidth: 380, textAlign: 'center', lineHeight: 1.5 }}>
                Upload photos, clips and music — or generate them with ✦ AI. Everything you add, generate
                or export is kept here and sorted by type, ready to reuse across screens and previews.</div>
            </div>
          ) : (
            <div style={{ display: 'grid', gridTemplateColumns: 'repeat(auto-fill, minmax(150px,1fr))', gap: 14 }}>
              {list.map(a => (
                <AssetCard key={a.id} asset={a} onDelete={x => deleteAsset(setAssets, x)} onDownload={downloadAsset}
                  onRename={(as, name) => setAssets(arr => arr.map(x => x.id === as.id ? { ...x, name } : x))} />
              ))}
            </div>
          )}
        </div>
      </div>
    </div>
  );
}

/* ---------- picker modal (used wherever media is added) ---------- */
function StoragePicker({ assets, accept, title, onPick, onClose, onUpload, onGenerate }) {
  // accept: 'image' | 'video' | 'audio' | 'image+video'
  const kinds = accept.split('+');
  const [tab, setTab] = useState('all');
  const fileRef = useRef(null);
  const list = (assets || []).filter(a => kinds.includes(a.kind) && (tab === 'all' || a.source === tab));
  const acceptAttr = kinds.map(k => k + '/*').join(',');
  return (
    <div onClick={onClose} style={{ position: 'fixed', inset: 0, zIndex: 1300, background: 'rgba(3,5,14,0.85)',
      display: 'flex', alignItems: 'center', justifyContent: 'center', fontFamily: BODY }}>
      <div onClick={e => e.stopPropagation()} style={{ width: 'min(720px, 94vw)', maxHeight: '86vh',
        borderRadius: 18, background: '#0a0f1c', border: '1px solid rgba(255,255,255,0.12)', display: 'flex',
        flexDirection: 'column', overflow: 'hidden' }}>
        <div style={{ display: 'flex', alignItems: 'center', justifyContent: 'space-between', padding: '16px 20px',
          borderBottom: '1px solid rgba(255,255,255,0.08)' }}>
          <span style={{ fontFamily: DISPLAY, fontWeight: 600, fontSize: 16, color: '#fff' }}>{title || 'Choose from storage'}</span>
          <div style={{ display: 'flex', gap: 8 }}>
            <button style={btnStyle({ padding: '7px 12px', fontSize: 12.5 })} onClick={() => fileRef.current.click()}>↥ Upload new</button>
            {onGenerate ? <button style={btnStyle({ padding: '7px 12px', fontSize: 12.5,
              background: 'linear-gradient(180deg,#3b78ff,#1f54d6)', border: '1px solid #3b78ff' })}
              onClick={onGenerate}>✦ Generate</button> : null}
            <button style={btnStyle({ padding: '7px 10px' })} onClick={onClose}>✕</button>
          </div>
          <input ref={fileRef} type="file" accept={acceptAttr} style={{ display: 'none' }}
            onChange={e => { const f = e.target.files[0]; e.target.value = ''; if (f && onUpload) onUpload(f); }} />
        </div>
        <div style={{ padding: '12px 20px 0', display: 'flex', gap: 4 }}>
          {STORE_TABS.map(t => (
            <button key={t.id} onClick={() => setTab(t.id)} style={{ padding: '5px 12px', borderRadius: 8,
              border: 'none', cursor: 'pointer', fontFamily: BODY, fontSize: 12.5, fontWeight: 600,
              background: tab === t.id ? 'rgba(47,109,246,0.2)' : 'transparent',
              color: tab === t.id ? '#9dbcff' : 'rgba(255,255,255,0.5)' }}>{t.label}</button>
          ))}
        </div>
        <div style={{ flex: 1, overflowY: 'auto', padding: 20 }}>
          {list.length === 0 ? (
            <div style={{ padding: '40px 0', textAlign: 'center', color: 'rgba(255,255,255,0.4)', fontSize: 13 }}>
              No {accept.replace('+', ' / ')} in storage yet — upload one or generate with ✦ AI.
            </div>
          ) : (
            <div style={{ display: 'grid', gridTemplateColumns: 'repeat(auto-fill, minmax(130px,1fr))', gap: 12 }}>
              {list.map(a => (
                <AssetCard key={a.id} asset={a} picking onPick={onPick} onDelete={null} />
              ))}
            </div>
          )}
        </div>
      </div>
    </div>
  );
}

Object.assign(window, {
  mediaWarmKey, mediaPrewarm, mediaSrcOf, isVideoMedia, useMediaWarm,
  storeAsset, deleteAsset, assetToRef, assetUid, downloadAsset, reconcileAssets,
  MediaThumb, StoragePanel, StoragePicker, AssetCard, bundledAssetRecords,
});
