/* Chladni brand dark: #1A1B1C */ (function(){if(window.__chladniPlantBuilderLoaded)return;window.__chladniPlantBuilderLoaded=true;var s=document.createElement("script");s.text="(function(){\n if (document.getElementById(\"chladni-plant-builder-css\")) return;\n var s = document.createElement(\"style\");\n s.id = \"chladni-plant-builder-css\";\n s.appendChild(document.createTextNode(\"\\n #chladni-plant-builder{ --ink:#1A1B1C; --paper:#FFFFFF; --panel:#F4F4F4; --muted:#8A8A8A; --hairline:#E2E2E2; }\\n #chladni-plant-builder, #chladni-plant-builder *{ margin:0; padding:0; box-sizing:border-box; }\\n @media (prefers-reduced-motion: reduce) { #chladni-plant-builder, #chladni-plant-builder *{ transition:none !important; } }\\n #chladni-plant-builder{ background:var(--paper); color:var(--ink); font-family:Arial,Helvetica,sans-serif; font-size:15px; line-height:1.5; }\\n\\n #chladni-plant-builder .topbar{ background:var(--ink); color:#fff; display:flex; align-items:center; justify-content:space-between; gap:6px 16px; flex-wrap:wrap; padding:14px 20px; }\\n #chladni-plant-builder .topbar b{ font-size:14px; letter-spacing:1px; }\\n #chladni-plant-builder .topbar span{ color:#cfcfcf; font-size:12px; }\\n /* v14: mobile responsiveness pass - below ~480px the title and the plant-name link no longer\\n fit side by side without wrapping mid-word into an overlapping mess (client-reported bug).\\n Stacking them into their own full-width rows, left-aligned, is simpler and more reliable\\n than trying to shrink text to fit one line on every device width. */\\n @media (max-width:480px){\\n #chladni-plant-builder .topbar{ flex-direction:column; align-items:flex-start; padding:12px 16px; }\\n }\\n\\n\\n #chladni-plant-builder{ overflow-x: clip; max-width: 100%; }\\n #chladni-plant-builder .appgrid{ display:grid; grid-template-columns:340px 1fr; min-height:calc(100vh - 46px); }\\n @media (max-width:880px){ #chladni-plant-builder .appgrid{ grid-template-columns:1fr; } }\\n\\n /* ---- left: asset hierarchy tree ---- */\\n #chladni-plant-builder .tree{ background:var(--panel); border-right:1px solid var(--hairline); padding:18px 16px; overflow-y:auto; }\\n #chladni-plant-builder .tree h2{ font-size:11px; letter-spacing:2.5px; text-transform:uppercase; font-weight:700; color:var(--muted); margin-bottom:14px; }\\n #chladni-plant-builder .treeEmpty{ font-size:13px; color:var(--muted); padding:18px 4px; line-height:1.6; }\\n #chladni-plant-builder .assetNode{ background:#fff; border:1.5px solid var(--hairline); padding:12px 13px; margin-bottom:10px; cursor:pointer; }\\n #chladni-plant-builder .assetNode:hover, #chladni-plant-builder .assetNode.sel{ border-color:var(--ink); }\\n #chladni-plant-builder .assetNode .an-top{ display:flex; justify-content:space-between; align-items:flex-start; gap:8px; }\\n #chladni-plant-builder .assetNode .an-name{ font-weight:900; font-size:14px; line-height:1.25; }\\n #chladni-plant-builder .assetNode .an-type{ font-size:11px; color:var(--muted); margin-top:2px; }\\n #chladni-plant-builder .assetNode .an-badges{ margin-top:8px; display:flex; gap:5px; flex-wrap:wrap; }\\n #chladni-plant-builder .badge{ font-size:9px; letter-spacing:1px; font-weight:700; padding:2.5px 6px; text-transform:uppercase; background:var(--panel); color:#555; }\\n #chladni-plant-builder .badge.crit-very{ background:var(--ink); color:#fff; }\\n #chladni-plant-builder .badge.crit-high{ background:#3a3a3a; color:#fff; }\\n #chladni-plant-builder .badge.legacy{ background:#fff; border:1px solid var(--ink); }\\n #chladni-plant-builder .badge.incomplete{ background:#fff; border:1px solid #b02020; color:#b02020; }\\n #chladni-plant-builder .an-row{ display:flex; align-items:center; justify-content:space-between; margin-top:10px; padding-top:9px; border-top:1px solid var(--hairline); }\\n #chladni-plant-builder .stepper{ display:flex; align-items:center; gap:6px; }\\n #chladni-plant-builder .stepper button{ width:22px; height:22px; border:1px solid #cfcfcf; background:#fff; font-weight:900; cursor:pointer; line-height:1; font-family:Arial; }\\n #chladni-plant-builder .stepper button:hover{ border-color:var(--ink); }\\n #chladni-plant-builder .stepper .sn{ font-weight:700; font-size:13px; min-width:16px; text-align:center; }\\n #chladni-plant-builder .an-actions{ display:flex; gap:10px; }\\n #chladni-plant-builder .an-actions button{ background:none; border:none; font-size:11px; font-weight:700; letter-spacing:0.5px; color:var(--muted); cursor:pointer; text-decoration:underline; font-family:Arial; }\\n #chladni-plant-builder .an-actions button:hover{ color:var(--ink); }\\n #chladni-plant-builder .an-price{ font-size:12px; color:#555; margin-top:6px; }\\n #chladni-plant-builder .addAssetBtn{ width:100%; margin-top:6px; background:#fff; border:2px dashed #bdbdbd; padding:14px; font-family:Arial; font-size:13px; font-weight:700; letter-spacing:0.5px; cursor:pointer; }\\n #chladni-plant-builder .addAssetBtn:hover{ border-color:var(--ink); }\\n #chladni-plant-builder .treeTotals{ margin-top:16px; padding-top:14px; border-top:1.5px solid var(--ink); }\\n #chladni-plant-builder .treeTotals .tt-row{ display:flex; justify-content:space-between; font-size:13px; margin-bottom:6px; }\\n #chladni-plant-builder .treeTotals .tt-price{ font-size:20px; font-weight:900; letter-spacing:-0.5px; }\\n\\n /* ---- right: centered working card ---- */\\n #chladni-plant-builder .cardOuter{ display:flex; justify-content:center; padding:26px 28px 60px; }\\n #chladni-plant-builder .card{ width:100%; max-width:720px; text-align:left; }\\n #chladni-plant-builder .crumb{ font-size:11px; letter-spacing:2px; text-transform:uppercase; color:var(--muted); font-weight:700; margin-bottom:6px; text-align:left; }\\n #chladni-plant-builder .cardTitle{ font-size:24px; font-weight:900; letter-spacing:-0.6px; margin-bottom:18px; text-align:left; }\\n\\n #chladni-plant-builder .search input{ width:100%; font-family:Arial; font-size:15px; padding:12px 14px; border:2px solid var(--ink); }\\n #chladni-plant-builder .search input:focus{ outline:none; }\\n #chladni-plant-builder .cat{ margin-top:20px; }\\n #chladni-plant-builder .cat h3{ font-size:11px; letter-spacing:2.5px; text-transform:uppercase; font-weight:700; color:var(--muted); margin-bottom:9px; text-align:left; }\\n #chladni-plant-builder .mgrid{ display:grid; grid-template-columns:repeat(2,1fr); gap:8px; }\\n @media (min-width:560px){ #chladni-plant-builder .mgrid{ grid-template-columns:repeat(3,1fr); } }\\n #chladni-plant-builder .mcardbtn{ font-family:Arial; font-size:13px; font-weight:700; color:var(--ink); background:#fff; border:1.5px solid #cfcfcf; padding:12px 10px; cursor:pointer; text-align:left; line-height:1.3; }\\n #chladni-plant-builder .mcardbtn:hover{ border-color:var(--ink); }\\n #chladni-plant-builder .mcardbtn.on{ border-color:var(--ink); background:var(--ink); color:#fff; }\\n\\n #chladni-plant-builder .fld{ margin-bottom:18px; text-align:left; }\\n #chladni-plant-builder .fldLabelRow{ display:flex; align-items:center; gap:6px; margin-bottom:7px; }\\n #chladni-plant-builder .fldLabelRow label{ font-size:11px; letter-spacing:1.5px; text-transform:uppercase; font-weight:700; color:var(--muted); }\\n #chladni-plant-builder .fldReq{ color:#b02020; }\\n #chladni-plant-builder .infoIcon{ width:16px; height:16px; border-radius:50%; border:1.3px solid var(--muted); color:var(--muted); font-size:10.5px; font-weight:900; display:inline-flex; align-items:center; justify-content:center; cursor:pointer; background:#fff; font-family:Arial; line-height:1; flex:none; }\\n #chladni-plant-builder .infoIcon:hover, #chladni-plant-builder .infoIcon:focus-visible{ border-color:var(--ink); color:var(--ink); outline:none; }\\n #chladni-plant-builder .fld input[type=text], #chladni-plant-builder .fld input[type=number], #chladni-plant-builder .fld input[type=email], #chladni-plant-builder .fld select{ width:100%; font-family:Arial; font-size:15px; padding:11px 12px; border:1px solid #cfcfcf; background:#fff; text-align:left; }\\n #chladni-plant-builder .fld input.errField{ border-color:#b02020; }\\n #chladni-plant-builder .fld input:focus, #chladni-plant-builder .fld select:focus{ outline:none; border-color:var(--ink); }\\n #chladni-plant-builder .fldErr{ font-size:12px; color:#b02020; font-weight:700; margin-top:6px; }\\n #chladni-plant-builder .tip{ position:fixed; max-width:min(280px, calc(100vw - 20px)); background:var(--ink); color:#fff; font-size:12.5px; line-height:1.5; padding:10px 28px 10px 12px; z-index:50; box-shadow:0 6px 18px rgba(0,0,0,.25); display:none; text-align:left; }\\n #chladni-plant-builder .tipClose{ position:absolute; top:4px; right:6px; width:22px; height:22px; border:none; background:none; color:#fff; font-size:16px; line-height:1; cursor:pointer; font-family:Arial; }\\n #chladni-plant-builder .tip.on{ display:block; }\\n\\n #chladni-plant-builder .opts{ display:grid; grid-template-columns:repeat(2,1fr); gap:8px; }\\n #chladni-plant-builder .opts.row4{ grid-template-columns:repeat(4,1fr); }\\n #chladni-plant-builder .opts.row2{ grid-template-columns:repeat(2,1fr); }\\n @media (min-width:520px){ #chladni-plant-builder .opts.wide{ grid-template-columns:repeat(3,1fr); } }\\n @media (max-width:640px){ #chladni-plant-builder .opts.row4{ grid-template-columns:repeat(2,1fr); } }\\n #chladni-plant-builder .opt{ font-family:Arial; background:#fff; border:1.5px solid #cfcfcf; padding:11px 9px; cursor:pointer; text-align:center; font-size:12.5px; font-weight:700; }\\n #chladni-plant-builder .opt:hover{ border-color:var(--ink); }\\n #chladni-plant-builder .opt.on{ border-color:var(--ink); background:var(--ink); color:#fff; }\\n #chladni-plant-builder .opt .od{ display:block; font-size:10px; font-weight:400; opacity:0.75; margin-top:3px; }\\n\\n #chladni-plant-builder .toggleRow{ display:flex; align-items:center; gap:10px; cursor:pointer; user-select:none; margin:22px 0 16px; padding:12px 14px; background:var(--panel); }\\n #chladni-plant-builder .toggleRow .tSwitch{ width:36px; height:20px; border-radius:10px; background:#cfcfcf; position:relative; flex:none; }\\n #chladni-plant-builder .toggleRow .tSwitch::after{ content:\\\"\\\"; width:16px; height:16px; border-radius:50%; background:#fff; position:absolute; top:2px; left:2px; transition:left .12s; }\\n #chladni-plant-builder .toggleRow.on .tSwitch{ background:var(--ink); }\\n #chladni-plant-builder .toggleRow.on .tSwitch::after{ left:18px; }\\n #chladni-plant-builder .toggleRow span.tLabel{ font-size:13.5px; font-weight:700; }\\n #chladni-plant-builder .advBlock{ display:none; border-left:2px solid var(--hairline); padding-left:16px; margin-bottom:18px; }\\n #chladni-plant-builder .advBlock.on{ display:block; }\\n\\n #chladni-plant-builder .driverCard{ border:1.5px solid var(--hairline); padding:14px 16px; margin-bottom:12px; }\\n /* v11: the driven-unit \\\"More about this \u003ctype\u003e\\\" card renders as a sibling of .tabPanel, not\\n nested inside it - this margin is what actually creates the visible gap that reads as a\\n separate, detached section rather than a box drawn inside another box. */\\n #chladni-plant-builder .detachedCard{ margin-top:16px; margin-bottom:0; }\\n #chladni-plant-builder .driverCard .dcHead{ display:flex; justify-content:space-between; align-items:center; margin-bottom:10px; }\\n #chladni-plant-builder .driverCard .dcHead b{ font-size:13px; }\\n #chladni-plant-builder .driverCard .dcRemove{ background:none; border:none; color:var(--muted); font-size:11px; font-weight:700; text-decoration:underline; cursor:pointer; font-family:Arial; }\\n #chladni-plant-builder .addDriverBtn, #chladni-plant-builder .addInterBtn{ background:#fff; border:1.5px dashed #bdbdbd; padding:10px 14px; font-family:Arial; font-size:12.5px; font-weight:700; cursor:pointer; margin-bottom:16px; }\\n #chladni-plant-builder .addDriverBtn:hover, #chladni-plant-builder .addInterBtn:hover{ border-color:var(--ink); }\\n\\n #chladni-plant-builder .btn{ display:inline-block; background:var(--ink); color:#fff; border:2px solid var(--ink); font-family:Arial; font-size:13px; font-weight:700; letter-spacing:1.5px; text-transform:uppercase; padding:13px 26px; cursor:pointer; }\\n #chladni-plant-builder .btn:hover{ background:#fff; color:var(--ink); }\\n #chladni-plant-builder .btn.ghost{ background:#fff; color:var(--ink); }\\n #chladni-plant-builder .btn.ghost:hover{ background:var(--ink); color:#fff; }\\n #chladni-plant-builder .btn:disabled{ opacity:0.35; cursor:not-allowed; }\\n #chladni-plant-builder .btn:disabled:hover{ background:var(--ink); color:#fff; }\\n #chladni-plant-builder .btnrow{ display:flex; gap:10px; flex-wrap:wrap; margin-top:22px; justify-content:space-between; }\\n\\n #chladni-plant-builder .sectionHead{ font-size:11px; letter-spacing:2.5px; text-transform:uppercase; font-weight:700; color:var(--muted); margin:24px 0 12px; padding-top:18px; border-top:1px solid var(--hairline); text-align:left; }\\n #chladni-plant-builder .sectionHead:first-child{ border-top:none; padding-top:0; margin-top:0; }\\n\\n #chladni-plant-builder .quoteTable{ width:100%; border-collapse:collapse; margin-top:10px; }\\n #chladni-plant-builder .quoteTable th, #chladni-plant-builder .quoteTable td{ text-align:left; padding:9px 10px; border-bottom:1px solid var(--hairline); font-size:13px; }\\n #chladni-plant-builder .quoteTable th{ font-size:10.5px; letter-spacing:1.5px; text-transform:uppercase; color:var(--muted); font-weight:700; }\\n #chladni-plant-builder .quoteTable td.num{ text-align:right; font-variant-numeric:tabular-nums; }\\n #chladni-plant-builder .reviewBlockHead td{ background:var(--panel); font-size:10.5px; letter-spacing:1.5px; text-transform:uppercase; color:var(--muted); font-weight:700; padding:7px 10px; }\\n #chladni-plant-builder .puckOverrideInp{ width:56px; font-family:Arial; font-size:13px; text-align:right; padding:5px 6px; border:1px solid #cfcfcf; }\\n #chladni-plant-builder .puckOverrideInp:focus{ outline:none; border-color:var(--ink); }\\n #chladni-plant-builder .quoteBig{ font-size:30px; font-weight:900; letter-spacing:-1px; margin-top:14px; text-align:left; }\\n #chladni-plant-builder .hint{ font-size:12.5px; color:var(--muted); margin-top:6px; text-align:left; }\\n #chladni-plant-builder .scopeNote{ border:1.5px solid #cfcfcf; background:var(--panel); padding:12px 13px; font-size:12.5px; line-height:1.5; color:var(--ink); margin:10px 0 16px; text-align:left; }\\n #chladni-plant-builder .scopeNote b{ font-weight:700; }\\n #chladni-plant-builder .checkline{ display:flex; align-items:center; gap:9px; border:1.5px solid #cfcfcf; padding:12px 13px; cursor:pointer; font-size:13px; font-weight:700; background:#fff; margin-bottom:16px; }\\n #chladni-plant-builder .checkline input{ width:16px; height:16px; }\\n #chladni-plant-builder .checkline.on{ border-color:var(--ink); }\\n\\n /* ---- tabbed drivetrain workflow ---- */\\n #chladni-plant-builder .tabbar{ display:flex; gap:0; margin:8px 0 0; border-bottom:2px solid var(--ink); }\\n #chladni-plant-builder .tabbtn{ flex:1; background:var(--panel); border:none; border-bottom:3px solid transparent; font-family:Arial; font-size:11.5px; font-weight:700; letter-spacing:0.5px; text-transform:uppercase; padding:12px 6px; cursor:pointer; color:var(--muted); text-align:center; }\\n #chladni-plant-builder .tabbtn .tn{ display:block; font-size:9px; letter-spacing:1px; opacity:0.7; margin-bottom:3px; }\\n #chladni-plant-builder .tabbtn.active{ background:#fff; color:var(--ink); border-bottom-color:var(--ink); }\\n #chladni-plant-builder .tabbtn.done:not(.active){ color:var(--ink); }\\n #chladni-plant-builder .tabbtn.done .tn::after{ content:\\\" \\\\2713\\\"; }\\n #chladni-plant-builder .tabbtn:disabled{ cursor:not-allowed; opacity:0.4; }\\n /* v14: 5 equal-width tabs plus their block-number sub-label get tight on a phone - shrink text\\n and padding further rather than letting any tab's label collide with its neighbor. */\\n @media (max-width:480px){\\n #chladni-plant-builder .tabbtn{ font-size:9.5px; padding:9px 3px; }\\n #chladni-plant-builder .tabbtn .tn{ font-size:7.5px; }\\n }\\n #chladni-plant-builder .tabPanel{ border:1.5px solid var(--hairline); border-top:none; padding:22px 20px; margin-bottom:0; }\\n @media (max-width:480px){ #chladni-plant-builder .tabPanel{ padding:16px 14px; } }\\n #chladni-plant-builder .lockNote{ font-size:12.5px; color:var(--muted); background:var(--panel); padding:12px 14px; margin:18px 0; text-align:left; }\\n\\n /* ---- plant hierarchy grouping ---- */\\n #chladni-plant-builder .zoneHead{ display:flex; align-items:center; justify-content:space-between; font-size:11px; letter-spacing:1.5px; text-transform:uppercase; font-weight:700; color:var(--ink); margin:16px 0 8px; padding-top:12px; border-top:1px solid var(--hairline); cursor:pointer; user-select:none; }\\n #chladni-plant-builder .zoneHead:first-child{ border-top:none; padding-top:0; margin-top:0; }\\n #chladni-plant-builder .zoneHead:hover{ color:#555; }\\n #chladni-plant-builder .zoneCaret{ font-size:10px; }\\n #chladni-plant-builder .zoneGroup{ padding-left:8px; border-left:1.5px solid var(--hairline); margin-left:2px; }\\n #chladni-plant-builder .treePlantInfo{ margin-bottom:16px; padding-bottom:14px; border-bottom:1.5px solid var(--ink); }\\n #chladni-plant-builder .treePlantInfo .tp-company{ font-weight:900; font-size:15px; line-height:1.3; }\\n #chladni-plant-builder .treePlantInfo .tp-plant{ font-size:12.5px; color:#555; margin-top:2px; }\\n #chladni-plant-builder .treePlantInfo .tp-meta{ font-size:10.5px; color:var(--muted); margin-top:5px; text-transform:uppercase; letter-spacing:0.5px; }\\n #chladni-plant-builder .topbarLink{ color:#fff; text-decoration:underline; cursor:pointer; font-size:12px; background:none; border:none; font-family:Arial; }\\n #chladni-plant-builder .topbarLink:hover{ color:#cfcfcf; }\\n\\n #chladni-plant-builder .bigInput{ font-size:17px !important; padding:15px 14px !important; }\\n\\n /* ---- v10: lead-gated pricing + quote export + shareable link ---- */\\n #chladni-plant-builder .lockedPrice{ font-size:13px; color:var(--muted); font-weight:700; }\\n #chladni-plant-builder .lockLink{ display:block; font-size:11.5px; text-decoration:underline; color:var(--ink); background:none; border:none; cursor:pointer; font-family:Arial; margin-top:4px; padding:0; text-align:left; }\\n #chladni-plant-builder .quoteActions{ display:flex; flex-direction:column; gap:8px; margin-top:14px; }\\n #chladni-plant-builder .quoteActions .btn{ text-align:center; font-size:11.5px; padding:11px 10px; }\\n #chladni-plant-builder .resumeBanner{ background:var(--ink); color:#fff; padding:12px 16px; display:flex; align-items:center; justify-content:space-between; gap:8px 14px; flex-wrap:wrap; font-size:13px; }\\n #chladni-plant-builder .resumeBanner button{ font-family:Arial; font-size:11.5px; font-weight:700; letter-spacing:0.5px; text-transform:uppercase; padding:8px 14px; cursor:pointer; border:1.5px solid #fff; background:none; color:#fff; }\\n #chladni-plant-builder .resumeBanner button:hover{ background:#fff; color:var(--ink); }\\n\\n #chladni-plant-builder .quoteOverlay{ display:none; position:fixed; inset:0; background:rgba(14,14,14,0.6); z-index:200; overflow-y:auto; padding:40px 16px; }\\n #chladni-plant-builder .quoteOverlay.on{ display:block; }\\n #chladni-plant-builder .quoteSheet{ max-width:720px; margin:0 auto; background:#fff; padding:40px 44px; }\\n #chladni-plant-builder .quoteSheet .qTop{ display:flex; justify-content:space-between; align-items:flex-start; gap:16px; margin-bottom:22px; border-bottom:2px solid var(--ink); padding-bottom:16px; }\\n #chladni-plant-builder .quoteSheet .qBrand{ font-size:22px; font-weight:900; letter-spacing:-0.5px; }\\n #chladni-plant-builder .quoteSheet .qMeta{ font-size:12px; color:var(--muted); margin-top:3px; text-align:right; }\\n #chladni-plant-builder .quoteSheet .qPrep{ font-size:13px; line-height:1.7; margin-bottom:18px; }\\n /* v14: the quote table has 6 columns (Asset/Type/Criticality/Qty/Sensors/Monthly) - on a phone\\n width that's more columns than can fit without either scrolling or crushing text unreadably.\\n Wrapping it in a scrollable strip (rather than shrinking font/padding until it's unreadable)\\n keeps every column legible; min-width on the table is what makes the wrapper actually scroll\\n instead of squeezing the table down to the wrapper's width. */\\n #chladni-plant-builder .quoteTableWrap{ overflow-x:auto; -webkit-overflow-scrolling:touch; margin:14px 0; }\\n #chladni-plant-builder .quoteSheet table{ width:100%; min-width:480px; border-collapse:collapse; margin:0; }\\n #chladni-plant-builder .quoteSheet th, #chladni-plant-builder .quoteSheet td{ text-align:left; padding:8px 6px; border-bottom:1px solid var(--hairline); font-size:12.5px; white-space:nowrap; }\\n #chladni-plant-builder .quoteSheet th{ font-size:10px; letter-spacing:1px; text-transform:uppercase; color:var(--muted); }\\n #chladni-plant-builder .quoteSheet td.num, #chladni-plant-builder .quoteSheet th.num{ text-align:right; font-variant-numeric:tabular-nums; }\\n #chladni-plant-builder .quoteActionsRow{ display:flex; gap:10px; margin-top:24px; justify-content:flex-end; flex-wrap:wrap; }\\n @media (max-width:600px){\\n #chladni-plant-builder .quoteOverlay{ padding:24px 8px; }\\n #chladni-plant-builder .quoteSheet{ padding:24px 18px; }\\n #chladni-plant-builder .quoteSheet .qTop{ flex-direction:column; gap:10px; }\\n #chladni-plant-builder .quoteSheet .qMeta{ text-align:left; }\\n #chladni-plant-builder .quoteActionsRow{ justify-content:stretch; }\\n #chladni-plant-builder .quoteActionsRow .btn{ flex:1; text-align:center; }\\n }\\n\\n @media print {\\n #chladni-plant-builder, #chladni-plant-builder *{ visibility:hidden; }\\n #chladni-plant-builder .quoteSheet, #chladni-plant-builder .quoteSheet *{ visibility:visible; }\\n #chladni-plant-builder .quoteOverlay{ position:absolute; inset:auto; left:0; top:0; background:#fff; padding:0; }\\n #chladni-plant-builder .quoteSheet{ box-shadow:none; max-width:100%; }\\n #chladni-plant-builder .quoteActionsRow{ display:none; }\\n }\\n/* Chladni visual refresh - presentation only; asset and sensor architecture is unchanged. */\\n#chladni-plant-builder{\\n --ink:#1A1B1C;\\n --paper:#eaeaea;\\n --panel:#e8e8e8;\\n --muted:#656565;\\n --hairline:#d4d4d4;\\n --surface:#f6f6f6;\\n --field:#fbfbfb;\\n --radius:22px;\\n}\\n#chladni-plant-builder html{ scroll-behavior:smooth; }\\n#chladni-plant-builder{ background:var(--paper); font-family:Arial,\\\"Helvetica Neue\\\",Helvetica,sans-serif; color:var(--ink); -webkit-font-smoothing:antialiased; }\\n#chladni-plant-builder button, #chladni-plant-builder input, #chladni-plant-builder select, #chladni-plant-builder textarea{ font-family:Arial,\\\"Helvetica Neue\\\",Helvetica,sans-serif !important; }\\n#chladni-plant-builder button{ transition:background .16s ease,border-color .16s ease,box-shadow .16s ease,color .16s ease; }\\n#chladni-plant-builder button:focus-visible, #chladni-plant-builder input:focus-visible, #chladni-plant-builder select:focus-visible, #chladni-plant-builder summary:focus-visible, #chladni-plant-builder a:focus-visible{ outline:3px solid #797979 !important; outline-offset:3px; }\\n#chladni-plant-builder button{ touch-action:manipulation; }\\n#chladni-plant-builder input, #chladni-plant-builder select{ accent-color:#1A1B1C; }\\n#chladni-plant-builder ::selection{ background:#d0d0d0; color:#1A1B1C; }\\n#chladni-plant-builder .topbar{ min-height:76px; padding:20px 32px; gap:12px 24px; background:#1A1B1C; border-bottom:1px solid #323232; }\\n#chladni-plant-builder .topbar b{ font-size:15px; font-weight:700; letter-spacing:1.8px; }\\n#chladni-plant-builder .topbar span{ color:#d6d6d6; }\\n#chladni-plant-builder .topbarLink{ color:#eaeaea; text-decoration:none; padding:8px 14px; border:1px solid #4b4b4b; border-radius:999px; font-size:12px; }\\n#chladni-plant-builder .topbarLink:hover{ color:#fff; background:#2f2f2f; }\\n#chladni-plant-builder .appgrid{ grid-template-columns:320px minmax(0,1fr); min-height:calc(100vh - 76px); }\\n#chladni-plant-builder .tree{ padding:28px 20px; background:#e2e2e2; border-right:1px solid #cecece; }\\n#chladni-plant-builder .tree h2{ color:#636363; font-size:10px; letter-spacing:2px; margin-bottom:20px; }\\n#chladni-plant-builder .treePlantInfo{ padding:0 2px 20px; margin-bottom:20px; border-bottom:1px solid #c3c3c3; }\\n#chladni-plant-builder .treePlantInfo .tp-company{ font-size:20px; letter-spacing:-.5px; font-weight:700; overflow-wrap:anywhere; }\\n#chladni-plant-builder .treePlantInfo .tp-plant{ color:#4f4f4f; margin-top:6px; }\\n#chladni-plant-builder .treePlantInfo .tp-meta{ color:#6b6b6b; line-height:1.5; }\\n#chladni-plant-builder .treeEmpty{ color:#636363; padding:24px 8px; }\\n#chladni-plant-builder .zoneHead{ padding-top:16px; margin-top:20px; color:#545454; border-top-color:#c8c8c8; letter-spacing:1px; }\\n#chladni-plant-builder .zoneGroup{ padding-left:9px; border-left:1px solid #c2c2c2; }\\n#chladni-plant-builder .assetNode{ border:1px solid #cecece; border-radius:17px; padding:16px; margin-bottom:10px; background:#f0f0f0; }\\n#chladni-plant-builder .assetNode:hover{ border-color:#949494; box-shadow:0 3px 12px #23232309; }\\n#chladni-plant-builder .assetNode.sel{ border-color:#2a2a2a; background:#f6f6f6; box-shadow:0 0 0 1px #2a2a2a; }\\n#chladni-plant-builder .assetNode .an-name{ font-size:15px; font-weight:700; letter-spacing:-.2px; overflow-wrap:anywhere; }\\n#chladni-plant-builder .assetNode .an-type{ color:#6a6a6a; margin-top:5px; }\\n#chladni-plant-builder .assetNode .an-badges{ margin-top:11px; gap:5px; }\\n#chladni-plant-builder .badge{ border-radius:999px; padding:4px 8px; font-size:9px; letter-spacing:.5px; background:#e2e2e2; color:#4c4c4c; }\\n#chladni-plant-builder .badge.crit-very{ background:#1A1B1C; }\\n#chladni-plant-builder .badge.crit-high{ background:#4f4f4f; }\\n#chladni-plant-builder .badge.legacy{ background:transparent; border-color:#7d7d7d; }\\n#chladni-plant-builder .badge.incomplete{ background:#f2e4de; border-color:#b57c67; color:#8b3d25; }\\n#chladni-plant-builder .an-row{ padding-top:11px; border-top-color:#d6d6d6; }\\n#chladni-plant-builder .stepper{ gap:8px; }\\n#chladni-plant-builder .stepper button{ width:28px; height:28px; border-radius:50%; color:#242424; border:1px solid #c4c4c4; background:#f7f7f7; }\\n#chladni-plant-builder .stepper button:hover{ background:#e4e4e4; }\\n#chladni-plant-builder .an-actions{ gap:9px; }\\n#chladni-plant-builder .an-actions button{ color:#696969; letter-spacing:0; text-decoration:none; padding:5px 0; }\\n#chladni-plant-builder .an-price{ color:#5b5b5b; margin-top:10px; }\\n#chladni-plant-builder .addAssetBtn{ border:1px dashed #939393; border-radius:999px; background:#ebebeb; min-height:48px; color:#2b2b2b; font-size:13px; letter-spacing:0; }\\n#chladni-plant-builder .addAssetBtn:hover{ background:#f3f3f3; border-color:#3b3b3b; }\\n#chladni-plant-builder .treeTotals{ padding-top:20px; margin-top:22px; border-top:1px solid #b5b5b5; }\\n#chladni-plant-builder .treeTotals .tt-row{ gap:12px; color:#4c4c4c; margin-bottom:9px; }\\n#chladni-plant-builder .treeTotals .tt-price{ color:#1A1B1C; font-size:26px; letter-spacing:-1px; }\\n#chladni-plant-builder .lockedPrice{ color:#626262; }\\n#chladni-plant-builder .tree .hint{ color:#696969; font-size:11.5px; }\\n#chladni-plant-builder .cardOuter{ padding:38px 36px 72px; min-width:0; }\\n#chladni-plant-builder .card{ max-width:820px; padding:32px; background:var(--surface); border:1px solid #dadada; border-radius:26px; box-shadow:0 12px 38px #2d2d2d06; align-self:flex-start; }\\n#chladni-plant-builder #plantScreenOuter{ padding-top:48px; }\\n#chladni-plant-builder #plantCard{ max-width:850px; padding:44px 48px; }\\n#chladni-plant-builder .crumb{ color:#717171; font-size:10px; letter-spacing:2px; margin-bottom:12px; }\\n#chladni-plant-builder .cardTitle{ color:#1A1B1C; font-size:31px; line-height:1.15; letter-spacing:-1.2px; font-weight:700; margin-bottom:20px; }\\n#chladni-plant-builder #plantCard .cardTitle{ font-size:44px; letter-spacing:-1.9px; line-height:1.08; margin-bottom:18px; }\\n#chladni-plant-builder #plantCard \u003e .hint:first-of-type{ color:#616161; font-size:15px; line-height:1.65; max-width:670px; }\\n#chladni-plant-builder .hint{ color:var(--muted); line-height:1.6; }\\n#chladni-plant-builder .scopeNote{ background:#ebebeb; border:1px solid #dadada; border-radius:16px; padding:17px 19px; margin:22px 0 26px; color:#575757; line-height:1.65; font-size:12.5px; }\\n#chladni-plant-builder .scopeNote b{ color:#3a3a3a; }\\n#chladni-plant-builder .fld{ margin-bottom:22px; }\\n#chladni-plant-builder .fldLabelRow{ gap:7px; margin-bottom:9px; }\\n#chladni-plant-builder .fldLabelRow label{ font-size:11px; letter-spacing:1px; color:#575757; }\\n#chladni-plant-builder .fldReq{ color:#955e47; }\\n#chladni-plant-builder .fld input[type=text], #chladni-plant-builder .fld input[type=number], #chladni-plant-builder .fld input[type=email], #chladni-plant-builder .fld select, #chladni-plant-builder .search input{ width:100%; min-height:48px; font-size:15px; border:1px solid #cecece; background:var(--field); border-radius:12px; color:#252525; padding:13px 15px; box-shadow:0 1px 2px #1e1e1e03; }\\n#chladni-plant-builder .fld input::placeholder, #chladni-plant-builder .search input::placeholder{ color:#8b8b8b; opacity:1; }\\n#chladni-plant-builder .fld input:focus, #chladni-plant-builder .fld select:focus, #chladni-plant-builder .search input:focus{ border-color:#636363; box-shadow:0 0 0 3px #73737314; outline:none; }\\n#chladni-plant-builder .fld input.errField{ border-color:#ab684c; }\\n#chladni-plant-builder .bigInput{ font-size:16px !important; padding:14px 15px !important; }\\n#chladni-plant-builder .infoIcon{ background:transparent; color:#818181; border-color:#a0a0a0; }\\n#chladni-plant-builder .infoIcon:hover{ color:#333333; border-color:#333333; }\\n#chladni-plant-builder .tip{ border-radius:13px; padding:13px 15px; background:#1A1B1C; box-shadow:0 8px 28px #1f1f1f30; }\\n#chladni-plant-builder .cat{ margin-top:26px; }\\n#chladni-plant-builder .cat h3{ color:#727272; font-size:10px; letter-spacing:1.8px; margin-bottom:12px; }\\n#chladni-plant-builder .mgrid, #chladni-plant-builder .opts{ gap:9px; }\\n#chladni-plant-builder .mcardbtn, #chladni-plant-builder .opt{ min-height:49px; border:1px solid #d1d1d1; border-radius:12px; background:#efefef; padding:13px 12px; color:#323232; font-weight:600; }\\n#chladni-plant-builder .mcardbtn:hover, #chladni-plant-builder .opt:hover{ border-color:#848484; background:#e8e8e8; }\\n#chladni-plant-builder .mcardbtn.on, #chladni-plant-builder .opt.on{ background:#262626; border-color:#262626; color:#fff; box-shadow:0 3px 8px #20202014; }\\n#chladni-plant-builder .opt .od{ font-size:11px; line-height:1.4; opacity:.8; }\\n#chladni-plant-builder .toggleRow{ border-radius:13px; background:#eaeaea; padding:15px; margin:22px 0 18px; }\\n#chladni-plant-builder .toggleRow .tSwitch{ background:#c7c7c7; }\\n#chladni-plant-builder .toggleRow.on .tSwitch{ background:#262626; }\\n#chladni-plant-builder .toggleRow span.tLabel{ color:#414141; font-size:13px; }\\n#chladni-plant-builder .advBlock{ border-left-color:#d4d4d4; padding-left:18px; }\\n#chladni-plant-builder .driverCard{ border:1px solid #d8d8d8; background:#f1f1f1; border-radius:16px; padding:20px; margin-bottom:16px; }\\n#chladni-plant-builder .driverCard .dcHead{ margin-bottom:18px; }\\n#chladni-plant-builder .driverCard .dcHead b{ font-size:14px; letter-spacing:-.2px; }\\n#chladni-plant-builder .driverCard .dcRemove{ color:#737373; text-decoration:none; padding:5px 0 5px 10px; }\\n#chladni-plant-builder .detachedCard{ margin-top:20px; }\\n#chladni-plant-builder .addDriverBtn, #chladni-plant-builder .addInterBtn{ border:1px dashed #b0b0b0; background:#f1f1f1; border-radius:999px; padding:12px 19px; min-height:44px; color:#434343; }\\n#chladni-plant-builder .addDriverBtn:hover, #chladni-plant-builder .addInterBtn:hover{ background:#e6e6e6; border-color:#707070; }\\n#chladni-plant-builder .btn{ min-height:48px; padding:13px 24px; border:1px solid #1A1B1C; border-radius:999px; background:#1A1B1C; color:#fff; font-size:13px; font-weight:700; line-height:1.45; text-transform:none; letter-spacing:0; box-shadow:0 4px 10px #2222220c; }\\n#chladni-plant-builder .btn:hover{ background:#353535; border-color:#353535; color:#fff; box-shadow:0 4px 12px #2222221a; }\\n#chladni-plant-builder .btn.ghost{ background:transparent; color:#272727; border-color:#bebebe; box-shadow:none; }\\n#chladni-plant-builder .btn.ghost:hover{ background:#e5e5e5; border-color:#979797; color:#272727; }\\n#chladni-plant-builder .btn:disabled{ opacity:.36; box-shadow:none; }\\n#chladni-plant-builder .btn:disabled:hover{ background:#1A1B1C; color:#fff; }\\n#chladni-plant-builder .btnrow{ gap:12px; margin-top:28px; }\\n#chladni-plant-builder .quoteActions{ gap:10px; margin-top:18px; }\\n#chladni-plant-builder .quoteActions .btn{ font-size:12.5px; padding:12px 15px; }\\n#chladni-plant-builder #plantCard .quoteActions{ border-top:1px solid #dbdbdb; padding-top:20px; }\\n#chladni-plant-builder #plantCard .quoteActions .btn{ align-self:flex-start; min-width:145px; }\\n#chladni-plant-builder .sectionHead{ color:#727272; font-size:10px; letter-spacing:1.8px; margin:28px 0 16px; padding-top:24px; border-top-color:#d9d9d9; }\\n#chladni-plant-builder .quoteTable th, #chladni-plant-builder .quoteTable td{ border-color:#dbdbdb; padding:12px 9px; }\\n#chladni-plant-builder .quoteTable th{ color:#767676; letter-spacing:1px; font-size:10px; }\\n#chladni-plant-builder .reviewBlockHead td{ background:#ebebeb; color:#656565; }\\n#chladni-plant-builder .puckOverrideInp{ width:64px; min-height:35px; border-radius:8px; border-color:#c2c2c2; padding:6px 8px; background:#fafafa; }\\n#chladni-plant-builder .quoteBig{ font-size:36px; font-weight:700; margin-top:20px; }\\n#chladni-plant-builder .checkline{ border:1px solid #cccccc; border-radius:13px; padding:15px; background:#f3f3f3; }\\n#chladni-plant-builder .checkline.on{ border-color:#878787; background:#ececec; }\\n#chladni-plant-builder .checkline input{ flex-shrink:0; }\\n#chladni-plant-builder .tabbar{ gap:4px; border:1px solid #d4d4d4; border-bottom:none; padding:6px; margin-top:20px; border-radius:16px 16px 0 0; background:#e7e7e7; }\\n#chladni-plant-builder .tabbtn{ min-width:0; min-height:60px; background:transparent; border:0; border-radius:10px; color:#7d7d7d; padding:10px 6px; font-size:10px; letter-spacing:.25px; line-height:1.4; }\\n#chladni-plant-builder .tabbtn .tn{ font-size:8px; letter-spacing:1px; margin-bottom:4px; }\\n#chladni-plant-builder .tabbtn.active{ border:0; background:#272727; color:#fff; box-shadow:0 2px 6px #25252520; }\\n#chladni-plant-builder .tabbtn.done:not(.active){ color:#4d4d4d; background:#f2f2f2; }\\n#chladni-plant-builder .tabbtn:disabled{ opacity:.42; }\\n#chladni-plant-builder .tabPanel{ border:1px solid #d4d4d4; border-top:0; border-radius:0 0 18px 18px; padding:26px 22px; background:#f7f7f7; }\\n#chladni-plant-builder .lockNote{ border-radius:12px; padding:15px 17px; background:#ebebeb; color:#6e6e6e; }\\n#chladni-plant-builder .resumeBanner{ padding:16px 32px; background:#dbdbdb; border-bottom:1px solid #c9c9c9; color:#414141; }\\n#chladni-plant-builder .resumeBanner button{ min-height:38px; border-radius:999px; border:1px solid #949494; color:#454545; background:transparent; text-transform:none; letter-spacing:0; }\\n#chladni-plant-builder .resumeBanner button:hover{ color:#333333; background:#ececec; border-color:#656565; }\\n#chladni-plant-builder .lockLink{ color:#545454; }\\n#chladni-plant-builder .quoteOverlay{ padding:36px 24px; background:rgba(20,20,20,.75); backdrop-filter:blur(10px); }\\n#chladni-plant-builder .quoteSheet{ position:relative; max-width:1160px; background:#efefef; border:1px solid #dadada; border-radius:28px; padding:34px; box-shadow:0 24px 90px #0a0a0a40; }\\n#chladni-plant-builder .quoteSheet .qTop{ align-items:center; margin-bottom:30px; padding-bottom:24px; border-bottom:1px solid #d2d2d2; }\\n#chladni-plant-builder .quoteSheet .qBrand{ color:#1A1B1C; font-size:24px; font-weight:700; letter-spacing:1.5px; }\\n#chladni-plant-builder .quoteSheet .qMeta{ color:#727272; font-size:11px; line-height:1.6; }\\n#chladni-plant-builder .qClose{ margin-left:auto; flex-shrink:0; min-height:40px; min-width:40px; padding:9px 14px; border:1px solid #c3c3c3; border-radius:999px; background:transparent; color:#4d4d4d; cursor:pointer; }\\n#chladni-plant-builder .qClose:hover{ background:#e4e4e4; border-color:#949494; }\\n#chladni-plant-builder .qHeading{ margin:0 0 28px; }\\n#chladni-plant-builder .qEyebrow{ color:#797979; font-size:10px; font-weight:700; text-transform:uppercase; letter-spacing:2px; margin-bottom:9px; }\\n#chladni-plant-builder .qTitle{ color:#252525; font-size:40px; line-height:1.12; letter-spacing:-1.5px; font-weight:700; overflow-wrap:anywhere; }\\n#chladni-plant-builder .qSubtitle{ color:#6d6d6d; font-size:14px; margin-top:12px; line-height:1.6; }\\n#chladni-plant-builder .qHero{ background:#1A1B1C; color:#fbfbfb; border-radius:21px; padding:25px 26px; margin-bottom:22px; }\\n#chladni-plant-builder .qHero .qEyebrow{ color:#bebebe; }\\n#chladni-plant-builder .qAmount{ display:inline-block; color:inherit; font-size:48px; font-weight:700; line-height:1.1; letter-spacing:-2px; font-variant-numeric:tabular-nums; }\\n#chladni-plant-builder .qPeriod{ font-size:13px; color:#b7b7b7; font-weight:400; margin:9px 0 0; letter-spacing:0; line-height:1.5; }\\n#chladni-plant-builder .qHero .hint, #chladni-plant-builder .qHero .qSubtitle{ color:#c7c7c7; }\\n#chladni-plant-builder .qStats{ display:grid; grid-template-columns:repeat(3,minmax(0,1fr)); gap:10px; margin:20px 0 26px; }\\n#chladni-plant-builder .qStat{ padding:17px 18px; background:#e6e6e6; border:1px solid #d5d5d5; border-radius:15px; color:#727272; font-size:12px; }\\n#chladni-plant-builder .qStat b, #chladni-plant-builder .qStat strong{ display:block; color:#363636; font-size:25px; font-weight:700; letter-spacing:-.75px; margin-bottom:2px; }\\n#chladni-plant-builder .qGrid{ display:grid; grid-template-columns:minmax(0,1fr) 320px; gap:24px; align-items:start; }\\n#chladni-plant-builder .qMain, #chladni-plant-builder .qSummary{ min-width:0; }\\n#chladni-plant-builder .qSummary{ position:sticky; top:12px; border:1px solid #d2d2d2; background:#f6f6f6; border-radius:22px; padding:22px; }\\n#chladni-plant-builder .qSummary .qHero{ margin:-10px -10px 22px; padding:24px 20px; border-radius:15px; }\\n#chladni-plant-builder .qSummary .qAmount{ font-size:43px; }\\n#chladni-plant-builder .qSection{ margin-bottom:22px; }\\n#chladni-plant-builder .qSection \u003e h2, #chladni-plant-builder .qSection \u003e h3, #chladni-plant-builder .qMain \u003e h2, #chladni-plant-builder .qSummary \u003e h2, #chladni-plant-builder .qSummary \u003e h3{ color:#464646; font-size:15px; letter-spacing:-.2px; line-height:1.4; margin:0 0 14px; }\\n#chladni-plant-builder .qAsset{ border:1px solid #d7d7d7; border-radius:20px; padding:22px; margin:0 0 16px; background:#f9f9f9; break-inside:avoid; }\\n#chladni-plant-builder .qAssetHead{ display:flex; justify-content:space-between; align-items:flex-start; gap:16px; margin-bottom:8px; }\\n#chladni-plant-builder .qAssetHead h2, #chladni-plant-builder .qAssetHead h3, #chladni-plant-builder .qAssetHead h4, #chladni-plant-builder .qAssetHead b, #chladni-plant-builder .qAssetHead strong{ font-size:17px; line-height:1.35; font-weight:700; letter-spacing:-.35px; overflow-wrap:anywhere; }\\n#chladni-plant-builder .qAssetPrice{ flex-shrink:0; font-size:18px; line-height:1.4; font-weight:700; letter-spacing:-.4px; color:#353535; text-align:right; font-variant-numeric:tabular-nums; }\\n#chladni-plant-builder .qAssetPrice small{ display:block; font-size:10px; font-weight:400; color:#8a8a8a; letter-spacing:0; }\\n#chladni-plant-builder .qAssetMeta{ color:#767676; font-size:12px; line-height:1.7; margin-bottom:16px; overflow-wrap:anywhere; }\\n#chladni-plant-builder .qBreakdown{ padding:14px 16px; background:#efefef; border-radius:12px; margin:14px 0 0; }\\n#chladni-plant-builder .qPriceRow{ display:flex; justify-content:space-between; gap:18px; padding:9px 0; color:#696969; font-size:12px; line-height:1.6; }\\n#chladni-plant-builder .qPriceRow \u003e :last-child{ flex-shrink:0; text-align:right; font-variant-numeric:tabular-nums; }\\n#chladni-plant-builder .qPriceRow strong, #chladni-plant-builder .qPriceRow b{ color:#404040; font-weight:700; }\\n#chladni-plant-builder .qTotalRow{ display:flex; justify-content:space-between; align-items:baseline; gap:18px; color:#343434; font-size:15px; font-weight:700; border-top:1px solid #cccccc; padding-top:17px; margin-top:10px; }\\n#chladni-plant-builder .qTotalRow \u003e :last-child{ font-size:23px; letter-spacing:-.7px; font-variant-numeric:tabular-nums; }\\n#chladni-plant-builder .qDetails{ border-top:1px solid #dcdcdc; margin-top:17px; padding-top:16px; color:#5e5e5e; }\\n#chladni-plant-builder .qDetails \u003e summary{ cursor:pointer; font-size:12px; font-weight:700; line-height:1.5; }\\n#chladni-plant-builder .qDetails \u003e summary:hover{ color:#353535; }\\n#chladni-plant-builder .qDetails[open] \u003e summary{ margin-bottom:16px; }\\n#chladni-plant-builder .qChain{ padding:12px 14px; background:#ededed; border-radius:11px; margin:10px 0 15px; font-size:11.5px; line-height:1.8; overflow-wrap:anywhere; }\\n#chladni-plant-builder .quoteSheet .qSensorTable{ min-width:0; table-layout:fixed; width:100%; }\\n#chladni-plant-builder .quoteSheet .qSensorTable th, #chladni-plant-builder .quoteSheet .qSensorTable td{ white-space:normal; padding:10px 7px; font-size:11.5px; line-height:1.5; color:#686868; vertical-align:top; overflow-wrap:anywhere; border-bottom-color:#e0e0e0; }\\n#chladni-plant-builder .quoteSheet .qSensorTable th{ font-size:9px; color:#838383; letter-spacing:.8px; }\\n#chladni-plant-builder .quoteSheet .qSensorTable th.num, #chladni-plant-builder .quoteSheet .qSensorTable td.num{ width:72px; color:#464646; }\\n#chladni-plant-builder .quoteSheet .qSensorTable tr:last-child td{ border-bottom:0; }\\n#chladni-plant-builder .qContact{ padding:20px 22px; background:#eaeaea; border:1px solid #d7d7d7; border-radius:17px; color:#686868; font-size:12px; line-height:1.8; margin:18px 0; overflow-wrap:anywhere; }\\n#chladni-plant-builder .qContact b, #chladni-plant-builder .qContact strong{ color:#4a4a4a; }\\n#chladni-plant-builder .qContact a{ color:inherit; text-underline-offset:3px; }\\n#chladni-plant-builder .qFooter{ margin-top:26px; padding-top:20px; border-top:1px solid #d8d8d8; color:#8a8a8a; font-size:11px; line-height:1.7; }\\n#chladni-plant-builder .qShareNotice{ border-radius:12px; padding:13px 15px; background:#ececec; color:#656565; margin-top:14px; font-size:11.5px; line-height:1.65; overflow-wrap:anywhere; }\\n#chladni-plant-builder .quoteActionsRow{ display:flex; flex-direction:column; gap:10px; margin-top:22px; }\\n#chladni-plant-builder .quoteActionsRow .btn{ width:100%; text-align:center; text-decoration:none; flex:auto; }\\n#chladni-plant-builder .actionStatus{ color:#606060; font-size:12px; line-height:1.6; margin-top:12px; overflow-wrap:anywhere; }\\n#chladni-plant-builder .actionStatus:empty{ display:none; }\\n\\n@media (min-width:1500px) { #chladni-plant-builder .appgrid{ grid-template-columns:350px minmax(0,1fr); } #chladni-plant-builder .cardOuter{ padding-top:48px; } }\\n@media (max-width:1080px) { #chladni-plant-builder .appgrid{ grid-template-columns:285px minmax(0,1fr); } #chladni-plant-builder .tree{ padding:24px 16px; } #chladni-plant-builder .cardOuter{ padding:28px 22px 56px; } #chladni-plant-builder .card{ padding:26px; } #chladni-plant-builder .qGrid{ grid-template-columns:minmax(0,1fr) 290px; gap:18px; } #chladni-plant-builder .quoteSheet{ padding:28px; } }\\n@media (max-width:880px) { #chladni-plant-builder .appgrid{ grid-template-columns:1fr; } #chladni-plant-builder .tree{ border-right:0; border-bottom:1px solid #cacaca; padding:24px; } #chladni-plant-builder .cardOuter{ padding:26px 24px 52px; } #chladni-plant-builder .card{ max-width:100%; } #chladni-plant-builder .qGrid{ grid-template-columns:1fr; } #chladni-plant-builder .qSummary{ position:static; } #chladni-plant-builder .qSummary .qHero{ margin:0 0 18px; } #chladni-plant-builder .quoteActionsRow{ flex-direction:row; } #chladni-plant-builder .quoteActionsRow .btn{ flex:1; width:auto; } }\\n@media (max-width:600px) { #chladni-plant-builder .topbar{ min-height:68px; padding:20px; } #chladni-plant-builder .topbar b{ font-size:12px; letter-spacing:1.2px; } #chladni-plant-builder .topbarLink{ padding:6px 10px; } #chladni-plant-builder #plantScreenOuter{ padding-top:22px; } #chladni-plant-builder .cardOuter{ padding:20px 14px 40px; } #chladni-plant-builder .card, #chladni-plant-builder #plantCard{ padding:26px 20px; border-radius:22px; } #chladni-plant-builder #plantCard .cardTitle{ font-size:35px; letter-spacing:-1.4px; } #chladni-plant-builder .cardTitle{ font-size:28px; letter-spacing:-1px; } #chladni-plant-builder .tree{ padding:23px 18px; } #chladni-plant-builder .tabbar{ padding:5px; gap:3px; } #chladni-plant-builder .tabbtn{ min-height:58px; font-size:9px; padding:9px 3px; letter-spacing:0; } #chladni-plant-builder .tabbtn .tn{ font-size:7px; letter-spacing:.6px; } #chladni-plant-builder .tabPanel{ padding:22px 15px; } #chladni-plant-builder .driverCard{ padding:16px 14px; } #chladni-plant-builder .scopeNote{ padding:15px; font-size:12px; } #chladni-plant-builder .fldLabelRow label{ letter-spacing:.6px; } #chladni-plant-builder .quoteOverlay{ padding:14px 10px; } #chladni-plant-builder .quoteSheet{ padding:23px 18px; border-radius:22px; } #chladni-plant-builder .quoteSheet .qTop{ flex-wrap:wrap; flex-direction:row; gap:14px; margin-bottom:25px; padding-bottom:19px; } #chladni-plant-builder .quoteSheet .qMeta{ text-align:left; font-size:10.5px; } #chladni-plant-builder .quoteSheet .qBrand{ font-size:21px; } #chladni-plant-builder .qTitle{ font-size:32px; letter-spacing:-1.2px; } #chladni-plant-builder .qStats{ gap:7px; margin:18px 0 23px; } #chladni-plant-builder .qStat{ padding:13px 11px; font-size:10px; border-radius:12px; } #chladni-plant-builder .qStat b, #chladni-plant-builder .qStat strong{ font-size:23px; } #chladni-plant-builder .qAsset{ padding:19px 15px; border-radius:17px; } #chladni-plant-builder .qAssetHead{ gap:10px; } #chladni-plant-builder .qAssetHead h2, #chladni-plant-builder .qAssetHead h3, #chladni-plant-builder .qAssetHead h4, #chladni-plant-builder .qAssetHead b, #chladni-plant-builder .qAssetHead strong{ font-size:16px; } #chladni-plant-builder .qAssetPrice{ font-size:16px; } #chladni-plant-builder .qSummary{ padding:20px; } #chladni-plant-builder .qHero{ padding:23px 20px; } #chladni-plant-builder .qContact{ padding:17px; } #chladni-plant-builder .quoteActionsRow{ flex-direction:column; } #chladni-plant-builder .quoteActionsRow .btn{ width:100%; } #chladni-plant-builder .resumeBanner{ padding:14px 20px; } }\\n@media (max-width:380px) { #chladni-plant-builder .card, #chladni-plant-builder #plantCard{ padding:23px 16px; } #chladni-plant-builder #plantCard .cardTitle{ font-size:30px; } #chladni-plant-builder .tabbtn{ font-size:8px; } #chladni-plant-builder .tabbtn .tn{ font-size:6.5px; } #chladni-plant-builder .qTitle{ font-size:29px; } #chladni-plant-builder .qAssetHead{ flex-direction:column; } #chladni-plant-builder .qAssetPrice{ text-align:left; } }\\n@media (prefers-reduced-motion:reduce) { #chladni-plant-builder html{ scroll-behavior:auto; } #chladni-plant-builder, #chladni-plant-builder *, #chladni-plant-builder *::before, #chladni-plant-builder *::after{ transition:none !important; animation:none !important; } }\\n@media print {\\n #chladni-plant-builder @page{ size:A4; margin:14mm; }\\n #chladni-plant-builder html, #chladni-plant-builder{ background:#fff !important; }\\n #chladni-plant-builder \u003e .topbar, #chladni-plant-builder \u003e #resumeBanner, #chladni-plant-builder \u003e #plantScreenOuter, #chladni-plant-builder \u003e #appgrid, #chladni-plant-builder \u003e .tip{ display:none !important; }\\n #chladni-plant-builder .quoteOverlay.on{ display:block !important; position:static !important; width:auto; padding:0; margin:0; overflow:visible; background:#fff; backdrop-filter:none; }\\n #chladni-plant-builder .quoteSheet{ max-width:none; width:100%; padding:0; border:0; border-radius:0; box-shadow:none; background:#fff; }\\n #chladni-plant-builder .quoteSheet *{ -webkit-print-color-adjust:exact; print-color-adjust:exact; }\\n #chladni-plant-builder .qGrid{ display:block; }\\n #chladni-plant-builder .qSummary{ position:static; margin:18px 0 0; background:#f5f5f5; break-inside:avoid; }\\n #chladni-plant-builder .qAsset, #chladni-plant-builder .qContact, #chladni-plant-builder .qHero, #chladni-plant-builder .qStats{ break-inside:avoid; }\\n #chladni-plant-builder .qTitle{ font-size:30px; }\\n #chladni-plant-builder .qAsset{ padding:16px; }\\n #chladni-plant-builder .qHero{ background:#e6e6e6 !important; color:#111 !important; }\\n #chladni-plant-builder .qHero .qEyebrow, #chladni-plant-builder .qHero .qPeriod, #chladni-plant-builder .qHero .hint{ color:#555 !important; }\\n #chladni-plant-builder .qClose, #chladni-plant-builder .quoteActionsRow, #chladni-plant-builder .qShareNotice, #chladni-plant-builder .actionStatus{ display:none !important; }\\n #chladni-plant-builder .qDetails{ display:block; }\\n #chladni-plant-builder .quoteTableWrap{ overflow:visible; }\\n #chladni-plant-builder .quoteSheet table{ min-width:0; }\\n #chladni-plant-builder .qFooter{ color:#555; }\\n}\\n\\n/* Quote details and sharing controls. */\\n#chladni-plant-builder .topbar b{display:flex;align-items:center;gap:20px;flex-wrap:wrap}\\n#chladni-plant-builder .brandLogo{display:block;width:188px;max-width:100%;height:auto;flex-shrink:0;mix-blend-mode:multiply}#chladni-plant-builder .brandLogo.onDark{filter:invert(1);mix-blend-mode:screen}#chladni-plant-builder .qBrand .brandLogo{width:196px}#chladni-plant-builder .qBrand{flex-shrink:0}\\n#chladni-plant-builder .brandDivider{height:20px;width:1px;background:#626262}#chladni-plant-builder .topbar .brandProduct{font-size:12px;letter-spacing:.3px;color:#bcbcbc;font-weight:400}\\n#chladni-plant-builder .qBrand{font-style:italic}#chladni-plant-builder .qPeriod{display:block}#chladni-plant-builder .qAssetPrice span{display:block;font-size:10px;font-weight:400;letter-spacing:0;color:#707070}\\n#chladni-plant-builder .qTextButton{background:transparent;border:0;color:#585858;text-decoration:underline;text-underline-offset:4px;cursor:pointer;display:block;margin:16px auto 0;font-size:12px}\\n#chladni-plant-builder .qSharePanel{margin-top:25px;border:1px solid #cecece;background:#f8f8f8;border-radius:20px;padding:24px}#chladni-plant-builder .qSharePanel[hidden]{display:none}\\n#chladni-plant-builder .qSharePanel h3{font-size:20px;letter-spacing:-.5px}#chladni-plant-builder .qSharePanel .btn{margin:14px 8px 0 0}\\n#chladni-plant-builder .qSharePanel textarea{display:block;width:100%;padding:13px;border:1px solid #bdbdbd;border-radius:12px;margin-top:16px;resize:vertical;font-size:12px}\\n#chladni-plant-builder .reviewPricing{background:#e7e7e7;border:1px solid #d1d1d1;border-radius:18px;padding:22px;margin-top:24px}#chladni-plant-builder .reviewPricing .quoteBig{margin:4px 0 14px}\\n#chladni-plant-builder .qSensorTable caption{text-align:left;font-size:11px;color:#626262;padding-bottom:8px}#chladni-plant-builder .qSensorTable th:last-child{width:72px;text-align:right}\\n#chladni-plant-builder .qContact h2.qSection{font-size:14px;margin-bottom:10px}#chladni-plant-builder .qPriceRow{color:#5f5f5f}#chladni-plant-builder .qFooter{color:#626262}\\n#chladni-plant-builder .qMeta{margin-left:auto}#chladni-plant-builder .qClose{margin-left:8px}#chladni-plant-builder .qSubtitle{overflow-wrap:anywhere}\\n@media(max-width:880px){#chladni-plant-builder .qSummary{grid-row:1}#chladni-plant-builder .qGrid{gap:24px}#chladni-plant-builder .qSummary .qHero{max-width:none}}\\n@media(max-width:600px){#chladni-plant-builder .qMeta{order:3;width:100%;margin:0}#chladni-plant-builder .qClose{margin-left:auto}#chladni-plant-builder .qSharePanel{padding:18px}#chladni-plant-builder .qSharePanel .btn{width:100%;margin-right:0}#chladni-plant-builder .topbar b{gap:14px}}\\n@media print{#chladni-plant-builder .qTextButton, #chladni-plant-builder .qSharePanel{display:none!important}#chladni-plant-builder{overflow:visible!important}#chladni-plant-builder .qGrid{display:flex;flex-direction:column}#chladni-plant-builder .qMain{order:2;width:100%}#chladni-plant-builder .qSummary{order:1;width:100%;margin:0 0 20px}#chladni-plant-builder details::details-content{display:block!important;content-visibility:visible!important}}\\n\\n/* Brighter Chladni theme: white surfaces, black typography and green prices. */\\n#chladni-plant-builder{\\n --ink:#1A1B1C;\\n --paper:#ffffff;\\n --panel:#f8f8f8;\\n --muted:#666666;\\n --hairline:#e4e4e4;\\n --surface:#ffffff;\\n --field:#ffffff;\\n --price:#117a4e;\\n --price-soft:#eef9f2;\\n}\\n#chladni-plant-builder{ background:#fff; color:#1A1B1C; font-size:16px; line-height:1.55; }\\n#chladni-plant-builder button, #chladni-plant-builder input, #chladni-plant-builder select, #chladni-plant-builder textarea{ font-size:inherit; }\\n#chladni-plant-builder .topbar{ background:#fff; color:#1A1B1C; border-bottom:1px solid #e6e6e6; }\\n#chladni-plant-builder .topbar span, #chladni-plant-builder .topbar .brandProduct{ color:#5a5a5a; font-size:14px; }\\n#chladni-plant-builder .brandLogo.onDark{ filter:none; mix-blend-mode:multiply; }\\n#chladni-plant-builder .brandDivider{ background:#dadada; }\\n#chladni-plant-builder .topbarLink{ color:#1A1B1C; background:#fff; border-color:#dbdbdb; font-size:14px; }\\n#chladni-plant-builder .topbarLink:hover{ color:#1A1B1C; background:#f5f5f5; }\\n#chladni-plant-builder .tree{ background:#fbfbfb; border-color:#e3e3e3; }\\n#chladni-plant-builder .tree h2, #chladni-plant-builder .treePlantInfo .tp-meta, #chladni-plant-builder .treeEmpty, #chladni-plant-builder .zoneHead, #chladni-plant-builder .tree .hint, #chladni-plant-builder .lockedPrice{ color:#696969; }\\n#chladni-plant-builder .tree h2, #chladni-plant-builder .treePlantInfo .tp-meta, #chladni-plant-builder .zoneHead{ font-size:12px; letter-spacing:1px; }\\n#chladni-plant-builder .treePlantInfo{ border-color:#e1e1e1; }\\n#chladni-plant-builder .treePlantInfo .tp-company{ color:#1A1B1C; font-size:22px; }\\n#chladni-plant-builder .treePlantInfo .tp-plant{ color:#545454; font-size:14px; }\\n#chladni-plant-builder .treeEmpty{ font-size:14px; }\\n#chladni-plant-builder .zoneGroup{ border-color:#dcdcdc; }\\n#chladni-plant-builder .assetNode{ background:#fff; border:1px solid #e1e1e1; }\\n#chladni-plant-builder .assetNode:hover{ border-color:#b1b1b1; box-shadow:0 3px 12px #23232305; }\\n#chladni-plant-builder .assetNode.sel{ background:#fff; border-color:#1A1B1C; box-shadow:0 0 0 1px #1A1B1C; }\\n#chladni-plant-builder .assetNode .an-name{ font-size:16px; color:#1A1B1C; }\\n#chladni-plant-builder .assetNode .an-type{ font-size:13px; color:#6f6f6f; }\\n#chladni-plant-builder .badge{ font-size:11px; padding:4px 9px; letter-spacing:.3px; background:#f2f2f2; color:#606060; }\\n#chladni-plant-builder .badge.crit-very{ background:#1A1B1C; color:#fff; }\\n#chladni-plant-builder .badge.crit-high{ background:#414141; color:#fff; }\\n#chladni-plant-builder .badge.legacy{ border-color:#929292; color:#5d5d5d; background:#fff; }\\n#chladni-plant-builder .badge.incomplete{ background:#fff4ee; color:#925637; border-color:#dcb49b; }\\n#chladni-plant-builder .an-row{ border-color:#e6e6e6; }\\n#chladni-plant-builder .stepper button{ background:#fff; color:#1A1B1C; border-color:#d0d0d0; }\\n#chladni-plant-builder .stepper button:hover{ background:#f3f3f3; }\\n#chladni-plant-builder .stepper .sn{ font-size:14px; }\\n#chladni-plant-builder .an-actions button{ font-size:12px; color:#676767; }\\n#chladni-plant-builder .an-price{ font-size:14px; color:var(--price); }\\n#chladni-plant-builder .addAssetBtn{ background:#fff; color:#1A1B1C; border-color:#b8b8b8; font-size:14px; }\\n#chladni-plant-builder .addAssetBtn:hover{ background:#f7f7f7; border-color:#737373; }\\n#chladni-plant-builder .treeTotals{ border-color:#d5d5d5; }\\n#chladni-plant-builder .treeTotals .tt-row{ color:#5e5e5e; font-size:14px; }\\n#chladni-plant-builder .treeTotals .tt-price{ color:var(--price); font-size:29px; }\\n#chladni-plant-builder .tree .hint, #chladni-plant-builder .lockLink{ font-size:13px; }\\n#chladni-plant-builder .lockLink{ color:#454545; }\\n#chladni-plant-builder .cardOuter{ background:#fff; }\\n#chladni-plant-builder .card{ background:#fff; border-color:#e7e7e7; box-shadow:0 8px 32px #20202003; }\\n#chladni-plant-builder .crumb{ font-size:12px; color:#737373; letter-spacing:1.5px; }\\n#chladni-plant-builder .cardTitle{ font-size:33px; color:#1A1B1C; }\\n#chladni-plant-builder #plantCard .cardTitle{ color:#1A1B1C; font-size:46px; }\\n#chladni-plant-builder #plantCard \u003e .hint:first-of-type{ font-size:16px; color:#696969; }\\n#chladni-plant-builder .hint{ color:#727272; font-size:14px; line-height:1.65; }\\n#chladni-plant-builder .scopeNote{ color:#636363; background:#fafafa; border-color:#e7e7e7; font-size:14px; }\\n#chladni-plant-builder .scopeNote b{ color:#343434; }\\n#chladni-plant-builder .fldLabelRow label{ color:#4c4c4c; font-size:13px; letter-spacing:.6px; }\\n#chladni-plant-builder .fld input[type=text], #chladni-plant-builder .fld input[type=number], #chladni-plant-builder .fld input[type=email], #chladni-plant-builder .fld select, #chladni-plant-builder .search input{ background:#fff; color:#1A1B1C; border-color:#d5d5d5; font-size:16px; min-height:50px; }\\n#chladni-plant-builder .fld input::placeholder, #chladni-plant-builder .search input::placeholder{ color:#8b8b8b; }\\n#chladni-plant-builder .fld input:focus, #chladni-plant-builder .fld select:focus, #chladni-plant-builder .search input:focus{ border-color:#656565; box-shadow:0 0 0 3px #b4b4b41b; }\\n#chladni-plant-builder .bigInput{ font-size:17px !important; }\\n#chladni-plant-builder .fldErr{ font-size:13px; }\\n#chladni-plant-builder .infoIcon{ color:#7c7c7c; border-color:#b0b0b0; background:#fff; }\\n#chladni-plant-builder .tip{ font-size:14px; background:#1A1B1C; }\\n#chladni-plant-builder .cat h3, #chladni-plant-builder .sectionHead{ font-size:12px; color:#727272; letter-spacing:1px; }\\n#chladni-plant-builder .mcardbtn, #chladni-plant-builder .opt{ background:#fff; border-color:#dadada; color:#313131; font-size:14px; min-height:52px; }\\n#chladni-plant-builder .mcardbtn:hover, #chladni-plant-builder .opt:hover{ background:#f7f7f7; border-color:#a0a0a0; }\\n#chladni-plant-builder .mcardbtn.on, #chladni-plant-builder .opt.on{ background:#1A1B1C; border-color:#1A1B1C; color:#fff; box-shadow:none; }\\n#chladni-plant-builder .opt .od{ font-size:12px; }\\n#chladni-plant-builder .toggleRow{ background:#f7f7f7; }\\n#chladni-plant-builder .toggleRow span.tLabel{ color:#373737; font-size:14px; }\\n#chladni-plant-builder .toggleRow .tSwitch{ background:#d7d7d7; }\\n#chladni-plant-builder .toggleRow.on .tSwitch{ background:#1A1B1C; }\\n#chladni-plant-builder .advBlock{ border-color:#e4e4e4; }\\n#chladni-plant-builder .driverCard{ background:#fff; border-color:#e6e6e6; }\\n#chladni-plant-builder .driverCard .dcHead b{ font-size:15px; color:#1A1B1C; }\\n#chladni-plant-builder .driverCard .dcRemove{ font-size:12px; color:#7e7e7e; }\\n#chladni-plant-builder .addDriverBtn, #chladni-plant-builder .addInterBtn{ background:#fff; color:#414141; border-color:#c7c7c7; font-size:14px; }\\n#chladni-plant-builder .addDriverBtn:hover, #chladni-plant-builder .addInterBtn:hover{ background:#f9f9f9; border-color:#8e8e8e; }\\n#chladni-plant-builder .btn{ background:#1A1B1C; border-color:#1A1B1C; color:#fff; font-size:14px; }\\n#chladni-plant-builder .btn:hover{ background:#333; border-color:#333; color:#fff; }\\n#chladni-plant-builder .btn.ghost{ background:#fff; border-color:#d3d3d3; color:#242424; }\\n#chladni-plant-builder .btn.ghost:hover{ background:#f8f8f8; border-color:#a2a2a2; color:#242424; }\\n#chladni-plant-builder .btn:disabled:hover{ background:#1A1B1C; color:#fff; }\\n#chladni-plant-builder .quoteActions .btn{ font-size:14px; }\\n#chladni-plant-builder #plantCard .quoteActions{ border-color:#e7e7e7; }\\n#chladni-plant-builder .quoteTable th, #chladni-plant-builder .quoteTable td{ border-color:#e8e8e8; font-size:14px; }\\n#chladni-plant-builder .quoteTable th{ color:#767676; font-size:12px; letter-spacing:.5px; }\\n#chladni-plant-builder .reviewBlockHead td{ color:#5f5f5f; background:#f8f8f8; font-size:12px; }\\n#chladni-plant-builder .puckOverrideInp{ background:#fff; color:#1A1B1C; border-color:#d4d4d4; font-size:14px; }\\n#chladni-plant-builder .quoteBig{ color:var(--price); font-size:39px; }\\n#chladni-plant-builder .checkline{ background:#fff; border-color:#dcdcdc; font-size:14px; }\\n#chladni-plant-builder .checkline.on{ background:#fbfbfb; border-color:#a3a3a3; }\\n#chladni-plant-builder .tabbar{ background:#f9f9f9; border-color:#e4e4e4; gap:5px; }\\n#chladni-plant-builder .tabbtn{ color:#777777; font-size:12px; letter-spacing:0; }\\n#chladni-plant-builder .tabbtn .tn{ color:inherit; font-size:10px; letter-spacing:.5px; }\\n#chladni-plant-builder .tabbtn.active{ color:#fff; background:#1A1B1C; box-shadow:none; }\\n#chladni-plant-builder .tabbtn.done:not(.active){ color:#464646; background:#fff; }\\n#chladni-plant-builder .tabPanel{ background:#fff; border-color:#e4e4e4; }\\n#chladni-plant-builder .lockNote{ background:#f9f9f9; color:#757575; font-size:14px; }\\n#chladni-plant-builder .resumeBanner{ background:#f9f9f9; color:#4d4d4d; border-color:#e2e2e2; font-size:14px; }\\n#chladni-plant-builder .resumeBanner button{ color:#404040; border-color:#adadad; font-size:13px; }\\n#chladni-plant-builder .resumeBanner button:hover{ background:#fff; color:#404040; border-color:#777777; }\\n#chladni-plant-builder .quoteOverlay{ background:rgba(20,20,20,.58); }\\n#chladni-plant-builder .quoteSheet{ background:#fff; border-color:#eaeaea; }\\n#chladni-plant-builder .quoteSheet .qTop{ border-color:#e7e7e7; }\\n#chladni-plant-builder .quoteSheet .qBrand{ color:#1A1B1C; }\\n#chladni-plant-builder .quoteSheet .qMeta{ font-size:13px; color:#7a7a7a; }\\n#chladni-plant-builder .qClose{ background:#fff; color:#353535; border-color:#d2d2d2; font-size:18px; }\\n#chladni-plant-builder .qClose:hover{ background:#f9f9f9; border-color:#949494; }\\n#chladni-plant-builder .qEyebrow{ color:#757575; font-size:12px; letter-spacing:1.3px; }\\n#chladni-plant-builder .qTitle{ color:#1A1B1C; font-size:42px; }\\n#chladni-plant-builder .qSubtitle{ color:#707070; font-size:16px; }\\n#chladni-plant-builder .qHero{ background:var(--price-soft); color:var(--price); border:1px solid #d0ebda; }\\n#chladni-plant-builder .qHero .qEyebrow, #chladni-plant-builder .qHero .qSubtitle, #chladni-plant-builder .qHero .hint{ color:#4f8064; }\\n#chladni-plant-builder .qAmount{ color:var(--price); font-size:50px; }\\n#chladni-plant-builder .qPeriod{ color:#407556; font-size:14px; }\\n#chladni-plant-builder .qStat{ background:#fcfcfc; border-color:#e6e6e6; color:#767676; font-size:14px; }\\n#chladni-plant-builder .qStat b, #chladni-plant-builder .qStat strong{ color:#1A1B1C; font-size:27px; }\\n#chladni-plant-builder .qSummary{ background:#fff; border-color:#e4e4e4; box-shadow:0 6px 20px #23232304; }\\n#chladni-plant-builder .qSummary .qAmount{ font-size:45px; }\\n#chladni-plant-builder .qSection \u003e h2, #chladni-plant-builder .qSection \u003e h3, #chladni-plant-builder .qMain \u003e h2, #chladni-plant-builder .qSummary \u003e h2, #chladni-plant-builder .qSummary \u003e h3{ color:#1A1B1C; font-size:17px; }\\n#chladni-plant-builder .qAsset{ background:#fff; border-color:#e5e5e5; }\\n#chladni-plant-builder .qAssetHead h2, #chladni-plant-builder .qAssetHead h3, #chladni-plant-builder .qAssetHead h4, #chladni-plant-builder .qAssetHead strong{ color:#1A1B1C; font-size:19px; }\\n#chladni-plant-builder .qAssetPrice, #chladni-plant-builder .qAssetPrice b{ color:var(--price); font-size:21px; }\\n#chladni-plant-builder .qAssetPrice span, #chladni-plant-builder .qAssetPrice small{ color:#6e6e6e; font-size:12px; }\\n#chladni-plant-builder .qAssetMeta{ color:#757575; font-size:14px; }\\n#chladni-plant-builder .qBreakdown{ background:#fbfbfb; border:1px solid #ececec; }\\n#chladni-plant-builder .qPriceRow{ color:#656565; font-size:14px; gap:13px; }\\n#chladni-plant-builder .qPriceRow b, #chladni-plant-builder .qPriceRow strong{ color:var(--price); }\\n#chladni-plant-builder .qTotalRow{ color:#1A1B1C; font-size:16px; border-color:#e0e0e0; }\\n#chladni-plant-builder .qTotalRow b, #chladni-plant-builder .qTotalRow strong, #chladni-plant-builder .qTotalRow \u003e :last-child{ color:var(--price); font-size:25px; }\\n#chladni-plant-builder .qDetails{ color:#464646; border-color:#e7e7e7; }\\n#chladni-plant-builder .qDetails \u003e summary{ font-size:14px; }\\n#chladni-plant-builder .qChain{ color:#626262; background:#fafafa; font-size:13px; }\\n#chladni-plant-builder .quoteSheet .qSensorTable th, #chladni-plant-builder .quoteSheet .qSensorTable td{ color:#626262; font-size:14px; border-color:#e8e8e8; }\\n#chladni-plant-builder .quoteSheet .qSensorTable th{ color:#6f6f6f; font-size:12px; letter-spacing:.4px; }\\n#chladni-plant-builder .quoteSheet .qSensorTable th.num, #chladni-plant-builder .quoteSheet .qSensorTable td.num{ color:#1A1B1C; }\\n#chladni-plant-builder .qSensorTable caption{ color:#767676; font-size:13px; }\\n#chladni-plant-builder .qContact{ background:#fff; border-color:#e4e4e4; color:#5e5e5e; font-size:14px; }\\n#chladni-plant-builder .qContact h2.qSection{ color:#1A1B1C; font-size:16px; }\\n#chladni-plant-builder .qContact b, #chladni-plant-builder .qContact strong{ color:#1A1B1C; }\\n#chladni-plant-builder .qFooter{ color:#7a7a7a; border-color:#e6e6e6; font-size:13px; }\\n#chladni-plant-builder .qShareNotice{ color:#656565; background:#f7f7f7; font-size:13px; }\\n#chladni-plant-builder .actionStatus{ color:#5d5d5d; font-size:14px; }\\n#chladni-plant-builder .qTextButton{ color:#545454; font-size:13px; }\\n#chladni-plant-builder .qSharePanel{ background:#fff; border-color:#e2e2e2; }\\n#chladni-plant-builder .qSharePanel h3{ color:#1A1B1C; font-size:22px; }\\n#chladni-plant-builder .qSharePanel textarea{ background:#fff; border-color:#c9c9c9; font-size:13px; }\\n#chladni-plant-builder .reviewPricing{ background:var(--price-soft); border-color:#d0ebda; }\\n#chladni-plant-builder .reviewPricing .quoteBig{ color:var(--price); }\\n\\n/* Cart uses the same asset cards and quote summary, without changing its data. */\\n#chladni-plant-builder .cartHeader{ display:flex; align-items:flex-start; justify-content:space-between; flex-wrap:wrap; gap:18px 24px; margin-bottom:25px; }\\n#chladni-plant-builder .cartHeader .qHeading{ margin-bottom:0; }\\n#chladni-plant-builder .cartNav{ display:flex; align-items:center; gap:10px; flex-wrap:wrap; }\\n#chladni-plant-builder .cartNav .btn{ min-height:44px; font-size:13px; padding:11px 19px; }\\n#chladni-plant-builder .qCartBadge{ display:inline-flex; align-items:center; gap:7px; border-radius:999px; padding:7px 12px; background:#eef9f2; border:1px solid #cce8d7; color:var(--price); font-size:13px; font-weight:700; line-height:1.5; }\\n#chladni-plant-builder .qCartNotice{ padding:16px 19px; margin:0 0 22px; background:#f2faf5; border:1px solid #d8eddf; border-radius:15px; color:#327153; font-size:14px; line-height:1.65; }\\n#chladni-plant-builder .cartItems{ min-width:0; }\\n#chladni-plant-builder .cartItems \u003e h2{ color:#1A1B1C; font-size:19px; line-height:1.4; margin:0 0 17px; }\\n#chladni-plant-builder .cartCheckout{ min-width:0; background:#fff; }\\n#chladni-plant-builder .cartCheckout .qHero{ background:var(--price-soft); }\\n#chladni-plant-builder .cartLineInfo{ display:flex; align-items:center; justify-content:space-between; flex-wrap:wrap; gap:9px 18px; border-top:1px solid #e8e8e8; margin-top:17px; padding-top:15px; color:#6a6a6a; font-size:14px; }\\n#chladni-plant-builder .cartLineInfo .stepper{ color:#1A1B1C; }\\n#chladni-plant-builder .cartLineInfo button{ font-size:13px; }\\n#chladni-plant-builder .cartContact{ padding:21px; border:1px solid #e4e4e4; border-radius:17px; background:#fff; margin:18px 0; color:#636363; font-size:14px; line-height:1.65; }\\n#chladni-plant-builder .cartContact h2, #chladni-plant-builder .cartContact h3, #chladni-plant-builder .cartTerms h2, #chladni-plant-builder .cartTerms h3{ color:#1A1B1C; font-size:17px; font-weight:700; line-height:1.4; margin-bottom:13px; }\\n#chladni-plant-builder .cartContact .fld:last-child{ margin-bottom:0; }\\n#chladni-plant-builder .cartContact label{ display:block; color:#464646; font-size:13px; font-weight:700; margin:0 0 7px; }\\n#chladni-plant-builder .cartContact input{ width:100%; min-height:48px; padding:12px 14px; background:#fff; color:#1A1B1C; border:1px solid #d4d4d4; border-radius:11px; font-size:16px; }\\n#chladni-plant-builder .cartTerms{ color:#727272; font-size:13px; line-height:1.7; padding:17px 0 0; margin-top:17px; border-top:1px solid #e4e4e4; }\\n#chladni-plant-builder .cartTerms label{ display:flex; align-items:flex-start; gap:10px; font-size:14px; color:#565656; }\\n#chladni-plant-builder .cartTerms input{ flex-shrink:0; margin-top:4px; width:17px; height:17px; accent-color:#1A1B1C; }\\n#chladni-plant-builder .cartActions{ display:flex; flex-direction:column; gap:11px; margin-top:21px; }\\n#chladni-plant-builder .cartActions .btn{ width:100%; text-align:center; }\\n#chladni-plant-builder .cartActions .qTextButton{ margin:4px auto 0; }\\n\\n@media (max-width:1080px) {\\n #chladni-plant-builder .qPriceRow{ font-size:13px; }\\n #chladni-plant-builder .qSummary .qAmount{ font-size:42px; }\\n}\\n@media (max-width:880px) {\\n #chladni-plant-builder .qPriceRow{ font-size:14px; }\\n #chladni-plant-builder .cartHeader{ align-items:flex-start; }\\n}\\n@media (max-width:600px) {\\n #chladni-plant-builder .topbar .brandProduct{ font-size:13px; }\\n #chladni-plant-builder .topbarLink{ font-size:13px; }\\n #chladni-plant-builder #plantCard .cardTitle{ font-size:37px; }\\n #chladni-plant-builder .cardTitle{ font-size:30px; }\\n #chladni-plant-builder .scopeNote{ font-size:14px; }\\n #chladni-plant-builder .fldLabelRow label{ font-size:13px; }\\n #chladni-plant-builder .tabbar{ gap:3px; }\\n #chladni-plant-builder .tabbtn{ font-size:11px; padding:11px 3px; overflow-wrap:anywhere; }\\n #chladni-plant-builder .tabbtn .tn{ font-size:9px; letter-spacing:0; }\\n #chladni-plant-builder .quoteSheet .qMeta{ font-size:12px; }\\n #chladni-plant-builder .qTitle{ font-size:34px; }\\n #chladni-plant-builder .qStat{ font-size:12px; padding:14px 11px; }\\n #chladni-plant-builder .qStat b, #chladni-plant-builder .qStat strong{ font-size:25px; }\\n #chladni-plant-builder .qAssetHead h2, #chladni-plant-builder .qAssetHead h3, #chladni-plant-builder .qAssetHead h4, #chladni-plant-builder .qAssetHead strong{ font-size:18px; }\\n #chladni-plant-builder .qAssetPrice, #chladni-plant-builder .qAssetPrice b{ font-size:19px; }\\n #chladni-plant-builder .qAssetMeta{ font-size:13px; }\\n #chladni-plant-builder .qPriceRow{ font-size:13px; }\\n #chladni-plant-builder .quoteSheet .qSensorTable td{ font-size:13px; }\\n #chladni-plant-builder .qContact, #chladni-plant-builder .qShareNotice{ font-size:13px; }\\n #chladni-plant-builder .cartHeader{ gap:16px; }\\n #chladni-plant-builder .cartNav{ width:100%; gap:8px; }\\n #chladni-plant-builder .cartNav .btn{ flex:1; padding:11px 14px; }\\n #chladni-plant-builder .cartContact{ padding:18px; }\\n #chladni-plant-builder .cartLineInfo{ font-size:13px; }\\n}\\n@media (max-width:380px) {\\n #chladni-plant-builder #plantCard .cardTitle{ font-size:32px; }\\n #chladni-plant-builder .qTitle{ font-size:31px; }\\n #chladni-plant-builder .tabbtn{ font-size:10px; }\\n #chladni-plant-builder .tabbtn .tn{ font-size:8px; }\\n #chladni-plant-builder .qAssetHead{ flex-direction:column; }\\n #chladni-plant-builder .qAssetPrice{ text-align:left; }\\n}\\n@media print {\\n #chladni-plant-builder .quoteSheet button{ display:none !important; }\\n #chladni-plant-builder .qHero{ background:#eef9f2 !important; color:#117a4e !important; }\\n #chladni-plant-builder .qHero .qPeriod, #chladni-plant-builder .qHero .qEyebrow, #chladni-plant-builder .qHero .hint{ color:#407556 !important; }\\n #chladni-plant-builder .qSummary{ background:#fff; box-shadow:none; }\\n #chladni-plant-builder .cartActions, #chladni-plant-builder .cartNav, #chladni-plant-builder .cartLineInfo button{ display:none !important; }\\n #chladni-plant-builder .qCartNotice, #chladni-plant-builder .cartContact, #chladni-plant-builder .cartTerms{ break-inside:avoid; }\\n}\\n\\n/* Black accents only: retain the bright surfaces, green prices and existing layout. */\\n#chladni-plant-builder .topbar{ background:#1A1B1C; color:#fff; border-bottom-color:#1A1B1C; }\\n#chladni-plant-builder .topbar span, #chladni-plant-builder .topbar .brandProduct{ color:#dedede; }\\n#chladni-plant-builder .topbar .brandDivider{ background:#626262; }\\n#chladni-plant-builder .topbar .brandLogo.onDark{ filter:invert(1); mix-blend-mode:screen; }\\n#chladni-plant-builder .topbarLink{ background:transparent; color:#fff; border-color:#6b6b6b; }\\n#chladni-plant-builder .topbarLink:hover{ background:#292929; color:#fff; border-color:#aaa; }\\n\\n#chladni-plant-builder .card{ border-color:#d0d0d0; box-shadow:inset 0 5px 0 #1A1B1C,0 8px 32px #20202005; }\\n#chladni-plant-builder .scopeNote{ background:#1A1B1C; border-color:#1A1B1C; color:#e5e5e5; }\\n#chladni-plant-builder .scopeNote b{ color:#fff; }\\n#chladni-plant-builder .crumb, #chladni-plant-builder .sectionHead, #chladni-plant-builder .cat h3, #chladni-plant-builder .tree h2, #chladni-plant-builder .zoneHead{ color:#1A1B1C; }\\n#chladni-plant-builder .sectionHead, #chladni-plant-builder .treeTotals, #chladni-plant-builder .treePlantInfo{ border-top-color:#1A1B1C; border-bottom-color:#1A1B1C; }\\n#chladni-plant-builder .assetNode.sel{ box-shadow:inset 4px 0 0 #1A1B1C,0 0 0 1px #1A1B1C; }\\n#chladni-plant-builder .btn.ghost{ border-color:#1A1B1C; }\\n#chladni-plant-builder .btn.ghost:hover{ border-color:#1A1B1C; background:#f3f3f3; }\\n#chladni-plant-builder .addAssetBtn, #chladni-plant-builder .addDriverBtn, #chladni-plant-builder .addInterBtn{ border-color:#555; }\\n\\n#chladni-plant-builder .quoteSheet .qTop{ border-bottom-color:#1A1B1C; }\\n#chladni-plant-builder .qStat{ background:#1A1B1C; border-color:#1A1B1C; color:#dedede; }\\n#chladni-plant-builder .qStat b, #chladni-plant-builder .qStat strong{ color:#fff; }\\n#chladni-plant-builder .qSummary{ border-color:#1A1B1C; box-shadow:inset 0 5px 0 #1A1B1C; }\\n#chladni-plant-builder .qSummary \u003e .qEyebrow{ color:#1A1B1C; }\\n#chladni-plant-builder .qAsset{ border-color:#d3d3d3; box-shadow:inset 4px 0 0 #1A1B1C; }\\n#chladni-plant-builder .qCartNotice{ background:#1A1B1C; border-color:#1A1B1C; color:#f1f1f1; }\\n#chladni-plant-builder .qTotalRow{ border-top-color:#1A1B1C; }\\n\\n@media print {\\n #chladni-plant-builder .card, #chladni-plant-builder .qAsset, #chladni-plant-builder .qSummary{ box-shadow:none; }\\n #chladni-plant-builder .qStat{ background:#111 !important; color:#fff !important; }\\n #chladni-plant-builder .qStat b, #chladni-plant-builder .qStat strong{ color:#fff !important; }\\n}\\n\\n\\n/* Room for the tip close button. */\\n#chladni-plant-builder .tip{ padding-right:32px; }\\n\\n/* Webflow capitalizes h1/h2 site-wide. */\\n#chladni-plant-builder .quoteSheet h1, #chladni-plant-builder .quoteSheet h2{ text-transform:none; }\\n\\n/* The uppercase \\\"Vibration sensors\\\" header does not fit a 72px column and would split mid-word. */\\n#chladni-plant-builder .quoteSheet .qSensorTable th:last-child{ width:132px; overflow-wrap:normal; }\\n#chladni-plant-builder .checkoutNotice{ background:var(--panel); color:var(--ink); padding:12px 16px; font-size:13px; border-bottom:1px solid var(--hairline); }\\n#chladni-plant-builder .checkoutNotice.ok{ background:var(--ink); color:#fff; }\\n#chladni-plant-builder .checkoutNoticeLink{ color:inherit; text-decoration:underline; margin-left:12px; white-space:nowrap; }\\n/* Hidden until Cloudflare needs a click; Managed widgets can show a challenge. */\\n#chladni-plant-builder #checkoutTurnstile{ position:fixed; z-index:300; right:16px; bottom:16px; width:1px; height:1px; overflow:hidden; clip:rect(0,0,0,0); }\\n#chladni-plant-builder #checkoutTurnstile.is-interactive{ width:auto; height:auto; overflow:visible; clip:auto; background:#fff; padding:12px; border:1px solid var(--hairline); box-shadow:0 12px 32px rgba(14,14,14,.18); }\\n\"));\n document.head.appendChild(s);\n})();\n/* ================= PLANT INFO (restored from v1) ================= */\nvar INDUSTRIES = [\"Food \u0026 Beverage\",\"Building Materials\",\"Plastics \u0026 Rubber\",\"Paper \u0026 Pulp\",\"Chemicals\",\"Pharmaceuticals\",\"Automotive\",\"Metals / Steel / Aluminum\",\"Mining \u0026 Aggregates\",\"General Manufacturing\",\"Packaging\",\"Textile\",\"Wood Products\",\"Water / Wastewater\",\"Oil \u0026 Gas\",\"Data Centers\",\"Warehousing / Distribution\",\"Other\"];\nvar plant = { company:\"\", name:\"\", industry:INDUSTRIES[0], country:\"United States\", state:\"\", contactName:\"\", contactEmail:\"\", _err:\"\", _emailErr:\"\" };\nvar appStage = \"plant\"; /* 'plant' | 'app' */\n\n/* ================= CONFIG ================= */\nvar CONFIG = {\n RATE: 79, BLOCK_SIZE: 4, EXTRA_SENSOR_MONTHLY: 18,\n MCA_MONTHLY: 79, MCA_STANDALONE_MONTHLY: 120,\n GATEWAY_CAPACITY: 40,\n BUNDLE_DISCOUNT: 0\n};\n/* v13: EDGE_STREAM_PER_DRIVER and the Legacy Rack Panel question it priced are retired - see\n diagnostic addendum below. This tool now scopes out equipment already on an existing\n monitoring system (Bently Nevada or similar) up front, on the Plant Info screen, instead of\n asking per-asset and switching to an Edge-Streaming price. */\n/* v13: smallMotorKw/smallMotorPucks (the old \"4 kW or less on a Direct coupling = 1 puck\"\n exemption) is retired - see motorSensorCount() below, which replaces it with an ISO\n 20816-3 / NEMA MG-1 horsepower-and-speed tier lookup per client request. pucksPerLocation\n remains the flat default for every non-motor bearing location in the tool. */\nvar SENSOR_RULES = { pucksPerLocation:2 };\n\n/* ================= v12-web: ANALYTICS =================\n Pushes named events onto window.dataLayer; Google Tag Manager on the hosting page reads them\n and fans out to GA4 / Meta. Tag and pixel IDs live in GTM, never in this file. No PII in params. */\nfunction track(name, params){\n try {\n window.dataLayer = window.dataLayer || [];\n var payload = { event: name };\n if (params){ for (var k in params){ if (params.hasOwnProperty(k)) payload[k] = params[k]; } }\n window.dataLayer.push(payload);\n } catch(e){}\n}\nvar plantContinueTracked = false;\n\n/* ================= CATALOG =================\n v4: Pump / Fan-Blower / Compressor sub-types (Centrifugal vs Reciprocating vs Screw, etc.)\n moved out of this picker and into Block 5 (Driven Unit), per the client - the picker now\n only needs the broad family, and Block 5 asks the engineering-specific follow-up questions\n contextually (category, bearing arrangement, cylinder/stage count).\n v7: restored three whole categories - Plastics, HVAC / Utilities, and Paper / Web Processing -\n that were present in the original tool (Chladni_Plant_Builder_39.html) but dropped when the\n relational rewrite started in v2 and never carried forward through v3-v6. These items don't get\n their own contextual Block 5 sub-questions the way Pump/Fan/Compressor do - they fall through to\n the same generic driven-component treatment Grinder, Shredder, and Vibrating Screen already get\n - but they're selectable again, matching the original tool's full equipment coverage.\n v9: Pump, Fan / Blower, and Compressor each had their own single-item category, and there was no\n entry anywhere for a gearbox as a standalone driven unit (a gearbox could already appear as an\n *intermediate stage* between driver and driven unit - that's unrelated and unchanged). Per client\n request, those three moved into one consolidated \"General\" category alongside Gearbox / Speed\n Reducer and two more common catch-all driven types (Generator, Flywheel). Nothing downstream keys\n off the category label (cat:) - only off the item name (pkg.machineType) - so this is a picker\n reorganization only; Pump/Fan-Blower/Compressor keep every bit of their existing Block 5\n sub-question logic and pricing untouched. */\nvar CATALOG = [\n { cat:\"General\", items:[\"Pump\",\"Fan / Blower\",\"Compressor\",\"Gearbox / Speed Reducer\",\"Generator\",\"Flywheel\"] },\n { cat:\"Heavy Equipment\", items:[\"Crusher\",\"Mill\",\"Grinder\",\"Shredder\",\"Vibrating Screen\"] },\n { cat:\"Conveying\", items:[\"Belt Conveyor\",\"Screw Conveyor\",\"Roller Conveyor\",\"Chain Conveyor\",\"Bucket Elevator\",\"Feeder\"] },\n { cat:\"Processing\", items:[\"Mixer\",\"Agitator\",\"Blender\",\"Homogenizer\",\"Centrifuge\",\"Separator\"] },\n { cat:\"Plastics\", items:[\"Extruder\",\"Injection Molding Machine\",\"Blow Molding Machine\",\"Pelletizer\",\"Granulator\"] },\n { cat:\"HVAC / Utilities\", items:[\"Air Handling Unit (AHU)\",\"Supply Fan\",\"Exhaust Fan\",\"Cooling Tower Fan\",\"HVAC Pump\",\"Chiller\",\"Refrigeration Compressor\",\"Other HVAC Equipment\"] },\n { cat:\"Paper / Web Processing\", items:[\"Paper Machine\",\"Corrugator\",\"Winder / Rewinder\",\"Calender\",\"Coater / Laminator\",\"Printing Line\",\"Other Paper / Web Equipment\"] },\n { cat:\"Other\", items:[\"Custom Machine\",\"Production Equipment\",\"Other Rotating Equipment\"] }\n];\n\n/* Coupling Type - gearbox removed per spec (gearbox now lives in the Intermediate block).\n \"Direct\" is the shared-shaft equivalent of v2's \"close-coupled\". */\nvar COUPLING_TYPES = [\n [\"belt\",\"Belt\"], [\"chain\",\"Chain\"], [\"direct\",\"Direct\"], [\"flexible\",\"Flexible\"],\n [\"fluidmag\",\"Fluid / Magnetic\"], [\"gear\",\"Gear\"], [\"integral\",\"Integral\"],\n [\"webdrive\",\"Sheet / Web Drive\"], [\"spline\",\"Spline\"], [\"unknown\",\"Unknown\"]\n];\nvar ORIENTATIONS = [\n [\"inline\",\"In-line\"], [\"angular\",\"Angular\"], [\"parallel\",\"Parallel\"], [\"perpendicular\",\"Perpendicular\"], [\"unknown\",\"Unknown\"]\n];\n\n/* ================= RELATIONAL SCHEMA (Level 6 / Level 7) ================= */\nvar driverSeq = 1, pkgSeq = 1, interSeq = 1;\n\nfunction initDriverFields(d){\n /* v13: Turbine and Engine driver types are retired - motors are the only driver this tool\n covers for now. See diagnostic addendum below. */\n d.control_type = d.control_type || \"\"; d.insulation_class = d.insulation_class || \"\";\n d.voltage_level = d.voltage_level || \"\"; d.mcc_panel_access = d.mcc_panel_access || \"\";\n}\nfunction newDriver(kind){\n /* v4: hp_tier/amperage_tier/speed_tier bucket options are gone - the client wants a plain\n number box for these instead of a tier picker (see diagnostic #13), so these are now free\n text fields holding whatever number the user types, never gating anything. */\n var d = { id:\"drv\"+(driverSeq++), driver_type: kind || \"motor\",\n horsepower:\"\", amperage:\"\", line_freq:\"\", speed_rpm:\"\" };\n initDriverFields(d);\n return d;\n}\nfunction newIntermediate(){\n /* v4: a stage is now ONE element type (gearbox / jackshaft / other), never both at once -\n see diagnostic #14. shaftCount only applies when type === \"gearbox\". */\n return { id:\"int\"+(interSeq++), type:\"\", shaftCount:\"\" };\n}\nfunction syncInterCouplings(pkg){\n /* Keeps pkg.interCouplings sized to exactly (intermediates.length - 1): one coupling\n question per junction between consecutive intermediate stages. Trims from the end on\n removal and appends blank entries on growth - a stage removed from the middle shifts\n which physical junction a later coupling answer was describing, which is an accepted\n simplification for this tool's scope (documented in diagnostic #14) rather than trying\n to track junction identity through arbitrary mid-array edits. */\n var needed = Math.max(0, pkg.intermediates.length - 1);\n while (pkg.interCouplings.length \u003c needed) pkg.interCouplings.push({ type:\"\", orientation:\"\" });\n while (pkg.interCouplings.length \u003e needed) pkg.interCouplings.pop();\n}\nfunction newPackage(){\n /* v5: machineType/machineCat are no longer chosen up front - the equipment-type picker moved\n into Block 5 (Driven Unit), see diagnostic #18. A brand-new draft starts with no type at all\n and picks one up only once the user reaches that block. */\n return {\n id:\"pkg\"+(pkgSeq++),\n name:\"\", zone:\"\", machineType:\"\", machineCat:\"\",\n criticality:\"medium\", instanceCount:1,\n bundleDiscountEligible:false,\n drivers:[ newDriver(\"motor\") ],\n coupling_nodes:[ { type:\"\", orientation:\"\" }, { type:\"\", orientation:\"\" } ],\n intermediateAnswer:\"\", /* '' | 'yes' | 'no' */\n intermediates:[],\n interCouplings:[], /* one {type,orientation} per junction between consecutive intermediate stages */\n driven_unit:{ category:\"\", cylinders:\"\", stages:\"\", bearingArrangement:\"\" },\n puckOverrides:{}, /* componentKey -\u003e manually-set puck count, overriding the calculated default */\n advancedOpen:false,\n activeTab:0,\n maxUnlocked:0\n };\n}\n\nvar assets = [];\nvar draft = null;\nvar editingId = null;\n\n/* ================= COMPONENT / CALCULATION ENGINE ================= */\n/* v13: motor sensor allocation, replacing the old flat \"kW \u2264 4 on Direct coupling = 1 puck\"\n exemption per client request, grounded in ISO 20816-3 machine-size tiers and NEMA MG-1\n frame/speed physics:\n - Any motor over 400 HP (ISO 20816-3 \"Large\") always gets 2 sensors - casing/frame size\n alone rules out single-point coverage.\n - Any motor below 1000 RPM (\"Low speed\", NEMA MG-1 high-pole-count frames) always gets 2\n sensors, regardless of horsepower - a low-speed frame's DE-to-NDE bearing span exceeds\n the ISO 13373-1 / 20816-3 \u00a75 30-inch single-point isolation limit even on a small motor.\n - Small motors (\u226420 HP) at standard or high speed get 1 sensor - compact single-housing\n frame, ISO 20816-3 Group tier baseline.\n - Medium motors (20-400 HP) at high speed (\u003e2000 RPM, compact 2-pole frame) get 1 sensor\n only up to 100 HP; above that, 2.\n - Every other combination (medium at standard speed, medium at high speed above 100 HP)\n gets 2 sensors - ISO 20816-3 dual-housing mandate.\n Verified against the client's three edge cases: 15 HP @ 900 RPM -\u003e 2; 75 HP @ 3600 RPM -\u003e 1;\n any motor \u003e 400 HP -\u003e 2, unconditionally. */\nfunction motorSensorCount(hp, rpm){\n if (hp \u003e 400) return 2;\n if (rpm \u003c 1000) return 2;\n if (hp \u003c= 20) return 1;\n if (rpm \u003e 2000 \u0026\u0026 hp \u003c= 100) return 1;\n return 2;\n}\nfunction pucksForDriver(driver, pkg){\n if (driver.driver_type === \"motor\"){\n var hp = parseFloat(driver.horsepower), rpm = parseFloat(driver.speed_rpm);\n if (!isNaN(hp) \u0026\u0026 hp \u003e 0 \u0026\u0026 !isNaN(rpm) \u0026\u0026 rpm \u003e 0){\n return motorSensorCount(hp, rpm);\n }\n }\n return SENSOR_RULES.pucksPerLocation;\n}\nfunction intermediateComponents(pkg){\n /* v4: a stage is a single element type now (gearbox XOR jackshaft XOR other - diagnostic #14),\n never both, so this is a plain switch instead of two independent yes/no checks. Gearbox\n puck count now scales with the shaft count the client asked to capture (diagnostic #14),\n defaulting to 2 shafts (input + output) if left blank rather than gating the workflow on it. */\n var out = [];\n pkg.intermediates.forEach(function(inter, i){\n var tag = pkg.intermediates.length\u003e1 ? \" #\"+(i+1) : \"\";\n if (inter.type === \"gearbox\"){\n var shafts = parseInt(inter.shaftCount, 10);\n if (!shafts || shafts \u003c 1) shafts = 2;\n out.push({ label:\"Gearbox / speed reducer\"+tag+\" (\"+shafts+\" shaft\"+(shafts===1?\"\":\"s\")+\")\", cls:\"A\",\n total: shafts * SENSOR_RULES.pucksPerLocation, reason:\"critical drivetrain\" });\n } else if (inter.type === \"jackshaft\"){\n out.push({ label:\"Jackshaft / countershaft bearing pedestals\"+tag, cls:\"B\", total:SENSOR_RULES.pucksPerLocation,\n reason:\"intermediate reduction stage - independent bearing fault frequencies\" });\n } else if (inter.type === \"other\"){\n out.push({ label:\"Other intermediate element\"+tag, cls:\"B\", total:SENSOR_RULES.pucksPerLocation,\n reason:\"intermediate stage present, type not otherwise listed\" });\n }\n });\n return out;\n}\nvar RULES = {\n \"Reciprocating Compressor\": function(pkg){\n var cyl = parseInt(pkg.driven_unit.cylinders, 10); if (!cyl || cyl \u003c 1) cyl = 1;\n return [\n { label:\"Crankshaft / main bearing housings\", cls:\"A\", total:SENSOR_RULES.pucksPerLocation, reason:\"main rotating element\" },\n { label:\"Cylinder \u0026 valve assembly\", cls:\"A\", total: cyl * SENSOR_RULES.pucksPerLocation, reason:\"valve failure is the leading cause of downtime\" },\n { label:\"Crosshead\", cls:\"B\", total:SENSOR_RULES.pucksPerLocation, reason:\"converts rotary to linear motion\" },\n { label:\"Packing / rod seal\", cls:\"B\", total: cyl, reason:\"gas leakage / seal wear, one per cylinder\" }\n ];\n },\n \"Centrifugal Compressor\": function(pkg){\n var stagesQty = pkg.driven_unit.stages === \"multi\" ? 2 : 1;\n return [\n { label:\"Main casing shaft bearings (radial)\", cls:\"A\", total: stagesQty * SENSOR_RULES.pucksPerLocation, reason:\"main rotor support\" },\n { label:\"Thrust bearing\", cls:\"A\", total:SENSOR_RULES.pucksPerLocation, reason:\"absorbs axial thrust - distinct failure point\" },\n { label:\"Shaft seal system\", cls:\"B\", total:SENSOR_RULES.pucksPerLocation, reason:\"dry gas / oil seal condition\" }\n ];\n },\n \"Rotary Screw Compressor\": function(pkg){\n return [ { label:\"Screw rotor bearing housings\", cls:\"A\", total:SENSOR_RULES.pucksPerLocation, reason:\"main rotating element\" } ];\n },\n /* v4 additions: Pump and Fan/Blower sub-categories, chosen in Block 5 instead of the initial\n picker (diagnostic #15). Reciprocating pump mirrors the reciprocating-compressor logic\n (valve/plunger wear dominates); Centrifugal pump and Fan/Blower scale with API 610-style\n bearing arrangement (Overhung = one bearing frame, Between Bearings = two, one each side\n of the impeller/wheel); Rotary and Screw pumps get a single main-rotor bearing point,\n matching how the tool already treats Rotary Screw Compressor. */\n \"Reciprocating Pump\": function(pkg){\n var cyl = parseInt(pkg.driven_unit.cylinders, 10); if (!cyl || cyl \u003c 1) cyl = 1;\n return [\n { label:\"Cylinder \u0026 valve/plunger assembly\", cls:\"A\", total: cyl * SENSOR_RULES.pucksPerLocation, reason:\"valve/plunger wear is the leading reciprocating-pump failure mode\" },\n { label:\"Packing / plunger seal\", cls:\"B\", total: cyl, reason:\"leakage / seal wear, one per plunger\" }\n ];\n },\n \"Centrifugal Pump\": function(pkg){\n var qty = pkg.driven_unit.bearingArrangement === \"between\" ? 2 : 1;\n return [ { label:\"Pump bearing housings (\"+(qty===2?\"Between Bearings\":\"Overhung\")+\")\", cls:\"B\",\n total: qty * SENSOR_RULES.pucksPerLocation, reason:\"driven machine bearings\" } ];\n },\n \"Rotary Pump\": function(pkg){\n return [ { label:\"Rotor bearing housings\", cls:\"B\", total:SENSOR_RULES.pucksPerLocation, reason:\"main rotating element\" } ];\n },\n \"Screw Pump\": function(pkg){\n return [ { label:\"Screw rotor bearing housings\", cls:\"B\", total:SENSOR_RULES.pucksPerLocation, reason:\"main rotating element\" } ];\n },\n \"Fan / Blower\": function(pkg){\n var qty = pkg.driven_unit.bearingArrangement === \"between\" ? 2 : 1;\n return [ { label:\"Fan/blower shaft bearings (\"+(qty===2?\"Between Bearings\":\"Overhung\")+\")\", cls:\"B\",\n total: qty * SENSOR_RULES.pucksPerLocation, reason:\"driven machine bearings\" } ];\n },\n \"Crusher\": function(pkg){\n return [ { label:\"Main rotor / shaft bearing housings\", cls:\"A\", total:SENSOR_RULES.pucksPerLocation, reason:\"main rotating element\" } ];\n },\n \"Mill\": function(pkg){\n return [ { label:\"Main shaft / trunnion bearings\", cls:\"A\", total: 2*SENSOR_RULES.pucksPerLocation, reason:\"both ends of the main rotating element\" } ];\n },\n /* v11 addition: Gearbox / Speed Reducer as a Block 5 driven unit was falling through to the\n generic defaultDrivenComponents() single bearing point, with no shaft-count question - the\n same gap the intermediate-stage gearbox already solved (diagnostic #14). Reuses that exact\n pattern: ask for shaft count, default to 2 (input + output) if left blank. */\n \"Gearbox / Speed Reducer\": function(pkg){\n var shafts = parseInt(pkg.driven_unit.shaftCount, 10);\n if (!shafts || shafts \u003c 1) shafts = 2;\n return [ { label:\"Gearbox / speed reducer shaft bearings (\"+shafts+\" shaft\"+(shafts===1?\"\":\"s\")+\")\", cls:\"A\",\n total: shafts * SENSOR_RULES.pucksPerLocation, reason:\"critical drivetrain\" } ];\n }\n};\n/* v4: Pump and Compressor no longer key RULES directly off pkg.machineType (that's just \"Pump\"\n or \"Compressor\" now - the specific engineering category is a Block 5 answer). Everything else\n (Fan/Blower, Crusher, Mill, ...) still keys off machineType exactly as before. */\nfunction drivenRuleKey(pkg){\n if (pkg.machineType === \"Compressor\"){\n return { centrifugal:\"Centrifugal Compressor\", reciprocating:\"Reciprocating Compressor\", screw:\"Rotary Screw Compressor\" }[pkg.driven_unit.category] || null;\n }\n if (pkg.machineType === \"Pump\"){\n return { centrifugal:\"Centrifugal Pump\", reciprocating:\"Reciprocating Pump\", rotary:\"Rotary Pump\", screw:\"Screw Pump\" }[pkg.driven_unit.category] || null;\n }\n return pkg.machineType;\n}\nfunction drivenCoupling(pkg){\n /* With an intermediate stage, Coupling 1 ends at that stage and Coupling 2 joins the driven unit. */\n var hasInter = pkg.intermediateAnswer === \"yes\" \u0026\u0026 pkg.intermediates.length \u003e 0;\n return pkg.coupling_nodes[hasInter ? 1 : 0];\n}\nfunction defaultDrivenComponents(pkg){\n if (drivenCoupling(pkg).type === \"direct\") return [];\n var lbl = \"Driven machine\";\n if (/Fan/.test(pkg.machineType)) lbl = \"Fan shaft\";\n else if (/Pump/.test(pkg.machineType)) lbl = \"Pump shaft\";\n else if (/Compressor/.test(pkg.machineType)) lbl = \"Compressor rotor\";\n return [ { label:lbl+\" bearings\", cls:\"B\", total:SENSOR_RULES.pucksPerLocation, reason:\"driven machine bearings\" } ];\n}\nfunction calcComponents(pkg){\n var comps = [];\n pkg.drivers.forEach(function(d, i){\n comps.push({ label:(d.driver_type.charAt(0).toUpperCase()+d.driver_type.slice(1)) + (pkg.drivers.length\u003e1 ? \" #\"+(i+1) : \"\"),\n cls:\"A\", total: pucksForDriver(d, pkg), reason:\"primary drive\", block:\"driver\", key:\"driver-\"+i });\n });\n var interComps = intermediateComponents(pkg);\n interComps.forEach(function(c, idx){ c.block = \"inter\"; c.key = \"inter-\"+idx; });\n comps = comps.concat(interComps);\n var rule = RULES[drivenRuleKey(pkg)];\n var drivenComps = rule ? rule(pkg) : defaultDrivenComponents(pkg);\n drivenComps.forEach(function(c, idx){ c.block = \"driven\"; c.key = \"driven-\"+idx; });\n comps = comps.concat(drivenComps);\n return comps;\n}\n/* v4: every component's puck count can be manually overridden in the Review screen (diagnostic\n #16) - \"maybe I like to put 4 sensors on my gearbox, or only 2 would be enough\". The override\n lives on the package (keyed by the component's stable key), never mutates the calculated\n default, and is what every downstream total (vib count, price, tree preview) actually reads. */\nfunction effectivePucks(pkg, c){\n return (pkg.puckOverrides \u0026\u0026 pkg.puckOverrides[c.key] != null) ? pkg.puckOverrides[c.key] : c.total;\n}\nfunction calcVib(pkg){ return calcComponents(pkg).reduce(function(s,c){ return s + effectivePucks(pkg, c); }, 0); }\nfunction driverMcaEligible(d, pkg){\n if (d.driver_type !== \"motor\") return false;\n if (d.mcc_panel_access !== \"yes\") return false;\n return pkg.criticality === \"very\" || pkg.criticality === \"high\";\n}\nfunction mcaCharged(pkg){\n return pkg.drivers.some(function(d){ return driverMcaEligible(d, pkg); });\n}\nfunction priceFor(pkg){\n var v = calcVib(pkg);\n var extraSensors = Math.max(0, v - CONFIG.BLOCK_SIZE);\n var hasMca = mcaCharged(pkg);\n var base = (v \u003e 0 || !hasMca) ? CONFIG.RATE : 0;\n var vib = base + extraSensors * CONFIG.EXTRA_SENSOR_MONTHLY;\n var mca = hasMca ? (v \u003e 0 ? CONFIG.MCA_MONTHLY : CONFIG.MCA_STANDALONE_MONTHLY) : 0;\n return { base:base, vib:vib, mca:mca, mcaTier:hasMca ? (v \u003e 0 ? \"with_vibration\" : \"standalone\") : \"none\",\n edge:0, discount:0, extraSensors:extraSensors, total:vib + mca };\n}\nfunction totals(){\n var t = { assets:0, pucks:0, monthly:0, gateways:0 };\n assets.forEach(function(pkg){\n var n = Math.max(1, pkg.instanceCount||1);\n var p = priceFor(pkg), v = calcVib(pkg);\n t.assets += n; t.pucks += v*n; t.monthly += p.total*n;\n });\n t.gateways = t.assets === 0 ? 0 : Math.max(1, Math.ceil(t.pucks / CONFIG.GATEWAY_CAPACITY));\n return t;\n}\nfunction money(n){ return \"$\" + Math.round(n).toLocaleString(\"en-US\"); }\n\n/* ================= v10: LEAD CAPTURE, AUTOSAVE, SHAREABLE LINK, QUOTE EXPORT =================\n Tier 1 sales-readiness additions, added together since all three lean on the same underlying\n piece: a full, round-trippable snapshot of {plant, assets}. No backend exists for this tool, so\n \"save and resume\" and \"shareable link\" are the same mechanism aimed at two different places to\n read from - localStorage for the browser coming back on its own, the URL hash for handing the\n exact configuration to someone else (or opening it on another device). Both are additive: they\n never change what calcComponents/priceFor/totals compute, only when the tool reads/writes state\n around them, so none of the existing pricing or drivetrain-gating logic needed touching. */\nfunction isValidEmail(s){\n var t = (s || \"\").trim();\n if (!t || t.length \u003e 254) return false;\n var at = t.indexOf(\"@\");\n if (at \u003c 1 || at !== t.lastIndexOf(\"@\")) return false;\n var local = t.slice(0, at);\n var domain = t.slice(at + 1);\n if (local.length \u003e 64 || !domain) return false;\n if (local.charAt(0) === \".\" || local.charAt(local.length - 1) === \".\") return false;\n if (local.indexOf(\"..\") !== -1) return false;\n if (!/^[A-Za-z0-9.!#$%\u0026'*+/=?^_`{|}~-]+$/.test(local)) return false;\n var labels = domain.split(\".\");\n if (labels.length \u003c 2) return false;\n for (var i = 0; i \u003c labels.length; i++){\n var lab = labels[i];\n if (!lab || lab.length \u003e 63) return false;\n if (lab.charAt(0) === \"-\" || lab.charAt(lab.length - 1) === \"-\") return false;\n if (!/^[A-Za-z0-9-]+$/.test(lab)) return false;\n }\n if (!/^[A-Za-z]{2,}$/.test(labels[labels.length - 1])) return false;\n return true;\n}\nfunction plantEmailError(s){\n if (!(s || \"\").trim()) return \"Enter a valid email so we can send the quote.\";\n if (!isValidEmail(s)) return \"That email doesn't look valid. Use a format like name@company.com.\";\n return \"\";\n}\nfunction leadCaptured(){ return !!(plant.contactName \u0026\u0026 plant.contactName.trim()) \u0026\u0026 isValidEmail(plant.contactEmail); }\n\nvar AUTOSAVE_KEY = \"chladni_plant_builder_autosave_v14\";\n/* v15: full configuration sharing; existing package and sensor schema are unchanged. */\nfunction snapshotState(){\n return { version:15, plant:plant, assets:assets, draft:draft, editingId:editingId,\n appStage:appStage, collapsedZones:collapsedZones, quoteNumber:quoteNumber,\n quoteIssuedAt:quoteIssuedAt, pkgSeq:pkgSeq, driverSeq:driverSeq, interSeq:interSeq };\n}\nfunction encodeState(state){\n try { return btoa(unescape(encodeURIComponent(JSON.stringify(state || snapshotState())))); }\n catch(e){ return null; }\n}\nfunction decodeState(s){\n try { return JSON.parse(decodeURIComponent(escape(atob(s)))); }\n catch(e){ return null; }\n}\n\nvar lastSavedAt = null;\nfunction persistLocal(){\n try {\n var enc = encodeState();\n if (enc){ localStorage.setItem(AUTOSAVE_KEY, enc); lastSavedAt = new Date(); }\n }\n catch(e){ /* private browsing / storage disabled - autosave just silently no-ops */ }\n}\nfunction savedStatusText(){\n if (!lastSavedAt) return \"Not saved yet\";\n return \"Saved \" + lastSavedAt.toLocaleTimeString(\"en-US\", { hour:\"numeric\", minute:\"2-digit\" });\n}\n/* v11: persistLocal() already ran silently on every render (renderTree(), plant-field edits,\n beforeunload) since v10 - but \"silently\" turned out to be the problem the client actually hit:\n there was no visible confirmation anywhere that anything had been saved at all, so from the\n outside it looked like the feature didn't exist. This is an explicit, visible \"Save progress\"\n action wired to the exact same persistLocal()/AUTOSAVE_KEY mechanism, not a second save path -\n clicking it just makes the existing save happen on demand and shows that it happened. */\nfunction saveProgressNow(btn){\n persistLocal();\n track(\"plant_builder_progress_saved\");\n if (!btn) return;\n var old = btn.textContent;\n btn.textContent = \"Saved \u2713\";\n setTimeout(function(){ btn.textContent = old; }, 1600);\n var status = document.getElementById(btn.getAttribute(\"data-status-target\"));\n if (status) status.textContent = savedStatusText();\n}\n/* Restoring a saved snapshot brings back plain data only (see newPackage()/newDriver() - no\n functions are ever stored on a package), so nothing here needs to touch the drivetrain-gating\n or pricing functions. The one thing that does need care is the id sequence counters: a restored\n asset already carries ids like \"pkg3\"/\"drv2\", and if driverSeq/pkgSeq/interSeq reset to 1 on\n reload, the next NEW driver or asset created after a restore could mint an id that collides\n with a restored one. Fixed by taking whichever is larger: the saved counter, or one past the\n highest numeric suffix actually found in the restored data. */\nfunction reseedCounters(state){\n var maxPkg = 0, maxDrv = 0, maxInt = 0;\n (state.assets||[]).concat(state.draft ? [state.draft] : []).forEach(function(pkg){\n var m = /^pkg(\\d+)$/.exec(pkg.id||\"\"); if (m) maxPkg = Math.max(maxPkg, parseInt(m[1],10));\n (pkg.drivers||[]).forEach(function(d){ var md = /^drv(\\d+)$/.exec(d.id||\"\"); if (md) maxDrv = Math.max(maxDrv, parseInt(md[1],10)); });\n (pkg.intermediates||[]).forEach(function(it){ var mi = /^int(\\d+)$/.exec(it.id||\"\"); if (mi) maxInt = Math.max(maxInt, parseInt(mi[1],10)); });\n });\n pkgSeq = Math.max(typeof state.pkgSeq === \"number\" ? state.pkgSeq : 1, maxPkg + 1);\n driverSeq = Math.max(typeof state.driverSeq === \"number\" ? state.driverSeq : 1, maxDrv + 1);\n interSeq = Math.max(typeof state.interSeq === \"number\" ? state.interSeq : 1, maxInt + 1);\n}\nfunction applyRestoredState(state){\n var basePlant = { company:\"\", name:\"\", industry:INDUSTRIES[0], country:\"United States\", state:\"\", contactName:\"\", contactEmail:\"\", _err:\"\", _emailErr:\"\" };\n var incoming = state.plant || {};\n for (var k in incoming){ if (incoming.hasOwnProperty(k)) basePlant[k] = incoming[k]; }\n plant = basePlant;\n assets = state.assets || [];\n draft = state.draft || null;\n editingId = state.editingId || null;\n appStage = state.appStage === 'app' ? 'app' : 'plant';\n collapsedZones = state.collapsedZones || {};\n quoteNumber = state.quoteNumber || null;\n quoteIssuedAt = state.quoteIssuedAt \u0026\u0026 !isNaN(Date.parse(state.quoteIssuedAt)) ? state.quoteIssuedAt : null;\n reseedCounters(state);\n if (editingId){\n var stillThere = assets.some(function(pkg){ return pkg.id === editingId; });\n if (!stillThere){ draft = null; editingId = null; }\n }\n}\nfunction shareLinkUrl(view){\n var state = snapshotState();\n if (view) state.view = view;\n var enc = encodeState(state);\n if (!enc) return null;\n return location.origin + location.pathname + location.search + \"#s=\" + enc;\n}\nvar quoteNumber = null;\nvar quoteIssuedAt = null;\nvar quoteReturnFocus = null;\nvar quoteView = \"quote\";\n\nfunction ensureQuoteNumber(){\n if (!quoteNumber) quoteNumber = \"CHL-\" + Date.now().toString(36).toUpperCase();\n if (!quoteIssuedAt) quoteIssuedAt = new Date().toISOString();\n return quoteNumber;\n}\n\n/* The explicit review action uses the native Webflow form and confirms acceptance. */\nfunction setField(form, name, value){\n var f = form.querySelector('[name=\"' + name + '\"]');\n if (!f) return;\n f.value = String(value == null ? \"\" : value);\n}\nfunction fmtDate(d){ return d.toLocaleDateString(\"en-US\",{year:\"numeric\",month:\"long\",day:\"numeric\"}); }\nfunction textEl(tag,cls,text){ var node = el(tag,cls); if (text != null) node.textContent = String(text); return node; }\nfunction actionButton(label,cls,fn){ var b = textEl(\"button\",cls,label); b.type = \"button\"; b.addEventListener(\"click\",fn); return b; }\nfunction priceRow(label,value,cls){\n var row = el(\"div\",cls || \"qPriceRow\"); row.appendChild(textEl(\"span\",null,label)); row.appendChild(textEl(\"b\",null,value)); return row;\n}\nfunction motorCurrentPricingNote(price){\n return price.mcaTier===\"with_vibration\" ?\n \"Motor current sensor monitoring is $79/month because this asset also has vibration sensors.\" :\n \"Motor current sensor monitoring is $120/month on its own. No vibration monitoring charge is added to this asset.\";\n}\nfunction monitoringPriceSummary(){\n var result={baseAssets:0,baseMonthly:0,extraPoints:0,extraMonthly:0,pairedAssets:0,pairedMonthly:0,standaloneAssets:0,standaloneMonthly:0};\n assets.forEach(function(pkg){\n var p=priceFor(pkg), n=Math.max(1,pkg.instanceCount||1);\n if(p.base){result.baseAssets+=n;result.baseMonthly+=p.base*n;}\n result.extraPoints+=p.extraSensors*n; result.extraMonthly+=p.extraSensors*CONFIG.EXTRA_SENSOR_MONTHLY*n;\n if(p.mcaTier===\"with_vibration\"){result.pairedAssets+=n;result.pairedMonthly+=p.mca*n;}\n if(p.mcaTier===\"standalone\"){result.standaloneAssets+=n;result.standaloneMonthly+=p.mca*n;}\n });\n return result;\n}\nfunction appendQuoteAsset(pkg,host,inCart){\n var n = Math.max(1,pkg.instanceCount||1), count = calcVib(pkg), p = priceFor(pkg);\n var card = el(\"section\",\"qAsset\"), head = el(\"div\",\"qAssetHead\"), title = el(\"div\");\n title.appendChild(textEl(\"h3\",null,pkg.name));\n title.appendChild(textEl(\"p\",\"qAssetMeta\",[pkg.zone || \"Unassigned area\",pkg.machineType || \"Equipment\",critLabel(pkg.criticality)+\" criticality\"].join(\" \u00b7 \")));\n head.appendChild(title);\n var amount = el(\"div\",\"qAssetPrice\"); amount.appendChild(textEl(\"b\",null,money(p.total*n))); amount.appendChild(textEl(\"span\",null,\" / month\")); head.appendChild(amount); card.appendChild(head);\n card.appendChild(textEl(\"p\",\"qAssetMeta\",n+\" asset\"+(n===1?\"\":\"s\")+\" \u00d7 \"+count+\" vibration sensors each \u00b7 \"+count*n+\" vibration sensors total\"));\n card.appendChild(textEl(\"p\",\"qCartBadge\",p.mcaTier===\"standalone\" ? \"Motor current monitoring only\" : p.extraSensors ? \"Large asset \u00b7 extended measurement coverage\" : \"Standard asset coverage\"));\n var breakdown = el(\"div\",\"qBreakdown\");\n if(p.base) breakdown.appendChild(priceRow(n+\" \u00d7 \"+money(p.base)+\" asset base\",money(p.base*n)));\n if (p.extraSensors) breakdown.appendChild(priceRow(p.extraSensors*n+\" additional measurement points \u00d7 \"+money(CONFIG.EXTRA_SENSOR_MONTHLY)+\"/month\",money(p.extraSensors*n*CONFIG.EXTRA_SENSOR_MONTHLY)));\n if(p.mca) breakdown.appendChild(priceRow(\"Motor current sensor \u00b7 \"+(p.mcaTier===\"with_vibration\"?\"with vibration\":\"standalone\")+\" (\"+n+\" \u00d7 \"+money(p.mca)+\"/month)\",money(p.mca*n)));\n card.appendChild(breakdown);\n var detail = el(\"details\",\"qDetails\"); detail.open = true;\n detail.appendChild(textEl(\"summary\",null,\"Drivetrain \u0026 sensor allocation\"));\n detail.appendChild(textEl(\"p\",\"qChain\",drivetrainChainSummary(pkg).join(\" -\u003e \")));\n var table = el(\"table\",\"qSensorTable\");\n var caption = textEl(\"caption\",null,\"Sensor allocation per asset\"); table.appendChild(caption);\n var thead = el(\"thead\"), hr = el(\"tr\");\n [\"Component\",\"Vibration sensors\"].forEach(function(s){ hr.appendChild(textEl(\"th\",null,s)); }); thead.appendChild(hr); table.appendChild(thead);\n var body = el(\"tbody\");\n calcComponents(pkg).forEach(function(c){\n var row = el(\"tr\"), label = c.label;\n if (pkg.puckOverrides \u0026\u0026 pkg.puckOverrides[c.key] != null) label += \" (custom count)\";\n row.appendChild(textEl(\"td\",null,label)); row.appendChild(textEl(\"td\",\"num\",effectivePucks(pkg,c))); body.appendChild(row);\n });\n var total = el(\"tr\"); total.appendChild(textEl(\"th\",null,\"Total per asset\")); total.appendChild(textEl(\"th\",\"num\",count)); body.appendChild(total);\n table.appendChild(body); detail.appendChild(table);\n detail.appendChild(textEl(\"p\",\"hint\",\"Couplings do not add vibration sensors.\"+(p.base?\" The $79/month asset base includes up to four measurement points.\":\"\")));\n if (p.extraSensors) detail.appendChild(textEl(\"p\",\"hint\",\"This is a large asset. Extended coverage is $18 per additional measurement point per month.\"));\n if(p.mca) detail.appendChild(textEl(\"p\",\"hint\",motorCurrentPricingNote(p)));\n var specs = el(\"details\",\"qDetails\"); specs.appendChild(textEl(\"summary\",null,\"View equipment specifications\"));\n pkg.drivers.forEach(function(d,i){\n specs.appendChild(textEl(\"p\",\"hint\",\"Motor \"+(i+1)+\" \u00b7 \"+d.horsepower+\" HP \u00b7 \"+d.speed_rpm+\" RPM \u00b7 VFD: \"+(d.control_type===\"vfd\"?\"Yes\":d.control_type===\"fixed\"?\"No\":d.control_type)+\" \u00b7 MCC access: \"+d.mcc_panel_access));\n var optional = [[\"Insulation\",d.insulation_class],[\"Voltage\",d.voltage_level],[\"Amperage\",d.amperage],[\"Frequency\",d.line_freq]].filter(function(x){return x[1]!==\"\" \u0026\u0026 x[1]!=null;});\n if (optional.length) specs.appendChild(textEl(\"p\",\"hint\",optional.map(function(x){return x[0]+\": \"+x[1];}).join(\" \u00b7 \")));\n });\n pkg.intermediates.forEach(function(it,i){ specs.appendChild(textEl(\"p\",\"hint\",\"Intermediate \"+(i+1)+\": \"+it.type+(it.shaftCount?\" \u00b7 \"+it.shaftCount+\" shafts\":\"\"))); });\n var du = pkg.driven_unit;\n var duFields = [[\"Category\",du.category],[\"Cylinders\",du.cylinders],[\"Stages\",du.stages],[\"Shafts\",du.shaftCount],[\"Bearing arrangement\",du.bearingArrangement]].filter(function(x){return x[1]!==\"\" \u0026\u0026 x[1]!=null;});\n if (duFields.length) specs.appendChild(textEl(\"p\",\"hint\",duFields.map(function(x){return x[0]+\": \"+x[1];}).join(\" \u00b7 \")));\n detail.appendChild(specs); card.appendChild(detail);\n if (inCart){\n var controls=el(\"div\",\"cartNav\"), qty=el(\"div\",\"stepper\");\n qty.appendChild(textEl(\"span\",null,\"Quantity\"));\n var minus=actionButton(\"\u2212\",\"\",function(){changeCartQuantity(pkg,-1);}); minus.disabled=n\u003c=1; minus.setAttribute(\"aria-label\",\"Reduce quantity of \"+pkg.name);\n var plus=actionButton(\"+\",\"\",function(){changeCartQuantity(pkg,1);}); plus.disabled=n\u003e=999; plus.setAttribute(\"aria-label\",\"Increase quantity of \"+pkg.name);\n qty.appendChild(minus); qty.appendChild(textEl(\"span\",\"sn\",n)); qty.appendChild(plus); controls.appendChild(qty);\n controls.appendChild(actionButton(\"Edit configuration\",\"btn ghost\",function(){closeQuote();editAsset(pkg);}));\n card.appendChild(controls);\n }\n host.appendChild(card);\n}\n/* The Webflow mount markup has no #brandLogo, so the quote sheet uses the site's own hosted logo. */\nvar BRAND_LOGO_URL = \"https://cdn.prod.website-files.com/6a9f32e11134ce9e4f4dde44/6aa15e3af1646cf5f0c5a114_chladni-logo.svg\";\nfunction brandLogoImg(cls){\n var logo = document.createElement(\"img\");\n var pageLogo = document.getElementById(\"brandLogo\");\n logo.className = cls; logo.src = pageLogo ? pageLogo.src : BRAND_LOGO_URL;\n logo.alt = \"Chladni\"; logo.width = 176; logo.height = 22;\n return logo;\n}\nfunction renderQuoteSheet(){\n if (typeof teamReviewConfirmationState!==\"undefined\") teamReviewConfirmationState=null;\n ensureQuoteNumber();\n var host=document.getElementById(\"quoteSheet\"); host.innerHTML=\"\";\n var today=new Date(quoteIssuedAt), t=totals();\n var top=el(\"div\",\"qTop\"),brand=el(\"div\",\"qBrand\");\n brand.appendChild(brandLogoImg(\"brandLogo\"));top.appendChild(brand);\n var meta=el(\"div\",\"qMeta\");meta.appendChild(textEl(\"div\",null,quoteNumber));meta.appendChild(textEl(\"div\",null,\"Prepared \"+fmtDate(today)));top.appendChild(meta);\n var close=actionButton(\"\u00d7\",\"qClose\",closeQuote);close.setAttribute(\"aria-label\",\"Close results\");top.appendChild(close);host.appendChild(top);\n var heading=el(\"div\",\"qHeading\");heading.appendChild(textEl(\"p\",\"qEyebrow\",\"YOUR PLANT. YOUR MONITORING PLAN.\"));\n var title=textEl(\"h1\",\"qTitle\",\"Your results.\");title.id=\"quoteTitle\";heading.appendChild(title);\n heading.appendChild(textEl(\"p\",\"qSubtitle\",(plant.company||\"Your company\")+(plant.name?\" \u00b7 \"+plant.name:\"\")));host.appendChild(heading);\n var stats=el(\"div\",\"qStats\");\n [[t.assets,\"Assets\"],[t.pucks,\"Vibration sensors\"],[t.gateways,\"Gateways\"]].forEach(function(x){var stat=el(\"div\",\"qStat\");stat.appendChild(textEl(\"b\",null,x[0]));stat.appendChild(textEl(\"span\",null,x[1]));stats.appendChild(stat);});\n var grid=el(\"div\",\"qGrid\"),main=el(\"div\",\"qMain\");main.appendChild(stats);main.appendChild(textEl(\"h2\",\"qSection\",\"Your assets \u0026 coverage\"));\n main.appendChild(textEl(\"p\",\"qCartNotice\",\"Your saved equipment, quantities and sensor allocation, together in one place.\"));\n if(hasPendingCartChanges()){\n var pending=el(\"div\",\"cartTerms\");pending.appendChild(textEl(\"p\",null,\"You have unsaved changes. Save them in the builder to include them in these results.\"));\n pending.appendChild(actionButton(\"Return to unsaved changes\",\"btn ghost\",closeQuote));main.appendChild(pending);\n }\n assets.forEach(function(pkg){appendQuoteAsset(pkg,main,true);});\n var contact=el(\"section\",\"qContact\");contact.appendChild(textEl(\"h2\",\"qSection\",\"Contact \u0026 plant information\"));\n contact.appendChild(textEl(\"p\",null,[plant.company,plant.name].filter(Boolean).join(\" \u00b7 \")));\n contact.appendChild(textEl(\"b\",null,plant.contactName));contact.appendChild(textEl(\"p\",null,plant.contactEmail));\n contact.appendChild(textEl(\"p\",\"hint\",[plant.industry,plant.state,plant.country].filter(Boolean).join(\" \u00b7 \")));\n contact.appendChild(actionButton(\"Edit contact \u0026 plant details\",\"btn ghost\",function(){closeQuote();showPlantStage();}));\n if(!leadCaptured())contact.appendChild(textEl(\"p\",\"fldErr\",\"Add your name and a valid email so our team can reach you.\"));\n main.appendChild(contact);grid.appendChild(main);\n var summary=el(\"aside\",\"qSummary\");summary.appendChild(textEl(\"p\",\"qEyebrow\",\"ESTIMATED MONTHLY COST\"));\n var hero=el(\"div\",\"qHero\");hero.appendChild(textEl(\"span\",\"qAmount\",money(t.monthly)));hero.appendChild(textEl(\"span\",\"qPeriod\",\"USD / month\"));summary.appendChild(hero);\n var charges=monitoringPriceSummary();\n if(charges.baseAssets)summary.appendChild(priceRow(charges.baseAssets+\" asset bases \u00d7 \"+money(CONFIG.RATE),money(charges.baseMonthly)));\n if(charges.extraPoints)summary.appendChild(priceRow(charges.extraPoints+\" additional measurement points \u00d7 \"+money(CONFIG.EXTRA_SENSOR_MONTHLY)+\"/month\",money(charges.extraMonthly)));\n if(charges.pairedAssets)summary.appendChild(priceRow(\"Motor current with vibration \u00b7 \"+charges.pairedAssets+\" \u00d7 \"+money(CONFIG.MCA_MONTHLY)+\"/month\",money(charges.pairedMonthly)));\n if(charges.standaloneAssets)summary.appendChild(priceRow(\"Motor current only \u00b7 \"+charges.standaloneAssets+\" \u00d7 \"+money(CONFIG.MCA_STANDALONE_MONTHLY)+\"/month\",money(charges.standaloneMonthly)));\n summary.appendChild(priceRow(\"Monthly total\",money(t.monthly),\"qTotalRow\"));\n if(t.monthly\u003e=1)summary.appendChild(textEl(\"p\",\"hint\",\"Annual equivalent: \"+money(Math.round(t.monthly)*12)+\". Our team will confirm the scope, pricing and terms with you.\"));\n if(charges.baseAssets)summary.appendChild(textEl(\"p\",\"hint\",\"Vibration monitoring starts at $79 per asset per month, including up to 4 measurement points. Additional measurement points are $18 per month, per point.\"));\n if(charges.pairedAssets||charges.standaloneAssets)summary.appendChild(textEl(\"p\",\"hint\",\"Motor current monitoring is $79/month alongside vibration on the same asset, or $120/month on its own.\"));\n var actions=el(\"div\",\"quoteActionsRow\");\n var review=actionButton(teamReviewBusy?\"Sending\u2026\":\"Submit to Chladni team for review\",\"btn\",requestTeamReviewUI);review.id=\"teamReviewBtn\";review.disabled=teamReviewBusy;actions.appendChild(review);\n var reviewNote=textEl(\"p\",\"qActionNote\",\"Send these results to Chladni. We\u2019ll review your configuration and reach out to you.\");actions.appendChild(reviewNote);\n var status=textEl(\"p\",\"actionStatus\",teamReviewStatusText());status.id=\"teamReviewStatus\";status.setAttribute(\"role\",\"status\");status.setAttribute(\"aria-live\",\"polite\");actions.appendChild(status);\n actions.appendChild(actionButton(\"Download results\",\"btn ghost\",downloadQuotePage));\n var forward=actionButton(\"Forward results\",\"btn ghost\",forwardResults);forward.setAttribute(\"data-forward-results\",\"\");actions.appendChild(forward);\n var resultStatus=textEl(\"p\",\"actionStatus\",\"\");resultStatus.id=\"resultsActionStatus\";resultStatus.setAttribute(\"role\",\"status\");resultStatus.setAttribute(\"aria-live\",\"polite\");actions.appendChild(resultStatus);\n summary.appendChild(actions);\n if(!cartReady())summary.appendChild(textEl(\"p\",\"fldErr\",\"Complete every asset\u2019s configuration before requesting a review.\"));\n summary.appendChild(actionButton(\"Print / save PDF\",\"qTextButton\",function(){window.print();}));\n summary.appendChild(actionButton(\"Back to plant builder\",\"qTextButton\",closeQuote));\n summary.appendChild(textEl(\"p\",\"qFooter\",\"An estimate based on your configuration. Sending it for review does not place an order or take a payment.\"));\n grid.appendChild(summary);host.appendChild(grid);\n var share=el(\"section\",\"qSharePanel\");share.id=\"sharePanel\";share.hidden=true;host.appendChild(share);\n}\nfunction setQuoteBackgroundInert(on){\n [\"appgrid\",\"plantScreenOuter\",\"resumeBanner\"].forEach(function(id){ var node = document.getElementById(id); if (node) node.inert = on; });\n var bar = document.querySelector(\"#chladni-plant-builder .topbar\") || document.querySelector(\".topbar\");\n if (bar) bar.inert = on;\n}\nfunction reviewResults(){ openQuote(\"quote\"); }\nfunction openQuote(view){\n var nextView = \"quote\";\n if (!assets.length) return;\n var overlay = document.getElementById(\"quoteOverlay\");\n var wasOpen = overlay.classList.contains(\"on\"), prevView = quoteView;\n quoteView = nextView;\n if (!wasOpen) quoteReturnFocus = document.activeElement;\n renderQuoteSheet(); overlay.classList.add(\"on\"); document.body.style.overflow = \"hidden\";\n setQuoteBackgroundInert(true);\n overlay.onclick = function(e){ if (e.target === overlay) closeQuote(); };\n overlay.querySelector(\".qClose\").focus();\n persistLocal();\n /* Cart quantity changes re-render the open sheet; only a real open or view switch counts. */\n if (wasOpen \u0026\u0026 prevView === nextView) return;\n var qt = totals();\n track(\"plant_builder_results_opened\", { quote_id: ensureQuoteNumber(), monthly_total: qt.monthly, asset_count: qt.assets, puck_count: qt.pucks });\n}\nfunction closeQuote(){\n if(typeof teamReviewConfirmationState!==\"undefined\" \u0026\u0026 teamReviewConfirmationState) returnFromTeamReviewConfirmation(false);\n document.getElementById(\"quoteOverlay\").classList.remove(\"on\"); document.body.style.overflow = \"\";\n setQuoteBackgroundInert(false);\n if (quoteReturnFocus \u0026\u0026 quoteReturnFocus.isConnected) quoteReturnFocus.focus();\n}\ndocument.addEventListener(\"keydown\",function(event){\n var overlay = document.getElementById(\"quoteOverlay\"); if (!overlay.classList.contains(\"on\")) return;\n if (event.key === \"Tab\"){\n var items = Array.prototype.slice.call(overlay.querySelectorAll(\"button,summary,textarea,a[href],input\")).filter(function(e){ return !e.disabled \u0026\u0026 e.getClientRects().length; });\n if (!items.length) return;\n var first = items[0], last = items[items.length-1];\n if (event.shiftKey \u0026\u0026 document.activeElement === first){ event.preventDefault(); last.focus(); }\n else if (!event.shiftKey \u0026\u0026 document.activeElement === last){ event.preventDefault(); first.focus(); }\n }\n});\nvar printedDetails = [];\nwindow.addEventListener(\"beforeprint\",function(){\n printedDetails = Array.prototype.slice.call(document.querySelectorAll(\"#quoteSheet details\")).map(function(node){ var entry = {node:node,open:node.open}; node.open = true; return entry; });\n});\nwindow.addEventListener(\"afterprint\",function(){ printedDetails.forEach(function(entry){ entry.node.open = entry.open; }); printedDetails = []; });\n\n/* The saved asset hierarchy is the cart's single source of truth. */\nfunction cartConfiguration(pkg){\n var copy=JSON.parse(JSON.stringify(pkg));\n [\"activeTab\",\"maxUnlocked\",\"advancedOpen\",\"_nameTouched\"].forEach(function(key){delete copy[key];});\n return copy;\n}\nfunction hasPendingCartChanges(){\n if(!draft) return false;\n var saved=assets.find(function(pkg){return pkg.id===editingId;});\n if(!editingId \u0026\u0026 (!draft.name || !draft.name.trim())) return false;\n return !saved || JSON.stringify(cartConfiguration(draft))!==JSON.stringify(cartConfiguration(saved));\n}\nfunction changeCartQuantity(pkg,delta){\n if(draft \u0026\u0026 editingId===pkg.id) draft.instanceCount=clamp((pkg.instanceCount||1)+delta,1,999);\n stepInstance(pkg,delta);\n openQuote(\"quote\");\n}\nfunction cartReady(){\n return assets.length\u003e0 \u0026\u0026 assets.every(function(pkg){return baselineValid(pkg) \u0026\u0026 allTabsComplete(pkg) \u0026\u0026 verifyDrivetrainPath(pkg).ok;});\n}\nfunction commitAssetToCart(pkg){\n if(!baselineValid(pkg) || !allTabsComplete(pkg) || !verifyDrivetrainPath(pkg).ok) return false;\n var idx=assets.findIndex(function(item){return item.id===(editingId || pkg.id);});\n if(editingId){if(idx\u003c0)return false; assets[idx]=pkg;}\n else {if(idx\u003e=0)return false; assets.push(pkg);}\n draft=null; editingId=null;\n return true;\n}\n/* ================= TAB DEFINITIONS \u0026 GATING =================\n Execution rule: all 5 blocks must be traversed and valid before the\n asset can be finalized. Each validator explicitly handles empty arrays. */\nvar TABS = [\n { key:\"driver\", label:\"Driver\", n:\"Block 1\" },\n { key:\"coupling1\",label:\"Coupling 1\", n:\"Block 2\" },\n { key:\"inter\", label:\"Intermediate\",n:\"Block 3\" },\n { key:\"coupling2\",label:\"Coupling 2\", n:\"Block 4\" },\n { key:\"driven\", label:\"Driven Unit\", n:\"Block 5\" }\n];\nfunction tabValid(pkg, key){\n if (key === \"driver\"){\n if (!pkg.drivers || pkg.drivers.length === 0) return false;\n return pkg.drivers.every(function(d){\n if (!d.driver_type) return false;\n /* v13: a motor's vital size question is Horsepower + Operating speed (RPM), not kW - see\n motorSensorCount()'s comment. Motors are the only driver type this tool covers. */\n var hp = parseFloat(d.horsepower), rpm = parseFloat(d.speed_rpm);\n if (isNaN(hp) || hp \u003c= 0) return false;\n if (isNaN(rpm) || rpm \u003c= 0) return false;\n /* VFD? and MCC/panel access are both vital for a motor now, not optional (diagnostic #13) -\n MCC access determines whether electrical monitoring can actually be delivered at all. */\n if (!d.control_type || !d.mcc_panel_access) return false;\n return true;\n });\n }\n if (key === \"coupling1\"){\n return !!pkg.coupling_nodes[0].type \u0026\u0026 !!pkg.coupling_nodes[0].orientation;\n }\n if (key === \"inter\"){\n if (pkg.intermediateAnswer === \"no\") return true;\n if (pkg.intermediateAnswer !== \"yes\") return false;\n if (!pkg.intermediates.length) return false;\n /* v4: each stage is a single element type (gearbox needs a shaft count too), and once\n there is more than one stage, the coupling between every consecutive pair must also be\n described - diagnostic #14. */\n var stagesOk = pkg.intermediates.every(function(inter){\n if (!inter.type) return false;\n if (inter.type === \"gearbox\" \u0026\u0026 !inter.shaftCount) return false;\n return true;\n });\n if (!stagesOk) return false;\n if (pkg.intermediates.length \u003e 1){\n for (var k=0; k\u003cpkg.intermediates.length-1; k++){\n var jc = pkg.interCouplings[k];\n if (!jc || !jc.type || !jc.orientation) return false;\n }\n }\n return true;\n }\n if (key === \"coupling2\"){\n if (pkg.intermediateAnswer !== \"yes\" || pkg.intermediates.length === 0) return true; /* auto-complete: nothing to bound */\n return !!pkg.coupling_nodes[1].type \u0026\u0026 !!pkg.coupling_nodes[1].orientation;\n }\n if (key === \"driven\"){\n /* v5: the equipment type itself is now chosen inside this block (diagnostic #18) - nothing\n downstream (including this validator) can assume a type has been picked yet.\n v11: per client request, picking an equipment type is no longer mandatory to complete this\n block - a prospect who isn't sure, or whose machine isn't in the catalog, can leave it blank\n and still finish the asset. Skipping it just means calcComponents() falls through to\n defaultDrivenComponents() (already null-safe on an empty machineType - see its own comment)\n instead of a type-specific rule set. The instant a type IS picked, though, that type's own\n follow-up questions (category, bearing arrangement, cylinder count, ...) are still required\n exactly as before - optional is \"skip the picker entirely,\" not \"half-answer it.\" */\n if (!pkg.machineType) return true;\n if (pkg.machineType === \"Compressor\"){\n if (!pkg.driven_unit.category) return false;\n if (pkg.driven_unit.category === \"reciprocating\" \u0026\u0026 !pkg.driven_unit.cylinders) return false;\n if (pkg.driven_unit.category === \"centrifugal\" \u0026\u0026 !pkg.driven_unit.stages) return false;\n return true;\n }\n if (pkg.machineType === \"Pump\"){\n if (!pkg.driven_unit.category) return false;\n if (!pkg.driven_unit.bearingArrangement) return false;\n if (pkg.driven_unit.category === \"reciprocating\" \u0026\u0026 !pkg.driven_unit.cylinders) return false;\n return true;\n }\n if (pkg.machineType === \"Fan / Blower\"){\n return !!pkg.driven_unit.bearingArrangement;\n }\n if (pkg.machineType === \"Gearbox / Speed Reducer\"){\n return !!pkg.driven_unit.shaftCount;\n }\n return true;\n }\n return false;\n}\nfunction baselineValid(pkg){ return !!(pkg.name \u0026\u0026 pkg.name.trim()); }\nfunction recomputeUnlock(pkg){\n var maxU = 0;\n for (var i=0;i\u003cTABS.length;i++){\n if (tabValid(pkg, TABS[i].key)) maxU = i+1; else break;\n }\n pkg.maxUnlocked = Math.max(pkg.maxUnlocked, maxU);\n /* re-lock if a later tab was previously trusted but is no longer valid (diagnostic #6) */\n for (var j=0;j\u003cTABS.length;j++){\n if (!tabValid(pkg, TABS[j].key)){ pkg.maxUnlocked = Math.min(pkg.maxUnlocked, j); break; }\n }\n}\nfunction allTabsComplete(pkg){ return TABS.every(function(t){ return tabValid(pkg, t.key); }); }\n\n/* ================= small DOM helpers ================= */\nfunction el(t,c,h){ var e=document.createElement(t); if(c) e.className=c; if(h!==undefined) e.innerHTML=h; return e; }\nfunction escapeHtml(value){ return String(value == null ? '' : value).replace(/[\u0026\u003c\u003e\"']/g,function(c){return {'\u0026':'\u0026amp;','\u003c':'\u0026lt;','\u003e':'\u0026gt;','\"':'\u0026quot;',\"'\":'\u0026#39;'}[c];}); }\nfunction clamp(n,lo,hi){ return Math.max(lo, Math.min(hi, n)); }\n\n/* Fix (v3.1): renderCard() tears down and rebuilds the entire #card subtree on every\n change, which is fine for buttons but silently breaks free-text/number inputs - the\n node holding focus gets destroyed and recreated, so a real user typing character by\n character loses focus after the first keystroke (this is what made Asset Name and\n Power Rating look \"stuck\": the model updates internally, but a rebuilt input can no\n longer be typed into without re-clicking it every time, and if a field never re-renders\n on 'input' at all - as Asset Name previously didn't - the rest of the UI never learns\n the value changed until an unrelated click happens to force a render). Any input that\n needs to trigger a live re-render tags itself with data-refocus=\"\u003ckey\u003e\" and calls this\n instead of renderCard() directly; it restores focus, cursor position and the card's\n scroll offset after the rebuild. */\nfunction reRenderPreserveFocus(){\n var active = document.activeElement;\n var key = (active \u0026\u0026 active.getAttribute) ? active.getAttribute(\"data-refocus\") : null;\n var selStart = null, selEnd = null;\n if (key \u0026\u0026 typeof active.selectionStart === \"number\"){ selStart = active.selectionStart; selEnd = active.selectionEnd; }\n var cardEl = document.getElementById(\"card\");\n var scrollTop = cardEl ? cardEl.scrollTop : 0;\n var outerScroll = window.scrollY;\n renderCard();\n if (key){\n var el2 = document.querySelector('[data-refocus=\"'+key+'\"]');\n if (el2){\n el2.focus();\n if (selStart != null \u0026\u0026 el2.setSelectionRange){ try { el2.setSelectionRange(selStart, selEnd); } catch(e){} }\n }\n }\n if (cardEl) cardEl.scrollTop = scrollTop;\n window.scrollTo(window.scrollX, outerScroll);\n}\n\nvar activeTip = null;\nfunction hideTip(){\n var tip = document.getElementById(\"tip\");\n if (tip) tip.classList.remove(\"on\");\n activeTip = null;\n}\nfunction showTip(icon){\n var tip = document.getElementById(\"tip\");\n var text = icon.getAttribute(\"data-tip\") || \"\";\n var r = icon.getBoundingClientRect();\n tip.innerHTML = \"\";\n tip.appendChild(document.createTextNode(text));\n var close = document.createElement(\"button\");\n close.type = \"button\";\n close.className = \"tipClose\";\n close.setAttribute(\"aria-label\", \"Close tip\");\n close.textContent = \"x\";\n close.addEventListener(\"click\", function(ev){ ev.stopPropagation(); hideTip(); });\n tip.appendChild(close);\n tip.style.position = \"fixed\";\n var maxLeft = Math.max(10, window.innerWidth - Math.min(280, window.innerWidth - 20) - 10);\n tip.style.left = clamp(r.left, 10, maxLeft) + \"px\";\n tip.style.top = (r.bottom + 8) + \"px\";\n tip.classList.add(\"on\");\n activeTip = icon;\n}\ndocument.addEventListener(\"click\", function(e){\n var icon = e.target.closest ? e.target.closest(\".infoIcon\") : null;\n if (icon){\n if (activeTip === icon){ hideTip(); e.stopPropagation(); return; }\n showTip(icon);\n e.stopPropagation();\n return;\n }\n if (e.target \u0026\u0026 e.target.closest \u0026\u0026 e.target.closest(\".tip\")) return;\n if (activeTip) hideTip();\n});\ndocument.addEventListener(\"keydown\", function(e){\n if (e.key === \"Escape\"){\n hideTip();\n if (document.getElementById(\"quoteOverlay\") \u0026\u0026 document.getElementById(\"quoteOverlay\").classList.contains(\"on\")) closeQuote();\n }\n});\n\nfunction fieldWrap(labelText, tipText, inner, required){\n var w = el(\"div\",\"fld\");\n var row = el(\"div\",\"fldLabelRow\");\n row.appendChild(el(\"label\",null,labelText + (required ? ' \u003cspan class=\"fldReq\"\u003e*\u003c/span\u003e' : \"\")));\n if (tipText){\n var ic = el(\"button\",\"infoIcon\",\"i\"); ic.type=\"button\"; ic.setAttribute(\"data-tip\", tipText); ic.setAttribute(\"aria-label\", \"About \" + labelText);\n row.appendChild(ic);\n }\n w.appendChild(row);\n w.appendChild(inner);\n var control = /^(INPUT|SELECT|TEXTAREA)$/.test(inner.tagName) ? inner : null;\n if (control) control.setAttribute(\"aria-label\",labelText);\n return w;\n}\nfunction optGroup(options, current, gridClass, onPick){\n var wrap = el(\"div\",\"opts\" + (gridClass ? \" \"+gridClass : \"\"));\n options.forEach(function(o){\n var b = el(\"button\",\"opt\" + (current===o[0] ? \" on\" : \"\"), '\u003cspan\u003e'+o[1]+'\u003c/span\u003e' + (o[2] ? '\u003cspan class=\"od\"\u003e'+o[2]+'\u003c/span\u003e' : \"\"));\n b.type = \"button\";\n b.addEventListener(\"click\", function(){ onPick(o[0]); });\n wrap.appendChild(b);\n });\n return wrap;\n}\n/* v4: plain \"empty box the client adds the number to\" pattern for optional numeric detail\n (Horsepower, Amperage, Frequency, Speed) - replacing the earlier tier-bucket button groups\n per direct client feedback. These fields never gate the workflow and nothing else on screen\n depends on them live, so the input just updates the model on every keystroke without ever\n re-rendering - no raw-string/reRenderPreserveFocus machinery needed, unlike Power Rating. */\nfunction numBox(obj, field, placeholder){\n var inp = document.createElement(\"input\"); inp.type=\"text\"; inp.inputMode=\"decimal\";\n inp.value = obj[field] || \"\";\n if (placeholder) inp.placeholder = placeholder;\n inp.addEventListener(\"input\", function(){ obj[field] = inp.value; });\n return inp;\n}\n/* v4 bug found while testing: gearbox shaft count uses the same \"empty box\" pattern, but unlike\n HP/Amperage/Frequency/Speed it DOES gate tabValid() - so with plain numBox() the Next button's\n disabled attribute went stale the instant shaft count was typed (it only reflects tabValid()\n as of the last render, and numBox never triggers one). A field that gates anything must\n re-render after every keystroke; this variant does that with the same focus-preserving\n approach already used for Power Rating and the Review screen's puck overrides, keyed by\n data-refocus so typing doesn't lose its place. */\nfunction numBoxLive(obj, field, placeholder, refocusKey){\n var inp = document.createElement(\"input\"); inp.type=\"text\"; inp.inputMode=\"decimal\";\n inp.value = obj[field] || \"\";\n if (placeholder) inp.placeholder = placeholder;\n if (refocusKey) inp.setAttribute(\"data-refocus\", refocusKey);\n inp.addEventListener(\"input\", function(){ obj[field] = inp.value; reRenderPreserveFocus(); });\n return inp;\n}\n\n/* ================= LEFT PANE: asset hierarchy tree ================= */\nfunction stepInstance(pkg, delta){\n pkg.instanceCount = clamp((pkg.instanceCount||1) + delta, 1, 999);\n renderTree(); renderCard();\n}\nfunction removeAsset(id){\n assets = assets.filter(function(p){ return p.id !== id; });\n if (editingId === id){ draft = null; editingId = null; }\n renderTree(); renderCard();\n}\nfunction duplicateAsset(pkg){\n var copy = JSON.parse(JSON.stringify(pkg));\n copy.id = \"pkg\"+(pkgSeq++);\n copy.name = pkg.name + \" (copy)\";\n assets.push(copy);\n renderTree();\n}\nfunction editAsset(pkg){\n draft = JSON.parse(JSON.stringify(pkg));\n editingId = pkg.id;\n recomputeUnlock(draft);\n draft.maxUnlocked = TABS.length; /* diagnostic #6: a previously completed asset opens fully unlocked */\n draft.activeTab = 0;\n renderTree(); renderCard();\n}\nfunction critLabel(c){ return {very:\"Very critical\", high:\"High\", medium:\"Medium\", low:\"Low\"}[c] || \"Medium\"; }\nfunction renderAssetNode(pkg){\n var n = el(\"div\",\"assetNode\" + (editingId===pkg.id ? \" sel\" : \"\"));\n var top = el(\"div\",\"an-top\");\n top.appendChild(el(\"div\",null,'\u003cdiv class=\"an-name\"\u003e'+escapeHtml(pkg.name)+'\u003c/div\u003e\u003cdiv class=\"an-type\"\u003e'+escapeHtml(pkg.machineType||\"Equipment\")+'\u003c/div\u003e'));\n n.appendChild(top);\n var badges = el(\"div\",\"an-badges\");\n badges.appendChild(el(\"span\",\"badge crit-\"+pkg.criticality, critLabel(pkg.criticality)));\n if (pkg.drivers.length\u003e1) badges.appendChild(el(\"span\",\"badge\", pkg.drivers.length+\" drivers\"));\n if (!allTabsComplete(pkg)) badges.appendChild(el(\"span\",\"badge incomplete\",\"Incomplete\"));\n n.appendChild(badges);\n var p = priceFor(pkg);\n var priceLabel = leadCaptured() ? (money(p.total) + \"/mo per unit\") : \"Pricing locked\";\n n.appendChild(el(\"div\",\"an-price\", priceLabel + \" \u00b7 \" + calcVib(pkg) + \" vibration sensors\"));\n var row = el(\"div\",\"an-row\");\n var st = el(\"div\",\"stepper\");\n var minus = el(\"button\",null,\"\u2212\"); minus.type=\"button\"; minus.addEventListener(\"click\", function(ev){ ev.stopPropagation(); stepInstance(pkg,-1); });\n var plus = el(\"button\",null,\"+\"); plus.type=\"button\"; plus.addEventListener(\"click\", function(ev){ ev.stopPropagation(); stepInstance(pkg,1); });\n st.appendChild(minus); st.appendChild(el(\"span\",\"sn\", String(pkg.instanceCount||1))); st.appendChild(plus);\n row.appendChild(st);\n var act = el(\"div\",\"an-actions\");\n var eb = el(\"button\",null,\"edit\"); eb.type=\"button\"; eb.addEventListener(\"click\", function(ev){ ev.stopPropagation(); editAsset(pkg); });\n var db = el(\"button\",null,\"duplicate\"); db.type=\"button\"; db.title=\"Create a copy of this asset with the same configuration\"; db.addEventListener(\"click\", function(ev){ ev.stopPropagation(); duplicateAsset(pkg); });\n var rb = el(\"button\",null,\"remove\"); rb.type=\"button\"; rb.addEventListener(\"click\", function(ev){ ev.stopPropagation(); removeAsset(pkg.id); });\n act.appendChild(eb); act.appendChild(db); act.appendChild(rb);\n row.appendChild(act);\n n.appendChild(row);\n n.addEventListener(\"click\", function(){ editAsset(pkg); });\n return n;\n}\nvar collapsedZones = {};\nfunction renderTree(){\n var host = document.getElementById(\"tree\");\n host.innerHTML = \"\";\n /* v4: the plant info from the intake screen now sits at the top of the hierarchy itself, not\n just in the topbar link (diagnostic #17) - \"put that information on the asset hierarchy...\n and keep updating it with new assets\". Since this reads `plant` fresh on every renderTree()\n call, and renderTree() already runs after every save/edit/remove, it stays current with no\n extra wiring. */\n var plantBox = el(\"div\",\"treePlantInfo\");\n plantBox.appendChild(textEl(\"div\",\"tp-company\", plant.company || \"-\"));\n if (plant.name) plantBox.appendChild(textEl(\"div\",\"tp-plant\", plant.name));\n var metaParts = [];\n if (plant.industry) metaParts.push(plant.industry);\n var loc = [plant.state, plant.country].filter(function(x){ return x \u0026\u0026 x.trim(); }).join(\", \");\n if (loc) metaParts.push(loc);\n if (metaParts.length) plantBox.appendChild(textEl(\"div\",\"tp-meta\", metaParts.join(\" \u00b7 \")));\n plantBox.appendChild(el(\"div\",\"tp-edit\",\"Edit plant details\"));\n plantBox.style.cursor = \"pointer\";\n plantBox.title = \"Edit plant details\";\n plantBox.addEventListener(\"click\", function(){ showPlantStage(); });\n host.appendChild(plantBox);\n // Rendering status does not submit: the visitor initiates intake via Continue.\n if (typeof renderInquiryNotificationStatus === \"function\") renderInquiryNotificationStatus();\n\n host.appendChild(el(\"h2\",null,\"Asset Hierarchy\"));\n if (!assets.length){\n host.appendChild(el(\"div\",\"treeEmpty\",\"No equipment packages yet. Add your first asset in the form to start building the plant hierarchy.\"));\n }\n /* Group by plant area/zone when more than one zone bucket is actually in use (diagnostic #9)\n - an unzoned fleet renders exactly as a flat list. Each zone group is independently\n collapsible/expandable (diagnostic #17: \"I want that tree being able to expand it or\n minimize each area\") - collapsedZones is a plain in-memory map, not written to any asset,\n so it never affects saved data or pricing. */\n var zoneOrder = [], zoneMap = {};\n assets.forEach(function(pkg){\n var z = (pkg.zone||\"\").trim() || \"Unassigned\";\n if (!zoneMap[z]){ zoneMap[z] = []; zoneOrder.push(z); }\n zoneMap[z].push(pkg);\n });\n var showZoneHeads = zoneOrder.length \u003e 1 || (zoneOrder.length===1 \u0026\u0026 zoneOrder[0] !== \"Unassigned\");\n zoneOrder.forEach(function(z){\n if (showZoneHeads){\n var collapsed = !!collapsedZones[z];\n var zh = el(\"div\",\"zoneHead\" + (collapsed ? \" collapsed\" : \"\"));\n zh.appendChild(textEl(\"span\",null, z + \" (\" + zoneMap[z].length + \")\"));\n zh.appendChild(el(\"span\",\"zoneCaret\", collapsed ? \"\u25b8\" : \"\u25be\"));\n zh.addEventListener(\"click\", function(){ collapsedZones[z] = !collapsedZones[z]; renderTree(); });\n host.appendChild(zh);\n if (collapsed) return;\n }\n var group = showZoneHeads ? el(\"div\",\"zoneGroup\") : host;\n zoneMap[z].forEach(function(pkg){ group.appendChild(renderAssetNode(pkg)); });\n if (showZoneHeads) host.appendChild(group);\n });\n var addBtn = el(\"button\",\"addAssetBtn\",\"+ Add asset\");\n addBtn.type = \"button\";\n addBtn.addEventListener(\"click\", function(){ draft = null; editingId = null; renderCard(); });\n host.appendChild(addBtn);\n\n /* v11: client flagged that there was nowhere in the tool visibly showing that progress gets\n saved - persistLocal() has run silently on every render since v10, but silent is exactly the\n problem: there was no on-screen confirmation, so from the outside it read as a feature that\n didn't exist. This row is always shown, even with zero assets (unlike the quote/share actions\n below, which need at least one asset to mean anything), and it is the SAME save mechanism -\n clicking it doesn't add a second save path, it just makes the existing one visible and gives\n it an explicit \"Saved \u2713\" confirmation plus a persistent last-saved timestamp. */\n var saveRow = el(\"div\",\"quoteActions\");\n var saveBtn = el(\"button\",\"btn ghost\",\"Save progress\"); saveBtn.type=\"button\";\n saveBtn.title = \"Saves everything on this plant to this browser, so it's still here if you close the tab and come back.\";\n saveBtn.setAttribute(\"data-status-target\",\"saveStatusTree\");\n saveBtn.addEventListener(\"click\", function(){ saveProgressNow(saveBtn); });\n saveRow.appendChild(saveBtn);\n var saveStatus = el(\"div\",\"hint\", savedStatusText()); saveStatus.id = \"saveStatusTree\";\n saveRow.appendChild(saveStatus);\n host.appendChild(saveRow);\n\n if (assets.length){\n var t = totals();\n var tt = el(\"div\",\"treeTotals\");\n tt.appendChild(el(\"div\",\"tt-row\",'\u003cspan\u003eAssets\u003c/span\u003e\u003cb\u003e'+t.assets+'\u003c/b\u003e'));\n tt.appendChild(el(\"div\",\"tt-row\",'\u003cspan\u003eVibration sensors\u003c/span\u003e\u003cb\u003e'+t.pucks+'\u003c/b\u003e'));\n tt.appendChild(el(\"div\",\"tt-row\",'\u003cspan\u003eGateways\u003c/span\u003e\u003cb\u003e'+t.gateways+'\u003c/b\u003e'));\n if (leadCaptured()){\n tt.appendChild(el(\"div\",\"tt-price\", money(t.monthly)+\"/mo\"));\n } else {\n tt.appendChild(el(\"div\",\"tt-price lockedPrice\",\"Pricing locked\"));\n var unlockBtn = el(\"button\",\"lockLink\",\"Add your name and email to view pricing\");\n unlockBtn.type = \"button\";\n unlockBtn.addEventListener(\"click\", showPlantStage);\n tt.appendChild(unlockBtn);\n }\n var qa = el(\"div\",\"quoteActions\");\n qa.appendChild(actionButton(\"Review my results\",\"btn\",reviewResults));\n tt.appendChild(qa);\n host.appendChild(tt);\n }\n persistLocal();\n var liveStatus = document.getElementById(\"saveStatusTree\");\n if (liveStatus) liveStatus.textContent = savedStatusText();\n}\n\n/* ================= RIGHT PANE: baseline + tabbed drivetrain wizard ================= */\n/* v5: the equipment-type picker (Pumps / Fans / Compressors / Heavy Equipment / ...) used to be\n a standalone screen shown before Baseline - that's what the client called \"the second page.\"\n Per direct feedback, it now lives inside Block 5 (Driven Unit) instead, so the very next thing\n after Plant Info is Baseline followed immediately by the 5-block question sequence. Selecting a\n type here resets driven_unit's contextual answers, since switching from e.g. Pump to Crusher\n would otherwise leave stale category/bearing-arrangement data behind. */\nfunction renderDrivenPicker(pkg, host){\n var wrap = el(\"div\");\n var searchWrap = el(\"div\",\"search\");\n var inp = document.createElement(\"input\"); inp.type=\"text\"; inp.placeholder=\"Search equipment type\u2026\";\n searchWrap.appendChild(inp);\n wrap.appendChild(searchWrap);\n var grid = el(\"div\");\n wrap.appendChild(grid);\n function paint(filter){\n grid.innerHTML = \"\";\n var f = (filter||\"\").toLowerCase();\n CATALOG.forEach(function(cat){\n var items = cat.items.filter(function(it){ return !f || it.toLowerCase().indexOf(f)\u003e=0; });\n if (!items.length) return;\n var sec = el(\"div\",\"cat\");\n sec.appendChild(el(\"h3\",null,cat.cat));\n var mg = el(\"div\",\"mgrid\");\n items.forEach(function(label){\n var b = el(\"button\",\"mcardbtn\" + (pkg.machineType===label ? \" on\" : \"\"), label); b.type=\"button\";\n b.addEventListener(\"click\", function(){\n if (pkg.machineType !== label){\n pkg.machineType = label; pkg.machineCat = cat.cat;\n pkg.driven_unit = { category:\"\", cylinders:\"\", stages:\"\", bearingArrangement:\"\", shaftCount:\"\" };\n }\n renderCard();\n });\n mg.appendChild(b);\n });\n sec.appendChild(mg);\n grid.appendChild(sec);\n });\n }\n paint(\"\");\n inp.addEventListener(\"input\", function(){ paint(inp.value); });\n host.appendChild(wrap);\n}\n\nfunction driverTypeFields(d){\n /* v13: Turbine and Engine driver types are retired (this tool covers motors only for now - see\n diagnostic addendum below), so this is unconditionally the motor's optional-detail fields.\n Control type (VFD?) and MCC/Panel Access both moved out of here and into renderDriverCard's\n always-visible vital fields - see diagnostic #13. Everything left in this function is\n optional supplementary detail, shown only inside the collapsible box below. */\n var wrap = document.createDocumentFragment();\n wrap.appendChild(fieldWrap(\"Insulation Class (IC)\",\"The motor's insulation temperature rating (on the nameplate) - Class F and H are the most common industrial ratings.\",\n optGroup([[\"b\",\"Class B\"],[\"f\",\"Class F\"],[\"h\",\"Class H\"]], d.insulation_class, null, function(v){ d.insulation_class=v; renderCard(); })));\n wrap.appendChild(fieldWrap(\"Voltage Level (V)\",\"The motor's supply voltage - affects which current-analysis hardware fits the panel.\",\n optGroup([[\"lv\",\"Low voltage (\u003c1kV)\"],[\"mv\",\"Medium voltage (1-15kV)\"]], d.voltage_level, null, function(v){ d.voltage_level=v; renderCard(); })));\n return wrap;\n}\n\nfunction renderDriverCard(pkg, d, i, host){\n var dc = el(\"div\",\"driverCard\");\n var head = el(\"div\",\"dcHead\");\n head.appendChild(textEl(\"b\",null, (d.driver_type.charAt(0).toUpperCase()+d.driver_type.slice(1)) + \" #\" + (i+1)));\n if (pkg.drivers.length \u003e 1){\n var rm = el(\"button\",\"dcRemove\",\"remove\"); rm.type=\"button\";\n rm.addEventListener(\"click\", function(){ pkg.drivers.splice(i,1); renderCard(); });\n head.appendChild(rm);\n }\n dc.appendChild(head);\n /* v13: Turbine and Engine driver types are retired - this tool covers motors only for now (see\n diagnostic addendum below), so the \"Driver type\" picker is gone; every driver is a motor. */\n /* v13: a motor's vital size question is Horsepower + Operating speed (RPM), not kW - per client\n request, sensor count for a motor is looked up from an ISO 20816-3 / NEMA MG-1 HP-and-speed\n tier table (see motorSensorCount()), which needs both numbers, not a single kW figure. Both\n fields gate tabValid() so they re-render on every keystroke, the same live-gating pattern\n used for gearbox shaft count. */\n dc.appendChild(fieldWrap(\"Horsepower (HP)\",\"The motor's nameplate horsepower. Combined with operating speed below, this determines the sensor count per ISO 20816-3 / NEMA MG-1 machine-size tiers.\",\n numBoxLive(d, \"horsepower\", \"e.g. 75\", \"hp-\"+d.id), true));\n dc.appendChild(fieldWrap(\"Operating speed (RPM)\",\"The motor's nameplate rotational speed. A low-speed motor (below 1,000 RPM) needs two-point bearing coverage regardless of horsepower.\",\n numBoxLive(d, \"speed_rpm\", \"e.g. 1780\", \"rpm-\"+d.id), true));\n /* Vital questions for a motor, per the client: VFD-or-not and MCC/panel access are the other\n two things that matter up front (diagnostic #13) - MCC access determines whether\n electrical monitoring can be delivered on this driver at all, which is as consequential as\n the size question above. Everything else about the motor is optional supplementary detail\n and lives in the collapsible box below, never gating the workflow. */\n dc.appendChild(fieldWrap(\"Variable Frequency Drive (VFD)?\",\"Whether this motor runs at fixed speed across-the-line, or has a Variable Frequency Drive controlling its speed.\",\n optGroup([[\"vfd\",\"Yes\"],[\"fixed\",\"No\"]], d.control_type, \"row2\", function(v){ d.control_type=v; renderCard(); }), true));\n dc.appendChild(fieldWrap(\"MCC / Panel Access\",\"Whether a technician can safely open the motor control panel to clamp on current-analysis hardware. This does not affect vibration sensor count, but it determines whether electrical monitoring can be offered on this driver.\",\n optGroup([[\"yes\",\"Yes\"],[\"no\",\"No\"],[\"notsure\",\"Not sure\"]], d.mcc_panel_access, \"wide\", function(v){ d.mcc_panel_access=v; renderCard(); }), true));\n /* Toggle lives right above the box it opens (one shared pkg.advancedOpen flag covers every\n driver card, so they all expand/collapse together, but each card gets its own visible\n control right next to its own optional fields rather than one control stranded below\n every card's content). */\n var advToggle = el(\"div\",\"toggleRow\" + (pkg.advancedOpen ? \" on\" : \"\"));\n advToggle.appendChild(el(\"div\",\"tSwitch\"));\n advToggle.appendChild(el(\"span\",\"tLabel\",\"Additional optional information\"));\n advToggle.addEventListener(\"click\", function(){ pkg.advancedOpen = !pkg.advancedOpen; renderCard(); });\n dc.appendChild(advToggle);\n if (pkg.advancedOpen){\n /* v4: plain number boxes instead of tier-bucket buttons, per direct client feedback -\n diagnostic #13. Nothing downstream reads these, so no render is needed on input (numBox\n just updates the model directly); see numBox's own comment for why that also sidesteps\n the earlier focus-loss/infinite-loop bug class rather than needing the same workaround. */\n var adv = el(\"div\",\"advBlock on\");\n /* v13: HP and RPM are asked as vitals above (they feed the sensor-count lookup directly), so\n they're left out here to avoid asking twice. */\n adv.appendChild(fieldWrap(\"A\",\"The driver's full-load amperage - helps size current-analysis hardware.\", numBox(d,\"amperage\",\"e.g. 62\")));\n adv.appendChild(fieldWrap(\"Hz\",\"The electrical supply frequency this driver runs on.\", numBox(d,\"line_freq\",\"e.g. 60\")));\n adv.appendChild(driverTypeFields(d));\n dc.appendChild(adv);\n }\n host.appendChild(dc);\n}\n\nfunction renderTabPanel(pkg, host){\n var key = TABS[pkg.activeTab].key;\n var panel = el(\"div\",\"tabPanel\");\n var drivenDetachedEl = null; /* v11: \"More about this \u003ctype\u003e\" card for the driven block, kept\n out of panel so it renders as its own detached section below it */\n if (key === \"driver\"){\n pkg.drivers.forEach(function(d,i){ renderDriverCard(pkg, d, i, panel); });\n var addDrv = el(\"button\",\"addDriverBtn\",\"+ Add another driver (multi-drive asset)\");\n addDrv.type=\"button\";\n addDrv.addEventListener(\"click\", function(){ pkg.drivers.push(newDriver(\"motor\")); renderCard(); });\n panel.appendChild(addDrv);\n }\n else if (key === \"coupling1\"){\n panel.appendChild(fieldWrap(\"Coupling type\",\"How the driver connects to the next stage down the drivetrain.\",\n optGroup(COUPLING_TYPES, pkg.coupling_nodes[0].type, \"wide\", function(v){ pkg.coupling_nodes[0].type=v; renderCard(); }), true));\n panel.appendChild(fieldWrap(\"Arrangement orientation\",\"The physical orientation of this coupling relative to the shafts it joins.\",\n optGroup(ORIENTATIONS, pkg.coupling_nodes[0].orientation, \"wide\", function(v){ pkg.coupling_nodes[0].orientation=v; renderCard(); }), true));\n }\n else if (key === \"inter\"){\n panel.appendChild(fieldWrap(\"Is there an intermediate stage between the driver and the driven unit?\",\"A jackshaft, countershaft, or gearbox stage sitting between the driver and the final driven machine - its own bearings, running at its own speed. Most direct-coupled or belt-driven assets have none - pick No to skip straight to Coupling 2.\",\n optGroup([[\"yes\",\"Yes\"],[\"no\",\"No, skip this block\"]], pkg.intermediateAnswer, \"row2\", function(v){\n pkg.intermediateAnswer = v;\n if (v===\"yes\" \u0026\u0026 !pkg.intermediates.length) pkg.intermediates.push(newIntermediate());\n if (v===\"no\") pkg.intermediates = [];\n syncInterCouplings(pkg);\n renderCard();\n }), true));\n if (pkg.intermediateAnswer === \"yes\"){\n /* v4: a stage is now a single element type - Gearbox / Jackshaft / Other, never more than\n one at once (diagnostic #14: \"I prefer to select only one at a time\"). Two elements on\n one shaft train become two separate stages, and once there is more than one stage, the\n coupling between each consecutive pair is asked right after the second of that pair. */\n pkg.intermediates.forEach(function(inter, i){\n var ic = el(\"div\",\"driverCard\");\n var ih = el(\"div\",\"dcHead\"); ih.appendChild(el(\"b\",null,\"Intermediate stage #\"+(i+1)));\n if (pkg.intermediates.length\u003e1){\n var rm = el(\"button\",\"dcRemove\",\"remove\"); rm.type=\"button\";\n rm.addEventListener(\"click\", function(){ pkg.intermediates.splice(i,1); syncInterCouplings(pkg); renderCard(); });\n ih.appendChild(rm);\n }\n ic.appendChild(ih);\n ic.appendChild(fieldWrap(\"Element type\",\"Pick the one that applies to this stage. If this shaft train actually has both a gearbox and a jackshaft, add a second intermediate stage below for the other one, then describe how the two connect.\",\n optGroup([[\"gearbox\",\"Gearbox\"],[\"jackshaft\",\"Jackshaft\"],[\"other\",\"Other\"]], inter.type, \"wide\", function(v){ inter.type=v; renderCard(); }), true));\n if (inter.type === \"gearbox\"){\n ic.appendChild(fieldWrap(\"Shafts\",\"How many shafts this gearbox has (input, output, and any intermediate shafts) - sets how many bearing locations get monitored on it.\", numBoxLive(inter,\"shaftCount\",\"e.g. 2\",\"shaftCount-\"+inter.id), true));\n }\n panel.appendChild(ic);\n if (i \u003c pkg.intermediates.length - 1){\n if (!pkg.interCouplings[i]) pkg.interCouplings[i] = { type:\"\", orientation:\"\" };\n var jc = pkg.interCouplings[i];\n var cpl = el(\"div\",\"driverCard\");\n cpl.appendChild(el(\"div\",\"dcHead\",'\u003cb\u003eCoupling - Intermediate #'+(i+1)+' to #'+(i+2)+'\u003c/b\u003e'));\n cpl.appendChild(fieldWrap(\"Coupling type\",\"How intermediate stage #\"+(i+1)+\" connects to stage #\"+(i+2)+\".\",\n optGroup(COUPLING_TYPES, jc.type, \"wide\", function(v){ jc.type=v; renderCard(); }), true));\n cpl.appendChild(fieldWrap(\"Arrangement orientation\",\"The physical orientation of this coupling relative to the shafts it joins.\",\n optGroup(ORIENTATIONS, jc.orientation, \"wide\", function(v){ jc.orientation=v; renderCard(); }), true));\n panel.appendChild(cpl);\n }\n });\n var addInt = el(\"button\",\"addInterBtn\",\"+ Add another intermediate stage\");\n addInt.type=\"button\";\n addInt.addEventListener(\"click\", function(){ pkg.intermediates.push(newIntermediate()); syncInterCouplings(pkg); renderCard(); });\n panel.appendChild(addInt);\n }\n }\n else if (key === \"coupling2\"){\n if (pkg.intermediateAnswer !== \"yes\" || pkg.intermediates.length === 0){\n panel.appendChild(el(\"div\",\"lockNote\",\"No intermediate stage was configured in Block 3, so there is no second boundary to describe. This block is automatically complete - add an intermediate stage in Block 3 first if one exists.\"));\n } else {\n panel.appendChild(fieldWrap(\"Coupling type\",\"How the intermediate stage connects onward to the driven machine.\",\n optGroup(COUPLING_TYPES, pkg.coupling_nodes[1].type, \"wide\", function(v){ pkg.coupling_nodes[1].type=v; renderCard(); }), true));\n panel.appendChild(fieldWrap(\"Arrangement orientation\",\"The physical orientation of this second coupling.\",\n optGroup(ORIENTATIONS, pkg.coupling_nodes[1].orientation, \"wide\", function(v){ pkg.coupling_nodes[1].orientation=v; renderCard(); }), true));\n }\n }\n else if (key === \"driven\"){\n /* v5: the equipment-type picker itself now lives here - see renderDrivenPicker's comment -\n followed by the same contextual follow-ups as before: bearing arrangement for Fan/Blower\n and Pump (API 610-style Overhung vs Between Bearings), and a process category for\n Pump/Compressor (Centrifugal / Reciprocating / Rotary / Screw), with a cylinder count for\n Reciprocating and a casing question for Centrifugal. Any element with more than one of the\n same wearing part - a gearbox's shafts, a reciprocating machine's cylinders - asks for the\n exact count as a number box rather than a capped bucket, per direct client feedback\n (\"make sure at any stage, if I selected an element which could have more than one shaft,\n ask for the number\"). */\n var pickHead = el(\"div\",\"fld\");\n var pickRow = el(\"div\",\"fldLabelRow\");\n pickRow.appendChild(el(\"label\",null,'Equipment type'));\n var pickTip = el(\"button\",\"infoIcon\",\"i\"); pickTip.type=\"button\";\n pickTip.setAttribute(\"data-tip\",\"Optional. What the driver is actually turning. Pick the specific machine so Chladni can ask the right follow-up questions and calculate the correct monitoring points - leave it blank if you're not sure, and this asset still gets a generic driven-component estimate.\");\n pickRow.appendChild(pickTip);\n pickHead.appendChild(pickRow);\n panel.appendChild(pickHead);\n renderDrivenPicker(pkg, panel);\n\n /* v6: per direct client feedback, the follow-up questions (category, bearing arrangement,\n cylinder count, ...) were sitting directly under the equipment-type grid with nothing to\n set them apart, which read as one long confusing list. They now sit in their own bordered\n section with its own heading, appearing only once an equipment type that actually has\n follow-ups has been picked - so the section itself doubles as a visual \"this is now asking\n about your specific machine\" cue rather than blending into the type picker above it.\n v11: that bordered section was still nested INSIDE the equipment-type picker's own bordered\n box, reading as a box-within-a-box rather than a clearly separate section (direct client\n feedback). It's now appended as its own standalone card straight to the block, below and\n fully detached from the picker's box, with visible space between the two. */\n if (pkg.machineType === \"Compressor\" || pkg.machineType === \"Pump\" || pkg.machineType === \"Fan / Blower\" || pkg.machineType === \"Gearbox / Speed Reducer\"){\n var detailBox = el(\"div\",\"driverCard detachedCard\");\n detailBox.appendChild(el(\"div\",\"dcHead\",'\u003cb\u003eMore about this ' + escapeHtml(pkg.machineType) + '\u003c/b\u003e'));\n if (pkg.machineType === \"Compressor\"){\n detailBox.appendChild(fieldWrap(\"Compressor category\",\"The compressor's operating principle - determines which failure modes and monitoring points apply.\",\n optGroup([[\"centrifugal\",\"Centrifugal\"],[\"reciprocating\",\"Reciprocating\"],[\"screw\",\"Rotary Screw\"],[\"rotaryother\",\"Rotary (Other)\"]], pkg.driven_unit.category, \"wide\", function(v){ pkg.driven_unit.category=v; renderCard(); }), true));\n if (pkg.driven_unit.category === \"reciprocating\"){\n detailBox.appendChild(fieldWrap(\"Cylinders / stages\",\"How many cylinders on the compressor frame - each one gets its own cylinder \u0026 valve monitoring point.\",\n numBoxLive(pkg.driven_unit,\"cylinders\",\"e.g. 2\",\"comp-cyl-\"+pkg.id), true));\n } else if (pkg.driven_unit.category === \"centrifugal\"){\n detailBox.appendChild(fieldWrap(\"Casing configuration\",\"Whether the compressor is a single casing or multiple casings / multistage.\",\n optGroup([[\"single\",\"Single casing\"],[\"multi\",\"Multiple casings / multistage\"]], pkg.driven_unit.stages, \"row2\", function(v){ pkg.driven_unit.stages=v; renderCard(); }), true));\n }\n } else if (pkg.machineType === \"Pump\"){\n detailBox.appendChild(fieldWrap(\"Pump category\",\"The pump's operating principle - determines which failure modes and monitoring points apply.\",\n optGroup([[\"centrifugal\",\"Centrifugal\"],[\"reciprocating\",\"Reciprocating\"],[\"rotary\",\"Rotary\"],[\"screw\",\"Screw\"]], pkg.driven_unit.category, \"wide\", function(v){ pkg.driven_unit.category=v; renderCard(); }), true));\n if (pkg.driven_unit.category === \"reciprocating\"){\n detailBox.appendChild(fieldWrap(\"Cylinders / plungers\",\"How many cylinders/plungers on the pump - each one gets its own valve/plunger monitoring point.\",\n numBoxLive(pkg.driven_unit,\"cylinders\",\"e.g. 2\",\"pump-cyl-\"+pkg.id), true));\n }\n detailBox.appendChild(fieldWrap(\"Bearing arrangement\",\"Overhung (API 610 OH-type): the impeller sits outside the bearing span, on one side. Between Bearings (API 610 BB-type): the impeller sits between two bearings - needs monitoring at both ends.\",\n optGroup([[\"overhung\",\"Overhung\"],[\"between\",\"Between Bearings\"]], pkg.driven_unit.bearingArrangement, \"row2\", function(v){ pkg.driven_unit.bearingArrangement=v; renderCard(); }), true));\n } else if (pkg.machineType === \"Fan / Blower\"){\n detailBox.appendChild(fieldWrap(\"Bearing arrangement\",\"Overhung: the fan/blower wheel sits outside the bearing span, on one side. Between Bearings: the wheel sits between two bearings - needs monitoring at both ends.\",\n optGroup([[\"overhung\",\"Overhung\"],[\"between\",\"Between Bearings\"]], pkg.driven_unit.bearingArrangement, \"row2\", function(v){ pkg.driven_unit.bearingArrangement=v; renderCard(); }), true));\n } else if (pkg.machineType === \"Gearbox / Speed Reducer\"){\n detailBox.appendChild(fieldWrap(\"Shafts\",\"How many shafts this gearbox has (input, output, and any intermediate shafts) - sets how many bearing locations get monitored on it.\",\n numBoxLive(pkg.driven_unit,\"shaftCount\",\"e.g. 2\",\"driven-gb-shaft-\"+pkg.id), true));\n }\n drivenDetachedEl = detailBox;\n } else if (pkg.machineType){\n drivenDetachedEl = textEl(\"div\",\"lockNote\",\"No additional driven-unit questions apply to \"+pkg.machineType+\". This block is complete - the driven-side component (if any) is calculated automatically.\");\n }\n }\n host.appendChild(panel);\n if (drivenDetachedEl) host.appendChild(drivenDetachedEl);\n}\n\nfunction renderTabbedWizard(pkg, host){\n var tabbar = el(\"div\",\"tabbar\");\n TABS.forEach(function(t, i){\n var b = el(\"button\",\"tabbtn\" + (i===pkg.activeTab ? \" active\" : \"\") + (i \u003c pkg.activeTab \u0026\u0026 tabValid(pkg,t.key) ? \" done\" : \"\"),\n '\u003cspan class=\"tn\"\u003e'+t.n+'\u003c/span\u003e'+t.label);\n b.type = \"button\";\n b.disabled = i \u003e pkg.maxUnlocked;\n b.addEventListener(\"click\", function(){ if (i \u003c= pkg.maxUnlocked){ pkg.activeTab = i; renderCard(); } });\n tabbar.appendChild(b);\n });\n host.appendChild(tabbar);\n renderTabPanel(pkg, host);\n\n var row = el(\"div\",\"btnrow\");\n var backBtn = el(\"button\",\"btn ghost\",\"Back\");\n backBtn.type=\"button\"; backBtn.disabled = pkg.activeTab === 0;\n backBtn.addEventListener(\"click\", function(){ if (pkg.activeTab\u003e0){ pkg.activeTab--; renderCard(); } });\n row.appendChild(backBtn);\n\n var curValid = tabValid(pkg, TABS[pkg.activeTab].key);\n if (pkg.activeTab \u003c TABS.length - 1){\n var nextBtn = el(\"button\",\"btn\", \"Next -\u003e\");\n nextBtn.type=\"button\"; nextBtn.disabled = !curValid;\n nextBtn.addEventListener(\"click\", function(){\n recomputeUnlock(pkg);\n pkg.activeTab = Math.min(TABS.length-1, pkg.activeTab+1);\n renderCard();\n });\n row.appendChild(nextBtn);\n } else {\n var finishBtn = el(\"button\",\"btn\", \"Review \u0026 Finish -\u003e\");\n finishBtn.type=\"button\"; finishBtn.disabled = !curValid;\n finishBtn.addEventListener(\"click\", function(){ recomputeUnlock(pkg); pkg.activeTab = TABS.length; renderCard(); });\n row.appendChild(finishBtn);\n }\n host.appendChild(row);\n\n if (!allTabsComplete(pkg)){\n host.appendChild(el(\"div\",\"lockNote\",\"Pricing and the final component list stay locked until all 5 drivetrain blocks above are complete. \" + TABS.filter(function(t){return !tabValid(pkg,t.key);}).length + \" block(s) remaining.\"));\n }\n}\n\nfunction couplingDesc(node){\n if (!node || !node.type) return \"not configured\";\n var t = COUPLING_TYPES.filter(function(x){ return x[0]===node.type; })[0];\n var o = ORIENTATIONS.filter(function(x){ return x[0]===node.orientation; })[0];\n return (t ? t[1] : node.type) + (o ? \", \" + o[1] : \"\");\n}\nfunction blockHeadRow(label){\n var tr = el(\"tr\",\"reviewBlockHead\");\n var td = textEl(\"td\",null,label); td.colSpan = 2;\n tr.appendChild(td);\n return tr;\n}\nfunction couplingInfoRow(label){\n var tr = el(\"tr\");\n tr.appendChild(textEl(\"td\",null,label));\n tr.appendChild(el(\"td\",\"num\",\"-\"));\n return tr;\n}\nfunction puckRow(pkg, c){\n var tr = el(\"tr\");\n tr.appendChild(textEl(\"td\",null,c.label));\n var td = el(\"td\",\"num\");\n var inp = document.createElement(\"input\");\n inp.type = \"text\"; inp.inputMode = \"numeric\"; inp.className = \"puckOverrideInp\";\n inp.value = String(effectivePucks(pkg, c));\n inp.setAttribute(\"data-refocus\", \"puck-\"+c.key);\n inp.addEventListener(\"input\", function(){\n var raw = inp.value.trim();\n if (raw === \"\"){ delete pkg.puckOverrides[c.key]; }\n else {\n var n = parseInt(raw, 10);\n if (!isNaN(n) \u0026\u0026 n \u003e= 0) pkg.puckOverrides[c.key] = n;\n }\n reRenderPreserveFocus();\n });\n td.appendChild(inp);\n tr.appendChild(td);\n return tr;\n}\n/* v5: \"at the end you should be able to investigate if the path from driver to driven has been\n successfully implemented\" - this walks the same chain the 5 blocks built, in order, and either\n confirms every link is there or lists exactly what's missing. allTabsComplete() already gates\n reaching this screen, so in practice this should always come back verified; it's shown anyway\n as an explicit confirmation rather than a silent assumption. */\nfunction drivetrainChainSummary(pkg){\n var parts = [];\n parts.push(pkg.drivers.map(function(d,i){\n return (d.driver_type.charAt(0).toUpperCase()+d.driver_type.slice(1)) + (pkg.drivers.length\u003e1 ? \" #\"+(i+1) : \"\");\n }).join(\" + \"));\n parts.push(\"Coupling 1 (\" + couplingDesc(pkg.coupling_nodes[0]) + \")\");\n if (pkg.intermediateAnswer === \"yes\" \u0026\u0026 pkg.intermediates.length){\n pkg.intermediates.forEach(function(inter, i){\n var lbl = inter.type === \"gearbox\" ? \"Gearbox\" : inter.type === \"jackshaft\" ? \"Jackshaft\" : \"Other element\";\n parts.push(\"Intermediate #\"+(i+1)+\" (\" + lbl + \")\");\n if (i \u003c pkg.intermediates.length-1) parts.push(\"Coupling (\" + couplingDesc(pkg.interCouplings[i]) + \")\");\n });\n parts.push(\"Coupling 2 (\" + couplingDesc(pkg.coupling_nodes[1]) + \")\");\n }\n parts.push(\"Driven Unit (\" + (pkg.machineType || \"-\") + \")\");\n return parts;\n}\nfunction verifyDrivetrainPath(pkg){\n var issues = [];\n if (!pkg.drivers.length) issues.push(\"No driver is defined.\");\n pkg.drivers.forEach(function(d, i){\n if (!d.driver_type){\n issues.push(\"Driver #\"+(i+1)+\" has no driver type selected.\");\n } else {\n var hp = parseFloat(d.horsepower), rpm = parseFloat(d.speed_rpm);\n if (isNaN(hp) || hp \u003c= 0 || isNaN(rpm) || rpm \u003c= 0) issues.push(\"Driver #\"+(i+1)+\" is missing a valid horsepower or operating speed.\");\n }\n if (!d.control_type || !d.mcc_panel_access) issues.push(\"Driver #\"+(i+1)+\" is missing VFD or MCC access.\");\n });\n if (!pkg.coupling_nodes[0].type || !pkg.coupling_nodes[0].orientation) issues.push(\"Coupling 1 is not fully defined.\");\n if (pkg.intermediateAnswer === \"yes\"){\n if (!pkg.intermediates.length) issues.push(\"Intermediate stage was flagged Yes but none is defined.\");\n pkg.intermediates.forEach(function(inter, i){\n if (!inter.type) issues.push(\"Intermediate stage #\"+(i+1)+\" has no element type.\");\n if (inter.type === \"gearbox\" \u0026\u0026 !inter.shaftCount) issues.push(\"Gearbox stage #\"+(i+1)+\" is missing a shaft count.\");\n });\n for (var k=0; k\u003cpkg.intermediates.length-1; k++){\n var jc = pkg.interCouplings[k];\n if (!jc || !jc.type || !jc.orientation) issues.push(\"The coupling between intermediate stages #\"+(k+1)+\" and #\"+(k+2)+\" is not defined.\");\n }\n if (!pkg.coupling_nodes[1].type || !pkg.coupling_nodes[1].orientation) issues.push(\"Coupling 2 is not fully defined.\");\n } else if (pkg.intermediateAnswer !== \"no\"){\n issues.push(\"Whether an intermediate stage exists was never answered.\");\n }\n /* v11: equipment type is optional (see tabValid's \"driven\" case) - an unpicked type is no\n longer an issue here either. The follow-up-questions check still applies whenever a type\n that HAS follow-ups was picked but left half-answered. */\n if (pkg.machineType \u0026\u0026 !tabValid(pkg, \"driven\")){\n issues.push(\"Driven unit follow-up questions for \" + pkg.machineType + \" are incomplete.\");\n }\n return { ok: issues.length===0, issues: issues, chain: drivetrainChainSummary(pkg) };\n}\nfunction renderReview(pkg, host){\n var verify = verifyDrivetrainPath(pkg);\n if (verify.ok){\n host.appendChild(el(\"div\",\"sectionHead\",\"Drivetrain path - verified\"));\n host.appendChild(textEl(\"div\",\"hint\", \"Driver -\u003e \" + verify.chain.slice(1).join(\" -\u003e \")));\n } else {\n host.appendChild(el(\"div\",\"sectionHead\",\"Drivetrain path - incomplete\"));\n var ul = document.createElement(\"ul\");\n ul.style.paddingLeft = \"18px\";\n verify.issues.forEach(function(msg){ var li=document.createElement(\"li\"); li.className=\"hint\"; li.textContent=msg; ul.appendChild(li); });\n host.appendChild(ul);\n }\n host.appendChild(textEl(\"div\",\"sectionHead\",\"Final configuration - \" + pkg.name));\n var comps = calcComponents(pkg);\n var p = priceFor(pkg);\n var tbl = el(\"table\",\"quoteTable\");\n var thead = el(\"tr\"); [\"Component\",\"Vibration sensors\"].forEach(function(h){ thead.appendChild(el(\"th\",null,h)); });\n tbl.appendChild(thead);\n /* v4: rows are grouped under a header per drivetrain block, and each puck count is an\n editable box - \"maybe I like to put 4 sensors on my gearbox, or only 2 would be enough\"\n (diagnostic #16). Coupling blocks are shown for transparency but never take an editable\n box: they contribute no pucks by design, and that's stated outright rather than just\n being absent from the table. */\n var driverComps = comps.filter(function(c){ return c.block===\"driver\"; });\n var interComps = comps.filter(function(c){ return c.block===\"inter\"; });\n var drivenComps = comps.filter(function(c){ return c.block===\"driven\"; });\n\n if (driverComps.length){\n tbl.appendChild(blockHeadRow(\"Driver\"));\n driverComps.forEach(function(c){ tbl.appendChild(puckRow(pkg, c)); });\n }\n tbl.appendChild(blockHeadRow(\"Coupling 1\"));\n tbl.appendChild(couplingInfoRow(\"Coupling 1 (\" + couplingDesc(pkg.coupling_nodes[0]) + \")\"));\n\n if (pkg.intermediateAnswer === \"yes\" \u0026\u0026 pkg.intermediates.length){\n tbl.appendChild(blockHeadRow(\"Intermediate\"));\n interComps.forEach(function(c){ tbl.appendChild(puckRow(pkg, c)); });\n pkg.interCouplings.forEach(function(jc, i){\n tbl.appendChild(couplingInfoRow(\"Coupling - Intermediate #\"+(i+1)+\" to #\"+(i+2)+\" (\" + couplingDesc(jc) + \")\"));\n });\n tbl.appendChild(blockHeadRow(\"Coupling 2\"));\n tbl.appendChild(couplingInfoRow(\"Coupling 2 (\" + couplingDesc(pkg.coupling_nodes[1]) + \")\"));\n }\n\n if (drivenComps.length){\n tbl.appendChild(blockHeadRow(\"Driven Unit\"));\n drivenComps.forEach(function(c){ tbl.appendChild(puckRow(pkg, c)); });\n }\n host.appendChild(tbl);\n host.appendChild(el(\"div\",\"hint\",\"Vibration sensors shown are the calculated defaults - edit any box to override that component's count. Coupling blocks never take a sensor, by design.\"));\n var pricing = el(\"div\",\"reviewPricing\");\n pricing.appendChild(textEl(\"div\",\"qEyebrow\",\"MONTHLY PRICE PER ASSET\"));\n pricing.appendChild(textEl(\"div\",\"quoteBig\",money(p.total) + \" / month\"));\n if(p.base) pricing.appendChild(priceRow(\"Asset base \u00b7 up to 4 measurement points\", money(p.base)));\n if(p.extraSensors) pricing.appendChild(priceRow(p.extraSensors + \" additional measurement points \u00d7 \" + money(CONFIG.EXTRA_SENSOR_MONTHLY) + \"/month\", money(p.extraSensors * CONFIG.EXTRA_SENSOR_MONTHLY)));\n if(p.mca) pricing.appendChild(priceRow(\"Motor current sensor \u00b7 \" + (p.mcaTier === \"with_vibration\" ? \"with vibration\" : \"standalone\"),money(p.mca) + \"/month\"));\n if(p.base) pricing.appendChild(textEl(\"p\",\"hint\",p.extraSensors ? \"This is a large asset. Extended coverage is $18 per additional measurement point per month, above the first four included in its $79 monthly base.\" : \"$79 per asset per month includes up to four measurement points. Each asset gets its own allowance.\"));\n if(p.mca) pricing.appendChild(textEl(\"p\",\"hint\",motorCurrentPricingNote(p)));\n host.appendChild(pricing);\n\n var row = el(\"div\",\"btnrow\");\n var backBtn = el(\"button\",\"btn ghost\",\"\u2190 Back to blocks\");\n backBtn.type=\"button\";\n backBtn.addEventListener(\"click\", function(){ pkg.activeTab = TABS.length-1; renderCard(); });\n /* v8 fix: the tab bar lets a user jump straight back to any unlocked tab (i \u003c= maxUnlocked),\n edit it, then jump straight to the last tab and hit Finish without ever passing through a\n Next button - the only place recomputeUnlock() ran mid-edit. Finish itself calls\n recomputeUnlock() but unconditionally opens Review regardless of the result, so Review could\n render (correctly) showing \"path incomplete,\" while this Save button stayed enabled and\n would still add the asset to the plant with a since-invalidated block. Gating it on\n verify.ok closes that path: Review can still be *viewed* in an incomplete state (useful for\n seeing exactly what's wrong), but nothing incomplete can be saved into the hierarchy. */\n var saveBtn = el(\"button\",\"btn\", editingId ? \"Save changes\" : \"Add to plant\");\n saveBtn.type=\"button\";\n saveBtn.disabled = !verify.ok;\n saveBtn.textContent = editingId ? \"Save changes\" : \"Add to plant\";\n function commitReviewed(){\n var isNew = !editingId;\n if (!verify.ok || !commitAssetToCart(pkg)) return false;\n if (isNew) track(\"plant_builder_asset_completed\", { machine_type: pkg.machineType || \"unspecified\" });\n renderTree(); renderCard();\n return true;\n }\n saveBtn.addEventListener(\"click\", commitReviewed);\n var resultsBtn = actionButton(\"Save \u0026 review results\",\"btn\",function(){\n if (commitReviewed()) reviewResults();\n });\n resultsBtn.disabled = !verify.ok;\n row.appendChild(backBtn); row.appendChild(saveBtn); row.appendChild(resultsBtn);\n host.appendChild(row);\n if (!verify.ok){\n host.appendChild(el(\"div\",\"lockNote\",\"This configuration can't be saved yet - go back to the block(s) listed above (\\\"Drivetrain path - incomplete\\\") and fix them first.\"));\n }\n}\n\nfunction renderBaseline(pkg, host){\n host.appendChild(el(\"div\",\"sectionHead\",\"Baseline\"));\n var nameInp = document.createElement(\"input\"); nameInp.type=\"text\"; nameInp.value=pkg.name;\n nameInp.className = (pkg.name.trim()===\"\" \u0026\u0026 pkg._nameTouched) ? \"errField\" : \"\";\n nameInp.placeholder = pkg.machineType ? ('e.g. \"' + pkg.machineType.split(\" \")[0] + ' P-101\"') : 'e.g. \"Asset name / tag number\"';\n nameInp.setAttribute(\"data-refocus\", \"assetName\");\n nameInp.addEventListener(\"input\", function(){ pkg.name = nameInp.value; reRenderPreserveFocus(); });\n /* Bug fix (v3.2): this blur handler used to call renderCard() (and, briefly while fixing the\n zone field below, a deferred reRenderPreserveFocus()) - both turned out to cause a genuine\n infinite render loop. Removing the currently-focused input from the DOM (which every render\n does, since #card is fully torn down and rebuilt) fires a *synchronous* blur on it as a side\n effect of the removal itself, not just on a real user-driven blur. So any rerender ever\n triggered from this handler re-fires this same handler again on the very next render's\n teardown, forever, with no user interaction required. The flag is still set immediately;\n showing the red inline error just waits for whatever render happens next for an unrelated\n reason (there is almost always one within a keystroke or two), which is a fine trade for not\n re-rendering on every removal-induced blur. */\n nameInp.addEventListener(\"blur\", function(){ pkg._nameTouched = true; });\n var nameField = fieldWrap(\"Asset name\", null, nameInp, true);\n if (pkg.name.trim()===\"\" \u0026\u0026 pkg._nameTouched) nameField.appendChild(el(\"div\",\"fldErr\",\"Asset name is required.\"));\n host.appendChild(nameField);\n\n var existingZones = [];\n assets.forEach(function(a){ var z=(a.zone||\"\").trim(); if (z \u0026\u0026 existingZones.indexOf(z)===-1) existingZones.push(z); });\n var zoneInp = document.createElement(\"input\"); zoneInp.type=\"text\"; zoneInp.value = pkg.zone || \"\";\n zoneInp.setAttribute(\"list\",\"zoneList\");\n zoneInp.setAttribute(\"data-refocus\", \"assetZone\");\n zoneInp.placeholder = \"e.g. Zone 1, Building A, North Wing\";\n zoneInp.addEventListener(\"input\", function(){ pkg.zone = zoneInp.value; });\n host.appendChild(fieldWrap(\"Plant area / zone\", \"Optional. Groups this asset under a physical section of the plant - like Zone 1, Building A, or North Wing - so the hierarchy on the left reflects how the plant is actually laid out, not just a flat equipment list.\", zoneInp));\n var zoneList = document.createElement(\"datalist\"); zoneList.id = \"zoneList\";\n existingZones.forEach(function(z){ var o=document.createElement(\"option\"); o.value=z; zoneList.appendChild(o); });\n host.appendChild(zoneList);\n\n host.appendChild(fieldWrap(\"Criticality\",\"What happens if this asset unexpectedly stops. Drives whether electrical (motor current) monitoring gets recommended - never changes the vibration sensor count.\",\n optGroup([[\"very\",\"Production stops immediately\"],[\"high\",\"Significant reduction\"],[\"medium\",\"Some disruption\"],[\"low\",\"Little impact\"]], pkg.criticality, \"row4\", function(v){ pkg.criticality=v; renderCard(); }), true));\n}\n\n/* ================= PLANT INFO SCREEN (gate ahead of the tree/card workspace) ================= */\nfunction renderPlantScreen(){\n var host = document.getElementById(\"plantCard\");\n host.innerHTML = \"\";\n host.appendChild(el(\"div\",\"crumb\",\"About the plant\"));\n host.appendChild(el(\"div\",\"cardTitle\",\"Tell us what machines you have.\u003cbr\u003eWe do the engineering.\"));\n host.appendChild(el(\"div\",\"hint\",\"Answer what you can see - the machine, how it's driven, how many motors. Chladni works out the vibration sensors, the gateways, and the price, instantly.\"));\n host.appendChild(el(\"div\",\"scopeNote\",\"\u003cb\u003eScope note:\u003c/b\u003e this quotation tool applies to physically accessible rotating equipment - assets a technician can reach directly on the plant floor. Submerged or otherwise inaccessible equipment (e.g. submersible pumps), and assets already integrated into an existing continuous monitoring system (e.g. Bently Nevada), fall outside its scope. Guarding or enclosures do not affect eligibility, provided the asset remains reachable for sensor installation.\"));\n\n var companyInp = document.createElement(\"input\"); companyInp.type=\"text\"; companyInp.value=plant.company; companyInp.autocomplete=\"organization\";\n companyInp.addEventListener(\"input\", function(){ plant.company = companyInp.value; persistLocal(); });\n host.appendChild(fieldWrap(\"Company name\", null, companyInp, true));\n\n /* v11: client flagged the email box specifically as too small to comfortably read/type an\n address into - bumped both contact fields (kept the pair visually matched) to a dedicated\n larger class rather than resizing every text input in the tool, since the fix was scoped to\n this one complaint, not a general redesign. */\n var contactNameInp = document.createElement(\"input\"); contactNameInp.type=\"text\"; contactNameInp.className=\"bigInput\"; contactNameInp.value=plant.contactName; contactNameInp.autocomplete=\"name\";\n contactNameInp.addEventListener(\"input\", function(){ plant.contactName = contactNameInp.value; persistLocal(); });\n host.appendChild(fieldWrap(\"Your name\", \"Who Chladni should follow up with about this quote.\", contactNameInp, true));\n\n var contactEmailInp = document.createElement(\"input\"); contactEmailInp.type=\"email\"; contactEmailInp.className=\"bigInput\"; contactEmailInp.value=plant.contactEmail; contactEmailInp.autocomplete=\"email\";\n contactEmailInp.required = true;\n contactEmailInp.setAttribute(\"inputmode\", \"email\");\n contactEmailInp.setAttribute(\"spellcheck\", \"false\");\n contactEmailInp.setAttribute(\"placeholder\", \"name@company.com\");\n if (plant._emailErr){\n contactEmailInp.className += \" errField\";\n contactEmailInp.setAttribute(\"aria-invalid\", \"true\");\n }\n var emailField = fieldWrap(\"Your email\", \"Enter a real work email. Our team will use this address to follow up about your configuration.\", contactEmailInp, true);\n if (plant._emailErr) emailField.appendChild(el(\"div\",\"fldErr\", plant._emailErr));\n function setEmailErr(msg){\n plant._emailErr = msg || \"\";\n var existing = emailField.querySelector(\".fldErr\");\n if (plant._emailErr){\n contactEmailInp.className = \"bigInput errField\";\n contactEmailInp.setAttribute(\"aria-invalid\", \"true\");\n if (existing) existing.textContent = plant._emailErr;\n else emailField.appendChild(el(\"div\",\"fldErr\", plant._emailErr));\n } else {\n contactEmailInp.className = \"bigInput\";\n contactEmailInp.removeAttribute(\"aria-invalid\");\n if (existing) emailField.removeChild(existing);\n }\n }\n contactEmailInp.addEventListener(\"input\", function(){\n plant.contactEmail = contactEmailInp.value;\n persistLocal();\n if (plant._emailErr) setEmailErr(plantEmailError(plant.contactEmail));\n });\n contactEmailInp.addEventListener(\"blur\", function(){\n setEmailErr(plantEmailError(plant.contactEmail));\n });\n host.appendChild(emailField);\n\n var nameInp = document.createElement(\"input\"); nameInp.type=\"text\"; nameInp.value=plant.name; nameInp.placeholder='e.g. \"Riverside Plant\"';\n nameInp.addEventListener(\"input\", function(){ plant.name = nameInp.value; });\n host.appendChild(fieldWrap(\"Plant name\", null, nameInp));\n\n var indSel = document.createElement(\"select\");\n INDUSTRIES.forEach(function(opt){\n var o = document.createElement(\"option\"); o.value=opt; o.textContent=opt;\n if (opt === plant.industry) o.selected = true;\n indSel.appendChild(o);\n });\n indSel.addEventListener(\"change\", function(){ plant.industry = indSel.value; });\n host.appendChild(fieldWrap(\"Industry\", null, indSel));\n\n var countryInp = document.createElement(\"input\"); countryInp.type=\"text\"; countryInp.value=plant.country;\n countryInp.addEventListener(\"input\", function(){ plant.country = countryInp.value; });\n host.appendChild(fieldWrap(\"Country\", null, countryInp));\n\n var stateInp = document.createElement(\"input\"); stateInp.type=\"text\"; stateInp.value=plant.state;\n stateInp.addEventListener(\"input\", function(){ plant.state = stateInp.value; });\n host.appendChild(fieldWrap(\"State / Province\", null, stateInp));\n\n if (plant._err) host.appendChild(textEl(\"div\",\"fldErr\", plant._err));\n\n var row = el(\"div\",\"btnrow\");\n var startBtn = el(\"button\",\"btn\", assets.length ? \"Continue\" : \"+ Add asset\");\n startBtn.type=\"button\";\n startBtn.addEventListener(\"click\", function(){\n if (!plant.company.trim()){\n plant._err = \"Company name is needed so the quote has a name on it.\";\n renderPlantScreen();\n return;\n }\n if (!String(plant.contactName || \"\").trim()){\n plant._err = \"Enter your name so our team knows who to contact.\";\n renderPlantScreen();\n return;\n }\n var emailMsg = plantEmailError(plant.contactEmail);\n if (emailMsg){\n plant._err = \"\";\n plant._emailErr = emailMsg;\n renderPlantScreen();\n return;\n }\n plant._err = \"\";\n plant._emailErr = \"\";\n persistLocal();\n if (!plantContinueTracked){\n plantContinueTracked = true;\n track(\"plant_builder_plant_info_continue\", { industry: plant.industry });\n }\n enterAppStage();\n notifyPlantInquiry();\n });\n row.appendChild(startBtn);\n host.appendChild(row);\n\n /* v11: a \"Save progress\" action visible on this very first screen too, not only once at least\n one asset exists - filling in company/contact info alone was already being autosaved, but\n with nothing on screen confirming it (see the matching addition in renderTree()). */\n var saveRow = el(\"div\",\"quoteActions\");\n var saveBtn = el(\"button\",\"btn ghost\",\"Save progress\"); saveBtn.type=\"button\";\n saveBtn.title = \"Saves what's filled in so far to this browser.\";\n saveBtn.setAttribute(\"data-status-target\",\"saveStatusPlant\");\n saveBtn.addEventListener(\"click\", function(){ saveProgressNow(saveBtn); });\n saveRow.appendChild(saveBtn);\n var saveStatus = el(\"div\",\"hint\", savedStatusText()); saveStatus.id = \"saveStatusPlant\";\n saveRow.appendChild(saveStatus);\n host.appendChild(saveRow);\n if (plant._emailErr) contactEmailInp.focus();\n}\nfunction updateTopbarPlantLink(){\n /* v12-web: the topbar is gone in the Webflow-hosted build (the page has its own chrome); the\n plant box at the top of the tree is the \"edit plant details\" entry point now. Kept as a\n no-op so any shell that still renders #topbarRight keeps working. */\n var span = document.getElementById(\"topbarRight\");\n if (!span) return;\n span.innerHTML = \"\";\n var link = document.createElement(\"button\");\n link.type = \"button\"; link.className = \"topbarLink\";\n link.textContent = plant.company + (plant.name ? \" \u00b7 \" + plant.name : \"\");\n link.addEventListener(\"click\", function(){ showPlantStage(); });\n span.appendChild(link);\n}\nfunction enterAppStage(){\n appStage = \"app\";\n hideResumeBanner();\n document.getElementById(\"plantScreenOuter\").style.display = \"none\";\n document.getElementById(\"appgrid\").style.display = \"\";\n updateTopbarPlantLink();\n renderTree();\n renderCard();\n}\nfunction showPlantStage(){\n appStage = \"plant\";\n document.getElementById(\"appgrid\").style.display = \"none\";\n document.getElementById(\"plantScreenOuter\").style.display = \"\";\n renderPlantScreen();\n}\n\nfunction renderCard(){\n var host = document.getElementById(\"card\");\n host.innerHTML = \"\";\n /* v5: there is no longer a standalone \"what equipment are you adding?\" picker screen before\n Baseline (diagnostic #18) - as soon as there's no draft in progress, a blank one is created\n and the user goes straight to Baseline, then the 5-block sequence. The equipment type itself\n is now a Block 5 (Driven Unit) question. */\n if (!draft){ draft = newPackage(); editingId = null; }\n var pkg = draft;\n host.appendChild(textEl(\"div\",\"crumb\", pkg.machineCat || \"Add asset\"));\n host.appendChild(textEl(\"div\",\"cardTitle\", editingId ? \"Edit asset\" : (pkg.machineType ? \"New asset: \" + pkg.machineType : \"New asset\")));\n renderBaseline(pkg, host);\n\n if (!baselineValid(pkg)){\n host.appendChild(el(\"div\",\"lockNote\",\"Complete the baseline fields above (asset name, criticality) to unlock the 5-block drivetrain workflow.\"));\n return;\n }\n host.appendChild(el(\"div\",\"sectionHead\",\"Drivetrain - 5-Block Sequential Configuration\"));\n if (pkg.activeTab \u003e= TABS.length){\n renderReview(pkg, host);\n } else {\n renderTabbedWizard(pkg, host);\n }\n}\n\n/* ================= v10: BOOT - shareable-link restore, then offer to resume an autosave ================= */\nfunction hideResumeBanner(){\n document.getElementById(\"resumeBanner\").style.display = \"none\";\n}\nfunction showResumeBanner(state){\n var host = document.getElementById(\"resumeBanner\");\n host.innerHTML = \"\";\n var label = (state.plant \u0026\u0026 state.plant.company) ? state.plant.company : \"a previous session\";\n var bar = el(\"div\",\"resumeBanner\");\n bar.appendChild(textEl(\"span\",null,\"Pick up where you left off with \" + label + \"?\"));\n var btns = el(\"div\");\n var resumeBtn = document.createElement(\"button\"); resumeBtn.type=\"button\"; resumeBtn.textContent=\"Resume\";\n resumeBtn.addEventListener(\"click\", function(){\n applyRestoredState(state);\n hideResumeBanner();\n if (assets.length || appStage === \"app\"){ enterAppStage(); } else { renderPlantScreen(); }\n });\n var freshBtn = document.createElement(\"button\"); freshBtn.type=\"button\"; freshBtn.textContent=\"Start fresh\";\n freshBtn.style.marginLeft = \"8px\";\n freshBtn.addEventListener(\"click\", function(){\n try { localStorage.removeItem(AUTOSAVE_KEY); } catch(e){}\n hideResumeBanner();\n });\n btns.appendChild(resumeBtn); btns.appendChild(freshBtn);\n bar.appendChild(btns);\n host.appendChild(bar);\n host.style.display = \"\";\n}\nfunction isRestorableState(state){\n if (!state || !state.plant || !Array.isArray(state.assets)) return false;\n return state.assets.concat(state.draft ? [state.draft] : []).every(function(pkg){\n return pkg \u0026\u0026 typeof pkg.name === 'string' \u0026\u0026 Array.isArray(pkg.drivers) \u0026\u0026\n Array.isArray(pkg.intermediates) \u0026\u0026 Array.isArray(pkg.interCouplings) \u0026\u0026\n Array.isArray(pkg.coupling_nodes) \u0026\u0026 pkg.coupling_nodes.length===2 \u0026\u0026 pkg.driven_unit \u0026\u0026\n pkg.drivers.every(function(d){return d \u0026\u0026 typeof d.driver_type==='string';});\n });\n}\nfunction restoreSharedPage(state){\n applyRestoredState(state);\n if (assets.length || appStage==='app') enterAppStage(); else renderPlantScreen();\n if (state.view==='quote' \u0026\u0026 assets.length \u0026\u0026 leadCaptured()) openQuote();\n if (state.view==='cart' \u0026\u0026 assets.length) openQuote('quote');\n}\n/* The Webflow mount markup carries a plain-text title; swap in the logo lockup at boot. */\nfunction brandTopbar(){\n var bar = document.querySelector(\"#chladni-plant-builder .topbar\") || document.querySelector(\".topbar\");\n var title = bar \u0026\u0026 bar.querySelector(\"b\");\n if (!title || document.getElementById(\"brandLogo\")) return;\n title.innerHTML = \"\";\n title.appendChild(brandLogoImg(\"brandLogo onDark\"));\n title.appendChild(el(\"span\",\"brandDivider\"));\n title.appendChild(textEl(\"span\",\"brandProduct\",\"Plant builder\"));\n}\nfunction boot(){\n brandTopbar();\n /* The Pricing page footer code resets the title text on DOMContentLoaded, which fires after this deferred bundle. */\n if (document.readyState !== \"complete\") document.addEventListener(\"DOMContentLoaded\", brandTopbar);\n var restored = bootRestore();\n track(\"plant_builder_page_view\", { restored: restored });\n}\nfunction bootRestore(){\n var hash = location.hash || \"\";\n if (hash.indexOf(\"#s=\") === 0){\n var linked = decodeState(hash.slice(3));\n if (isRestorableState(linked)){\n try { history.replaceState(null, \"\", location.pathname + location.search); } catch(e){}\n restoreSharedPage(linked);\n return \"share_link\";\n }\n }\n var embedded = document.getElementById(\"embeddedQuoteState\");\n if (embedded){\n var fromFile = null;\n try { fromFile = JSON.parse(embedded.textContent); } catch(e){}\n if (isRestorableState(fromFile)){ restoreSharedPage(fromFile); return \"quote_file\"; }\n }\n try {\n var raw = localStorage.getItem(AUTOSAVE_KEY);\n var saved = raw ? decodeState(raw) : null;\n if (isRestorableState(saved) \u0026\u0026 (saved.assets.length || saved.draft || saved.plant.company)){\n applyRestoredState(saved);\n if (assets.length || draft || appStage === \"app\"){ enterAppStage(); } else { renderPlantScreen(); }\n return \"autosave\";\n }\n } catch(e){}\n renderPlantScreen();\n return \"none\";\n}\nwindow.addEventListener(\"beforeunload\", persistLocal);\n/* Verified team-review delivery through the site's existing Webflow form.\n Include after the builder helpers; this file performs no requests until clicked. */\nvar TEAM_REVIEW_SUCCESS = \"Your results have been sent to our team. We\u2019ll review your configuration and reach out to you.\";\nvar TEAM_REVIEW_TIMEOUT_MS = 30000;\nvar teamReviewBusy = false;\nvar teamReviewPending = null;\nvar teamReviewFeedback = null;\n\nfunction teamReviewStableValue(value){\n if (Array.isArray(value)) return value.map(teamReviewStableValue);\n if (!value || typeof value !== \"object\") return value;\n var result = {};\n Object.keys(value).sort().forEach(function(key){\n if (key.charAt(0) === \"_\" || [\"activeTab\",\"maxUnlocked\",\"advancedOpen\",\"teamReviewReceipt\",\"inquiryReceipt\",\"leadSentAt\",\"quoteSentTotal\"].indexOf(key) !== -1) return;\n result[key] = teamReviewStableValue(value[key]);\n });\n return result;\n}\n/* Two independent 32-bit FNV-1a lanes plus serialized length keep saved receipts\n compact. This is a change detector, never an authentication/security token. */\nfunction teamReviewCompactFingerprint(text){\n var first = 0x811c9dc5, second = 0x9e3779b9;\n for (var i=0; i\u003ctext.length; i++){\n var code = text.charCodeAt(i);\n first = Math.imul(first ^ (code \u0026 255), 0x01000193) \u003e\u003e\u003e 0;\n first = Math.imul(first ^ (code \u003e\u003e\u003e 8), 0x01000193) \u003e\u003e\u003e 0;\n second = Math.imul(second ^ (code \u003e\u003e\u003e 8), 0x01000193) \u003e\u003e\u003e 0;\n second = Math.imul(second ^ (code \u0026 255), 0x01000193) \u003e\u003e\u003e 0;\n }\n function hex(value){ return (\"00000000\"+value.toString(16)).slice(-8); }\n return \"review-v1:\"+hex(first)+hex(second)+\":\"+text.length;\n}\nfunction teamReviewFingerprint(){\n var contact = {};\n [\"company\",\"name\",\"industry\",\"country\",\"state\",\"contactName\",\"contactEmail\"].forEach(function(key){\n contact[key] = String(plant[key] || \"\").trim();\n });\n var t = totals();\n return teamReviewCompactFingerprint(JSON.stringify(teamReviewStableValue({\n quoteId: quoteNumber || \"\", plant: contact, assets: assets,\n totals: { assets:t.assets, pucks:t.pucks, monthly:t.monthly }\n })));\n}\nfunction teamReviewConfirmed(fingerprint){\n var receipt = plant.teamReviewReceipt;\n return !!(receipt \u0026\u0026 receipt.fingerprint === fingerprint \u0026\u0026 receipt.acceptedAt \u0026\u0026 !isNaN(Date.parse(receipt.acceptedAt)));\n}\nfunction teamReviewStatusText(){\n var fingerprint;\n try { fingerprint = teamReviewFingerprint(); } catch(e) { return \"\"; }\n if (teamReviewConfirmed(fingerprint)) return TEAM_REVIEW_SUCCESS;\n if (teamReviewFeedback \u0026\u0026 (teamReviewBusy || !teamReviewFeedback.fingerprint || teamReviewFeedback.fingerprint === fingerprint)) return teamReviewFeedback.message;\n return \"\";\n}\nfunction teamReviewShow(message, kind, fingerprint){\n teamReviewFeedback = {message:message,kind:kind || \"status\",fingerprint:fingerprint || \"\"};\n var status = document.getElementById(\"teamReviewStatus\");\n if (status){\n status.textContent = message;\n status.setAttribute(\"role\", kind === \"error\" ? \"alert\" : \"status\");\n status.setAttribute(\"aria-live\", \"polite\");\n }\n var button = document.getElementById(\"teamReviewBtn\");\n if (button){ button.disabled = teamReviewBusy; button.textContent = teamReviewBusy ? \"Sending\u2026\" : \"Submit to Chladni team for review\"; }\n}\nfunction teamReviewRequestData(raw){\n if (!raw) return {};\n if (typeof raw === \"object\") return raw;\n if (typeof raw !== \"string\") return {};\n if (/^\\s*\\{/.test(raw)){\n try { return JSON.parse(raw); } catch(e) { return {}; }\n }\n var result = {};\n try {\n raw.split(\"\u0026\").forEach(function(pair){\n var index = pair.indexOf(\"=\"), key = index \u003c 0 ? pair : pair.slice(0,index), value = index \u003c 0 ? \"\" : pair.slice(index+1);\n result[decodeURIComponent(key.replace(/\\+/g,\" \"))] = decodeURIComponent(value.replace(/\\+/g,\" \"));\n });\n } catch(e){ return {}; }\n return result;\n}\nfunction teamReviewRequestField(data, name){\n if (data.fields \u0026\u0026 typeof data.fields === \"object\" \u0026\u0026 data.fields[name] != null) return String(data.fields[name]);\n if (data[\"fields[\"+name+\"]\"] != null) return String(data[\"fields[\"+name+\"]\"]);\n if (typeof data.get === \"function\"){\n var value = data.get(\"fields[\"+name+\"]\");\n if (value != null) return String(value);\n }\n return \"\";\n}\nfunction teamReviewRequestMatches(settings, expected){\n if (!settings || !/^(?:https?:\\/\\/[^/?#]+)?\\/api\\/v1\\/form\\//.test(String(settings.url || \"\"))) return false;\n var data = teamReviewRequestData(settings.data);\n var name = typeof data.get === \"function\" ? data.get(\"name\") : data.name;\n return name === \"Plant Builder Lead\" \u0026\u0026\n teamReviewRequestField(data,\"stage\") === \"team_review\" \u0026\u0026\n teamReviewRequestField(data,\"quote_id\") === expected.quoteId \u0026\u0026\n teamReviewRequestField(data,\"contact_email\") === expected.email \u0026\u0026\n teamReviewRequestField(data,\"config_link\") === expected.configLink;\n}\nfunction teamReviewResponseHasError(value){\n if (Array.isArray(value)) return value.length \u003e 0;\n if (value \u0026\u0026 typeof value === \"object\") return Object.keys(value).length \u003e 0;\n return !!value;\n}\nfunction submitTeamReview(){\n // Both actions use the same native form. Wait before replacing its input values.\n if (typeof inquiryNotificationBusy !== \"undefined\" \u0026\u0026 inquiryNotificationBusy \u0026\u0026 inquiryNotificationPending){\n teamReviewShow(\"Finishing your contact submission\u2026\",\"status\");\n return inquiryNotificationPending.then(function(){ return submitTeamReview(); });\n }\n if (teamReviewBusy) return teamReviewPending || Promise.resolve(false);\n function invalid(message){ teamReviewShow(message,\"error\"); return Promise.resolve(false); }\n if (!leadCaptured()) return invalid(\"Add your name and a valid email before sending your results to our team.\");\n if (!assets.length) return invalid(\"Add at least one asset before requesting a review.\");\n if (!cartReady()) return invalid(\"Complete the required details for every asset before requesting a review.\");\n if (hasPendingCartChanges()) return invalid(\"Save your current asset changes before sending your results to our team.\");\n\n ensureQuoteNumber();\n var fingerprint = teamReviewFingerprint();\n if (teamReviewConfirmed(fingerprint)){\n teamReviewShow(TEAM_REVIEW_SUCCESS,\"success\",fingerprint);\n return Promise.resolve(true);\n }\n var form = document.querySelector('form[data-name=\"Plant Builder Lead\"]');\n var jq = window.jQuery;\n var formsRuntime;\n try { formsRuntime = window.Webflow \u0026\u0026 typeof window.Webflow.require === \"function\" \u0026\u0026 window.Webflow.require(\"forms\"); } catch(e) {}\n if (!form || typeof jq !== \"function\" || !formsRuntime) return invalid(\"The review form is not ready. Refresh this page and try again, or contact us directly.\");\n var eventHost = jq(document);\n if (!eventHost || typeof eventHost.on !== \"function\" || typeof eventHost.off !== \"function\") return invalid(\"The review form is not ready. Refresh this page and try again.\");\n var t = totals(), expected = {quoteId:quoteNumber,email:String(plant.contactEmail).trim(),configLink:shareLinkUrl(\"quote\")};\n if (!expected.configLink) return invalid(\"Your configuration could not be prepared. Save your progress, refresh this page and try again.\");\n var fields = {\n stage:\"team_review\", contact_name:String(plant.contactName).trim(), contact_email:expected.email,\n company:plant.company, plant_name:plant.name, industry:plant.industry, country:plant.country, state:plant.state,\n asset_count:t.assets, puck_count:t.pucks, monthly_total:Math.round(t.monthly),\n quote_id:expected.quoteId, config_link:expected.configLink, page_url:location.href.split(\"#\")[0]\n };\n var missing = Object.keys(fields).some(function(name){ return !form.querySelector('[name=\"'+name+'\"]'); });\n if (missing) return invalid(\"The review form is missing a required field. Please contact us directly while we fix it.\");\n Object.keys(fields).forEach(function(name){ setField(form,name,fields[name]); });\n\n teamReviewBusy = true;\n teamReviewShow(\"Sending your results to our team\u2026\",\"status\",fingerprint);\n var pending = new Promise(function(resolve){\n var settled = false, timeout;\n function finish(ok,message){\n if (settled) return;\n settled = true;\n clearTimeout(timeout);\n eventHost.off(\".plantTeamReview\");\n if (ok){\n plant.teamReviewReceipt = {fingerprint:fingerprint,acceptedAt:new Date().toISOString()};\n try { persistLocal(); } catch(e) {}\n try { track(\"plant_builder_review_requested\",{asset_count:t.assets}); } catch(e) {}\n try { recordTeamReviewConversion(expected.quoteId); } catch(e) {}\n }\n teamReviewShow(message,ok ? \"success\" : \"error\",fingerprint);\n resolve(ok);\n }\n function failure(){ finish(false,\"We could not confirm delivery. Please try again. Your results are still saved here.\"); }\n eventHost.on(\"ajaxSuccess.plantTeamReview\",function(event,xhr,settings){\n if (!teamReviewRequestMatches(settings,expected)) return;\n var response = xhr \u0026\u0026 xhr.responseJSON;\n if (response \u0026\u0026 response.code === 200 \u0026\u0026 !teamReviewResponseHasError(response.error) \u0026\u0026 !teamReviewResponseHasError(response.errors) \u0026\u0026 response.success !== false){\n var message = TEAM_REVIEW_SUCCESS;\n if (teamReviewFingerprint() !== fingerprint) message += \" Your latest changes have not been sent; send again to include them.\";\n finish(true,message);\n } else { failure(); }\n });\n eventHost.on(\"ajaxError.plantTeamReview\",function(event,xhr,settings){\n if (teamReviewRequestMatches(settings,expected)) failure();\n });\n timeout = setTimeout(function(){ finish(false,\"Delivery is taking longer than expected, so we cannot confirm it. Please try again. Your results are still saved here.\"); },TEAM_REVIEW_TIMEOUT_MS);\n try {\n if (typeof form.requestSubmit === \"function\") form.requestSubmit();\n else {\n var submit = form.querySelector('[type=\"submit\"]');\n if (!submit || typeof submit.click !== \"function\") throw new Error(\"No form submit control\");\n submit.click();\n }\n } catch(e){ failure(); }\n });\n var operation = pending.catch(function(){\n eventHost.off(\".plantTeamReview\");\n teamReviewShow(\"We could not confirm delivery. Please try again.\",\"error\",fingerprint);\n return false;\n }).finally(function(){\n if (teamReviewPending === operation){\n teamReviewBusy = false;\n teamReviewPending = null;\n var status = teamReviewFeedback;\n teamReviewShow(status.message,status.kind,status.fingerprint);\n }\n });\n teamReviewPending = operation;\n return operation;\n}\n\n/* Static results export and explicit sharing. Loaded after the builder so it replaces the\n legacy page-cloning export without inheriting scripts, tracking or ordering controls. */\nvar RESULTS_WORDMARK_DATA_URI = \"data:image/svg+xml;base64,PHN2ZyB3aWR0aD0iMTc2IiBoZWlnaHQ9IjIyIiB2aWV3Qm94PSIwIDAgMTc2IDIyIiBmaWxsPSJub25lIiB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciPgo8cGF0aCBkPSJNMTA3LjI1NCAyLjE3NzY0QzExMC40ODkgMi4wMzE4NCAxMTQuMzIxIDIuMTk0MTggMTE3LjYyMiAyLjEzODcxQzExOS4zODggMi4xMDkwMSAxMjMuNzA2IDIuMDU4NCAxMjUuMTgzIDIuMjUyMDFDMTI2LjI0NiAyLjM4NzMgMTI3LjI3IDIuNzI1MjMgMTI4LjE5NyAzLjI0NjMzQzEyOS42NzcgNC4wOTI0IDEzMC40MzEgNS4yOTI0NyAxMzAuOTE4IDYuODY4ODFDMTMxLjYzMyA5LjE4MzU1IDEzMC44MjIgMTEuNjUwNSAxMjkuNjggMTMuNjk3OUMxMjkuMDI1IDE0Ljg3MjggMTI2LjQ3OCAxNy43MDczIDEyNS4xNzEgMTguMDM0OEMxMjQuNjk3IDE4LjY1MzQgMTIxLjg2MiAxOS41MTkzIDEyMS4wNDUgMTkuNjA1QzExOC41NDIgMTkuODY3OCAxMTUuOTMgMTkuODQ4IDExMy40MDcgMTkuODIxMkMxMTAuNzI3IDE5Ljc5MjggMTA3Ljk2MSAxOS44OTAxIDEwNS4yOTEgMTkuNzYxNEMxMDUuMjQ4IDE5LjcxNjggMTA1LjE5OSAxOS42NjUzIDEwNS4xNDUgMTkuNjA2OUMxMDYuMDc3IDE2LjA5MzcgMTA3LjI1NCAxMi4xMDEyIDEwOC4xNjYgOC42NDI0OUMxMDguNjEyIDYuOTQ4MDMgMTA4LjkwNCA2LjI3ODI4IDEwOS4wODYgNC45OTM1OEMxMDkuMTc1IDQuMzY0NTcgMTA3LjQxOCAyLjkzMTk2IDEwNy4yNTQgMi4xNzc2NFpNMTExLjUxIDE2LjE4MzlDMTEyLjk1MiAxNi4xNzIgMTE0LjM5NiAxNi4xNjk0IDExNS44MzggMTYuMTc2M0MxMTguNTAzIDE2LjE4MDggMTIwLjIzMSAxNi4zODAyIDEyMi41MzYgMTQuOTExOEMxMjQuOTQ3IDEzLjA1OTkgMTI2Ljg0NSAxMC4xNDczIDEyNC44NTkgNy4yNjM1NEMxMjQuNTEzIDYuNzYxNDQgMTIyLjk4IDUuODc4NjkgMTIyLjM3MiA1Ljg1NjU5QzExOC44NjEgNS43Mjg5MiAxMTUuMTYgNS43MTkyNiAxMTEuNjUxIDUuNzY2OThDMTExLjg5NSA2LjYxOTg5IDExMy40NTkgOC4wNTYxNiAxMTMuNTkxIDguNDg4NDhDMTEzLjYyNiA4LjYwNTM2IDExMS42OTQgMTUuNDY3MiAxMTEuNTEgMTYuMTgzOVoiIGZpbGw9IiMxQjFCMUIiLz4KPHBhdGggZD0iTTYwLjUwMjYgMi4xODQ3NkM2My4wNjMgMi4xMTA3NCA2NS45ODk2IDIuMTIyNTEgNjguNTU0NCAyLjE3MDIyQzY3LjYyMzcgNi4xNjQyNCA2NS43OTk2IDExLjU3NjggNjQuODQ2MiAxNS43MDA0QzY0LjgzMjIgMTUuNzYwOSA2NC43Njg4IDE1LjgyMTggNjQuNzQzIDE1Ljk1NUM2NC43NTI4IDE1Ljk3MDEgNjQuNzYzMiAxNS45ODM0IDY0Ljc3NjMgMTUuOTk2MUM2NC45NTQ2IDE2LjE2OTQgNjUuNDU3OCAxNi4xODA1IDY1LjY5MzQgMTYuMTgzOEM2OC4wMjc1IDE2LjIxNjMgNzAuMzc0MSAxNi4xNjE5IDcyLjcxMDIgMTYuMTY5OUM3My43NzgyIDE2LjE3MzYgNzcuODk1IDE2LjM0MzUgNzguNjg5NCAxNi4xMTM2Qzc5LjQ0NzQgMTUuODk0MSA4Ny4yODI2IDUuNTUwMiA4OC45MDA1IDQuMDI5NjlDODguNzMyMiAzLjM2NjI2IDg3Ljc5OTggMi44MzY3IDg3LjczMDcgMi4zNDI3MUw4Ny44NDQ5IDIuMjA1MzZDODguNDYwNCAyLjExNDY2IDkwLjI3ODQgMi4xNTczIDkxLjAzNTUgMi4xNTY4NEM5My4zNDU5IDIuMTUzMzMgOTUuNjU2MyAyLjE2MjMzIDk3Ljk2NjYgMi4xODM4NEM5OC40MTM4IDQuMDQwODMgOTkuMDA0NSA1LjkyNTggOTkuNTIxNiA3Ljc3MDIzTDEwMS40NzkgMTQuOTkwNEMxMDEuNzczIDE2LjA2NiAxMDIuNTgyIDE4LjY2ODMgMTAyLjczOCAxOS41OTk0TDEwMi42MDYgMTkuNzQ4OEMxMDIuMDcyIDE5LjgwNCAxMDEuMjM4IDE5LjcwNDIgMTAwLjY0OSAxOS43MzNDOTguMzI4OCAxOS44NDYyIDk1Ljk5NjMgMTkuODE4MiA5My42NzE4IDE5LjgxNUM5Mi43Mzc1IDE5LjgxNjcgODYuNjc2OSAxOS44NTEzIDg2LjA4OCAxOS43MDUxQzg2LjA4OCAxOS40MTI0IDg4LjAxNDEgMTcuMzkzIDg4LjI2OTEgMTYuOTMzN0M4OC41NTc4IDE2LjQxNDUgOTAuODc4OCAxNi41OTMxIDkxLjQ4OTggMTYuNTk2NEw5Ni4yMTE1IDE2LjU5OTVDOTYuMDkwMiAxNi4yMTQyIDk1LjgyODkgMTUuMDEzIDk1LjcwODUgMTQuNzQxN0M5NS4xNzkgMTMuNTU1MSA5My42MTc4IDYuMjY1NDUgOTMuMDg2NCA1Ljg1MTYyQzkzLjA0OTIgNS44NjU0NCA5MS4zMzEzIDcuOTIwODMgOTEuMTgyNSA4LjA5OTY1TDg3LjUzNDEgMTIuNTE1NUw4My43Njk3IDE3LjA4OTNDODMuMDk5MyAxNy45MDU4IDgyLjQwNiAxOC43NjY4IDgxLjcwNTUgMTkuNTYxNUM4MS42NDE3IDE5LjYzNDUgODEuNDk4MyAxOS42NTAzIDgxLjM5ODIgMTkuNjYyNEM4MC41NDcyIDE5Ljc2NiA3OS42MjQ1IDE5LjY4NzkgNzguNzcwOCAxOS43MjQzQzc3LjA0MDUgMTkuNzk4MSA3NS4zMjcgMTkuODMwOSA3My41OTY2IDE5LjgxNjNDNjkuMTM3IDE5Ljc3ODUgNjQuNjY1NyAxOS44OTk1IDYwLjIwOTcgMTkuNzQxMUM1OS43MDg2IDE5LjcyMzMgNTguNzY1MyAxOS44NjE3IDU4LjQyMTMgMTkuNjE4MkM1OC4zMzA5IDE5LjIzOTcgNTguNDkxMiAxOS4xMjUgNTguNTc5MyAxOC43NDZDNTguOTMwOCAxNy4yMDQ4IDU5LjQxMTQgMTUuODA0MSA1OS44MjUyIDE0LjI3MTdDNjAuNjQ1MyAxMS4yMzM0IDYxLjUzNzQgOC4xMjM4NiA2Mi4zMDMzIDUuMDc5MzRDNjIuNDczOCA0LjQwMDk5IDYwLjU1NjUgMi45NjA1NiA2MC41MDI2IDIuMTg0NzZaIiBmaWxsPSIjMUIxQjFCIi8+CjxwYXRoIGQ9Ik0xMzMuNjU3IDIuMTc1MDNDMTM0Ljc3MyAyLjA1NDI0IDE0NC43ODkgMi4wNDE0OCAxNDUuNzUyIDIuMjM5NDRDMTQ2LjAxNCAyLjI5MzA0IDE1MC42NDcgMTMuNjg2NCAxNTEuMTI5IDE0Ljg1NjdDMTUxLjM0OSAxNS4zODcyIDE1MS43NjEgMTYuNjg4OCAxNTIuMDQgMTcuMTA1NkMxNTIuNzIxIDE2LjgxNTYgMTU1LjU0NCA0LjEwMDc5IDE1Ni4zODIgMi4zNjUxN0MxNTYuODQzIDIuMDE5NDUgMTYwLjg4MSAyLjE4NTAxIDE2MS43NjEgMi4xOTU3MUMxNjAuODAyIDYuMDYxMDQgMTU5LjU2NiA5LjkxNjAyIDE1OC42MDUgMTMuNzg2NkMxNTguMjE2IDE1LjM1MTggMTU3LjQyMiAxNy41NTQgMTU3LjE1NSAxOS4wOTgzQzE1Ny4wODIgMTkuMTYwNCAxNTcuMDA0IDE5LjU0MjEgMTU2Ljk3OSAxOS42NDk5QzE1Ni42MjggMTkuNzcxMiAxNDcuOTMzIDE5Ljc1OCAxNDcuNDE4IDE5LjYwMjVDMTQ3LjAwNyAxOC44NzQgMTQ2LjM0NyAxNy4wNzQ1IDE0NS45NzcgMTYuMjE4QzE0NC40MTkgMTIuNjEyNiAxNDIuOTggOC45NDIxOCAxNDEuNDQxIDUuMzI1NTVDMTQxLjM0NyA1LjEwNDgzIDE0MS4yNjggNC45ODUwNSAxNDEuMDU5IDQuOTA5MDlDMTQwLjcwMSA1LjI1MTQyIDEzNy40MTcgMTkuMTYzNiAxMzYuODE0IDE5LjY2MDRDMTM2LjI3NyAxOS43NDU3IDEzMi4xOTcgMTkuNzQ4NyAxMzEuNzAyIDE5LjYzMDZDMTMxLjY4NiAxOS42MTM4IDEzMS42MDIgMTkuNTM2NyAxMzEuNjAxIDE5LjUyMTlDMTMxLjU5NyAxOS40OTY4IDEzNC4xOTcgMTAuMDQxNCAxMzQuNDUxIDkuMTM1MzZDMTM0Ljc0OCA4LjA3NzM1IDEzNS42ODcgNS45MDY2MSAxMzUuNTA5IDQuODU5MDNDMTM1LjM3NiA0LjA3MTM2IDEzNC4wOTIgMy4wMjY1IDEzMy42NTcgMi4xNzUwM1oiIGZpbGw9IiMxQjFCMUIiLz4KPHBhdGggZD0iTTIuMTk2MjUgMi4xNzQ2N0M3LjcyMzQyIDIuMTEzNDEgMTMuMzM1NCAyLjE2MjY0IDE4Ljg2NTIgMi4xNjY2N0wyNC41MjczIDIuMTU4OTNDMjUuMDI0NSAyLjE1NjgyIDI3LjgxMTkgMi4wNDI2MyAyOC4wNTcxIDIuMzA4OTlDMjguMTM0NyAyLjg2MTk0IDI4LjA0OTcgMi43MTk5NSAyNy44NzYyIDMuMjM1ODVDMjcuNjUgMy45MDkxMSAyNi41MzcyIDkuMDM2MTUgMjYuMDU2NSA5LjA2Njc0QzI1LjE3MjkgOS4xMjMwMyAyMS43MDMyIDkuMjc1MjMgMjEuMDk4NSA5LjAxNjY0QzIxLjAyMjggOC45MjQwOSAyMC45MzE1IDguODQ4ODkgMjAuOTU4NCA4Ljc0MTEzQzIxLjIyMjEgNy42ODMwNyAyMS42NDczIDYuOTA1OTEgMjEuNjgwNiA1Ljc1ODg2QzIwLjA3NyA1Ljc4OTQ4IDYuODAzNzkgNS42OTM0NiA2LjYyMTQ2IDUuODQ5NzJDNi43MzczOCA2LjU2OTQ0IDguMzY3MDMgNy45MTE4MSA4LjM2OTY5IDguNjc1MDRDOC4zNzM1OSA5LjgwNjQxIDYuMjI4MjggMTUuNTM2MSA2LjY2MjQ2IDE2LjExOTRDNy4zNDg3NSAxNi4yMTU5IDguNjExNzEgMTYuMTc0NSA5LjM0NjI2IDE2LjE2OTlMMTguODY1IDE2LjE2NzlDMTkuMjQ2MSAxNS4wODgzIDE5LjU0MzMgMTMuODY5NCAxOS44NDE3IDEyLjc1OEMyMC40NzMxIDEyLjcxMTUgMjQuNjE4OCAxMi43MDI4IDI1LjA2MDcgMTIuODIxM0MyNS4yOTc5IDEzLjU2MzEgMjMuNjczMSAxOC40ODA3IDIzLjM2MzUgMTkuNjAzNUMyMi44ODkxIDE5Ljg3ODUgMS43MjM1NSAxOS45MTM3IDAuMDQwOTM1NiAxOS42Mjk4Qy0wLjA1NjgyNzkgMTkuMzUyOSAzLjcwMzg4IDYuODM0NTQgMy45NTg2NSA1LjQwNjFDNC4yMDc0OCA0LjAxMTA3IDIuMzkzNzIgMy4yNjA2MSAyLjE5NjI1IDIuMTc0NjdaIiBmaWxsPSIjMUIxQjFCIi8+CjxwYXRoIGQ9Ik01Mi4xODYzIDIuMTg2MjhDNTIuOTAwMiAyLjE2MjAyIDU2Ljk5MTggMi4wOTMzOSA1Ny40MjQzIDIuMjUwNjlDNTcuNTQwNCAyLjgxMzQ2IDUzLjIxMzMgMTguMDU5IDUyLjc2NTggMTkuNzE5NkM1MS45MzM4IDE5LjcyMTEgNDcuOTc0MSAxOS43ODE4IDQ3LjQxMDUgMTkuNjI2OEM0Ny40NDM0IDE5LjE4OTEgNDguNTU0NCAxNS4xOTc5IDQ4Ljc0NDQgMTQuNjM0OEM0OC43MjgyIDE0LjQ5NzIgNDkuMTg3MiAxMy4zODg3IDQ5LjIwODggMTIuOTExNUM0OC43NTg0IDEyLjQ5MzggMzkuMTAwNyAxMi45MDgyIDM3LjYyNDcgMTIuNzAzOUwzNy41OTk2IDEyLjU4MTVDMzguMDg2NSAxMi4xNjU3IDQwLjQwNjQgOS4yMzA5OCA0MC42ODcgOS4yMTMxMUM0My4zMTQyIDkuMDQ2MDUgNDYuMjEwMSA5LjE3Nzk2IDQ4Ljg2MTkgOS4xNDM1QzQ5LjMzMTUgOS4xMzc0IDQ5Ljc3OTYgOS4xNDg4MyA1MC4yMjcxIDkuMDM4MjNDNTAuODkzMyA3Ljc3OTA0IDUwLjk2NzQgNS42NzU1MyA1MS42Mjc2IDQuMjk5MjZDNTEuODA4MyAzLjkyMjUgNTEuOTI5MiAyLjgwMTMxIDUyLjE4NjMgMi4xODYyOFoiIGZpbGw9IiMxQjFCMUIiLz4KPHBhdGggZD0iTTMxLjEyNjMgMi4xODgxNkMzMy44MTM1IDIuMTUxMzkgMzYuNTAxIDIuMTQzMzIgMzkuMTg4NCAyLjE2Mzk3QzM4LjM1MDMgNi4wNzUwNCAzNi43MDc5IDEwLjIwMTYgMzUuOTAzNyAxNC4xNjczQzM1LjYyMTggMTUuNTU2OSAzNC42NDEyIDE3Ljg1OTcgMzQuNTM2MyAxOS4yMDZMMzQuMzMzIDE5LjYwMDlDMzQuMDg2MSAxOS43ODY2IDI5LjQyMzcgMTkuODAxIDI5LjExNjkgMTkuNjEyN0MyOC44MjU5IDE5LjEwNTMgMjkuMjQxNyAxOC42ODcyIDI5LjM2OTQgMTguMTYyNkMzMC40Mzc3IDEzLjc3MjUgMzEuOTA2OCA5LjQ1OTI3IDMyLjkwODggNS4wNTU5MkMzMy4wNjM1IDQuMzc2MDkgMzEuMjA4MyAyLjk1Njk3IDMxLjEyNjMgMi4xODgxNloiIGZpbGw9IiMxQjFCMUIiLz4KPHBhdGggZD0iTTE2NC44MSAyLjE5MTQ3QzE2Ny4zMTUgMi4xMjY3MSAxNjkuOTg0IDIuMTUzNzkgMTcyLjQ5NiAyLjE4NTE0QzE3Mi41MjggMi4xODU1MiAxNzIuNjM1IDIuMzE1NDMgMTcyLjY2NyAyLjM0OTk5QzE3Mi41OTcgMi45NDY3NCAxNzEuNTk4IDYuMzkyMSAxNzEuMzc1IDcuMjA2ODVDMTcwLjI4OCAxMS4xOTAxIDE2OS4wNjIgMTUuMjI1OSAxNjguMDcxIDE5LjIzNTdMMTY3LjkyNCAxOS42MjY1QzE2Ny42NzYgMTkuNzk5NSAxNjMuMDc5IDE5LjcxNTQgMTYyLjQ5NiAxOS43MTYzQzE2My4zMjYgMTYuNjQ3MyAxNjQuMjE0IDEzLjU5NzcgMTY1LjAzOCAxMC41MjQ0QzE2NS40NzcgOS42NzQxMiAxNjUuNDAyIDguNzI5NzUgMTY1Ljc1NyA3Ljg3MTA2QzE2Ny4zNDggNC4wMTM0NyAxNjUuOTM0IDQuODYwNDcgMTY0Ljc0NCAyLjM1OTEzTDE2NC44MSAyLjE5MTQ3WiIgZmlsbD0iIzFCMUIxQiIvPgo8L3N2Zz4K\";\nfunction setResultsActionStatus(message){\n var node = document.getElementById(\"resultsActionStatus\") || document.getElementById(\"teamReviewStatus\");\n if (node){ node.textContent = message; node.setAttribute(\"role\",\"status\"); node.setAttribute(\"aria-live\",\"polite\"); }\n}\nfunction publicResultsLink(){\n try {\n var here = new URL(location.href), host = here.hostname.toLowerCase();\n if (here.protocol !== \"https:\" || !host || /^(localhost$|127\\.|0\\.|\\[?::1\\]?$)/.test(host) || /\\.(local|localhost)$/.test(host) || /^(10\\.|192\\.168\\.|169\\.254\\.|172\\.(1[6-9]|2\\d|3[01])\\.)/.test(host)) return null;\n return typeof shareLinkUrl === \"function\" ? shareLinkUrl(\"quote\") : null;\n } catch (e){ return null; }\n}\nfunction resultsFileName(){\n return String(ensureQuoteNumber()).replace(/[^A-Za-z0-9_-]/g,\"-\") + \"-results.html\";\n}\nfunction portableQuoteHTML(){\n ensureQuoteNumber();\n var source = document.getElementById(\"quoteSheet\");\n if (!source || !source.textContent.trim()) throw new Error(\"Open your results before downloading them.\");\n var sheet = source.cloneNode(true);\n sheet.removeAttribute(\"aria-modal\"); sheet.removeAttribute(\"role\"); sheet.removeAttribute(\"tabindex\");\n Array.prototype.forEach.call(sheet.querySelectorAll(\"button,form,input,textarea,select,script,iframe,object,embed,video,audio,link,meta,.quoteActionsRow,.quoteActions,.cartNav,.qTextButton,.qClose,.qSharePanel,#sharePanel,#teamReviewPanel,#teamReviewForm,.teamReviewPanel,.teamReviewForm,.teamReviewActions,.actionStatus,[role='status'],#resultsActionStatus,#teamReviewStatus\"),function(node){ node.remove(); });\n Array.prototype.forEach.call(sheet.querySelectorAll(\"*\"),function(node){\n Array.prototype.slice.call(node.attributes).forEach(function(attr){\n if (/^on/i.test(attr.name) || /^(contenteditable|inert|autofocus)$/i.test(attr.name)) node.removeAttribute(attr.name);\n });\n if (node.tagName === \"A\"){\n var href = node.getAttribute(\"href\") || \"\";\n if (!/^https:\\/\\//i.test(href)){ node.removeAttribute(\"href\"); }\n else { node.setAttribute(\"rel\",\"noopener noreferrer\"); }\n }\n });\n Array.prototype.forEach.call(sheet.querySelectorAll(\"details\"),function(node){ node.open = true; });\n Array.prototype.forEach.call(sheet.querySelectorAll(\"img\"),function(node){\n if (node.classList.contains(\"brandLogo\") || /chladni/i.test(node.alt || \"\")){\n node.src = RESULTS_WORDMARK_DATA_URI; node.removeAttribute(\"srcset\"); node.removeAttribute(\"sizes\"); node.removeAttribute(\"loading\");\n node.alt = \"Chladni\"; node.width = 176; node.height = 22;\n } else if (!/^data:image\\//i.test(node.getAttribute(\"src\") || \"\")){ node.remove(); }\n });\n Array.prototype.forEach.call(sheet.querySelectorAll(\"svg foreignObject,svg image\"),function(node){node.remove();});\n var doc = document.implementation.createHTMLDocument(\"Chladni results \u2014 \" + ensureQuoteNumber());\n doc.documentElement.lang = \"en\";\n var charset = doc.createElement(\"meta\"); charset.setAttribute(\"charset\",\"utf-8\"); doc.head.prepend(charset);\n var viewport = doc.createElement(\"meta\"); viewport.name = \"viewport\"; viewport.content = \"width=device-width, initial-scale=1\"; doc.head.appendChild(viewport);\n var privacy = doc.createElement(\"meta\"); privacy.httpEquiv = \"Content-Security-Policy\";\n privacy.content = \"default-src 'none'; img-src data:; style-src 'unsafe-inline'; base-uri 'none'; form-action 'none'\"; doc.head.appendChild(privacy);\n var style = doc.createElement(\"style\");\n var sourceStyle = document.getElementById(\"chladni-plant-builder-css\");\n var css = sourceStyle ? sourceStyle.textContent : \"\";\n css = css.replace(/@import\\s+[^;]+;/gi,\"\").replace(/url\\((?!\\s*['\\\"]?data:)[^)]*\\)/gi,\"none\");\n style.textContent = css + \"\\nhtml,body{margin:0;padding:0;background:#fff;color:#111;font-family:Arial,Helvetica,sans-serif}body{padding:28px 20px}#chladni-plant-builder{max-width:1200px;margin:0 auto;overflow:visible}#chladni-plant-builder .quoteSheet{max-width:none;margin:0;padding:24px;box-shadow:none;border:1px solid #ddd}#chladni-plant-builder .qSummary{position:static;top:auto}#chladni-plant-builder .qGrid{align-items:start}#chladni-plant-builder details\u003esummary{cursor:default;list-style:none}#chladni-plant-builder details\u003esummary::-webkit-details-marker{display:none}#chladni-plant-builder .qMeta{overflow-wrap:anywhere}#chladni-plant-builder .resultsRecord,#chladni-plant-builder .resultsReturn{margin-top:24px;border-top:1px solid #ddd;padding-top:20px}#chladni-plant-builder .resultsRecord h2,#chladni-plant-builder .resultsReturn h2{font-size:18px;margin-bottom:10px}#chladni-plant-builder .resultsRecord p,#chladni-plant-builder .resultsReturn p{font-size:13px;margin-bottom:12px;color:#555}#chladni-plant-builder .resultsRecord pre{font-family:ui-monospace,SFMono-Regular,Consolas,monospace;font-size:11px;line-height:1.5;white-space:pre-wrap;overflow-wrap:anywhere;background:#f5f5f5;padding:16px}#chladni-plant-builder .resultsReturn a{font-weight:700;color:#111;overflow-wrap:anywhere}#chladni-plant-builder .qHero{background:#f4f4f4!important;color:#111!important}#chladni-plant-builder .qHero *{color:#111!important}@media(max-width:700px){body{padding:10px}#chladni-plant-builder .quoteSheet{padding:18px}#chladni-plant-builder .qGrid{display:block}#chladni-plant-builder .qSummary{margin-top:24px}}@page{size:auto;margin:12mm}@media print{body{padding:0}#chladni-plant-builder .quoteSheet{padding:0;border:0}#chladni-plant-builder .qGrid{display:block}#chladni-plant-builder .qSummary{margin-top:22px;break-inside:avoid}#chladni-plant-builder .qAsset{break-inside:avoid}#chladni-plant-builder .qTop{break-after:avoid}#chladni-plant-builder .resultsRecord{break-before:page}#chladni-plant-builder .resultsReturn{display:none}a{color:#111}*{-webkit-print-color-adjust:exact;print-color-adjust:exact}}\";\n doc.head.appendChild(style);\n var root = doc.createElement(\"main\"); root.id = \"chladni-plant-builder\"; root.appendChild(doc.importNode(sheet,true)); doc.body.appendChild(root);\n var link = publicResultsLink();\n if (link){\n var section = doc.createElement(\"section\"); section.className = \"resultsReturn\";\n var heading = doc.createElement(\"h2\"); heading.textContent = \"Reopen this configuration\"; section.appendChild(heading);\n var note = doc.createElement(\"p\"); note.textContent = \"Anyone with this file or link can view the plant, contact and complete configuration details.\"; section.appendChild(note);\n var anchor = doc.createElement(\"a\"); anchor.href = link; anchor.rel = \"noopener noreferrer\"; anchor.textContent = \"View the complete results on Chladni\"; section.appendChild(anchor); root.appendChild(section);\n }\n var record = doc.createElement(\"section\"); record.className = \"resultsRecord\";\n var title = doc.createElement(\"h2\"); title.textContent = \"Complete configuration record\"; record.appendChild(title);\n var note = doc.createElement(\"p\"); note.textContent = \"A plain-text copy of the saved equipment, contact and configuration details accompanies the results above. Draft changes, when present, are marked separately and are not included in the displayed estimate.\"; record.appendChild(note);\n var snapshot = snapshotState();\n var data = {reference:ensureQuoteNumber(),issuedAt:quoteIssuedAt,plant:snapshot.plant,assets:snapshot.assets};\n if (snapshot.draft) data.unsavedDraft = snapshot.draft;\n var pre = doc.createElement(\"pre\");\n pre.textContent = JSON.stringify(data,function(key,value){ return /^(leadSentAt|quoteSentTotal|_err|_emailErr)$/.test(key) ? undefined : value; },2);\n record.appendChild(pre); root.appendChild(record);\n return \"\u003c!DOCTYPE html\u003e\\n\" + doc.documentElement.outerHTML;\n}\nfunction downloadQuotePage(){\n try {\n var blob = new Blob([portableQuoteHTML()],{type:\"text/html;charset=utf-8\"});\n var url = URL.createObjectURL(blob), anchor = document.createElement(\"a\");\n anchor.href = url; anchor.download = resultsFileName(); anchor.style.display = \"none\"; document.body.appendChild(anchor); anchor.click(); anchor.remove();\n setTimeout(function(){ URL.revokeObjectURL(url); },30000);\n setResultsActionStatus(\"Download started. The HTML file opens in any browser and can be printed or saved as a PDF.\");\n return true;\n } catch (error){ setResultsActionStatus(error.message || \"The download could not start. Please try again.\"); return false; }\n}\n/* Show a reliable on-page sharing panel first. Native sharing is opt-in. */\nfunction ensureResultsShareStyles(){\n if(document.getElementById('chladni-results-share-css'))return;\n var style=document.createElement('style');style.id='chladni-results-share-css';\n style.textContent='#chladni-plant-builder .qSharePanel{border:1px solid #dedede;border-top:3px solid #E2550A;background:#fff;box-shadow:0 12px 36px #00000009;margin:24px 0;padding:26px;border-radius:18px;scroll-margin-top:18px}#chladni-plant-builder .sharePanelHeader{display:flex;align-items:center;justify-content:space-between;gap:18px}#chladni-plant-builder .sharePanelHeader h2{font-size:26px;line-height:1.2;letter-spacing:-.7px;margin:0;outline:none}#chladni-plant-builder .shareClose{width:36px;height:36px;flex:none;border:1px solid #ddd;border-radius:50%;background:#fff;color:#1A1B1C;font:24px/1 Arial;cursor:pointer}#chladni-plant-builder .shareIntro{font-size:15px;line-height:1.6;margin:13px 0 18px;color:#555}#chladni-plant-builder .shareChoices{display:flex;flex-wrap:wrap;gap:10px}#chladni-plant-builder .qSharePanel .shareChoices .btn{margin:0;min-height:46px;text-decoration:none;display:inline-flex;align-items:center;justify-content:center;text-align:center}#chladni-plant-builder .shareLinkDetails{margin-top:19px;border-top:1px solid #e6e6e6;padding-top:15px}#chladni-plant-builder .shareLinkDetails summary{font-size:14px;color:#555;cursor:pointer}#chladni-plant-builder .qSharePanel textarea{min-height:80px;font-size:13px;line-height:1.5;word-break:break-all}#chladni-plant-builder .sharePrivacy{font-size:12px;line-height:1.5;color:#777;margin:16px 0 0}#chladni-plant-builder .qSharePanel .actionStatus{font-size:14px;line-height:1.5;color:#333;margin-top:15px}#chladni-plant-builder .qSharePanel .shareMore{font:600 13px/1.5 Arial,sans-serif;border:0;background:none;color:#555;padding:0;text-decoration:underline;text-underline-offset:3px;cursor:pointer;margin-top:17px}#chladni-plant-builder .qSharePanel :is(a,button):focus-visible{outline:2px solid #E2550A;outline-offset:3px}@media(max-width:600px){#chladni-plant-builder .qSharePanel{padding:20px 16px}#chladni-plant-builder .shareChoices{display:grid;grid-template-columns:1fr 1fr}#chladni-plant-builder .qSharePanel .shareChoices .btn{font-size:13px;padding:12px 8px;width:100%}#chladni-plant-builder .shareChoices .shareDownload{grid-column:1/-1}}';\n document.head.appendChild(style);\n}\nfunction showResultsSharePanel(){\n if(typeof teamReviewConfirmationState!=='undefined' \u0026\u0026 teamReviewConfirmationState) returnFromTeamReviewConfirmation(false);\n var overlay=document.getElementById('quoteOverlay');\n if(assets.length \u0026\u0026 overlay \u0026\u0026 !overlay.classList.contains('on'))openQuote('quote');\n var host=document.getElementById('sharePanel'),sheet=document.getElementById('quoteSheet');\n if(!host \u0026\u0026 sheet){host=textEl('section','qSharePanel','');host.id='sharePanel';sheet.appendChild(host);}\n if(!host){setResultsActionStatus('Open your results, then choose Forward results again.');return null;}\n ensureResultsShareStyles();\n host.hidden=false;host.innerHTML='';host.setAttribute('role','region');host.setAttribute('aria-labelledby','resultsShareTitle');\n var header=textEl('div','sharePanelHeader','');\n var title=textEl('h2',null,'Forward your results');title.id='resultsShareTitle';title.tabIndex=-1;header.appendChild(title);\n var close=actionButton('\u00d7','shareClose',function(){host.hidden=true;var trigger=sheet.querySelector('[data-forward-results]')||sheet.querySelector('#quoteTitle');if(trigger){if(trigger.tagName!=='BUTTON')trigger.tabIndex=-1;trigger.focus({preventScroll:true});}});close.setAttribute('aria-label','Close sharing options');header.appendChild(close);host.appendChild(header);\n host.appendChild(textEl('p','shareIntro','Copy the complete results link into an email or message, or open an email draft.'));\n var link=publicResultsLink(),status=textEl('p','actionStatus',''),input=null,details=null;\n status.id='shareActionStatus';status.setAttribute('role','status');status.setAttribute('aria-live','polite');\n var choices=textEl('div','shareChoices','');\n if(link){\n details=textEl('details','shareLinkDetails','');details.appendChild(textEl('summary',null,'View the complete results link'));\n input=document.createElement('textarea');input.readOnly=true;input.value=link;input.rows=3;input.setAttribute('aria-label','Complete results link');details.appendChild(input);\n choices.appendChild(actionButton('Copy results link','btn',function(){\n function manual(){details.open=true;input.focus();input.select();input.setSelectionRange(0,input.value.length);var copied=false;try{copied=!!document.execCommand('copy');}catch(e){}status.textContent=copied?'Results link copied. Paste it into your email or message.':'Link selected. Copy it and paste it into your email or message.';}\n if(navigator.clipboard \u0026\u0026 typeof navigator.clipboard.writeText==='function'){\n try{navigator.clipboard.writeText(link).then(function(){status.textContent='Results link copied. Paste it into your email or message.';},manual);}catch(e){manual();}\n }else manual();\n }));\n }\n var subject='Chladni monitoring results \u2014 '+ensureQuoteNumber();\n var body='Hi,\\n\\nPlease review our Chladni monitoring configuration'+(plant.company?' for '+plant.company:'')+'.\\n\\n';\n body+=link?'Open the complete results:\\n'+link+'\\n\\n':'I will attach the downloaded results file to this email.\\n\\n';\n body+='Thanks';\n var email=document.createElement('a');email.className='btn ghost';email.textContent='Open email draft';email.href='mailto:?subject='+encodeURIComponent(subject)+'\u0026body='+encodeURIComponent(body);\n email.addEventListener('click',function(){status.textContent='If an email draft opens, choose the recipients and send it. If no email app opens, use Copy results link and paste it into your email. Nothing has been sent automatically.';});choices.appendChild(email);\n var download=actionButton('Download to attach','btn ghost shareDownload',function(){var ok=downloadQuotePage();status.textContent=ok?'Download started. Attach the results file in your email or message.':'The download could not start. Please try again.';});choices.appendChild(download);host.appendChild(choices);\n if(details)host.appendChild(details);\n else host.appendChild(textEl('p','hint','A public results link is unavailable in this preview. Download the results file and attach it to your message.'));\n host.appendChild(status);\n if(typeof navigator.share==='function'){\n var more=actionButton('More sharing options','shareMore',function(){openNativeResultsShare({host:host,status:status,button:more,link:link});});host.appendChild(more);\n }\n host.appendChild(textEl('p','sharePrivacy','Shared results include your plant and contact details, equipment configuration and estimated pricing.'));\n var top=sheet.querySelector('.qTop');if(top)top.insertAdjacentElement('afterend',host);else sheet.prepend(host);\n // The panel is visible before calling any browser or email application.\n if(overlay){var target=host.getBoundingClientRect().top-overlay.getBoundingClientRect().top+overlay.scrollTop-18;overlay.scrollTop=Math.max(0,target);}\n title.focus({preventScroll:true});\n return {host:host,link:link,input:input,status:status};\n}\nfunction copyShareLink(){return forwardResults();}\nfunction forwardResults(){return showResultsSharePanel();}\nfunction openNativeResultsShare(panel){\n var status=panel.status,button=panel.button;\n button.disabled=true;status.textContent='Opening your device\u2019s sharing options\u2026 You can also copy the link or download the file above.';\n var timer=setTimeout(function(){if(panel.host.isConnected\u0026\u0026!panel.host.hidden){button.disabled=false;status.textContent='If no sharing window appeared, use Copy results link or Open email draft above.';}},6000);\n function finish(message){clearTimeout(timer);if(panel.host.isConnected\u0026\u0026!panel.host.hidden){button.disabled=false;status.textContent=message;}}\n try{\n var shareData={title:'Chladni monitoring results \u2014 '+ensureQuoteNumber(),text:'Complete monitoring configuration and estimate.'};\n if(panel.link)shareData.url=panel.link;\n else{\n var file=new File([portableQuoteHTML()],resultsFileName(),{type:'text/html'});\n if(!navigator.canShare || !navigator.canShare({files:[file]})){finish('Your browser cannot share this file directly. Download it and attach it instead.');return;}\n shareData.files=[file];\n }\n Promise.resolve(navigator.share(shareData)).then(function(){finish('Results handed to your sharing app. Complete any remaining steps there.');},function(error){finish(error\u0026\u0026error.name==='AbortError'?'Sharing cancelled. You can still use the options above.':'Your browser could not open sharing. Copy the link or download the file instead.');});\n }catch(error){finish('Your browser could not open sharing. Copy the link or download the file instead.');}\n}\n\n(function(){var style=document.createElement(\"style\");style.textContent=\"#chladni-plant-builder .quoteActionsRow{display:flex;flex-direction:column;gap:10px}#chladni-plant-builder .quoteActionsRow .btn{width:100%;min-height:48px}#chladni-plant-builder .qActionNote{font-size:14px;line-height:1.5;margin:0 0 4px;color:#555}#chladni-plant-builder .actionStatus{font-size:14px;line-height:1.5;overflow-wrap:anywhere}#chladni-plant-builder .actionStatus:empty{display:none}#chladni-plant-builder .qSharePanel textarea{max-width:100%;overflow-wrap:anywhere}#chladni-plant-builder .qSummary{min-width:0}@media(max-width:700px){#chladni-plant-builder .qMeta{font-size:11px;overflow-wrap:anywhere}#chladni-plant-builder .qTitle{font-size:34px}#chladni-plant-builder .qAssetTop{flex-wrap:wrap}}\";document.head.appendChild(style);})();\nfunction recordTeamReviewConversion(reference){\n var key=\"chladni_review_conversion_\"+reference;\n try { if(sessionStorage.getItem(key)) return; sessionStorage.setItem(key,\"1\"); } catch(e) {}\n var sentGA=false,sentMeta=false;\n try {sentGA=localStorage.getItem(\"chladni_ga_lead\")===\"1\";sentMeta=localStorage.getItem(\"chladni_fb_lead\")===\"1\";}catch(e){}\n if(!sentGA){track(\"generate_lead\",{lead_source:\"plant_builder\",industry:plant.industry||\"\",country:plant.country||\"\"});try{localStorage.setItem(\"chladni_ga_lead\",\"1\");}catch(e){}}\n if(!sentMeta \u0026\u0026 typeof window.fbq===\"function\"){\n window.fbq(\"trackSingle\",\"1844610536954936\",\"Lead\",{content_name:\"Plant configuration review\",lead_source:\"plant_builder\"},{eventID:\"plant-review-\"+reference});\n try{localStorage.setItem(\"chladni_fb_lead\",\"1\");}catch(e){}\n }\n}\n/* Call only after the existing submission handler confirms receipt. This helper has no\n transport, tracking or submission side effects. Results are kept intact for export. */\nvar teamReviewConfirmationState = null;\nfunction injectTeamReviewConfirmationStyles(){\n if (document.getElementById(\"chladni-review-confirmation-css\")) return;\n var style=document.createElement(\"style\"); style.id=\"chladni-review-confirmation-css\";\n style.textContent=`\n#chladni-plant-builder .quoteSheet.reviewConfirmationMode{width:100%;max-width:980px;box-sizing:border-box;padding:38px 44px;background:#fff;color:#1A1B1C;}\n#chladni-plant-builder .reviewConfirmation,.reviewConfirmation *{box-sizing:border-box;}\n#chladni-plant-builder .reviewConfirmation{font-family:Arial,Helvetica,sans-serif;}\n#chladni-plant-builder .reviewConfirmationTop{display:flex;align-items:center;justify-content:space-between;gap:20px;padding-bottom:30px;border-bottom:1px solid #e3e3e3;}\n#chladni-plant-builder .reviewConfirmationLogo{display:block;width:176px;height:22px;object-fit:contain;}\n#chladni-plant-builder .reviewConfirmationClose{border:1px solid #dedede;border-radius:50%;width:38px;height:38px;display:flex;align-items:center;justify-content:center;background:#fff;color:#1A1B1C;font:26px/1 Arial,sans-serif;cursor:pointer;}\n#chladni-plant-builder .reviewConfirmationClose:hover{background:#f5f5f5;}\n#chladni-plant-builder .reviewConfirmationIntro{padding:40px 0 30px;max-width:730px;}\n#chladni-plant-builder .reviewConfirmationMark{display:flex;align-items:center;justify-content:center;width:56px;height:56px;border-radius:50%;background:#1A1B1C;color:#fff;margin-bottom:26px;}\n#chladni-plant-builder .reviewConfirmationMark svg{width:28px;height:28px;}\n#chladni-plant-builder .reviewConfirmationEyebrow{font-size:12px;font-weight:700;letter-spacing:1.7px;color:#666;margin:0 0 13px;text-transform:uppercase;}\n#chladni-plant-builder .reviewConfirmationTitle{font-size:clamp(34px,4.6vw,54px);line-height:1.08;letter-spacing:-1.8px;font-weight:700;color:#1A1B1C;text-transform:none;margin:0 0 22px;outline:none;}\n#chladni-plant-builder .reviewConfirmationTitle span{color:#E2550A;}\n#chladni-plant-builder .reviewConfirmationLead{font-size:18px;line-height:1.6;color:#494949;margin:0;max-width:650px;}\n#chladni-plant-builder .reviewConfirmationContact{font-size:15px;line-height:1.6;color:#555;margin-top:14px;overflow-wrap:anywhere;}\n#chladni-plant-builder .reviewConfirmationContact strong{color:#1A1B1C;font-weight:700;}\n#chladni-plant-builder .reviewConfirmationSteps{display:grid;grid-template-columns:1fr 1fr;gap:28px;border-top:1px solid #e3e3e3;border-bottom:1px solid #e3e3e3;padding:25px 0;margin:0 0 28px;}\n#chladni-plant-builder .reviewConfirmationStep{display:flex;align-items:flex-start;gap:12px;}\n#chladni-plant-builder .reviewConfirmationStepNumber{font-size:12px;font-weight:700;letter-spacing:1px;color:#E2550A;line-height:1.7;min-width:23px;}\n#chladni-plant-builder .reviewConfirmationStep h2{font-size:16px;line-height:1.4;font-weight:700;margin:0 0 6px;color:#1A1B1C;text-transform:none;letter-spacing:0;}\n#chladni-plant-builder .reviewConfirmationStep p{font-size:14px;line-height:1.6;color:#626262;margin:0;}\n#chladni-plant-builder .reviewConfirmationActions{display:flex;gap:10px;flex-wrap:wrap;align-items:stretch;margin:0;}\n#chladni-plant-builder .reviewConfirmationAction{display:inline-flex;align-items:center;justify-content:center;gap:9px;border:1px solid #1A1B1C;border-radius:6px;padding:14px 18px;min-height:50px;background:#fff;color:#1A1B1C;font:700 14px/1.3 Arial,Helvetica,sans-serif;cursor:pointer;text-decoration:none;text-transform:none;letter-spacing:0;}\n#chladni-plant-builder .reviewConfirmationAction.primary{background:#1A1B1C;color:#fff;}\n#chladni-plant-builder .reviewConfirmationAction:hover{background:#f5f5f5;color:#1A1B1C;}\n#chladni-plant-builder .reviewConfirmationAction.primary:hover{background:#303030;color:#fff;}\n#chladni-plant-builder .reviewConfirmationAction svg{width:18px;height:18px;flex:none;}\n#chladni-plant-builder .reviewConfirmationBack{display:inline-flex;align-items:center;gap:8px;background:none;border:none;padding:18px 0 0;color:#555;font:700 14px/1.4 Arial,Helvetica,sans-serif;cursor:pointer;text-decoration:underline;text-underline-offset:3px;}\n#chladni-plant-builder .reviewConfirmationReference{font-size:12px;color:#747474;margin-top:28px;line-height:1.5;}\n#chladni-plant-builder .reviewConfirmationNotice{padding:13px 15px;margin-top:18px;border-left:3px solid #E2550A;background:#fafafa;font-size:14px;line-height:1.6;color:#444;}\n#chladni-plant-builder .reviewConfirmation :is(button,a):focus-visible{outline:3px solid #E2550A;outline-offset:4px;}\n@media(max-width:600px){\n #chladni-plant-builder .quoteSheet.reviewConfirmationMode{padding:25px 22px;}\n #chladni-plant-builder .reviewConfirmationTop{padding-bottom:22px;}\n #chladni-plant-builder .reviewConfirmationLogo{width:152px;height:19px;}\n #chladni-plant-builder .reviewConfirmationIntro{padding:28px 0 24px;}\n #chladni-plant-builder .reviewConfirmationMark{width:48px;height:48px;margin-bottom:22px;}\n #chladni-plant-builder .reviewConfirmationTitle{font-size:36px;letter-spacing:-1.2px;}\n #chladni-plant-builder .reviewConfirmationLead{font-size:16px;}\n #chladni-plant-builder .reviewConfirmationSteps{grid-template-columns:1fr;gap:20px;padding:22px 0;}\n #chladni-plant-builder .reviewConfirmationActions{display:grid;grid-template-columns:1fr 1fr;}\n #chladni-plant-builder .reviewConfirmationAction.primary{grid-column:1/-1;}\n #chladni-plant-builder .reviewConfirmationAction{font-size:13px;padding:13px 10px;}\n}\n@media(prefers-reduced-motion:reduce){#chladni-plant-builder .reviewConfirmation *{scroll-behavior:auto;}}\n@media print{#chladni-plant-builder .reviewConfirmationActions,#chladni-plant-builder .reviewConfirmationBack,#chladni-plant-builder .reviewConfirmationClose{display:none;}}\n`;\n document.head.appendChild(style);\n}\nfunction reviewConfirmationElement(tag,cls,text){\n var node=document.createElement(tag); if(cls) node.className=cls; if(text!=null) node.textContent=String(text); return node;\n}\nfunction reviewConfirmationIcon(type){\n var ns=\"http://www.w3.org/2000/svg\", svg=document.createElementNS(ns,\"svg\");\n svg.setAttribute(\"viewBox\",\"0 0 24 24\");svg.setAttribute(\"fill\",\"none\");svg.setAttribute(\"stroke\",\"currentColor\");svg.setAttribute(\"stroke-width\",\"1.8\");svg.setAttribute(\"stroke-linecap\",\"round\");svg.setAttribute(\"stroke-linejoin\",\"round\");svg.setAttribute(\"aria-hidden\",\"true\");\n var paths={check:\"M5 12l4 4L19 6\",download:\"M12 3v12m-5-5 5 5 5-5M5 16v5h14v-5\",forward:\"M14 5l7 7-7 7M21 12H8a5 5 0 0 0-5 5\",phone:\"M7 3H4a1 1 0 0 0-1 1c0 9.4 7.6 17 17 17a1 1 0 0 0 1-1v-3l-5-2-2 2a15 15 0 0 1-7-7l2-2-2-5Z\"};\n var path=document.createElementNS(ns,\"path\");path.setAttribute(\"d\",paths[type]||paths.check);svg.appendChild(path);return svg;\n}\nfunction returnFromTeamReviewConfirmation(focus){\n var state=teamReviewConfirmationState; if(!state) return;\n var sheet=document.getElementById(\"quoteSheet\"); if(!sheet) return;\n sheet.innerHTML=\"\"; sheet.classList.remove(\"reviewConfirmationMode\"); sheet.appendChild(state.results);\n teamReviewConfirmationState=null;\n var overlay=document.getElementById(\"quoteOverlay\");if(overlay) overlay.scrollTop=0;\n if(focus!==false){var title=sheet.querySelector(\"#quoteTitle,h1\");if(title){title.setAttribute(\"tabindex\",\"-1\");title.focus({preventScroll:true});}}\n}\nfunction renderTeamReviewConfirmation(receipt){\n receipt=receipt||{};\n injectTeamReviewConfirmationStyles();\n var sheet=document.getElementById(\"quoteSheet\"); if(!sheet) return false;\n if(teamReviewConfirmationState) returnFromTeamReviewConfirmation(false);\n var results=document.createDocumentFragment();while(sheet.firstChild) results.appendChild(sheet.firstChild);\n var state={results:results,receipt:receipt};teamReviewConfirmationState=state;\n sheet.classList.add(\"reviewConfirmationMode\");\n var page=reviewConfirmationElement(\"section\",\"reviewConfirmation\");page.setAttribute(\"aria-labelledby\",\"reviewConfirmationTitle\");\n var top=reviewConfirmationElement(\"div\",\"reviewConfirmationTop\");\n var logo=reviewConfirmationElement(\"img\",\"reviewConfirmationLogo\");logo.alt=\"Chladni\";logo.width=176;logo.height=22;\n if(typeof RESULTS_WORDMARK_DATA_URI===\"string\") logo.src=RESULTS_WORDMARK_DATA_URI;\n else if(typeof BRAND_LOGO_URL===\"string\") logo.src=BRAND_LOGO_URL;\n top.appendChild(logo);\n var close=reviewConfirmationElement(\"button\",\"reviewConfirmationClose\",\"\u00d7\");close.type=\"button\";close.setAttribute(\"aria-label\",\"Close confirmation\");\n close.addEventListener(\"click\",function(){returnFromTeamReviewConfirmation(false);if(typeof closeQuote===\"function\") closeQuote();});top.appendChild(close);page.appendChild(top);\n var intro=reviewConfirmationElement(\"div\",\"reviewConfirmationIntro\");\n var mark=reviewConfirmationElement(\"div\",\"reviewConfirmationMark\");mark.appendChild(reviewConfirmationIcon(\"check\"));intro.appendChild(mark);\n var eyebrow=reviewConfirmationElement(\"p\",\"reviewConfirmationEyebrow\",\"Submitted to Chladni\");eyebrow.setAttribute(\"role\",\"status\");intro.appendChild(eyebrow);\n var heading=reviewConfirmationElement(\"h1\",\"reviewConfirmationTitle\",\"Your plan is with our \");heading.id=\"reviewConfirmationTitle\";heading.tabIndex=-1;heading.appendChild(reviewConfirmationElement(\"span\",null,\"team.\"));intro.appendChild(heading);\n intro.appendChild(reviewConfirmationElement(\"p\",\"reviewConfirmationLead\",\"Thank you. We\u2019ll review your equipment and monitoring plan, then reach out to discuss the details with you.\"));\n var email=receipt.email||receipt.contactEmail||(typeof plant!==\"undefined\"\u0026\u0026plant.contactEmail)||\"\";\n if(email){var contact=reviewConfirmationElement(\"p\",\"reviewConfirmationContact\",\"We\u2019ll contact you at \");contact.appendChild(reviewConfirmationElement(\"strong\",null,email));contact.appendChild(document.createTextNode(\".\"));intro.appendChild(contact);}\n if(receipt.hasNewChanges) intro.appendChild(reviewConfirmationElement(\"p\",\"reviewConfirmationNotice\",\"The submitted version is with our team. Changes you made while it was sending have not been submitted; return to your results to send the latest version.\"));\n page.appendChild(intro);\n var steps=reviewConfirmationElement(\"div\",\"reviewConfirmationSteps\");\n [[\"01\",\"We check the configuration\",\"Our team reviews the equipment, sensor coverage and estimated pricing.\"],[\"02\",\"We discuss the next step\",\"We\u2019ll get in touch to confirm the details and answer your questions.\"]].forEach(function(step){var row=reviewConfirmationElement(\"div\",\"reviewConfirmationStep\");row.appendChild(reviewConfirmationElement(\"span\",\"reviewConfirmationStepNumber\",step[0]));var copy=reviewConfirmationElement(\"div\");copy.appendChild(reviewConfirmationElement(\"h2\",null,step[1]));copy.appendChild(reviewConfirmationElement(\"p\",null,step[2]));row.appendChild(copy);steps.appendChild(row);});page.appendChild(steps);\n var actions=reviewConfirmationElement(\"div\",\"reviewConfirmationActions\");\n var call=reviewConfirmationElement(\"a\",\"reviewConfirmationAction primary\");call.href=\"tel:+16402213449\";call.appendChild(reviewConfirmationIcon(\"phone\"));call.appendChild(document.createTextNode(\"Call us\"));call.setAttribute(\"aria-label\",\"Call Chladni at +1 640 221 3449\");actions.appendChild(call);\n var download=reviewConfirmationElement(\"button\",\"reviewConfirmationAction\");download.type=\"button\";download.appendChild(reviewConfirmationIcon(\"download\"));download.appendChild(document.createTextNode(\"Download results\"));\n download.addEventListener(\"click\",function(){\n returnFromTeamReviewConfirmation(false);\n var ok=typeof downloadQuotePage===\"function\"?downloadQuotePage():false;\n renderTeamReviewConfirmation(receipt);\n var message=document.getElementById(\"reviewConfirmationActionStatus\");if(message) message.textContent=ok?\"Download started. Your results file opens in any browser.\":\"The download could not start. Return to your results and try again.\";\n var focus=document.querySelector(\".reviewConfirmationActions button\");if(focus) focus.focus({preventScroll:true});\n });actions.appendChild(download);\n var forward=reviewConfirmationElement(\"button\",\"reviewConfirmationAction\");forward.type=\"button\";forward.appendChild(reviewConfirmationIcon(\"forward\"));forward.appendChild(document.createTextNode(\"Forward results\"));\n forward.addEventListener(\"click\",function(){returnFromTeamReviewConfirmation(false);if(typeof forwardResults===\"function\")forwardResults();});actions.appendChild(forward);page.appendChild(actions);\n var status=reviewConfirmationElement(\"p\",\"reviewConfirmationContact\",\"\");status.id=\"reviewConfirmationActionStatus\";status.setAttribute(\"role\",\"status\");status.setAttribute(\"aria-live\",\"polite\");page.appendChild(status);\n var back=reviewConfirmationElement(\"button\",\"reviewConfirmationBack\",\"\u2190 Back to results\");back.type=\"button\";back.addEventListener(\"click\",function(){returnFromTeamReviewConfirmation(true);});page.appendChild(back);\n var reference=receipt.quoteId||receipt.reference||(typeof quoteNumber!==\"undefined\"\u0026\u0026quoteNumber)||\"\";\n if(reference) page.appendChild(reviewConfirmationElement(\"p\",\"reviewConfirmationReference\",\"Configuration reference: \"+reference));\n sheet.appendChild(page);\n var overlay=document.getElementById(\"quoteOverlay\");if(overlay) overlay.scrollTop=0;\n heading.focus({preventScroll:true});\n return true;\n}\n\nfunction requestTeamReviewUI(){\n var quoteId=ensureQuoteNumber(), email=String(plant.contactEmail||\"\").trim(), fingerprint=teamReviewFingerprint();\n return submitTeamReview().then(function(ok){\n var overlay=document.getElementById(\"quoteOverlay\");\n if(ok \u0026\u0026 overlay \u0026\u0026 overlay.classList.contains(\"on\")){\n renderTeamReviewConfirmation({quoteId:quoteId,email:email,hasNewChanges:teamReviewFingerprint()!==fingerprint});\n }\n return ok;\n });\n}\n\n\n/* Initial pricing inquiry, sent only by the visitor's deliberate Continue action.\n renderInquiryNotificationStatus() renders feedback but never sends a request.\n The existing Webflow form owns delivery and notifications; acknowledge only its\n matching success response, never a click, a render, or a legacy leadSentAt flag. */\nvar INQUIRY_NOTIFICATION_TIMEOUT_MS = 30000;\nvar INQUIRY_NOTIFICATION_SUCCESS = \"Your contact details have been received by Chladni.\";\nvar inquiryNotificationBusy = false;\nvar inquiryNotificationPending = null;\nvar inquiryNotificationQueued = null;\nvar inquiryNotificationLastAttempt = null;\nvar inquiryNotificationFeedback = null;\n\nfunction inquiryNotificationFingerprint(){\n var identity = [\"company\",\"name\",\"industry\",\"country\",\"state\",\"contactName\",\"contactEmail\"].map(function(key){ return String(plant[key] || \"\").trim(); });\n var text = JSON.stringify(identity), first = 0x811c9dc5, second = 0x9e3779b9;\n for (var i=0; i\u003ctext.length; i++){\n var code = text.charCodeAt(i);\n first = Math.imul(first ^ (code \u0026 255),0x01000193) \u003e\u003e\u003e 0;\n first = Math.imul(first ^ (code \u003e\u003e\u003e 8),0x01000193) \u003e\u003e\u003e 0;\n second = Math.imul(second ^ (code \u003e\u003e\u003e 8),0x01000193) \u003e\u003e\u003e 0;\n second = Math.imul(second ^ (code \u0026 255),0x01000193) \u003e\u003e\u003e 0;\n }\n function hex(n){ return (\"00000000\"+n.toString(16)).slice(-8); }\n return \"inquiry-v1:\"+hex(first)+hex(second)+\":\"+text.length;\n}\nfunction inquiryNotificationConfirmed(fingerprint){\n var receipt = plant.inquiryReceipt;\n return !!(receipt \u0026\u0026 receipt.fingerprint === fingerprint \u0026\u0026 receipt.acceptedAt \u0026\u0026 !isNaN(Date.parse(receipt.acceptedAt)));\n}\nfunction inquiryNotificationStatus(){\n var fingerprint = inquiryNotificationFingerprint();\n if (inquiryNotificationConfirmed(fingerprint)) return {message:INQUIRY_NOTIFICATION_SUCCESS,kind:\"success\"};\n if (inquiryNotificationFeedback \u0026\u0026 (!inquiryNotificationFeedback.fingerprint || inquiryNotificationFeedback.fingerprint === fingerprint)) return inquiryNotificationFeedback;\n return {message:\"\",kind:\"status\"};\n}\nfunction inquiryStatusText(){ return inquiryNotificationStatus().message; }\nfunction renderInquiryNotificationStatus(){\n var state = inquiryNotificationStatus();\n var node = document.getElementById(\"inquiryStatus\");\n if (!node \u0026\u0026 state.message){\n var host = document.getElementById(\"tree\") || document.getElementById(\"plantCard\");\n if (!host) return null;\n node = document.createElement(\"div\"); node.id = \"inquiryStatus\";\n var anchor = host.querySelector \u0026\u0026 host.querySelector(\".treePlantInfo\");\n if (anchor \u0026\u0026 anchor.parentNode \u0026\u0026 typeof anchor.insertAdjacentElement === \"function\") anchor.insertAdjacentElement(\"afterend\",node);\n else host.appendChild(node);\n }\n if (!node) return null;\n node.textContent = \"\";\n node.hidden = !state.message;\n node.setAttribute(\"role\",state.kind === \"error\" ? \"alert\" : \"status\");\n node.setAttribute(\"aria-live\",\"polite\");\n node.style.fontSize = \"13px\"; node.style.lineHeight = \"1.5\";\n node.style.margin = \"12px 0\"; node.style.color = state.kind === \"error\" ? \"#b02020\" : \"#5e5e5e\";\n var message = document.createElement(\"div\"); message.textContent = state.message; node.appendChild(message);\n if (state.kind === \"error\" \u0026\u0026 !inquiryNotificationBusy){\n var retry = document.createElement(\"button\"); retry.id = \"inquiryRetryBtn\"; retry.type = \"button\";\n retry.textContent = \"Retry sending contact details\";\n retry.style.cssText = \"margin-top:6px;padding:0;border:0;background:none;color:inherit;text-decoration:underline;font:inherit;cursor:pointer\";\n retry.addEventListener(\"click\",retryPlantInquiry); node.appendChild(retry);\n }\n return node;\n}\nfunction inquiryNotificationShow(message,kind,fingerprint){\n inquiryNotificationFeedback = {message:message,kind:kind || \"status\",fingerprint:fingerprint || \"\"};\n renderInquiryNotificationStatus();\n}\nfunction inquiryNotificationRequestData(raw){\n if (!raw) return {};\n if (typeof raw === \"object\") return raw;\n if (typeof raw !== \"string\") return {};\n if (/^\\s*\\{/.test(raw)){ try { return JSON.parse(raw); } catch(e){ return {}; } }\n var result = {};\n try {\n raw.split(\"\u0026\").forEach(function(pair){\n var split = pair.indexOf(\"=\"), key = split \u003c 0 ? pair : pair.slice(0,split), value = split \u003c 0 ? \"\" : pair.slice(split+1);\n result[decodeURIComponent(key.replace(/\\+/g,\" \"))] = decodeURIComponent(value.replace(/\\+/g,\" \"));\n });\n } catch(e){ return {}; }\n return result;\n}\nfunction inquiryNotificationRequestField(data,name){\n if (data.fields \u0026\u0026 typeof data.fields === \"object\" \u0026\u0026 data.fields[name] != null) return String(data.fields[name]);\n if (data[\"fields[\"+name+\"]\"] != null) return String(data[\"fields[\"+name+\"]\"]);\n if (typeof data.get === \"function\"){\n var value = data.get(\"fields[\"+name+\"]\"); if (value != null) return String(value);\n }\n return \"\";\n}\nfunction inquiryNotificationRequestMatches(settings,expected){\n if (!settings || !/^(?:https?:\\/\\/[^/?#]+)?\\/api\\/v1\\/form\\//.test(String(settings.url || \"\"))) return false;\n var data = inquiryNotificationRequestData(settings.data), name = typeof data.get === \"function\" ? data.get(\"name\") : data.name;\n return name === \"Plant Builder Lead\" \u0026\u0026\n [\"stage\",\"contact_name\",\"contact_email\",\"company\",\"plant_name\",\"quote_id\",\"config_link\"].every(function(field){ return inquiryNotificationRequestField(data,field) === String(expected[field]); });\n}\nfunction inquiryNotificationResponseError(value){\n if (Array.isArray(value)) return value.length \u003e 0;\n if (value \u0026\u0026 typeof value === \"object\") return Object.keys(value).length \u003e 0;\n return !!value;\n}\nfunction retryPlantInquiry(){ return notifyPlantInquiry({retry:true}); }\nfunction notifyPlantInquiry(options){\n options = options || {};\n if (inquiryNotificationBusy) return inquiryNotificationPending || Promise.resolve(false);\n var fingerprint = inquiryNotificationFingerprint();\n function failed(message){\n inquiryNotificationLastAttempt = {fingerprint:fingerprint,ok:false};\n inquiryNotificationShow(message,\"error\",fingerprint);\n return Promise.resolve(false);\n }\n if (!leadCaptured() || !String(plant.company || \"\").trim()) return failed(\"Add your name, company and a valid email before sending your contact details.\");\n if (inquiryNotificationConfirmed(fingerprint)){\n inquiryNotificationShow(INQUIRY_NOTIFICATION_SUCCESS,\"success\",fingerprint);\n return Promise.resolve(true);\n }\n /* Re-rendering or re-entering the builder never creates an automatic retry. */\n if (!options.retry \u0026\u0026 inquiryNotificationLastAttempt \u0026\u0026 inquiryNotificationLastAttempt.fingerprint === fingerprint){\n renderInquiryNotificationStatus();\n return Promise.resolve(inquiryNotificationLastAttempt.ok);\n }\n /* The full-review action and inquiry share one native Webflow form. */\n if (typeof teamReviewBusy !== \"undefined\" \u0026\u0026 teamReviewBusy){\n if (inquiryNotificationQueued) return inquiryNotificationQueued;\n if (typeof teamReviewPending === \"undefined\" || !teamReviewPending || typeof teamReviewPending.then !== \"function\") return failed(\"Your review is still being sent. Retry sending your contact details when it finishes.\");\n inquiryNotificationShow(\"Waiting to send your contact details\u2026\",\"status\",fingerprint);\n inquiryNotificationQueued = Promise.resolve(teamReviewPending).catch(function(){ return false; }).then(function(){\n inquiryNotificationQueued = null;\n return notifyPlantInquiry(options);\n });\n return inquiryNotificationQueued;\n }\n var form = document.querySelector('form[data-name=\"Plant Builder Lead\"]'), jq = window.jQuery, runtime;\n try { runtime = window.Webflow \u0026\u0026 typeof window.Webflow.require === \"function\" \u0026\u0026 window.Webflow.require(\"forms\"); } catch(e) {}\n if (!form || typeof jq !== \"function\" || !runtime) return failed(\"We could not send your contact details because the form is not ready. Refresh the page and try again, or contact Chladni directly.\");\n var eventHost = jq(document);\n if (!eventHost || typeof eventHost.on !== \"function\" || typeof eventHost.off !== \"function\") return failed(\"We could not send your contact details. Refresh the page and try again.\");\n var total = totals(), link = \"\";\n try { link = typeof shareLinkUrl === \"function\" ? shareLinkUrl() || \"\" : \"\"; } catch(e) {}\n var fields = {\n stage:\"lead\",contact_name:String(plant.contactName).trim(),contact_email:String(plant.contactEmail).trim(),\n company:String(plant.company).trim(),plant_name:plant.name || \"\",industry:plant.industry || \"\",country:plant.country || \"\",state:plant.state || \"\",\n asset_count:total.assets || 0,puck_count:total.pucks || 0,monthly_total:Math.round(total.monthly || 0),\n quote_id:typeof quoteNumber !== \"undefined\" \u0026\u0026 quoteNumber ? quoteNumber : \"\",config_link:link,page_url:location.href.split(\"#\")[0]\n };\n if (Object.keys(fields).some(function(name){ return !form.querySelector('[name=\"'+name+'\"]'); })) return failed(\"We could not send your contact details because a required form field is missing. Contact Chladni directly or retry later.\");\n Object.keys(fields).forEach(function(name){ setField(form,name,fields[name]); });\n inquiryNotificationLastAttempt = {fingerprint:fingerprint,ok:false};\n inquiryNotificationBusy = true;\n inquiryNotificationShow(\"Sending your contact details to Chladni\u2026\",\"status\",fingerprint);\n var pending = new Promise(function(resolve){\n var settled = false, timeout;\n function finish(ok,message){\n if (settled) return;\n settled = true; clearTimeout(timeout); eventHost.off(\".plantInquiry\");\n inquiryNotificationLastAttempt = {fingerprint:fingerprint,ok:ok};\n if (ok){\n plant.inquiryReceipt = {fingerprint:fingerprint,acceptedAt:new Date().toISOString()};\n try { persistLocal(); } catch(e) {}\n try { track(\"plant_builder_inquiry_received\",{has_email:true}); } catch(e) {}\n }\n inquiryNotificationShow(message,ok ? \"success\" : \"error\",fingerprint);\n resolve(ok);\n }\n function rejected(){ finish(false,\"We could not confirm that your contact details were received. Retry sending them; your configuration is still saved here.\"); }\n eventHost.on(\"ajaxSuccess.plantInquiry\",function(event,xhr,settings){\n if (!inquiryNotificationRequestMatches(settings,fields)) return;\n var response = xhr \u0026\u0026 xhr.responseJSON;\n if (response \u0026\u0026 response.code === 200 \u0026\u0026 !inquiryNotificationResponseError(response.error) \u0026\u0026 !inquiryNotificationResponseError(response.errors) \u0026\u0026 response.success !== false) finish(true,INQUIRY_NOTIFICATION_SUCCESS);\n else rejected();\n });\n eventHost.on(\"ajaxError.plantInquiry\",function(event,xhr,settings){ if (inquiryNotificationRequestMatches(settings,fields)) rejected(); });\n timeout = setTimeout(function(){ finish(false,\"Sending your contact details is taking longer than expected. We cannot confirm delivery. Use Retry to try again.\"); },INQUIRY_NOTIFICATION_TIMEOUT_MS);\n try {\n if (typeof form.requestSubmit === \"function\") form.requestSubmit();\n else {\n var button = form.querySelector('[type=\"submit\"]');\n if (!button || typeof button.click !== \"function\") throw new Error(\"Missing native form submit control\");\n button.click();\n }\n } catch(e){ rejected(); }\n });\n var operation = pending.catch(function(){\n eventHost.off(\".plantInquiry\");\n inquiryNotificationShow(\"We could not send your contact details. Please retry.\",\"error\",fingerprint);\n return false;\n }).finally(function(){\n if (inquiryNotificationPending === operation){\n inquiryNotificationBusy = false; inquiryNotificationPending = null;\n renderInquiryNotificationStatus();\n }\n });\n inquiryNotificationPending = operation;\n return operation;\n}\n\nboot();\n";document.head.appendChild(s);})();