// admin/app.jsx — Phase 2 of the Lulius content admin.
//
// Phase 1 surfaces (still here):
//   - Sign in with Google → JWT cookie
//   - Dispatch list view (read-only browse)
//
// Phase 2 adds:
//   - Click an entry in the list → edit form for top-level fields
//   - Save → PUT /api/admin/entries/:slug → atomic write to the volume
//   - "Back to list" navigation
//
// Body blocks (paragraphs/headings/pullquotes/lists) remain read-only here;
// the rich block editor + image upload land in Phase 3.

const { useState, useEffect } = React;

function api(path, opts = {}) {
  return fetch(`/api/admin${path}`, {
    credentials: "include",
    headers: { "Content-Type": "application/json" },
    ...opts,
  });
}

// ───────────────────────── shell + auth ─────────────────────────────────────

function App() {
  const [me, setMe] = useState(null);
  const [authChecked, setAuthChecked] = useState(false);
  // view.name: "list" | "edit" | "new" (dispatch) | "careers" | "careers-edit"
  //            | "careers-new" | "team" | "team-edit" | "team-new"
  const [view, setView] = useState({ name: "list" });
  const [error, setError] = useState("");
  const [flash, setFlash] = useState("");

  useEffect(() => {
    api("/me")
      .then((r) => r.ok ? r.json() : null)
      .then((data) => { if (data?.ok) setMe({ email: data.email, name: data.name }); })
      .catch(() => {})
      .finally(() => setAuthChecked(true));
  }, []);

  if (!authChecked) return <div className="admin-wrap"><LoadingShell /></div>;
  if (!me) return <div className="admin-wrap"><SignIn error={error} /></div>;

  const nav = (v) => { setView(v); setError(""); setFlash(""); };
  const goList = () => nav({ name: "list" });
  const goEdit = (slug) => nav({ name: "edit", slug });
  const goNew = () => nav({ name: "new" });
  const goCareers = () => nav({ name: "careers" });
  const goCareerEdit = (slug) => nav({ name: "careers-edit", slug });
  const goCareerNew = () => nav({ name: "careers-new" });
  const goTeam = () => nav({ name: "team" });
  const goTeamEdit = (id) => nav({ name: "team-edit", id });
  const goTeamNew = () => nav({ name: "team-new" });
  const goCorr = () => nav({ name: "corr" });

  return (
    <div className="admin-wrap">
      <Header me={me} view={view} onDispatch={goList} onCareers={goCareers} onTeam={goTeam} onCorr={goCorr} onSignOut={async () => {
        await api("/logout", { method: "POST" });
        setMe(null);
      }} />
      {flash && <FlashBanner text={flash} />}
      {error && <ErrorBanner text={error} />}

      {view.name === "list" && <DispatchList onError={setError} onEdit={goEdit} onNew={goNew} onFlash={setFlash} />}
      {view.name === "edit" && (
        <EntryEdit slug={view.slug} onError={setError} onSaved={setFlash} onBack={goList} />
      )}
      {view.name === "new" && (
        <EntryEdit isNew onError={setError} onSaved={(msg) => { setFlash(msg); goList(); }} onBack={goList} />
      )}

      {view.name === "careers" && (
        <DispatchList kind="career" noun="position" plural="positions" onError={setError} onEdit={goCareerEdit} onNew={goCareerNew} onFlash={setFlash} />
      )}
      {view.name === "careers-edit" && (
        <EntryEdit slug={view.slug} kind="career" onError={setError} onSaved={setFlash} onBack={goCareers} />
      )}
      {view.name === "careers-new" && (
        <EntryEdit isNew kind="career" onError={setError} onSaved={(msg) => { setFlash(msg); goCareers(); }} onBack={goCareers} />
      )}

      {view.name === "team" && (
        <PersonnelList onError={setError} onEdit={goTeamEdit} onNew={goTeamNew} onFlash={setFlash} />
      )}
      {(view.name === "team-edit" || view.name === "team-new") && (
        <PersonEdit
          id={view.name === "team-edit" ? view.id : null}
          onError={setError}
          onSaved={(msg) => { setFlash(msg); goTeam(); }}
          onBack={goTeam}
        />
      )}

      {view.name === "corr" && (
        <CorrespondenceList onError={setError} onFlash={setFlash} />
      )}
    </div>
  );
}

function LoadingShell({ label = "LOADING" }) {
  return (
    <div style={{ padding: "120px 0", textAlign: "center" }}>
      <div className="eyebrow mute"><span className="dot"></span>{label}</div>
    </div>
  );
}

function SignIn({ error }) {
  return (
    <div style={{ padding: "120px 0", maxWidth: 520 }}>
      <div className="eyebrow accent-text" style={{ marginBottom: 16 }}>
        <span className="dot"></span>LULIUS · ADMIN
      </div>
      <h1 className="display" style={{ fontSize: "clamp(36px, 5vw, 64px)", marginBottom: 24 }}>
        Sign in.
      </h1>
      <p className="lede mute" style={{ marginBottom: 40 }}>
        Content editing for Lulius dispatches. Access is restricted to authorized
        Google accounts.
      </p>
      <a href="/api/admin/auth/google" className="btn">
        Sign in with Google <span className="arrow">→</span>
      </a>
      {error && <div style={{ marginTop: 32 }}><ErrorBanner text={error} /></div>}
    </div>
  );
}

const TITLES = {
  "list": "Dispatch",
  "edit": "Edit dispatch",
  "new": "New dispatch",
  "careers": "Careers",
  "careers-edit": "Edit position",
  "careers-new": "New position",
  "team": "Personnel",
  "team-edit": "Edit person",
  "team-new": "New person",
  "corr": "Correspondence",
};

function Header({ me, view, onDispatch, onCareers, onTeam, onCorr, onSignOut }) {
  const section = view.name.startsWith("team") ? "team"
    : view.name.startsWith("careers") ? "careers"
    : view.name === "corr" ? "corr"
    : "dispatch";
  const tab = (active) => ({
    ...mutedBtn,
    borderColor: active ? "var(--accent)" : "var(--line-2)",
    color: active ? "var(--accent)" : "var(--paper)",
    background: active ? "rgba(200, 164, 92, .08)" : "transparent",
  });
  return (
    <header style={{
      display: "flex", justifyContent: "space-between", alignItems: "center",
      paddingBottom: 24, marginBottom: 32, borderBottom: "1px solid var(--line)",
      gap: 24, flexWrap: "wrap",
    }}>
      <div>
        <div className="eyebrow accent-text">
          <span className="dot"></span>LULIUS · ADMIN
        </div>
        <h1 className="h2" style={{ margin: "12px 0 14px", fontSize: "clamp(24px, 3vw, 32px)" }}>
          {TITLES[view.name] || "Admin"}
        </h1>
        <div style={{ display: "flex", gap: 8 }}>
          <button onClick={onDispatch} style={tab(section === "dispatch")}>Dispatch</button>
          <button onClick={onCareers} style={tab(section === "careers")}>Careers</button>
          <button onClick={onTeam} style={tab(section === "team")}>Personnel</button>
          <button onClick={onCorr} style={tab(section === "corr")}>Correspondence</button>
        </div>
      </div>
      <div style={{ textAlign: "right" }}>
        <div className="eyebrow mute" style={{ marginBottom: 6 }}>SIGNED IN</div>
        <div className="h3" style={{ margin: 0, fontSize: 14 }}>{me.email}</div>
        <button onClick={onSignOut} style={{ ...mutedBtn, marginTop: 10 }}>Sign out</button>
      </div>
    </header>
  );
}

const mutedBtn = {
  background: "transparent",
  border: "1px solid var(--line-2)",
  color: "var(--paper)",
  padding: "6px 12px",
  fontFamily: "var(--f-mono)",
  fontSize: 11,
  letterSpacing: ".12em",
  textTransform: "uppercase",
  cursor: "pointer",
};

function ErrorBanner({ text }) {
  return (
    <div style={{
      marginBottom: 24, padding: "14px 18px",
      border: "1px solid var(--accent)", color: "var(--paper)",
      fontFamily: "var(--f-mono)", fontSize: 12, letterSpacing: ".06em",
    }}>
      ERR · {text}
    </div>
  );
}

function FlashBanner({ text }) {
  return (
    <div style={{
      marginBottom: 24, padding: "14px 18px",
      border: "1px solid var(--accent)",
      background: "rgba(200, 164, 92, .08)",
      color: "var(--paper)",
      fontFamily: "var(--f-mono)", fontSize: 12, letterSpacing: ".06em",
    }}>
      ✓ {text}
    </div>
  );
}

// ───────────────────────── entry list (dispatch / careers) ──────────────────

function DispatchList({ kind = "dispatch", noun = "dispatch", plural = "dispatches", onError, onEdit, onNew, onFlash }) {
  const [entries, setEntries] = useState(null);
  const [busy, setBusy] = useState(null);

  const load = () => {
    api("/entries")
      .then((r) => r.ok ? r.json() : Promise.reject(new Error(`HTTP ${r.status}`)))
      .then((data) => setEntries(data.entries || []))
      .catch((err) => { onError(err.message); setEntries([]); });
  };
  useEffect(load, []);

  if (entries === null) return <LoadingShell />;

  const dispatches = entries
    .filter((e) => e.kind === kind)
    .sort((a, b) => String(b.date || "").localeCompare(String(a.date || "")));

  const setArchived = async (e, archived) => {
    setBusy(e.slug); onError("");
    try {
      const r = await api(`/entries/${e.slug}`, { method: "PUT", body: JSON.stringify({ archived }) });
      const j = await r.json();
      if (!r.ok || !j.ok) throw new Error(j.error || `HTTP ${r.status}`);
      onFlash(`"${e.title}" ${archived ? "archived" : "restored"}.`);
      load();
    } catch (err) { onError(err.message); } finally { setBusy(null); }
  };

  const remove = async (e) => {
    if (!window.confirm(`Permanently delete "${e.title}"? This cannot be undone. (Use Archive to just hide it.)`)) return;
    setBusy(e.slug); onError("");
    try {
      const r = await api(`/entries/${e.slug}`, { method: "DELETE" });
      const j = await r.json();
      if (!r.ok || !j.ok) throw new Error(j.error || `HTTP ${r.status}`);
      onFlash(`"${e.title}" deleted.`);
      load();
    } catch (err) { onError(err.message); } finally { setBusy(null); }
  };

  return (
    <div>
      <div style={{ display: "flex", justifyContent: "space-between", alignItems: "baseline", marginBottom: 16 }}>
        <div className="eyebrow mute tabular">{dispatches.length} ENTRIES</div>
        <button onClick={onNew} className="btn" style={{ padding: "8px 16px" }}>+ New {noun}</button>
      </div>
      {dispatches.length === 0 ? (
        <div style={{ padding: "40px 0", textAlign: "center" }}>
          <p className="mute">No {plural} yet. Click “New {noun}” to publish one.</p>
        </div>
      ) : (
        <div style={{ borderTop: "1px solid var(--line)" }}>
          {dispatches.map((e) => (
            <DispatchRow
              key={e.slug}
              entry={e}
              busy={busy === e.slug}
              onEdit={onEdit}
              onArchive={() => setArchived(e, true)}
              onRestore={() => setArchived(e, false)}
              onDelete={() => remove(e)}
            />
          ))}
        </div>
      )}
    </div>
  );
}

function DispatchRow({ entry, busy, onEdit, onArchive, onRestore, onDelete }) {
  const archived = !!entry.archived;
  return (
    <div style={{
      display: "grid", gridTemplateColumns: "110px 1fr 110px 1fr",
      gap: 20, alignItems: "center",
      padding: "20px 0",
      borderBottom: "1px solid var(--line)",
      opacity: archived ? 0.55 : 1,
    }}>
      <div className="eyebrow mute tabular">{entry.dateLabel || entry.date || "—"}</div>
      <div>
        <div className="h3" style={{ margin: 0, fontSize: 17 }}>{entry.title}</div>
        {entry.subtitle && (
          <div className="mute" style={{ fontSize: 13, marginTop: 4 }}>{entry.subtitle}</div>
        )}
        <div className="eyebrow mute tabular" style={{ marginTop: 8, opacity: 0.7 }}>
          {entry.slug}
        </div>
      </div>
      <div className="eyebrow" style={{
        color: archived ? "var(--mute)" : "var(--accent)",
        letterSpacing: ".12em",
      }}>
        {archived ? "ARCHIVED" : "PUBLISHED"}
      </div>
      <div style={{ textAlign: "right", display: "flex", gap: 8, justifyContent: "flex-end", flexWrap: "wrap" }}>
        <a href={`/#/entry/${entry.slug}`} target="_blank" rel="noopener" style={{ ...mutedBtn, textDecoration: "none", color: "var(--paper)" }}>
          View →
        </a>
        <button onClick={() => onEdit(entry.slug)} disabled={busy} style={{ ...mutedBtn, borderColor: "var(--accent)", color: "var(--accent)" }}>
          Edit →
        </button>
        {archived
          ? <button onClick={onRestore} disabled={busy} style={mutedBtn}>Restore</button>
          : <button onClick={onArchive} disabled={busy} style={mutedBtn}>Archive</button>}
        <button onClick={onDelete} disabled={busy} style={{ ...mutedBtn, color: "var(--mute)" }}>Delete</button>
      </div>
    </div>
  );
}

// ───────────────────────── edit entry ───────────────────────────────────────

function EntryEdit({ slug, isNew, kind = "dispatch", onError, onSaved, onBack }) {
  const [entry, setEntry] = useState(null);
  const [form, setForm] = useState(isNew ? initialFormForNew(kind) : null);
  const [blocks, setBlocks] = useState(isNew ? [] : null);
  const [saving, setSaving] = useState(false);

  useEffect(() => {
    if (isNew) return;
    api(`/entries/${slug}`)
      .then((r) => r.ok ? r.json() : Promise.reject(new Error(`HTTP ${r.status}`)))
      .then((data) => {
        setEntry(data.entry);
        setForm(initialFormFromEntry(data.entry));
        setBlocks(Array.isArray(data.entry.blocks) ? data.entry.blocks.map((b) => ({ ...b })) : []);
      })
      .catch((err) => onError(err.message));
  }, [slug]);

  if (!form || blocks === null) return <LoadingShell label="LOADING ENTRY" />;

  const setF = (k, v) => setForm({ ...form, [k]: v });

  const save = async () => {
    if (!form.title.trim()) { onError("Title is required."); return; }
    setSaving(true);
    onError("");
    try {
      const payload = formToPayload(form);
      payload.blocks = cleanBlocks(blocks);
      if (isNew && form.slug.trim()) payload.slug = form.slug.trim();
      if (isNew) payload.kind = kind;

      const r = await api(isNew ? "/entries" : `/entries/${slug}`, {
        method: isNew ? "POST" : "PUT",
        body: JSON.stringify(payload),
      });
      const j = await r.json();
      if (!r.ok || !j.ok) throw new Error(j.error || `HTTP ${r.status}`);

      if (isNew) {
        onSaved(`Published "${j.entry.title}". Live at /#/entry/${j.entry.slug}`);
      } else {
        setEntry(j.entry);
        setForm(initialFormFromEntry(j.entry));
        setBlocks(Array.isArray(j.entry.blocks) ? j.entry.blocks.map((b) => ({ ...b })) : []);
        onSaved("Saved. Live on /#/entry/" + slug);
      }
    } catch (err) {
      onError(err.message);
    } finally {
      setSaving(false);
    }
  };

  return (
    <div style={{ display: "flex", flexDirection: "column", gap: 32 }}>
      <button onClick={onBack} style={mutedBtn}>← Back to {kind === "career" ? "careers" : "dispatch"} list</button>

      <SectionTitle title="Identifiers" />
      {isNew ? (
        <FormRow label="Slug (URL)" hint="Becomes /#/entry/<slug>. Leave blank to auto-generate from the title. Locked after publishing.">
          <input value={form.slug} onChange={(e) => setF("slug", e.target.value)} placeholder="auto from title" style={inputStyle} />
        </FormRow>
      ) : (
        <div style={{ display: "grid", gridTemplateColumns: "1fr 1fr", gap: 24 }}>
          <Locked label="Slug (locked)" value={entry.slug} />
          <Locked label="Kind (locked)" value={entry.kind} />
        </div>
      )}

      <SectionTitle title="Headline" />
      <FormRow label="Title" required>
        <input value={form.title} onChange={(e) => setF("title", e.target.value)} style={inputStyle} />
      </FormRow>
      <FormRow label="Subtitle">
        <input value={form.subtitle} onChange={(e) => setF("subtitle", e.target.value)} style={inputStyle} />
      </FormRow>
      <FormRow label="Lede" hint="One short paragraph that opens the page.">
        <textarea value={form.lede} onChange={(e) => setF("lede", e.target.value)} rows={3} style={{ ...inputStyle, resize: "vertical" }} />
      </FormRow>

      <SectionTitle title="Meta" />
      <div style={{ display: "grid", gridTemplateColumns: "1fr 1fr 1fr", gap: 24 }}>
        <FormRow label="Tag" hint="e.g. Platform, Recognition, Company">
          <input value={form.tag} onChange={(e) => setF("tag", e.target.value)} style={inputStyle} />
        </FormRow>
        <FormRow label="Date (YYYY-MM-DD)" hint="Used for sort order.">
          <input value={form.date} onChange={(e) => setF("date", e.target.value)} placeholder="2026-05-27" style={inputStyle} />
        </FormRow>
        <FormRow label="Date label" hint="Display only, e.g. 27 MAY 26">
          <input value={form.dateLabel} onChange={(e) => setF("dateLabel", e.target.value)} placeholder="27 MAY 26" style={inputStyle} />
        </FormRow>
      </div>
      <FormRow label="Doc id" hint="e.g. LI-PR-014">
        <input value={form.doc} onChange={(e) => setF("doc", e.target.value)} style={inputStyle} />
      </FormRow>

      <SectionTitle title="Hero" hint="Optional. Turn off for a text-only entry (renders with just the headline)." />
      <label style={{ display: "flex", alignItems: "center", gap: 12, cursor: "pointer", color: "var(--paper)" }}>
        <input type="checkbox" checked={form.hasHero} onChange={(e) => setF("hasHero", e.target.checked)} />
        <span>This entry has a hero image / media</span>
      </label>

      {form.hasHero ? (
        <>
          <FormRow label="Hero URL" hint="For now, paste a URL (image OR self-contained .html). Cloudinary upload widget lands in Phase 3.">
            <input value={form.hero} onChange={(e) => setF("hero", e.target.value)} placeholder="media/entries/.../hero.jpg or https://…" style={inputStyle} />
          </FormRow>
          <div style={{ display: "grid", gridTemplateColumns: "1fr 1fr", gap: 24 }}>
            <FormRow label="Hero layout">
              <select value={form.heroLayout || ""} onChange={(e) => setF("heroLayout", e.target.value)} style={inputStyle}>
                <option value="">— (default: cinematic)</option>
                <option value="cinematic">cinematic (21:9 full-bleed)</option>
                <option value="feature">feature (16:9 contained)</option>
                <option value="profile">profile (2-col with title block)</option>
              </select>
            </FormRow>
            <FormRow label="Hero caption">
              <input value={form.heroCaption} onChange={(e) => setF("heroCaption", e.target.value)} style={inputStyle} />
            </FormRow>
          </div>
          <div style={{ display: "grid", gridTemplateColumns: "1fr 1fr 1fr 1fr", gap: 24 }}>
            <FormRow label="Hero aspect" hint='e.g. "16 / 9"'>
              <input value={form.heroAspect} onChange={(e) => setF("heroAspect", e.target.value)} placeholder="16 / 9" style={inputStyle} />
            </FormRow>
            <FormRow label="Hero position" hint='e.g. "50% 25%"'>
              <input value={form.heroPosition} onChange={(e) => setF("heroPosition", e.target.value)} placeholder="50% 50%" style={inputStyle} />
            </FormRow>
            <FormRow label="Base width" hint="HTML hero only">
              <input type="number" value={form.heroBaseWidth} onChange={(e) => setF("heroBaseWidth", e.target.value)} placeholder="1920" style={inputStyle} />
            </FormRow>
            <FormRow label="Base height" hint="HTML hero only">
              <input type="number" value={form.heroBaseHeight} onChange={(e) => setF("heroBaseHeight", e.target.value)} placeholder="1080" style={inputStyle} />
            </FormRow>
          </div>
        </>
      ) : (
        <div className="mute" style={{ fontSize: 13, padding: "8px 0" }}>
          No hero — this entry will render with just the headline (no image, no caption).
        </div>
      )}

      <SectionTitle title="Meta table" hint='Repeatable "key, value" rows displayed in the entry meta strip.' />
      <RepeatableList
        items={form.meta}
        onChange={(items) => setF("meta", items)}
        addLabel="+ Add meta row"
        renderItem={(row, update) => (
          <>
            <input value={row[0] || ""} onChange={(e) => update([e.target.value, row[1] || ""])} placeholder="key" style={inputStyle} />
            <input value={row[1] || ""} onChange={(e) => update([row[0] || "", e.target.value])} placeholder="value" style={inputStyle} />
          </>
        )}
        emptyItem={() => ["", ""]}
        gridTemplate="1fr 1fr 40px"
      />

      <SectionTitle title="Links" hint="External CTAs / references rendered below the body." />
      <RepeatableList
        items={form.links}
        onChange={(items) => setF("links", items)}
        addLabel="+ Add link"
        renderItem={(row, update) => (
          <>
            <input value={row.label || ""} onChange={(e) => update({ ...row, label: e.target.value })} placeholder="label" style={inputStyle} />
            <input value={row.href || ""} onChange={(e) => update({ ...row, href: e.target.value })} placeholder="https:// or #/route" style={inputStyle} />
            <label style={{ display: "flex", alignItems: "center", gap: 6, color: "var(--mute)", fontSize: 12 }}>
              <input type="checkbox" checked={!!row.external} onChange={(e) => update({ ...row, external: e.target.checked })} />
              external
            </label>
          </>
        )}
        emptyItem={() => ({ label: "", href: "", external: false })}
        gridTemplate="1fr 1.5fr auto 40px"
      />

      <SectionTitle title="Related entries" hint="Comma-separated slugs. Other entries to surface at the bottom of the page." />
      <FormRow label="">
        <input
          value={(form.related || []).join(", ")}
          onChange={(e) => setF("related", e.target.value.split(",").map((s) => s.trim()).filter(Boolean))}
          placeholder="capability-app-dev, capability-it-strategy"
          style={inputStyle}
        />
      </FormRow>

      <SectionTitle title="Status" />
      <label style={{ display: "flex", alignItems: "center", gap: 12, cursor: "pointer", color: "var(--paper)" }}>
        <input type="checkbox" checked={!!form.archived} onChange={(e) => setF("archived", e.target.checked)} />
        <span>
          {kind === "career"
            ? "Archived — position closed; hidden from the Careers page (direct URL still works)"
            : "Archived — hide from public Dispatch list (direct URL still works)"}
        </span>
      </label>

      <SectionTitle title="Body" hint="The article. Add paragraphs, headings, pullquotes, and lists; reorder with ▲ ▼." />
      <BlocksEditor blocks={blocks} onChange={setBlocks} />

      <div style={{ display: "flex", gap: 12, marginTop: 24, paddingTop: 24, borderTop: "1px solid var(--line)" }}>
        <button onClick={save} disabled={saving} className="btn" style={{ opacity: saving ? 0.5 : 1, cursor: saving ? "wait" : "pointer" }}>
          {saving ? "Saving…" : <>{isNew ? (kind === "career" ? "Publish position" : "Publish dispatch") : "Save changes"} <span className="arrow">→</span></>}
        </button>
        <button onClick={onBack} style={mutedBtn}>Cancel</button>
      </div>
    </div>
  );
}

function initialFormForNew(kind) {
  return {
    slug: "", title: "", subtitle: "", lede: "", tag: "", date: "", dateLabel: "", doc: "",
    hasHero: false, hero: "", heroLayout: "", heroAspect: "", heroPosition: "", heroCaption: "",
    heroBaseWidth: "", heroBaseHeight: "",
    // Careers prefill: Location/Type surface on the public listing + entry meta
    // strip; the Apply link defaults to the application flow and stays editable
    // per posting (point it at an external ATS later if needed).
    meta: kind === "career" ? [["Location", ""], ["Type", ""]] : [],
    links: kind === "career" ? [{ label: "Apply", href: "#/apply", external: false }] : [],
    related: [], archived: false,
  };
}

function initialFormFromEntry(e) {
  return {
    title: e.title || "",
    subtitle: e.subtitle || "",
    lede: e.lede || "",
    tag: e.tag || "",
    date: e.date || "",
    dateLabel: e.dateLabel || "",
    doc: e.doc || "",
    hasHero: !!e.hero,
    hero: e.hero || "",
    heroLayout: e.heroLayout || "",
    heroAspect: e.heroAspect || "",
    heroPosition: e.heroPosition || "",
    heroCaption: e.heroCaption || "",
    heroBaseWidth: e.heroBaseWidth ?? "",
    heroBaseHeight: e.heroBaseHeight ?? "",
    meta: Array.isArray(e.meta) ? e.meta.map((r) => [...r]) : [],
    links: Array.isArray(e.links) ? e.links.map((l) => ({ ...l })) : [],
    related: Array.isArray(e.related) ? [...e.related] : [],
    archived: !!e.archived,
  };
}

// Build the PUT payload. Strip empty optional fields so they don't pollute
// entries.json with empty strings (writes the same shape they came in with).
function formToPayload(f) {
  const out = {
    title: f.title.trim(),
    subtitle: f.subtitle.trim(),
    lede: f.lede.trim(),
    tag: f.tag.trim(),
    date: f.date.trim(),
    dateLabel: f.dateLabel.trim(),
    doc: f.doc.trim(),
    meta: f.meta.filter((r) => (r[0] || "").trim() || (r[1] || "").trim()),
    links: f.links.filter((l) => (l.label || "").trim() && (l.href || "").trim()),
    related: f.related,
    archived: !!f.archived,
  };
  if (f.hasHero) {
    out.hero = f.hero.trim();
    out.heroCaption = f.heroCaption.trim();
    // Optional hero settings — only include if set.
    if (f.heroLayout) out.heroLayout = f.heroLayout;
    if (f.heroAspect.trim()) out.heroAspect = f.heroAspect.trim();
    if (f.heroPosition.trim()) out.heroPosition = f.heroPosition.trim();
    if (f.heroBaseWidth !== "" && f.heroBaseWidth != null) out.heroBaseWidth = Number(f.heroBaseWidth);
    if (f.heroBaseHeight !== "" && f.heroBaseHeight != null) out.heroBaseHeight = Number(f.heroBaseHeight);
  } else {
    // No hero — clear the display-affecting fields so nothing renders. The
    // dimensional hints (heroAspect/heroPosition/heroBase*) are left dormant;
    // EntryPage ignores them once hero is empty.
    out.hero = "";
    out.heroCaption = "";
    out.heroLayout = "";
  }
  return out;
}

// ───────────────────────── form helpers ─────────────────────────────────────

function SectionTitle({ title, hint }) {
  return (
    <div style={{ marginTop: 16, borderBottom: "1px solid var(--line)", paddingBottom: 8 }}>
      <div className="eyebrow accent-text" style={{ marginBottom: 4 }}>
        <span className="dot"></span>{title.toUpperCase()}
      </div>
      {hint && <div className="mute" style={{ fontSize: 12 }}>{hint}</div>}
    </div>
  );
}

function FormRow({ label, hint, required, children }) {
  return (
    <div>
      <label style={{
        display: "block",
        fontFamily: "var(--f-mono)", fontSize: 11, letterSpacing: ".14em",
        textTransform: "uppercase", color: "var(--mute)",
        marginBottom: 6,
      }}>
        {label}{required && <span style={{ color: "var(--accent)" }}> ·</span>}
      </label>
      {children}
      {hint && <div className="mute" style={{ fontSize: 12, marginTop: 4 }}>{hint}</div>}
    </div>
  );
}

function Locked({ label, value }) {
  return (
    <div>
      <div className="eyebrow mute" style={{ marginBottom: 6 }}>{label}</div>
      <div style={{ ...inputStyle, opacity: 0.55, cursor: "not-allowed" }}>{value}</div>
    </div>
  );
}

const inputStyle = {
  background: "transparent",
  border: "1px solid var(--line-2)",
  color: "var(--paper)",
  padding: "10px 12px",
  fontFamily: "var(--f-body)",
  fontSize: 15,
  width: "100%",
  outline: "none",
  boxSizing: "border-box",
};

function RepeatableList({ items, onChange, addLabel, renderItem, emptyItem, gridTemplate }) {
  return (
    <div style={{ display: "flex", flexDirection: "column", gap: 8 }}>
      {items.map((item, i) => (
        <div key={i} style={{ display: "grid", gridTemplateColumns: gridTemplate, gap: 8, alignItems: "center" }}>
          {renderItem(item, (next) => {
            const copy = [...items];
            copy[i] = next;
            onChange(copy);
          })}
          <button onClick={() => onChange(items.filter((_, j) => j !== i))} style={{ ...mutedBtn, color: "var(--mute)", padding: "6px 8px" }}>×</button>
        </div>
      ))}
      <button onClick={() => onChange([...items, emptyItem()])} style={{ ...mutedBtn, alignSelf: "flex-start" }}>
        {addLabel}
      </button>
    </div>
  );
}

// Body block editor. Rich text-ish blocks (paragraph/heading/pullquote/list)
// get real inputs. Any other block type already in an entry (image/video/stats/
// spacer/html) is preserved verbatim and shown as a read-only "advanced" card,
// so editing an existing entry never drops its richer blocks.
const EDITABLE_BLOCK_TYPES = ["paragraph", "heading", "pullquote", "list"];

function newBlock(type) {
  if (type === "list") return { type: "list", items: [""] };
  if (type === "pullquote") return { type: "pullquote", text: "", attribution: "" };
  return { type, text: "" };
}

// Drop empty text/list blocks on save; keep advanced blocks intact.
function cleanBlocks(blocks) {
  const out = [];
  for (const b of blocks) {
    if (b.type === "list") {
      const items = (b.items || []).map((s) => String(s).trim()).filter(Boolean);
      if (items.length) out.push({ ...b, items });
    } else if (b.type === "paragraph" || b.type === "heading" || b.type === "pullquote") {
      const text = String(b.text || "").trim();
      if (!text) continue;
      const nb = { type: b.type, text };
      if (b.type === "pullquote" && String(b.attribution || "").trim()) nb.attribution = b.attribution.trim();
      out.push(nb);
    } else {
      out.push(b);
    }
  }
  return out;
}

function BlocksEditor({ blocks, onChange }) {
  const update = (i, next) => { const c = [...blocks]; c[i] = next; onChange(c); };
  const removeAt = (i) => onChange(blocks.filter((_, j) => j !== i));
  const move = (i, dir) => {
    const j = i + dir;
    if (j < 0 || j >= blocks.length) return;
    const c = [...blocks];
    [c[i], c[j]] = [c[j], c[i]];
    onChange(c);
  };

  return (
    <div style={{ display: "flex", flexDirection: "column", gap: 12 }}>
      {blocks.length === 0 && (
        <div className="mute" style={{ fontSize: 14 }}>No body yet — add a block below.</div>
      )}
      {blocks.map((b, i) => (
        <BlockCard
          key={i}
          block={b}
          isFirst={i === 0}
          isLast={i === blocks.length - 1}
          onChange={(next) => update(i, next)}
          onUp={() => move(i, -1)}
          onDown={() => move(i, 1)}
          onRemove={() => removeAt(i)}
        />
      ))}
      <div style={{ display: "flex", gap: 8, flexWrap: "wrap", marginTop: 4 }}>
        {EDITABLE_BLOCK_TYPES.map((t) => (
          <button key={t} onClick={() => onChange([...blocks, newBlock(t)])} style={{ ...mutedBtn, borderColor: "var(--accent)", color: "var(--accent)" }}>
            + {t}
          </button>
        ))}
      </div>
    </div>
  );
}

function BlockCard({ block, isFirst, isLast, onChange, onUp, onDown, onRemove }) {
  const editable = EDITABLE_BLOCK_TYPES.includes(block.type);
  const hb = { ...mutedBtn, padding: "4px 8px" };
  return (
    <div style={{ border: "1px solid var(--line-2)", padding: "14px 16px", display: "flex", flexDirection: "column", gap: 10 }}>
      <div style={{ display: "flex", justifyContent: "space-between", alignItems: "center", gap: 12 }}>
        <span className="eyebrow accent-text" style={{ fontSize: 10 }}>{block.type}</span>
        <div style={{ display: "flex", gap: 6 }}>
          <button onClick={onUp} disabled={isFirst} style={{ ...hb, opacity: isFirst ? 0.4 : 1 }}>▲</button>
          <button onClick={onDown} disabled={isLast} style={{ ...hb, opacity: isLast ? 0.4 : 1 }}>▼</button>
          <button onClick={onRemove} style={{ ...hb, color: "var(--mute)" }}>×</button>
        </div>
      </div>

      {block.type === "paragraph" && (
        <textarea value={block.text || ""} onChange={(e) => onChange({ ...block, text: e.target.value })} rows={3} placeholder="Paragraph text…" style={{ ...inputStyle, resize: "vertical" }} />
      )}
      {block.type === "heading" && (
        <input value={block.text || ""} onChange={(e) => onChange({ ...block, text: e.target.value })} placeholder="Heading…" style={inputStyle} />
      )}
      {block.type === "pullquote" && (
        <>
          <textarea value={block.text || ""} onChange={(e) => onChange({ ...block, text: e.target.value })} rows={2} placeholder="Quote…" style={{ ...inputStyle, resize: "vertical" }} />
          <input value={block.attribution || ""} onChange={(e) => onChange({ ...block, attribution: e.target.value })} placeholder="Attribution (optional)" style={inputStyle} />
        </>
      )}
      {block.type === "list" && (
        <textarea
          value={(block.items || []).join("\n")}
          onChange={(e) => onChange({ ...block, items: e.target.value.split("\n") })}
          rows={Math.max(3, (block.items || []).length)}
          placeholder="One item per line"
          style={{ ...inputStyle, resize: "vertical" }}
        />
      )}
      {!editable && (
        <div className="mute" style={{ fontSize: 12, fontFamily: "var(--f-mono)" }}>
          Advanced block — preserved as-is (edit via entries.json).{" "}
          {block.src ? block.src : block.text ? `"${String(block.text).slice(0, 80)}"` : Array.isArray(block.items) ? `${block.items.length} items` : ""}
        </div>
      )}
    </div>
  );
}

// ───────────────────────── personnel list ──────────────────────────────────

const GROUP_LABEL = { advisory: "Advisory", team: "Team" };

// The admin app is served from /admin/, so repo-relative paths (media/…,
// content/…) must be rooted with a leading slash to load. Absolute URLs and
// already-rooted paths pass through unchanged.
// ───────────────────────── correspondence list ──────────────────────────────
// Read-and-remove only: subscribers join exclusively through the public
// double-opt-in flow at /#/correspondence. Export CSV includes each row's
// signed unsubscribe link for use in whatever mailer sends the actual emails.

function CorrespondenceList({ onError, onFlash }) {
  const [subs, setSubs] = useState(null);
  const [busy, setBusy] = useState(null);

  const load = () => {
    api("/correspondence")
      .then((r) => r.ok ? r.json() : Promise.reject(new Error(`HTTP ${r.status}`)))
      .then((data) => setSubs(data.subscribers || []))
      .catch((err) => { onError(err.message); setSubs([]); });
  };
  useEffect(load, []);

  if (subs === null) return <LoadingShell />;

  const sorted = [...subs].sort((a, b) => String(b.subscribedAt || "").localeCompare(String(a.subscribedAt || "")));

  const remove = async (s) => {
    if (!window.confirm(`Remove ${s.email} from the correspondence list?`)) return;
    setBusy(s.email); onError("");
    try {
      const r = await api(`/correspondence/${encodeURIComponent(s.email)}`, { method: "DELETE" });
      const j = await r.json();
      if (!r.ok || !j.ok) throw new Error(j.error || `HTTP ${r.status}`);
      onFlash(`${s.email} removed.`);
      load();
    } catch (err) { onError(err.message); } finally { setBusy(null); }
  };

  const exportCsv = () => {
    const esc = (v) => `"${String(v ?? "").replace(/"/g, '""')}"`;
    const rows = [
      ["email", "subscribedAt", "source", "unsubscribeUrl"].join(","),
      ...sorted.map((s) => [esc(s.email), esc(s.subscribedAt), esc(s.source), esc(s.unsubscribeUrl)].join(",")),
    ];
    const blob = new Blob([rows.join("\n") + "\n"], { type: "text/csv" });
    const a = document.createElement("a");
    a.href = URL.createObjectURL(blob);
    a.download = `lulius-correspondence-${new Date().toISOString().slice(0, 10)}.csv`;
    a.click();
    URL.revokeObjectURL(a.href);
  };

  return (
    <div>
      <div style={{ display: "flex", justifyContent: "space-between", alignItems: "baseline", marginBottom: 16 }}>
        <div className="eyebrow mute tabular">{sorted.length} SUBSCRIBER{sorted.length === 1 ? "" : "S"}</div>
        <button onClick={exportCsv} disabled={sorted.length === 0} className="btn" style={{ padding: "8px 16px", opacity: sorted.length === 0 ? 0.5 : 1 }}>
          Export CSV
        </button>
      </div>
      <p className="mute" style={{ fontSize: 13, marginBottom: 24 }}>
        Every address here completed double opt-in (verified a 6-digit emailed code). The CSV export
        includes each subscriber's signed one-click unsubscribe link — include it in anything you send.
      </p>
      {sorted.length === 0 ? (
        <div style={{ padding: "40px 0", textAlign: "center" }}>
          <p className="mute">No subscribers yet. The public signup lives at /#/correspondence.</p>
        </div>
      ) : (
        <div style={{ borderTop: "1px solid var(--line)" }}>
          {sorted.map((s) => (
            <div key={s.email} style={{
              display: "grid", gridTemplateColumns: "1fr 160px 110px auto",
              gap: 20, alignItems: "center", padding: "16px 0",
              borderBottom: "1px solid var(--line)",
            }}>
              <div className="h3" style={{ margin: 0, fontSize: 15, wordBreak: "break-all" }}>{s.email}</div>
              <div className="eyebrow mute tabular">{String(s.subscribedAt || "").slice(0, 10) || "—"}</div>
              <div className="eyebrow" style={{ color: "var(--accent)", letterSpacing: ".12em" }}>{s.source || "direct"}</div>
              <div style={{ textAlign: "right" }}>
                <button onClick={() => remove(s)} disabled={busy === s.email} style={{ ...mutedBtn, color: "var(--mute)" }}>Remove</button>
              </div>
            </div>
          ))}
        </div>
      )}
    </div>
  );
}

function adminSrc(p) {
  if (!p) return "";
  if (/^https?:\/\//i.test(p) || p.startsWith("/")) return p;
  return "/" + p;
}

function PersonnelList({ onError, onEdit, onNew, onFlash }) {
  const [people, setPeople] = useState(null);
  const [busyId, setBusyId] = useState(null);

  const load = () => {
    api("/team")
      .then((r) => r.ok ? r.json() : Promise.reject(new Error(`HTTP ${r.status}`)))
      .then((data) => setPeople(data.people || []))
      .catch((err) => { onError(err.message); setPeople([]); });
  };
  useEffect(load, []);

  if (people === null) return <LoadingShell label="LOADING PERSONNEL" />;

  const setArchived = async (p, archived) => {
    setBusyId(p.id);
    onError("");
    try {
      const r = await api(`/team/${p.id}`, { method: "PUT", body: JSON.stringify({ archived }) });
      const j = await r.json();
      if (!r.ok || !j.ok) throw new Error(j.error || `HTTP ${r.status}`);
      onFlash(`${p.name} ${archived ? "archived" : "restored"}.`);
      load();
    } catch (err) { onError(err.message); } finally { setBusyId(null); }
  };

  const remove = async (p) => {
    if (!window.confirm(`Permanently delete ${p.name}? This cannot be undone. (Use Archive to just hide them.)`)) return;
    setBusyId(p.id);
    onError("");
    try {
      const r = await api(`/team/${p.id}`, { method: "DELETE" });
      const j = await r.json();
      if (!r.ok || !j.ok) throw new Error(j.error || `HTTP ${r.status}`);
      onFlash(`${p.name} deleted.`);
      load();
    } catch (err) { onError(err.message); } finally { setBusyId(null); }
  };

  return (
    <div>
      <div style={{ display: "flex", justifyContent: "space-between", alignItems: "baseline", marginBottom: 16 }}>
        <div className="eyebrow mute tabular">{people.length} PEOPLE</div>
        <button onClick={onNew} className="btn" style={{ padding: "8px 16px" }}>+ New person</button>
      </div>
      <div style={{ borderTop: "1px solid var(--line)" }}>
        {people.map((p) => (
          <PersonRow
            key={p.id}
            person={p}
            busy={busyId === p.id}
            onEdit={onEdit}
            onArchive={() => setArchived(p, true)}
            onRestore={() => setArchived(p, false)}
            onDelete={() => remove(p)}
          />
        ))}
      </div>
    </div>
  );
}

function PersonRow({ person, busy, onEdit, onArchive, onRestore, onDelete }) {
  const archived = !!person.archived;
  return (
    <div style={{
      display: "grid", gridTemplateColumns: "56px 1fr 110px 240px",
      gap: 20, alignItems: "center",
      padding: "16px 0", borderBottom: "1px solid var(--line)",
      opacity: archived ? 0.55 : 1,
    }}>
      <div style={{ width: 56, height: 56, background: "#0A0A0A", border: "1px solid var(--line)", overflow: "hidden" }}>
        {person.img
          ? <img src={adminSrc(person.img)} alt="" style={{ width: "100%", height: "100%", objectFit: "cover", objectPosition: "50% 30%" }} />
          : <div style={{ width: "100%", height: "100%" }} />}
      </div>
      <div>
        <div className="h3" style={{ margin: 0, fontSize: 16 }}>{person.name}</div>
        <div className="mute" style={{ fontSize: 13, marginTop: 2 }}>{person.role || "—"}</div>
        <div className="eyebrow mute tabular" style={{ marginTop: 6, opacity: 0.7 }}>
          {GROUP_LABEL[person.group] || person.group} · {person.id}
        </div>
      </div>
      <div className="eyebrow" style={{ color: archived ? "var(--mute)" : "var(--accent)", letterSpacing: ".12em" }}>
        {archived ? "ARCHIVED" : "PUBLISHED"}
      </div>
      <div style={{ textAlign: "right", display: "flex", gap: 8, justifyContent: "flex-end", flexWrap: "wrap" }}>
        <button onClick={() => onEdit(person.id)} disabled={busy} style={{ ...mutedBtn, borderColor: "var(--accent)", color: "var(--accent)" }}>Edit →</button>
        {archived
          ? <button onClick={onRestore} disabled={busy} style={mutedBtn}>Restore</button>
          : <button onClick={onArchive} disabled={busy} style={mutedBtn}>Archive</button>}
        <button onClick={onDelete} disabled={busy} style={{ ...mutedBtn, color: "var(--mute)" }}>Delete</button>
      </div>
    </div>
  );
}

// ───────────────────────── person edit / create ─────────────────────────────

function PersonEdit({ id, onError, onSaved, onBack }) {
  const isNew = !id;
  const [form, setForm] = useState(isNew ? blankPerson() : null);
  const [saving, setSaving] = useState(false);

  useEffect(() => {
    if (isNew) return;
    api(`/team/${id}`)
      .then((r) => r.ok ? r.json() : Promise.reject(new Error(`HTTP ${r.status}`)))
      .then((data) => setForm({
        group: data.person.group || "team",
        name: data.person.name || "",
        role: data.person.role || "",
        img: data.person.img || "",
        linkedin: data.person.linkedin || "",
        archived: !!data.person.archived,
      }))
      .catch((err) => onError(err.message));
  }, [id]);

  if (!form) return <LoadingShell label="LOADING PERSON" />;

  const setF = (k, v) => setForm({ ...form, [k]: v });

  const save = async () => {
    if (!form.name.trim()) { onError("Name is required."); return; }
    setSaving(true);
    onError("");
    try {
      const payload = {
        group: form.group,
        name: form.name.trim(),
        role: form.role.trim(),
        img: form.img.trim(),
        linkedin: form.linkedin.trim(),
        archived: !!form.archived,
      };
      const r = await api(isNew ? "/team" : `/team/${id}`, {
        method: isNew ? "POST" : "PUT",
        body: JSON.stringify(payload),
      });
      const j = await r.json();
      if (!r.ok || !j.ok) throw new Error(j.error || `HTTP ${r.status}`);
      onSaved(isNew ? `Added ${payload.name}. Live on the Company page.` : `Saved ${payload.name}.`);
    } catch (err) {
      onError(err.message);
    } finally {
      setSaving(false);
    }
  };

  return (
    <div style={{ display: "flex", flexDirection: "column", gap: 32 }}>
      <button onClick={onBack} style={mutedBtn}>← Back to personnel</button>

      <SectionTitle title="Identity" />
      <div style={{ display: "grid", gridTemplateColumns: "1fr 1fr", gap: 24 }}>
        <FormRow label="Group" hint="Where they appear in the Personnel filter.">
          <select value={form.group} onChange={(e) => setF("group", e.target.value)} style={inputStyle}>
            <option value="team">Team</option>
            <option value="advisory">Advisory Board</option>
          </select>
        </FormRow>
        <FormRow label="Name" required>
          <input value={form.name} onChange={(e) => setF("name", e.target.value)} placeholder="COL(Ret) Jane Doe" style={inputStyle} />
        </FormRow>
      </div>
      <FormRow label="Role / Title" hint="e.g. Solutions Architect, Board Member">
        <input value={form.role} onChange={(e) => setF("role", e.target.value)} style={inputStyle} />
      </FormRow>
      <FormRow label="LinkedIn URL" hint="Optional. Shows the 'in' badge on their card.">
        <input value={form.linkedin} onChange={(e) => setF("linkedin", e.target.value)} placeholder="https://www.linkedin.com/in/…" style={inputStyle} />
      </FormRow>

      <SectionTitle title="Photo" hint="Upload a JPEG, PNG, or WebP. Square images look best (cards crop to 1:1)." />
      <ImageUpload value={form.img} onChange={(v) => setF("img", v)} onError={onError} />

      <SectionTitle title="Status" />
      <label style={{ display: "flex", alignItems: "center", gap: 12, cursor: "pointer", color: "var(--paper)" }}>
        <input type="checkbox" checked={!!form.archived} onChange={(e) => setF("archived", e.target.checked)} />
        <span>Archived — hide from the public Company page</span>
      </label>

      <div style={{ display: "flex", gap: 12, marginTop: 24, paddingTop: 24, borderTop: "1px solid var(--line)" }}>
        <button onClick={save} disabled={saving} className="btn" style={{ opacity: saving ? 0.5 : 1, cursor: saving ? "wait" : "pointer" }}>
          {saving ? "Saving…" : <>{isNew ? "Add person" : "Save changes"} <span className="arrow">→</span></>}
        </button>
        <button onClick={onBack} style={mutedBtn}>Cancel</button>
      </div>
    </div>
  );
}

function blankPerson() {
  return { group: "team", name: "", role: "", img: "", linkedin: "", archived: false };
}

function ImageUpload({ value, onChange, onError }) {
  const [busy, setBusy] = useState(false);
  const inputRef = React.useRef(null);
  // Resolve a stored path to something the <img> can load from the admin origin.
  const previewSrc = adminSrc(value);

  const onFile = async (e) => {
    const file = e.target.files && e.target.files[0];
    e.target.value = ""; // allow re-picking the same file
    if (!file) return;
    if (!/^image\/(jpeg|png|webp)$/.test(file.type)) {
      onError("Only JPEG, PNG, or WebP images are allowed.");
      return;
    }
    setBusy(true);
    onError("");
    try {
      const r = await fetch("/api/admin/upload?dir=team", {
        method: "POST",
        credentials: "include",
        headers: { "Content-Type": file.type },
        body: file,
      });
      const j = await r.json();
      if (!r.ok || !j.ok) throw new Error(j.error || `HTTP ${r.status}`);
      onChange(j.path);
    } catch (err) {
      onError("Upload failed: " + err.message);
    } finally {
      setBusy(false);
    }
  };

  return (
    <div style={{ display: "flex", gap: 20, alignItems: "flex-start", flexWrap: "wrap" }}>
      <div style={{ width: 120, height: 120, background: "#0A0A0A", border: "1px solid var(--line)", overflow: "hidden", flexShrink: 0 }}>
        {previewSrc
          ? <img src={previewSrc} alt="" style={{ width: "100%", height: "100%", objectFit: "cover", objectPosition: "50% 30%" }} />
          : <div style={{ width: "100%", height: "100%", display: "flex", alignItems: "center", justifyContent: "center", color: "var(--mute)", fontFamily: "var(--f-mono)", fontSize: 10, letterSpacing: ".14em" }}>NO PHOTO</div>}
      </div>
      <div style={{ flex: 1, minWidth: 240, display: "flex", flexDirection: "column", gap: 10 }}>
        <input ref={inputRef} type="file" accept="image/jpeg,image/png,image/webp" onChange={onFile} style={{ display: "none" }} />
        <button onClick={() => inputRef.current && inputRef.current.click()} disabled={busy} style={{ ...mutedBtn, borderColor: "var(--accent)", color: "var(--accent)", alignSelf: "flex-start" }}>
          {busy ? "Uploading…" : value ? "Replace photo" : "Upload photo"}
        </button>
        <FormRow label="Image path" hint="Auto-filled on upload. You can also paste an existing path (e.g. media/team/name.jpg) or external URL.">
          <input value={value} onChange={(e) => onChange(e.target.value)} placeholder="media/team/name.jpg" style={inputStyle} />
        </FormRow>
      </div>
    </div>
  );
}

const root = ReactDOM.createRoot(document.getElementById("admin-root"));
root.render(<App />);
