/* adom-layout-viewer interactive viewer core — shared by the live app (app.html) and the
 * self-contained embed (embed.html). Consumes window.__PCB__ = { svg, meta }
 * (embed) OR fetches /state (live). The whole reason this works: the SVG is
 * self-rendered with data-net / data-ref / data-pad, so net/trace highlighting
 * and component hover are pure DOM queries. */
(function () {
  "use strict";
  const $ = (s, r) => (r || document).querySelector(s);
  const state = { meta: null, active: null, pinned: null, scale: 1, enrichCache: {}, side: "top", raised: null };

  // ── mount ──────────────────────────────────────────────────────────────
  function mount(svgText, meta) {
    state.meta = meta || { net_names: {}, components: [] };
    const box = $("#svgbox");
    box.innerHTML = svgText;
    const svg = box.querySelector("svg");
    if (!svg) return;
    svg.removeAttribute("width");
    svg.removeAttribute("height");
    svg.style.width = "100%";
    svg.style.height = "100%";
    state.svg = svg;
    state.vb = (svg.getAttribute("viewBox") || "0 0 10 10").split(/\s+/).map(Number);
    state.vb0 = state.vb.slice();
    fitOverlayToBase(svg);
    wireHover(svg);
    wirePanZoom(svg);
    buildLayerHUD();
    buildNetList();
    setStat();
    updateChrome();
  }
  // Show the side panel (Layers/Nets) + stat only when there's room — i.e. the
  // standalone tab or a fullscreened pane. In a small embedded thumbnail (APM
  // board view) the chrome is hidden so it doesn't clutter the little preview.
  function updateChrome() {
    const big = innerWidth > 620;
    // Header only when standalone/large; inside APM's iframe (embed) APM already
    // wears the canonical header, so we'd double it. Panels still show when big.
    document.body.classList.toggle("chrome", big && !window.__EMBED__);
    ["#side", "#stat"].forEach((s) => { const e = $(s); if (e) e.style.display = big ? "" : "none"; });
  }
  addEventListener("resize", updateChrome);

  // ── observability: mirror console + errors to the server ring buffer (GET
  // /console). Live-server only (an embed has no server). ──
  function post(path, body) { try { fetch(path, { method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify(body) }); } catch (e) {} }
  function wireObservability() {
    if (window.__EMBED__) return;
    ["log", "warn", "error"].forEach((k) => { const orig = console[k]; console[k] = function () { try { post("console", { level: k, msg: [].slice.call(arguments).map(String).join(" ") }); } catch (e) {} return orig.apply(console, arguments); }; });
    addEventListener("error", (e) => post("console", { level: "error", msg: (e.message || "") + " @" + (e.filename || "") + ":" + (e.lineno || 0) }));
    addEventListener("unhandledrejection", (e) => post("console", { level: "error", msg: "unhandledrejection: " + (e.reason && e.reason.message || e.reason) }));
  }

  // ── toasts: every action answers back + the AI's live narration channel ──
  const TOAST_ICONS = {
    success: '<svg class="ti" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.2" stroke-linecap="round" stroke-linejoin="round"><path d="M20 6 9 17l-5-5"/></svg>',
    error: '<svg class="ti" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.2" stroke-linecap="round"><path d="M18 6 6 18M6 6l12 12"/></svg>',
    info: '<svg class="ti" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.2" stroke-linecap="round"><path d="M12 8h.01M11 12h1v4h1"/><circle cx="12" cy="12" r="9"/></svg>',
  };
  function toast(msg, type) {
    type = type || "info";
    const box = $("#toasts"); if (!box || !msg) return;
    const el = document.createElement("div"); el.className = "toast " + type;
    el.innerHTML = (TOAST_ICONS[type] || TOAST_ICONS.info) + '<span class="tmsg"></span>';
    el.querySelector(".tmsg").textContent = msg;
    while (box.children.length >= 4) box.removeChild(box.firstChild);
    box.appendChild(el);
    const kill = () => { el.classList.add("out"); setTimeout(() => el.remove(), 200); };
    el.onclick = kill;
    if (type !== "error") setTimeout(kill, 4000);
  }
  // Drain AI-pushed toasts + drive-commands (POST /ui/toast, /ui/cmd). Live only.
  function pollUi() {
    if (window.__EMBED__) return;
    fetch("ui/pull").then((r) => r.json()).then((q) => {
      (q.toasts || []).forEach((t) => toast(t.message, t.type));
      (q.cmds || []).forEach(runCmd);
    }).catch(() => {});
    setTimeout(pollUi, 1000);
  }
  function runCmd(c) {
    if (!c || !c.action) return;
    if (c.action === "fit") { fit(); toast("Fit to view", "info"); }
    else if (c.action === "net" && c.arg) { highlightNet(c.arg); toast("Net " + c.arg, "info"); }
    else if (c.action === "clear") { clearNet && clearNet(); toast("Cleared", "info"); }
    else if (c.action === "toast") { toast(c.arg || "", c.type || "info"); }
  }

  // ── tooltip: body-appended fixed div, 600ms reveal, clamped to viewport ──
  let ttEl = null, ttTimer = null;
  function showTip(target, e) {
    const msg = target.getAttribute("data-tip"); if (!msg) return;
    clearTimeout(ttTimer);
    const cx = e.clientX, cy = e.clientY;
    ttTimer = setTimeout(() => {
      if (!ttEl) { ttEl = document.createElement("div"); ttEl.id = "tt"; document.body.appendChild(ttEl); }
      ttEl.textContent = msg; ttEl.style.left = "-9999px"; ttEl.style.top = "0px"; ttEl.classList.add("show");
      const M = 8, w = ttEl.offsetWidth, h = ttEl.offsetHeight;
      let x = cx + 14, y = cy + 16;
      if (x + w > innerWidth - M) x = cx - w - 14;
      if (y + h > innerHeight - M) y = cy - h - 14;
      x = Math.max(M, Math.min(x, innerWidth - w - M));
      y = Math.max(M, Math.min(y, innerHeight - h - M));
      ttEl.style.left = x + "px"; ttEl.style.top = y + "px";
    }, 600);
  }
  function hideTip() { clearTimeout(ttTimer); if (ttEl) ttEl.classList.remove("show"); }
  document.addEventListener("mouseover", (e) => { const t = e.target.closest && e.target.closest("[data-tip]"); if (t) showTip(t, e); });
  document.addEventListener("mouseout", (e) => { if (e.target.closest && e.target.closest("[data-tip]")) hideTip(); });
  document.addEventListener("mousedown", hideTip);

  // ── header subject ──
  function setSubject(name, meta) {
    const n = $("#hdr-name"), m = $("#hdr-meta");
    if (n) n.textContent = name || "";
    if (m) m.textContent = meta || "";
  }
  function fitToast() { fit(); toast("Fit to view", "info"); }

  // ── net + component highlighting ─────────────────────────────────────────
  function highlightNet(net) {
    if (net == null || net === "0" || net === "") return clearNet();
    state.active = net;
    state.svg.classList.add("dim");
    state.svg.querySelectorAll(".adom-overlay [data-net]").forEach((el) => {
      el.classList.toggle("hl", el.getAttribute("data-net") === String(net));
    });
    const name = state.meta.net_names[net] || "(unnamed)";
    const cnt = state.svg.querySelectorAll('.adom-overlay [data-net="' + net + '"]').length;
    chip('<b>net</b> ' + escapeHtml(name) + ' <span class="muted">· ' + cnt + " items</span>");
    document.querySelectorAll("#netlist .netrow").forEach((r) => r.classList.toggle("sel", r.getAttribute("data-net") === String(net)));
  }
  function clearNet() {
    if (state.pinned != null) return; // keep pinned net lit
    state.active = null;
    state.svg.classList.remove("dim");
    state.svg.querySelectorAll(".hl").forEach((el) => el.classList.remove("hl"));
    document.querySelectorAll("#netlist .netrow.sel").forEach((r) => r.classList.remove("sel"));
    chip("");
  }
  // gentle single-pad highlight (hover)
  function padHl(pad) { clearPadHl(); pad.classList.add("pad-hl"); }
  function clearPadHl() { state.svg.querySelectorAll(".pad-hl").forEach((el) => el.classList.remove("pad-hl")); }

  // ── CROSS-PROBE: click a part here → highlight the SAME reference in the sibling
  // schematic view (and vice-versa), relayed by the host (APM) between the two
  // same-origin iframes. Keyed by the component reference (both views tag by it). ──
  function xpost(ref) { try { if (window.parent && window.parent !== window) window.parent.postMessage({ adomXprobe: true, ref: ref }, "*"); } catch (e) {} }
  function clearComp() { if (state.svg) state.svg.querySelectorAll(".comp-xhl").forEach((el) => el.classList.remove("comp-xhl")); }
  function highlightComp(ref) {
    clearComp();
    if (!state.svg || !ref) return;
    state.svg.querySelectorAll('[data-ref="' + cssEsc(ref) + '"]').forEach((el) => el.classList.add("comp-xhl"));
  }
  // union bbox of the component's pads (svg user units = board mm), for recenter
  function compBBox(ref) {
    let mnx = 1e9, mny = 1e9, mxx = -1e9, mxy = -1e9, found = false;
    state.svg.querySelectorAll('[data-ref="' + cssEsc(ref) + '"]').forEach((el) => {
      try { const b = el.getBBox(); if (b.width || b.height) { mnx = Math.min(mnx, b.x); mny = Math.min(mny, b.y); mxx = Math.max(mxx, b.x + b.width); mxy = Math.max(mxy, b.y + b.height); found = true; } } catch (e) {}
    });
    return found ? [mnx, mny, mxx, mxy] : null;
  }
  function centerOn(ref) { const bb = compBBox(ref); if (bb) frameBBox(bb); }
  // Zoom-to-fit a bbox: frame the part with margin (matching the viewport aspect),
  // keep some context for tiny parts, never zoom out past the whole board.
  function frameBBox(bb) {
    if (!bb || !state.svg) return;
    const x0 = bb[0], y0 = bb[1], x1 = bb[2], y1 = bb[3], cx = (x0 + x1) / 2, cy = (y0 + y1) / 2;
    const rc = state.svg.getBoundingClientRect();
    const aspect = (rc.width && rc.height) ? rc.width / rc.height : ((state.vb0[2] / state.vb0[3]) || 1);
    let span = Math.max(x1 - x0, y1 - y0, 0.3) * 3.2; // the part fills ~1/3 of the short side
    span = Math.max(span, 8);                          // don't over-zoom a tiny footprint
    let w = aspect >= 1 ? span * aspect : span;
    let h = aspect >= 1 ? span : span / aspect;
    if (w > state.vb0[2] && h > state.vb0[3]) { w = state.vb0[2]; h = state.vb0[3]; }
    state.vb = [cx - w / 2, cy - h / 2, w, h];
    state.svg.setAttribute("viewBox", state.vb.join(" "));
  }
  function xclear() { try { if (window.parent && window.parent !== window) window.parent.postMessage({ adomXprobe: true, clear: true }, "*"); } catch (e) {} }
  // Apply a cross-probe FROM the schematic view: HIGHLIGHT ONLY + recenter (no info
  // card — the card shows only when the user clicks a part IN this app). No re-emit.
  function applyXprobe(ref) { if (!ref) return; highlightComp(ref); centerOn(ref); }
  // deselect propagated from the sibling view → clear our cross-probe highlight
  function applyXclear() { clearComp(); }
  addEventListener("message", (e) => { const d = e.data; if (!d || !d.adomXprobe) return; if (d.clear) applyXclear(); else if (d.ref) applyXprobe(d.ref); });

  function wireHover(svg) {
    // HOVER: pads + vias. Highlight the element + show its info. No net/fill lighting.
    svg.addEventListener("mousemove", (e) => {
      const pad = e.target.closest(".adom-pad");
      if (pad) { padHl(pad); showPadCard(pad, e); return; }
      const via = e.target.closest(".adom-via");
      if (via) { padHl(via); showViaCard(via, e); return; }
      clearPadHl(); hideCard();
    });
    svg.addEventListener("mouseleave", () => { clearPadHl(); hideCard(); });
    // CLICK: light up the whole net (the "fill") + show fill info. Toggle off on
    // empty click or clicking the same net again.
    svg.addEventListener("click", (e) => {
      const t = e.target.closest("[data-net]");
      // click empty space = deselect here AND in the sibling schematic view
      if (!t) { state.pinned = null; clearNet(); clearComp(); xclear(); return; }
      const net = t.getAttribute("data-net");
      state.pinned = state.pinned === net ? null : net;
      if (state.pinned) highlightNet(net); else clearNet();
      // cross-probe: clicking a part (pad/footprint) selects it in the schematic too
      const ref = t.getAttribute("data-ref");
      if (ref) { highlightComp(ref); xpost(ref); }
    });
  }

  // ── pad info card (with async enrichment for the parent part) ─────────────
  function showPadCard(pad, e) {
    const ref = pad.getAttribute("data-ref");
    const num = pad.getAttribute("data-pad");
    const net = pad.getAttribute("data-net");
    const c = (state.meta.components || []).find((k) => k.reference === ref) || {};
    const card = $("#card");
    const mpn = c.mpn || "";
    const netName = (state.meta.net_names || {})[net] || (net === "0" ? "(no net)" : "net " + net);
    const rows = [];
    rows.push('<div class="ci-h"><span class="ci-ref">' + escapeHtml(ref || "?") + ' · pad ' + escapeHtml(num || "?") +
      '</span><span class="ci-val">' + escapeHtml(c.value || "") + "</span></div>");
    rows.push(row("net", netName));
    rows.push(row("pad", (pad.getAttribute("data-size") || "") + " · " + (pad.getAttribute("data-shape") || "") + " · " + (pad.getAttribute("data-side") === "B" ? "bottom" : "top")));
    if (mpn) rows.push(row("mpn", mpn));
    if (c.lcsc) rows.push(row("lcsc", c.lcsc));
    rows.push('<div class="ci-enrich" id="ci-enrich">' + (mpn ? '<span class="muted">loading price · stock · wiki…</span>' : "") + "</div>");
    card.innerHTML = rows.join("");
    card.style.display = "block";
    placeCard(card, e);
    if (mpn) enrich(mpn, c.lcsc);
  }
  function showViaCard(via, e) {
    const net = via.getAttribute("data-net");
    const netName = (state.meta.net_names || {})[net] || (net === "0" ? "(no net)" : "net " + net);
    const card = $("#card");
    card.innerHTML = '<div class="ci-h"><span class="ci-ref">via</span></div>' + row("net", netName);
    card.style.display = "block";
    placeCard(card, e);
  }
  function row(k, v) { return '<div class="ci-row"><span class="ci-k">' + k + '</span><span class="ci-v">' + escapeHtml(String(v)) + "</span></div>"; }
  function hideCard() { const c = $("#card"); if (c) c.style.display = "none"; }
  function placeCard(card, e) {
    const pad = 14, w = card.offsetWidth, h = card.offsetHeight;
    let x = e.clientX + pad, y = e.clientY + pad;
    if (x + w > innerWidth) x = e.clientX - w - pad;
    if (y + h > innerHeight) y = e.clientY - h - pad;
    card.style.left = Math.max(4, x) + "px";
    card.style.top = Math.max(4, y) + "px";
  }
  function enrich(mpn, lcsc) {
    const key = mpn + "|" + (lcsc || "");
    const render = (d) => {
      const el = $("#ci-enrich");
      if (!el || state.enrichKey !== key) return;
      if (!d || d.error) { el.innerHTML = '<span class="muted">enrichment unavailable</span>'; return; }
      const parts = [];
      if (d.stock != null) parts.push('<span class="pill ' + (d.stock > 0 ? "ok" : "no") + '">' + (d.stock > 0 ? d.stock + " in stock" : "no stock") + "</span>");
      if (d.price) parts.push('<span class="pill">' + escapeHtml(d.price) + "</span>");
      if (d.tier) parts.push('<span class="pill tier">' + escapeHtml(d.tier) + "</span>");
      if (d.popularity != null) parts.push('<span class="pill pop">★ ' + d.popularity + "</span>");
      let html = '<div class="pills">' + parts.join("") + "</div>";
      if (d.wiki_url) html += '<a class="ci-wiki" href="' + escapeHtml(d.wiki_url) + '" target="_blank" rel="noopener">wiki page ↗</a>';
      el.innerHTML = html || '<span class="muted">no data</span>';
    };
    state.enrichKey = key;
    if (state.enrichCache[key]) return render(state.enrichCache[key]);
    if (typeof fetch !== "function" || window.__EMBED__) return render(null);
    const url = "enrich?mpn=" + encodeURIComponent(mpn) + (lcsc ? "&lcsc=" + encodeURIComponent(lcsc) : "");
    let tries = 0;
    const go = () => fetch(url).then((r) => r.json()).then((d) => {
      if (d && d.pending && tries++ < 20) { setTimeout(go, 1500); return; } // vendor lookup warming
      state.enrichCache[key] = d; render(d);
    }).catch(() => render(null));
    go();
  }

  // When the visual base is the EDA's OWN exported SVG (Altium's render/pcb.svg),
  // the base is in PDF pixel space and our overlay is in board mm — unrelated
  // systems. Measure both with getBBox() (which accounts for any transforms the
  // exporter baked in) and scale/translate the overlay onto the base.
  function fitOverlayToBase(svg) {
    const base = svg.querySelector(".adom-base");
    const fit = svg.querySelector(".adom-fit");
    if (!base || !fit) return;
    let bb, ob;
    try { bb = base.getBBox(); ob = fit.getBBox(); } catch (e) { return; }
    if (!bb.width || !bb.height || !ob.width || !ob.height) return;
    // uniform scale so the overlay can't shear relative to the render
    const s = Math.min(bb.width / ob.width, bb.height / ob.height);
    const tx = bb.x + (bb.width - ob.width * s) / 2 - ob.x * s;
    const ty = bb.y + (bb.height - ob.height * s) / 2 - ob.y * s;
    fit.setAttribute("transform", "translate(" + tx + " " + ty + ") scale(" + s + ")");
    state.baseFit = s;
  }

  // ── pan / zoom (mutate viewBox) ───────────────────────────────────────────
  function wirePanZoom(svg) {
    svg.addEventListener("wheel", (e) => {
      e.preventDefault();
      const [x, y, w, h] = state.vb;
      const f = e.deltaY > 0 ? 1.12 : 0.893;
      const r = svg.getBoundingClientRect();
      const mx = x + ((e.clientX - r.left) / r.width) * w;
      const my = y + ((e.clientY - r.top) / r.height) * h;
      const nw = Math.min(state.vb0[2] * 8, Math.max(state.vb0[2] / 200, w * f));
      const nh = nw * (h / w);
      state.vb = [mx - (mx - x) * (nw / w), my - (my - y) * (nh / h), nw, nh];
      svg.setAttribute("viewBox", state.vb.join(" "));
    }, { passive: false });
    let drag = null;
    svg.addEventListener("mousedown", (e) => { drag = { x: e.clientX, y: e.clientY, vb: state.vb.slice() }; });
    addEventListener("mouseup", () => (drag = null));
    addEventListener("mousemove", (e) => {
      if (!drag) return;
      const r = svg.getBoundingClientRect();
      const dx = ((e.clientX - drag.x) / r.width) * drag.vb[2];
      const dy = ((e.clientY - drag.y) / r.height) * drag.vb[3];
      state.vb = [drag.vb[0] - dx, drag.vb[1] - dy, drag.vb[2], drag.vb[3]];
      svg.setAttribute("viewBox", state.vb.join(" "));
    });
  }
  function fit() { if (state.svg) { state.vb = state.vb0.slice(); state.svg.setAttribute("viewBox", state.vb.join(" ")); } }

  // ── Layers HUD ────────────────────────────────────────────────────────────
  // key = <g class="adom-layer-KEY">; label shown; on = visible by default.
  // KiCad-native base layers (each a <g class="adom-layer-KEY"> wrapping the EDA SVG)
  const LAYERS = [
    ["copperF", "Copper · Top", true], ["copperB", "Copper · Bot", true],
    ["maskF", "Mask · Top", true], ["maskB", "Mask · Bot", true],
    ["silkF", "Silk · Top", true], ["silkB", "Silk · Bot", true],
    ["fabF", "Fab · Top", true], ["fabB", "Fab · Bot", true],
    ["crtydF", "Courtyard · Top", false], ["crtydB", "Courtyard · Bot", false],
    ["edge", "Board edge", true],
  ];
  const SIDED = ["copper", "mask", "silk", "fab", "crtyd"]; // bases with F/B groups
  // canonical z-order (bottom→top): whole FAR side first, then whole NEAR side.
  // Within a side: mask (under copper), copper, silk, fab, courtyard.
  const STACK = ["mask", "copper", "silk", "fab", "crtyd"];

  // Inner copper layers for THIS board, front->back e.g. ["In1.Cu","In2.Cu"].
  // Comes from meta so a 2/4/6-layer board configures itself.
  function innerLayers() {
    return (state.meta && state.meta.inner_layers) || [];
  }
  const innerKey = (l) => "copper" + String(l).replace(/\.Cu$/, "");

  // Full copper stack, FRONT -> BACK: copperF, copperIn1 .. copperInN, copperB.
  // This is the physical depth order and drives both z-ordering and dimming.
  function copperStack() {
    return ["copperF", ...innerLayers().map(innerKey), "copperB"];
  }

  function order(side) {
    const F = side !== "bottom";
    const lo = (b) => (F ? b + "B" : b + "F"); // far
    const hi = (b) => (F ? b + "F" : b + "B"); // near
    // inner copper sits physically between the two sides. Deepest-from-the-viewer
    // first: viewing top that's In(n)..In1; viewing bottom it reverses.
    const inners = innerLayers().map(innerKey);
    const mid = F ? inners.slice().reverse() : inners.slice();
    return [...STACK.map(lo), ...mid, ...STACK.map(hi), "edge"];
  }
  function setActiveSide(side) {
    state.side = side;
    const svg = state.svg, flip = svg.querySelector(".adom-flip");
    if (!flip) return;
    order(side).forEach((k) => { const g = svg.querySelector(".adom-layer-" + k); if (g) flip.appendChild(g); });
    const ov = svg.querySelector(".adom-overlay"); if (ov) flip.appendChild(ov); // overlay stays on top
    // MIRROR the board about its vertical centre when viewing the bottom
    const cx = parseFloat(flip.getAttribute("data-cx")) || 0;
    if (side === "bottom") flip.setAttribute("transform", "translate(" + (2 * cx) + " 0) scale(-1 1)");
    else flip.removeAttribute("transform");
    // fade the FAR side back (like KiCad) so the near side reads clearly
    const far = side === "bottom" ? "F" : "B", near = side === "bottom" ? "B" : "F";
    SIDED.forEach((base) => {
      const fg = svg.querySelector(".adom-layer-" + base + far); if (fg) fg.style.opacity = "0.28";
      const ng = svg.querySelector(".adom-layer-" + base + near); if (ng) ng.style.opacity = "1";
    });
    // buried copper reads dimmed until explicitly raised
    innerLayers().map(innerKey).forEach((k) => {
      const g = svg.querySelector(".adom-layer-" + k); if (g) g.style.opacity = "0.28";
    });
    state.raised = null;
    document.querySelectorAll("#sidebtns .sidebtn").forEach((b) => b.classList.toggle("on", b.dataset.side === side));
    markRaised();
  }

  // Copper layers physically IN FRONT of `key`, from the current viewing side.
  // Viewing top, in front of In2 = [In1, F]; viewing bottom it is [In1's far side…]
  // i.e. the same stack walked from the other end.
  function copperInFrontOf(key) {
    const stack = state.side === "bottom" ? copperStack().slice().reverse() : copperStack();
    const i = stack.indexOf(key);
    return i <= 0 ? [] : stack.slice(0, i);
  }

  function markRaised() {
    document.querySelectorAll("#layers .lyr").forEach((r) => {
      r.classList.toggle("raised", r.dataset.key === state.raised);
    });
  }

  function raise(key) {
    const svg = state.svg, flip = svg.querySelector(".adom-flip"), g = svg.querySelector(".adom-layer-" + key);
    if (!g || !flip) return;

    // Raising the SAME layer twice restores the normal stack.
    if (state.raised === key) { setActiveSide(state.side); return; }

    flip.appendChild(g);
    const ov = svg.querySelector(".adom-overlay"); if (ov) flip.appendChild(ov);
    const cb = document.querySelector('#layers input[data-layer="' + key + '"]');
    if (cb && !cb.checked) { cb.checked = true; g.style.display = ""; }

    if (copperStack().indexOf(key) >= 0) {
      // Bring this copper layer fully forward and dim everything stacked in front
      // of it — the near outer layer AND any intermediate inner layers — so the
      // raised layer reads clearly through the board.
      g.style.opacity = "1";
      copperInFrontOf(key).forEach((k) => {
        const o = svg.querySelector(".adom-layer-" + k); if (o) o.style.opacity = "0.28";
      });
      state.raised = key;
      markRaised();
    }
  }
  // LAYERS + this board's inner copper, inserted right after Copper · Top so the
  // panel reads in physical stack order (Top, In1, In2, Bot).
  function layerRows() {
    const inners = innerLayers().map((l) => [innerKey(l), "Copper · " + String(l).replace(/\.Cu$/, ""), true]);
    if (!inners.length) return LAYERS;
    const rows = LAYERS.slice();
    const at = rows.findIndex(([k]) => k === "copperF");
    rows.splice(at < 0 ? 0 : at + 1, 0, ...inners);
    return rows;
  }

  function buildLayerHUD() {
    const el = $("#layers"); if (!el) return;
    el.innerHTML =
      '<div id="sidebtns"><span class="lbl">Front side</span>' +
      '<button class="sidebtn on" data-side="top">Top</button>' +
      '<button class="sidebtn" data-side="bottom">Bottom</button></div>' +
      layerRows().map(([k, label, on]) =>
        '<div class="lyr" data-key="' + k + '"><input type="checkbox" data-layer="' + k + '"' + (on ? " checked" : "") +
        '><span class="lyr-name" data-raise="' + k + '">' + label + "</span></div>"
      ).join("");
    el.querySelectorAll("input").forEach((cb) => cb.addEventListener("change", () => {
      const g = state.svg.querySelector(".adom-layer-" + cb.getAttribute("data-layer"));
      if (g) g.style.display = cb.checked ? "" : "none";
    }));
    el.querySelectorAll(".lyr-name").forEach((n) => n.addEventListener("click", () => raise(n.dataset.raise)));
    el.querySelectorAll("#sidebtns .sidebtn").forEach((b) => b.addEventListener("click", () => setActiveSide(b.dataset.side)));
    // apply default order + hide default-off layers
    setActiveSide("top");
    layerRows().filter(([, , on]) => !on).forEach(([k]) => { const g = state.svg.querySelector(".adom-layer-" + k); if (g) g.style.display = "none"; });
  }
  function buildNetList() {
    const el = $("#netlist"); if (!el) return;
    const names = state.meta.net_names || {};
    const rows = Object.keys(names).filter((k) => k !== "0" && names[k]).sort((a, b) => names[a].localeCompare(names[b]));
    el.innerHTML = rows.map((id) => '<div class="netrow" data-net="' + id + '">' + escapeHtml(names[id]) + "</div>").join("") || '<div class="muted">no named nets</div>';
    el.querySelectorAll(".netrow").forEach((r) => {
      // click-to-toggle only (net/fill highlight is a click action, not hover)
      r.addEventListener("click", () => { const n = r.getAttribute("data-net"); state.pinned = state.pinned === n ? null : n; state.pinned ? highlightNet(n) : clearNet(); });
    });
  }
  function setStat() {
    const m = state.meta, el = $("#stat");
    const txt = (m.components ? m.components.length : 0) + " components · " + (m.track_count || 0) + " tracks · " + (Object.keys(m.net_names || {}).length) + " nets";
    if (el) el.textContent = txt;
    setSubject(m.name || "Board", txt);
  }

  // ── helpers ───────────────────────────────────────────────────────────────
  function chip(html) { const c = $("#netchip"); if (!c) return; c.innerHTML = html; c.style.display = html ? "block" : "none"; }
  function escapeHtml(s) { return String(s).replace(/[&<>"]/g, (c) => ({ "&": "&amp;", "<": "&lt;", ">": "&gt;", '"': "&quot;" }[c])); }
  function cssEsc(s) { return String(s).replace(/["\\]/g, "\\$&"); }

  // ── boot ──────────────────────────────────────────────────────────────────
  window.AdomPcb = { mount, fit, highlightNet, toast, runCmd };
  function boot() {
    $("#fitbtn") && $("#fitbtn").addEventListener("click", fitToast);
    $("#hdr-fit") && $("#hdr-fit").addEventListener("click", fitToast);
    if (window.__PCB__) { window.__EMBED__ = true; mount(window.__PCB__.svg, window.__PCB__.meta); return; }
    // live mode: AI channel (toasts + drive cmds), console ring buffer, header version
    wireObservability();
    pollUi();
    fetch("version").then((r) => r.json()).then((v) => { const el = $("#ai-ver"); if (el && v && v.version) el.textContent = "v" + v.version; }).catch(() => {});
    fetch("state").then((r) => r.json()).then((s) => {
      if (s && s.svg) mount(s.svg, s.meta || {});
      else $("#svgbox").innerHTML = '<div class="empty">No board loaded. POST a .kicad_pcb to /load.</div>';
    }).catch(() => { $("#svgbox").innerHTML = '<div class="empty">server unreachable</div>'; });
  }
  if (document.readyState === "loading") document.addEventListener("DOMContentLoaded", boot); else boot();
})();