/* ============================================================
   Mesper Docs — Confluence-style wiki (Phase 1). Spaces → page tree,
   document WYSIWYG, cross-tool backlinks, search, comments, versions,
   templates. Mounts at /docs. Loads AFTER screen-tasks/crm so it can
   reuse htmlToMarkdown / FormatToolbar / RichText / Icon / Button.
   ============================================================ */

/* ---- i18n (EN/DE, persisted; namespaced to avoid clashing with CRM) ---- */
const DOC_DICT = {
  en: {
    docs: "Docs", spaces: "Clients", recent: "Recent", favorites: "Favorites", search: "Search", searchPh: "Search docs…",
    newPage: "New page", newSpace: "New client", subpage: "Add subpage", untitled: "Untitled",
    folder: "Folder", folders: "Folders", newFolder: "New folder", newDoc: "New doc", noFolders: "No folders yet – create the first.", emptyFolder: "No documents yet.", documents: "Documents", clientEmpty: "Nothing here yet – add a folder.",
    trash: "Trash", trashHint: "Deleted items are kept for 30 days, then permanently removed.", trashEmpty: "Trash is empty.", deleteForever: "Delete forever", archive: "Archive", moveToTrash: "Move to trash", daysLeft: "days left", deletedBy: "Deleted by", confirmPurge: "Permanently delete this? It cannot be restored.", editClient: "Edit client",
    duplicate: "Duplicate", duplicateError: "Could not duplicate the page.",
    edit: "Edit", save: "Save", cancel: "Cancel", delete: "Delete", rename: "Rename", back: "Back", open: "Open",
    templates: "Templates", useTemplate: "From template", blankPage: "Blank page",
    backlinks: "Links", linkedFrom: "Linked from", linksTo: "Links to", noBacklinks: "No links yet.",
    comments: "Comments", addComment: "Add a comment…  ·  @ to mention", resolve: "Resolve", resolved: "Resolved", noComments: "No comments yet.",
    versions: "History", restore: "Restore", restored: "Restored", by: "by",
    createSpace: "Add client", spaceName: "Client name", spaceKey: "Key (short)", spaceDesc: "Description", color: "Color",
    confirmDelete: "Delete this page and all its subpages? This cannot be undone.", emptySpace: "No pages yet – create the first.", writeSomething: "Start writing…  ·  “/” soon, @ to link people, docs, companies, projects",
    tBold: "Bold", tItalic: "Italic", tUnderline: "Underline", tStrike: "Strikethrough", tH1: "Heading 1", tH2: "Heading 2", tH3: "Heading 3", tUl: "Bulleted list", tOl: "Numbered list", tQuote: "Quote", tCode: "Inline code", tCodeblock: "Code block", tLink: "Link", tTable: "Table", tImage: "Image", tMention: "Mention / link (@)",
    star: "Star", lastEdited: "Edited", noResults: "No results.", inDocs: "Mentioned in docs", searchAll: "Search all docs…",
    mDoc: "Docs", mCompany: "Companies", mProject: "Projects", mPerson: "People", writeComment: "Write a comment…",
    home: "Home", allPages: "pages", pageIcon: "Emoji", movePage: "Move", welcome: "Welcome to Docs", welcomeSub: "Your team wiki – SOPs, onboarding, know-how. Everything linkable to CRM, Tasks and people.",
    access: "Access", everyone: "Whole team", everyoneDesc: "Everyone at Mesper can view & edit.", specificPeople: "Restricted", specificDesc: "Only selected people can see this page.", whoCanAccess: "Who can access", applySubpages: "Also apply to all subpages",
    slashHint: "Insert…", sH1: "Heading 1", sH2: "Heading 2", sH3: "Heading 3", sUl: "Bulleted list", sOl: "Numbered list", sQuote: "Quote", sCodeblock: "Code block", sTable: "Table", sImage: "Image", sDivider: "Divider", sMention: "Mention / link",
    tH4: "Heading 4", tH5: "Heading 5", tPara: "Normal text", tStyle: "Text style", tMore: "More", tColor: "Text color", cDefault: "Default", tTask: "Task list", tEmoji: "Emoji", tDate: "Date", tUndo: "Undo", tRedo: "Redo", tPanel: "Panel", insertBtn: "Insert",
    sInfo: "Info panel", sSuccess: "Success panel", sWarning: "Warning panel", sNote: "Note panel", sDecision: "Decision", sTask: "Task list", sEmoji: "Emoji", sDate: "Date", sStatus: "Status", statusPh: "Status label",
    tRowAbove: "Row above", tRowBelow: "Row below", tColLeft: "Column left", tColRight: "Column right", tDelRow: "Delete row", tDelCol: "Delete column", tHeaderToggle: "Toggle header", tDelTable: "Delete table",
    share: "Share", copyLink: "Copy link", copied: "Copied", invite: "Invite", guests: "Guests", guestsInArea: "guests in this space",
    externalGuests: "External guests", guestEmail: "Email", guestName: "Name (optional)", noGuests: "No guests yet.", inviteErr: "Could not send invite.", area: "Space",
    guestHint: "Guests create their own account with email and password at app.mesper.de – use the invite text above to invite them.",
    inviteSent: "Invitation sent – the guest gets an email with a sign-in link.", inviteSavedNoMail: "Invitation saved. Mail limit reached – send the link again later.", sendLink: "Send link", linkSent: "Sent", sendFail: "Failed",
    inviteSavedCopyHint: "Invited. Copy the invite text and send it to the guest.", inviteMailSent: "Invitation sent – the guest received an email with a sign-in link.", statusRegistered: "Registered", statusPending: "Pending", sendReset: "Send password reset", resetSent: "Reset sent", resetFail: "Failed", copyInvite: "Copy invite text", inviteCopied: "Copied!",
    updateBtn: "Update", closeBtn: "Close", searchTree: "Search by title", byLine: "By", updatedLabel: "Updated", editedLabel: "Edited",
    create: "Create", pageItem: "Page", confirmDiscard: "Discard your changes?",
    tplMeeting: "Meeting notes", tplMeetingDesc: "Agenda, attendees, action items, decisions.", tplBlankDesc: "Start from a clean page.",
    activity: "Activity", activityEmpty: "No activity yet.", allSpaces: "All spaces", actAll: "All actions", actEdits: "Edits", actCreated: "Created", actTrash: "Deleted / Trash", actGuests: "Guests", dToday: "Today", dYesterday: "Yesterday",
    avPage: "page", avFolder: "folder", avSpace: "space",
    avaCreated: "created", avaUpdated: "edited", avaDuplicated: "duplicated", avaArchived: "archived", avaUnarchived: "unarchived", avaDeleted: "moved to trash", avaRestored: "restored", avaPurged: "permanently deleted",
    avgInvited: "invited guest", avgRemoved: "removed guest", avgLogin: "signed in",
    kindLabel: "Type", kindClient: "Client", kindInternal: "Internal",
    linksLabel: "Links", linkCrm: "CRM", linkBoard: "Board", createBoard: "Create board project",
  },
  de: {
    docs: "Docs", spaces: "Kunden", recent: "Zuletzt", favorites: "Favoriten", search: "Suche", searchPh: "Docs durchsuchen…",
    newPage: "Neue Seite", newSpace: "Neuer Kunde", subpage: "Unterseite", untitled: "Ohne Titel",
    folder: "Ordner", folders: "Ordner", newFolder: "Neuer Ordner", newDoc: "Neue Doku", noFolders: "Noch keine Ordner – leg den ersten an.", emptyFolder: "Noch keine Dokumente.", documents: "Dokumente", clientEmpty: "Noch nichts hier – leg einen Ordner an.",
    trash: "Papierkorb", trashHint: "Gelöschtes bleibt 30 Tage erhalten, dann wird es endgültig entfernt.", trashEmpty: "Papierkorb ist leer.", deleteForever: "Endgültig löschen", archive: "Archivieren", moveToTrash: "In den Papierkorb", daysLeft: "Tage übrig", deletedBy: "Gelöscht von", confirmPurge: "Endgültig löschen? Kann nicht wiederhergestellt werden.", editClient: "Kunde bearbeiten",
    duplicate: "Duplizieren", duplicateError: "Seite konnte nicht dupliziert werden.",
    edit: "Bearbeiten", save: "Speichern", cancel: "Abbrechen", delete: "Löschen", rename: "Umbenennen", back: "Zurück", open: "Öffnen",
    templates: "Vorlagen", useTemplate: "Aus Vorlage", blankPage: "Leere Seite",
    backlinks: "Verknüpfungen", linkedFrom: "Verlinkt von", linksTo: "Verlinkt auf", noBacklinks: "Noch keine Verknüpfungen.",
    comments: "Kommentare", addComment: "Kommentar hinzufügen…  ·  @ für Erwähnung", resolve: "Erledigen", resolved: "Erledigt", noComments: "Noch keine Kommentare.",
    versions: "Verlauf", restore: "Wiederherstellen", restored: "Wiederhergestellt", by: "von",
    createSpace: "Kunde anlegen", spaceName: "Kundenname", spaceKey: "Kürzel", spaceDesc: "Beschreibung", color: "Farbe",
    confirmDelete: "Diese Seite und alle Unterseiten löschen? Nicht rückgängig zu machen.", emptySpace: "Noch keine Seiten – leg die erste an.", writeSomething: "Schreib los…  ·  @ verlinkt Personen, Docs, Firmen, Projekte",
    tBold: "Fett", tItalic: "Kursiv", tUnderline: "Unterstrichen", tStrike: "Durchgestrichen", tH1: "Überschrift 1", tH2: "Überschrift 2", tH3: "Überschrift 3", tUl: "Aufzählung", tOl: "Nummerierte Liste", tQuote: "Zitat", tCode: "Code inline", tCodeblock: "Codeblock", tLink: "Link", tTable: "Tabelle", tImage: "Bild", tMention: "Erwähnen / verlinken (@)",
    star: "Favorit", lastEdited: "Bearbeitet", noResults: "Keine Treffer.", inDocs: "In Docs erwähnt", searchAll: "Alle Docs durchsuchen…",
    mDoc: "Docs", mCompany: "Firmen", mProject: "Projekte", mPerson: "Team", writeComment: "Kommentar schreiben…",
    home: "Start", allPages: "Seiten", pageIcon: "Emoji", movePage: "Verschieben", welcome: "Willkommen bei Docs", welcomeSub: "Euer Team-Wiki – SOPs, Onboarding, Wissen. Alles verlinkbar mit CRM, Tasks und Personen.",
    access: "Zugriff", everyone: "Ganzes Team", everyoneDesc: "Jeder bei Mesper kann sehen & bearbeiten.", specificPeople: "Eingeschränkt", specificDesc: "Nur ausgewählte Personen sehen diese Seite.", whoCanAccess: "Wer hat Zugriff", applySubpages: "Auch auf alle Unterseiten anwenden",
    slashHint: "Einfügen…", sH1: "Überschrift 1", sH2: "Überschrift 2", sH3: "Überschrift 3", sUl: "Aufzählung", sOl: "Nummerierte Liste", sQuote: "Zitat", sCodeblock: "Codeblock", sTable: "Tabelle", sImage: "Bild", sDivider: "Trennlinie", sMention: "Erwähnen / verlinken",
    tH4: "Überschrift 4", tH5: "Überschrift 5", tPara: "Normaler Text", tStyle: "Textstil", tMore: "Mehr", tColor: "Textfarbe", cDefault: "Standard", tTask: "Aufgabenliste", tEmoji: "Emoji", tDate: "Datum", tUndo: "Rückgängig", tRedo: "Wiederholen", tPanel: "Panel", insertBtn: "Einfügen",
    sInfo: "Info-Panel", sSuccess: "Erfolg-Panel", sWarning: "Warnung-Panel", sNote: "Notiz-Panel", sDecision: "Entscheidung", sTask: "Aufgabenliste", sEmoji: "Emoji", sDate: "Datum", sStatus: "Status", statusPh: "Status-Text",
    tRowAbove: "Zeile darüber", tRowBelow: "Zeile darunter", tColLeft: "Spalte links", tColRight: "Spalte rechts", tDelRow: "Zeile löschen", tDelCol: "Spalte löschen", tHeaderToggle: "Kopfzeile an/aus", tDelTable: "Tabelle löschen",
    share: "Teilen", copyLink: "Link kopieren", copied: "Kopiert", invite: "Einladen", guests: "Gäste", guestsInArea: "Gäste in diesem Bereich",
    externalGuests: "Externe Gäste", guestEmail: "E-Mail", guestName: "Name (optional)", noGuests: "Noch keine Gäste.", inviteErr: "Einladung fehlgeschlagen.", area: "Bereich",
    guestHint: "Gäste erstellen ihr Konto selbst mit E-Mail und Passwort auf app.mesper.de – nutze den Einladungstext oben, um sie einzuladen.",
    inviteSent: "Einladung gesendet – der Gast erhält eine E-Mail mit Anmelde-Link.", inviteSavedNoMail: "Einladung gespeichert. Mail-Limit erreicht – Link später erneut senden.", sendLink: "Link senden", linkSent: "Gesendet", sendFail: "Fehler",
    inviteSavedCopyHint: "Eingeladen. Kopiere den Einladungstext und schick ihn dem Gast.", inviteMailSent: "Einladung gesendet – der Gast hat eine E-Mail mit Anmelde-Link erhalten.", statusRegistered: "Registriert", statusPending: "Ausstehend", sendReset: "Passwort-Reset senden", resetSent: "Reset gesendet", resetFail: "Fehlgeschlagen", copyInvite: "Einladungstext kopieren", inviteCopied: "Kopiert!",
    updateBtn: "Aktualisieren", closeBtn: "Schließen", searchTree: "Nach Titel suchen", byLine: "Von", updatedLabel: "Aktualisiert", editedLabel: "Bearbeitet",
    create: "Erstellen", pageItem: "Seite", confirmDiscard: "Änderungen verwerfen?",
    tplMeeting: "Besprechungsnotizen", tplMeetingDesc: "Agenda, Teilnehmer, Handlungspunkte, Entscheidungen.", tplBlankDesc: "Mit einer leeren Seite starten.",
    activity: "Aktivität", activityEmpty: "Noch keine Aktivität.", allSpaces: "Alle Bereiche", actAll: "Alle Aktionen", actEdits: "Bearbeitungen", actCreated: "Erstellt", actTrash: "Gelöscht / Papierkorb", actGuests: "Gäste", dToday: "Heute", dYesterday: "Gestern",
    avPage: "Seite", avFolder: "Ordner", avSpace: "Bereich",
    avaCreated: "erstellt", avaUpdated: "bearbeitet", avaDuplicated: "dupliziert", avaArchived: "archiviert", avaUnarchived: "wiederhergestellt", avaDeleted: "in den Papierkorb verschoben", avaRestored: "wiederhergestellt", avaPurged: "endgültig gelöscht",
    avgInvited: "hat Gast eingeladen", avgRemoved: "hat Gast entfernt", avgLogin: "hat sich angemeldet",
    kindLabel: "Art", kindClient: "Kunde", kindInternal: "Intern",
    linksLabel: "Verknüpfungen", linkCrm: "CRM", linkBoard: "Board", createBoard: "Board-Projekt anlegen",
  },
};
const DocI18n = React.createContext({ lang: "de", t: (k) => k });
function useDT() { return React.useContext(DocI18n); }

/* ---- helpers ---- */
function docFieldLabel() { return { fontSize: 12.5, fontWeight: 600, color: "var(--ink-3)", marginBottom: 6 }; }
function docRel(iso) { if (!iso) return ""; const s = Math.round((Date.now() - new Date(iso).getTime()) / 1000); if (s < 60) return "gerade eben"; const m = Math.round(s / 60); if (m < 60) return m + "m"; const h = Math.round(m / 60); if (h < 24) return h + "h"; const d = Math.round(h / 24); if (d < 7) return d + "d"; try { return new Date(iso).toLocaleDateString("de-DE", { day: "numeric", month: "short" }); } catch (_) { return ""; } }
function docPersonName(id) { const p = (typeof VP_DATA !== "undefined" && VP_DATA.byId && VP_DATA.byId[id]) || null; return p ? `${p.first || ""} ${p.last || ""}`.trim() || p.email : ""; }
function docPerson(id) { return (typeof VP_DATA !== "undefined" && VP_DATA.byId && VP_DATA.byId[id]) || null; }
function docBuildTree(pages) {
  const byId = {}, roots = [];
  pages.forEach((p) => { byId[p.id] = { ...p, children: [] }; });
  pages.forEach((p) => { const n = byId[p.id]; if (p.parentId && byId[p.parentId]) byId[p.parentId].children.push(n); else roots.push(n); });
  const sortRec = (arr) => { arr.sort((a, b) => (a.sort - b.sort) || a.title.localeCompare(b.title)); arr.forEach((n) => sortRec(n.children)); };
  sortRec(roots);
  return { roots, byId };
}
// keep pages whose title matches q, plus all their ancestors (so the tree stays navigable)
function docFilterTreePages(pages, q) {
  const byId = {}; pages.forEach((p) => (byId[p.id] = p));
  const keep = new Set();
  pages.forEach((p) => { if ((p.title || "").toLowerCase().includes(q)) { let cur = p, guard = 0; while (cur && guard++ < 60) { keep.add(cur.id); cur = cur.parentId ? byId[cur.parentId] : null; } } });
  return pages.filter((p) => keep.has(p.id));
}
// parse #/mesper/<type>/<id> links out of markdown → backlink rows
function docParseLinks(md) {
  const out = [], seen = new Set();
  const re = /\[([^\]]*)\]\(#\/mesper\/([a-z_]+)\/([^)]+)\)/g; let m;
  while ((m = re.exec(md || ""))) { const key = m[2] + "|" + m[3]; if (seen.has(key)) continue; seen.add(key); out.push({ label: m[1], targetType: m[2], targetId: decodeURIComponent(m[3]) }); }
  return out;
}
function docNavigateRef(type, id, navigate) {
  if (type === "doc") navigate("/docs/p/" + id);
  else if (type === "crm_company") navigate("/crm/c/" + id);
  else if (type === "project") navigate("/approvals/p/" + id);
  else if (type === "task") navigate("/approvals/t/" + id);
  // user → no navigation
}
const DOC_REF_ICON = { doc: "file", crm_company: "users", project: "inbox", task: "check", user: "star" };

/* ---- docs-specific HTML↔Markdown (own Turndown w/ GFM tables) ---- */
function docGetTurndown() {
  if (window.__docTD !== undefined) return window.__docTD;
  if (!window.TurndownService) { window.__docTD = null; return null; }
  const td = new window.TurndownService({ headingStyle: "atx", bulletListMarker: "-", codeBlockStyle: "fenced", emDelimiter: "*", strongDelimiter: "**" });
  if (window.turndownPluginGfm && window.turndownPluginGfm.gfm) { try { td.use(window.turndownPluginGfm.gfm); } catch (_) {} }
  td.addRule("strikethrough", { filter: ["del", "s", "strike"], replacement: (c) => "~~" + c + "~~" });
  td.keep(["u"]);
  window.__docTD = td;
  return td;
}
function docHtmlToMarkdown(html) {
  let h = String(html || "").replace(/<img\b[^>]*\bdata-media="([^"]+)"[^>]*>/gi, (m, p) => ` [[MEDIA:${p}]] `);
  h = h.replace(/<span style="[^"]*line-through[^"]*">([\s\S]*?)<\/span>/gi, "<s>$1</s>");
  const clean = window.DOMPurify ? DOMPurify.sanitize(h, { ADD_TAGS: ["table", "thead", "tbody", "tr", "td", "th"] }) : h;
  const td = docGetTurndown();
  let md = "";
  if (td) { try { md = td.turndown(clean); } catch (_) { const t = document.createElement("div"); t.innerHTML = clean; md = t.textContent || ""; } }
  else { const t = document.createElement("div"); t.innerHTML = clean; md = t.textContent || ""; }
  md = md.replace(/\[\[MEDIA:([^\]]+)\]\]/g, (m, p) => `\n\n![](media://${p})\n\n`).replace(/\n{3,}/g, "\n\n").trim();
  return md;
}
function docPlainText(md) { return String(md || "").replace(/!\[[^\]]*\]\([^)]*\)/g, " ").replace(/\[([^\]]*)\]\([^)]*\)/g, "$1").replace(/[#*_`>~|-]/g, " ").replace(/\s+/g, " ").trim(); }

/* ---- docs render pipeline (allows img/table/# links) ---- */
const DOCS_PURIFY_CFG = {
  ALLOWED_TAGS: ["p", "br", "strong", "em", "b", "i", "u", "s", "strike", "del", "mark", "span", "div", "h1", "h2", "h3", "h4", "h5", "ul", "ol", "li", "blockquote", "code", "pre", "a", "hr", "input", "img", "table", "thead", "tbody", "tr", "td", "th"],
  ALLOWED_ATTR: ["href", "target", "rel", "type", "checked", "disabled", "class", "start", "src", "alt", "colspan", "rowspan", "contenteditable", "width"],
  ALLOW_DATA_ATTR: true,
  ALLOWED_URI_REGEXP: /^(https?:|mailto:|media:|#)/i,
  // A custom ALLOWED_URI_REGEXP makes DOMPurify vet EVERY attribute value that
  // is not URI-safe — plain value attrs like type="checkbox" would be stripped.
  ADD_URI_SAFE_ATTR: ["type", "checked", "disabled", "start", "colspan", "rowspan", "contenteditable", "data-date", "data-media", "data-type", "width"],
};
function docSanitizeHtml(html) {
  if (!window.DOMPurify) return String(html || "");
  try { return DOMPurify.sanitize(String(html || ""), DOCS_PURIFY_CFG); } catch (e) { return ""; }
}
// Paste cleanup: strip editor chrome from copied HTML (Office/Docs wrappers,
// styles, empty spans) and unwrap a sole block so it merges into the caret line
// instead of forcing a new paragraph. Falls back to plain text when no HTML.
function docSanitizePaste(html) {
  if (!window.DOMPurify) return "";
  // Google Docs wraps whole pastes in <b style="font-weight:normal"> — strip that
  // fake-bold shell before sanitize removes the style and leaves real <b>.
  let raw = String(html || "").replace(/<b\b[^>]*font-weight:\s*normal[^>]*>([\s\S]*?)<\/b>/gi, "$1");
  let clean = "";
  try { clean = DOMPurify.sanitize(raw, DOCS_PURIFY_CFG); } catch (e) { return ""; }
  const d = document.createElement("div"); d.innerHTML = clean;
  // drop classless/styleless spans (Google Docs wraps every run in one)
  d.querySelectorAll("span").forEach((s) => { if (!s.className) { const p = s.parentNode; while (s.firstChild) p.insertBefore(s.firstChild, s); p.removeChild(s); } });
  // strip empty leading/trailing paragraphs
  Array.prototype.slice.call(d.children).forEach((c) => { if ((c.tagName === "P" || c.tagName === "DIV") && !c.textContent.trim() && !c.querySelector("img,br+*")) { /* keep single <br> paragraphs out */ } });
  // if the whole paste is one block, unwrap it so it stays on the current line
  const blocks = Array.prototype.slice.call(d.children);
  if (blocks.length === 1 && (blocks[0].tagName === "P" || blocks[0].tagName === "DIV")) {
    const b = blocks[0], p = b.parentNode; while (b.firstChild) p.insertBefore(b.firstChild, b); p.removeChild(b);
  }
  return d.innerHTML;
}
function docEscapeHtml(s) { return String(s || "").replace(/[<>&]/g, (c) => ({ "<": "&lt;", ">": "&gt;", "&": "&amp;" }[c])); }
function docRenderHtml(md) {
  const text = (md || "").trim();
  if (!text || !window.marked || !window.DOMPurify) return null;
  try { if (typeof rtSetup === "function") rtSetup(); return DOMPurify.sanitize(marked.parse(text), DOCS_PURIFY_CFG); } catch (e) { return null; }
}
/* text-colour classes for the editor palette + date formatting helpers */
const DOC_COLORS = [
  { cls: "doc-c-red", hex: "#C9372C" }, { cls: "doc-c-orange", hex: "#B65C02" },
  { cls: "doc-c-green", hex: "#1F845A" }, { cls: "doc-c-blue", hex: "#0C66E4" },
  { cls: "doc-c-purple", hex: "#6E5DC6" }, { cls: "doc-c-gray", hex: "#626F86" },
];
const DOC_STATUS_COLORS = ["gray", "blue", "green", "yellow", "red"];
function docToday() { const d = new Date(); const p = (n) => String(n).padStart(2, "0"); return d.getFullYear() + "-" + p(d.getMonth() + 1) + "-" + p(d.getDate()); }
function docFormatDate(iso) { const m = /^(\d{4})-(\d{2})-(\d{2})$/.exec(iso || ""); if (!m) return iso || ""; try { return new Date(+m[1], +m[2] - 1, +m[3]).toLocaleDateString("de-DE", { day: "numeric", month: "long", year: "numeric" }); } catch (_) { return iso || ""; } }
// built-in "Besprechungsnotizen" template (1:1 nach PDF), stored as HTML body
function DOC_TPL_MEETING() {
  const d = docToday(), lbl = docFormatDate(d);
  return [
    "<h2>📅 Datum</h2>",
    '<p><span class="doc-date" data-date="' + d + '" contenteditable="false">' + lbl + "</span>&nbsp;</p>",
    "<h2>🧑‍🤝‍🧑 Teilnehmer</h2>",
    "<ul><li><br></li></ul>",
    "<h2>🥅 Ziele</h2>",
    "<ul><li><br></li></ul>",
    "<h2>🗣️ Diskussionsthemen</h2>",
    "<table><thead><tr><th>Zeit</th><th>Thema</th><th>Referent</th><th>Notizen</th></tr></thead><tbody>",
    "<tr><td>30 Min.</td><td><br></td><td><br></td><td><br></td></tr>",
    "<tr><td><br></td><td><br></td><td><br></td><td><br></td></tr>",
    "<tr><td><br></td><td><br></td><td><br></td><td><br></td></tr>",
    "</tbody></table>",
    "<h2>✅ Handlungspunkte</h2>",
    '<ul class="doc-tasks"><li><input type="checkbox">&nbsp;</li></ul>',
    "<h2>📜 Zusammenfassung</h2>",
    "<p><br></p>",
    "<h2>⤵️ Entscheidungen</h2>",
    '<div class="doc-panel doc-panel-decision"><p><br></p></div>',
    "<p><br></p>",
  ].join("");
}
function docMeetingTitle(spaceName) { return (docToday() + " Weekly " + (spaceName || "")).trim(); }
// toggle the n-th .doc-tasks checkbox's checked attribute inside an HTML string
function docToggleTaskHtml(html, index, checked) {
  const div = document.createElement("div"); div.innerHTML = String(html || "");
  const boxes = div.querySelectorAll(".doc-tasks input[type=checkbox]");
  const box = boxes[index]; if (!box) return String(html || "");
  if (checked) box.setAttribute("checked", ""); else box.removeAttribute("checked");
  return div.innerHTML;
}
// curated emoji set (smileys, gestures, objects, symbols + meeting-template glyphs)
const DOC_EMOJI = "📄 📘 📗 📕 📙 📓 📔 📚 📝 ✏️ 🖊️ 📌 📎 🔖 🗂️ 📁 📂 🗃️ 🗄️ 📋 📊 📈 📉 🗓️ 📅 📆 ⏰ ⏱️ ⌛ 🔑 🔒 🔓 🔔 📣 📢 💬 🗯️ 🗨️ 💡 🔦 🔥 ⭐ 🌟 ✨ ⚡ 🎯 🥅 🚀 🛠️ ⚙️ 🧰 🔧 🔨 📐 📏 🧭 🗺️ 🏁 🚩 🎉 🎊 🏆 🥇 🎖️ ✅ ☑️ ✔️ ❌ ❎ ➕ ➖ ⤵️ ↗️ ➡️ 🔁 🔄 ♻️ ⚠️ ❗ ❓ ❔ 💯 🆕 🆗 🔴 🟠 🟡 🟢 🔵 🟣 ⚫ ⚪ 🟥 🟧 🟨 🟩 🟦 🟪 😀 😃 😄 😁 😉 😊 🙂 😍 🤩 😎 🤔 🧐 😴 😅 😬 😳 🙌 👏 👍 👎 👌 🤝 🙏 💪 🫡 👀 🧑‍🤝‍🧑 👥 🗣️ 📜 💰 💸 📦 🏷️ 🌐 🔗".split(" ");
function DocEmojiGrid({ onPick }) {
  return (
    <div style={{ display: "grid", gridTemplateColumns: "repeat(8, 1fr)", gap: 2, maxHeight: 208, overflowY: "auto" }}>
      {DOC_EMOJI.map((e, i) => (
        <button key={e + i} type="button" onMouseDown={(ev) => { ev.preventDefault(); onPick(e); }}
          style={{ fontSize: 19, border: "none", background: "transparent", cursor: "pointer", padding: 4, borderRadius: 6, lineHeight: 1 }}
          onMouseEnter={(ev) => (ev.currentTarget.style.background = "var(--surface-2)")} onMouseLeave={(ev) => (ev.currentTarget.style.background = "transparent")}>{e}</button>
      ))}
    </div>
  );
}
function DocBody({ md, bodyHtml, navigate, pageId, onToggleTask }) {
  const ref = React.useRef(null);
  const useHtml = !!(bodyHtml && bodyHtml.trim());
  const html = React.useMemo(() => useHtml ? docSanitizeHtml(bodyHtml) : docRenderHtml(md), [useHtml, bodyHtml, md]);
  React.useEffect(() => {
    const root = ref.current; if (!root) return;
    root.querySelectorAll('a[href^="#/mesper/"]').forEach((a) => {
      const mm = (a.getAttribute("href") || "").match(/^#\/mesper\/([a-z_]+)\/(.+)$/); if (!mm) return;
      a.classList.add("doc-chip"); a.removeAttribute("target"); a.removeAttribute("rel");
      a.onclick = (e) => { e.preventDefault(); docNavigateRef(mm[1], decodeURIComponent(mm[2]), navigate); };
    });
    const imgs = Array.prototype.slice.call(root.querySelectorAll('img[src^="media://"]'));
    if (imgs.length && MesperDB.docsSignMedia) {
      const paths = imgs.map((i) => (i.getAttribute("src") || "").slice(8));
      MesperDB.docsSignMedia(paths).then((map) => imgs.forEach((i) => { const p = (i.getAttribute("src") || "").slice(8); if (map[p]) i.src = map[p]; })).catch(() => {});
    }
    // clickable task checkboxes in view mode
    if (typeof onToggleTask === "function") {
      const boxes = Array.prototype.slice.call(root.querySelectorAll(".doc-tasks input[type=checkbox]"));
      boxes.forEach((box, i) => {
        box.checked = box.hasAttribute("checked");
        box.style.cursor = "pointer";
        box.onclick = (e) => { e.preventDefault(); const nx = !box.hasAttribute("checked"); onToggleTask(pageId, i, nx); };
      });
    }
  }, [html, pageId]);
  if (html == null) return <div className="doc-rt" style={{ whiteSpace: "pre-wrap", color: "var(--ink-2)" }}>{md || ""}</div>;
  return <div className="doc-rt rt" ref={ref} dangerouslySetInnerHTML={{ __html: html }} />;
}

/* ---- injected styles (scoped to .doc-*) ---- */
function DocsStyles() {
  return (<style dangerouslySetInnerHTML={{ __html: `
.doc-rt{font-size:15.5px;line-height:1.65;color:var(--ink-2)}
.doc-rt h1{font-size:26px;margin:22px 0 10px;font-weight:800;color:var(--ink);font-family:var(--font-display)}
.doc-rt h2{font-size:20px;margin:20px 0 8px;font-weight:700;color:var(--ink)}
.doc-rt h3{font-size:16.5px;margin:16px 0 6px;font-weight:700;color:var(--ink)}
.doc-rt p{margin:9px 0}.doc-rt ul,.doc-rt ol{margin:9px 0;padding-left:24px}.doc-rt li{margin:3px 0}
.doc-rt blockquote{margin:12px 0;padding:6px 14px;border-left:3px solid var(--accent);background:var(--surface-2);border-radius:0 8px 8px 0;color:var(--ink-3)}
.doc-rt code{background:var(--surface-2);border:1px solid var(--line);border-radius:5px;padding:1px 5px;font-size:13px;font-family:ui-monospace,Menlo,monospace}
.doc-rt pre{background:var(--surface-2);border:1px solid var(--line);border-radius:10px;padding:12px 14px;overflow-x:auto;margin:12px 0}
.doc-rt pre code{background:none;border:none;padding:0}
.doc-rt img{max-width:100%;border-radius:10px;border:1px solid var(--line);margin:10px 0;display:block}
.doc-rt table{border-collapse:collapse;width:100%;margin:12px 0;font-size:14px}
.doc-rt th,.doc-rt td{border:1px solid var(--line);padding:7px 11px;text-align:left;vertical-align:top;word-break:break-word}
.doc-rt th{background:#EAF1FB;color:#172B4D;font-weight:700}
.doc-editor table{table-layout:fixed}
.doc-editor th,.doc-editor td{position:relative}
.doc-rt h4{font-size:15.5px;margin:14px 0 5px;font-weight:700;color:var(--ink)}
.doc-rt h5{font-size:14px;margin:12px 0 4px;font-weight:700;color:var(--ink)}
.doc-rt hr{border:none;border-top:1px solid var(--line);margin:18px 0}
.doc-rt a{color:var(--accent);text-decoration:none}.doc-rt a:hover{text-decoration:underline}
.doc-chip{display:inline-flex;align-items:center;gap:4px;background:var(--accent-soft);color:var(--accent-ink)!important;border-radius:6px;padding:0 6px;font-weight:600;font-size:.94em;text-decoration:none!important;cursor:pointer;white-space:nowrap}
.doc-chip:hover{background:var(--accent);color:#fff!important}
.doc-tasks{list-style:none;padding-left:6px}
.doc-tasks li{display:flex;gap:8px;align-items:flex-start;margin:3px 0}
.doc-tasks input{margin-top:4px;accent-color:var(--accent);flex:0 0 auto}
.doc-date,.doc-status{display:inline-block;border-radius:4px;padding:1px 6px;font-size:.88em;font-weight:600;line-height:1.5;white-space:nowrap}
.doc-date{background:var(--surface-2);border:1px solid var(--line);color:var(--ink-2)}
.doc-editor .doc-date{cursor:pointer}
.doc-status{text-transform:uppercase;letter-spacing:.02em}
.doc-status-gray{background:#EBECF0;color:#42526E}
.doc-status-blue{background:#DEEBFF;color:#0747A6}
.doc-status-green{background:#E3FCEF;color:#064}
.doc-status-yellow{background:#FFF7D6;color:#7A5900}
.doc-status-red{background:#FFEBE6;color:#B71C1C}
.doc-panel{border-radius:10px;padding:12px 16px;margin:12px 0;border:1px solid;color:var(--ink-2)}
.doc-panel>:first-child{margin-top:0}.doc-panel>:last-child{margin-bottom:0}
.doc-panel-info{background:#EAF1FB;border-color:#94B8E8}
.doc-panel-success{background:#E7F6EC;border-color:#9AD4AC}
.doc-panel-warning{background:#FDF3DC;border-color:#E8CD8C}
.doc-panel-note{background:#F1ECFB;border-color:#C2AEE8}
.doc-panel-decision{background:#E7F6EC;border-color:#9AD4AC}
.doc-c-red{color:#C9372C}.doc-c-orange{color:#B65C02}.doc-c-green{color:#1F845A}.doc-c-blue{color:#0C66E4}.doc-c-purple{color:#6E5DC6}.doc-c-gray{color:#626F86}
.doc-tbl-tool{position:absolute;z-index:35;display:flex;align-items:center;gap:1px;background:var(--surface);border:1px solid var(--line);border-radius:var(--r-sm);box-shadow:var(--sh-md);padding:3px 4px}
.doc-tbl-tool button{border:none;background:transparent;cursor:pointer;color:var(--ink-3);font-size:12px;font-weight:700;padding:3px 5px;border-radius:5px;line-height:1;min-width:22px}
.doc-tbl-tool button:hover{background:var(--surface-2);color:var(--ink)}
.doc-editor{outline:none;min-height:340px;font-size:15.5px;line-height:1.65;color:var(--ink)}
.doc-editor:empty:before{content:attr(data-ph);color:var(--ink-4)}
.doc-treerow:hover{background:var(--surface-2)}
.doc-treerow:hover .doc-treeadd{opacity:1}
` }} />);
}

/* ============================================================
   Root
   ============================================================ */
function DocsApp({ user, onHome, path, navigate }) {
  const [lang, setLangState] = useState(() => { try { return localStorage.getItem("mesper.lang") || "de"; } catch (_) { return "de"; } });
  const setLang = (l) => { setLangState(l); try { localStorage.setItem("mesper.lang", l); localStorage.setItem("mesper.docsLang", l); localStorage.setItem("mesper.crmLang", l); } catch (_) {} };
  const t = (k) => (DOC_DICT[lang] && DOC_DICT[lang][k]) || DOC_DICT.en[k] || k;
  const narrow = typeof useIsNarrow === "function" ? useIsNarrow(820) : false;
  const [treeOpen, setTreeOpen] = useState(false);

  const isGuest = !!(user && user.isGuest);
  const sub = (path || "").replace(/^\/docs\/?/, "");
  let view = "home", spaceId = null, pageId = null, q = "";
  if (sub === "" || sub === "home") view = "home";
  else if (sub === "templates") view = "templates";
  else if (sub === "trash") view = "trash";
  else if (sub === "activity") view = "activity";
  else if (sub.startsWith("search")) { view = "search"; try { q = decodeURIComponent((sub.split("?q=")[1] || "")); } catch (_) {} }
  else if (sub.startsWith("s/")) { view = "space"; spaceId = sub.slice(2); }
  else if (sub.startsWith("p/")) { view = "page"; pageId = sub.slice(2); }

  const go = (to) => { setTreeOpen(false); navigate(to); };
  // guests have no access to the internal activity log → send them back to Docs home
  useEffect(() => { if (view === "activity" && isGuest) navigate("/docs"); }, [view, isGuest]);

  const header = (
    <header style={{ display: "flex", alignItems: "center", justifyContent: "space-between", gap: 14, padding: narrow ? "11px 16px" : "13px 28px", borderBottom: "1px solid var(--line)", background: "var(--surface)", flex: "0 0 auto" }}>
      <div style={{ display: "flex", alignItems: "center", gap: 9 }}>
        <button type="button" onClick={onHome} className="vp-focusable" title="All tools" style={{ display: "flex", alignItems: "center", gap: 9, background: "transparent", border: "none", cursor: "pointer" }}>
          <Icon name="arrowLeft" size={18} style={{ color: "var(--ink-4)" }} />
          <BrandLockup height={22} sub="Docs" />
        </button>
        {narrow && <button type="button" onClick={() => setTreeOpen((o) => !o)} style={{ ...(typeof navBtn !== "undefined" ? navBtn : {}), border: "1px solid var(--line-2)", background: "var(--surface)", cursor: "pointer" }}><Icon name="menu" size={17} /></button>}
      </div>
      <div style={{ display: "flex", alignItems: "center", gap: 12 }}>
        <DocSearchBox navigate={go} placeholder={t("searchPh")} narrow={narrow} />
        <div className="lang-toggle" style={{ display: "inline-flex", border: "1px solid var(--line-2)", borderRadius: "var(--r-pill)", overflow: "hidden", flex: "0 0 auto" }}>
          {["de", "en"].map((l) => <button key={l} type="button" onClick={() => setLang(l)} style={{ border: "none", background: lang === l ? "var(--accent)" : "transparent", color: lang === l ? "#fff" : "var(--ink-3)", padding: "8px 12px", fontSize: 12.5, fontWeight: 700, cursor: "pointer" }}>{l.toUpperCase()}</button>)}
        </div>
        {typeof NotificationBell === "function" && <NotificationBell navigate={go} />}
        <AccountMenu user={user} onLogout={() => MesperDB.signOut()} />
      </div>
    </header>
  );

  return (
    <DocI18n.Provider value={{ lang, t }}>
      <DocsStyles />
      <div style={{ height: "100vh", display: "flex", flexDirection: "column", background: "var(--bg)" }}>
        {header}
        <div style={{ flex: 1, display: "flex", minHeight: 0, position: "relative" }}>
          {(!narrow || treeOpen) && (
            <div style={narrow ? { position: "absolute", inset: 0, zIndex: 40, display: "flex" } : { display: "flex" }}>
              <DocSidebar navigate={go} activeSpace={spaceId} activePage={pageId} user={user} narrow={narrow} />
              {narrow && <div onClick={() => setTreeOpen(false)} style={{ flex: 1, background: "rgba(10,14,25,0.3)" }} />}
            </div>
          )}
          <main style={{ flex: 1, minWidth: 0, overflowY: "auto", padding: narrow ? "16px 16px 60px" : "24px 28px 60px" }}>
            {view === "home" && <DocsHome user={user} navigate={go} />}
            {view === "space" && <DocSpaceView spaceId={spaceId} user={user} navigate={go} />}
            {view === "page" && <DocPageView key={pageId} pageId={pageId} user={user} navigate={go} />}
            {view === "templates" && <DocTemplatesView navigate={go} />}
            {view === "search" && <DocSearchView q={q} navigate={go} />}
            {view === "trash" && <DocTrashView navigate={go} />}
            {view === "activity" && !isGuest && <DocActivityView navigate={go} />}
          </main>
        </div>
      </div>
    </DocI18n.Provider>
  );
}

/* ---- header search box ---- */
function DocSearchBox({ navigate, placeholder, narrow }) {
  const [q, setQ] = useState("");
  if (narrow) return null;
  return (
    <input value={q} onChange={(e) => setQ(e.target.value)} onKeyDown={(e) => { if (e.key === "Enter" && q.trim()) navigate("/docs/search?q=" + encodeURIComponent(q.trim())); }}
      placeholder={placeholder} style={{ ...(typeof selectStyle !== "undefined" ? selectStyle : {}), width: 220, maxWidth: 220 }} />
  );
}

/* ---- sidebar: spaces + favorites + page tree ---- */
function DocSidebar({ navigate, activeSpace, activePage, user, narrow }) {
  const { t } = useDT();
  const [spaces, setSpaces] = useState([]);
  const [favs, setFavs] = useState([]);
  const [trees, setTrees] = useState({});           // spaceId -> pages
  const [openSpaces, setOpenSpaces] = useState({});
  const [drag, setDrag] = useState(null);           // {id, spaceId}
  const [dropT, setDropT] = useState(null);         // {id, pos}
  const [treeQuery, setTreeQuery] = useState("");   // client-side title filter
  const [guestCounts, setGuestCounts] = useState({}); // spaceId -> guest count (non-guests only)
  const isGuest = !!(user && user.isGuest);
  function loadGuests(sid) { if (isGuest || !sid) return; MesperDB.docsListGuests(sid).then((gs) => setGuestCounts((c) => ({ ...c, [sid]: gs.length }))).catch(() => {}); }
  function reloadSpaces() { MesperDB.docsListSpaces().then((s) => { setSpaces(s); if (activeSpace) setOpenSpaces((o) => ({ ...o, [activeSpace]: true })); }).catch(() => {}); }
  function isDescendant(pages, ancestorId, nodeId) { let cur = pages.find((p) => p.id === nodeId), guard = 0; while (cur && guard++ < 60) { if (cur.parentId === ancestorId) return true; cur = cur.parentId ? pages.find((p) => p.id === cur.parentId) : null; } return false; }
  async function handleDrop() {
    const d = drag, target = dropT; setDrag(null); setDropT(null);
    if (!d || !target || d.id === target.id) return;
    const pages = trees[d.spaceId] || [];
    if (target.pos === "into" && isDescendant(pages, d.id, target.id)) return;
    const tp = pages.find((p) => p.id === target.id);
    const newParent = target.pos === "into" ? target.id : (tp ? tp.parentId : null);
    if (target.pos !== "into" && newParent && (newParent === d.id || isDescendant(pages, d.id, newParent))) return;
    const dragged = pages.find((p) => p.id === d.id); if (!dragged) return;
    const siblings = pages.filter((p) => (p.parentId || null) === (newParent || null) && p.id !== d.id).sort((a, b) => (a.sort - b.sort) || a.title.localeCompare(b.title));
    let ordered;
    if (target.pos === "into") ordered = [...siblings, dragged];
    else { const i = siblings.findIndex((s) => s.id === target.id); ordered = i < 0 ? [...siblings, dragged] : [...siblings.slice(0, i), dragged, ...siblings.slice(i)]; }
    const updated = pages.map((p) => (p.id === d.id ? { ...p, parentId: newParent } : { ...p }));
    ordered.forEach((s, idx) => { const u = updated.find((p) => p.id === s.id); if (u) u.sort = idx; });
    setTrees((tr) => ({ ...tr, [d.spaceId]: updated }));
    try { for (let i = 0; i < ordered.length; i++) await MesperDB.docsMovePage(ordered[i].id, { parentId: newParent, sort: i, spaceId: d.spaceId }); } catch (e) { loadTree(d.spaceId); }
  }
  const dnd = { drag, dropT, onStart: (id, sid) => setDrag({ id, spaceId: sid }), onOver: (id, pos) => setDropT({ id, pos }), onDrop: handleDrop, onLeave: () => { setDrag(null); setDropT(null); } };
  // space reordering (drag & drop)
  const [spDrag, setSpDrag] = useState(null);       // spaceId being dragged
  const [spDrop, setSpDrop] = useState(null);       // {id, pos}
  async function handleSpaceDrop() {
    const d = spDrag, target = spDrop; setSpDrag(null); setSpDrop(null);
    if (!d || !target || d === target.id) return;
    const ids = spaces.map((s) => s.id).filter((id) => id !== d);
    let idx = ids.indexOf(target.id); if (idx < 0) return; if (target.pos === "after") idx += 1;
    ids.splice(idx, 0, d);
    const arr = ids.map((id) => spaces.find((s) => s.id === id)).filter(Boolean);
    setSpaces(arr);
    try { for (let i = 0; i < arr.length; i++) await MesperDB.docsUpdateSpace(arr[i].id, { sort: i }); } catch (e) { reloadSpaces(); }
  }
  useEffect(() => { reloadSpaces(); MesperDB.docsListFavorites(user.id).then(setFavs).catch(() => {}); }, []);
  useEffect(() => { // ensure the space containing the active page is expanded + loaded
    if (activePage) { MesperDB.docsGetPage(activePage).then((d) => { const sid = d.page.spaceId; if (sid) { setOpenSpaces((o) => ({ ...o, [sid]: true })); loadTree(sid); loadGuests(sid); } }).catch(() => {}); }
    if (activeSpace) { loadTree(activeSpace); loadGuests(activeSpace); }
  }, [activePage, activeSpace]);
  function loadTree(sid) { if (!sid) return; MesperDB.docsListTree(sid).then((pg) => setTrees((tr) => ({ ...tr, [sid]: pg }))).catch(() => {}); }
  function toggleSpace(sid) { setOpenSpaces((o) => { const nx = { ...o, [sid]: !o[sid] }; if (nx[sid] && !trees[sid]) loadTree(sid); if (nx[sid]) loadGuests(sid); return nx; }); }
  async function createFolder(sid) { try { const f = await MesperDB.docsCreatePage({ spaceId: sid, isFolder: true, title: t("newFolder") }); setOpenSpaces((o) => ({ ...o, [sid]: true })); loadTree(sid); navigate("/docs/p/" + f.id); } catch (e) {} }
  async function createPage(sid) { try { const p = await MesperDB.docsCreatePage({ spaceId: sid, title: t("untitled") }); setOpenSpaces((o) => ({ ...o, [sid]: true })); loadTree(sid); navigate("/docs/p/" + p.id); } catch (e) {} }
  async function createTemplate(sid) { const sp = spaces.find((s) => s.id === sid); try { const p = await MesperDB.docsCreatePage({ spaceId: sid, bodyHtml: DOC_TPL_MEETING(), title: docMeetingTitle(sp ? sp.name : "") }); setOpenSpaces((o) => ({ ...o, [sid]: true })); loadTree(sid); navigate("/docs/p/" + p.id); } catch (e) {} }
  const [adding, setAdding] = useState(false);
  const tq = treeQuery.trim().toLowerCase();

  return (
    <aside style={{ width: 258, flex: "0 0 auto", borderRight: "1px solid var(--line)", background: "var(--surface-2)", overflowY: "auto", padding: "14px 10px 40px", display: "flex", flexDirection: "column", gap: 4 }}>
      {favs.length > 0 && (
        <div style={{ marginBottom: 8 }}>
          <div style={docSideHead()}>{t("favorites")}</div>
          {favs.map((p) => <button key={p.id} type="button" onClick={() => navigate("/docs/p/" + p.id)} style={docPageRowStyle(activePage === p.id)}><span style={{ fontSize: 14 }}>{p.icon || "⭐"}</span><span style={docRowLabel()}>{p.title}</span></button>)}
        </div>
      )}
      <div style={{ position: "relative", margin: "2px 6px 8px" }}>
        <Icon name="search" size={13} style={{ position: "absolute", left: 9, top: "50%", transform: "translateY(-50%)", color: "var(--ink-4)", pointerEvents: "none" }} />
        <input value={treeQuery} onChange={(e) => setTreeQuery(e.target.value)} placeholder={t("searchTree")}
          style={{ width: "100%", boxSizing: "border-box", padding: "7px 9px 7px 28px", fontSize: 13, border: "1px solid var(--line-2)", borderRadius: "var(--r-sm)", background: "var(--surface)", color: "var(--ink)", fontFamily: "var(--font-body)" }} />
      </div>
      <div style={{ display: "flex", alignItems: "center", gap: 6, padding: "2px 8px" }}>
        <div style={{ ...docSideHead(), margin: 0, flex: 1 }}>{t("spaces")}</div>
        {!isGuest && <button type="button" onClick={() => setAdding(true)} title={t("newSpace")} style={{ border: "none", background: "transparent", cursor: "pointer", color: "var(--ink-4)", padding: 2 }}><Icon name="plus" size={15} /></button>}
      </div>
      {spaces.map((sp) => (
        <div key={sp.id}>
          <div className="doc-treerow"
            draggable
            onDragStart={(e) => { setSpDrag(sp.id); try { e.dataTransfer.effectAllowed = "move"; e.dataTransfer.setData("text/plain", "space:" + sp.id); } catch (_) {} }}
            onDragOver={(e) => { if (spDrag) { e.preventDefault(); e.stopPropagation(); const r = e.currentTarget.getBoundingClientRect(); setSpDrop({ id: sp.id, pos: (e.clientY - r.top) < r.height / 2 ? "before" : "after" }); } }}
            onDrop={(e) => { if (spDrag) { e.preventDefault(); e.stopPropagation(); handleSpaceDrop(); } }}
            onDragEnd={() => { setSpDrag(null); setSpDrop(null); }}
            style={{ display: "flex", alignItems: "center", borderRadius: "var(--r-sm)", cursor: "grab", opacity: spDrag === sp.id ? 0.4 : 1, boxShadow: spDrop && spDrop.id === sp.id ? (spDrop.pos === "before" ? "inset 0 2px 0 var(--accent)" : "inset 0 -2px 0 var(--accent)") : "none" }}>
            <button type="button" onClick={() => toggleSpace(sp.id)} style={{ border: "none", background: "transparent", cursor: "pointer", padding: "6px 2px 6px 6px", color: "var(--ink-4)", flex: "0 0 auto", transform: openSpaces[sp.id] ? "rotate(90deg)" : "none", transition: "transform .12s" }}><Icon name="chevRight" size={13} /></button>
            <button type="button" onClick={() => navigate("/docs/s/" + sp.id)} style={{ flex: 1, display: "flex", alignItems: "center", gap: 8, border: "none", background: "transparent", cursor: "pointer", padding: "6px 6px 6px 0", textAlign: "left", color: activeSpace === sp.id ? "var(--ink)" : "var(--ink-2)", fontWeight: 700, fontSize: 13.5, minWidth: 0 }}>
              <span style={{ width: 8, height: 8, borderRadius: 3, background: sp.color, flex: "0 0 auto" }} /><span style={docRowLabel()}>{sp.name}</span>
            </button>
            <button type="button" className="doc-treeadd" onClick={(e) => { e.stopPropagation(); createFolder(sp.id); }} title={t("newFolder")} style={{ opacity: 0, transition: "opacity .1s", border: "none", background: "transparent", cursor: "pointer", color: "var(--ink-4)", padding: "0 8px" }}><Icon name="plus" size={13} /></button>
          </div>
          {!isGuest && !tq && openSpaces[sp.id] && guestCounts[sp.id] > 0 && (
            <div style={{ fontSize: 11.5, color: "var(--ink-4)", padding: "0 8px 4px 26px" }}>{guestCounts[sp.id]} {t("guestsInArea")}</div>
          )}
          {(() => {
            const rawPages = trees[sp.id] || [];
            const pages = tq ? docFilterTreePages(rawPages, tq) : rawPages;
            const roots = docBuildTree(pages).roots;
            const showTree = openSpaces[sp.id] || (tq && roots.length > 0);
            if (!showTree) return null;
            return <>
              <DocTreeNodes nodes={roots} depth={0} activePage={activePage} navigate={navigate} onChanged={() => loadTree(sp.id)} spaceId={sp.id} dnd={dnd} expandAll={!!tq} />
              {!tq && <DocCreateRow isGuest={isGuest} onPage={() => createPage(sp.id)} onFolder={() => createFolder(sp.id)} onTemplate={() => createTemplate(sp.id)} />}
            </>;
          })()}
        </div>
      ))}
      {!isGuest && <><div style={{ height: 1, background: "var(--line)", margin: "10px 8px 4px" }} />
      <button type="button" onClick={() => navigate("/docs/trash")} style={{ display: "flex", alignItems: "center", gap: 10, width: "100%", padding: "7px 10px", border: "none", cursor: "pointer", background: "transparent", borderRadius: "var(--r-sm)", textAlign: "left", fontSize: 13, color: "var(--ink-3)", fontWeight: 600 }}><Icon name="trash" size={15} style={{ color: "var(--ink-4)" }} /> {t("trash")}</button>
      <button type="button" onClick={() => navigate("/docs/activity")} style={{ display: "flex", alignItems: "center", gap: 10, width: "100%", padding: "7px 10px", border: "none", cursor: "pointer", background: "transparent", borderRadius: "var(--r-sm)", textAlign: "left", fontSize: 13, color: "var(--ink-3)", fontWeight: 600 }}><Icon name="clock" size={15} style={{ color: "var(--ink-4)" }} /> {t("activity")}</button></>}
      {adding && !isGuest && <DocSpaceModal onClose={() => setAdding(false)} onSaved={(s) => { setAdding(false); reloadSpaces(); navigate("/docs/s/" + s.id); }} />}
    </aside>
  );
}
function docSideHead() { return { fontSize: 11, fontWeight: 700, letterSpacing: "0.05em", textTransform: "uppercase", color: "var(--ink-4)", padding: "4px 8px", margin: "0 0 2px" }; }
function docRowLabel() { return { whiteSpace: "nowrap", overflow: "hidden", textOverflow: "ellipsis", flex: 1, minWidth: 0 }; }
function docPageRowStyle(active) { return { display: "flex", alignItems: "center", gap: 8, width: "100%", padding: "6px 8px", border: "none", cursor: "pointer", background: active ? "var(--surface)" : "transparent", boxShadow: active ? "var(--sh-sm)" : "none", borderRadius: "var(--r-sm)", textAlign: "left", fontSize: 13.5, color: active ? "var(--ink)" : "var(--ink-2)", fontWeight: active ? 600 : 500 }; }

function DocTreeNodes({ nodes, depth, activePage, navigate, onChanged, spaceId, dnd, expandAll }) {
  const { t } = useDT();
  const [expanded, setExpanded] = useState({});
  async function addChild(parentId) { try { const p = await MesperDB.docsCreatePage({ spaceId, parentId, title: t("untitled") }); setExpanded((e) => ({ ...e, [parentId]: true })); onChanged(); navigate("/docs/p/" + p.id); } catch (e) {} }
  function overPos(e) { const r = e.currentTarget.getBoundingClientRect(); return (e.clientY - r.top) < r.height * 0.42 ? "before" : "into"; }
  return (
    <div>
      {nodes.map((n) => {
        const isDrop = dnd && dnd.dropT && dnd.dropT.id === n.id;
        const dragging = dnd && dnd.drag && dnd.drag.id === n.id;
        const open = expandAll || expanded[n.id];
        return (
          <div key={n.id}>
            <div className="doc-treerow"
              draggable
              onDragStart={(e) => { e.stopPropagation(); try { e.dataTransfer.effectAllowed = "move"; e.dataTransfer.setData("text/plain", n.id); } catch (_) {} dnd && dnd.onStart(n.id, spaceId); }}
              onDragOver={(e) => { if (dnd && dnd.drag) { e.preventDefault(); e.stopPropagation(); dnd.onOver(n.id, overPos(e)); } }}
              onDrop={(e) => { if (dnd && dnd.drag) { e.preventDefault(); e.stopPropagation(); dnd.onDrop(); } }}
              onDragEnd={() => dnd && dnd.onLeave()}
              style={{ display: "flex", alignItems: "center", borderRadius: "var(--r-sm)", paddingLeft: 12 + depth * 12, background: isDrop && dnd.dropT.pos === "into" ? "var(--accent-soft)" : "transparent", boxShadow: isDrop && dnd.dropT.pos === "before" ? "inset 0 2px 0 var(--accent)" : "none", opacity: dragging ? 0.4 : 1 }}>
              {n.children.length > 0
                ? <button type="button" onClick={() => setExpanded((e) => ({ ...e, [n.id]: !e[n.id] }))} style={{ border: "none", background: "transparent", cursor: "pointer", padding: "5px 2px", color: "var(--ink-4)", transform: open ? "rotate(90deg)" : "none", transition: "transform .12s" }}><Icon name="chevRight" size={12} /></button>
                : <span style={{ width: 17, flex: "0 0 auto" }} />}
              <button type="button" onClick={() => navigate("/docs/p/" + n.id)} style={{ flex: 1, display: "flex", alignItems: "center", gap: 7, border: "none", background: activePage === n.id ? "var(--surface)" : "transparent", boxShadow: activePage === n.id ? "var(--sh-sm)" : "none", borderRadius: "var(--r-sm)", cursor: "pointer", padding: "5px 6px", textAlign: "left", fontSize: 13, color: activePage === n.id ? "var(--ink)" : "var(--ink-3)", fontWeight: n.isFolder || activePage === n.id ? 600 : 500, minWidth: 0 }}>
                <span style={{ fontSize: 13, flex: "0 0 auto" }}>{n.isFolder ? "📁" : (n.icon || "📄")}</span><span style={docRowLabel()}>{n.title}</span>
              </button>
              {n.isFolder && <button type="button" className="doc-treeadd" onClick={() => addChild(n.id)} title={t("newDoc")} style={{ opacity: 0, transition: "opacity .1s", border: "none", background: "transparent", cursor: "pointer", color: "var(--ink-4)", padding: "0 6px" }}><Icon name="plus" size={13} /></button>}
            </div>
            {open && n.children.length > 0 && <DocTreeNodes nodes={n.children} depth={depth + 1} activePage={activePage} navigate={navigate} onChanged={onChanged} spaceId={spaceId} dnd={dnd} expandAll={expandAll} />}
          </div>
        );
      })}
    </div>
  );
}

/* ---- "+ Erstellen" row: page / folder / from-template menu (bundled per space) ---- */
function DocCreateRow({ isGuest, onPage, onFolder, onTemplate }) {
  const { t } = useDT();
  const [open, setOpen] = useState(false);
  const ref = useRef(null);
  useEffect(() => { const h = (e) => { if (ref.current && !ref.current.contains(e.target)) setOpen(false); }; document.addEventListener("mousedown", h); return () => document.removeEventListener("mousedown", h); }, []);
  const item = (label, fn) => (
    <button key={label} type="button" onClick={() => { setOpen(false); fn(); }}
      style={{ display: "flex", alignItems: "center", gap: 8, width: "100%", padding: "7px 9px", border: "none", background: "transparent", cursor: "pointer", borderRadius: "var(--r-sm)", textAlign: "left", fontSize: 13, color: "var(--ink-2)" }}
      onMouseEnter={(e) => (e.currentTarget.style.background = "var(--surface-2)")} onMouseLeave={(e) => (e.currentTarget.style.background = "transparent")}>{label}</button>
  );
  return (
    <div ref={ref} style={{ position: "relative", paddingLeft: 12 }}>
      <button type="button" onClick={() => setOpen((o) => !o)} style={{ display: "flex", alignItems: "center", gap: 6, width: "100%", padding: "5px 6px", border: "none", background: "transparent", cursor: "pointer", borderRadius: "var(--r-sm)", textAlign: "left", fontSize: 12.5, color: "var(--ink-4)", fontWeight: 600 }}><Icon name="plus" size={13} /> {t("create")}</button>
      {open && (
        <div className="vp-pop" style={{ position: "absolute", top: "100%", left: 12, marginTop: 2, background: "var(--surface)", border: "1px solid var(--line)", borderRadius: "var(--r-md)", boxShadow: "var(--sh-lg)", padding: 5, zIndex: 50, minWidth: 160 }}>
          {item(t("pageItem"), onPage)}
          {item(t("folder"), onFolder)}
          {item(t("useTemplate"), onTemplate)}
        </div>
      )}
    </div>
  );
}

/* ---- home ---- */
function DocsHome({ user, navigate }) {
  const { t } = useDT();
  const [spaces, setSpaces] = useState([]);
  const [recent, setRecent] = useState([]);
  useEffect(() => { MesperDB.docsListSpaces().then(setSpaces).catch(() => {}); MesperDB.docsListRecent(8).then(setRecent).catch(() => {}); }, []);
  return (
    <div style={{ maxWidth: 900, margin: "0 auto" }}>
      <div style={{ marginBottom: 24 }}>
        <h1 style={{ fontSize: 27, margin: "0 0 6px" }}>{t("welcome")}</h1>
        <p style={{ color: "var(--ink-3)", fontSize: 15, margin: 0, maxWidth: 560, lineHeight: 1.5 }}>{t("welcomeSub")}</p>
      </div>
      <div style={{ display: "flex", alignItems: "center", gap: 10, marginBottom: 12 }}>
        <div style={{ fontSize: 13, fontWeight: 700, letterSpacing: "0.04em", textTransform: "uppercase", color: "var(--ink-4)" }}>{t("spaces")}</div>
        <div style={{ flex: 1 }} /><Button size="sm" variant="ghost" icon="file" onClick={() => navigate("/docs/templates")}>{t("templates")}</Button>
      </div>
      <div style={{ display: "grid", gridTemplateColumns: "repeat(auto-fill, minmax(230px, 1fr))", gap: 14, marginBottom: 30 }}>
        {spaces.map((sp) => (
          <button key={sp.id} type="button" onClick={() => navigate("/docs/s/" + sp.id)} style={{ display: "flex", alignItems: "flex-start", gap: 12, padding: 16, border: "1px solid var(--line)", borderRadius: "var(--r-lg)", background: "var(--surface)", cursor: "pointer", textAlign: "left", boxShadow: "var(--sh-sm)" }}>
            <span style={{ width: 40, height: 40, borderRadius: "var(--r-md)", background: sp.color + "1f", color: sp.color, display: "grid", placeItems: "center", flex: "0 0 auto" }}><Icon name={sp.icon || "book"} size={21} /></span>
            <div style={{ minWidth: 0 }}>
              <div style={{ fontWeight: 700, fontSize: 15.5, color: "var(--ink)" }}>{sp.name}</div>
              <div style={{ fontSize: 12.5, color: "var(--ink-4)", marginTop: 2, lineHeight: 1.4 }}>{sp.description}</div>
            </div>
          </button>
        ))}
      </div>
      {recent.length > 0 && (<>
        <div style={{ fontSize: 13, fontWeight: 700, letterSpacing: "0.04em", textTransform: "uppercase", color: "var(--ink-4)", marginBottom: 10 }}>{t("recent")}</div>
        <div style={{ display: "flex", flexDirection: "column", gap: 6 }}>
          {recent.map((p) => (
            <button key={p.id} type="button" onClick={() => navigate("/docs/p/" + p.id)} style={{ display: "flex", alignItems: "center", gap: 10, padding: "10px 13px", border: "1px solid var(--line)", borderRadius: "var(--r-md)", background: "var(--surface)", cursor: "pointer", textAlign: "left" }}>
              <span style={{ fontSize: 15 }}>{p.icon || "📄"}</span>
              <span style={{ fontWeight: 600, fontSize: 14.5, color: "var(--ink)", flex: 1, minWidth: 0, whiteSpace: "nowrap", overflow: "hidden", textOverflow: "ellipsis" }}>{p.title}</span>
              <span style={{ fontSize: 12, color: "var(--ink-4)" }}>{t("lastEdited")} {docRel(p.updatedAt)}</span>
            </button>
          ))}
        </div>
      </>)}
    </div>
  );
}

/* ---- space view (page tree list + new page) ---- */
/* ---- client (space) view: folders + loose docs ---- */
function DocSpaceView({ spaceId, user, navigate }) {
  const { t } = useDT();
  const [space, setSpace] = useState(null);
  const [pages, setPages] = useState(null);
  const [editing, setEditing] = useState(false);
  const [inviting, setInviting] = useState(false);
  function reload() { MesperDB.docsListSpaces().then((s) => setSpace(s.find((x) => x.id === spaceId) || false)).catch(() => {}); MesperDB.docsListTree(spaceId).then(setPages).catch(() => setPages([])); }
  useEffect(() => { reload(); }, [spaceId]);
  async function newFolder() { try { const f = await MesperDB.docsCreatePage({ spaceId, isFolder: true, title: t("newFolder") }); navigate("/docs/p/" + f.id); } catch (e) {} }
  async function newDoc() { try { const p = await MesperDB.docsCreatePage({ spaceId, title: t("untitled") }); navigate("/docs/p/" + p.id); } catch (e) {} }
  if (space === false) return <div style={{ color: "var(--ink-4)" }}>—</div>;
  const all = pages || [];
  const folders = all.filter((p) => p.isFolder && !p.parentId);
  const looseDocs = all.filter((p) => !p.isFolder && !p.parentId);
  const docCount = (fid) => all.filter((p) => p.parentId === fid && !p.isFolder).length;
  return (
    <div style={{ maxWidth: 860, margin: "0 auto" }}>
      <div style={{ display: "flex", alignItems: "flex-start", gap: 13, marginBottom: 20, flexWrap: "wrap" }}>
        {space && <span style={{ width: 46, height: 46, borderRadius: "var(--r-md)", background: space.color + "1f", color: space.color, display: "grid", placeItems: "center", flex: "0 0 auto" }}><Icon name={space.icon || "book"} size={24} /></span>}
        <div style={{ flex: 1, minWidth: 140 }}>
          <h1 style={{ fontSize: 25, margin: 0 }}>{space ? space.name : ""}</h1>
          {space && space.description && <div style={{ color: "var(--ink-4)", fontSize: 14, marginTop: 3 }}>{space.description}</div>}
        </div>
        <div style={{ display: "flex", gap: 8, alignItems: "center" }}>
          <Button variant="ghost" icon="file" onClick={newDoc}>{t("newDoc")}</Button>
          <Button icon="book" onClick={newFolder}>{t("newFolder")}</Button>
          {!(user && user.isGuest) && <Button variant="outline" icon="users" onClick={() => setInviting(true)}>{t("invite")}</Button>}
          <DocKebab items={[
            { label: t("editClient"), icon: "settings", onClick: () => setEditing(true) },
            { label: t("archive"), icon: "book", onClick: async () => { try { await MesperDB.docsArchiveSpace(spaceId, true); } catch (e) {} navigate("/docs"); } },
            { label: t("moveToTrash"), icon: "trash", danger: true, onClick: async () => { try { await MesperDB.docsTrashSpace(spaceId); } catch (e) {} navigate("/docs"); } },
          ]} />
        </div>
      </div>
      {editing && space && <DocSpaceModal space={space} onClose={() => setEditing(false)} onSaved={() => { setEditing(false); reload(); }} />}
      {inviting && space && <DocSpaceInviteModal space={space} onClose={() => setInviting(false)} />}
      {!(user && user.isGuest) && space && space.kind === "client" && <SpaceLinksRow spaceId={spaceId} navigate={navigate} />}
      {pages === null ? <div style={{ color: "var(--ink-4)" }}>…</div> : (folders.length === 0 && looseDocs.length === 0) ? (
        <div style={{ color: "var(--ink-4)", fontSize: 14.5, padding: "24px 0" }}>{t("clientEmpty")}</div>
      ) : (
        <div style={{ display: "grid", gap: 24 }}>
          {folders.length > 0 && (
            <div>
              <div style={docSideHead()}>{t("folders")}</div>
              <div style={{ display: "grid", gridTemplateColumns: "repeat(auto-fill, minmax(220px, 1fr))", gap: 12 }}>
                {folders.map((f) => (
                  <button key={f.id} type="button" onClick={() => navigate("/docs/p/" + f.id)} style={{ display: "flex", alignItems: "center", gap: 12, padding: "14px 15px", border: "1px solid var(--line)", borderRadius: "var(--r-md)", background: "var(--surface)", cursor: "pointer", textAlign: "left", boxShadow: "var(--sh-sm)" }}>
                    <span style={{ fontSize: 24, flex: "0 0 auto" }}>📁</span>
                    <div style={{ minWidth: 0, flex: 1 }}>
                      <div style={{ fontWeight: 700, fontSize: 14.5, color: "var(--ink)", whiteSpace: "nowrap", overflow: "hidden", textOverflow: "ellipsis" }}>{f.title}</div>
                      <div style={{ fontSize: 12, color: "var(--ink-4)" }}>{docCount(f.id)} {t("documents")}</div>
                    </div>
                  </button>
                ))}
              </div>
            </div>
          )}
          {looseDocs.length > 0 && (
            <div>
              <div style={docSideHead()}>{t("documents")}</div>
              <div style={{ border: "1px solid var(--line)", borderRadius: "var(--r-md)", background: "var(--surface)", overflow: "hidden" }}>
                {looseDocs.map((d, i) => (
                  <button key={d.id} type="button" onClick={() => navigate("/docs/p/" + d.id)} style={{ display: "flex", alignItems: "center", gap: 10, width: "100%", padding: "12px 16px", border: "none", borderTop: i ? "1px solid var(--line)" : "none", background: "transparent", cursor: "pointer", textAlign: "left" }}>
                    <span style={{ fontSize: 15 }}>{d.icon || "📄"}</span>
                    <span style={{ fontWeight: 600, fontSize: 14.5, color: "var(--ink)", flex: 1 }}>{d.title}</span>
                    <span style={{ fontSize: 12, color: "var(--ink-4)" }}>{docRel(d.updatedAt)}</span>
                  </button>
                ))}
              </div>
            </div>
          )}
        </div>
      )}
    </div>
  );
}

/* ---- invite dialog for a client (space): guest management only, no single-page link ---- */
function DocSpaceInviteModal({ space, onClose }) {
  const { t } = useDT();
  return (
    <Modal onClose={onClose} width={460}>
      <div style={{ padding: "18px 22px", borderBottom: "1px solid var(--line)", display: "flex", alignItems: "center", gap: 9 }}>
        <Icon name="users" size={18} style={{ color: "var(--ink-4)" }} />
        <h2 style={{ fontSize: 18, margin: 0 }}>{t("invite")} – {space.name}</h2>
      </div>
      <div style={{ padding: "16px 22px" }}>
        <DocGuestManager spaceId={space.id} space={space} />
      </div>
      <div style={{ padding: "13px 22px", borderTop: "1px solid var(--line)", display: "flex", justifyContent: "flex-end" }}>
        <Button variant="ghost" onClick={onClose}>{t("closeBtn")}</Button>
      </div>
    </Modal>
  );
}

/* ---- folder view (Art der Doku): lists its docs ---- */
function DocFolderView({ folder, user, navigate, onChanged }) {
  const { t } = useDT();
  const [space, setSpace] = useState(null);
  const [docs, setDocs] = useState(null);
  const [renaming, setRenaming] = useState(false);
  const [name, setName] = useState(folder.title);
  const ss = typeof selectStyle !== "undefined" ? selectStyle : {};
  function reload() { MesperDB.docsListSpaces().then((s) => setSpace(s.find((x) => x.id === folder.spaceId) || null)).catch(() => {}); MesperDB.docsListTree(folder.spaceId).then((all) => setDocs(all.filter((p) => p.parentId === folder.id && !p.isFolder))).catch(() => setDocs([])); }
  useEffect(() => { setName(folder.title); reload(); }, [folder.id]);
  async function newDoc() { try { const p = await MesperDB.docsCreatePage({ spaceId: folder.spaceId, parentId: folder.id, title: t("untitled") }); navigate("/docs/p/" + p.id); } catch (e) {} }
  async function saveName() { setRenaming(false); if (name.trim() && name.trim() !== folder.title) { try { await MesperDB.docsUpdatePage(folder.id, { title: name.trim(), updatedBy: user.id }); onChanged && onChanged(); } catch (e) {} } }
  async function del() { if (!window.confirm(t("confirmDelete"))) return; try { await MesperDB.docsDeletePage(folder.id); } catch (e) {} navigate("/docs/s/" + folder.spaceId); }
  return (
    <div style={{ maxWidth: 820, margin: "0 auto" }}>
      <div style={{ display: "flex", alignItems: "center", gap: 6, fontSize: 12.5, color: "var(--ink-4)", marginBottom: 14 }}>
        {space && <><button type="button" onClick={() => navigate("/docs/s/" + space.id)} style={docCrumbBtn()}>{space.name}</button><Icon name="chevRight" size={12} /></>}
        <span style={{ color: "var(--ink-3)", fontWeight: 600 }}>{folder.title}</span>
      </div>
      <div style={{ display: "flex", alignItems: "center", gap: 12, marginBottom: 20 }}>
        <span style={{ fontSize: 30, flex: "0 0 auto" }}>📁</span>
        {renaming
          ? <input autoFocus value={name} onChange={(e) => setName(e.target.value)} onBlur={saveName} onKeyDown={(e) => { if (e.key === "Enter") saveName(); }} style={{ ...ss, fontSize: 25, fontWeight: 800, flex: 1 }} />
          : <h1 onClick={() => { setName(folder.title); setRenaming(true); }} style={{ fontSize: 27, margin: 0, flex: 1, cursor: "text", minWidth: 0 }}>{folder.title}</h1>}
        <DocKebab items={[
          { label: t("archive"), icon: "book", onClick: async () => { try { await MesperDB.docsArchivePage(folder.id, true); } catch (e) {} navigate("/docs/s/" + folder.spaceId); } },
          { label: t("moveToTrash"), icon: "trash", danger: true, onClick: async () => { try { await MesperDB.docsTrashPage(folder.id, folder.spaceId); } catch (e) {} navigate("/docs/s/" + folder.spaceId); } },
        ]} />
        <Button icon="plus" onClick={newDoc}>{t("newDoc")}</Button>
      </div>
      {docs === null ? <div style={{ color: "var(--ink-4)" }}>…</div> : docs.length === 0 ? (
        <div style={{ color: "var(--ink-4)", fontSize: 14.5, padding: "24px 0" }}>{t("emptyFolder")}</div>
      ) : (
        <div style={{ border: "1px solid var(--line)", borderRadius: "var(--r-md)", background: "var(--surface)", overflow: "hidden" }}>
          {docs.map((d, i) => (
            <button key={d.id} type="button" onClick={() => navigate("/docs/p/" + d.id)} style={{ display: "flex", alignItems: "center", gap: 10, width: "100%", padding: "12px 16px", border: "none", borderTop: i ? "1px solid var(--line)" : "none", background: "transparent", cursor: "pointer", textAlign: "left" }}>
              <span style={{ fontSize: 15 }}>{d.icon || "📄"}</span>
              <span style={{ fontWeight: 600, fontSize: 14.5, color: "var(--ink)", flex: 1 }}>{d.title}</span>
              <span style={{ fontSize: 12, color: "var(--ink-4)" }}>{docRel(d.updatedAt)}</span>
            </button>
          ))}
        </div>
      )}
    </div>
  );
}

/* ---- space create/edit modal ---- */
function DocSpaceModal({ onClose, onSaved, space }) {
  const { t } = useDT();
  const [name, setName] = useState(space?.name || "");
  const [key, setKey] = useState(space?.key || "");
  const [kind, setKind] = useState(space?.kind || "client");
  const [desc, setDesc] = useState(space?.description || "");
  const [color, setColor] = useState(space?.color || "#5A58E7");
  const [busy, setBusy] = useState(false);
  const ss = typeof selectStyle !== "undefined" ? selectStyle : { width: "100%", padding: "9px 12px", border: "1px solid var(--line-2)", borderRadius: 8, fontSize: 14 };
  const COLORS = ["#5A58E7", "#1FA36B", "#3490EB", "#E0792B", "#7D50CA", "#E5484D"];
  const KINDS = [{ id: "client", label: t("kindClient") }, { id: "internal", label: t("kindInternal") }];
  async function save() {
    if (!name.trim() || busy) return;
    setBusy(true);
    try {
      const s = space
        ? await MesperDB.docsUpdateSpace(space.id, { name, key, description: desc, color, kind })
        : await MesperDB.docsCreateSpace({ name, key, description: desc, color, kind });
      if (kind === "client") { try { await MesperDB.clientProvisionFromSpace(s.id); } catch (_) {} }
      onSaved(s);
    } catch (e) { setBusy(false); }
  }
  return (
    <Modal onClose={onClose} width={440}>
      <div style={{ padding: "18px 22px", borderBottom: "1px solid var(--line)" }}><h2 style={{ fontSize: 18, margin: 0 }}>{space ? t("rename") : t("createSpace")}</h2></div>
      <div style={{ padding: "16px 22px", display: "grid", gap: 12 }}>
        <div style={{ display: "grid", gridTemplateColumns: "1fr 110px", gap: 10 }}>
          <div><div style={docFieldLabel()}>{t("spaceName")}</div><input autoFocus value={name} onChange={(e) => setName(e.target.value)} style={ss} placeholder="Onboarding" /></div>
          <div><div style={docFieldLabel()}>{t("spaceKey")}</div><input value={key} onChange={(e) => setKey(e.target.value.toUpperCase().slice(0, 6))} style={ss} placeholder="ONB" /></div>
        </div>
        <div>
          <div style={docFieldLabel()}>{t("kindLabel")}</div>
          <div style={{ display: "flex", gap: 8 }}>
            {KINDS.map((k) => {
              const on = kind === k.id;
              return <button key={k.id} type="button" onClick={() => setKind(k.id)} className="vp-focusable"
                style={{ flex: 1, padding: "9px 0", borderRadius: "var(--r-sm)", cursor: "pointer", fontWeight: 600, fontSize: 13.5, border: `1px solid ${on ? "transparent" : "var(--line-2)"}`, background: on ? "var(--accent-soft)" : "var(--surface)", color: on ? "var(--accent-ink)" : "var(--ink-2)", boxShadow: on ? "inset 0 0 0 1px var(--accent)" : "none" }}>{k.label}</button>;
            })}
          </div>
        </div>
        <div><div style={docFieldLabel()}>{t("spaceDesc")}</div><input value={desc} onChange={(e) => setDesc(e.target.value)} style={ss} /></div>
        <div><div style={docFieldLabel()}>{t("color")}</div><div style={{ display: "flex", gap: 8 }}>{COLORS.map((c) => <button key={c} type="button" onClick={() => setColor(c)} style={{ width: 26, height: 26, borderRadius: "50%", background: c, border: color === c ? "2px solid var(--ink)" : "2px solid transparent", cursor: "pointer" }} />)}</div></div>
      </div>
      <div style={{ padding: "13px 22px", borderTop: "1px solid var(--line)", display: "flex", justifyContent: "flex-end", gap: 10 }}>
        <Button variant="ghost" onClick={onClose}>{t("cancel")}</Button>
        <Button icon="check" onClick={save} disabled={busy || !name.trim()}>{space ? t("save") : t("createSpace")}</Button>
      </div>
    </Modal>
  );
}

/* ============================================================
   Page view + editor
   ============================================================ */
function DocPageView({ pageId, user, navigate }) {
  const { t } = useDT();
  const [data, setData] = useState(null);
  const [space, setSpace] = useState(null);
  const [crumbs, setCrumbs] = useState([]);
  const [editing, setEditing] = useState(false);
  const [fav, setFav] = useState(false);
  const [access, setAccess] = useState(false);
  const [share, setShare] = useState(false);
  const [tab, setTab] = useState(null); // 'history'|'links'|null (comments live at page end)
  function reload() {
    MesperDB.docsGetPage(pageId).then((d) => {
      setData(d);
      if (d.page.spaceId) { MesperDB.docsListSpaces().then((sp) => setSpace(sp.find((x) => x.id === d.page.spaceId) || null)); MesperDB.docsListTree(d.page.spaceId).then((pg) => setCrumbs(docCrumbTrail(pg, pageId))).catch(() => {}); }
      if (!d.page.bodyMd && !d.page.title.trim()) setEditing(true);
    }).catch(() => setData(false));
  }
  useEffect(() => { reload(); MesperDB.docsListFavorites(user.id).then((f) => setFav(f.some((p) => p.id === pageId))).catch(() => {}); }, [pageId]);
  async function toggleFav() { const nx = !fav; setFav(nx); try { await MesperDB.docsToggleFavorite(pageId, user.id, nx); } catch (e) {} }
  async function toggleTask(pid, index, checked) {
    const cur = data && data.page && data.page.bodyHtml; if (cur == null) return;
    const nx = docToggleTaskHtml(cur, index, checked);
    setData((d) => (d && d.page) ? { ...d, page: { ...d.page, bodyHtml: nx } } : d);
    try { await MesperDB.docsUpdatePage(pid, { bodyHtml: nx, updatedBy: user.id }); } catch (e) {}
  }
  if (data === null) return <div style={{ color: "var(--ink-4)" }}>…</div>;
  if (data === false) return <div style={{ color: "var(--ink-4)" }}>—</div>;
  const page = data.page;
  if (page.isFolder) return <DocFolderView folder={page} user={user} navigate={navigate} onChanged={reload} />;
  if (editing) return <DocEditor page={page} user={user} space={space} onDone={() => { setEditing(false); reload(); }} onCancel={() => setEditing(false)} navigate={navigate} />;

  const isGuest = !!(user && user.isGuest);
  const editorPerson = docPerson(page.updatedBy);
  const creatorPerson = docPerson(page.createdBy) || docPerson(page.updatedBy);
  const shareUrl = "https://app.mesper.de/docs/p/" + page.id;
  const kebabItems = [
    { label: t("star") + (fav ? " ✓" : ""), icon: "star", onClick: toggleFav },
    { label: t("backlinks"), icon: "link", onClick: () => setTab(tab === "links" ? null : "links") },
    { label: t("versions"), icon: "history", onClick: () => setTab(tab === "history" ? null : "history") },
    { label: t("duplicate"), icon: "file", onClick: async () => { try { const dup = await MesperDB.docsDuplicatePage(pageId); navigate("/docs/p/" + dup.id); } catch (e) { window.alert(t("duplicateError")); } } },
  ];
  if (!isGuest) {
    kebabItems.push({ label: t("access"), icon: page.restricted ? "lock" : "unlock", onClick: () => setAccess(true) });
    kebabItems.push({ label: t("archive"), icon: "book", onClick: async () => { try { await MesperDB.docsArchivePage(pageId, true); } catch (e) {} navigate(page.parentId ? "/docs/p/" + page.parentId : "/docs/s/" + page.spaceId); } });
    kebabItems.push({ label: t("moveToTrash"), icon: "trash", danger: true, onClick: async () => { try { await MesperDB.docsTrashPage(pageId, page.spaceId); } catch (e) {} navigate(page.parentId ? "/docs/p/" + page.parentId : "/docs/s/" + page.spaceId); } });
  }
  return (
    <div style={{ maxWidth: 1240, margin: "0 auto" }}>
      {/* row 1: breadcrumb (left) + updated / editor / actions (right) */}
      <div style={{ display: "flex", alignItems: "center", gap: 12, flexWrap: "wrap", marginBottom: 16 }}>
        <div style={{ display: "flex", alignItems: "center", gap: 6, flexWrap: "wrap", fontSize: 12.5, color: "var(--ink-4)", flex: "1 1 auto", minWidth: 0 }}>
          {space && <><button type="button" onClick={() => navigate("/docs/s/" + space.id)} style={docCrumbBtn()}>{space.name}</button><Icon name="chevRight" size={12} /></>}
          {crumbs.slice(0, -1).map((c) => <React.Fragment key={c.id}><button type="button" onClick={() => navigate("/docs/p/" + c.id)} style={docCrumbBtn()}>{c.title}</button><Icon name="chevRight" size={12} /></React.Fragment>)}
          <span style={{ color: "var(--ink-3)", fontWeight: 600 }}>{page.title}</span>
        </div>
        <div style={{ display: "flex", alignItems: "center", gap: 8, flex: "0 0 auto" }}>
          <span style={{ fontSize: 12.5, color: "var(--ink-4)", whiteSpace: "nowrap" }}>{t("updatedLabel")}: {docRel(page.updatedAt)}</span>
          {editorPerson && <Avatar person={editorPerson} size={22} />}
          <Button size="sm" onClick={() => setEditing(true)}>{t("edit")}</Button>
          <Button size="sm" variant="outline" icon="link" onClick={() => setShare(true)}>{t("share")}</Button>
          <DocCopyBtn text={shareUrl} iconOnly />
          <DocKebab items={kebabItems} />
        </div>
      </div>
      {/* title row */}
      <div style={{ display: "flex", alignItems: "flex-start", gap: 12, marginBottom: 6 }}>
        <span style={{ fontSize: 30, lineHeight: 1.1, flex: "0 0 auto" }}>{page.icon || "📄"}</span>
        <h1 style={{ fontSize: 30, margin: 0, flex: 1, minWidth: 0, lineHeight: 1.15 }}>{page.title}</h1>
      </div>
      {/* byline */}
      {(() => {
        const byName = docPersonName(page.createdBy) || docPersonName(page.updatedBy);
        if (!byName) return <div style={{ marginBottom: 22 }} />;
        return (
          <div style={{ display: "flex", alignItems: "center", gap: 8, marginBottom: 22 }}>
            {creatorPerson && <Avatar person={creatorPerson} size={20} />}
            <span style={{ fontSize: 12.5, color: "var(--ink-4)" }}>{t("byLine")} {byName}</span>
          </div>
        );
      })()}

      {tab === "links" && <DocBacklinksPanel pageId={pageId} outLinks={data.outLinks} navigate={navigate} />}
      {tab === "history" && <DocVersions pageId={pageId} onRestore={reload} />}

      <DocBody md={page.bodyMd} bodyHtml={page.bodyHtml} navigate={navigate} pageId={pageId} onToggleTask={toggleTask} />

      {/* comments fixed to the page end (Confluence pattern) */}
      <div style={{ borderTop: "1px solid var(--line)", marginTop: 34, paddingTop: 22 }}>
        <h2 style={{ fontSize: 17, margin: "0 0 14px" }}>{t("comments")}</h2>
        <DocComments pageId={pageId} pageTitle={page.title} user={user} bare />
      </div>

      {access && !isGuest && <DocAccessModal page={page} user={user} onClose={() => setAccess(false)} onSaved={() => { setAccess(false); reload(); }} />}
      {share && <DocShareModal page={page} space={space} user={user} onClose={() => setShare(false)} />}
    </div>
  );
}

/* ---- copy-to-clipboard button (icon-only for chrome, labelled inside share dialog) ---- */
function DocCopyBtn({ text, label, iconOnly }) {
  const { t } = useDT();
  const [done, setDone] = useState(false);
  function copy() { try { if (navigator.clipboard && navigator.clipboard.writeText) navigator.clipboard.writeText(text); } catch (_) {} setDone(true); setTimeout(() => setDone(false), 1500); }
  if (iconOnly) return (
    <button type="button" onClick={copy} title={done ? t("copied") : t("copyLink")}
      style={{ border: "1px solid var(--line-2)", background: "var(--surface)", cursor: "pointer", color: done ? "var(--st-approved, #1FA36B)" : "var(--ink-3)", padding: "6px 8px", borderRadius: "var(--r-sm)", display: "inline-flex", alignItems: "center" }}>
      <Icon name={done ? "check" : "link"} size={16} />
    </button>
  );
  return <Button size="sm" variant="outline" icon={done ? "check" : "link"} onClick={copy}>{done ? t("copied") : (label || t("copyLink"))}</Button>;
}

/* ---- share dialog: copy link + external-guest management (internal only) ---- */
function DocShareModal({ page, space, user, onClose }) {
  const { t } = useDT();
  const isGuest = !!(user && user.isGuest);
  const spaceId = page.spaceId || (space && space.id) || null;
  const url = "https://app.mesper.de/docs/p/" + page.id;
  return (
    <Modal onClose={onClose} width={460}>
      <div style={{ padding: "18px 22px", borderBottom: "1px solid var(--line)", display: "flex", alignItems: "center", gap: 9 }}>
        <Icon name="link" size={18} style={{ color: "var(--ink-4)" }} />
        <h2 style={{ fontSize: 18, margin: 0 }}>{t("share")}</h2>
      </div>
      <div style={{ padding: "16px 22px", display: "grid", gridTemplateColumns: "minmax(0, 1fr)", gap: 18 }}>
        <div>
          <div style={docFieldLabel()}>{t("copyLink")}</div>
          <div style={{ display: "flex", gap: 8 }}>
            <input readOnly value={url} onFocus={(e) => e.target.select()} style={{ ...(typeof selectStyle !== "undefined" ? selectStyle : { width: "100%", padding: "9px 11px", border: "1px solid var(--line-2)", borderRadius: 8, fontSize: 14 }), flex: 1, minWidth: 0 }} />
            <DocCopyBtn text={url} label={t("copyLink")} />
          </div>
        </div>
        {!isGuest && spaceId && (
          <div style={{ borderTop: "1px solid var(--line)", paddingTop: 16 }}>
            <DocGuestManager spaceId={spaceId} space={space} />
          </div>
        )}
      </div>
      <div style={{ padding: "13px 22px", borderTop: "1px solid var(--line)", display: "flex", justifyContent: "flex-end" }}>
        <Button variant="ghost" onClick={onClose}>{t("closeBtn")}</Button>
      </div>
    </Modal>
  );
}

/* ---- guest list + invite form for a space; shared by DocShareModal (per-document) and DocSpaceInviteModal (per-client) ---- */
function DocGuestManager({ spaceId, space }) {
  const { t } = useDT();
  const [guests, setGuests] = useState(null);
  const [email, setEmail] = useState("");
  const [name, setName] = useState("");
  const [busy, setBusy] = useState(false);
  const [err, setErr] = useState("");
  const [okMsg, setOkMsg] = useState("");
  const ss = typeof selectStyle !== "undefined" ? selectStyle : { width: "100%", padding: "9px 11px", border: "1px solid var(--line-2)", borderRadius: 8, fontSize: 14 };
  function reload() { if (!spaceId) return; MesperDB.docsListGuests(spaceId).then(setGuests).catch(() => setGuests([])); }
  useEffect(() => { reload(); }, [spaceId]);
  async function invite() {
    const e = email.trim(); if (!e || busy) return; setBusy(true); setErr(""); setOkMsg("");
    try {
      const nm = name.trim();
      await MesperDB.docsInviteGuest(spaceId, e, nm);
      setEmail(""); setName(""); reload();
      // Invite is saved regardless of the mail outcome — the guest can
      // always fall back to the copied invite text (see DocGuestRow).
      try { await MesperDB.docsSendGuestInviteEmail(e, nm); setOkMsg(t("inviteMailSent")); }
      catch (mailEx) { setOkMsg(t("inviteSavedCopyHint")); }
    } catch (ex) { setErr((ex && ex.message) || t("inviteErr")); }
    setBusy(false);
  }
  async function remove(id) { try { await MesperDB.docsRemoveGuest(id); } catch (ex) {} reload(); }
  return (
    <div>
      <div style={docFieldLabel()}>{t("externalGuests")}{space ? " (" + t("area") + " " + space.name + ")" : ""}</div>
      <div style={{ display: "flex", flexDirection: "column", gap: 6, marginBottom: 12 }}>
        {guests === null ? <div style={{ fontSize: 13, color: "var(--ink-4)" }}>…</div>
          : guests.length === 0 ? <div style={{ fontSize: 13, color: "var(--ink-4)" }}>{t("noGuests")}</div>
          : guests.map((g) => (
            <DocGuestRow key={g.id} guest={g} onRemove={() => remove(g.id)} />
          ))}
      </div>
      <div style={{ display: "grid", gridTemplateColumns: "1fr 1fr", gap: 8, marginBottom: 8 }}>
        <input value={email} onChange={(e) => setEmail(e.target.value)} placeholder={t("guestEmail")} type="email" style={ss} onKeyDown={(e) => { if (e.key === "Enter") { e.preventDefault(); invite(); } }} />
        <input value={name} onChange={(e) => setName(e.target.value)} placeholder={t("guestName")} style={ss} onKeyDown={(e) => { if (e.key === "Enter") { e.preventDefault(); invite(); } }} />
      </div>
      {err && <div style={{ fontSize: 12.5, color: "var(--st-declined, #E5484D)", marginBottom: 8 }}>{err}</div>}
      {okMsg && !err && <div style={{ fontSize: 12.5, color: "var(--ink-3)", background: "var(--surface-2)", border: "1px solid var(--line)", borderRadius: "var(--r-sm)", padding: "7px 10px", marginBottom: 8, lineHeight: 1.45 }}>{okMsg}</div>}
      <div style={{ display: "flex", justifyContent: "flex-end", marginBottom: 10 }}>
        <Button size="sm" icon="plus" onClick={invite} disabled={busy || !email.trim()}>{t("invite")}</Button>
      </div>
      <div style={{ fontSize: 12, color: "var(--ink-4)", lineHeight: 1.5 }}>{t("guestHint")}</div>
    </div>
  );
}

/* invite text sent outside the system (Slack/mail/phone) so the guest can self-register */
function docInviteText(guest) {
  return `Hallo${guest.displayName ? " " + guest.displayName : ""},

du hast Zugriff auf einen Bereich in Mesper Docs erhalten. Erstelle dein Konto:
1. Öffne https://app.mesper.de
2. Wähle unten "Externer Gast?" → "Konto erstellen"
3. Nutze diese E-Mail-Adresse: ${guest.email}
4. Bestätige die E-Mail, die du danach erhältst

Danach kannst du dich jederzeit mit E-Mail und Passwort anmelden.`;
}

/* one guest row in the share dialog: registered guests get a password-reset button,
   pending (not yet registered) guests get a copy-invite-text button */
function DocGuestRow({ guest, onRemove }) {
  const { t } = useDT();
  const registered = guest.userId != null;
  const [st, setSt] = useState(""); // "" | sending | sent | err
  const [copied, setCopied] = useState(false);
  const mounted = useRef(true);
  useEffect(() => () => { mounted.current = false; }, []);
  async function sendReset() {
    if (st === "sending") return;
    setSt("sending");
    try {
      const res = await MesperDB.sendGuestPasswordReset(guest.email);
      if (res && res.error) throw res.error;
      if (mounted.current) setSt("sent");
    } catch (ex) { if (mounted.current) setSt("err"); }
    setTimeout(() => { if (mounted.current) setSt((s) => (s === "sent" || s === "err") ? "" : s); }, 2800);
  }
  function copyInvite() {
    try { if (navigator.clipboard && navigator.clipboard.writeText) navigator.clipboard.writeText(docInviteText(guest)); } catch (_) {}
    setCopied(true);
    setTimeout(() => { if (mounted.current) setCopied(false); }, 1500);
  }
  const resetColor = st === "sent" ? "var(--st-approved, #2E7D32)" : st === "err" ? "var(--st-declined, #E5484D)" : "var(--ink-4)";
  const resetLabel = st === "sent" ? t("resetSent") : st === "err" ? t("resetFail") : st === "sending" ? "…" : t("sendReset");
  return (
    <div style={{ display: "flex", alignItems: "center", gap: 10, padding: "7px 10px", border: "1px solid var(--line)", borderRadius: "var(--r-sm)", background: "var(--surface-2)" }}>
      <div style={{ flex: 1, minWidth: 0 }}>
        <div style={{ display: "flex", alignItems: "center", gap: 7 }}>
          <div style={{ fontSize: 13.5, color: "var(--ink)", whiteSpace: "nowrap", overflow: "hidden", textOverflow: "ellipsis" }}>{guest.email}</div>
          <span style={{ flex: "0 0 auto", fontSize: 11, fontWeight: 600, padding: "2px 8px", borderRadius: "var(--r-pill)", whiteSpace: "nowrap",
            color: registered ? "var(--ink-4)" : "var(--st-pending)", background: registered ? "var(--surface)" : "var(--st-pending-soft)" }}>
            {registered ? t("statusRegistered") : t("statusPending")}
          </span>
        </div>
        {guest.displayName && <div style={{ fontSize: 12, color: "var(--ink-4)" }}>{guest.displayName}</div>}
      </div>
      {registered ? (
        <button type="button" onClick={sendReset} disabled={st === "sending"} title={t("sendReset")}
          style={{ display: "inline-flex", alignItems: "center", gap: 4, border: "1px solid var(--line-2)", background: "var(--surface)", borderRadius: "var(--r-sm)", cursor: st === "sending" ? "default" : "pointer", color: resetColor, padding: "3px 8px", fontSize: 12, fontWeight: 600, whiteSpace: "nowrap", flex: "0 0 auto" }}>
          <Icon name="mail" size={13} /> {resetLabel}
        </button>
      ) : (
        <button type="button" onClick={copyInvite} title={t("copyInvite")}
          style={{ display: "inline-flex", alignItems: "center", gap: 4, border: "1px solid var(--line-2)", background: "var(--surface)", borderRadius: "var(--r-sm)", cursor: "pointer", color: copied ? "var(--st-approved, #1FA36B)" : "var(--ink-4)", padding: "3px 8px", fontSize: 12, fontWeight: 600, whiteSpace: "nowrap", flex: "0 0 auto" }}>
          <Icon name={copied ? "check" : "link"} size={13} /> {copied ? t("inviteCopied") : t("copyInvite")}
        </button>
      )}
      <button type="button" onClick={onRemove} title={t("delete")} style={{ border: "none", background: "transparent", cursor: "pointer", color: "var(--ink-4)", padding: 4, display: "flex", flex: "0 0 auto" }}><Icon name="x" size={15} /></button>
    </div>
  );
}

/* per-page access control (E) */
function DocAccessModal({ page, user, onClose, onSaved }) {
  const { t } = useDT();
  const [restricted, setRestricted] = useState(!!page.restricted);
  const [ids, setIds] = useState([]);
  const [applySub, setApplySub] = useState(false);
  const [busy, setBusy] = useState(false);
  const people = (typeof VP_DATA !== "undefined" && VP_DATA.people) || [];
  useEffect(() => { MesperDB.docsGetPageAccess(page.id).then((a) => { setRestricted(a.restricted); setIds(a.userIds && a.userIds.length ? a.userIds : [user.id]); }).catch(() => setIds([user.id])); }, [page.id]);
  function toggle(uid) { setIds((xs) => xs.includes(uid) ? xs.filter((x) => x !== uid) : [...xs, uid]); }
  async function save() { if (busy) return; setBusy(true); const userIds = restricted ? Array.from(new Set([...ids, user.id])) : []; try { await MesperDB.docsSetPageAccess(page.id, { restricted, userIds, applyToSubpages: applySub, spaceId: page.spaceId }); onSaved(); } catch (e) { setBusy(false); } }
  return (
    <Modal onClose={onClose} width={420}>
      <div style={{ padding: "18px 22px", borderBottom: "1px solid var(--line)" }}><h2 style={{ fontSize: 18, margin: 0 }}>{t("access")}</h2></div>
      <div style={{ padding: "16px 22px", display: "grid", gap: 14 }}>
        <div style={{ display: "flex", flexDirection: "column", gap: 8 }}>
          <label style={{ display: "flex", gap: 10, alignItems: "flex-start", cursor: "pointer", padding: "10px 12px", border: `1px solid ${!restricted ? "var(--accent)" : "var(--line-2)"}`, borderRadius: "var(--r-md)", background: !restricted ? "var(--accent-soft)" : "var(--surface)" }}>
            <input type="radio" checked={!restricted} onChange={() => setRestricted(false)} style={{ marginTop: 2, accentColor: "var(--accent)" }} />
            <div><div style={{ fontWeight: 600, fontSize: 14 }}>{t("everyone")}</div><div style={{ fontSize: 12.5, color: "var(--ink-4)" }}>{t("everyoneDesc")}</div></div>
          </label>
          <label style={{ display: "flex", gap: 10, alignItems: "flex-start", cursor: "pointer", padding: "10px 12px", border: `1px solid ${restricted ? "var(--accent)" : "var(--line-2)"}`, borderRadius: "var(--r-md)", background: restricted ? "var(--accent-soft)" : "var(--surface)" }}>
            <input type="radio" checked={restricted} onChange={() => setRestricted(true)} style={{ marginTop: 2, accentColor: "var(--accent)" }} />
            <div><div style={{ fontWeight: 600, fontSize: 14 }}>{t("specificPeople")}</div><div style={{ fontSize: 12.5, color: "var(--ink-4)" }}>{t("specificDesc")}</div></div>
          </label>
        </div>
        {restricted && (
          <div>
            <div style={docFieldLabel()}>{t("whoCanAccess")}</div>
            <div style={{ display: "flex", flexDirection: "column", gap: 4, maxHeight: 200, overflowY: "auto" }}>
              {people.map((p) => (
                <label key={p.id} style={{ display: "flex", alignItems: "center", gap: 9, padding: "6px 8px", borderRadius: "var(--r-sm)", cursor: p.id === user.id ? "default" : "pointer", background: ids.includes(p.id) ? "var(--surface-2)" : "transparent" }}>
                  <input type="checkbox" checked={ids.includes(p.id)} disabled={p.id === user.id} onChange={() => toggle(p.id)} style={{ accentColor: "var(--accent)" }} />
                  <Avatar person={p} size={22} /><span style={{ fontSize: 13.5 }}>{`${p.first || ""} ${p.last || ""}`.trim()}{p.id === user.id ? " (du)" : ""}</span>
                </label>
              ))}
            </div>
            <label style={{ display: "inline-flex", alignItems: "center", gap: 8, marginTop: 10, fontSize: 13, color: "var(--ink-3)", cursor: "pointer" }}>
              <input type="checkbox" checked={applySub} onChange={(e) => setApplySub(e.target.checked)} style={{ accentColor: "var(--accent)" }} /> {t("applySubpages")}
            </label>
          </div>
        )}
      </div>
      <div style={{ padding: "13px 22px", borderTop: "1px solid var(--line)", display: "flex", justifyContent: "flex-end", gap: 10 }}>
        <Button variant="ghost" onClick={onClose}>{t("cancel")}</Button>
        <Button icon="check" onClick={save} disabled={busy}>{t("save")}</Button>
      </div>
    </Modal>
  );
}
function docCrumbBtn() { return { border: "none", background: "transparent", cursor: "pointer", color: "var(--ink-4)", padding: 0, fontSize: 12.5 }; }
function docPageTab(on) { return { display: "inline-flex", alignItems: "center", gap: 5, border: "1px solid var(--line-2)", background: on ? "var(--accent-soft)" : "var(--surface)", color: on ? "var(--accent-ink)" : "var(--ink-3)", borderRadius: "var(--r-pill)", padding: "5px 11px", fontSize: 12, fontWeight: 600, cursor: "pointer" }; }
/* ---- cross-tool "Verknüpfungen" chips: CRM / Board, navigable when the link exists ---- */
function docLinkChip() { return { display: "inline-flex", alignItems: "center", gap: 5, border: "1px solid var(--line-2)", background: "var(--surface)", borderRadius: "var(--r-pill)", padding: "5px 11px", fontSize: 12, fontWeight: 600, color: "var(--ink-2)", cursor: "pointer" }; }
function SpaceLinksRow({ spaceId, navigate }) {
  const { t } = useDT();
  const [links, setLinks] = useState(null);
  const [busy, setBusy] = useState(false);
  function reload() { MesperDB.clientLinks({ spaceId }).then(setLinks).catch(() => setLinks(null)); }
  useEffect(() => { reload(); }, [spaceId]);
  async function createBoardProject() {
    if (!links || !links.companyId || busy) return;
    setBusy(true);
    try { await MesperDB.clientProvisionFromCompany(links.companyId); } catch (_) {}
    setBusy(false);
    reload();
  }
  if (!links || !links.companyId) return null;
  return (
    <div style={{ marginBottom: 20 }}>
      <div style={docFieldLabel()}>{t("linksLabel")}</div>
      <div style={{ display: "flex", flexWrap: "wrap", gap: 8 }}>
        <button type="button" onClick={() => navigate("/crm/c/" + links.companyId)} style={docLinkChip()}><Icon name="check" size={13} /> {t("linkCrm")}: {links.companyName}</button>
        {links.projectId
          ? <button type="button" onClick={() => navigate("/approvals/p/" + links.projectKey)} style={docLinkChip()}><Icon name="check" size={13} /> {t("linkBoard")}: {links.projectKey}</button>
          : <Button size="sm" variant="outline" icon="plus" onClick={createBoardProject} disabled={busy}>{t("createBoard")}</Button>}
      </div>
    </div>
  );
}
function docCrumbTrail(pages, pageId) { const byId = {}; pages.forEach((p) => (byId[p.id] = p)); const trail = []; let cur = byId[pageId]; let guard = 0; while (cur && guard++ < 20) { trail.unshift({ id: cur.id, title: cur.title }); cur = cur.parentId ? byId[cur.parentId] : null; } return trail; }

/* ---- backlinks panel ---- */
function DocBacklinksPanel({ pageId, outLinks, navigate }) {
  const { t } = useDT();
  const [inbound, setInbound] = useState(null);
  useEffect(() => { MesperDB.docsListBacklinks("doc", pageId).then(setInbound).catch(() => setInbound([])); }, [pageId]);
  const out = (outLinks || []);
  return (
    <div style={{ border: "1px solid var(--line)", borderRadius: "var(--r-md)", background: "var(--surface-2)", padding: "14px 16px", marginBottom: 18, display: "grid", gap: 14 }}>
      <div>
        <div style={docSideHead()}>{t("linksTo")}</div>
        {out.length === 0 ? <div style={{ fontSize: 13, color: "var(--ink-4)" }}>{t("noBacklinks")}</div> : (
          <div style={{ display: "flex", flexWrap: "wrap", gap: 7 }}>
            {out.map((l) => <button key={l.id} type="button" onClick={() => docNavigateRef(l.targetType, l.targetId, navigate)} style={docRefChip()}><Icon name={DOC_REF_ICON[l.targetType] || "link"} size={12} /> {l.label || l.targetId}</button>)}
          </div>
        )}
      </div>
      <div>
        <div style={docSideHead()}>{t("linkedFrom")}</div>
        {inbound === null ? <div style={{ fontSize: 13, color: "var(--ink-4)" }}>…</div> : inbound.length === 0 ? <div style={{ fontSize: 13, color: "var(--ink-4)" }}>{t("noBacklinks")}</div> : (
          <div style={{ display: "flex", flexWrap: "wrap", gap: 7 }}>
            {inbound.map((p) => <button key={p.pageId} type="button" onClick={() => navigate("/docs/p/" + p.pageId)} style={docRefChip()}><span>{p.icon || "📄"}</span> {p.title}</button>)}
          </div>
        )}
      </div>
    </div>
  );
}
function docRefChip() { return { display: "inline-flex", alignItems: "center", gap: 5, border: "1px solid var(--line-2)", background: "var(--surface)", borderRadius: "var(--r-pill)", padding: "4px 10px", fontSize: 12.5, fontWeight: 600, color: "var(--ink-2)", cursor: "pointer" }; }

/* ---- versions ---- */
function DocVersions({ pageId, onRestore }) {
  const { t } = useDT();
  const [list, setList] = useState(null);
  useEffect(() => { MesperDB.docsListVersions(pageId).then(setList).catch(() => setList([])); }, [pageId]);
  async function restore(v) { try { await MesperDB.docsRestoreVersion(pageId, v.id); onRestore(); } catch (e) {} }
  return (
    <div style={{ border: "1px solid var(--line)", borderRadius: "var(--r-md)", background: "var(--surface-2)", padding: "14px 16px", marginBottom: 18 }}>
      <div style={docSideHead()}>{t("versions")}</div>
      {list === null ? <div style={{ fontSize: 13, color: "var(--ink-4)" }}>…</div> : list.length === 0 ? <div style={{ fontSize: 13, color: "var(--ink-4)" }}>—</div> : (
        <div style={{ display: "flex", flexDirection: "column", gap: 4 }}>
          {list.map((v, i) => (
            <div key={v.id} style={{ display: "flex", alignItems: "center", gap: 10, padding: "7px 4px", borderTop: i ? "1px solid var(--line)" : "none" }}>
              <span style={{ fontSize: 13, color: "var(--ink-2)", flex: 1 }}>{v.title || "—"}</span>
              <span style={{ fontSize: 12, color: "var(--ink-4)" }}>{docRel(v.at)}{v.editedBy ? " · " + docPersonName(v.editedBy) : ""}</span>
              {i > 0 && <button type="button" onClick={() => restore(v)} style={{ border: "1px solid var(--line-2)", background: "var(--surface)", borderRadius: "var(--r-sm)", padding: "3px 9px", fontSize: 12, fontWeight: 600, cursor: "pointer", color: "var(--accent-ink)" }}>{t("restore")}</button>}
            </div>
          ))}
        </div>
      )}
    </div>
  );
}

/* ---- comments (mention-capable composer + @mention notifications) ---- */
function DocComments({ pageId, pageTitle, user, bare }) {
  const { t } = useDT();
  const [list, setList] = useState(null);
  function reload() { MesperDB.docsListComments(pageId).then(setList).catch(() => setList([])); }
  useEffect(() => { reload(); }, [pageId]);
  async function add(body, mentionIds) { const text = (body || "").trim(); if (!text) return; try { await MesperDB.docsAddComment({ pageId, pageTitle, body: text, mentions: mentionIds || [] }); } catch (e) {} reload(); }
  async function del(id) { try { await MesperDB.docsDeleteComment(id); } catch (e) {} reload(); }
  const people = (typeof VP_DATA !== "undefined" && VP_DATA.people) || [];
  const wrapStyle = bare ? {} : { border: "1px solid var(--line)", borderRadius: "var(--r-md)", background: "var(--surface-2)", padding: "14px 16px", marginBottom: 18 };
  return (
    <div style={wrapStyle}>
      {!bare && <div style={docSideHead()}>{t("comments")}</div>}
      <div style={{ marginBottom: (list && list.length) ? 12 : 0 }}>
        {typeof MentionInput === "function"
          ? <MentionInput people={people} onSend={add} placeholder={t("writeComment") + "  ·  @"} />
          : <div style={{ display: "flex", gap: 8 }}><textarea id="docc" rows={2} placeholder={t("writeComment")} style={{ ...(typeof selectStyle !== "undefined" ? selectStyle : {}), resize: "vertical" }} /><Button size="sm" icon="check" onClick={() => { const el = document.getElementById("docc"); add(el.value); el.value = ""; }} /></div>}
      </div>
      <div style={{ display: "flex", flexDirection: "column", gap: 8 }}>
        {(list || []).map((c) => (
          <div key={c.id} style={{ background: "var(--surface)", border: "1px solid var(--line)", borderRadius: "var(--r-sm)", padding: "8px 11px" }}>
            <div style={{ display: "flex", alignItems: "baseline", gap: 8, marginBottom: 3 }}>
              <span style={{ fontSize: 12, fontWeight: 700, color: "var(--ink-2)" }}>{docPersonName(c.createdBy) || "—"}</span>
              <span style={{ fontSize: 11, color: "var(--ink-4)" }}>{docRel(c.at)}</span>
              <div style={{ flex: 1 }} />
              {c.createdBy === user.id && <button type="button" onClick={() => del(c.id)} style={{ border: "none", background: "transparent", cursor: "pointer", color: "var(--ink-4)", padding: 0 }}><Icon name="x" size={12} /></button>}
            </div>
            {typeof CommentBody === "function" ? <CommentBody body={c.body} /> : <div style={{ fontSize: 13.5, whiteSpace: "pre-wrap" }}>{c.body}</div>}
          </div>
        ))}
      </div>
    </div>
  );
}

/* ============================================================
   WYSIWYG editor
   ============================================================ */
function DocEditor({ page, user, space, onDone, onCancel, navigate }) {
  const { t } = useDT();
  const narrow = typeof useIsNarrow === "function" ? useIsNarrow(820) : false;
  const [title, setTitle] = useState(page.title === "Ohne Titel" || page.title === "Untitled" ? "" : page.title);
  const [icon, setIcon] = useState(page.icon || "");
  const [busy, setBusy] = useState(false);
  const [mention, setMention] = useState(null); // {query, x, y} | null
  const [slash, setSlash] = useState(null);     // {query, x, y} | null
  const [emojiPop, setEmojiPop] = useState(null); // {x, y} | null (slash/inline)
  const [statusPop, setStatusPop] = useState(null); // {x, y} | null
  const [datePop, setDatePop] = useState(null); // {x, y, el} | null — el is the clicked .doc-date span
  const [tableUI, setTableUI] = useState(null); // {top, left} | null
  const edRef = useRef(null);
  const wrapRef = useRef(null);
  const fileRef = useRef(null);
  const savedRange = useRef(null);
  const activeCellRef = useRef(null);
  const activeTableRef = useRef(null);
  const tableUIRef = useRef(null);
  const dirtyRef = useRef(false);
  function markDirty() { dirtyRef.current = true; }
  function handleCancel() { if (dirtyRef.current && !window.confirm(t("confirmDiscard"))) return; onCancel(); }
  const editorPerson = docPerson(page.updatedBy) || docPerson(user.id);

  // initialize editor HTML from stored HTML (or legacy markdown), then resolve media images
  useEffect(() => {
    const el = edRef.current; if (!el) return;
    el.innerHTML = (page.bodyHtml && page.bodyHtml.trim()) ? docSanitizeHtml(page.bodyHtml) : (page.bodyMd ? (docRenderHtml(page.bodyMd) || "") : "");
    docNormalizeBlocks(el);
    const imgs = Array.prototype.slice.call(el.querySelectorAll('img[src^="media://"]'));
    imgs.forEach((i) => i.setAttribute("data-media", (i.getAttribute("src") || "").slice(8)));
    if (imgs.length && MesperDB.docsSignMedia) { MesperDB.docsSignMedia(imgs.map((i) => i.getAttribute("data-media"))).then((map) => imgs.forEach((i) => { const p = i.getAttribute("data-media"); if (map[p]) i.src = map[p]; })).catch(() => {}); }
    el.querySelectorAll(".doc-tasks input[type=checkbox]").forEach((b) => { b.checked = b.hasAttribute("checked"); });
  }, []);

  // execCommand quality: force <p> paragraphs (Chrome defaults to <div>, which
  // makes Enter and list toggling erratic) and never style with inline CSS.
  function primeEditingModes() {
    try { document.execCommand("defaultParagraphSeparator", false, "p"); } catch (_) {}
    try { document.execCommand("styleWithCSS", false, false); } catch (_) {}
  }
  // Top-level bare text/inline nodes make lists jump to the previous line — wrap
  // any loose inline content into <p> blocks so every line is a real block.
  function docNormalizeBlocks(el) {
    if (!el) return;
    const BLOCK = "P,H1,H2,H3,H4,H5,UL,OL,TABLE,BLOCKQUOTE,PRE,HR,DIV";
    let run = [];
    const flush = () => {
      if (!run.length) return;
      const p = document.createElement("p"); run[0].parentNode.insertBefore(p, run[0]);
      run.forEach((nd) => p.appendChild(nd)); run = [];
    };
    Array.prototype.slice.call(el.childNodes).forEach((nd) => {
      const isBlock = nd.nodeType === 1 && nd.matches && nd.matches(BLOCK);
      if (isBlock) { flush(); } else if (nd.nodeType === 3 && !nd.textContent.trim()) { /* skip whitespace */ } else { run.push(nd); }
    });
    flush();
    if (!el.firstChild) el.innerHTML = "<p><br></p>";
  }
  // execCommand list toggling in Chrome wraps the <ul>/<ol> inside the source
  // <p> and leaves empty paragraphs behind — that is the "jumps to the line
  // above" symptom. Lift lists out of paragraphs and drop the empty shells.
  function docFixLists(el) {
    if (!el) return;
    el.querySelectorAll("p > ul, p > ol").forEach((list) => {
      const p = list.parentElement; if (!p || p.parentNode !== el && !el.contains(p)) return;
      while (p.firstChild) p.parentNode.insertBefore(p.firstChild, p);
      p.parentNode.removeChild(p);
    });
    el.querySelectorAll("p").forEach((p) => { if (!p.childNodes.length) p.parentNode.removeChild(p); });
  }

  function focusEd() { const el = edRef.current; if (el) el.focus(); }
  function saveSel() { const s = window.getSelection(); if (s && s.rangeCount) savedRange.current = s.getRangeAt(0).cloneRange(); }
  function restoreSel() { const s = window.getSelection(); if (savedRange.current && s) { s.removeAllRanges(); s.addRange(savedRange.current); } }

  function exec(kind) {
    focusEd(); restoreSel();
    primeEditingModes();
    const sel = window.getSelection();
    if (kind === "bold") document.execCommand("bold");
    else if (kind === "italic") document.execCommand("italic");
    else if (kind === "underline") document.execCommand("underline");
    else if (kind === "strike") document.execCommand("strikeThrough");
    else if (kind === "para") document.execCommand("formatBlock", false, "p");
    else if (kind === "h1") document.execCommand("formatBlock", false, "h1");
    else if (kind === "h2") document.execCommand("formatBlock", false, "h2");
    else if (kind === "h3") document.execCommand("formatBlock", false, "h3");
    else if (kind === "h4") document.execCommand("formatBlock", false, "h4");
    else if (kind === "h5") document.execCommand("formatBlock", false, "h5");
    else if (kind === "ul") { document.execCommand("insertUnorderedList"); docFixLists(edRef.current); }
    else if (kind === "ol") { document.execCommand("insertOrderedList"); docFixLists(edRef.current); }
    else if (kind === "quote") document.execCommand("formatBlock", false, "blockquote");
    else if (kind === "undo") document.execCommand("undo");
    else if (kind === "redo") document.execCommand("redo");
    else if (kind === "code") { const g = window.getSelection(); if (g && g.rangeCount && !g.isCollapsed) { const r = g.getRangeAt(0), c = document.createElement("code"); try { r.surroundContents(c); } catch (_) { c.appendChild(r.extractContents()); r.insertNode(c); } g.removeAllRanges(); } }
    else if (kind === "codeblock") { if (sel && sel.rangeCount) { const r = sel.getRangeAt(0), pre = document.createElement("pre"), code = document.createElement("code"); code.appendChild(r.extractContents()); pre.appendChild(code); r.insertNode(pre); sel.removeAllRanges(); } }
    else if (kind === "task") { document.execCommand("insertHTML", false, '<ul class="doc-tasks"><li><input type="checkbox">&nbsp;</li></ul><p><br></p>'); }
    else if (kind === "table") { document.execCommand("insertHTML", false, '<table><thead><tr><th>&nbsp;</th><th>&nbsp;</th><th>&nbsp;</th></tr></thead><tbody><tr><td>&nbsp;</td><td>&nbsp;</td><td>&nbsp;</td></tr><tr><td>&nbsp;</td><td>&nbsp;</td><td>&nbsp;</td></tr></tbody></table><p><br></p>'); }
    else if (kind === "divider") { document.execCommand("insertHTML", false, "<hr><p><br></p>"); }
    else if (kind === "link") { const url = window.prompt("Link URL", "https://"); if (url) { if (sel && !sel.isCollapsed) document.execCommand("createLink", false, url); else document.execCommand("insertHTML", false, `<a href="${url.replace(/"/g, "&quot;")}">${url.replace(/[<>&]/g, (c) => ({ "<": "&lt;", ">": "&gt;", "&": "&amp;" }[c]))}</a>&nbsp;`); } }
    saveSel();
  }

  // apply / remove a text-colour class over the current selection (no style attribute)
  function applyColor(cls) {
    focusEd(); restoreSel();
    const sel = window.getSelection(); if (!sel || !sel.rangeCount) { saveSel(); return; }
    const r = sel.getRangeAt(0); if (r.collapsed) { saveSel(); return; }
    if (cls) {
      const span = document.createElement("span"); span.className = cls;
      try { r.surroundContents(span); } catch (_) { span.appendChild(r.extractContents()); r.insertNode(span); }
      sel.removeAllRanges();
    } else {
      const frag = r.extractContents(), holder = document.createElement("div"); holder.appendChild(frag);
      holder.querySelectorAll("span[class*='doc-c-']").forEach((s) => { const par = s.parentNode; while (s.firstChild) par.insertBefore(s.firstChild, s); par.removeChild(s); });
      const out = document.createDocumentFragment(); while (holder.firstChild) out.appendChild(holder.firstChild);
      r.insertNode(out); sel.removeAllRanges();
    }
    saveSel();
  }
  function insertHtmlAtCursor(html) { focusEd(); restoreSel(); document.execCommand("insertHTML", false, html); saveSel(); }
  function insertEmoji(e) { focusEd(); restoreSel(); document.execCommand("insertText", false, e); saveSel(); }
  function insertDate(iso) { const d = iso || docToday(); const label = docFormatDate(d).replace(/[<>&]/g, (c) => ({ "<": "&lt;", ">": "&gt;", "&": "&amp;" }[c])); insertHtmlAtCursor(`<span class="doc-date" data-date="${d}" contenteditable="false">${label}</span>&nbsp;`); }
  function updateDateChip(el, iso) { el.setAttribute("data-date", iso); el.textContent = docFormatDate(iso); markDirty(); setDatePop(null); }
  function insertPanel(kind) { const inner = kind === "decision" ? "<p>⤵️&nbsp;</p>" : "<p><br></p>"; insertHtmlAtCursor(`<div class="doc-panel doc-panel-${kind}">${inner}</div><p><br></p>`); }
  function insertStatus(label, color) { const safe = (label || "STATUS").replace(/[<>&]/g, (c) => ({ "<": "&lt;", ">": "&gt;", "&": "&amp;" }[c])); insertHtmlAtCursor(`<span class="doc-status doc-status-${color}" contenteditable="false">${safe}</span>&nbsp;`); }

  // ---- table UX: floating toolbar tracking + DOM operations ----
  function updateTableUI() {
    const el = edRef.current, wrap = wrapRef.current;
    const sel = window.getSelection();
    let cell = null, table = null;
    if (el && sel && sel.anchorNode) {
      const an = sel.anchorNode.nodeType === 3 ? sel.anchorNode.parentElement : sel.anchorNode;
      if (an && el.contains(an) && an.closest) { cell = an.closest("td,th"); table = an.closest("table"); }
    }
    activeCellRef.current = cell; activeTableRef.current = table;
    if (!table || !wrap) { if (tableUIRef.current) { tableUIRef.current = null; setTableUI(null); } return; }
    // Anchor the toolbar to the ACTIVE ROW, not the table top — in a long table
    // the table top can be scrolled far out of view, leaving the controls
    // unreachable ("empty rows cannot be deleted").
    const wr = wrap.getBoundingClientRect(), tr = table.getBoundingClientRect();
    const rowRect = cell && cell.parentElement ? cell.parentElement.getBoundingClientRect() : tr;
    const top = rowRect.top - wr.top - 36, left = tr.left - wr.left;
    const prev = tableUIRef.current;
    if (prev && prev.table === table && Math.abs(prev.top - top) < 1 && Math.abs(prev.left - left) < 1) return;
    tableUIRef.current = { table, top, left }; setTableUI({ top, left });
  }
  function tblAddRow(below) {
    const cell = activeCellRef.current, table = activeTableRef.current; if (!cell || !table) return;
    markDirty();
    const tr = cell.parentElement, cols = tr.cells.length, at = tr.rowIndex + (below ? 1 : 0);
    const nr = table.insertRow(at); for (let i = 0; i < cols; i++) { const td = nr.insertCell(i); td.innerHTML = "<br>"; }
    updateTableUI();
  }
  function tblAddCol(right) {
    const cell = activeCellRef.current, table = activeTableRef.current; if (!cell || !table) return;
    markDirty();
    const idx = cell.cellIndex + (right ? 1 : 0);
    Array.prototype.slice.call(table.rows).forEach((row) => { const head = row.parentElement && row.parentElement.tagName === "THEAD"; const c = document.createElement(head ? "th" : "td"); c.innerHTML = "<br>"; row.insertBefore(c, row.cells[idx] || null); });
    updateTableUI();
  }
  function tblDelRow() {
    const cell = activeCellRef.current, table = activeTableRef.current; if (!cell || !table) return;
    markDirty();
    if (table.rows.length <= 1) { tblDelTable(); return; }
    const tr = cell.parentElement; if (tr.parentElement) tr.parentElement.removeChild(tr);
    activeCellRef.current = null; updateTableUI();
  }
  function tblDelCol() {
    const cell = activeCellRef.current, table = activeTableRef.current; if (!cell || !table) return;
    markDirty();
    if (table.rows[0] && table.rows[0].cells.length <= 1) { tblDelTable(); return; }
    const idx = cell.cellIndex;
    Array.prototype.slice.call(table.rows).forEach((row) => { if (row.cells[idx]) row.removeChild(row.cells[idx]); });
    activeCellRef.current = null; updateTableUI();
  }
  function tblToggleHeader() {
    const table = activeTableRef.current; if (!table) return;
    markDirty();
    const thead = table.querySelector("thead");
    if (thead) {
      let tbody = table.querySelector("tbody"); if (!tbody) { tbody = document.createElement("tbody"); table.appendChild(tbody); }
      Array.prototype.slice.call(thead.rows).reverse().forEach((row) => { const nr = document.createElement("tr"); Array.prototype.slice.call(row.cells).forEach((c) => { const td = document.createElement("td"); td.innerHTML = c.innerHTML; nr.appendChild(td); }); tbody.insertBefore(nr, tbody.firstChild); });
      table.removeChild(thead);
    } else {
      const first = table.rows[0]; if (!first) return;
      const th = document.createElement("thead"), nr = document.createElement("tr");
      Array.prototype.slice.call(first.cells).forEach((c) => { const el = document.createElement("th"); el.innerHTML = c.innerHTML; nr.appendChild(el); });
      th.appendChild(nr); table.insertBefore(th, table.firstChild); if (first.parentElement) first.parentElement.removeChild(first);
    }
    activeCellRef.current = null; updateTableUI();
  }
  function tblDelTable() {
    const table = activeTableRef.current; if (!table) return;
    markDirty();
    const next = table.nextElementSibling; if (table.parentElement) table.parentElement.removeChild(table);
    if (next && next.tagName === "P" && !(next.textContent || "").trim() && next.parentElement) next.parentElement.removeChild(next);
    activeTableRef.current = null; activeCellRef.current = null; tableUIRef.current = null; setTableUI(null);
  }
  useEffect(() => { const h = () => updateTableUI(); document.addEventListener("selectionchange", h); return () => document.removeEventListener("selectionchange", h); }, []);

  // helper: the list item / table cell the caret sits in (if any)
  function caretContext() {
    const sel = window.getSelection(); const el = edRef.current;
    if (!sel || !sel.anchorNode || !el) return {};
    let node = sel.anchorNode; if (node.nodeType === 3) node = node.parentElement;
    if (!node || !el.contains(node) || !node.closest) return {};
    return { li: node.closest("li"), cell: node.closest("td,th"), table: node.closest("table") };
  }

  // Tab / list indent, Enter-in-table, markdown autoformat on Space
  function onEditorKeyDown(e) {
    // Tab: indent/outdent a list, hop table cells, else insert a real indent
    if (e.key === "Tab") {
      const ctx = caretContext();
      if (ctx.li) { e.preventDefault(); primeEditingModes(); document.execCommand(e.shiftKey ? "outdent" : "indent"); docFixLists(edRef.current); markDirty(); saveSel(); return; }
      if (ctx.cell && ctx.table) {
        e.preventDefault();
        const cells = Array.prototype.slice.call(ctx.table.querySelectorAll("th,td"));
        const i = cells.indexOf(ctx.cell); const nx = cells[i + (e.shiftKey ? -1 : 1)];
        if (nx) { const sel = window.getSelection(), rr = document.createRange(); rr.selectNodeContents(nx); rr.collapse(false); sel.removeAllRanges(); sel.addRange(rr); }
        saveSel(); return;
      }
      e.preventDefault(); document.execCommand("insertHTML", false, "&nbsp;&nbsp;&nbsp;&nbsp;"); markDirty(); return;
    }
    // Backspace in an empty cell of a fully empty body row removes the row
    // (the intuitive way to get rid of leftover empty table rows).
    if (e.key === "Backspace") {
      const ctx = caretContext();
      if (ctx.cell && ctx.table && !ctx.cell.textContent.trim() && !ctx.cell.querySelector("img")) {
        const tr = ctx.cell.parentElement;
        const rowEmpty = Array.prototype.every.call(tr.cells, (c) => !c.textContent.trim() && !c.querySelector("img"));
        const isHeader = tr.parentElement && tr.parentElement.tagName === "THEAD";
        if (rowEmpty && !isHeader && ctx.table.rows.length > 1) {
          e.preventDefault(); markDirty();
          const idx = Math.max(0, tr.rowIndex - 1);
          tr.parentElement.removeChild(tr);
          const target = ctx.table.rows[Math.min(idx, ctx.table.rows.length - 1)];
          if (target && target.cells[0]) { const sel = window.getSelection(), rr = document.createRange(); rr.selectNodeContents(target.cells[0]); rr.collapse(false); sel.removeAllRanges(); sel.addRange(rr); }
          activeCellRef.current = null; updateTableUI(); saveSel(); return;
        }
        // otherwise swallow Backspace in an empty cell so it cannot eat the structure
        e.preventDefault(); return;
      }
    }
    if (e.key === "Enter" && !e.shiftKey) {
      const cell = activeCellRef.current, table = activeTableRef.current;
      if (cell && table) {
        const tr = cell.parentElement;
        if (tr.rowIndex === table.rows.length - 1 && cell.cellIndex === tr.cells.length - 1) {
          e.preventDefault(); markDirty();
          const cols = tr.cells.length, nr = table.insertRow(table.rows.length); let firstCell = null;
          for (let i = 0; i < cols; i++) { const td = nr.insertCell(i); td.innerHTML = "<br>"; if (i === 0) firstCell = td; }
          const sel = window.getSelection(); if (sel && firstCell) { const rr = document.createRange(); rr.selectNodeContents(firstCell); rr.collapse(true); sel.removeAllRanges(); sel.addRange(rr); }
          updateTableUI(); saveSel(); return;
        }
      }
      // Enter on an empty list item: outdent one level if nested, else leave
      // the list and drop a clean paragraph below it (Confluence behaviour).
      const ctx = caretContext();
      if (ctx.li && !ctx.li.textContent.trim() && !ctx.li.querySelector("img")) {
        e.preventDefault(); primeEditingModes();
        const list = ctx.li.parentElement;
        if (list && list.parentElement && list.parentElement.closest("li")) {
          document.execCommand("outdent");
        } else if (list) {
          ctx.li.parentNode.removeChild(ctx.li);
          const p = document.createElement("p"); p.innerHTML = "<br>";
          list.parentNode.insertBefore(p, list.nextSibling);
          const rr = document.createRange(); rr.setStart(p, 0); rr.collapse(true);
          const sel = window.getSelection(); sel.removeAllRanges(); sel.addRange(rr);
          if (!list.querySelector("li")) list.parentNode.removeChild(list);
        }
        markDirty(); saveSel(); return;
      }
    }
    if (e.key !== " ") return;
    const sel = window.getSelection(); if (!sel || !sel.rangeCount) return;
    const r = sel.getRangeAt(0); if (!r.collapsed) return;
    const node = r.startContainer; if (!node || node.nodeType !== 3) return;
    const before = node.textContent.slice(0, r.startOffset);
    let cmd = null;
    if (before === "#") cmd = "h1"; else if (before === "##") cmd = "h2"; else if (before === "###") cmd = "h3";
    else if (before === "-" || before === "*") cmd = "ul"; else if (before === "1.") cmd = "ol"; else if (before === "[]") cmd = "task";
    if (!cmd) return;
    e.preventDefault();
    const rr = document.createRange(); rr.setStart(node, 0); rr.setEnd(node, r.startOffset); sel.removeAllRanges(); sel.addRange(rr); document.execCommand("insertText", false, "");
    saveSel(); exec(cmd);
  }
  // Paste: keep files as image upload, otherwise clean copied HTML (or plain
  // text) and drop it at the caret without dragging in foreign block structure.
  function onEditorPaste(e) {
    const cd = e.clipboardData; if (!cd) return;
    if (cd.files && cd.files.length) { e.preventDefault(); onFiles(cd.files); return; }
    e.preventDefault(); primeEditingModes();
    const html = cd.getData("text/html"); const text = cd.getData("text/plain");
    if (html && html.trim()) {
      const clean = docSanitizePaste(html);
      if (clean) { document.execCommand("insertHTML", false, clean); markDirty(); saveSel(); return; }
    }
    if (text) {
      // insertText keeps a single line inline (no stray block break) and lets the
      // browser handle newlines natively for multi-line pastes.
      document.execCommand("insertText", false, text);
    }
    markDirty(); saveSel();
  }

  // ---- table column resize (drag the border between two columns) ----
  const colDrag = useRef(null);
  function cellEdgeAt(e) {
    const el = edRef.current; if (!el) return null;
    let node = e.target; if (node && node.nodeType === 3) node = node.parentElement;
    const cell = node && node.closest ? node.closest("td,th") : null;
    if (!cell || !el.contains(cell)) return null;
    const r = cell.getBoundingClientRect();
    if (e.clientX >= r.right - 6) return { cell, side: "right" };
    if (e.clientX <= r.left + 6 && cell.cellIndex > 0) { const prev = cell.parentElement.cells[cell.cellIndex - 1]; return prev ? { cell: prev, side: "right" } : null; }
    return null;
  }
  function onEditorMouseMove(e) {
    if (colDrag.current) {
      const dx = e.clientX - colDrag.current.startX;
      const w = Math.max(48, colDrag.current.startW + dx);
      colDrag.current.cells.forEach((c) => c.setAttribute("width", String(Math.round(w))));
      return;
    }
    const el = edRef.current; if (!el) return;
    el.style.cursor = cellEdgeAt(e) ? "col-resize" : "";
  }
  function onEditorMouseDown(e) {
    const edge = cellEdgeAt(e); if (!edge) return;
    e.preventDefault();
    const table = edge.cell.closest("table"); const idx = edge.cell.cellIndex;
    const cells = Array.prototype.slice.call(table.rows).map((row) => row.cells[idx]).filter(Boolean);
    colDrag.current = { cells, startX: e.clientX, startW: edge.cell.getBoundingClientRect().width };
    const move = (ev) => onEditorMouseMove(ev);
    const up = () => { colDrag.current = null; markDirty(); document.removeEventListener("mousemove", move); document.removeEventListener("mouseup", up); };
    document.addEventListener("mousemove", move); document.addEventListener("mouseup", up);
  }

  // toggle task checkboxes inside the editor (contenteditable would swallow the native toggle)
  function onEditorClick(e) {
    const tgt = e.target;
    if (tgt && tgt.tagName === "INPUT" && tgt.type === "checkbox") { e.preventDefault(); markDirty(); const on = !tgt.hasAttribute("checked"); if (on) tgt.setAttribute("checked", ""); else tgt.removeAttribute("checked"); tgt.checked = on; return; }
    const dateEl = tgt && tgt.closest && tgt.closest(".doc-date");
    if (dateEl) { e.preventDefault(); const r = dateEl.getBoundingClientRect(); setDatePop({ x: r.left, y: r.bottom, el: dateEl }); return; }
    updateTableUI();
  }

  // image upload → insert <img data-media> (private bucket)
  async function onFiles(list) {
    const file = list && list[0]; if (!file || !file.type.startsWith("image/")) return;
    focusEd(); restoreSel();
    try { const { path } = await MesperDB.docsUploadMedia(file); const map = await MesperDB.docsSignMedia([path]); const url = map[path] || ""; document.execCommand("insertHTML", false, `<img src="${url}" data-media="${path}" alt=""><p><br></p>`); } catch (e) {}
    saveSel();
  }

  // @-mention → entity picker · "/" → slash command menu
  function onInput() {
    markDirty();
    const sel = window.getSelection(); if (!sel || !sel.rangeCount) { setMention(null); setSlash(null); return; }
    const r = sel.getRangeAt(0), node = r.startContainer;
    if (node && node.nodeType === 3) {
      const before = node.textContent.slice(0, r.startOffset);
      const at = before.match(/(?:^|\s)@([\wÀ-ÿ.\-]*)$/);
      if (at) { const rect = r.getBoundingClientRect(); setMention({ query: at[1], x: rect.left, y: rect.bottom }); setSlash(null); return; }
      const sl = before.match(/(?:^|\s)\/([\w]*)$/);
      if (sl) { const rect = r.getBoundingClientRect(); setSlash({ query: sl[1], x: rect.left, y: rect.bottom }); setMention(null); return; }
    }
    setMention(null); setSlash(null);
  }
  function runSlash(cmd) {
    focusEd();
    const sel = window.getSelection();
    if (sel && sel.rangeCount) { const r = sel.getRangeAt(0), node = r.startContainer, off = r.startOffset; if (node.nodeType === 3) { const m = node.textContent.slice(0, off).match(/\/([\w]*)$/); if (m) { const start = off - m[0].length; const rr = document.createRange(); rr.setStart(node, start); rr.setEnd(node, off); sel.removeAllRanges(); sel.addRange(rr); document.execCommand("insertText", false, ""); } } }
    setSlash(null); saveSel();
    let px = 220, py = 220; const gs = window.getSelection(); if (gs && gs.rangeCount) { const rc = gs.getRangeAt(0).getBoundingClientRect(); if (rc && (rc.left || rc.bottom)) { px = rc.left; py = rc.bottom; } }
    if (cmd === "_img") { if (fileRef.current) fileRef.current.click(); }
    else if (cmd === "_at") { focusEd(); document.execCommand("insertText", false, "@"); onInput(); }
    else if (cmd === "_emoji") setEmojiPop({ x: px, y: py });
    else if (cmd === "_date") insertDate(docToday());
    else if (cmd === "_status") setStatusPop({ x: px, y: py });
    else if (cmd === "info" || cmd === "success" || cmd === "warning" || cmd === "note" || cmd === "decision") insertPanel(cmd);
    else exec(cmd);
  }
  function insertRef(ref) {
    focusEd();
    const sel = window.getSelection(); if (sel && sel.rangeCount) {
      const r = sel.getRangeAt(0), node = r.startContainer, off = r.startOffset;
      if (node.nodeType === 3) { const m = node.textContent.slice(0, off).match(/@([\wÀ-ÿ.\-]*)$/); if (m) { const start = off - m[0].length; const rr = document.createRange(); rr.setStart(node, start); rr.setEnd(node, off); sel.removeAllRanges(); sel.addRange(rr); } }
    }
    const label = (ref.label || "").replace(/[<>&"]/g, (c) => ({ "<": "&lt;", ">": "&gt;", "&": "&amp;", '"': "&quot;" }[c]));
    document.execCommand("insertHTML", false, `<a href="#/mesper/${ref.targetType}/${encodeURIComponent(ref.targetId)}" class="doc-chip">${label}</a>&nbsp;`);
    setMention(null); saveSel();
  }

  async function save() {
    if (busy) return; setBusy(true);
    const el = edRef.current;
    let bodyHtml = "", bodyText = ""; const links = [], seen = new Set();
    if (el) {
      const holder = document.createElement("div"); holder.innerHTML = el.innerHTML;
      docFixLists(holder); // lift any <p><ul></ul></p> the browser produced
      // store media:// refs, not signed URLs
      holder.querySelectorAll("img[data-media]").forEach((im) => im.setAttribute("src", "media://" + im.getAttribute("data-media")));
      bodyHtml = docSanitizeHtml(holder.innerHTML);
      bodyText = (el.textContent || "").replace(/\s+/g, " ").trim();
      el.querySelectorAll('a[href^="#/mesper/"]').forEach((a) => {
        const mm = (a.getAttribute("href") || "").match(/^#\/mesper\/([a-z_]+)\/(.+)$/); if (!mm) return;
        const key = mm[1] + "|" + mm[2]; if (seen.has(key)) return; seen.add(key);
        links.push({ label: a.textContent || "", targetType: mm[1], targetId: decodeURIComponent(mm[2]) });
      });
    }
    docParseLinks(bodyText).forEach((l) => { const key = l.targetType + "|" + encodeURIComponent(l.targetId); if (seen.has(key)) return; seen.add(key); links.push(l); });
    try { await MesperDB.docsUpdatePage(page.id, { title: title.trim() || t("untitled"), icon: icon || null, bodyHtml, bodyText, links, updatedBy: user.id }); onDone(); } catch (e) { setBusy(false); }
  }

  const [emoOpen, setEmoOpen] = useState(false);
  return (
    <div style={{ maxWidth: 1240, margin: "0 auto" }}>
      {/* sticky edit chrome: back + title (left), edited / avatar / update / close (right), toolbar below */}
      {/* `top` cancels <main>'s own padding-top (16px narrow / 24px wide, screen-docs.jsx DocsApp)
          — position:sticky's offset is resolved against the scrolling ancestor's padding box, so
          without this the bar always sticks `padding-top` short of main's true scroll edge, leaving
          scrolled content visible in that gap. */}
      <div style={{ position: "sticky", top: narrow ? -16 : -24, zIndex: 30, background: "var(--surface)", margin: "0 0 14px" }}>
        <div style={{ display: "flex", alignItems: "center", gap: 10, padding: "8px 0", borderBottom: "1px solid var(--line)" }}>
          <button type="button" onClick={handleCancel} title={t("back")} style={{ border: "none", background: "transparent", cursor: "pointer", color: "var(--ink-3)", padding: 4, display: "flex", alignItems: "center", flex: "0 0 auto" }}><Icon name="chevLeft" size={20} /></button>
          <span style={{ fontSize: 13, color: "var(--ink-3)", fontWeight: 600, whiteSpace: "nowrap", overflow: "hidden", textOverflow: "ellipsis", minWidth: 0, flex: "0 1 auto" }}>{title.trim() || t("untitled")}</span>
          <div style={{ flex: 1 }} />
          <span style={{ fontSize: 12.5, color: "var(--ink-4)", whiteSpace: "nowrap" }}>{t("editedLabel")} {docRel(page.updatedAt)}</span>
          {editorPerson && <Avatar person={editorPerson} size={22} />}
          <Button size="sm" icon="check" onClick={save} disabled={busy}>{busy ? "…" : t("updateBtn")}</Button>
          <Button size="sm" variant="ghost" onClick={handleCancel}>{t("closeBtn")}</Button>
        </div>
        <div style={{ paddingTop: 8 }}>
          <DocToolbar sticky={false} onCmd={exec} onImage={() => fileRef.current && fileRef.current.click()} onMention={() => { focusEd(); document.execCommand("insertText", false, "@"); onInput(); }} onColor={applyColor} onEmoji={insertEmoji} onDate={insertDate} onPanel={insertPanel} />
        </div>
      </div>
      {/* big title + emoji (scrolls under the sticky chrome) */}
      <div style={{ display: "flex", alignItems: "flex-start", gap: 10, marginBottom: 12, position: "relative" }}>
        <button type="button" onClick={() => setEmoOpen((o) => !o)} style={{ fontSize: 30, lineHeight: 1.1, border: "none", background: "transparent", cursor: "pointer", flex: "0 0 auto", padding: 0 }}>{icon || "📄"}</button>
        {emoOpen && <div style={{ position: "absolute", top: 44, left: 0, zIndex: 20, background: "var(--surface)", border: "1px solid var(--line)", borderRadius: "var(--r-md)", boxShadow: "var(--sh-md)", padding: 8, width: 300 }}><DocEmojiGrid onPick={(e) => { setIcon(e); markDirty(); setEmoOpen(false); }} /></div>}
        <input autoFocus value={title} onChange={(e) => { setTitle(e.target.value); markDirty(); }} placeholder={t("untitled")} style={{ flex: 1, border: "none", outline: "none", background: "transparent", fontSize: 30, fontWeight: 800, fontFamily: "var(--font-display)", color: "var(--ink)", padding: 0, lineHeight: 1.15 }} />
      </div>
      <div ref={wrapRef} style={{ position: "relative" }}>
        {tableUI && (
          <div className="doc-tbl-tool" style={{ top: Math.max(tableUI.top, -6), left: tableUI.left }}>
            {[
              { tip: t("tRowAbove"), lbl: "↑+", fn: () => tblAddRow(false) },
              { tip: t("tRowBelow"), lbl: "↓+", fn: () => tblAddRow(true) },
              { tip: t("tColLeft"), lbl: "←+", fn: () => tblAddCol(false) },
              { tip: t("tColRight"), lbl: "+→", fn: () => tblAddCol(true) },
              { div: 1 },
              { tip: t("tDelRow"), lbl: "⌦Z", fn: tblDelRow },
              { tip: t("tDelCol"), lbl: "⌦S", fn: tblDelCol },
              { tip: t("tHeaderToggle"), lbl: "TH", fn: tblToggleHeader },
              { tip: t("tDelTable"), ic: "trash", fn: tblDelTable },
            ].map((b, i) => b.div
              ? <span key={"d" + i} style={{ width: 1, height: 15, background: "var(--line)", margin: "0 2px", display: "inline-block" }} />
              : <span key={i} className="fmt-wrap" data-tip={b.tip}><button type="button" aria-label={b.tip} onMouseDown={(e) => { e.preventDefault(); b.fn(); }}>{b.ic ? <Icon name={b.ic} size={13} /> : b.lbl}</button></span>)}
          </div>
        )}
        <div ref={edRef} className="doc-editor doc-rt" contentEditable suppressContentEditableWarning data-ph={t("writeSomething")}
          onInput={onInput} onKeyDown={onEditorKeyDown} onKeyUp={saveSel} onMouseUp={saveSel} onMouseDown={onEditorMouseDown} onMouseMove={onEditorMouseMove} onClick={onEditorClick} onFocus={primeEditingModes} onBlur={saveSel}
          onPaste={onEditorPaste}
          style={{ border: "1px solid var(--line)", borderRadius: "var(--r-md)", background: "var(--surface)", padding: "16px 18px", marginTop: 8 }} />
      </div>
      {mention && <DocMentionMenu query={mention.query} x={mention.x} y={mention.y} onPick={insertRef} onClose={() => setMention(null)} />}
      {slash && <DocSlashMenu query={slash.query} x={slash.x} y={slash.y} onPick={runSlash} onClose={() => setSlash(null)} />}
      {emojiPop && <DocCursorPop x={emojiPop.x} y={emojiPop.y} width={300} onClose={() => setEmojiPop(null)}><DocEmojiGrid onPick={(e) => { setEmojiPop(null); insertEmoji(e); }} /></DocCursorPop>}
      {statusPop && <DocCursorPop x={statusPop.x} y={statusPop.y} width={232} onClose={() => setStatusPop(null)}><DocStatusForm onInsert={(label, color) => { setStatusPop(null); insertStatus(label, color); }} /></DocCursorPop>}
      {datePop && <DocCursorPop x={datePop.x} y={datePop.y} width={210} onClose={() => setDatePop(null)}><DocDateForm initial={datePop.el.getAttribute("data-date")} onInsert={(iso) => updateDateChip(datePop.el, iso)} /></DocCursorPop>}
      <input ref={fileRef} type="file" accept="image/*" style={{ display: "none" }} onChange={(e) => { onFiles(e.target.files); e.target.value = ""; }} />
    </div>
  );
}

/* small popover wrapper for toolbar dropdowns (pattern: DocKebab) */
function DocToolPop({ tip, label, width, align, render }) {
  const [open, setOpen] = useState(false);
  const ref = useRef(null);
  useEffect(() => { const h = (e) => { if (ref.current && !ref.current.contains(e.target)) setOpen(false); }; document.addEventListener("mousedown", h); return () => document.removeEventListener("mousedown", h); }, []);
  return (
    <span ref={ref} className="fmt-wrap" data-tip={tip} style={{ position: "relative", display: "inline-flex" }}>
      <button type="button" aria-label={tip} className="fmt-btn vp-focusable" onMouseDown={(e) => { e.preventDefault(); setOpen((o) => !o); }}>{label}</button>
      {open && <div className="vp-pop" style={{ position: "absolute", top: "100%", marginTop: 4, [align === "right" ? "right" : "left"]: 0, background: "var(--surface)", border: "1px solid var(--line)", borderRadius: "var(--r-md)", boxShadow: "var(--sh-lg)", padding: 6, zIndex: 80, width }}>{render(() => setOpen(false))}</div>}
    </span>
  );
}
/* fixed popover anchored to the caret (slash-triggered emoji / status) */
function DocCursorPop({ x, y, width, onClose, children }) {
  const ref = useRef(null);
  useEffect(() => {
    const h = (e) => { if (ref.current && !ref.current.contains(e.target)) onClose(); };
    const k = (e) => { if (e.key === "Escape") onClose(); };
    document.addEventListener("mousedown", h); document.addEventListener("keydown", k, true);
    return () => { document.removeEventListener("mousedown", h); document.removeEventListener("keydown", k, true); };
  }, []);
  const w = typeof window !== "undefined" ? window.innerWidth : 1000, hgt = typeof window !== "undefined" ? window.innerHeight : 800;
  const left = Math.max(8, Math.min(x, w - width - 12)), top = Math.min(y + 6, hgt - 280);
  return <div ref={ref} className="vp-pop" style={{ position: "fixed", left, top, width, background: "var(--surface)", border: "1px solid var(--line)", borderRadius: "var(--r-md)", boxShadow: "var(--sh-lg)", padding: 8, zIndex: 90 }}>{children}</div>;
}
function DocStatusForm({ onInsert }) {
  const { t } = useDT();
  const [label, setLabel] = useState("");
  const [color, setColor] = useState("gray");
  const swatch = { gray: "#42526E", blue: "#0747A6", green: "#036B45", yellow: "#7A5900", red: "#B71C1C" };
  return (
    <div style={{ display: "grid", gap: 8 }}>
      <input autoFocus value={label} onChange={(e) => setLabel(e.target.value)} placeholder={t("statusPh")} onKeyDown={(e) => { if (e.key === "Enter") { e.preventDefault(); onInsert((label || "STATUS").toUpperCase(), color); } }}
        style={{ width: "100%", boxSizing: "border-box", border: "1px solid var(--line-2)", borderRadius: "var(--r-sm)", padding: "6px 8px", fontSize: 13.5, background: "var(--surface)", color: "var(--ink)" }} />
      <div style={{ display: "flex", gap: 6 }}>
        {DOC_STATUS_COLORS.map((c) => <button key={c} type="button" title={c} onClick={() => setColor(c)} style={{ width: 24, height: 24, borderRadius: 5, cursor: "pointer", background: swatch[c], border: color === c ? "2px solid var(--ink)" : "1px solid var(--line)" }} />)}
      </div>
      <Button size="sm" icon="check" onClick={() => onInsert((label || "STATUS").toUpperCase(), color)}>{t("insertBtn")}</Button>
    </div>
  );
}
function DocDateForm({ initial, onInsert }) {
  const { t } = useDT();
  const [val, setVal] = useState(initial || docToday());
  return (
    <div style={{ display: "grid", gap: 8 }}>
      <input type="date" value={val} onChange={(e) => setVal(e.target.value)}
        style={{ width: "100%", boxSizing: "border-box", border: "1px solid var(--line-2)", borderRadius: "var(--r-sm)", padding: "6px 8px", fontSize: 13.5, background: "var(--surface)", color: "var(--ink)" }} />
      <Button size="sm" icon="check" onClick={() => onInsert(val || docToday())}>{t("insertBtn")}</Button>
    </div>
  );
}

function DocToolbar({ onCmd, onImage, onMention, onColor, onEmoji, onDate, onPanel, sticky = true }) {
  const { t } = useDT();
  const fmtBtn = (tip, content, fn) => (
    <span className="fmt-wrap" data-tip={tip}>
      <button type="button" aria-label={tip} className="fmt-btn vp-focusable" onMouseDown={(e) => { e.preventDefault(); fn(); }}>{content}</button>
    </span>
  );
  const divider = (k) => <span key={k} style={{ width: 1, height: 16, background: "var(--line)", margin: "0 4px" }} />;
  const menuItem = (label, fn, close, extra) => (
    <button key={label} type="button" onMouseDown={(e) => { e.preventDefault(); fn(); close(); }}
      style={{ display: "flex", alignItems: "center", gap: 9, width: "100%", padding: "7px 9px", border: "none", cursor: "pointer", background: "transparent", borderRadius: "var(--r-sm)", textAlign: "left", fontSize: 13.5, color: "var(--ink)" }}
      onMouseEnter={(e) => (e.currentTarget.style.background = "var(--surface-2)")} onMouseLeave={(e) => (e.currentTarget.style.background = "transparent")}>{extra}{label}</button>
  );
  const styleItems = [["para", t("tPara")], ["h1", t("tH1")], ["h2", t("tH2")], ["h3", t("tH3")], ["h4", t("tH4")], ["h5", t("tH5")], ["quote", t("tQuote")]];
  const moreItems = [["strike", t("tStrike")], ["code", t("tCode")], ["codeblock", t("tCodeblock")]];
  const panelItems = [["info", t("sInfo")], ["success", t("sSuccess")], ["warning", t("sWarning")], ["note", t("sNote")], ["decision", t("sDecision")]];
  const panelDot = { info: "#94B8E8", success: "#9AD4AC", warning: "#E8CD8C", note: "#C2AEE8", decision: "#9AD4AC" };
  return (
    <div style={{ display: "flex", alignItems: "center", gap: 1, flexWrap: "wrap", ...(sticky ? { position: "sticky", top: 0, zIndex: 5 } : {}), background: "var(--surface-2)", border: "1px solid var(--line)", borderRadius: "var(--r-md)", padding: "5px 7px" }}>
      <DocToolPop tip={t("tStyle")} width={180} label={<span style={{ display: "inline-flex", alignItems: "center", gap: 3 }}><b style={{ fontSize: 12.5 }}>Aa</b><Icon name="chevDown" size={11} /></span>} render={(close) => <div style={{ display: "flex", flexDirection: "column" }}>{styleItems.map(([k, l]) => menuItem(l, () => onCmd(k), close))}</div>} />
      {divider("d0")}
      {fmtBtn(t("tBold"), <b>B</b>, () => onCmd("bold"))}
      {fmtBtn(t("tItalic"), <span style={{ fontStyle: "italic", fontFamily: "Georgia,serif" }}>I</span>, () => onCmd("italic"))}
      {fmtBtn(t("tUnderline"), <span style={{ textDecoration: "underline" }}>U</span>, () => onCmd("underline"))}
      <DocToolPop tip={t("tMore")} width={170} label={<b style={{ fontSize: 14, lineHeight: 1 }}>⋯</b>} render={(close) => <div style={{ display: "flex", flexDirection: "column" }}>{moreItems.map(([k, l]) => menuItem(l, () => onCmd(k), close))}</div>} />
      {divider("d1")}
      <DocToolPop tip={t("tColor")} width={168} label={<span style={{ display: "inline-flex", alignItems: "center", gap: 3 }}><span style={{ width: 13, height: 13, borderRadius: 3, background: "linear-gradient(135deg,#C9372C,#0C66E4)", display: "inline-block" }} /><Icon name="chevDown" size={11} /></span>} render={(close) => (
        <div>
          <div style={{ display: "flex", flexWrap: "wrap", gap: 6, padding: "2px 2px 6px" }}>
            {DOC_COLORS.map((c) => <button key={c.cls} type="button" title={c.cls} onMouseDown={(e) => { e.preventDefault(); onColor(c.cls); close(); }} style={{ width: 22, height: 22, borderRadius: 5, border: "1px solid var(--line)", background: c.hex, cursor: "pointer" }} />)}
          </div>
          {menuItem(t("cDefault"), () => onColor(""), close, <span style={{ width: 22, height: 22, borderRadius: 5, border: "1px solid var(--line)", background: "var(--surface)", display: "inline-block", flex: "0 0 auto" }} />)}
        </div>
      )} />
      {divider("d2")}
      {fmtBtn(t("tUl"), <span style={{ fontSize: 17, lineHeight: 1 }}>•</span>, () => onCmd("ul"))}
      {fmtBtn(t("tOl"), <span style={{ fontSize: 11, fontWeight: 700 }}>1.</span>, () => onCmd("ol"))}
      {fmtBtn(t("tTask"), <span style={{ fontSize: 13 }}>☑</span>, () => onCmd("task"))}
      {divider("d3")}
      {fmtBtn(t("tLink"), <Icon name="link" size={15} />, () => onCmd("link"))}
      {fmtBtn(t("tImage"), <Icon name="image" size={15} />, onImage)}
      {fmtBtn(t("tMention"), <b style={{ fontSize: 15 }}>@</b>, onMention)}
      <DocToolPop tip={t("tEmoji")} width={300} label={<span style={{ fontSize: 15, lineHeight: 1 }}>🙂</span>} render={(close) => <DocEmojiGrid onPick={(e) => { onEmoji(e); close(); }} />} />
      <DocToolPop tip={t("tDate")} width={210} label={<Icon name="calendar" size={15} />} render={(close) => <DocDateForm onInsert={(iso) => { onDate(iso); close(); }} />} />
      {fmtBtn(t("tTable"), <Icon name="grip" size={15} />, () => onCmd("table"))}
      <DocToolPop tip={t("tPanel")} width={182} label={<span style={{ display: "inline-flex", alignItems: "center", gap: 3 }}><Icon name="info" size={15} /><Icon name="chevDown" size={11} /></span>} render={(close) => <div style={{ display: "flex", flexDirection: "column" }}>{panelItems.map(([k, l]) => menuItem(l, () => onPanel(k), close, <span style={{ width: 12, height: 12, borderRadius: 3, flex: "0 0 auto", background: panelDot[k] }} />))}</div>} />
      {divider("d4")}
      {fmtBtn(t("tUndo"), <span style={{ fontSize: 15, lineHeight: 1 }}>↶</span>, () => onCmd("undo"))}
      {fmtBtn(t("tRedo"), <span style={{ fontSize: 15, lineHeight: 1 }}>↷</span>, () => onCmd("redo"))}
    </div>
  );
}

/* ---- slash command menu ---- */
function DocSlashMenu({ query, x, y, onPick, onClose }) {
  const { t } = useDT();
  const CMDS = [
    { k: "h1", label: t("sH1"), icon: "H1" }, { k: "h2", label: t("sH2"), icon: "H2" }, { k: "h3", label: t("sH3"), icon: "H3" },
    { k: "ul", label: t("sUl"), icon: "•" }, { k: "ol", label: t("sOl"), icon: "1." }, { k: "task", label: t("sTask"), icon: "☑" },
    { k: "quote", label: t("sQuote"), icon: "❝" }, { k: "codeblock", label: t("sCodeblock"), icon: "{}" }, { k: "table", label: t("sTable"), ic: "grip" },
    { k: "_img", label: t("sImage"), ic: "image" }, { k: "_at", label: t("sMention"), icon: "@" }, { k: "_emoji", label: t("sEmoji"), icon: "🙂" },
    { k: "_date", label: t("sDate"), ic: "calendar" }, { k: "_status", label: t("sStatus"), icon: "◍" }, { k: "divider", label: t("sDivider"), icon: "―" },
    { k: "info", label: t("sInfo"), icon: "ℹ" }, { k: "success", label: t("sSuccess"), icon: "✓" }, { k: "warning", label: t("sWarning"), icon: "⚠" },
    { k: "note", label: t("sNote"), icon: "✎" }, { k: "decision", label: t("sDecision"), icon: "⤵" },
  ];
  const q = (query || "").toLowerCase();
  const list = CMDS.filter((c) => !q || c.label.toLowerCase().includes(q));
  const [hi, setHi] = useState(0);
  useEffect(() => { setHi(0); }, [query]);
  useEffect(() => {
    function onKey(e) { if (e.key === "Escape") onClose(); else if (e.key === "ArrowDown") { e.preventDefault(); setHi((h) => Math.min(h + 1, list.length - 1)); } else if (e.key === "ArrowUp") { e.preventDefault(); setHi((h) => Math.max(0, h - 1)); } else if ((e.key === "Enter" || e.key === "Tab") && list[hi]) { e.preventDefault(); onPick(list[hi].k); } }
    document.addEventListener("keydown", onKey, true); return () => document.removeEventListener("keydown", onKey, true);
  }, [list, hi]);
  if (!list.length) return null;
  const top = Math.min(y + 6, (typeof window !== "undefined" ? window.innerHeight : 800) - 330);
  return (
    <div className="vp-pop" style={{ position: "fixed", left: Math.min(x, (typeof window !== "undefined" ? window.innerWidth : 1000) - 250), top, width: 240, background: "var(--surface)", border: "1px solid var(--line)", borderRadius: "var(--r-md)", boxShadow: "var(--sh-lg)", padding: 5, zIndex: 90, maxHeight: 320, overflowY: "auto" }}>
      {list.map((c, i) => (
        <button key={c.k} type="button" onMouseDown={(e) => { e.preventDefault(); onPick(c.k); }} onMouseEnter={() => setHi(i)}
          style={{ display: "flex", alignItems: "center", gap: 10, width: "100%", padding: "7px 9px", border: "none", cursor: "pointer", background: i === hi ? "var(--surface-2)" : "transparent", borderRadius: "var(--r-sm)", textAlign: "left" }}>
          <span style={{ width: 24, height: 24, display: "grid", placeItems: "center", background: "var(--surface-2)", borderRadius: 6, fontWeight: 700, fontSize: 12, color: "var(--ink-3)", flex: "0 0 auto" }}>{c.ic ? <Icon name={c.ic} size={14} /> : c.icon}</span>
          <span style={{ fontSize: 13.5, color: "var(--ink)" }}>{c.label}</span>
        </button>
      ))}
    </div>
  );
}

/* ---- mention/entity picker (docs / companies / projects / people) ---- */
function DocMentionMenu({ query, x, y, onPick, onClose }) {
  const { t } = useDT();
  const [src, setSrc] = useState({ docs: [], companies: [], projects: [], people: [] });
  const [hi, setHi] = useState(0);
  useEffect(() => {
    let alive = true;
    Promise.all([
      MesperDB.docsListRecent ? MesperDB.docsListRecent(60).catch(() => []) : [],
      MesperDB.crmListCompanies ? MesperDB.crmListCompanies().catch(() => []) : [],
      MesperDB.loadTasksData ? MesperDB.loadTasksData().then((d) => d.projects || []).catch(() => []) : [],
    ]).then(([docs, companies, projects]) => { if (alive) setSrc({ docs, companies, projects, people: (typeof VP_DATA !== "undefined" && VP_DATA.people) || [] }); });
    return () => { alive = false; };
  }, []);
  const q = (query || "").toLowerCase();
  const results = [];
  src.docs.filter((d) => !q || (d.title || "").toLowerCase().includes(q)).slice(0, 5).forEach((d) => results.push({ targetType: "doc", targetId: d.id, label: d.title, group: t("mDoc"), icon: d.icon || "📄" }));
  src.companies.filter((c) => !q || (c.name || "").toLowerCase().includes(q)).slice(0, 5).forEach((c) => results.push({ targetType: "crm_company", targetId: c.id, label: c.name, group: t("mCompany"), icon: "🏢" }));
  src.projects.filter((p) => !q || (p.name || "").toLowerCase().includes(q)).slice(0, 4).forEach((p) => results.push({ targetType: "project", targetId: p.key || p.id, label: p.name, group: t("mProject"), icon: "📋" }));
  src.people.filter((p) => { const n = `${p.first || ""} ${p.last || ""}`.toLowerCase(); return !q || n.includes(q); }).slice(0, 5).forEach((p) => results.push({ targetType: "user", targetId: p.id, label: `${p.first || ""} ${p.last || ""}`.trim(), group: t("mPerson"), icon: "👤" }));
  const list = results.slice(0, 14);
  useEffect(() => { setHi(0); }, [query]);
  useEffect(() => {
    function onKey(e) { if (e.key === "Escape") onClose(); else if (e.key === "ArrowDown") { e.preventDefault(); setHi((h) => Math.min(h + 1, list.length - 1)); } else if (e.key === "ArrowUp") { e.preventDefault(); setHi((h) => Math.max(0, h - 1)); } else if ((e.key === "Enter" || e.key === "Tab") && list[hi]) { e.preventDefault(); onPick(list[hi]); } }
    document.addEventListener("keydown", onKey, true); return () => document.removeEventListener("keydown", onKey, true);
  }, [list, hi]);
  if (!list.length) return null;
  const top = Math.min(y + 6, (typeof window !== "undefined" ? window.innerHeight : 800) - 300);
  return (
    <div className="vp-pop" style={{ position: "fixed", left: Math.min(x, (typeof window !== "undefined" ? window.innerWidth : 1000) - 280), top, width: 270, background: "var(--surface)", border: "1px solid var(--line)", borderRadius: "var(--r-md)", boxShadow: "var(--sh-lg)", padding: 5, zIndex: 90, maxHeight: 290, overflowY: "auto" }}>
      {list.map((r, i) => (
        <button key={r.targetType + r.targetId} type="button" onMouseDown={(e) => { e.preventDefault(); onPick(r); }} onMouseEnter={() => setHi(i)}
          style={{ display: "flex", alignItems: "center", gap: 9, width: "100%", padding: "7px 9px", border: "none", cursor: "pointer", background: i === hi ? "var(--surface-2)" : "transparent", borderRadius: "var(--r-sm)", textAlign: "left" }}>
          <span style={{ fontSize: 15, flex: "0 0 auto" }}>{r.icon}</span>
          <span style={{ fontSize: 13.5, color: "var(--ink)", flex: 1, minWidth: 0, whiteSpace: "nowrap", overflow: "hidden", textOverflow: "ellipsis" }}>{r.label}</span>
          <span style={{ fontSize: 10.5, color: "var(--ink-4)", fontWeight: 700, textTransform: "uppercase" }}>{r.group}</span>
        </button>
      ))}
    </div>
  );
}

/* ---- templates (built-in, client-side, no DB seed) ---- */
function DocTemplatesView({ navigate }) {
  const { t } = useDT();
  const [spaces, setSpaces] = useState([]);
  const [pick, setPick] = useState(null);
  useEffect(() => { MesperDB.docsListSpaces().then(setSpaces).catch(() => {}); }, []);
  const builtins = [
    { id: "meeting", title: t("tplMeeting"), icon: "calendar", desc: t("tplMeetingDesc") },
    { id: "blank", title: t("blankPage"), icon: "file", desc: t("tplBlankDesc") },
  ];
  async function create(tpl, sp) {
    try {
      const p = tpl.id === "meeting"
        ? await MesperDB.docsCreatePage({ spaceId: sp.id, bodyHtml: DOC_TPL_MEETING(), title: docMeetingTitle(sp.name) })
        : await MesperDB.docsCreatePage({ spaceId: sp.id, title: t("untitled") });
      navigate("/docs/p/" + p.id);
    } catch (e) {}
  }
  return (
    <div style={{ maxWidth: 820, margin: "0 auto" }}>
      <h1 style={{ fontSize: 24, margin: "0 0 16px" }}>{t("templates")}</h1>
      <div style={{ display: "grid", gridTemplateColumns: "repeat(auto-fill, minmax(230px, 1fr))", gap: 14 }}>
        {builtins.map((tp) => (
          <div key={tp.id} style={{ border: "1px solid var(--line)", borderRadius: "var(--r-lg)", background: "var(--surface)", padding: 16, boxShadow: "var(--sh-sm)" }}>
            <div style={{ display: "flex", alignItems: "center", gap: 10, marginBottom: 8 }}><span style={{ width: 36, height: 36, borderRadius: "var(--r-md)", background: "var(--accent-soft)", color: "var(--accent)", display: "grid", placeItems: "center" }}><Icon name={tp.icon} size={19} /></span><div style={{ fontWeight: 700, fontSize: 15 }}>{tp.title}</div></div>
            <div style={{ fontSize: 12.5, color: "var(--ink-4)", marginBottom: 10, lineHeight: 1.4 }}>{tp.desc}</div>
            <Button size="sm" icon="plus" onClick={() => setPick(tp)}>{t("useTemplate")}</Button>
          </div>
        ))}
      </div>
      {pick && (
        <Modal onClose={() => setPick(null)} width={380}>
          <div style={{ padding: "18px 22px", borderBottom: "1px solid var(--line)" }}><h2 style={{ fontSize: 17, margin: 0 }}>{pick.title}</h2></div>
          <div style={{ padding: "16px 22px", display: "grid", gap: 8 }}>
            <div style={docFieldLabel()}>{t("spaces")}</div>
            {spaces.map((s) => <button key={s.id} type="button" onClick={() => create(pick, s)} style={{ display: "flex", alignItems: "center", gap: 10, padding: "10px 12px", border: "1px solid var(--line)", borderRadius: "var(--r-sm)", background: "var(--surface-2)", cursor: "pointer", textAlign: "left" }}><span style={{ width: 8, height: 8, borderRadius: 3, background: s.color }} /><span style={{ fontWeight: 600, fontSize: 14 }}>{s.name}</span></button>)}
          </div>
        </Modal>
      )}
    </div>
  );
}

/* ---- search results ---- */
function DocSearchView({ q, navigate }) {
  const { t } = useDT();
  const [query, setQuery] = useState(q || "");
  const [res, setRes] = useState(null);
  useEffect(() => { setQuery(q || ""); }, [q]);
  useEffect(() => { const term = (query || "").trim(); if (!term) { setRes([]); return; } setRes(null); const id = setTimeout(() => { MesperDB.docsSearch(term).then(setRes).catch(() => setRes([])); }, 200); return () => clearTimeout(id); }, [query]);
  const ss = typeof selectStyle !== "undefined" ? selectStyle : { width: "100%", padding: "10px 12px", border: "1px solid var(--line-2)", borderRadius: 8, fontSize: 15 };
  return (
    <div style={{ maxWidth: 760, margin: "0 auto" }}>
      <input autoFocus value={query} onChange={(e) => setQuery(e.target.value)} placeholder={t("searchAll")} style={{ ...ss, marginBottom: 18, fontSize: 16 }} />
      {res === null ? <div style={{ color: "var(--ink-4)" }}>…</div> : res.length === 0 ? <div style={{ color: "var(--ink-4)", fontSize: 14.5 }}>{query.trim() ? t("noResults") : ""}</div> : (
        <div style={{ display: "flex", flexDirection: "column", gap: 8 }}>
          {res.map((r) => (
            <button key={r.id} type="button" onClick={() => navigate("/docs/p/" + r.id)} style={{ display: "block", width: "100%", padding: "12px 15px", border: "1px solid var(--line)", borderRadius: "var(--r-md)", background: "var(--surface)", cursor: "pointer", textAlign: "left" }}>
              <div style={{ display: "flex", alignItems: "center", gap: 8, marginBottom: 3 }}><span style={{ fontSize: 15 }}>{r.icon || "📄"}</span><span style={{ fontWeight: 700, fontSize: 15, color: "var(--ink)" }}>{r.title}</span></div>
              {r.snippet && <div style={{ fontSize: 13, color: "var(--ink-4)", lineHeight: 1.45 }}>{r.snippet}</div>}
            </button>
          ))}
        </div>
      )}
    </div>
  );
}

/* ---- small overflow menu (archive / trash / …) ---- */
function DocKebab({ items }) {
  const [open, setOpen] = useState(false);
  const ref = useRef(null);
  useEffect(() => { const h = (e) => { if (ref.current && !ref.current.contains(e.target)) setOpen(false); }; document.addEventListener("mousedown", h); return () => document.removeEventListener("mousedown", h); }, []);
  return (
    <div ref={ref} style={{ position: "relative" }}>
      <button type="button" onClick={() => setOpen((o) => !o)} title="…" style={{ border: "none", background: "transparent", cursor: "pointer", color: "var(--ink-4)", padding: "2px 6px", fontSize: 20, lineHeight: 1 }}>⋯</button>
      {open && (
        <div className="vp-pop" style={{ position: "absolute", top: "100%", right: 0, marginTop: 4, background: "var(--surface)", border: "1px solid var(--line)", borderRadius: "var(--r-md)", boxShadow: "var(--sh-lg)", padding: 5, zIndex: 60, minWidth: 180 }}>
          {items.map((it, i) => (
            <button key={i} type="button" onClick={() => { setOpen(false); it.onClick(); }}
              style={{ display: "flex", alignItems: "center", gap: 9, width: "100%", padding: "8px 10px", border: "none", background: "transparent", cursor: "pointer", borderRadius: "var(--r-sm)", textAlign: "left", fontSize: 13.5, color: it.danger ? "var(--st-declined, #E5484D)" : "var(--ink-2)" }}
              onMouseEnter={(e) => (e.currentTarget.style.background = "var(--surface-2)")} onMouseLeave={(e) => (e.currentTarget.style.background = "transparent")}>
              {it.icon && <Icon name={it.icon} size={15} />} {it.label}
            </button>
          ))}
        </div>
      )}
    </div>
  );
}

/* ---- trash view (30-day restore + purge) ---- */
function DocTrashView({ navigate }) {
  const { t } = useDT();
  const [items, setItems] = useState(null);
  function reload() { MesperDB.docsListTrash().then(setItems).catch(() => setItems([])); }
  useEffect(() => { reload(); }, []);
  async function restore(it) { try { if (it.type === "space") await MesperDB.docsRestoreSpace(it.id); else await MesperDB.docsRestorePage(it.id); } catch (e) {} reload(); }
  async function purge(it) { if (!window.confirm(t("confirmPurge"))) return; try { if (it.type === "space") await MesperDB.docsPurgeSpace(it.id); else await MesperDB.docsPurgePage(it.id); } catch (e) {} reload(); }
  return (
    <div style={{ maxWidth: 760, margin: "0 auto" }}>
      <h1 style={{ fontSize: 24, margin: "0 0 6px" }}>{t("trash")}</h1>
      <p style={{ color: "var(--ink-4)", fontSize: 13.5, margin: "0 0 18px" }}>{t("trashHint")}</p>
      {items === null ? <div style={{ color: "var(--ink-4)" }}>…</div> : items.length === 0 ? (
        <div style={{ color: "var(--ink-4)", fontSize: 14.5, padding: "20px 0" }}>{t("trashEmpty")}</div>
      ) : (
        <div style={{ display: "flex", flexDirection: "column", gap: 8 }}>
          {items.map((it) => (
            <div key={it.type + it.id} style={{ display: "flex", alignItems: "center", gap: 12, padding: "11px 14px", border: "1px solid var(--line)", borderRadius: "var(--r-md)", background: "var(--surface)" }}>
              <span style={{ fontSize: 17, flex: "0 0 auto" }}>{it.type === "space" ? "🗂️" : it.icon}</span>
              <div style={{ flex: 1, minWidth: 0 }}>
                <div style={{ fontWeight: 600, fontSize: 14, color: "var(--ink)", whiteSpace: "nowrap", overflow: "hidden", textOverflow: "ellipsis" }}>{it.title}</div>
                <div style={{ fontSize: 12, color: "var(--ink-4)" }}>{t("deletedBy")} {docPersonName(it.deletedBy) || "—"} · {docRel(it.deletedAt)} · <span style={{ color: it.daysLeft <= 5 ? "var(--st-declined, #E5484D)" : "var(--ink-4)", fontWeight: it.daysLeft <= 5 ? 600 : 400 }}>{it.daysLeft} {t("daysLeft")}</span></div>
              </div>
              <Button size="sm" variant="ghost" icon="history" onClick={() => restore(it)}>{t("restore")}</Button>
              <button type="button" onClick={() => purge(it)} title={t("deleteForever")} style={{ border: "none", background: "transparent", cursor: "pointer", color: "var(--st-declined, #E5484D)", padding: 5 }}><Icon name="trash" size={16} /></button>
            </div>
          ))}
        </div>
      )}
    </div>
  );
}

/* ---- activity log (audit trail, internal only) ---- */
const DOC_ACT_KIND = {
  created: "entity", updated: "entity", duplicated: "entity", archived: "entity",
  unarchived: "entity", deleted: "entity", restored: "entity", purged: "entity",
  guest_invited: "guest", guest_removed: "guest", guest_login: "guest",
};
const DOC_ACT_KEY = {
  created: "avaCreated", updated: "avaUpdated", duplicated: "avaDuplicated",
  archived: "avaArchived", unarchived: "avaUnarchived", deleted: "avaDeleted",
  restored: "avaRestored", purged: "avaPurged",
  guest_invited: "avgInvited", guest_removed: "avgRemoved", guest_login: "avgLogin",
};
// which raw actions belong to each dropdown group
const DOC_ACT_GROUPS = {
  edits: ["updated"],
  created: ["created", "duplicated"],
  trash: ["deleted", "restored", "purged", "archived", "unarchived"],
  guests: ["guest_invited", "guest_removed", "guest_login"],
};
function docEntityWordKey(entity) { if (entity === "doc_space") return "avSpace"; if (entity === "doc_folder") return "avFolder"; return "avPage"; }
function docDayKey(iso) { const d = new Date(iso); return d.getFullYear() + "-" + d.getMonth() + "-" + d.getDate(); }
function docDayLabel(iso, lang, t) {
  const d = new Date(iso), now = new Date();
  const startOf = (x) => new Date(x.getFullYear(), x.getMonth(), x.getDate()).getTime();
  const diff = Math.round((startOf(now) - startOf(d)) / 864e5);
  if (diff === 0) return t("dToday");
  if (diff === 1) return t("dYesterday");
  try { return d.toLocaleDateString(lang === "de" ? "de-DE" : "en-US", { day: "numeric", month: "long", year: "numeric" }); } catch (_) { return ""; }
}
function docSelStyle() { return { appearance: "none", WebkitAppearance: "none", padding: "8px 30px 8px 12px", fontSize: 13, fontWeight: 600, border: "1px solid var(--line-2)", borderRadius: "var(--r-sm)", background: "var(--surface)", color: "var(--ink-2)", cursor: "pointer", fontFamily: "var(--font-body)", backgroundImage: "url(\"data:image/svg+xml;utf8,<svg xmlns='http://www.w3.org/2000/svg' width='16' height='16' viewBox='0 0 24 24' fill='none' stroke='%238a90a2' stroke-width='2' stroke-linecap='round' stroke-linejoin='round'><path d='M6 9l6 6 6-6'/></svg>\")", backgroundRepeat: "no-repeat", backgroundPosition: "right 8px center" }; }

function DocActivityRow({ e, lang, t, navigate }) {
  const person = docPerson(e.actor);
  const name = docPersonName(e.actor) || e.actorEmail || "System";
  const kind = DOC_ACT_KIND[e.action] || "entity";
  const verb = t(DOC_ACT_KEY[e.action] || "");
  const entWord = t(docEntityWordKey(e.entity));
  const title = (e.details && e.details.title) || "";
  const email = (e.details && e.details.email) || "";
  const clickable = (e.entity === "doc_page" || e.entity === "doc_folder") && !!e.entityId;
  const quoted = title ? "„" + title + "“" : "";
  const openTitle = () => { try { navigate("/docs/p/" + e.entityId); } catch (_) {} };
  const titleNode = title ? (clickable
    ? <button type="button" onClick={openTitle} style={{ border: "none", background: "transparent", padding: 0, margin: 0, cursor: "pointer", color: "var(--accent)", font: "inherit", fontWeight: 600 }}>{quoted}</button>
    : <span style={{ color: "var(--ink)", fontWeight: 600 }}>{quoted}</span>) : null;
  const nameEl = <span style={{ color: "var(--ink)", fontWeight: 700 }}>{name}</span>;
  let sentence;
  if (kind === "guest") sentence = <>{nameEl}{" "}{verb}{email && email !== name ? " (" + email + ")" : ""}</>;
  else if (lang === "de") sentence = <>{nameEl}{" hat "}{entWord}{titleNode ? <>{" "}{titleNode}</> : null}{" "}{verb}</>;
  else sentence = <>{nameEl}{" "}{verb}{" "}{entWord}{titleNode ? <>{" "}{titleNode}</> : null}</>;
  const avatar = person ? <Avatar person={person} size={32} /> : (
    <div style={{ width: 32, height: 32, borderRadius: "50%", flex: "0 0 auto", display: "grid", placeItems: "center", background: "var(--ink-4)", color: "#fff", fontFamily: "var(--font-display)", fontWeight: 600, fontSize: 12, letterSpacing: "-0.02em", userSelect: "none" }}>{((e.actorEmail || "?").trim()[0] || "?").toUpperCase()}</div>
  );
  return (
    <div style={{ display: "flex", alignItems: "center", gap: 12, padding: "10px 4px" }}>
      {avatar}
      <div style={{ flex: 1, minWidth: 0, fontSize: 13.5, color: "var(--ink-3)", lineHeight: 1.45 }}>{sentence}</div>
      <span style={{ fontSize: 12, color: "var(--ink-4)", flex: "0 0 auto", whiteSpace: "nowrap" }}>{docRel(e.at)}</span>
    </div>
  );
}

function DocActivityView({ navigate }) {
  const { lang, t } = useDT();
  const [entries, setEntries] = useState(null);
  const [spaces, setSpaces] = useState([]);
  const [spaceFilter, setSpaceFilter] = useState("all");
  const [actionFilter, setActionFilter] = useState("all");
  useEffect(() => {
    MesperDB.docsListAudit(200).then((xs) => setEntries((xs || []).slice().sort((a, b) => new Date(b.at) - new Date(a.at)))).catch(() => setEntries([]));
    MesperDB.docsListSpaces().then(setSpaces).catch(() => {});
  }, []);
  const filtered = (entries || []).filter((e) => {
    if (spaceFilter !== "all" && ((e.details && e.details.space_id) || "") !== spaceFilter) return false;
    if (actionFilter !== "all") { const g = DOC_ACT_GROUPS[actionFilter] || []; if (g.indexOf(e.action) < 0) return false; }
    return true;
  });
  // preserve the descending order coming from the DB, bucket by calendar day
  const groups = [];
  let curKey = null;
  filtered.forEach((e) => { const k = docDayKey(e.at); if (k !== curKey) { curKey = k; groups.push({ key: k, label: docDayLabel(e.at, lang, t), items: [] }); } groups[groups.length - 1].items.push(e); });
  return (
    <div style={{ maxWidth: 820, margin: "0 auto" }}>
      <h1 style={{ fontSize: 24, margin: "0 0 14px" }}>{t("activity")}</h1>
      <div style={{ display: "flex", flexWrap: "wrap", gap: 10, marginBottom: 18 }}>
        <select value={spaceFilter} onChange={(e) => setSpaceFilter(e.target.value)} style={docSelStyle()}>
          <option value="all">{t("allSpaces")}</option>
          {spaces.map((sp) => <option key={sp.id} value={sp.id}>{sp.name}</option>)}
        </select>
        <select value={actionFilter} onChange={(e) => setActionFilter(e.target.value)} style={docSelStyle()}>
          <option value="all">{t("actAll")}</option>
          <option value="edits">{t("actEdits")}</option>
          <option value="created">{t("actCreated")}</option>
          <option value="trash">{t("actTrash")}</option>
          <option value="guests">{t("actGuests")}</option>
        </select>
      </div>
      {entries === null ? <div style={{ color: "var(--ink-4)" }}>…</div> : groups.length === 0 ? (
        <div style={{ color: "var(--ink-4)", fontSize: 14.5, padding: "20px 0" }}>{t("activityEmpty")}</div>
      ) : (
        <div style={{ display: "flex", flexDirection: "column", gap: 22 }}>
          {groups.map((g) => (
            <div key={g.key}>
              <div style={{ fontSize: 11.5, fontWeight: 700, letterSpacing: "0.05em", textTransform: "uppercase", color: "var(--ink-4)", padding: "0 4px 4px" }}>{g.label}</div>
              <div style={{ display: "flex", flexDirection: "column", borderTop: "1px solid var(--line)" }}>
                {g.items.map((e) => <div key={e.id} style={{ borderBottom: "1px solid var(--line)" }}><DocActivityRow e={e} lang={lang} t={t} navigate={navigate} /></div>)}
              </div>
            </div>
          ))}
        </div>
      )}
    </div>
  );
}

Object.assign(window, { DocsApp });
