app
Component Widgets
Public Made by Adomby adom
Reusable component widgets for pinouts, thermal vias, molecule relationships, reviewed chip outlines and lightweight four-level LOD cache listings.
master
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348
/*!
* <adom-pinout> — a reusable, dependency-free pinout widget.
*
* Drop-in:
* <script src="pinout-widget.js"></script>
* <adom-pinout src="part.json"></adom-pinout>
* <adom-pinout><script type="application/json">{ ...pin data... }</script></adom-pinout>
* // or: document.querySelector('adom-pinout').data = { ... }
*
* Data contract (everything except pins[].name is optional):
* {
* "mpn": "STM32F103C8T6", // label shown in the chip body
* "package": "LQFP-48", // label + hints the inferred layout (quad vs dual)
* "pins": [
* { "number": 1, "name": "VBAT", // number+name are the essentials
* "type": "power", // power|io|clock|ctrl|other (auto-inferred from name if omitted)
* "xy": [-2.75, 4.25] } // mm pad centre (footprint frame, +y up) — best placement.
* // omit xy → placed by inferred quad/dual layout.
* ]
* }
*
* Attributes: src (fetch JSON), layout ("auto"|"quad"|"dual"), heading (override title).
* Self-contained (Shadow DOM), themes to the host via prefers-color-scheme.
*/
(() => {
const TYPE_COLORS = {
power: '#ff5c5c', io: '#00d3c8', clock: '#4c9dff', ctrl: '#b57cff', control: '#b57cff', other: '#8b97ac',
};
const TYPE_LABEL = { power: 'power', io: 'I/O', clock: 'clock', ctrl: 'control', control: 'control', other: 'other' };
function inferType(name) {
const n = String(name || '').toUpperCase();
if (n === 'NC' || n === '') return 'other';
if (/^(VDD|VSS|VCC|GND|VBAT|VDDA|VSSA|AVDD|AVSS|VREF|VEE|VBUS|V\+|V-|AGND|DGND)/.test(n)) return 'power';
if (/(OSC|XTAL|CLKIN|CLKOUT|\bCLK\b|SCLK|MCLK)/.test(n)) return 'clock';
if (/^(NRST|~?RESET|RST|BOOT|EN\b|~?CE|~?CS|MODE|TEST|WAKE|SHDN|~?SHDN|PDN)/.test(n)) return 'ctrl';
return 'io';
}
// Exposed / thermal pad (QFN/DFN/SON/PowerPAD) — belongs in the chip centre, not on an edge.
const isCenterPad = p => {
const n = String(p.name || '').trim().toUpperCase();
return /^(EP|EPAD|PAD|TAB|THERMAL|E[-_]?PAD|EXP|EXPPAD)$/.test(n) || /EXPOSED\s*PAD|THERMAL\s*PAD/.test(n);
};
// Return each pin as { ...pin, x, y, side } in a S×S box. Center pads get side:'center'.
function layout(pins, pkg, forced) {
const S = 400, pad = 60, mid = S / 2;
const centers = pins.filter(isCenterPad);
const edge = pins.filter(p => !isCenterPad(p));
let laid;
const hasXY = edge.length > 0 && edge.every(p => Array.isArray(p.xy) && p.xy.length === 2);
if (hasXY && forced !== 'quad' && forced !== 'dual') {
const xs = edge.map(p => p.xy[0]), ys = edge.map(p => p.xy[1]);
const minx = Math.min(...xs), maxx = Math.max(...xs), miny = Math.min(...ys), maxy = Math.max(...ys);
const span = Math.max(maxx - minx, maxy - miny) || 1, sc = (S - 2 * pad) / span;
const cx = (maxx + minx) / 2, cy = (maxy + miny) / 2, off = (v, c) => mid + (v - c) * sc;
laid = edge.map(p => {
const x = off(p.xy[0], cx), y = S - off(p.xy[1], cy); // flip y (mm +y up → screen down)
const side = p.xy[1] === maxy ? 'top' : p.xy[1] === miny ? 'bottom' : p.xy[0] === maxx ? 'right' : 'left';
return { ...p, x, y, side };
});
} else {
// The body is drawn inset 16 from the outermost pins, so a run distributed
// across the whole inner..outer span puts the FIRST and LAST pin of each
// side outside the body: at 12 per side they landed 4.3px past each corner
// and collided with the end pin of the neighbouring side (1 into 48, 12
// into 13, and so on). Real quad packages have a corner keep-out anyway,
// which is why the xy path never showed this. Inset the run to match.
const N = edge.length, inner = pad, outer = S - pad, at = (a, b, t) => a + (b - a) * t;
const CORNER = 26, runLo = inner + CORNER, runHi = outer - CORNER;
const quad = forced === 'quad' || (forced !== 'dual' && /QF|BGA|LGA|LCC|QFN|QFP/i.test(pkg || ''));
if (quad) {
const per = Math.ceil(N / 4);
laid = edge.map((p, i) => {
const s = Math.floor(i / per), t = ((i % per) + 0.5) / per;
if (s === 0) return { ...p, x: inner, y: at(runLo, runHi, t), side: 'left' };
if (s === 1) return { ...p, x: at(runLo, runHi, t), y: outer, side: 'bottom' };
if (s === 2) return { ...p, x: outer, y: at(runHi, runLo, t), side: 'right' };
return { ...p, x: at(runHi, runLo, t), y: inner, side: 'top' };
});
} else {
const half = Math.ceil(N / 2); // DIP-style: left top→bottom, right bottom→top
laid = edge.map((p, i) => i < half
? { ...p, x: inner, y: at(runLo, runHi, (i + 0.5) / half), side: 'left' }
: { ...p, x: outer, y: at(runHi, runLo, (i - half + 0.5) / (N - half)), side: 'right' });
}
}
centers.forEach(p => laid.push({ ...p, x: mid, y: mid, side: 'center' }));
return laid;
}
const esc = s => String(s).replace(/[&<>"]/g, c => ({ '&': '&', '<': '<', '>': '>', '"': '"' }[c]));
class AdomPinout extends HTMLElement {
set data(d) { this._data = d; if (this.shadowRoot) this._render(); }
get data() { return this._data; }
async connectedCallback() {
// A host page that pins its color-scheme (the wiki is dark-only) decides the theme; inline, prefers-color-scheme
// would follow the reader's OS instead (a frame inherits the embedding page's scheme).
if (!this.hasAttribute('theme')) { const cs = getComputedStyle(document.documentElement).colorScheme.trim(); if (/^(only )?(dark|light)( only)?$/.test(cs)) this.setAttribute('theme', cs.replace(/only|\s/g, '')); }
if (this._init) return; this._init = true;
this.attachShadow({ mode: 'open' });
this._busInit();
if (!this._data) {
const inline = this.querySelector('script[type="application/json"]');
if (inline) { try { this._data = JSON.parse(inline.textContent); } catch (e) { /* ignore */ } }
const src = this.getAttribute('src');
if (!this._data && src) { try { this._data = await (await fetch(src)).json(); } catch (e) { this._err = 'could not load ' + src; } }
}
this._render();
}
_render() {
const d = this._data;
const root = this.shadowRoot;
if (this._err || !d || !Array.isArray(d.pins)) {
root.innerHTML = `<style>${this._css()}</style><div class="card"><div class="empty">${esc(this._err || 'no pin data')}</div></div>`;
return;
}
const pins = d.pins.map(p => ({ ...p, type: (p.type || inferType(p.name)).toLowerCase() }));
const laid = layout(pins, d.package, this.getAttribute('layout'));
const typesPresent = [...new Set(pins.map(p => p.type))];
const heading = this.getAttribute('heading') || d.mpn || '';
root.innerHTML = `<style>${this._css()}</style>
<div class="card">
<div class="hd">${heading ? `<span class="mpn">${esc(heading)}</span>` : ''}${d.package ? `<span class="pkg">${esc(d.package)}</span>` : ''}<span class="count">${pins.length} pins</span><span class="tools"><button class="tbtn" data-act="copy" title="Copy the pin map as JSON">copy</button><button class="tbtn" data-act="dl" title="Download the pin map as JSON">JSON ↓</button></span></div>
<div class="body">
<div class="diagram">
<div class="readout" data-readout>hover a pin →</div>
${this._svg(laid, d)}
<div class="legend">${typesPresent.map(t => `<span><i style="background:${TYPE_COLORS[t] || TYPE_COLORS.other}"></i>${TYPE_LABEL[t] || t}</span>`).join('')}</div>
</div>
<div class="tablewrap"><table>
<thead><tr><th>#</th><th>Name</th><th>Type</th></tr></thead>
<tbody>${pins.slice().sort((a, b) => a.number - b.number).map(p => `
<tr data-pin="${esc(p.number)}"><td class="num">${esc(p.number)}</td><td class="nm">${esc(p.name)}</td>
<td><span class="tp" style="color:${TYPE_COLORS[p.type] || TYPE_COLORS.other}">${TYPE_LABEL[p.type] || p.type}</span></td></tr>`).join('')}</tbody>
</table></div>
</div>
</div>`;
this._wire(pins);
}
_svg(laid, d) {
const S = 400;
const edge = laid.filter(p => p.side !== 'center');
const cen = laid.filter(p => p.side === 'center');
const xs = edge.map(p => p.x), ys = edge.map(p => p.y);
const pminX = Math.min(...xs), pmaxX = Math.max(...xs), pminY = Math.min(...ys), pmaxY = Math.max(...ys);
const sides = new Set(edge.map(p => p.side)), hasTB = sides.has('top') || sides.has('bottom');
const bL = pminX + 16, bR = pmaxX - 16, bT = hasTB ? pminY + 16 : pminY - 12, bB = hasTB ? pmaxY - 16 : pmaxY + 12;
const cx = (bL + bR) / 2, cyB = (bT + bB) / 2;
let s = `<svg viewBox="0 0 ${S} ${S}" class="chip">`;
s += `<rect x="${bL}" y="${bT}" width="${bR - bL}" height="${bB - bT}" rx="10" class="pkgbody"/>`;
s += `<circle cx="${bL + 15}" cy="${bT + 15}" r="4" class="pin1"/>`;
const lY = cen.length ? bT + 32 : cyB - 3;
s += `<text x="${cx}" y="${lY}" text-anchor="middle" class="lbl">${esc(d.mpn || '')}</text>`;
if (d.package) s += `<text x="${cx}" y="${lY + 13}" text-anchor="middle" class="lbl2">${esc(d.package)}</text>`;
// center pad(s) — exposed/thermal pad, drawn in the body centre
cen.forEach((p, i) => {
const col = TYPE_COLORS[p.type] || TYPE_COLORS.other;
const ex = cx + (i - (cen.length - 1) / 2) * 40, ey = cyB + (cen.length ? 22 : 0);
s += `<g class="pin" data-pin="${esc(p.number)}">`
+ `<rect class="ep" x="${ex - 19}" y="${ey - 13}" width="38" height="26" rx="4" fill="${col}" fill-opacity="0.15" stroke="${col}"/>`
+ `<text class="ept" x="${ex}" y="${ey + 3}" text-anchor="middle">${esc(p.name)}·${esc(p.number)}</text></g>`;
});
// Hover target. What a pin actually draws is a 2px stub, a 3.1px dot and an
// 8px label, so the pointer has to land nearly on the line to select it.
// Each pin gets a transparent fat line down its whole lane instead, from the
// body edge out past the number, and that is what catches the mouse. The
// width tracks the real pin pitch so a wider target never bleeds into the
// neighbouring pin on a dense package.
const pitch = (() => {
let m = Infinity;
for (const side of ['left', 'right', 'top', 'bottom']) {
const v = edge.filter(q => q.side === side)
.map(q => (side === 'left' || side === 'right') ? q.y : q.x)
.sort((a, b) => a - b);
for (let i = 1; i < v.length; i++) m = Math.min(m, v[i] - v[i - 1]);
}
return isFinite(m) && m > 0 ? m : 16;
})();
const HIT = Math.max(7, Math.min(20, pitch * 0.86));
const REACH = 30; // out past the pin number, which sits 9-14 out
for (const p of edge) {
const col = TYPE_COLORS[p.type] || TYPE_COLORS.other;
let ex = p.x, ey = p.y, lx = p.x, ly = p.y, anc = 'middle';
if (p.side === 'left') { ex = bL; lx = p.x - 11; anc = 'end'; ly = p.y + 3; }
else if (p.side === 'right') { ex = bR; lx = p.x + 11; anc = 'start'; ly = p.y + 3; }
else if (p.side === 'top') { ey = bT; ly = p.y - 9; }
else { ey = bB; ly = p.y + 14; }
let hx = p.x, hy = p.y;
if (p.side === 'left') hx = p.x - REACH;
else if (p.side === 'right') hx = p.x + REACH;
else if (p.side === 'top') hy = p.y - REACH;
else hy = p.y + REACH;
s += `<g class="pin" data-pin="${esc(p.number)}">`
+ `<line class="hit" x1="${ex}" y1="${ey}" x2="${hx}" y2="${hy}" stroke-width="${HIT.toFixed(1)}"/>`
+ `<line class="stub" x1="${p.x}" y1="${p.y}" x2="${ex}" y2="${ey}" stroke="${col}"/>`
+ `<circle class="dot" cx="${p.x}" cy="${p.y}" r="3.1" fill="${col}"/>`
+ `<text class="pn" x="${lx}" y="${ly}" text-anchor="${anc}">${esc(p.number)}</text></g>`;
}
return s + `</svg>`;
}
// Set the highlighted pin. `emit` fires a `pinhover` event (internal hover);
// external callers use highlight() which does NOT emit (avoids feedback loops).
_set(n, emit) {
const root = this.shadowRoot; if (!root) return;
const s = (n == null ? '' : String(n));
root.querySelectorAll('tr[data-pin]').forEach(r => r.classList.toggle('hot', r.dataset.pin === s));
root.querySelectorAll('svg .pin[data-pin]').forEach(g => g.classList.toggle('hot', g.dataset.pin === s));
const p = (this._pinsRendered || []).find(x => String(x.number) === s);
const ro = root.querySelector('[data-readout]');
if (ro) ro.innerHTML = p
? `<b>PIN ${esc(p.number)}</b> · ${esc(p.name)}<span class="rt" style="color:${TYPE_COLORS[p.type] || TYPE_COLORS.other}">${TYPE_LABEL[p.type] || p.type}</span>`
: 'hover a pin →';
if (emit) { this.dispatchEvent(new CustomEvent('pinhover', { detail: p || null, bubbles: true })); this._emitBus(s); }
}
/** Public: drive the highlight from outside (e.g. a linked footprint/symbol widget). */
highlight(n) { this._set(n, false); }
/** Public: the canonical pin map as a plain object — the structured handle for code/AI.
* Types are resolved (inferred filled in) so consumers get a complete map. No fabricated fields. */
toJSON() {
const d = this._data || {};
const pins = (Array.isArray(d.pins) ? d.pins : [])
.map(p => ({ number: p.number, name: p.name, type: (p.type || inferType(p.name)).toLowerCase(), ...(p.xy ? { xy: p.xy } : {}) }))
.sort((a, b) => (Number(a.number) - Number(b.number)) || String(a.number).localeCompare(String(b.number)));
const out = {};
if (d.mpn) out.mpn = d.mpn;
if (d.package) out.package = d.package;
out.pins = pins;
return out;
}
_exportJson() { return JSON.stringify(this.toJSON(), null, 2); }
_exportName() { const d = this._data || {}; return ((d.mpn || 'pinout') + (d.package ? '-' + d.package : '')).replace(/[^\w.-]+/g, '_') + '.pins.json'; }
_download() {
// A data: URL, not a blob:. As a registered widget this renders inside the
// wiki's sandboxed frame, whose CSP has no blob:, so createObjectURL is
// refused at runtime and the button does nothing at all. data: is the only
// download route that works in every host we render in.
const a = document.createElement('a');
a.href = 'data:application/json;charset=utf-8,' + encodeURIComponent(this._exportJson());
a.download = this._exportName();
document.body.appendChild(a); a.click(); a.remove();
}
async _copy(btn) {
const txt = this._exportJson(), done = () => { if (!btn) return; const o = btn.textContent; btn.textContent = 'copied'; btn.classList.add('ok'); setTimeout(() => { btn.textContent = o; btn.classList.remove('ok'); }, 1100); };
try { await navigator.clipboard.writeText(txt); done(); return; } catch (e) { /* sandboxed / denied — fall back */ }
try { const ta = document.createElement('textarea'); ta.value = txt; ta.style.position = 'fixed'; ta.style.opacity = '0'; document.body.appendChild(ta); ta.select(); document.execCommand('copy'); ta.remove(); done(); }
catch (e2) { this._download(); } // last resort: hand them the file
}
// adom-lbr shared cross-highlight bus: BroadcastChannel('lbr-hover') + parent relay.
_busInit() {
this._self = 'w' + Math.random().toString(36).slice(2);
this._bus = ('BroadcastChannel' in window) ? new BroadcastChannel('lbr-hover') : null;
const on = e => { const d = e.data || {}; if (d.type === 'lbr-hover' && d.src !== this._self) this._set(d.pin == null ? '' : d.pin, false); };
if (this._bus) this._bus.onmessage = on;
this._winmsg = on; window.addEventListener('message', on);
}
disconnectedCallback() { try { this._bus && this._bus.close(); } catch (_) { /*noop*/ } if (this._winmsg) window.removeEventListener('message', this._winmsg); }
_emitBus(n) {
const m = { type: 'lbr-hover', pin: (n === '' || n == null) ? null : String(n), mpn: (this._data && this._data.mpn) || '', src: this._self };
try { this._bus && this._bus.postMessage(m); } catch (_) { /*noop*/ }
// parent relay only from the wiki's sandboxed widget frame; inline, BroadcastChannel already reaches every widget
try { if (/^\/[^/]+\/[^/]+\/widget\/[^/]+\/?$/.test(location.pathname)) window.parent.postMessage(m, '*'); } catch (_) { /*noop*/ }
}
_wire(pins) {
this._pinsRendered = pins;
const root = this.shadowRoot;
const dl = root.querySelector('.tbtn[data-act="dl"]'); if (dl) dl.onclick = () => this._download();
const cp = root.querySelector('.tbtn[data-act="copy"]'); if (cp) cp.onclick = () => this._copy(cp);
root.querySelectorAll('tr[data-pin]').forEach(r => { r.onmouseenter = () => this._set(r.dataset.pin, true); r.onmouseleave = () => this._set('', true); });
root.querySelectorAll('svg .pin[data-pin]').forEach(g => { g.onmouseenter = () => this._set(g.dataset.pin, true); g.onmouseleave = () => this._set('', true); });
// Match the pin table's height to the chip diagram column so it fills the
// space and scrolls, instead of a fixed short box next to a taller chip.
const dia = root.querySelector('.diagram'), tw = root.querySelector('.tablewrap');
if (dia && tw) {
const fit = () => { const h = dia.getBoundingClientRect().height; if (h > 40) tw.style.maxHeight = Math.round(h) + 'px'; };
requestAnimationFrame(fit); setTimeout(fit, 60);
try { if (this._ro) this._ro.disconnect(); this._ro = new ResizeObserver(fit); this._ro.observe(dia); } catch (e) { /* */ }
}
}
_css() {
return `
:host{display:block;
--bg:#0d1420;--panel:#121a26;--line:#26324a;--line2:#33425f;--text:#dbe2ec;--dim:#8b97ac;--dim2:#66738c;--accent:#00e6dc;
--hot:rgba(0,230,220,.13)}
@media (prefers-color-scheme:light){:host{--bg:#f6f8fc;--panel:#fff;--line:#dbe2ee;--line2:#c3cede;--text:#1a2230;--dim:#5a6678;--dim2:#8592a6;--accent:#0d8f88;--hot:rgba(13,143,136,.10)}}
:host([theme="dark"]){--bg:#0d1420;--panel:#121a26;--line:#26324a;--line2:#33425f;--text:#dbe2ec;--dim:#8b97ac;--dim2:#66738c;--accent:#00e6dc;--hot:rgba(0,230,220,.13)}
:host([theme="light"]){--bg:#f6f8fc;--panel:#fff;--line:#dbe2ee;--line2:#c3cede;--text:#1a2230;--dim:#5a6678;--dim2:#8592a6;--accent:#0d8f88;--hot:rgba(13,143,136,.10)}
*{box-sizing:border-box}
.card{background:var(--bg);border:1px solid var(--line);border-radius:14px;padding:16px;color:var(--text);
font:14px/1.5 -apple-system,'Segoe UI',Roboto,Arial,sans-serif;container-type:inline-size}
.hd{display:flex;align-items:baseline;gap:10px;margin-bottom:12px}
.hd .mpn{font:700 17px ui-monospace,'SF Mono',Menlo,monospace;letter-spacing:-.2px}
.hd .pkg{font:600 11px ui-monospace,monospace;color:var(--accent);background:color-mix(in srgb,var(--accent) 12%,transparent);border:1px solid color-mix(in srgb,var(--accent) 30%,transparent);padding:2px 8px;border-radius:6px}
.hd .count{margin-left:auto;font:12px ui-monospace,monospace;color:var(--dim2)}
.hd .tools{display:flex;gap:6px;align-self:center}
.tbtn{font:600 10.5px ui-monospace,monospace;letter-spacing:.03em;color:var(--dim);background:var(--panel);
border:1px solid var(--line);border-radius:6px;padding:3px 8px;cursor:pointer;transition:color .12s,border-color .12s,background .12s;white-space:nowrap}
.tbtn:hover{color:var(--text);border-color:var(--line2)}
.tbtn.ok{color:var(--accent);border-color:color-mix(in srgb,var(--accent) 45%,transparent)}
.body{display:grid;grid-template-columns:minmax(300px,1fr) minmax(220px,1fr);gap:20px;align-items:start}
/* stack diagram over table when the WIDGET (not the viewport) is narrow —
e.g. side-by-side in a cross-link row — so the table never clips */
@container (max-width:580px){.body{grid-template-columns:1fr}}
.readout{height:24px;display:flex;align-items:center;gap:9px;font:600 13px ui-monospace,monospace;color:var(--dim);padding:0 4px}
.readout b{color:var(--accent)} .readout .rt{margin-left:auto;font-size:11px;text-transform:uppercase;letter-spacing:.08em}
.diagram{background:var(--panel);border:1px solid var(--line);border-radius:12px;padding:8px}
svg.chip{width:100%;height:auto;display:block;max-height:440px}
.pkgbody{fill:color-mix(in srgb,var(--text) 6%,var(--panel));stroke:var(--line2);stroke-width:1}
.pin1{fill:var(--accent)}
.lbl{fill:var(--dim);font:600 11px ui-monospace,monospace} .lbl2{fill:var(--dim2);font:9px ui-monospace,monospace}
.hit{stroke:transparent;fill:none;stroke-linecap:round;pointer-events:stroke}
.pin .stub{stroke-width:2} .pin .pn{fill:var(--dim2);font:8px ui-monospace,monospace}
.pin:hover .stub,.pin.hot .stub{stroke-width:3.6} .pin:hover .pn,.pin.hot .pn{fill:var(--text);font-weight:700}
.pin .ep{stroke-width:1.2} .pin .ept{fill:var(--dim);font:8px ui-monospace,monospace}
.pin:hover .ep,.pin.hot .ep{fill-opacity:.34;stroke-width:2} .pin:hover .ept,.pin.hot .ept{fill:var(--text);font-weight:700}
.pin{cursor:pointer}
.legend{display:flex;flex-wrap:wrap;gap:13px;margin:8px 2px 2px;font-size:11.5px;color:var(--dim)}
.legend i{display:inline-block;width:9px;height:9px;border-radius:2px;margin-right:5px;vertical-align:middle}
.tablewrap{max-height:440px;overflow:auto;border:1px solid var(--line);border-radius:10px;scrollbar-width:thin;scrollbar-color:var(--line2) transparent}
.tablewrap::-webkit-scrollbar{width:9px;height:9px}
.tablewrap::-webkit-scrollbar-track{background:transparent}
.tablewrap::-webkit-scrollbar-thumb{background:var(--line2);border-radius:9px;border:2px solid transparent;background-clip:padding-box}
.tablewrap::-webkit-scrollbar-thumb:hover{background:var(--dim2);border:2px solid transparent;background-clip:padding-box}
.tablewrap::-webkit-scrollbar-button{display:none;width:0;height:0}
.tablewrap::-webkit-scrollbar-corner{background:transparent}
table{width:100%;border-collapse:collapse;font-size:12.5px}
thead th{position:sticky;top:0;background:var(--panel);text-align:left;color:var(--dim2);font:600 10px ui-monospace,monospace;letter-spacing:.08em;text-transform:uppercase;padding:7px 9px;border-bottom:1px solid var(--line)}
td{padding:4px 9px;border-bottom:1px solid color-mix(in srgb,var(--line) 55%,transparent);font-family:ui-monospace,monospace}
td.num{color:var(--dim2)} td.nm{color:var(--text)}
tr[data-pin]{cursor:default} tr.hot td{background:var(--hot)}
.tp{font-size:10px;text-transform:uppercase;letter-spacing:.05em}
.empty{color:var(--dim2);padding:30px;text-align:center;font-family:ui-monospace,monospace}`;
}
}
if (!customElements.get('adom-pinout')) customElements.define('adom-pinout', AdomPinout);
})();