// App-wide comment overlay. Brings the same commenting experience the canvas has (Cmd+C → drop a
// pin → thread modal, sidebar, reactions, resolve) to the regular application screens (Assembly,
// Drawing workflow, Settings, …).
//
// Key differences from the canvas comment system (design-canvas.jsx):
//   • Anchoring is by *viewport coordinate*, not pan/zoom world space — there is no zoom here, so
//     pins are stored as plain client x/y and the overlay is a fixed full-viewport layer.
//   • Scoping is by *screen*, not artboard slot — every comment carries a `scope` string (e.g.
//     "assembly/bom") and only renders while that screen/sub-view is active, so a comment dropped
//     on the BOM doesn't bleed onto the 3D viewer.
//
// The thread modal / draft composer / HUD / sidebar are the exact components the canvas uses,
// re-exported on window.DCComments — so the look stays identical and there's no duplicated UI.
// No object-rest destructuring (scripts/check-browser-sources.mjs).

const AC_STATE_API_PATH = "/api/app-comments-state";
const AC_LOCAL_STATE_KEY = "drafternotes.app-comments-state";
const AC_POLL_INTERVAL_MS = 2000;
const AC_COMMENT_CURSOR = 'url("assets/CommentCursor.svg") 4 18, crosshair';
const AC_FALLBACK_USER = { id: "guest", name: "Guest", initials: "GU", avatar: "linear-gradient(135deg,#FFB86C,#FF6A3D,#D93A7A)" };

// One-time CSS for the app overlay. Repositions the shared HUD to the bottom-right (top-left would
// collide with the app's logo/header) and tints the full-screen capture layer. The dc-* comment
// styles themselves are injected by design-canvas.jsx at load, so pins/threads look identical.
if (typeof document !== "undefined" && !document.getElementById("app-comments-styles")) {
  const s = document.createElement("style");
  s.id = "app-comments-styles";
  s.textContent = [
    ".app-comments-root{position:fixed;inset:0;z-index:140;pointer-events:none;--dc-zoom:1}",
    // In dev mode, lift the overlay above every app layer (incl. the z:99998 GD&T picker) so the
    // capture can intercept events over popovers/menus and inspect them. Safe: comment mode is off.
    ".app-comments-root.is-dev{z-index:100000}",
    ".app-comments-capture{position:absolute;inset:0;pointer-events:auto;cursor:" + AC_COMMENT_CURSOR + ";background:rgba(99,102,241,0.04)}",
    ".app-comments-layer{position:absolute;inset:0;pointer-events:none}",
    // Move the reused HUD out of the top-left (clashes with app headers) to the bottom-right.
    // The overlay root is pointer-events:none so the app underneath stays interactive; re-enable
    // events on the HUD itself, or it can't be hovered/clicked (only the ⌘C shortcut would work).
    // z above the GD&T picker backdrop (99998) so the dev/comment toggles stay clickable while a
    // popover is open — clicking the dev button then keeps the picker open instead of dismissing it.
    ".app-comments-root .dc-comments-hud{top:auto;bottom:20px;left:auto;right:20px;pointer-events:auto;z-index:99999}",
    // Sidebar reused as-is (fixed right, full height) — only shows in comment mode.
    ".app-comments-empty{position:fixed;bottom:64px;right:20px;pointer-events:none;background:var(--gray-900,#1c1c1c);color:#fff;font-family:var(--font-sans);font-size:12px;font-weight:500;padding:8px 12px;border-radius:10px;box-shadow:0 8px 22px rgba(0,0,0,0.18);opacity:0.92}",

    // ---- Dev mode (Figma-style inspector) ----
    // Capture layer blocks the app's own clicks; faint orange tint signals dev mode (vs comment's indigo).
    ".app-dev-capture{position:absolute;inset:0;z-index:1;pointer-events:auto;cursor:crosshair;background:rgba(253,126,20,0.03)}",
    // Interact passthrough (⌥ held): drop the capture so the live app receives clicks/hovers again.
    ".app-dev-capture.is-interact{pointer-events:none;background:transparent}",
    ".app-dev-hint{bottom:64px}",
    ".app-dev-interact{background:#1971C2}",
    // First-run explainer for the ⌥ interact gesture (dark card, bottom-right above the HUD).
    ".dev-intro{position:fixed;bottom:74px;right:20px;z-index:100001;pointer-events:auto;width:320px;max-width:calc(100vw - 40px);background:#1c1d22;border:1px solid #34363d;border-radius:12px;box-shadow:0 12px 40px rgba(0,0,0,0.45);padding:16px;font-family:var(--font-sans);color:#e6e7ea;animation:devIntroIn 160ms var(--ease,ease) both}",
    "@keyframes devIntroIn{from{opacity:0;transform:translateY(6px)}to{opacity:1;transform:none}}",
    ".dev-intro-title{font-size:14px;font-weight:600;color:#f3f4f6;margin-bottom:8px}",
    ".dev-intro-body{font-size:12.5px;line-height:1.55;color:#c7c9d1;margin:0 0 14px}",
    ".dev-intro-kbd{display:inline-block;font-family:var(--font-mono);font-size:11px;background:#2a2c33;border:1px solid #3f424b;border-radius:5px;padding:1px 6px;color:#f3f4f6;white-space:nowrap}",
    ".dev-intro-foot{display:flex;align-items:center;gap:10px;justify-content:space-between}",
    ".dev-intro-note{font-size:11px;color:#8b8d96}",
    ".dev-intro-btn{flex:none;appearance:none;border:0;background:var(--orange-500);color:#fff;font-family:var(--font-sans);font-size:12.5px;font-weight:600;padding:7px 14px;border-radius:7px;cursor:pointer}",
    ".dev-intro-btn:hover{background:var(--orange-600)}",
    // Highlight overlays (viewport-fixed, non-interactive).
    // Hover = orange wash; selected = persistent blue outline (Figma convention) lifted above the
    // spacing bands so it stays visible on small elements.
    ".dev-highlight{position:fixed;z-index:2;pointer-events:none;border:1px solid var(--orange-400);background:rgba(253,126,20,0.08);border-radius:1px}",
    ".dev-highlight-selected{z-index:5;border:1.5px solid #228BE6;background:transparent;box-shadow:0 0 0 1px rgba(34,139,230,0.45),0 0 0 4px rgba(34,139,230,0.18)}",
    // Size pill under the hovered element.
    ".dev-dim-badge{position:fixed;z-index:4;pointer-events:none;transform:translateX(-50%);background:var(--orange-500,#fd7e14);color:#fff;font-family:var(--font-mono);font-size:10px;font-weight:600;line-height:1;padding:3px 5px;border-radius:3px;white-space:nowrap;box-shadow:0 1px 3px rgba(0,0,0,0.25)}",
    // Figma-style red distance guides between the selected and hovered elements.
    ".dev-measure-line{position:fixed;z-index:3;pointer-events:none;background:#fa5252}",
    ".dev-measure-line.h{height:1px;transform:translateY(-0.5px)}",
    ".dev-measure-line.v{width:1px;transform:translateX(-0.5px)}",
    ".dev-measure-label{position:fixed;z-index:4;pointer-events:none;transform:translate(-50%,-50%);background:#fa5252;color:#fff;font-family:var(--font-mono);font-size:10px;font-weight:600;line-height:1;padding:2px 4px;border-radius:3px;white-space:nowrap}",
    // Hatched padding/gap bands (Figma-style pink stripes) with a centered px label.
    ".dev-region{position:fixed;z-index:2;pointer-events:none;background-image:repeating-linear-gradient(-45deg,rgba(240,101,149,0.55) 0,rgba(240,101,149,0.55) 1.5px,rgba(240,101,149,0.12) 1.5px,rgba(240,101,149,0.12) 6px)}",
    ".dev-region-label{position:fixed;z-index:4;pointer-events:none;transform:translate(-50%,-50%);background:#e64980;color:#fff;font-family:var(--font-mono);font-size:10px;font-weight:600;line-height:1;padding:2px 4px;border-radius:3px;white-space:nowrap}",
    // Floating, draggable panel — hugs its contents (width + height), scrolls if it outgrows the viewport.
    // Below the HUD (z130) so the dev toggle stays clickable. Default-positioned top-right; drag moves it.
    ".dev-inspector{position:fixed;top:16px;right:16px;width:max-content;min-width:300px;max-width:400px;max-height:calc(100vh - 32px);z-index:125;pointer-events:auto;display:flex;flex-direction:column;overflow:hidden;background:#1c1d22;border:1px solid #34363d;border-radius:12px;box-shadow:0 12px 40px rgba(0,0,0,0.45);font-family:var(--font-sans);color:#e6e7ea}",
    ".dev-inspector-head{position:relative;padding:12px 14px;padding-right:38px;border-bottom:1px solid #34363d;flex:none;cursor:move;user-select:none;touch-action:none}",
    ".dev-inspector-title{font-size:11px;font-weight:600;letter-spacing:.08em;text-transform:uppercase;color:#8b8d96}",
    ".dev-inspector-name{font-size:14px;font-weight:600;color:#f3f4f6;margin-top:4px}",
    ".dev-inspector-tag{font-family:var(--font-mono);font-size:11px;color:#8b8d96;margin-top:2px;word-break:break-all;line-height:1.4}",
    ".dev-inspector-close{position:absolute;top:10px;right:10px;width:24px;height:24px;display:grid;place-items:center;border:0;background:transparent;color:#8b8d96;font-size:18px;line-height:1;border-radius:var(--r-sm);cursor:pointer}",
    ".dev-inspector-close:hover{background:#2a2c33;color:#f3f4f6}",
    ".dev-inspector-body{flex:1;min-height:0;overflow-y:auto;padding-bottom:20px}",
    ".dev-section{padding:12px 14px;border-bottom:1px solid #34363d}",
    ".dev-section:last-child{border-bottom:0}",
    ".dev-section-title{font-size:12px;font-weight:600;color:#c7c9d1;margin-bottom:10px;letter-spacing:.01em}",
    ".dev-row{display:flex;align-items:flex-start;gap:8px;min-height:22px}",
    ".dev-row+.dev-row{margin-top:3px}",
    ".dev-row-label{flex:0 0 84px;font-size:11.5px;color:#8b8d96;padding-top:2px}",
    ".dev-row-value{flex:1;min-width:0;font-size:12.5px;color:#e6e7ea;display:flex;align-items:center;gap:7px;flex-wrap:wrap;word-break:break-word}",
    ".dev-swatch{width:14px;height:14px;border-radius:3px;border:1px solid rgba(255,255,255,0.18);flex:none}",
    ".dev-color-text{font-size:12px}",
    ".dev-mono{font-family:var(--font-mono);font-size:11px;color:#9b9da6}",
    // Off-palette color callout — flags a non-token color and names the closest design token.
    ".dev-custom-tag{font-size:10px;font-weight:600;letter-spacing:.03em;text-transform:uppercase;color:#fbbf24;background:rgba(251,191,36,0.12);border-radius:4px;padding:1px 5px;margin-left:2px}",
    ".dev-near{flex-basis:100%;display:flex;align-items:center;gap:6px;flex-wrap:wrap;margin-top:3px;font-size:11.5px;color:#9b9da6}",
    ".dev-near-arrow{color:#6b6d76;font-size:11px}",
    ".dev-text-content{font-family:var(--font-mono);font-size:11.5px;line-height:1.5;color:#c7c9d1;background:#15161a;border:1px solid #34363d;border-radius:8px;padding:9px 11px;max-height:160px;overflow-y:auto;white-space:pre-wrap;word-break:break-word;user-select:text}",
    // Hover-reveal copy buttons on the values worth grabbing (colors, font, size, text content).
    ".dev-section-title-row{display:flex;align-items:center;gap:6px}",
    ".dev-copy{opacity:0;flex:none;margin-left:auto;width:20px;height:20px;display:inline-grid;place-items:center;padding:0;border:0;background:transparent;color:#8b8d96;border-radius:5px;cursor:pointer;transition:opacity .12s ease,background .12s ease,color .12s ease}",
    ".dev-copy svg{display:block}",
    ".dev-copy:hover{background:#2a2c33;color:#f3f4f6}",
    ".dev-copy-done{opacity:1;color:#4ade80}",
    ".dev-row:hover .dev-copy,.dev-section-title-row:hover .dev-copy,.dev-text-wrap:hover .dev-copy,.dev-copy:focus-visible{opacity:1}",
    ".dev-text-wrap{position:relative}",
    // Assets section — downloadable SVG icons / images with a thumbnail + download button.
    ".dev-asset-list{display:flex;flex-direction:column;gap:8px}",
    ".dev-asset{display:flex;align-items:center;gap:10px}",
    // Light checkerboard (like Figma's asset preview) + dark currentColor so both dark and light
    // icons stay visible — a flat dark fill hid dark-stroked SVGs against the panel.
    ".dev-asset-thumb{width:40px;height:40px;flex:none;border:1px solid #34363d;border-radius:8px;display:grid;place-items:center;overflow:hidden;color:#1c1d22;background-color:#fff;background-image:linear-gradient(45deg,#d6d8dc 25%,transparent 25%),linear-gradient(-45deg,#d6d8dc 25%,transparent 25%),linear-gradient(45deg,transparent 75%,#d6d8dc 75%),linear-gradient(-45deg,transparent 75%,#d6d8dc 75%);background-size:10px 10px;background-position:0 0,0 5px,5px -5px,-5px 0}",
    ".dev-asset-thumb svg,.dev-asset-thumb img{max-width:28px;max-height:28px;width:auto;height:auto;display:block}",
    ".dev-asset-meta{flex:1;min-width:0}",
    ".dev-asset-name{font-size:12.5px;color:#e6e7ea;white-space:nowrap;overflow:hidden;text-overflow:ellipsis}",
    ".dev-asset-dims{font-family:var(--font-mono);font-size:11px;color:#8b8d96;margin-top:2px}",
    ".dev-asset-dl{flex:none;width:30px;height:30px;display:grid;place-items:center;padding:0;border:0;background:transparent;color:#8b8d96;border-radius:6px;cursor:pointer;transition:background .12s ease,color .12s ease}",
    ".dev-asset-dl svg{display:block}",
    ".dev-asset-dl:hover{background:#2a2c33;color:#f3f4f6}",
    ".dev-copy-corner{position:absolute;top:6px;right:6px;margin:0;background:#23252b;color:#c7c9d1}",
    ".dev-copy-corner:hover{background:#34363d;color:#f3f4f6}",
    // List / Code segmented toggle + the CSS code block it reveals.
    ".dev-modebar{padding:10px 14px 4px;display:flex;align-items:center;gap:8px}",
    // Copy-as format dropdown (CSS / JSX / Mantine / Tailwind).
    ".dev-fmt{margin-left:auto;appearance:none;background:#2a2c33;color:#e6e7ea;border:1px solid #34363d;border-radius:6px;font-family:var(--font-sans);font-size:12px;font-weight:500;padding:4px 9px;cursor:pointer}",
    ".dev-fmt:hover{background:#34363d}",
    // Interaction-state forcing: a toggle per available pseudo-state + its declarations.
    ".dev-state{margin-bottom:8px}",
    ".dev-state-toggle{display:flex;align-items:center;gap:8px;width:100%;text-align:left;appearance:none;border:1px solid #34363d;background:#1f2025;color:#e6e7ea;border-radius:7px;padding:7px 10px;cursor:pointer;transition:border-color .12s ease,background .12s ease}",
    ".dev-state-toggle:hover{background:#26272d}",
    ".dev-state-toggle.is-on{border-color:var(--orange-500);background:rgba(253,126,20,0.12)}",
    ".dev-state-dot{width:8px;height:8px;border-radius:50%;background:#5a5c66;flex:none}",
    ".dev-state-toggle.is-on .dev-state-dot{background:var(--orange-400)}",
    ".dev-state-name{font-size:12.5px;font-weight:600}",
    ".dev-state-action{margin-left:auto;font-size:11px;color:#8b8d96}",
    ".dev-state-toggle.is-on .dev-state-action{color:var(--orange-300,#FFC078)}",
    ".dev-state-decls{margin-top:6px}",
    ".dev-seg{display:inline-flex;gap:2px;background:#2a2c33;border-radius:7px;padding:2px}",
    ".dev-seg-btn{appearance:none;border:0;background:transparent;color:#9b9da6;font-family:var(--font-sans);font-size:12px;font-weight:500;padding:4px 13px;border-radius:5px;cursor:pointer;transition:background .12s ease,color .12s ease}",
    ".dev-seg-btn:hover{color:#e6e7ea}",
    ".dev-seg-btn.is-active{background:#15161a;color:#f3f4f6;box-shadow:0 1px 2px rgba(0,0,0,0.35)}",
    ".dev-code{font-family:var(--font-mono);font-size:11.5px;line-height:1.6;color:#c7c9d1;background:#15161a;border:1px solid #34363d;border-radius:8px;padding:9px 11px;margin:0;max-width:100%;overflow-x:auto;white-space:pre;user-select:text}",
    // Box-model diagram (nested border → padding → content with per-edge px labels).
    ".dev-bm{padding:10px 14px 14px}",
    ".dev-bm-layer{position:relative;border-radius:6px}",
    ".dev-bm-border{background:#2a2c33;padding:22px}",
    ".dev-bm-padding{background:rgba(96,165,250,0.18);padding:22px}",
    ".dev-bm-content{background:#1c1d22;border:1px dashed #4b4d56;border-radius:4px;min-height:30px;display:grid;place-items:center;font-family:var(--font-mono);font-size:11px;color:#c7c9d1}",
    ".dev-bm-tag{position:absolute;top:4px;left:8px;font-size:9px;letter-spacing:.06em;text-transform:uppercase;color:#8b8d96}",
    ".dev-bm-edge{position:absolute;font-family:var(--font-mono);font-size:10px;color:#9b9da6}",
    ".dev-bm-t{top:5px;left:50%;transform:translateX(-50%)}",
    ".dev-bm-b{bottom:5px;left:50%;transform:translateX(-50%)}",
    ".dev-bm-l{left:6px;top:50%;transform:translateY(-50%)}",
    ".dev-bm-r{right:6px;top:50%;transform:translateY(-50%)}",
  ].join("\n");
  document.head.appendChild(s);
}

function acReadLocal() {
  try {
    const raw = window.localStorage.getItem(AC_LOCAL_STATE_KEY);
    const parsed = raw ? JSON.parse(raw) : {};
    return Array.isArray(parsed.comments) ? parsed.comments : [];
  } catch (err) {
    return [];
  }
}
function acWriteLocal(comments) {
  try {
    window.localStorage.setItem(AC_LOCAL_STATE_KEY, JSON.stringify({ comments }));
  } catch (err) {}
}
// How many times the dev-mode intro has been shown (we surface it the first 5 entries, then stop).
const AC_DEVINTRO_KEY = "ac-devmode-intro-count";
function acReadDevIntro() { try { return parseInt(window.localStorage.getItem(AC_DEVINTRO_KEY) || "0", 10) || 0; } catch (err) { return 0; } }
function acWriteDevIntro(n) { try { window.localStorage.setItem(AC_DEVINTRO_KEY, String(n)); } catch (err) {} }
async function acReadShared() {
  try {
    const response = await fetch(AC_STATE_API_PATH, { cache: "no-store", headers: { "Cache-Control": "no-store" } });
    if (response.ok) {
      const data = await response.json();
      return Array.isArray(data.comments) ? data.comments : [];
    }
  } catch (err) {}
  return null;
}
async function acWriteShared(comments) {
  try {
    const response = await fetch(AC_STATE_API_PATH, {
      method: "POST",
      cache: "no-store",
      headers: { "Cache-Control": "no-store", "Content-Type": "application/json" },
      body: JSON.stringify({ comments }),
    });
    return response.ok;
  } catch (err) {
    return false;
  }
}

// Is the current keyboard focus / selection somewhere we should NOT hijack Cmd+C for commenting?
// (Lets normal copy work in inputs and when text is selected.)
function acShouldIgnoreShortcut() {
  const sel = typeof window.getSelection === "function" ? window.getSelection() : null;
  if (sel && sel.toString() && !sel.isCollapsed) return true;
  const ae = document.activeElement;
  if (ae instanceof Element && ae.closest('input, textarea, select, [contenteditable="true"], [contenteditable=""], .dc-comment-thread, .dc-comments-sidebar')) return true;
  return false;
}

// ---- Dev mode (Figma-style inspector) helpers -----------------------------------------------
// The real Mantine default palette (https://mantine.dev/theming/colors/) — 14 hues × 10 shades (0–9),
// exact hex. This is the canonical reference: colors are named when they match exactly, and otherwise
// flagged off-palette with the closest Mantine shade. Shade 6 is each hue's primary (the big swatch).
const AC_MANTINE = {
  dark:   ["#C9C9C9", "#B8B8B8", "#828282", "#696969", "#424242", "#3B3B3B", "#2E2E2E", "#242424", "#1F1F1F", "#141414"],
  gray:   ["#F8F9FA", "#F1F3F5", "#E9ECEF", "#DEE2E6", "#CED4DA", "#ADB5BD", "#868E96", "#495057", "#343A40", "#212529"],
  red:    ["#FFF5F5", "#FFE3E3", "#FFC9C9", "#FFA8A8", "#FF8787", "#FF6B6B", "#FA5252", "#F03E3E", "#E03131", "#C92A2A"],
  pink:   ["#FFF0F6", "#FFDEEB", "#FCC2D7", "#FAA2C1", "#F783AC", "#F06595", "#E64980", "#D6336C", "#C2255C", "#A61E4D"],
  grape:  ["#F8F0FC", "#F3D9FA", "#EEBEFA", "#E599F7", "#DA77F2", "#CC5DE8", "#BE4BDB", "#AE3EC9", "#9C36B5", "#862E9C"],
  violet: ["#F3F0FF", "#E5DBFF", "#D0BFFF", "#B197FC", "#9775FA", "#845EF7", "#7950F2", "#7048E8", "#6741D9", "#5F3DC4"],
  indigo: ["#EDF2FF", "#DBE4FF", "#BAC8FF", "#91A7FF", "#748FFC", "#5C7CFA", "#4C6EF5", "#4263EB", "#3B5BDB", "#364FC7"],
  blue:   ["#E7F5FF", "#D0EBFF", "#A5D8FF", "#74C0FC", "#4DABF7", "#339AF0", "#228BE6", "#1C7ED6", "#1971C2", "#1864AB"],
  cyan:   ["#E3FAFC", "#C5F6FA", "#99E9F2", "#66D9E8", "#3BC9DB", "#22B8CF", "#15AABF", "#1098AD", "#0C8599", "#0B7285"],
  teal:   ["#E6FCF5", "#C3FAE8", "#96F2D7", "#63E6BE", "#38D9A9", "#20C997", "#12B886", "#0CA678", "#099268", "#087F5B"],
  green:  ["#EBFBEE", "#D3F9D8", "#B2F2BB", "#8CE99A", "#69DB7C", "#51CF66", "#40C057", "#37B24D", "#2F9E44", "#2B8A3E"],
  lime:   ["#F4FCE3", "#E9FAC8", "#D8F5A2", "#C0EB75", "#A9E34B", "#94D82D", "#82C91E", "#74B816", "#66A80F", "#5C940D"],
  yellow: ["#FFF9DB", "#FFF3BF", "#FFEC99", "#FFE066", "#FFD43B", "#FCC419", "#FAB005", "#F59F00", "#F08C00", "#E67700"],
  orange: ["#FFF4E6", "#FFE8CC", "#FFD8A8", "#FFC078", "#FFA94D", "#FF922B", "#FD7E14", "#F76707", "#E8590C", "#D9480F"],
};

function acParseColor(str) {
  if (!str) return null;
  str = String(str).trim();
  if (str === "transparent") return { r: 0, g: 0, b: 0, a: 0 };
  let m = str.match(/^#([0-9a-f]{3})$/i);
  if (m) { const h = m[1]; return { r: parseInt(h[0] + h[0], 16), g: parseInt(h[1] + h[1], 16), b: parseInt(h[2] + h[2], 16), a: 1 }; }
  m = str.match(/^#([0-9a-f]{6})$/i);
  if (m) { const h = m[1]; return { r: parseInt(h.slice(0, 2), 16), g: parseInt(h.slice(2, 4), 16), b: parseInt(h.slice(4, 6), 16), a: 1 }; }
  m = str.match(/^rgba?\(([^)]+)\)$/i);
  if (m) { const p = m[1].split(",").map((s) => s.trim()); return { r: parseInt(p[0], 10), g: parseInt(p[1], 10), b: parseInt(p[2], 10), a: p[3] !== undefined ? parseFloat(p[3]) : 1 }; }
  return null;
}
function acHex(rgb) {
  const h = (n) => ("0" + Math.max(0, Math.min(255, Math.round(n))).toString(16)).slice(-2).toUpperCase();
  return "#" + h(rgb.r) + h(rgb.g) + h(rgb.b);
}
// Flatten AC_MANTINE into a lookup list + exact hex map, once.
let _acMantineList = null, _acMantineMap = null;
function acMantine() {
  if (_acMantineList) return _acMantineList;
  const list = [], map = {};
  for (const hue in AC_MANTINE) {
    const cap = hue.charAt(0).toUpperCase() + hue.slice(1);
    AC_MANTINE[hue].forEach((hex, i) => {
      const entry = { label: cap + " " + i, mantine: hue + "." + i, hex, swatch: hex, rgb: acParseColor(hex) };
      list.push(entry);
      map[hex] = entry;
    });
  }
  // theme.white / theme.black are named Mantine colors outside the hue scales, not off-palette.
  [["White", "white", "#FFFFFF"], ["Black", "black", "#000000"]].forEach(([label, mantine, hex]) => {
    const entry = { label, mantine, hex, swatch: hex, rgb: acParseColor(hex) };
    list.push(entry);
    map[hex] = entry;
  });
  _acMantineList = list;
  _acMantineMap = map;
  return list;
}
// Closest Mantine shade to an arbitrary rgb, by Euclidean distance in RGB space.
function acNearestToken(rgb) {
  let best = null, bestD = Infinity;
  for (const t of acMantine()) {
    const dr = t.rgb.r - rgb.r, dg = t.rgb.g - rgb.g, db = t.rgb.b - rgb.b;
    const d = dr * dr + dg * dg + db * db;
    if (d < bestD) { bestD = d; best = t; }
  }
  if (!best) return null;
  return { label: best.label, mantine: best.mantine, hex: best.hex, swatch: best.hex, dist: Math.round(Math.sqrt(bestD)) };
}
// Resolve a computed color string to { swatch, value, label?, mantine?, transparent?, custom?, nearest? }.
function acColorInfo(cssColor) {
  const rgb = acParseColor(cssColor);
  if (!rgb) return null;
  if (rgb.a === 0) return { transparent: true, value: "transparent" };
  const hex = acHex(rgb);
  acMantine();
  const tok = _acMantineMap[hex];
  const value = rgb.a < 1 ? hex + " · " + Math.round(rgb.a * 100) + "%" : hex;
  if (tok) return { swatch: cssColor, value, label: tok.label, mantine: tok.mantine };
  // Off-palette color — surface the closest Mantine shade so it can be reconciled to the system.
  return { swatch: cssColor, value, custom: true, nearest: acNearestToken(rgb) };
}

const AC_COMPONENT_RULES = [
  { has: "btn", name: (cl) => cl.includes("btn-primary") ? "Button / Primary" : cl.includes("btn-secondary") ? "Button / Secondary" : cl.includes("btn-danger") ? "Button / Danger" : cl.includes("btn-ghost") ? "Button / Ghost" : cl.includes("btn-icon") ? "Icon button" : "Button" },
  { has: "gdt-symbol-btn", name: () => "Toolbar button" },
  { has: "gdt-editor-field", name: () => "Note editor" },
  { has: "note-row", name: () => "Note row" },
  { has: "flag-note", name: () => "Specific-note flag" },
  { has: "filter-rail", name: () => "Filter rail" },
  { has: "filter-row", name: () => "Filter row" },
  { has: "asm-props-section", name: () => "Properties section" },
  { has: "asm-props", name: () => "Properties panel" },
  { has: "asm-feature-row", name: () => "Tree row" },
  { has: "asm-tree-item", name: () => "Tree row" },
  { has: "dc-comments-hud", name: () => "Comment toolbar" },
  { has: "dc-comments-sidebar", name: () => "Comment sidebar" },
  { has: "dev-inspector", name: () => "Dev inspector" },
  { has: "tagchip", name: () => "Tag chip" },
  { has: "tt-kbd", name: () => "Shortcut chip" },
  { has: "card", name: () => "Card" },
];
function acHumanize(s) {
  return String(s).replace(/^(asm|dc|gdt|np|ps|ff|tt)-/, "").replace(/-/g, " ").replace(/\b\w/g, (m) => m.toUpperCase());
}
function acComponentLabel(el) {
  const tag = el.tagName ? el.tagName.toLowerCase() : "node";
  const classes = Array.from(el.classList || []);
  const cl = classes.join(" ");
  let name = null;
  for (const r of AC_COMPONENT_RULES) { if (cl.indexOf(r.has) !== -1) { name = r.name(cl); break; } }
  if (!name) {
    if (tag === "button") name = "Button";
    else if (tag === "input") name = "Input";
    else if (tag === "select") name = "Select";
    else if (tag === "img") name = "Image";
    else if (tag === "svg") name = "Icon";
    else if (tag === "a") name = "Link";
    else if (classes.length) name = acHumanize(classes.slice().sort((a, b) => b.length - a.length)[0]);
    else name = tag.charAt(0).toUpperCase() + tag.slice(1);
  }
  return { name, tag: tag + classes.map((c) => "." + c).join("") };
}

function acFlow(cs) {
  const d = cs.display;
  if (d.indexOf("grid") !== -1) return "Grid";
  if (d.indexOf("flex") !== -1) return /^column/.test(cs.flexDirection) ? "Vertical" : "Horizontal";
  if (d.indexOf("inline") === 0) return "Inline";
  if (d === "none") return "None";
  return "Block";
}

// Read all the layout/color facts off a real DOM element (Figma "Layer properties" equivalent).
function acInspect(el) {
  const cs = getComputedStyle(el);
  const rect = el.getBoundingClientRect();
  const num = (v) => Math.round(parseFloat(v) || 0);
  const box = (a, b, c, d) => ({ top: num(cs[a]), right: num(cs[b]), bottom: num(cs[c]), left: num(cs[d]) });
  const padding = box("paddingTop", "paddingRight", "paddingBottom", "paddingLeft");
  const border = box("borderTopWidth", "borderRightWidth", "borderBottomWidth", "borderLeftWidth");
  const margin = box("marginTop", "marginRight", "marginBottom", "marginLeft");
  const uni = (b) => (b.top === b.right && b.right === b.bottom && b.bottom === b.left);
  const boxText = (b) => uni(b) ? b.top + "px" : b.top + "px " + b.right + "px " + b.bottom + "px " + b.left + "px";
  const contentW = Math.max(0, Math.round(rect.width - padding.left - padding.right - border.left - border.right));
  const contentH = Math.max(0, Math.round(rect.height - padding.top - padding.bottom - border.top - border.bottom));

  // Parent content box — used to infer "Fill" sizing.
  const parent = el.parentElement;
  let parentCW = null;
  if (parent) {
    const pcs = getComputedStyle(parent);
    const pr = parent.getBoundingClientRect();
    parentCW = pr.width - num(pcs.paddingLeft) - num(pcs.paddingRight) - num(pcs.borderLeftWidth) - num(pcs.borderRightWidth);
  }
  const grow = parseFloat(cs.flexGrow) || 0;
  const isInline = cs.display.indexOf("inline") === 0;
  const fixedW = el.style && el.style.width && el.style.width !== "auto" && el.style.width.indexOf("%") === -1;
  const fixedH = el.style && el.style.height && el.style.height !== "auto" && el.style.height.indexOf("%") === -1;
  const widthLabel = fixedW ? "Fixed"
    : (grow > 0 || cs.alignSelf === "stretch" || (parentCW != null && Math.abs(rect.width - parentCW) <= 1 && !isInline)) ? "Fill" : "Hug";
  const heightLabel = fixedH ? "Fixed" : (grow > 0 && /column/.test(cs.flexDirection)) ? "Fill" : "Hug";

  // Gap (flex/grid only).
  let gap = null;
  if (cs.display.indexOf("flex") !== -1 || cs.display.indexOf("grid") !== -1) {
    const g = cs.gap;
    if (g && g !== "normal") {
      const parts = g.split(" ");
      if (!parts.every((p) => num(p) === 0)) gap = (parts.length === 2 && parts[0] === parts[1]) ? parts[0] : g;
    }
  }
  const anyBorder = border.top || border.right || border.bottom || border.left;
  const borderText = !anyBorder ? "None" : (uni(border) ? border.top + "px " + cs.borderTopStyle : boxText(border) + " " + cs.borderTopStyle);

  // Only report "Text" color for elements that actually render their own text. Every element
  // inherits `color`, so an <img>, icon <svg>, or pure container would otherwise show a bogus
  // text swatch. We require a direct, non-whitespace text node (not text from descendants).
  const hasOwnText = Array.prototype.some.call(
    el.childNodes || [],
    (n) => n.nodeType === 3 && n.textContent.trim() !== ""
  );

  // Typography + text content — only meaningful for elements that render their own text.
  let typo = null, textContent = null;
  if (hasOwnText) {
    const fam = (cs.fontFamily || "").split(",")[0].replace(/["']/g, "").trim();
    const px = (v) => { const n = parseFloat(v); return isNaN(n) ? v : (Math.round(n * 100) / 100) + "px"; };
    typo = {
      font: fam || "—",
      weight: cs.fontWeight,
      style: cs.fontStyle && cs.fontStyle !== "normal" ? cs.fontStyle.charAt(0).toUpperCase() + cs.fontStyle.slice(1) : "Regular",
      size: px(cs.fontSize),
      lineHeight: cs.lineHeight === "normal" ? "Auto" : px(cs.lineHeight),
      letterSpacing: !cs.letterSpacing || cs.letterSpacing === "normal" ? "0" : px(cs.letterSpacing),
      align: cs.textAlign,
      transform: cs.textTransform && cs.textTransform !== "none" ? cs.textTransform : null,
    };
    const t = (el.textContent || "").replace(/\s+/g, " ").trim();
    textContent = t || null;
  }

  return {
    el, rect,
    tagName: (el.tagName || "div").toLowerCase(),
    classList: Array.from(el.classList || []),
    component: acComponentLabel(el),
    flow: acFlow(cs),
    width: Math.round(rect.width), height: Math.round(rect.height),
    widthLabel, heightLabel,
    radius: (cs.borderRadius && cs.borderRadius !== "0px") ? cs.borderRadius : "0",
    borderText, hasBorder: !!anyBorder,
    paddingText: boxText(padding), gap,
    padding, border, contentW, contentH,
    bg: acColorInfo(cs.backgroundColor),
    borderColor: anyBorder ? acColorInfo(cs.borderTopColor) : null,
    color: hasOwnText ? acColorInfo(cs.color) : null,
    typo, text: textContent,
    assets: acAssets(el),
  };
}

// Serialize an <svg> to a standalone, paste/download-ready string. Resolves <use href="#id">
// against the in-document sprite (index.html <symbol> defs) so sprite icons aren't exported empty.
function acSerializeSvg(svg) {
  const clone = svg.cloneNode(true);
  Array.from(clone.querySelectorAll("use")).forEach((use) => {
    const href = use.getAttribute("href") || use.getAttribute("xlink:href") || "";
    const sym = href.charAt(0) === "#" && document.getElementById(href.slice(1));
    if (!sym) return;
    // The <use> inherits presentation + viewBox from the symbol; carry them onto the root.
    ["fill", "stroke", "stroke-width", "stroke-linecap", "stroke-linejoin"].forEach((a) => {
      const v = sym.getAttribute(a);
      if (v && !clone.getAttribute(a)) clone.setAttribute(a, v);
    });
    if (!clone.getAttribute("viewBox") && sym.getAttribute("viewBox")) clone.setAttribute("viewBox", sym.getAttribute("viewBox"));
    const frag = clone.ownerDocument.createDocumentFragment();
    Array.from(sym.childNodes).forEach((n) => frag.appendChild(n.cloneNode(true)));
    use.parentNode.replaceChild(frag, use);
  });
  // Strip app CSS hooks so the standalone icon (thumbnail + download) renders from its own
  // presentation attributes instead of being overridden by .icon/.icon-sm/note-row rules — that
  // override is what was blanking the thumbnails (CSS beats presentation attributes).
  clone.removeAttribute("class");
  Array.from(clone.querySelectorAll("[class]")).forEach((n) => n.removeAttribute("class"));
  // Give it intrinsic dimensions from the viewBox so it has a stable size on its own.
  if (!clone.getAttribute("width") && !clone.getAttribute("height")) {
    const vb = (clone.getAttribute("viewBox") || "").split(/[\s,]+/);
    if (vb.length === 4) { clone.setAttribute("width", vb[2]); clone.setAttribute("height", vb[3]); }
  }
  if (!clone.getAttribute("xmlns")) clone.setAttribute("xmlns", "http://www.w3.org/2000/svg");
  return new XMLSerializer().serializeToString(clone);
}

// Downloadable assets within (or equal to) the inspected element: SVG icons and <img>s.
function acAssets(el) {
  if (!el || !el.tagName) return [];
  const out = [], seen = new Set(), keys = new Set(); // keys = content identity, to collapse dupes
  const tag = el.tagName.toLowerCase();
  const svgs = tag === "svg" ? [el] : [];
  if (el.querySelectorAll) Array.prototype.push.apply(svgs, Array.from(el.querySelectorAll("svg")));
  svgs.forEach((svg) => {
    if (seen.has(svg)) return; seen.add(svg);
    const markup = acSerializeSvg(svg);
    if (keys.has(markup)) return; keys.add(markup); // identical icon already listed once
    const use = svg.querySelector("use");
    const href = use ? (use.getAttribute("href") || use.getAttribute("xlink:href") || "") : "";
    const id = href.charAt(0) === "#" ? href.slice(1) : "";
    const aria = svg.getAttribute("aria-label");
    const name = id || svg.id || (aria && aria.length <= 40 ? aria : "") || ("icon-" + (out.length + 1));
    const r = svg.getBoundingClientRect();
    out.push({ kind: "svg", name, w: Math.round(r.width) || 24, h: Math.round(r.height) || 24, markup });
  });
  const imgs = tag === "img" ? [el] : [];
  if (el.querySelectorAll) Array.prototype.push.apply(imgs, Array.from(el.querySelectorAll("img")));
  imgs.forEach((img) => {
    if (seen.has(img) || !img.src) return; seen.add(img);
    if (keys.has(img.src)) return; keys.add(img.src); // same source already listed once
    const base = img.src.split("/").pop().split("?")[0].split("#")[0] || ("image-" + (out.length + 1));
    const r = img.getBoundingClientRect();
    out.push({ kind: "img", name: base, w: img.naturalWidth || Math.round(r.width), h: img.naturalHeight || Math.round(r.height), src: img.src });
  });
  return out.slice(0, 24);
}

// Figma-style measurement between the selected rect (anchor) and the hovered rect. Returns red
// guide segments in viewport coords: gaps when the rects are apart (sibling spacing), insets when
// the hovered rect sits inside the selected one (or vice versa). Each segment is one px distance.
function acMeasure(sel, hov) {
  const a = { l: sel.left, t: sel.top, r: sel.left + sel.width, b: sel.top + sel.height };
  const b = { l: hov.left, t: hov.top, r: hov.left + hov.width, b: hov.top + hov.height };
  const segs = [];
  const H = (x1, x2, y) => { const d = Math.round(Math.abs(x2 - x1)); if (d > 0) segs.push({ type: "h", x1: Math.min(x1, x2), x2: Math.max(x1, x2), y, label: d }); };
  const V = (y1, y2, x) => { const d = Math.round(Math.abs(y2 - y1)); if (d > 0) segs.push({ type: "v", y1: Math.min(y1, y2), y2: Math.max(y1, y2), x, label: d }); };
  const overlapX = Math.min(a.r, b.r) > Math.max(a.l, b.l);
  const overlapY = Math.min(a.b, b.b) > Math.max(a.t, b.t);

  if (overlapY) {
    // Share a horizontal band → measure left/right at the band's vertical center.
    const y = (Math.max(a.t, b.t) + Math.min(a.b, b.b)) / 2;
    if (b.r <= a.l) H(b.r, a.l, y);           // hovered sits left of selected
    else if (b.l >= a.r) H(a.r, b.l, y);      // hovered sits right of selected
    else { H(a.l, b.l, y); H(b.r, a.r, y); }  // overlapping → left + right insets
  }
  if (overlapX) {
    const x = (Math.max(a.l, b.l) + Math.min(a.r, b.r)) / 2;
    if (b.b <= a.t) V(b.b, a.t, x);           // hovered sits above selected
    else if (b.t >= a.b) V(a.b, b.t, x);      // hovered sits below selected
    else { V(a.t, b.t, x); V(b.b, a.b, x); }  // overlapping → top + bottom insets
  }
  if (!overlapX && !overlapY) {
    // Diagonal: an L of one horizontal + one vertical gap toward the hovered rect's center.
    const y = (b.t + b.b) / 2, x = (b.l + b.r) / 2;
    if (b.r <= a.l) H(b.r, a.l, y); else if (b.l >= a.r) H(a.r, b.l, y);
    if (b.b <= a.t) V(b.b, a.t, x); else if (b.t >= a.b) V(a.b, b.t, x);
  }
  return segs;
}

// Hatched spacing regions for one element (computed fresh so child rects stay aligned): the four
// padding bands (the ring between border and content box) and the gap bands between flex children.
function acPadGapRegions(el) {
  if (!el || !el.getBoundingClientRect) return { pads: [], gaps: [] };
  const cs = getComputedStyle(el);
  const r = el.getBoundingClientRect();
  const num = (v) => Math.round(parseFloat(v) || 0);
  const pt = num(cs.paddingTop), pr = num(cs.paddingRight), pb = num(cs.paddingBottom), pl = num(cs.paddingLeft);
  const bt = num(cs.borderTopWidth), br = num(cs.borderRightWidth), bb = num(cs.borderBottomWidth), bl = num(cs.borderLeftWidth);
  // Content box (inside the border) — padding bands hug these edges.
  const iL = r.left + bl, iT = r.top + bt, iR = r.right - br, iB = r.bottom - bb;
  const iW = iR - iL, iH = iB - iT;
  const pads = [];
  if (pt > 0) pads.push({ left: iL, top: iT, width: iW, height: pt, label: pt });
  if (pb > 0) pads.push({ left: iL, top: iB - pb, width: iW, height: pb, label: pb });
  if (pl > 0) pads.push({ left: iL, top: iT + pt, width: pl, height: iH - pt - pb, label: pl });
  if (pr > 0) pads.push({ left: iR - pr, top: iT + pt, width: pr, height: iH - pt - pb, label: pr });

  const gaps = [];
  if (cs.display.indexOf("flex") !== -1) {
    const kids = Array.from(el.children)
      .filter((k) => { const ks = getComputedStyle(k); return ks.display !== "none" && ks.position !== "absolute" && ks.position !== "fixed"; })
      .map((k) => k.getBoundingClientRect())
      .filter((kr) => kr.width > 0 || kr.height > 0);
    const col = /column/.test(cs.flexDirection);
    if (col) {
      kids.sort((a, b) => a.top - b.top);
      for (let i = 0; i < kids.length - 1; i++) {
        const g = kids[i + 1].top - kids[i].bottom;
        if (g > 0.5) { const l = Math.max(kids[i].left, kids[i + 1].left), rr = Math.min(kids[i].right, kids[i + 1].right);
          gaps.push({ left: l, top: kids[i].bottom, width: Math.max(0, rr - l), height: g, label: Math.round(g) }); }
      }
    } else {
      kids.sort((a, b) => a.left - b.left);
      for (let i = 0; i < kids.length - 1; i++) {
        const g = kids[i + 1].left - kids[i].right;
        if (g > 0.5) { const t = Math.max(kids[i].top, kids[i + 1].top), bm = Math.min(kids[i].bottom, kids[i + 1].bottom);
          gaps.push({ left: kids[i].right, top: t, width: g, height: Math.max(0, bm - t), label: Math.round(g) }); }
      }
    }
  }
  return { pads, gaps };
}

// Hover-reveal copy button. Copies `value` to the clipboard and flashes a check.
function DevCopyBtn(props) {
  const [done, setDone] = React.useState(false);
  const onClick = (e) => {
    e.stopPropagation();
    const finish = () => { setDone(true); window.setTimeout(() => setDone(false), 1100); };
    try {
      if (navigator.clipboard && navigator.clipboard.writeText) navigator.clipboard.writeText(props.value).then(finish, finish);
      else finish();
    } catch (err) { finish(); }
  };
  return (
    <button type="button" className={"dev-copy" + (done ? " dev-copy-done" : "") + (props.className ? " " + props.className : "")} aria-label="Copy" title={done ? "Copied" : "Copy"} onClick={onClick}>
      {done
        ? <svg width="12" height="12" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2.4" strokeLinecap="round" strokeLinejoin="round"><polyline points="20 6 9 17 4 12" /></svg>
        : <svg width="12" height="12" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round"><rect x="9" y="9" width="11" height="11" rx="2" /><path d="M5 15H4a2 2 0 0 1-2-2V4a2 2 0 0 1 2-2h9a2 2 0 0 1 2 2v1" /></svg>}
    </button>
  );
}
// One downloadable asset (SVG icon or image) with a thumbnail and a download button.
function DevAssetItem(props) {
  const a = props.asset;
  const download = () => {
    let url = a.src, revoke = false;
    if (a.kind === "svg") {
      url = URL.createObjectURL(new Blob([a.markup], { type: "image/svg+xml" }));
      revoke = true;
    }
    const safe = (a.name || "asset").replace(/[^\w.-]+/g, "-").replace(/^-+|-+$/g, "");
    const link = document.createElement("a");
    link.href = url;
    link.download = a.kind === "svg" ? safe + ".svg" : safe;
    document.body.appendChild(link);
    link.click();
    link.remove();
    if (revoke) window.setTimeout(() => URL.revokeObjectURL(url), 2000);
  };
  return (
    <div className="dev-asset">
      <div className="dev-asset-thumb">
        {a.kind === "svg"
          ? <span dangerouslySetInnerHTML={{ __html: a.markup }} />
          : <img src={a.src} alt="" />}
      </div>
      <div className="dev-asset-meta">
        <div className="dev-asset-name" title={a.name}>{a.name}</div>
        <div className="dev-asset-dims">{a.w} × {a.h} · {a.kind.toUpperCase()}</div>
      </div>
      <button type="button" className="dev-asset-dl" aria-label="Download" title="Download" onClick={download}>
        <svg width="15" height="15" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round"><path d="M12 3v12" /><path d="m7 11 5 5 5-5" /><path d="M5 21h14" /></svg>
      </button>
    </div>
  );
}
function DevRow(props) {
  return (
    <div className="dev-row">
      <span className="dev-row-label">{props.label}</span>
      <span className="dev-row-value">{props.value}</span>
      {props.copy != null && props.copy !== "" && <DevCopyBtn value={String(props.copy)} />}
    </div>
  );
}
function DevColorRow(props) {
  const info = props.info;
  if (!info) return null;
  return (
    <div className="dev-row">
      <span className="dev-row-label">{props.label}</span>
      <span className="dev-row-value">
        {!info.transparent && <span className="dev-swatch" style={{ background: info.swatch }} />}
        <span className="dev-color-text">
          {info.label
            ? (<React.Fragment>{info.mantine} <span className="dev-mono">· {info.value}</span></React.Fragment>)
            : info.transparent
              ? (<span className="dev-mono">transparent</span>)
              : (<React.Fragment><span className="dev-mono">{info.value}</span> <span className="dev-custom-tag">off-palette</span></React.Fragment>)}
        </span>
        {info.custom && info.nearest && (
          <span className="dev-near">
            <span className="dev-near-arrow">↳ closest</span>
            <span className="dev-swatch" style={{ background: info.nearest.swatch }} />
            {info.nearest.mantine} <span className="dev-mono">· {info.nearest.hex}</span>
          </span>
        )}
      </span>
      {!info.transparent && info.value && <DevCopyBtn value={info.custom && info.nearest ? info.nearest.mantine : info.value} />}
    </div>
  );
}
// A clean, paste-ready CSS value for a color (hex when opaque, the raw rgba() when it has alpha).
function acCssColor(info) {
  if (!info || info.transparent) return null;
  return info.value.indexOf(" ") === -1 ? info.value : info.swatch;
}
function acColorLine(prop, info) {
  const v = acCssColor(info);
  if (!v) return null;
  let line = prop + ": " + v + ";";
  if (info.mantine) line += " /* " + info.mantine + " */";
  else if (info.nearest) line += " /* closest " + info.nearest.mantine + " */";
  return line;
}
// Build the "Code" view: relevant facts as paste-ready CSS, grouped like the List view.
function acCodeBlocks(c) {
  const blocks = [];
  const radiusTok = acRadiusToken(c.radius);
  const layout = [
    "width: " + c.width + "px;",
    "height: " + c.height + "px;",
    c.radius !== "0" ? "border-radius: " + c.radius + ";" + (radiusTok ? " /* " + radiusTok + " */" : "") : null,
    c.hasBorder ? "border: " + c.borderText + ";" : null,
    "padding: " + c.paddingText + ";" + acSpaceNote(c.paddingText),
    c.gap ? "gap: " + c.gap + ";" + acSpaceNote(c.gap) : null,
  ].filter(Boolean);
  blocks.push({ title: "Layout", code: layout.join("\n") });

  if (c.typo) {
    const t = c.typo;
    const typ = [
      acColorLine("color", c.color),
      'font-family: "' + t.font + '";',
      "font-size: " + t.size + ";",
      "font-weight: " + t.weight + ";",
      "font-style: " + (t.style === "Regular" ? "normal" : t.style.toLowerCase()) + ";",
      "line-height: " + (t.lineHeight === "Auto" ? "normal" : t.lineHeight) + ";",
      "letter-spacing: " + (t.letterSpacing === "0" ? "normal" : t.letterSpacing) + ";",
      t.align && t.align !== "start" ? "text-align: " + t.align + ";" : null,
      t.transform ? "text-transform: " + t.transform + ";" : null,
    ].filter(Boolean);
    blocks.push({ title: "Typography", code: typ.join("\n") });
  }

  const colors = [
    acColorLine("background", c.bg),
    acColorLine("border-color", c.borderColor),
    !c.typo ? acColorLine("color", c.color) : null,
  ].filter(Boolean);
  if (colors.length) blocks.push({ title: "Colors", code: colors.join("\n") });

  return blocks;
}

// ---- Design-system token maps (mirror ds/colors.css) — used to annotate values with their token.
const AC_SPACE_TOKENS = { 4: "--s-1", 8: "--s-2", 12: "--s-3", 16: "--s-4", 20: "--s-5", 24: "--s-6", 32: "--s-8", 40: "--s-10", 48: "--s-12", 64: "--s-16", 96: "--s-24" };
const AC_RADIUS_TOKENS = { 2: "--r-xs", 4: "--r-sm", 6: "--r-md", 8: "--r-lg" };
function acSpaceToken(px) { return AC_SPACE_TOKENS[Math.round(parseFloat(px))] || null; }
function acRadiusToken(px) { return AC_RADIUS_TOKENS[Math.round(parseFloat(px))] || null; }
function acOffGrid(px) { const n = parseFloat(px); return !isNaN(n) && n > 0 && n % 4 !== 0; }
// Annotate a single-value px with its spacing token, or flag it as off the 4px grid (for QA).
function acSpaceNote(px) { if (String(px).indexOf(" ") !== -1) return ""; const t = acSpaceToken(px); if (t) return " /* " + t + " */"; if (acOffGrid(px)) return " /* off-grid */"; return ""; }

// ---- Copy-as code generators. CSS is exact; JSX mirrors the real element; Mantine/Tailwind are
// best-effort mappings of the computed layout (labelled "approx" so nobody treats them as ground truth).
function acMantineSpace(px) { const n = parseFloat(px) || 0; const scale = [["xs", 10], ["sm", 12], ["md", 16], ["lg", 20], ["xl", 32]]; let best = scale[0]; for (const s of scale) if (Math.abs(s[1] - n) < Math.abs(best[1] - n)) best = s; return best[0]; }
function acTwStep(px) { return Math.round((parseFloat(px) || 0) / 4); }
function acCodeJsx(c) {
  const tag = c.tagName || "div";
  const cls = (c.classList && c.classList.length) ? ' className="' + c.classList.join(" ") + '"' : "";
  const txt = c.text ? (c.text.length > 48 ? c.text.slice(0, 48) + "…" : c.text) : "";
  return [{ title: "JSX", code: txt ? "<" + tag + cls + ">" + txt + "</" + tag + ">" : "<" + tag + cls + " />" }];
}
function acCodeMantine(c) {
  const single = (v) => v && String(v).indexOf(" ") === -1 && v !== "0px";
  const p = single(c.paddingText) ? ' p="' + acMantineSpace(c.paddingText) + '"' : "";
  const g = c.gap ? ' gap="' + acMantineSpace(c.gap) + '"' : "";
  let comp = c.flow === "Horizontal" ? "Group" : c.flow === "Vertical" ? "Stack" : "Box";
  if (/Button/.test(c.component.name)) comp = "Button";
  return [{ title: "Mantine (approx)", code: "<" + comp + g + p + ">" + (c.text || "") + "</" + comp + ">" }];
}
function acCodeTailwind(c) {
  const cls = [];
  if (c.flow === "Horizontal") cls.push("flex");
  else if (c.flow === "Vertical") cls.push("flex", "flex-col");
  if (c.gap) cls.push("gap-" + acTwStep(c.gap));
  if (c.paddingText && c.paddingText.indexOf(" ") === -1 && c.paddingText !== "0px") cls.push("p-" + acTwStep(c.paddingText));
  const rad = { "2px": "rounded-sm", "4px": "rounded", "6px": "rounded-md", "8px": "rounded-lg" }[c.radius];
  if (rad) cls.push(rad);
  if (c.typo) { const fs = parseFloat(c.typo.size); cls.push(fs <= 12 ? "text-xs" : fs <= 14 ? "text-sm" : fs <= 16 ? "text-base" : fs <= 18 ? "text-lg" : "text-xl"); }
  return [{ title: "Tailwind (approx)", code: "<" + (c.tagName || "div") + ' className="' + cls.join(" ") + '">' }];
}
function acCodeForFormat(c, fmt) {
  if (fmt === "jsx") return acCodeJsx(c);
  if (fmt === "mantine") return acCodeMantine(c);
  if (fmt === "tailwind") return acCodeTailwind(c);
  return acCodeBlocks(c);
}

// ---- Interaction states. Scan same-origin stylesheets for :hover/:focus/:active/:disabled rules
// whose base selector matches the element, so we can both SHOW the state's styling and FORCE it on
// the live DOM (rewrite ":state" → ".dev-force-state" + add that class). Figma can't do this.
const AC_PSEUDO_STATES = ["hover", "focus-visible", "focus", "active", "disabled"];
const AC_STATE_LABEL = { hover: "Hover", "focus-visible": "Focus visible", focus: "Focus", active: "Active", disabled: "Disabled" };
function acStripPseudos(sel) { return sel.replace(/:(focus-visible|hover|focus|active|disabled)/g, "").trim(); }
function acStateRules(el) {
  const found = {};
  if (!el || !el.matches) return found;
  let sheets; try { sheets = Array.from(document.styleSheets); } catch (e) { return found; }
  for (const sheet of sheets) {
    let rules; try { rules = sheet.cssRules; } catch (e) { continue; } // cross-origin sheet — skip
    if (!rules) continue;
    for (const rule of Array.from(rules)) {
      if (!rule.selectorText || !rule.style) continue;
      const sel = rule.selectorText;
      for (const st of AC_PSEUDO_STATES) {
        if (sel.indexOf(":" + st) === -1) continue;
        const keep = sel.split(",").map((s) => s.trim()).filter((part) => {
          if (part.indexOf(":" + st) === -1) return false;
          if (st === "focus" && part.indexOf(":focus-visible") !== -1) return false;
          const base = acStripPseudos(part);
          try { return base && el.matches(base); } catch (e) { return false; }
        });
        if (!keep.length) continue;
        (found[st] = found[st] || []).push({ parts: keep, body: rule.style.cssText });
        break; // attribute each rule to the first (most specific) state it matches
      }
    }
  }
  return found;
}
function acForcedCss(found, activeStates) {
  const out = [];
  activeStates.forEach((st) => {
    const cls = ".dev-force-" + st;
    (found[st] || []).forEach(({ parts, body }) => {
      const sel = parts.map((p) => p
        .replace(/:focus-visible/g, cls).replace(/:hover/g, cls)
        .replace(/:focus/g, cls).replace(/:active/g, cls).replace(/:disabled/g, cls)
      ).join(", ");
      out.push(sel + "{" + body + "}");
    });
  });
  return out.join("\n");
}
function acStateDecls(found, st) {
  return (found[st] || []).map((r) => r.body).join(" ").split(";").map((d) => d.trim()).filter(Boolean).map((d) => d + ";").join("\n");
}

function DevInspector(props) {
  const c = props.data;
  const b = c.border, p = c.padding;
  const [mode, setMode] = React.useState("list");
  const [fmt, setFmt] = React.useState("css"); // copy-as format for the Code view
  // Free-floating position. null until the user drags, so it defaults to the CSS top-right anchor.
  const [pos, setPos] = React.useState(null);
  const drag = React.useRef(null);

  // Interaction states: the :hover/:focus/... rules matching this element, and which ones we're
  // forcing on. Forcing rewrites those rules to a marker class and adds it to the live element.
  const stateRules = React.useMemo(() => acStateRules(c.el), [c.el]);
  const stateKeys = Object.keys(stateRules);
  const [forced, setForced] = React.useState([]);
  React.useEffect(() => { setForced([]); }, [c.el]); // reset when the selected element changes
  React.useEffect(() => {
    const el = c.el;
    if (!el || !el.classList) return undefined;
    const styleEl = document.createElement("style");
    styleEl.setAttribute("data-dev-force", "");
    document.head.appendChild(styleEl);
    const classes = forced.map((s) => "dev-force-" + s);
    classes.forEach((cl) => el.classList.add(cl));
    styleEl.textContent = acForcedCss(stateRules, forced);
    return () => { classes.forEach((cl) => el.classList.remove(cl)); styleEl.remove(); };
  }, [forced, c.el, stateRules]);
  const toggleForce = (st) => setForced((f) => f.includes(st) ? f.filter((s) => s !== st) : f.concat(st));

  const onHeadPointerDown = (e) => {
    if (e.target.closest(".dev-inspector-close")) return; // let the close button work
    const panel = e.currentTarget.parentElement;
    const rect = panel.getBoundingClientRect();
    drag.current = { dx: e.clientX - rect.left, dy: e.clientY - rect.top, w: rect.width, h: rect.height };
    e.currentTarget.setPointerCapture(e.pointerId);
    e.preventDefault();
  };
  const onHeadPointerMove = (e) => {
    const d = drag.current;
    if (!d) return;
    const left = Math.max(0, Math.min(window.innerWidth - d.w, e.clientX - d.dx));
    const top = Math.max(0, Math.min(window.innerHeight - d.h, e.clientY - d.dy));
    setPos({ left, top });
  };
  const onHeadPointerUp = (e) => {
    drag.current = null;
    try { e.currentTarget.releasePointerCapture(e.pointerId); } catch (err) {}
  };

  // Once dragged, pin via left/top and release the right-edge anchor.
  const style = pos ? { left: pos.left + "px", top: pos.top + "px", right: "auto" } : null;

  return (
    <aside className="dev-inspector" style={style} onClick={(e) => e.stopPropagation()}>
      <div className="dev-inspector-head" onPointerDown={onHeadPointerDown} onPointerMove={onHeadPointerMove} onPointerUp={onHeadPointerUp}>
        <button className="dev-inspector-close" type="button" aria-label="Close" onClick={props.onClose}>×</button>
        <div className="dev-inspector-title">Layer properties</div>
        <div className="dev-inspector-name">{c.component.name}</div>
        <div className="dev-inspector-tag">{c.component.tag}</div>
      </div>
      <div className="dev-inspector-body">
        {/* Box model: border → padding → content, with per-edge px labels. */}
        <div className="dev-bm">
          <div className="dev-bm-layer dev-bm-border">
            <span className="dev-bm-tag">Border</span>
            <span className="dev-bm-edge dev-bm-t">{b.top}</span>
            <span className="dev-bm-edge dev-bm-r">{b.right}</span>
            <span className="dev-bm-edge dev-bm-b">{b.bottom}</span>
            <span className="dev-bm-edge dev-bm-l">{b.left}</span>
            <div className="dev-bm-layer dev-bm-padding">
              <span className="dev-bm-tag">Padding</span>
              <span className="dev-bm-edge dev-bm-t">{p.top}</span>
              <span className="dev-bm-edge dev-bm-r">{p.right}</span>
              <span className="dev-bm-edge dev-bm-b">{p.bottom}</span>
              <span className="dev-bm-edge dev-bm-l">{p.left}</span>
              <div className="dev-bm-content">{c.contentW} × {c.contentH}</div>
            </div>
          </div>
        </div>

        <div className="dev-modebar">
          <div className="dev-seg">
            <button type="button" className={"dev-seg-btn" + (mode === "list" ? " is-active" : "")} onClick={() => setMode("list")}>List</button>
            <button type="button" className={"dev-seg-btn" + (mode === "code" ? " is-active" : "")} onClick={() => setMode("code")}>Code</button>
          </div>
          {mode === "code" && (
            <select className="dev-fmt" value={fmt} onChange={(e) => setFmt(e.target.value)} aria-label="Code format">
              <option value="css">CSS</option>
              <option value="jsx">JSX</option>
              <option value="mantine">Mantine</option>
              <option value="tailwind">Tailwind</option>
            </select>
          )}
        </div>

        {mode === "code" ? (
          acCodeForFormat(c, fmt).map((blk) => (
            <div className="dev-section" key={blk.title}>
              <div className="dev-section-title">{blk.title}</div>
              <div className="dev-text-wrap">
                <pre className="dev-code">{blk.code}</pre>
                <DevCopyBtn value={blk.code} className="dev-copy-corner" />
              </div>
            </div>
          ))
        ) : (
        <React.Fragment>
        <div className="dev-section">
          <div className="dev-section-title">Layout</div>
          <DevRow label="Flow" value={c.flow} />
          <DevRow label="Width" value={c.widthLabel + " (" + c.width + "px)"} />
          <DevRow label="Height" value={c.heightLabel + " (" + c.height + "px)"} />
          <DevRow label="Radius" value={c.radius} />
          <DevRow label="Border" value={c.borderText} />
          {c.hasBorder && <DevRow label="Alignment" value="Inside" />}
          <DevRow label="Padding" value={c.paddingText} />
          {c.gap && <DevRow label="Gap" value={c.gap} />}
        </div>

        {c.typo && (
          <div className="dev-section">
            <div className="dev-section-title">Typography</div>
            <DevRow label="Font" value={c.typo.font} copy={c.typo.font} />
            <DevRow label="Weight" value={c.typo.weight} />
            <DevRow label="Style" value={c.typo.style} />
            <DevRow label="Size" value={c.typo.size} copy={c.typo.size} />
            <DevRow label="Line height" value={c.typo.lineHeight} copy={c.typo.lineHeight !== "Auto" ? c.typo.lineHeight : null} />
            <DevRow label="Letter spacing" value={c.typo.letterSpacing} />
            <DevRow label="Align" value={c.typo.align} />
            {c.typo.transform && <DevRow label="Transform" value={c.typo.transform} />}
          </div>
        )}

        <div className="dev-section">
          <div className="dev-section-title">Colors</div>
          <DevColorRow label="Background" info={c.bg} />
          <DevColorRow label="Border" info={c.borderColor} />
          <DevColorRow label="Text" info={c.color} />
        </div>
        </React.Fragment>
        )}

        {stateKeys.length > 0 && (
          <div className="dev-section">
            <div className="dev-section-title">Interaction states</div>
            {stateKeys.map((st) => {
              const on = forced.includes(st);
              return (
                <div className="dev-state" key={st}>
                  <button type="button" className={"dev-state-toggle" + (on ? " is-on" : "")}
                    aria-pressed={on} onClick={() => toggleForce(st)}>
                    <span className="dev-state-dot" />
                    <span className="dev-state-name">{AC_STATE_LABEL[st] || st}</span>
                    <span className="dev-state-action">{on ? "Forced — click to release" : "Force on element"}</span>
                  </button>
                  <pre className="dev-code dev-state-decls">{acStateDecls(stateRules, st)}</pre>
                </div>
              );
            })}
          </div>
        )}

        {c.text && (
          <div className="dev-section">
            <div className="dev-section-title">Text content</div>
            <div className="dev-text-wrap">
              <div className="dev-text-content">{c.text}</div>
              <DevCopyBtn value={c.text} className="dev-copy-corner" />
            </div>
          </div>
        )}

        {c.assets && c.assets.length > 0 && (
          <div className="dev-section">
            <div className="dev-section-title">Assets</div>
            <div className="dev-asset-list">
              {c.assets.map((a, i) => <DevAssetItem key={i} asset={a} />)}
            </div>
          </div>
        )}
      </div>
    </aside>
  );
}

function AppComments(props) {
  const C = window.DCComments;
  const scope = props.scope || "app";
  const currentUser = props.currentUser || AC_FALLBACK_USER;
  const commentUsers = props.commentUsers;
  // When true (Cmd+/ pressed), hide the HUD pill and idle pins for a clean full-bleed view — but
  // keep the overlay mounted so Cmd+C still toggles comment mode and lets you drop a comment.
  const chromeHidden = !!props.chromeHidden;

  const resolvedUsers = React.useMemo(() => (
    Array.isArray(commentUsers) && commentUsers.length > 0 ? commentUsers : [currentUser]
  ), [commentUsers, currentUser]);
  const usersById = React.useMemo(
    () => Object.fromEntries(resolvedUsers.map((user) => [user.id, user])),
    [resolvedUsers]
  );
  const activeUser = (currentUser && usersById[currentUser.id]) ? usersById[currentUser.id] : (currentUser || resolvedUsers[0] || AC_FALLBACK_USER);

  const [comments, setComments] = React.useState(() => acReadLocal());
  const [commentMode, setCommentMode] = React.useState(false);
  const [commentsVisible, setCommentsVisible] = React.useState(true);
  // Dev mode — click any element to inspect its layout/colors (Figma-style) in a right panel.
  const [devMode, setDevMode] = React.useState(false);
  const [inspect, setInspect] = React.useState(null); // acInspect() result for the selected element
  const [hoverRect, setHoverRect] = React.useState(null); // live hover highlight rect
  // Hold ⌥/Alt to temporarily pass clicks through to the real app (open a picker/menu, type) so you
  // can drive the UI into the state you want to inspect; release to resume inspecting.
  const [devInteract, setDevInteract] = React.useState(false);
  // First-run explainer for the ⌥ interact gesture — shown the first 5 times dev mode is entered.
  const [devIntro, setDevIntro] = React.useState(false);
  const [activeThreadId, setActiveThreadId] = React.useState(null);
  const [draftThread, setDraftThread] = React.useState(null);
  const rootRef = React.useRef(null);
  const pendingPersist = React.useRef(0);
  const lastSyncedJson = React.useRef(JSON.stringify(acReadLocal()));
  const ready = React.useRef(false);

  // Initial load from the shared store (source of truth); falls back to the localStorage seed.
  React.useEffect(() => {
    let alive = true;
    acReadShared().then((remote) => {
      if (!alive || !remote) { ready.current = true; return; }
      lastSyncedJson.current = JSON.stringify(remote);
      setComments(remote);
      ready.current = true;
    });
    return () => { alive = false; };
  }, []);

  // Debounced persist on change. Mirrors the canvas pendingPersist guard so a poll can't clobber
  // an in-flight write.
  React.useEffect(() => {
    if (!ready.current) return undefined;
    const json = JSON.stringify(comments);
    if (json === lastSyncedJson.current) return undefined;
    acWriteLocal(comments);
    const timer = window.setTimeout(() => {
      pendingPersist.current += 1;
      acWriteShared(comments).then(() => {
        lastSyncedJson.current = json;
      }).finally(() => {
        pendingPersist.current = Math.max(0, pendingPersist.current - 1);
      });
    }, 350);
    return () => window.clearTimeout(timer);
  }, [comments]);

  // Poll for remote changes (multi-user). Skip while a local write is pending.
  React.useEffect(() => {
    const id = window.setInterval(() => {
      if (pendingPersist.current > 0) return;
      acReadShared().then((remote) => {
        if (!remote) return;
        const json = JSON.stringify(remote);
        if (json === lastSyncedJson.current) return;
        lastSyncedJson.current = json;
        setComments(remote);
      });
    }, AC_POLL_INTERVAL_MS);
    return () => window.clearInterval(id);
  }, []);

  // Close any open/draft thread when the screen scope changes — they belong to the screen they
  // were opened on. Comment mode itself stays on so you can keep commenting on the new screen.
  React.useEffect(() => {
    setActiveThreadId(null);
    setDraftThread(null);
  }, [scope]);

  // Cmd/Ctrl+C toggles comment mode; Cmd/Ctrl+Shift+D toggles dev mode; Escape exits.
  React.useEffect(() => {
    const onKey = (e) => {
      if (e.key === "Escape") {
        if (draftThread) { setDraftThread(null); return; }
        if (activeThreadId) { setActiveThreadId(null); return; }
        if (commentMode) { setCommentMode(false); return; }
        if (devMode) { setDevMode(false); setInspect(null); setHoverRect(null); return; }
        return;
      }
      const mod = (e.metaKey || e.ctrlKey) && !e.altKey;
      // Cmd/Ctrl+Shift+D → dev mode (Shift avoids the browser ⌘D bookmark clash).
      if (mod && e.shiftKey && (e.key === "d" || e.key === "D")) {
        e.preventDefault();
        setCommentMode(false); setActiveThreadId(null); setDraftThread(null);
        setDevMode((open) => { const next = !open; if (!next) { setInspect(null); setHoverRect(null); } return next; });
        return;
      }
      // Cmd/Ctrl+C → comment mode.
      const isC = (e.key === "c" || e.key === "C");
      if (!isC || !(e.metaKey || e.ctrlKey) || e.shiftKey || e.altKey) return;
      if (acShouldIgnoreShortcut()) return;
      e.preventDefault();
      setDevMode(false); setInspect(null); setHoverRect(null);
      setActiveThreadId(null);
      setDraftThread(null);
      setCommentMode((open) => !open);
    };
    window.addEventListener("keydown", onKey);
    return () => window.removeEventListener("keydown", onKey);
  }, [commentMode, activeThreadId, draftThread, devMode]);

  // Find the real app element under a point, skipping our own overlay (capture/highlight/HUD/panel).
  const devTargetAt = (x, y) => {
    const els = document.elementsFromPoint(x, y);
    for (const el of els) {
      if (el.closest && el.closest(".app-comments-root, .dev-inspector")) continue;
      return el;
    }
    return null;
  };
  const onDevMove = (e) => {
    const el = devTargetAt(e.clientX, e.clientY);
    if (!el) { setHoverRect(null); return; }
    const r = el.getBoundingClientRect();
    setHoverRect({ left: r.left, top: r.top, width: r.width, height: r.height, regions: acPadGapRegions(el) });
  };
  const onDevClick = (e) => {
    const el = devTargetAt(e.clientX, e.clientY);
    if (el) setInspect(acInspect(el));
  };
  // Padding/gap bands for the selected element — shown when the cursor isn't over another element.
  const selPadGap = React.useMemo(() => (inspect ? acPadGapRegions(inspect.el) : null), [inspect]);
  // While in dev mode, holding ⌥/Alt drops the capture overlay so the app is interactive again.
  React.useEffect(() => {
    if (!devMode) { setDevInteract(false); return undefined; }
    const down = (e) => { if (e.key === "Alt") { setDevInteract(true); setHoverRect(null); } };
    const up = (e) => { if (e.key === "Alt") setDevInteract(false); };
    const blur = () => setDevInteract(false);
    window.addEventListener("keydown", down);
    window.addEventListener("keyup", up);
    window.addEventListener("blur", blur);
    return () => { window.removeEventListener("keydown", down); window.removeEventListener("keyup", up); window.removeEventListener("blur", blur); };
  }, [devMode]);
  // On each entry into dev mode, show the intro for the first 5 entries (count persisted locally).
  React.useEffect(() => {
    if (!devMode) { setDevIntro(false); return; }
    const n = acReadDevIntro();
    if (n < 5) { acWriteDevIntro(n + 1); setDevIntro(true); }
  }, [devMode]);
  const toggleDevMode = () => {
    setCommentMode(false); setActiveThreadId(null); setDraftThread(null);
    setDevMode((open) => { const next = !open; if (!next) { setInspect(null); setHoverRect(null); } return next; });
  };

  const placeDraft = (e) => {
    const root = rootRef.current;
    const rect = root ? root.getBoundingClientRect() : { left: 0, top: 0 };
    const x = e.clientX - rect.left;
    const y = e.clientY - rect.top;
    setDraftThread({ id: C.makeId("draft"), scope, x, y });
    setCommentMode(false);
  };

  const createThread = (body) => {
    const trimmed = body.trim();
    if (!trimmed || !draftThread) return;
    const now = new Date().toISOString();
    const next = {
      id: C.makeId("thread"),
      scope: draftThread.scope,
      x: draftThread.x,
      y: draftThread.y,
      createdAt: now,
      updatedAt: now,
      resolvedAt: null,
      unreadBy: resolvedUsers.filter((u) => u.id !== activeUser.id).map((u) => u.id),
      messages: [{ id: C.makeId("message"), userId: activeUser.id, body: trimmed, createdAt: now }],
    };
    setComments((list) => [...list, next]);
    setDraftThread(null);
    setActiveThreadId(next.id);
  };

  const replyToThread = (threadId, body) => {
    const trimmed = body.trim();
    if (!trimmed) return;
    const now = new Date().toISOString();
    const nextUnread = resolvedUsers.filter((u) => u.id !== activeUser.id).map((u) => u.id);
    setComments((list) => list.map((t) => {
      if (t.id !== threadId) return t;
      return {
        ...t,
        updatedAt: now,
        unreadBy: Array.from(new Set([...(t.unreadBy || []), ...nextUnread])),
        messages: [...(t.messages || []), { id: C.makeId("message"), userId: activeUser.id, body: trimmed, createdAt: now }],
      };
    }));
  };

  const resolveThread = (threadId, resolved) => {
    const now = new Date().toISOString();
    setComments((list) => list.map((t) => {
      if (t.id !== threadId) return t;
      if (!resolved) {
        const copy = { ...t, updatedAt: now };
        delete copy.resolvedAt;
        delete copy.resolvedBy;
        return copy;
      }
      return { ...t, updatedAt: now, resolvedAt: now, resolvedBy: activeUser.id };
    }));
    if (resolved) setActiveThreadId((open) => (open === threadId ? null : open));
  };

  const deleteThread = (threadId) => {
    setComments((list) => list.filter((t) => t.id !== threadId));
    setActiveThreadId((open) => (open === threadId ? null : open));
  };

  const toggleReaction = (threadId, messageId, emoji) => {
    const mark = emoji || "❤️";
    setComments((list) => list.map((t) => {
      if (t.id !== threadId) return t;
      return {
        ...t,
        messages: C.getThreadMessages(t).map((m) => {
          if (m.id !== messageId) return m;
          const reactions = { ...(m.reactions || {}) };
          const users = Array.isArray(reactions[mark]) ? reactions[mark] : [];
          reactions[mark] = users.includes(activeUser.id) ? users.filter((id) => id !== activeUser.id) : [...users, activeUser.id];
          if (reactions[mark].length === 0) delete reactions[mark];
          return { ...m, reactions };
        }),
      };
    }));
  };

  const deleteMessage = (threadId, messageId) => {
    setComments((list) => {
      const thread = list.find((t) => t.id === threadId);
      const messages = C.getThreadMessages(thread);
      if (!thread || messages[0]?.id === messageId || messages.length <= 1) {
        return list.filter((t) => t.id !== threadId);
      }
      return list.map((t) => (t.id === threadId ? { ...t, messages: C.getThreadMessages(t).filter((m) => m.id !== messageId) } : t));
    });
    setActiveThreadId((open) => (open === threadId ? null : open));
  };

  const openThread = (threadId) => {
    setDraftThread(null);
    setActiveThreadId((open) => (open === threadId ? null : threadId));
    setComments((list) => list.map((t) => (t.id === threadId ? { ...t, unreadBy: (t.unreadBy || []).filter((id) => id !== activeUser.id) } : t)));
  };

  const toggleCommentMode = () => {
    setActiveThreadId(null);
    setDraftThread(null);
    setDevMode(false); setInspect(null); setHoverRect(null);
    setCommentMode((open) => !open);
  };

  // Only this screen's comments. Ordered oldest-first so thread numbers are stable.
  const scopedComments = React.useMemo(() => (
    comments.filter((t) => t.scope === scope).slice().sort((a, b) => new Date(a.createdAt || 0) - new Date(b.createdAt || 0))
  ), [comments, scope]);
  const threadNumbers = React.useMemo(
    () => new Map(scopedComments.map((t, i) => [t.id, i + 1])),
    [scopedComments]
  );
  const activeThread = scopedComments.find((t) => t.id === activeThreadId) || null;
  const openCount = scopedComments.filter((t) => !t.resolvedAt).length;
  // In the clean (chrome-hidden) view, idle pins stay hidden — they only surface once you're
  // actively commenting (Cmd+C) or have a thread open, so you can still see what you're doing.
  const shouldShowPins = chromeHidden
    ? (commentMode || !!activeThreadId || !!draftThread)
    : (commentsVisible || commentMode || !!activeThreadId);
  const visiblePins = scopedComments.filter((t) => !t.resolvedAt || t.id === activeThreadId);

  if (!C) return null;

  return (
    <div className={"app-comments-root" + (devMode ? " is-dev" : "")} ref={rootRef}>
      {!chromeHidden && (
        <C.Hud
          currentUser={activeUser}
          commentMode={commentMode}
          commentsVisible={commentsVisible}
          commentCount={openCount}
          onToggleCommentMode={toggleCommentMode}
          onToggleCommentsVisible={() => setCommentsVisible((v) => !v)}
          devActive={devMode}
          onToggleDevMode={toggleDevMode}
        />
      )}

      {commentMode && (
        <div
          className="app-comments-capture"
          onClick={placeDraft}
        />
      )}

      {/* Dev mode: a capture layer that blocks app clicks, live hover highlight + size pill, a
          stronger ring on the selected element, Figma-style red distance guides from the selection
          to the hovered element, and the right-side inspector panel. Stays on so you can inspect
          many elements in a row. */}
      {devMode && (
        <div className={"app-dev-capture" + (devInteract ? " is-interact" : "")}
          onMouseMove={onDevMove} onMouseLeave={() => setHoverRect(null)} onClick={onDevClick} />
      )}
      {devMode && hoverRect && (
        <div className="dev-highlight" style={{ left: hoverRect.left, top: hoverRect.top, width: hoverRect.width, height: hoverRect.height }} />
      )}
      {devMode && inspect && (
        <div className="dev-highlight dev-highlight-selected" style={{ left: inspect.rect.left, top: inspect.rect.top, width: inspect.rect.width, height: inspect.rect.height }} />
      )}
      {/* Hatched padding + flex-gap bands. Follows the hovered element; falls back to the selection. */}
      {devMode && (() => {
        const reg = hoverRect ? hoverRect.regions : selPadGap;
        if (!reg) return null;
        return reg.pads.concat(reg.gaps).map((b, i) => (
          b.width > 0 && b.height > 0 ? (
            <React.Fragment key={"reg" + i}>
              <div className="dev-region" style={{ left: b.left, top: b.top, width: b.width, height: b.height }} />
              <div className="dev-region-label" style={{ left: b.left + b.width / 2, top: b.top + b.height / 2 }}>{b.label}</div>
            </React.Fragment>
          ) : null
        ));
      })()}
      {/* Size pill on the hovered element (Figma shows W×H under the highlight). */}
      {devMode && hoverRect && (
        <div className="dev-dim-badge" style={{ left: hoverRect.left + hoverRect.width / 2, top: hoverRect.top + hoverRect.height + 4 }}>
          {Math.round(hoverRect.width)} × {Math.round(hoverRect.height)}
        </div>
      )}
      {/* Distance guides: with an element selected, hovering another draws red spacing/inset lines. */}
      {devMode && inspect && hoverRect &&
        !(Math.abs(hoverRect.left - inspect.rect.left) < 1 && Math.abs(hoverRect.top - inspect.rect.top) < 1 &&
          Math.abs(hoverRect.width - inspect.rect.width) < 1 && Math.abs(hoverRect.height - inspect.rect.height) < 1) &&
        acMeasure(inspect.rect, hoverRect).map((s, i) => (
          s.type === "h" ? (
            <React.Fragment key={i}>
              <div className="dev-measure-line h" style={{ left: s.x1, top: s.y, width: s.x2 - s.x1 }} />
              <div className="dev-measure-label" style={{ left: (s.x1 + s.x2) / 2, top: s.y }}>{s.label}</div>
            </React.Fragment>
          ) : (
            <React.Fragment key={i}>
              <div className="dev-measure-line v" style={{ left: s.x, top: s.y1, height: s.y2 - s.y1 }} />
              <div className="dev-measure-label" style={{ left: s.x, top: (s.y1 + s.y2) / 2 }}>{s.label}</div>
            </React.Fragment>
          )
        ))}
      {devMode && !chromeHidden && devIntro && (
        <div className="dev-intro" onClick={(e) => e.stopPropagation()}>
          <div className="dev-intro-title">Inspecting interactive UI</div>
          <p className="dev-intro-body">
            Click any element to inspect it. To open a menu, dropdown, or picker, hold{" "}
            <span className="dev-intro-kbd">⌥ Option</span> to interact with the page — then release{" "}
            <span className="dev-intro-kbd">⌥</span> and the inspector freezes whatever opened so you can examine it.
          </p>
          <div className="dev-intro-foot">
            <span className="dev-intro-note">Shown the first few times you open dev mode</span>
            <button type="button" className="dev-intro-btn" onClick={() => setDevIntro(false)}>Got it</button>
          </div>
        </div>
      )}
      {devMode && !chromeHidden && devInteract && (
        <div className="app-comments-empty app-dev-hint app-dev-interact">Interacting with the page · release ⌥ to inspect</div>
      )}
      {devMode && !chromeHidden && !devInteract && !inspect && (
        <div className="app-comments-empty app-dev-hint">Click to inspect · hold ⌥ to interact · Esc to exit</div>
      )}
      {devMode && !chromeHidden && inspect && (
        <DevInspector data={inspect} onClose={() => setInspect(null)} />
      )}

      {/* In the clean (Cmd+/) view, comment mode is intentionally minimal: just the crosshair
          capture layer and the point-of-contact UI (draft composer / pins). The hint pill and the
          right sidebar only appear when the chrome is visible. */}
      {commentMode && !chromeHidden && !draftThread && (
        <div className="app-comments-empty">Click anywhere to leave a comment · Esc to cancel</div>
      )}

      {commentMode && !chromeHidden && (
        <C.Sidebar
          comments={scopedComments}
          activeThreadId={activeThreadId}
          currentUser={activeUser}
          usersById={usersById}
          onOpenThread={openThread}
          onResolveThread={resolveThread}
          onDeleteThread={deleteThread}
        />
      )}

      <div className="app-comments-layer dc-comments-layer">
        {shouldShowPins && visiblePins.map((thread) => {
          const owner = C.getThreadOwner(usersById, thread);
          return (
            <div key={thread.id} className="dc-comment-pin" style={{ left: thread.x, top: thread.y }}>
              <button
                type="button"
                data-open={activeThreadId === thread.id ? "true" : "false"}
                data-muted={thread.resolvedAt ? "true" : "false"}
                style={{ background: C.getUserAvatar(owner) }}
                onPointerDown={(e) => e.stopPropagation()}
                onClick={(e) => { e.stopPropagation(); openThread(thread.id); }}
                title={`${owner.name} left a comment here`}
              >
                {owner.initials || "?"}
              </button>
              <div className="dc-comment-pin-preview">
                <strong>{owner.name} <span className="dc-comment-message-time">{C.formatRelativeTime(thread.updatedAt || thread.createdAt)}</span></strong>
                <div>{C.getThreadPreview(thread)}</div>
              </div>
            </div>
          );
        })}

        {draftThread && (
          <C.DraftComposer
            draftThread={draftThread}
            currentUser={activeUser}
            onCancel={() => setDraftThread(null)}
            onSubmit={createThread}
          />
        )}

        {activeThread && !draftThread && (
          <C.Thread
            key={activeThread.id}
            thread={activeThread}
            index={threadNumbers.get(activeThread.id) || 1}
            currentUser={activeUser}
            usersById={usersById}
            onClose={() => setActiveThreadId(null)}
            onResolve={(resolved) => resolveThread(activeThread.id, resolved)}
            onToggleReaction={(messageId, emoji) => toggleReaction(activeThread.id, messageId, emoji)}
            onDeleteMessage={(messageId) => deleteMessage(activeThread.id, messageId)}
            onSubmit={(body) => replyToThread(activeThread.id, body)}
          />
        )}
      </div>
    </div>
  );
}

Object.assign(window, { AppComments });
