{"id":25002,"date":"2026-10-28T03:43:43","date_gmt":"2026-10-28T03:43:43","guid":{"rendered":"https:\/\/www.mechstream.com\/?p=25002"},"modified":"2026-10-28T03:43:45","modified_gmt":"2026-10-28T03:43:45","slug":"material-selection-comparison-scoring-calculator","status":"publish","type":"post","link":"https:\/\/www.mechstream.com\/pt\/material-selection-comparison-scoring-calculator\/","title":{"rendered":"Material Selection Comparison &amp; Scoring Calculator"},"content":{"rendered":"\n<p class=\"wp-block-paragraph\">The <strong>Material Selection Comparison &amp; Scoring Calculator<\/strong> is a powerful online tool designed to help engineers, manufacturers, and material specialists compare and evaluate engineering materials based on their properties and performance. By analyzing strength, density, corrosion resistance, thermal properties, manufacturing compatibility, and cost, users can assign custom weights, calculate material scores, and identify suitable options. This free calculator provides material rankings, weighted scoring, and visual comparisons to support informed material selection. It is ideal for metal fabrication, mechanical engineering, product design, and industrial manufacturing applications.<\/p>\n\n\n\n<iframe class=\"mscs-frame skip-lazy no-lazyload\" data-no-lazy=\"1\" data-skip-lazy=\"1\" title=\"Material Selection Comparison &amp; Scoring Calculator\" scrolling=\"no\" style=\"display:block !important;width:100% !important;max-width:1280px !important;height:2400px;border:0 !important;margin:0 auto !important;padding:0 !important;overflow:hidden;background:transparent;float:none !important\" srcdoc=\"&lt;!DOCTYPE html&gt;&#10;&lt;html lang=&quot;en&quot;&gt;&#10;&lt;head&gt;&#10;&lt;meta charset=&quot;UTF-8&quot;&gt;&#10;&lt;meta name=&quot;viewport&quot; content=&quot;width=device-width, initial-scale=1&quot;&gt;&#10;&lt;title&gt;Material Selection Comparison &amp;amp; Scoring Calculator&lt;\/title&gt;&#10;&lt;style&gt;html,body{margin:0;padding:0;background:transparent}&lt;\/style&gt;&#10;&lt;\/head&gt;&#10;&lt;body&gt;&#10;&#10;&lt;div class=&quot;mscs-root&quot; id=&quot;mscs-app&quot;&gt;&#10;&lt;style&gt;&#10;\/* ===== Scoped styles: every rule is prefixed with .mscs-root so the host page (WordPress \/ Elementor) is not affected ===== *\/&#10;.mscs-root{--mscs-blue:#1e40af;--mscs-blue-d:#1e3a8a;--mscs-blue-l:#e8eefb;--mscs-bg:#f3f5f8;--mscs-card:#ffffff;--mscs-text:#1f2937;--mscs-muted:#5b6575;--mscs-border:#dce2ea;--mscs-ok:#15803d;--mscs-ok-bg:#e7f6ec;--mscs-bad:#b91c1c;--mscs-bad-bg:#fdecec;--mscs-warn:#a15c00;--mscs-warn-bg:#fff4e0;--mscs-ne:#4b5563;--mscs-ne-bg:#eef0f3;&#10;  font-family:-apple-system,BlinkMacSystemFont,&quot;Segoe UI&quot;,Roboto,&quot;Helvetica Neue&quot;,Arial,sans-serif;font-size:15px;line-height:1.55;color:var(--mscs-text);background:var(--mscs-bg);padding:24px 16px;margin:0;text-align:left;}&#10;.mscs-root *,.mscs-root *::before,.mscs-root *::after{box-sizing:border-box;}&#10;.mscs-root .mscs-wrap{max-width:1280px;margin:0 auto;}&#10;.mscs-root h1,.mscs-root h2,.mscs-root h3,.mscs-root h4{color:#111827;font-family:inherit;line-height:1.3;margin:0 0 10px;padding:0;text-transform:none;letter-spacing:normal;}&#10;.mscs-root h1{font-size:28px;font-weight:700;}&#10;.mscs-root h2{font-size:20px;font-weight:700;}&#10;.mscs-root h3{font-size:16px;font-weight:650;}&#10;.mscs-root h4{font-size:14px;font-weight:650;}&#10;.mscs-root p{margin:0 0 10px;}&#10;.mscs-root ul,.mscs-root ol{margin:0 0 10px;padding-left:22px;}&#10;.mscs-root li{margin:2px 0;}&#10;.mscs-root a{color:var(--mscs-blue);}&#10;.mscs-root .mscs-toolbar{display:flex;flex-wrap:wrap;gap:8px;margin-top:14px;}&#10;.mscs-root .mscs-card{background:var(--mscs-card);border:1px solid var(--mscs-border);border-radius:12px;box-shadow:0 1px 3px rgba(15,23,42,.06);padding:18px;margin-bottom:18px;}&#10;.mscs-root .mscs-sec-title{display:flex;align-items:center;gap:10px;margin-bottom:12px;}&#10;.mscs-root .mscs-sec-title h2{margin:0;}&#10;.mscs-root .mscs-sec-num{flex:0 0 auto;width:30px;height:30px;border-radius:8px;background:var(--mscs-blue-l);color:var(--mscs-blue);display:flex;align-items:center;justify-content:center;}&#10;.mscs-root .mscs-sec-num svg{width:18px;height:18px;}&#10;.mscs-root .mscs-muted{color:var(--mscs-muted);font-size:13px;}&#10;.mscs-root .mscs-small{font-size:12.5px;}&#10;\/* Buttons *\/&#10;.mscs-root .mscs-btn{display:inline-flex;align-items:center;gap:6px;font:inherit;font-size:14px;font-weight:600;line-height:1.2;text-transform:none;letter-spacing:normal;padding:8px 14px;border-radius:8px;border:1px solid var(--mscs-blue);background:var(--mscs-blue);color:#fff;cursor:pointer;box-shadow:none;margin:0;min-height:0;width:auto;text-decoration:none;}&#10;.mscs-root .mscs-btn:hover{background:var(--mscs-blue-d);border-color:var(--mscs-blue-d);color:#fff;}&#10;.mscs-root .mscs-btn:focus-visible,.mscs-root input:focus-visible,.mscs-root select:focus-visible,.mscs-root summary:focus-visible,.mscs-root textarea:focus-visible{outline:2px solid #f59e0b;outline-offset:2px;}&#10;.mscs-root .mscs-btn-sec{background:#fff;color:var(--mscs-blue);}&#10;.mscs-root .mscs-btn-sec:hover{background:var(--mscs-blue-l);color:var(--mscs-blue-d);}&#10;.mscs-root .mscs-btn-danger{background:#fff;color:var(--mscs-bad);border-color:#e7b4b4;}&#10;.mscs-root .mscs-btn-danger:hover{background:var(--mscs-bad-bg);color:var(--mscs-bad);}&#10;.mscs-root .mscs-btn-sm{padding:5px 9px;font-size:12.5px;border-radius:6px;}&#10;.mscs-root .mscs-btn[disabled]{opacity:.45;cursor:not-allowed;}&#10;\/* Form controls *\/&#10;.mscs-root input[type=text],.mscs-root input[type=number],.mscs-root input[type=search],.mscs-root select,.mscs-root textarea{font:inherit;font-size:14px;color:var(--mscs-text);background:#fff;border:1px solid #c9d1dc;border-radius:7px;padding:6px 8px;margin:0;width:100%;max-width:100%;height:auto;min-height:0;box-shadow:none;line-height:1.3;}&#10;.mscs-root input[type=number]{min-width:0;}&#10;.mscs-root input[type=checkbox]{width:16px;height:16px;margin:0;accent-color:var(--mscs-blue);vertical-align:middle;}&#10;.mscs-root input[type=range]{width:100%;min-width:90px;margin:0;accent-color:var(--mscs-blue);padding:0;border:0;background:transparent;}&#10;.mscs-root label{font-weight:600;font-size:13px;color:#374151;display:block;margin:0 0 3px;}&#10;.mscs-root label.mscs-inline{display:inline-flex;align-items:center;gap:6px;font-weight:500;margin:0;}&#10;.mscs-root .mscs-field{margin-bottom:10px;}&#10;.mscs-root .mscs-row{display:flex;flex-wrap:wrap;gap:12px;align-items:flex-end;}&#10;.mscs-root .mscs-row &gt; .mscs-field{flex:1 1 180px;}&#10;\/* Candidate cards *\/&#10;.mscs-root .mscs-cands{display:grid;grid-template-columns:repeat(auto-fill,minmax(270px,1fr));gap:14px;}&#10;.mscs-root .mscs-cand{border:1px solid var(--mscs-border);border-radius:10px;padding:12px;background:#fafbfd;border-top:4px solid var(--mscs-c,#1e40af);}&#10;.mscs-root .mscs-cand-head{display:flex;justify-content:space-between;align-items:center;margin-bottom:8px;gap:8px;}&#10;.mscs-root .mscs-cand-head strong{font-size:14px;}&#10;.mscs-root .mscs-cand select[size]{height:auto;padding:2px;}&#10;.mscs-root .mscs-src{font-size:12px;color:var(--mscs-muted);background:#fff;border:1px dashed var(--mscs-border);border-radius:6px;padding:6px 8px;margin-top:6px;}&#10;\/* Tables *\/&#10;.mscs-root .mscs-table-wrap{width:100%;overflow-x:auto;-webkit-overflow-scrolling:touch;border:1px solid var(--mscs-border);border-radius:9px;margin-bottom:12px;}&#10;.mscs-root table{border-collapse:collapse;width:100%;margin:0;font-size:13.5px;background:#fff;border:0;}&#10;.mscs-root th,.mscs-root td{padding:7px 9px;border:0;border-bottom:1px solid #e8ecf1;text-align:left;vertical-align:middle;}&#10;.mscs-root thead th{background:#f1f4f9;color:#1f2a44;font-weight:650;font-size:12.5px;white-space:nowrap;position:sticky;top:0;}&#10;.mscs-root tbody tr:hover td{background:#f8fafd;}&#10;.mscs-root td.mscs-num,.mscs-root th.mscs-num{text-align:right;font-variant-numeric:tabular-nums;white-space:nowrap;}&#10;.mscs-root tr.mscs-inactive td{color:#9aa3af;}&#10;.mscs-root tr.mscs-inactive input[type=range]{opacity:.5;}&#10;.mscs-root .mscs-crit-table td{white-space:nowrap;}&#10;.mscs-root .mscs-crit-table input[type=number]{width:80px;}&#10;.mscs-root .mscs-crit-table select{width:auto;min-width:120px;}&#10;.mscs-root .mscs-wcell{display:flex;align-items:center;gap:6px;min-width:190px;}&#10;.mscs-root .mscs-wcell input[type=number]{width:70px;}&#10;.mscs-root .mscs-matrix td{min-width:150px;vertical-align:top;}&#10;.mscs-root .mscs-matrix td:first-child{min-width:190px;}&#10;.mscs-root .mscs-cell{display:flex;flex-direction:column;gap:4px;}&#10;.mscs-root .mscs-cell-sub{display:flex;gap:6px;align-items:center;}&#10;.mscs-root .mscs-cell-sub input[type=text]{font-size:12px;padding:3px 6px;}&#10;.mscs-root .mscs-cell.mscs-is-est input[type=number],.mscs-root .mscs-cell.mscs-is-est select{background:#fff8e6;border-color:#e8b75a;}&#10;.mscs-root .mscs-derived{font-variant-numeric:tabular-nums;color:#374151;}&#10;.mscs-root details.mscs-cat{border:1px solid var(--mscs-border);border-radius:9px;margin-bottom:10px;background:#fff;}&#10;.mscs-root details.mscs-cat &gt; summary{cursor:pointer;padding:9px 12px;font-weight:650;list-style:revert;}&#10;.mscs-root details.mscs-cat .mscs-table-wrap{border:0;border-top:1px solid var(--mscs-border);border-radius:0;margin:0;}&#10;\/* Badges *\/&#10;.mscs-root .mscs-badge{display:inline-block;font-size:11.5px;font-weight:700;padding:2px 8px;border-radius:99px;white-space:nowrap;}&#10;.mscs-root .mscs-pass{background:var(--mscs-ok-bg);color:var(--mscs-ok);}&#10;.mscs-root .mscs-fail{background:var(--mscs-bad-bg);color:var(--mscs-bad);}&#10;.mscs-root .mscs-ne{background:var(--mscs-ne-bg);color:var(--mscs-ne);}&#10;.mscs-root .mscs-est{background:var(--mscs-warn-bg);color:var(--mscs-warn);}&#10;.mscs-root .mscs-none{background:#f1f4f9;color:#6b7280;}&#10;\/* Messages *\/&#10;.mscs-root .mscs-msg{border-radius:8px;padding:9px 12px;margin-bottom:8px;font-size:13.5px;}&#10;.mscs-root .mscs-msg ul{margin:4px 0 0;}&#10;.mscs-root .mscs-msg-err{background:var(--mscs-bad-bg);color:#7f1d1d;border:1px solid #f3c2c2;}&#10;.mscs-root .mscs-msg-warn{background:var(--mscs-warn-bg);color:#6b3d00;border:1px solid #f1d29b;}&#10;.mscs-root .mscs-msg-info{background:var(--mscs-blue-l);color:#1e3a8a;border:1px solid #c9d6f5;}&#10;\/* KPI cards *\/&#10;.mscs-root .mscs-kpis{display:grid;grid-template-columns:repeat(auto-fit,minmax(180px,1fr));gap:12px;margin-bottom:14px;}&#10;.mscs-root .mscs-kpi{border:1px solid var(--mscs-border);border-radius:10px;padding:12px;background:#fbfcfe;}&#10;.mscs-root .mscs-kpi-label{font-size:12px;color:var(--mscs-muted);text-transform:uppercase;letter-spacing:.03em;font-weight:600;}&#10;.mscs-root .mscs-kpi-val{font-size:22px;font-weight:700;color:var(--mscs-blue-d);line-height:1.25;margin-top:2px;word-break:break-word;}&#10;.mscs-root .mscs-kpi-sub{font-size:12.5px;color:var(--mscs-muted);}&#10;.mscs-root .mscs-kpi.mscs-kpi-top{border-color:#b9c9f0;background:var(--mscs-blue-l);}&#10;.mscs-root .mscs-chips{display:flex;flex-wrap:wrap;gap:6px;margin-bottom:12px;}&#10;.mscs-root .mscs-chip{font-size:12.5px;background:#f1f4f9;border:1px solid var(--mscs-border);border-radius:99px;padding:3px 10px;}&#10;.mscs-root .mscs-chip.mscs-dropped{text-decoration:line-through;color:#9aa3af;}&#10;.mscs-root .mscs-interp{border-left:4px solid var(--mscs-blue);background:#f8faff;padding:12px 14px;border-radius:0 9px 9px 0;margin-bottom:14px;}&#10;.mscs-root .mscs-interp h4{margin:10px 0 4px;}&#10;.mscs-root .mscs-interp h4:first-child{margin-top:0;}&#10;.mscs-root details.mscs-details{border:1px solid var(--mscs-border);border-radius:9px;padding:0;margin-bottom:12px;background:#fff;}&#10;.mscs-root details.mscs-details &gt; summary{cursor:pointer;padding:10px 14px;font-weight:650;color:var(--mscs-blue-d);}&#10;.mscs-root details.mscs-details &gt; .mscs-details-body{padding:4px 14px 14px;}&#10;\/* Charts *\/&#10;.mscs-root label.mscs-inline{flex-wrap:wrap;}&#10;.mscs-root .mscs-cell-sub label.mscs-inline{flex-wrap:nowrap;white-space:nowrap;flex:0 0 auto;}&#10;.mscs-root .mscs-empty{padding:28px 10px;text-align:center;color:var(--mscs-muted);font-size:13.5px;}&#10;\/* Content *\/&#10;.mscs-root .mscs-content h2{margin-top:6px;}&#10;.mscs-root .mscs-content h3{margin-top:14px;}&#10;.mscs-root .mscs-apps{display:grid;grid-template-columns:repeat(auto-fit,minmax(240px,1fr));gap:12px;}&#10;.mscs-root .mscs-app-card{border:1px solid var(--mscs-border);border-radius:10px;padding:12px;background:#fbfcfe;}&#10;.mscs-root .mscs-faq details{border-bottom:1px solid var(--mscs-border);padding:8px 0;}&#10;.mscs-root .mscs-faq summary{cursor:pointer;font-weight:650;}&#10;.mscs-root .mscs-faq details p{margin:6px 0 2px;}&#10;.mscs-root .mscs-formula{font-family:Consolas,&quot;Courier New&quot;,monospace;background:#f1f4f9;border-radius:6px;padding:2px 6px;font-size:13px;}&#10;.mscs-root .mscs-grid2{display:grid;grid-template-columns:repeat(auto-fit,minmax(min(100%,420px),1fr));gap:14px;}&#10;.mscs-root .mscs-hidden{display:none !important;}&#10;.mscs-root .mscs-sub-sep{margin-top:22px;padding-top:18px;border-top:1px solid var(--mscs-border);}&#10;.mscs-root .mscs-calc-wrap{display:block !important;width:100% !important;text-align:center !important;margin:22px 0 4px !important;padding:18px 0 0 !important;float:none !important;clear:both;border-top:1px solid var(--mscs-border);}&#10;.mscs-root .mscs-calc-wrap .mscs-btn-calc{display:inline-flex !important;justify-content:center;float:none !important;width:auto !important;min-width:200px;margin:0 auto !important;position:static !important;padding:11px 28px;font-size:16px;}&#10;.mscs-root .mscs-sr{position:absolute;width:1px;height:1px;overflow:hidden;clip:rect(0 0 0 0);white-space:nowrap;}&#10;.mscs-root{position:relative;}&#10;.mscs-root .mscs-toast{position:absolute;background:#111827;color:#fff;padding:10px 14px;border-radius:8px;font-size:13.5px;z-index:99999;box-shadow:0 4px 14px rgba(0,0,0,.25);}&#10;@media (max-width:640px){.mscs-root{padding:16px 10px;font-size:14.5px;}.mscs-root h1{font-size:23px;}.mscs-root .mscs-card{padding:14px;}.mscs-root .mscs-header{padding:18px 16px;}}&#10;@media print{.mscs-root .mscs-toolbar,.mscs-root .mscs-btn{display:none !important;}}&#10;&lt;\/style&gt;&#10;&#10;&lt;div class=&quot;mscs-wrap&quot;&gt;&#10;&#10;&#10;  &lt;!-- ============ INPUT BLOCK (material selection + evaluation configuration) ============ --&gt;&#10;  &lt;section class=&quot;mscs-card&quot; aria-label=&quot;Calculator inputs&quot;&gt;&#10;    &lt;div class=&quot;mscs-sec-title&quot;&gt;&#10;      &lt;span class=&quot;mscs-sec-num&quot; aria-hidden=&quot;true&quot;&gt;&lt;svg viewBox=&quot;0 0 24 24&quot; fill=&quot;none&quot; stroke=&quot;currentColor&quot; stroke-width=&quot;2&quot;&gt;&lt;path d=&quot;M12 2 3 7v10l9 5 9-5V7z&quot;\/&gt;&lt;path d=&quot;M3 7l9 5 9-5M12 12v10&quot;\/&gt;&lt;\/svg&gt;&lt;\/span&gt;&#10;      &lt;h2 id=&quot;mscs-s1&quot;&gt;1. Material Selection&lt;\/h2&gt;&#10;    &lt;\/div&gt;&#10;    &lt;p class=&quot;mscs-muted&quot;&gt;Compare two to six candidates. Search the reference database by name or grade, filter by category, or add a custom material. Database values are &lt;strong&gt;approximate reference values for the stated condition&lt;\/strong&gt; (room temperature); every value is editable in the criterion value matrix below. Properties that are not documented are left blank \u2014 they are never filled with zero.&lt;\/p&gt;&#10;    &lt;div class=&quot;mscs-cands&quot; id=&quot;mscs-cands&quot;&gt;&lt;\/div&gt;&#10;    &lt;div class=&quot;mscs-toolbar&quot;&gt;&#10;      &lt;button type=&quot;button&quot; class=&quot;mscs-btn mscs-btn-sec&quot; data-act=&quot;addMat&quot;&gt;+ Add Material&lt;\/button&gt;&#10;      &lt;button type=&quot;button&quot; class=&quot;mscs-btn mscs-btn-sec&quot; data-act=&quot;addCustom&quot;&gt;+ Add Custom Material&lt;\/button&gt;&#10;      &lt;span class=&quot;mscs-muted&quot; id=&quot;mscs-cand-count&quot; aria-live=&quot;polite&quot;&gt;&lt;\/span&gt;&#10;    &lt;\/div&gt;&#10;&#10;    &lt;div class=&quot;mscs-sec-title mscs-sub-sep&quot;&gt;&#10;      &lt;span class=&quot;mscs-sec-num&quot; aria-hidden=&quot;true&quot;&gt;&lt;svg viewBox=&quot;0 0 24 24&quot; fill=&quot;none&quot; stroke=&quot;currentColor&quot; stroke-width=&quot;2&quot;&gt;&lt;path d=&quot;M4 6h10M18 6h2M4 12h4M12 12h8M4 18h12M20 18h0&quot;\/&gt;&lt;circle cx=&quot;16&quot; cy=&quot;6&quot; r=&quot;2&quot;\/&gt;&lt;circle cx=&quot;10&quot; cy=&quot;12&quot; r=&quot;2&quot;\/&gt;&lt;circle cx=&quot;18&quot; cy=&quot;18&quot; r=&quot;2&quot;\/&gt;&lt;\/svg&gt;&lt;\/span&gt;&#10;      &lt;h2 id=&quot;mscs-s2&quot;&gt;2. Evaluation Configuration&lt;\/h2&gt;&#10;    &lt;\/div&gt;&#10;&#10;    &lt;div class=&quot;mscs-row&quot;&gt;&#10;      &lt;div class=&quot;mscs-field&quot;&gt;&#10;        &lt;label for=&quot;mscs-preset&quot;&gt;Application preset&lt;\/label&gt;&#10;        &lt;select id=&quot;mscs-preset&quot; data-g=&quot;preset&quot;&gt;&lt;\/select&gt;&#10;      &lt;\/div&gt;&#10;      &lt;div class=&quot;mscs-field&quot;&gt;&#10;        &lt;label for=&quot;mscs-mode&quot;&gt;Missing-data handling&lt;\/label&gt;&#10;        &lt;select id=&quot;mscs-mode&quot; data-g=&quot;missingMode&quot;&gt;&#10;          &lt;option value=&quot;common&quot;&gt;Option 2 \u2013 Use common available criteria&lt;\/option&gt;&#10;          &lt;option value=&quot;complete&quot;&gt;Option 1 \u2013 Require complete data&lt;\/option&gt;&#10;          &lt;option value=&quot;estimate&quot;&gt;Option 3 \u2013 Accept user-supplied estimates&lt;\/option&gt;&#10;        &lt;\/select&gt;&#10;      &lt;\/div&gt;&#10;      &lt;div class=&quot;mscs-field&quot; style=&quot;flex:0 0 auto&quot;&gt;&#10;        &lt;div class=&quot;mscs-toolbar&quot; style=&quot;margin-top:0&quot;&gt;&#10;          &lt;button type=&quot;button&quot; class=&quot;mscs-btn mscs-btn-sec&quot; data-act=&quot;normW&quot;&gt;Normalize Weights&lt;\/button&gt;&#10;          &lt;button type=&quot;button&quot; class=&quot;mscs-btn mscs-btn-sec&quot; data-act=&quot;resetW&quot;&gt;Reset Weights (equal)&lt;\/button&gt;&#10;        &lt;\/div&gt;&#10;      &lt;\/div&gt;&#10;    &lt;\/div&gt;&#10;    &lt;p class=&quot;mscs-muted&quot; id=&quot;mscs-preset-note&quot;&gt;&lt;\/p&gt;&#10;    &lt;p class=&quot;mscs-muted&quot; id=&quot;mscs-mode-note&quot;&gt;&lt;\/p&gt;&#10;    &lt;div id=&quot;mscs-weight-total&quot; class=&quot;mscs-msg mscs-msg-info&quot; aria-live=&quot;polite&quot;&gt;&lt;\/div&gt;&#10;&#10;    &lt;h3&gt;Active criteria, optimization direction &amp;amp; weights&lt;\/h3&gt;&#10;    &lt;p class=&quot;mscs-muted&quot;&gt;Tick a criterion to activate it, choose whether higher, lower or a target value is best, and set its weight (0\u2013100%). Normalization defaults to the comparison set (min\u2013max of the ranked candidates); tick &lt;em&gt;Ref.&lt;\/em&gt; and enter a reference minimum and maximum for stable scores that do not change when candidates are added or removed. Ratings (1\u20135) are &lt;strong&gt;user assessments&lt;\/strong&gt; and are never filled in automatically.&lt;\/p&gt;&#10;    &lt;div id=&quot;mscs-crit&quot;&gt;&lt;\/div&gt;&#10;&#10;    &lt;details class=&quot;mscs-details&quot;&gt;&#10;      &lt;summary&gt;+ Add custom evaluation criterion&lt;\/summary&gt;&#10;      &lt;div class=&quot;mscs-details-body&quot;&gt;&#10;        &lt;div class=&quot;mscs-row&quot;&gt;&#10;          &lt;div class=&quot;mscs-field&quot;&gt;&lt;label for=&quot;mscs-nc-name&quot;&gt;Criterion name&lt;\/label&gt;&lt;input type=&quot;text&quot; id=&quot;mscs-nc-name&quot; placeholder=&quot;e.g. Chemical compatibility&quot;&gt;&lt;\/div&gt;&#10;          &lt;div class=&quot;mscs-field&quot;&gt;&lt;label for=&quot;mscs-nc-unit&quot;&gt;Unit&lt;\/label&gt;&lt;input type=&quot;text&quot; id=&quot;mscs-nc-unit&quot; placeholder=&quot;e.g. mm\/year&quot;&gt;&lt;\/div&gt;&#10;          &lt;div class=&quot;mscs-field&quot;&gt;&lt;label for=&quot;mscs-nc-cat&quot;&gt;Category&lt;\/label&gt;&lt;select id=&quot;mscs-nc-cat&quot;&gt;&lt;\/select&gt;&lt;\/div&gt;&#10;          &lt;div class=&quot;mscs-field&quot;&gt;&lt;label for=&quot;mscs-nc-kind&quot;&gt;Value type&lt;\/label&gt;&lt;select id=&quot;mscs-nc-kind&quot;&gt;&lt;option value=&quot;num&quot;&gt;Numerical value&lt;\/option&gt;&lt;option value=&quot;rating&quot;&gt;Rating 1\u20135 (user assessment)&lt;\/option&gt;&lt;\/select&gt;&lt;\/div&gt;&#10;        &lt;\/div&gt;&#10;        &lt;div class=&quot;mscs-row&quot;&gt;&#10;          &lt;div class=&quot;mscs-field&quot;&gt;&lt;label for=&quot;mscs-nc-dir&quot;&gt;Optimization direction&lt;\/label&gt;&lt;select id=&quot;mscs-nc-dir&quot;&gt;&lt;option value=&quot;higher&quot;&gt;Higher is better&lt;\/option&gt;&lt;option value=&quot;lower&quot;&gt;Lower is better&lt;\/option&gt;&lt;option value=&quot;target&quot;&gt;Closest to target&lt;\/option&gt;&lt;\/select&gt;&lt;\/div&gt;&#10;          &lt;div class=&quot;mscs-field&quot;&gt;&lt;label for=&quot;mscs-nc-w&quot;&gt;Weight (%)&lt;\/label&gt;&lt;input type=&quot;number&quot; id=&quot;mscs-nc-w&quot; min=&quot;0&quot; max=&quot;100&quot; step=&quot;0.5&quot; value=&quot;10&quot;&gt;&lt;\/div&gt;&#10;          &lt;div class=&quot;mscs-field&quot;&gt;&lt;label for=&quot;mscs-nc-t&quot;&gt;Target value (optional)&lt;\/label&gt;&lt;input type=&quot;number&quot; id=&quot;mscs-nc-t&quot; step=&quot;any&quot;&gt;&lt;\/div&gt;&#10;          &lt;div class=&quot;mscs-field&quot;&gt;&lt;label for=&quot;mscs-nc-tol&quot;&gt;Tolerance (optional, &amp;gt; 0)&lt;\/label&gt;&lt;input type=&quot;number&quot; id=&quot;mscs-nc-tol&quot; step=&quot;any&quot; min=&quot;0&quot;&gt;&lt;\/div&gt;&#10;        &lt;\/div&gt;&#10;        &lt;button type=&quot;button&quot; class=&quot;mscs-btn&quot; data-act=&quot;addCrit&quot;&gt;Add Criterion&lt;\/button&gt;&#10;        &lt;span class=&quot;mscs-muted&quot; id=&quot;mscs-nc-msg&quot; aria-live=&quot;polite&quot;&gt;&lt;\/span&gt;&#10;      &lt;\/div&gt;&#10;    &lt;\/details&gt;&#10;&#10;    &lt;h3 style=&quot;margin-top:16px&quot;&gt;Criterion values for each material&lt;\/h3&gt;&#10;    &lt;p class=&quot;mscs-muted&quot;&gt;Enter or edit values in the units shown. Tick &lt;span class=&quot;mscs-badge mscs-est&quot;&gt;Est.&lt;\/span&gt; to mark a value as a user estimate (used only with Option 3) and add a source note. Specific strength and specific stiffness are derived from yield strength, elastic modulus and density.&lt;\/p&gt;&#10;    &lt;label class=&quot;mscs-inline&quot; style=&quot;margin-bottom:8px&quot;&gt;&lt;input type=&quot;checkbox&quot; data-g=&quot;showAll&quot;&gt; Show rows for inactive criteria too&lt;\/label&gt;&#10;    &lt;div class=&quot;mscs-table-wrap&quot;&gt;&lt;table class=&quot;mscs-matrix&quot; id=&quot;mscs-matrix&quot;&gt;&lt;\/table&gt;&lt;\/div&gt;&#10;&#10;    &lt;h3 style=&quot;margin-top:16px&quot;&gt;Mandatory requirements (screening)&lt;\/h3&gt;&#10;    &lt;p class=&quot;mscs-muted&quot;&gt;Optional pass\/fail limits checked before ranking. A failed requirement is never overridden by a high weighted score. Missing data gives &lt;span class=&quot;mscs-badge mscs-ne&quot;&gt;Not Evaluated&lt;\/span&gt;, never \u201cPass\u201d.&lt;\/p&gt;&#10;    &lt;div id=&quot;mscs-reqs&quot;&gt;&lt;\/div&gt;&#10;    &lt;div class=&quot;mscs-toolbar&quot;&gt;&#10;      &lt;button type=&quot;button&quot; class=&quot;mscs-btn mscs-btn-sec&quot; data-act=&quot;addReq&quot;&gt;+ Add Requirement&lt;\/button&gt;&#10;      &lt;label class=&quot;mscs-inline&quot;&gt;&lt;input type=&quot;checkbox&quot; data-g=&quot;excludeFailed&quot; checked&gt; Exclude failed candidates from ranking&lt;\/label&gt;&#10;      &lt;label class=&quot;mscs-inline&quot;&gt;&lt;input type=&quot;checkbox&quot; data-g=&quot;excludeNE&quot;&gt; Also exclude candidates with Not Evaluated requirements&lt;\/label&gt;&#10;    &lt;\/div&gt;&#10;&#10;    &lt;div class=&quot;mscs-calc-wrap&quot;&gt;&#10;      &lt;button type=&quot;button&quot; class=&quot;mscs-btn mscs-btn-calc&quot; data-act=&quot;calculate&quot;&gt;Calculate&lt;\/button&gt;&#10;    &lt;\/div&gt;&#10;  &lt;\/section&gt;&#10;&#10;  &lt;!-- ============ SECTION 3: RESULTS ============ --&gt;&#10;  &lt;section class=&quot;mscs-card mscs-hidden&quot; aria-labelledby=&quot;mscs-s3&quot; id=&quot;mscs-results-sec&quot; tabindex=&quot;-1&quot;&gt;&#10;    &lt;div class=&quot;mscs-sec-title&quot;&gt;&#10;      &lt;span class=&quot;mscs-sec-num&quot; aria-hidden=&quot;true&quot;&gt;&lt;svg viewBox=&quot;0 0 24 24&quot; fill=&quot;none&quot; stroke=&quot;currentColor&quot; stroke-width=&quot;2&quot;&gt;&lt;path d=&quot;M8 21h8M12 17v4M7 4h10v5a5 5 0 0 1-10 0z&quot;\/&gt;&lt;path d=&quot;M17 5h3v2a3 3 0 0 1-3 3M7 5H4v2a3 3 0 0 0 3 3&quot;\/&gt;&lt;\/svg&gt;&lt;\/span&gt;&#10;      &lt;h2 id=&quot;mscs-s3&quot;&gt;3. Comparison Results&lt;\/h2&gt;&#10;    &lt;\/div&gt;&#10;    &lt;div id=&quot;mscs-results&quot; aria-live=&quot;polite&quot;&gt;&lt;\/div&gt;&#10;  &lt;\/section&gt;&#10;&#10;&lt;\/div&gt;&lt;!-- \/.mscs-wrap --&gt;&#10;&#10;&lt;script&gt;&#10;(function () {&#10;  &#x27;use strict&#x27;;&#10;  var root = document.getElementById(&#x27;mscs-app&#x27;);&#10;  if (!root || root.getAttribute(&#x27;data-mscs-ready&#x27;) === &#x27;1&#x27;) return;&#10;  root.setAttribute(&#x27;data-mscs-ready&#x27;, &#x27;1&#x27;);&#10;&#10;  \/* =====================================================================&#10;     1. MATERIAL REFERENCE DATA  (kept separate from calculation logic)&#10;     Units: density g\/cm\u00b3 \u00b7 yield &amp; tensile MPa \u00b7 modulus GPa \u00b7&#10;            thermal conductivity W\/(m\u00b7K) \u00b7 CTE \u00b5m\/(m\u00b7K)  \u2014 room temperature.&#10;     null = not documented here (never treated as zero).&#10;     ===================================================================== *\/&#10;  var SRC_TYP = &#x27;Approximate typical room-temperature handbook values for the stated condition. Values vary with product form, size, heat treatment, testing temperature and supplier \u2014 verify against certificates or the governing standard.&#x27;;&#10;  var SRC_PLASTIC = &#x27;Approximate typical values for an unfilled general-purpose grade at 23 \u00b0C. Polymer properties vary widely by grade, filler, moisture and temperature. &quot;Yield strength&quot; is left blank because polymer yield is not equivalent to metal 0.2% offset yield; the tensile value is tensile strength. Verify with the resin datasheet.&#x27;;&#10;  function mat(id, name, cat, grade, cond, p, src) {&#10;    return { id: id, name: name, cat: cat, grade: grade, cond: cond, src: src || SRC_TYP,&#10;      props: { density: p[0], yield: p[1], uts: p[2], modulus: p[3], thermalCond: p[4], cte: p[5] } };&#10;  }&#10;  var MATERIALS = [&#10;    mat(&#x27;a36&#x27;, &#x27;ASTM A36&#x27;, &#x27;Carbon &amp; Low-Alloy Steels&#x27;, &#x27;ASTM A36&#x27;, &#x27;As-rolled plate\/shapes&#x27;, [7.85, 250, 400, 200, 51.9, 11.7], &#x27;Yield (250 MPa) and tensile (400 MPa, range 400\u2013550) are ASTM A36 specified minimums; physical properties are approximate typical values.&#x27;),&#10;    mat(&#x27;1018&#x27;, &#x27;AISI 1018&#x27;, &#x27;Carbon &amp; Low-Alloy Steels&#x27;, &#x27;UNS G10180&#x27;, &#x27;Cold drawn bar&#x27;, [7.87, 370, 440, 205, 51.9, 12.0]),&#10;    mat(&#x27;1045&#x27;, &#x27;AISI 1045&#x27;, &#x27;Carbon &amp; Low-Alloy Steels&#x27;, &#x27;UNS G10450&#x27;, &#x27;Cold drawn bar&#x27;, [7.85, 530, 625, 205, 49.8, 11.2]),&#10;    mat(&#x27;4140&#x27;, &#x27;AISI 4140&#x27;, &#x27;Carbon &amp; Low-Alloy Steels&#x27;, &#x27;UNS G41400 (Cr-Mo alloy steel)&#x27;, &#x27;Annealed&#x27;, [7.85, 415, 655, 205, 42.6, 12.2]),&#10;    mat(&#x27;ss304&#x27;, &#x27;AISI 304&#x27;, &#x27;Stainless Steels&#x27;, &#x27;UNS S30400&#x27;, &#x27;Annealed sheet\/plate&#x27;, [8.00, 205, 515, 193, 16.2, 17.3], &#x27;Yield and tensile are ASTM A240 annealed minimums; physical properties are approximate typical values.&#x27;),&#10;    mat(&#x27;ss316&#x27;, &#x27;AISI 316&#x27;, &#x27;Stainless Steels&#x27;, &#x27;UNS S31600&#x27;, &#x27;Annealed sheet\/plate&#x27;, [8.00, 205, 515, 193, 16.3, 15.9], &#x27;Yield and tensile are ASTM A240 annealed minimums; physical properties are approximate typical values.&#x27;),&#10;    mat(&#x27;ss430&#x27;, &#x27;AISI 430&#x27;, &#x27;Stainless Steels&#x27;, &#x27;UNS S43000 (ferritic)&#x27;, &#x27;Annealed sheet\/plate&#x27;, [7.75, 205, 450, 200, 26.1, 10.4], &#x27;Yield and tensile are ASTM A240 annealed minimums; physical properties are approximate typical values.&#x27;),&#10;    mat(&#x27;ss321&#x27;, &#x27;AISI 321&#x27;, &#x27;Stainless Steels&#x27;, &#x27;UNS S32100 (Ti-stabilized)&#x27;, &#x27;Annealed sheet\/plate&#x27;, [7.92, 205, 515, 193, 16.1, 16.6], &#x27;Yield and tensile are ASTM A240 annealed minimums; physical properties are approximate typical values.&#x27;),&#10;    mat(&#x27;ss310&#x27;, &#x27;AISI 310 \/ 310S&#x27;, &#x27;Stainless Steels&#x27;, &#x27;UNS S31008&#x27;, &#x27;Annealed sheet\/plate&#x27;, [7.90, 205, 515, 200, 14.2, 15.9], &#x27;Yield and tensile are ASTM A240 annealed minimums; physical properties are approximate typical values.&#x27;),&#10;    mat(&#x27;al6061&#x27;, &#x27;Aluminum 6061-T6&#x27;, &#x27;Aluminum Alloys&#x27;, &#x27;AA 6061&#x27;, &#x27;T6 temper&#x27;, [2.70, 276, 310, 68.9, 167, 23.6]),&#10;    mat(&#x27;al6063&#x27;, &#x27;Aluminum 6063-T6&#x27;, &#x27;Aluminum Alloys&#x27;, &#x27;AA 6063&#x27;, &#x27;T6 temper (extrusion)&#x27;, [2.69, 214, 241, 68.9, 200, 23.4]),&#10;    mat(&#x27;al5052&#x27;, &#x27;Aluminum 5052-H32&#x27;, &#x27;Aluminum Alloys&#x27;, &#x27;AA 5052&#x27;, &#x27;H32 temper (sheet)&#x27;, [2.68, 193, 228, 70.3, 138, 23.8]),&#10;    mat(&#x27;al7075&#x27;, &#x27;Aluminum 7075-T6&#x27;, &#x27;Aluminum Alloys&#x27;, &#x27;AA 7075&#x27;, &#x27;T6 temper&#x27;, [2.81, 503, 572, 71.7, 130, 23.6]),&#10;    mat(&#x27;cu110&#x27;, &#x27;Pure Copper&#x27;, &#x27;Copper Alloys&#x27;, &#x27;C11000 (ETP)&#x27;, &#x27;Annealed&#x27;, [8.94, 69, 220, 115, 388, 17.0]),&#10;    mat(&#x27;brass360&#x27;, &#x27;Brass&#x27;, &#x27;Copper Alloys&#x27;, &#x27;C36000 free-cutting brass&#x27;, &#x27;Half hard (H02) rod&#x27;, [8.50, 310, 385, 97, 115, 20.5]),&#10;    mat(&#x27;bronze932&#x27;, &#x27;Bronze&#x27;, &#x27;Copper Alloys&#x27;, &#x27;C93200 bearing bronze&#x27;, &#x27;As cast&#x27;, [8.93, 125, 240, 100, 58, 18.0]),&#10;    mat(&#x27;ticp2&#x27;, &#x27;Commercially Pure Titanium&#x27;, &#x27;Titanium Alloys&#x27;, &#x27;Grade 2 (UNS R50400)&#x27;, &#x27;Annealed&#x27;, [4.51, 275, 345, 105, 16.4, 8.6], &#x27;Yield and tensile are ASTM B265 Grade 2 minimums; physical properties are approximate typical values.&#x27;),&#10;    mat(&#x27;ti64&#x27;, &#x27;Ti-6Al-4V&#x27;, &#x27;Titanium Alloys&#x27;, &#x27;Grade 5 (UNS R56400)&#x27;, &#x27;Annealed&#x27;, [4.43, 828, 895, 114, 6.7, 8.6], &#x27;Yield and tensile are ASTM B265 Grade 5 minimums; physical properties are approximate typical values.&#x27;),&#10;    mat(&#x27;in600&#x27;, &#x27;Inconel 600&#x27;, &#x27;Nickel Alloys&#x27;, &#x27;UNS N06600&#x27;, &#x27;Annealed&#x27;, [8.47, 240, 550, 207, 14.9, 13.3], &#x27;Yield and tensile are ASTM B168 annealed minimums; physical properties are approximate typical values.&#x27;),&#10;    mat(&#x27;in625&#x27;, &#x27;Inconel 625&#x27;, &#x27;Nickel Alloys&#x27;, &#x27;UNS N06625&#x27;, &#x27;Annealed (Grade 1)&#x27;, [8.44, 414, 827, 207, 9.8, 12.8], &#x27;Yield and tensile are ASTM B443 Grade 1 minimums; physical properties are approximate typical values.&#x27;),&#10;    mat(&#x27;in718&#x27;, &#x27;Inconel 718&#x27;, &#x27;Nickel Alloys&#x27;, &#x27;UNS N07718&#x27;, &#x27;Solution treated + aged&#x27;, [8.19, 1034, 1241, 200, 11.4, 13.0], &#x27;Yield and tensile are AMS 5663 aged minimums; physical properties are approximate typical values.&#x27;),&#10;    mat(&#x27;abs&#x27;, &#x27;ABS&#x27;, &#x27;Plastics&#x27;, &#x27;Acrylonitrile butadiene styrene&#x27;, &#x27;General-purpose, unfilled&#x27;, [1.05, null, 40, 2.3, 0.17, 90], SRC_PLASTIC),&#10;    mat(&#x27;pvc&#x27;, &#x27;PVC&#x27;, &#x27;Plastics&#x27;, &#x27;Rigid (unplasticized) PVC&#x27;, &#x27;Unfilled&#x27;, [1.40, null, 45, 3.0, 0.16, 60], SRC_PLASTIC),&#10;    mat(&#x27;pp&#x27;, &#x27;Polypropylene (PP)&#x27;, &#x27;Plastics&#x27;, &#x27;PP homopolymer&#x27;, &#x27;Unfilled&#x27;, [0.905, null, 33, 1.5, 0.12, 100], SRC_PLASTIC),&#10;    mat(&#x27;pe&#x27;, &#x27;Polyethylene (PE)&#x27;, &#x27;Plastics&#x27;, &#x27;HDPE&#x27;, &#x27;Unfilled&#x27;, [0.95, null, 26, 1.0, 0.45, 120], SRC_PLASTIC),&#10;    mat(&#x27;pa&#x27;, &#x27;Nylon (PA)&#x27;, &#x27;Plastics&#x27;, &#x27;PA 66&#x27;, &#x27;Unfilled, dry as molded&#x27;, [1.14, null, 80, 3.0, 0.25, 80], SRC_PLASTIC),&#10;    mat(&#x27;peek&#x27;, &#x27;PEEK&#x27;, &#x27;Plastics&#x27;, &#x27;Polyether ether ketone&#x27;, &#x27;Unfilled&#x27;, [1.30, null, 100, 3.6, 0.25, 47], SRC_PLASTIC),&#10;    mat(&#x27;ptfe&#x27;, &#x27;PTFE&#x27;, &#x27;Plastics&#x27;, &#x27;Polytetrafluoroethylene&#x27;, &#x27;Unfilled&#x27;, [2.17, null, 25, 0.5, 0.25, 120], SRC_PLASTIC),&#10;    mat(&#x27;ci30&#x27;, &#x27;Cast Iron&#x27;, &#x27;Other Materials&#x27;, &#x27;Gray iron, ASTM A48 Class 30&#x27;, &#x27;As cast&#x27;, [7.15, null, 207, 100, 46, 10.8], &#x27;Tensile is the ASTM A48 Class 30 minimum (30 ksi). Gray iron has no well-defined yield point, so yield is left blank. Modulus varies with stress level (approx. 90\u2013115 GPa).&#x27;),&#10;    mat(&#x27;az31&#x27;, &#x27;Magnesium Alloy&#x27;, &#x27;Other Materials&#x27;, &#x27;AZ31B&#x27;, &#x27;H24 sheet&#x27;, [1.77, 220, 290, 45, 96, 26.0]),&#10;    mat(&#x27;cfrp&#x27;, &#x27;Carbon Fiber Composite&#x27;, &#x27;Other Materials&#x27;, &#x27;CFRP laminate (epoxy matrix)&#x27;, &#x27;Layup-dependent&#x27;, [1.55, null, null, null, null, null], &#x27;Only an approximate density is given. Strength, modulus, conductivity and expansion of composites depend strongly on fiber type, volume fraction, layup and load direction \u2014 enter laminate-specific data.&#x27;),&#10;    mat(&#x27;gfrp&#x27;, &#x27;Fiberglass Composite&#x27;, &#x27;Other Materials&#x27;, &#x27;GFRP laminate (E-glass)&#x27;, &#x27;Layup-dependent&#x27;, [1.85, null, null, null, null, null], &#x27;Only an approximate density is given. Composite properties depend on fiber content, layup and load direction \u2014 enter laminate-specific data.&#x27;)&#10;  ];&#10;  var CUSTOM_MAT = { id: &#x27;custom&#x27;, name: &#x27;Custom Material&#x27;, cat: &#x27;Custom&#x27;, grade: &#x27;&#x27;, cond: &#x27;&#x27;, src: &#x27;User-defined material \u2014 enter properties and their sources.&#x27;, props: {} };&#10;&#10;  var CATS = { mech: &#x27;Mechanical&#x27;, phys: &#x27;Physical&#x27;, env: &#x27;Environmental&#x27;, mfg: &#x27;Manufacturing&#x27;, econ: &#x27;Economic&#x27; };&#10;  var CAT_KEYS = [&#x27;mech&#x27;, &#x27;phys&#x27;, &#x27;env&#x27;, &#x27;mfg&#x27;, &#x27;econ&#x27;];&#10;  var DIR_LABEL = { higher: &#x27;Higher is better&#x27;, lower: &#x27;Lower is better&#x27;, target: &#x27;Closest to target&#x27; };&#10;  var MODE_LABEL = { complete: &#x27;Option 1 \u2013 Require complete data&#x27;, common: &#x27;Option 2 \u2013 Use common available criteria&#x27;, estimate: &#x27;Option 3 \u2013 Accept user-supplied estimates&#x27; };&#10;  var RATING_UNIT = &#x27;rating 1\u20135&#x27;;&#10;  var RATING_TXT = { 1: &#x27;1 \u2013 Very Poor&#x27;, 2: &#x27;2 \u2013 Poor&#x27;, 3: &#x27;3 \u2013 Moderate&#x27;, 4: &#x27;4 \u2013 Good&#x27;, 5: &#x27;5 \u2013 Excellent&#x27; };&#10;&#10;  \/* Built-in criteria. kind: num (numeric property), rating (1\u20135 user assessment), derived (computed). *\/&#10;  function cdef(id, name, cat, unit, kind, dir) { return { id: id, name: name, cat: cat, unit: unit, kind: kind, dir: dir }; }&#10;  var BUILTIN = [&#10;    cdef(&#x27;yield&#x27;, &#x27;Yield strength&#x27;, &#x27;mech&#x27;, &#x27;MPa&#x27;, &#x27;num&#x27;, &#x27;higher&#x27;),&#10;    cdef(&#x27;uts&#x27;, &#x27;Tensile strength&#x27;, &#x27;mech&#x27;, &#x27;MPa&#x27;, &#x27;num&#x27;, &#x27;higher&#x27;),&#10;    cdef(&#x27;modulus&#x27;, &#x27;Elastic modulus (stiffness)&#x27;, &#x27;mech&#x27;, &#x27;GPa&#x27;, &#x27;num&#x27;, &#x27;higher&#x27;),&#10;    cdef(&#x27;hardness&#x27;, &#x27;Hardness&#x27;, &#x27;mech&#x27;, &#x27;HB&#x27;, &#x27;num&#x27;, &#x27;higher&#x27;),&#10;    cdef(&#x27;fatigue&#x27;, &#x27;Fatigue strength&#x27;, &#x27;mech&#x27;, &#x27;MPa&#x27;, &#x27;num&#x27;, &#x27;higher&#x27;),&#10;    cdef(&#x27;impact&#x27;, &#x27;Impact toughness&#x27;, &#x27;mech&#x27;, &#x27;J&#x27;, &#x27;num&#x27;, &#x27;higher&#x27;),&#10;    cdef(&#x27;hotStrength&#x27;, &#x27;Strength at operating temperature&#x27;, &#x27;mech&#x27;, &#x27;MPa&#x27;, &#x27;num&#x27;, &#x27;higher&#x27;),&#10;    cdef(&#x27;specStrength&#x27;, &#x27;Specific strength (yield \u00f7 density)&#x27;, &#x27;mech&#x27;, &#x27;MPa\/(g\/cm\u00b3)&#x27;, &#x27;derived&#x27;, &#x27;higher&#x27;),&#10;    cdef(&#x27;specStiffness&#x27;, &#x27;Specific stiffness (modulus \u00f7 density)&#x27;, &#x27;mech&#x27;, &#x27;GPa\/(g\/cm\u00b3)&#x27;, &#x27;derived&#x27;, &#x27;higher&#x27;),&#10;    cdef(&#x27;density&#x27;, &#x27;Density&#x27;, &#x27;phys&#x27;, &#x27;g\/cm\u00b3&#x27;, &#x27;num&#x27;, &#x27;lower&#x27;),&#10;    cdef(&#x27;thermalCond&#x27;, &#x27;Thermal conductivity&#x27;, &#x27;phys&#x27;, &#x27;W\/(m\u00b7K)&#x27;, &#x27;num&#x27;, &#x27;higher&#x27;),&#10;    cdef(&#x27;cte&#x27;, &#x27;Thermal expansion coefficient&#x27;, &#x27;phys&#x27;, &#x27;\u00b5m\/(m\u00b7K)&#x27;, &#x27;num&#x27;, &#x27;lower&#x27;),&#10;    cdef(&#x27;specificHeat&#x27;, &#x27;Specific heat capacity&#x27;, &#x27;phys&#x27;, &#x27;J\/(kg\u00b7K)&#x27;, &#x27;num&#x27;, &#x27;higher&#x27;),&#10;    cdef(&#x27;elecCond&#x27;, &#x27;Electrical conductivity&#x27;, &#x27;phys&#x27;, &#x27;% IACS&#x27;, &#x27;num&#x27;, &#x27;higher&#x27;),&#10;    cdef(&#x27;corrosion&#x27;, &#x27;Corrosion resistance&#x27;, &#x27;env&#x27;, RATING_UNIT, &#x27;rating&#x27;, &#x27;higher&#x27;),&#10;    cdef(&#x27;oxidation&#x27;, &#x27;Oxidation resistance&#x27;, &#x27;env&#x27;, RATING_UNIT, &#x27;rating&#x27;, &#x27;higher&#x27;),&#10;    cdef(&#x27;tempMax&#x27;, &#x27;Verified max. service temperature&#x27;, &#x27;env&#x27;, &#x27;\u00b0C&#x27;, &#x27;num&#x27;, &#x27;higher&#x27;),&#10;    cdef(&#x27;wear&#x27;, &#x27;Wear resistance&#x27;, &#x27;env&#x27;, RATING_UNIT, &#x27;rating&#x27;, &#x27;higher&#x27;),&#10;    cdef(&#x27;machinability&#x27;, &#x27;Machinability&#x27;, &#x27;mfg&#x27;, RATING_UNIT, &#x27;rating&#x27;, &#x27;higher&#x27;),&#10;    cdef(&#x27;weldability&#x27;, &#x27;Weldability&#x27;, &#x27;mfg&#x27;, RATING_UNIT, &#x27;rating&#x27;, &#x27;higher&#x27;),&#10;    cdef(&#x27;formability&#x27;, &#x27;Formability&#x27;, &#x27;mfg&#x27;, RATING_UNIT, &#x27;rating&#x27;, &#x27;higher&#x27;),&#10;    cdef(&#x27;castability&#x27;, &#x27;Castability&#x27;, &#x27;mfg&#x27;, RATING_UNIT, &#x27;rating&#x27;, &#x27;higher&#x27;),&#10;    cdef(&#x27;surfaceTreat&#x27;, &#x27;Surface treatment compatibility&#x27;, &#x27;mfg&#x27;, RATING_UNIT, &#x27;rating&#x27;, &#x27;higher&#x27;),&#10;    cdef(&#x27;mfgCompat&#x27;, &#x27;Overall manufacturing compatibility&#x27;, &#x27;mfg&#x27;, RATING_UNIT, &#x27;rating&#x27;, &#x27;higher&#x27;),&#10;    cdef(&#x27;matCost&#x27;, &#x27;Material cost&#x27;, &#x27;econ&#x27;, &#x27;cost per kg (consistent currency)&#x27;, &#x27;num&#x27;, &#x27;lower&#x27;),&#10;    cdef(&#x27;procCost&#x27;, &#x27;Processing cost (5 = lowest cost)&#x27;, &#x27;econ&#x27;, RATING_UNIT, &#x27;rating&#x27;, &#x27;higher&#x27;),&#10;    cdef(&#x27;availability&#x27;, &#x27;Availability&#x27;, &#x27;econ&#x27;, RATING_UNIT, &#x27;rating&#x27;, &#x27;higher&#x27;),&#10;    cdef(&#x27;maintenance&#x27;, &#x27;Maintenance requirements (5 = lowest)&#x27;, &#x27;econ&#x27;, RATING_UNIT, &#x27;rating&#x27;, &#x27;higher&#x27;),&#10;    cdef(&#x27;recyclability&#x27;, &#x27;Recyclability&#x27;, &#x27;econ&#x27;, RATING_UNIT, &#x27;rating&#x27;, &#x27;higher&#x27;),&#10;    cdef(&#x27;leadTime&#x27;, &#x27;Supply lead time&#x27;, &#x27;econ&#x27;, &#x27;weeks&#x27;, &#x27;num&#x27;, &#x27;lower&#x27;)&#10;  ];&#10;&#10;  \/* Application presets: editable starting points (raw weights, %). *\/&#10;  var PRESETS = {&#10;    structural: { name: &#x27;Structural Engineering&#x27;, w: { yield: 30, modulus: 20, density: 15, matCost: 20, corrosion: 15 } },&#10;    sheet: { name: &#x27;Sheet Metal Fabrication&#x27;, w: { formability: 25, weldability: 25, matCost: 20, yield: 15, availability: 15 } },&#10;    hightemp: { name: &#x27;High-Temperature Applications&#x27;, w: { tempMax: 30, oxidation: 25, hotStrength: 20, cte: 10, matCost: 15 } },&#10;    light: { name: &#x27;Lightweight Design&#x27;, w: { density: 25, specStrength: 25, specStiffness: 20, matCost: 15, mfgCompat: 15 } },&#10;    corrosive: { name: &#x27;Corrosive Environments&#x27;, w: { corrosion: 35, matCost: 20, yield: 15, availability: 15, maintenance: 15 } },&#10;    general: { name: &#x27;General Manufacturing&#x27;, w: { matCost: 25, machinability: 25, yield: 20, availability: 15, weldability: 15 } }&#10;  };&#10;&#10;  var MAT_COLORS = [&#x27;#1e40af&#x27;, &#x27;#d97706&#x27;, &#x27;#0f8a7e&#x27;, &#x27;#c0392b&#x27;, &#x27;#7c3aed&#x27;, &#x27;#64748b&#x27;];&#10;  var MAX_CANDS = 6, MIN_CANDS = 2;&#10;&#10;  \/* =====================================================================&#10;     2. UTILITIES&#10;     ===================================================================== *\/&#10;  var uidN = 1;&#10;  function uid(p) { return p + (uidN++) + &#x27;_&#x27; + Math.random().toString(36).slice(2, 6); }&#10;  function isNum(v) { return typeof v === &#x27;number&#x27; &amp;&amp; isFinite(v); }&#10;  function clamp(v, a, b) { return Math.max(a, Math.min(b, v)); }&#10;  function esc(s) { return String(s == null ? &#x27;&#x27; : s).replace(\/[&amp;&lt;&gt;&quot;&#x27;]\/g, function (c) { return { &#x27;&amp;&#x27;: &#x27;&amp;amp;&#x27;, &#x27;&lt;&#x27;: &#x27;&amp;lt;&#x27;, &#x27;&gt;&#x27;: &#x27;&amp;gt;&#x27;, &#x27;&quot;&#x27;: &#x27;&amp;quot;&#x27;, &quot;&#x27;&quot;: &#x27;&amp;#39;&#x27; }[c]; }); }&#10;  function fmt(v, d) { if (!isNum(v)) return &#x27;\u2014&#x27;; d = d == null ? 2 : d; return v.toFixed(d); }&#10;  function fmtVal(v) { if (!isNum(v)) return &#x27;\u2014&#x27;; var a = Math.abs(v); if (a !== 0 &amp;&amp; (a &lt; 0.01 || a &gt;= 1e6)) return v.toExponential(3); return String(Math.round(v * 1000) \/ 1000); }&#10;  function parseNum(s) { if (s == null) return null; s = String(s).trim(); if (s === &#x27;&#x27;) return null; var n = Number(s); return isFinite(n) ? n : NaN; }&#10;  function q(sel) { return root.querySelector(sel); }&#10;  function qa(sel) { return Array.prototype.slice.call(root.querySelectorAll(sel)); }&#10;  function findMat(id) { for (var i = 0; i &lt; MATERIALS.length; i++) if (MATERIALS[i].id === id) return MATERIALS[i]; return id === &#x27;custom&#x27; ? CUSTOM_MAT : null; }&#10;  function getCrit(id) { for (var i = 0; i &lt; state.criteria.length; i++) if (state.criteria[i].id === id) return state.criteria[i]; return null; }&#10;  function getCand(id) { for (var i = 0; i &lt; state.candidates.length; i++) if (state.candidates[i].uid === id) return state.candidates[i]; return null; }&#10;  function candColor(c) { var i = state.candidates.indexOf(c); return MAT_COLORS[(i &lt; 0 ? 0 : i) % MAT_COLORS.length]; }&#10;  var lastClickEl = null;&#10;  function toast(msg) {&#10;    qa(&#x27;.mscs-toast&#x27;).forEach(function (x) { x.parentNode.removeChild(x); });&#10;    var t = document.createElement(&#x27;div&#x27;); t.className = &#x27;mscs-toast&#x27;; t.setAttribute(&#x27;role&#x27;, &#x27;status&#x27;); t.textContent = msg;&#10;    var rr = root.getBoundingClientRect(), top = 20, left = 20;&#10;    if (lastClickEl &amp;&amp; root.contains(lastClickEl)) { var br = lastClickEl.getBoundingClientRect(); top = br.bottom - rr.top + 8; left = br.left - rr.left; }&#10;    t.style.top = top + &#x27;px&#x27;; t.style.left = Math.max(8, Math.min(left, rr.width - 280)) + &#x27;px&#x27;;&#10;    root.appendChild(t); setTimeout(function () { if (t.parentNode) t.parentNode.removeChild(t); }, 2600);&#10;  }&#10;  function download(name, text, mime) {&#10;    var blob = new Blob([text], { type: mime }); var url = URL.createObjectURL(blob);&#10;    var a = document.createElement(&#x27;a&#x27;); a.href = url; a.download = name; document.body.appendChild(a); a.click();&#10;    setTimeout(function () { document.body.removeChild(a); URL.revokeObjectURL(url); }, 100);&#10;  }&#10;&#10;  \/* =====================================================================&#10;     3. STATE&#10;     ===================================================================== *\/&#10;  var state;&#10;  function makeCriterion(def, custom) {&#10;    return { id: def.id, name: def.name, cat: def.cat, unit: def.unit, kind: def.kind, dir: def.dir, custom: !!custom,&#10;      active: false, weight: 0, target: null, tol: null, useRef: false, refMin: null, refMax: null };&#10;  }&#10;  function loadMaterialInto(c, m) {&#10;    c.matId = m.id; c.name = m.name; c.category = m.cat;&#10;    c.grade = [m.grade, m.cond].filter(Boolean).join(&#x27; \u2014 &#x27;);&#10;    c.source = m.src; c.vals = {};&#10;    Object.keys(m.props).forEach(function (k) {&#10;      if (isNum(m.props[k])) c.vals[k] = { v: m.props[k], est: false, note: &#x27;Database reference value&#x27; };&#10;    });&#10;  }&#10;  function makeCandidate(matId) {&#10;    var c = { uid: uid(&#x27;c&#x27;), filter: &#x27;&#x27;, catFilter: &#x27;&#x27; };&#10;    loadMaterialInto(c, findMat(matId) || CUSTOM_MAT);&#10;    return c;&#10;  }&#10;  function applyPreset(s, key) {&#10;    s.criteria.forEach(function (c) { c.active = false; c.weight = 0; });&#10;    s.preset = key;&#10;    var p = PRESETS[key]; if (!p) return;&#10;    Object.keys(p.w).forEach(function (id) {&#10;      for (var i = 0; i &lt; s.criteria.length; i++) if (s.criteria[i].id === id) { s.criteria[i].active = true; s.criteria[i].weight = p.w[id]; }&#10;    });&#10;  }&#10;  function defaultState() {&#10;    var s = { criteria: BUILTIN.map(function (d) { return makeCriterion(d, false); }),&#10;      candidates: [makeCandidate(&#x27;a36&#x27;), makeCandidate(&#x27;al6061&#x27;), makeCandidate(&#x27;ss304&#x27;)],&#10;      requirements: [], missingMode: &#x27;common&#x27;, excludeFailed: true, excludeNE: false, preset: &#x27;structural&#x27;,&#10;      showAll: false, radarMode: &#x27;category&#x27;, propChart: &#x27;yield&#x27;, sensCrit: &#x27;&#x27;, example: false };&#10;    applyPreset(s, &#x27;structural&#x27;);&#10;    return s;&#10;  }&#10;&#10;  \/* =====================================================================&#10;     4. CALCULATION ENGINE&#10;     ===================================================================== *\/&#10;&#10;  \/* Property validation rules (unit-consistent checks). Returns an error string or null. *\/&#10;  function validateValue(cr, v) {&#10;    if (!isNum(v)) return &#x27;not a valid number&#x27;;&#10;    if (cr.kind === &#x27;rating&#x27; &amp;&amp; (v &lt; 1 || v &gt; 5)) return &#x27;rating must be between 1 and 5&#x27;;&#10;    switch (cr.id) {&#10;      case &#x27;density&#x27;: if (v &lt;= 0) return &#x27;density must be positive&#x27;; break;&#10;      case &#x27;modulus&#x27;: if (v &lt;= 0) return &#x27;elastic modulus must be positive&#x27;; break;&#10;      case &#x27;yield&#x27;: case &#x27;uts&#x27;: case &#x27;fatigue&#x27;: case &#x27;hotStrength&#x27;: if (v &lt; 0) return &#x27;strength cannot be negative&#x27;; break;&#10;      case &#x27;matCost&#x27;: if (v &lt; 0) return &#x27;material cost cannot be negative&#x27;; break;&#10;      case &#x27;leadTime&#x27;: if (v &lt; 0) return &#x27;lead time cannot be negative&#x27;; break;&#10;      case &#x27;hardness&#x27;: case &#x27;impact&#x27;: case &#x27;thermalCond&#x27;: case &#x27;specificHeat&#x27;: case &#x27;elecCond&#x27;: if (v &lt; 0) return &#x27;value cannot be negative&#x27;; break;&#10;    }&#10;    return null;&#10;  }&#10;&#10;  \/* Raw cell for candidate\/criterion. Derived values are computed, never stored. *\/&#10;  function cellRaw(c, cr) {&#10;    if (cr.kind === &#x27;derived&#x27;) {&#10;      var num = c.vals[cr.id === &#x27;specStrength&#x27; ? &#x27;yield&#x27; : &#x27;modulus&#x27;], den = c.vals.density;&#10;      if (num &amp;&amp; den &amp;&amp; isNum(num.v) &amp;&amp; isNum(den.v) &amp;&amp; den.v &gt; 0 &amp;&amp; num.v &gt;= 0) {&#10;        \/\/ Specific strength = \u03c3y \/ \u03c1 ; Specific stiffness = E \/ \u03c1&#10;        return { v: num.v \/ den.v, est: !!(num.est || den.est), note: &#x27;Derived&#x27; };&#10;      }&#10;      return { v: null, est: false, note: &#x27;&#x27; };&#10;    }&#10;    return c.vals[cr.id] || { v: null, est: false, note: &#x27;&#x27; };&#10;  }&#10;&#10;  \/* Usability of a value under the selected missing-data mode.&#10;     status: ok | missing | invalid | est (estimate not accepted in this mode) *\/&#10;  function valueStatus(c, cr, mode) {&#10;    var r = cellRaw(c, cr);&#10;    if (r.v === null || r.v === undefined) return { status: &#x27;missing&#x27;, v: null, est: false };&#10;    if (!isNum(r.v) || validateValue(cr, r.v)) return { status: &#x27;invalid&#x27;, v: null, est: r.est };&#10;    if (r.est &amp;&amp; mode !== &#x27;estimate&#x27;) return { status: &#x27;est&#x27;, v: r.v, est: true };&#10;    return { status: &#x27;ok&#x27;, v: r.v, est: !!r.est };&#10;  }&#10;&#10;  \/* Screening: Pass \/ Fail \/ Not Evaluated for one requirement. *\/&#10;  function reqStatus(c, rq, mode) {&#10;    var cr = getCrit(rq.crit);&#10;    if (!cr || !isNum(rq.value)) return { st: &#x27;ne&#x27;, why: &#x27;requirement incomplete&#x27; };&#10;    var s = valueStatus(c, cr, mode);&#10;    if (s.status !== &#x27;ok&#x27;) return { st: &#x27;ne&#x27;, why: s.status === &#x27;est&#x27; ? &#x27;only an estimate is available&#x27; : &#x27;no valid data&#x27; };&#10;    var pass = rq.op === &#x27;&gt;=&#x27; ? s.v &gt;= rq.value : s.v &lt;= rq.value;&#10;    return { st: pass ? &#x27;pass&#x27; : &#x27;fail&#x27;, v: s.v, est: s.est };&#10;  }&#10;  function reqLabel(rq) { var cr = getCrit(rq.crit); return (cr ? cr.name : &#x27;?&#x27;) + &#x27; &#x27; + (rq.op === &#x27;&gt;=&#x27; ? &#x27;\u2265&#x27; : &#x27;\u2264&#x27;) + &#x27; &#x27; + (isNum(rq.value) ? fmtVal(rq.value) : &#x27;?&#x27;) + (cr &amp;&amp; cr.unit ? &#x27; &#x27; + cr.unit : &#x27;&#x27;); }&#10;&#10;  \/* Min\u2013max \/ target normalization of one criterion across the ranked set.&#10;     Higher:  s = 100\u00b7(x \u2212 min)\/(max \u2212 min)&#10;     Lower:   s = 100\u00b7(max \u2212 x)\/(max \u2212 min)&#10;     Target:  s = 100\u00b7max(0, 1 \u2212 |x \u2212 T|\/tol)&#10;     Identical values \u2192 neutral 100 for all candidates. Results clamped to 0\u2013100. *\/&#10;  function normalizeCriterion(cr, ranked, mode) {&#10;    var vals = ranked.map(function (c) { return valueStatus(c, cr, mode).v; });&#10;    var mn = null, mx = null, method, identical = false, scores = {};&#10;    if (cr.dir === &#x27;target&#x27;) method = &#x27;Target &#x27; + fmtVal(cr.target) + &#x27; \u00b1 &#x27; + fmtVal(cr.tol);&#10;    else if (cr.useRef) { mn = cr.refMin; mx = cr.refMax; method = &#x27;Reference min\u2013max&#x27;; }&#10;    else { mn = Math.min.apply(null, vals); mx = Math.max.apply(null, vals); method = &#x27;Comparison-set min\u2013max&#x27;; }&#10;    ranked.forEach(function (c, i) {&#10;      var x = vals[i], s;&#10;      if (cr.dir === &#x27;target&#x27;) s = 100 * Math.max(0, 1 - Math.abs(x - cr.target) \/ cr.tol);&#10;      else if (mx === mn) { s = 100; identical = true; }&#10;      else if (cr.dir === &#x27;higher&#x27;) s = 100 * (x - mn) \/ (mx - mn);&#10;      else s = 100 * (mx - x) \/ (mx - mn);&#10;      scores[c.uid] = clamp(s, 0, 100);&#10;    });&#10;    return { min: mn, max: mx, method: method, identical: identical, scores: scores, raw: vals };&#10;  }&#10;&#10;  \/* Weighted total S = \u03a3 s\u1d62\u00b7w\u1d62 with \u03a3w\u1d62 = 1 *\/&#10;  function totalFor(uidc, eff, norm, weights) {&#10;    var W = 0, S = 0;&#10;    eff.forEach(function (cr) { var w = weights[cr.id]; if (w &gt; 0) { W += w; S += norm[cr.id].scores[uidc] * w; } });&#10;    return W &gt; 0 ? S \/ W : null;&#10;  }&#10;&#10;  function compute() {&#10;    var s = state, mode = s.missingMode;&#10;    var R = { errors: [], warnings: [], notes: [], rows: [], excluded: [], effective: [], dropped: [], zeroW: [], norm: {}, screen: {}, complete: {}, ok: false, mode: mode };&#10;    var cands = s.candidates;&#10;&#10;    \/\/ --- Input validation&#10;    cands.forEach(function (c) {&#10;      s.criteria.forEach(function (cr) {&#10;        if (cr.kind === &#x27;derived&#x27;) return;&#10;        var r = c.vals[cr.id]; if (!r || r.v === null || r.v === undefined) return;&#10;        var e = validateValue(cr, r.v); if (e) R.errors.push(c.name + &#x27; \u2014 &#x27; + cr.name + &#x27;: &#x27; + e + &#x27;.&#x27;);&#10;      });&#10;    });&#10;    s.requirements.forEach(function (rq) {&#10;      if (rq.value !== null &amp;&amp; !isNum(rq.value)) R.errors.push(&#x27;Requirement &quot;&#x27; + reqLabel(rq) + &#x27;&quot;: limit value is not a valid number.&#x27;);&#10;    });&#10;    if (cands.length &lt; MIN_CANDS) R.warnings.push(&#x27;Add at least two materials to compare.&#x27;);&#10;&#10;    var active = s.criteria.filter(function (c) { return c.active; });&#10;    if (!active.length) R.errors.push(&#x27;No evaluation criteria are active. Tick at least one criterion or apply a preset.&#x27;);&#10;    var usable = [];&#10;    active.forEach(function (c) {&#10;      var ok = true;&#10;      if (!isNum(c.weight) || c.weight &lt; 0) { R.errors.push(c.name + &#x27;: weight cannot be negative.&#x27;); ok = false; }&#10;      if (c.dir === &#x27;target&#x27;) {&#10;        if (!isNum(c.target)) { R.errors.push(c.name + &#x27;: a target value is required for &quot;Closest to target&quot;.&#x27;); ok = false; }&#10;        if (!isNum(c.tol) || c.tol &lt;= 0) { R.errors.push(c.name + &#x27;: target tolerance must be greater than zero.&#x27;); ok = false; }&#10;      } else if (c.useRef) {&#10;        if (!isNum(c.refMin) || !isNum(c.refMax) || c.refMin &gt;= c.refMax) { R.errors.push(c.name + &#x27;: reference minimum must be a number less than the reference maximum.&#x27;); ok = false; }&#10;      }&#10;      if (ok) usable.push(c);&#10;    });&#10;    var wSum = active.reduce(function (a, c) { return a + (isNum(c.weight) &amp;&amp; c.weight &gt; 0 ? c.weight : 0); }, 0);&#10;    R.wSum = wSum;&#10;    if (active.length &amp;&amp; wSum &lt;= 0) R.errors.push(&#x27;All active criterion weights are zero. Assign a positive weight to at least one criterion.&#x27;);&#10;    R.zeroW = usable.filter(function (c) { return !(c.weight &gt; 0); });&#10;&#10;    \/\/ --- Mandatory requirement screening&#10;    cands.forEach(function (c) {&#10;      var rows = s.requirements.map(function (rq) { return reqStatus(c, rq, mode); });&#10;      var overall = &#x27;none&#x27;;&#10;      if (rows.length) overall = rows.some(function (r) { return r.st === &#x27;fail&#x27;; }) ? &#x27;fail&#x27; : rows.some(function (r) { return r.st === &#x27;ne&#x27;; }) ? &#x27;ne&#x27; : &#x27;pass&#x27;;&#10;      R.screen[c.uid] = { rows: rows, overall: overall, passed: rows.filter(function (r) { return r.st === &#x27;pass&#x27;; }).length, total: rows.length };&#10;    });&#10;&#10;    \/\/ --- Data completeness (active criteria)&#10;    cands.forEach(function (c) {&#10;      var k = 0, miss = [];&#10;      active.forEach(function (cr) {&#10;        var st = valueStatus(c, cr, mode);&#10;        if (st.status === &#x27;ok&#x27;) k++;&#10;        else miss.push(cr.name + (st.status === &#x27;est&#x27; ? &#x27; (estimate only)&#x27; : st.status === &#x27;invalid&#x27; ? &#x27; (invalid)&#x27; : &#x27;&#x27;));&#10;      });&#10;      R.complete[c.uid] = { k: k, n: active.length, pct: active.length ? 100 * k \/ active.length : 100, miss: miss };&#10;    });&#10;&#10;    \/\/ --- Eligibility pool&#10;    var pool = [];&#10;    cands.forEach(function (c) {&#10;      var sc = R.screen[c.uid];&#10;      if (sc.overall === &#x27;fail&#x27; &amp;&amp; s.excludeFailed) {&#10;        var f = s.requirements.filter(function (rq, i) { return sc.rows[i].st === &#x27;fail&#x27;; }).map(reqLabel);&#10;        R.excluded.push({ c: c, kind: &#x27;fail&#x27;, reason: &#x27;Failed mandatory requirement: &#x27; + f.join(&#x27;; &#x27;) }); return;&#10;      }&#10;      if (sc.overall === &#x27;ne&#x27; &amp;&amp; s.excludeNE) { R.excluded.push({ c: c, kind: &#x27;ne&#x27;, reason: &#x27;Mandatory requirement not evaluated (missing data)&#x27; }); return; }&#10;      pool.push(c);&#10;    });&#10;&#10;    \/\/ --- Missing-data handling&#10;    var scoring = usable.filter(function (c) { return c.weight &gt; 0; });&#10;    var eff = [], ranked = [];&#10;    if (mode === &#x27;common&#x27;) {&#10;      eff = scoring.filter(function (cr) { return pool.length &amp;&amp; pool.every(function (c) { return valueStatus(c, cr, mode).status === &#x27;ok&#x27;; }); });&#10;      R.dropped = scoring.filter(function (cr) { return eff.indexOf(cr) &lt; 0; });&#10;      ranked = pool.slice();&#10;    } else {&#10;      eff = scoring.slice();&#10;      pool.forEach(function (c) {&#10;        var miss = eff.filter(function (cr) { return valueStatus(c, cr, mode).status !== &#x27;ok&#x27;; });&#10;        if (miss.length) R.excluded.push({ c: c, kind: &#x27;data&#x27;, reason: &#x27;Incomplete data for: &#x27; + miss.map(function (m) { return m.name; }).join(&#x27;, &#x27;) + (mode === &#x27;estimate&#x27; ? &#x27; \u2014 enter a value or an estimate (tick Est.)&#x27; : &#x27;&#x27;) });&#10;        else ranked.push(c);&#10;      });&#10;    }&#10;    R.effective = eff;&#10;    if (R.errors.length) return R;&#10;    var effW = eff.reduce(function (a, c) { return a + c.weight; }, 0);&#10;    if (!ranked.length) { R.errors.push(&#x27;No candidates are eligible for ranking. Review the excluded candidates below.&#x27;); return R; }&#10;    if (!eff.length || effW &lt;= 0) { R.errors.push(&#x27;None of the weighted criteria have usable data for all ranked candidates. Enter property values or ratings in the criterion value matrix.&#x27;); return R; }&#10;    if (ranked.length === 1) R.warnings.push(&#x27;Only one candidate is eligible, so comparison-set normalization cannot discriminate (every min\u2013max criterion scores 100).&#x27;);&#10;&#10;    \/\/ --- Normalization &amp; weighting&#10;    var weights = {};&#10;    eff.forEach(function (cr) {&#10;      var n = normalizeCriterion(cr, ranked, mode);&#10;      n.wn = cr.weight \/ effW; weights[cr.id] = n.wn;&#10;      R.norm[cr.id] = n;&#10;      if (n.identical &amp;&amp; ranked.length &gt; 1) R.notes.push(cr.name + &#x27;: all ranked candidates have the same value, so each receives a neutral score of 100 and this criterion does not distinguish between them.&#x27;);&#10;    });&#10;    R.weights = weights; R.effW = effW;&#10;    R.rows = ranked.map(function (c) {&#10;      var contrib = {}, tot = 0, catS = {};&#10;      eff.forEach(function (cr) { var v = R.norm[cr.id].scores[c.uid] * weights[cr.id]; contrib[cr.id] = v; tot += v; });&#10;      \/\/ Category score = \u03a3(s\u1d62\u00b7w\u1d62)\/\u03a3w\u1d62 within the category&#10;      CAT_KEYS.forEach(function (k) {&#10;        var cc = eff.filter(function (cr) { return cr.cat === k; }), w = cc.reduce(function (a, cr) { return a + cr.weight; }, 0);&#10;        catS[k] = w &gt; 0 ? cc.reduce(function (a, cr) { return a + R.norm[cr.id].scores[c.uid] * cr.weight; }, 0) \/ w : null;&#10;      });&#10;      return { c: c, total: tot, contrib: contrib, catS: catS };&#10;    });&#10;    R.rows.sort(function (a, b) { return b.total - a.total; });&#10;    R.rows.forEach(function (r, i) {&#10;      r.rank = (i &gt; 0 &amp;&amp; Math.abs(r.total - R.rows[i - 1].total) &lt; 1e-9) ? R.rows[i - 1].rank : i + 1;&#10;    });&#10;    R.ok = true;&#10;    R.sens = sensitivity(R);&#10;    return R;&#10;  }&#10;&#10;  \/* One-at-a-time weight sensitivity: does the leader change if a weight is removed or doubled? *\/&#10;  function sensitivity(R) {&#10;    if (R.rows.length &lt; 2) return { flips: [], margin: null };&#10;    var leader = R.rows[0].c.uid, flips = [];&#10;    R.effective.forEach(function (cr) {&#10;      [0, 2].forEach(function (f) {&#10;        var w = {}; R.effective.forEach(function (x) { w[x.id] = R.weights[x.id] * (x === cr ? f : 1); });&#10;        var best = null, bestS = -1;&#10;        R.rows.forEach(function (r) { var t = totalFor(r.c.uid, R.effective, R.norm, w); if (t !== null &amp;&amp; t &gt; bestS + 1e-9) { bestS = t; best = r.c; } });&#10;        if (best &amp;&amp; best.uid !== leader) flips.push({ cr: cr, factor: f, newLeader: best });&#10;      });&#10;    });&#10;    return { flips: flips, margin: R.rows[0].total - R.rows[1].total };&#10;  }&#10;&#10;  \/* =====================================================================&#10;     5. RENDERING \u2014 INPUT SECTIONS&#10;     ===================================================================== *\/&#10;  function matOptions(c) {&#10;    var f = (c.filter || &#x27;&#x27;).toLowerCase(), cf = c.catFilter || &#x27;&#x27;;&#10;    var list = MATERIALS.filter(function (m) {&#10;      if (cf &amp;&amp; m.cat !== cf) return false;&#10;      if (!f) return true;&#10;      return (m.name + &#x27; &#x27; + m.grade + &#x27; &#x27; + m.cat + &#x27; &#x27; + m.cond).toLowerCase().indexOf(f) &gt;= 0;&#10;    });&#10;    var h = &#x27;&lt;option value=&quot;&quot;&gt;\u2014 Select a material (&#x27; + list.length + &#x27; match&#x27; + (list.length === 1 ? &#x27;&#x27; : &#x27;es&#x27;) + &#x27;) \u2014&lt;\/option&gt;&#x27;;&#10;    var groups = {};&#10;    list.forEach(function (m) { (groups[m.cat] = groups[m.cat] || []).push(m); });&#10;    Object.keys(groups).forEach(function (g) {&#10;      h += &#x27;&lt;optgroup label=&quot;&#x27; + esc(g) + &#x27;&quot;&gt;&#x27; + groups[g].map(function (m) {&#10;        return &#x27;&lt;option value=&quot;&#x27; + m.id + &#x27;&quot;&#x27; + (m.id === c.matId ? &#x27; selected&#x27; : &#x27;&#x27;) + &#x27;&gt;&#x27; + esc(m.name + &#x27; \u2014 &#x27; + m.cond) + &#x27;&lt;\/option&gt;&#x27;;&#10;      }).join(&#x27;&#x27;) + &#x27;&lt;\/optgroup&gt;&#x27;;&#10;    });&#10;    h += &#x27;&lt;option value=&quot;custom&quot;&#x27; + (c.matId === &#x27;custom&#x27; ? &#x27; selected&#x27; : &#x27;&#x27;) + &#x27;&gt;Custom Material (user-defined)&lt;\/option&gt;&#x27;;&#10;    return h;&#10;  }&#10;  function renderCandidates() {&#10;    var cats = []; MATERIALS.forEach(function (m) { if (cats.indexOf(m.cat) &lt; 0) cats.push(m.cat); });&#10;    var canRemove = state.candidates.length &gt; MIN_CANDS;&#10;    q(&#x27;#mscs-cands&#x27;).innerHTML = state.candidates.map(function (c, i) {&#10;      var id = c.uid;&#10;      return &#x27;&lt;div class=&quot;mscs-cand&quot; style=&quot;--mscs-c:&#x27; + MAT_COLORS[i % 6] + &#x27;&quot; data-cand=&quot;&#x27; + id + &#x27;&quot;&gt;&#x27; +&#10;        &#x27;&lt;div class=&quot;mscs-cand-head&quot;&gt;&lt;strong&gt;Material &#x27; + (i + 1) + &#x27;&lt;\/strong&gt;&#x27; +&#10;        &#x27;&lt;button type=&quot;button&quot; class=&quot;mscs-btn mscs-btn-danger mscs-btn-sm&quot; data-act=&quot;removeMat&quot; data-cand=&quot;&#x27; + id + &#x27;&quot;&#x27; + (canRemove ? &#x27;&#x27; : &#x27; disabled title=&quot;At least two materials are required&quot;&#x27;) + &#x27; aria-label=&quot;Remove material &#x27; + (i + 1) + &#x27;&quot;&gt;Remove&lt;\/button&gt;&lt;\/div&gt;&#x27; +&#10;        &#x27;&lt;div class=&quot;mscs-row&quot; style=&quot;gap:8px&quot;&gt;&#x27; +&#10;        &#x27;&lt;div class=&quot;mscs-field&quot; style=&quot;flex:1 1 110px&quot;&gt;&lt;label for=&quot;&#x27; + id + &#x27;-cf&quot;&gt;Category&lt;\/label&gt;&lt;select id=&quot;&#x27; + id + &#x27;-cf&quot; data-cand=&quot;&#x27; + id + &#x27;&quot; data-f=&quot;catFilter&quot;&gt;&lt;option value=&quot;&quot;&gt;All categories&lt;\/option&gt;&#x27; +&#10;        cats.map(function (k) { return &#x27;&lt;option&#x27; + (k === c.catFilter ? &#x27; selected&#x27; : &#x27;&#x27;) + &#x27;&gt;&#x27; + esc(k) + &#x27;&lt;\/option&gt;&#x27;; }).join(&#x27;&#x27;) + &#x27;&lt;\/select&gt;&lt;\/div&gt;&#x27; +&#10;        &#x27;&lt;div class=&quot;mscs-field&quot; style=&quot;flex:1 1 120px&quot;&gt;&lt;label for=&quot;&#x27; + id + &#x27;-fl&quot;&gt;Search&lt;\/label&gt;&lt;input type=&quot;search&quot; id=&quot;&#x27; + id + &#x27;-fl&quot; data-cand=&quot;&#x27; + id + &#x27;&quot; data-f=&quot;filter&quot; value=&quot;&#x27; + esc(c.filter) + &#x27;&quot; placeholder=&quot;e.g. 316, Ti, PEEK&quot;&gt;&lt;\/div&gt;&lt;\/div&gt;&#x27; +&#10;        &#x27;&lt;div class=&quot;mscs-field&quot;&gt;&lt;label for=&quot;&#x27; + id + &#x27;-pk&quot;&gt;Material&lt;\/label&gt;&lt;select id=&quot;&#x27; + id + &#x27;-pk&quot; data-cand=&quot;&#x27; + id + &#x27;&quot; data-f=&quot;pick&quot;&gt;&#x27; + matOptions(c) + &#x27;&lt;\/select&gt;&lt;\/div&gt;&#x27; +&#10;        &#x27;&lt;div class=&quot;mscs-field&quot;&gt;&lt;label for=&quot;&#x27; + id + &#x27;-nm&quot;&gt;Display name&lt;\/label&gt;&lt;input type=&quot;text&quot; id=&quot;&#x27; + id + &#x27;-nm&quot; data-cand=&quot;&#x27; + id + &#x27;&quot; data-f=&quot;name&quot; value=&quot;&#x27; + esc(c.name) + &#x27;&quot;&gt;&lt;\/div&gt;&#x27; +&#10;        &#x27;&lt;div class=&quot;mscs-row&quot; style=&quot;gap:8px&quot;&gt;&lt;div class=&quot;mscs-field&quot; style=&quot;flex:1 1 110px&quot;&gt;&lt;label for=&quot;&#x27; + id + &#x27;-ct&quot;&gt;Material category&lt;\/label&gt;&lt;input type=&quot;text&quot; id=&quot;&#x27; + id + &#x27;-ct&quot; data-cand=&quot;&#x27; + id + &#x27;&quot; data-f=&quot;category&quot; value=&quot;&#x27; + esc(c.category) + &#x27;&quot;&gt;&lt;\/div&gt;&#x27; +&#10;        &#x27;&lt;div class=&quot;mscs-field&quot; style=&quot;flex:1 1 140px&quot;&gt;&lt;label for=&quot;&#x27; + id + &#x27;-gr&quot;&gt;Grade \/ condition&lt;\/label&gt;&lt;input type=&quot;text&quot; id=&quot;&#x27; + id + &#x27;-gr&quot; data-cand=&quot;&#x27; + id + &#x27;&quot; data-f=&quot;grade&quot; value=&quot;&#x27; + esc(c.grade) + &#x27;&quot;&gt;&lt;\/div&gt;&lt;\/div&gt;&#x27; +&#10;        &#x27;&lt;div class=&quot;mscs-field&quot; style=&quot;margin-bottom:0&quot;&gt;&lt;label for=&quot;&#x27; + id + &#x27;-sr&quot;&gt;Reference source \/ notes&lt;\/label&gt;&lt;textarea id=&quot;&#x27; + id + &#x27;-sr&quot; rows=&quot;2&quot; data-cand=&quot;&#x27; + id + &#x27;&quot; data-f=&quot;source&quot;&gt;&#x27; + esc(c.source) + &#x27;&lt;\/textarea&gt;&lt;\/div&gt;&#x27; +&#10;        &#x27;&lt;\/div&gt;&#x27;;&#10;    }).join(&#x27;&#x27;);&#10;    q(&#x27;#mscs-cand-count&#x27;).textContent = state.candidates.length + &#x27; of &#x27; + MAX_CANDS + &#x27; materials&#x27;;&#10;    qa(&#x27;[data-act=&quot;addMat&quot;],[data-act=&quot;addCustom&quot;]&#x27;).forEach(function (b) { b.disabled = state.candidates.length &gt;= MAX_CANDS; });&#10;  }&#10;  function refreshPickOptions(c) {&#10;    var sel = root.querySelector(&#x27;select[data-f=&quot;pick&quot;][data-cand=&quot;&#x27; + c.uid + &#x27;&quot;]&#x27;);&#10;    if (sel) sel.innerHTML = matOptions(c);&#10;  }&#10;&#10;  function dirSelect(cr) {&#10;    return &#x27;&lt;select data-crit=&quot;&#x27; + cr.id + &#x27;&quot; data-f=&quot;dir&quot; aria-label=&quot;Optimization direction for &#x27; + esc(cr.name) + &#x27;&quot;&gt;&#x27; +&#10;      [&#x27;higher&#x27;, &#x27;lower&#x27;, &#x27;target&#x27;].map(function (d) { return &#x27;&lt;option value=&quot;&#x27; + d + &#x27;&quot;&#x27; + (cr.dir === d ? &#x27; selected&#x27; : &#x27;&#x27;) + &#x27;&gt;&#x27; + DIR_LABEL[d] + &#x27;&lt;\/option&gt;&#x27;; }).join(&#x27;&#x27;) + &#x27;&lt;\/select&gt;&#x27;;&#10;  }&#10;  function numIn(cr, f, v, label, disabled) {&#10;    return &#x27;&lt;input type=&quot;number&quot; step=&quot;any&quot; data-crit=&quot;&#x27; + cr.id + &#x27;&quot; data-f=&quot;&#x27; + f + &#x27;&quot; value=&quot;&#x27; + (isNum(v) ? v : &#x27;&#x27;) + &#x27;&quot; aria-label=&quot;&#x27; + esc(label + &#x27; for &#x27; + cr.name) + &#x27;&quot; placeholder=&quot;&#x27; + label + &#x27;&quot;&#x27; + (disabled ? &#x27; disabled&#x27; : &#x27;&#x27;) + &#x27;&gt;&#x27;;&#10;  }&#10;  function renderCriteria() {&#10;    var open = {};&#10;    qa(&#x27;details.mscs-cat&#x27;).forEach(function (d) { open[d.getAttribute(&#x27;data-cat&#x27;)] = d.open; });&#10;    var h = &#x27;&#x27;;&#10;    CAT_KEYS.forEach(function (k) {&#10;      var list = state.criteria.filter(function (c) { return c.cat === k; });&#10;      var nAct = list.filter(function (c) { return c.active; }).length;&#10;      var isOpen = open[k] !== undefined ? open[k] : nAct &gt; 0;&#10;      h += &#x27;&lt;details class=&quot;mscs-cat&quot; data-cat=&quot;&#x27; + k + &#x27;&quot;&#x27; + (isOpen ? &#x27; open&#x27; : &#x27;&#x27;) + &#x27;&gt;&lt;summary&gt;&#x27; + CATS[k] + &#x27; criteria &lt;span class=&quot;mscs-muted&quot; data-catcount=&quot;&#x27; + k + &#x27;&quot;&gt;(&#x27; + nAct + &#x27; active of &#x27; + list.length + &#x27;)&lt;\/span&gt;&lt;\/summary&gt;&#x27; +&#10;        &#x27;&lt;div class=&quot;mscs-table-wrap&quot;&gt;&lt;table class=&quot;mscs-crit-table&quot;&gt;&lt;thead&gt;&lt;tr&gt;&lt;th&gt;Active&lt;\/th&gt;&lt;th&gt;Criterion&lt;\/th&gt;&lt;th&gt;Unit&lt;\/th&gt;&lt;th&gt;Direction&lt;\/th&gt;&lt;th&gt;Target&lt;\/th&gt;&lt;th&gt;Tolerance&lt;\/th&gt;&lt;th&gt;Ref.&lt;\/th&gt;&lt;th&gt;Ref. min&lt;\/th&gt;&lt;th&gt;Ref. max&lt;\/th&gt;&lt;th&gt;Weight (%)&lt;\/th&gt;&lt;th class=&quot;mscs-num&quot;&gt;Normalized&lt;\/th&gt;&lt;th&gt;&lt;\/th&gt;&lt;\/tr&gt;&lt;\/thead&gt;&lt;tbody&gt;&#x27; +&#10;        list.map(function (cr) {&#10;          var tgt = cr.dir === &#x27;target&#x27;;&#10;          return &#x27;&lt;tr data-row=&quot;&#x27; + cr.id + &#x27;&quot; class=&quot;&#x27; + (cr.active ? &#x27;&#x27; : &#x27;mscs-inactive&#x27;) + &#x27;&quot;&gt;&#x27; +&#10;            &#x27;&lt;td&gt;&lt;input type=&quot;checkbox&quot; data-crit=&quot;&#x27; + cr.id + &#x27;&quot; data-f=&quot;active&quot;&#x27; + (cr.active ? &#x27; checked&#x27; : &#x27;&#x27;) + &#x27; aria-label=&quot;Activate &#x27; + esc(cr.name) + &#x27;&quot;&gt;&lt;\/td&gt;&#x27; +&#10;            &#x27;&lt;td&gt;&#x27; + (cr.custom ? &#x27;&lt;input type=&quot;text&quot; style=&quot;min-width:170px&quot; data-crit=&quot;&#x27; + cr.id + &#x27;&quot; data-f=&quot;cname&quot; value=&quot;&#x27; + esc(cr.name) + &#x27;&quot; aria-label=&quot;Custom criterion name&quot;&gt;&#x27; : esc(cr.name)) +&#10;            (cr.kind === &#x27;rating&#x27; ? &#x27; &lt;span class=&quot;mscs-badge mscs-none&quot;&gt;user rating&lt;\/span&gt;&#x27; : cr.kind === &#x27;derived&#x27; ? &#x27; &lt;span class=&quot;mscs-badge mscs-none&quot;&gt;derived&lt;\/span&gt;&#x27; : &#x27;&#x27;) + (cr.custom ? &#x27; &lt;span class=&quot;mscs-badge mscs-none&quot;&gt;custom&lt;\/span&gt;&#x27; : &#x27;&#x27;) + &#x27;&lt;\/td&gt;&#x27; +&#10;            &#x27;&lt;td&gt;&#x27; + (cr.custom &amp;&amp; cr.kind !== &#x27;rating&#x27; ? &#x27;&lt;input type=&quot;text&quot; style=&quot;width:110px&quot; data-crit=&quot;&#x27; + cr.id + &#x27;&quot; data-f=&quot;cunit&quot; value=&quot;&#x27; + esc(cr.unit) + &#x27;&quot; aria-label=&quot;Unit&quot;&gt;&#x27; : esc(cr.unit)) + &#x27;&lt;\/td&gt;&#x27; +&#10;            &#x27;&lt;td&gt;&#x27; + dirSelect(cr) + &#x27;&lt;\/td&gt;&#x27; +&#10;            &#x27;&lt;td&gt;&#x27; + numIn(cr, &#x27;target&#x27;, cr.target, &#x27;Target&#x27;, !tgt) + &#x27;&lt;\/td&gt;&#x27; +&#10;            &#x27;&lt;td&gt;&#x27; + numIn(cr, &#x27;tol&#x27;, cr.tol, &#x27;Tolerance&#x27;, !tgt) + &#x27;&lt;\/td&gt;&#x27; +&#10;            &#x27;&lt;td&gt;&lt;input type=&quot;checkbox&quot; data-crit=&quot;&#x27; + cr.id + &#x27;&quot; data-f=&quot;useRef&quot;&#x27; + (cr.useRef ? &#x27; checked&#x27; : &#x27;&#x27;) + (tgt ? &#x27; disabled&#x27; : &#x27;&#x27;) + &#x27; aria-label=&quot;Use reference min\u2013max for &#x27; + esc(cr.name) + &#x27;&quot;&gt;&lt;\/td&gt;&#x27; +&#10;            &#x27;&lt;td&gt;&#x27; + numIn(cr, &#x27;refMin&#x27;, cr.refMin, &#x27;Min&#x27;, tgt || !cr.useRef) + &#x27;&lt;\/td&gt;&#x27; +&#10;            &#x27;&lt;td&gt;&#x27; + numIn(cr, &#x27;refMax&#x27;, cr.refMax, &#x27;Max&#x27;, tgt || !cr.useRef) + &#x27;&lt;\/td&gt;&#x27; +&#10;            &#x27;&lt;td&gt;&lt;div class=&quot;mscs-wcell&quot;&gt;&lt;input type=&quot;range&quot; min=&quot;0&quot; max=&quot;100&quot; step=&quot;1&quot; data-crit=&quot;&#x27; + cr.id + &#x27;&quot; data-f=&quot;wRange&quot; value=&quot;&#x27; + (isNum(cr.weight) ? cr.weight : 0) + &#x27;&quot; aria-label=&quot;Weight slider for &#x27; + esc(cr.name) + &#x27;&quot;&#x27; + (cr.active ? &#x27;&#x27; : &#x27; disabled&#x27;) + &#x27;&gt;&#x27; +&#10;            &#x27;&lt;input type=&quot;number&quot; min=&quot;0&quot; max=&quot;100&quot; step=&quot;0.5&quot; data-crit=&quot;&#x27; + cr.id + &#x27;&quot; data-f=&quot;wNum&quot; value=&quot;&#x27; + (isNum(cr.weight) ? cr.weight : 0) + &#x27;&quot; aria-label=&quot;Weight percentage for &#x27; + esc(cr.name) + &#x27;&quot;&#x27; + (cr.active ? &#x27;&#x27; : &#x27; disabled&#x27;) + &#x27;&gt;&lt;\/div&gt;&lt;\/td&gt;&#x27; +&#10;            &#x27;&lt;td class=&quot;mscs-num&quot; data-wn=&quot;&#x27; + cr.id + &#x27;&quot;&gt;\u2014&lt;\/td&gt;&#x27; +&#10;            &#x27;&lt;td&gt;&#x27; + (cr.custom ? &#x27;&lt;button type=&quot;button&quot; class=&quot;mscs-btn mscs-btn-danger mscs-btn-sm&quot; data-act=&quot;delCrit&quot; data-crit=&quot;&#x27; + cr.id + &#x27;&quot; aria-label=&quot;Delete criterion &#x27; + esc(cr.name) + &#x27;&quot;&gt;Delete&lt;\/button&gt;&#x27; : &#x27;&#x27;) + &#x27;&lt;\/td&gt;&lt;\/tr&gt;&#x27;;&#10;        }).join(&#x27;&#x27;) + &#x27;&lt;\/tbody&gt;&lt;\/table&gt;&lt;\/div&gt;&lt;\/details&gt;&#x27;;&#10;    });&#10;    q(&#x27;#mscs-crit&#x27;).innerHTML = h;&#10;    updateWeightDisplay();&#10;  }&#10;  function updateWeightDisplay() {&#10;    var active = state.criteria.filter(function (c) { return c.active; });&#10;    var sum = active.reduce(function (a, c) { return a + (isNum(c.weight) &amp;&amp; c.weight &gt; 0 ? c.weight : 0); }, 0);&#10;    state.criteria.forEach(function (c) {&#10;      var el = root.querySelector(&#x27;[data-wn=&quot;&#x27; + c.id + &#x27;&quot;]&#x27;);&#10;      if (el) el.textContent = c.active &amp;&amp; sum &gt; 0 &amp;&amp; isNum(c.weight) ? fmt(100 * Math.max(0, c.weight) \/ sum, 1) + &#x27;%&#x27; : &#x27;\u2014&#x27;;&#10;    });&#10;    CAT_KEYS.forEach(function (k) {&#10;      var el = root.querySelector(&#x27;[data-catcount=&quot;&#x27; + k + &#x27;&quot;]&#x27;); if (!el) return;&#10;      var list = state.criteria.filter(function (c) { return c.cat === k; });&#10;      el.textContent = &#x27;(&#x27; + list.filter(function (c) { return c.active; }).length + &#x27; active of &#x27; + list.length + &#x27;)&#x27;;&#10;    });&#10;    var box = q(&#x27;#mscs-weight-total&#x27;);&#10;    if (!active.length) { box.className = &#x27;mscs-msg mscs-msg-warn&#x27;; box.textContent = &#x27;No criteria are active.&#x27;; return; }&#10;    if (sum &lt;= 0) { box.className = &#x27;mscs-msg mscs-msg-err&#x27;; box.textContent = &#x27;All active weights are zero \u2014 assign a positive weight to at least one criterion.&#x27;; return; }&#10;    var off = Math.abs(sum - 100) &gt; 0.05;&#10;    box.className = &#x27;mscs-msg &#x27; + (off ? &#x27;mscs-msg-warn&#x27; : &#x27;mscs-msg-info&#x27;);&#10;    box.innerHTML = &#x27;&lt;strong&gt;Total weight: &#x27; + fmt(sum, 1) + &#x27;%&lt;\/strong&gt; across &#x27; + active.length + &#x27; active criteria. &#x27; +&#10;      (off ? &#x27;Weights are normalized automatically in the calculation (the \u201cNormalized\u201d column); click &lt;em&gt;Normalize Weights&lt;\/em&gt; to rescale the inputs to exactly 100%.&#x27; : &#x27;Weights total 100%.&#x27;);&#10;  }&#10;&#10;  function renderMatrix() {&#10;    var crits = state.criteria.filter(function (c) { return state.showAll || c.active; });&#10;    var reqIds = state.requirements.map(function (r) { return r.crit; });&#10;    if (!state.showAll) state.criteria.forEach(function (c) { if (!c.active &amp;&amp; reqIds.indexOf(c.id) &gt;= 0 &amp;&amp; crits.indexOf(c) &lt; 0) crits.push(c); });&#10;    var h = &#x27;&lt;thead&gt;&lt;tr&gt;&lt;th&gt;Criterion (unit)&lt;\/th&gt;&#x27; + state.candidates.map(function (c, i) {&#10;      return &#x27;&lt;th&gt;&lt;span style=&quot;display:inline-block;width:10px;height:10px;border-radius:2px;background:&#x27; + MAT_COLORS[i % 6] + &#x27;;margin-right:6px&quot;&gt;&lt;\/span&gt;&#x27; + esc(c.name) + &#x27;&lt;\/th&gt;&#x27;;&#10;    }).join(&#x27;&#x27;) + &#x27;&lt;\/tr&gt;&lt;\/thead&gt;&lt;tbody&gt;&#x27;;&#10;    if (!crits.length) h += &#x27;&lt;tr&gt;&lt;td colspan=&quot;&#x27; + (state.candidates.length + 1) + &#x27;&quot; class=&quot;mscs-muted&quot;&gt;No active criteria. Activate criteria above or tick \u201cShow rows for inactive criteria\u201d.&lt;\/td&gt;&lt;\/tr&gt;&#x27;;&#10;    var lastCat = &#x27;&#x27;;&#10;    crits.sort(function (a, b) { return CAT_KEYS.indexOf(a.cat) - CAT_KEYS.indexOf(b.cat); });&#10;    crits.forEach(function (cr) {&#10;      if (cr.cat !== lastCat) { lastCat = cr.cat; h += &#x27;&lt;tr&gt;&lt;th colspan=&quot;&#x27; + (state.candidates.length + 1) + &#x27;&quot; style=&quot;background:#f7f9fc;color:#1e3a8a&quot;&gt;&#x27; + CATS[cr.cat] + &#x27;&lt;\/th&gt;&lt;\/tr&gt;&#x27;; }&#10;      h += &#x27;&lt;tr&gt;&lt;td&gt;&lt;strong&gt;&#x27; + esc(cr.name) + &#x27;&lt;\/strong&gt;&lt;br&gt;&lt;span class=&quot;mscs-muted&quot;&gt;&#x27; + esc(cr.unit) + (cr.active ? &#x27;&#x27; : &#x27; \u00b7 inactive&#x27;) + (reqIds.indexOf(cr.id) &gt;= 0 ? &#x27; \u00b7 used in requirement&#x27; : &#x27;&#x27;) + &#x27;&lt;\/span&gt;&lt;\/td&gt;&#x27;;&#10;      state.candidates.forEach(function (c) {&#10;        var r = cellRaw(c, cr), lab = esc(cr.name + &#x27; for &#x27; + c.name);&#10;        if (cr.kind === &#x27;derived&#x27;) {&#10;          h += &#x27;&lt;td&gt;&lt;span class=&quot;mscs-derived&quot;&gt;&#x27; + (isNum(r.v) ? fmtVal(r.v) : &#x27;&lt;span class=&quot;mscs-muted&quot;&gt;needs &#x27; + (cr.id === &#x27;specStrength&#x27; ? &#x27;yield&#x27; : &#x27;modulus&#x27;) + &#x27; &amp;amp; density&lt;\/span&gt;&#x27;) + &#x27;&lt;\/span&gt;&#x27; + (r.est ? &#x27; &lt;span class=&quot;mscs-badge mscs-est&quot;&gt;Est.&lt;\/span&gt;&#x27; : &#x27;&#x27;) + &#x27;&lt;\/td&gt;&#x27;;&#10;          return;&#10;        }&#10;        var input;&#10;        if (cr.kind === &#x27;rating&#x27;) {&#10;          input = &#x27;&lt;select data-cand=&quot;&#x27; + c.uid + &#x27;&quot; data-crit=&quot;&#x27; + cr.id + &#x27;&quot; data-f=&quot;val&quot; aria-label=&quot;&#x27; + lab + &#x27;&quot;&gt;&lt;option value=&quot;&quot;&gt;\u2014 not rated \u2014&lt;\/option&gt;&#x27; +&#10;            [1, 2, 3, 4, 5].map(function (n) { return &#x27;&lt;option value=&quot;&#x27; + n + &#x27;&quot;&#x27; + (r.v === n ? &#x27; selected&#x27; : &#x27;&#x27;) + &#x27;&gt;&#x27; + RATING_TXT[n] + &#x27;&lt;\/option&gt;&#x27;; }).join(&#x27;&#x27;) + &#x27;&lt;\/select&gt;&#x27;;&#10;        } else {&#10;          input = &#x27;&lt;input type=&quot;number&quot; step=&quot;any&quot; data-cand=&quot;&#x27; + c.uid + &#x27;&quot; data-crit=&quot;&#x27; + cr.id + &#x27;&quot; data-f=&quot;val&quot; value=&quot;&#x27; + (isNum(r.v) ? r.v : &#x27;&#x27;) + &#x27;&quot; placeholder=&quot;no data&quot; aria-label=&quot;&#x27; + lab + &#x27;&quot;&gt;&#x27;;&#10;        }&#10;        h += &#x27;&lt;td&gt;&lt;div class=&quot;mscs-cell&#x27; + (r.est ? &#x27; mscs-is-est&#x27; : &#x27;&#x27;) + &#x27;&quot;&gt;&#x27; + input +&#10;          &#x27;&lt;div class=&quot;mscs-cell-sub&quot;&gt;&lt;label class=&quot;mscs-inline&quot; style=&quot;font-size:12px&quot;&gt;&lt;input type=&quot;checkbox&quot; data-cand=&quot;&#x27; + c.uid + &#x27;&quot; data-crit=&quot;&#x27; + cr.id + &#x27;&quot; data-f=&quot;est&quot;&#x27; + (r.est ? &#x27; checked&#x27; : &#x27;&#x27;) + &#x27; aria-label=&quot;Mark as estimate: &#x27; + lab + &#x27;&quot;&gt;Est.&lt;\/label&gt;&#x27; +&#10;          &#x27;&lt;input type=&quot;text&quot; data-cand=&quot;&#x27; + c.uid + &#x27;&quot; data-crit=&quot;&#x27; + cr.id + &#x27;&quot; data-f=&quot;note&quot; value=&quot;&#x27; + esc(r.note || &#x27;&#x27;) + &#x27;&quot; placeholder=&quot;source \/ note&quot; aria-label=&quot;Source note: &#x27; + lab + &#x27;&quot;&gt;&lt;\/div&gt;&lt;\/div&gt;&lt;\/td&gt;&#x27;;&#10;      });&#10;      h += &#x27;&lt;\/tr&gt;&#x27;;&#10;    });&#10;    q(&#x27;#mscs-matrix&#x27;).innerHTML = h + &#x27;&lt;\/tbody&gt;&#x27;;&#10;  }&#10;&#10;  function critOptions(sel) {&#10;    return CAT_KEYS.map(function (k) {&#10;      return &#x27;&lt;optgroup label=&quot;&#x27; + CATS[k] + &#x27;&quot;&gt;&#x27; + state.criteria.filter(function (c) { return c.cat === k; }).map(function (c) {&#10;        return &#x27;&lt;option value=&quot;&#x27; + c.id + &#x27;&quot;&#x27; + (c.id === sel ? &#x27; selected&#x27; : &#x27;&#x27;) + &#x27;&gt;&#x27; + esc(c.name) + &#x27; (&#x27; + esc(c.unit) + &#x27;)&lt;\/option&gt;&#x27;;&#10;      }).join(&#x27;&#x27;) + &#x27;&lt;\/optgroup&gt;&#x27;;&#10;    }).join(&#x27;&#x27;);&#10;  }&#10;  function renderReqs() {&#10;    if (!state.requirements.length) { q(&#x27;#mscs-reqs&#x27;).innerHTML = &#x27;&lt;p class=&quot;mscs-muted&quot;&gt;No mandatory requirements defined. Examples: Yield strength \u2265 250 MPa, Density \u2264 3 g\/cm\u00b3, Material cost \u2264 5, Verified max. service temperature \u2265 400 \u00b0C, Weldability \u2265 3, Supply lead time \u2264 6 weeks.&lt;\/p&gt;&#x27;; return; }&#10;    q(&#x27;#mscs-reqs&#x27;).innerHTML = &#x27;&lt;div class=&quot;mscs-table-wrap&quot;&gt;&lt;table&gt;&lt;thead&gt;&lt;tr&gt;&lt;th&gt;Property \/ criterion&lt;\/th&gt;&lt;th&gt;Condition&lt;\/th&gt;&lt;th&gt;Limit&lt;\/th&gt;&lt;th&gt;&lt;\/th&gt;&lt;\/tr&gt;&lt;\/thead&gt;&lt;tbody&gt;&#x27; +&#10;      state.requirements.map(function (rq, i) {&#10;        return &#x27;&lt;tr&gt;&lt;td&gt;&lt;select data-req=&quot;&#x27; + rq.id + &#x27;&quot; data-f=&quot;rcrit&quot; aria-label=&quot;Requirement &#x27; + (i + 1) + &#x27; criterion&quot;&gt;&#x27; + critOptions(rq.crit) + &#x27;&lt;\/select&gt;&lt;\/td&gt;&#x27; +&#10;          &#x27;&lt;td&gt;&lt;select data-req=&quot;&#x27; + rq.id + &#x27;&quot; data-f=&quot;rop&quot; aria-label=&quot;Requirement &#x27; + (i + 1) + &#x27; condition&quot; style=&quot;width:auto&quot;&gt;&lt;option value=&quot;&gt;=&quot;&#x27; + (rq.op === &#x27;&gt;=&#x27; ? &#x27; selected&#x27; : &#x27;&#x27;) + &#x27;&gt;Minimum (\u2265)&lt;\/option&gt;&lt;option value=&quot;&lt;=&quot;&#x27; + (rq.op === &#x27;&lt;=&#x27; ? &#x27; selected&#x27; : &#x27;&#x27;) + &#x27;&gt;Maximum (\u2264)&lt;\/option&gt;&lt;\/select&gt;&lt;\/td&gt;&#x27; +&#10;          &#x27;&lt;td&gt;&lt;input type=&quot;number&quot; step=&quot;any&quot; style=&quot;width:120px&quot; data-req=&quot;&#x27; + rq.id + &#x27;&quot; data-f=&quot;rval&quot; value=&quot;&#x27; + (isNum(rq.value) ? rq.value : &#x27;&#x27;) + &#x27;&quot; aria-label=&quot;Requirement &#x27; + (i + 1) + &#x27; limit&quot;&gt;&lt;\/td&gt;&#x27; +&#10;          &#x27;&lt;td&gt;&lt;button type=&quot;button&quot; class=&quot;mscs-btn mscs-btn-danger mscs-btn-sm&quot; data-act=&quot;delReq&quot; data-req=&quot;&#x27; + rq.id + &#x27;&quot;&gt;Remove&lt;\/button&gt;&lt;\/td&gt;&lt;\/tr&gt;&#x27;;&#10;      }).join(&#x27;&#x27;) + &#x27;&lt;\/tbody&gt;&lt;\/table&gt;&lt;\/div&gt;&#x27;;&#10;  }&#10;&#10;  function renderPresetSelect() {&#10;    var sel = q(&#x27;#mscs-preset&#x27;);&#10;    sel.innerHTML = &#x27;&lt;option value=&quot;&quot;&gt;\u2014 Custom (no preset) \u2014&lt;\/option&gt;&#x27; + Object.keys(PRESETS).map(function (k) { return &#x27;&lt;option value=&quot;&#x27; + k + &#x27;&quot;&#x27; + (state.preset === k ? &#x27; selected&#x27; : &#x27;&#x27;) + &#x27;&gt;&#x27; + PRESETS[k].name + &#x27;&lt;\/option&gt;&#x27;; }).join(&#x27;&#x27;);&#10;    var p = PRESETS[state.preset];&#10;    q(&#x27;#mscs-preset-note&#x27;).innerHTML = p ? &#x27;&lt;strong&gt;&#x27; + p.name + &#x27;&lt;\/strong&gt; preset: &#x27; + Object.keys(p.w).map(function (id) { var c = getCrit(id); return (c ? esc(c.name) : id) + &#x27; &#x27; + p.w[id] + &#x27;%&#x27;; }).join(&#x27;, &#x27;) + &#x27;. Presets are editable starting points, not engineering standards. Criteria without data (for example user ratings you have not entered yet) are handled by the missing-data option.&#x27; : &#x27;Custom configuration \u2014 activate criteria and set weights below.&#x27;;&#10;  }&#10;  function renderGlobals() {&#10;    q(&#x27;#mscs-mode&#x27;).value = state.missingMode;&#10;    var notes = { common: &#x27;Ranks all eligible candidates using only active criteria that have valid data for every candidate; remaining weights are renormalized and excluded criteria are disclosed. Values marked \u201cEst.\u201d are treated as missing.&#x27;,&#10;      complete: &#x27;Only candidates with valid data for every active weighted criterion are ranked; others are listed as excluded. Values marked \u201cEst.\u201d are treated as missing.&#x27;,&#10;      estimate: &#x27;Values marked \u201cEst.\u201d are accepted and flagged in the results; candidates still missing a value are excluded until a value or estimate is entered.&#x27; };&#10;    q(&#x27;#mscs-mode-note&#x27;).textContent = notes[state.missingMode];&#10;    var cb = function (g, v) { var el = root.querySelector(&#x27;[data-g=&quot;&#x27; + g + &#x27;&quot;]&#x27;); if (el) el.checked = !!v; };&#10;    cb(&#x27;excludeFailed&#x27;, state.excludeFailed); cb(&#x27;excludeNE&#x27;, state.excludeNE); cb(&#x27;showAll&#x27;, state.showAll);&#10;  }&#10;  function renderAll() {&#10;    renderPresetSelect(); renderGlobals(); renderCandidates(); renderCriteria(); renderMatrix(); renderReqs(); recompute();&#10;  }&#10;&#10;  \/* =====================================================================&#10;     6. RENDERING \u2014 RESULTS&#10;     ===================================================================== *\/&#10;  var lastR = null;&#10;  function badge(st) {&#10;    var m = { pass: [&#x27;mscs-pass&#x27;, &#x27;Pass&#x27;], fail: [&#x27;mscs-fail&#x27;, &#x27;Fail&#x27;], ne: [&#x27;mscs-ne&#x27;, &#x27;Not Evaluated&#x27;], none: [&#x27;mscs-none&#x27;, &#x27;No requirements&#x27;] }[st];&#10;    return &#x27;&lt;span class=&quot;mscs-badge &#x27; + m[0] + &#x27;&quot;&gt;&#x27; + m[1] + &#x27;&lt;\/span&gt;&#x27;;&#10;  }&#10;  function msgs(R) {&#10;    var h = &#x27;&#x27;;&#10;    if (R.errors.length) h += &#x27;&lt;div class=&quot;mscs-msg mscs-msg-err&quot; role=&quot;alert&quot;&gt;&lt;strong&gt;Please fix the following before ranking:&lt;\/strong&gt;&lt;ul&gt;&#x27; + R.errors.map(function (e) { return &#x27;&lt;li&gt;&#x27; + esc(e) + &#x27;&lt;\/li&gt;&#x27;; }).join(&#x27;&#x27;) + &#x27;&lt;\/ul&gt;&lt;\/div&gt;&#x27;;&#10;    if (R.warnings.length) h += &#x27;&lt;div class=&quot;mscs-msg mscs-msg-warn&quot;&gt;&lt;ul style=&quot;margin:0&quot;&gt;&#x27; + R.warnings.map(function (e) { return &#x27;&lt;li&gt;&#x27; + esc(e) + &#x27;&lt;\/li&gt;&#x27;; }).join(&#x27;&#x27;) + &#x27;&lt;\/ul&gt;&lt;\/div&gt;&#x27;;&#10;    return h;&#10;  }&#10;  function screeningTable(R) {&#10;    if (!state.requirements.length) return &#x27;&#x27;;&#10;    return &#x27;&lt;h3&gt;Mandatory requirement screening&lt;\/h3&gt;&lt;div class=&quot;mscs-table-wrap&quot;&gt;&lt;table&gt;&lt;thead&gt;&lt;tr&gt;&lt;th&gt;Requirement&lt;\/th&gt;&#x27; + state.candidates.map(function (c) { return &#x27;&lt;th&gt;&#x27; + esc(c.name) + &#x27;&lt;\/th&gt;&#x27;; }).join(&#x27;&#x27;) + &#x27;&lt;\/tr&gt;&lt;\/thead&gt;&lt;tbody&gt;&#x27; +&#10;      state.requirements.map(function (rq, i) {&#10;        return &#x27;&lt;tr&gt;&lt;td&gt;&#x27; + esc(reqLabel(rq)) + &#x27;&lt;\/td&gt;&#x27; + state.candidates.map(function (c) {&#10;          var r = R.screen[c.uid].rows[i];&#10;          return &#x27;&lt;td&gt;&#x27; + badge(r.st) + (isNum(r.v) ? &#x27; &lt;span class=&quot;mscs-muted&quot;&gt;&#x27; + fmtVal(r.v) + (r.est ? &#x27; (est.)&#x27; : &#x27;&#x27;) + &#x27;&lt;\/span&gt;&#x27; : r.why ? &#x27; &lt;span class=&quot;mscs-muted&quot;&gt;&#x27; + esc(r.why) + &#x27;&lt;\/span&gt;&#x27; : &#x27;&#x27;) + &#x27;&lt;\/td&gt;&#x27;;&#10;        }).join(&#x27;&#x27;) + &#x27;&lt;\/tr&gt;&#x27;;&#10;      }).join(&#x27;&#x27;) + &#x27;&lt;tr&gt;&lt;td&gt;&lt;strong&gt;Overall&lt;\/strong&gt;&lt;\/td&gt;&#x27; + state.candidates.map(function (c) { return &#x27;&lt;td&gt;&#x27; + badge(R.screen[c.uid].overall) + &#x27;&lt;\/td&gt;&#x27;; }).join(&#x27;&#x27;) + &#x27;&lt;\/tr&gt;&lt;\/tbody&gt;&lt;\/table&gt;&lt;\/div&gt;&#x27;;&#10;  }&#10;  function interpretation(R) {&#10;    var top = R.rows[0], second = R.rows[1], eff = R.effective;&#10;    var nm = esc(top.c.name);&#10;    var strengths = eff.filter(function (cr) { return R.norm[cr.id].scores[top.c.uid] &gt;= 60; })&#10;      .sort(function (a, b) { return top.contrib[b.id] - top.contrib[a.id]; }).slice(0, 4);&#10;    var weak = eff.filter(function (cr) { return R.norm[cr.id].scores[top.c.uid] &lt;= 40; })&#10;      .sort(function (a, b) { return R.weights[b.id] - R.weights[a.id]; });&#10;    var trade = second ? eff.filter(function (cr) { return R.norm[cr.id].scores[second.c.uid] &gt; R.norm[cr.id].scores[top.c.uid] + 1e-9; }) : [];&#10;    var h = &#x27;&lt;div class=&quot;mscs-interp&quot;&gt;&lt;h4&gt;Why &#x27; + nm + &#x27; leads&lt;\/h4&gt;&lt;p&gt;&#x27; + nm + &#x27; has the highest weighted score (&#x27; + fmt(top.total) + &#x27;) under the current criteria, weights and data&#x27; +&#10;      (second ? &#x27;, &#x27; + fmt(R.sens.margin) + &#x27; points ahead of &#x27; + esc(second.c.name) + &#x27; (&#x27; + fmt(second.total) + &#x27;)&#x27; : &#x27;&#x27;) + &#x27;. This ranking is relative to the compared candidates and does not mean the material is safe, certified or suitable for every application.&lt;\/p&gt;&#x27;;&#10;    h += &#x27;&lt;h4&gt;Main strengths&lt;\/h4&gt;&lt;p&gt;&#x27; + (strengths.length ? strengths.map(function (cr) { return esc(cr.name) + &#x27; (score &#x27; + fmt(R.norm[cr.id].scores[top.c.uid], 0) + &#x27;, weight &#x27; + fmt(100 * R.weights[cr.id], 1) + &#x27;%)&#x27;; }).join(&#x27;; &#x27;) : &#x27;No single criterion stands out; the lead comes from a balanced profile.&#x27;) + &#x27;&lt;\/p&gt;&#x27;;&#10;    h += &#x27;&lt;h4&gt;Main trade-offs&lt;\/h4&gt;&lt;p&gt;&#x27; + (trade.length ? &#x27;Compared with &#x27; + esc(second.c.name) + &#x27;, &#x27; + nm + &#x27; scores lower on &#x27; + trade.map(function (cr) { return esc(cr.name); }).join(&#x27;, &#x27;) + &#x27;.&#x27; : second ? nm + &#x27; scores at least as well as &#x27; + esc(second.c.name) + &#x27; on every scored criterion.&#x27; : &#x27;Only one candidate is ranked.&#x27;) + &#x27;&lt;\/p&gt;&#x27;;&#10;    h += &#x27;&lt;h4&gt;Important weaknesses&lt;\/h4&gt;&lt;p&gt;&#x27; + (weak.length ? weak.map(function (cr) { return esc(cr.name) + &#x27; (score &#x27; + fmt(R.norm[cr.id].scores[top.c.uid], 0) + &#x27;)&#x27;; }).join(&#x27;; &#x27;) + &#x27;. Check whether these are acceptable for the application.&#x27; : &#x27;No scored criterion falls at or below 40 for the leading material.&#x27;) + &#x27;&lt;\/p&gt;&#x27;;&#10;    var cmp = R.complete[top.c.uid];&#10;    var missing = cmp.miss.slice();&#10;    h += &#x27;&lt;h4&gt;Missing property data&lt;\/h4&gt;&lt;p&gt;&#x27; + (missing.length ? nm + &#x27; lacks usable data for: &#x27; + missing.map(esc).join(&#x27;, &#x27;) + &#x27;.&#x27; : &#x27;All active criteria have usable data for &#x27; + nm + &#x27;.&#x27;) + (R.dropped.length ? &#x27; Criteria excluded from scoring for all candidates: &#x27; + R.dropped.map(function (c) { return esc(c.name); }).join(&#x27;, &#x27;) + &#x27;.&#x27; : &#x27;&#x27;) +&#10;      (R.mode === &#x27;estimate&#x27; &amp;&amp; eff.some(function (cr) { return valueStatus(top.c, cr, &#x27;estimate&#x27;).est; }) ? &#x27; Some of its scored values are user estimates.&#x27; : &#x27;&#x27;) + &#x27;&lt;\/p&gt;&#x27;;&#10;    var failed = R.excluded.filter(function (x) { return x.kind === &#x27;fail&#x27;; });&#10;    var ne = state.candidates.filter(function (c) { return R.screen[c.uid].overall === &#x27;ne&#x27;; });&#10;    h += &#x27;&lt;h4&gt;Failed requirements&lt;\/h4&gt;&lt;p&gt;&#x27; + (!state.requirements.length ? &#x27;No mandatory requirements were defined.&#x27; : failed.length ? failed.map(function (x) { return esc(x.c.name); }).join(&#x27;, &#x27;) + &#x27; failed at least one mandatory requirement and &#x27; + (failed.length === 1 ? &#x27;is&#x27; : &#x27;are&#x27;) + &#x27; excluded regardless of score.&#x27; : &#x27;No candidate failed a mandatory requirement.&#x27;) +&#10;      (ne.length ? &#x27; Not evaluated (missing data): &#x27; + ne.map(function (c) { return esc(c.name); }).join(&#x27;, &#x27;) + &#x27;.&#x27; : &#x27;&#x27;) + &#x27;&lt;\/p&gt;&#x27;;&#10;    var fl = R.sens.flips, uniq = {};&#10;    fl.forEach(function (f) { uniq[f.cr.id + f.factor] = f; });&#10;    var fls = Object.keys(uniq).map(function (k) { return uniq[k]; });&#10;    h += &#x27;&lt;h4&gt;Sensitivity to criterion weights&lt;\/h4&gt;&lt;p&gt;&#x27; + (!second ? &#x27;Not applicable with one ranked candidate.&#x27; : fls.length ? &#x27;The top choice is &lt;strong&gt;sensitive&lt;\/strong&gt;: &#x27; + fls.map(function (f) { return (f.factor === 0 ? &#x27;removing &#x27; : &#x27;doubling &#x27;) + esc(f.cr.name) + &#x27; makes &#x27; + esc(f.newLeader.name) + &#x27; the leader&#x27;; }).join(&#x27;; &#x27;) + &#x27;. Confirm these weights with the design requirements.&#x27; : &#x27;Removing or doubling any single criterion weight does not change the leader, so the top choice is relatively robust to individual weight changes&#x27; + (R.sens.margin &lt; 3 ? &#x27;, although the lead margin is small (&#x27; + fmt(R.sens.margin) + &#x27; points)&#x27; : &#x27;&#x27;) + &#x27;.&#x27;) + &#x27;&lt;\/p&gt;&lt;\/div&gt;&#x27;;&#10;    return h;&#10;  }&#10;  function detailsPanel(R) {&#10;    var rows = R.rows;&#10;    var h = &#x27;&lt;details class=&quot;mscs-details&quot; id=&quot;mscs-calc-details&quot;&#x27; + (detailsOpen ? &#x27; open&#x27; : &#x27;&#x27;) + &#x27;&gt;&lt;summary&gt;Calculation Details&lt;\/summary&gt;&lt;div class=&quot;mscs-details-body&quot;&gt;&#x27; +&#10;      &#x27;&lt;div class=&quot;mscs-table-wrap&quot;&gt;&lt;table&gt;&lt;thead&gt;&lt;tr&gt;&lt;th&gt;Criterion&lt;\/th&gt;&lt;th&gt;Direction&lt;\/th&gt;&lt;th&gt;Normalization&lt;\/th&gt;&lt;th class=&quot;mscs-num&quot;&gt;Weight&lt;\/th&gt;&#x27; +&#10;      rows.map(function (r) { return &#x27;&lt;th class=&quot;mscs-num&quot;&gt;&#x27; + esc(r.c.name) + &#x27;&lt;br&gt;&lt;span class=&quot;mscs-muted&quot;&gt;value \u2192 score \u2192 points&lt;\/span&gt;&lt;\/th&gt;&#x27;; }).join(&#x27;&#x27;) + &#x27;&lt;\/tr&gt;&lt;\/thead&gt;&lt;tbody&gt;&#x27;;&#10;    R.effective.forEach(function (cr) {&#10;      var n = R.norm[cr.id];&#10;      h += &#x27;&lt;tr&gt;&lt;td&gt;&#x27; + esc(cr.name) + &#x27;&lt;br&gt;&lt;span class=&quot;mscs-muted&quot;&gt;&#x27; + esc(cr.unit) + &#x27;&lt;\/span&gt;&lt;\/td&gt;&lt;td&gt;&#x27; + DIR_LABEL[cr.dir] + &#x27;&lt;\/td&gt;&lt;td&gt;&#x27; + esc(n.method) + (cr.dir !== &#x27;target&#x27; ? &#x27;&lt;br&gt;&lt;span class=&quot;mscs-muted&quot;&gt;min &#x27; + fmtVal(n.min) + &#x27;, max &#x27; + fmtVal(n.max) + &#x27;&lt;\/span&gt;&#x27; : &#x27;&#x27;) + &#x27;&lt;\/td&gt;&lt;td class=&quot;mscs-num&quot;&gt;&#x27; + fmt(100 * n.wn, 2) + &#x27;%&lt;\/td&gt;&#x27; +&#10;        rows.map(function (r) { var st = valueStatus(r.c, cr, R.mode); return &#x27;&lt;td class=&quot;mscs-num&quot;&gt;&#x27; + fmtVal(st.v) + (st.est ? &#x27;*&#x27; : &#x27;&#x27;) + &#x27; \u2192 &#x27; + fmt(n.scores[r.c.uid]) + &#x27; \u2192 &lt;strong&gt;&#x27; + fmt(r.contrib[cr.id]) + &#x27;&lt;\/strong&gt;&lt;\/td&gt;&#x27;; }).join(&#x27;&#x27;) + &#x27;&lt;\/tr&gt;&#x27;;&#10;    });&#10;    h += &#x27;&lt;tr&gt;&lt;td colspan=&quot;4&quot;&gt;&lt;strong&gt;Total score&lt;\/strong&gt;&lt;\/td&gt;&#x27; + rows.map(function (r) { return &#x27;&lt;td class=&quot;mscs-num&quot;&gt;&lt;strong&gt;&#x27; + fmt(r.total) + &#x27;&lt;\/strong&gt;&lt;\/td&gt;&#x27;; }).join(&#x27;&#x27;) + &#x27;&lt;\/tr&gt;&#x27; +&#10;      &#x27;&lt;tr&gt;&lt;td colspan=&quot;4&quot;&gt;&lt;strong&gt;Final rank&lt;\/strong&gt;&lt;\/td&gt;&#x27; + rows.map(function (r) { return &#x27;&lt;td class=&quot;mscs-num&quot;&gt;&lt;strong&gt;#&#x27; + r.rank + &#x27;&lt;\/strong&gt;&lt;\/td&gt;&#x27;; }).join(&#x27;&#x27;) + &#x27;&lt;\/tr&gt;&lt;\/tbody&gt;&lt;\/table&gt;&lt;\/div&gt;&#x27; +&#10;      (R.mode === &#x27;estimate&#x27; ? &#x27;&lt;p class=&quot;mscs-muted&quot;&gt;* = estimate&lt;\/p&gt;&#x27; : &#x27;&#x27;) + &#x27;&lt;\/div&gt;&lt;\/details&gt;&#x27;;&#10;    return h;&#10;  }&#10;  var detailsOpen = false, showExcluded = true;&#10;  function renderResults(R) {&#10;    var h = msgs(R);&#10;    var cands = state.candidates;&#10;    if (R.ok) {&#10;      var top = R.rows[0], sc = R.screen[top.c.uid], cm = R.complete[top.c.uid];&#10;      var tie = R.rows.length &gt; 1 &amp;&amp; R.rows[1].rank === 1;&#10;      h += &#x27;&lt;div class=&quot;mscs-kpis&quot;&gt;&#x27; +&#10;        &#x27;&lt;div class=&quot;mscs-kpi mscs-kpi-top&quot;&gt;&lt;div class=&quot;mscs-kpi-label&quot;&gt;Highest-scoring eligible material&lt;\/div&gt;&lt;div class=&quot;mscs-kpi-val&quot;&gt;&#x27; + esc(top.c.name) + &#x27;&lt;\/div&gt;&lt;div class=&quot;mscs-kpi-sub&quot;&gt;&#x27; + (tie ? &#x27;Tied for first place&#x27; : &#x27;Rank #1 of &#x27; + R.rows.length + &#x27; ranked&#x27;) + &#x27;&lt;\/div&gt;&lt;\/div&gt;&#x27; +&#10;        &#x27;&lt;div class=&quot;mscs-kpi&quot;&gt;&lt;div class=&quot;mscs-kpi-label&quot;&gt;Overall material score&lt;\/div&gt;&lt;div class=&quot;mscs-kpi-val&quot;&gt;&#x27; + fmt(top.total) + &#x27; &lt;span style=&quot;font-size:14px;color:#5b6575&quot;&gt;\/ 100&lt;\/span&gt;&lt;\/div&gt;&lt;\/div&gt;&#x27; +&#10;        &#x27;&lt;div class=&quot;mscs-kpi&quot;&gt;&lt;div class=&quot;mscs-kpi-label&quot;&gt;Evaluation criteria scored&lt;\/div&gt;&lt;div class=&quot;mscs-kpi-val&quot;&gt;&#x27; + R.effective.length + &#x27;&lt;\/div&gt;&lt;div class=&quot;mscs-kpi-sub&quot;&gt;of &#x27; + state.criteria.filter(function (c) { return c.active; }).length + &#x27; active \u00b7 &#x27; + MODE_LABEL[R.mode].split(&#x27; \u2013 &#x27;)[0] + &#x27;&lt;\/div&gt;&lt;\/div&gt;&#x27; +&#10;        &#x27;&lt;div class=&quot;mscs-kpi&quot;&gt;&lt;div class=&quot;mscs-kpi-label&quot;&gt;Requirements passed&lt;\/div&gt;&lt;div class=&quot;mscs-kpi-val&quot;&gt;&#x27; + (sc.total ? sc.passed + &#x27; \/ &#x27; + sc.total : &#x27;\u2014&#x27;) + &#x27;&lt;\/div&gt;&lt;div class=&quot;mscs-kpi-sub&quot;&gt;&#x27; + (sc.total ? &#x27;Leading material&#x27; : &#x27;No requirements defined&#x27;) + &#x27;&lt;\/div&gt;&lt;\/div&gt;&#x27; +&#10;        &#x27;&lt;div class=&quot;mscs-kpi&quot;&gt;&lt;div class=&quot;mscs-kpi-label&quot;&gt;Data completeness&lt;\/div&gt;&lt;div class=&quot;mscs-kpi-val&quot;&gt;&#x27; + fmt(cm.pct, 0) + &#x27;%&lt;\/div&gt;&lt;div class=&quot;mscs-kpi-sub&quot;&gt;&#x27; + cm.k + &#x27; of &#x27; + cm.n + &#x27; active criteria (leader)&lt;\/div&gt;&lt;\/div&gt;&lt;\/div&gt;&#x27;;&#10;      h += &#x27;&lt;h3&gt;Evaluation criteria &amp;amp; normalized weights&lt;\/h3&gt;&lt;div class=&quot;mscs-chips&quot;&gt;&#x27; + R.effective.map(function (cr) { return &#x27;&lt;span class=&quot;mscs-chip&quot;&gt;&#x27; + esc(cr.name) + &#x27; \u2014 &lt;strong&gt;&#x27; + fmt(100 * R.weights[cr.id], 1) + &#x27;%&lt;\/strong&gt;&lt;\/span&gt;&#x27;; }).join(&#x27;&#x27;) +&#10;        R.dropped.map(function (cr) { return &#x27;&lt;span class=&quot;mscs-chip mscs-dropped&quot; title=&quot;Excluded: missing data&quot;&gt;&#x27; + esc(cr.name) + &#x27;&lt;\/span&gt;&#x27;; }).join(&#x27;&#x27;) + &#x27;&lt;\/div&gt;&#x27;;&#10;      h += &#x27;&lt;h3&gt;Material ranking&lt;\/h3&gt;&lt;div class=&quot;mscs-table-wrap&quot;&gt;&lt;table&gt;&lt;thead&gt;&lt;tr&gt;&lt;th&gt;Rank&lt;\/th&gt;&lt;th&gt;Material&lt;\/th&gt;&lt;th class=&quot;mscs-num&quot;&gt;Overall score&lt;\/th&gt;&#x27; + CAT_KEYS.map(function (k) { return &#x27;&lt;th class=&quot;mscs-num&quot;&gt;&#x27; + CATS[k] + &#x27;&lt;\/th&gt;&#x27;; }).join(&#x27;&#x27;) + &#x27;&lt;th&gt;Requirement status&lt;\/th&gt;&lt;th class=&quot;mscs-num&quot;&gt;Data completeness&lt;\/th&gt;&lt;\/tr&gt;&lt;\/thead&gt;&lt;tbody&gt;&#x27; +&#10;        R.rows.map(function (r) {&#10;          var s2 = R.screen[r.c.uid];&#10;          return &#x27;&lt;tr&gt;&lt;td&gt;&lt;strong&gt;#&#x27; + r.rank + &#x27;&lt;\/strong&gt;&lt;\/td&gt;&lt;td&gt;&lt;span style=&quot;display:inline-block;width:10px;height:10px;border-radius:2px;background:&#x27; + candColor(r.c) + &#x27;;margin-right:6px&quot;&gt;&lt;\/span&gt;&#x27; + esc(r.c.name) + &#x27;&lt;br&gt;&lt;span class=&quot;mscs-muted&quot;&gt;&#x27; + esc(r.c.grade) + &#x27;&lt;\/span&gt;&lt;\/td&gt;&lt;td class=&quot;mscs-num&quot;&gt;&lt;strong&gt;&#x27; + fmt(r.total) + &#x27;&lt;\/strong&gt;&lt;\/td&gt;&#x27; +&#10;            CAT_KEYS.map(function (k) { return &#x27;&lt;td class=&quot;mscs-num&quot;&gt;&#x27; + (r.catS[k] === null ? &#x27;N\/A&#x27; : fmt(r.catS[k], 1)) + &#x27;&lt;\/td&gt;&#x27;; }).join(&#x27;&#x27;) +&#10;            &#x27;&lt;td&gt;&#x27; + badge(s2.overall) + (s2.total ? &#x27; &lt;span class=&quot;mscs-muted&quot;&gt;&#x27; + s2.passed + &#x27;\/&#x27; + s2.total + &#x27;&lt;\/span&gt;&#x27; : &#x27;&#x27;) + &#x27;&lt;\/td&gt;&lt;td class=&quot;mscs-num&quot;&gt;&#x27; + fmt(R.complete[r.c.uid].pct, 0) + &#x27;%&lt;\/td&gt;&lt;\/tr&gt;&#x27;;&#10;        }).join(&#x27;&#x27;) + &#x27;&lt;\/tbody&gt;&lt;\/table&gt;&lt;\/div&gt;&#x27;;&#10;    }&#10;    if (R.excluded.length) {&#10;      h += &#x27;&lt;h3&gt;Excluded candidates &lt;button type=&quot;button&quot; class=&quot;mscs-btn mscs-btn-sec mscs-btn-sm&quot; data-act=&quot;toggleExcl&quot;&gt;&#x27; + (showExcluded ? &#x27;Hide&#x27; : &#x27;Show&#x27;) + &#x27; (&#x27; + R.excluded.length + &#x27;)&lt;\/button&gt;&lt;\/h3&gt;&#x27;;&#10;      if (showExcluded) h += &#x27;&lt;div class=&quot;mscs-table-wrap&quot;&gt;&lt;table&gt;&lt;thead&gt;&lt;tr&gt;&lt;th&gt;Material&lt;\/th&gt;&lt;th&gt;Reason&lt;\/th&gt;&lt;th&gt;Requirement status&lt;\/th&gt;&lt;th class=&quot;mscs-num&quot;&gt;Data completeness&lt;\/th&gt;&lt;\/tr&gt;&lt;\/thead&gt;&lt;tbody&gt;&#x27; +&#10;        R.excluded.map(function (x) { return &#x27;&lt;tr&gt;&lt;td&gt;&#x27; + esc(x.c.name) + &#x27;&lt;\/td&gt;&lt;td&gt;&#x27; + esc(x.reason) + &#x27;&lt;\/td&gt;&lt;td&gt;&#x27; + badge(R.screen[x.c.uid].overall) + &#x27;&lt;\/td&gt;&lt;td class=&quot;mscs-num&quot;&gt;&#x27; + fmt(R.complete[x.c.uid].pct, 0) + &#x27;%&lt;\/td&gt;&lt;\/tr&gt;&#x27;; }).join(&#x27;&#x27;) + &#x27;&lt;\/tbody&gt;&lt;\/table&gt;&lt;\/div&gt;&#x27;;&#10;    }&#10;    h += screeningTable(R);&#10;    if (R.ok) h += detailsPanel(R);&#10;    if (!R.ok &amp;&amp; cands.length) {&#10;      h += &#x27;&lt;h3&gt;Data completeness&lt;\/h3&gt;&lt;div class=&quot;mscs-table-wrap&quot;&gt;&lt;table&gt;&lt;thead&gt;&lt;tr&gt;&lt;th&gt;Material&lt;\/th&gt;&lt;th class=&quot;mscs-num&quot;&gt;Completeness&lt;\/th&gt;&lt;th&gt;Missing active criteria&lt;\/th&gt;&lt;\/tr&gt;&lt;\/thead&gt;&lt;tbody&gt;&#x27; +&#10;        cands.map(function (c) { var cm2 = R.complete[c.uid]; return &#x27;&lt;tr&gt;&lt;td&gt;&#x27; + esc(c.name) + &#x27;&lt;\/td&gt;&lt;td class=&quot;mscs-num&quot;&gt;&#x27; + fmt(cm2.pct, 0) + &#x27;%&lt;\/td&gt;&lt;td&gt;&#x27; + (cm2.miss.length ? esc(cm2.miss.join(&#x27;, &#x27;)) : &#x27;\u2014&#x27;) + &#x27;&lt;\/td&gt;&lt;\/tr&gt;&#x27;; }).join(&#x27;&#x27;) + &#x27;&lt;\/tbody&gt;&lt;\/table&gt;&lt;\/div&gt;&#x27;;&#10;    }&#10;    q(&#x27;#mscs-results&#x27;).innerHTML = h;&#10;  }&#10;&#10;  var resultsShown = false;&#10;  function hideResults() { resultsShown = false; q(&#x27;#mscs-results-sec&#x27;).classList.add(&#x27;mscs-hidden&#x27;); }&#10;  function recompute() {&#10;    var R = compute(); lastR = R;&#10;    renderResults(R); updateWeightDisplay();&#10;    return R;&#10;  }&#10;&#10;  \/* =====================================================================&#10;     8. EXPORTS (copy \/ CSV \/ JSON \/ print)&#10;     ===================================================================== *\/&#10;  function textSummary(R) {&#10;    var L = [&#x27;MATERIAL SELECTION COMPARISON &amp; SCORING \u2014 RESULTS&#x27;, &#x27;Generated: &#x27; + new Date().toLocaleString(), &#x27;Missing-data handling: &#x27; + MODE_LABEL[R.mode], &#x27;&#x27;];&#10;    if (state.example) L.push(&#x27;NOTE: Hypothetical example \u2014 fictional data, not real material properties.&#x27;, &#x27;&#x27;);&#10;    if (!R.ok) { L.push(&#x27;No ranking available:&#x27;); R.errors.forEach(function (e) { L.push(&#x27; - &#x27; + e); }); }&#10;    else {&#10;      L.push(&#x27;Criteria and normalized weights:&#x27;);&#10;      R.effective.forEach(function (cr) { L.push(&#x27; - &#x27; + cr.name + &#x27; [&#x27; + cr.unit + &#x27;, &#x27; + DIR_LABEL[cr.dir] + &#x27;]: &#x27; + fmt(100 * R.weights[cr.id], 2) + &#x27;%&#x27;); });&#10;      if (R.dropped.length) L.push(&#x27;Excluded criteria (missing data): &#x27; + R.dropped.map(function (c) { return c.name; }).join(&#x27;, &#x27;));&#10;      L.push(&#x27;&#x27;, &#x27;Ranking:&#x27;);&#10;      R.rows.forEach(function (r) { L.push(&#x27; #&#x27; + r.rank + &#x27; &#x27; + r.c.name + &#x27; \u2014 &#x27; + fmt(r.total) + &#x27; \/ 100  (requirements: &#x27; + R.screen[r.c.uid].overall + &#x27;, completeness &#x27; + fmt(R.complete[r.c.uid].pct, 0) + &#x27;%)&#x27;); });&#10;      L.push(&#x27;&#x27;, &#x27;Highest-scoring eligible material: &#x27; + R.rows[0].c.name + &#x27; (&#x27; + fmt(R.rows[0].total) + &#x27;)&#x27;);&#10;    }&#10;    if (R.excluded.length) { L.push(&#x27;&#x27;, &#x27;Excluded candidates:&#x27;); R.excluded.forEach(function (x) { L.push(&#x27; - &#x27; + x.c.name + &#x27;: &#x27; + x.reason); }); }&#10;    L.push(&#x27;&#x27;, &#x27;Scores are relative to the compared materials, user-defined weights and supplied data. A high score does not establish suitability, certification or code compliance.&#x27;);&#10;    return L.join(&#x27;\\n&#x27;);&#10;  }&#10;  function copyText(t) {&#10;    if (navigator.clipboard &amp;&amp; window.isSecureContext) { navigator.clipboard.writeText(t).then(function () { toast(&#x27;Results copied to clipboard&#x27;); }, function () { fallbackCopy(t); }); }&#10;    else fallbackCopy(t);&#10;  }&#10;  function fallbackCopy(t) {&#10;    var ta = document.createElement(&#x27;textarea&#x27;); ta.value = t; ta.setAttribute(&#x27;readonly&#x27;, &#x27;&#x27;); ta.style.position = &#x27;fixed&#x27;; ta.style.left = &#x27;-9999px&#x27;;&#10;    document.body.appendChild(ta); ta.select();&#10;    try { document.execCommand(&#x27;copy&#x27;); toast(&#x27;Results copied to clipboard&#x27;); } catch (e) { toast(&#x27;Copy failed \u2014 select and copy manually&#x27;); }&#10;    document.body.removeChild(ta);&#10;  }&#10;  function csvCell(v) { v = v == null ? &#x27;&#x27; : String(v); return \/[&quot;,\\n\\r]\/.test(v) ? &#x27;&quot;&#x27; + v.replace(\/&quot;\/g, &#x27;&quot;&quot;&#x27;) + &#x27;&quot;&#x27; : v; }&#10;  function buildCSV(R) {&#10;    var rows = [], c = state.candidates, mode = R.mode;&#10;    rows.push([&#x27;Material Selection Comparison &amp; Scoring Calculator \u2014 export&#x27;, new Date().toISOString()]);&#10;    rows.push([&#x27;Missing-data handling&#x27;, MODE_LABEL[mode]]);&#10;    if (state.example) rows.push([&#x27;Note&#x27;, &#x27;Hypothetical example data \u2014 not real material properties&#x27;]);&#10;    rows.push([]); rows.push([&#x27;MATERIALS&#x27;]); rows.push([&#x27;Material&#x27;, &#x27;Category&#x27;, &#x27;Grade \/ condition&#x27;, &#x27;Reference source&#x27;]);&#10;    c.forEach(function (x) { rows.push([x.name, x.category, x.grade, x.source]); });&#10;    rows.push([]); rows.push([&#x27;INPUT PROPERTIES&#x27;]); rows.push([&#x27;Criterion&#x27;, &#x27;Unit&#x27;].concat(c.map(function (x) { return x.name; })));&#10;    state.criteria.forEach(function (cr) {&#10;      var any = c.some(function (x) { return cellRaw(x, cr).v !== null; });&#10;      if (!any &amp;&amp; !cr.active) return;&#10;      rows.push([cr.name, cr.unit].concat(c.map(function (x) { var r = cellRaw(x, cr); return r.v === null ? &#x27;&#x27; : (isNum(r.v) ? r.v : &#x27;invalid&#x27;) + (r.est ? &#x27; (est.)&#x27; : &#x27;&#x27;) + (r.note &amp;&amp; r.note !== &#x27;Database reference value&#x27; &amp;&amp; r.note !== &#x27;Derived&#x27; ? &#x27; [&#x27; + r.note + &#x27;]&#x27; : &#x27;&#x27;); })));&#10;    });&#10;    rows.push([]); rows.push([&#x27;SELECTED CRITERIA &amp; WEIGHTS&#x27;]); rows.push([&#x27;Criterion&#x27;, &#x27;Category&#x27;, &#x27;Direction&#x27;, &#x27;Normalization&#x27;, &#x27;Input weight (%)&#x27;, &#x27;Normalized weight (%)&#x27;, &#x27;Status&#x27;]);&#10;    state.criteria.filter(function (cr) { return cr.active; }).forEach(function (cr) {&#10;      var used = R.ok &amp;&amp; R.norm[cr.id];&#10;      rows.push([cr.name, CATS[cr.cat], DIR_LABEL[cr.dir], used ? R.norm[cr.id].method : (cr.dir === &#x27;target&#x27; ? &#x27;Target&#x27; : cr.useRef ? &#x27;Reference min\u2013max&#x27; : &#x27;Comparison-set min\u2013max&#x27;), cr.weight, used ? fmt(100 * R.weights[cr.id], 4) : &#x27;0&#x27;, used ? &#x27;Scored&#x27; : R.dropped.indexOf(cr) &gt;= 0 ? &#x27;Excluded (missing data)&#x27; : !(cr.weight &gt; 0) ? &#x27;Zero weight&#x27; : &#x27;Not scored&#x27;]);&#10;    });&#10;    if (R.ok) {&#10;      rows.push([]); rows.push([&#x27;NORMALIZED SCORES (0-100)&#x27;]); rows.push([&#x27;Criterion&#x27;].concat(R.rows.map(function (r) { return r.c.name; })));&#10;      R.effective.forEach(function (cr) { rows.push([cr.name].concat(R.rows.map(function (r) { return fmt(R.norm[cr.id].scores[r.c.uid], 4); }))); });&#10;      rows.push([]); rows.push([&#x27;WEIGHTED CONTRIBUTIONS&#x27;]); rows.push([&#x27;Criterion&#x27;].concat(R.rows.map(function (r) { return r.c.name; })));&#10;      R.effective.forEach(function (cr) { rows.push([cr.name].concat(R.rows.map(function (r) { return fmt(r.contrib[cr.id], 4); }))); });&#10;      rows.push([]); rows.push([&#x27;RANKING&#x27;]); rows.push([&#x27;Rank&#x27;, &#x27;Material&#x27;, &#x27;Total score&#x27;].concat(CAT_KEYS.map(function (k) { return CATS[k] + &#x27; score&#x27;; }), [&#x27;Requirement status&#x27;, &#x27;Data completeness (%)&#x27;]));&#10;      R.rows.forEach(function (r) { rows.push([r.rank, r.c.name, fmt(r.total, 4)].concat(CAT_KEYS.map(function (k) { return r.catS[k] === null ? &#x27;N\/A&#x27; : fmt(r.catS[k], 4); }), [R.screen[r.c.uid].overall, fmt(R.complete[r.c.uid].pct, 1)])); });&#10;    } else { rows.push([]); rows.push([&#x27;RANKING&#x27;, &#x27;Not available: &#x27; + R.errors.join(&#x27; | &#x27;)]); }&#10;    if (R.excluded.length) { rows.push([]); rows.push([&#x27;EXCLUDED CANDIDATES&#x27;]); rows.push([&#x27;Material&#x27;, &#x27;Reason&#x27;]); R.excluded.forEach(function (x) { rows.push([x.c.name, x.reason]); }); }&#10;    rows.push([]); rows.push([&#x27;REQUIREMENT STATUS&#x27;]); rows.push([&#x27;Requirement&#x27;].concat(c.map(function (x) { return x.name; })));&#10;    state.requirements.forEach(function (rq, i) { rows.push([reqLabel(rq)].concat(c.map(function (x) { var r = R.screen[x.uid].rows[i]; return r.st === &#x27;ne&#x27; ? &#x27;Not Evaluated&#x27; : r.st === &#x27;pass&#x27; ? &#x27;Pass&#x27; : &#x27;Fail&#x27;; }))); });&#10;    rows.push([&#x27;Overall&#x27;].concat(c.map(function (x) { var o = R.screen[x.uid].overall; return { pass: &#x27;Pass&#x27;, fail: &#x27;Fail&#x27;, ne: &#x27;Not Evaluated&#x27;, none: &#x27;No requirements&#x27; }[o]; })));&#10;    return &#x27;\ufeff&#x27; + rows.map(function (r) { return r.map(csvCell).join(&#x27;,&#x27;); }).join(&#x27;\\r\\n&#x27;);&#10;  }&#10;  function printReport(R) {&#10;    var css = &#x27;body{font-family:Arial,Helvetica,sans-serif;color:#1f2937;margin:24px;font-size:12.5px}h1{font-size:20px;color:#1e3a8a;margin:0 0 4px}h2{font-size:15px;color:#1e3a8a;border-bottom:1px solid #cbd5e1;padding-bottom:3px;margin-top:18px}table{border-collapse:collapse;width:100%;margin:6px 0}th,td{border:1px solid #cbd5e1;padding:4px 6px;text-align:left;font-size:11.5px;vertical-align:top}th{background:#eef2f8}.n{text-align:right}.note{background:#fff7ed;border:1px solid #fed7aa;padding:8px;border-radius:6px}.mscs-badge{font-weight:bold}.mscs-pass{color:#15803d}.mscs-fail{color:#b91c1c}.mscs-ne{color:#4b5563}svg{max-width:100%;height:auto}.mscs-legend span{margin-right:10px}.mscs-legend i{display:inline-block;width:10px;height:10px;margin-right:4px}.mscs-interp h4{margin:8px 0 2px}.mscs-interp p{margin:0 0 4px}@media print{h2{page-break-after:avoid}table,svg{page-break-inside:avoid}}&#x27;;&#10;    var h = &#x27;&lt;!DOCTYPE html&gt;&lt;html&gt;&lt;head&gt;&lt;meta charset=&quot;utf-8&quot;&gt;&lt;title&gt;Material Selection Report&lt;\/title&gt;&lt;style&gt;&#x27; + css + &#x27;&lt;\/style&gt;&lt;\/head&gt;&lt;body&gt;&#x27;;&#10;    h += &#x27;&lt;h1&gt;Material Selection Comparison &amp;amp; Scoring Report&lt;\/h1&gt;&lt;div&gt;Generated &#x27; + esc(new Date().toLocaleString()) + &#x27; \u00b7 &#x27; + esc(MODE_LABEL[R.mode]) + &#x27;&lt;\/div&gt;&#x27;;&#10;    if (state.example) h += &#x27;&lt;p class=&quot;note&quot;&gt;Hypothetical example \u2014 fictional data, not properties of real material grades.&lt;\/p&gt;&#x27;;&#10;    h += &#x27;&lt;h2&gt;Candidate materials&lt;\/h2&gt;&lt;table&gt;&lt;tr&gt;&lt;th&gt;Material&lt;\/th&gt;&lt;th&gt;Category&lt;\/th&gt;&lt;th&gt;Grade \/ condition&lt;\/th&gt;&lt;th&gt;Source&lt;\/th&gt;&lt;\/tr&gt;&#x27; + state.candidates.map(function (c) { return &#x27;&lt;tr&gt;&lt;td&gt;&#x27; + esc(c.name) + &#x27;&lt;\/td&gt;&lt;td&gt;&#x27; + esc(c.category) + &#x27;&lt;\/td&gt;&lt;td&gt;&#x27; + esc(c.grade) + &#x27;&lt;\/td&gt;&lt;td&gt;&#x27; + esc(c.source) + &#x27;&lt;\/td&gt;&lt;\/tr&gt;&#x27;; }).join(&#x27;&#x27;) + &#x27;&lt;\/table&gt;&#x27;;&#10;    var act = state.criteria.filter(function (c) { return c.active; });&#10;    h += &#x27;&lt;h2&gt;Criterion values&lt;\/h2&gt;&lt;table&gt;&lt;tr&gt;&lt;th&gt;Criterion&lt;\/th&gt;&lt;th&gt;Unit&lt;\/th&gt;&#x27; + state.candidates.map(function (c) { return &#x27;&lt;th&gt;&#x27; + esc(c.name) + &#x27;&lt;\/th&gt;&#x27;; }).join(&#x27;&#x27;) + &#x27;&lt;\/tr&gt;&#x27; +&#10;      act.map(function (cr) { return &#x27;&lt;tr&gt;&lt;td&gt;&#x27; + esc(cr.name) + &#x27;&lt;\/td&gt;&lt;td&gt;&#x27; + esc(cr.unit) + &#x27;&lt;\/td&gt;&#x27; + state.candidates.map(function (c) { var r = cellRaw(c, cr); return &#x27;&lt;td class=&quot;n&quot;&gt;&#x27; + (isNum(r.v) ? fmtVal(r.v) + (r.est ? &#x27; (est.)&#x27; : &#x27;&#x27;) : &#x27;\u2014&#x27;) + &#x27;&lt;\/td&gt;&#x27;; }).join(&#x27;&#x27;) + &#x27;&lt;\/tr&gt;&#x27;; }).join(&#x27;&#x27;) + &#x27;&lt;\/table&gt;&#x27;;&#10;    if (R.ok) {&#10;      h += &#x27;&lt;h2&gt;Criteria &amp;amp; normalized weights&lt;\/h2&gt;&lt;table&gt;&lt;tr&gt;&lt;th&gt;Criterion&lt;\/th&gt;&lt;th&gt;Direction&lt;\/th&gt;&lt;th&gt;Normalization&lt;\/th&gt;&lt;th class=&quot;n&quot;&gt;Weight&lt;\/th&gt;&lt;\/tr&gt;&#x27; + R.effective.map(function (cr) { var n = R.norm[cr.id]; return &#x27;&lt;tr&gt;&lt;td&gt;&#x27; + esc(cr.name) + &#x27;&lt;\/td&gt;&lt;td&gt;&#x27; + DIR_LABEL[cr.dir] + &#x27;&lt;\/td&gt;&lt;td&gt;&#x27; + esc(n.method) + (cr.dir !== &#x27;target&#x27; ? &#x27; (min &#x27; + fmtVal(n.min) + &#x27;, max &#x27; + fmtVal(n.max) + &#x27;)&#x27; : &#x27;&#x27;) + &#x27;&lt;\/td&gt;&lt;td class=&quot;n&quot;&gt;&#x27; + fmt(100 * n.wn, 2) + &#x27;%&lt;\/td&gt;&lt;\/tr&gt;&#x27;; }).join(&#x27;&#x27;) + &#x27;&lt;\/table&gt;&#x27; + (R.dropped.length ? &#x27;&lt;p&gt;Excluded criteria (missing data): &#x27; + R.dropped.map(function (c) { return esc(c.name); }).join(&#x27;, &#x27;) + &#x27;&lt;\/p&gt;&#x27; : &#x27;&#x27;);&#10;      h += &#x27;&lt;h2&gt;Ranking&lt;\/h2&gt;&lt;table&gt;&lt;tr&gt;&lt;th&gt;Rank&lt;\/th&gt;&lt;th&gt;Material&lt;\/th&gt;&lt;th class=&quot;n&quot;&gt;Score&lt;\/th&gt;&#x27; + CAT_KEYS.map(function (k) { return &#x27;&lt;th class=&quot;n&quot;&gt;&#x27; + CATS[k] + &#x27;&lt;\/th&gt;&#x27;; }).join(&#x27;&#x27;) + &#x27;&lt;th&gt;Requirements&lt;\/th&gt;&lt;th class=&quot;n&quot;&gt;Completeness&lt;\/th&gt;&lt;\/tr&gt;&#x27; +&#10;        R.rows.map(function (r) { return &#x27;&lt;tr&gt;&lt;td&gt;#&#x27; + r.rank + &#x27;&lt;\/td&gt;&lt;td&gt;&#x27; + esc(r.c.name) + &#x27;&lt;\/td&gt;&lt;td class=&quot;n&quot;&gt;&lt;b&gt;&#x27; + fmt(r.total) + &#x27;&lt;\/b&gt;&lt;\/td&gt;&#x27; + CAT_KEYS.map(function (k) { return &#x27;&lt;td class=&quot;n&quot;&gt;&#x27; + (r.catS[k] === null ? &#x27;N\/A&#x27; : fmt(r.catS[k], 1)) + &#x27;&lt;\/td&gt;&#x27;; }).join(&#x27;&#x27;) + &#x27;&lt;td&gt;&#x27; + badge(R.screen[r.c.uid].overall) + &#x27;&lt;\/td&gt;&lt;td class=&quot;n&quot;&gt;&#x27; + fmt(R.complete[r.c.uid].pct, 0) + &#x27;%&lt;\/td&gt;&lt;\/tr&gt;&#x27;; }).join(&#x27;&#x27;) + &#x27;&lt;\/table&gt;&#x27;;&#10;      h += &#x27;&lt;h2&gt;Normalized scores &amp;amp; contributions&lt;\/h2&gt;&lt;table&gt;&lt;tr&gt;&lt;th&gt;Criterion&lt;\/th&gt;&#x27; + R.rows.map(function (r) { return &#x27;&lt;th class=&quot;n&quot;&gt;&#x27; + esc(r.c.name) + &#x27;&lt;br&gt;score \/ points&lt;\/th&gt;&#x27;; }).join(&#x27;&#x27;) + &#x27;&lt;\/tr&gt;&#x27; + R.effective.map(function (cr) { return &#x27;&lt;tr&gt;&lt;td&gt;&#x27; + esc(cr.name) + &#x27;&lt;\/td&gt;&#x27; + R.rows.map(function (r) { return &#x27;&lt;td class=&quot;n&quot;&gt;&#x27; + fmt(R.norm[cr.id].scores[r.c.uid]) + &#x27; \/ &#x27; + fmt(r.contrib[cr.id]) + &#x27;&lt;\/td&gt;&#x27;; }).join(&#x27;&#x27;) + &#x27;&lt;\/tr&gt;&#x27;; }).join(&#x27;&#x27;) + &#x27;&lt;\/table&gt;&#x27;;&#10;    } else h += &#x27;&lt;h2&gt;Ranking&lt;\/h2&gt;&lt;p&gt;No ranking available: &#x27; + R.errors.map(esc).join(&#x27; &#x27;) + &#x27;&lt;\/p&gt;&#x27;;&#10;    if (R.excluded.length) h += &#x27;&lt;h2&gt;Excluded candidates&lt;\/h2&gt;&lt;table&gt;&lt;tr&gt;&lt;th&gt;Material&lt;\/th&gt;&lt;th&gt;Reason&lt;\/th&gt;&lt;\/tr&gt;&#x27; + R.excluded.map(function (x) { return &#x27;&lt;tr&gt;&lt;td&gt;&#x27; + esc(x.c.name) + &#x27;&lt;\/td&gt;&lt;td&gt;&#x27; + esc(x.reason) + &#x27;&lt;\/td&gt;&lt;\/tr&gt;&#x27;; }).join(&#x27;&#x27;) + &#x27;&lt;\/table&gt;&#x27;;&#10;    if (state.requirements.length) h += &#x27;&lt;h2&gt;Mandatory requirements&lt;\/h2&gt;&lt;table&gt;&lt;tr&gt;&lt;th&gt;Requirement&lt;\/th&gt;&#x27; + state.candidates.map(function (c) { return &#x27;&lt;th&gt;&#x27; + esc(c.name) + &#x27;&lt;\/th&gt;&#x27;; }).join(&#x27;&#x27;) + &#x27;&lt;\/tr&gt;&#x27; + state.requirements.map(function (rq, i) { return &#x27;&lt;tr&gt;&lt;td&gt;&#x27; + esc(reqLabel(rq)) + &#x27;&lt;\/td&gt;&#x27; + state.candidates.map(function (c) { return &#x27;&lt;td&gt;&#x27; + badge(R.screen[c.uid].rows[i].st) + &#x27;&lt;\/td&gt;&#x27;; }).join(&#x27;&#x27;) + &#x27;&lt;\/tr&gt;&#x27;; }).join(&#x27;&#x27;) + &#x27;&lt;\/table&gt;&#x27;;&#10;    h += &#x27;&lt;\/body&gt;&lt;\/html&gt;&#x27;;&#10;    var w = null;&#10;    try { w = window.open(&#x27;&#x27;, &#x27;_blank&#x27;); } catch (e) { w = null; }&#10;    if (w &amp;&amp; w.document) {&#10;      w.document.open(); w.document.write(h); w.document.close();&#10;      setTimeout(function () { try { w.focus(); w.print(); } catch (e) { \/* user can print manually *\/ } }, 350);&#10;    } else {&#10;      download(&#x27;material-selection-report.html&#x27;, h, &#x27;text\/html;charset=utf-8&#x27;);&#10;      toast(&#x27;Pop-up blocked \u2014 report downloaded as HTML; open it and print.&#x27;);&#10;    }&#10;  }&#10;  function saveJSON() {&#10;    var data = { app: &#x27;mscs-material-selection&#x27;, version: 1, saved: new Date().toISOString(), state: JSON.parse(JSON.stringify(state)) };&#10;    download(&#x27;material-selection-configuration.json&#x27;, JSON.stringify(data, null, 2), &#x27;application\/json&#x27;);&#10;    toast(&#x27;Configuration saved&#x27;);&#10;  }&#10;  function loadJSONText(txt) {&#10;    var d; try { d = JSON.parse(txt); } catch (e) { toast(&#x27;Invalid JSON file&#x27;); return; }&#10;    var s = d &amp;&amp; d.state;&#10;    if (!s || !Array.isArray(s.criteria) || !Array.isArray(s.candidates) || s.candidates.length &lt; 1) { toast(&#x27;File is not a valid calculator configuration&#x27;); return; }&#10;    var base = defaultState();&#10;    \/\/ Merge built-in criteria definitions with saved settings; keep valid custom criteria.&#10;    var crit = [];&#10;    s.criteria.forEach(function (c) {&#10;      if (!c || typeof c.id !== &#x27;string&#x27;) return;&#10;      var def = null; BUILTIN.forEach(function (b) { if (b.id === c.id) def = b; });&#10;      var obj = makeCriterion(def || { id: c.id, name: String(c.name || &#x27;Custom criterion&#x27;), cat: CATS[c.cat] ? c.cat : &#x27;mech&#x27;, unit: String(c.unit || &#x27;&#x27;), kind: c.kind === &#x27;rating&#x27; ? &#x27;rating&#x27; : &#x27;num&#x27;, dir: &#x27;higher&#x27; }, !def);&#10;      obj.active = !!c.active; obj.weight = isNum(c.weight) ? clamp(c.weight, 0, 100) : 0;&#10;      obj.dir = DIR_LABEL[c.dir] ? c.dir : obj.dir; obj.target = isNum(c.target) ? c.target : null; obj.tol = isNum(c.tol) ? c.tol : null;&#10;      obj.useRef = !!c.useRef; obj.refMin = isNum(c.refMin) ? c.refMin : null; obj.refMax = isNum(c.refMax) ? c.refMax : null;&#10;      crit.push(obj);&#10;    });&#10;    BUILTIN.forEach(function (b) { if (!crit.some(function (c) { return c.id === b.id; })) crit.push(makeCriterion(b, false)); });&#10;    base.criteria = crit;&#10;    base.candidates = s.candidates.slice(0, MAX_CANDS).map(function (c) {&#10;      var vals = {};&#10;      if (c.vals &amp;&amp; typeof c.vals === &#x27;object&#x27;) Object.keys(c.vals).forEach(function (k) { var v = c.vals[k]; if (v &amp;&amp; (v.v === null || typeof v.v === &#x27;number&#x27;)) vals[k] = { v: v.v, est: !!v.est, note: String(v.note || &#x27;&#x27;) }; });&#10;      return { uid: uid(&#x27;c&#x27;), matId: String(c.matId || &#x27;custom&#x27;), name: String(c.name || &#x27;Material&#x27;), category: String(c.category || &#x27;&#x27;), grade: String(c.grade || &#x27;&#x27;), source: String(c.source || &#x27;&#x27;), vals: vals, filter: &#x27;&#x27;, catFilter: &#x27;&#x27; };&#10;    });&#10;    while (base.candidates.length &lt; MIN_CANDS) base.candidates.push(makeCandidate(&#x27;custom&#x27;));&#10;    base.requirements = Array.isArray(s.requirements) ? s.requirements.filter(function (r) { return r &amp;&amp; typeof r.crit === &#x27;string&#x27;; }).map(function (r) { return { id: uid(&#x27;r&#x27;), crit: r.crit, op: r.op === &#x27;&lt;=&#x27; ? &#x27;&lt;=&#x27; : &#x27;&gt;=&#x27;, value: isNum(r.value) ? r.value : null }; }) : [];&#10;    base.missingMode = MODE_LABEL[s.missingMode] ? s.missingMode : &#x27;common&#x27;;&#10;    base.excludeFailed = s.excludeFailed !== false; base.excludeNE = !!s.excludeNE; base.showAll = !!s.showAll;&#10;    base.preset = PRESETS[s.preset] ? s.preset : &#x27;&#x27;; base.example = !!s.example;&#10;    base.radarMode = s.radarMode === &#x27;criteria&#x27; ? &#x27;criteria&#x27; : &#x27;category&#x27;;&#10;    state = base; hideResults(); renderAll(); toast(&#x27;Configuration loaded \u2014 click Calculate&#x27;);&#10;  }&#10;&#10;  \/* =====================================================================&#10;     9. ACTIONS &amp; EVENTS&#10;     ===================================================================== *\/&#10;  function loadExample() {&#10;    var s = { criteria: BUILTIN.map(function (d) { return makeCriterion(d, false); }), candidates: [], requirements: [], missingMode: &#x27;complete&#x27;, excludeFailed: true, excludeNE: false, preset: &#x27;&#x27;, showAll: false, radarMode: &#x27;category&#x27;, propChart: &#x27;yield&#x27;, sensCrit: &#x27;&#x27;, example: true };&#10;    var ex = [[&#x27;exStrength&#x27;, &#x27;Strength&#x27;, &#x27;mech&#x27;, 30, [80, 90, 70]], [&#x27;exLight&#x27;, &#x27;Lightweight Performance&#x27;, &#x27;phys&#x27;, 25, [60, 75, 95]], [&#x27;exCorr&#x27;, &#x27;Corrosion Resistance&#x27;, &#x27;env&#x27;, 20, [70, 85, 90]], [&#x27;exMfg&#x27;, &#x27;Manufacturing Ease&#x27;, &#x27;mfg&#x27;, 15, [90, 70, 75]], [&#x27;exCost&#x27;, &#x27;Cost Efficiency&#x27;, &#x27;econ&#x27;, 10, [80, 60, 70]]];&#10;    [&#x27;A&#x27;, &#x27;B&#x27;, &#x27;C&#x27;].forEach(function (L) { s.candidates.push({ uid: uid(&#x27;c&#x27;), matId: &#x27;custom&#x27;, name: &#x27;Material &#x27; + L, category: &#x27;Hypothetical&#x27;, grade: &#x27;Fictional example&#x27;, source: &#x27;Hypothetical example data \u2014 not a real material grade.&#x27;, vals: {}, filter: &#x27;&#x27;, catFilter: &#x27;&#x27; }); });&#10;    ex.forEach(function (e) {&#10;      var c = makeCriterion({ id: e[0], name: e[1], cat: e[2], unit: &#x27;score 0\u2013100&#x27;, kind: &#x27;num&#x27;, dir: &#x27;higher&#x27; }, true);&#10;      c.active = true; c.weight = e[3]; c.useRef = true; c.refMin = 0; c.refMax = 100;&#10;      s.criteria.push(c);&#10;      s.candidates.forEach(function (cd, i) { cd.vals[e[0]] = { v: e[4][i], est: false, note: &#x27;Hypothetical pre-normalized score&#x27; }; });&#10;    });&#10;    s.sensCrit = &#x27;exStrength&#x27;; s.propChart = &#x27;exStrength&#x27;;&#10;    state = s; renderAll();&#10;    hideResults();&#10;    toast(&#x27;Hypothetical example loaded \u2014 click Calculate&#x27;);&#10;  }&#10;  function addCustomCriterion() {&#10;    var name = q(&#x27;#mscs-nc-name&#x27;).value.trim(), unit = q(&#x27;#mscs-nc-unit&#x27;).value.trim(), cat = q(&#x27;#mscs-nc-cat&#x27;).value, kind = q(&#x27;#mscs-nc-kind&#x27;).value, dir = q(&#x27;#mscs-nc-dir&#x27;).value;&#10;    var w = parseNum(q(&#x27;#mscs-nc-w&#x27;).value), t = parseNum(q(&#x27;#mscs-nc-t&#x27;).value), tol = parseNum(q(&#x27;#mscs-nc-tol&#x27;).value), msg = q(&#x27;#mscs-nc-msg&#x27;);&#10;    if (!name) { msg.textContent = &#x27;Enter a criterion name.&#x27;; return; }&#10;    if (!isNum(w) || w &lt; 0 || w &gt; 100) { msg.textContent = &#x27;Weight must be between 0 and 100.&#x27;; return; }&#10;    if (dir === &#x27;target&#x27; &amp;&amp; (!isNum(t) || !isNum(tol) || tol &lt;= 0)) { msg.textContent = &#x27;Closest-to-target needs a target value and a tolerance greater than zero.&#x27;; return; }&#10;    if (tol !== null &amp;&amp; isNum(tol) &amp;&amp; tol &lt;= 0) { msg.textContent = &#x27;Tolerance must be greater than zero.&#x27;; return; }&#10;    var c = makeCriterion({ id: uid(&#x27;x&#x27;), name: name, cat: cat, unit: kind === &#x27;rating&#x27; ? RATING_UNIT : (unit || &#x27;\u2014&#x27;), kind: kind, dir: dir }, true);&#10;    c.active = true; c.weight = w; c.target = isNum(t) ? t : null; c.tol = isNum(tol) ? tol : null;&#10;    state.criteria.push(c); state.preset = &#x27;&#x27;;&#10;    [&#x27;#mscs-nc-name&#x27;, &#x27;#mscs-nc-unit&#x27;, &#x27;#mscs-nc-t&#x27;, &#x27;#mscs-nc-tol&#x27;].forEach(function (s2) { q(s2).value = &#x27;&#x27;; });&#10;    msg.textContent = &#x27;Added \u201c&#x27; + name + &#x27;\u201d. Enter its values in the criterion value matrix.&#x27;;&#10;    renderPresetSelect(); renderCriteria(); renderMatrix(); renderReqs(); recompute();&#10;  }&#10;&#10;  root.addEventListener(&#x27;click&#x27;, function (e) {&#10;    var b = e.target.closest(&#x27;[data-act]&#x27;); if (!b || !root.contains(b) || b.disabled) return;&#10;    lastClickEl = b;&#10;    var act = b.getAttribute(&#x27;data-act&#x27;);&#10;    switch (act) {&#10;      case &#x27;calculate&#x27;: resultsShown = true; q(&#x27;#mscs-results-sec&#x27;).classList.remove(&#x27;mscs-hidden&#x27;); recompute(); q(&#x27;#mscs-results-sec&#x27;).scrollIntoView({ behavior: &#x27;smooth&#x27;, block: &#x27;start&#x27; }); break;&#10;      case &#x27;reset&#x27;: state = defaultState(); detailsOpen = false; hideResults(); renderAll(); toast(&#x27;Calculator reset to default setup&#x27;); break;&#10;      case &#x27;example&#x27;: loadExample(); break;&#10;      case &#x27;copy&#x27;: copyText(textSummary(lastR || compute())); break;&#10;      case &#x27;csv&#x27;: download(&#x27;material-selection-results.csv&#x27;, buildCSV(lastR || compute()), &#x27;text\/csv;charset=utf-8&#x27;); toast(&#x27;CSV exported&#x27;); break;&#10;      case &#x27;print&#x27;: printReport(lastR || compute()); break;&#10;      case &#x27;saveJson&#x27;: saveJSON(); break;&#10;      case &#x27;loadJson&#x27;: q(&#x27;#mscs-json-file&#x27;).click(); break;&#10;      case &#x27;addMat&#x27;: if (state.candidates.length &lt; MAX_CANDS) { state.candidates.push(makeCandidate(&#x27;ss316&#x27;)); state.example = false; renderCandidates(); renderMatrix(); recompute(); } break;&#10;      case &#x27;addCustom&#x27;: if (state.candidates.length &lt; MAX_CANDS) { state.candidates.push(makeCandidate(&#x27;custom&#x27;)); renderCandidates(); renderMatrix(); recompute(); } break;&#10;      case &#x27;removeMat&#x27;: if (state.candidates.length &gt; MIN_CANDS) { state.candidates = state.candidates.filter(function (c) { return c.uid !== b.getAttribute(&#x27;data-cand&#x27;); }); renderCandidates(); renderMatrix(); recompute(); } break;&#10;      case &#x27;normW&#x27;: {&#10;        var act2 = state.criteria.filter(function (c) { return c.active; }), sum = act2.reduce(function (a, c) { return a + (c.weight &gt; 0 ? c.weight : 0); }, 0);&#10;        if (sum &lt;= 0) { toast(&#x27;All active weights are zero \u2014 nothing to normalize&#x27;); updateWeightDisplay(); break; }&#10;        act2.forEach(function (c) { c.weight = Math.round(10000 * Math.max(0, c.weight) \/ sum) \/ 100; });&#10;        renderCriteria(); recompute(); toast(&#x27;Weights normalized to 100%&#x27;); break;&#10;      }&#10;      case &#x27;resetW&#x27;: {&#10;        var a3 = state.criteria.filter(function (c) { return c.active; });&#10;        if (!a3.length) { toast(&#x27;No active criteria&#x27;); break; }&#10;        a3.forEach(function (c) { c.weight = Math.round(10000 \/ a3.length) \/ 100; });&#10;        state.preset = &#x27;&#x27;; renderPresetSelect(); renderCriteria(); recompute(); toast(&#x27;Weights reset to equal values&#x27;); break;&#10;      }&#10;      case &#x27;addCrit&#x27;: addCustomCriterion(); break;&#10;      case &#x27;delCrit&#x27;: {&#10;        var id = b.getAttribute(&#x27;data-crit&#x27;);&#10;        state.criteria = state.criteria.filter(function (c) { return c.id !== id; });&#10;        state.candidates.forEach(function (c) { delete c.vals[id]; });&#10;        state.requirements = state.requirements.filter(function (r) { return r.crit !== id; });&#10;        renderCriteria(); renderMatrix(); renderReqs(); recompute(); break;&#10;      }&#10;      case &#x27;addReq&#x27;: state.requirements.push({ id: uid(&#x27;r&#x27;), crit: &#x27;yield&#x27;, op: &#x27;&gt;=&#x27;, value: null }); renderReqs(); renderMatrix(); recompute(); break;&#10;      case &#x27;delReq&#x27;: state.requirements = state.requirements.filter(function (r) { return r.id !== b.getAttribute(&#x27;data-req&#x27;); }); renderReqs(); renderMatrix(); recompute(); break;&#10;      case &#x27;toggleExcl&#x27;: showExcluded = !showExcluded; renderResults(lastR); break;&#10;    }&#10;  });&#10;&#10;  function handleField(e) {&#10;    var t = e.target, f = t.getAttribute(&#x27;data-f&#x27;), g = t.getAttribute(&#x27;data-g&#x27;), type = e.type;&#10;    if (g) {&#10;      if (g === &#x27;preset&#x27;) { if (t.value) applyPreset(state, t.value); else state.preset = &#x27;&#x27;; state.example = false; renderPresetSelect(); renderCriteria(); renderMatrix(); recompute(); return; }&#10;      if (g === &#x27;missingMode&#x27;) { state.missingMode = t.value; renderGlobals(); recompute(); return; }&#10;      if (g === &#x27;excludeFailed&#x27; || g === &#x27;excludeNE&#x27;) { state[g] = t.checked; recompute(); return; }&#10;      if (g === &#x27;showAll&#x27;) { state.showAll = t.checked; renderMatrix(); return; }&#10;      return;&#10;    }&#10;    if (!f) return;&#10;    var cand = t.getAttribute(&#x27;data-cand&#x27;) ? getCand(t.getAttribute(&#x27;data-cand&#x27;)) : null;&#10;    var crit = t.getAttribute(&#x27;data-crit&#x27;) ? getCrit(t.getAttribute(&#x27;data-crit&#x27;)) : null;&#10;    var req = null; if (t.getAttribute(&#x27;data-req&#x27;)) state.requirements.forEach(function (r) { if (r.id === t.getAttribute(&#x27;data-req&#x27;)) req = r; });&#10;&#10;    \/\/ Candidate card fields&#10;    if (cand &amp;&amp; !crit) {&#10;      if (f === &#x27;filter&#x27;) { cand.filter = t.value; refreshPickOptions(cand); return; }&#10;      if (f === &#x27;catFilter&#x27;) { cand.catFilter = t.value; refreshPickOptions(cand); return; }&#10;      if (f === &#x27;pick&#x27;) {&#10;        if (type !== &#x27;change&#x27; || !t.value) return;&#10;        loadMaterialInto(cand, findMat(t.value) || CUSTOM_MAT); state.example = false;&#10;        renderCandidates(); renderMatrix(); recompute(); return;&#10;      }&#10;      if (f === &#x27;name&#x27; || f === &#x27;grade&#x27; || f === &#x27;source&#x27; || f === &#x27;category&#x27;) {&#10;        cand[f] = t.value;&#10;        if (f === &#x27;name&#x27;) { if (type === &#x27;change&#x27;) renderMatrix(); recompute(); }&#10;        return;&#10;      }&#10;    }&#10;    \/\/ Matrix cells&#10;    if (cand &amp;&amp; crit) {&#10;      var cell = cand.vals[crit.id] || (cand.vals[crit.id] = { v: null, est: false, note: &#x27;&#x27; });&#10;      if (f === &#x27;val&#x27;) { cell.v = parseNum(t.value); if (crit.id === &#x27;yield&#x27; || crit.id === &#x27;modulus&#x27; || crit.id === &#x27;density&#x27;) { if (type === &#x27;change&#x27;) renderMatrix(); } recompute(); return; }&#10;      if (f === &#x27;est&#x27;) { cell.est = t.checked; var box = t.closest(&#x27;.mscs-cell&#x27;); if (box) box.classList.toggle(&#x27;mscs-is-est&#x27;, t.checked); if (crit.id === &#x27;yield&#x27; || crit.id === &#x27;modulus&#x27; || crit.id === &#x27;density&#x27;) renderMatrix(); recompute(); return; }&#10;      if (f === &#x27;note&#x27;) { cell.note = t.value; return; }&#10;    }&#10;    \/\/ Criteria configuration&#10;    if (crit) {&#10;      if (f === &#x27;active&#x27;) {&#10;        crit.active = t.checked; state.preset = &#x27;&#x27;; state.example = false;&#10;        var row = t.closest(&#x27;tr&#x27;); if (row) { row.classList.toggle(&#x27;mscs-inactive&#x27;, !t.checked); qa(&#x27;[data-crit=&quot;&#x27; + crit.id + &#x27;&quot;][data-f=&quot;wRange&quot;],[data-crit=&quot;&#x27; + crit.id + &#x27;&quot;][data-f=&quot;wNum&quot;]&#x27;).forEach(function (x) { x.disabled = !t.checked; }); }&#10;        if (t.checked &amp;&amp; !(crit.weight &gt; 0)) { crit.weight = 10; qa(&#x27;[data-crit=&quot;&#x27; + crit.id + &#x27;&quot;][data-f=&quot;wRange&quot;],[data-crit=&quot;&#x27; + crit.id + &#x27;&quot;][data-f=&quot;wNum&quot;]&#x27;).forEach(function (x) { x.value = 10; }); }&#10;        renderPresetSelect(); renderMatrix(); recompute(); return;&#10;      }&#10;      if (f === &#x27;wRange&#x27; || f === &#x27;wNum&#x27;) {&#10;        var v = parseNum(t.value);&#10;        crit.weight = isNum(v) ? v : 0;&#10;        var other = root.querySelector(&#x27;[data-crit=&quot;&#x27; + crit.id + &#x27;&quot;][data-f=&quot;&#x27; + (f === &#x27;wRange&#x27; ? &#x27;wNum&#x27; : &#x27;wRange&#x27;) + &#x27;&quot;]&#x27;);&#10;        if (other) other.value = isNum(v) ? v : 0;&#10;        if (state.preset) { state.preset = &#x27;&#x27;; renderPresetSelect(); }&#10;        recompute(); return;&#10;      }&#10;      if (f === &#x27;dir&#x27;) {&#10;        crit.dir = t.value; var tg = crit.dir === &#x27;target&#x27;;&#10;        qa(&#x27;[data-crit=&quot;&#x27; + crit.id + &#x27;&quot;][data-f=&quot;target&quot;],[data-crit=&quot;&#x27; + crit.id + &#x27;&quot;][data-f=&quot;tol&quot;]&#x27;).forEach(function (x) { x.disabled = !tg; });&#10;        qa(&#x27;[data-crit=&quot;&#x27; + crit.id + &#x27;&quot;][data-f=&quot;useRef&quot;]&#x27;).forEach(function (x) { x.disabled = tg; });&#10;        qa(&#x27;[data-crit=&quot;&#x27; + crit.id + &#x27;&quot;][data-f=&quot;refMin&quot;],[data-crit=&quot;&#x27; + crit.id + &#x27;&quot;][data-f=&quot;refMax&quot;]&#x27;).forEach(function (x) { x.disabled = tg || !crit.useRef; });&#10;        recompute(); return;&#10;      }&#10;      if (f === &#x27;useRef&#x27;) { crit.useRef = t.checked; qa(&#x27;[data-crit=&quot;&#x27; + crit.id + &#x27;&quot;][data-f=&quot;refMin&quot;],[data-crit=&quot;&#x27; + crit.id + &#x27;&quot;][data-f=&quot;refMax&quot;]&#x27;).forEach(function (x) { x.disabled = !t.checked; }); recompute(); return; }&#10;      if (f === &#x27;target&#x27; || f === &#x27;tol&#x27; || f === &#x27;refMin&#x27; || f === &#x27;refMax&#x27;) { crit[f] = parseNum(t.value); recompute(); return; }&#10;      if (f === &#x27;cname&#x27;) { crit.name = t.value || &#x27;Custom criterion&#x27;; if (type === &#x27;change&#x27;) { renderMatrix(); renderReqs(); } recompute(); return; }&#10;      if (f === &#x27;cunit&#x27;) { crit.unit = t.value; if (type === &#x27;change&#x27;) renderMatrix(); return; }&#10;    }&#10;    \/\/ Requirements&#10;    if (req) {&#10;      if (f === &#x27;rcrit&#x27;) { req.crit = t.value; renderMatrix(); recompute(); return; }&#10;      if (f === &#x27;rop&#x27;) { req.op = t.value; recompute(); return; }&#10;      if (f === &#x27;rval&#x27;) { req.value = parseNum(t.value); recompute(); return; }&#10;    }&#10;  }&#10;  root.addEventListener(&#x27;input&#x27;, function (e) {&#10;    var t = e.target;&#10;    \/\/ selects \/ checkboxes are handled on change to avoid double work&#10;    if (t.tagName === &#x27;SELECT&#x27; || t.type === &#x27;checkbox&#x27; || t.type === &#x27;file&#x27;) return;&#10;    handleField(e);&#10;  });&#10;  root.addEventListener(&#x27;change&#x27;, function (e) {&#10;    var t = e.target;&#10;    if (t.id === &#x27;mscs-json-file&#x27;) {&#10;      var file = t.files &amp;&amp; t.files[0]; if (!file) return;&#10;      var rd = new FileReader(); rd.onload = function () { loadJSONText(String(rd.result)); t.value = &#x27;&#x27;; }; rd.readAsText(file); return;&#10;    }&#10;    if (t.tagName === &#x27;SELECT&#x27; || t.type === &#x27;checkbox&#x27;) { handleField(e); return; }&#10;    \/\/ text\/number change: refresh labels that depend on committed values&#10;    var f = t.getAttribute(&#x27;data-f&#x27;);&#10;    if (f === &#x27;name&#x27; || f === &#x27;cname&#x27; || f === &#x27;cunit&#x27; || (f === &#x27;val&#x27; &amp;&amp; \/^(yield|modulus|density)$\/.test(t.getAttribute(&#x27;data-crit&#x27;) || &#x27;&#x27;))) handleField(e);&#10;  });&#10;  root.addEventListener(&#x27;toggle&#x27;, function (e) { if (e.target &amp;&amp; e.target.id === &#x27;mscs-calc-details&#x27;) detailsOpen = e.target.open; }, true);&#10;&#10;  \/* =====================================================================&#10;     10. INIT&#10;     ===================================================================== *\/&#10;  q(&#x27;#mscs-nc-cat&#x27;).innerHTML = CAT_KEYS.map(function (k) { return &#x27;&lt;option value=&quot;&#x27; + k + &#x27;&quot;&gt;&#x27; + CATS[k] + &#x27;&lt;\/option&gt;&#x27;; }).join(&#x27;&#x27;);&#10;  state = defaultState();&#10;  renderAll();&#10;})();&#10;&lt;\/script&gt;&#10;&lt;\/div&gt;&lt;!-- \/.mscs-root --&gt;&#10;&#10;&lt;script&gt;&#10;(function () {&#10;  var f = null;&#10;  try { f = window.frameElement; } catch (e) { f = null; }&#10;  if (!f) return;&#10;  var root = document.getElementById(&#x27;mscs-app&#x27;);&#10;  function fit() {&#10;    var h = Math.ceil((root || document.body).getBoundingClientRect().height);&#10;    if (h &gt; 0) f.style.setProperty(&#x27;height&#x27;, h + &#x27;px&#x27;, &#x27;important&#x27;);&#10;  }&#10;  fit();&#10;  if (window.ResizeObserver) new ResizeObserver(fit).observe(root || document.body);&#10;  window.addEventListener(&#x27;load&#x27;, fit);&#10;  window.addEventListener(&#x27;resize&#x27;, fit);&#10;  setTimeout(fit, 300);&#10;})();&#10;&lt;\/script&gt;&#10;&lt;\/body&gt;&#10;&lt;\/html&gt;&#10;\"><\/iframe>\n","protected":false},"excerpt":{"rendered":"<p>The Material Selection Comparison &amp; Scoring Calculator is a powerful online tool designed to help engineers, manufacturers, and material specialists compare and evaluate engineering materials&#8230;<\/p>","protected":false},"author":2,"featured_media":25003,"comment_status":"open","ping_status":"open","sticky":false,"template":"","format":"standard","meta":{"_acf_changed":false,"pmpro_default_level":"","_jetpack_newsletter_access":"","_jetpack_dont_email_post_to_subs":false,"_jetpack_newsletter_tier_id":0,"_jetpack_memberships_contains_paywalled_content":false,"_jetpack_feature_clip_id":0,"_jetpack_memberships_contains_paid_content":false,"footnotes":"","jetpack_post_was_ever_published":false},"categories":[8647],"tags":[8806,8679,8509,8805],"class_list":["post-25002","post","type-post","status-publish","format-standard","has-post-thumbnail","hentry","category-materials-science-and-properties","tag-engineering-materials","tag-material-comparison","tag-material-selection","tag-weighted-scoring","pmpro-has-access"],"acf":[],"jetpack_sharing_enabled":true,"jetpack_featured_media_url":"https:\/\/www.mechstream.com\/wp-content\/uploads\/2026\/10\/Material-Selection-Comparison-Scoring-Calculator.png","_links":{"self":[{"href":"https:\/\/www.mechstream.com\/pt\/wp-json\/wp\/v2\/posts\/25002","targetHints":{"allow":["GET"]}}],"collection":[{"href":"https:\/\/www.mechstream.com\/pt\/wp-json\/wp\/v2\/posts"}],"about":[{"href":"https:\/\/www.mechstream.com\/pt\/wp-json\/wp\/v2\/types\/post"}],"author":[{"embeddable":true,"href":"https:\/\/www.mechstream.com\/pt\/wp-json\/wp\/v2\/users\/2"}],"replies":[{"embeddable":true,"href":"https:\/\/www.mechstream.com\/pt\/wp-json\/wp\/v2\/comments?post=25002"}],"version-history":[{"count":1,"href":"https:\/\/www.mechstream.com\/pt\/wp-json\/wp\/v2\/posts\/25002\/revisions"}],"predecessor-version":[{"id":25004,"href":"https:\/\/www.mechstream.com\/pt\/wp-json\/wp\/v2\/posts\/25002\/revisions\/25004"}],"wp:featuredmedia":[{"embeddable":true,"href":"https:\/\/www.mechstream.com\/pt\/wp-json\/wp\/v2\/media\/25003"}],"wp:attachment":[{"href":"https:\/\/www.mechstream.com\/pt\/wp-json\/wp\/v2\/media?parent=25002"}],"wp:term":[{"taxonomy":"category","embeddable":true,"href":"https:\/\/www.mechstream.com\/pt\/wp-json\/wp\/v2\/categories?post=25002"},{"taxonomy":"post_tag","embeddable":true,"href":"https:\/\/www.mechstream.com\/pt\/wp-json\/wp\/v2\/tags?post=25002"}],"curies":[{"name":"wp","href":"https:\/\/api.w.org\/{rel}","templated":true}]}}