← Commit history

Footprint viewer regenerated with adom-footprint: 300 um solder balls hidden by default, same positions as the hero

John Lauer ·634d4fa3e3 ·16d ago ·parent dc71e82
1 file changed +204−799
CR0402-FX-4701GLF-fpview.html+204−799
@@ -1,805 +1,210 @@-<!DOCTYPE html>-<html lang="en">-<head>-<meta charset="UTF-8">-<meta name="viewport" content="width=device-width, initial-scale=1.0">-<title>CR0402-FX-4701GLF</title>+<!doctype html><html><head><meta charset="utf-8">+<title>CR0402-FX-4701GLF - adom-footprint</title>+<meta name="viewport" content="width=device-width,initial-scale=1"> <style>-  * { margin: 0; padding: 0; box-sizing: border-box; }-  body {-    background: #0d1117;-    font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;-    overflow: hidden;-    height: 100vh;-    display: flex;-    flex-direction: column;-    color: #e6edf3;-  }--  .toolbar {-    display: flex;-    align-items: center;-    gap: 6px;-    padding: 8px 14px;-    background: #161b22;-    border-bottom: 1px solid #21262d;-    flex-shrink: 0;-    font-size: 13px;-  }-  .toolbar .title {-    font-weight: 600;-    color: #00b8b0;-    font-size: 15px;-    letter-spacing: 0.02em;-    overflow: hidden;-    text-overflow: ellipsis;-    white-space: nowrap;-    min-width: 0;-  }-  .toolbar .badge {-    font-size: 10px;-    padding: 2px 6px;-    border-radius: 4px;-    background: rgba(0, 184, 176, 0.12);-    color: #00b8b0;-    border: 1px solid rgba(0, 184, 176, 0.25);-    margin-left: 6px;-  }-  .toolbar .spacer { flex: 1; }-  .toolbar .sep {-    width: 1px;-    height: 22px;-    background: #21262d;-    margin: 0 4px;-    flex-shrink: 0;-  }-  .toolbar .btn-group {-    display: flex;-    align-items: center;-    gap: 2px;-    flex-shrink: 0;-  }-  .toolbar button {-    background: rgba(0, 184, 176, 0.12);-    border: 1px solid rgba(0, 184, 176, 0.25);-    border-radius: 4px;-    padding: 4px 10px;-    cursor: pointer;-    font-size: 12px;-    color: #8b949e;-    transition: all 0.15s;-  }-  .toolbar button:hover { background: rgba(0,184,176,0.18); color: #e6edf3; border-color: rgba(0,184,176,0.4); }-  .toolbar button:active { background: rgba(0,184,176,0.28); }-  .toolbar button.active { background: rgba(0,184,176,0.2); color: #00b8b0; border-color: rgba(0,184,176,0.4); }-  .toolbar .zoom-level {-    font-size: 11px;-    color: #8b949e;-    min-width: 44px;-    text-align: center;-    font-variant-numeric: tabular-nums;-  }--  .canvas-wrap {-    flex: 1;-    overflow: hidden;-    position: relative;-    cursor: grab;-    background-color: #0d1117;-    background-image:-      linear-gradient(rgba(255, 255, 255, 0.03) 1px, transparent 1px),-      linear-gradient(90deg, rgba(255, 255, 255, 0.03) 1px, transparent 1px);-    background-size: 20px 20px;-  }-  .canvas-wrap.dragging { cursor: grabbing; }-  .canvas-wrap.no-grid { background-image: none; }--  .canvas {-    position: absolute;-    transform-origin: 0 0;-  }-  .canvas svg { display: block; }--  .tooltip {-    display: none;-    position: fixed;-    background: #21262d;-    backdrop-filter: blur(12px);-    color: #e6edf3;-    padding: 10px 14px;-    border-radius: 8px;-    border: 1px solid rgba(0, 184, 176, 0.25);-    font-size: 12px;-    line-height: 1.6;-    pointer-events: none;-    z-index: 100;-    max-width: 280px;-    box-shadow: 0 8px 24px rgba(0,0,0,0.4);-  }-  .tooltip.visible { display: block; }-  .tooltip .tt-pad { font-weight: 700; font-size: 15px; color: #00e6dc; }-  .tooltip .tt-signal { color: #f0c040; font-size: 13px; margin-left: 6px; font-weight: 600; }-  .tooltip .tt-dims {-    display: inline-block;-    margin-top: 4px;-    padding: 2px 8px;-    border-radius: 4px;-    font-size: 11px;-    font-weight: 500;-    background: rgba(200,52,52,0.15);-    color: #C83434;-    border: 1px solid rgba(200,52,52,0.3);-  }-  .tooltip .tt-desc {-    margin-top: 6px;-    padding-top: 6px;-    border-top: 1px solid #21262d;-    color: #8b949e;-    font-size: 11px;-    line-height: 1.5;-  }-  .tooltip.sticky {-    pointer-events: auto;-    user-select: text;-    border-color: #00e6dc;-  }-  .tooltip .tt-hint {-    margin-top: 6px;-    font-size: 9px;-    color: #484f58;-    text-align: center;-  }-  .tooltip .tt-source {-    margin-top: 6px;-    padding-top: 4px;-    border-top: 1px solid #21262d;-    font-size: 9px;-  }-  .tooltip .tt-source-label { color: #8C6BF7; }-  .tooltip .tt-source a { color: #58a6ff; text-decoration: none; }-  .tooltip .tt-source a:hover { text-decoration: underline; }--  .edit-btn {-    position: absolute; top: 10px; right: 10px; z-index: 100;-    display: flex; align-items: center; gap: 6px;-    padding: 5px 10px;-    background: rgba(22,27,34,0.75);-    backdrop-filter: blur(8px);-    border: 1px solid rgba(140,107,247,0.2);-    border-radius: 6px;-    color: #8b949e; font-size: 11px;-    cursor: pointer; text-decoration: none;-    transition: all 0.2s; opacity: 0.7;-  }-  .edit-btn:hover {-    opacity: 1;-    background: rgba(140,107,247,0.12);-    border-color: rgba(140,107,247,0.4);-    color: #C5B3FF;-  }-  .edit-btn svg { width: 12px; height: 12px; }--  /* Regen prompt modal */-  .regen-overlay {-    display: none;-    position: fixed; inset: 0;-    background: rgba(0,0,0,0.6);-    z-index: 2000;-    align-items: center; justify-content: center;-  }-  .regen-overlay.visible { display: flex; }-  .regen-modal {-    background: #161b22;-    border: 1px solid #21262d;-    border-radius: 10px;-    padding: 20px 24px;-    max-width: 520px;-    width: 90%;-    box-shadow: 0 12px 40px rgba(0,0,0,0.5);-  }-  .regen-modal h3 {-    margin: 0 0 12px;-    font-size: 14px;-    font-weight: 600;-    color: #00b8b0;-  }-  .regen-modal .prompt-text {-    background: undefined;-    border: 1px solid #21262d;-    border-radius: 6px;-    padding: 12px 14px;-    font-size: 13px;-    line-height: 1.5;-    color: undefined;-    white-space: pre-wrap;-    word-break: break-word;-    user-select: all;-  }-  .regen-modal .modal-footer {-    display: flex;-    align-items: center;-    justify-content: space-between;-    margin-top: 14px;-  }-  .regen-modal .copy-status {-    font-size: 11px;-    color: #484f58;-  }-  .regen-modal .copy-status.copied { color: #00b8b0; }-  .regen-modal .modal-btns { display: flex; gap: 8px; }-  .regen-modal .modal-btns button {-    padding: 6px 14px;-    border-radius: 6px;-    border: 1px solid #21262d;-    background: #161b22;-    color: #8b949e;-    font-size: 12px;-    cursor: pointer;-  }-  .regen-modal .modal-btns button:hover { background: rgba(255,255,255,0.06); }-  .regen-modal .modal-btns .copy-btn {-    background: rgba(0,184,176,0.12);-    border-color: rgba(0,184,176,0.3);-    color: #00b8b0;-  }-  .regen-modal .modal-btns .copy-btn:hover { background: rgba(0,184,176,0.22); }--  /* Pad highlight */-  .pad-highlight {-    stroke: #00b8b0 !important;-    stroke-width: 0.15 !important;-    fill-opacity: 0.6 !important;-  }--  .bottom-panel {-    background: #161b22;-    border-top: 1px solid #21262d;-    flex-shrink: 0;-  }-  .info-row {-    display: flex;-    align-items: baseline;-    flex-wrap: wrap;-    gap: 1px 0;-    padding: 5px 8px;-    font-size: 10px;-  }-  .info-item {-    display: inline-flex;-    align-items: baseline;-    gap: 5px;-    padding: 1px 0;-    margin-right: 8px;-  }-  .info-item::after {-    content: '';-    display: inline-block;-    width: 1px;-    height: 10px;-    background: #21262d;-    margin-left: 8px;-    vertical-align: middle;-  }-  .info-item:last-child::after { display: none; }-  .info-item:last-child { margin-right: 0; }-  .info-label {-    color: #484f58;-    text-transform: uppercase;-    letter-spacing: 0.04em;-    font-size: 9px;-  }-  .info-value { color: #8b949e; }-  .info-value a { color: #00b8b0; text-decoration: none; }-  .info-value a:hover { text-decoration: underline; }-  .info-divider {-    height: 1px;-    background: #21262d;-    margin: 0 8px;-  }-  .coord-row {-    display: flex;-    align-items: center;-    justify-content: space-between;-    padding: 4px 8px;-    font-size: 10px;-    color: #8b949e;-    font-variant-numeric: tabular-nums;-  }-</style>-</head>+/* ── adom-footprint shared viewer core (CSS) ───────────────────────────────+   Included VERBATIM by both the live app (app.html) and the static embed+   (embed.html) so the canvas, zoom toolbar and layer-panel rows look identical+   and cannot drift. Self-contained literal colours (no dependency on either+   file's CSS vars). Class prefix: av- */++.av-vp{position:absolute;inset:0;overflow:hidden;cursor:grab;background:radial-gradient(circle at 50% 42%,#121a26 0%,#0d1117 72%)}+.av-vp.grab{cursor:grabbing}+/* DIRECT child only — the zoom-toolbar button icons (.av-tools svg) also live+   inside .av-vp; without '>' they'd inherit position:absolute and stack at 0,0. */+.av-vp > svg{position:absolute;left:0;top:0;width:100%;height:100%;transform-origin:0 0;display:block}++/* zoom toolbar */+/* Grouped translucent toolbar (matches adom-symbol's .tools): one container, borderless+   buttons that highlight on hover — instead of individually-boxed buttons. */+.av-tools{position:absolute;left:10px;top:10px;display:flex;gap:3px;padding:3px;z-index:5;background:rgba(22,27,34,.82);backdrop-filter:blur(10px);-webkit-backdrop-filter:blur(10px);border:1px solid #222b38;border-radius:9px}+.av-tools button{width:30px;height:30px;border-radius:6px;border:none;background:transparent;color:#cfd8e2;cursor:pointer;display:grid;place-items:center;transition:background .15s ease,color .15s ease}+.av-tools button svg{width:16px;height:16px;display:block}+.av-tools button:hover{background:#222b38;color:#37c8a6}+.av-tools button:focus,.av-tools button:focus-visible{outline:none}+.av-panhint{position:absolute;right:10px;bottom:9px;color:#8b949e;font-size:11px;z-index:5;pointer-events:none;user-select:none;-webkit-user-select:none}++/* layer panel rows */+.av-lrow{display:flex;align-items:center;gap:9px;padding:8px 9px;border-radius:8px;cursor:pointer}+.av-lrow:hover{background:#161c27}+.av-lrow .sw{width:13px;height:13px;border-radius:3px;border:1px solid rgba(255,255,255,.15);flex:0 0 13px}+.av-lrow .sw.ring{background:transparent!important}+.av-lrow .ln{flex:1;font-size:13px}+.av-lrow .meta{font-size:10px;color:#8b949e}+.av-lrow .chk{width:16px;height:16px;border-radius:4px;border:1.5px solid #3a4757;display:grid;place-items:center;color:#0b0f17;font-size:11px}+.av-lrow.on .chk{background:#37c8a6;border-color:#37c8a6}+.av-lrow.off .ln,.av-lrow.off .sw{opacity:.4}+.av-lrow.special{background:linear-gradient(100deg,rgba(55,200,166,.10),transparent);border:1px solid rgba(55,200,166,.25);margin-top:4px}+.av-lrow.special .ln{color:#7ef0d2;font-weight:600}++:root{--bg:#0d1117;--panel:#11161f;--border:#222b38;--text:#e6edf3;--muted:#8b949e;--accent:#37c8a6}+*{box-sizing:border-box;margin:0;padding:0}html,body{height:100%}+body{background:var(--bg);color:var(--text);font-family:'Satoshi','Segoe UI',system-ui,sans-serif;display:flex;flex-direction:column;overflow:hidden}+header{display:flex;align-items:center;gap:9px;padding:9px 14px;border-bottom:1px solid var(--border);flex:0 0 auto}+header .dot{width:9px;height:9px;border-radius:50%;background:var(--accent);box-shadow:0 0 10px var(--accent)}+header .b{font-weight:700;font-size:14px}header .b span{color:var(--muted);font-weight:400}+header .nm{margin-left:auto;font-weight:600;font-size:13px;color:#cfd8e2}+main{flex:1;display:flex;min-height:0}+.vpwrap{flex:1;position:relative;min-width:0}+aside{flex:0 0 248px;border-left:1px solid var(--border);background:var(--panel);display:flex;flex-direction:column;min-height:0}+aside .ah{padding:9px 12px;border-bottom:1px solid var(--border);font-size:12px;color:var(--muted)}+.layers{padding:6px}+.note{font-size:11px;color:var(--muted);line-height:1.45;padding:8px 11px}+.note b{color:#7ef0d2}+</style></head> <body>--<div class="toolbar">-  <span class="title">CR0402-FX-4701GLF</span>-  <span class="badge">Footprint</span>-  -  <div class="spacer"></div>-  <div class="btn-group">-    <button onclick="zoomOut()">&#x2212;</button>-    <span class="zoom-level" id="zoom-level">100%</span>-    <button onclick="zoomIn()">&#x2b;</button>-  </div>-  <div class="sep"></div>-  <button onclick="zoomFit()">Fit</button>-  <button onclick="zoomReset()">1:1</button>-  <button onclick="toggleGrid()" id="grid-btn" class="active">Grid</button>-  <button onclick="toggleSolder()" id="solder-btn" class="active" title="250µm solder jet blobs for Adom InstaPCB paste jetting. Adom uses this placement data for its jetting sequence. Blob count, position, and size (250–600µm) are adjustable. Placement accuracy: ±50µm.">Solder</button>-  <button onclick="toggleLabels()" id="labels-btn">Labels</button>-  <div class="sep"></div>-  <button onclick="showRegenPrompt()" id="regen-btn" title="Show regeneration prompt to paste into Claude Code">Regen</button>-</div>--<div class="regen-overlay" id="regen-overlay" onclick="if(event.target===this)closeRegenPrompt()">-  <div class="regen-modal">-    <h3>Regeneration Prompt</h3>-    <div class="prompt-text" id="regen-prompt-text"></div>-    <div class="modal-footer">-      <span class="copy-status" id="regen-copy-status">Copied to clipboard</span>-      <div class="modal-btns">-        <button onclick="closeRegenPrompt()">Close</button>-        <button class="copy-btn" onclick="copyRegenText()">Copy</button>+  <header><span class="dot"></span><span class="b">adom-footprint <span>· interactive</span></span><span class="nm">CR0402-FX-4701GLF</span></header>+  <main>+    <div class="vpwrap">+      <div class="av-vp" id="vp">+        <svg xmlns="http://www.w3.org/2000/svg" viewBox="-2.000 -1.500 4.000 3.000" preserveAspectRatio="xMidYMid meet"><g class="adom-layer-courtyard"><rect x="-1.000" y="-0.500" width="2.000" height="1.000" fill="none" stroke="#FF26E2" stroke-width="0.05" stroke-dasharray="0.2,0.1"/></g><g class="adom-layer-fab"><rect x="-0.500" y="-0.250" width="1.000" height="0.500" fill="none" stroke="#6e7681" stroke-width="0.1"/></g><g class="adom-layer-copper"><g data-pad="1" data-shape="rect" data-size="0.500x0.500" data-mount="smd"><rect x="-0.750" y="-0.250" width="0.500" height="0.500" fill="#C83434" stroke="#ef5350" stroke-width="0.04"/></g><g data-pad="2" data-shape="rect" data-size="0.500x0.500" data-mount="smd"><rect x="0.250" y="-0.250" width="0.500" height="0.500" fill="#C83434" stroke="#ef5350" stroke-width="0.04"/></g></g><g class="adom-layer-heatsink"></g><g class="adom-layer-drill"></g><g class="adom-layer-silk"><line x1="-0.165" y1="-0.360" x2="0.165" y2="-0.360" stroke="#00cccc" stroke-width="0.12" stroke-linecap="round"/><line x1="-0.165" y1="0.360" x2="0.165" y2="0.360" stroke="#00cccc" stroke-width="0.12" stroke-linecap="round"/></g><g class="adom-layer-padnum"><text x="-0.500" y="0.000" text-anchor="middle" dominant-baseline="central" font-size="0.210" fill="#fff" font-family="sans-serif" style="pointer-events:none">1</text><text x="0.500" y="0.000" text-anchor="middle" dominant-baseline="central" font-size="0.210" fill="#fff" font-family="sans-serif" style="pointer-events:none">2</text></g><g class="adom-layer-paste-apertures"><rect x="-0.750" y="-0.250" width="0.500" height="0.500" rx="0.05" transform="rotate(0.00 -0.500 0.000)" fill="none" stroke="#8aa0b8" stroke-width="0.03" stroke-dasharray="0.12 0.08" opacity="0.7"/><rect x="0.250" y="-0.250" width="0.500" height="0.500" rx="0.05" transform="rotate(0.00 0.500 0.000)" fill="none" stroke="#8aa0b8" stroke-width="0.03" stroke-dasharray="0.12 0.08" opacity="0.7"/></g><g class="adom-layer-paste-dots"><circle cx="-0.500" cy="0.000" r="0.150" fill="#37c8a6" opacity="0.92"/><circle cx="0.500" cy="0.000" r="0.150" fill="#37c8a6" opacity="0.92"/></g></svg>       </div>     </div>-  </div>-</div>--<div class="canvas-wrap" id="canvas-wrap">-  <div class="canvas" id="canvas">-    <svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 4 3">-<rect x="0" y="0" width="4" height="3" fill="#0d1117"/>-<rect x="1" y="1" width="2" height="1" fill="none" stroke="#FF26E2" stroke-width="0.05" stroke-dasharray="0.2,0.1"/>-<rect x="1.5" y="1.25" width="1" height="0.5" fill="none" stroke="#6e7681" stroke-width="0.1"/>-<line x1="1.835" y1="1.14" x2="2.165" y2="1.14" stroke="#00cccc" stroke-width="0.12" stroke-linecap="round"/><line x1="1.835" y1="1.86" x2="2.165" y2="1.86" stroke="#00cccc" stroke-width="0.12" stroke-linecap="round"/>-<rect x="1.25" y="1.25" width="0.5" height="0.5" rx="0" ry="0" fill="#C83434" stroke="#ef5350" stroke-width="0.04"/>-<text x="1.5" y="1.5" text-anchor="middle" dominant-baseline="central" font-size="0.225" fill="#fff" font-family="sans-serif">1</text>-<rect x="2.25" y="1.25" width="0.5" height="0.5" rx="0" ry="0" fill="#C83434" stroke="#ef5350" stroke-width="0.04"/>-<text x="2.5" y="1.5" text-anchor="middle" dominant-baseline="central" font-size="0.225" fill="#fff" font-family="sans-serif">2</text>-</svg>-  </div>-  <a class="edit-btn" href="https://coder.john-service-wiki-a054078430ce38e4.containers.adom.inc/proxy/8785/wiki/components/cr0402-fx-4701glf/edit?field=content" target="_blank" title="Suggest corrections on Adom Wiki">-    <svg viewBox="0 0 16 16" fill="none" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round">-      <path d="M11.5 1.5l3 3L5 14H2v-3L11.5 1.5z"/>-      <path d="M9.5 3.5l3 3"/>-    </svg>-    Suggest edit-  </a>-</div>--<div class="tooltip" id="tooltip"></div>--<div class="bottom-panel">-  <div class="info-row">-    -    -    <div class="info-item"><span class="info-label">Desc</span><span class="info-value">Land pattern: Bourns CR Series Thick Film Chip Resistors datasheet, REV. 03/21, p.2 (Recommended Pad Layout: a 0.50, b 1.50, c 0.50, pad inner edge on the 0.25 mm bottom terminal)</span></div><div class="info-item"><span class="info-label">Pads</span><span class="info-value">2 &middot; 0.5 &times; 0.5 mm, r 0 mm</span></div><div class="info-item"><span class="info-label">Body</span><span class="info-value">1 &times; 0.5 mm</span></div><div class="info-item"><span class="info-label">Courtyard</span><span class="info-value">2 &times; 1 mm</span></div>-  </div>-  <div class="info-divider"></div>-  <div class="coord-row">-    <span id="cursor-pos">X: 0.00 &nbsp; Y: 0.00 mm</span>-  </div>-</div>--<script>-  const pads = [{"number": "1", "signal": "", "x": -0.5, "y": 0.0, "width": 0.5, "height": 0.5, "shape": "roundrect", "mountType": "smd", "rotation": 0, "desc": ""}, {"number": "2", "signal": "", "x": 0.5, "y": 0.0, "width": 0.5, "height": 0.5, "shape": "roundrect", "mountType": "smd", "rotation": 0, "desc": ""}];-  const svgW = 4;-  const svgH = 3;-  const svgVbX = 0;-  const svgVbY = 0;-  const offsetX = 2;-  const offsetY = 1.5;-  const datasheetUrl = "";-  const _partName = "";-  const _fpName = "CR0402-FX-4701GLF";-  const _kicadModPath = "/home/adom/project/project-content/schematics/footprints/CR0402-FX-4701GLF/CR0402-FX-4701GLF.kicad_mod";--  const wrap = document.getElementById('canvas-wrap');-  const canvas = document.getElementById('canvas');-  const tooltip = document.getElementById('tooltip');-  const zoomLabel = document.getElementById('zoom-level');-  const cursorPos = document.getElementById('cursor-pos');--  let scale = 1, panX = 0, panY = 0;-  let isDragging = false;-  let dragStartX, dragStartY, panStartX, panStartY;-  let gridVisible = true;--  const MM_TO_PX = 3.78;-  const svgPxW = svgW * MM_TO_PX;-  const svgPxH = svgH * MM_TO_PX;--  const svgEl = canvas.querySelector('svg');-  if (svgEl) {-    svgEl.setAttribute('width', svgPxW + 'px');-    svgEl.setAttribute('height', svgPxH + 'px');-  }--  function updateTransform() {-    canvas.style.transform = 'translate(' + panX + 'px,' + panY + 'px) scale(' + scale + ')';-    zoomLabel.textContent = Math.round(scale * 100) + '%';-  }--  function zoomFit() {-    const ww = wrap.clientWidth, wh = wrap.clientHeight;-    if (ww === 0 || wh === 0) return; // iframe not laid out yet — ResizeObserver will retry-    const margin = 24;-    // Use getBBox() to fit to visible content only (excludes hidden labels)-    let fitW = svgPxW, fitH = svgPxH, fitX = 0, fitY = 0;-    if (svgEl) {-      try {-        const bb = svgEl.getBBox();-        if (bb.width > 0 && bb.height > 0) {-          fitW = bb.width * MM_TO_PX;-          fitH = bb.height * MM_TO_PX;-          fitX = (bb.x - svgVbX) * MM_TO_PX;-          fitY = (bb.y - svgVbY) * MM_TO_PX;-        }-      } catch {}-    }-    scale = Math.min((ww - margin * 2) / fitW, (wh - margin * 2) / fitH, 100);-    panX = (ww - fitW * scale) / 2 - fitX * scale;-    panY = (wh - fitH * scale) / 2 - fitY * scale;-    updateTransform();-  }--  function zoomReset() {-    scale = 1;-    panX = (wrap.clientWidth - svgPxW) / 2;-    panY = (wrap.clientHeight - svgPxH) / 2;-    updateTransform();-  }--  function zoomIn() { zoomAt(wrap.clientWidth / 2, wrap.clientHeight / 2, 1.25); }-  function zoomOut() { zoomAt(wrap.clientWidth / 2, wrap.clientHeight / 2, 0.8); }--  function zoomAt(cx, cy, factor) {-    const ns = Math.max(0.1, Math.min(scale * factor, 40));-    const r = ns / scale;-    panX = cx - (cx - panX) * r;-    panY = cy - (cy - panY) * r;-    scale = ns;-    updateTransform();-  }--  function toggleGrid() {-    gridVisible = !gridVisible;-    wrap.classList.toggle('no-grid', !gridVisible);-    document.getElementById('grid-btn').classList.toggle('active', gridVisible);-  }--  // ── Solder blob jetting layer ──-  let solderVisible = true;-  let solderGroup = null;--  function setupSolderBlobs() {-    if (!svgEl) return;--    solderGroup = document.createElementNS('http://www.w3.org/2000/svg', 'g');-    solderGroup.setAttribute('id', 'solder-layer');-    solderGroup.style.display = '';-    solderGroup.style.pointerEvents = 'none';--    const BLOB_RADIUS = 0.125;   // 250µm diameter / 2-    const BLOB_SPACING = 0.25;   // center-to-center (blobs touching — fills the pad)--    pads.forEach(pad => {-      if (pad.mountType !== 'smd') return;--      const cx = pad.x + offsetX;-      const cy = pad.y + offsetY;-      const rot = (pad.rotation || 0) * Math.PI / 180;--      // Each axis computed independently: if pad is wide enough, grid; otherwise single centered blob-      const inset = BLOB_RADIUS;  // blob edges align with pad edges — no overhang-      const availW = pad.width - 2 * inset;-      const availH = pad.height - 2 * inset;--      // At minimum 1 blob per axis — even narrow pads get a centered row/column-      const nCols = availW > 0 ? Math.max(1, Math.floor(availW / BLOB_SPACING) + 1) : 1;-      const nRows = availH > 0 ? Math.max(1, Math.floor(availH / BLOB_SPACING) + 1) : 1;-      const spanW = availW > 0 ? availW : 0;-      const spanH = availH > 0 ? availH : 0;-      const stepX = nCols > 1 ? spanW / (nCols - 1) : 0;-      const stepY = nRows > 1 ? spanH / (nRows - 1) : 0;-      const startX = nCols > 1 ? -spanW / 2 : 0;-      const startY = nRows > 1 ? -spanH / 2 : 0;-      const cosR = Math.cos(rot);-      const sinR = Math.sin(rot);--      for (let row = 0; row < nRows; row++) {-        for (let col = 0; col < nCols; col++) {-          const lx = startX + col * stepX;-          const ly = startY + row * stepY;-          const rx = lx * cosR - ly * sinR;-          const ry = lx * sinR + ly * cosR;--          const circle = document.createElementNS('http://www.w3.org/2000/svg', 'circle');-          circle.setAttribute('cx', (cx + rx).toFixed(4));-          circle.setAttribute('cy', (cy + ry).toFixed(4));-          circle.setAttribute('r', BLOB_RADIUS.toFixed(4));-          circle.setAttribute('fill', '#D4AA70');-          circle.setAttribute('fill-opacity', '0.75');-          circle.setAttribute('stroke', '#B8860B');-          circle.setAttribute('stroke-width', '0.015');-          solderGroup.appendChild(circle);-        }-      }-    });--    svgEl.appendChild(solderGroup);-  }--  function toggleSolder() {-    solderVisible = !solderVisible;-    if (solderGroup) solderGroup.style.display = solderVisible ? '' : 'none';-    document.getElementById('solder-btn').classList.toggle('active', solderVisible);-  }--  // ── Label visibility toggle ──-  let labelsVisible = false;-  let labelGroups = [];--  function setupLabelToggle() {-    if (!svgEl) return;-    // KiCad SVGs render labels as <g> containing <text> + <g class="stroked-text">-    // Find all <text> elements and collect their parent <g> groups-    svgEl.querySelectorAll('text').forEach(txt => {-      const parent = txt.parentElement;-      if (parent && parent.tagName === 'g' && !labelGroups.includes(parent)) {-        labelGroups.push(parent);-        parent.style.display = 'none';  // hidden by default-      }-    });-  }--  function toggleLabels() {-    labelsVisible = !labelsVisible;-    labelGroups.forEach(g => g.style.display = labelsVisible ? '' : 'none');-    document.getElementById('labels-btn').classList.toggle('active', labelsVisible);-  }--  // ── Regen prompt modal ──-  let _regenPrompt = '';-  function buildRegenPrompt() {-    const name = _partName || _fpName;-    const folder = _kicadModPath.replace(/\/[^/]+$/, '');-    return 'Regenerate the FpView for ' + name + '.\n\n'-      + 'Footprint: ' + _kicadModPath + '\n'-      + 'Metadata: ' + folder + '/\n\n'-      + 'Re-export the SVG via the KiCad service, regenerate the FpView HTML at the same path, '-      + 'and re-display it.';-  }-  function showRegenPrompt() {-    _regenPrompt = buildRegenPrompt();-    document.getElementById('regen-prompt-text').textContent = _regenPrompt;-    document.getElementById('regen-overlay').classList.add('visible');-    const status = document.getElementById('regen-copy-status');-    navigator.clipboard.writeText(_regenPrompt).then(() => {-      status.textContent = 'Copied to clipboard';-      status.classList.add('copied');-    }).catch(() => {-      status.textContent = 'Click Copy to copy';-      status.classList.remove('copied');-    });-  }-  function closeRegenPrompt() {-    document.getElementById('regen-overlay').classList.remove('visible');-  }-  function copyRegenText() {-    navigator.clipboard.writeText(_regenPrompt).then(() => {-      const status = document.getElementById('regen-copy-status');-      status.textContent = 'Copied to clipboard';-      status.classList.add('copied');+    <aside>+      <div class="ah">Layers</div>+      <div class="layers" id="layers"></div>+      <div class="note">The <b>solder paste jet</b> layer shows the 300µm paste-dispensing dots that travel with the footprint. Off by default — toggle it on.</div>+    </aside>+  </main>+<script>/* ── adom-footprint shared viewer core (window.AV) ─────────────────────────+   Single source of truth for the layer list, toggle behaviour, defaults,+   pan/zoom and toolbar icons. Included VERBATIM by both app.html (live) and+   embed.html (static) so the two viewers cannot drift. Pure client-side; works+   on any SVG that carries the <g class="adom-layer-*"> groups. */+(function(){+  var ICON_ZIN='<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><circle cx="10.5" cy="10.5" r="6.5"/><path d="M20 20l-4.5-4.5M10.5 8v5M8 10.5h5"/></svg>';+  var ICON_ZOUT='<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><circle cx="10.5" cy="10.5" r="6.5"/><path d="M20 20l-4.5-4.5M8 10.5h5"/></svg>';+  // Zoom-to-FIT = a magnifier with a small square inside (fit content to view).+  // Deliberately NOT the four-corner expand/"fullscreen" glyph.+  var ICON_FIT='<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><circle cx="10.5" cy="10.5" r="6.5"/><path d="M20 20l-4.5-4.5"/><rect x="7.6" y="7.6" width="5.8" height="5.8" rx="1.2"/></svg>';++  // The canonical layer list. Every layer is a <g class="adom-layer-*"> in the+  // self-rendered SVG, so toggling is group-based (robust) — never colour-match.+  // `kind:'group'` on every entry: all layers are toggled by their <g> group+  // (robust), never by colour-match. (app.html still reads def.kind.)+  var LAYER_DEFS=[+    {key:'copper',    name:'Copper',           kind:'group', sel:'.adom-layer-copper',          sw:'#C83434'},+    // Auto-generated thermal-via array under the exposed pad — only present (and+    // listed) when the footprint has an exposed thermal pad.+    {key:'heatsink',  name:'Heatsink vias',    kind:'group', sel:'.adom-layer-heatsink',        sw:'#e8a13a'},+    {key:'padnum',    name:'Pad numbers',      kind:'group', sel:'.adom-layer-padnum',          sw:'#ffffff', ring:true},+    {key:'silk',      name:'Silkscreen',       kind:'group', sel:'.adom-layer-silk',            sw:'#00cccc'},+    {key:'fab',       name:'Fab (F.Fab)',      kind:'group', sel:'.adom-layer-fab',             sw:'#6e7681', ring:true},+    {key:'courtyard', name:'Courtyard',        kind:'group', sel:'.adom-layer-courtyard',       sw:'#FF26E2', ring:true},+    {key:'drill',     name:'Drill',            kind:'group', sel:'.adom-layer-drill',           sw:'#888888', ring:true},+    {key:'paste-ap',  name:'Paste apertures',  kind:'group', sel:'.adom-layer-paste-apertures', sw:'#8aa0b8', ring:true},+    // Generic name — the 300µm solder-paste dispensing dots. Off by default.+    {key:'paste-dots',name:'Solder paste jet', kind:'group', sel:'.adom-layer-paste-dots',      sw:'#37c8a6', special:true},+  ];++  // True only if the group actually holds drawn geometry (not an empty <g>) — so+  // layers with nothing in THIS footprint aren't listed as dead, dim rows.+  function groupHasGeo(svg,def){+    if(!svg) return false; var n=0,g=svg.querySelectorAll(def.sel);+    for(var i=0;i<g.length;i++){ n+=g[i].querySelectorAll('path,rect,circle,line,polygon,polyline,text').length; }+    return n>0;+  }+  function layerPresent(svg,def){ return groupHasGeo(svg,def); }+  function applyLayer(svg,def,on){+    if(!svg) return; var g=svg.querySelectorAll(def.sel);+    for(var i=0;i<g.length;i++) g[i].style.display=on?'':'none';+  }++  // Build layer rows into a container. opts:+  //   svg, container, state {key:bool}, meta {pasteDots,pasteApertures},+  //   onToggle(key,on). Missing state key -> special OFF, others ON. Skips empty+  //   non-special groups. Returns the keys actually shown.+  function buildLayers(opts){+    var svg=opts.svg, box=opts.container, state=opts.state||{}, meta=opts.meta||{}, shown=[];+    box.innerHTML='';+    LAYER_DEFS.forEach(function(def){+      if(!def.special && !groupHasGeo(svg,def)) return;+      var on = def.special ? (state[def.key]===true) : (state[def.key]!==false);+      var m = def.key==='paste-dots' ? ((meta.pasteDots||0)+' @ 300µm')+            : def.key==='paste-ap'   ? ((meta.pasteApertures||0)+' openings') : '';+      var row=document.createElement('div');+      row.className='av-lrow '+(on?'on':'off')+(def.special?' special':'');+      row.innerHTML='<span class="sw'+(def.ring?' ring':'')+'" style="'+(def.ring?('border-color:'+def.sw):('background:'+def.sw))+'"></span>'+        +'<span class="ln">'+def.name+'</span>'+(m?'<span class="meta">'+m+'</span>':'')+        +'<span class="chk">'+(on?'✓':'')+'</span>';+      row.onclick=function(){+        var nv=!row.classList.contains('on');+        row.classList.toggle('on',nv); row.classList.toggle('off',!nv);+        row.querySelector('.chk').textContent=nv?'✓':'';+        state[def.key]=nv; applyLayer(svg,def,nv);+        if(opts.onToggle) opts.onToggle(def.key,nv);+      };+      box.appendChild(row); applyLayer(svg,def,on); shown.push(def.key);     });-  }--  wrap.addEventListener('wheel', (e) => {-    e.preventDefault();-    const r = wrap.getBoundingClientRect();-    zoomAt(e.clientX - r.left, e.clientY - r.top, e.deltaY < 0 ? 1.12 : 0.89);-  }, { passive: false });--  wrap.addEventListener('mousedown', (e) => {-    if (e.button !== 0) return;-    isDragging = true;-    dragStartX = e.clientX; dragStartY = e.clientY;-    panStartX = panX; panStartY = panY;-    wrap.classList.add('dragging');-  });-  window.addEventListener('mousemove', (e) => {-    const r = wrap.getBoundingClientRect();-    // Convert screen coords to kicad_mod coords-    const svgX = (e.clientX - r.left - panX) / scale / MM_TO_PX;-    const svgY = (e.clientY - r.top - panY) / scale / MM_TO_PX;-    const kicadX = svgX - offsetX;-    const kicadY = svgY - offsetY;-    cursorPos.innerHTML = 'X: ' + kicadX.toFixed(2) + ' &nbsp; Y: ' + kicadY.toFixed(2) + ' mm';-    if (!isDragging) return;-    panX = panStartX + (e.clientX - dragStartX);-    panY = panStartY + (e.clientY - dragStartY);-    updateTransform();-  });-  window.addEventListener('mouseup', () => { isDragging = false; wrap.classList.remove('dragging'); });--  // ── Pad hit zones ──-  let highlightedRects = [];-  let stickyPad = null;--  function setupPadHitZones() {-    if (!svgEl) return;-    pads.forEach(pad => {-      const cx = pad.x + offsetX;-      const cy = pad.y + offsetY;-      const rx = cx - pad.width / 2;-      const ry = cy - pad.height / 2;--      const rect = document.createElementNS('http://www.w3.org/2000/svg', 'rect');-      rect.setAttribute('x', rx);-      rect.setAttribute('y', ry);-      rect.setAttribute('width', pad.width);-      rect.setAttribute('height', pad.height);-      rect.setAttribute('fill', 'transparent');-      rect.setAttribute('stroke', 'none');-      rect.setAttribute('data-pad', pad.number);-      rect.style.cursor = 'pointer';-      rect.style.pointerEvents = 'all';-      svgEl.appendChild(rect);--      rect.addEventListener('mouseenter', (ev) => showPadTooltip(ev.clientX, ev.clientY, pad, rect));-      rect.addEventListener('mousemove', (ev) => showPadTooltip(ev.clientX, ev.clientY, pad, rect));-      rect.addEventListener('mouseleave', () => hidePadTooltip(rect));-      rect.addEventListener('click', (ev) => { ev.stopPropagation(); handlePadClick(pad, rect); });-    });-  }--  function buildTooltipHtml(pad) {-    let html = '<div><span class="tt-pad">Pad ' + pad.number + '</span>';-    if (pad.signal) html += '<span class="tt-signal">' + pad.signal + '</span>';-    html += '</div>';-    html += '<div><span class="tt-dims">' + pad.width.toFixed(3) + ' × ' + pad.height.toFixed(3) + ' mm · ' + pad.shape + ' · ' + pad.mountType + '</span></div>';-    if (pad.desc) {-      html += '<div class="tt-desc">' + pad.desc + '</div>';-    }-    if (datasheetUrl) {-      html += '<div class="tt-source"><span class="tt-source-label">Source:</span> <a href="' + datasheetUrl + '" target="_blank">Datasheet</a></div>';-    }-    html += '<div class="tt-hint">click to pin · select text</div>';-    return html;-  }--  function positionTooltip(x, y) {-    // Render first to measure-    tooltip.style.left = '0px';-    tooltip.style.top = '0px';-    const ttRect = tooltip.getBoundingClientRect();-    const ttW = ttRect.width, ttH = ttRect.height;-    const pad = 14;-    let tx = x + 14, ty = y + 14;-    if (tx + ttW > window.innerWidth - pad) tx = x - ttW - 14;-    if (ty + ttH > window.innerHeight - pad) ty = y - ttH - 14;-    tx = Math.max(pad, Math.min(tx, window.innerWidth - ttW - pad));-    ty = Math.max(pad, Math.min(ty, window.innerHeight - ttH - pad));-    tooltip.style.left = tx + 'px';-    tooltip.style.top = ty + 'px';-  }--  /* lbr-hover-bus v1 */ window.addEventListener('message',function(e){var d=e.data||{};if(d.type!=='lbr-hover')return;clearHighlight();if(d.pin==null||!svgEl)return;svgEl.querySelectorAll('rect[data-pad]').forEach(function(r){if(r.getAttribute('data-pad')===String(d.pin)){r.setAttribute('stroke','#00b8b1');r.setAttribute('stroke-width','0.12');r.setAttribute('fill','rgba(0,184,177,0.2)');highlightedRects.push(r);}});});-  function showPadTooltip(x, y, pad, rect) {-    try{parent.postMessage({type:'lbr-hover',pin:String(pad.number)},'*')}catch(_){}-    -    if (stickyPad !== null) return; // suppress hover while sticky--    clearHighlight();-    rect.setAttribute('stroke', '#00b8b1');-    rect.setAttribute('stroke-width', '0.12');-    rect.setAttribute('fill', 'rgba(0,184,177,0.2)');-    highlightedRects.push(rect);--    tooltip.innerHTML = buildTooltipHtml(pad);-    tooltip.classList.add('visible');-    tooltip.classList.remove('sticky');-    positionTooltip(x, y);-  }--  function hidePadTooltip(rect) { try{parent.postMessage({type:'lbr-hover',pin:null},'*')}catch(_){}-    if (stickyPad !== null) return; // don't hide while sticky-    tooltip.classList.remove('visible');-    clearHighlight();-  }--  function handlePadClick(pad, rect) {-    if (stickyPad === pad.number) {-      // Unpin-      stickyPad = null;-      tooltip.classList.remove('sticky', 'visible');-      clearHighlight();-    } else {-      // Pin this pad-      stickyPad = pad.number;-      clearHighlight();-      rect.setAttribute('stroke', '#00b8b1');-      rect.setAttribute('stroke-width', '0.12');-      rect.setAttribute('fill', 'rgba(0,184,177,0.2)');-      highlightedRects.push(rect);-      tooltip.innerHTML = buildTooltipHtml(pad);-      tooltip.classList.add('visible', 'sticky');-      // Remove hint when sticky-      const hint = tooltip.querySelector('.tt-hint');-      if (hint) hint.style.display = 'none';-    }-  }--  // Dismiss sticky tooltip on click outside-  document.addEventListener('click', (e) => {-    if (stickyPad !== null && !tooltip.contains(e.target)) {-      stickyPad = null;-      tooltip.classList.remove('sticky', 'visible');-      clearHighlight();-    }-  });--  function clearHighlight() {-    highlightedRects.forEach(r => {-      r.setAttribute('fill', 'transparent');-      r.setAttribute('stroke', 'none');-    });-    highlightedRects = [];-  }--  wrap.addEventListener('mouseleave', () => {-    if (stickyPad !== null) return;-    tooltip.classList.remove('visible');-    clearHighlight();-  });--  // Keyboard-  window.addEventListener('keydown', (e) => {-    if (e.key === 'Escape') closeRegenPrompt();-    else if (e.key === 'f' || e.key === 'F') zoomFit();-    else if (e.key === '1') zoomReset();-    else if (e.key === '+' || e.key === '=') zoomIn();-    else if (e.key === '-') zoomOut();-    else if (e.key === 'g' || e.key === 'G') toggleGrid();-    else if (e.key === 's' || e.key === 'S') toggleSolder();-    else if (e.key === 'l' || e.key === 'L') toggleLabels();-  });--  // Screenshot capture via postMessage (parent viewer sends mgmt_capture_request)-  window.addEventListener('message', (e) => {-    if (e.data?.type === 'mgmt_capture_request' && svgEl) {-      const xml = new XMLSerializer().serializeToString(svgEl);-      parent.postMessage({-        type: 'mgmt_canvas_capture',-        _reqId: e.data._reqId,-        svgXml: xml,-        svgWidth: svgW,-        svgHeight: svgH,-        bgColor: 'undefined',-      }, '*');-    }-  });--  // Auto-fit on window/container resize-  new ResizeObserver(() => zoomFit()).observe(wrap);-  window.addEventListener('resize', () => requestAnimationFrame(zoomFit));--  // Init — set up layers then fit. Use multiple retries because srcdoc iframes-  // may not have layout dimensions during the first requestAnimationFrame.-  let inited = false;-  function init() {-    if (inited) return;-    setupLabelToggle();-    setupSolderBlobs();-    setupPadHitZones();-    inited = true;-  }-  function initAndFit() {-    init();-    zoomFit();-  }-  requestAnimationFrame(initAndFit);-  setTimeout(initAndFit, 100);-  setTimeout(initAndFit, 500);+    return shown;+  }++  // Wire pan + zoom on a viewport element (the box that contains the <svg>).+  // Returns a controller {fit, zoomIn, zoomOut, apply}.+  function initViewport(vp){+    var svg=vp.querySelector('svg');+    if(svg){ svg.removeAttribute('width'); svg.removeAttribute('height'); }+    var s=1,tx=0,ty=0;+    function apply(){ var el=vp.querySelector('svg'); if(el) el.style.transform='translate('+tx+'px,'+ty+'px) scale('+s+')'; }+    function zoomAt(cx,cy,f){ tx=cx-(cx-tx)*f; ty=cy-(cy-ty)*f; s*=f; apply(); }+    function fit(){ s=1;tx=0;ty=0;apply(); }+    // Smooth, delta-PROPORTIONAL zoom (matches adom-symbol). A fixed per-event+    // factor makes the trackpad leap — one gentle two-finger nudge fires a burst+    // of tiny wheel events and a fixed 1.12× compounds. Instead derive the factor+    // from how much actually scrolled (exp → smooth + reversible), normalise+    // device units, and clamp one event so a chunky mouse notch can't jump.+    var ZOOM_K=0.0022; // sensitivity: lower = gentler+    vp.addEventListener('wheel',function(e){+      e.preventDefault();+      var d=e.deltaY;+      if(e.deltaMode===1) d*=16; else if(e.deltaMode===2) d*=vp.clientHeight;+      d=Math.max(-50,Math.min(50,d));+      var r=vp.getBoundingClientRect();+      zoomAt(e.clientX-r.left,e.clientY-r.top,Math.exp(-d*ZOOM_K));+    },{passive:false});+    var drag=false,lx=0,ly=0;+    vp.addEventListener('pointerdown',function(e){ if(e.target.closest('.av-tools'))return; drag=true;lx=e.clientX;ly=e.clientY;vp.classList.add('grab');vp.setPointerCapture(e.pointerId); });+    vp.addEventListener('pointermove',function(e){ if(!drag)return; tx+=e.clientX-lx;ty+=e.clientY-ly;lx=e.clientX;ly=e.clientY;apply(); });+    vp.addEventListener('pointerup',function(){ drag=false;vp.classList.remove('grab'); });+    fit();+    return {+      fit:fit,+      zoomIn:function(){ var r=vp.getBoundingClientRect(); zoomAt(r.width/2,r.height/2,1.2); },+      zoomOut:function(){ var r=vp.getBoundingClientRect(); zoomAt(r.width/2,r.height/2,1/1.2); },+      apply:apply+    };+  }++  // Inject the standard zoom toolbar (+ / − / fit) + pan hint into a viewport,+  // wired to a controller from initViewport. Idempotent.+  function addToolbar(vp,ctl){+    if(vp.querySelector('.av-tools')) return;+    var t=document.createElement('div'); t.className='av-tools';+    t.innerHTML='<button data-z="in" title="Zoom in">'+ICON_ZIN+'</button>'+      +'<button data-z="out" title="Zoom out">'+ICON_ZOUT+'</button>'+      +'<button data-z="fit" title="Zoom to fit">'+ICON_FIT+'</button>';+    vp.appendChild(t);+    t.querySelector('[data-z="in"]').onclick=ctl.zoomIn;+    t.querySelector('[data-z="out"]').onclick=ctl.zoomOut;+    t.querySelector('[data-z="fit"]').onclick=ctl.fit;+    var h=document.createElement('div'); h.className='av-panhint'; h.textContent='drag to pan · scroll to zoom'; vp.appendChild(h);+  }++  window.AV={+    LAYER_DEFS:LAYER_DEFS, groupHasGeo:groupHasGeo, layerPresent:layerPresent,+    applyLayer:applyLayer, buildLayers:buildLayers, initViewport:initViewport,+    addToolbar:addToolbar, ICON_FIT:ICON_FIT, ICON_ZIN:ICON_ZIN, ICON_ZOUT:ICON_ZOUT+  };+})(); </script>--</body>-</html>\ No newline at end of file+<script>+var META={"pasteApertures":2,"pasteDots":2};+(function(){+  var vp=document.getElementById('vp');+  var svg=vp.querySelector('svg');+  var ctl=AV.initViewport(vp);+  AV.addToolbar(vp,ctl);+  AV.buildLayers({ svg:svg, container:document.getElementById('layers'), state:{}, meta:META });+})();+</script>+<style>/* lbr-hover-bus v1 */ g[data-pad]{cursor:pointer} g[data-pad].lbr-hi rect,g[data-pad].lbr-hi circle,g[data-pad].lbr-hi path,g[data-pad].lbr-hi ellipse{stroke:#00e6dc!important;stroke-width:.12!important;fill:#e8505b!important;filter:drop-shadow(0 0 .25px #00e6dc)}</style><script>/* lbr-hover-bus v1 */(function(){function clr(){document.querySelectorAll('g[data-pad].lbr-hi').forEach(function(g){g.classList.remove('lbr-hi')})}function hi(n){clr();if(n==null)return;document.querySelectorAll('g[data-pad]').forEach(function(g){if(g.getAttribute('data-pad')===String(n))g.classList.add('lbr-hi')})}function post(n){try{parent.postMessage({type:'lbr-hover',pin:n==null?null:String(n)},'*')}catch(_){}}function wire(){document.querySelectorAll('g[data-pad]').forEach(function(g){if(g.__lbr)return;g.__lbr=1;var n=g.getAttribute('data-pad');g.addEventListener('mouseenter',function(){hi(n);post(n)});g.addEventListener('mouseleave',function(){clr();post(null)})})}addEventListener('message',function(e){var d=e.data||{};if(d.type==='lbr-hover')hi(d.pin)});if(document.readyState==='loading')addEventListener('DOMContentLoaded',wire);else wire();setTimeout(wire,800)})();</script></body></html>