/* Main Studio app: state machine, agent orchestration, drawer, library */

const { useState, useEffect, useMemo, useCallback, useRef } = React;

const STEPS = [
  { id: 1, name: 'Format' },
  { id: 2, name: 'Audience' },
  { id: 3, name: 'Intel' },
  { id: 4, name: 'Strategy' },
  { id: 5, name: 'Generate' },
  { id: 6, name: 'Export' }
];

const DEFAULT_INTAKE = {
  formats: [],
  goal: '',
  audience_type: '',
  vertical: '',
  persona: '',
  product: '',
  region: 'us',
  account_name: '',
  account_domain: '',
  account_logo_url: '',
  current_practice: '',
  facility_layout: '',
  usage_volume: '',
  competitors: [],
  protocol_gaps: '',
  notes: '',
  attachments: [],
  // Slide deck template (narrative_5 / intro_3 / pitch_10 / all_hands_6).
  // Only used when "deck" is in formats.
  deck_template: 'narrative_5',
  // Global preview/print font scale multiplier (0.85–1.20). Applied at
  // render time via CSS variable so artifacts and exports both pick it up.
  font_scale: 1,
  // Account logo color treatment.
  //   'auto'  — white silhouette on dark slides, full color on light (default)
  //   'mono'  — always white silhouette
  //   'color' — always full color, even on dark backgrounds
  logo_color_mode: 'auto'
};

const ALL_GOALS = [
  { key: 'awareness',     tag: 'TOP',     name: 'Awareness',       desc: 'Introduce Ventum to a new audience or vertical.' },
  { key: 'education',     tag: 'MID',     name: 'Education',       desc: "Teach what Ventum is and why it's different." },
  { key: 'lead_gen',      tag: 'MID',     name: 'Lead generation', desc: 'Capture qualified interest for an outreach pipeline.' },
  { key: 'nurture',       tag: 'MID',     name: 'Nurture',         desc: 'Move a warm account from interested to evaluating.' },
  { key: 'case_proof',    tag: 'BOTTOM',  name: 'Case proof',      desc: 'Use a customer story to remove buyer doubt.' },
  { key: 'event_capture', tag: 'EVENT',   name: 'Event capture',   desc: 'Activation for a trade show, conference, or roadshow.' },
  { key: 'retention',     tag: 'RETAIN',  name: 'Retention',       desc: 'Deepen existing account, drive expansion or renewal.' },
  { key: 'recruit',       tag: 'INTERNAL',name: 'Recruit / team',  desc: 'Hiring, partner enablement, or internal alignment.' }
];

const ALL_FORMATS = Object.entries(window.VENTUM_BRAND.formats).map(([k,v]) => ({ key: k, ...v }));

// Strip oversized fields from a value before persisting to localStorage.
// With image uploads now going to Supabase Storage (URLs instead of
// data URLs), this is rarely triggered — only if a legacy data URL is
// still in an artifact, or the operator uses the Storage-unavailable
// fallback path in EditableImage.
function stripHeavyFields(value) {
  if (value == null) return value;
  if (typeof value === 'string') {
    // Drop oversized data URLs to null rather than a sentinel string.
    // Earlier we used '[stripped:dataurl]' but that string ended up in
    // image_overrides[slot] and image src attributes, producing 404s
    // and broken-image icons. Null lets the renderer fall back to the
    // default vertical/product photo.
    if (value.startsWith('data:') && value.length > 4096) return null;
    return value;
  }
  if (Array.isArray(value)) return value.map(stripHeavyFields);
  if (typeof value === 'object') {
    const out = {};
    for (const [k, v] of Object.entries(value)) {
      out[k] = stripHeavyFields(v);
    }
    return out;
  }
  return value;
}

// Intake-specific slimmer: keep attachment metadata but strip the heavy
// text blob — the user can re-upload if they need it on restore, and the
// blob can be 30k+ chars per file.
function slimIntakeForAutosave(intake) {
  if (!intake) return intake;
  const slim = stripHeavyFields(intake);
  if (Array.isArray(slim.attachments)) {
    slim.attachments = slim.attachments.map(a => ({
      ...a,
      text: a?.text && a.text.length > 2000 ? `[stripped: ${a.text.length} chars — re-upload to re-parse]` : a?.text
    }));
  }
  return slim;
}

// Durable key/value store on IndexedDB. localStorage strips heavy image
// data (data URLs) to fit its ~5MB quota, which is how a replaced image can
// silently revert on reload. IndexedDB has a much larger quota, so we keep a
// FULL, unstripped shadow copy of the autosave here — replaced images survive
// a reload even if their cloud (Supabase Storage) upload failed. Best-effort:
// every call is wrapped so a private-mode / disabled-IDB browser degrades to
// the localStorage-only behavior rather than throwing.
const IDB = (() => {
  const DB = 'ventum-studio', STORE = 'kv';
  let p = null;
  function open() {
    if (p) return p;
    p = new Promise((res, rej) => {
      try {
        const req = indexedDB.open(DB, 1);
        req.onupgradeneeded = () => { if (!req.result.objectStoreNames.contains(STORE)) req.result.createObjectStore(STORE); };
        req.onsuccess = () => res(req.result);
        req.onerror = () => rej(req.error);
      } catch (e) { rej(e); }
    });
    return p;
  }
  return {
    async set(key, val) {
      try {
        const db = await open();
        await new Promise((res, rej) => { const t = db.transaction(STORE, 'readwrite'); t.objectStore(STORE).put(val, key); t.oncomplete = res; t.onerror = () => rej(t.error); });
      } catch (_) { /* durable copy is best-effort */ }
    },
    async get(key) {
      try {
        const db = await open();
        return await new Promise((res) => { const t = db.transaction(STORE, 'readonly'); const r = t.objectStore(STORE).get(key); r.onsuccess = () => res(r.result); r.onerror = () => res(undefined); });
      } catch (_) { return undefined; }
    }
  };
})();

// ============================================================
// MAIN APP
// ============================================================
function StudioApp() {
  // ---- core state ----
  const [step, setStep] = useState(1);
  const [intake, setIntake] = useState(DEFAULT_INTAKE);
  const [strategy, setStrategy] = useState(null);
  const [strategyStatus, setStrategyStatus] = useState(null); // null | live | done | error
  const [artifacts, setArtifacts] = useState({});
  const [agentStatus, setAgentStatus] = useState({}); // {format: live|done|error}
  const [strategyError, setStrategyError] = useState(null); // { message, technical }
  const [agentErrors, setAgentErrors] = useState({}); // {format: { message, technical }}

  // ---- UI state ----
  const [drawerOpen, setDrawerOpen] = useState(false);
  const [drawerTab, setDrawerTab] = useState('voice');
  const [toast, setToast] = useState('');
  const [libraryOpen, setLibraryOpen] = useState(false);
  const [dashboardOpen, setDashboardOpen] = useState(false);
  const [library, setLibrary] = useState([]);
  const [templates, setTemplates] = useState([]);
  const [deepEditFormat, setDeepEditFormat] = useState(null);
  const [autosaveState, setAutosaveState] = useState('idle'); // 'idle' | 'dirty' | 'saving' | 'saved'
  const [tourActive, setTourActive] = useState(false);
  // Help cluster — the "?" button opens a small popover with User Guide + Replay Tour.
  // helpDrawerState controls the side-docked drawer that renders USER_GUIDE.md.
  //   'closed'    — not visible
  //   'open'      — ~480px wide docked panel, no backdrop, user can keep working
  //   'collapsed' — thin ~44px strip on the right with rotated "USER GUIDE" label
  const [helpMenuOpen, setHelpMenuOpen] = useState(false);
  const [avatarMenuOpen, setAvatarMenuOpen] = useState(false);
  const [helpDrawerState, setHelpDrawerState] = useState('closed');

  // Server-side PDF render progress. When non-null, shows a centered
  // overlay with spinner + elapsed time. Set by window.__pdfRender.start()
  // / .stop() — called by the headless-PDF export flow in rooms-5-6.
  const [pdfRender, setPdfRender] = useState(null);
  const [batchOpen, setBatchOpen] = useState(false);
  // Streaming progress — chars received per stream key ('strategy' | format key)
  const [streamProgress, setStreamProgress] = useState({});
  // Supabase auth user (shown in top bar; null while still checking or signed out
  // — though signed-out users never reach here, the AuthGate blocks them upstream).
  const [authUser, setAuthUser] = useState(null);
  // Feature flags from the server, keyed off the signed-in user's email.
  // Cached on window so non-React code (pdf-export, rooms-5-6 export
  // handlers) can read it without prop-drilling.
  const [featureFlags, setFeatureFlags] = useState(null);
  // Supabase id of the brief currently loaded (null = not saved yet).
  // Set when restoring from library or after the first Save. Used by the
  // approval card to read/write the approvals + comments tables.
  const [currentBriefId, setCurrentBriefId] = useState(null);
  useEffect(() => {
    const sb = window.ventumSupabase;
    if (!sb) return;
    sb.auth.getUser().then(({ data }) => setAuthUser(data?.user || null));
    const { data: sub } = sb.auth.onAuthStateChange((_event, sess) => {
      setAuthUser(sess?.user || null);
    });
    return () => sub?.subscription?.unsubscribe();
  }, []);
  // Fetch feature flags whenever the signed-in user changes. Stash on
  // window.__featureFlags so the PDF export (which lives outside React)
  // can read it synchronously when the user clicks Download. Defaults
  // to {} on any failure — non-admin users get the existing html2canvas
  // path, which is the safe-and-known behavior.
  useEffect(() => {
    const email = authUser?.email || '';
    if (!email) { setFeatureFlags(null); window.__featureFlags = null; return; }
    let cancelled = false;
    fetch('/api/feature-flags', {
      method: 'POST',
      credentials: 'include',
      headers: { 'content-type': 'application/json' },
      body: JSON.stringify({ email })
    })
      .then((r) => r.ok ? r.json() : Promise.reject(new Error('HTTP ' + r.status)))
      .then((data) => {
        if (cancelled) return;
        setFeatureFlags(data);
        window.__featureFlags = data;
        // Soft console marker so we can verify in DevTools which path
        // a given user is on without poking at React state.
        if (data?.flags?.headlessPdf) {
          console.info('[VENTUM] feature flag headlessPdf=ON for ' + email + ' (mode=' + data.mode + ')');
        }
      })
      .catch((e) => {
        if (cancelled) return;
        console.warn('[VENTUM] feature-flag fetch failed, defaulting to legacy PDF path:', e?.message || e);
        setFeatureFlags({ flags: {} });
        window.__featureFlags = { flags: {} };
      });
    return () => { cancelled = true; };
  }, [authUser?.email]);
  const signOut = async () => {
    try { await window.ventumSupabase?.auth.signOut(); } catch (_) {}
  };
  // Rough session cost — incremented per agent call.
  // strategy ≈ $0.02, copy ≈ $0.04, research ≈ $0.16 (web_search 3-5 calls).
  // These are point estimates; actual cost depends on token counts.
  const [sessionCost, setSessionCost] = useState(0);
  const addCost = useCallback((kind) => {
    const COSTS = { strategy: 0.02, copy: 0.04, research: 0.16, refine: 0.01, consistency: 0.03 };
    setSessionCost(prev => prev + (COSTS[kind] || 0));
  }, []);

  // ---- toast plumbing ----
  useEffect(() => {
    window.__toast = (msg) => {
      setToast(msg);
      setTimeout(() => setToast(''), 1800);
    };
  }, []);

  // ---- PDF render progress plumbing ----
  // window.__pdfRender.start() / .stop() exposed for the export
  // handlers (in rooms-5-6.jsx) so non-React export code can drive
  // the overlay. Tick every 100ms while active so the elapsed counter
  // updates smoothly. Cleared cleanly on unmount.
  useEffect(() => {
    let intervalId = null;
    window.__pdfRender = {
      start: () => {
        const startedAt = Date.now();
        setPdfRender({ startedAt, elapsedMs: 0 });
        if (intervalId) clearInterval(intervalId);
        intervalId = setInterval(() => {
          setPdfRender(prev => prev ? { ...prev, elapsedMs: Date.now() - prev.startedAt } : prev);
        }, 100);
      },
      stop: () => {
        if (intervalId) { clearInterval(intervalId); intervalId = null; }
        setPdfRender(null);
      }
    };
    return () => {
      if (intervalId) clearInterval(intervalId);
      window.__pdfRender = undefined;
    };
  }, []);

  // ---- library load ----
  // Phase 2: library lives in Supabase. We fetch on mount (after the user
  // is signed in) and whenever authUser changes. The local list is now a
  // read-through cache; mutations go through VentumStorage.* and we re-read.
  // Legacy localStorage library is detected and offered for one-time import.
  const [libraryLoading, setLibraryLoading] = useState(false);
  const [legacyImportPending, setLegacyImportPending] = useState(false);
  const [legacyImporting, setLegacyImporting] = useState(false);

  const reloadLibrary = useCallback(async () => {
    if (!window.VentumStorage) return;
    if (!authUser) { setLibrary([]); setTemplates([]); return; }
    setLibraryLoading(true);
    try {
      const [list, tpls] = await Promise.all([
        window.VentumStorage.listBriefs(),
        window.VentumStorage.listTemplates?.() || Promise.resolve([])
      ]);
      setLibrary(list);
      setTemplates(tpls);
    } catch (e) {
      console.error('listBriefs failed', e);
      window.__toast && window.__toast('Library load failed — check connection');
    } finally {
      setLibraryLoading(false);
    }
  }, [authUser]);

  useEffect(() => {
    reloadLibrary();
    // Surface the legacy-import prompt if the user still has localStorage
    // entries from before Phase 2. Triggered once per session per user.
    if (authUser) {
      try {
        const raw = localStorage.getItem('ventum-studio-library');
        if (raw) {
          const arr = JSON.parse(raw);
          if (Array.isArray(arr) && arr.length > 0) setLegacyImportPending(true);
        }
      } catch (_) {}
    }
  }, [authUser, reloadLibrary]);

  const importLegacyLibrary = async () => {
    if (!window.VentumStorage) return;
    setLegacyImporting(true);
    try {
      const result = await window.VentumStorage.migrateLocalLibrary();
      window.VentumStorage.clearLegacyLibrary();
      setLegacyImportPending(false);
      window.__toast && window.__toast(`Imported ${result.imported} brief${result.imported === 1 ? '' : 's'} to shared library`);
      await reloadLibrary();
    } catch (e) {
      console.error('legacy import failed', e);
      window.__toast && window.__toast('Import failed — see console');
    } finally {
      setLegacyImporting(false);
    }
  };

  const dismissLegacyImport = () => {
    setLegacyImportPending(false);
    // Don't clear the localStorage entries — user might want to re-trigger later
  };

  // ---- autosave: persists current session to localStorage on every change ----
  // Strips embedded base64 image data (data: URIs) and the multi-KB
  // extracted text on attachments before serializing. Without this the
  // autosave payload for a 5-slide deck with images can blow past the
  // localStorage quota (~5MB), JSON.stringify of the bloated payload
  // can block the main thread, the setItem throws, and on next refresh
  // there's no autosave to restore — landing the user back at step 1.
  useEffect(() => {
    if (Object.keys(artifacts).length === 0 && step === 1) return; // nothing yet
    setAutosaveState('dirty');
    const handle = setTimeout(() => {
      setAutosaveState('saving');
      try {
        const slimArtifacts = stripHeavyFields(artifacts);
        const slimIntake = slimIntakeForAutosave(intake);
        const savedAt = Date.now();
        const payload = { intake: slimIntake, strategy, artifacts: slimArtifacts, step, briefId: currentBriefId, savedAt };
        const serialized = JSON.stringify(payload);
        // Guardrail: localStorage quota is ~5MB. If we're over 4MB even
        // after stripping, drop the autosave entirely — better to lose
        // the recovery than to hang the tab or wipe the previous save.
        if (serialized.length > 4 * 1024 * 1024) {
          console.warn('Autosave skipped — payload exceeded 4MB after slimming');
          setAutosaveState('idle');
          return;
        }
        localStorage.setItem('ventum-studio-autosave', serialized);
        // Durable full copy (images intact) in IndexedDB, written AFTER the
        // localStorage commit and keyed to the SAME savedAt — so the restore's
        // savedAt match never rejects a shadow copy that's newer than the
        // stripped localStorage one. On restore we rehydrate stripped images
        // from here so a replaced image never silently reverts.
        IDB.set('autosave-full', { intake, strategy, artifacts, step, briefId: currentBriefId, savedAt });
        setAutosaveState('saved');
        setTimeout(() => setAutosaveState((s) => s === 'saved' ? 'idle' : s), 1400);
      } catch (e) {
        console.warn('Autosave failed', e);
        setAutosaveState('idle');
      }
    }, 600);
    return () => clearTimeout(handle);
  }, [intake, strategy, artifacts, step, currentBriefId]);

  // ---- Session undo: ⌘Z reverts to the previous artifacts snapshot ----
  // Keep up to 30 recent snapshots in memory. Snapshot on every distinct
  // artifacts change (debounced 400ms so a fast keystroke run becomes one
  // entry). ⌘Z pops the latest snapshot, ⌘⇧Z redoes. Bounded so memory
  // stays predictable on long sessions.
  const undoStackRef = useRef([]);  // newest at the end
  const redoStackRef = useRef([]);
  const snapshotDebounceRef = useRef(null);
  const skipNextSnapshotRef = useRef(false);
  useEffect(() => {
    if (skipNextSnapshotRef.current) {
      skipNextSnapshotRef.current = false;
      return;
    }
    if (snapshotDebounceRef.current) clearTimeout(snapshotDebounceRef.current);
    snapshotDebounceRef.current = setTimeout(() => {
      undoStackRef.current.push(JSON.stringify({ intake, strategy, artifacts }));
      if (undoStackRef.current.length > 30) undoStackRef.current.shift();
      redoStackRef.current = []; // any new edit invalidates the redo path
    }, 400);
    return () => { if (snapshotDebounceRef.current) clearTimeout(snapshotDebounceRef.current); };
  }, [intake, strategy, artifacts]);

  const applySnapshot = (snap) => {
    try {
      const obj = JSON.parse(snap);
      skipNextSnapshotRef.current = true;
      setIntake(obj.intake);
      setStrategy(obj.strategy);
      setArtifacts(obj.artifacts);
    } catch (_) {}
  };

  useEffect(() => {
    const onKey = (e) => {
      if (!(e.metaKey || e.ctrlKey)) return;
      if (e.key.toLowerCase() !== 'z') return;
      // Don't fight contenteditable's native undo behavior — only catch
      // when the focused element isn't a text field.
      const t = e.target;
      const inField = t && (t.isContentEditable || ['INPUT','TEXTAREA','SELECT'].includes(t.tagName));
      if (inField) return;
      e.preventDefault();
      if (e.shiftKey) {
        // Redo
        const next = redoStackRef.current.pop();
        if (!next) return;
        undoStackRef.current.push(JSON.stringify({ intake, strategy, artifacts }));
        applySnapshot(next);
        window.__toast && window.__toast('Redone');
      } else {
        // Undo
        const prev = undoStackRef.current.pop();
        if (!prev) return;
        redoStackRef.current.push(JSON.stringify({ intake, strategy, artifacts }));
        applySnapshot(prev);
        window.__toast && window.__toast('Undone');
      }
    };
    window.addEventListener('keydown', onKey);
    return () => window.removeEventListener('keydown', onKey);
  }, [intake, strategy, artifacts]);

  // ---- Concurrent-edit detection (teammate updates this brief) ----
  // When the local user is editing a brief that's already in Supabase,
  // subscribe to UPDATE events on that brief row. If a teammate saves
  // a newer version, surface a banner with a Reload button so the local
  // user can pull their teammate's changes. We track the last seen
  // updated_at so the banner doesn't fire for our own writes.
  const [remoteUpdate, setRemoteUpdate] = useState(null); // { updated_at, by_email }
  const lastSeenUpdatedAtRef = useRef(null);
  useEffect(() => {
    if (!currentBriefId || !window.VentumStorage?.subscribeBrief) return;
    // Stamp the current Supabase version as the last-seen so we don't
    // ban our own update echoes.
    window.VentumStorage.getBriefById?.(currentBriefId).then(b => {
      if (b?.updated_at) lastSeenUpdatedAtRef.current = b.updated_at;
    });
    const unsub = window.VentumStorage.subscribeBrief(currentBriefId, {
      onBriefUpdate: (payload) => {
        const row = payload?.new;
        if (!row) return;
        const remoteAt = row.updated_at;
        const localAt = lastSeenUpdatedAtRef.current;
        if (!remoteAt) return;
        // If we last knew of an updated_at and the new one is the same,
        // it's our own echo (cloud autosave).
        if (localAt && remoteAt <= localAt) return;
        // It's not us — somebody else wrote.
        lastSeenUpdatedAtRef.current = remoteAt;
        setRemoteUpdate({ updated_at: remoteAt, by_email: row.created_by });
      }
    });
    return () => unsub();
  }, [currentBriefId]);

  // Update lastSeenUpdatedAt whenever we successfully push our own change
  // (the cloud autosave fires). The effect below watches artifacts/intake/
  // strategy, refetches the brief's current updated_at, and stamps it so
  // the subscription's echo check works.
  useEffect(() => {
    if (!currentBriefId || !window.VentumStorage?.getBriefById) return;
    const handle = setTimeout(() => {
      window.VentumStorage.getBriefById(currentBriefId).then(b => {
        if (b?.updated_at) lastSeenUpdatedAtRef.current = b.updated_at;
      });
    }, 3000); // run shortly after cloud autosave settles
    return () => clearTimeout(handle);
  }, [currentBriefId, intake, strategy, artifacts]);

  const reloadFromRemote = async () => {
    if (!currentBriefId || !window.VentumStorage?.getBriefById) return;
    try {
      const brief = await window.VentumStorage.getBriefById(currentBriefId);
      if (!brief) return;
      const liveSig = intakeSig(brief.intake || DEFAULT_INTAKE);
      setIntake(brief.intake || DEFAULT_INTAKE);
      let s = brief.strategy || null;
      if (s && !s._intake_sig) s = { ...s, _intake_sig: liveSig };
      setStrategy(s);
      const stamped = {};
      Object.entries(brief.artifacts || {}).forEach(([k, art]) => {
        if (!art) { stamped[k] = art; return; }
        stamped[k] = (art._intake_sig && art._strategy_sig)
          ? art
          : { ...art, _intake_sig: art._intake_sig || liveSig, _strategy_sig: art._strategy_sig || (s?._intake_sig || liveSig) };
      });
      setArtifacts(stamped);
      window.__ventumEditableScales = { ...(brief.intake?._field_scales || {}) };
      if (brief.updated_at) lastSeenUpdatedAtRef.current = brief.updated_at;
      setRemoteUpdate(null);
      window.__toast && window.__toast('Loaded teammate\'s changes');
    } catch (e) {
      console.error('reload failed', e);
      window.__toast && window.__toast('Reload failed — see console');
    }
  };

  // ---- Mark "last edited at" for race-guard against rehydrate ----
  // The autosave-restore path async-fetches the canonical brief from
  // Supabase ~1s after mount. If the operator starts editing during
  // that window, the fetch must NOT overwrite their changes. We stamp
  // window.__ventumLastEditAt on every intake/strategy/artifacts change
  // and the rehydrate handler skips when this is more recent than its
  // fetchStartedAt. Stamping a ref instead of state so it doesn't
  // re-trigger renders.
  useEffect(() => {
    window.__ventumLastEditAt = Date.now();
  }, [intake, strategy, artifacts]);

  // ---- Listen for per-field font-scale changes from Editable ----
  // The A−/A+ control in renderers.jsx stores scales in
  // window.__ventumEditableScales. We mirror them into intake._field_scales
  // (debounced 500ms) so they ride along with cloud autosave to Supabase
  // and round-trip through restoreFromLibrary / autosave restore.
  useEffect(() => {
    let handle;
    const onScaleChanged = () => {
      if (handle) clearTimeout(handle);
      handle = setTimeout(() => {
        const cache = { ...(window.__ventumEditableScales || {}) };
        setIntake(prev => ({ ...prev, _field_scales: cache }));
      }, 500);
    };
    window.addEventListener('ventum-scale-changed', onScaleChanged);
    return () => {
      window.removeEventListener('ventum-scale-changed', onScaleChanged);
      if (handle) clearTimeout(handle);
    };
  }, []);

  // ---- Cloud autosave: sync edits to Supabase for an existing brief ----
  // When the user is editing a brief that's already in the shared library
  // (currentBriefId set), debounce-push intake/strategy/artifacts to
  // Supabase so reloading or reopening from the library shows their
  // edits. Without this, only the explicit "Update" button persists to
  // the cloud and a hard refresh between edits silently reverted them.
  //
  // Skips when:
  //   · No currentBriefId (it's a new unsaved brief — autosaves locally only)
  //   · Storage layer unavailable
  //   · Update fails (e.g. RLS — user doesn't own the brief). They can
  //     "Save as copy" to fork it.
  //
  // Pending payload + timer are held in refs so flushCloudAutosave() can
  // force them to fire BEFORE we reload from the library — without that,
  // clicking Library → Open on the same brief within 2.5s reverts the
  // user's in-flight edits.
  const cloudAutosaveTimerRef = useRef(null);
  const cloudAutosavePayloadRef = useRef(null);

  useEffect(() => {
    if (!currentBriefId || !window.VentumStorage) return;
    if (Object.keys(artifacts).length === 0) return;
    cloudAutosavePayloadRef.current = {
      briefId: currentBriefId,
      title: deriveTitle(intake, strategy),
      intake, strategy, artifacts
    };
    if (cloudAutosaveTimerRef.current) clearTimeout(cloudAutosaveTimerRef.current);
    cloudAutosaveTimerRef.current = setTimeout(async () => {
      const p = cloudAutosavePayloadRef.current;
      if (!p) return;
      try {
        await window.VentumStorage.updateBrief(p.briefId, {
          title: p.title, intake: p.intake, strategy: p.strategy, artifacts: p.artifacts
        });
        cloudAutosavePayloadRef.current = null;
      } catch (e) {
        console.warn('[cloud autosave] update skipped:', e?.message || e);
      }
    }, 2500);
    return () => clearTimeout(cloudAutosaveTimerRef.current);
  }, [currentBriefId, intake, strategy, artifacts]);

  // Force any pending cloud autosave to flush NOW. Awaited before
  // navigation that re-hydrates state from the cloud (library open,
  // dashboard open) so stale snapshots don't blow away unsynced edits.
  const flushCloudAutosave = useCallback(async () => {
    const p = cloudAutosavePayloadRef.current;
    if (!p || !window.VentumStorage) return;
    if (cloudAutosaveTimerRef.current) {
      clearTimeout(cloudAutosaveTimerRef.current);
      cloudAutosaveTimerRef.current = null;
    }
    try {
      await window.VentumStorage.updateBrief(p.briefId, {
        title: p.title, intake: p.intake, strategy: p.strategy, artifacts: p.artifacts
      });
      cloudAutosavePayloadRef.current = null;
    } catch (e) {
      console.warn('[cloud autosave] flush failed:', e?.message || e);
    }
  }, []);

  // ---- restore most recent autosave on first mount (skip if share link present) ----
  useEffect(() => {
    if (/#share=/.test(location.hash)) return;
    try {
      const raw = localStorage.getItem('ventum-studio-autosave');
      if (!raw) return;
      const payload = JSON.parse(raw);
      if (!payload?.artifacts || !Object.keys(payload.artifacts).length) return;
      // 24-hour freshness
      if (Date.now() - (payload.savedAt || 0) > 24 * 60 * 60 * 1000) return;
      const restoredIntake = payload.intake || DEFAULT_INTAKE;
      // Stamp _intake_sig on legacy strategy/artifacts that pre-date the
      // staleness detection — without this they look "fresh forever" and
      // the orange "intake has changed" banner never appears.
      const restoredSig = intakeSig(restoredIntake);
      setIntake(restoredIntake);
      let restoredStrategy = payload.strategy || null;
      if (restoredStrategy && !restoredStrategy._intake_sig) {
        restoredStrategy = { ...restoredStrategy, _intake_sig: restoredSig };
      }
      setStrategy(restoredStrategy);
      const restoredArtifacts = {};
      Object.entries(payload.artifacts || {}).forEach(([k, art]) => {
        if (!art) { restoredArtifacts[k] = art; return; }
        restoredArtifacts[k] = (art._intake_sig && art._strategy_sig)
          ? art
          : { ...art, _intake_sig: art._intake_sig || restoredSig, _strategy_sig: art._strategy_sig || (restoredStrategy?._intake_sig || restoredSig) };
      });
      setArtifacts(restoredArtifacts);
      // Rehydrate images the localStorage copy stripped, from the durable
      // IndexedDB shadow copy. Only for UNSAVED briefs (no briefId): a saved
      // brief gets its correct images from the canonical Supabase fetch below,
      // so running IDB there would race that fetch and could revert canonical
      // or in-flight-edited state back to the local snapshot. Guarded against
      // edits started during the async read either way.
      if (!payload.briefId) {
        const idbRestoreStartedAt = Date.now();
        window.__ventumLastEditAt = window.__ventumLastEditAt || 0;
        IDB.get('autosave-full').then((full) => {
          if (!full || full.savedAt !== payload.savedAt || !full.artifacts) return;
          if ((window.__ventumLastEditAt || 0) > idbRestoreStartedAt) return; // don't clobber edits made while reading IDB
          if (full.intake) setIntake(full.intake);
          const rr = {};
          Object.entries(full.artifacts).forEach(([k, art]) => {
            if (!art) { rr[k] = art; return; }
            rr[k] = (art._intake_sig && art._strategy_sig) ? art : { ...art, _intake_sig: art._intake_sig || restoredSig, _strategy_sig: art._strategy_sig || (restoredStrategy?._intake_sig || restoredSig) };
          });
          setArtifacts(rr);
        }).catch(() => {});
      }
      const status = {};
      Object.keys(restoredArtifacts).forEach(k => status[k] = 'done');
      setAgentStatus(status);
      setStrategyStatus(restoredStrategy ? 'done' : null);
      setStep(payload.step || 5);
      // Restore the loaded brief's identity so the next "Save" updates
      // the existing row instead of inserting a duplicate. Without this,
      // every page refresh during an edit session would orphan the prior
      // brief and create a new one.
      if (payload.briefId) {
        setCurrentBriefId(payload.briefId);
        // Supabase is the source of truth for saved briefs. Fetch the
        // canonical version so any stale data in localStorage (legacy
        // stripped data URLs from before image storage was wired) gets
        // replaced. Guard: if the operator has started editing during
        // the ~1s fetch window, skip the replacement so we don't blow
        // away their in-flight changes.
        if (window.VentumStorage?.getBriefById) {
          const fetchStartedAt = Date.now();
          window.__ventumLastEditAt = window.__ventumLastEditAt || 0;
          window.VentumStorage.getBriefById(payload.briefId).then((brief) => {
            if (!brief) return;
            if ((window.__ventumLastEditAt || 0) > fetchStartedAt) {
              console.info('[restore] skipping supabase replace — local edits in flight');
              return;
            }
            const liveSig = intakeSig(brief.intake || DEFAULT_INTAKE);
            setIntake(brief.intake || DEFAULT_INTAKE);
            let s = brief.strategy || null;
            if (s && !s._intake_sig) s = { ...s, _intake_sig: liveSig };
            setStrategy(s);
            const stamped = {};
            Object.entries(brief.artifacts || {}).forEach(([k, art]) => {
              if (!art) { stamped[k] = art; return; }
              stamped[k] = (art._intake_sig && art._strategy_sig)
                ? art
                : { ...art, _intake_sig: art._intake_sig || liveSig, _strategy_sig: art._strategy_sig || (s?._intake_sig || liveSig) };
            });
            setArtifacts(stamped);
            // Hydrate font-scale cache from the canonical version too.
            window.__ventumEditableScales = { ...(brief.intake?._field_scales || {}) };
          }).catch((e) => {
            console.warn('[restore] supabase fetch failed; keeping localStorage version', e?.message || e);
          });
        }
      }
      // Hydrate per-field font-scale cache from autosaved intake.
      if (restoredIntake?._field_scales) {
        window.__ventumEditableScales = { ...restoredIntake._field_scales };
      }
    } catch (_) {}
  }, []);

  // ---- keyboard shortcuts ----
  useEffect(() => {
    const handler = (e) => {
      const tgt = e.target;
      const isField = tgt && (tgt.isContentEditable || ['INPUT','TEXTAREA','SELECT'].includes(tgt.tagName));
      const meta = e.metaKey || e.ctrlKey;
      // ⌘S = save to library
      if (meta && e.key.toLowerCase() === 's') {
        e.preventDefault();
        if (Object.keys(artifacts).length) saveToLibrary();
        return;
      }
      // ⌘L = open library
      if (meta && e.key.toLowerCase() === 'l') {
        e.preventDefault();
        flushCloudAutosave().finally(() => { reloadLibrary(); setLibraryOpen(true); });
        return;
      }
      if (isField) return; // skip non-meta shortcuts while typing
      // [ and ] flip artifact tabs (handled via custom event so the room owns activeFormat)
      if (e.key === '[' || e.key === ']') {
        window.dispatchEvent(new CustomEvent('__ventum_tab_nav', { detail: { dir: e.key === '[' ? -1 : 1 } }));
        e.preventDefault();
        return;
      }
      // ? opens shortcut hint
      if (e.key === '?' && e.shiftKey) {
        window.__toast && window.__toast('[ ] flip artifact · ⌘S save · ⌘L library · ⌘P export PDF');
        e.preventDefault();
      }
    };
    window.addEventListener('keydown', handler);
    return () => window.removeEventListener('keydown', handler);
  }, [artifacts]);

  // ---- shared-link decode on mount ----
  useEffect(() => {
    const hash = location.hash;
    const m = /#share=([^&]+)/.exec(hash);
    if (m) {
      try {
        const payload = JSON.parse(decodeURIComponent(escape(atob(m[1]))));
        const sharedIntake = payload.intake || DEFAULT_INTAKE;
        const sharedSig = intakeSig(sharedIntake);
        if (payload.intake) setIntake(sharedIntake);
        let sharedStrategy = payload.strategy || null;
        if (sharedStrategy && !sharedStrategy._intake_sig) {
          sharedStrategy = { ...sharedStrategy, _intake_sig: sharedSig };
        }
        if (sharedStrategy) setStrategy(sharedStrategy);
        if (payload.artifacts) {
          const stamped = {};
          Object.entries(payload.artifacts).forEach(([k, art]) => {
            if (!art) { stamped[k] = art; return; }
            stamped[k] = (art._intake_sig && art._strategy_sig)
              ? art
              : { ...art, _intake_sig: art._intake_sig || sharedSig, _strategy_sig: art._strategy_sig || (sharedStrategy?._intake_sig || sharedSig) };
          });
          setArtifacts(stamped);
          const status = {};
          Object.keys(stamped).forEach(k => status[k] = 'done');
          setAgentStatus(status);
          setStrategyStatus('done');
        }
        setStep(6);
        setToast('Loaded from share link');
        setTimeout(() => setToast(''), 1800);
      } catch (e) {
        console.warn('Share link decode failed', e);
      }
    }
  }, []);

  // ---- first-run tour: open automatically the first time the studio is opened
  useEffect(() => {
    try {
      const seen = localStorage.getItem('ventum-tour-done');
      if (!seen) {
        // small delay so the page paints first
        const t = setTimeout(() => setTourActive(true), 600);
        return () => clearTimeout(t);
      }
    } catch (_) {}
  }, []);

  const finishTour = (dismiss = true) => {
    setTourActive(false);
    if (dismiss) {
      try { localStorage.setItem('ventum-tour-done', '1'); } catch (_) {}
    }
  };

  // Formats whose schemas explicitly require an account_name — gated at canProceed
  // so the operator can't slide past Step 02 without the field these formats need.
  const ACCOUNT_REQUIRED_FORMATS = ['account_brief', 'info_pack', 'case_study_summary'];

  // ---- canProceed by step ----
  const canProceed = (s) => {
    if (s === 1) return intake.goal && (intake.formats || []).length > 0;
    if (s === 2) {
      if (!intake.audience_type || !intake.vertical) return false;
      const needsAccount = (intake.formats || []).some(f => ACCOUNT_REQUIRED_FORMATS.includes(f));
      if (needsAccount && !(intake.account_name && intake.account_name.trim())) return false;
      return true;
    }
    if (s === 3) return true; // intel optional
    if (s === 4) return !!strategy;
    if (s === 5) return Object.keys(artifacts).length > 0;
    return true;
  };

  // Human-readable reason the Next button is disabled, surfaced in the bottombar.
  const missingForNext = () => {
    if (step === 1) {
      if (!intake.goal) return 'Pick a campaign goal';
      if (!(intake.formats || []).length) return 'Pick at least one format';
    }
    if (step === 2) {
      if (!intake.audience_type) return 'Pick an audience type';
      if (!intake.vertical) return 'Pick a vertical';
      const needsAccount = (intake.formats || []).some(f => ACCOUNT_REQUIRED_FORMATS.includes(f));
      if (needsAccount && !(intake.account_name && intake.account_name.trim())) {
        return 'Account name required for account-anchored formats';
      }
    }
    return '';
  };

  // Hash of the intake fields that actually drive generation. Used to detect
  // when the strategy or copy artifacts have gone stale because the user
  // changed the brief — so hitting "next" regenerates instead of showing the
  // last run's output (which was the "took me to the last entry" complaint).
  const intakeSig = (i) => JSON.stringify({
    formats: (i.formats || []).slice().sort(),
    vertical: i.vertical || '',
    goal: i.goal || '',
    persona: i.persona || '',
    product: i.product || '',
    products: (i.products || []).slice().sort(),
    subject_framing: i.subject_framing || 'product',
    account_name: i.account_name || '',
    campaign_objective: i.campaign_objective || '',
    key_message: i.key_message || '',
    time_window: i.time_window || '',
    partner: i.partner || '',
    supporting_proofs: (i.supporting_proofs || []).slice().sort(),
    current_practice: i.current_practice || '',
    facility_layout: i.facility_layout || '',
    usage_volume: i.usage_volume || '',
    competitors: (i.competitors || []).slice().sort(),
    protocol_gaps: i.protocol_gaps || ''
  });
  const currentSig = intakeSig(intake);
  // A strategy/artifact is "stale" only when its stored sig explicitly
  // disagrees with the current intake sig. Absent sigs are treated as
  // "unknown / probably fine" — the restore path stamps sigs on legacy state
  // so the strict check still fires after the user touches any tracked field.
  const strategyStale = !!strategy && !!strategy._intake_sig && strategy._intake_sig !== currentSig;
  const stratSigForArtifacts = strategy?._intake_sig || '';
  const artifactStaleKeys = Object.entries(artifacts)
    .filter(([k, a]) => !!a && (
      (a._intake_sig && a._intake_sig !== currentSig) ||
      (stratSigForArtifacts && a._strategy_sig && a._strategy_sig !== stratSigForArtifacts)
    ))
    .map(([k]) => k);

  // Debug helper — inspect from DevTools console:
  //   window.__VENTUM_DEBUG
  if (typeof window !== 'undefined') {
    window.__VENTUM_DEBUG = {
      currentSig,
      strategyExists: !!strategy,
      strategySig: strategy?._intake_sig,
      strategyStale,
      artifactStaleKeys,
      artifactSigs: Object.fromEntries(Object.entries(artifacts).map(([k, a]) => [k, { intake: a?._intake_sig, strategy: a?._strategy_sig }]))
    };
  }

  // ---- step nav ----
  const goNext = async () => {
    if (step === 3) {
      setStep(4);
      // run strategy if missing or stale
      if (!strategy || strategyStale) {
        if (strategyStale) setStrategy(null);
        await doRunStrategy();
      }
      return;
    }
    if (step === 4) {
      setStep(5);
      // clear stale artifacts so the generation auto-kick below fires
      let nextArtifacts = artifacts;
      if (artifactStaleKeys.length) {
        nextArtifacts = { ...artifacts };
        artifactStaleKeys.forEach(k => delete nextArtifacts[k]);
        setArtifacts(nextArtifacts);
      }
      if (!Object.keys(nextArtifacts).length && (intake.formats || []).length > 0) {
        setTimeout(() => doRunCopyAll(), 200);
      }
      return;
    }
    setStep(Math.min(6, step + 1));
  };
  const goBack = () => setStep(Math.max(1, step - 1));
  const jumpTo = (s) => setStep(s);

  // ---- agent runners ----
  const friendlyError = (e) => {
    const msg = String(e?.message || e || '');
    if (/529|overloaded/i.test(msg)) return { message: 'Anthropic is briefly overloaded. We retried a few times and gave up — wait 30 seconds and retry.', technical: msg };
    if (/rate.?limit|429/i.test(msg)) return { message: 'The AI service hit a rate limit. Wait a moment and retry.', technical: msg };
    if (/502|503|504/i.test(msg)) return { message: 'The AI service had a brief infrastructure blip. Retry usually fixes this.', technical: msg };
    if (/network|fetch|connect|ERR_NETWORK/i.test(msg)) return { message: 'Network connection failed. Check your internet and retry.', technical: msg };
    if (/quota|limit.*tokens|exceeded/i.test(msg)) return { message: 'Daily generation quota reached. Try again tomorrow or ask your admin to increase the cap.', technical: msg };
    if (/empty response|content-policy|refusal/i.test(msg)) return { message: 'The model returned an empty response (often a content-policy edge case). Try simplifying the brief or rephrasing the attached document.', technical: msg };
    if (/JSON|parse|output/i.test(msg)) return { message: 'The AI returned an unexpected response. Retry usually fixes this.', technical: msg };
    if (/api.?key|auth|401|403/i.test(msg)) return { message: 'API authentication failed. Contact your admin.', technical: msg };
    if (/400|prompt is too long|context.*length|max.*tokens.*exceed/i.test(msg)) return { message: 'The prompt is too long — the attached briefs + intake together exceed the model context. Remove an attachment or trim the longest one.', technical: msg };
    return { message: 'Something went wrong while generating. Retry — if it keeps failing, share the technical details below with your admin.', technical: msg };
  };

  const doRunStrategy = async () => {
    setStrategyError(null);
    setStrategyStatus('live');
    setStreamProgress(prev => ({ ...prev, strategy: 0 }));
    const sigAtRun = intakeSig(intake);
    try {
      const s = await window.VentumAgents.runStrategyAgent(intake, {
        onStream: (_chunk, full) => {
          setStreamProgress(prev => ({ ...prev, strategy: full.length }));
        }
      });
      addCost('strategy');
      setStrategy({ ...s, _intake_sig: sigAtRun });
      setStrategyStatus('done');
      setStreamProgress(prev => { const n = { ...prev }; delete n.strategy; return n; });
    } catch (e) {
      console.error(e);
      setStrategyStatus('error');
      setStrategyError(friendlyError(e));
      setStreamProgress(prev => { const n = { ...prev }; delete n.strategy; return n; });
    }
  };

  const doRunCopyAll = async () => {
    const formats = intake.formats || [];
    setAgentErrors({});
    // mark all live
    const startStatus = {};
    formats.forEach(f => { startStatus[f] = 'live'; });
    setAgentStatus(startStatus);
    setStreamProgress(prev => {
      const next = { ...prev };
      formats.forEach(f => { next[f] = 0; });
      return next;
    });

    const sigAtRun = intakeSig(intake);
    const strategySigAtRun = strategy?._intake_sig || '';

    // generate in parallel
    const promises = formats.map(async (f) => {
      try {
        const a = await window.VentumAgents.runCopyAgent(f, intake, strategy, {}, null, {
          onStream: (_chunk, full) => {
            setStreamProgress(prev => ({ ...prev, [f]: full.length }));
          }
        });
        addCost('copy');
        return { f, ok: true, a };
      } catch (e) {
        console.error(`Copy agent (${f}) failed`, e);
        return { f, ok: false, e };
      }
    });
    const results = await Promise.all(promises);

    const nextArtifacts = { ...artifacts };
    const nextStatus = {};
    const nextErrors = {};
    results.forEach(({ f, ok, a, e }) => {
      if (ok) { nextArtifacts[f] = { ...a, _intake_sig: sigAtRun, _strategy_sig: strategySigAtRun }; nextStatus[f] = 'done'; }
      else    { nextStatus[f] = 'error'; nextErrors[f] = friendlyError(e); }
    });
    setArtifacts(nextArtifacts);
    setAgentStatus(nextStatus);
    setAgentErrors(nextErrors);
    setStreamProgress(prev => {
      const next = { ...prev };
      formats.forEach(f => { delete next[f]; });
      return next;
    });
  };

  const doRunVariants = async (format, artifact) => {
    return window.VentumAgents.runVariantAgent(format, artifact, intake, strategy);
  };

  // Per-post campaign regeneration — returns the new post object only.
  // The renderer is responsible for splicing it into the campaign's posts[].
  const doRegenerateCampaignPost = async (format, postIndex, instruction) => {
    const currentArtifact = artifacts[format];
    if (!currentArtifact) throw new Error(`No artifact for ${format}`);
    return window.VentumAgents.runRegenerateCampaignPost(format, intake, strategy, currentArtifact, postIndex, instruction);
  };

  const doRunRefine = async (format, fieldKey, currentValue, instruction) => {
    return window.VentumAgents.runRefineAgent(format, artifacts[format], fieldKey, currentValue, instruction);
  };

  // Single-artifact regenerate (used from deep edit)
  const doRegenerateOne = async (format) => {
    setAgentStatus(prev => ({ ...prev, [format]: 'live' }));
    setAgentErrors(prev => { const n = { ...prev }; delete n[format]; return n; });
    setStreamProgress(prev => ({ ...prev, [format]: 0 }));
    const sigAtRun = intakeSig(intake);
    const strategySigAtRun = strategy?._intake_sig || '';
    try {
      const a = await window.VentumAgents.runCopyAgent(format, intake, strategy, {}, null, {
        onStream: (_chunk, full) => {
          setStreamProgress(prev => ({ ...prev, [format]: full.length }));
        }
      });
      addCost('copy');
      const stamped = { ...a, _intake_sig: sigAtRun, _strategy_sig: strategySigAtRun };
      setArtifacts(prev => ({ ...prev, [format]: stamped }));
      setAgentStatus(prev => ({ ...prev, [format]: 'done' }));
      setStreamProgress(prev => { const n = { ...prev }; delete n[format]; return n; });
      return stamped;
    } catch (e) {
      console.error(e);
      setAgentStatus(prev => ({ ...prev, [format]: 'error' }));
      setAgentErrors(prev => ({ ...prev, [format]: friendlyError(e) }));
      setStreamProgress(prev => { const n = { ...prev }; delete n[format]; return n; });
      throw e;
    }
  };

  // ---- library actions (Phase 2: Supabase-backed shared library) ----
  const saveToLibrary = async () => {
    if (!window.VentumStorage) {
      window.__toast && window.__toast('Storage unavailable — try refreshing');
      return;
    }
    try {
      // If we already have a brief id (loaded from library), update in place.
      // Otherwise create a new row.
      let savedId;
      if (currentBriefId) {
        const updated = await window.VentumStorage.updateBrief(currentBriefId, {
          title: deriveTitle(intake, strategy),
          intake, strategy, artifacts
        });
        savedId = updated.id;
      } else {
        const created = await window.VentumStorage.saveBrief({
          title: deriveTitle(intake, strategy),
          intake, strategy, artifacts
        });
        savedId = created.id;
        setCurrentBriefId(savedId);
      }
      window.__toast(currentBriefId ? 'Updated · everyone signed in sees the change' : 'Saved to shared library');
      reloadLibrary();
    } catch (e) {
      console.error('saveBrief failed', e);
      window.__toast && window.__toast(e.message || 'Save failed — see console');
    }
  };

  // Start a NEW brief from a template entry. Clones the template's
  // intake (minus account-specific fields) so the operator can fill in
  // the new account + regenerate. Templates are NOT mutated by the new
  // brief — only the original template stays the template.
  const useTemplate = (template) => {
    if (!window.VentumStorage?.cloneTemplateForUse) return;
    const cloned = window.VentumStorage.cloneTemplateForUse(template);
    setIntake(cloned.intake);
    setStrategy(cloned.strategy);
    setArtifacts(cloned.artifacts);
    setCurrentBriefId(null); // new brief — not the template's id
    setAgentStatus({});
    setStrategyStatus(null);
    setStep(2); // skip Format step since template already picked it
    window.__toast && window.__toast(`Started from template · "${template.template_label || template.title}"`);
  };

  // Save the current brief data as a template — a reusable starting
  // point. Prompts for a label (kept simple — a window.prompt is fine
  // for now; can upgrade to a modal once the team uses it heavily).
  const saveAsTemplate = async () => {
    if (!window.VentumStorage?.createTemplate) return;
    const label = (window.prompt('Template name (e.g. "Hospitality account brief")', deriveTitle(intake, strategy)) || '').trim();
    if (!label) return;
    const description = (window.prompt('Short description for teammates (optional)', '') || '').trim();
    try {
      await window.VentumStorage.createTemplate({
        title: label,
        intake, strategy, artifacts,
        label,
        description
      });
      window.__toast && window.__toast('Template saved — available in Step 1 next time you start a brief');
    } catch (e) {
      console.error('createTemplate failed', e);
      window.__toast && window.__toast(e.message || 'Save template failed');
    }
  };

  // Force-save a brand-new copy regardless of currentBriefId. Useful when
  // editing someone else's brief and you explicitly want your own version,
  // or when you want to fork a brief for a variant pitch.
  const saveAsCopy = async () => {
    if (!window.VentumStorage) return;
    try {
      const created = await window.VentumStorage.saveBrief({
        title: deriveTitle(intake, strategy) + ' (copy)',
        intake, strategy, artifacts
      });
      setCurrentBriefId(created.id);
      window.__toast && window.__toast('Saved as a new copy');
      reloadLibrary();
    } catch (e) {
      console.error('saveAsCopy failed', e);
      window.__toast && window.__toast(e.message || 'Save-as-copy failed');
    }
  };
  const restoreFromLibrary = async (entry) => {
    // 1. Flush any pending cloud autosave for the CURRENT brief before we
    //    swap state — otherwise the user's in-flight edits get blown away
    //    by the restore. Critical for Library → Open-the-same-brief race.
    await flushCloudAutosave();

    // 2. Re-fetch the canonical row by id. The library list snapshot is
    //    loaded once (or on coarse triggers) and may be older than what's
    //    in Supabase right now — especially the brief the user was just
    //    editing.
    let fresh = entry;
    if (entry?.id && window.VentumStorage?.getBriefById) {
      try {
        const reloaded = await window.VentumStorage.getBriefById(entry.id);
        if (reloaded) fresh = reloaded;
      } catch (e) {
        console.warn('[restoreFromLibrary] getBriefById failed, using cached row', e?.message || e);
      }
    }

    const restoredIntake = fresh.intake || DEFAULT_INTAKE;
    setIntake(restoredIntake);
    setStrategy(fresh.strategy || null);
    setArtifacts(fresh.artifacts || {});
    setCurrentBriefId(fresh.id || null);
    // Hydrate the per-field font-scale cache from the saved intake so
    // A−/A+ adjustments survive Library → Open round-trips.
    window.__ventumEditableScales = { ...(restoredIntake._field_scales || {}) };
    const status = {};
    Object.keys(fresh.artifacts || {}).forEach(k => status[k] = 'done');
    setAgentStatus(status);
    setStrategyStatus(fresh.strategy ? 'done' : null);
    setLibraryOpen(false);
    setStep(5);
    window.__toast('Restored from library');
  };
  const duplicateFromLibrary = (entry) => {
    setIntake({ ...(entry.intake || DEFAULT_INTAKE) });
    setStrategy(null);
    setArtifacts({});
    setAgentStatus({});
    setStrategyStatus(null);
    setCurrentBriefId(null);  // a duplicate is a fresh brief — no shared identity
    setLibraryOpen(false);
    setStep(1);
    window.__toast('Brief duplicated as a new piece');
  };
  const deleteFromLibrary = async (id) => {
    if (!window.VentumStorage) return;
    try {
      await window.VentumStorage.deleteBrief(id);
      setLibrary(prev => prev.filter(e => e.id !== id));
      window.__toast && window.__toast('Deleted');
    } catch (e) {
      console.error('deleteBrief failed', e);
      window.__toast && window.__toast('Delete failed — you can only delete briefs you created');
    }
  };

  const startNew = () => {
    setIntake(DEFAULT_INTAKE);
    setStrategy(null);
    setStrategyStatus(null);
    setArtifacts({});
    setAgentStatus({});
    setCurrentBriefId(null);
    setStep(1);
    window.__toast('New piece — wizard reset');
  };

  // ============================================================
  // RENDER
  // ============================================================
  return (
    <div className="studio-app">
      {/* Legacy library migration banner — appears once when we detect the
          user has localStorage library entries that haven't been imported
          to Supabase yet. One-click import or dismiss. */}
      {legacyImportPending && (
        <div className="migration-banner">
          <div>
            <strong>Your saved library is now shared with your team.</strong>{' '}
            We found local saved briefs from before this update. Import them to the shared library?
          </div>
          <div style={{ display: 'flex', gap: 8 }}>
            <button onClick={importLegacyLibrary} disabled={legacyImporting} className="migration-banner__btn migration-banner__btn--primary">
              {legacyImporting ? 'Importing…' : 'Import to shared library'}
            </button>
            <button onClick={dismissLegacyImport} disabled={legacyImporting} className="migration-banner__btn">
              Skip
            </button>
          </div>
        </div>
      )}

      {/* Concurrent-edit banner — a teammate just updated this brief in
          Supabase. Reload to see their changes; Dismiss to keep working on
          your version (your next Save will overwrite theirs). */}
      {remoteUpdate && (
        <div className="migration-banner" style={{ background: 'linear-gradient(90deg, #E8F4FF 0%, #DBEBFA 100%)', borderBottom: '1px solid #7CB1E0' }}>
          <div style={{ color: '#1F4F7A' }}>
            <strong>Teammate updated this brief.</strong>{' '}
            Reload to see their changes, or keep editing and your next Save will overwrite theirs.
          </div>
          <div style={{ display: 'flex', gap: 8 }}>
            <button onClick={reloadFromRemote} className="migration-banner__btn migration-banner__btn--primary" style={{ background: '#1F4F7A', color: 'white', borderColor: '#1F4F7A' }}>
              Reload latest
            </button>
            <button onClick={() => setRemoteUpdate(null)} className="migration-banner__btn" style={{ borderColor: '#7CB1E0', color: '#1F4F7A' }}>
              Keep mine
            </button>
          </div>
        </div>
      )}

      {/* TOP BAR */}
      <div className="topbar">
        <div className="brand">
          <img src="assets/Ventum-wordmark-black.png" alt="Ventum" />
          <div className="meta">Studio</div>
        </div>
        <div className="steps">
          {STEPS.map((s, i) => (
            <React.Fragment key={s.id}>
              <button
                className={"step" + (step === s.id ? " current" : step > s.id ? " done" : "")}
                disabled={s.id > step && !canProceed(step) && step < s.id}
                onClick={() => jumpTo(s.id)}
              >
                <span className="num">{String(s.id).padStart(2,'0')}</span>
                <span>{s.name}</span>
              </button>
              {i < STEPS.length - 1 && <span className="step-sep" />}
            </React.Fragment>
          ))}
        </div>
        <div className="topbar-actions">
          {autosaveState !== 'idle' && (
            <span className={"autosave-pill autosave-" + autosaveState} title="Autosaves your work to this browser">
              {autosaveState === 'dirty' && <><span className="autosave-dot" />Editing…</>}
              {autosaveState === 'saving' && <><span className="autosave-dot pulse" />Saving</>}
              {autosaveState === 'saved' && <><svg width="11" height="11" viewBox="0 0 16 16" fill="none" stroke="currentColor" strokeWidth="2"><path d="M3 8l3.5 3.5L13 5" /></svg>Saved</>}
            </span>
          )}
          <HelpCluster
            menuOpen={helpMenuOpen}
            setMenuOpen={setHelpMenuOpen}
            onOpenGuide={() => { setHelpMenuOpen(false); setHelpDrawerState('open'); }}
            onReplayTour={() => { setHelpMenuOpen(false); setTourActive(true); }}
          />
          <button
            className="icon-btn"
            onClick={async () => { await flushCloudAutosave(); setDashboardOpen(true); }}
            title="Team dashboard — open reviews, recent activity"
          >
            <span>Dashboard</span>
          </button>
          <button
            className="icon-btn"
            onClick={async () => {
              await flushCloudAutosave();
              reloadLibrary();
              setLibraryOpen(true);
            }}
            title="Open library (⌘L)"
          >
            <span>Library</span>
            {library.length > 0 && <span style={{ fontSize: 9, opacity: 0.6 }}>· {library.length}</span>}
          </button>
          <a
            className="icon-btn"
            href="Ventum Brand Guide.html"
            target="_blank"
            rel="noopener"
            title="Open the full Brand & Messaging Guide in a new tab"
          >
            <span className="dot" />
            <span>Brand Guide</span>
          </a>
          {authUser && (
            <AvatarMenu
              authUser={authUser}
              signOut={signOut}
              open={avatarMenuOpen}
              setOpen={setAvatarMenuOpen}
            />
          )}
        </div>
      </div>

      {/* STAGE */}
      <div className="stage">
        <div className="stage-inner">
          {step === 1 && (
            <window.Room1_Format
              intake={intake}
              setIntake={setIntake}
              allFormats={ALL_FORMATS}
              allGoals={ALL_GOALS}
              templates={templates}
              onUseTemplate={useTemplate}
            />
          )}
          {step === 2 && (
            <window.Room2_Audience
              intake={intake}
              setIntake={setIntake}
            />
          )}
          {step === 3 && (
            <window.Room3_Intel
              intake={intake}
              setIntake={setIntake}
            />
          )}
          {step === 4 && (
            <window.Room4_Strategy
              intake={intake}
              strategy={strategy}
              setStrategy={setStrategy}
              strategyStatus={strategyStatus}
              strategyError={strategyError}
              runStrategy={doRunStrategy}
              strategyStale={strategyStale}
              streamChars={streamProgress.strategy || 0}
            />
          )}
          {step === 5 && (
            <window.Room5_Generate
              intake={intake}
              setIntake={setIntake}
              strategy={strategy}
              artifacts={artifacts}
              setArtifacts={setArtifacts}
              agentStatus={agentStatus}
              agentErrors={agentErrors}
              runAgents={doRunCopyAll}
              regenerateOne={doRegenerateOne}
              regenerateCampaignPost={doRegenerateCampaignPost}
              runVariants={doRunVariants}
              runRefine={doRunRefine}
              openDeepEdit={(format) => setDeepEditFormat(format)}
              artifactStaleKeys={artifactStaleKeys}
              strategyStale={strategyStale}
              streamProgress={streamProgress}
            />
          )}
          {step === 6 && (
            <window.Room6_Export
              intake={intake}
              setIntake={setIntake}
              strategy={strategy}
              artifacts={artifacts}
              setArtifacts={setArtifacts}
              library={library}
              briefId={currentBriefId}
              authUser={authUser}
              onSaveLibrary={saveToLibrary}
              onSaveAsCopy={saveAsCopy}
              onSaveAsTemplate={saveAsTemplate}
              onNew={startNew}
            />
          )}
        </div>
      </div>

      {/* BOTTOM BAR */}
      <div className="bottombar">
        <div className="status">
          {step === 4 && strategyStatus === 'live' && <><span className="spinner" style={{ marginRight: 8 }} />Strategy Agent running</>}
          {step === 5 && Object.values(agentStatus).some(s => s === 'live') && <><span className="spinner" style={{ marginRight: 8 }} />Copy Agent running</>}
          {!(step === 4 && strategyStatus === 'live') && !(step === 5 && Object.values(agentStatus).some(s => s === 'live')) && (
            <span>{step}/6 · {STEPS[step-1].name}</span>
          )}
          {sessionCost > 0 && (
            <span className="mono" style={{ marginLeft: 14, fontSize: 10, letterSpacing: '0.12em', color: 'var(--ink-40)', textTransform: 'uppercase' }} title="Approximate API cost this session (rough estimate)">
              ~${sessionCost.toFixed(2)} session
            </span>
          )}
        </div>
        <div className="nav">
          <button className="btn-ghost" onClick={goBack} disabled={step === 1}>← Back</button>
          {step === 6 ? (
            <button className="btn-primary" onClick={startNew} title="Start a fresh brief">
              <span>+ New brief</span>
              <span className="arrow" />
            </button>
          ) : (
            <>
              {!canProceed(step) && missingForNext() && (
                <span className="mono" style={{ fontSize: 10, letterSpacing: '0.12em', color: 'var(--ink-40)', textTransform: 'uppercase', marginRight: 10 }} title="Required to advance">
                  · {missingForNext()}
                </span>
              )}
              <button className="btn-primary" onClick={goNext} disabled={!canProceed(step)} title={!canProceed(step) ? missingForNext() : ''}>
                <span>
                  {step === 3 ? 'Build strategy →' :
                   step === 4 ? 'Generate content →' :
                   step === 5 ? 'Review & export →' :
                   'Next'}
                </span>
                <span className="arrow" />
              </button>
            </>
          )}
        </div>
        <div className="nav-right">
          <button className="btn-ghost" onClick={startNew} title="Start a new piece">+ New</button>
        </div>
      </div>

      {/* BRAND DRAWER */}
      <div className={"drawer-mask" + (drawerOpen ? " open" : "")} onClick={() => setDrawerOpen(false)} />
      <div className={"drawer" + (drawerOpen ? " open" : "")}>
        <div className="drawer-head">
          <h3>Brand reference</h3>
          <button className="drawer-close" onClick={() => setDrawerOpen(false)}>×</button>
        </div>
        <div className="drawer-tabs">
          {['voice','pillars','language','claims','formats','imagery'].map(t => (
            <button key={t} className={drawerTab === t ? 'active' : ''} onClick={() => setDrawerTab(t)}>{t}</button>
          ))}
        </div>
        <div className="drawer-body">
          <BrandDrawerContent tab={drawerTab} />
        </div>
      </div>

      {/* LIBRARY MODAL */}
      {dashboardOpen && (
        <DashboardModal
          authUserId={authUser?.id}
          onClose={() => setDashboardOpen(false)}
          onOpenBrief={(brief) => restoreFromLibrary(brief)}
        />
      )}

      {libraryOpen && (
        <LibraryBrowser
          library={library}
          loading={libraryLoading}
          authUserId={authUser?.id}
          intake={intake}
          onClose={() => setLibraryOpen(false)}
          onRestore={restoreFromLibrary}
          onDuplicate={duplicateFromLibrary}
          onDelete={deleteFromLibrary}
          onImport={async (payload) => {
            if (!window.VentumStorage) return;
            try {
              await window.VentumStorage.saveBrief({
                title: deriveTitle(payload.intake || {}, payload.strategy),
                intake: payload.intake || {},
                strategy: payload.strategy || null,
                artifacts: payload.artifacts || {}
              });
              window.__toast('Briefcase imported to shared library');
              reloadLibrary();
            } catch (e) {
              console.error('briefcase import failed', e);
              window.__toast && window.__toast('Import failed — see console');
            }
          }}
        />
      )}

      {/* BATCH PERSONALIZE */}
      {window.BatchPersonalizeModal && (
        <window.BatchPersonalizeModal
          open={batchOpen}
          onClose={() => setBatchOpen(false)}
          intake={intake}
          onPushToLibrary={async (entry) => {
            if (!window.VentumStorage) return;
            try {
              await window.VentumStorage.saveBrief({
                title: deriveTitle(entry.intake || {}, entry.strategy),
                intake: entry.intake || {},
                strategy: entry.strategy || null,
                artifacts: entry.artifacts || {}
              });
              reloadLibrary();
            } catch (e) {
              console.error('batch push failed', e);
            }
          }}
        />
      )}

      {/* TOAST */}
      <div className={"toast" + (toast ? " show" : "")}>{toast}</div>

      {/* PDF render overlay — shown while the server-side headless
          Chromium renderer is producing a high-quality PDF. The flow
          takes ~2-5s (vs ~1s for html2canvas) so the user gets clear
          visual feedback that work is happening. Click-through is
          blocked so they can't accidentally fire a second render. */}
      {pdfRender && (
        <div className="pdf-render-overlay" role="status" aria-live="polite">
          <div className="pdf-render-card">
            <span className="spinner" />
            <div className="pdf-render-msg">Rendering high-quality PDF…</div>
            <div className="pdf-render-elapsed mono">{(pdfRender.elapsedMs / 1000).toFixed(1)}s</div>
            <div className="pdf-render-hint">Using server-side Chromium — pixel-perfect match to the preview.</div>
          </div>
        </div>
      )}

      {/* ONBOARDING TOUR */}
      {tourActive && <OnboardingTour onClose={() => finishTour(true)} onJumpToStep={(s) => { setStep(s); finishTour(false); }} />}

      {/* HELP / USER GUIDE DRAWER */}
      <HelpDrawer state={helpDrawerState} setState={setHelpDrawerState} />

      {/* DEEP EDIT OVERLAY */}
      {deepEditFormat && artifacts[deepEditFormat] && (
        <window.DeepEditOverlay
          format={deepEditFormat}
          artifact={artifacts[deepEditFormat]}
          intake={intake}
          strategy={strategy}
          onClose={() => setDeepEditFormat(null)}
          onUpdateIntake={(next) => setIntake(next)}
          onUpdateStrategy={(next) => setStrategy(next)}
          onUpdateArtifact={(next) => setArtifacts(prev => ({ ...prev, [deepEditFormat]: next }))}
          onRegenerate={() => doRegenerateOne(deepEditFormat)}
          onRefine={(fieldKey, currentValue, instruction) => doRunRefine(deepEditFormat, fieldKey, currentValue, instruction)}
          onVariants={(artifact) => doRunVariants(deepEditFormat, artifact)}
        />
      )}
    </div>
  );
}

function deriveTitle(intake, strategy) {
  const v = window.VENTUM_BRAND.verticals[intake.vertical]?.name || 'Untitled';
  const f = (intake.formats || []).map(k => window.VENTUM_BRAND.formats[k]?.name).filter(Boolean).slice(0,2).join(' + ');
  const a = intake.account_name ? ` · ${intake.account_name}` : '';
  return `${v} — ${f || 'piece'}${a}`;
}

// ============================================================
// ONBOARDING TOUR — 5-step intro shown on first visit.
// Saves a flag so it doesn't re-trigger. Replayable via the
// "?" button in the topbar.
// ============================================================
const TOUR_STEPS = [
  {
    title: 'Welcome to Ventum Studio',
    body: 'Brief once, ship everything. Six rooms walk you from goal → ready-to-export deliverables in about 5 minutes. Here\'s a quick tour.'
  },
  {
    title: 'Step 1–3 · The brief',
    body: 'Pick what you\'re making, who it\'s for, and paste the messy intel you have. The Strategy Agent will turn it into a sharp brief in Step 4.',
    tip: 'In a rush? On Step 1 there\'s a "Try the example brief" button that pre-fills everything for a Marriott hotel scenario.'
  },
  {
    title: 'Step 4 · The strategy brief',
    body: 'The Strategy Agent picks the angle, brand pillar, proof points, and CTA. You can edit or re-run.'
  },
  {
    title: 'Step 5 · Generate + refine',
    body: 'All deliverables draft in parallel. Every text field is inline-editable. Replace photos, swap layouts, compare 3 AI variants side-by-side, sync messaging across artifacts.',
    tip: 'Click any number in a chart to retype it. The bars rescale live.'
  },
  {
    title: 'Step 6 · Export',
    body: 'PDF (single artifact or all), pixel-perfect PPTX for decks, plain-text copy for email, or save the whole brief to your library to reopen later.'
  }
];

// ============================================================
// Help cluster — "?" button in the topbar that opens a compact
// popover with two entries: open the in-app User Guide (renders
// USER_GUIDE.md inside a side-docked drawer) or replay the
// onboarding tour. The popover footprint is intentionally small
// so it doesn't bleed into body content underneath.
// ============================================================
function HelpCluster({ menuOpen, setMenuOpen, onOpenGuide, onReplayTour }) {
  const btnRef = useRef(null);
  const menuRef = useRef(null);
  useEffect(() => {
    if (!menuOpen) return;
    const onDoc = (e) => {
      if (
        (btnRef.current && btnRef.current.contains(e.target)) ||
        (menuRef.current && menuRef.current.contains(e.target))
      ) return;
      setMenuOpen(false);
    };
    const onKey = (e) => { if (e.key === 'Escape') setMenuOpen(false); };
    document.addEventListener('mousedown', onDoc);
    window.addEventListener('keydown', onKey);
    return () => {
      document.removeEventListener('mousedown', onDoc);
      window.removeEventListener('keydown', onKey);
    };
  }, [menuOpen, setMenuOpen]);
  return (
    <span className="help-cluster" style={{ position: 'relative', display: 'inline-flex' }}>
      <button
        ref={btnRef}
        className="icon-btn"
        onClick={() => setMenuOpen(v => !v)}
        title="Help — user guide and tour"
        style={{ padding: '6px 9px' }}
        aria-haspopup="menu"
        aria-expanded={menuOpen}
      >?</button>
      {menuOpen && (
        <>
          {/* Lightweight click-catcher behind the popover so it dismisses
              cleanly when the user touches anywhere else on the page. */}
          <div className="help-menu-scrim" onClick={() => setMenuOpen(false)} />
          <div ref={menuRef} className="help-menu" role="menu">
            <button role="menuitem" className="help-menu__item" onClick={onOpenGuide}>
              User guide
            </button>
            <button role="menuitem" className="help-menu__item" onClick={onReplayTour}>
              Replay onboarding tour
            </button>
          </div>
        </>
      )}
    </span>
  );
}

// ============================================================
// AvatarMenu — circular initials button in the topbar. Opens a
// small popover showing the signed-in account (the "profile" at a
// glance) plus a Sign out action. Replaces the old email pill so the
// topbar stays compact.
// ============================================================
function AvatarMenu({ authUser, signOut, open, setOpen }) {
  const btnRef = useRef(null);
  const menuRef = useRef(null);
  useEffect(() => {
    if (!open) return;
    const onDoc = (e) => {
      if (
        (btnRef.current && btnRef.current.contains(e.target)) ||
        (menuRef.current && menuRef.current.contains(e.target))
      ) return;
      setOpen(false);
    };
    const onKey = (e) => { if (e.key === 'Escape') setOpen(false); };
    document.addEventListener('mousedown', onDoc);
    window.addEventListener('keydown', onKey);
    return () => {
      document.removeEventListener('mousedown', onDoc);
      window.removeEventListener('keydown', onKey);
    };
  }, [open, setOpen]);

  const email = (authUser && authUser.email) || '';
  const local = email.split('@')[0] || '';
  const parts = local.split(/[.\-_]+/).filter(Boolean);
  const initials = (parts.length >= 2
    ? parts[0][0] + parts[1][0]
    : (local.slice(0, 2) || '?')).toUpperCase();

  return (
    <span className="avatar-menu" style={{ position: 'relative', display: 'inline-flex' }}>
      <button
        ref={btnRef}
        className="avatar-btn"
        onClick={() => setOpen(v => !v)}
        title={email}
        aria-haspopup="menu"
        aria-expanded={open}
      >{initials}</button>
      {open && (
        <>
          <div className="help-menu-scrim" onClick={() => setOpen(false)} />
          <div ref={menuRef} className="avatar-dropdown" role="menu">
            <div className="avatar-dropdown__head">
              <span className="avatar-btn avatar-btn--lg" aria-hidden="true">{initials}</span>
              <div className="avatar-dropdown__id">
                <div className="avatar-dropdown__email" title={email}>{email}</div>
                <div className="avatar-dropdown__sub">Signed in</div>
              </div>
            </div>
            <button
              role="menuitem"
              className="avatar-signout"
              onClick={() => { setOpen(false); signOut(); }}
            >
              <svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round" aria-hidden="true">
                <path d="M9 21H5a2 2 0 0 1-2-2V5a2 2 0 0 1 2-2h4" />
                <polyline points="16 17 21 12 16 7" />
                <line x1="21" y1="12" x2="9" y2="12" />
              </svg>
              Sign out
            </button>
          </div>
        </>
      )}
    </span>
  );
}

// ============================================================
// HelpDrawer — side-docked panel that fetches USER_GUIDE.md
// and renders it via marked.js. The .md file is the source of
// truth; updating it and pushing flows through to the platform
// without touching this component.
//
// State machine:
//   'closed'    — not visible
//   'open'      — ~480px-wide panel pinned to the right edge.
//                 No backdrop mask, so the operator can keep
//                 clicking around the studio while it's up.
//   'collapsed' — a thin ~44px strip on the right edge with the
//                 rotated "USER GUIDE" label + expand affordance.
//                 Click anywhere on the strip to reopen.
// ============================================================
function HelpDrawer({ state, setState }) {
  const [html, setHtml] = useState('');
  const [loading, setLoading] = useState(false);
  const [error, setError] = useState(null);
  const fetched = useRef(false);
  const bodyRef = useRef(null);

  const visible = state !== 'closed';

  useEffect(() => {
    if (!visible || fetched.current) return;
    fetched.current = true;
    setLoading(true);
    fetch('USER_GUIDE.md?v=' + Date.now(), { cache: 'no-store' })
      .then((r) => {
        if (!r.ok) throw new Error('HTTP ' + r.status);
        return r.text();
      })
      .then((md) => {
        if (!window.marked) throw new Error('marked.js failed to load');
        // marked.parse returns HTML. Source is the repo's own USER_GUIDE.md,
        // not user-controlled input, so sanitization is not strictly needed.
        setHtml(window.marked.parse(md));
        setLoading(false);
      })
      .catch((e) => {
        console.error('[help-drawer] load failed', e);
        setError(String(e.message || e));
        setLoading(false);
      });
  }, [visible]);

  useEffect(() => {
    if (state !== 'open') return;
    if (bodyRef.current) bodyRef.current.scrollTop = 0;
  }, [state]);

  useEffect(() => {
    if (state === 'closed') return;
    const onKey = (e) => { if (e.key === 'Escape') setState('closed'); };
    window.addEventListener('keydown', onKey);
    return () => window.removeEventListener('keydown', onKey);
  }, [state, setState]);

  // Reflect drawer state on <body> so global CSS can adjust the
  // studio chrome (e.g. pull the bottom bar in by the drawer width).
  useEffect(() => {
    document.body.setAttribute('data-help-drawer', state);
    return () => document.body.removeAttribute('data-help-drawer');
  }, [state]);

  if (state === 'closed') return null;

  if (state === 'collapsed') {
    return (
      <div
        className="help-drawer-strip"
        onClick={() => setState('open')}
        role="button"
        tabIndex={0}
        title="Expand user guide"
        aria-label="Expand user guide"
        onKeyDown={(e) => { if (e.key === 'Enter' || e.key === ' ') setState('open'); }}
      >
        <span className="help-drawer-strip__chevron" aria-hidden="true">‹</span>
        <span className="help-drawer-strip__label mono">USER GUIDE</span>
        <button
          className="help-drawer-strip__close"
          onClick={(e) => { e.stopPropagation(); setState('closed'); }}
          aria-label="Close user guide"
          title="Close user guide"
        >×</button>
      </div>
    );
  }

  // state === 'open'
  return (
    <div className="drawer help-drawer help-drawer--docked open" role="complementary" aria-label="User guide">
      <div className="drawer-head help-drawer__head">
        <h3>User Guide</h3>
        <div className="help-drawer__head-actions">
          <button
            className="help-drawer__btn"
            onClick={() => setState('collapsed')}
            title="Collapse to right edge (keep available)"
            aria-label="Collapse user guide"
          >›</button>
          <button
            className="drawer-close"
            onClick={() => setState('closed')}
            title="Close user guide"
            aria-label="Close user guide"
          >×</button>
        </div>
      </div>
      <div ref={bodyRef} className="drawer-body user-guide-body">
        {loading && (
          <div className="muted" style={{ fontSize: 13 }}>Loading guide…</div>
        )}
        {error && (
          <div style={{ fontSize: 13, color: 'var(--ink-80)' }}>
            <strong style={{ color: 'var(--ink-100)' }}>Could not load the user guide.</strong>
            <br />{error}
            <br /><span className="muted">Try refreshing the page. If it keeps failing, tell Jeff.</span>
          </div>
        )}
        {!loading && !error && (
          <div dangerouslySetInnerHTML={{ __html: html }} />
        )}
      </div>
    </div>
  );
}

function OnboardingTour({ onClose, onJumpToStep }) {
  const [i, setI] = useState(0);
  const total = TOUR_STEPS.length;
  const step = TOUR_STEPS[i];
  return (
    <div className="tour-mask" onClick={(e) => { if (e.target === e.currentTarget) onClose(); }}>
      <div className="tour-card">
        <div className="tour-card__head">
          <div className="mono" style={{ fontSize: 10, color: 'var(--ink-40)', letterSpacing: '0.18em', textTransform: 'uppercase' }}>{i+1} of {total} · Quick tour</div>
          <button className="drawer-close" onClick={onClose} aria-label="Skip tour">×</button>
        </div>
        <h2 className="tour-card__title">{step.title}</h2>
        <p className="tour-card__body">{step.body}</p>
        {step.tip && (
          <div className="tour-card__tip">
            <span className="mono">TIP</span>
            <span>{step.tip}</span>
          </div>
        )}
        <div className="tour-card__dots">
          {TOUR_STEPS.map((_, idx) => (
            <button
              key={idx}
              className={"tour-dot" + (idx === i ? ' is-active' : '') + (idx < i ? ' is-done' : '')}
              onClick={() => setI(idx)}
              aria-label={`Step ${idx + 1}`}
            />
          ))}
        </div>
        <div className="tour-card__actions">
          <button className="btn-ghost" onClick={onClose}>Skip</button>
          <div style={{ display: 'flex', gap: 8 }}>
            {i > 0 && <button className="btn-ghost" onClick={() => setI(i-1)}>← Back</button>}
            {i < total - 1 && <button className="btn-primary" onClick={() => setI(i+1)}>Next →</button>}
            {i === total - 1 && (
              <button className="btn-primary" onClick={() => onJumpToStep(1)}>Let's go →</button>
            )}
          </div>
        </div>
      </div>
    </div>
  );
}

// ============================================================
// LIBRARY BROWSER — searchable, with thumbnails and import
// ============================================================
// ============================================================
// OPERATIONAL DASHBOARD — landing for the busy operator.
//
// Three columns:
//   · Open reviews — every brief with an in_review / changes_requested
//     approval, jump-to button per row
//   · Recent comments — last 7 days of team discussion, attributed
//   · Recent edits — briefs touched in the last 7 days, who/when
//
// All queries hit Supabase fresh on open. No persistent state.
// ============================================================
function DashboardModal({ authUserId, onClose, onOpenBrief }) {
  const [loading, setLoading] = useState(true);
  const [reviewBriefs, setReviewBriefs] = useState([]); // [{brief, formats: [{format,status,...}]}]
  const [comments, setComments] = useState([]);
  const [recentEdits, setRecentEdits] = useState([]);
  const [profilesById, setProfilesById] = useState({});

  useEffect(() => {
    let cancelled = false;
    (async () => {
      if (!window.VentumStorage) { setLoading(false); return; }
      const [reviewMap, recentComments, recentlyEdited] = await Promise.all([
        window.VentumStorage.listInReviewBriefs(),
        window.VentumStorage.listRecentComments(168),
        window.VentumStorage.listRecentlyEditedBriefs(168)
      ]);
      if (cancelled) return;
      // Hydrate briefs for the review map
      const briefIds = Object.keys(reviewMap || {});
      const briefRows = await Promise.all(briefIds.map(id => window.VentumStorage.getBriefById(id)));
      const reviewList = briefRows
        .filter(Boolean)
        .map(b => ({ brief: b, formats: reviewMap[b.id] || [] }))
        .sort((a, b) => new Date(b.brief.updated_at) - new Date(a.brief.updated_at));
      // Collect author IDs from comments
      const commentAuthorIds = [...new Set((recentComments || []).map(c => c.author_id).filter(Boolean))];
      let profs = {};
      if (commentAuthorIds.length && window.ventumSupabase) {
        const { data } = await window.ventumSupabase.from('profiles').select('id, email').in('id', commentAuthorIds);
        (data || []).forEach(p => { profs[p.id] = p; });
      }
      setReviewBriefs(reviewList);
      setComments(recentComments);
      setRecentEdits(recentlyEdited);
      setProfilesById(profs);
      setLoading(false);
    })();
    return () => { cancelled = true; };
  }, []);

  const ago = (iso) => {
    if (!iso) return '';
    const m = Math.floor((Date.now() - new Date(iso).getTime()) / 60000);
    if (m < 1) return 'just now';
    if (m < 60) return m + 'm ago';
    const h = Math.floor(m / 60);
    if (h < 24) return h + 'h ago';
    return Math.floor(h / 24) + 'd ago';
  };
  const handle = (email) => (email || '').split('@')[0];

  // Roll-ups for the summary tiles + the prioritized "needs attention" queue.
  const allReviewFormats = reviewBriefs.flatMap(({ brief, formats }) =>
    (formats || []).map((f) => ({ ...f, brief })));
  const needsAttention = allReviewFormats.filter((f) => f.status === 'changes_requested');
  const yourEdits = recentEdits.filter((b) => b._last_edited_by === authUserId).length;
  const stats = [
    { label: 'Needs attention', value: needsAttention.length, tone: 'alert', hint: 'changes requested' },
    { label: 'In review', value: reviewBriefs.length, tone: 'warn', hint: 'awaiting sign-off' },
    { label: 'Edited this week', value: recentEdits.length, tone: 'teal', hint: yourEdits + ' by you' },
    { label: 'Comments this week', value: comments.length, tone: 'navy', hint: 'across the team' },
  ];

  return (
    <div className="modal-mask" onClick={(e) => { if (e.target === e.currentTarget) onClose(); }}>
      <div className="modal dash-modal" style={{ width: 'min(1120px, 96vw)', maxHeight: 'calc(100vh - 60px)', display: 'flex', flexDirection: 'column' }}>
        <div className="modal-head dash-head">
          <div>
            <h3>Team dashboard</h3>
            <span className="mono dash-head__sub">What needs attention · last 7 days</span>
          </div>
          <button className="drawer-close" onClick={onClose}>×</button>
        </div>
        <div className="modal-body dash-body" style={{ overflowY: 'auto' }}>
          {loading && <div className="empty"><h2>Loading…</h2></div>}
          {!loading && (
            <>
              {/* Summary tiles */}
              <div className="dash-stats">
                {stats.map((s) => (
                  <div key={s.label} className={'dash-stat dash-stat--' + s.tone}>
                    <div className="dash-stat__value">{s.value}</div>
                    <div className="dash-stat__label">{s.label}</div>
                    <div className="dash-stat__hint mono">{s.hint}</div>
                  </div>
                ))}
              </div>

              {/* Needs attention — changes-requested formats, most recent first */}
              {needsAttention.length > 0 && (
                <div className="dash-section">
                  <div className="dash-section__title mono">Needs attention · {needsAttention.length}</div>
                  <div className="dash-attn-grid">
                    {needsAttention.slice(0, 8).map((f, i) => (
                      <button key={i} className="dash-attn" onClick={() => { onOpenBrief(f.brief); onClose(); }}>
                        <span className="dash-attn__chip mono">Changes requested</span>
                        <div className="dash-attn__title">{f.brief.title}</div>
                        <div className="dash-attn__meta mono">{f.format} · {handle(f.brief._editor_email) || 'team'} · {ago(f.brief.updated_at)}</div>
                      </button>
                    ))}
                  </div>
                </div>
              )}

              <div className="dash-cols">
                {/* Open reviews */}
                <div className="dash-section">
                  <div className="dash-section__title mono">Open reviews · {reviewBriefs.length}</div>
                  {reviewBriefs.length === 0 && <div className="dash-empty">Nothing in review. You're clear.</div>}
                  {reviewBriefs.slice(0, 12).map(({ brief, formats }) => (
                    <button key={brief.id} className="dash-card" onClick={() => { onOpenBrief(brief); onClose(); }}>
                      <div className="dash-card__title">{brief.title}</div>
                      <div className="dash-card__meta mono">{formats.length} format{formats.length === 1 ? '' : 's'} · {handle(brief._editor_email) || 'team'} · {ago(brief.updated_at)}</div>
                      <div className="dash-card__chips">
                        {formats.slice(0, 6).map((f, i) => (
                          <span key={i} className={'dash-fchip mono ' + (f.status === 'changes_requested' ? 'is-alert' : 'is-review')}>{f.format}</span>
                        ))}
                      </div>
                    </button>
                  ))}
                </div>

                {/* Recent activity — comments + edits */}
                <div className="dash-section">
                  <div className="dash-section__title mono">Recent activity</div>
                  {comments.length === 0 && recentEdits.length === 0 && <div className="dash-empty">Quiet week so far.</div>}
                  {comments.slice(0, 10).map((c) => {
                    const authorEmail = c.author_name || profilesById[c.author_id]?.email;
                    return (
                      <div key={c.id} className="dash-activity dash-activity--comment">
                        <div className="dash-activity__head mono"><span>{handle(authorEmail) || 'anon'} · {c.format}</span><span>{ago(c.created_at)}</span></div>
                        <div className="dash-activity__body">{c.body}</div>
                      </div>
                    );
                  })}
                  {recentEdits.slice(0, 8).map((brief) => (
                    <button key={brief.id} className="dash-activity dash-activity--edit" onClick={() => { onOpenBrief(brief); onClose(); }}>
                      <div className="dash-activity__head mono"><span>{brief._last_edited_by === authUserId ? 'you' : handle(brief._editor_email) || 'team'} edited</span><span>{ago(brief.updated_at)}</span></div>
                      <div className="dash-activity__body">{brief.title}</div>
                    </button>
                  ))}
                </div>
              </div>
            </>
          )}
        </div>
      </div>
    </div>
  );
}

const LIB_VISUAL_FMTS = new Set(['one_pager', 'account_brief', 'brochure', 'print_ad', 'web_hero', 'trade_show_banner', 'infographic']);
const LIB_DECK_FMTS = new Set(['deck', 'deck_slide']);

function briefHeadline(art, fallback) {
  if (!art) return fallback || '';
  const s0 = Array.isArray(art.slides) ? art.slides[0] : null;
  return (
    art.headline || art.hero_title || art.title || art.subject || art.hook ||
    (s0 && (s0.title || s0.headline || s0.eyebrow)) || art.eyebrow || fallback || ''
  );
}

// Schematic preview — a clean, format-aware placeholder (hero band + headline /
// slide / text lines). Shown instantly and as the fallback if a real capture
// can't be produced. Built from plain divs so it always renders cleanly.
function SchematicPreview({ category, headline, accountName }) {
  if (category === 'deck') {
    return (
      <div className="lib-schema lib-schema--deck">
        <span className="lib-schema__kicker" />
        <div className="lib-schema__dtitle">{headline}</div>
        <div className="lib-schema__cols"><i /><i /><i /></div>
      </div>
    );
  }
  if (category === 'visual') {
    return (
      <div className="lib-schema lib-schema--visual">
        <div className="lib-schema__hero">
          <span className="lib-schema__brand mono">VENTUM</span>
          {accountName && <span className="lib-schema__prepared mono">Prepared for {accountName}</span>}
        </div>
        <div className="lib-schema__head">{headline}</div>
        <div className="lib-schema__benefits"><i /><i /><i /></div>
      </div>
    );
  }
  return (
    <div className="lib-schema lib-schema--text">
      <span className="lib-schema__brand mono">VENTUM</span>
      <div className="lib-schema__thead">{headline}</div>
      <div className="lib-schema__lines"><span /><span /><span /></div>
      <span className="lib-schema__cta" />
    </div>
  );
}

// --- Real rendered thumbnails --------------------------------------------
// Capture the actual artifact to a PNG once (off-screen mount + html2canvas,
// the same proven path the PPTX screenshot export uses), cache it, and show it
// as a plain <img>. Queued (2 at a time) so scrolling the library doesn't spin
// up dozens of heavy captures at once. Falls back to the schematic on failure.
const THUMB_CACHE = new Map();
let THUMB_ACTIVE = 0;
const THUMB_QUEUE = [];
function runThumbQueue() {
  while (THUMB_ACTIVE < 2 && THUMB_QUEUE.length) {
    const job = THUMB_QUEUE.shift();
    THUMB_ACTIVE++;
    Promise.resolve().then(job).finally(() => { THUMB_ACTIVE--; runThumbQueue(); });
  }
}
function queueThumb(fn) {
  return new Promise((resolve) => {
    THUMB_QUEUE.push(() => fn().then(resolve, () => resolve(null)));
    runThumbQueue();
  });
}
async function captureArtifactThumb(format, artifact, intake, strategy) {
  if (typeof html2canvas === 'undefined' || typeof ReactDOM === 'undefined' || !window.ArtifactRender) return null;
  const HOST_W = 900;
  const host = document.createElement('div');
  host.setAttribute('data-thumb-host', 'true');
  host.style.cssText = 'position:fixed;left:-99999px;top:0;width:' + HOST_W + 'px;background:#fff;z-index:-1;pointer-events:none;';
  document.body.appendChild(host);
  const target = document.createElement('div');
  host.appendChild(target);
  let unmount = () => {};
  try {
    if (ReactDOM.createRoot) {
      const root = ReactDOM.createRoot(target);
      root.render(React.createElement(window.ArtifactRender, { format, artifact, onPatch: () => {}, intake: intake || {}, strategy: strategy || {} }));
      unmount = () => { try { root.unmount(); } catch (_) {} };
    } else {
      ReactDOM.render(React.createElement(window.ArtifactRender, { format, artifact, onPatch: () => {}, intake: intake || {}, strategy: strategy || {} }), target);
      unmount = () => { try { ReactDOM.unmountComponentAtNode(target); } catch (_) {} };
    }
    await new Promise((r) => requestAnimationFrame(() => requestAnimationFrame(r)));
    if (document.fonts && document.fonts.ready) { try { await document.fonts.ready; } catch (_) {} }
    await new Promise((r) => setTimeout(r, 300));
    const imgs = target.querySelectorAll('img');
    await Promise.all([...imgs].map((img) => img.complete ? Promise.resolve() : new Promise((res) => {
      img.addEventListener('load', res, { once: true });
      img.addEventListener('error', res, { once: true });
    })));
    await new Promise((r) => setTimeout(r, 120));
    const node = target.firstElementChild || target;
    const w = node.offsetWidth || HOST_W;
    // Capture only the top strip (preview aspect ≈ 1.7) to keep the PNG small.
    const h = Math.min(node.offsetHeight || w, Math.round(w / 1.7));
    const canvas = await html2canvas(node, {
      backgroundColor: '#ffffff',
      scale: 1,
      useCORS: true,
      allowTaint: false,
      logging: false,
      width: w,
      height: h,
      windowWidth: HOST_W
    });
    return canvas.toDataURL('image/png');
  } catch (e) {
    console.warn('[VENTUM_THUMB] capture failed:', e?.message || e);
    return null;
  } finally {
    unmount();
    host.remove();
  }
}

function LibraryThumb({ entry, onOpen, variant = 'card' }) {
  const firstFmt = (entry.intake?.formats || []).find(f => entry.artifacts?.[f]) || Object.keys(entry.artifacts || {})[0] || (entry.intake?.formats || [])[0];
  const fmtName = window.VENTUM_BRAND?.formats?.[firstFmt]?.name || firstFmt || 'Brief';
  const fmtAbbrev = window.VENTUM_BRAND?.formats?.[firstFmt]?.icon || (firstFmt || 'BR').slice(0, 3).toUpperCase();
  const art = firstFmt ? entry.artifacts?.[firstFmt] : null;
  const headline = briefHeadline(art, entry.title);
  const accountName = entry.intake?.account_name || '';
  const category = LIB_VISUAL_FMTS.has(firstFmt) ? 'visual' : (LIB_DECK_FMTS.has(firstFmt) ? 'deck' : 'text');
  const cacheKey = (entry.id || entry.title || '') + ':' + firstFmt;
  const isRow = variant === 'row';

  const rootRef = useRef(null);
  const [visible, setVisible] = useState(false);
  const [shot, setShot] = useState(() => THUMB_CACHE.get(cacheKey) || null);

  // Only capture once the card scrolls into view.
  useEffect(() => {
    if (!rootRef.current || shot) return;
    if (!('IntersectionObserver' in window)) { setVisible(true); return; }
    const io = new IntersectionObserver((es) => {
      if (es.some((e) => e.isIntersecting)) { setVisible(true); io.disconnect(); }
    }, { rootMargin: '300px' });
    io.observe(rootRef.current);
    return () => io.disconnect();
  }, [shot]);

  useEffect(() => {
    if (shot || !visible || !art || !window.ArtifactRender) return;
    let alive = true;
    queueThumb(() => captureArtifactThumb(firstFmt, art, entry.intake || {}, entry.strategy || {}))
      .then((url) => {
        if (!url) return;
        THUMB_CACHE.set(cacheKey, url);
        if (alive) setShot(url);
      });
    return () => { alive = false; };
  }, [visible, shot]);

  return (
    <div
      ref={rootRef}
      className={'library-card__preview' + (isRow ? ' library-card__preview--row' : '')}
      role="button"
      tabIndex={0}
      onClick={onOpen}
      onKeyDown={(e) => { if (e.key === 'Enter' || e.key === ' ') { e.preventDefault(); onOpen(); } }}
      title={'Open · ' + fmtName}
    >
      {shot
        ? <img className="library-card__shot" src={shot} alt={headline || fmtName} />
        : (isRow
            ? <div className="library-thumb-mini mono">{fmtAbbrev}</div>
            : <SchematicPreview category={category} headline={headline} accountName={accountName} />)}
      {!isRow && <span className="library-card__preview-open">Open →</span>}
    </div>
  );
}

// Shared per-entry display metadata for both the gallery card and list row.
function libEntryMeta(entry, authUserId) {
  const isOwn = authUserId && entry._created_by === authUserId;
  const goalName = (ALL_GOALS.find(g => g.key === entry.intake?.goal) || {}).name || '';
  const verticalName = entry.intake?.vertical ? (window.VENTUM_BRAND.verticals[entry.intake.vertical]?.name || entry.intake.vertical) : '';
  const savedDate = entry.saved_at ? new Date(entry.saved_at).toLocaleDateString(undefined, { month: 'short', day: 'numeric' }) : '';
  const authorName = isOwn ? 'you' : (entry._author_email ? entry._author_email.split('@')[0] : '');
  const fmtList = entry.intake?.formats || [];
  let editedAgo = null;
  if (entry._editor_email && entry.updated_at && entry._last_edited_by !== entry._created_by) {
    const ms = Date.now() - new Date(entry.updated_at).getTime();
    const m = Math.floor(ms / 60000);
    editedAgo = m < 1 ? 'just now' : (m < 60 ? m + 'm ago' : (Math.floor(m / 60) < 24 ? Math.floor(m / 60) + 'h ago' : Math.floor(m / 1440) + 'd ago'));
  }
  return { isOwn, goalName, verticalName, savedDate, authorName, fmtList, editedAgo };
}

function DeleteConfirm({ entry, confirmDelete, setConfirmDelete, onDelete }) {
  if (confirmDelete !== entry.id) return null;
  return (
    <div className="library-card__confirm" onClick={(e) => e.stopPropagation()}>
      <span>Delete "{entry.title}"?</span>
      <button className="btn-ghost" style={{ fontSize: 10, padding: '4px 9px' }} onClick={() => setConfirmDelete(null)}>Cancel</button>
      <button
        style={{ fontSize: 10, padding: '4px 9px', background: '#C0392B', color: 'white', border: 'none', borderRadius: 2, cursor: 'pointer' }}
        onClick={() => { onDelete(entry.id); setConfirmDelete(null); }}
      >Delete</button>
    </div>
  );
}

// Gallery card — thumbnail on top, full metadata below.
function LibraryCard({ entry, authUserId, onRestore, onDuplicate, onDelete, confirmDelete, setConfirmDelete }) {
  const { isOwn, goalName, verticalName, savedDate, authorName, fmtList, editedAgo } = libEntryMeta(entry, authUserId);
  const forBits = [goalName, verticalName].filter(Boolean);
  return (
    <div className="library-card">
      <LibraryThumb entry={entry} onOpen={() => onRestore(entry)} />
      <div className="library-card__body">
        {fmtList.length > 0 && (
          <div className="library-card__formats">
            {fmtList.slice(0, 4).map((f, i) => (
              <span key={i} className="library-card__fmt mono">{window.VENTUM_BRAND.formats[f]?.name || f}</span>
            ))}
            {fmtList.length > 4 && <span className="library-card__fmt library-card__fmt--more mono">+{fmtList.length - 4}</span>}
          </div>
        )}
        <div className="library-card__title" onClick={() => onRestore(entry)} role="button" tabIndex={0} onKeyDown={(e) => { if (e.key === 'Enter' || e.key === ' ') { e.preventDefault(); onRestore(entry); } }}>{entry.title}</div>
        {entry.intake?.account_name && <div className="library-card__account mono">{entry.intake.account_name}</div>}
        {forBits.length > 0 && <div className="library-card__for"><b>For:</b> {forBits.join(' · ')}</div>}
        {entry.strategy?.angle && <div className="library-card__desc">{entry.strategy.angle}</div>}
        <div className="library-card__foot">
          <span className="library-card__meta-sm mono">{savedDate}{authorName ? ' · ' + authorName : ''}{editedAgo ? ' · edited ' + editedAgo : ''}</span>
          <span className="library-card__foot-spacer" />
          <button className="btn-primary library-card__open" onClick={() => onRestore(entry)}>Open</button>
          <button className="btn-ghost library-card__dup" onClick={() => onDuplicate(entry)} title="Open a copy as a new brief">Duplicate</button>
          {isOwn && <button className="library-card__delete" onClick={() => setConfirmDelete(entry.id)} title="Delete this saved brief" aria-label="Delete">×</button>}
        </div>
        <DeleteConfirm entry={entry} confirmDelete={confirmDelete} setConfirmDelete={setConfirmDelete} onDelete={onDelete} />
      </div>
    </div>
  );
}

// Dense list row — scan many at once.
function LibraryRow({ entry, authUserId, onRestore, onDuplicate, onDelete, confirmDelete, setConfirmDelete }) {
  const { isOwn, goalName, verticalName, savedDate, authorName, fmtList, editedAgo } = libEntryMeta(entry, authUserId);
  return (
    <div className="lib-row" onClick={() => onRestore(entry)} role="button" tabIndex={0} onKeyDown={(e) => { if (e.key === 'Enter') onRestore(entry); }}>
      <LibraryThumb entry={entry} onOpen={() => onRestore(entry)} variant="row" />
      <div className="lib-row__main">
        <div className="lib-row__title">{entry.title}</div>
        <div className="lib-row__sub mono">{entry.intake?.account_name || '—'}{savedDate ? ' · ' + savedDate : ''}{authorName ? ' · ' + authorName : ''}{editedAgo ? ' · edited ' + editedAgo : ''}</div>
      </div>
      <div className="lib-row__cell">{verticalName && <span className="lib-row__vchip">{verticalName}</span>}</div>
      <div className="lib-row__cell lib-row__fmts">
        {fmtList.slice(0, 3).map((f, i) => <span key={i} className="lib-row__fmt mono">{window.VENTUM_BRAND.formats[f]?.icon || f.slice(0, 3).toUpperCase()}</span>)}
        {fmtList.length > 3 && <span className="lib-row__fmt lib-row__fmt--more mono">+{fmtList.length - 3}</span>}
      </div>
      <div className="lib-row__cell lib-row__goal mono">{goalName || '—'}</div>
      <div className="lib-row__actions" onClick={(e) => e.stopPropagation()}>
        <button className="btn-primary" style={{ fontSize: 10.5, padding: '5px 11px' }} onClick={() => onRestore(entry)}>Open</button>
        <button className="btn-ghost" style={{ fontSize: 10.5, padding: '5px 9px' }} onClick={() => onDuplicate(entry)} title="Open a copy">Dup</button>
        {isOwn && <button className="library-card__delete" onClick={() => setConfirmDelete(entry.id)} title="Delete" aria-label="Delete">×</button>}
      </div>
      <DeleteConfirm entry={entry} confirmDelete={confirmDelete} setConfirmDelete={setConfirmDelete} onDelete={onDelete} />
    </div>
  );
}

// Multi-select facet dropdown (Vertical / Format / Goal) with counts.
function FacetDropdown({ label, options, selected, onToggle, onClear }) {
  const [open, setOpen] = useState(false);
  const n = selected.size;
  return (
    <div className="lib-dd-wrap">
      <button className={'lib-dd' + (n ? ' is-active' : '')} onClick={() => setOpen((o) => !o)}>
        {label}{n ? <b> · {n}</b> : ''}<span className="lib-dd__caret">▾</span>
      </button>
      {open && (
        <React.Fragment>
          <div className="lib-dd__backdrop" onClick={() => setOpen(false)} />
          <div className="lib-dd__panel">
            {options.length === 0 && <div className="lib-dd__empty">Nothing to filter</div>}
            {options.map((o) => (
              <label key={o.key} className="lib-dd__opt">
                <input type="checkbox" checked={selected.has(o.key)} onChange={() => onToggle(o.key)} />
                <span className="lib-dd__name">{o.name}</span>
                <span className="lib-dd__count mono">{o.count}</span>
              </label>
            ))}
            {n > 0 && <button className="lib-dd__clear" onClick={onClear}>Clear</button>}
          </div>
        </React.Fragment>
      )}
    </div>
  );
}

function LibraryBrowser({ library, loading = false, authUserId = null, onClose, onRestore, onDuplicate, onDelete, onImport }) {
  const [q, setQ] = useState('');
  const [confirmDelete, setConfirmDelete] = useState(null);
  const [dragOver, setDragOver] = useState(false);
  const fileInputRef = useRef(null);
  const lsGet = (k, d) => { try { return localStorage.getItem(k) || d; } catch (_) { return d; } };
  const [view, setView] = useState(() => lsGet('ventum_lib_view', 'list'));
  const [sort, setSort] = useState(() => lsGet('ventum_lib_sort', 'recent'));
  const [groupBy, setGroupBy] = useState(() => lsGet('ventum_lib_group', 'vertical'));
  const [facets, setFacets] = useState({ vertical: new Set(), format: new Set(), goal: new Set() });
  useEffect(() => { try { localStorage.setItem('ventum_lib_view', view); } catch (_) {} }, [view]);
  useEffect(() => { try { localStorage.setItem('ventum_lib_sort', sort); } catch (_) {} }, [sort]);
  useEffect(() => { try { localStorage.setItem('ventum_lib_group', groupBy); } catch (_) {} }, [groupBy]);

  const toggleFacet = (kind, key) => setFacets((prev) => {
    const next = { vertical: new Set(prev.vertical), format: new Set(prev.format), goal: new Set(prev.goal) };
    if (next[kind].has(key)) next[kind].delete(key); else next[kind].add(key);
    return next;
  });
  const clearFacet = (kind) => setFacets((prev) => ({ ...prev, [kind]: new Set() }));
  const clearAllFacets = () => setFacets({ vertical: new Set(), format: new Set(), goal: new Set() });
  const activeFacetCount = facets.vertical.size + facets.format.size + facets.goal.size;

  // Facet option lists with live counts.
  const vertOpts = useMemo(() => {
    const m = new Map();
    library.forEach((e) => { const k = e.intake?.vertical; if (k) m.set(k, (m.get(k) || 0) + 1); });
    return [...m.entries()].map(([k, c]) => ({ key: k, name: window.VENTUM_BRAND.verticals[k]?.name || k, count: c })).sort((a, b) => a.name.localeCompare(b.name));
  }, [library]);
  const goalOpts = useMemo(() => {
    const m = new Map();
    library.forEach((e) => { const k = e.intake?.goal; if (k) m.set(k, (m.get(k) || 0) + 1); });
    return [...m.entries()].map(([k, c]) => ({ key: k, name: (ALL_GOALS.find((g) => g.key === k) || {}).name || k, count: c })).sort((a, b) => a.name.localeCompare(b.name));
  }, [library]);
  const fmtOpts = useMemo(() => {
    const m = new Map();
    library.forEach((e) => (e.intake?.formats || []).forEach((f) => m.set(f, (m.get(f) || 0) + 1)));
    return [...m.entries()].map(([k, c]) => ({ key: k, name: window.VENTUM_BRAND.formats[k]?.name || k, count: c })).sort((a, b) => a.name.localeCompare(b.name));
  }, [library]);

  const filtered = useMemo(() => library.filter((e) => {
    if (q.trim()) {
      const firstFmt = (e.intake?.formats || [])[0];
      const hl = briefHeadline(e.artifacts?.[firstFmt], '');
      const hay = [e.title, e.intake?.account_name, e.intake?.vertical, e.intake?.goal, (e.intake?.formats || []).join(' '), e.strategy?.angle, e.strategy?.pillar, hl].filter(Boolean).join(' ').toLowerCase();
      if (!hay.includes(q.toLowerCase())) return false;
    }
    if (facets.vertical.size && !facets.vertical.has(e.intake?.vertical)) return false;
    if (facets.goal.size && !facets.goal.has(e.intake?.goal)) return false;
    if (facets.format.size && !(e.intake?.formats || []).some((f) => facets.format.has(f))) return false;
    return true;
  }), [library, q, facets]);

  const sorted = useMemo(() => {
    const arr = [...filtered];
    const vname = (e) => window.VENTUM_BRAND.verticals[e.intake?.vertical]?.name || e.intake?.vertical || '~';
    if (sort === 'account') arr.sort((a, b) => (a.intake?.account_name || '~').localeCompare(b.intake?.account_name || '~'));
    else if (sort === 'vertical') arr.sort((a, b) => vname(a).localeCompare(vname(b)));
    else arr.sort((a, b) => new Date(b.updated_at || b.saved_at || 0) - new Date(a.updated_at || a.saved_at || 0));
    return arr;
  }, [filtered, sort]);

  const groups = useMemo(() => {
    if (groupBy === 'none') return [{ label: null, items: sorted }];
    const m = new Map();
    sorted.forEach((e) => {
      const k = groupBy === 'vertical'
        ? (window.VENTUM_BRAND.verticals[e.intake?.vertical]?.name || e.intake?.vertical || 'Other')
        : (e.intake?.account_name || 'No account');
      if (!m.has(k)) m.set(k, []);
      m.get(k).push(e);
    });
    return [...m.entries()].sort((a, b) => a[0].localeCompare(b[0])).map(([label, items]) => ({ label, items }));
  }, [sorted, groupBy]);

  const toast = (m) => (window.__toast ? window.__toast(m) : alert(m));
  const handleFile = (file) => {
    if (!file) { toast('No file selected'); return; }
    if (!/\.json$/i.test(file.name) && file.type && !/json/.test(file.type)) {
      toast('Please choose a Ventum .json file');
      return;
    }
    const reader = new FileReader();
    reader.onerror = () => toast('Could not read that file');
    reader.onload = () => {
      let payload;
      try {
        payload = JSON.parse(reader.result);
      } catch (e) {
        toast('That file isn’t valid JSON');
        return;
      }
      // Accept a briefcase (has intake), a raw artifacts bundle, or a saved
      // library entry export — anything with intake or artifacts to restore.
      if (!payload || (!payload.intake && !payload.artifacts)) {
        toast('That JSON isn’t a Ventum brief (no intake or artifacts)');
        return;
      }
      onImport(payload);
    };
    reader.readAsText(file);
  };

  const onDrop = (e) => {
    e.preventDefault();
    setDragOver(false);
    const file = e.dataTransfer?.files?.[0];
    if (file) handleFile(file);
  };

  // Pick a representative thumbnail snippet from the entry
  const thumbFor = (entry) => {
    const formats = entry.intake?.formats || [];
    return formats.slice(0, 3);
  };

  return (
    <div
      className="modal-mask"
      onClick={(e) => { if (e.target === e.currentTarget) onClose(); }}
      onDragOver={(e) => { e.preventDefault(); setDragOver(true); }}
      onDragLeave={() => setDragOver(false)}
      onDrop={onDrop}
    >
      <div className={"modal library-modal" + (dragOver ? ' is-dragover' : '')}>
        <div className="modal-head">
          <div>
            <h3>Library</h3>
            <span className="mono" style={{ fontSize: 10, color: 'var(--ink-40)', letterSpacing: '0.12em', textTransform: 'uppercase' }}>
              {library.length} saved{filtered.length !== library.length ? ` · ${filtered.length} shown` : ''} · drag a JSON anywhere to import
            </span>
          </div>
          <div style={{ display: 'flex', alignItems: 'center', gap: 10 }}>
            {/* A <label> wrapping the input triggers the file picker natively —
                more reliable than a button calling ref.click() on a hidden
                input (which silently no-ops in some browsers). */}
            <label
              className="btn-ghost"
              style={{ fontSize: 11, padding: '7px 12px', cursor: 'pointer', display: 'inline-flex', alignItems: 'center' }}
            >+ Import
              <input
                ref={fileInputRef}
                type="file"
                accept=".json,application/json"
                style={{ position: 'absolute', width: 1, height: 1, opacity: 0, overflow: 'hidden', clip: 'rect(0 0 0 0)' }}
                onChange={(e) => { const f = e.target.files?.[0]; if (f) handleFile(f); e.target.value = ''; }}
              />
            </label>
            <button className="drawer-close" onClick={onClose}>×</button>
          </div>
        </div>

        {/* Control bar: search · facets · sort · group · view toggle */}
        <div className="lib-controls">
          <div className="lib-search">
            <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2"><circle cx="11" cy="11" r="7" /><path d="m21 21-4-4" /></svg>
            <input type="text" placeholder="Search title, account, vertical, headline…" value={q} onChange={(e) => setQ(e.target.value)} />
          </div>
          <FacetDropdown label="Vertical" options={vertOpts} selected={facets.vertical} onToggle={(k) => toggleFacet('vertical', k)} onClear={() => clearFacet('vertical')} />
          <FacetDropdown label="Format" options={fmtOpts} selected={facets.format} onToggle={(k) => toggleFacet('format', k)} onClear={() => clearFacet('format')} />
          <FacetDropdown label="Goal" options={goalOpts} selected={facets.goal} onToggle={(k) => toggleFacet('goal', k)} onClear={() => clearFacet('goal')} />
          {activeFacetCount > 0 && <button className="lib-clearall" onClick={clearAllFacets}>Clear filters</button>}
          <span className="lib-controls__spacer" />
          <label className="lib-select">Sort
            <select value={sort} onChange={(e) => setSort(e.target.value)}>
              <option value="recent">Recent</option>
              <option value="account">Account A–Z</option>
              <option value="vertical">Vertical</option>
            </select>
          </label>
          <label className="lib-select">Group
            <select value={groupBy} onChange={(e) => setGroupBy(e.target.value)}>
              <option value="none">None</option>
              <option value="vertical">Vertical</option>
              <option value="account">Account</option>
            </select>
          </label>
          <div className="lib-toggle">
            <button className={view === 'gallery' ? 'is-active' : ''} onClick={() => setView('gallery')} title="Gallery view" aria-label="Gallery view">▦</button>
            <button className={view === 'list' ? 'is-active' : ''} onClick={() => setView('list')} title="List view" aria-label="List view">☰</button>
          </div>
        </div>

        <div className="library-scroll">
          {loading && library.length === 0 && (
            <div className="empty"><h2>Loading library…</h2><p>Fetching saved briefs from Supabase.</p></div>
          )}
          {!loading && library.length === 0 && (
            <div className="empty"><h2>No saved briefs yet.</h2><p>From Step 06, click "Save to library". Or drag a briefcase JSON here to import one.</p></div>
          )}
          {library.length > 0 && filtered.length === 0 && (
            <div className="empty"><h2>Nothing matches your filters</h2><p><button className="btn-ghost" style={{ fontSize: 12, padding: '6px 12px' }} onClick={() => { setQ(''); clearAllFacets(); }}>Clear search &amp; filters</button></p></div>
          )}
          {groups.map((g) => (
            <div key={g.label || 'all'} className="lib-group">
              {g.label && <div className="lib-group__head">{g.label}<span className="lib-group__n mono">{g.items.length}</span></div>}
              {view === 'gallery' ? (
                <div className="library-grid">
                  {g.items.map((e) => (
                    <LibraryCard key={e.id} entry={e} authUserId={authUserId} onRestore={onRestore} onDuplicate={onDuplicate} onDelete={onDelete} confirmDelete={confirmDelete} setConfirmDelete={setConfirmDelete} />
                  ))}
                </div>
              ) : (
                <div className="lib-list">
                  <div className="lib-list__head">
                    <span /><span>Title / Account</span><span>Vertical</span><span>Formats</span><span>Goal</span><span />
                  </div>
                  {g.items.map((e) => (
                    <LibraryRow key={e.id} entry={e} authUserId={authUserId} onRestore={onRestore} onDuplicate={onDuplicate} onDelete={onDelete} confirmDelete={confirmDelete} setConfirmDelete={setConfirmDelete} />
                  ))}
                </div>
              )}
            </div>
          ))}
        </div>
        {dragOver && (
          <div className="library-dropzone">
            <div className="mono">Drop briefcase JSON to import</div>
          </div>
        )}
      </div>
    </div>
  );
}

// ============================================================
// BRAND DRAWER CONTENT
// ============================================================
function BrandDrawerContent({ tab }) {
  const B = window.VENTUM_BRAND;
  if (tab === 'voice') {
    return (
      <div>
        <div className="mono" style={{ fontSize: 10, color: 'var(--ink-40)', letterSpacing: '0.16em', textTransform: 'uppercase', marginBottom: 10 }}>Promise</div>
        <p style={{ fontSize: 20, lineHeight: 1.3, letterSpacing: '-0.015em', marginBottom: 24 }}>Invisible threats. Visible confidence.</p>
        <div className="mono" style={{ fontSize: 10, color: 'var(--ink-40)', letterSpacing: '0.16em', textTransform: 'uppercase', marginBottom: 10 }}>Always sound like</div>
        <ul style={{ paddingLeft: 18, color: 'var(--ink-80)', fontSize: 14, lineHeight: 1.6 }}>
          <li>Calm, confident, science-led</li>
          <li>Useful to the operator, not just impressive to the scientist</li>
          <li>Specific about what we do — and what we don't claim</li>
          <li>Protective of the spaces and people we serve, never fear-based</li>
          <li>Premium and infrastructural, not a cleaning-product brand</li>
        </ul>
        <div className="mono" style={{ fontSize: 10, color: 'var(--ink-40)', letterSpacing: '0.16em', textTransform: 'uppercase', marginTop: 22, marginBottom: 10 }}>Never sound like</div>
        <ul style={{ paddingLeft: 18, color: 'var(--ink-80)', fontSize: 14, lineHeight: 1.6 }}>
          <li>"Kills 99.9%" cleaning-product marketing</li>
          <li>Fearful or alarmist about disease and pathogens</li>
          <li>Academic, jargon-heavy, or condescending to operators</li>
          <li>Making medical or unverifiable regulatory claims</li>
        </ul>
      </div>
    );
  }
  if (tab === 'pillars') {
    return (
      <div>
        {Object.entries(B.pillars).map(([k,v]) => (
          <div key={k} style={{ paddingBottom: 14, marginBottom: 14, borderBottom: '1px solid var(--ink-10)' }}>
            <div className="mono" style={{ fontSize: 10, color: 'var(--teal)', letterSpacing: '0.14em', textTransform: 'uppercase' }}>{k}</div>
            <h3 style={{ fontSize: 18, fontWeight: 500, letterSpacing: '-0.01em', marginTop: 4 }}>{v.name}</h3>
            <p style={{ fontSize: 13.5, color: 'var(--ink-60)', marginTop: 4 }}>{v.claim}</p>
          </div>
        ))}
      </div>
    );
  }
  if (tab === 'language') {
    return (
      <div>
        <div className="mono" style={{ fontSize: 10, color: 'var(--ink-40)', letterSpacing: '0.16em', textTransform: 'uppercase', marginBottom: 10 }}>Banned phrases</div>
        <div className="chip-row" style={{ marginBottom: 22 }}>
          {B.banned.map(b => <span key={b} className="chip" style={{ borderColor: '#F1B8B1', color: '#A33027', background: '#FDECEA' }}>{b}</span>)}
        </div>
        <div className="mono" style={{ fontSize: 10, color: 'var(--ink-40)', letterSpacing: '0.16em', textTransform: 'uppercase', marginBottom: 10 }}>Approved CTAs</div>
        <ul style={{ paddingLeft: 0, listStyle: 'none', color: 'var(--ink-80)', fontSize: 14 }}>
          {Object.values(B.cta_pool).map(c => <li key={c} style={{ padding: '8px 0', borderTop: '1px solid var(--ink-10)' }}>{c}</li>)}
        </ul>
        <div className="mono" style={{ fontSize: 10, color: 'var(--ink-40)', letterSpacing: '0.16em', textTransform: 'uppercase', marginTop: 22, marginBottom: 10 }}>Approved hashtags</div>
        <div className="chip-row">
          {B.hashtag_pool.map(h => <span key={h} className="chip">{h}</span>)}
        </div>
      </div>
    );
  }
  if (tab === 'claims') {
    const claims = window.VENTUM_CLAIMS ? window.VENTUM_CLAIMS.all() : [];
    return (
      <div>
        <div className="mono" style={{ fontSize: 10, color: 'var(--ink-40)', letterSpacing: '0.16em', textTransform: 'uppercase', marginBottom: 8 }}>Approved claim phrases</div>
        <p style={{ fontSize: 13, color: 'var(--ink-60)', lineHeight: 1.55, marginBottom: 18 }}>
          Pinned wording for regulated topics (efficacy, EPA status, residue, medical). The Strategy and Copy agents prefer these verbatim. QA flags any forbidden phrasing with a one-click fix to the approved wording.
        </p>
        {claims.map(c => (
          <div key={c.id} style={{ paddingBottom: 14, marginBottom: 14, borderBottom: '1px solid var(--ink-10)' }}>
            <div style={{ display: 'flex', justifyContent: 'space-between', alignItems: 'baseline', marginBottom: 6 }}>
              <div className="mono" style={{ fontSize: 10, color: 'var(--teal)', letterSpacing: '0.14em', textTransform: 'uppercase' }}>{c.topic}</div>
              <div className="mono" style={{ fontSize: 9, color: 'var(--ink-40)', letterSpacing: '0.14em' }}>{c.id}</div>
            </div>
            {c.approved.length > 0 && (
              <>
                <div className="mono" style={{ fontSize: 9, color: 'var(--ink-40)', letterSpacing: '0.14em', textTransform: 'uppercase', marginBottom: 4 }}>Use verbatim</div>
                <ul style={{ paddingLeft: 0, listStyle: 'none', marginBottom: 8 }}>
                  {c.approved.map(p => (
                    <li key={p} style={{ fontSize: 13, padding: '4px 8px', background: '#EEF7F3', color: '#1F5A48', borderRadius: 2, marginBottom: 3, fontFamily: 'inherit' }}>"{p}"</li>
                  ))}
                </ul>
              </>
            )}
            {(c.forbidden_labels || c.forbidden).length > 0 && (
              <>
                <div className="mono" style={{ fontSize: 9, color: 'var(--ink-40)', letterSpacing: '0.14em', textTransform: 'uppercase', marginBottom: 4 }}>Never write</div>
                <ul style={{ paddingLeft: 0, listStyle: 'none', marginBottom: 8 }}>
                  {(c.forbidden_labels || c.forbidden).map((p, i) => (
                    <li key={i} style={{ fontSize: 12, padding: '4px 8px', background: '#FDECEA', color: '#A33027', borderRadius: 2, marginBottom: 3, lineHeight: 1.4 }}>{p}</li>
                  ))}
                </ul>
              </>
            )}
            <div style={{ fontSize: 11, color: 'var(--ink-60)', lineHeight: 1.5, marginTop: 6 }}>
              <strong style={{ color: 'var(--ink-100)', fontWeight: 500 }}>Source:</strong> {c.citation}
            </div>
            {c.notes && <div style={{ fontSize: 11, color: 'var(--ink-60)', fontStyle: 'italic', lineHeight: 1.5, marginTop: 4 }}>{c.notes}</div>}
          </div>
        ))}
      </div>
    );
  }
  if (tab === 'formats') {
    return (
      <div>
        {Object.entries(B.formats).map(([k,v]) => (
          <div key={k} style={{ display: 'grid', gridTemplateColumns: '60px 1fr', gap: 14, paddingBottom: 14, marginBottom: 14, borderBottom: '1px solid var(--ink-10)' }}>
            <div className="mono" style={{ fontSize: 11, padding: '4px 8px', background: 'var(--ink-05)', textAlign: 'center', letterSpacing: '0.1em', height: 'fit-content' }}>{v.icon}</div>
            <div>
              <h4 style={{ fontSize: 15, fontWeight: 500 }}>{v.name}</h4>
              <p style={{ fontSize: 13, color: 'var(--ink-60)', marginTop: 4 }}>{v.desc}</p>
            </div>
          </div>
        ))}
      </div>
    );
  }
  if (tab === 'imagery') {
    const groups = window.VENTUM_IMAGERY?.groupedForBrowser ? window.VENTUM_IMAGERY.groupedForBrowser() : {};
    const realN = window.VENTUM_IMAGERY?.realCount || 0;
    const phN = window.VENTUM_IMAGERY?.placeholderCount || 0;
    const labelFor = (k) => (window.VENTUM_BRAND.verticals[k]?.name || (k === 'atmosphere' ? 'Atmospheres' : k));
    return (
      <div>
        <div className="mono" style={{ fontSize: 10, color: 'var(--ink-40)', letterSpacing: '0.16em', textTransform: 'uppercase', marginBottom: 8 }}>Visual library</div>
        <p style={{ fontSize: 13, color: 'var(--ink-60)', lineHeight: 1.55, marginBottom: 14 }}>
          {realN} shot photo{realN === 1 ? '' : 's'} · {phN} planned slot{phN === 1 ? '' : 's'} awaiting a real shot. Drop a JPG into <code style={{ fontFamily: 'var(--mono)', fontSize: 11, background: 'var(--ink-05)', padding: '2px 5px', borderRadius: 2 }}>/assets/</code> using the slot name shown on hover, or replace inline via the <em>Replace</em> pill on any artifact.
        </p>
        {Object.entries(groups).map(([k, items]) => (
          <div key={k} style={{ marginBottom: 22 }}>
            <div style={{ display: 'flex', justifyContent: 'space-between', alignItems: 'baseline', marginBottom: 8 }}>
              <div className="mono" style={{ fontSize: 10, color: 'var(--ink-100)', letterSpacing: '0.14em', textTransform: 'uppercase', fontWeight: 500 }}>{labelFor(k)}</div>
              <div className="mono" style={{ fontSize: 9, color: 'var(--ink-40)', letterSpacing: '0.14em' }}>
                {items.filter(i => !i.placeholder).length}/{items.length} sourced
              </div>
            </div>
            <div style={{ display: 'grid', gridTemplateColumns: 'repeat(2, 1fr)', gap: 6 }}>
              {items.map((item, i) => (
                <div key={i} style={{ position: 'relative', aspectRatio: '3/2', borderRadius: 2, overflow: 'hidden', border: '1px solid var(--ink-10)', background: 'var(--ink-05)' }}
                  title={item.placeholder ? `Drop into: assets/${item.slot}.jpg` : item.src}>
                  <img src={item.src} alt={item.label} style={{ position: 'absolute', inset: 0, width: '100%', height: '100%', objectFit: 'cover' }} />
                  {item.placeholder && (
                    <div className="mono" style={{ position: 'absolute', top: 6, right: 6, fontSize: 8, padding: '2px 5px', background: 'rgba(255,255,255,0.9)', color: 'var(--ink-60)', letterSpacing: '0.12em', borderRadius: 2 }}>SLOT</div>
                  )}
                  <div style={{ position: 'absolute', bottom: 0, left: 0, right: 0, padding: '20px 8px 6px', background: 'linear-gradient(180deg, rgba(11,31,51,0), rgba(11,31,51,0.85))' }}>
                    <div style={{ color: 'white', fontSize: 11, fontWeight: 500, letterSpacing: '-0.005em', lineHeight: 1.2 }}>{item.label}</div>
                  </div>
                </div>
              ))}
            </div>
          </div>
        ))}
      </div>
    );
  }
  return null;
}

// ============================================================
// MOUNT
// ============================================================
// Mount under the Supabase auth gate when the gate component is loaded.
// Falls through to the bare app if auth.jsx hasn't loaded for any reason —
// keeps the studio usable while we iterate on the auth layer.
{
  const root = ReactDOM.createRoot(document.getElementById('root'));
  if (window.VentumAuthGate) {
    root.render(<window.VentumAuthGate><StudioApp /></window.VentumAuthGate>);
  } else {
    console.warn('[VENTUM] VentumAuthGate not found — rendering app without auth.');
    root.render(<StudioApp />);
  }
}
