/* eslint-disable no-undef */
/**
 * Supply Chain Intelligence & Situation Report Hub — frontend SPA.
 *
 * Compiled in-browser by @babel/standalone (no build step).
 * State + routing all live in App() at the bottom; views below are pure
 * components fed via props.
 */

const { useState, useEffect, useRef, useCallback, createContext, useContext } = React;

// =============================================================================
// Globals
// =============================================================================

const CFG = window.STUDIO_CONFIG || {};
const APP_NAME_FALLBACK = CFG.appName || "Supply Chain Intelligence & Situation Report Hub";
const SHORT_NAME_FALLBACK = CFG.shortName || "SCI Hub";
const TAGLINE = CFG.tagline || "";
const ACCEPTED_TYPES = (CFG.acceptedFileTypes || []).join(",");
const UPLOAD_MAX_BYTES = CFG.uploadMaxBytes || (100 * 1024 * 1024);

// =============================================================================
// API helper
// =============================================================================

async function api(method, path, body) {
  const opts = { method, credentials: "include", headers: {} };
  if (body instanceof FormData) {
    opts.body = body;
  } else if (body !== undefined && body !== null) {
    opts.headers["Content-Type"] = "application/json";
    opts.body = JSON.stringify(body);
  }
  const res = await fetch(path, opts);
  let data = null;
  try { data = await res.json(); } catch {}
  if (!res.ok) {
    const msg = (data && data.error) || `Request failed (${res.status})`;
    const err = new Error(msg);
    err.status = res.status;
    throw err;
  }
  return data || {};
}

// =============================================================================
// Toast helper
// =============================================================================

function showToast(message, type = "info") {
  const container = document.getElementById("toast-container");
  if (!container) return;
  const colors = {
    info: { bg: "#1a1a1e", border: "#2a2a2e", text: "#f0ede8" },
    success: { bg: "#0f2918", border: "#16a34a", text: "#dcfce7" },
    error: { bg: "#2c0e0e", border: "#dc2626", text: "#fecaca" },
  };
  const c = colors[type] || colors.info;
  const el = document.createElement("div");
  el.className = "toast";
  el.style.cssText = `background:${c.bg};border:1px solid ${c.border};color:${c.text};`;
  el.textContent = message;
  container.appendChild(el);
  setTimeout(() => el.remove(), 4000);
}

// =============================================================================
// Format helpers
// =============================================================================

function fmtBytes(n) {
  if (!n) return "0 B";
  const units = ["B", "KB", "MB", "GB"];
  let i = 0;
  let v = n;
  while (v >= 1024 && i < units.length - 1) { v /= 1024; i++; }
  return `${v.toFixed(v >= 100 || i === 0 ? 0 : 1)} ${units[i]}`;
}

function fmtDate(iso) {
  if (!iso) return "";
  const d = new Date(iso.includes("T") ? iso : iso + "Z");
  if (isNaN(d.getTime())) return iso;
  return d.toLocaleDateString(undefined, { year: "numeric", month: "short", day: "numeric" });
}

function fmtDateTime(iso) {
  if (!iso) return "";
  const d = new Date(iso.includes("T") ? iso : iso + "Z");
  if (isNaN(d.getTime())) return iso;
  return d.toLocaleString(undefined, { year: "numeric", month: "short", day: "numeric", hour: "2-digit", minute: "2-digit" });
}

function fileEmoji(mime) {
  if (!mime) return "📄";
  if (mime.startsWith("image/")) return "🖼️";
  if (mime.startsWith("video/")) return "🎬";
  if (mime.startsWith("audio/")) return "🎧";
  if (mime === "application/pdf") return "📕";
  if (mime.includes("presentation") || mime.includes("powerpoint")) return "📊";
  if (mime.includes("spreadsheet") || mime.includes("excel")) return "📈";
  if (mime.includes("word")) return "📝";
  if (mime.includes("zip") || mime.includes("compressed")) return "🗜️";
  if (mime.startsWith("text/")) return "📃";
  return "📄";
}

// Short uppercase label used on cards. Editorial / typographic, not iconographic.
function fileTypeLabel(mime) {
  if (!mime) return "FILE";
  if (mime === "application/pdf") return "PDF";
  if (mime.includes("powerpoint") || mime.includes("presentation")) return "DECK";
  if (mime.includes("word")) return "DOC";
  if (mime.includes("excel") || mime.includes("spreadsheet")) return "SHEET";
  if (mime.startsWith("image/")) return "IMAGE";
  if (mime.startsWith("video/")) return "VIDEO";
  if (mime.startsWith("audio/")) return "AUDIO";
  if (mime.startsWith("text/")) return "TEXT";
  if (mime.includes("zip") || mime.includes("compressed")) return "ARCHIVE";
  return "FILE";
}

function previewType(mime) {
  if (!mime) return "none";
  if (mime === "application/pdf") return "pdf";
  if (mime.startsWith("image/")) return "image";
  if (mime.startsWith("video/")) return "video";
  if (mime.startsWith("audio/")) return "audio";
  if (mime.startsWith("text/")) return "text";
  return "none";
}

// =============================================================================
// UI primitives
// =============================================================================

function Btn({ variant = "primary", size, className = "", children, ...rest }) {
  const cls = `btn btn-${variant}${size ? ` btn-${size}` : ""} ${className}`.trim();
  return <button className={cls} {...rest}>{children}</button>;
}

function Modal({ open, onClose, title, children, footer, size = "default" }) {
  useEffect(() => {
    if (!open) return;
    function onKey(e) { if (e.key === "Escape") onClose(); }
    window.addEventListener("keydown", onKey);
    return () => window.removeEventListener("keydown", onKey);
  }, [open, onClose]);
  if (!open) return null;
  const widthClass = size === "wide" ? "modal-wide" : size === "xl" ? "modal-xl" : "";
  return (
    <div className="modal-backdrop" onClick={onClose}>
      <div className={`modal-panel ${widthClass}`} onClick={(e) => e.stopPropagation()}>
        <div className="modal-header">
          <h3>{title}</h3>
          <Btn variant="ghost" size="sm" onClick={onClose} aria-label="Close">✕</Btn>
        </div>
        <div className="modal-body">{children}</div>
        {footer && <div className="modal-footer">{footer}</div>}
      </div>
    </div>
  );
}

function Field({ label, htmlFor, children, hint }) {
  return (
    <div className="field">
      {label && <label htmlFor={htmlFor}>{label}</label>}
      {children}
      {hint && <div style={{ fontSize: 12, color: "var(--muted)", marginTop: 4 }}>{hint}</div>}
    </div>
  );
}

// Password input with show/hide eye toggle.
function PasswordInput({ id, value, onChange, required, minLength, autoFocus, placeholder }) {
  const [shown, setShown] = useState(false);
  return (
    <div style={{ position: "relative" }}>
      <input
        id={id}
        type={shown ? "text" : "password"}
        value={value}
        onChange={onChange}
        required={required}
        minLength={minLength}
        autoFocus={autoFocus}
        placeholder={placeholder}
        style={{ paddingRight: 42 }}
      />
      <button
        type="button"
        onClick={() => setShown(s => !s)}
        title={shown ? "Hide password" : "Show password"}
        aria-label={shown ? "Hide password" : "Show password"}
        style={{
          position: "absolute", right: 4, top: "50%", transform: "translateY(-50%)",
          padding: "6px 8px", background: "transparent", border: "none",
          color: "var(--subtext)", cursor: "pointer", fontSize: 16, lineHeight: 1,
        }}
      >
        {shown ? "🙈" : "👁"}
      </button>
    </div>
  );
}

function Spinner({ size = 18 }) {
  return (
    <span className="spin" style={{ width: size, height: size, display: "inline-block" }}>
      <svg width={size} height={size} viewBox="0 0 24 24" fill="none">
        <circle cx="12" cy="12" r="10" stroke="currentColor" strokeWidth="2" strokeDasharray="40" strokeDashoffset="20" />
      </svg>
    </span>
  );
}

// =============================================================================
// Notification bell + dropdown feed
// =============================================================================

function NotificationBell({ onJump }) {
  const [count, setCount] = useState(0);
  const [open, setOpen] = useState(false);
  const [items, setItems] = useState(null);
  const [busy, setBusy] = useState(false);
  const wrapperRef = useRef(null);

  // Poll unread count every 30s.
  useEffect(() => {
    let cancelled = false;
    async function tick() {
      try {
        const r = await api("GET", "/api/notifications/unread-count");
        if (!cancelled) setCount(r.count || 0);
      } catch { /* network blip */ }
    }
    tick();
    const interval = setInterval(tick, 30000);
    return () => { cancelled = true; clearInterval(interval); };
  }, []);

  // Click outside closes.
  useEffect(() => {
    if (!open) return;
    function onClick(e) {
      if (wrapperRef.current && !wrapperRef.current.contains(e.target)) setOpen(false);
    }
    document.addEventListener("mousedown", onClick);
    return () => document.removeEventListener("mousedown", onClick);
  }, [open]);

  async function toggle() {
    if (open) { setOpen(false); return; }
    setOpen(true);
    setBusy(true);
    try {
      const r = await api("GET", "/api/notifications");
      setItems(r.notifications || []);
    } catch (e) {
      showToast(e.message, "error");
      setItems([]);
    } finally { setBusy(false); }
  }

  async function markAllRead() {
    if (count === 0) return;
    try {
      await api("POST", "/api/notifications/read");
      setItems(prev => (prev || []).map(n => ({ ...n, read: true })));
      setCount(0);
    } catch (e) { showToast(e.message, "error"); }
  }

  async function clickItem(n) {
    setOpen(false);
    if (!n.read) {
      try {
        await api("POST", `/api/notifications/${n.id}/read`);
        setCount(c => Math.max(0, c - 1));
      } catch { /* best effort */ }
    }
    if (n.type === "report") {
      onJump({ view: "reports" });
    } else if (n.type === "mention") {
      onJump({ view: "chat", channelId: n.metadata && n.metadata.channelId });
    } else if (n.type === "dm") {
      onJump({ view: "inbox", threadId: n.metadata && n.metadata.threadId });
    }
  }

  return (
    <div ref={wrapperRef} style={{ position: "relative" }}>
      <button onClick={toggle} title="Notifications" style={{
        position: "relative", background: "transparent", border: "1px solid var(--border)",
        borderRadius: 8, padding: "6px 10px", cursor: "pointer", fontSize: 16, color: "var(--text)",
      }}>
        🔔
        {count > 0 && (
          <span style={{
            position: "absolute", top: -4, right: -4,
            background: "var(--primary)", color: "#fff",
            borderRadius: 999, padding: "1px 5px", fontSize: 10, fontWeight: 700,
            minWidth: 16, textAlign: "center", lineHeight: "14px",
          }}>{count > 99 ? "99+" : count}</span>
        )}
      </button>
      {open && (
        <div style={{
          position: "absolute", top: "calc(100% + 8px)", right: 0,
          width: 380, maxHeight: 480,
          background: "var(--surface)", border: "1px solid var(--border)",
          borderRadius: 10, boxShadow: "0 10px 30px rgba(0,0,0,0.4)",
          zIndex: 150, display: "flex", flexDirection: "column",
        }}>
          <div style={{ padding: "12px 14px", borderBottom: "1px solid var(--border)",
                        display: "flex", justifyContent: "space-between", alignItems: "center" }}>
            <strong style={{ fontSize: 13 }}>Notifications</strong>
            {count > 0 && (
              <button onClick={markAllRead} style={{
                background: "transparent", border: "none", color: "var(--primary)",
                fontSize: 11, cursor: "pointer", fontWeight: 600,
              }}>Mark all read</button>
            )}
          </div>
          <div style={{ flex: 1, overflowY: "auto", maxHeight: 420 }}>
            {busy ? (
              <div style={{ padding: 16 }}><div className="skeleton" style={{ height: 60 }} /></div>
            ) : !items || items.length === 0 ? (
              <div style={{ padding: 30, textAlign: "center", color: "var(--muted)", fontSize: 13 }}>
                No notifications yet.
              </div>
            ) : (
              items.map(n => (
                <button key={n.id} onClick={() => clickItem(n)} style={{
                  display: "block", width: "100%", textAlign: "left",
                  padding: "10px 14px", background: n.read ? "transparent" : "rgba(200,155,60,0.07)",
                  border: "none", borderBottom: "1px solid var(--border)", cursor: "pointer",
                }}>
                  <div style={{ display: "flex", gap: 8, alignItems: "flex-start" }}>
                    <span style={{ fontSize: 14, marginTop: 2 }}>
                      {n.type === "mention" ? "💬" : "📥"}
                    </span>
                    <div style={{ flex: 1, minWidth: 0 }}>
                      <div style={{ fontSize: 13, fontWeight: n.read ? 500 : 700, color: "var(--text)", marginBottom: 2 }}>
                        {n.title}
                      </div>
                      {n.body && (
                        <div style={{ fontSize: 12, color: "var(--subtext)",
                                      overflow: "hidden", textOverflow: "ellipsis",
                                      display: "-webkit-box", WebkitLineClamp: 2, WebkitBoxOrient: "vertical" }}>
                          {n.body}
                        </div>
                      )}
                      <div style={{ fontSize: 11, color: "var(--muted)", marginTop: 4 }}>
                        {fmtDateTime(n.createdAt)}
                      </div>
                    </div>
                    {!n.read && (
                      <span style={{ width: 8, height: 8, borderRadius: "50%",
                                     background: "var(--primary)", marginTop: 6, flexShrink: 0 }} />
                    )}
                  </div>
                </button>
              ))
            )}
          </div>
        </div>
      )}
    </div>
  );
}

function ConfirmDialog({ open, title, message, confirmLabel = "Confirm", danger, onConfirm, onClose }) {
  const [busy, setBusy] = useState(false);
  async function handleConfirm() {
    setBusy(true);
    try { await onConfirm(); onClose(); }
    catch (e) { showToast(e.message, "error"); }
    finally { setBusy(false); }
  }
  return (
    <Modal open={open} onClose={onClose} title={title} footer={
      <>
        <Btn variant="ghost" onClick={onClose} disabled={busy}>Cancel</Btn>
        <Btn variant={danger ? "danger" : "primary"} onClick={handleConfirm} disabled={busy}>
          {busy ? <Spinner /> : confirmLabel}
        </Btn>
      </>
    }>
      <p style={{ color: "var(--subtext)", lineHeight: 1.6 }}>{message}</p>
    </Modal>
  );
}

// =============================================================================
// Auth screens
// =============================================================================

function AuthShell({ children, hubName, tagline }) {
  return (
    <div style={{
      minHeight: "100vh",
      display: "flex", alignItems: "center", justifyContent: "center",
      padding: 24,
      background: "var(--bg)",
    }}>
      <div style={{
        width: "100%", maxWidth: 440,
        background: "var(--surface)",
        border: "1px solid var(--border-soft)",
        borderRadius: 12,
        padding: "44px 36px 36px",
        boxShadow: "var(--shadow-overlay)",
      }}>
        <div style={{ textAlign: "center", marginBottom: 36 }}>
          <div className="eyebrow" style={{ marginBottom: 4 }}>
            Resilience Engineers
          </div>
          <div style={{ width: 32, height: 2, background: "var(--primary)", margin: "12px auto 18px" }} />
          <div className="serif" style={{
            fontSize: 23, fontWeight: 700, color: "var(--text)",
            letterSpacing: "-0.02em", lineHeight: 1.25, marginBottom: 8,
          }}>
            {hubName}
          </div>
          {tagline && <div style={{ fontSize: 13, color: "var(--subtext)", lineHeight: 1.55, fontStyle: "italic" }}>{tagline}</div>}
        </div>
        {children}
      </div>
    </div>
  );
}

function LoginScreen({ onAuth, onForgot, onSignup, hubName, tagline }) {
  const [email, setEmail] = useState("");
  const [password, setPassword] = useState("");
  const [busy, setBusy] = useState(false);
  const [err, setErr] = useState("");

  async function handleSubmit(e) {
    e.preventDefault();
    setErr("");
    setBusy(true);
    try {
      const res = await fetch("/api/auth/login", {
        method: "POST", credentials: "include",
        headers: { "Content-Type": "application/json" },
        body: JSON.stringify({ email, password }),
      });
      const data = await res.json().catch(() => ({}));
      if (res.status === 402 && data.gated) {
        // Payment required — push the user back to checkout to complete payment.
        // The 402 response already issued a short-lived session cookie.
        onAuth(); // App will pick up the gated state via /api/auth/me
        return;
      }
      if (!res.ok) {
        setErr(data.error || `Login failed (${res.status})`);
        return;
      }
      await onAuth();
    } catch (e) {
      setErr(e.message);
    } finally {
      setBusy(false);
    }
  }

  return (
    <AuthShell hubName={hubName} tagline={tagline}>
      <form onSubmit={handleSubmit}>
        <Field label="Email" htmlFor="login-email">
          <input id="login-email" type="email" value={email}
                 onChange={e => setEmail(e.target.value)} required autoFocus />
        </Field>
        <Field label="Password" htmlFor="login-password">
          <PasswordInput id="login-password" value={password}
                         onChange={e => setPassword(e.target.value)} required />
        </Field>
        {err && <div style={{ color: "var(--danger)", fontSize: 13, marginBottom: 12 }}>{err}</div>}
        <Btn type="submit" disabled={busy} style={{ width: "100%" }}>
          {busy ? <Spinner /> : "Sign in"}
        </Btn>
      </form>
      <div style={{ marginTop: 16, textAlign: "center" }}>
        <button type="button" onClick={onForgot}
                style={{ background: "none", border: "none", color: "var(--subtext)", cursor: "pointer", fontSize: 13 }}>
          Forgot password?
        </button>
      </div>
      <div style={{ marginTop: 22, paddingTop: 18, borderTop: "1px solid var(--border)", textAlign: "center" }}>
        <div style={{ fontSize: 13, color: "var(--subtext)", marginBottom: 8 }}>New here?</div>
        <Btn variant="ghost" onClick={onSignup} style={{ width: "100%" }}>Create an account</Btn>
      </div>
    </AuthShell>
  );
}

// =============================================================================
// Signup screen with plan picker → Stripe checkout
// =============================================================================

function SignupScreen({ onBack, onSignedUp, hubName, tagline }) {
  const [step, setStep] = useState(1); // 1: plan, 2: account, 3: redirecting
  const [plans, setPlans] = useState(null);
  const [planId, setPlanId] = useState(null);
  const [email, setEmail] = useState("");
  const [fullName, setFullName] = useState("");
  const [password, setPassword] = useState("");
  const [emailOptIn, setEmailOptIn] = useState(true);
  const [busy, setBusy] = useState(false);
  const [err, setErr] = useState("");

  useEffect(() => {
    api("GET", "/api/plans").then(d => {
      setPlans(d.plans || []);
      if (d.plans && d.plans.length > 0) setPlanId(d.plans[0].id);
    }).catch(e => setErr(e.message));
  }, []);

  async function handleSubmit(e) {
    e.preventDefault();
    setErr("");
    if (!planId) { setErr("Pick a plan first"); return; }
    if (password.length < 8) { setErr("Password must be at least 8 characters"); return; }
    setBusy(true);
    try {
      // 1. Create the account (issues a session cookie).
      const sr = await api("POST", "/api/auth/signup", {
        email, password, fullName, planId, emailOptIn,
      });
      // 2. If they picked the free tier, skip Stripe and bounce them straight in.
      if (sr.status === "free" || planId === "free") {
        showToast("Welcome — you're on the Free tier.", "success");
        onSignedUp && onSignedUp();
        return;
      }
      // 3. Otherwise, request a Stripe Checkout URL and redirect.
      const ck = await api("POST", "/api/stripe/checkout-session", { planId });
      setStep(3);
      window.location.href = ck.url;
    } catch (e) {
      setErr(e.message);
      setBusy(false);
    }
  }

  return (
    <AuthShell hubName={hubName} tagline={tagline}>
      {step === 3 && (
        <div style={{ textAlign: "center", padding: "20px 0" }}>
          <Spinner size={28} />
          <div style={{ marginTop: 14, color: "var(--subtext)" }}>Redirecting to secure checkout…</div>
        </div>
      )}

      {step === 1 && plans === null && (
        <div className="skeleton" style={{ height: 220, marginBottom: 14 }} />
      )}

      {step === 1 && plans && plans.length === 0 && (
        <div style={{ textAlign: "center", color: "var(--subtext)", padding: "20px 0" }}>
          <div style={{ fontSize: 14, marginBottom: 12 }}>No plans configured yet.</div>
          <Btn variant="ghost" onClick={onBack}>Back to sign in</Btn>
        </div>
      )}

      {step === 1 && plans && plans.length > 0 && (
        <>
          <h2 style={{ fontSize: 16, fontWeight: 700, marginBottom: 14 }}>Choose a plan</h2>
          <div style={{ display: "flex", flexDirection: "column", gap: 10, marginBottom: 18 }}>
            {plans.map(p => {
              const selected = planId === p.id;
              const isFreeTier = p.id === "free" || p.price === 0;
              return (
                <button key={p.id} type="button" onClick={() => setPlanId(p.id)} style={{
                  textAlign: "left",
                  padding: "14px 16px",
                  background: selected ? "rgba(200,155,60,0.1)" : "var(--surface2)",
                  border: "1px solid " + (selected ? "var(--primary)" : "var(--border)"),
                  borderRadius: 10,
                  cursor: "pointer",
                  transition: "border-color 0.12s, background 0.12s",
                  position: "relative",
                }}>
                  <div style={{ display: "flex", justifyContent: "space-between", alignItems: "baseline", marginBottom: 6 }}>
                    <div style={{ fontSize: 15, fontWeight: 700, color: "var(--text)" }}>{p.name}</div>
                    <div style={{ fontSize: 13, fontWeight: 600, color: "var(--primary)" }}>
                      {isFreeTier ? "Free" : `${p.currency === "EUR" ? "€" : ""}${p.price}/${p.period}`}
                    </div>
                  </div>
                  <ul style={{ listStyle: "none", padding: 0, margin: 0, fontSize: 12, color: "var(--subtext)" }}>
                    {(p.features || []).map(f => (
                      <li key={f} style={{ padding: "2px 0" }}>· {f}</li>
                    ))}
                  </ul>
                  {!isFreeTier && !p.configured && (
                    <div style={{ marginTop: 8, fontSize: 11, color: "var(--warn)" }}>
                      ⚠ This plan has no Stripe price ID set yet — checkout will fail until configured.
                    </div>
                  )}
                </button>
              );
            })}
          </div>
          <Btn type="button" onClick={() => setStep(2)} disabled={!planId} style={{ width: "100%" }}>
            Continue
          </Btn>
          <div style={{ marginTop: 14, textAlign: "center" }}>
            <button type="button" onClick={onBack}
                    style={{ background: "none", border: "none", color: "var(--subtext)", cursor: "pointer", fontSize: 13 }}>
              ← Already have an account? Sign in
            </button>
          </div>
        </>
      )}

      {step === 2 && (
        <>
          <h2 style={{ fontSize: 16, fontWeight: 700, marginBottom: 14 }}>Your details</h2>
          <form onSubmit={handleSubmit}>
            <Field
              label="Email address (required)"
              htmlFor="su-email"
              hint="You'll sign in with this address. We also use it to notify you when new reports and SITREPs are published."
            >
              <input id="su-email" type="email" required autoFocus
                     value={email} onChange={e => setEmail(e.target.value)} />
            </Field>
            <Field label="Full name" htmlFor="su-fn">
              <input id="su-fn" type="text"
                     value={fullName} onChange={e => setFullName(e.target.value)} />
            </Field>
            <Field label="Password" htmlFor="su-pw" hint="Min 8 characters">
              <PasswordInput id="su-pw" required minLength={8}
                             value={password} onChange={e => setPassword(e.target.value)} />
            </Field>
            <Field>
              <label style={{ display: "flex", alignItems: "flex-start", gap: 10, cursor: "pointer", textTransform: "none", letterSpacing: 0, fontSize: 13, color: "var(--text)", lineHeight: 1.5 }}>
                <input type="checkbox" checked={emailOptIn}
                       onChange={e => setEmailOptIn(e.target.checked)}
                       style={{ width: "auto", marginTop: 3 }} />
                <span>
                  Email me when new reports are published.
                  <span style={{ display: "block", fontSize: 11, color: "var(--muted)", marginTop: 2 }}>
                    You can change this any time in Settings. We never share your email.
                  </span>
                </span>
              </label>
            </Field>
            {err && <div style={{ color: "var(--danger)", fontSize: 13, marginBottom: 12 }}>{err}</div>}
            <Btn type="submit" disabled={busy} style={{ width: "100%" }}>
              {busy ? <Spinner /> : (planId === "free" ? "Create free account" : "Continue to payment")}
            </Btn>
          </form>
          <div style={{ marginTop: 14, textAlign: "center" }}>
            <button type="button" onClick={() => setStep(1)}
                    style={{ background: "none", border: "none", color: "var(--subtext)", cursor: "pointer", fontSize: 13 }}>
              ← Change plan
            </button>
          </div>
          {planId !== "free" && (
            <div style={{ marginTop: 16, fontSize: 11, color: "var(--muted)", textAlign: "center", lineHeight: 1.5 }}>
              You'll be redirected to Stripe to complete payment securely.
              Your account stays in pending state until the subscription is active.
            </div>
          )}
          {planId === "free" && (
            <div style={{ marginTop: 16, fontSize: 11, color: "var(--muted)", textAlign: "center", lineHeight: 1.5 }}>
              No payment required — your free account is created immediately.
              You can upgrade any time from the sidebar.
            </div>
          )}
        </>
      )}
    </AuthShell>
  );
}

// =============================================================================
// Payment-required gate (shown when login returns 402 or user is mid-flow)
// =============================================================================

function PaymentRequiredScreen({ user, onLogout, onContinue, hubName, tagline }) {
  const [busy, setBusy] = useState(false);
  const [err, setErr] = useState("");

  async function continueCheckout() {
    setBusy(true); setErr("");
    try {
      const planId = user.plan;
      if (!planId) {
        setErr("No plan selected. Pick one and contact the admin.");
        setBusy(false);
        return;
      }
      const ck = await api("POST", "/api/stripe/checkout-session", { planId });
      window.location.href = ck.url;
    } catch (e) {
      setErr(e.message);
      setBusy(false);
    }
  }

  const message = ({
    pending: "Your account is awaiting payment. Complete checkout to access the hub.",
    past_due: "Your subscription payment is past due. Please update your payment method.",
    canceled: "Your subscription was canceled. Re-subscribe below to regain access.",
  })[user.stripe_status] || "Your account is not active. Please complete payment to continue.";

  return (
    <AuthShell hubName={hubName} tagline={tagline}>
      <h2 style={{ fontSize: 16, fontWeight: 700, marginBottom: 8 }}>Payment required</h2>
      <p style={{ fontSize: 14, color: "var(--subtext)", marginBottom: 18, lineHeight: 1.6 }}>
        {message}
      </p>
      <div style={{ background: "var(--surface2)", border: "1px solid var(--border)", borderRadius: 10, padding: 14, marginBottom: 16, fontSize: 13 }}>
        <div style={{ color: "var(--muted)", fontSize: 11, textTransform: "uppercase", letterSpacing: "0.5px", marginBottom: 4 }}>Account</div>
        <div>{user.email}</div>
        {user.plan && <div style={{ marginTop: 6, color: "var(--subtext)" }}>Plan: <strong style={{ color: "var(--text)" }}>{user.plan}</strong></div>}
      </div>
      {err && <div style={{ color: "var(--danger)", fontSize: 13, marginBottom: 12 }}>{err}</div>}
      <Btn onClick={continueCheckout} disabled={busy} style={{ width: "100%" }}>
        {busy ? <Spinner /> : "Continue to payment"}
      </Btn>
      <div style={{ marginTop: 12, textAlign: "center" }}>
        <button type="button" onClick={onLogout}
                style={{ background: "none", border: "none", color: "var(--muted)", cursor: "pointer", fontSize: 12 }}>
          Sign out
        </button>
      </div>
    </AuthShell>
  );
}

function ForgotScreen({ onBack, hubName, tagline }) {
  const [email, setEmail] = useState("");
  const [busy, setBusy] = useState(false);
  const [done, setDone] = useState(false);

  async function handleSubmit(e) {
    e.preventDefault();
    setBusy(true);
    try {
      await api("POST", "/api/auth/forgot", { email });
      setDone(true);
    } catch (e) { showToast(e.message, "error"); }
    finally { setBusy(false); }
  }

  return (
    <AuthShell hubName={hubName} tagline={tagline}>
      {done ? (
        <div style={{ textAlign: "center" }}>
          <div style={{ fontSize: 14, marginBottom: 18, color: "var(--text)", lineHeight: 1.6 }}>
            If an account exists for <strong>{email}</strong>, a reset link has been sent.
          </div>
          <Btn variant="ghost" onClick={onBack}>Back to sign in</Btn>
        </div>
      ) : (
        <>
          <h2 style={{ fontSize: 16, fontWeight: 700, marginBottom: 16 }}>Reset your password</h2>
          <form onSubmit={handleSubmit}>
            <Field label="Email" htmlFor="forgot-email">
              <input id="forgot-email" type="email" required autoFocus
                     value={email} onChange={e => setEmail(e.target.value)} />
            </Field>
            <Btn type="submit" disabled={busy} style={{ width: "100%" }}>
              {busy ? <Spinner /> : "Send reset link"}
            </Btn>
          </form>
          <div style={{ marginTop: 14, textAlign: "center" }}>
            <button type="button" onClick={onBack}
                    style={{ background: "none", border: "none", color: "var(--subtext)", cursor: "pointer", fontSize: 13 }}>
              ← Back to sign in
            </button>
          </div>
        </>
      )}
    </AuthShell>
  );
}

function ResetScreen({ token, onDone, hubName, tagline }) {
  const [pw1, setPw1] = useState("");
  const [pw2, setPw2] = useState("");
  const [busy, setBusy] = useState(false);
  const [err, setErr] = useState("");

  async function handleSubmit(e) {
    e.preventDefault();
    setErr("");
    if (pw1.length < 8) { setErr("Password must be at least 8 characters"); return; }
    if (pw1 !== pw2) { setErr("Passwords don't match"); return; }
    setBusy(true);
    try {
      await api("POST", "/api/auth/reset", { token, password: pw1 });
      showToast("Password reset. You can sign in now.", "success");
      onDone();
    } catch (e) { setErr(e.message); }
    finally { setBusy(false); }
  }

  return (
    <AuthShell hubName={hubName} tagline={tagline}>
      <h2 style={{ fontSize: 16, fontWeight: 700, marginBottom: 16 }}>Set a new password</h2>
      <form onSubmit={handleSubmit}>
        <Field label="New password" htmlFor="reset-pw1">
          <PasswordInput id="reset-pw1" required minLength={8}
                         value={pw1} onChange={e => setPw1(e.target.value)} autoFocus />
        </Field>
        <Field label="Confirm password" htmlFor="reset-pw2">
          <PasswordInput id="reset-pw2" required minLength={8}
                         value={pw2} onChange={e => setPw2(e.target.value)} />
        </Field>
        {err && <div style={{ color: "var(--danger)", fontSize: 13, marginBottom: 12 }}>{err}</div>}
        <Btn type="submit" disabled={busy} style={{ width: "100%" }}>
          {busy ? <Spinner /> : "Save password"}
        </Btn>
      </form>
    </AuthShell>
  );
}

// =============================================================================
// Sidebar + topbar
// =============================================================================

function Sidebar({ user, view, setView, hubShortName, onLogout, onUpgrade }) {
  const isAdmin = user.role === "admin";
  const isFree = user.stripe_status === "free";
  const allItems = [
    { id: "reports", label: "Reports", icon: "📥", paid: false },
    { id: "archive", label: "Archive", icon: "📦", paid: true },
    { id: "chat", label: "Chat", icon: "💬", paid: true },
    { id: "inbox", label: "Inbox", icon: "✉️", paid: true },
    { id: "request", label: "Request topic", icon: "💡", paid: false },
    { id: "settings", label: "Settings", icon: "⚙️", paid: false },
  ];
  const items = isFree ? allItems.filter(i => !i.paid) : allItems;
  if (isAdmin) items.push({ id: "admin", label: "Admin", icon: "🛠️" });

  return (
    <aside style={{
      width: 220, flexShrink: 0,
      background: "var(--surface)",
      borderRight: "1px solid var(--border)",
      display: "flex", flexDirection: "column",
      height: "100vh",
    }}>
      <div style={{ padding: "26px 22px 22px", borderBottom: "1px solid var(--border)" }}>
        <div style={{ fontSize: 9.5, fontWeight: 700, color: "var(--primary)",
                      letterSpacing: "2px", textTransform: "uppercase", marginBottom: 8 }}>
          Resilience Engineers
        </div>
        <div className="serif" style={{ fontSize: 18, fontWeight: 700, color: "var(--text)",
                                          letterSpacing: "-0.02em", lineHeight: 1.15 }}>
          {hubShortName}
        </div>
        {isAdmin && <div style={{ fontSize: 9.5, color: "var(--muted)", marginTop: 12, fontWeight: 600, letterSpacing: "1.6px" }}>ADMIN</div>}
        {isFree && <div style={{ fontSize: 9.5, color: "var(--muted)", marginTop: 12, fontWeight: 600, letterSpacing: "1.6px" }}>FREE TIER</div>}
      </div>
      <nav style={{ flex: 1, padding: "16px 12px", display: "flex", flexDirection: "column", gap: 1 }}>
        {items.map(item => {
          const active = view === item.id;
          return (
            <button key={item.id} onClick={() => setView(item.id)} style={{
              display: "flex", alignItems: "center", gap: 12,
              padding: "10px 14px", borderRadius: 4,
              fontSize: 14, fontWeight: active ? 600 : 500, textAlign: "left",
              background: active ? "var(--surface2)" : "transparent",
              color: active ? "var(--text)" : "var(--subtext)",
              border: "none",
              cursor: "pointer", letterSpacing: "0.1px",
              transition: "color 0.15s ease, background 0.15s ease",
              position: "relative",
            }}
              onMouseEnter={e => { if (!active) e.currentTarget.style.color = "var(--text)"; }}
              onMouseLeave={e => { if (!active) e.currentTarget.style.color = "var(--subtext)"; }}>
              {active && <span style={{
                position: "absolute", left: 0, top: 8, bottom: 8, width: 2,
                background: "var(--primary)",
              }} />}
              <span style={{ fontSize: 14, opacity: active ? 1 : 0.7 }}>{item.icon}</span>
              {item.label}
            </button>
          );
        })}
      </nav>
      {isFree && (
        <div style={{ margin: "12px 16px 8px",
                      padding: "16px 16px 14px",
                      background: "var(--surface2)",
                      borderTop: "2px solid var(--primary)",
                      borderRadius: "0 0 4px 4px" }}>
          <div className="eyebrow" style={{ marginBottom: 8 }}>
            Upgrade
          </div>
          <div style={{ fontSize: 12.5, color: "var(--text)", lineHeight: 1.55, marginBottom: 12 }}>
            Unlock the full archive, member chat, and direct messages.
          </div>
          <button onClick={onUpgrade} style={{
            width: "100%", padding: "9px 12px", borderRadius: 4,
            background: "var(--primary)", color: "#0a1c2f", border: "none",
            fontWeight: 700, fontSize: 12, cursor: "pointer", letterSpacing: "0.4px",
          }}>Upgrade plan →</button>
        </div>
      )}
      <div style={{ padding: "16px 18px 18px", borderTop: "1px solid var(--border)" }}>
        <div style={{ fontSize: 12, color: "var(--text)", marginBottom: 4, overflow: "hidden", textOverflow: "ellipsis", whiteSpace: "nowrap", fontWeight: 600 }}>
          {user.full_name || (user.email || "").split("@")[0]}
        </div>
        <div style={{ fontSize: 11, color: "var(--muted)", marginBottom: 8, overflow: "hidden", textOverflow: "ellipsis", whiteSpace: "nowrap" }}>
          {user.email}
        </div>
        <button onClick={onLogout} style={{
          fontSize: 12, color: "var(--muted)", background: "none", border: "none",
          cursor: "pointer", padding: 0, letterSpacing: "0.1px",
        }}>Sign out</button>
      </div>
    </aside>
  );
}

function TopBar({ title, subtitle, actions }) {
  return (
    <div style={{
      display: "flex", alignItems: "flex-end", justifyContent: "space-between",
      padding: "32px 36px 24px", borderBottom: "1px solid var(--border)", flexShrink: 0,
      gap: 24,
    }}>
      <div style={{ minWidth: 0 }}>
        <h1 className="serif" style={{
          fontSize: 32, fontWeight: 700, color: "var(--text)",
          letterSpacing: "-0.02em", lineHeight: 1.1, margin: 0,
        }}>{title}</h1>
        {subtitle && <div style={{ fontSize: 13, color: "var(--subtext)", marginTop: 8, letterSpacing: "0.15px", lineHeight: 1.5 }}>{subtitle}</div>}
      </div>
      <div style={{ display: "flex", gap: 10, alignItems: "center", flexShrink: 0 }}>{actions}</div>
    </div>
  );
}

// =============================================================================
// Reports list view (handles active OR archive, member OR admin)
// =============================================================================

function ReportsView({ user, archived, refreshKey, onChange, autoPreviewReportId, onAutoPreviewHandled }) {
  const isAdmin = user.role === "admin";
  const [reports, setReports] = useState(null);
  const [search, setSearch] = useState("");
  const [activeTags, setActiveTags] = useState(new Set());
  // The new IA: a single "view" selector covering crisis / one-off / folder / latest / unread.
  // Shape: { kind: 'latest' | 'unread' | 'oneoff' | 'crisis' | 'folder' | 'folder-root', id?: string }
  const [activeView, setActiveView] = useState({ kind: "latest" });
  const [crises, setCrises] = useState(null);          // [{id, title, reportCount, latestDay, lastReportAt}]
  const [oneoffCount, setOneoffCount] = useState(0);
  const [folders, setFolders] = useState(null);
  const [uncategorizedCount, setUncategorizedCount] = useState(0);
  const [previewing, setPreviewing] = useState(null);
  const [editing, setEditing] = useState(null);
  const [confirming, setConfirming] = useState(null);
  const [folderEditTarget, setFolderEditTarget] = useState(null); // folder being edited / 'new'

  // Reset view + filters when toggling the Active vs Archive top-level tab.
  useEffect(() => {
    setSearch("");
    setActiveTags(new Set());
    setActiveView({ kind: "latest" });
  }, [archived]);

  // When the chat passes a reportId via auto-preview, fetch + open the modal
  // for that report regardless of current view filters.
  useEffect(() => {
    if (!autoPreviewReportId) return;
    api("GET", `/api/reports/${autoPreviewReportId}`).then(d => {
      if (d.report) setPreviewing(d.report);
    }).catch(() => {
      showToast("Report not found or no longer available.", "error");
    });
    onAutoPreviewHandled && onAutoPreviewHandled();
  }, [autoPreviewReportId, onAutoPreviewHandled]);

  // Load crises (with stats) for the sidebar — only when not archived.
  const loadCrises = useCallback(async () => {
    if (archived) { setCrises([]); return; }
    try {
      const d = await api("GET", "/api/crisis");
      setCrises(d.pages || []);
      setOneoffCount(d.oneoffCount || 0);
    } catch { setCrises([]); setOneoffCount(0); }
  }, [archived]);

  // Load folders (legacy IA — collapsed under "Reference / Folders" section).
  const loadFolders = useCallback(async () => {
    if (archived) return;
    try {
      const d = await api("GET", "/api/folders");
      setFolders(d.folders || []);
      setUncategorizedCount(d.uncategorizedCount || 0);
    } catch { setFolders([]); }
  }, [archived]);

  useEffect(() => { loadCrises(); loadFolders(); }, [loadCrises, loadFolders, refreshKey]);

  const load = useCallback(async () => {
    try {
      let path;
      if (archived) {
        path = "/api/reports/archive";
      } else {
        path = "/api/reports";
        const params = [];
        if (activeView.kind === "crisis" && activeView.id) {
          params.push(`crisis=${encodeURIComponent(activeView.id)}`);
        } else if (activeView.kind === "oneoff") {
          params.push(`crisis=oneoff`);
        } else if (activeView.kind === "folder" && activeView.id) {
          params.push(`folder=${encodeURIComponent(activeView.id)}`);
        } else if (activeView.kind === "folder-root") {
          params.push(`folder=root`);
        }
        if (params.length) path += "?" + params.join("&");
      }
      const data = await api("GET", path);
      setReports(data.reports || []);
    } catch (e) {
      showToast(e.message, "error");
      setReports([]);
    }
  }, [archived, activeView]);

  useEffect(() => { load(); }, [load, refreshKey]);

  async function toggleArchive(r) {
    try {
      await api("PATCH", `/api/admin/reports/${r.id}`, { archived: !r.archived });
      showToast(r.archived ? "Restored" : "Archived", "success");
      onChange();
    } catch (e) { showToast(e.message, "error"); }
  }

  function deleteReport(r) {
    setConfirming({
      title: "Delete report?",
      message: `"${r.title}" will be permanently deleted from D1 and R2. This cannot be undone.`,
      confirmLabel: "Delete",
      danger: true,
      action: async () => {
        await api("DELETE", `/api/admin/reports/${r.id}`);
        showToast("Deleted", "success");
        onChange();
      },
    });
  }

  // All unique tags across the loaded set, alphabetically sorted, with counts.
  const tagCounts = (() => {
    const m = new Map();
    (reports || []).forEach(r => (r.tags || []).forEach(t => m.set(t, (m.get(t) || 0) + 1)));
    return [...m.entries()].sort((a, b) => a[0].localeCompare(b[0]));
  })();

  function toggleTag(t) {
    setActiveTags(prev => {
      const next = new Set(prev);
      if (next.has(t)) next.delete(t); else next.add(t);
      return next;
    });
  }

  const filtered = (reports || []).filter(r => {
    // Unread view: only show reports the user hasn't opened yet.
    if (activeView.kind === "unread" && r.unread === false) return false;
    if (activeTags.size > 0) {
      const tags = r.tags || [];
      // Report must have ALL selected tags (intersection / "AND" semantics).
      for (const t of activeTags) if (!tags.includes(t)) return false;
    }
    if (!search.trim()) return true;
    const q = search.toLowerCase();
    return (
      r.title.toLowerCase().includes(q) ||
      (r.summary || "").toLowerCase().includes(q) ||
      (r.tags || []).some(t => t.toLowerCase().includes(q)) ||
      (r.crisisTitle || "").toLowerCase().includes(q)
    );
  });

  // For the Latest / Unread / One-off / Folder views we render a chronological
  // feed grouped by date. For a Crisis view we render a Day-ordered list with
  // a series header at the top.
  function dateBucket(iso) {
    if (!iso) return "Earlier";
    // Treat naive timestamps (no timezone) as UTC, matching the SQLite default.
    const d = new Date(iso + (iso.includes("T") ? "" : "Z"));
    if (isNaN(d.getTime())) return "Earlier";
    const ageMs = Date.now() - d.getTime();
    const day = 86400000;
    if (ageMs < day && d.toDateString() === new Date().toDateString()) return "Today";
    if (ageMs < day * 2) return "Yesterday";
    if (ageMs < day * 7) return "This week";
    if (ageMs < day * 30) return "This month";
    return "Earlier";
  }
  const groupedByDate = (() => {
    const order = ["Today", "Yesterday", "This week", "This month", "Earlier"];
    const buckets = new Map(order.map(k => [k, []]));
    for (const r of filtered) {
      const k = dateBucket(r.publishedAt);
      buckets.get(k).push(r);
    }
    return order.map(k => ({ name: k, items: buckets.get(k) })).filter(g => g.items.length > 0);
  })();

  // Active crisis (when view = 'crisis') — for series header.
  const activeCrisis = activeView.kind === "crisis"
    ? (crises || []).find(c => c.id === activeView.id)
    : null;

  // View headline for the main pane (above the search bar).
  function viewHeadline() {
    switch (activeView.kind) {
      case "latest":     return { title: "Latest", subtitle: "Newest reports across every crisis and one-off event." };
      case "unread":     return { title: "Unread", subtitle: "Reports you haven't opened yet." };
      case "oneoff":     return { title: "One-off events", subtitle: "Reports that aren't part of an ongoing crisis." };
      case "folder-root":return { title: "Uncategorized", subtitle: "Reports without a folder." };
      case "folder": {
        const f = (folders || []).find(x => x.id === activeView.id);
        return { title: f ? f.name : "Folder", subtitle: f && f.description ? f.description : null };
      }
      case "crisis":
        return activeCrisis
          ? { title: activeCrisis.title,
              subtitle: activeCrisis.subtitle
                || (activeCrisis.latestDay != null
                    ? `Crisis is at Day ${activeCrisis.latestDay} · ${activeCrisis.reportCount} report${activeCrisis.reportCount === 1 ? "" : "s"}`
                    : `${activeCrisis.reportCount} report${activeCrisis.reportCount === 1 ? "" : "s"}`) }
          : { title: "Crisis", subtitle: null };
      default:           return { title: "Reports", subtitle: null };
    }
  }
  const headline = viewHeadline();

  return (
    <div style={{ display: "flex", flex: 1, overflow: "hidden" }}>
      {!archived && (
        <ReportsSidebar
          activeView={activeView}
          setActiveView={setActiveView}
          crises={crises}
          oneoffCount={oneoffCount}
          folders={folders}
          uncategorizedCount={uncategorizedCount}
          isAdmin={isAdmin}
          onCreateFolder={() => setFolderEditTarget("new")}
          onEditFolder={(f) => setFolderEditTarget(f)}
        />
      )}
      <div style={{ padding: "20px 28px", flex: 1, overflowY: "auto" }}>
      {!archived && (
        <div style={{ marginBottom: 14 }}>
          <h2 className="serif" style={{ fontSize: 22, fontWeight: 700, margin: 0, letterSpacing: "-0.01em" }}>
            {headline.title}
          </h2>
          {headline.subtitle && (
            <div style={{ fontSize: 13, color: "var(--muted)", marginTop: 4, lineHeight: 1.5 }}>
              {headline.subtitle}
            </div>
          )}
        </div>
      )}
      <div style={{ display: "flex", gap: 12, marginBottom: 14, alignItems: "center" }}>
        <input type="search" placeholder="Search title, summary, tags, crisis…"
               value={search} onChange={e => setSearch(e.target.value)}
               style={{ flex: 1, maxWidth: 420 }} />
        <div style={{ fontSize: 13, color: "var(--subtext)" }}>
          {reports === null ? "" : `${filtered.length} ${filtered.length === 1 ? "report" : "reports"}`}
        </div>
      </div>

      {tagCounts.length > 0 && (
        <div style={{ display: "flex", flexWrap: "wrap", gap: 6, marginBottom: 18, alignItems: "center" }}>
          <span style={{ fontSize: 11, fontWeight: 700, color: "var(--muted)", textTransform: "uppercase", letterSpacing: "0.5px", marginRight: 4 }}>
            Filter by tag:
          </span>
          {tagCounts.map(([t, count]) => {
            const on = activeTags.has(t);
            return (
              <button key={t} type="button" onClick={() => toggleTag(t)} style={{
                padding: "4px 10px",
                borderRadius: 999,
                fontSize: 12,
                fontWeight: 600,
                background: on ? "var(--primary)" : "var(--surface2)",
                color: on ? "#fff" : "var(--subtext)",
                border: "1px solid " + (on ? "var(--primary)" : "var(--border)"),
                cursor: "pointer",
                transition: "background 0.12s, color 0.12s",
              }}>
                {t} <span style={{ opacity: 0.6, fontWeight: 400, marginLeft: 2 }}>{count}</span>
              </button>
            );
          })}
          {activeTags.size > 0 && (
            <button type="button" onClick={() => setActiveTags(new Set())} style={{
              padding: "4px 10px", fontSize: 12, color: "var(--subtext)",
              background: "transparent", border: "none", cursor: "pointer",
            }}>
              Clear ({activeTags.size})
            </button>
          )}
        </div>
      )}

      {reports === null ? (
        <div className="report-grid">
          {[0, 1, 2].map(i => (
            <div key={i} className="skeleton" style={{ height: 280 }} />
          ))}
        </div>
      ) : filtered.length === 0 ? (
        <div className="empty-state">
          <h3>{archived ? "No archived reports" :
                activeView.kind === "unread" ? "All caught up" :
                activeView.kind === "crisis" ? "No reports for this crisis yet" :
                activeView.kind === "oneoff" ? "No one-off reports" :
                "No reports yet"}</h3>
          <p>{archived
            ? "Reports you archive will appear here."
            : activeView.kind === "unread"
              ? "Every report has been opened. Check Latest for the full feed."
              : (isAdmin ? "Click \"Upload report\" to add one." : "Reports will appear here once your admin uploads them.")
          }</p>
        </div>
      ) : archived || activeView.kind === "crisis" ? (
        // Crisis view + archive view: flat ordered list (Day-DESC for crisis,
        // archived-DESC for archive). The backend already sorts.
        <div className="report-grid">
          {filtered.map(r => (
            <ReportCard key={r.id} report={r} isAdmin={isAdmin}
                        onView={() => setPreviewing(r)}
                        onEdit={() => setEditing(r)}
                        onArchive={() => toggleArchive(r)}
                        onDelete={() => deleteReport(r)}
                        onCrisisClick={(id) => setActiveView({ kind: "crisis", id })} />
          ))}
        </div>
      ) : (
        // Latest / Unread / One-off / Folder views: date-grouped feed.
        <div>
          {groupedByDate.map(g => (
            <div key={g.name} style={{ marginBottom: 24 }}>
              <div style={{
                fontSize: 11, fontWeight: 700, color: "var(--muted)",
                textTransform: "uppercase", letterSpacing: "1px",
                marginBottom: 10, paddingBottom: 6,
                borderBottom: "1px solid var(--border-soft, var(--border))",
              }}>{g.name} <span style={{ fontWeight: 400, marginLeft: 6 }}>· {g.items.length}</span></div>
              <div className="report-grid">
                {g.items.map(r => (
                  <ReportCard key={r.id} report={r} isAdmin={isAdmin}
                              onView={() => setPreviewing(r)}
                              onEdit={() => setEditing(r)}
                              onArchive={() => toggleArchive(r)}
                              onDelete={() => deleteReport(r)}
                              onCrisisClick={(id) => setActiveView({ kind: "crisis", id })} />
                ))}
              </div>
            </div>
          ))}
        </div>
      )}

      {previewing && (
        <ReportPreviewModal report={previewing} open={!!previewing} onClose={() => setPreviewing(null)} />
      )}
      {editing && (
        <ReportEditModal report={editing} open={!!editing} onClose={() => setEditing(null)}
                         folders={folders || []}
                         onUpdated={() => { setEditing(null); onChange(); loadFolders(); }} />
      )}
      {folderEditTarget !== null && (
        <FolderEditModal
          folder={folderEditTarget === "new" ? null : folderEditTarget}
          allFolders={folders || []}
          onClose={() => setFolderEditTarget(null)}
          onSaved={() => { setFolderEditTarget(null); loadFolders(); }}
          onDeleted={() => { setFolderEditTarget(null); setActiveView({ kind: "latest" }); loadFolders(); load(); }}
        />
      )}
      {confirming && (
        <ConfirmDialog open={!!confirming}
                       title={confirming.title}
                       message={confirming.message}
                       confirmLabel={confirming.confirmLabel}
                       danger={confirming.danger}
                       onConfirm={confirming.action}
                       onClose={() => setConfirming(null)} />
      )}
      </div>
    </div>
  );
}

// =============================================================================
// Folder sidebar — hierarchical tree with admin controls
// =============================================================================

function buildFolderTree(folders) {
  const byId = new Map();
  (folders || []).forEach(f => byId.set(f.id, { ...f, children: [] }));
  const roots = [];
  byId.forEach(node => {
    if (node.parentId && byId.has(node.parentId)) {
      byId.get(node.parentId).children.push(node);
    } else {
      roots.push(node);
    }
  });
  return roots;
}

// Flatten tree to "Parent / Child" entries for dropdowns
function flattenFoldersForDropdown(tree, prefix = "") {
  const out = [];
  for (const node of tree) {
    const path = prefix ? `${prefix} / ${node.name}` : node.name;
    out.push({ id: node.id, label: path });
    out.push(...flattenFoldersForDropdown(node.children, path));
  }
  return out;
}

// Reports IA sidebar — three sections:
//   1. Top-level: Latest, Unread
//   2. Active crises (each row links to a crisis-scoped Day-ordered view)
//   3. One-off events
//   4. Folders (collapsed by default — used only for stable reference docs)
function ReportsSidebar({
  activeView, setActiveView,
  crises, oneoffCount,
  folders, uncategorizedCount,
  isAdmin, onCreateFolder, onEditFolder,
}) {
  const [foldersOpen, setFoldersOpen] = useState(false);
  const tree = buildFolderTree(folders);

  function relTime(iso) {
    if (!iso) return null;
    const d = new Date(iso + (iso.includes("T") ? "" : "Z"));
    if (isNaN(d.getTime())) return null;
    const ageMin = (Date.now() - d.getTime()) / 60000;
    if (ageMin < 60) return `${Math.max(1, Math.round(ageMin))}m ago`;
    if (ageMin < 1440) return `${Math.round(ageMin / 60)}h ago`;
    return `${Math.round(ageMin / 1440)}d ago`;
  }

  const isCrisisActive = (id) => activeView.kind === "crisis" && activeView.id === id;
  const sectionLabel = {
    fontSize: 11, fontWeight: 700, color: "var(--muted)",
    textTransform: "uppercase", letterSpacing: "0.5px",
    padding: "16px 12px 6px",
  };

  return (
    <aside style={{
      width: 240, flexShrink: 0,
      background: "var(--surface)",
      borderRight: "1px solid var(--border)",
      display: "flex", flexDirection: "column",
    }}>
      <div style={{ flex: 1, overflowY: "auto", padding: "8px 4px" }}>
        {/* Section: Latest + Unread */}
        <FolderRow label="Latest" icon="📰" depth={0}
                   active={activeView.kind === "latest"}
                   onClick={() => setActiveView({ kind: "latest" })} />
        <FolderRow label="Unread" icon="🔔" depth={0}
                   active={activeView.kind === "unread"}
                   onClick={() => setActiveView({ kind: "unread" })} />

        {/* Section: Active crises */}
        <div style={sectionLabel}>Active crises</div>
        {crises === null ? (
          <div className="skeleton" style={{ height: 28, margin: "4px 8px" }} />
        ) : crises.length === 0 ? (
          <div style={{ padding: "4px 12px 8px", fontSize: 11, color: "var(--muted)", lineHeight: 1.5 }}>
            {isAdmin
              ? "No crisis pages yet. Admins → Crisis pages → New."
              : "No ongoing crises."}
          </div>
        ) : (
          crises.map(c => {
            const sub = c.latestDay != null
              ? `Day ${c.latestDay} · ${c.reportCount} report${c.reportCount === 1 ? "" : "s"}`
              : `${c.reportCount} report${c.reportCount === 1 ? "" : "s"}`;
            const time = relTime(c.lastReportAt);
            return (
              <div key={c.id} style={{ padding: "0 4px" }}>
                <button onClick={() => setActiveView({ kind: "crisis", id: c.id })} style={{
                  width: "100%", textAlign: "left", border: "1px solid transparent",
                  background: isCrisisActive(c.id) ? "var(--surface2)" : "transparent",
                  borderColor: isCrisisActive(c.id) ? "var(--border)" : "transparent",
                  borderRadius: 6, padding: "8px 10px", cursor: "pointer", display: "block",
                }}>
                  <div style={{ fontSize: 13, fontWeight: 600,
                                color: isCrisisActive(c.id) ? "var(--text)" : "var(--subtext)",
                                overflow: "hidden", textOverflow: "ellipsis", whiteSpace: "nowrap" }}>
                    {c.title}
                  </div>
                  <div style={{ fontSize: 11, color: "var(--muted)", marginTop: 2,
                                display: "flex", justifyContent: "space-between", gap: 4 }}>
                    <span style={{ overflow: "hidden", textOverflow: "ellipsis", whiteSpace: "nowrap" }}>{sub}</span>
                    {time && <span style={{ flexShrink: 0 }}>{time}</span>}
                  </div>
                </button>
              </div>
            );
          })
        )}

        {/* Section: One-off events */}
        <div style={sectionLabel}>One-off events</div>
        <FolderRow label="All one-offs" icon="📌" count={oneoffCount} depth={0}
                   active={activeView.kind === "oneoff"}
                   onClick={() => setActiveView({ kind: "oneoff" })} />

        {/* Section: Folders (collapsed) */}
        <button onClick={() => setFoldersOpen(o => !o)} style={{
          ...sectionLabel,
          display: "flex", justifyContent: "space-between", alignItems: "center",
          width: "100%", border: "none", background: "transparent",
          cursor: "pointer", textAlign: "left",
        }}>
          <span>Folders (reference)</span>
          <span style={{ fontSize: 10 }}>{foldersOpen ? "▾" : "▸"}</span>
        </button>
        {foldersOpen && (
          <div style={{ padding: "0 4px 6px" }}>
            {folders === null ? (
              <div className="skeleton" style={{ height: 24, margin: "4px 0" }} />
            ) : (
              <>
                {(folders || []).length > 0 && (
                  <FolderRow label="Uncategorized" count={uncategorizedCount}
                             active={activeView.kind === "folder-root"}
                             onClick={() => setActiveView({ kind: "folder-root" })}
                             icon="📄" depth={0} muted />
                )}
                <FolderTreeNodes nodes={tree} depth={0}
                                 activeId={activeView.kind === "folder" ? activeView.id : null}
                                 setActive={(id) => setActiveView({ kind: "folder", id })}
                                 isAdmin={isAdmin} onEdit={onEditFolder} />
                {isAdmin && (
                  <button onClick={onCreateFolder} style={{
                    fontSize: 11, color: "var(--primary)", background: "transparent",
                    border: "1px dashed var(--border)", padding: "5px 10px",
                    margin: "6px 8px 0", borderRadius: 6, cursor: "pointer",
                  }}>+ New folder</button>
                )}
                {(folders || []).length === 0 && !isAdmin && (
                  <div style={{ padding: "4px 12px 0", fontSize: 11, color: "var(--muted)" }}>
                    No folders.
                  </div>
                )}
              </>
            )}
          </div>
        )}
      </div>
    </aside>
  );
}

function FolderTreeNodes({ nodes, depth, activeId, setActive, isAdmin, onEdit }) {
  return nodes.map(n => (
    <React.Fragment key={n.id}>
      <FolderRow label={n.name} count={n.reportCount || 0}
                 active={activeId === n.id}
                 onClick={() => setActive(n.id)}
                 onEdit={isAdmin ? () => onEdit(n) : null}
                 icon="📁" depth={depth} />
      {n.children.length > 0 && (
        <FolderTreeNodes nodes={n.children} depth={depth + 1}
                         activeId={activeId} setActive={setActive}
                         isAdmin={isAdmin} onEdit={onEdit} />
      )}
    </React.Fragment>
  ));
}

function FolderRow({ label, count, active, onClick, onEdit, icon, depth, muted }) {
  return (
    <div style={{
      display: "flex", alignItems: "center", gap: 6,
      paddingLeft: 6 + depth * 14,
    }} className="folder-row">
      <button onClick={onClick} style={{
        flex: 1,
        display: "flex", alignItems: "center", gap: 8,
        padding: "7px 10px", borderRadius: 6,
        fontSize: 13, fontWeight: active ? 700 : 500, textAlign: "left",
        background: active ? "var(--surface2)" : "transparent",
        color: muted && !active ? "var(--muted)" : (active ? "var(--text)" : "var(--subtext)"),
        border: "1px solid " + (active ? "var(--border)" : "transparent"),
        cursor: "pointer", overflow: "hidden",
      }}>
        <span style={{ fontSize: 13 }}>{icon}</span>
        <span style={{ flex: 1, overflow: "hidden", textOverflow: "ellipsis", whiteSpace: "nowrap" }}>{label}</span>
        <span style={{ fontSize: 11, color: "var(--muted)", flexShrink: 0 }}>{count}</span>
      </button>
      {onEdit && (
        <button onClick={onEdit} title="Edit folder" style={{
          background: "transparent", border: "none", color: "var(--muted)",
          cursor: "pointer", fontSize: 12, padding: "0 6px",
        }}>✏</button>
      )}
    </div>
  );
}

function FolderEditModal({ folder, allFolders, onClose, onSaved, onDeleted }) {
  const isCreate = !folder;
  const [name, setName] = useState(folder?.name || "");
  const [description, setDescription] = useState(folder?.description || "");
  const [parentId, setParentId] = useState(folder?.parentId || "");
  const [busy, setBusy] = useState(false);
  const [confirmingDelete, setConfirmingDelete] = useState(false);

  const flatOptions = flattenFoldersForDropdown(buildFolderTree(allFolders));
  // When editing, hide self + descendants from parent options.
  const blockedIds = (() => {
    if (!folder) return new Set();
    const blocked = new Set([folder.id]);
    const visit = (id) => {
      (allFolders || []).filter(f => f.parentId === id).forEach(child => {
        blocked.add(child.id);
        visit(child.id);
      });
    };
    visit(folder.id);
    return blocked;
  })();

  async function save(e) {
    if (e) e.preventDefault();
    if (!name.trim()) { showToast("Name is required", "error"); return; }
    setBusy(true);
    try {
      const body = {
        name: name.trim(),
        description: description.trim() || null,
        parentId: parentId || null,
      };
      if (isCreate) {
        await api("POST", "/api/admin/folders", body);
        showToast("Folder created", "success");
      } else {
        await api("PATCH", `/api/admin/folders/${folder.id}`, body);
        showToast("Folder updated", "success");
      }
      onSaved();
    } catch (e) { showToast(e.message, "error"); }
    finally { setBusy(false); }
  }

  async function doDelete() {
    setBusy(true);
    try {
      await api("DELETE", `/api/admin/folders/${folder.id}`);
      showToast("Folder deleted", "success");
      onDeleted();
    } catch (e) { showToast(e.message, "error"); setBusy(false); setConfirmingDelete(false); }
  }

  return (
    <>
      <Modal open onClose={busy ? () => {} : onClose}
             title={isCreate ? "New folder" : `Edit: ${folder.name}`}
             footer={
        <>
          {!isCreate && (
            <Btn variant="danger" onClick={() => setConfirmingDelete(true)} disabled={busy}>
              Delete folder
            </Btn>
          )}
          <div style={{ flex: 1 }} />
          <Btn variant="ghost" onClick={onClose} disabled={busy}>Cancel</Btn>
          <Btn onClick={save} disabled={busy}>{busy ? <Spinner /> : (isCreate ? "Create" : "Save")}</Btn>
        </>
      }>
        <form onSubmit={save}>
          <Field label="Name" htmlFor="f-name">
            <input id="f-name" type="text" required maxLength={80} autoFocus
                   value={name} onChange={e => setName(e.target.value)} />
          </Field>
          <Field label="Description (optional)" htmlFor="f-desc">
            <input id="f-desc" type="text" value={description}
                   onChange={e => setDescription(e.target.value)} />
          </Field>
          <Field label="Parent folder" htmlFor="f-parent"
                 hint="Leave blank for top-level. Cannot be moved under itself or its descendants.">
            <select id="f-parent" value={parentId}
                    onChange={e => setParentId(e.target.value)}>
              <option value="">— Top level —</option>
              {flatOptions.filter(o => !blockedIds.has(o.id)).map(o => (
                <option key={o.id} value={o.id}>{o.label}</option>
              ))}
            </select>
          </Field>
          {!isCreate && (
            <div style={{ fontSize: 12, color: "var(--muted)", marginTop: 14, lineHeight: 1.6 }}>
              Deleting this folder also deletes its sub-folders. Reports inside become uncategorized — they are NOT deleted.
            </div>
          )}
        </form>
      </Modal>
      <ConfirmDialog
        open={confirmingDelete}
        title="Delete folder?"
        message={`"${folder?.name}" and all its sub-folders will be deleted. Reports inside become uncategorized (not deleted). This cannot be undone.`}
        confirmLabel="Delete folder"
        danger
        onConfirm={doDelete}
        onClose={() => setConfirmingDelete(false)}
      />
    </>
  );
}

function ReportCard({ report, isAdmin, onView, onEdit, onArchive, onDelete, onCrisisClick }) {
  const r = report;
  const showUnreadDot = r.unread === true;
  return (
    <div className="report-card" style={showUnreadDot ? { position: "relative" } : undefined}>
      {showUnreadDot && (
        <span title="Unread" style={{
          position: "absolute", top: 12, right: 12, width: 8, height: 8,
          borderRadius: "50%", background: "var(--primary)",
          boxShadow: "0 0 0 3px rgba(200,155,60,0.18)",
        }} />
      )}
      <div className="report-card-body">
        {/* Crisis chip + Day badge above the title — primary IA cue. */}
        {(r.crisisTitle || r.dayNumber != null) && (
          <div style={{
            display: "flex", flexWrap: "wrap", gap: 6, marginBottom: 8, alignItems: "center",
          }}>
            {r.crisisTitle && (
              <button type="button"
                      onClick={onCrisisClick && r.crisisId ? (e) => { e.stopPropagation(); onCrisisClick(r.crisisId); } : undefined}
                      style={{
                        padding: "2px 8px", borderRadius: 999,
                        fontSize: 11, fontWeight: 600,
                        background: "rgba(200,155,60,0.12)",
                        color: "var(--primary)",
                        border: "1px solid rgba(200,155,60,0.35)",
                        cursor: onCrisisClick ? "pointer" : "default",
                      }}>
                {r.crisisTitle}
              </button>
            )}
            {r.dayNumber != null && (
              <span style={{
                padding: "2px 8px", borderRadius: 999,
                fontSize: 11, fontWeight: 700,
                background: "var(--surface2)", color: "var(--text)",
                border: "1px solid var(--border)",
                fontVariantNumeric: "tabular-nums",
              }}>Day {r.dayNumber}</span>
            )}
          </div>
        )}
        <div className="report-card-title">{r.title}</div>
        {r.summary && <div className="report-card-summary">{r.summary}</div>}
        <div className="report-card-meta">
          <span>{fmtDate(r.publishedAt)}</span>
          <span className="report-card-meta-sep">·</span>
          <span>{fileTypeLabel(r.fileType)}</span>
          <span className="report-card-meta-sep">·</span>
          <span>{fmtBytes(r.fileSize)}</span>
          {!r.downloadable && <>
            <span className="report-card-meta-sep">·</span>
            <span style={{ color: "var(--warn)" }}>View-only</span>
          </>}
          {r.isFreeSample && <>
            <span className="report-card-meta-sep">·</span>
            <span style={{ color: "var(--primary)" }}>Free sample</span>
          </>}
        </div>
      </div>
      <div className="report-card-actions">
        <Btn variant="ghost" size="sm" onClick={onView}>View</Btn>
        {r.downloadable && (
          <a href={`/api/reports/${r.id}/file?download=1`}
             className="btn btn-ghost btn-sm" download={r.fileName}
             style={{ textDecoration: "none" }}>
            Download
          </a>
        )}
        {isAdmin && (
          <div style={{ marginLeft: "auto", display: "flex", gap: 4 }}>
            <Btn variant="ghost" size="sm" onClick={onEdit} title="Edit">Edit</Btn>
            <Btn variant="ghost" size="sm" onClick={onArchive} title={r.archived ? "Restore" : "Archive"}>
              {r.archived ? "Restore" : "Archive"}
            </Btn>
            <Btn variant="danger" size="sm" onClick={onDelete} title="Delete">Delete</Btn>
          </div>
        )}
      </div>
    </div>
  );
}

// =============================================================================
// Report preview modal
// =============================================================================

function ReportPreviewModal({ report, open, onClose }) {
  const r = report;
  const url = `/api/reports/${r.id}/file`;
  const kind = previewType(r.fileType);
  const [textContent, setTextContent] = useState(null);

  useEffect(() => {
    if (kind === "text" && open) {
      fetch(url, { credentials: "include" }).then(res => res.text()).then(setTextContent).catch(() => setTextContent("(could not load)"));
    } else {
      setTextContent(null);
    }
  }, [open, url, kind]);

  return (
    <Modal open={open} onClose={onClose} title={r.title} size="xl" footer={
      <>
        <Btn variant="ghost" onClick={onClose}>Close</Btn>
        {r.downloadable && (
          <a href={`${url}?download=1`} className="btn btn-primary" download={r.fileName}
             style={{ textDecoration: "none" }}>Download</a>
        )}
      </>
    }>
      <div style={{ minHeight: 400 }}>
        {(r.crisisTitle || r.dayNumber != null) && (
          <div style={{ display: "flex", flexWrap: "wrap", gap: 8, marginBottom: 12, alignItems: "center" }}>
            {r.crisisTitle && (
              <span style={{
                padding: "3px 10px", borderRadius: 999, fontSize: 12, fontWeight: 600,
                background: "rgba(200,155,60,0.12)", color: "var(--primary)",
                border: "1px solid rgba(200,155,60,0.35)",
              }}>{r.crisisTitle}</span>
            )}
            {r.dayNumber != null && (
              <span style={{
                padding: "3px 10px", borderRadius: 999, fontSize: 12, fontWeight: 700,
                background: "var(--surface2)", color: "var(--text)",
                border: "1px solid var(--border)", fontVariantNumeric: "tabular-nums",
              }}>Day {r.dayNumber}</span>
            )}
          </div>
        )}
        {r.summary && (
          <div style={{ marginBottom: 14, fontSize: 14, color: "var(--subtext)", lineHeight: 1.6 }}>
            {r.summary}
          </div>
        )}
        {r.tags && r.tags.length > 0 && (
          <div style={{ marginBottom: 14 }}>
            {r.tags.map(t => <span key={t} className="tag">{t}</span>)}
          </div>
        )}
        <div style={{ fontSize: 12, color: "var(--muted)", marginBottom: 14 }}>
          {fileEmoji(r.fileType)} {r.fileName} · {fmtBytes(r.fileSize)} · published {fmtDate(r.publishedAt)}
        </div>
        <div style={{ background: "var(--surface2)", borderRadius: 10, overflow: "hidden", border: "1px solid var(--border)" }}>
          {kind === "pdf" && (
            <iframe src={url} style={{ width: "100%", height: "70vh", border: "none" }} title={r.title} />
          )}
          {kind === "image" && (
            <img src={url} alt={r.title} style={{ width: "100%", height: "auto", display: "block" }} />
          )}
          {kind === "video" && (
            <video src={url} controls style={{ width: "100%", maxHeight: "70vh", display: "block" }} />
          )}
          {kind === "audio" && (
            <div style={{ padding: 30, textAlign: "center" }}>
              <audio src={url} controls style={{ width: "100%", maxWidth: 500 }} />
            </div>
          )}
          {kind === "text" && (
            <pre style={{
              padding: 16, fontSize: 13, color: "var(--text)", background: "var(--surface2)",
              maxHeight: "60vh", overflowY: "auto", whiteSpace: "pre-wrap", fontFamily: "ui-monospace, monospace",
            }}>{textContent || "Loading…"}</pre>
          )}
          {kind === "none" && (
            <div style={{ padding: 60, textAlign: "center", color: "var(--subtext)" }}>
              <div style={{ fontSize: 48, marginBottom: 10 }}>{fileEmoji(r.fileType)}</div>
              <div style={{ fontSize: 14, marginBottom: 6 }}>No inline preview for this file type.</div>
              {r.downloadable
                ? <div style={{ fontSize: 13, color: "var(--muted)" }}>Use the Download button below.</div>
                : <div style={{ fontSize: 13, color: "var(--warn)" }}>This report is view-only and has no inline preview available for its format.</div>}
            </div>
          )}
        </div>
      </div>
    </Modal>
  );
}

// =============================================================================
// Report upload + edit modals
// =============================================================================

function ReportUploadModal({ open, onClose, onUploaded, defaultDownloadable }) {
  const [file, setFile] = useState(null);
  const [title, setTitle] = useState("");
  const [summary, setSummary] = useState("");
  const [tags, setTags] = useState("");
  const [downloadable, setDownloadable] = useState(defaultDownloadable);
  const [isFreeSample, setIsFreeSample] = useState(false);
  const [folderId, setFolderId] = useState("");
  const [folders, setFolders] = useState([]);
  // Crisis taxonomy. Empty crisisId = one-off event.
  const [crises, setCrises] = useState([]);          // [{id, title, active}]
  const [crisisId, setCrisisId] = useState("");
  const [dayOverride, setDayOverride] = useState(""); // "" = auto-derive from title
  const [notifyChat, setNotifyChat] = useState(false);
  const [notifyEmail, setNotifyEmail] = useState(false);
  // Scheduling: when enabled, the upload goes to /api/admin/reports/scheduled
  // with a scheduledFor ISO timestamp. The cron worker publishes it later.
  const [schedule, setSchedule] = useState(false);
  const [scheduledLocal, setScheduledLocal] = useState("");  // <input type="datetime-local"> value
  const [busy, setBusy] = useState(false);
  const [drag, setDrag] = useState(false);

  useEffect(() => {
    if (open) {
      setDownloadable(defaultDownloadable);
      setIsFreeSample(false);
      setFolderId("");
      setCrisisId("");
      setDayOverride("");
      setNotifyChat(false);
      setNotifyEmail(false);
      setSchedule(false);
      // Default scheduled time = next round hour, local
      const d = new Date(Date.now() + 60 * 60 * 1000);
      d.setMinutes(0, 0, 0);
      const pad = (n) => String(n).padStart(2, "0");
      setScheduledLocal(
        `${d.getFullYear()}-${pad(d.getMonth() + 1)}-${pad(d.getDate())}T${pad(d.getHours())}:${pad(d.getMinutes())}`
      );
      api("GET", "/api/folders").then(d => setFolders(d.folders || [])).catch(() => setFolders([]));
      // Pull crises (admin endpoint includes inactive too — useful for back-dating).
      api("GET", "/api/admin/crisis")
        .then(d => setCrises((d.pages || []).map(p => ({ id: p.id, title: p.title, active: p.active }))))
        .catch(() => setCrises([]));
    }
  }, [open, defaultDownloadable]);

  // Auto-detect Day N from the title (only relevant when a crisis is selected).
  // Computed reactively so the user sees the inferred number as they type.
  const detectedDay = (() => {
    if (!crisisId) return null;
    const m = (title || "").match(/\bDay\s+(\d{1,4})\b/i);
    return m ? parseInt(m[1], 10) : null;
  })();

  const folderOptions = flattenFoldersForDropdown(buildFolderTree(folders));

  function pickFile(f) {
    if (!f) return;
    if (f.size > UPLOAD_MAX_BYTES) {
      showToast(`File too large (max ${fmtBytes(UPLOAD_MAX_BYTES)})`, "error");
      return;
    }
    setFile(f);
    if (!title) setTitle(f.name.replace(/\.[^.]+$/, ""));
  }

  async function handleSubmit(e) {
    e.preventDefault();
    if (!file) { showToast("Pick a file first", "error"); return; }
    if (!title.trim()) { showToast("Title is required", "error"); return; }
    let scheduledIso = null;
    if (schedule) {
      if (!scheduledLocal) {
        showToast("Pick a date and time", "error");
        return;
      }
      const dt = new Date(scheduledLocal);
      if (isNaN(dt.getTime())) { showToast("Invalid date/time", "error"); return; }
      if (dt.getTime() < Date.now() - 60_000) {
        showToast("Schedule time must be in the future", "error");
        return;
      }
      scheduledIso = dt.toISOString();
    }
    setBusy(true);
    try {
      const fd = new FormData();
      fd.append("file", file);
      fd.append("title", title.trim());
      if (summary.trim()) fd.append("summary", summary.trim());
      if (tags.trim()) fd.append("tags", tags);
      fd.append("downloadable", downloadable ? "1" : "0");
      fd.append("isFreeSample", isFreeSample ? "1" : "0");
      if (folderId) fd.append("folderId", folderId);
      // Crisis taxonomy. Empty crisisId = one-off; backend treats empty string as null.
      if (crisisId) fd.append("crisisId", crisisId);
      // dayOverride wins over title-parse; otherwise let backend auto-detect.
      if (dayOverride.trim() !== "") fd.append("dayNumber", dayOverride.trim());
      fd.append("notifyChat", notifyChat ? "1" : "0");
      fd.append("notifyEmail", notifyEmail ? "1" : "0");
      if (scheduledIso) {
        fd.append("scheduledFor", scheduledIso);
        await api("POST", "/api/admin/reports/scheduled", fd);
        showToast(`Scheduled for ${new Date(scheduledIso).toLocaleString()}`, "success");
      } else {
        await api("POST", "/api/admin/reports", fd);
        showToast("Report uploaded", "success");
      }
      setFile(null); setTitle(""); setSummary(""); setTags("");
      onUploaded();
    } catch (e) {
      showToast(e.message, "error");
    } finally { setBusy(false); }
  }

  return (
    <Modal open={open} onClose={busy ? () => {} : onClose} title="Upload report" size="wide" footer={
      <>
        <Btn variant="ghost" onClick={onClose} disabled={busy}>Cancel</Btn>
        <Btn onClick={handleSubmit} disabled={busy || !file}>
          {busy ? <Spinner /> : (schedule ? "Schedule" : "Upload")}
        </Btn>
      </>
    }>
      <form onSubmit={handleSubmit}>
        <div onDragOver={e => { e.preventDefault(); setDrag(true); }}
             onDragLeave={() => setDrag(false)}
             onDrop={e => {
               e.preventDefault(); setDrag(false);
               if (e.dataTransfer.files[0]) pickFile(e.dataTransfer.files[0]);
             }}
             style={{
               border: `2px dashed ${drag ? "var(--primary)" : "var(--border)"}`,
               borderRadius: 12,
               padding: 26, textAlign: "center",
               background: drag ? "rgba(200,155,60,0.08)" : "var(--surface2)",
               marginBottom: 16, cursor: "pointer",
               transition: "border-color 0.15s, background 0.15s",
             }}
             onClick={() => document.getElementById("upload-file-input").click()}>
          {file ? (
            <div>
              <div style={{ fontSize: 24, marginBottom: 6 }}>{fileEmoji(file.type)}</div>
              <div style={{ fontSize: 14, fontWeight: 600 }}>{file.name}</div>
              <div style={{ fontSize: 12, color: "var(--subtext)", marginTop: 4 }}>
                {fmtBytes(file.size)} · {file.type || "unknown"}
              </div>
              <button type="button" onClick={(e) => { e.stopPropagation(); setFile(null); }}
                      style={{ marginTop: 8, fontSize: 12, color: "var(--muted)", background: "none", border: "none", cursor: "pointer" }}>
                Remove
              </button>
            </div>
          ) : (
            <div>
              <div style={{ fontSize: 28, marginBottom: 6 }}>📤</div>
              <div style={{ fontSize: 14, color: "var(--subtext)" }}>Drag a file here or click to pick</div>
              <div style={{ fontSize: 11, color: "var(--muted)", marginTop: 4 }}>Max {fmtBytes(UPLOAD_MAX_BYTES)}</div>
            </div>
          )}
          <input id="upload-file-input" type="file" style={{ display: "none" }}
                 accept={ACCEPTED_TYPES || undefined}
                 onChange={e => pickFile(e.target.files[0])} />
        </div>

        <Field label="Title" htmlFor="up-title">
          <input id="up-title" type="text" required
                 value={title} onChange={e => setTitle(e.target.value)} />
        </Field>
        <Field label="Summary (optional)" htmlFor="up-summary">
          <textarea id="up-summary" rows={3}
                    placeholder="Short description shown to readers"
                    value={summary} onChange={e => setSummary(e.target.value)} />
        </Field>
        <Field label="Crisis" htmlFor="up-crisis"
               hint="Tie this report to an ongoing crisis (Hormuz, Suez, …). Leave blank if it's a one-off event (e.g. Japan earthquake).">
          <select id="up-crisis" value={crisisId}
                  onChange={e => setCrisisId(e.target.value)}>
            <option value="">— One-off event (no crisis) —</option>
            {crises.map(c => (
              <option key={c.id} value={c.id}>
                {c.title}{!c.active ? " (inactive)" : ""}
              </option>
            ))}
          </select>
        </Field>
        {crisisId && (
          <Field label="Day number" htmlFor="up-day"
                 hint={detectedDay != null
                   ? `Auto-detected "Day ${detectedDay}" from the title — leave the override blank to use it.`
                   : "Optional. Use a number for crisis updates (Day 1, Day 47…). Leave blank for crisis-background pieces with no Day designation."}>
            <input id="up-day" type="number" min={0} max={9999}
                   placeholder={detectedDay != null ? String(detectedDay) : "e.g. 47"}
                   value={dayOverride}
                   onChange={e => setDayOverride(e.target.value)}
                   style={{ width: 140 }} />
          </Field>
        )}
        <Field label="Folder (optional, advanced)" htmlFor="up-folder"
               hint="Used for stable reference docs. Most reports should live under a Crisis instead.">
          <select id="up-folder" value={folderId}
                  onChange={e => setFolderId(e.target.value)}>
            <option value="">— Uncategorized —</option>
            {folderOptions.map(o => <option key={o.id} value={o.id}>{o.label}</option>)}
          </select>
        </Field>
        <Field label="Tags (optional, comma-separated)" htmlFor="up-tags">
          <input id="up-tags" type="text" placeholder="e.g. Hormuz, Crude, SITREP"
                 value={tags} onChange={e => setTags(e.target.value)} />
        </Field>
        <Field>
          <label style={{ display: "flex", alignItems: "center", gap: 10, cursor: "pointer", textTransform: "none", letterSpacing: 0, fontSize: 14, color: "var(--text)" }}>
            <input type="checkbox" checked={downloadable}
                   onChange={e => setDownloadable(e.target.checked)}
                   style={{ width: "auto", marginRight: 4 }} />
            Members can download this file (otherwise: view-only)
          </label>
        </Field>
        <Field>
          <label style={{ display: "flex", alignItems: "center", gap: 10, cursor: "pointer", textTransform: "none", letterSpacing: 0, fontSize: 14, color: "var(--text)" }}>
            <input type="checkbox" checked={isFreeSample}
                   onChange={e => setIsFreeSample(e.target.checked)}
                   style={{ width: "auto", marginRight: 4 }} />
            Make this a <strong>free-tier sample</strong> (visible to free-tier members)
          </label>
        </Field>

        <div style={{ marginTop: 8, padding: "12px 14px", background: "var(--surface2)", border: "1px solid var(--border)", borderRadius: 10 }}>
          <div style={{ fontSize: 11, fontWeight: 700, color: "var(--subtext)", textTransform: "uppercase", letterSpacing: "0.5px", marginBottom: 8 }}>
            Notify members when this is uploaded
          </div>
          <label style={{ display: "flex", alignItems: "center", gap: 10, cursor: "pointer", fontSize: 13, color: "var(--text)", marginBottom: 6 }}>
            <input type="checkbox" checked={notifyChat}
                   onChange={e => setNotifyChat(e.target.checked)}
                   style={{ width: "auto" }} />
            Post to chat (fires once chat module is wired)
          </label>
          <label style={{ display: "flex", alignItems: "center", gap: 10, cursor: "pointer", fontSize: 13, color: "var(--text)" }}>
            <input type="checkbox" checked={notifyEmail}
                   onChange={e => setNotifyEmail(e.target.checked)}
                   style={{ width: "auto" }} />
            Email all members (fires once Resend is configured)
          </label>
          {(notifyChat || notifyEmail) && (
            <div style={{ fontSize: 11, color: "var(--warn)", marginTop: 8 }}>
              Note: chat + email modules aren't live yet. Your selection is captured and will fire once those Phase-3 features ship.
            </div>
          )}
        </div>

        <div style={{ marginTop: 12, padding: "12px 14px", background: "var(--surface2)", border: "1px solid var(--border)", borderRadius: 10 }}>
          <label style={{ display: "flex", alignItems: "center", gap: 10, cursor: "pointer", fontSize: 13, color: "var(--text)" }}>
            <input type="checkbox" checked={schedule}
                   onChange={e => setSchedule(e.target.checked)}
                   style={{ width: "auto" }} />
            <strong>Schedule for later</strong>
            <span style={{ fontSize: 11, color: "var(--muted)", fontWeight: 400 }}>
              — file uploads now, publishes at the chosen time
            </span>
          </label>
          {schedule && (
            <div style={{ marginTop: 10 }}>
              <label htmlFor="up-scheduled" style={{
                display: "block", fontSize: 11, fontWeight: 700,
                color: "var(--subtext)", textTransform: "uppercase",
                letterSpacing: "0.5px", marginBottom: 6,
              }}>Publish at (your local time)</label>
              <input id="up-scheduled" type="datetime-local"
                     value={scheduledLocal}
                     onChange={e => setScheduledLocal(e.target.value)}
                     style={{ width: "100%" }} />
              <div style={{ marginTop: 6, fontSize: 11, color: "var(--muted)" }}>
                {scheduledLocal && !isNaN(new Date(scheduledLocal).getTime()) ? (
                  <>Will fire ≈ {new Date(scheduledLocal).toLocaleString()} ({Math.max(0, Math.round((new Date(scheduledLocal).getTime() - Date.now()) / 60000))} min from now)</>
                ) : "Pick a future time"}
              </div>
            </div>
          )}
        </div>
      </form>
    </Modal>
  );
}

function ReportEditModal({ report, open, onClose, onUpdated, folders }) {
  const r = report;
  const [title, setTitle] = useState(r.title);
  const [summary, setSummary] = useState(r.summary || "");
  const [tags, setTags] = useState((r.tags || []).join(", "));
  const [downloadable, setDownloadable] = useState(!!r.downloadable);
  const [isFreeSample, setIsFreeSample] = useState(!!r.isFreeSample);
  const [folderId, setFolderId] = useState(r.folderId || "");
  const [crisisId, setCrisisId] = useState(r.crisisId || "");
  const [dayNumber, setDayNumber] = useState(r.dayNumber != null ? String(r.dayNumber) : "");
  const [crises, setCrises] = useState([]);
  const [busy, setBusy] = useState(false);

  const folderOptions = flattenFoldersForDropdown(buildFolderTree(folders || []));

  useEffect(() => {
    api("GET", "/api/admin/crisis")
      .then(d => setCrises((d.pages || []).map(p => ({ id: p.id, title: p.title, active: p.active }))))
      .catch(() => setCrises([]));
  }, []);

  async function handleSubmit(e) {
    e.preventDefault();
    if (!title.trim()) { showToast("Title is required", "error"); return; }
    setBusy(true);
    try {
      const patch = {
        title: title.trim(),
        summary: summary.trim(),
        tags: tags.split(",").map(s => s.trim()).filter(Boolean),
        downloadable,
        isFreeSample,
        folderId: folderId || null,
        crisisId: crisisId || null,
      };
      // Day number — empty string clears it; numeric override sets it.
      if (dayNumber.trim() === "") {
        patch.dayNumber = null;
      } else {
        const n = parseInt(dayNumber, 10);
        if (!Number.isFinite(n) || n < 0 || n > 9999) {
          showToast("Day number must be 0–9999", "error"); setBusy(false); return;
        }
        patch.dayNumber = n;
      }
      await api("PATCH", `/api/admin/reports/${r.id}`, patch);
      showToast("Saved", "success");
      onUpdated();
    } catch (e) { showToast(e.message, "error"); }
    finally { setBusy(false); }
  }

  return (
    <Modal open={open} onClose={busy ? () => {} : onClose} title="Edit report" footer={
      <>
        <Btn variant="ghost" onClick={onClose} disabled={busy}>Cancel</Btn>
        <Btn onClick={handleSubmit} disabled={busy}>{busy ? <Spinner /> : "Save"}</Btn>
      </>
    }>
      <form onSubmit={handleSubmit}>
        <Field label="Title" htmlFor="ed-title">
          <input id="ed-title" type="text" required
                 value={title} onChange={e => setTitle(e.target.value)} />
        </Field>
        <Field label="Summary" htmlFor="ed-summary">
          <textarea id="ed-summary" rows={3} value={summary} onChange={e => setSummary(e.target.value)} />
        </Field>
        <Field label="Tags (comma-separated)" htmlFor="ed-tags">
          <input id="ed-tags" type="text" value={tags} onChange={e => setTags(e.target.value)} />
        </Field>
        <Field label="Crisis" htmlFor="ed-crisis"
               hint="Tie this report to an ongoing crisis, or leave blank to make it a one-off event.">
          <select id="ed-crisis" value={crisisId}
                  onChange={e => setCrisisId(e.target.value)}>
            <option value="">— One-off event (no crisis) —</option>
            {crises.map(c => (
              <option key={c.id} value={c.id}>{c.title}{!c.active ? " (inactive)" : ""}</option>
            ))}
          </select>
        </Field>
        {crisisId && (
          <Field label="Day number" htmlFor="ed-day"
                 hint="Position in the crisis series. Leave blank for crisis-background pieces with no Day designation.">
            <input id="ed-day" type="number" min={0} max={9999}
                   placeholder="e.g. 47"
                   value={dayNumber}
                   onChange={e => setDayNumber(e.target.value)}
                   style={{ width: 140 }} />
          </Field>
        )}
        <Field label="Folder (advanced)" htmlFor="ed-folder"
               hint="Used for stable reference docs. Most reports should live under a Crisis instead.">
          <select id="ed-folder" value={folderId}
                  onChange={e => setFolderId(e.target.value)}>
            <option value="">— Uncategorized —</option>
            {folderOptions.map(o => <option key={o.id} value={o.id}>{o.label}</option>)}
          </select>
        </Field>
        <Field>
          <label style={{ display: "flex", alignItems: "center", gap: 10, cursor: "pointer", textTransform: "none", letterSpacing: 0, fontSize: 14, color: "var(--text)" }}>
            <input type="checkbox" checked={downloadable}
                   onChange={e => setDownloadable(e.target.checked)}
                   style={{ width: "auto", marginRight: 4 }} />
            Members can download this file
          </label>
        </Field>
        <Field>
          <label style={{ display: "flex", alignItems: "center", gap: 10, cursor: "pointer", textTransform: "none", letterSpacing: 0, fontSize: 14, color: "var(--text)" }}>
            <input type="checkbox" checked={isFreeSample}
                   onChange={e => setIsFreeSample(e.target.checked)}
                   style={{ width: "auto", marginRight: 4 }} />
            Free-tier sample (visible to free-tier members)
          </label>
        </Field>
        <div style={{ fontSize: 12, color: "var(--muted)", marginTop: 14, borderTop: "1px solid var(--border)", paddingTop: 12 }}>
          File: {r.fileName} · {fmtBytes(r.fileSize)} · {r.fileType || "unknown"}<br />
          Uploaded {fmtDateTime(r.publishedAt)}
        </div>
      </form>
    </Modal>
  );
}

// =============================================================================
// Chat — channels + messages with polling
// =============================================================================

function ChatView({ user, jumpToChannelId, onJumpHandled, onJumpToReport }) {
  const isAdmin = user.role === "admin";
  const [channels, setChannels] = useState(null);
  const [activeId, setActiveId] = useState(null);
  const [messages, setMessages] = useState([]);
  const [composer, setComposer] = useState("");
  const [sending, setSending] = useState(false);
  const [showChannelEdit, setShowChannelEdit] = useState(false);
  const [creatingChannel, setCreatingChannel] = useState(false);
  const [confirming, setConfirming] = useState(null);
  const [members, setMembers] = useState([]); // for @-mention autocomplete + rendering
  const [editingMessageId, setEditingMessageId] = useState(null);
  const messagesEndRef = useRef(null);
  const scrollContainerRef = useRef(null);
  const composerRef = useRef(null);

  const activeChannel = (channels || []).find(c => c.id === activeId) || null;
  const memberById = (() => {
    const m = new Map();
    (members || []).forEach(u => m.set(u.id, u));
    return m;
  })();

  // Load member directory once for @-mention autocomplete + rendering.
  useEffect(() => {
    api("GET", "/api/members").then(d => setMembers(d.members || [])).catch(() => {});
  }, []);

  // Allow notification clicks to jump to a specific channel.
  useEffect(() => {
    if (jumpToChannelId && channels) {
      const ch = channels.find(c => c.id === jumpToChannelId);
      if (ch) { setActiveId(ch.id); onJumpHandled && onJumpHandled(); }
    }
  }, [jumpToChannelId, channels, onJumpHandled]);

  // Load channels once at mount.
  useEffect(() => {
    api("GET", "/api/chat/channels").then(d => {
      const list = d.channels || [];
      setChannels(list);
      const def = list.find(c => c.is_default) || list[0];
      if (def) setActiveId(def.id);
    }).catch(e => { showToast(e.message, "error"); setChannels([]); });
  }, []);

  // Load initial messages whenever channel changes.
  useEffect(() => {
    if (!activeId) return;
    setMessages([]);
    api("GET", `/api/chat/channels/${activeId}/messages`).then(d => {
      setMessages(d.messages || []);
    }).catch(e => showToast(e.message, "error"));
  }, [activeId]);

  // Poll for new messages every 5s while channel is active.
  useEffect(() => {
    if (!activeId) return;
    const tick = async () => {
      try {
        const lastId = messages.length > 0 ? messages[messages.length - 1].id : null;
        const path = lastId
          ? `/api/chat/channels/${activeId}/messages?since=${lastId}`
          : `/api/chat/channels/${activeId}/messages`;
        const d = await api("GET", path);
        const fresh = d.messages || [];
        if (fresh.length > 0) {
          setMessages(prev => {
            // De-dupe in case of overlap
            const seen = new Set(prev.map(m => m.id));
            return [...prev, ...fresh.filter(m => !seen.has(m.id))];
          });
        }
      } catch { /* network blip — ignore */ }
    };
    const interval = setInterval(tick, 5000);
    return () => clearInterval(interval);
  }, [activeId, messages]);

  // Auto-scroll to bottom when messages arrive (only if user is near bottom).
  useEffect(() => {
    const c = scrollContainerRef.current;
    if (!c) return;
    const nearBottom = c.scrollHeight - c.scrollTop - c.clientHeight < 200;
    if (nearBottom && messagesEndRef.current) {
      messagesEndRef.current.scrollIntoView({ behavior: "smooth" });
    }
  }, [messages]);

  async function sendMessage(e) {
    if (e) e.preventDefault();
    const text = composer.trim();
    if (!text || !activeId || sending) return;
    setSending(true);
    try {
      const r = await api("POST", `/api/chat/channels/${activeId}/messages`, { body: text });
      setMessages(prev => [...prev, r.message]);
      setComposer("");
    } catch (e) {
      showToast(e.message, "error");
    } finally { setSending(false); }
  }

  function onComposerKey(e) {
    if (e.key === "Enter" && !e.shiftKey) {
      e.preventDefault();
      sendMessage();
    }
  }

  async function deleteMessage(m) {
    setConfirming({
      title: "Delete message?",
      message: "This message will be permanently deleted.",
      confirmLabel: "Delete",
      danger: true,
      action: async () => {
        await api("DELETE", `/api/chat/messages/${m.id}`);
        setMessages(prev => prev.filter(x => x.id !== m.id));
      },
    });
  }

  async function saveEdit(messageId, newBody) {
    try {
      const r = await api("PATCH", `/api/chat/messages/${messageId}`, { body: newBody });
      setMessages(prev => prev.map(x => x.id === messageId ? r.message : x));
      setEditingMessageId(null);
      showToast("Message updated", "success");
    } catch (e) { showToast(e.message, "error"); }
  }

  async function refreshChannels() {
    const d = await api("GET", "/api/chat/channels");
    setChannels(d.channels || []);
  }

  return (
    <div style={{ display: "flex", flex: 1, overflow: "hidden" }}>
      {/* Channel sidebar */}
      <div style={{
        width: 220, flexShrink: 0,
        background: "var(--surface)",
        borderRight: "1px solid var(--border)",
        display: "flex", flexDirection: "column",
      }}>
        <div style={{
          padding: "14px 16px", borderBottom: "1px solid var(--border)",
          display: "flex", alignItems: "center", justifyContent: "space-between",
        }}>
          <div style={{ fontSize: 12, fontWeight: 700, color: "var(--subtext)", textTransform: "uppercase", letterSpacing: "0.5px" }}>
            Channels
          </div>
          {isAdmin && (
            <button onClick={() => setCreatingChannel(true)} title="New channel" style={{
              background: "transparent", border: "none", color: "var(--primary)",
              cursor: "pointer", fontSize: 18, padding: 0, lineHeight: 1,
            }}>+</button>
          )}
        </div>
        <div style={{ flex: 1, overflowY: "auto", padding: 8 }}>
          {channels === null ? (
            <div className="skeleton" style={{ height: 36, margin: "4px 0" }} />
          ) : channels.length === 0 ? (
            <div style={{ padding: 16, fontSize: 12, color: "var(--muted)", textAlign: "center" }}>
              No channels yet.
            </div>
          ) : (
            channels.map(c => {
              const active = c.id === activeId;
              return (
                <button key={c.id} onClick={() => setActiveId(c.id)} style={{
                  display: "flex", alignItems: "center", gap: 6,
                  width: "100%", padding: "8px 10px", marginBottom: 2,
                  borderRadius: 6, fontSize: 14, textAlign: "left",
                  background: active ? "var(--surface2)" : "transparent",
                  color: active ? "var(--text)" : "var(--subtext)",
                  border: "1px solid " + (active ? "var(--border)" : "transparent"),
                  cursor: "pointer",
                }}>
                  <span style={{ color: "var(--muted)" }}>#</span>
                  <span style={{ flex: 1, overflow: "hidden", textOverflow: "ellipsis", whiteSpace: "nowrap" }}>
                    {c.display_name}
                  </span>
                  {c.is_default && (
                    <span title="Default channel — auto-announces post here" style={{ fontSize: 10 }}>⭐</span>
                  )}
                </button>
              );
            })
          )}
        </div>
      </div>

      {/* Message stream + composer */}
      <div style={{ flex: 1, display: "flex", flexDirection: "column", overflow: "hidden" }}>
        {activeChannel ? (
          <>
            <div style={{
              padding: "16px 24px", borderBottom: "1px solid var(--border)",
              display: "flex", alignItems: "center", justifyContent: "space-between", flexShrink: 0,
            }}>
              <div>
                <div style={{ fontSize: 16, fontWeight: 700, color: "var(--text)" }}>
                  # {activeChannel.display_name}
                </div>
                {activeChannel.description && (
                  <div style={{ fontSize: 12, color: "var(--subtext)", marginTop: 2 }}>
                    {activeChannel.description}
                  </div>
                )}
              </div>
              {isAdmin && (
                <Btn variant="ghost" size="sm" onClick={() => setShowChannelEdit(true)}>Edit channel</Btn>
              )}
            </div>

            <div ref={scrollContainerRef} style={{ flex: 1, overflowY: "auto", padding: "16px 24px" }}>
              {messages.length === 0 ? (
                <div className="empty-state" style={{ padding: "40px 0" }}>
                  <h3>No messages yet</h3>
                  <p>Be the first to post in #{activeChannel.display_name}</p>
                </div>
              ) : (
                messages.map((m, i) => {
                  const prev = messages[i - 1];
                  const sameAuthor = prev && prev.user && m.user && prev.user.id === m.user.id;
                  const closeInTime = prev && (new Date(m.createdAt) - new Date(prev.createdAt) < 5 * 60 * 1000);
                  const grouped = sameAuthor && closeInTime && m.type === "message" && prev.type === "message";
                  const isOwner = m.user && m.user.id === user.id;
                  const canDelete = isAdmin || isOwner;
                  // Edit window: 10 min for owners, always for admins.
                  const ageMs = Date.now() - new Date(m.createdAt + (m.createdAt && m.createdAt.includes("T") ? "" : "Z")).getTime();
                  const canEdit = m.type === "message" && (isAdmin || (isOwner && ageMs < 10 * 60 * 1000));
                  // Admin can delete system/announce messages too (member-authored
                  // messages still use the existing canDelete logic above).
                  const isAnnounce = m.type === "report_announce";
                  const announceCanDelete = isAdmin;
                  return (
                    <ChatMessageRow key={m.id} message={m} grouped={grouped}
                                    memberById={memberById}
                                    canDelete={isAnnounce ? announceCanDelete : canDelete}
                                    canEdit={canEdit}
                                    isEditing={editingMessageId === m.id}
                                    onStartEdit={() => setEditingMessageId(m.id)}
                                    onCancelEdit={() => setEditingMessageId(null)}
                                    onSaveEdit={(text) => saveEdit(m.id, text)}
                                    onDelete={() => deleteMessage(m)}
                                    onJumpToReport={onJumpToReport} />
                  );
                })
              )}
              <div ref={messagesEndRef} />
            </div>

            <ChatComposer
              composerRef={composerRef}
              value={composer} onChange={setComposer}
              onKeyDown={onComposerKey}
              onSend={sendMessage}
              members={members}
              channelName={activeChannel.display_name}
              channelId={activeChannel.id}
              isAdmin={isAdmin}
              sending={sending}
              onScheduled={() => setComposer("")}
            />
          </>
        ) : channels !== null ? (
          <div className="empty-state" style={{ marginTop: 60 }}>
            <h3>No channels available</h3>
            <p>{isAdmin ? "Click the + button in the sidebar to create one." : "Ask your admin to create a channel."}</p>
          </div>
        ) : (
          <div style={{ padding: 24 }}><div className="skeleton" style={{ height: 60 }} /></div>
        )}
      </div>

      {creatingChannel && (
        <ChannelCreateModal
          open={creatingChannel}
          onClose={() => setCreatingChannel(false)}
          onCreated={(ch) => { setCreatingChannel(false); refreshChannels(); setActiveId(ch.id); }}
        />
      )}
      {showChannelEdit && activeChannel && (
        <ChannelEditModal
          channel={activeChannel}
          open={showChannelEdit}
          onClose={() => setShowChannelEdit(false)}
          onUpdated={() => { setShowChannelEdit(false); refreshChannels(); }}
          onDeleted={() => { setShowChannelEdit(false); setActiveId(null); refreshChannels(); }}
        />
      )}
      {confirming && (
        <ConfirmDialog open={!!confirming}
                       title={confirming.title}
                       message={confirming.message}
                       confirmLabel={confirming.confirmLabel}
                       danger={confirming.danger}
                       onConfirm={confirming.action}
                       onClose={() => setConfirming(null)} />
      )}
    </div>
  );
}

// Render message body with `<@user-id>` mention markers replaced by chips.
function renderMessageBody(body, memberById) {
  const parts = [];
  const re = /<@([a-f0-9]{32})>/g;
  let lastIndex = 0;
  let m;
  let key = 0;
  while ((m = re.exec(body)) !== null) {
    if (m.index > lastIndex) parts.push(<span key={key++}>{body.slice(lastIndex, m.index)}</span>);
    const member = memberById && memberById.get(m[1]);
    const display = member ? member.displayName : "member";
    parts.push(
      <span key={key++} style={{
        display: "inline-block", padding: "0 6px", borderRadius: 4,
        background: "rgba(200,155,60,0.2)", color: "var(--primary)", fontWeight: 600,
      }}>@{display}</span>
    );
    lastIndex = re.lastIndex;
  }
  if (lastIndex < body.length) parts.push(<span key={key++}>{body.slice(lastIndex)}</span>);
  return parts;
}

function ChatMessageRow({ message, grouped, memberById, canDelete, canEdit, isEditing, onStartEdit, onCancelEdit, onSaveEdit, onDelete, onJumpToReport }) {
  const m = message;
  const [editText, setEditText] = useState(m.body);
  useEffect(() => { setEditText(m.body); }, [m.body, isEditing]);

  const isSystem = m.type !== "message";
  const senderName = m.user ? m.user.displayName : "System";
  const isAdminSender = m.user && m.user.role === "admin";
  const initials = (senderName || "?")
    .split(/\s+/).map(w => w[0]).filter(Boolean).slice(0, 2).join("").toUpperCase() || "?";

  if (isSystem) {
    const reportId = m.metadata && m.metadata.reportId;
    const reportTitle = m.metadata && m.metadata.reportTitle;
    const isClickable = !!reportId && !!onJumpToReport;
    return (
      <div
        onClick={isClickable ? () => onJumpToReport(reportId) : undefined}
        role={isClickable ? "button" : undefined}
        tabIndex={isClickable ? 0 : undefined}
        onKeyDown={isClickable ? (e) => { if (e.key === "Enter" || e.key === " ") { e.preventDefault(); onJumpToReport(reportId); } } : undefined}
        style={{
          margin: "10px 0",
          padding: "12px 16px",
          background: "rgba(200,155,60,0.08)",
          border: "1px solid rgba(200,155,60,0.28)",
          borderRadius: 6,
          fontSize: 13,
          cursor: isClickable ? "pointer" : "default",
          transition: "background 0.15s ease, border-color 0.15s ease",
          position: "relative",
        }}
        onMouseEnter={isClickable ? (e) => {
          e.currentTarget.style.background = "rgba(200,155,60,0.14)";
          e.currentTarget.style.borderColor = "var(--primary)";
        } : undefined}
        onMouseLeave={isClickable ? (e) => {
          e.currentTarget.style.background = "rgba(200,155,60,0.08)";
          e.currentTarget.style.borderColor = "rgba(200,155,60,0.28)";
        } : undefined}
      >
        <div style={{ display: "flex", alignItems: "center", gap: 8 }}>
          <span style={{ fontSize: 14 }}>📥</span>
          <strong style={{ color: "var(--primary)", flex: 1 }}>{m.body}</strong>
          {isClickable && (
            <span style={{ fontSize: 12, color: "var(--primary)", opacity: 0.7 }}>View →</span>
          )}
          {canDelete && (
            <button
              onClick={(e) => { e.stopPropagation(); onDelete(); }}
              title="Delete announcement"
              style={{
                background: "transparent", border: "none",
                color: "var(--muted)", cursor: "pointer",
                fontSize: 14, padding: "0 4px", lineHeight: 1,
              }}>×</button>
          )}
        </div>
        <div style={{ marginTop: 4, fontSize: 11, color: "var(--muted)" }}>
          {fmtDateTime(m.createdAt)}
        </div>
      </div>
    );
  }

  function onEditKey(e) {
    if (e.key === "Enter" && !e.shiftKey) {
      e.preventDefault();
      onSaveEdit(editText.trim());
    } else if (e.key === "Escape") {
      e.preventDefault();
      onCancelEdit();
    }
  }

  return (
    <div style={{
      display: "flex", gap: 10,
      padding: grouped ? "1px 0" : "8px 0",
      marginTop: grouped ? 0 : 4,
    }} className="chat-msg-row">
      <div style={{ width: 32, flexShrink: 0 }}>
        {!grouped && (
          <div style={{
            width: 32, height: 32, borderRadius: "50%",
            background: "var(--surface3)", color: "var(--text)",
            display: "flex", alignItems: "center", justifyContent: "center",
            fontSize: 12, fontWeight: 700,
          }}>{initials}</div>
        )}
      </div>
      <div style={{ flex: 1, minWidth: 0 }}>
        {!grouped && (
          <div style={{ display: "flex", alignItems: "baseline", gap: 8, marginBottom: 2 }}>
            <span style={{ fontSize: 14, fontWeight: 700, color: "var(--text)" }}>{senderName}</span>
            {isAdminSender && (
              <span style={{ fontSize: 10, fontWeight: 700, color: "var(--primary)", textTransform: "uppercase" }}>admin</span>
            )}
            <span style={{ fontSize: 11, color: "var(--muted)" }}>{fmtDateTime(m.createdAt)}</span>
            {m.editedAt && !isEditing && (
              <span title={`Edited ${fmtDateTime(m.editedAt)}`}
                    style={{ fontSize: 11, color: "var(--muted)", fontStyle: "italic" }}>(edited)</span>
            )}
            <div style={{ marginLeft: "auto", display: "flex", gap: 4 }}>
              {canEdit && !isEditing && (
                <button onClick={onStartEdit} title="Edit"
                        style={{ background: "transparent", border: "none", color: "var(--muted)", cursor: "pointer", fontSize: 12, padding: "0 4px" }}>
                  ✏
                </button>
              )}
              {canDelete && !isEditing && (
                <button onClick={onDelete} title="Delete"
                        style={{ background: "transparent", border: "none", color: "var(--muted)", cursor: "pointer", fontSize: 14, padding: "0 4px" }}>
                  ×
                </button>
              )}
            </div>
          </div>
        )}
        {isEditing ? (
          <div>
            <textarea value={editText} onChange={e => setEditText(e.target.value)}
                      onKeyDown={onEditKey} autoFocus rows={2} maxLength={4000}
                      style={{ width: "100%", resize: "vertical" }} />
            <div style={{ marginTop: 4, display: "flex", gap: 6, alignItems: "center" }}>
              <Btn size="sm" onClick={() => onSaveEdit(editText.trim())} disabled={!editText.trim() || editText === m.body}>
                Save
              </Btn>
              <Btn size="sm" variant="ghost" onClick={onCancelEdit}>Cancel</Btn>
              <span style={{ fontSize: 11, color: "var(--muted)" }}>Enter to save · Esc to cancel</span>
            </div>
          </div>
        ) : (
          <div style={{ fontSize: 14, color: "var(--text)", whiteSpace: "pre-wrap", wordBreak: "break-word" }}>
            {renderMessageBody(m.body, memberById)}
          </div>
        )}
      </div>
    </div>
  );
}

// =============================================================================
// Composer with @-mention autocomplete
// =============================================================================

function ChatComposer({ composerRef, value, onChange, onKeyDown, onSend, members, channelName, channelId, isAdmin, sending, onScheduled }) {
  const [autocomplete, setAutocomplete] = useState(null); // { start, query, matches, selectedIndex } | null
  const [scheduleOpen, setScheduleOpen] = useState(false);
  const [scheduledLocal, setScheduledLocal] = useState("");
  const [scheduling, setScheduling] = useState(false);
  const localRef = useRef(null);
  const taRef = composerRef || localRef;

  // When the schedule popover opens, default to "next round hour" local
  useEffect(() => {
    if (scheduleOpen) {
      const d = new Date(Date.now() + 60 * 60 * 1000);
      d.setMinutes(0, 0, 0);
      const pad = (n) => String(n).padStart(2, "0");
      setScheduledLocal(
        `${d.getFullYear()}-${pad(d.getMonth() + 1)}-${pad(d.getDate())}T${pad(d.getHours())}:${pad(d.getMinutes())}`
      );
    }
  }, [scheduleOpen]);

  async function submitSchedule() {
    if (!value.trim()) { showToast("Type a message first", "error"); return; }
    if (!scheduledLocal) { showToast("Pick a date and time", "error"); return; }
    const dt = new Date(scheduledLocal);
    if (isNaN(dt.getTime()) || dt.getTime() < Date.now() - 60_000) {
      showToast("Schedule time must be in the future", "error"); return;
    }
    setScheduling(true);
    try {
      await api("POST", "/api/admin/chat/scheduled", {
        channelId,
        body: value.trim(),
        scheduledFor: dt.toISOString(),
      });
      showToast(`Scheduled for ${dt.toLocaleString()}`, "success");
      setScheduleOpen(false);
      if (onScheduled) onScheduled();
    } catch (e) {
      showToast(e.message, "error");
    } finally { setScheduling(false); }
  }

  // Detect "@token" being typed at the caret. If the character immediately
  // before the caret is part of an @-token (no whitespace separator), show
  // the autocomplete with matching members.
  useEffect(() => {
    const ta = taRef.current;
    if (!ta) return;
    const caret = ta.selectionStart || 0;
    // Look back from caret for @
    let i = caret - 1;
    while (i >= 0) {
      const c = value[i];
      if (!c) break;
      if (c === "@") {
        // Make sure @ is at start or preceded by whitespace
        if (i === 0 || /\s/.test(value[i - 1])) {
          const query = value.slice(i + 1, caret);
          if (/^[A-Za-z0-9 ._-]*$/.test(query) && query.length <= 30) {
            const lower = query.toLowerCase();
            const matches = (members || [])
              .filter(u => u.displayName.toLowerCase().includes(lower))
              .slice(0, 8);
            setAutocomplete({ start: i, query, matches, selectedIndex: 0 });
            return;
          }
        }
        break;
      }
      if (/\s/.test(c)) break;
      i--;
    }
    setAutocomplete(null);
  }, [value, taRef]);

  function pickMember(memberId, memberDisplay) {
    if (!autocomplete) return;
    const before = value.slice(0, autocomplete.start);
    const after = value.slice((taRef.current && taRef.current.selectionStart) || value.length);
    const insertion = `<@${memberId}> `;
    const newValue = before + insertion + after;
    onChange(newValue);
    setAutocomplete(null);
    requestAnimationFrame(() => {
      const ta = taRef.current;
      if (ta) {
        const caret = before.length + insertion.length;
        ta.focus();
        ta.setSelectionRange(caret, caret);
      }
    });
  }

  function handleKeyDown(e) {
    if (autocomplete && autocomplete.matches.length > 0) {
      if (e.key === "ArrowDown") {
        e.preventDefault();
        setAutocomplete(a => ({ ...a, selectedIndex: (a.selectedIndex + 1) % a.matches.length }));
        return;
      }
      if (e.key === "ArrowUp") {
        e.preventDefault();
        setAutocomplete(a => ({ ...a, selectedIndex: (a.selectedIndex - 1 + a.matches.length) % a.matches.length }));
        return;
      }
      if (e.key === "Enter" || e.key === "Tab") {
        e.preventDefault();
        const m = autocomplete.matches[autocomplete.selectedIndex];
        pickMember(m.id, m.displayName);
        return;
      }
      if (e.key === "Escape") {
        e.preventDefault();
        setAutocomplete(null);
        return;
      }
    }
    onKeyDown(e);
  }

  return (
    <form onSubmit={(e) => { e.preventDefault(); onSend(); }} style={{
      padding: "12px 24px 16px", borderTop: "1px solid var(--border)", flexShrink: 0, position: "relative",
    }}>
      {autocomplete && autocomplete.matches.length > 0 && (
        <div style={{
          position: "absolute", bottom: "100%", left: 24, marginBottom: 4,
          background: "var(--surface)", border: "1px solid var(--border)", borderRadius: 8,
          boxShadow: "0 4px 12px rgba(0,0,0,0.3)", overflow: "hidden", minWidth: 240, zIndex: 50,
        }}>
          <div style={{ padding: "6px 10px", fontSize: 11, color: "var(--muted)",
                        textTransform: "uppercase", letterSpacing: "0.5px", borderBottom: "1px solid var(--border)" }}>
            Mention a member
          </div>
          {autocomplete.matches.map((m, idx) => (
            <button key={m.id} type="button"
                    onMouseDown={(e) => { e.preventDefault(); pickMember(m.id, m.displayName); }}
                    style={{
                      display: "block", width: "100%", textAlign: "left",
                      padding: "8px 12px", fontSize: 13, cursor: "pointer",
                      background: idx === autocomplete.selectedIndex ? "var(--surface2)" : "transparent",
                      color: "var(--text)", border: "none", borderBottom: "1px solid var(--border)",
                    }}>
              <strong>{m.displayName}</strong>
              {m.role === "admin" && (
                <span style={{ marginLeft: 6, fontSize: 10, color: "var(--primary)", fontWeight: 700, textTransform: "uppercase" }}>admin</span>
              )}
            </button>
          ))}
        </div>
      )}
      {scheduleOpen && (
        <div style={{
          position: "absolute", bottom: "100%", right: 24, marginBottom: 4,
          background: "var(--surface)", border: "1px solid var(--border)", borderRadius: 10,
          boxShadow: "0 8px 24px rgba(0,0,0,0.35)", padding: 14, minWidth: 280, zIndex: 60,
        }}>
          <div style={{ fontSize: 11, fontWeight: 700, color: "var(--subtext)",
                        textTransform: "uppercase", letterSpacing: "0.5px", marginBottom: 8 }}>
            Schedule message
          </div>
          <input type="datetime-local"
                 value={scheduledLocal}
                 onChange={e => setScheduledLocal(e.target.value)}
                 style={{ width: "100%", marginBottom: 8 }} />
          <div style={{ fontSize: 11, color: "var(--muted)", marginBottom: 10 }}>
            {scheduledLocal && !isNaN(new Date(scheduledLocal).getTime()) ? (
              <>Posts ≈ {new Date(scheduledLocal).toLocaleString()}</>
            ) : "Pick a future time"}
          </div>
          <div style={{ display: "flex", gap: 8, justifyContent: "flex-end" }}>
            <Btn variant="ghost" onClick={() => setScheduleOpen(false)} disabled={scheduling}>Cancel</Btn>
            <Btn onClick={submitSchedule} disabled={scheduling || !value.trim()}>
              {scheduling ? <Spinner /> : "Schedule"}
            </Btn>
          </div>
        </div>
      )}
      <div style={{ display: "flex", gap: 8, alignItems: "flex-end" }}>
        <textarea ref={taRef}
                  value={value}
                  onChange={e => onChange(e.target.value)}
                  onKeyDown={handleKeyDown}
                  placeholder={`Message #${channelName}…  (type @ to mention)`}
                  rows={2}
                  maxLength={4000}
                  style={{ flex: 1, resize: "none", minHeight: 44 }} />
        {isAdmin && (
          <Btn type="button" variant="ghost"
               onClick={() => setScheduleOpen(o => !o)}
               disabled={sending || !value.trim()}
               title="Schedule for later"
               style={{ padding: "0 12px" }}>
            🕐
          </Btn>
        )}
        <Btn type="submit" disabled={sending || !value.trim()}>
          {sending ? <Spinner /> : "Send"}
        </Btn>
      </div>
      <div style={{ marginTop: 4, fontSize: 11, color: "var(--muted)" }}>
        Enter to send · Shift+Enter for new line · @ to mention{isAdmin ? " · 🕐 to schedule" : ""} · {value.length}/4000
      </div>
    </form>
  );
}

function ChannelCreateModal({ open, onClose, onCreated }) {
  const [name, setName] = useState("");
  const [description, setDescription] = useState("");
  const [busy, setBusy] = useState(false);
  useEffect(() => { if (open) { setName(""); setDescription(""); } }, [open]);

  async function handleSubmit(e) {
    e.preventDefault();
    if (!name.trim()) return;
    setBusy(true);
    try {
      const r = await api("POST", "/api/admin/chat/channels", {
        displayName: name.trim(),
        description: description.trim() || null,
      });
      showToast("Channel created", "success");
      onCreated(r.channel);
    } catch (e) { showToast(e.message, "error"); }
    finally { setBusy(false); }
  }

  return (
    <Modal open={open} onClose={busy ? () => {} : onClose} title="New channel" footer={
      <>
        <Btn variant="ghost" onClick={onClose} disabled={busy}>Cancel</Btn>
        <Btn onClick={handleSubmit} disabled={busy || !name.trim()}>{busy ? <Spinner /> : "Create"}</Btn>
      </>
    }>
      <form onSubmit={handleSubmit}>
        <Field label="Channel name" htmlFor="ch-name" hint="Shown as # name in the sidebar.">
          <input id="ch-name" type="text" required maxLength={60} autoFocus
                 value={name} onChange={e => setName(e.target.value)}
                 placeholder="e.g. Hormuz Watch" />
        </Field>
        <Field label="Description (optional)" htmlFor="ch-desc">
          <input id="ch-desc" type="text" maxLength={200}
                 value={description} onChange={e => setDescription(e.target.value)} />
        </Field>
      </form>
    </Modal>
  );
}

function ChannelEditModal({ channel, open, onClose, onUpdated, onDeleted }) {
  const [name, setName] = useState(channel.display_name);
  const [description, setDescription] = useState(channel.description || "");
  const [isDefault, setIsDefault] = useState(!!channel.is_default);
  const [busy, setBusy] = useState(false);
  const [confirmingDelete, setConfirmingDelete] = useState(false);

  async function save(e) {
    e.preventDefault();
    if (!name.trim()) return;
    setBusy(true);
    try {
      await api("PATCH", `/api/admin/chat/channels/${channel.id}`, {
        displayName: name.trim(),
        description: description.trim() || null,
        isDefault,
      });
      showToast("Saved", "success");
      onUpdated();
    } catch (e) { showToast(e.message, "error"); }
    finally { setBusy(false); }
  }

  async function doDelete() {
    setBusy(true);
    try {
      await api("DELETE", `/api/admin/chat/channels/${channel.id}`);
      showToast("Channel deleted", "success");
      onDeleted();
    } catch (e) { showToast(e.message, "error"); setBusy(false); setConfirmingDelete(false); }
  }

  return (
    <>
      <Modal open={open} onClose={busy ? () => {} : onClose} title={`Edit #${channel.display_name}`} footer={
        <>
          <Btn variant="danger" onClick={() => setConfirmingDelete(true)} disabled={busy || channel.is_default}
               title={channel.is_default ? "Set another channel as default before deleting this one" : ""}>
            Delete channel
          </Btn>
          <div style={{ flex: 1 }} />
          <Btn variant="ghost" onClick={onClose} disabled={busy}>Cancel</Btn>
          <Btn onClick={save} disabled={busy}>{busy ? <Spinner /> : "Save"}</Btn>
        </>
      }>
        <form onSubmit={save}>
          <Field label="Display name" htmlFor="ce-name">
            <input id="ce-name" type="text" required maxLength={60}
                   value={name} onChange={e => setName(e.target.value)} />
          </Field>
          <Field label="Description" htmlFor="ce-desc">
            <input id="ce-desc" type="text" maxLength={200}
                   value={description} onChange={e => setDescription(e.target.value)} />
          </Field>
          <Field>
            <label style={{ display: "flex", alignItems: "center", gap: 10, cursor: "pointer", textTransform: "none", letterSpacing: 0, fontSize: 14, color: "var(--text)" }}>
              <input type="checkbox" checked={isDefault}
                     onChange={e => setIsDefault(e.target.checked)}
                     style={{ width: "auto" }} />
              Default channel (new report announcements auto-post here)
            </label>
          </Field>
        </form>
      </Modal>
      <ConfirmDialog
        open={confirmingDelete}
        title="Delete channel?"
        message={`#${channel.display_name} and all its messages will be permanently deleted. This cannot be undone.`}
        confirmLabel="Delete"
        danger
        onConfirm={doDelete}
        onClose={() => setConfirmingDelete(false)}
      />
    </>
  );
}

// =============================================================================
// Inbox — 1:1 direct messages
// =============================================================================

function InboxView({ user, jumpToThreadId, onJumpHandled }) {
  const [threads, setThreads] = useState(null);
  const [activeThreadId, setActiveThreadId] = useState(null);
  const [messages, setMessages] = useState([]);
  const [composer, setComposer] = useState("");
  const [sending, setSending] = useState(false);
  const [creatingDm, setCreatingDm] = useState(false);
  const [editingMessageId, setEditingMessageId] = useState(null);
  const [confirming, setConfirming] = useState(null);
  const messagesEndRef = useRef(null);
  const scrollContainerRef = useRef(null);

  const activeThread = (threads || []).find(t => t.id === activeThreadId) || null;

  // Load threads on mount + every 30s.
  useEffect(() => {
    let cancelled = false;
    async function loadThreads() {
      try {
        const r = await api("GET", "/api/dms/threads");
        if (!cancelled) setThreads(r.threads || []);
      } catch (e) {
        if (!cancelled) { showToast(e.message, "error"); setThreads([]); }
      }
    }
    loadThreads();
    const interval = setInterval(loadThreads, 30000);
    return () => { cancelled = true; clearInterval(interval); };
  }, []);

  // Honor a jump target from the notification dropdown.
  useEffect(() => {
    if (!jumpToThreadId || !threads) return;
    setActiveThreadId(jumpToThreadId);
    onJumpHandled && onJumpHandled();
  }, [jumpToThreadId, threads, onJumpHandled]);

  // Load messages when thread changes; mark thread read on entry.
  useEffect(() => {
    if (!activeThreadId) return;
    setMessages([]);
    api("GET", `/api/dms/threads/${activeThreadId}/messages`).then(d => {
      setMessages(d.messages || []);
    }).catch(e => showToast(e.message, "error"));
    // Mark notifications for this thread as read (idempotent).
    api("POST", `/api/dms/threads/${activeThreadId}/read`)
      .then(() => setThreads(prev => prev ? prev.map(t => t.id === activeThreadId ? { ...t, unread: 0 } : t) : prev))
      .catch(() => { /* best-effort */ });
  }, [activeThreadId]);

  // Poll for new messages in the active thread every 5s.
  useEffect(() => {
    if (!activeThreadId) return;
    const tick = async () => {
      try {
        const lastId = messages.length > 0 ? messages[messages.length - 1].id : null;
        const path = lastId
          ? `/api/dms/threads/${activeThreadId}/messages?since=${lastId}`
          : `/api/dms/threads/${activeThreadId}/messages`;
        const d = await api("GET", path);
        const fresh = d.messages || [];
        if (fresh.length > 0) {
          setMessages(prev => {
            const seen = new Set(prev.map(m => m.id));
            return [...prev, ...fresh.filter(m => !seen.has(m.id))];
          });
          // Auto-mark read when receiving messages while the thread is open.
          api("POST", `/api/dms/threads/${activeThreadId}/read`).catch(() => {});
        }
      } catch { /* network blip */ }
    };
    const interval = setInterval(tick, 5000);
    return () => clearInterval(interval);
  }, [activeThreadId, messages]);

  // Auto-scroll to bottom on new messages.
  useEffect(() => {
    const c = scrollContainerRef.current;
    if (!c) return;
    const nearBottom = c.scrollHeight - c.scrollTop - c.clientHeight < 200;
    if (nearBottom && messagesEndRef.current) {
      messagesEndRef.current.scrollIntoView({ behavior: "smooth" });
    }
  }, [messages]);

  async function refreshThreads() {
    try {
      const r = await api("GET", "/api/dms/threads");
      setThreads(r.threads || []);
    } catch { /* best-effort */ }
  }

  async function startDm(otherUserId) {
    try {
      const r = await api("POST", "/api/dms/threads", { otherUserId });
      const tid = r.thread.id;
      // If we don't already have it in the list, refresh.
      setThreads(prev => {
        const existing = (prev || []).find(t => t.id === tid);
        if (existing) return prev;
        return [r.thread, ...(prev || [])];
      });
      setActiveThreadId(tid);
      setCreatingDm(false);
    } catch (e) { showToast(e.message, "error"); }
  }

  async function sendMessage(e) {
    if (e) e.preventDefault();
    const text = composer.trim();
    if (!text || !activeThreadId || sending) return;
    setSending(true);
    try {
      const r = await api("POST", `/api/dms/threads/${activeThreadId}/messages`, { body: text });
      setMessages(prev => [...prev, r.message]);
      setComposer("");
      // Update last-message preview locally
      setThreads(prev => prev ? prev.map(t => t.id === activeThreadId
        ? { ...t, lastMessage: { body: text, createdAt: r.message.createdAt, fromMe: true }, lastMessageAt: r.message.createdAt }
        : t) : prev);
    } catch (e) { showToast(e.message, "error"); }
    finally { setSending(false); }
  }

  function onComposerKey(e) {
    if (e.key === "Enter" && !e.shiftKey) {
      e.preventDefault();
      sendMessage();
    }
  }

  async function saveEdit(messageId, newBody) {
    try {
      const r = await api("PATCH", `/api/dms/messages/${messageId}`, { body: newBody });
      setMessages(prev => prev.map(m => m.id === messageId ? r.message : m));
      setEditingMessageId(null);
      showToast("Message updated", "success");
    } catch (e) { showToast(e.message, "error"); }
  }

  function deleteMessage(m) {
    setConfirming({
      title: "Delete message?",
      message: "This message will be permanently deleted from the conversation.",
      confirmLabel: "Delete",
      danger: true,
      action: async () => {
        await api("DELETE", `/api/dms/messages/${m.id}`);
        setMessages(prev => prev.filter(x => x.id !== m.id));
      },
    });
  }

  return (
    <div style={{ display: "flex", flex: 1, overflow: "hidden" }}>
      {/* Thread sidebar */}
      <div style={{
        width: 260, flexShrink: 0,
        background: "var(--surface)",
        borderRight: "1px solid var(--border)",
        display: "flex", flexDirection: "column",
      }}>
        <div style={{
          padding: "14px 16px", borderBottom: "1px solid var(--border)",
          display: "flex", alignItems: "center", justifyContent: "space-between",
        }}>
          <div style={{ fontSize: 12, fontWeight: 700, color: "var(--subtext)", textTransform: "uppercase", letterSpacing: "0.5px" }}>
            Conversations
          </div>
          <button onClick={() => setCreatingDm(true)} title="Start a new direct message"
                  style={{
                    background: "transparent", border: "none", color: "var(--primary)",
                    cursor: "pointer", fontSize: 18, padding: 0, lineHeight: 1,
                  }}>+</button>
        </div>
        <div style={{ flex: 1, overflowY: "auto", padding: 8 }}>
          {threads === null ? (
            <div className="skeleton" style={{ height: 60, margin: "4px 0" }} />
          ) : threads.length === 0 ? (
            <div style={{ padding: 18, fontSize: 12, color: "var(--muted)", textAlign: "center", lineHeight: 1.6 }}>
              No conversations yet.<br />
              Click + to message a member directly.
            </div>
          ) : (
            threads.map(t => {
              const active = t.id === activeThreadId;
              const initials = (t.otherUser.displayName || "?")
                .split(/\s+/).map(w => w[0]).filter(Boolean).slice(0, 2).join("").toUpperCase() || "?";
              return (
                <button key={t.id} onClick={() => setActiveThreadId(t.id)} style={{
                  display: "flex", gap: 10, alignItems: "flex-start",
                  width: "100%", padding: "10px 12px", marginBottom: 2,
                  borderRadius: 8, fontSize: 13, textAlign: "left",
                  background: active ? "var(--surface2)" : "transparent",
                  border: "1px solid " + (active ? "var(--border)" : "transparent"),
                  cursor: "pointer", position: "relative",
                }}>
                  <div style={{
                    width: 32, height: 32, borderRadius: "50%",
                    background: "var(--surface3)", color: "var(--text)",
                    display: "flex", alignItems: "center", justifyContent: "center",
                    fontSize: 12, fontWeight: 700, flexShrink: 0,
                  }}>{initials}</div>
                  <div style={{ flex: 1, minWidth: 0 }}>
                    <div style={{ display: "flex", justifyContent: "space-between", gap: 6 }}>
                      <span style={{ fontSize: 13, fontWeight: t.unread > 0 ? 700 : 600,
                                     color: "var(--text)", overflow: "hidden",
                                     textOverflow: "ellipsis", whiteSpace: "nowrap" }}>
                        {t.otherUser.displayName}
                      </span>
                      {t.unread > 0 && (
                        <span style={{
                          background: "var(--primary)", color: "#fff",
                          borderRadius: 999, padding: "1px 6px", fontSize: 10, fontWeight: 700,
                          minWidth: 18, textAlign: "center", lineHeight: "14px", flexShrink: 0,
                        }}>{t.unread > 99 ? "99+" : t.unread}</span>
                      )}
                    </div>
                    {t.lastMessage ? (
                      <div style={{ fontSize: 12, color: "var(--subtext)",
                                    overflow: "hidden", textOverflow: "ellipsis", whiteSpace: "nowrap" }}>
                        {t.lastMessage.fromMe ? "You: " : ""}{t.lastMessage.body}
                      </div>
                    ) : (
                      <div style={{ fontSize: 12, color: "var(--muted)", fontStyle: "italic" }}>No messages yet</div>
                    )}
                  </div>
                </button>
              );
            })
          )}
        </div>
      </div>

      {/* Messages + composer */}
      <div style={{ flex: 1, display: "flex", flexDirection: "column", overflow: "hidden" }}>
        {activeThread ? (
          <>
            <div style={{
              padding: "14px 24px", borderBottom: "1px solid var(--border)", flexShrink: 0,
              display: "flex", alignItems: "center", gap: 10,
            }}>
              <div style={{
                width: 32, height: 32, borderRadius: "50%",
                background: "var(--surface3)", color: "var(--text)",
                display: "flex", alignItems: "center", justifyContent: "center",
                fontSize: 12, fontWeight: 700,
              }}>
                {(activeThread.otherUser.displayName || "?").split(/\s+/).map(w => w[0]).filter(Boolean).slice(0, 2).join("").toUpperCase() || "?"}
              </div>
              <div>
                <div style={{ fontSize: 16, fontWeight: 700, color: "var(--text)" }}>
                  {activeThread.otherUser.displayName}
                </div>
                <div style={{ fontSize: 12, color: "var(--subtext)" }}>
                  Direct message · only you two can see this
                </div>
              </div>
            </div>

            <div ref={scrollContainerRef} style={{ flex: 1, overflowY: "auto", padding: "16px 24px" }}>
              {messages.length === 0 ? (
                <div className="empty-state" style={{ padding: "40px 0" }}>
                  <h3>No messages yet</h3>
                  <p>Say hi to {activeThread.otherUser.displayName}.</p>
                </div>
              ) : (
                messages.map((m, i) => {
                  const prev = messages[i - 1];
                  const sameAuthor = prev && prev.sender && m.sender && prev.sender.id === m.sender.id;
                  const closeInTime = prev && (new Date(m.createdAt) - new Date(prev.createdAt) < 5 * 60 * 1000);
                  const grouped = sameAuthor && closeInTime;
                  const isOwner = m.isMine;
                  const ageMs = Date.now() - new Date(m.createdAt + (m.createdAt && m.createdAt.includes("T") ? "" : "Z")).getTime();
                  const canEdit = isOwner && ageMs < 10 * 60 * 1000;
                  return (
                    <DmMessageRow key={m.id} message={m} grouped={grouped}
                                  canEdit={canEdit} canDelete={isOwner}
                                  isEditing={editingMessageId === m.id}
                                  onStartEdit={() => setEditingMessageId(m.id)}
                                  onCancelEdit={() => setEditingMessageId(null)}
                                  onSaveEdit={(text) => saveEdit(m.id, text)}
                                  onDelete={() => deleteMessage(m)} />
                  );
                })
              )}
              <div ref={messagesEndRef} />
            </div>

            <form onSubmit={sendMessage} style={{
              padding: "12px 24px 16px", borderTop: "1px solid var(--border)", flexShrink: 0,
            }}>
              <div style={{ display: "flex", gap: 8, alignItems: "flex-end" }}>
                <textarea value={composer} onChange={e => setComposer(e.target.value)}
                          onKeyDown={onComposerKey}
                          placeholder={`Message ${activeThread.otherUser.displayName}…`}
                          rows={2} maxLength={4000}
                          style={{ flex: 1, resize: "none", minHeight: 44 }} />
                <Btn type="submit" disabled={sending || !composer.trim()}>
                  {sending ? <Spinner /> : "Send"}
                </Btn>
              </div>
              <div style={{ marginTop: 4, fontSize: 11, color: "var(--muted)" }}>
                Enter to send · Shift+Enter for new line · {composer.length}/4000
              </div>
            </form>
          </>
        ) : (
          <div className="empty-state" style={{ marginTop: 60 }}>
            <h3>Select a conversation</h3>
            <p>{(threads || []).length === 0
              ? "Click + in the sidebar to start a new conversation."
              : "Pick a conversation from the left to read or reply."}</p>
          </div>
        )}
      </div>

      {creatingDm && (
        <NewDmModal currentUserId={user.id}
                    onPick={startDm}
                    onClose={() => setCreatingDm(false)} />
      )}
      {confirming && (
        <ConfirmDialog open={!!confirming}
                       title={confirming.title}
                       message={confirming.message}
                       confirmLabel={confirming.confirmLabel}
                       danger={confirming.danger}
                       onConfirm={confirming.action}
                       onClose={() => setConfirming(null)} />
      )}
    </div>
  );
}

function DmMessageRow({ message, grouped, canEdit, canDelete, isEditing, onStartEdit, onCancelEdit, onSaveEdit, onDelete }) {
  const m = message;
  const [editText, setEditText] = useState(m.body);
  useEffect(() => { setEditText(m.body); }, [m.body, isEditing]);

  const senderName = m.sender ? m.sender.displayName : "Member";
  const initials = (senderName || "?")
    .split(/\s+/).map(w => w[0]).filter(Boolean).slice(0, 2).join("").toUpperCase() || "?";

  function onEditKey(e) {
    if (e.key === "Enter" && !e.shiftKey) { e.preventDefault(); onSaveEdit(editText.trim()); }
    else if (e.key === "Escape") { e.preventDefault(); onCancelEdit(); }
  }

  return (
    <div style={{
      display: "flex", gap: 10,
      padding: grouped ? "1px 0" : "8px 0",
      marginTop: grouped ? 0 : 4,
    }} className="chat-msg-row">
      <div style={{ width: 32, flexShrink: 0 }}>
        {!grouped && (
          <div style={{
            width: 32, height: 32, borderRadius: "50%",
            background: m.isMine ? "var(--primary)" : "var(--surface3)",
            color: m.isMine ? "#fff" : "var(--text)",
            display: "flex", alignItems: "center", justifyContent: "center",
            fontSize: 12, fontWeight: 700,
          }}>{initials}</div>
        )}
      </div>
      <div style={{ flex: 1, minWidth: 0 }}>
        {!grouped && (
          <div style={{ display: "flex", alignItems: "baseline", gap: 8, marginBottom: 2 }}>
            <span style={{ fontSize: 14, fontWeight: 700, color: "var(--text)" }}>{senderName}</span>
            <span style={{ fontSize: 11, color: "var(--muted)" }}>{fmtDateTime(m.createdAt)}</span>
            {m.editedAt && !isEditing && (
              <span title={`Edited ${fmtDateTime(m.editedAt)}`}
                    style={{ fontSize: 11, color: "var(--muted)", fontStyle: "italic" }}>(edited)</span>
            )}
            <div style={{ marginLeft: "auto", display: "flex", gap: 4 }}>
              {canEdit && !isEditing && (
                <button onClick={onStartEdit} title="Edit"
                        style={{ background: "transparent", border: "none", color: "var(--muted)", cursor: "pointer", fontSize: 12, padding: "0 4px" }}>
                  ✏
                </button>
              )}
              {canDelete && !isEditing && (
                <button onClick={onDelete} title="Delete"
                        style={{ background: "transparent", border: "none", color: "var(--muted)", cursor: "pointer", fontSize: 14, padding: "0 4px" }}>
                  ×
                </button>
              )}
            </div>
          </div>
        )}
        {isEditing ? (
          <div>
            <textarea value={editText} onChange={e => setEditText(e.target.value)}
                      onKeyDown={onEditKey} autoFocus rows={2} maxLength={4000}
                      style={{ width: "100%", resize: "vertical" }} />
            <div style={{ marginTop: 4, display: "flex", gap: 6, alignItems: "center" }}>
              <Btn size="sm" onClick={() => onSaveEdit(editText.trim())} disabled={!editText.trim() || editText === m.body}>
                Save
              </Btn>
              <Btn size="sm" variant="ghost" onClick={onCancelEdit}>Cancel</Btn>
              <span style={{ fontSize: 11, color: "var(--muted)" }}>Enter to save · Esc to cancel</span>
            </div>
          </div>
        ) : (
          <div style={{ fontSize: 14, color: "var(--text)", whiteSpace: "pre-wrap", wordBreak: "break-word" }}>
            {m.body}
          </div>
        )}
      </div>
    </div>
  );
}

function NewDmModal({ currentUserId, onPick, onClose }) {
  const [members, setMembers] = useState(null);
  const [query, setQuery] = useState("");
  const [busyPick, setBusyPick] = useState(null);

  useEffect(() => {
    const t = setTimeout(() => {
      api("GET", `/api/members${query ? `?q=${encodeURIComponent(query)}` : ""}`)
        .then(d => setMembers(d.members || []))
        .catch(e => { showToast(e.message, "error"); setMembers([]); });
    }, query ? 200 : 0);
    return () => clearTimeout(t);
  }, [query]);

  return (
    <Modal open onClose={busyPick ? () => {} : onClose} title="New direct message" footer={
      <Btn variant="ghost" onClick={onClose} disabled={!!busyPick}>Cancel</Btn>
    }>
      <Field label="Search members" htmlFor="dm-q">
        <input id="dm-q" type="search" autoFocus value={query}
               onChange={e => setQuery(e.target.value)}
               placeholder="Search by name…" />
      </Field>
      <div style={{ marginTop: 12, maxHeight: 320, overflowY: "auto",
                    border: "1px solid var(--border)", borderRadius: 8 }}>
        {members === null ? (
          <div style={{ padding: 12 }}><div className="skeleton" style={{ height: 60 }} /></div>
        ) : members.length === 0 ? (
          <div style={{ padding: 24, color: "var(--muted)", fontSize: 13, textAlign: "center" }}>
            No members found.
          </div>
        ) : (
          members.filter(m => m.id !== currentUserId).map(m => {
            const initials = (m.displayName || "?").split(/\s+/).map(w => w[0]).filter(Boolean).slice(0, 2).join("").toUpperCase() || "?";
            return (
              <button key={m.id}
                      onClick={async () => { setBusyPick(m.id); await onPick(m.id); }}
                      disabled={!!busyPick}
                      style={{
                        display: "flex", alignItems: "center", gap: 10,
                        width: "100%", padding: "10px 14px", textAlign: "left",
                        background: busyPick === m.id ? "var(--surface2)" : "transparent",
                        border: "none", borderBottom: "1px solid var(--border)",
                        cursor: busyPick ? "default" : "pointer", fontSize: 14, color: "var(--text)",
                      }}>
                <div style={{
                  width: 32, height: 32, borderRadius: "50%",
                  background: "var(--surface3)", color: "var(--text)",
                  display: "flex", alignItems: "center", justifyContent: "center",
                  fontSize: 12, fontWeight: 700,
                }}>{initials}</div>
                <div style={{ flex: 1 }}>
                  <strong>{m.displayName}</strong>
                  {m.role === "admin" && (
                    <span style={{ marginLeft: 6, fontSize: 10, color: "var(--primary)", fontWeight: 700, textTransform: "uppercase" }}>admin</span>
                  )}
                </div>
                {busyPick === m.id && <Spinner />}
              </button>
            );
          })
        )}
      </div>
    </Modal>
  );
}

// =============================================================================
// RequestTopicView — members submit topic / event requests; see status of
// their own previous submissions. Free-form textarea; submission rate-limited
// to 5 / hour by the backend. Disclaimer is prominent: no guarantee of
// coverage — the value comes from being a signal channel, not a SLA.
// =============================================================================

function RequestTopicView({ user }) {
  const [body, setBody] = useState("");
  const [busy, setBusy] = useState(false);
  const [mine, setMine] = useState(null);

  function loadMine() {
    api("GET", "/api/requests/mine")
      .then(d => setMine(d.requests || []))
      .catch(() => setMine([]));
  }
  useEffect(() => { loadMine(); }, []);

  async function submit(e) {
    if (e) e.preventDefault();
    const text = body.trim();
    if (!text) { showToast("Describe the topic first", "error"); return; }
    if (text.length > 2000) { showToast("Request is too long (max 2000 characters)", "error"); return; }
    setBusy(true);
    try {
      await api("POST", "/api/requests", { body: text });
      showToast("Request submitted — thanks for the suggestion", "success");
      setBody("");
      loadMine();
    } catch (e) { showToast(e.message, "error"); }
    finally { setBusy(false); }
  }

  const STATUS_META = {
    open:     { label: "Open",     bg: "rgba(56,114,224,0.14)",  fg: "#5b8eef" },
    planned:  { label: "Planned",  bg: "rgba(200,155,60,0.14)",  fg: "var(--primary)" },
    covered:  { label: "Covered",  bg: "rgba(34,197,94,0.14)",   fg: "#22c55e" },
    declined: { label: "Declined", bg: "rgba(239,68,68,0.14)",   fg: "#f87171" },
    archived: { label: "Archived", bg: "var(--surface2)",          fg: "var(--muted)" },
  };

  return (
    <div style={{ flex: 1, overflowY: "auto", padding: "32px 28px", maxWidth: 760, width: "100%", margin: "0 auto" }}>
      <h2 className="serif" style={{ fontSize: 24, fontWeight: 700, marginBottom: 6, letterSpacing: "-0.01em" }}>
        Request a topic or event
      </h2>
      <div style={{ fontSize: 13, color: "var(--muted)", marginBottom: 22, lineHeight: 1.6 }}>
        Tell us what you'd like covered — an emerging crisis, a region, a commodity, a specific
        company or trade lane. Our analysts review every request.
      </div>

      <form onSubmit={submit} style={{
        background: "var(--surface)",
        border: "1px solid var(--border)",
        borderRadius: 12, padding: 20, marginBottom: 28,
      }}>
        <label htmlFor="req-body" style={{
          display: "block", fontSize: 11, fontWeight: 700,
          color: "var(--subtext)", textTransform: "uppercase",
          letterSpacing: "0.5px", marginBottom: 8,
        }}>Your request</label>
        <textarea id="req-body"
                  value={body}
                  onChange={e => setBody(e.target.value)}
                  placeholder="e.g. Coverage of the Suez Canal disruption — implications for Asia–Europe container rates over the next 90 days."
                  rows={5}
                  maxLength={2000}
                  style={{ width: "100%", resize: "vertical", minHeight: 120 }} />
        <div style={{ marginTop: 6, display: "flex", justifyContent: "space-between", fontSize: 11, color: "var(--muted)" }}>
          <span>Submitted as <strong style={{ color: "var(--subtext)" }}>{user.full_name || user.email}</strong></span>
          <span>{body.length}/2000</span>
        </div>

        <div style={{
          marginTop: 14, padding: "10px 12px",
          background: "rgba(200,155,60,0.08)",
          border: "1px solid rgba(200,155,60,0.3)",
          borderRadius: 8,
          fontSize: 12, color: "var(--text)", lineHeight: 1.55,
        }}>
          <strong>Note:</strong> Submitting a request does <strong>not</strong> guarantee that the topic will be covered.
          We review every suggestion, but only publish on topics where we have meaningful intelligence to share.
          You'll see the status update here once we've reviewed it.
        </div>

        <div style={{ marginTop: 14, display: "flex", justifyContent: "flex-end" }}>
          <Btn onClick={submit} disabled={busy || !body.trim()}>
            {busy ? <Spinner /> : "Submit request"}
          </Btn>
        </div>
      </form>

      <h3 style={{ fontSize: 14, fontWeight: 700, color: "var(--subtext)",
                   textTransform: "uppercase", letterSpacing: "0.5px", marginBottom: 12 }}>
        Your previous requests
      </h3>
      {mine === null ? (
        <div className="skeleton" style={{ height: 80 }} />
      ) : mine.length === 0 ? (
        <div style={{ padding: 16, fontSize: 13, color: "var(--muted)",
                      border: "1px dashed var(--border)", borderRadius: 10 }}>
          You haven't submitted any requests yet.
        </div>
      ) : (
        <div style={{ display: "flex", flexDirection: "column", gap: 10 }}>
          {mine.map(r => {
            const meta = STATUS_META[r.status] || STATUS_META.open;
            return (
              <div key={r.id} style={{
                background: "var(--surface)", border: "1px solid var(--border)",
                borderRadius: 10, padding: 14,
              }}>
                <div style={{ display: "flex", gap: 10, alignItems: "center", marginBottom: 8 }}>
                  <span style={{
                    padding: "2px 10px", borderRadius: 999, fontSize: 11, fontWeight: 700,
                    background: meta.bg, color: meta.fg,
                  }}>{meta.label}</span>
                  <span style={{ fontSize: 11, color: "var(--muted)" }}>
                    {fmtDate(r.createdAt)}
                  </span>
                </div>
                <div style={{ fontSize: 13, color: "var(--text)", whiteSpace: "pre-wrap", lineHeight: 1.55 }}>
                  {r.body}
                </div>
                {r.adminNote && (
                  <div style={{
                    marginTop: 10, padding: "8px 12px",
                    background: "var(--surface2)", borderLeft: "3px solid var(--primary)",
                    borderRadius: 4, fontSize: 12, color: "var(--subtext)", lineHeight: 1.55,
                  }}>
                    <strong style={{ color: "var(--text)" }}>From the editor:</strong> {r.adminNote}
                  </div>
                )}
              </div>
            );
          })}
        </div>
      )}
    </div>
  );
}

// =============================================================================
// Settings (member self-edit)
// =============================================================================

function SettingsPage({ user, onUserUpdated }) {
  const [fullName, setFullName] = useState(user.full_name || "");
  const [displayName, setDisplayName] = useState(user.display_name || "");
  const [emailOptIn, setEmailOptIn] = useState(!user.email_opt_out);
  const [busyProfile, setBusyProfile] = useState(false);

  const [currentPw, setCurrentPw] = useState("");
  const [newPw, setNewPw] = useState("");
  const [newPw2, setNewPw2] = useState("");
  const [busyPw, setBusyPw] = useState(false);

  const [newEmail, setNewEmail] = useState("");
  const [busyEmail, setBusyEmail] = useState(false);

  async function saveProfile(e) {
    e.preventDefault();
    setBusyProfile(true);
    try {
      await api("PATCH", "/api/auth/me", { fullName, displayName, emailOptIn });
      showToast("Profile updated", "success");
      onUserUpdated();
    } catch (e) { showToast(e.message, "error"); }
    finally { setBusyProfile(false); }
  }

  async function changePassword(e) {
    e.preventDefault();
    if (newPw !== newPw2) { showToast("New passwords don't match", "error"); return; }
    if (newPw.length < 8) { showToast("Password must be at least 8 characters", "error"); return; }
    setBusyPw(true);
    try {
      await api("PATCH", "/api/auth/me", { currentPassword: currentPw, newPassword: newPw });
      showToast("Password changed", "success");
      setCurrentPw(""); setNewPw(""); setNewPw2("");
    } catch (e) { showToast(e.message, "error"); }
    finally { setBusyPw(false); }
  }

  async function changeEmail(e) {
    e.preventDefault();
    setBusyEmail(true);
    try {
      await api("POST", "/api/auth/change-email", { newEmail });
      showToast("Confirmation email sent (if email is wired up)", "success");
      setNewEmail("");
    } catch (e) { showToast(e.message, "error"); }
    finally { setBusyEmail(false); }
  }

  return (
    <div style={{ padding: "20px 28px", flex: 1, overflowY: "auto", maxWidth: 720 }}>
      <Section title="Profile">
        <form onSubmit={saveProfile}>
          <Field label="Email">
            <input type="email" value={user.email} disabled
                   style={{ background: "var(--surface3)", color: "var(--subtext)" }} />
          </Field>
          <Field label="Full name" htmlFor="set-fn">
            <input id="set-fn" type="text" value={fullName} onChange={e => setFullName(e.target.value)} />
          </Field>
          <Field label="Display name (optional)" htmlFor="set-dn">
            <input id="set-dn" type="text" value={displayName} onChange={e => setDisplayName(e.target.value)} />
          </Field>
          <Field>
            <label style={{ display: "flex", alignItems: "flex-start", gap: 10, cursor: "pointer", textTransform: "none", letterSpacing: 0, fontSize: 14, color: "var(--text)", lineHeight: 1.5 }}>
              <input type="checkbox" checked={emailOptIn}
                     onChange={e => setEmailOptIn(e.target.checked)}
                     style={{ width: "auto", marginTop: 3 }} />
              <span>
                Email me when new reports are published.
                <span style={{ display: "block", fontSize: 12, color: "var(--muted)", marginTop: 2 }}>
                  Uncheck to stop receiving notifications. You can also use the unsubscribe link in any email.
                </span>
              </span>
            </label>
          </Field>
          <Btn type="submit" disabled={busyProfile}>{busyProfile ? <Spinner /> : "Save profile"}</Btn>
        </form>
      </Section>

      <Section title="Change password">
        <form onSubmit={changePassword}>
          <Field label="Current password" htmlFor="cur-pw">
            <PasswordInput id="cur-pw" value={currentPw} onChange={e => setCurrentPw(e.target.value)} required />
          </Field>
          <Field label="New password" htmlFor="new-pw">
            <PasswordInput id="new-pw" value={newPw} onChange={e => setNewPw(e.target.value)} required minLength={8} />
          </Field>
          <Field label="Confirm new password" htmlFor="new-pw2">
            <PasswordInput id="new-pw2" value={newPw2} onChange={e => setNewPw2(e.target.value)} required minLength={8} />
          </Field>
          <Btn type="submit" disabled={busyPw}>{busyPw ? <Spinner /> : "Change password"}</Btn>
        </form>
      </Section>

      <Section title="Change email" hint="A confirmation link will be sent to the new address. Requires Resend to be configured.">
        <form onSubmit={changeEmail}>
          <Field label="New email" htmlFor="new-email">
            <input id="new-email" type="email" value={newEmail} onChange={e => setNewEmail(e.target.value)} required />
          </Field>
          <Btn type="submit" variant="ghost" disabled={busyEmail || !newEmail}>
            {busyEmail ? <Spinner /> : "Request change"}
          </Btn>
        </form>
      </Section>

      {(user.stripe_status === "active" || user.stripe_status === "past_due" || user.stripe_status === "canceled") && (
        <SubscriptionSection user={user} />
      )}
    </div>
  );
}

function SubscriptionSection({ user }) {
  const [busy, setBusy] = useState(null); // null | 'manage' | 'update' | 'cancel'

  async function openPortal(flow) {
    setBusy(flow);
    try {
      const r = await api("POST", "/api/stripe/portal-session", flow ? { flow } : null);
      window.location.href = r.url;
    } catch (e) {
      showToast(e.message, "error");
      setBusy(null);
    }
  }

  const statusLabel = ({
    active: { text: "Active", color: "var(--success)" },
    past_due: { text: "Past due", color: "var(--warn)" },
    canceled: { text: "Canceled", color: "var(--danger)" },
  })[user.stripe_status] || { text: user.stripe_status, color: "var(--subtext)" };

  const isActive = user.stripe_status === "active" || user.stripe_status === "past_due";

  return (
    <Section title="Subscription" hint="Manage your plan, payment method, or cancel via the Stripe customer portal.">
      <div style={{ background: "var(--surface2)", border: "1px solid var(--border)", borderRadius: 10, padding: 14, marginBottom: 14, fontSize: 13 }}>
        <div style={{ display: "flex", justifyContent: "space-between", marginBottom: 6 }}>
          <span style={{ color: "var(--subtext)" }}>Plan</span>
          <strong style={{ color: "var(--text)" }}>{user.plan || "—"}</strong>
        </div>
        <div style={{ display: "flex", justifyContent: "space-between" }}>
          <span style={{ color: "var(--subtext)" }}>Status</span>
          <strong style={{ color: statusLabel.color }}>{statusLabel.text}</strong>
        </div>
      </div>
      <div style={{ display: "flex", gap: 8, flexWrap: "wrap" }}>
        {isActive && (
          <Btn onClick={() => openPortal("update")} disabled={busy !== null}>
            {busy === "update" ? <Spinner /> : "Change plan"}
          </Btn>
        )}
        <Btn variant="ghost" onClick={() => openPortal("manage")} disabled={busy !== null}>
          {busy === "manage" ? <Spinner /> : "Manage payment + invoices"}
        </Btn>
        {isActive && (
          <Btn variant="danger" onClick={() => openPortal("cancel")} disabled={busy !== null}>
            {busy === "cancel" ? <Spinner /> : "Cancel"}
          </Btn>
        )}
      </div>
    </Section>
  );
}

function Section({ title, hint, children }) {
  return (
    <div style={{ marginBottom: 36 }}>
      <h2 className="serif" style={{ fontSize: 19, fontWeight: 700, marginBottom: 4, letterSpacing: "-0.01em" }}>{title}</h2>
      {hint && <div style={{ fontSize: 12, color: "var(--muted)", marginBottom: 16, lineHeight: 1.5 }}>{hint}</div>}
      {!hint && <div style={{ height: 16 }} />}
      {children}
    </div>
  );
}

// =============================================================================
// Admin panel
// =============================================================================

function AdminPanel({ user, onUserUpdated, refreshKey, onChange }) {
  const [tab, setTab] = useState("users");

  return (
    <div style={{ flex: 1, display: "flex", flexDirection: "column", overflow: "hidden" }}>
      <div style={{
        display: "flex", gap: 4, padding: "12px 28px",
        borderBottom: "1px solid var(--border)",
        flexWrap: "wrap",
      }}>
        <TabBtn active={tab === "users"} onClick={() => setTab("users")}>Members</TabBtn>
        <TabBtn active={tab === "reports"} onClick={() => setTab("reports")}>Reports</TabBtn>
        <TabBtn active={tab === "scheduled"} onClick={() => setTab("scheduled")}>Scheduled</TabBtn>
        <TabBtn active={tab === "requests"} onClick={() => setTab("requests")}>Requests</TabBtn>
        <TabBtn active={tab === "crisis"} onClick={() => setTab("crisis")}>Crisis pages</TabBtn>
        <TabBtn active={tab === "revenue"} onClick={() => setTab("revenue")}>Revenue</TabBtn>
        <TabBtn active={tab === "activity"} onClick={() => setTab("activity")}>Activity</TabBtn>
        <TabBtn active={tab === "settings"} onClick={() => setTab("settings")}>Hub settings</TabBtn>
      </div>

      <div style={{ flex: 1, overflowY: "auto" }}>
        {tab === "users" && <AdminUsersTab self={user} />}
        {tab === "reports" && <AdminReportsTab user={user} refreshKey={refreshKey} onChange={onChange} />}
        {tab === "scheduled" && <AdminScheduledTab />}
        {tab === "requests" && <AdminRequestsTab />}
        {tab === "crisis" && <AdminCrisisTab />}
        {tab === "revenue" && <AdminRevenueTab />}
        {tab === "activity" && <AdminActivityTab />}
        {tab === "settings" && <AdminSettingsTab onChange={onChange} />}
      </div>
    </div>
  );
}

function TabBtn({ active, children, ...props }) {
  return (
    <button {...props} style={{
      padding: "8px 14px", borderRadius: 8,
      fontSize: 13, fontWeight: 600,
      background: active ? "var(--surface2)" : "transparent",
      color: active ? "var(--text)" : "var(--subtext)",
      border: "1px solid " + (active ? "var(--border)" : "transparent"),
      cursor: "pointer",
    }}>{children}</button>
  );
}

function AdminUsersTab({ self }) {
  const [users, setUsers] = useState(null);
  const [search, setSearch] = useState("");
  const [editing, setEditing] = useState(null);
  const [adding, setAdding] = useState(false);
  const [viewingDetail, setViewingDetail] = useState(null);
  const [confirming, setConfirming] = useState(null);
  const [refreshKey, setRefreshKey] = useState(0);
  const refresh = () => setRefreshKey(k => k + 1);

  useEffect(() => {
    const path = `/api/admin/analytics/members${search ? `?search=${encodeURIComponent(search)}` : ""}`;
    api("GET", path)
      .then(d => setUsers(d.members || []))
      .catch(e => { showToast(e.message, "error"); setUsers([]); });
  }, [refreshKey, search]);

  function deleteUser(u) {
    setConfirming({
      title: "Delete user?",
      message: `${u.email} will lose access immediately. This cannot be undone.`,
      confirmLabel: "Delete user",
      danger: true,
      action: async () => {
        await api("DELETE", `/api/admin/users/${u.id}`);
        showToast("Deleted", "success");
        refresh();
      },
    });
  }

  function resetPassword(u) {
    setConfirming({
      title: "Reset password?",
      message: `A new temporary password will be generated for ${u.email}. Make sure you can copy it before the dialog closes.`,
      confirmLabel: "Reset password",
      action: async () => {
        const r = await api("PATCH", `/api/admin/users/${u.id}`, { resetPassword: true });
        if (r.tempPassword) {
          window.prompt("Temporary password — copy now:", r.tempPassword);
        }
      },
    });
  }

  return (
    <div style={{ padding: "20px 28px" }}>
      <div style={{ display: "flex", gap: 12, marginBottom: 18, alignItems: "center", justifyContent: "space-between" }}>
        <input type="search" placeholder="Search email or name…"
               value={search} onChange={e => setSearch(e.target.value)}
               style={{ maxWidth: 320 }} />
        <div style={{ display: "flex", gap: 8 }}>
          <a href="/api/admin/users/export"
             className="btn btn-ghost"
             title="Download CSV of all members"
             style={{ textDecoration: "none" }}>
            Export CSV
          </a>
          <Btn onClick={() => setAdding(true)}>+ Add member</Btn>
        </div>
      </div>

      {users === null ? (
        <div style={{ height: 220 }} className="skeleton" />
      ) : users.length === 0 ? (
        <div className="empty-state"><h3>No users found</h3></div>
      ) : (
        <div style={{ background: "var(--surface)", border: "1px solid var(--border)", borderRadius: 10, overflow: "hidden" }}>
          <table>
            <thead>
              <tr>
                <th>Email</th>
                <th>Name</th>
                <th>Role</th>
                <th>Plan</th>
                <th>Status</th>
                <th>Last login</th>
                <th style={{ textAlign: "right" }}>Logins</th>
                <th>Joined</th>
                <th style={{ textAlign: "right" }}>Actions</th>
              </tr>
            </thead>
            <tbody>
              {users.map(u => {
                const statusColor = ({
                  active: "#16a34a",
                  comp: "#16a34a",
                  past_due: "#eab308",
                  canceled: "#dc2626",
                  pending: "var(--muted)",
                })[u.stripe_status] || "var(--subtext)";
                return (
                  <tr key={u.id} style={{ cursor: "pointer" }}
                      onClick={() => setViewingDetail(u.id)}>
                    <td style={{ fontWeight: 600 }}>{u.email}</td>
                    <td style={{ color: "var(--subtext)" }}>{u.full_name || "—"}</td>
                    <td>
                      <span style={{
                        fontSize: 11, fontWeight: 600, letterSpacing: "0.5px",
                        textTransform: "uppercase",
                        color: u.role === "admin" ? "var(--primary)" : "var(--muted)",
                      }}>{u.role}</span>
                    </td>
                    <td style={{ color: "var(--subtext)" }}>{u.plan || "—"}</td>
                    <td>
                      <span className={
                        u.stripe_status === 'active' || u.stripe_status === 'comp' ? 'status status-active' :
                        u.stripe_status === 'past_due' ? 'status status-warn' :
                        u.stripe_status === 'canceled' ? 'status status-danger' :
                        'status status-pending'
                      }>{u.stripe_status || "—"}</span>
                    </td>
                    <td style={{ color: "var(--muted)", fontSize: 12 }}>
                      {u.last_login_at ? fmtDate(u.last_login_at) : <span style={{ fontStyle: "italic" }}>never</span>}
                    </td>
                    <td style={{ textAlign: "right", color: "var(--subtext)" }}>{u.login_count || 0}</td>
                    <td style={{ color: "var(--muted)", fontSize: 12 }}>{fmtDate(u.created_at)}</td>
                    <td style={{ textAlign: "right" }} onClick={(e) => e.stopPropagation()}>
                      <Btn variant="ghost" size="sm" onClick={() => setEditing(u)}>Edit</Btn>
                      <Btn variant="ghost" size="sm" onClick={() => resetPassword(u)}>Reset PW</Btn>
                      {u.id !== self.id && <Btn variant="danger" size="sm" onClick={() => deleteUser(u)}>Delete</Btn>}
                    </td>
                  </tr>
                );
              })}
            </tbody>
          </table>
        </div>
      )}

      {adding && (
        <AddMemberModal onClose={() => setAdding(false)}
                        onAdded={() => { setAdding(false); refresh(); }} />
      )}

      {viewingDetail && (
        <MemberDetailModal userId={viewingDetail}
                           onClose={() => setViewingDetail(null)}
                           onChanged={refresh} />
      )}

      {editing && (
        <EditUserModal user={editing} self={self} open={!!editing}
                       onClose={() => setEditing(null)}
                       onUpdated={() => { setEditing(null); refresh(); }} />
      )}
      {confirming && (
        <ConfirmDialog open={!!confirming}
                       title={confirming.title}
                       message={confirming.message}
                       confirmLabel={confirming.confirmLabel}
                       danger={confirming.danger}
                       onConfirm={confirming.action}
                       onClose={() => setConfirming(null)} />
      )}
    </div>
  );
}

// =============================================================================
// Member detail modal (clicked from Users table) + Revenue dashboard + Activity
// =============================================================================

function fmtMoney(cents, currency = "EUR") {
  if (cents == null) return "—";
  const symbol = currency === "EUR" ? "€" : currency === "USD" ? "$" : (currency.toUpperCase() + " ");
  const v = cents / 100;
  return `${symbol}${v.toLocaleString(undefined, { minimumFractionDigits: 2, maximumFractionDigits: 2 })}`;
}

function AddMemberModal({ onClose, onAdded }) {
  const [email, setEmail] = useState("");
  const [fullName, setFullName] = useState("");
  const [role, setRole] = useState("member");
  const [tier, setTier] = useState("comp");
  const [autoPw, setAutoPw] = useState(true);
  const [password, setPassword] = useState("");
  // Access duration: "unlimited" or "until <date>".
  const [hasExpiry, setHasExpiry] = useState(false);
  const [expiryDate, setExpiryDate] = useState(""); // YYYY-MM-DD
  // Email the invite by default — admin can untick if they want to send creds manually.
  const [sendInviteEmail, setSendInviteEmail] = useState(true);
  const [busy, setBusy] = useState(false);

  async function handleSubmit(e) {
    if (e) e.preventDefault();
    if (hasExpiry && !expiryDate) {
      showToast("Pick an access end date or untick the limit", "error");
      return;
    }
    setBusy(true);
    try {
      const body = {
        email: email.trim(),
        fullName: fullName.trim() || null,
        role, tier,
        sendInviteEmail,
        accessExpiresAt: hasExpiry ? expiryDate : null,
      };
      if (!autoPw && password) body.password = password;
      const r = await api("POST", "/api/admin/users", body);
      // Tailor the success toast to what actually happened.
      const ie = r.inviteEmail;
      if (sendInviteEmail && ie && ie.ok) {
        showToast(`Member added — invite emailed to ${email}`, "success");
      } else if (sendInviteEmail && ie && ie.skipped) {
        showToast(`Member added — invite skipped (${ie.reason})`, "warn");
      } else if (sendInviteEmail) {
        showToast(`Member added — invite email failed; share the temp password manually`, "warn");
      } else {
        showToast("Member added", "success");
      }
      // If the email didn't actually go out (or we asked not to send),
      // surface the temp password as a fallback so the admin can share it.
      const emailDelivered = sendInviteEmail && ie && ie.ok;
      if (r.tempPassword && !emailDelivered) {
        window.prompt(`Temporary password for ${email} — copy and share securely:`, r.tempPassword);
      }
      onAdded();
    } catch (e) { showToast(e.message, "error"); }
    finally { setBusy(false); }
  }

  return (
    <Modal open onClose={busy ? () => {} : onClose} title="Add a member" footer={
      <>
        <Btn variant="ghost" onClick={onClose} disabled={busy}>Cancel</Btn>
        <Btn onClick={handleSubmit} disabled={busy || !email.trim()}>
          {busy ? <Spinner /> : "Create account"}
        </Btn>
      </>
    }>
      <form onSubmit={handleSubmit}>
        <Field label="Email" htmlFor="add-email">
          <input id="add-email" type="email" required value={email}
                 onChange={e => setEmail(e.target.value)} autoFocus />
        </Field>
        <Field label="Full name (optional)" htmlFor="add-fn">
          <input id="add-fn" type="text" value={fullName}
                 onChange={e => setFullName(e.target.value)} />
        </Field>
        <Field label="Access tier" htmlFor="add-tier"
               hint="Comp = full hub access (no billing). Free = limited preview (sample reports + crisis teasers).">
          <select id="add-tier" value={tier} onChange={e => setTier(e.target.value)}>
            <option value="comp">Comp · full access (free for them)</option>
            <option value="free">Free tier · limited preview</option>
          </select>
        </Field>
        <Field label="Role" htmlFor="add-role">
          <select id="add-role" value={role} onChange={e => setRole(e.target.value)}>
            <option value="member">Member</option>
            <option value="admin">Admin (full hub control)</option>
          </select>
        </Field>
        <Field>
          <label style={{ display: "flex", alignItems: "center", gap: 10, cursor: "pointer", textTransform: "none", letterSpacing: 0, fontSize: 14, color: "var(--text)" }}>
            <input type="checkbox" checked={autoPw}
                   onChange={e => setAutoPw(e.target.checked)}
                   style={{ width: "auto" }} />
            Auto-generate temporary password
          </label>
        </Field>
        {!autoPw && (
          <Field label="Password" htmlFor="add-pw" hint="Min 8 characters. Share securely.">
            <input id="add-pw" type="text" minLength={8} value={password}
                   onChange={e => setPassword(e.target.value)} required />
          </Field>
        )}

        {/* Access duration */}
        <Field label="Access duration"
               hint="Leave unlimited for permanent access, or pick a date for trials, evaluations, or fixed-term comp accounts.">
          <div style={{ display: "flex", flexDirection: "column", gap: 8 }}>
            <label style={{ display: "flex", alignItems: "center", gap: 10, cursor: "pointer", fontSize: 14, color: "var(--text)" }}>
              <input type="radio" name="add-expiry" checked={!hasExpiry}
                     onChange={() => setHasExpiry(false)}
                     style={{ width: "auto" }} />
              Unlimited
            </label>
            <label style={{ display: "flex", alignItems: "center", gap: 10, cursor: "pointer", fontSize: 14, color: "var(--text)" }}>
              <input type="radio" name="add-expiry" checked={hasExpiry}
                     onChange={() => setHasExpiry(true)}
                     style={{ width: "auto" }} />
              Until
              <input type="date"
                     value={expiryDate}
                     onChange={e => { setExpiryDate(e.target.value); setHasExpiry(true); }}
                     min={new Date().toISOString().slice(0, 10)}
                     style={{ width: 180, marginLeft: 4 }} />
            </label>
            {hasExpiry && expiryDate && (
              <div style={{ fontSize: 11, color: "var(--muted)", paddingLeft: 26 }}>
                Access ends at end-of-day on {new Date(expiryDate).toLocaleDateString()}.
                After that they cannot log in.
              </div>
            )}
          </div>
        </Field>

        {/* Send invite email */}
        <Field>
          <label style={{ display: "flex", alignItems: "center", gap: 10, cursor: "pointer", textTransform: "none", letterSpacing: 0, fontSize: 14, color: "var(--text)" }}>
            <input type="checkbox" checked={sendInviteEmail}
                   onChange={e => setSendInviteEmail(e.target.checked)}
                   style={{ width: "auto" }} />
            <span>
              <strong>Email invite</strong> with login URL + temporary password
              <span style={{ fontSize: 11, color: "var(--muted)", fontWeight: 400, display: "block", marginTop: 2 }}>
                Untick if you'll share credentials yourself. If sending fails, the temp password is shown afterwards as a fallback.
              </span>
            </span>
          </label>
        </Field>
      </form>
    </Modal>
  );
}

function MemberDetailModal({ userId, onClose, onChanged }) {
  const [data, setData] = useState(null);
  // Editable access fields (separate from `data` so we don't lose user input on refetch).
  const [accessHasExpiry, setAccessHasExpiry] = useState(false);
  const [accessExpiry, setAccessExpiry] = useState("");
  const [accessBusy, setAccessBusy] = useState(false);
  const [resendBusy, setResendBusy] = useState(false);

  function loadDetail() {
    return api("GET", `/api/admin/analytics/members/${userId}`).then(d => {
      setData(d);
      // Seed the access edit state from current value.
      if (d.member && d.member.access_expires_at) {
        setAccessHasExpiry(true);
        // The DB stores ISO. Trim to YYYY-MM-DD for the date input.
        setAccessExpiry(String(d.member.access_expires_at).slice(0, 10));
      } else {
        setAccessHasExpiry(false);
        setAccessExpiry("");
      }
    });
  }

  useEffect(() => {
    loadDetail().catch(e => {
      showToast(e.message, "error");
      onClose();
    });
  }, [userId]);

  async function saveAccess() {
    if (accessHasExpiry && !accessExpiry) {
      showToast("Pick a date or switch to Unlimited", "error");
      return;
    }
    setAccessBusy(true);
    try {
      await api("PATCH", `/api/admin/users/${userId}`, {
        accessExpiresAt: accessHasExpiry ? accessExpiry : null,
      });
      showToast("Access updated", "success");
      await loadDetail();
      if (onChanged) onChanged();
    } catch (e) { showToast(e.message, "error"); }
    finally { setAccessBusy(false); }
  }

  async function resendInvite() {
    setResendBusy(true);
    try {
      const r = await api("PATCH", `/api/admin/users/${userId}`, {
        resetPassword: true,
        sendInviteEmail: true,
      });
      const ie = r.inviteEmail;
      if (ie && ie.ok) {
        showToast(`Invite resent — new temporary password emailed`, "success");
      } else if (r.tempPassword) {
        // Email failed but we have a fresh temp password to share manually.
        showToast(`Email send failed — new temp password generated, share manually`, "warn");
        window.prompt("Temporary password — copy and share securely:", r.tempPassword);
      } else {
        showToast("Could not resend invite", "error");
      }
      if (onChanged) onChanged();
    } catch (e) { showToast(e.message, "error"); }
    finally { setResendBusy(false); }
  }

  if (!data) {
    return (
      <Modal open onClose={onClose} title="Loading…" size="xl">
        <div className="skeleton" style={{ height: 400 }} />
      </Modal>
    );
  }

  const m = data.member;
  const counts = data.counts || {};
  const expiryStatus = m.access_expires_at
    ? (new Date(m.access_expires_at).getTime() < Date.now()
        ? { label: "EXPIRED", color: "var(--warn, #dc2626)" }
        : null)
    : null;

  const KV = ({ label, value }) => (
    <div style={{ display: "flex", justifyContent: "space-between", padding: "6px 0", borderBottom: "1px solid var(--border)", fontSize: 13 }}>
      <span style={{ color: "var(--subtext)" }}>{label}</span>
      <span style={{ color: "var(--text)", fontWeight: 600 }}>{value}</span>
    </div>
  );

  return (
    <Modal open onClose={onClose}
           title={m.full_name || m.email}
           size="xl"
           footer={<Btn variant="ghost" onClick={onClose}>Close</Btn>}>
      <div style={{ display: "grid", gridTemplateColumns: "1fr 1fr", gap: 24 }}>
        {/* Left: profile + subscription */}
        <div>
          <h3 style={{ fontSize: 13, fontWeight: 700, marginBottom: 10, color: "var(--subtext)", textTransform: "uppercase", letterSpacing: "0.5px" }}>Profile</h3>
          <KV label="Email" value={m.email} />
          <KV label="Role" value={m.role} />
          <KV label="Joined" value={fmtDateTime(m.created_at)} />
          <KV label="Email verified" value={m.email_verified ? "Yes" : "No"} />
          <KV label="Email opt-out" value={m.email_opt_out ? "Yes (no notification emails)" : "No"} />
          <KV label="Welcome email sent" value={m.welcome_email_sent_at ? fmtDateTime(m.welcome_email_sent_at) : "Not yet"} />

          <h3 style={{ fontSize: 13, fontWeight: 700, marginTop: 24, marginBottom: 10, color: "var(--subtext)", textTransform: "uppercase", letterSpacing: "0.5px" }}>Subscription</h3>
          <KV label="Status" value={m.stripe_status || "—"} />
          <KV label="Plan" value={m.plan || "—"} />
          {m.stripe_customer_id && (
            <KV label="Stripe customer" value={
              <a href={`https://dashboard.stripe.com/${m.stripe_status === 'comp' ? '' : 'test/'}customers/${m.stripe_customer_id}`}
                 target="_blank" rel="noreferrer"
                 style={{ color: "var(--primary)", fontSize: 12 }}>
                {m.stripe_customer_id} ↗
              </a>
            } />
          )}
          {data.lifetimeRevenue && (
            <KV label="Lifetime revenue" value={fmtMoney(data.lifetimeRevenue.amountCents, (data.lifetimeRevenue.currency || "eur").toUpperCase())} />
          )}

          {/* Access duration + invite resend */}
          <h3 style={{ fontSize: 13, fontWeight: 700, marginTop: 24, marginBottom: 10, color: "var(--subtext)", textTransform: "uppercase", letterSpacing: "0.5px" }}>Access</h3>
          <KV label="Currently"
              value={m.access_expires_at
                ? <>
                    {new Date(m.access_expires_at).toLocaleString()}
                    {expiryStatus && (
                      <span style={{ color: expiryStatus.color, fontSize: 11, fontWeight: 700, marginLeft: 8 }}>
                        {expiryStatus.label}
                      </span>
                    )}
                  </>
                : "Unlimited"} />

          <div style={{ marginTop: 12, padding: "12px 14px", background: "var(--surface2)", border: "1px solid var(--border)", borderRadius: 10 }}>
            <div style={{ fontSize: 11, fontWeight: 700, color: "var(--subtext)", textTransform: "uppercase", letterSpacing: "0.5px", marginBottom: 8 }}>
              Change access duration
            </div>
            <label style={{ display: "flex", alignItems: "center", gap: 8, fontSize: 13, marginBottom: 6, cursor: "pointer" }}>
              <input type="radio" name={`acc-exp-${userId}`} checked={!accessHasExpiry}
                     onChange={() => setAccessHasExpiry(false)} style={{ width: "auto" }} />
              Unlimited
            </label>
            <label style={{ display: "flex", alignItems: "center", gap: 8, fontSize: 13, cursor: "pointer" }}>
              <input type="radio" name={`acc-exp-${userId}`} checked={accessHasExpiry}
                     onChange={() => setAccessHasExpiry(true)} style={{ width: "auto" }} />
              Until
              <input type="date"
                     value={accessExpiry}
                     onChange={e => { setAccessExpiry(e.target.value); setAccessHasExpiry(true); }}
                     style={{ width: 170, marginLeft: 4 }} />
            </label>
            <div style={{ marginTop: 10, display: "flex", justifyContent: "flex-end" }}>
              <Btn size="sm" onClick={saveAccess}
                   disabled={accessBusy
                     || (accessHasExpiry === !!m.access_expires_at
                          && (!accessHasExpiry || accessExpiry === String(m.access_expires_at || "").slice(0,10)))}>
                {accessBusy ? <Spinner /> : "Save"}
              </Btn>
            </div>
          </div>

          <div style={{ marginTop: 12, padding: "12px 14px", background: "var(--surface2)", border: "1px solid var(--border)", borderRadius: 10 }}>
            <div style={{ fontSize: 11, fontWeight: 700, color: "var(--subtext)", textTransform: "uppercase", letterSpacing: "0.5px", marginBottom: 6 }}>
              Resend invite
            </div>
            <div style={{ fontSize: 12, color: "var(--muted)", marginBottom: 10, lineHeight: 1.5 }}>
              Generates a fresh temporary password and emails the member the sign-in link. Use this if they lost the original invite or never received it.
            </div>
            <Btn size="sm" variant="ghost" onClick={resendInvite} disabled={resendBusy}>
              {resendBusy ? <Spinner /> : "Reset password & resend invite"}
            </Btn>
          </div>
        </div>

        {/* Right: activity */}
        <div>
          <h3 style={{ fontSize: 13, fontWeight: 700, marginBottom: 10, color: "var(--subtext)", textTransform: "uppercase", letterSpacing: "0.5px" }}>Activity</h3>
          <KV label="Logins (lifetime)" value={m.login_count || 0} />
          <KV label="Last login" value={m.last_login_at ? fmtDateTime(m.last_login_at) : "Never"} />
          <KV label="Reports downloaded" value={counts.downloads || 0} />
          <KV label="Reports viewed (inline)" value={counts.views || 0} />
          <KV label="Chat messages sent" value={counts.chat_messages_sent || 0} />
          <KV label="DMs sent" value={counts.dms_sent || 0} />
          <KV label="Unread notifications" value={counts.unread_notifications || 0} />

          {data.recentDownloads && data.recentDownloads.length > 0 && (
            <>
              <h3 style={{ fontSize: 13, fontWeight: 700, marginTop: 24, marginBottom: 10, color: "var(--subtext)", textTransform: "uppercase", letterSpacing: "0.5px" }}>Recent downloads / views</h3>
              <div style={{ fontSize: 12 }}>
                {data.recentDownloads.map(d => (
                  <div key={d.id} style={{ display: "flex", justifyContent: "space-between", padding: "6px 0", borderBottom: "1px solid var(--border)", gap: 8 }}>
                    <span style={{ overflow: "hidden", textOverflow: "ellipsis", whiteSpace: "nowrap", flex: 1 }}>
                      {d.report_title || "(deleted report)"}
                    </span>
                    <span style={{ color: d.is_download ? "var(--primary)" : "var(--subtext)", fontWeight: 600, flexShrink: 0, fontSize: 11 }}>
                      {d.is_download ? "↓ Download" : "View"}
                    </span>
                    <span style={{ color: "var(--muted)", flexShrink: 0, fontSize: 11 }}>
                      {fmtDate(d.downloaded_at)}
                    </span>
                  </div>
                ))}
              </div>
            </>
          )}

          {data.recentCharges && data.recentCharges.length > 0 && (
            <>
              <h3 style={{ fontSize: 13, fontWeight: 700, marginTop: 24, marginBottom: 10, color: "var(--subtext)", textTransform: "uppercase", letterSpacing: "0.5px" }}>Recent charges</h3>
              <div style={{ fontSize: 12 }}>
                {data.recentCharges.map(c => (
                  <div key={c.id} style={{ display: "flex", justifyContent: "space-between", padding: "6px 0", borderBottom: "1px solid var(--border)" }}>
                    <span style={{ color: "var(--subtext)" }}>{new Date(c.created * 1000).toLocaleDateString()}</span>
                    <span style={{ color: c.status === "succeeded" ? "var(--success)" : "var(--warn)", fontWeight: 600 }}>
                      {fmtMoney(c.amount, (c.currency || "eur").toUpperCase())}
                    </span>
                  </div>
                ))}
              </div>
            </>
          )}
        </div>
      </div>

      <h3 style={{ fontSize: 13, fontWeight: 700, marginTop: 28, marginBottom: 10, color: "var(--subtext)", textTransform: "uppercase", letterSpacing: "0.5px" }}>Recent logins</h3>
      {(!data.logins || data.logins.length === 0) ? (
        <div style={{ color: "var(--muted)", fontSize: 13, fontStyle: "italic" }}>No login records yet.</div>
      ) : (
        <div style={{ maxHeight: 280, overflowY: "auto", border: "1px solid var(--border)", borderRadius: 8 }}>
          <table>
            <thead>
              <tr>
                <th>When</th>
                <th>IP</th>
                <th>User agent</th>
              </tr>
            </thead>
            <tbody>
              {data.logins.map(l => (
                <tr key={l.id}>
                  <td style={{ fontSize: 12 }}>{fmtDateTime(l.logged_in_at)}</td>
                  <td style={{ fontSize: 12, color: "var(--muted)" }}>{l.ip || "—"}</td>
                  <td style={{ fontSize: 11, color: "var(--muted)", maxWidth: 360, overflow: "hidden", textOverflow: "ellipsis", whiteSpace: "nowrap" }}>
                    {l.user_agent || "—"}
                  </td>
                </tr>
              ))}
            </tbody>
          </table>
        </div>
      )}
    </Modal>
  );
}

// =============================================================================
// Revenue dashboard
// =============================================================================

function AdminRevenueTab() {
  const [days, setDays] = useState(30);
  const [data, setData] = useState(null);
  const [loading, setLoading] = useState(true);

  function load() {
    setLoading(true);
    api("GET", `/api/admin/analytics/revenue?days=${days}`)
      .then(d => { setData(d); setLoading(false); })
      .catch(e => { showToast(e.message, "error"); setLoading(false); setData({ error: e.message }); });
  }

  useEffect(() => { load(); }, [days]);

  const StatCard = ({ label, value, sub, accent }) => (
    <div style={{
      background: "var(--surface)", border: "1px solid var(--border-soft)",
      borderRadius: "var(--r-md)",
      padding: "20px 22px", flex: 1, minWidth: 180,
      boxShadow: "var(--shadow-card)",
    }}>
      <div style={{ fontSize: 10, fontWeight: 700, color: "var(--muted)", textTransform: "uppercase", letterSpacing: "1.5px", marginBottom: 10 }}>
        {label}
      </div>
      <div className="serif" style={{ fontSize: 28, fontWeight: 700, color: accent || "var(--text)", letterSpacing: "-0.015em", lineHeight: 1.1 }}>{value}</div>
      {sub && <div style={{ fontSize: 12, color: "var(--muted)", marginTop: 6, lineHeight: 1.5 }}>{sub}</div>}
    </div>
  );

  if (loading) {
    return <div style={{ padding: 28 }}><div className="skeleton" style={{ height: 200 }} /></div>;
  }

  if (!data || !data.stripeConnected) {
    return (
      <div style={{ padding: "20px 28px" }}>
        <div style={{ background: "rgba(234,179,8,0.1)", border: "1px solid rgba(234,179,8,0.4)", borderRadius: 10, padding: 18 }}>
          <div style={{ fontSize: 14, fontWeight: 700, color: "var(--warn)", marginBottom: 6 }}>Stripe data unavailable</div>
          <div style={{ fontSize: 13, color: "var(--text)", marginBottom: 8 }}>{data?.error || "Stripe not configured"}</div>
          {data?.hint && <div style={{ fontSize: 13, color: "var(--subtext)" }}>{data.hint}</div>}
        </div>
      </div>
    );
  }

  const cur = (data.currency || "eur").toUpperCase();

  return (
    <div style={{ padding: "20px 28px" }}>
      <div style={{ display: "flex", justifyContent: "space-between", alignItems: "center", marginBottom: 18 }}>
        <h2 style={{ fontSize: 16, fontWeight: 700 }}>Revenue · last {data.period.days} days</h2>
        <div style={{ display: "flex", gap: 4 }}>
          {[7, 30, 90, 180, 365].map(d => (
            <TabBtn key={d} active={days === d} onClick={() => setDays(d)}>{d}d</TabBtn>
          ))}
        </div>
      </div>

      <div style={{ display: "flex", gap: 12, flexWrap: "wrap", marginBottom: 24 }}>
        <StatCard label="MRR" value={fmtMoney(data.mrrCents, cur)}
                  sub="Monthly recurring revenue"
                  accent="var(--primary)" />
        <StatCard label="Active subscribers" value={data.activeSubscriptions}
                  sub={data.activeSubscriptions === 1 ? "1 paid member" : `${data.activeSubscriptions} paid members`} />
        <StatCard label={`New (${data.period.days}d)`} value={data.newSubscriptions}
                  sub="Subscriptions created" />
        <StatCard label={`Canceled (${data.period.days}d)`} value={data.canceledSubscriptions}
                  sub="Subscriptions ended"
                  accent={data.canceledSubscriptions > 0 ? "var(--danger)" : undefined} />
      </div>

      <div style={{ display: "flex", gap: 12, flexWrap: "wrap", marginBottom: 24 }}>
        <StatCard label="Gross revenue" value={fmtMoney(data.grossRevenueCents, cur)}
                  sub={`In the last ${data.period.days} days`} />
        <StatCard label="Refunded" value={fmtMoney(data.refundedCents, cur)}
                  accent={data.refundedCents > 0 ? "var(--warn)" : undefined} />
        <StatCard label="Net revenue" value={fmtMoney(data.netRevenueCents, cur)}
                  accent="var(--success)" />
      </div>

      <h3 style={{ fontSize: 14, fontWeight: 700, marginBottom: 12, marginTop: 24 }}>
        Recent transactions ({data.recentTransactions.length})
      </h3>
      {data.recentTransactions.length === 0 ? (
        <div className="empty-state">
          <p>No transactions in this period yet.</p>
        </div>
      ) : (
        <div style={{ background: "var(--surface)", border: "1px solid var(--border)", borderRadius: 10, overflow: "hidden" }}>
          <table>
            <thead>
              <tr>
                <th>Date</th>
                <th>Customer</th>
                <th>Description</th>
                <th>Status</th>
                <th style={{ textAlign: "right" }}>Amount</th>
              </tr>
            </thead>
            <tbody>
              {data.recentTransactions.map(t => (
                <tr key={t.id}>
                  <td style={{ fontSize: 12 }}>{new Date(t.created * 1000).toLocaleDateString()}</td>
                  <td style={{ fontSize: 13 }}>{t.customerName || t.customerEmail || "—"}</td>
                  <td style={{ fontSize: 12, color: "var(--subtext)" }}>{t.description || "—"}</td>
                  <td>
                    <span style={{ fontSize: 12, color: t.status === "succeeded" ? "var(--success)" : "var(--warn)", fontWeight: 600 }}>
                      {t.status}
                    </span>
                    {t.refunded && <span style={{ marginLeft: 6, fontSize: 11, color: "var(--warn)" }}>refunded</span>}
                  </td>
                  <td style={{ textAlign: "right", fontWeight: 600 }}>{fmtMoney(t.amount, (t.currency || "eur").toUpperCase())}</td>
                </tr>
              ))}
            </tbody>
          </table>
        </div>
      )}
    </div>
  );
}

// =============================================================================
// Activity tab — daily login chart + global stats
// =============================================================================

function AdminActivityTab() {
  const [days, setDays] = useState(30);
  const [data, setData] = useState(null);

  useEffect(() => {
    setData(null);
    api("GET", `/api/admin/analytics/usage?days=${days}`).then(setData).catch(e => showToast(e.message, "error"));
  }, [days]);

  if (!data) return <div style={{ padding: 28 }}><div className="skeleton" style={{ height: 200 }} /></div>;

  const maxLogins = Math.max(1, ...data.byDay.map(d => d.logins));

  return (
    <div style={{ padding: "20px 28px" }}>
      <div style={{ display: "flex", justifyContent: "space-between", alignItems: "center", marginBottom: 18 }}>
        <h2 style={{ fontSize: 16, fontWeight: 700 }}>Activity · last {data.period.days} days</h2>
        <div style={{ display: "flex", gap: 4 }}>
          {[7, 30, 90].map(d => (
            <TabBtn key={d} active={days === d} onClick={() => setDays(d)}>{d}d</TabBtn>
          ))}
        </div>
      </div>

      <div style={{ display: "flex", gap: 12, flexWrap: "wrap", marginBottom: 24 }}>
        <div style={{ background: "var(--surface)", border: "1px solid var(--border)", borderRadius: 10, padding: "16px 18px", flex: 1, minWidth: 160 }}>
          <div style={{ fontSize: 11, fontWeight: 700, color: "var(--subtext)", textTransform: "uppercase", letterSpacing: "0.5px", marginBottom: 6 }}>Total members</div>
          <div style={{ fontSize: 24, fontWeight: 800 }}>{data.totals.totalUsers}</div>
        </div>
        <div style={{ background: "var(--surface)", border: "1px solid var(--border)", borderRadius: 10, padding: "16px 18px", flex: 1, minWidth: 160 }}>
          <div style={{ fontSize: 11, fontWeight: 700, color: "var(--subtext)", textTransform: "uppercase", letterSpacing: "0.5px", marginBottom: 6 }}>Active accounts</div>
          <div style={{ fontSize: 24, fontWeight: 800, color: "var(--success)" }}>{data.totals.activeUsers}</div>
        </div>
        <div style={{ background: "var(--surface)", border: "1px solid var(--border)", borderRadius: 10, padding: "16px 18px", flex: 1, minWidth: 160 }}>
          <div style={{ fontSize: 11, fontWeight: 700, color: "var(--subtext)", textTransform: "uppercase", letterSpacing: "0.5px", marginBottom: 6 }}>Logged in (period)</div>
          <div style={{ fontSize: 24, fontWeight: 800 }}>{data.totals.activeInPeriod}</div>
          <div style={{ fontSize: 11, color: "var(--muted)", marginTop: 2 }}>distinct users</div>
        </div>
      </div>

      <h3 style={{ fontSize: 14, fontWeight: 700, marginBottom: 12 }}>Logins per day</h3>
      {data.byDay.length === 0 ? (
        <div className="empty-state"><p>No login activity in this period.</p></div>
      ) : (
        <div style={{ background: "var(--surface)", border: "1px solid var(--border)", borderRadius: 10, padding: 20 }}>
          <div style={{ display: "flex", alignItems: "flex-end", gap: 4, height: 200 }}>
            {data.byDay.map(d => {
              const h = Math.max(2, (d.logins / maxLogins) * 180);
              return (
                <div key={d.date} title={`${d.date}: ${d.logins} login${d.logins === 1 ? "" : "s"} from ${d.unique_users} user${d.unique_users === 1 ? "" : "s"}`}
                     style={{
                       flex: 1, height: h, background: "var(--primary)", opacity: 0.8,
                       borderRadius: "3px 3px 0 0", minWidth: 4,
                     }} />
              );
            })}
          </div>
          <div style={{ display: "flex", justifyContent: "space-between", marginTop: 8, fontSize: 11, color: "var(--muted)" }}>
            <span>{data.byDay[0]?.date}</span>
            <span>{data.byDay[data.byDay.length - 1]?.date}</span>
          </div>
        </div>
      )}
    </div>
  );
}

function InviteUserModal({ open, onClose, onInvited }) {
  const [email, setEmail] = useState("");
  const [fullName, setFullName] = useState("");
  const [role, setRole] = useState("member");
  const [password, setPassword] = useState("");
  const [autoPw, setAutoPw] = useState(true);
  const [busy, setBusy] = useState(false);

  useEffect(() => { if (open) { setEmail(""); setFullName(""); setRole("member"); setPassword(""); setAutoPw(true); } }, [open]);

  async function handleSubmit(e) {
    e.preventDefault();
    setBusy(true);
    try {
      const body = { email, fullName, role };
      if (!autoPw && password) body.password = password;
      const r = await api("POST", "/api/admin/users", body);
      showToast("Member invited", "success");
      if (r.tempPassword) {
        window.prompt(`Temporary password for ${email} — copy and share securely:`, r.tempPassword);
      }
      onInvited();
    } catch (e) { showToast(e.message, "error"); }
    finally { setBusy(false); }
  }

  return (
    <Modal open={open} onClose={busy ? () => {} : onClose} title="Invite a member" footer={
      <>
        <Btn variant="ghost" onClick={onClose} disabled={busy}>Cancel</Btn>
        <Btn onClick={handleSubmit} disabled={busy || !email}>{busy ? <Spinner /> : "Create account"}</Btn>
      </>
    }>
      <form onSubmit={handleSubmit}>
        <Field label="Email" htmlFor="inv-email">
          <input id="inv-email" type="email" required value={email} onChange={e => setEmail(e.target.value)} />
        </Field>
        <Field label="Full name (optional)" htmlFor="inv-fn">
          <input id="inv-fn" type="text" value={fullName} onChange={e => setFullName(e.target.value)} />
        </Field>
        <Field label="Role" htmlFor="inv-role">
          <select id="inv-role" value={role} onChange={e => setRole(e.target.value)}>
            <option value="member">Member</option>
            <option value="admin">Admin (full access — including this panel)</option>
          </select>
        </Field>
        <Field>
          <label style={{ display: "flex", alignItems: "center", gap: 10, cursor: "pointer", textTransform: "none", letterSpacing: 0, fontSize: 14, color: "var(--text)" }}>
            <input type="checkbox" checked={autoPw} onChange={e => setAutoPw(e.target.checked)}
                   style={{ width: "auto" }} />
            Auto-generate temporary password
          </label>
        </Field>
        {!autoPw && (
          <Field label="Password" htmlFor="inv-pw" hint="Min 8 characters. Share securely with the invitee.">
            <input id="inv-pw" type="text" minLength={8} value={password} onChange={e => setPassword(e.target.value)} required />
          </Field>
        )}
      </form>
    </Modal>
  );
}

function EditUserModal({ user, self, open, onClose, onUpdated }) {
  const [fullName, setFullName] = useState(user.full_name || "");
  const [role, setRole] = useState(user.role);
  const [busy, setBusy] = useState(false);
  const isSelf = user.id === self.id;

  async function handleSubmit(e) {
    e.preventDefault();
    setBusy(true);
    try {
      await api("PATCH", `/api/admin/users/${user.id}`, { fullName, role });
      showToast("Saved", "success");
      onUpdated();
    } catch (e) { showToast(e.message, "error"); }
    finally { setBusy(false); }
  }

  return (
    <Modal open={open} onClose={busy ? () => {} : onClose} title={`Edit ${user.email}`} footer={
      <>
        <Btn variant="ghost" onClick={onClose} disabled={busy}>Cancel</Btn>
        <Btn onClick={handleSubmit} disabled={busy}>{busy ? <Spinner /> : "Save"}</Btn>
      </>
    }>
      <form onSubmit={handleSubmit}>
        <Field label="Full name" htmlFor="eu-fn">
          <input id="eu-fn" type="text" value={fullName} onChange={e => setFullName(e.target.value)} />
        </Field>
        <Field label="Role" htmlFor="eu-role" hint={isSelf ? "You can't remove your own admin role." : ""}>
          <select id="eu-role" value={role} onChange={e => setRole(e.target.value)} disabled={isSelf}>
            <option value="member">Member</option>
            <option value="admin">Admin</option>
          </select>
        </Field>
      </form>
    </Modal>
  );
}

function AdminReportsTab({ user, refreshKey, onChange }) {
  const [showArchive, setShowArchive] = useState(false);
  return (
    <div style={{ display: "flex", flexDirection: "column" }}>
      <div style={{ padding: "12px 28px 0", display: "flex", gap: 8 }}>
        <TabBtn active={!showArchive} onClick={() => setShowArchive(false)}>Active</TabBtn>
        <TabBtn active={showArchive} onClick={() => setShowArchive(true)}>Archive</TabBtn>
      </div>
      <ReportsView user={user} archived={showArchive} refreshKey={refreshKey} onChange={onChange} />
    </div>
  );
}

// =============================================================================
// AdminScheduledTab — pending scheduled reports + chat messages, with cancel
// =============================================================================

function AdminScheduledTab() {
  const [reports, setReports] = useState(null);
  const [chats, setChats] = useState(null);
  const [busy, setBusy] = useState(null); // { kind, id } currently being mutated
  const [confirming, setConfirming] = useState(null);
  const [tick, setTick] = useState(0);

  function load() {
    api("GET", "/api/admin/reports/scheduled?status=pending")
      .then(d => setReports(d.scheduled || []))
      .catch(e => { showToast(e.message, "error"); setReports([]); });
    api("GET", "/api/admin/chat/scheduled?status=pending")
      .then(d => setChats(d.scheduled || []))
      .catch(e => { showToast(e.message, "error"); setChats([]); });
  }

  useEffect(() => { load(); }, [tick]);
  // Refresh every 30s so freshly-published items disappear from the list
  // without needing a manual refresh.
  useEffect(() => {
    const t = setInterval(() => setTick(x => x + 1), 30000);
    return () => clearInterval(t);
  }, []);

  async function cancelReport(id) {
    setBusy({ kind: "report", id });
    try {
      await api("DELETE", `/api/admin/reports/scheduled/${id}`);
      showToast("Scheduled upload canceled", "success");
      setTick(x => x + 1);
    } catch (e) { showToast(e.message, "error"); }
    finally { setBusy(null); }
  }

  async function cancelChat(id) {
    setBusy({ kind: "chat", id });
    try {
      await api("DELETE", `/api/admin/chat/scheduled/${id}`);
      showToast("Scheduled message canceled", "success");
      setTick(x => x + 1);
    } catch (e) { showToast(e.message, "error"); }
    finally { setBusy(null); }
  }

  function fmtIn(iso) {
    if (!iso) return "—";
    const d = new Date(iso);
    if (isNaN(d.getTime())) return iso;
    const diffMs = d.getTime() - Date.now();
    const mins = Math.round(diffMs / 60000);
    const abs = d.toLocaleString();
    if (diffMs < 0) return `${abs} (overdue)`;
    if (mins < 60) return `${abs} (in ${mins} min)`;
    if (mins < 1440) return `${abs} (in ${Math.round(mins / 60)} h)`;
    return `${abs} (in ${Math.round(mins / 1440)} d)`;
  }

  const cellHeader = {
    padding: "10px 12px", fontSize: 11, fontWeight: 700,
    color: "var(--muted)", textTransform: "uppercase", letterSpacing: "0.5px",
    background: "var(--surface2)", borderBottom: "1px solid var(--border)",
    textAlign: "left",
  };
  const cell = { padding: "12px", borderBottom: "1px solid var(--border)", verticalAlign: "top", fontSize: 13 };

  return (
    <div style={{ padding: "20px 28px" }}>
      <div style={{ display: "flex", justifyContent: "space-between", alignItems: "baseline", marginBottom: 16 }}>
        <div>
          <h2 className="serif" style={{ fontSize: 19, fontWeight: 700, marginBottom: 4 }}>Scheduled publishing</h2>
          <div style={{ fontSize: 12, color: "var(--muted)", lineHeight: 1.5 }}>
            Reports and chat messages queued to fire automatically. The cron worker checks every minute.
          </div>
        </div>
        <Btn variant="ghost" onClick={() => setTick(x => x + 1)}>Refresh</Btn>
      </div>

      <h3 style={{ fontSize: 14, fontWeight: 700, marginBottom: 8, color: "var(--subtext)", textTransform: "uppercase", letterSpacing: "0.5px" }}>
        Reports ({reports ? reports.length : "—"})
      </h3>
      {reports === null ? (
        <div className="skeleton" style={{ height: 80, marginBottom: 24 }} />
      ) : reports.length === 0 ? (
        <div style={{ padding: 16, fontSize: 13, color: "var(--muted)", border: "1px dashed var(--border)", borderRadius: 10, marginBottom: 24 }}>
          No scheduled reports.
        </div>
      ) : (
        <div style={{ border: "1px solid var(--border)", borderRadius: 10, overflow: "hidden", marginBottom: 28 }}>
          <table style={{ width: "100%", borderCollapse: "collapse" }}>
            <thead>
              <tr>
                <th style={cellHeader}>Title</th>
                <th style={cellHeader}>Scheduled for</th>
                <th style={cellHeader}>File</th>
                <th style={cellHeader}>Notify</th>
                <th style={{ ...cellHeader, textAlign: "right" }}>Actions</th>
              </tr>
            </thead>
            <tbody>
              {reports.map(r => (
                <tr key={r.id}>
                  <td style={cell}>
                    <div style={{ fontWeight: 600 }}>{r.title}</div>
                    {r.summary && <div style={{ fontSize: 12, color: "var(--muted)", marginTop: 2 }}>{r.summary.slice(0, 120)}</div>}
                  </td>
                  <td style={cell}>{fmtIn(r.scheduledFor)}</td>
                  <td style={cell}>
                    <div>{r.fileName}</div>
                    <div style={{ fontSize: 11, color: "var(--muted)" }}>{fmtBytes(r.fileSize || 0)}</div>
                  </td>
                  <td style={cell}>
                    {[r.notifyChat && "chat", r.notifyEmail && "email"].filter(Boolean).join(" + ") || "feed only"}
                  </td>
                  <td style={{ ...cell, textAlign: "right" }}>
                    <Btn variant="danger" disabled={busy && busy.kind === "report" && busy.id === r.id}
                         onClick={() => setConfirming({
                           title: "Cancel scheduled upload?",
                           message: `"${r.title}" will not be published. The staged file will be deleted.`,
                           confirmLabel: "Cancel upload",
                           danger: true,
                           action: () => { setConfirming(null); cancelReport(r.id); },
                         })}>
                      {busy && busy.kind === "report" && busy.id === r.id ? <Spinner /> : "Cancel"}
                    </Btn>
                  </td>
                </tr>
              ))}
            </tbody>
          </table>
        </div>
      )}

      <h3 style={{ fontSize: 14, fontWeight: 700, marginBottom: 8, color: "var(--subtext)", textTransform: "uppercase", letterSpacing: "0.5px" }}>
        Chat messages ({chats ? chats.length : "—"})
      </h3>
      {chats === null ? (
        <div className="skeleton" style={{ height: 80 }} />
      ) : chats.length === 0 ? (
        <div style={{ padding: 16, fontSize: 13, color: "var(--muted)", border: "1px dashed var(--border)", borderRadius: 10 }}>
          No scheduled chat messages.
        </div>
      ) : (
        <div style={{ border: "1px solid var(--border)", borderRadius: 10, overflow: "hidden" }}>
          <table style={{ width: "100%", borderCollapse: "collapse" }}>
            <thead>
              <tr>
                <th style={cellHeader}>Message</th>
                <th style={cellHeader}>Channel</th>
                <th style={cellHeader}>Scheduled for</th>
                <th style={{ ...cellHeader, textAlign: "right" }}>Actions</th>
              </tr>
            </thead>
            <tbody>
              {chats.map(c => (
                <tr key={c.id}>
                  <td style={cell}>
                    <div style={{ whiteSpace: "pre-wrap", maxWidth: 480 }}>{c.body.slice(0, 240)}{c.body.length > 240 ? "…" : ""}</div>
                  </td>
                  <td style={cell}>
                    <span style={{ fontFamily: "monospace", fontSize: 11, color: "var(--subtext)" }}>{c.channelId.slice(0, 8)}…</span>
                  </td>
                  <td style={cell}>{fmtIn(c.scheduledFor)}</td>
                  <td style={{ ...cell, textAlign: "right" }}>
                    <Btn variant="danger" disabled={busy && busy.kind === "chat" && busy.id === c.id}
                         onClick={() => setConfirming({
                           title: "Cancel scheduled message?",
                           message: "This message will not be posted.",
                           confirmLabel: "Cancel message",
                           danger: true,
                           action: () => { setConfirming(null); cancelChat(c.id); },
                         })}>
                      {busy && busy.kind === "chat" && busy.id === c.id ? <Spinner /> : "Cancel"}
                    </Btn>
                  </td>
                </tr>
              ))}
            </tbody>
          </table>
        </div>
      )}

      {confirming && (
        <ConfirmDialog open={!!confirming}
                       title={confirming.title}
                       message={confirming.message}
                       confirmLabel={confirming.confirmLabel}
                       danger={confirming.danger}
                       onConfirm={confirming.action}
                       onClose={() => setConfirming(null)} />
      )}
    </div>
  );
}

// =============================================================================
// AdminRequestsTab — triage queue for member-submitted topic requests.
//   Default view: open requests sorted newest-first.
//   Status flow: open → planned / covered / declined (terminal) / archived.
//   Editing the admin note is inline + auto-saves on blur.
// =============================================================================

function AdminRequestsTab() {
  const [data, setData] = useState(null);
  const [statusFilter, setStatusFilter] = useState("all"); // all / open / planned / covered / declined / archived
  const [busyId, setBusyId] = useState(null);
  const [confirming, setConfirming] = useState(null);

  const STATUS_META = {
    open:     { label: "Open",     bg: "rgba(56,114,224,0.14)",  fg: "#5b8eef" },
    planned:  { label: "Planned",  bg: "rgba(200,155,60,0.14)",  fg: "var(--primary)" },
    covered:  { label: "Covered",  bg: "rgba(34,197,94,0.14)",   fg: "#22c55e" },
    declined: { label: "Declined", bg: "rgba(239,68,68,0.14)",   fg: "#f87171" },
    archived: { label: "Archived", bg: "var(--surface2)",          fg: "var(--muted)" },
  };
  const STATUSES = ["open", "planned", "covered", "declined", "archived"];

  function load() {
    const q = statusFilter === "all" ? "" : `?status=${statusFilter}`;
    api("GET", `/api/admin/requests${q}`)
      .then(setData)
      .catch(e => { showToast(e.message, "error"); setData({ requests: [], openCount: 0 }); });
  }
  useEffect(() => { load(); }, [statusFilter]);

  async function setStatus(id, status) {
    setBusyId(id);
    try {
      await api("PATCH", `/api/admin/requests/${id}`, { status });
      load();
    } catch (e) { showToast(e.message, "error"); }
    finally { setBusyId(null); }
  }

  async function saveNote(id, adminNote) {
    try {
      await api("PATCH", `/api/admin/requests/${id}`, { adminNote });
      load();
    } catch (e) { showToast(e.message, "error"); }
  }

  async function hardDelete(id) {
    setBusyId(id);
    try {
      await api("DELETE", `/api/admin/requests/${id}`);
      showToast("Request deleted", "success");
      load();
    } catch (e) { showToast(e.message, "error"); }
    finally { setBusyId(null); }
  }

  if (!data) {
    return <div style={{ padding: 28 }}><div className="skeleton" style={{ height: 200 }} /></div>;
  }

  return (
    <div style={{ padding: "20px 28px" }}>
      <div style={{ display: "flex", justifyContent: "space-between", alignItems: "baseline", marginBottom: 16 }}>
        <div>
          <h2 className="serif" style={{ fontSize: 19, fontWeight: 700, marginBottom: 4 }}>
            Topic requests {data.openCount > 0 && (
              <span style={{ marginLeft: 8, fontSize: 12, color: "var(--primary)", fontWeight: 700 }}>
                · {data.openCount} open
              </span>
            )}
          </h2>
          <div style={{ fontSize: 12, color: "var(--muted)", lineHeight: 1.5 }}>
            Member-submitted suggestions. Update the status to keep them informed —
            members see "Open / Planned / Covered / Declined" on their own request page.
          </div>
        </div>
        <Btn variant="ghost" onClick={load}>Refresh</Btn>
      </div>

      <div style={{ display: "flex", gap: 6, marginBottom: 16, flexWrap: "wrap" }}>
        {[["all","All"], ["open","Open"], ["planned","Planned"], ["covered","Covered"], ["declined","Declined"], ["archived","Archived"]].map(([k, label]) => (
          <button key={k} onClick={() => setStatusFilter(k)} style={{
            padding: "5px 12px", borderRadius: 999, fontSize: 12, fontWeight: 600,
            background: statusFilter === k ? "var(--primary)" : "var(--surface2)",
            color: statusFilter === k ? "#fff" : "var(--subtext)",
            border: "1px solid " + (statusFilter === k ? "var(--primary)" : "var(--border)"),
            cursor: "pointer",
          }}>{label}</button>
        ))}
      </div>

      {data.requests.length === 0 ? (
        <div style={{ padding: 24, fontSize: 13, color: "var(--muted)",
                      border: "1px dashed var(--border)", borderRadius: 10 }}>
          No requests {statusFilter !== "all" ? `with status "${statusFilter}"` : "yet"}.
        </div>
      ) : (
        <div style={{ display: "flex", flexDirection: "column", gap: 12 }}>
          {data.requests.map(r => (
            <RequestRow key={r.id} request={r}
                        busy={busyId === r.id}
                        statusMeta={STATUS_META[r.status]}
                        statuses={STATUSES} statusMetaMap={STATUS_META}
                        onSetStatus={(s) => setStatus(r.id, s)}
                        onSaveNote={(note) => saveNote(r.id, note)}
                        onDelete={() => setConfirming({
                          title: "Delete request?",
                          message: "The request will be permanently removed. This cannot be undone — the member won't see anything in their request history. Prefer 'Archived' if you just want it out of your queue.",
                          confirmLabel: "Delete",
                          danger: true,
                          action: () => { setConfirming(null); hardDelete(r.id); },
                        })} />
          ))}
        </div>
      )}

      {confirming && (
        <ConfirmDialog open={!!confirming}
                       title={confirming.title}
                       message={confirming.message}
                       confirmLabel={confirming.confirmLabel}
                       danger={confirming.danger}
                       onConfirm={confirming.action}
                       onClose={() => setConfirming(null)} />
      )}
    </div>
  );
}

function RequestRow({ request, busy, statusMeta, statuses, statusMetaMap, onSetStatus, onSaveNote, onDelete }) {
  const r = request;
  const [note, setNote] = useState(r.adminNote || "");
  const [noteDirty, setNoteDirty] = useState(false);
  // Sync local state when the parent reloads.
  useEffect(() => { setNote(r.adminNote || ""); setNoteDirty(false); }, [r.adminNote]);

  return (
    <div style={{
      background: "var(--surface)", border: "1px solid var(--border)",
      borderRadius: 10, padding: 16,
    }}>
      <div style={{ display: "flex", gap: 10, alignItems: "center", marginBottom: 10, flexWrap: "wrap" }}>
        <span style={{
          padding: "2px 10px", borderRadius: 999, fontSize: 11, fontWeight: 700,
          background: statusMeta.bg, color: statusMeta.fg,
        }}>{statusMeta.label}</span>
        <span style={{ fontSize: 12, fontWeight: 600, color: "var(--text)" }}>
          {r.userName || "(deleted member)"}
        </span>
        {r.userEmail && (
          <a href={`mailto:${r.userEmail}`} style={{ fontSize: 12, color: "var(--primary)", textDecoration: "none" }}>
            {r.userEmail}
          </a>
        )}
        <span style={{ fontSize: 11, color: "var(--muted)" }}>
          · {fmtDateTime(r.createdAt)}
        </span>
      </div>

      <div style={{ fontSize: 14, color: "var(--text)", whiteSpace: "pre-wrap", lineHeight: 1.55, marginBottom: 14 }}>
        {r.body}
      </div>

      <div style={{ marginBottom: 12 }}>
        <label style={{
          display: "block", fontSize: 11, fontWeight: 700,
          color: "var(--subtext)", textTransform: "uppercase",
          letterSpacing: "0.5px", marginBottom: 6,
        }}>Note (visible to the member)</label>
        <textarea value={note}
                  onChange={e => { setNote(e.target.value); setNoteDirty(true); }}
                  onBlur={() => { if (noteDirty) { onSaveNote(note); setNoteDirty(false); } }}
                  placeholder="Optional reply — e.g. 'Scheduled for next week's brief' or 'We've covered this in Day 12 of the Hormuz series.'"
                  rows={2}
                  maxLength={2000}
                  style={{ width: "100%", resize: "vertical", minHeight: 50, fontSize: 13 }} />
        {noteDirty && <div style={{ fontSize: 11, color: "var(--muted)", marginTop: 4 }}>Click outside to save.</div>}
      </div>

      <div style={{ display: "flex", gap: 6, flexWrap: "wrap", alignItems: "center" }}>
        <span style={{ fontSize: 11, fontWeight: 700, color: "var(--muted)",
                       textTransform: "uppercase", letterSpacing: "0.5px", marginRight: 4 }}>
          Set status:
        </span>
        {statuses.map(s => {
          const meta = statusMetaMap[s];
          const active = s === r.status;
          return (
            <button key={s} onClick={() => !active && !busy && onSetStatus(s)}
                    disabled={active || busy}
                    style={{
                      padding: "4px 10px", borderRadius: 999, fontSize: 11, fontWeight: 600,
                      background: active ? meta.bg : "transparent",
                      color: active ? meta.fg : "var(--subtext)",
                      border: "1px solid " + (active ? "transparent" : "var(--border)"),
                      cursor: active || busy ? "default" : "pointer",
                      opacity: busy && !active ? 0.5 : 1,
                    }}>
              {meta.label}
            </button>
          );
        })}
        <div style={{ flex: 1 }} />
        <Btn variant="danger" size="sm" onClick={onDelete} disabled={busy}>Delete</Btn>
      </div>
    </div>
  );
}

function AdminCrisisTab() {
  const [pages, setPages] = useState(null);
  const [editingId, setEditingId] = useState(null); // 'new' | id | null
  const [refreshKey, setRefreshKey] = useState(0);
  const [confirming, setConfirming] = useState(null);

  useEffect(() => {
    api("GET", "/api/admin/crisis")
      .then(d => setPages(d.pages || []))
      .catch(e => { showToast(e.message, "error"); setPages([]); });
  }, [refreshKey]);

  function refresh() { setRefreshKey(k => k + 1); }

  async function toggleActive(p) {
    try {
      await api("PATCH", `/api/admin/crisis/${p.id}`, { active: !p.active });
      showToast(p.active ? "Page hidden" : "Page published", "success");
      refresh();
    } catch (e) { showToast(e.message, "error"); }
  }

  function deletePage(p) {
    setConfirming({
      title: "Delete crisis page?",
      message: `"${p.title}" will be permanently deleted. The public URL /crisis/${p.slug} will return 404. This cannot be undone.`,
      confirmLabel: "Delete page",
      danger: true,
      action: async () => {
        await api("DELETE", `/api/admin/crisis/${p.id}`);
        showToast("Deleted", "success");
        refresh();
      },
    });
  }

  return (
    <div style={{ padding: "20px 28px" }}>
      <div style={{ display: "flex", justifyContent: "space-between", alignItems: "center", marginBottom: 18 }}>
        <div style={{ fontSize: 13, color: "var(--subtext)", maxWidth: 600 }}>
          Public situation briefs at <code>/crisis/&lt;slug&gt;</code>. Toggle "Live" to publish or hide.
          Active pages are SEO-friendly (server-rendered HTML) and accessible without login.
        </div>
        <Btn onClick={() => setEditingId("new")}>+ New crisis page</Btn>
      </div>

      {pages === null ? (
        <div className="skeleton" style={{ height: 220 }} />
      ) : pages.length === 0 ? (
        <div className="empty-state">
          <h3>No crisis pages yet</h3>
          <p>Create one to publish a public situation brief that anyone can view without logging in.</p>
        </div>
      ) : (
        <div style={{ background: "var(--surface)", border: "1px solid var(--border)", borderRadius: 10, overflow: "hidden" }}>
          <table>
            <thead>
              <tr>
                <th>Title</th>
                <th>URL</th>
                <th>Variables</th>
                <th>Status</th>
                <th>Updated</th>
                <th style={{ textAlign: "right" }}>Actions</th>
              </tr>
            </thead>
            <tbody>
              {pages.map(p => (
                <tr key={p.id}>
                  <td style={{ fontWeight: 600 }}>{p.title}</td>
                  <td>
                    {p.active ? (
                      <a href={`/crisis/${p.slug}`} target="_blank" rel="noreferrer"
                         style={{ color: "var(--primary)", fontSize: 13 }}>
                        /crisis/{p.slug} ↗
                      </a>
                    ) : (
                      <span style={{ color: "var(--muted)", fontSize: 13 }}>/crisis/{p.slug}</span>
                    )}
                  </td>
                  <td style={{ color: "var(--subtext)" }}>{p.keyVariables.length}</td>
                  <td>
                    <span className="tag" style={{
                      background: p.active ? "rgba(22,163,74,0.18)" : "var(--surface3)",
                      color: p.active ? "#16a34a" : "var(--subtext)",
                    }}>
                      {p.active ? "LIVE" : "Hidden"}
                    </span>
                  </td>
                  <td style={{ color: "var(--muted)", fontSize: 12 }}>{fmtDateTime(p.updatedAt)}</td>
                  <td style={{ textAlign: "right" }}>
                    <Btn variant="ghost" size="sm" onClick={() => toggleActive(p)}>
                      {p.active ? "Hide" : "Publish"}
                    </Btn>
                    <Btn variant="ghost" size="sm" onClick={() => setEditingId(p.id)}>Edit</Btn>
                    <Btn variant="danger" size="sm" onClick={() => deletePage(p)}>Delete</Btn>
                  </td>
                </tr>
              ))}
            </tbody>
          </table>
        </div>
      )}

      {editingId !== null && (
        <CrisisEditModal
          mode={editingId === "new" ? "create" : "edit"}
          pageId={editingId === "new" ? null : editingId}
          onClose={() => setEditingId(null)}
          onSaved={() => { setEditingId(null); refresh(); }}
        />
      )}

      {confirming && (
        <ConfirmDialog open={!!confirming}
                       title={confirming.title}
                       message={confirming.message}
                       confirmLabel={confirming.confirmLabel}
                       danger={confirming.danger}
                       onConfirm={confirming.action}
                       onClose={() => setConfirming(null)} />
      )}
    </div>
  );
}

function CrisisEditModal({ mode, pageId, onClose, onSaved }) {
  const [loading, setLoading] = useState(mode === "edit");
  const [busy, setBusy] = useState(false);
  const [title, setTitle] = useState("");
  const [subtitle, setSubtitle] = useState("");
  const [summary, setSummary] = useState("");
  const [slug, setSlug] = useState("");
  const [active, setActive] = useState(false);
  const [isTeaser, setIsTeaser] = useState(false);
  const [externalUrl, setExternalUrl] = useState("");
  const [metaDescription, setMetaDescription] = useState("");
  const [keyVariables, setKeyVariables] = useState([]);

  useEffect(() => {
    if (mode === "edit" && pageId) {
      api("GET", `/api/admin/crisis/${pageId}`).then(d => {
        const p = d.page;
        setTitle(p.title || "");
        setSubtitle(p.subtitle || "");
        setSummary(p.summary || "");
        setSlug(p.slug || "");
        setActive(!!p.active);
        setIsTeaser(!!p.isTeaser);
        setExternalUrl(p.externalUrl || "");
        setMetaDescription(p.metaDescription || "");
        setKeyVariables(p.keyVariables || []);
        setLoading(false);
      }).catch(e => { showToast(e.message, "error"); onClose(); });
    }
  }, [mode, pageId]);

  function addVariable() {
    setKeyVariables(prev => [...prev, { label: "", value: "", unit: "", trend: null, note: "", updatedAt: new Date().toISOString() }]);
  }
  function updateVariable(i, patch) {
    setKeyVariables(prev => prev.map((v, idx) => idx === i ? { ...v, ...patch, updatedAt: new Date().toISOString() } : v));
  }
  function removeVariable(i) {
    setKeyVariables(prev => prev.filter((_, idx) => idx !== i));
  }
  function moveVariable(i, delta) {
    setKeyVariables(prev => {
      const next = [...prev];
      const j = i + delta;
      if (j < 0 || j >= next.length) return next;
      [next[i], next[j]] = [next[j], next[i]];
      return next;
    });
  }

  async function handleSave(e) {
    if (e) e.preventDefault();
    if (!title.trim()) { showToast("Title is required", "error"); return; }
    setBusy(true);
    try {
      const body = {
        title: title.trim(),
        subtitle: subtitle.trim() || null,
        summary: summary.trim() || null,
        metaDescription: metaDescription.trim() || null,
        keyVariables: keyVariables.filter(v => v.label && v.label.trim()),
        active,
        isTeaser,
        externalUrl: externalUrl.trim() || null,
      };
      if (mode === "create") {
        if (slug.trim()) body.slug = slug.trim();
        await api("POST", "/api/admin/crisis", body);
        showToast("Crisis page created", "success");
      } else {
        if (slug.trim()) body.slug = slug.trim();
        await api("PATCH", `/api/admin/crisis/${pageId}`, body);
        showToast("Saved", "success");
      }
      onSaved();
    } catch (e) {
      showToast(e.message, "error");
    } finally { setBusy(false); }
  }

  if (loading) {
    return (
      <Modal open onClose={onClose} title="Loading…" size="xl">
        <div className="skeleton" style={{ height: 400 }} />
      </Modal>
    );
  }

  return (
    <Modal open onClose={busy ? () => {} : onClose}
           title={mode === "create" ? "New crisis page" : `Edit: ${title || "(untitled)"}`}
           size="xl"
           footer={
      <>
        <Btn variant="ghost" onClick={onClose} disabled={busy}>Cancel</Btn>
        <Btn onClick={handleSave} disabled={busy}>{busy ? <Spinner /> : (mode === "create" ? "Create" : "Save")}</Btn>
      </>
    }>
      <form onSubmit={handleSave}>
        <Field label="Title" htmlFor="cp-title" hint="Big headline at the top of the page.">
          <input id="cp-title" type="text" required value={title}
                 onChange={e => setTitle(e.target.value)} />
        </Field>
        <Field label="Subtitle (optional)" htmlFor="cp-sub" hint="Smaller line below the headline (italic).">
          <input id="cp-sub" type="text" value={subtitle}
                 onChange={e => setSubtitle(e.target.value)} />
        </Field>
        <Field label="URL slug" htmlFor="cp-slug" hint="Public URL: /crisis/<slug>. Lowercase letters, numbers, hyphens only.">
          <input id="cp-slug" type="text" value={slug}
                 placeholder={mode === "create" ? "auto-generated from title" : ""}
                 onChange={e => setSlug(e.target.value.toLowerCase().replace(/[^a-z0-9-]/g, "-"))} />
        </Field>
        <Field label="External URL (optional)" htmlFor="cp-ext"
               hint="If set, /crisis/<slug> redirects here (e.g. a dashboard you built in Claude and host on GitHub Pages). Leave blank to use the built-in template below.">
          <input id="cp-ext" type="url" value={externalUrl}
                 placeholder="https://username.github.io/hormuz-dashboard/"
                 onChange={e => setExternalUrl(e.target.value)} />
        </Field>
        <Field label="Summary" htmlFor="cp-summary"
               hint="Plain text. Blank lines start new paragraphs. Shows above the key variables grid (only if no external URL is set).">
          <textarea id="cp-summary" rows={5} value={summary}
                    onChange={e => setSummary(e.target.value)} />
        </Field>
        <Field label="Meta description (SEO)" htmlFor="cp-meta"
               hint="Shown in search results / link previews. Aim for 130-160 chars.">
          <input id="cp-meta" type="text" maxLength={300} value={metaDescription}
                 onChange={e => setMetaDescription(e.target.value)} />
        </Field>

        <div style={{ marginTop: 22, marginBottom: 10, display: "flex", justifyContent: "space-between", alignItems: "center" }}>
          <h3 style={{ fontSize: 14, fontWeight: 700 }}>Key variables ({keyVariables.length})</h3>
          <Btn type="button" variant="ghost" size="sm" onClick={addVariable}>+ Add variable</Btn>
        </div>
        <div style={{ fontSize: 12, color: "var(--muted)", marginBottom: 12 }}>
          Each variable shows as a card on the page. Update the value here whenever the situation changes — visitors see the latest at <code>/crisis/&lt;slug&gt;</code>.
        </div>

        {keyVariables.length === 0 && (
          <div style={{ padding: "20px 16px", border: "1px dashed var(--border)", borderRadius: 8, textAlign: "center", color: "var(--muted)", fontSize: 13 }}>
            No variables yet. Add one to start tracking a metric.
          </div>
        )}

        {keyVariables.map((v, i) => (
          <div key={i} style={{
            border: "1px solid var(--border)", borderRadius: 10, padding: 12,
            marginBottom: 10, background: "var(--surface2)",
          }}>
            <div style={{ display: "flex", gap: 8, marginBottom: 8 }}>
              <input placeholder="Label (e.g. Hormuz daily transit)" value={v.label}
                     onChange={e => updateVariable(i, { label: e.target.value })}
                     style={{ flex: 2 }} />
              <input placeholder="Value (e.g. 17.4)" value={v.value}
                     onChange={e => updateVariable(i, { value: e.target.value })}
                     style={{ flex: 1 }} />
              <input placeholder="Unit (e.g. M bbl/day)" value={v.unit || ""}
                     onChange={e => updateVariable(i, { unit: e.target.value })}
                     style={{ flex: 1 }} />
            </div>
            <div style={{ display: "flex", gap: 8, alignItems: "center" }}>
              <select value={v.trend || ""}
                      onChange={e => updateVariable(i, { trend: e.target.value || null })}
                      style={{ width: 140 }}>
                <option value="">No trend</option>
                <option value="up">▲ Up</option>
                <option value="down">▼ Down</option>
                <option value="flat">→ Flat</option>
              </select>
              <input placeholder="Note (e.g. vs 21.3M baseline)" value={v.note || ""}
                     onChange={e => updateVariable(i, { note: e.target.value })}
                     style={{ flex: 1 }} />
              <Btn type="button" variant="ghost" size="sm" onClick={() => moveVariable(i, -1)}
                   disabled={i === 0} title="Move up">↑</Btn>
              <Btn type="button" variant="ghost" size="sm" onClick={() => moveVariable(i, 1)}
                   disabled={i === keyVariables.length - 1} title="Move down">↓</Btn>
              <Btn type="button" variant="danger" size="sm" onClick={() => removeVariable(i)}>×</Btn>
            </div>
          </div>
        ))}

        <div style={{ marginTop: 18, padding: "12px 14px", background: "var(--surface2)", border: "1px solid var(--border)", borderRadius: 10 }}>
          <label style={{ display: "flex", alignItems: "center", gap: 10, cursor: "pointer", textTransform: "none", letterSpacing: 0, fontSize: 14, color: "var(--text)" }}>
            <input type="checkbox" checked={active}
                   onChange={e => setActive(e.target.checked)}
                   style={{ width: "auto" }} />
            <strong>Live</strong> · publish at <code>/crisis/{slug || "<slug>"}</code>
          </label>
          <div style={{ marginTop: 6, fontSize: 12, color: "var(--muted)" }}>
            When unchecked, the page returns 404. Toggle on once you're ready for visitors.
          </div>
          <label style={{ display: "flex", alignItems: "center", gap: 10, cursor: "pointer", textTransform: "none", letterSpacing: 0, fontSize: 14, color: "var(--text)", marginTop: 14 }}>
            <input type="checkbox" checked={isTeaser}
                   onChange={e => setIsTeaser(e.target.checked)}
                   style={{ width: "auto" }} />
            Free-tier teaser · visible to free-tier members in the hub nav
          </label>
        </div>
      </form>
    </Modal>
  );
}

function AdminSettingsTab({ onChange }) {
  const [settings, setSettings] = useState(null);
  const [busy, setBusy] = useState(false);

  useEffect(() => {
    api("GET", "/api/admin/settings")
      .then(d => setSettings(d.settings))
      .catch(e => showToast(e.message, "error"));
  }, []);

  async function save(e) {
    e.preventDefault();
    setBusy(true);
    try {
      await api("PATCH", "/api/admin/settings", {
        hubName: settings.hubName,
        hubShortName: settings.hubShortName,
        defaultDownloadable: !!settings.defaultDownloadable,
        showCrisisNav: settings.showCrisisNav !== false,
        notifyEmail: settings.notifyEmail || "",
        welcomeSubject: settings.welcomeSubject || "",
        welcomeBody: settings.welcomeBody || "",
      });
      showToast("Settings saved", "success");
      onChange();
    } catch (e) { showToast(e.message, "error"); }
    finally { setBusy(false); }
  }

  if (!settings) return <div style={{ padding: 28 }}><div className="skeleton" style={{ height: 220 }} /></div>;

  return (
    <div style={{ padding: "20px 28px", maxWidth: 720 }}>
      <h2 style={{ fontSize: 16, fontWeight: 700, marginBottom: 14 }}>Hub settings</h2>
      <form onSubmit={save}>
        <Field label="Hub name (login screen + emails)" htmlFor="s-name">
          <input id="s-name" type="text" required
                 value={settings.hubName || ""}
                 onChange={e => setSettings({ ...settings, hubName: e.target.value })} />
        </Field>
        <Field label="Short name (sidebar + browser tab)" htmlFor="s-sname">
          <input id="s-sname" type="text" required maxLength={60}
                 value={settings.hubShortName || ""}
                 onChange={e => setSettings({ ...settings, hubShortName: e.target.value })} />
        </Field>
        <Field label="Notification email (sender domain for Resend)" htmlFor="s-email">
          <input id="s-email" type="email"
                 value={settings.notifyEmail || ""}
                 onChange={e => setSettings({ ...settings, notifyEmail: e.target.value })} />
        </Field>
        <Field>
          <label style={{ display: "flex", alignItems: "center", gap: 10, cursor: "pointer", textTransform: "none", letterSpacing: 0, fontSize: 14, color: "var(--text)" }}>
            <input type="checkbox"
                   checked={!!settings.defaultDownloadable}
                   onChange={e => setSettings({ ...settings, defaultDownloadable: e.target.checked })}
                   style={{ width: "auto" }} />
            New uploads default to downloadable (members can save the file)
          </label>
        </Field>
        <Field>
          <label style={{ display: "flex", alignItems: "center", gap: 10, cursor: "pointer", textTransform: "none", letterSpacing: 0, fontSize: 14, color: "var(--text)" }}>
            <input type="checkbox"
                   checked={settings.showCrisisNav !== false}
                   onChange={e => setSettings({ ...settings, showCrisisNav: e.target.checked })}
                   style={{ width: "auto" }} />
            Show crisis nav bar above Reports (toggle off when no crisis is live)
          </label>
        </Field>

        <div style={{ marginTop: 28, paddingTop: 20, borderTop: "1px solid var(--border)" }}>
          <h3 style={{ fontSize: 14, fontWeight: 700, marginBottom: 6 }}>Welcome email</h3>
          <div style={{ fontSize: 12, color: "var(--muted)", marginBottom: 14, lineHeight: 1.6 }}>
            Sent automatically when a member's Stripe subscription becomes active for the first time.
            Available variables: <code>{"{{name}}"}</code>, <code>{"{{hub_name}}"}</code>, <code>{"{{login_url}}"}</code>, <code>{"{{hub_short_name}}"}</code>.
            Plain text — blank lines start new paragraphs. Requires Resend to be configured.
          </div>
          <Field label="Subject" htmlFor="s-wsub">
            <input id="s-wsub" type="text" maxLength={200}
                   value={settings.welcomeSubject || ""}
                   onChange={e => setSettings({ ...settings, welcomeSubject: e.target.value })} />
          </Field>
          <Field label="Body" htmlFor="s-wbody">
            <textarea id="s-wbody" rows={10} maxLength={8000}
                      value={settings.welcomeBody || ""}
                      onChange={e => setSettings({ ...settings, welcomeBody: e.target.value })}
                      style={{ fontFamily: "ui-monospace, monospace", fontSize: 13 }} />
          </Field>
        </div>

        <Btn type="submit" disabled={busy}>{busy ? <Spinner /> : "Save"}</Btn>
      </form>
    </div>
  );
}

// =============================================================================
// App root
// =============================================================================

function App() {
  // Branding pulled live from /api/settings (so it's editable without redeploy).
  const [hubName, setHubName] = useState(APP_NAME_FALLBACK);
  const [hubShortName, setHubShortName] = useState(SHORT_NAME_FALLBACK);
  const [defaultDownloadable, setDefaultDownloadable] = useState(true);

  const [phase, setPhase] = useState("loading"); // loading | login | signup | forgot | reset | gated | app
  const [user, setUser] = useState(null);
  const [view, setView] = useState("reports");
  const [theme, setTheme] = useState("dark");
  const [refreshKey, setRefreshKey] = useState(0);
  const refresh = () => setRefreshKey(k => k + 1);

  const [resetToken, setResetToken] = useState(null);
  const [uploadOpen, setUploadOpen] = useState(false);
  const [upgradeOpen, setUpgradeOpen] = useState(false);
  const [jumpToChannelId, setJumpToChannelId] = useState(null);
  const [jumpToThreadId, setJumpToThreadId] = useState(null);

  function jumpFromNotification(target) {
    if (target.view) setView(target.view);
    if (target.channelId) setJumpToChannelId(target.channelId);
    if (target.threadId) setJumpToThreadId(target.threadId);
  }

  // Used by chat report-announce messages — switches to Reports view and
  // opens the preview modal for that report. The hub-level state holds
  // a "report to auto-preview" flag that ReportsView watches on entry.
  const [autoPreviewReportId, setAutoPreviewReportId] = useState(null);
  function jumpFromChatToReport(reportId) {
    setAutoPreviewReportId(reportId);
    setView("reports");
  }

  // Apply theme
  useEffect(() => {
    document.documentElement.dataset.theme = theme;
  }, [theme]);

  // Update tab title once we have the short name
  useEffect(() => {
    document.title = hubShortName || "Hub";
  }, [hubShortName]);

  // Pull branding (works whether logged in or not — falls back gracefully)
  const loadBranding = useCallback(async () => {
    try {
      const r = await api("GET", "/api/settings");
      if (r.settings.hubName) setHubName(r.settings.hubName);
      if (r.settings.hubShortName) setHubShortName(r.settings.hubShortName);
      if (typeof r.settings.defaultDownloadable === "boolean") setDefaultDownloadable(r.settings.defaultDownloadable);
    } catch {
      // Not logged in — leave fallbacks. /api/settings requires auth.
    }
  }, []);

  // Auth bootstrap
  const checkAuth = useCallback(async () => {
    try {
      const r = await api("GET", "/api/auth/me");
      setUser(r.user);
      if (r.user.theme_preference) setTheme(r.user.theme_preference);

      // Subscription gate: active, comp and free accounts can enter the app.
      // Pending / past_due / canceled bounce to the payment-required screen.
      const allowed = ["active", "comp", "free"];
      if (!allowed.includes(r.user.stripe_status)) {
        setPhase("gated");
      } else {
        setPhase("app");
      }
      await loadBranding();
    } catch {
      setUser(null);
      setPhase("login");
    }
  }, [loadBranding]);

  useEffect(() => {
    const url = new URL(window.location.href);

    // Handle ?reset=TOKEN for password reset flow
    const tok = url.searchParams.get("reset");
    if (tok) { setResetToken(tok); setPhase("reset"); return; }

    // Handle ?paid=1 / ?paid=0 returning from Stripe checkout
    const paid = url.searchParams.get("paid");
    if (paid === "1") {
      // Webhook activates the account asynchronously. Show a friendly status
      // while we poll /api/auth/me until stripe_status flips.
      window.history.replaceState({}, "", "/");
      showToast("Payment successful — activating your account…", "success");
    } else if (paid === "0") {
      window.history.replaceState({}, "", "/");
      showToast("Checkout canceled. You can resume any time.", "info");
    }

    checkAuth();
  }, [checkAuth]);

  async function handleLogout() {
    try { await api("POST", "/api/auth/logout"); } catch {}
    setUser(null);
    setPhase("login");
  }

  if (phase === "loading") {
    return (
      <div style={{ minHeight: "100vh", display: "flex", alignItems: "center", justifyContent: "center" }}>
        <Spinner size={28} />
      </div>
    );
  }

  if (phase === "login") {
    return <LoginScreen
      onAuth={checkAuth}
      onForgot={() => setPhase("forgot")}
      onSignup={() => setPhase("signup")}
      hubName={hubName}
      tagline={TAGLINE}
    />;
  }
  if (phase === "signup") {
    return <SignupScreen onBack={() => setPhase("login")}
                         onSignedUp={checkAuth}
                         hubName={hubName} tagline={TAGLINE} />;
  }
  if (phase === "forgot") {
    return <ForgotScreen onBack={() => setPhase("login")} hubName={hubName} tagline={TAGLINE} />;
  }
  if (phase === "reset") {
    return <ResetScreen token={resetToken} hubName={hubName} tagline={TAGLINE}
                        onDone={() => {
                          window.history.replaceState({}, "", "/");
                          setPhase("login");
                        }} />;
  }
  if (phase === "gated") {
    return <PaymentRequiredScreen user={user}
                                  onLogout={handleLogout}
                                  onContinue={checkAuth}
                                  hubName={hubName} tagline={TAGLINE} />;
  }

  // ---- Authenticated app ----
  const isAdmin = user.role === "admin";
  const titleByView = {
    reports: { title: "Reports", subtitle: "Latest situation briefs and deep dives" },
    archive: { title: "Archive", subtitle: "Older reports, kept for reference" },
    chat: { title: "Chat", subtitle: "Discuss with the hub members" },
    inbox: { title: "Inbox", subtitle: "Direct messages between you and other members" },
    settings: { title: "Settings", subtitle: "Your profile and password" },
    admin: { title: "Admin panel", subtitle: "Members, reports, and hub settings" },
  };
  const t = titleByView[view] || { title: "" };

  return (
    <div style={{ display: "flex", height: "100vh", background: "var(--bg)" }}>
      <Sidebar user={user} view={view} setView={setView}
               hubShortName={hubShortName} onLogout={handleLogout}
               onUpgrade={() => setUpgradeOpen(true)} />
      <main style={{ flex: 1, display: "flex", flexDirection: "column", overflow: "hidden" }}>
        <CrisisNav user={user} setView={setView} />
        <TopBar
          title={t.title}
          subtitle={t.subtitle}
          actions={
            <>
              {(view === "reports" || view === "archive") && isAdmin && (
                <Btn onClick={() => setUploadOpen(true)}>+ Upload report</Btn>
              )}
              <NotificationBell onJump={jumpFromNotification} />
              <Btn variant="ghost" size="sm"
                   onClick={() => {
                     const next = theme === "dark" ? "light" : "dark";
                     setTheme(next);
                     api("PATCH", "/api/auth/me", { themePreference: next }).catch(() => {});
                   }}
                   title="Toggle theme">
                {theme === "dark" ? "☀️" : "🌙"}
              </Btn>
            </>
          }
        />
        {view === "reports" && <ReportsView user={user} archived={false}
                                            refreshKey={refreshKey} onChange={refresh}
                                            autoPreviewReportId={autoPreviewReportId}
                                            onAutoPreviewHandled={() => setAutoPreviewReportId(null)} />}
        {view === "archive" && <ReportsView user={user} archived={true}
                                            refreshKey={refreshKey} onChange={refresh} />}
        {view === "chat" && <ChatView user={user}
                                      jumpToChannelId={jumpToChannelId}
                                      onJumpHandled={() => setJumpToChannelId(null)}
                                      onJumpToReport={jumpFromChatToReport} />}
        {view === "inbox" && <InboxView user={user}
                                       jumpToThreadId={jumpToThreadId}
                                       onJumpHandled={() => setJumpToThreadId(null)} />}
        {view === "request" && <RequestTopicView user={user} />}
        {view === "settings" && <SettingsPage user={user} onUserUpdated={checkAuth} />}
        {view === "admin" && isAdmin && <AdminPanel user={user}
                                                    onUserUpdated={checkAuth}
                                                    refreshKey={refreshKey}
                                                    onChange={() => { refresh(); loadBranding(); }} />}

        <ReportUploadModal open={uploadOpen}
                           defaultDownloadable={defaultDownloadable}
                           onClose={() => setUploadOpen(false)}
                           onUploaded={() => { setUploadOpen(false); refresh(); }} />
        {upgradeOpen && (
          <UpgradePlanModal currentPlan={user.plan}
                            onClose={() => setUpgradeOpen(false)} />
        )}
      </main>
    </div>
  );
}

// =============================================================================
// CrisisNav — top nav bar listing active crisis pages (toggleable in admin)
// =============================================================================

function CrisisNav({ user }) {
  const [pages, setPages] = useState(null);
  const [showNav, setShowNav] = useState(true);

  useEffect(() => {
    let cancelled = false;
    Promise.all([
      api("GET", "/api/crisis").catch(() => ({ pages: [] })),
      api("GET", "/api/settings").catch(() => ({ settings: { showCrisisNav: true } })),
    ]).then(([cr, st]) => {
      if (cancelled) return;
      setPages(cr.pages || []);
      setShowNav(st.settings?.showCrisisNav !== false);
    });
    return () => { cancelled = true; };
  }, []);

  if (!showNav || !pages || pages.length === 0) return null;

  return (
    <div style={{
      display: "flex", alignItems: "center", gap: 4,
      padding: "8px 28px",
      background: "linear-gradient(90deg, rgba(200,155,60,0.06), transparent 60%)",
      borderBottom: "1px solid var(--border)",
      fontSize: 12, fontWeight: 600, letterSpacing: "0.3px", flexShrink: 0,
      overflowX: "auto", whiteSpace: "nowrap",
    }}>
      <span style={{ color: "var(--primary)", textTransform: "uppercase",
                     fontSize: 11, letterSpacing: "1px", marginRight: 12, flexShrink: 0 }}>
        ◆ Live crisis briefs:
      </span>
      {pages.map((p, i) => (
        <a key={p.id}
           href={p.externalUrl || `/crisis/${p.slug}`}
           target={p.externalUrl ? "_blank" : "_self"}
           rel={p.externalUrl ? "noreferrer" : undefined}
           style={{
             padding: "4px 10px", marginRight: 4,
             color: "var(--text)", textDecoration: "none",
             borderRadius: 4, fontSize: 12, fontWeight: 600,
             border: "1px solid transparent", transition: "border-color 0.15s, background 0.15s",
           }}
           onMouseEnter={(e) => {
             e.currentTarget.style.borderColor = "var(--primary)";
             e.currentTarget.style.background = "rgba(200,155,60,0.08)";
           }}
           onMouseLeave={(e) => {
             e.currentTarget.style.borderColor = "transparent";
             e.currentTarget.style.background = "transparent";
           }}>
          {p.title}
          {p.externalUrl && <span style={{ marginLeft: 4, fontSize: 10, color: "var(--muted)" }}>↗</span>}
          {p.isTeaser && <span style={{ marginLeft: 4, fontSize: 10, color: "var(--primary)" }}>preview</span>}
        </a>
      ))}
    </div>
  );
}

// =============================================================================
// UpgradePlanModal — for free users + canceled members to start a paid plan
// =============================================================================

function UpgradePlanModal({ currentPlan, onClose }) {
  const [plans, setPlans] = useState(null);
  const [busy, setBusy] = useState(null);

  useEffect(() => {
    api("GET", "/api/plans").then(d => setPlans((d.plans || []).filter(p => p.id !== "free")))
      .catch(e => showToast(e.message, "error"));
  }, []);

  async function pick(planId) {
    setBusy(planId);
    try {
      const r = await api("POST", "/api/stripe/checkout-session", { planId });
      window.location.href = r.url;
    } catch (e) {
      showToast(e.message, "error");
      setBusy(null);
    }
  }

  return (
    <Modal open onClose={busy ? () => {} : onClose} title="Upgrade plan" size="wide" footer={
      <Btn variant="ghost" onClick={onClose} disabled={!!busy}>Cancel</Btn>
    }>
      <div style={{ fontSize: 13, color: "var(--subtext)", marginBottom: 16, lineHeight: 1.6 }}>
        Pick a paid plan and you'll be redirected to Stripe to complete payment. Your access flips to full immediately after payment.
      </div>
      {plans === null ? (
        <div className="skeleton" style={{ height: 160 }} />
      ) : plans.length === 0 ? (
        <div className="empty-state"><p>No paid plans configured yet.</p></div>
      ) : (
        <div style={{ display: "flex", flexDirection: "column", gap: 10 }}>
          {plans.map(p => {
            const isCurrent = currentPlan === p.id;
            return (
              <button key={p.id} type="button"
                      onClick={() => !isCurrent && pick(p.id)}
                      disabled={isCurrent || !!busy}
                      style={{
                        textAlign: "left",
                        padding: "16px 18px",
                        background: "var(--surface2)",
                        border: "1px solid var(--border)",
                        borderRadius: 10,
                        cursor: (isCurrent || busy) ? "default" : "pointer",
                        opacity: isCurrent ? 0.5 : 1,
                        position: "relative",
                      }}>
                <div style={{ display: "flex", justifyContent: "space-between", alignItems: "baseline", marginBottom: 8 }}>
                  <div style={{ fontSize: 16, fontWeight: 700, color: "var(--text)" }}>{p.name}</div>
                  <div style={{ fontSize: 14, fontWeight: 700, color: "var(--primary)" }}>
                    {p.currency === "EUR" ? "€" : ""}{p.price}/{p.period}
                  </div>
                </div>
                <ul style={{ listStyle: "none", padding: 0, margin: 0, fontSize: 12, color: "var(--subtext)" }}>
                  {(p.features || []).map(f => <li key={f} style={{ padding: "2px 0" }}>· {f}</li>)}
                </ul>
                {isCurrent && (
                  <div style={{ marginTop: 8, fontSize: 11, color: "var(--success)", fontWeight: 700 }}>Your current plan</div>
                )}
                {busy === p.id && (
                  <div style={{ position: "absolute", inset: 0, display: "flex", alignItems: "center", justifyContent: "center", background: "rgba(11,29,51,0.6)", borderRadius: 10 }}>
                    <Spinner size={28} />
                  </div>
                )}
              </button>
            );
          })}
        </div>
      )}
    </Modal>
  );
}

ReactDOM.createRoot(document.getElementById("app")).render(<App />);
