// 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";
}

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", 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={() => onNav("admin")}
          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>
          <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 }) {
  // The upload cap comes from the server (/api/me limits) so the meter can
  // never drift from what's actually enforced. Falls back to 100 before load.
  const limits = (typeof window.useVandayLimits === "function") ? window.useVandayLimits() : null;
  const uploadCap = limits?.maxUploads ?? 100;
  // 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);
  // Which nested folders are expanded in the tree. Starts all-collapsed.
  const [expanded, setExpanded] = React.useState(() => new Set());
  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)}
          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);
          }}
          title={canWrite && f.isServer ? "Right-click to delete folder" : 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>
          {f.name}
          {isRestricted && <IcShield size={11} className="sidebar-lock" />}
          <span className="sidebar-count">{f.count}</span>
          {canWrite && (
            <span
              className="sidebar-addsub"
              role="button"
              title="New subfolder"
              onClick={(e) => {
                e.stopPropagation();
                setExpanded((s) => new Set(s).add(f.id)); // reveal the new child
                onNewFolder && onNewFolder(f.id);
              }}
            >
              <IcPlus size={12} />
            </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")}
        >
          <span className="ic"><IcGrid size={15} /></span>
          All assets
        </button>
        <button
          className={`sidebar-item ${activeFolder === "__recent" ? "active" : ""}`}
          onClick={() => setActiveFolder("__recent")}
        >
          <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")}
            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>
        )}
        <button
          className={`sidebar-item ${activeFolder === "__shared" ? "active" : ""}`}
          onClick={() => setActiveFolder("__shared")}
        >
          <span className="ic"><IcShare size={15} /></span>
          Shared with me
        </button>
      </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")}
        >
          <span className="ic"><IcTrash size={15} /></span>
          Recently deleted
        </button>
      </div>

      <div className="rail-spacer" />

      {(() => {
        // Usage meters. Uploads is driven by the real count of the user's
        // uploaded originals (crops/variants don't count) against the Free
        // Beta cap; the bar shifts accent → amber (≥75%) → red (≥90%) so
        // people can see they're approaching the limit. Storage stays a
        // placeholder until per-account byte usage is wired up.
        const UPLOAD_CAP = uploadCap;
        const uploadsUsed = Math.min(
          UPLOAD_CAP,
          (window.ASSETS || []).filter((a) => a.isServer && a.kind !== "crop").length
        );
        const upRatio = Math.min(100, Math.round((uploadsUsed / UPLOAD_CAP) * 100));
        const upColor = upRatio >= 90 ? "var(--status-danger)"
          : upRatio >= 75 ? "var(--status-warning)"
          : "var(--accent)";
        // Storage meter — real per-account byte usage against the beta cap.
        const storageCapBytes = limits?.maxStorageBytes ?? (50 * 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 style={{ display: "inline-flex", alignItems: "center", gap: 7 }}><IcUpload size={13} /> Uploads</span>
                <span style={{ fontFamily: "var(--font-mono)" }}><b style={{ color: "var(--text)", fontWeight: 600 }}>{uploadsUsed}</b> / {UPLOAD_CAP}</span>
              </div>
              <div style={meterTrack}>
                <div style={{ width: upRatio + "%", height: "100%", background: upColor, borderRadius: 999 }} />
              </div>
            </div>
            <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>
        );
      })()}
    </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) => {
    if (e.shiftKey || e.metaKey || selectionMode) {
      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" 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={asset.url} alt={asset.name} loading="lazy" />
        )}
        <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>
  );
}

// ============================================================
// 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(${asset.url})` }}
          />
        )}
      </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>
  );
}

// ============================================================
// LibraryFilterPopover — the secondary filter menu, anchored under
// the Filter button. Holds the things that used to live inline in
// the toolbar: orientation, workflow status, and the variants toggle.
// Click outside or hit Escape to close.
// ============================================================
function LibraryFilterPopover({
  anchorRef, onClose,
  workflowLabels, labelFilter, setLabelFilter,
}) {
  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 && (
          <button
            type="button"
            onClick={() => setLabelFilter(null)}
            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 === l.id;
            return (
              <button
                key={l.id}
                type="button"
                onClick={() => { setLabelFilter(on ? null : l.id); onClose(); }}
                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-sunken)"; }}
                onMouseLeave={(e) => { if (!on) e.currentTarget.style.background = "transparent"; }}
              >
                <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>
                {on && <IcCheck size={13} style={{ color: "var(--accent, #c8553d)" }} />}
              </button>
            );
          })}
          <div style={{ height: 1, background: "var(--border)", margin: "4px 0" }} />
          {(() => {
            const on = labelFilter === "__unlabeled";
            return (
              <button
                type="button"
                onClick={() => { setLabelFilter(on ? null : "__unlabeled"); onClose(); }}
                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-sunken)"; }}
                onMouseLeave={(e) => { if (!on) e.currentTarget.style.background = "transparent"; }}
              >
                <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>
                {on && <IcCheck size={13} style={{ color: "var(--accent, #c8553d)" }} />}
              </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("");
  const [orientation, setOrientation] = React.useState("all"); // RD-03
  const [typeFilter, setTypeFilter] = React.useState("all"); // all | images | videos | documents
  const [filterOpen, setFilterOpen] = React.useState(false);
  const filterBtnRef = React.useRef(null);
  const [selected, setSelected] = React.useState(new Set()); // GP-07
  const [lastSelectedId, setLastSelectedId] = React.useState(null);
  const [similarFacet, setSimilarFacet] = React.useState("subject"); // FT-07
  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);
  // Bump to force a re-render after in-place asset/folder-count mutations.
  const [, forceTick] = React.useState(0);
  // Single-select workflow-label filter. A string label id (or the
  // sentinel "__unlabeled") when set; null = no filter.
  const [labelFilter, setLabelFilter] = React.useState(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);
      const isImg = mime.startsWith("image/") || /\.(jpe?g|png|gif|webp|heic|avif|svg|tiff?)$/i.test(name);
      if (typeFilter === "videos") return isVid;
      if (typeFilter === "images") return isImg && !isVid;
      if (typeFilter === "documents") return !isImg && !isVid;
      return true;
    });
  }

  // Workflow-label filter. Single-select: keep only assets whose label
  // matches the chosen one. "__unlabeled" matches assets with no label.
  if (labelFilter) {
    visibleAssets = visibleAssets.filter((a) => {
      if (labelFilter === "__unlabeled") return !a.workflowLabelId;
      return a.workflowLabelId === labelFilter;
    });
  }

  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={setActiveFolder}
        onNav={onNav}
        onNewFolder={newFolder}
        onFolderDrop={onFolderDrop}
        onFolderDelete={async (f) => {
          const count = f.count || 0;
          const msg = count
            ? `Delete folder "${f.name}"? Its ${count} asset${count === 1 ? "" : "s"} will move to your Inbox.`
            : `Delete empty folder "${f.name}"?`;
          if (!window.confirm(msg)) return;
          if (count) {
            // Move every asset in this folder to 'raw' before delete.
            const orphans = (window.ASSETS || []).filter((a) => a.folder === f.id && a.isServer);
            for (const a of orphans) {
              try { await VandayAPI.patchAsset(a.id, { folder: "raw" }); } catch {}
            }
          }
          try {
            const r = await fetch(`/api/folders/${encodeURIComponent(f.id)}`, { method: "DELETE" });
            if (!r.ok) throw new Error("delete failed");
          } catch {}
          if (activeFolder === f.id) setActiveFolder("all");
        }}
      />

      {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)}
              />
              <kbd>⌘K</kbd>
            </div>
            <span className="trial-pill" title={features.mode === "dev" ? "Dev build" : "Free beta plan"}>
              <span className="dot" />
              {features.mode === "dev" ? "Dev" : "Free Beta"}
            </span>
            {showShare && features.canWrite && (
              <button className="btn" onClick={() => onShare && onShare(currentFolder || { id: activeFolder, name: "Library" })}>
                <IcShare 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, flex: 1 }}>
              <div
                style={{
                  width: 64, height: 64, borderRadius: 8,
                  backgroundImage: `url(${similarSource.url})`,
                  backgroundSize: "cover", backgroundPosition: "center",
                  flexShrink: 0,
                  boxShadow: "var(--shadow-sm)",
                }}
              />
              <div style={{ flex: 1, 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>
                <h1 className="lib-title" style={{ fontFamily: "var(--font-mono)", fontSize: 18 }}>{similarSource.name}</h1>
                <div className="lib-sub">
                  <span>{visibleAssets.length} matches</span>
                  <span className="dot" />
                  <span>Ranked by visual embedding</span>
                </div>
              </div>
              <button className="btn" onClick={onClearSimilar}>
                <IcClose size={14} /> Exit similar
              </button>
            </div>
          ) : (
          <div>
            <h1 className="lib-title" style={{ display: "flex", alignItems: "center", gap: 10 }}>
              {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
                  .reduce((s, a) => s + parseFloat(a.size), 0)
                  .toFixed(1)}{" "}
                MB
              </span>
              {isSemanticQuery ? (
                <>
                  <span className="dot" />
                  <span style={{ color: "var(--accent)" }}>Ranked by relevance</span>
                </>
              ) : (
                <>
                  <span className="dot" />
                  <span>Updated 12 min ago</span>
                </>
              )}
            </div>
          </div>
          )}
          {features.canWrite && (
            <div style={{ display: "flex", gap: 8 }}>
              <button className="btn" onClick={() => newFolder()}><IcPlus size={14} /> New folder</button>
            </div>
          )}
        </section>

        {activeFolder === "all" && !isSimilarMode && !query && (
          <div className="folder-strip">
            {FOLDERS.slice(1).map((f) => {
              const isInbox = f.id === "raw";
              return (
                <div
                  key={f.id}
                  className={`folder-tile ${isInbox ? "inbox" : ""} ${activeFolder === f.id ? "active" : ""}`}
                  onClick={() => setActiveFolder(f.id)}
                >
                  <div className="folder-icon">
                    {isInbox ? <IcInbox size={17} /> : <IcFolder size={16} />}
                  </div>
                  <div className="folder-name">{f.name}</div>
                  <div className="folder-count">{f.count} items</div>
                </div>
              );
            })}
          </div>
        )}

        <section className="lib-tools">
          {isSimilarMode ? (
            <>
              <span style={{ fontSize: 11, textTransform: "uppercase", letterSpacing: "0.06em", color: "var(--text-faint)", fontWeight: 500, marginRight: 6 }}>
                Match by
              </span>
              {[
                { id: "subject",     label: "Subject"        },
                { id: "color",       label: "Colour & mood"  },
                { id: "composition", label: "Composition"    },
              ].map((f) => (
                <span
                  key={f.id}
                  className={`chip ${similarFacet === f.id ? "active" : ""}`}
                  style={{ cursor: "pointer" }}
                  onClick={() => setSimilarFacet(f.id)}
                >
                  {f.label}
                </span>
              ))}
              <span style={{ width: 1, height: 18, background: "var(--border)", margin: "0 4px" }} />
            </>
          ) : (
            <>
              {/* Primary inline filter: media type. Short labels, four
                  choices — fits naturally without crowding the toolbar.
                  Everything else (orientation, status, variants) lives in
                  the Filter popover below. */}
              {[
                { id: "all",        label: "All types"  },
                { id: "images",     label: "Images"     },
                { id: "videos",     label: "Videos"     },
                { id: "documents",  label: "Documents"  },
              ].map((t) => (
                <span
                  key={t.id}
                  className={`chip ${typeFilter === t.id ? "active" : ""}`}
                  style={{ cursor: "pointer" }}
                  onClick={() => setTypeFilter(t.id)}
                >
                  {t.label}
                </span>
              ))}

              {/* Published — a search/filter scope rather than a permanent
                  nav entry. Toggling it swaps the grid for the publishing
                  dashboard (PublishedView) and back to "All assets". */}
              <span
                className={`chip ${isPublished ? "active" : ""}`}
                style={{ cursor: "pointer", display: "inline-flex", alignItems: "center", gap: 5 }}
                onClick={() => setActiveFolder(isPublished ? "all" : "__published")}
                title="Show assets you've published"
              >
                <IcSend size={12} /> Published
              </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 — the
                  whole group reads as visual orientation choice at a
                  glance. No label needed; the shapes are the label. */}
              <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) => (
                  <button
                    key={o.id}
                    className={`orient-btn ${orientation === o.id ? "is-on" : ""}`}
                    onClick={() => setOrientation(o.id)}
                    title={o.label}
                    aria-label={o.label}
                  >
                    {o.svg}
                    <span className="orient-tip">{o.label}</span>
                  </button>
                ))}
              </div>

              {/* Active workflow-label pill — the Filter popover is now
                  single-select for workflow status, so at most one pill
                  shows here. The orientation chips above already show
                  their own selection state. */}
              {labelFilter && (
                <>
                  <span style={{ width: 1, height: 18, background: "var(--border)", margin: "0 4px" }} />
                  {(() => {
                    const l = labelFilter === "__unlabeled" ? null : workflowLabels.find((x) => x.id === labelFilter);
                    return (
                      <ActiveFilterPill
                        label={l ? l.name : "Unlabeled"}
                        color={l ? `wf-color-${l.color}` : null}
                        onRemove={() => setLabelFilter(null)}
                      />
                    );
                  })()}
                </>
              )}
            </>
          )}
          <button
            ref={filterBtnRef}
            className="btn sm lib-filter-btn"
            onClick={() => setFilterOpen((v) => !v)}
            style={{ position: "relative" }}
          >
            <IcFilter size={13} /> Filter
            {labelFilter && (
              <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)",
              }}>
                1
              </span>
            )}
          </button>
          <div style={{ marginLeft: "auto", display: "inline-flex", alignItems: "center", gap: 8 }}>
            <button className="btn sm"><IcSort size={13} /> Sort: {isSemanticQuery ? "Relevance" : "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 || 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.
                  </>
                ) : 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(null);
                        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" onClick={selectAll}>Select all ({visibleAssets.length})</button>
            {features.canWrite && <span className="bulk-sep" />}
            {features.canWrite && (
              <button className="bulk-act" 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" 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" 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))} />
            {features.canWrite && <span className="bulk-sep" />}
            {features.canWrite && (
              <button className="bulk-act is-danger" 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>
        )}

        {filterOpen && (
          <LibraryFilterPopover
            anchorRef={filterBtnRef}
            onClose={() => setFilterOpen(false)}
            workflowLabels={workflowLabels}
            labelFilter={labelFilter}
            setLabelFilter={setLabelFilter}
          />
        )}

        {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); }}
          />
        )}
      </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;
