1234567891011121314151617181920212223242526272829303132333435363738394041424344454647484950515253545556575859606162636465666768697071727374757677787980818283848586878889909192939495969798991001011021031041051061071081091101111121131141151161171181191201211221231241251261271281291301311321331341351361371381391401411421431441451461471481491501511521531541551561571581591601611621631641651661671681691701711721731741751761771781791801811821831841851861871881891901911921931941951961971981992002012022032042052062072082092102112122132142152162172182192202212222232242252262272282292302312322332342352362372382392402412422432442452462472482492502512522532542552562572582592602612622632642652662672682692702712722732742752762772782792802812822832842852862872882892902912922932942952962972982993003013023033043053063073083093103113123133143153163173183193203213223233243253263273283293303313323333343353363373383393403413423433443453463473483493503513523533543553563573583593603613623633643653663673683693703713723733743753763773783793803813823833843853863873883893903913923933943953963973983994004014024034044054064074084094104114124134144154164174184194204214224234244254264274284294304314324334344354364374384394404414424434444454464474484494504514524534544554564574584594604614624634644654664674684694704714724734744754764774784794804814824834844854864874884894904914924934944954964974984995005015025035045055065075085095105115125135145155165175185195205215225235245255265275285295305315325335345355365375385395405415425435445455465475485495505515525535545555565575585595605615625635645655665675685695705715725735745755765775785795805815825835845855865875885895905915925935945955965975985996006016026036046056066076086096106116126136146156166176186196206216226236246256266276286296306316326336346356366376386396406416426436446456466476486496506516526536546556566576586596606616626636646656666676686696706716726736746756766776786796806816826836846856866876886896906916926936946956966976986997007017027037047057067077087097107117127137147157167177187197207217227237247257267277287297307317327337347357367377387397407417427437447457467477487497507517527537547557567577587597607617627637647657667677687697707717727737747757767777787797807817827837847857867877887897907917927937947957967977987998008018028038048058068078088098108118128138148158168178188198208218228238248258268278288298308318328338348358368378388398408418428438448458468478488498508518528538548558568578588598608618628638648658668678688698708718728738748758768778788798808818828838848858868878888898908918928938948958968978988999009019029039049059069079089099109119129139149159169179189199209219229239249259269279289299309319329339349359369379389399409419429439449459469479489499509519529539549559569579589599609619629639649659669679689699709719729739749759769779789799809819829839849859869879889899909919929939949959969979989991000100110021003100410051006100710081009101010111012101310141015101610171018101910201021102210231024102510261027102810291030103110321033103410351036103710381039104010411042104310441045104610471048104910501051105210531054105510561057105810591060106110621063106410651066106710681069107010711072107310741075107610771078107910801081108210831084108510861087108810891090109110921093109410951096109710981099110011011102110311041105110611071108110911101111111211131114111511161117111811191120112111221123112411251126112711281129113011311132113311341135113611371138113911401141114211431144114511461147114811491150115111521153115411551156115711581159116011611162116311641165116611671168116911701171117211731174117511761177117811791180118111821183118411851186118711881189119011911192119311941195119611971198119912001201120212031204120512061207120812091210121112121213121412151216121712181219122012211222122312241225122612271228122912301231123212331234123512361237123812391240124112421243124412451246124712481249125012511252125312541255125612571258125912601261126212631264126512661267126812691270127112721273127412751276127712781279128012811282128312841285128612871288128912901291129212931294129512961297129812991300130113021303130413051306130713081309131013111312131313141315131613171318131913201321132213231324132513261327132813291330133113321333133413351336133713381339134013411342134313441345134613471348134913501351135213531354135513561357135813591360136113621363136413651366136713681369137013711372137313741375137613771378137913801381138213831384138513861387138813891390139113921393139413951396139713981399140014011402140314041405140614071408140914101411141214131414141514161417141814191420142114221423142414251426142714281429143014311432143314341435143614371438143914401441144214431444144514461447144814491450145114521453145414551456145714581459146014611462146314641465146614671468146914701471147214731474147514761477147814791480148114821483148414851486148714881489149014911492149314941495149614971498149915001501150215031504150515061507150815091510151115121513151415151516151715181519152015211522152315241525152615271528152915301531153215331534153515361537153815391540154115421543154415451546154715481549155015511552155315541555155615571558155915601561156215631564156515661567156815691570157115721573157415751576157715781579158015811582158315841585158615871588158915901591159215931594159515961597159815991600160116021603160416051606160716081609161016111612161316141615161616171618161916201621162216231624162516261627162816291630163116321633163416351636163716381639164016411642164316441645164616471648164916501651165216531654165516561657165816591660166116621663166416651666166716681669167016711672167316741675167616771678167916801681168216831684168516861687168816891690169116921693169416951696169716981699170017011702170317041705170617071708170917101711171217131714171517161717171817191720172117221723172417251726172717281729173017311732173317341735173617371738173917401741174217431744174517461747174817491750175117521753175417551756175717581759176017611762176317641765176617671768176917701771177217731774177517761777177817791780178117821783178417851786178717881789179017911792179317941795179617971798179918001801180218031804180518061807180818091810181118121813181418151816181718181819182018211822182318241825182618271828182918301831183218331834183518361837183818391840184118421843184418451846184718481849185018511852185318541855185618571858185918601861186218631864186518661867186818691870187118721873187418751876187718781879188018811882188318841885188618871888188918901891189218931894189518961897189818991900190119021903190419051906190719081909191019111912191319141915191619171918191919201921192219231924192519261927192819291930193119321933193419351936193719381939194019411942194319441945194619471948194919501951195219531954195519561957195819591960196119621963196419651966196719681969197019711972197319741975197619771978197919801981198219831984198519861987198819891990199119921993199419951996199719981999200020012002200320042005200620072008200920102011201220132014201520162017201820192020202120222023202420252026202720282029203020312032203320342035203620372038203920402041204220432044204520462047204820492050205120522053205420552056205720582059206020612062206320642065206620672068206920702071207220732074207520762077207820792080208120822083208420852086208720882089209020912092209320942095209620972098209921002101210221032104210521062107210821092110211121122113211421152116211721182119212021212122212321242125212621272128212921302131213221332134213521362137213821392140214121422143214421452146214721482149215021512152215321542155215621572158215921602161216221632164216521662167216821692170217121722173217421752176217721782179218021812182218321842185218621872188218921902191219221932194219521962197219821992200220122022203220422052206220722082209221022112212221322142215221622172218221922202221222222232224222522262227222822292230223122322233223422352236223722382239224022412242224322442245224622472248224922502251225222532254225522562257225822592260226122622263226422652266226722682269227022712272227322742275227622772278227922802281228222832284228522862287228822892290229122922293229422952296229722982299230023012302230323042305230623072308230923102311231223132314231523162317231823192320232123222323232423252326232723282329233023312332233323342335233623372338233923402341234223432344234523462347234823492350235123522353235423552356235723582359236023612362236323642365236623672368236923702371237223732374237523762377237823792380238123822383238423852386238723882389239023912392239323942395239623972398239924002401240224032404240524062407240824092410241124122413241424152416241724182419242024212422242324242425242624272428242924302431243224332434243524362437243824392440244124422443244424452446244724482449245024512452245324542455245624572458245924602461246224632464246524662467246824692470247124722473247424752476247724782479248024812482248324842485248624872488248924902491249224932494249524962497249824992500250125022503250425052506250725082509251025112512251325142515251625172518251925202521252225232524252525262527252825292530253125322533253425352536253725382539254025412542254325442545254625472548254925502551255225532554255525562557255825592560256125622563256425652566256725682569257025712572257325742575257625772578257925802581258225832584258525862587258825892590259125922593259425952596259725982599260026012602260326042605260626072608260926102611261226132614261526162617261826192620262126222623262426252626262726282629263026312632263326342635263626372638263926402641264226432644264526462647264826492650265126522653265426552656265726582659266026612662266326642665266626672668266926702671267226732674267526762677267826792680268126822683268426852686268726882689269026912692269326942695269626972698269927002701270227032704270527062707270827092710271127122713271427152716271727182719272027212722272327242725272627272728272927302731273227332734273527362737273827392740274127422743274427452746274727482749275027512752275327542755275627572758275927602761276227632764276527662767276827692770277127722773277427752776277727782779278027812782278327842785278627872788278927902791279227932794279527962797279827992800280128022803280428052806280728082809281028112812281328142815281628172818281928202821282228232824282528262827282828292830283128322833283428352836283728382839284028412842284328442845284628472848284928502851285228532854285528562857285828592860286128622863286428652866286728682869287028712872287328742875287628772878287928802881288228832884288528862887288828892890289128922893289428952896289728982899290029012902290329042905290629072908290929102911291229132914291529162917291829192920292129222923292429252926292729282929293029312932293329342935293629372938293929402941294229432944294529462947294829492950295129522953295429552956295729582959296029612962296329642965296629672968296929702971297229732974297529762977297829792980298129822983298429852986298729882989299029912992299329942995299629972998299930003001300230033004300530063007300830093010301130123013301430153016301730183019302030213022302330243025302630273028302930303031303230333034303530363037303830393040304130423043304430453046304730483049305030513052305330543055305630573058305930603061306230633064306530663067306830693070307130723073307430753076307730783079308030813082308330843085308630873088308930903091309230933094309530963097309830993100310131023103310431053106310731083109311031113112311331143115311631173118311931203121312231233124312531263127312831293130313131323133313431353136313731383139314031413142314331443145314631473148314931503151315231533154315531563157315831593160316131623163316431653166316731683169317031713172317331743175317631773178317931803181318231833184318531863187318831893190319131923193319431953196319731983199320032013202320332043205320632073208320932103211321232133214321532163217321832193220322132223223322432253226322732283229323032313232323332343235323632373238323932403241324232433244324532463247324832493250325132523253325432553256325732583259326032613262326332643265326632673268326932703271327232733274327532763277327832793280328132823283328432853286328732883289329032913292329332943295329632973298329933003301330233033304330533063307330833093310331133123313331433153316331733183319332033213322332333243325332633273328332933303331333233333334333533363337333833393340334133423343334433453346334733483349335033513352335333543355335633573358335933603361336233633364336533663367336833693370337133723373337433753376337733783379338033813382338333843385338633873388338933903391339233933394339533963397339833993400340134023403340434053406340734083409341034113412341334143415341634173418341934203421342234233424342534263427342834293430343134323433343434353436343734383439344034413442344334443445344634473448344934503451345234533454345534563457345834593460346134623463346434653466346734683469347034713472347334743475347634773478347934803481348234833484348534863487348834893490349134923493349434953496349734983499350035013502350335043505350635073508350935103511351235133514351535163517351835193520352135223523352435253526352735283529353035313532353335343535353635373538353935403541354235433544354535463547354835493550355135523553355435553556355735583559356035613562356335643565356635673568356935703571357235733574357535763577357835793580358135823583358435853586358735883589359035913592359335943595359635973598359936003601360236033604360536063607360836093610361136123613361436153616361736183619362036213622362336243625362636273628362936303631363236333634363536363637363836393640364136423643364436453646364736483649365036513652365336543655365636573658365936603661366236633664366536663667366836693670367136723673367436753676367736783679368036813682368336843685368636873688368936903691369236933694369536963697369836993700370137023703370437053706370737083709371037113712371337143715371637173718371937203721372237233724372537263727372837293730373137323733373437353736373737383739374037413742374337443745374637473748374937503751375237533754375537563757375837593760376137623763376437653766376737683769377037713772377337743775377637773778377937803781378237833784378537863787378837893790379137923793379437953796379737983799380038013802380338043805380638073808380938103811381238133814381538163817381838193820382138223823382438253826382738283829383038313832383338343835383638373838383938403841384238433844384538463847384838493850385138523853385438553856385738583859386038613862386338643865386638673868386938703871387238733874387538763877387838793880388138823883388438853886388738883889389038913892389338943895389638973898389939003901390239033904390539063907390839093910391139123913391439153916391739183919392039213922392339243925392639273928392939303931393239333934393539363937393839393940394139423943394439453946394739483949395039513952395339543955395639573958395939603961396239633964396539663967396839693970397139723973397439753976397739783979398039813982398339843985398639873988398939903991399239933994399539963997399839994000400140024003400440054006400740084009401040114012401340144015401640174018401940204021402240234024402540264027402840294030403140324033403440354036403740384039404040414042404340444045404640474048404940504051405240534054405540564057405840594060406140624063406440654066406740684069407040714072407340744075407640774078407940804081408240834084408540864087408840894090409140924093409440954096409740984099410041014102410341044105410641074108410941104111411241134114411541164117411841194120412141224123412441254126412741284129413041314132413341344135413641374138413941404141414241434144414541464147414841494150415141524153415441554156415741584159416041614162416341644165416641674168416941704171417241734174417541764177417841794180418141824183418441854186418741884189419041914192419341944195419641974198419942004201420242034204420542064207420842094210421142124213421442154216421742184219422042214222422342244225422642274228422942304231423242334234423542364237423842394240424142424243424442454246424742484249425042514252425342544255425642574258425942604261426242634264426542664267426842694270427142724273427442754276427742784279428042814282428342844285428642874288428942904291429242934294429542964297429842994300430143024303430443054306430743084309431043114312431343144315431643174318431943204321432243234324432543264327432843294330433143324333433443354336433743384339434043414342434343444345434643474348434943504351435243534354435543564357435843594360436143624363436443654366436743684369437043714372437343744375437643774378437943804381438243834384438543864387438843894390439143924393439443954396439743984399440044014402440344044405440644074408440944104411441244134414441544164417441844194420442144224423442444254426442744284429443044314432443344344435443644374438443944404441444244434444444544464447444844494450445144524453445444554456445744584459446044614462446344644465446644674468446944704471447244734474447544764477447844794480448144824483448444854486448744884489449044914492449344944495449644974498449945004501450245034504450545064507450845094510451145124513451445154516451745184519452045214522452345244525452645274528452945304531453245334534453545364537453845394540454145424543454445454546454745484549455045514552455345544555455645574558455945604561456245634564456545664567456845694570457145724573457445754576457745784579458045814582458345844585458645874588458945904591459245934594459545964597459845994600460146024603460446054606460746084609461046114612461346144615461646174618461946204621462246234624462546264627462846294630463146324633463446354636463746384639464046414642464346444645464646474648464946504651465246534654465546564657465846594660466146624663466446654666466746684669467046714672467346744675467646774678467946804681468246834684468546864687468846894690469146924693469446954696469746984699470047014702470347044705470647074708470947104711471247134714471547164717471847194720472147224723472447254726472747284729473047314732473347344735473647374738473947404741474247434744474547464747474847494750475147524753475447554756475747584759476047614762476347644765476647674768476947704771477247734774477547764777477847794780478147824783478447854786478747884789479047914792479347944795479647974798479948004801480248034804480548064807480848094810481148124813481448154816481748184819482048214822482348244825482648274828482948304831483248334834483548364837483848394840484148424843484448454846484748484849485048514852485348544855485648574858485948604861486248634864486548664867486848694870487148724873487448754876487748784879488048814882488348844885488648874888488948904891489248934894489548964897489848994900490149024903490449054906490749084909491049114912491349144915491649174918491949204921492249234924492549264927492849294930493149324933493449354936493749384939494049414942494349444945494649474948494949504951495249534954495549564957495849594960496149624963496449654966496749684969497049714972497349744975497649774978497949804981498249834984498549864987498849894990499149924993499449954996499749984999500050015002500350045005500650075008500950105011501250135014501550165017501850195020502150225023502450255026502750285029503050315032503350345035503650375038
// chipsmith-overlay.js — chipsmith-specific UI layered on top of the
// inherited adom-step base (vendor/adom-step/src/assets/shell.html).
//
// The shell.html itself comes verbatim from adom-step via git submodule
// — this file is everything that makes the running app "chipsmith" vs
// "adom-step": branding swap, the 8-tab right pane, detection pipeline,
// sign-off gates, multi-source footprint cross-validation, faux PCB,
// heatsink vias, solder-jet domes, pin-1 detection, and provenance
// captions. Loaded from /chipsmith-overlay.js, auto-injected before
// </body> of the inherited shell.html by chipsmith's server.
//
// Architecture per gallia/skills/3d-viewer-design §2 — chipsmith is
// Tier-3 (canonical viewer + adom-step base, inherited via submodule).
// Upstream-fix flow: edit adom-step → push → chipsmith
// `git submodule update --remote --recursive && cargo build` → fix
// inherits automatically.
//
// UI design skills (READ BEFORE TOUCHING):
//   gallia/skills/human-ui-patterns/SKILL.md
//   gallia/skills/human-ui-patterns/playbooks/icons-and-displays.md
//     §4b — NO Unicode emoji as UI icons. All chipsmith icons live in
//     the ICONS map below and ship as monochrome currentColor SVGs.
//   gallia/skills/adom-app-header/SKILL.md
//   gallia/skills/3d-viewer-design/SKILL.md
//   gallia/skills/electrical-engineering/SKILL.md
//   gallia/skills/measure-tool-design/SKILL.md
//   gallia/skills/adom-vscode/SKILL.md (reveal buttons → POST /api/reveal)
//   gallia/skills/adom-pdf-viewer/SKILL.md (datasheet viewer port 8985)
// PLAN.md "UI design compliance" section enumerates the same with the
// per-subsystem ladder.

(function chipsmithOverlay() {
  // Chipsmith app icon — custom BGA-from-below silhouette designed
  // specifically for chipsmith. Chip body has a chamfered top-left
  // corner that doubles as the pin-1 indicator (the EE-canonical
  // diagonal cut on QFN/QFP/BGA datasheet drawings), with a 3x3 grid of
  // BGA ball cut-outs inside. Cut-outs rely on fill-rule:evenodd so
  // the balls render as transparent holes punched through the teal body.
  // Built from scratch — not derived from any MDI / Lucide / external
  // icon set so chipsmith has a unique mark.
  const CHIPSMITH_ICON_PATH = [
    // Body: square 5..19 with sharp 1-radius rounded corners on three
    // corners and a 3-unit diagonal CHAMFER at the top-left = pin-1.
    'M8 5 H18 A1 1 0 0 1 19 6 V18 A1 1 0 0 1 18 19 H6 A1 1 0 0 1 5 18 V8 L8 5 Z',
    // 3x3 BGA balls — radius 1 at (8|12|16, 8|12|16). The pin-1 chamfer
    // intentionally clips into the top-left ball area, reinforcing the
    // pin-1 corner reading.
    'M7 8 a1 1 0 1 0 2 0 a1 1 0 1 0 -2 0Z',
    'M11 8 a1 1 0 1 0 2 0 a1 1 0 1 0 -2 0Z',
    'M15 8 a1 1 0 1 0 2 0 a1 1 0 1 0 -2 0Z',
    'M7 12 a1 1 0 1 0 2 0 a1 1 0 1 0 -2 0Z',
    'M11 12 a1 1 0 1 0 2 0 a1 1 0 1 0 -2 0Z',
    'M15 12 a1 1 0 1 0 2 0 a1 1 0 1 0 -2 0Z',
    'M7 16 a1 1 0 1 0 2 0 a1 1 0 1 0 -2 0Z',
    'M11 16 a1 1 0 1 0 2 0 a1 1 0 1 0 -2 0Z',
    'M15 16 a1 1 0 1 0 2 0 a1 1 0 1 0 -2 0Z',
  ].join(' ');

  // ─── 1. Branding swap ────────────────────────────────────────────
  // Wait for the adom-step shell to mount, then swap "STEP Viewer" /
  // "adom-step" → "Chipsmith" in the canonical 44px header.
  function applyBranding() {
    document.title = 'Chipsmith';
    const appName = document.querySelector('.app-name');
    if (appName) appName.textContent = 'Chipsmith';
    const popoverTitle = document.querySelector('.popover-title');
    if (popoverTitle) popoverTitle.textContent = 'Chipsmith';
    // Swap the inline header logo SVG to the chip icon. The adom-step
    // shell hard-codes a single <path> inside the .logo svg, so we just
    // overwrite its `d` attribute. The viewBox stays at 0 0 24 24.
    // fill-rule:evenodd is required so the BGA ball sub-paths punch
    // transparent holes through the body fill instead of stacking.
    const logoPath = document.querySelector('.brand .logo path');
    if (logoPath && logoPath.getAttribute('d') !== CHIPSMITH_ICON_PATH) {
      logoPath.setAttribute('d', CHIPSMITH_ICON_PATH);
      logoPath.setAttribute('fill-rule', 'evenodd');
    }
    // Also swap the favicon so the browser tab + the Hydrogen tab
    // strip both pick up the chip icon. evenodd fill-rule keeps the BGA
    // balls as transparent holes in the favicon too.
    const fav = document.querySelector('link[rel="icon"]');
    if (fav) {
      const dataUri = 'data:image/svg+xml;utf8,' + encodeURIComponent(
        '<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="#00b8b1" fill-rule="evenodd">' +
        '<path d="' + CHIPSMITH_ICON_PATH + '"/></svg>');
      fav.setAttribute('href', dataUri);
      fav.setAttribute('type', 'image/svg+xml');
    }
    // Wiki + GitHub links per adom-app-header skill — verified at publish.
    document.querySelectorAll('.popover-link').forEach(a => {
      const href = a.getAttribute('href') || '';
      if (href.includes('/apps/adom-step')) a.setAttribute('href', href.replace('/apps/adom-step', '/apps/adom-chipsmith'));
      if (href.includes('adom-inc/adom-step')) a.setAttribute('href', href.replace('adom-inc/adom-step', 'adom-inc/adom-chipsmith'));
    });
  }

  // Run as soon as DOM is ready; re-run on header re-render if it
  // happens (defensive — re-render normally only happens on hot reload).
  if (document.readyState === 'loading') {
    document.addEventListener('DOMContentLoaded', applyBranding);
  } else {
    applyBranding();
  }
  // One delayed pass — adom-step's header sometimes finalizes after
  // initial DOMContentLoaded as the canonical viewer attaches.
  setTimeout(applyBranding, 200);
  setTimeout(applyBranding, 1000);

  // ─── 2. Right pane — 8 tabs (left-to-right validation flow) ─────
  // Source → Measurements → Detection → Footprint → Pin-1 → Sign-off →
  // Heatsink → Solder-jet. Each tab has a status pill (●/◐/○/✗/─).
  // The pane floats over the canvas like the outline (transparent bg
  // with text-shadow for legibility) and is collapsible — chevron at
  // the right edge of the title row, expand-tab at canvas-right when
  // collapsed, keyboard 'P' toggles. Persistent via localStorage.

  // Pin/feature DETECTION was removed in d8b02af — the heuristic
  // approaches (Mode 0 named-walk, Mode 1 surface-type clustering)
  // didn't generalize across enough chips to be trustworthy. Pin-1
  // indicator placement, footprint pad rendering, and sign-off all
  // continue to work without it. Tab numbering 1..7 (was 1..8).
  const TABS = [
    { id: 'source',       label: 'Source',       hotkey: '1' },
    { id: 'measurements', label: 'Measurements', hotkey: '2' },
    { id: 'footprint',    label: 'Footprint',    hotkey: '3' },
    { id: 'pin1',         label: 'Pin-1',        hotkey: '4' },
    { id: 'signoff',      label: 'Sign-off',     hotkey: '5' },
    { id: 'heatsink',     label: 'Heatsink',     hotkey: '6' },
    { id: 'solderjet',    label: 'Solder-jet',   hotkey: '7' },
  ];

  // Shared state — populated as data lands from /api/* endpoints. Tabs
  // read from this; future detection / measurements / sign-off phases
  // also write into it. Status field per tab drives the pill glyph.
  const state = {
    paths: null,         // /api/paths response
    spec: null,          // /api/spec response (info.json)
    footprintSources: null,  // /api/footprint-sources response
    activeTab: 'source',
    status: {            // gate state per tab — '○' pending, '◐' partial, '●' pass, '✗' fail, '─' n/a
      source: '○', measurements: '○', footprint: '○',
      pin1: '○', signoff: '○', heatsink: '─', solderjet: '─',
    },
    // Names of GLB-product entries the user has flagged (or auto-flagged
    // by name match) as "decorative" — manufacturer-mark logos, lens
    // cosmetic features, vendor-brand reliefs. On bake, these get
    // reparented under a `chipsmith_decorative` branch (visibility OFF
    // by default in downstream Adom tools, toggleable on). NEVER
    // deleted, only moved.
    decorativeProductNames: [],
  };

  // ── Custom monochrome SVG icons ─────────────────────────────────
  // Per gallia/skills/human-ui-patterns/playbooks/icons-and-displays.md
  // §4b: no Unicode emoji as UI icons (they render multi-color and mix
  // poorly with hand-drawn SVG). All icons in chipsmith are monochrome
  // currentColor SVGs at viewBox 0 0 24 24, stroke style ≈1.6, custom
  // drawn for the specific concept they represent. MDI is only the
  // fallback for fully-generic things (folder, eye, cog).
  const ICONS = {
    // Folder (outline) — for "reveal in Explorer" affordances. The lift
    // tab on top distinguishes a folder from a generic rectangle.
    folder:
      '<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.6" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true">' +
      '<path d="M3 7a2 2 0 0 1 2-2h4l2 2h8a2 2 0 0 1 2 2v8a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2V7z"/>' +
      '</svg>',
    // Open book — chipsmith-specific custom icon for "open the
    // datasheet in a viewer". Two facing pages with a binding seam,
    // not a generic book emoji. Drawn with the same stroke weight as
    // the folder so the action-button row reads as a coherent set.
    bookOpen:
      '<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.6" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true">' +
      '<path d="M3 5.5c2.4-.6 5.1-.9 8-.5 1.4.2 2.5.6 4 1.5 1.5-.9 2.6-1.3 4-1.5 2.9-.4 5.6-.1 8 .5"/>' +
      '<path d="M3 5.5v13c2.4-.6 5.1-.9 8-.5 1.4.2 2.5.6 4 1.5 1.5-.9 2.6-1.3 4-1.5 2.9-.4 5.6-.1 8 .5v-13"/>' +
      '<path d="M12 6.5v13"/>' +
      '</svg>',
  };
  function svgIcon(name) { return `<span class="cs-ic">${ICONS[name] || ''}</span>`; }

  // Inject chipsmith-specific styles. Kept inside the overlay so adom-step
  // doesn't pick them up. Tracks the outline's design language: transparent
  // bg, text-shadow, thin overlay scrollbar, slim collapse chevron.
  function injectStyles() {
    if (document.getElementById('chipsmith-overlay-styles')) return;
    const style = document.createElement('style');
    style.id = 'chipsmith-overlay-styles';
    style.textContent = `
      #cs-pane {
        /* Floating HUD — positioned via top/left at runtime (saved in
           localStorage). Default landing spot is set in init() to a
           location that clears the ViewCube top-right zone. The cube
           occupies viewport (0.85, 0.85, 0.15, 0.15) and at 1500px-tall
           windows that's ~225px of vertical space. top:170 leaves a
           comfortable gap below the cube. */
        position: absolute;
        top: 170px;
        right: 10px;
        width: 360px;
        max-height: calc(100vh - 240px);
        min-height: 280px;
        z-index: 50;
        display: flex;
        flex-direction: column;
        background: rgba(20,24,30,0.55);
        backdrop-filter: blur(6px) saturate(1.1);
        -webkit-backdrop-filter: blur(6px) saturate(1.1);
        border: 1px solid rgba(255,255,255,0.10);
        border-radius: 6px;
        box-shadow: 0 6px 24px rgba(0,0,0,0.35);
        transition: opacity 200ms ease;
        font-family: inherit;
        color: var(--text);
        resize: none;
      }
      #cs-pane.dragging {
        transition: none;
        opacity: 0.92;
      }
      #cs-pane.collapsed {
        display: none;
      }
      .cs-tabs {
        cursor: grab;             /* drag handle: tab strip is the grab target */
      }
      .cs-tabs:active { cursor: grabbing; }
      #cs-pane *,
      #cs-pane {
        text-shadow: 0 1px 2px rgba(0,0,0,0.85), 0 0 4px rgba(0,0,0,0.55);
      }
      .cs-tabs {
        display: flex;
        gap: 2px;
        padding: 6px 8px 0 8px;
        flex-wrap: wrap;
        border-bottom: 1px solid rgba(255,255,255,0.10);
        user-select: none;
      }
      .cs-tab {
        background: rgba(20,24,30,0.45);
        border: 1px solid rgba(255,255,255,0.10);
        border-bottom: none;
        color: var(--text-dim);
        padding: 5px 10px 5px 8px;
        font-size: 11px;
        font-family: inherit;
        cursor: pointer;
        border-radius: 4px 4px 0 0;
        display: inline-flex;
        align-items: center;
        gap: 5px;
        line-height: 1.2;
      }
      .cs-tab:hover {
        background: rgba(0,184,177,0.15);
        color: var(--accent);
      }
      .cs-tab.active {
        background: rgba(0,184,177,0.20);
        color: var(--accent);
        border-color: var(--accent);
      }
      .cs-tab .pill {
        font-size: 10px;
        opacity: 0.85;
      }
      .cs-tab .pill.ok      { color: #1de9b6; }
      .cs-tab .pill.partial { color: #ffb74d; }
      .cs-tab .pill.fail    { color: #ff7043; }
      .cs-tab .pill.pending { color: #b0bec5; opacity: 0.55; }
      .cs-tab .pill.na      { color: #b0bec5; opacity: 0.4; }
      .cs-body {
        flex: 1 1 auto;
        overflow-y: overlay;
        overflow-y: auto;
        padding: 10px 12px 14px 12px;
        scrollbar-width: thin;
        scrollbar-color: rgba(255,255,255,0.18) transparent;
      }
      .cs-body::-webkit-scrollbar       { width: 6px; background: transparent; }
      .cs-body::-webkit-scrollbar-track { background: transparent; }
      .cs-body::-webkit-scrollbar-thumb { background: rgba(255,255,255,0.10); border-radius: 3px; }
      #cs-pane:hover .cs-body::-webkit-scrollbar-thumb { background: rgba(255,255,255,0.28); }
      .cs-section { margin-bottom: 14px; }
      .cs-section-title {
        font-size: 11px;
        text-transform: uppercase;
        letter-spacing: 0.06em;
        color: var(--text-dim);
        margin: 0 0 6px 0;
        font-weight: 600;
      }
      .cs-row {
        display: flex;
        align-items: center;
        gap: 8px;
        font-size: 12px;
        padding: 3px 0;
      }
      .cs-row .label { color: var(--text-dim); flex: 0 0 90px; }
      .cs-row .value { color: var(--text); flex: 1 1 auto; word-break: break-word; }
      .cs-row .value.mono { font-family: ui-monospace, monospace; font-size: 11px; }
      .cs-row .value.dim  { color: var(--text-dim); font-style: italic; }
      /* Inline-row reveal icon button — sits next to a file path in the
         Files section. Slim 22x22 square, just the 📂 glyph; the
         tooltip carries the full label per human-ui-patterns §1a. */
      .cs-reveal-icon {
        background: transparent;
        border: 1px solid rgba(255,255,255,0.14);
        color: var(--text-dim);
        cursor: pointer;
        font-size: 12px;
        padding: 0;
        margin-left: 4px;
        line-height: 1;
        font-family: inherit;
        width: 22px;
        height: 22px;
        border-radius: 4px;
        display: inline-flex;
        align-items: center;
        justify-content: center;
        flex: 0 0 auto;
      }
      .cs-reveal-icon:hover { background: var(--accent-soft); color: var(--accent); border-color: var(--accent); }
      /* Action button row — primary + secondary CTAs at the bottom of a
         section. Per human-ui-patterns §1: never ALL CAPS, tooltip on
         every button, ≥28px height (real-pointer friendly hit target),
         visible focus state. Primary uses the brand teal; secondary is
         outline-only. */
      .cs-actions {
        display: flex;
        gap: 8px;
        margin-top: 10px;
        flex-wrap: wrap;
      }
      .cs-btn {
        display: inline-flex;
        align-items: center;
        gap: 6px;
        height: 30px;
        padding: 0 12px;
        border-radius: 5px;
        font-family: inherit;
        font-size: 12px;
        line-height: 1;
        cursor: pointer;
        transition: background 120ms ease, border-color 120ms ease, color 120ms ease;
      }
      .cs-btn .lbl  { font-weight: 500; }
      /* Inline SVG icon container — keeps icons monochrome via
         currentColor and sized consistently across buttons + inline
         rows (16px in CTAs, 14px in slim inline buttons). */
      .cs-ic { display: inline-flex; align-items: center; line-height: 1; }
      .cs-ic svg { width: 16px; height: 16px; display: block; }
      .cs-reveal-icon .cs-ic svg { width: 14px; height: 14px; }
      .cs-btn:focus-visible {
        outline: 2px solid var(--accent);
        outline-offset: 2px;
      }
      .cs-btn-primary {
        background: rgba(0,184,177,0.18);
        border: 1px solid var(--accent);
        color: var(--accent);
      }
      .cs-btn-primary:hover {
        background: rgba(0,184,177,0.32);
      }
      .cs-btn-secondary {
        background: rgba(20,24,30,0.45);
        border: 1px solid rgba(255,255,255,0.14);
        color: var(--text);
      }
      .cs-btn-secondary:hover {
        background: var(--accent-soft);
        color: var(--accent);
        border-color: var(--accent);
      }
      /* Shape radio (Pin-1 tab): three options laid out horizontally
         with the active one tinted with the brand teal. Uses native
         <input type=radio> for keyboard + screen-reader, but visually
         styled as a button group. */
      .cs-shape-radio {
        display: flex;
        gap: 6px;
        margin: 6px 0 4px 0;
        flex-wrap: wrap;
      }
      .cs-shape-opt {
        display: inline-flex;
        align-items: center;
        gap: 6px;
        padding: 5px 10px;
        height: 28px;
        border-radius: 5px;
        background: rgba(20,24,30,0.45);
        border: 1px solid rgba(255,255,255,0.14);
        color: var(--text);
        font-size: 12px;
        cursor: pointer;
        user-select: none;
        transition: background 120ms ease, border-color 120ms ease, color 120ms ease;
      }
      .cs-shape-opt:hover { border-color: var(--accent); color: var(--accent); }
      .cs-shape-opt input { margin: 0; cursor: pointer; }
      .cs-shape-opt.active {
        background: rgba(0,184,177,0.18);
        border-color: var(--accent);
        color: var(--accent);
      }
      .cs-collapse {
        width: 22px;
        height: 22px;
        background: transparent;
        border: 1px solid transparent;
        color: var(--text-dim);
        cursor: pointer;
        font-size: 12px;
        font-family: inherit;
        border-radius: 4px;
        padding: 0;
        display: inline-flex;
        align-items: center;
        justify-content: center;
        line-height: 1;
        margin-left: auto;
      }
      .cs-collapse:hover { background: var(--accent-soft); color: var(--accent); border-color: var(--accent); }
      #cs-expander {
        position: absolute;
        top: 50%;
        right: 0;
        transform: translateY(-50%);
        width: 22px;
        height: 64px;
        background: rgba(20,24,30,0.55);
        border: 1px solid var(--border);
        border-right: none;
        border-radius: 6px 0 0 6px;
        color: var(--text-dim);
        cursor: pointer;
        font-size: 14px;
        font-family: inherit;
        display: none;
        align-items: center;
        justify-content: center;
        z-index: 51;
        padding: 0;
      }
      #cs-expander:hover { background: rgba(0,184,177,0.18); color: var(--accent); }
      body.cs-collapsed #cs-expander { display: flex; }
      .cs-amber  { color: #ffb74d; }
      .cs-empty  { color: var(--text-dim); font-size: 11px; font-style: italic; padding: 6px 0; }
      /* Status block — large readable banner explaining the measurement
         outcome. Color-keyed via .pill-* class on the parent. */
      .cs-meas-status {
        font-size: 12px;
        line-height: 1.45;
        padding: 8px 10px;
        border-radius: 4px;
        border-left: 3px solid;
      }
      .cs-meas-status.pill-ok      { background: rgba(29,233,182,0.10); border-color: #1de9b6; color: #b9f6e2; }
      .cs-meas-status.pill-partial { background: rgba(255,183, 77,0.10); border-color: #ffb74d; color: #ffe0b2; }
      .cs-meas-status.pill-fail    { background: rgba(255,112, 67,0.12); border-color: #ff7043; color: #ffccbc; }
      .cs-meas-status code { font-family: ui-monospace, monospace; font-size: 11px; }
      /* Candidate table — compact list of every rescale candidate, %.
         The winning row gets a brand-teal left border. */
      .cs-tbl {
        width: 100%;
        border-collapse: collapse;
        font-size: 11px;
        margin-top: 4px;
      }
      .cs-tbl th {
        text-align: left;
        font-weight: 500;
        color: var(--text-dim);
        padding: 4px 6px;
        border-bottom: 1px solid rgba(255,255,255,0.10);
      }
      .cs-tbl th.num, .cs-tbl td.num { text-align: right; }
      .cs-tbl td {
        padding: 4px 6px;
        border-bottom: 1px solid rgba(255,255,255,0.06);
        vertical-align: top;
      }
      .cs-tbl td.dim { color: var(--text-dim); }
      .cs-tbl td.mono { font-family: ui-monospace, monospace; }
      .cs-tbl tr.is-winner td:first-child { border-left: 2px solid var(--accent); padding-left: 4px; }
      .cs-tbl tr.is-winner td { color: var(--text); }
      .cs-tbl tr.is-ok td.num { color: #1de9b6; }
      /* Clickable feature row in Tab 3 — hover hints at its
         click-to-isolate behaviour. */
      .cs-row.cs-clickable {
        cursor: pointer;
        border-radius: 3px;
        padding: 3px 6px;
        margin: 0 -6px;
      }
      .cs-row.cs-clickable:hover {
        background: rgba(0,184,177,0.12);
      }
      /* Spec editor inline form. */
      .cs-edit-spec {
        background: transparent;
        border: 1px solid rgba(255,255,255,0.18);
        color: var(--text-dim);
        font-size: 10px;
        padding: 1px 6px;
        margin-left: 6px;
        border-radius: 3px;
        cursor: pointer;
        font-family: inherit;
        text-transform: none;
        letter-spacing: 0;
      }
      .cs-edit-spec:hover { background: var(--accent-soft); color: var(--accent); border-color: var(--accent); }
      .cs-spec-input {
        background: rgba(20,24,30,0.55);
        border: 1px solid rgba(255,255,255,0.14);
        color: var(--text);
        padding: 4px 6px;
        border-radius: 3px;
        font-size: 12px;
        font-family: ui-monospace, monospace;
        outline: none;
        width: 100%;
      }
      .cs-spec-input:focus { border-color: var(--accent); }
      .cs-spec-input.cs-body-dim {
        width: 60px;
        margin-right: 4px;
      }
      /* Per-source color swatch — small dot before the source label
         so the user immediately knows which 3D color each source maps
         to. Per the multi-source-cross-validation plan section. */
      .cs-src-swatch {
        display: inline-block;
        width: 10px; height: 10px;
        border-radius: 2px;
        margin-right: 6px;
        flex: 0 0 auto;
        border: 1px solid rgba(255,255,255,0.18);
      }
      /* Floating Sign-off HUD — always visible, top-left default
         (top-right is taken by the right-pane HUD + ViewCube).
         Drag the title row, collapse to compact badge, 'g' toggle. */
      #cs-signoff-hud {
        position: absolute;
        top: 90px;
        left: 300px;             /* clear of the Outline panel (280 wide) */
        width: 280px;
        z-index: 56;
        background: rgba(20,24,30,0.78);
        backdrop-filter: blur(6px) saturate(1.1);
        -webkit-backdrop-filter: blur(6px) saturate(1.1);
        border: 1px solid rgba(255,255,255,0.10);
        border-radius: 6px;
        box-shadow: 0 4px 18px rgba(0,0,0,0.45);
        font-family: inherit;
        color: var(--text);
        font-size: 11px;
        transition: opacity 200ms ease;
        text-shadow: 0 1px 2px rgba(0,0,0,0.55);
      }
      #cs-signoff-hud.dragging { transition: none; opacity: 0.92; }
      #cs-signoff-hud.collapsed .cs-so-body,
      #cs-signoff-hud.collapsed .cs-so-foot { display: none; }
      #cs-signoff-hud.collapsed { width: auto; }
      #cs-signoff-hud .cs-so-head {
        display: flex;
        align-items: center;
        gap: 6px;
        padding: 6px 10px;
        cursor: grab;
        border-bottom: 1px solid rgba(255,255,255,0.08);
        user-select: none;
      }
      #cs-signoff-hud.collapsed .cs-so-head { border-bottom: none; cursor: pointer; }
      #cs-signoff-hud .cs-so-head:active { cursor: grabbing; }
      #cs-signoff-hud .cs-so-title {
        font-weight: 600;
        font-size: 11px;
        text-transform: uppercase;
        letter-spacing: 0.06em;
        color: var(--text-dim);
      }
      #cs-signoff-hud .cs-so-tally {
        font-family: ui-monospace, monospace;
        font-size: 11px;
        margin-left: auto;
        margin-right: 4px;
        padding: 2px 6px;
        border-radius: 3px;
      }
      #cs-signoff-hud .cs-so-tally.pass  { background: rgba(29,233,182,0.18); color: #b9f6e2; }
      #cs-signoff-hud .cs-so-tally.block { background: rgba(255,112, 67,0.18); color: #ffccbc; }
      #cs-signoff-hud .cs-so-collapse {
        background: transparent;
        border: 1px solid transparent;
        color: var(--text-dim);
        cursor: pointer;
        font-size: 11px;
        padding: 0;
        width: 18px; height: 18px;
        border-radius: 3px;
      }
      #cs-signoff-hud .cs-so-collapse:hover { background: var(--accent-soft); color: var(--accent); border-color: var(--accent); }
      #cs-signoff-hud .cs-so-body { padding: 4px 8px 6px 8px; max-height: 60vh; overflow-y: auto; }
      #cs-signoff-hud .cs-so-section { margin-top: 6px; }
      #cs-signoff-hud .cs-so-section:first-child { margin-top: 0; }
      #cs-signoff-hud .cs-so-section-title {
        font-size: 9.5px;
        text-transform: uppercase;
        letter-spacing: 0.08em;
        color: var(--text-dim);
        margin: 4px 0 2px 0;
        opacity: 0.75;
      }
      #cs-signoff-hud .cs-so-row {
        display: flex;
        align-items: center;
        gap: 6px;
        padding: 2px 4px;
        border-radius: 3px;
        cursor: pointer;
        line-height: 1.3;
      }
      #cs-signoff-hud .cs-so-row:hover { background: rgba(0,184,177,0.12); }
      #cs-signoff-hud .cs-so-glyph {
        font-family: ui-monospace, monospace;
        width: 12px;
        text-align: center;
        flex: 0 0 auto;
      }
      #cs-signoff-hud .cs-so-glyph.ok      { color: #1de9b6; }
      #cs-signoff-hud .cs-so-glyph.partial { color: #ffb74d; }
      #cs-signoff-hud .cs-so-glyph.fail    { color: #ff7043; }
      #cs-signoff-hud .cs-so-glyph.pending { color: #b0bec5; opacity: 0.65; }
      #cs-signoff-hud .cs-so-glyph.na      { color: #b0bec5; opacity: 0.45; }
      #cs-signoff-hud .cs-so-label.soft   { color: var(--text-dim); font-style: italic; }
      #cs-signoff-hud .cs-so-foot { padding: 6px 8px 8px 8px; border-top: 1px solid rgba(255,255,255,0.08); }
      #cs-signoff-hud .cs-so-foot button { width: 100%; }
      /* Pin-reveal strobe badge animation — pop in then fade. */
      @keyframes cs-strobe-pop {
        0%   { opacity: 0; transform: translate(-50%, -90%) scale(0.7); }
        15%  { opacity: 1; transform: translate(-50%, -110%) scale(1.15); }
        30%  { opacity: 1; transform: translate(-50%, -100%) scale(1.0); }
        100% { opacity: 0; transform: translate(-50%, -90%) scale(0.95); }
      }
    `;
    document.head.appendChild(style);
  }

  // Build right-pane DOM. Tabs strip on top + body container that swaps
  // content per active tab. Inserted into <body> after .canvas-wrap so
  // it's a sibling of the absolute-positioned outline.
  function buildPane() {
    if (document.getElementById('cs-pane')) return;
    const pane = document.createElement('aside');
    pane.id = 'cs-pane';
    pane.innerHTML = `
      <div class="cs-tabs" role="tablist"></div>
      <div class="cs-body" id="cs-body"></div>
    `;
    const tabsEl = pane.querySelector('.cs-tabs');
    TABS.forEach(t => {
      const btn = document.createElement('button');
      btn.className = 'cs-tab' + (t.id === state.activeTab ? ' active' : '');
      btn.dataset.tab = t.id;
      btn.setAttribute('role', 'tab');
      btn.setAttribute('data-tooltip', `${t.label} — ${t.hotkey} or click. Status pill: ● pass · ◐ partial · ○ pending · ✗ fail · ─ n/a.`);
      btn.innerHTML = `<span class="label">${t.label}</span><span class="pill pending">○</span>`;
      btn.onclick = () => setActiveTab(t.id);
      tabsEl.appendChild(btn);
    });
    // Add the collapse chevron on the rightmost tab row — sits inline at
    // the end of the tab strip so it doesn't waste a header row.
    const collapseBtn = document.createElement('button');
    collapseBtn.className = 'cs-collapse';
    collapseBtn.id = 'csCollapse';
    collapseBtn.textContent = '❯';
    collapseBtn.setAttribute('aria-label', 'Collapse pane');
    collapseBtn.setAttribute('data-tooltip', 'Collapse the pane (P) — slides out, leaves the canvas full-width. Click the tab at the right edge to expand.');
    tabsEl.appendChild(collapseBtn);
    document.body.appendChild(pane);

    const expander = document.createElement('button');
    expander.id = 'cs-expander';
    expander.textContent = '❮';
    expander.setAttribute('aria-label', 'Expand pane');
    expander.setAttribute('data-tooltip', 'Expand the chipsmith pane (P).');
    document.body.appendChild(expander);

    const KEY_COLLAPSED = 'chipsmith.pane.collapsed';
    const KEY_POS = 'chipsmith.pane.pos';
    function setCollapsed(c) {
      pane.classList.toggle('collapsed', c);
      document.body.classList.toggle('cs-collapsed', c);
      try { localStorage.setItem(KEY_COLLAPSED, c ? '1' : '0'); } catch (_) {}
    }
    try {
      if (localStorage.getItem(KEY_COLLAPSED) === '1') setCollapsed(true);
    } catch (_) {}
    collapseBtn.onclick = () => setCollapsed(true);
    expander.onclick = () => setCollapsed(false);

    // ── Drag logic ────────────────────────────────────────────
    // Grab anywhere in the tab strip (NOT on a tab/collapse button) to
    // drag. Position persists in localStorage. Constrained to keep at
    // least a corner of the HUD on-screen so the user can never lose it.
    function clamp(v, lo, hi) { return Math.max(lo, Math.min(hi, v)); }
    function applyPos(pos) {
      pane.style.top = pos.y + 'px';
      pane.style.left = pos.x + 'px';
      pane.style.right = 'auto';
    }
    function loadPos() {
      try {
        const raw = localStorage.getItem(KEY_POS);
        if (raw) return JSON.parse(raw);
      } catch (_) {}
      return null;
    }
    function savePos(pos) {
      try { localStorage.setItem(KEY_POS, JSON.stringify(pos)); } catch (_) {}
    }
    const initialPos = loadPos();
    if (initialPos) applyPos(initialPos);

    let dragOrigin = null;
    tabsEl.addEventListener('mousedown', (e) => {
      // Don't start drag if click is on a tab button or collapse button.
      const onTab = e.target.closest('.cs-tab, .cs-collapse');
      if (onTab) return;
      const r = pane.getBoundingClientRect();
      dragOrigin = {
        startX: e.clientX, startY: e.clientY,
        baseX: r.left, baseY: r.top,
      };
      pane.classList.add('dragging');
      e.preventDefault();
    });
    document.addEventListener('mousemove', (e) => {
      if (!dragOrigin) return;
      const dx = e.clientX - dragOrigin.startX;
      const dy = e.clientY - dragOrigin.startY;
      // Header is 44 px; canvas content starts there. Clamp keeps at
      // least 60×60 of the pane on-screen so it can never be lost.
      const minX = -pane.offsetWidth + 60;
      const maxX = window.innerWidth - 60;
      const minY = 44;
      const maxY = window.innerHeight - 60;
      const x = clamp(dragOrigin.baseX + dx, minX, maxX);
      const y = clamp(dragOrigin.baseY + dy, minY, maxY);
      applyPos({ x, y });
    });
    document.addEventListener('mouseup', () => {
      if (!dragOrigin) return;
      const r = pane.getBoundingClientRect();
      savePos({ x: r.left, y: r.top });
      pane.classList.remove('dragging');
      dragOrigin = null;
    });
    document.addEventListener('keydown', (e) => {
      if (e.ctrlKey || e.metaKey || e.altKey) return;
      const t = e.target;
      if (t && (t.tagName === 'INPUT' || t.tagName === 'TEXTAREA' || t.isContentEditable)) return;
      if (e.key === 'p' || e.key === 'P') {
        setCollapsed(!pane.classList.contains('collapsed'));
        return;
      }
      // Number-key tab hotkeys.
      const tab = TABS.find(x => x.hotkey === e.key);
      if (tab) {
        if (pane.classList.contains('collapsed')) setCollapsed(false);
        setActiveTab(tab.id);
      }
    });
  }

  function setActiveTab(id) {
    state.activeTab = id;
    document.querySelectorAll('.cs-tab').forEach(el => {
      el.classList.toggle('active', el.dataset.tab === id);
    });
    renderActiveTab();
  }

  // Recompute every tab's status pill from current state, without
  // rendering any tab. Called on every data mutation (spec save,
  // detection run, footprint load, sign-off action) so cross-tab
  // status cascades correctly even when a downstream tab isn't the
  // active tab.
  function recomputeAllStatuses() {
    // Source.
    const s = state.spec;
    if (s) {
      const pin = s.pin_count, struct = s.structural_type, body = Array.isArray(s.body_dims_mm) ? s.body_dims_mm : null;
      const present = [pin, struct, body].filter(v => v !== undefined && v !== null && v !== '' && v !== '?').length;
      state.status.source = present === 3 ? '●' : (present > 0 ? '◐' : '○');
    }
    // Measurements.
    if (state.measurements) {
      state.status.measurements = state.measurements.status;
    }
    // Footprint.
    const fps = state.footprintSources;
    if (fps) {
      const sourcesPresent = ['kicad','fusion','altium','datasheet','ds2sf'].filter(k => fps[k] && !fps[k]._stub && fps[k].pad_count > 0);
      state.status.footprint = sourcesPresent.length === 0 ? '✗' : (sourcesPresent.length >= 2 ? '◐' : '○');
    }
    // Pin-1: chip-side = user-placed indicator (state.userPin1Corner);
    // fp-side = silkscreen marker on any footprint source.
    {
      const chipPin1 = !!state.userPin1Corner;
      const fpPin1 = ['kicad','fusion','altium','datasheet','ds2sf'].some(k => fps && fps[k] && fps[k].pin1_silk && fps[k].pin1_silk.found);
      state.status.pin1 = (chipPin1 || fpPin1) ? '◐' : '✗';
    }
    // Heatsink + solderjet recompute themselves on render — not
    // status-data-cheap to recompute without running their algorithms.
    refreshStatusPills();
    renderSignoffHud();
  }

  function refreshStatusPills() {
    document.querySelectorAll('.cs-tab').forEach(el => {
      const id = el.dataset.tab;
      const glyph = state.status[id] || '○';
      const pill = el.querySelector('.pill');
      if (!pill) return;
      pill.textContent = glyph;
      pill.className = 'pill ' + ({
        '●': 'ok', '◐': 'partial', '○': 'pending', '✗': 'fail', '─': 'na',
      }[glyph] || 'pending');
    });
  }

  // ── Tab 1: Source ───────────────────────────────────────────────
  // Manifest provenance + datasheet PDF inline + reveal-in-VS-Code
  // buttons next to every file path. Status:
  //   ● when pin_count + structural_type + body_dims_mm are present
  //   ◐ when at least one is present
  //   ○ when none present (default until chip-fetcher / user populates)

  async function renderSourceTab() {
    const body = document.getElementById('cs-body');
    if (!state.spec || !state.paths) {
      body.innerHTML = `<div class="cs-empty">Loading source…</div>`;
      return;
    }
    const s = state.spec, p = state.paths;
    const has = (k) => s[k] !== undefined && s[k] !== null && s[k] !== '' && s[k] !== '?';
    const declaredPinCount = has('pin_count') ? s.pin_count : null;
    const declaredStruct   = has('structural_type') ? s.structural_type : null;
    const declaredBody     = Array.isArray(s.body_dims_mm) ? s.body_dims_mm : null;
    const specReady = (declaredPinCount && declaredStruct && declaredBody) ? '●'
                    : (declaredPinCount || declaredStruct || declaredBody) ? '◐' : '○';
    state.status.source = specReady;
    refreshStatusPills();

    const reveal = (path) => path
      ? `<button class="cs-reveal-icon" data-path="${escAttr(path)}" aria-label="Reveal in Explorer" data-tooltip="Reveal in the VS Code Explorer sidebar (delegates to adom-vscode reveal).">${svgIcon('folder')}</button>`
      : '';
    const fmtBody = declaredBody ? `${declaredBody[0]} × ${declaredBody[1]} × ${declaredBody[2]} mm`
                                 : '<span class="cs-amber">declare body_dims_mm</span>';
    const fmtPin  = declaredPinCount || '<span class="cs-amber">declare pin_count</span>';
    const fmtStr  = declaredStruct   || '<span class="cs-amber">declare structural_type</span>';

    body.innerHTML = `
      <div class="cs-section">
        <h3 class="cs-section-title">Manifest</h3>
        <div class="cs-row"><span class="label">MPN</span><span class="value mono">${esc(s.mpn || '?')}</span></div>
        <div class="cs-row"><span class="label">Manufacturer</span><span class="value">${esc(s.manufacturer || '?')}</span></div>
        <div class="cs-row"><span class="label">Package</span><span class="value">${esc(s.package || '?')}</span></div>
        <div class="cs-row"><span class="label">Family</span><span class="value">${esc(s.family || '?')}</span></div>
        <div class="cs-row"><span class="label">Description</span><span class="value dim">${esc(s.description || '—')}</span></div>
      </div>
      <div class="cs-section">
        <h3 class="cs-section-title">Spec <button class="cs-edit-spec" data-tooltip="Edit pin_count / structural_type / body_dims_mm inline. Saves into info.json so the values persist across reloads. The Measurements + Detection tabs cascade automatically.">${state.editingSpec ? '✕ cancel' : 'edit ✎'}</button></h3>
        ${state.editingSpec ? `
          <div class="cs-row"><span class="label">Pin count</span><input type="number" class="cs-spec-input" id="spec-pin_count" min="0" value="${declaredPinCount || ''}" placeholder="e.g. 16"/></div>
          <div class="cs-row"><span class="label">Structure</span>
            <select class="cs-spec-input" id="spec-structural_type">
              <option value="">— pick —</option>
              <option value="smd_pad"      ${declaredStruct === 'smd_pad' ? 'selected' : ''}>smd_pad (QFN/LGA/DFN)</option>
              <option value="smd_leg"      ${declaredStruct === 'smd_leg' ? 'selected' : ''}>smd_leg (SOIC/QFP/SOT)</option>
              <option value="ball"         ${declaredStruct === 'ball'    ? 'selected' : ''}>ball (BGA)</option>
              <option value="thru_hole_pin"${declaredStruct === 'thru_hole_pin' ? 'selected' : ''}>thru_hole_pin (DIP)</option>
              <option value="ep_thermal"   ${declaredStruct === 'ep_thermal'    ? 'selected' : ''}>ep_thermal (EP only)</option>
              <option value="lead_with_ep" ${declaredStruct === 'lead_with_ep'  ? 'selected' : ''}>lead_with_ep (QFN with center EP)</option>
              <option value="hybrid_smd_thru" ${declaredStruct === 'hybrid_smd_thru' ? 'selected' : ''}>hybrid_smd_thru (USB-C)</option>
            </select>
          </div>
          <div class="cs-row"><span class="label">Body (mm)</span>
            <input type="number" step="0.1" class="cs-spec-input cs-body-dim" id="spec-body-x" placeholder="W" value="${declaredBody?.[0] || ''}"/>
            <input type="number" step="0.1" class="cs-spec-input cs-body-dim" id="spec-body-y" placeholder="H" value="${declaredBody?.[1] || ''}"/>
            <input type="number" step="0.1" class="cs-spec-input cs-body-dim" id="spec-body-z" placeholder="D" value="${declaredBody?.[2] || ''}"/>
          </div>
          <div class="cs-actions">
            <button class="cs-btn cs-btn-primary" id="cs-spec-save" data-tooltip="Save → POST /api/spec → write into info.json. Cascades to Measurements + Detection.">${svgIcon('bookOpen')}<span class="lbl">Save</span></button>
          </div>
        ` : `
          <div class="cs-row"><span class="label">Pin count</span><span class="value mono">${fmtPin}</span></div>
          <div class="cs-row"><span class="label">Structure</span><span class="value mono">${fmtStr}</span></div>
          <div class="cs-row"><span class="label">Body (mm)</span><span class="value mono">${fmtBody}</span></div>
        `}
      </div>
      <div class="cs-section">
        <h3 class="cs-section-title">Files</h3>
        <div class="cs-row"><span class="label">STEP</span><span class="value mono" title="${escAttr(p.step||'')}">${esc(basename(p.step))}</span>${reveal(p.step)}</div>
        <div class="cs-row"><span class="label">Footprint</span><span class="value mono" title="${escAttr(p.kicad_mod||'')}">${esc(basename(p.kicad_mod))}</span>${reveal(p.kicad_mod)}</div>
        <div class="cs-row"><span class="label">Datasheet</span><span class="value mono" title="${escAttr(p.datasheet||'')}">${esc(basename(p.datasheet))}</span>${reveal(p.datasheet)}</div>
        <div class="cs-row"><span class="label">info.json</span><span class="value mono" title="${escAttr(p.info_json||'')}">${esc(basename(p.info_json))}</span>${reveal(p.info_json)}</div>
        <div class="cs-row"><span class="label">Folder</span><span class="value mono" title="${escAttr(p.dir||'')}">${esc(p.dir||'?')}</span>${reveal(p.dir)}</div>
      </div>
      <div class="cs-section">
        <h3 class="cs-section-title">Datasheet</h3>
        ${p.datasheet
          ? `<div class="cs-row"><span class="label">File</span><span class="value mono" title="${escAttr(p.datasheet)}">${esc(basename(p.datasheet))}</span></div>
             <div class="cs-actions">
               <button class="cs-btn cs-btn-primary cs-open-pdf"
                       data-href="${escAttr(pdfViewerUrl(s, p))}"
                       data-tooltip="Open in the Adom PDF viewer (PDF.js + thumbnail strip + page deep-links). Pops a full-screen tab so you can read alongside chipsmith. The viewer also remembers cited pages once detection runs.">
                 ${svgIcon('bookOpen')}<span class="lbl">Open in viewer</span>
               </button>
               <button class="cs-btn cs-btn-secondary cs-reveal"
                       data-path="${escAttr(p.datasheet)}"
                       data-tooltip="Reveal the datasheet PDF in the VS Code Explorer sidebar — useful for confirming where it lives on disk.">
                 ${svgIcon('folder')}<span class="lbl">Reveal</span>
               </button>
             </div>`
          : `<div class="cs-empty">No datasheet attached.</div>`}
      </div>
      <div class="cs-section">
        <h3 class="cs-section-title">Cosmetic — laser etch</h3>
        <div class="cs-row dim"><span class="value">Engrave the MPN onto the chip's top face. Cosmetic only — auto-positioned to avoid the pin-1 indicator. Baked into <code>.chipsmith.step</code> at sign-off so the engraving survives every future load.</span></div>
        <div class="cs-row">
          <span class="label">Text</span>
          <input type="text" id="cs-laser-text" value="${escAttr(state.laserEtchText || (state.spec?.mpn || ''))}" style="background:rgba(20,24,30,0.65); border:1px solid rgba(255,255,255,0.14); color:var(--text); padding:4px 8px; border-radius:4px; font-family:inherit; font-size:12px; min-width:140px" placeholder="MPN" data-tooltip="Defaults to the chip's MPN. Override for variant SKUs (e.g. 'LM358 / Ti')."/>
        </div>
        <div class="cs-actions">
          <button class="cs-btn cs-btn-${state.laserEtch ? 'primary' : 'secondary'}" id="cs-laser-toggle" data-tooltip="${state.laserEtch ? 'Hide the laser etch on the chip top.' : 'Render the MPN as a 50µm-deep darker recess on the body top face. Auto-sized + auto-positioned away from the pin-1 indicator.'}">${svgIcon('bookOpen')}<span class="lbl">${state.laserEtch ? 'Hide etch' : 'Show etch'}</span></button>
        </div>
      </div>
      <div class="cs-section">
        <h3 class="cs-section-title">Debug — anchor bboxes</h3>
        <div class="cs-row dim"><span class="value">Show four colored slabs marking where chipsmith thinks each canonical surface is: chip-top (cyan), chip-bottom (magenta), footprint-area (yellow @ Y=0), pcb-top (green @ Y=0). Use when chipsmith's placements look wrong — the slabs reveal whether it's a placement bug or a wrong-anchor bug.</span></div>
        <div class="cs-actions">
          <button class="cs-btn cs-btn-${state.debugAnchorsOn ? 'primary' : 'secondary'}" id="cs-debug-anchors-toggle" data-tooltip="${state.debugAnchorsOn ? 'Hide the four debug anchor slabs.' : 'Render four 50µm-thick colored slabs at the chipsmith-internal anchor planes (chip-top, chip-bottom, footprint-area, pcb-top). Each appears in the Outline as a child of Debug — anchor bboxes.'}">${svgIcon('bookOpen')}<span class="lbl">${state.debugAnchorsOn ? 'Hide anchors' : 'Show anchors'}</span></button>
        </div>
      </div>
    `;
    // Wire spec editor controls.
    const editBtn = body.querySelector('.cs-edit-spec');
    if (editBtn) editBtn.onclick = () => {
      state.editingSpec = !state.editingSpec;
      renderSourceTab();
    };
    const saveBtn = document.getElementById('cs-spec-save');
    if (saveBtn) saveBtn.onclick = async () => {
      const merged = Object.assign({}, state.spec || {});
      const pin = parseInt(document.getElementById('spec-pin_count').value, 10);
      if (Number.isFinite(pin) && pin > 0) merged.pin_count = pin;
      const struct = document.getElementById('spec-structural_type').value;
      if (struct) merged.structural_type = struct;
      const bx = parseFloat(document.getElementById('spec-body-x').value);
      const by = parseFloat(document.getElementById('spec-body-y').value);
      const bz = parseFloat(document.getElementById('spec-body-z').value);
      if (Number.isFinite(bx) && Number.isFinite(by) && Number.isFinite(bz)) {
        merged.body_dims_mm = [bx, by, bz];
      }
      saveBtn.disabled = true;
      saveBtn.querySelector('.lbl').textContent = 'Saving…';
      try {
        const r = await fetch('api/spec', {
          method: 'POST',
          headers: { 'Content-Type': 'application/json' },
          body: JSON.stringify(merged),
        });
        const j = await r.json();
        state.spec = merged;
        state.editingSpec = false;
        // Cascade: recompute every status pill, then re-render whichever
        // tab is currently visible.
        recomputeAllStatuses();
        if (state.activeTab === 'source') renderSourceTab();
        if (state.activeTab === 'measurements' && state.measurements) renderMeasurementsTab();
        if (state.activeTab === 'signoff') renderSignoffTab();
      } catch (e) {
        console.error('[chipsmith] spec save failed', e);
        saveBtn.disabled = false;
        saveBtn.querySelector('.lbl').textContent = 'Save (failed — retry)';
      }
    };
    // Laser-etch toggle + text input. Defaults to MPN; user can
    // override per-chip. Re-renders the source tab so the button label
    // flips between "Show etch" / "Hide etch".
    const laserBtn = document.getElementById('cs-laser-toggle');
    const laserInput = document.getElementById('cs-laser-text');
    if (laserInput) {
      laserInput.oninput = () => {
        state.laserEtchText = laserInput.value;
        // Live-update the etch if it's currently visible.
        if (state.laserEtch) renderLaserEtch({ text: laserInput.value });
      };
    }
    if (laserBtn) {
      laserBtn.onclick = () => {
        const text = (laserInput?.value || state.spec?.mpn || '').trim();
        if (state.laserEtch) {
          ensureOverlayDisposed('laser-etch');
          state.laserEtch = null;
        } else {
          renderLaserEtch({ text });
        }
        renderSourceTab();
      };
    }
    const debugBtn = document.getElementById('cs-debug-anchors-toggle');
    if (debugBtn) {
      debugBtn.onclick = () => {
        if (state.debugAnchorsOn) {
          ensureOverlayDisposed('debug-anchors');
          state.debugAnchorsOn = false;
        } else {
          renderDebugAnchors();
        }
        renderSourceTab();
      };
    }
    // Wire reveal buttons → POST /api/reveal { path }. Both the inline
    // file-row 📂 buttons (cs-reveal-icon) and the datasheet "Reveal"
    // CTA (cs-reveal) carry data-path; one handler covers both.
    body.querySelectorAll('.cs-reveal-icon, .cs-reveal').forEach(btn => {
      btn.onclick = async () => {
        const path = btn.dataset.path;
        try {
          await fetch('api/reveal', {
            method: 'POST',
            headers: { 'Content-Type': 'application/json' },
            body: JSON.stringify({ path }),
          });
        } catch (e) { console.error('[chipsmith] reveal failed', e); }
      };
    });
    // Wire "Open full viewer" → opens the same adom-pdf-viewer URL in a
    // new browser tab so the user can read at full screen alongside the
    // chipsmith webview. The inline iframe stays for at-a-glance.
    body.querySelectorAll('.cs-open-pdf').forEach(btn => {
      btn.onclick = () => {
        const href = btn.dataset.href;
        if (href) window.open(href, '_blank', 'noopener');
      };
    });
  }

  // Build the adom-pdf-viewer URL for the loaded chip. The viewer runs
  // on port 8985 (per its SKILL.md) and is reached via the Hydrogen
  // proxy. We pass the datasheet through chipsmith's /api/datasheet
  // endpoint so the viewer fetches a same-origin URL — no CORS surprises.
  // `title` and `reveal` give the viewer's topbar a label + the
  // reveal-in-Explorer button.
  function pdfViewerUrl(spec, paths) {
    // Locate this page's origin + proxy prefix so the constructed URL
    // works under both /proxy/8872/ (Hydrogen) and direct localhost.
    const proxyMatch = location.pathname.match(/^(\/proxy\/\d+\/)/);
    const origin = location.origin;
    // Same-origin URL to chipsmith's /api/datasheet — the pdf-viewer
    // fetches this and renders it in PDF.js.
    const datasheetSrc = (proxyMatch ? proxyMatch[1] : '/') + 'api/datasheet';
    const viewerBase = origin + (proxyMatch ? proxyMatch[1].replace(/\d+/, '8985') : '/proxy/8985/');
    const params = new URLSearchParams();
    params.set('src', datasheetSrc);
    if (spec && spec.mpn) params.set('title', spec.mpn + ' datasheet');
    if (paths && paths.datasheet) params.set('reveal', paths.datasheet);
    return viewerBase + 'v?' + params.toString();
  }

  // ── Tab 2: Measurements ─────────────────────────────────────────
  // Read declared body_dims_mm from the manifest (or extract from the
  // package field as a fallback — many chip-fetcher entries write
  // "OLGA-16 (6.4×3×1.75 mm)" into `package` without populating the
  // structured body_dims_mm yet). Then read the detected body bbox via
  // window.adomStep.bbox('Body') — adom-step returns it in mm — and run
  // a rescale negotiation:
  //
  //   ×1          (declared unit honored)
  //   ÷25.4       (file says mm but is really inches)
  //   ÷1000       (file says mm but is really metres)
  //   ×25.4       (file says inches but is really mm)
  //   ×1000       (file says metres but is really mm)
  //   ÷10         (file says mm but is really cm)
  //
  // Pick the scale whose product (detected × scale) lands within ±5%
  // on every axis (sorted-extents — orientation-agnostic, since chips
  // can ship Y-up vs Z-up; the absolute axis-by-axis match is too
  // brittle here). Status pill: ● if ×1, ◐ if non-1 rescale, ✗ if no
  // scale fits.

  function parseBodyDims(spec) {
    if (!spec) return null;
    if (Array.isArray(spec.body_dims_mm) && spec.body_dims_mm.length === 3) return spec.body_dims_mm.map(Number);
    // Fallback: extract "<W>×<H>×<D> mm" or "<W>x<H>x<D> mm" from the
    // free-text package field. Examples: "OLGA-16 (6.4×3×1.75 mm)"
    // "QFN-32 5x5x0.9 mm".
    const text = String(spec.package || spec.description || '');
    const m = text.match(/(\d+(?:\.\d+)?)\s*[×x]\s*(\d+(?:\.\d+)?)\s*[×x]\s*(\d+(?:\.\d+)?)\s*mm/i);
    if (!m) return null;
    return [parseFloat(m[1]), parseFloat(m[2]), parseFloat(m[3])];
  }

  const RESCALE_CANDIDATES = [
    { factor: 1,        label: '×1',     reason: 'Declared unit honored — STEP says mm and is mm.' },
    { factor: 1/25.4,   label: '÷25.4',  reason: 'STEP says mm but the file is really inches (US-mfr CAD export).' },
    { factor: 1/1000,   label: '÷1000',  reason: 'STEP says mm but the file is really metres (Fusion export bug).' },
    { factor: 25.4,     label: '×25.4',  reason: 'STEP says inches but the file is really mm.' },
    { factor: 1000,     label: '×1000',  reason: 'STEP says metres but the file is really mm.' },
    { factor: 1/10,     label: '÷10',    reason: 'STEP says mm but the file is really cm.' },
  ];

  // Score a rescale candidate. Sort BOTH declared + detected extents
  // ascending (so [4.4, 5.4, 1.5] vs detected [1.5, 5.4, 4.4] still
  // matches), then compute max-axis percent deviation. Lower is better;
  // ≤5% counts as a match.
  function scoreScale(declaredMm, detectedMm, factor) {
    const d  = [...declaredMm].sort((a, b) => a - b);
    const dt = detectedMm.map(v => v * factor).sort((a, b) => a - b);
    let maxPct = 0;
    for (let i = 0; i < 3; i++) {
      const pct = Math.abs(dt[i] - d[i]) / d[i];
      if (pct > maxPct) maxPct = pct;
    }
    return { factor, maxPct, scaledMm: detectedMm.map(v => v * factor) };
  }

  function negotiateScale(declaredMm, detectedMm) {
    let best = null;
    for (const c of RESCALE_CANDIDATES) {
      const s = scoreScale(declaredMm, detectedMm, c.factor);
      const cand = { ...c, ...s };
      if (!best || cand.maxPct < best.maxPct) best = cand;
    }
    return best;
  }

  // Get the body bbox via window.adomStep.bbox('Body'). If "Body" isn't
  // in the assembly tree (some STEPs ship a flat hierarchy), fall back
  // to the whole model's bbox.
  function detectBodyBbox() {
    if (!window.adomStep || typeof window.adomStep.bbox !== 'function') return null;
    let r = window.adomStep.bbox('Body');
    if (!r || r.error) r = window.adomStep.bbox();
    if (!r || r.error || !Array.isArray(r.mm)) return null;
    return { mm: r.mm.map(v => Math.abs(v)), source: r.name };
  }

  function renderMeasurementsTab() {
    const body = document.getElementById('cs-body');
    const declared = parseBodyDims(state.spec);
    const detected = detectBodyBbox();

    if (!declared) {
      state.status.measurements = '○';
      refreshStatusPills();
      body.innerHTML = `
        <div class="cs-section">
          <h3 class="cs-section-title">Measurements</h3>
          <div class="cs-row"><span class="value dim">No <code>body_dims_mm</code> declared in info.json — and the <code>package</code> field doesn't carry a parseable <code>W×H×D mm</code> tuple either. Add <code>"body_dims_mm": [w, h, d]</code> to info.json, then come back.</span></div>
        </div>`;
      return;
    }
    if (!detected) {
      state.status.measurements = '○';
      refreshStatusPills();
      body.innerHTML = `
        <div class="cs-section">
          <h3 class="cs-section-title">Measurements</h3>
          <div class="cs-row"><span class="value dim">Waiting for the model to load — body bbox not available yet. <button class="cs-btn cs-btn-secondary" id="cs-meas-retry" style="margin-top:8px"><span class="lbl">Retry</span></button></span></div>
        </div>`;
      const r = document.getElementById('cs-meas-retry');
      if (r) r.onclick = () => renderMeasurementsTab();
      return;
    }
    const result = negotiateScale(declared, detected.mm);
    const passes = result.maxPct <= 0.05;
    const isUnscaled = Math.abs(result.factor - 1) < 1e-9;
    const status = passes ? (isUnscaled ? '●' : '◐') : '✗';
    state.status.measurements = status;
    refreshStatusPills();
    state.measurements = {
      declared_mm: declared,
      detected_mm: detected.mm,
      detected_source: detected.source,
      applied_scale: passes ? result.factor : 1,
      applied_label: passes ? result.label : '×1',
      max_axis_pct: result.maxPct,
      passes, status,
    };

    const fmt = (a) => a.map(v => v.toFixed(3)).join(' × ');
    const fmtMils = (mm) => (mm / 0.0254).toFixed(1);
    const declaredMils = declared.map(fmtMils).join(' × ');
    const scaledMils   = result.scaledMm.map(fmtMils).join(' × ');
    const pctReadout   = (result.maxPct * 100).toFixed(2) + '%';
    const statusText   = passes
      ? (isUnscaled
        ? 'Match within ±5% — declared mm honored, no rescale needed.'
        : `Auto-rescaled by ${result.label} — ${escAttr(result.reason)} Sign-off will record the applied scale in <code>&lt;mpn&gt;.chipsmith.json</code>.`)
      : `No rescale lands within ±5% on every axis (best: ${result.label}, max Δ ${pctReadout}). Either the declared <code>body_dims_mm</code> is wrong or the STEP is geometrically broken. Sign-off blocked.`;
    const statusClass = passes ? (isUnscaled ? 'ok' : 'partial') : 'fail';

    body.innerHTML = `
      <div class="cs-section">
        <h3 class="cs-section-title">Declared vs detected</h3>
        <div class="cs-row"><span class="label">Declared</span><span class="value mono">${esc(fmt(declared))} mm <span class="dim">(${esc(declaredMils)} mils)</span></span></div>
        <div class="cs-row"><span class="label">Detected</span><span class="value mono">${esc(fmt(detected.mm))} mm <span class="dim">(${esc(detected.mm.map(fmtMils).join(' × '))} mils)</span></span></div>
        <div class="cs-row"><span class="label">Source node</span><span class="value mono">${esc(detected.source)}</span></div>
      </div>
      <div class="cs-section">
        <h3 class="cs-section-title">Rescale negotiation</h3>
        <div class="cs-row"><span class="label">Best scale</span><span class="value mono">${esc(result.label)}</span></div>
        <div class="cs-row"><span class="label">Scaled</span><span class="value mono">${esc(fmt(result.scaledMm))} mm <span class="dim">(${esc(scaledMils)} mils)</span></span></div>
        <div class="cs-row"><span class="label">Max Δ</span><span class="value mono">${esc(pctReadout)}</span></div>
        <div class="cs-row"><span class="label">Tolerance</span><span class="value mono">±5%</span></div>
      </div>
      <div class="cs-section">
        <h3 class="cs-section-title">Status</h3>
        <div class="cs-meas-status pill-${statusClass}">${statusText}</div>
      </div>
      <div class="cs-section">
        <h3 class="cs-section-title">Candidates</h3>
        <table class="cs-tbl">
          <thead><tr><th>Scale</th><th class="num">Max Δ</th><th>Reason</th></tr></thead>
          <tbody>
            ${RESCALE_CANDIDATES.map(c => {
              const s = scoreScale(declared, detected.mm, c.factor);
              const isWinner = Math.abs(c.factor - result.factor) < 1e-9;
              const ok = s.maxPct <= 0.05;
              return `<tr class="${isWinner ? 'is-winner' : ''} ${ok ? 'is-ok' : ''}">
                <td class="mono">${esc(c.label)}</td>
                <td class="num mono">${(s.maxPct * 100).toFixed(2)}%</td>
                <td class="dim">${esc(c.reason)}</td>
              </tr>`;
            }).join('')}
          </tbody>
        </table>
      </div>
    `;
  }

  // ── Tab 4: Footprint ────────────────────────────────────────────
  // /api/footprint-sources already returns parsed pads from
  // KiCad / Fusion (.lbr) / Altium / datasheet. v1.0 renders a summary
  // + cross-source agreement matrix; full 3D rendering of the pads at
  // z=0 with bbox-fit raycast is v1.1 (needs Babylon scene mutation
  // hooks beyond the current adom-step API surface).

  function fpExtent(pads) {
    if (!pads || !pads.length) return null;
    let minX = Infinity, maxX = -Infinity, minY = Infinity, maxY = -Infinity;
    for (const p of pads) {
      const cx = p.at?.[0] || 0, cy = p.at?.[1] || 0;
      const w = p.size?.[0] || 0, h = p.size?.[1] || 0;
      minX = Math.min(minX, cx - w/2); maxX = Math.max(maxX, cx + w/2);
      minY = Math.min(minY, cy - h/2); maxY = Math.max(maxY, cy + h/2);
    }
    return { width: maxX - minX, height: maxY - minY };
  }

  // ── 3D overlay layers ──────────────────────────────────────────
  // Add footprint pads as thin extruded rectangles at z≈0, faux PCB
  // plate as a 1.6mm FR4 box below the chip, and heatsink vias as
  // cylinders inside the EP. Each overlay is registered with adom-step
  // under a named group so it can be toggled / disposed as a unit.
  //
  // Coordinate system: GLB is in metres (1 unit = 1 mm × 0.001). All
  // footprint pad coords + sizes from /api/footprint-sources are in
  // mm — convert via × 0.001 before authoring meshes.

  const MM = 0.001;

  // ── World-up convention ─────────────────────────────────────────
  // chip-fetcher GLBs come authored Z-up (CAD/EE convention; camera
  // upVector = [0,0,1]). Older Adom 3D viewers were Y-up. Every
  // chipsmith renderer that places overlays at "z above the seat
  // plane" must consult these helpers — picking Y vs Z literally
  // changes which way is "up" in the world.
  //
  // worldUpIdx()    → 1 (Y-up) | 2 (Z-up)
  // planeAxes()     → [0, 2] for Y-up, [0, 1] for Z-up
  //                   The two scene-axis indices that make up the
  //                   horizontal plane (where the PCB lives).
  // fpPlace(x, y, h_mm)
  //                 → returns [worldX, worldY, worldZ] (in scene
  //                   units, metres) for placing a feature at
  //                   footprint-x = x mm, footprint-y = y mm,
  //                   height-above-ground = h mm. Use this for every
  //                   overlay mesh that sits on / above / below the
  //                   PCB plane.
  // upRotation()    → Babylon Euler-angle rotation needed to make a
  //                   default-Z-facing plane (CreatePlane) lie flat
  //                   on the ground. Y-up = π/2 about X; Z-up = 0
  //                   (default plane already faces +Z).
  function worldUpIdx() {
    if (typeof window === 'undefined' || !window.adomStep || !window.adomStep.getScene) return 1;
    try {
      const cam = window.adomStep.getScene().activeCamera;
      if (cam && cam.upVector && Math.abs(cam.upVector.z) > 0.5) return 2;
    } catch (_) {}
    return 1;
  }
  function planeAxes() {
    return worldUpIdx() === 2 ? [0, 1] : [0, 2];
  }
  function fpPlace(fpX_mm, fpY_mm, h_mm) {
    const upI = worldUpIdx();
    const ax = planeAxes();
    const out = [0, 0, 0];
    out[ax[0]] = (fpX_mm || 0) * MM;
    out[ax[1]] = (fpY_mm || 0) * MM;
    out[upI]   = (h_mm   || 0) * MM;
    return out;
  }
  function upRotation() {
    return worldUpIdx() === 1 ? Math.PI / 2 : 0;
  }
  // For CreateBox — pick the (width, height, depth) tuple so that the
  // box's THIN axis aligns with world-up. plane_w / plane_h map to the
  // two horizontal axes; thickness maps to world-up.
  function flatBoxDims(plane_w_mm, plane_h_mm, thickness_mm) {
    const upI = worldUpIdx();
    if (upI === 1) {
      // Y-up: thickness in Y, big dims in X (width) + Z (depth)
      return { width: plane_w_mm * MM, height: thickness_mm * MM, depth: plane_h_mm * MM };
    }
    // Z-up: thickness in Z, big dims in X (width) + Y (height)
    return { width: plane_w_mm * MM, height: plane_h_mm * MM, depth: thickness_mm * MM };
  }
  // Read body bbox extent along the world-up axis. Use this anywhere
  // the legacy code grabbed bb.max[1] / bb.min[1] expecting "top" /
  // "bottom" — those work in Y-up but read the chip's LENGTH axis in
  // Z-up. Returns { min, max, center } in scene units (metres).
  function bodyHeightExtent() {
    const bb = window.adomStep && window.adomStep.bbox && window.adomStep.bbox('Body');
    if (!bb || bb.error) return null;
    const upI = worldUpIdx();
    return {
      min: bb.min[upI],
      max: bb.max[upI],
      center: (bb.min[upI] + bb.max[upI]) / 2,
      // Plane-axis extents (everything not the up axis), useful for
      // sizing decals that should fit the chip's top face.
      planeW: bb.max[planeAxes()[0]] - bb.min[planeAxes()[0]],
      planeH: bb.max[planeAxes()[1]] - bb.min[planeAxes()[1]],
      planeCx: (bb.min[planeAxes()[0]] + bb.max[planeAxes()[0]]) / 2,
      planeCy: (bb.min[planeAxes()[1]] + bb.max[planeAxes()[1]]) / 2,
    };
  }
  // Fill in the spec fields chipsmith needs to run gates from a
  // single chip-fetcher-style `package` string. chip-fetcher writes
  // strings like "SOIC-8 (D, 4.9×3.9 mm, 1.27 mm pitch)" and
  // "VQFN-32 (RRG, 5×5×0.9 mm)" but leaves the structured fields
  // (pin_count / package_kind / structural_type / body_dims_mm /
  // expected_pitch_mm / has_ep) null. We parse the string here so the
  // user doesn't have to hand-populate every chip's info.json before
  // chipsmith works. Existing non-null fields are NEVER overwritten.
  //
  // Coverage: every package family in
  // gallia/skills/electrical-engineering/SKILL.md §"Package families".
  // When a string fails to parse, returns the input unchanged.
  function enrichSpec(spec) {
    if (!spec || typeof spec !== 'object') return spec;
    const out = Object.assign({}, spec);
    const pkgStr = (spec.package || spec.package_family || '').toString();
    // Family + pin count: leading word then optional dash + integer.
    // "VQFN-32 (RRG, 5×5×0.9 mm)"           → kind "VQFN", pins 32, family "VQFN-32"
    // "SOIC-8 (D, 4.9×3.9 mm, 1.27 mm pitch)" → kind "SOIC", pins 8, family "SOIC-8"
    // "SOT-23 (3-lead)"                     → kind "SOT", pins 3, family "SOT-23-3"
    // "LGA-16 (4.4x5.4mm Pitch0.5mm)"       → kind "LGA", pins 16, family "LGA-16"
    let m = pkgStr.match(/^([A-Z][A-Z0-9-]*?)-(\d+)\b/i);
    if (m) {
      if (out.package_kind == null)   out.package_kind = m[1].toUpperCase().replace(/^V?Q?FN$/, 'QFN').replace(/^[VST]?SO[PI]C?$/, 'SOIC');
      if (out.pin_count == null)      out.pin_count = parseInt(m[2], 10);
      if (out.package_family == null) out.package_family = `${m[1].toUpperCase()}-${m[2]}`;
    } else if ((m = pkgStr.match(/^(SOT-23(?:-\d+)?|TO-220|TO-247|DPAK|D2PAK|USB-?C|RJ-?45|DIP-?\d*|SIP-?\d*)/i))) {
      // Connector-ish + special non-dash families.
      if (out.package_kind == null)   out.package_kind = m[1].toUpperCase().replace(/-.*$/, '');
      if (out.package_family == null) out.package_family = m[1].toUpperCase();
    }

    // body_dims_mm: parse "5×5×0.9 mm" / "4.9x3.9 mm" / "5×5×0.9mm" /
    // "4.4×5.4×1.5 mm". Fall back to two-axis if no third found.
    if (!Array.isArray(out.body_dims_mm)) {
      m = pkgStr.match(/(\d+(?:\.\d+)?)\s*[×xX]\s*(\d+(?:\.\d+)?)(?:\s*[×xX]\s*(\d+(?:\.\d+)?))?\s*mm/);
      if (m) {
        const w = parseFloat(m[1]), h = parseFloat(m[2]), z = m[3] ? parseFloat(m[3]) : null;
        // Per-package-kind default thickness when not given. Datasheet
        // typical, NOT max. Conservative — gates use ±5%.
        const kind = out.package_kind || '';
        const defaultZ = ({
          SOIC: 1.65, SSOP: 1.20, TSSOP: 1.10, MSOP: 1.10, VSSOP: 1.10,
          QFN:  0.85, DFN: 0.85, VQFN: 0.85, LGA: 0.93, BGA: 1.10,
          SOT:  1.10, TQFP: 1.40, LQFP: 1.40, QFP: 1.40, DIP: 4.00,
        }[kind] || 1.00);
        out.body_dims_mm = z != null ? [w, h, z] : [w, h, defaultZ];
      }
    }

    // expected_pitch_mm: parse "0.5 mm pitch" / "Pitch 0.5mm" / "1.27 mm pitch".
    if (out.expected_pitch_mm == null) {
      m = pkgStr.match(/(?:Pitch\s*|pitch[ :]*|P)(\d+(?:\.\d+)?)\s*mm|(\d+(?:\.\d+)?)\s*mm\s*pitch/i);
      if (m) out.expected_pitch_mm = parseFloat(m[1] || m[2]);
      else if (out.package_kind) {
        // Fallback by package kind (canonical pitch per family).
        out.expected_pitch_mm = ({
          SOIC: 1.27, SSOP: 0.65, TSSOP: 0.65, MSOP: 0.65, VSSOP: 0.50,
          QFN:  0.50, DFN: 0.50, VQFN: 0.50, LGA: 0.50, BGA: 0.50,
          DIP:  2.54, SOT: 0.95, TQFP: 0.50, LQFP: 0.50, QFP: 0.65,
        }[out.package_kind] || null);
      }
    }

    // structural_type: derived from package_kind.
    if (out.structural_type == null && out.package_kind) {
      out.structural_type = ({
        SOIC: 'smd_leg', SSOP: 'smd_leg', TSSOP: 'smd_leg', MSOP: 'smd_leg', VSSOP: 'smd_leg',
        TQFP: 'smd_leg', LQFP: 'smd_leg', QFP: 'smd_leg', SOT: 'smd_leg',
        QFN: 'smd_pad', DFN: 'smd_pad', VQFN: 'smd_pad', LGA: 'smd_pad',
        BGA: 'ball',
        DIP: 'thru_hole_pin', PDIP: 'thru_hole_pin',
      }[out.package_kind] || null);
    }

    // has_ep: QFN/DFN/VQFN almost always. LGA sometimes. Others rarely.
    if (out.has_ep == null && out.package_kind) {
      const epKinds = new Set(['QFN', 'DFN', 'VQFN', 'TQFN']);
      // Mark LGA "maybe EP" — datasheet-specific. Conservative default true
      // for LGA so the heatsink-via gate runs.
      if (epKinds.has(out.package_kind)) out.has_ep = true;
    }

    return out;
  }

  // Apply the standard "lay down on the ground plane" rotation to a
  // mesh whose long axis is Babylon's default +Y (cylinder, capsule).
  // Y-up: leave Y-axis alone. Z-up: rotate so Y maps to Z.
  function alignVerticalToWorldUp(mesh) {
    if (worldUpIdx() === 2) {
      mesh.rotation.x = Math.PI / 2;
    }
  }
  // Place a mesh at footprint (fpX, fpY) mm with its center at world-up
  // height h_mm. Assigns to mesh.position directly.
  function placeMeshAt(mesh, fpX_mm, fpY_mm, h_mm) {
    const p = fpPlace(fpX_mm, fpY_mm, h_mm);
    mesh.position.x = p[0];
    mesh.position.y = p[1];
    mesh.position.z = p[2];
  }

  // Source-tinted overlay colors per the plan §"Multi-source footprint
  // cross-validation". Datasheet = canonical truth (orange); KiCad =
  // green; Fusion = blue; Altium = purple.
  const SRC_COLOR = {
    datasheet: { r: 1.00, g: 0.55, b: 0.10 },
    kicad:     { r: 0.30, g: 0.85, b: 0.45 },
    fusion:    { r: 0.30, g: 0.55, b: 0.95 },
    altium:    { r: 0.65, g: 0.40, b: 0.85 },
    // ds2sf v0.6.0 — 5th independent pad-position source. Warm amber
    // distinct from datasheet-orange and kicad-green.
    ds2sf:     { r: 0.95, g: 0.75, b: 0.20 },
    // Stock-library ghost — service-kicad-backed authoritative footprint.
    // Translucent off-white so it reads as a "ghost" against any of the
    // four user sources without competing for the eye.
    stock:     { r: 0.85, g: 0.85, b: 0.92 },
  };

  function ensureOverlayDisposed(group) {
    if (window.adomStep && window.adomStep.removeOverlay) {
      try { window.adomStep.removeOverlay(group); } catch (_) {}
    }
  }

  const SRC_LABEL = { kicad: 'KiCad', fusion: 'Fusion (.lbr)', altium: 'Altium (.PcbLib)', datasheet: 'Datasheet-derived', ds2sf: 'ds2sf pads' };

  function renderFootprintOverlay(srcKey, padsMm) {
    if (!(window.adomStep && window.adomStep.getScene && window.adomStep.getBabylon)) return null;
    const scene = window.adomStep.getScene();
    const B = window.adomStep.getBabylon();
    if (!scene || !B) return null;
    const group = 'fp-' + srcKey;
    ensureOverlayDisposed(group);
    const color = SRC_COLOR[srcKey] || { r: 0.5, g: 0.5, b: 0.5 };
    const mat = new B.StandardMaterial(group + '-mat', scene);
    mat.diffuseColor = new B.Color3(color.r, color.g, color.b);
    mat.alpha = 0.55;
    mat.specularColor = new B.Color3(0, 0, 0);
    mat.backFaceCulling = false;
    mat.zOffset = -1;
    // Per-source Z stagger — when multiple sources render their pads at
    // the same height they Z-fight. Stagger 5 µm between sources so the
    // user can visibly stack them in the outline without flicker.
    // datasheet sits lowest (it's the canonical truth, behind everything),
    // then kicad / fusion / altium ascending.
    const Z_STAGGER = { datasheet: 0.020, kicad: 0.025, fusion: 0.030, altium: 0.035, ds2sf: 0.040 };
    const padHeight_mm = Z_STAGGER[srcKey] != null ? Z_STAGGER[srcKey] : 0.025;

    // Build per-pad children — each pad gets its own outline row with
    // inline metadata: number, shape, size, layers, rotation.
    // Pad rendering covers KiCad's full shape vocabulary:
    //   • rect / rounded — box (rounded uses box too; corner radius
    //     would need CSG which isn't worth it for an overlay).
    //   • circle — cylinder along world-up axis.
    //   • oval — capsule along world-up; long axis = footprint X
    //     (rotation applied after).
    //   • custom polygon — fallback to rect bbox.
    // Pad rotation (third value of `at`, in degrees) rotates the mesh
    // around the world-up axis. Critical for SOIC/BGA layouts where
    // pads are rotated 90° vs the part's symbol orientation.
    const children = [];
    for (const p of padsMm) {
      if (!p.size || p.size[0] <= 0 || p.size[1] <= 0) continue;
      const fpX = p.at?.[0] || 0;
      const fpY = p.at?.[1] || 0;
      const rotDeg = p.at?.[2] || 0;
      const w = p.size[0], h = p.size[1];
      const shape = (p.shape || 'rect').toLowerCase();
      let m;
      const cr = p.corner_radius || 0;
      if (shape === 'circle') {
        m = B.MeshBuilder.CreateCylinder(`${group}-pad-${p.number}`, {
          diameter: Math.max(w, h) * MM, height: 0.04 * MM, tessellation: 24,
        }, scene);
        alignVerticalToWorldUp(m);
      } else if (shape === 'oval') {
        const longer = Math.max(w, h) * MM;
        const shorter = Math.min(w, h) * MM;
        m = B.MeshBuilder.CreateCapsule(`${group}-pad-${p.number}`, {
          radius: shorter / 2, height: longer, tessellation: 18,
        }, scene);
        alignVerticalToWorldUp(m);
        if (w >= h) {
          if (worldUpIdx() === 1) m.rotation.z = Math.PI / 2;
          else                    m.rotation.y = Math.PI / 2;
        }
        m.scaling.y = (0.04 * MM) / longer;
      } else if ((shape === 'roundrect' || cr > 0.01) && typeof window.earcut === 'function') {
        // Rounded-rect pad via earcut. Quarter-circle arcs at each
        // corner, 6 points per arc. Two face layers (top + bottom at
        // ±0.02mm) so the mesh has Z-extent and scene.pick works.
        const hw = w / 2, hh = h / 2;
        const r = Math.min(cr, hw, hh);
        const N = 6;
        const pts2d = [];
        for (let i = 0; i <= N; i++) { const a = -Math.PI/2 + (Math.PI/2)*i/N; pts2d.push([hw - r + r*Math.cos(a), -hh + r + r*Math.sin(a)]); }
        for (let i = 0; i <= N; i++) { const a = 0 + (Math.PI/2)*i/N; pts2d.push([hw - r + r*Math.cos(a), hh - r + r*Math.sin(a)]); }
        for (let i = 0; i <= N; i++) { const a = Math.PI/2 + (Math.PI/2)*i/N; pts2d.push([-hw + r + r*Math.cos(a), hh - r + r*Math.sin(a)]); }
        for (let i = 0; i <= N; i++) { const a = Math.PI + (Math.PI/2)*i/N; pts2d.push([-hw + r + r*Math.cos(a), -hh + r + r*Math.sin(a)]); }
        const flat = new Float64Array(pts2d.length * 2);
        for (let i = 0; i < pts2d.length; i++) { flat[i*2] = pts2d[i][0]; flat[i*2+1] = pts2d[i][1]; }
        const tris = window.earcut(flat);
        if (tris && tris.length >= 3) {
          const upI = worldUpIdx();
          const nPts = pts2d.length;
          const thick = 0.04 * MM;
          const positions = new Float32Array(nPts * 2 * 3);
          const normals = new Float32Array(nPts * 2 * 3);
          for (let k = 0; k < nPts; k++) {
            const px = pts2d[k][0] * MM, py = pts2d[k][1] * MM;
            if (upI === 2) {
              positions[k*3] = px; positions[k*3+1] = py; positions[k*3+2] = thick/2;
              positions[(nPts+k)*3] = px; positions[(nPts+k)*3+1] = py; positions[(nPts+k)*3+2] = -thick/2;
            } else {
              positions[k*3] = px; positions[k*3+1] = thick/2; positions[k*3+2] = -py;
              positions[(nPts+k)*3] = px; positions[(nPts+k)*3+1] = -thick/2; positions[(nPts+k)*3+2] = -py;
            }
            normals[k*3+upI] = 1;
            normals[(nPts+k)*3+upI] = -1;
          }
          const indices = new Uint32Array(tris.length * 2);
          for (let k = 0; k < tris.length; k += 3) {
            indices[k] = tris[k]; indices[k+1] = tris[k+1]; indices[k+2] = tris[k+2];
            indices[tris.length+k] = tris[k]+nPts; indices[tris.length+k+1] = tris[k+2]+nPts; indices[tris.length+k+2] = tris[k+1]+nPts;
          }
          m = new B.Mesh(`${group}-pad-${p.number}`, scene);
          const vd = new B.VertexData();
          vd.positions = positions; vd.indices = indices; vd.normals = normals;
          vd.applyToMesh(m, false);
        } else {
          m = B.MeshBuilder.CreateBox(`${group}-pad-${p.number}`, flatBoxDims(w, h, 0.04), scene);
        }
      } else {
        m = B.MeshBuilder.CreateBox(`${group}-pad-${p.number}`, flatBoxDims(w, h, 0.04), scene);
      }
      const pos = fpPlace(fpX, fpY, padHeight_mm);
      m.position.x = pos[0]; m.position.y = pos[1]; m.position.z = pos[2];
      // Apply pad rotation around the world-up axis.
      if (rotDeg) {
        const rotRad = -rotDeg * Math.PI / 180;   // KiCad rotates CW; Babylon Y-rotation is CCW
        if (worldUpIdx() === 1) m.rotation.y = (m.rotation.y || 0) + rotRad;
        else                    m.rotation.z = (m.rotation.z || 0) + rotRad;
      }
      m.material = mat;
      const layerStr = (p.layers || []).map(l => l.replace(/^F\./,'F.').replace(/^B\./,'B.').replace(/^\*\./, '*.')).join('+');
      const rotStr = rotDeg ? `  rot ${rotDeg.toFixed(0)}°` : '';
      const nameStr = p.name ? ` (${p.name})` : '';
      const shapeStr = cr > 0.01 ? `roundrect r=${cr.toFixed(2)}` : shape;
      const label = `pad ${p.number}${nameStr}  ${shapeStr} ${w.toFixed(2)}×${h.toFixed(2)} mm  c=(${fpX.toFixed(2)},${fpY.toFixed(2)})${rotStr}  ${layerStr}`;
      children.push({ meshes: [m], label, kind: 'overlay-fp-pad' });
    }
    const SRC_PROVENANCE = {
      kicad:     'Parsed from the .kicad_mod file in the chip-fetcher library.\nThird-party footprint — a KiCad library maintainer traced the\nmanufacturer datasheet into this format.',
      fusion:    'Parsed from the Eagle .lbr file (Fusion 360 format) in the\nchip-fetcher library. Third-party footprint — authored by\nSnapEDA, SnapMagic, or Ultra Librarian.',
      altium:    'Parsed from the .PcbLib file in the chip-fetcher library.\nThird-party footprint — authored by the Altium library\necosystem (SnapEDA, SnapMagic, Ultra Librarian, or CSE).',
      datasheet: 'Derived from the manufacturer datasheet PDF. First-party\nsource — the pad positions trace directly to the mechanical\ndrawing page, not to any EDA library.',
      ds2sf:     'Extracted by ds2sf directly from the manufacturer datasheet\nmechanical drawing. This is the only source that traces pad\npositions back to the manufacturer — every other source is a\nthird-party interpretation. Disagreements here mean the EDA\nlibraries may have gotten it wrong.',
    };
    const provStr = SRC_PROVENANCE[srcKey] || '';
    const displayLabel = `${SRC_LABEL[srcKey] || srcKey} footprint  (${children.length} pads)`;
    window.adomStep.addOverlay({
      group,
      label: displayLabel,
      tooltip: provStr ? `${displayLabel}\n\n${provStr}` : displayLabel,
      kind: 'overlay-footprint',
      children,
    });
    return { group, count: children.length };
  }

  // Render a 1.6 mm FR4 plate sized to the footprint extent + 5 mm
  // margin, CSG-drilled with every hole declared in the footprint
  // pads. Four hole types (per the plan):
  //   • plated round (PTH) — pad.type == 'thru_hole', drill.shape circle
  //   • unplated round (NPTH) — pad.type == 'np_thru_hole', drill.shape circle
  //   • plated slot — pad.type == 'thru_hole', drill.shape oval
  //   • unplated slot — pad.type == 'np_thru_hole', drill.shape oval
  // Plated entries get a Cu-tone (0xC79A4F) inner-bore shell + a top
  // and bottom annular ring so the plating reads visibly on zoom. CSG
  // is via Babylon's B.CSG; we union all holes once then subtract in
  // a single pass so a 12-pin DIP doesn't take 12 round-trips.
  function renderFauxPcbPlate(extentMm, pads) {
    if (!extentMm) return null;
    if (!(window.adomStep && window.adomStep.getScene && window.adomStep.getBabylon)) return null;
    const scene = window.adomStep.getScene();
    const B = window.adomStep.getBabylon();
    if (!scene || !B) return null;
    const group = 'faux-pcb';
    ensureOverlayDisposed(group);
    const padMargin = 5;
    const plateW_mm = extentMm.width  + padMargin * 2;
    const plateH_mm = extentMm.height + padMargin * 2;
    const w = plateW_mm * MM;
    const h = plateH_mm * MM;
    const t_mm = 1.6;
    const plate = B.MeshBuilder.CreateBox('faux-pcb-plate', flatBoxDims(plateW_mm, plateH_mm, t_mm), scene);
    // Plate top face flush with seat plane (height = 0). Center of
    // the plate is therefore 0.8 mm BELOW the seat plane.
    placeMeshAt(plate, 0, 0, -t_mm / 2);
    const matFR4 = new B.StandardMaterial('faux-pcb-mat', scene);
    matFR4.diffuseColor = new B.Color3(0.08, 0.35, 0.18);
    matFR4.alpha = 1.0;
    matFR4.specularColor = new B.Color3(0.05, 0.10, 0.07);
    matFR4.backFaceCulling = false;
    plate.material = matFR4;

    // Bucket pads by hole kind. SMD (no drill) is filtered out.
    // ALSO filter out thru-hole pads that fall inside the EP region
    // when chipsmith has its own heatsink-via grid: those holes belong
    // to the heatsink overlay (rendered as hollow Cu barrels), not to
    // the faux PCB. Otherwise the user sees the same holes drilled
    // twice — once by the faux PCB CSG, once by the heatsink overlay.
    // (Manufacturer .kicad_mod files often include the recommended
    // heatsink-via grid as thru_hole pads inside the EP. chipsmith
    // owns those when its grid is on.)
    const ep = state.heatsinkGrid?.ep;
    const epHalfX = ep ? ep.size_mm[0] / 2 : 0;
    const epHalfY = ep ? ep.size_mm[1] / 2 : 0;
    const epOx   = ep ? ep.origin_mm.x : 0;
    const epOy   = ep ? ep.origin_mm.y : 0;
    function isInsideEp(x_mm, y_mm) {
      if (!ep) return false;
      return Math.abs(x_mm - epOx) <= epHalfX && Math.abs(y_mm - epOy) <= epHalfY;
    }
    let suppressedByEp = 0;
    const drilled = [];
    if (Array.isArray(pads)) {
      for (const p of pads) {
        const d = p && p.drill;
        if (!d || typeof d !== 'object') continue;
        const isPlated = (p.type === 'thru_hole');
        const isNP     = (p.type === 'np_thru_hole');
        if (!isPlated && !isNP) continue;
        const isSlot = d.shape === 'oval';
        const at = Array.isArray(p.at) ? p.at : [0, 0, 0];
        let dw, dh;
        if (isSlot && Array.isArray(d.size)) { dw = d.size[0]; dh = d.size[1]; }
        else { dw = d.diameter || 0; dh = d.diameter || 0; }
        if (dw <= 0 || dh <= 0) continue;
        // EP suppression — owned by the heatsink overlay.
        if (isInsideEp(at[0] || 0, at[1] || 0)) { suppressedByEp++; continue; }
        drilled.push({
          number: p.number || '?',
          x_mm: at[0] || 0,
          y_mm: at[1] || 0,
          dw_mm: dw,
          dh_mm: dh,
          isPlated, isSlot,
        });
      }
    }
    if (suppressedByEp > 0) {
      console.log(`[chipsmith] faux PCB: suppressed ${suppressedByEp} thru-hole pad(s) inside the EP (owned by heatsink overlay)`);
    }
    state.fauxPcbSuppressedEpHoles = suppressedByEp;

    // CSG-subtract every hole at once. Babylon CSG is slow to construct
    // per-mesh, so we union in a tight loop. Hole height is plate
    // thickness + 0.2 mm overrun on each side to guarantee a clean cut
    // through both faces (CSG is sensitive to coplanar shells).
    let plateMesh = plate;
    if (drilled.length) {
      const overrun_mm = t_mm + 0.4;
      let cutCSG = null;
      const cutMeshes = [];
      for (const hole of drilled) {
        let cutter;
        if (hole.isSlot) {
          // Capsule (slot cutter) — Babylon's default capsule axis is +Y;
          // we lay it along world-up so it cuts through the plate's
          // thickness, then orient it within the plane so its length
          // runs along whichever footprint axis is longer.
          const longer  = Math.max(hole.dw_mm, hole.dh_mm) * MM;
          const shorter = Math.min(hole.dw_mm, hole.dh_mm) * MM;
          cutter = B.MeshBuilder.CreateCapsule('cutter-' + hole.number, {
            radius: shorter / 2, height: longer, tessellation: 18,
          }, scene);
          // 1) Lay along world-up. 2) Rotate within the plane so the
          // capsule's length runs along the longer footprint axis.
          alignVerticalToWorldUp(cutter);
          if (hole.dw_mm >= hole.dh_mm) {
            // Long axis along plane[0] (footprint X)
            if (worldUpIdx() === 1) cutter.rotation.z = Math.PI / 2;
            else                    cutter.rotation.y = Math.PI / 2;
          }
          cutter.scaling.y = (overrun_mm * MM) / longer;
          placeMeshAt(cutter, hole.x_mm, hole.y_mm, -t_mm / 2);
        } else {
          // Cylinder cutter — default Babylon axis is +Y, align to
          // world-up so it cuts through plate thickness.
          cutter = B.MeshBuilder.CreateCylinder('cutter-' + hole.number, {
            diameter: hole.dw_mm * MM, height: overrun_mm * MM, tessellation: 24,
          }, scene);
          alignVerticalToWorldUp(cutter);
          placeMeshAt(cutter, hole.x_mm, hole.y_mm, -t_mm / 2);
        }
        cutter.isVisible = false;
        cutMeshes.push(cutter);
        const csg = B.CSG.FromMesh(cutter);
        cutCSG = cutCSG ? cutCSG.union(csg) : csg;
      }
      try {
        const plateCSG = B.CSG.FromMesh(plate);
        const drilledCSG = plateCSG.subtract(cutCSG);
        const drilledMesh = drilledCSG.toMesh('faux-pcb-plate-drilled', matFR4, scene);
        // CSG.toMesh re-anchors at world origin; plate top face stays
        // flush with seat plane (height=0) because the source plate
        // was already in that pose.
        plate.dispose();
        plateMesh = drilledMesh;
      } catch (e) {
        console.warn('[chipsmith] CSG drill failed; rendering undrilled plate', e);
      }
      for (const c of cutMeshes) c.dispose();
    }

    // Cu plating + annular rings for plated holes/slots only.
    const matCu = new B.StandardMaterial('faux-pcb-cu', scene);
    matCu.diffuseColor = new B.Color3(0.78, 0.55, 0.25);
    matCu.specularColor = new B.Color3(0.30, 0.20, 0.10);
    const plateMeshes = [plateMesh];
    const annularT = 0.05 * MM;
    const ringExtra = 0.20 * MM; // ring extends 0.20 mm past hole edge
    const platedPTH = drilled.filter(h =>  h.isPlated && !h.isSlot);
    const platedSLT = drilled.filter(h =>  h.isPlated &&  h.isSlot);
    const npPTH     = drilled.filter(h => !h.isPlated && !h.isSlot);
    const npSLT     = drilled.filter(h => !h.isPlated &&  h.isSlot);
    function addPlatingForRound(hole, target) {
      const innerR_mm = hole.dw_mm / 2;
      const outerR_mm = innerR_mm + (ringExtra / MM);
      // Inner-bore plating (cylinder along world-up axis, plate-thick
      // minus a hair so it's visible inside the drilled hole).
      const bore = B.MeshBuilder.CreateCylinder('plating-bore-' + hole.number, {
        diameter: innerR_mm * 2 * MM + 0.005 * MM,
        height: (t_mm - 0.001) * MM,
        tessellation: 24,
      }, scene);
      alignVerticalToWorldUp(bore);
      placeMeshAt(bore, hole.x_mm, hole.y_mm, -t_mm / 2);
      bore.material = matCu;
      target.push(bore);
      // Top + bottom annular pad rings.
      for (const h_mm of [0.001, -(t_mm + 0.001)]) {
        const ring = B.MeshBuilder.CreateCylinder('plating-ring-' + hole.number + '-' + h_mm.toFixed(4), {
          diameterTop: outerR_mm * 2 * MM, diameterBottom: outerR_mm * 2 * MM,
          height: annularT, tessellation: 24,
        }, scene);
        alignVerticalToWorldUp(ring);
        placeMeshAt(ring, hole.x_mm, hole.y_mm, h_mm);
        ring.material = matCu;
        target.push(ring);
      }
    }
    function addPlatingForSlot(hole, target) {
      const longer  = Math.max(hole.dw_mm, hole.dh_mm) * MM;
      const shorter = Math.min(hole.dw_mm, hole.dh_mm) * MM;
      const outerLong  = longer  + ringExtra * 2;
      const outerShort = shorter + ringExtra * 2;
      for (const h_mm of [0.001, -(t_mm + 0.001)]) {
        const ring = B.MeshBuilder.CreateCapsule('slot-ring-' + hole.number + '-' + h_mm.toFixed(4), {
          radius: outerShort / 2, height: outerLong, tessellation: 18,
        }, scene);
        // 1) Lay along world-up; 2) rotate within plane so length runs
        // along the longer footprint axis. 3) Squash along its long
        // axis to annular-thickness.
        alignVerticalToWorldUp(ring);
        if (hole.dw_mm >= hole.dh_mm) {
          if (worldUpIdx() === 1) ring.rotation.z = Math.PI / 2;
          else                    ring.rotation.y = Math.PI / 2;
        }
        ring.scaling.y = annularT / outerLong;
        placeMeshAt(ring, hole.x_mm, hole.y_mm, h_mm);
        ring.material = matCu;
        target.push(ring);
      }
    }
    const platingMeshes = [];
    for (const h of platedPTH) addPlatingForRound(h, platingMeshes);
    for (const h of platedSLT) addPlatingForSlot(h, platingMeshes);

    // Build per-bucket child rows. Each leaf row carries a list of
    // pad numbers in its label so the user can scan which holes ended
    // up where without expanding nine times.
    function summary(holes, kindLabel, allMeshes) {
      if (!holes.length) {
        return { meshes: [], label: `${kindLabel}  0`, kind: 'overlay-pcb-' + kindLabel.toLowerCase().replace(/[^a-z]/g, '-') };
      }
      const nums = holes.map(h => h.number).join(', ');
      const sizeStr = holes[0].isSlot
        ? holes.map(h => `${h.dw_mm.toFixed(2)}×${h.dh_mm.toFixed(2)}`).join(', ')
        : holes.map(h => `Ø${h.dw_mm.toFixed(2)}`).join(', ');
      return {
        meshes: allMeshes,
        label: `${kindLabel}  ${holes.length}  pads ${nums}  ${sizeStr} mm`,
        kind: 'overlay-pcb-' + kindLabel.toLowerCase().replace(/[^a-z]/g, '-'),
      };
    }

    const children = [
      { meshes: [plateMesh, ...platingMeshes], label: `FR4 plate  ${plateW_mm.toFixed(1)}×${plateH_mm.toFixed(1)}×1.6 mm  translucent green${platingMeshes.length ? `  + ${platingMeshes.length} Cu plating shells` : ''}`, kind: 'overlay-pcb-plate' },
      summary(platedPTH, 'Plated holes (PTH)', platingMeshes),
      summary(npPTH,     'Unplated holes (NPTH)', []),
      summary(platedSLT, 'Plated slots',     platingMeshes),
      summary(npSLT,     'Unplated slots',   []),
    ];
    if (suppressedByEp > 0) {
      children.push({
        meshes: [],
        label: `↳ ${suppressedByEp} EP-region thru-hole(s) suppressed — owned by Heatsink overlay`,
        kind: 'overlay-pcb-ep-deferred',
      });
    }
    const epNote = suppressedByEp > 0 ? `  ·  ${suppressedByEp} deferred to Heatsink` : '';
    window.adomStep.addOverlay({
      group,
      label: `Faux PCB  (1.6 mm FR4, ${plateW_mm.toFixed(1)}×${plateH_mm.toFixed(1)} mm  ·  ${drilled.length} drill${drilled.length === 1 ? '' : 's'}${epNote})`,
      kind: 'overlay-pcb',
      children,
    });
    state.fauxPcbDims = { w_mm: plateW_mm, h_mm: plateH_mm, drilled };
    return { group, w_mm: plateW_mm, h_mm: plateH_mm, drilled };
  }

  // ── ds2sf SVG vector parser ─────────────────────────────────────
  // ds2sf gives PURE VECTOR DATA (mm-accurate <path> elements with
  // viewBox in mm). Rasterizing it to a texture and slapping it on
  // a plane was the original approach — looked nasty the moment you
  // zoomed in (texture filtering blur + mipmap loss). This pass
  // parses each <path>'s d-attribute into 2D points and builds real
  // Babylon polygon meshes via earcut, so the geometry stays sharp
  // at any zoom — same way the chip body and pad overlays do.
  function parseSvgPathD(d) {
    if (!d) return [];
    const tokens = d.replace(/\s+/g, ' ').match(/[MLZmlz]|-?\d*\.?\d+(?:[eE][+-]?\d+)?/g) || [];
    const points = [];
    let cmd = null;
    let i = 0;
    while (i < tokens.length) {
      const t = tokens[i];
      if (/^[MLZmlz]$/.test(t)) {
        cmd = t.toUpperCase();
        i++;
        if (cmd === 'Z') continue;
        continue;
      }
      const x = parseFloat(tokens[i]);
      const y = parseFloat(tokens[i + 1]);
      if (isFinite(x) && isFinite(y)) points.push([x, y]);
      i += 2;
      if (cmd === 'M') cmd = 'L';
    }
    return points;
  }
  // Resolve an SVG element's fill color. Checks (in order):
  // 1. Inline style="fill:#xxx"
  // 2. fill="..." attribute
  // 3. CSS class rules from the <style> block (ds2sf-synthesized SVGs
  //    use classes like .pad, .body, .pin1 with fills defined in <style>)
  // 4. Walk up the parent chain (inherited fills from <g> groups —
  //    pcbnew stdlib SVGs use this)
  function svgResolveFill(el, cssMap) {
    const styleAttr = (e, key) => {
      const s = e.getAttribute('style') || '';
      const m = s.match(new RegExp(key + '\\s*:\\s*([^;]+)', 'i'));
      return m ? m[1].trim() : null;
    };
    let cur = el;
    while (cur && cur.nodeType === 1) {
      const f = styleAttr(cur, 'fill') || cur.getAttribute('fill');
      if (f && f.toLowerCase() !== 'none') return f;
      if (cssMap) {
        const cls = (cur.getAttribute('class') || '').split(/\s+/);
        for (const c of cls) {
          if (c && cssMap[c]) return cssMap[c];
        }
      }
      cur = cur.parentElement;
    }
    return null;
  }
  function svgResolveFillOpacity(el, cssMap) {
    const styleAttr = (e, key) => {
      const s = e.getAttribute('style') || '';
      const m = s.match(new RegExp(key + '\\s*:\\s*([^;]+)', 'i'));
      return m ? m[1].trim() : null;
    };
    let cur = el;
    while (cur && cur.nodeType === 1) {
      const f = styleAttr(cur, 'fill-opacity');
      if (f) return parseFloat(f) || 1;
      cur = cur.parentElement;
    }
    return 1;
  }
  // Parse a <style> block into a className → fill map so class-based
  // SVGs (ds2sf-synthesized) resolve fills without getComputedStyle.
  function svgParseCssMap(doc) {
    const map = {};
    const styleEls = doc.querySelectorAll('style');
    for (const sEl of styleEls) {
      const txt = sEl.textContent || '';
      const re = /\.([a-zA-Z0-9_-]+)\s*\{[^}]*fill\s*:\s*([^;}\s]+)/g;
      let m;
      while ((m = re.exec(txt))) map[m[1]] = m[2].trim();
    }
    return map;
  }
  // Convert an SVG <rect> to 4 corner points (with optional rx rounding
  // approximated as straight corners — good enough for the overlay).
  function svgRectToPoints(el) {
    const x = parseFloat(el.getAttribute('x')) || 0;
    const y = parseFloat(el.getAttribute('y')) || 0;
    const w = parseFloat(el.getAttribute('width')) || 0;
    const h = parseFloat(el.getAttribute('height')) || 0;
    if (w <= 0 || h <= 0) return [];
    return [[x, y], [x + w, y], [x + w, y + h], [x, y + h]];
  }
  // Convert an SVG <circle> to N-point approximation.
  function svgCircleToPoints(el, n) {
    const cx = parseFloat(el.getAttribute('cx')) || 0;
    const cy = parseFloat(el.getAttribute('cy')) || 0;
    const r = parseFloat(el.getAttribute('r')) || 0;
    if (r <= 0) return [];
    const pts = [];
    for (let i = 0; i < (n || 24); i++) {
      const a = (2 * Math.PI * i) / (n || 24);
      pts.push([cx + r * Math.cos(a), cy + r * Math.sin(a)]);
    }
    return pts;
  }

  // ds2sf v0.5.1 authoritative footprint — parsed as REAL VECTOR
  // POLYGONS via earcut (NOT a texture-mapped raster — that approach
  // looked nasty at any zoom level). First-class Outline citizen
  // alongside the kicad / fusion / altium overlays; one sub-row per
  // SVG path so the user can isolate / inspect any individual pad
  // outline, silk stroke, fab label, or body silhouette.
  //
  // SVG → world coord transform:
  //   • viewBox is in millimetres (pcbnew SVG convention).
  //   • Translate so chip-center → world origin (0,0).
  //   • SVG-Y matches the kicad/fusion/altium pad coord convention
  //     (Y increases "down the page" — pcbnew authored both), so
  //     pads from ds2sf and from .kicad_mod end up at the same
  //     world XY. NO Y-flip.
  //
  // Layer Z-stacking from pcbnew's standard fill colors:
  //   #C83434 → F.Cu / pad copper (z = 0.006 mm above seat)
  //   #B4A09A → solder mask        (z = 0.002 mm)
  //   #D864FF → silkscreen         (z = 0.013 mm)
  //   #F2EDA1 → F.Fab labels       (z = 0.016 mm)
  //   any other colour             (z = 0.008 mm)
  async function renderDs2sfOverlay(opts = {}) {
    if (!(window.adomStep && window.adomStep.getScene && window.adomStep.getBabylon)) return null;
    const ds2sf = state.ds2sfResult;
    if (!ds2sf || !ds2sf.available) return { error: 'no ds2sf result for this chip' };
    if (typeof window.earcut !== 'function') return { error: 'earcut not loaded — vector polygon build needs window.earcut' };
    const source = ds2sf.outputs?.footprint_svg_source || 'unknown';
    const scene = window.adomStep.getScene();
    const B = window.adomStep.getBabylon();
    const group = 'ds2sf';
    ensureOverlayDisposed(group);

    // Pull the SVG text and parse the DOM client-side.
    let svgText;
    try {
      svgText = await fetch('api/ds2sf-svg?cb=' + Date.now()).then(r => r.ok ? r.text() : Promise.reject(new Error('HTTP ' + r.status)));
    } catch (e) {
      return { error: 'fetch ds2sf SVG: ' + (e?.message || e) };
    }
    const doc = new DOMParser().parseFromString(svgText, 'image/svg+xml');
    const svg = doc.documentElement;
    const vb = (svg.getAttribute('viewBox') || '0 0 10 10').trim().split(/\s+/).map(parseFloat);
    const vbX = vb[0] || 0, vbY = vb[1] || 0, vbW = vb[2] || 10, vbH = vb[3] || 10;

    // Detect SVG format. pcbnew stdlib SVGs use mm in the viewBox
    // (width="22.6568mm"). ds2sf-synthesized SVGs use px (width="277",
    // no unit suffix) and carry an extent_mm in the extraction result.
    // We need a scale factor from SVG units → mm.
    const widthAttr = svg.getAttribute('width') || '';
    const isPx = !widthAttr.includes('mm');
    let svgToMm = 1;
    if (isPx) {
      const ext = ds2sf.outputs?.footprint_extent_mm;
      if (ext && Array.isArray(ext) && ext.length === 2 && ext[0] > 0) {
        svgToMm = ext[0] / vbW;
      } else {
        // ds2sf-synthesized SVGs embed a <rect class="body"> whose
        // px dimensions correspond to the chip's body_dims_mm.
        // Use that to derive px → mm.
        const bodyRect = doc.querySelector('rect.body');
        const bodyDims = state.spec?.body_dims_mm;
        if (bodyRect && bodyDims && Array.isArray(bodyDims) && bodyDims[0] > 0) {
          const bodyPxW = parseFloat(bodyRect.getAttribute('width')) || 1;
          svgToMm = bodyDims[0] / bodyPxW;
        } else if (bodyDims && Array.isArray(bodyDims) && bodyDims[0] > 0) {
          svgToMm = bodyDims[0] / vbW * 3;
        } else {
          svgToMm = 0.01;
        }
      }
    }
    const cx = vbX + vbW / 2;
    const cy = vbY + vbH / 2;

    // Parse CSS <style> for class-based fills (ds2sf-synthesized format)
    const cssMap = svgParseCssMap(doc);

    function zForFill(fill) {
      const f = (fill || '').toLowerCase();
      if (f === '#c83434') return 0.006;   // F.Cu (copper) — pcbnew
      if (f === '#b4a09a') return 0.002;   // solder mask — pcbnew
      if (f === '#d864ff') return 0.013;   // silkscreen — pcbnew
      if (f === '#f2eda1') return 0.016;   // F.Fab labels — pcbnew
      if (f === '#d68830') return 0.006;   // pad — ds2sf synthesized
      if (f === '#2c5f2d') return 0.003;   // body — ds2sf synthesized
      if (f === '#ff5555') return 0.010;   // pin-1 — ds2sf synthesized
      if (f === '#0d1117') return -0.001;  // background — skip rendering
      return 0.008;
    }
    function rgbForFill(fill) {
      const f = (fill || '').toLowerCase();
      if (/^#[0-9a-f]{6}$/.test(f)) {
        return {
          r: parseInt(f.slice(1, 3), 16) / 255,
          g: parseInt(f.slice(3, 5), 16) / 255,
          b: parseInt(f.slice(5, 7), 16) / 255,
        };
      }
      return { r: 0.5, g: 0.5, b: 0.5 };
    }
    const matCache = {};
    function getMat(fill, opacity) {
      const key = fill + '@' + opacity;
      if (matCache[key]) return matCache[key];
      const c = rgbForFill(fill);
      const m = new B.StandardMaterial(group + '-mat-' + Object.keys(matCache).length, scene);
      m.diffuseColor = new B.Color3(c.r, c.g, c.b);
      m.emissiveColor = new B.Color3(c.r * 0.4, c.g * 0.4, c.b * 0.4);
      m.specularColor = new B.Color3(0, 0, 0);
      m.alpha = (opacity != null ? opacity : 1) * 0.9;
      m.backFaceCulling = false;
      m.zOffset = -1;
      matCache[key] = m;
      return m;
    }

    // Build one VertexData mesh per filled SVG shape (path / rect /
    // circle) using earcut for the triangulation. Handles BOTH SVG
    // formats: pcbnew stdlib (<path> with inline styles) and ds2sf
    // synthesized (<rect>/<circle> with CSS-class fills).
    const meshes = [];
    const childGroups = [];
    let kept = 0, skipped = 0;
    const counterByLayer = {};

    // Collect shapes from all renderable element types.
    const shapeEls = doc.querySelectorAll('path, rect, circle');
    for (let i = 0; i < shapeEls.length; i++) {
      const el = shapeEls[i];
      const tag = el.tagName.toLowerCase();
      const fill = svgResolveFill(el, cssMap);
      if (!fill || fill.toLowerCase() === 'none') { skipped++; continue; }
      if (zForFill(fill) < 0) { skipped++; continue; }
      const opacity = svgResolveFillOpacity(el, cssMap);
      let pts;
      if (tag === 'path') {
        pts = parseSvgPathD(el.getAttribute('d'));
      } else if (tag === 'rect') {
        pts = svgRectToPoints(el);
      } else if (tag === 'circle') {
        pts = svgCircleToPoints(el, 24);
      } else { skipped++; continue; }
      if (!pts || pts.length < 3) { skipped++; continue; }
      const z = zForFill(fill);
      const layerKey = fill.toLowerCase();
      counterByLayer[layerKey] = (counterByLayer[layerKey] || 0) + 1;

      // Triangulate via earcut. earcut takes a flat [x0,y0,x1,y1,...]
      // array and returns a flat triangle-index list.
      const flat = new Array(pts.length * 2);
      for (let k = 0; k < pts.length; k++) {
        flat[k * 2]     = pts[k][0];
        flat[k * 2 + 1] = pts[k][1];
      }
      const tris = window.earcut(flat);
      if (!tris || tris.length < 3) { skipped++; continue; }

      // Build positions in world coords. svgToMm converts SVG units
      // (px for synthesized, mm for pcbnew) into millimetres. Then
      // MM (0.001) converts mm → scene metres.
      const upI = worldUpIdx();
      const positions = new Float32Array(pts.length * 3);
      const zMeters = z * MM;
      if (upI === 2) {
        for (let k = 0; k < pts.length; k++) {
          positions[k * 3]     = (pts[k][0] - cx) * svgToMm * MM;
          positions[k * 3 + 1] = (pts[k][1] - cy) * svgToMm * MM;
          positions[k * 3 + 2] = zMeters;
        }
      } else {
        for (let k = 0; k < pts.length; k++) {
          positions[k * 3]     =  (pts[k][0] - cx) * svgToMm * MM;
          positions[k * 3 + 1] = zMeters;
          positions[k * 3 + 2] = -(pts[k][1] - cy) * svgToMm * MM;
        }
      }
      // Up-facing normals along world-up.
      const normals = new Float32Array(pts.length * 3);
      for (let k = 0; k < pts.length; k++) normals[k * 3 + upI] = 1;

      // Earcut returns CCW or CW depending on input winding. We want
      // the up-facing side. Test the first triangle's signed area in
      // input (svg) space — pcbnew authored Y-down so a CCW input is
      // "wound the wrong way" for our +Z-up normals; flip the index
      // order in that case so the front face points up.
      const a = pts[tris[0]], b = pts[tris[1]], c = pts[tris[2]];
      const cross = (b[0] - a[0]) * (c[1] - a[1]) - (b[1] - a[1]) * (c[0] - a[0]);
      const indices = new Uint32Array(tris.length);
      if (cross < 0) {
        // Already wound the way we want — keep as-is.
        for (let k = 0; k < tris.length; k++) indices[k] = tris[k];
      } else {
        // Flip every triangle.
        for (let k = 0; k < tris.length; k += 3) {
          indices[k]     = tris[k];
          indices[k + 1] = tris[k + 2];
          indices[k + 2] = tris[k + 1];
        }
      }

      const mesh = new B.Mesh(`${group}-poly-${i}`, scene);
      const vd = new B.VertexData();
      vd.positions = positions;
      vd.indices = indices;
      vd.normals = normals;
      vd.applyToMesh(mesh, false);
      mesh.material = getMat(fill, opacity);
      // Render on top of source overlays at same z but we let the
      // material's zOffset push us forward in the depth test.
      mesh.alphaIndex = 5;
      mesh.isPickable = false;
      meshes.push(mesh);

      // Outline child entry — one sub-row per polygon, labelled by
      // layer. Use the per-layer counter so the row reads
      // "F.Cu polygon 3 of N", "silk polygon 2 of N", etc.
      const layerName = ({ '#c83434': 'F.Cu', '#b4a09a': 'mask', '#d864ff': 'silk', '#f2eda1': 'fab' })[layerKey] || 'other';
      childGroups.push({
        mesh, layer: layerName, idx: counterByLayer[layerKey],
        verts: pts.length,
      });
      kept++;
    }

    if (!meshes.length) {
      return { error: 'no renderable paths in ds2sf SVG' };
    }

    const sourceTag = source === 'service-kicad-stdlib' ? '✓ stdlib (pixel-trustworthy)' : '◐ synthesized (layout-only)';
    const vbMmW = vbW * svgToMm, vbMmH = vbH * svgToMm;
    const layerMap = { '#c83434':'F.Cu','#b4a09a':'mask','#d864ff':'silk','#f2eda1':'fab',
                       '#d68830':'pad','#2c5f2d':'body','#ff5555':'pin1' };
    const layerSummary = Object.entries(counterByLayer)
      .map(([f, n]) => `${layerMap[f] || 'other'}:${n}`)
      .join(' ');
    window.adomStep.addOverlay({
      group,
      meshes,
      label: `ds2sf svg  ${vbMmW.toFixed(2)}×${vbMmH.toFixed(2)} mm  ${kept} polys (${layerSummary})  ${sourceTag}`,
      tooltip: `ds2sf svg  ${vbMmW.toFixed(2)}×${vbMmH.toFixed(2)} mm  ${kept} polys\n\n` +
        (source === 'ds2sf-datasheet-derived'
          ? 'Rendered by ds2sf from the manufacturer datasheet mechanical\ndrawing. Every shape traces back to a specific figure on a\nspecific page of the PDF. This is NOT a KiCad or third-party\nlibrary render — it is drawn from the manufacturer\'s own data.'
          : source === 'service-kicad-stdlib'
          ? 'Rendered by KiCad pcbnew from the standard library footprint.\nThis is a third-party render (a KiCad maintainer traced the\ndatasheet). Kept for visual comparison against the ds2sf pads.'
          : 'SVG source: ' + source),
      kind: 'overlay-ds2sf',
    });
    state.ds2sfOverlayOn = true;
    return { ok: true, source, widthMm: vbW, heightMm: vbH, polygonCount: kept, skippedCount: skipped, byLayer: counterByLayer };
  }

  // Combined heatsink renderer — one overlay group "heatsink" with
  // children: the BOTTOM-side EP copper pour (B.Cu, on the underside of
  // the PCB), and the via grid that connects EP-on-top to pour-on-bottom.
  //
  // The pour goes on B.Cu, NOT F.Cu. The whole point of the heatsink
  // architecture is to dump heat from the EP (on top, where the chip
  // sits) THROUGH the plated vias TO a copper region on the bottom that
  // radiates / conducts away (often a free zone fill that connects to
  // a larger ground plane or external heatsink). Putting the pour on
  // F.Cu would just duplicate the EP — useless.
  //
  // Outline structure: one Heatsink parent row, two children:
  //   • EP copper pour (B.Cu, bottom)
  //   • Heatsink vias (N × Ø d mm)   — one sub-row per via
  // The user can eye-toggle either child independently. Default on
  // for both; sidecar always carries them regardless of visibility.
  function renderHeatsink(epOriginMm, epSize, grid, opts = {}) {
    if (!grid || !grid.vias || !grid.vias.length) return null;
    if (!(window.adomStep && window.adomStep.getScene && window.adomStep.getBabylon)) return null;
    const scene = window.adomStep.getScene();
    const B = window.adomStep.getBabylon();
    if (!scene || !B) return null;
    const group = 'heatsink';
    ensureOverlayDisposed(group);

    const matCu = new B.StandardMaterial(group + '-cu', scene);
    matCu.diffuseColor  = new B.Color3(0.78, 0.55, 0.25);
    matCu.specularColor = new B.Color3(0.30, 0.20, 0.10);
    matCu.emissiveColor = new B.Color3(0.10, 0.07, 0.03);

    const ox_mm = epOriginMm?.x || 0;
    const oy_mm = epOriginMm?.y || 0;
    const PCB_THICKNESS_MM = 1.6;
    const drillD_mm = grid.drill;
    const outerD_mm = grid.outerCu;
    const annularT  = 0.05 * MM;
    const ringMm = Math.max(0, (grid.outerCu - grid.drill) / 2);

    const children = [];

    // ── Child 1: EP copper pour on B.Cu (bottom of PCB) ──
    // The pour is CSG-drilled with a hole at each via position. Real
    // PCB convention: the bottom Cu pour around plated through-holes
    // has a clearance hole at each drill (the donut around each via
    // is part of the pour itself; what's "missing" from the rectangle
    // is the via's drilled hole). Without these holes the pour is a
    // solid sheet that the via barrels appear to pass THROUGH at the
    // mesh-intersection level — visually wrong and electrically wrong
    // (downstream tools would extrude a solid pour with no clearance).
    const expansion_mm = opts.expansion_mm ?? 0;
    const pourW_mm = epSize.width + expansion_mm * 2;
    const pourH_mm = epSize.height + expansion_mm * 2;
    const pourThick_mm = 0.04;
    const pourSolid = B.MeshBuilder.CreateBox(group + '-pour-solid', flatBoxDims(pourW_mm, pourH_mm, pourThick_mm), scene);
    placeMeshAt(pourSolid, ox_mm, oy_mm, -(PCB_THICKNESS_MM + 0.02));
    pourSolid.material = matCu;
    // CSG-drill holes at each via position. Use the drill diameter
    // (no expansion) so the via barrel passes cleanly through.
    let pour = pourSolid;
    try {
      let holesCSG = null;
      const cutters = [];
      for (const v of grid.vias) {
        const fpX_mm = v.x_mm + ox_mm;
        const fpY_mm = v.y_mm + oy_mm;
        const cutter = B.MeshBuilder.CreateCylinder(group + '-pour-cut-' + v.index, {
          diameter: drillD_mm * MM,
          height: (pourThick_mm + 0.4) * MM,        // overrun for clean cut
          tessellation: 18,
        }, scene);
        alignVerticalToWorldUp(cutter);
        placeMeshAt(cutter, fpX_mm, fpY_mm, -(PCB_THICKNESS_MM + 0.02));
        cutter.isVisible = false;
        cutters.push(cutter);
        const csg = B.CSG.FromMesh(cutter);
        holesCSG = holesCSG ? holesCSG.union(csg) : csg;
      }
      if (holesCSG) {
        const pourCSG = B.CSG.FromMesh(pourSolid);
        const drilledCSG = pourCSG.subtract(holesCSG);
        const drilled = drilledCSG.toMesh(group + '-pour', matCu, scene);
        pourSolid.dispose();
        pour = drilled;
      }
      for (const c of cutters) c.dispose();
    } catch (e) {
      console.warn('[chipsmith] EP pour CSG drill failed; rendering solid pour', e);
      pourSolid.name = group + '-pour';   // still keep the solid mesh
    }
    children.push({
      meshes: [pour],
      label: `EP copper pour (B.Cu, bottom, ${grid.vias.length} via clearances)  ${pourW_mm.toFixed(2)} × ${pourH_mm.toFixed(2)} mm  ${expansion_mm > 0 ? `(+${expansion_mm.toFixed(2)} mm expansion)` : '(matches EP)'}`,
      kind: 'overlay-heatsink-pour',
    });
    state.epCopperPour = {
      enabled: true,
      layer: 'B.Cu',
      origin_mm: { x: ox_mm, y: oy_mm },
      size_mm: { width: pourW_mm, height: pourH_mm },
      expansion_mm,
      world_height_mm: -PCB_THICKNESS_MM,
    };

    // ── Children 2..N+1: per-via meshes ──
    // Each via is a real plated through-hole:
    //   1. Hollow Cu barrel — a tube (cylinder with NO_CAP) at the
    //      drill radius, DOUBLE-SIDE so both inner + outer faces of
    //      the Cu wall render. The interior is empty — you can see
    //      light through the hole if you look down it from the top
    //      face. (Real PTHs are plated with ~17.5 µm Cu on the wall;
    //      we render the wall as a 2D shell for performance, not a
    //      true thickness — visually indistinguishable at chip scale.)
    //   2. Top + bottom annular rings — thin Cu discs around the
    //      outside of the hole, on F.Cu and B.Cu. These close the
    //      "donut" view from above + below.
    for (const v of grid.vias) {
      const fpX_mm = v.x_mm + ox_mm;
      const fpY_mm = v.y_mm + oy_mm;
      const viaMeshes = [];
      // Hollow Cu barrel. Babylon's CreateTube wants a path; a single
      // straight segment along Y (Babylon's default cylinder axis).
      // We then alignVerticalToWorldUp() to lay it along world-up if
      // needed (Z-up), and placeMeshAt() to position. cap = NO_CAP
      // means open ends so the hole is visibly through-and-through.
      const halfH = (PCB_THICKNESS_MM / 2) * MM;
      const tubePath = [
        new B.Vector3(0, -halfH, 0),
        new B.Vector3(0,  halfH, 0),
      ];
      const barrel = B.MeshBuilder.CreateTube(`${group}-barrel-${v.index}`, {
        path: tubePath,
        radius: (drillD_mm / 2) * MM,
        tessellation: 18,
        cap: B.Mesh.NO_CAP,
        sideOrientation: B.Mesh.DOUBLESIDE,
      }, scene);
      alignVerticalToWorldUp(barrel);
      // Center the tube at the PCB's mid-thickness so it spans the
      // full plate (z = -PCB_THICKNESS to 0). Putting it at z=0
      // would float half the tube above the seat plane.
      placeMeshAt(barrel, fpX_mm, fpY_mm, -PCB_THICKNESS_MM / 2);
      barrel.material = matCu;
      viaMeshes.push(barrel);
      // Top + bottom annular rings — real DONUTS (with the hole in
      // the middle), not solid discs. Lathe a rectangular cross-
      // section (inner = drillR, outer = outerR, height = annularT)
      // around the lathe axis to produce a washer-shaped ring. The
      // inner edge lines up with the barrel's outer wall so the
      // hole reads cleanly down through both the top ring + the
      // barrel + the bottom ring.
      const innerR = (drillD_mm / 2) * MM;
      const outerR = (outerD_mm / 2) * MM;
      const ringProfile = [
        new B.Vector3(innerR, 0, 0),
        new B.Vector3(outerR, 0, 0),
        new B.Vector3(outerR, annularT, 0),
        new B.Vector3(innerR, annularT, 0),
        new B.Vector3(innerR, 0, 0),       // close the loop
      ];
      for (const h_mm of [0.001, -(PCB_THICKNESS_MM + annularT / MM + 0.001)]) {
        const ring = B.MeshBuilder.CreateLathe(`${group}-ring-${v.index}-${h_mm.toFixed(4)}`, {
          shape: ringProfile,
          radius: 1,        // ignored when shape carries absolute coords
          tessellation: 18,
          sideOrientation: B.Mesh.DOUBLESIDE,
        }, scene);
        alignVerticalToWorldUp(ring);
        placeMeshAt(ring, fpX_mm, fpY_mm, h_mm);
        ring.material = matCu;
        viaMeshes.push(ring);
      }
      const label = `via ${v.index}  (${v.x_mm.toFixed(2)}, ${v.y_mm.toFixed(2)}) mm  plated PTH (hollow)  Ø${grid.drill} drill / Ø${grid.outerCu} outer Cu / ${ringMm.toFixed(3)} mm ring`;
      viaMeshes.parentGroup = 'heatsink-vias';
      children.push({ meshes: viaMeshes, label, kind: 'overlay-via' });
    }

    window.adomStep.addOverlay({
      group,
      label: `Heatsink  (${grid.vias.length} vias  +  EP pour ${pourW_mm.toFixed(1)}×${pourH_mm.toFixed(1)} mm on B.Cu)`,
      kind: 'overlay-heatsink',
      children,
    });
    return { group, vias: grid.vias.length, pour: state.epCopperPour };
  }

  // Legacy aliases — preserved so existing callers (CLI verbs, the
  // signoff payload tooling) don't break. Both delegate to the
  // combined renderHeatsink().
  function renderHeatsinkVias(epOriginMm, grid) {
    if (!state.heatsinkGrid) return null;
    return renderHeatsink(epOriginMm, { width: state.heatsinkGrid.ep.size_mm[0], height: state.heatsinkGrid.ep.size_mm[1] }, grid);
  }
  function renderEpCopperPour(epOriginMm, epSize, opts) {
    if (!state.heatsinkGrid) return null;
    return renderHeatsink(epOriginMm, epSize, state.heatsinkGrid.grid, opts);
  }

  function pairAgreement(a, b, tolMm = 0.05) {
    if (!a || !b || !a.pads || !b.pads || a._stub || b._stub) return null;
    const byNum = (pads) => Object.fromEntries(pads.map(p => [p.number, p]));
    const ai = byNum(a.pads), bi = byNum(b.pads);
    const keys = new Set([...Object.keys(ai), ...Object.keys(bi)]);
    let matched = 0, posOff = 0, sizeOff = 0, missing = 0, maxDelta = 0;
    for (const k of keys) {
      const pa = ai[k], pb = bi[k];
      if (!pa || !pb) { missing++; continue; }
      const dx = (pa.at[0] - pb.at[0]), dy = (pa.at[1] - pb.at[1]);
      const dist = Math.sqrt(dx*dx + dy*dy);
      const ds = Math.max(Math.abs(pa.size[0] - pb.size[0]), Math.abs(pa.size[1] - pb.size[1]));
      if (dist > tolMm) posOff++;
      if (ds   > tolMm) sizeOff++;
      maxDelta = Math.max(maxDelta, dist);
      matched++;
    }
    const total = keys.size;
    return { matched, missing, posOff, sizeOff, maxDelta, total, ok: posOff === 0 && sizeOff === 0 && missing === 0 };
  }

  function renderFootprintTab() {
    const body = document.getElementById('cs-body');
    const fps = state.footprintSources;
    if (!fps) {
      body.innerHTML = `<div class="cs-empty">Loading footprint sources…</div>`;
      fetch('api/footprint-sources').then(r => r.ok ? r.json() : null).then(d => {
        state.footprintSources = d;
        if (state.activeTab === 'footprint') renderFootprintTab();
      });
      return;
    }
    const SRCS = ['kicad', 'fusion', 'altium', 'datasheet', 'ds2sf'];
    const sourcesPresent = SRCS.filter(s => fps[s] && !fps[s]._stub && fps[s].pad_count > 0);
    const overview = SRCS.map(s => {
      const f = fps[s];
      if (!f) return null;
      const stub = !!f._stub;
      const pc = f.pad_count || 0;
      const ext = stub ? null : fpExtent(f.pads);
      const status = stub ? '○' : (pc > 0 ? '●' : '✗');
      const cls = stub ? 'pending' : (pc > 0 ? 'ok' : 'fail');
      return `<div class="cs-row">
        <span class="label">${esc(SRC_LABEL[s])}</span>
        <span class="value mono">${stub ? '— stub' : (pc + ' pads' + (ext ? ` · ${ext.width.toFixed(2)}×${ext.height.toFixed(2)} mm` : ''))}</span>
        <span class="pill ${cls}" style="margin-left:auto">${status}</span>
      </div>`;
    }).filter(Boolean).join('');

    // Agreement matrix — pairwise across whichever sources are present.
    let agreementHTML = '';
    if (sourcesPresent.length >= 2) {
      const rows = [];
      for (let i = 0; i < sourcesPresent.length; i++) {
        for (let j = i + 1; j < sourcesPresent.length; j++) {
          const a = sourcesPresent[i], b = sourcesPresent[j];
          const agg = pairAgreement(fps[a], fps[b]);
          if (!agg) continue;
          const cls = agg.ok ? 'is-ok' : (agg.matched > 0 ? '' : '');
          rows.push(`<tr class="${cls}">
            <td>${esc(SRC_LABEL[a])} ↔ ${esc(SRC_LABEL[b])}</td>
            <td class="num mono">${agg.matched}</td>
            <td class="num mono">${agg.missing}</td>
            <td class="num mono">${agg.posOff}</td>
            <td class="num mono">${agg.maxDelta.toFixed(3)}</td>
          </tr>`);
        }
      }
      agreementHTML = `
        <div class="cs-section">
          <h3 class="cs-section-title">Cross-source agreement</h3>
          <table class="cs-tbl">
            <thead><tr><th>Pair</th><th class="num">Matched</th><th class="num">Missing</th><th class="num">Off</th><th class="num">Max Δ mm</th></tr></thead>
            <tbody>${rows.join('')}</tbody>
          </table>
          <div class="cs-row dim" style="margin-top:8px"><span class="value">Tolerance: position ±0.05 mm. Per-source counts include only pads with matching pad numbers.</span></div>
        </div>`;
    }

    // Bbox-fit: compare chip body extent (from Tab 2) vs the larger of
    // the two pad-extent bboxes. The chip's body should fit INSIDE the
    // footprint pad envelope (or be very close to it).
    let bboxFitHTML = '';
    if (state.measurements && sourcesPresent.length) {
      const chipMm = state.measurements.detected_mm.slice().sort((a, b) => b - a); // desc
      const fpMaxExt = sourcesPresent
        .map(s => fpExtent(fps[s].pads))
        .filter(Boolean)
        .reduce((acc, e) => acc && acc.width > e.width ? acc : e, null);
      if (fpMaxExt) {
        const dx = chipMm[0] - fpMaxExt.width;
        const dy = chipMm[1] - fpMaxExt.height;
        const fits = (dx <= 0.2) && (dy <= 0.2);
        bboxFitHTML = `
          <div class="cs-section">
            <h3 class="cs-section-title">Bbox fit (chip ⊂ footprint)</h3>
            <div class="cs-row"><span class="label">Chip XY</span><span class="value mono">${chipMm[0].toFixed(2)} × ${chipMm[1].toFixed(2)} mm</span></div>
            <div class="cs-row"><span class="label">Footprint XY</span><span class="value mono">${fpMaxExt.width.toFixed(2)} × ${fpMaxExt.height.toFixed(2)} mm</span></div>
            <div class="cs-row"><span class="label">Δ</span><span class="value mono">${dx.toFixed(3)} × ${dy.toFixed(3)} mm</span></div>
            <div class="cs-meas-status pill-${fits ? 'ok' : 'fail'}">${fits ? 'Chip body fits within the footprint envelope.' : 'Chip body exceeds the footprint envelope by &gt;0.2mm — likely Y-up vs Z-up mismatch or wrong footprint.'}</div>
          </div>`;
      }
    }

    state.status.footprint = (sourcesPresent.length > 0)
      ? (sourcesPresent.length >= 2 ? '◐' : '○')
      : '✗';
    refreshStatusPills();

    // Overlay toggles per source. Tracks state.fpOverlaysOn so toggles
    // survive tab switches. On the FIRST render of Tab 4 with at least
    // one non-stub source we auto-show every present source — the user
    // came to this tab to compare libraries, so the comparison should
    // be visible by default. Subsequent renders honor the user's
    // toggles (state.fpOverlaysAutoShown is the latch).
    state.fpOverlaysOn = state.fpOverlaysOn || {};
    if (!state.fpOverlaysAutoShown && sourcesPresent.length > 0) {
      for (const s of sourcesPresent) {
        if (!state.fpOverlaysOn[s]) {
          renderFootprintOverlay(s, fps[s].pads);
          state.fpOverlaysOn[s] = true;
        }
      }
      state.fpOverlaysAutoShown = true;
    }
    // ds2sf authoritative SVG plane — auto-shown once on first Tab 4
    // visit when a result is available. Same auto-show convention as
    // the per-source overlays. User can hide via the toggle below.
    if (!state.ds2sfAutoShown && state.ds2sfResult?.available) {
      renderDs2sfOverlay();
      state.ds2sfAutoShown = true;
    }
    const allSourcesOn = sourcesPresent.length > 0 && sourcesPresent.every(s => state.fpOverlaysOn[s]);
    const someSourcesOn = sourcesPresent.some(s => state.fpOverlaysOn[s]);
    const overlayRows = sourcesPresent.map(s => {
      const on = !!state.fpOverlaysOn[s];
      const c = SRC_COLOR[s] || { r: 0.5, g: 0.5, b: 0.5 };
      const swatch = `<span class="cs-src-swatch" style="background:rgba(${Math.round(c.r*255)},${Math.round(c.g*255)},${Math.round(c.b*255)},0.55)"></span>`;
      return `<div class="cs-row">
        ${swatch}<span class="label">${esc(SRC_LABEL[s])}</span>
        <button class="cs-btn cs-btn-${on ? 'primary' : 'secondary'}" data-fpsrc="${esc(s)}" data-tooltip="Toggle the ${esc(s)} footprint as a translucent overlay at z=0 in the 3D scene. Each source has its own color (datasheet orange, kicad green, fusion blue, altium purple) — stack multiple to see disagreement visually.">${svgIcon('bookOpen')}<span class="lbl">${on ? 'Hide' : 'Show'} in 3D</span></button>
      </div>`;
    }).join('');
    // Stock-library overlay — service-kicad-backed ghost. Visible only
    // when the spec carries footprint_name (chip-fetcher's responsibility
    // to populate at fetch time). Internally fetched from KiCad's
    // library, but never labeled "KiCad" in the UI per the plan's
    // EE-tool-neutrality rule.
    const stockFpName = state.spec?.footprint_name || state.spec?.stock_footprint_id || null;
    const stockState = state.stockOverlay || {}; // { loading, loaded, parsed, fetched_url, error }
    const stockOn = !!state.fpOverlaysOn?.stock;

    // ds2sf v0.5.1 authoritative SVG. Two flavours:
    //   • service-kicad-stdlib  — pixel-trustworthy KiCad render (green/✓)
    //   • ds2sf-synthesized     — best-effort layout reference  (amber/◐)
    // The state.ds2sfResult is hydrated lazily on first Tab 4 render.
    const ds2sf = state.ds2sfResult || null;
    const ds2sfSrc = ds2sf?.outputs?.footprint_svg_source;
    const ds2sfAvailable = !!ds2sf?.available && !!ds2sfSrc;
    const ds2sfStdlib = ds2sfSrc === 'service-kicad-stdlib';
    const ds2sfHTML = ds2sfAvailable ? `
      <div class="cs-section">
        <h3 class="cs-section-title">ds2sf svg <span class="dim">(pcbnew render)</span></h3>
        <div class="cs-row">
          <span class="label">Source</span>
          <span class="value mono">${esc(ds2sfSrc)}</span>
          <span class="pill ${ds2sfStdlib ? 'ok' : 'partial'}" style="margin-left:auto" data-tooltip="${ds2sfStdlib
            ? 'service-kicad-stdlib — canonical KiCad render. Pixel-trustworthy: pad positions, pad sizes, body outline, pin-1 marker are all authoritative ground truth. Diff your 3D rendering against this.'
            : 'ds2sf-synthesized — best-effort 2D layout from extracted body + lead dimensions. Layout-only reference: pad shapes + absolute positions are best-effort, NOT pixel-accurate. Use as visual sanity-check, not as a measurement.'}">${ds2sfStdlib ? '●' : '◐'}</span>
        </div>
        <div class="cs-row dim" style="font-size:11px"><span class="value">${ds2sfStdlib
          ? 'Pixel-trustworthy ground truth — pad positions, body outline, and pin-1 marker (red dot) match the canonical KiCad library exactly. Use for diffing your 3D rendering.'
          : 'Layout-only reference — body + leads from datasheet extraction. Stamped as such in the SVG itself. Treat as visual sanity-check, not as a pad-position oracle.'}</span></div>
        <div class="cs-svg-frame" style="margin-top:8px;background:rgba(20,24,30,0.45);border:1px solid rgba(255,255,255,0.14);border-radius:5px;padding:10px;text-align:center">
          <img src="api/ds2sf-svg" alt="${esc(ds2sfSrc)} authoritative footprint" style="max-width:100%;max-height:280px;display:inline-block"/>
        </div>
      </div>` : '';
    const stockHTML = stockFpName ? `
      <div class="cs-section">
        <h3 class="cs-section-title">Stock library footprint</h3>
        <div class="cs-row">
          <span class="label">Library identity</span>
          <span class="value mono">${esc(stockFpName)}</span>
        </div>
        ${stockState.loaded ? `
          <div class="cs-row">
            <span class="label">Pads</span>
            <span class="value mono">${stockState.parsed?.pad_count || 0}</span>
            <span class="pill ok" style="margin-left:auto">●</span>
          </div>
          ${stockState.parsed?.pin1_silk?.found ? `<div class="cs-row"><span class="label">Pin-1 silk</span><span class="value mono">marker present</span></div>` : ''}
          <div class="cs-row">
            <span class="cs-src-swatch" style="background:rgba(217,217,235,0.55)"></span>
            <span class="label">Ghost overlay</span>
            <button class="cs-btn cs-btn-${stockOn ? 'primary' : 'secondary'}" id="cs-stock-toggle" data-tooltip="Render the authoritative library footprint as a translucent off-white ghost stacked alongside the four user sources. Per-pad delta against each present source surfaces below.">${svgIcon('bookOpen')}<span class="lbl">${stockOn ? 'Hide' : 'Show'} stock ghost</span></button>
          </div>
        ` : (stockState.loading
          ? `<div class="cs-row dim"><span class="value">Fetching stock library footprint…</span></div>`
          : (stockState.error
            ? `<div class="cs-row"><span class="label">Status</span><span class="value mono">unavailable</span><span class="pill fail" style="margin-left:auto">✗</span></div>
               <div class="cs-row dim" style="font-size:11px"><span class="value">${esc(stockState.error)}</span></div>
               <div class="cs-actions"><button class="cs-btn cs-btn-secondary" id="cs-stock-retry" data-tooltip="Re-fetch /api/kicad-stock?footprint_name=${esc(stockFpName)}">${svgIcon('bookOpen')}<span class="lbl">Retry</span></button></div>`
            : `<div class="cs-actions"><button class="cs-btn cs-btn-primary" id="cs-stock-load" data-tooltip="Pull the authoritative library footprint from service-kicad and parse it into the same pad/silk schema as the four user sources, so it joins cross-validation.">${svgIcon('bookOpen')}<span class="lbl">Fetch stock library</span></button></div>`))}
      </div>` : '';

    const ds2sfOn = !!state.ds2sfOverlayOn;
    const ds2sfRowHTML = ds2sfAvailable ? `
        <div class="cs-row">
          <span class="cs-src-swatch" style="background:${ds2sfStdlib ? 'rgba(120,200,140,0.55)' : 'rgba(220,170,90,0.55)'}"></span>
          <span class="label">ds2sf svg</span>
          <button class="cs-btn cs-btn-${ds2sfOn ? 'primary' : 'secondary'}" id="cs-ds2sf-toggle" data-tooltip="${ds2sfStdlib
            ? 'KiCad pcbnew SVG render — rounded copper pads, silk outline, pin-1 dot, solder mask. The prettiest ground truth.'
            : 'ds2sf-synthesized SVG — simpler rects, layout-only reference.'}">${svgIcon('bookOpen')}<span class="lbl">${ds2sfOn ? 'Hide' : 'Show'} ds2sf svg</span></button>
        </div>` : '';
    const overlayHTML = sourcesPresent.length ? `
      <div class="cs-section">
        <h3 class="cs-section-title">3D overlays</h3>
        <div class="cs-row">
          <span class="label">All sources</span>
          <button class="cs-btn cs-btn-${allSourcesOn ? 'primary' : 'secondary'}" id="cs-fp-all-toggle" data-tooltip="${allSourcesOn ? 'Hide every source overlay' : 'Show every available source as a translucent overlay simultaneously, stacked. Each source uses its own tint so disagreements pop visually — datasheet orange + kicad green + fusion blue + altium purple.'}">${svgIcon('bookOpen')}<span class="lbl">${allSourcesOn ? 'Hide all' : 'Show all (' + sourcesPresent.length + ')'}</span></button>
        </div>
        ${overlayRows}
        ${ds2sfRowHTML}
        <div class="cs-row">
          <span class="label">Faux PCB plate</span>
          <button class="cs-btn cs-btn-${state.fauxPcbOn ? 'primary' : 'secondary'}" id="cs-fpplate-toggle" data-tooltip="1.6mm FR4 plate sized to the footprint extent + 5mm margin. Visualizes how the chip will sit on a real board.">${svgIcon('bookOpen')}<span class="lbl">${state.fauxPcbOn ? 'Hide' : 'Show'} 1.6mm faux PCB</span></button>
        </div>
      </div>` : '';

    body.innerHTML = `
      <div class="cs-section">
        <h3 class="cs-section-title">Sources</h3>
        ${overview || '<div class="cs-empty">No footprint sources available.</div>'}
      </div>
      ${bboxFitHTML}
      ${ds2sfHTML}
      ${stockHTML}
      ${overlayHTML}
      ${agreementHTML}
    `;
    body.querySelectorAll('button[data-fpsrc]').forEach(btn => {
      btn.onclick = () => {
        const s = btn.dataset.fpsrc;
        const on = !state.fpOverlaysOn[s];
        if (on) {
          renderFootprintOverlay(s, fps[s].pads);
        } else {
          ensureOverlayDisposed('fp-' + s);
        }
        state.fpOverlaysOn[s] = on;
        renderFootprintTab();
      };
    });
    const allBtn = document.getElementById('cs-fp-all-toggle');
    if (allBtn) {
      allBtn.onclick = () => {
        const turnOn = !allSourcesOn;
        for (const s of sourcesPresent) {
          if (turnOn && !state.fpOverlaysOn[s]) {
            renderFootprintOverlay(s, fps[s].pads);
          } else if (!turnOn && state.fpOverlaysOn[s]) {
            ensureOverlayDisposed('fp-' + s);
          }
          state.fpOverlaysOn[s] = turnOn;
        }
        renderFootprintTab();
      };
    }
    const plateBtn = document.getElementById('cs-fpplate-toggle');
    if (plateBtn) {
      plateBtn.onclick = () => {
        const on = !state.fauxPcbOn;
        if (on) {
          // Use the largest extent across present sources.
          // Pick the first source with the widest extent; pass its pads
          // through so CSG-drilling has access to drill geometry.
          let bestSrc = null, bestExt = null;
          for (const s of sourcesPresent) {
            const e = fpExtent(fps[s].pads);
            if (e && (!bestExt || e.width > bestExt.width)) { bestExt = e; bestSrc = s; }
          }
          renderFauxPcbPlate(bestExt, bestSrc ? fps[bestSrc].pads : null);
        } else {
          ensureOverlayDisposed('faux-pcb');
        }
        state.fauxPcbOn = on;
        renderFootprintTab();
      };
    }
    const ds2sfBtn = document.getElementById('cs-ds2sf-toggle');
    if (ds2sfBtn) {
      ds2sfBtn.onclick = () => {
        const on = !state.ds2sfOverlayOn;
        if (on) {
          renderDs2sfOverlay();
        } else {
          ensureOverlayDisposed('ds2sf');
          state.ds2sfOverlayOn = false;
        }
        renderFootprintTab();
      };
    }
    const stockLoadBtn = document.getElementById('cs-stock-load');
    if (stockLoadBtn) stockLoadBtn.onclick = () => loadStockLibraryOverlay();
    const stockRetryBtn = document.getElementById('cs-stock-retry');
    if (stockRetryBtn) stockRetryBtn.onclick = () => loadStockLibraryOverlay({ force: true });
    const stockToggleBtn = document.getElementById('cs-stock-toggle');
    if (stockToggleBtn) {
      stockToggleBtn.onclick = () => {
        state.fpOverlaysOn = state.fpOverlaysOn || {};
        const on = !state.fpOverlaysOn.stock;
        if (on) {
          const parsed = state.stockOverlay?.parsed;
          if (parsed && Array.isArray(parsed.pads)) renderFootprintOverlay('stock', parsed.pads);
        } else {
          ensureOverlayDisposed('fp-stock');
        }
        state.fpOverlaysOn.stock = on;
        renderFootprintTab();
      };
    }
  }

  // Pull the authoritative library footprint from service-kicad via
  // /api/kicad-stock, parse server-side (Rust .kicad_mod parser), and
  // store on state so renderFootprintTab can offer the toggle. Uses
  // spec.footprint_name (chip-fetcher persists this at fetch time).
  async function loadStockLibraryOverlay(opts) {
    const fpName = state.spec?.footprint_name || state.spec?.stock_footprint_id;
    if (!fpName) return { error: 'no footprint_name in spec' };
    if (!opts?.force && state.stockOverlay?.loaded) return { ok: true, cached: true };
    state.stockOverlay = { loading: true };
    if (state.activeTab === 'footprint') renderFootprintTab();
    try {
      const url = 'api/kicad-stock?footprint_name=' + encodeURIComponent(fpName)
        + (state.spec?.kicad_3d_name ? '&kicad_3d_name=' + encodeURIComponent(state.spec.kicad_3d_name) : '');
      const r = await fetch(url);
      const j = await r.json();
      if (j.parsed) {
        state.stockOverlay = {
          loaded: true,
          parsed: j.parsed,
          fetched_url: j.kicad_mod_url || null,
          matched: j.matched_footprint_name || fpName,
          kicad_3d_url: j.kicad_3d_url || null,
        };
      } else {
        state.stockOverlay = {
          loaded: false,
          error: j.error_kicad_mod?.error || 'no parsed pads in response (service-kicad probably 404)',
        };
      }
    } catch (e) {
      state.stockOverlay = { loaded: false, error: String(e) };
    }
    if (state.activeTab === 'footprint') renderFootprintTab();
    return state.stockOverlay;
  }

  // ── Tab 5: Pin-1 ────────────────────────────────────────────────
  // Chip-side: user-placed indicator (state.userPin1Corner) — chipsmith
  // does NOT auto-detect manufacturer-shipped pin-1 dots; the heuristic
  // approaches were too unreliable. The user clicks NW/NE/SW/SE below
  // and chipsmith places a synthetic dimple. Footprint-side: every
  // footprint source's pin1_silk field (KiCad/Fusion/Altium parsers
  // report the silkscreen marker if found).

  function renderPin1Tab() {
    const body = document.getElementById('cs-body');
    const fps = state.footprintSources || {};
    const chipPin1 = state.userPin1Corner ? { corner: state.userPin1Corner } : null;
    const SRCS = ['kicad', 'fusion', 'altium', 'datasheet', 'ds2sf'];
    const fpPin1Found = SRCS.some(s => fps[s] && fps[s].pin1_silk && fps[s].pin1_silk.found);

    let status = '○';
    let summary = '';
    if (chipPin1 && fpPin1Found) {
      status = '◐';
      summary = `Chip-side indicator (user-placed at ${esc(chipPin1.corner)}) + footprint silkscreen marker both present. For visual corner-agreement, hover the chip top to spot the dimple and compare to the silk dot on the footprint overlay.`;
    } else if (chipPin1 && !fpPin1Found) {
      status = '◐';
      summary = `Chip-side indicator placed at ${esc(chipPin1.corner)}, but no silkscreen marker on any of the ${SRCS.length} footprint sources. Footprint may be missing its pin-1 dot — fix in upstream library.`;
    } else if (!chipPin1 && fpPin1Found) {
      status = '◐';
      summary = `Footprint silkscreen marker present. Place a chip-side indicator using one of the corner buttons below.`;
    } else {
      status = '✗';
      summary = `No pin-1 indicator on either side. Place a chip-side indicator using one of the corner buttons below — sign-off needs at least one.`;
    }
    state.status.pin1 = status;
    refreshStatusPills();

    const fpRows = SRCS.map(s => {
      const f = fps[s];
      if (!f) return null;
      if (f._stub) return `<div class="cs-row"><span class="label">${esc(s)}</span><span class="value dim">stub — not parsed</span></div>`;
      const found = !!(f.pin1_silk && f.pin1_silk.found);
      return `<div class="cs-row">
        <span class="label">${esc(s)}</span>
        <span class="value mono">${found ? 'silkscreen marker present' : 'no marker'}</span>
        <span class="pill ${found ? 'ok' : 'pending'}" style="margin-left:auto">${found ? '●' : '○'}</span>
      </div>`;
    }).filter(Boolean).join('');

    // Place-pin-1 affordance — only shown when chip-side is missing.
    // Three placement modes (per the plan's Pin-1 spec):
    //   1. "Use footprint marker" — project pin1_silk from any footprint
    //      source onto the body top + snap to nearest corner. Default
    //      when a marker exists; chip + silk agree by construction.
    //   2. "Pick a corner" — explicit NW/NE/SW/SE buttons.
    //   3. "Click to place" — JS API only for now: window.adomChipsmith
    //      .placePin1({ mode: 'click', xyMm: [x,y] }). UI canvas-pick
    //      lands in v1.1.
    // Three shapes (radio): dimple (default, recessed cylinder), raised
    // dot (protruding), corner chamfer (angled cut). All tagged
    // 'pin1_indicator_user_placed' + registered as first-class Outline
    // overlays + baked into .chipsmith.step at sign-off.
    const curShape = state.userPin1Shape || 'dimple';
    const SHAPES = [
      { id: 'dimple',    label: 'Dimple',         tip: 'Recessed cylinder, Ø 0.40 × 0.05 mm deep, slightly darker tint than the body. The default — matches QFN/QFP/LGA datasheet conventions.' },
      { id: 'raisedDot', label: 'Raised dot',     tip: 'Protruding cylinder, Ø 0.40 × 0.05 mm tall, body color. Common on SOIC and some moulded plastic packages where the dot is silk-printed not embossed.' },
      { id: 'chamfer',   label: 'Corner chamfer', tip: 'Angled cut on the body\'s top corner edge. Common on TQFP and some BGA where the corner itself is shaved.' },
    ];
    const shapeRadio = SHAPES.map(s => `
      <label class="cs-shape-opt${curShape === s.id ? ' active' : ''}" data-shape="${s.id}" data-tooltip="${escAttr(s.tip)}">
        <input type="radio" name="cs-pin1-shape" value="${s.id}"${curShape === s.id ? ' checked' : ''}>
        <span class="lbl">${esc(s.label)}</span>
      </label>`).join('');

    const fpMarkerBtn = fpPin1Found
      ? `<button class="cs-btn cs-btn-primary cs-pin1-fpmarker" data-tooltip="Project the silkscreen pin-1 marker from the first available footprint source onto the body top, snap to the nearest corner. Chip-side and silkscreen agree by construction — the safest default when a footprint marker exists.">${svgIcon('bookOpen')}<span class="lbl">Use footprint marker</span></button>`
      : `<button class="cs-btn cs-btn-secondary" disabled data-tooltip="No silkscreen pin-1 marker on any footprint source. Use a corner button below or place via the JS API: window.adomChipsmith.placePin1({mode:'click', xyMm:[x,y]}).">${svgIcon('bookOpen')}<span class="lbl">Use footprint marker — unavailable</span></button>`;

    const placeUI = chipPin1 ? '' : `
      <div class="cs-section">
        <h3 class="cs-section-title">Place a synthetic indicator</h3>
        <div class="cs-row dim"><span class="value">Manufacturer's STEP has no pin-1 dot. Place one here — it'll be tagged <code>pin1_indicator_user_placed</code> and baked into <code>.chipsmith.step</code> at sign-off (v1.1) so it survives every future load.</span></div>
        <div class="cs-row" style="margin-top:6px"><span class="label">Shape</span></div>
        <div class="cs-shape-radio">${shapeRadio}</div>
        <div class="cs-row" style="margin-top:8px"><span class="label">Mode</span></div>
        <div class="cs-actions" style="flex-wrap:wrap;gap:6px">
          ${fpMarkerBtn}
        </div>
        <div class="cs-row" style="margin-top:6px"><span class="label">Or pick a corner</span></div>
        <div class="cs-actions">
          <button class="cs-btn cs-btn-secondary cs-place-pin1" data-corner="NW" data-tooltip="Place at the chip's top-left (NW) corner — the canonical pin-1 location for QFN/QFP/LGA datasheet drawings.">${svgIcon('bookOpen')}<span class="lbl">NW</span></button>
          <button class="cs-btn cs-btn-secondary cs-place-pin1" data-corner="NE" data-tooltip="Place at the top-right (NE) corner.">${svgIcon('bookOpen')}<span class="lbl">NE</span></button>
          <button class="cs-btn cs-btn-secondary cs-place-pin1" data-corner="SW" data-tooltip="Place at the bottom-left (SW) corner.">${svgIcon('bookOpen')}<span class="lbl">SW</span></button>
          <button class="cs-btn cs-btn-secondary cs-place-pin1" data-corner="SE" data-tooltip="Place at the bottom-right (SE) corner.">${svgIcon('bookOpen')}<span class="lbl">SE</span></button>
        </div>
        <div class="cs-row" style="margin-top:6px"><span class="label">Or click on the body</span></div>
        <div class="cs-actions">
          <button class="cs-btn cs-btn-${state.pin1ClickArmed ? 'primary' : 'secondary'}" id="cs-pin1-click-arm" data-tooltip="Arm canvas-pick mode — your next click on the chip body's top face places the indicator at that point. Hold Alt to use the exact click point; otherwise chipsmith snaps to the nearest body corner. Press Esc to cancel.">${svgIcon('bookOpen')}<span class="lbl">${state.pin1ClickArmed ? 'Cancel click-to-place' : 'Click-to-place mode'}</span></button>
        </div>
        <div class="cs-row dim" style="margin-top:6px"><span class="value" style="font-size:11px">JS API: <code>window.adomChipsmith.placePin1({mode:'click', xyMm:[x,y], shape:'${curShape}'})</code></span></div>
      </div>`;

    body.innerHTML = `
      <div class="cs-section">
        <h3 class="cs-section-title">Chip-side indicator</h3>
        <div class="cs-row">
          <span class="label">Detected</span>
          <span class="value mono">${chipPin1 ? esc(chipPin1.name) : '<span class="cs-amber">none — manufacturer STEP has no pin-1 product</span>'}</span>
          <span class="pill ${chipPin1 ? 'ok' : 'pending'}" style="margin-left:auto">${chipPin1 ? '●' : '○'}</span>
        </div>
      </div>
      ${placeUI}
      <div class="cs-section">
        <h3 class="cs-section-title">Footprint silkscreen marker</h3>
        ${fpRows}
      </div>
      <div class="cs-section">
        <h3 class="cs-section-title">Agreement</h3>
        <div class="cs-meas-status pill-${status === '✗' ? 'fail' : (status === '●' ? 'ok' : 'partial')}">${summary}</div>
      </div>
    `;
    body.querySelectorAll('.cs-place-pin1').forEach(btn => {
      btn.onclick = () => placePin1({ corner: btn.dataset.corner, shape: state.userPin1Shape || 'dimple' });
    });
    body.querySelectorAll('.cs-shape-opt input').forEach(inp => {
      inp.onchange = () => {
        state.userPin1Shape = inp.value;
        renderPin1Tab();
      };
    });
    const fpBtn = body.querySelector('.cs-pin1-fpmarker');
    if (fpBtn) {
      fpBtn.onclick = () => {
        const r = placePin1({ mode: 'fpMarker', shape: state.userPin1Shape || 'dimple' });
        if (r && r.error) { console.warn('[chipsmith] placePin1 fpMarker:', r.error); alert(r.error); }
      };
    }
    const armBtn = document.getElementById('cs-pin1-click-arm');
    if (armBtn) {
      armBtn.onclick = () => {
        if (state.pin1ClickArmed) { disarmPin1Click(); }
        else                       { armPin1Click(); }
        renderPin1Tab();
      };
    }
  }

  // Click-to-place pin-1 — single-shot canvas-pick handler. When armed,
  // the next pointer-up on a body mesh resolves to (x,y) in mm relative
  // to the body bbox center, snaps to the nearest corner unless Alt is
  // held, then calls placePin1({mode:'click'}). Esc cancels. The crosshair
  // cursor + dim banner-overlay tells the user something is armed; auto-
  // disarm after one successful pick or on Esc.
  let _pin1ClickHandlers = null;
  function armPin1Click() {
    if (state.pin1ClickArmed) return;
    if (!(window.adomStep && window.adomStep.getScene && window.adomStep.getBabylon)) return;
    const scene = window.adomStep.getScene();
    const canvas = scene.getEngine().getRenderingCanvas();
    if (!canvas) return;
    const bb = window.adomStep.bbox('Body');
    if (!bb || bb.error) {
      alert('Click-to-place needs a body bbox. Tab 2 must finish loading first.');
      return;
    }
    state.pin1ClickArmed = true;
    canvas.style.cursor = 'crosshair';
    showClickArmBanner();

    function onPointerUp(ev) {
      const rect = canvas.getBoundingClientRect();
      const x = ev.clientX - rect.left;
      const y = ev.clientY - rect.top;
      const pick = scene.pick(x, y);
      if (!pick || !pick.hit || !pick.pickedPoint) {
        return; // miss — keep armed for another click
      }
      // Convert hit point to footprint-mm coordinates (the chipsmith
      // canonical XY plane perpendicular to world-up). placePin1's
      // 'click' mode interprets xyMm as positions in this same plane.
      const ax = planeAxes();
      const hit = [pick.pickedPoint.x, pick.pickedPoint.y, pick.pickedPoint.z];
      const planeCx = (bb.min[ax[0]] + bb.max[ax[0]]) / 2;
      const planeCy = (bb.min[ax[1]] + bb.max[ax[1]]) / 2;
      const dxMm = (hit[ax[0]] - planeCx) / MM;
      const dyMm = (hit[ax[1]] - planeCy) / MM;
      const altHeld = !!ev.altKey;
      const opts = altHeld
        ? { mode: 'click', xyMm: [dxMm, dyMm], shape: state.userPin1Shape || 'dimple' }
        : { mode: 'corner', shape: state.userPin1Shape || 'dimple',
            corner: (dxMm < 0 ? (dyMm < 0 ? 'SW' : 'NW') : (dyMm < 0 ? 'SE' : 'NE')) };
      const r = placePin1(opts);
      if (r && r.error) { console.warn('[chipsmith] placePin1 click:', r.error); alert(r.error); }
      disarmPin1Click();
      if (state.activeTab === 'pin1') renderPin1Tab();
    }
    function onKey(ev) {
      if (ev.key === 'Escape') disarmPin1Click();
    }
    canvas.addEventListener('pointerup', onPointerUp);
    document.addEventListener('keydown', onKey);
    _pin1ClickHandlers = { canvas, onPointerUp, onKey };
  }
  function disarmPin1Click() {
    if (!state.pin1ClickArmed) return;
    state.pin1ClickArmed = false;
    if (_pin1ClickHandlers) {
      const { canvas, onPointerUp, onKey } = _pin1ClickHandlers;
      canvas.removeEventListener('pointerup', onPointerUp);
      document.removeEventListener('keydown', onKey);
      canvas.style.cursor = '';
      _pin1ClickHandlers = null;
    }
    hideClickArmBanner();
    if (state.activeTab === 'pin1') renderPin1Tab();
  }
  function showClickArmBanner() {
    let b = document.getElementById('cs-pin1-arm-banner');
    if (!b) {
      b = document.createElement('div');
      b.id = 'cs-pin1-arm-banner';
      b.style.cssText = 'position:fixed;top:48px;left:50%;transform:translateX(-50%);background:rgba(0,184,177,0.92);color:#0a0c10;font-size:12px;padding:6px 12px;border-radius:5px;z-index:99998;pointer-events:none;font-family:inherit;font-weight:500;box-shadow:0 4px 16px rgba(0,0,0,0.4)';
      document.body.appendChild(b);
    }
    b.textContent = 'Click on the chip body to place pin-1 (Alt = exact point, otherwise snap to nearest corner; Esc cancels)';
  }
  function hideClickArmBanner() {
    const b = document.getElementById('cs-pin1-arm-banner');
    if (b) b.remove();
  }

  // Place a synthetic pin-1 indicator on the chip's top face.
  //
  //   placePin1(cornerOrOpts, optsOrUndef)
  //
  // Calling shapes:
  //   placePin1('NW')                                      // legacy — corner + dimple
  //   placePin1({ corner: 'NW', shape: 'dimple' })         // explicit
  //   placePin1({ mode: 'fpMarker', shape: 'raisedDot' })  // snap to footprint silk
  //   placePin1({ mode: 'click', xyMm: [1.5, -0.8], shape: 'chamfer' })
  //
  // Modes:
  //   corner    — pick from NW/NE/SW/SE (default if `corner` is given)
  //   fpMarker  — project pin1_silk from a footprint source onto body top,
  //               snap to nearest corner; use this when you want chip + silk
  //               to agree by construction
  //   click     — caller passes xyMm (relative to chip body center)
  //
  // Shapes:
  //   dimple    — Ø 0.40 × 0.05 mm recessed cylinder (default)
  //   raisedDot — Ø 0.40 × 0.05 mm protruding cylinder, body color
  //   chamfer   — angled cut on the corner edge (cone segment)
  function placePin1(cornerOrOpts, optsOrUndef) {
    if (!(window.adomStep && window.adomStep.getScene && window.adomStep.getBabylon && window.adomStep.bbox)) return { error: 'adom-step API not ready' };
    let mode, corner, xyMm, shape;
    if (typeof cornerOrOpts === 'string') {
      // Legacy form: placePin1('NW')
      mode = 'corner'; corner = cornerOrOpts;
      shape = (optsOrUndef && optsOrUndef.shape) || 'dimple';
    } else {
      const o = cornerOrOpts || {};
      mode = o.mode || (o.corner ? 'corner' : 'corner');
      corner = o.corner;
      xyMm = o.xyMm;
      shape = o.shape || 'dimple';
    }
    const scene = window.adomStep.getScene();
    const B = window.adomStep.getBabylon();
    const bb = window.adomStep.bbox('Body');
    if (!bb || bb.error) return { error: 'no body bbox' };
    ensureOverlayDisposed('user-pin1');
    // World-up-aware corner geometry. The body's "top" is along the
    // world-up axis; the corners live in the plane perpendicular to it.
    const upI = worldUpIdx();
    const ax  = planeAxes();              // [planeA, planeB]
    const inset_mm = 0.4;
    const minA = bb.min[ax[0]], maxA = bb.max[ax[0]];   // body extent along plane-A
    const minB = bb.min[ax[1]], maxB = bb.max[ax[1]];   // body extent along plane-B
    const topUp_mm    = bb.max[upI] / MM;
    const bottomUp_mm = bb.min[upI] / MM;
    const bodyCenterA = (minA + maxA) / 2;
    const bodyCenterB = (minB + maxB) / 2;

    // Pin-1 corner naming uses NW/NE/SW/SE — North/South map to the
    // higher/lower plane-B value, West/East map to the lower/higher
    // plane-A value. Same convention as legacy code; just plumbed
    // through the actual plane axes.
    function nearestCornerFromPlanePos_mm(a_mm, b_mm) {
      const left = (a_mm * MM) < bodyCenterA;
      const back = (b_mm * MM) < bodyCenterB;
      if ( left && !back) return 'NW';
      if (!left && !back) return 'NE';
      if ( left &&  back) return 'SW';
      return 'SE';
    }
    let centerA_mm, centerB_mm, sourceLabel = '';
    if (mode === 'fpMarker') {
      const fps = state.footprintSources || {};
      let pad = null, src = null;
      for (const s of ['kicad','fusion','altium','datasheet','ds2sf']) {
        const f = fps[s];
        if (f && f.pin1_silk && f.pin1_silk.found && Array.isArray(f.pin1_silk.center)) {
          pad = f.pin1_silk; src = s; break;
        }
      }
      if (!pad) return { error: 'no pin1_silk marker on any footprint source — try corner or click mode' };
      // Footprint marker is in footprint XY mm; project to body plane.
      corner = nearestCornerFromPlanePos_mm(pad.center[0], pad.center[1]);
      sourceLabel = `from ${src} silk marker`;
    }
    if (mode === 'click' && Array.isArray(xyMm)) {
      centerA_mm = xyMm[0];
      centerB_mm = xyMm[1];
      sourceLabel = `at click (${xyMm[0].toFixed(2)}, ${xyMm[1].toFixed(2)})`;
    } else {
      // corner mode (or fpMarker after corner-snap).
      const minA_mm = minA / MM, maxA_mm = maxA / MM;
      const minB_mm = minB / MM, maxB_mm = maxB / MM;
      switch (corner) {
        case 'NW': centerA_mm = minA_mm + inset_mm; centerB_mm = maxB_mm - inset_mm; break;
        case 'NE': centerA_mm = maxA_mm - inset_mm; centerB_mm = maxB_mm - inset_mm; break;
        case 'SW': centerA_mm = minA_mm + inset_mm; centerB_mm = minB_mm + inset_mm; break;
        case 'SE':
        default:   centerA_mm = maxA_mm - inset_mm; centerB_mm = minB_mm + inset_mm; break;
      }
      sourceLabel = sourceLabel || `corner ${corner}`;
    }

    // Build the indicator mesh per shape, with its long axis aligned
    // to world-up. placeMeshAt uses footprint-coords semantics (X,Y in
    // the plane, height along world-up) — same convention as every
    // other chipsmith overlay so domes / etch / pin-1 all agree.
    const meshes = [];
    if (shape === 'raisedDot') {
      const dot = B.MeshBuilder.CreateCylinder('user-pin1-raisedDot', {
        diameter: 0.40 * MM, height: 0.05 * MM, tessellation: 24,
      }, scene);
      alignVerticalToWorldUp(dot);
      // Cylinder centered 0.025 mm above body top so it protrudes.
      placeMeshAt(dot, centerA_mm, centerB_mm, topUp_mm + 0.025);
      const m = new B.StandardMaterial('user-pin1-mat', scene);
      m.diffuseColor = new B.Color3(0.20, 0.20, 0.22);
      m.specularColor = new B.Color3(0.05, 0.05, 0.05);
      dot.material = m;
      meshes.push(dot);
    } else if (shape === 'chamfer') {
      // Angled cut on the corner edge. Box has width along plane-A,
      // depth along plane-B (per flatBoxDims), thickness along world-up.
      // Then we rotate ABOUT the world-up axis so the long edge points
      // inward to the body center.
      const cham = B.MeshBuilder.CreateBox('user-pin1-chamfer', flatBoxDims(0.85, 0.10, 0.10), scene);
      placeMeshAt(cham, centerA_mm, centerB_mm, topUp_mm - 0.05);
      const dirA = ((centerA_mm * MM) > bodyCenterA ? 1 : -1);
      const dirB = ((centerB_mm * MM) > bodyCenterB ? 1 : -1);
      const yaw = Math.atan2(-dirA, -dirB) + Math.PI / 4;
      // Rotate around world-up axis so the chamfer's long edge points
      // toward the body center diagonally.
      if (upI === 1) cham.rotation.y = yaw;
      else           cham.rotation.z = yaw;
      const m = new B.StandardMaterial('user-pin1-mat', scene);
      m.diffuseColor = new B.Color3(0.08, 0.08, 0.09);
      m.specularColor = new B.Color3(0.05, 0.05, 0.05);
      cham.material = m;
      meshes.push(cham);
    } else {
      // dimple (default): recessed cylinder.
      const dim = B.MeshBuilder.CreateCylinder('user-pin1-dimple', {
        diameter: 0.40 * MM, height: 0.05 * MM, tessellation: 24,
      }, scene);
      alignVerticalToWorldUp(dim);
      // Cylinder centered 0.025 mm BELOW body top so it reads as recessed.
      placeMeshAt(dim, centerA_mm, centerB_mm, topUp_mm - 0.025);
      const m = new B.StandardMaterial('user-pin1-mat', scene);
      m.diffuseColor = new B.Color3(0.10, 0.10, 0.12);
      m.specularColor = new B.Color3(0.05, 0.05, 0.05);
      dim.material = m;
      meshes.push(dim);
    }
    const finalCorner = corner || nearestCornerFromPlanePos_mm(centerA_mm, centerB_mm);
    window.adomStep.addOverlay({
      group: 'user-pin1', meshes,
      label: `Pin-1 indicator  ${shape}  ${sourceLabel}`,
      kind: 'overlay-pin1-user',
    });
    state.userPin1Corner = finalCorner;
    state.userPin1Shape = shape;
    state.userPin1Mode = mode;
    recomputeAllStatuses();
    if (state.activeTab === 'pin1') renderPin1Tab();
    return { ok: true, corner: finalCorner, shape, mode };
  }

  // ── Laser-etch (cosmetic MPN engraving on the chip's top face) ──
  // Per the plan: "Optional laser-etch of the MPN onto the chip's top
  // surface — toggleable per-chip cosmetic feature that engraves the
  // part number into the body's top face (50µm-deep recess, slightly
  // darker than body color, auto-sized + auto-positioned to avoid the
  // pin-1 indicator). EEs love seeing the part number on the chip
  // when they're laying out a board downstream."
  //
  // Implementation: a thin Babylon plane sits 1µm above the body's top
  // face (visually "engraved" via a darker emissive vs the body), with
  // a DynamicTexture that paints the MPN text. Auto-sized to fit
  // within max_size_mm bounds (default 0.6 of body XZ). Auto-positioned
  // away from the pin-1 corner: if the user-placed pin-1 indicator is
  // at NW, the etch shifts SE-ward by 0.4 mm so the two don't visually
  // collide. Tagged 'overlay-laser-etch' for outline + hover-HUD.
  function renderLaserEtch(opts) {
    if (!(window.adomStep && window.adomStep.getScene && window.adomStep.getBabylon && window.adomStep.bbox)) return null;
    const scene = window.adomStep.getScene();
    const B = window.adomStep.getBabylon();
    const bb = window.adomStep.bbox('Body');
    if (!bb || bb.error) return { error: 'no body bbox — cannot place laser etch' };
    ensureOverlayDisposed('laser-etch');
    const text = (opts && opts.text) || state.spec?.mpn || '???';
    const depthMm = (opts && opts.depth_mm) || 0.05;
    // World-up-aware sizing: pick the body's two PLANE-axis extents
    // for the etch's width/height (not its world-up height).
    const upI = worldUpIdx();
    const ax  = planeAxes();
    const bodyW_mm = (bb.max[ax[0]] - bb.min[ax[0]]) / MM;   // plane-A
    const bodyD_mm = (bb.max[ax[1]] - bb.min[ax[1]]) / MM;   // plane-B
    const maxW_mm = (opts && opts.max_size_mm && opts.max_size_mm[0]) || Math.min(bodyW_mm * 0.7, 3.0);
    const maxH_mm = (opts && opts.max_size_mm && opts.max_size_mm[1]) || Math.min(bodyD_mm * 0.18, 0.6);
    // Base position = body top-face center along the plane axes;
    // height = body bbox max along world-up.
    const planeCx_mm = ((bb.min[ax[0]] + bb.max[ax[0]]) / 2) / MM;
    const planeCy_mm = ((bb.min[ax[1]] + bb.max[ax[1]]) / 2) / MM;
    const topUp_mm   = bb.max[upI] / MM;
    let dxMm = 0, dyMm = 0;
    // Auto-shift away from the pin-1 indicator if one is placed.
    if (state.userPin1Corner) {
      const c = state.userPin1Corner;
      const off = 0.4;
      if (c === 'NW') { dxMm =  off; dyMm = -off; }
      if (c === 'NE') { dxMm = -off; dyMm = -off; }
      if (c === 'SW') { dxMm =  off; dyMm =  off; }
      if (c === 'SE') { dxMm = -off; dyMm =  off; }
    }

    // DynamicTexture label. Real chip silkscreen is light text on the
    // body's matte top — we paint a SOLID body-colored background with
    // bright off-white text on top, then render with full emissive so
    // the text reads regardless of scene lighting. No alpha-blending
    // (transparency was causing the text to wash out into the body
    // through mipmap sampling). The plane sits a hair above the body
    // top so it doesn't z-fight.
    const txW = 1024, txH = 192;
    const dt = new B.DynamicTexture('laser-etch-tex', { width: txW, height: txH }, scene, true, B.Texture.TRILINEAR_SAMPLINGMODE);
    const ctx = dt.getContext();
    // Background = matte-black body color (matches typical SOIC/QFN body).
    ctx.fillStyle = 'rgb(20, 20, 22)';
    ctx.fillRect(0, 0, txW, txH);
    let fontPx = Math.floor(txH * 0.72);
    ctx.fillStyle = 'rgb(232, 232, 220)';            // off-white silkscreen ink
    ctx.textAlign = 'center';
    ctx.textBaseline = 'middle';
    while (fontPx > 12) {
      ctx.font = `bold ${fontPx}px sans-serif`;
      const w = ctx.measureText(text).width;
      if (w <= txW - 24) break;
      fontPx -= 4;
    }
    ctx.font = `bold ${fontPx}px sans-serif`;
    ctx.fillText(text, txW / 2, txH / 2);
    dt.update();

    const mat = new B.StandardMaterial('laser-etch-mat', scene);
    mat.diffuseTexture = dt;
    mat.emissiveTexture = dt;
    mat.emissiveColor = new B.Color3(1, 1, 1);
    mat.diffuseColor  = new B.Color3(1, 1, 1);
    mat.specularColor = new B.Color3(0, 0, 0);
    // Render both faces so the etch reads regardless of which side
    // the camera approaches from. (The plane's "up" face after rotation
    // depends on Babylon's handedness; doublesided sidesteps it.)
    mat.backFaceCulling = false;

    // Plane sized in scene units. Babylon's CreatePlane defaults to
    // facing +Z (normal along +Z, lies in XY plane). For Z-up worlds
    // the default orientation already faces world-up — perfect.
    // For Y-up worlds we rotate +π/2 about X so the painted face
    // points to +Y. dt.vScale = -1 flips the V axis so text reads
    // right-side-up when viewed from above.
    const planeW = maxW_mm * MM;
    const planeH = maxH_mm * MM;
    const plane = B.MeshBuilder.CreatePlane('laser-etch-plane', {
      width: planeW, height: planeH,
    }, scene);
    if (upI === 1) {
      plane.rotation.x = Math.PI / 2;     // Y-up: rotate to face +Y
      dt.vScale = -1; dt.vOffset = 1;     // flip V so text reads right-side up
    }
    // Position 10 µm above the body's top face along world-up. Plane
    // axes get the offset+center; world-up gets topUp + 0.010.
    placeMeshAt(plane, planeCx_mm + dxMm, planeCy_mm + dyMm, topUp_mm + 0.010);
    plane.material = mat;

    window.adomStep.addOverlay({
      group: 'laser-etch',
      meshes: [plane],
      label: `Laser etch  "${text}"  ${maxW_mm.toFixed(2)}×${maxH_mm.toFixed(2)} mm  depth ${(depthMm*1000).toFixed(0)} µm`,
      kind: 'overlay-laser-etch',
    });
    state.laserEtch = { text, depth_mm: depthMm, max_size_mm: [maxW_mm, maxH_mm], position: { dx_mm: dxMm, dy_mm: dyMm } };
    return { ok: true, ...state.laserEtch };
  }

  // ── Debug: anchor bboxes ────────────────────────────────────────
  // Four colored thin slabs that visualize EXACTLY where chipsmith
  // thinks each canonical surface is in the scene. The user toggles
  // these on when reporting "your placement is wrong" so we can both
  // see whether the discrepancy is a placement bug (chipsmith put the
  // dome at the right Y but wrong XZ) or a model bug (chipsmith thinks
  // the body's top is at Y=1.65 mm but actually it's at Y=2.10 mm).
  //
  // Anchors:
  //   chip-top         body bbox max-Y plane                     (cyan)
  //   chip-bottom      body bbox min-Y plane                     (magenta)
  //   footprint-area   bbox of all pad centers from best source  (yellow)
  //                    sitting at the same Y-plane chipsmith uses
  //                    when rendering footprint overlays (z=0 in
  //                    KiCad coords → Y=0 in scene)
  //   pcb-top          1.6mm faux PCB top face plane             (green)
  //                    derived from the PCB plate's expected
  //                    footprint extent + 5mm margin, even when
  //                    the plate isn't currently rendered
  //
  // Each anchor is a thin (50µm) horizontal slab so it reads as a
  // "this is the plane I'm anchoring on" surface, not a shape that
  // could be confused with real geometry.
  function renderDebugAnchors() {
    if (!(window.adomStep && window.adomStep.getScene && window.adomStep.getBabylon && window.adomStep.bbox)) {
      return { error: 'adom-step API not ready' };
    }
    const scene = window.adomStep.getScene();
    const B = window.adomStep.getBabylon();
    const group = 'debug-anchors';
    ensureOverlayDisposed(group);
    const SLAB = 0.05 * MM;
    const out = [];
    const children = [];

    // World-up detection — the chip-fetcher GLBs are authored with
    // Z as the height axis (CAD/EE convention; camera.upVector =
    // [0,0,1]). The chipsmith renderers were originally authored
    // Y-up; the debug anchors here use the SAME convention as the
    // active camera so they actually anchor where the chip lives,
    // and they expose to the user when other chipsmith renderers
    // disagree with that convention.
    const cam = scene.activeCamera;
    const up = cam && cam.upVector
      ? Math.abs(cam.upVector.z) > 0.5 ? 'Z' : (Math.abs(cam.upVector.y) > 0.5 ? 'Y' : 'X')
      : 'Y';
    const upIdx = up === 'Z' ? 2 : (up === 'Y' ? 1 : 0);
    const planeAxes = up === 'Z' ? [0, 1] : (up === 'Y' ? [0, 2] : [1, 2]); // axes orthogonal to up

    // makeSlab(name, w_m, h_m, center_xyz, color, labelExtra, slabUpAxis)
    //   slabUpAxis: 'X' | 'Y' | 'Z' — which axis the slab is THIN along.
    //   Defaults to the world-up axis. Override for slabs that anchor
    //   chipsmith's CURRENT placement (legacy Y-up footprint pads) so
    //   the user sees if those placements disagree with the world-up.
    function makeSlab(name, w_m, h_m, center_xyz, color, labelExtra, slabUpAxis) {
      const slabUp  = slabUpAxis || up;
      const slabIdx = slabUp === 'Z' ? 2 : (slabUp === 'Y' ? 1 : 0);
      const slabPlane = slabUp === 'Z' ? [0, 1] : (slabUp === 'Y' ? [0, 2] : [1, 2]);
      const dims = { width: 0.001, height: 0.001, depth: 0.001 };
      const dimNames = ['width', 'height', 'depth'];
      dims[dimNames[slabPlane[0]]] = w_m;
      dims[dimNames[slabPlane[1]]] = h_m;
      dims[dimNames[slabIdx]]      = SLAB;
      const m = B.MeshBuilder.CreateBox(name, dims, scene);
      m.position.x = center_xyz[0];
      m.position.y = center_xyz[1];
      m.position.z = center_xyz[2];
      const mat = new B.StandardMaterial(name + '-mat', scene);
      mat.diffuseColor  = new B.Color3(color.r, color.g, color.b);
      mat.emissiveColor = new B.Color3(color.r * 0.7, color.g * 0.7, color.b * 0.7);
      mat.specularColor = new B.Color3(0, 0, 0);
      mat.alpha = 0.55;
      m.material = mat;
      out.push(m);
      const labelW_mm = (w_m / MM).toFixed(2);
      const labelH_mm = (h_m / MM).toFixed(2);
      const upMm = (center_xyz[slabIdx] / MM).toFixed(3);
      const axisNote = slabUp === up ? '' : ` (THIN-${slabUp} ⚠ world-up=${up})`;
      children.push({
        meshes: [m],
        label: `${name.replace(/^debug-/, '')}  ${labelW_mm}×${labelH_mm} mm @ ${slabUp}=${upMm} mm${axisNote}  ${labelExtra || ''}`.trim(),
        kind: 'overlay-debug-anchor',
      });
    }

    // ── chip-top + chip-bottom ──────────────────────────
    // Use the body's bbox along the world-UP axis, with the slab
    // spanning the body's full footprint in the two plane axes.
    const bb = window.adomStep.bbox('Body');
    if (bb && !bb.error) {
      const planeW = bb.max[planeAxes[0]] - bb.min[planeAxes[0]];
      const planeH = bb.max[planeAxes[1]] - bb.min[planeAxes[1]];
      const ctrA   = (bb.min[planeAxes[0]] + bb.max[planeAxes[0]]) / 2;
      const ctrB   = (bb.min[planeAxes[1]] + bb.max[planeAxes[1]]) / 2;
      function pos(upVal) {
        const p = [0, 0, 0];
        p[planeAxes[0]] = ctrA;
        p[planeAxes[1]] = ctrB;
        p[upIdx]        = upVal;
        return p;
      }
      makeSlab('debug-chip-top',    planeW, planeH, pos(bb.max[upIdx]),
        { r: 0.30, g: 0.85, b: 0.95 }, `— body bbox max-${up}`);
      makeSlab('debug-chip-bottom', planeW, planeH, pos(bb.min[upIdx]),
        { r: 0.95, g: 0.30, b: 0.75 }, `— body bbox min-${up}`);
    } else {
      children.push({ meshes: [], label: 'chip-top — unavailable (no Body bbox)', kind: 'overlay-debug-anchor' });
      children.push({ meshes: [], label: 'chip-bottom — unavailable (no Body bbox)', kind: 'overlay-debug-anchor' });
    }

    // ── footprint-area ──────────────────────────────────
    // Compute extent of pad centers from the most-pad-trustworthy
    // source. The pads themselves are placed by renderFootprintOverlay
    // in world XZ (legacy Y-up convention) — we mirror THAT placement
    // so the anchor coincides with where chipsmith currently puts
    // its overlays, even if that placement disagrees with world-up.
    // (When the user sees the yellow slab in a different plane than
    // the green pcb-top slab, that's the bug.)
    const fps = state.footprintSources || {};
    const candidates = ['kicad', 'fusion', 'altium', 'datasheet']
      .map(s => ({ src: s, fp: fps[s] }))
      .filter(x => x.fp && !x.fp._stub && Array.isArray(x.fp.pads) && x.fp.pads.length > 0);
    if (candidates.length) {
      const best = candidates.sort((a, b) => b.fp.pad_count - a.fp.pad_count)[0];
      const ext = fpExtent(best.fp.pads);
      if (ext) {
        const w = ext.width  * MM;
        const h = ext.height * MM;
        const xs = best.fp.pads.map(p => p.at?.[0] || 0);
        const ys = best.fp.pads.map(p => p.at?.[1] || 0);
        const cxMm = (Math.min(...xs) + Math.max(...xs)) / 2;
        const cyMm = (Math.min(...ys) + Math.max(...ys)) / 2;
        // chipsmith places footprint pads at fpPlace(fpX, fpY, 0.02mm),
        // so the anchor goes there too. The slab is thin along the
        // world-up axis (matching the pad's flat orientation) — when
        // the chip body and footprint are correctly aligned, this
        // anchor sits flat on the seat plane right under chip-bottom.
        const center = fpPlace(cxMm, cyMm, 0);
        makeSlab('debug-footprint-area',
          w, h, center,
          { r: 0.95, g: 0.85, b: 0.20 },
          `— ${best.fp.pad_count} pads from ${best.src}`);
      }
    } else {
      children.push({ meshes: [], label: 'footprint-area — unavailable (no parsed pads)', kind: 'overlay-debug-anchor' });
    }

    // ── pcb-top ─────────────────────────────────────────
    // Where chipsmith currently puts the faux PCB plate's +Y face:
    // world Y=0, sized footprint extent + 5mm margin in world XZ.
    // (Same legacy Y-up convention as the footprint pads.)
    if (candidates.length) {
      const best = candidates.sort((a, b) => b.fp.pad_count - a.fp.pad_count)[0];
      const ext = fpExtent(best.fp.pads);
      if (ext) {
        const padMargin = 5;
        const w = (ext.width  + padMargin * 2) * MM;
        const h = (ext.height + padMargin * 2) * MM;
        // PCB plate top face is at world-up height = 0 (renderFauxPcbPlate
        // places the plate so its +up face is flush with the seat plane).
        makeSlab('debug-pcb-top', w, h, fpPlace(0, 0, 0),
          { r: 0.30, g: 0.85, b: 0.45 },
          '— FR4 plate +up face @ height=0');
      }
    } else {
      children.push({ meshes: [], label: 'pcb-top — unavailable (no faux PCB extent)', kind: 'overlay-debug-anchor' });
    }

    window.adomStep.addOverlay({
      group,
      label: `Debug — anchor bboxes  (${children.length})  · world-up=${up}`,
      kind: 'overlay-debug-anchors',
      children,
    });
    state.debugAnchorsOn = true;
    return { ok: true, count: out.length, world_up_axis: up };
  }

  // ── Tab 7: Heatsink vias ────────────────────────────────────────
  // Auto-generate a square via grid under any detected exposed pad
  // (EP / ep_thermal). Drill 0.3mm, outer Cu 0.6mm, pitch 1.2mm,
  // 0.4mm edge margin per the plan. Output: positions in the EP's
  // local frame. v1.0 surfaces the data; v1.1 renders the 3D vias as
  // an overlay layer the user can toggle.

  function generateViaGrid(epSize, opts = {}) {
    const drill   = opts.drill_mm   || 0.30;
    const outerCu = opts.outer_cu_mm || 0.60;
    const pitch   = opts.pitch_mm   || 1.20;
    const margin  = opts.margin_mm  || 0.40;
    if (!epSize || epSize.width <= 2 * margin) return [];
    const usableX = epSize.width  - 2 * margin;
    const usableY = epSize.height - 2 * margin;
    const nx = Math.max(1, Math.floor(usableX / pitch) + 1);
    const ny = Math.max(1, Math.floor(usableY / pitch) + 1);
    const startX = -((nx - 1) * pitch) / 2;
    const startY = -((ny - 1) * pitch) / 2;
    const vias = [];
    for (let iy = 0; iy < ny; iy++) {
      for (let ix = 0; ix < nx; ix++) {
        vias.push({
          index: iy * nx + ix + 1,
          x_mm: startX + ix * pitch,
          y_mm: startY + iy * pitch,
          drill_mm: drill,
          outer_cu_mm: outerCu,
        });
      }
    }
    return { drill, outerCu, pitch, margin, nx, ny, count: nx * ny, vias };
  }

  function renderHeatsinkTab() {
    const body = document.getElementById('cs-body');
    const fps = state.footprintSources || {};
    if (!fps || Object.keys(fps).length === 0) {
      body.innerHTML = `<div class="cs-empty">Loading footprint sources…</div>`;
      fetch('api/footprint-sources').then(r => r.ok ? r.json() : null).then(d => {
        state.footprintSources = d;
        if (state.activeTab === 'heatsink') renderHeatsinkTab();
      });
      return;
    }
    // Find the EP from any footprint source (its size is what we need).
    const SRCS = ['kicad', 'fusion', 'altium', 'datasheet', 'ds2sf'];
    let epPad = null, epSrc = null;
    for (const s of SRCS) {
      const f = fps[s];
      if (!f || f._stub || !f.pads) continue;
      const ep = f.pads.find(p => p.size && p.size[0] >= 1.5 && p.size[1] >= 1.5 && p.size[0] === p.size[1]);
      // Heuristic: a square SMD pad ≥1.5×1.5 mm is almost always the EP/thermal.
      // Or look for the largest pad if the heuristic fails.
      const largest = f.pads.reduce((acc, p) => {
        if (!p.size) return acc;
        const a = p.size[0] * p.size[1];
        return (!acc || a > acc.area) ? { pad: p, area: a } : acc;
      }, null);
      const candidate = ep || (largest && largest.area > 2.0 ? largest.pad : null);
      if (candidate) { epPad = candidate; epSrc = s; break; }
    }
    if (!epPad) {
      state.status.heatsink = '─';
      refreshStatusPills();
      body.innerHTML = `
        <div class="cs-section">
          <h3 class="cs-section-title">Heatsink vias</h3>
          <div class="cs-meas-status pill-partial">No exposed pad / thermal pad detected on any footprint source. Heatsink-via grid is N/A for this chip.</div>
        </div>`;
      return;
    }
    const epSize = { width: epPad.size[0], height: epPad.size[1] };
    const grid = generateViaGrid(epSize);
    const epOriginMm = { x: epPad.at?.[0] || 0, y: epPad.at?.[1] || 0 };
    state.heatsinkGrid = { ep: { source: epSrc, pad_number: epPad.number, size_mm: epPad.size, origin_mm: epOriginMm }, grid };
    state.status.heatsink = grid.count > 0 ? '●' : '─';
    refreshStatusPills();
    // Derived spec figures EEs care about. ringMm = (outer Cu - drill) / 2.
    // InstaPCB minimum: 0.10 mm. The InstaPCB skill (gallia/skills/instapcb)
    // doesn't yet publish a min annular ring; 0.10 mm is the working value
    // confirmed by the user, derived from UV-fiber-laser positional
    // accuracy (~±25–50 µm) + drill-kerf tolerance (~±25 µm). For
    // reference: IPC-2221 Class 2 (commercial) is 0.05 mm, Class 3
    // (mil/medical) is 0.13 mm.
    const ringMm = Math.max(0, (grid.outerCu - grid.drill) / 2);
    const ringClass = ringMm >= 0.13 ? 'InstaPCB ✓ + IPC-2221 Class 3'
                    : ringMm >= 0.10 ? 'InstaPCB-OK (≥0.10 mm)'
                    : ringMm >= 0.05 ? '⚠ below InstaPCB min (0.10 mm) — IPC-2221 Class 2 only'
                    : '✗ below InstaPCB min (0.10 mm) and below IPC-2221 Class 2';
    // EP coverage: cumulative drill-hole area over the EP area.
    const drillArea = grid.count * Math.PI * Math.pow(grid.drill / 2, 2);
    const epArea    = epSize.width * epSize.height;
    const epCoveragePct = epArea > 0 ? (drillArea / epArea) * 100 : 0;
    const sample = grid.vias.slice(0, 16).map((v, i) => `
      <div class="cs-row">
        <span class="label">via ${v.index}</span>
        <span class="value mono">(${v.x_mm.toFixed(3)}, ${v.y_mm.toFixed(3)}) mm</span>
        <span class="value dim">drill Ø${v.drill_mm} / outer Ø${v.outer_cu_mm} / ring ${ringMm.toFixed(3)} mm</span>
      </div>`).join('');
    body.innerHTML = `
      <div class="cs-section">
        <h3 class="cs-section-title">Exposed pad</h3>
        <div class="cs-row"><span class="label">Source</span><span class="value mono">${esc(epSrc)}</span></div>
        <div class="cs-row"><span class="label">Pad number</span><span class="value mono">${esc(epPad.number)}</span></div>
        <div class="cs-row"><span class="label">Size</span><span class="value mono">${esc(epSize.width)} × ${esc(epSize.height)} mm</span></div>
      </div>
      <div class="cs-section">
        <h3 class="cs-section-title">Via grid</h3>
        <div class="cs-row" data-tooltip="Finished hole diameter — what the drill bit produces. InstaPCB minimum is 0.20 mm / 8 mil; 0.30 mm gives comfortable margin for laser-ablated barrels at 0.5 oz Cu plating."><span class="label">Drill (finished hole)</span><span class="value mono">Ø ${grid.drill.toFixed(2)} mm</span></div>
        <div class="cs-row" data-tooltip="Outer pad diameter (the copper land around the hole). Defines the via's outer Cu disc on top + bottom layers."><span class="label">Outer Cu pad</span><span class="value mono">Ø ${grid.outerCu.toFixed(2)} mm</span></div>
        <div class="cs-row" data-tooltip="Annular ring width = (outer pad Ø − drill Ø) / 2. The copper that surrounds the hole on each face. InstaPCB minimum: 0.10 mm (derived from UV-fiber-laser positional accuracy + kerf tolerance). IPC-2221 Class 2 ≥ 0.05 mm; Class 3 ≥ 0.13 mm."><span class="label">Annular ring</span><span class="value mono">${ringMm.toFixed(3)} mm  <span class="dim">(${ringClass})</span></span></div>
        <div class="cs-row" data-tooltip="Cu wall thickness inside the barrel. InstaPCB plates 0.5 oz Cu (≈ 17.5 µm / 0.0175 mm) per the published capabilities table. Affects the via's current-carrying capacity + thermal conductance through the barrel."><span class="label">Plating thickness</span><span class="value mono">17.5 µm <span class="dim">(0.5 oz Cu, InstaPCB)</span></span></div>
        <div class="cs-row"><span class="label">Pitch (center-to-center)</span><span class="value mono">${grid.pitch.toFixed(2)} mm</span></div>
        <div class="cs-row"><span class="label">Edge margin (from EP edge)</span><span class="value mono">${grid.margin.toFixed(2)} mm</span></div>
        <div class="cs-row"><span class="label">Grid</span><span class="value mono">${grid.nx} × ${grid.ny} = ${grid.count} vias</span></div>
        <div class="cs-row" data-tooltip="Fraction of the EP's copper area that is via opening (drill cross-section). High coverage = better thermal path but more solder loss into the holes during reflow. ~3–8% is typical."><span class="label">EP coverage</span><span class="value mono">${epCoveragePct.toFixed(1)}% <span class="dim">(via Cu / EP area)</span></span></div>
        <div class="cs-meas-status pill-ok">${grid.count} plated through-hole vias auto-generated under the EP. Sign-off bakes the grid into <code>&lt;mpn&gt;.chipsmith.json</code> so downstream board-design tools can ingest as a single "place this heatsink-via grid" action.</div>
      </div>
      <div class="cs-section">
        <h3 class="cs-section-title">EP copper pour (B.Cu, bottom side)</h3>
        <div class="cs-row dim"><span class="value">Copper region on the <strong>bottom</strong> of the PCB that the heatsink vias dump into. The chip's EP solders to F.Cu on top; vias drill through the FR4; pour on B.Cu radiates the heat away. <strong>On by default</strong> — drawing this pour by hand is busywork; chipsmith bakes its outline into <code>&lt;mpn&gt;.chipsmith.json</code> so downstream board-design tools can drop it as a single zone fill.</span></div>
        <div class="cs-row"><span class="label">Pour size</span><span class="value mono">${epSize.width.toFixed(2)} × ${epSize.height.toFixed(2)} mm <span class="dim">(matches EP)</span></span></div>
        <div class="cs-row"><span class="label">Layer</span><span class="value mono">B.Cu (bottom)</span></div>
      </div>
      <div class="cs-section">
        <h3 class="cs-section-title">3D overlay</h3>
        <div class="cs-row dim"><span class="value">Both the via grid and the EP pour are children of the <code>Heatsink</code> row in the Outline (left). Eye-toggle each independently from the Outline. The button below toggles the whole heatsink (both children) at once.</span></div>
        <div class="cs-row">
          <span class="label">Visibility</span>
          <button class="cs-btn cs-btn-${state.heatsinkOn ? 'primary' : 'secondary'}" id="cs-heatsink-toggle" data-tooltip="Toggle the entire heatsink overlay (vias + EP pour) on/off. For per-piece control, use the eye icons in the Outline panel on the heatsink's child rows.">${svgIcon('bookOpen')}<span class="lbl">${state.heatsinkOn ? 'Hide' : 'Show'} heatsink (${grid.count} vias + EP pour)</span></button>
        </div>
      </div>
      <div class="cs-section">
        <h3 class="cs-section-title">Positions (first ${Math.min(16, grid.count)} of ${grid.count})</h3>
        ${sample}
      </div>
    `;
    const hsBtn = document.getElementById('cs-heatsink-toggle');
    if (hsBtn) {
      hsBtn.onclick = () => {
        const on = !state.heatsinkOn;
        if (on) renderHeatsink(epOriginMm, epSize, grid);
        else { ensureOverlayDisposed('heatsink'); state.epCopperPour = null; }
        state.heatsinkOn = on;
        state.viasOn = on;       // legacy state mirrors
        state.epPourOn = on;
        renderHeatsinkTab();
      };
    }
    // Default-ON behaviour — first time the heatsink tab renders for
    // this chip, draw the entire heatsink (vias + B.Cu pour) automatically.
    // The user can flip it off, but the sidecar always carries the spec
    // regardless of visibility — downstream tools inherit it.
    if (state.heatsinkOn === undefined && epOriginMm && epSize) {
      state.heatsinkOn = true;
      state.viasOn = true;
      state.epPourOn = true;
      renderHeatsink(epOriginMm, epSize, grid);
    }
  }

  // ── Tab 8: Solder-jet ───────────────────────────────────────────
  // Per gallia/skills/solder-jetting/SKILL.md: 300µm dome per dispense,
  // intra-pad spacing ≥100µm edge-to-edge, inter-pad spacing ≥300µm
  // edge-to-edge. Always at least 1 dot per pad — pads narrower than
  // the droplet get a single centred dot.

  function generateSolderJets(pads, opts = {}) {
    const D = (opts.droplet_um || 300) / 1000;       // mm
    const INTRA = (opts.intra_um || 100) / 1000;     // mm dot-edge to dot-edge inside a pad
    const INTER = (opts.inter_um || 300) / 1000;     // mm dot-edge to dot-edge across pads
    const all = [];
    for (const pad of pads) {
      if (!pad.size || pad.size[0] <= 0 || pad.size[1] <= 0) continue;
      const padW = pad.size[0], padH = pad.size[1];
      const cx = pad.at?.[0] || 0, cy = pad.at?.[1] || 0;
      // Pad too narrow for the dot? center one dot.
      if (Math.min(padW, padH) < D) {
        all.push({ x: cx, y: cy, padId: pad.number, droplet: D });
        continue;
      }
      const nx = Math.max(1, Math.floor((padW - D) / (D + INTRA)) + 1);
      const ny = Math.max(1, Math.floor((padH - D) / (D + INTRA)) + 1);
      const stepX = nx > 1 ? (padW - D) / (nx - 1) : 0;
      const stepY = ny > 1 ? (padH - D) / (ny - 1) : 0;
      const startX = cx - (padW - D) / 2;
      const startY = cy - (padH - D) / 2;
      for (let iy = 0; iy < ny; iy++) {
        for (let ix = 0; ix < nx; ix++) {
          all.push({
            x: startX + ix * stepX,
            y: startY + iy * stepY,
            padId: pad.number,
            droplet: D,
          });
        }
      }
    }
    // Prune cross-pad violations: any pair with different pad IDs
    // closer than D + INTER (centre to centre) drops the one whose
    // pad has more remaining dots.
    const minPair = D + INTER;
    const counts = {};
    for (const c of all) counts[c.padId] = (counts[c.padId] || 0) + 1;
    const drop = new Set();
    for (let i = 0; i < all.length; i++) {
      if (drop.has(i)) continue;
      for (let j = i + 1; j < all.length; j++) {
        if (drop.has(j)) continue;
        const a = all[i], b = all[j];
        if (a.padId === b.padId) continue;
        const dx = a.x - b.x, dy = a.y - b.y;
        if (Math.hypot(dx, dy) < minPair) {
          // Drop from whichever has more remaining dots.
          const losing = counts[a.padId] >= counts[b.padId] ? i : j;
          drop.add(losing);
          counts[all[losing].padId]--;
        }
      }
    }
    const kept = all.filter((_, i) => !drop.has(i));
    return {
      droplet_mm: D,
      intra_mm: INTRA,
      inter_mm: INTER,
      total: kept.length,
      pruned: drop.size,
      domes: kept,
      per_pad: counts,
    };
  }

  function renderSolderJetOverlay(domes) {
    if (!domes || !domes.length) return null;
    if (!(window.adomStep && window.adomStep.getScene && window.adomStep.getBabylon)) return null;
    const scene = window.adomStep.getScene();
    const B = window.adomStep.getBabylon();
    if (!scene || !B) return null;
    const group = 'solder-jet';
    ensureOverlayDisposed(group);
    const mat = new B.StandardMaterial(group + '-mat', scene);
    mat.diffuseColor = new B.Color3(0.75, 0.78, 0.85);
    mat.specularColor = new B.Color3(0.30, 0.32, 0.36);
    mat.emissiveColor = new B.Color3(0.08, 0.08, 0.10);
    // Group domes by their parent pad, one outline child per pad.
    // Babylon's CreateSphere with slice:0.5 keeps the +Y hemisphere
    // (flat side at y=0, curved side bulging up to y=+R). For Y-up
    // worlds that's already correct. For Z-up worlds we rotate by
    // -π/2 about X so the curved side bulges toward +Z (up).
    const byPad = new Map();
    let total = 0;
    for (const d of domes) {
      const dome = B.MeshBuilder.CreateSphere(`${group}-${total}`, {
        diameter: d.droplet * MM, segments: 8, slice: 0.5,
      }, scene);
      if (worldUpIdx() === 2) dome.rotation.x = -Math.PI / 2;
      // Sit just above the pad's top face. Pad top is at height = 0.04
      // (renderFootprintOverlay puts pads at h = 0.02 ± 0.02 thickness),
      // dome flat face anchored at h=0.05 leaves a tiny visible gap so
      // the dome doesn't z-fight the pad.
      placeMeshAt(dome, d.x, d.y, 0.05);
      dome.material = mat;
      total++;
      const key = String(d.padId);
      if (!byPad.has(key)) byPad.set(key, { padId: key, meshes: [], drop: d.droplet });
      byPad.get(key).meshes.push(dome);
    }
    // Approx volume per dome: hemisphere V = (2/3)πr³, in nL (mm³).
    function volNl(r) { return (2/3) * Math.PI * r * r * r; }
    const children = Array.from(byPad.values()).map(p => {
      const r = (p.drop / 2);
      const v = p.meshes.length * volNl(r);
      return {
        meshes: p.meshes,
        label: `pad ${p.padId}  ${p.meshes.length} dome${p.meshes.length === 1 ? '' : 's'}  Ø ${(p.drop*1000).toFixed(0)} µm  vol Σ ${v.toFixed(2)} nL`,
        kind: 'overlay-solderjet-pad',
      };
    });
    window.adomStep.addOverlay({
      group,
      label: `Solder-jet  (${total} × Ø${(domes[0].droplet * 1000).toFixed(0)} µm  intra ≥0.10  inter ≥0.30)`,
      kind: 'overlay-solderjet',
      children,
    });
    return { group, count: total };
  }

  function renderSolderjetTab() {
    const body = document.getElementById('cs-body');
    const fps = state.footprintSources || {};
    // Source-picker policy: filter out sources whose pads are clearly
    // garbage-scaled (the kicad library entry for VL53L8CX, for example,
    // ships pads at 0.1×0.2 mm — smaller than the 300 µm solder droplet,
    // which would force one centered dome per pad regardless of the
    // real package geometry). A pad whose smaller side is < 0.15 mm
    // (smaller than half the droplet) is treated as untrustworthy; a
    // source where the MEDIAN pad area is < 0.04 mm² is rejected
    // entirely. Among trustworthy sources, prefer the one with the
    // most pads. Fall back to raw-most-pads if every source fails the
    // sanity gate so we still render something.
    const candidates = ['kicad', 'fusion', 'altium', 'datasheet']
      .map(s => ({ src: s, fp: fps[s] }))
      .filter(x => x.fp && !x.fp._stub && Array.isArray(x.fp.pads) && x.fp.pads.length > 0);
    function padArea(p) {
      const w = p?.size?.[0] || 0, h = p?.size?.[1] || 0;
      return w * h;
    }
    function padMinSide(p) {
      const w = p?.size?.[0] || 0, h = p?.size?.[1] || 0;
      return Math.min(w, h);
    }
    function median(arr) {
      if (!arr.length) return 0;
      const s = arr.slice().sort((a, b) => a - b);
      const m = Math.floor(s.length / 2);
      return s.length % 2 ? s[m] : (s[m - 1] + s[m]) / 2;
    }
    const trustworthy = candidates.filter(c => {
      const minSides = c.fp.pads.map(padMinSide).filter(v => v > 0);
      const areas    = c.fp.pads.map(padArea).filter(v => v > 0);
      if (minSides.length === 0) return false;
      const minMin = Math.min(...minSides);
      const medArea = median(areas);
      // 0.15 mm = half the 300 µm droplet; below that a single dome
      // can't sit on the pad without overhang. 0.04 mm² ≈ 200×200 µm,
      // smaller than just about any real SMD pad.
      return minMin >= 0.15 && medArea >= 0.04;
    });
    const pool = trustworthy.length ? trustworthy : candidates;
    pool.sort((a, b) => b.fp.pad_count - a.fp.pad_count);
    const best = pool[0];
    const sourceDistrust = trustworthy.length === 0 && candidates.length > 0;
    if (!best) {
      state.status.solderjet = '─';
      refreshStatusPills();
      body.innerHTML = `<div class="cs-empty">No footprint pads — load a footprint source first.</div>`;
      return;
    }
    const sj = generateSolderJets(best.fp.pads);
    state.solderjet = { source: best.src, ...sj };
    state.status.solderjet = sj.total > 0 ? '●' : '─';
    refreshStatusPills();

    // Per-pad row breakdown.
    const padRows = best.fp.pads.slice(0, 12).map(p => {
      const c = sj.per_pad[p.number] || 0;
      return `<div class="cs-row">
        <span class="label">${esc(p.number)}</span>
        <span class="value mono">${esc(p.size?.[0] || '?')} × ${esc(p.size?.[1] || '?')} mm</span>
        <span class="value dim">${c} dot${c === 1 ? '' : 's'}</span>
      </div>`;
    }).join('');

    // Build a per-source diagnostic so the user can see exactly why we
    // picked the source we did (and which sources were rejected).
    const sourceDiagRows = candidates.map(c => {
      const minSides = c.fp.pads.map(padMinSide).filter(v => v > 0);
      const areas    = c.fp.pads.map(padArea).filter(v => v > 0);
      const minMin = minSides.length ? Math.min(...minSides) : 0;
      const medArea = median(areas);
      const trust = (minMin >= 0.15 && medArea >= 0.04);
      const isPicked = c.src === best.src;
      const reason = trust
        ? (isPicked ? 'picked' : 'eligible')
        : (minMin < 0.15
          ? `rejected — smallest pad side ${(minMin * 1000).toFixed(0)} µm (&lt; 150 µm)`
          : `rejected — median pad area ${(medArea).toFixed(3)} mm² (&lt; 0.04 mm²)`);
      const cls = isPicked ? 'ok' : (trust ? 'pending' : 'fail');
      const glyph = isPicked ? '●' : (trust ? '○' : '✗');
      return `<div class="cs-row">
        <span class="label">${esc(c.src)}</span>
        <span class="value mono">${c.fp.pads.length} pads · min side ${(minMin*1000).toFixed(0)} µm · med area ${medArea.toFixed(3)} mm²</span>
        <span class="value dim" style="margin-left:auto">${reason}</span>
        <span class="pill ${cls}" style="margin-left:6px">${glyph}</span>
      </div>`;
    }).join('');
    const distrustNote = sourceDistrust
      ? `<div class="cs-meas-status pill-fail">Every available source has untrustworthy pad sizes (smaller than the 300 µm droplet) — falling back to raw pad count. The dome layout below may not reflect the real datasheet pad geometry; fix the upstream chip-fetcher data or load a better source.</div>`
      : '';

    body.innerHTML = `
      <div class="cs-section">
        <h3 class="cs-section-title">Source pick</h3>
        ${sourceDiagRows}
        ${distrustNote}
      </div>
      <div class="cs-section">
        <h3 class="cs-section-title">Spec (per gallia/skills/solder-jetting)</h3>
        <div class="cs-row"><span class="label">Droplet Ø</span><span class="value mono">${(sj.droplet_mm * 1000).toFixed(0)} µm</span></div>
        <div class="cs-row"><span class="label">Intra-pad gap</span><span class="value mono">≥ ${(sj.intra_mm * 1000).toFixed(0)} µm</span></div>
        <div class="cs-row"><span class="label">Inter-pad gap</span><span class="value mono">≥ ${(sj.inter_mm * 1000).toFixed(0)} µm</span></div>
      </div>
      <div class="cs-section">
        <h3 class="cs-section-title">Result</h3>
        <div class="cs-row"><span class="label">Total domes</span><span class="value mono">${sj.total}</span></div>
        <div class="cs-row"><span class="label">Pruned</span><span class="value mono">${sj.pruned} <span class="dim">(no-bridging override)</span></span></div>
        <div class="cs-meas-status pill-${sj.total > 0 ? 'ok' : 'fail'}">${sj.total} half-sphere domes auto-laid out across ${best.fp.pads.length} pads. Each dome ≥ 1 per pad. Sign-off bakes the dome layout into <code>&lt;mpn&gt;.chipsmith.json</code> for the InstaPCB solder-jet head.</div>
      </div>
      <div class="cs-section">
        <h3 class="cs-section-title">3D overlay</h3>
        <div class="cs-row">
          <span class="label">Visibility</span>
          <button class="cs-btn cs-btn-${state.solderjetOn ? 'primary' : 'secondary'}" id="cs-sj-toggle" data-tooltip="Toggle the ${sj.total} solder-jet domes as half-spheres at z>0 above each footprint pad. Pre-reflow paste preview.">${svgIcon('bookOpen')}<span class="lbl">${state.solderjetOn ? 'Hide' : 'Show'} ${sj.total} domes in 3D</span></button>
        </div>
      </div>
      <div class="cs-section">
        <h3 class="cs-section-title">Per-pad (first 12)</h3>
        ${padRows}
      </div>
    `;
    const sjBtn = document.getElementById('cs-sj-toggle');
    if (sjBtn) {
      sjBtn.onclick = () => {
        const on = !state.solderjetOn;
        if (on) renderSolderJetOverlay(sj.domes);
        else ensureOverlayDisposed('solder-jet');
        state.solderjetOn = on;
        renderSolderjetTab();
      };
    }
  }

  // ── Tab 6: Sign-off ─────────────────────────────────────────────
  // Gate evaluator. Reads every state.status pill + the underlying
  // state objects, builds the canonical sign-off gate list per the
  // plan, evaluates pass/blocked, and offers a "Sign off" CTA when
  // every required gate passes. v1.0 writes <mpn>.chipsmith.json
  // sidecar via POST /api/signoff (the Rust endpoint already exists).
  // Baking <mpn>.chipsmith.step is v1.1 (needs upstream service-step2glb
  // /bake endpoint).

  function gateState() {
    const status = state.status;
    return [
      { id: 'source',         label: 'Manifest + spec complete',           pill: status.source,       hard: true,  tab: 'source' },
      { id: 'measurements',   label: 'Measurements within ±5%',            pill: status.measurements, hard: true,  tab: 'measurements' },
      { id: 'footprint',      label: 'Footprint source(s) loaded',         pill: status.footprint,    hard: true,  tab: 'footprint' },
      { id: 'pin1',           label: 'Pin-1 indicator',                    pill: status.pin1,         hard: false, tab: 'pin1' },
      { id: 'heatsink',       label: 'Heatsink vias generated (if EP)',    pill: status.heatsink,     hard: false, tab: 'heatsink' },
      { id: 'solderjet',      label: 'Solder-jet layout',                  pill: status.solderjet,    hard: false, tab: 'solderjet' },
    ];
  }

  function canSignOff() {
    const gates = gateState();
    const blockers = gates.filter(g => g.hard && (g.pill === '○' || g.pill === '✗'));
    return { ok: blockers.length === 0, blockers };
  }

  function renderSignoffTab() {
    const body = document.getElementById('cs-body');
    const gates = gateState();
    const { ok, blockers } = canSignOff();
    state.status.signoff = ok ? '●' : (blockers.length === gates.length ? '○' : '◐');
    refreshStatusPills();
    const rows = gates.map(g => {
      const cls = ({ '●': 'ok', '◐': 'partial', '○': 'pending', '✗': 'fail', '─': 'na' }[g.pill] || 'pending');
      const label = g.hard ? esc(g.label) : `${esc(g.label)} <span class="dim">(soft)</span>`;
      return `<div class="cs-row cs-clickable" data-tab="${g.tab}" data-tooltip="Click to jump to the ${esc(g.tab)} tab and address this gate.">
        <span class="label">${label}</span>
        <span class="pill ${cls}" style="margin-left:auto">${g.pill}</span>
      </div>`;
    }).join('');

    // Already signed off? Show the emitted artifact paths with reveal
    // buttons so the user can jump straight to them in the VS Code
    // Explorer. The `state.signoffResult` is set in the click handler;
    // on tab re-render after page reload, fetch GET /api/signoff to
    // re-hydrate from server state.
    let artifactsHTML = '';
    function buildArtifactsHTML(r) {
      if (!r) return '';
      const reveal = (path) => path
        ? `<button class="cs-reveal-icon" data-path="${escAttr(path)}" aria-label="Reveal in Explorer" data-tooltip="Reveal this file in the VS Code Explorer sidebar (delegates to adom-vscode reveal).">${svgIcon('folder')}</button>`
        : '';
      const dirOf = (p) => p ? p.replace(/\/[^/]+$/, '') : null;
      const enclosing = r.sidecar_path ? dirOf(r.sidecar_path) : null;
      const rows = [];
      if (r.sidecar_path) rows.push(`<div class="cs-row"><span class="label">.chipsmith.json</span><span class="value mono" title="${escAttr(r.sidecar_path)}">${esc(basename(r.sidecar_path))}</span>${reveal(r.sidecar_path)}</div>`);
      if (r.step_path)    rows.push(`<div class="cs-row"><span class="label">.chipsmith.step</span><span class="value mono" title="${escAttr(r.step_path)}">${esc(basename(r.step_path))}</span>${reveal(r.step_path)}</div>`);
      if (enclosing)      rows.push(`<div class="cs-row"><span class="label">Enclosing folder</span><span class="value mono" title="${escAttr(enclosing)}">${esc(enclosing)}</span>${reveal(enclosing)}</div>`);
      const bakeNote = r.bake_status
        ? `<div class="cs-row dim" style="margin-top:6px"><span class="value" style="font-size:11px">${esc(r.bake_status)}</span></div>`
        : '';
      return `
        <div class="cs-section">
          <h3 class="cs-section-title">Sign-off artifacts</h3>
          ${rows.join('')}
          ${bakeNote}
        </div>`;
    }
    if (state.signoffResult && state.signoffResult.ok) {
      artifactsHTML = buildArtifactsHTML(state.signoffResult);
    }
    body.innerHTML = `
      <div class="cs-section">
        <h3 class="cs-section-title">Gates</h3>
        ${rows}
      </div>
      <div class="cs-section">
        <h3 class="cs-section-title">Sign-off</h3>
        <div class="cs-meas-status pill-${ok ? 'ok' : 'partial'}">
          ${ok
            ? 'All hard gates pass. Click <strong>Sign off</strong> to write <code>&lt;mpn&gt;.chipsmith.json</code> with the applied scale, detected breakdown, and pin-1 agreement. Baking <code>.chipsmith.step</code> is v1.1.'
            : `${blockers.length} hard gate(s) blocking: <strong>${blockers.map(b => esc(b.label)).join(', ')}</strong>. Click each red row above to jump to its tab.`}
        </div>
        <div class="cs-actions">
          <button class="cs-btn cs-btn-primary" id="cs-signoff-btn" ${ok ? '' : 'disabled style="opacity:0.5;cursor:not-allowed"'} data-tooltip="Write the &lt;mpn&gt;.chipsmith.json sidecar with the full chipsmith state — applied scale, detected breakdown, pin-1 agreement, sign-off timestamp. Disabled until every hard gate passes.">${svgIcon('bookOpen')}<span class="lbl">Sign off this chip</span></button>
        </div>
      </div>
      ${artifactsHTML}
    `;
    // Hydrate sign-off artifacts from server (in case the page was
    // reloaded after a previous sign-off).
    if (!state.signoffResult) {
      fetch('api/signoff').then(r => r.ok ? r.json() : null).then(j => {
        if (j && j.signed) {
          state.signoffResult = { ok: true, sidecar_path: j.sidecar_path, step_path: j.step_path, bake_status: j.bake_status };
          if (state.activeTab === 'signoff') renderSignoffTab();
        }
      }).catch(()=>{});
    }
    // Wire reveal icons inside the artifacts section.
    body.querySelectorAll('.cs-reveal-icon').forEach(btn => {
      btn.onclick = async (e) => {
        e.stopPropagation();
        const path = btn.dataset.path;
        try {
          await fetch('api/reveal', { method: 'POST', headers: {'Content-Type':'application/json'}, body: JSON.stringify({path}) });
          btn.classList.add('cs-reveal-ok');
          setTimeout(() => btn.classList.remove('cs-reveal-ok'), 500);
        } catch (err) { console.error('[chipsmith] reveal failed', err); }
      };
    });
    body.querySelectorAll('.cs-clickable[data-tab]').forEach(row => {
      row.onclick = () => setActiveTab(row.dataset.tab);
    });
    const btn = document.getElementById('cs-signoff-btn');
    if (btn && ok) {
      btn.onclick = async () => {
        btn.disabled = true;
        btn.querySelector('.lbl').textContent = 'Signing off…';
        try {
          const payload = {
            mpn: state.spec?.mpn,
            measurements: state.measurements,
            heatsink: state.heatsinkGrid, ep_copper_pour: state.epCopperPour || null,
            user_placed_pin1: state.userPin1Corner ? { corner: state.userPin1Corner } : null,
            decorative_product_names: state.decorativeProductNames || [],
            signed_at: new Date().toISOString(),
          };
          const r = await fetch('api/signoff', {
            method: 'POST',
            headers: { 'Content-Type': 'application/json' },
            body: JSON.stringify(payload),
          });
          const j = await r.json();
          btn.querySelector('.lbl').textContent = j.ok ? 'Signed off ✓' : 'Failed — see console';
          if (!j.ok) console.error('[chipsmith] signoff failed', j);
          else {
            state.signoffResult = j;
            renderSignoffTab(); // re-render to surface the artifacts panel + reveal buttons
          }
        } catch (e) {
          console.error('[chipsmith] signoff error', e);
          btn.querySelector('.lbl').textContent = 'Failed — see console';
        }
      };
    }
  }

  // ── Other tabs — placeholder content for now (Phase 9+) ─────────
  function renderPlaceholder(label, what) {
    const body = document.getElementById('cs-body');
    body.innerHTML = `
      <div class="cs-empty">
        <strong>${esc(label)}</strong> — ${esc(what)}
      </div>
    `;
  }

  function renderActiveTab() {
    const id = state.activeTab;
    if (id === 'source') return renderSourceTab();
    if (id === 'measurements') return renderMeasurementsTab();
    if (id === 'footprint')    return renderFootprintTab();
    if (id === 'pin1')         return renderPin1Tab();
    if (id === 'signoff')      return renderSignoffTab();
    if (id === 'heatsink')     return renderHeatsinkTab();
    if (id === 'solderjet')    return renderSolderjetTab();
  }

  // ── tiny helpers ──────────────────────────────────────────────
  function esc(s) {
    if (s === undefined || s === null) return '';
    return String(s).replace(/[&<>]/g, c => ({ '&':'&amp;','<':'&lt;','>':'&gt;' }[c]));
  }
  function escAttr(s) { return esc(s).replace(/"/g, '&quot;'); }
  function basename(p) { return p ? String(p).split('/').pop() : '—'; }

  // ── Wire it up: build pane, fetch backend state, render Tab 1 ─
  async function init() {
    injectStyles();
    buildPane();
    refreshStatusPills();
    try {
      const [paths, spec, fps, ds2sf, ds2sfPads, chip3d] = await Promise.all([
        fetch('api/paths').then(r => r.ok ? r.json() : null).catch(() => null),
        fetch('api/spec').then(r => r.ok ? r.json() : null).catch(() => null),
        fetch('api/footprint-sources').then(r => r.ok ? r.json() : null).catch(() => null),
        fetch('api/ds2sf').then(r => r.ok ? r.json() : null).catch(() => null),
        fetch('api/ds2sf-pads').then(r => r.ok ? r.json() : null).catch(() => null),
        fetch('api/ds2sf-chip3d').then(r => r.ok ? r.json() : null).catch(() => null),
      ]);
      state.paths = paths;
      state.spec = enrichSpec(spec);
      state.footprintSources = fps;
      state.ds2sfResult = ds2sf;
      state.chip3d = chip3d;
      // ds2sf v0.6.0 per-pad data → inject as a 5th footprint source
      // alongside kicad / fusion / altium / datasheet. Same schema.
      if (ds2sfPads && Array.isArray(ds2sfPads.pads) && ds2sfPads.pads.length > 0 && fps) {
        fps.ds2sf = {
          pads: ds2sfPads.pads,
          pad_count: ds2sfPads.pad_count || ds2sfPads.pads.length,
          source: ds2sfPads.source || 'ds2sf',
        };
      }
      enrichSubjectPopover();
      // Show loading placeholders in the outline while OCCT generates.
      const generating = [];
      if (chip3d && chip3d.chip_3d) generating.push('chip');
      if (ds2sfPads && ds2sfPads.pads && ds2sfPads.pads.length > 0) generating.push('footprint');
      if (generating.length && window.adomStep?.addOverlay) {
        for (const g of generating) {
          const group = g === 'chip' ? 'adom-chip' : 'adom-footprint';
          const label = g === 'chip'
            ? 'Adom 3D chip  ⏳ generating from datasheet via OCCT...'
            : 'Adom footprint  ⏳ generating extruded copper pads...';
          window.adomStep.addOverlay({ group, meshes: [], label, kind: 'overlay-generating' });
        }
      }
      for (const g of generating) loadGeneratedGlb(g);
    } catch (e) {
      console.error('[chipsmith] initial fetch failed', e);
    }
    renderActiveTab();
  }

  async function loadGeneratedGlb(kind) {
    const tag = kind === 'chip' ? 'chip' : 'footprint';
    const url = `api/generate-${tag === 'chip' ? 'chip' : 'footprint'}-glb?cb=${Date.now()}`;
    try {
      const resp = await fetch(url);
      if (!resp.ok) { console.log(`[chipsmith] generate-${tag}-glb: ${resp.status}`); return; }
      const blob = await resp.blob();
      const blobUrl = URL.createObjectURL(blob);
      const B = window.adomStep?.getBabylon();
      const scene = window.adomStep?.getScene();
      if (!B || !scene) return;
      const result = await B.SceneLoader.ImportMeshAsync('', '', blobUrl, scene, null, '.glb');
      const allMeshes = result.meshes.filter(m => m.getTotalVertices && m.getTotalVertices() > 0);
      if (!allMeshes.length) return;
      const group = tag === 'chip' ? 'adom-chip' : 'adom-footprint';
      ensureOverlayDisposed(group);

      // Read annotations from the server cache (the chipsmith_metadata
      // STEP product doesn't survive STEP→GLB conversion).
      let annotations = null;
      try {
        const annotResp = await fetch(`api/generated-${tag === 'chip' ? 'chip' : 'footprint'}-annotations`);
        if (annotResp.ok) {
          const config = await annotResp.json();
          // Build annotation structure from the generation config.
          const doc = config.document_metadata || {};
          const products = {};
          if (tag === 'chip') {
            products.chipsmith_body = { body_width_mm: config.body_width_mm, body_depth_mm: config.body_depth_mm, body_height_mm: config.body_height_mm, standoff_mm: config.standoff_height_mm };
            for (const la of (config.lead_annotations || [])) {
              products[`chipsmith_lead_${la.number}`] = { ...la, lead_shape: config.lead_shape, source: 'OCCT' };
            }
          } else {
            for (const p of (config.pads || [])) {
              products[`chipsmith_fp_pad_${p.number}`] = { pin_number: p.number, signal: p.name, function: p.function, shape: p.shape, corner_radius_mm: p.corner_radius, size_mm: p.size, center_mm: p.at, type: p.type, layers: p.layers, source: 'OCCT' };
            }
          }
          annotations = { document: doc, products };
        }
      } catch (_) {}
      const prodMeta = annotations?.products || {};

      // Group meshes by their parent node. Assembly hierarchy produces
      // =>[0:1:1:N] usage-occurrence names — extract the index N and
      // map to the annotation by position (index 1 = first product).
      const groups = new Map();
      for (const m of allMeshes) {
        const parentName = m.parent?.name || m.name;
        if (parentName.startsWith('chipsmith_metadata')) continue;
        if (parentName.includes('metadata')) continue;
        if (!groups.has(parentName)) groups.set(parentName, []);
        groups.get(parentName).push(m);
      }

      // Build an ordered annotation list so we can map by index.
      // The STEP assembly orders children: for chip = body, leads 1-N, pin1, ep, metadata.
      // For footprint = pads 1-N, metadata.
      const annotList = [];
      if (tag === 'chip') {
        annotList.push({ key: 'chipsmith_body', ...prodMeta.chipsmith_body });
        for (let i = 1; i <= 100; i++) {
          const k = `chipsmith_lead_${i}`;
          if (prodMeta[k]) annotList.push({ key: k, ...prodMeta[k] });
          else break;
        }
        if (prodMeta.chipsmith_pin1) annotList.push({ key: 'chipsmith_pin1', ...prodMeta.chipsmith_pin1 });
        if (prodMeta.chipsmith_ep) annotList.push({ key: 'chipsmith_ep', ...prodMeta.chipsmith_ep });
      } else {
        for (let i = 1; i <= 100; i++) {
          const k = `chipsmith_fp_pad_${i}`;
          if (prodMeta[k]) annotList.push({ key: k, ...prodMeta[k] });
          else break;
        }
      }

      // Build children from groups, matching by index.
      const children = [];
      let groupIdx = 0;
      for (const [prodName, meshes] of groups) {
        if (groupIdx >= annotList.length) break;
        const annot = annotList[groupIdx] || {};
        groupIdx++;
        let childLabel;
        const key = annot.key || prodName;
        if (key === 'chipsmith_body') {
          childLabel = `body  ${annot.body_width_mm||'?'}×${annot.body_depth_mm||'?'}×${annot.body_height_mm||'?'} mm`;
        } else if (key === 'chipsmith_pin1') {
          childLabel = 'pin-1 indicator';
        } else if (key === 'chipsmith_ep') {
          childLabel = `exposed pad  ${annot.ep_width_mm||'?'}×${annot.ep_depth_mm||'?'} mm`;
        } else if (annot.signal || annot.name) {
          childLabel = `${annot.pin_number || annot.number || '?'} (${annot.signal || annot.name})`;
        } else if (annot.pin_number || annot.number) {
          childLabel = `pad ${annot.pin_number}`;
        } else {
          childLabel = key.replace('chipsmith_', '').replace(/_/g, ' ');
        }
        children.push({ meshes, label: childLabel, kind: 'overlay-generated-child' });
      }

      const parentLabel = tag === 'chip'
        ? `Adom 3D chip  (${children.length} parts, generated from datasheet)`
        : `Adom footprint  (${children.length} pads, extruded copper at z=0)`;
      const tooltip = tag === 'chip'
        ? `Adom 3D chip (generated)\n\nParametric chip body + leads built by Adom from the\nmanufacturer datasheet mechanical drawing via ds2sf +\nservice-step2glb /create-chip (OCCT). Copyright-free.\n\n${annotations?.document?.license || ''}`
        : `Adom footprint (generated)\n\nExtruded copper pads built by Adom from ds2sf pad\npositions via /create-footprint (OCCT). Pad top at\nz=0, copper extends 35 µm downward.\n\n${annotations?.document?.license || ''}`;
      window.adomStep.addOverlay({ group, meshes: [], label: parentLabel, tooltip, kind: 'overlay-generated', children });
      state[tag === 'chip' ? 'generatedChipLoaded' : 'generatedFpLoaded'] = true;
      state[tag === 'chip' ? 'chipAnnotations' : 'fpAnnotations'] = annotations;
      console.log(`[chipsmith] ${tag} GLB loaded: ${allMeshes.length} meshes, ${children.length} products`);
    } catch (e) {
      console.log(`[chipsmith] generate-${tag}-glb failed: ${e.message || e}`);
    }
  }

  // ── AI-drivability surface ────────────────────────────────────
  // Every UI verb on the right-pane gets a JS hook so demo scripts and
  // tests can drive it without click-by-click DOM walking. CLI parity
  // is via `adom-chipsmith eval "<expr>"`.
  window.adomChipsmith = window.adomChipsmith || {};
  Object.assign(window.adomChipsmith, {
    overlayLoaded: true,
    version: 'overlay-v0.3',

    // Core state access + control.
    state:           () => state,
    setActiveTab,
    refreshStatus:   refreshStatusPills,
    reload:          init,

    // Per-tab actions.
    runMeasurements: () => { renderMeasurementsTab(); recomputeAllStatuses(); return state.measurements; },
    recomputeStatuses: recomputeAllStatuses,
    placePin1,                                       // (corner | {mode, shape, …})
    // Click-to-place arming — UI counterpart calls these from the
    // Pin-1 tab toggle. Programmatic callers can also arm + simulate
    // a pick via window.adomChipsmith.placePin1({mode:'click', xyMm}).
    armPin1Click,
    disarmPin1Click,
    // Laser-etch — cosmetic MPN engraving on the chip's top face.
    // Toggle via setLaserEtchVisible(true|false). renderLaserEtch
    // takes opts: {text, depth_mm, max_size_mm:[w,h]}.
    renderLaserEtch,
    setLaserEtchVisible: (on, opts) => {
      if (on) return renderLaserEtch(opts || {});
      ensureOverlayDisposed('laser-etch');
      state.laserEtch = null;
      return { ok: true, hidden: true };
    },
    // Debug — show four colored slabs marking the chipsmith-internal
    // anchor planes (chip-top, chip-bottom, footprint-area, pcb-top).
    // Toggle on when reporting "your placement is wrong" — the slabs
    // show whether chipsmith's understanding of "the body's top face"
    // matches reality.
    renderDebugAnchors,
    setDebugAnchorsVisible: (on) => {
      if (on) return renderDebugAnchors();
      ensureOverlayDisposed('debug-anchors');
      state.debugAnchorsOn = false;
      return { ok: true, hidden: true };
    },
    removePin1User:  () => {
      ensureOverlayDisposed('user-pin1');
      state.userPin1Corner = null;
      recomputeAllStatuses();
      if (state.activeTab === 'pin1') renderPin1Tab();
      return { ok: true };
    },
    // Alias matching the CLI verb `set-overlay pin1 off`.
    removePin1Indicator: () => {
      ensureOverlayDisposed('user-pin1');
      state.userPin1Corner = null;
      recomputeAllStatuses();
      if (state.activeTab === 'pin1') renderPin1Tab();
      return { ok: true };
    },

    // 3D overlay toggles (programmatic counterparts to the buttons).
    setFootprintOverlay: (src, on) => {
      const fps = state.footprintSources;
      if (!fps || !fps[src] || fps[src]._stub) return { error: 'no source ' + src };
      state.fpOverlaysOn = state.fpOverlaysOn || {};
      if (on) renderFootprintOverlay(src, fps[src].pads);
      else ensureOverlayDisposed('fp-' + src);
      state.fpOverlaysOn[src] = !!on;
      if (state.activeTab === 'footprint') renderFootprintTab();
      return { ok: true, src, on: !!on };
    },
    setAllFootprintOverlays: (on) => {
      const fps = state.footprintSources;
      if (!fps) return { error: 'no footprint sources loaded' };
      const present = ['kicad','fusion','altium','datasheet','ds2sf'].filter(s => fps[s] && !fps[s]._stub && fps[s].pad_count > 0);
      state.fpOverlaysOn = state.fpOverlaysOn || {};
      for (const s of present) {
        if (on && !state.fpOverlaysOn[s]) renderFootprintOverlay(s, fps[s].pads);
        else if (!on && state.fpOverlaysOn[s]) ensureOverlayDisposed('fp-' + s);
        state.fpOverlaysOn[s] = !!on;
      }
      if (state.activeTab === 'footprint') renderFootprintTab();
      return { ok: true, count: present.length, on: !!on };
    },
    // Stock-library overlay — fetch + show/hide + status. Backed by
    // service-kicad via /api/kicad-stock; the spec must carry
    // footprint_name (and ideally kicad_3d_name) for the lookup to
    // succeed. Returns { loaded, parsed, fetched_url } on success.
    loadStockLibraryOverlay: (force) => loadStockLibraryOverlay({ force: !!force }),
    setStockOverlayVisible: (on) => {
      const parsed = state.stockOverlay?.parsed;
      if (!parsed) return { error: 'stock library not loaded — call loadStockLibraryOverlay() first' };
      state.fpOverlaysOn = state.fpOverlaysOn || {};
      if (on) renderFootprintOverlay('stock', parsed.pads);
      else ensureOverlayDisposed('fp-stock');
      state.fpOverlaysOn.stock = !!on;
      if (state.activeTab === 'footprint') renderFootprintTab();
      return { ok: true, on: !!on };
    },
    setFauxPcbVisible: (on) => {
      const fps = state.footprintSources;
      if (!fps) return { error: 'no footprint sources loaded' };
      const present = ['kicad','fusion','altium','datasheet','ds2sf'].filter(s => fps[s] && !fps[s]._stub && fps[s].pads?.length);
      if (on) {
        let bestSrc = null, bestExt = null;
        for (const s of present) {
          const e = fpExtent(fps[s].pads);
          if (e && (!bestExt || e.width > bestExt.width)) { bestExt = e; bestSrc = s; }
        }
        renderFauxPcbPlate(bestExt, bestSrc ? fps[bestSrc].pads : null);
      } else {
        ensureOverlayDisposed('faux-pcb');
      }
      state.fauxPcbOn = !!on;
      if (state.activeTab === 'footprint') renderFootprintTab();
      return { ok: true, on: !!on };
    },
    setDs2sfVisible: async (on) => {
      if (on) {
        if (!state.ds2sfResult?.available) return { error: 'no ds2sf result for this chip' };
        const r = await renderDs2sfOverlay();
        if (state.activeTab === 'footprint') renderFootprintTab();
        return { ok: !!state.ds2sfOverlayOn, on: !!state.ds2sfOverlayOn, ...r };
      } else {
        ensureOverlayDisposed('ds2sf');
        state.ds2sfOverlayOn = false;
        if (state.activeTab === 'footprint') renderFootprintTab();
        return { ok: true, on: false };
      }
    },
    setHeatsinkVisible: (on, opts) => {
      if (!state.heatsinkGrid) return { error: 'no heatsink grid — run Tab 7 first' };
      const ep = state.heatsinkGrid.ep;
      if (on) renderHeatsink(ep.origin_mm, { width: ep.size_mm[0], height: ep.size_mm[1] }, state.heatsinkGrid.grid, opts || {});
      else { ensureOverlayDisposed('heatsink'); state.epCopperPour = null; }
      state.heatsinkOn = !!on;
      state.viasOn = !!on;
      state.epPourOn = !!on;
      if (state.activeTab === 'heatsink') renderHeatsinkTab();
      return { ok: true, on: !!on, pour: state.epCopperPour };
    },
    setSolderJetVisible: (on) => {
      if (!state.solderjet || !state.solderjet.domes) return { error: 'no solder-jet layout — run Tab 8 first' };
      if (on) renderSolderJetOverlay(state.solderjet.domes);
      else ensureOverlayDisposed('solder-jet');
      state.solderjetOn = !!on;
      if (state.activeTab === 'solderjet') renderSolderjetTab();
      return { ok: true, on: !!on };
    },

    // Sign-off API.
    signoff: {
      gates:        () => gateState(),
      canSignOff:   () => canSignOff(),
      signOff:      async () => {
        const { ok, blockers } = canSignOff();
        if (!ok) return { error: 'blocked', blockers };
        const payload = {
          mpn: state.spec?.mpn,
          measurements: state.measurements,
          heatsink: state.heatsinkGrid, ep_copper_pour: state.epCopperPour || null,
          solderjet: state.solderjet ? {
            droplet_mm: state.solderjet.droplet_mm,
            total: state.solderjet.total,
            pruned: state.solderjet.pruned,
          } : null,
          user_placed_pin1: state.userPin1Corner ? { corner: state.userPin1Corner } : null,
          decorative_product_names: state.decorativeProductNames || [],
          signed_at: new Date().toISOString(),
        };
        const r = await fetch('api/signoff', {
          method: 'POST',
          headers: { 'Content-Type': 'application/json' },
          body: JSON.stringify(payload),
        });
        const j = await r.json();
        // Cache the result so renderSignoffTab can render the artifacts
        // panel (sidecar + step + enclosing folder rows + reveal icons)
        // even when sign-off was triggered programmatically (CLI / AI
        // hook), not via the button.
        if (j && j.ok) {
          state.signoffResult = j;
          if (state.activeTab === 'signoff') renderSignoffTab();
        }
        return j;
      },
    },
  });

  // ── Hover-inspect HUD ───────────────────────────────────────────
  // Floats next to the cursor whenever it's over the 3D canvas.
  // Names what the user is looking at in plain language BEFORE they
  // have to click anything. Per the PLAN "Hover-inspect HUD" section.
  // Pickable filtered to chipsmith-tagged meshes + body / pads / etc;
  // throttled to one update per render frame; suppressed during camera
  // drag. Toggle: 'i' key or right-edge inspect button.

  function setupHoverHud() {
    if (!(window.adomStep && window.adomStep.getScene)) {
      setTimeout(setupHoverHud, 500);
      return;
    }
    const scene = window.adomStep.getScene();
    if (!scene) {
      setTimeout(setupHoverHud, 500);
      return;
    }
    const card = document.createElement('div');
    card.id = 'cs-hover-hud';
    card.style.cssText = `position:absolute; pointer-events:none; z-index:55;
      display:none; background:rgba(20,24,30,0.94);
      border:1px solid rgba(255,255,255,0.14); border-left:3px solid #00b8b1;
      border-radius:6px; padding:10px 12px; min-width:200px; max-width:320px;
      font-family:'Satoshi',sans-serif; font-size:11.5px; color:#e6edf3;
      box-shadow:0 6px 18px rgba(0,0,0,0.55);
      backdrop-filter:blur(4px); -webkit-backdrop-filter:blur(4px);`;
    const style = document.createElement('style');
    style.textContent = `
      #cs-hover-hud .title { font-family:'Familjen Grotesk',sans-serif; font-weight:600; margin-bottom:5px; }
      #cs-hover-hud .row { display:flex; justify-content:space-between; gap:12px; padding:2px 0; line-height:1.45; }
      #cs-hover-hud .row span { color:rgba(200,210,225,0.5); flex-shrink:0; min-width:56px; font-size:10.5px; }
      #cs-hover-hud .row b { color:#e6edf3; font-weight:500; text-align:right; word-break:break-word; }
      #cs-hover-hud .row b.mono { font-family:'JetBrains Mono',monospace; font-size:10.5px; }
    `;
    document.head.appendChild(style);
    document.body.appendChild(card);
    let suspended = false;       // suspended during camera drag
    let hoverEnabled = true;     // toggleable via 'i' / Esc
    let lastFrame = 0;

    // Categorize a picked mesh into a kind label + tint. Mirrors the
    // PLAN's category table.
    const KIND_TINTS = {
      body:        { label: 'chip body',                   tint: '#888' },
      pin1:        { label: 'pin-1 indicator',             tint: '#ffb74d' },
      pad:         { label: 'SMD pad',                     tint: '#00b8b1' },
      lead:        { label: 'SMD leg',                     tint: '#9d6cff' },
      ball:        { label: 'BGA ball',                    tint: '#cfd8dc' },
      ep:          { label: 'exposed pad / thermal',       tint: '#ff7043' },
      tab:         { label: 'reinforcement tab',           tint: '#90a4ae' },
      marker:      { label: 'silkscreen marker',           tint: '#fff' },
      'overlay-footprint': { label: 'footprint pad (overlay)', tint: '#1de9b6' },
      'overlay-pcb':       { label: 'FR4 board (faux)',         tint: '#66bb6a' },
      'overlay-vias':      { label: 'heatsink via',             tint: '#c79a4f' },
      'overlay-solderjet': { label: 'solder-jet dome',          tint: '#cfd8dc' },
      'overlay-pin1-user': { label: 'pin-1 indicator (user)',   tint: '#ffb74d' },
    };

    function classifyMesh(mesh) {
      if (!mesh || !mesh.name) return null;
      const n = mesh.name;
      // Overlay meshes — name pattern starts with "fp-<src>-pad-..."
      // or "faux-pcb-..." or "heatsink-vias-..." etc.
      if (n.startsWith('fp-')) {
        const m = n.match(/^fp-(\w+)-pad-(.+?)$/);
        return m
          ? { kind: 'overlay-footprint', label: KIND_TINTS['overlay-footprint'].label, tint: KIND_TINTS['overlay-footprint'].tint, detail: { source: m[1], pad: m[2] } }
          : null;
      }
      if (n === 'faux-pcb-plate') return { kind: 'overlay-pcb', ...KIND_TINTS['overlay-pcb'], detail: state.fauxPcbDims || {} };
      if (n.startsWith('heatsink-vias-bore')) {
        const m = n.match(/-(\d+)$/);
        return { kind: 'overlay-vias', ...KIND_TINTS['overlay-vias'], detail: { via: m && m[1] } };
      }
      if (n.startsWith('solder-jet-')) return { kind: 'overlay-solderjet', ...KIND_TINTS['overlay-solderjet'], detail: {} };
      if (n.startsWith('user-pin1-')) return { kind: 'overlay-pin1-user', ...KIND_TINTS['overlay-pin1-user'], detail: {} };

      // OCCT-generated products from /create-chip and /create-footprint.
      // Mesh names are "chipsmith_lead_1_primitive0" etc — walk to the
      // parent TransformNode to get the product name, then look up the
      // rich annotation metadata from the embedded chipsmith_metadata.
      let prodName = null;
      let cur = mesh.parent || mesh;
      while (cur) {
        const pn = cur.name || '';
        if (pn.startsWith('chipsmith_')) { prodName = pn; break; }
        cur = cur.parent;
      }
      if (!prodName && (n.startsWith('chipsmith_') || (mesh.parent && mesh.parent.name && mesh.parent.name.startsWith('chipsmith_')))) {
        prodName = mesh.parent?.name || n;
      }
      if (prodName && !prodName.startsWith('chipsmith_metadata')) {
        const chipMeta = state.chipAnnotations?.products?.[prodName];
        const fpMeta = state.fpAnnotations?.products?.[prodName];
        const meta = chipMeta || fpMeta || {};
        if (prodName === 'chipsmith_body') {
          return { kind: 'body', label: 'chip body (generated)', tint: '#888', detail: { ...meta, source: 'OCCT' } };
        }
        if (prodName === 'chipsmith_pin1') {
          return { kind: 'pin1', label: 'pin-1 indicator (generated)', tint: '#ffb74d', detail: meta };
        }
        if (prodName === 'chipsmith_ep') {
          return { kind: 'ep', label: 'exposed pad (generated)', tint: '#ff7043', detail: meta };
        }
        if (prodName.startsWith('chipsmith_lead_')) {
          const sig = meta.signal || '';
          const fn = meta.function || '';
          return { kind: 'lead', label: sig ? `lead ${meta.pin_number} (${sig})` : prodName, tint: '#d4a84b', detail: { ...meta, source: 'OCCT' } };
        }
        if (prodName.startsWith('chipsmith_fp_pad_')) {
          const sig = meta.signal || '';
          return { kind: 'overlay-footprint', label: sig ? `pad ${meta.pin_number} (${sig})` : prodName, tint: '#c79a4f', detail: { ...meta, source: 'OCCT' } };
        }
      }

      // Non-overlay, non-generated meshes — walk the parent chain.
      cur = mesh.parent;
      let parentName = null;
      while (cur) {
        if (cur.name && !cur.name.startsWith('__') && cur.name !== 'rootnode') {
          parentName = cur.name;
          break;
        }
        cur = cur.parent;
      }
      if (parentName) {
        const lower = parentName.toLowerCase();
        if (lower === 'body')     return { kind: 'body',  ...KIND_TINTS.body,  detail: { name: parentName } };
        if (lower.includes('contact') || lower.includes('lead') || lower.includes('pin'))
                                  return { kind: 'lead',  ...KIND_TINTS.lead,  detail: { name: parentName } };
        if (lower.includes('marker'))
                                  return { kind: 'marker',...KIND_TINTS.marker,detail: { name: parentName } };
        // Generic fallback — surface the PRODUCT name + raw mesh name so
        // the user has SOMETHING readable rather than a silent miss.
        return { kind: 'body', label: 'chip part — ' + parentName, tint: '#90a4ae', detail: { name: parentName } };
      }
      return null;
    }

    function fmtBbox(mesh) {
      try {
        mesh.computeWorldMatrix(true);
        const bi = mesh.getBoundingInfo();
        const lo = bi.boundingBox.minimumWorld, hi = bi.boundingBox.maximumWorld;
        const sx = (hi.x - lo.x) * 1000, sy = (hi.y - lo.y) * 1000, sz = (hi.z - lo.z) * 1000;
        return `${sx.toFixed(2)} × ${sz.toFixed(2)} × ${sy.toFixed(2)} mm`;
      } catch (_) { return '—'; }
    }

    function lookupPadData(source, padNum) {
      const fps = state.footprintSources;
      if (!fps || !fps[source]) return null;
      const pads = fps[source].pads;
      if (!Array.isArray(pads)) return null;
      return pads.find(p => String(p.number) === String(padNum));
    }

    function renderAnnotatedCard(detailLines, meta) {
      if (meta.source === 'OCCT') detailLines.push(`<div class="row"><span>source</span><b>STEP file (OCCT-generated)</b></div>`);
      if (meta.pin_number) detailLines.push(`<div class="row"><span>pad</span><b>${esc(meta.pin_number)}</b></div>`);
      if (meta.signal)     detailLines.push(`<div class="row"><span>signal</span><b>${esc(meta.signal)}</b></div>`);
      if (meta.function)   detailLines.push(`<div class="row"><span>function</span><b style="font-weight:400;font-size:10.5px">${esc(meta.function)}</b></div>`);
      if (meta.lead_shape) detailLines.push(`<div class="row"><span>lead shape</span><b>${esc(meta.lead_shape)}</b></div>`);
      if (meta.side)       detailLines.push(`<div class="row"><span>side</span><b>${esc(meta.side)}</b></div>`);
      if (meta.shape)      detailLines.push(`<div class="row"><span>shape</span><b>${esc(meta.shape)}${meta.corner_radius_mm ? ' r=' + meta.corner_radius_mm + ' mm' : ''}</b></div>`);
      const sz = meta.size_mm;
      if (Array.isArray(sz)) detailLines.push(`<div class="row"><span>pad size</span><b class="mono">${sz[0].toFixed(2)} × ${sz[1].toFixed(2)} mm</b></div>`);
      const ct = meta.center_mm;
      if (Array.isArray(ct)) detailLines.push(`<div class="row"><span>center</span><b class="mono">(${ct[0].toFixed(3)}, ${ct[1].toFixed(3)}) mm</b></div>`);
      const ly = meta.layers;
      if (Array.isArray(ly)) detailLines.push(`<div class="row"><span>layers</span><b>${esc(ly.join(' + '))}</b></div>`);
      if (meta.body_width_mm) detailLines.push(`<div class="row"><span>body</span><b class="mono">${meta.body_width_mm} × ${meta.body_depth_mm} × ${meta.body_height_mm} mm</b></div>`);
      if (meta.standoff_mm) detailLines.push(`<div class="row"><span>standoff</span><b class="mono">${meta.standoff_mm} mm</b></div>`);
      if (meta.ep_width_mm) detailLines.push(`<div class="row"><span>EP size</span><b class="mono">${meta.ep_width_mm} × ${meta.ep_depth_mm} mm</b></div>`);
    }

    function renderCard(c) {
      card.style.borderLeftColor = c.cls.tint;
      const detailLines = [];
      const d = c.cls.detail || {};

      // OCCT-generated products — read annotation metadata from the STEP
      if (d.source === 'OCCT' || d.pin_number || d.signal || d.body_width_mm) {
        renderAnnotatedCard(detailLines, d);
      }
      // Babylon overlay pads (ds2sf pads / kicad / fusion / altium)
      else if (c.cls.kind === 'overlay-footprint' && d.pad) {
        const pd = lookupPadData(d.source, d.pad);
        const srcLabel = (SRC_LABEL[d.source] || d.source);
        detailLines.push(`<div class="row"><span>source</span><b>${esc(srcLabel)}</b></div>`);
        detailLines.push(`<div class="row"><span>pad</span><b>${esc(d.pad)}</b></div>`);
        if (pd) {
          if (pd.name)     detailLines.push(`<div class="row"><span>signal</span><b>${esc(pd.name)}</b></div>`);
          if (pd.function) detailLines.push(`<div class="row"><span>function</span><b style="font-weight:400;font-size:10.5px">${esc(pd.function)}</b></div>`);
          detailLines.push(`<div class="row"><span>shape</span><b>${esc(pd.shape || 'rect')}${pd.corner_radius ? ' r=' + pd.corner_radius.toFixed(2) + ' mm' : ''}</b></div>`);
          if (pd.size) detailLines.push(`<div class="row"><span>pad size</span><b class="mono">${pd.size[0].toFixed(2)} × ${pd.size[1].toFixed(2)} mm</b></div>`);
          if (pd.at)   detailLines.push(`<div class="row"><span>center</span><b class="mono">(${pd.at[0].toFixed(3)}, ${pd.at[1].toFixed(3)}) mm</b></div>`);
          if (pd.layers) detailLines.push(`<div class="row"><span>layers</span><b>${esc(pd.layers.join(' + '))}</b></div>`);
          if (pd.drill)  detailLines.push(`<div class="row"><span>drill</span><b class="mono">${pd.drill.diameter ? pd.drill.diameter.toFixed(2) + ' mm ' + pd.drill.shape : 'none'}</b></div>`);
        }
      }
      // Generic chip parts
      else if (c.cls.kind === 'pad' || c.cls.kind === 'ball' || c.cls.kind === 'lead') {
        detailLines.push(`<div class="row"><span>name</span><b>${esc(d.name || c.mesh.name)}</b></div>`);
        detailLines.push(`<div class="row"><span>size</span><b>${fmtBbox(c.mesh)}</b></div>`);
      } else if (c.cls.kind === 'overlay-vias') {
        detailLines.push(`<div class="row"><span>via</span><b>${esc(d.via || '?')}</b></div>`);
        detailLines.push(`<div class="row"><span>size</span><b>${fmtBbox(c.mesh)}</b></div>`);
      } else {
        detailLines.push(`<div class="row"><span>size</span><b>${fmtBbox(c.mesh)}</b></div>`);
      }
      card.innerHTML = `
        <div class="title" style="color:${c.cls.tint};">${esc(c.cls.label)}</div>
        ${detailLines.join('')}`;
    }

    function pointerMove(e) {
      if (!hoverEnabled || suspended) { card.style.display = 'none'; return; }
      const now = performance.now();
      if (now - lastFrame < 16) return;
      lastFrame = now;
      // Babylon's scene.pointerX/Y only update on real input events; for
      // synthetic events (AI hook hoverAt + tests) we derive canvas-local
      // coords from clientX/Y instead.
      const canvasEl = scene.getEngine().getRenderingCanvas();
      const r = canvasEl ? canvasEl.getBoundingClientRect() : { left: 0, top: 0 };
      const pickX = (e && typeof e.clientX === 'number') ? (e.clientX - r.left) : scene.pointerX;
      const pickY = (e && typeof e.clientY === 'number') ? (e.clientY - r.top)  : scene.pointerY;
      const pickResult = scene.pick(pickX, pickY, m => {
        if (!m || !m.isEnabled() || !m.isVisible) return false;
        if (!m.getTotalVertices || m.getTotalVertices() <= 0) return false;
        if (['skyBox','shadowGround','ground','ViewerRoot'].includes(m.name)) return false;
        if ((m.name || '').startsWith('viewCube')) return false;
        // Skip ghosted meshes (isolation sets alpha ~0.1) so the focused
        // overlay pads underneath are reachable by the pick ray.
        const mat = m.material;
        if (mat && typeof mat.alpha === 'number' && mat.alpha < 0.15) return false;
        return true;
      });
      if (!pickResult || !pickResult.hit || !pickResult.pickedMesh) {
        card.style.display = 'none';
        return;
      }
      const cls = classifyMesh(pickResult.pickedMesh);
      if (!cls) { card.style.display = 'none'; return; }
      renderCard({ mesh: pickResult.pickedMesh, cls });
      // Position 12px down-right of cursor; flip if near edges.
      const x = e.clientX + 14, y = e.clientY + 14;
      const cw = card.offsetWidth, ch = card.offsetHeight;
      const ww = window.innerWidth, wh = window.innerHeight;
      card.style.left = (x + cw > ww ? e.clientX - cw - 14 : x) + 'px';
      card.style.top  = (y + ch > wh ? e.clientY - ch - 14 : y) + 'px';
      card.style.display = 'block';
    }

    // Listen on the canvas (only fires when cursor is over the 3D view)
    const canvas = scene.getEngine().getRenderingCanvas();
    if (canvas) {
      canvas.addEventListener('pointermove', pointerMove);
      canvas.addEventListener('pointerleave', () => { card.style.display = 'none'; });
      canvas.addEventListener('pointerdown', () => { suspended = true; card.style.display = 'none'; });
      window.addEventListener('pointerup', () => { suspended = false; });
    }
    document.addEventListener('keydown', (e) => {
      if (e.ctrlKey || e.metaKey || e.altKey) return;
      const t = e.target;
      if (t && (t.tagName === 'INPUT' || t.tagName === 'TEXTAREA' || t.isContentEditable)) return;
      if (e.key === 'i' || e.key === 'I') {
        hoverEnabled = !hoverEnabled;
        if (!hoverEnabled) card.style.display = 'none';
      } else if (e.key === 'Escape') {
        hoverEnabled = false;
        card.style.display = 'none';
      }
    });

    // AI hooks per PLAN: hoverAt({x, y}) returns same payload as the
    // visible card; lastHover() returns the most recent.
    let lastHover = null;
    const origRender = renderCard;
    renderCard = (c) => { lastHover = { name: c.mesh.name, kind: c.cls.kind, label: c.cls.label }; origRender(c); };
    window.adomChipsmith.hoverAt = ({ x, y }) => {
      const fakeEvent = { clientX: x, clientY: y };
      pointerMove(fakeEvent);
      return lastHover;
    };
    window.adomChipsmith.lastHover = () => lastHover;
    window.adomChipsmith.setHoverEnabled = (on) => {
      hoverEnabled = !!on;
      if (!hoverEnabled) card.style.display = 'none';
    };
  }

  // ── Floating Sign-off HUD (always visible from every tab) ──────
  // Per PLAN.md: "A floating verification card pinned to the top-right
  // of the canvas, visible from every tab." Top-right is taken by the
  // ViewCube + the right-pane HUD; we land at top-LEFT instead (offset
  // below the Outline header so it doesn't cover the search box).
  // Drag-positionable, collapsible to a compact badge, keyboard 'g'
  // toggles. State persists in localStorage.

  function setupSignoffHud() {
    if (document.getElementById('cs-signoff-hud')) return;
    const hud = document.createElement('div');
    hud.id = 'cs-signoff-hud';
    hud.innerHTML = `
      <div class="cs-so-head">
        <span class="cs-so-title">Sign-off</span>
        <span class="cs-so-tally"></span>
        <button class="cs-so-collapse" aria-label="Collapse" data-tooltip="Collapse to a compact badge that stays pinned. Click the badge to re-expand.">❮</button>
      </div>
      <div class="cs-so-body"></div>
      <div class="cs-so-foot">
        <button class="cs-btn cs-btn-primary cs-so-signoff-btn" disabled data-tooltip="Sign off — POST /api/signoff → writes <mpn>.chipsmith.json + bakes <mpn>.chipsmith.step. Disabled until every hard gate passes.">${svgIcon('bookOpen')}<span class="lbl">Sign off</span></button>
      </div>
    `;
    document.body.appendChild(hud);
    const HUD_KEY_POS = 'chipsmith.signoff-hud.pos';
    const HUD_KEY_COLLAPSED = 'chipsmith.signoff-hud.collapsed';
    function applyPos(pos) {
      hud.style.top = pos.y + 'px';
      hud.style.left = pos.x + 'px';
      hud.style.right = 'auto';
      hud.style.bottom = 'auto';
    }
    try {
      const raw = localStorage.getItem(HUD_KEY_POS);
      if (raw) applyPos(JSON.parse(raw));
    } catch (_) {}
    function setCollapsed(c) {
      hud.classList.toggle('collapsed', c);
      try { localStorage.setItem(HUD_KEY_COLLAPSED, c ? '1' : '0'); } catch (_) {}
    }
    try {
      if (localStorage.getItem(HUD_KEY_COLLAPSED) === '1') setCollapsed(true);
    } catch (_) {}
    hud.querySelector('.cs-so-collapse').onclick = () => setCollapsed(true);
    hud.querySelector('.cs-so-head').onclick = (e) => {
      // Click anywhere on the head area while collapsed → expand. Drag
      // start handled separately via mousedown below.
      if (hud.classList.contains('collapsed') && !e.target.closest('.cs-so-collapse')) setCollapsed(false);
    };

    // Drag.
    let dragOrigin = null;
    hud.querySelector('.cs-so-head').addEventListener('mousedown', (e) => {
      if (e.target.closest('.cs-so-collapse')) return;
      const r = hud.getBoundingClientRect();
      dragOrigin = { startX: e.clientX, startY: e.clientY, baseX: r.left, baseY: r.top };
      hud.classList.add('dragging');
      e.preventDefault();
    });
    document.addEventListener('mousemove', (e) => {
      if (!dragOrigin) return;
      const dx = e.clientX - dragOrigin.startX, dy = e.clientY - dragOrigin.startY;
      const x = Math.max(-hud.offsetWidth + 60, Math.min(window.innerWidth - 60, dragOrigin.baseX + dx));
      const y = Math.max(44, Math.min(window.innerHeight - 60, dragOrigin.baseY + dy));
      applyPos({ x, y });
    });
    document.addEventListener('mouseup', () => {
      if (!dragOrigin) return;
      const r = hud.getBoundingClientRect();
      try { localStorage.setItem(HUD_KEY_POS, JSON.stringify({ x: r.left, y: r.top })); } catch (_) {}
      hud.classList.remove('dragging');
      dragOrigin = null;
    });

    // Keyboard 'g' toggles (gates) — picked because P/O/B/I/M are taken.
    document.addEventListener('keydown', (e) => {
      if (e.ctrlKey || e.metaKey || e.altKey) return;
      const t = e.target;
      if (t && (t.tagName === 'INPUT' || t.tagName === 'TEXTAREA' || t.isContentEditable)) return;
      if (e.key === 'g' || e.key === 'G') {
        setCollapsed(!hud.classList.contains('collapsed'));
      }
    });
    return hud;
  }

  // Re-render the floating Sign-off HUD body. Called on every status
  // change. Groups gates per the PLAN spec (Inputs / Integrity /
  // Footprint / Pin-1 / Manufacturing).
  function renderSignoffHud() {
    const hud = document.getElementById('cs-signoff-hud');
    if (!hud) return;
    const body = hud.querySelector('.cs-so-body');
    const tallyEl = hud.querySelector('.cs-so-tally');
    const btn = hud.querySelector('.cs-so-signoff-btn');
    if (!body) return;

    // Build the structured gate list. Sections per PLAN.md.
    const s = state.spec || {};
    const fps = state.footprintSources || {};
    const m = state.measurements;
    const sj = state.solderjet;
    const hs = state.heatsinkGrid;
    const has = (k) => s[k] !== undefined && s[k] !== null && s[k] !== '' && s[k] !== '?';
    const fpsPresent = ['kicad','fusion','altium','datasheet','ds2sf'].filter(k => fps[k] && !fps[k]._stub && fps[k].pad_count > 0);
    const fpAnyPin1 = ['kicad','fusion','altium','datasheet','ds2sf'].some(k => fps[k] && fps[k].pin1_silk && fps[k].pin1_silk.found);
    const userPin1 = !!state.userPin1Corner;

    function row(id, label, pill, hard, tab, reason) {
      return { id, label, pill, hard, tab, reason };
    }

    const sections = [
      { title: 'Inputs', rows: [
        row('input_step',     'STEP loaded',           state.paths?.step ? '●' : '○',           true,  'source', state.paths?.step || '—'),
        row('input_fp',       'Footprint loaded',      fpsPresent.length > 0 ? '●' : '○',       true,  'source', fpsPresent.join(', ') || 'none'),
        row('input_datasheet','Datasheet loaded',      state.paths?.datasheet ? '●' : '─',      false, 'source', state.paths?.datasheet || '—'),
        row('input_spec',     'Spec complete',
          (has('pin_count') && has('structural_type') && Array.isArray(s.body_dims_mm)) ? '●'
          : ([has('pin_count'), has('structural_type'), Array.isArray(s.body_dims_mm)].filter(Boolean).length ? '◐' : '○'),
          true, 'source',
          `pin_count=${s.pin_count || '—'}, structural_type=${s.structural_type || '—'}, body_dims_mm=${Array.isArray(s.body_dims_mm) ? '['+s.body_dims_mm.join(',')+']' : '—'}`),
      ]},
      { title: 'Integrity', rows: [
        row('measurements', 'Measurements within ±5%',
          m ? m.status : '○',
          true, 'measurements',
          m ? `applied ${m.applied_label}, max Δ ${(m.max_axis_pct*100).toFixed(2)}%` : 'not run'),
      ]},
      { title: 'Footprint', rows: [
        row('fp_sources_loaded', 'Sources loaded',
          fpsPresent.length === 0 ? '✗' : (fpsPresent.length >= 2 ? '●' : '◐'),
          true, 'footprint',
          fpsPresent.join(', ') || 'none'),
        row('fp_bbox_fit',       'Bbox fit (chip ⊂ fp)',
          (m && fpsPresent.length) ? '◐' : '─',
          false, 'footprint',
          'visual check on Tab 3'),
      ]},
      { title: 'Pin-1', rows: [
        row('pin1_chip',  'Chip-side indicator',  userPin1   ? '●' : '○', false, 'pin1', userPin1 ? `placed ${state.userPin1Corner}` : 'not placed'),
        row('pin1_fp',    'Footprint silk marker', fpAnyPin1 ? '●' : '○', false, 'pin1', fpAnyPin1 ? 'present in at least one source' : 'no marker on any source'),
        row('pin1_agree', 'Cross-side agreement',  (userPin1 && fpAnyPin1) ? '◐' : '─', false, 'pin1', 'visual check'),
      ]},
      { title: 'Manufacturing', rows: [
        row('mfg_heatsink',  'Heatsink vias generated (if EP)', hs ? '●' : '─', false, 'heatsink',  hs ? `${hs.grid.count} vias under ${hs.ep.pad_number}` : 'no EP detected'),
        row('mfg_solderjet', 'Solder-jet layout',                sj ? '●' : '─', false, 'solderjet', sj ? `${sj.total} domes (Ø ${(sj.droplet_mm*1000).toFixed(0)} µm)` : 'not generated'),
        row('mfg_etch',      'Laser-etch',                       '─',                       false, 'source',    'optional, opt-in'),
      ]},
    ];

    // Tally = count of ● + ─ over total (excluding ─ from "fail" math
    // but including in "satisfied" because N/A is satisfied-by-design).
    const all = sections.flatMap(sec => sec.rows);
    const total = all.length;
    const passed = all.filter(r => r.pill === '●' || r.pill === '─').length;
    const blockers = all.filter(r => r.hard && (r.pill === '○' || r.pill === '✗'));
    const canSign = blockers.length === 0;

    tallyEl.textContent = `${passed} / ${total} ✓`;
    tallyEl.className = 'cs-so-tally ' + (canSign ? 'pass' : 'block');

    btn.disabled = !canSign;
    btn.classList.toggle('cs-btn-primary', canSign);
    btn.classList.toggle('cs-btn-secondary', !canSign);
    btn.title = canSign ? '' : `${blockers.length} hard gate${blockers.length === 1 ? '' : 's'} blocking: ${blockers.map(b => b.label).join(', ')}`;
    btn.onclick = canSign ? doSignoff : null;

    // Body markup.
    body.innerHTML = sections.map(sec => `
      <div class="cs-so-section">
        <div class="cs-so-section-title">${esc(sec.title)}</div>
        ${sec.rows.map(r => {
          const cls = ({'●':'ok','◐':'partial','○':'pending','✗':'fail','─':'na'}[r.pill] || 'pending');
          return `<div class="cs-so-row" data-tab="${esc(r.tab)}" title="${escAttr(r.reason)}" data-tooltip="${escAttr(r.reason)} — click to jump to ${esc(r.tab)} tab">
            <span class="cs-so-glyph ${cls}">${r.pill}</span>
            <span class="cs-so-label${r.hard ? '' : ' soft'}">${esc(r.label)}</span>
          </div>`;
        }).join('')}
      </div>
    `).join('');
    body.querySelectorAll('.cs-so-row').forEach(rEl => {
      rEl.onclick = () => setActiveTab(rEl.dataset.tab);
    });
  }

  async function doSignoff() {
    if (!window.adomChipsmith?.signoff?.signOff) return;
    const btn = document.querySelector('#cs-signoff-hud .cs-so-signoff-btn');
    if (btn) { btn.disabled = true; btn.querySelector('.lbl').textContent = 'Signing off…'; }
    try {
      const r = await window.adomChipsmith.signoff.signOff();
      if (btn) btn.querySelector('.lbl').textContent = r && r.ok ? 'Signed ✓' : 'Failed';
      if (state.activeTab === 'signoff') renderSignoffTab();
    } catch (e) {
      console.error('[chipsmith] sign-off failed', e);
      if (btn) btn.querySelector('.lbl').textContent = 'Failed — see console';
    }
  }

  // Decorative geometry — flagging and bake-payload contribution.
  // Per the plan, on /bake the flagged products get reparented under
  // a `chipsmith_decorative` branch so downstream tools can default
  // them off. chipsmith's responsibility here is to (a) auto-flag
  // products whose names match marketing-mark patterns, and (b)
  // surface a context-menu verb so the user can flag/unflag any
  // product. The baked-side reparent is upstream of us in
  // service-step2glb /bake.
  const DECORATIVE_NAME_PATTERNS = [
    /\b(logo|brand|trademark|copyright)\b/i,
    /^[A-Z]{2,3}$/,                         // bare 2-3 letter manufacturer mark, e.g., "ST" / "TI" / "NXP"
    /\b(silkscreen|laser|engrav)/i,
  ];
  function isAutoDecorativeName(name) {
    if (!name || typeof name !== 'string') return false;
    return DECORATIVE_NAME_PATTERNS.some(re => re.test(name));
  }
  function setDecorative(name, on) {
    if (!name) return;
    const set = new Set(state.decorativeProductNames || []);
    if (on) set.add(name); else set.delete(name);
    state.decorativeProductNames = Array.from(set);
    refreshSignoffHud();
  }
  function isDecorative(name) {
    return (state.decorativeProductNames || []).includes(name);
  }
  function autoFlagDecorativeFromTree() {
    if (!(window.adomStep && window.adomStep.listComponents)) return;
    let changed = false;
    const set = new Set(state.decorativeProductNames || []);
    for (const c of window.adomStep.listComponents()) {
      if (isAutoDecorativeName(c.name) && !set.has(c.name)) { set.add(c.name); changed = true; }
    }
    if (changed) state.decorativeProductNames = Array.from(set);
  }
  function registerCtxExtras() {
    window.adomStepCtxMenuExtras = (window.adomStepCtxMenuExtras || []).filter(e => !String(e.id).startsWith('chipsmith-'));
    window.adomStepCtxMenuExtras.push({
      id: 'chipsmith-decorative',
      visibleFor: (entry) => entry && !entry.isOverlay && !entry.isMaterialGroup && !!entry.name,
      label: (entry) => isDecorative(entry.name)
        ? 'Unflag decorative — keep on canonical bake'
        : 'Move to decorative branch on bake',
      tooltip: (entry) => isDecorative(entry.name)
        ? `Currently flagged decorative — will be reparented under chipsmith_decorative on sign-off (default OFF in downstream tools). Click to unflag.`
        : `Mark "${entry.name}" as decorative geometry. On sign-off the bake reparents it under chipsmith_decorative — visible by default in chipsmith but OFF in downstream Adom tools (chipfit, InstaPCB preview, etc.). Use for vendor logos, lens cosmetics, embossed marks. Geometry is moved, NEVER deleted.`,
      onClick: (entry) => setDecorative(entry.name, !isDecorative(entry.name)),
    });
  }
  function refreshSignoffHud() {
    if (typeof recomputeAllStatuses === 'function') recomputeAllStatuses();
  }

  // Run after the adom-step shell + outline mount. Slight delay so the
  // viewer has finished its initial frame and the canvas/right-edge is
  // measurable.
  function enrichSubjectPopover() {
    try {
      const sp = state.spec;
      if (!sp) return;
      const subj = document.getElementById('subject-info');
      if (!subj) return;
      if (subj.querySelector('.cs-chip-meta')) return;
      const esc = s => String(s).replace(/&/g, '&amp;').replace(/</g, '&lt;').replace(/>/g, '&gt;');
      const addRow = (k, v) => {
        if (!v) return;
        const txt = esc(v.length > 120 ? v.slice(0, 117) + '...' : v);
        const row = document.createElement('div');
        row.className = 'popover-row cs-chip-meta';
        row.innerHTML = '<span class="k">' + esc(k) + '</span><span class="v">' + txt + '</span>';
        subj.appendChild(row);
      };
      addRow('manufacturer', sp.manufacturer);
      addRow('package', sp.package || sp.package_family);
      addRow('family', sp.family);
      addRow('description', sp.description);
    } catch (_) {}
  }

  function bootChipsmith() {
    init();
    setupHoverHud();
    setupSignoffHud();
    registerCtxExtras();
    autoFlagDecorativeFromTree();
    // Chipsmith always needs the bottom fill light — you're inspecting
    // BGA balls, QFN pads, through-hole leg bottoms, footprint overlays.
    // The base viewer defaults to OFF; chipsmith overrides to ON.
    try {
      const btn = document.getElementById('tbBottomLight');
      const viewer = window.adomStep;
      if (viewer && viewer.setBottomLight && !viewer.getBottomLightState()) {
        viewer.setBottomLight(true);
        if (btn) btn.classList.add('active');
        try { localStorage.setItem('adom-step.bottomLight', '1'); } catch (_) {}
      }
    } catch (_) {}
  }
  if (document.readyState === 'loading') {
    document.addEventListener('DOMContentLoaded', () => setTimeout(bootChipsmith, 250));
  } else {
    setTimeout(bootChipsmith, 250);
  }

  console.log('[chipsmith-overlay] loaded — branding + Phase 1 right-pane + Tab 1 Source live');
})();