// Vanday DAM — library (landing) screen

// True when an asset is a video (mp4 / mov / webm). Checks the server-provided
// MIME first, then falls back to the file extension on the name or URL. Global
// (classic script) so preview.jsx and other screens can reuse it.
function assetIsVideo(asset) {
  if (!asset) return false;
  const m = (asset.mime || "").toLowerCase();
  if (m.startsWith("video/")) return true;
  const src = (asset.name || asset.url || "").toLowerCase();
  const ext = src.split(/[?#]/)[0].split(".").pop();
  return ext === "mp4" || ext === "mov" || ext === "webm";
}

// Return the URL to use for a SMALL thumbnail render. For server-side raster
// images this is the disk-cached /api/thumb endpoint (a 20-60 KB JPEG vs the
// 2-10 MB original). For everything else — videos, demo/mock assets, crops
// (which the server generates separately), assets without an id we can hit —
// fall back to the original url. The grid stays correct in every case.
function thumbSrc(asset, w = 400) {
  if (!asset) return "";
  if (!asset.isServer) return asset.url || ""; // demo / mock
  if (asset.isServerVariant) return asset.url || ""; // crops already small
  if (assetIsVideo(asset)) return asset.url || "";   // videos: needs poster, not in scope
  if (!asset.id || !window.VandayAPI?.thumbUrl) return asset.url || "";
  return window.VandayAPI.thumbUrl(asset.id, w);
}

function BulkDownloadMenu({ getPicked }) {
  const [open, setOpen] = React.useState(false);
  const [busy, setBusy] = React.useState(false);
  const [coords, setCoords] = React.useState(null);
  const btnRef = React.useRef(null);
  const menuRef = React.useRef(null);

  // Reposition the (portaled) menu above the button. The bulk bar uses
  // overflow-x: auto, which would otherwise clip an absolutely-positioned
  // dropdown, so we render the menu in a portal with position: fixed.
  const reposition = React.useCallback(() => {
    if (!btnRef.current) return;
    const r = btnRef.current.getBoundingClientRect();
    const MENU_W = 260;
    const left = Math.max(8, Math.min(window.innerWidth - MENU_W - 8, r.left));
    setCoords({ left, bottom: window.innerHeight - r.top + 6 });
  }, []);

  React.useEffect(() => {
    if (!open) return;
    reposition();
    const onClick = (e) => {
      if (btnRef.current && btnRef.current.contains(e.target)) return;
      if (menuRef.current && menuRef.current.contains(e.target)) return;
      setOpen(false);
    };
    const onKey = (e) => { if (e.key === "Escape") setOpen(false); };
    const onResize = () => reposition();
    window.addEventListener("mousedown", onClick);
    window.addEventListener("keydown", onKey);
    window.addEventListener("resize", onResize);
    window.addEventListener("scroll", onResize, true);
    return () => {
      window.removeEventListener("mousedown", onClick);
      window.removeEventListener("keydown", onKey);
      window.removeEventListener("resize", onResize);
      window.removeEventListener("scroll", onResize, true);
    };
  }, [open, reposition]);

  const pick = async (format) => {
    setOpen(false);
    const picked = getPicked();
    if (!picked || picked.length === 0) return;
    setBusy(true);
    try { await VandayServer.downloadMany(picked, format); }
    finally { setBusy(false); }
  };

  const menu = open && coords ? ReactDOM.createPortal(
    React.createElement(
      "div",
      {
        ref: menuRef,
        className: "send-menu",
        style: { position: "fixed", top: "auto", left: coords.left, bottom: coords.bottom, right: "auto", zIndex: 1000 },
      },
      React.createElement("div", { className: "send-menu-h" }, "Download as"),
      React.createElement(
        "button",
        { className: "send-menu-item", onClick: () => pick("original") },
        React.createElement("div", { style: { flex: 1 } },
          React.createElement("div", { className: "send-menu-name" }, "Original"),
          React.createElement("div", { className: "send-menu-account" }, "Keep each file's source format"),
        ),
      ),
      React.createElement(
        "button",
        { className: "send-menu-item", onClick: () => pick("jpg") },
        React.createElement("div", { style: { flex: 1 } },
          React.createElement("div", { className: "send-menu-name" }, "JPG"),
          React.createElement("div", { className: "send-menu-account" }, "Smaller files, no transparency"),
        ),
      ),
      React.createElement(
        "button",
        { className: "send-menu-item", onClick: () => pick("png") },
        React.createElement("div", { style: { flex: 1 } },
          React.createElement("div", { className: "send-menu-name" }, "PNG"),
          React.createElement("div", { className: "send-menu-account" }, "Lossless, keeps transparency"),
        ),
      ),
    ),
    document.body
  ) : null;

  return React.createElement(
    React.Fragment,
    null,
    React.createElement(
      "button",
      { ref: btnRef, className: "bulk-act", "data-bulk": "download", disabled: busy, onClick: () => setOpen((v) => !v) },
      React.createElement(IcDownload, { size: 13 }),
      " ",
      busy ? "Preparing…" : "Download",
      React.createElement(IcChevD, { size: 11, style: { marginLeft: 2 } }),
    ),
    menu,
  );
}

function Rail({ active, onNav, onLogout, onSearch, onNotifs, onHelp, onQuickUpload }) {
  // Beta hides the notifications icon; dev mode keeps it visible.
  const features = (typeof window.useVandayFeatures === "function")
    ? window.useVandayFeatures()
    : { notifications: true, isAdmin: false };
  return (
    <nav className="rail">
      <div className="rail-logo">v.</div>
      <button
        className={`rail-btn ${active === "library" ? "active" : ""}`}
        onClick={() => onNav("library")}
        title="Library"
      >
        <IcHome />
      </button>
      {/* Search button removed from the rail — the search input at the top
          of the library grid is the canonical entry point. ⌘K still focuses it. */}
      {features.notifications && (
        <button className="rail-btn" title="Notifications" onClick={onNotifs}>
          <IcBell />
          <span className="rail-dot" />
        </button>
      )}
      {features.canWrite && (
        <button
          className="rail-btn"
          onClick={onQuickUpload || (() => onNav("upload"))}
          title="Upload"
        >
          <IcUpload />
        </button>
      )}
      <div className="rail-spacer" />
      {features.isAdmin && (
        <button
          className={`rail-btn ${active === "admin" ? "active" : ""}`}
          onClick={() => window.open("/admin", "_blank")}
          title="Admin dashboard — only you can see this"
          style={{
            color: "white",
            background: active === "admin" ? "oklch(0.45 0.18 32)" : "oklch(0.55 0.18 32)",
            borderRadius: 8,
            position: "relative",
          }}
        >
          <IcShield />
          <span style={{
            position: "absolute",
            top: -4, right: -4,
            background: "oklch(0.55 0.18 32)",
            color: "white",
            fontSize: 8, fontWeight: 700, letterSpacing: 0.3,
            padding: "1px 4px", borderRadius: 6,
            border: "2px solid var(--bg, white)",
            textTransform: "uppercase",
          }}>Admin</span>
        </button>
      )}
      <button className="rail-btn" title="Help & shortcuts" onClick={onHelp}>
        <IcInfo />
      </button>
      <button
        className={`rail-btn ${active === "settings" ? "active" : ""}`}
        onClick={() => onNav("settings")}
        title="Settings"
      >
        <IcSettings />
      </button>
      <UserMenu onLogout={onLogout} onNav={onNav} />
    </nav>
  );
}

function UserMenu({ onLogout, onNav }) {
  const [open, setOpen] = React.useState(false);
  const ref = React.useRef(null);
  const me = window.VandaySession.getActiveUser();
  const myInitials = window.VandaySession.initials(me);
  const others = (window.USERS || []).filter((u) => !me || u.id !== me.id);

  React.useEffect(() => {
    if (!open) return;
    const onClick = (e) => { if (ref.current && !ref.current.contains(e.target)) setOpen(false); };
    const onKey = (e) => { if (e.key === "Escape") setOpen(false); };
    window.addEventListener("mousedown", onClick);
    window.addEventListener("keydown", onKey);
    return () => {
      window.removeEventListener("mousedown", onClick);
      window.removeEventListener("keydown", onKey);
    };
  }, [open]);

  return (
    <div ref={ref} style={{ position: "relative" }}>
      <button
        className={`rail-avatar ${open ? "is-active" : ""}`}
        onClick={() => setOpen((v) => !v)}
        title={`Signed in as ${me ? me.name : "—"}`}
        style={me && me.color ? { background: me.color } : undefined}
      >
        {myInitials}
      </button>
      {open && (
        <div className="acct-menu" role="menu">
          <div className="acct-label">Signed in as</div>
          <div className="acct-current">
            <span className="acct-av" style={me && me.color ? { background: me.color } : undefined}>{myInitials}</span>
            <span className="acct-id">
              <div className="acct-name">{me ? me.name : "—"}</div>
              <div className="acct-role">{me ? me.role : ""}</div>
            </span>
            <span className="acct-check"><IcCheck size={16} /></span>
          </div>

          {others.length > 0 && (
            <>
              <div className="acct-sep" />
              <div className="acct-label" style={{ paddingTop: 0 }}>Switch user</div>
              {others.map((u) => (
                <button
                  key={u.id}
                  className="acct-switch"
                  onClick={() => { window.VandaySession.setActiveUserId(u.id); setOpen(false); }}
                  title={`Switch to ${u.name}`}
                >
                  <span className="acct-av" style={{ background: u.color }}>
                    {window.VandaySession.initials(u)}
                  </span>
                  <span className="acct-id">
                    <div className="acct-name">{u.name}</div>
                    <div className="acct-role">{u.role}</div>
                  </span>
                </button>
              ))}
            </>
          )}

          <div className="acct-sep" />
          <button className="acct-row" onClick={() => { setOpen(false); onNav && onNav("settings"); }}>
            <IcSettings className="ic" size={16} /> Account settings
          </button>
          <a
            className="acct-row"
            href="https://vanday.ai/support"
            target="_blank"
            rel="noreferrer"
            style={{ color: "inherit", textDecoration: "none" }}
            onClick={() => setOpen(false)}
          >
            <IcInfo className="ic" size={16} /> Help &amp; support
          </a>
          <button className="acct-row danger" onClick={onLogout}>
            <IcExternal className="ic" size={16} /> Sign out
          </button>
        </div>
      )}
    </div>
  );
}

function Sidebar({ activeFolder, setActiveFolder, onNav, onNewFolder, onFolderDrop, onFolderDelete, onFolderRename, onFolderMove }) {
  // The upload cap comes from the server (/api/me limits) so the meter can
  // never drift from what's actually enforced.
  const limits = (typeof window.useVandayLimits === "function") ? window.useVandayLimits() : null;
  // Viewers (read-only role) can't create or delete folders — hide the
  // affordances so the UI matches what the server allows.
  const features = (typeof window.useVandayFeatures === "function") ? window.useVandayFeatures() : null;
  const canWrite = features ? features.canWrite : true;
  const [dropTarget, setDropTarget] = React.useState(null);
  // On mobile, the sidebar lives in an off-canvas drawer (app.jsx owns the
  // drawer-open state). After the user picks a folder we want the drawer to
  // close so they can see the asset list they just navigated to. The
  // window-event pattern matches what vanday-search uses.
  const closeDrawer = () => {
    try { window.dispatchEvent(new CustomEvent("vanday-drawer-close")); } catch {}
  };
  // Which nested folders are expanded in the tree. Starts all-collapsed.
  const [expanded, setExpanded] = React.useState(() => new Set());
  // Inline-rename state: which folder is being edited + the current draft.
  // Triggered by double-clicking the folder name. Enter saves, Esc cancels,
  // blur saves (matches the TagEditor pattern in preview.jsx).
  const [renamingFolderId, setRenamingFolderId] = React.useState(null);
  const [renameDraft, setRenameDraft] = React.useState("");
  const startRename = (f) => {
    if (!canWrite || !f.isServer || !onFolderRename) return;
    setRenamingFolderId(f.id);
    setRenameDraft(f.name);
  };
  const commitRename = async (f) => {
    const next = renameDraft.trim().slice(0, 60);
    setRenamingFolderId(null);
    setRenameDraft("");
    if (!next || next === f.name) return;
    try { await onFolderRename(f, next); } catch {}
  };
  const cancelRename = () => { setRenamingFolderId(null); setRenameDraft(""); };
  // Folder actions menu (the ⋯ kebab). One menu open at a time, anchored to the
  // clicked button's screen position. `menuMode` toggles the root actions vs.
  // the "Move to…" destination list.
  const [menuFolderId, setMenuFolderId] = React.useState(null);
  const [menuMode, setMenuMode] = React.useState("root");
  const [menuAnchor, setMenuAnchor] = React.useState(null);
  const closeMenu = () => { setMenuFolderId(null); setMenuMode("root"); };
  const openMenu = (f, e) => {
    e.stopPropagation();
    if (menuFolderId === f.id) { closeMenu(); return; }
    const r = e.currentTarget.getBoundingClientRect();
    setMenuAnchor({ x: r.right, y: r.bottom });
    setMenuMode("root");
    setMenuFolderId(f.id);
  };
  // Close the menu on Escape (outside-click is handled by the overlay).
  React.useEffect(() => {
    if (!menuFolderId) return;
    const onKey = (e) => { if (e.key === "Escape") closeMenu(); };
    window.addEventListener("keydown", onKey);
    return () => window.removeEventListener("keydown", onKey);
  }, [menuFolderId]);
  const toggleExpand = (id) =>
    setExpanded((s) => {
      const next = new Set(s);
      if (next.has(id)) next.delete(id);
      else next.add(id);
      return next;
    });

  const byName = (a, b) => a.name.localeCompare(b.name, undefined, { sensitivity: "base" });
  const canSee = (f) => window.VandaySession.canSeeFolder(f.id);

  // Recursive folder row — handles nesting, alphabetized children, drag-drop,
  // and right-click delete. `depth` drives the indentation.
  const renderFolder = (f, depth) => {
    const kids = window.childFolders(f.id).filter(canSee).sort(byName);
    const hasKids = kids.length > 0;
    const isOpen = expanded.has(f.id);
    const isRestricted = FOLDER_PERMS[f.id] && FOLDER_PERMS[f.id].link === "restricted";
    const isHover = dropTarget === f.id;
    return (
      <React.Fragment key={f.id}>
        <button
          className={`sidebar-item ${activeFolder === f.id ? "active" : ""}`}
          style={{
            paddingLeft: 10 + depth * 16,
            ...(isHover ? { outline: "1.5px dashed var(--accent)", outlineOffset: -2, background: "var(--bg-hover, rgba(255,255,255,0.04))" } : {}),
          }}
          onClick={() => { setActiveFolder(f.id); closeDrawer(); }}
          onDragOver={(e) => { e.preventDefault(); e.dataTransfer.dropEffect = "move"; setDropTarget(f.id); }}
          onDragLeave={() => setDropTarget((t) => (t === f.id ? null : t))}
          onDrop={(e) => { setDropTarget(null); onFolderDrop && onFolderDrop(f.id, e); }}
          onContextMenu={(e) => {
            if (!canWrite || !f.isServer || !onFolderDelete) return;
            e.preventDefault();
            onFolderDelete(f);
          }}
          onDoubleClick={(e) => {
            if (!canWrite || !f.isServer || !onFolderRename) return;
            e.preventDefault();
            e.stopPropagation();
            startRename(f);
          }}
          title={canWrite && f.isServer ? "Double-click to rename · Right-click to delete" : undefined}
        >
          {hasKids ? (
            <span
              className="sidebar-twisty"
              role="button"
              title={isOpen ? "Collapse" : "Expand"}
              onClick={(e) => { e.stopPropagation(); toggleExpand(f.id); }}
            >
              {isOpen ? <IcChevD size={12} /> : <IcChevR size={12} />}
            </span>
          ) : (
            <span className="sidebar-twisty sidebar-twisty-empty" />
          )}
          <span className="ic"><IcFolder size={15} /></span>
          {renamingFolderId === f.id ? (
            <input
              autoFocus
              value={renameDraft}
              onChange={(e) => setRenameDraft(e.target.value)}
              onClick={(e) => e.stopPropagation()}
              onKeyDown={(e) => {
                e.stopPropagation();
                if (e.key === "Enter") { e.preventDefault(); commitRename(f); }
                else if (e.key === "Escape") { e.preventDefault(); cancelRename(); }
              }}
              onBlur={() => commitRename(f)}
              maxLength={60}
              style={{
                flex: 1, minWidth: 60, background: "var(--bg, #fff)",
                border: "1px dashed var(--accent)", color: "var(--text)",
                borderRadius: 4, padding: "1px 6px", fontSize: "inherit",
                fontFamily: "inherit", outline: "none", marginRight: 4,
              }}
            />
          ) : (
            f.name
          )}
          {isRestricted && <IcShield size={11} className="sidebar-lock" />}
          <span className="sidebar-count">{f.count}</span>
          {canWrite && f.isServer && (
            <span
              className={`sidebar-kebab ${menuFolderId === f.id ? "is-open" : ""}`}
              role="button"
              title="Folder actions"
              aria-label="Folder actions"
              onClick={(e) => openMenu(f, e)}
            >
              <IcMore size={14} />
            </span>
          )}
        </button>
        {hasKids && isOpen && kids.map((c) => renderFolder(c, depth + 1))}
      </React.Fragment>
    );
  };

  // Top-level folders for the tree — exclude "raw" (shown as "Inbox" in
  // the quick-access section above).
  const topFolders = window.topLevelFolders().filter((f) => f.id !== "raw" && canSee(f)).sort(byName);
  const raw = FOLDERS.find((f) => f.id === "raw");

  return (
    <aside className="sidebar">
      {/* Workspace identity — shows the workspace's own name (set at signup,
          editable in Settings → Workspace). Replaces Clerk's OrganizationSwitcher,
          which is hidden in the beta since multiple workspaces is a future
          paid feature. */}
      {window.WorkspaceBadge && (
        <div style={{
          padding: "12px 14px 12px",
          borderBottom: "1px solid var(--border)",
          marginBottom: 8,
        }}>
          <WorkspaceBadge />
        </div>
      )}
      {/* Quick-access views — not folders. "All assets" stays as the home
          view; the three below are the shortcuts the user asked for. */}
      <div className="sidebar-section" style={{ marginTop: 8 }}>
        <button
          className={`sidebar-item ${activeFolder === "all" ? "active" : ""}`}
          onClick={() => { setActiveFolder("all"); closeDrawer(); }}
        >
          <span className="ic"><IcGrid size={15} /></span>
          All assets
        </button>
        <button
          className={`sidebar-item ${activeFolder === "__recent" ? "active" : ""}`}
          onClick={() => { setActiveFolder("__recent"); closeDrawer(); }}
        >
          <span className="ic"><IcClock size={15} /></span>
          Recently uploaded
        </button>
        {raw && canSee(raw) && (
          <button
            className={`sidebar-item sidebar-item-unsorted ${activeFolder === "raw" ? "active" : ""} ${dropTarget === "raw" ? "is-drop" : ""}`}
            onClick={() => { setActiveFolder("raw"); closeDrawer(); }}
            onDragOver={(e) => { e.preventDefault(); e.dataTransfer.dropEffect = "move"; setDropTarget("raw"); }}
            onDragLeave={() => setDropTarget((t) => (t === "raw" ? null : t))}
            onDrop={(e) => { setDropTarget(null); onFolderDrop && onFolderDrop("raw", e); }}
          >
            {/* Inbox tray = "new things land here, not yet filed" */}
            <span className="ic"><IcInbox size={15} /></span>
            Inbox
            <span className="sidebar-flyover">New uploads land here until you file them — drag them into a folder to organize.</span>
            <span className="sidebar-count">{raw.count}</span>
          </button>
        )}
        {/* "Shared with me" sidebar item removed — it only mocked a
            user-to-user sharing flow that doesn't exist. Vanday's share
            feature creates public portal links, not workspace invites,
            so there's nothing for "shared with me" to populate. Bring
            it back if/when we ship workspace member sharing. */}
      </div>

      <div className="sidebar-divider" />

      <div className="sidebar-header">
        <span className="sidebar-title">Folders</span>
        {canWrite && (
          <button className="btn ghost sm" title="New folder" onClick={() => onNewFolder && onNewFolder()}>
            <IcPlus size={14} />
          </button>
        )}
      </div>
      <div className="sidebar-section">
        {topFolders.length === 0 ? (
          <div style={{ padding: "6px 12px", fontSize: 12.5, color: "var(--text-faint)", lineHeight: 1.5 }}>
            {canWrite ? "No folders yet. Hit + to create one." : "No folders yet."}
          </div>
        ) : (
          topFolders.map((f) => renderFolder(f, 0))
        )}
      </div>

      <div className="sidebar-divider" />

      <div className="sidebar-section">
        <button
          className={`sidebar-item ${activeFolder === "__trash" ? "active" : ""}`}
          onClick={() => { setActiveFolder("__trash"); closeDrawer(); }}
        >
          <span className="ic"><IcTrash size={15} /></span>
          Recently deleted
        </button>
      </div>

      <div className="rail-spacer" />

      {(() => {
        // Storage meter — real per-account byte usage against the plan cap.
        // Storage is the cap now; the old upload-count meter was removed.
        const storageCapBytes = limits?.maxStorageBytes ?? (5 * 1024 * 1024 * 1024);
        const storageUsedBytes = limits?.storageBytesUsed ?? 0;
        const capGB = storageCapBytes / (1024 * 1024 * 1024);
        const fmtSize = (bytes) => {
          const gb = (bytes || 0) / (1024 * 1024 * 1024);
          if (gb < 0.1) return ((bytes || 0) / (1024 * 1024)).toFixed(1) + " MB";
          return gb.toFixed(2) + " GB";
        };
        const stRatio = Math.min(100, Math.round((storageUsedBytes / storageCapBytes) * 100));
        const stColor = stRatio >= 90 ? "var(--status-danger)"
          : stRatio >= 75 ? "var(--status-warning)"
          : "var(--accent)";
        const meterLabel = { display: "flex", justifyContent: "space-between", marginBottom: 6 };
        const meterTrack = { height: 4, background: "var(--surface-sunken)", borderRadius: 999, overflow: "hidden" };
        return (
          <div style={{ padding: "14px 18px", borderTop: "1px solid var(--border)", display: "flex", flexDirection: "column", gap: 13 }}>
            <div style={{ fontSize: 12, color: "var(--text-muted)" }}>
              <div style={meterLabel}>
                <span>Storage</span>
                <span style={{ fontFamily: "var(--font-mono)" }}>{fmtSize(storageUsedBytes)} / {capGB} GB</span>
              </div>
              <div style={meterTrack}>
                <div style={{ width: Math.max(2, stRatio) + "%", height: "100%", background: stColor, borderRadius: 999 }} />
              </div>
            </div>
          </div>
        );
      })()}

      {menuFolderId && menuAnchor && (() => {
        const f = FOLDERS.find((x) => x.id === menuFolderId);
        if (!f) { return null; }
        // Valid move destinations: every server folder the user can see, minus
        // this folder and its descendants (moving into either would create a
        // cycle) and minus its current parent (no-op). Labelled with their full
        // path so nested destinations are unambiguous.
        const excluded = new Set(window.descendantFolderIds(f.id));
        const curParent = f.parent || null;
        const dests = FOLDERS
          .filter((d) => d.isServer && !excluded.has(d.id) && canSee(d) && d.id !== curParent)
          .sort(byName);
        const pathLabel = (d) => window.folderPath(d.id).map((p) => p.name).join(" / ");
        const atTop = curParent === null;
        const left = Math.max(8, Math.min(menuAnchor.x - 210, window.innerWidth - 226));
        const top = Math.min(menuAnchor.y + 4, window.innerHeight - 260);
        return (
          <div className="folder-menu-overlay" onClick={closeMenu}>
            <div className="folder-menu" style={{ left, top }} onClick={(e) => e.stopPropagation()}>
              {menuMode === "root" ? (
                <>
                  <button className="folder-menu-item" onClick={() => {
                    closeMenu();
                    setExpanded((s) => new Set(s).add(f.id));
                    onNewFolder && onNewFolder(f.id);
                  }}>
                    <IcPlus size={13} /> New subfolder
                  </button>
                  <button className="folder-menu-item" onClick={() => { closeMenu(); startRename(f); }}>
                    <IcPencil size={13} /> Rename
                  </button>
                  <button className="folder-menu-item" onClick={() => setMenuMode("move")}>
                    <IcFolder size={13} /> Move to…
                  </button>
                  <div className="folder-menu-sep" />
                  <button className="folder-menu-item danger" onClick={() => { closeMenu(); onFolderDelete && onFolderDelete(f); }}>
                    <IcTrash size={13} /> Delete
                  </button>
                </>
              ) : (
                <>
                  <div className="folder-menu-head">
                    <button className="folder-menu-back" title="Back" onClick={() => setMenuMode("root")}>
                      <IcChevR size={12} style={{ transform: "rotate(180deg)" }} />
                    </button>
                    <span className="folder-menu-head-txt">Move “{f.name}” to</span>
                  </div>
                  <div className="folder-menu-scroll">
                    {!atTop && (
                      <button className="folder-menu-item" onClick={() => { closeMenu(); onFolderMove && onFolderMove(f, null); }}>
                        <IcGrid size={13} /> Top level
                      </button>
                    )}
                    {dests.map((d) => (
                      <button key={d.id} className="folder-menu-item" onClick={() => {
                        closeMenu();
                        setExpanded((s) => new Set(s).add(d.id)); // reveal the moved folder
                        onFolderMove && onFolderMove(f, d.id);
                      }}>
                        <IcFolder size={13} /> <span className="folder-menu-path">{pathLabel(d)}</span>
                      </button>
                    ))}
                    {dests.length === 0 && atTop && (
                      <div className="folder-menu-empty">No other folders to move into.</div>
                    )}
                  </div>
                </>
              )}
            </div>
          </div>
        );
      })()}
    </aside>
  );
}

function AssetCard({ asset, onOpen, onOpenParent, onFindSimilar, selected, onToggleSelect, selectionMode, matchReason }) {
  const pubs = asset.publications || [];
  const isCrop = asset.kind === "crop";
  const variantCount = asset.variantIds ? asset.variantIds.length : 0;
  const parent = isCrop ? ASSETS.find((p) => p.id === asset.parentId) : null;
  const aspect = isCrop ? `${asset.w} / ${asset.h}` : "4 / 3";
  // Workflow label lookup — only meaningful on server assets, but harmless on others.
  const { byId: wfById } = (typeof window.VandayServer !== "undefined" && window.VandayServer.useWorkflowLabels)
    ? window.VandayServer.useWorkflowLabels()
    : { byId: () => null };
  const wfLabel = asset.workflowLabelId ? wfById(asset.workflowLabelId) : null;

  const handleCardClick = (e) => {
    // On desktop: holding shift / meta or being already in selectionMode
    // means a card tap toggles selection (fluid multi-select). On phones the
    // "always toggle once anything is selected" pattern is confusing — Leigh
    // had 3 stale selected from a previous view and couldn't open a new asset
    // from search results. Mobile gets a simpler rule: card tap always opens.
    // To select on mobile, tap the visible checkbox in the top-right corner.
    const isMobile = typeof window !== "undefined"
      && window.matchMedia
      && window.matchMedia("(max-width: 820px)").matches;
    if (e.shiftKey || e.metaKey || (selectionMode && !isMobile)) {
      onToggleSelect && onToggleSelect(asset.id, e.shiftKey);
      return;
    }
    onOpen(asset);
  };

  // Drag payload: a JSON list of asset ids. If this card is part of a
  // selection, the whole selection drags; otherwise just this asset.
  const onDragStart = (e) => {
    if (!asset.isServer || asset.isServerVariant) {
      // Sample images can't actually be moved, so disable drag for them.
      e.preventDefault();
      return;
    }
    const win = window;
    const sel = win.__currentSelection__ || new Set();
    const ids = sel.has(asset.id) && sel.size > 1 ? Array.from(sel) : [asset.id];
    e.dataTransfer.effectAllowed = "move";
    e.dataTransfer.setData("application/x-vanday-ids", JSON.stringify(ids));
    e.dataTransfer.setData("text/plain", ids.join(","));
  };

  return (
    <article
      className={`card ${selected ? "is-selected" : ""}`}
      onClick={handleCardClick}
      onDoubleClick={() => onOpen(asset)}
      draggable={!!asset.isServer && !asset.isServerVariant}
      onDragStart={onDragStart}
    >
      <div className={`thumb ${asset.hasAlpha ? "is-alpha" : ""}`} style={isCrop ? { aspectRatio: aspect } : undefined}>
        {assetIsVideo(asset) ? (
          <>
            <video
              src={asset.url}
              muted
              playsInline
              preload="metadata"
              style={{ width: "100%", height: "100%", objectFit: "cover", display: "block" }}
            />
            <span
              aria-hidden="true"
              style={{
                position: "absolute", top: "50%", left: "50%",
                transform: "translate(-50%, -50%)",
                width: 40, height: 40, borderRadius: 999,
                background: "rgba(0,0,0,0.55)", color: "#fff",
                display: "flex", alignItems: "center", justifyContent: "center",
                pointerEvents: "none",
              }}
            >
              <IcPlay size={18} />
            </span>
          </>
        ) : (
          <img
            src={thumbSrc(asset, 400)}
            alt={asset.name}
            loading="lazy"
            // Don't upscale images that are smaller than their tile — a tiny
            // logo or icon should show at its real size, centered, not blown
            // up and blurry. If both dimensions fit inside the tile, switch
            // off the fill (cover) sizing.
            onLoad={(e) => {
              const img = e.currentTarget;
              const small =
                img.naturalWidth > 0 &&
                img.naturalWidth < img.clientWidth &&
                img.naturalHeight < img.clientHeight;
              img.classList.toggle("is-small", small);
            }}
            // If the server thumb 415s (non-raster), fall back to the
            // original so the card never shows a broken image.
            onError={(e) => {
              if (e.currentTarget.src !== asset.url && asset.url) {
                e.currentTarget.src = asset.url;
              }
            }}
          />
        )}
        <button
          className={`card-check ${selected ? "is-on" : ""}`}
          onClick={(e) => { e.stopPropagation(); onToggleSelect && onToggleSelect(asset.id, e.shiftKey); }}
          title={selected ? "Deselect" : "Select"}
        >
          {selected && <IcCheck size={11} />}
        </button>
        {isCrop && (
          <span className="ratio-tag" style={{ position: "absolute", top: 8, left: 8 }}>
            {asset.ratio}
          </span>
        )}
        {asset.isDemo && !isCrop && (
          <span
            title="Sample image we added to get you started — feel free to delete."
            style={{
              position: "absolute", top: 8, right: 8,
              background: "oklch(0.95 0.04 280)",
              color: "oklch(0.32 0.13 280)",
              padding: "2px 8px", borderRadius: 999,
              fontSize: 10, fontWeight: 600, letterSpacing: 0.4,
              textTransform: "uppercase",
              boxShadow: "0 1px 3px rgba(0,0,0,0.12)",
            }}
          >
            Demo
          </span>
        )}
        {!isCrop && variantCount > 0 && (
          <span
            title={`${variantCount} auto-generated crops`}
            style={{
              position: "absolute", top: 8, left: 8,
              background: "oklch(1 0 0 / 0.92)", color: "var(--text)",
              padding: "2px 7px", borderRadius: 4,
              fontFamily: "var(--font-mono)", fontSize: 10,
              display: "inline-flex", alignItems: "center", gap: 4,
            }}
          >
            <IcCrop size={10} /> +{variantCount}
          </span>
        )}
        {/* Hover-revealed similar button — FT-07 */}
        {!selectionMode && onFindSimilar && (
          <button
            className="card-similar"
            onClick={(e) => { e.stopPropagation(); onFindSimilar(asset); }}
            title="Find similar"
          >
            <IcSparkles size={11} /> Similar
          </button>
        )}
        {pubs.length > 0 && (
          <div className="site-strip" title={`Published to ${pubs.map((p) => p.site).join(", ")}`}>
            {pubs.slice(0, 3).map((p, i) => (
              <SiteMark key={i} site={p.site} size={14} />
            ))}
            {pubs.length > 3 && <span style={{ marginLeft: 2 }}>+{pubs.length - 3}</span>}
          </div>
        )}
        {wfLabel && (
          <span
            className={`card-wf-badge wf-color-${wfLabel.color}`}
            title={`Status: ${wfLabel.name}`}
          >
            <span className="wf-dot" />
            {wfLabel.name}
          </span>
        )}
        {/* FT-15 \u2014 expiry badge */}
        {asset.expiry && (
          <div
            className={`expiry-badge is-${asset.expiry.status}`}
            title={
              asset.expiry.status === "expired"
                ? `Expired ${asset.expiry.date}`
                : `Expires ${asset.expiry.date} · ${asset.expiry.daysLeft}d left`
            }
          >
            <IcCalendar size={9} />
            {asset.expiry.status === "expired" ? "Expired" : `${asset.expiry.daysLeft}d`}
          </div>
        )}
      </div>
      <div className="card-body">
        <div className="card-name">{asset.name}</div>
        <div className="card-meta">
          {isCrop ? (
            <>
              <span style={{ display: "inline-flex", alignItems: "center", gap: 4 }}>
                <IcLink size={11} />
                <a
                  href="#"
                  onClick={(e) => { e.stopPropagation(); onOpenParent && onOpenParent(parent); }}
                  style={{ color: "var(--text-muted)", textDecoration: "none" }}
                  title={`Crop of ${parent?.name}`}
                >
                  {parent?.name}
                </a>
              </span>
            </>
          ) : (
            <>
              <span>{asset.uploader}</span>
              <span style={{ color: "var(--text-faint)" }}>·</span>
              <span style={{ fontFamily: "var(--font-mono)" }}>{asset.size}</span>
            </>
          )}
        </div>
        {isCrop ? (
          <div className="tag-row">
            {asset.sites.map((sid) => (
              <span key={sid} className="tag" style={{ display: "inline-flex", alignItems: "center", gap: 4 }}>
                <SiteMark site={sid} size={10} /> {SITES.find((s) => s.id === sid)?.name}
              </span>
            ))}
          </div>
        ) : (
          <div className="tag-row">
            <span className="tag ai">
              <IcSparkles size={9} style={{ verticalAlign: "middle", marginRight: 3 }} />
              {asset.tags[0]}
            </span>
            <span className="tag ai">{asset.tags[1]}</span>
            {asset.tags[2] && <span className="tag">+{asset.tags.length - 2}</span>}
          </div>
        )}
        {/* Why this result? — FT-08 */}
        {matchReason && (
          <div className="match-why" title={matchReason.full}>
            <IcInfo size={10} /> via <strong>{matchReason.kind}</strong>
            <span className="match-why-tags">{matchReason.bits.join(" · ")}</span>
          </div>
        )}
      </div>
    </article>
  );
}

// Compute orientation bucket from an asset's dimensions.
function orientationOf(a) {
  const r = a.w / a.h;
  if (Math.abs(r - 1) < 0.06) return "square";
  return r > 1 ? "landscape" : "portrait";
}

// Fake "why this result?" data — picks plausible match reasons given a query.
// Real impl would come from the search backend.
function matchReasonFor(asset, query) {
  if (!query) return null;
  const q = query.toLowerCase().trim();
  const bits = [];
  const tags = (asset.tags || []).filter((t) => q.includes(t.toLowerCase()) || t.toLowerCase().includes(q.split(" ")[0] || ""));
  if (tags.length) bits.push(...tags.slice(0, 2).map((t) => `“${t}”`));
  if (asset.name.toLowerCase().includes(q.split(" ")[0] || "")) bits.push("filename");
  // Crude "semantic" signal — prepend a visual-concept reason for longer queries.
  let kind = "keyword";
  if (q.split(/\s+/).length >= 3) {
    kind = "visual + keyword";
    if (bits.length === 0) bits.push("composition", "color");
  }
  if (bits.length === 0) bits.push("tag overlap");
  return {
    kind,
    bits,
    full: `Matched on: ${bits.join(", ")}`,
  };
}

// ============================================================
// BulkTagModal — replaces the unstyled window.prompt for bulk-tagging.
// Lets the user type a comma-separated list of tags (or hit Enter on
// each one chip-style) and applies them to every selected asset.
// ============================================================
function BulkTagModal({ count, onApply, onClose }) {
  const [raw, setRaw] = React.useState("");
  const inputRef = React.useRef(null);
  React.useEffect(() => { inputRef.current && inputRef.current.focus(); }, []);
  React.useEffect(() => {
    const onKey = (e) => { if (e.key === "Escape") onClose(); };
    window.addEventListener("keydown", onKey);
    document.body.style.overflow = "hidden";
    return () => {
      window.removeEventListener("keydown", onKey);
      document.body.style.overflow = "";
    };
  }, [onClose]);

  const tags = raw
    .split(",")
    .map((t) => t.trim().toLowerCase().replace(/^#/, ""))
    .filter(Boolean);

  const submit = () => {
    if (tags.length === 0) return;
    onApply(tags);
  };

  return (
    <div className="modal-backdrop" onClick={onClose}>
      <div className="modal" onClick={(e) => e.stopPropagation()} style={{ maxWidth: 460 }}>
        <header className="modal-head">
          <div>
            <h2 className="modal-title">Add tags</h2>
            <div style={{ fontSize: 12.5, color: "var(--text-muted)", marginTop: 2 }}>
              Will be applied to <strong style={{ color: "var(--text)", fontWeight: 600 }}>{count}</strong> {count === 1 ? "asset" : "assets"}.
            </div>
          </div>
          <button className="btn ghost sm" onClick={onClose} title="Close"><IcClose size={16} /></button>
        </header>
        <div className="modal-body" style={{ display: "block", padding: 20 }}>
          <label style={{ fontSize: 11, textTransform: "uppercase", letterSpacing: "0.06em", color: "var(--text-faint)", fontWeight: 500 }}>
            Tags
          </label>
          <input
            ref={inputRef}
            type="text"
            value={raw}
            onChange={(e) => setRaw(e.target.value)}
            onKeyDown={(e) => { if (e.key === "Enter") { e.preventDefault(); submit(); } }}
            placeholder="autumn, brand, hero-image"
            style={{
              width: "100%", marginTop: 6,
              padding: "10px 12px",
              fontSize: 14,
              background: "var(--surface)",
              border: "1px solid var(--border)",
              borderRadius: 8,
              color: "var(--text)",
              outline: "none",
              boxSizing: "border-box",
            }}
          />
          <div style={{ fontSize: 12, color: "var(--text-muted)", marginTop: 8, lineHeight: 1.5 }}>
            Comma-separated. Hashtags and casing are normalized. New tags are merged with whatever is already on each asset — nothing is replaced.
          </div>
          {tags.length > 0 && (
            <div style={{ display: "flex", flexWrap: "wrap", gap: 6, marginTop: 12 }}>
              {tags.map((t) => (
                <span key={t} style={{
                  fontSize: 11.5, padding: "3px 9px", borderRadius: 999,
                  background: "var(--surface-sunken)", color: "var(--text)",
                  border: "1px solid var(--border)",
                }}>#{t}</span>
              ))}
            </div>
          )}
        </div>
        <footer className="modal-foot">
          <button className="btn" onClick={onClose}>Cancel</button>
          <button
            className="btn primary"
            onClick={submit}
            disabled={tags.length === 0}
          >
            <IcTag size={13} /> Add {tags.length || ""} {tags.length === 1 ? "tag" : "tags"}
          </button>
        </footer>
      </div>
    </div>
  );
}

// ============================================================
// NewFolderModal — styled replacement for the old window.prompt folder
// naming flow. Handles both top-level folders and subfolders (when a
// parent name is supplied for context).
// ============================================================
function NewFolderModal({ parentName, onCreate, onClose }) {
  const [name, setName] = React.useState("");
  const [busy, setBusy] = React.useState(false);
  const [error, setError] = React.useState(null);
  const inputRef = React.useRef(null);
  React.useEffect(() => { inputRef.current && inputRef.current.focus(); }, []);
  React.useEffect(() => {
    const onKey = (e) => { if (e.key === "Escape") onClose(); };
    window.addEventListener("keydown", onKey);
    document.body.style.overflow = "hidden";
    return () => {
      window.removeEventListener("keydown", onKey);
      document.body.style.overflow = "";
    };
  }, [onClose]);

  const submit = async () => {
    const trimmed = name.trim();
    if (!trimmed || busy) return;
    setBusy(true); setError(null);
    try {
      await onCreate(trimmed);
    } catch (e) {
      setError(e.message || "Could not create folder.");
      setBusy(false);
    }
  };

  return (
    <div className="modal-backdrop" onClick={onClose}>
      <div className="modal" onClick={(e) => e.stopPropagation()} style={{ maxWidth: 440 }}>
        <header className="modal-head">
          <div>
            <h2 className="modal-title">{parentName ? "New subfolder" : "New folder"}</h2>
            <div style={{ fontSize: 12.5, color: "var(--text-muted)", marginTop: 2 }}>
              {parentName
                ? <>Inside <strong style={{ color: "var(--text)", fontWeight: 600 }}>{parentName}</strong></>
                : "Folders help you organize your assets."}
            </div>
          </div>
          <button className="btn ghost sm" onClick={onClose} title="Close"><IcClose size={16} /></button>
        </header>
        <div className="modal-body" style={{ display: "block", padding: 20 }}>
          <label style={{ fontSize: 11, textTransform: "uppercase", letterSpacing: "0.06em", color: "var(--text-faint)", fontWeight: 500 }}>
            Folder name
          </label>
          <input
            ref={inputRef}
            type="text"
            value={name}
            onChange={(e) => setName(e.target.value)}
            onKeyDown={(e) => { if (e.key === "Enter") { e.preventDefault(); submit(); } }}
            placeholder={parentName ? "e.g. Drafts" : "e.g. Campaigns"}
            style={{
              width: "100%", marginTop: 6,
              padding: "10px 12px",
              fontSize: 14,
              background: "var(--surface)",
              border: "1px solid var(--border)",
              borderRadius: 8,
              color: "var(--text)",
              outline: "none",
              boxSizing: "border-box",
            }}
          />
          {error && (
            <div style={{ fontSize: 12.5, color: "var(--danger, oklch(0.6 0.18 25))", marginTop: 8 }}>{error}</div>
          )}
        </div>
        <footer className="modal-foot">
          <button className="btn" onClick={onClose}>Cancel</button>
          <button className="btn primary" onClick={submit} disabled={!name.trim() || busy}>
            <IcFolder size={13} /> {busy ? "Creating…" : "Create folder"}
          </button>
        </footer>
      </div>
    </div>
  );
}

// ============================================================
// DeleteFolderModal — a prominent, opt-in confirmation before deleting a
// folder. Spells out exactly what happens: the folder (and any nested
// subfolders) are removed, and every asset inside is moved to the Inbox —
// files are never deleted. Counts are computed from the live client state.
// ============================================================
function DeleteFolderModal({ folder, activeFolder, onConfirm, onClose }) {
  const [busy, setBusy] = React.useState(false);
  const [error, setError] = React.useState(null);
  React.useEffect(() => {
    const onKey = (e) => { if (e.key === "Escape" && !busy) onClose(); };
    window.addEventListener("keydown", onKey);
    document.body.style.overflow = "hidden";
    return () => { window.removeEventListener("keydown", onKey); document.body.style.overflow = ""; };
  }, [onClose, busy]);

  const { subfolderCount, assetCount } = React.useMemo(() => {
    const ids = [folder.id];
    const gather = (id) => {
      const kids = (typeof window.childFolders === "function" ? window.childFolders(id) : []) || [];
      for (const k of kids) { ids.push(k.id); gather(k.id); }
    };
    gather(folder.id);
    const idSet = new Set(ids);
    const assets = (window.ASSETS || []).filter((a) => a.isServer && idSet.has(a.folder));
    return { subfolderCount: ids.length - 1, assetCount: assets.length };
  }, [folder.id]);

  const confirm = async () => {
    if (busy) return;
    setBusy(true); setError(null);
    try { await onConfirm(); }
    catch (e) { setError(e.message || "Could not delete the folder."); setBusy(false); }
  };

  return (
    <div className="modal-backdrop" onClick={() => !busy && onClose()}>
      <div className="modal" onClick={(e) => e.stopPropagation()} style={{ maxWidth: 460 }}>
        <header className="modal-head">
          <div>
            <h2 className="modal-title">Delete folder?</h2>
            <div style={{ fontSize: 12.5, color: "var(--text-muted)", marginTop: 2 }}>It moves to Recently deleted — recoverable for 30 days.</div>
          </div>
          <button className="btn ghost sm" onClick={onClose} title="Close"><IcClose size={16} /></button>
        </header>
        <div className="modal-body" style={{ display: "block", padding: 20 }}>
          <div style={{ display: "flex", gap: 12, alignItems: "flex-start", padding: "14px 16px", borderRadius: 10, background: "color-mix(in oklch, var(--accent, #c8553d) 8%, var(--surface))", border: "1px solid color-mix(in oklch, var(--accent, #c8553d) 30%, var(--border))" }}>
            <IcTrash size={18} style={{ color: "var(--accent, #c8553d)", flexShrink: 0, marginTop: 1 }} />
            <div style={{ fontSize: 13.5, lineHeight: 1.55 }}>
              <strong>"{folder.name}"</strong>
              {subfolderCount > 0 && <> and its <strong>{subfolderCount}</strong> subfolder{subfolderCount === 1 ? "" : "s"}</>}
              {assetCount > 0
                ? <> (with <strong>{assetCount}</strong> asset{assetCount === 1 ? "" : "s"})</>
                : null}
              {" "}will move to <strong>Recently deleted</strong>. You can restore it for <strong>30 days</strong> before it's permanently removed — nothing is deleted right now.
            </div>
          </div>
          {error && <div style={{ fontSize: 12.5, color: "var(--danger, oklch(0.6 0.18 25))", marginTop: 10 }}>{error}</div>}
        </div>
        <footer className="modal-foot">
          <button className="btn" onClick={onClose} disabled={busy}>Cancel</button>
          <button
            className="btn"
            onClick={confirm}
            disabled={busy}
            style={{ background: "var(--accent, #c8553d)", color: "#fff", borderColor: "var(--accent, #c8553d)", fontWeight: 600 }}
          >
            <IcTrash size={13} /> {busy ? "Deleting…" : "Delete folder"}
          </button>
        </footer>
      </div>
    </div>
  );
}

// ============================================================
// TrashView — "Recently deleted". Lists trashed folders (each as one
// restorable entry) and individually-deleted assets, each with a Restore
// action. Items are permanently purged after the retention window.
// ============================================================
function TrashView({ onBack, onChanged }) {
  const [data, setData] = React.useState(null); // { folders, assets, retentionDays }
  const [busyId, setBusyId] = React.useState(null);
  const [error, setError] = React.useState(null);
  const rowStyle = { display: "flex", alignItems: "center", gap: 12, padding: "12px 14px", borderBottom: "1px solid var(--border)" };

  const load = React.useCallback(() => {
    VandayAPI.listTrash().then(setData).catch(() => setError("Couldn't load Recently deleted."));
  }, []);
  React.useEffect(() => { load(); }, [load]);

  const doRestore = async (kind, item) => {
    const key = kind + ":" + item.id;
    setBusyId(key); setError(null);
    try {
      if (kind === "f") await VandayAPI.restoreFolder(item.id);
      else await VandayAPI.restoreAsset(item.id);
      onChanged && onChanged();
      load();
    } catch { setError("Couldn't restore that item. Please try again."); }
    finally { setBusyId(null); }
  };

  const ago = (ts) => {
    if (!ts) return "";
    const days = Math.floor((Date.now() - ts) / (24 * 60 * 60 * 1000));
    return days <= 0 ? "today" : days === 1 ? "yesterday" : `${days} days ago`;
  };
  const retention = data?.retentionDays || 30;
  const folders = data?.folders || [];
  const assets = data?.assets || [];
  const empty = data && folders.length === 0 && assets.length === 0;

  return (
    <main className="main">
      <header className="topbar">
        <div className="crumbs">
          <span>{window.WorkspaceName ? <WorkspaceName /> : "Library"}</span>
          <span className="sep">/</span>
          <span className="here">Recently deleted</span>
        </div>
      </header>
      <section className="lib-header">
        <div>
          <h1 className="lib-title">Recently deleted</h1>
          <div className="lib-sub"><span>Restore anything here. Items are permanently removed after {retention} days.</span></div>
        </div>
      </section>
      <section style={{ padding: "4px 24px 40px" }}>
        {error && <div style={{ color: "var(--accent, #c8553d)", fontSize: 13, marginBottom: 12 }}>{error}</div>}
        {!data && <div style={{ color: "var(--text-muted)", fontSize: 13, padding: 24 }}>Loading…</div>}
        {empty && (
          <div style={{ textAlign: "center", padding: "80px 24px", color: "var(--text-faint)", fontSize: 13 }}>
            <div style={{ marginBottom: 10 }}><IcTrash size={28} style={{ color: "var(--text-faint)" }} /></div>
            <strong style={{ color: "var(--text)", display: "block", fontSize: 14, marginBottom: 4 }}>Nothing in Recently deleted</strong>
            Deleted folders and assets show up here, recoverable for {retention} days.
          </div>
        )}
        {folders.map((f) => (
          <div key={"f" + f.id} style={rowStyle}>
            <span style={{ color: "var(--text-muted)", flexShrink: 0 }}><IcFolder size={18} /></span>
            <div style={{ minWidth: 0, flex: 1 }}>
              <div style={{ fontWeight: 600, fontSize: 13.5 }}>{f.name}</div>
              <div style={{ fontSize: 11.5, color: "var(--text-muted)" }}>
                Folder · {f.assetCount} asset{f.assetCount === 1 ? "" : "s"}{f.subfolderCount > 0 ? ` · ${f.subfolderCount} subfolder${f.subfolderCount === 1 ? "" : "s"}` : ""} · deleted {ago(f.deletedAt)}
              </div>
            </div>
            <button className="btn sm" disabled={busyId === "f:" + f.id} onClick={() => doRestore("f", f)}>
              {busyId === "f:" + f.id ? "Restoring…" : "Restore"}
            </button>
          </div>
        ))}
        {assets.map((a) => (
          <div key={"a" + a.id} style={rowStyle}>
            <div style={{ width: 36, height: 36, borderRadius: 6, backgroundColor: "var(--surface-sunken)", backgroundImage: a.url ? `url(${a.url})` : undefined, backgroundSize: "cover", backgroundPosition: "center", flexShrink: 0 }} />
            <div style={{ minWidth: 0, flex: 1 }}>
              <div style={{ fontWeight: 600, fontSize: 13.5, whiteSpace: "nowrap", overflow: "hidden", textOverflow: "ellipsis" }}>{a.name}</div>
              <div style={{ fontSize: 11.5, color: "var(--text-muted)" }}>Asset · deleted {ago(a.deletedAt)}</div>
            </div>
            <button className="btn sm" disabled={busyId === "a:" + a.id} onClick={() => doRestore("a", a)}>
              {busyId === "a:" + a.id ? "Restoring…" : "Restore"}
            </button>
          </div>
        ))}
      </section>
    </main>
  );
}

// ============================================================
// BulkLabelModal — picks a workflow label to apply to every selected
// asset. Labels are user-configured per-org via Settings → Workflow.
// Includes a "Clear label" option that nulls the workflowLabelId.
// ============================================================
function BulkLabelModal({ count, labels, onApply, onClose }) {
  React.useEffect(() => {
    const onKey = (e) => { if (e.key === "Escape") onClose(); };
    window.addEventListener("keydown", onKey);
    document.body.style.overflow = "hidden";
    return () => {
      window.removeEventListener("keydown", onKey);
      document.body.style.overflow = "";
    };
  }, [onClose]);

  return (
    <div className="modal-backdrop" onClick={onClose}>
      <div className="modal" onClick={(e) => e.stopPropagation()} style={{ maxWidth: 460 }}>
        <header className="modal-head">
          <div>
            <h2 className="modal-title">Set workflow label</h2>
            <div style={{ fontSize: 12.5, color: "var(--text-muted)", marginTop: 2 }}>
              Apply a workflow status to <strong style={{ color: "var(--text)", fontWeight: 600 }}>{count}</strong> {count === 1 ? "asset" : "assets"}.
            </div>
          </div>
          <button className="btn ghost sm" onClick={onClose} title="Close"><IcClose size={16} /></button>
        </header>
        <div className="modal-body" style={{ display: "block", padding: 12 }}>
          {(!labels || labels.length === 0) ? (
            <div style={{ padding: "24px 12px", textAlign: "center", color: "var(--text-muted)", fontSize: 13 }}>
              No workflow labels yet. Add some in <strong style={{ color: "var(--text)" }}>Settings → Workflow</strong>, then come back.
            </div>
          ) : (
            <div style={{ display: "flex", flexDirection: "column", gap: 2 }}>
              {labels.map((l) => (
                <button
                  key={l.id}
                  type="button"
                  onClick={() => onApply(l.id)}
                  style={{
                    display: "flex", alignItems: "center", gap: 10,
                    padding: "10px 12px",
                    background: "transparent",
                    border: 0, borderRadius: 8,
                    cursor: "pointer", textAlign: "left",
                    fontSize: 13.5,
                  }}
                  onMouseEnter={(e) => { e.currentTarget.style.background = "var(--surface-sunken)"; }}
                  onMouseLeave={(e) => { e.currentTarget.style.background = "transparent"; }}
                >
                  {/* Dot uses the wf-color text color via currentColor. The
                      label text itself stays in the default text color so
                      it's readable on every theme. */}
                  <span className={`wf-color-${l.color}`} style={{
                    display: "inline-block", width: 12, height: 12, borderRadius: 4,
                    background: "currentColor", opacity: 0.9, flexShrink: 0,
                  }} />
                  <span style={{ color: "var(--text)", fontWeight: 500 }}>{l.name}</span>
                </button>
              ))}
              <div style={{ height: 1, background: "var(--border)", margin: "6px 0" }} />
              <button
                type="button"
                onClick={() => onApply(null)}
                style={{
                  display: "flex", alignItems: "center", gap: 10,
                  padding: "10px 12px",
                  background: "transparent",
                  border: 0, borderRadius: 8,
                  cursor: "pointer", textAlign: "left",
                  fontSize: 13.5, color: "var(--text-muted)",
                }}
                onMouseEnter={(e) => { e.currentTarget.style.background = "var(--surface-sunken)"; }}
                onMouseLeave={(e) => { e.currentTarget.style.background = "transparent"; }}
              >
                <IcClose size={12} />
                <span>Clear label</span>
              </button>
            </div>
          )}
        </div>
        <footer className="modal-foot">
          <button className="btn" onClick={onClose}>Cancel</button>
        </footer>
      </div>
    </div>
  );
}

// ============================================================
// BulkMoveModal — pick a destination folder for the selected assets.
// Replaces the old window.prompt("type a number") flow with a styled
// folder list that matches BulkLabelModal.
// ============================================================
function BulkMoveModal({ count, folders, onApply, onClose }) {
  React.useEffect(() => {
    const onKey = (e) => { if (e.key === "Escape") onClose(); };
    window.addEventListener("keydown", onKey);
    document.body.style.overflow = "hidden";
    return () => {
      window.removeEventListener("keydown", onKey);
      document.body.style.overflow = "";
    };
  }, [onClose]);

  return (
    <div className="modal-backdrop" onClick={onClose}>
      <div className="modal" onClick={(e) => e.stopPropagation()} style={{ maxWidth: 460 }}>
        <header className="modal-head">
          <div>
            <h2 className="modal-title">Move to folder</h2>
            <div style={{ fontSize: 12.5, color: "var(--text-muted)", marginTop: 2 }}>
              Move <strong style={{ color: "var(--text)", fontWeight: 600 }}>{count}</strong> {count === 1 ? "asset" : "assets"} into a folder.
            </div>
          </div>
          <button className="btn ghost sm" onClick={onClose} title="Close"><IcClose size={16} /></button>
        </header>
        <div className="modal-body" style={{ display: "block", padding: 12 }}>
          {(!folders || folders.length === 0) ? (
            <div style={{ padding: "24px 12px", textAlign: "center", color: "var(--text-muted)", fontSize: 13 }}>
              No folders yet. Create one first, then come back.
            </div>
          ) : (
            <div style={{ display: "flex", flexDirection: "column", gap: 2, maxHeight: 360, overflowY: "auto" }}>
              {folders.map((f) => (
                <button
                  key={f.id}
                  type="button"
                  onClick={() => onApply(f.id)}
                  style={{
                    display: "flex", alignItems: "center", gap: 10,
                    padding: "10px 12px",
                    background: "transparent",
                    border: 0, borderRadius: 8,
                    cursor: "pointer", textAlign: "left",
                    fontSize: 13.5,
                  }}
                  onMouseEnter={(e) => { e.currentTarget.style.background = "var(--surface-sunken)"; }}
                  onMouseLeave={(e) => { e.currentTarget.style.background = "transparent"; }}
                >
                  <IcFolder size={15} style={{ color: "var(--text-muted)", flexShrink: 0 }} />
                  <span style={{ color: "var(--text)", fontWeight: 500 }}>{f.name}</span>
                </button>
              ))}
            </div>
          )}
        </div>
        <footer className="modal-foot">
          <button className="btn" onClick={onClose}>Cancel</button>
        </footer>
      </div>
    </div>
  );
}

// Move-from-Inbox modal (DAM Refresh design). Opens from an empty folder's
// "Move from Inbox" action: a searchable, multi-select grid of unfiled
// (Inbox) assets that get filed into the current folder on confirm.
function MoveFromInboxModal({ targetFolderId, targetFolderName, onClose, onMoved }) {
  const inbox = React.useMemo(
    () => (window.ASSETS || []).filter(
      (a) => a.isServer && a.kind !== "crop" && (a.folder === "raw" || a.folder == null)
    ),
    [],
  );
  const [query, setQuery] = React.useState("");
  const [sel, setSel] = React.useState(() => new Set());
  const [busy, setBusy] = React.useState(false);

  React.useEffect(() => {
    const onKey = (e) => { if (e.key === "Escape") onClose(); };
    window.addEventListener("keydown", onKey);
    document.body.style.overflow = "hidden";
    return () => { window.removeEventListener("keydown", onKey); document.body.style.overflow = ""; };
  }, [onClose]);

  const shown = query.trim()
    ? inbox.filter((a) => (a.name || "").toLowerCase().includes(query.trim().toLowerCase()))
    : inbox;
  const toggle = (id) => setSel((s) => { const n = new Set(s); n.has(id) ? n.delete(id) : n.add(id); return n; });
  const selectAll = () => setSel(new Set(shown.map((a) => a.id)));

  const apply = async () => {
    if (sel.size === 0 || busy) return;
    setBusy(true);
    const picked = inbox.filter((a) => sel.has(a.id));
    for (const a of picked) {
      a.folder = targetFolderId; // optimistic — mirrors BulkMoveModal
      try { await VandayAPI.patchAsset(a.id, { folder: targetFolderId }); } catch {}
    }
    setBusy(false);
    onMoved && onMoved();
    onClose();
  };

  return (
    <div className="modal-backdrop" onClick={onClose}>
      <div className="modal" onClick={(e) => e.stopPropagation()} style={{ maxWidth: 760, width: "min(760px, 96vw)" }}>
        <header className="modal-head">
          <div>
            <h2 className="modal-title">Move from Inbox</h2>
            <div style={{ fontSize: 12.5, color: "var(--text-muted)", marginTop: 3 }}>
              Pick the assets to file into <strong style={{ color: "var(--text)", fontWeight: 600 }}>{targetFolderName}</strong>. They'll leave your Inbox once moved.
            </div>
          </div>
          <button className="btn ghost sm" onClick={onClose} title="Close"><IcClose size={16} /></button>
        </header>

        <div style={{ padding: "12px 16px", display: "flex", alignItems: "center", gap: 12, borderBottom: "1px solid var(--border)" }}>
          <div style={{ display: "flex", alignItems: "center", gap: 8, flex: 1, background: "var(--surface-soft)", border: "1px solid var(--border)", borderRadius: 8, padding: "7px 12px", color: "var(--text-muted)" }}>
            <IcSearch size={14} />
            <input
              value={query}
              onChange={(e) => setQuery(e.target.value)}
              placeholder="Search Inbox…"
              style={{ border: 0, background: "transparent", outline: "none", flex: 1, fontSize: 13, color: "var(--text)" }}
            />
          </div>
          <button className="btn ghost sm" onClick={selectAll} style={{ whiteSpace: "nowrap" }}>Select all ({shown.length})</button>
        </div>

        <div className="modal-body" style={{ display: "block", padding: 16, overflowY: "auto", maxHeight: "min(56vh, 520px)" }}>
          {inbox.length === 0 ? (
            <div style={{ padding: "32px 12px", textAlign: "center", color: "var(--text-muted)", fontSize: 13 }}>
              Your Inbox is empty — there's nothing to move. New uploads land in the Inbox until you file them.
            </div>
          ) : (
            <div style={{ display: "grid", gridTemplateColumns: "repeat(3, 1fr)", gap: 12 }}>
              {shown.map((a) => {
                const on = sel.has(a.id);
                return (
                  <button
                    key={a.id}
                    type="button"
                    onClick={() => toggle(a.id)}
                    style={{
                      border: `1px solid ${on ? "var(--accent)" : "var(--border)"}`,
                      boxShadow: on ? "0 0 0 2px var(--accent-soft)" : "none",
                      background: "var(--surface)", borderRadius: "var(--radius)",
                      overflow: "hidden", cursor: "pointer", textAlign: "left", padding: 0,
                    }}
                  >
                    <div style={{ aspectRatio: "4 / 3", position: "relative", background: "var(--surface-sunken)" }}>
                      {a.url && <img src={a.url} alt="" loading="lazy" style={{ width: "100%", height: "100%", objectFit: "cover", display: "block" }} />}
                      <span style={{
                        position: "absolute", top: 8, right: 8, width: 22, height: 22, borderRadius: 6,
                        background: on ? "var(--accent)" : "oklch(1 0 0 / 0.92)",
                        border: `1px solid ${on ? "var(--accent)" : "var(--border-strong)"}`,
                        display: "grid", placeItems: "center", color: "#fff",
                      }}>{on && <IcCheck size={12} />}</span>
                    </div>
                    <div style={{ padding: "8px 10px 10px" }}>
                      <div style={{ fontFamily: "var(--font-mono)", fontSize: 11, color: "var(--text)", overflow: "hidden", textOverflow: "ellipsis", whiteSpace: "nowrap" }}>{a.name}</div>
                    </div>
                  </button>
                );
              })}
            </div>
          )}
        </div>

        <footer className="modal-foot">
          <span style={{ fontSize: 13, color: "var(--text-muted)" }}>
            <strong style={{ color: "var(--text)", fontFamily: "var(--font-mono)" }}>{sel.size}</strong> of {inbox.length} selected
          </span>
          <div style={{ marginLeft: "auto", display: "flex", gap: 10 }}>
            <button className="btn" onClick={onClose}>Cancel</button>
            <button
              className="btn accent"
              onClick={apply}
              disabled={sel.size === 0 || busy}
              style={(sel.size === 0 || busy) ? { opacity: 0.5, pointerEvents: "none" } : null}
            >
              <IcFolder size={15} /> Move {sel.size > 0 ? sel.size : ""} to {targetFolderName}
            </button>
          </div>
        </footer>
      </div>
    </div>
  );
}

// ============================================================
// AssetRow — list-view row for the library grid. Same selection &
// double-click semantics as AssetCard but laid out as a horizontal
// row: thumb, name, tags, workflow label, size.
// ============================================================
function AssetRow({ asset, onOpen, selected, onToggleSelect, selectionMode }) {
  const { byId: wfById } = (typeof window.VandayServer !== "undefined" && window.VandayServer.useWorkflowLabels)
    ? window.VandayServer.useWorkflowLabels()
    : { byId: () => null };
  const wfLabel = asset.workflowLabelId ? wfById(asset.workflowLabelId) : null;

  const handleClick = (e) => {
    if (e.shiftKey || e.metaKey || selectionMode) {
      onToggleSelect && onToggleSelect(asset.id, e.shiftKey);
      return;
    }
    onOpen(asset);
  };

  const tags = asset.tags || [];
  const shownTags = tags.slice(0, 3);
  const moreTags = tags.length - shownTags.length;

  return (
    <div
      className={`lib-list-row ${selected ? "is-selected" : ""}`}
      onClick={handleClick}
    >
      <div className="lib-list-cell" style={{ width: 24, justifyContent: "center" }}>
        <button
          type="button"
          className={`row-check ${selected ? "checked" : ""}`}
          onClick={(e) => { e.stopPropagation(); onToggleSelect && onToggleSelect(asset.id, e.shiftKey); }}
          aria-label={selected ? "Unselect" : "Select"}
        >
          {selected && <IcCheck size={11} />}
        </button>
      </div>
      <div className="lib-list-cell" style={{ width: 44 }}>
        {assetIsVideo(asset) ? (
          <div
            className="lib-list-thumb"
            style={{
              background: "oklch(0.28 0.02 260)", color: "#fff",
              display: "flex", alignItems: "center", justifyContent: "center",
            }}
            title="Video"
          >
            <IcPlay size={14} />
          </div>
        ) : (
          <div
            className="lib-list-thumb"
            style={{ backgroundImage: `url(${thumbSrc(asset, 200)})` }}
          />
        )}
      </div>
      <div className="lib-list-cell" style={{ flex: 2, minWidth: 0, flexDirection: "column", alignItems: "flex-start", gap: 2 }}>
        <div className="lib-list-name">{asset.name}</div>
        <div className="lib-list-sub">
          {asset.folder || "—"}
          {asset.uploader && <> · {asset.uploader}</>}
        </div>
      </div>
      <div className="lib-list-cell" style={{ flex: 2, minWidth: 0, gap: 4, flexWrap: "wrap" }}>
        {shownTags.map((t) => (
          <span key={t} className="lib-list-tag">#{t}</span>
        ))}
        {moreTags > 0 && (
          <span className="lib-list-tag dim">+{moreTags}</span>
        )}
      </div>
      <div className="lib-list-cell" style={{ width: 110 }}>
        {wfLabel ? (
          <span className={`lib-list-wf wf-color-${wfLabel.color}`}>
            <span className="wf-dot" style={{ background: "currentColor" }} />
            {wfLabel.name}
          </span>
        ) : (
          <span style={{ color: "var(--text-faint)", fontSize: 11.5 }}>—</span>
        )}
      </div>
      <div className="lib-list-cell" style={{ width: 80, justifyContent: "flex-end", fontFamily: "var(--font-mono)", fontSize: 11.5, color: "var(--text-muted)" }}>
        {asset.size}
      </div>
      <div className="lib-list-cell" style={{ width: 28, justifyContent: "center", color: "var(--text-faint)" }}>
        <IcChevR size={12} />
      </div>
    </div>
  );
}

// ============================================================
// ActiveFilterPill — small dismissible chip that shows in the toolbar
// when an orientation / status / variants filter is active. One pill
// per active filter; X clears just that one.
// ============================================================
function ActiveFilterPill({ label, color, onRemove }) {
  return (
    <span
      className={`chip ${color || ""}`}
      style={{
        display: "inline-flex", alignItems: "center", gap: 6,
        fontWeight: 500,
        // Just rely on the wf-color tint for the active visual — a thin
        // 1px border in the chip's own text color reads as "this is set"
        // without screaming. No inset ring.
        borderColor: color ? "currentColor" : "var(--text)",
        borderWidth: 1,
      }}
    >
      {label}
      <button
        type="button"
        onClick={onRemove}
        title={`Remove ${label}`}
        style={{
          display: "inline-flex", alignItems: "center", justifyContent: "center",
          background: "transparent", border: 0, padding: 0,
          color: "inherit", cursor: "pointer", opacity: 0.6,
          marginRight: -2,
        }}
        onMouseEnter={(e) => { e.currentTarget.style.opacity = "1"; }}
        onMouseLeave={(e) => { e.currentTarget.style.opacity = "0.6"; }}
      >
        <IcClose size={11} />
      </button>
    </span>
  );
}

// ============================================================
// SortPopover — small menu under the "Sort: …" button. Same anchor /
// outside-click / Escape behavior as WorkflowLabelPopover. Picking a
// row commits the sort and closes the menu. "Relevance" is only
// offered when the current query is semantic; for other queries it's
// the same thing as Newest and would be confusing to list twice.
// ============================================================
function SortPopover({ anchorRef, onClose, sortBy, setSortBy, isSemanticQuery }) {
  const popRef = React.useRef(null);

  React.useEffect(() => {
    const onMouse = (e) => {
      if (popRef.current && popRef.current.contains(e.target)) return;
      if (anchorRef && anchorRef.current && anchorRef.current.contains(e.target)) return;
      onClose();
    };
    const onKey = (e) => { if (e.key === "Escape") onClose(); };
    document.addEventListener("mousedown", onMouse);
    window.addEventListener("keydown", onKey);
    return () => {
      document.removeEventListener("mousedown", onMouse);
      window.removeEventListener("keydown", onKey);
    };
  }, [anchorRef, onClose]);

  const [pos, setPos] = React.useState({ top: 0, left: 0 });
  React.useLayoutEffect(() => {
    if (!anchorRef || !anchorRef.current) return;
    const r = anchorRef.current.getBoundingClientRect();
    // Right-align under the anchor — Sort sits on the right edge of
    // the toolbar, so anchoring by .right keeps the menu on-screen.
    setPos({ top: r.bottom + 6, left: Math.max(8, r.right - 220) });
  }, [anchorRef]);

  const OPTIONS = [
    // The "auto" row is labeled by what it would resolve to right now,
    // so the user never sees the word "auto".
    isSemanticQuery
      ? { id: "auto",    label: "Relevance",   hint: "Best match for your query" }
      : { id: "auto",    label: "Newest",      hint: "Most recently uploaded first" },
    ...(isSemanticQuery ? [{ id: "newest", label: "Newest", hint: "Most recently uploaded first" }] : []),
    { id: "oldest",   label: "Oldest",       hint: "Earliest uploads first" },
    { id: "name-az",  label: "Name A→Z",     hint: "Alphabetical by filename" },
    { id: "name-za",  label: "Name Z→A",     hint: "Reverse alphabetical" },
    { id: "largest",  label: "Largest",      hint: "Biggest file size first" },
    { id: "smallest", label: "Smallest",     hint: "Smallest file size first" },
  ];

  return (
    <div
      ref={popRef}
      className="lib-filter-popover"
      style={{
        position: "fixed",
        top: pos.top, left: pos.left,
        width: 220,
        background: "var(--surface)",
        border: "1px solid var(--border)",
        borderRadius: 12,
        boxShadow: "0 12px 40px rgba(0,0,0,0.12)",
        padding: 6,
        zIndex: 1000,
        fontSize: 13,
      }}
    >
      <div style={{ display: "flex", flexDirection: "column", gap: 1 }}>
        {OPTIONS.map((opt) => {
          const on = sortBy === opt.id;
          return (
            <button
              key={opt.id}
              type="button"
              onClick={() => { setSortBy(opt.id); onClose(); }}
              title={opt.hint}
              style={{
                display: "flex", alignItems: "center", justifyContent: "space-between",
                gap: 10,
                padding: "9px 10px",
                background: on ? "var(--surface-sunken)" : "transparent",
                border: 0, borderRadius: 8,
                cursor: "pointer", textAlign: "left",
                fontSize: 13.5, color: "var(--text)",
              }}
              onMouseEnter={(e) => { if (!on) e.currentTarget.style.background = "var(--surface-soft)"; }}
              onMouseLeave={(e) => { if (!on) e.currentTarget.style.background = "transparent"; }}
            >
              <span>{opt.label}</span>
              {on && <IcCheck size={13} />}
            </button>
          );
        })}
      </div>
    </div>
  );
}

// ============================================================
// WorkflowLabelPopover — multi-select dropdown anchored under the
// "Workflow label" toolbar button. Each label row is a checkbox; the
// "Unlabeled" row matches assets with no label. Toggling a row
// updates the parent immediately (no Apply button). Click outside or
// hit Escape to close.
// ============================================================
function WorkflowLabelPopover({
  anchorRef, onClose,
  workflowLabels, labelFilter, setLabelFilter,
}) {
  const toggle = (id) => {
    setLabelFilter((prev) => prev.includes(id)
      ? prev.filter((x) => x !== id)
      : [...prev, id]);
  };
  const popRef = React.useRef(null);

  // Close on outside click + Escape.
  React.useEffect(() => {
    const onMouse = (e) => {
      if (popRef.current && popRef.current.contains(e.target)) return;
      if (anchorRef && anchorRef.current && anchorRef.current.contains(e.target)) return;
      onClose();
    };
    const onKey = (e) => { if (e.key === "Escape") onClose(); };
    document.addEventListener("mousedown", onMouse);
    window.addEventListener("keydown", onKey);
    return () => {
      document.removeEventListener("mousedown", onMouse);
      window.removeEventListener("keydown", onKey);
    };
  }, [anchorRef, onClose]);

  // Position the popover directly under the Filter button.
  const [pos, setPos] = React.useState({ top: 0, left: 0 });
  React.useLayoutEffect(() => {
    if (!anchorRef || !anchorRef.current) return;
    const r = anchorRef.current.getBoundingClientRect();
    setPos({ top: r.bottom + 6, left: r.left });
  }, [anchorRef]);

  return (
    <div
      ref={popRef}
      className="lib-filter-popover"
      style={{
        position: "fixed",
        top: pos.top, left: pos.left,
        width: 320,
        background: "var(--surface)",
        border: "1px solid var(--border)",
        borderRadius: 12,
        boxShadow: "0 12px 40px rgba(0,0,0,0.12)",
        padding: 14,
        zIndex: 1000,
        fontSize: 13,
      }}
    >
      <div style={{ display: "flex", alignItems: "center", justifyContent: "space-between", marginBottom: 8 }}>
        <strong style={{ fontSize: 13.5 }}>Filter by workflow</strong>
        {labelFilter.length > 0 && (
          <button
            type="button"
            onClick={() => setLabelFilter([])}
            style={{
              background: "transparent", border: 0, padding: 0,
              color: "var(--accent, #c8553d)",
              fontSize: 12, cursor: "pointer",
              textDecoration: "underline", textUnderlineOffset: 2,
            }}
          >
            Clear
          </button>
        )}
      </div>

      {workflowLabels.length === 0 ? (
        <div style={{ padding: "12px 4px", fontSize: 12.5, color: "var(--text-muted)", lineHeight: 1.5 }}>
          No workflow labels yet. Add some in <strong style={{ color: "var(--text)" }}>Settings → Workflow</strong>, then come back.
        </div>
      ) : (
        <div style={{ display: "flex", flexDirection: "column", gap: 2 }}>
          {workflowLabels.map((l) => {
            const on = labelFilter.includes(l.id);
            return (
              <button
                key={l.id}
                type="button"
                onClick={() => toggle(l.id)}
                style={{
                  display: "flex", alignItems: "center", gap: 10,
                  padding: "9px 10px",
                  background: on ? "var(--surface-sunken)" : "transparent",
                  border: 0, borderRadius: 8,
                  cursor: "pointer", textAlign: "left",
                  fontSize: 13.5,
                }}
                onMouseEnter={(e) => { if (!on) e.currentTarget.style.background = "var(--surface-soft)"; }}
                onMouseLeave={(e) => { if (!on) e.currentTarget.style.background = "transparent"; }}
              >
                {/* Tri-state checkbox glyph — filled when on, hollow
                    when off. Lets the user see at-a-glance which
                    labels are part of the current multi-select set. */}
                <span style={{
                  display: "inline-flex", alignItems: "center", justifyContent: "center",
                  width: 16, height: 16, borderRadius: 4,
                  border: `1.5px solid ${on ? "var(--accent, #c8553d)" : "var(--border-strong, #d8d6cf)"}`,
                  background: on ? "var(--accent, #c8553d)" : "transparent",
                  color: "white",
                  flexShrink: 0,
                }}>
                  {on && <IcCheck size={11} />}
                </span>
                <span className={`wf-color-${l.color}`} style={{
                  display: "inline-block", width: 10, height: 10, borderRadius: 999,
                  background: "currentColor", opacity: 0.9, flexShrink: 0,
                }} />
                <span style={{ color: "var(--text)", fontWeight: on ? 600 : 500, flex: 1 }}>{l.name}</span>
              </button>
            );
          })}
          <div style={{ height: 1, background: "var(--border)", margin: "4px 0" }} />
          {(() => {
            const on = labelFilter.includes("__unlabeled");
            return (
              <button
                type="button"
                onClick={() => toggle("__unlabeled")}
                style={{
                  display: "flex", alignItems: "center", gap: 10,
                  padding: "9px 10px",
                  background: on ? "var(--surface-sunken)" : "transparent",
                  border: 0, borderRadius: 8,
                  cursor: "pointer", textAlign: "left",
                  fontSize: 13.5,
                  color: "var(--text-muted)",
                }}
                onMouseEnter={(e) => { if (!on) e.currentTarget.style.background = "var(--surface-soft)"; }}
                onMouseLeave={(e) => { if (!on) e.currentTarget.style.background = "transparent"; }}
              >
                <span style={{
                  display: "inline-flex", alignItems: "center", justifyContent: "center",
                  width: 16, height: 16, borderRadius: 4,
                  border: `1.5px solid ${on ? "var(--accent, #c8553d)" : "var(--border-strong, #d8d6cf)"}`,
                  background: on ? "var(--accent, #c8553d)" : "transparent",
                  color: "white",
                  flexShrink: 0,
                }}>
                  {on && <IcCheck size={11} />}
                </span>
                <span style={{
                  display: "inline-block", width: 10, height: 10, borderRadius: 999,
                  border: "1.5px dashed var(--text-faint)", flexShrink: 0,
                }} />
                <span style={{ fontWeight: on ? 600 : 500, flex: 1 }}>Unlabeled</span>
              </button>
            );
          })()}
        </div>
      )}
    </div>
  );
}

function LibraryPage({ onOpenAsset, onNav, activeFolder, setActiveFolder, onPublish, onFindSimilar, similarSource, onClearSimilar, onPermissions, onShare, onQuickUpload }) {
  // Plan limits — hide Share/Publish buttons for free_beta users.
  const limits = (typeof window.useVandayLimits === "function")
    ? window.useVandayLimits()
    : { sharingEnabled: true, publishingEnabled: true };
  const features = (typeof window.useVandayFeatures === "function")
    ? window.useVandayFeatures()
    : { mode: "beta" };
  const showShare = limits.sharingEnabled !== false;
  const [view, setView] = React.useState("grid");
  const [showVariants, setShowVariants] = React.useState(false);
  const [query, setQuery] = React.useState(() => {
    // A clicked keyword in the asset preview stashes itself here, then closes
    // the preview (which remounts this library). Pick it up and search for it.
    const pending = window.__vandayPendingSearch;
    window.__vandayPendingSearch = null;
    return pending || "";
  });
  // Mobile search: the app shell (web/app.jsx) renders a search input in the
  // mobile topbar. When the user types there it dispatches a vanday-search
  // window event we mirror into this library's query state, so a single source
  // of truth handles both desktop search input and mobile overlay search.
  React.useEffect(() => {
    const handler = (e) => setQuery((e?.detail?.q) || "");
    window.addEventListener("vanday-search", handler);
    return () => window.removeEventListener("vanday-search", handler);
  }, []);
  const [orientation, setOrientation] = React.useState("all"); // RD-03
  const [typeFilter, setTypeFilter] = React.useState("all"); // all | images | videos | documents
  const [sourceFilter, setSourceFilter] = React.useState("all"); // all | shopify
  // Whether this workspace has the Shopify integration connected — gates the
  // "Shopify" filter chip so it only appears for accounts that set it up.
  const [shopifyConnected, setShopifyConnected] = React.useState(false);
  React.useEffect(() => {
    let cancelled = false;
    if (VandayAPI?.shopifyStatus) {
      VandayAPI.shopifyStatus()
        .then((s) => { if (!cancelled) setShopifyConnected(!!(s && s.connected)); })
        .catch(() => {});
    }
    return () => { cancelled = true; };
  }, []);
  const [selected, setSelected] = React.useState(new Set()); // GP-07
  const [lastSelectedId, setLastSelectedId] = React.useState(null);
  // Switching folders drops any batch selection (and hides the bulk toolbar) —
  // a selection made in one folder shouldn't carry into the next.
  React.useEffect(() => {
    setSelected(new Set());
    setLastSelectedId(null);
  }, [activeFolder]);
  const [popover, setPopover] = React.useState(null); // 'notifs' | 'help' | null
  // Modal toggles for the styled bulk-action flows.
  const [bulkTagOpen, setBulkTagOpen] = React.useState(false);
  const [bulkLabelOpen, setBulkLabelOpen] = React.useState(false);
  const [bulkMoveOpen, setBulkMoveOpen] = React.useState(false);
  // New-folder modal. null = closed; otherwise { parentId, parentName }.
  const [newFolderModal, setNewFolderModal] = React.useState(null);
  // Move-from-Inbox modal: holds the destination folder while open, else null.
  const [moveInboxModal, setMoveInboxModal] = React.useState(null);
  // Delete-folder confirmation modal: holds the folder pending deletion.
  const [deleteFolderTarget, setDeleteFolderTarget] = React.useState(null);
  // Bump to force a re-render after in-place asset/folder-count mutations.
  const [, forceTick] = React.useState(0);
  // Multi-select workflow-label filter. Array of label ids; the
  // sentinel "__unlabeled" matches assets that have no label. An empty
  // array means the filter is off. Predicate: an asset passes if its
  // labelId is in this array (or array contains "__unlabeled" and the
  // asset has no label).
  const [labelFilter, setLabelFilter] = React.useState([]);
  const [workflowLabelOpen, setWorkflowLabelOpen] = React.useState(false);
  const workflowLabelBtnRef = React.useRef(null);
  // Sort order for the result grid. "auto" = Relevance for semantic
  // queries (server score order is preserved), Newest everywhere else.
  // Explicit values override: newest / oldest / name-az / name-za /
  // largest / smallest.
  const [sortBy, setSortBy] = React.useState("auto");
  const [sortOpen, setSortOpen] = React.useState(false);
  const sortBtnRef = React.useRef(null);
  const searchInputRef = React.useRef(null);
  const isSimilarMode = !!similarSource;

  // Workflow labels (per-org, user-configured). Used by both the bulk
  // "Set label" modal and the inline label-filter row.
  const wfHook = (typeof window.VandayServer !== "undefined" && window.VandayServer.useWorkflowLabels)
    ? window.VandayServer.useWorkflowLabels()
    : { labels: [], byId: () => null };
  const workflowLabels = wfHook.labels || [];

  // Real semantic-search via /api/search. Debounced so a single keystroke
  // doesn't fire 8 requests. Empty → no server results (we fall back to the
  // existing client-side substring filter).
  const [serverResults, setServerResults] = React.useState(null);
  const [searchLoading, setSearchLoading] = React.useState(false);
  React.useEffect(() => {
    const q = query.trim();
    if (q.length < 3) { setServerResults(null); setSearchLoading(false); return; }
    setSearchLoading(true);
    const t = setTimeout(async () => {
      try {
        const r = await VandayAPI.search(q);
        const ids = new Set((r.results || []).map((x) => x.id));
        setServerResults({ ids, query: q });
      } catch {
        setServerResults(null);
      } finally {
        setSearchLoading(false);
      }
    }, 220);
    return () => clearTimeout(t);
  }, [query]);

  // Find similar — when the source is a real uploaded asset, ask the server
  // for cosine-ranked neighbours over the stored embeddings.
  const [similarIds, setSimilarIds] = React.useState(null);
  React.useEffect(() => {
    if (!similarSource || !similarSource.isServer) { setSimilarIds(null); return; }
    let stop = false;
    (async () => {
      try {
        const r = await VandayAPI.similar(similarSource.id);
        if (!stop) setSimilarIds(new Set((r.results || []).map((x) => x.id)));
      } catch {
        if (!stop) setSimilarIds(null);
      }
    })();
    return () => { stop = true; };
  }, [similarSource]);

  const focusSearch = () => {
    setPopover(null);
    setTimeout(() => {
      const el = searchInputRef.current;
      if (!el) return;
      el.focus();
      el.select && el.select();
      // Flash the search box so the user clearly sees the click did something.
      const wrap = el.closest(".search");
      if (wrap) {
        wrap.classList.remove("search-flash");
        // Force reflow so the animation restarts on repeated clicks.
        void wrap.offsetWidth;
        wrap.classList.add("search-flash");
      }
    }, 0);
  };
  const togglePopover = (id) => setPopover((p) => (p === id ? null : id));
  const newFolder = (parentId) => {
    // parentId may arrive as a click event when wired directly to onClick;
    // guard so only a real string id nests the new folder.
    const parent = typeof parentId === "string" ? parentId : null;
    const parentFolder = parent ? FOLDERS.find((f) => f.id === parent) : null;
    setNewFolderModal({ parentId: parent, parentName: parentFolder ? parentFolder.name : null });
  };
  const createFolderFromModal = async (name) => {
    const parent = newFolderModal ? newFolderModal.parentId : null;
    const f = await VandayAPI.createFolder(name, parent);
    // The polling hook will refresh FOLDERS within a couple seconds — open the
    // new folder now so the user sees something happen.
    setActiveFolder(f.id);
    setNewFolderModal(null);
  };

  // Close popovers on Escape
  React.useEffect(() => {
    if (!popover) return;
    const onKey = (e) => { if (e.key === "Escape") setPopover(null); };
    window.addEventListener("keydown", onKey);
    return () => window.removeEventListener("keydown", onKey);
  }, [popover]);

  const isPublished = activeFolder === "__published";
  const isRecent    = activeFolder === "__recent";
  const isShared    = activeFolder === "__shared";
  const isTrash     = activeFolder === "__trash";
  const isCollection = typeof activeFolder === "string" && activeFolder.startsWith("__col_");
  const collectionId = isCollection ? activeFolder.slice(6) : null;
  const collection = isCollection ? COLLECTIONS.find((c) => c.id === collectionId) : null;
  const isVirtual = isPublished || isRecent || isShared || isTrash || isCollection;

  // Filter out assets from folders the active user can't see. This is what
  // makes the "View as" permission test actually work — switching to a user
  // who isn't a member of `products` or `team` makes those folders' assets
  // disappear from "All assets" and search too.
  const _baseList0 = showVariants ? ALL_ASSETS : ASSETS;
  const baseList = _baseList0.filter((a) => window.VandaySession.canSeeFolder(a.folder));
  let visibleAssets;
  if (activeFolder === "all" || isPublished) {
    visibleAssets = baseList;
  } else if (isRecent) {
    // Recently uploaded — newest real uploads first (server assets carry a
    // numeric createdAtMs; mocks fall back to 0 and trail behind). Show the
    // most recent slice rather than the whole library.
    visibleAssets = baseList
      .slice()
      .sort((a, b) => (b.createdAtMs || 0) - (a.createdAtMs || 0))
      .slice(0, 60);
  } else if (isCollection) {
    // Fake: collection = first N assets by id for the prototype.
    visibleAssets = baseList.slice(0, collection ? collection.count : 0);
  } else if (isShared) {
    // Fake: a couple of assets from partners.
    visibleAssets = baseList.filter((a) => ["a04", "a09", "a17", "a24"].includes(a.id));
  } else if (isTrash) {
    visibleAssets = [];
  } else {
    visibleAssets = baseList.filter((a) => a.folder === activeFolder);
  }

  if (query) {
    const q = query.toLowerCase();
    const localMatches = ALL_ASSETS.filter((a) =>
      a.name.toLowerCase().includes(q) ||
      (a.tags || []).some((t) => t.toLowerCase().includes(q)) ||
      (a.uploader || "").toLowerCase().includes(q) ||
      (a.ratio || "").toLowerCase().includes(q) ||
      (a.caption || "").toLowerCase().includes(q)
    );
    if (serverResults && serverResults.query === query.trim()) {
      // Semantic hits from the server come first, then local fuzzy matches
      // (de-duped) so the user sees AI-ranked results without losing the
      // simple substring matches the prototype already supported.
      const seen = new Set();
      const ordered = [];
      for (const id of serverResults.ids) {
        const a = ALL_ASSETS.find((x) => x.id === id);
        if (a && !seen.has(a.id)) { seen.add(a.id); ordered.push(a); }
      }
      for (const a of localMatches) {
        if (!seen.has(a.id)) { seen.add(a.id); ordered.push(a); }
      }
      visibleAssets = ordered;
    } else {
      visibleAssets = localMatches;
    }
  }

  // Similar mode — for real uploads we use the server's cosine similarity
  // over stored embeddings. For mocked assets (or while the server response
  // is still pending) we fall back to the tag-overlap heuristic.
  if (isSimilarMode) {
    const src = similarSource;
    if (similarIds && src.isServer) {
      const order = Array.from(similarIds);
      visibleAssets = order
        .map((id) => ALL_ASSETS.find((a) => a.id === id))
        .filter(Boolean);
    } else {
      const srcTags = new Set(src.tags || []);
      visibleAssets = ASSETS
        .filter((a) => a.id !== src.id)
        .map((a) => {
          const overlap = (a.tags || []).filter((t) => srcTags.has(t)).length;
          const folderBonus = a.folder === src.folder ? 1 : 0;
          return { a, score: overlap * 2 + folderBonus };
        })
        .sort((x, y) => y.score - x.score)
        .slice(0, 18)
        .map((x) => x.a);
    }
  }

  // Orientation filter — RD-03
  if (orientation !== "all") {
    visibleAssets = visibleAssets.filter((a) => orientationOf(a) === orientation);
  }

  // Media-type filter. Best-effort on the prototype mock data: we sniff
  // the extension and the mime field (server assets have it; mocks usually
  // don't). Documents = anything that isn't clearly image/video.
  if (typeFilter !== "all") {
    visibleAssets = visibleAssets.filter((a) => {
      const name = (a.name || "").toLowerCase();
      const mime = (a.mime || "").toLowerCase();
      const isVid = mime.startsWith("video/") || /\.(mp4|mov|webm|m4v|avi)$/i.test(name);
      // Vector / print files (SVG, PDF, EPS, AI) are visual assets — they show
      // a rendered preview and are AI-tagged, so they belong under "Images".
      const isImg = mime.startsWith("image/")
        || /\.(jpe?g|png|gif|webp|heic|avif|svg|tiff?|pdf|eps|ai)$/i.test(name)
        || mime === "application/pdf" || mime === "application/postscript" || mime === "application/illustrator";
      if (typeFilter === "videos") return isVid;
      if (typeFilter === "images") return isImg && !isVid;
      if (typeFilter === "documents") return !isImg && !isVid;
      return true;
    });
  }

  // Shopify filter — anything connected to Shopify: imported FROM Shopify
  // (source) OR sent TO a Shopify product (a shopify publication record).
  if (sourceFilter === "shopify") {
    visibleAssets = visibleAssets.filter((a) =>
      a.source === "shopify" ||
      (a.publications || []).some((p) => p.site === "shopify"),
    );
  }

  // Workflow-label filter. Single-select: keep only assets whose label
  // matches the chosen one. "__unlabeled" matches assets with no label.
  if (labelFilter.length > 0) {
    const wantsUnlabeled = labelFilter.includes("__unlabeled");
    const wantedIds = new Set(labelFilter.filter((id) => id !== "__unlabeled"));
    visibleAssets = visibleAssets.filter((a) => {
      if (!a.workflowLabelId) return wantsUnlabeled;
      return wantedIds.has(a.workflowLabelId);
    });
  }

  // ----- Sort -----
  // "auto" keeps whatever order the upstream pipeline produced
  // (relevance for semantic queries; recent-uploaded slice for Recents;
  // similarity for the Similar mode; otherwise the underlying ASSETS
  // order which is "newest first" in the demo data). Every other value
  // re-sorts the list in place.
  {
    const sizeOf = (a) =>
      typeof a.bytes === "number" ? a.bytes :
      typeof a.size === "number"  ? a.size  :
      0;
    const nameOf = (a) => (a.name || "").toLowerCase();
    const tOf    = (a) => a.createdAtMs || 0;
    const apply = (cmp) => { visibleAssets = visibleAssets.slice().sort(cmp); };
    switch (sortBy) {
      case "newest":   apply((a, b) => tOf(b) - tOf(a)); break;
      case "oldest":   apply((a, b) => tOf(a) - tOf(b)); break;
      case "name-az":  apply((a, b) => nameOf(a).localeCompare(nameOf(b))); break;
      case "name-za":  apply((a, b) => nameOf(b).localeCompare(nameOf(a))); break;
      case "largest":  apply((a, b) => sizeOf(b) - sizeOf(a)); break;
      case "smallest": apply((a, b) => sizeOf(a) - sizeOf(b)); break;
      // "auto" / "relevance" — no-op.
      default: break;
    }
  }

  const currentFolder = FOLDERS.find((f) => f.id === activeFolder) || (
    isCollection ? { id: activeFolder, name: collection?.name || "Collection" } :
    isShared     ? { id: "__shared",  name: "Shared with me" } :
    isTrash      ? { id: "__trash",   name: "Recently deleted" } :
    isPublished  ? { id: "__published", name: "Published" } :
    isRecent     ? { id: "__recent",  name: "Recently uploaded" } : null
  );
  const cropsCount = visibleAssets.filter((a) => a.kind === "crop").length;
  const origsCount = visibleAssets.length - cropsCount;

  // GP-07 — selection helpers.
  const toggleSelect = (id, withShift) => {
    setSelected((s) => {
      const next = new Set(s);
      if (withShift && lastSelectedId) {
        const ids = visibleAssets.map((a) => a.id);
        const i = ids.indexOf(lastSelectedId);
        const j = ids.indexOf(id);
        if (i >= 0 && j >= 0) {
          const [from, to] = [Math.min(i, j), Math.max(i, j)];
          for (let k = from; k <= to; k++) next.add(ids[k]);
        } else next.add(id);
      } else {
        if (next.has(id)) next.delete(id);
        else next.add(id);
      }
      return next;
    });
    setLastSelectedId(id);
  };
  const selectAll = () => setSelected(new Set(visibleAssets.map((a) => a.id)));
  const clearSelected = () => setSelected(new Set());

  // Mirror selection on window so the asset-card drag handler can read it
  // without prop-drilling. AssetCard is rendered far below.
  React.useEffect(() => {
    window.__currentSelection__ = selected;
    return () => { if (window.__currentSelection__ === selected) window.__currentSelection__ = null; };
  }, [selected]);

  // Drop handler used by sidebar folder buttons. Moves dragged asset ids
  // (one or many) into the target folder, then clears selection.
  const onFolderDrop = React.useCallback(async (folderId, e) => {
    e.preventDefault();
    let ids = [];
    try { ids = JSON.parse(e.dataTransfer.getData("application/x-vanday-ids") || "[]"); } catch {}
    if (!Array.isArray(ids) || ids.length === 0) return;
    for (const id of ids) {
      try { await VandayAPI.patchAsset(id, { folder: folderId }); } catch {}
    }
    clearSelected();
  }, []);

  // FT-08 — treat multi-word queries as semantic.
  const isSemanticQuery = query && query.trim().split(/\s+/).length >= 3;

  return (
    <div className="app">
      <Rail
        active="library"
        onNav={onNav}
        onLogout={() => (window.__vandayLogout ? window.__vandayLogout() : onNav("login"))}
        onSearch={focusSearch}
        onNotifs={() => togglePopover("notifs")}
        onHelp={() => togglePopover("help")}
        onQuickUpload={onQuickUpload}
      />
      <Sidebar
        activeFolder={activeFolder}
        setActiveFolder={(id) => {
          // Clicking a folder = fresh context. Wipe any active
          // search/filter/similar state so the user lands on the new
          // folder's full contents instead of an empty grid filtered
          // by predicates inherited from where they came from.
          setQuery("");
          setTypeFilter("all");
          setOrientation("all");
          setLabelFilter([]);
          setSourceFilter("all");
          if (isSimilarMode) onClearSimilar && onClearSimilar();
          setActiveFolder(id);
        }}
        onNav={onNav}
        onNewFolder={newFolder}
        onFolderDrop={onFolderDrop}
        onFolderRename={async (f, name) => {
          // Optimistic: mutate the in-memory folder, bump the server-rev so
          // every useServerRev() subscriber (incl. app.jsx → Library →
          // breadcrumb) re-renders with the new name immediately. On PATCH
          // failure we revert and bump again so the UI doesn't lie.
          const prev = f.name;
          f.name = name;
          try { window.VandayServer?.bumpRev?.(); } catch {}
          try {
            await VandayAPI.renameFolder(f.id, name);
          } catch {
            f.name = prev;
            try { window.VandayServer?.bumpRev?.(); } catch {}
          }
        }}
        onFolderMove={async (f, parentId) => {
          // Optimistic: re-parent the in-memory folder and bump the rev so the
          // sidebar tree re-nests immediately. Revert on PATCH failure (e.g.
          // the server rejected a cycle) so the UI can't drift from the server.
          const prev = f.parent || null;
          if (prev === (parentId || null)) return;
          f.parent = parentId || null;
          try { window.VandayServer?.bumpRev?.(); } catch {}
          try {
            await VandayAPI.moveFolder(f.id, parentId || null);
          } catch {
            f.parent = prev;
            try { window.VandayServer?.bumpRev?.(); } catch {}
          }
        }}
        onFolderDelete={(f) => setDeleteFolderTarget(f)}
      />

      {isTrash ? (
        <TrashView onBack={() => setActiveFolder("all")} onChanged={() => { try { window.VandayServer?.bumpRev?.(); } catch {} forceTick((t) => t + 1); }} />
      ) : isPublished ? (
        <PublishedView onOpenAsset={onOpenAsset} onNav={onNav} onBack={() => setActiveFolder("all")} />
      ) : (
      <main className="main">
        <header className="topbar">
          <div className="crumbs">
            <span>{window.WorkspaceName ? <WorkspaceName /> : "Library"}</span>
            <span className="sep">/</span>
            <span>Library</span>
            <span className="sep">/</span>
            <span className="here">{currentFolder?.name}</span>
            <IcChevD size={14} style={{ color: "var(--text-faint)" }} />
          </div>
          <div className="topbar-right">
            <div className="search">
              <IcSearch size={14} />
              <input
                ref={searchInputRef}
                placeholder="Search assets, tags, crops…"
                value={query}
                onChange={(e) => setQuery(e.target.value)}
              />
              {/* Option B "search-clear" affordance: while there's any
                  text in the box the ⌘K hint swaps for a circular ×
                  that clears the keyword and refocuses the input. The
                  keyword's "remove on the control" lives here, not in
                  the page title. */}
              {query ? (
                <button
                  className="search-clear"
                  title="Clear search"
                  onClick={() => {
                    setQuery("");
                    if (searchInputRef.current) searchInputRef.current.focus();
                  }}
                  aria-label="Clear search"
                >
                  <IcClose size={12} />
                </button>
              ) : (
                <kbd>⌘K</kbd>
              )}
            </div>
            <span className="trial-pill" title={features.mode === "dev" ? "Dev build" : ({ free_beta: "Free Beta", starter: "Starter", growth: "Growth", pro: "Pro" }[limits?.planName] || "Free Beta") + " plan"}>
              <span className="dot" />
              {features.mode === "dev" ? "Dev" : ({ free_beta: "Free Beta", starter: "Starter", growth: "Growth", pro: "Pro" }[limits?.planName] || "Free Beta")}
            </span>
            {showShare && features.canWrite && (
              <button className="btn" onClick={() => onShare && onShare(currentFolder || { id: activeFolder, name: "Library" })}>
                <IcLink size={14} /> Share
              </button>
            )}
            {features.canWrite && (
              <button className="btn accent" onClick={onQuickUpload || (() => onNav("upload"))}>
                <IcUpload size={14} /> Upload
              </button>
            )}
          </div>
        </header>

        <section className="lib-header">
          {isSimilarMode ? (
            <div style={{ display: "flex", alignItems: "center", gap: 14 }}>
              <div
                style={{
                  width: 64, height: 64, borderRadius: 8,
                  backgroundImage: `url(${similarSource.url})`,
                  backgroundSize: "cover", backgroundPosition: "center",
                  flexShrink: 0,
                  boxShadow: "var(--shadow-sm)",
                }}
              />
              <div style={{ minWidth: 0 }}>
                <div style={{ display: "inline-flex", alignItems: "center", gap: 6, fontSize: 11, textTransform: "uppercase", letterSpacing: "0.06em", color: "var(--text-faint)", fontWeight: 500 }}>
                  <IcSparkles size={12} /> Similar to
                </div>
                {/* Exit-similar moved out of the title; it now lives
                    in the toolbar as a removable "Similar to: NAME ×"
                    chip alongside the rest of the search filters. */}
                <h1 className="lib-title" style={{ fontFamily: "var(--font-mono)", fontSize: 18, margin: 0 }}>{similarSource.name}</h1>
                <div className="lib-sub">
                  <span>{visibleAssets.length} matches</span>
                  <span className="dot" />
                  <span>Ranked by visual embedding</span>
                </div>
              </div>
            </div>
          ) : (
          <div>
            <h1 className="lib-title" style={{ display: "flex", alignItems: "center", gap: 10, flexWrap: "wrap" }}>
              {query ? (
                <>
                  {isSemanticQuery && <span className="semantic-pill"><IcSparkles size={12} /> Semantic</span>}
                  Results: <span style={{ fontWeight: 400, color: "var(--text-muted)" }}>“{query}”</span>
                </>
              ) : currentFolder?.name}
            </h1>
            <div className="lib-sub">
              <span>
                {origsCount} {origsCount === 1 ? "asset" : "assets"}
                {showVariants && cropsCount > 0 && (
                  <span style={{ color: "var(--text-faint)" }}>
                    {" "}+ {cropsCount} crops
                  </span>
                )}
              </span>
              <span className="dot" />
              <span>
                {visibleAssets
                  // Real (server) assets store size as bytes in size_bytes;
                  // sample assets carry it as the string "1.2 MB" in .size.
                  // Coerce defensively so a missing or non-numeric value
                  // doesn't poison the sum into NaN.
                  .reduce((s, a) => {
                    const mb = (typeof a.size_bytes === "number")
                      ? a.size_bytes / (1024 * 1024)
                      : parseFloat(a.size);
                    return s + (Number.isFinite(mb) ? mb : 0);
                  }, 0)
                  .toFixed(1)}{" "}
                MB
              </span>
              {isSemanticQuery && (
                <>
                  <span className="dot" />
                  <span style={{ color: "var(--accent)" }}>Ranked by relevance</span>
                </>
              )}
            </div>
          </div>
          )}
          {features.canWrite && (() => {
            // Surface folder-filling actions in the header when the folder
            // already has assets. When it's empty the centered empty-state
            // already shows these, so we hide the header copies to avoid
            // duplicating them top-and-center. Only in a real, user-created
            // folder with something in the Inbox to move.
            const isRealFolder = activeFolder && activeFolder !== "all" && activeFolder !== "raw" && !String(activeFolder).startsWith("__") && !isSimilarMode && !query;
            const folderEmpty = visibleAssets.length === 0;
            const showFolderActions = isRealFolder && !folderEmpty;
            const inboxCount = (window.ASSETS || []).filter(
              (a) => a.isServer && a.kind !== "crop" && (a.folder === "raw" || a.folder == null)
            ).length;
            return (
              <div style={{ display: "flex", gap: 8 }}>
                {showFolderActions && (
                  <button className="btn accent" onClick={onQuickUpload || (() => onNav("upload"))}>
                    <IcUpload size={14} /> Upload to {currentFolder?.name || "this folder"}
                  </button>
                )}
                {showFolderActions && inboxCount > 0 && (
                  <button className="btn" onClick={() => setMoveInboxModal({ id: activeFolder, name: currentFolder?.name })}>
                    <IcInbox size={15} /> Move from Inbox ({inboxCount})
                  </button>
                )}
                <button className="btn" onClick={() => newFolder()}><IcPlus size={14} /> New folder</button>
                {isRealFolder && currentFolder && (
                  <button
                    className="btn"
                    onClick={() => setDeleteFolderTarget(currentFolder)}
                    title="Delete this folder"
                    style={{ color: "var(--accent, #c8553d)" }}
                  >
                    <IcTrash size={14} /> Delete folder
                  </button>
                )}
              </div>
            );
          })()}
        </section>

        {/* Folder strip removed — the sidebar already exposes the
            same folders, and the strip added clutter without value.
            Left here as a comment so future-Claude doesn't reintroduce
            it by mistake. */}

        <section className="lib-tools">
          {/* "Similar to: NAME" chip — only in similar mode. Treated
              like every other active filter (Option-B removable chip):
              click × to exit similar mode. The filename is truncated
              with an ellipsis (max ~160px of text) so a long Shutter-
              stock-style filename doesn't blow up the chip width and
              force the rest of the toolbar to wrap. Hover title shows
              the full name. */}
          {isSimilarMode && (
            <span
              className="chip active removable"
              title={`Showing assets visually similar to ${similarSource.name}`}
              style={{ maxWidth: 260 }}
            >
              <IcSparkles size={12} />
              <span style={{
                display: "inline-block",
                maxWidth: 160,
                overflow: "hidden",
                textOverflow: "ellipsis",
                whiteSpace: "nowrap",
                verticalAlign: "bottom",
              }}>
                Similar: {similarSource.name}
              </span>
              <button
                className="cx"
                title="Exit similar"
                onClick={(e) => { e.stopPropagation(); onClearSimilar && onClearSimilar(); }}
                aria-label="Exit similar"
              >
                <IcClose size={11} />
              </button>
            </span>
          )}
          {(
            <>
              {/* Primary inline filter: media type. Short labels, four
                  choices — fits naturally without crowding the toolbar.
                  Active non-default types render with an inline × button
                  (Option B "remove on the control" pattern) so the user
                  can drop the filter without re-clicking "All types". */}
              {[
                { id: "all",        label: "All types"  },
                { id: "images",     label: "Images"     },
                { id: "videos",     label: "Videos"     },
                // "Documents" intentionally omitted — Vanday doesn't
                // support document assets yet. The filter logic in the
                // type predicate above still handles "documents" so
                // any pre-existing URLs / state survive gracefully,
                // but we don't surface the chip in the toolbar.
              ].map((t) => {
                const on = typeFilter === t.id;
                if (on && t.id !== "all") {
                  return (
                    <span key={t.id} className="chip active removable">
                      {t.label}
                      <button
                        className="cx"
                        title={`Remove ${t.label}`}
                        onClick={(e) => { e.stopPropagation(); setTypeFilter("all"); }}
                        aria-label={`Remove ${t.label} filter`}
                      >
                        <IcClose size={11} />
                      </button>
                    </span>
                  );
                }
                return (
                  <span
                    key={t.id}
                    className={`chip ${on ? "active" : ""}`}
                    style={{ cursor: "pointer" }}
                    onClick={() => setTypeFilter(t.id)}
                  >
                    {t.label}
                  </span>
                );
              })}

              {/* Published — search/filter scope. Removable when active,
                  same as the type chips above. */}
              {isPublished ? (
                <span className="chip active removable">
                  <IcSend size={12} /> Published
                  <button
                    className="cx"
                    title="Remove Published filter"
                    onClick={(e) => { e.stopPropagation(); setActiveFolder("all"); }}
                    aria-label="Remove Published filter"
                  >
                    <IcClose size={11} />
                  </button>
                </span>
              ) : (
                <span
                  className="chip"
                  style={{ cursor: "pointer", display: "inline-flex", alignItems: "center", gap: 5 }}
                  onClick={() => setActiveFolder("__published")}
                  title="Show assets you've published"
                >
                  <IcSend size={12} /> Published
                </span>
              )}

              {/* Shopify — a destination filter alongside Published, but only for
                  workspaces that have connected the Shopify integration. Filters
                  to assets imported from Shopify. */}
              {shopifyConnected && (
                sourceFilter === "shopify" ? (
                  <span className="chip active removable">
                    Shopify
                    <button
                      className="cx"
                      title="Remove Shopify filter"
                      onClick={(e) => { e.stopPropagation(); setSourceFilter("all"); }}
                      aria-label="Remove Shopify filter"
                    >
                      <IcClose size={11} />
                    </button>
                  </span>
                ) : (
                  <span
                    className="chip"
                    style={{ cursor: "pointer" }}
                    onClick={() => setSourceFilter("shopify")}
                    title="Show assets imported from or sent to Shopify"
                  >
                    Shopify
                  </span>
                )
              )}

              <span style={{ width: 1, height: 18, background: "var(--border)", margin: "0 4px" }} />

              {/* Orientation: icon group with tooltips on hover. Each icon
                  is a little rectangle in the shape it represents. When a
                  non-"any" orientation is active, an × badge appears on
                  its top-right corner — click resets to "any". */}
              <div className="orient-group">
                {[
                  { id: "all",       svg: <span style={{ display: "inline-block", width: 12, height: 12, border: "1.5px solid currentColor", borderRadius: 2 }} />, label: "Any orientation" },
                  { id: "landscape", svg: <span style={{ display: "inline-block", width: 14, height: 10, border: "1.5px solid currentColor", borderRadius: 2 }} />, label: "Landscape — wider than tall" },
                  { id: "portrait",  svg: <span style={{ display: "inline-block", width: 10, height: 14, border: "1.5px solid currentColor", borderRadius: 2 }} />, label: "Portrait — taller than wide" },
                  { id: "square",    svg: <span style={{ display: "inline-block", width: 12, height: 12, border: "1.5px solid currentColor", borderRadius: 2 }} />, label: "Square — within ±5%" },
                ].map((o) => {
                  const on = orientation === o.id;
                  const showRemove = on && o.id !== "all";
                  return (
                    <button
                      key={o.id}
                      className={`orient-btn ${on ? "is-on" : ""}`}
                      onClick={() => setOrientation(o.id)}
                      title={o.label}
                      aria-label={o.label}
                    >
                      {o.svg}
                      <span className="orient-tip">{o.label}</span>
                      {showRemove && (
                        <span
                          className="obx"
                          role="button"
                          tabIndex={0}
                          title="Clear orientation"
                          onClick={(e) => { e.stopPropagation(); setOrientation("all"); }}
                          onKeyDown={(e) => {
                            if (e.key === "Enter" || e.key === " ") {
                              e.preventDefault();
                              e.stopPropagation();
                              setOrientation("all");
                            }
                          }}
                          aria-label="Clear orientation filter"
                        >
                          <IcClose size={8} />
                        </span>
                      )}
                    </button>
                  );
                })}
              </div>

              {/* Workflow-label chips intentionally NOT rendered here.
                  The "Workflow label" toolbar button already shows a
                  count badge of how many labels are active, and the
                  dropdown is where the user removes individual labels
                  (or hits Clear all to wipe everything). Rendering a
                  chip per label was redundant with the badge and blew
                  out the toolbar when several were selected. */}

              {/* Master "Clear all · N" pill lives at the end of the
                  toolbar row — rendered below, after the view toggle —
                  so it always reads as "wipe everything I just set up,"
                  not as another filter chip. */}
            </>
          )}
          {/* Workflow label — surfaced directly in the toolbar
              (previously buried inside a "Filter" popover). Multi-
              select: opening the dropdown shows every label as a
              checkbox row; toggling each updates labelFilter
              immediately. The count badge shows how many labels are
              currently selected. */}
          <button
            ref={workflowLabelBtnRef}
            className={`btn sm lib-filter-btn ${workflowLabelOpen ? "active" : ""}`}
            onClick={() => setWorkflowLabelOpen((v) => !v)}
            style={{ position: "relative" }}
            title="Filter by workflow label"
          >
            <IcCheck size={13} /> Workflow label
            {labelFilter.length > 0 && (
              <span style={{
                marginLeft: 4,
                background: "var(--accent, #c8553d)",
                color: "white",
                fontSize: 10, fontWeight: 700,
                borderRadius: 999, padding: "1px 6px",
                minWidth: 16, textAlign: "center",
                fontFamily: "var(--font-mono)",
              }}>
                {labelFilter.length}
              </span>
            )}
          </button>
          {/* "Clear all · N" pill — last item in the search/filter
              group on the left, just before the right-aligned Sort +
              view-toggle block. Only renders when at least one
              constraint is active. */}
          {(() => {
            const activeCount =
              (query.trim() ? 1 : 0) +
              (typeFilter !== "all" ? 1 : 0) +
              (orientation !== "all" ? 1 : 0) +
              labelFilter.length +
              (isPublished ? 1 : 0) +
              (isSimilarMode ? 1 : 0);
            if (activeCount === 0) return null;
            const clearAll = () => {
              setQuery("");
              setTypeFilter("all");
              setOrientation("all");
              setLabelFilter([]);
              if (isPublished) setActiveFolder("all");
              if (isSimilarMode) onClearSimilar && onClearSimilar();
            };
            return (
              <button
                className="clear-pill"
                onClick={clearAll}
                title="Clear search and all filters"
              >
                <IcClose size={12} /> Clear all
                <span className="cp-n">{activeCount}</span>
              </button>
            );
          })()}
          <div style={{ marginLeft: "auto", display: "inline-flex", alignItems: "center", gap: 8 }}>
            {(() => {
              // Build the visible label from sortBy. "auto" defers to
              // whatever the pipeline produced (Relevance for semantic
              // queries, Newest otherwise) so the button label matches
              // what the user is actually seeing.
              const LABELS = {
                "auto":     isSemanticQuery ? "Relevance" : "Newest",
                "newest":   "Newest",
                "oldest":   "Oldest",
                "name-az":  "Name A→Z",
                "name-za":  "Name Z→A",
                "largest":  "Largest",
                "smallest": "Smallest",
              };
              return (
                <button
                  ref={sortBtnRef}
                  className={`btn sm ${sortOpen ? "active" : ""}`}
                  onClick={() => setSortOpen((v) => !v)}
                  title="Change sort order"
                >
                  <IcSort size={13} /> Sort: {LABELS[sortBy] || "Newest"}
                </button>
              );
            })()}
            <div className="view-toggle">
              <button className={view === "grid" ? "active" : ""} onClick={() => setView("grid")} title="Grid">
                <IcGrid size={14} />
              </button>
              <button className={view === "list" ? "active" : ""} onClick={() => setView("list")} title="List">
                <IcList size={14} />
              </button>
            </div>
          </div>
        </section>

        <section className={view === "list" ? "lib-list" : "grid"}>
          {view === "list" && visibleAssets.length > 0 && (
            <div className="lib-list-head">
              <div style={{ width: 24 }} />
              <div style={{ width: 44 }} />
              <div style={{ flex: 2, minWidth: 0 }}>Name</div>
              <div style={{ flex: 2, minWidth: 0 }}>Tags</div>
              <div style={{ width: 110 }}>Status</div>
              <div style={{ width: 80, textAlign: "right" }}>Size</div>
              <div style={{ width: 28 }} />
            </div>
          )}
          {visibleAssets.map((a) => (
            view === "list" ? (
              <AssetRow
                key={a.id}
                asset={a}
                onOpen={onOpenAsset}
                selected={selected.has(a.id)}
                onToggleSelect={toggleSelect}
                selectionMode={selected.size > 0}
              />
            ) : (
              <AssetCard
                key={a.id}
                asset={a}
                onOpen={onOpenAsset}
                onOpenParent={onOpenAsset}
                onFindSimilar={onFindSimilar}
                selected={selected.has(a.id)}
                onToggleSelect={toggleSelect}
                selectionMode={selected.size > 0}
                matchReason={!isSimilarMode && query ? matchReasonFor(a, query) : null}
              />
            )
          ))}
          {visibleAssets.length === 0 && (() => {
            // Three flavors of "no results":
            //   1. Trash / Shared have their own dedicated copy.
            //   2. Search active → say so, with a tip.
            //   3. Filter (label, orientation) active → "no matches, clear filter."
            //   4. Genuinely empty folder → upload CTA.
            // Without case 3, an active label filter that excludes everything
            // wrongly looks like "this folder is empty, upload more!"
            const hasActiveSearch = !!query && query.trim().length > 0;
            const hasActiveFilter = labelFilter.length > 0 || orientation !== "all" || typeFilter !== "all" || showVariants;
            const activeFolderName = (() => {
              if (!FOLDERS) return null;
              const f = FOLDERS.find((x) => x.id === activeFolder);
              return f ? f.name : null;
            })();
            return (
              <div style={{
                gridColumn: "1 / -1", textAlign: "center", padding: "96px 24px",
                color: "var(--text-faint)", fontSize: 13,
              }}>
                {isTrash ? (
                  <>
                    <div style={{ marginBottom: 10 }}>
                      <IcTrash size={28} style={{ color: "var(--text-faint)" }} />
                    </div>
                    <strong style={{ color: "var(--text)", display: "block", fontSize: 14, marginBottom: 4 }}>
                      Trash is empty
                    </strong>
                    Deleted assets stay here for 60 days on the Team plan.
                  </>
                ) : isShared ? (
                  <>
                    <div style={{ marginBottom: 10 }}>
                      <IcShare size={28} style={{ color: "var(--text-faint)" }} />
                    </div>
                    <strong style={{ color: "var(--text)", display: "block", fontSize: 14, marginBottom: 4 }}>
                      Nothing shared with you yet
                    </strong>
                    When a teammate shares a folder, it'll appear here.
                  </>
                ) : searchLoading ? (
                  <>
                    <div style={{ marginBottom: 10 }}>
                      <span className="search-spinner" aria-hidden="true" />
                    </div>
                    <strong style={{ color: "var(--text)", display: "block", fontSize: 14, marginBottom: 4 }}>
                      Searching…
                    </strong>
                    Scanning your library for "{query.trim()}".
                  </>
                ) : hasActiveSearch ? (
                  <>
                    <div style={{ marginBottom: 10 }}>
                      <IcSearch size={26} style={{ color: "var(--text-faint)" }} />
                    </div>
                    <strong style={{ color: "var(--text)", display: "block", fontSize: 14, marginBottom: 4 }}>
                      No assets match "{query.trim()}"
                    </strong>
                    Try different keywords, or clear the search to see everything.
                  </>
                ) : hasActiveFilter ? (
                  <div style={{ display: "flex", flexDirection: "column", alignItems: "center", gap: 12, maxWidth: 360, margin: "0 auto" }}>
                    <div style={{ marginBottom: 4 }}>
                      <IcFilter size={26} style={{ color: "var(--text-faint)" }} />
                    </div>
                    <div>
                      <strong style={{ color: "var(--text)", display: "block", fontSize: 14, marginBottom: 4 }}>
                        No assets match the current filter
                      </strong>
                      <div style={{ color: "var(--text-muted)", fontSize: 13, lineHeight: 1.5 }}>
                        Nothing in {activeFolderName || "this view"} matches the workflow label or orientation you picked.
                      </div>
                    </div>
                    <button
                      className="btn"
                      onClick={() => {
                        setLabelFilter([]); // must stay an array — null crashes labelFilter.length
                        setOrientation("all");
                        setTypeFilter("all");
                        setShowVariants(false);
                      }}
                    >
                      <IcClose size={12} /> Clear filters
                    </button>
                  </div>
                ) : (
                  // Real empty state — no search, just no files yet. Centered
                  // upload CTA so the next step is unambiguous.
                  <div style={{ display: "flex", flexDirection: "column", alignItems: "center", gap: 14, maxWidth: 360, margin: "0 auto" }}>
                    <div style={{
                      width: 56, height: 56, borderRadius: 14,
                      background: "var(--accent-soft, oklch(0.94 0.04 38))",
                      color: "var(--accent, #c8553d)",
                      display: "grid", placeItems: "center",
                    }}>
                      <IcUpload size={24} />
                    </div>
                    <div>
                      <strong style={{ color: "var(--text)", display: "block", fontSize: 15, marginBottom: 4 }}>
                        {activeFolderName ? `${activeFolderName} is empty` : "Nothing here yet"}
                      </strong>
                      <div style={{ color: "var(--text-muted)", fontSize: 13, lineHeight: 1.5 }}>
                        Upload photos or video to start building your library. Vanday auto-tags, captions, and crops on import.
                      </div>
                    </div>
                    {(() => {
                      // Offer "Move from Inbox" only when we're in a real,
                      // user-created folder (not all / Inbox / virtual views)
                      // AND there's actually something sitting in the Inbox to
                      // move. Otherwise the button would dead-end.
                      const isRealFolder = activeFolder && activeFolder !== "all" && activeFolder !== "raw" && !String(activeFolder).startsWith("__");
                      const inboxCount = (window.ASSETS || []).filter(
                        (a) => a.isServer && a.kind !== "crop" && (a.folder === "raw" || a.folder == null)
                      ).length;
                      return (
                        <div style={{ display: "flex", gap: 8, marginTop: 4, flexWrap: "wrap", justifyContent: "center" }}>
                          {features.canWrite && (
                            <button
                              className="btn primary"
                              onClick={() => { (onQuickUpload || (() => onNav("upload")))(); }}
                            >
                              <IcUpload size={14} /> Upload to {activeFolderName || "this folder"}
                            </button>
                          )}
                          {features.canWrite && isRealFolder && inboxCount > 0 && (
                            <button
                              className="btn"
                              onClick={() => setMoveInboxModal({ id: activeFolder, name: activeFolderName })}
                            >
                              <IcInbox size={15} /> Move from Inbox ({inboxCount})
                            </button>
                          )}
                        </div>
                      );
                    })()}
                  </div>
                )}
              </div>
            );
          })()}
        </section>

        {/* GP-07 — Bulk action bar */}
        {selected.size > 0 && (
          <div className="bulk-bar">
            <button className="bulk-clear" onClick={clearSelected} title="Clear selection">
              <IcClose size={14} />
            </button>
            <span className="bulk-count">
              <strong>{selected.size}</strong> selected
            </span>
            <button className="bulk-link" data-bulk="select-all" onClick={selectAll}>Select all ({visibleAssets.length})</button>
            {features.canWrite && <span className="bulk-sep" data-bulk="sep" />}
            {features.canWrite && (
              <button className="bulk-act" data-bulk="tag" onClick={() => {
                // Quick pre-flight: if nothing in the selection is a real
                // uploaded asset, there's nothing the API can tag — surface
                // a friendly message instead of opening an empty modal.
                const picked = visibleAssets.filter((a) => selected.has(a.id) && a.isServer && !a.isServerVariant);
                if (picked.length === 0) {
                  window.alert("Pick at least one uploaded asset — sample images can't be retagged.");
                  return;
                }
                setBulkTagOpen(true);
              }}><IcTag size={13} /> Tag</button>
            )}
            {features.canWrite && (
              <button className="bulk-act" data-bulk="label" onClick={() => {
                const picked = visibleAssets.filter((a) => selected.has(a.id) && a.isServer && !a.isServerVariant);
                if (picked.length === 0) {
                  window.alert("Pick at least one uploaded asset — sample images can't have a workflow label set.");
                  return;
                }
                setBulkLabelOpen(true);
              }}><IcCheck size={13} /> Workflow label</button>
            )}
            {features.canWrite && (
              <button className="bulk-act" data-bulk="move" onClick={() => {
                const picked = visibleAssets.filter((a) => selected.has(a.id) && a.isServer && !a.isServerVariant);
                if (picked.length === 0) {
                  window.alert("Pick at least one uploaded asset — sample images can't be moved.");
                  return;
                }
                setBulkMoveOpen(true);
              }}><IcFolder size={13} /> Move</button>
            )}
            <BulkDownloadMenu getPicked={() => visibleAssets.filter((a) => selected.has(a.id))} />
            {showShare && features.canWrite && (
              <button
                className="bulk-act"
                data-bulk="share"
                onClick={() => {
                  const picked = visibleAssets.filter((a) => selected.has(a.id) && a.isServer && !a.isServerVariant);
                  if (picked.length === 0) {
                    window.alert("Pick at least one uploaded asset — sample images can't be shared via real portal links.");
                    return;
                  }
                  onShare && onShare({ presetAssets: picked });
                }}
              >
                <IcLink size={13} /> Share
              </button>
            )}
            {features.canWrite && <span className="bulk-sep" data-bulk="sep" />}
            {features.canWrite && (
              <button className="bulk-act is-danger" data-bulk="delete" onClick={async () => {
                const picked = visibleAssets.filter((a) => selected.has(a.id));
                const serverOnes = picked.filter((a) => a.isServer);
                const mockCount = picked.length - serverOnes.length;
                const msg = serverOnes.length === 0
                  ? `None of the ${picked.length} selected are real uploads — sample images can't be deleted.`
                  : `Delete ${serverOnes.length} uploaded file${serverOnes.length === 1 ? "" : "s"} permanently?${mockCount ? ` (${mockCount} sample item${mockCount === 1 ? "" : "s"} will be skipped.)` : ""}`;
                if (!window.confirm(msg)) return;
                for (const a of serverOnes) {
                  try { await VandayAPI.deleteAsset(a.id); } catch {}
                }
                clearSelected();
              }}><IcTrash size={13} /> Delete</button>
            )}
          </div>
        )}

        {workflowLabelOpen && (
          <WorkflowLabelPopover
            anchorRef={workflowLabelBtnRef}
            onClose={() => setWorkflowLabelOpen(false)}
            workflowLabels={workflowLabels}
            labelFilter={labelFilter}
            setLabelFilter={setLabelFilter}
          />
        )}

        {sortOpen && (
          <SortPopover
            anchorRef={sortBtnRef}
            onClose={() => setSortOpen(false)}
            sortBy={sortBy}
            setSortBy={setSortBy}
            isSemanticQuery={isSemanticQuery}
          />
        )}

        {bulkTagOpen && (() => {
          const picked = visibleAssets.filter((a) => selected.has(a.id) && a.isServer && !a.isServerVariant);
          return (
            <BulkTagModal
              count={picked.length}
              onClose={() => setBulkTagOpen(false)}
              onApply={async (newTags) => {
                for (const a of picked) {
                  const merged = Array.from(new Set([...(a.tags || []), ...newTags]));
                  a.tags = merged;
                  try { await VandayAPI.patchAsset(a.id, { tags: merged }); } catch {}
                }
                setBulkTagOpen(false);
                clearSelected();
              }}
            />
          );
        })()}

        {bulkLabelOpen && (() => {
          const picked = visibleAssets.filter((a) => selected.has(a.id) && a.isServer && !a.isServerVariant);
          return (
            <BulkLabelModal
              count={picked.length}
              labels={workflowLabels}
              onClose={() => setBulkLabelOpen(false)}
              onApply={async (labelId) => {
                for (const a of picked) {
                  a.workflowLabelId = labelId;
                  try { await VandayAPI.patchAsset(a.id, { workflowLabelId: labelId }); } catch {}
                }
                setBulkLabelOpen(false);
                clearSelected();
              }}
            />
          );
        })()}

        {bulkMoveOpen && (() => {
          const picked = visibleAssets.filter((a) => selected.has(a.id) && a.isServer && !a.isServerVariant);
          return (
            <BulkMoveModal
              count={picked.length}
              folders={FOLDERS.filter((f) => f.id !== "all")}
              onClose={() => setBulkMoveOpen(false)}
              onApply={async (folderId) => {
                for (const a of picked) {
                  a.folder = folderId;
                  try { await VandayAPI.patchAsset(a.id, { folder: folderId }); } catch {}
                }
                setBulkMoveOpen(false);
                clearSelected();
              }}
            />
          );
        })()}

        {newFolderModal && (
          <NewFolderModal
            parentName={newFolderModal.parentName}
            onCreate={createFolderFromModal}
            onClose={() => setNewFolderModal(null)}
          />
        )}
        {moveInboxModal && (
          <MoveFromInboxModal
            targetFolderId={moveInboxModal.id}
            targetFolderName={moveInboxModal.name}
            onClose={() => setMoveInboxModal(null)}
            onMoved={() => { forceTick((t) => t + 1); }}
          />
        )}
        {deleteFolderTarget && (
          <DeleteFolderModal
            folder={deleteFolderTarget}
            activeFolder={activeFolder}
            onClose={() => setDeleteFolderTarget(null)}
            onConfirm={async () => {
              const f = deleteFolderTarget;
              // Collect the folder + its descendant ids so we can redirect the
              // view if the user is currently inside the part being deleted.
              const ids = new Set([f.id]);
              (function gather(id) {
                for (const k of (window.childFolders?.(id) || [])) { ids.add(k.id); gather(k.id); }
              })(f.id);
              const r = await fetch(`/api/folders/${encodeURIComponent(f.id)}`, { method: "DELETE" });
              if (!r.ok) throw new Error("Couldn't delete the folder. Please try again.");
              try { window.VandayServer?.bumpRev?.(); } catch {}
              forceTick((t) => t + 1);
              if (ids.has(activeFolder)) setActiveFolder("all");
              setDeleteFolderTarget(null);
            }}
          />
        )}
      </main>
      )}

      {/* Rail popovers \u2014 notifications + help */}
      {popover === "notifs" && (
        <div className="rail-popover" onClick={(e) => e.stopPropagation()}>
          <div className="rail-popover-head">
            <span>Notifications</span>
            <button className="btn ghost sm" onClick={() => setPopover(null)}><IcClose size={12} /></button>
          </div>
          <div className="rail-popover-body">
            {[
              { kind: "approve",  who: "Devon Kim",     what: "approved",           target: "studio-portrait-04.jpg", when: "2 min ago" },
              { kind: "mention",  who: "Priya Shah",    what: "mentioned you on",   target: "campaign-billboard.jpg", when: "38 min ago" },
              { kind: "publish",  who: "Alex Lawson",   what: "published",          target: "ceramics-flatlay.jpg \u2192 Instagram", when: "2 h ago" },
              { kind: "expire",   who: "System",        what: "flagged",            target: "team-offsite-DSC_1142.jpg expiring in 2 days", when: "5 h ago" },
              { kind: "share",    who: "External viewer",what: "viewed portal",      target: "Spring '26 \u2014 Client review", when: "Yesterday" },
            ].map((n, i) => (
              <button key={i} className="rail-notif">
                <span className={`rail-notif-dot is-${n.kind}`} />
                <div style={{ minWidth: 0, flex: 1 }}>
                  <div className="rail-notif-text">
                    <strong>{n.who}</strong> {n.what} <span className="rail-notif-target">{n.target}</span>
                  </div>
                  <div className="rail-notif-when">{n.when}</div>
                </div>
              </button>
            ))}
          </div>
          <div className="rail-popover-foot">
            <button className="btn sm">Mark all read</button>
            <button className="btn ghost sm" onClick={() => { setPopover(null); onNav("settings"); }}>Notification settings</button>
          </div>
        </div>
      )}

      {popover === "help" && (
        <div className="rail-popover" onClick={(e) => e.stopPropagation()}>
          <div className="rail-popover-head">
            <span>Help & shortcuts</span>
            <button className="btn ghost sm" onClick={() => setPopover(null)}><IcClose size={12} /></button>
          </div>
          <div className="rail-popover-body">
            <div className="help-h">Keyboard shortcuts</div>
            {[
              ["\u2318 K",     "Focus search"],
              ["\u2318 U",     "Open upload"],
              ["\u2192 / \u2190",  "Next / prev in preview"],
              ["Esc",      "Close preview / modal"],
              ["Shift + click", "Range-select cards"],
              ["\u2318 click", "Toggle-select a card"],
            ].map(([k, label], i) => (
              <div key={i} className="help-row">
                <kbd className="help-kbd">{k}</kbd>
                <span>{label}</span>
              </div>
            ))}
            <div className="help-h" style={{ marginTop: 14 }}>Resources</div>
            <button className="help-link"><IcInfo size={12} /> Docs & getting started</button>
            <button className="help-link"><IcSend size={12} /> Contact support</button>
            <button className="help-link"><IcSparkles size={12} /> What's new in May '26</button>
          </div>
        </div>
      )}

      {popover && (
        <div
          className="rail-popover-scrim"
          onClick={() => setPopover(null)}
        />
      )}
    </div>
  );
}

window.LibraryPage = LibraryPage;
window.Rail = Rail;
