main
Caleb viewer bundle rf-workcell-module b5af022 1mo ago
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628
<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8" />
<meta name="viewport" content="width=device-width, initial-scale=1" />
<title>adom-schematic</title>
<style>
  /* Canonical Adom design tokens (adom-ui-design). Legacy names kept as aliases so
     the rest of this stylesheet is untouched. Primary accent is #00b8b0; #00e6dc is
     the bright hover variant. */
  :root { color-scheme: dark;
    --bg:#0d1117; --panel:#161b22; --elevated:#1c2128; --overlay:#21262d; --edge:#30363d;
    --ink:#e6edf3; --mut:#8b949e; --acc:#00b8b0; --acc2:#00e6dc;
    --green:#3fb950; --red:#f85149; --yellow:#d29922;
    --line:var(--edge); --txt:var(--ink); --muted:var(--mut); --accent:var(--acc); }
  * { box-sizing: border-box; }
  /* Three fonts by role (declared by name w/ system fallbacks, matching APM): body
     Satoshi, headers Familjen Grotesk, data/meta JetBrains Mono. */
  html, body { margin: 0; height: 100%; background: var(--bg); color: var(--txt); font: 13px/1.45 'Satoshi', -apple-system, 'Segoe UI', Roboto, sans-serif; overflow: hidden; }
  /* dragging to pan shouldn't select the sheet's text/labels */
  #svgbox, #svgbox * { user-select: none; -webkit-user-select: none; }
  /* match the sheet colour so the drawing reads as one infinite sheet, not a
     white page floating on a dark viewport */
  #svgbox { position: absolute; inset: 0; cursor: grab; background: #fbfbf6; }
  #svgbox:active { cursor: grabbing; }
  #svgbox svg { display: block; }
  .empty { position: absolute; inset: 0; display: grid; place-items: center; color: var(--muted); }

  /* component hotspots: transparent hit targets (no visual). */
  #svgbox svg .adom-comp { pointer-events: fill; cursor: pointer; }
  /* hierarchical-sheet links: highlight the box on hover, click to descend */
  #svgbox svg .adom-sheet-link { pointer-events: fill; cursor: pointer; }
  #svgbox svg .adom-sheet-link:hover { fill: var(--accent); fill-opacity: .12; stroke: var(--accent); stroke-width: .4; }
  /* sheet breadcrumb, top-left */
  #crumb { position: fixed; right: 12px; top: 12px; z-index: 40; display: none; align-items: center; gap: 6px;
           background: var(--panel); border: 1px solid var(--line); border-radius: 8px; padding: 5px 10px; font-size: 12px; }
  #crumb a { color: var(--accent); cursor: pointer; text-decoration: none; }
  #crumb a:hover { text-decoration: underline; }
  #crumb .sep { color: var(--muted); }
  #crumb .cur { color: var(--txt); }
  #crumb .sheetlbl { color: var(--muted); font-size: 10px; letter-spacing: .06em; text-transform: uppercase; }
  #crumb select { background: var(--elevated, #1c2128); color: var(--txt); border: 1px solid var(--line); border-radius: 6px; padding: 3px 6px; font: inherit; font-size: 12px; cursor: pointer; }
  #crumb select:focus { outline: none; border-color: var(--accent); }
  /* Hover highlight: recolour the hovered symbol's real KiCad strokes to the
     accent teal. This is a plain paint (a `stroke` swap), NOT an SVG `filter` —
     per-element drop-shadow filters forced an offscreen render for EACH of a
     symbol's strokes (a connector like J3 has ~1500), which made hovering crawl.
     A stroke recolour reads just as clearly and repaints almost for free.
     `paint-order:stroke` + a hairline widen makes it pop without a filter. */
  #svgbox svg .adom-sheet .symhl {
    stroke: #00b3a6 !important;
    stroke-width: .28 !important;
    paint-order: stroke;
  }
  /* keep the body fill readable: recolour its outline but don't fatten the fill */
  #svgbox svg .adom-sheet rect.symhl { stroke-width: .2 !important; }

  .panel { position: absolute; background: rgba(22,27,34,.82); backdrop-filter: blur(8px); border: 1px solid var(--line); border-radius: 10px; }
  #side { top: 12px; right: 12px; width: 220px; max-height: 94vh; padding: 10px 12px; display: flex; flex-direction: column; }
  #side h1 { margin: 0 0 8px; font-size: 12px; letter-spacing: .04em; text-transform: uppercase; color: var(--muted); font-weight: 600; }
  .sec-h { font-size: 10px; letter-spacing: .06em; text-transform: uppercase; color: var(--muted); font-weight: 600; margin: 2px 0 5px; border-bottom: 1px solid var(--line); padding-bottom: 3px; }
  #complist { overflow-y: auto; max-height: 74vh; }
  .crow { padding: 3px 7px; border-radius: 5px; cursor: pointer; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }
  .crow b { color: var(--accent); }
  .crow:hover { background: #1c2a2a; }
  .crow.sel { background: var(--accent); color: #06232a; }
  .crow.sel b { color: #06232a; }
  #fitbtn { margin-top: 8px; width: 100%; background: #17231c; color: var(--txt); border: 1px solid var(--line); border-radius: 7px; padding: 5px; cursor: pointer; }
  #fitbtn:hover { border-color: var(--accent); }
  #stat { bottom: 12px; left: 12px; padding: 6px 11px; color: var(--muted); }
  .muted { color: var(--muted); }

  /* follows the cursor and flips away from the viewport edges (same as the
     layout viewer); a click pins it where it currently sits, and only then does
     it accept pointer events so the JLC/wiki/datasheet links are clickable. */
  #card { position: fixed; z-index: 9999; display: none; min-width: 210px; max-width: 300px; padding: 10px 12px; pointer-events: none; }
  /* abridged hover card (small/embedded screens): ref · MPN · stock only */
  #card.brief { min-width: 0; max-width: 230px; padding: 7px 9px; }
  #card.brief .ci-h { margin-bottom: 4px; padding-bottom: 4px; }
  #card.brief .ci-ref { font-size: 13px; }
  #card.pinned { pointer-events: auto; box-shadow: 0 0 0 1px var(--accent); }
  .ci-h { display: flex; justify-content: space-between; align-items: baseline; gap: 8px; margin-bottom: 5px; padding-bottom: 5px; border-bottom: 1px solid var(--line); }
  .ci-ref { font-weight: 700; color: var(--accent); font-size: 15px; }
  .ci-val { color: var(--txt); }
  .ci-desc { color: var(--muted); font-size: 12px; margin-bottom: 6px; }
  .ci-row { display: flex; justify-content: space-between; gap: 10px; padding: 1px 0; }
  .ci-k { color: var(--muted); }
  .ci-v { text-align: right; word-break: break-word; }
  .ci-enrich { margin-top: 7px; padding-top: 7px; border-top: 1px solid var(--line); }
  .pills { display: flex; flex-wrap: wrap; gap: 5px; }
  .pill { background: #17231c; border: 1px solid var(--line); border-radius: 999px; padding: 1px 8px; font-size: 11px; }
  .pill.ok { color: #57d97e; border-color: #2c5a3a; }
  .pill.no { color: #e08a8a; border-color: #5a2c2c; }
  .pill.tier { color: #f2d060; }
  .pill.pop { color: #cba6ff; }
  .ci-mfr { color: var(--txt); font-size: 12px; margin-bottom: 5px; }
  .ci-links { margin-top: 6px; display: flex; gap: 12px; }
  .ci-lnk, .ci-ds { color: #6bb3ff; text-decoration: none; font-size: 12px; }
  .ci-ds { display: inline-block; margin-top: 6px; }
  #side h1, .sec-h { font-family: 'Familjen Grotesk', -apple-system, sans-serif; }

  /* ── canonical Adom 44px three-zone header (brand · subject · actions) ──
     Shown standalone / fullscreened; hidden in APM's small embedded panes
     (body.chrome gate, same as the side panels). */
  header.app { display: none; position: fixed; top: 0; left: 0; right: 0; height: 44px; z-index: 50;
    grid-template-columns: 1fr auto 1fr; align-items: center; gap: 12px; padding: 0 14px;
    background: var(--panel); border-bottom: 1px solid var(--edge); }
  body.chrome header.app { display: grid; }
  body.chrome #svgbox { top: 44px; }
  body.chrome #side { top: 56px; }
  body.chrome #crumb { top: 56px; }
  header.app .brand { display: flex; align-items: center; gap: 8px; min-width: 0; position: relative; cursor: default; padding-right: 14px; border-right: 1px solid var(--edge); }
  header.app .brand svg { width: 16px; height: 16px; color: var(--ink); flex: 0 0 16px; }
  header.app .app-name { font-family: 'Familjen Grotesk', -apple-system, sans-serif; font-weight: 600; font-size: 13px; color: var(--acc); white-space: nowrap; }
  header.app .app-sub { color: var(--mut); font-size: 12px; white-space: nowrap; }
  header.app .app-sub::before { content: "·"; margin: 0 6px; color: var(--mut); }
  header.app .subject { justify-self: center; display: flex; align-items: baseline; gap: 10px; min-width: 0; }
  header.app .subject-name { font-weight: 600; font-size: 13px; max-width: 44vw; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
  header.app .subject-meta { font-family: 'JetBrains Mono', ui-monospace, monospace; font-size: 11px; color: var(--mut); white-space: nowrap; }
  header.app .actions { justify-self: end; display: flex; align-items: center; gap: 8px; }
  header.app .actions button { background: var(--elevated); color: var(--ink); border: 1px solid var(--edge); border-radius: 7px; width: 30px; height: 26px; cursor: pointer; display: grid; place-items: center; }
  header.app .actions button:hover { border-color: var(--acc); }
  header.app .actions button svg { width: 15px; height: 15px; }
  header.app .appinfo { position: absolute; top: calc(100% + 6px); left: 0; min-width: 220px; background: var(--overlay); border: 1px solid var(--edge);
    border-radius: 10px; box-shadow: 0 12px 34px #000a; padding: 12px 14px; font-size: 12px; opacity: 0; visibility: hidden; transition: opacity .18s ease; pointer-events: none; z-index: 60; }
  header.app .brand:hover .appinfo { opacity: 1; visibility: visible; pointer-events: auto; transition-delay: .6s; }
  header.app .appinfo .ai-row { display: flex; justify-content: space-between; gap: 16px; padding: 2px 0; }
  header.app .appinfo .ai-k { color: var(--mut); }
  header.app .appinfo .ai-v { font-family: 'JetBrains Mono', ui-monospace, monospace; }
  header.app .appinfo a { color: var(--acc); text-decoration: none; }
  header.app .appinfo a:hover { text-decoration: underline; }

  /* ── toasts: transient feedback, bottom-center; also the AI's live channel ── */
  #toasts { position: fixed; left: 50%; bottom: 18px; transform: translateX(-50%); z-index: 3000; display: flex; flex-direction: column-reverse; gap: 8px; align-items: center; pointer-events: none; }
  .toast { pointer-events: auto; min-width: 200px; max-width: min(420px,90vw); display: flex; align-items: center; gap: 10px; padding: 10px 13px; border-radius: 10px;
    background: var(--overlay); border: 1px solid var(--edge); color: var(--ink); font-size: 13px; box-shadow: 0 10px 30px #000a; cursor: pointer; animation: toastin .18s ease; }
  @keyframes toastin { from { opacity: 0; transform: translateY(8px); } to { opacity: 1; transform: none; } }
  .toast.out { opacity: 0; transform: translateY(8px); transition: opacity .2s ease, transform .2s ease; }
  .toast .ti { flex: 0 0 16px; width: 16px; height: 16px; }
  .toast.success .ti { color: var(--green); } .toast.success { border-color: #2c5a3a; }
  .toast.error .ti { color: var(--red); } .toast.error { border-color: #5a2c2c; }
  .toast.info .ti { color: var(--acc); }
  .toast .tmsg { flex: 1; min-width: 0; }

  /* ── tooltips: body-appended fixed div, 600ms reveal, viewport-clamped both axes.
     Never a ::after (clipped by panel overflow/backdrop-filter). ── */
  #tt { position: fixed; z-index: 99999; max-width: 260px; background: var(--overlay); color: var(--ink); border: 1px solid var(--edge);
    border-radius: 7px; padding: 6px 9px; font-size: 12px; line-height: 1.4; box-shadow: 0 8px 22px #000b; pointer-events: none;
    opacity: 0; transform: translateY(3px); transition: opacity .18s ease, transform .18s ease; text-transform: none; }
  #tt.show { opacity: 1; transform: none; }
  @media (hover: none), (pointer: coarse) { #tt { display: none; } }
</style>
</head>
<body>
  <header class="app">
    <div class="brand">
      <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round"><path d="M3 12h4l2-5 3 10 2-5h7"/><circle cx="19" cy="12" r="1.4"/></svg>
      <span class="app-name">Schematic Viewer</span><span class="app-sub">interactive sheet</span>
      <div class="appinfo">
        <div class="ai-row"><span class="ai-k">version</span><span class="ai-v" id="ai-ver">-</span></div>
        <div class="ai-row"><span class="ai-k">engine</span><span class="ai-v">KiCad · Altium · Fusion</span></div>
        <div class="ai-row"><span class="ai-k">source</span><a href="https://wiki.adom.inc/adom/adom-schematic" target="_blank" rel="noopener">wiki page ↗</a></div>
      </div>
    </div>
    <div class="subject"><span class="subject-name" id="hdr-name"></span><span class="subject-meta" id="hdr-meta"></span></div>
    <div class="actions">
      <button id="hdr-fit" data-tip="Fit the whole sheet to the view (also: the Fit button, or press F)"><svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M4 9V4h5M20 9V4h-5M4 15v5h5M20 15v5h-5"/></svg></button>
    </div>
  </header>
  <div id="svgbox"><div class="empty">loading…</div></div>
  <div id="crumb"></div>
  <div id="side" class="panel">
    <div class="sec-h">Components</div>
    <div id="complist"></div>
    <button id="fitbtn" data-tip="Zoom to fit the entire sheet">Fit</button>
  </div>
  <div id="stat" class="panel"></div>
  <div id="card" class="panel"></div>
  <div id="toasts"></div>
  <script>window.__SCH__={svg:"<svg xmlns=\"http://www.w3.org/2000/svg\" viewBox=\"96.392 68.723 103.864 53.053\" preserveAspectRatio=\"xMidYMid meet\"><rect class=\"adom-sheet-bg\" x=\"96.392\" y=\"68.723\" width=\"103.864\" height=\"53.053\" fill=\"#fbfbf6\"/><g class=\"adom-sheet\"><line x1=\"106.680\" y1=\"101.600\" x2=\"106.680\" y2=\"107.950\" stroke=\"#008484\" stroke-width=\"0.15\" stroke-linecap=\"round\"/><line x1=\"148.590\" y1=\"87.630\" x2=\"148.590\" y2=\"101.600\" stroke=\"#008484\" stroke-width=\"0.15\" stroke-linecap=\"round\"/><line x1=\"142.240\" y1=\"87.630\" x2=\"148.590\" y2=\"87.630\" stroke=\"#008484\" stroke-width=\"0.15\" stroke-linecap=\"round\"/><line x1=\"109.220\" y1=\"101.600\" x2=\"106.680\" y2=\"101.600\" stroke=\"#008484\" stroke-width=\"0.15\" stroke-linecap=\"round\"/><line x1=\"109.220\" y1=\"87.630\" x2=\"106.680\" y2=\"87.630\" stroke=\"#008484\" stroke-width=\"0.15\" stroke-linecap=\"round\"/><line x1=\"148.590\" y1=\"101.600\" x2=\"148.590\" y2=\"107.950\" stroke=\"#008484\" stroke-width=\"0.15\" stroke-linecap=\"round\"/><line x1=\"114.300\" y1=\"101.600\" x2=\"137.160\" y2=\"101.600\" stroke=\"#008484\" stroke-width=\"0.15\" stroke-linecap=\"round\"/><line x1=\"106.680\" y1=\"87.630\" x2=\"106.680\" y2=\"101.600\" stroke=\"#008484\" stroke-width=\"0.15\" stroke-linecap=\"round\"/><line x1=\"114.300\" y1=\"87.630\" x2=\"137.160\" y2=\"87.630\" stroke=\"#008484\" stroke-width=\"0.15\" stroke-linecap=\"round\"/><line x1=\"148.590\" y1=\"101.600\" x2=\"142.240\" y2=\"101.600\" stroke=\"#008484\" stroke-width=\"0.15\" stroke-linecap=\"round\"/><circle cx=\"106.680\" cy=\"101.600\" r=\"0.4\" fill=\"#008484\"/><circle cx=\"148.590\" cy=\"101.600\" r=\"0.4\" fill=\"#008484\"/><g data-cref=\"J1\"><path d=\"M114.300 87.630 L112.268 87.630 \" fill=\"none\" stroke=\"#840000\" stroke-width=\"0.15\"/><circle cx=\"111.760\" cy=\"87.630\" r=\"0.508\" fill=\"none\" stroke=\"#840000\" stroke-width=\"0.15\"/><line x1=\"114.300\" y1=\"87.630\" x2=\"113.030\" y2=\"87.630\" stroke=\"#840000\" stroke-width=\"0.15\"/><line x1=\"109.220\" y1=\"87.630\" x2=\"110.490\" y2=\"87.630\" stroke=\"#840000\" stroke-width=\"0.15\"/><text x=\"114.700\" y=\"88.830\" font-size=\"1.3\" fill=\"#840000\" font-family=\"monospace\">J1</text><text x=\"114.700\" y=\"90.330\" font-size=\"1.1\" fill=\"#008484\" font-family=\"monospace\">Conn_Coaxial_Small</text></g><g data-cref=\"#PWR02\"><path d=\"M148.590 107.950 L148.590 109.220 L149.860 109.220 L148.590 110.490 L147.320 109.220 L148.590 109.220 \" fill=\"none\" stroke=\"#840000\" stroke-width=\"0.15\"/><line x1=\"148.590\" y1=\"107.950\" x2=\"148.590\" y2=\"107.950\" stroke=\"#840000\" stroke-width=\"0.15\"/><text x=\"148.590\" y=\"112.090\" font-size=\"1.4\" fill=\"#840000\" font-family=\"monospace\" text-anchor=\"middle\">GND</text></g><g data-cref=\"H3\"><circle cx=\"177.800\" cy=\"101.600\" r=\"1.270\" fill=\"none\" stroke=\"#840000\" stroke-width=\"0.15\"/><text x=\"178.200\" y=\"102.800\" font-size=\"1.3\" fill=\"#840000\" font-family=\"monospace\">H3</text><text x=\"178.200\" y=\"104.300\" font-size=\"1.1\" fill=\"#008484\" font-family=\"monospace\">MountingHole</text></g><g data-cref=\"#PWR01\"><path d=\"M106.680 107.950 L106.680 109.220 L107.950 109.220 L106.680 110.490 L105.410 109.220 L106.680 109.220 \" fill=\"none\" stroke=\"#840000\" stroke-width=\"0.15\"/><line x1=\"106.680\" y1=\"107.950\" x2=\"106.680\" y2=\"107.950\" stroke=\"#840000\" stroke-width=\"0.15\"/><text x=\"106.680\" y=\"112.090\" font-size=\"1.4\" fill=\"#840000\" font-family=\"monospace\" text-anchor=\"middle\">GND</text></g><g data-cref=\"H1\"><circle cx=\"177.800\" cy=\"76.200\" r=\"1.270\" fill=\"none\" stroke=\"#840000\" stroke-width=\"0.15\"/><text x=\"178.200\" y=\"77.400\" font-size=\"1.3\" fill=\"#840000\" font-family=\"monospace\">H1</text><text x=\"178.200\" y=\"78.900\" font-size=\"1.1\" fill=\"#008484\" font-family=\"monospace\">MountingHole</text></g><g data-cref=\"J2\"><path d=\"M114.300 101.600 L112.268 101.600 \" fill=\"none\" stroke=\"#840000\" stroke-width=\"0.15\"/><circle cx=\"111.760\" cy=\"101.600\" r=\"0.508\" fill=\"none\" stroke=\"#840000\" stroke-width=\"0.15\"/><line x1=\"114.300\" y1=\"101.600\" x2=\"113.030\" y2=\"101.600\" stroke=\"#840000\" stroke-width=\"0.15\"/><line x1=\"109.220\" y1=\"101.600\" x2=\"110.490\" y2=\"101.600\" stroke=\"#840000\" stroke-width=\"0.15\"/><text x=\"114.700\" y=\"102.800\" font-size=\"1.3\" fill=\"#840000\" font-family=\"monospace\">J2</text><text x=\"114.700\" y=\"104.300\" font-size=\"1.1\" fill=\"#008484\" font-family=\"monospace\">Conn_Coaxial_Small</text></g><g data-cref=\"H4\"><circle cx=\"177.800\" cy=\"114.300\" r=\"1.270\" fill=\"none\" stroke=\"#840000\" stroke-width=\"0.15\"/><text x=\"178.200\" y=\"115.500\" font-size=\"1.3\" fill=\"#840000\" font-family=\"monospace\">H4</text><text x=\"178.200\" y=\"117.000\" font-size=\"1.1\" fill=\"#008484\" font-family=\"monospace\">MountingHole</text></g><g data-cref=\"J3\"><path d=\"M137.160 87.630 L139.192 87.630 \" fill=\"none\" stroke=\"#840000\" stroke-width=\"0.15\"/><circle cx=\"139.700\" cy=\"87.630\" r=\"0.508\" fill=\"none\" stroke=\"#840000\" stroke-width=\"0.15\"/><line x1=\"137.160\" y1=\"87.630\" x2=\"138.430\" y2=\"87.630\" stroke=\"#840000\" stroke-width=\"0.15\"/><line x1=\"142.240\" y1=\"87.630\" x2=\"140.970\" y2=\"87.630\" stroke=\"#840000\" stroke-width=\"0.15\"/><text x=\"142.640\" y=\"88.830\" font-size=\"1.3\" fill=\"#840000\" font-family=\"monospace\">J3</text><text x=\"142.640\" y=\"90.330\" font-size=\"1.1\" fill=\"#008484\" font-family=\"monospace\">Conn_Coaxial_Small</text></g><g data-cref=\"H2\"><circle cx=\"177.800\" cy=\"88.900\" r=\"1.270\" fill=\"none\" stroke=\"#840000\" stroke-width=\"0.15\"/><text x=\"178.200\" y=\"90.100\" font-size=\"1.3\" fill=\"#840000\" font-family=\"monospace\">H2</text><text x=\"178.200\" y=\"91.600\" font-size=\"1.1\" fill=\"#008484\" font-family=\"monospace\">MountingHole</text></g><g data-cref=\"J4\"><path d=\"M137.160 101.600 L139.192 101.600 \" fill=\"none\" stroke=\"#840000\" stroke-width=\"0.15\"/><circle cx=\"139.700\" cy=\"101.600\" r=\"0.508\" fill=\"none\" stroke=\"#840000\" stroke-width=\"0.15\"/><line x1=\"137.160\" y1=\"101.600\" x2=\"138.430\" y2=\"101.600\" stroke=\"#840000\" stroke-width=\"0.15\"/><line x1=\"142.240\" y1=\"101.600\" x2=\"140.970\" y2=\"101.600\" stroke=\"#840000\" stroke-width=\"0.15\"/><text x=\"142.640\" y=\"102.800\" font-size=\"1.3\" fill=\"#840000\" font-family=\"monospace\">J4</text><text x=\"142.640\" y=\"104.300\" font-size=\"1.1\" fill=\"#008484\" font-family=\"monospace\">Conn_Coaxial_Small</text></g></g><g class=\"adom-overlay\"><rect class=\"adom-comp\" x=\"109.220\" y=\"86.322\" width=\"5.080\" height=\"2.616\" data-ref=\"J1\" fill=\"transparent\"/><rect class=\"adom-comp\" x=\"176.530\" y=\"100.330\" width=\"2.540\" height=\"2.540\" data-ref=\"H3\" fill=\"transparent\"/><rect class=\"adom-comp\" x=\"176.530\" y=\"74.930\" width=\"2.540\" height=\"2.540\" data-ref=\"H1\" fill=\"transparent\"/><rect class=\"adom-comp\" x=\"109.220\" y=\"100.292\" width=\"5.080\" height=\"2.616\" data-ref=\"J2\" fill=\"transparent\"/><rect class=\"adom-comp\" x=\"176.530\" y=\"113.030\" width=\"2.540\" height=\"2.540\" data-ref=\"H4\" fill=\"transparent\"/><rect class=\"adom-comp\" x=\"137.160\" y=\"86.322\" width=\"5.080\" height=\"2.616\" data-ref=\"J3\" fill=\"transparent\"/><rect class=\"adom-comp\" x=\"176.530\" y=\"87.630\" width=\"2.540\" height=\"2.540\" data-ref=\"H2\" fill=\"transparent\"/><rect class=\"adom-comp\" x=\"137.160\" y=\"100.292\" width=\"5.080\" height=\"2.616\" data-ref=\"J4\" fill=\"transparent\"/></g></svg>",meta:{"view_box":[96.3925,68.7234,103.8635,53.05300000000001],"components":[{"reference":"J1","value":"Conn_Coaxial_Small","footprint":"coax:925-169j-51pt","lcsc":"","description":"small coaxial connector (BNC, SMA, SMB, SMC, Cinch/RCA, LEMO, ...)","datasheet":"","x":111.76,"y":87.63,"bbox":[109.22,86.32199999999999,114.30000000000001,88.93809999999999],"pin_count":2},{"reference":"H3","value":"MountingHole","footprint":"MountingHole:MountingHole_3.2mm_M3","lcsc":"","description":"Mounting Hole without connection","datasheet":"","x":177.8,"y":101.6,"bbox":[176.53,100.33,179.07000000000002,102.86999999999999],"pin_count":0},{"reference":"H1","value":"MountingHole","footprint":"MountingHole:MountingHole_3.2mm_M3","lcsc":"","description":"Mounting Hole without connection","datasheet":"","x":177.8,"y":76.2,"bbox":[176.53,74.93,179.07000000000002,77.47],"pin_count":0},{"reference":"J2","value":"Conn_Coaxial_Small","footprint":"coax:925-169j-51pt","lcsc":"","description":"small coaxial connector (BNC, SMA, SMB, SMC, Cinch/RCA, LEMO, ...)","datasheet":"","x":111.76,"y":101.6,"bbox":[109.22,100.29199999999999,114.30000000000001,102.90809999999999],"pin_count":2},{"reference":"H4","value":"MountingHole","footprint":"MountingHole:MountingHole_3.2mm_M3","lcsc":"","description":"Mounting Hole without connection","datasheet":"","x":177.8,"y":114.3,"bbox":[176.53,113.03,179.07000000000002,115.57],"pin_count":0},{"reference":"J3","value":"Conn_Coaxial_Small","footprint":"coax:132322","lcsc":"","description":"small coaxial connector (BNC, SMA, SMB, SMC, Cinch/RCA, LEMO, ...)","datasheet":"","x":139.7,"y":87.63,"bbox":[137.16,86.32199999999999,142.23999999999998,88.93809999999999],"pin_count":2},{"reference":"H2","value":"MountingHole","footprint":"MountingHole:MountingHole_3.2mm_M3","lcsc":"","description":"Mounting Hole without connection","datasheet":"","x":177.8,"y":88.9,"bbox":[176.53,87.63000000000001,179.07000000000002,90.17],"pin_count":0},{"reference":"J4","value":"Conn_Coaxial_Small","footprint":"coax:132322","lcsc":"","description":"small coaxial connector (BNC, SMA, SMB, SMC, Cinch/RCA, LEMO, ...)","datasheet":"","x":139.7,"y":101.6,"bbox":[137.16,100.29199999999999,142.23999999999998,102.90809999999999],"pin_count":2}],"comp_count":8,"sub_sheets":[]}};window.__SHEETS__={"__root__":{"meta":{"comp_count":8,"components":[{"bbox":[109.22,86.32199999999999,114.30000000000001,88.93809999999999],"datasheet":"","description":"small coaxial connector (BNC, SMA, SMB, SMC, Cinch/RCA, LEMO, ...)","footprint":"coax:925-169j-51pt","lcsc":"","pin_count":2,"reference":"J1","value":"Conn_Coaxial_Small","x":111.76,"y":87.63},{"bbox":[176.53,100.33,179.07000000000002,102.86999999999999],"datasheet":"","description":"Mounting Hole without connection","footprint":"MountingHole:MountingHole_3.2mm_M3","lcsc":"","pin_count":0,"reference":"H3","value":"MountingHole","x":177.8,"y":101.6},{"bbox":[176.53,74.93,179.07000000000002,77.47],"datasheet":"","description":"Mounting Hole without connection","footprint":"MountingHole:MountingHole_3.2mm_M3","lcsc":"","pin_count":0,"reference":"H1","value":"MountingHole","x":177.8,"y":76.2},{"bbox":[109.22,100.29199999999999,114.30000000000001,102.90809999999999],"datasheet":"","description":"small coaxial connector (BNC, SMA, SMB, SMC, Cinch/RCA, LEMO, ...)","footprint":"coax:925-169j-51pt","lcsc":"","pin_count":2,"reference":"J2","value":"Conn_Coaxial_Small","x":111.76,"y":101.6},{"bbox":[176.53,113.03,179.07000000000002,115.57],"datasheet":"","description":"Mounting Hole without connection","footprint":"MountingHole:MountingHole_3.2mm_M3","lcsc":"","pin_count":0,"reference":"H4","value":"MountingHole","x":177.8,"y":114.3},{"bbox":[137.16,86.32199999999999,142.23999999999998,88.93809999999999],"datasheet":"","description":"small coaxial connector (BNC, SMA, SMB, SMC, Cinch/RCA, LEMO, ...)","footprint":"coax:132322","lcsc":"","pin_count":2,"reference":"J3","value":"Conn_Coaxial_Small","x":139.7,"y":87.63},{"bbox":[176.53,87.63000000000001,179.07000000000002,90.17],"datasheet":"","description":"Mounting Hole without connection","footprint":"MountingHole:MountingHole_3.2mm_M3","lcsc":"","pin_count":0,"reference":"H2","value":"MountingHole","x":177.8,"y":88.9},{"bbox":[137.16,100.29199999999999,142.23999999999998,102.90809999999999],"datasheet":"","description":"small coaxial connector (BNC, SMA, SMB, SMC, Cinch/RCA, LEMO, ...)","footprint":"coax:132322","lcsc":"","pin_count":2,"reference":"J4","value":"Conn_Coaxial_Small","x":139.7,"y":101.6}],"sub_sheets":[],"view_box":[96.3925,68.7234,103.8635,53.05300000000001]},"svg":"<svg xmlns=\"http://www.w3.org/2000/svg\" viewBox=\"96.392 68.723 103.864 53.053\" preserveAspectRatio=\"xMidYMid meet\"><rect class=\"adom-sheet-bg\" x=\"96.392\" y=\"68.723\" width=\"103.864\" height=\"53.053\" fill=\"#fbfbf6\"/><g class=\"adom-sheet\"><line x1=\"106.680\" y1=\"101.600\" x2=\"106.680\" y2=\"107.950\" stroke=\"#008484\" stroke-width=\"0.15\" stroke-linecap=\"round\"/><line x1=\"148.590\" y1=\"87.630\" x2=\"148.590\" y2=\"101.600\" stroke=\"#008484\" stroke-width=\"0.15\" stroke-linecap=\"round\"/><line x1=\"142.240\" y1=\"87.630\" x2=\"148.590\" y2=\"87.630\" stroke=\"#008484\" stroke-width=\"0.15\" stroke-linecap=\"round\"/><line x1=\"109.220\" y1=\"101.600\" x2=\"106.680\" y2=\"101.600\" stroke=\"#008484\" stroke-width=\"0.15\" stroke-linecap=\"round\"/><line x1=\"109.220\" y1=\"87.630\" x2=\"106.680\" y2=\"87.630\" stroke=\"#008484\" stroke-width=\"0.15\" stroke-linecap=\"round\"/><line x1=\"148.590\" y1=\"101.600\" x2=\"148.590\" y2=\"107.950\" stroke=\"#008484\" stroke-width=\"0.15\" stroke-linecap=\"round\"/><line x1=\"114.300\" y1=\"101.600\" x2=\"137.160\" y2=\"101.600\" stroke=\"#008484\" stroke-width=\"0.15\" stroke-linecap=\"round\"/><line x1=\"106.680\" y1=\"87.630\" x2=\"106.680\" y2=\"101.600\" stroke=\"#008484\" stroke-width=\"0.15\" stroke-linecap=\"round\"/><line x1=\"114.300\" y1=\"87.630\" x2=\"137.160\" y2=\"87.630\" stroke=\"#008484\" stroke-width=\"0.15\" stroke-linecap=\"round\"/><line x1=\"148.590\" y1=\"101.600\" x2=\"142.240\" y2=\"101.600\" stroke=\"#008484\" stroke-width=\"0.15\" stroke-linecap=\"round\"/><circle cx=\"106.680\" cy=\"101.600\" r=\"0.4\" fill=\"#008484\"/><circle cx=\"148.590\" cy=\"101.600\" r=\"0.4\" fill=\"#008484\"/><g data-cref=\"J1\"><path d=\"M114.300 87.630 L112.268 87.630 \" fill=\"none\" stroke=\"#840000\" stroke-width=\"0.15\"/><circle cx=\"111.760\" cy=\"87.630\" r=\"0.508\" fill=\"none\" stroke=\"#840000\" stroke-width=\"0.15\"/><line x1=\"114.300\" y1=\"87.630\" x2=\"113.030\" y2=\"87.630\" stroke=\"#840000\" stroke-width=\"0.15\"/><line x1=\"109.220\" y1=\"87.630\" x2=\"110.490\" y2=\"87.630\" stroke=\"#840000\" stroke-width=\"0.15\"/><text x=\"114.700\" y=\"88.830\" font-size=\"1.3\" fill=\"#840000\" font-family=\"monospace\">J1</text><text x=\"114.700\" y=\"90.330\" font-size=\"1.1\" fill=\"#008484\" font-family=\"monospace\">Conn_Coaxial_Small</text></g><g data-cref=\"#PWR02\"><path d=\"M148.590 107.950 L148.590 109.220 L149.860 109.220 L148.590 110.490 L147.320 109.220 L148.590 109.220 \" fill=\"none\" stroke=\"#840000\" stroke-width=\"0.15\"/><line x1=\"148.590\" y1=\"107.950\" x2=\"148.590\" y2=\"107.950\" stroke=\"#840000\" stroke-width=\"0.15\"/><text x=\"148.590\" y=\"112.090\" font-size=\"1.4\" fill=\"#840000\" font-family=\"monospace\" text-anchor=\"middle\">GND</text></g><g data-cref=\"H3\"><circle cx=\"177.800\" cy=\"101.600\" r=\"1.270\" fill=\"none\" stroke=\"#840000\" stroke-width=\"0.15\"/><text x=\"178.200\" y=\"102.800\" font-size=\"1.3\" fill=\"#840000\" font-family=\"monospace\">H3</text><text x=\"178.200\" y=\"104.300\" font-size=\"1.1\" fill=\"#008484\" font-family=\"monospace\">MountingHole</text></g><g data-cref=\"#PWR01\"><path d=\"M106.680 107.950 L106.680 109.220 L107.950 109.220 L106.680 110.490 L105.410 109.220 L106.680 109.220 \" fill=\"none\" stroke=\"#840000\" stroke-width=\"0.15\"/><line x1=\"106.680\" y1=\"107.950\" x2=\"106.680\" y2=\"107.950\" stroke=\"#840000\" stroke-width=\"0.15\"/><text x=\"106.680\" y=\"112.090\" font-size=\"1.4\" fill=\"#840000\" font-family=\"monospace\" text-anchor=\"middle\">GND</text></g><g data-cref=\"H1\"><circle cx=\"177.800\" cy=\"76.200\" r=\"1.270\" fill=\"none\" stroke=\"#840000\" stroke-width=\"0.15\"/><text x=\"178.200\" y=\"77.400\" font-size=\"1.3\" fill=\"#840000\" font-family=\"monospace\">H1</text><text x=\"178.200\" y=\"78.900\" font-size=\"1.1\" fill=\"#008484\" font-family=\"monospace\">MountingHole</text></g><g data-cref=\"J2\"><path d=\"M114.300 101.600 L112.268 101.600 \" fill=\"none\" stroke=\"#840000\" stroke-width=\"0.15\"/><circle cx=\"111.760\" cy=\"101.600\" r=\"0.508\" fill=\"none\" stroke=\"#840000\" stroke-width=\"0.15\"/><line x1=\"114.300\" y1=\"101.600\" x2=\"113.030\" y2=\"101.600\" stroke=\"#840000\" stroke-width=\"0.15\"/><line x1=\"109.220\" y1=\"101.600\" x2=\"110.490\" y2=\"101.600\" stroke=\"#840000\" stroke-width=\"0.15\"/><text x=\"114.700\" y=\"102.800\" font-size=\"1.3\" fill=\"#840000\" font-family=\"monospace\">J2</text><text x=\"114.700\" y=\"104.300\" font-size=\"1.1\" fill=\"#008484\" font-family=\"monospace\">Conn_Coaxial_Small</text></g><g data-cref=\"H4\"><circle cx=\"177.800\" cy=\"114.300\" r=\"1.270\" fill=\"none\" stroke=\"#840000\" stroke-width=\"0.15\"/><text x=\"178.200\" y=\"115.500\" font-size=\"1.3\" fill=\"#840000\" font-family=\"monospace\">H4</text><text x=\"178.200\" y=\"117.000\" font-size=\"1.1\" fill=\"#008484\" font-family=\"monospace\">MountingHole</text></g><g data-cref=\"J3\"><path d=\"M137.160 87.630 L139.192 87.630 \" fill=\"none\" stroke=\"#840000\" stroke-width=\"0.15\"/><circle cx=\"139.700\" cy=\"87.630\" r=\"0.508\" fill=\"none\" stroke=\"#840000\" stroke-width=\"0.15\"/><line x1=\"137.160\" y1=\"87.630\" x2=\"138.430\" y2=\"87.630\" stroke=\"#840000\" stroke-width=\"0.15\"/><line x1=\"142.240\" y1=\"87.630\" x2=\"140.970\" y2=\"87.630\" stroke=\"#840000\" stroke-width=\"0.15\"/><text x=\"142.640\" y=\"88.830\" font-size=\"1.3\" fill=\"#840000\" font-family=\"monospace\">J3</text><text x=\"142.640\" y=\"90.330\" font-size=\"1.1\" fill=\"#008484\" font-family=\"monospace\">Conn_Coaxial_Small</text></g><g data-cref=\"H2\"><circle cx=\"177.800\" cy=\"88.900\" r=\"1.270\" fill=\"none\" stroke=\"#840000\" stroke-width=\"0.15\"/><text x=\"178.200\" y=\"90.100\" font-size=\"1.3\" fill=\"#840000\" font-family=\"monospace\">H2</text><text x=\"178.200\" y=\"91.600\" font-size=\"1.1\" fill=\"#008484\" font-family=\"monospace\">MountingHole</text></g><g data-cref=\"J4\"><path d=\"M137.160 101.600 L139.192 101.600 \" fill=\"none\" stroke=\"#840000\" stroke-width=\"0.15\"/><circle cx=\"139.700\" cy=\"101.600\" r=\"0.508\" fill=\"none\" stroke=\"#840000\" stroke-width=\"0.15\"/><line x1=\"137.160\" y1=\"101.600\" x2=\"138.430\" y2=\"101.600\" stroke=\"#840000\" stroke-width=\"0.15\"/><line x1=\"142.240\" y1=\"101.600\" x2=\"140.970\" y2=\"101.600\" stroke=\"#840000\" stroke-width=\"0.15\"/><text x=\"142.640\" y=\"102.800\" font-size=\"1.3\" fill=\"#840000\" font-family=\"monospace\">J4</text><text x=\"142.640\" y=\"104.300\" font-size=\"1.1\" fill=\"#008484\" font-family=\"monospace\">Conn_Coaxial_Small</text></g></g><g class=\"adom-overlay\"><rect class=\"adom-comp\" x=\"109.220\" y=\"86.322\" width=\"5.080\" height=\"2.616\" data-ref=\"J1\" fill=\"transparent\"/><rect class=\"adom-comp\" x=\"176.530\" y=\"100.330\" width=\"2.540\" height=\"2.540\" data-ref=\"H3\" fill=\"transparent\"/><rect class=\"adom-comp\" x=\"176.530\" y=\"74.930\" width=\"2.540\" height=\"2.540\" data-ref=\"H1\" fill=\"transparent\"/><rect class=\"adom-comp\" x=\"109.220\" y=\"100.292\" width=\"5.080\" height=\"2.616\" data-ref=\"J2\" fill=\"transparent\"/><rect class=\"adom-comp\" x=\"176.530\" y=\"113.030\" width=\"2.540\" height=\"2.540\" data-ref=\"H4\" fill=\"transparent\"/><rect class=\"adom-comp\" x=\"137.160\" y=\"86.322\" width=\"5.080\" height=\"2.616\" data-ref=\"J3\" fill=\"transparent\"/><rect class=\"adom-comp\" x=\"176.530\" y=\"87.630\" width=\"2.540\" height=\"2.540\" data-ref=\"H2\" fill=\"transparent\"/><rect class=\"adom-comp\" x=\"137.160\" y=\"100.292\" width=\"5.080\" height=\"2.616\" data-ref=\"J4\" fill=\"transparent\"/></g></svg>"}};</script>
<script>/* adom-schematic core — KiCad-native schematic SVG + a component hotspot
 * overlay. Hover a symbol → what it is (ref/value/footprint) + live stock & price.
 * Consumes window.__SCH__ = { svg, meta } (embed) OR fetches /state (live). */
(function () {
  "use strict";
  const $ = (s, r) => (r || document).querySelector(s);
  const state = { meta: null, pinned: null, enrichCache: {} };

  function mount(svgText, meta) {
    state.meta = meta || { 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.hlRef = null; // fresh DOM: nothing is highlighted yet
    // Match the viewport to the sheet's own background so the drawing reads as one
    // infinite sheet. The fill differs per EDA (Altium #ffffff, KiCad/Eagle #fbfbf6),
    // so read it off the rendered sheet rather than hardcoding.
    const bg = svg.querySelector(".adom-sheet-bg");
    if (bg) { const f = bg.getAttribute("fill"); if (f) box.style.background = f; }
    state.vb = (svg.getAttribute("viewBox") || "0 0 100 100").split(/\s+/).map(Number);
    state.vb0 = state.vb.slice();
    wireHover(svg);
    wirePanZoom(svg);
    buildCompList();
    buildCrumb();
    setStat();
    updateChrome();
  }
  // Show the canonical header + Components panel + stat only when there's room
  // (standalone tab or a fullscreened pane); hide them in a small embedded
  // thumbnail (APM board view) so the sheet fills the pane.
  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);

  // ── POST helper + observability: mirror console + errors to the server ring
  // buffer (GET /console) so the AI can read what the UI is doing. Live-server only
  // (an embed has no server of its own). ──────────────────────────────────────
  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); // errors stay until dismissed
  }
  // 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);
  }
  // The AI drives the front-end the same way a user would (see CLI `ui` verbs).
  function runCmd(c) {
    if (!c || !c.action) return;
    if (c.action === "fit") { fit(); toast("Fit to view", "info"); }
    else if (c.action === "sheet" && c.arg) { openSheet(c.arg); }
    else if (c.action === "highlight" && c.arg) { state.pinned = c.arg; compHl(c.arg); showCard(c.arg); toast("Highlight " + c.arg, "info"); }
    else if (c.action === "clear") { state.pinned = null; clearSym(); hideCard(); }
    else if (c.action === "toast") { toast(c.arg || "", c.type || "info"); }
  }

  // ── tooltip: body-appended fixed div, 600ms reveal, clamped to viewport on BOTH
  // axes, flips at the right/bottom edges. Never a ::after (clipped by panels). ──
  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;   // flip left at the right edge
      if (y + h > innerHeight - M) y = cy - h - 14;  // flip up at the bottom edge
      x = Math.max(M, Math.min(x, innerWidth - w - M)); // clamp both axes so it never
      y = Math.max(M, Math.min(y, innerHeight - h - M)); // runs off any edge
      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 (center zone) ──
  function setSubject(name, meta) {
    const n = $("#hdr-name"), m = $("#hdr-meta");
    if (n) n.textContent = name || "";
    if (m) m.textContent = meta || "";
  }
  function curSheetLabel() {
    if (!state.curSheet) return "Root sheet";
    const subs = (state.sheets && state.sheets.__root__ && state.sheets.__root__.meta && state.sheets.__root__.meta.sub_sheets) || state.meta.sub_sheets || [];
    const s = subs.find((x) => x.file === state.curSheet);
    return s ? (s.name || s.file) : state.curSheet;
  }

  // ── CROSS-PROBE with the sibling PCB view (host/APM relays between the two
  // same-origin iframes, keyed by the component reference). ──
  function xpost(ref) { try { if (window.parent && window.parent !== window) window.parent.postMessage({ adomXprobe: true, ref: ref }, "*"); } catch (e) {} }
  function centerOn(ref) {
    const c = (state.meta.components || []).find((k) => k.reference === ref);
    if (c && c.bbox) frameBBox(c.bbox);
  }
  // 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 sheet.
  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.5) * 3.2; // the part fills ~1/3 of the short side
    span = Math.max(span, 10);                         // don't over-zoom a tiny part
    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 PCB view. If the referenced part lives on another
  // bundled sheet, switch to it first, then 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;
    if (!(state.meta.components || []).some((k) => k.reference === ref) && state.sheets) {
      for (const key in state.sheets) { const cs = state.sheets[key].meta && state.sheets[key].meta.components; if (cs && cs.some((k) => k.reference === ref)) { mountSheet(key); break; } }
    }
    state.pinned = ref; compHl(ref); centerOn(ref);
  }
  // deselect propagated from the sibling view → clear our highlight + card too
  function applyXclear() { state.pinned = null; clearSym(); hideCard(); }
  addEventListener("message", (e) => { const d = e.data; if (!d || !d.adomXprobe) return; if (d.clear) applyXclear(); else if (d.ref) applyXprobe(d.ref); });

  // ── component highlight: screen-blend teal over the symbol (turns the symbol
  // strokes teal, leaves the white sheet white — no box) + list sync ──────────
  function clearSym() {
    if (state.hlRef == null) return; // nothing highlighted — skip the DOM sweep
    state.hlRef = null;
    state.svg.querySelectorAll(".symhl").forEach((el) => el.classList.remove("symhl"));
    document.querySelectorAll("#complist .crow.sel").forEach((r) => r.classList.remove("sel"));
  }
  function compHl(ref) {
    if (state.hlRef === ref) return; // already highlighted — don't re-tag ~1500 nodes
    clearSym();
    state.hlRef = ref;
    // recolour the hovered symbol's real KiCad strokes teal (cheap paint, no filter)
    state.svg.querySelectorAll('.adom-sheet [data-cref="' + cssEsc(ref) + '"]').forEach((el) => el.classList.add("symhl"));
    document.querySelectorAll("#complist .crow").forEach((r) => r.classList.toggle("sel", r.getAttribute("data-ref") === ref));
  }
  function clearHl() { if (!state.pinned) clearSym(); }
  function wireHover(svg) {
    svg.addEventListener("mousemove", (e) => {
      state.mouse = e; // remembered so a pin lands where the card already is
      if (state.pinned) return; // keep the pinned card + highlight stable
      const c = e.target.closest(".adom-comp");
      if (c) {
        const ref = c.getAttribute("data-ref");
        // still on the SAME symbol: only trail the card. Don't re-highlight (a
        // ~1500-node re-tag) or rebuild the card / re-fire enrichment every frame.
        if (ref === state.hlRef) { placeCard($("#card"), e); return; }
        compHl(ref); showCard(ref, e); return;
      }
      clearHl(); hideCard();
    });
    svg.addEventListener("mouseleave", () => { if (!state.pinned) { clearHl(); hideCard(); } });
    svg.addEventListener("click", (e) => {
      // hierarchical-sheet box: descend into that child sheet
      const sh = e.target.closest(".adom-sheet-link");
      if (sh) { openSheet(sh.getAttribute("data-sheet")); return; }
      const c = e.target.closest(".adom-comp");
      // click empty space = deselect here AND in the sibling PCB view
      if (!c) { state.pinned = null; clearSym(); hideCard(); xclear(); return; }
      const ref = c.getAttribute("data-ref");
      state.pinned = state.pinned === ref ? null : ref;
      // pin: keep the card exactly where the hover card already sits, and expand
      // it to the full detail view so the links become reachable + cross-probe it.
      // Un-pinning (toggling off) deselects the PCB too.
      if (state.pinned) { compHl(ref); showCard(ref, e); xpost(ref); } else { clearSym(); hideCard(); xclear(); }
    });
  }

  // ── hierarchical sheet navigation ─────────────────────────────────────────
  // When every sheet is bundled (window.__SHEETS__, injected by `embed --sheet-dir`),
  // switching sheets is INSTANT: we just re-mount the pre-rendered svg/meta, no
  // reload, no fetch. The top-left is a dropdown of all sheets; sheet boxes are
  // also click-through. Without a bundle we fall back to ?sheet= URL navigation.
  function setQuery(key, val) {
    const u = new URL(window.location.href);
    if (val == null) u.searchParams.delete(key); else u.searchParams.set(key, val);
    return u.pathname + u.search;
  }
  function openSheet(file) {
    if (!file) return;
    if (state.sheets && state.sheets[file]) { mountSheet(file); return; } // instant
    window.location.href = setQuery("sheet", file); // fallback: server-rendered
  }
  // mount a bundled sheet in-place (key: filename, or "__root__")
  function mountSheet(key) {
    const s = state.sheets && state.sheets[key]; if (!s) return;
    state.curSheet = key === "__root__" ? null : key;
    mount(s.svg, s.meta);
    toast("Sheet: " + curSheetLabel(), "info");
  }
  // the sheet list is the ROOT's children (so you can jump anywhere from anywhere)
  function rootSubs() {
    const r = state.sheets && state.sheets.__root__;
    return (r && r.meta && r.meta.sub_sheets) || state.meta.sub_sheets || [];
  }
  function buildCrumb() {
    const el = document.getElementById("crumb"); if (!el) return;
    const subs = rootSubs();
    if (!subs.length) { el.style.display = "none"; return; }
    // a dropdown of every sheet (Root + children); selecting one switches instantly
    const cur = state.curSheet || "__root__";
    const opt = (v, label) => '<option value="' + escapeHtml(v) + '"' + (v === cur ? " selected" : "") + ">" + escapeHtml(label) + "</option>";
    const opts = [opt("__root__", "Root")].concat(subs.map((s) => opt(s.file, s.name || s.file.replace(/\.kicad_sch$/, ""))));
    el.innerHTML = '<span class="sheetlbl">Sheet</span><select id="sheetsel">' + opts.join("") + "</select>";
    el.style.display = "flex";
    // both the dropdown and the components panel live top-right; push the panel
    // below the dropdown so they don't overlap (the panel is hidden on small panes)
    const side = document.getElementById("side"); if (side) side.style.top = "52px";
    const sel = el.querySelector("#sheetsel");
    if (sel) sel.addEventListener("change", (e) => {
      const v = e.target.value;
      if (state.sheets && state.sheets[v]) mountSheet(v);
      else window.location.href = setQuery("sheet", v === "__root__" ? null : v);
    });
  }

  function comp(ref) { return (state.meta.components || []).find((k) => k.reference === ref); }
  // Abridged while merely hovering on a small/embedded screen (APM's board pane):
  // reference · MPN · stock, nothing else. Pinning always shows the full card.
  function isBrief(ref) { return state.pinned !== ref && innerWidth <= 620; }
  function showCard(ref, e) {
    const c = comp(ref); if (!c) return;
    const card = $("#card");
    const pinned = state.pinned === ref, brief = isBrief(ref);
    const rows = [];
    if (brief) {
      rows.push('<div class="ci-h"><span class="ci-ref">' + escapeHtml(ref) + "</span></div>");
      // real MPN arrives with enrichment; show the schematic value meanwhile
      rows.push('<div class="ci-mfr" id="ci-mpn">' + escapeHtml(c.value || "") + "</div>");
      rows.push('<div class="ci-enrich" id="ci-enrich">' + (c.lcsc || c.value ? '<span class="muted">stock…</span>' : "") + "</div>");
    } else {
      rows.push('<div class="ci-h"><span class="ci-ref">' + escapeHtml(ref) + '</span><span class="ci-val">' + escapeHtml(c.value || "") + "</span></div>");
      if (c.description) rows.push('<div class="ci-desc">' + escapeHtml(c.description) + "</div>");
      if (c.footprint) rows.push(row("footprint", c.footprint.split(":").pop()));
      if (c.lcsc) rows.push(row("lcsc", c.lcsc));
      rows.push('<div class="ci-enrich" id="ci-enrich">' + (c.lcsc || c.value ? '<span class="muted">loading stock · price…</span>' : "") + "</div>");
      if (c.datasheet && c.datasheet !== "~") rows.push('<a class="ci-ds" href="' + escapeHtml(c.datasheet) + '" target="_blank" rel="noopener">datasheet ↗</a>');
    }
    card.innerHTML = rows.join("");
    card.classList.toggle("brief", brief);
    card.style.display = "block";
    // clickable only when pinned, so hovering never eats the pointer
    card.classList.toggle("pinned", pinned);
    // follow the cursor while hovering; once pinned it stays put where it is —
    // but the pinned card is bigger than the brief one it replaced, so pull it
    // back inside the viewport if that growth pushed it off an edge.
    if (!pinned) placeCard(card, e || state.mouse); else clampCard(card);
    state.briefMode = brief;
    if (c.lcsc || c.value) enrich(c.value, c.lcsc);
  }
  // place near the cursor, flipping to the other side at the viewport edges
  function placeCard(card, e) {
    if (!e) { card.style.left = "12px"; card.style.top = "12px"; return; }
    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";
  }
  // keep an already-placed card fully inside the viewport
  function clampCard(card) {
    const w = card.offsetWidth, h = card.offsetHeight;
    const x = parseFloat(card.style.left) || 0, y = parseFloat(card.style.top) || 0;
    card.style.left = Math.max(4, Math.min(x, innerWidth - w - 4)) + "px";
    card.style.top = Math.max(4, Math.min(y, innerHeight - h - 4)) + "px";
  }
  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 && !state.pinned) { c.style.display = "none"; c.classList.remove("pinned", "brief"); } }

  // ── enrichment (stock/price via LCSC PN, wiki page + popularity) ──────────
  function enrich(value, lcsc) {
    const key = (value || "") + "|" + (lcsc || "");
    const render = (d) => {
      const el = $("#ci-enrich"); if (!el || state.enrichKey !== key) return;
      // abridged card: the real MPN + stock, nothing else
      if (state.briefMode) {
        const m = $("#ci-mpn");
        if (m && d && d.mpn) m.textContent = d.mpn;
        if (!d || d.error || d.stock == null) { el.innerHTML = '<span class="muted">no stock data</span>'; return; }
        el.innerHTML = '<div class="pills"><span class="pill ' + (d.stock > 0 ? "ok" : "no") + '">' + (d.stock > 0 ? d.stock + " in stock" : "no stock") + "</span></div>";
        return;
      }
      if (!d || d.error) { el.innerHTML = '<span class="muted">no stock/price found</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 = "";
      if (d.mpn) html += '<div class="ci-mfr">' + escapeHtml(d.mpn) + "</div>";
      if (d.mfr) html += '<div class="ci-mfr muted">' + escapeHtml(d.mfr) + "</div>";
      html += '<div class="pills">' + parts.join("") + "</div>";
      const links = [];
      if (d.jlc_url) links.push('<a class="ci-lnk" href="' + escapeHtml(d.jlc_url) + '" target="_blank" rel="noopener">JLCPCB ↗</a>');
      if (d.wiki_url) links.push('<a class="ci-lnk" href="' + escapeHtml(d.wiki_url) + '" target="_blank" rel="noopener">wiki page ↗</a>');
      if (links.length) html += '<div class="ci-links">' + links.join("") + "</div>";
      el.innerHTML = html || '<span class="muted">no data</span>';
    };
    state.enrichKey = key;
    if (state.enrichCache[key]) return render(state.enrichCache[key]);
    // NOTE: relative "enrich" resolves against whatever serves the page — our own
    // /enrich when running standalone, or the HOST app's (APM's
    // /board-view/<slug>/enrich) when embedded. So embeds get live stock too.
    if (typeof fetch !== "function") return render(null);
    const url = "enrich?mpn=" + encodeURIComponent(value || "") + (lcsc ? "&lcsc=" + encodeURIComponent(lcsc) : "");
    let tries = 0;
    const go = () => fetch(url).then((r) => r.json()).then((d) => {
      if (d && d.pending && tries++ < 25) { setTimeout(go, 1500); return; }
      state.enrichCache[key] = d; render(d);
    }).catch(() => render(null));
    go();
  }

  // ── pan / zoom ────────────────────────────────────────────────────────────
  // A schematic sheet is ~4500 nodes, so every viewBox write re-rasterizes a lot.
  // Coalesce writes to ONE per animation frame: a burst of wheel/drag events then
  // costs a single repaint per frame instead of 2-3, which is what makes panning
  // and zooming a dense sheet feel smooth.
  function wirePanZoom(svg) {
    let vbPending = false;
    const flushVB = () => {
      if (vbPending) return;
      vbPending = true;
      requestAnimationFrame(() => { vbPending = false; svg.setAttribute("viewBox", state.vb.join(" ")); });
    };
    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, 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)), nh = nw * (h / w);
      state.vb = [mx - (mx - x) * (nw / w), my - (my - y) * (nh / h), nw, nh];
      flushVB();
    }, { passive: false });
    let drag = null;
    // cache the rect at grab time — it can't change mid-drag, so we avoid a forced
    // layout (getBoundingClientRect) on every single pan frame.
    svg.addEventListener("mousedown", (e) => {
      const r = svg.getBoundingClientRect();
      drag = { x: e.clientX, y: e.clientY, vb: state.vb.slice(), rw: r.width, rh: r.height };
    });
    addEventListener("mouseup", () => (drag = null));
    addEventListener("mousemove", (e) => {
      if (!drag) return;
      // preserveAspectRatio="…meet" scales the viewBox UNIFORMLY to fit the box, so
      // there is ONE viewBox-units-per-pixel factor for both axes: max(vbW/rw, vbH/rh)
      // (the larger ratio is the limiting one under "meet"). Using vbW/rw for x but
      // vbH/rh for y made horizontal and vertical drag move at different rates on any
      // non-matching aspect (i.e. every wide schematic).
      const upp = Math.max(drag.vb[2] / drag.rw, drag.vb[3] / drag.rh);
      state.vb = [drag.vb[0] - (e.clientX - drag.x) * upp, drag.vb[1] - (e.clientY - drag.y) * upp, drag.vb[2], drag.vb[3]];
      flushVB();
    });
  }
  function fit() { if (state.svg) { state.vb = state.vb0.slice(); state.svg.setAttribute("viewBox", state.vb.join(" ")); } }
  function fitToast() { fit(); toast("Fit to view", "info"); }

  // ── component list ────────────────────────────────────────────────────────
  function buildCompList() {
    const el = $("#complist"); if (!el) return;
    const cs = (state.meta.components || []).slice().sort((a, b) => a.reference.localeCompare(b.reference, undefined, { numeric: true }));
    el.innerHTML = cs.map((c) => '<div class="crow" data-ref="' + escapeHtml(c.reference) + '"><b>' + escapeHtml(c.reference) + "</b> " + escapeHtml(c.value || "") + "</div>").join("") || '<div class="muted">no components</div>';
    el.querySelectorAll(".crow").forEach((r) => {
      const ref = r.getAttribute("data-ref");
      r.addEventListener("mouseenter", () => { if (!state.pinned) compHl(ref); });
      r.addEventListener("mouseleave", () => { if (!state.pinned) clearHl(); });
      r.addEventListener("click", () => { state.pinned = state.pinned === ref ? null : ref; if (state.pinned) { compHl(ref); showCard(ref); } else { clearSym(); hideCard(); } });
    });
  }
  function setStat() {
    const n = state.meta.components ? state.meta.components.length : 0;
    const el = $("#stat"); if (el) el.textContent = n + " components";
    setSubject(curSheetLabel(), n + (n === 1 ? " component" : " components"));
  }

  // ── helpers ───────────────────────────────────────────────────────────────
  function escapeHtml(s) { return String(s).replace(/[&<>"]/g, (c) => ({ "&": "&amp;", "<": "&lt;", ">": "&gt;", '"': "&quot;" }[c])); }
  function cssEsc(s) { return String(s).replace(/["\\]/g, "\\$&"); }

  window.AdomSch = { mount, fit, compHl, toast, runCmd };
  function boot() {
    $("#fitbtn") && $("#fitbtn").addEventListener("click", fitToast);
    $("#hdr-fit") && $("#hdr-fit").addEventListener("click", fitToast);
    state.sheets = window.__SHEETS__ || null; // pre-rendered bundle for instant nav
    if (window.__SCH__) { window.__EMBED__ = true; state.curSheet = null; mount(window.__SCH__.svg, window.__SCH__.meta); return; }
    // standalone live server: enable the AI channel (toasts + drive cmds), the
    // console ring buffer, and fill the header's app-info 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 schematic loaded. POST a .kicad_sch to /load.</div>';
    }).catch(() => { $("#svgbox").innerHTML = '<div class="empty">server unreachable</div>'; });
  }
  if (document.readyState === "loading") document.addEventListener("DOMContentLoaded", boot); else boot();
})();
</script>
</body>
</html>