/* =========================================================================
 *  CTWCAD — Access control (allowlist) and Admin page
 *  -----------------------------------------------------------------------
 *  Front-end gate ONLY. Real enforcement must happen server-side.
 *  Persists allowlist + public/private mode to localStorage.
 * =======================================================================*/

const OWNER_EMAIL = "sethbenricks@gmail.com";

const ALLOWLIST_KEY = "ctwcad.allowlist";
const PUBLISH_KEY   = "ctwcad.public";

const ROLES = [
  { id: "owner",  label: "Owner",       desc: "Full control. Cannot be removed.",   color: "owner"   },
  { id: "admin",  label: "Admin",       desc: "Manage members, projects, settings.", color: "admin"   },
  { id: "member", label: "Member",      desc: "Standard collaborator.",              color: "member"  },
  { id: "viewer", label: "Viewer",      desc: "Read-only access to shared content.", color: "viewer"  },
  { id: "guest",  label: "Guest",       desc: "Limited, time-bound access.",         color: "guest"   },
];

// Per-user storage limit defaults (in bytes).
// - Owner / admin: unlimited (null)
// - Everyone else: 100 MB
// An explicit `storageLimit` on the user doc overrides these defaults.
const DEFAULT_USER_LIMIT_BYTES = 100 * 1024 * 1024;
function effectiveUserLimit(user) {
  if (!user) return DEFAULT_USER_LIMIT_BYTES;
  if (user.storageLimit !== undefined && user.storageLimit !== null) {
    return user.storageLimit;
  }
  const isOwnerByEmail = (user.email || "").toLowerCase() === OWNER_EMAIL;
  const role = (user.role || "").toLowerCase();
  if (isOwnerByEmail || role === "owner" || role === "admin") return null; // unlimited
  return DEFAULT_USER_LIMIT_BYTES;
}
function fmtLimit(bytes) {
  if (bytes === null || bytes === undefined) return "Unlimited";
  return fmtBytes(bytes);
}
window.CTWCAD_LIMITS = { DEFAULT_USER_LIMIT_BYTES, effectiveUserLimit, fmtLimit };

const DEFAULT_ALLOWLIST = [
  { email: OWNER_EMAIL, role: "owner",  addedAt: new Date().toISOString() },
];

/* localStorage acts as a synchronous cache. In firebase mode we boot by
 * pulling /access/config from Firestore once on app load (see
 * bootstrapAccessConfig below) so that the synchronous gate (isAllowed)
 * still works in existing code paths. Writes go to Firestore AND localStorage
 * so the next page load is instantly correct.
 */
function loadAllowlist() {
  try {
    const raw = localStorage.getItem(ALLOWLIST_KEY);
    if (!raw) return DEFAULT_ALLOWLIST.slice();
    const arr = JSON.parse(raw);
    if (!arr.some(e => e.email.toLowerCase() === OWNER_EMAIL)) {
      arr.unshift(DEFAULT_ALLOWLIST[0]);
    }
    return arr;
  } catch { return DEFAULT_ALLOWLIST.slice(); }
}
function saveAllowlistLocal(list) {
  localStorage.setItem(ALLOWLIST_KEY, JSON.stringify(list));
}
function saveAllowlist(list) {
  saveAllowlistLocal(list);
  // In firebase mode, replace the WHOLE allowlist atomically — single
  // setDoc on /access/config. Avoids the read-modify-write race that
  // happened when we fanned out per-entry add/remove calls. Failures
  // surface as a toast via the ctwcad:save-error event.
  if (window.CTWCAD_ACTIVE_ADAPTER === "firebase" && window.api?.setAllowlist) {
    Promise.resolve(window.api.setAllowlist(list)).catch((e) => {
      console.error("[CTWCAD] setAllowlist write rejected:", e);
      window.dispatchEvent(new CustomEvent("ctwcad:save-error", {
        detail: { what: "allowlist", error: e?.code || e?.message || String(e) }
      }));
    });
  }
}
function loadPublic() {
  return localStorage.getItem(PUBLISH_KEY) === "1";
}
function savePublicLocal(v) {
  if (v) localStorage.setItem(PUBLISH_KEY, "1");
  else   localStorage.removeItem(PUBLISH_KEY);
}
function savePublic(v) {
  savePublicLocal(v);
  if (window.CTWCAD_ACTIVE_ADAPTER === "firebase" && window.api?.setPublic) {
    Promise.resolve(window.api.setPublic(!!v)).catch((e) => {
      console.error("[CTWCAD] setPublic write rejected:", e);
      window.dispatchEvent(new CustomEvent("ctwcad:save-error", {
        detail: { what: "public", error: e?.code || e?.message || String(e) }
      }));
    });
  }
}
function isAllowed(email) {
  if (loadPublic()) return true;            // open beta — anyone with a Google sign-in
  if (!email) return false;
  const list = loadAllowlist();
  return list.some(e => e.email.toLowerCase() === email.toLowerCase());
}
function isOwner(email) {
  return email && email.toLowerCase() === OWNER_EMAIL;
}
// Admin = owner OR a user whose role on the allowlist is "admin".
// Mirrors the rules' isAdmin() so the UI gates match what the server allows.
function isAdmin(email) {
  if (!email) return false;
  if (isOwner(email)) return true;
  const e = email.toLowerCase();
  return loadAllowlist().some(x => x.email.toLowerCase() === e && x.role === "admin");
}

/* On boot, if firebase is active AND the user is already signed in, pull
 * the canonical /access/config doc into the localStorage cache so the
 * synchronous gate gives the right answer. We skip this on the sign-in
 * screen because /access/config requires auth — the read would just
 * 401 and we'd waste a round-trip on every cold load. handleSignIn
 * itself populates the cache after a fresh sign-in. */
async function bootstrapAccessConfig() {
  if (window.CTWCAD_ACTIVE_ADAPTER !== "firebase" || !window.api?.getAccessConfig) return;
  try {
    const cfg = await window.api.getAccessConfig();
    if (!cfg) return;
    if (Array.isArray(cfg.allowlist)) saveAllowlistLocal(cfg.allowlist);
    savePublicLocal(!!cfg.public);
    window.dispatchEvent(new CustomEvent("ctwcad:access-config-loaded", { detail: cfg }));
  } catch (e) {
    console.warn("[CTWCAD] failed to load /access/config from Firestore:", e?.message);
  }
}
function maybeBootstrap() {
  if (localStorage.getItem("ctwcad.signedIn") === "1") {
    bootstrapAccessConfig();
  }
}
if (window.api) maybeBootstrap();
else window.addEventListener("DOMContentLoaded", maybeBootstrap);

window.CTWCAD_ACCESS = { OWNER_EMAIL, ROLES, loadAllowlist, saveAllowlist, loadPublic, savePublic, isAllowed, isOwner, isAdmin, bootstrapAccessConfig };

/* ---------- "Sign in with Google" picker (mock) -------------------- */
function GoogleAccountPicker({ onPick, onCancel }) {
  const M = window.FramerMotion.motion;
  const [custom, setCustom] = useState("");
  const accounts = [
    { name: "Seth Ricks",      email: OWNER_EMAIL,             hue: 200, initials: "SR" },
    { name: "Tess Carrow",     email: "tess@atelier-hw.com",   hue: 280, initials: "TC" },
    { name: "Dana Friend",     email: "dana@meridian.io",      hue: 174, initials: "DF" },
    { name: "Random visitor",  email: "stranger@example.com",  hue: 36,  initials: "?"  },
  ];
  return (
    <M.div className="ct-google-picker"
      initial={{ opacity: 0, scale: 0.96, y: 10 }}
      animate={{ opacity: 1, scale: 1, y: 0 }}
      exit={{ opacity: 0, scale: 0.97 }}
      transition={{ type: "spring", stiffness: 320, damping: 28 }}
    >
      <div className="ct-google-picker-head">
        <GoogleGlyph/>
        <div>
          <div className="ct-google-picker-title">Choose an account</div>
          <div className="ct-mono ct-dim" style={{fontSize:11}}>to continue to CTWCAD</div>
        </div>
        <button className="ct-iconbtn" onClick={onCancel}><Icon name="x" size={14}/></button>
      </div>
      <ul className="ct-google-picker-list">
        {accounts.map(a => (
          <li key={a.email}>
            <button className="ct-google-picker-item" onClick={() => onPick(a)}>
              <span className="ct-google-picker-avatar" style={{
                background: `linear-gradient(135deg, oklch(0.72 0.10 ${a.hue}), oklch(0.55 0.13 ${a.hue + 30}))`
              }}>{a.initials}</span>
              <span>
                <span className="ct-google-picker-name">{a.name}</span>
                <span className="ct-mono ct-dim">{a.email}</span>
              </span>
            </button>
          </li>
        ))}
      </ul>
      <div className="ct-google-picker-custom">
        <input
          value={custom}
          onChange={(e) => setCustom(e.target.value)}
          placeholder="or enter another email…"
          onKeyDown={(e) => {
            if (e.key === "Enter" && custom.includes("@")) {
              onPick({ name: custom.split("@")[0], email: custom.toLowerCase(), hue: 200, initials: custom[0]?.toUpperCase() || "?" });
            }
          }}
        />
        <button className="ct-btn ct-btn-primary" disabled={!custom.includes("@")}
          onClick={() => onPick({ name: custom.split("@")[0], email: custom.toLowerCase(), hue: 200, initials: custom[0]?.toUpperCase() || "?" })}>
          Use
        </button>
      </div>
      <div className="ct-google-picker-foot ct-dim">Mock OAuth — see notes for real Google Sign-In.</div>
    </M.div>
  );
}

/* ---------- Access denied screen ----------------------------------- */
function AccessDeniedView({ email, onBack }) {
  const M = window.FramerMotion.motion;
  return (
    <div className="ct-signin">
      <div className="ct-signin-bg" aria-hidden="true">
        <svg viewBox="0 0 800 800" preserveAspectRatio="xMidYMid slice">
          <defs>
            <pattern id="bg-grid-d" width="40" height="40" patternUnits="userSpaceOnUse">
              <path d="M 40 0 L 0 0 0 40" fill="none" stroke="currentColor" strokeWidth="0.4" opacity="0.18"/>
            </pattern>
          </defs>
          <rect width="800" height="800" fill="url(#bg-grid-d)"/>
        </svg>
      </div>
      <M.div className="ct-signin-card ct-denied-card"
        initial={{ opacity: 0, y: 20, scale: 0.97 }}
        animate={{ opacity: 1, y: 0, scale: 1 }}
        transition={{ type: "spring", stiffness: 240, damping: 28 }}>
        <div className="ct-denied-icon"><Icon name="shield-alert" size={26}/></div>
        <h1>You don't have access yet</h1>
        <p className="ct-dim">CTWCAD is in private beta. Access is invite-only while we test with a small group.</p>
        <div className="ct-denied-email">
          <Icon name="mail" size={13}/>
          <span className="ct-mono">{email}</span>
        </div>
        <p className="ct-dim" style={{fontSize:13, marginTop:6}}>
          If you think you should have access, ask the owner to add this address to the allowlist.
        </p>
        <button className="ct-btn ct-btn-ghost" onClick={onBack} style={{marginTop:14}}>
          <Icon name="arrow-left" size={13}/> Use a different account
        </button>
      </M.div>
    </div>
  );
}

/* ---------- Confirm dialog (publish to everyone) ------------------- */
function ConfirmDialog({ open, title, body, confirmLabel, danger, onConfirm, onCancel }) {
  const M = window.FramerMotion.motion;
  const A = window.FramerMotion.AnimatePresence;
  return (
    <A>
      {open && (
        <>
          <M.div className="ct-backdrop"
            initial={{ opacity: 0 }} animate={{ opacity: 1 }} exit={{ opacity: 0 }}
            onClick={onCancel}/>
          <M.div className="ct-confirm"
            initial={{ opacity: 0, y: 16, scale: 0.96 }}
            animate={{ opacity: 1, y: 0, scale: 1 }}
            exit={{ opacity: 0, scale: 0.97 }}
            transition={{ type: "spring", stiffness: 320, damping: 26 }}>
            <h3>{title}</h3>
            <p className="ct-dim">{body}</p>
            <div className="ct-confirm-actions">
              <button className="ct-btn ct-btn-ghost" onClick={onCancel}>Cancel</button>
              <button
                className={"ct-btn " + (danger ? "ct-btn-danger" : "ct-btn-primary")}
                onClick={onConfirm}>
                {confirmLabel}
              </button>
            </div>
          </M.div>
        </>
      )}
    </A>
  );
}

/* ---------- Role picker (popover) ---------------------------------- */
function RolePopover({ open, current, onPick, onClose, anchorRect }) {
  const M = window.FramerMotion.motion;
  const A = window.FramerMotion.AnimatePresence;
  const ref = useRef(null);
  useEffect(() => {
    if (!open) return;
    const onDoc = (e) => { if (ref.current && !ref.current.contains(e.target)) onClose(); };
    document.addEventListener("mousedown", onDoc);
    return () => document.removeEventListener("mousedown", onDoc);
  }, [open, onClose]);
  if (!open || !anchorRect) return null;
  const style = {
    position: "fixed",
    top: anchorRect.bottom + 6,
    left: Math.min(anchorRect.left, window.innerWidth - 280),
  };
  return (
    <A>
      <M.div
        ref={ref}
        className="ct-role-popover"
        style={style}
        initial={{ opacity: 0, y: -6, scale: 0.97 }}
        animate={{ opacity: 1, y: 0, scale: 1 }}
        exit={{ opacity: 0, y: -4 }}
        transition={{ type: "spring", stiffness: 380, damping: 28 }}
      >
        {ROLES.filter(r => r.id !== "owner").map(r => (
          <button
            key={r.id}
            className={"ct-role-item " + (current === r.id ? "is-active" : "")}
            onClick={() => onPick(r.id)}
          >
            <span className={"ct-pill ct-pill-" + r.color}>{r.label}</span>
            <span className="ct-dim" style={{fontSize:11.5}}>{r.desc}</span>
            {current === r.id && <Icon name="check" size={13}/>}
          </button>
        ))}
      </M.div>
    </A>
  );
}

/* ---------- Admin page (admins + owner) ----------------------------
   Wave-4 layout: tabbed dashboard. The first tab "Overview" surfaces a
   counts/stats card and a top-storage chart; subsequent tabs each focus
   on one operational concern (access, files, activity, admin staff).
   Tabs are CLIENT-SIDE only — every fetch they fire is guarded by the
   existing Firestore rules, so non-owners just see permission-denied if
   they somehow reach this page. */
function AdminView({ user, onToast, viewAll, onToggleViewAll, onImpersonate, impersonating, onOpenFile }) {
  const [list, setList] = useState(loadAllowlist);
  const [pub, setPub] = useState(loadPublic);
  const [email, setEmail] = useState("");
  const [role, setRole] = useState("member");
  const [confirmPublish, setConfirmPublish] = useState(false);
  const [confirmRevoke, setConfirmRevoke] = useState(false);
  const [rolePop, setRolePop] = useState(null); // { email, anchor }
  const [allUsers, setAllUsers] = useState(null);
  const [allKeys, setAllKeys] = useState(null);  // connected devices across users
  const [userMenu, setUserMenu] = useState(null); // { user, x, y } | null
  const [limitModal, setLimitModal] = useState(null); // { user } | null
  const [guestExpModal, setGuestExpModal] = useState(null); // { email } | null
  // Wave-4: top-level tab. Persisted to localStorage so refresh / navigation
  // back to /admin keeps the operator on whichever tab they were last using.
  const [tab, setTab] = useState(() => {
    const t = localStorage.getItem("ctwcad.admin.tab");
    return ["overview", "access", "files", "activity", "staff"].includes(t) ? t : "overview";
  });
  useEffect(() => { localStorage.setItem("ctwcad.admin.tab", tab); }, [tab]);
  // Wave-4: allowlist toolbar state (search + sort).
  const [memberSearch, setMemberSearch] = useState("");
  const [memberSort, setMemberSort] = useState("addedAt-desc"); // email-asc | role | addedAt-desc | addedAt-asc
  const M = window.FramerMotion.motion;

  const isOwnerUser = isOwner(user?.email);
  const isAdminUser = isAdmin(user?.email);

  // Load full member list (with file/project/storage stats) so the cards can
  // show meaningful numbers next to each name. Errors used to swallow
  // silently and leave the section showing nothing forever — now we
  // capture the failure and surface it in the empty-state UI.
  const [usersError, setUsersError] = useState(null);
  useEffect(() => {
    if (!isAdminUser) return;
    let alive = true;
    setUsersError(null);
    window.api.listAllUsers?.().then(u => {
      if (!alive) return;
      setAllUsers(u || []);
    }).catch((err) => {
      if (!alive) return;
      console.warn("[CTWCAD] listAllUsers failed:", err?.code || err?.message, err);
      setUsersError(err?.code || err?.message || "unknown error");
      setAllUsers([]);
    });
    window.api.listAllApiKeys?.().then(k => { if (alive) setAllKeys(k); })
      .catch((err) => console.warn("[CTWCAD] listAllApiKeys failed:", err?.code || err?.message));
    return () => { alive = false; };
  }, [isAdminUser, impersonating?.id, viewAll]);

  const add = () => {
    const e = email.trim().toLowerCase();
    if (!e.includes("@")) { onToast?.("Enter a valid email", "alert-circle"); return; }
    if (list.some(x => x.email.toLowerCase() === e)) { onToast?.("Already on the list", "info"); return; }
    const next = [...list, { email: e, role, addedAt: new Date().toISOString() }];
    setList(next); saveAllowlist(next); setEmail("");
    onToast?.(`Added ${e} as ${role}`, "user-plus");
  };
  const remove = (e) => {
    if (e.toLowerCase() === OWNER_EMAIL) { onToast?.("Can't remove the owner", "shield"); return; }
    const next = list.filter(x => x.email.toLowerCase() !== e.toLowerCase());
    setList(next); saveAllowlist(next);
    onToast?.(`Removed ${e}`, "user-minus");
  };
  const changeRole = (e, newRole) => {
    if (e.toLowerCase() === OWNER_EMAIL) return;
    if (newRole === "guest") {
      // Guests need an expiration; close the role popover and open the
      // expiration picker. The actual write happens after the picker
      // saves (see saveGuestExpiration below).
      setRolePop(null);
      setGuestExpModal({ email: e });
      return;
    }
    const next = list.map(x => x.email.toLowerCase() === e.toLowerCase()
      // Clear expiresAt when role changes away from guest.
      ? { ...x, role: newRole, expiresAt: null }
      : x);
    setList(next); saveAllowlist(next);
    setRolePop(null);
    onToast?.(`${e} → ${newRole}`, "shield-check");
  };

  const saveGuestExpiration = (email, expiresAtIso) => {
    const next = list.map(x => x.email.toLowerCase() === email.toLowerCase()
      ? { ...x, role: "guest", expiresAt: expiresAtIso || null }
      : x);
    setList(next); saveAllowlist(next);
    setGuestExpModal(null);
    const friendly = expiresAtIso
      ? `expires ${fmtRelative(expiresAtIso)}`
      : "no expiration";
    onToast?.(`${email} → guest (${friendly})`, "clock");
  };

  const doPublish = () => {
    setPub(true); savePublic(true); setConfirmPublish(false);
    onToast?.("Site is now public — anyone with Google can sign in", "globe");
  };
  const doRevoke = () => {
    setPub(false); savePublic(false); setConfirmRevoke(false);
    onToast?.("Site is private again — allowlist enforced", "lock");
  };

  if (!isAdminUser) {
    return (
      <div className="ct-page">
        <div className="ct-empty">
          <Icon name="shield" size={36}/>
          <div className="ct-empty-title">Admin only</div>
          <div className="ct-dim">This area is restricted to admins and the workspace owner.</div>
        </div>
      </div>
    );
  }

  // Tab metadata. The header text + sub-line update with the active tab so
  // the page makes sense even if the operator deep-links to a tab.
  const TAB_META = {
    overview: { title: "Admin overview",   sub: "System-wide totals and storage." },
    access:   { title: "Access control",   sub: pub
                  ? "Site is public. Anyone signing in with Google can use CTWCAD."
                  : "Only people on the allowlist can sign in. Everyone else sees the beta gate." },
    files:    { title: "All files",        sub: "Every file in the workspace, newest activity first." },
    activity: { title: "Activity log",     sub: "Audit trail of file changes across all users." },
    staff:    { title: "Admin staff",      sub: "Who else has admin powers besides the owner." },
  };
  const meta = TAB_META[tab] || TAB_META.overview;

  return (
    <div className="ct-page">
      <div className="ct-page-head">
        <div>
          <div className="ct-eyebrow ct-mono">ADMIN · {isOwnerUser ? "OWNER" : "STAFF"}</div>
          <h1>{meta.title}</h1>
          <p className="ct-dim">{meta.sub}</p>
        </div>
        <div className={"ct-admin-status " + (pub ? "is-public" : "is-private")}>
          <span className="ct-mono">{pub ? "PUBLIC" : "PRIVATE BETA"}</span>
          <span className={"ct-status-dot " + (pub ? "is-warn" : "is-on")}/>
        </div>
      </div>

      {/* TAB NAVIGATION */}
      <nav className="ct-admin-tabs" role="tablist" aria-label="Admin sections">
        {[
          { id: "overview", label: "Overview", icon: "layout-dashboard" },
          { id: "access",   label: "Access",   icon: "shield",     count: list.length },
          { id: "files",    label: "All files", icon: "file" },
          { id: "activity", label: "Activity", icon: "activity" },
          { id: "staff",    label: "Admins",   icon: "user-cog" },
        ].map(t => (
          <button
            key={t.id}
            role="tab"
            aria-selected={tab === t.id}
            className={"ct-admin-tab " + (tab === t.id ? "is-active" : "")}
            onClick={() => setTab(t.id)}>
            <Icon name={t.icon} size={13}/>
            <span>{t.label}</span>
            {t.count !== undefined && <span className="ct-admin-tab-count">{t.count}</span>}
          </button>
        ))}
      </nav>

      {tab === "overview" && (
        <AdminOverviewView allUsers={allUsers} onToast={onToast}/>
      )}

      {tab === "files" && (
        <AdminAllFilesView allUsers={allUsers} onToast={onToast} onOpenFile={onOpenFile}/>
      )}

      {tab === "activity" && (
        <AdminActivityView allUsers={allUsers} onToast={onToast} onOpenFile={onOpenFile}/>
      )}

      {tab === "staff" && (
        <AdminStaffView
          list={list}
          isOwnerUser={isOwnerUser}
          onSave={(next) => { setList(next); saveAllowlist(next); }}
          onToast={onToast}
        />
      )}

      {tab === "access" && <>
      {/* PUBLISH MODE */}
      <section className="ct-section">
        <h2>Visibility</h2>
        <div className={"ct-publish-card " + (pub ? "is-public" : "")}>
          <div className="ct-publish-icon">
            <Icon name={pub ? "globe" : "lock"} size={22}/>
          </div>
          <div className="ct-publish-body">
            <div className="ct-publish-title">
              {pub ? "Public — open to everyone" : "Private beta — invite only"}
            </div>
            <div className="ct-dim">
              {pub
                ? "Allowlist is bypassed. Anyone with a Google account can sign in. The allowlist below is preserved and will become active again if you make the site private."
                : "The allowlist below is the only way in. Use Publish to lift the gate when you're ready to launch."}
            </div>
          </div>
          {pub
            ? <button className="ct-btn ct-btn-ghost" onClick={() => setConfirmRevoke(true)}>
                <Icon name="lock" size={13}/>Make private again
              </button>
            : <button className="ct-btn ct-btn-primary ct-btn-publish" onClick={() => setConfirmPublish(true)}>
                <Icon name="globe" size={13}/>Publish to everyone
              </button>}
        </div>
      </section>

      {/* ADD MEMBER */}
      <section className="ct-section">
        <h2>Add member</h2>
        <div className="ct-admin-add">
          <Icon name="mail" size={14}/>
          <input
            value={email}
            onChange={(e) => setEmail(e.target.value)}
            placeholder="person@company.com"
            onKeyDown={(e) => e.key === "Enter" && add()}
          />
          <select value={role} onChange={(e) => setRole(e.target.value)} className="ct-select">
            {ROLES.filter(r => r.id !== "owner").map(r => (
              <option key={r.id} value={r.id}>{r.label}</option>
            ))}
          </select>
          <button className="ct-btn ct-btn-primary" onClick={add}>
            <Icon name="user-plus" size={13}/>Add
          </button>
        </div>
        {/* Live validation: warn early on bad email shape and on duplicates so
            the operator doesn't have to click Add to see the toast. Empty-state
            stays silent. */}
        {(() => {
          const trimmed = email.trim();
          if (!trimmed) return null;
          const looksLikeEmail = /^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(trimmed);
          if (!looksLikeEmail) {
            return (
              <div className="ct-admin-add-hint is-error">
                <Icon name="alert-circle" size={12}/>
                <span>That doesn't look like a valid email.</span>
              </div>
            );
          }
          const dup = list.find(x => x.email.toLowerCase() === trimmed.toLowerCase());
          if (dup) {
            return (
              <div className="ct-admin-add-hint is-warn">
                <Icon name="info" size={12}/>
                <span>{dup.email} is already on the list as <strong>{dup.role}</strong>.</span>
              </div>
            );
          }
          return (
            <div className="ct-admin-add-hint is-ok">
              <Icon name="check-circle" size={12}/>
              <span>Looks good — press Enter or click Add.</span>
            </div>
          );
        })()}
        {pub && (
          <div className="ct-publish-note ct-dim">
            <Icon name="info" size={12}/>
            <span>Site is public. The allowlist is preserved but not enforced — adding members here defines their role for when you switch back to private.</span>
          </div>
        )}
      </section>

      {/* MEMBER LIST */}
      <section className="ct-section">
        <h2>Members ({list.length})</h2>
        {/* Toolbar: search by email + sort. Owner row stays pinned to the top
            irrespective of sort so the workspace owner is always discoverable. */}
        <div className="ct-admin-toolbar">
          <div className="ct-admin-toolbar-search">
            <Icon name="search" size={13}/>
            <input
              value={memberSearch}
              onChange={(e) => setMemberSearch(e.target.value)}
              placeholder="Search members…"/>
            {memberSearch && (
              <button className="ct-iconbtn" title="Clear" onClick={() => setMemberSearch("")}>
                <Icon name="x" size={12}/>
              </button>
            )}
          </div>
          <select className="ct-select" value={memberSort}
                  onChange={(e) => setMemberSort(e.target.value)}>
            <option value="addedAt-desc">Newest first</option>
            <option value="addedAt-asc">Oldest first</option>
            <option value="email-asc">Email A–Z</option>
            <option value="role">By role</option>
          </select>
          <span className="ct-admin-toolbar-summary">
            {(() => {
              const q = memberSearch.trim().toLowerCase();
              const visible = q ? list.filter(x => x.email.toLowerCase().includes(q)) : list;
              return q ? `${visible.length} of ${list.length} match` : `${list.length} total`;
            })()}
          </span>
        </div>
        <div className="ct-admin-list">
          {(() => {
            const q = memberSearch.trim().toLowerCase();
            const filtered = q ? list.filter(x => x.email.toLowerCase().includes(q)) : list.slice();
            const ROLE_RANK = { owner: 0, admin: 1, member: 2, viewer: 3, guest: 4 };
            filtered.sort((a, b) => {
              // Owner always first.
              const aOwn = a.email.toLowerCase() === OWNER_EMAIL ? 0 : 1;
              const bOwn = b.email.toLowerCase() === OWNER_EMAIL ? 0 : 1;
              if (aOwn !== bOwn) return aOwn - bOwn;
              switch (memberSort) {
                case "email-asc":
                  return a.email.localeCompare(b.email);
                case "role":
                  return (ROLE_RANK[a.role] ?? 9) - (ROLE_RANK[b.role] ?? 9);
                case "addedAt-asc":
                  return new Date(a.addedAt || 0) - new Date(b.addedAt || 0);
                case "addedAt-desc":
                default:
                  return new Date(b.addedAt || 0) - new Date(a.addedAt || 0);
              }
            });
            return filtered;
          })().map((entry, i) => {
            const isOwn = entry.email.toLowerCase() === OWNER_EMAIL;
            const roleDef = ROLES.find(r => r.id === entry.role) || ROLES[2];
            // Match this allowlist entry to a real /users doc so we can
            // show their actual usage and pipe the storage modal through
            // their uid. Members who haven't signed in yet have no
            // /users doc → show "—" and disable the storage actions.
            const userDoc = (allUsers || []).find(u =>
              (u.email || "").toLowerCase() === entry.email.toLowerCase());
            const used = userDoc?.sizeBytes || 0;
            const limit = userDoc
              ? effectiveUserLimit(userDoc)
              : effectiveUserLimit({ email: entry.email, role: entry.role });
            const overLimit = limit !== null && used > limit;
            const canEditStorage = !!userDoc && (!isOwn || isOwnerUser);
            const openStorageModal = () => {
              if (!canEditStorage) return;
              setLimitModal({ user: userDoc });
            };
            return (
              <M.div key={entry.email}
                className="ct-admin-row"
                initial={{ opacity: 0, x: -6 }} animate={{ opacity: 1, x: 0 }}
                transition={{ delay: i * 0.03, duration: 0.25 }}
                onContextMenu={(e) => {
                  if (!canEditStorage) return;
                  e.preventDefault();
                  e.stopPropagation();
                  openStorageModal();
                }}
                title={canEditStorage ? "Right-click to edit storage limit" : ""}>
                <Avatar user={{ initials: entry.email[0].toUpperCase(), avatarHue: hashHue(entry.email) }} size={28}/>
                <div className="ct-admin-row-id">
                  <div className="ct-mono">{entry.email}</div>
                  <div className="ct-dim" style={{fontSize:11}}>
                    Added {fmtRelative(entry.addedAt)}
                    {entry.role === "guest" && entry.expiresAt && (() => {
                      const exp = new Date(entry.expiresAt).getTime();
                      const now = Date.now();
                      const expired = exp < now;
                      const soon = !expired && (exp - now) < 24 * 3600 * 1000;
                      const cls = "ct-admin-row-expires"
                        + (expired ? " is-expired" : soon ? " is-soon" : "");
                      return (
                        <>
                          {" · "}
                          <span className={cls}>
                            {expired
                              ? `expired ${fmtRelative(entry.expiresAt)}`
                              : `expires ${fmtRelative(entry.expiresAt)}`}
                          </span>
                        </>
                      );
                    })()}
                    {entry.role === "guest" && !entry.expiresAt && (
                      <>{" · "}<span className="ct-admin-row-expires">no expiration</span></>
                    )}
                  </div>
                </div>
                <button
                  className="ct-admin-row-storage"
                  onClick={(e) => { e.stopPropagation(); openStorageModal(); }}
                  disabled={!canEditStorage}
                  title={!userDoc
                    ? "User hasn't signed in yet"
                    : (canEditStorage ? "Edit storage limit" : "Owner limit can only be changed by the owner")}>
                  <span className="ct-mono ct-admin-row-storage-num"
                        style={{color: overLimit ? "var(--ct-danger)" : undefined}}>
                    {userDoc ? fmtBytes(used) : "—"}
                  </span>
                  <span className="ct-dim ct-mono">/</span>
                  <span className="ct-dim ct-mono">{fmtLimit(limit)}</span>
                  {canEditStorage && <Icon name="edit-3" size={10}/>}
                </button>
                <button
                  className={"ct-pill ct-pill-" + roleDef.color + (isOwn ? " is-locked" : "")}
                  disabled={isOwn}
                  title={isOwn ? "Owner role can't be changed" : "Change role"}
                  onClick={(e) => {
                    if (isOwn) return;
                    const rect = e.currentTarget.getBoundingClientRect();
                    setRolePop({ email: entry.email, anchor: rect });
                  }}>
                  {roleDef.label}
                  {!isOwn && <Icon name="chevron-down" size={11} style={{marginLeft:4}}/>}
                </button>
                <button
                  className="ct-iconbtn"
                  disabled={isOwn}
                  onClick={() => remove(entry.email)}
                  title={isOwn ? "Owner can't be removed" : "Remove"}>
                  <Icon name="trash-2" size={14}/>
                </button>
              </M.div>
            );
          })}
          {memberSearch.trim() && list.filter(x =>
              x.email.toLowerCase().includes(memberSearch.trim().toLowerCase())).length === 0 && (
            <div className="ct-admin-empty ct-mono">
              <Icon name="search" size={14}/>
              <span>No members match "{memberSearch.trim()}".</span>
            </div>
          )}
        </div>
      </section>

      {/* USERS & IMPERSONATION */}
      <section className="ct-section">
        <div className="ct-admin-users-head">
          <h2>Users &amp; impersonation</h2>
          <label className={"ct-godview-toggle " + (viewAll ? "is-on" : "")} title="See files from every user, regardless of owner">
            <input type="checkbox" checked={!!viewAll} onChange={(e) => onToggleViewAll?.(e.target.checked)} />
            <span className="ct-godview-track"><span className="ct-godview-thumb"/></span>
            <span className="ct-godview-label">
              <Icon name={viewAll ? "eye" : "eye-off"} size={12}/>
              <strong>Admin view</strong>
              <span className="ct-dim ct-mono">{viewAll ? "ON" : "OFF"}</span>
            </span>
          </label>
        </div>
        <p className="ct-dim ct-admin-users-sub">
          See every member, browse their files in read-only mode, or sign in as them to verify what they see.
          Impersonation is logged server-side—every action while impersonating is recorded against the admin's account.
        </p>
        {usersError && (
          <div className="ct-admin-empty ct-mono">
            <Icon name="alert-triangle" size={14}/>
            <span>Couldn't load users ({usersError}). Open DevTools → Console
              for details. Often a cached/stale page — try hard-refresh.</span>
          </div>
        )}
        {!usersError && allUsers && allUsers.length === 0 && (
          <div className="ct-admin-empty ct-mono">
            <Icon name="info" size={14}/>
            <span>No users on file yet. New members appear here the first
              time they sign in (a /users/&lt;uid&gt; doc gets written
              then). Make sure people on the allowlist have actually
              completed the Google sign-in flow at least once.</span>
          </div>
        )}
        <div className="ct-admin-users">
          {(allUsers || Array.from({length: 4})).map((u, i) => {
            const skeleton = !u;
            const isMe = u && user?.email && u.email.toLowerCase() === user.email.toLowerCase();
            const isCurrent = u && impersonating && impersonating.email.toLowerCase() === u.email.toLowerCase();
            const isTargetOwner = u && u.email.toLowerCase() === OWNER_EMAIL;
            // Non-owner admins can't impersonate or edit the owner.
            const blockOwnerOps = isTargetOwner && !isOwnerUser;
            const allowEntry = u && list.find(x => x.email.toLowerCase() === u.email.toLowerCase());
            const r = allowEntry?.role || (isTargetOwner ? "owner" : "member");
            const roleDef = ROLES.find(x => x.id === r) || ROLES[2];
            const userKeys = u && allKeys ? allKeys.filter(k => k.uid === u.id) : null;
            return (
              <M.div key={u?.id || i}
                className={"ct-user-card " + (isCurrent ? "is-current" : "") + (skeleton ? " ct-skeleton" : "")}
                initial={{ opacity: 0, y: 6 }} animate={{ opacity: 1, y: 0 }}
                transition={{ delay: i * 0.04, duration: 0.28 }}
                onContextMenu={(e) => {
                  if (skeleton || isMe || blockOwnerOps) return;
                  e.preventDefault();
                  setUserMenu({ user: u, x: e.clientX, y: e.clientY });
                }}>
                {!skeleton && (() => {
                  const limit = effectiveUserLimit(u);
                  const used = u.sizeBytes || 0;
                  const pct = limit ? Math.min(100, (used / limit) * 100)
                    : Math.min(100, (used / (5 * 1024 * 1024 * 1024)) * 100); // 5 GB for visual scale only
                  const overLimit = limit !== null && used > limit;
                  return <>
                  {/* Storage headline — biggest visual hit on the card.
                      Right-click anywhere in here opens the limit modal,
                      so admins can manage storage without hunting for the
                      Edit-limit button. */}
                  <div className="ct-user-storage"
                       onContextMenu={(e) => {
                         if (blockOwnerOps) return;
                         e.preventDefault();
                         e.stopPropagation();
                         setLimitModal({ user: u });
                       }}
                       title={blockOwnerOps ? "" : "Right-click to edit storage limit"}>
                    <div className="ct-user-storage-headline">
                      <span className="ct-user-storage-num"
                            style={{color: overLimit ? "var(--ct-danger)" : undefined}}>
                        {fmtBytes(used)}
                      </span>
                      <span className="ct-user-storage-divider ct-dim">/</span>
                      <span className="ct-user-storage-cap ct-dim">{fmtLimit(limit)}</span>
                    </div>
                    <div className="ct-user-storage-bar" aria-hidden="true">
                      <div className={"ct-user-storage-fill" + (overLimit ? " is-over" : "")}
                           style={{ width: pct + "%" }}/>
                    </div>
                    <div className="ct-user-storage-label ct-dim">
                      {overLimit
                        ? `over by ${fmtBytes(used - limit)}`
                        : (limit === null
                            ? "storage used (no cap)"
                            : `${Math.round(pct)}% of cap used`)}
                    </div>
                    <button
                      className="ct-storage-edit-btn"
                      title={blockOwnerOps ? "Owner limit can only be changed by the owner" : "Edit storage limit"}
                      disabled={blockOwnerOps}
                      onClick={(e) => { e.stopPropagation(); setLimitModal({ user: u }); }}>
                      <Icon name="edit-3" size={10}/>Edit limit
                    </button>
                  </div>
                  <div className="ct-user-card-id">
                    <Avatar user={{ initials: u.initials, avatarHue: u.avatarHue }} size={36}/>
                    <div className="ct-user-card-name">
                      <div>
                        <strong>{u.name}</strong>
                        {isMe && <span className="ct-pill ct-pill-owner" style={{marginLeft:8}}>You</span>}
                        {isCurrent && <span className="ct-pill ct-pill-admin" style={{marginLeft:8}}>Viewing now</span>}
                      </div>
                      <div className="ct-mono ct-dim">{u.email}</div>
                    </div>
                    <span className={"ct-pill ct-pill-" + roleDef.color}>{roleDef.label}</span>
                  </div>
                  <div className="ct-user-card-stats">
                    <div><span className="ct-mono">{u.fileCount}</span><span className="ct-dim">files</span></div>
                    <div><span className="ct-mono">{u.projectCount}</span><span className="ct-dim">projects</span></div>
                    <div>
                      <span className="ct-mono">{userKeys ? userKeys.length : "·"}</span>
                      <span className="ct-dim">connected</span>
                    </div>
                  </div>
                  <div className="ct-user-card-actions">
                    <button
                      className="ct-btn ct-btn-ghost"
                      disabled={isMe || blockOwnerOps}
                      onClick={() => {
                        if (isMe || blockOwnerOps) return;
                        onToggleViewAll?.(true);
                        onToast?.(`Admin view on — ${u.name}'s files now visible`, "eye");
                      }}
                      title={blockOwnerOps ? "Owner files require owner access" : "View their files (admin view)"}>
                      <Icon name="folder-search" size={13}/>View files
                    </button>
                    <button
                      className="ct-btn ct-btn-primary"
                      disabled={isMe || blockOwnerOps}
                      onClick={() => onImpersonate?.(u)}
                      title={blockOwnerOps ? "Owner can't be impersonated by admins" : "Sign in as them — see exactly what they see"}>
                      <Icon name="user-check" size={13}/>Sign in as
                    </button>
                    <button
                      className="ct-btn ct-btn-ghost ct-user-card-more"
                      disabled={isMe || blockOwnerOps}
                      onClick={(e) => {
                        const r = e.currentTarget.getBoundingClientRect();
                        setUserMenu({ user: u, x: r.right - 8, y: r.bottom + 4 });
                      }}
                      title="More actions">
                      <Icon name="more-horizontal" size={14}/>
                    </button>
                  </div>
                </>;
                })()}
              </M.div>
            );
          })}
        </div>
      </section>

      {/* CONNECTED DEVICES (all users) */}
      <section className="ct-section">
        <div className="ct-admin-users-head">
          <h2>Connected desktop apps {allKeys ? `(${allKeys.length})` : ""}</h2>
        </div>
        <p className="ct-dim ct-admin-users-sub">
          Every CTWCAD desktop install authorized for any user. Revoking signs the
          desktop app out on its next sync.
        </p>
        <div className="ct-admin-list">
          {(allKeys || []).map((k, i) => {
            const targetIsOwner = (k.userEmail || "").toLowerCase() === OWNER_EMAIL;
            const blockOwnerOps = targetIsOwner && !isOwnerUser;
            return (
              <M.div key={k.uid + ":" + k.id}
                className="ct-admin-row"
                initial={{ opacity: 0, x: -6 }} animate={{ opacity: 1, x: 0 }}
                transition={{ delay: i * 0.03, duration: 0.25 }}>
                <Icon name="monitor" size={18}/>
                <div className="ct-admin-row-id">
                  <div className="ct-mono">
                    {k.deviceName} <span className="ct-dim">· {k.userEmail || k.uid}</span>
                  </div>
                  <div className="ct-dim" style={{fontSize:11}}>
                    {k.os || "—"} · {k.appVersion ? `v${k.appVersion}` : "version unknown"} ·
                    {k.lastUsedAt ? ` last used ${fmtRelative(k.lastUsedAt)}` : " never used"} ·
                    added {fmtRelative(k.createdAt)}
                  </div>
                </div>
                <button
                  className="ct-btn ct-btn-ghost"
                  disabled={blockOwnerOps}
                  title={blockOwnerOps ? "Only the owner can revoke the owner's devices" : "Revoke this connection"}
                  onClick={async () => {
                    try {
                      await window.api.revokeApiKey(k.uid, k.id);
                      setAllKeys(prev => prev.filter(x => !(x.uid === k.uid && x.id === k.id)));
                      onToast?.(`Revoked ${k.deviceName}`, "x-circle");
                    } catch (e) {
                      onToast?.(`Couldn't revoke (${e?.code || e?.message || "error"})`, "alert-circle");
                    }
                  }}>
                  <Icon name="x" size={13}/>Revoke
                </button>
              </M.div>
            );
          })}
          {allKeys && allKeys.length === 0 && (
            <div className="ct-dim ct-mono" style={{padding:14}}>No connected desktop apps yet.</div>
          )}
        </div>
      </section>

      {/* HOW IT WORKS */}
      <section className="ct-section">
        <h2>How this works</h2>
        <div className="ct-admin-info">
          <div className="ct-admin-info-row">
            <Icon name="lock" size={14}/>
            <div>
              <div><strong>Front-end gate.</strong> Anyone reaching the app sees the sign-in prompt only.</div>
              <div className="ct-dim">No content, dashboard, or files are loaded until access is verified.</div>
            </div>
          </div>
          <div className="ct-admin-info-row">
            <Icon name="check-circle" size={14}/>
            <div>
              <div><strong>Allowlist or open mode.</strong> Switch between invite-only and fully public from this page.</div>
              <div className="ct-dim">Mismatched emails see a friendly "in beta" message while the gate is up.</div>
            </div>
          </div>
          <div className="ct-admin-info-row">
            <Icon name="users" size={14}/>
            <div>
              <div><strong>Roles.</strong> Owner, Admin, Member, Viewer, Guest. Click any pill to change a member's role.</div>
              <div className="ct-dim">Roles drive UI capability flags client-side; the WordPress bridge enforces them server-side.</div>
            </div>
          </div>
          <div className="ct-admin-info-row">
            <Icon name="shield-check" size={14}/>
            <div>
              <div><strong>Real enforcement is server-side.</strong> The same allowlist + visibility flag must live on the WordPress bridge plugin.</div>
              <div className="ct-dim">It rejects API calls from non-allowlisted accounts when private; the front end is just UX.</div>
            </div>
          </div>
        </div>
      </section>
      </>}

      <ConfirmDialog
        open={confirmPublish}
        title="Publish CTWCAD to everyone?"
        body="The allowlist will be bypassed. Anyone who signs in with Google will be able to access the app. You can switch back to private anytime — the allowlist is kept."
        confirmLabel="Publish to everyone"
        onCancel={() => setConfirmPublish(false)}
        onConfirm={doPublish}
      />
      <ConfirmDialog
        open={confirmRevoke}
        title="Make CTWCAD private again?"
        body="The allowlist will be enforced again. New users that aren't on the list will see the beta gate. Existing sessions for non-allowlisted users will be signed out on next reload."
        confirmLabel="Make private"
        danger
        onCancel={() => setConfirmRevoke(false)}
        onConfirm={doRevoke}
      />
      <RolePopover
        open={!!rolePop}
        current={rolePop && (list.find(x => x.email.toLowerCase() === rolePop.email.toLowerCase())?.role)}
        anchorRect={rolePop?.anchor}
        onClose={() => setRolePop(null)}
        onPick={(r) => changeRole(rolePop.email, r)}
      />
      <GuestExpirationModal
        target={guestExpModal && list.find(x => x.email.toLowerCase() === guestExpModal.email.toLowerCase())}
        onClose={() => setGuestExpModal(null)}
        onSave={(iso) => saveGuestExpiration(guestExpModal.email, iso)}
      />
      <StorageLimitModal
        target={limitModal?.user}
        onClose={() => setLimitModal(null)}
        onSave={async (limitBytes) => {
          const u = limitModal?.user;
          if (!u) return;
          try {
            await window.api.setUserStorageLimit(u.id, limitBytes);
            // Update local list optimistically so the card re-renders.
            setAllUsers((prev) => prev?.map(x =>
              x.id === u.id ? { ...x, storageLimit: limitBytes } : x
            ));
            onToast?.(`Updated ${u.name}'s storage limit`, "hard-drive");
          } catch (e) {
            onToast?.(`Couldn't update limit (${e?.code || e?.message || "error"})`, "alert-circle");
          }
          setLimitModal(null);
        }}
      />
      <UserContextMenu
        open={!!userMenu}
        x={userMenu?.x}
        y={userMenu?.y}
        user={userMenu?.user}
        onClose={() => setUserMenu(null)}
        onImpersonate={() => { onImpersonate?.(userMenu.user); setUserMenu(null); }}
        onViewFiles={() => {
          onToggleViewAll?.(true);
          onToast?.(`Admin view on — ${userMenu.user.name}'s files now visible`, "eye");
          setUserMenu(null);
        }}
        onCopyEmail={() => {
          navigator.clipboard?.writeText(userMenu.user.email);
          onToast?.("Email copied", "clipboard-check");
          setUserMenu(null);
        }}
        onChangeRole={() => {
          // anchor at the click point, then open RolePopover
          setRolePop({ email: userMenu.user.email, anchor: { left: userMenu.x, top: userMenu.y, right: userMenu.x, bottom: userMenu.y, height: 0 } });
          setUserMenu(null);
        }}
        onEditStorage={() => {
          setLimitModal({ user: userMenu.user });
          setUserMenu(null);
        }}
      />
    </div>
  );
}

/* ---------- Guest expiration picker ------------------------------
   Triggered when an admin sets a member's role to "guest". Lets the
   admin pick a duration (preset) or specific date/time, or "no
   expiration". Writes back as an ISO timestamp on the allowlist entry. */
function GuestExpirationModal({ target, onClose, onSave }) {
  const M = window.FramerMotion.motion;
  const A = window.FramerMotion.AnimatePresence;
  // Modes:
  //   "duration" — count + unit (minutes / hours / days / weeks) plus quick presets
  //   "datetime" — specific local date+time picker
  //   "none"     — no expiration (admin revokes manually)
  const [mode, setMode] = useState("duration");
  const [durationN, setDurationN] = useState(1);
  const [durationU, setDurationU] = useState("week");
  const [customIso, setCustomIso] = useState("");

  // Bigger preset list now that we support sub-day expirations. Each
  // preset just seeds the number + unit fields so the user can fine-
  // tune from a starting point.
  const PRESETS = [
    { label: "30 min",  n: 30, u: "minute" },
    { label: "1 hour",  n: 1,  u: "hour"   },
    { label: "4 hours", n: 4,  u: "hour"   },
    { label: "1 day",   n: 1,  u: "day"    },
    { label: "1 week",  n: 1,  u: "week"   },
    { label: "30 days", n: 30, u: "day"    },
    { label: "90 days", n: 90, u: "day"    },
  ];

  const UNIT_MS = {
    minute: 60 * 1000,
    hour:   60 * 60 * 1000,
    day:    24 * 60 * 60 * 1000,
    week:   7 * 24 * 60 * 60 * 1000,
  };
  const durationMs = Math.max(1, durationN) * (UNIT_MS[durationU] || UNIT_MS.day);

  useEffect(() => {
    if (!target) return;
    setMode("duration");
    setDurationN(1);
    setDurationU("week");
    // Default custom datetime to "1 week from now" formatted for datetime-local input.
    const d = new Date(Date.now() + 7 * 24 * 3600 * 1000);
    const tz = d.getTimezoneOffset();
    const localIso = new Date(d.getTime() - tz * 60000).toISOString().slice(0, 16);
    setCustomIso(localIso);
  }, [target?.email]);

  useEffect(() => {
    if (!target) return;
    const onKey = (e) => { if (e.key === "Escape") onClose(); };
    window.addEventListener("keydown", onKey);
    return () => window.removeEventListener("keydown", onKey);
  }, [target, onClose]);

  if (!target) return null;

  const submit = (e) => {
    e?.preventDefault();
    if (mode === "none") { onSave(null); return; }
    if (mode === "duration") {
      const iso = new Date(Date.now() + durationMs).toISOString();
      onSave(iso);
      return;
    }
    if (mode === "datetime") {
      // datetime-local gives a local time string (no Z). Convert to ISO.
      const d = new Date(customIso);
      if (isNaN(d.getTime())) return;
      onSave(d.toISOString());
      return;
    }
  };

  return (
    <A>
      <M.div className="ct-modal-scrim"
        initial={{ opacity: 0 }} animate={{ opacity: 1 }} exit={{ opacity: 0 }}
        transition={{ duration: 0.12 }}
        onClick={onClose}>
        <M.form
          className="ct-modal ct-modal-guest-exp"
          initial={{ opacity: 0, y: 8, scale: 0.98 }}
          animate={{ opacity: 1, y: 0, scale: 1 }}
          exit={{ opacity: 0, y: 4, scale: 0.98 }}
          transition={{ type: "spring", stiffness: 320, damping: 28 }}
          onClick={(e) => e.stopPropagation()}
          onSubmit={submit}>
          <div className="ct-modal-eyebrow ct-mono">guest expiration</div>
          <div className="ct-modal-title" style={{ marginBottom: 4 }}>{target.email}</div>
          <div className="ct-mono ct-dim" style={{ fontSize: 11, marginBottom: 14 }}>
            Pick how long this guest can keep their access. After expiration
            they'll see the access-denied screen on next sign-in.
          </div>

          <div className="ct-guest-mode-tabs">
            <button type="button" className={mode === "duration" ? "is-active" : ""}
                    onClick={() => setMode("duration")}>Duration</button>
            <button type="button" className={mode === "datetime" ? "is-active" : ""}
                    onClick={() => setMode("datetime")}>Specific date</button>
            <button type="button" className={mode === "none" ? "is-active" : ""}
                    onClick={() => setMode("none")}>No expiration</button>
          </div>

          {mode === "duration" && (
            <>
              <div className="ct-guest-presets">
                {PRESETS.map((p) => {
                  const active = durationN === p.n && durationU === p.u;
                  return (
                    <button key={p.label} type="button"
                      className={"ct-guest-preset" + (active ? " is-active" : "")}
                      onClick={() => { setDurationN(p.n); setDurationU(p.u); }}>
                      {p.label}
                    </button>
                  );
                })}
              </div>
              <div className="ct-guest-duration-row">
                <label className="ct-mono ct-dim ct-guest-duration-label">or custom:</label>
                <input type="number" min="1" step="1"
                  className="ct-input ct-guest-duration-num"
                  value={durationN}
                  onChange={(e) => setDurationN(Math.max(1, parseInt(e.target.value) || 1))}/>
                <select className="ct-select ct-guest-duration-unit"
                  value={durationU}
                  onChange={(e) => setDurationU(e.target.value)}>
                  <option value="minute">minute{durationN === 1 ? "" : "s"}</option>
                  <option value="hour">hour{durationN === 1 ? "" : "s"}</option>
                  <option value="day">day{durationN === 1 ? "" : "s"}</option>
                  <option value="week">week{durationN === 1 ? "" : "s"}</option>
                </select>
              </div>
              <div className="ct-mono ct-dim" style={{ fontSize: 11, marginTop: 10 }}>
                Will expire {fmtRelative(new Date(Date.now() + durationMs).toISOString())}
                {" · "}
                {new Date(Date.now() + durationMs).toLocaleString(undefined, {
                  month: "short", day: "numeric",
                  hour: "numeric", minute: "2-digit",
                })}
              </div>
            </>
          )}

          {mode === "datetime" && (
            <input
              type="datetime-local"
              className="ct-input"
              value={customIso}
              onChange={(e) => setCustomIso(e.target.value)}
            />
          )}

          {mode === "none" && (
            <div className="ct-mono ct-dim" style={{ fontSize: 12, padding: "6px 0" }}>
              Guest access will not auto-expire. You can still revoke
              manually by removing the member or changing their role.
            </div>
          )}

          <div className="ct-modal-actions" style={{ marginTop: 18 }}>
            <button type="button" className="ct-btn ct-btn-ghost" onClick={onClose}>Cancel</button>
            <button type="submit" className="ct-btn ct-btn-primary">Save</button>
          </div>
        </M.form>
      </M.div>
    </A>
  );
}

/* ---------- Storage limit edit modal ----------------------------- */
function StorageLimitModal({ target, onClose, onSave }) {
  const M = window.FramerMotion.motion;
  const A = window.FramerMotion.AnimatePresence;
  const [unlimited, setUnlimited] = useState(false);
  const [valueMB, setValueMB] = useState("100");
  const inputRef = useRef(null);

  useEffect(() => {
    if (!target) return;
    const cur = effectiveUserLimit(target);
    setUnlimited(cur === null);
    setValueMB(cur === null ? "100" : String(Math.round(cur / (1024 * 1024))));
    setTimeout(() => inputRef.current?.focus(), 50);
  }, [target?.id]);

  useEffect(() => {
    if (!target) return;
    const onKey = (e) => { if (e.key === "Escape") onClose(); };
    window.addEventListener("keydown", onKey);
    return () => window.removeEventListener("keydown", onKey);
  }, [target, onClose]);

  if (!target) return null;

  const submit = (e) => {
    e?.preventDefault();
    if (unlimited) { onSave(null); return; }
    const mb = Math.max(0, parseFloat(valueMB) || 0);
    onSave(Math.round(mb * 1024 * 1024));
  };

  return (
    <A>
      <M.div
        className="ct-modal-scrim"
        initial={{ opacity: 0 }}
        animate={{ opacity: 1 }}
        exit={{ opacity: 0 }}
        transition={{ duration: 0.12 }}
        onClick={onClose}
      >
        <M.form
          className="ct-modal ct-modal-limit"
          initial={{ opacity: 0, y: 8, scale: 0.98 }}
          animate={{ opacity: 1, y: 0, scale: 1 }}
          exit={{ opacity: 0, y: 4, scale: 0.98 }}
          transition={{ type: "spring", stiffness: 320, damping: 28 }}
          onClick={(e) => e.stopPropagation()}
          onSubmit={submit}
        >
          <div className="ct-modal-eyebrow ct-mono">storage limit</div>
          <div className="ct-modal-title" style={{ marginBottom: 4 }}>{target.name}</div>
          <div className="ct-mono ct-dim" style={{ fontSize: 11, marginBottom: 14 }}>{target.email}</div>

          <label className="ct-godview-toggle" style={{ marginBottom: 14 }}>
            <input
              type="checkbox"
              checked={unlimited}
              onChange={(e) => setUnlimited(e.target.checked)}
            />
            <span className="ct-godview-track"><span className="ct-godview-thumb"/></span>
            <span className="ct-godview-label">
              <Icon name="infinity" size={12}/>
              <strong>Unlimited</strong>
              <span className="ct-dim ct-mono">{unlimited ? "ON" : "OFF"}</span>
            </span>
          </label>

          {!unlimited && (
            <div className="ct-limit-input-row">
              <input
                ref={inputRef}
                className="ct-input"
                type="number"
                min="0"
                step="1"
                value={valueMB}
                onChange={(e) => setValueMB(e.target.value)}
              />
              <span className="ct-mono ct-dim">MB</span>
            </div>
          )}

          <div className="ct-mono ct-dim" style={{ fontSize: 11, marginTop: 12 }}>
            Currently using {fmtBytes(target.sizeBytes || 0)}.
            Default for members is 100 MB; admins and the owner are unlimited unless overridden.
          </div>

          <div className="ct-modal-actions" style={{ marginTop: 18 }}>
            <button type="button" className="ct-btn ct-btn-ghost" onClick={onClose}>Cancel</button>
            <button type="submit" className="ct-btn ct-btn-primary">Save</button>
          </div>
        </M.form>
      </M.div>
    </A>
  );
}

/* ---------- User card right-click menu ---------------------------- */
function UserContextMenu({ open, x, y, user, onClose, onImpersonate, onViewFiles, onCopyEmail, onChangeRole, onEditStorage }) {
  const M = window.FramerMotion.motion;
  const A = window.FramerMotion.AnimatePresence;
  // Clamp position so the menu stays in viewport. Menu is ~220×200ish.
  const left = Math.min(x ?? 0, window.innerWidth - 232);
  const top  = Math.min(y ?? 0, window.innerHeight - 220);
  return (
    <A>
      {open && (
        <>
          <div className="ct-ctx-scrim" onClick={onClose} onContextMenu={(e) => { e.preventDefault(); onClose(); }}/>
          <M.div
            className="ct-ctx-menu"
            style={{ left, top }}
            initial={{ opacity: 0, scale: 0.96, y: -2 }}
            animate={{ opacity: 1, scale: 1, y: 0 }}
            exit={{ opacity: 0, scale: 0.96, y: -2 }}
            transition={{ duration: 0.12 }}
            role="menu">
            <div className="ct-ctx-header">
              <Avatar user={{ initials: user?.initials, avatarHue: user?.avatarHue }} size={24}/>
              <div className="ct-ctx-header-name">
                <div><strong>{user?.name}</strong></div>
                <div className="ct-mono ct-dim">{user?.email}</div>
              </div>
            </div>
            <button className="ct-ctx-item" onClick={onImpersonate} role="menuitem">
              <Icon name="user-check" size={14}/>
              <span>Sign in as <strong>{user?.name?.split(" ")[0]}</strong></span>
              <span className="ct-ctx-kbd">↵</span>
            </button>
            <button className="ct-ctx-item" onClick={onViewFiles} role="menuitem">
              <Icon name="folder-search" size={14}/>
              <span>View their files</span>
            </button>
            <div className="ct-ctx-sep"/>
            <button className="ct-ctx-item" onClick={onChangeRole} role="menuitem">
              <Icon name="shield" size={14}/>
              <span>Change role…</span>
            </button>
            <button className="ct-ctx-item" onClick={onEditStorage} role="menuitem">
              <Icon name="hard-drive" size={14}/>
              <span>Edit storage limit…</span>
            </button>
            <button className="ct-ctx-item" onClick={onCopyEmail} role="menuitem">
              <Icon name="copy" size={14}/>
              <span>Copy email</span>
            </button>
          </M.div>
        </>
      )}
    </A>
  );
}

/* =================================================================
 *  Wave 4 — admin sub-views
 *  -----------------------------------------------------------------
 *  Each view fetches its own data on mount and is unmounted when the
 *  operator switches tabs. Failed reads degrade to an empty state with
 *  a small note in the console — admins are mostly the workspace
 *  owner, who has rules-level access to all of these collections, so
 *  errors here usually mean a transient network blip or that the page
 *  is stale (cache age > 1h).
 * =================================================================*/

/* ---------- Overview tab: counts + storage chart ---------------- */
function AdminOverviewView({ allUsers, onToast }) {
  const [counts, setCounts] = useState(null);   // {files, projects, users, hubs, activity7d}
  const [totalBytes, setTotalBytes] = useState(null);

  useEffect(() => {
    let alive = true;
    window.api.adminGetSystemCounts?.().then(c => { if (alive) setCounts(c); })
      .catch(() => { if (alive) setCounts({}); });
    window.api.adminGetTotalStorageBytes?.().then(b => { if (alive) setTotalBytes(b); })
      .catch(() => {});
    return () => { alive = false; };
  }, []);

  // Top-10 storage hogs from the cached allUsers list. Falls back gracefully
  // if the parent hasn't finished loading allUsers yet.
  const topUsers = (allUsers || [])
    .filter(u => (u.sizeBytes || 0) > 0)
    .slice()
    .sort((a, b) => (b.sizeBytes || 0) - (a.sizeBytes || 0))
    .slice(0, 10);
  const maxBytes = topUsers[0]?.sizeBytes || 1;

  const stat = (label, value, sub) => (
    <div className="ct-admin-stat" key={label}>
      <div className="ct-admin-stat-label">{label}</div>
      {value === null
        ? <div className="ct-skel-pulse"/>
        : <div className="ct-admin-stat-value">{value}</div>}
      {sub && <div className="ct-admin-stat-sub">{sub}</div>}
    </div>
  );

  return (
    <>
      <section className="ct-section">
        <h2>System totals</h2>
        <div className="ct-admin-stats">
          {stat("Users",    counts?.users    ?? null)}
          {stat("Projects", counts?.projects ?? null)}
          {stat("Files",    counts?.files    ?? null)}
          {stat("Hubs",     counts?.hubs     ?? null)}
          {stat("Storage used", totalBytes === null ? null : fmtBytes(totalBytes),
                allUsers ? `${allUsers.length} signed-in users` : "")}
          {stat("Activity (7 d)", counts?.activity7d ?? null, "events written this week")}
        </div>
      </section>

      <section className="ct-section">
        <h2>Top storage by user</h2>
        {!allUsers && (
          <div className="ct-admin-empty ct-mono">
            <Icon name="loader" size={14}/>
            <span>Loading user totals…</span>
          </div>
        )}
        {allUsers && topUsers.length === 0 && (
          <div className="ct-admin-empty ct-mono">
            <Icon name="info" size={14}/>
            <span>No files uploaded yet, so nothing to chart.</span>
          </div>
        )}
        {topUsers.length > 0 && (
          <div className="ct-admin-bar-list">
            {topUsers.map(u => {
              const pct = Math.max(2, Math.round(((u.sizeBytes || 0) / maxBytes) * 100));
              return (
                <div className="ct-admin-bar-row" key={u.id}>
                  <div className="ct-admin-bar-name">
                    <Avatar user={{ initials: u.initials, avatarHue: u.avatarHue }} size={22}/>
                    <span><strong>{u.name}</strong> <span className="ct-mono">{u.email}</span></span>
                  </div>
                  <div className="ct-admin-bar-track">
                    <div className="ct-admin-bar-fill" style={{ width: pct + "%" }}/>
                  </div>
                  <div className="ct-admin-bar-bytes">{fmtBytes(u.sizeBytes || 0)}</div>
                </div>
              );
            })}
          </div>
        )}
      </section>
    </>
  );
}

/* ---------- All-files tab: system-wide table ------------------- */
function AdminAllFilesView({ allUsers, onToast, onOpenFile }) {
  const [files, setFiles] = useState(null);
  const [search, setSearch] = useState("");
  const [kindFilter, setKindFilter] = useState("");

  useEffect(() => {
    let alive = true;
    window.api.adminListAllFiles?.(200).then(f => { if (alive) setFiles(f || []); })
      .catch(() => { if (alive) setFiles([]); });
    return () => { alive = false; };
  }, []);

  const kindsAvailable = Array.from(new Set((files || []).map(f => f.kind).filter(Boolean))).sort();

  const ownersByUid = {};
  (allUsers || []).forEach(u => { ownersByUid[u.id] = u; });

  const q = search.trim().toLowerCase();
  const visible = (files || []).filter(f => {
    if (kindFilter && f.kind !== kindFilter) return false;
    if (!q) return true;
    const owner = ownersByUid[f.ownerUid] || ownersByUid[f.ownerId];
    return (f.name || "").toLowerCase().includes(q)
      || (owner?.email || "").toLowerCase().includes(q)
      || (f.kind || "").toLowerCase().includes(q);
  });

  const copyShareLink = async (file) => {
    try {
      if (file.shareToken) {
        const url = `${location.origin}/?share=${file.id}&t=${file.shareToken}`;
        await navigator.clipboard.writeText(url);
        onToast?.("Public share link copied", "link");
      } else {
        const url = `${location.origin}/?file=${file.id}`;
        await navigator.clipboard.writeText(url);
        onToast?.("Internal file link copied", "link");
      }
    } catch (e) {
      onToast?.("Couldn't copy link", "alert-circle");
    }
  };

  return (
    <section className="ct-section">
      <div className="ct-admin-toolbar">
        <div className="ct-admin-toolbar-search">
          <Icon name="search" size={13}/>
          <input
            value={search}
            onChange={(e) => setSearch(e.target.value)}
            placeholder="Search by name, owner email, kind…"/>
          {search && (
            <button className="ct-iconbtn" title="Clear" onClick={() => setSearch("")}>
              <Icon name="x" size={12}/>
            </button>
          )}
        </div>
        <select className="ct-select" value={kindFilter}
                onChange={(e) => setKindFilter(e.target.value)}>
          <option value="">All kinds</option>
          {kindsAvailable.map(k => <option key={k} value={k}>{k}</option>)}
        </select>
        <span className="ct-admin-toolbar-summary">
          {files === null ? "Loading…" : `${visible.length} of ${files.length} shown`}
        </span>
      </div>

      {files === null && (
        <div className="ct-admin-empty ct-mono">
          <Icon name="loader" size={14}/>
          <span>Loading files… (capped at the 200 most recently updated)</span>
        </div>
      )}
      {files && files.length === 0 && (
        <div className="ct-admin-empty ct-mono">
          <Icon name="alert-triangle" size={14}/>
          <span>No files visible. If you expected results here, hard-refresh
                (Ctrl+Shift+R) to clear stale rules cache and try again.</span>
        </div>
      )}
      {files && files.length > 0 && (
        <div className="ct-admin-table-wrap">
          <table className="ct-admin-table">
            <thead>
              <tr>
                <th>Name</th>
                <th>Owner</th>
                <th>Kind</th>
                <th style={{ textAlign: "right" }}>Size</th>
                <th>Modified</th>
                <th>Share</th>
                <th></th>
              </tr>
            </thead>
            <tbody>
              {visible.map(f => {
                const owner = ownersByUid[f.ownerUid] || ownersByUid[f.ownerId];
                const isPublic = f.shareState === "public-read";
                return (
                  <tr key={f.id}>
                    <td>
                      <div className="ct-cell-name" title={f.name}>
                        <Icon name="file" size={13}/>
                        <span>{f.name}</span>
                      </div>
                    </td>
                    <td>
                      <span className="ct-mono" title={owner?.email}>
                        {owner?.email || (f.ownerUid ? `${f.ownerUid.slice(0,8)}…` : "—")}
                      </span>
                    </td>
                    <td><span className="ct-mono">{f.kind || "—"}</span></td>
                    <td className="ct-cell-num">{fmtBytes(f.sizeBytes || 0)}</td>
                    <td><span className="ct-mono ct-dim">{fmtRelative(f.updatedAt)}</span></td>
                    <td>
                      <span className={"ct-admin-share-pill " + (isPublic ? "is-public" : "")}>
                        {isPublic ? "Public" : "Private"}
                      </span>
                    </td>
                    <td>
                      <div className="ct-cell-actions">
                        <button
                          className="ct-iconbtn"
                          title="Copy share link"
                          onClick={() => copyShareLink(f)}>
                          <Icon name="link" size={13}/>
                        </button>
                        <button
                          className="ct-iconbtn"
                          title="Open file"
                          onClick={() => onOpenFile?.(f.id)}>
                          <Icon name="external-link" size={13}/>
                        </button>
                      </div>
                    </td>
                  </tr>
                );
              })}
            </tbody>
          </table>
        </div>
      )}
    </section>
  );
}

/* ---------- Activity tab: audit log ---------------------------- */
function AdminActivityView({ allUsers, onToast, onOpenFile }) {
  const [events, setEvents] = useState(null);
  const [typeFilter, setTypeFilter] = useState("");
  const [actorFilter, setActorFilter] = useState("");

  useEffect(() => {
    let alive = true;
    window.api.adminListActivity?.(100).then(e => { if (alive) setEvents(e || []); })
      .catch(() => { if (alive) setEvents([]); });
    return () => { alive = false; };
  }, []);

  const ownersByUid = {};
  (allUsers || []).forEach(u => { ownersByUid[u.id] = u; });

  const types = Array.from(new Set((events || []).map(e => e.type).filter(Boolean))).sort();
  const visible = (events || []).filter(e => {
    if (typeFilter && e.type !== typeFilter) return false;
    if (actorFilter && e.actorUid !== actorFilter) return false;
    return true;
  });

  return (
    <section className="ct-section">
      <div className="ct-admin-toolbar">
        <select className="ct-select" value={typeFilter}
                onChange={(e) => setTypeFilter(e.target.value)}>
          <option value="">All event types</option>
          {types.map(t => <option key={t} value={t}>{t}</option>)}
        </select>
        <select className="ct-select" value={actorFilter}
                onChange={(e) => setActorFilter(e.target.value)}>
          <option value="">All users</option>
          {(allUsers || []).map(u => (
            <option key={u.id} value={u.id}>{u.email}</option>
          ))}
        </select>
        <span className="ct-admin-toolbar-summary">
          {events === null ? "Loading…" : `${visible.length} of ${events.length} events`}
        </span>
      </div>

      {events === null && (
        <div className="ct-admin-empty ct-mono">
          <Icon name="loader" size={14}/>
          <span>Loading activity…</span>
        </div>
      )}
      {events && events.length === 0 && (
        <div className="ct-admin-empty ct-mono">
          <Icon name="info" size={14}/>
          <span>No activity events yet. Uploads, restores, and shares all
                write to the audit log automatically.</span>
        </div>
      )}
      {visible.length > 0 && (
        <div className="ct-admin-activity-list">
          {visible.map(e => {
            const actor = ownersByUid[e.actorUid];
            return (
              <div className="ct-admin-activity-row" key={e.id}>
                <span className="ct-admin-activity-time" title={fmtDate(e.at)}>
                  {fmtRelative(e.at)}
                </span>
                <span className="ct-admin-activity-type">{e.type || "event"}</span>
                <span className="ct-admin-activity-actor">
                  {actor && <Avatar user={{ initials: actor.initials, avatarHue: actor.avatarHue }} size={20}/>}
                  <span>
                    <strong>{actor?.name || (e.actorUid ? e.actorUid.slice(0, 8) + "…" : "Unknown")}</strong>
                    {actor?.email && <> · <span className="ct-mono">{actor.email}</span></>}
                  </span>
                </span>
                <span className="ct-admin-activity-target">
                  {e.fileId
                    ? <button
                        className="ct-iconbtn"
                        title={"Open file " + e.fileId}
                        style={{ padding: "2px 6px", fontFamily: "var(--ct-mono)", fontSize: 11 }}
                        onClick={() => onOpenFile?.(e.fileId)}>
                        {e.fileId.slice(0, 8)}…
                      </button>
                    : <span className="ct-dim">—</span>}
                  {e.source && <> · <span className="ct-mono ct-dim">{e.source}</span></>}
                </span>
              </div>
            );
          })}
        </div>
      )}
    </section>
  );
}

/* ---------- Admin staff tab: who else can manage --------------- */
// Surfaces the subset of allowlist entries with role === "admin", plus the
// hardcoded owner. Adding/removing admins is the same operation as toggling
// an allowlist entry's role to/from "admin" — we just present it through a
// dedicated UI so the operator doesn't have to scroll the full member list.
function AdminStaffView({ list, isOwnerUser, onSave, onToast }) {
  const [email, setEmail] = useState("");

  const owner = list.find(x => x.email.toLowerCase() === OWNER_EMAIL)
    || { email: OWNER_EMAIL, role: "owner", addedAt: null };
  const admins = list.filter(x => x.role === "admin"
    && x.email.toLowerCase() !== OWNER_EMAIL);

  const promote = () => {
    const e = email.trim().toLowerCase();
    if (!/^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(e)) {
      onToast?.("Enter a valid email", "alert-circle"); return;
    }
    if (e === OWNER_EMAIL) {
      onToast?.("Owner is already an admin", "info"); return;
    }
    const next = list.slice();
    const idx = next.findIndex(x => x.email.toLowerCase() === e);
    if (idx >= 0) {
      if (next[idx].role === "admin") {
        onToast?.(`${e} is already an admin`, "info"); return;
      }
      next[idx] = { ...next[idx], role: "admin" };
    } else {
      next.push({ email: e, role: "admin", addedAt: new Date().toISOString() });
    }
    onSave(next);
    setEmail("");
    onToast?.(`${e} is now an admin`, "shield-check");
  };

  const demote = (targetEmail) => {
    if (targetEmail.toLowerCase() === OWNER_EMAIL) {
      onToast?.("Can't change the owner's role", "shield"); return;
    }
    const next = list.map(x =>
      x.email.toLowerCase() === targetEmail.toLowerCase()
        ? { ...x, role: "member" }
        : x);
    onSave(next);
    onToast?.(`${targetEmail} → member`, "user-minus");
  };

  // Live-validation hint for the promote input.
  const hint = (() => {
    const trimmed = email.trim().toLowerCase();
    if (!trimmed) return null;
    if (!/^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(trimmed)) {
      return { cls: "is-error", icon: "alert-circle", text: "That doesn't look like an email." };
    }
    if (trimmed === OWNER_EMAIL) {
      return { cls: "is-warn", icon: "info", text: "Owner already has admin powers." };
    }
    const cur = list.find(x => x.email.toLowerCase() === trimmed);
    if (cur && cur.role === "admin") {
      return { cls: "is-warn", icon: "info", text: `${trimmed} is already an admin.` };
    }
    if (cur) {
      return { cls: "is-ok", icon: "shield-check",
               text: `Will promote ${trimmed} from ${cur.role} to admin.` };
    }
    return { cls: "is-ok", icon: "user-plus",
             text: `Will add ${trimmed} to the allowlist as admin.` };
  })();

  return (
    <>
      <section className="ct-section">
        <h2>Promote a user to admin</h2>
        <p className="ct-dim ct-admin-users-sub">
          Admins can read and write every collection except the owner's own
          profile. The owner ({OWNER_EMAIL}) is hardcoded in the rules and
          can't be removed.
        </p>
        <div className="ct-admin-add">
          <Icon name="mail" size={14}/>
          <input
            value={email}
            onChange={(e) => setEmail(e.target.value)}
            placeholder="person@company.com"
            onKeyDown={(e) => e.key === "Enter" && promote()}/>
          <span className="ct-mono ct-dim" style={{ fontSize: 11, padding: "0 8px" }}>→ ADMIN</span>
          <button className="ct-btn ct-btn-primary" onClick={promote}>
            <Icon name="shield-check" size={13}/>Promote
          </button>
        </div>
        {hint && (
          <div className={"ct-admin-add-hint " + hint.cls}>
            <Icon name={hint.icon} size={12}/>
            <span>{hint.text}</span>
          </div>
        )}
      </section>

      <section className="ct-section">
        <h2>Current admins ({admins.length + 1})</h2>
        <div className="ct-admin-emails-list">
          {/* Owner row — always present, always first, never removable. */}
          <div className="ct-admin-emails-row is-owner">
            <div>
              <div className="ct-mono"><strong>{owner.email}</strong></div>
              <div className="ct-dim" style={{ fontSize: 11 }}>
                Workspace owner · hardcoded in the rules
              </div>
            </div>
            <span className="ct-pill ct-pill-owner">Owner</span>
            <span className="ct-mono ct-dim" style={{ fontSize: 10.5 }}>—</span>
          </div>
          {admins.map(a => (
            <div className="ct-admin-emails-row" key={a.email}>
              <div>
                <div className="ct-mono">{a.email}</div>
                <div className="ct-dim" style={{ fontSize: 11 }}>
                  Promoted {a.addedAt ? fmtRelative(a.addedAt) : "—"}
                </div>
              </div>
              <span className="ct-pill ct-pill-admin">Admin</span>
              <button
                className="ct-btn ct-btn-ghost"
                disabled={!isOwnerUser}
                title={isOwnerUser ? "Demote to member" : "Only the owner can demote admins"}
                onClick={() => demote(a.email)}>
                <Icon name="user-minus" size={13}/>Demote
              </button>
            </div>
          ))}
          {admins.length === 0 && (
            <div className="ct-admin-empty ct-mono">
              <Icon name="info" size={14}/>
              <span>No additional admins. The owner is the sole administrator.</span>
            </div>
          )}
        </div>
      </section>
    </>
  );
}

function hashHue(s) {
  let h = 0;
  for (let i = 0; i < s.length; i++) h = (h * 31 + s.charCodeAt(i)) % 360;
  return h;
}

Object.assign(window, {
  GoogleAccountPicker, AccessDeniedView, AdminView, ConfirmDialog,
  RolePopover, UserContextMenu, StorageLimitModal, GuestExpirationModal,
  AdminOverviewView, AdminAllFilesView, AdminActivityView, AdminStaffView,
});
