// Main App — Supabase-backed

const {
  useState: useStateA,
  useEffect: useEffectA,
  useMemo: useMemoA,
  useCallback: useCallbackA,
  useRef: useRefA,
} = React;

// ── Loading screen ─────────────────────────────────────────────────────────

function LoadingScreen() {
  return (
    <div className="loading-screen">
      <img src="assets/umn-logo.png" alt="University of Minnesota" width="200" />
      <div className="loading-spinner" aria-label="Loading…"></div>
      <p>Loading AI Hub Sitemap…</p>
    </div>
  );
}

// ── Error screen ───────────────────────────────────────────────────────────

function ErrorScreen({ error }) {
  return (
    <div className="error-screen">
      <img src="assets/umn-logo.png" alt="University of Minnesota" width="200" />
      <h2>Could not load sitemap data</h2>
      <p>{error}</p>
      <p style={{ fontSize: "0.85rem", color: "#666", marginTop: "1rem" }}>
        Check that your Supabase URL and anon key are filled in correctly in <code>config.js</code>,
        and that the schema has been applied in your Supabase project.
      </p>
      <button className="btn-save" style={{ marginTop: "1.5rem" }} onClick={() => window.location.reload()}>
        Try again
      </button>
    </div>
  );
}

// ── User identity banner ───────────────────────────────────────────────────

function UserBanner() {
  const [editing, setEditing] = useStateA(() => {
    const name = localStorage.getItem("aihub_name");
    return !name; // show edit form on first visit
  });
  const [name, setName] = useStateA(() => localStorage.getItem("aihub_name") || "");
  const [role, setRole] = useStateA(() => localStorage.getItem("aihub_role") || "Reviewer");

  const save = () => {
    const trimmed = name.trim() || "Anonymous";
    localStorage.setItem("aihub_name", trimmed);
    localStorage.setItem("aihub_role", role.trim() || "Reviewer");
    setName(trimmed);
    setEditing(false);
  };

  const displayName = localStorage.getItem("aihub_name") || "Anonymous";
  const displayRole = localStorage.getItem("aihub_role") || "Reviewer";

  if (!editing) {
    return (
      <div className="user-banner">
        <span>
          Commenting as <strong>{displayName}</strong>
          {displayRole ? <> · {displayRole}</> : null}
        </span>
        <button className="btn-save" style={{ marginLeft: "1rem", padding: "2px 10px", fontSize: "0.78rem" }} onClick={() => setEditing(true)}>
          Change
        </button>
      </div>
    );
  }

  return (
    <div className="user-banner editing">
      <span style={{ marginRight: "0.5rem" }}>Your name:</span>
      <input
        className=""
        value={name}
        onChange={(e) => setName(e.target.value)}
        onKeyDown={(e) => e.key === "Enter" && save()}
        placeholder="Your name"
        autoFocus
        style={{ width: "140px" }}
      />
      <span style={{ margin: "0 0.5rem" }}>Role:</span>
      <input
        value={role}
        onChange={(e) => setRole(e.target.value)}
        onKeyDown={(e) => e.key === "Enter" && save()}
        placeholder="e.g. UX Lead"
        style={{ width: "120px" }}
      />
      <button className="btn-save" style={{ marginLeft: "0.75rem" }} onClick={save}>
        Save
      </button>
    </div>
  );
}

// ── Main App ───────────────────────────────────────────────────────────────

function App() {
  const data = window.SITEMAP;

  // ── Loading / error state ─────────────────────────────────────────────
  const [isLoading, setIsLoading] = useStateA(true);
  const [loadError, setLoadError] = useStateA(null);
  const [isSyncing, setIsSyncing] = useStateA(false);

  // ── Mutable graph — start empty, populated from Supabase ──────────────
  const [nodes, setNodes] = useStateA({});
  const [childOrder, setChildOrder] = useStateA({});
  const [commentsByNode, setCommentsByNode] = useStateA({});
  const [suggestionsByContent, setSuggestionsByContent] = useStateA({});
  const [functionalityByContent, setFunctionalityByContent] = useStateA({});
  const [votes, setVotes] = useStateA({});

  // Refs for latest state (avoid stale closures in drag callbacks)
  const nodesRef = useRefA(nodes);
  const childOrderRef = useRefA(childOrder);
  useEffectA(() => { nodesRef.current = nodes; }, [nodes]);
  useEffectA(() => { childOrderRef.current = childOrder; }, [childOrder]);

  // ── Load from Supabase on mount ───────────────────────────────────────
  const applyData = (result) => {
    setNodes(result.nodes);
    setChildOrder(result.childOrder);
    setCommentsByNode(result.commentsByNode);
    setSuggestionsByContent(result.suggestionsByContent);
    setFunctionalityByContent(result.functionalityByContent);
    // Reset open map
    setOpenMap(() => {
      const m = {};
      for (const id of Object.keys(result.nodes)) {
        if (id.startsWith("page:") || id.startsWith("sec:")) m[id] = false;
      }
      return m;
    });
  };

  useEffectA(() => {
    if (!window.loadAllData) {
      // Supabase not configured — fall back to local data
      const initial = window.normalize(data);
      setNodes({ ...initial.nodes });
      const o = {};
      for (const [k, v] of Object.entries(initial.children)) o[k] = v.slice();
      setChildOrder(o);
      const c = {};
      for (const [k, v] of Object.entries(initial.commentsByNode)) c[k] = v.slice();
      setCommentsByNode(c);
      const sug = {};
      for (const [k, list] of Object.entries(data.suggestionsByContent || {})) {
        sug[k] = list.map((s) => ({ ...s, comments: (s.comments || []).slice() }));
      }
      setSuggestionsByContent(sug);
      const fnc = {};
      for (const [k, list] of Object.entries(data.functionalityByContent || {})) {
        fnc[k] = list.map((n) => ({ ...n, comments: (n.comments || []).slice() }));
      }
      setFunctionalityByContent(fnc);
      setIsLoading(false);
      return;
    }

    window.loadAllData()
      .then((result) => {
        applyData(result);
        setIsLoading(false);
      })
      .catch((err) => {
        setLoadError(err.message || "Unknown error");
        setIsLoading(false);
      });
  }, []);

  // ── Sync (re-fetch from Supabase) ─────────────────────────────────────
  const handleSync = useCallbackA(() => {
    if (!window.loadAllData || isSyncing) return;
    setIsSyncing(true);
    window.loadAllData()
      .then((result) => {
        applyData(result);
        setIsSyncing(false);
      })
      .catch((err) => {
        console.error("Sync failed:", err);
        setIsSyncing(false);
      });
  }, [isSyncing]);

  // ── Open/closed map ───────────────────────────────────────────────────
  const [openMap, setOpenMap] = useStateA({});

  const [selectedId, setSelectedId] = useStateA(null);
  const [searchRaw, setSearchRaw] = useStateA("");
  const search = searchRaw.trim().toLowerCase();
  const [typeFilter, setTypeFilter] = useStateA(() => new Set());
  const [statusFilter, setStatusFilter] = useStateA(() => new Set());
  const [commentOnly, setCommentOnly] = useStateA(false);
  const [activeBand, setActiveBand] = useStateA("cat:entry");
  const [sbCollapsed, setSbCollapsed] = useStateA(false);

  // ── Drag reorder ──────────────────────────────────────────────────────
  window.dragState.onDrop = (fromId, toId) => {
    if (fromId === toId) return;
    const currentNodes = nodesRef.current;
    const currentChildOrder = childOrderRef.current;
    const fromParent = currentNodes[fromId]?.parentId;
    const toParent = currentNodes[toId]?.parentId;
    if (!fromParent || fromParent !== toParent) return;

    setChildOrder((prev) => {
      const list = (prev[fromParent] || []).slice();
      const fromIdx = list.indexOf(fromId);
      const toIdx = list.indexOf(toId);
      if (fromIdx === -1 || toIdx === -1) return prev;
      list.splice(fromIdx, 1);
      const newToIdx = list.indexOf(toId);
      list.splice(newToIdx, 0, fromId);
      const next = { ...prev, [fromParent]: list };
      // Persist to Supabase
      if (window.DB) window.DB.upsertChildOrder(fromParent, list);
      return next;
    });
  };

  // ── Toggle expand ─────────────────────────────────────────────────────
  const handleToggle = useCallbackA((id) => {
    setOpenMap((m) => ({ ...m, [id]: !m[id] }));
  }, []);

  const handleSelect = useCallbackA((id) => {
    setSelectedId(id);
    const n = nodesRef.current[id];
    if (!n) return;
    if (n.type === "section") {
      setOpenMap((m) => ({ ...m, [n.parentId]: true }));
    } else if (n.type === "content") {
      const sec = nodesRef.current[n.parentId];
      setOpenMap((m) => ({ ...m, [sec?.parentId]: true, [n.parentId]: true }));
    }
  }, []);

  const handleNavigate = useCallbackA((id) => {
    handleSelect(id);
    const n = nodesRef.current[id];
    const targetId =
      n.type === "page" ? n.id
      : n.type === "section" ? n.parentId
      : nodesRef.current[n.parentId]?.parentId;
    requestAnimationFrame(() => {
      const el = document.getElementById(`node-${targetId}`);
      if (el) {
        const top = el.getBoundingClientRect().top + window.scrollY - 130;
        window.scrollTo({ top, behavior: "smooth" });
      }
    });
  }, [handleSelect]);

  const handleLocate = useCallbackA((id) => {
    const n = nodesRef.current[id];
    if (!n) return;
    setSelectedId(id);
    setOpenMap((m) => {
      const next = { ...m };
      if (n.type === "page") {
        next[n.id] = true;
      } else if (n.type === "section") {
        next[n.parentId] = true;
        next[n.id] = true;
      } else if (n.type === "content") {
        const sec = nodesRef.current[n.parentId];
        next[sec?.parentId] = true;
        next[sec?.id] = true;
      }
      return next;
    });
    setTimeout(() => {
      const el = document.getElementById(`node-${id}`);
      if (el) {
        const top = el.getBoundingClientRect().top + window.scrollY - 140;
        window.scrollTo({ top, behavior: "smooth" });
        el.classList.add("locate-flash");
        setTimeout(() => el.classList.remove("locate-flash"), 1800);
      }
    }, 80);
  }, []);

  const getBreadcrumb = useCallbackA((id) => {
    const out = [];
    let cur = nodesRef.current[id];
    while (cur) {
      if (cur.type === "category") { out.unshift({ id: cur.id, title: cur.title }); break; }
      out.unshift({ id: cur.id, title: cur.title });
      cur = nodesRef.current[cur.parentId];
    }
    return out;
  }, []);

  // ── Comments ──────────────────────────────────────────────────────────
  const todayShort = () =>
    new Date().toLocaleDateString("en-US", { month: "short", day: "numeric" });

  const handleAddComment = useCallbackA((nodeId, body) => {
    const user = window.getCurrentUser ? window.getCurrentUser() : { author: "Anonymous", role: "" };
    const c = {
      id: `cm-${Date.now()}-${Math.random().toString(36).slice(2, 7)}`,
      author: user.author,
      role: user.role,
      date: todayShort(),
      body,
    };
    setCommentsByNode((m) => ({ ...m, [nodeId]: [...(m[nodeId] || []), c] }));
    if (window.DB) window.DB.addComment(nodeId, c);
  }, []);

  const handleDeleteComment = useCallbackA((nodeId, commentId) => {
    setCommentsByNode((m) => ({
      ...m,
      [nodeId]: (m[nodeId] || []).filter((c) => c.id !== commentId),
    }));
    if (window.DB) window.DB.deleteComment(commentId);
  }, []);

  // ── Graph mutations ───────────────────────────────────────────────────

  const handleAddPage = useCallbackA((catId) => {
    const id = `page:user-${window.uid()}`;
    const newPage = {
      id, type: "page",
      title: "Untitled page",
      url: "/untitled",
      status: "mvp",
      owner: "—",
      summary: "Describe this page's purpose.",
      ux: "",
      aeo: [],
      parentId: catId, categoryId: catId,
      commentCount: 0,
    };
    setNodes((m) => ({ ...m, [id]: newPage }));
    setChildOrder((c) => ({
      ...c,
      [catId]: [...(c[catId] || []), id],
      [id]: [],
      [`foldlist:${id}`]: [],
    }));
    setOpenMap((m) => ({ ...m, [id]: true }));
    setSelectedId(id);
    if (window.DB) {
      window.DB.upsertNode(newPage);
      window.DB.upsertChildOrder(catId, [...(childOrderRef.current[catId] || []), id]);
    }
    return id;
  }, []);

  const handleAddSection = useCallbackA((pageId) => {
    const id = `sec:user-${window.uid()}`;
    const pageNode = nodesRef.current[pageId];
    const newSec = {
      id, type: "section",
      title: "New section",
      summary: "",
      status: pageNode?.status || "mvp",
      parentId: pageId, pageId,
    };
    setNodes((m) => ({ ...m, [id]: newSec }));
    setChildOrder((c) => ({
      ...c,
      [pageId]: [...(c[pageId] || []), id],
      [id]: [],
    }));
    setOpenMap((m) => ({ ...m, [pageId]: true, [id]: true }));
    setSelectedId(id);
    if (window.DB) {
      window.DB.upsertNode(newSec);
      window.DB.upsertChildOrder(pageId, [...(childOrderRef.current[pageId] || []), id]);
    }
    return id;
  }, []);

  const handleAddContent = useCallbackA((sectionId, title = "New content item") => {
    const id = `con:user-${window.uid()}`;
    const sec = nodesRef.current[sectionId];
    const newCon = {
      id, type: "content",
      title,
      parentId: sectionId,
      sectionId,
      pageId: sec?.pageId,
    };
    setNodes((m) => ({ ...m, [id]: newCon }));
    setChildOrder((c) => ({
      ...c,
      [sectionId]: [...(c[sectionId] || []), id],
    }));
    setOpenMap((m) => ({ ...m, [sec?.pageId]: true, [sectionId]: true }));
    setSelectedId(id);
    if (window.DB) {
      window.DB.upsertNode(newCon);
      window.DB.upsertChildOrder(sectionId, [...(childOrderRef.current[sectionId] || []), id]);
    }
    return id;
  }, []);

  const handleAddFoldItem = useCallbackA((pageId, title = "New above-the-fold item") => {
    const id = `fold:user-${window.uid()}`;
    const newFold = {
      id, type: "content", subtype: "fold",
      title,
      parentId: pageId, pageId, sectionId: null,
    };
    setNodes((m) => ({ ...m, [id]: newFold }));
    setChildOrder((c) => {
      const key = `foldlist:${pageId}`;
      return { ...c, [key]: [...(c[key] || []), id] };
    });
    setOpenMap((m) => ({ ...m, [pageId]: true }));
    setSelectedId(id);
    if (window.DB) {
      window.DB.upsertNode(newFold);
      const key = `foldlist:${pageId}`;
      window.DB.upsertChildOrder(key, [...(childOrderRef.current[key] || []), id]);
    }
    return id;
  }, []);

  const handleDeleteNode = useCallbackA((nodeId) => {
    const n = nodesRef.current[nodeId];
    if (!n) return;

    const all = new Set([nodeId]);
    const stack = [nodeId];
    const curChildOrder = childOrderRef.current;
    while (stack.length) {
      const id = stack.pop();
      for (const cid of (curChildOrder[id] || [])) {
        if (!all.has(cid)) { all.add(cid); stack.push(cid); }
      }
      if (id.startsWith("page:")) {
        for (const fid of (curChildOrder[`foldlist:${id}`] || [])) {
          if (!all.has(fid)) all.add(fid);
        }
      }
    }

    setNodes((m) => {
      const next = { ...m };
      for (const id of all) delete next[id];
      return next;
    });
    setChildOrder((c) => {
      const next = { ...c };
      const parentKey = n.subtype === "fold" ? `foldlist:${n.pageId}` : n.parentId;
      if (next[parentKey]) {
        next[parentKey] = next[parentKey].filter((x) => x !== nodeId);
        if (window.DB) window.DB.upsertChildOrder(parentKey, next[parentKey]);
      }
      for (const id of all) {
        delete next[id];
        delete next[`foldlist:${id}`];
      }
      return next;
    });
    setOpenMap((m) => {
      const next = { ...m };
      for (const id of all) delete next[id];
      return next;
    });
    setSuggestionsByContent((m) => {
      const next = { ...m };
      for (const id of all) delete next[id];
      return next;
    });
    setFunctionalityByContent((m) => {
      const next = { ...m };
      for (const id of all) delete next[id];
      return next;
    });
    setCommentsByNode((m) => {
      const next = { ...m };
      for (const id of all) delete next[id];
      return next;
    });
    setSelectedId((cur) => (all.has(cur) ? null : cur));
    if (window.DB) window.DB.deleteNodes([...all]);
  }, []);

  // ── Suggestions ───────────────────────────────────────────────────────

  const handleSubmitSuggestion = useCallbackA((nodeId, body) => {
    const user = window.getCurrentUser ? window.getCurrentUser() : { author: "Anonymous", role: "" };
    const s = {
      id: `sg-${Date.now()}-${Math.random().toString(36).slice(2, 7)}`,
      author: user.author,
      role: user.role,
      date: todayShort(),
      body,
      upvotes: 0,
      comments: [],
    };
    setSuggestionsByContent((m) => ({ ...m, [nodeId]: [...(m[nodeId] || []), s] }));
    if (window.DB) window.DB.addSuggestion(nodeId, s);
  }, []);

  const handleUpvoteSuggestion = useCallbackA((nodeId, suggestionId) => {
    setVotes((v) => {
      const next = { ...v };
      const hadVote = !!next[suggestionId];
      if (hadVote) delete next[suggestionId];
      else next[suggestionId] = true;
      setSuggestionsByContent((m) => ({
        ...m,
        [nodeId]: (m[nodeId] || []).map((s) => {
          if (s.id !== suggestionId) return s;
          const newUpvotes = Math.max(0, s.upvotes + (hadVote ? -1 : 1));
          if (window.DB) window.DB.updateUpvotes(suggestionId, newUpvotes);
          return { ...s, upvotes: newUpvotes };
        }),
      }));
      return next;
    });
  }, []);

  const handleDeleteSuggestion = useCallbackA((nodeId, suggestionId) => {
    setSuggestionsByContent((m) => ({
      ...m,
      [nodeId]: (m[nodeId] || []).filter((s) => s.id !== suggestionId),
    }));
    setVotes((v) => { const n = { ...v }; delete n[suggestionId]; return n; });
    if (window.DB) window.DB.deleteSuggestion(suggestionId);
  }, []);

  const handleAddSuggestionComment = useCallbackA((nodeId, suggestionId, body) => {
    const user = window.getCurrentUser ? window.getCurrentUser() : { author: "Anonymous", role: "" };
    const c = {
      id: `sgc-${Date.now()}-${Math.random().toString(36).slice(2, 7)}`,
      author: user.author, role: user.role, date: todayShort(), body,
    };
    setSuggestionsByContent((m) => ({
      ...m,
      [nodeId]: (m[nodeId] || []).map((s) =>
        s.id === suggestionId ? { ...s, comments: [...(s.comments || []), c] } : s
      ),
    }));
    if (window.DB) window.DB.addSuggestionComment(suggestionId, c);
  }, []);

  const handleDeleteSuggestionComment = useCallbackA((nodeId, suggestionId, commentId) => {
    setSuggestionsByContent((m) => ({
      ...m,
      [nodeId]: (m[nodeId] || []).map((s) =>
        s.id === suggestionId
          ? { ...s, comments: (s.comments || []).filter((c) => c.id !== commentId) }
          : s
      ),
    }));
    if (window.DB) window.DB.deleteSuggestionComment(commentId);
  }, []);

  // ── Functionality notes ───────────────────────────────────────────────

  const handleAddFunctionality = useCallbackA((nodeId, { kind, body, targetNodeId }) => {
    const user = window.getCurrentUser ? window.getCurrentUser() : { author: "Anonymous", role: "" };
    const n = {
      id: `fn-${Date.now()}-${Math.random().toString(36).slice(2, 7)}`,
      author: user.author, role: user.role, date: todayShort(),
      kind, body, targetNodeId: targetNodeId || null,
      comments: [],
    };
    setFunctionalityByContent((m) => ({ ...m, [nodeId]: [...(m[nodeId] || []), n] }));
    if (window.DB) window.DB.addFunctionalityNote(nodeId, n);
  }, []);

  const handleDeleteFunctionality = useCallbackA((nodeId, noteId) => {
    setFunctionalityByContent((m) => ({
      ...m,
      [nodeId]: (m[nodeId] || []).filter((n) => n.id !== noteId),
    }));
    if (window.DB) window.DB.deleteFunctionalityNote(noteId);
  }, []);

  const handleAddFunctionalityComment = useCallbackA((nodeId, noteId, body) => {
    const user = window.getCurrentUser ? window.getCurrentUser() : { author: "Anonymous", role: "" };
    const c = {
      id: `fnc-${Date.now()}-${Math.random().toString(36).slice(2, 7)}`,
      author: user.author, role: user.role, date: todayShort(), body,
    };
    setFunctionalityByContent((m) => ({
      ...m,
      [nodeId]: (m[nodeId] || []).map((n) =>
        n.id === noteId ? { ...n, comments: [...(n.comments || []), c] } : n
      ),
    }));
    if (window.DB) window.DB.addFunctionalityComment(noteId, c);
  }, []);

  const handleDeleteFunctionalityComment = useCallbackA((nodeId, noteId, commentId) => {
    setFunctionalityByContent((m) => ({
      ...m,
      [nodeId]: (m[nodeId] || []).map((n) =>
        n.id === noteId
          ? { ...n, comments: (n.comments || []).filter((c) => c.id !== commentId) }
          : n
      ),
    }));
    if (window.DB) window.DB.deleteFunctionalityComment(commentId);
  }, []);

  // ── Derived state ─────────────────────────────────────────────────────

  const pageNodes = useMemoA(() => Object.values(nodes).filter((n) => n.type === "page"), [nodes]);

  const toggleSetMember = (setState, val) => {
    setState((s) => {
      const next = new Set(s);
      if (next.has(val)) next.delete(val); else next.add(val);
      return next;
    });
  };

  const stats = useMemoA(() => {
    let pages = 0, sections = 0, content = 0, comments = 0;
    for (const id of Object.keys(nodes)) {
      if (id.startsWith("page:")) pages++;
      else if (id.startsWith("sec:")) sections++;
      else if (id.startsWith("con:")) content++;
    }
    for (const v of Object.values(commentsByNode)) comments += v.length;
    return { pages, sections, content, comments };
  }, [nodes, commentsByNode]);

  const catCounts = useMemoA(() => {
    const out = {};
    for (const cat of data.categories) {
      const key = `cat:${cat.id}`;
      out[key] = (childOrder[key] || []).length;
    }
    return out;
  }, [data, childOrder]);

  // ── Expand / collapse ─────────────────────────────────────────────────

  const expandAll = () => {
    setOpenMap((m) => {
      const next = { ...m };
      for (const id of Object.keys(nodes)) {
        if (id.startsWith("page:") || id.startsWith("sec:")) next[id] = true;
      }
      return next;
    });
  };
  const collapseAll = () => {
    setOpenMap((m) => {
      const next = { ...m };
      for (const id of Object.keys(nodes)) {
        if (id.startsWith("page:") || id.startsWith("sec:")) next[id] = false;
      }
      return next;
    });
  };
  const defaultExpand = () => {
    setOpenMap(() => {
      const m = {};
      for (const id of Object.keys(nodes)) {
        if (id.startsWith("page:") || id.startsWith("sec:")) m[id] = false;
      }
      return m;
    });
  };

  // ── Scrollspy ─────────────────────────────────────────────────────────

  useEffectA(() => {
    const handler = () => {
      let best = null, bestDist = Infinity;
      for (const cat of data.categories) {
        const el = document.getElementById(`band-${cat.id}`);
        if (!el) continue;
        const rect = el.getBoundingClientRect();
        const dist = Math.abs(rect.top - 140);
        if (rect.top < window.innerHeight && dist < bestDist) {
          bestDist = dist; best = `cat:${cat.id}`;
        }
      }
      if (best) setActiveBand(best);
    };
    window.addEventListener("scroll", handler, { passive: true });
    handler();
    return () => window.removeEventListener("scroll", handler);
  }, []);

  const jumpToBand = (catId) => {
    const id = catId.replace(/^cat:/, "");
    const el = document.getElementById(`band-${id}`);
    if (el) {
      const top = el.getBoundingClientRect().top + window.scrollY - 120;
      window.scrollTo({ top, behavior: "smooth" });
    }
  };

  // ── Early returns ─────────────────────────────────────────────────────

  if (isLoading) return <LoadingScreen />;
  if (loadError) return <ErrorScreen error={loadError} />;

  // ── Render ────────────────────────────────────────────────────────────

  return (
    <div className="app">
      {/* UMN GLOBAL UTILITY STRIP */}
      <div className="umn-utility">
        <div className="umn-utility-inner">
          <a className="umn-wordmark" href="#" aria-label="University of Minnesota — Driven to Discover">
            <img src="assets/umn-logo.png" alt="University of Minnesota — Driven to Discover" width="333" height="35" />
          </a>
        </div>
      </div>

      {/* UNIT HEADER */}
      <header className="umn-unit-header">
        <div className="umn-unit-inner">
          <a className="umn-unit-mark" href="#" aria-label="AI Hub home">
            <span className="umn-unit-text">
              <span className="umn-unit-title">AI Hub</span>
            </span>
          </a>
          <div className="umn-unit-meta">
            <span className="umn-unit-tag">Sitemap · Working draft v0.1</span>
            <span className="umn-unit-date">Updated {data.meta.lastUpdated}</span>
          </div>
        </div>
        <div className="umn-unit-stripe" aria-hidden="true"></div>
      </header>

      {/* SECONDARY NAV */}
      <div className="topnav">
        <div className="topnav-inner">
          <div className="topnav-eyebrow">
            <span className="topnav-eyebrow-label">In this sitemap</span>
          </div>
          <nav className="nav-pills" aria-label="Jump to category">
            {data.categories.map((cat) => (
              <button
                key={cat.id}
                className={`nav-pill ${activeBand === `cat:${cat.id}` ? "active" : ""}`}
                onClick={() => jumpToBand(`cat:${cat.id}`)}
              >
                {cat.label}<span className="count">{catCounts[`cat:${cat.id}`]}</span>
              </button>
            ))}
          </nav>
          <div className="nav-tools">
            <div className="search-wrap">
              <window.SearchIcon />
              <input
                className="search-input"
                placeholder="Search pages, sections, content…"
                value={searchRaw}
                onChange={(e) => setSearchRaw(e.target.value)}
              />
            </div>
            {window.loadAllData && (
              <button
                className={`sync-btn ${isSyncing ? "syncing" : ""}`}
                onClick={handleSync}
                title="Sync changes from other users"
                aria-label="Sync"
              >
                <svg width="14" height="14" viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg" aria-hidden="true">
                  <path d="M13.65 2.35A8 8 0 1 0 15 8h-2a6 6 0 1 1-1.76-4.24L9 6h6V0l-1.35 2.35z" fill="currentColor"/>
                </svg>
                {isSyncing ? "Syncing…" : "Sync"}
              </button>
            )}
          </div>
        </div>

        {/* Filters */}
        <div className="filters">
          <div className="filters-inner">
            <div className="filter-group">
              <span className="filter-label">Show</span>
              {["page", "section", "content"].map((t) => (
                <button
                  key={t}
                  className={`chip ${typeFilter.has(t) ? "on" : ""}`}
                  data-tone={t}
                  onClick={() => toggleSetMember(setTypeFilter, t)}
                >
                  <span className="chip-dot"></span>
                  {t === "page" ? "Pages" : t === "section" ? "Sections" : "Content needs"}
                </button>
              ))}
            </div>

            <div className="filter-group">
              <span className="filter-label">Phase</span>
              {["mvp", "phase2"].map((s) => (
                <button
                  key={s}
                  className={`chip ${statusFilter.has(s) ? "on" : ""}`}
                  data-tone={s}
                  onClick={() => toggleSetMember(setStatusFilter, s)}
                >
                  <span className="chip-dot"></span>
                  {s === "mvp" ? "Phase 1 (MVP)" : "Phase 2"}
                </button>
              ))}
            </div>

            <div className="filter-group">
              <button
                className={`chip ${commentOnly ? "on" : ""}`}
                onClick={() => setCommentOnly((v) => !v)}
                title="Show only items with open comments"
              >
                <window.CommentGlyph />
                With comments only
              </button>
            </div>

            <div className="filter-group bulk-controls">
              <span className="filter-label">Layout</span>
              <button className="chip" onClick={expandAll}>Expand all</button>
              <button className="chip" onClick={defaultExpand}>Default</button>
              <button className="chip" onClick={collapseAll}>Collapse all</button>
            </div>

            <div className="filter-stats">
              {stats.pages} pages · {stats.sections} sections · {stats.content} content needs · {stats.comments} comments
            </div>
          </div>
        </div>
      </div>

      {/* MAIN */}
      <div className={`main ${sbCollapsed ? "sb-collapsed" : ""}`}>
        <div className="canvas">
          <header className="canvas-header">
            <h1 className="canvas-title">Information architecture, <em>visualised</em></h1>
            <p className="canvas-sub">
              A working draft of the AI Hub. Sixteen pages, organised by audience pathways and topical functional areas — built to surface answers
              for humans <em>and</em> retrieval engines. Drag to reorder within a parent, click anything to inspect and discuss, collapse sections to focus.
            </p>
            <div className="canvas-meta">
              <span>Phase 1 launch · <strong>Fall 2026</strong></span>
              <span>Owner · <strong>{data.meta.ownerTeam}</strong></span>
              <span>Hosted on <strong>ai.umn.edu</strong></span>
            </div>

            <div className="legend">
              <span className="filter-label">Legend</span>
              <span className="legend-item"><span className="legend-glyph"><window.Glyph type="page" /></span> Page — full URL, owner, governance</span>
              <span className="legend-item"><span className="legend-glyph"><window.Glyph type="section" /></span> Section — block on a page, anchored</span>
              <span className="legend-item"><span className="legend-glyph"><window.Glyph type="content" /></span> Content need — module to author</span>
              <span className="legend-item" style={{ marginLeft: "auto", color: "var(--muted)" }}>Drag the dots to reorder · Click any item to open the inspector →</span>
            </div>
          </header>

          {data.categories.map((cat, ci) => {
            const catId = `cat:${cat.id}`;
            const pageIds = childOrder[catId] || [];
            return (
              <section key={cat.id} id={`band-${cat.id}`} className="band">
                <div className="band-head">
                  <div className="band-num">{String(ci + 1).padStart(2, "0")}</div>
                  <h2 className="band-label">{cat.label}</h2>
                  <p className="band-caption">{cat.caption}</p>
                </div>
                <div className="band-pages">
                  {pageIds.map((pid) => {
                    const node = nodes[pid];
                    if (!node) return null;
                    return (
                      <window.PageCard
                        key={pid}
                        node={node}
                        childOrder={childOrder}
                        nodes={nodes}
                        openMap={openMap}
                        onToggle={handleToggle}
                        selectedId={selectedId}
                        onSelect={handleSelect}
                        commentsByNode={commentsByNode}
                        parentId={catId}
                        dragOver={false}
                        suggestionsByContent={suggestionsByContent}
                        functionalityByContent={functionalityByContent}
                        onAddSection={handleAddSection}
                        onAddFoldItem={handleAddFoldItem}
                        onAddContent={handleAddContent}
                        onDragOverItem={() => {}}
                        onDropItem={(toId) => {
                          const from = window.dragState.current?.id;
                          if (from) window.dragState.onDrop?.(from, toId, false);
                          window.endDrag();
                        }}
                        search={search}
                        typeFilter={typeFilter}
                        statusFilter={statusFilter}
                        commentOnly={commentOnly}
                      />
                    );
                  })}
                  <button
                    className="add-page-btn"
                    onClick={() => handleAddPage(catId)}
                    title={`Add a page in ${cat.label}`}
                  >
                    <span className="add-glyph">+</span>
                    Add page to {cat.label}
                  </button>
                </div>
              </section>
            );
          })}

          <footer className="foot">
            <div>
              <strong>Drafting principles.</strong> Audience-first navigation · functional secondary nav for SEO/AEO · structured metadata on every node · governance baked in.
            </div>
            <div>
              <strong>Next.</strong> Wireframe homepage · finalise tool-detail template · partner ingestion for statewide inventory · AI assistant retrieval spec.
            </div>
          </footer>

          <window.DownloadPanel
            data={data}
            nodes={nodes}
            childOrder={childOrder}
            commentsByNode={commentsByNode}
            suggestionsByContent={suggestionsByContent}
            functionalityByContent={functionalityByContent}
          />
        </div>

        <window.Sidebar
          selectedId={selectedId}
          nodes={nodes}
          children={childOrder}
          commentsByNode={commentsByNode}
          onAddComment={handleAddComment}
          onDeleteComment={handleDeleteComment}
          onClearSelection={() => setSelectedId(null)}
          onNavigate={handleNavigate}
          onLocate={handleLocate}
          getBreadcrumb={getBreadcrumb}
          collapsed={sbCollapsed}
          onToggleCollapsed={() => setSbCollapsed((v) => !v)}
          suggestionsByContent={suggestionsByContent}
          functionalityByContent={functionalityByContent}
          votes={votes}
          pageNodes={pageNodes}
          onSubmitSuggestion={handleSubmitSuggestion}
          onUpvoteSuggestion={handleUpvoteSuggestion}
          onDeleteSuggestion={handleDeleteSuggestion}
          onAddSuggestionComment={handleAddSuggestionComment}
          onDeleteSuggestionComment={handleDeleteSuggestionComment}
          onAddFunctionality={handleAddFunctionality}
          onDeleteFunctionality={handleDeleteFunctionality}
          onAddFunctionalityComment={handleAddFunctionalityComment}
          onDeleteFunctionalityComment={handleDeleteFunctionalityComment}
          onDeleteNode={handleDeleteNode}
        />
      </div>

      {/* UMN FOOTER */}
      <footer className="umn-footer">
        <div className="umn-footer-inner">
          <a className="umn-footer-mark-link" href="#" aria-label="University of Minnesota">
            <img src="assets/umn-logo.png" alt="University of Minnesota — Driven to Discover" />
          </a>
        </div>
      </footer>

      <UserBanner />
    </div>
  );
}

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