// Vanday DAM — single asset preview
// Editable tag chips. For real (uploaded) assets, changes are persisted via
// PATCH /api/assets/:id. For mocked/demo assets we update locally only —
// they don't exist on the server so we can't save.
function TagEditor({ asset }) {
  // Read directly off the asset; bumping a state forces re-renders after a save.
  const [, bump] = React.useState(0);
  const [draft, setDraft] = React.useState("");
  const tags = Array.isArray(asset.tags) ? asset.tags : [];
  // Viewers see tags but can't edit them (the server rejects the PATCH too).
  const canWrite = (typeof window.useVandayFeatures === "function")
    ? window.useVandayFeatures().canWrite : true;

  const persist = async (next) => {
    asset.tags = next;
    bump((n) => n + 1);
    if (asset.isServer && !asset.isServerVariant) {
      try { await VandayAPI.patchAsset(asset.id, { tags: next }); } catch {}
    }
  };
  const removeTag = (t) => persist(tags.filter((x) => x !== t));
  const addTag = (raw) => {
    const t = raw.toLowerCase().trim().replace(/^#/, "").slice(0, 32);
    if (!t || tags.includes(t)) return;
    persist([...tags, t]);
  };
  const onKey = (e) => {
    if (e.key === "Enter" || e.key === ",") {
      e.preventDefault();
      addTag(draft);
      setDraft("");
    } else if (e.key === "Backspace" && !draft && tags.length) {
      removeTag(tags[tags.length - 1]);
    }
  };

  return (
    <div style={{ display: "flex", flexWrap: "wrap", gap: 6, alignItems: "center" }}>
      {tags.map((t) => (
        <span
          key={t}
          className="kw"
          style={{ display: "inline-flex", alignItems: "center", gap: 4, cursor: "default" }}
          title="Click × to remove"
        >
          {t}
          {canWrite && (
            <button
              onClick={() => removeTag(t)}
              style={{
                background: "transparent", border: 0, color: "inherit",
                opacity: 0.55, cursor: "pointer", padding: "0 2px", lineHeight: 1,
                fontSize: 11,
              }}
              title="Remove tag"
            >×</button>
          )}
        </span>
      ))}
      {!canWrite && tags.length === 0 && (
        <span style={{ color: "var(--text-faint)", fontSize: 11.5, fontStyle: "italic" }}>No tags</span>
      )}
      {canWrite && (
        <input
          value={draft}
          onChange={(e) => setDraft(e.target.value)}
          onKeyDown={onKey}
          onBlur={() => { if (draft) { addTag(draft); setDraft(""); } }}
          placeholder={tags.length ? "+ add" : "Add a tag…"}
          style={{
            flex: "1 1 80px", minWidth: 60, background: "transparent",
            border: "1px dashed var(--border)", color: "var(--text)",
            borderRadius: 6, padding: "3px 8px", fontSize: 11.5,
            fontFamily: "inherit", outline: "none",
          }}
        />
      )}
    </div>
  );
}

function DownloadMenu({ asset }) {
  const [open, setOpen] = React.useState(false);
  const [busy, setBusy] = React.useState(false);
  const ref = React.useRef(null);

  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]);

  const pick = async (format) => {
    setOpen(false);
    setBusy(true);
    try { await VandayServer.downloadAsset(asset, format); }
    finally { setBusy(false); }
  };

  const origExt = (() => {
    const m = /\.([^.]+)$/.exec(asset && asset.name ? asset.name : "");
    return m ? m[1].toUpperCase() : "file";
  })();

  return (
    <div ref={ref} style={{ position: "relative" }}>
      <button className="btn" disabled={busy} onClick={() => setOpen((v) => !v)}>
        <IcDownload size={14} /> {busy ? "Preparing…" : "Download"}
        <IcChevD size={12} style={{ marginLeft: 2 }} />
      </button>
      {open && (
        <div className="send-menu" style={{ left: 0, right: "auto" }}>
          <div className="send-menu-h">Download as</div>
          <button className="send-menu-item" onClick={() => pick("original")}>
            <div style={{ flex: 1 }}>
              <div className="send-menu-name">Original</div>
              <div className="send-menu-account">Keep the source file ({origExt})</div>
            </div>
          </button>
          <button className="send-menu-item" onClick={() => pick("jpg")}>
            <div style={{ flex: 1 }}>
              <div className="send-menu-name">JPG</div>
              <div className="send-menu-account">Smaller file, no transparency</div>
            </div>
          </button>
          <button className="send-menu-item" onClick={() => pick("png")}>
            <div style={{ flex: 1 }}>
              <div className="send-menu-name">PNG</div>
              <div className="send-menu-account">Lossless, keeps transparency</div>
            </div>
          </button>
        </div>
      )}
    </div>
  );
}

// --- Workflow label picker ---------------------------------------------
// Lets any org member change an asset's workflow status. Server assets POST
// to /api/assets/:id; mock/demo assets just update locally.
function WorkflowLabelPicker({ asset }) {
  const { labels } = window.VandayServer.useWorkflowLabels();
  const [open, setOpen] = React.useState(false);
  const [currentId, setCurrentId] = React.useState(asset.workflowLabelId || null);
  const ref = React.useRef(null);
  // Viewers see the status but can't change it.
  const canWrite = (typeof window.useVandayFeatures === "function")
    ? window.useVandayFeatures().canWrite : true;

  React.useEffect(() => { setCurrentId(asset.workflowLabelId || null); }, [asset.id, asset.workflowLabelId]);

  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]);

  const current = (labels || []).find((l) => l.id === currentId) || null;
  const colorClass = current ? `wf-color-${current.color}` : "";

  // Read-only badge for viewers: show the current status, nothing clickable.
  if (!canWrite) {
    if (!current) return null;
    return (
      <span className={`wf-pill ${colorClass}`}>
        <span className="wf-dot" />
        {current.name}
      </span>
    );
  }

  const choose = async (labelId) => {
    setOpen(false);
    setCurrentId(labelId);
    asset.workflowLabelId = labelId;
    if (asset.isServer && !asset.isServerVariant) {
      try {
        await VandayAPI.patchAsset(asset.id, { workflowLabelId: labelId });
      } catch (e) {
        console.warn("Failed to set workflow label", e);
      }
    }
  };

  return (
    <div ref={ref} style={{ position: "relative", display: "inline-block" }}>
      <button
        type="button"
        className={`wf-pill is-button ${current ? colorClass : "is-empty"}`}
        onClick={() => setOpen((v) => !v)}
        title="Change status"
      >
        {current ? (
          <>
            <span className="wf-dot" />
            {current.name}
          </>
        ) : (
          <>+ Set status</>
        )}
      </button>
      {open && (
        <div className="wf-picker-pop">
          {(labels || []).length === 0 && (
            <div style={{ padding: "8px 10px", fontSize: 12, color: "var(--text-faint)" }}>
              No labels yet
            </div>
          )}
          {(labels || []).map((l) => (
            <button
              key={l.id}
              className="wf-picker-opt"
              onClick={() => choose(l.id)}
            >
              <span className={`wf-swatch wf-color-${l.color}`} />
              <span style={{ flex: 1 }}>{l.name}</span>
              {currentId === l.id && <IcCheck size={11} />}
            </button>
          ))}
          {current && (
            <button className="wf-picker-opt is-clear" onClick={() => choose(null)}>
              Clear status
            </button>
          )}
        </div>
      )}
    </div>
  );
}

// --- Server-backed comments + @mention autocomplete --------------------
function ServerComments({ asset, embedded }) {
  const [comments, setComments] = React.useState([]);
  const [draft, setDraft] = React.useState("");
  const [loading, setLoading] = React.useState(true);
  const [mentionState, setMentionState] = React.useState(null); // {query, start, active}
  const [mentioned, setMentioned] = React.useState({}); // displayName -> userId
  const members = window.VandayServer.useOrgMembers();
  const memberDisplay = window.VandayServer.memberDisplayName;
  const inputRef = React.useRef(null);
  const me = window.VandaySession && window.VandaySession.getActiveUser
    ? window.VandaySession.getActiveUser() : null;
  // Viewers can read the thread but can't post (server rejects it too).
  const canWrite = (typeof window.useVandayFeatures === "function")
    ? window.useVandayFeatures().canWrite : true;

  React.useEffect(() => {
    let cancelled = false;
    setLoading(true);
    VandayAPI.listComments(asset.id)
      .then((r) => { if (!cancelled) setComments(r.comments || r || []); })
      .catch(() => { if (!cancelled) setComments([]); })
      .finally(() => { if (!cancelled) setLoading(false); });
    return () => { cancelled = true; };
  }, [asset.id]);

  // Build a quick lookup so we can show author name + initials for each comment.
  const memberById = React.useMemo(() => {
    const m = new Map();
    for (const u of members || []) m.set(u.id, u);
    return m;
  }, [members]);

  const authorFor = (c) => {
    const u = memberById.get(c.userId);
    return u ? memberDisplay(u) : (c.userId ? c.userId.slice(0, 6) : "Unknown");
  };
  const initialsFor = (c) => {
    const n = authorFor(c);
    return n.split(/[\s._-]+/).filter(Boolean).slice(0, 2).map((s) => s[0].toUpperCase()).join("") || "?";
  };
  const whenFor = (c) => {
    if (!c.createdAt) return "";
    const d = new Date(c.createdAt);
    const diff = Date.now() - d.getTime();
    if (diff < 60 * 1000) return "just now";
    if (diff < 3600 * 1000) return `${Math.floor(diff / 60000)}m ago`;
    if (diff < 86400 * 1000) return `${Math.floor(diff / 3600000)}h ago`;
    return d.toLocaleDateString();
  };

  // Detect @mention as the user types.
  const updateDraft = (value, caret) => {
    setDraft(value);
    const upToCaret = value.slice(0, caret);
    const m = upToCaret.match(/(^|\s)@([\w.-]*)$/);
    if (m) {
      setMentionState({ query: m[2].toLowerCase(), start: caret - m[2].length - 1 });
    } else {
      setMentionState(null);
    }
  };

  const filteredMembers = React.useMemo(() => {
    if (!mentionState) return [];
    const q = mentionState.query;
    return (members || [])
      .filter((u) => !me || u.id !== me.id)
      .filter((u) => {
        if (!q) return true;
        const name = memberDisplay(u).toLowerCase();
        const email = (u.email || "").toLowerCase();
        return name.includes(q) || email.includes(q);
      })
      .slice(0, 6);
  }, [mentionState, members, me, memberDisplay]);

  const insertMention = (user) => {
    const name = memberDisplay(user);
    const before = draft.slice(0, mentionState.start);
    const after = draft.slice(mentionState.start + 1 + mentionState.query.length);
    const next = `${before}@${name} ${after}`;
    setDraft(next);
    setMentioned((m) => ({ ...m, [name]: user.id }));
    setMentionState(null);
    requestAnimationFrame(() => {
      if (inputRef.current) {
        inputRef.current.focus();
        const pos = (before + "@" + name + " ").length;
        try { inputRef.current.setSelectionRange(pos, pos); } catch {}
      }
    });
  };

  const submit = async () => {
    const body = draft.trim();
    if (!body) return;
    // Only include mentions that still appear in the body.
    const ids = Object.entries(mentioned)
      .filter(([name]) => body.includes(`@${name}`))
      .map(([, id]) => id);
    try {
      const created = await VandayAPI.postComment(asset.id, body, ids);
      setComments((cs) => [...cs, created]);
      setDraft("");
      setMentioned({});
      setMentionState(null);
    } catch (e) {
      console.warn("Failed to post comment", e);
    }
  };

  const onKeyDown = (e) => {
    if (mentionState && filteredMembers.length && (e.key === "Enter" || e.key === "Tab")) {
      e.preventDefault();
      insertMention(filteredMembers[0]);
      return;
    }
    if (e.key === "Enter" && !e.shiftKey && !mentionState) {
      e.preventDefault();
      submit();
    }
    if (e.key === "Escape" && mentionState) {
      setMentionState(null);
    }
  };

  // Render comment body with @mentions visually highlighted.
  const renderBody = (body) => {
    const parts = body.split(/(@[\w.-]+)/g);
    return parts.map((p, i) =>
      p.startsWith("@")
        ? <strong key={i} style={{ color: "var(--accent)", fontWeight: 600 }}>{p}</strong>
        : <span key={i}>{p}</span>
    );
  };

  return (
    <div className={embedded ? "" : "rp-body"} style={{ color: "var(--text-muted)", fontSize: 13, ...(embedded ? { paddingTop: 4 } : {}) }}>
      {loading && (
        <div style={{ padding: "16px 0", fontSize: 12.5, color: "var(--text-faint)", fontStyle: "italic" }}>
          Loading comments…
        </div>
      )}
      {!loading && comments.length === 0 && (
        <div style={{ padding: "16px 0", fontSize: 12.5, color: "var(--text-faint)", fontStyle: "italic" }}>
          No comments yet.
        </div>
      )}
      {!loading && comments.map((c) => (
        <div key={c.id} style={{ display: "flex", gap: 10, marginBottom: 18 }}>
          <span className="pill-user" style={{ background: "transparent", padding: 0 }}>
            <span className="av">{initialsFor(c)}</span>
          </span>
          <div style={{ flex: 1 }}>
            <div style={{ fontSize: 12.5, color: "var(--text)" }}>
              <strong style={{ fontWeight: 600 }}>{authorFor(c)}</strong> · {whenFor(c)}
              {me && c.userId === me.id && (
                <button
                  onClick={async () => {
                    if (!window.confirm("Delete this comment?")) return;
                    try {
                      await VandayAPI.deleteComment(asset.id, c.id);
                      setComments((cs) => cs.filter((x) => x.id !== c.id));
                    } catch (e) { console.warn(e); }
                  }}
                  style={{
                    marginLeft: 6, background: "transparent", border: 0,
                    color: "var(--text-faint)", cursor: "pointer", fontSize: 11,
                  }}
                  title="Delete comment"
                >×</button>
              )}
            </div>
            <p style={{ margin: "4px 0 0", color: "var(--text)" }}>{renderBody(c.body)}</p>
          </div>
        </div>
      ))}

      <div style={{ position: "relative", display: canWrite ? "block" : "none" }}>
        <input
          ref={inputRef}
          value={draft}
          onChange={(e) => updateDraft(e.target.value, e.target.selectionStart || e.target.value.length)}
          onKeyDown={onKeyDown}
          placeholder="Add a comment… type @ to mention"
          style={{
            width: "100%",
            background: "var(--surface)",
            border: "1px solid var(--border)",
            borderRadius: 8,
            padding: "10px 12px",
            fontSize: 13,
            outline: "none",
          }}
        />
        {mentionState && filteredMembers.length > 0 && (
          <div className="mention-pop" style={{ bottom: "100%", left: 0, marginBottom: 4 }}>
            {filteredMembers.map((u, i) => (
              <button
                key={u.id}
                className={`mention-opt ${i === 0 ? "is-active" : ""}`}
                onClick={() => insertMention(u)}
              >
                <span className="av">
                  {memberDisplay(u).split(/[\s._-]+/).filter(Boolean).slice(0, 2).map((s) => s[0].toUpperCase()).join("") || "?"}
                </span>
                <span style={{ flex: 1 }}>
                  <div style={{ color: "var(--text)" }}>{memberDisplay(u)}</div>
                  {u.email && <div style={{ fontSize: 11, color: "var(--text-faint)" }}>{u.email}</div>}
                </span>
              </button>
            ))}
          </div>
        )}
      </div>
    </div>
  );
}

function PreviewPage({ asset, onClose, onNav, onPrev, onNext, index, total, onPublish, onOpenAsset, onFindSimilar, onShare, backStack, onBack, onQuickUpload }) {
  // Plan limits drive which action buttons we render. Default to "off" while
  // /api/me is loading so the UI doesn't briefly flash beta-disallowed items.
  const limits = (typeof window.useVandayLimits === "function")
    ? window.useVandayLimits()
    : { sharingEnabled: true, publishingEnabled: true };
  const features = (typeof window.useVandayFeatures === "function")
    ? window.useVandayFeatures()
    : { comments: true, versions: true };
  // Role gates: viewers can't publish/share/delete; contributors can't publish.
  const showPublish = limits.publishingEnabled !== false && features.canPublish;
  const showShare = limits.sharingEnabled !== false && features.canWrite;
  // Server-backed assets always get the comments tab (real DB-backed thread)
  // regardless of the prototype feature flag.
  const showComments = features.comments || (asset && asset.isServer && !asset.isServerVariant);
  const showVersions = features.versions;
  const [tab, setTab] = React.useState("fields");
  const [zoom, setZoom] = React.useState(100);
  // Right-panel collapse state + the pop-up Comments modal.
  const [panelOpen, setPanelOpen] = React.useState(true);
  const [commentsModalOpen, setCommentsModalOpen] = React.useState(false);
  // Bumped whenever the comments modal closes so the Details-tab comment
  // list (a separate ServerComments instance) remounts and re-fetches —
  // otherwise comments added in the modal wouldn't appear until refresh.
  const [commentsRev, setCommentsRev] = React.useState(0);
  const closeCommentsModal = React.useCallback(() => {
    setCommentsModalOpen(false);
    setCommentsRev((r) => r + 1);
  }, []);
  React.useEffect(() => {
    if (!commentsModalOpen) return;
    const onKey = (e) => { if (e.key === "Escape") closeCommentsModal(); };
    window.addEventListener("keydown", onKey);
    return () => window.removeEventListener("keydown", onKey);
  }, [commentsModalOpen, closeCommentsModal]);
  // The Crops tab is hidden for videos; if we navigate onto a video while it
  // was the active tab (e.g. arrow keys between assets), fall back to Details.
  React.useEffect(() => {
    if (tab === "crops" && assetIsVideo(asset)) setTab("fields");
  }, [asset && asset.id, tab]);
  const [overlay, setOverlay] = React.useState(null);
  const [sendTarget, setSendTarget] = React.useState(null);
  const [commentDraft, setCommentDraft] = React.useState("");
  // Comments start empty for free beta — fake "Maya / Devon" seed comments
  // were a prototype affordance. Real comment threads will arrive with team
  // workspaces.
  const [comments, setComments] = React.useState(() => (
    Array.isArray(asset.comments) ? asset.comments : []
  ));
  const addComment = () => {
    const body = commentDraft.trim();
    if (!body) return;
    const me = window.VandaySession.getActiveUser();
    const next = [
      ...comments,
      {
        id: `c-${Date.now()}`,
        author: (me && me.name) || "Unknown",
        initials: window.VandaySession.initials(me),
        color: (me && me.color) || null,
        when: "Just now",
        body,
      },
    ];
    setComments(next);
    asset.comments = next;
    setCommentDraft("");
  };

  if (!asset) return null;

  const isCrop = asset.kind === "crop";
  // Videos get no auto-crops, so the Crops tab is hidden for them.
  const isVideo = assetIsVideo(asset);
  const parent = isCrop ? ASSETS.find((p) => p.id === asset.parentId) : asset;
  const siblings = VARIANTS.filter((v) => v.parentId === parent.id);
  const cropAspect = isCrop ? `${asset.w} / ${asset.h}` : null;

  // Shared comment thread (list + input). Used both in the Details tab and
  // the pop-up Comments modal. Server-backed assets get the live thread;
  // sample assets fall back to local in-memory comments.
  const renderCommentsBody = () => (
    asset.isServer && !asset.isServerVariant ? (
      <ServerComments key={`sc-${commentsRev}`} asset={asset} embedded />
    ) : (
      <div style={{ color: "var(--text-muted)", fontSize: 13 }}>
        {comments.length === 0 && (
          <div style={{ padding: "8px 0", fontSize: 12.5, color: "var(--text-faint)", fontStyle: "italic" }}>
            No comments yet.
          </div>
        )}
        {comments.map((c) => (
          <div key={c.id} style={{ display: "flex", gap: 10, marginBottom: 18 }}>
            <span className="pill-user" style={{ background: "transparent", padding: 0 }}>
              <span className="av" style={c.color ? { background: c.color } : undefined}>{c.initials}</span>
            </span>
            <div>
              <div style={{ fontSize: 12.5, color: "var(--text)" }}>
                <strong style={{ fontWeight: 600 }}>{c.author}</strong> · {c.when}
              </div>
              <p style={{ margin: "4px 0 0", color: "var(--text)" }}>{c.body}</p>
            </div>
          </div>
        ))}
        <input
          value={commentDraft}
          onChange={(e) => setCommentDraft(e.target.value)}
          onKeyDown={(e) => {
            if (e.key === "Enter" && !e.shiftKey) {
              e.preventDefault();
              addComment();
            }
          }}
          placeholder="Add a comment…"
          style={{
            width: "100%",
            background: "var(--surface)",
            border: "1px solid var(--border)",
            borderRadius: 8,
            padding: "10px 12px",
            fontSize: 13,
            outline: "none",
          }}
        />
      </div>
    )
  );

  return (
    <div className={"app preview-shell" + (panelOpen ? "" : " panel-collapsed")}>
      <Rail active="library" onNav={onNav} onLogout={() => onNav("login")} onSearch={() => onNav("library")} onNotifs={() => onNav("library")} onHelp={() => onNav("library")} onQuickUpload={onQuickUpload} />

      <main className="main">
        <header className="topbar preview-topbar">
          <div className="preview-topbar-row">
            {backStack && backStack.length > 0 ? (
              <button className="btn ghost sm preview-back" onClick={onBack} title={`Back to ${backStack[backStack.length - 1].name}`}>
                <IcChevL size={14} />
                <span className="preview-back-label">{backStack[backStack.length - 1].name}</span>
              </button>
            ) : (
              <button className="btn ghost sm" onClick={onClose} title="Back to library">
                <IcChevL size={16} />
                <span style={{ fontSize: 12.5 }}>Library</span>
              </button>
            )}
            <div className="crumbs preview-crumbs">
              <span className="sep">/</span>
              <span>{(FOLDERS.find((f) => f.id === asset.folder) || {}).name}</span>
              <span className="sep">/</span>
              {isCrop ? (
                <>
                  <a
                    href="#"
                    onClick={(e) => { e.preventDefault(); onOpenAsset && onOpenAsset(parent); }}
                    className="crumb-file"
                    title={`Open original — ${parent.name}`}
                  >
                    {parent.name}
                  </a>
                  <span className="sep">/</span>
                  <span className="here crumb-file">{asset.ratio} {asset.ratioName}</span>
                </>
              ) : (
                <span className="here crumb-file" title={asset.name}>{asset.name}</span>
              )}
            </div>
            <div className="preview-nav">
              <button className="btn ghost sm" onClick={onPrev} title="Previous"><IcChevL size={14} /></button>
              <span className="preview-nav-counter">
                {index + 1} <span style={{ color: "var(--text-faint)" }}>of</span> {total}
              </span>
              <button className="btn ghost sm" onClick={onNext} title="Next"><IcChevR size={14} /></button>
            </div>
          </div>
          <div className="preview-topbar-row preview-actions">
            <DownloadMenu asset={asset} />
            <SendToMenu asset={parent} onSend={(integration) => setSendTarget(integration)} />
            <button className="btn" onClick={() => onFindSimilar && onFindSimilar(parent)}>
              <IcSparkles size={14} /> Find similar
            </button>
            {asset.isServer && features.canWrite && (
              <button
                className="btn"
                title="Delete this asset"
                onClick={async () => {
                  if (!window.confirm(`Delete "${asset.name}" permanently?`)) return;
                  try { await VandayAPI.deleteAsset(asset.id); } catch {}
                  if (onClose) onClose();
                }}
              >
                <IcTrash size={14} /> Delete
              </button>
            )}
            <span style={{ flex: 1 }} />
            {showPublish && (
              <button className="btn" onClick={() => onPublish && onPublish(asset)}>
                <IcSend size={14} /> Publish
              </button>
            )}
            {showShare && (
              <button className="btn accent" onClick={() => onShare && onShare({ asset: parent })}>
                <IcShare size={14} /> Share
              </button>
            )}
          </div>
        </header>

        <div className="preview-canvas">
          {assetIsVideo(asset) ? (
            <video
              src={asset.url}
              controls
              playsInline
              preload="metadata"
            />
          ) : isCrop ? (
            <div
              style={{
                aspectRatio: cropAspect,
                maxHeight: "78%",
                maxWidth: "78%",
                background: "black",
                borderRadius: 4,
                boxShadow: "var(--shadow-lg)",
                overflow: "hidden",
                transform: `scale(${zoom / 100})`,
                transition: "transform 160ms",
              }}
            >
              <img
                src={asset.url}
                alt={asset.name}
                style={{ width: "100%", height: "100%", objectFit: "cover", objectPosition: "center", display: "block" }}
              />
            </div>
          ) : (
            <img
              src={asset.url}
              alt={asset.name}
              style={{ transform: `scale(${zoom / 100})`, transition: "transform 160ms" }}
            />
          )}
          {!isVideo && (
          <div className="preview-toolbar">
            <button onClick={() => setZoom((z) => Math.max(25, z - 25))} title="Zoom out">
              <IcZoomOut size={16} />
            </button>
            <span className="zoom-val">{zoom}%</span>
            <button onClick={() => setZoom((z) => Math.min(400, z + 25))} title="Zoom in">
              <IcZoomIn size={16} />
            </button>
            <span className="sep" />
            <button onClick={() => setZoom(100)} title="Fit"><IcFullscreen size={16} /></button>
            <span className="sep" />
            {showComments && (
              <button title="Comments" onClick={() => setCommentsModalOpen(true)}><IcInfo size={16} /></button>
            )}
          </div>
          )}
        </div>
      </main>

      {!panelOpen && (
        <button
          className="rp-reopen"
          title="Show details panel"
          onClick={() => setPanelOpen(true)}
        >
          <IcChevL size={16} />
        </button>
      )}

      <aside className="right-panel" style={panelOpen ? undefined : { display: "none" }}>
        <div className="rp-tabs">
          <button className={`rp-tab ${tab === "fields" ? "active" : ""}`} onClick={() => setTab("fields")}>
            Details
          </button>
          {!isVideo && (
            <button className={`rp-tab ${tab === "crops" ? "active" : ""}`} onClick={() => setTab("crops")}>
              Crops
            </button>
          )}
          <button className={`rp-tab ${tab === "related" ? "active" : ""}`} onClick={() => setTab("related")}>
            Related
          </button>
          {showVersions && (
            <button className={`rp-tab ${tab === "versions" ? "active" : ""}`} onClick={() => setTab("versions")}>
              Versions
            </button>
          )}
          <button
            className="rp-collapse"
            title="Hide panel"
            onClick={() => setPanelOpen(false)}
          >
            <IcChevR size={16} />
          </button>
        </div>

        {tab === "fields" && (
          <div className="rp-body">
            {isCrop && (
              <div style={{
                background: "var(--surface-soft)",
                border: "1px solid var(--border)",
                borderRadius: 10,
                padding: 12,
                marginBottom: 18,
                display: "flex",
                gap: 12,
                alignItems: "center",
              }}>
                <div style={{
                  width: 44, height: 44, borderRadius: 6,
                  backgroundImage: `url(${parent.url})`,
                  backgroundSize: "cover", backgroundPosition: "center",
                  flexShrink: 0,
                }} />
                <div style={{ minWidth: 0, flex: 1 }}>
                  <div style={{ display: "inline-flex", alignItems: "center", gap: 6, fontSize: 10.5, color: "var(--text-faint)", textTransform: "uppercase", letterSpacing: "0.06em", fontWeight: 500 }}>
                    <IcLink size={11} /> Crop of
                  </div>
                  <a
                    href="#"
                    onClick={(e) => { e.preventDefault(); onOpenAsset && onOpenAsset(parent); }}
                    style={{
                      display: "block",
                      fontFamily: "var(--font-mono)", fontSize: 12,
                      color: "var(--text)", textDecoration: "none",
                      overflow: "hidden", textOverflow: "ellipsis", whiteSpace: "nowrap",
                      marginTop: 2,
                    }}
                  >
                    {parent.name}
                  </a>
                </div>
                <button className="btn sm" onClick={() => onOpenAsset && onOpenAsset(parent)}>
                  Open original
                </button>
              </div>
            )}
            <h3 className="rp-title">
              {isCrop ? `${asset.ratioName} · ${asset.ratio}` : asset.name}
            </h3>
            <p className="rp-sub">
              {asset.w} × {asset.h} · {asset.size}
              {!isCrop && ` · uploaded ${asset.date}`}
              {isCrop && asset.sites && (
                <span style={{ display: "inline-flex", gap: 4, marginLeft: 8, verticalAlign: "middle" }}>
                  {asset.sites.map((sid) => <SiteMark key={sid} site={sid} size={12} />)}
                </span>
              )}
            </p>

            <div className="field-grid">
              <div className="field-block">
                <div className="field-label"><IcEye size={11} /> Status</div>
                <WorkflowLabelPicker asset={asset} />
              </div>
              <div className="field-block">
                <div className="field-label">Date uploaded</div>
                <span className="field-value field-mono">{asset.date}</span>
              </div>
              <div className="field-block">
                <div className="field-label">File size</div>
                <span className="field-value field-mono">{asset.size}</span>
              </div>
              <div className="field-block">
                <div className="field-label">Dimensions</div>
                <span className="field-value field-mono">{asset.w} × {asset.h}</span>
              </div>
            </div>

            <div className="field-block">
              <div className="field-label">
                <IcSparkles size={11} /> Auto-tagged keywords
                <span className="tag ai" style={{ marginLeft: "auto", fontSize: 9 }}>AI</span>
              </div>
              <TagEditor asset={asset} />
            </div>

            <div className="field-block">
              <div className="field-label">Folder</div>
              <span className="role-pill">
                {(FOLDERS.find((f) => f.id === asset.folder) || {}).name}
              </span>
            </div>

            {showPublish && (
              <div className="field-block">
                <div className="field-label">
                  <IcSend size={11} /> Publications
                  <button
                    className="btn ghost sm"
                    style={{ marginLeft: "auto", padding: "2px 8px", fontSize: 11 }}
                    onClick={() => onPublish && onPublish(asset)}
                  >
                    <IcPlus size={11} /> Publish
                  </button>
                </div>
                <PublicationList asset={asset} />
              </div>
            )}

            {/* Comments now live inside Details (no separate tab). */}
            <div className="field-block" style={{ borderTop: "1px solid var(--border)", paddingTop: 16, marginTop: 4 }}>
              <div className="field-label">Comments</div>
              {renderCommentsBody()}
            </div>
          </div>
        )}

        {tab === "crops" && (
          <div className="rp-body">
            <h3 className="rp-title" style={{ fontFamily: "var(--font-sans)", fontSize: 14 }}>
              {isCrop ? "Other crops of this asset" : "Auto-generated crops"}
            </h3>
            <p className="rp-sub">
              <IcSparkles size={11} style={{ verticalAlign: "middle", marginRight: 4 }} />
              Subject-aware reframes — click any crop to open it.
            </p>
            <CropsGrid
              asset={asset}
              onOpenVariant={(v) => onOpenAsset && onOpenAsset(v)}
              highlightId={isCrop ? asset.id : null}
            />
            {showPublish && (
              <p className="rp-sub" style={{ marginTop: 16, color: "var(--text-faint)" }}>
                You'll pick which crop to publish (or let Vanday choose) when you hit Publish.
              </p>
            )}
          </div>
        )}

        {tab === "related" && (
          <RelatedTab
            asset={parent}
            onOpenAsset={onOpenAsset}
            onPeek={(a, siblings) => setOverlay({ siblings, idx: siblings.findIndex((s) => s.id === a.id) })}
          />
        )}

        {tab === "versions" && (
          <div className="rp-body" style={{ fontSize: 13, color: "var(--text-muted)" }}>
            {[3, 2, 1].map((v) => {
              const isCurrent = v === 3;
              const baseName = asset.name.replace(/\.[^.]+$/, "");
              const ext = (asset.name.match(/\.[^.]+$/) || [".jpg"])[0];
              const versionAsset = isCurrent ? asset : {
                ...asset,
                id: `${asset.id}-v${v}`,
                name: `${baseName}-v${v}${ext}`,
                date: `May ${v + 6}, 2026`,
                comments: [],
                publications: [],
                isVersionOf: asset.id,
              };
              const open = () => {
                if (isCurrent) return;
                onOpenAsset && onOpenAsset(versionAsset);
              };
              return (
                <button
                  key={v}
                  onClick={open}
                  disabled={isCurrent}
                  title={isCurrent ? "You're viewing this version" : `Open Version ${v}`}
                  style={{
                    display: "flex", gap: 12, padding: "12px 0",
                    width: "100%", textAlign: "left",
                    background: "transparent", border: 0,
                    borderBottom: v > 1 ? "1px solid var(--border)" : 0,
                    cursor: isCurrent ? "default" : "pointer",
                    color: "inherit",
                  }}
                >
                  <div style={{
                    width: 46, height: 46, borderRadius: 6,
                    backgroundImage: `url(${asset.url})`,
                    backgroundSize: "cover", backgroundPosition: "center",
                    flexShrink: 0,
                    outline: isCurrent ? "1.5px solid var(--accent)" : "1px solid var(--border)",
                    outlineOffset: -1,
                  }} />
                  <div style={{ flex: 1, minWidth: 0 }}>
                    <div style={{ color: "var(--text)", fontSize: 13, fontWeight: 500 }}>
                      Version {v}{isCurrent ? " (current)" : ""}
                    </div>
                    <div style={{ fontSize: 12, marginTop: 2 }}>{asset.uploader} · May {v + 6}, 2026</div>
                  </div>
                  {!isCurrent && (
                    <span style={{ alignSelf: "center", color: "var(--text-faint)" }}>
                      <IcChevR size={14} />
                    </span>
                  )}
                </button>
              );
            })}
          </div>
        )}
      </aside>

      {overlay && (
        <PreviewOverlay
          siblings={overlay.siblings}
          startIdx={overlay.idx}
          sourceAsset={parent}
          onClose={() => setOverlay(null)}
          onOpenFull={(a) => { setOverlay(null); onOpenAsset && onOpenAsset(a); }}
        />
      )}

      {sendTarget && (
        <SendToModal
          asset={parent}
          integration={sendTarget}
          onClose={() => setSendTarget(null)}
        />
      )}

      {commentsModalOpen && (
        <div className="modal-backdrop" onClick={closeCommentsModal}>
          <div className="modal" onClick={(e) => e.stopPropagation()} style={{ maxWidth: 480 }}>
            <header className="modal-head">
              <div>
                <h2 className="modal-title">Comments</h2>
                <div style={{ fontSize: 12.5, color: "var(--text-muted)", marginTop: 2, fontFamily: "var(--font-mono)" }}>
                  {asset.name}
                </div>
              </div>
              <button className="btn ghost sm" onClick={closeCommentsModal} title="Close">
                <IcClose size={16} />
              </button>
            </header>
            <div className="modal-body" style={{ display: "block", padding: 20, maxHeight: "62vh", overflowY: "auto" }}>
              {renderCommentsBody()}
            </div>
          </div>
        </div>
      )}
    </div>
  );
}

// FT-11 \u2014 Related assets. Stored per-asset in module-scope so the
// relationship persists when you navigate between asset previews
// (without a real backend). In production this would round-trip to
// the API and be bidirectional.
const RELATIONSHIP_KINDS = [
  { id: "alt",     label: "Alternate angle" },
  { id: "ba",      label: "Before \u2192 after" },
  { id: "variant", label: "Variant"          },
  { id: "version", label: "Version"          },
  { id: "master",  label: "Master / derivative" },
];

const _relatedStore = {};
function getRelated(assetId) {
  if (_relatedStore[assetId]) return _relatedStore[assetId];
  // Seed with two examples per asset on first read.
  const self = ASSETS.find((a) => a.id === assetId);
  if (!self) return (_relatedStore[assetId] = []);
  const seeds = ASSETS
    .filter((a) => a.id !== assetId && a.folder === self.folder)
    .slice(0, 2)
    .map((a, i) => ({ id: a.id, kind: i === 0 ? "alt" : "variant" }));
  _relatedStore[assetId] = seeds;
  return seeds;
}
function setRelated(assetId, list) {
  _relatedStore[assetId] = list;
}

// Asset browser modal — used by Related (and reusable for bulk move,
// publish picker, etc). Big grid + search + folder rail + "Find similar"
// toggle so a 10k-asset library is still navigable.
function AssetBrowserModal({
  title,
  subtitle,
  source,         // optional — asset that "Find similar" ranks against
  excludeIds,     // Set of ids to hide (already linked, etc)
  relationshipKinds,
  initialKind,
  onClose,
  onConfirm,
  confirmLabel = "Add",
}) {
  const [query, setQuery] = React.useState("");
  const [folder, setFolder] = React.useState("all");
  const [orient, setOrient] = React.useState("all");
  const [type, setType] = React.useState("all"); // all | images | videos | docs
  const [similar, setSimilar] = React.useState(false);
  const [picked, setPicked] = React.useState(new Set());
  const [kind, setKind] = React.useState(initialKind || (relationshipKinds && relationshipKinds[0].id));

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

  let pool = ASSETS.filter((a) => !excludeIds || !excludeIds.has(a.id));
  if (source) pool = pool.filter((a) => a.id !== source.id);
  if (folder !== "all") pool = pool.filter((a) => a.folder === folder);
  if (orient !== "all") {
    pool = pool.filter((a) => {
      const r = a.w / a.h;
      const o = Math.abs(r - 1) < 0.06 ? "square" : r > 1 ? "landscape" : "portrait";
      return o === orient;
    });
  }
  if (query) {
    const q = query.toLowerCase();
    pool = pool.filter((a) =>
      a.name.toLowerCase().includes(q) ||
      (a.tags || []).some((t) => t.toLowerCase().includes(q)) ||
      (a.uploader || "").toLowerCase().includes(q)
    );
  }

  // "Find similar" — rank by tag overlap + folder match with source.
  if (similar && source) {
    const srcTags = new Set(source.tags || []);
    pool = pool
      .map((a) => {
        const overlap = (a.tags || []).filter((t) => srcTags.has(t)).length;
        const folderBonus = a.folder === source.folder ? 1 : 0;
        return { a, score: overlap * 2 + folderBonus };
      })
      .filter((x) => x.score > 0)
      .sort((x, y) => y.score - x.score)
      .map((x) => x.a);
  }

  const togglePick = (id, withShift) => {
    setPicked((s) => {
      const next = new Set(s);
      if (next.has(id)) next.delete(id);
      else next.add(id);
      return next;
    });
  };

  const folderTotals = React.useMemo(() => {
    const m = { all: 0 };
    ASSETS.forEach((a) => {
      m.all += 1;
      m[a.folder] = (m[a.folder] || 0) + 1;
    });
    return m;
  }, []);

  return (
    <div className="modal-backdrop" onClick={onClose}>
      <div
        className="modal browser-modal"
        onClick={(e) => e.stopPropagation()}
      >
        <div className="modal-head">
          <div>
            <h2 className="modal-title">{title}</h2>
            {subtitle && (
              <div style={{ fontSize: 12, color: "var(--text-muted)", marginTop: 4 }}>
                {subtitle}
              </div>
            )}
          </div>
          <button className="btn ghost sm" onClick={onClose}>
            <IcClose size={14} />
          </button>
        </div>

        {/* Toolbar */}
        <div className="browser-toolbar">
          <div className="browser-search">
            <IcSearch size={14} />
            <input
              autoFocus
              placeholder="Search by filename, tag, or uploader…"
              value={query}
              onChange={(e) => setQuery(e.target.value)}
            />
            {query && (
              <button className="browser-clear" onClick={() => setQuery("")} title="Clear">
                <IcClose size={11} />
              </button>
            )}
          </div>

          {source && (
            <button
              className={`browser-similar ${similar ? "is-on" : ""}`}
              onClick={() => setSimilar((v) => !v)}
              title="Rank by visual similarity to the source asset"
            >
              <div
                className="browser-similar-thumb"
                style={{ backgroundImage: `url(${source.url})` }}
              />
              <span>
                <span className="browser-similar-h">
                  <IcSparkles size={11} /> Find similar
                </span>
                <span className="browser-similar-sub">
                  {similar ? "Ranked by visual + tag match" : `to ${source.name}`}
                </span>
              </span>
              <span className={`toggle ${similar ? "on" : ""}`} style={{ marginLeft: "auto" }} />
            </button>
          )}

          <div className="browser-segments">
            {[
              { id: "all",    label: "All"       },
              { id: "images", label: "Images"    },
              { id: "videos", label: "Videos"    },
              { id: "docs",   label: "Documents" },
            ].map((t) => (
              <button
                key={t.id}
                className={`browser-seg ${type === t.id ? "is-on" : ""}`}
                onClick={() => setType(t.id)}
              >
                {t.label}
              </button>
            ))}
          </div>
          <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 ${orient === o.id ? "is-on" : ""}`}
                onClick={() => setOrient(o.id)}
                title={o.label}
                aria-label={o.label}
              >
                {o.svg}
                <span className="orient-tip">{o.label}</span>
              </button>
            ))}
          </div>
        </div>

        {/* Body: folder rail + result grid */}
        <div className="browser-body">
          <aside className="browser-rail">
            <div className="browser-rail-h">Folders</div>
            <button
              className={`browser-folder ${folder === "all" ? "is-on" : ""}`}
              onClick={() => setFolder("all")}
            >
              <span><IcGrid size={13} /> All assets</span>
              <span className="browser-folder-count">{folderTotals.all}</span>
            </button>
            {FOLDERS.slice(1).map((f) => (
              <button
                key={f.id}
                className={`browser-folder ${folder === f.id ? "is-on" : ""}`}
                onClick={() => setFolder(f.id)}
              >
                <span><IcFolder size={13} /> {f.name}</span>
                <span className="browser-folder-count">{folderTotals[f.id] || 0}</span>
              </button>
            ))}
          </aside>

          <div className="browser-grid-wrap">
            {similar && source && (
              <div className="browser-similar-bar">
                <IcSparkles size={12} />
                Showing visually similar to <strong>{source.name}</strong> · ranked by combined embedding + tag overlap
                <button onClick={() => setSimilar(false)}>Turn off</button>
              </div>
            )}
            {pool.length === 0 ? (
              <div className="browser-empty">
                <div style={{ marginBottom: 8 }}><IcSearch size={22} /></div>
                Nothing matches these filters.
                <button
                  className="btn sm"
                  style={{ marginTop: 12 }}
                  onClick={() => { setQuery(""); setFolder("all"); setOrient("all"); setSimilar(false); }}
                >
                  Reset filters
                </button>
              </div>
            ) : (
              <div className="browser-grid">
                {pool.map((a) => {
                  const isOn = picked.has(a.id);
                  return (
                    <button
                      key={a.id}
                      className={`browser-card ${isOn ? "is-on" : ""}`}
                      onClick={(e) => togglePick(a.id, e.shiftKey)}
                    >
                      <div className="browser-card-thumb">
                        <img src={a.url} alt="" loading="lazy" />
                        <span className={`browser-card-check ${isOn ? "is-on" : ""}`}>
                          {isOn && <IcCheck size={11} />}
                        </span>
                      </div>
                      <div className="browser-card-body">
                        <div className="browser-card-name">{a.name}</div>
                        <div className="browser-card-meta">
                          <span>{a.uploader}</span>
                          <span>·</span>
                          <span style={{ fontFamily: "var(--font-mono)" }}>
                            {a.w}×{a.h}
                          </span>
                        </div>
                      </div>
                    </button>
                  );
                })}
              </div>
            )}
          </div>
        </div>

        <div className="modal-foot browser-foot">
          <div style={{ fontSize: 12.5, color: "var(--text-muted)", display: "flex", alignItems: "center", gap: 14 }}>
            <span>
              <strong style={{ color: "var(--text)", fontFamily: "var(--font-mono)" }}>
                {picked.size}
              </strong>{" "}
              selected
            </span>
            <span style={{ color: "var(--text-faint)" }}>·</span>
            <span>{pool.length.toLocaleString()} of {ASSETS.length.toLocaleString()} shown</span>
            {picked.size > 0 && (
              <button
                className="bulk-link"
                style={{ color: "var(--text-muted)", padding: "2px 6px" }}
                onClick={() => setPicked(new Set())}
              >
                Clear
              </button>
            )}
          </div>
          <div style={{ display: "flex", alignItems: "center", gap: 10 }}>
            {relationshipKinds && (
              <>
                <span style={{ fontSize: 12, color: "var(--text-muted)" }}>{confirmLabel}</span>
                <select
                  className="related-kind-select is-large"
                  value={kind}
                  onChange={(e) => setKind(e.target.value)}
                >
                  {relationshipKinds.map((k) => (
                    <option key={k.id} value={k.id}>{k.label}</option>
                  ))}
                </select>
              </>
            )}
            <button className="btn" onClick={onClose}>Cancel</button>
            <button
              className="btn accent"
              disabled={picked.size === 0}
              style={picked.size === 0 ? { opacity: 0.5, cursor: "not-allowed" } : undefined}
              onClick={() => onConfirm(Array.from(picked), kind)}
            >
              {relationshipKinds ? `Link ${picked.size || ""}` : `${confirmLabel} ${picked.size || ""}`}
            </button>
          </div>
        </div>
      </div>
    </div>
  );
}

window.AssetBrowserModal = AssetBrowserModal;

function RelatedTab({ asset, onOpenAsset, onPeek }) {
  const [items, setItems] = React.useState(() => getRelated(asset.id));
  const [picking, setPicking] = React.useState(false);
  const [pendingKind, setPendingKind] = React.useState("alt");

  // Sync back to store whenever items change (so other previews see updates).
  React.useEffect(() => { setRelated(asset.id, items); }, [items, asset.id]);

  // Reset when switching to a different asset.
  React.useEffect(() => {
    setItems(getRelated(asset.id));
    setPicking(false);
  }, [asset.id]);

  const linkedIds = new Set(items.map((r) => r.id));

  const removeOne = (id) => setItems((xs) => xs.filter((r) => r.id !== id));
  const addMany = (ids, kind) => {
    setPendingKind(kind);
    setItems((xs) => [
      ...xs,
      ...ids.filter((id) => !linkedIds.has(id)).map((id) => ({ id, kind })),
    ]);
    setPicking(false);
  };
  const changeKind = (id, kind) => {
    setItems((xs) => xs.map((r) => (r.id === id ? { ...r, kind } : r)));
  };

  return (
    <div className="rp-body" style={{ fontSize: 13 }}>
      <h3 className="rp-title" style={{ fontFamily: "var(--font-sans)", fontSize: 14 }}>
        Related assets
      </h3>
      <p className="rp-sub">
        Manually linked variants, alts and before/afters. Linked both ways.
      </p>

      {items.length === 0 && !picking && (
        <div style={{
          padding: 24, textAlign: "center",
          background: "var(--surface-soft)",
          border: "1px dashed var(--border-strong)",
          borderRadius: 10,
          color: "var(--text-faint)",
          fontSize: 12.5,
          marginBottom: 10,
        }}>
          <div style={{ marginBottom: 6 }}>
            <IcLink size={20} style={{ color: "var(--text-faint)" }} />
          </div>
          No related assets yet. Use <strong style={{ color: "var(--text)", fontWeight: 600 }}>+ Add related</strong> to link one.
        </div>
      )}

      {items.map((rel) => {
        const a = ASSETS.find((x) => x.id === rel.id);
        if (!a) return null;
        const kind = RELATIONSHIP_KINDS.find((k) => k.id === rel.kind) || RELATIONSHIP_KINDS[0];
        return (
          <div key={rel.id} className="related-row">
            <button
              className="related-row-main"
              onClick={() => onPeek ? onPeek(a, items.map((r) => ASSETS.find((x) => x.id === r.id)).filter(Boolean)) : onOpenAsset && onOpenAsset(a)}
              title="Peek at related asset"
            >
              <div className="related-thumb" style={{ backgroundImage: `url(${a.url})` }} />
              <div style={{ minWidth: 0, flex: 1 }}>
                <div className="related-kind-row">
                  <IcLink size={10} />
                  <select
                    className="related-kind-select"
                    value={rel.kind}
                    onClick={(e) => e.stopPropagation()}
                    onChange={(e) => { e.stopPropagation(); changeKind(rel.id, e.target.value); }}
                  >
                    {RELATIONSHIP_KINDS.map((k) => (
                      <option key={k.id} value={k.id}>{k.label}</option>
                    ))}
                  </select>
                </div>
                <div className="related-name">{a.name}</div>
                <div className="related-meta">Linked by {a.uploader} · {a.date}</div>
              </div>
            </button>
            <button
              className="related-remove"
              onClick={() => removeOne(rel.id)}
              title="Remove relationship"
              aria-label={`Remove relationship to ${a.name}`}
            >
              <IcClose size={13} />
            </button>
          </div>
        );
      })}

      <button
        className="btn btn-block"
        style={{ marginTop: 14, justifyContent: "center", borderStyle: "dashed", color: "var(--text-muted)" }}
        onClick={() => setPicking(true)}
      >
        <IcPlus size={13} /> Add related asset
      </button>

      {picking && (
        <AssetBrowserModal
          title="Link related assets"
          subtitle={`Browse, filter, multi-select. ${ASSETS.length.toLocaleString()} assets in this workspace.`}
          source={asset}
          excludeIds={linkedIds}
          relationshipKinds={RELATIONSHIP_KINDS}
          initialKind={pendingKind}
          onClose={() => setPicking(false)}
          onConfirm={(ids, kind) => addMany(ids, kind)}
          confirmLabel="Link as"
        />
      )}

      <div style={{ marginTop: 22, padding: "14px 16px", background: "var(--surface-soft)", borderRadius: 10, fontSize: 12, color: "var(--text-muted)", lineHeight: 1.5 }}>
        <strong style={{ color: "var(--text)", fontWeight: 600 }}>Tip.</strong> In the browser, toggle <strong style={{ color: "var(--text)", fontWeight: 600 }}>Find similar</strong> to pre-rank by visual similarity to this asset — useful when your library has thousands of items.
      </div>
    </div>
  );
}

function PreviewOverlay({ siblings, startIdx, sourceAsset, onClose, onOpenFull }) {
  const [idx, setIdx] = React.useState(Math.max(0, startIdx));
  const safeIdx = Math.max(0, Math.min(idx, siblings.length - 1));
  const current = siblings[safeIdx];

  // Keyboard navigation \u2014 arrows + esc, scoped while overlay is open.
  React.useEffect(() => {
    const onKey = (e) => {
      if (e.key === "Escape") { e.stopPropagation(); onClose(); }
      else if (e.key === "ArrowLeft" && safeIdx > 0) { e.stopPropagation(); setIdx((i) => i - 1); }
      else if (e.key === "ArrowRight" && safeIdx < siblings.length - 1) { e.stopPropagation(); setIdx((i) => i + 1); }
    };
    // Capture phase so we beat the parent PreviewPage's own arrow handlers.
    window.addEventListener("keydown", onKey, true);
    document.body.style.overflow = "hidden";
    return () => {
      window.removeEventListener("keydown", onKey, true);
      document.body.style.overflow = "";
    };
  }, [safeIdx, siblings.length, onClose]);

  if (!current) return null;

  const prev = () => safeIdx > 0 && setIdx((i) => i - 1);
  const next = () => safeIdx < siblings.length - 1 && setIdx((i) => i + 1);

  return (
    <div className="overlay-backdrop" onClick={onClose}>
      <header className="overlay-topbar" onClick={(e) => e.stopPropagation()}>
        <div className="overlay-crumbs">
          <span className="overlay-source-thumb" style={{ backgroundImage: `url(${sourceAsset.url})` }} />
          <span className="overlay-crumb-text">
            <span style={{ color: "var(--text-faint)" }}>Peeking from</span>
            <span className="overlay-source-name">{sourceAsset.name}</span>
          </span>
        </div>
        <div className="overlay-position">
          <button
            className="overlay-nav-btn"
            onClick={prev}
            disabled={safeIdx === 0}
            title="Previous related (\u2190)"
          >
            <IcChevL size={16} />
          </button>
          <span className="overlay-counter">
            Related <strong>{safeIdx + 1}</strong> of {siblings.length}
          </span>
          <button
            className="overlay-nav-btn"
            onClick={next}
            disabled={safeIdx === siblings.length - 1}
            title="Next related (\u2192)"
          >
            <IcChevR size={16} />
          </button>
        </div>
        <div className="overlay-actions">
          <button className="btn sm" onClick={() => onOpenFull(current)}>
            Open full preview
          </button>
          <button className="overlay-close" onClick={onClose} title="Close (Esc)">
            <IcClose size={16} />
          </button>
        </div>
      </header>

      <div className="overlay-stage" onClick={onClose}>
        <button
          className="overlay-side-nav left"
          onClick={(e) => { e.stopPropagation(); prev(); }}
          disabled={safeIdx === 0}
          title="Previous (\u2190)"
        >
          <IcChevL size={22} />
        </button>
        <img
          className="overlay-image"
          src={current.url}
          alt={current.name}
          onClick={(e) => e.stopPropagation()}
        />
        <button
          className="overlay-side-nav right"
          onClick={(e) => { e.stopPropagation(); next(); }}
          disabled={safeIdx === siblings.length - 1}
          title="Next (\u2192)"
        >
          <IcChevR size={22} />
        </button>
      </div>

      <div className="overlay-meta" onClick={(e) => e.stopPropagation()}>
        <div>
          <div className="overlay-asset-name">{current.name}</div>
          <div className="overlay-asset-sub">
            {current.w} × {current.h} · {current.size} · by {current.uploader}
          </div>
        </div>
        <div className="overlay-tags">
          {(current.tags || []).slice(0, 6).map((t) => (
            <span key={t} className="tag ai">
              <IcSparkles size={9} style={{ verticalAlign: "middle", marginRight: 3 }} />
              {t}
            </span>
          ))}
        </div>
      </div>

      <div className="overlay-filmstrip" onClick={(e) => e.stopPropagation()}>
        {siblings.map((s, i) => (
          <button
            key={s.id}
            className={`overlay-strip-thumb ${i === safeIdx ? "is-on" : ""}`}
            style={{ backgroundImage: `url(${s.url})` }}
            onClick={() => setIdx(i)}
            title={s.name}
          />
        ))}
      </div>
    </div>
  );
}

function captionFor(asset) {
  const captions = {
    a01: "Wide harbor scene at dawn, low fog over moored boats, soft cool color palette with muted grays and blues.",
    a02: "Studio headshot of a woman against a neutral gray backdrop, soft key light from camera left.",
    a03: "Flatlay of handmade ceramic bowls and cups arranged on a linen surface, natural overhead light.",
    a04: "Group of colleagues walking together outdoors, candid stride, golden-hour backlight.",
    a05: "Overhead view of a wooden desk with an open laptop, coffee mug and notebook.",
  };
  return captions[asset.id] || `Auto-generated description of ${asset.name.replace(/\.[^.]+$/, "")}. Subjects, colors and composition detected by Vanday's vision model.`;
}

window.PreviewPage = PreviewPage;
