// cms-admin.jsx
// The admin layer. A schema-driven editor that lives inside the TweaksPanel
// shell, entirely on top of the live site. Renders from the CMS registry
// (CMS.schema / CMS.meta / CMS.addableTypes / CMS.skinOptions) — it never
// hard-codes a field or a module type.
//
// Edits flow into a *working draft*; Commit publishes it, Discard reverts.
// A front-end-only login gate stands in for the real (Phase B) backend auth.
//
// Props: { doc(working), published, dirty, authed, previewing, actions, auth }
// Load AFTER tweaks-panel.jsx + cms-core.js + cms-data.js, BEFORE the app.

const { useState: useAdminState, useRef: useAdminRef, useEffect: useAdminEffect } = React;

const __CMS_STYLE = `
  .cms-hint{color:rgba(41,38,27,.42);font-size:10px;line-height:1.35;margin:-2px 0 2px}
  .cms-textarea{appearance:none;width:100%;padding:6px 8px;border:.5px solid rgba(0,0,0,.1);
    border-radius:7px;background:rgba(255,255,255,.6);color:inherit;font:inherit;
    line-height:1.45;resize:vertical;outline:none}
  .cms-textarea:focus{border-color:rgba(0,0,0,.25);background:rgba(255,255,255,.85)}

  .cms-mod{border:.5px solid rgba(0,0,0,.1);border-radius:9px;background:rgba(255,255,255,.4);
    overflow:hidden;flex-shrink:0}
  .cms-newbar,.cms-foot{flex-shrink:0}
  .cms-mod+.cms-mod{margin-top:6px}
  .cms-mod.off{opacity:.55}
  .cms-mod-top{display:flex;align-items:center;gap:6px;padding:6px 8px}
  .cms-grip{display:flex;flex-direction:column;gap:2px}
  .cms-grip-spacer{width:20px;flex-shrink:0}
  .cms-ico{appearance:none;border:0;background:transparent;color:rgba(41,38,27,.5);
    width:20px;height:18px;border-radius:5px;cursor:pointer;font-size:11px;line-height:1;
    display:flex;align-items:center;justify-content:center;padding:0}
  .cms-ico:hover:not(:disabled){background:rgba(0,0,0,.07);color:#29261b}
  .cms-ico:disabled{opacity:.28;cursor:default}
  .cms-ico.sm{width:18px;height:16px}
  .cms-mod-name{flex:1;font-weight:600;font-size:11.5px;letter-spacing:.01em;
    overflow:hidden;text-overflow:ellipsis;white-space:nowrap}
  .cms-tag{font-size:9px;font-weight:600;letter-spacing:.06em;text-transform:uppercase;
    color:rgba(41,38,27,.4);background:rgba(0,0,0,.05);padding:2px 5px;border-radius:4px}
  .cms-edit{border-top:.5px solid rgba(0,0,0,.08);padding:10px;display:flex;
    flex-direction:column;gap:9px;background:rgba(255,255,255,.35)}

  .cms-list{display:flex;flex-direction:column;gap:6px}
  .cms-rowfields{display:flex;gap:6px}
  .cms-rowfields .twk-field{flex:1;min-width:0}
  .cms-list-h{font-size:10px;font-weight:600;letter-spacing:.04em;text-transform:uppercase;
    color:rgba(41,38,27,.5)}
  .cms-card{border:.5px solid rgba(0,0,0,.1);border-radius:7px;padding:8px;
    background:rgba(255,255,255,.55);display:flex;flex-direction:column;gap:7px}
  .cms-card.collapsible{gap:0}
  .cms-card.collapsible .cms-card-h{gap:9px}
  .cms-card.collapsed{background:rgba(255,255,255,.38)}
  .cms-card-body{display:flex;flex-direction:column;gap:7px;margin-top:8px}
  .cms-card-thumb{width:42px;height:30px;border-radius:5px;flex-shrink:0;
    background:#ddd9d2;background-size:cover;background-position:center;
    border:.5px solid rgba(0,0,0,.14);display:flex;align-items:center;justify-content:center}
  .cms-card-thumb-x{color:rgba(41,38,27,.3);font-size:15px;line-height:1}
  .cms-card-title{appearance:none;border:0;background:transparent;padding:0;cursor:pointer;
    flex:1;min-width:0;display:flex;flex-direction:column;gap:1px;text-align:left;font:inherit}
  .cms-card-title-main{font-weight:600;font-size:11.5px;color:rgba(41,38,27,.82);
    overflow:hidden;text-overflow:ellipsis;white-space:nowrap}
  .cms-card-title-sub{font-size:10px;color:rgba(41,38,27,.45);
    overflow:hidden;text-overflow:ellipsis;white-space:nowrap}
  .cms-card.collapsed .cms-card-title-main{color:rgba(41,38,27,.65)}
  .cms-card-h{display:flex;align-items:center;justify-content:space-between;gap:6px}
  .cms-card-h span{font-weight:600;font-size:10.5px;overflow:hidden;text-overflow:ellipsis;
    white-space:nowrap;color:rgba(41,38,27,.7)}
  .cms-card-tools{display:flex;align-items:center;gap:1px;flex-shrink:0}
  .cms-mini{appearance:none;border:0;background:rgba(0,0,0,.06);color:rgba(41,38,27,.6);
    font:inherit;font-size:10px;padding:2px 7px;border-radius:5px;cursor:pointer;flex-shrink:0}
  .cms-mini:hover{background:rgba(220,60,40,.12);color:rgb(180,40,25)}
  .cms-add{appearance:none;border:.5px dashed rgba(0,0,0,.2);background:transparent;
    color:rgba(41,38,27,.6);font:inherit;font-size:10.5px;font-weight:500;padding:5px;
    border-radius:7px;cursor:pointer}
  .cms-add:hover{background:rgba(0,0,0,.04);color:#29261b}

  .cms-newbar{display:flex;gap:6px;align-items:stretch}
  .cms-newbar select{flex:1}

  /* Section list grouped into one container (rows share borders, no gaps) */
  .cms-group{flex:0 0 auto;border:.5px solid rgba(0,0,0,.1);
    border-radius:9px;overflow:hidden;background:rgba(255,255,255,.4);margin-bottom:6px}
  .cms-group .cms-mod{border:0;border-radius:0;background:transparent}
  .cms-group .cms-mod+.cms-mod{margin-top:0;border-top:.5px solid rgba(0,0,0,.08)}
  .cms-group .cms-mod.off{background:rgba(0,0,0,.02)}

  /* Gallery field — compact summary in the form */
  .cms-count{font-size:10px;color:rgba(41,38,27,.4);font-weight:600}

  /* Compact rows: label left, control right on one line (Theme & skin) */
  .cms-compact .twk-row{flex-direction:row;align-items:center;justify-content:space-between;gap:12px;padding:5px 0}
  .cms-compact .twk-lbl{flex:1 1 auto;min-width:0}
  .cms-compact .twk-row>select.twk-field,
  .cms-compact .twk-row>.twk-seg,
  .cms-compact .twk-row>.cms-hexrow{flex:0 0 56%;max-width:56%;min-width:0}
  .cms-compact .twk-row>select.twk-field{width:100%}
  .cms-compact .cms-hexrow{display:flex;gap:6px}
  .cms-compact .cms-hexrow .twk-field{min-width:0}

  /* Disclosure (collapsible splash content) */
  .cms-disclosure{appearance:none;width:100%;display:flex;align-items:center;gap:7px;
    border:.5px solid rgba(0,0,0,.1);border-radius:8px;background:rgba(255,255,255,.45);
    color:rgba(41,38,27,.78);font:inherit;font-weight:600;font-size:11.5px;
    padding:8px 10px;cursor:pointer;margin-top:2px}
  .cms-disclosure:hover{background:rgba(255,255,255,.7)}
  .cms-disc-caret{display:flex;width:14px;height:14px;color:rgba(41,38,27,.5);
    transition:transform .2s}
  .cms-disc-caret.open{transform:rotate(180deg)}
  .cms-disc-body{display:flex;flex-direction:column;gap:9px;padding:10px 2px 2px}
  .cms-gal-summary{display:flex;align-items:center;gap:8px;justify-content:space-between;
    border:.5px solid rgba(0,0,0,.12);border-radius:8px;padding:7px 9px;cursor:pointer;
    background:rgba(255,255,255,.55)}
  .cms-gal-summary:hover{border-color:rgba(0,0,0,.28)}
  .cms-gal-empty{font-size:11px;color:rgba(41,38,27,.55)}
  .cms-gal-strip{display:flex;gap:4px;align-items:center;flex-wrap:wrap}
  .cms-gal-chip{width:30px;height:24px;border-radius:4px;background-size:cover;
    background-position:center;border:.5px solid rgba(0,0,0,.12)}
  .cms-gal-chip-vid{display:flex;align-items:center;justify-content:center;
    background:#29261b;color:#fff;font-size:9px}
  .cms-gal-more{font-size:10px;color:rgba(41,38,27,.5);font-weight:600}
  .cms-gal-manage{font-size:10.5px;font-weight:600;color:rgba(41,38,27,.6);flex-shrink:0}

  /* Gallery editor modal */
  .cms-gal-modal{position:fixed;inset:0;z-index:2147483647;display:flex;align-items:center;
    justify-content:center;padding:24px;background:rgba(20,17,13,.55);
    -webkit-backdrop-filter:blur(8px);backdrop-filter:blur(8px)}
  .cms-gal-panel{width:min(560px,100%);max-height:88vh;display:flex;flex-direction:column;
    background:rgba(250,249,247,.97);border:.5px solid rgba(255,255,255,.6);border-radius:14px;
    box-shadow:0 24px 70px rgba(0,0,0,.4);overflow:hidden;color:#29261b}
  .cms-gal-head{display:flex;align-items:center;gap:8px;padding:12px 14px;
    border-bottom:.5px solid rgba(0,0,0,.1)}
  .cms-gal-head b{font-size:13px}
  .cms-gal-hint{flex:1;font-size:10.5px;color:rgba(41,38,27,.45)}
  .cms-gal-body{padding:14px;overflow-y:auto}
  .cms-gal-blank{font-size:12px;color:rgba(41,38,27,.5);text-align:center;padding:24px 0}
  .cms-gal-grid{display:grid;grid-template-columns:repeat(auto-fill,minmax(120px,1fr));gap:10px}
  .cms-gal-card{display:flex;flex-direction:column;gap:5px;cursor:grab}
  .cms-gal-card.dragging{opacity:.4}
  .cms-gal-thumb{position:relative;aspect-ratio:3/2;border-radius:7px;background:#ddd9d2;
    background-size:cover;background-position:center;border:.5px solid rgba(0,0,0,.12)}
  .cms-gal-thumb-vid{background:#29261b}
  .cms-gal-vidbadge{position:absolute;left:0;bottom:0;right:0;display:flex;align-items:center;
    justify-content:center;gap:4px;padding:4px;background:rgba(20,17,13,.5);color:#fff;
    font-size:10px;font-weight:600;letter-spacing:.04em}
  .cms-gal-ord{position:absolute;left:5px;top:5px;width:18px;height:18px;border-radius:50%;
    background:rgba(20,17,13,.7);color:#fff;font-size:10px;font-weight:700;display:flex;
    align-items:center;justify-content:center}
  .cms-gal-x{position:absolute;right:5px;top:5px;width:20px;height:20px;border-radius:50%;
    border:0;background:rgba(20,17,13,.7);color:#fff;cursor:pointer;display:flex;
    align-items:center;justify-content:center;padding:0}
  .cms-gal-x:hover{background:rgb(180,40,25)}
  .cms-gal-cap{font-size:11px;padding:5px 7px}
  .cms-gal-foot{display:flex;align-items:center;justify-content:space-between;gap:10px;
    padding:12px 14px;border-top:.5px solid rgba(0,0,0,.1)}
  .cms-gal-count{font-size:11px;color:rgba(41,38,27,.5)}
  .cms-foot{display:flex;gap:6px;padding-top:4px}
  .cms-foot .twk-btn{flex:1}

  /* Commit / discard bar — sticky to the top of the scroll body */
  .cms-bar{position:sticky;top:0;z-index:3;margin:-2px -2px 2px;padding:8px;
    display:flex;align-items:center;gap:8px;border-radius:9px;flex-shrink:0;
    background:rgba(247,246,243,.96);-webkit-backdrop-filter:blur(8px);backdrop-filter:blur(8px);
    border:.5px solid rgba(0,0,0,.08)}
  .cms-status{flex:1;font-size:10.5px;font-weight:600;letter-spacing:.01em;
    display:flex;align-items:center;gap:6px;color:rgba(41,38,27,.5)}
  .cms-status.dirty{color:#9a6b00}
  .cms-status .dot{width:7px;height:7px;border-radius:50%;background:#34c759}
  .cms-status.dirty .dot{background:#e8a300}
  .cms-bar-btns{display:flex;gap:6px}
  .cms-pub{appearance:none;border:0;border-radius:7px;font:inherit;font-weight:600;
    font-size:11px;padding:5px 11px;cursor:pointer;background:#1c8a4a;color:#fff}
  .cms-pub:disabled{background:rgba(0,0,0,.1);color:rgba(41,38,27,.4);cursor:default}
  .cms-disc{appearance:none;border:.5px solid rgba(0,0,0,.12);border-radius:7px;font:inherit;
    font-weight:500;font-size:11px;padding:5px 10px;cursor:pointer;background:transparent;color:inherit}
  .cms-disc:disabled{opacity:.4;cursor:default}

  /* Login gate */
  .cms-login{display:flex;flex-direction:column;gap:9px;padding:4px 2px}
  .cms-login-h{font-size:13px;font-weight:600}
  .cms-login-err{font-size:10.5px;color:rgb(180,40,25);font-weight:500;margin-top:-3px}
  .cms-note{font-size:10px;line-height:1.4;color:rgba(41,38,27,.5);
    background:rgba(0,0,0,.04);border-radius:7px;padding:8px;margin-top:2px}
  .cms-note b{font-weight:600;color:rgba(41,38,27,.7)}
  .cms-whoami{display:flex;align-items:center;justify-content:space-between;
    font-size:10.5px;color:rgba(41,38,27,.5);padding-top:2px}
  .cms-link{appearance:none;border:0;background:transparent;color:rgba(41,38,27,.55);
    font:inherit;font-size:10.5px;text-decoration:underline;cursor:pointer;padding:0}
  .cms-link:hover{color:#29261b}

  .cms-swatches{display:flex;gap:6px;margin-bottom:3px}
  .cms-sw{width:22px;height:22px;border-radius:6px;border:1px solid rgba(0,0,0,.15);
    cursor:pointer;padding:0;appearance:none}
  .cms-sw.on{box-shadow:0 0 0 2px rgba(247,246,243,1),0 0 0 3.5px rgba(41,38,27,.65)}
  .cms-hexrow{display:flex;gap:6px;align-items:center}
  .cms-hexrow .twk-field{flex:1}
`;

const cmsClone = (x) => JSON.parse(JSON.stringify(x));

function blankFromItem(item) {
  if (item.type === "text") return "";
  const o = {};
  (item.fields || []).forEach((f) => { o[f.key] = f.type === "select" ? (f.options[0] || "") : ""; });
  return o;
}

// ── Multi-line text input ─────────────────────────────────────────
function CmsTextArea({ label, value, hint, onChange }) {
  return (
    <div className="twk-row">
      <div className="twk-lbl"><span>{label}</span></div>
      {hint && <div className="cms-hint">{hint}</div>}
      <textarea className="cms-textarea" rows={3} value={value ?? ""} onChange={(e) => onChange(e.target.value)} />
    </div>
  );
}

// ── Recursive field editor (value / onChange) ─────────────────────
// Driving everything off value+onChange (instead of mutation paths) lets lists
// nest arbitrarily — e.g. a team member's "facts" list living inside the team
// list. Each level just replaces its own slice and bubbles up.
function FieldEditor({ field, value, onChange }) {
  if (field.type === "gallery") {
    return <GalleryField label={field.label} hint={field.hint} value={Array.isArray(value) ? value : []} onChange={onChange} />;
  }
  if (field.type === "image" || field.type === "video") {
    const AssetField = window.AssetField;
    return <AssetField label={field.label} hint={field.hint} kind={field.type} value={value} onChange={onChange} />;
  }
  if (field.type === "textarea")
    return <CmsTextArea label={field.label} value={value} hint={field.hint} onChange={onChange} />;
  if (field.type === "select")
    return <TweakSelect label={field.label} value={value} options={field.options} onChange={onChange} />;
  if (field.type === "list")
    return <ListEditor field={field} list={Array.isArray(value) ? value : []} onChange={onChange} />;
  return (
    <>
      {field.hint && <div className="cms-hint" style={{ marginBottom: -4 }}>{field.hint}</div>}
      <TweakText label={field.label} value={value ?? ""} onChange={onChange} />
    </>
  );
}

// ── List editor (array of strings OR array of objects, any depth) ──
// item.layout === "row" lays paired scalar fields (e.g. Item / Value) inline.
function ListEditor({ field, list, onChange }) {
  const isText = field.item.type === "text";
  const row = field.item.layout === "row";
  // Collapsible accordion mode: established object items (projects, team) sit as
  // compact reorderable rows with a cover thumbnail, expanding only on demand —
  // keeps a long, rarely-edited list from dominating the panel's scroll height.
  const collapsible = !isText && !!field.item.collapsible;
  const thumbKey = field.item.thumbKey;
  const subtitleKey = field.item.subtitleKey;
  const singular = field.label.replace(/s$/, "");
  const [openIdx, setOpenIdx] = useAdminState(null);
  const setItem = (i, nv) => { const next = list.slice(); next[i] = nv; onChange(next); };
  const add = () => { onChange([...list, blankFromItem(field.item)]); if (collapsible) setOpenIdx(list.length); };
  const remove = (i) => { const next = list.slice(); next.splice(i, 1); onChange(next); setOpenIdx(null); };
  const move = (i, dir) => {
    const j = i + dir;
    if (j < 0 || j >= list.length) return;
    const next = list.slice();
    [next[i], next[j]] = [next[j], next[i]];
    onChange(next);
    setOpenIdx((o) => (o === i ? j : o === j ? i : o));
  };

  const renderBody = (item, i) => (
    isText ? (
      <input className="twk-field" value={item} onChange={(e) => setItem(i, e.target.value)} />
    ) : row ? (
      <div className="cms-rowfields">
        {field.item.fields.map((sub) => (
          <input key={sub.key} className="twk-field" placeholder={sub.label}
            value={item[sub.key] ?? ""} onChange={(e) => setItem(i, { ...item, [sub.key]: e.target.value })} />
        ))}
      </div>
    ) : (
      field.item.fields.map((sub) => (
        <FieldEditor key={sub.key} field={sub} value={item[sub.key]}
          onChange={(sv) => setItem(i, { ...item, [sub.key]: sv })} />
      ))
    )
  );

  return (
    <div className="cms-list">
      <div className="cms-list-h">{field.label}</div>
      {list.map((item, i) => {
        const expanded = !collapsible || openIdx === i;
        const title = isText ? `#${i + 1}` : (item[field.item.titleKey] || `Item ${i + 1}`);
        const thumbUrl = thumbKey && item[thumbKey] && (item[thumbKey].url || "").trim();
        const subtitle = subtitleKey ? item[subtitleKey] : "";
        return (
          <div className={"cms-card" + (collapsible ? " collapsible" : "") + (collapsible && !expanded ? " collapsed" : "")} key={i}>
            <div className="cms-card-h">
              {collapsible && thumbKey && (
                <span className="cms-card-thumb" style={{ backgroundImage: thumbUrl ? `url("${thumbUrl}")` : "none" }} aria-hidden="true">
                  {!thumbUrl && <span className="cms-card-thumb-x">×</span>}
                </span>
              )}
              {collapsible ? (
                <button type="button" className="cms-card-title" onClick={() => setOpenIdx((o) => (o === i ? null : i))} title={expanded ? "Collapse" : "Edit"}>
                  <span className="cms-card-title-main">{title}</span>
                  {subtitle ? <span className="cms-card-title-sub">{subtitle}</span> : null}
                </button>
              ) : (
                <span>{title}</span>
              )}
              <div className="cms-card-tools">
                {list.length > 1 && (
                  <>
                    <button className="cms-ico sm" title="Move up" disabled={i === 0} onClick={() => move(i, -1)}>{ICON.up}</button>
                    <button className="cms-ico sm" title="Move down" disabled={i === list.length - 1} onClick={() => move(i, 1)}>{ICON.down}</button>
                  </>
                )}
                {collapsible && (
                  <button className="cms-ico sm" title={expanded ? "Collapse" : "Edit"} onClick={() => setOpenIdx((o) => (o === i ? null : i))}>{expanded ? ICON.up : ICON.edit}</button>
                )}
                <button className="cms-mini" onClick={() => remove(i)}>Remove</button>
              </div>
            </div>
            {expanded && <div className="cms-card-body">{renderBody(item, i)}</div>}
          </div>
        );
      })}
      <button className="cms-add" onClick={add}>+ Add {singular.toLowerCase()}</button>
    </div>
  );
}

// ── Gallery field: compact summary + pop-open drag/caption editor ──
// A project's MIXED media reel — images and videos intermixed in any order.
// The compact control shows a thumbnail strip + count; "Manage" opens a modal
// with drag-to-reorder, inline captions, remove, and separate "+ Add image" /
// "+ Add video" pickers (each reuses the media library, staying open for multi-add).
// Item shape: { type: "image"|"video", url, caption }. Legacy items with no
// `type` are treated as images, so old galleries upgrade transparently.
const galKind = (g) => (g && g.type === "video" ? "video" : "image");

function GalleryField({ label, hint, value, onChange }) {
  const [open, setOpen] = useAdminState(false);
  const list = Array.isArray(value) ? value : [];
  const withUrl = list.filter((g) => g && (g.url || "").trim());
  return (
    <div className="twk-row">
      <div className="twk-lbl"><span>{label}</span><span className="cms-count">{withUrl.length || ""}</span></div>
      {hint && <div className="cms-hint">{hint}</div>}
      <div className="cms-gal-summary" onClick={() => setOpen(true)} role="button" tabIndex={0}
           onKeyDown={(e) => { if (e.key === "Enter" || e.key === " ") { e.preventDefault(); setOpen(true); } }}>
        {withUrl.length === 0 ? (
          <span className="cms-gal-empty">+ Add media</span>
        ) : (
          <>
            <div className="cms-gal-strip">
              {withUrl.slice(0, 5).map((g, i) => (
                galKind(g) === "video"
                  ? <span key={i} className="cms-gal-chip cms-gal-chip-vid" title="Video">▶</span>
                  : <span key={i} className="cms-gal-chip" style={{ backgroundImage: `url("${g.url}")` }} />
              ))}
              {withUrl.length > 5 && <span className="cms-gal-more">+{withUrl.length - 5}</span>}
            </div>
            <span className="cms-gal-manage">Manage</span>
          </>
        )}
      </div>
      {open && <GalleryEditor value={list} onChange={onChange} onClose={() => setOpen(false)} />}
    </div>
  );
}

function GalleryEditor({ value, onChange, onClose }) {
  const [picking, setPicking] = useAdminState(null); // null | "image" | "video"
  const [dragIdx, setDragIdx] = useAdminState(null);
  const list = Array.isArray(value) ? value : [];
  const counts = list.reduce((a, g) => { a[galKind(g)]++; return a; }, { image: 0, video: 0 });

  useAdminEffect(() => {
    const onKey = (e) => { if (e.key === "Escape" && !picking) onClose(); };
    window.addEventListener("keydown", onKey);
    const prev = document.body.style.overflow; document.body.style.overflow = "hidden";
    return () => { window.removeEventListener("keydown", onKey); document.body.style.overflow = prev; };
  }, [onClose, picking]);

  const setCaption = (i, cap) => { const n = list.slice(); n[i] = { ...n[i], caption: cap }; onChange(n); };
  const remove = (i) => { const n = list.slice(); n.splice(i, 1); onChange(n); };
  const move = (from, to) => {
    if (from === to || from == null || to == null) return;
    const n = list.slice(); const [it] = n.splice(from, 1); n.splice(to, 0, it); onChange(n);
  };
  const summary = [counts.image ? `${counts.image} image${counts.image === 1 ? "" : "s"}` : "", counts.video ? `${counts.video} video${counts.video === 1 ? "" : "s"}` : ""].filter(Boolean).join(" · ") || "Empty";

  return (
    <div className="cms-gal-modal" onClick={onClose}>
      <div className="cms-gal-panel" onClick={(e) => e.stopPropagation()}>
        <div className="cms-gal-head">
          <b>Media reel</b>
          <span className="cms-gal-hint">Images &amp; video · drag to reorder · click a caption to edit</span>
          <button className="cms-ico" title="Done" onClick={onClose}>{ICON.close}</button>
        </div>

        <div className="cms-gal-body">
          {list.length === 0 && <div className="cms-gal-blank">No media yet. Add images or video below — they play in this order.</div>}
          <div className="cms-gal-grid">
            {list.map((g, i) => {
              const isVid = galKind(g) === "video";
              return (
                <div key={i}
                  className={"cms-gal-card" + (dragIdx === i ? " dragging" : "")}
                  draggable
                  onDragStart={() => setDragIdx(i)}
                  onDragOver={(e) => { e.preventDefault(); if (dragIdx !== null && dragIdx !== i) { move(dragIdx, i); setDragIdx(i); } }}
                  onDragEnd={() => setDragIdx(null)}>
                  <div className={"cms-gal-thumb" + (isVid ? " cms-gal-thumb-vid" : "")} style={{ backgroundImage: (!isVid && g.url) ? `url("${g.url}")` : "none" }}>
                    <span className="cms-gal-ord">{i + 1}</span>
                    <button className="cms-gal-x" title="Remove" onClick={() => remove(i)}>{ICON.close}</button>
                    {isVid && <span className="cms-gal-vidbadge">▶ Video</span>}
                  </div>
                  <input className="twk-field cms-gal-cap" placeholder="Caption (optional)"
                    value={g.caption || ""} onChange={(e) => setCaption(i, e.target.value)} />
                </div>
              );
            })}
          </div>
        </div>

        <div className="cms-gal-foot">
          <div style={{ display: "flex", gap: 6 }}>
            <TweakButton label="+ Add image" onClick={() => setPicking("image")} />
            <TweakButton label="+ Add video" secondary onClick={() => setPicking("video")} />
          </div>
          <span className="cms-gal-count">{summary}</span>
        </div>
      </div>

      {picking && (
        <window.AssetManager assetType={picking}
          onSelect={(a) => onChange([...(Array.isArray(value) ? value : []), { type: picking, url: a.url, caption: "" }])}
          onClose={() => setPicking(null)} />
      )}
    </div>
  );
}

// ── Editor for one module (drives off CMS.schema(type)) ───────────
function ModuleEditor({ module, updateModule }) {
  const schema = window.CMS.schema(module.type);
  if (!schema.length) return <div className="cms-hint">No editable fields.</div>;
  return (
    <div className="cms-edit">
      {schema.map((field) => (
        <FieldEditor key={field.key} field={field} value={module.content[field.key]}
          onChange={(v) => updateModule(module.id, (c) => { c[field.key] = v; })} />
      ))}
    </div>
  );
}

// ── Monochrome icon set (stroke = currentColor, matches the row glyphs) ──
function SvgIcon({ children }) {
  return (
    <svg viewBox="0 0 24 24" width="13" height="13" fill="none" stroke="currentColor"
      strokeWidth="1.9" strokeLinecap="round" strokeLinejoin="round" style={{ display: "block" }}>{children}</svg>
  );
}
const ICON = {
  up:     <SvgIcon><path d="M18 15l-6-6-6 6" /></SvgIcon>,
  down:   <SvgIcon><path d="M6 9l6 6 6-6" /></SvgIcon>,
  eye:    <SvgIcon><path d="M1 12s4-7 11-7 11 7 11 7-4 7-11 7-11-7-11-7z" /><circle cx="12" cy="12" r="3" /></SvgIcon>,
  eyeOff: <SvgIcon><path d="M17.94 17.94A10.07 10.07 0 0 1 12 20C5 20 1 12 1 12a18.45 18.45 0 0 1 5.06-5.94M9.9 4.24A9.12 9.12 0 0 1 12 4c7 0 11 8 11 8a18.5 18.5 0 0 1-2.16 3.19" /><path d="M1 1l22 22" /></SvgIcon>,
  edit:   <SvgIcon><path d="M12 20h9" /><path d="M16.5 3.5a2.12 2.12 0 0 1 3 3L7 19l-4 1 1-4Z" /></SvgIcon>,
  close:  <SvgIcon><path d="M18 6L6 18M6 6l12 12" /></SvgIcon>,
};

// ── A module row in the manager ───────────────────────────────────
function ModuleRow({ module, actions, expanded, onExpand, canUp, canDown, reorderable, removable }) {
  const meta = window.CMS.meta(module.type);
  return (
    <div className={`cms-mod${module.visible ? "" : " off"}`}>
      <div className="cms-mod-top">
        {reorderable ? (
          <div className="cms-grip">
            <button className="cms-ico" title="Move up" disabled={!canUp} onClick={() => actions.moveModule(module.id, -1)}>{ICON.up}</button>
            <button className="cms-ico" title="Move down" disabled={!canDown} onClick={() => actions.moveModule(module.id, 1)}>{ICON.down}</button>
          </div>
        ) : (
          <span className="cms-grip-spacer" />
        )}
        <span className="cms-mod-name">{meta.label}</span>
        {!reorderable && <span className="cms-tag">{meta.region}</span>}
        <button className="cms-ico" title={module.visible ? "Hide" : "Show"} onClick={() => actions.toggleModule(module.id)}>{module.visible ? ICON.eye : ICON.eyeOff}</button>
        <button className="cms-ico" title="Edit content" onClick={onExpand}>{expanded ? ICON.up : ICON.edit}</button>
        {removable && (
          <button className="cms-ico" title="Delete module"
                  onClick={() => { if (confirm(`Delete the ${meta.label} module?`)) actions.removeModule(module.id); }}>{ICON.close}</button>
        )}
      </div>
      {expanded && <ModuleEditor module={module} updateModule={actions.updateModule} />}
    </div>
  );
}

// ── Login gate (front-end placeholder, NOT a security boundary) ───
function LoginGate({ onLogin, twoFactor }) {
  const [pw, setPw] = useAdminState("");
  const [code, setCode] = useAdminState("");
  const [err, setErr] = useAdminState("");
  const [busy, setBusy] = useAdminState(false);
  const submit = async () => {
    if (busy) return;
    setBusy(true); setErr("");
    const r = await onLogin(pw, code);
    setBusy(false);
    if (!r || !r.ok) { setErr((r && r.error) || "Incorrect password."); setPw(""); setCode(""); }
  };
  return (
    <div className="cms-login">
      <div className="cms-login-h">Admin sign-in</div>
      <input className="twk-field" type="password" placeholder="Password" value={pw} autoFocus
             onChange={(e) => { setPw(e.target.value); setErr(""); }}
             onKeyDown={(e) => { if (e.key === "Enter") submit(); }} />
      {twoFactor && (
        <input className="twk-field" type="text" inputMode="numeric" autoComplete="one-time-code"
               placeholder="6-digit code" value={code} maxLength={6}
               onChange={(e) => { setCode(e.target.value.replace(/\D/g, "").slice(0, 6)); setErr(""); }}
               onKeyDown={(e) => { if (e.key === "Enter") submit(); }} />
      )}
      {err && <div className="cms-login-err">{err}</div>}
      <TweakButton label={busy ? "Signing in…" : "Sign in"} onClick={submit} />
      {twoFactor ? (
        <div className="cms-note">
          Enter your password and the current 6-digit code from your authenticator app.
        </div>
      ) : (
        <div className="cms-note">
          <b>Demo gate only.</b> A browser check is not real security — anyone can bypass client-side
          JavaScript. Genuine authentication (password + 2FA) runs server-side once deployed.
        </div>
      )}
    </div>
  );
}

// ── Accent colour: brand swatches + full picker + hex field ───────
function CmsAccent({ value, onChange }) {
  const [hex, setHex] = useAdminState(value);
  React.useEffect(() => { setHex(value); }, [value]);
  const onText = (v) => {
    setHex(v);
    let s = v.trim();
    if (s && !s.startsWith("#")) s = "#" + s;
    if (/^#([0-9a-fA-F]{3}|[0-9a-fA-F]{6})$/.test(s)) onChange(s);
  };
  const safe = /^#[0-9a-fA-F]{6}$/.test(value) ? value : "#000000";
  return (
    <div className="twk-row">
      <div className="twk-lbl"><span>Accent colour</span></div>
      <div className="cms-hexrow">
        <input type="color" className="twk-swatch" value={safe} onChange={(e) => onChange(e.target.value)} />
        <input className="twk-field" value={hex} spellCheck={false} placeholder="#rrggbb" onChange={(e) => onText(e.target.value)} />
      </div>
    </div>
  );
}

// ── Authenticated editor body ─────────────────────────────────────
function AdminBody({ doc, published, dirty, saveState, actions, auth }) {
  const [expandedId, setExpandedId] = useAdminState(null);
  const [newType, setNewType] = useAdminState(window.CMS.addableTypes()[0]);
  const [perfOpen, setPerfOpen] = useAdminState(false);
  const [csOpen, setCsOpen] = useAdminState(false);
  const fileRef = useAdminRef(null);
  const toggleExpand = (id) => setExpandedId((cur) => (cur === id ? null : id));

  const META = window.CMS.meta.bind(window.CMS);
  const body = doc.modules.filter((m) => META(m.type).region === "body");
  const addable = window.CMS.addableTypes();
  const skinOpts = window.CMS.skinOptions();

  const exportDoc = () => {
    // Never let a backup silently capture nothing. If the published doc is gated
    // (visitor-facing stub) or empty, refuse — a “backup” of an empty site is a trap.
    if (!published || published.__gated || !Array.isArray(published.modules) || published.modules.length === 0) {
      alert("Nothing to export yet — the published content isn't loaded.\n\nMake sure you're signed in and your sections are showing, then try again.");
      return;
    }
    const blob = new Blob([JSON.stringify(published, null, 2)], { type: "application/json" });
    const url = URL.createObjectURL(blob);
    const a = document.createElement("a");
    a.href = url; a.download = "techready-content.json";
    document.body.appendChild(a); a.click(); a.remove();
    setTimeout(() => URL.revokeObjectURL(url), 1000);
  };

  const onImportFile = (e) => {
    const file = e.target.files && e.target.files[0];
    e.target.value = "";
    if (!file) return;
    const reader = new FileReader();
    reader.onload = () => {
      try {
        const parsed = JSON.parse(reader.result);
        if (!parsed || !Array.isArray(parsed.modules) || !parsed.theme) throw new Error("missing modules/theme");
        if (parsed.__gated) throw new Error("this file is a coming-soon stub, not real content");
        if (parsed.modules.length === 0) throw new Error("this file has no sections");
        actions.importDoc(parsed);
        alert(`Imported ${parsed.modules.length} section${parsed.modules.length === 1 ? "" : "s"} into the working draft. Review the page, then Commit to publish.`);
      } catch (err) {
        alert("Couldn't import that file — it isn't a valid TechReady content document.\n\n" + err.message);
      }
    };
    reader.readAsText(file);
  };

  const saving = saveState === "saving";
  const saveErr = saveState === "error";
  const statusText = saving ? "Publishing…" : saveErr ? "Couldn’t publish — retry" : dirty ? "Unpublished changes" : "Published — up to date";
  return (
    <>
      <div className="cms-bar">
        <span className={(dirty || saveErr) ? "cms-status dirty" : "cms-status"}>
          <span className="dot" />{statusText}
        </span>
        <div className="cms-bar-btns">
          <button className="cms-disc" disabled={!dirty || saving} onClick={actions.cancel}>Discard</button>
          <button className="cms-pub" disabled={(!dirty && !saveErr) || saving} onClick={actions.commit}>{saving ? "…" : "Commit"}</button>
        </div>
      </div>

      <TweakSection label="Theme Options">
        <div className="cms-compact">
        <TweakSelect label="Skin" value={doc.theme.skin} options={skinOpts}
          onChange={(v) => actions.setTheme("skin", v)} />
        <TweakSelect label="Headline font" value={doc.theme.headlineFont} options={window.HEAD_FONTS}
          onChange={(v) => actions.setTheme("headlineFont", v)} />
        <TweakSelect label="Headline weight" value={doc.theme.headlineWeight}
          options={[
            { label: "Thin · 100", value: 100 },
            { label: "Extralight · 200", value: 200 },
            { label: "Light · 300", value: 300 },
            { label: "Regular · 400", value: 400 },
            { label: "Medium · 500", value: 500 },
            { label: "Semibold · 600", value: 600 },
            { label: "Bold · 700", value: 700 },
            { label: "Extrabold · 800", value: 800 },
            { label: "Black · 900", value: 900 },
          ]}
          onChange={(v) => actions.setTheme("headlineWeight", Number(v))} />
        <TweakSelect label="Body font" value={doc.theme.bodyFont} options={window.BODY_FONTS}
          onChange={(v) => actions.setTheme("bodyFont", v)} />
        </div>
      </TweakSection>

      <TweakSection label="Page sections">
        <div className="cms-group">
          {doc.modules.map((m) => {
            const isBody = META(m.type).region === "body";
            const bIdx = isBody ? body.findIndex((x) => x.id === m.id) : -1;
            return (
              <ModuleRow key={m.id} module={m} actions={actions}
                expanded={expandedId === m.id} onExpand={() => toggleExpand(m.id)}
                reorderable={isBody} removable={isBody}
                canUp={isBody && bIdx > 0} canDown={isBody && bIdx < body.length - 1} />
            );
          })}
        </div>
        <div className="cms-newbar">
          <select className="twk-field" value={newType} onChange={(e) => setNewType(e.target.value)}>
            {addable.map((t) => <option key={t} value={t}>{META(t).label}</option>)}
          </select>
          <TweakButton label="+ Add" secondary onClick={() => actions.addModule(newType)} />
        </div>
      </TweakSection>

      <TweakSection label="Site visibility">
        {(() => { const cs = doc.comingSoon || {}; const remote = window.ContentBackend && window.ContentBackend.isRemote && window.ContentBackend.isRemote(); return (
          <>
            <div className="cms-compact">
              <TweakToggle label="Maintenance Mode" value={!!cs.enabled}
                onChange={(v) => actions.setComingSoon("enabled", v)} />
            </div>
            <div className="cms-hint">When on, visitors see only the splash. {remote ? "The server withholds the real site from anyone not signed in." : "You (signed in) still see the full site."} Remember to <b>Commit</b> to apply.</div>
            <button type="button" className="cms-disclosure" aria-expanded={csOpen} onClick={() => setCsOpen((o) => !o)}>
              <span className={"cms-disc-caret" + (csOpen ? " open" : "")}>{ICON.down}</span>
              <span>Splash content</span>
            </button>
            {csOpen && (
              <div className="cms-disc-body">
                <CmsTextArea label="Splash heading" value={cs.title} onChange={(v) => actions.setComingSoon("title", v)} />
                <CmsTextArea label="Splash message" value={cs.message} onChange={(v) => actions.setComingSoon("message", v)} />
                <TweakText label="Contact email (optional)" value={cs.email || ""} onChange={(v) => actions.setComingSoon("email", v)} />
                <TweakText label="Contact phone (optional)" value={cs.phone || ""} onChange={(v) => actions.setComingSoon("phone", v)} />
                <div className="cms-hint">Shown on the splash as Call / Email buttons. Both are obfuscated against scrapers.</div>
              </div>
            )}
          </>
        ); })()}
      </TweakSection>

      <TweakSection label="Backup & restore">
        <div className="cms-foot">
          <TweakButton label="Export JSON" secondary onClick={exportDoc} />
          <TweakButton label="Import JSON" secondary onClick={() => fileRef.current && fileRef.current.click()} />
        </div>
        <input ref={fileRef} type="file" accept="application/json,.json" style={{ display: "none" }} onChange={onImportFile} />
        <div className="cms-hint">Export saves the <b>published</b> document. Import loads into the working draft for review before you commit.</div>
        <div className="cms-foot">
          <TweakButton label="Reset to defaults" secondary
            onClick={() => {
              const ok = prompt("This REPLACES all published and draft content with the sample defaults — it cannot be undone, and it publishes immediately.\n\nExport a backup first if you might want this content back.\n\nType  RESET  to confirm:");
              if (ok !== null && ok.trim().toUpperCase() === "RESET") actions.resetAll();
              else if (ok !== null) alert("Reset cancelled — you didn't type RESET.");
            }} />
        </div>
      </TweakSection>

      <div className="cms-whoami">
        <span>Signed in</span>
        <button className="cms-link" onClick={auth.logout}>Sign out</button>
      </div>
    </>
  );
}

// ── Panel wrapper ─────────────────────────────────────────────────
function CmsAdmin({ doc, published, dirty, saveState, authed, actions, auth }) {
  return (
    <TweaksPanel title="TechReady CMS" dock="right" resizable
                 defaultWidth={380} minWidth={300} maxWidth={640} storageKey="fg_admin_width">
      <style>{__CMS_STYLE}</style>
      {authed
        ? <AdminBody doc={doc} published={published} dirty={dirty} saveState={saveState} actions={actions} auth={auth} />
        : <LoginGate onLogin={auth.login} twoFactor={auth.twoFactor} />}
    </TweaksPanel>
  );
}

Object.assign(window, { CmsAdmin });
