/* ============================================================
   admin.jsx — Developer Console: login + visual page editor
   Exposes window.Admin
   ============================================================ */
const { useState: aS, useEffect: aE, useRef: aR, useMemo: aM } = React;

const DEV_PASSWORD = "konform-docs";   // change me — see the login hint (offline mode only)

/* ---------- block helpers ---------- */
const BLOCK_TYPES = [
  { type: "h2", label: "Heading", icon: "hash" },
  { type: "h3", label: "Sub-heading", icon: "hash" },
  { type: "p", label: "Paragraph", icon: "doc" },
  { type: "ul", label: "Bullet list", icon: "book" },
  { type: "ol", label: "Numbered list", icon: "book" },
  { type: "code", label: "Code block", icon: "copy" },
  { type: "callout", label: "Callout", icon: "info" },
  { type: "tabs", label: "Tabs", icon: "copy" },
  { type: "steps", label: "Steps", icon: "layers" },
  { type: "props", label: "Table", icon: "hud" },
  { type: "placeholder", label: "Image slot", icon: "image" },
  { type: "embed", label: "Video / embed", icon: "camera" },
  { type: "hr", label: "Divider", icon: "chevron" },
];

function blockDefault(t) {
  switch (t) {
    case "h2": return { type: "h2", text: "New section" };
    case "h3": return { type: "h3", text: "New sub-section" };
    case "p": return { type: "p", text: "Write something here." };
    case "ul": return { type: "ul", items: ["First item", "Second item"] };
    case "ol": return { type: "ol", items: ["First step", "Second step"] };
    case "code": return { type: "code", file: "declaration.json", lang: "json", code: "{\n  \"directive\": \"2006/42/EC\",\n  \"product\": \"\",\n  \"standards\": []\n}", highlight: [] };
    case "callout": return { type: "callout", variant: "note", title: "Note", text: "Something worth highlighting." };
    case "tabs": return { type: "tabs", tabs: [{ label: "Tab one", text: "First tab content." }, { label: "Tab two", text: "Second tab content." }] };
    case "steps": return { type: "steps", steps: [{ title: "Step one", text: "Do this first." }] };
    case "props": return { type: "props", cols: ["Field", "Type", "Notes"], rows: [{ name: "field", type: "string", desc: "Description." }] };
    case "placeholder": return { type: "placeholder", label: "diagram: example.png", caption: "Describe what goes here." };
    case "embed": return { type: "embed", url: "", caption: "" };
    case "hr": return { type: "hr" };
    default: return { type: "p", text: "" };
  }
}

const ICON_OPTIONS = ["doc", "book", "run", "gamepad", "camera", "anim", "sword", "heart", "target", "user", "network", "save", "hud", "bag", "cube", "skull", "rocket", "layers", "bolt", "flame"];

/* Read an image file, downscale it, and return a data URL (keeps localStorage small). */
function fileToDataURL(file, maxW = 1400) {
  return new Promise((resolve, reject) => {
    const reader = new FileReader();
    reader.onerror = reject;
    reader.onload = () => {
      const img = new Image();
      img.onload = () => {
        let { width, height } = img;
        if (width > maxW) { height = Math.round(height * maxW / width); width = maxW; }
        const c = document.createElement("canvas");
        c.width = width; c.height = height;
        c.getContext("2d").drawImage(img, 0, 0, width, height);
        const isPng = file.type === "image/png";
        let url = c.toDataURL(isPng ? "image/png" : "image/jpeg", 0.85);
        if (isPng && url.length > 1_400_000) url = c.toDataURL("image/jpeg", 0.85); // png too big → jpeg
        resolve(url);
      };
      img.onerror = reject;
      img.src = reader.result;
    };
    reader.readAsDataURL(file);
  });
}

/* Returns a usable image src: an Azure Blob Storage URL in cloud mode, else a downscaled data URL. */
async function processImage(file) {
  const dataUrl = await fileToDataURL(file);
  if (!window.CLOUD) return dataUrl;
  const blob = await (await fetch(dataUrl)).blob();
  const ext = blob.type === "image/png" ? "png" : "jpg";
  return await window.uploadImageRemote(blob, ext);
}

function todayISO() {
  const d = new Date(); const p = (n) => String(n).padStart(2, "0");
  return `${d.getFullYear()}-${p(d.getMonth() + 1)}-${p(d.getDate())}`;
}

/* Reusable cover-image uploader (same engine as the image block). */
function CoverField({ src, onChange }) {
  const onFile = async (f) => { if (!f) return; try { onChange(await processImage(f)); } catch (x) { alert("Upload failed: " + (x.message || x)); } };
  return (
    <div className="field">
      <label>Cover image</label>
      {src ? (
        <div className="img-preview">
          <img src={src} alt="" />
          <div className="img-preview-actions">
            <label className="con-topbtn" style={{ cursor: "pointer" }}>
              <I.image /> Replace
              <input type="file" accept="image/*" hidden onChange={async (e) => { await onFile(e.target.files[0]); e.target.value = ""; }} />
            </label>
            <button className="con-topbtn danger" onClick={() => onChange(undefined)}><I.x /> Remove</button>
          </div>
        </div>
      ) : (
        <label className="upload-drop"
          onDragOver={(e) => { e.preventDefault(); e.currentTarget.classList.add("drag"); }}
          onDragLeave={(e) => e.currentTarget.classList.remove("drag")}
          onDrop={async (e) => { e.preventDefault(); e.currentTarget.classList.remove("drag"); const f = e.dataTransfer.files[0]; if (f && f.type.startsWith("image/")) await onFile(f); }}>
          <I.image />
          <span>Click to upload — or drop a cover image</span>
          <input type="file" accept="image/*" hidden onChange={async (e) => { await onFile(e.target.files[0]); e.target.value = ""; }} />
        </label>
      )}
      <div className="field-hint">Optional. Shown at the top of the entry and on the feed. {window.CLOUD ? "Uploaded to Azure Blob Storage." : "Stored in this browser."}</div>
    </div>
  );
}

/* =================== Login =================== */
function Login({ onAuth, onExit, theme, setTheme }) {
  const cloud = window.CLOUD;
  const [pw, setPw] = aS("");
  const [err, setErr] = aS("");
  const [busy, setBusy] = aS(false);
  const ref = aR(null);
  aE(() => { ref.current?.focus(); }, []);
  const submit = async (e) => {
    e.preventDefault();
    if (cloud) {
      setBusy(true); setErr("");
      const res = await window.adminLogin(pw);
      setBusy(false);
      if (!res.ok) { setErr(res.error || "Sign-in failed."); setPw(""); return; }
      onAuth();
    } else {
      const expected = (window.getSetting && window.getSetting("localPassword")) || DEV_PASSWORD;
      if (pw === expected) onAuth();
      else { setErr("Incorrect password."); setPw(""); }
    }
  };
  return (
    <div className="login-screen">
      <div className="login-bg" />
      <div className="login-glow" />
      <a className="login-back" href="#/" onClick={(e) => { e.preventDefault(); onExit(); }}>
        <I.chevron style={{ width: 15, height: 15, transform: "rotate(90deg)" }} /> Back to site
      </a>
      <form className="login-card" onSubmit={submit}>
        <div className="login-mark"><I.check /></div>
        <h1>Developer Console</h1>
        <p className="login-sub">Sign in to create and edit documentation.</p>
        <label className="login-label" htmlFor="pw">Password</label>
        <input id="pw" ref={ref} type="password" className="login-input" value={pw}
               onChange={(e) => { setPw(e.target.value); setErr(""); }} placeholder="••••••••" autoComplete="current-password" />
        {err && <p className="login-error"><I.warn style={{ width: 14, height: 14 }} /> {err}</p>}
        <button type="submit" className="login-btn" disabled={busy}>{busy ? "Signing in…" : <>Unlock console <I.arrowR /></>}</button>
        <p className="login-hint">
          {cloud
            ? <>Enter the console password — the <code>ADMIN_PASSWORD</code> app setting on your Static Web App.</>
            : <>Default password is <code>{DEV_PASSWORD}</code>. Change it in <code>admin.jsx</code> (the <code>DEV_PASSWORD</code> constant). This is a front-end gate for a local prototype, not real security.</>}
        </p>
      </form>
    </div>
  );
}

/* =================== Field primitives =================== */
/* IDE-style tab handling for a textarea value. Uses 4 spaces.
   Returns the new value + selection so the caret stays put. */
function tabIndent({ value, selStart, selEnd, shift }) {
  const TAB = "    ";
  const v = value ?? "";
  if (selStart === selEnd) {
    if (!shift) {
      const nv = v.slice(0, selStart) + TAB + v.slice(selStart);
      const c = selStart + TAB.length;
      return { value: nv, selStart: c, selEnd: c };
    }
    // Shift+Tab with no selection: dedent the current line
    const lineStart = v.lastIndexOf("\n", selStart - 1) + 1;
    const m = v.slice(lineStart).match(/^(\t| {1,4})/);
    if (!m) return { value: v, selStart, selEnd };
    const cut = m[0].length;
    const nv = v.slice(0, lineStart) + v.slice(lineStart + cut);
    const c = Math.max(lineStart, selStart - cut);
    return { value: nv, selStart: c, selEnd: c };
  }
  // Selection spanning one or more lines: indent/outdent each full line
  const lineStart = v.lastIndexOf("\n", selStart - 1) + 1;
  const block = v.slice(lineStart, selEnd);
  const lines = block.split("\n");
  let firstDelta = 0, totalDelta = 0;
  const out = lines.map((ln, i) => {
    if (shift) {
      const m = ln.match(/^(\t| {1,4})/);
      if (!m) return ln;
      const cut = m[0].length;
      if (i === 0) firstDelta -= cut;
      totalDelta -= cut;
      return ln.slice(cut);
    }
    if (i === 0) firstDelta += TAB.length;
    totalDelta += TAB.length;
    return TAB + ln;
  });
  const nv = v.slice(0, lineStart) + out.join("\n") + v.slice(selEnd);
  return { value: nv, selStart: Math.max(lineStart, selStart + firstDelta), selEnd: selEnd + totalDelta };
}

/* Shared markdown-insert hook: wraps the current selection (or inserts a stub)
   with link / bold / code syntax, then restores a sensible selection after
   React re-commits the controlled value (via the pending-selection effect). */
function useMarkdownInsert(value, onChange) {
  const ref = aR(null);
  const pending = aR(null);
  aE(() => {
    if (pending.current && ref.current) {
      const { a, b } = pending.current; pending.current = null;
      try { ref.current.focus(); ref.current.selectionStart = a; ref.current.selectionEnd = b; } catch (_) {}
    }
  });
  const apply = (kind) => {
    const el = ref.current; const v = value ?? "";
    const start = el ? el.selectionStart : v.length;
    const end = el ? el.selectionEnd : v.length;
    const sel = v.slice(start, end);
    let snippet, a, b;
    if (kind === "link") { const t = sel || "link text"; snippet = "[" + t + "](https://)"; if (sel) { a = start + t.length + 3; b = a + "https://".length; } else { a = start + 1; b = start + 1 + t.length; } }
    else if (kind === "bold") { const t = sel || "bold text"; snippet = "**" + t + "**"; a = start + 2; b = start + 2 + t.length; }
    else { const t = sel || "code"; snippet = "`" + t + "`"; a = start + 1; b = start + 1 + t.length; }
    pending.current = { a, b };
    onChange(v.slice(0, start) + snippet + v.slice(end));
  };
  return { ref, apply };
}

const FMT_BTNS = [
  { k: "link", node: <><I.link style={{ width: 13, height: 13 }} /> Link</>, title: "Insert a link" },
  { k: "bold", node: <span style={{ fontWeight: 800 }}>B</span>, title: "Bold" },
  { k: "code", node: <span style={{ fontFamily: "var(--font-mono)", fontSize: 11 }}>&lt;/&gt;</span>, title: "Inline code" },
];
function FmtToolbar({ apply, only }) {
  const btns = only ? FMT_BTNS.filter((b) => only.includes(b.k)) : FMT_BTNS;
  return (
    <div className="fmt-toolbar">
      {btns.map((b) => (
        <button key={b.k} type="button" className="field-tool" title={b.title}
          onMouseDown={(e) => e.preventDefault()} onClick={() => apply(b.k)}>{b.node}</button>
      ))}
    </div>
  );
}

function Text({ label, value, onChange, hint, mono, area, placeholder, disabled, rows, tab, format }) {
  const { ref, apply } = useMarkdownInsert(value, onChange);
  const common = { ref, className: (area ? "ta" : "inp") + (mono ? " mono" : ""), value: value ?? "", placeholder, disabled,
    onChange: (e) => onChange(e.target.value) };
  if (area && tab) {
    common.spellCheck = false;
    common.onKeyDown = (e) => {
      if (e.key !== "Tab") return;
      e.preventDefault();
      const ta = e.target;
      const r = tabIndent({ value: ta.value, selStart: ta.selectionStart, selEnd: ta.selectionEnd, shift: e.shiftKey });
      onChange(r.value);
      requestAnimationFrame(() => { try { ta.selectionStart = r.selStart; ta.selectionEnd = r.selEnd; } catch (_) {} });
    };
  }
  return (
    <div className="field">
      {(label || format) && (
        <div className="field-labelrow">
          {label ? <label>{label}</label> : <span />}
          {format && <FmtToolbar apply={apply} only={Array.isArray(format) ? format : null} />}
        </div>
      )}
      {area ? <textarea {...common} rows={rows || 3} /> : <input {...common} />}
      {hint && <div className="field-hint">{hint}</div>}
    </div>
  );
}

/* Compact markdown field for use inside MiniList rows (steps, tabs). */
function FmtArea({ value, onChange, rows, placeholder, only }) {
  const { ref, apply } = useMarkdownInsert(value, onChange);
  return (
    <div className="fmt-field">
      <FmtToolbar apply={apply} only={only} />
      <textarea ref={ref} className="ta" rows={rows || 2} value={value ?? ""} placeholder={placeholder} onChange={(e) => onChange(e.target.value)} />
    </div>
  );
}

function Toggle({ label, on, onChange }) {
  return (
    <div className="field">
      <div className="switch-row">
        <button type="button" className={"switch" + (on ? " on" : "")} onClick={() => onChange(!on)} aria-pressed={on} />
        <label style={{ margin: 0 }}>{label}</label>
      </div>
    </div>
  );
}

/* =================== Block editors =================== */
function MiniList({ items, render, onChange, addLabel, makeNew }) {
  const set = (i, v) => { const a = [...items]; a[i] = v; onChange(a); };
  const del = (i) => onChange(items.filter((_, k) => k !== i));
  return (
    <>
      {items.map((it, i) => (
        <div className="mini-row" key={i}>
          <div className="mini-fields">{render(it, (v) => set(i, v))}</div>
          <button className="mini-del" onClick={() => del(i)} title="Remove"><I.x /></button>
        </div>
      ))}
      <button className="mini-add" onClick={() => onChange([...items, makeNew()])}><I.menu style={{ width: 13, height: 13 }} /> {addLabel}</button>
    </>
  );
}

function BlockBody({ b, onChange }) {
  const up = (patch) => onChange({ ...b, ...patch });
  switch (b.type) {
    case "h2": case "h3":
      return <Text label="Heading text" value={b.text} onChange={(v) => up({ text: v })} />;
    case "p":
      return <Text label="Text" area rows={3} format value={b.text} onChange={(v) => up({ text: v })} hint="Markdown-ish: **bold**, `code`, [link](#/docs/stats)" />;
    case "ul": case "ol":
      return <Text label="Items (one per line)" area rows={4} format value={(b.items || []).join("\n")} onChange={(v) => up({ items: v.split("\n") })} hint="Each line becomes a list item. Markdown-ish supported." />;
    case "code":
      return (
        <>
          <div className="field-row">
            <Text label="File name" value={b.file} onChange={(v) => up({ file: v })} placeholder="declaration.json" />
            <Text label="Highlight lines" value={b.highlightText !== undefined ? b.highlightText : (b.highlight || []).join(", ")} onChange={(v) => up({ highlightText: v, highlight: v.split(",").map((x) => parseInt(x.trim(), 10)).filter((n) => !isNaN(n)) })} placeholder="4, 5, 9" hint="Comma-separated line numbers" />
          </div>
          <Text label="Code" area mono rows={8} value={b.code} onChange={(v) => up({ code: v })} tab />
        </>
      );
    case "callout":
      return (
        <>
          <div className="field">
            <label>Style</label>
            <select className="sel" value={b.variant} onChange={(e) => up({ variant: e.target.value })}>
              <option value="note">Note (blue)</option>
              <option value="tip">Tip (green)</option>
              <option value="warn">Warning (red)</option>
              <option value="gold">Highlight (gold)</option>
            </select>
          </div>
          <Text label="Title" value={b.title} onChange={(v) => up({ title: v })} />
          <Text label="Text" area rows={2} format value={b.text} onChange={(v) => up({ text: v })} hint="Markdown-ish supported" />
        </>
      );
    case "tabs":
      return (
        <div className="field">
          <label>Tabs</label>
          <MiniList items={b.tabs || []} addLabel="Add tab" makeNew={() => ({ label: "New tab", text: "" })}
            onChange={(tabs) => up({ tabs })}
            render={(t, set) => { const isCode = t.code !== undefined; return (<>
              <div className="field-row" style={{ gridTemplateColumns: "1fr auto", gap: 8, alignItems: "center" }}>
                <input className="inp" value={t.label || ""} placeholder="Tab label" onChange={(e) => set({ ...t, label: e.target.value })} />
                <select className="sel" style={{ width: "auto" }} value={isCode ? "code" : "text"}
                  onChange={(e) => { if (e.target.value === "code") set({ label: t.label, code: t.code || "", file: t.file || "", lang: "csharp" }); else set({ label: t.label, text: t.text || "" }); }}>
                  <option value="text">Text</option>
                  <option value="code">Code</option>
                </select>
              </div>
              {isCode
                ? <><input className="inp" value={t.file || ""} placeholder="File name (optional)" onChange={(e) => set({ ...t, file: e.target.value })} /><textarea className="ta mono" rows={5} value={t.code} placeholder="Code" spellCheck={false} onChange={(e) => set({ ...t, code: e.target.value })} /></>
                : <FmtArea value={t.text} rows={3} placeholder="Tab content — markdown-ish" onChange={(v) => set({ ...t, text: v })} />}
            </>); }} />
        </div>
      );
    case "steps":
      return (
        <div className="field">
          <label>Steps</label>
          <MiniList items={b.steps || []} addLabel="Add step" makeNew={() => ({ title: "New step", text: "" })}
            onChange={(steps) => up({ steps })}
            render={(s, set) => (<>
              <input className="inp" value={s.title} placeholder="Step title" onChange={(e) => set({ ...s, title: e.target.value })} />
              <FmtArea value={s.text} rows={2} placeholder="Step description" onChange={(v) => set({ ...s, text: v })} />
            </>)} />
        </div>
      );
    case "props":
      return (
        <>
          <div className="field">
            <label>Column headers</label>
            <div className="field-row-3">
              {[0, 1, 2].map((i) => (
                <input key={i} className="inp" value={(b.cols || ["", "", ""])[i] || ""} onChange={(e) => { const cols = [...(b.cols || ["", "", ""])]; cols[i] = e.target.value; up({ cols }); }} />
              ))}
            </div>
          </div>
          <div className="field">
            <label>Rows</label>
            <MiniList items={b.rows || []} addLabel="Add row" makeNew={() => ({ name: "", type: "", desc: "" })}
              onChange={(rows) => up({ rows })}
              render={(r, set) => (<div className="field-row-3" style={{ gap: 6 }}>
                <input className="inp" value={r.name} placeholder="name" onChange={(e) => set({ ...r, name: e.target.value })} />
                <input className="inp" value={r.type} placeholder="type" onChange={(e) => set({ ...r, type: e.target.value })} />
                <input className="inp" value={r.desc} placeholder="description" onChange={(e) => set({ ...r, desc: e.target.value })} />
              </div>)} />
          </div>
        </>
      );
    case "placeholder":
      return (
        <>
          <div className="field">
            <label>Image</label>
            {b.src ? (
              <div className="img-preview">
                <img src={b.src} alt="" />
                <div className="img-preview-actions">
                  <label className="con-topbtn" style={{ cursor: "pointer" }}>
                    <I.image /> Replace
                    <input type="file" accept="image/*" hidden onChange={async (e) => { const f = e.target.files[0]; if (f) { try { up({ src: await processImage(f) }); } catch (x) { alert("Upload failed: " + (x.message || x)); } } e.target.value = ""; }} />
                  </label>
                  <button className="con-topbtn danger" onClick={() => up({ src: undefined })}><I.x /> Remove</button>
                </div>
              </div>
            ) : (
              <label className="upload-drop"
                onDragOver={(e) => { e.preventDefault(); e.currentTarget.classList.add("drag"); }}
                onDragLeave={(e) => e.currentTarget.classList.remove("drag")}
                onDrop={async (e) => { e.preventDefault(); e.currentTarget.classList.remove("drag"); const f = e.dataTransfer.files[0]; if (f && f.type.startsWith("image/")) { try { up({ src: await processImage(f) }); } catch (x) { alert("Upload failed: " + (x.message || x)); } } }}>
                <I.image />
                <span>Click to upload — or drop an image here</span>
                <input type="file" accept="image/*" hidden onChange={async (e) => { const f = e.target.files[0]; if (f) { try { up({ src: await processImage(f) }); } catch (x) { alert("Upload failed: " + (x.message || x)); } } e.target.value = ""; }} />
              </label>
            )}
            <div className="field-hint">{window.CLOUD ? "Uploaded to your Azure Blob Storage container." : "Stored in this browser. Large images are auto-downscaled to ~1400px."}</div>
          </div>
          <Text label={b.src ? "Caption" : "Placeholder label (shown until you add an image)"} value={b.src ? b.caption : b.label} onChange={(v) => up(b.src ? { caption: v } : { label: v })} placeholder={b.src ? "Describe the image" : "diagram: conformity-process.png"} />
          {b.src && <Text label="Placeholder label (fallback)" value={b.label} onChange={(v) => up({ label: v })} placeholder="diagram: conformity-process.png" />}
          {!b.src && <Text label="Caption" value={b.caption} onChange={(v) => up({ caption: v })} />}
        </>
      );
    case "hr":
      return <div className="field-hint" style={{ margin: 0 }}>A horizontal divider — nothing to configure.</div>;
    case "embed":
      return (
        <>
          <Text label="Video URL" value={b.url} onChange={(v) => up({ url: v })} placeholder="https://youtube.com/watch?v=… or https://youtu.be/…" hint="YouTube & Vimeo links auto-convert to a player. Any other embeddable URL is used as-is." />
          <Text label="Caption (optional)" value={b.caption} onChange={(v) => up({ caption: v })} />
        </>
      );
    default:
      return null;
  }
}

function BlockCard({ b, idx, total, onChange, onMove, onDelete, onDuplicate, dragging, over, onDragStart, onDragOver, onDrop, onDragEnd }) {
  const meta = BLOCK_TYPES.find((t) => t.type === b.type);
  return (
    <div className={"block-card" + (dragging ? " dragging" : "") + (over ? " drag-over" : "")}
      data-block-idx={idx}
      onDragOver={(e) => { e.preventDefault(); e.dataTransfer.dropEffect = "move"; onDragOver(); }}
      onDrop={(e) => { e.preventDefault(); onDrop(); }}>
      <div className="block-head">
        <span className="block-grip" title="Drag to reorder" draggable
          onDragStart={(e) => { e.dataTransfer.effectAllowed = "move"; try { e.dataTransfer.setData("text/plain", String(idx)); } catch (_) {} onDragStart(); }}
          onDragEnd={onDragEnd}><I.grip style={{ width: 16, height: 16 }} /></span>
        <span className="block-type">{meta ? meta.label : b.type}</span>
        <span className="bh-spacer" />
        <button className="block-ctrl" disabled={idx === 0} onClick={() => onMove(-1)} title="Move up"><I.chevron style={{ transform: "rotate(180deg)" }} /></button>
        <button className="block-ctrl" disabled={idx === total - 1} onClick={() => onMove(1)} title="Move down"><I.chevron /></button>
        <button className="block-ctrl" onClick={onDuplicate} title="Duplicate"><I.copy /></button>
        <button className="block-ctrl del" onClick={onDelete} title="Delete"><I.x /></button>
      </div>
      <div className="block-body"><BlockBody b={b} onChange={onChange} /></div>
    </div>
  );
}

/* Subtle gutter control that inserts a new block at a given gap. */
function InsertGutter({ open, onToggle, onPick }) {
  return (
    <div className={"insert-gutter" + (open ? " open" : "")}>
      <button className="insert-gutter-btn" title="Insert block here"
        onClick={(e) => { e.stopPropagation(); onToggle(); }}><I.plus style={{ width: 14, height: 14 }} /></button>
      {open && (
        <div className="insert-menu" onMouseLeave={onToggle}>
          {BLOCK_TYPES.map((t) => { const Ic = I[t.icon] || I.doc; return (
            <button key={t.type} onClick={() => onPick(t.type)}><Ic /> {t.label}</button>
          ); })}
        </div>
      )}
    </div>
  );
}

function BlocksEditor({ blocks, onChange }) {
  const [menu, setMenu] = aS(false);
  const [insertGap, setInsertGap] = aS(null);
  const [dragIdx, setDragIdx] = aS(null);
  const [overIdx, setOverIdx] = aS(null);
  const set = (i, nb) => { const a = [...blocks]; a[i] = nb; onChange(a); };
  const move = (i, dir) => { const j = i + dir; if (j < 0 || j >= blocks.length) return; const a = [...blocks]; [a[i], a[j]] = [a[j], a[i]]; onChange(a); };
  const del = (i) => onChange(blocks.filter((_, k) => k !== i));
  const dup = (i) => { const a = [...blocks]; a.splice(i + 1, 0, cloneData(blocks[i])); onChange(a); };
  const insertAt = (i, t) => { const a = [...blocks]; a.splice(i, 0, blockDefault(t)); onChange(a); setInsertGap(null); };
  const add = (t) => { onChange([...blocks, blockDefault(t)]); setMenu(false); };
  const dropOn = (to) => {
    const from = dragIdx;
    setDragIdx(null); setOverIdx(null);
    if (from == null || from === to) return;
    const a = [...blocks];
    const [m] = a.splice(from, 1);
    a.splice(to, 0, m);
    onChange(a);
  };
  return (
    <div className="blocks-editor">
      {blocks.map((b, i) => (
        <div className="block-slot" key={i}>
          <InsertGutter open={insertGap === i} onToggle={() => setInsertGap((g) => (g === i ? null : i))} onPick={(t) => insertAt(i, t)} />
          <BlockCard b={b} idx={i} total={blocks.length}
            onChange={(nb) => set(i, nb)} onMove={(d) => move(i, d)} onDelete={() => del(i)} onDuplicate={() => dup(i)}
            dragging={dragIdx === i} over={overIdx === i && dragIdx !== null && dragIdx !== i}
            onDragStart={() => setDragIdx(i)}
            onDragOver={() => { if (overIdx !== i) setOverIdx(i); }}
            onDrop={() => dropOn(i)}
            onDragEnd={() => { setDragIdx(null); setOverIdx(null); }} />
        </div>
      ))}
      <div className="add-block">
        {menu && (
          <div className="add-menu" onMouseLeave={() => setMenu(false)}>
            {BLOCK_TYPES.map((t) => { const Ic = I[t.icon] || I.doc; return (
              <button key={t.type} onClick={() => add(t.type)}><Ic /> {t.label}</button>
            ); })}
          </div>
        )}
        <button className="add-block-btn" onClick={() => setMenu((m) => !m)}><I.menu style={{ width: 16, height: 16 }} /> Add content block</button>
      </div>
    </div>
  );
}

/* =================== Revision history =================== */
function fmtRevTime(iso) {
  if (!iso) return "—";
  const d = new Date(iso);
  if (isNaN(d.getTime())) return "—";
  return d.toLocaleString(undefined, { month: "short", day: "numeric", year: "numeric", hour: "numeric", minute: "2-digit" });
}

function RevisionHistory({ draft, onRestore }) {
  const revs = draft.revisions || [];
  const list = [...revs].reverse().slice(0, 3); // newest filed first, keep last 3
  const blockCount = (b) => { const n = (b || []).length; return n + " block" + (n === 1 ? "" : "s"); };
  return (
    <>
      <div className="con-section-label">Revision history</div>
      <div className="rev-panel">
        <div className="rev-row current">
          <span className="rev-ver cur">v{draft.version || "1.0"}</span>
          <div className="rev-main">
            <div className="rev-title">Current version</div>
            <div className="rev-sub">{blockCount(draft.blocks)} · saved {fmtRevTime(draft.updatedAt)}</div>
          </div>
          <span className="rev-now">Live</span>
        </div>
        {list.length === 0 ? (
          <div className="rev-empty">No earlier versions yet. Each time you save a change, the previous version is filed here so you can roll back to it.</div>
        ) : list.map((r, i) => {
          const renamed = r.snapshot && r.snapshot.title && r.snapshot.title !== draft.title;
          return (
            <div className="rev-row" key={i}>
              <span className="rev-ver">v{r.version}</span>
              <div className="rev-main">
                <div className="rev-title">{blockCount(r.snapshot && r.snapshot.blocks)}{renamed ? " · “" + r.snapshot.title + "”" : ""}</div>
                <div className="rev-sub">Saved {fmtRevTime(r.savedAt)}</div>
              </div>
              <button className="rev-restore" onClick={() => onRestore(r)} title={"Restore v" + r.version}>
                <I.arrowR style={{ width: 14, height: 14, transform: "rotate(180deg)" }} /> Restore
              </button>
            </div>
          );
        })}
      </div>
    </>
  );
}

/* =================== Preview =================== */
function Preview({ draft }) {
  const isDev = draft.kind === "devlog";
  const Icon = resolveIcon(draft.iconKey, isDev ? "book" : "doc");
  const dateLabel = window.dvFullDate ? window.dvFullDate(draft.date) : (draft.date || "Undated");

  // Click any rendered block → scroll the editor to its card and flash it.
  const jumpToBlock = (idx) => {
    const editor = document.querySelector(".con-editor");
    const card = editor && editor.querySelector('.block-card[data-block-idx="' + idx + '"]');
    if (!editor || !card) return;
    const top = card.getBoundingClientRect().top - editor.getBoundingClientRect().top + editor.scrollTop - 72;
    editor.scrollTo({ top: Math.max(0, top), behavior: "smooth" });
    editor.querySelectorAll(".block-card.block-flash").forEach((el) => el.classList.remove("block-flash"));
    card.classList.add("block-flash");
    setTimeout(() => card.classList.remove("block-flash"), 1500);
  };
  const onProseClick = (e) => {
    if (e.target.closest("a")) return;          // let links work normally
    const prose = e.currentTarget;
    let node = e.target;
    while (node && node.parentElement !== prose) node = node.parentElement;
    if (!node) return;
    const idx = Array.prototype.indexOf.call(prose.children, node);
    if (idx >= 0) jumpToBlock(idx);
  };

  return (
    <div className="content" style={{ maxWidth: "none" }}>
      {isDev && draft.cover && <div className="dv-cover da-cover"><img src={draft.cover} alt="" /></div>}
      <div className="page-eyebrow">{isDev ? "Devlog" : (draft.group || "Section")}</div>
      <h1 className="page-title">{draft.title || (isDev ? "Untitled entry" : "Untitled page")}</h1>
      {draft.lede && <p className="page-lede">{draft.lede}</p>}
      <div className="meta-row">
        {isDev ? (
          <>
            <span className="chip"><I.book /> {dateLabel}</span>
            {(draft.tags || []).filter(Boolean).map((t, i) => <span className="chip" key={i}>{t}</span>)}
          </>
        ) : (
          <>
            <span className="chip">{Icon ? <><Icon /> {draft.group || "—"}</> : (draft.group || "—")}</span>
            {draft.meta?.difficulty && <span className="chip">{draft.meta.difficulty}</span>}
            {draft.meta?.scripts > 0 && <span className="chip">{`${draft.meta.scripts} script${draft.meta.scripts > 1 ? "s" : ""}`}</span>}
            {draft.meta?.time && <span className="chip">{draft.meta.time}</span>}
          </>
        )}
      </div>
      <div className="prose prose-pickable" onClick={onProseClick} title="Click a block to jump to it in the editor"><Blocks blocks={draft.blocks || []} /></div>
    </div>
  );
}

/* ---------- JSON page import ---------- */
const VALID_BLOCK_TYPES = new Set(BLOCK_TYPES.map((t) => t.type));
function normalizeImported(obj) {
  if (!obj || typeof obj !== "object" || Array.isArray(obj)) throw new Error("Each entry must be a page object.");
  const o = cloneData(obj);
  if (!o.title || !String(o.title).trim()) throw new Error('A page is missing its "title".');
  if (!Array.isArray(o.blocks)) o.blocks = [];
  o.blocks = o.blocks.filter((b) => b && typeof b === "object" && VALID_BLOCK_TYPES.has(b.type));
  o.id = slugify(o.id && String(o.id).trim() ? o.id : o.title);
  if (o.kind === "devlog") {
    if (typeof o.tags === "string") o.tags = o.tags.split(",").map((s) => s.trim()).filter(Boolean);
    if (!Array.isArray(o.tags)) o.tags = [];
    if (!o.date) o.date = todayISO();
    if (!o.iconKey) o.iconKey = "book";
    delete o.group; delete o.eyebrow; delete o.meta;
  } else {
    o.group = (o.group && String(o.group).trim()) || "Custom";
    o.eyebrow = o.group;
    o.meta = (o.meta && typeof o.meta === "object") ? o.meta : {};
    o.meta.scripts = parseInt(o.meta.scripts, 10) || 0;
    if (!o.meta.difficulty) o.meta.difficulty = "Reference";
    if (!o.meta.time) o.meta.time = "Reference";
    if (!o.iconKey) o.iconKey = PAGE_ICON[o.id] || "doc";
    if (o.draft === undefined) o.draft = false;
  }
  o._new = true;
  return o;
}

function ImportModal({ onClose, onLoadOne, onImportAll }) {
  const [text, setText] = aS("");
  const [err, setErr] = aS(null);
  let count = 0;
  try { const p = JSON.parse(text); count = Array.isArray(p) ? p.length : (p && typeof p === "object" ? 1 : 0); } catch (e) {}
  const run = (fn) => {
    let parsed;
    try { parsed = JSON.parse(text); }
    catch (e) { setErr("That's not valid JSON — " + e.message); return; }
    const arr = Array.isArray(parsed) ? parsed : [parsed];
    if (!arr.length) { setErr("Nothing to import."); return; }
    try { fn(arr); } catch (e) { setErr(e.message); }
  };
  const ph = '{\n  "title": "Risk Assessment Workflow",\n  "group": "Risk",\n  "lede": "One-line summary.",\n  "blocks": [\n    { "type": "h2", "text": "Overview" },\n    { "type": "p", "text": "What this page covers." },\n    { "type": "callout", "variant": "tip", "title": "Tip", "text": "Worth knowing." }\n  ]\n}';
  return (
    <div className="con-modal-backdrop" onMouseDown={(e) => { if (e.target === e.currentTarget) onClose(); }}>
      <div className="con-modal">
        <div className="con-modal-head">
          <h3>Import page JSON</h3>
          <button className="con-modal-x" onClick={onClose} aria-label="Close"><I.x /></button>
        </div>
        <p className="con-modal-sub">Paste a page object — or an array of pages — generated by Claude Code. A single page loads into the editor so you can review it before publishing; multiple pages import straight to drafts.</p>
        <textarea className="con-modal-ta" value={text} spellCheck={false} placeholder={ph}
          onChange={(e) => { setText(e.target.value); setErr(null); }} />
        {err && <div className="con-modal-err">{err}</div>}
        <div className="con-modal-foot">
          {count > 1 && <span className="con-modal-count">{count} pages detected</span>}
          <span className="spacer" style={{ flex: 1 }} />
          <button className="con-mbtn ghost" onClick={onClose}>Cancel</button>
          {count > 1
            ? <button className="con-mbtn" disabled={!text.trim()} onClick={() => run(onImportAll)}>Import all {count}</button>
            : <button className="con-mbtn" disabled={!text.trim()} onClick={() => run((a) => onLoadOne(a[0]))}>Load into editor</button>}
        </div>
      </div>
    </div>
  );
}

/* ---------- groups manager body (reused by the modal and the settings pane) ---------- */
function GroupsPanel({ onChanged }) {
  const [, force] = aS(0);
  const [newName, setNewName] = aS("");
  const [newIcon, setNewIcon] = aS("layers");
  const groups = window.ALL_GROUPS || (window.ADMIN_NAV || NAV).map((g) => g.group);
  const meta = window.GROUP_META || {};
  const overrides = window.getGroupIcons ? window.getGroupIcons() : {};
  const isBuiltin = (g) => (window.BASE_NAV || NAV).some((x) => x.group === g);
  const pageCount = (g) => { const grp = (window.ADMIN_NAV || NAV).find((x) => x.group === g); return grp ? grp.items.length : 0; };
  const iconFor = (g) => overrides[g] || meta[g] || "layers";
  const add = async () => {
    const n = newName.trim();
    if (!n) return;
    if (groups.indexOf(n) !== -1) { onChanged && onChanged("That group already exists"); return; }
    window.__lastError = "";
    const ok = await window.createGroup(n, newIcon);
    force((x) => x + 1);
    if (!ok) { onChanged && onChanged("Couldn't save group: " + (window.__lastError || "check the connection")); return; }
    setNewName(""); setNewIcon("layers"); onChanged && onChanged("Group created");
  };
  const change = async (g, val) => {
    window.__lastError = "";
    const ok = await window.setGroupIcon(g, val);
    force((x) => x + 1);
    onChanged && onChanged(ok ? "Group icon updated" : ("Couldn't save: " + (window.__lastError || "check the connection")));
  };
  const del = async (g) => {
    if (!confirm(`Delete the “${g}” group?`)) return;
    window.__lastError = "";
    const ok = await window.deleteGroup(g);
    force((x) => x + 1);
    if (ok) { onChanged && onChanged("Group deleted"); return; }
    onChanged && onChanged(window.__lastError ? ("Couldn't save: " + window.__lastError) : "Move its pages to another group first");
  };
  return (
    <>
      <p className="con-modal-sub">Create the sidebar sections your pages live in and pick each one's icon. The <strong>Group</strong> dropdown on every page is filled from this list.</p>
      <div className="gi-create">
        <input className="inp" placeholder="New group name…" value={newName}
          onChange={(e) => setNewName(e.target.value)} onKeyDown={(e) => { if (e.key === "Enter") add(); }} />
        <select className="sel" value={newIcon} onChange={(e) => setNewIcon(e.target.value)} title="Icon">
          <option value="none">— none —</option>
          {ICON_OPTIONS.map((k) => <option key={k} value={k}>{k}</option>)}
        </select>
        <button className="con-mbtn" disabled={!newName.trim()} onClick={add}><I.menu style={{ width: 14, height: 14 }} /> Add</button>
      </div>
      <div className="gi-list">
        {groups.length === 0 && <p className="settings-field-sub" style={{ padding: "6px 2px" }}>No groups yet — add one above. New pages will live under the group you pick.</p>}
        {groups.map((g) => { const Ic = resolveIcon(iconFor(g), "layers"); const n = pageCount(g); const bi = isBuiltin(g); return (
          <div className="gi-row" key={g}>
            <span className="gi-ic">{Ic ? <Ic /> : null}</span>
            <span className="gi-name">{g}{bi && <span className="gi-tag">built-in</span>}</span>
            <span className="gi-count">{n} page{n === 1 ? "" : "s"}</span>
            <select className="sel" value={iconFor(g)} onChange={(e) => change(g, e.target.value)}>
              <option value="none">— none —</option>
              {ICON_OPTIONS.map((k) => <option key={k} value={k}>{k}</option>)}
            </select>
            <button className="gi-del" disabled={bi || n > 0} onClick={() => del(g)}
              title={bi ? "Built-in groups can't be deleted" : (n > 0 ? "Move its pages out first" : "Delete group")}><I.x /></button>
          </div>
        ); })}
      </div>
    </>
  );
}

/* ---------- groups manager modal (opened from the console list panel) ---------- */
function GroupsModal({ onClose, onChanged }) {
  return (
    <div className="con-modal-backdrop" onMouseDown={(e) => { if (e.target === e.currentTarget) onClose(); }}>
      <div className="con-modal">
        <div className="con-modal-head">
          <h3>Groups</h3>
          <button className="con-modal-x" onClick={onClose} aria-label="Close"><I.x /></button>
        </div>
        <GroupsPanel onChanged={onChanged} />
        <div className="con-modal-foot"><span style={{ flex: 1 }} /><button className="con-mbtn" onClick={onClose}>Done</button></div>
      </div>
    </div>
  );
}

/* Order a group's pages so each child sits directly beneath its parent, with a depth
   for indentation — mirrors the live-site sidebar tree (parent-in-same-group only). */
function flattenGroupItems(items) {
  const list = (items || []).filter(Boolean);
  const ids = new Set(list.map((p) => p.id));
  const childrenOf = {};
  const roots = [];
  list.forEach((p) => {
    const par = p.parent && p.parent !== p.id && ids.has(p.parent) ? p.parent : null;
    if (par) (childrenOf[par] = childrenOf[par] || []).push(p);
    else roots.push(p);
  });
  const out = [];
  const walk = (p, depth) => {
    out.push({ page: p, depth });
    (childrenOf[p.id] || []).forEach((c) => walk(c, depth + 1));
  };
  roots.forEach((r) => walk(r, 0));
  return out;
}

/* Pages a given draft may be nested under: same group, excluding itself and its own descendants. */
function parentCandidates(draft) {
  if (!draft || draft.kind === "devlog") return [];
  const all = [];
  (window.ADMIN_NAV || NAV).forEach((g) => g.items.forEach((p) => all.push(p)));
  const childrenOf = {};
  all.forEach((p) => { if (p.parent) (childrenOf[p.parent] = childrenOf[p.parent] || []).push(p.id); });
  const banned = new Set([draft.id]);
  const stack = [draft.id];
  while (stack.length) { const x = stack.pop(); (childrenOf[x] || []).forEach((c) => { if (!banned.has(c)) { banned.add(c); stack.push(c); } }); }
  return all.filter((p) => p.group === draft.group && !banned.has(p.id));
}

/* =================== Settings modal =================== */
function settRelTime(iso) {
  if (!iso) return "";
  const d = new Date(iso), s = Math.floor((Date.now() - d.getTime()) / 1000);
  if (s < 60) return "just now";
  const m = Math.floor(s / 60); if (m < 60) return m + "m ago";
  const h = Math.floor(m / 60); if (h < 24) return h + "h ago";
  const dd = Math.floor(h / 24); if (dd < 30) return dd + "d ago";
  return d.toLocaleDateString();
}

function ReportRow({ r, onOpenPage, onClose }) {
  const resolved = r.status === "resolved";
  const goPage = () => {
    const pid = String(r.page_id || "");
    const id = pid.startsWith("devlog/") ? pid.slice(7) : pid;
    if (id) onOpenPage(id);
    onClose();
  };
  return (
    <div className={"report-row" + (resolved ? " resolved" : "")}>
      <div className="report-row-top">
        <span className="report-badge">{r.type}</span>
        {r.page_title
          ? <button className="report-page" onClick={goPage} title="Open this page in the editor">{r.page_title}</button>
          : <span className="report-page muted">Unknown page</span>}
        <span className="report-row-date">{settRelTime(r.created_at)}</span>
      </div>
      <div className="report-msg">{r.message}</div>
      <div className="report-row-actions">
        <button className="report-act" onClick={() => window.setReportStatus(r.id, resolved ? "new" : "resolved")}>
          {resolved ? "Reopen" : "Mark resolved"}
        </button>
        <button className="report-act danger" onClick={() => { if (confirm("Delete this report permanently?")) window.deleteReport(r.id); }}>
          <I.trash /> Delete
        </button>
      </div>
    </div>
  );
}

/* ---- reusable settings controls (persist via window.setSetting) ---- */
/* Debounced setting writer that also flushes any pending value when the
   control unmounts (e.g. the user closes the settings modal). */
function useDebouncedSetting(sk, delay) {
  const timer = aR(null);
  const pending = aR(undefined);
  aE(() => () => {
    if (pending.current !== undefined) { clearTimeout(timer.current); window.setSetting(sk, pending.current); }
  }, []);
  return (val) => {
    pending.current = val;
    clearTimeout(timer.current);
    timer.current = setTimeout(() => { window.setSetting(sk, pending.current); pending.current = undefined; }, delay == null ? 400 : delay);
  };
}

function SetRow({ title, sub, children }) {
  return (
    <div className="settings-row">
      <div className="settings-row-text">
        <div className="settings-row-title">{title}</div>
        {sub && <div className="settings-row-sub">{sub}</div>}
      </div>
      {children}
    </div>
  );
}
function SetToggle({ sk, title, sub, onToast }) {
  const [v, setV] = aS(() => window.cfg(sk));
  const set = (nv) => { setV(nv); window.setSetting(sk, nv); onToast && onToast("Saved"); };
  return <SetRow title={title} sub={sub}><button className={"settings-toggle" + (v ? " on" : "")} role="switch" aria-checked={v} onClick={() => set(!v)}><span className="settings-knob" /></button></SetRow>;
}
function SetText({ sk, title, sub, placeholder, area }) {
  const [v, setV] = aS(() => window.cfg(sk) || "");
  const commit = useDebouncedSetting(sk);
  const onChange = (e) => { setV(e.target.value); commit(e.target.value); };
  return (
    <div className="settings-field">
      <label>{title}</label>
      {sub && <div className="settings-field-sub">{sub}</div>}
      {area
        ? <textarea className="settings-input" rows={3} value={v} placeholder={placeholder || ""} onChange={onChange} />
        : <input className="settings-input" value={v} placeholder={placeholder || ""} onChange={onChange} onKeyDown={(e) => { if (e.key === "Enter") e.target.blur(); }} />}
    </div>
  );
}
function SetNumber({ sk, title, sub, min, max, unit }) {
  const [v, setV] = aS(() => window.cfg(sk));
  const set = (raw) => { let n = parseInt(raw, 10); if (isNaN(n)) n = 0; if (min != null) n = Math.max(min, n); if (max != null) n = Math.min(max, n); setV(n); window.setSetting(sk, n); };
  return <SetRow title={title} sub={sub}><div className="settings-num"><input type="number" value={v} min={min} max={max} onChange={(e) => set(e.target.value)} />{unit && <span>{unit}</span>}</div></SetRow>;
}
function SetSelect({ sk, title, sub, options }) {
  const [v, setV] = aS(() => window.cfg(sk));
  const set = (nv) => { setV(nv); window.setSetting(sk, nv); };
  return <SetRow title={title} sub={sub}><select className="sel" value={v} onChange={(e) => set(e.target.value)}>{options.map((o) => <option key={o.value} value={o.value}>{o.label}</option>)}</select></SetRow>;
}

/* Header-banner uploader + placement controls. Separate pictures for light & dark themes. */
function BannerSetting({ onToast }) {
  const [tab, setTab] = aS("light");           // which theme's picture is being edited
  const [imgLight, setImgLight] = aS(() => window.cfg("bannerImage") || "");
  const [imgDark, setImgDark] = aS(() => window.cfg("bannerImageDark") || "");
  const [posY, setPosY] = aS(() => window.cfg("bannerPosY"));
  const [height, setHeight] = aS(() => window.cfg("bannerHeight"));
  const [overlay, setOverlay] = aS(() => window.cfg("bannerOverlay"));
  const [vignette, setVignette] = aS(() => window.cfg("bannerVignette"));
  const [vigColor, setVigColor] = aS(() => window.cfg("bannerVignetteColor") || "black");
  const [busy, setBusy] = aS(false);
  const commitPosY = useDebouncedSetting("bannerPosY");
  const commitHeight = useDebouncedSetting("bannerHeight");
  const commitOverlay = useDebouncedSetting("bannerOverlay");
  const commitVignette = useDebouncedSetting("bannerVignette");

  const vignetteGradient = (color) => color === "transparent" ? "none"
    : `radial-gradient(ellipse at center, transparent 38%, ${color === "white" ? "#fff" : "#000"} 122%)`;
  const pickVigColor = async (c) => { setVigColor(c); await window.setSetting("bannerVignetteColor", c); };

  const H_MIN = 180, H_MAX = 540;

  const isDark = tab === "dark";
  const settingKey = isDark ? "bannerImageDark" : "bannerImage";
  const img = isDark ? imgDark : imgLight;
  const setImg = isDark ? setImgDark : setImgLight;

  const onFile = async (f) => {
    if (!f || !f.type.startsWith("image/")) return;
    setBusy(true);
    try { const url = await processImage(f); setImg(url); await window.setSetting(settingKey, url); onToast && onToast((isDark ? "Dark" : "Light") + " banner updated"); }
    catch (x) { alert("Upload failed: " + (x.message || x)); }
    setBusy(false);
  };
  const remove = async () => { setImg(""); await window.setSetting(settingKey, ""); onToast && onToast((isDark ? "Dark" : "Light") + " banner removed"); };

  // Preview scales across the full slider range so the height control is visibly responsive.
  const previewH = Math.round(120 + ((height - H_MIN) / (H_MAX - H_MIN)) * (300 - 120));

  return (
    <div className="settings-field">
      <label>Header banner</label>
      <div className="settings-field-sub">A picture behind the home-page title. Set a separate image for each theme — the matching one shows when a visitor is in light or dark mode. Placement &amp; darkening below apply to both.</div>
      <div className="banner-tabs">
        <button type="button" className={"banner-tab" + (!isDark ? " active" : "")} onClick={() => setTab("light")}>
          <I.sun style={{ width: 14, height: 14 }} /> Light theme{imgLight ? <span className="banner-tab-dot" /> : null}
        </button>
        <button type="button" className={"banner-tab" + (isDark ? " active" : "")} onClick={() => setTab("dark")}>
          <I.moon style={{ width: 14, height: 14 }} /> Dark theme{imgDark ? <span className="banner-tab-dot" /> : null}
        </button>
      </div>
      {img ? (
        <>
          <div className={"banner-preview" + (isDark ? " on-dark" : "")} style={{ height: Math.max(120, Math.min(previewH, 300)) }}>
            <div className="banner-preview-img" style={{ backgroundImage: `url("${img}")`, backgroundPosition: `center ${posY}%` }} />
            <div className="banner-preview-veil" style={{ opacity: overlay / 100 }} />
            <div className="banner-preview-vignette" style={{ opacity: vignette / 100, background: vignetteGradient(vigColor) }} />
            <div className="banner-preview-title">{window.cfg("siteTitle")} <span>{window.cfg("siteTitleAccent")}</span></div>
          </div>
          <div className="banner-ctrls">
            <label>Vertical<input type="range" min="0" max="100" value={posY}
              onChange={(e) => { const x = +e.target.value; setPosY(x); commitPosY(x); }} /></label>
            <label>Height<input type="range" min={H_MIN} max={H_MAX} step="10" value={height}
              onChange={(e) => { const x = +e.target.value; setHeight(x); commitHeight(x); }} /></label>
            <label>Darken<input type="range" min="0" max="80" value={overlay}
              onChange={(e) => { const x = +e.target.value; setOverlay(x); commitOverlay(x); }} /></label>
            <label>Vignette<input type="range" min="0" max="100" value={vignette} disabled={vigColor === "transparent"}
              onChange={(e) => { const x = +e.target.value; setVignette(x); commitVignette(x); }} /></label>
            <label>Edge tint<span className="banner-vig-seg">
              {[["black", "Black"], ["white", "White"], ["transparent", "None"]].map(([v, lbl]) => (
                <button type="button" key={v} className={"banner-vig-opt" + (vigColor === v ? " active" : "")} onClick={() => pickVigColor(v)}>{lbl}</button>
              ))}
            </span></label>
          </div>
          <div className="banner-actions">
            <label className="con-mbtn ghost" style={{ cursor: "pointer" }}>{busy ? "Uploading…" : "Replace image"}
              <input type="file" accept="image/*" hidden onChange={async (e) => { await onFile(e.target.files[0]); e.target.value = ""; }} /></label>
            <button className="con-mbtn danger" onClick={remove}><I.trash style={{ width: 14, height: 14 }} /> Remove</button>
          </div>
        </>
      ) : (
        <label className="upload-drop"
          onDragOver={(e) => { e.preventDefault(); e.currentTarget.classList.add("drag"); }}
          onDragLeave={(e) => e.currentTarget.classList.remove("drag")}
          onDrop={async (e) => { e.preventDefault(); e.currentTarget.classList.remove("drag"); await onFile(e.dataTransfer.files[0]); }}>
          <I.image />
          <span>{busy ? "Uploading…" : `Click to upload — or drop a ${isDark ? "dark" : "light"}-theme banner`}</span>
          <input type="file" accept="image/*" hidden onChange={async (e) => { await onFile(e.target.files[0]); e.target.value = ""; }} />
        </label>
      )}
    </div>
  );
}

function SettingsModal({ onClose, onOpenPage, onToast }) {
  const [cat, setCat] = aS("general");
  const [reportsOn, setReportsOn] = aS(window.getSetting ? window.getSetting("reportsEnabled", true) : true);
  const [reports, setReports] = aS(null);   // null = loading · "error" · array
  const [filter, setFilter] = aS("new");
  const [pw1, setPw1] = aS(""); const [pw2, setPw2] = aS("");
  const [pwMsg, setPwMsg] = aS(null); const [pwBusy, setPwBusy] = aS(false);

  const refresh = async () => {
    let r = await window.loadReports();
    if (Array.isArray(r)) {
      const days = parseInt(window.cfg("reportArchiveDays"), 10) || 0;
      if (days > 0) {
        const cutoff = Date.now() - days * 86400000;
        const stale = r.filter((x) => x.status === "resolved" && x.created_at && new Date(x.created_at).getTime() < cutoff);
        if (stale.length) { for (const s of stale) await window.deleteReport(s.id); r = r.filter((x) => stale.indexOf(x) === -1); }
      }
    }
    setReports(r === null ? "error" : r);
  };
  aE(() => { refresh(); }, []);
  aE(() => {
    const on = () => refresh();
    window.addEventListener("konform-reports-updated", on);
    return () => window.removeEventListener("konform-reports-updated", on);
  }, []);

  const toggleReports = async (v) => {
    setReportsOn(v);
    await window.setSetting("reportsEnabled", v);
    onToast && onToast(v ? "Issue reporting turned on" : "Issue reporting turned off");
  };

  const doExport = async () => {
    const data = await window.exportAllData();
    const blob = new Blob([JSON.stringify(data, null, 2)], { type: "application/json" });
    const url = URL.createObjectURL(blob);
    const a = document.createElement("a");
    a.href = url; a.download = "wiki-backup-" + new Date().toISOString().slice(0, 10) + ".json";
    document.body.appendChild(a); a.click(); a.remove(); URL.revokeObjectURL(url);
    onToast && onToast("Backup downloaded");
  };
  const doWipe = async () => {
    if (!confirm("Delete ALL pages and devlogs permanently? Your settings, groups and icons are kept. This cannot be undone.")) return;
    if (!confirm("Really wipe everything? Export a backup first if you're unsure.")) return;
    const ok = await window.wipeAllContent();
    onToast && onToast(ok ? "All content wiped" : "Wipe failed");
  };
  const doChangePw = async () => {
    setPwMsg(null);
    if (pw1 !== pw2) { setPwMsg({ err: true, t: "Passwords don't match." }); return; }
    setPwBusy(true);
    const res = await window.changeConsolePassword(pw1);
    setPwBusy(false);
    if (res.ok) { setPw1(""); setPw2(""); setPwMsg({ err: false, t: "Password updated." }); }
    else setPwMsg({ err: true, t: res.error || "Couldn't update." });
  };

  const list = Array.isArray(reports) ? reports : [];
  const newCount = list.filter((r) => r.status === "new").length;
  const shown = list.filter((r) => filter === "all" ? true : filter === "new" ? r.status === "new" : r.status === "resolved");

  const groups = window.ALL_GROUPS || [];
  const groupOpts = [{ value: "", label: "First group (default)" }].concat(groups.map((g) => ({ value: g, label: g })));

  const CATS = [
    { id: "general", label: "General", icon: "cog" },
    { id: "editor", label: "Editor", icon: "doc" },
    { id: "groups", label: "Groups", icon: "layers" },
    { id: "reports", label: "Issue Reports", icon: "inbox" },
    { id: "data", label: "Data", icon: "save" },
    { id: "access", label: "Access", icon: "lock" },
    { id: "about", label: "About", icon: "info" },
  ];

  return (
    <div className="con-modal-backdrop" onMouseDown={(e) => { if (e.target === e.currentTarget) onClose(); }}>
      <div className="settings-modal" onMouseDown={(e) => e.stopPropagation()}>
        <aside className="settings-side">
          <div className="settings-side-head"><I.cog /> Settings</div>
          {CATS.map((c) => { const Ic = I[c.icon] || I.doc; return (
            <button key={c.id} className={"settings-cat" + (cat === c.id ? " active" : "")} onClick={() => setCat(c.id)}>
              <Ic /> <span>{c.label}</span>
              {c.id === "reports" && newCount > 0 && <span className="settings-badge">{newCount}</span>}
            </button>
          ); })}
        </aside>
        <div className="settings-main">
          <button className="settings-close" onClick={onClose} aria-label="Close"><I.x /></button>

          {cat === "general" && (
            <div className="settings-pane">
              <h3 className="settings-h">General</h3>
              <SetText sk="siteTitle" title="Site title" placeholder="Konform" />
              <SetText sk="siteTitleAccent" title="Title accent" sub="The highlighted second part of the wordmark." placeholder="Docs" />
              <SetText sk="siteTagline" title="Home tagline" area placeholder="One line shown under the hero title." />
              <SetText sk="githubUrl" title="GitHub URL" sub="Powers the navbar GitHub icon — leave blank to hide it." placeholder="https://github.com/you/repo" />
              <SetText sk="footerNote" title="Footer note" area />
              <SetSelect sk="defaultTheme" title="Default theme" sub="Applied to first-time visitors who haven't picked one." options={[{ value: "light", label: "Light" }, { value: "dark", label: "Dark" }]} />
              <div className="settings-divider" />
              <BannerSetting onToast={onToast} />
            </div>
          )}

          {cat === "groups" && (
            <div className="settings-pane">
              <h3 className="settings-h">Groups</h3>
              <GroupsPanel onChanged={onToast} />
            </div>
          )}

          {cat === "editor" && (
            <div className="settings-pane">
              <h3 className="settings-h">Editor</h3>
              <SetSelect sk="defaultGroup" title="Default group for new pages" sub="Which sidebar section a new page starts in." options={groupOpts} />
              <div className="settings-divider" />
              <SetToggle sk="autosave" title="Autosave edits" sub="Quietly saves an existing page ~2s after you stop typing." onToast={onToast} />
              <div className="settings-divider" />
              <SetToggle sk="showDrafts" title="Show draft pages on the live site" sub="Off hides every page/entry marked Draft from visitors (still visible here)." onToast={onToast} />
            </div>
          )}

          {cat === "reports" && (
            <div className="settings-pane">
              <h3 className="settings-h">Issue Reports</h3>
              <div className="settings-row">
                <div className="settings-row-text">
                  <div className="settings-row-title">Allow visitors to report problems</div>
                  <div className="settings-row-sub">Shows a subtle “Report an issue” button on documentation pages. Switch off if it ever gets abused.</div>
                </div>
                <button className={"settings-toggle" + (reportsOn ? " on" : "")} role="switch" aria-checked={reportsOn} onClick={() => toggleReports(!reportsOn)}><span className="settings-knob" /></button>
              </div>
              <SetNumber sk="reportCooldownSec" title="Submission cooldown" sub="Minimum gap between reports from one browser." min={0} max={3600} unit="sec" />
              <SetNumber sk="reportArchiveDays" title="Auto-delete resolved after" sub="Clears out old resolved reports. 0 = keep forever." min={0} max={365} unit="days" />
              <div className="settings-divider" />
              <div className="settings-listhead">
                <div className="settings-tabs">
                  {[["new", "New"], ["resolved", "Resolved"], ["all", "All"]].map(([f, lbl]) => (
                    <button key={f} className={"settings-tab" + (filter === f ? " active" : "")} onClick={() => setFilter(f)}>
                      {lbl}{f === "new" && newCount > 0 ? " · " + newCount : ""}
                    </button>
                  ))}
                </div>
                <button className="settings-refresh" onClick={refresh} title="Refresh"><I.ring /></button>
              </div>
              {reports === null ? <div className="settings-empty">Loading reports…</div>
                : reports === "error" ? <div className="settings-empty err">Couldn't load reports — check that the API and Cosmos DB are reachable (and that you have the <code>admin</code> role). Visitor reports still work in the meantime.</div>
                : shown.length === 0 ? <div className="settings-empty">No {filter === "all" ? "" : filter + " "}reports{filter === "new" ? " — you're all caught up." : "."}</div>
                : <div className="report-list">{shown.map((r) => <ReportRow key={r.id} r={r} onOpenPage={onOpenPage} onClose={onClose} />)}</div>}
            </div>
          )}

          {cat === "data" && (
            <div className="settings-pane">
              <h3 className="settings-h">Data</h3>
              <SetRow title="Export everything" sub="Download all pages, devlogs, reports and settings as a single JSON backup.">
                <button className="con-mbtn ghost" onClick={doExport}><I.save style={{ width: 14, height: 14 }} /> Export JSON</button>
              </SetRow>
              <div className="settings-divider" />
              <div className="settings-danger">
                <div className="settings-danger-head">Danger zone</div>
                <SetRow title="Wipe all content" sub="Permanently delete every page and devlog. Settings, groups and icons are kept.">
                  <button className="con-mbtn danger" onClick={doWipe}><I.trash style={{ width: 14, height: 14 }} /> Wipe…</button>
                </SetRow>
              </div>
            </div>
          )}

          {cat === "access" && (
            <div className="settings-pane">
              <h3 className="settings-h">Access</h3>
              {window.CLOUD ? (
                <div className="settings-field">
                  <label>Console access</label>
                  <div className="settings-field-sub">The console password is the <code>ADMIN_PASSWORD</code> app setting on your Static Web App (Configuration → Application settings). Update it there and save — it applies on the next sign-in.</div>
                </div>
              ) : (
                <div className="settings-field">
                  <label>Change console password</label>
                  <div className="settings-field-sub">Sets the local console gate password.</div>
                  <input className="settings-input" type="password" placeholder="New password" value={pw1} onChange={(e) => setPw1(e.target.value)} />
                  <input className="settings-input" type="password" placeholder="Confirm new password" value={pw2} onChange={(e) => setPw2(e.target.value)} style={{ marginTop: 8 }} />
                  {pwMsg && <div className={"settings-pwmsg" + (pwMsg.err ? " err" : "")}>{pwMsg.t}</div>}
                  <div style={{ marginTop: 10 }}><button className="con-mbtn" disabled={pwBusy || pw1.length < 6} onClick={doChangePw}>{pwBusy ? "Saving…" : "Update password"}</button></div>
                </div>
              )}
              <div className="settings-divider" />
              <SetNumber sk="autoLogoutMin" title="Auto-logout after inactivity" sub="Signs you out of the console after this many idle minutes. 0 = never." min={0} max={240} unit="min" />
            </div>
          )}

          {cat === "about" && (
            <div className="settings-pane">
              <h3 className="settings-h">About</h3>
              <div className="settings-row">
                <div className="settings-row-text">
                  <div className="settings-row-title">Site version</div>
                  <div className="settings-row-sub">Storage mode: {window.CLOUD ? "Cloud (Azure)" : "Local (this browser)"}</div>
                </div>
                <span className="ct-tag ver">v{window.SITE_VERSION || "1.00"}</span>
              </div>
              <div className="settings-divider" />
              <div className="settings-changelog">
                {(window.SITE_CHANGELOG || []).slice(0, 5).map((c, i) => (
                  <div className="settings-clog" key={i}>
                    <span className="settings-clog-ver">v{c.version}</span>
                    <div className="settings-clog-body">
                      <div className="settings-clog-note">{c.note}</div>
                      <div className="settings-clog-date">{c.date} · {c.kind}</div>
                    </div>
                  </div>
                ))}
              </div>
            </div>
          )}
        </div>
      </div>
    </div>
  );
}

/* =================== Console =================== */
function Console({ onExit, onLogout, theme, setTheme }) {
  const [selectedId, setSelectedId] = aS(null);
  const [draft, setDraft] = aS(null);
  const [dirty, setDirty] = aS(false);
  const [saving, setSaving] = aS(false);
  const [toast, setToast] = aS(null);
  const [importOpen, setImportOpen] = aS(false);
  const [groupIconsOpen, setGroupIconsOpen] = aS(false);
  const [settingsOpen, setSettingsOpen] = aS(false);
  const [newReports, setNewReports] = aS(0);

  aE(() => {
    let alive = true;
    const refresh = async () => { const r = await window.loadReports(); if (alive && Array.isArray(r)) setNewReports(r.filter((x) => x.status === "new").length); };
    refresh();
    window.addEventListener("konform-reports-updated", refresh);
    return () => { alive = false; window.removeEventListener("konform-reports-updated", refresh); };
  }, []);

  const showToast = (msg) => { setToast(msg); setTimeout(() => setToast(null), 2200); };

  // A brand-new draft with nothing typed in isn't worth a discard warning.
  const isDraftEmpty = (d) => {
    if (!d) return true;
    const has = (v) => (v || "").toString().trim().length > 0;
    return !(has(d.title) || has(d.lede) || has(d.eyebrow)
      || (Array.isArray(d.blocks) && d.blocks.length > 0)
      || (Array.isArray(d.tags) && d.tags.length > 0)
      || !!d.cover);
  };
  const guardDiscard = () => {
    if (!dirty) return true;
    if (draft && draft._new && isDraftEmpty(draft)) return true;
    return confirm("Discard unsaved changes?");
  };

  const openPage = (id) => {
    if (!guardDiscard()) return;
    const live = (window.ADMIN_PAGE_BY_ID && window.ADMIN_PAGE_BY_ID[id]) || PAGE_BY_ID[id] || (window.DEVLOG_BY_ID && window.DEVLOG_BY_ID[id]);
    if (!live) return;
    const d = cloneData(live);
    if (!d.meta) d.meta = {};
    if (!d.iconKey) d.iconKey = PAGE_ICON[id] || (d.kind === "devlog" ? "book" : "doc");
    if (!d.blocks) d.blocks = [];
    if (d.kind === "devlog") { if (!Array.isArray(d.tags)) d.tags = []; if (!d.date) d.date = todayISO(); }
    setDraft(d); setSelectedId(id); setDirty(false);
  };

  const defaultGroupForNew = () => {
    const groups = window.ALL_GROUPS || (window.ADMIN_NAV || NAV).map((g) => g.group);
    const pref = window.cfg ? window.cfg("defaultGroup") : "";
    if (pref && groups.indexOf(pref) !== -1) return pref;
    return groups[0] || "General";
  };

  const newPage = () => {
    if (!guardDiscard()) return;
    const d = { id: "", title: "", group: defaultGroupForNew(), eyebrow: "", lede: "", iconKey: "doc", draft: true, meta: { difficulty: "Draft", time: "Draft", scripts: 0 }, blocks: [], _new: true };
    setDraft(d); setSelectedId("__new"); setDirty(true);
  };

  const newDevlog = () => {
    if (!guardDiscard()) return;
    const d = { id: "", kind: "devlog", title: "", date: todayISO(), tags: [], cover: undefined, lede: "", iconKey: "book", draft: false, blocks: [], _new: true };
    setDraft(d); setSelectedId("__new"); setDirty(true);
  };

  const patch = (p) => { setDraft((d) => ({ ...d, ...p })); setDirty(true); };
  const patchMeta = (p) => { setDraft((d) => ({ ...d, meta: { ...d.meta, ...p } })); setDirty(true); };

  const loadImported = (obj) => {
    const d = normalizeImported(obj);
    setDraft(d); setSelectedId("__new"); setDirty(true); setImportOpen(false);
    showToast("Loaded — review it, then Save to publish");
  };
  const importAllPages = async (arr) => {
    let norm;
    try { norm = arr.map(normalizeImported); }
    catch (e) { showToast(e.message); return; }
    setImportOpen(false); setSaving(true);
    let n = 0;
    for (const p of norm) { const pg = { ...p }; delete pg._new; if (await savePage(pg)) n++; }
    setSaving(false);
    showToast("Imported " + n + " page" + (n === 1 ? "" : "s") + " & published");
  };

  const idForSave = () => {
    if (!draft._new) return draft.id;
    return (draft.id && draft.id.trim()) ? slugify(draft.id) : slugify(draft.title);
  };

  const canSave = draft && draft.title.trim() && (draft._new ? !!idForSave() : true);

  const doSave = async () => {
    const id = idForSave();
    if (!id) { showToast("Add a title first"); return; }
    if (draft._new && PAGE_BY_ID[id] && !window.BASE_IDS.has(id)) {
      if (!confirm(`A page with id "${id}" exists. Overwrite it?`)) return;
    }
    const page = { ...draft, id };
    delete page._new;
    if (draft.kind === "devlog") {
      page.kind = "devlog";
      if (typeof page.tags === "string") page.tags = page.tags.split(",").map((s) => s.trim()).filter(Boolean);
      if (!Array.isArray(page.tags)) page.tags = [];
      delete page.eyebrow; delete page.meta; delete page.group;
    } else {
      page.eyebrow = draft.group;
      page.meta = { ...page.meta, scripts: parseInt(page.meta?.scripts, 10) || 0 };
    }
    setSaving(true);
    const saved = await savePage(page);
    setSaving(false);
    if (!saved) { showToast(window.CLOUD ? ("Save failed: " + (window.__lastError || "check the connection")) : "Storage full — try fewer or smaller images"); return; }
    setDraft({ ...(typeof saved === "object" ? saved : page) }); setSelectedId(id); setDirty(false);
    showToast(draft._new ? "Page created & published" : "Saved & published");
  };

  // Autosave: quietly persist an existing page a moment after edits stop.
  aE(() => {
    if (!(window.cfg && window.cfg("autosave"))) return;
    if (!draft || draft._new || !dirty || !canSave || saving) return;
    const t = setTimeout(() => { doSave(); }, 2000);
    return () => clearTimeout(t);
  }, [draft, dirty]);

  const restoreRevision = (rev) => {
    if (!rev || !rev.snapshot) return;
    if (!confirm(`Restore version ${rev.version}? It replaces what's in the editor now. Save afterwards to keep it — the current content is filed as a new revision, so nothing is lost.`)) return;
    setDraft((d) => ({ ...d, ...cloneData(rev.snapshot) }));
    setDirty(true);
    showToast(`Loaded v${rev.version} — Save to keep it`);
  };

  const doDelete = async () => {
    if (!draft || draft._new) { setDraft(null); setSelectedId(null); setDirty(false); return; }
    if (!confirm("Delete this page permanently?")) return;
    await removePage(draft.id);
    setDraft(null); setSelectedId(null); setDirty(false);
    showToast("Page deleted");
  };

  const toggleHidden = async (id) => {
    const willHide = !window.isHidden(id);
    const ok = await window.setPageHidden(id, willHide);
    if (ok) showToast(willHide ? "Hidden from site" : "Now visible on site");
    else showToast("Couldn't update visibility");
  };

  const groupOptions = window.ALL_GROUPS || Array.from(new Set((window.ADMIN_NAV || NAV).map((g) => g.group)));

  return (
    <div className="console">
      <header className="console-top">
        <div className="ct-brand">
          <span className="ct-mark"><I.check /></span>
          <span className="ct-title">Developer Console</span>
          <span className="ct-tag">{window.CLOUD ? "Cloud" : "Local"}</span>
          <span className="ct-tag ver" title={(window.SITE_CHANGELOG && window.SITE_CHANGELOG[0]) ? window.SITE_CHANGELOG[0].note : ""}>v{window.SITE_VERSION || "1.00"}</span>
        </div>
        <span className="spacer" />
        <button className="con-topbtn settings-btn" onClick={() => setSettingsOpen(true)} title="Settings">
          <I.cog />{newReports > 0 && <span className="con-topbadge">{newReports}</span>}
        </button>
        <button className="con-topbtn" onClick={() => setTheme(theme === "dark" ? "light" : "dark")}>
          {theme === "dark" ? <I.sun /> : <I.moon />}
        </button>
        <a className="con-topbtn" href="#/" onClick={(e) => { e.preventDefault(); onExit(); }}><I.arrowR /> View site</a>
        <button className="con-topbtn danger" onClick={onLogout}><I.x /> Log out</button>
      </header>

      {importOpen && <ImportModal onClose={() => setImportOpen(false)} onLoadOne={loadImported} onImportAll={importAllPages} />}
      {groupIconsOpen && <GroupsModal onClose={() => setGroupIconsOpen(false)} onChanged={(m) => showToast(m || "Groups updated")} />}
      {settingsOpen && <SettingsModal onClose={() => setSettingsOpen(false)} onOpenPage={openPage} onToast={showToast} />}
      <div className="console-body">
        {/* page list */}
        <aside className="con-list">
          <button className="con-newbtn" onClick={newPage}><I.doc style={{ width: 15, height: 15 }} /> New page</button>
          <button className="con-importbtn" onClick={() => setImportOpen(true)}><I.doc style={{ width: 14, height: 14 }} /> Import JSON</button>
          <button className="con-importbtn" onClick={() => setGroupIconsOpen(true)}><I.layers style={{ width: 14, height: 14 }} /> Groups</button>
          {(window.ADMIN_NAV || NAV).map((g) => (
            <div key={g.group}>
              <div className="con-list-group">{g.group}</div>
              {flattenGroupItems(g.items).map(({ page: p, depth }) => {
                const Ic = resolveIcon(PAGE_ICON[p.id], "doc");
                return (
                  <button key={p.id} className={"con-list-item" + (selectedId === p.id ? " active" : "") + (window.isHidden(p.id) ? " is-hidden" : "") + (depth > 0 ? " is-child" : "")} style={depth > 1 ? { paddingLeft: 22 + (depth - 1) * 16 } : undefined} onClick={() => openPage(p.id)}>
                    <span className="cli-ic">{Ic ? <Ic /> : null}</span>
                    <span className="cli-name">{p.title.replace(/ —.*$/, "")}</span>
                    {window.isHidden(p.id) && <span className="cli-tag hidden">Hidden</span>}
                    {p.draft && <span className="cli-dot draft" title="Draft" />}
                  </button>
                );
              })}
            </div>
          ))}
          {(window.ADMIN_DEVLOG && window.ADMIN_DEVLOG.length > 0) && (
            <div>
              <div className="con-list-group">Devlog</div>
              {window.ADMIN_DEVLOG.map((p) => {
                const Ic = resolveIcon(p.iconKey, "book");
                return (
                  <button key={p.id} className={"con-list-item" + (selectedId === p.id ? " active" : "") + (window.isHidden(p.id) ? " is-hidden" : "")} onClick={() => openPage(p.id)}>
                    <span className="cli-ic">{Ic ? <Ic /> : null}</span>
                    <span className="cli-name">{p.title}</span>
                    {window.isHidden(p.id) && <span className="cli-tag hidden">Hidden</span>}
                    {p.draft && <span className="cli-dot draft" title="Draft" />}
                  </button>
                );
              })}
            </div>
          )}
        </aside>

        {/* editor */}
        <main className="con-editor">
          {!draft ? (
            <div className="con-empty">
              <I.doc />
              <div>Select a page to edit, or create a new one.</div>
            </div>
          ) : (
            <>
              <div className="con-editor-head">
                <h2>{draft._new ? (draft.kind === "devlog" ? "New devlog entry" : "New page") : (draft.title || "Untitled")}</h2>
                {!draft._new && <span className="con-ver" title={"Version " + (draft.version || "1.0")}>v{draft.version || "1.0"}</span>}
                <span className={"con-status " + (dirty ? "dirty" : "saved")}>{saving ? "Saving…" : (dirty ? "Unsaved" : "Published")}</span>
                <button className="con-savebtn" onClick={doSave} disabled={!canSave || !dirty || saving}><I.check /> {saving ? "Saving…" : (draft._new ? "Publish" : "Save")}</button>
              </div>

              <div className="con-section-label">{draft.kind === "devlog" ? "Entry settings" : "Page settings"}</div>
              <Text label="Title" value={draft.title} onChange={(v) => patch({ title: v })} placeholder="e.g. Risk Assessment Workflow" />
              {draft.kind === "devlog" ? (
                <>
                  <div className="field-row">
                    <div className="field">
                      <label>Date</label>
                      <input className="inp" type="date" value={draft.date || ""} onChange={(e) => patch({ date: e.target.value })} />
                      <div className="field-hint">Orders the feed (newest first) and groups entries by month.</div>
                    </div>
                    <div className="field">
                      <label>Entry ID (URL)</label>
                      <input className="inp" value={draft.id} disabled={!draft._new}
                        placeholder={draft.title ? slugify(draft.title) : "auto-from-title"}
                        onChange={(e) => patch({ id: e.target.value })} />
                      <div className="field-hint">{draft._new ? `#/devlog/${idForSave() || "…"}` : "Fixed once published."}</div>
                    </div>
                  </div>
                  <Text label="Tags" value={(draft.tags || []).join(", ")} onChange={(v) => patch({ tags: v.split(",").map((s) => s.trim()).filter(Boolean) })} placeholder="Machinery, CE marking" hint="Comma-separated. Shown as chips on the entry and feed." />
                  <CoverField src={draft.cover} onChange={(src) => patch({ cover: src })} />
                  <Text label="Summary (lede)" area rows={2} value={draft.lede} onChange={(v) => patch({ lede: v })} placeholder="One-line summary shown on the feed and under the title." />
                  <div className="field" style={{ maxWidth: 240 }}>
                    <label>Fallback icon (used when there's no cover)</label>
                    <select className="sel" value={draft.iconKey} onChange={(e) => patch({ iconKey: e.target.value })}>
                      <option value="none">— none —</option>
                      {ICON_OPTIONS.map((k) => <option key={k} value={k}>{k}</option>)}
                    </select>
                  </div>
                  <Toggle label="Mark as draft (shows a Draft badge)" on={!!draft.draft} onChange={(v) => patch({ draft: v })} />
                </>
              ) : (
                <>
                  <div className="field-row">
                    <div className="field">
                      <label>Group</label>
                      <select className="sel" value={draft.group} onChange={(e) => patch({ group: e.target.value, parent: undefined })}>
                        {draft.group && groupOptions.indexOf(draft.group) === -1 && <option value={draft.group}>{draft.group}</option>}
                        {groupOptions.map((g) => <option key={g} value={g}>{g}</option>)}
                      </select>
                      <div className="field-hint">Sections are managed in <strong>Groups</strong> (top of the list). Changing the group clears the parent.</div>
                    </div>
                    <div className="field">
                      <label>Page ID (URL)</label>
                      <input className="inp" value={draft._new ? draft.id : draft.id} disabled={!draft._new}
                        placeholder={draft.title ? slugify(draft.title) : "auto-from-title"}
                        onChange={(e) => patch({ id: e.target.value })} />
                      <div className="field-hint">{draft._new ? `#/docs/${idForSave() || "…"}` : "Fixed for built-in pages."}</div>
                    </div>
                  </div>
                  {(() => { const opts = parentCandidates(draft); return (
                    <div className="field">
                      <label>Parent page (optional)</label>
                      <select className="sel" value={(draft.parent && opts.some((o) => o.id === draft.parent)) ? draft.parent : ""} onChange={(e) => patch({ parent: e.target.value || undefined })}>
                        <option value="">— None (top level) —</option>
                        {opts.map((p) => <option key={p.id} value={p.id}>{p.title.replace(/ —.*$/, "")}</option>)}
                      </select>
                      <div className="field-hint">Nest this page beneath another in the same group — it appears indented in the sidebar. Re-parent by changing the group or this field.</div>
                    </div>
                  ); })()}
                  <Text label="Summary (lede)" area rows={2} value={draft.lede} onChange={(v) => patch({ lede: v })} placeholder="One-line description under the title." />
                  <div className="field-row-3">
                    <div className="field">
                      <label>Icon</label>
                      <select className="sel" value={draft.iconKey} onChange={(e) => patch({ iconKey: e.target.value })}>
                        <option value="none">— none —</option>
                        {ICON_OPTIONS.map((k) => <option key={k} value={k}>{k}</option>)}
                      </select>
                    </div>
                    <Text label="Difficulty / tag" value={draft.meta?.difficulty} onChange={(v) => patchMeta({ difficulty: v })} placeholder="Reference" />
                    <Text label="Read time" value={draft.meta?.time} onChange={(v) => patchMeta({ time: v })} placeholder="8 min read" />
                  </div>
                  <div className="field" style={{ maxWidth: 240 }}>
                    <label>Scripts documented</label>
                    <input className="inp" type="number" min="0" value={draft.meta?.scripts ?? 0}
                      onChange={(e) => patchMeta({ scripts: Math.max(0, parseInt(e.target.value, 10) || 0) })} />
                    <div className="field-hint">Shows as the “{draft.meta?.scripts || 0} script{(draft.meta?.scripts === 1) ? "" : "s"}” chip on the page. Set to 0 to hide the chip entirely.</div>
                  </div>
                  <Toggle label="Mark as draft (shows a Draft badge)" on={!!draft.draft} onChange={(v) => patch({ draft: v })} />
                </>
              )}

              {!draft._new && (
                <div className="field" style={{ marginTop: 8 }}>
                  <div className="switch-row">
                    <button type="button" className={"switch" + (!window.isHidden(draft.id) ? " on" : "")} onClick={() => toggleHidden(draft.id)} aria-pressed={!window.isHidden(draft.id)} />
                    <label style={{ margin: 0 }}>Visible on site{window.isHidden(draft.id) ? " — currently hidden" : ""}</label>
                  </div>
                  <div className="field-hint">{window.BASE_IDS.has(draft.id) ? "Hide this built-in page from the live site — or remove it entirely with “Delete template” below. Takes effect immediately for all visitors." : "Hide this from the live site without deleting it. Takes effect immediately."}</div>
                </div>
              )}

              <div className="con-section-label">Content</div>
              <BlocksEditor blocks={draft.blocks} onChange={(blocks) => { setDraft((d) => ({ ...d, blocks })); setDirty(true); }} />

              {!draft._new && <RevisionHistory draft={draft} onRestore={restoreRevision} />}

              <div style={{ marginTop: 28, display: "flex", gap: 10, flexWrap: "wrap" }}>
                {draft._new ? (
                  <button className="con-topbtn danger" onClick={doDelete}><I.x /> Discard</button>
                ) : (
                  <button className="con-topbtn danger" onClick={doDelete}><I.x /> {draft.kind === "devlog" ? "Delete entry" : "Delete page"}</button>
                )}
                {!draft._new && !dirty && <a className="con-topbtn" href={(draft.kind === "devlog" ? "#/devlog/" : "#/docs/") + draft.id} onClick={onExit}><I.arrowR /> {draft.kind === "devlog" ? "Open live entry" : "Open live page"}</a>}
              </div>
            </>
          )}
        </main>

        {/* preview */}
        <aside className="con-preview">
          <div className="con-preview-bar"><span className="live-dot" /> Live preview <span className="cpb-hint">— click a block to find it</span></div>
          <div className="con-preview-inner">
            {draft ? <Preview draft={draft} /> : <div className="con-empty" style={{ paddingTop: 60 }}><I.image /><div>Nothing selected</div></div>}
          </div>
        </aside>
      </div>

      {toast && <div className="con-toast"><I.check /> {toast}</div>}
    </div>
  );
}

/* =================== Admin root =================== */
function Admin({ onExit, theme, setTheme }) {
  const cloud = window.CLOUD;
  const [authed, setAuthed] = aS(() => cloud ? false : sessionStorage.getItem("konform-docs-auth") === "1");
  const [checking, setChecking] = aS(cloud);

  // cloud: confirm a stored session token is still valid before showing the editor
  aE(() => {
    if (!cloud) return;
    let alive = true;
    window.adminPing().then((ok) => {
      if (!alive) return;
      setAuthed(ok);
      setChecking(false);
    });
    return () => { alive = false; };
  }, []);

  const doLogout = async () => {
    if (cloud) { window.adminLogout(); setAuthed(false); }
    else { sessionStorage.removeItem("konform-docs-auth"); setAuthed(false); }
  };

  // Auto-logout after a period of inactivity (0 = off).
  aE(() => {
    if (!authed) return;
    const mins = window.cfg ? parseInt(window.cfg("autoLogoutMin"), 10) || 0 : 0;
    if (mins <= 0) return;
    let timer;
    const reset = () => { clearTimeout(timer); timer = setTimeout(() => { doLogout(); }, mins * 60000); };
    const evts = ["mousedown", "keydown", "scroll", "touchstart", "mousemove"];
    evts.forEach((e) => window.addEventListener(e, reset, { passive: true }));
    reset();
    return () => { clearTimeout(timer); evts.forEach((e) => window.removeEventListener(e, reset)); };
  }, [authed]);

  if (checking) {
    return <div className="login-screen"><div className="login-bg" /><div className="con-empty" style={{ position: "relative" }}><I.ring style={{ width: 34, height: 34, opacity: 0.5 }} /><div>Connecting…</div></div></div>;
  }
  if (!authed) {
    return <Login theme={theme} setTheme={setTheme} onExit={onExit}
      onAuth={() => { if (!cloud) sessionStorage.setItem("konform-docs-auth", "1"); setAuthed(true); }} />;
  }
  return <Console theme={theme} setTheme={setTheme} onExit={onExit} onLogout={doLogout} />;
}

window.Admin = Admin;
