{"id":49057,"date":"2026-06-10T13:57:33","date_gmt":"2026-06-10T13:57:33","guid":{"rendered":"https:\/\/hollandmechanics.com\/?page_id=49057"},"modified":"2026-10-13T09:58:40","modified_gmt":"2026-10-13T09:58:40","slug":"wheel-spoke-calculator","status":"publish","type":"page","link":"https:\/\/hollandmechanics.com\/zh\/wheel-spoke-calculator\/","title":{"rendered":"Wheel &amp; Spoke Calculator"},"content":{"rendered":"\t\t<div data-elementor-type=\"wp-page\" data-elementor-id=\"49057\" class=\"elementor elementor-49057\" data-elementor-post-type=\"page\">\n\t\t\t\t<div class=\"elementor-element elementor-element-d3f3ce5 e-flex e-con-boxed e-con e-parent\" data-id=\"d3f3ce5\" data-element_type=\"container\" data-e-type=\"container\">\n\t\t\t\t\t<div class=\"e-con-inner\">\n\t\t<div class=\"elementor-element elementor-element-d610ccc e-con-full e-flex e-con e-child\" data-id=\"d610ccc\" data-element_type=\"container\" data-e-type=\"container\">\n\t\t<div class=\"elementor-element elementor-element-fca3a7d e-flex e-con-boxed e-con e-child\" data-id=\"fca3a7d\" data-element_type=\"container\" data-e-type=\"container\">\n\t\t\t\t\t<div class=\"e-con-inner\">\n\t\t\t\t<div class=\"elementor-element elementor-element-51bd97c elementor-widget__width-inherit elementor-widget elementor-widget-html\" data-id=\"51bd97c\" data-element_type=\"widget\" data-e-type=\"widget\" data-widget_type=\"html.default\">\n\t\t\t\t\t<!-- Holland Mechanics spoke calculator: paste into ONE Elementor HTML widget (one per page).\n     Full screen width by default; add data-boxed to <div id=\"hmsc-app\"> to keep it inside the column. -->\n<!--noptimize-->\n<link rel=\"preconnect\" href=\"https:\/\/fonts.googleapis.com\">\n<link rel=\"preconnect\" href=\"https:\/\/fonts.gstatic.com\" crossorigin>\n<link href=\"https:\/\/fonts.googleapis.com\/css2?family=Inter:opsz,wght@14..32,400..800&display=swap\" rel=\"stylesheet\">\n<style>\n\/* theme \/ Elementor guard: back to browser defaults, the calculator's own rules apply on top *\/\n#hmsc-app :where(button,input,select,textarea,h1,h2,h3,h4,h5,h6,p,a,label,table,thead,tbody,tr,th,td,ul,ol,li,summary,details,small,b,strong,em,i,header,footer,nav,fieldset,legend,output){all:revert}\n#hmsc-app :where(img,svg,canvas){max-width:none;border:0;border-radius:0;box-shadow:none}\n#hmsc-app :where(img){height:auto}\n#hmsc-app{\n  --primary:#084490;\n  --primary-soft:#B9CCE3;\n  --canvas:#EBF0F4;\n  --card:#FFFFFF;\n  --chip:#EDF2F6;\n  --chip-press:#E2EAF1;\n  --line:#D4DEE8;\n  --field:#C6D3DF;\n  --ok:#119915;\n  --ok-bg:#E0F1E1;\n  --ok-ink:#0E7A12;\n  --danger:#FA1414;\n  --danger-bg:#FDECEC;\n  --danger-ink:#B31111;\n  --side-b:#0065E3;      \n  --ds:#D9530A;            \n  --sa:#084490; --sb:#4F8FE6; --sd:#D9530A; --sc:#F5A23F;   \n  --hl:#D4237A;            \n  --muted:rgba(8,68,144,.62);\n  --faint:rgba(8,68,144,.42);\n  --radius-card:14px;\n  --radius-tile:10px;\n  --shadow-card:0 1px 3px rgba(8,68,144,.06),0 4px 14px rgba(8,68,144,.05);\n  --sans:\"Inter\",-apple-system,BlinkMacSystemFont,\"Segoe UI\",Roboto,sans-serif;\n}\n#hmsc-app,#hmsc-app *{box-sizing:border-box}\n#hmsc-app{scroll-behavior:smooth;-webkit-text-size-adjust:100%;text-size-adjust:100%}\n#hmsc-app button,#hmsc-app select,#hmsc-app input[type=checkbox]{touch-action:manipulation}\n@media(max-width:700px){#hmsc-app input,#hmsc-app select,#hmsc-app textarea{font-size:16px!important}}\n#hmsc-app{\n  margin:0;font-family:var(--sans);color:var(--primary);\n  background:var(--canvas);\n  font-size:15px;line-height:1.5;-webkit-font-smoothing:antialiased;\n}\n@media(prefers-reduced-motion:reduce){#hmsc-app,#hmsc-app *,#hmsc-app *::before,#hmsc-app *::after{animation:none!important;transition:none!important}}\n#hmsc-app button:focus-visible,#hmsc-app summary:focus-visible,#hmsc-app a:focus-visible{\n  outline:2px solid var(--side-b);outline-offset:2px\n}\n#hmsc-app .wrap{position:relative;z-index:1;max-width:1180px;margin:0 auto;padding:0 20px 80px}\n#hmsc-app header.top{\n  background:var(--card);\n  border-bottom:1px solid var(--line);\n}\n#hmsc-app .top-inner{max-width:1180px;margin:0 auto;padding:20px 20px;\n  display:flex;align-items:center;justify-content:space-between;gap:24px;flex-wrap:wrap}\n#hmsc-app .brand{display:flex;align-items:center;gap:22px;min-width:0}\n#hmsc-app .brand img{height:34px;width:auto;display:block;flex:none}\n#hmsc-app .brand-text{border-left:1px solid var(--line);padding-left:22px;min-width:0}\n#hmsc-app .brand h1{margin:0;color:var(--primary);font-weight:800;font-size:22px;letter-spacing:-.01em;line-height:1.2;text-wrap:balance}\n#hmsc-app .brand .sub{color:var(--muted);font-size:13px;margin-top:3px;max-width:560px}\n#hmsc-app .top-tag{display:flex;flex-direction:column;align-items:flex-end;gap:6px;margin-left:auto}\n#hmsc-app .top-chip{background:var(--chip);color:var(--primary);border-radius:8px;padding:5px 12px;\n  font-size:12px;font-weight:600;white-space:nowrap}\n@media(max-width:820px){#hmsc-app .top-tag{display:none}\n#hmsc-app .brand{gap:16px}\n#hmsc-app .brand-text{padding-left:16px}}\n#hmsc-app .tabs{display:inline-flex;background:var(--card);border:1px solid var(--field);border-radius:12px;\n  margin:26px 0 22px;overflow:hidden}\n#hmsc-app .tab{\n  appearance:none;border:0;background:var(--card);color:var(--primary);\n  font-family:var(--sans);font-weight:700;font-size:14.5px;\n  padding:12px 24px;cursor:pointer;position:relative;\n  display:inline-flex;align-items:center;justify-content:center;gap:9px;transition:background .15s;\n}\n#hmsc-app .tab svg{width:19px;height:19px;flex:none}\n#hmsc-app .tab+.tab{border-left:1px solid var(--line)}\n#hmsc-app .tab[aria-selected=\"true\"]{background:var(--primary);color:#fff}\n#hmsc-app .tab:not([aria-selected=\"true\"]):hover{background:var(--chip)}\n@media(max-width:700px){#hmsc-app .tabs{display:flex}\n#hmsc-app .tab{flex:1;padding:12px 8px;font-size:13px}\n#hmsc-app .tab svg{display:none}}\n#hmsc-app .panel{display:none}\n#hmsc-app .panel.active{display:block;animation:fade .25s ease}\n@keyframes fade{from{opacity:0;transform:translateY(6px)}to{opacity:1;transform:none}}\n#hmsc-app .grid{display:grid;grid-template-columns:1fr 1fr;gap:18px;align-items:start}\n@media(max-width:820px){#hmsc-app .grid{grid-template-columns:1fr}}\n#hmsc-app .card{\n  background:var(--card);border-radius:var(--radius-card);padding:20px 20px 22px;\n  box-shadow:var(--shadow-card);position:relative;\n}\n#hmsc-app .card.span2{grid-column:1\/-1}\n#hmsc-app .card h2{\n  margin:0 0 4px;font-size:16.5px;font-weight:700;color:var(--primary);\n  display:flex;align-items:center;gap:10px;justify-content:space-between\n}\n#hmsc-app .card .note{color:var(--muted);font-size:13px;margin:0 0 15px;font-weight:400}\n#hmsc-app .seg{display:inline-flex;background:var(--card);border:1px solid var(--field);border-radius:var(--radius-tile);overflow:hidden;flex:none}\n#hmsc-app .seg button{\n  appearance:none;border:0;background:transparent;font-family:var(--sans);font-weight:700;font-size:12.5px;\n  color:var(--primary);padding:8px 14px;cursor:pointer;transition:background .12s\n}\n#hmsc-app .seg button+button{border-left:1px solid var(--line)}\n#hmsc-app .seg button[aria-pressed=\"true\"]{background:var(--primary);color:#fff}\n#hmsc-app .seg button:not([aria-pressed=\"true\"]):not(.is-off):hover{background:var(--chip)}\n#hmsc-app .seg button.is-off{opacity:.4;cursor:not-allowed}\n#hmsc-app .chk-box{margin:0 0 16px}\n#hmsc-app .chk-box .ten-warn{margin:0 0 8px}\n#hmsc-app .fields{display:grid;grid-template-columns:1fr 1fr;gap:13px 16px}\n#hmsc-app .fields.one{grid-template-columns:1fr}\n#hmsc-app .field{display:flex;flex-direction:column;gap:6px;min-width:0}\n#hmsc-app .field.full{grid-column:1\/-1}\n#hmsc-app .field>.inp,#hmsc-app .field>input,#hmsc-app .field>select{margin-top:auto}\n#hmsc-app .field label{font-size:12.5px;font-weight:600;color:var(--primary);display:flex;align-items:center;gap:6px;flex-wrap:wrap}\n#hmsc-app .field label .u{color:var(--faint);font-weight:500;font-size:11px;white-space:nowrap}\n#hmsc-app .tip{\n  width:15px;height:15px;border-radius:50%;background:var(--chip);color:var(--primary);\n  font-size:10px;font-weight:700;display:inline-grid;place-items:center;cursor:help;flex:none\n}\n#hmsc-app .field input,#hmsc-app .field select{\n  font-family:var(--sans);font-size:14.5px;font-weight:600;color:var(--primary);background:var(--card);\n  border:1px solid var(--field);border-radius:var(--radius-tile);padding:10px 12px;width:100%;transition:border-color .12s,box-shadow .12s;min-width:0\n}\n#hmsc-app .field input:focus,#hmsc-app .field select:focus{outline:0;border-color:var(--primary);box-shadow:0 0 0 3px rgba(8,68,144,.12)}\n#hmsc-app .field input::placeholder{color:var(--faint);font-weight:400}\n#hmsc-app .inp{display:flex;align-items:center;background:var(--card);border:1px solid var(--field);border-radius:var(--radius-tile);\n  transition:border-color .12s,box-shadow .12s;min-width:0}\n#hmsc-app .inp:focus-within{border-color:var(--primary);box-shadow:0 0 0 3px rgba(8,68,144,.12)}\n#hmsc-app .inp input{border:none!important;box-shadow:none!important;background:transparent;flex:1;min-width:0;\n  font-family:var(--sans);font-size:14.5px;font-weight:600;color:var(--primary);padding:10px 12px;\n  font-variant-numeric:tabular-nums}\n#hmsc-app .inp input:focus{outline:0;border:none;box-shadow:none!important}\n#hmsc-app .inp em{font-style:normal;font-size:12.5px;font-weight:600;color:var(--faint);padding-right:12px;flex:none}\n#hmsc-app .chk{display:flex;align-items:center;gap:9px;font-size:13px;color:var(--primary);margin-top:4px}\n#hmsc-app .chk input{width:17px;height:17px;accent-color:var(--primary)}\n#hmsc-app .choices{display:flex;flex-wrap:wrap;gap:6px}\n#hmsc-app .choices button{\n  appearance:none;border:0;background:var(--chip);color:var(--primary);\n  font-family:var(--sans);font-size:13px;font-weight:600;padding:9px 0;border-radius:var(--radius-tile);cursor:pointer;\n  flex:1 1 0;min-width:42px;transition:background .12s;font-variant-numeric:tabular-nums\n}\n#hmsc-app .choices.cross button{flex:1 1 60px}\n#hmsc-app .lp-view svg .hl-t.hl{fill:var(--hl)!important}\n#hmsc-app .lp-view svg .hl-s.hl{stroke:var(--hl)!important}\n#hmsc-app .lp-view svg .hl-f.hl{fill:var(--hl)!important}\n#hmsc-app .lp-view svg .hl-d.hl{stroke:var(--hl)!important;stroke-width:2}\n#hmsc-app .lp-stat.hl{box-shadow:inset 0 0 0 2px var(--hl)}\n#hmsc-app .w3-card h2{flex-wrap:wrap}\n#hmsc-app .w3-stage{position:relative;height:460px;border-radius:var(--radius-tile);overflow:hidden;background:#F6F9FB;border:1px solid var(--line)}\n@media(max-width:700px){#hmsc-app .w3-stage{height:330px}}\n#hmsc-app .w3-stage canvas{display:block;width:100%;height:100%;cursor:grab;touch-action:none}\n#hmsc-app .w3-stage canvas:active{cursor:grabbing}\n#hmsc-app .w3-msg{position:absolute;inset:0;display:flex;align-items:center;justify-content:center;text-align:center;padding:24px;font-size:13px;color:var(--muted)}\n#hmsc-app .w3-tip{position:absolute;left:0;top:0;pointer-events:none;background:var(--primary);color:#fff;font-size:12px;line-height:1.5;padding:7px 10px;border-radius:8px;box-shadow:0 10px 26px rgba(8,68,144,.3);opacity:0;transition:opacity .1s;max-width:240px;z-index:2;font-variant-numeric:tabular-nums}\n#hmsc-app .w3-tip.show{opacity:1}\n#hmsc-app .w3-legend{position:absolute;left:10px;bottom:10px;display:flex;flex-wrap:wrap;gap:4px 12px;background:rgba(255,255,255,.88);border-radius:8px;padding:5px 10px;font-size:11.5px;font-weight:600;color:var(--primary);pointer-events:none}\n#hmsc-app .w3-legend:empty{display:none}\n#hmsc-app .w3-legend i{display:inline-block;width:14px;height:3px;border-radius:2px;margin-right:6px;vertical-align:middle}\n#hmsc-app .w3-bar{display:flex;flex-wrap:wrap;gap:8px;align-items:center;margin-top:12px}\n#hmsc-app .w3-btn{min-height:36px;padding:0 14px;font-size:13px}\n#hmsc-app .w3-btn[aria-pressed=\"true\"]{background:var(--primary);color:#fff}\n#hmsc-app .w3-look{margin-top:8px}\n#hmsc-app .w3-lbl{font-size:12px;font-weight:700;color:var(--primary);margin-left:2px}\n#hmsc-app .w3-text{min-height:36px;width:150px;border:1px solid var(--field);border-radius:var(--radius-tile);padding:0 10px;font-family:var(--sans);font-size:13px;font-weight:700;color:var(--primary);background:var(--card)}\n#hmsc-app .w3-text:focus{outline:0;border-color:var(--primary);box-shadow:0 0 0 3px rgba(8,68,144,.12)}\n#hmsc-app .w3-swatches{display:inline-flex;gap:6px;align-items:center}\n#hmsc-app .w3-swatch{width:24px;height:24px;border-radius:50%;border:2px solid #fff;box-shadow:0 0 0 1px var(--field);cursor:pointer;padding:0}\n#hmsc-app .w3-swatch[aria-pressed=\"true\"]{box-shadow:0 0 0 2px var(--primary)}\n#hmsc-app .w3-concept{margin-left:auto;font-size:11px;font-weight:600;color:var(--faint)}\n#hmsc-app .seg-mini{margin-left:auto}\n#hmsc-app .seg-mini button{padding:5px 11px;font-size:11.5px}\n#hmsc-app .cross-row{display:flex;align-items:center;gap:10px}\n#hmsc-app .cross-row+.cross-row{margin-top:6px}\n#hmsc-app .cross-row .choices{flex:1;min-width:0}\n#hmsc-app .cross-side{display:none;flex:none;width:30px;font-size:11px;font-weight:800;color:var(--primary)}\n#hmsc-app .cross-split .cross-side{display:inline-block}\n#hmsc-app .cross-r .cross-side{color:var(--ds)}\n#hmsc-app .choices button[aria-pressed=\"true\"]{background:var(--primary);color:#fff}\n#hmsc-app .choices button:not([aria-pressed=\"true\"]):not(.is-off):hover{background:var(--chip-press)}\n#hmsc-app .choices button.is-off{opacity:.35;cursor:not-allowed}\n#hmsc-app .choices.drill button{flex:0 1 auto;padding:9px 16px;min-width:0}\n#hmsc-app .disclaimer{\n  grid-column:1\/-1;background:var(--card);border-radius:var(--radius-card);box-shadow:var(--shadow-card);\n  padding:16px 20px;display:flex;flex-direction:column;gap:12px\n}\n#hmsc-app .disclaimer p{margin:0;font-size:12.5px;color:var(--muted);line-height:1.6}\n#hmsc-app .disclaimer summary{cursor:pointer;font-weight:700;color:var(--primary);font-size:13px}\n#hmsc-app .disclaimer details[open] summary{margin-bottom:8px}\n#hmsc-app .accept{display:flex;align-items:center;gap:10px;font-size:13.5px;color:var(--primary);font-weight:600}\n#hmsc-app .accept input{width:18px;height:18px;accent-color:var(--primary)}\n#hmsc-app .actions{grid-column:1\/-1;display:flex;gap:10px;flex-wrap:wrap;align-items:center}\n#hmsc-app .btn{\n  appearance:none;border:0;font-family:var(--sans);font-weight:700;font-size:15px;\n  min-height:46px;padding:0 24px;border-radius:var(--radius-tile);cursor:pointer;transition:filter .12s,opacity .12s;\n  display:inline-flex;align-items:center;gap:9px;justify-content:center\n}\n#hmsc-app .btn-primary{background:var(--primary);color:#fff}\n#hmsc-app .btn-primary:hover:not(:disabled){filter:brightness(1.12)}\n#hmsc-app .btn-primary:active:not(:disabled){filter:brightness(.94)}\n#hmsc-app .btn-primary:disabled{opacity:.35;cursor:not-allowed}\n#hmsc-app .btn-ghost{background:var(--chip);color:var(--primary)}\n#hmsc-app .btn-ghost:hover{background:var(--chip-press)}\n#hmsc-app .btn-ghost:active{filter:brightness(.94)}\n#hmsc-app .errors{grid-column:1\/-1;background:var(--card);border-left:4px solid var(--danger);border-radius:12px;\n  box-shadow:var(--shadow-card);padding:14px 18px;display:none}\n#hmsc-app .errors.show{display:block;animation:fade .2s}\n#hmsc-app .errors h3{margin:0 0 6px;color:var(--danger);font-size:13px;font-weight:800}\n#hmsc-app .errors ul{margin:0;padding-left:18px;font-size:13px;color:var(--danger-ink)}\n#hmsc-app .results{grid-column:1\/-1;display:none;margin-top:6px}\n#hmsc-app .results.show{display:block;animation:fade .3s}\n#hmsc-app .panel.stale [data-results].show > *{opacity:.5}\n#hmsc-app .panel.stale [data-results].show::before{content:\"Inputs changed \u2014 press Calculate to refresh these results.\";display:block;\n  padding:10px 14px;background:var(--danger-bg);border-left:3px solid var(--danger);border-radius:8px;\n  font-size:13px;font-weight:600;color:var(--danger-ink);margin:0 0 14px}\n#hmsc-app .panel.stale [data-act=\"print\"]{display:none!important}\n#hmsc-app .result-head{display:flex;align-items:baseline;justify-content:space-between;gap:14px;margin:8px 2px 18px;flex-wrap:wrap}\n#hmsc-app .result-head h2{margin:0;color:var(--primary);font-size:20px;font-weight:800;letter-spacing:-.01em}\n#hmsc-app .result-head .meta{font-size:12px;color:var(--muted);font-weight:500;font-variant-numeric:tabular-nums}\n#hmsc-app .len-grid{display:grid;grid-template-columns:1fr 1fr;gap:16px;margin-bottom:18px}\n@media(max-width:560px){#hmsc-app .len-grid{grid-template-columns:1fr}}\n#hmsc-app .len{\n  background:var(--card);color:var(--primary);border-radius:var(--radius-card);box-shadow:var(--shadow-card);\n  padding:18px 22px 20px;position:relative;overflow:hidden\n}\n#hmsc-app .len::before{content:\"\";position:absolute;left:0;right:0;top:0;height:5px;background:var(--primary)}\n#hmsc-app .len.r::before{background:var(--ds)}\n#hmsc-app .len .side{font-size:13px;font-weight:800;color:var(--primary)}\n#hmsc-app .len .tag{font-size:12px;color:var(--muted);margin-top:1px}\n#hmsc-app .len .val{font-weight:800;font-size:46px;line-height:1;margin:14px 0 4px;letter-spacing:-.02em;\n  font-variant-numeric:tabular-nums}\n#hmsc-app .len .val small{font-size:17px;color:var(--muted);font-weight:600}\n#hmsc-app .len .pair{display:flex;gap:28px;margin:14px 0 4px}\n#hmsc-app .len .pair .pnum{font-weight:800;font-size:30px;line-height:1;letter-spacing:-.02em;font-variant-numeric:tabular-nums}\n#hmsc-app .len .pair .pnum small{font-size:13px;color:var(--muted);font-weight:600}\n#hmsc-app .len .pair .plab{font-size:11px;font-weight:600;color:var(--muted);margin-top:4px}\n#hmsc-app .len .exact{font-size:12.5px;color:var(--muted);font-variant-numeric:tabular-nums}\n#hmsc-app .subgrid{display:grid;grid-template-columns:1fr 1fr;gap:16px}\n#hmsc-app .subgrid .span2{grid-column:1\/-1}\n@media(max-width:820px){#hmsc-app .subgrid{grid-template-columns:1fr}}\n#hmsc-app .rcard{background:var(--card);border-radius:13px;padding:18px;box-shadow:var(--shadow-card)}\n#hmsc-app .rcard h3{margin:0 0 14px;font-size:14px;font-weight:700;color:var(--primary)}\n#hmsc-app table.geo{width:100%;border-collapse:collapse;font-size:13px}\n#hmsc-app table.geo td{padding:8px 4px;border-bottom:1px solid var(--line)}\n#hmsc-app table.geo tr:last-child td{border-bottom:0}\n#hmsc-app table.geo td.lbl{color:var(--muted)}\n#hmsc-app table.geo td.l,#hmsc-app table.geo td.r{text-align:right;color:var(--primary);font-weight:600;font-variant-numeric:tabular-nums}\n#hmsc-app table.geo th{font-size:10.5px;color:var(--faint);text-transform:uppercase;letter-spacing:.07em;font-weight:700;text-align:right;padding:0 4px 8px}\n#hmsc-app table.geo th.lbl{text-align:left}\n#hmsc-app .bar-row{margin-bottom:14px}\n#hmsc-app .bar-row:last-child{margin-bottom:0}\n#hmsc-app .bar-top{display:flex;justify-content:space-between;font-size:12.5px;margin-bottom:5px}\n#hmsc-app .bar-top .s{font-weight:600;color:var(--primary)}\n#hmsc-app .bar-top .v{color:var(--primary);font-weight:700;font-variant-numeric:tabular-nums}\n#hmsc-app .bar{height:10px;background:var(--chip);border-radius:999px;overflow:hidden}\n#hmsc-app .bar i{display:block;height:100%;border-radius:999px;background:var(--primary)}\n#hmsc-app .bar.r i{background:var(--ds)}\n#hmsc-app .wt{display:flex;flex-direction:column;gap:9px}\n#hmsc-app .wt-row{display:flex;justify-content:space-between;font-size:13.5px;padding:3px 0}\n#hmsc-app .wt-row.total{border-top:2px solid var(--primary);margin-top:4px;padding-top:11px;font-weight:800;color:var(--primary);font-size:15px}\n#hmsc-app .wt-row .v{font-weight:700;font-variant-numeric:tabular-nums}\n#hmsc-app .wt-row .lbl{color:var(--muted);display:flex;gap:8px;align-items:center}\n#hmsc-app .wt-row .lbl b{color:var(--primary);font-weight:600}\n#hmsc-app .lacing-wrap{display:grid;grid-template-columns:1fr 1fr;gap:10px}\n@media(max-width:560px){#hmsc-app .lacing-wrap{grid-template-columns:1fr}}\n#hmsc-app .lace-col{text-align:center}\n#hmsc-app .lace-col .cap{font-size:11.5px;font-weight:700;color:var(--muted);margin-bottom:6px}\n#hmsc-app .lace-col svg{width:100%;height:auto;max-width:230px}\n#hmsc-app .legend{display:flex;gap:16px;justify-content:center;margin-top:10px;font-size:12px;color:var(--muted);flex-wrap:wrap}\n#hmsc-app .legend span{display:inline-flex;align-items:center;gap:6px}\n#hmsc-app .legend i{width:16px;height:3px;border-radius:2px;display:inline-block}\n#hmsc-app .foot-note{grid-column:1\/-1;font-size:12px;color:var(--muted);margin-top:4px;padding:0 2px;line-height:1.6}\n#hmsc-app footer.site{margin-top:60px;border-top:1px solid var(--line);padding:30px 20px;background:var(--card)}\n#hmsc-app .foot-inner{max-width:1180px;margin:0 auto;display:flex;align-items:center;justify-content:center;gap:20px;flex-wrap:wrap}\n#hmsc-app .foot-inner img{height:30px;opacity:.95}\n#hmsc-app .foot-inner p{margin:0;font-size:12px;color:var(--muted);max-width:560px;line-height:1.6}\n#hmsc-app .section-svg{width:100%;height:auto;max-width:320px;display:block;margin:2px auto 0}\n#hmsc-app .section-note{font-size:11.5px;color:var(--muted);text-align:center;margin:8px 0 0;line-height:1.5}\n#hmsc-app .print-head{display:none}\n#hmsc-app #toast{position:fixed;left:50%;bottom:28px;transform:translateX(-50%) translateY(20px);background:var(--primary);color:#fff;\n  padding:11px 22px;border-radius:var(--radius-tile);font-size:13.5px;font-weight:600;box-shadow:0 12px 34px rgba(8,68,144,.35);\n  opacity:0;pointer-events:none;transition:.25s;z-index:99}\n#hmsc-app #toast.show{opacity:1;transform:translateX(-50%) translateY(0)}\n#hmsc-app .loaded-banner{grid-column:1\/-1;background:rgba(8,68,144,.07);border:1px solid var(--primary-soft);color:var(--primary);\n  border-radius:var(--radius-tile);padding:11px 16px;font-size:13px;font-weight:600;display:none;align-items:center;gap:9px}\n#hmsc-app .loaded-banner.show{display:flex}\n#hmsc-app .loaded-banner::before{content:\"\";width:9px;height:9px;border-radius:50%;background:var(--side-b);flex:none}\n#hmsc-app .btn-share{margin-left:auto}\n#hmsc-app .dimb{display:inline-grid;place-items:center;min-width:17px;height:17px;padding:0 5px;border-radius:9px;\n  background:var(--primary);color:#fff;font-size:8.5px;font-weight:700;flex:none;letter-spacing:.02em}\n#hmsc-app .dimb.r{background:#E63329}\n#hmsc-app .lp-card .lp-tag{font-size:11px;font-weight:600;color:var(--faint)}\n#hmsc-app .lp-views{display:grid;grid-template-columns:1fr 1fr;gap:14px}\n#hmsc-app .lp-views .lp-stats{grid-column:2;margin-top:0}\n@media(max-width:560px){#hmsc-app .lp-views{grid-template-columns:1fr}\n#hmsc-app .lp-views .lp-stats{grid-column:auto}}\n#hmsc-app .lp-view{background:#F6F9FB;border:1px solid var(--line);border-radius:var(--radius-tile);padding:10px 8px 6px;text-align:center}\n#hmsc-app .lp-view svg{width:100%;max-width:340px;height:auto}\n#hmsc-app .lp-cap .lp-lg{display:inline-flex;align-items:center;gap:4px;margin-left:10px;color:var(--muted);font-weight:600}\n#hmsc-app .lp-cap .lp-lg i{display:inline-block;width:12px;height:3px;border-radius:2px;margin-left:6px}\n#hmsc-app .lp-cap .lp-lg i.n,#hmsc-app .lp-cap .lp-lg i.sa{background:var(--sa)}\n#hmsc-app .lp-cap .lp-lg i.d,#hmsc-app .lp-cap .lp-lg i.sd{background:var(--sd)}\n#hmsc-app .lp-cap .lp-lg i.sb{background:var(--sb)}\n#hmsc-app .lp-cap .lp-lg i.sc{background:var(--sc)}\n#hmsc-app .lp-cap .lp-lg i.sb,#hmsc-app .lp-cap .lp-lg i.sc{margin-left:2px}\n#hmsc-app .lp-cap .lp-lg em{font-style:normal;font-weight:500;margin-left:8px;color:var(--faint)}\n#hmsc-app .lp-cap{font-size:11.5px;color:var(--primary);font-weight:600;margin-top:2px;font-variant-numeric:tabular-nums}\n#hmsc-app .lp-stats{display:flex;flex-wrap:wrap;gap:8px;margin-top:12px}\n#hmsc-app .lp-stat{background:var(--chip);border-radius:8px;padding:6px 12px;font-size:12px;color:var(--muted);display:flex;align-items:baseline;gap:7px}\n#hmsc-app .lp-stat b{font-size:13.5px;color:var(--primary);font-weight:700;font-variant-numeric:tabular-nums}\n#hmsc-app #lptip{position:fixed;z-index:999;background:var(--primary);color:#fff;font-family:var(--sans);font-size:12px;\n  padding:8px 12px;border-radius:9px;box-shadow:0 10px 28px rgba(8,68,144,.35);pointer-events:none;\n  opacity:0;transition:opacity .12s;max-width:240px;line-height:1.45}\n#hmsc-app #lptip.show{opacity:1}\n#hmsc-app #lptip .tt{font-weight:700;font-size:11.5px;font-variant-numeric:tabular-nums}\n#hmsc-app #lptip.info{max-width:340px}\n#hmsc-app #lptip .tv{display:block;margin:7px 0 1px;background:#fff;border-radius:7px;padding:3px 4px}\n#hmsc-app #lptip .tv svg{display:block;width:100%;height:auto}\n#hmsc-app #lptip.info .tt{font-weight:500;font-size:12px}\n#hmsc-app #lptip .th{display:block;margin-top:6px;padding-top:6px;border-top:1px solid rgba(255,255,255,.18);font-size:11.5px}\n#hmsc-app #lptip .th b{display:block;font-size:10px;font-weight:800;letter-spacing:.06em;text-transform:uppercase;color:var(--primary-soft);margin-bottom:2px}\n#hmsc-app .tip:focus-visible{outline:2px solid var(--primary);outline-offset:1px}\n#hmsc-app #lptip .ta{display:block;margin-top:5px;color:var(--primary-soft);font-size:11px;border-top:1px solid rgba(255,255,255,.18);padding-top:5px}\n#hmsc-app .pcode{display:inline-block;background:var(--chip);color:var(--primary);font-size:9.5px;font-weight:700;border-radius:6px;padding:1px 6px;vertical-align:1px;letter-spacing:.02em}\n#hmsc-app .inp.flash,#hmsc-app .field input.flash{border-color:var(--side-b)!important;box-shadow:0 0 0 3px rgba(0,101,227,.28)!important}\n#hmsc-app [data-goto]:hover{opacity:.7}\n#hmsc-app .ok-chip,#hmsc-app .warn-chip{display:inline-block;font-size:9.5px;font-weight:700;border-radius:6px;padding:1px 6px;vertical-align:1px;letter-spacing:.03em}\n#hmsc-app .ok-chip{background:var(--ok-bg);color:var(--ok-ink)}\n#hmsc-app .warn-chip{background:var(--danger-bg);color:var(--danger-ink)}\n#hmsc-app .tcs-link{color:var(--side-b);font-weight:600;text-decoration:none;white-space:nowrap}\n#hmsc-app .tcs-link:hover{text-decoration:underline}\n#hmsc-app .sp-torsion{color:var(--danger-ink)}\n#hmsc-app .ten-warn{margin:9px 0 0;padding:9px 12px;background:var(--danger-bg);border-left:3px solid var(--danger);\n  border-radius:8px;font-size:12px;color:var(--danger-ink);line-height:1.5}\n#hmsc-app .spec-card .spec-sub{font-size:11px;font-weight:500;color:var(--faint);margin-left:8px}\n#hmsc-app .spec-table{width:100%;border-collapse:collapse;margin-top:10px;font-size:12.5px}\n#hmsc-app .spec-table th{font-size:10px;letter-spacing:.06em;text-transform:uppercase;color:var(--faint);font-weight:700;\n  text-align:left;padding:6px 10px;border-bottom:1.5px solid var(--line)}\n#hmsc-app .spec-table td{padding:7px 10px;border-bottom:1px solid var(--line);color:var(--primary)}\n#hmsc-app .spec-table tr:last-child td{border-bottom:none}\n#hmsc-app .spec-table td b{font-weight:700;font-variant-numeric:tabular-nums}\n@media print{#hmsc-app .spec-card{break-inside:avoid}\n#hmsc-app .ten-warn{border:1px solid #999}}\n#hmsc-app #leadwrap{position:fixed;inset:0;z-index:1000;display:none;align-items:center;justify-content:center;padding:18px}\n#hmsc-app #leadwrap.show{display:flex}\n#hmsc-app #leadwrap .ovl{position:absolute;inset:0;background:rgba(8,30,58,.55);backdrop-filter:blur(2px)}\n#hmsc-app .leadcard{position:relative;background:var(--card);border-radius:16px;max-width:480px;width:100%;max-height:92vh;overflow:auto;\n  padding:26px 26px 22px;box-shadow:0 30px 80px rgba(8,30,58,.4)}\n#hmsc-app .leadcard h3{margin:0 0 4px;font-size:18px;color:var(--primary);font-weight:800}\n#hmsc-app .leadcard .sub{font-size:13px;color:var(--muted);margin:0 0 16px;line-height:1.5}\n#hmsc-app .leadcard .lrow{display:grid;grid-template-columns:1fr 1fr;gap:10px}\n#hmsc-app .leadcard .lf{margin-bottom:10px}\n#hmsc-app .leadcard .lf.full{grid-column:1\/-1}\n#hmsc-app .leadcard .lf label{display:block;font-size:11px;font-weight:600;letter-spacing:.04em;text-transform:uppercase;color:var(--faint);margin-bottom:4px}\n#hmsc-app .leadcard input,#hmsc-app .leadcard select{width:100%;box-sizing:border-box;border:1px solid var(--field);border-radius:9px;padding:10px 11px;\n  font-family:var(--sans);font-size:13.5px;font-weight:600;color:var(--primary);background:var(--card)}\n#hmsc-app .leadcard input:focus,#hmsc-app .leadcard select:focus{outline:none;border-color:var(--primary);box-shadow:0 0 0 3px rgba(8,68,144,.12)}\n#hmsc-app .leadcard .consent{display:flex;gap:9px;align-items:flex-start;font-size:11.5px;color:var(--muted);margin:12px 0 14px;line-height:1.45}\n#hmsc-app .leadcard .consent input{width:auto;margin-top:2px;accent-color:var(--primary)}\n#hmsc-app .leadcard .consent a{color:var(--side-b)}\n#hmsc-app .leadcard .lerr{display:none;color:var(--danger-ink);font-size:12px;margin:0 0 10px}\n#hmsc-app .leadcard .lerr.show{display:block}\n#hmsc-app .leadcard .lactions{display:flex;gap:10px;align-items:center}\n#hmsc-app .leadcard .lclose{position:absolute;top:12px;right:14px;border:none;background:none;font-size:20px;color:var(--faint);cursor:pointer;line-height:1}\n#hmsc-app .leadcard .lok{display:none;text-align:center;padding:18px 0 6px}\n#hmsc-app .leadcard .lok.show{display:block}\n#hmsc-app .leadcard .lok .big{font-size:34px;margin-bottom:8px;color:var(--ok)}\n@media(max-width:560px){#hmsc-app .leadcard .lrow{grid-template-columns:1fr}}\n@media print{#hmsc-app #leadwrap{display:none!important}}\n@media print{#hmsc-app .lp-card{display:none!important}}\n#hmsc-app .dwrap.hub-art{flex-direction:column;gap:6px;padding:10px 14px}\n#hmsc-app .hub-main svg{width:100%;max-width:430px;height:auto;display:block;margin:0 auto}\n#hmsc-app .hub-insets{display:flex;gap:14px;justify-content:center;flex-wrap:wrap}\n#hmsc-app .hub-insets svg{width:170px;height:auto}\n#hmsc-app .card h2 .hd-l{display:inline-flex;align-items:center;gap:9px}\n#hmsc-app .hd-ico{display:inline-flex;color:var(--primary)}\n#hmsc-app .hd-ico svg{width:19px;height:19px}\n#hmsc-app .guide{margin:14px 0 2px}\n#hmsc-app .guide summary{cursor:pointer;font-size:12px;font-weight:700;color:var(--primary);\n  display:inline-flex;align-items:center;gap:7px;list-style:none}\n#hmsc-app .guide summary::-webkit-details-marker{display:none}\n#hmsc-app .guide summary::before{content:\"+\";font-weight:700;width:15px;height:15px;text-align:center;\n  background:var(--chip);border-radius:50%;font-size:11px;line-height:15px}\n#hmsc-app .guide[open] summary::before{content:\"\u2212\"}\n#hmsc-app .guide .dwrap,#hmsc-app .spoke-illo .dwrap{margin-top:12px;background:#F6F9FB;border:1px solid var(--line);border-radius:var(--radius-tile);\n  padding:14px;display:flex;justify-content:center}\n#hmsc-app .guide .dwrap svg{width:100%;max-width:400px;height:auto}\n#hmsc-app .spoke-illo{margin:2px 0 6px}\n#hmsc-app .spoke-illo .dwrap{margin-top:0;padding:8px 14px}\n#hmsc-app .spoke-illo .dwrap svg{width:100%;max-width:760px;height:auto}\n#hmsc-app .spoke-illo .dwrap{flex-direction:column;align-items:center}\n#hmsc-app .spoke-illo svg .hl-f.hl{fill:var(--hl)!important}\n@media print{#hmsc-app .guide,#hmsc-app .spoke-illo{display:none!important}}\n#hmsc-app .guide .dwrap{flex-direction:column;align-items:center}\n#hmsc-app .hub-main{width:100%;max-width:680px;margin:0 auto}\n#hmsc-app .spec-seq{margin-top:14px;padding-top:12px;border-top:1px dashed var(--line);break-inside:avoid}\n#hmsc-app .spec-seq-h{font-size:12.5px;font-weight:700;color:var(--primary);margin-bottom:4px}\n#hmsc-app .spec-seq svg{width:100%;max-width:520px;height:auto;display:block;margin:0 auto}\n#hmsc-app.embed{background:#fff}\n#hmsc-app.embed footer.site{display:none}\n#hmsc-app.embed .wrap{padding-bottom:34px}\n@media print{@page{margin:14mm}\n#hmsc-app header.top,#hmsc-app footer.site{display:none!important}\n#hmsc-app{background:#fff;font-size:12px}\n#hmsc-app .wrap{padding:0;max-width:none}\n#hmsc-app .tabs,#hmsc-app .actions,#hmsc-app .disclaimer,#hmsc-app .errors,#hmsc-app .loaded-banner,#hmsc-app .card[data-card],#hmsc-app .tab{display:none!important}\n#hmsc-app .panel{display:none!important}\n#hmsc-app .panel.has-results{display:block!important}\n#hmsc-app .panel.has-results .grid{display:block}\n#hmsc-app .results{display:block!important;margin:0}\n#hmsc-app .rcard,#hmsc-app .len{box-shadow:none!important;border:1px solid var(--line);break-inside:avoid}\n#hmsc-app .subgrid{gap:10px}\n#hmsc-app .result-head h2{font-size:16px}\n#hmsc-app .print-head{display:flex!important;align-items:center;justify-content:space-between;\n    border-bottom:2px solid var(--primary);padding-bottom:10px;margin:0 0 16px}\n#hmsc-app .print-head img{height:32px}\n#hmsc-app .print-head .ph-meta{text-align:right;font-size:11px;color:var(--muted);line-height:1.5}\n#hmsc-app .print-head .ph-meta b{display:block;color:var(--primary);font-size:15px;font-weight:800}\n#hmsc-app .len::before,#hmsc-app .bar i{-webkit-print-color-adjust:exact;print-color-adjust:exact;color-adjust:exact}}\n#hmsc-app .hub-src{margin:0 0 12px}\n#hmsc-app .hub-src .seg{display:flex;width:100%}\n#hmsc-app .hub-src .seg button{flex:1 1 0;padding:8px 10px}\n#hmsc-app .hubm{container-type:inline-size;display:flex;flex-wrap:wrap;align-items:center;justify-content:center;gap:6px 10px;background:#F6F9FB;border:1px solid var(--line);border-radius:var(--radius-tile);padding:10px 8px 8px;margin:0 0 14px}\n#hmsc-app .hubm svg{display:block;height:auto}\n#hmsc-app .hubm svg .hl-t.hl,#hmsc-app .hubm svg .hl-f.hl{fill:var(--hl)!important}\n#hmsc-app .hubm svg .hl-s.hl{stroke:var(--hl)!important;stroke-width:1.8}\n#hmsc-app .hubm svg .hl-d.hl{stroke:var(--hl)!important;stroke-width:1.8}\n#hmsc-app .hubm .hm-side{flex:1 1 100%;min-width:0;max-width:440px;order:-1}\n#hmsc-app .hubm .hm-fl{flex:0 1 42%;min-width:0;max-width:136px}\n@container (min-width:600px){#hmsc-app .hubm .hm-side{flex:1 1 300px;order:0}\n#hmsc-app .hubm .hm-fl{flex:0 1 112px}\n#hmsc-app .hubm .hm-fl.l{order:-1}}\n#hmsc-app .hubm-cap{flex-basis:100%;margin:2px 6px 0;font-size:11.5px;line-height:1.6;color:var(--muted);text-align:center}\n#hmsc-app .step{display:inline-grid;place-items:center;width:18px;height:18px;border-radius:50%;background:var(--primary);color:#fff;font-size:10.5px;font-weight:700;flex:none;font-variant-numeric:tabular-nums;line-height:1}\n#hmsc-app .hubm-cap .step,#hmsc-app .fc-out .step,#hmsc-app [data-err] .step{width:15px;height:15px;font-size:9px;vertical-align:-3px;margin:0 1px}\n#hmsc-app .hw-row{display:flex;gap:8px;align-items:center;flex-wrap:wrap}\n#hmsc-app .hw-row .inp{flex:1 1 120px}\n#hmsc-app .hw-chips{display:flex;gap:6px;flex-wrap:wrap}\n#hmsc-app .hw-chips button{appearance:none;border:0;background:var(--chip);color:var(--primary);font-family:var(--sans);font-size:12px;font-weight:600;padding:7px 11px;border-radius:8px;cursor:pointer;font-variant-numeric:tabular-nums}\n#hmsc-app .hw-chips button:hover{background:var(--chip-press)}\n#hmsc-app .hw-chips button[aria-pressed=\"true\"]{background:var(--primary);color:#fff}\n#hmsc-app .fc-out{display:flex;flex-wrap:wrap;align-items:center;gap:4px 8px;background:var(--chip);border-radius:8px;padding:8px 12px;font-size:12px;color:var(--muted)}\n#hmsc-app .fc-out b{color:var(--primary);font-size:13px;font-variant-numeric:tabular-nums}\n#hmsc-app .fc-out .fco-l{font-weight:600;color:var(--primary)}\n#hmsc-app .fc-out .warn{color:var(--danger-ink);font-weight:600}\n#hmsc-app .card[data-card=\"hub\"].src-spec .measure-only{display:none!important}\n#hmsc-app .card[data-card=\"hub\"]:not(.src-spec) .spec-only{display:none!important}\n#hmsc-app .card[data-card=\"rim\"].src-measure .rim-spec-only{display:none!important}\n#hmsc-app .card[data-card=\"rim\"]:not(.src-measure) .rim-measure-only{display:none!important}\n#hmsc-app .hubm-cap .dimb{vertical-align:-3px}\n#hmsc-app .osb-guide{margin-top:0}\n#hmsc-app .osb-guide .hubm{margin:12px 0 0}\n#hmsc-app .osb-steps{flex-basis:100%;list-style:none;margin:4px 4px 0;padding:0;font-size:12px;line-height:1.55;color:var(--muted)}\n#hmsc-app .osb-steps li{margin:6px 0}\n#hmsc-app .osb-row .inp{flex:1 1 64px}\n#hmsc-app .osb-side button{padding:7px 9px;font-size:11.5px;white-space:nowrap}\n#hmsc-app .osb-steps b{color:var(--primary);font-weight:600}\n#hmsc-app .osb-steps .step,#hmsc-app .adv summary .step{width:15px;height:15px;font-size:9px;vertical-align:-3px;margin:0 1px}\n#hmsc-app .adv{margin:14px 0 0;border:1px solid var(--line);border-radius:var(--radius-tile);padding:10px 12px;background:#FAFCFD}\n#hmsc-app .adv summary{cursor:pointer;font-size:12.5px;font-weight:700;color:var(--primary);display:flex;align-items:center;gap:8px;list-style:none;flex-wrap:wrap}\n#hmsc-app .adv summary::-webkit-details-marker{display:none}\n#hmsc-app .adv summary::before{content:\"+\";font-weight:700;width:16px;height:16px;text-align:center;background:var(--chip);border-radius:50%;font-size:11px;line-height:16px;flex:none}\n#hmsc-app .adv[open] summary::before{content:\"\u2212\"}\n#hmsc-app .adv .adv-sum{font-weight:500;color:var(--faint);font-size:11.5px}\n#hmsc-app .adv .adv-note{margin:8px 0 10px;font-size:12px}\n#hmsc-app .hub-opt{margin-top:14px;padding-top:12px;border-top:1px dashed var(--line)}\n#hmsc-app .sheet-head{display:flex;align-items:flex-end;justify-content:space-between;gap:16px;margin:10px 2px 14px;flex-wrap:wrap}\n#hmsc-app .sheet-head .sh-kicker{font-size:11px;font-weight:700;letter-spacing:.08em;text-transform:uppercase;color:var(--faint)}\n#hmsc-app .sheet-head h2{margin:2px 0 0;font-size:24px;font-weight:800;color:var(--primary);letter-spacing:-.01em}\n#hmsc-app .sheet-head .sh-sub{font-size:13px;color:var(--muted);margin-top:3px;font-weight:500}\n#hmsc-app .sheet-head .sh-r{text-align:right}\n#hmsc-app .sheet-head img{height:30px;display:block;margin-left:auto}\n#hmsc-app .sheet-head .sh-date{font-size:11.5px;color:var(--faint);margin-top:6px;font-variant-numeric:tabular-nums}\n#hmsc-app .sh-facts{display:flex;flex-wrap:wrap;gap:8px;margin:0 0 16px}\n#hmsc-app .sh-facts span{background:var(--chip);border-radius:8px;padding:6px 11px;font-size:12px;color:var(--muted);font-weight:600;font-variant-numeric:tabular-nums}\n#hmsc-app .sh-facts span b{color:var(--primary);font-weight:700}\n#hmsc-app .len{padding:18px 22px 16px}\n#hmsc-app .len-top{display:flex;justify-content:space-between;align-items:baseline;gap:10px;flex-wrap:wrap}\n#hmsc-app .len .val{margin:12px 0 2px}\n#hmsc-app .len .pair{margin:12px 0 2px}\n#hmsc-app .len-foot{display:flex;gap:14px;flex-wrap:wrap;margin-top:12px;padding-top:10px;border-top:1px dashed var(--line);font-size:11.5px;color:var(--faint);font-weight:600;font-variant-numeric:tabular-nums}\n#hmsc-app .len-foot b{color:var(--primary)}\n#hmsc-app .glance{display:grid;grid-template-columns:1fr 1fr;gap:12px}\n#hmsc-app .glance .lp-view{display:flex;flex-direction:column;justify-content:center;align-items:center}\n#hmsc-app .glance .lp-view svg{width:100%}\n#hmsc-app .glance .lp-view svg{max-width:380px}\n#hmsc-app .rcard h3 .spec-sub{font-size:11px;font-weight:500;color:var(--faint);margin-left:8px}\n#hmsc-app .inp-grid{display:grid;grid-template-columns:repeat(3,1fr);gap:4px 24px}\n#hmsc-app .inp-h{font-size:10.5px;letter-spacing:.07em;text-transform:uppercase;color:var(--faint);font-weight:700;margin:4px 0 6px}\n#hmsc-app .inp-row{display:flex;align-items:center;gap:8px;font-size:12.5px;padding:5px 0;border-bottom:1px solid var(--line)}\n#hmsc-app .inp-row:last-child{border-bottom:0}\n#hmsc-app .inp-row .step{width:16px;height:16px;font-size:9.5px}\n#hmsc-app .inp-row .step.none{background:transparent}\n#hmsc-app .inp-row .k{color:var(--muted);flex:1;min-width:0}\n#hmsc-app .inp-row .v{color:var(--primary);font-weight:700;font-variant-numeric:tabular-nums;text-align:right}\n#hmsc-app .notes-card ul{margin:0;padding-left:18px;font-size:12.5px;color:var(--muted);line-height:1.6}\n#hmsc-app .notes-card li{margin:4px 0}\n#hmsc-app .notes-card b{color:var(--primary)}\n@media(max-width:700px){#hmsc-app .glance{grid-template-columns:1fr}\n#hmsc-app .inp-grid{grid-template-columns:1fr}\n#hmsc-app .sheet-head h2{font-size:20px}}\n@media print{#hmsc-app .sheet-head{border-bottom:2px solid var(--primary);padding-bottom:10px;margin:0 0 12px}\n#hmsc-app .sheet-head h2{font-size:18px}\n#hmsc-app .sh-facts span{border:1px solid var(--line);background:#fff}\n#hmsc-app .glance-card,#hmsc-app .inputs-card,#hmsc-app .notes-card{break-inside:avoid}\n#hmsc-app .inp-grid{grid-template-columns:repeat(3,1fr)}\n#hmsc-app .len-foot{color:var(--muted)}}\n#hmsc-app .choices.nip-type{display:grid;grid-template-columns:repeat(5,minmax(0,1fr));gap:6px}\n#hmsc-app .choices.nip-type button{display:flex;flex-direction:column;align-items:center;gap:4px;padding:6px 3px 8px;min-width:0}\n#hmsc-app .choices.nip-type .ni,#hmsc-app .choices.nip-type .ni svg{display:block;width:40px;height:46px}\n#hmsc-app .choices.nip-type .nl{font-size:11px;line-height:1.15;text-align:center}\n#hmsc-app .choices.nip-len button,#hmsc-app .choices.nip-mat button{flex:1 1 auto;padding:8px 10px;min-width:0}\n#hmsc-app .erd-ref{flex-wrap:wrap;max-width:100%}\n#hmsc-app .erd-ref button{padding:7px 11px;font-size:12px}\n#hmsc-app .nip-box{display:flex;gap:12px;align-items:center;flex-wrap:wrap;background:var(--chip);border-radius:10px;padding:10px}\n#hmsc-app .nip-art{flex:0 0 auto;background:#fff;border-radius:8px;padding:4px 2px}\n#hmsc-app .nip-art svg{display:block;width:270px;max-width:100%;height:auto}\n#hmsc-app .nip-out{flex:1 1 180px;font-size:12px;line-height:1.5;color:var(--muted)}\n#hmsc-app .nip-out p{margin:0 0 5px}\n#hmsc-app .nip-out p:last-child{margin-bottom:0}\n#hmsc-app .nip-out b{color:var(--primary)}\n#hmsc-app .nip-out .warn{color:var(--danger-ink);font-weight:600}\n@media (max-width:560px){#hmsc-app .choices.nip-type{grid-template-columns:repeat(3,minmax(0,1fr))}\n#hmsc-app .nip-art{flex:1 1 100%}\n#hmsc-app .nip-art svg{width:100%}\n#hmsc-app .nip-len-f,#hmsc-app .nip-mat-f{grid-column:1\/-1}}\n#hmsc-app .gsw{display:inline-block;width:9px;height:9px;border-radius:3px;margin-right:2px;vertical-align:-0.5px}\n#hmsc-app .gsw+b{margin-left:4px}\n#hmsc-app .len.l .pcode{background:#E3ECF8;color:var(--sa)}\n#hmsc-app .len.r .pcode{background:#FCE8DA;color:#B4460A}\n#hmsc-app .legend i{height:4px}\n#hmsc-app{\n  --track:#E3EAF1;                       \n  --fill:#F4F7FA;                        \n  --hair:rgba(8,68,144,.11);             \n  --hair-strong:rgba(8,68,144,.18);\n  --ring:rgba(8,68,144,.14);             \n  --tint:rgba(8,68,144,.07);             \n  --tint-2:rgba(8,68,144,.11);\n  --shadow-1:0 1px 2px rgba(15,39,66,.05),0 10px 24px -14px rgba(15,39,66,.16);\n  --shadow-thumb:0 1px 2px rgba(15,39,66,.14),0 2px 6px rgba(15,39,66,.08);\n  --shadow-press:0 6px 16px -6px rgba(8,68,144,.55);\n  --r-card:18px; --r-ctl:11px; --r-thumb:8px;\n}\n@media screen{#hmsc-app{font-size:15px;line-height:1.5;font-feature-settings:\"cv11\",\"ss01\";letter-spacing:-.003em;text-rendering:optimizeLegibility}\n#hmsc-app header.top{background:rgba(255,255,255,.92);border-bottom:1px solid var(--hair);backdrop-filter:saturate(1.4) blur(10px);-webkit-backdrop-filter:saturate(1.4) blur(10px)}\n#hmsc-app .top-inner{padding-block:18px}\n#hmsc-app .brand-text{border-left-color:var(--hair)}\n#hmsc-app .brand h1{font-size:26px;line-height:1.15;font-weight:700;letter-spacing:-.024em}\n#hmsc-app .brand .sub{font-size:13.5px;line-height:1.45;margin-top:5px}\n#hmsc-app .top-chip{background:transparent;border:1px solid var(--hair-strong);color:var(--muted);border-radius:999px;padding:4px 11px;font-size:11.5px;font-weight:600}\n#hmsc-app .tabs{background:var(--track);border:0;border-radius:14px;padding:4px;gap:2px;margin:28px 0 24px;overflow:visible}\n#hmsc-app .tab{background:transparent;border-radius:10px;font-weight:600;font-size:14.5px;padding:10px 22px;color:var(--muted);transition:background .15s,color .15s,box-shadow .15s}\n#hmsc-app .tab+.tab{border-left:0}\n#hmsc-app .tab[aria-selected=\"true\"]{background:var(--card);color:var(--primary);box-shadow:var(--shadow-thumb)}\n#hmsc-app .tab:not([aria-selected=\"true\"]):hover{background:rgba(255,255,255,.55);color:var(--primary)}\n#hmsc-app .grid{gap:20px}\n#hmsc-app .card{border-radius:var(--r-card);padding:24px 24px 26px;border:1px solid var(--hair);box-shadow:var(--shadow-1)}\n#hmsc-app .card h2{font-size:17px;font-weight:650;letter-spacing:-.012em;margin-bottom:6px}\n#hmsc-app .card .note{font-size:13.5px;line-height:1.5;margin-bottom:18px}\n#hmsc-app .hd-l{display:inline-flex;align-items:center;gap:10px}\n#hmsc-app .hd-ico{width:30px;height:30px;border-radius:9px;background:var(--tint);display:inline-grid;place-items:center}\n#hmsc-app .hd-ico svg{width:17px;height:17px}\n#hmsc-app .seg{background:var(--track);border:0;border-radius:var(--r-ctl);padding:3px;gap:2px;overflow:visible}\n#hmsc-app .seg button{border-radius:var(--r-thumb);font-weight:600;font-size:12.5px;color:var(--muted);padding:7px 14px;transition:background .15s,color .15s,box-shadow .15s}\n#hmsc-app .seg button+button{border-left:0}\n#hmsc-app .seg button[aria-pressed=\"true\"]{background:var(--card);color:var(--primary);box-shadow:var(--shadow-thumb)}\n#hmsc-app .seg button:not([aria-pressed=\"true\"]):not(.is-off):hover{background:rgba(255,255,255,.55);color:var(--primary)}\n#hmsc-app .seg-mini{padding:2px}\n#hmsc-app .seg-mini button{padding:5px 11px;font-size:11.5px;border-radius:7px}\n#hmsc-app .hub-src .seg button{padding:8px 10px}\n#hmsc-app .choices{gap:6px}\n#hmsc-app .choices button{background:var(--fill);box-shadow:inset 0 0 0 1px var(--hair);font-weight:600;font-size:13px;padding:10px 0;border-radius:var(--r-ctl);transition:background .12s,box-shadow .12s,color .12s}\n#hmsc-app .choices button:not([aria-pressed=\"true\"]):not(.is-off):hover{background:var(--card);box-shadow:inset 0 0 0 1px var(--hair-strong)}\n#hmsc-app .choices button[aria-pressed=\"true\"]{background:var(--primary);color:#fff;box-shadow:var(--shadow-press)}\n#hmsc-app .choices button.is-off{opacity:.32;box-shadow:none}\n#hmsc-app .choices.drill button{padding:10px 16px}\n#hmsc-app .hw-chips button{background:var(--fill);box-shadow:inset 0 0 0 1px var(--hair);border-radius:999px;padding:6px 12px}\n#hmsc-app .hw-chips button:hover{background:var(--card);box-shadow:inset 0 0 0 1px var(--hair-strong)}\n#hmsc-app .hw-chips button[aria-pressed=\"true\"]{background:var(--primary);box-shadow:none}\n#hmsc-app .fields{gap:16px 16px}\n#hmsc-app .field{gap:7px}\n#hmsc-app .field label{font-size:13px;font-weight:600;letter-spacing:-.005em}\n#hmsc-app .field label .u{font-size:11.5px}\n#hmsc-app .inp,#hmsc-app .field>input,#hmsc-app .field>select,#hmsc-app .field input:not([type=checkbox]),#hmsc-app .field select{background:var(--fill);border:1px solid var(--hair);border-radius:var(--r-ctl)}\n#hmsc-app .inp input,#hmsc-app .field input:not([type=checkbox]),#hmsc-app .field select{font-size:15px;font-weight:600;padding:11px 13px}\n#hmsc-app .inp input{background:transparent;border:0}\n#hmsc-app .inp:hover,#hmsc-app .field input:not([type=checkbox]):hover,#hmsc-app .field select:hover{border-color:var(--hair-strong)}\n#hmsc-app .inp:focus-within,#hmsc-app .field input:not([type=checkbox]):focus,#hmsc-app .field select:focus{background:var(--card);border-color:var(--primary);box-shadow:0 0 0 4px var(--ring)}\n#hmsc-app .inp em{font-size:12.5px;padding-right:13px}\n#hmsc-app .tip{width:16px;height:16px;background:var(--tint);font-size:10px;transition:background .12s,color .12s}\n#hmsc-app .tip:hover,#hmsc-app .tip:focus-visible{background:var(--primary);color:#fff}\n#hmsc-app .step{width:19px;height:19px;font-size:10.5px}\n#hmsc-app .guide summary::before,#hmsc-app .adv summary::before{background:var(--tint)}\n#hmsc-app .adv{background:var(--fill);border:1px solid var(--hair);border-radius:var(--r-ctl);padding:12px 14px}\n#hmsc-app .guide .dwrap,#hmsc-app .spoke-illo .dwrap,#hmsc-app .lp-view{background:#F7F9FB;border:1px solid var(--hair);border-radius:14px}\n#hmsc-app .btn{border-radius:12px;font-weight:600;font-size:15px;min-height:48px;padding:0 26px;transition:filter .12s,transform .12s,box-shadow .12s,background .12s}\n#hmsc-app .btn-primary{box-shadow:var(--shadow-press)}\n#hmsc-app .btn-primary:hover:not(:disabled){filter:brightness(1.1);transform:translateY(-1px)}\n#hmsc-app .btn-primary:active:not(:disabled){transform:none;filter:brightness(.95)}\n#hmsc-app .btn-primary:disabled{box-shadow:none}\n#hmsc-app .btn-ghost{background:var(--tint)}\n#hmsc-app .btn-ghost:hover{background:var(--tint-2)}\n#hmsc-app .w3-btn{min-height:36px;border-radius:10px}\n#hmsc-app .disclaimer{border-radius:var(--r-card);border:1px solid var(--hair);box-shadow:none;padding:18px 22px}\n#hmsc-app .accept input,#hmsc-app .chk input{accent-color:var(--primary)}\n#hmsc-app .loaded-banner{border-radius:12px;background:var(--tint);border-color:transparent}\n#hmsc-app .errors{border-radius:14px;border:1px solid rgba(250,20,20,.18);border-left:4px solid var(--danger);box-shadow:none}\n#hmsc-app .result-head h2{font-size:22px;font-weight:700;letter-spacing:-.02em}\n#hmsc-app .len{border-radius:var(--r-card);border:1px solid var(--hair);box-shadow:var(--shadow-1);padding:20px 24px 22px}\n#hmsc-app .len::before{height:4px}\n#hmsc-app .len .side{font-size:13px;font-weight:700}\n#hmsc-app .len .val{font-size:52px;font-weight:700;letter-spacing:-.035em;margin:16px 0 6px}\n#hmsc-app .len .val small{font-size:17px;font-weight:600;letter-spacing:0}\n#hmsc-app .len .pair .pnum{font-size:34px;font-weight:700;letter-spacing:-.03em}\n#hmsc-app .rcard{border-radius:16px;border:1px solid var(--hair);box-shadow:var(--shadow-1);padding:20px}\n#hmsc-app .rcard h3{font-size:15px;font-weight:650;letter-spacing:-.01em}\n#hmsc-app .subgrid{gap:18px}\n#hmsc-app .sh-facts span,#hmsc-app .lp-stat{background:var(--fill);box-shadow:inset 0 0 0 1px var(--hair);border-radius:999px}\n#hmsc-app .lp-stat{border-radius:10px}\n#hmsc-app .bar{height:8px;background:var(--track)}\n#hmsc-app table.geo td,#hmsc-app .spec-table td,#hmsc-app .inp-row{border-bottom-color:var(--hair)}\n#hmsc-app .spec-table tbody tr:nth-child(even) td{background:rgba(8,68,144,.025)}\n#hmsc-app .ten-warn{border-radius:10px}\n#hmsc-app #lptip{border-radius:12px;padding:10px 14px;box-shadow:0 14px 34px -8px rgba(8,30,58,.45)}\n#hmsc-app #toast{border-radius:12px;font-weight:600;box-shadow:0 16px 36px -10px rgba(8,30,58,.5)}\n#hmsc-app footer.site{background:transparent;border-top-color:var(--hair)}}\n@media screen and (max-width:700px){#hmsc-app .tabs{display:flex}\n#hmsc-app .tab{padding:10px 6px}\n#hmsc-app .card{padding:20px 18px 22px}\n#hmsc-app .brand h1{font-size:21px}\n#hmsc-app .len .val{font-size:44px}\n#hmsc-app .seg[data-seg=\"erdRef\"]{display:flex;width:100%;flex-wrap:nowrap}\n#hmsc-app .seg[data-seg=\"erdRef\"] button{flex:1 1 0;padding:7px 4px;white-space:nowrap;font-size:12px}}\n@media screen{#hmsc-app .wrap{max-width:2200px;padding-inline:clamp(16px,3vw,48px)}\n#hmsc-app .top-inner{max-width:2200px;padding-inline:clamp(16px,3vw,48px)}\n#hmsc-app .embed .wrap,#hmsc-app .embed .top-inner{max-width:none}\n#hmsc-app .embed header.top{display:none}\n#hmsc-app .embed .tabs{margin-top:8px}\n#hmsc-app header.top{border-bottom:0;backdrop-filter:none;-webkit-backdrop-filter:none;color:#fff;\n    background:url(\"data:image\/svg+xml,%3Csvg xmlns=%22http:\/\/www.w3.org\/2000\/svg%22 viewBox=%220 0 400 400%22%3E%3Ccircle cx=%22200%22 cy=%22200%22 r=%22176%22 fill=%22none%22 stroke=%22white%22 stroke-opacity=%22.16%22 stroke-width=%2212%22\/%3E%3Ccircle cx=%22200%22 cy=%22200%22 r=%22160%22 fill=%22none%22 stroke=%22white%22 stroke-opacity=%22.10%22 stroke-width=%221.5%22\/%3E%3Ccircle cx=%22200%22 cy=%22200%22 r=%2226%22 fill=%22none%22 stroke=%22white%22 stroke-opacity=%22.22%22 stroke-width=%223%22\/%3E%3Ccircle cx=%22200%22 cy=%22200%22 r=%228%22 fill=%22white%22 fill-opacity=%22.18%22\/%3E%3Cline x1=%22220.0%22 y1=%22200.0%22 x2=%22262.0%22 y2=%22349.7%22 stroke=%22white%22 stroke-opacity=%220.2%22 stroke-width=%221.4%22\/%3E%3Cline x1=%22218.5%22 y1=%22207.7%22 x2=%22314.6%22 y2=%2285.4%22 stroke=%22white%22 stroke-opacity=%220.2%22 stroke-width=%221.4%22\/%3E%3Cline x1=%22214.1%22 y1=%22214.1%22 x2=%22138.0%22 y2=%22349.7%22 stroke=%22white%22 stroke-opacity=%220.2%22 stroke-width=%221.4%22\/%3E%3Cline x1=%22207.7%22 y1=%22218.5%22 x2=%22362.0%22 y2=%22200.0%22 stroke=%22white%22 stroke-opacity=%220.2%22 stroke-width=%221.4%22\/%3E%3Cline x1=%22200.0%22 y1=%22220.0%22 x2=%2250.3%22 y2=%22262.0%22 stroke=%22white%22 stroke-opacity=%220.2%22 stroke-width=%221.4%22\/%3E%3Cline x1=%22192.3%22 y1=%22218.5%22 x2=%22314.6%22 y2=%22314.6%22 stroke=%22white%22 stroke-opacity=%220.2%22 stroke-width=%221.4%22\/%3E%3Cline x1=%22185.9%22 y1=%22214.1%22 x2=%2250.3%22 y2=%22138.0%22 stroke=%22white%22 stroke-opacity=%220.2%22 stroke-width=%221.4%22\/%3E%3Cline x1=%22181.5%22 y1=%22207.7%22 x2=%22200.0%22 y2=%22362.0%22 stroke=%22white%22 stroke-opacity=%220.2%22 stroke-width=%221.4%22\/%3E%3Cline x1=%22180.0%22 y1=%22200.0%22 x2=%22138.0%22 y2=%2250.3%22 stroke=%22white%22 stroke-opacity=%220.2%22 stroke-width=%221.4%22\/%3E%3Cline x1=%22181.5%22 y1=%22192.3%22 x2=%2285.4%22 y2=%22314.6%22 stroke=%22white%22 stroke-opacity=%220.2%22 stroke-width=%221.4%22\/%3E%3Cline x1=%22185.9%22 y1=%22185.9%22 x2=%22262.0%22 y2=%2250.3%22 stroke=%22white%22 stroke-opacity=%220.2%22 stroke-width=%221.4%22\/%3E%3Cline x1=%22192.3%22 y1=%22181.5%22 x2=%2238.0%22 y2=%22200.0%22 stroke=%22white%22 stroke-opacity=%220.2%22 stroke-width=%221.4%22\/%3E%3Cline x1=%22200.0%22 y1=%22180.0%22 x2=%22349.7%22 y2=%22138.0%22 stroke=%22white%22 stroke-opacity=%220.2%22 stroke-width=%221.4%22\/%3E%3Cline x1=%22207.7%22 y1=%22181.5%22 x2=%2285.4%22 y2=%2285.4%22 stroke=%22white%22 stroke-opacity=%220.2%22 stroke-width=%221.4%22\/%3E%3Cline x1=%22214.1%22 y1=%22185.9%22 x2=%22349.7%22 y2=%22262.0%22 stroke=%22white%22 stroke-opacity=%220.2%22 stroke-width=%221.4%22\/%3E%3Cline x1=%22218.5%22 y1=%22192.3%22 x2=%22200.0%22 y2=%2238.0%22 stroke=%22white%22 stroke-opacity=%220.2%22 stroke-width=%221.4%22\/%3E%3Cline x1=%22219.6%22 y1=%22203.9%22 x2=%22290.0%22 y2=%2265.3%22 stroke=%22white%22 stroke-opacity=%220.13%22 stroke-width=%221.4%22\/%3E%3Cline x1=%22216.6%22 y1=%22211.1%22 x2=%22168.4%22 y2=%22358.9%22 stroke=%22white%22 stroke-opacity=%220.13%22 stroke-width=%221.4%22\/%3E%3Cline x1=%22211.1%22 y1=%22216.6%22 x2=%22358.9%22 y2=%22168.4%22 stroke=%22white%22 stroke-opacity=%220.13%22 stroke-width=%221.4%22\/%3E%3Cline x1=%22203.9%22 y1=%22219.6%22 x2=%2265.3%22 y2=%22290.0%22 stroke=%22white%22 stroke-opacity=%220.13%22 stroke-width=%221.4%22\/%3E%3Cline x1=%22196.1%22 y1=%22219.6%22 x2=%22334.7%22 y2=%22290.0%22 stroke=%22white%22 stroke-opacity=%220.13%22 stroke-width=%221.4%22\/%3E%3Cline x1=%22188.9%22 y1=%22216.6%22 x2=%2241.1%22 y2=%22168.4%22 stroke=%22white%22 stroke-opacity=%220.13%22 stroke-width=%221.4%22\/%3E%3Cline x1=%22183.4%22 y1=%22211.1%22 x2=%22231.6%22 y2=%22358.9%22 stroke=%22white%22 stroke-opacity=%220.13%22 stroke-width=%221.4%22\/%3E%3Cline x1=%22180.4%22 y1=%22203.9%22 x2=%22110.0%22 y2=%2265.3%22 stroke=%22white%22 stroke-opacity=%220.13%22 stroke-width=%221.4%22\/%3E%3Cline x1=%22180.4%22 y1=%22196.1%22 x2=%22110.0%22 y2=%22334.7%22 stroke=%22white%22 stroke-opacity=%220.13%22 stroke-width=%221.4%22\/%3E%3Cline x1=%22183.4%22 y1=%22188.9%22 x2=%22231.6%22 y2=%2241.1%22 stroke=%22white%22 stroke-opacity=%220.13%22 stroke-width=%221.4%22\/%3E%3Cline x1=%22188.9%22 y1=%22183.4%22 x2=%2241.1%22 y2=%22231.6%22 stroke=%22white%22 stroke-opacity=%220.13%22 stroke-width=%221.4%22\/%3E%3Cline x1=%22196.1%22 y1=%22180.4%22 x2=%22334.7%22 y2=%22110.0%22 stroke=%22white%22 stroke-opacity=%220.13%22 stroke-width=%221.4%22\/%3E%3Cline x1=%22203.9%22 y1=%22180.4%22 x2=%2265.3%22 y2=%22110.0%22 stroke=%22white%22 stroke-opacity=%220.13%22 stroke-width=%221.4%22\/%3E%3Cline x1=%22211.1%22 y1=%22183.4%22 x2=%22358.9%22 y2=%22231.6%22 stroke=%22white%22 stroke-opacity=%220.13%22 stroke-width=%221.4%22\/%3E%3Cline x1=%22216.6%22 y1=%22188.9%22 x2=%22168.4%22 y2=%2241.1%22 stroke=%22white%22 stroke-opacity=%220.13%22 stroke-width=%221.4%22\/%3E%3Cline x1=%22219.6%22 y1=%22196.1%22 x2=%22290.0%22 y2=%22334.7%22 stroke=%22white%22 stroke-opacity=%220.13%22 stroke-width=%221.4%22\/%3E%3C\/svg%3E\") no-repeat right max(-90px,calc(50% - 760px)) center\/440px auto,\n               linear-gradient(118deg,#0b55ad 0%,var(--primary) 46%,#06326b 100%)}\n#hmsc-app .top-inner{padding-block:34px 36px}\n#hmsc-app .brand img{filter:brightness(0) invert(1);height:38px}\n#hmsc-app .brand-text{border-left-color:rgba(255,255,255,.24)}\n#hmsc-app .brand h1{color:#fff;font-size:32px;letter-spacing:-.028em}\n#hmsc-app .brand .sub{color:rgba(255,255,255,.8);font-size:14px;max-width:620px}\n#hmsc-app .top-chip{color:#fff;border-color:rgba(255,255,255,.38);background:rgba(255,255,255,.08)}\n#hmsc-app .card h2{font-size:19px;font-weight:700;letter-spacing:-.02em}\n#hmsc-app .hd-ico{width:36px;height:36px;border-radius:10px;background:var(--primary);color:#fff;box-shadow:0 6px 14px -6px rgba(8,68,144,.6)}\n#hmsc-app .hd-ico svg{width:19px;height:19px}\n#hmsc-app .len{background:var(--primary);color:#fff;border:0;box-shadow:0 20px 44px -20px rgba(8,68,144,.7)}\n#hmsc-app .len.r{background:var(--ds);box-shadow:0 20px 44px -20px rgba(217,83,10,.65)}\n#hmsc-app .len::before{display:none}\n#hmsc-app .len .side,#hmsc-app .len .val,#hmsc-app .len .pair .pnum{color:#fff}\n#hmsc-app .len .tag,#hmsc-app .len .exact,#hmsc-app .len .val small,#hmsc-app .len .pair .plab,#hmsc-app .len .pair .pnum small{color:rgba(255,255,255,.8)}\n#hmsc-app .len .pcode{background:rgba(255,255,255,.2);color:#fff}\n#hmsc-app .len-foot{border-top-color:rgba(255,255,255,.28);color:rgba(255,255,255,.82)}\n#hmsc-app .len-foot b{color:#fff}}\n@media screen and (max-width:700px){#hmsc-app header.top{background-size:300px auto,auto;background-position:right -120px center,0 0}\n#hmsc-app .brand h1{font-size:22px}\n#hmsc-app .top-inner{padding-block:22px 24px}}\n@media screen{#hmsc-app .wrap{container:calc \/ inline-size}}\n@container calc (min-width:1260px){#hmsc-app .panel.active{display:grid;grid-template-columns:minmax(0,2fr) minmax(360px,1fr);\n    grid-template-rows:auto auto auto auto auto 1fr;grid-auto-flow:row dense;gap:20px;align-items:start}\n#hmsc-app .panel.active>*{grid-column:2;margin:0;min-width:0}\n#hmsc-app .panel.active>.grid{grid-column:1;grid-row:1\/7}\n#hmsc-app .panel.active>.lp-card{grid-column:1;grid-row:7}\n#hmsc-app .panel.active>.w3-card{grid-column:2;grid-row:7}\n#hmsc-app .panel.active>.lp-card,#hmsc-app .panel.active>.w3-card{align-self:stretch}\n#hmsc-app .panel.active>.results{grid-column:1\/-1;grid-row:8}\n#hmsc-app .panel.active>.actions{flex-wrap:wrap}\n#hmsc-app #panel-example.active{display:block}\n#hmsc-app .panel.active [data-choice=\"count\"]{display:grid;grid-template-columns:repeat(5,minmax(0,1fr))}}\n\/* full screen width (data-boxed on the wrapper keeps it in the column) *\/\n#hmsc-app:not([data-boxed]){width:100vw;max-width:none;margin-left:calc(50% - 50vw);margin-right:0}\n#hmsc-app{display:block;text-align:left}\n<\/style>\n<div id=\"hmsc-app\">\n<header class=\"top\">\n  <div class=\"top-inner\">\n    <div class=\"brand\">\n      <img id=\"logoTop\" alt=\"Holland Mechanics\">\n      <div class=\"brand-text\">\n        <h1>Spoke Length &amp; Wheel Configurator<\/h1>\n        <div class=\"sub\">Spoke length, build weight, tension balance and lacing geometry for classic and straight-pull hubs. Everything runs in your browser.<\/div>\n      <\/div>\n    <\/div>\n    <div class=\"top-tag\">\n      <span class=\"top-chip\">Wheelbuilding tool \u00b7 front &amp; rear<\/span>\n      <span class=\"top-chip\">All values in mm \u00b7 dot (.) for decimals<\/span>\n    <\/div>\n  <\/div>\n<\/header>\n\n<div class=\"wrap\">\n  <div class=\"tabs\" role=\"tablist\">\n    <button class=\"tab\" id=\"tab-front\" role=\"tab\" aria-selected=\"true\" aria-controls=\"panel-front\"><svg viewBox=\"0 0 24 24\" xmlns=\"http:\/\/www.w3.org\/2000\/svg\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"1.8\" stroke-linecap=\"round\" aria-hidden=\"true\"><circle cx=\"12\" cy=\"12\" r=\"8.6\"\/><circle cx=\"12\" cy=\"12\" r=\"2\"\/><path d=\"M12 3.4v6.6M12 14v6.6M3.4 12H10M14 12h6.6\"\/><\/svg>Front wheel<\/button>\n    <button class=\"tab\" id=\"tab-rear\" role=\"tab\" aria-selected=\"false\" aria-controls=\"panel-rear\"><svg viewBox=\"0 0 24 24\" xmlns=\"http:\/\/www.w3.org\/2000\/svg\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"1.8\" stroke-linecap=\"round\" aria-hidden=\"true\"><circle cx=\"12\" cy=\"12\" r=\"8.6\"\/><circle cx=\"12\" cy=\"12\" r=\"4.8\"\/><circle cx=\"12\" cy=\"12\" r=\"1.7\"\/><\/svg>Rear wheel<\/button>\n    <button class=\"tab\" id=\"tab-example\" role=\"tab\" aria-selected=\"false\" aria-controls=\"panel-example\"><svg viewBox=\"0 0 24 24\" xmlns=\"http:\/\/www.w3.org\/2000\/svg\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"1.8\" stroke-linecap=\"round\" aria-hidden=\"true\"><rect x=\"5.2\" y=\"3.4\" width=\"13.6\" height=\"17.2\" rx=\"2\"\/><path d=\"M8.6 8.2h6.8M8.6 12h6.8M8.6 15.8h4.2\"\/><\/svg>Example report<\/button>\n  <\/div>\n\n  <div id=\"panel-front\" class=\"panel active\" role=\"tabpanel\"><\/div>\n  <div id=\"panel-rear\" class=\"panel\" role=\"tabpanel\"><\/div>\n  <div id=\"panel-example\" class=\"panel\" role=\"tabpanel\"><\/div>\n<\/div>\n\n<footer class=\"site\">\n  <div class=\"foot-inner\">\n    <img id=\"logoBlue\" alt=\"Holland Mechanics\">\n  <\/div>\n<\/footer>\n<\/div>\n<script data-no-optimize=\"1\" data-no-defer=\"1\" data-no-minify=\"1\" data-cfasync=\"false\" data-pagespeed-no-defer nowprocket>\n(function(){\nconst HMSC_ROOT=document.getElementById(\"hmsc-app\");\nif(!HMSC_ROOT||HMSC_ROOT.dataset.hmscInit) return; HMSC_ROOT.dataset.hmscInit=\"1\";\n\n\/* =========================================================\n   Holland Mechanics Spoke Calculator\n   ========================================================= *\/\n\n\/* ---- logos (embedded) ---- *\/\nconst LOGO_B = \"data:image\/png;base64,iVBORw0KGgoAAAANSUhEUgAAAVUAAAB4CAYAAABCSWFyAACM4klEQVR42uy9d5xdV3Uv\/l1r73POLdObRtKo92bZkiwX2R4ZTIcQwCNDQiCkkBCSvLT3yMsvj\/GQkOSlJ5QQk5AQkhA0DhACxICxNe5Ntmz13qWRNL3ccs7ee\/3+OOdO0dyRRja8h3kcf+5nLFtz77l7r7P2Wt\/1Xd9F+NFV9loApE4CAiAC4Gb4awRAsrMWNxX82vXEapizFUPQgZFiFPBQd3Px\/J6HANiZvddGrVvCWyhdN0SpVB4AbF\/3Ul9X9DZB7zp5squI+B5ncjHathM6t5U+uyq15M61rn7erUhXbzJBdi6TnS0qEDgTkCnkQDgog30nMXD2m\/bQdx8EYEAEyEcZ6HDXuqaqftVrqapxiCuzgxBH9tLZJZZ9RmXqeRx84lxp\/V7GdsW\/17K6TuvsWlgSrqm9CABuZKhKFYZqi+f3PngN+\/hyLwLA\/opbl8KvBADY3r4WTqf6o76zR9F\/bPCVfkdv9roNjtPVxBRxTc2l8f\/LAvFIek9konMvvTDj91ywIAikbovzsgOUyY4AgBsarpMozFov+xjK21j8HZqbG5Vq3qTqZx+FOJr2U6JRQaFAlL9UbL5wsvskUJj0\/9uF0XEvXo5N\/SBe+kfuc8LV1qbQ2Wmzq+7admHxzf87k+t3tPc\/fnz04pndQPvVHUnbdkbnNmsX3fxpmrv2Xa5YtI60A8FBp5Q6vyeH83taQDQMkekfrtZWja4uk1ld95Nm6ZZ\/MPBCiIDYCS3iwIUD6H3uG60AHgHaFNBpZ\/K90LkNXtOKdVh2y93Kr3ivqWha7NJVACsABCsWAgYofmogtIJrLRCu+BW15MadqufEF8Nn\/+3TQEd+7D1n4swBFwSNS7Bu64NR9TxxzkUgAS2IfOVloY4\/+okQ+FW0tit0dZhr37d43YOW6z9p56x7jxSLYllHQnDEKkDfkW6c37sIRMUrrvsruVrbNbo6jL\/hnb9s59\/4xyJiyFmNBdaH9lxw+vnfK+48du\/L\/I4EQNJIzzXLtzxCtfOyzhTh2A8hKG2WgH1Rs1ewx7IpOrN7D9BO09pscr9B1bLfcUvu+F\/OcCQMASAg+N5oH3k7\/2NLAXhiio0l3yFo3vybbtHmjxjHOTiny7jd0r8LnCWSMDwr1OMXhy\/ChjvdYPeL6tgT3y520PFkH9WEQ\/9HTvWH4rq4mgAgqp375qh59ULv\/N5ztRfPHBslAqRDrmr429scCJ6pnLPeBrUCCkmYPDgjFFSQ8bMPAhjG3V+6ivFsBdAFU79os6uYC4lGFMDKkYVSaTGF3mKx59ip+O92yhXvKXE4qRTmuxve3eFqFr\/P1czlCAzYAiDWInIgEAniaIMgyQOhnCUCeSlGqnYjqlo2+tWzP+COPPGzprPz6ZLzv4pDJ3R2wsxfdx3ql8ApX0icLwSAlFHOEMLiIwCApn0vz9kl6y4VzTfadB3ABRGGD2eFUhVke0\/tAVDERx2jg74P0VA745GPmaDlhqVu9uo\/cOkqX0zoExiAGPgZjYqmm+PvuObav2N7O6GjQ8yamxtc7fysTWWFbJYA+EIEEgAkEIGjxkXMzat+CWd2fwitUOiaJjpP7sNVLb7BZpvFhaOKQAw4wMvCFYcPRv1Hd8aZyWWH59Z7Hbo6IJXNt9lMo7hiLg1mQsluCIBQcjhLYlEEEHwiqrA0ZyFAm7lpFWzz6rw3cPY7OL\/vo1HnthchQqD4lHi1uhH+kSed4IB23GsBKOdnNiHKOxnue\/ECMIqPOr76JrcTiMRvXr1MvPRiiYoQiQg2AmANTF4w2P1s7Lz30hXfauu9DgCZdPUNzoUQ5yAuElhrHRwhn38OKJ5Au\/AVUlqCCNC5zXqr3\/JBd8f\/eNEuvOOnTWUTS5QzCEcdnAMECuSUwDIo\/keISJhI2CmQUSIgiXLO2mJkGlesodWv2+Ev3vKO2KG2qRkcVKTq5q6G9gEXOXFGYI2Qc1qGL1J49tBz8fnQKS\/LoRGJX7dmqfPTiyUqCiRkuEgAMRQVhPN93wYA7LiXvy9207aGIEKyePPf2+o5GSmOWoiFuFBgDVsbwfmZVQBSuP8eW4otZ3ztiJ9TTjducUFWYKwVZyHOCFwk4iKJ1zSCsw5S1fx2AJV45GNmms+i5D6US1dfJyYiWAsRI+JcBHGQ\/MhzAIq4+0tqyu92sAOQdpnqBWJDgrMCGwmcGX9JFN+bNRLbbigwoUgUOglzTqKcsWKNqZyVNotu\/DGsfdOz\/o0\/+b9AJGiPj9wfOdUfBqdKJJlMYyMytS1kQ+ZC\/wszfhhbdzAAUF3LbcjWMCAWRPGZS1pxcYik7\/SeGURkjA5yfmXDUibaKM4CgAIRgVjYOehc79NXuS+GCEAU6A3v+VdZ1fq3Yd3iGglHDZyFQGkQMURcjJOyBWsHUg7EDiADETsedgAgYoA8FAatqZqTwvLbv+C1rFgHut\/G0Mg0VxwRiWSytzhWSCIRAkigNagwfAyDJy+97LQ8WXdpatniMvUMcS65ZwIpxcVhor5zz7+iSPjKab9C5zbrX\/f237Sz197hoqIBs4rDNCaQMJwTBNk5fvOyRRCJD+BrueJDFs5LvwXsxREhAck60thPZhZnLWrmzPYWv+YtECG0tqrpDlyvcc1aDtJzIEbAxPF7KWJXhB4++2L5AKCNAYGev3EDgqq5Yq0DE4\/fS3KYCwRCEofQJKUnDASObQka4jRsKFLI2yjTqGXx5o\/pm9\/3RXSQoK2Nf+RUfxCc4qRXO09+XRVPjZ\/ERatXSrqmmgvDQK63a8YPY9OH43SqZvYGp1OAxAGkAA7KY8oP99hThx5LIjJ31ftYfmuLVDR4ItZOKEYQoiJkpOeRUghTdh3aBSBSqVve\/69Yftt7jEoZCodFiDXBgcQKiC28NJOX0QxSZPKswmFmU2BWWlOQUSAtEJGSmRAMnPIVolET1S3KugW3fRYijPZ7p9+T+++xQHWtCyqvi7NUKb2ZAxSggi8DGMXWe9XLS\/m2AgBM3fw18AKQMyLEgEBIKZb88IB37IXdLz8Svsrzs+Nem5q9er6bu6ZD4CycVRODLAGDXGSRqtSmfsV1Ew+CGV8fYwdAuWz9CoEApTUst+A2hE1VCepmfwiAYOsON62NLblhvs3UahHrSncLgkJhGKa\/O7b9rstsvzWGyFT9kg0uVUtjhh7vLcBK4GUIXobhBww\/xeR5TEoRWFkABkJC44c1gUmRLSASCWXhpnerje9qR2enRdt29SNM9f\/YQdCeGObW8ZP8Y+ziKKB0dVzbA5TgqcXa5ZslVQn0dw\/qEy++WLyaE5yM6ymnM1tjbCnBhoSEoIAwvwsYHIgrnVfA9ZJ0WTJ1N4uXAUxRYoxKBEorHrmU06f3PBMCQFeXLYuhdpD1Nv3EA9HCTa+zphjBOQ8UVyEg7Ej7rJxV3HfiFOUHv0Ujl560hYGjOj+CKFur\/FTl9bai8S1S0\/LaKF0NDvMi5JEQAXAQYo1o1GLWipvUytfdZTvo2+ULV20M6bR60dJl8LPzYCNBEryDGGxD6IFTh15RZWLHvRbUQZ7vbQmTvCBGRMiBlSJTeGEEI5euuu4vL0plEBmz9Rf+3tXMy7hw1I59vwlHvUAgXhpeZcOa4gTMfMb2LuL85tUrXFARr+EVUmOBMKyBNLZsSqdb5uY76GwSPLnLbQxav0G0D0QFKf0ytCbKDQ5mju87MhSfRK4sFpvKvg6KgWj8MROloXJDRKOXLjit7NgnehmwUtUSVGUlqIDYEDDGgceDOiEi2MiznLL+7JUfyc5b84Whzm1Hp9z7j5zq9yoCbWO0rqbYeZKDdCAG4BPD7OoAAB9AuhZAmJ0VhF56tud5iOCDxObDJj6BffvCq6SpIK\/iNmgfCIcPjo5evJg4x6sXqYgkk8k0hZnquUnKzoCAmETBgvIDOyek7O6KqV5Xh4iubCVikAgJlYIeRRSN7MrlLl0oe1+trQqd24y+pe237IKbX+eMiyDOQ+IMQdoq7SvuOzlAFw9\/PNz1lb8DMFD69VLFKQ88DOAvvAW3vNtbecefmdoFcygcESEvcVgMck4kVSXcuOyncOA73y4dSlOimi5ANSxeb1N1ArFWiDREAGamXK\/YswcOl42IrgGySaVq57kguw5j604AkZAIaOTi3hmt+7VebW0KnR0mWP+GX7CNK+9yJm9BiCOrGHqZ4OmEBQyn1JsAfDQ5CGbouFsZXV2OGubfjEy1D4EBkZ745pM+jJgoshZVszOybPM9eOnMn6O1ndE1xgKgBGtNOXjviA9ap0AMAA6sFEz++SEM9Zc9iGIsNkC6em38u5ZBDIhY0oEKeg49nHvqn38syTrGfjeom1tv6pfeTs1Lb3OVze9z1XMrxOQtRMYje2JCFIqpmpcqzr3+V3F673+77N5\/5FRfkSNtbVVo+rDg\/nsspNOia8x5tvjNN1SibtZa1M5ZYP3KBUSyUoDFor3qIS8FEHlwUhWCIMzwwlFTc\/zRTQPAi9NQo2jMWIJgDYSgo5FHLSDYeq+e4G+me8AYnZ22uPS1q5CuqYATF+NGAjBIojxkZGgmFW7Gx9hVoKKxEGRviCFPYYBBEAc4tsXcLpS7r7Y2hc77TWrZHbebprV\/Ik4ZuJyOHxaBSOxQ9fmXHpXn\/\/PnwuFzh0AE3PFRPXZPnQDakkhmKxB1dPxbpRnYWbj+nU+bilnVcdGNktiLmVxESKVvBqAmFEWmfD+Tqr3NqYAoGo6rwPEBwVIcPWu7dz8T\/1rnywhY2xjotJi3aZHNNKRgjUNccgcREUV5kMk\/8L3HU9sZ2+91QdPSpW7Ohj+17DmK8iysADiB8ohcGNPTxnyfg62aXQ+AwXwNTiKOal3jsiVOZ4AoN36eiIUon+LDZJy\/RLBwnIatnfWzAP5iihMXASqWVkplY1X8u6XsgYSFIPnRpwEIdtyrLjuI4qi5YeUCm66aJ3HUnMA5LGStiPB\/gGgEzo1\/TwHyfWfPoO\/sF3G464t+w+K\/VCu3\/qOZe\/2t1hhHEI5hVwIJWJwVU9H8ZgD\/HTvujWZ8AP3IqU5jrK1gdH3MxJXlLgBIBQtu3Gqbl2+kVMUdQv5ttqIujSBLpFJwrGIcXATkBG7s1EMUpzM+U\/+ZwYHDR45PDwskaWrL9TeLHyyiwihosOf5K+CWZaEDDqrucEE2Tm0AhkCElEKux+gLew7Z1naNY+cJrdMUKtJ9Cvk6Gx75+mYJsk3OGpcUlCCkwSYC9Z85UPa+tm93IPLs3HV\/a7MNoHyORevYFwss+75SZ174bvHRv3szgBAbP+hh531mCmeyM\/nZBeCNvxIMP\/CJw9Rw4K9pRXO7xE5cx\/iZIec8kJeai7q5s9F39gwuZyfGD3MAP7VFYBKAEQCRJWLN+aFdFiig\/WVSnZJIGJU1W+FnABs6IFkvxYpHLhXV8d0vXhXHvtYDv20NgcjJLe\/\/J1vXUoH8qBXlM5wBE0j1HIGpWwiCxItBIHGRKC+YU9GyevHQmX1HZpzWJvAGWG11MhFWsoAK4PUdH3Tp2kqbruIkUodjKBjjqGbuitTijVsKRI+NwTNJAOC3LLjJpisr4OzYQQRm4jAHr\/\/Uzny5gyiJmmXOkg0uU60h1oI4js5ZgYpDVDy3rxsihE2bVIzHT1y3NsKxWg533ncYjx17o976S49x86p1EkYWRCoGwoUgjpDKzE7Pq2\/IE72SppD\/Z51qHJXu2GFB5NAFVwnU5xe1vgdzFt6GilmbbSq7yAXVEPYAERFyMX4q8UaMWS4LWCJAHMQ5DyIgCNTAsTMGfSNJOiPTPpyNSza4TBPxSE\/R9R3YFTuXre6q+FcCHUi26iZHGpBiwrUjIfaICyO7ipeOHkki7StdBgDchrculFS1wCbOmQRgpVWuN6K+c9+acl+t7RpEJlj3lp+OmpatkqKxUKRiKrc4+GmmS4cPho\/+3Y+DOIS8S2HnfdFVd+aBT0RAO8upL3xOzVn536WyKS0uitNNigkOooLAn7UkHfadBdAOoGNiau78uqVLrF+xGNZMpBKAxIgqjjwavZLUPOFL2mx9zCwwJY4jWWatyGJnvu\/I2e8pntrWFjcarHvbB03L9bdIwRgircWFQl4Wfu\/RY\/bks1+khiW\/C+uskFMAEcRZ51cGhaYb1uHMviMzTGsZRA7ZxU1Op64nZyBJng5iQ8rTuu\/YJ2z9krtsZePNKI4mGRIBYpxkGrSrWf7LwM7H4xSkcwxPparmm8SvAEzkAGISiQOA0W7juo\/sLF\/YS7DgoO5O6NQELFYExJqKgz1B775HcoBg505zmSMUdHYCgMXGjR52Pj8sx5\/+b1Q952HnVxKkdLYTQZyQ0lnKLpgN9J5Lmhh+5FRn7EwfeSSOSomQXtG6ycxa\/tOFVPU7pHL2HGgPIgQHiiBGkThWcARjFYrDkGikF8KHqTDSL86dUL46QPnhPEaHCTYfkcAKKZUtFp4KiRx9jGETSg9N3PDk4XSpmi3wsxB7+nB4at+BOCK4quGXWj8zzq9eH6fsJechBBfBKb85dfsv3ucUExxJef\/BAIGVi1wUpF8rsXEpEECiHJhZTOFodO6lY5fdVyki1LZx6W+J0gITUlwBd4DSULl++Cefel8eGMEdt2t0dc60o8dB7iVQxymY4mFR\/nrYuFNAEliBmF1aV9qwXIW5sxNYcN0SydZTHNWQSrJMRcUcmZFLO5Jn1c28bjNh3e+FoAPKVdQtiovQpTQWAiLY\/OCBadLYV5T2p+vnzY1a1v+RU54TV2AhBkhZtqF2fSd+i0aHD3I4+rvWq6Cx4qmQiOdD0umbAXxlhg6c0NmJ9OI1i4vZ2rSMRZUCQDNHeVCu+6suqB3kernZEbkYLiJARIlEkNrZbwJQjc57BgBQieJmq5uXiVKACccwAWJNZM2u4uCpU2UPogRGcJV1ayX+jFIR3xErBSdP5XK581ftjNq5M0J7O9uOji694jV7kalbgzByIGJK4gBAAVT7qmxb\/b\/jVNvaFO6\/3ybdOIFe\/9afpqbl747S9VtdthbOGgA2JCGfFaBMwcNov6FifjcVB5\/EyOCT6D25055+du\/E42u6XSwCAYBaAQwRDU\/5CzFlJat0aosVByr07wHgrt75NI7rpeZvuj5KZWePRZcJsCfGwdUtaAm1+vmrZzAMJxbO2bhCWvL9RA5M7EaH9wEwk+6rrY1BZNWK17wJNbOXi4ksWBQgIIEl5Su6dPQ\/84efeqbUmvhyDkAJc47gxnN7cRD2IDbkfN\/ZMtSXJDLygrfB84GoVCYWAXvEuYsD\/v4njhsA6Hg5UUj8vYNZa+5w2l9hrR1fdzDIFEH9p\/Z9b6v9iKv9W37uPqlprpWwEB8UzlkOsprOPf\/dcOeXv5ppmD87KgyPSFBZASMx48FJ3PqkVCsALh3kM4GVKFXxOvYy5GxkS7ASFDGG+0zu7OkTQR73S\/2SP6JMhRLnEnASBGutq5tb6a974+vC3Q\/cj40f1Oi8JwIq66H4DrEGEFEgghA7hjCN9u8GYLHjXj0FTyVyqGhuJC+YUhRkEaj+88cMQFdtbgEA3AugQ5wtnANozXhUm9TdnIWM5H9EqZqRM42xPwugyt\/w479ATUvfbyrnrDE6BYoKTkwIUopVFPo8crYXxdGHON\/z77z74UdyuZ7zpbfKZrNNbs1b3kBVlUspXTdXhJcLaAm8lIL2QVFhjgX5c2ze3DfykvhRngpOCtkg3f3l7nP\/9slTx\/54e1ub2tbZKRBxft2C+a6qthY2ggb+ywCYkXEk0IGZteg6SVWCxDmAWUo2wgZijZBRVujKbyckcHAgAROBJaHcCgnYhuBw+BE35b5i50X1c9\/jUtXgYm5c2oIVUWEA+tLBPzEQQtO2l5tCCbEWmcTkIQEpYrED4cjxnil4dUwxg83Ous5BJZAIQI4c\/EAhzO0fxejFl52aJ+tuG5beKKlaQMaiOAGTovyg9YcuPGhifNh9T2y3s8N463\/8vZi98s3WWEMELSICzwcNn3LmxDO\/BRHKEfVqUzgLVitQ6qAgInECStfOWwD4Jzu4cFWsMIGVwormDY5jTN0RARBHSiuO8vswfG6wOHyuVy+56TuorHsjbCFhIRDgDFyQJalb8nMAOrFxI7DzPuiFG5e6oGo2rJtAcdPgqAgaHSjPgU6i5tS89SvDTG0l3ISioBAhCiGF\/kcAyLUVBdVlPkgAUhATFtzg6f7Ern7kVMuD+3EPOoiQuvHd77d1c3\/X1rQstRwApmApzIG9QNFwT54Gu5+kQv+n65\/b\/l\/ngRwAeE03XJdedVebS1e9xmRrlxe1WuyC6oD8FIS82D5LTUAAWCyEfCwNL+INLgMjKRALPOXPfqSn9\/MAsPfiRUJrK6Gry0nj9Te4TE2A\/BAK58+enHHFODF8TtXe4pQHCcOk37mUQBNAIEdOl3Qv4iw1gRjHeqVLfdMMoYnPmgBQTMVRQV\/3kzGeum+8GNR5jwWQonRdK8RC4FTC5nLi+Yyh88cKh594Kn7vay7WEJgFQBYqmI0xaEMgTELMECvHMTg4cBnFKwa461Epmbp5cGaMxSBEogCowsBTrwhPLZ0bs5pbnPLAxpaWUMCaXTHfmzvxzEHMTLfhqh+D1dsFjQubZc6qP7E65RCV0n6yikTj4vHfw8kXd2Fbpw8ghLGHmWSFI05Scsdw1omfmXVh9oYbcf75R8fYC9PCSgkjxVObICEAp8Ya6klBTPHxMRy+79S\/UcPiN4KUyFjOJiwmFJ2tucNvvmFB4b5fPAkAet6qhcVUpcBYByIFcQIWLcXRIvrPJKT\/LlcuapbG2esoqI4DICImgRAzI99bxLm9M22yoCQ79KH0YhaLUhucxPxihglPFIdOH0\/s6lUFA3z\/O6ra2uJOmc5t1l\/S+uP+Xb\/2lFl48z+a2mVLnRWDMOfgQMya+egzO9Qzn1sXPfa3rw2f2\/7vPSvfutK75b1\/7d35oWNu89tfDJfc\/FfRvPVvdzXzVplMoy86KIqTIkxoYYyDuMTmFJgZ0IQb892w0aj0GSuhcXbP8Ij9xvmzjwPAvq4uKXVCUVW2FUEF1EjPcOrE48dnbBwxFavCae81DgRCpGRiLYyVgLQDayusrSgv\/snJTzX+38FapgQvEtOPaKQ\/Zw4\/fiIp0Sd\/oZ0AgT9nxQLJVDc6a2IwMYYeHBNAucEXAETY1jkD\/YJyrAiBblyznlLpZjgXf0WUmhoAyg\/sQUzxUpOKORAEdTffRJ4\/J64wJ\/fFICqOgIZ6Hp8xu2K6qjjAjtNbLQhj6XXCI1ejvQcBhDPTbbhaVNzO6CCnV77hE652XrNEBQERk7OOvEDThSPnq5\/5tz\/Fxg96eOYbDLQpGh06QROL33GA6VyqUknDnHXx+168QuqS7O38zUskXT3LORGHhLFMCmTywGjfYyU4NHXo8W+p\/nPDonXsJEuf6mBdVVNK5swfg59EzNuINY2f\/iRQClwYPF889cy5xITKkv5B3huFGZLkQwIIlCYa7T0X9hw\/OjMxlHaCCHmzV64lPzPPuQkRc9zXC13o3w3AJXb7o0h1bEPbhdBBNpWqnSfX\/9j\/Ns0r3mPT9UA4asmOkDBrEAHERkg0DZ1\/sHjp9KXUjfd8JKpp2SaZ2g2SrkH80IQWoAjGaGLWpH0icQFMERSOWi6MRI7cBTGuT5G6pE0Oxgnd7M7dBvZT7MQFrNV5kz\/3bH5oHxOhU8RhextAQFRRfR3YhzOFF6NC\/6mZpaXtBOkQv37FXJOqbSEbQcA0seGFowKROALR5Iav8WrWBGEfglPeZOI4yClmJcX8HmD00qSTu3UHowtONS5bZYIaj8RagNTEGJfJxGIlM8K5yqfYNG\/5m2xFHcE4WzqICQQyEaj\/7KNTnOOY2tf8LQgyCYuBOO4KU4pGeo3rOfp82YhoprZFJNmmpqZCULkQrgBH43xJiAPBfBmAK4MNvry0\/7o33SNNy+52UWRA0BARUT684mBBXTzQ1gcMYed9QKy\/CxTe8aIYm9B6kx12DvAzoMrGNZOq6WXXPt5bSWd\/FkGlB5GY9C+I4Y3CkEldOrQzAoBfuE\/ncpe6U\/mBz1te+GFHbAFoIQa5iB0ysLWL7k7oGRSlsjdBOOaEkgAER8SMMLcLJRGVybWEUtRcYb3Km5wzse+L8VQHIpYo\/+SM6xCtYHTBUMu61yPbxDBh0tDgYmqWDeEKw1+NbelT9COnGluiAt1v0UGSWv36n7Rz1\/+lbVzY4CLjUBwCmNSkFFesdsQwK+74Pb34xo+YyqZKRwHg8lasMUQckNaKIqMoNwBEo4eRH9zD+dwu5Pv3UGH0JTraFUXAeQBhSVn6w8CctTfdeiwM0qIEjhl8KswfBJCzH\/0oU0eHxClrda0KahY7MeDRnj12pmlp2z5CJ6BmL1tlM7XiYlxPkRDAJGwKxMeefL\/khk6KSJqsc+U9RMmniq9WtnZKZXPK2WJ8elNMDFPFwcemNiPED2WxsrkJWkOiCIkOXOx3nAO5aORlH4pxMYW5bu6PGfIAl2cwxbQ2rRQNdY9G5w9\/I3GOdgokUtG02rJPMEUkTCCB8klMuL944dDpGXarlXN0jM5OW5iz5TpJVVWKyDiOSCAOR4FzR06\/oki4lMlt3+7SmfoW27zuE5GfdQhHGDGJQVhpxtHnuvnsrhHdsn4zjBE4Vh4VHAZOViAagfMqOS6hxlvsIIiCmvUTWSflYaUPC9AFqZ8\/3ykPCPMEAlhIhDUjzJ0aPvHccRABB\/9VAFB0cteD3DD\/l51fSQnkAmEwTNFJVfOy1Io7byp0H9unguzCyBqUGjkAAqxACiOPlD+AY0pT5aLr5uQyNXVwZryLS4TgDNjkv2kBmcHhXbKrDCqbftGxAomLoRQRgfKYh84W1ZHnYrrdyzt0f8icamtrTNkRZPWtP\/MJ07zyA9bPQMKCAUSDp4nmrcAFVeTSVZVwrkiuEJCuVGxHFA2dGuVC4Ztu+Mxj3tmjT+a79+4aiwgmbVfsTO6++3\/5nVhjZ+\/6g7tmB5WBcZFRBBSF0D2aewIAduzYwWhrE3R22tTyzaujVE0jinkQcl+f8cNYwpnSmTeJFxBMOM7bY48o1zcavfS1rwIYmsnSBYtveY1NVaQszHjLIzFxOAo13P1MNM19UXF4oSRV2ImZFxHg8sXleDkyaq3tCh1k9JrXf8jULr1OTNGCS\/cEy0pp5Ac7cWlf92UUmhIkUkns3QHnUPKoAIQg4FzvHgBmRt1qV1h3VVF9a+SngShedxInojIKI3396b6XvjuNPsK1pf1Extz8\/r+y9YsbqThihbVKhEdYbBEyZ9XCcM7qXY40SGLnaeAg0HCcAsTwhOVncRbw9Fog04wO6p62WBUX+gJKVV0nE85eBydMDC7077NAOBYZisASPaSX37SXmqpXIxInSiUOXZykq7XMWvXWlBtVUbZRQ5wFJNlPUlwYEG+o+3ETn9WTKW5xZOmKFfPeTKlKPRY1QwClmXP9NtV\/8rloJkXBjR\/U6KDIX\/e2D9qGFQsQjlowlWzdsmLNA2e\/Xug\/evrVKlr9vXWqGz\/ooeu+SDcu30Lr3vR3dtbylWJCK1GRQdBTn+2J2hDOxQQ1XyulAhq+kNODex81Axe+qvZ851tFFI+j9AQSAXd\/SeHiXkLTPkHnagE6YmcGke3YZ6mzwzYsX\/u2rAIGrMBn4guRwd7R0YcA4NNdXZJ0NpFpmH+dpGtIDZ8dpeMHD8aR1wxI\/yXyeeWs9S7mhXLi2B3YU2Tt00D7CDbO8bD4wemNbbhZI\/+SlXD+FhdUgUxeEh6ggJVCvjfvzhzdNd19kQ5yU59KgWMPVFHbdM2YYmurxiMfM\/DrV6B5zScca0cmz0JcolIxj\/QBp3ffl2DPUyARr2XjQpeua4QLJZF6S1oZDcjkv\/OK7CxZdxNU3RH3f1gCKUBYmBXpfP\/hwanFs5eV9vOat97j5q55pzMFQ8SaRErIJuAcbLoxPikmLLFFLN5EYmlKlOaMSLq6Ws9fv9icerJ7GmI7gUiCWcvnmFTNIlgzjhYpEhInkhuYqM1bymCG\/Uunt4cNSzusYgsRTrqvlDiBTVe8W\/t1N0kc+ZagiRgTjfLn8\/u+HReapqG4Se3C5VZnQOFIIs4DB9YMWzwyfPCJo\/Etdkyv77u6zcPO+0J\/wQ2r3ILrf9\/G+T6PBSLaIx4679y5XR+falf\/7znVUnU\/Sq95\/bvMgk2fs9UtVS7Kx22NVF5Yh4QhcALAkU4pFsvUf+Y89R7\/Tz61808Kl44eGXOkbdsVLn6K0LXVQTrkiidYTNvieZnMGojAwrGnNB8fHTFfv3D2PAHonABlwg\/ugPbhTO5gdGnfkRmT\/jvIVQANRT+7Ik63ki\/JsTgoj\/a9BPy5Q0U7X3H0SNt24IFPWLf1lzcnnOqkMyg2eCmMngiHjx+a7r4oVXWUBRARGqNtiSixkVAq+zYflcvDHfceBnUoXG0+Vmt77FBFAu+2d99nmpaomI\/JTCUFDS+l+NRzD4ZHH3kywZ7t5VigV9+yMUxVKYiJM5Q4klYoDIkbOBvjvC+P6kToYNcCpLuD7ArnJqSxHKu9OGseTZyvelmRMEBYvV1QObdet6z988jLCqIcC02mTUrSVQZ3+XymWC427v2XyZmUiEWqQus58xaZU08+UYoCLzvUFLq6jDSvvB6ZGq\/UOEEiEA6A4ijRxcvalZPuLHNmz\/2Yu+Z3UTVLkwlF4u43ElME0jWLXMsNi8QU470QgIQdqVjNC0C+THRYEmABa3VHLEEBTkoHcaW+OLIfgEFbGSwW7YRWMB75PYN9naE\/f\/NqWfmab9mKWVlEBQfiksVaZqX54pHPhCdfeuEaxvX8UDrVsYKUf92bPxot2dJhg2qgmLPgkppOuexTIIAh5WkmKAycPC49J\/42u7Pzs0NAXxz0CGPHVkZXlxvfrCtHj+0AE5G7Lqhe2BykFkXWguA5zcQj1j47AuwXESIiSYwlI156E0igRgdjPHVGYHts+OGiLbdIqromNvw4PRYiUtGwcK5nJnqsnHxWLbR3ixMHwHLMqyZHEFb5i+XvK3lfNXTidFTXIkI+k7iYkkVEMAVr65Zk1PrXfRxEbRARbN2qp6bEiQrYjnstiEwKWOBaf\/FfzKxVWyQMXakTSmCFggy8\/mOjcuSZX5kw+uJyLJBMtv42q1OQcKRU+RbogKg4erKmr+f4pfaHNXbsAJq2XxmauHwfEjy1b82dy+Fl58AaKfElhYiUGQX3njiOtu0Kw+cV2rbLVd5fpmDnCfTh3\/5zf2zrF8xBMWdArBNJx1iLdtITwOWR8nLPmDgCBwCCuwD8yzSheGznOvNG0T5KsJJABAylcj0jdH7Pc5cV+hzatquwc9s+PdL7HdS0vFmkONbFBjhYnRJblR6r3MckDgiJA4bOH56moEkQkVTtnHlRumaBOBebJRgAgZ2BLg4\/aVrbdbLe8W+tbpN4QkCHJIdGKrj5fe91TUt+32XqZyEsxg4ZDiLWUlCt+cKBo+lnvvg\/w++HTOOryKky2gXoIOdvfOd9bvHtP29IWYpyDFbTd0OIc2Af7CnN\/ef6\/YsH\/jL3\/Jf\/AsDwUCkq7dwmycJe0+KuSUjKd7Y0rG\/x\/VSIyCrSEGGcCsOdALBj61YFtAmk06pZq2+WILOUolFRo8PPzLxSHhu+m7VyqUtVAyYvY+PySCnJDeW4+9QTSRrjrngMoAP+0ttm23RdHZwd59gTEdlQWHB\/2Upo8r764M4DUeP6AlU1pmHHp3UQKYUodLLkxrszZD6aI\/r4eORG4\/xYJCpg1MHB8tf9jJ2\/7g9dw5JGF4aWYJUg7vIk9owKQ4+Pv\/A\/8r2HDmDr1ssx0fExHX7mTkiYiKgQQGTBSlMxv+PSpX0j6Ljz5VlcgqeGmabXunQNwYQGKMkJ+goj50O971v3F\/d9y2JmU2vLpv3qure90TYu+YA1eUuID0sSB1EeMSuN5PC6kqEIADFmXFilROcAwWbq1wBQZYtVSTuorai\/XojGRVRAjpiVmPCFsDh4bCpDpRMASA2e\/mtpWvomy0yXhTGUtPNOjJ4JUUEozD1YNgBIRFRs\/arXSaqyAq5opHTAECsUhsUdf+ErOPsPpkxW4HvLbl2tGhbfZSuaPxDVzlstREBUcGBmgoEIO3gZ1n1HnRx89D2DoAHs2\/Y9ait+9TnVkkNFcOPd95tFt7\/Lgg1sXgnpklBwGTODIS+rVWEAfPbQF\/HSA7+fG+neBxDQ+lGNrg77SsDpxosx929duvKWGq0xEhlREBo0FieHR54cS5haLxK6AD1n0cIo2wCMjpBcOvbSDCLLSRVu5anXOULMkyxVxpUm5AcvjV54aXAMt5o24k2oM40NN0u6kmDduPIPkeL8IOyZo\/unwVMF7cIjHdTvD51\/3NXNv0tsaJE4AYFAYNiptMOy1g6vav7b1ODJv+KzT34319MzkjzrWrdsWo+WVddxpu6nbM3cG42fBaK4M0egEOOIZFgHnjr+9F\/lD3zn09O0vBJEpKJidm0+W1snIgAlE+BEFNkQDrhV3\/LT9zvlxaPqysG9BIGXifzRi93Fp7\/0G5OKObETIlKptyc6nhM8m4UI6+KN7\/0iBUFfMn2u\/NqT7zwRx+de+GTh2NOPob2d0dERQ0fV8+rQvOxz1q8EogIJcVwe1xn4w2d66cLhv4GTWP9ruuwNToiozszb9EviZRWJKU0ViSvz2l+fRrq5jIh0jANXV9dSOrs0ppyOS\/ORCGSk7xBizYfJDJXOTgeQFF\/6xlO6fskwmpdXIcwLaKJznSC9KgIon3jwHLnDL\/SM++UyUfOseQvETwNRcRLyC2etm3\/DX+qFN+QdPIgIkecJ6VQjab9F\/PTiMFsHBwZs0cJaBsURKpyyFHhKDZ13dOTpd0dnnnv21Zz2v1KnSqUIVd\/yvn8JF256FyIXkeS9GHcqg6HGRRfHOqV54MRLcuzF3w0PPvCfYzheV4d9mX3pk01g61aHri5uJv92Jc4akM2A1MmiyX01l7TgdXU5tG0noAvwal4LvwKq71SPOr33EEAzkYkrpex1zsvc4sSO8\/ZAjgmsCv1P2\/Kcv7LUGetS20R7gM3H1ioiII+kMNBjzjzVnRQBpj7E+7YRAKv6T\/+ua1p2l\/HTMonyQgSIYUPK0twVm9ysJV\/AvI3DXBzOibMEP8WiUw3I1CJiDbGhRTTKoCTTECvCnlE68PjY4\/cXn\/7nX5uCo45HeYTOThRb1t5FqcoauHERFRCR2AjStHQZmJfFeOQ0XZpigKAWOPX0AQC\/kcBLsVHFnTgpydYsxpjeLCVUBwOXrWXUzL1KGJy0QhaGISee+Ye4OLMvlmQkMv4tH\/jfpn7ZbESjSZXbAvCsllDzqZ2fLOz59oz7JnXT8p+06co6iZwATCAhOOeQrvLsghvW4+QTZ8fEZ0pQDDqtV7n8TZKqqqN4VphK2B3EUQE61\/OQKd8OKgmuOYih8\/\/Os5Z\/oMRZLb8K5IjBVBw4Fg2f3F22eymOmpWI\/\/bkcCwVYwHn4IIKJQs3vRkUT0wlieU3CYlukIsgJjJJEVcl6mYCUlalMpr7T\/Txocd\/onj00W+htVWjs9PgVX7pl+VQY8xJ9Kaf+JIs2LxNIhMRIk9IY3J7JaMkQwYdkDJFxede+lTU9TcfATCaOFP3vXCmJWdHHR1uaRAsac76tyitUeNI+SkP50b6zh7p6+sew1O3twkIcFXNi4R9EOHhUYxemBmNI07ZvXnrZ0umtoqsk1jnInFi1oiJCg\/OCEqIqTPE1XPmGKGxUIJAVlhrCQtPA7hUpghQik4s2rarfOe2p\/3K5n\/kJbf9tJNcSM7641oDBBJREhadZRZk6ispW18pzPGDYcWJdY6iAoORGH4MosJLk3KhR8ce\/1z09D\/\/LNq2qyTllOlSc1e74DrnZQFTmNruYELniByJG18vmRCLEoFgDdGQRjEhgI9HZAQR8RdsXGjTtbNhzZTyvoCA4qgFkUwD5QNwAp0hN3rhfHTm+UfiLOkioavTBKtf+0bbvOLnnCmMacdCYCkItDuza0+059t\/iLZ2H8fOCypmT5+BrJhD6H\/QuSHeD1JbkvtXiWNyLlWlqWH+Upx8ApOmJpRkKOtarpegGglmnAQlWkl+qIhLJx5P9n7q53fGGkM0cOLPOLf2PRLU+yL5JHmRcqcLxESPARjFts7LC5ml5c1IRcP8yQpsY1Ae4kJmImtc4p87iSN8EiZAJzCGA8iRDjRDNJ976Sk68J2fKXYf3j+jcec\/tE61tV2hq8Nk1r\/9b8zim7aF1kaA8aZUOsEgIZBEVoK0UoMXhM7t\/rlo13\/8fUyJuluhs+N7uoglNc\/FmUz4yMBg586+4XRKqUtVmVTuuz2XTgIw4\/QecplMw+zQ89eQzcMN9h3GTBV2kootNy650WaqOZ48ioS\/SIryQ0TnTx50wNXGhDCIXHX1rIV5L7M4VtaPIy8hAksElR\/aEV7tvjq3lYoUv65SVYt1y+o7TGQjsqESLrHuHUqC13DFSXiEEBggFo4fXgI5MJi1r7jvtKGTO38nPPDgH0\/A8ORKVCeVSt3sJmGBkx5TBoQnicvQxJ8CIU0U5ZXJJ7ShUkSWRHRSO+8mpGqVOGulxOcdewPBGO8R09WP2BBDc3FkjwXyaP2oju+d6mjeuk\/boMpRmGNhjn2MDqCGeyxOvfjLAIXAPoWdV0lRm7YrdHZavu2Xztm6FhBxIkgT18WE0+Bs7cppYaXKuuus0hCTj+liCROEcwPnCt37z04PK3U4tAtHHbTXm3fjCzSn6RYUYYVElQlBhG0I7u85UD4AiOleqeU3X2fSqVpYKygHI48VaMdOLQjHmBgETuLol+GlmImZBrov6J5DHy8++2+figtsbeqHIUJ9eU41wdH8NW\/4nWjZll+MoCOyOS8eIyFlrNcYpKq0vnToPO958APF8y99ayzV\/z7gJh0JvvTt\/v7T3+7v3zYd2JWcii5qWX8H0rVVyA84Gjn7JK5RYcdUz15rdcogLJSqEQIdEMLus7Vnnnz+UozpuSswCBhdXS7fcv0Gk6pJAbYYp90CYeVQGHHUf3bXDO5L0LnNgWjAPvKpt\/q3\/fxn9exV99ggBZjIjo\/MEJpCxpDkH4IDMaCUJtaKc71A94Gvy75HOqLeg89N4HxOjyN2kAMqGp1OXy+StPTgWnmiDiAtnO\/Np08\/u3t4YkQ2JoVXuVU830hUMIlCzbWaiiE4SN+5M7EjA6ODjL+h7dNh3dJFEuWKCa7tAGWZOcDZvf9kTjzVNWNCeuKgdP7Si07sOxzITLBAJxADP9g48TACUJoBRSbbsFCcNQQVA8\/xtATF0dCTuFz+8fIrjuyFe8\/8i2teutmxF0GsXL7xYOWQG7Rq4Oh3E2KNKxc8mMoFt7tUrYEzBuCr+4z4wGQwM1iTImEqhuC+U8cwev4fw8e\/8DcW6EkOBn61Y6gv36m2tsYOdfntb5clt308UmkDM6onr\/E4lkqwhoKs1md37+Wn\/\/VNhUL\/6Veg53nNEIVMnBt+8SJ1NjXJtrHNSyr3NXOvd+lqcO+pYnhuz4vTplSXO48YZ6rQYn9SFGtRrEEaEAvyU6D84NFLwAjaOhU6r1SBju9DVda90WVqFcIRJVrFxHGdhho4I8VzB16a4X1JgncNh4999t3eurf9F81d8euUbVwvfmUcHTmHsepvCXUlIpAHIjBsATx0LsfF\/Jfo5K5\/Kh7t2jFWESeyV6maMzo7bXr+6lZT3VjLxBAvwLU6PBIL9lKgCxeHh3vOnpgUkZVEPapnrYX2NLtQx5DTtXyGAMSaTR5upPeheG07wmDFa34ai2+5B0Jg7QdCDLIO8H2tzu3vi57\/+m9NOzmivGeLj4ji8D7lrIb2tZRUAMXFMIef2VxT07xgoINOxlhZG0E6bdC0\/DWSqVrhWAHaL8kHaAWBDPe\/cFVYKeasCg7teMCbtUKheYlyUXTZMEIH8jLg3MnR\/JkX95dV8yq1G6czb5JUVnN+WItSV9k\/gbgw1oUIR4pcHDmJYriDBk59Ndz99UcBjIyzLDrtq7nKf8VkaAaJNYM+5vy6ZStl89ueNtVLKlDMQVRZgh4IMORltOre92D08CfuAagPrXf84GAm8cMB7\/af\/7ZZtOW1+tiT56LH\/nYFiEYgMtN5OEFm49vfa6vn2ThtB4hExPOJuw8cyO9+8OkkfbqC0cTV6fSq176dZq9sdC6KRMBE7Ni5gPrPDI\/u\/uYXcW0zekq8YQfA81e9\/i2oaXmXpGs2wg+aRXkVonQsKODgwLjgFQcvOFd8Vi6e3K0OPvWdYvHSkfF1uhdX\/g4ToAzAZefduEYWr7rFcToUdy0qaEmlPixmUByar93Id0d3ffu7SSQ66bunt\/zkO1ymuhphWCI6XNulFLj3TG3+xec\/A5zJA0Dlyta3RjVz1yFdeVacqHjeAonNaLLH9+81h7777BXZBOWfLaloXtpYrF3625SpHKCKutNj70FQCPPkHXviP0a6j1xCSbABcH7z6tVq0catEmRHRRzHiqUkbPKBPfzCd4oXXjo+A5sgAJxe89ofd0HdDaisOzI5mCQRT1PQd+Lc0M6vf3ua70YAJH3DW9\/lKhqbFbwRy+Dp10ABLkQ00AcyudP63L5jxcFTpzGRxldi+LyKZk59P5xqqVsq49\/+s4+blk3rpDhiRSk1RXYpPoUNBVVanX\/pgejhT94NolHI3erlTcv8vlxJ0SO1QG\/9hYN2\/rrAO\/LI58Mn\/umnX629xtNEjvF0hfE9ogpU1EcLllQgVQOgABockkL3\/m4Axcm\/u13F1egfrrTse\/CsyI+W4VpXLWkpR2eJVfNDv4ZXT\/9b2xU6txlv07v+wM69YZ0zOUNMOq7ejostExjijKVUhVbnn388evgzb4+HzP0vBjp+gB7OuJyVWXJrfbGqyUNYhEj4raumVNNBIthaJv2Cm2F0N+4AJ1aAx9KvffKy8ab49yjWNm0D7r\/HjshID06+2DM1SBSKWzp3YHL32st0Pq3tr3wMxpW++3Trda3XJCgqmeRb9u9d435ey3qUj9qm\/51rjfKutlYzsbFrXu8dQFOTIJ6qIT80gcr3JFJNcA+97o7bseSNjxg\/a2AjfZneZ1wQdNYiyCjVvedw6uFPbxkhvpQ41B8szCTBdYNNd\/9StHTrp3jkIvwXvnZ97uyuF4H2H7z7\/Z7v9cTx2B0lIFx+AGzvR1Hgj67vhS3J\/+3P0Vd8g9XbBaAMmjd+RlJVoLAQs84uv1woCKpY953ow64H3jkCugR5l\/rBilAnXzZVtUX8NCgcPVx3dteh3MxEVCZAIm1XxwtfVrrTzmjbd5V++M6p\/erlYI62tjLvM3EWXtvMIKBYBeyVHjaM1lZG04cFq9sEH+MJUwQBfNQx9nXSy0gTy+9FJ64Vvph+T79naWuZvf1+vvcr+x5T1+PiRUoi0Ku9z5XW0r5ix9baqoCtMWviXgiIJwwNZOCjtqQZ8kqx29hmS59VzmZ33MsxbXL8e02\/CQm+6G28+5NuWeuHbRRZgiiZEl+IkE5Zle+F3vefdxWOPNf1A03kTUqp6vW\/9axrXrNJnX7hfvPwX7f9UOGpP0gRRFsb4\/5\/t8l8q4mXf9mfw8k43N3qh41q86PrlZwZ7Yx9a2iaZ9RfAPBJoAJATxk\/do0HVzujrexnpVsAnIkbJMIpEBrdS0CHmyZSbVO4\/x6rm2\/YhFmrP2xBsaBtOQk\/JkuwGid2\/ffCkee6SpqqP6BbwxBx6bnrWiI\/u4xMAWro\/N4ZT04tVUPrls11DQtWGuN8ymQGII7GT0sAnoaOhhGcPrJ\/cPDkAGZW5CAA7C256XqpavQx3Qp6aaRGLgyMHH5k\/zTvSwCkcs7K5WFm9kKbyg5Pub9rutJgGR0I935zP66lWFMqlCWO0a9btkqWXH+T8yrv5CC7yCk9X7wUM5E4E4GcnNLDZ0\/JcP9Xwn0PfBOdnbmkH386eCK+l3k3zNHVCxYAwwDS4wmYHXVmeP9LOBNX9696zZ6d0TUr1kHVMGAAUQSyAu3Iu9RzKn\/2mTN4+cUqAkCZJbfdEKZr\/Fhf3QO8NMzwxdM40vVK3xsVCzauMFXNjcYIQXE0ac+FOCic80eP7nwGwOjM3nZpoFevWA\/2FCSMaf3D\/XMoE1xKmYF9I0d2XSpzzwRAqqvn1xYaW260qaZhSBjfh4OCFNlE6mkceaB4TQ5O7pXx9tn0HH\/1a25FZcVWl2lc6fxsNZHMOsMe6bCQBdkTyI1c4KGL3\/Z3f+2fRzq3XSpNXZjR+paCq04gBcyz697yJlc7fwP7\/moBLT7vZaHEOjg7SCa8QMPdu9X5Q18pEMWTZNvalC67Se3bBR1EsuT6v3LVs4WKIxDmqfqQ4ix7Ga1OPf\/16KWv\/Pn\/QR7qy8RTE8Wd5mXXuXRNNeeGHEb6dgCYmYhKzIJwMm\/Np93yO38M0Ui8JpOQlljE2JFCsSi\/hcGTf1aSCbzCfWl0dRlv6W3v4dVv+EKoYwnay4krIg4cVMGefno7DuOesQm1E\/cuntaqzZwbvuIWbV7twtxYE9kVUaFy\/10cEFSCzu3eCWDThB78qx1ckvBafX\/VGz5kZy15r8vUrncVjZ4oP8EtYi0VW+rAITU\/alwICos\/4c1dd4jP7v6zYkfHfWW7ssb3wgZ18z7rlt\/+ZhfmAPIAsRAvDa\/3kFHHHlleBI5jsmBJ+bWvXPlWrHjNlyx5GCvCEkHbCDL4Xz8F4J9LXNxrtLn4\/Ve89gNmVevfCwUQsbGYSVAB3b3vS+ZI17vL7OXMHCqRQMQvzt3wbdeycp6ExSlNTwLAwkBT9YfMkYc+c8VMsnS\/S+bcg+VbPm9JxbzauCEFyhRhdn3tfQC+MMWuS3KYyze9Wxbc8mkxEcYmBisNL9eD1K6vrx4B9s+gfjFhAnMHUqvecLtrWvJzNlXzY7airgZeKp7plthpIvoDIq4lAGLNG4vz1v9O0HP07xqf+dePnQHyV7SD8WfH+s1LV2PBbe2mtvn1UtlU43QGDi62rfHC\/DwirKWm5a+VOWt+zVt84xNy7LFfN52dz+gyEQajg6y\/+M532lkrbhVTsDLW9jdhVAesQGeJe48M+rv+8+ciolJXyA9wwWErgC5Iuu42pLKQ0f5LxZ5dOyfgTVc24LjbRdvq2auNTjsRN0GvcpJ\/MkZ5mqpmNU\/83GnfNxZVCdziTb8RVTYKijkrRDwlKyAyYOdFYRSLMJeLruPoJB3WzZ9ldcrFPE9+eZVygiGd0hKF8Wjsq83tKhG6iRCsfePPoHnV\/zC181Y47UGsA5wzMLmSk6TLvLhI3OUDqV+wXKrn\/m2mour6HNEvo72dpkSsiWaC1M2fY\/yMg5AT0gwYC79COa\/ya1GxePzqsE68N65q3mak68SZyCAmXAqUT3bwfMG7+OK3Z2gjUw+YHTssFi6skcUbOqJMgyAqJpqBTqB90uxtBVCD+++ZaUZzuSOQTOOCukLNnAZHvoNHZfbb2aLfpILZK37MHHnoM9i6w6GLrrQehIYF61yqSpwzEQQ61kvwYfNDRXVpb9w0MWV+VLyWUdXczSaocaBhC3DczeJlGPmBA4XuvYdmMHaaQRSzUBbedpNetL49qp79Jpeuh7gIcJGFKUrMKS+JR8TrkSjeCpQSWzmrQVU3\/\/alirrXVjzzyTePjKBn2uyOSECk0je84\/dt8+pfNTVzM845wBqDcCSx1\/HPkriFUkBKEFQRtWy4VVW3fDMze89vTwWTV28XAMq1rG6Hn427cMoUagmeZVdknHzhI6OjFy\/gjjte2dTK\/yM+9V6XpNC3gRic6zuF3t7cDAn\/BBGk03XNyNTMgS0wxGiI4ckvxxDLIGFVWZO6eiTTrtC5zfqbt31IapfcgOKoBZyO32fC+zpDENGcGxRcPLIrNurLouvSaOgFN23kVLYOpgA4p+L3sMnLXMNLWBWHmAZOxV08V5rbFSsMWVTOXRa0\/sK\/m1Wv+\/uwcckK68hKMXRwkQBOg0mBRIGE4xcYRCUFIwWBkjDvLCMMF93+IX\/TPZ9BR4e7rPBBIJKKiop6m65eJNYwnFGQiCFCbIuM4Z7dmAmsE4+6BqfTtzkIwSXf3QoRgSnXc3J0dLTvGppCJu4tg0j8+a\/7M1e7tAWFUQexCjAMZxVMkZCuavRq582N+cTt13b4JYVIqV16E6cyaUgkkNJ+T7QdpxEOk6loeL3fsGpZ0hjCV1gPQapisxATrI3fz1oisOLcpRP5fP582fVIxoazTm0iW2S4kg1bMCzrwbPHAFjce8XiKAPkIJINbtz2cW\/Na55wzde9yXpZkXDUwoSCWOBXx5oDsQ1JbEMEIgaTgjgNkxcbFqKoed2NhZt+41vV8+fXJrxtuvy53iji6dYPfSlccedvh1XNGRfmTKyRbDWIJnwWJS\/ENgunYSMWExWRqql3KljLUyKNDnLBmre+Fo0LrnPGOrBTkyMKAhwsvJTmM88\/aQ7u+CzatqtXgcJM0mVUX+nSNYshAML84wDspJn1VzFgu2DDepepy4gYG+dZpUYYGt8rcSSkQERLYmhhjUyLF22FC6qWLHX1S35fYmNSkxVGJryvUiz5kQFzeueLSSl18iGWcAlt\/cIbbbomGZRE44ezOIGwS4Z6XvnlxALMUhjM8aVD3y4fmYw5D42uLqNXtG7ybvmJR8O517\/Tgg2KRUcQRUQMIUBgErFqgL2kjZwNJkiT0vi5rIQVVOCvQDLVdGwhWlsVABSbrr+Lgspq2MgKqySKYEZUAIdDM4F1xlSYbLZ2oYw9cAQwOQKBbPRdxEMK1TU6PIWuDuMtv3Oba1zxMxLlkgF3yX4QA05EMrVMc9csiH9n37U51dJ+N86\/zvlZQNwEcYcJLyISsdZVNylZsP69Yw6\/7HqwoLKy3qUr18BZUCLwQ8SOoQCy3wTgkvW4TEGJBItXtxi\/YmmsN1HCxhiwFpDwPydkPOWfB8ABwYLg1vc+bJbc8TsuU0tSHLJwQoljIwgsiA1ICZSO7YjYJsJGlyV3TEAEJllnXLol2W+aBDEQqRdv+9A3pGX9u6wxEUUFASkNIoJzFiAD0kA8Pl4AmLHWaREhHYgXDQW8\/9v3Fp76wq9xmSiVpGn+79igAiJGIJd\/fyvwAvDQOYuTOz8ch+mvggFdSaQTLF61GX56rhRzcMXhrqtGYJcZsKtvucEF6aQfvZxtCCBM5IBIBwtih9BWLtwH2tYQOjqcW3HTH7qauVkxyVjq8pcDa3A48hyAYbQLTxc5qWz1ujjrmnhbCqRTREHA8FMMP3OVV1ohXcUoDvfl+852Jym7lMfgOkzqurtuw4o7v23qF86S4oiBOB0rZDkIYEl7xH5aK4hS+UGokT54hREwk2Y\/xSTs4BgAGwoyrHKDrE4+9pH84194A9COyZ+\/Nf5RO2eDDSpKAx9BQgJWTLmBXPHUIweunrLHNqEXvXYNBVWNsczemIoZyBRAhdGHZ2wjEx3M9u0Oqdp5tOC6T5tUxsJFPNVexImfgtPpH59oYzO+4sOaJF1xk2MGHKbROyQgGQOtKut+CoCXRJU0dT0EuvG6VQjq6sVaJ4k9ChOJGQWNDD9Vdj1aW+O1DOa9mVLZlIgzsS0LQMxUGIXtP7H7Cgcdgz7mkK6f49\/xvgfNwi03WmsiK5aEWYEcyJGDCkBBRimCVmGO1GgfVK4fSkQpP6tAypFYkBNLOg0m0urcnq+ET39p7eiZg7uT7+wm4fKb7\/kjmbfudTbMhYB48QEdgYgsB1VKM2lVGIQa7YGKImIv0KwUQWBYB6RGexwfePBnwz1f70Db9gmFqiRKTS3essVUz2mVKHSgyY3VsWqbiwd09Rz+ZHhu\/ws\/8MWpy5yiVM+9WdI14JGLo17v4RdsHIFdvUBQUhLSwWsSTczJBZSEqoVSnRERmINGAJUgHpxiwG1tCp332NTCG+8wc1bf7ay1oCRKnfBeE7FSdhE43\/fsNPhmaUBbvZB6kwiBxKlE31JICVH\/+bwORw9b4iQiLH0OlVTpkWivQgiWvJSi\/rPfsIDBRz\/K6Li8sNCm0NVp0os2bo7m3\/JNm2mqlOKoBbNOECcR9kUpUTR0YZAHz33FH+7+enEkd5ByBZuuVMFouubHpHHFO1C\/8HoBDCul6cLBo+rw4x8qnt75nUToY7IZJuNGKFXROqZ0RASBE1aKdDSy31661H3Vaaol7dKmeTdKKsuwLhlSCIBIc24wwrkX98RR+gym605O+42\/+af+2DSsqqfiiAE8mnImiSMhBlXUL42x9TXXhqd2brMVQEOB9B3jkeF0f5kZpmij2pZF3twb3h0R\/fPUQlO8Hrqi\/rUmyECcTWACAUgzj14yOLtvb\/n1SOoGlQ03QaVBUTGpt4uQ1sz5CyP+gSeOmvigm4pptgtmdVC296Z3fMvM2bBUiqMG7DwkxXGSeEqAHr4EjPR9h\/qOP+Tygw\/x8MgwKiuhKqqWusysD0rTireK71lwSqn+U4a7d\/9muOvrfz2JMVKy3c5tNjX\/5i1m9pr\/Zl1k4JwXP9MOIF9YjOLu55\/Wl87+qzHDD9HAkOWGphqTavwAGlrei8alaXXh0JA78fw7i4ce\/m4Jv9eXOR0xLdd90GVqgTDvpkw0EytQaaX6T+T00Qf\/ILw64PyDcyUG66ob14qXAoW5o4UTL5y66riTMeiAHYBKl6peIxMV50vHbDEP8VPJdA9HIg6k\/LqgaVl98eLhwakAeRuATtj51\/+ZZGqAYjGZ9UdCYZ6c8gGlxp0rEVOYB4b7p0lrk9HQNctabEVDgzhTwiYAsGX2tT793N8U93\/7N1\/W+k1xqGC0bxd8asFss3TrV23lnEoUhyxYJ0LMVkinSBUHSJ89+mnZ+cCfFIsXTkwk94XdAIBdwDd\/z7\/1\/f8os1a+T53Y9dnCM1\/6nwboHZOJvNwbxY4yaysaF07cOiJyBGZbHHk+OQhnNk21smaDsAbZMMnoyIE9tmH+lDt34DiupTEkmXHlr3rjm23Lde92Jm+ISI\/BLzRhWxAPeJBsQwsALsn+zQy7jScEFBZsWeoy9ZWw1sVRT6KLknxU6bCMHRyJpKoEc1duw9kXvlCaOnH5ZZoWz7NKAyX1xmQUtRTzJ8Lze46WXY\/4oCOpaFojUMlBB0DgiDwFkh2jwEWUG+qXDFrs3fKz\/yizN651Yc4QWQ3RIDEQlbKKRPHZPc+4U0\/9D3Ni1+Sbju1oP4D\/9Na+8Wdo8c1\/L6NHHsWLD\/xa2HP0+ZKA0qQgpK0N6OyEbV7++y7TqBHmTTx2xgGknZYQfPChT4W7H\/iVaOpnPalb1v+dt2zL\/yoeeOpenH9+JzZ+0EPntmjiycbo+pjJzlrchMqGtzprJcb2Lk9slSXFJH3HP5\/r6TmPbZ2MV4d0F+H+e+xSICDPvwEAKD+4H4DEYg8zMWBBquWGGyiVbYJ1DkREEIAZKgqhz+8XIirRWQiOnXg+27rFsyZisuNFnW3Wu\/6dP+FmrdpkTWjBogANNiHpc3tDYk4calwZJ2am\/MCId\/rFF8umta07GADU\/BUrka0VSGTHRIOJmQpDoCj8GoiB7RKr+8\/k1d7O02LMHeT8pbf9q2laOlvCUSPsq3igDET8LPHwhZD2Pvz+whOf\/3CxeOEE2rYrtLWpGDtLXhs\/6IFIwic+\/4v85BffVHjmSx8E0BtHwR1mqoOJU\/b0gg3rEFQ2jjsTgZAHmCIwcOnFGaTslKTAPoTuiusVlktTpIgYXBh4AUA0MxspRUJtAKrqpGXtJ5yfcXDxewogpDwCFCYmBWQNnJ+Zk25Z31ymiIIrRtkAuH72zRRk42GaY9h5DC0Lqcn0KrHKORBq599cVVVVh8577CR8MV6PDJhvk\/gQKI2qccQEyg\/um2Y9CESSzWYbyc+uEhdhbHIYswgsMDi4tyyemmDPet0bPoy5q+52URgBTguSujenrHaR4oM7Phc+8pnbzYldsaZta6ueZEdtbQoiFO154HP0wr+9Pvrup7ZGPUefR2urLjNAlNC5zWazTbOkoWWDc6EATiVrZ8kLWPWceiLc\/cCvQISnftZ2Zc68+Ezx4U+\/Deef34n2dsbOcW6+HktXujpcsfn697uKplrYKBnJO5l\/ScpTPHQqpH37\/zq+sU68Oq44ijs1f\/USCSoWU1SANzr49IxJ\/0laZOqX3iSpGsBZBxBLzMck2GI\/otFOIe+DJKFzTAwYET\/NKhXMnWKAW3c4vFBdh+alH3cqJYhyBJCjwCOc3f8ctC7Cz96G4oiL1frJgbXi\/ODRkZHu\/vJpbUyFkcpZG6EDoqgQK82LOCjFVBy8EFx64aXC3e9S2LaVgHdN7yjaMN4K21EmQitpQize8gu25YatEoYGFNsSORHxPOcPnbP2wMPviI498U188IMe7ruv\/EDHnWMTIvPF7pceGIdVpuGDJnthK+e8HqksQZxFaf4LiVL5AaOHzj1h2rYrHOsntH24\/Pc89iBjW6fTTSs3uFTlArFmTJMPIJArgvq6D2KmNjIBo9M3\/eRf2IYlixGOJEMcBcSK\/EsHTFizUEFpGgu5xVn201k7a\/V6nHnxsnlVV8+8JKi8U1QA2BJVzQEqBX3piIgXiK2Zz2QKcaRKRHChddWzGooLtr4Lu7\/2d6VJHkAbg8iq6iWbxcuugDUTGAIEOCMoDD1Sdj0S\/m40+6abkK6okLFx7QKQIg6HQSNny2GxjO3bXbqhYU40Z\/3vOU45MgUdT1wQgJRRZDUfeOQz4e7\/+FBsG9tU2XH1nQkE197OxY6OBDb6KJeFJhPYwyy56QaXrqtKgiSOtcBZGALJ9+0DEbD1Xh9dXUWgSyZ9FtoZ7fcCHfdOyeL0hNBdqeqm9zrWQqZAcrlUKpEhrTX1dX8jHNlz4FXV1lmaWFq\/aoPNNnqc6weN9nclvsjNFCqj+nkLYwMewdjsD\/aJCqNnKT\/4JEE+GE88Q6yor32ICtYCuH+sCJGkOmrTPffa2vkLJcoZEDSBHRVHiI8+8+ey7i3tIhOGa0KEwBCxpUq0npLWxnsoLlV9s4sZGpQ8EgKlYItDBwcHB\/tn9MBe7a\/cf78FwDL\/hv9p\/aygmONxLIqdkkipI0\/\/ZnjsiW9idZuP++4Lr\/KOSUU2frBnsheubt5Sp7zx6Z4CB+Wz5PpO5k4+9wJObgMAi53TvoXFzvuARbdvQqYa5IwFSCdIC1NxVNzwuQdnwCCYcNBss2rNG7bKnDXvc7ZoQEqzsyJ+StTguQIdefKXeMPszzrP9+BcKVgR52fgUpnNAL45w2IVJTxduKo5852M4YUgyxapQHFh4B9cQVVx\/aJ3uQiu5NzhLCTIiKuf\/x4Anx2rFSTThXnxDc02UyeQcUcDYubCCGHg\/JNl16NUxJ27dKELKgATjj8DihVGhvPq3N7ncHn9IsGezS3v\/TVXN78WxdAIJz7JiaUg0OrU098s7v6PD6H9YZ3Yhr06TNWmIJ1XUBZL8N+qWfOgPYwfIAQRYeeMUG3LHRlJN+e6OuKEv11KegJJ1Nvh0FF+\/mMc1hK5YP7m+VLVuMrZkMZGJE\/aRsXI98NdPPWJJP3Eq+ZKsCNKZe8gPwX0Hs\/lDjxzKdmEqz8wscMKmO1dEDthoiSEScErDjyPgZ79ZAuw7BM5EzOIiCFB5fxJlJEd99pg3teWStOSXwydsySkBWwpCBQf3\/0d9F\/aK0F6JUwhwd8EYCEyBejB809G5R9yApHU19dXDqYqlsbB29h0VyJnQf3dOrPk9g1huoIpHmM9zePKogWBF\/b0Dh194cgUjC\/puEnf8GNvjRoWz6Mo56TUACGwCALFZ178Vv7gg3+VYKIzbVmWGQiglIpUHnz\/xlhfq7QXEq87+7ODG9\/zpM3U9o7pDdAE1136syOCiCCVWiakIGLjvFzIkVZMI0OXzMmdT89wum5JfChNc9Z+3qaqBGGBiQQgzzKRposH\/qpwete\/6TVv\/jRSNR7EJgVJRyBGwNRaAPSk0SpXpoK5YNZ1i4znrYw5wMkpryBsihAbPoahoSOYm78bKp5KkQSHyplIqGrWrf6i65aHHXQodiixo6EgeD20JkRhyek7aM08ONibOvroweFy65HcM4He5JiT71QKOjRRfvB0vu\/sucvqF4Suj5nq+dW1uZr5P+WcCCiGG0iMiJ8mNXimD3se\/pUkQr0GNbWZdb2JLcyKm2UmoJgEligUWzNvZfT6X37K7z3yCb3\/gX\/JdVD3BNaAigcslnfaOplDI9Iw\/90m2+jBmCRlcRNTfwfPY+q\/sNsefagLr6YCFVCa+xM4lblTSEMVR5+36D9TFjQvS\/Ug59ctXWK9ymUymXYjZAtwUfHR4khfTpkQCFJjgZcQgzIVlWPpWlsngci5LT\/zSVPT4qGQTKHUGmrwPHDgO7\/O626\/xQRVIBtaiWe0CMhjKgzkpPvwsyhXPU3Sr8Hmm9Y5PzMbLhqnZhFYTAhZuPnW0Jmdjq5YJI77mvwU6Ogjfwu88Ivj6WHpAYq7cUz1vA87L8MIh21cz3QAK+LRfuizL\/5\/4TU\/CDPKrxlEVlcsuxlediVEHAEJDsoEW4StnZ9xjUtulhK2SOVj4vFWXAMxYZI7xqPBhRQoGt0JII92d3UbSbKP4Jb3\/b5pWDpfwpwFkRIHh8BX6uL+g9HOr\/weQEW2xSOG9XVA6AAwCRjWwQRV6wB46OD8VYtVCVwXNa24BamKFOx4Zx9BKc4PQg1ePFA48N1n9PzVZ13twrmITBx5EoOcsZJpCKR50\/tw\/KXfRWt82IM6GKnaTUmhKXGMLEQKlB88NAz0llmP0p9rnc5smcRCIHJExGLyjwFwk+ZqJXY12vzmba6quTmGHEnHX11ZDWjVc+RTxcFTx+LM7Hs5GDCGICQqHFE2JCc0ubOXiMSEYurmLqDalj+V5rX\/U+cuPauGLnyN9j70zULntpMTYbCpDuOR3zMAxPlV74sH+MW544QjMa6qioMaHvgXlCf+\/mAXqUSQaWyslcqGJtgILsw9C0CmJyFP5d9h2YaFLltHcb8ljReAijlxA90vZEf7L1BULIJAAopb6MTBsbcQALC6kdDZaYN1d73WNa98g5hi\/CCQWGZR0n34C1H\/6b02U3e7qAlzlyQ+7SXMnSp07z9Vli9aShmrGm6XmLc5ZaNFp2D8KhGvQpyXnf4VVBsHJcbJf5VJ9eIHqHlpo83WbhFbhMQVtWScdYpp5OJDuaNPvxAXMb\/HKlNtyYYuWtniKuohzsjEqawCBmwRrjhipZizEubinxNfpf8W5q0UR61Exk30vEJKSAAqFh4vW1gpl\/Z3dZjM0ltfb5tX\/4a1BQNyigQgpYQLg6TO7fogiPKAEGx0IO5Ajh2TEJG4SChdnfEaly2Pt3ZmnVWqvmmV+GmAnIwFP8ojROGpQu9LuwFY6T27Q5HFeBDkIAIWWLiK+nsABLFDJUH9iqzN1M6NZzZiPHAAgQojTwNALM84aQEIALwFty5Atq5ifOJqPGeQjAEP98d46sVP0aToFgCq5\/4YWAskPu5jhpGv1NCFkfShR\/4eEEpmbn3vrq4uBxDo4N6daqh7FH4GZKfIqJFEoXPOWJutr7ez178xWtr6aXvnL+5O3fFLf+PNWrUGnZ22XCGXIQ7pdP0cqm1sFucEsJepMRiAfdajvU6dfD6Zurjv1SMonJD+TcP6DZKprOTCEHj44rMzxsqaPhwHQkq\/Dp4POJEJzo6pONxTd+DbB3IYvUhEZ2JaBklpbcHebAAZYKsDkLL1q+4TPyOwjgAjotLM\/Wd6\/ZNPfASorySoO52zEJESIipEAA33HgMQazhe7lQTEjjp7B1UGg09JQB1gEQk4ghiyr9cBMAqGr7o6FjCR5wYFScMhqBxyVsoU5MVsePVY2JhG0GGLn0DgLvmKQozuUqHR0XtZuggpihdFobGi0UKhKu8RIE5bped9EQQUTgCXDx2YAYMAgbaUA3UmHkbPmeDaoG1HCcYzpIXKH1u9\/2F\/Y8+gjf8ZRADJMVd5NwkvgBInARVacxZtXYik2PaK3ZIzER3JRDIOMDBBMoPnMSlSyMAoM8f+BxyQw6kecxsiFhs5KSqYWlw3du2lIqeft28OyjI1MIZN57pMKg4Ajdw9pkpjnECC4FqGl6HVJaTwmEyWkwplesXfeFg3Obc1SQTD+d03dwW0uqOSU4cJFCaZOjivoGB7pPJLX+vs2KHtrtVOLz\/MI6\/+Pt+cUDBTzFASXffGBTGEFHORiLFEeucM7ZyTmU477pfpI3veNZf9eZfjTHcyY41djjzrrvNZepr4sWcLMYgYEdKMeX7X8x379kZd\/K8inQuS6T\/qrmbJVVDlBvIq3MvPlE2jS53rW4TADDpWTcKNAhjBuyIFSjM7boAjAqgpDicj1PhEjHHAawb\/TkrW9BBTm\/c9mE3a9liCUMX970rqyRi6j78R7meU+e92QuWu3TtPDgrKLWyMQs5A84P7pgmuqaEGqNcpnqtK0fLEScQSVpQZfrWVMCCPYEt7i0OHT0xpb+71BaZbb7d6cwEfCyO2lEcBgbOPPd9O3iTwwPZhutABHKXCYeQErCe+evy5jWBMJFClOsJB\/Y+MqWwUi4N79xmR2\/c9memefVcifIWpJisFfgp1r3HBtTRx34VbdsV8s8T0KYkP3gCtogx7p0A5Eic54Mrm1aMF1KukHl1kKuurq4yqdqVcAYERaWoksUC4fCjSXFFF88+\/zANnNsD7XGyxyWbcBJUSFQ755dK\/ymqmbvFBRUEobEqGjFryvfldN+RriuuR3XzGqdTGG83JgF7JLn+vnz3noOxSSZYbKnle9bqdTbTUCHOujHuF5EjsZAo1xUzZe5V3xe\/0Nlp0damCoe+\/Ud06MFfUYNnc9rTGjrgsZbX0qEdawooiNPOhuIKIyasnJvC8lv\/Kr3szncCHS6mCk5wqtS4cIXzK2TqyQ\/EbV8OkhvumnHK\/IN0JWkGZ6tugM4AJr+\/MNB9JnEYVy9AdJCrqJjdoPzMyjiSS8jbyenuovDxUjWZ4M7EJXeRpAgB+GlfGpd5qFjayA0Lf09ADjBMAsc6rXDpyOFw11c+A4BozoLFlKkCwTkqOWcilmIOtqd7Z3ln1U6AwF9522JKZWeRLRHMASq1S3tpgp9m+GmGl\/yc+O\/JT9IpzakMq2LhBYz3u8uUlC1dsyjplqHxqJ2ZiqM5v\/\/k4cRqv9dOldC5zdbV1VWKl7pBnIOQ4wTfidv1wxypXD9xfoAoP3jFl8r3E5twakDPHpAbuogLFy5O25o7lvZ\/zKSW3Xo75l73AWfDWAgHDqS0Y1MkOrX7f+Z6Tp1H5zaLrs8XgE6LnpO7KRwVINYqiOGLRAAxXX3dRLrUlTKvwtxbNyCdriFr3Vi0TUQSReAwF6tI7fhpDUCo++g3lTNgYlcip0BEwUbgTP1bUjWrFgCAV9O8UFRSPEv2VZQHcbIr33v6XNnW6ESQxqRq18XdwvG9CJEl0iBrHgcwgrYvjdtSKdDxa++ATsX2Mw53EWwEEhdDdE3fx6w4Sd+Lex78pHriU+vV6Z1\/qIfOHWdxioK0gk4RhOyk5iACEbFGmHNRZZ3Y5hV\/DCA9UZsipi8o7w4QUakSeRmViigaBV84sn8G6dAP2sXoIJcFmiLSt5OLwPnePQAstnWqq9IzkgKQWbh6mUtV1otzQmNte0wUjoKHzj47llOIOztZBFWs+BkVhbnZ\/qpNv2rrF6WlWLAEZjA5mCFW5w\/\/ugWNAALJNN5idQaICgK4hJalmPt7hsyp5\/cmlnAZ6R+MLjjy698APxuIWBNTgwRgDWXywKXjF5xKD0Euaw2fULQRJoHAedoZ1XOkKywHj9wLoQ6A05mKyW20JAARWPeu6jnVs3Nyrf17B+N0dtr83M2rJaioloT0H7fUKlHiSB977IOu7\/wzUErB2vJ7K5pB7Gj0\/HzZ8M4vu6alGlFJc0GESMBDFw5ZgPBRR9MUqRKSf2fKzLvx71y6lhAVKDlInei0UmcPnvCHzr+gN21byyhEIo5MX1+jzY+u4uKItUGlGu8UFoYIrNIbAGRw\/z25q2de9ZvEr4KYopOShq3SinM9I\/rk3j1FAOjKRQCgLh7+Gxo499+imnkpmJwkkReJtQZVc1K8Yv3r8fT+vyPYu8Q6TJC4ExIHNXTxmB3Hl93lRVyvdvFa0Xqts0YmpPFgF4JyA08AkHJwEPnewoS5cVkJRGDZDf8f8RCxApoqdnYewWOf\/51ZwMf7N77zda5i9rs5W\/1aUzmrwUHHNDGKi5kCARSxFCNyVfWLg6Zls4tEx5LvLhpA1vkVK2Pe3GXplIgjVgqFkWHvwvNfLl4tHfqBu+LJqTL\/5habbahGNALKj5YnME9vwGS5cpsLUgJjrYB1QqhXVBjuT\/cdezIskfGtvZg4XEl6aBgAPPY\/7hqWbLI2EoJVELKkA8Vn9j0YHvjWN7BdFLaRdUF2c9zI4ggssfIOaw0XPgmMXrgSW8FWNW2wXhoojmKMuqI1cf+FHq\/rb9ePAhevUlUWAFSc+P\/LVDY\/CvAfqoAxCbaNJagItmpfur4R+d5z3y8YJ8o23i6pKoYJDQAWsBAr4pHefPHcnm+g9\/SMPttr2WQlO4vJWJEJuDDZUOCir8RZ2VZVNpspTRi+8Z4\/cbNWLHdhOFG3gWEKcLXz5xU2zXnKghP1YwAt8UPpdCpuAS3BDwSGM+K87Bx\/0W0t4fHHDk0r4hxX6dMQ976YCuYYpACIA2slNnxp9OKeC0n13qJtuyp0bjvlj176OtXPv9sZtmOHrhN2PklUOet9XrblKZOtaSQjIjQBerJFSDjynbIBVSL67uZdv0KyNT7EjusLEysqDgkPXXx02vpFkC2OEf0nx3HwiVL5mXaXfS8iVoDR2s4Xuj42ip1f\/iqAr2YymdluzdvfT43Lfs1WNswad6wlWo2D8yqNWXWrw8XDY\/6G0bBqjgSZxlg3deoEKigFKY6eGBkZ6X9ZmpL\/N69k5HDYOP9GSlcycv0onj1+5BoxP5G6efOF\/aQvWAAhidv2hk8Mnto9gHtj4RlVHLxAY\/qzNIaruiW3bbbZeoYNYxFd7REP9xo+ufu\/QYSwDa6iorkRXmo9JEo4poldOwsZvHh62kp0nH55SGc3ighISlVbtmCGWPPoKNGFMRpcLMZb5sUY+4npZeE+CWRdcaQl9jWltBNxZ5OXrraNizdgpoMR0c4QoWlbYafiqUC6eouUuJBxjByfc4Whs+g9HR88E1sKL3+tbvOBNsVzFm5BupIJYidALRq5IYrOH4vVlLq2uunSfr10a6ubs\/ZXrI0sYHiSXKM42CClIr8Czs\/A+hUUv7LO+mknVGYCjohFukJJ\/ZwNE2233B4ASLls\/UIRkyCpMYbJxFCjQ3sSqEZNKCyRvXjgH6gwRMwTRJ2ZWIwhCao209yVfw0dTOiUFYBYIz8aUf\/JJ8uvR4z9qrq5K8lLYYIcnoNSJGGhp3DwhZcSxzVlLZUteonxTPSoQkrDhu4tcYQ7w2YIESpXoL3q77W2lqg2LqYOCqGtTaFtu8rlcufNs1\/8I37un1+nh86PQKdjUYXENToGyEQcnD08CfflYNaSReSl\/aSDgsolOjTaezpJmV9deGrpS2ZqNtugApwfPovu556fBJpfacG7PmYAVJPv3yTOIRbHdQA7IWZwvn9vgjNrAJAo7IaNxjMgCBwBNsi6uKhIAMEqRYwLB\/4pPPXMPrzpV32AxDYtvx3p6qTFjxIHzoyoCC3hF6c57RlEkpqzpJm8iuVkDYTG0y9yITDUfQAihE2\/oBBjXlT+5cZ\/lic1C8RRLzAihNOJTus4u95Zsek6UPPKuwAI9kJdlYqEjtjJd0wG+svuRSyfmIKfWScxFzJOg8kJkUCN9u8eh3M63LSvxtUO6LQ2Xb\/Zah8OJq7fCATKAxf6L+Dcc6fit++Q8mm\/eDJv\/Wddpk7DRjT+3JTESxI8XWzyMpK8YhHzske3A5QP8vzWKyxarEHRvG4dVTQEcPEzKyX0xRTBuZ4nLqMPWRCJPfT4M9x\/8hx0iuFKBRgBnIFL1fh26U1bLWkALra9uDUaXBw8Ex57+mS832VEVACA3JtE4i7PsdSFCJwfPApcGp0uGHPsLhAuUyAUw5YUpLLpNQD8seLk9GuiJgUH8WfNzBETSaLSNQmzQ2dnqaWa0Nqeii4e202jA99STEyibHJwAKSh8r15deSJBK6JmzY4qp3T5HSayJWRVyQWOAcpDl9bD\/QPypUokSNdeQOIQFFuJ4BhtDu+esQdF4B0yy3LXap2bkyoB5EwGB7YRKDCQFwRvbQv5hxK\/ixsEWO96BMMpZSSk\/KY+04PeHseaYcI4VS3AEBYv+g6F1QgNvik0sTMevRSiKNPHCvLVkg4tK5+2c2Srg9grR3DqIgYYR6ukOtC23ZGY8BobddobVdXfLVtZ0zXHZAUrsiYw3FxaELIJaLEGXF1838+qJq3BPs6Q2z8oJdEvYmhJ8IXbdtj7YCGVbcHr\/nQU8Hy1jeis9PGf3+avSASf+GtCySoXIiYtVMSkwa5UDzW\/xrv+VaegU0o+BWbxBlAuCQaEkf2zj4EoBetH9VTbCSZ0hBs2PZH1LRkmYQFO9YIIgJAm1jQmOKf4NLLJi8DIPn\/U\/IhclCw6dq1k4qCZehLMnflGpuq1YkGRYm2oCQ\/bO35gy9dlokJ7vioBtCLkeG\/IxECJ63AwhBhOBLY7CwX9zbb0uHvwAyOCk8DCKcTUUFVVZ0E1StjAa5xbisAOFt8EIBMV8G3IyOnKQon+x0Cw+SsNCxaHKx98wfQuc3ig3+bCJpg\/NXWptDaroFOC5HA2\/Kz3\/I2tn0aRIL2qznWNhXnOIL06q0fBpABESZErbGydmurwo57iwAY2lsTz6kqHYrimLVIFD6RA86jbfsYVKR5dPBtVnlJv65c\/gUJNgQC76FpcZEf5CJV3Mq30OnUapgiyIVdKA+4l4cOuuBUw8L1JlUJiB3rDXfEjHw\/cOFMbMDpYwIAQX\/3QHFOQVw6RZPfvQTDsCWQlu5Df5DPnz2DrVs11nzYYl8nqLZm1bjeQqKWw56ykdlvBy+cKt\/FthVAF6KaudfD9yFRYYLugGLu78mbvd94Cnu\/cfWe6Wu45Nz+YWpciJhmkIxCj4VlxNY0Z7Dhzd\/ynvny3dHO+3ZdVhWQWFegE971b\/0JzFr1N1HDyirONHxVpap+wu6878tl9XlLxbjapjuRqmQITFxkFYBYSX4IxQsJr\/TKuqfxGJbZG2vzQfWiRNouds5CYGtAvafPln0g29sZHR8zmWWb10dz1\/6yEbLj8o8CIi2soEkkHulEVwqRLFwMaSaUZklK9wYIMssAVKGDhqZg4AkEYoPKuEHEhhRTyyDQAdHI8UJ07qVTUwqaXXFJLDqx58vcvOx3kK5RCZNl\/D5dxMkyJB3OAnYWVBx8omxAlRQO9fxbViCVqRcbyQRaFFEUAiM9T5fFYuP7AfWc+C7m32AkXa3iVluK\/bJzbLS2WLT5z9KmcDF\/3y98pQwOGsOyTevukjV3\/J5tXnOz2CJ81n7YQT8XxzWuTITcpoBO2wrop27\/+e3R3NXv8OoXL4oe\/dxvXTa9RNDV5UCE9A0\/\/vtR\/cKVYovJ9Ia4CMymQHLx2AMTYBYAgKZsdRAL\/AJTR2oq4nBE\/J4Ll\/Lxs\/DquRK1H5m1dK3NNgScHwBdOLF3xgyGJO1wgf8GURoII4rXSQQKrHIDl6IzTx0GCNj5VgvsBOXPdouzgyCugUAm6a4JHPu+4vP7j5sXv\/rXSdHJAo8IgBRRcAucBcQpEENAwhDofO9LFnBl2QqJWLNWwdaIJghnEwDnAC9F+g2\/8VciHDidHr5yCCdwJFaFhdnq\/L5PFff813eSEdETHs743+3giT\/XI73vl0yjBxtKzG2OA3lnIkfN65bwbdWPqUtHPxuMHP3H3OFdpwGQv3BdAzeu2GQr537A1c57regUXKHPSHZWoBZt\/nffud8Kuzr+bDpxaUnVbRHlAVFhrGwO1sS5wYvBsZ3dUfmUfWJkH0\/7bJ57qwTpyiR9ZhIL4RRzMQ+V6\/taNJXKQ9i3hgCpjeZc\/yVT2eBTftSJUnGqzJ7TuR7GqWf\/2NPZIyLERFM6dCDimIidHR2cRwtv\/B1XPZtj+CDmv4k1Ql5mll5863Jz7Inn4mypQyZBIIQUBdmbE456wvkixwSlCoPPG6CMilmHS+ztRerb\/DwWNG5GMRpXkpos8QrAQVgRFYeAS8f3lw2oEqyTs1V3RKk6wBQtAD3GQhjtGfUuHNqVjyGIy9Yivp+og17yV935KFXM2io2dBOKXAQTssnUZ7F865d17cLtFYVLHy\/s39lfKPQhqF6ueU7z+qhhwbtcZdN7bfVcuDBviSB2yW0\/63vpyvCJz\/0UgHDSwdTaqtHVaXxULnv6NT99n5m1equYKMTs63\/Te82Hb6ZLh\/5SnTvwRL739MWgcd4CV792C+rn\/UI4e8WtjtgpU1SWfZATR36K0XvoyMIDD3z6SLzeYw5ZI6hMTfOMCZgYJizKyMXz3yfu4ffvKinnaL0NQQbUe2FQnd71EmbGYChNTiWbrV2fjKWdQPpnJSZ8CkB\/0gstADBy\/nyowkJUdqQyk3CYYzq3+7cBFLFvm0qqheKtunGJDarmxi1+Y9VFgg3BUvjaNNBL8uBU17qgcjXsROV3IjgDW9GQIh28XxD\/Y68QPo1JAkRFeOcP\/kURAPZNmZnkEnWyg8HCY\/9cqGr5WWuLEcF5kmRMJI5dVHC2bn6W6+b+WnF07a\/xsrf2ACDDXINMtRKVAaKCg8kTSGlEObF+pZOVd\/5pqn4OFYj+dJJDjwU72GZq1zqMi5Ak8660ED04gpGemU5OtVWzbxI\/uYekW4OYmPMD\/f7hXXsLl0MtpWr\/hrs\/4pqvW4Fi3ohiDVjAsWVPK+ne\/1nz0gMfmal6jN9y\/etY6ZudDZN7IILAuHS1VvWL1+LYE8+VIvQJEIgLGpe0mFT1gkQghUq2BTEQyNcQt5GXUzFjAI6HL\/2FRPkvOi4hVGUfaQdSSoq5i6nDu58rlis0xXtCguBdMQRjKW5zdzHpPyoeyJ\/dfWZanZB92+J77zn4p6p69p2GtMTwg0vGvhPBRGKDSqKFG7eN5Ae3SfOaIluHSPvMqSrPBlUQGwoVc0JMCqLgTDHC\/A3bfD81O3vq6bf1H3vrcAnrRFeX0bXzt+D6t34lnLWm0UWjhoR8IXHSvHIL1y7cYubfPELa745MNI9SlYHzs5AodGQjdqzj8eXas1446OHcwd8+AhSxbdukgIfFFJYlQhJTA5kYcB\/1Rk6PvuqqUyVMqmZ2sygfVBg9lMv1nL\/SbKfLqolAS0stZWvqyZpxAyYSOAsavrQHsWpS\/H4xQD5MjHOJU3Ul3AriLOuM0t2HH4oOPbK9hCmWKrzizX4DpdKMBCwERKCYOT\/szJn9+yemTJOLFkCwaO0mpCtrMLErZewrGLgoZyTKG0QFgyg\/7UvCXCTGGjfU3Tey5+svTFexRec2QXs7uwNd7Xzx4EUElZ44sSTxOSxEcbEsyoszzthUHVzlrAZX2VTvMnXKCVkJR6zAsbAiEgshJUIC0j4QFRdiskNndJDzKxctJc9fi5ieqsZpLRZq8PwhAHRVzD+2CVIqc2usax1TwSDswArWFF8awlBfYiNurKD2yMeMXnT7Zpq79tccYASJgLuQIz9FqufYqcbn7v81tD+ssfGDXoxdt07zak+hrU1RNHwkySrc5cUqla5ZPzXziteDm1eupkxVPJCudAAzERVHYC+eODRtJtbVYQGh6KWnvsZDvd2kPEUirjxOIWClQAMXBwYxODSFuw4QPsYOgC+Vs5rFWcSxsYDAjglQ0ejTk1gI5WhM7e0c7vrGN+nsS\/+l\/EADiCbB+SQkzsKFBWu8NGzVnMDVtgSuosEz2rMSjljYkIQ5EQI3IgI4ZgBuTpQb9CH3SulZ8Ve03o2NbQ+Z2asbXXHYAtCJkDtLsWgte9ZVNFZIqnqpq6gPLCsrYd4BhiXuvBVhHSnWnpzd\/8fh3v\/693KiKix+RiAOU6OrmF4jpC4NDw\/3jmUarxY8tYMcsk2z4KU3kA0hI\/0HpqUllYMOAKiqVa+Fn65zInZcJIJYFUehB049g4lpYjx2V1xYGBo3DALBCrQPPXTO8oknfgOQcRHiBCPjyvoboVIT2BoCcEA80tsfnX7xXNlKdFK0cDUtWxBUTlB+nxJ\/6pm9iJmh2YbP48qDBR06gHzfmbPq+KM\/7g+cHuJUpRJSETkr45QRpnjUdiQwRYEJBc7EEyVK2p7WOiE25KVZhyOsDnzn44Un\/umXgXYeM9SkGKfmL1mLbJ2GRLYEHIOIuDAM9J56EVfvviF0kKtCVa3zgg3OjU++EEpI7iNn91xmI7GknwjJohs+HVXMCiQm+Se24DkVDrM9\/fxvnwdy2HEvsPO+CF0dBl1d5V+AQWenxWjPsfiw5okMABYhWK22TKqux0YJAGTJf694mQTKTqbck1KUGxgJzu979gqZmKD1XgWcz6ncwJeIFUDOldtiIXbkHCga3AHA4e5\/u0xAqY0hAj3ruuspVdmSHOgsJHF\/iSkKDXbvvupz1tEhaBeOnvpCm+re97Tys54QR5P77wkgKIiNbciEAmsEzqpY4yEZ3SMwpNOkNXveiecfDHd85taR7iOXQERo3y5tAHPjoj90c1f7UiwUaEzetAR\/kIIYFX9G5GCScd+UNFaIsqR88uA8PvfSn0ZP\/9NHkuDITU1KtZfk+lP3IN7FKJFCfzWFqbFT1HM3LnWZmioKc46K+R0z\/vUEOtC1c9dKUGkAikBsAI7AgUNxeNC7lPSGlxY1eRA1yQmwNmAVgcgIcZGVr\/jiwc\/nzu59EW0T1JtiiEG7dP1aAVsCORAbsCoSa0MSPQSgf1KL32VFC6qfM8+qlAElVeWX\/eKIwEYKI8\/gqu3IHQ6trbpw9Nknec+33qS6951WnvbgpWLCuYglJy42xgmuPp4lUBr5a8nPsmKtdc\/BA\/row63FXf\/5u7Ezn0jdibmQpnLuZhdUGIhE4GQvlCdSGB0ILx57tCw7okxkH625cYlkalKAhCBlQWyIyZINrRXztUmRXlsbo4McXd\/2MWpcvFFMVIRSkthCkXxP09lDD7oDD39xxmPaE8fPhcEXYUIL1iYet8yGmC3ERS5VuTCDTPOkccqxBoXY+vnNwp4honhMM3MIFRi20QtXzcQSRoDq3vWPNDrgQH48bjn5\/PGXZyQqOioOf\/vyIszEA53mrFhjM3UWghCkYhtkFhRyxBePPVc+w7rMyXQQQDQaPPTJt\/CpZ\/5LwXjwAwbYQciQE0ux04wV80rFeWEXQxzaQKeJgpT2hk7n9JEnfzt84u9eh\/FmF4eOe9EJsubk3vfQqZ3n2U+nRHuAsIl7\/EVIJD4TkmMFYIFw\/Gwon9jPKm+ke4SOPf5b0SOf+e8J1OTKrTXHRZHpg1ASg1fdlWy6X1PVqiobNOWHGeePPH8Z1eRqaaICBW3ESiuRlAK0gnhKa02D5\/JDQ0O5cmRjGuxhT1z8OyJae6mU7j0yUDjybAfa2xmd25LPb2eIwGtcuIaD7FoSq5jhx5+DlJJIS67vhWnx1NghZ8mZNg2rlbiUIuixFyb8e7k\/X\/7fGCllilpfOHxsRsW8ri6DtjZVOPXcE9FDf30DH33yz9RQd48SURQESoI0k\/YJ2iNRAUEFRDo2TqV9rWxO6d6jh\/3Dj\/1G9OBfbSrsfujR0kTfcjAOpdNbiUgruLG90EprHu09h+FzvVeFdUqRfabhLSpV5Svn\/HgNSCuVCjjXp+yR\/WfGGQRxWpdaen2rP2\/l7wo0FEyQfLZW2g+8nqPAsYd+CyKEzm0zs83O1bFAz4UTx3VxUCmmlBLRCqSZ4bEYTweVjXr1zfPHqX0gdECqq6trVSq9nsVoRS6+F0HgkdN88Xjc1jlFmm\/Sh1uIUO7wE3tV\/8mTyg+8xN7GXyJaKU55uT7G2V0TJqeWOdAVfop9TyuxyZ6w1jrleUPdoT79wnFcrXBYynxEeBjUGz32uTerF\/\/rI\/rS0YPsIlZeoCnIKPKyBO0TlCIoTaR9Ii9g7aW0ItH+0LkL\/snnP4Gnv7ipsPNL\/3sCV1XGectC5vSzz\/kP\/8MmderZz3pDlyz7FL+\/ThG0R1AeifIJyfuzn9KKoXm0d1ife+Zz2HX\/pnDn\/X+GduEEuy\/73Ui9+XeO2MrmJZNbsJAIU6dY9586ZL71xysnEGtfBRBA3OJXs+r2O6K6xTfa4f5LhZe+9s\/Jvc\/k\/mONyLVvaJN0tp5EClI01RBi1kKc63+qcOixxy+jvBAAqVjeutKkqt5iHAmTE1aKqdj7WH7\/Y09jksR46e8vb4iqlv2Yo5QV41KITAVAwiRO9Z\/qzJ958Symtpcmf56VTa9Y9XNU3dQf5vN1cXPCy70cvKho\/O5Lnx8cfHEAM57qObGdMjPbW\/\/mN1Jl7RtMqno96aCBtEdwpg6s+yUsOEh0mPMDj9P5E4+FR7\/7HQA5EAF3311W8Ld0pVbf+RNG186GMwLE87dZ+UzDF\/cWjz72wGVrO+19BgtufiPSNVuEuc+xYoBEs5Dkh4aKh7v+YUKBhwG47IqN62xm3utM0QEsY0pKrHymwfMvFY8\/\/p2rf3bZS2VWveanjZetcY6SnmYHBw2QMtk6+\/nBrv8YmEDJkbq6pVVDc1a9H8w+OwMHRnz\/EfTA+QdGTu3ad\/V9i9chtWTTraice4ux4jCpnkLCLORFhQuj+7\/zL5PS1svsr\/7GN20aNt4dMZIS74kmYl0YODJy6NH\/mLkNTahjxNG5r1a+\/vWoX\/g6lUpvtF5qMby0R6ZQB1KREPWzCU9LcWQ\/DZ7f4b\/wlW8kbdi4crFygq02rNugFq68R1XMuc166UUIfJ+gFYnxRWzehfkhcvZJPXTqieKx3f+BviNnxjD2ziur9JF68+8cdpXNS2FCkXJOdfDcAfNff7AK4zqdryau6g\/S9cO8dlQaejcpCwIqa2trEUWp2UrlLgwODjoAg5Pwsjs+Wn4M9Y\/29f\/Nq61N4f777WTBHmRraxdrowqzwsiGxcELPQBGJv\/edpVkge5l2GpFbW2tSqVSXqFAafSfG+oHCgCK4\/5YGB33Yvq5VxOd6l2\/ftzVL14IU7zMqcLBC5gHzp5zD\/zhCsRKSq8yA2rnuMK+AzPCu8pt8MXVFKfCWy\/DpqY9rRitl\/Wyb51mKmlpk1tb1eUY4njF9irrHXeB4CoanDO8dpSKHPKyHUxrq0LThwX333P5g1E6rGONzKZ9kuDRcm17UQ4nvBZ93wk2MWnNprWRqfs5ER\/tfAXawq2t\/3975x4c1XHl4d\/pvvfOaAb0QA8EChIIjHg\/7WBi7JHtJH4QBz8YubyOXc5uxevUpja1m2zlnYnsVLLJOvZWbbJJeVPJ2psYI9nexIkTx7ETZAyYl8GABsviJUAIBBLogTQzt7vP\/jEzCJBQMLazAvqr0h9I6Orenp5vTp\/uPu2c83Ub+rXP9JXqof6\/eXfRcrYdhu0L6oKu8V7bJVs7on0GnTqVYKh+VFMv0P4jurA+m7n3c12fCFi+Umau\/67alsRNX1zDBZM+Ar\/fMJ22doKZ4Xgkug8nA6t+Mrm\/v7P1Aoc5lss3Mh8qmrNRneVC+9LpRzjySO2rQpLTlp79P8vW6eWSTI4Ht7A0J2N3+\/Jazhc+7ct8AG8Ey+XXl8xpfWnE9lUhutq9cwSf6WqxjhtI5hRPTA\/B4mRfX4vFYhlGqirZdwJ8dtR76t+G3TAQLJoEAGhvt1K1WCyWYSNVidVkVKb27dkHoaWrsSDgTU9\/o9q2mMVisQwnVZnobkKq3zDE4Fk8NsQkwIFw+kCyoWo8WiwWi2VAqomWpkPw+1IkHDrj1MBMDoAZ4GDuFADBTBEFmwKwWCyWc0kV3bsPI9nbkTke4+wVAAT2YQKhCd74OeXpdYcxK1WLxWI5p1SBXpHs3ZY5huOsSJUIRmuE8gUXVi4GAERWCdtsFovFcm6pAsn+tcTgIXcWGGbjhsC5xdXpb1TbVrNYLJZhpXricAOSXQQSQ1SjZUlsADdnGYACvPawgs2rfoCcOijPYrFcnFIlqOaNcdnXeQTSFYMmqwhE2hjKKy6QlYsXpDMEUZsC+MCoTe\/oSJ\/OaNvZYrnopBpdKYGeDvT1rCchmBhnVJBnELQxRueMgfzQ9CUATtWmtLz\/hMbNXgigIFNFZ+BID0SlbR2L5WKQaroAMomu\/b+XyieQzCj19IBVCQOG8XL\/DkAgc8yDFev7BwFAbi7GqOmR151bv7bDW3Tfk6GZ18wdOFokXWA4kx6wWCwjVqqZEmPy0N5fo7ejhx0pmQ2f4UwiAe1rzp8wwau6+TYQ4cxydZb3RCQmAaC\/4pa7VMmUgBo9vpSnXHM\/y9H3ArXGnXfrPbL8qhvTE4m1dgOGxTKipQowYiz6jr3dRl1t20g6TENVWGHD7OWAS6d8BgCj5B9sxaH3i+p0e5viqnsBj4gThk8c7uYD+38YHDejnMfPfxrz73jF+9g\/rgrPvfX6dGRrI1aLZaRK9dShddyx53npJwjCGaq6sGOUb5A\/NuJWLJ6P+qixeb73gWhUoraWA5Mj1cgtjhjd70OGHPS2rUkcj+83H5p5D4dLjQnm9+uiKRGVSE0EwHa9sMUykqXaUKsBgmrd8CSd2HcCwpGDt6wSoJVR4cIAT5j9RYAY0ahtwfduVQBgPXbKN0xOAWCI3eQJyOOHHgXgilElD7IQROwH5ZF39iebXvklmOkcxxBbLJYRIVWAEfmmg56eDvSceJoIBBKD3rQEdjgdrUa9ioXT0tGqHYa+B6FK1N+tw5OWzEFR5RKopBJu0OWO1t3Jbb9pCMy57VpVMLHScL8P6RF1H3sSQArV3xp8ZLXFYhlRUj11PnegbfsP6eQxH0KKs3etMjGgDZvRpS6Pm\/lv6Wh1pl0FcOFDfwCMZPmM73B4tMdGGNIJQufe7wHQJm\/8100gBMCVoutAIrl\/839nRhZ2sspiGfFSRa1BjEVvy5adsvvQ\/0opBMD6zJVTAiAtjdaax1Z9wqv88FLU363T6ygt71KoEvU1OlBVfRMXTl5qfOWTK1zR2dLsb3\/x5+E5S2eZwgnXGpXyhfSk6Dr4DNqb92Q2BVipWiwjX6oAamsIAKH3UIz6TmgSLtGg9y+BVIJMqIB5wrzHAc7J5AVtxHr+UKbNRnPZ9B8YL5eZCUInSR7b8zAAlcov+1cOFjggItnTZrhly2MAgPpGO+y3WC4aqaJeI1onUpt\/+7bsanueXFcw86DcKgsp2O\/XpnTmFe7cT3wF9TU6u9bSch5EYhL1NVrOvf3LqmTaTPb7UuQFXdGxf23ird\/8wpl242IzpmKpUb4vpOvI9nde8g9u344YC7tO1WIZ2QwWYRwC2MkksJfyJ3zaBMIEo0W6NOBpoRYzGeka6YWvlcnu58z2uiPpJVZxG0kNP+6X2P9j7VbMmY8pkZ9pdxQgmLzEcSF3vX6f6tzfQnNuW2nyx08A2Di9HcDetXforrYONNQI274Wy0UVqWaj1ZVS7dm4CW3NLwjhSGJoQKaPAchGqyQIfgIqv0ziishPwRzILLGyaYDhhv2xOgZzUFRe9zOTW+qBE1rKoGNam36Z2PV6Q3DebfdQYcVHWKkUyYDL7c1PpVq27ES0XgD1dhmVxTLi3+Tnki0zB\/JLJ5rFD2z3C8qDlOwVLFwatJKHoYWbI+W+9Y\/5bzz5BSx80MXmJ3zbtEOQaRtnwV1PcdX195lkfwqBUa7TuW9f6I0VC1MO5\/gL7tmux5Tlg4id462d\/uoVc3Gy5TCICHaCymK5CIf\/WVXG41K\/uaHTC+WlUFh5EzMZ0FCRLQuAFUaPucaRoV3mrae3IhJz0NJgBTCEUANTr\/8sV1V\/VbP0SbjCSRyXzq6GB3pbt72FObc\/xWXT5xulUw7Ydfes\/opqffNVrIKDFrvY32K5mKUKxONAtE6qP39nvSwsX8pjysrYV3qQWAkAa2I3ZJxQ6DYnkVyrtz29G5GIg5YWK1YAiEQcrHteBSdeGeHpN\/xC5eQTmSQL13HF7nX\/ldzx0qPujFsfQOWiL2sg5bhuQBzYsiO5+bmHEK1j\/O5zth0tloteqkB60op2agdmNY0a+5AJ5QFGU2YoOmBVAsEYmJwCl8IFnzRdnS9j56ZDVqwZoTY0KK9w6jQz9+YXVV55HlIJTd5oV7RsavQ3rrjDK515BU+99ldqVKEgCJLdR1TwnT9GUr0d7WicQUCDnZyyWC4S\/sIW03qN5ctlqmXLTjq0\/UuO8SUJoQfvkBQAjOBkn1ZFlQXevJufCwbHlaOhQSEScS57oXqF03jeLa\/ogkljOXXSp0COK442dajm9bcD0Ki65kldOClMKqUFK4cObPhqT+vOZlx3nWOXUFksl1Kkmk4DMCIxR294Yo2bW\/phU1xRxcqowflVAkgISiU155eNkQWFy2Rn0wu6qanzsoxYFy50sW6dCgSKp5gln3pJl04rR7LHF17YEV0Hfexp+Lg5uHWbd9Xf\/FRXXLXU+P1J6QYD2Lv+NbXlVw8iWifxu0etUC2WS06qANDSAMSY9C\/vf1kWl0eRVzKGldZDiZWkFlAJbfIqCilvwnLZc7TB7NzSioUPumjbfHlIIhJzsG6FcgvGzjSL7nnVlE4tp8RJxW6OlCc7Qc1r7lO7Xn\/JmXfnV0zl1V\/U2qSEFwzItsZD3pqffdyPcR\/+cxZgi6ZYLJeoVAFGQ1wAW3tFf2qNKBh3L4fyXTKKiYgIjMyMFQAJIiGgktrkleaLgtLlHtNaFf\/VvsyqgEtZFJSOMD+nA+PnfZQX3PmCKZoyjpMJBccVMtEleO\/6+\/TbL69wqm54iKcueVzLoE+ulG7X\/qTc8cdP9vcei6OhUQBxG6VaLJewVAHEGZGIYxrfOOgg2CwKSqImEDYwhpgEnUoBZO1CEKx8zaOKw8gtuleG8vaZ9U9sBQkA3xSX3uRLTIBWMxrr2J1+\/d\/q6Tc8a\/InhI1\/0kA6EMaX1PhyrXn75f8IzfroUnPFNU\/qYB5AZJxkj4tdq+5L7d30+3Sq5Hd2+ZTFculLFUBLi8HCB13dWL\/dDYbakFe+jB1Xw6izVgRkBEskoLUxwTzXGV1ypwyVTNFt2\/8ANKQuqbWs0TqJ+OcMwKHAwrsf5clLvsOhAs1+ChBCAARiBbdj37NcOGW2nrzkKR0qcJmFckzCE02vfcmPv\/oEFj7oYt3zynZLi+VykSoAtG1Oi3XLMxtdx5EoqrjeCE+TUSLt1WwqgDIRKxOZFGsvx6C4fJ5bNPFj0qi1etvKI2AmxOMS8Yt1P3tUghuBmlnGK50yQy5Y\/oKedOUdmoRmrSQEETEAYmLhEArLb0VRxTIOjHLYCOVAeXL36kdS2178djoP+5gVqsVykXPh+\/SjdRL1NdpdsOwRVF73dSVcxVpJEGjo+RUGIJRwA47oOtwnDu\/8durNZ78PQKev1XgRnRQaE4h9C6glA0C6s5d9g8pmfMkfUxFk\/6QCszO4aRkEwUxkQIIl+47cv+kbqfXPfDvTlgZ2YspiuYylChAiMYmGWiXn3fYIKqu\/bhzHsE4SSJ77usyanICUJgXuPLDBObjhy8mmtX8eEPVIlmtMIAKBhloFAIFJSz7KE+c\/rIorF4MAVsoAEEwOhtymbzTDyWHP9Akc2vqF1NoVj2U\/nGxXtFisVNO\/H60TqK\/Rgfmf+HtVvugnJicfSPUbCCF4yMszwMwQ0ggnIKn\/ONz2Xc\/qlk2P+K07tmXES6ipEaivHwnRGyESkaheZTKRKdySqtmYvDjGxZPu0uFCIJXSYC1YOkRGg89ILzOIBZi1gRckt78L1LL1X1Jbn\/1Bph6AshGqxWKleiaRmIOGWiVn37RMTJj\/jM4rDxo\/oQDjDPsnjDGQkqSbQ9Td6uP4kTo6sPlxf\/+bm9N3R8B133TQEGfgrypYAqICkRmE1x5W2bO6csqmXa0rrnsI+SV367zxQaOSho1OT8id4zIEgLTWCISl7D6cwr43P+3Hf\/u0reZlsVipnpdYnbI5izD9xhVcMmWS9pMaxojBKwMGlENGg2E0nBwJx4XsPqyp5+if6GjLE6n4i78G4J8S7PKVEu0\/IjRUG6CW30fJEhAjRFYJoBqnixRA0J18\/f1y3OS7dP6HbtC5pY5RKZBKahZCDntJ1kxwNAUCjuzcu1dse+UzyUNbXs22le1+FouV6vBEoxL19XoUUJS65v5fmLL5Nyk4pwnozJKgBAYje9qyYbAxEEFJjgtK9YJOduwUXUd\/GzjS+OvePW9sBpAY+OWsZBvTz1ASZ9TPYKAWw8iWgBgQjRPaZww8+5kSBYDAqKmLrjL5E2tSBeUfQ7homg6OAisN0inFYJnOG\/M5\/ogBa2HgueSwIWp7a5W\/5ec16MXRbD0A2\/UsFivV8zWrBJ7VACNn0ac+q4smPu7nTQggeVIRa2mEQ8PXWmYGw0AIkAxIgoDs7wT3dzaj9\/gmeeLIKj5xYFXq0LaDAPqGfbTs0zH+UlBL3ripVTx21gIaNeZGDo6OIDx2sgnlwhgFNlrDaAA4Z9Q9cPuGQeno1O06ZMSR5q8mNq78fvo47+US9bZ6v8VipXoh140xoZZMqOLK+Wry1T82xZWLFAUAv18DQoKyo3caTlCGSBgmKUlKIhIgPwn0d\/mi73i74ORGDXcdOlp7pTm5E6prL7XvN4njbUcAKAzsnQUAGSwYV2pyx4cQzJ2qAnllXn5JgQrmTmXQleR400y4SLLjgRkQOsFsjAaEAEgwnUemgVnD8aQE4Bxt3oqmP\/1TsrVxFZgpc8aXnZCyWKxU3wMDucOAO\/fOr9H4af+s8svCrHyGVoYFyb98e5xOGTAZEBkQASQdSCc9P8QM0imQ3w9Wvk8qwSTEYWijwAwCA4JgpCfZqFIWjkdeDrETAKQHQABswEYBrDXYMJjEQLEYArE5a0b\/9PszIAMN6YCcoJQ9bV3yaPN3E+uf\/ncASZs\/tVisVN9nYgL0sAEzwgUls1NVt3yPx066xYRKYPwUg5UBQb6rW2HOhrkMIk7XE4AAicypr6cf\/koDWQU2aUGDNRgMNtmfifREPb279mClIQIknKAQfccgug78D+1+vTZ5oHE3iABeLu1hfRaLleoH83cy61kBQFZed4uomPN5ziu9SYcKwSoJKF+DKJOzzN6WeRe3yACnh9eUli3A6UiXwaD0tFh27+wFPDeno9qsnYUU5ATJ6T8B2bX\/Ff\/Qjkd10+o\/nBahazvct1isVD9oBGKc3d6J4LTqCI+b9Xk9uuSTJlws2SiwVoqMokzUSXxBXuIhHpPO8bPzkTUxAYbJMMmAQ8KB7D0K6j76Mu\/b+nO\/5fVn0kE5C9RS9tPAYrFYqf61iErE6vjUDqXJVy+g0mkP6dyyO3h0SRGkB9YpMLOCUQKZJa1\/vdtlgJHZbCBAUkiSQZD2IXoOnqCuIy+Ytnd+qvauXZ1JH2R3gNmhvsVipfr\/SDQqgSiyaYFwGCXJqjuiTv642\/1w0UcQKggZ6YG1AbQ2gDFgk819niVacx6PxZm1seLMqJXBAHF61kswSBCEIyElhO4H9Xb6SHSvpe7WZ9yNz\/2mH2g9FZnGa8jK1GKxjAypniImEJ1JpxcW8cZPrOKCmQ\/w6LE3c37hJOQU5iEQBguZnqfSGszKCMNgMLMAYCjzTExnPR9nTiVgYmLAAMRgEkRCCJAACwlBAjAaMtUFpPpbcbLnTercvQHH4itTrXuaBz4M6iTq62EnoSwWywiV6mn3c1bxkgxj5dQbFoi88VcjlD8PXs4McuREHcx32AkC2c1aIPCp+aqzcqdEA5lVMmnvKgWh+sBKdyPR2y7Z34je7rg82hQP7Vnzxw6gZyDIZUJ1tURDg52AslgsF41Uz4xeIxB47RF1aunTAIFQ3vgZauysCi6pCEJiGsOZQTmjPWNoDDPGkyuAxMkKMDuQ7nEWspON6ZaO0+L0dEAbtQ9a7fB6O\/bqtqZdieN7DgNInZmeqEtvg22AsUdFWyyW8+H\/AFEE\/DMt7vavAAAAAElFTkSuQmCC\";\ndocument.querySelectorAll('#logoBlue,#logoTop').forEach(el=>{el.src=LOGO_B;});\n\n\/* ---- component illustrations (static SVG) ---- *\/\nconst ICON={\n  hub:`<svg viewBox=\"0 0 24 24\" xmlns=\"http:\/\/www.w3.org\/2000\/svg\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"1.8\" stroke-linecap=\"round\" stroke-linejoin=\"round\"><line x1=\"2.6\" y1=\"12\" x2=\"21.4\" y2=\"12\"\/><rect x=\"9.4\" y=\"9.2\" width=\"5.2\" height=\"5.6\" rx=\"1.4\"\/><line x1=\"7.4\" y1=\"5.6\" x2=\"7.4\" y2=\"18.4\"\/><line x1=\"16.6\" y1=\"5.6\" x2=\"16.6\" y2=\"18.4\"\/><\/svg>`,\n  rim:`<svg viewBox=\"0 0 24 24\" xmlns=\"http:\/\/www.w3.org\/2000\/svg\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"1.8\" stroke-linecap=\"round\" stroke-linejoin=\"round\"><circle cx=\"12\" cy=\"12\" r=\"9.2\"\/><circle cx=\"12\" cy=\"12\" r=\"5.8\" stroke-width=\"1.3\"\/><line x1=\"12\" y1=\"2.8\" x2=\"12\" y2=\"6.2\" stroke-width=\"2.2\"\/><\/svg>`,\n  lacing:`<svg viewBox=\"0 0 24 24\" xmlns=\"http:\/\/www.w3.org\/2000\/svg\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"1.8\" stroke-linecap=\"round\" stroke-linejoin=\"round\"><circle cx=\"12\" cy=\"12\" r=\"8.8\"\/><circle cx=\"12\" cy=\"12\" r=\"2.2\"\/><line x1=\"12.00\" y1=\"9.80\" x2=\"19.63\" y2=\"7.62\" stroke-width=\"1.4\"\/><line x1=\"13.91\" y1=\"10.90\" x2=\"11.98\" y2=\"3.20\" stroke-width=\"1.4\"\/><line x1=\"13.91\" y1=\"13.10\" x2=\"11.98\" y2=\"20.80\" stroke-width=\"1.4\"\/><line x1=\"12.00\" y1=\"14.20\" x2=\"19.63\" y2=\"16.38\" stroke-width=\"1.4\"\/><line x1=\"10.09\" y1=\"13.10\" x2=\"4.39\" y2=\"7.58\" stroke-width=\"1.4\"\/><line x1=\"10.09\" y1=\"10.90\" x2=\"4.39\" y2=\"16.42\" stroke-width=\"1.4\"\/><\/svg>`,\n  spoke:`<svg viewBox=\"0 0 24 24\" xmlns=\"http:\/\/www.w3.org\/2000\/svg\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"1.8\" stroke-linecap=\"round\" stroke-linejoin=\"round\"><line x1=\"7\" y1=\"7\" x2=\"16\" y2=\"16\"\/><rect x=\"3.4\" y=\"3.4\" width=\"6\" height=\"3.6\" rx=\"1.2\" transform=\"rotate(45 6.4 5.2)\"\/><rect x=\"14.6\" y=\"14.2\" width=\"6.4\" height=\"4\" rx=\"1.2\" transform=\"rotate(45 17.8 16.2)\"\/><\/svg>`\n};\nconst ILLO={\n  rim:`<svg viewBox=\"0 0 430 228\" xmlns=\"http:\/\/www.w3.org\/2000\/svg\"><defs> <linearGradient id=\"rmsteel\" x1=\"0\" y1=\"0\" x2=\"0\" y2=\"1\"> <stop offset=\"0\" stop-color=\"#c9d4e0\"\/><stop offset=\".3\" stop-color=\"#95a8bc\"\/> <stop offset=\".55\" stop-color=\"#60798f\"\/><stop offset=\"1\" stop-color=\"#3a526b\"\/><\/linearGradient> <linearGradient id=\"rmbody\" x1=\"0\" y1=\"0\" x2=\"0\" y2=\"1\"> <stop offset=\"0\" stop-color=\"#3a6691\"\/><stop offset=\".45\" stop-color=\"#1f4a75\"\/> <stop offset=\"1\" stop-color=\"#132c47\"\/><\/linearGradient> <linearGradient id=\"rmfl\" x1=\"0\" y1=\"0\" x2=\"0\" y2=\"1\"> <stop offset=\"0\" stop-color=\"#2f5b86\"\/><stop offset=\".5\" stop-color=\"#084490\"\/> <stop offset=\"1\" stop-color=\"#10243a\"\/><\/linearGradient> <linearGradient id=\"rmfh\" x1=\"0\" y1=\"0\" x2=\"0\" y2=\"1\"> <stop offset=\"0\" stop-color=\"#dbe1e9\"\/><stop offset=\".4\" stop-color=\"#aebccb\"\/> <stop offset=\"1\" stop-color=\"#70849a\"\/><\/linearGradient> <\/defs><path d=\"M112 26 A 78 78 0 1 0 112.1 26 Z M112 41 A 63 63 0 1 1 111.9 41 Z\" fill=\"url(#rmbody)\" fill-rule=\"evenodd\"\/><circle cx=\"112\" cy=\"104\" r=\"78\" fill=\"none\" stroke=\"#084490\" stroke-width=\"1\"\/><circle cx=\"112\" cy=\"104\" r=\"63\" fill=\"none\" stroke=\"#084490\" stroke-width=\"1\"\/><circle cx=\"112\" cy=\"41\" r=\"3.4\" fill=\"#E63329\"\/><circle cx=\"112\" cy=\"167\" r=\"3.4\" fill=\"#E63329\"\/><line x1=\"112\" y1=\"42\" x2=\"112\" y2=\"166\" stroke=\"#E63329\" stroke-width=\"1.2\"\/><path d=\"M112.0 41.0 L108.8 50.0 L115.2 50.0 Z\" fill=\"#E63329\"\/><path d=\"M112.0 167.0 L115.2 158.0 L108.8 158.0 Z\" fill=\"#E63329\"\/><circle cx=\"112.0\" cy=\"104.0\" r=\"14.0\" fill=\"#fff\"\/><circle cx=\"112.0\" cy=\"104.0\" r=\"11.5\" fill=\"#E63329\"\/><text x=\"112.0\" y=\"107.6\" font-family=\"Inter, sans-serif\" font-size=\"8.5\" font-weight=\"700\" fill=\"#fff\" text-anchor=\"middle\">ERD<\/text><text x=\"112\" y=\"204\" font-family=\"Inter, sans-serif\" font-size=\"9.5\" fill=\"#084490\" text-anchor=\"middle\">nipple seat to nipple seat<\/text><path d=\"M274 44 V96 Q274 122 300 126 H344 Q370 122 370 96 V44 Z\" fill=\"url(#rmbody)\" stroke=\"#084490\" stroke-width=\"1.6\"\/><path d=\"M286 56 V92 Q286 110 306 113 H338 Q358 110 358 92 V56 Z\" fill=\"#fff\" stroke=\"#C6D3DF\" stroke-width=\"1.2\"\/><path d=\"M274 44 V52 Q274 58 281 58 M370 44 V52 Q370 58 363 58\" fill=\"none\" stroke=\"#084490\" stroke-width=\"1.5\"\/><line x1=\"322\" y1=\"34\" x2=\"322\" y2=\"178\" stroke=\"#96ABC2\" stroke-width=\"1\" stroke-dasharray=\"3 3\"\/><text x=\"322\" y=\"28\" font-family=\"Inter, sans-serif\" font-size=\"8.5\" fill=\"#96ABC2\" text-anchor=\"middle\">rim centre<\/text><circle cx=\"304\" cy=\"123\" r=\"3.6\" fill=\"#fff\" stroke=\"#084490\" stroke-width=\"1.3\"\/><rect x=\"301.2\" y=\"126\" width=\"5.6\" height=\"26\" rx=\"1.6\" fill=\"url(#rmsteel)\" stroke=\"#084490\" stroke-width=\".9\"\/><line x1=\"304.0\" y1=\"154.0\" x2=\"304.0\" y2=\"176.0\" stroke=\"#96ABC2\" stroke-width=\".9\"\/><line x1=\"304\" y1=\"170\" x2=\"322\" y2=\"170\" stroke=\"#5d6770\" stroke-width=\"1.1\"\/><line x1=\"288\" y1=\"170\" x2=\"304\" y2=\"170\" stroke=\"#5d6770\" stroke-width=\"1.1\"\/><line x1=\"322\" y1=\"170\" x2=\"338\" y2=\"170\" stroke=\"#5d6770\" stroke-width=\"1.1\"\/><path d=\"M304.0 170.0 L295.0 166.8 L295.0 173.2 Z\" fill=\"#5d6770\"\/><path d=\"M322.0 170.0 L331.0 173.2 L331.0 166.8 Z\" fill=\"#5d6770\"\/><circle cx=\"313.0\" cy=\"189.0\" r=\"14.0\" fill=\"#fff\"\/><circle cx=\"313.0\" cy=\"189.0\" r=\"11.5\" fill=\"#E63329\"\/><text x=\"313.0\" y=\"192.6\" font-family=\"Inter, sans-serif\" font-size=\"8.5\" font-weight=\"700\" fill=\"#fff\" text-anchor=\"middle\">OSB<\/text><text x=\"322\" y=\"216\" font-family=\"Inter, sans-serif\" font-size=\"9.5\" fill=\"#084490\" text-anchor=\"middle\">offset spoke bed (section)<\/text><\/svg>`\n};\nconst HUBART={\n  classic:`<svg viewBox=\"0 0 460 272\" xmlns=\"http:\/\/www.w3.org\/2000\/svg\"><defs> <linearGradient id=\"fcsteel\" x1=\"0\" y1=\"0\" x2=\"0\" y2=\"1\"> <stop offset=\"0\" stop-color=\"#c9d4e0\"\/><stop offset=\".3\" stop-color=\"#95a8bc\"\/> <stop offset=\".55\" stop-color=\"#60798f\"\/><stop offset=\"1\" stop-color=\"#3a526b\"\/><\/linearGradient> <linearGradient id=\"fcbody\" x1=\"0\" y1=\"0\" x2=\"0\" y2=\"1\"> <stop offset=\"0\" stop-color=\"#3a6691\"\/><stop offset=\".45\" stop-color=\"#1f4a75\"\/> <stop offset=\"1\" stop-color=\"#132c47\"\/><\/linearGradient> <linearGradient id=\"fcfl\" x1=\"0\" y1=\"0\" x2=\"0\" y2=\"1\"> <stop offset=\"0\" stop-color=\"#2f5b86\"\/><stop offset=\".5\" stop-color=\"#084490\"\/> <stop offset=\"1\" stop-color=\"#10243a\"\/><\/linearGradient> <linearGradient id=\"fcfh\" x1=\"0\" y1=\"0\" x2=\"0\" y2=\"1\"> <stop offset=\"0\" stop-color=\"#dbe1e9\"\/><stop offset=\".4\" stop-color=\"#aebccb\"\/> <stop offset=\"1\" stop-color=\"#70849a\"\/><\/linearGradient> <\/defs><line x1=\"240.0\" y1=\"34\" x2=\"240.0\" y2=\"240\" stroke=\"#C6D3DF\" stroke-width=\"1\" stroke-dasharray=\"4 4\"\/><text x=\"240.0\" y=\"254\" font-family=\"Inter, sans-serif\" font-size=\"9\" fill=\"#96ABC2\" text-anchor=\"middle\">wheel centre<\/text><rect x=\"132\" y=\"142\" width=\"216\" height=\"8\" rx=\"4\" fill=\"url(#fcsteel)\" stroke=\"#084490\" stroke-width=\".8\"\/><rect x=\"114\" y=\"135\" width=\"20\" height=\"22\" rx=\"3\" fill=\"url(#fcsteel)\" stroke=\"#084490\" stroke-width=\"1\"\/><rect x=\"346\" y=\"135\" width=\"20\" height=\"22\" rx=\"3\" fill=\"url(#fcsteel)\" stroke=\"#084490\" stroke-width=\"1\"\/><path d=\"M184 123 C 212 131 268 131 296 123 L 296 169 C 268 161 212 161 184 169 Z\" fill=\"url(#fcbody)\" stroke=\"#084490\" stroke-width=\"1.2\"\/><path d=\"M189 129 C 218 135 262 135 291 129\" fill=\"none\" stroke=\"#fff\" stroke-opacity=\".18\" stroke-width=\"2\"\/><rect x=\"168\" y=\"76\" width=\"16\" height=\"140\" rx=\"7\" fill=\"url(#fcfl)\" stroke=\"#084490\" stroke-width=\"1.4\"\/><line x1=\"172\" y1=\"85\" x2=\"172\" y2=\"207\" stroke=\"#fff\" stroke-opacity=\".2\" stroke-width=\"2\"\/><circle cx=\"176\" cy=\"89\" r=\"3.2\" fill=\"#fff\" stroke=\"#5d6770\" stroke-width=\"1\"\/><circle cx=\"176\" cy=\"106\" r=\"3.2\" fill=\"#fff\" stroke=\"#5d6770\" stroke-width=\"1\"\/><circle cx=\"176\" cy=\"186\" r=\"3.2\" fill=\"#fff\" stroke=\"#5d6770\" stroke-width=\"1\"\/><circle cx=\"176\" cy=\"203\" r=\"3.2\" fill=\"#fff\" stroke=\"#5d6770\" stroke-width=\"1\"\/><rect x=\"296\" y=\"76\" width=\"16\" height=\"140\" rx=\"7\" fill=\"url(#fcfl)\" stroke=\"#084490\" stroke-width=\"1.4\"\/><line x1=\"300\" y1=\"85\" x2=\"300\" y2=\"207\" stroke=\"#fff\" stroke-opacity=\".2\" stroke-width=\"2\"\/><circle cx=\"304\" cy=\"89\" r=\"3.2\" fill=\"#fff\" stroke=\"#5d6770\" stroke-width=\"1\"\/><circle cx=\"304\" cy=\"106\" r=\"3.2\" fill=\"#fff\" stroke=\"#5d6770\" stroke-width=\"1\"\/><circle cx=\"304\" cy=\"186\" r=\"3.2\" fill=\"#fff\" stroke=\"#5d6770\" stroke-width=\"1\"\/><circle cx=\"304\" cy=\"203\" r=\"3.2\" fill=\"#fff\" stroke=\"#5d6770\" stroke-width=\"1\"\/><line x1=\"165.0\" y1=\"76.0\" x2=\"52.0\" y2=\"76.0\" stroke=\"#96ABC2\" stroke-width=\".9\"\/><line x1=\"165.0\" y1=\"216.0\" x2=\"52.0\" y2=\"216.0\" stroke=\"#96ABC2\" stroke-width=\".9\"\/><line x1=\"58\" y1=\"77\" x2=\"58\" y2=\"215\" stroke=\"#5d6770\" stroke-width=\"1.1\"\/><path d=\"M58.0 76.0 L54.8 85.0 L61.2 85.0 Z\" fill=\"#5d6770\"\/><path d=\"M58.0 216.0 L61.2 207.0 L54.8 207.0 Z\" fill=\"#5d6770\"\/><rect x=\"41.6\" y=\"135.5\" width=\"32.8\" height=\"21\" rx=\"10.5\" fill=\"#fff\"\/><rect x=\"43.6\" y=\"137.5\" width=\"28.799999999999997\" height=\"17\" rx=\"8.5\" fill=\"#084490\"\/><text x=\"58.0\" y=\"149.2\" font-family=\"Inter, sans-serif\" font-size=\"8.5\" font-weight=\"700\" fill=\"#fff\" text-anchor=\"middle\">NDS<\/text><line x1=\"315.0\" y1=\"76.0\" x2=\"408.0\" y2=\"76.0\" stroke=\"#96ABC2\" stroke-width=\".9\"\/><line x1=\"315.0\" y1=\"216.0\" x2=\"408.0\" y2=\"216.0\" stroke=\"#96ABC2\" stroke-width=\".9\"\/><line x1=\"402\" y1=\"77\" x2=\"402\" y2=\"215\" stroke=\"#5d6770\" stroke-width=\"1.1\"\/><path d=\"M402.0 76.0 L398.8 85.0 L405.2 85.0 Z\" fill=\"#5d6770\"\/><path d=\"M402.0 216.0 L405.2 207.0 L398.8 207.0 Z\" fill=\"#5d6770\"\/><rect x=\"388.4\" y=\"135.5\" width=\"27.2\" height=\"21\" rx=\"10.5\" fill=\"#fff\"\/><rect x=\"390.4\" y=\"137.5\" width=\"23.2\" height=\"17\" rx=\"8.5\" fill=\"#084490\"\/><text x=\"402.0\" y=\"149.2\" font-family=\"Inter, sans-serif\" font-size=\"8.5\" font-weight=\"700\" fill=\"#fff\" text-anchor=\"middle\">DS<\/text><line x1=\"176.0\" y1=\"70.0\" x2=\"176.0\" y2=\"40.0\" stroke=\"#96ABC2\" stroke-width=\".9\"\/><line x1=\"304.0\" y1=\"70.0\" x2=\"304.0\" y2=\"40.0\" stroke=\"#96ABC2\" stroke-width=\".9\"\/><line x1=\"177\" y1=\"46\" x2=\"239.0\" y2=\"46\" stroke=\"#5d6770\" stroke-width=\"1.1\"\/><path d=\"M176.0 46.0 L185.0 49.2 L185.0 42.8 Z\" fill=\"#5d6770\"\/><path d=\"M240.0 46.0 L231.0 42.8 L231.0 49.2 Z\" fill=\"#5d6770\"\/><rect x=\"186.0\" y=\"17.5\" width=\"44.0\" height=\"21\" rx=\"10.5\" fill=\"#fff\"\/><rect x=\"188.0\" y=\"19.5\" width=\"40.0\" height=\"17\" rx=\"8.5\" fill=\"#084490\"\/><text x=\"208.0\" y=\"31.2\" font-family=\"Inter, sans-serif\" font-size=\"8.5\" font-weight=\"700\" fill=\"#fff\" text-anchor=\"middle\">W-NDS<\/text><line x1=\"241.0\" y1=\"46\" x2=\"303\" y2=\"46\" stroke=\"#5d6770\" stroke-width=\"1.1\"\/><path d=\"M240.0 46.0 L249.0 49.2 L249.0 42.8 Z\" fill=\"#5d6770\"\/><path d=\"M304.0 46.0 L295.0 42.8 L295.0 49.2 Z\" fill=\"#5d6770\"\/><rect x=\"252.8\" y=\"17.5\" width=\"38.4\" height=\"21\" rx=\"10.5\" fill=\"#fff\"\/><rect x=\"254.8\" y=\"19.5\" width=\"34.4\" height=\"17\" rx=\"8.5\" fill=\"#084490\"\/><text x=\"272.0\" y=\"31.2\" font-family=\"Inter, sans-serif\" font-size=\"8.5\" font-weight=\"700\" fill=\"#fff\" text-anchor=\"middle\">W-DS<\/text><\/svg>`,\n  straight:`<svg viewBox=\"0 0 460 272\" xmlns=\"http:\/\/www.w3.org\/2000\/svg\"><defs> <linearGradient id=\"fssteel\" x1=\"0\" y1=\"0\" x2=\"0\" y2=\"1\"> <stop offset=\"0\" stop-color=\"#c9d4e0\"\/><stop offset=\".3\" stop-color=\"#95a8bc\"\/> <stop offset=\".55\" stop-color=\"#60798f\"\/><stop offset=\"1\" stop-color=\"#3a526b\"\/><\/linearGradient> <linearGradient id=\"fsbody\" x1=\"0\" y1=\"0\" x2=\"0\" y2=\"1\"> <stop offset=\"0\" stop-color=\"#3a6691\"\/><stop offset=\".45\" stop-color=\"#1f4a75\"\/> <stop offset=\"1\" stop-color=\"#132c47\"\/><\/linearGradient> <linearGradient id=\"fsfl\" x1=\"0\" y1=\"0\" x2=\"0\" y2=\"1\"> <stop offset=\"0\" stop-color=\"#2f5b86\"\/><stop offset=\".5\" stop-color=\"#084490\"\/> <stop offset=\"1\" stop-color=\"#10243a\"\/><\/linearGradient> <linearGradient id=\"fsfh\" x1=\"0\" y1=\"0\" x2=\"0\" y2=\"1\"> <stop offset=\"0\" stop-color=\"#dbe1e9\"\/><stop offset=\".4\" stop-color=\"#aebccb\"\/> <stop offset=\"1\" stop-color=\"#70849a\"\/><\/linearGradient> <\/defs><line x1=\"240.0\" y1=\"34\" x2=\"240.0\" y2=\"240\" stroke=\"#C6D3DF\" stroke-width=\"1\" stroke-dasharray=\"4 4\"\/><text x=\"240.0\" y=\"254\" font-family=\"Inter, sans-serif\" font-size=\"9\" fill=\"#96ABC2\" text-anchor=\"middle\">wheel centre<\/text><rect x=\"132\" y=\"142\" width=\"216\" height=\"8\" rx=\"4\" fill=\"url(#fssteel)\" stroke=\"#084490\" stroke-width=\".8\"\/><rect x=\"114\" y=\"135\" width=\"20\" height=\"22\" rx=\"3\" fill=\"url(#fssteel)\" stroke=\"#084490\" stroke-width=\"1\"\/><rect x=\"346\" y=\"135\" width=\"20\" height=\"22\" rx=\"3\" fill=\"url(#fssteel)\" stroke=\"#084490\" stroke-width=\"1\"\/><path d=\"M174 100 C 220 128 260 128 306 100 L 306 192 C 260 164 220 164 174 192 Z\" fill=\"url(#fsbody)\" stroke=\"#084490\" stroke-width=\"1.2\"\/><path d=\"M182 110 C 222 134 258 134 298 110\" fill=\"none\" stroke=\"#fff\" stroke-opacity=\".15\" stroke-width=\"2\"\/><line x1=\"180\" y1=\"108\" x2=\"166\" y2=\"74\" stroke=\"url(#fssteel)\" stroke-width=\"4.4\" stroke-linecap=\"round\"\/><line x1=\"180\" y1=\"108\" x2=\"166\" y2=\"74\" stroke=\"#084490\" stroke-width=\"1\" stroke-opacity=\".4\"\/><circle cx=\"180\" cy=\"108\" r=\"3.8\" fill=\"#EDF2F6\" stroke=\"#084490\" stroke-width=\"1.3\"\/><line x1=\"180\" y1=\"184\" x2=\"166\" y2=\"218\" stroke=\"url(#fssteel)\" stroke-width=\"4.4\" stroke-linecap=\"round\"\/><line x1=\"180\" y1=\"184\" x2=\"166\" y2=\"218\" stroke=\"#084490\" stroke-width=\"1\" stroke-opacity=\".4\"\/><circle cx=\"180\" cy=\"184\" r=\"3.8\" fill=\"#EDF2F6\" stroke=\"#084490\" stroke-width=\"1.3\"\/><line x1=\"300\" y1=\"108\" x2=\"314\" y2=\"74\" stroke=\"url(#fssteel)\" stroke-width=\"4.4\" stroke-linecap=\"round\"\/><line x1=\"300\" y1=\"108\" x2=\"314\" y2=\"74\" stroke=\"#084490\" stroke-width=\"1\" stroke-opacity=\".4\"\/><circle cx=\"300\" cy=\"108\" r=\"3.8\" fill=\"#EDF2F6\" stroke=\"#084490\" stroke-width=\"1.3\"\/><line x1=\"300\" y1=\"184\" x2=\"314\" y2=\"218\" stroke=\"url(#fssteel)\" stroke-width=\"4.4\" stroke-linecap=\"round\"\/><line x1=\"300\" y1=\"184\" x2=\"314\" y2=\"218\" stroke=\"#084490\" stroke-width=\"1\" stroke-opacity=\".4\"\/><circle cx=\"300\" cy=\"184\" r=\"3.8\" fill=\"#EDF2F6\" stroke=\"#084490\" stroke-width=\"1.3\"\/><line x1=\"165.0\" y1=\"100.0\" x2=\"52.0\" y2=\"100.0\" stroke=\"#96ABC2\" stroke-width=\".9\"\/><line x1=\"165.0\" y1=\"192.0\" x2=\"52.0\" y2=\"192.0\" stroke=\"#96ABC2\" stroke-width=\".9\"\/><line x1=\"58\" y1=\"101\" x2=\"58\" y2=\"191\" stroke=\"#5d6770\" stroke-width=\"1.1\"\/><path d=\"M58.0 100.0 L54.8 109.0 L61.2 109.0 Z\" fill=\"#5d6770\"\/><path d=\"M58.0 192.0 L61.2 183.0 L54.8 183.0 Z\" fill=\"#5d6770\"\/><rect x=\"41.6\" y=\"135.5\" width=\"32.8\" height=\"21\" rx=\"10.5\" fill=\"#fff\"\/><rect x=\"43.6\" y=\"137.5\" width=\"28.799999999999997\" height=\"17\" rx=\"8.5\" fill=\"#084490\"\/><text x=\"58.0\" y=\"149.2\" font-family=\"Inter, sans-serif\" font-size=\"8.5\" font-weight=\"700\" fill=\"#fff\" text-anchor=\"middle\">NDS<\/text><line x1=\"315.0\" y1=\"100.0\" x2=\"408.0\" y2=\"100.0\" stroke=\"#96ABC2\" stroke-width=\".9\"\/><line x1=\"315.0\" y1=\"192.0\" x2=\"408.0\" y2=\"192.0\" stroke=\"#96ABC2\" stroke-width=\".9\"\/><line x1=\"402\" y1=\"101\" x2=\"402\" y2=\"191\" stroke=\"#5d6770\" stroke-width=\"1.1\"\/><path d=\"M402.0 100.0 L398.8 109.0 L405.2 109.0 Z\" fill=\"#5d6770\"\/><path d=\"M402.0 192.0 L405.2 183.0 L398.8 183.0 Z\" fill=\"#5d6770\"\/><rect x=\"388.4\" y=\"135.5\" width=\"27.2\" height=\"21\" rx=\"10.5\" fill=\"#fff\"\/><rect x=\"390.4\" y=\"137.5\" width=\"23.2\" height=\"17\" rx=\"8.5\" fill=\"#084490\"\/><text x=\"402.0\" y=\"149.2\" font-family=\"Inter, sans-serif\" font-size=\"8.5\" font-weight=\"700\" fill=\"#fff\" text-anchor=\"middle\">DS<\/text><line x1=\"176.0\" y1=\"94.0\" x2=\"176.0\" y2=\"40.0\" stroke=\"#96ABC2\" stroke-width=\".9\"\/><line x1=\"304.0\" y1=\"94.0\" x2=\"304.0\" y2=\"40.0\" stroke=\"#96ABC2\" stroke-width=\".9\"\/><line x1=\"177\" y1=\"46\" x2=\"239.0\" y2=\"46\" stroke=\"#5d6770\" stroke-width=\"1.1\"\/><path d=\"M176.0 46.0 L185.0 49.2 L185.0 42.8 Z\" fill=\"#5d6770\"\/><path d=\"M240.0 46.0 L231.0 42.8 L231.0 49.2 Z\" fill=\"#5d6770\"\/><rect x=\"186.0\" y=\"17.5\" width=\"44.0\" height=\"21\" rx=\"10.5\" fill=\"#fff\"\/><rect x=\"188.0\" y=\"19.5\" width=\"40.0\" height=\"17\" rx=\"8.5\" fill=\"#084490\"\/><text x=\"208.0\" y=\"31.2\" font-family=\"Inter, sans-serif\" font-size=\"8.5\" font-weight=\"700\" fill=\"#fff\" text-anchor=\"middle\">W-NDS<\/text><line x1=\"241.0\" y1=\"46\" x2=\"303\" y2=\"46\" stroke=\"#5d6770\" stroke-width=\"1.1\"\/><path d=\"M240.0 46.0 L249.0 49.2 L249.0 42.8 Z\" fill=\"#5d6770\"\/><path d=\"M304.0 46.0 L295.0 42.8 L295.0 49.2 Z\" fill=\"#5d6770\"\/><rect x=\"252.8\" y=\"17.5\" width=\"38.4\" height=\"21\" rx=\"10.5\" fill=\"#fff\"\/><rect x=\"254.8\" y=\"19.5\" width=\"34.4\" height=\"17\" rx=\"8.5\" fill=\"#084490\"\/><text x=\"272.0\" y=\"31.2\" font-family=\"Inter, sans-serif\" font-size=\"8.5\" font-weight=\"700\" fill=\"#fff\" text-anchor=\"middle\">W-DS<\/text><\/svg>`\n};\nconst HUBINSET={\n  hole:`<svg viewBox=\"0 0 210 156\" xmlns=\"http:\/\/www.w3.org\/2000\/svg\"><defs> <linearGradient id=\"histeel\" x1=\"0\" y1=\"0\" x2=\"0\" y2=\"1\"> <stop offset=\"0\" stop-color=\"#c9d4e0\"\/><stop offset=\".3\" stop-color=\"#95a8bc\"\/> <stop offset=\".55\" stop-color=\"#60798f\"\/><stop offset=\"1\" stop-color=\"#3a526b\"\/><\/linearGradient> <linearGradient id=\"hibody\" x1=\"0\" y1=\"0\" x2=\"0\" y2=\"1\"> <stop offset=\"0\" stop-color=\"#3a6691\"\/><stop offset=\".45\" stop-color=\"#1f4a75\"\/> <stop offset=\"1\" stop-color=\"#132c47\"\/><\/linearGradient> <linearGradient id=\"hifl\" x1=\"0\" y1=\"0\" x2=\"0\" y2=\"1\"> <stop offset=\"0\" stop-color=\"#2f5b86\"\/><stop offset=\".5\" stop-color=\"#084490\"\/> <stop offset=\"1\" stop-color=\"#10243a\"\/><\/linearGradient> <linearGradient id=\"hifh\" x1=\"0\" y1=\"0\" x2=\"0\" y2=\"1\"> <stop offset=\"0\" stop-color=\"#dbe1e9\"\/><stop offset=\".4\" stop-color=\"#aebccb\"\/> <stop offset=\"1\" stop-color=\"#70849a\"\/><\/linearGradient> <\/defs><rect x=\"33\" y=\"28\" width=\"144\" height=\"68\" rx=\"15\" fill=\"url(#hifl)\" stroke=\"#084490\" stroke-width=\"1.4\"\/><line x1=\"40\" y1=\"38\" x2=\"170\" y2=\"38\" stroke=\"#fff\" stroke-opacity=\".18\" stroke-width=\"2\"\/><circle cx=\"105\" cy=\"62\" r=\"17\" fill=\"#fff\" stroke=\"#5d6770\" stroke-width=\"1.4\"\/><circle cx=\"105\" cy=\"62\" r=\"11.5\" fill=\"#EDF2F6\" stroke=\"#96ABC2\" stroke-width=\"1\"\/><line x1=\"88.0\" y1=\"65.0\" x2=\"88.0\" y2=\"118.0\" stroke=\"#96ABC2\" stroke-width=\".9\"\/><line x1=\"122.0\" y1=\"65.0\" x2=\"122.0\" y2=\"118.0\" stroke=\"#96ABC2\" stroke-width=\".9\"\/><line x1=\"88\" y1=\"112\" x2=\"122\" y2=\"112\" stroke=\"#5d6770\" stroke-width=\"1.1\"\/><line x1=\"72\" y1=\"112\" x2=\"88\" y2=\"112\" stroke=\"#5d6770\" stroke-width=\"1.1\"\/><line x1=\"122\" y1=\"112\" x2=\"138\" y2=\"112\" stroke=\"#5d6770\" stroke-width=\"1.1\"\/><path d=\"M88.0 112.0 L79.0 108.8 L79.0 115.2 Z\" fill=\"#5d6770\"\/><path d=\"M122.0 112.0 L131.0 115.2 L131.0 108.8 Z\" fill=\"#5d6770\"\/><circle cx=\"105.0\" cy=\"131.0\" r=\"13.0\" fill=\"#fff\"\/><circle cx=\"105.0\" cy=\"131.0\" r=\"10.5\" fill=\"#E63329\"\/><text x=\"105.0\" y=\"134.6\" font-family=\"Inter, sans-serif\" font-size=\"10\" font-weight=\"700\" fill=\"#fff\" text-anchor=\"middle\">H<\/text><text x=\"105\" y=\"151\" font-family=\"Inter, sans-serif\" font-size=\"10\" fill=\"#084490\" text-anchor=\"middle\">spoke hole \u00d8<\/text><\/svg>`,\n  offnds:`<svg viewBox=\"0 0 210 148\" xmlns=\"http:\/\/www.w3.org\/2000\/svg\"><defs> <linearGradient id=\"spHsteel\" x1=\"0\" y1=\"0\" x2=\"0\" y2=\"1\"> <stop offset=\"0\" stop-color=\"#c9d4e0\"\/><stop offset=\".3\" stop-color=\"#95a8bc\"\/> <stop offset=\".55\" stop-color=\"#60798f\"\/><stop offset=\"1\" stop-color=\"#3a526b\"\/><\/linearGradient> <linearGradient id=\"spHbody\" x1=\"0\" y1=\"0\" x2=\"0\" y2=\"1\"> <stop offset=\"0\" stop-color=\"#3a6691\"\/><stop offset=\".45\" stop-color=\"#1f4a75\"\/> <stop offset=\"1\" stop-color=\"#132c47\"\/><\/linearGradient> <linearGradient id=\"spHfl\" x1=\"0\" y1=\"0\" x2=\"0\" y2=\"1\"> <stop offset=\"0\" stop-color=\"#2f5b86\"\/><stop offset=\".5\" stop-color=\"#084490\"\/> <stop offset=\"1\" stop-color=\"#10243a\"\/><\/linearGradient> <linearGradient id=\"spHfh\" x1=\"0\" y1=\"0\" x2=\"0\" y2=\"1\"> <stop offset=\"0\" stop-color=\"#dbe1e9\"\/><stop offset=\".4\" stop-color=\"#aebccb\"\/> <stop offset=\"1\" stop-color=\"#70849a\"\/><\/linearGradient> <\/defs><path d=\"M76 26 L150 26 Q172 28 180 50 L192 88 L76 88 Z\" fill=\"url(#spHbody)\" stroke=\"#084490\" stroke-width=\"1.4\"\/><line x1=\"76\" y1=\"18\" x2=\"76\" y2=\"124\" stroke=\"#96ABC2\" stroke-width=\"1\" stroke-dasharray=\"3 3\"\/><text x=\"76\" y=\"14\" font-family=\"Inter, sans-serif\" font-size=\"8.5\" fill=\"#96ABC2\" text-anchor=\"middle\">flange face<\/text><line x1=\"124\" y1=\"58\" x2=\"170\" y2=\"110\" stroke=\"url(#spHsteel)\" stroke-width=\"4.4\" stroke-linecap=\"round\"\/><circle cx=\"124\" cy=\"58\" r=\"4\" fill=\"#EDF2F6\" stroke=\"#084490\" stroke-width=\"1.3\"\/><line x1=\"124.0\" y1=\"64.0\" x2=\"124.0\" y2=\"118.0\" stroke=\"#96ABC2\" stroke-width=\".9\"\/><line x1=\"77\" y1=\"112\" x2=\"123\" y2=\"112\" stroke=\"#5d6770\" stroke-width=\"1.1\"\/><path d=\"M76.0 112.0 L85.0 115.2 L85.0 108.8 Z\" fill=\"#5d6770\"\/><path d=\"M124.0 112.0 L115.0 108.8 L115.0 115.2 Z\" fill=\"#5d6770\"\/><circle cx=\"100.0\" cy=\"112.0\" r=\"13.0\" fill=\"#fff\"\/><circle cx=\"100.0\" cy=\"112.0\" r=\"10.5\" fill=\"#E63329\"\/><text x=\"100.0\" y=\"115.6\" font-family=\"Inter, sans-serif\" font-size=\"10\" font-weight=\"700\" fill=\"#fff\" text-anchor=\"middle\">H<\/text><text x=\"130.0\" y=\"140\" font-family=\"Inter, sans-serif\" font-size=\"10\" fill=\"#084490\" text-anchor=\"middle\">spoke offset NDS<\/text><\/svg>`,\n  offds:`<svg viewBox=\"0 0 210 148\" xmlns=\"http:\/\/www.w3.org\/2000\/svg\"><defs> <linearGradient id=\"spGsteel\" x1=\"0\" y1=\"0\" x2=\"0\" y2=\"1\"> <stop offset=\"0\" stop-color=\"#c9d4e0\"\/><stop offset=\".3\" stop-color=\"#95a8bc\"\/> <stop offset=\".55\" stop-color=\"#60798f\"\/><stop offset=\"1\" stop-color=\"#3a526b\"\/><\/linearGradient> <linearGradient id=\"spGbody\" x1=\"0\" y1=\"0\" x2=\"0\" y2=\"1\"> <stop offset=\"0\" stop-color=\"#3a6691\"\/><stop offset=\".45\" stop-color=\"#1f4a75\"\/> <stop offset=\"1\" stop-color=\"#132c47\"\/><\/linearGradient> <linearGradient id=\"spGfl\" x1=\"0\" y1=\"0\" x2=\"0\" y2=\"1\"> <stop offset=\"0\" stop-color=\"#2f5b86\"\/><stop offset=\".5\" stop-color=\"#084490\"\/> <stop offset=\"1\" stop-color=\"#10243a\"\/><\/linearGradient> <linearGradient id=\"spGfh\" x1=\"0\" y1=\"0\" x2=\"0\" y2=\"1\"> <stop offset=\"0\" stop-color=\"#dbe1e9\"\/><stop offset=\".4\" stop-color=\"#aebccb\"\/> <stop offset=\"1\" stop-color=\"#70849a\"\/><\/linearGradient> <\/defs><path d=\"M76 26 L150 26 Q172 28 180 50 L192 88 L76 88 Z\" fill=\"url(#spGbody)\" stroke=\"#084490\" stroke-width=\"1.4\"\/><line x1=\"76\" y1=\"18\" x2=\"76\" y2=\"124\" stroke=\"#96ABC2\" stroke-width=\"1\" stroke-dasharray=\"3 3\"\/><text x=\"76\" y=\"14\" font-family=\"Inter, sans-serif\" font-size=\"8.5\" fill=\"#96ABC2\" text-anchor=\"middle\">flange face<\/text><line x1=\"124\" y1=\"58\" x2=\"170\" y2=\"110\" stroke=\"url(#spGsteel)\" stroke-width=\"4.4\" stroke-linecap=\"round\"\/><circle cx=\"124\" cy=\"58\" r=\"4\" fill=\"#EDF2F6\" stroke=\"#084490\" stroke-width=\"1.3\"\/><line x1=\"124.0\" y1=\"64.0\" x2=\"124.0\" y2=\"118.0\" stroke=\"#96ABC2\" stroke-width=\".9\"\/><line x1=\"77\" y1=\"112\" x2=\"123\" y2=\"112\" stroke=\"#5d6770\" stroke-width=\"1.1\"\/><path d=\"M76.0 112.0 L85.0 115.2 L85.0 108.8 Z\" fill=\"#5d6770\"\/><path d=\"M124.0 112.0 L115.0 108.8 L115.0 115.2 Z\" fill=\"#5d6770\"\/><circle cx=\"100.0\" cy=\"112.0\" r=\"13.0\" fill=\"#fff\"\/><circle cx=\"100.0\" cy=\"112.0\" r=\"10.5\" fill=\"#E63329\"\/><text x=\"100.0\" y=\"115.6\" font-family=\"Inter, sans-serif\" font-size=\"10\" font-weight=\"700\" fill=\"#fff\" text-anchor=\"middle\">G<\/text><text x=\"130.0\" y=\"140\" font-family=\"Inter, sans-serif\" font-size=\"10\" fill=\"#084490\" text-anchor=\"middle\">spoke offset DS<\/text><\/svg>`\n};\n\/* ---- spoke models: estimated linear density g\/mm (mid-section).\n   Cross-section area derived as gmm\/0.00785 for stretch. Estimates only. ---- *\/\nconst SPOKE_MODELS = [\n  [\"Alpina One, 2.00mm\",0.0240],[\"Alpina One Aero, 2.30 x 1.70mm\",0.0235],\n  [\"Alpina One XL, 2.30mm\",0.0320],[\"Alpina One XXL, 2.30mm\",0.0320],\n  [\"Alpina Hyperlite, 1.30mm\",0.0105],[\"Alpina Hyperlite Aero, 2.00 x 0.90mm\",0.0110],\n  [\"Alpina Ultralite, 1.50mm\",0.0140],[\"Alpina Ultralite Aero, 2.20 x 0.95mm\",0.0150],\n  [\"Alpina Superlite, 1.60mm\",0.0160],[\"Alpina Superlite Aero, 2.30 x 1.10mm\",0.0165],\n  [\"Alpina Extralite, 1.70mm\",0.0182],[\"Alpina Extralite Aero, 2.30 x 1.20mm\",0.0185],\n  [\"Alpina Basiclite, 1.80mm\",0.0200],[\"Alpina Basiclite Aero, 2.30 x 1.40mm\",0.0205],\n  [\"Alpina Spark, 2.30 \/ 2.00mm\",0.0250],[\"Alpina Spark XL, 2.60 \/ 2.30mm\",0.0330],\n  [\"Sapim Leader, 2.00mm\",0.0240],[\"Sapim Strong, 2.30 \/ 2.00mm\",0.0250],\n  [\"Sapim Race, 1.80mm (db)\",0.0205],[\"Sapim Force, 1.80mm (db)\",0.0205],\n  [\"Sapim D-Light, 1.65mm (db)\",0.0172],[\"Sapim Laser, 1.50mm (db)\",0.0140],\n  [\"Sapim CX-Sprint, 2.25 x 1.25mm\",0.0205],[\"Sapim CX-Ray, 2.20 x 0.90mm\",0.0150],\n  [\"DT Competition Race, 1.60mm\",0.0160],[\"DT Competition, 1.80mm (db)\",0.0205],\n  [\"DT Aerolite, 2.30 x 0.90mm\",0.0150],[\"DT Aero Comp, 2.30 x 1.20mm\",0.0200],\n  [\"DT Alpine III, 1.85mm\",0.0218],[\"DT Revolution, 1.50mm (db)\",0.0140],\n  [\"DT Alpine, 2.34 \/ 2.00mm\",0.0250],[\"DT Champion, 2.00mm\",0.0240],\n  [\"cnSpoke Aero330, 1.80 x 0.90mm\",0.0135],[\"cnSpoke Aero360, 2.00 x 0.90mm\",0.0150],\n  [\"cnSpoke Aero424, 2.20 x 0.90mm\",0.0150],[\"cnSpoke Aero416, 2.20 x 1.00mm\",0.0170],\n  [\"cnSpoke DB454, 1.80mm (db)\",0.0200],[\"cnSpoke DB474, 1.50mm (db)\",0.0140],\n  [\"cnSpoke MAC14, 2.00mm\",0.0240],[\"cnSpoke MAC134T e-bike, 2.30mm\",0.0320],\n  [\"cnSpoke XT233 e-bike, 2.00mm\",0.0240],[\"cnSpoke XT344 e-bike, 2.30mm\",0.0320]\n];\nconst STEEL_GMM3 = 0.00785;   \/\/ g per mm^3\nconst E_STEEL = 200000;       \/\/ N\/mm^2\nconst KGF = 9.80665;          \/\/ N per kgf\n\nconst SPOKE_COUNTS = [16,18,20,21,24,27,28,32,36,48];\nconst WHEEL_SIZES = [[686,'32\u2033'],[630,'27\u2033 (630)'],[622,'28\u2033 \/ 700C \/ 29\u2033'],[590,'650A (590)'],[584,'27.5\u2033 \/ 650B'],[571,'650C'],[559,'26\u2033'],[507,'24\u2033'],[451,'20\u2033 (451)'],[406,'20\u2033 (406)'],[349,'16\u2033 (349)'],[305,'16\u2033 (305)']];\nconst SEAT_TO_HOOK = 10;   \/\/ mm: nipple seat to inner wall (~4) + bead seat to hook top (~6)\n\/* spoke colours: hue = side (NDS navy, DS orange), shade = group (dark = leading, light = trailing).\n   Codes per the parameter drawing: Sa NDS leading, Sb NDS trailing, Sd DS leading, Sc DS trailing. *\/\nconst GC={Sa:'#084490', Sb:'#4F8FE6', Sd:'#D9530A', Sc:'#F5A23F', NDS:'#084490', DS:'#D9530A'};\nfunction grpCol(side, role){\n  const nds=side==='L'||side==='NDS'||side==='l', trail=role==='trailing'||role==='T';\n  return nds ? (trail?GC.Sb:GC.Sa) : (trail?GC.Sc:GC.Sd);\n}\n\/* ---------- nipples ----------\n   Where the spoke should end, measured from the nipple seat: the face the nipple rests on (spoke bed,\n   eyelet or washer). Typical values from sectioned nipples of the common makes; makes differ by about\n   \u00b11 mm. The round 12\/14 mm nipple (spoke end about 2 mm above the seat, at the top of the head) is the\n   reference that spec-sheet ERDs are measured with. head = head height above the seat (drawings). *\/\nconst NIP_STD = 2.0;\nconst NIPPLES = {\n  round:    {name:'Round \u00b7 slotted', lens:[12,14,16], def:12, add:{12:2.0,14:2.0,16:0.6}, head:2.2},\n  hex:      {name:'Hex head',        lens:[12,14,16], def:14, add:{12:2.5,14:2.5,16:2.5}, head:2.8},\n  square:   {name:'Double square',   lens:[15,16,18], def:16, add:{15:3.2,16:3.2,18:3.2}, head:5.7},\n  internal: {name:'Internal',        lens:[10,12,14], def:12, add:{10:9,12:11,14:13},    head:0},\n  custom:   {name:'Custom',          lens:[],         def:12, add:{},                    head:2.2}\n};\nconst NIP_GPM = {brass:0.08, alu:0.028};          \/\/ grams per mm of nipple length (weight estimate)\nconst ERD_REF_TXT = {std:'a standard nipple', mine:'your own nipple', seat:'the nipple seat'};\nfunction erdRef(host){ return pressed(host,'[data-seg=\"erdRef\"]')||'std'; }\nfunction setErdRef(host,v){ host.querySelectorAll('[data-seg=\"erdRef\"] button').forEach(b=>b.setAttribute('aria-pressed',b.dataset.v===v?'true':'false')); }\nfunction nippleSel(host){\n  const pt=pressed(host,'[data-choice=\"nipType\"]'), type=NIPPLES[pt]?pt:'round', N=NIPPLES[type];\n  let len=parseInt(pressed(host,'[data-choice=\"nipLen\"]'),10); if(!N.lens.includes(len)) len=N.def;\n  const mat=pressed(host,'[data-choice=\"nipMat\"]')==='alu'?'alu':'brass';\n  const w=num(host,'washer'), washer=w>0?w:0, c=num(host,'nipAdd');\n  const add=type==='custom' ? (c>=0?c:NaN) : N.add[len];\n  return {type, name:N.name, len, mat, washer, add, total:add+washer, head:type==='custom'?NIPPLES.round.head:N.head,\n    gEach:NIP_GPM[mat]*len, label:type==='custom'?'Custom':`${N.name} ${len} mm`};\n}\nfunction refreshNipple(host){\n  const t=pressed(host,'[data-choice=\"nipType\"]'), N=NIPPLES[t]||NIPPLES.round;\n  const box=host.querySelector('[data-choice=\"nipLen\"]'); if(!box) return;\n  const cur=parseInt(pressed(host,'[data-choice=\"nipLen\"]'),10), keep=N.lens.includes(cur)?cur:N.def;\n  box.innerHTML=N.lens.map(l=>`<button type=\"button\" data-v=\"${l}\" aria-pressed=\"${l===keep}\">${l} mm<\/button>`).join('');\n  const lf=host.querySelector('.nip-len-f'); if(lf) lf.style.display=N.lens.length?'':'none';\n  const cf=host.querySelector('.nip-custom'); if(cf) cf.style.display=t==='custom'?'':'none';\n}\n\/* a nipple in its seat, in section: up = into the rim (toward the tyre), down = toward the hub.\n   cx = axis, yS = the face the nipple rests on, k = px per mm. *\/\nfunction nipShape(type,len,cx,yS,k,fill,stroke){\n  const f=v=>(+v).toFixed(1), N=NIPPLES[type]||NIPPLES.round, hh=(type==='custom'?NIPPLES.round.head:N.head)*k;\n  const ln=(x1,y1,x2,y2)=>`M${f(x1)} ${f(y1)} L${f(x2)} ${f(y2)}`, rr=v=>f(Math.min(v*k,1.5));\n  if(type==='internal'){\n    const w=4.4*k, x=cx-w\/2, yT=yS-len*k, r=Math.min(0.6*k,2);\n    return `<path d=\"M${f(x+0.9*k)} ${f(yS)} L${f(x)} ${f(yS-1.1*k)} V${f(yT+r)} Q${f(x)} ${f(yT)} ${f(x+r)} ${f(yT)} H${f(x+w-r)} Q${f(x+w)} ${f(yT)} ${f(x+w)} ${f(yT+r)} V${f(yS-1.1*k)} L${f(x+w-0.9*k)} ${f(yS)} Z\" fill=\"${fill}\" stroke=\"${stroke}\" stroke-width=\".8\"\/>`+\n      `<path d=\"${ln(cx-w*0.22,yT+0.5*k,cx-w*0.22,yS-1.3*k)} ${ln(cx+w*0.22,yT+0.5*k,cx+w*0.22,yS-1.3*k)}\" stroke=\"${stroke}\" stroke-width=\".6\" opacity=\".55\"\/>`;\n  }\n  const bw=3.2*k, below=Math.max(1,len*k-hh), xb=cx-bw\/2;\n  let s=`<rect x=\"${f(xb)}\" y=\"${f(yS)}\" width=\"${f(bw)}\" height=\"${f(below)}\" rx=\"${rr(0.4)}\" fill=\"${fill}\" stroke=\"${stroke}\" stroke-width=\".8\"\/>`+\n    `<path d=\"${ln(xb+bw*0.28,yS+below*0.52,xb+bw*0.28,yS+below-0.5*k)}\" stroke=\"${stroke}\" stroke-width=\".6\" opacity=\".55\"\/>`;\n  if(type==='square'||type==='hex'){ const w=(type==='square'?3.6:4.6)*k;\n    s+=`<rect x=\"${f(cx-w\/2)}\" y=\"${f(yS-hh)}\" width=\"${f(w)}\" height=\"${f(hh)}\" rx=\"${rr(0.3)}\" fill=\"${fill}\" stroke=\"${stroke}\" stroke-width=\".8\"\/>`+\n      (type==='hex' ? `<path d=\"${ln(cx-w*0.25,yS-hh,cx-w*0.25,yS)} ${ln(cx+w*0.25,yS-hh,cx+w*0.25,yS)}\" stroke=\"${stroke}\" stroke-width=\".6\" opacity=\".55\"\/>`\n                    : `<path d=\"${ln(cx-w*0.22,yS-hh+0.4*k,cx-w*0.22,yS-0.4*k)}\" stroke=\"${stroke}\" stroke-width=\".6\" opacity=\".55\"\/>`);\n  } else { const w=4.0*k;\n    s+=`<path d=\"M${f(cx-w\/2)} ${f(yS)} V${f(yS-hh*0.4)} Q${f(cx-w\/2)} ${f(yS-hh)} ${f(cx)} ${f(yS-hh)} Q${f(cx+w\/2)} ${f(yS-hh)} ${f(cx+w\/2)} ${f(yS-hh*0.4)} V${f(yS)} Z\" fill=\"${fill}\" stroke=\"${stroke}\" stroke-width=\".8\"\/>`+\n      `<rect x=\"${f(cx-0.45*k)}\" y=\"${f(yS-hh-0.3)}\" width=\"${f(0.9*k)}\" height=\"${f(Math.min(hh*0.5,1.3*k))}\" fill=\"${stroke}\" opacity=\".8\"\/>`;\n  }\n  return s;\n}\nfunction nipIconSVG(type){\n  const N=NIPPLES[type]||NIPPLES.round, len=N.def, k=1.85, cx=20, f=v=>(+v).toFixed(1);\n  const add=type==='custom'?NIP_STD:N.add[len], hd=type==='custom'?NIPPLES.round.head:N.head;\n  const above=type==='internal'?len:Math.max(hd,add), below=type==='internal'?4:len-hd;\n  const yS=(46-(above+below)*k)\/2+above*k, yE=yS-add*k, hole=(type==='internal'?1.4:2.15)*k, t=2.2*k;\n  return `<svg viewBox=\"0 0 40 46\" xmlns=\"http:\/\/www.w3.org\/2000\/svg\" aria-hidden=\"true\">`+\n    `<rect x=\".5\" y=\".5\" width=\"39\" height=\"45\" rx=\"7\" fill=\"#fff\"\/>`+\n    `<rect x=\"${f(cx-k)}\" y=\"${f(yE)}\" width=\"${f(2*k)}\" height=\"${f(46-yE)}\" fill=\"#9FB1C3\"\/>`+\n    `<path d=\"M3 ${f(yS)} H${f(cx-hole)} V${f(yS+t)} H3 Z M${f(cx+hole)} ${f(yS)} H37 V${f(yS+t)} H${f(cx+hole)} Z\" fill=\"#084490\"\/>`+\n    nipShape(type,len,cx,yS,k,'#C9D3DD','#5E6E7E')+\n    `<path d=\"M${f(cx-5.5)} ${f(yE)} H${f(cx+5.5)}\" stroke=\"#E63329\" stroke-width=\"1.5\"\/>`+\n    (type==='custom'?`<text x=\"32\" y=\"15\" font-family=\"Inter,sans-serif\" font-size=\"12\" font-weight=\"800\" fill=\"#E63329\" text-anchor=\"middle\">?<\/text>`:'')+\n    `<\/svg>`;\n}\n\/* the chosen nipple, larger, with the nipple seat, the spoke end and the height between them *\/\nfunction nipDetailSVG(re){\n  const {vdim}=measureKit(), n=re.nip, type=n.type, len=n.len, w=n.washer, add=isFinite(n.add)?n.add:NIP_STD, hd=n.head;\n  const above=Math.max(type==='internal'?len:hd, add)+w, below=type==='internal'?4:(len-hd)+1;\n  const k=Math.min(6.2, 116\/(above+below+1.5)), W=300, H=150, cx=58, f=v=>(+v).toFixed(1);\n  const yS=Math.round(14+(above+0.8)*k), yN=yS-w*k, yE=yN-add*k, t=2.2*k, hole=(type==='internal'?1.4:2.15)*k;\n  const tipEnd=`The spoke ends ${f1(add+w)} mm above the nipple seat${w?` (${f1(add)} in the nipple + ${f1(w)} washer)`:''}: this is the diameter the lengths use (the ERD)`;\n  let g=`<svg viewBox=\"0 0 ${W} ${H}\" xmlns=\"http:\/\/www.w3.org\/2000\/svg\" role=\"img\" aria-label=\"${n.label} nipple in its seat, in section\">`+\n    `<defs><pattern id=\"nbHatch\" width=\"5\" height=\"5\" patternUnits=\"userSpaceOnUse\" patternTransform=\"rotate(45)\"><rect width=\"5\" height=\"5\" fill=\"#0A4A9A\"\/><line x1=\"0\" y1=\"0\" x2=\"0\" y2=\"5\" stroke=\"#3F78C2\" stroke-width=\"1.6\"\/><\/pattern>`+\n    `<linearGradient id=\"nbNip\" x1=\"0\" y1=\"0\" x2=\"1\" y2=\"0\"><stop offset=\"0\" stop-color=\"#8796a8\"\/><stop offset=\".45\" stop-color=\"#eef2f6\"\/><stop offset=\"1\" stop-color=\"#6f8399\"\/><\/linearGradient>`+\n    `<linearGradient id=\"nbSteel\" x1=\"0\" y1=\"0\" x2=\"1\" y2=\"0\"><stop offset=\"0\" stop-color=\"#6A8098\"\/><stop offset=\".4\" stop-color=\"#DCE4EC\"\/><stop offset=\"1\" stop-color=\"#465C73\"\/><\/linearGradient><\/defs>`;\n  g+=`<rect x=\"${f(cx-k)}\" y=\"${f(yE)}\" width=\"${f(2*k)}\" height=\"${f(H-yE)}\" fill=\"url(#nbSteel)\" stroke=\"#465C73\" stroke-width=\".6\"\/>`;\n  g+=`<path class=\"hl-f\" data-hl=\"rimWall erd\" d=\"M10 ${f(yS)} H${f(cx-hole)} V${f(yS+t)} H10 Z M${f(cx+hole)} ${f(yS)} H${f(cx+40)} V${f(yS+t)} H${f(cx+hole)} Z\" fill=\"url(#nbHatch)\" stroke=\"#084490\" stroke-width=\".8\" data-tip=\"Spoke bed (inner wall of the rim) in section\"\/>`;\n  if(w>0) g+=`<rect class=\"hl-f\" data-hl=\"washer\" x=\"${f(cx-3.4*k)}\" y=\"${f(yN)}\" width=\"${f(6.8*k)}\" height=\"${f(Math.max(1.2,w*k))}\" fill=\"#E8EDF2\" stroke=\"#6f8399\" stroke-width=\".7\" data-tip=\"Washer ${f1(w)} mm\"\/>`;\n  g+=`<g class=\"hl-f\" data-hl=\"nipAdd washer\" data-tip=\"${n.label} nipple${n.mat==='alu'?' \u00b7 alloy':' \u00b7 brass'}\">${nipShape(type,len,cx,yN,k,'url(#nbNip)','#5E6E7E')}<\/g>`;\n  const hidFrom = type==='internal' ? yN : yN+(len-hd)*k;\n  g+=`<path d=\"M${f(cx-k)} ${f(hidFrom)} V${f(yE)} H${f(cx+k)} V${f(hidFrom)}\" fill=\"none\" stroke=\"#465C73\" stroke-width=\".8\" stroke-dasharray=\"2 1.6\"\/>`;\n  \/\/ nipple seat, spoke end, and the standard nipple's spoke end when it differs clearly\n  const xA=cx+3.4*k+4, xT=cx+78;\n  g+=`<line x1=\"${f(cx+hole+1)}\" y1=\"${f(yS)}\" x2=\"${W-6}\" y2=\"${f(yS)}\" stroke=\"#96ABC2\" stroke-width=\".9\" stroke-dasharray=\"3 2\"\/>`+\n    `<text x=\"${xT}\" y=\"${f(yS+12)}\" font-family=\"Inter,sans-serif\" font-size=\"9.5\" font-weight=\"600\" fill=\"#647E9C\">nipple seat${re.nsd>0?` \u00b7 \u00d8 ${f1(re.nsd)}`:''}<\/text>`;\n  const dStd=(add+w-NIP_STD)*k;\n  if(Math.abs(dStd)>=13 && re.ref!=='mine'){ const yStd=yS-NIP_STD*k;\n    g+=`<line x1=\"${f(cx+2.6*k)}\" y1=\"${f(yStd)}\" x2=\"${W-6}\" y2=\"${f(yStd)}\" stroke=\"#96ABC2\" stroke-width=\".9\" stroke-dasharray=\"1.5 2\"\/>`+\n      `<text x=\"${W-8}\" y=\"${f(yStd-4)}\" font-family=\"Inter,sans-serif\" font-size=\"9\" fill=\"#96ABC2\" text-anchor=\"end\">a standard nipple ends here<\/text>`; }\n  g+=`<line x1=\"${f(cx-2.4*k)}\" y1=\"${f(yE)}\" x2=\"${W-6}\" y2=\"${f(yE)}\" stroke=\"#E63329\" stroke-width=\"1.1\" stroke-dasharray=\"4 2\" data-tip=\"${tipEnd}\"\/>`+\n    `<text x=\"${xT}\" y=\"${f(yE-5)}\" font-family=\"Inter,sans-serif\" font-size=\"10\" font-weight=\"700\" fill=\"#E63329\">spoke end${re.erd>0?` \u00b7 ERD ${f1(re.erd)}`:''}<\/text>`;\n  g+=vdim(xA+8,yE,yS,'nipAdd washer')+`<text x=\"${f(xA+15)}\" y=\"${f((yE+yS)\/2+3.8)}\" font-family=\"Inter,sans-serif\" font-size=\"11\" font-weight=\"800\" fill=\"#084490\" paint-order=\"stroke\" stroke=\"#fff\" stroke-width=\"3\" stroke-linejoin=\"round\">${f1(add+w)} mm<\/text>`;\n  return g+'<\/svg>';\n}\nfunction nipOut(re,host){\n  const n=re.nip, mm=x=>f1(x)+' mm';\n  if(!isFinite(n.add)) return `<p>Custom nipple: enter where the spoke should end, measured from the nipple seat.<\/p>`;\n  const dl=n.total-NIP_STD;\n  let o=`<p><b>Spoke end ${f1(n.total)} mm above the nipple seat<\/b>${n.washer?` (${f1(n.add)} in the nipple + ${f1(n.washer)} washer)`:''}`+\n    (re.ref==='mine'||Math.abs(dl)<0.05 ? '.' : `: ${f1(Math.abs(dl))} mm ${dl>0?'higher':'lower'} than in a standard nipple, so the spokes are about ${f1(Math.abs(dl))} mm ${dl>0?'longer':'shorter'}.`)+`<\/p>`;\n  if(re.mode==='measure') o+= re.nsd>0 ? `<p>ERD = nipple seat ${mm(re.nsd)} + 2 \u00d7 ${f1(n.total)} = <b>${mm(re.erd)}<\/b><\/p>` : '';\n  else if(!(re.input>0)) o+='';\n  else if(re.ref==='mine') o+=`<p>ERD <b>${mm(re.input)}<\/b>, used as you measured it with your nipples (and washers, if fitted): the nipple type and washer here only set the weight and the drawings.<\/p>`;\n  else if(re.ref==='seat') o+=`<p>ERD = nipple seat ${mm(re.input)} + 2 \u00d7 ${f1(n.total)} = <b>${mm(re.erd)}<\/b><\/p>`;\n  else o+= Math.abs(dl)<0.05 ? `<p>ERD <b>${mm(re.erd)}<\/b>, as on the spec sheet.<\/p>` : `<p>ERD = spec ${mm(re.input)} ${dl>0?'+':'\u2212'} 2 \u00d7 ${f1(Math.abs(dl))} = <b>${mm(re.erd)}<\/b><\/p>`;\n  if(n.type==='internal' && host){ const rl=rimLive(host, re.nsd>0?re.nsd:(re.erd>0?re.erd-2*n.total:560));\n    if(rl.rimDepth < n.len+8) o+=`<p class=\"warn\">A ${n.len} mm internal nipple sits inside the rim: it needs a rim of about ${n.len+8} mm or deeper (this one is ${Math.round(rl.rimDepth)} mm${rl.rimDepthSet?'':', estimated'}).<\/p>`; }\n  if(n.type==='round' && n.len>=16) o+=`<p class=\"note-s\">16 mm nipples start their thread deeper in the barrel, so the spoke stops lower than in a 12 or 14 mm nipple.<\/p>`;\n  return o;\n}\nfunction drawNipple(host){\n  const re=rimERD(host), art=host.querySelector('[data-nipart]'), out=host.querySelector('[data-nipout]');\n  if(art) art.innerHTML=uniqSVG(nipDetailSVG(re), host.id==='panel-rear'?'r':'f');\n  if(out) out.innerHTML=nipOut(re,host);\n}\nfunction nipNote(d){\n  const n=d.nip, dl=n.total-NIP_STD, more=`about ${f1(Math.abs(dl))} mm ${dl>=0?'longer':'shorter'} than with standard nipples`;\n  if(d.erdRef==='mine') return `<b>ERD measured with your own nipples:<\/b> used as entered, washers included. The nipple type and washer only set the weight and the drawings.`;\n  let t = n.type==='internal' ? `<b>Internal nipples:<\/b> the spoke runs about ${f1(n.add)} mm past the nipple seat, almost to the top of the nipple, so the spokes are ${more}. They are tightened from the tyre side, before the rim tape goes on.`\n    : (n.type==='square'||n.type==='hex') ? `<b>${n.name} nipples:<\/b> the taller head takes the spoke about ${f1(n.add)} mm past the seat, so the spokes are ${more}. The head can be driven from the tyre side, by hand or by machine.`\n    : (n.type==='round' && n.len>=16) ? `<b>16 mm nipples:<\/b> the thread starts deeper in the long barrel, so the spoke stops about ${f1(n.add)} mm above the seat instead of at the top of the head (${more}). A longer spoke runs out of thread before full tension.`\n    : n.type==='custom' ? `<b>Custom nipple:<\/b> spoke end ${f1(n.add)} mm above the nipple seat, as entered.` : '';\n  if(n.washer) t+=(t?' ':'')+`<b>Washers:<\/b> each lifts its nipple ${f1(n.washer)} mm, which is added to the length.`;\n  if(t && n.type!=='custom') t+=' Nipple figures are typical (makes differ by about \u00b11 mm): check one spoke before ordering in bulk.';\n  return t||null;\n}\nfunction rimLive(host,erd){\n  const wall=rimMode(host)==='measure' && num(host,'rimWall')>0 ? num(host,'rimWall') : 4;\n  const bsd=parseInt(val(host,'bsd'),10)||622, calc=(bsd-erd)\/2+SEAT_TO_HOOK-4+wall;\n  const hIn=num(host,'rimH'), wIn=num(host,'rimWidth');\n  return {bsd, rimWidth:wIn>0?wIn:22, rimDepthCalc:calc, rimDepthSet:hIn>0, rimDepth:hIn>0?hIn:Math.max(12,calc)};\n}\nconst CROSSES = [0,1,2,3,4];\n\/\/ past 90\u00b0 between hub hole and rim hole a spoke leaves the flange beyond tangent, back over the hub\nconst CROSS_MAX_DEG = 90;\nconst crossLabel = c => c===0 ? \"Radial\" : c+\"-cross\";\n\n\/* tooltips *\/\nconst TIPS = {\n  flL:\"Step 4: NDS flange \u00d8, the circle through the middle of the spoke holes (pitch circle). Straight pull: the circle through the spoke anchor points. Shown as R1 (half the \u00d8) in the dish view.\",\n  flR:\"Step 5: DS flange \u00d8, the circle through the middle of the spoke holes, as in step 4. Shown as R4 (half the \u00d8) in the dish view.\",\n  fcL:\"Step 2 (spec sheet): NDS flange to wheel centre, as printed on the hub spec sheet (often called flange distance or centre to flange). Shown as W-NDS in the dish view.\",\n  fcR:\"Step 3 (spec sheet): DS flange to wheel centre, as printed on the hub spec sheet. Shown as W-DS in the dish view.\",\n  hubWidth:\"Step 1: hub width over the lock-nuts, the distance between the two outer end faces that clamp in the fork or frame. Usually 100 or 110 mm at the front and 130, 135, 142, 148 or 157 mm at the rear. Tap a common size to fill it in.\",\n  endL:\"Step 2: from the NDS (left, disc-side) end face to the middle of the NDS flange, the line through the spoke holes (straight pull: the middle of the spoke anchors).\",\n  endR:\"Step 3: from the middle of the DS (right) flange to the DS end face (rear wheels: the cassette side).\",\n  hole:\"Step 6: spoke hole \u00d8 in the flange, usually 2.5 to 2.7 mm. Half of it is subtracted from the spoke length, so leave 2.6 if you are not sure.\",\n  drill:\"How the spoke holes are spaced around the rim (not sideways: that is steps 9 and 10). Even = equal spacing, almost every rim. Paired = holes in pairs, one NDS and one DS hole close together with a bigger gap to the next pair (some factory wheels, 1:1 lacing); leading and trailing spokes then get two lengths per side. Holes grouped in threes (some 2:1 factory wheels) are not covered yet.\",\n  pairSpacing:\"Step 11: distance along the rim between the centres of the two holes of one pair, in mm. It must be smaller than the even spacing shown next to the label. Assumes a hub with evenly spaced flange holes.\",\n  grpAngle:\"Straight-pull group offset: how far the anchors sit rotated from the standard position of the chosen anchor layout, in degrees. Leading spokes get \u03b8 + offset and trailing spokes \u03b8 \u2212 offset, so the two groups differ in spoke angle and length, and a little in tension (the wheel's wind-up balance). 0 = none.\",\n  spLayout:\"How the straight-pull hub holds its spokes. Paired (standard, most straight-pull hubs): each anchor sends out one leading and one trailing spoke, so a crossed spoke lands half a hole further round than on a J-bend hub: \u03b8 = (crosses + \u00bd) \u00d7 360\u00b0 \/ spokes per side. Evenly spaced: one anchor per spoke, spaced like J-bend holes: \u03b8 = crosses \u00d7 360\u00b0 \/ spokes per side. A radial side has no crossings, so its anchors are taken as evenly spaced in both layouts.\",\n  grpSpreadL:\"NDS anchor spread (A2): distance between the centres of the two anchor rows on the NDS flange (leading and trailing spokes), in mm. In the drawing it is split evenly around the red reference point where step 2 ends. Each group gets its own flange-to-centre distance, bracing angle and length; the tension stays even within the side. Positive = leading group outboard; 0 = one row.\",\n  grpSpreadR:\"DS anchor spread (A3): distance between the centres of the two anchor rows on the DS flange, in mm. Split evenly around the red reference point where step 3 ends. Each group gets its own bracing angle and length; the tension stays even within the side. Positive = leading group outboard; 0 = one row.\",\n  offL:\"NDS spoke offset: distance from the anchor point on the anchor circle back to the seat under the straight-pull head, measured along the spoke. It is added to the length; hub spec sheets often call it the spoke-hole offset. 0 = the anchor circle runs through the head seat.\",\n  offR:\"DS spoke offset: distance from the anchor point on the anchor circle back to the seat under the straight-pull head, measured along the spoke. It is added to the length; hub spec sheets often call it the spoke-hole offset. 0 = the anchor circle runs through the head seat.\",\n  rimInner:\"Step 7: internal rim diameter, across the inside of the rim from the spoke bed at one spoke hole to the spoke bed at the hole opposite (the hub-facing surface the nipples come through). This is not the ERD: step 8 and the nipple add the rest.\",\n  rimWall:\"Step 8: thickness of the rim's inner wall (spoke bed) at a spoke hole, eyelet included if the rim has eyelets. The nipple seats on top of it. Usually 1.5 to 4 mm. Nipple seat \u00d8 = step 7 + 2 \u00d7 step 8.\",\n  erd:\"Effective Rim Diameter: the diameter on which the spoke ends sit, from the rim spec sheet. Below it, say what it was measured to (most spec sheets: a standard nipple). No spec sheet? Switch to Measure the rim. Parameter drawing: R is the nipple-seat radius; the spokes end the nipple's spoke-end height further in.\",\n  erdRef:\"What the ERD was measured to. Standard nipple: most spec sheets, the spoke end in a round 12 or 14 mm nipple, about 2 mm above the nipple seat. My nipple: you measured it yourself with the nipples you build with, so it is used as entered. Nipple seat: nipple bed to nipple bed, without the nipple head (some carbon rim makers); the nipple below adds the rest.\",\n  nipType:\"Nipple type. It sets where the spoke must end, measured from the nipple seat (where the nipple rests on the spoke bed): round slotted 12 or 14 mm about 2 mm (the reference), round 16 mm about 0.6 mm, hex head about 2.5 mm, double square about 3.2 mm, internal about the nipple length minus 1 mm. Typical values: makes differ by about \u00b11 mm, so use Custom with the maker's figure when you have it.\",\n  nipLen:\"Nipple length. Round 12 and 14 mm nipples take the same spoke. Long 16 mm nipples start their thread deeper in the barrel, so the spoke has to stop lower (about 1.4 mm shorter) or the nipple runs out of spoke thread. Internal nipples are filled almost to the top, so a longer internal nipple takes a longer spoke.\",\n  nipMat:\"Brass or alloy (aluminium). Changes the weight estimate only.\",\n  nipAdd:\"Custom nipple: the distance from the nipple seat (the face the nipple rests on) to where the spoke should end, in mm. Take it from the nipple maker, or measure it.\",\n  washer:\"Washer under each nipple (common on carbon rims), in mm. It lifts the nipple by its thickness, so each spoke gets that much longer. Leave empty when the rim's ERD already includes the washers. With ERD measured to \\\"My nipple\\\" the ERD is used as you measured it, so washers are taken as included.\",\n  osb:\"Step 9: offset spoke bed (asymmetric rims): how far the spoke holes sit off the rim centre line, in mm. Pick the side with the buttons next to the field: rear wheels are usually offset toward the NDS, disc front wheels toward the DS (away from the disc). 0 for symmetric rims.\",\n  rimZig:\"Step 10: zigzag drilling: how far each spoke hole sits off the spoke-bed centre, toward the flange it feeds. Also for 1:2 \/ 2:1 rims, where each hole leans toward its own side. Adds to the offset spoke bed; use a negative value if the holes lean away from their flange.\",\n  count:\"Total spoke count for the wheel (both sides). 21h and 27h suit triplet (1:2) lacing.\",\n  ratio:\"Spokes per side, NDS:DS. 1:1 is the standard even split. 1:2 (triplet, e.g. 9\/18 on 27h) puts twice as many spokes on the drive side, raising NDS tension on dished rear wheels. 2:1 mirrors this for disc fronts. Total count must divide by 2 (1:1) or 3 (1:2, 2:1). Triplet lengths assume evenly spaced rim holes; rims with grouped drilling can differ by up to ~1 mm.\",\n  cross:\"Lacing pattern. Radial = 0 crosses. Higher cross = longer spokes. Per side laces NDS and DS differently. 1:2 and 2:1 are always set per side: the two sides have a different number of spokes, so one cross number gives each side a different spoke angle \u2014 e.g. a 2:1 disc front with the 14-spoke rotor side 3-cross and the 7-spoke side radial. Patterns that put the spoke angle past 90\u00b0 are greyed out: the spoke would leave the flange beyond tangent.\",\n  tension:\"Target tension on the higher-tension side, in kgf. The calculator works out which side that is (usually the drive side of a rear wheel and the rotor side of a disc front) and predicts the other. Used for stretch and balance.\",\n  tenMax:\"Maximum spoke tension the rim manufacturer allows, in kgf. The calculator checks your target against it.\",\n  tenMin:\"Optional: the rim maker's minimum spoke tension, in kgf. Below it nipples tend to loosen in service. When you enter it, the calculator checks the predicted low side (and each spoke group) against it; left empty, no minimum is checked.\",\n  hubW:\"Hub weight in grams (optional, for total build weight).\",\n  rimW:\"Rim weight in grams (optional).\",\n  spokeW:\"Override: weight per spoke in grams. Leave blank to estimate from the model.\",\n  bsd:\"Wheel size by bead seat diameter (BSD, the number on the tyre sidewall, e.g. 622 for 28\u2033\/700C\/29\u2033). Together with the ERD it gives the rim height for the drawings and the 3D view; spoke lengths only use the ERD.\",\n  rimWidth:\"Outer width of the rim in mm, from the rim's spec sheet. Shapes the drawings and the 3D view only.\",\n  rimH:\"Rim height (profile depth) in mm, from the spec sheet. Leave empty to estimate it as (BSD \u2212 nipple seat \u00d8)\/2 + 10 mm: the nipple seat to the inner wall plus the bead seat to the hook. Shapes the drawings and the 3D view only.\",\n  customDia:\"Mid-section thickness of the spoke in mm. Butted spokes: the thin middle section; aero\/bladed spokes: the round donor-wire gauge. Weight and stretch are both derived from this \u00d8 (steel, 7.85 g\/cm\u00b3).\"\n};\n\n\/* ---------- core spoke maths ---------- *\/\n\/* ================= LEAD CAPTURE (HubSpot) =================\n   Create a HubSpot form with these field internal names:\n     email, firstname, lastname, company, wheel_type, wheel_configuration\n   then fill in portalId + formGuid below. While the placeholders\n   remain, the gate stays OFF and printing works directly.\n   GDPR: configure consent text\/processing on the HubSpot form itself;\n   the modal shows a consent checkbox and links to the privacy policy. *\/\nconst LEAD_GATE={\n  enabled:true,\n  portalId:\"5340083\",\n  formGuid:\"2e33de3a-308c-452e-b970-af7305a3357f\",\n  privacyUrl:\"https:\/\/hollandmechanics.com\/privacy-policy\/\"\n};\nfunction gateActive(){\n  return LEAD_GATE.enabled && \/^\\d+$\/.test(LEAD_GATE.portalId) && LEAD_GATE.formGuid.length>20;\n}\nlet leadUnlocked=false;\ntry{ if(localStorage.getItem('hm_lead_unlocked')==='1') leadUnlocked=true; }catch(e){}\nconst LAST_CALC={};\n\nfunction sideLength(o){\n  \/\/ o: {flangeDia, flangeToCenter, erd, count, cross, hole, hubType, offset, paired}\n  const R = o.erd\/2;\n  const r = o.flangeDia\/2;\n  const f = o.flangeToCenter;\n  \/\/ paired straight-pull anchors put the rim half a pitch round, so a crossed spoke spans k+1\/2 pitches\n  const half = (o.paired && o.cross>0) ? 0.5 : 0;\n  const theta = (o.cross + half) * 2*Math.PI \/ (o.sideCount || o.count\/2) + (o.thetaOffset||0); \/\/ pitch per flange = 2\u03c0\/spokes-on-that-side\n  const chord = Math.sqrt(r*r + R*R - 2*r*R*Math.cos(theta)); \/\/ in-plane chord\n  let L = Math.sqrt(r*r + R*R + f*f - 2*r*R*Math.cos(theta)); \/\/ to hole centre\n  if(o.hubType===\"classic\"){ L -= o.hole\/2; }\n  else { L += (o.offset||0); }   \/\/ the straight-pull head seat sits behind the anchor point\n  \/\/ geometry angles (deg)\n  const brace = Math.atan2(f, chord) * 180\/Math.PI;                       \/\/ out-of-plane lean\n  const flangeExit = 180 - Math.acos(clamp((r*r+chord*chord-R*R)\/(2*r*chord)))*180\/Math.PI;\n  const rimWrap   = Math.acos(clamp((R*R+chord*chord-r*r)\/(2*R*chord)))*180\/Math.PI;\n  return {L, chord, brace, flangeExit, rimWrap, theta:theta*180\/Math.PI};\n}\nfunction clamp(x){ return Math.max(-1,Math.min(1,x)); }\n\n\/* ---------- build a wheel panel ---------- *\/\nfunction buildPanel(key, host){\n  const isRear = key===\"rear\";\n  host.innerHTML = `\n  <div class=\"grid\">\n    <!-- HUB -->\n    <div class=\"card\" data-card=\"hub\">\n      <h2><span class=\"hd-l\"><span class=\"hd-ico\">${ICON.hub}<\/span>Hub<\/span>\n        <span class=\"seg\" data-seg=\"hubType\">\n          <button type=\"button\" data-v=\"classic\" aria-pressed=\"true\">J-bend<\/button>\n          <button type=\"button\" data-v=\"straight\" aria-pressed=\"false\">Straight pull<\/button>\n        <\/span>\n      <\/h2>\n      <p class=\"note\">${isRear?'Rear':'Front'} hub. Measure it as shown, or switch to the values on the hub's spec sheet. NDS = left (disc side) \u00b7 DS = right${isRear?' (cassette side)':''}.<\/p>\n      <div class=\"hub-src\">\n        <span class=\"seg\" data-seg=\"hubMode\">\n          <button type=\"button\" data-v=\"measure\" aria-pressed=\"true\">Measure the hub<\/button>\n          <button type=\"button\" data-v=\"spec\" aria-pressed=\"false\">Spec sheet<\/button>\n        <\/span>\n      <\/div>\n      <div class=\"hubm\" data-hubart><\/div>\n      <div class=\"fields\">\n        <div class=\"field full measure-only\">\n          <label><span class=\"step\">1<\/span>Hub width ${tip('hubWidth')}<span class=\"u\">over the lock-nuts<\/span><\/label>\n          <div class=\"hw-row\"><span class=\"inp\"><input data-f=\"hubWidth\" inputmode=\"decimal\" placeholder=\"${isRear?'142':'100'}\"><em>mm<\/em><\/span><span class=\"hw-chips\">${(isRear?[130,135,142,148]:[100,110]).map(v=>`<button type=\"button\" data-hw=\"${v}\" aria-pressed=\"false\">${v}<\/button>`).join('')}<\/span><\/div>\n        <\/div>\n        <div class=\"field measure-only\"><label><span class=\"step\">2<\/span>NDS end \u2192 flange ${tip('endL')}<\/label><span class=\"inp\"><input data-f=\"endL\" inputmode=\"decimal\" placeholder=\"${isRear?'36':'16'}\"><em>mm<\/em><\/span><\/div>\n        <div class=\"field measure-only\"><label><span class=\"step\">3<\/span>DS flange \u2192 end ${tip('endR')}<\/label><span class=\"inp\"><input data-f=\"endR\" inputmode=\"decimal\" placeholder=\"${isRear?'52':'16'}\"><em>mm<\/em><\/span><\/div>\n        <div class=\"field spec-only\"><label><span class=\"step\">2<\/span>NDS flange \u2192 centre ${tip('fcL')}<\/label><span class=\"inp\"><input data-f=\"fcL\" inputmode=\"decimal\" placeholder=\"${isRear?'35':'34'}\"><em>mm<\/em><\/span><\/div>\n        <div class=\"field spec-only\"><label><span class=\"step\">3<\/span>DS flange \u2192 centre ${tip('fcR')}<\/label><span class=\"inp\"><input data-f=\"fcR\" inputmode=\"decimal\" placeholder=\"${isRear?'19':'34'}\"><em>mm<\/em><\/span><\/div>\n        <div class=\"field full measure-only\"><div class=\"fc-out\" data-fcout><\/div><\/div>\n        <div class=\"field\"><label><span class=\"step\">4<\/span>NDS flange \u00d8 ${tip('flL')}<\/label><span class=\"inp\"><input data-f=\"flL\" inputmode=\"decimal\" placeholder=\"44\"><em>mm<\/em><\/span><\/div>\n        <div class=\"field\"><label><span class=\"step\">5<\/span>DS flange \u00d8 ${tip('flR')}<\/label><span class=\"inp\"><input data-f=\"flR\" inputmode=\"decimal\" placeholder=\"44\"><em>mm<\/em><\/span><\/div>\n        <div class=\"field classic-only\"><label><span class=\"step\">6<\/span>Spoke hole \u00d8 ${tip('hole')}<span class=\"u\">usually 2.6<\/span><\/label><span class=\"inp\"><input data-f=\"hole\" inputmode=\"decimal\" placeholder=\"2.6\" value=\"2.6\"><em>mm<\/em><\/span><\/div>\n      <\/div>\n      <details class=\"adv straight-only\" data-adv style=\"display:none\">\n        <summary>Advanced straight-pull settings <span class=\"adv-sum\" data-advsum><\/span><\/summary>\n        <p class=\"note adv-note\">Most straight-pull hubs need nothing here.<\/p>\n        <div class=\"fields\">\n          <div class=\"field full\">\n            <label>Anchor layout ${tip('spLayout')}<\/label>\n            <div class=\"choices\" data-choice=\"spLayout\">\n              <button type=\"button\" data-v=\"paired\" aria-pressed=\"true\">Paired<\/button>\n              <button type=\"button\" data-v=\"even\" aria-pressed=\"false\">Evenly spaced<\/button>\n            <\/div>\n          <\/div>\n          <div class=\"field\"><label>Spoke offset NDS ${tip('offL')}<span class=\"u\">0 = none<\/span><\/label><span class=\"inp\"><input data-f=\"offL\" inputmode=\"decimal\" placeholder=\"0\"><em>mm<\/em><\/span><\/div>\n          <div class=\"field\"><label>Spoke offset DS ${tip('offR')}<span class=\"u\">0 = none<\/span><\/label><span class=\"inp\"><input data-f=\"offR\" inputmode=\"decimal\" placeholder=\"0\"><em>mm<\/em><\/span><\/div>\n          <div class=\"field\"><label><span class=\"dimb\">A2<\/span>Anchor spread NDS ${tip('grpSpreadL')}<span class=\"u\">0 = one row<\/span><\/label><span class=\"inp\"><input data-f=\"grpSpreadL\" inputmode=\"decimal\" placeholder=\"0\"><em>mm<\/em><\/span><\/div>\n          <div class=\"field\"><label><span class=\"dimb\">A3<\/span>Anchor spread DS ${tip('grpSpreadR')}<span class=\"u\">0 = one row<\/span><\/label><span class=\"inp\"><input data-f=\"grpSpreadR\" inputmode=\"decimal\" placeholder=\"0\"><em>mm<\/em><\/span><\/div>\n          <div class=\"field full\"><label>Spoke group offset ${tip('grpAngle')}<span class=\"u\">0 = none<\/span><\/label><span class=\"inp\"><input data-f=\"grpAngle\" inputmode=\"decimal\" placeholder=\"0\"><em>\u00b0<\/em><\/span><\/div>\n        <\/div>\n      <\/details>\n      <div class=\"fields hub-opt\">\n        <div class=\"field name\"><label>Hub name <span class=\"u\">optional<\/span><\/label><input data-f=\"hubName\" placeholder=\"e.g. Holland Mechanics RD-32\"><\/div>\n        <div class=\"field\"><label>Hub weight ${tip('hubW')}<span class=\"u\">optional<\/span><\/label><span class=\"inp\"><input data-f=\"hubW\" inputmode=\"decimal\" placeholder=\"0\"><em>g<\/em><\/span><\/div>\n      <\/div>\n    <\/div>\n\n    <!-- RIM -->\n    <div class=\"card\" data-card=\"rim\">\n      <h2><span class=\"hd-l\"><span class=\"hd-ico\">${ICON.rim}<\/span>Rim<\/span><\/h2>\n      <p class=\"note\">Where the spokes end drives the calculation: the rim's ERD (spec sheet or measured) plus the nipple. OSB matters for asymmetric rims.<\/p>\n      <div class=\"hub-src\">\n        <span class=\"seg\" data-seg=\"rimMode\">\n          <button type=\"button\" data-v=\"spec\" aria-pressed=\"true\">Spec sheet (ERD)<\/button>\n          <button type=\"button\" data-v=\"measure\" aria-pressed=\"false\">Measure the rim<\/button>\n        <\/span>\n      <\/div>\n      <div class=\"hubm rim-measure-only\" data-rimart><\/div>\n      <div class=\"fields\">\n        <div class=\"field name full\"><label>Rim name <span class=\"u\">optional<\/span><\/label><input data-f=\"rimName\" placeholder=\"e.g. carbon 45mm\"><\/div>\n        <div class=\"field rim-measure-only\"><label><span class=\"step\">7<\/span>Internal rim \u00d8 ${tip('rimInner')}<span class=\"u\">not the ERD<\/span><\/label><span class=\"inp\"><input data-f=\"rimInner\" inputmode=\"decimal\" placeholder=\"556\"><em>mm<\/em><\/span><\/div>\n        <div class=\"field rim-measure-only\"><label><span class=\"step\">8<\/span>Inner wall thickness ${tip('rimWall')}<span class=\"u\">at a spoke hole<\/span><\/label><span class=\"inp\"><input data-f=\"rimWall\" inputmode=\"decimal\" placeholder=\"2\"><em>mm<\/em><\/span><\/div>\n        <div class=\"field full rim-measure-only\"><div class=\"fc-out\" data-erdout><\/div><\/div>\n        <div class=\"field full rim-spec-only\"><label>Rim ERD ${tip('erd')}<span class=\"u\">spec sheet<\/span><\/label><span class=\"inp\"><input data-f=\"erd\" inputmode=\"decimal\" placeholder=\"560\"><em>mm<\/em><\/span><\/div>\n        <div class=\"field full rim-spec-only\"><label>ERD measured to ${tip('erdRef')}<\/label><span class=\"seg erd-ref\" data-seg=\"erdRef\"><button type=\"button\" data-v=\"std\" aria-pressed=\"true\">Standard nipple<\/button><button type=\"button\" data-v=\"mine\" aria-pressed=\"false\">My nipple<\/button><button type=\"button\" data-v=\"seat\" aria-pressed=\"false\">Nipple seat<\/button><\/span><\/div>\n        <div class=\"field full nip-field\"><label>Nipple ${tip('nipType')}<span class=\"u\">sets where the spoke ends<\/span><\/label>\n          <div class=\"choices nip-type\" data-choice=\"nipType\">${Object.keys(NIPPLES).map(t=>`<button type=\"button\" data-v=\"${t}\" aria-pressed=\"${t==='round'}\"><span class=\"ni\">${nipIconSVG(t)}<\/span><span class=\"nl\">${NIPPLES[t].name}<\/span><\/button>`).join('')}<\/div><\/div>\n        <div class=\"field nip-len-f\"><label>Nipple length ${tip('nipLen')}<\/label><div class=\"choices nip-len\" data-choice=\"nipLen\"><\/div><\/div>\n        <div class=\"field nip-mat-f\"><label>Material ${tip('nipMat')}<\/label><div class=\"choices nip-mat\" data-choice=\"nipMat\"><button type=\"button\" data-v=\"brass\" aria-pressed=\"true\">Brass<\/button><button type=\"button\" data-v=\"alu\" aria-pressed=\"false\">Alloy<\/button><\/div><\/div>\n        <div class=\"field nip-custom\" style=\"display:none\"><label>Spoke end above the seat ${tip('nipAdd')}<\/label><span class=\"inp\"><input data-f=\"nipAdd\" inputmode=\"decimal\" placeholder=\"2.0\"><em>mm<\/em><\/span><\/div>\n        <div class=\"field\"><label>Washer ${tip('washer')}<span class=\"u\">optional<\/span><\/label><span class=\"inp\"><input data-f=\"washer\" inputmode=\"decimal\" placeholder=\"none\"><em>mm<\/em><\/span><\/div>\n        <div class=\"field full\"><div class=\"nip-box\"><div class=\"nip-art\" data-nipart><\/div><div class=\"nip-out\" data-nipout><\/div><\/div><\/div>\n        <div class=\"field\"><label><span class=\"step\">9<\/span>Offset spoke bed ${tip('osb')}<span class=\"u\">0 = symmetric<\/span><\/label><div class=\"hw-row osb-row\"><span class=\"inp\"><input data-f=\"osb\" inputmode=\"decimal\" placeholder=\"0\" value=\"0\"><em>mm<\/em><\/span><span class=\"seg osb-side\" data-seg=\"osbSide\" title=\"Side the spoke holes are offset toward\"><button type=\"button\" data-v=\"NDS\" aria-pressed=\"${isRear?'true':'false'}\">\u2190 NDS<\/button><button type=\"button\" data-v=\"DS\" aria-pressed=\"${isRear?'false':'true'}\">DS \u2192<\/button><\/span><\/div><\/div>\n        <div class=\"field\"><label><span class=\"step\">10<\/span>Rim hole offset left\/right ${tip('rimZig')}<span class=\"u\">0 = none<\/span><\/label><span class=\"inp\"><input data-f=\"rimZig\" inputmode=\"decimal\" placeholder=\"0\" value=\"0\"><em>mm<\/em><\/span><\/div>\n        <div class=\"field full\">\n          <details class=\"adv osb-guide\" data-osbguide>\n            <summary>How to measure <span class=\"step\">9<\/span> and <span class=\"step\">10<\/span><\/summary>\n            <div class=\"hubm osbm\" data-osbart><\/div>\n          <\/details>\n        <\/div>\n        <div class=\"field full\"><label>Wheel size ${tip('bsd')}<\/label><select data-f=\"bsd\">${WHEEL_SIZES.map(([b,l])=>`<option value=\"${b}\"${b===622?' selected':''}>${l} \u00b7 BSD ${b}<\/option>`).join('')}<\/select><\/div>\n        <div class=\"field\"><label>Rim width ${tip('rimWidth')}<span class=\"u\">outer<\/span><\/label><span class=\"inp\"><input data-f=\"rimWidth\" inputmode=\"decimal\" placeholder=\"22\"><em>mm<\/em><\/span><\/div>\n        <div class=\"field\"><label>Rim height ${tip('rimH')}<span class=\"u\">auto<\/span><\/label><span class=\"inp\"><input data-f=\"rimH\" inputmode=\"decimal\" placeholder=\"auto\"><em>mm<\/em><\/span><\/div>\n        <div class=\"field full\"><p class=\"note\" style=\"margin:0\" data-rimhint><\/p><\/div>\n        <div class=\"field\"><label>Rim weight ${tip('rimW')}<span class=\"u\">optional<\/span><\/label><span class=\"inp\"><input data-f=\"rimW\" inputmode=\"decimal\" placeholder=\"0\"><em>g<\/em><\/span><\/div>\n      <\/div>\n      <div class=\"fields one\" style=\"margin-top:13px\">\n        <div class=\"field full\">\n          <label>Hole spacing around the rim ${tip('drill')}<\/label>\n          <div class=\"choices drill\" data-choice=\"drill\">\n            <button type=\"button\" data-v=\"even\" aria-pressed=\"true\">Even<\/button>\n            <button type=\"button\" data-v=\"zigzag\" aria-pressed=\"false\">Paired<\/button>\n          <\/div>\n        <\/div>\n        <div class=\"field full drill-angle\" style=\"display:none\"><label><span class=\"step\">11<\/span>Hole spacing within a pair ${tip('pairSpacing')}<span class=\"u\" data-evenhint><\/span><\/label><span class=\"inp\"><input data-f=\"pairSpacing\" inputmode=\"decimal\" placeholder=\"20\"><em>mm<\/em><\/span><\/div>\n        <div class=\"field full\">\n          <details class=\"adv osb-guide\" data-drillguide>\n            <summary>How to recognise the hole pattern<\/summary>\n            <div class=\"hubm osbm\" data-drillart><\/div>\n          <\/details>\n        <\/div>\n      <\/div>\n    <\/div>\n    <\/div>\n\n    <!-- LACING -->\n    <div class=\"card span2\" data-card=\"lacing\">\n      <h2><span class=\"hd-l\"><span class=\"hd-ico\">${ICON.lacing}<\/span>Lacing<\/span><\/h2>\n      <div class=\"fields one\" style=\"gap:18px\">\n        <div class=\"field full\">\n          <label>Number of spokes ${tip('count')}<\/label>\n          <div class=\"choices\" data-choice=\"count\"><\/div>\n        <\/div>\n        <div class=\"field full\">\n          <label>Spokes per side \u00b7 NDS:DS ${tip('ratio')}<\/label>\n          <div class=\"choices\" data-choice=\"ratio\">\n            <button type=\"button\" data-v=\"1:1\" aria-pressed=\"true\">1:1<\/button>\n            <button type=\"button\" data-v=\"1:2\" aria-pressed=\"false\">1:2<\/button>\n            <button type=\"button\" data-v=\"2:1\" aria-pressed=\"false\">2:1<\/button>\n          <\/div>\n        <\/div>\n        <div class=\"field full\">\n          <label>Lacing pattern ${tip('cross')}<span class=\"seg seg-mini\" data-seg=\"crossMode\"><button type=\"button\" data-v=\"same\" aria-pressed=\"true\">Same both sides<\/button><button type=\"button\" data-v=\"split\" aria-pressed=\"false\">Per side<\/button><\/span><\/label>\n          <div class=\"cross-row\"><span class=\"cross-side\">NDS<\/span><div class=\"choices cross\" data-choice=\"cross\"><\/div><\/div>\n          <div class=\"cross-row cross-r\" style=\"display:none\"><span class=\"cross-side\">DS<\/span><div class=\"choices cross\" data-choice=\"crossR\"><\/div><\/div>\n        <\/div>\n      <\/div>\n    <\/div>\n\n    <!-- LIVE PREVIEW -->\n    <div class=\"card span2 lp-card\" data-card=\"live\">\n      <h2><span class=\"hd-l\"><span class=\"hd-ico\">${ICON.lacing}<\/span>Live preview<\/span><span class=\"lp-tag\" data-lpdefaults style=\"display:none\">example values shown \u2014 type to update<\/span><\/h2>\n      <p class=\"note\">Redraws as you type. Spoke lengths appear after Calculate.<\/p>\n      <div class=\"lp-views\">\n        <div class=\"lp-view\"><div data-lpfront><\/div><div class=\"lp-cap\" data-lpfrontcap><\/div><\/div>\n        <div class=\"lp-view\"><div data-lpside><\/div><div class=\"lp-cap\" data-lpsidecap><\/div><\/div>\n        <div class=\"lp-stats\" data-lpstats><\/div>\n      <\/div>\n    <\/div>\n\n    <!-- 3D WHEEL -->\n    <div class=\"card span2 w3-card\" data-card=\"wheel3d\">\n      <h2><span class=\"hd-l\"><span class=\"hd-ico\">${ICON.lacing}<\/span>3D wheel<\/span>\n        <span class=\"seg\" data-w3seg=\"color\"><button type=\"button\" data-v=\"side\" aria-pressed=\"false\">Side<\/button><button type=\"button\" data-v=\"lead\" aria-pressed=\"true\">Groups<\/button><button type=\"button\" data-v=\"tension\" aria-pressed=\"false\">Tension<\/button><\/span>\n      <\/h2>\n      <p class=\"note\">Built live from your inputs. Drag to rotate \u00b7 scroll or pinch to zoom \u00b7 right-drag to pan \u00b7 hover a spoke for its details.<\/p>\n      <div class=\"w3-stage\" data-w3stage><div class=\"w3-msg\" data-w3msg>Loading the 3D view\u2026<\/div><div class=\"w3-tip\" data-w3tip><\/div><div class=\"w3-legend\" data-w3legend><\/div><\/div>\n      <div class=\"w3-bar\">\n        <span class=\"seg\" data-w3seg=\"view\"><button type=\"button\" data-v=\"iso\" aria-pressed=\"true\">3\/4<\/button><button type=\"button\" data-v=\"front\" aria-pressed=\"false\">Front<\/button><button type=\"button\" data-v=\"side\" aria-pressed=\"false\">Dish<\/button><button type=\"button\" data-v=\"hub\" aria-pressed=\"false\">Hub<\/button><\/span>\n        <button type=\"button\" class=\"btn btn-ghost w3-btn\" data-w3act=\"lace\">Simulate lacing<\/button>\n        <button type=\"button\" class=\"btn btn-ghost w3-btn\" data-w3act=\"spin\" aria-pressed=\"false\">Spin<\/button>\n        <button type=\"button\" class=\"btn btn-ghost w3-btn\" data-w3act=\"tyre\" aria-pressed=\"false\">Tyre<\/button>\n      <\/div>\n      <div class=\"w3-bar w3-look\">\n        <span class=\"w3-lbl\">Rim finish<\/span>\n        <span class=\"seg\" data-w3seg=\"finish\"><button type=\"button\" data-v=\"auto\" aria-pressed=\"true\">Auto<\/button><button type=\"button\" data-v=\"alloy\" aria-pressed=\"false\">Alloy<\/button><button type=\"button\" data-v=\"ud\" aria-pressed=\"false\">Carbon UD<\/button><button type=\"button\" data-v=\"3k\" aria-pressed=\"false\">Carbon 3K<\/button><\/span>\n        <span class=\"w3-lbl\">Decal<\/span>\n        <span class=\"seg\" data-w3seg=\"decal\"><button type=\"button\" data-v=\"none\" aria-pressed=\"true\">None<\/button><button type=\"button\" data-v=\"logo\" aria-pressed=\"false\">HM logo<\/button><button type=\"button\" data-v=\"text\" aria-pressed=\"false\">Text<\/button><button type=\"button\" data-v=\"stripes\" aria-pressed=\"false\">Stripes<\/button><\/span>\n        <input class=\"w3-text\" data-w3text maxlength=\"18\" value=\"HM RACE 45\" aria-label=\"Decal text\" style=\"display:none\">\n        <span class=\"w3-swatches\" data-w3swatches style=\"display:none\"><button type=\"button\" class=\"w3-swatch\" data-w3color=\"#FFFFFF\" aria-pressed=\"true\" aria-label=\"White\" style=\"background:#FFFFFF\"><\/button><button type=\"button\" class=\"w3-swatch\" data-w3color=\"#E63329\" aria-pressed=\"false\" aria-label=\"HM red\" style=\"background:#E63329\"><\/button><button type=\"button\" class=\"w3-swatch\" data-w3color=\"#0065E3\" aria-pressed=\"false\" aria-label=\"Blue\" style=\"background:#0065E3\"><\/button><button type=\"button\" class=\"w3-swatch\" data-w3color=\"#C5CED8\" aria-pressed=\"false\" aria-label=\"Silver\" style=\"background:#C5CED8\"><\/button><button type=\"button\" class=\"w3-swatch\" data-w3color=\"#2A2E33\" aria-pressed=\"false\" aria-label=\"Stealth black\" style=\"background:#2A2E33\"><\/button><\/span>\n        <span class=\"w3-concept\">Concept preview<\/span>\n      <\/div>\n    <\/div>\n\n    <!-- SPOKES \/ TENSION -->\n    <div class=\"card span2\" data-card=\"spoke\">\n      <h2><span class=\"hd-l\"><span class=\"hd-ico\">${ICON.spoke}<\/span>Spokes &amp; tension<\/span>\n        <span class=\"seg\" data-seg=\"spokeType\">\n          <button type=\"button\" data-v=\"std\" aria-pressed=\"true\">Standard<\/button>\n          <button type=\"button\" data-v=\"tcs\" aria-pressed=\"false\">TCS<\/button>\n        <\/span>\n      <\/h2>\n      <p class=\"note\">For weight and stretch estimates \\u2014 stretch assumes the mid-section thickness over the full length (slightly high for butted spokes). <a class=\"tcs-link\" href=\"https:\/\/hollandmechanics.com\/en\/tcs-spoke\/\" target=\"_blank\" rel=\"noopener\">About TCS spokes \u2192<\/a><\/p>\n      <div class=\"spoke-illo\"><div class=\"dwrap\" data-spokeart><\/div><\/div>\n      <div class=\"fields\">\n        <div class=\"field full\"><label>Spoke model<\/label>\n          <select data-f=\"model\"><\/select><\/div>\n        <div class=\"field custom-spoke\" style=\"display:none\"><label>Custom name <span class=\"u\">optional<\/span><\/label><input data-f=\"customName\" placeholder=\"e.g. HM house spoke 2.0\"><\/div>\n        <div class=\"field custom-spoke\" style=\"display:none\"><label>Mid-section \\u00d8 ${tip('customDia')}<\/label><span class=\"inp\"><input data-f=\"customDia\" inputmode=\"decimal\" placeholder=\"2.0\"><em>mm<\/em><\/span><\/div>\n        <div class=\"field full custom-spoke\" style=\"display:none\"><p class=\"note\" style=\"margin:0\" data-customhint><\/p><\/div>\n        <div class=\"field\"><label>Weight per spoke ${tip('spokeW')}<span class=\"u\">optional<\/span><\/label><span class=\"inp\"><input data-f=\"spokeW\" inputmode=\"decimal\" placeholder=\"auto\"><em>g<\/em><\/span><\/div>\n        <div class=\"field\"><label>Target tension ${tip('tension')}<\/label><span class=\"inp\"><input data-f=\"tension\" inputmode=\"decimal\" placeholder=\"110\" value=\"110\"><em>kgf<\/em><\/span><\/div>\n        <div class=\"field\"><label>Max rim tension ${tip('tenMax')}<span class=\"u\">optional<\/span><\/label><span class=\"inp\"><input data-f=\"tenMax\" inputmode=\"decimal\" placeholder=\"140\"><em>kgf<\/em><\/span><\/div>\n        <div class=\"field\"><label>Min spoke tension ${tip('tenMin')}<span class=\"u\">optional<\/span><\/label><span class=\"inp\"><input data-f=\"tenMin\" inputmode=\"decimal\" placeholder=\"rim spec\"><em>kgf<\/em><\/span><\/div>\n      <\/div>\n    <\/div>\n\n    <!-- DISCLAIMER -->\n    <div class=\"disclaimer\">\n      <details>\n        <summary>Disclaimer \u2014 read before calculating<\/summary>\n        <p>All measurements are in mm; enter values without units and use a dot (.) as the decimal separator. This calculator is a guide only, provided \"as is\", for metal\/stainless spokes (not titanium). Classic-hub spoke length uses Rinard's exact formula. Straight-pull results follow the standard offset model and must be verified against the hub spec. Nipple spoke-end heights are typical values; makes differ by about \u00b11 mm. Always sanity-check before cutting or ordering spokes.<\/p>\n      <\/details>\n      <label class=\"accept\"><input type=\"checkbox\" data-f=\"accept\"> I have read and accept the disclaimer<\/label>\n    <\/div>\n\n    <div class=\"loaded-banner\" data-banner>Build loaded from a shared link. Accept the disclaimer and press Calculate.<\/div>\n\n    <div class=\"errors\" data-err><h3>Please check these inputs<\/h3><ul><\/ul><\/div>\n\n    <div class=\"actions\">\n      <button class=\"btn btn-primary\" data-act=\"calc\" disabled>Calculate<\/button>\n      <button class=\"btn btn-ghost\" data-act=\"clear\">Clear<\/button>\n      ${isRear?'<button class=\"btn btn-ghost\" data-act=\"copyfront\">Copy from front<\/button>':''}\n      <button class=\"btn btn-ghost\" data-act=\"print\" style=\"display:none\">Print build sheet<\/button>\n      <button class=\"btn btn-ghost btn-share\" data-act=\"share\">Copy share link<\/button>\n    <\/div>\n\n    <!-- RESULTS -->\n    <div class=\"results\" data-results><\/div>\n  <\/div>`;\n\n  \/\/ populate choices\n  const countBox = host.querySelector('[data-choice=\"count\"]');\n  SPOKE_COUNTS.forEach((n,i)=>{\n    const b=document.createElement('button'); b.type='button'; b.textContent=n; b.dataset.v=n;\n    b.setAttribute('aria-pressed', n===32?'true':'false'); countBox.appendChild(b);\n  });\n  ['cross','crossR'].forEach(grp=>{\n    const crossBox = host.querySelector(`[data-choice=\"${grp}\"]`);\n    CROSSES.forEach(c=>{\n    const b=document.createElement('button'); b.type='button'; b.textContent=crossLabel(c); b.dataset.v=c;\n      b.setAttribute('aria-pressed', c===3?'true':'false'); crossBox.appendChild(b);\n    });\n  });\n  \/\/ models\n  const sel = host.querySelector('[data-f=\"model\"]');\n  SPOKE_MODELS.forEach(([name],i)=>{const o=document.createElement('option');o.value=i;o.textContent=name;sel.appendChild(o);});\n  const co=document.createElement('option'); co.value='custom'; co.textContent='Custom spoke\\u2026'; sel.appendChild(co);\n  sel.value = SPOKE_MODELS.findIndex(m=>m[0].startsWith(\"Sapim Race\"));\n  sel.addEventListener('change',()=>toggleCustomSpoke(host));\n\n  wire(host, key);\n  refreshNipple(host);\n  updateHubArt(host);\n  refreshChoices(host);\n  toggleCustomSpoke(host);\n  updateLive(host);\n}\nconst MEASURE = {\"hubWidth\": \"Caliper across the two outer end faces (lock-nuts or end caps), with the end caps fitted that clamp in the frame or fork. Thru-axle hubs: end cap to end cap, not the axle.\", \"endL\": \"Hub level on the bench. Caliper depth rod (or jaws) from the NDS end face to the outer face of the NDS flange, then add half the flange thickness, measured at a spoke hole. Straight pull: measure to the middle of the anchor holes; with two anchor rows, to the outer row plus half of A2.\", \"endR\": \"Caliper from the outer face of the DS flange to the DS end face (rear: the end cap on the cassette side, past the freehub body), then add half the flange thickness. Straight pull: to the middle of the anchor holes; with two anchor rows, to the outer row plus half of A3.\", \"flL\": \"Caliper from the left edge of one spoke hole to the left edge of the hole directly opposite: edge to same edge equals centre to centre. Odd number of holes on the flange (7, 9, 11): measure to one of the two holes nearest opposite and divide by 0.975 (7), 0.985 (9) or 0.990 (11). Straight pull: the same across the anchor holes.\", \"flR\": \"Caliper as in step 4: left edge of one spoke hole to the left edge of the hole directly opposite. Odd number of holes (7, 9, 11): measure to a hole nearest opposite and divide by 0.975 (7), 0.985 (9) or 0.990 (11).\", \"hole\": \"Push drill bits or pin gauges through a spoke hole: the largest one that fits is the hole \u00d8. Or use the inside jaws of a caliper.\", \"fcL\": \"Printed on the hub spec sheet (flange distance, centre to flange). No spec sheet: switch to Measure the hub and fill in steps 1 to 3; the calculator works this out.\", \"fcR\": \"Printed on the hub spec sheet. No spec sheet: switch to Measure the hub and fill in steps 1 to 3; the calculator works this out.\", \"offL\": \"Usually from the hub spec sheet. To measure: seat a spoke in the anchor and measure along the spoke from the seat under the head to where the spoke crosses the anchor circle.\", \"offR\": \"Usually from the hub spec sheet. To measure: seat a spoke in the anchor and measure along the spoke from the seat under the head to where the spoke crosses the anchor circle.\", \"grpSpreadL\": \"Caliper across the NDS flange from one edge of a hole in the inner anchor row to the same edge of a hole in the outer row: same edge to same edge = centre to centre.\", \"grpSpreadR\": \"Caliper across the DS flange from one edge of a hole in the inner anchor row to the same edge of a hole in the outer row: same edge to same edge = centre to centre.\", \"grpAngle\": \"From the hub drawing or the hub maker. Leave 0 when it is not given.\", \"erd\": \"From the rim spec sheet, then choose below what it was measured to. No spec sheet: switch to Measure the rim and follow steps 7 and 8.\", \"rimInner\": \"Caliper inside jaws, or a steel rule on large rims, across the inside of the rim from the spoke bed at one spoke hole to the spoke bed at the hole opposite. Measure two or three hole pairs and take the average. Rims with 21 or 27 holes have no hole exactly opposite: measure to a hole nearest opposite and divide by 0.997 (21) or 0.998 (27).\", \"rimWall\": \"Seat a bare nipple in a spoke hole from the tyre side. Measure the nipple's shank below the head (s), then how far it sticks out on the hub side (p) with the depth rod. Wall = s \u2212 p, eyelet included.\", \"osb\": \"Caliper depth rod on the spoke bed, from each rim edge to the middle of the same spoke hole. Step 9 = (larger \u2212 smaller) \u00f7 2, toward the edge with the smaller distance. On a zigzag-drilled rim, average one left-leaning and one right-leaning hole.\", \"rimZig\": \"Caliper across the spoke bed, sideways only, from the middle of a left-leaning hole to the middle of a right-leaning hole, and enter half of it: 2.5 for rows 5 mm apart.\", \"pairSpacing\": \"Caliper or a flexible rule along the spoke bed, from one edge of the first hole of a pair to the same edge of the second hole.\", \"bsd\": \"Read it from the tyre sidewall or the rim sticker: in a size like 25-622 the second number is the BSD.\", \"rimWidth\": \"Caliper across the outside of the rim at its widest point (the hooks or brake track).\", \"rimH\": \"Caliper jaws from the top of the rim edge to the spoke bed on the hub side, at a spoke hole (a rule works too).\", \"washer\": \"Caliper over one washer.\", \"nipAdd\": \"Thread a nipple onto a spoke until the thread runs out, then measure from the seat face under the nipple head to the spoke end.\", \"customDia\": \"Micrometer or caliper on the middle of the spoke, away from the butts. Bladed spokes: the round wire gauge from the spoke maker.\", \"spokeW\": \"Weigh 10 spokes on a kitchen or jewellery scale and divide by 10.\", \"hubW\": \"Weigh the hub with its end caps on a kitchen scale.\", \"rimW\": \"Weigh the rim without tape or valve.\", \"tenMax\": \"From the rim spec sheet. Given in newtons? 1 kgf = 9.81 N, so 1200 N \u2248 122 kgf.\", \"tenMin\": \"From the rim spec sheet. Given in newtons? 1 kgf = 9.81 N.\", \"tension\": \"On the built wheel, check it with a spoke tension meter on the higher-tension side; the calculator predicts the other side.\"};\nfunction escA(x){ return String(x).replace(\/&\/g,'&amp;').replace(\/<\/g,'&lt;').replace(\/>\/g,'&gt;').replace(\/\"\/g,'&quot;'); }\n\/\/ info icon: what the field is, plus how to measure it, in the page's own tooltip (hover, keyboard focus, tap)\nfunction tip(k){ const t=TIPS[k]||'', h=MEASURE[k]||'';\n  return `<span class=\"tip\" tabindex=\"0\" role=\"note\" aria-label=\"${escA(t+(h?' How to measure: '+h:''))}\" data-info=\"1\" data-tip=\"${escA(t)}\"${h?` data-how=\"${escA(h)}\"`:''}${HOWART.has(k)?` data-art=\"${k}\"`:''}>i<\/span>`; }\n\n\/* ---------- wiring ---------- *\/\nfunction applyHubType(host,v){\n  host.querySelectorAll('[data-seg=\"hubType\"] button').forEach(x=>x.setAttribute('aria-pressed', x.dataset.v===v?'true':'false'));\n  const straight = v===\"straight\";\n  host.querySelectorAll('.classic-only').forEach(e=>e.style.display=straight?'none':'');\n  host.querySelectorAll('.straight-only').forEach(e=>e.style.display=straight?'':'none');\n  updateHubArt(host);\n  if(typeof scheduleLive==='function') scheduleLive(host);\n}\nfunction uniqSVG(svg, key){\n  \/\/ suffix all defs ids per panel so duplicate-id gradient refs can't break fills\n  return svg.replace(\/id=\"([^\"]+)\"\/g, `id=\"${key}_$1\"`).replace(\/url\\(#([^)]+)\\)\/g, `url(#${key}_$1)`);\n}\nfunction applySpokeType(host,v){\n  host.querySelectorAll('[data-seg=\"spokeType\"] button').forEach(x=>x.setAttribute('aria-pressed', x.dataset.v===v?'true':'false'));\n  updateHubArt(host);\n}\n\/* ---------- hub measuring flow ----------\n   The numbered labels in the drawing match the numbered hub fields one to one. *\/\nfunction hubMode(host){ return pressed(host,'[data-seg=\"hubMode\"]')==='spec'?'spec':'measure'; }\nfunction applyHubMode(host,v){\n  host.querySelectorAll('[data-seg=\"hubMode\"] button').forEach(x=>x.setAttribute('aria-pressed', x.dataset.v===v?'true':'false'));\n  const card=host.querySelector('[data-card=\"hub\"]'); if(card) card.classList.toggle('src-spec', v==='spec');\n}\nfunction hubFC(host){\n  if(hubMode(host)==='spec') return {mode:'spec', fcL:num(host,'fcL'), fcR:num(host,'fcR')};\n  const hw=num(host,'hubWidth'), a=num(host,'endL'), b=num(host,'endR'), r3=x=>Math.round(x*1000)\/1000;\n  return {mode:'measure', hw, a, b,\n    fcL:(hw>0 && !isNaN(a)) ? r3(hw\/2-a) : NaN,\n    fcR:(hw>0 && !isNaN(b)) ? r3(hw\/2-b) : NaN};\n}\nfunction hubFcOut(hf){\n  const st=n=>`<span class=\"step\">${n}<\/span>`, mm=x=>(Math.round(x*10)\/10).toFixed(1)+' mm';\n  if(!(hf.hw>0) || isNaN(hf.a) || isNaN(hf.b))\n    return `<span>Fill in ${st(1)} ${st(2)} ${st(3)} and the flange-to-centre distances are worked out for you.<\/span>`;\n  let o=`<span class=\"fco-l\">Flange to centre<\/span> NDS <b>${hf.fcL>0?mm(hf.fcL):'\u2014'}<\/b> \u00b7 DS <b>${hf.fcR>0?mm(hf.fcR):'\u2014'}<\/b>`;\n  if(!(hf.fcL>0) || !(hf.fcR>0)) o+=` <span class=\"warn\">${!(hf.fcL>0)?st(2):''}${!(hf.fcR>0)?st(3):''} is more than half the hub width \u2014 check the measurement.<\/span>`;\n  else if(hf.hw<70 || hf.hw>200) o+=` <span class=\"warn\">${st(1)} looks unusual \u2014 hubs are usually 100\u2013157 mm wide (fat bikes up to 197 mm).<\/span>`;\n  else if(hf.fcL<8 || hf.fcR<8 || hf.fcL>60 || hf.fcR>60) o+=` <span class=\"warn\">Unusual result \u2014 check ${st(2)} and ${st(3)}.<\/span>`;\n  return o;\n}\nfunction advSummary(host){\n  const lay=(pressed(host,'[data-choice=\"spLayout\"]')||'paired')==='even'?'evenly spaced':'paired';\n  const oL=num(host,'offL')||0, oR=num(host,'offR')||0, sL=num(host,'grpSpreadL')||0, sR=num(host,'grpSpreadR')||0, g=num(host,'grpAngle')||0;\n  return `${lay} anchors${(oL||oR)?` \u00b7 offset ${oL}\/${oR} mm`:''}${(sL||sR)?` \u00b7 spread ${sL}\/${sR} mm`:''}${g?` \u00b7 group ${g}\u00b0`:''}`;\n}\nfunction openAdvIfSet(host){\n  const adv=host.querySelector('[data-adv]'); if(!adv) return;\n  if((num(host,'offL')||0)||(num(host,'offR')||0)||(num(host,'grpSpreadL')||0)||(num(host,'grpSpreadR')||0)||(num(host,'grpAngle')||0)||pressed(host,'[data-choice=\"spLayout\"]')==='even') adv.open=true;\n}\n\/* shared pieces for the measuring drawings *\/\nfunction measureKit(){\n  const NAVY=GC.NDS, DSC=GC.DS, INK='#5d6770', LINE='#96ABC2';\n  const ok=x=>typeof x==='number' && isFinite(x);\n  const fx=x=>(+x).toFixed(1);\n  const vtxt=x=>String(Math.round(x*10)\/10);\n  const lbl=(x,y,n,val,hl,goto,tipTxt)=>{\n    const has=ok(val), vt=has?vtxt(val):'?', w=22+String(vt).length*6.3, gx=x-w\/2;\n    return `<g data-goto=\"${goto}\" data-tip=\"${tipTxt} \u00b7 click to edit\" style=\"cursor:pointer\">`+\n      `<rect x=\"${fx(gx-3)}\" y=\"${y-10}\" width=\"${fx(w+6)}\" height=\"20\" rx=\"10\" fill=\"#fff\" fill-opacity=\".94\"\/>`+\n      `<circle class=\"hl-f\" data-hl=\"${hl}\" cx=\"${fx(gx+9)}\" cy=\"${y}\" r=\"8.5\" fill=\"${NAVY}\"\/>`+\n      `<text x=\"${fx(gx+9)}\" y=\"${fx(y+3.7)}\" font-family=\"Inter,sans-serif\" font-size=\"10.5\" font-weight=\"700\" fill=\"#fff\" text-anchor=\"middle\" pointer-events=\"none\">${n}<\/text>`+\n      `<text class=\"hl-t\" data-hl=\"${hl}\" x=\"${fx(gx+21)}\" y=\"${fx(y+3.8)}\" font-family=\"Inter,sans-serif\" font-size=\"11\" font-weight=\"700\" fill=\"${has?NAVY:LINE}\">${vt}<\/text><\/g>`;\n  };\n  const tag=(x,y,n,hl,tipTxt)=>`<g data-goto=\"${hl}\" data-tip=\"${tipTxt} \u00b7 click to edit\" style=\"cursor:pointer\"><circle class=\"hl-f\" data-hl=\"${hl}\" cx=\"${fx(x)}\" cy=\"${fx(y)}\" r=\"7.5\" fill=\"${NAVY}\" stroke=\"#fff\" stroke-width=\"1.5\"\/><text x=\"${fx(x)}\" y=\"${fx(y+3.4)}\" font-family=\"Inter,sans-serif\" font-size=\"9.5\" font-weight=\"700\" fill=\"#fff\" text-anchor=\"middle\" pointer-events=\"none\">${n}<\/text><\/g>`;\n  const head=(x,y,dir,hl)=>`<path class=\"hl-f\" data-hl=\"${hl}\" d=\"M${fx(x)} ${y} L${fx(x-dir*6.5)} ${fx(y-2.8)} L${fx(x-dir*6.5)} ${fx(y+2.8)} Z\" fill=\"${INK}\"\/>`;\n  const dim=(x1,x2,y,hl)=>{\n    let o=`<line class=\"hl-d\" data-hl=\"${hl}\" x1=\"${fx(x1)}\" y1=\"${y}\" x2=\"${fx(x2)}\" y2=\"${y}\" stroke=\"${INK}\" stroke-width=\"1.1\"\/>`;\n    if(x2-x1>=16) return o+head(x1,y,-1,hl)+head(x2,y,1,hl);\n    return o+`<line class=\"hl-d\" data-hl=\"${hl}\" x1=\"${fx(x1-12)}\" y1=\"${y}\" x2=\"${fx(x2+12)}\" y2=\"${y}\" stroke=\"${INK}\" stroke-width=\"1.1\"\/>`+head(x1,y,1,hl)+head(x2,y,-1,hl);\n  };\n  const ext=(x,y1,y2,hl)=>`<line class=\"hl-d\" data-hl=\"${hl}\" x1=\"${fx(x)}\" y1=\"${fx(y1)}\" x2=\"${fx(x)}\" y2=\"${fx(y2)}\" stroke=\"${LINE}\" stroke-width=\".8\"\/>`;\n  const vhead=(x,y,dir,hl)=>`<path class=\"hl-f\" data-hl=\"${hl}\" d=\"M${fx(x)} ${fx(y)} L${fx(x-2.8)} ${fx(y-dir*6.5)} L${fx(x+2.8)} ${fx(y-dir*6.5)} Z\" fill=\"${INK}\"\/>`;\n  const vdim=(x,y1,y2,hl)=>{\n    const o=`<line class=\"hl-d\" data-hl=\"${hl}\" x1=\"${fx(x)}\" y1=\"${fx(y1)}\" x2=\"${fx(x)}\" y2=\"${fx(y2)}\" stroke=\"${INK}\" stroke-width=\"1.1\"\/>`;\n    if(y2-y1>=16) return o+vhead(x,y1,-1,hl)+vhead(x,y2,1,hl);\n    return o+`<line class=\"hl-d\" data-hl=\"${hl}\" x1=\"${fx(x)}\" y1=\"${fx(y1-14)}\" x2=\"${fx(x)}\" y2=\"${fx(y2+14)}\" stroke=\"${INK}\" stroke-width=\"1.1\"\/>`+vhead(x,y1,1,hl)+vhead(x,y2,-1,hl);\n  };\n  const hext=(x1,x2,y,hl)=>`<line class=\"hl-d\" data-hl=\"${hl}\" x1=\"${fx(x1)}\" y1=\"${fx(y)}\" x2=\"${fx(x2)}\" y2=\"${fx(y)}\" stroke=\"${LINE}\" stroke-width=\".8\"\/>`;\n  \/\/ code badge (A2\/A3) + value, styled like the field badge\n  const codeLbl=(x,y,code,val,hl,goto,tipTxt,anchor,col)=>{\n    const c=col||NAVY, hasV=val!=='' && val!=null, vt=hasV?(ok(val)?vtxt(val):'?'):'';\n    const cw=Math.max(24,String(code).length*6+10), w=cw+(hasV?4+String(vt).length*6.3:0), gx=anchor==='end'?x-w:(anchor==='start'?x:x-w\/2);\n    return `<g${goto?` data-goto=\"${goto}\"`:''} data-tip=\"${tipTxt}${goto?' \u00b7 click to edit':''}\" style=\"cursor:${goto?'pointer':'help'}\">`+\n      `<rect x=\"${fx(gx-3)}\" y=\"${fx(y-10)}\" width=\"${fx(w+6)}\" height=\"20\" rx=\"10\" fill=\"#fff\" fill-opacity=\".94\"\/>`+\n      `<rect class=\"hl-f\" data-hl=\"${hl||''}\" x=\"${fx(gx)}\" y=\"${fx(y-8.5)}\" width=\"${cw}\" height=\"17\" rx=\"8.5\" fill=\"${c}\"\/>`+\n      `<text x=\"${fx(gx+cw\/2)}\" y=\"${fx(y+3.4)}\" font-family=\"Inter,sans-serif\" font-size=\"9.5\" font-weight=\"700\" fill=\"#fff\" text-anchor=\"middle\" pointer-events=\"none\">${code}<\/text>`+\n      (hasV?`<text class=\"hl-t\" data-hl=\"${hl||''}\" x=\"${fx(gx+cw+4)}\" y=\"${fx(y+3.8)}\" font-family=\"Inter,sans-serif\" font-size=\"11\" font-weight=\"700\" fill=\"${c}\">${vt}<\/text>`:'')+`<\/g>`;\n  };\n  return {NAVY,DSC,INK,LINE,ok,fx,vtxt,lbl,tag,head,dim,ext,vdim,hext,codeLbl};\n}\n\/* ---------- \"How to measure\" illustrations for the info tooltips ----------\n   Small generated drawings (320 \u00d7 150) in the measuring-drawing style: parts in navy \/ DS orange,\n   the measuring tool in steel, and the distance you read in the highlight colour with its step badge. *\/\nconst HOWART=(function(){\n  const W=320, H=150, NAVY='#084490', HLC='#D4237A', INK='#5d6770', LINE='#96ABC2', SOFT='#EDF2F6';\n  const DSC=()=>(typeof GC!=='undefined'&&GC.DS)||'#D9530A';\n  const f=x=>(+x).toFixed(1);\n  const defs=`<defs><linearGradient id=\"hwS\" x1=\"0\" y1=\"0\" x2=\"0\" y2=\"1\"><stop offset=\"0\" stop-color=\"#e9eef3\"\/><stop offset=\".5\" stop-color=\"#c3ccd6\"\/><stop offset=\"1\" stop-color=\"#9aa6b3\"\/><\/linearGradient>`+\n    `<linearGradient id=\"hwSv\" x1=\"0\" y1=\"0\" x2=\"1\" y2=\"0\"><stop offset=\"0\" stop-color=\"#e9eef3\"\/><stop offset=\".5\" stop-color=\"#c3ccd6\"\/><stop offset=\"1\" stop-color=\"#9aa6b3\"\/><\/linearGradient>`+\n    `<linearGradient id=\"hwR\" x1=\"0\" y1=\"0\" x2=\"0\" y2=\"1\"><stop offset=\"0\" stop-color=\"#3e6c9c\"\/><stop offset=\".5\" stop-color=\"#084490\"\/><stop offset=\"1\" stop-color=\"#0f2742\"\/><\/linearGradient>`+\n    `<marker id=\"hwA\" viewBox=\"0 0 8 8\" refX=\"7.2\" refY=\"4\" markerWidth=\"7\" markerHeight=\"7\" orient=\"auto-start-reverse\"><path d=\"M0 0.6 L8 4 L0 7.4 Z\" fill=\"${HLC}\"\/><\/marker>`+\n    `<marker id=\"hwAg\" viewBox=\"0 0 8 8\" refX=\"7.2\" refY=\"4\" markerWidth=\"6\" markerHeight=\"6\" orient=\"auto-start-reverse\"><path d=\"M0 0.6 L8 4 L0 7.4 Z\" fill=\"${INK}\"\/><\/marker><\/defs>`;\n  const svg=(body,label)=>`<svg viewBox=\"0 0 ${W} ${H}\" xmlns=\"http:\/\/www.w3.org\/2000\/svg\" role=\"img\" aria-label=\"${label}\">${defs}${body}<\/svg>`;\n  const txt=(x,y,t,o={})=>`<text x=\"${f(x)}\" y=\"${f(y)}\" font-family=\"Inter,sans-serif\" font-size=\"${o.s||9}\" font-weight=\"${o.w||600}\" fill=\"${o.c||INK}\" text-anchor=\"${o.a||'middle'}\"${o.it?' font-style=\"italic\"':''}>${t}<\/text>`;\n  \/\/ step badge (number) or code pill (A2, BSD ...)\n  const badge=(x,y,n,c)=>{ const s=String(n), w=s.length>1&&isNaN(+s)?s.length*6+10:17;\n    return w>17 ? `<rect x=\"${f(x-w\/2)}\" y=\"${f(y-8.5)}\" width=\"${w}\" height=\"17\" rx=\"8.5\" fill=\"${c||NAVY}\" stroke=\"#fff\" stroke-width=\"1.5\"\/>${txt(x,y+3.4,s,{c:'#fff',w:700,s:9.5})}`\n                : `<circle cx=\"${f(x)}\" cy=\"${f(y)}\" r=\"8.5\" fill=\"${c||NAVY}\" stroke=\"#fff\" stroke-width=\"1.5\"\/>${txt(x,y+3.6,s,{c:'#fff',w:700,s:10})}`; };\n  \/\/ the distance you read: highlight colour, arrows both ends, badge in the middle\n  const mdim=(x1,y1,x2,y2,n,c,bo)=>{ const mx=(x1+x2)\/2, my=(y1+y2)\/2, b=bo||[0,0];\n    return `<line x1=\"${f(x1)}\" y1=\"${f(y1)}\" x2=\"${f(x2)}\" y2=\"${f(y2)}\" stroke=\"${HLC}\" stroke-width=\"1.6\" marker-start=\"url(#hwA)\" marker-end=\"url(#hwA)\"\/>`+(n!=null?badge(mx+b[0],my+b[1],n,c):''); };\n  const gdim=(x1,y1,x2,y2,t,to)=>{ const o=to||[0,-5];\n    return `<line x1=\"${f(x1)}\" y1=\"${f(y1)}\" x2=\"${f(x2)}\" y2=\"${f(y2)}\" stroke=\"${INK}\" stroke-width=\"1\" marker-start=\"url(#hwAg)\" marker-end=\"url(#hwAg)\"\/>`+(t?txt((x1+x2)\/2+o[0],(y1+y2)\/2+o[1],t,{s:8.5}):''); };\n  \/\/ thin part: two short arrows pointing in from outside\n  const inH=(x1,x2,y,n,c)=>`<line x1=\"${f(x1-16)}\" y1=\"${f(y)}\" x2=\"${f(x1)}\" y2=\"${f(y)}\" stroke=\"${HLC}\" stroke-width=\"1.6\" marker-end=\"url(#hwA)\"\/>`+\n    `<line x1=\"${f(x2+16)}\" y1=\"${f(y)}\" x2=\"${f(x2)}\" y2=\"${f(y)}\" stroke=\"${HLC}\" stroke-width=\"1.6\" marker-end=\"url(#hwA)\"\/>`+(n!=null?badge(x2+30,y,n,c||HLC):'');\n  const inV=(x,y1,y2,n,c,bx)=>`<line x1=\"${f(x)}\" y1=\"${f(y1-16)}\" x2=\"${f(x)}\" y2=\"${f(y1)}\" stroke=\"${HLC}\" stroke-width=\"1.6\" marker-end=\"url(#hwA)\"\/>`+\n    `<line x1=\"${f(x)}\" y1=\"${f(y2+16)}\" x2=\"${f(x)}\" y2=\"${f(y2)}\" stroke=\"${HLC}\" stroke-width=\"1.6\" marker-end=\"url(#hwA)\"\/>`+(n!=null?badge(bx!=null?bx:x-16,(y1+y2)\/2,n,c||HLC):'');\n  const ext=(x1,y1,x2,y2)=>`<line x1=\"${f(x1)}\" y1=\"${f(y1)}\" x2=\"${f(x2)}\" y2=\"${f(y2)}\" stroke=\"${LINE}\" stroke-width=\".8\" stroke-dasharray=\"2.5 2\"\/>`;\n  \/\/ vernier caliper, jaws hanging down; measuring faces at xa (fixed jaw) and xb (sliding jaw)\n  const caliper=(xa,xb,yb,yt,o={})=>{\n    const L=o.left!=null?o.left:xa-16, R=o.right!=null?o.right:xb+58;\n    let g=`<rect x=\"${f(L)}\" y=\"${f(yb-6)}\" width=\"${f(R-L)}\" height=\"12\" rx=\"1.5\" fill=\"url(#hwS)\" stroke=\"${INK}\" stroke-width=\".7\"\/>`;\n    for(let x=xa+3;x<R-6;x+=5) g+=`<line x1=\"${f(x)}\" y1=\"${f(yb+6)}\" x2=\"${f(x)}\" y2=\"${f(yb+((Math.round((x-xa-3)\/5))%2?3:1))}\" stroke=\"${INK}\" stroke-width=\".5\"\/>`;\n    g+=`<path d=\"M${f(L)} ${f(yb+6)} H${f(xa)} V${f(yt)} H${f(xa-3)} L${f(L)} ${f(yb+20)} Z\" fill=\"url(#hwSv)\" stroke=\"${INK}\" stroke-width=\".7\"\/>`;\n    g+=`<rect x=\"${f(xb-4)}\" y=\"${f(yb-9)}\" width=\"40\" height=\"18\" rx=\"2.5\" fill=\"#dfe6ee\" stroke=\"${INK}\" stroke-width=\".7\"\/>`+\n       `<rect x=\"${f(xb+5)}\" y=\"${f(yb-6)}\" width=\"26\" height=\"12\" rx=\"1.5\" fill=\"#e3efe6\" stroke=\"${INK}\" stroke-width=\".5\"\/>`+txt(xb+18,yb+2.8,o.read||'mm',{s:7,w:700,c:'#3c6b4c'});\n    g+=`<path d=\"M${f(xb)} ${f(yb+9)} H${f(xb+16)} V${f(yb+20)} L${f(xb+3)} ${f(yt)} H${f(xb)} Z\" fill=\"url(#hwSv)\" stroke=\"${INK}\" stroke-width=\".7\"\/>`;\n    return g; };\n  \/\/ caliper used with its depth rod: tail of the beam at xt resting on a face, rod reaching xr (dir = +1 rod to the right)\n  const depthCal=(xt,xr,y,dir)=>{ const d=dir||1, x0=xt-d*112;\n    const lo=Math.min(x0,xt), hi=Math.max(x0,xt), sx=d>0?x0+30:x0-70;\n    return `<rect x=\"${f(lo)}\" y=\"${f(y-6)}\" width=\"${f(hi-lo)}\" height=\"12\" rx=\"1.5\" fill=\"url(#hwS)\" stroke=\"${INK}\" stroke-width=\".7\"\/>`+\n      `<rect x=\"${f(sx)}\" y=\"${f(y-9)}\" width=\"40\" height=\"18\" rx=\"2.5\" fill=\"#dfe6ee\" stroke=\"${INK}\" stroke-width=\".7\"\/>`+\n      `<rect x=\"${f(sx+7)}\" y=\"${f(y-6)}\" width=\"26\" height=\"12\" rx=\"1.5\" fill=\"#e3efe6\" stroke=\"${INK}\" stroke-width=\".5\"\/>`+txt(sx+20,y+2.8,'mm',{s:7,w:700,c:'#3c6b4c'})+\n      `<line x1=\"${f(xt)}\" y1=\"${f(y)}\" x2=\"${f(xr)}\" y2=\"${f(y)}\" stroke=\"#7d8996\" stroke-width=\"2.2\" stroke-linecap=\"square\"\/>`; };\n  \/\/ hub in side view (axle horizontal); returns svg and key x positions\n  const hub=(o)=>{ const cy=o.cy, xL=o.xL, xR=o.xR, fL=o.fL, fR=o.fR, t=o.t||7, rF=o.rF||34, rB=o.rB||11, rC=o.rC||8, dsC=DSC();\n    let g=`<line x1=\"${f(xL-10)}\" y1=\"${cy}\" x2=\"${f(xR+10)}\" y2=\"${cy}\" stroke=\"${LINE}\" stroke-width=\".8\" stroke-dasharray=\"7 3 2 3\"\/>`;\n    g+=`<rect x=\"${f(xL)}\" y=\"${f(cy-rC)}\" width=\"${f(fL-xL)}\" height=\"${f(2*rC)}\" fill=\"url(#hwS)\" stroke=\"${INK}\" stroke-width=\".7\"\/>`;\n    if(o.freehub){ g+=`<rect x=\"${f(fR)}\" y=\"${f(cy-rC)}\" width=\"${f(xR-fR)}\" height=\"${f(2*rC)}\" fill=\"url(#hwS)\" stroke=\"${INK}\" stroke-width=\".7\"\/>`+\n        `<rect x=\"${f(fR+t\/2+3)}\" y=\"${f(cy-15)}\" width=\"${f(o.freehub)}\" height=\"30\" rx=\"2\" fill=\"#d5dce4\" stroke=\"${INK}\" stroke-width=\".7\"\/>`;\n      for(let x=fR+t\/2+7;x<fR+t\/2+o.freehub;x+=5) g+=`<line x1=\"${f(x)}\" y1=\"${f(cy-15)}\" x2=\"${f(x)}\" y2=\"${f(cy+15)}\" stroke=\"#aab4bf\" stroke-width=\".8\"\/>`; }\n    else g+=`<rect x=\"${f(fR)}\" y=\"${f(cy-rC)}\" width=\"${f(xR-fR)}\" height=\"${f(2*rC)}\" fill=\"url(#hwS)\" stroke=\"${INK}\" stroke-width=\".7\"\/>`;\n    g+=`<rect x=\"${f(fL)}\" y=\"${f(cy-rB)}\" width=\"${f(fR-fL)}\" height=\"${f(2*rB)}\" fill=\"#dfe6ee\" stroke=\"${INK}\" stroke-width=\".7\"\/>`;\n    g+=`<rect x=\"${f(fL-t\/2)}\" y=\"${f(cy-rF)}\" width=\"${t}\" height=\"${f(2*rF)}\" rx=\"2\" fill=\"${NAVY}\"\/>`+\n       `<rect x=\"${f(fR-t\/2)}\" y=\"${f(cy-rF)}\" width=\"${t}\" height=\"${f(2*rF)}\" rx=\"2\" fill=\"${dsC}\"\/>`;\n    for(const s of [-1,1]) for(const x of [fL,fR]) g+=`<circle cx=\"${f(x)}\" cy=\"${f(cy+s*(rF-5))}\" r=\"1.6\" fill=\"#fff\"\/>`;\n    g+=`<rect x=\"${f(xL-2)}\" y=\"${f(cy-rC-3)}\" width=\"4\" height=\"${f(2*rC+6)}\" rx=\"1\" fill=\"#8b97a4\"\/><rect x=\"${f(xR-2)}\" y=\"${f(cy-rC-3)}\" width=\"4\" height=\"${f(2*rC+6)}\" rx=\"1\" fill=\"#8b97a4\"\/>`;\n    return g; };\n  \/\/ flange seen face-on with n holes on a pitch circle\n  const flangeFace=(cx,cy,r,rp,n,col,rot)=>{ let g=`<circle cx=\"${cx}\" cy=\"${cy}\" r=\"${r}\" fill=\"${col}\"\/><circle cx=\"${cx}\" cy=\"${cy}\" r=\"${f(r*0.36)}\" fill=\"#dfe6ee\" stroke=\"#fff\" stroke-width=\"1\"\/><circle cx=\"${cx}\" cy=\"${cy}\" r=\"${f(r*0.16)}\" fill=\"#fff\" stroke=\"${INK}\" stroke-width=\".6\"\/>`+\n      `<circle cx=\"${cx}\" cy=\"${cy}\" r=\"${rp}\" fill=\"none\" stroke=\"#fff\" stroke-opacity=\".55\" stroke-width=\".8\" stroke-dasharray=\"3 2\"\/>`;\n    for(let i=0;i<n;i++){ const a=(rot||0)+i*2*Math.PI\/n; g+=`<circle cx=\"${f(cx+rp*Math.cos(a))}\" cy=\"${f(cy+rp*Math.sin(a))}\" r=\"3.6\" fill=\"#fff\" stroke=\"${INK}\" stroke-width=\".5\"\/>`; }\n    return g; };\n  \/\/ rim cross-section (U), hub side at the bottom; returns path pieces\n  const rimSection=(cx,top,bot,w,o={})=>{ const xl=cx-w\/2, xr=cx+w\/2, sw=o.sw||7, bed=o.bed||6, hk=6;\n    const path=`M${xl} ${top} H${xl+sw} V${bot-bed-10} Q${xl+sw} ${bot-bed} ${xl+sw+12} ${bot-bed} H${xr-sw-12} Q${xr-sw} ${bot-bed} ${xr-sw} ${bot-bed-10} V${top} H${xr} V${bot-12} Q${xr} ${bot} ${xr-16} ${bot} H${xl+16} Q${xl} ${bot} ${xl} ${bot-12} Z`;\n    let g=`<path d=\"${path}\" fill=\"url(#hwR)\" stroke=\"${NAVY}\" stroke-width=\".8\"\/>`+\n      `<path d=\"M${xl+sw} ${top} h${hk} v5 h-${hk} Z M${xr-sw} ${top} h-${hk} v5 h${hk} Z\" fill=\"url(#hwR)\"\/>`;\n    return {g, xl, xr, sw, bed, top, bot}; };\n  const art={};\n\n  art.hubWidth=()=>{ const cy=96;\n    let g=hub({cy,xL:46,xR:274,fL:102,fR:226,rF:30});\n    g+=caliper(44,276,30,cy-12,{left:26,right:316});\n    g+=ext(46,cy+10,46,cy+32)+ext(274,cy+10,274,cy+32);\n    g+=mdim(46,cy+28,274,cy+28,1,null);\n    g+=txt(46,cy+44,'end face',{s:8.5})+txt(274,cy+44,'end face',{s:8.5});\n    return svg(g,'Step 1: caliper over the two end faces'); };\n  const endArt=(ds)=>{ const cy=78, dsC=DSC();\n    let g='';\n    if(!ds){ \/\/ NDS: end face left, caliper body further left, depth rod along the axle to the flange\n      const xE=132, xF=236, t=8;\n      g+=hub({cy,xL:xE,xR:460,fL:xF,fR:420,t,rF:32});\n      g+=depthCal(xE,xF-t\/2,cy-14,1);\n      g+=txt(68,cy-26,'depth rod',{s:8.5,it:1})+txt(xF+8,cy-38,'flange',{s:8.5,w:700,c:NAVY,a:'start'});\n      g+=ext(xE,cy+12,xE,cy+60)+ext(xF-t\/2,cy+34,xF-t\/2,cy+46)+ext(xF,cy+34,xF,cy+60);\n      g+=gdim(xE,cy+42,xF-t\/2,cy+42,'reading')+txt(xF+6,cy+45,'+ \u00bd flange',{s:8.5,a:'start'});\n      g+=mdim(xE,cy+58,xF,cy+58,2,null)+txt(xE-4,cy+61,'end face',{s:8.5,a:'end'});\n    } else { \/\/ DS: flange left, freehub, end face right, caliper body to the right\n      const xF=84, xE=196, t=8;\n      g+=hub({cy,xL:-200,xR:xE,fL:-150,fR:xF,t,rF:32,freehub:96});\n      g+=depthCal(xE,xF+t\/2,cy-24,-1);\n      g+=txt(140,cy+3,'cassette body',{s:8,it:1})+txt(xF-8,cy-38,'flange',{s:8.5,w:700,c:dsC,a:'end'});\n      g+=ext(xE,cy+18,xE,cy+60)+ext(xF+t\/2,cy+34,xF+t\/2,cy+46)+ext(xF,cy+34,xF,cy+60);\n      g+=gdim(xF+t\/2,cy+42,xE,cy+42,'reading')+txt(xF-6,cy+45,'+ \u00bd flange',{s:8.5,a:'end'});\n      g+=mdim(xF,cy+58,xE,cy+58,3,null)+txt(xE+4,cy+61,'end face',{s:8.5,a:'start'});\n    }\n    return svg(g, ds?'Step 3: depth rod from the DS end face to the flange, plus half the flange':'Step 2: depth rod from the NDS end face to the flange, plus half the flange'); };\n  art.endL=()=>endArt(false);\n  art.endR=()=>endArt(true);\n\n  const flArt=(ds)=>{ const cx=112, cy=82, r=44, rp=33, n=16, col=ds?DSC():NAVY;\n    let g=flangeFace(cx,cy,r,rp,n,col,Math.PI);\n    const xa=cx-rp-3.6, xb=cx+rp-3.6; \/\/ left edge of the left hole, left edge of the right hole\n    g+=caliper(xa,xb,18,cy-1,{left:xa-14,right:xb+70});\n    g+=ext(xa,cy+4,xa,cy+r+14)+ext(xb,cy+4,xb,cy+r+14);\n    g+=mdim(xa,cy+r+10,xb,cy+r+10,ds?5:4,ds?DSC():NAVY);\n    g+=txt(250,74,'left edge \u2192',{s:8.5})+txt(250,85,'left edge',{s:8.5})+txt(250,100,'= centre to centre',{s:8.5,w:700,c:HLC});\n    return svg(g,`Step ${ds?5:4}: caliper from the left edge of one hole to the left edge of the hole opposite`); };\n  art.flL=()=>flArt(false);\n  art.flR=()=>flArt(true);\n\n  art.hole=()=>{ const cy=78;\n    let g=`<rect x=\"96\" y=\"24\" width=\"30\" height=\"112\" rx=\"3\" fill=\"${NAVY}\"\/>`+\n      `<rect x=\"96\" y=\"${cy-7}\" width=\"30\" height=\"14\" fill=\"#fff\" stroke=\"${INK}\" stroke-width=\".5\"\/>`;\n    \/\/ drill bit through the hole: shank left, flutes right\n    g+=`<rect x=\"20\" y=\"${cy-6.2}\" width=\"96\" height=\"12.4\" rx=\"1\" fill=\"url(#hwS)\" stroke=\"${INK}\" stroke-width=\".7\"\/>`;\n    g+=`<path d=\"M116 ${cy-6.2} H212 L222 ${cy} L212 ${cy+6.2} H116 Z\" fill=\"url(#hwS)\" stroke=\"${INK}\" stroke-width=\".7\"\/>`;\n    for(let x=122;x<210;x+=9) g+=`<path d=\"M${x} ${cy+6.2} L${x+7} ${cy-6.2}\" stroke=\"${INK}\" stroke-width=\".7\"\/>`;\n    g+=inV(140,cy-7,cy+7,null)+badge(246,cy,6);\n    g+=txt(160,cy-22,'largest bit that fits',{s:8.5,a:'start'})+txt(160,cy+30,'= hole \u00d8',{s:8.5,w:700,c:HLC,a:'start'});\n    g+=txt(111,148,'flange',{s:8.5,c:NAVY});\n    return svg(g,'Step 6: the largest drill bit that fits the spoke hole'); };\n\n  art.rimInner=()=>{ const cx=118, cy=78, Ro=66, Ri=55;\n    let g=`<path fill-rule=\"evenodd\" d=\"M${cx-Ro} ${cy}a${Ro} ${Ro} 0 1 0 ${2*Ro} 0a${Ro} ${Ro} 0 1 0 ${-2*Ro} 0Z M${cx-Ri} ${cy}a${Ri} ${Ri} 0 1 0 ${2*Ri} 0a${Ri} ${Ri} 0 1 0 ${-2*Ri} 0Z\" fill=\"url(#hwR)\" stroke=\"${NAVY}\" stroke-width=\".8\"\/>`;\n    for(let i=0;i<24;i++){ const a=i*Math.PI\/12; g+=`<circle cx=\"${f(cx+(Ri+2.2)*Math.cos(a))}\" cy=\"${f(cy+(Ri+2.2)*Math.sin(a))}\" r=\"1.4\" fill=\"#EDF2F6\"\/>`; }\n    \/\/ inside jaws reaching the spoke bed at two opposite holes\n    const xa=cx-Ri, xb=cx+Ri, yb=cy+16;\n    g+=`<rect x=\"${f(xa+10)}\" y=\"${f(yb-5)}\" width=\"${f(xb-xa-20)}\" height=\"10\" rx=\"1.5\" fill=\"url(#hwS)\" stroke=\"${INK}\" stroke-width=\".7\"\/>`;\n    g+=`<path d=\"M${f(xa+10)} ${f(yb-5)} V${f(cy+3)} L${f(xa)} ${f(cy)} L${f(xa+10)} ${f(cy-3)} V${f(cy-1)}\" fill=\"url(#hwSv)\" stroke=\"${INK}\" stroke-width=\".7\"\/>`;\n    g+=`<path d=\"M${f(xb-10)} ${f(yb-5)} V${f(cy+3)} L${f(xb)} ${f(cy)} L${f(xb-10)} ${f(cy-3)} V${f(cy-1)}\" fill=\"url(#hwSv)\" stroke=\"${INK}\" stroke-width=\".7\"\/>`;\n    g+=`<rect x=\"${f(cx+8)}\" y=\"${f(yb-8)}\" width=\"36\" height=\"16\" rx=\"2.5\" fill=\"#dfe6ee\" stroke=\"${INK}\" stroke-width=\".7\"\/><rect x=\"${f(cx+13)}\" y=\"${f(yb-5)}\" width=\"26\" height=\"10\" rx=\"1.5\" fill=\"#e3efe6\" stroke=\"${INK}\" stroke-width=\".5\"\/>`+txt(cx+26,yb+2.5,'mm',{s:7,w:700,c:'#3c6b4c'});\n    g+=mdim(xa,cy-12,xb,cy-12,7);\n    g+=txt(254,64,'spoke bed,',{s:8.5})+txt(254,75,'hub side,',{s:8.5})+txt(254,86,'at two holes',{s:8.5})+txt(254,97,'opposite',{s:8.5});\n    return svg(g,'Step 7: inside measurement across the rim at two opposite spoke holes'); };\n\n  art.rimWall=()=>{ const cx=104, top=22, bot=96, bedT=8;\n    const r=rimSection(cx,top,bot,104,{bed:bedT});\n    let g=r.g;\n    \/\/ a bare nipple seated in a spoke hole from the tyre side; its shank sticks out on the hub side\n    const hs=bot-bedT, sh=34;\n    g+=`<rect x=\"${cx-3.5}\" y=\"${hs}\" width=\"7\" height=\"${bedT}\" fill=\"#fff\"\/>`;\n    g+=`<path d=\"M${cx-9} ${hs} V${hs-5} Q${cx-9} ${hs-9} ${cx-5} ${hs-9} H${cx+5} Q${cx+9} ${hs-9} ${cx+9} ${hs-5} V${hs} Z\" fill=\"#e3c77a\" stroke=\"#9c7f2a\" stroke-width=\".7\"\/>`;\n    g+=`<rect x=\"${cx-3.5}\" y=\"${hs}\" width=\"7\" height=\"${sh}\" fill=\"#e3c77a\" stroke=\"#9c7f2a\" stroke-width=\".7\"\/>`;\n    g+=ext(cx+4,hs,cx+52,hs)+ext(cx+4,bot,cx+34,bot)+ext(cx+4,hs+sh,cx+52,hs+sh);\n    g+=gdim(cx+48,hs,cx+48,hs+sh,'')+txt(cx+54,hs+sh\/2+3,'s',{s:9,w:700,a:'start'});\n    g+=gdim(cx+30,bot,cx+30,hs+sh,'')+txt(cx+24,bot+(hs+sh-bot)\/2+3,'p',{s:9,w:700,a:'end'});\n    g+=ext(r.xl-4,hs,r.xl-24,hs)+ext(r.xl-4,bot,r.xl-24,bot);\n    g+=inV(r.xl-20,hs,bot,8,NAVY,r.xl-36);\n    g+=badge(250,52,8)+txt(250,74,'s \u2212 p',{s:10,w:700,c:HLC})+txt(250,88,'s: nipple shank below the head',{s:8})+txt(250,99,'p: how far it sticks out',{s:8})+txt(250,110,'(eyelet included)',{s:8});\n    g+=txt(cx,top-8,'tyre side',{s:8,it:1});\n    return svg(g,'Step 8: wall thickness from a nipple seated in a spoke hole'); };\n  art.osb=()=>{ const cx=104, top=30, bot=104, w=124;\n    const r=rimSection(cx,top,bot,w,{bed:7});\n    let g=r.g; const hx=cx-12;\n    g+=`<rect x=\"${hx-3.5}\" y=\"${bot-7.5}\" width=\"7\" height=\"8\" fill=\"#fff\"\/>`+`<line x1=\"${hx}\" y1=\"${top-6}\" x2=\"${hx}\" y2=\"${bot+30}\" stroke=\"${HLC}\" stroke-width=\".8\" stroke-dasharray=\"3 2\"\/>`;\n    g+=`<line x1=\"${cx}\" y1=\"${top-6}\" x2=\"${cx}\" y2=\"${bot+8}\" stroke=\"${LINE}\" stroke-width=\".8\" stroke-dasharray=\"7 3 2 3\"\/>`;\n    g+=ext(r.xl,bot,r.xl,bot+32)+ext(r.xr,bot,r.xr,bot+32);\n    g+=gdim(r.xl,bot+18,hx,bot+18,'a',[0,-4])+gdim(hx,bot+28,r.xr,bot+28,'b',[0,-4]);\n    g+=badge(244,62,9)+txt(244,84,'(b \u2212 a) \u00f7 2',{s:10,w:700,c:HLC})+txt(244,98,'toward the smaller side',{s:8.5});\n    g+=txt(r.xl,top-10,'NDS',{s:8.5,w:700,c:NAVY,a:'start'})+txt(r.xr,top-10,'DS',{s:8.5,w:700,c:DSC(),a:'end'});\n    return svg(g,'Step 9: from each rim edge to the middle of the same spoke hole'); };\n\n  art.rimZig=()=>{ const y0=40, y1=112, xs=[46,98,150,202];\n    let g=`<rect x=\"24\" y=\"${y0}\" width=\"200\" height=\"${y1-y0}\" rx=\"4\" fill=\"#dfe6ee\" stroke=\"${NAVY}\" stroke-width=\".8\"\/>`;\n    const mid=(y0+y1)\/2;\n    g+=`<line x1=\"20\" y1=\"${mid}\" x2=\"228\" y2=\"${mid}\" stroke=\"${LINE}\" stroke-width=\".8\" stroke-dasharray=\"7 3 2 3\"\/>`;\n    xs.forEach((x,i)=>{ const y=mid+(i%2?9:-9); g+=`<circle cx=\"${x}\" cy=\"${y}\" r=\"5\" fill=\"#fff\" stroke=\"${INK}\" stroke-width=\".7\"\/>`; });\n    g+=txt(16,mid-12,'NDS',{s:8,w:700,c:NAVY,a:'end'})+txt(16,mid+16,'DS',{s:8,w:700,c:DSC(),a:'end'});\n    g+=ext(98,mid+9,140,mid+9)+ext(150,mid-9,140,mid-9);\n    g+=gdim(140,mid-9,140,mid+9,'',null)+txt(132,mid+3,'c',{s:9,w:700,a:'end'});\n    g+=badge(266,58,10)+txt(266,80,'c \u00f7 2',{s:10,w:700,c:HLC})+txt(266,94,'middle of a left hole',{s:8})+txt(266,104,'to a right hole,',{s:8})+txt(266,114,'sideways only',{s:8});\n    g+=txt(124,y1+20,'spoke bed seen from the hub',{s:8.5,it:1});\n    return svg(g,'Step 10: sideways distance between a left and a right hole, halved'); };\n\n  art.pairSpacing=()=>{ const y0=50, y1=100, mid=75;\n    let g=`<rect x=\"16\" y=\"${y0}\" width=\"288\" height=\"${y1-y0}\" rx=\"4\" fill=\"#dfe6ee\" stroke=\"${NAVY}\" stroke-width=\".8\"\/>`;\n    const holes=[40,62,138,160,236,258];\n    holes.forEach(x=>{ g+=`<circle cx=\"${x}\" cy=\"${mid}\" r=\"5\" fill=\"#fff\" stroke=\"${INK}\" stroke-width=\".7\"\/>`; });\n    g+=ext(133,mid,133,y0-16)+ext(155,mid,155,y0-16);\n    g+=mdim(133,y0-12,155,y0-12,null)+badge(144,y0-28,11);\n    g+=txt(144,y1+16,'edge \u2192 same edge',{s:8.5})+txt(144,y1+28,'of the two holes of one pair',{s:8.5});\n    g+=txt(40,y1+16,'NDS',{s:8,w:700,c:NAVY})+txt(62,y1+16,'DS',{s:8,w:700,c:DSC()});\n    return svg(g,'Step 11: hole spacing within one pair'); };\n\n  const spreadArt=(ds)=>{ const col=ds?DSC():NAVY, x0=118, w=40, top=24, bot=126, rows=[x0+10,x0+30];\n    let g=`<rect x=\"${x0}\" y=\"${top}\" width=\"${w}\" height=\"${bot-top}\" rx=\"4\" fill=\"${col}\"\/>`;\n    for(let k=0;k<5;k++){ const y=top+12+k*21; g+=`<circle cx=\"${rows[0]}\" cy=\"${y}\" r=\"4\" fill=\"#fff\"\/><circle cx=\"${rows[1]}\" cy=\"${y+10}\" r=\"4\" fill=\"#fff\"\/>`; }\n    g+=ext(rows[0]-4,top+12,rows[0]-4,top-6)+ext(rows[1]-4,top+22,rows[1]-4,top-6);\n    g+=mdim(rows[0],bot+12,rows[1],bot+12,null)+ext(rows[0],bot,rows[0],bot+16)+ext(rows[1],bot,rows[1],bot+16)+badge(rows[1]+30,bot+12,ds?'A3':'A2',col);\n    g+=txt(238,52,'two anchor rows',{s:8.5})+txt(238,70,'same edge of a hole',{s:8.5})+txt(238,81,'in each row',{s:8.5})+txt(238,96,'= centre to centre',{s:8.5,w:700,c:HLC});\n    g+=txt(60,74,ds?'DS flange':'NDS flange',{s:8.5,w:700,c:col})+txt(60,86,'from above the hub',{s:8,it:1});\n    return svg(g,`Anchor spread ${ds?'A3':'A2'}: distance between the two anchor rows`); };\n  art.grpSpreadL=()=>spreadArt(false);\n  art.grpSpreadR=()=>spreadArt(true);\n\n  const offArt=(ds)=>{ const col=ds?DSC():NAVY;\n    \/\/ anchor block with the head seated, spoke leaving to the right and down\n    let g=`<rect x=\"40\" y=\"40\" width=\"60\" height=\"70\" rx=\"6\" fill=\"${col}\"\/>`;\n    const a=-0.18, sx=74, sy=76, L=200, ex=sx+L*Math.cos(a), ey=sy-L*Math.sin(a);\n    g+=`<line x1=\"${sx}\" y1=\"${sy}\" x2=\"${f(ex)}\" y2=\"${f(ey)}\" stroke=\"#7d8996\" stroke-width=\"3.2\" stroke-linecap=\"round\"\/>`;\n    g+=`<rect x=\"${sx-12}\" y=\"${sy-6}\" width=\"12\" height=\"12\" rx=\"2\" fill=\"#5d6770\"\/>`;\n    \/\/ anchor circle (dashed arc) crossing the spoke at the flange edge\n    const ax=100, ay=sy+18*Math.sin(-a)*0+3.2;\n    g+=`<path d=\"M${ax-14} 20 Q${ax+6} ${sy} ${ax-14} 134\" fill=\"none\" stroke=\"${LINE}\" stroke-width=\"1\" stroke-dasharray=\"4 3\"\/>`;\n    g+=ext(sx,sy-6,sx,sy-50)+ext(ax-2,sy-2,ax-2,sy-50);\n    g+=mdim(sx,sy-46,ax-2,sy-46,null)+badge((sx+ax)\/2-1,sy-60,'offset',col);\n    g+=txt(210,44,'along the spoke:',{s:8.5})+txt(210,56,'head seat \u2192 where the spoke',{s:8.5})+txt(210,67,'crosses the anchor circle',{s:8.5})+txt(116,140,'anchor circle',{s:8,it:1,a:'start'});\n    return svg(g,'Spoke offset: from the head seat to the anchor circle, along the spoke'); };\n  art.offL=()=>offArt(false);\n  art.offR=()=>offArt(true);\n\n  art.nipAdd=()=>{ const x=96, seat=70;\n    \/\/ nipple in cutaway (head on top), spoke threaded in from below until the thread runs out\n    const g0=`<path d=\"M${x-20} ${seat} V${seat-14} Q${x-20} ${seat-24} ${x-10} ${seat-24} H${x+10} Q${x+20} ${seat-24} ${x+20} ${seat-14} V${seat} Z\" fill=\"#e3c77a\" stroke=\"#9c7f2a\" stroke-width=\".8\"\/>`+\n       `<rect x=\"${x-9}\" y=\"${seat}\" width=\"18\" height=\"62\" fill=\"#e3c77a\" stroke=\"#9c7f2a\" stroke-width=\".8\"\/>`+\n       `<rect x=\"${x-4}\" y=\"${seat-14}\" width=\"8\" height=\"76\" fill=\"#fff8e6\"\/>`;\n    let g=g0+`<rect x=\"${x-3.6}\" y=\"${seat-9}\" width=\"7.2\" height=\"${150-(seat-9)}\" fill=\"#9aa6b3\" stroke=\"${INK}\" stroke-width=\".5\"\/>`;\n    for(let y=seat-7;y<seat+30;y+=3) g+=`<line x1=\"${x-3.6}\" y1=\"${y}\" x2=\"${x+3.6}\" y2=\"${y+1.6}\" stroke=\"${INK}\" stroke-width=\".5\"\/>`;\n    g+=`<line x1=\"${x-34}\" y1=\"${seat}\" x2=\"${x+40}\" y2=\"${seat}\" stroke=\"${LINE}\" stroke-width=\".9\" stroke-dasharray=\"4 3\"\/>`+txt(x-38,seat+3,'seat face',{s:8,a:'end'});\n    g+=ext(x+4,seat-9,x+40,seat-9);\n    g+=inV(x+34,seat-9,seat,'end',HLC,x+58);\n    g+=txt(x+14,seat+46,'thread ends here',{s:8,it:1,a:'start'})+`<line x1=\"${x+12}\" y1=\"${seat+43}\" x2=\"${x+4}\" y2=\"${seat+30}\" stroke=\"${LINE}\" stroke-width=\".7\"\/>`;\n    g+=txt(236,48,'thread the nipple on',{s:8.5})+txt(236,59,'until the thread runs out,',{s:8.5})+txt(236,74,'seat face \u2192 spoke end',{s:8.5,w:700,c:HLC});\n    return svg(g,'Custom nipple: from the nipple seat face to the spoke end'); };\n  art.washer=()=>{ const xa=150, xb=158, top=64, bot=132;\n    \/\/ washer seen edge-on, the hole in the middle\n    let g=`<rect x=\"${xa}\" y=\"${top}\" width=\"${xb-xa}\" height=\"${bot-top}\" rx=\"1\" fill=\"#d5dce4\" stroke=\"${INK}\" stroke-width=\".7\"\/>`+\n      `<rect x=\"${xa}\" y=\"${(top+bot)\/2-8}\" width=\"${xb-xa}\" height=\"16\" fill=\"#fff\" stroke=\"${INK}\" stroke-width=\".5\"\/>`;\n    g+=caliper(xa,xb,26,top+18,{left:xa-30,right:xb+90});\n    g+=inH(xa,xb,bot+8,'t');\n    g+=txt(70,98,'washer,',{s:8.5})+txt(70,109,'seen edge-on',{s:8.5});\n    return svg(g,'Washer: caliper jaws over its thickness'); };\n  art.customDia=()=>{ const cx=150;\n    \/\/ spoke standing up: butts at the ends, thinner middle section between the jaws\n    let g=`<path d=\"M${cx-2.4} 2 V30 L${cx-1.6} 44 V112 L${cx-2.4} 126 V150 H${cx+2.4} V126 L${cx+1.6} 112 V44 L${cx+2.4} 30 V2 Z\" fill=\"#9aa6b3\" stroke=\"${INK}\" stroke-width=\".5\"\/>`;\n    g+=caliper(cx-1.6,cx+1.6,20,78,{left:cx-30,right:cx+100});\n    g+=inH(cx-1.6,cx+1.6,92,'\u00d8');\n    g+=txt(cx-36,40,'butt',{s:8,a:'end'})+txt(cx-8,140,'butt',{s:8,a:'end'})+txt(cx+30,112,'middle section',{s:8.5,w:700,c:HLC,a:'start'});\n    return svg(g,'Spoke \u00d8: on the middle section, away from the butts'); };\n  art.bsd=()=>{ let g=`<path d=\"M20 140 A170 170 0 0 1 300 140\" fill=\"none\" stroke=\"#3a3f45\" stroke-width=\"30\"\/>`+`<path d=\"M20 140 A170 170 0 0 1 300 140\" fill=\"none\" stroke=\"#565c63\" stroke-width=\"2\" stroke-dasharray=\"2 4\"\/>`;\n    g+=`<text font-family=\"Inter,sans-serif\" font-size=\"11\" font-weight=\"800\" fill=\"#d7dce1\" letter-spacing=\"1\"><textPath href=\"#hwBsdArc\" startOffset=\"50%\" text-anchor=\"middle\">ETRTO 25-<tspan fill=\"${HLC}\">622<\/tspan> \u00b7 700\u00d725C<\/textPath><\/text>`;\n    g=`<path id=\"hwBsdArc\" d=\"M32 144 A158 158 0 0 1 288 144\" fill=\"none\"\/>`+g;\n    g+=badge(160,96,'BSD',HLC)+txt(160,116,'second number = bead seat \u00d8',{s:8.5});\n    return svg(g,'BSD: the second number of the tyre size'); };\n\n  art.rimWidth=()=>{ const cx=120, top=52, bot=130;\n    const r=rimSection(cx,top,bot,96,{bed:7});\n    let g=r.g+caliper(r.xl,r.xr,22,top+8,{left:r.xl-18,right:r.xr+72});\n    g+=mdim(r.xl,bot+12,r.xr,bot+12,null)+ext(r.xl,bot,r.xl,bot+16)+ext(r.xr,bot,r.xr,bot+16)+badge(cx,bot+12,'W',HLC);\n    g+=txt(272,92,'outside,',{s:8.5})+txt(272,103,'widest point',{s:8.5});\n    return svg(g,'Rim width: caliper across the outside'); };\n\n  art.rimH=()=>{ const cx=96, top=34, bot=118;\n    const r=rimSection(cx,top,bot,80,{bed:7});\n    let g=r.g;\n    \/\/ caliper standing upright on the right, jaws reaching over the rim edge and under the spoke bed\n    g+=`<g transform=\"rotate(90)\">`+caliper(top,bot,-(r.xr+48),-(cx+4),{left:top-16,right:bot+26,read:' '})+`<\/g>`;\n    g+=ext(r.xl-4,top,r.xl-24,top)+ext(r.xl-4,bot,r.xl-24,bot);\n    g+=mdim(r.xl-20,top,r.xl-20,bot,null)+badge(r.xl-20,(top+bot)\/2,'H',HLC);\n    g+=txt(262,70,'rim edge \u2192',{s:8.5})+txt(262,81,'spoke bed,',{s:8.5})+txt(262,92,'hub side',{s:8.5});\n    return svg(g,'Rim height: from the rim edge to the spoke bed'); };\n  art.erd=()=>{ const cx=118, cy=76, Ro=64, Ri=54, re=50;\n    let g=`<path fill-rule=\"evenodd\" d=\"M${cx-Ro} ${cy}a${Ro} ${Ro} 0 1 0 ${2*Ro} 0a${Ro} ${Ro} 0 1 0 ${-2*Ro} 0Z M${cx-Ri} ${cy}a${Ri} ${Ri} 0 1 0 ${2*Ri} 0a${Ri} ${Ri} 0 1 0 ${-2*Ri} 0Z\" fill=\"url(#hwR)\" stroke=\"${NAVY}\" stroke-width=\".8\"\/>`;\n    g+=`<circle cx=\"${cx}\" cy=\"${cy}\" r=\"${re}\" fill=\"none\" stroke=\"${HLC}\" stroke-width=\".8\" stroke-dasharray=\"3 2\"\/>`;\n    for(const s of [-1,1]){ g+=`<line x1=\"${cx+s*re}\" y1=\"${cy}\" x2=\"${cx+s*12}\" y2=\"${cy+s*30}\" stroke=\"#7d8996\" stroke-width=\"1.6\"\/>`+`<rect x=\"${cx+s*(Ri-1)-(s>0?6:0)}\" y=\"${cy-2.5}\" width=\"6\" height=\"5\" fill=\"#e3c77a\"\/>`; }\n    g+=`<circle cx=\"${cx}\" cy=\"${cy}\" r=\"9\" fill=\"#dfe6ee\" stroke=\"${INK}\" stroke-width=\".6\"\/>`;\n    g+=mdim(cx-re,cy-18,cx+re,cy-18,'ERD',HLC);\n    g+=txt(256,60,'spoke ends,',{s:8.5})+txt(256,71,'opposite holes',{s:8.5})+txt(256,90,'spec sheet or',{s:8.5})+txt(256,101,'Measure the rim',{s:8.5,w:700,c:NAVY});\n    return svg(g,'ERD: the diameter on which the spoke ends sit'); };\n\n  const fcArt=(ds)=>{ const cy=80, xC=160, fL=96, fR=212, col=ds?DSC():NAVY;\n    let g=hub({cy,xL:40,xR:280,fL,fR,rF:32});\n    g+=`<line x1=\"${xC}\" y1=\"${cy-50}\" x2=\"${xC}\" y2=\"${cy+52}\" stroke=\"${INK}\" stroke-width=\".9\" stroke-dasharray=\"7 3 2 3\"\/>`+txt(xC,cy-54,'wheel centre',{s:8.5});\n    const x=ds?fR:fL;\n    g+=ext(x,cy+34,x,cy+52);\n    g+=mdim(Math.min(x,xC),cy+48,Math.max(x,xC),cy+48,ds?3:2,col);\n    g+=txt(160,cy+66,'from the hub spec sheet',{s:8.5,it:1});\n    return svg(g,`Flange to centre, ${ds?'DS':'NDS'}`); };\n  art.fcL=()=>fcArt(false);\n  art.fcR=()=>fcArt(true);\n\n  return {get:k=>art[k]?art[k]():'', has:k=>!!art[k], keys:Object.keys(art)};\n})();\n\nfunction hubMeasureSVG(p){\n  const {NAVY,DSC,INK,LINE,ok,fx,vtxt,lbl,tag,dim,ext,codeLbl}=measureKit(), K=p.key;\n  const sideName=L=>L?'NDS':'DS';\n  \/\/ ---- side view, drawn to the entered proportions\n  const W=360, H=p.sp?238:218, cy=110, x0=W\/2, yTop=16, yBot=p.sp?226:206;\n  const hwD=ok(p.hw)&&p.hw>=40&&p.hw<=220 ? p.hw : (p.rear?142:100);\n  const s=288\/hwD, xa=x0-144, xb=x0+144;\n  const dfl=p.rear?[35,19]:[34,34];\n  const fcD=(v,d)=>Math.max(5,Math.min(ok(v)&&v>0?v:d, hwD\/2-6));\n  const xL=x0-fcD(p.fcL,dfl[0])*s, xR=x0+fcD(p.fcR,dfl[1])*s;\n  const hOf=d=>Math.max(44,Math.min(66,22+(ok(d)&&d>0?d:44)*0.9));\n  const hL=hOf(p.dL), hR=hOf(p.dR);\n  const t=p.sp?Math.max(9,6.5*s):Math.max(5,3*s);\n  const capW=Math.max(9,7*s), rc=12, rcone=14, rfl=20, rb=14;\n  const spPx=v=>ok(v)&&v>0 ? Math.max(5,Math.min(t-3,v*s)) : Math.max(5,t\/2);\n  let g=`<svg class=\"hm-side\" viewBox=\"0 0 ${W} ${H}\" xmlns=\"http:\/\/www.w3.org\/2000\/svg\" role=\"img\" aria-label=\"Hub, side view\">`+\n    `<defs><linearGradient id=\"hmB${K}\" x1=\"0\" y1=\"0\" x2=\"0\" y2=\"1\"><stop offset=\"0\" stop-color=\"#1f4a75\"\/><stop offset=\".38\" stop-color=\"#4a76a3\"\/><stop offset=\"1\" stop-color=\"#132c47\"\/><\/linearGradient>`+\n    `<linearGradient id=\"hmL${K}\" x1=\"0\" y1=\"0\" x2=\"0\" y2=\"1\"><stop offset=\"0\" stop-color=\"#133457\"\/><stop offset=\".45\" stop-color=\"#3e6c9c\"\/><stop offset=\"1\" stop-color=\"#0f2742\"\/><\/linearGradient>`+\n    `<linearGradient id=\"hmR${K}\" x1=\"0\" y1=\"0\" x2=\"0\" y2=\"1\"><stop offset=\"0\" stop-color=\"#9C3804\"\/><stop offset=\".45\" stop-color=\"#F29A55\"\/><stop offset=\"1\" stop-color=\"#7E2E03\"\/><\/linearGradient>`+\n    `<linearGradient id=\"hmC${K}\" x1=\"0\" y1=\"0\" x2=\"0\" y2=\"1\"><stop offset=\"0\" stop-color=\"#e6ebf1\"\/><stop offset=\".45\" stop-color=\"#aebccb\"\/><stop offset=\"1\" stop-color=\"#6f8399\"\/><\/linearGradient><\/defs>`;\n  g+=`<line x1=\"${x0}\" y1=\"30\" x2=\"${x0}\" y2=\"${H-3}\" stroke=\"#C6D3DF\" stroke-width=\"1\" stroke-dasharray=\"7 3 1.5 3\"\/>`+\n    `<rect x=\"${x0-16}\" y=\"29\" width=\"32\" height=\"12\" fill=\"#F6F9FB\"\/><text x=\"${x0}\" y=\"38\" font-family=\"Inter,sans-serif\" font-size=\"8.5\" fill=\"#96ABC2\" text-anchor=\"middle\">centre<\/text>`;\n  const fil=Math.max(0,Math.min(7,(xR-xL-t)\/3));\n  g+=`<path d=\"M${fx(xa+capW)} ${cy-rcone} L${fx(xL-t\/2)} ${cy-rfl} L${fx(xL-t\/2)} ${cy+rfl} L${fx(xa+capW)} ${cy+rcone} Z\" fill=\"url(#hmB${K})\"\/>`+\n    `<path d=\"M${fx(xL+t\/2)} ${cy-rfl} L${fx(xL+t\/2+fil)} ${cy-rb} L${fx(xR-t\/2-fil)} ${cy-rb} L${fx(xR-t\/2)} ${cy-rfl} L${fx(xR-t\/2)} ${cy+rfl} L${fx(xR-t\/2-fil)} ${cy+rb} L${fx(xL+t\/2+fil)} ${cy+rb} L${fx(xL+t\/2)} ${cy+rfl} Z\" fill=\"url(#hmB${K})\"\/>`;\n  if(p.rear){\n    const c1=xR+t\/2+1, c2=xb-capW-1, rfh=21;\n    g+=`<rect x=\"${fx(c1)}\" y=\"${cy-rfh}\" width=\"${fx(Math.max(4,c2-c1))}\" height=\"${rfh*2}\" rx=\"2\" fill=\"url(#hmC${K})\" stroke=\"#8796a8\" stroke-width=\".6\"\/>`;\n    for(let x=c1+5;x<c2-2;x+=5) g+=`<line x1=\"${fx(x)}\" y1=\"${cy-rfh+2}\" x2=\"${fx(x)}\" y2=\"${cy+rfh-2}\" stroke=\"#8796a8\" stroke-width=\".6\"\/>`;\n  } else {\n    g+=`<path d=\"M${fx(xR+t\/2)} ${cy-rfl} L${fx(xb-capW)} ${cy-rcone} L${fx(xb-capW)} ${cy+rcone} L${fx(xR+t\/2)} ${cy+rfl} Z\" fill=\"url(#hmB${K})\"\/>`;\n  }\n  g+=`<rect class=\"hl-s\" data-hl=\"hubWidth endL\" x=\"${fx(xa)}\" y=\"${cy-rc}\" width=\"${fx(capW)}\" height=\"${rc*2}\" rx=\"2\" fill=\"url(#hmC${K})\" stroke=\"#6f8399\" stroke-width=\".8\"\/>`+\n    `<rect class=\"hl-s\" data-hl=\"hubWidth endR\" x=\"${fx(xb-capW)}\" y=\"${cy-rc}\" width=\"${fx(capW)}\" height=\"${rc*2}\" rx=\"2\" fill=\"url(#hmC${K})\" stroke=\"#6f8399\" stroke-width=\".8\"\/>`;\n  const flange=(x,h,L)=>{\n    const d=L?p.dL:p.dR, hl=L?'flL endL fcL hole':'flR endR fcR hole';\n    let o=`<rect class=\"hl-s\" data-hl=\"${hl}\" x=\"${fx(x-t\/2)}\" y=\"${fx(cy-h)}\" width=\"${fx(t)}\" height=\"${fx(2*h)}\" rx=\"${fx(Math.min(3,t\/2))}\" fill=\"url(#hm${L?'L':'R'}${K})\" stroke=\"${L?NAVY:DSC}\" stroke-width=\".8\" data-tip=\"${sideName(L)} flange${ok(d)?` \u2014 \u00d8 ${vtxt(d)} mm`:''}\" style=\"cursor:help\"\/>`;\n    if(p.sp){\n      const half=spPx(L?p.sprL:p.sprR)\/2;\n      for(const xx of [x-half, x+half]){\n        o+=`<line x1=\"${fx(xx)}\" y1=\"${fx(cy-h+4)}\" x2=\"${fx(xx)}\" y2=\"${fx(cy+h-4)}\" stroke=\"#EDF2F6\" stroke-opacity=\".35\" stroke-width=\".8\" stroke-dasharray=\"2 3\"\/>`;\n        for(const yy of [cy-h+7, cy+h-7]) o+=`<circle cx=\"${fx(xx)}\" cy=\"${fx(yy)}\" r=\"1.9\" fill=\"#EDF2F6\"\/>`;\n      }\n    }\n    else { for(const yy of [cy-h+6, cy+h-6]) o+=`<circle class=\"hl-f\" data-hl=\"hole\" cx=\"${fx(x)}\" cy=\"${fx(yy)}\" r=\"1.9\" fill=\"#EDF2F6\"\/>`; }\n    return o;\n  };\n  g+=flange(xL,hL,true)+flange(xR,hR,false);\n  if(p.sp){\n    for(const L of [true,false]){\n      const x=L?xL:xR, h=L?hL:hR, v=L?p.sprL:p.sprR, half=spPx(v)\/2, out=L?-1:1, yA=cy+h+11;\n      const hl=L?'grpSpreadL':'grpSpreadR', code=L?'A2':'A3', step=L?2:3;\n      g+=ext(x-half,cy+h+2,yA+4,hl)+ext(x+half,cy+h+2,yA+4,hl)+dim(x-half,x+half,yA,hl);\n      g+=`<circle cx=\"${fx(x)}\" cy=\"${yA}\" r=\"2.8\" fill=\"#E63329\" stroke=\"#fff\" stroke-width=\"1\" data-tip=\"Reference point: halfway between the two anchor rows. Step ${step} measures to here; ${code} is split evenly around it.\" style=\"cursor:help\"\/>`;\n      g+=codeLbl(x+out*(half+16), yA, code, ok(v)?v:0, hl, hl, `${code}: anchor spread ${sideName(L)}, between the two anchor rows`, L?'end':'start');\n    }\n  }\n  if(!p.spec){\n    g+=ext(xa,cy-rc-3,yTop-5,'hubWidth')+ext(xb,cy-rc-3,yTop-5,'hubWidth')+dim(xa,xb,yTop,'hubWidth')+\n      lbl(x0,yTop,1,p.hw,'hubWidth','hubWidth','Step 1: hub width, over the lock-nuts');\n    g+=ext(xa,cy+rc+3,yBot+5,'endL')+ext(xL,cy+hL+3,yBot+5,'endL')+dim(xa,xL,yBot,'endL')+\n      lbl((xa+xL)\/2,yBot-13,2,p.a,'endL','endL','Step 2: NDS end face to the middle of the NDS flange');\n    g+=ext(xb,cy+rc+3,yBot+5,'endR')+ext(xR,cy+hR+3,yBot+5,'endR')+dim(xR,xb,yBot,'endR')+\n      lbl((xR+xb)\/2,yBot-13,3,p.b,'endR','endR','Step 3: middle of the DS flange to the DS end face');\n  } else {\n    g+=ext(xL,cy+hL+3,yBot+5,'fcL')+ext(xR,cy+hR+3,yBot+5,'fcR')+dim(xL,x0,yBot,'fcL')+dim(x0,xR,yBot,'fcR')+\n      lbl((xL+x0)\/2,yBot-13,2,p.fcL,'fcL','fcL','Step 2: NDS flange to wheel centre, from the spec sheet')+\n      lbl((x0+xR)\/2,yBot-13,3,p.fcR,'fcR','fcR','Step 3: DS flange to wheel centre, from the spec sheet');\n  }\n  g+=tag(xL,cy-hL-11,4,'flL','Step 4: NDS flange \u00d8')+tag(xR,cy-hR-11,5,'flR','Step 5: DS flange \u00d8')+'<\/svg>';\n  \/\/ ---- flange front views\n  const flView=L=>{\n    const n=Math.max(4,Math.min(36,(L?p.nL:p.nR)||16)), d=L?p.dL:p.dR, hl=L?'flL':'flR', step=L?4:5, col=L?NAVY:DSC, id=`hmF${L?'L':'R'}${K}`;\n    const VW=124, VH=170, cx=62, cyy=96, R=46, rh=38;\n    let o=`<svg class=\"hm-fl ${L?'l':'r'}\" viewBox=\"0 0 ${VW} ${VH}\" xmlns=\"http:\/\/www.w3.org\/2000\/svg\" role=\"img\" aria-label=\"${sideName(L)} flange, front view\">`+\n      `<defs><radialGradient id=\"${id}\" cx=\".42\" cy=\".38\" r=\".75\"><stop offset=\"0\" stop-color=\"${L?'#3e6c9c':'#F29A55'}\"\/><stop offset=\".7\" stop-color=\"${col}\"\/><stop offset=\"1\" stop-color=\"${L?'#0f2742':'#7E2E03'}\"\/><\/radialGradient><\/defs>`;\n    if(p.sp){\n      o+=`<circle cx=\"${cx}\" cy=\"${cyy}\" r=\"${rh}\" fill=\"none\" stroke=\"${LINE}\" stroke-width=\".9\" stroke-dasharray=\"3 3\"\/>`+\n        `<circle class=\"hl-s\" data-hl=\"${hl}\" cx=\"${cx}\" cy=\"${cyy}\" r=\"30\" fill=\"url(#${id})\" stroke=\"${col}\" stroke-width=\".8\"\/>`;\n      const pairs=p.paired && n%2===0, E=pairs?n\/2:n;\n      for(let i=0;i<E;i++){\n        const a=i*2*Math.PI\/E, ca=Math.cos(a), sa=Math.sin(a), px=cx+rh*ca, py=cyy+rh*sa;\n        for(const dd of (pairs?[1,-1]:[i%2?1:-1])) o+=`<line x1=\"${fx(px)}\" y1=\"${fx(py)}\" x2=\"${fx(px-dd*sa*17)}\" y2=\"${fx(py+dd*ca*17)}\" stroke=\"#8a9bb0\" stroke-width=\"1.6\" stroke-linecap=\"round\"\/>`;\n        o+=`<circle cx=\"${fx(px)}\" cy=\"${fx(py)}\" r=\"5\" fill=\"url(#${id})\" stroke=\"${col}\" stroke-width=\".7\"\/>`;\n      }\n    } else {\n      o+=`<circle class=\"hl-s\" data-hl=\"${hl}\" cx=\"${cx}\" cy=\"${cyy}\" r=\"${R}\" fill=\"url(#${id})\" stroke=\"${col}\" stroke-width=\".8\"\/>`+\n        `<circle cx=\"${cx}\" cy=\"${cyy}\" r=\"${rh}\" fill=\"none\" stroke=\"#fff\" stroke-opacity=\".35\" stroke-width=\".8\" stroke-dasharray=\"2.5 2.5\"\/>`;\n      for(let i=0;i<n;i++){ const a=i*2*Math.PI\/n; o+=`<circle class=\"hl-f\" data-hl=\"hole\" cx=\"${fx(cx+rh*Math.cos(a))}\" cy=\"${fx(cyy+rh*Math.sin(a))}\" r=\"2.4\" fill=\"#EDF2F6\"\/>`; }\n    }\n    if(!L && p.rear){\n      o+=`<circle cx=\"${cx}\" cy=\"${cyy}\" r=\"19\" fill=\"#c3ccd6\" stroke=\"#8796a8\" stroke-width=\".8\"\/>`;\n      for(let i=0;i<12;i++){ const a=i*Math.PI\/6; o+=`<line x1=\"${fx(cx+15*Math.cos(a))}\" y1=\"${fx(cyy+15*Math.sin(a))}\" x2=\"${fx(cx+19*Math.cos(a))}\" y2=\"${fx(cyy+19*Math.sin(a))}\" stroke=\"#8796a8\" stroke-width=\"1.4\"\/>`; }\n    } else o+=`<circle cx=\"${cx}\" cy=\"${cyy}\" r=\"15\" fill=\"#c9d2dc\" stroke=\"#8796a8\" stroke-width=\".8\"\/>`;\n    o+=`<circle cx=\"${cx}\" cy=\"${cyy}\" r=\"6.5\" fill=\"#fff\" stroke=\"#8796a8\" stroke-width=\".8\"\/>`;\n    o+=ext(cx-rh,cyy-3,14,hl)+ext(cx+rh,cyy-3,14,hl)+dim(cx-rh,cx+rh,20,hl)+\n      lbl(cx,20,step,d,hl,hl,`Step ${step}: ${sideName(L)} flange \u00d8, ${p.sp?'across the spoke anchor points':'hole centre to hole centre'}`);\n    if(L && !p.sp){\n      const a=Math.PI\/4, hx=cx+rh*Math.cos(a), hy=cyy+rh*Math.sin(a);\n      o+=`<line class=\"hl-d\" data-hl=\"hole\" x1=\"${fx(hx+2)}\" y1=\"${fx(hy+2)}\" x2=\"103\" y2=\"148\" stroke=\"${INK}\" stroke-width=\".8\"\/>`+tag(108,154,6,'hole','Step 6: spoke hole \u00d8');\n    }\n    return o+`<text x=\"6\" y=\"${VH-6}\" font-family=\"Inter,sans-serif\" font-size=\"9.5\" font-weight=\"600\" fill=\"${col}\">${sideName(L)} flange<\/text><\/svg>`;\n  };\n  const st=n=>`<span class=\"step\">${n}<\/span>`;\n  const cap = p.spec\n    ? `${st(2)}${st(3)} flange to wheel centre and ${st(4)}${st(5)} ${p.sp?'anchor circle':'pitch circle'} \u00d8, as printed on the hub spec sheet.`\n    : `${st(1)} over the lock-nuts \u00b7 ${st(2)}${st(3)} end face to the middle of the flange${p.sp?' (the spoke anchors)':''} \u00b7 ${st(4)}${st(5)} ${p.sp?'across the spoke anchor points':'hole centre to hole centre: from the left edge of one hole to the left edge of the hole opposite'}${p.sp?'':` \u00b7 ${st(6)} spoke hole`}.`;\n  const capSp = p.sp ? ` \u00b7 <span class=\"dimb\">A2<\/span> <span class=\"dimb\">A3<\/span> distance between the two anchor rows (Advanced), split evenly around the red reference point` : '';\n  return flView(true)+g+flView(false)+`<p class=\"hubm-cap\">${cap.replace(\/\\.$\/,'')}${capSp}.<\/p>`;\n}\nfunction drawHubMeasure(host){\n  const box=host.querySelector('[data-hubart]'); if(!box) return;\n  const hf=hubFC(host);\n  const count=parseInt(pressed(host,'[data-choice=\"count\"]'),10)||32, ratio=pressed(host,'[data-choice=\"ratio\"]')||'1:1';\n  const [nL,nR]=sidesFor(count,ratio);\n  const v=f=>{ const x=num(host,f); return isNaN(x)?null:x; };\n  box.innerHTML=hubMeasureSVG({key:host.id==='panel-rear'?'r':'f', rear:host.id==='panel-rear',\n    sp:pressed(host,'[data-seg=\"hubType\"]')==='straight', spec:hf.mode==='spec',\n    hw:hf.hw, a:hf.a, b:hf.b, fcL:hf.fcL, fcR:hf.fcR, dL:v('flL'), dR:v('flR'), nL, nR,\n    paired:(pressed(host,'[data-choice=\"spLayout\"]')||'paired')!=='even', sprL:num(host,'grpSpreadL')||0, sprR:num(host,'grpSpreadR')||0});\n  const out=host.querySelector('[data-fcout]'); if(out) out.innerHTML=hubFcOut(hf);\n  const as=host.querySelector('[data-advsum]'); if(as) as.textContent=advSummary(host);\n  host.querySelectorAll('[data-hw]').forEach(b=>b.setAttribute('aria-pressed', String(hf.hw>0 && +b.dataset.hw===hf.hw)));\n}\n\/* ---------- rim measuring: ERD = internal rim \u00d8 + 2 \u00d7 inner wall thickness ---------- *\/\nfunction rimMode(host){ return pressed(host,'[data-seg=\"rimMode\"]')==='measure'?'measure':'spec'; }\nfunction applyRimMode(host,v){\n  host.querySelectorAll('[data-seg=\"rimMode\"] button').forEach(x=>x.setAttribute('aria-pressed', x.dataset.v===v?'true':'false'));\n  const card=host.querySelector('[data-card=\"rim\"]'); if(card) card.classList.toggle('src-measure', v==='measure');\n}\nfunction rimERD(host){\n  \/\/ nsd = nipple seat diameter; erd = where the spoke ends sit (what the lengths use)\n  const nip=nippleSel(host), tot=nip.total, r3=x=>Math.round(x*1000)\/1000;\n  if(rimMode(host)==='spec'){\n    const v=num(host,'erd'), ref=erdRef(host);\n    const erd = ref==='mine' ? v : (ref==='seat' ? v+2*tot : v+2*(tot-NIP_STD));\n    const nsd = ref==='seat' ? v : (ref==='mine' ? v-2*tot : v-2*NIP_STD);\n    return {mode:'spec', ref, input:v, nsd, erd:(v>0 && isFinite(erd)) ? r3(erd) : NaN, nip};\n  }\n  const di=num(host,'rimInner'), t=num(host,'rimWall'), nsd=(di>0 && t>0) ? di+2*t : NaN;\n  return {mode:'measure', di, t, nsd, erd:(nsd>0 && isFinite(tot)) ? r3(nsd+2*tot) : NaN, nip};\n}\nfunction rimErdOut(re,bsd){\n  const st=n=>`<span class=\"step\">${n}<\/span>`, mm=x=>(Math.round(x*10)\/10).toFixed(1)+' mm';\n  if(!(re.di>0) || !(re.t>0)) return `<span>Fill in ${st(7)} and ${st(8)}: nipple seat \u00d8 = ${st(7)} + 2 \u00d7 ${st(8)}. The nipple below adds the last part, up to the spoke end (the ERD).<\/span>`;\n  let o=`<span class=\"fco-l\">Nipple seat \u00d8<\/span> ${st(7)} + 2 \u00d7 ${st(8)} = <b>${mm(re.nsd)}<\/b>`;\n  if(re.erd>0) o+=` <span class=\"fco-l\">ERD with the nipple<\/span> <b>${mm(re.erd)}<\/b>`;\n  if(re.t>8) o+=` <span class=\"warn\">${st(8)} looks thick \u2014 spoke beds are usually 1.5\u20134 mm. Measure the wall, not the rim height.<\/span>`;\n  else if(bsd && re.nsd>=bsd) o+=` <span class=\"warn\">The nipple seat \u00d8 can't be larger than the wheel size (BSD ${bsd}) \u2014 check ${st(7)} and the wheel size.<\/span>`;\n  return o;\n}\nfunction rimMeasureSVG(p){\n  const {NAVY,INK,LINE,fx,lbl,tag,vdim,hext,dim}=measureKit(), K=p.key;\n  const W=380, H=186, cx=92, cy=96, Ro=78, Ri=60, Re=66;\n  let g=`<svg class=\"hm-side\" viewBox=\"0 0 ${W} ${H}\" xmlns=\"http:\/\/www.w3.org\/2000\/svg\" role=\"img\" aria-label=\"Rim measuring diagram\">`+\n    `<defs><linearGradient id=\"rmG${K}\" x1=\"0\" y1=\"0\" x2=\"0\" y2=\"1\"><stop offset=\"0\" stop-color=\"#3e6c9c\"\/><stop offset=\".5\" stop-color=\"#084490\"\/><stop offset=\"1\" stop-color=\"#0f2742\"\/><\/linearGradient>`+\n    `<linearGradient id=\"rmN${K}\" x1=\"0\" y1=\"0\" x2=\"1\" y2=\"0\"><stop offset=\"0\" stop-color=\"#8796a8\"\/><stop offset=\".45\" stop-color=\"#e6ebf1\"\/><stop offset=\"1\" stop-color=\"#6f8399\"\/><\/linearGradient><\/defs>`;\n  \/\/ front view: step 7 runs across the inside of the rim\n  g+=`<path class=\"hl-s\" data-hl=\"rimInner erd\" fill-rule=\"evenodd\" d=\"M${cx-Ro} ${cy}a${Ro} ${Ro} 0 1 0 ${2*Ro} 0a${Ro} ${Ro} 0 1 0 ${-2*Ro} 0Z M${cx-Ri} ${cy}a${Ri} ${Ri} 0 1 0 ${2*Ri} 0a${Ri} ${Ri} 0 1 0 ${-2*Ri} 0Z\" fill=\"url(#rmG${K})\" stroke=\"${NAVY}\" stroke-width=\".8\"\/>`;\n  for(let i=0;i<24;i++){ const a=i*Math.PI\/12; g+=`<circle cx=\"${fx(cx+(Ri+2.5)*Math.cos(a))}\" cy=\"${fx(cy+(Ri+2.5)*Math.sin(a))}\" r=\"1.5\" fill=\"#EDF2F6\"\/>`; }\n  g+=`<circle class=\"hl-d\" data-hl=\"erd rimWall\" cx=\"${cx}\" cy=\"${cy}\" r=\"${Re}\" fill=\"none\" stroke=\"#fff\" stroke-opacity=\".6\" stroke-width=\".9\" stroke-dasharray=\"3 3\"\/>`+\n    `<text x=\"${cx}\" y=\"${cy-Re-4}\" font-family=\"Inter,sans-serif\" font-size=\"8.5\" font-weight=\"700\" fill=\"#fff\" fill-opacity=\".9\" text-anchor=\"middle\">nipple seat \u00d8<\/text>`;\n  g+=dim(cx-Ri,cx+Ri,cy,'rimInner')+lbl(cx,cy,7,p.di,'rimInner','rimInner','Step 7: internal rim \u00d8, across the inside at the spoke holes');\n  \/\/ section: step 8 is the inner wall (spoke bed) the nipple head sits on\n  const px=268, top=26, sw=10, xl=px-62, xr=px+62, yb=150, ySeat=136;\n  g+=`<path class=\"hl-s\" data-hl=\"rimWall rimInner\" d=\"M${xl} ${top} H${xl+sw} V${ySeat-18} Q${xl+sw} ${ySeat} ${xl+sw+24} ${ySeat} H${xr-sw-24} Q${xr-sw} ${ySeat} ${xr-sw} ${ySeat-18} V${top} H${xr} V${yb-18} Q${xr} ${yb} ${xr-30} ${yb} H${xl+30} Q${xl} ${yb} ${xl} ${yb-18} Z\" fill=\"url(#rmG${K})\" stroke=\"${NAVY}\" stroke-width=\".8\"\/>`+\n    `<path d=\"M${xl+sw} ${top} h7 v7 h-7 Z M${xr-sw} ${top} h-7 v7 h7 Z\" fill=\"url(#rmG${K})\"\/>`+\n    `<path d=\"M${xl+sw} ${top+36} Q${px} ${top+46} ${xr-sw} ${top+36} v6 Q${px} ${top+52} ${xl+sw} ${top+42} Z\" fill=\"#2f5b86\"\/>`;\n  g+=`<rect x=\"${px-4.5}\" y=\"${ySeat}\" width=\"9\" height=\"${yb-ySeat+20}\" rx=\"1.5\" fill=\"url(#rmN${K})\" stroke=\"#6f8399\" stroke-width=\".7\"\/>`+\n    `<rect x=\"${px-8}\" y=\"${ySeat-9}\" width=\"16\" height=\"9\" rx=\"3\" fill=\"url(#rmN${K})\" stroke=\"#6f8399\" stroke-width=\".7\"\/>`+\n    `<line x1=\"${px}\" y1=\"${yb+20}\" x2=\"${px}\" y2=\"${H}\" stroke=\"#8a9bb0\" stroke-width=\"2.2\"\/>`;\n  g+=hext(xl-26,px-9,ySeat,'erd rimWall')+`<text x=\"${xl-6}\" y=\"${ySeat-4}\" font-family=\"Inter,sans-serif\" font-size=\"8.5\" font-weight=\"700\" fill=\"${INK}\" text-anchor=\"end\">seat<\/text>`;\n  g+=hext(xl-6,px-5,yb,'rimInner')+tag(xl-14,yb,7,'rimInner','Step 7 is measured at this surface');\n  g+=hext(px+9,xr+26,ySeat,'rimWall')+hext(px+5,xr+26,yb,'rimWall')+vdim(xr+20,ySeat,yb,'rimWall')+\n    lbl(xr+20,ySeat-26,8,p.t,'rimWall','rimWall','Step 8: inner wall thickness at a spoke hole');\n  return g+'<\/svg>';\n}\nfunction drawRimMeasure(host){\n  const re=rimERD(host), box=host.querySelector('[data-rimart]');\n  if(box && re.mode==='measure'){\n    const st=n=>`<span class=\"step\">${n}<\/span>`;\n    box.innerHTML=rimMeasureSVG({key:host.id==='panel-rear'?'r':'f', di:re.di, t:re.t})+\n      `<p class=\"hubm-cap\">${st(7)} across the inside of the rim, at the spoke holes (this is not the ERD) \u00b7 ${st(8)} wall thickness at a spoke hole, eyelet included \u00b7 nipple seat \u00d8 = ${st(7)} + 2 \u00d7 ${st(8)}; the nipple below adds the last part, up to the spoke end (the ERD).<\/p>`;\n  }\n  const out=host.querySelector('[data-erdout]'); if(out) out.innerHTML=rimErdOut(re, parseInt(val(host,'bsd'),10)||622);\n}\n\/* ---------- offset spoke bed (step 9) and rim hole offset (step 10) ---------- *\/\nfunction osbSide(host){ return pressed(host,'[data-seg=\"osbSide\"]')==='DS'?'DS':'NDS'; }\nfunction setOsbSide(host,v){ host.querySelectorAll('[data-seg=\"osbSide\"] button').forEach(b=>b.setAttribute('aria-pressed', b.dataset.v===v?'true':'false')); }\n\/\/ signed for the maths: + = bed toward the NDS (left), \u2212 = toward the DS\nfunction osbSigned(host){ const v=num(host,'osb')||0; return osbSide(host)==='DS' ? -v : v; }\nfunction osbText(v){ return `${Math.round(Math.abs(v)*100)\/100} mm toward ${v<0?'DS':'NDS'}`; }\nfunction osbSVG(p){\n  const {NAVY,DSC,INK,LINE,fx,lbl,dim,ext}=measureKit(), K=p.key;\n  const W=380, H=214;\n  const ring=(x,y,t)=>`<circle cx=\"${fx(x)}\" cy=\"${fx(y)}\" r=\"7\" fill=\"#EDF2F6\" stroke=\"${LINE}\" stroke-width=\".8\"\/><text x=\"${fx(x)}\" y=\"${fx(y+3.2)}\" font-family=\"Inter,sans-serif\" font-size=\"9\" font-style=\"italic\" font-weight=\"700\" fill=\"${INK}\" text-anchor=\"middle\">${t}<\/text>`;\n  const side=(x,y,t,c,a)=>`<text x=\"${x}\" y=\"${y}\" font-family=\"Inter,sans-serif\" font-size=\"9\" font-weight=\"700\" fill=\"${c}\" text-anchor=\"${a}\">${t}<\/text>`;\n  let g=`<svg class=\"hm-side\" viewBox=\"0 0 ${W} ${H}\" xmlns=\"http:\/\/www.w3.org\/2000\/svg\" role=\"img\" aria-label=\"Offset spoke bed and rim hole offset\">`+\n    `<defs><linearGradient id=\"obG${K}\" x1=\"0\" y1=\"0\" x2=\"0\" y2=\"1\"><stop offset=\"0\" stop-color=\"#3e6c9c\"\/><stop offset=\".5\" stop-color=\"#084490\"\/><stop offset=\"1\" stop-color=\"#0f2742\"\/><\/linearGradient>`+\n    `<linearGradient id=\"obN${K}\" x1=\"0\" y1=\"0\" x2=\"1\" y2=\"0\"><stop offset=\"0\" stop-color=\"#8796a8\"\/><stop offset=\".45\" stop-color=\"#e6ebf1\"\/><stop offset=\"1\" stop-color=\"#6f8399\"\/><\/linearGradient><\/defs>`;\n  \/\/ step 9: rim cross-section, spoke holes off the rim centre line (drawn toward the NDS)\n  const toDS=p.side==='DS', cx=100, xl=40, xr=160, top=34, yb=118, ySeat=106, sw=9, hx=cx+(toDS?13:-13), y9=158, ya=188;\n  g+=side(xl,18,'NDS',NAVY,'start')+side(xr,18,'DS',DSC,'end');\n  g+=`<path class=\"hl-s\" data-hl=\"osb\" d=\"M${xl} ${top} H${xl+sw} V${ySeat-14} Q${xl+sw} ${ySeat} ${xl+sw+18} ${ySeat} H${xr-sw-18} Q${xr-sw} ${ySeat} ${xr-sw} ${ySeat-14} V${top} H${xr} V${yb-14} Q${xr} ${yb} ${xr-22} ${yb} H${xl+22} Q${xl} ${yb} ${xl} ${yb-14} Z\" fill=\"url(#obG${K})\" stroke=\"${NAVY}\" stroke-width=\".8\"\/>`+\n    `<path d=\"M${xl+sw} ${top} h6 v6 h-6 Z M${xr-sw} ${top} h-6 v6 h6 Z\" fill=\"url(#obG${K})\"\/>`+\n    `<path d=\"M${xl+sw} ${top+28} Q${cx} ${top+36} ${xr-sw} ${top+28} v5 Q${cx} ${top+41} ${xl+sw} ${top+33} Z\" fill=\"#2f5b86\"\/>`;\n  g+=`<line x1=\"${cx}\" y1=\"24\" x2=\"${cx}\" y2=\"${y9+6}\" stroke=\"#C6D3DF\" stroke-width=\"1\" stroke-dasharray=\"7 3 1.5 3\"\/>`+\n    `<text x=\"${cx+3}\" y=\"30\" font-family=\"Inter,sans-serif\" font-size=\"8\" fill=\"#96ABC2\">rim centre<\/text>`;\n  g+=`<rect x=\"${hx-3.5}\" y=\"${ySeat}\" width=\"7\" height=\"${yb-ySeat+16}\" rx=\"1.3\" fill=\"url(#obN${K})\" stroke=\"#6f8399\" stroke-width=\".6\"\/>`+\n    `<rect x=\"${hx-6.5}\" y=\"${ySeat-7}\" width=\"13\" height=\"7\" rx=\"2.5\" fill=\"url(#obN${K})\" stroke=\"#6f8399\" stroke-width=\".6\"\/>`+\n    `<line x1=\"${hx}\" y1=\"${yb+16}\" x2=\"${hx}\" y2=\"${yb+28}\" stroke=\"#8a9bb0\" stroke-width=\"2\"\/>`;\n  g+=`<line class=\"hl-d\" data-hl=\"osb\" x1=\"${hx}\" y1=\"${top+10}\" x2=\"${hx}\" y2=\"${ya+4}\" stroke=\"#E63329\" stroke-width=\".9\" stroke-dasharray=\"3 3\"\/>`;\n  g+=dim(Math.min(hx,cx),Math.max(hx,cx),y9,'osb')+lbl(cx+(toDS?-34:34),y9,9,p.osb,'osb','osb',`Step 9: offset spoke bed, from the rim centre line to the spoke holes, toward the ${toDS?'DS':'NDS'}`);\n  g+=ext(xl,yb-8,ya+4,'osb')+ext(xr,yb-8,ya+4,'osb')+dim(xl,hx,ya,'osb')+dim(hx,xr,ya,'osb')+ring((xl+hx)\/2,ya,'a')+ring((hx+xr)\/2,ya,'b');\n  \/\/ step 10: spoke bed seen from the hub, holes alternating left and right of its centre line\n  const bx=292, bw=96, by=26, bh=150, z=12, y10=188, yc=206;\n  g+=side(bx-bw\/2,18,'NDS',NAVY,'start')+side(bx+bw\/2,18,'DS',DSC,'end');\n  g+=`<rect class=\"hl-s\" data-hl=\"rimZig\" x=\"${bx-bw\/2}\" y=\"${by}\" width=\"${bw}\" height=\"${bh}\" rx=\"8\" fill=\"#dfe8f2\" stroke=\"${NAVY}\" stroke-width=\".8\"\/>`+\n    `<line x1=\"${bx}\" y1=\"${by-6}\" x2=\"${bx}\" y2=\"${y10+6}\" stroke=\"#96ABC2\" stroke-width=\"1\" stroke-dasharray=\"7 3 1.5 3\"\/>`+\n    `<text x=\"${bx+3}\" y=\"${by+11}\" font-family=\"Inter,sans-serif\" font-size=\"8\" fill=\"#96ABC2\">bed centre<\/text>`;\n  const holes=[[by+16,-1],[by+44,1],[by+72,-1],[by+100,1],[by+128,-1]];\n  for(const [y,sg] of holes) g+=`<circle class=\"hl-f\" data-hl=\"rimZig\" cx=\"${bx+sg*z}\" cy=\"${y}\" r=\"5\" fill=\"#fff\" stroke=\"${sg<0?NAVY:DSC}\" stroke-width=\"1.6\"\/>`;\n  g+=ext(bx+z,by+100+6,y10+4,'rimZig')+dim(bx,bx+z,y10,'rimZig')+lbl(bx+z+34,y10,10,p.zig,'rimZig','rimZig','Step 10: rim hole offset, half the distance between the two rows of holes');\n  g+=ext(bx-z,by+128+6,yc+4,'rimZig')+ext(bx+z,y10+4,yc+4,'rimZig')+dim(bx-z,bx+z,yc,'rimZig')+ring(bx+z+22,yc,'c');\n  return g+'<\/svg>';\n}\nfunction drawOsbGuide(host){\n  const box=host.querySelector('[data-osbart]'); if(!box) return;\n  const v=f=>{ const x=num(host,f); return isNaN(x)?0:x; };\n  const st=n=>`<span class=\"step\">${n}<\/span>`, sp=pressed(host,'[data-seg=\"hubType\"]')==='straight';\n  box.innerHTML=osbSVG({key:host.id==='panel-rear'?'r':'f', osb:Math.abs(v('osb')), side:osbSigned(host)<0?'DS':'NDS', zig:v('rimZig')})+\n    `<ul class=\"osb-steps\">`+\n    `<li>${st(9)} <b>Offset spoke bed<\/b> (asymmetric rims): the spoke holes sit off the rim's centre line, toward one side; pick that side with the buttons next to the field. Use the spec-sheet value, or measure on the spoke bed from each rim edge to the middle of the same spoke hole: <b>${st(9)} = (larger \u2212 smaller) \u00f7 2<\/b>, toward the edge with the smaller distance. Rear wheels are usually offset toward the NDS, disc front wheels toward the DS. Symmetric rim: 0.<\/li>`+\n    `<li>${st(10)} <b>Rim hole offset<\/b> (zigzag drilling): the holes alternate left and right of the spoke-bed centre, each leaning toward the flange it feeds. Measure across from the middle of a left hole to the middle of a right hole (c) and enter half of it: <b>${st(10)} = c \u00f7 2<\/b>. Holes in one line: 0. If a spec sheet gives the full distance c, halve it.<\/li>`+\n    (sp?'':`<li>Not the same as Hole drilling below: that choice is about the spacing of the holes around the rim, this one about their sideways position.<\/li>`)+\n    `<\/ul>`;\n}\n\/* ---------- hole spacing around the rim (step 11) ----------\n   Paired holes: one NDS and one DS hole close together. On a hub with evenly offset flanges each\n   NDS hole sits this much further round than even spacing and each DS hole this much less. *\/\nfunction pairAngle(count,ratio,erd,s){\n  return (ratio==='1:1' && s>0 && erd>0 && count>0 && s<Math.PI*erd\/count) ? Math.PI\/count - s\/erd : 0;\n}\nfunction drillSVG(p){\n  const {NAVY,DSC,INK,LINE,fx,lbl,dim,ext}=measureKit();\n  const W=380, H=196, ev=p.erd>0&&p.n>0 ? Math.PI*p.erd\/p.n : 0;\n  const share=Math.min(.8,Math.max(.25,(p.pairS>0&&ev) ? p.pairS\/ev : .4));\n  const ring=(x,y,t)=>`<circle cx=\"${fx(x)}\" cy=\"${fx(y)}\" r=\"7\" fill=\"#EDF2F6\" stroke=\"${LINE}\" stroke-width=\".8\"\/><text x=\"${fx(x)}\" y=\"${fx(y+3.2)}\" font-family=\"Inter,sans-serif\" font-size=\"9\" font-style=\"italic\" font-weight=\"700\" fill=\"${INK}\" text-anchor=\"middle\">${t}<\/text>`;\n  const hole=(x,y,nds)=>`<circle cx=\"${fx(x)}\" cy=\"${y}\" r=\"5.5\" fill=\"#fff\" stroke=\"${nds?NAVY:DSC}\" stroke-width=\"1.8\"\/>`;\n  const title=(y,t)=>`<text x=\"30\" y=\"${y}\" font-family=\"Inter,sans-serif\" font-size=\"10\" font-weight=\"700\" fill=\"${NAVY}\">${t}<\/text>`;\n  const strip=y=>`<rect x=\"30\" y=\"${y}\" width=\"320\" height=\"30\" rx=\"8\" fill=\"#dfe8f2\" stroke=\"${NAVY}\" stroke-width=\".8\"\/>`;\n  let g=`<svg class=\"hm-side\" viewBox=\"0 0 ${W} ${H}\" xmlns=\"http:\/\/www.w3.org\/2000\/svg\" role=\"img\" aria-label=\"Even and paired spoke holes around the rim\">`;\n  \/\/ even: equal steps all the way round\n  g+=`<g opacity=\"${p.sel==='even'?1:.45}\">`+title(20,'Even')+strip(28);\n  for(let i=0;i<8;i++) g+=hole(50+40*i,43,i%2===0);\n  g+=ext(50,49,70,'drill')+ext(90,49,70,'drill')+dim(50,90,66,'drill')+ring(110,66,'e')+\n    `<text x=\"122\" y=\"69.5\" font-family=\"Inter,sans-serif\" font-size=\"10\" fill=\"${INK}\">${ev?'= '+ev.toFixed(1)+' mm on this rim':'even spacing'}<\/text><\/g>`;\n  \/\/ paired: one NDS and one DS hole close together, a bigger gap to the next pair\n  const sx=40*share, on=p.sel==='zigzag';\n  g+=`<g opacity=\"${on?1:.45}\">`+title(104,'Paired')+strip(112);\n  for(let i=0;i<4;i++){ const c=70+80*i; g+=hole(c-sx\/2,127,true)+hole(c+sx\/2,127,false); }\n  g+=ext(70-sx\/2,133,156,'pairSpacing')+ext(70+sx\/2,133,156,'pairSpacing')+dim(70-sx\/2,70+sx\/2,152,'pairSpacing')+\n    lbl(70+sx\/2+40,152,11,p.pairS>0?p.pairS:NaN,'pairSpacing','pairSpacing','Step 11: hole spacing within a pair, centre to centre along the rim')+'<\/g>';\n  g+=`<circle cx=\"36\" cy=\"182\" r=\"5\" fill=\"#fff\" stroke=\"${NAVY}\" stroke-width=\"1.8\"\/><text x=\"46\" y=\"185.5\" font-family=\"Inter,sans-serif\" font-size=\"9.5\" fill=\"${INK}\">NDS hole<\/text>`+\n    `<circle cx=\"112\" cy=\"182\" r=\"5\" fill=\"#fff\" stroke=\"${DSC}\" stroke-width=\"1.8\"\/><text x=\"122\" y=\"185.5\" font-family=\"Inter,sans-serif\" font-size=\"9.5\" fill=\"${INK}\">DS hole \u00b7 seen from the hub, rim unrolled<\/text>`;\n  return g+'<\/svg>';\n}\nfunction drawDrillGuide(host){\n  const sel=pressed(host,'[data-choice=\"drill\"]')||'even', erd=readLive(host).erd;\n  const count=parseInt(pressed(host,'[data-choice=\"count\"]'),10)||32, ev=erd>0 ? Math.PI*erd\/count : 0;\n  const hint=host.querySelector('[data-evenhint]'); if(hint) hint.textContent=ev?`even spacing \u2248 ${ev.toFixed(1)} mm`:'';\n  const box=host.querySelector('[data-drillart]'); if(!box) return;\n  const st=n=>`<span class=\"step\">${n}<\/span>`, s=num(host,'pairSpacing');\n  box.innerHTML=drillSVG({n:count, erd, pairS:s>0?s:0, sel})+\n    `<ul class=\"osb-steps\">`+\n    `<li><b>Even<\/b>: the holes are equally spaced all the way round. Almost every rim; nothing to enter.<\/li>`+\n    `<li><b>Paired<\/b>: the holes come in pairs, one NDS and one DS hole close together with a bigger gap to the next pair (some factory wheels, 1:1 lacing). Measure ${st(11)} along the rim from the centre of one hole to the centre of the other hole of the same pair. Each side then needs two spoke lengths.<\/li>`+\n    `<li>Holes grouped in threes (some 2:1 factory wheels) are not covered yet. Holes that alternate left and right across the rim are step ${st(10)}, not this.<\/li><\/ul>`;\n}\nfunction updateHubArt(host){\n  drawHubMeasure(host);\n  refreshSpokeArt(host);\n}\n\n\/* ---------- live preview ---------- *\/\nfunction readLive(host){\n  const key = host.id==='panel-rear' ? 'rear' : 'front';\n  const dflt = key==='rear' ? {flL:44,flR:44,fcL:35,fcR:19,erd:560} : {flL:44,flR:44,fcL:34,fcR:34,erd:560};\n  const pick=(f,d)=>{ const v=num(host,f); return isNaN(v)?{v:d,def:true}:{v,def:false}; };\n  const hf=hubFC(host), pickFC=(v,d)=>v>0?{v,def:false}:{v:d,def:true};\n  const re=rimERD(host), nipT=isFinite(re.nip.total)?re.nip.total:NIP_STD;\n  const flL=pick('flL',dflt.flL), flR=pick('flR',dflt.flR), fcL=pickFC(hf.fcL,dflt.fcL), fcR=pickFC(hf.fcR,dflt.fcR), erd=pickFC(re.erd,dflt.erd);\n  const nsd=(re.nsd>0 && !erd.def) ? re.nsd : erd.v-2*nipT;\n  const osb=osbSigned(host), rimZig=num(host,'rimZig')||0;\n  const tension=isNaN(num(host,'tension'))?110:num(host,'tension');\n  const drill=pressed(host,'[data-choice=\"drill\"]')||'even';\n  const pairS=drill==='zigzag' ? (num(host,'pairSpacing')||0) : 0;\n  const grpAngle=num(host,'grpAngle')||0;\n  const grpSpreadL=num(host,'grpSpreadL')||0;\n  const grpSpreadR=num(host,'grpSpreadR')||0;\n  const hubType=pressed(host,'[data-seg=\"hubType\"]')==='straight'?'straight':'classic';\n  return {\n    key, hubType,\n    count: parseInt(pressed(host,'[data-choice=\"count\"]'),10)||32,\n    ratio: pressed(host,'[data-choice=\"ratio\"]')||'1:1',\n    cross: crossPair(host)[0], crossR: crossPair(host)[1],\n    drill, pairS,\n    grpAngle: hubType==='straight'?grpAngle:0,\n    grpSpreadL: hubType==='straight'?grpSpreadL:0,\n    grpSpreadR: hubType==='straight'?grpSpreadR:0,\n    spLayout: pressed(host,'[data-choice=\"spLayout\"]')||'paired',\n    osb, rimZig, tension,\n    hole: hubType==='classic' ? (isNaN(num(host,'hole'))?2.6:num(host,'hole')) : 0,\n    offL: hubType==='straight' ? (num(host,'offL')||0) : 0,\n    offR: hubType==='straight' ? (num(host,'offR')||0) : 0,\n    tenMin: num(host,'tenMin')>0 ? num(host,'tenMin') : 0,\n    tenMax: num(host,'tenMax')>0 ? num(host,'tenMax') : null,\n    spokeDia: liveSpokeDia(host),\n    ...rimLive(host, nsd), nsd, nip:re.nip, nipAdd:nipT,\n    flL:flL.v, flR:flR.v, fcL:fcL.v, fcR:fcR.v, erd:erd.v,\n    hubSrc: hf.mode, rimSrc: rimMode(host),\n    usingDefaults: flL.def||flR.def||fcL.def||fcR.def||erd.def\n  };\n}\n\/* Spoke drawing generated from the inputs: hub connection, spoke profile (from the model\n   name or custom \u00d8), TCS grip, the rim section from the rim inputs and the calculated\n   lengths. Diameters at 4 px\/mm, rim and nipple at their own scale; the shaft is broken\n   because its length is not to scale. *\/\nfunction spokeProfile(host){\n  const mv=val(host,'model');\n  if(mv==='custom'){\n    const d=num(host,'customDia')>0?num(host,'customDia'):1.8, nm=val(host,'customName')||'Custom spoke';\n    return d<2 ? {kind:'db',end:2,mid:d,name:nm} : {kind:'plain',d,name:nm};\n  }\n  const name=(SPOKE_MODELS[parseInt(mv,10)]||['Spoke, 2.00mm'])[0], short=name.split(',')[0];\n  let m;\n  if((m=name.match(\/(\\d\\.\\d+)\\s*x\\s*(\\d\\.\\d+)\\s*mm\/))) return {kind:'aero',w:+m[1],t:+m[2],end:2,name:short};\n  if((m=name.match(\/(\\d\\.\\d+)\\s*\\\/\\s*(\\d\\.\\d+)\\s*mm\/))) return {kind:'sb',head:+m[1],rest:+m[2],name:short};\n  m=name.match(\/(\\d\\.\\d+)\\s*mm\/); const d=m?+m[1]:2;\n  return (\/\\(db\\)\/.test(name)||d<2) ? {kind:'db',end:2,mid:d,name:short} : {kind:'plain',d,name:short};\n}\nfunction rimSectionPts(H,Wd){   \/\/ (r, a) in mm; r from the rim's inner surface outward, a across the width\n  const hw=Wd\/2, deep=H>30, bead=H-5.5;\n  const bw=hw*(deep?0.42:0.8), widen=Math.min(H-7,Math.max(6,H*(deep?0.6:0.4)));\n  const half=[[0,0],[0.5,-bw*0.55],[2,-bw]];\n  for(let i=1;i<=6;i++){ const f=i\/6; half.push([2+(widen-2)*f,-(bw+(hw-bw)*Math.sin(f*Math.PI\/2))]); }\n  half.push([H-1.2,-hw],[H,-hw+1.2],[H-0.6,-hw+2.6],[bead+0.4,-hw+3],[bead,-hw+3.4],[bead-2,-hw*0.45]);\n  const outer=[...half,[bead-2.6,0],...half.slice(1).reverse().map(([r,a])=>[r,-a])];\n  const t=2.2, hw2=hw-t, bw2=Math.max(1,bw-t), topC=Math.max(t+3,bead-4.6), wid2=Math.max(t+1.8,Math.min(widen,topC-1));\n  const h2=[[t,0],[t+0.5,-bw2*0.55],[t+1.6,-bw2]];\n  for(let i=1;i<=5;i++){ const f=i\/5; h2.push([t+1.6+(wid2-t-1.6)*f,-(bw2+(hw2-bw2)*Math.sin(f*Math.PI\/2))]); }\n  h2.push([topC-1,-hw2],[topC,-hw2+1.4]);\n  return {outer, cavity:[...h2,[topC,0],...h2.slice(1).reverse().map(([r,a])=>[r,-a])]};\n}\nfunction spokeArt(host){\n  const sp=pressed(host,'[data-seg=\"hubType\"]')==='straight', tcs=pressed(host,'[data-seg=\"spokeType\"]')==='tcs';\n  const P=spokeProfile(host), o=readLive(host), k=4, cy=100;\n  const fm=n=>(+n).toFixed(Math.abs(n*10-Math.round(n*10))<1e-9?1:2);\n  const GA={'2.6':'12G','2.3':'13G','2.0':'14G','1.8':'15G'};\n  const thrD = P.kind==='plain'?P.d:(P.kind==='sb'?P.rest:2);\n  const prof = P.kind==='db' ? `double-butted \u00d8${fm(P.end)} \/ ${fm(P.mid)} \/ ${fm(P.end)}`\n    : P.kind==='sb' ? `single-butted \u00d8${fm(P.head)} \/ ${fm(P.rest)}`\n    : P.kind==='aero' ? `bladed ${fm(P.w)} \u00d7 ${fm(P.t)}` : `straight gauge \u00d8${fm(P.d)}`;\n  const lerp=(a,b,t)=>a+(b-a)*Math.max(0,Math.min(1,t));\n  const radAt=u=> P.kind==='plain' ? P.d\/2\n    : P.kind==='sb' ? lerp(P.head,P.rest,(u-0.14)\/0.07)\/2\n    : P.kind==='db' ? (u<0.5?lerp(P.end,P.mid,(u-0.12)\/0.07):lerp(P.mid,P.end,(u-0.81)\/0.07))\/2\n    : (u<0.5?lerp(P.end,P.w,(u-0.09)\/0.07):lerp(P.w,P.end,(u-0.84)\/0.07))\/2;\n  const xs=116, xe=tcs?520:548, g0=322, g1=340, xr=600;\n  const H=Math.max(12,Math.min(110,o.rimDepth||22)), Wd=Math.max(14,Math.min(70,o.rimWidth||22));\n  const NP=o.nip||{type:'round',len:12,add:NIP_STD,washer:0,head:2.2,label:'Round \u00b7 slotted 12 mm'}, nAdd=isFinite(NP.add)?NP.add:NIP_STD, nW=NP.washer||0;\n  const nHead=NP.head||0, nLen=NP.len||12, rSeat=2.2+nW;     \/\/ the nipple's own seat: on the spoke bed (2.2 mm in this section) or on its washer\n  const s=Math.min(4,150\/H,170\/Wd), xEnd=xr+(rSeat+nAdd)*s, VW=Math.round(xr+Math.max(H,rSeat+(NP.type==='internal'?nLen:nHead)+1)*s+34), yd=cy+92;\n  const ux=x=>(x-xs)\/(xe-xs), X=r=>xr+r*s, Y=a=>cy+a*s;\n  const tp=t=>`data-tip=\"${t}\" style=\"cursor:help\"`;\n  const piece=(xa,xb)=>{ const top=[],bot=[];\n    for(let x=xa;;x=Math.min(xb,x+2)){ const r=radAt(ux(x))*k; top.push(`${x.toFixed(1)} ${(cy-r).toFixed(2)}`); bot.unshift(`${x.toFixed(1)} ${(cy+r).toFixed(2)}`); if(x>=xb) break; }\n    return `M${top.join(' L')} L${bot.join(' L')} Z`; };\n  const poly=pts=>pts.map(([r,a],i)=>`${i?'L':'M'}${X(r).toFixed(1)} ${Y(a).toFixed(1)}`).join(' ')+' Z';\n  const lbl=(x,y,t,anchor,col,lx,ly)=>(lx!=null?`<line x1=\"${x}\" y1=\"${y+(y<ly?3:-9)}\" x2=\"${lx}\" y2=\"${ly}\" stroke=\"#96ABC2\" stroke-width=\".8\"\/>`:'')+\n    `<text x=\"${x}\" y=\"${y}\" font-family=\"Inter,sans-serif\" font-size=\"9.5\" font-weight=\"600\" fill=\"${col||'#647E9C'}\" text-anchor=\"${anchor||'middle'}\">${t}<\/text>`;\n  let svg=`<svg viewBox=\"0 0 ${VW} 214\" xmlns=\"http:\/\/www.w3.org\/2000\/svg\" role=\"img\" aria-label=\"${P.name} spoke drawing\">\n  <defs>\n    <linearGradient id=\"spSteel\" x1=\"0\" y1=\"0\" x2=\"0\" y2=\"1\"><stop offset=\"0\" stop-color=\"#DCE4EC\"\/><stop offset=\".35\" stop-color=\"#9FB1C3\"\/><stop offset=\".7\" stop-color=\"#6A8098\"\/><stop offset=\"1\" stop-color=\"#465C73\"\/><\/linearGradient>\n    <linearGradient id=\"spNip\" x1=\"0\" y1=\"0\" x2=\"0\" y2=\"1\"><stop offset=\"0\" stop-color=\"#EEF2F6\"\/><stop offset=\".45\" stop-color=\"#B7C3CF\"\/><stop offset=\"1\" stop-color=\"#7C8C9C\"\/><\/linearGradient>\n    <linearGradient id=\"spGold\" x1=\"0\" y1=\"0\" x2=\"0\" y2=\"1\"><stop offset=\"0\" stop-color=\"#F0D58C\"\/><stop offset=\".5\" stop-color=\"#C79B3F\"\/><stop offset=\"1\" stop-color=\"#8F6B22\"\/><\/linearGradient>\n    <pattern id=\"spHatch\" width=\"5\" height=\"5\" patternUnits=\"userSpaceOnUse\" patternTransform=\"rotate(45)\"><rect width=\"5\" height=\"5\" fill=\"#0A4A9A\"\/><line x1=\"0\" y1=\"0\" x2=\"0\" y2=\"5\" stroke=\"#3F78C2\" stroke-width=\"1.6\"\/><\/pattern>\n  <\/defs>\n  <text x=\"14\" y=\"17\" font-family=\"Inter,sans-serif\" font-size=\"11\" font-weight=\"700\" fill=\"#084490\">${P.name} \u00b7 ${prof} \u00b7 ${sp?'straight-pull':'J-bend'}${tcs?' \u00b7 TCS':''}<\/text>`;\n\n  \/\/ hub side\n  if(!sp){\n    const hx=88;\n    svg+=`<rect class=\"hl-f\" data-hl=\"hole hubName hubW\" x=\"6\" y=\"${cy-30}\" width=\"98\" height=\"10\" rx=\"5\" fill=\"url(#spHatch)\" ${tp(`Hub flange in section \u2014 spoke hole \u00d8 ${o.hole} mm`)}\/>`;\n    svg+=`<path d=\"M${hx} ${cy-31} L${hx} ${cy-12} Q${hx} ${cy} ${hx+12} ${cy} L${xs+1} ${cy}\" fill=\"none\" stroke=\"url(#spSteel)\" stroke-width=\"${(P.kind==='sb'?P.head:P.kind==='plain'?P.d:2)*k}\" ${tp('J-bend: the elbow passes through the flange hole and turns toward the rim')}\/>`;\n    svg+=`<ellipse cx=\"${hx}\" cy=\"${cy-33}\" rx=\"7.5\" ry=\"3.8\" fill=\"url(#spSteel)\" stroke=\"#465C73\" stroke-width=\".8\" ${tp('J-bend head \u2014 rests on the outside of the flange')}\/>`;\n    svg+=lbl(52,cy-50,'J-bend head','middle',null,hx-4,cy-36)+lbl(8,cy+34,'hub flange (section)','start',null,40,cy-20)+lbl(132,cy+30,'elbow','middle',null,hx+6,cy-3);\n  } else {\n    svg+=`<rect class=\"hl-f\" data-hl=\"offL offR hubName hubW\" x=\"24\" y=\"${cy-26}\" width=\"88\" height=\"52\" rx=\"6\" fill=\"url(#spHatch)\" ${tp('Straight-pull hub anchor in section')}\/>`;\n    svg+=`<rect x=\"54\" y=\"${cy-5.5}\" width=\"59\" height=\"11\" fill=\"#E7EEF6\"\/><rect x=\"40\" y=\"${cy-9.5}\" width=\"15\" height=\"19\" fill=\"#E7EEF6\"\/>`;\n    svg+=`<rect x=\"56\" y=\"${cy-4}\" width=\"${xs-55}\" height=\"8\" fill=\"url(#spSteel)\"\/>`;\n    svg+=`<rect x=\"42\" y=\"${cy-7.5}\" width=\"14\" height=\"15\" rx=\"2.5\" fill=\"url(#spSteel)\" stroke=\"#465C73\" stroke-width=\".8\" ${tp('Straight-pull head \u2014 seated in the anchor bore')}\/>`;\n    svg+=lbl(48,cy-38,'straight-pull head','middle',null,49,cy-8)+lbl(68,cy+42,'hub anchor (section)','middle',null,80,cy+26);\n  }\n\n  \/\/ shaft, broken in the middle\n  svg+=`<g class=\"hl-f\" data-hl=\"model customDia customName spokeW\" fill=\"url(#spSteel)\" stroke=\"#465C73\" stroke-width=\".7\" ${tp(`${P.name}: ${prof}. Weight and stretch use the thinnest section.`)}>`+\n    `<path d=\"${piece(xs,g0)}\"\/><path d=\"${piece(g1,xe)}\"\/><\/g>`;\n  const br=radAt(ux(g0))*k+5;\n  svg+=`<path d=\"M${g0-3} ${cy+br} L${g0+3} ${cy-br} M${g1-3} ${cy+br} L${g1+3} ${cy-br}\" stroke=\"#647E9C\" stroke-width=\"1\"\/>`;\n  if(P.kind==='aero') svg+=`<rect x=\"${xs+(xe-xs)*0.17}\" y=\"${cy-0.6}\" width=\"${g0-xs-(xe-xs)*0.17}\" height=\"1.2\" fill=\"#fff\" opacity=\".55\"\/><rect x=\"${g1}\" y=\"${cy-0.6}\" width=\"${xs+(xe-xs)*0.83-g1}\" height=\"1.2\" fill=\"#fff\" opacity=\".55\"\/>`;\n  \/\/ diameter callouts\n  const dl=(u,t)=>{ const x=xs+(xe-xs)*u, r=radAt(u)*k; return lbl(x.toFixed(0),cy-24,t,'middle','#084490',x.toFixed(0),cy-r-1); };\n  if(P.kind==='db') svg+=dl(0.05,`\u00d8${fm(P.end)}`)+dl(0.32,`\u00d8${fm(P.mid)}`)+dl(0.95,`\u00d8${fm(P.end)}`);\n  else if(P.kind==='sb') svg+=dl(0.06,`\u00d8${fm(P.head)}`)+dl(0.5,`\u00d8${fm(P.rest)}`);\n  else if(P.kind==='aero') svg+=dl(0.04,`\u00d8${fm(P.end)}`)+dl(0.32,`blade ${fm(P.w)} \u00d7 ${fm(P.t)}`)+dl(0.96,`\u00d8${fm(P.end)}`);\n  else svg+=dl(0.32,`\u00d8${fm(P.d)}`);\n\n  \/\/ TCS grip and thread\n  let xt=xe;\n  if(tcs){ const q=thrD*k*1.2;\n    svg+=`<rect x=\"${xe}\" y=\"${(cy-q\/2).toFixed(1)}\" width=\"26\" height=\"${q.toFixed(1)}\" fill=\"url(#spGold)\" stroke=\"#8F6B22\" stroke-width=\".8\" ${tp('TCS grip \u2014 hold this square while turning the nipple so the spoke cannot wind up')}\/>`;\n    svg+=lbl(xe+13,cy-42,'TCS grip (square)','middle','#E63329',xe+13,cy-q\/2-1);\n    xt=xe+26;\n  }\n  const tr=thrD*k\/2, xNip=NP.type==='internal'?X(rSeat):X(rSeat-(nLen-nHead));\n  let hatch=''; for(let x=xt+3;x<xNip;x+=3.2) hatch+=`M${x.toFixed(1)} ${(cy-tr).toFixed(1)} L${(x-2.4).toFixed(1)} ${(cy+tr).toFixed(1)} `;\n  svg+=`<rect x=\"${xt}\" y=\"${(cy-tr).toFixed(1)}\" width=\"${(xNip-xt).toFixed(1)}\" height=\"${(2*tr).toFixed(1)}\" fill=\"url(#spSteel)\" stroke=\"#465C73\" stroke-width=\".7\" ${tp(`Rolled thread \u00d8${fm(thrD)}${GA[fm(thrD)]?' ('+GA[fm(thrD)]+')':''} \u2014 screws into the nipple`)}\/>`;\n  svg+=`<path d=\"${hatch}\" stroke=\"#465C73\" stroke-width=\".7\" opacity=\".7\"\/>`;\n  svg+=lbl(xt+18,cy+34,`thread \u00d8${fm(thrD)}${GA[fm(thrD)]?' \u00b7 '+GA[fm(thrD)]:''}`,'middle',null,xt+12,cy+tr+1);\n\n  \/\/ rim section from the rim inputs\n  const RS=rimSectionPts(H,Wd);\n  svg+=`<path class=\"hl-f\" data-hl=\"rimWidth rimH bsd erd rimName\" d=\"${poly(RS.outer)}\" fill=\"url(#spHatch)\" stroke=\"#084490\" stroke-width=\".8\" ${tp(`Rim in section \u2014 ${Wd} \u00d7 ${Math.round(H)} mm (width \u00d7 height) from your rim inputs`)}\/>`;\n  svg+=`<path d=\"${poly(RS.cavity)}\" fill=\"#F6F9FB\"\/>`;\n  \/\/ the chosen nipple: barrel through the spoke bed with the head on it, or (internal) the whole nipple inside the rim\n  const nb=3.9*s\/2, nd=3.3*s\/2, q1=v=>(+v).toFixed(1);\n  let ng='';\n  if(nW>0) ng+=`<rect x=\"${q1(X(2.2))}\" y=\"${q1(cy-3.4*s)}\" width=\"${q1(nW*s)}\" height=\"${q1(6.8*s)}\" fill=\"#E8EDF2\" stroke=\"#6f8399\" stroke-width=\".7\"\/>`;\n  if(NP.type==='internal'){\n    const ih=4.4*s\/2, x1=X(rSeat), x2=X(rSeat+nLen), r=Math.min(0.6*s,2);\n    ng+=`<rect x=\"${q1(X(-0.3))}\" y=\"${q1(cy-tr)}\" width=\"${q1((rSeat+0.3)*s)}\" height=\"${q1(2*tr)}\" fill=\"url(#spSteel)\" stroke=\"#465C73\" stroke-width=\".6\"\/>`+\n      `<path d=\"M${q1(x1)} ${q1(cy-ih*0.7)} L${q1(x1+1.1*s)} ${q1(cy-ih)} H${q1(x2-r)} Q${q1(x2)} ${q1(cy-ih)} ${q1(x2)} ${q1(cy-ih+r)} V${q1(cy+ih-r)} Q${q1(x2)} ${q1(cy+ih)} ${q1(x2-r)} ${q1(cy+ih)} H${q1(x1+1.1*s)} L${q1(x1)} ${q1(cy+ih*0.7)} Z\" fill=\"url(#spNip)\" stroke=\"#5E6E7E\" stroke-width=\".7\"\/>`+\n      `<path d=\"M${q1(x1+1.3*s)} ${q1(cy-ih*0.45)} H${q1(x2-0.4*s)} M${q1(x1+1.3*s)} ${q1(cy+ih*0.45)} H${q1(x2-0.4*s)}\" stroke=\"#5E6E7E\" stroke-width=\".6\" opacity=\".6\"\/>`;\n  } else {\n    const xa=X(rSeat-(nLen-nHead)), xm=X(rSeat-(nLen-nHead)*0.45), xS=X(rSeat), xH=X(rSeat+nHead);\n    ng+=`<rect x=\"${q1(xa)}\" y=\"${q1(cy-nd)}\" width=\"${q1(xm-xa)}\" height=\"${q1(2*nd)}\" fill=\"url(#spNip)\" stroke=\"#5E6E7E\" stroke-width=\".7\"\/>`+\n      `<rect x=\"${q1(xm)}\" y=\"${q1(cy-nb)}\" width=\"${q1(xS-xm)}\" height=\"${q1(2*nb)}\" fill=\"url(#spNip)\" stroke=\"#5E6E7E\" stroke-width=\".7\"\/>`+\n      `<line x1=\"${q1(xa+0.4*s)}\" y1=\"${q1(cy-nd*0.45)}\" x2=\"${q1(xm-0.4*s)}\" y2=\"${q1(cy-nd*0.45)}\" stroke=\"#5E6E7E\" stroke-width=\".6\"\/>`;\n    if(NP.type==='square'||NP.type==='hex'){ const hw=(NP.type==='square'?3.6:4.6)*s\/2;\n      ng+=`<rect x=\"${q1(xS)}\" y=\"${q1(cy-hw)}\" width=\"${q1(xH-xS)}\" height=\"${q1(2*hw)}\" rx=\"1.2\" fill=\"url(#spNip)\" stroke=\"#5E6E7E\" stroke-width=\".7\"\/>`+\n        `<path d=\"M${q1(xS+0.3*s)} ${q1(cy-hw*0.5)} H${q1(xH-0.3*s)}${NP.type==='hex'?` M${q1(xS+0.3*s)} ${q1(cy+hw*0.5)} H${q1(xH-0.3*s)}`:''}\" stroke=\"#5E6E7E\" stroke-width=\".6\" opacity=\".6\"\/>`;\n    } else { const nh=4.0*s\/2;\n      ng+=`<path d=\"M${q1(xS)} ${q1(cy-nh)} L${q1(xS+(xH-xS)*0.55)} ${q1(cy-nh)} Q${q1(xH+0.25*s)} ${cy} ${q1(xS+(xH-xS)*0.55)} ${q1(cy+nh)} L${q1(xS)} ${q1(cy+nh)} Z\" fill=\"url(#spNip)\" stroke=\"#5E6E7E\" stroke-width=\".7\"\/>`+\n        `<rect x=\"${q1(xH-0.9*s)}\" y=\"${q1(cy-0.45*s)}\" width=\"${q1(1.2*s)}\" height=\"${q1(0.9*s)}\" fill=\"#5E6E7E\" opacity=\".8\"\/>`;\n    }\n  }\n  svg+=`<g ${tp(`${NP.label||'Nipple'} nipple \u2014 the spoke ends ${fm(nAdd+nW)} mm above the nipple seat${nW?` (${fm(nAdd)} in the nipple + ${fm(nW)} washer)`:''}`)}>${ng}<\/g>`;\n  \/\/ spoke thread hidden inside the nipple\n  svg+=`<path d=\"M${xNip.toFixed(1)} ${(cy-tr).toFixed(1)} L${xEnd.toFixed(1)} ${(cy-tr).toFixed(1)} L${xEnd.toFixed(1)} ${(cy+tr).toFixed(1)} L${xNip.toFixed(1)} ${(cy+tr).toFixed(1)}\" fill=\"none\" stroke=\"#465C73\" stroke-width=\".8\" stroke-dasharray=\"2.2 1.8\"\/>`;\n  svg+=lbl(xr-26,tcs?cy-58:cy-40,NP.type==='internal'?'internal nipple':'nipple','middle',null,NP.type==='internal'?X(rSeat+nLen*0.5):X(rSeat-(nLen-nHead)*0.3),cy-(NP.type==='internal'?2.2*s:nb)-1);\n  svg+=lbl(Math.round(X(H\/2)),cy+Wd*s\/2+15,`rim ${+Wd.toFixed(1)} \u00d7 ${Math.round(H)} mm (section)`,'middle');\n  svg+=`<line x1=\"${xEnd.toFixed(1)}\" y1=\"${(cy-Wd*s\/2-4).toFixed(1)}\" x2=\"${xEnd.toFixed(1)}\" y2=\"${yd+4}\" stroke=\"#E63329\" stroke-width=\".8\" stroke-dasharray=\"3 2\"\/>`;\n  svg+=`<text x=\"${(xEnd+3).toFixed(1)}\" y=\"${(cy-Wd*s\/2-6).toFixed(1)}\" font-family=\"Inter,sans-serif\" font-size=\"9\" font-weight=\"700\" fill=\"#E63329\" ${tp(`The spoke ends ${fm(nAdd+nW)} mm above the nipple seat: this diameter is the ERD the lengths use`)}>spoke end<\/text>`;\n\n  \/\/ length dimension\n  const x0=sp?56:92, done=host.classList.contains('has-results')&&!host.classList.contains('stale')&&host._calcLens;\n  const fl=a=>a.map(v=>v.toFixed(1)).join(' \/ ');\n  const lenTxt = done ? `spoke length \u00b7 NDS ${fl(host._calcLens.L)} mm \u00b7 DS ${fl(host._calcLens.R)} mm` : 'spoke length \u2014 press Calculate';\n  svg+=`<line x1=\"${x0}\" y1=\"${cy+8}\" x2=\"${x0}\" y2=\"${yd+4}\" stroke=\"#96ABC2\" stroke-width=\".8\"\/>`;\n  svg+=`<line x1=\"${x0}\" y1=\"${yd}\" x2=\"${xEnd.toFixed(1)}\" y2=\"${yd}\" stroke=\"#084490\" stroke-width=\"1\"\/>`;\n  svg+=`<path d=\"M${x0} ${yd} l7 -3 v6 z M${xEnd.toFixed(1)} ${yd} l-7 -3 v6 z\" fill=\"#084490\"\/>`;\n  const lw=lenTxt.length*5.3+16;\n  svg+=`<rect x=\"${(331-lw\/2).toFixed(1)}\" y=\"${yd-9}\" width=\"${lw.toFixed(1)}\" height=\"18\" rx=\"4\" fill=\"#F6F9FB\"\/>`;\n  svg+=`<text x=\"331\" y=\"${yd+3.5}\" font-family=\"Inter,sans-serif\" font-size=\"10\" font-weight=\"700\" fill=\"#084490\" text-anchor=\"middle\" ${tp(`Measured from ${sp?'under the head':'the inside of the elbow'} to the spoke end (on the ERD)`)}>${lenTxt}<\/text>`;\n  svg+=`<text x=\"331\" y=\"${yd+18}\" font-family=\"Inter,sans-serif\" font-size=\"8.5\" fill=\"#96ABC2\" text-anchor=\"middle\">length not to scale<\/text>`;\n  return svg+'<\/svg>';\n}\nfunction refreshSpokeArt(host){\n  const sa=host.querySelector('[data-spokeart]');\n  if(sa) sa.innerHTML=uniqSVG(spokeArt(host), host.id==='panel-rear'?'r':'f');\n}\nfunction liveSpokeDia(host){\n  const mv=val(host,'model');\n  const g = mv==='custom' ? customGmm(host) : (SPOKE_MODELS[parseInt(mv,10)]||[,0.022])[1];\n  return g>0 ? 2*Math.sqrt(g\/STEEL_GMM3\/Math.PI) : 1.8;\n}\n\/* One wheel model for the 2D front view and the 3D view: hub anchor and rim seat per\n   spoke. Leading spokes on both flanges turn the same way, so leading\/trailing match the\n   build specification (leading = theta + offset). On triplet rims the doubled side's holes\n   land in pairs and the single side's holes sit in the gaps, which together form the\n   evenly drilled rim of a real 2:1 \/ 1:2 wheel. *\/\nfunction wheelModel(o){\n  const N=o.count, ratio=o.ratio||'1:1';\n  const [nL,nR]=sidesFor(N,ratio);\n  const R=o.erd\/2, bed=-(o.osb||0), zig=o.rimZig||0, sp=o.hubType==='straight';\n  \/\/ an all-radial wheel just clocks the rim to the hub, so drilling\/group angles change nothing (as in calculate)\n  const anyX=(o.cross||0)>0 || ((o.crossR==null?o.cross:o.crossR)||0)>0;\n  const pA=(anyX && o.drill==='zigzag') ? pairAngle(N,ratio,o.erd,o.pairS) : 0;\n  const gA=(sp && anyX && o.grpAngle) ? o.grpAngle*Math.PI\/180 : 0;\n  const pr=sp && o.spLayout!=='even';\n  const sides={\n    L:{key:'L',name:'NDS',n:nL,k:o.cross||0,fl:o.flL,fc:o.fcL,spr:sp?(o.grpSpreadL||0):0,xs:-1,seatX:bed-zig,off:o.offL||0,tri:tripletDelta(N,ratio,'L')},\n    R:{key:'R',name:'DS',n:nR,k:(o.crossR==null?o.cross:o.crossR)||0,fl:o.flR,fc:o.fcR,spr:sp?(o.grpSpreadR||0):0,xs:1,seatX:bed+zig,off:o.offR||0,tri:tripletDelta(N,ratio,'R')}\n  };\n  for(const sd of [sides.L,sides.R]) sd.half=(pr && sd.k>0) ? 0.5 : 0;\n  const CODE={L:['Sa','Sb'],R:['Sd','Sc']}, base=-Math.PI\/2, spokes=[];\n  const push=(sd,j,lead,hubA,rimA)=>{\n    const role=sd.k>0?(lead?'leading':'trailing'):'radial';\n    spokes.push({side:sd.key,sideName:sd.name,role,lead,j,\n      code:sd.k>0?CODE[sd.key][lead?0:1]:(sd.key==='L'?'Sa\u00b7Sb':'Sc\u00b7Sd'),\n      hub:{x:sd.xs*(sd.fc+(sd.spr?(lead?1:-1)*sd.spr\/2:0)),r:sd.fl\/2,a:hubA-gA},\n      rim:{x:sd.seatX,r:R,a:rimA+(sd.key==='L'?pA:-pA)}});\n  };\n  const hubFirst=(sd,phase)=>{\n    const step=2*Math.PI\/sd.n, th=(sd.k+sd.half)*step+sd.tri;\n    \/\/ paired: leading spoke j and trailing spoke j+1 share one anchor; the rim holes stay put\n    for(let j=0;j<sd.n;j++){ const lead=j%2===0, h=phase+(sd.half?j-j%2-0.5:j)*step; push(sd,j,lead,h,h+(lead?1:-1)*th); }\n  };\n  const dbl = ratio==='2:1' ? 'L' : (ratio==='1:2' ? 'R' : null);\n  if(!dbl){ hubFirst(sides.L,base); hubFirst(sides.R,base+Math.PI\/nL); }\n  else {\n    const Dd=sides[dbl], Sg=sides[dbl==='L'?'R':'L'];\n    hubFirst(Dd,base);\n    const step2=2*Math.PI\/Dd.n, step1=2*Math.PI\/Sg.n, r0=base+(Dd.k%2+1.5)*step2;\n    for(let m=0;m<Sg.n;m++){ const lead=m%2===0, rim=r0+m*step1; push(Sg,m,lead,rim-(lead?1:-1)*(Sg.k+Sg.half)*step1,rim); }\n  }\n  for(const s of spokes){\n    const sd=sides[s.side], d=s.rim.a-s.hub.a;\n    s.theta=Math.abs(Math.atan2(Math.sin(d),Math.cos(d)))*180\/Math.PI;\n    const chord=Math.sqrt(s.hub.r*s.hub.r+R*R-2*s.hub.r*R*Math.cos(s.theta*Math.PI\/180)), dx=Math.abs(s.rim.x-s.hub.x);\n    s.brace=Math.atan2(dx,chord)*180\/Math.PI;\n    s.len=Math.sqrt(chord*chord+dx*dx)+(sp?sd.off:-(o.hole||0)\/2);\n  }\n  return {R,N,nL,nR,sides,spokes,dbl};\n}\nfunction liveFront(o,T){\n  T=T||{};\n  const S=240, c=120, TAU=2*Math.PI, K=o.idKey||o.key;\n  const M=wheelModel(o), depth=o.rimDepth||22, sc=113\/(M.R-4+depth), rIn=M.R*sc;\n  const rimIn=(M.R-4)*sc, rimOut=(M.R-4+depth)*sc;\n  \/\/ hub drawn at 1.6x so flange size stays visible next to the rim\n  const rFl={L:Math.max(5,M.sides.L.fl\/2*sc*1.6), R:Math.max(5,M.sides.R.fl\/2*sc*1.6)};\n  const col={L:GC.NDS,R:GC.DS}, f=v=>v.toFixed(1), P=(r,a)=>[c+r*Math.cos(a), c+r*Math.sin(a)];\n  let back='', front='', hits='', nips='';\n  for(const s of M.spokes){\n    const [hx,hy]=P(rFl[s.side],s.hub.a), [ex,ey]=P(rIn,s.rim.a), [nx,ny]=P(rimIn+.6,s.rim.a);\n    const t=`${s.sideName} ${s.role} spoke (${s.code}) \u2014 \u03b8 ${s.theta.toFixed(1)}\u00b0`;\n    let a=(s.side==='L'?T.ndsAdv:T.dsAdv)||'';\n    const gSp=s.side==='L'?(T.grpSplitL!=null?T.grpSplitL:T.grpSplit):(T.grpSplitR!=null?T.grpSplitR:T.grpSplit);\n    if(gSp!=null && s.role!=='radial') a=`${T.grpLbl||'Group offset'}: trailing runs \u2248${gSp}% of leading tension on this flange.`;\n    const ln=`<line x1=\"${f(hx)}\" y1=\"${f(hy)}\" x2=\"${f(ex)}\" y2=\"${f(ey)}\" stroke=\"#fff\" stroke-width=\"2.8\" stroke-opacity=\".9\" stroke-linecap=\"round\"\/>`+\n      `<line x1=\"${f(hx)}\" y1=\"${f(hy)}\" x2=\"${f(ex)}\" y2=\"${f(ey)}\" stroke=\"${grpCol(s.side,s.role)}\" stroke-width=\"1.35\" stroke-linecap=\"round\"\/>`;\n    if(s.side==='R') back+=ln; else front+=ln;\n    hits+=`<line x1=\"${f(hx)}\" y1=\"${f(hy)}\" x2=\"${f(nx)}\" y2=\"${f(ny)}\" stroke=\"#000\" stroke-opacity=\"0\" stroke-width=\"9\" data-tip=\"${t}\"${a?` data-adv=\"${a}\"`:''} style=\"pointer-events:stroke;cursor:help\"\/>`;\n    nips+=`<circle cx=\"${f(nx)}\" cy=\"${f(ny)}\" r=\"1.5\" fill=\"#e6ebf1\" stroke=\"#6f8399\" stroke-width=\".5\"\/>`;\n  }\n  \/\/ valve hole: the gap between rim holes closest to the top\n  const ang=M.spokes.map(s=>((s.rim.a%TAU)+TAU)%TAU).sort((p,q)=>p-q);\n  let va=-Math.PI\/2, best=1e9;\n  for(let i=0;i<ang.length;i++){\n    const mid=(ang[i]+(i+1<ang.length?ang[i+1]:ang[0]+TAU))\/2, d=Math.abs(Math.atan2(Math.sin(mid+Math.PI\/2),Math.cos(mid+Math.PI\/2)));\n    if(d<best){ best=d; va=mid; }\n  }\n  const [v1x,v1y]=P(rimIn-.5,va), [v2x,v2y]=P(rimIn-8,va);\n  const flHoles=side=>{\n    const sp=M.spokes.filter(s=>s.side===side);\n    if(rFl[side]*TAU\/sp.length<=5) return '';\n    return sp.map(s=>{ const [x,y]=P(rFl[side],s.hub.a); return `<circle cx=\"${f(x)}\" cy=\"${f(y)}\" r=\"1.2\" fill=\"#fff\" stroke=\"${col[side]}\" stroke-width=\".6\"\/>`; }).join('');\n  };\n  const ring=(r1,r2)=>`M${f(c-r2)} ${c}a${f(r2)} ${f(r2)} 0 1 0 ${f(2*r2)} 0a${f(r2)} ${f(r2)} 0 1 0 ${f(-2*r2)} 0Z M${f(c-r1)} ${c}a${f(r1)} ${f(r1)} 0 1 0 ${f(2*r1)} 0a${f(r1)} ${f(r1)} 0 1 0 ${f(-2*r1)} 0Z`;\n  return `<svg viewBox=\"0 0 ${S} ${S}\" xmlns=\"http:\/\/www.w3.org\/2000\/svg\">\n    <defs>\n      <radialGradient id=\"lfRim${K}\" cx=\"${c}\" cy=\"${c}\" r=\"${f(rimOut)}\" gradientUnits=\"userSpaceOnUse\"><stop offset=\"${(rimIn\/rimOut).toFixed(3)}\" stop-color=\"#0f2742\"\/><stop offset=\"${((rimIn+rimOut)\/2\/rimOut).toFixed(3)}\" stop-color=\"#3e6c9c\"\/><stop offset=\"1\" stop-color=\"#084490\"\/><\/radialGradient>\n      <radialGradient id=\"lfHub${K}\" cx=\".4\" cy=\".35\" r=\".8\"><stop offset=\"0\" stop-color=\"#3e6c9c\"\/><stop offset=\"1\" stop-color=\"#0f2742\"\/><\/radialGradient>\n    <\/defs>\n    ${back}\n    <circle class=\"hl-f\" data-hl=\"flR fcR endR hole offR\" cx=\"${c}\" cy=\"${c}\" r=\"${f(rFl.R)}\" fill=\"#FCE5D3\" stroke=\"${GC.DS}\" stroke-width=\".8\"\/>\n    ${flHoles('R')}\n    ${front}\n    <circle class=\"hl-f\" data-hl=\"flL fcL endL hole offL\" cx=\"${c}\" cy=\"${c}\" r=\"${f(rFl.L)}\" fill=\"url(#lfHub${K})\" stroke=\"#084490\" stroke-width=\".8\"\/>\n    ${flHoles('L')}\n    <circle cx=\"${c}\" cy=\"${c}\" r=\"2.4\" fill=\"#e6ebf1\" stroke=\"#6f8399\" stroke-width=\".6\"\/>\n    <path class=\"hl-f\" data-hl=\"erd rimName rimW bsd rimWidth rimH rimInner rimWall\" fill-rule=\"evenodd\" d=\"${ring(rimIn,rimOut)}\" fill=\"url(#lfRim${K})\" stroke=\"#084490\" stroke-width=\".6\" data-tip=\"Rim \u2014 ERD ${o.erd} mm \u00b7 ${o.rimWidth} \u00d7 ${Math.round(depth)} mm\" style=\"cursor:help\"\/>\n    <circle cx=\"${c}\" cy=\"${c}\" r=\"${f(rimOut-1.6)}\" fill=\"none\" stroke=\"#fff\" stroke-opacity=\".22\" stroke-width=\".8\"\/>\n    ${nips}\n    <line x1=\"${f(v1x)}\" y1=\"${f(v1y)}\" x2=\"${f(v2x)}\" y2=\"${f(v2y)}\" stroke=\"#8a9bb0\" stroke-width=\"3\" stroke-linecap=\"round\" data-tip=\"Valve hole\" style=\"cursor:help\"\/>\n    ${hits}\n  <\/svg>`;\n}\nfunction liveSide(o,T){\n  T=T||{};\n  const {fx,dim,vdim,ext,hext,codeLbl}=measureKit();\n  const NAVY=GC.NDS, DSC=GC.DS, GREY='#647E9C', K=o.idKey||o.key;\n  const S=300, H=246, cx=150, cy=180, rimY=46, seatY=rimY+8;\n  const radScale=(cy-rimY)\/(o.erd\/2);\n  const maxLat=Math.max(o.fcL,o.fcR,Math.abs(o.osb)+Math.abs(o.rimZig))+16;\n  const latScale=Math.min(100\/maxLat, 4.4);\n  const X=l=>cx+l*latScale;\n  const bed=-o.osb, seatL=bed-o.rimZig, seatR=bed+o.rimZig;\n  const lx=X(-o.fcL), rx=X(o.fcR);\n  const flHL=Math.min(54,Math.max(16,(o.flL\/2)*radScale*2.2)), flHR=Math.min(54,Math.max(16,(o.flR\/2)*radScale*2.2));\n  const sp=o.hubType==='straight';\n  const xl=X(seatL), xr=X(seatR);\n  const r1=v=>Math.round(v*10)\/10;\n  const tip=(t,a)=>`data-tip=\"${t}\"${a?` data-adv=\"${a}\"`:''}`;\n  const VAL={flL:`R1 \u2014 half the NDS flange \u00d8 (step 4: ${o.flL} mm) = ${+(o.flL\/2).toFixed(2)} mm`, flR:`R4 \u2014 half the DS flange \u00d8 (step 5: ${o.flR} mm) = ${+(o.flR\/2).toFixed(2)} mm`,\n    erd:`R \u2014 nipple-seat radius ${+(o.nsd\/2).toFixed(2)} mm (nipple seat \u00d8 ${+o.nsd.toFixed(2)} mm${o.rimSrc==='measure'?' = step 7 + 2 \u00d7 step 8':''}); the spokes end ${+o.nipAdd.toFixed(2)} mm further in, on the ERD ${+o.erd.toFixed(2)} mm`, fcL:`W-NDS \u2014 NDS flange to centre ${o.fcL} mm${o.hubSrc==='measure'?' (step 1 \u00f7 2 \u2212 step 2)':' (step 2)'}`, fcR:`W-DS \u2014 DS flange to centre ${o.fcR} mm${o.hubSrc==='measure'?' (step 1 \u00f7 2 \u2212 step 3)':' (step 3)'}`};\n  const spoke=(x1,y1,x2,y2,col,t,a,op)=>\n    `<line x1=\"${fx(x1)}\" y1=\"${fx(y1)}\" x2=\"${fx(x2)}\" y2=\"${fx(y2)}\" stroke=\"#fff\" stroke-width=\"3.8\" stroke-opacity=\".9\"\/>`+\n    `<line x1=\"${fx(x1)}\" y1=\"${fx(y1)}\" x2=\"${fx(x2)}\" y2=\"${fx(y2)}\" stroke=\"${col}\" stroke-width=\"1.9\"${op!=null&&op<1?` stroke-opacity=\"${op}\"`:''}\/>`+\n    `<line x1=\"${fx(x1)}\" y1=\"${fx(y1)}\" x2=\"${fx(x2)}\" y2=\"${fx(y2)}\" stroke=\"#000\" stroke-opacity=\"0\" stroke-width=\"12\" ${tip(t,a)} style=\"pointer-events:stroke;cursor:help\"\/>`;\n  let s=`<svg viewBox=\"0 0 ${S} ${H}\" xmlns=\"http:\/\/www.w3.org\/2000\/svg\"><defs>`+\n    `<linearGradient id=\"lsRim${K}\" x1=\"0\" y1=\"0\" x2=\"0\" y2=\"1\"><stop offset=\"0\" stop-color=\"#3e6c9c\"\/><stop offset=\".55\" stop-color=\"#084490\"\/><stop offset=\"1\" stop-color=\"#0f2742\"\/><\/linearGradient>`+\n    `<linearGradient id=\"lsL${K}\" x1=\"0\" y1=\"0\" x2=\"1\" y2=\"0\"><stop offset=\"0\" stop-color=\"#2f5b86\"\/><stop offset=\".5\" stop-color=\"#084490\"\/><stop offset=\"1\" stop-color=\"#10243a\"\/><\/linearGradient>`+\n    `<linearGradient id=\"lsR${K}\" x1=\"0\" y1=\"0\" x2=\"1\" y2=\"0\"><stop offset=\"0\" stop-color=\"#F29A55\"\/><stop offset=\".5\" stop-color=\"#D9530A\"\/><stop offset=\"1\" stop-color=\"#7E2E03\"\/><\/linearGradient>`+\n    `<linearGradient id=\"lsHub${K}\" x1=\"0\" y1=\"0\" x2=\"0\" y2=\"1\"><stop offset=\"0\" stop-color=\"#eef3f8\"\/><stop offset=\".45\" stop-color=\"#cdd8e4\"\/><stop offset=\"1\" stop-color=\"#9fb1c4\"\/><\/linearGradient>`+\n    `<linearGradient id=\"lsN${K}\" x1=\"0\" y1=\"0\" x2=\"1\" y2=\"0\"><stop offset=\"0\" stop-color=\"#8796a8\"\/><stop offset=\".45\" stop-color=\"#e6ebf1\"\/><stop offset=\"1\" stop-color=\"#6f8399\"\/><\/linearGradient><\/defs>`;\n  \/\/ centre lines, dash-dot like the parameter drawing\n  s+=`<line x1=\"10\" y1=\"${cy}\" x2=\"${S-10}\" y2=\"${cy}\" stroke=\"#C6D3DF\" stroke-width=\"1\" stroke-dasharray=\"7 3 1.5 3\"\/>`+\n    `<line x1=\"${cx}\" y1=\"${rimY-30}\" x2=\"${cx}\" y2=\"${cy+8}\" stroke=\"#C6D3DF\" stroke-width=\"1\" stroke-dasharray=\"3 4\"\/>`;\n  \/\/ hub: axle with end caps, cones out to the flanges, barrel between them\n  const hb=Math.max(6,Math.min(14,Math.round(0.42*Math.min(flHL,flHR)))), ax0=X(-(maxLat-5)), ax1=X(maxLat-5);\n  s+=`<rect x=\"${fx(ax0)}\" y=\"${cy-2.5}\" width=\"${fx(ax1-ax0)}\" height=\"5\" rx=\"2.5\" fill=\"#8a9bb0\"\/>`+\n    `<path d=\"M${fx(ax0+6)} ${cy-5} L${fx(lx)} ${cy-hb} L${fx(lx)} ${cy+hb} L${fx(ax0+6)} ${cy+5} Z\" fill=\"url(#lsHub${K})\" stroke=\"#9fb1c4\" stroke-width=\".6\"\/>`+\n    (o.key==='rear'\n      ? `<rect x=\"${fx(rx+4)}\" y=\"${cy-hb-3}\" width=\"${fx(Math.max(4,ax1-7-rx-4))}\" height=\"${2*hb+6}\" rx=\"2\" fill=\"url(#lsHub${K})\" stroke=\"#8796a8\" stroke-width=\".6\" data-tip=\"Cassette body (drive side)\" style=\"cursor:help\"\/>`+\n        Array.from({length:Math.max(0,Math.floor((ax1-7-rx-8)\/4))},(_,i)=>`<line x1=\"${fx(rx+8+4*i)}\" y1=\"${cy-hb-1}\" x2=\"${fx(rx+8+4*i)}\" y2=\"${cy+hb+1}\" stroke=\"#8796a8\" stroke-width=\".5\"\/>`).join('')\n      : `<path d=\"M${fx(ax1-6)} ${cy-5} L${fx(rx)} ${cy-hb} L${fx(rx)} ${cy+hb} L${fx(ax1-6)} ${cy+5} Z\" fill=\"url(#lsHub${K})\" stroke=\"#9fb1c4\" stroke-width=\".6\"\/>`)+\n    `<rect x=\"${fx(lx)}\" y=\"${cy-hb+2}\" width=\"${fx(Math.max(4,rx-lx))}\" height=\"${(hb-2)*2}\" fill=\"url(#lsHub${K})\" stroke=\"#9fb1c4\" stroke-width=\".6\"\/>`+\n    `<rect x=\"${fx(ax0-1)}\" y=\"${cy-6}\" width=\"7\" height=\"12\" rx=\"2\" fill=\"url(#lsHub${K})\" stroke=\"#6f8399\" stroke-width=\".6\"\/>`+\n    `<rect x=\"${fx(ax1-6)}\" y=\"${cy-6}\" width=\"7\" height=\"12\" rx=\"2\" fill=\"url(#lsHub${K})\" stroke=\"#6f8399\" stroke-width=\".6\"\/>`;\n  const fw=sp?8:5;\n  s+=`<rect class=\"hl-f\" data-hl=\"flL fcL endL hubWidth hole offL hubName hubW\" x=\"${fx(lx-fw\/2)}\" y=\"${fx(cy-flHL)}\" width=\"${fw}\" height=\"${fx(2*flHL)}\" rx=\"${fw\/2}\" fill=\"url(#lsL${K})\" stroke=\"${NAVY}\" stroke-width=\".7\" ${tip(`NDS flange \u2014 \u00d8 ${o.flL} mm \u00b7 ${o.fcL} mm to centre${sp?(o.offL?` \u00b7 spoke offset ${o.offL} mm`:''):` \u00b7 spoke hole \u00d8 ${o.hole} mm`}`,T.ndsFlAdv||'')} style=\"cursor:help\"\/>`;\n  s+=`<rect class=\"hl-f\" data-hl=\"flR fcR endR hubWidth hole offR hubName hubW\" x=\"${fx(rx-fw\/2)}\" y=\"${fx(cy-flHR)}\" width=\"${fw}\" height=\"${fx(2*flHR)}\" rx=\"${fw\/2}\" fill=\"url(#lsR${K})\" stroke=\"${DSC}\" stroke-width=\".7\" ${tip(`DS flange \u2014 \u00d8 ${o.flR} mm \u00b7 ${o.fcR} mm to centre${sp?(o.offR?` \u00b7 spoke offset ${o.offR} mm`:''):` \u00b7 spoke hole \u00d8 ${o.hole} mm`}`,T.dsFlAdv||'')} style=\"cursor:help\"\/>`;\n  \/\/ spokes from the flange to the nipple; a straight-pull spread splits the anchors (A2 \/ A3)\n  const sprL=(sp && o.grpSpreadL) ? o.grpSpreadL : 0, sprR=(sp && o.grpSpreadR) ? o.grpSpreadR : 0;\n  const drawFlangeSpokes=(x, flH, seatX, col, spread, outSign, baseTip, baseAdv, leadTip, trailTip, grpAdv)=>{\n    const y1=cy-flH+2;\n    if(!spread) return spoke(x,y1,seatX,seatY,col,baseTip,baseAdv)+`<circle cx=\"${fx(x)}\" cy=\"${fx(y1)}\" r=\"2.2\" fill=\"#fff\" stroke=\"${col}\" stroke-width=\"1.2\"\/>`;\n    const dx=Math.max(2.5, Math.abs(spread\/2)*latScale)*Math.sign(spread), xLead=x+outSign*dx, xTrail=x-outSign*dx;\n    let out='';\n    const colT=col===GC.NDS?GC.Sb:GC.Sc;\n    for(const [xx,cc,t] of [[xLead,col,leadTip],[xTrail,colT,trailTip]])\n      out+=spoke(xx,y1,seatX,seatY,cc,t,grpAdv,1)+`<circle cx=\"${fx(xx)}\" cy=\"${fx(y1)}\" r=\"2.2\" fill=\"#fff\" stroke=\"${cc}\" stroke-width=\"1.2\"\/>`;\n    const hl=outSign<0?'grpSpreadL':'grpSpreadR', code=outSign<0?'A2':'A3', a1=Math.min(xLead,xTrail), a2=Math.max(xLead,xTrail), ay=y1-10;\n    spreadPills.push({x:outSign<0?a2+14:a1-14, y:ay, code, v:Math.abs(spread), hl, nds:outSign<0});\n    return out+dim(a1,a2,ay,hl);\n  };\n  const spreadPills=[];\n  s+=drawFlangeSpokes(lx, flHL, xl, NAVY, sprL, -1, T.ndsTip||'NDS spoke', T.ndsAdv||'', T.ndsLeadTip||'NDS leading', T.ndsTrailTip||'NDS trailing', T.grpAdv||'');\n  s+=drawFlangeSpokes(rx, flHR, xr, DSC, sprR, +1, T.dsTip||'DS spoke', T.dsAdv||'', T.dsLeadTip||'DS leading', T.dsTrailTip||'DS trailing', T.grpAdv||'');\n  \/\/ A2 sits right of the NDS anchors, A3 left of the DS anchors; stack them when the flanges are close\n  if(spreadPills.length===2){\n    const [pl,pr]=spreadPills[0].nds?spreadPills:[spreadPills[1],spreadPills[0]], wA=v=>24+4+String(r1(v)).length*6.3+6;\n    if(pl.x+wA(pl.v)+4>pr.x-wA(pr.v)) pr.y-=22;\n  }\n  for(const q of spreadPills) s+=codeLbl(q.x, q.y, q.code, r1(q.v), q.hl, q.hl, `${q.code}: anchor spread ${q.nds?'NDS':'DS'}, between the two anchor rows`, q.nds?'start':'end');\n  \/\/ rim cross-section (spoke bed at the nipple seats) with the nipples\n  const rwPx=Math.max(18,Math.min(110,(o.rimWidth||22)*latScale)), rhPx=Math.max(8,Math.min(34,(o.rimDepth||22)*radScale));\n  const rb=rimY+3, rt=rb-rhPx, rl=cx-rwPx\/2, rr=cx+rwPx\/2, w=2.4;\n  s+=`<path class=\"hl-f\" data-hl=\"erd osb rimName rimW bsd rimWidth rimH rimInner rimWall\" d=\"M${fx(rl)} ${fx(rt)} H${fx(rl+w)} V${fx(rb-w-2)} Q${fx(rl+w)} ${fx(rb-w)} ${fx(rl+w+4)} ${fx(rb-w)} H${fx(rr-w-4)} Q${fx(rr-w)} ${fx(rb-w)} ${fx(rr-w)} ${fx(rb-w-2)} V${fx(rt)} H${fx(rr)} V${fx(rb-3)} Q${fx(rr)} ${fx(rb)} ${fx(rr-5)} ${fx(rb)} H${fx(rl+5)} Q${fx(rl)} ${fx(rb)} ${fx(rl)} ${fx(rb-3)} Z\" fill=\"url(#lsRim${K})\" stroke=\"${NAVY}\" stroke-width=\".6\" ${tip(`Rim \u2014 ERD ${o.erd} mm \u00b7 ${o.rimWidth} \u00d7 ${Math.round(o.rimDepth||22)} mm (width \u00d7 height)${o.osb?` \u00b7 OSB ${osbText(o.osb)}`:''}`,T.rimAdv||'')} style=\"cursor:help\"\/>`+\n    `<path d=\"M${fx(rl+w)} ${fx(rt)} h3 v2.5 h-3 Z M${fx(rr-w)} ${fx(rt)} h-3 v2.5 h3 Z\" fill=\"#084490\"\/>`+\n    `<path d=\"M${fx(rl+w)} ${fx(rt+rhPx*.38)} Q${cx} ${fx(rt+rhPx*.55)} ${fx(rr-w)} ${fx(rt+rhPx*.38)}\" fill=\"none\" stroke=\"#2f5b86\" stroke-width=\"1.6\"\/>`;\n  const intN=o.nip && o.nip.type==='internal';\n  const nipple=(x,headCol,t)=>intN\n    ? `<line x1=\"${fx(x)}\" y1=\"${seatY}\" x2=\"${fx(x)}\" y2=\"${rimY-5}\" stroke=\"#465C73\" stroke-width=\"1.6\"\/>`+\n      `<rect x=\"${fx(x-2.6)}\" y=\"${rimY-10}\" width=\"5.2\" height=\"10.6\" rx=\"1.4\" fill=\"url(#lsN${K})\" stroke=\"#6f8399\" stroke-width=\".5\"\/>`+\n      `<rect class=\"hl-f\" data-hl=\"rimZig osb\" x=\"${fx(x-3.2)}\" y=\"${rimY-0.4}\" width=\"6.4\" height=\"2\" rx=\"1\" fill=\"${headCol}\" ${tip(t+' \u00b7 internal nipple, inside the rim','')} style=\"cursor:help\"\/>`\n    : `<rect x=\"${fx(x-2)}\" y=\"${rimY-2}\" width=\"4\" height=\"10\" rx=\"1\" fill=\"url(#lsN${K})\" stroke=\"#6f8399\" stroke-width=\".5\"\/>`+\n      `<rect class=\"hl-f\" data-hl=\"rimZig osb\" x=\"${fx(x-3.2)}\" y=\"${rimY-4.5}\" width=\"6.4\" height=\"3.4\" rx=\"1.2\" fill=\"${headCol}\" ${tip(t,'')} style=\"cursor:help\"\/>`;\n  s+=nipple(xl, o.rimZig?NAVY:'#E63329', o.rimZig?`NDS nipple seat \u2014 ${o.rimZig} mm toward the NDS flange`:`Nipple seats${o.osb?` \u2014 spoke bed ${osbText(o.osb)}`:' \u2014 on the rim centre'}`);\n  if(o.rimZig) s+=nipple(xr, DSC, `DS nipple seat \u2014 ${o.rimZig} mm toward the DS flange`);\n  \/\/ bracing angle \u03b3 between the wheel plane and each spoke, at the rim\n  const gamma=(sx,px,py,val,col,left)=>{\n    const r=24, a0=Math.PI\/2, a1=Math.atan2(py-seatY,px-sx), sweep=a1>a0?1:0, la=a1+(a1>a0?.3:-.3);\n    return `<line x1=\"${fx(sx)}\" y1=\"${seatY}\" x2=\"${fx(sx)}\" y2=\"${seatY+32}\" stroke=\"#96ABC2\" stroke-width=\".8\" stroke-dasharray=\"2 2\"\/>`+\n      `<path d=\"M${fx(sx)} ${seatY+r} A${r} ${r} 0 0 ${sweep} ${fx(sx+r*Math.cos(a1))} ${fx(seatY+r*Math.sin(a1))}\" fill=\"none\" stroke=\"${col}\" stroke-width=\"1\"\/>`+\n      `<text x=\"${fx(sx+40*Math.cos(la))}\" y=\"${fx(seatY+40*Math.sin(la)+3)}\" font-family=\"Inter,sans-serif\" font-size=\"9.5\" font-weight=\"700\" fill=\"${col}\" text-anchor=\"${left?'end':'start'}\">\u03b3 ${val.toFixed(1)}\u00b0<\/text>`;\n  };\n  if(T.gL!=null) s+=gamma(xl,lx,cy-flHL+2,T.gL,NAVY,true);\n  if(T.gR!=null) s+=gamma(xr,rx,cy-flHR+2,T.gR,DSC,false);\n  \/\/ parameter-drawing codes as value badges, linked to the fields\n  const along=(x0,y0,x1,y1,t)=>[x0+(x1-x0)*t, y0+(y1-y0)*t];\n  const [pLx,pLy]=along(xl,seatY,lx,cy-flHL+2,.62), [pRx,pRy]=along(xr,seatY,rx,cy-flHR+2,.62);\n  s+=codeLbl(pLx-12,pLy,'Sa\u00b7Sb','',null,null,'NDS spokes: Sa leading, Sb trailing','end',NAVY);\n  s+=codeLbl(pRx+12,pRy,'Sc\u00b7Sd','',null,null,'DS spokes: Sd leading, Sc trailing','start',DSC);\n  s+=hext(lx-22,lx-3,cy-flHL,'flL')+vdim(lx-16,cy-flHL,cy,'flL')+codeLbl(lx-22,cy-flHL\/2,'R1',r1(o.flL\/2),'flL','flL',VAL.flL,'end',NAVY);\n  s+=hext(rx+3,rx+22,cy-flHR,'flR')+vdim(rx+16,cy-flHR,cy,'flR')+codeLbl(rx+22,cy-flHR\/2,'R4',r1(o.flR\/2),'flR','flR',VAL.flR,'start',DSC);\n  s+=codeLbl(rr+8,rt+rhPx\/2,'R',r1(o.nsd\/2),'erd','erd',VAL.erd,'start',GREY);\n  const yW=cy+30, pw=(code,v)=>Math.max(24,code.length*6+10)+4+String(v).length*6.3+6;\n  \/\/ keep the two width badges apart on strongly dished wheels\n  let wcL=(lx+cx)\/2, wcR=(cx+rx)\/2;\n  const lap=wcL+pw('W-NDS',r1(o.fcL))\/2+4-(wcR-pw('W-DS',r1(o.fcR))\/2);\n  if(lap>0){ wcL-=lap\/2; wcR+=lap\/2; }\n  if(flHL<30) s+=ext(lx,cy+flHL+2,yW+4,'fcL');\n  if(flHR<30) s+=ext(rx,cy+flHR+2,yW+4,'fcR');\n  s+=ext(cx,cy+8,yW+4,'fcL fcR')+dim(lx,cx,yW,'fcL endL')+dim(cx,rx,yW,'fcR endR')+\n    codeLbl(wcL,yW+17,'W-NDS',r1(o.fcL),'fcL','fcL',VAL.fcL,null,NAVY)+codeLbl(wcR,yW+17,'W-DS',r1(o.fcR),'fcR','fcR',VAL.fcR,null,DSC);\n  \/\/ side identity beside the axle ends\n  s+=`<text x=\"${fx(ax0-4)}\" y=\"${cy+18}\" font-family=\"Inter,sans-serif\" font-size=\"9.5\" font-weight=\"700\" fill=\"${NAVY}\" text-anchor=\"end\">NDS<\/text>`+\n    `<text x=\"${fx(ax1+4)}\" y=\"${cy+18}\" font-family=\"Inter,sans-serif\" font-size=\"9.5\" font-weight=\"700\" fill=\"${DSC}\" text-anchor=\"start\">DS<\/text>`;\n  return s+'<\/svg>';\n}\nfunction updateLive(host){\n  const fr=host.querySelector('[data-lpfront]'); if(!fr) return;\n  const o=readLive(host);\n  \/\/ geometry first (no spoke lengths \u2014 those stay behind Calculate)\n  const fcLeff=o.fcL-o.osb-o.rimZig, fcReff=o.fcR+o.osb-o.rimZig;\n  let nL,nR;\n  if(o.ratio==='1:2'){ nL=o.count\/3; nR=2*o.count\/3; }\n  else if(o.ratio==='2:1'){ nL=2*o.count\/3; nR=o.count\/3; }\n  else { nL=o.count\/2; nR=o.count\/2; }\n  const triL=tripletDelta(o.count,o.ratio,'L'), triR=tripletDelta(o.count,o.ratio,'R');\n  const pr=o.hubType==='straight' && o.spLayout!=='even';\n  const L=sideLength({flangeDia:o.flL,flangeToCenter:fcLeff,erd:o.erd,count:o.count,cross:o.cross,hole:0,hubType:'classic',paired:pr,thetaOffset:triL,sideCount:nL});\n  const R=sideLength({flangeDia:o.flR,flangeToCenter:fcReff,erd:o.erd,count:o.count,cross:o.crossR,hole:0,hubType:'classic',paired:pr,thetaOffset:triR,sideCount:nR});\n  const sL=Math.sin(L.brace*Math.PI\/180), sR=Math.sin(R.brace*Math.PI\/180);\n  \/\/ count-weighted: nL\u00b7T_L\u00b7sin(\u03b3L) = nR\u00b7T_R\u00b7sin(\u03b3R)\n  const wL=nL*sL, wR=nR*sR;\n  const bal=(wL>0&&wR>0)?Math.round(Math.min(wL,wR)\/Math.max(wL,wR)*100):100;\n  let tenL=o.tension, tenR=o.tension;\n  if(wL>0&&wR>0){ if(wL<=wR){tenL=o.tension; tenR=o.tension*wL\/wR;} else {tenR=o.tension; tenL=o.tension*wR\/wL;} }\n  o.nL=nL; o.nR=nR;\n  const hiNDS = tenL>=tenR;\n  const balAdv = bal>=85 ? 'Well balanced \u2014 both sides reach similar tension.'\n    : bal>=60 ? `Typical dished wheel \u2014 tension the ${hiNDS?'NDS':'DS'} to target, the other side follows.`\n    : `Low balance (${bal}%) \u2014 consider an asymmetric (OSB) rim or different hub geometry.`;\n  \/\/ straight-pull group split (trail vs lead): angular (rim-wrap) and\/or lateral (bracing) equilibrium\n  let grpStat='', grpSplit=null, T2={}, gSpL=null, gSpR=null;\n  if(o.hubType==='straight' && (o.grpAngle||o.grpSpreadL||o.grpSpreadR)){\n    const gR=(o.grpAngle||0)*Math.PI\/180, sHL=(o.grpSpreadL||0)\/2, sHR=(o.grpSpreadR||0)\/2;\n    const angOn=o.grpAngle&&(o.cross>0||o.crossR>0), tOff=angOn?gR:0;\n    const mkS=(fl,fc,toff,n,cr)=>sideLength({flangeDia:fl,flangeToCenter:fc,erd:o.erd,count:o.count,cross:cr,hole:0,hubType:'classic',paired:pr,thetaOffset:toff,sideCount:n});\n    const Ld=mkS(o.flL,fcLeff+sHL,+tOff+triL,o.nL,o.cross), Lt=mkS(o.flL,fcLeff-sHL,-tOff+triL,o.nL,o.cross);\n    const Rd=mkS(o.flR,fcReff+sHR,+tOff+triR,o.nR,o.crossR), Rt=mkS(o.flR,fcReff-sHR,-tOff+triR,o.nR,o.crossR);\n    const gt=groupTensions(o.nL,o.nR,Ld,Lt,Rd,Rt,o.erd\/2,o.tension);\n    const spL=gt?gt.splitL:1, spR=gt?gt.splitR:1;\n    const pL=Math.round(spL*100), pR=Math.round(spR*100); gSpL=spL; gSpR=spR;\n    grpSplit=pL;\n    grpStat=(pL===pR)\n      ? `<span class=\"lp-stat\">trail\/lead tension <b>${pL}%<\/b><\/span>`\n      : `<span class=\"lp-stat\">trail\/lead tension <b>NDS ${pL}% \u00b7 DS ${pR}%<\/b><\/span>`;\n    \/\/ per-group tip strings for the dish view\n    const tLeadL=2*tenL\/(1+spL), tTrailL=spL*tLeadL;\n    const tLeadR=2*tenR\/(1+spR), tTrailR=spR*tLeadR;\n    T2={grpSplitL:pL, grpSplitR:pR,\n      ndsLeadTip:`NDS leading \u2014 \u03b3 ${Ld.brace.toFixed(1)}\u00b0 \u00b7 \u2248${Math.round(tLeadL)} kgf`,\n      ndsTrailTip:`NDS trailing \u2014 \u03b3 ${Lt.brace.toFixed(1)}\u00b0 \u00b7 \u2248${Math.round(tTrailL)} kgf`,\n      dsLeadTip:`DS leading \u2014 \u03b3 ${Rd.brace.toFixed(1)}\u00b0 \u00b7 \u2248${Math.round(tLeadR)} kgf`,\n      dsTrailTip:`DS trailing \u2014 \u03b3 ${Rt.brace.toFixed(1)}\u00b0 \u00b7 \u2248${Math.round(tTrailR)} kgf`,\n      grpAdv:((o.grpSpreadL||o.grpSpreadR)?`Anchor spread${o.grpSpreadL?` NDS ${o.grpSpreadL} mm`:''}${o.grpSpreadR?` DS ${o.grpSpreadR} mm`:''}: each group has its own bracing angle and length; the tension stays even within the side.`:`Group offset: the groups differ in spoke angle and length, and a little in tension to balance the wind-up.`)\n    };\n  }\n  \/\/ tooltip strings\n  const T={\n    ndsTip:`NDS spoke \u2014 \u03b3 ${L.brace.toFixed(1)}\u00b0 \u00b7 \u2248${Math.round(tenL)} kgf (${Math.round(tenL\/Math.max(tenL,tenR)*100)}%)`,\n    dsTip:`DS spoke \u2014 \u03b3 ${R.brace.toFixed(1)}\u00b0 \u00b7 \u2248${Math.round(tenR)} kgf (${Math.round(tenR\/Math.max(tenL,tenR)*100)}%)`,\n    ndsAdv: hiNDS?'Higher-tension side \u2014 tension this side to target.':balAdv,\n    dsAdv: !hiNDS?'Higher-tension side \u2014 tension this side to target.':balAdv,\n    ndsFlAdv:`Bracing \u03b3 ${L.brace.toFixed(1)}\u00b0 on this side.`,\n    dsFlAdv:`Bracing \u03b3 ${R.brace.toFixed(1)}\u00b0 on this side.`,\n    rimAdv: o.osb?`Asymmetric bed improves balance to ${bal}%.`:balAdv,\n    thetaDeg:L.theta, thetaDegR:R.theta, gL:L.brace, gR:R.brace, grpSplit, ...T2\n  };\n  fr.innerHTML=liveFront(o,T);\n  host.querySelector('[data-lpside]').innerHTML=liveSide(o,T);\n  host.querySelector('[data-lpfrontcap]').innerHTML=`${o.count}h${o.ratio!=='1:1'?' ('+o.nL+'\/'+o.nR+')':''} \u00b7 ${crossText(o.cross,o.crossR)}${o.drill==='zigzag'&&o.pairS?' \u00b7 paired holes '+o.pairS+' mm':''}${o.grpAngle?' \u00b7 group \u00b1'+o.grpAngle+'\u00b0':''} <span class=\"lp-lg\"><i class=\"sa\"><\/i><i class=\"sb\"><\/i>NDS<i class=\"sd\"><\/i><i class=\"sc\"><\/i>DS<em>dark leading \u00b7 light trailing<\/em><\/span>`;\n  host.querySelector('[data-lpsidecap]').textContent=`dish view \u00b7 ERD ${o.erd}${o.osb?' \u00b7 OSB '+osbText(o.osb):''}${o.rimZig?' \u00b7 zigzag '+o.rimZig:''}${(o.grpSpreadL||o.grpSpreadR)?' \u00b7 spread '+(o.grpSpreadL||0)+'\/'+(o.grpSpreadR||0):''}`;\n  host.querySelector('[data-lpstats]').innerHTML=\n    (Math.abs(L.theta-R.theta)<0.05 ? `<span class=\"lp-stat\">spoke angle \u03b8 <b>${L.theta.toFixed(1)}\u00b0<\/b><\/span>`\n      : `<span class=\"lp-stat\">\u03b8 NDS <b>${L.theta.toFixed(1)}\u00b0<\/b><\/span><span class=\"lp-stat\">\u03b8 DS <b>${R.theta.toFixed(1)}\u00b0<\/b><\/span>`)+\n    `<span class=\"lp-stat\">\u03b3 NDS <b>${L.brace.toFixed(1)}\u00b0<\/b><\/span>`+\n    `<span class=\"lp-stat\">\u03b3 DS <b>${R.brace.toFixed(1)}\u00b0<\/b><\/span>`+\n    `<span class=\"lp-stat\">tension balance <b>${bal}%<\/b><\/span>`+grpStat+\n    (()=>{ const lo=Math.min(tenL,tenR), hi=Math.max(tenL,tenR);\n      const st = lo<o.tenMin ? '<span class=\"warn-chip\">LOW<\/span>' : (o.tenMax&&hi>o.tenMax ? '<span class=\"warn-chip\">OVER<\/span>' : '<span class=\"ok-chip\">OK<\/span>');\n      return `<span class=\"lp-stat\" data-hl=\"tension tenMin tenMax\" data-tip=\"${(o.tenMin||o.tenMax)?`Window ${o.tenMin?Math.round(o.tenMin):'\u2014'}\u2013${o.tenMax?Math.round(o.tenMax):'\u2014'} kgf`:'No tension window entered'}\">kgf NDS <b>${Math.round(tenL)}<\/b> \u00b7 DS <b>${Math.round(tenR)}<\/b> ${st}<\/span>`; })();\n  const tag=host.querySelector('[data-lpdefaults]'); if(tag) tag.style.display=o.usingDefaults?'':'none';\n  const rh=host.querySelector('[data-rimhint]');\n  if(rh){\n    const lbl=(WHEEL_SIZES.find(w=>w[0]===o.bsd)||[0,''])[1];\n    const hi=host.querySelector('[data-f=\"rimH\"]'); if(hi) hi.placeholder=o.rimDepthCalc>=12?'\u2248 '+Math.round(o.rimDepthCalc):'auto';\n    rh.textContent = o.rimDepthCalc<12\n      ? `A nipple seat \u00d8 of ${+o.nsd.toFixed(1)} mm does not fit a ${lbl} rim (BSD ${o.bsd}) \u2014 check the wheel size or the ERD.`\n      : o.rimDepthSet\n        ? (Math.abs(o.rimDepth-o.rimDepthCalc)>8\n            ? `Nipple seat \u00d8 ${+o.nsd.toFixed(1)} on a ${lbl} wheel suggests a rim height of about ${Math.round(o.rimDepthCalc)} mm \u2014 check the wheel size and the ERD if your rim is ${o.rimDepth} mm.`\n            : `Rim ${o.rimWidth} \u00d7 ${o.rimDepth} mm on a ${lbl} wheel.`)\n        : `Rim height \u2248 ${Math.round(o.rimDepthCalc)} mm, from the nipple seat \u00d8 ${+o.nsd.toFixed(1)} on a ${lbl} wheel. Enter the manufacturer's height to override.`;\n  }\n  drawHubMeasure(host);\n  drawRimMeasure(host);\n  drawNipple(host);\n  drawOsbGuide(host);\n  drawDrillGuide(host);\n  refreshSpokeArt(host);\n  if(window.HM3D) HM3D.update(host,{o,M:wheelModel(o),key:o.key,tenL,tenR,spL:gSpL,spR:gSpR,tenMin:o.tenMin,tenMax:o.tenMax,spokeDia:o.spokeDia});\n  if(host._hl) setHL(host,host._hl);\n}\n\/* field <-> drawing link: focusing or hovering a field highlights its dimension *\/\nfunction setHL(host,f){\n  host._hl=f||null;\n  host.querySelectorAll('.hl').forEach(e=>e.classList.remove('hl'));\n  if(f) host.querySelectorAll(`[data-hl~=\"${f}\"]`).forEach(e=>e.classList.add('hl'));\n  if(window.HM3D) HM3D.highlight(host,host._hl);\n}\nlet _lpT={};\nfunction scheduleLive(host){ clearTimeout(_lpT[host.id]); _lpT[host.id]=setTimeout(()=>updateLive(host),140); }\n\n\/* ---------- live preview hover tooltips ---------- *\/\n(function(){\n  const tipEl=document.createElement('div'); tipEl.id='lptip'; HMSC_ROOT.appendChild(tipEl);\n  let hideT=null;\n  function esc(s){return String(s).replace(\/[&<>\"]\/g,c=>({'&':'&amp;','<':'&lt;','>':'&gt;','\"':'&quot;'}[c]));}\n  function show(t,a,x,y,h,info,art){\n    tipEl.classList.toggle('info',!!info);\n    const pic=art?HOWART.get(art):'';\n    tipEl.innerHTML=`<span class=\"tt\">${esc(t)}<\/span>`+(pic?`<span class=\"tv\">${pic}<\/span>`:'')+(h?`<span class=\"th\"><b>How to measure<\/b>${esc(h)}<\/span>`:'')+(a?`<span class=\"ta\">${esc(a)}<\/span>`:'');\n    tipEl.classList.add('show'); move(x,y);\n  }\n  const showEl=(el,x,y)=>show(el.getAttribute('data-tip'),el.getAttribute('data-adv'),x,y,el.getAttribute('data-how'),el.hasAttribute('data-info'),el.getAttribute('data-art'));\n  function move(x,y){\n    const w=tipEl.offsetWidth||200, h=tipEl.offsetHeight||50;\n    let px=x+14, py=y+14;\n    if(px+w>window.innerWidth-8) px=x-w-12;\n    if(py+h>window.innerHeight-8) py=y-h-12;\n    px=Math.max(8,Math.min(px,window.innerWidth-w-8)); py=Math.max(8,Math.min(py,window.innerHeight-h-8));\n    tipEl.style.left=px+'px'; tipEl.style.top=py+'px';\n  }\n  function hide(){ tipEl.classList.remove('show'); }\n  document.addEventListener('mouseover',e=>{\n    const el=e.target.closest&&e.target.closest('[data-tip]');\n    if(el){ clearTimeout(hideT); showEl(el,e.clientX,e.clientY); }\n  });\n  document.addEventListener('focusin',e=>{\n    const el=e.target.closest&&e.target.closest('.tip[data-tip]');\n    if(el){ const r=el.getBoundingClientRect(); clearTimeout(hideT); showEl(el,r.right,r.bottom); }\n  });\n  document.addEventListener('focusout',e=>{ if(e.target.closest&&e.target.closest('.tip[data-tip]')) hide(); });\n  document.addEventListener('mousemove',e=>{\n    const el=e.target.closest&&e.target.closest('[data-tip]');\n    if(el) move(e.clientX,e.clientY); else hide();\n  });\n  document.addEventListener('mouseout',e=>{\n    if(e.target.closest&&e.target.closest('[data-tip]')) hide();\n  });\n  \/\/ touch: tap to show, auto-hide\n  document.addEventListener('touchstart',e=>{\n    const el=e.target.closest&&e.target.closest('[data-tip]');\n    if(el){ const t=e.touches[0]; showEl(el,t.clientX,t.clientY);\n      const n=(el.getAttribute('data-tip')||'').length+(el.getAttribute('data-how')||'').length;\n      clearTimeout(hideT); hideT=setTimeout(hide,Math.min(16000,2800+n*30+(el.hasAttribute('data-art')?3000:0))); }\n    else hide();\n  },{passive:true});\n})();\n\/* conditional lacing choices: counts must fit the NDS:DS ratio (1:1 even,\n   1:2 & 2:1 divisible by 3) and the cross must keep the spoke angle (hub hole to\n   rim hole, seen from the axle) within CROSS_MAX_DEG. Invalid options grey out;\n   when a change invalidates the current selection it snaps to the nearest valid\n   option with a toast. *\/\nfunction crossMode(host){ return pressed(host,'[data-seg=\"crossMode\"]')==='split'?'split':'same'; }\nfunction crossPair(host){\n  const cL=parseInt(pressed(host,'[data-choice=\"cross\"]'),10)||0;\n  return [cL, crossMode(host)==='split' ? (parseInt(pressed(host,'[data-choice=\"crossR\"]'),10)||0) : cL];\n}\nfunction crossText(cL,cR){ return cL===cR ? crossLabel(cL) : `NDS ${crossLabel(cL)} \/ DS ${crossLabel(cR)}`; }\nfunction setCrossModeUI(host,v){\n  const split=v==='split';\n  host.querySelectorAll('[data-seg=\"crossMode\"] button').forEach(x=>x.setAttribute('aria-pressed',x.dataset.v===(split?'split':'same')?'true':'false'));\n  const card=host.querySelector('[data-card=\"lacing\"]'); if(card) card.classList.toggle('cross-split',split);\n  const r=host.querySelector('.cross-r'); if(r) r.style.display=split?'':'none';\n}\nfunction applyCrossMode(host,v,interactive){\n  setCrossModeUI(host,v);\n  if(v==='split') setChoice(host,'crossR',pressed(host,'[data-choice=\"cross\"]')||'0');\n  refreshChoices(host,interactive);\n}\n\/* Triplet rims (1:2 \/ 2:1) drill the doubled side's holes in pairs rather than evenly.\n   With the doubled flange centred on the pairs, each of its spokes lands 90\/N degrees\n   further round than even spacing when the two spokes of a pair splay apart (V, the\n   standard build) and 90\/N degrees less when they cross each other at the rim (X). *\/\nfunction tripletDelta(count,ratio,side){\n  const doubled=(ratio==='2:1'&&side==='L')||(ratio==='1:2'&&side==='R');\n  return doubled ? Math.PI\/(2*count) : 0;\n}\nfunction sidesFor(count,ratio){\n  if(ratio==='1:2') return [count\/3, 2*count\/3];\n  if(ratio==='2:1') return [2*count\/3, count\/3];\n  return [count\/2, count\/2];\n}\nfunction maxCrossAngle(count,ratio,k){\n  const [a,b]=sidesFor(count,ratio);\n  return Math.max(360*k\/a, 360*k\/b);\n}\nfunction crossAngle(k,nS,spHalf,triDeg){ return 360*(k+(k>0?spHalf:0))\/nS + triDeg; }\nfunction refreshChoices(host, interactive){\n  let snapped=false; const msgs=[];\n  if(!pressed(host,'[data-choice=\"ratio\"]')){ setChoice(host,'ratio','1:1'); snapped=true; }\n  const ratio = pressed(host,'[data-choice=\"ratio\"]')||'1:1';\n  const cRaw = pressed(host,'[data-choice=\"count\"]');\n  let count = parseInt(cRaw,10)||32;\n  const okCounts = SPOKE_COUNTS.filter(n=> ratio==='1:1' ? n%2===0 : n%3===0);\n  if(!cRaw || !okCounts.includes(count)){\n    let best=okCounts[0];\n    for(const n of okCounts){ if(Math.abs(n-count)<Math.abs(best-count)) best=n; }\n    setChoice(host,'count',best); snapped=true;\n    if(cRaw) msgs.push(`${count} spokes is not possible with ${ratio} \u2014 switched to ${best}.`);\n    count=best;\n  }\n  host.querySelectorAll('[data-choice=\"count\"] button').forEach(b=>{\n    const n=+b.dataset.v, valid=okCounts.includes(n);\n    b.classList.toggle('is-off',!valid);\n    b.setAttribute('aria-disabled',String(!valid));\n    if(!valid) b.setAttribute('data-tip', ratio==='1:1'\n      ? `${n} spokes needs triplet lacing (1:2 or 2:1) \u2014 1:1 needs an even count`\n      : `${n} spokes is not divisible by 3, as ${ratio} lacing requires`);\n    else b.removeAttribute('data-tip');\n  });\n  const [sL0,sR0]=sidesFor(count,ratio);\n  \/\/ 1:2 and 2:1 put a different number of spokes on each side, so one cross number would\n  \/\/ mean a different spoke angle per side: the pattern is always chosen per side. When the\n  \/\/ switch was automatic, going back to 1:1 restores Same both sides.\n  const triplet=ratio!=='1:1';\n  if(triplet && crossMode(host)!=='split'){\n    setCrossModeUI(host,'split');\n    setChoice(host,'crossR',pressed(host,'[data-choice=\"cross\"]')||'0');\n    host.dataset.crossAuto='1';\n    msgs.push(`${ratio} puts ${sL0} spokes on the NDS and ${sR0} on the DS, so the lacing pattern is set per side.`);\n  } else if(!triplet && host.dataset.crossAuto){\n    delete host.dataset.crossAuto;\n    if(crossMode(host)==='split'){\n      const a=parseInt(pressed(host,'[data-choice=\"cross\"]'),10)||0, b=parseInt(pressed(host,'[data-choice=\"crossR\"]'),10)||0;\n      setChoice(host,'cross',String(Math.max(a,b))); setCrossModeUI(host,'same');\n      if(a!==b) msgs.push(`Back to 1:1: the same lacing pattern on both sides (${crossLabel(Math.max(a,b))}).`);\n    }\n  }\n  host.querySelectorAll('[data-seg=\"crossMode\"] button[data-v=\"same\"]').forEach(b=>{\n    b.classList.toggle('is-off',triplet);\n    b.setAttribute('aria-disabled',String(triplet));\n    if(triplet) b.setAttribute('data-tip',`Not with ${ratio} lacing: the NDS has ${sL0} spokes and the DS ${sR0}, so each side gets its own pattern.`);\n    else b.removeAttribute('data-tip');\n  });\n  const spHalf=(pressed(host,'[data-seg=\"hubType\"]')==='straight' && (pressed(host,'[data-choice=\"spLayout\"]')||'paired')!=='even') ? 0.5 : 0;\n  const split=crossMode(host)==='split';\n  const crossGroups = split ? [['cross',sL0,'NDS','L'],['crossR',sR0,'DS','R']] : [['cross',sL0,'','L']];\n  const oddSide = sL0%2!==0 ? 'NDS' : (sR0%2!==0 ? 'DS' : '');\n  for(const [grp,nS,sideName,sk] of crossGroups){\n    const curRaw=pressed(host,`[data-choice=\"${grp}\"]`);\n    const cur=parseInt(curRaw,10)||0;\n    const odd = split ? nS%2!==0 : !!oddSide;\n    const oddName = split ? sideName : oddSide, oddN = split ? nS : (oddSide==='NDS'?sL0:sR0);\n    const triDeg = tripletDelta(count,ratio,sk)*180\/Math.PI;   \/\/ a triplet's doubled side lands 90\/N further round\n    const ang=k=>crossAngle(k,nS,spHalf,triDeg);\n    const ok=CROSSES.filter(k=>k===0 || (!odd && ang(k)<=CROSS_MAX_DEG+1e-9));\n    const reason=k=> odd\n      ? `crossed lacing needs an even number of spokes per side so leading and trailing spokes can pair up \u2014 the ${oddName} side has ${oddN}, so it can only be radial.`\n      : `${crossLabel(k)} on ${nS} spokes${sideName?' ('+sideName+')':''} puts the spoke angle at ${Math.round(ang(k))}\u00b0${spHalf?' (paired straight-pull anchors add half a hole)':''}${triDeg?` (the paired triplet holes add ${triDeg.toFixed(1)}\u00b0)`:''}. Past ${CROSS_MAX_DEG}\u00b0 the spoke would leave the flange beyond tangent, back over the hub.`;\n    if(!curRaw || !ok.includes(cur)){\n      const lower=ok.filter(k=>k<=cur); const best=lower.length?Math.max(...lower):0;\n      setChoice(host,grp,best); snapped=true;\n      if(curRaw) msgs.push(`${crossLabel(cur)} is not possible with ${nS} spokes${sideName?' on the '+sideName+' side':''} \u2014 switched to ${crossLabel(best)}.`);\n    }\n    host.querySelectorAll(`[data-choice=\"${grp}\"] button`).forEach(b=>{\n      const k=+b.dataset.v, valid=ok.includes(k);\n      b.classList.toggle('is-off',!valid);\n      b.setAttribute('aria-disabled',String(!valid));\n      if(!valid) b.setAttribute('data-tip',`Not buildable: ${reason(k)}`);\n      else b.removeAttribute('data-tip');\n    });\n  }\n  \/\/ paired holes hold one NDS and one DS spoke each, so they need 1:1 lacing\n  const drillGrp = host.querySelector('[data-choice=\"drill\"]');\n  if(drillGrp){\n    const curDrill = pressed(host,'[data-choice=\"drill\"]')||'even';\n    if(ratio!=='1:1' && curDrill!=='even'){\n      setChoice(host,'drill','even'); snapped=true;\n      const da=host.querySelector('.drill-angle'); if(da) da.style.display='none';\n      msgs.push('Paired holes hold one NDS and one DS spoke, so they need 1:1 lacing \u2014 switched to Even.');\n    }\n    drillGrp.querySelectorAll('button').forEach(b=>{\n      const valid = b.dataset.v==='even' || ratio==='1:1';\n      b.classList.toggle('is-off',!valid);\n      b.setAttribute('aria-disabled',String(!valid));\n      if(!valid) b.setAttribute('data-tip','Paired holes hold one NDS and one DS spoke, so they need 1:1 lacing. Holes grouped in threes (some 2:1 wheels) are not covered yet.');\n      else b.removeAttribute('data-tip');\n    });\n  }\n  if(interactive && msgs.length) toast(msgs.join(' '));\n  return snapped;\n}\nfunction markStale(host){ if(host.classList.contains('has-results')) host.classList.add('stale'); }\n\/* rim-hole repeat strip for the build specification: which spoke group goes in each hole *\/\nfunction groupSeqSVG(count,ratio,cL,cR){\n  let seq, desc;\n  if(ratio==='1:2'){ seq=[['NDS','L'],['DS','L'],['DS','T'],['NDS','T'],['DS','L'],['DS','T']]; desc='1 NDS : 2 DS'; }\n  else if(ratio==='2:1'){ seq=[['DS','L'],['NDS','L'],['NDS','T'],['DS','T'],['NDS','L'],['NDS','T']]; desc='2 NDS : 1 DS'; }\n  else { seq=[['NDS','L'],['DS','L'],['NDS','T'],['DS','T']]; desc='alternating NDS \/ DS'; }\n  const radialSide=side=>(side==='NDS'?cL:cR)===0;\n  const CODES={'NDS L':'Sa','NDS T':'Sb','DS L':'Sd','DS T':'Sc'};\n  const W=460,Hh=126,n=seq.length,x0=70,xe=W-70,step=(xe-x0)\/(n-1);\n  let s=`<svg viewBox=\"0 0 ${W} ${Hh}\" xmlns=\"http:\/\/www.w3.org\/2000\/svg\">`;\n  s+=`<text x=\"${W\/2}\" y=\"13\" font-family=\"Inter,sans-serif\" font-size=\"10.5\" font-weight=\"700\" fill=\"#084490\" text-anchor=\"middle\">${count}h \\u00b7 ${ratio} \\u00b7 ${crossText(cL,cR)} \\u2014 rim-hole repeat of ${n} (${desc})<\/text>`;\n  s+=`<line x1=\"34\" y1=\"30\" x2=\"${W-34}\" y2=\"30\" stroke=\"#084490\" stroke-width=\"8\" stroke-linecap=\"round\"\/>`;\n  seq.forEach((sr,i)=>{\n    const side=sr[0], role=sr[1];\n    const x=x0+i*step, col=radialSide(side)?(side==='NDS'?GC.Sa:GC.Sd):grpCol(side,role);\n    const dir=radialSide(side)?0:(role==='L'?-1:1);\n    const x2=x+dir*12;\n    s+=`<line x1=\"${x.toFixed(1)}\" y1=\"34\" x2=\"${x2.toFixed(1)}\" y2=\"86\" stroke=\"${col}\" stroke-width=\"2.4\"\/>`;\n    s+=`<circle cx=\"${x.toFixed(1)}\" cy=\"30\" r=\"3.2\" fill=\"#fff\" stroke=\"${col}\" stroke-width=\"1.6\"\/>`;\n    s+=`<text x=\"${x.toFixed(1)}\" y=\"103\" font-family=\"Inter,sans-serif\" font-size=\"11\" font-weight=\"700\" fill=\"${col}\" text-anchor=\"middle\">${i+1}<\/text>`;\n    s+=`<text x=\"${x.toFixed(1)}\" y=\"117\" font-family=\"Inter,sans-serif\" font-size=\"8.5\" font-weight=\"600\" fill=\"rgba(8,68,144,.62)\" text-anchor=\"middle\">${side}${radialSide(side)?'':' \\u00b7 '+CODES[side+' '+role]}<\/text>`;\n  });\n  return s+'<\/svg>';\n}\n\/* custom spoke model: the mid-section \u00d8 drives weight & stretch *\/\nfunction customGmm(host){\n  const d=num(host,'customDia');\n  return (!isNaN(d)&&d>0) ? Math.PI*Math.pow(d\/2,2)*STEEL_GMM3 : NaN;\n}\nfunction toggleCustomSpoke(host){\n  const on = val(host,'model')==='custom';\n  host.querySelectorAll('.custom-spoke').forEach(el=>el.style.display=on?'':'none');\n  updateCustomHint(host);\n}\nfunction updateCustomHint(host){\n  const el=host.querySelector('[data-customhint]'); if(!el) return;\n  const g=customGmm(host);\n  el.textContent = isNaN(g)\n    ? 'Enter the mid-section \\u00d8 \\u2014 weight and stretch are derived from it.'\n    : `\\u2248 ${(g*1000).toFixed(1)} g\/m \\u00b7 ${(g*290).toFixed(1)} g per 290 mm spoke \\u00b7 section ${(Math.PI*Math.pow(num(host,'customDia')\/2,2)).toFixed(2)} mm\\u00b2`;\n}\n\/* parameter-code labels: click to jump to the linked field *\/\ndocument.addEventListener('click',e=>{\n  const t=e.target.closest&&e.target.closest('[data-goto]');\n  if(!t) return;\n  const host=t.closest('.panel'); if(!host) return;\n  const f=t.getAttribute('data-goto'), alt={fcL:'endL',fcR:'endR',endL:'fcL',endR:'fcR',erd:'rimInner',rimInner:'erd'};\n  let el=host.querySelector(`[data-f=\"${f}\"]`);\n  if(el && !el.offsetParent && alt[f]){ const e2=host.querySelector(`[data-f=\"${alt[f]}\"]`); if(e2 && e2.offsetParent) el=e2; }\n  if(!el) return;\n  const det=el.closest('details'); if(det && !det.open) det.open=true;\n  el.scrollIntoView({block:'center',behavior:'smooth'});\n  el.focus({preventScroll:true});\n  const box=el.closest('.inp')||el;\n  box.classList.add('flash');\n  setTimeout(()=>box.classList.remove('flash'),1600);\n});\nfunction wire(host, key){\n  host.addEventListener('focusin',e=>{ const f=e.target.dataset&&e.target.dataset.f; if(f) setHL(host,f);\n    if(f==='pairSpacing'){ const g=host.querySelector('[data-drillguide]'); if(g && !g.dataset.closed){ g.open=true; drawDrillGuide(host); setHL(host,f); } }\n    if(f==='osb'||f==='rimZig'){ const g=host.querySelector('[data-osbguide]'); if(g && !g.dataset.closed){ g.open=true; drawOsbGuide(host); setHL(host,f); } } });\n  for(const sel of ['[data-osbguide]','[data-drillguide]']){ const g=host.querySelector(sel); if(g) g.addEventListener('toggle',()=>{ if(!g.open) g.dataset.closed='1'; }); }\n  host.addEventListener('focusout',e=>{ if(e.target.dataset&&e.target.dataset.f) setHL(host,null); });\n  host.addEventListener('mouseover',e=>{\n    const fld=e.target.closest&&e.target.closest('.field'), inp=fld&&fld.querySelector('[data-f]');\n    const ae=document.activeElement, focused=(host.contains(ae)&&ae.dataset&&ae.dataset.f)||null;\n    const want=inp?inp.dataset.f:focused;\n    if(want!==host._hl) setHL(host,want);\n  });\n  \/\/ segmented hub type\n  host.querySelectorAll('[data-seg=\"hubType\"] button').forEach(b=>{\n    b.addEventListener('click',()=>{ markStale(host); applyHubType(host,b.dataset.v); refreshChoices(host,true); });\n  });\n  host.querySelectorAll('[data-seg=\"hubMode\"] button').forEach(b=>{\n    b.addEventListener('click',()=>{\n      const to=b.dataset.v; if(to===hubMode(host)) return;\n      if(to==='spec'){\n        const hf=hubFC(host);\n        [['fcL',hf.fcL],['fcR',hf.fcR]].forEach(([f,v])=>{ const el=host.querySelector(`[data-f=\"${f}\"]`); if(el && el.value.trim()==='' && v>0) el.value=String(Math.round(v*100)\/100); });\n      }\n      markStale(host); applyHubMode(host,to); drawHubMeasure(host); scheduleLive(host);\n    });\n  });\n  host.querySelectorAll('[data-seg=\"rimMode\"] button').forEach(b=>{\n    b.addEventListener('click',()=>{\n      const to=b.dataset.v; if(to===rimMode(host)) return;\n      if(to==='spec'){ const re=rimERD(host), el=host.querySelector('[data-f=\"erd\"]'); if(el && el.value.trim()==='' && re.nsd>0){ el.value=String(Math.round(re.nsd*100)\/100); setErdRef(host,'seat'); } }\n      markStale(host); applyRimMode(host,to); drawRimMeasure(host); scheduleLive(host);\n    });\n  });\n  host.querySelectorAll('[data-seg=\"osbSide\"] button').forEach(b=>b.addEventListener('click',()=>{\n    if(b.getAttribute('aria-pressed')==='true') return;\n    setOsbSide(host,b.dataset.v); if(num(host,'osb')) markStale(host); drawOsbGuide(host); scheduleLive(host);\n  }));\n  host.querySelectorAll('[data-hw]').forEach(b=>b.addEventListener('click',()=>{\n    const el=host.querySelector('[data-f=\"hubWidth\"]'); if(!el) return;\n    el.value=b.dataset.hw; el.dispatchEvent(new Event('input',{bubbles:true})); drawHubMeasure(host);\n  }));\n  \/\/ segmented spoke type (Standard \/ TCS)\n  host.querySelectorAll('[data-seg=\"spokeType\"] button').forEach(b=>{\n    b.addEventListener('click',()=>{ markStale(host); applySpokeType(host,b.dataset.v); });\n  });\n  host.querySelectorAll('[data-seg=\"crossMode\"] button').forEach(b=>{\n    b.addEventListener('click',()=>{ if(b.classList.contains('is-off')||b.getAttribute('aria-pressed')==='true') return; delete host.dataset.crossAuto; markStale(host); applyCrossMode(host,b.dataset.v,true); scheduleLive(host); });\n  });\n  \/\/ choice groups\n  host.querySelectorAll('.choices').forEach(grp=>{\n    grp.addEventListener('click',e=>{\n      const b=e.target.closest('button'); if(!b||b.classList.contains('is-off'))return;\n      grp.querySelectorAll('button').forEach(x=>x.setAttribute('aria-pressed','false'));\n      b.setAttribute('aria-pressed','true');\n      const g=grp.dataset.choice;\n      if(g==='count'||g==='ratio'||g==='cross'||g==='crossR'||g==='spLayout') refreshChoices(host,true);\n      markStale(host);\n      scheduleLive(host);\n    });\n  });\n  \/\/ any typed input redraws the preview\n  host.addEventListener('input',e=>{ if(e.target && e.target.matches('input[data-f],select[data-f]')){ if(!['bsd','rimWidth','rimH','accept'].includes(e.target.dataset.f)) markStale(host); if(e.target.dataset.f==='customDia') updateCustomHint(host);\n    if(e.target.dataset.f==='osb' && num(host,'osb')<0){ const v=-num(host,'osb'); e.target.value=String(v); setOsbSide(host, osbSide(host)==='DS'?'NDS':'DS'); toast(`Offset spoke bed: ${v} mm toward the ${osbSide(host)}. Pick the side with the buttons next to the field.`); }\n    scheduleLive(host); } });\n  \/\/ drilling toggle reveals the offset angle\n  const drill=host.querySelector('[data-choice=\"drill\"]');\n  if(drill) drill.addEventListener('click',e=>{ const b=e.target.closest('button'); if(!b||b.classList.contains('is-off'))return;\n    const show=b.dataset.v!=='even';\n    host.querySelector('.drill-angle').style.display = show?'':'none';\n    const g=host.querySelector('[data-drillguide]'); if(show && g && !g.dataset.closed) g.open=true;\n    drawDrillGuide(host);\n  });\n  \/\/ accept gates calculate\n  const acc = host.querySelector('[data-f=\"accept\"]');\n  const calcBtn = host.querySelector('[data-act=\"calc\"]');\n  acc.addEventListener('change',()=>calcBtn.disabled=!acc.checked);\n  \/\/ actions\n  calcBtn.addEventListener('click',()=>calculate(host,key));\n  host.querySelector('[data-act=\"clear\"]').addEventListener('click',()=>clearPanel(host,key));\n  host.querySelector('[data-act=\"print\"]').addEventListener('click',()=>{\n    if(gateActive() && !leadUnlocked){ openLeadModal(host); return; }\n    window.print();\n  });\n  host.querySelector('[data-act=\"share\"]').addEventListener('click',copyShare);\n  const cf=host.querySelector('[data-act=\"copyfront\"]'); if(cf) cf.addEventListener('click',copyFrontToRear);\n  \/\/ ERD reference and nipple type\n  host.querySelectorAll('[data-seg=\"erdRef\"] button').forEach(b=>b.addEventListener('click',()=>{\n    if(b.getAttribute('aria-pressed')==='true') return;\n    setErdRef(host,b.dataset.v); markStale(host); scheduleLive(host);\n  }));\n  const nt=host.querySelector('[data-choice=\"nipType\"]');\n  if(nt) nt.addEventListener('click',e=>{ if(e.target.closest('button')) refreshNipple(host); });\n}\n\nfunction val(host,f){ const el=host.querySelector(`[data-f=\"${f}\"]`); return el?el.value.trim():''; }\nfunction num(host,f){\n  let v=val(host,f); if(v==='') return NaN;\n  v=v.replace(\/[\\u2212\\u2013]\/g,'-').replace(\/\\s+\/g,'');\n  if(v.includes('.')&&v.includes(',')){\n    v = v.lastIndexOf(',')>v.lastIndexOf('.') ? v.replace(\/\\.\/g,'').replace(',','.') : v.replace(\/,\/g,'');\n  } else v=v.replace(',','.');\n  const x=parseFloat(v);\n  return isFinite(x) ? x : NaN;\n}\nfunction pressed(host,sel){ const b=host.querySelector(`${sel} button[aria-pressed=\"true\"]`); return b?b.dataset.v:null; }\n\nfunction clearPanel(host,key){\n  delete LAST_CALC[host.id];\n  host.querySelectorAll('input[data-f]').forEach(i=>{ if(i.type==='checkbox')i.checked=false; else i.value=''; });\n  host.querySelector('[data-f=\"hole\"]').value='2.6';\n  host.querySelector('[data-f=\"osb\"]').value='0';\n  host.querySelector('[data-f=\"rimZig\"]').value='0';\n  host.querySelector('[data-f=\"pairSpacing\"]').value='';\n  host.querySelector('[data-f=\"grpAngle\"]').value='';\n  host.querySelector('[data-f=\"grpSpreadL\"]').value='';\n  host.querySelector('[data-f=\"grpSpreadR\"]').value='';\n  setChoice(host,'drill','even');\n  setChoice(host,'spLayout','paired');\n  applyHubMode(host,'measure');\n  applyRimMode(host,'spec');\n  setErdRef(host,'std');\n  setChoice(host,'nipType','round'); setChoice(host,'nipLen',''); refreshNipple(host); setChoice(host,'nipMat','brass');\n  setOsbSide(host, host.id==='panel-rear'?'NDS':'DS');\n  { const adv=host.querySelector('[data-adv]'); if(adv) adv.open=false; }\n  setChoice(host,'ratio','1:1');\n  applySpokeType(host,'std');\n  host.querySelector('.drill-angle').style.display='none';\n  host.querySelector('[data-f=\"tension\"]').value='110';\n  host.querySelector('[data-f=\"tenMax\"]').value='';\n  host.querySelector('[data-f=\"tenMin\"]').value='';\n  host.querySelector('[data-f=\"bsd\"]').value='622';\n  host.querySelector('[data-act=\"calc\"]').disabled=true;\n  host.querySelector('[data-results]').classList.remove('show');\n  host.querySelector('[data-err]').classList.remove('show');\n  host.querySelector('[data-banner]').classList.remove('show');\n  host.querySelector('[data-act=\"print\"]').style.display='none';\n  host.classList.remove('has-results');\n  host.classList.remove('stale');\n  setChoice(host,'count','32');\n  setChoice(host,'cross','3');\n  setChoice(host,'crossR','3');\n  setCrossModeUI(host,'same');\n  refreshChoices(host);\n  updateLive(host);\n}\n\n\/* ---------- calculate + render ---------- *\/\n\/* Leading\/trailing tensions. Hub and rim only have to balance as a whole: the sideways pull of the two\n   sides (\u03a3 T\u00b7sin\u03b3) and the wind-up torque about the axle (\u03a3 \u00b1T\u00b7R\u00b7sin(rim wrap)\u00b7cos\u03b3, leading +, trailing \u2212,\n   same handedness on both flanges). The rim is far too stiff to show a hole-to-hole difference, so a builder\n   who evens the tension per side ends at the least-squares solution: the smallest leading\/trailing\n   difference that still balances. With equal torque arms (anchor spread alone, symmetric paired holes) the\n   difference is zero; a group offset or paired holes on a dished wheel give a few kgf. *\/\nfunction groupTensions(nL,nR,La,Lb,Rd,Rc,R,tgt){\n  const d2r=Math.PI\/180, sn=g=>Math.sin(g.brace*d2r);\n  const arm=(g,dir)=>dir*Math.sign(g.theta||0)*R*Math.sin(g.rimWrap*d2r)*Math.cos(g.brace*d2r);\n  const hL=nL\/2, hR=nR\/2, sa=sn(La), sb=sn(Lb), sd=sn(Rd), sc=sn(Rc), ma=arm(La,1), mb=arm(Lb,-1), md=arm(Rd,1), mc=arm(Rc,-1);\n  \/\/ unknowns with T_DS = 1: NDS lead\/trail = TL \u00b1 p, DS lead\/trail = 1 \u00b1 q\n  const a1=hL*(sa+sb), b1=hL*(sa-sb), c1=-hR*(sd-sc), d1=hR*(sd+sc);      \/\/ sideways\n  const a2=hL*(ma+mb), b2=hL*(ma-mb), c2=hR*(md-mc), d2=-hR*(md+mc);      \/\/ wind-up\n  if(!(a1>0) || !(d1>0)) return null;\n  const ep=b2-a2*b1\/a1, eq=c2-a2*c1\/a1, f=d2-a2*d1\/a1, D=ep*ep\/hL+eq*eq\/hR;\n  const p=D>1e-12?f*(ep\/hL)\/D:0, q=D>1e-12?f*(eq\/hR)\/D:0, TL=(d1-b1*p-c1*q)\/a1;\n  const k=tgt\/Math.max(TL,1), Ta=k*(TL+p), Tb=k*(TL-p), Td=k*(1+q), Tc=k*(1-q);\n  return {tenL:k*TL, tenR:k, Ta, Tb, Td, Tc, splitL:Ta>0?Tb\/Ta:1, splitR:Td>0?Tc\/Td:1};\n}\nfunction calculate(host,key){\n  const hubType = pressed(host,'[data-seg=\"hubType\"]')===\"straight\"?\"straight\":\"classic\";\n  const count = parseInt(pressed(host,'[data-choice=\"count\"]'),10);\n  const ratio = pressed(host,'[data-choice=\"ratio\"]')||'1:1';\n  let nL, nR;\n  if(ratio==='1:2'){ nL=count\/3; nR=2*count\/3; }\n  else if(ratio==='2:1'){ nL=2*count\/3; nR=count\/3; }\n  else { nL=count\/2; nR=count\/2; }\n  const [crossL, crossR] = crossPair(host);\n  const triL = tripletDelta(count,ratio,'L'), triR = tripletDelta(count,ratio,'R');\n  const re=rimERD(host), erd=re.erd;\n  const hf=hubFC(host);\n  const flL=num(host,'flL'), flR=num(host,'flR'), fcL=hf.fcL, fcR=hf.fcR;\n  const hole = hubType===\"classic\"? (isNaN(num(host,'hole'))?2.6:num(host,'hole')) : 0;\n  const offL = num(host,'offL')||0, offR = num(host,'offR')||0;\n  const paired = hubType==='straight' && (pressed(host,'[data-choice=\"spLayout\"]')||'paired')!=='even';\n  const osb = osbSigned(host);               \/\/ + = bed offset toward NDS (left)\n  const rimZig = num(host,'rimZig')||0;      \/\/ alternating: each hole toward its served flange\n\n  \/\/ validation\n  const errs=[];\n  if(ratio!=='1:1' && count%3!==0) errs.push(`${ratio} lacing needs a spoke count divisible by 3 \u2014 try 21, 24, 27 or 36.`);\n  if(ratio==='1:1' && count%2!==0) errs.push(\"1:1 lacing needs an even spoke count.\");\n  if(re.mode==='measure'){\n    const st=n=>`<span class=\"step\">${n}<\/span>`;\n    if(!(re.di>0)) errs.push(`${st(7)} Internal rim \u00d8 is required: measure across the inside of the rim, at the spoke holes.`);\n    if(!(re.t>0)) errs.push(`${st(8)} Inner wall thickness is required: measure the spoke bed at a spoke hole (usually 1.5\u20134 mm).`);\n  } else if(!(re.input>0)) errs.push(\"Rim ERD is required and must be a positive number.\");\n  if(re.nip.type==='custom' && !(re.nip.add>=0)) errs.push(\"Custom nipple: enter where the spoke should end, measured from the nipple seat, in mm.\");\n  else if(re.nip.add>25) errs.push(\"Custom nipple: a spoke end more than 25 mm above the nipple seat looks wrong \u2014 check the value.\");\n  if(num(host,'washer')<0) errs.push(\"Washer thickness cannot be negative.\");\n  else if(re.nip.washer>3) errs.push(\"Washer thickness looks too large: nipple washers are usually 0.5\u20131.5 mm.\");\n  { const st=n=>`<span class=\"step\">${n}<\/span>`;\n    if(hf.mode==='measure'){\n      if(!(hf.hw>0)) errs.push(`${st(1)} Hub width is required: measure over the lock-nuts (front usually 100 or 110 mm, rear 130\u2013157 mm).`);\n      if(isNaN(hf.a)) errs.push(`${st(2)} NDS end \u2192 flange is required.`);\n      if(isNaN(hf.b)) errs.push(`${st(3)} DS flange \u2192 end is required.`);\n      if(hf.hw>0 && !isNaN(hf.a) && !(hf.fcL>0)) errs.push(`${st(2)} is more than half the hub width, so the NDS flange would sit past the wheel centre. Check ${st(1)} and ${st(2)}.`);\n      if(hf.hw>0 && !isNaN(hf.b) && !(hf.fcR>0)) errs.push(`${st(3)} is more than half the hub width, so the DS flange would sit past the wheel centre. Check ${st(1)} and ${st(3)}.`);\n      if(hf.a<0) errs.push(`${st(2)} can't be negative: it runs from the NDS lock-nut face inward to the middle of the flange.`);\n      if(hf.b<0) errs.push(`${st(3)} can't be negative: it runs from the middle of the DS flange outward to the lock-nut face.`);\n    } else {\n      if(isNaN(fcL)) errs.push(`${st(2)} NDS flange \u2192 centre is required (from the hub spec sheet).`);\n      else if(fcL<=0) errs.push(`${st(2)} NDS flange \u2192 centre must be more than 0 mm: it is the distance from the wheel centre to the middle of the flange.`);\n      if(isNaN(fcR)) errs.push(`${st(3)} DS flange \u2192 centre is required (from the hub spec sheet).`);\n      else if(fcR<=0) errs.push(`${st(3)} DS flange \u2192 centre must be more than 0 mm: it is the distance from the wheel centre to the middle of the flange.`);\n    }\n    if(isNaN(flL)||flL<=0) errs.push(`${st(4)} NDS flange \u00d8 is required.`);\n    if(isNaN(flR)||flR<=0) errs.push(`${st(5)} DS flange \u00d8 is required.`); }\n  if(erd && (erd < flL || erd < flR)) errs.push(\"ERD looks smaller than a flange \u2014 check your measurements.\");\n  if(hubType==='classic'){ const h=num(host,'hole');\n    if(!isNaN(h) && (h<=0 || h>5)) errs.push(`<span class=\"step\">6<\/span> Spoke hole \u00d8 ${h} mm looks wrong: flange holes are about 2.3\u20133.0 mm.`); }\n  { \/\/ the spoke must still lean in toward the rim after the bed offset, hole offset and anchor spread\n    const sprL=hubType==='straight'?(num(host,'grpSpreadL')||0)\/2:0, sprR=hubType==='straight'?(num(host,'grpSpreadR')||0)\/2:0;\n    const eL=fcL-osb-rimZig-Math.abs(sprL), eR=fcR+osb-rimZig-Math.abs(sprR);\n    if(fcL>0 && fcR>0 && (eL<2 || eR<2)) errs.push(`With the offset spoke bed <span class=\"step\">9<\/span>, rim hole offset <span class=\"step\">10<\/span>${(sprL||sprR)?' and anchor spread':''} the ${eL<2?'NDS':'DS'} spokes would end up ${(eL<2?eL:eR).toFixed(1)} mm from the flange plane, so they could not hold the rim sideways. Check these values and the side chosen next to <span class=\"step\">9<\/span>.`); }\n  { const [cl,cr]=crossPair(host);\n    if(cl>0 && nL%2) errs.push(`Crossed lacing needs an even number of spokes per side \u2014 the NDS side has ${nL}, so lace it radially.`);\n    if(cr>0 && nR%2) errs.push(`Crossed lacing needs an even number of spokes per side \u2014 the DS side has ${nR}, so lace it radially.`); }\n  const tgtChk=num(host,'tension');\n  if(val(host,'tension')!=='' && isNaN(tgtChk)) errs.push(\"Target tension must be a number in kgf.\");\n  if(!isNaN(tgtChk) && tgtChk<=0) errs.push(\"Target tension must be greater than 0 kgf.\");\n  const maxChk=num(host,'tenMax'), minChk=num(host,'tenMin');\n  if(!isNaN(maxChk)&&maxChk>0&&!isNaN(minChk)&&minChk>0&&minChk>maxChk) errs.push(\"Min spoke tension is above the rim maximum \u2014 check the tension window.\");\n  for(const wf of [['hubW','Hub weight'],['rimW','Rim weight'],['spokeW','Weight per spoke']]){ const wv=num(host,wf[0]); if(!isNaN(wv)&&wv<0) errs.push(wf[1]+\" cannot be negative.\"); }\n  if(hubType==='straight' && (nL%2!==0 || nR%2!==0) && ((num(host,'grpAngle')||0)!==0 || (num(host,'grpSpreadL')||0)!==0 || (num(host,'grpSpreadR')||0)!==0))\n    errs.push(\"Group offset \/ anchor spread splits each side into equal leading and trailing halves \u2014 use an even spoke count per side.\");\n  if(val(host,'model')==='custom'){\n    const cd=num(host,'customDia');\n    if(!(cd>0)) errs.push(\"Custom spoke: enter the mid-section \u00d8 in mm.\");\n    else if(cd>4) errs.push(\"Custom spoke \u00d8 looks too large \u2014 check the value (mid-section, in mm).\");\n  }\n  if((pressed(host,'[data-choice=\"drill\"]')||'even')==='zigzag'){\n    const s=num(host,'pairSpacing'), ev=(erd>0 && count>0) ? Math.PI*erd\/count : NaN;\n    if(!(s>0)) errs.push(`<span class=\"step\">11<\/span> Paired holes: enter the hole spacing within a pair (centre to centre along the rim, in mm), or choose Even.`);\n    else if(ev && s>=ev) errs.push(`<span class=\"step\">11<\/span> The pair spacing (${s} mm) must be smaller than the even hole spacing of this rim (${ev.toFixed(1)} mm), otherwise the holes are not paired.`);\n  }\n  const errBox=host.querySelector('[data-err]');\n  if(errs.length){\n    errBox.querySelector('ul').innerHTML=errs.map(e=>`<li>${e}<\/li>`).join('');\n    errBox.classList.add('show');\n    host.querySelector('[data-results]').classList.remove('show');\n    errBox.scrollIntoView({behavior:'smooth',block:'center'});\n    return;\n  }\n  errBox.classList.remove('show');\n\n  \/\/ OSB shifts the whole bed one way; alternating (zigzag) offset shifts each hole toward its served flange\n  const fcLeff = fcL - osb - rimZig;   \/\/ NDS (left)\n  const fcReff = fcR + osb - rimZig;   \/\/ DS (right)\n  const L = sideLength({flangeDia:flL,flangeToCenter:fcLeff,erd,count,cross:crossL,hole,hubType,paired,offset:offL,thetaOffset:triL,sideCount:nL});\n  const R = sideLength({flangeDia:flR,flangeToCenter:fcReff,erd,count,cross:crossR,hole,hubType,paired,offset:offR,thetaOffset:triR,sideCount:nR});\n  \/\/ the other way to lace a triplet's doubled side: the two spokes of each rim pair cross (X)\n  let tripletAlt=null;\n  if((triL&&crossL>0)||(triR&&crossR>0)){\n    const xs = triL ? sideLength({flangeDia:flL,flangeToCenter:fcLeff,erd,count,cross:crossL,hole,hubType,paired,offset:offL,thetaOffset:-triL,sideCount:nL})\n                    : sideLength({flangeDia:flR,flangeToCenter:fcReff,erd,count,cross:crossR,hole,hubType,paired,offset:offR,thetaOffset:-triR,sideCount:nR});\n    tripletAlt={side:triL?'NDS':'DS', n:triL?nL:nR, diff:(triL?L:R).L-xs.L, deltaDeg:(triL||triR)*180\/Math.PI};\n  }\n\n  \/\/ paired rim holes and\/or a straight-pull group offset: leading and trailing spokes get two lengths per side\n  const drill = pressed(host,'[data-choice=\"drill\"]') || 'even';\n  const pairS = drill==='zigzag' ? num(host,'pairSpacing') : NaN;\n  const pairA = pairAngle(count,ratio,erd,pairS);\n  const grpAngle = hubType==='straight' ? (num(host,'grpAngle')||0) : 0;\n  const gRad = grpAngle*Math.PI\/180;\n  const grpSpreadL = hubType==='straight' ? (num(host,'grpSpreadL')||0) : 0;\n  const grpSpreadR = hubType==='straight' ? (num(host,'grpSpreadR')||0) : 0;\n  const offMode = pairA ? 'paired' : ((grpAngle||grpSpreadL||grpSpreadR)?'group':'even');\n  const sHL = grpSpreadL\/2, sHR = grpSpreadR\/2;\n  const anyX = crossL>0||crossR>0;\n  \/\/ the NDS holes move one way and the DS holes the other, so the split mirrors between the sides\n  const aL = anyX ? gRad+pairA : 0, aR = anyX ? gRad-pairA : 0;\n  const angOn = Math.abs(aL)>1e-9 || Math.abs(aR)>1e-9;\n  const sprOnL = Math.abs(sHL)>1e-9, sprOnR = Math.abs(sHR)>1e-9;\n  const offsetOn = angOn || sprOnL || sprOnR;\n  let Llead=null,Ltrail=null,Rlead=null,Rtrail=null,splitL=null,splitR=null;\n  if(offsetOn){\n    \/\/ positive spread = leading group outboard (larger flange-to-centre), per flange\n    Llead =sideLength({flangeDia:flL,flangeToCenter:fcLeff+sHL,erd,count,cross:crossL,hole,hubType,paired,offset:offL,thetaOffset:+aL+triL,sideCount:nL});\n    Ltrail=sideLength({flangeDia:flL,flangeToCenter:fcLeff-sHL,erd,count,cross:crossL,hole,hubType,paired,offset:offL,thetaOffset:-aL+triL,sideCount:nL});\n    Rlead =sideLength({flangeDia:flR,flangeToCenter:fcReff+sHR,erd,count,cross:crossR,hole,hubType,paired,offset:offR,thetaOffset:+aR+triR,sideCount:nR});\n    Rtrail=sideLength({flangeDia:flR,flangeToCenter:fcReff-sHR,erd,count,cross:crossR,hole,hubType,paired,offset:offR,thetaOffset:-aR+triR,sideCount:nR});\n    \/\/ split = T_trail\/T_lead per side, from the balance of the whole wheel (groupTensions, below)\n  }\n  const effL = offsetOn ? (Llead.L+Ltrail.L)\/2 : L.L;   \/\/ representative length for weight\/stretch\n  const effR = offsetOn ? (Rlead.L+Rtrail.L)\/2 : R.L;\n\n  \/\/ tension balance: T*sin(brace) equal both sides. Higher-tension side = smaller brace.\n  const tcs = pressed(host,'[data-seg=\"spokeType\"]')==='tcs';\n  const tgt = isNaN(num(host,'tension'))?110:num(host,'tension');  \/\/ kgf on higher side\n  const sL=Math.sin(L.brace*Math.PI\/180), sR=Math.sin(R.brace*Math.PI\/180);\n  \/\/ count-weighted lateral equilibrium: nL\u00b7T_L\u00b7sin(\u03b3L) = nR\u00b7T_R\u00b7sin(\u03b3R)\n  const wL=nL*sL, wR=nR*sR;\n  let tenL, tenR;\n  if(wL<=0||wR<=0){ tenL=tenR=tgt; }\n  else if(wL<=wR){ tenL=tgt; tenR=tgt*wL\/wR; }   \/\/ L higher\n  else { tenR=tgt; tenL=tgt*wR\/wL; }             \/\/ R higher\n  if(offsetOn){ const gt=groupTensions(nL,nR,Llead,Ltrail,Rlead,Rtrail,erd\/2,tgt);\n    if(gt){ tenL=gt.tenL; tenR=gt.tenR; splitL=gt.splitL; splitR=gt.splitR; } else { splitL=splitR=1; } }\n  const maxTen=Math.max(tenL,tenR);\n\n  \/\/ tension window check + per-group expected tensions\n  const tenMax = num(host,'tenMax'); const hasMax=!isNaN(tenMax)&&tenMax>0;\n  const tenMinIn = num(host,'tenMin'); const tenMin = (!isNaN(tenMinIn)&&tenMinIn>0)?tenMinIn:0;   \/\/ checked only when entered\n  \/\/ groups: lead\/trail per side when offsetOn, else one group per side.\n  \/\/ Within a side, average of lead+trail equals the side tension; split = T_trail\/T_lead.\n  function grpPair(tenSide,split,Ld,Lt,side,n){\n    const tLead=2*tenSide\/(1+(split||1)), tTrail=(split||1)*tLead;\n    return [{name:side+' leading',len:Ld,ten:tLead,n:n\/2},{name:side+' trailing',len:Lt,ten:tTrail,n:n\/2}];\n  }\n  const groups = offsetOn\n    ? [...grpPair(tenL,splitL,Llead,Ltrail,'NDS',nL), ...grpPair(tenR,splitR,Rlead,Rtrail,'DS',nR)]\n    : [{name:'NDS',len:L,ten:tenL,n:nL},{name:'DS',len:R,ten:tenR,n:nR}];\n  const grpLo=groups.reduce((a,g)=>g.ten<a.ten?g:a), grpHi=groups.reduce((a,g)=>g.ten>a.ten?g:a);\n  const lowSide = Math.min(tenL,tenR);\n  const fmt=x=>Math.round(x*10)\/10;\n  const warns=[];\n  if(hasMax && tgt>tenMax) warns.push(`Target ${fmt(tgt)} kgf exceeds the rim maximum of ${fmt(tenMax)} kgf \u2014 lower the target or confirm with the rim manufacturer.`);\n  else if(hasMax && offsetOn && grpHi.ten>tenMax) warns.push(`The ${grpHi.name} spokes (${SCODE[grpHi.name]}) run \u2248 ${Math.round(grpHi.ten)} kgf, above the rim maximum of ${fmt(tenMax)} kgf, although the side average is on target. Lower the target to about ${Math.floor(tgt*tenMax\/grpHi.ten)} kgf.`);\n  if(offsetOn && lowSide>=tenMin && grpLo.ten<tenMin) warns.push(`The ${grpLo.name} spokes (${SCODE[grpLo.name]}) run \u2248 ${Math.round(grpLo.ten)} kgf, below the ${fmt(tenMin)} kgf minimum, although the side average is above it \u2014 nipples in that group may loosen in service.`);\n  if(lowSide < tenMin) warns.push(`Predicted ${tenL<=tenR?'NDS':'DS'} tension \u2248 ${Math.round(lowSide)} kgf falls below the ${fmt(tenMin)} kgf minimum \u2014 nipples may loosen in service. Consider a higher target${hasMax?' (within the rim max)':''}, an asymmetric (OSB) rim, or different hub geometry.`);\n\n  \/\/ weight\n  const modelVal = val(host,'model');\n  const isCustom = modelVal==='custom';\n  const modelIdx = parseInt(modelVal,10);\n  const gmm = isCustom ? customGmm(host) : (SPOKE_MODELS[modelIdx]||[,0.022])[1];\n  const manualSpoke = num(host,'spokeW');\n  const perL = !isNaN(manualSpoke)&&manualSpoke>0 ? manualSpoke : gmm*effL;\n  const perR = !isNaN(manualSpoke)&&manualSpoke>0 ? manualSpoke : gmm*effR;\n  const spokeWt = perL*nL + perR*nR;\n  const hubW = num(host,'hubW')||0, rimW = num(host,'rimW')||0;\n  const nipWt = count*re.nip.gEach;\n  const totalWt = spokeWt + nipWt + hubW + rimW;\n\n  \/\/ stretch  dL = F*Lspoke\/(A*E),  A = gmm\/density\n  const area = gmm\/STEEL_GMM3;\n  const stretchL = (tenL*KGF)*effL\/(area*E_STEEL);\n  const stretchR = (tenR*KGF)*effR\/(area*E_STEEL);\n\n  \/\/ spoke past tangent at the flange: it would run back over the neighbouring spoke heads\n  const exitOf=(k,a,b,c)=>k>0?Math.max(...[a,b,c].filter(Boolean).map(x=>x.flangeExit)):0;\n  const exL=exitOf(crossL,L,Llead,Ltrail), exR=exitOf(crossR,R,Rlead,Rtrail);\n  const pastTan=[exL>90?['NDS',exL]:null, exR>90?['DS',exR]:null].filter(Boolean);\n  const crossWarn = pastTan.length>0;\n  \/\/ plausibility checks that do not block the calculation\n  const checks=[];\n  if(key==='rear' && fcR>fcL+1) checks.push(`The DS flange sits further from the centre (${fmt(fcR)} mm) than the NDS flange (${fmt(fcL)} mm). On a rear hub with a cassette it is normally the other way round, so check that <span class=\"step\">2<\/span> and <span class=\"step\">3<\/span> are not swapped. Single-speed and hub-gear hubs can be symmetric.`);\n  if(osb && ((osb>0 && fcL<fcR) || (osb<0 && fcR<fcL))) checks.push(`The offset spoke bed <span class=\"step\">9<\/span> points toward the ${osb>0?'NDS':'DS'}, the side whose flange is already closer to the centre, so it makes the tension difference larger. Check the side chosen next to <span class=\"step\">9<\/span>.`);\n  for(const [nm,fl] of [['NDS',flL],['DS',flR]]) if(fl<25 || fl>110) checks.push(`${nm} flange \u00d8 ${fmt(fl)} mm is unusual (hub flanges are about 30\u201370 mm, large-flange hubs up to about 100 mm) \u2014 check <span class=\"step\">${nm==='NDS'?4:5}<\/span>.`);\n  if(hubType==='classic' && (hole<2.2 || hole>3.2)) checks.push(`<span class=\"step\">6<\/span> Spoke hole \u00d8 ${fmt(hole)} mm is unusual: flange holes are about 2.3\u20133.0 mm.`);\n  for(const [nm,fc,stp] of [['NDS',fcL,2],['DS',fcR,3]]) if(fc<8 || fc>60) checks.push(`<span class=\"step\">${stp}<\/span> ${nm} flange to centre ${fmt(fc)} mm is unusual (hubs are about 15\u201345 mm, fat bikes up to about 60 mm) \u2014 check the value${hf.mode==='measure'?' and <span class=\"step\">1<\/span>':''}.`);\n  { const bsd=num(host,'bsd'); if(bsd>0 && erd>0){\n      if(erd>=bsd-4) checks.push(`ERD ${fmt(erd)} mm is (almost) as large as the bead seat of this wheel size (${bsd} mm) \u2014 check the wheel size and the ERD.`);\n      else if((bsd-erd)\/2+10>110) checks.push(`ERD ${fmt(erd)} mm is small for this wheel size (${bsd} mm): the rim would be about ${Math.round((bsd-erd)\/2+10)} mm deep. Check the wheel size and the ERD.`); } }\n  if(key==='rear' && ratio==='2:1') checks.push('2:1 puts the extra spokes on the NDS. Rear wheels normally use 1:2, with the extra spokes on the drive side.');\n  if(key==='front' && ratio==='1:2') checks.push('1:2 puts the extra spokes on the DS. Disc fronts normally use 2:1, with the extra spokes on the rotor side (NDS).');\n  if(key==='rear' && crossL===0 && crossR===0) checks.push('<b>Radial on both sides:<\/b> a rear wheel needs crossed spokes to pass the drive torque from the hub to the rim; radial spokes cannot. Lace at least the drive side 2- or 3-cross, and the NDS too when the wheel has a disc brake.');\n\n  render(host,{key,hubType,count,cross:crossL,crossR,tripletAlt,nL,nR,ratio,erd,osb,rimZig,L,R,tenL,tenR,maxTen,crossWarn,pastTan,checks,grpLo,grpHi,\n    drill:offMode,drillAngle:grpAngle,pairS:pairA?pairS:0,pairDeg:anyX?pairA*180\/Math.PI:0,offsetOn,Llead,Ltrail,Rlead,Rtrail,splitL,splitR,\n    tgt,tenMin,tenMax:hasMax?tenMax:null,warns,groups,tcs,grpSpreadL,grpSpreadR,spLayout:hubType==='straight'?(paired?'paired':'even'):null,offL,offR,\n    flL,flR,fcL,fcR,fcLeff,fcReff,hubWidth:hf.mode==='measure'?hf.hw:null,erdM:re.mode==='measure'?{di:re.di,t:re.t}:null,\n    hubName:val(host,'hubName'),rimName:val(host,'rimName'),\n    spokeWt,hubW,rimW,totalWt,perL,perR,stretchL,stretchR,nip:re.nip,nipWt,nsd:re.nsd,erdRef:re.mode==='spec'?re.ref:null,erdIn:re.mode==='spec'?re.input:null,\n    modelName:(isCustom ? (val(host,'customName')||('Custom \\u00d8 '+val(host,'customDia')+' mm')) : (SPOKE_MODELS[modelIdx]||[\"custom\"])[0]),manualSpoke:(!isNaN(manualSpoke)&&manualSpoke>0)});\n  host.classList.add('has-results');\n  host.classList.remove('stale');\n  host._calcLens={L:offsetOn?[Llead.L,Ltrail.L]:[L.L], R:offsetOn?[Rlead.L,Rtrail.L]:[R.L]};\n  refreshSpokeArt(host);\n  const hiL=tenL>=tenR*0.995, hiR=tenR>=tenL*0.995, oL=x=>hiL?Math.floor(x+1e-6):Math.round(x), oR=x=>hiR?Math.floor(x+1e-6):Math.round(x);\n  LAST_CALC[host.id]=`${key==='rear'?'Rear':'Front'} ${count}h${ratio!=='1:1'?' ('+nL+'\/'+nR+')':''} ${crossL===crossR?crossL+'x':'NDS '+crossL+'x \/ DS '+crossR+'x'} ${hubType==='straight'?(paired?'straight-pull':'straight-pull even anchors'):'J-bend'}${tcs?' TCS':''} | ERD ${erd} (${re.nip.label} nipple) fl ${flL}\/${flR} fc ${fcL}\/${fcR}${osb?' OSB '+Math.abs(osb)+(osb>0?' NDS':' DS'):''}${rimZig?' zig '+rimZig:''}${offMode==='paired'?' paired holes '+pairS:''}${re.nip.washer?' washer '+re.nip.washer:''} | lengths NDS ${offsetOn?oL(Llead.L)+'\/'+oL(Ltrail.L):oL(L.L)} \/ DS ${offsetOn?oR(Rlead.L)+'\/'+oR(Rtrail.L):oR(R.L)} mm (order) | target ${tgt} kgf (NDS ${Math.round(tenL)} \/ DS ${Math.round(tenR)})${warns.length?' | LOW-TENSION FLAG':''}`;\n  host.querySelector('[data-banner]').classList.remove('show');\n  host.querySelector('[data-act=\"print\"]').style.display='';\n}\n\nfunction radialNote(d){\n  const rL=d.cross===0, rR=d.crossR===0, rear=d.key==='rear';\n  const why='Radial spokes run straight out from the hub, so they cannot pass torque between hub and rim; only crossed spokes can.';\n  if(rL&&rR) return rear ? null\n    : `<b>Radial on both sides:<\/b> fine for a rim-brake front. ${why} With a disc brake the rotor pushes its braking torque into the hub on the NDS flange, so lace the NDS (rotor side) crossed, 2- or 3-cross as the lacing card allows; the DS may stay radial.`;\n  if(rL===rR) return null;\n  if(rear) return rR\n    ? `<b>Radial drive side:<\/b> ${why} The chain drives the hub on the DS, so with a radial DS all drive torque has to twist through the hub shell to the crossed NDS spokes, which many hubs are not made for. Better: lace the DS crossed (2- or 3-cross) and make the NDS the radial side. Keep a radial DS only on a hub approved for it.`\n    : `<b>Radial NDS:<\/b> a common rim-brake rear layout: the crossed drive side carries the drive torque and the radial NDS saves a little weight. ${why} With a disc brake the rotor sits on the NDS, so lace the NDS crossed as well (2- or 3-cross) to carry the braking torque.`;\n  return rL\n    ? `<b>Radial NDS:<\/b> fine for a rim-brake front. ${why} With a disc brake, swap the patterns: lace the NDS (rotor side) crossed, 2- or 3-cross, and keep the DS radial, because the braking torque enters the hub on the rotor side.`\n    : `<b>Radial DS:<\/b> the usual disc-front layout: the crossed NDS (rotor side) carries the braking torque, the radial DS keeps the wheel light. Also fine with rim brakes.`;\n}\nconst SCODE={'NDS leading':'Sa','NDS trailing':'Sb','DS leading':'Sd','DS trailing':'Sc','NDS':'Sa\\u00b7Sb','DS':'Sc\\u00b7Sd'};\nconst f1=x=>x.toFixed(1), f0=x=>Math.round(x), f2=x=>x.toFixed(2);\nconst tenPct=(t,m)=>m>0?Math.round(t\/m*100):100;\n\nfunction render(host,d){\n  const box=host.querySelector('[data-results]');\n  const wheelName=(d.key==='rear'?'Rear':'Front')+' wheel';\n  const dished = Math.abs(d.fcLeff-d.fcReff)>0.5, sp=d.hubType==='straight';\n  const today=new Date();\n  const ds=String(today.getDate()).padStart(2,'0')+'-'+String(today.getMonth()+1).padStart(2,'0')+'-'+today.getFullYear();\n  const pct=(a,b)=>Math.round(a\/b*100), mm=x=>f1(x)+' mm';\n  const win=(d.tenMin&&d.tenMax)?`${f0(d.tenMin)}\u2013${f0(d.tenMax)} kgf`:(d.tenMax?`up to ${f0(d.tenMax)} kgf`:(d.tenMin?`at least ${f0(d.tenMin)} kgf`:'none entered'));\n  const lo=d.offsetOn?{v:d.grpLo.ten,lbl:`lowest group (${SCODE[d.grpLo.name]})`}:{v:Math.min(d.tenL,d.tenR),lbl:'predicted low side'};\n  const hi=d.offsetOn?{v:d.grpHi.ten,lbl:`highest group (${SCODE[d.grpHi.name]})`}:{v:Math.max(d.tenL,d.tenR),lbl:'high side'};\n  const st=n=>`<span class=\"step\">${n}<\/span>`;\n  \/\/ the same drawings as the live preview, with the calculated tensions in their tooltips\n  const o={...readLive(host), idKey:d.key+'r'};\n  const T={ndsTip:`NDS spoke \u2014 \u03b3 ${f1(d.L.brace)}\u00b0 \u00b7 \u2248${f0(d.tenL)} kgf (${pct(d.tenL,d.maxTen)}%)`, dsTip:`DS spoke \u2014 \u03b3 ${f1(d.R.brace)}\u00b0 \u00b7 \u2248${f0(d.tenR)} kgf (${pct(d.tenR,d.maxTen)}%)`,\n    ndsFlAdv:`Bracing \u03b3 ${f1(d.L.brace)}\u00b0 on this side.`, dsFlAdv:`Bracing \u03b3 ${f1(d.R.brace)}\u00b0 on this side.`, gL:d.L.brace, gR:d.R.brace,\n    ...(d.offsetOn?{grpSplitL:Math.round((d.splitL||1)*100), grpSplitR:Math.round((d.splitR||1)*100), grpLbl:d.drill==='paired'?'Paired holes':'Group offset'}:{})};\n  const facts=[\n    `<b>${d.count}<\/b> spokes \u00b7 ${d.ratio}${d.ratio!=='1:1'?' ('+d.nL+' NDS \/ '+d.nR+' DS)':''} \u00b7 <b>${crossText(d.cross,d.crossR)}<\/b>`,\n    sp?`<b>Straight pull<\/b> \u00b7 ${d.spLayout==='even'?'evenly spaced':'paired'} anchors`:'<b>J-bend<\/b>',\n    `ERD <b>${f1(d.erd)} mm<\/b>${d.erdM?` \u00b7 seat \u00d8 ${f1(d.nsd)} measured`:(d.erdRef==='seat'?` \u00b7 seat \u00d8 ${f1(d.erdIn)} + nipple`:(d.erdRef==='mine'?' \u00b7 as measured':(Math.abs(d.erd-d.erdIn)>0.05?` \u00b7 spec ${f1(d.erdIn)} + nipple`:'')))}`,\n    `<b>${escape(d.nip.label)}<\/b> nipple \u00b7 ${d.nip.mat==='alu'?'alloy':'brass'}${d.nip.washer?' \u00b7 washer '+f1(d.nip.washer)+' mm':''}`,\n    d.osb?`offset bed <b>${osbText(d.osb)}<\/b>`:null,\n    d.rimZig?`hole offset <b>${f1(d.rimZig)} mm<\/b>`:null,\n    d.pairS?`paired holes <b>${f1(d.pairS)} mm<\/b>`:null,\n    (d.grpSpreadL||d.grpSpreadR)?`anchor spread <b>${f1(d.grpSpreadL||0)} \/ ${f1(d.grpSpreadR||0)} mm<\/b>`:null,\n    `<b>${escape(d.modelName.split(',')[0])}<\/b>${d.tcs?' \u00b7 TCS':''}`,\n    `target <b>${f0(d.tgt)} kgf<\/b>`\n  ].filter(Boolean);\n  const sideTag = d.key==='rear' ? {L:'non-drive side',R:'drive side'} : {L:'left side',R:'right side'};\n  \/\/ order length: the higher-tension side rounds down (a spoke that bottoms out in the nipple can't reach\n  \/\/ tension), the other side to the nearest mm; a balanced wheel counts both sides as high\n  const hiSide={L:d.tenL>=d.tenR*0.995, R:d.tenR>=d.tenL*0.995};\n  const ordL=(x,sd)=>hiSide[sd]?Math.floor(x+1e-6):Math.round(x);\n  const hero=(side,name,code,tag,n,len,lead,trail,ten,stretch)=>{ const sd=side==='l'?'L':'R'; return `\n    <div class=\"len ${side}\">\n      <div class=\"len-top\"><div class=\"side\">${name} spoke <span class=\"pcode\">${code}<\/span><\/div><div class=\"tag\">${tag} \u00b7 ${n} spokes<\/div><\/div>\n      ${d.offsetOn?`<div class=\"pair\"><div class=\"p\"><div class=\"pnum\">${ordL(lead.L,sd)}<small> mm<\/small><\/div><div class=\"plab\">leading<\/div><\/div><div class=\"p\"><div class=\"pnum\">${ordL(trail.L,sd)}<small> mm<\/small><\/div><div class=\"plab\">trailing<\/div><\/div><\/div>\n      <div class=\"exact\">exact ${f2(lead.L)} \/ ${f2(trail.L)} mm<\/div>`\n      :`<div class=\"val\">${ordL(len.L,sd)}<small> mm<\/small><\/div><div class=\"exact\">exact ${f2(len.L)} mm<\/div>`}\n      <div class=\"len-foot\"><span>order length, ${hiSide[sd]?'rounded down (higher-tension side)':'rounded to the nearest mm'}<\/span><span>expected \u2248 <b>${f0(ten)} kgf<\/b> \u00b7 ${pct(ten,d.maxTen)}%<\/span><span>stretch \u2248 ${f2(stretch)} mm<\/span><\/div>\n    <\/div>`; };\n  \/\/ the inputs behind the sheet, numbered like the form\n  const rl=rimLive(host,d.nsd>0?d.nsd:d.erd), sizeLbl=(WHEEL_SIZES.find(w=>w[0]===rl.bsd)||[0,''])[1];\n  const row=(n,k,v)=>`<div class=\"inp-row\">${n?st(n):'<span class=\"step none\"><\/span>'}<span class=\"k\">${k}<\/span><span class=\"v\">${v}<\/span><\/div>`;\n  const hubRows=(hubMode(host)==='measure' && d.hubWidth\n      ? row(1,'Hub width',mm(d.hubWidth))+row(2,'NDS end \u2192 flange',mm(num(host,'endL')))+row(3,'DS flange \u2192 end',mm(num(host,'endR')))+row('','Flange to centre, worked out',`${f1(d.fcL)} \/ ${f1(d.fcR)} mm`)\n      : row(2,'NDS flange \u2192 centre',mm(d.fcL))+row(3,'DS flange \u2192 centre',mm(d.fcR)))\n    + row(4,'NDS flange \u00d8',mm(d.flL))+row(5,'DS flange \u00d8',mm(d.flR))\n    + (sp ? row('','Anchor layout',d.spLayout==='even'?'evenly spaced':'paired')\n          + ((d.offL||d.offR)?row('','Spoke offset NDS \/ DS',`${f1(d.offL||0)} \/ ${f1(d.offR||0)} mm`):'')\n          + ((d.grpSpreadL||d.grpSpreadR)?row('','Anchor spread A2 \/ A3',`${f1(d.grpSpreadL||0)} \/ ${f1(d.grpSpreadR||0)} mm`):'')\n          + (d.drillAngle?row('','Spoke group offset',f1(d.drillAngle)+'\u00b0'):'')\n        : row(6,'Spoke hole \u00d8',mm(isNaN(num(host,'hole'))?2.6:num(host,'hole'))));\n  const rimRows=(d.erdM\n      ? row(7,'Internal rim \u00d8',mm(d.erdM.di))+row(8,'Inner wall thickness',mm(d.erdM.t))+row('','Nipple seat \u00d8, worked out',mm(d.nsd))\n      : row('','Rim ERD, spec sheet',mm(d.erdIn))+row('','ERD measured to',ERD_REF_TXT[d.erdRef]||ERD_REF_TXT.std))\n    + row('','Nipple',`${escape(d.nip.label)} \u00b7 ${d.nip.mat==='alu'?'alloy':'brass'}`)\n    + (d.nip.washer?row('','Washer under each nipple',mm(d.nip.washer)):'')\n    + row('','Spoke end above the nipple seat',mm(d.nip.total))\n    + row('','ERD used, worked out',mm(d.erd))\n    + row(9,'Offset spoke bed',d.osb?osbText(d.osb):'0 \u00b7 symmetric')\n    + row(10,'Rim hole offset left \/ right',d.rimZig?mm(d.rimZig):'0 \u00b7 holes in one line')\n    + row(11,'Hole spacing around the rim',d.pairS?'paired \u00b7 '+f1(d.pairS)+' mm within a pair':'even')\n    + row('','Wheel size',`${sizeLbl} \u00b7 BSD ${rl.bsd}`)\n    + row('','Rim width \u00d7 height',`${f0(rl.rimWidth)} \u00d7 ${f0(rl.rimDepth)} mm${rl.rimDepthSet?'':' (height estimated)'}`);\n  const laceRows=row('','Spokes',`${d.count} \u00b7 ${d.ratio}${d.ratio!=='1:1'?' \u00b7 '+d.nL+' NDS \/ '+d.nR+' DS':''}`)\n    + row('','Lacing',crossText(d.cross,d.crossR))\n    + row('','Hub type',sp?'Straight pull':'J-bend')\n    + row('','Spoke',`${escape(d.modelName)}${d.tcs?' \u00b7 TCS':''}`)\n    + row('','Target tension',`${f0(d.tgt)} kgf \u00b7 window ${win}`)\n    + ((d.hubW||d.rimW)?row('','Weights entered',`hub ${d.hubW?f0(d.hubW)+' g':'\u2014'} \u00b7 rim ${d.rimW?f0(d.rimW)+' g':'\u2014'}`):'');\n  const notes=[\n    `<b>Order lengths:<\/b> the higher-tension side is rounded down to the whole mm and the other side to the nearest mm, with the exact value beneath. On the higher-tension side a spoke that ends a little short is safer than one that bottoms out in the nipple before it reaches tension. If a length is not stocked, take the next shorter one on that side.`,\n    nipNote(d),\n    d.offsetOn?`<b>${d.drill==='paired'?'Paired holes':'Straight-pull group offset'}:<\/b> each side needs two spoke lengths (leading and trailing)${d.drill==='paired'?', one longer and one shorter in every pair of holes':''}. Which set is which depends on the hub's drilling or anchor direction, so confirm against one spoke before ordering in bulk.`:null,\n    d.tripletAlt?`<b>Triplet rim (${d.ratio}):<\/b> the ${d.tripletAlt.side} holes are drilled in pairs, so the ${d.tripletAlt.side} lengths use the real hole positions (${d.tripletAlt.deltaDeg.toFixed(1)}\u00b0 from even spacing), laced the standard way: the two spokes of each rim pair splay apart in a V. Laced so that they cross at the rim, the ${d.tripletAlt.side} spokes are ${f1(d.tripletAlt.diff)} mm shorter. Assumes the ${d.tripletAlt.side} flange holes sit centred on the rim pairs.`:null,\n    radialNote(d),\n    sp?`<b>Straight pull:<\/b> ${d.spLayout==='even'?'evenly spaced anchors, spoke angle \u03b8 = crosses \u00d7 360\u00b0 \/ spokes per side.':'paired anchors, so a crossed spoke lands half a hole further round than on a J-bend hub: \u03b8 = (crosses + \u00bd) \u00d7 360\u00b0 \/ spokes per side. A radial side is taken as evenly spaced anchors.'}${(d.offL||d.offR)?' The spoke offset is added to the length.':''} Verify against the hub manufacturer's specification before ordering.`:null,\n    `Expected tensions are static design values per group; the per-spoke spread after stabilisation is a machine\/QC tolerance, not predicted here.${d.offsetOn?' Leading and trailing values: hub and rim only have to balance as a whole (sideways and against wind-up), so with the tension evened out per side the groups differ only as much as that balance needs. An anchor spread alone gives no difference; a group offset or paired holes on a dished wheel give a few kgf.':''}`\n  ].filter(Boolean);\n\n  box.innerHTML = `\n  <div class=\"sheet-head\">\n    <div class=\"sh-l\"><div class=\"sh-kicker\">Wheel build sheet<\/div><h2>${wheelName}<\/h2><div class=\"sh-sub\">${d.hubName?escape(d.hubName):'Hub'} \u00b7 ${d.rimName?escape(d.rimName):'Rim'}<\/div><\/div>\n    <div class=\"sh-r\"><img decoding=\"async\" src=\"${LOGO_B}\" alt=\"Holland Mechanics\"><div class=\"sh-date\">${ds}<\/div><\/div>\n  <\/div>\n  <div class=\"sh-facts\">${facts.map(f=>`<span>${f}<\/span>`).join('')}<\/div>\n\n  <div class=\"len-grid\">\n    ${hero('l','NDS','Sa\u00b7Sb',sideTag.L,d.nL,d.L,d.Llead,d.Ltrail,d.tenL,d.stretchL)}\n    ${hero('r','DS','Sc\u00b7Sd',sideTag.R,d.nR,d.R,d.Rlead,d.Rtrail,d.tenR,d.stretchR)}\n  <\/div>\n  ${(d.checks.length||d.pastTan.length)?`<div class=\"chk-box\">${d.pastTan.length?`<p class=\"ten-warn\">\u26a0 <b>Spoke past tangent<\/b> at the ${d.pastTan.map(p=>p[0]+' flange (exit angle '+f1(p[1])+'\u00b0)').join(' and ')}: the spoke leaves the flange beyond tangent and runs over the heads of the neighbouring spokes. One cross less is usually the better build; check on the real hub before ordering.<\/p>`:''}${d.checks.map(c=>`<p class=\"ten-warn\">\u26a0 ${c}<\/p>`).join('')}<\/div>`:''}\n\n  <div class=\"subgrid\">\n    <div class=\"rcard span2 glance-card\">\n      <h3>Wheel at a glance <span class=\"spec-sub\">drawn from the entered geometry \u00b7 hover a part for its values<\/span><\/h3>\n      <div class=\"glance\">\n        <div class=\"lp-view\">${liveFront(o,T)}<div class=\"lp-cap\">${d.count}h \u00b7 ${crossText(d.cross,d.crossR)} <span class=\"lp-lg\"><i class=\"sa\"><\/i><i class=\"sb\"><\/i>NDS<i class=\"sd\"><\/i><i class=\"sc\"><\/i>DS<em>dark leading \u00b7 light trailing<\/em><\/span><\/div><\/div>\n        <div class=\"lp-view\">${liveSide(o,T)}<div class=\"lp-cap\">dish view \u00b7 \u03b3 ${f1(d.L.brace)}\u00b0 NDS \u00b7 ${f1(d.R.brace)}\u00b0 DS \u00b7 ${dished?'dished':'symmetric'}<\/div><\/div>\n      <\/div>\n    <\/div>\n\n    <div class=\"rcard span2 spec-card\">\n      <h3>Build specification <span class=\"spec-sub\">design targets for assembly &amp; QC \u00b7 spoke codes Sa\u2013Sd per the parameter drawing<\/span><\/h3>\n      <table class=\"spec-table\">\n        <thead><tr><th>Group<\/th><th>Spokes<\/th><th>Length (order)<\/th><th>Length (exact)<\/th><th>Expected tension<\/th><th>Share<\/th><\/tr><\/thead>\n        <tbody>\n        ${d.groups.map((g,i)=>`<tr>\n          <td>${({'NDS leading':['Sa'],'NDS trailing':['Sb'],'DS leading':['Sd'],'DS trailing':['Sc'],'NDS':['Sa','Sb'],'DS':['Sd','Sc']}[g.name]||[]).map(c=>`<i class=\"gsw\" style=\"background:${GC[c]}\"><\/i>`).join('')}<b>${i+1}<\/b> \u00b7 ${g.name}${SCODE[g.name]?' <span class=\"pcode\">'+SCODE[g.name]+'<\/span>':''}<\/td>\n          <td>${(g.n%1?g.n.toFixed(1):f0(g.n))}\u00d7<\/td>\n          <td><b>${ordL(g.len.L, g.name.indexOf('NDS')===0?'L':'R')} mm<\/b><\/td>\n          <td>${f2(g.len.L)} mm<\/td>\n          <td>${f0(g.ten)} kgf<\/td>\n          <td>${f0(g.ten\/d.maxTen*100)}%<\/td>\n        <\/tr>`).join('')}\n        <\/tbody>\n      <\/table>\n      <div class=\"spec-seq\">\n        <div class=\"spec-seq-h\">Lacing order <span class=\"spec-sub\">one repeat of rim holes, continuing the same way round the rim<\/span><\/div>\n        ${groupSeqSVG(d.count,d.ratio,d.cross,d.crossR)}\n      <\/div>\n      <p class=\"section-note\">Spoke type: <b>${d.tcs?'TCS (torsion-controlled, square grip section)':'standard'}<\/b>. ${d.tcs?'Grip the square section while turning the nipple: the spoke cannot wind up, so set tensions land accurately.':(sp?'<span class=\"sp-torsion\">Straight-pull spokes can rotate while tensioning: a TCS spoke or a holding tool is recommended.<\/span>':'Hold the spoke against wind-up while tensioning, or residual torsion will shed tension afterwards.')} Target ${f0(d.tgt)} kgf on the higher side \u00b7 window ${win}.<\/p>\n    <\/div>\n\n    <div class=\"rcard\">\n      <h3>Wheel geometry<\/h3>\n      <table class=\"geo\">\n        <tr><th class=\"lbl\">Parameter<\/th><th>NDS<\/th><th>DS<\/th><\/tr>\n        <tr><td class=\"lbl\">Flange \u00d8<\/td><td class=\"l\">${f1(d.flL)} mm<\/td><td class=\"r\">${f1(d.flR)} mm<\/td><\/tr>\n        <tr><td class=\"lbl\">Flange to centre${d.hubWidth?' (hub '+f1(d.hubWidth)+' mm)':''}<\/td><td class=\"l\">${f1(d.fcL)} mm<\/td><td class=\"r\">${f1(d.fcR)} mm<\/td><\/tr>\n        <tr><td class=\"lbl\">Spoke angle (\u03b8)<\/td><td class=\"l\">${f1(d.L.theta)}\u00b0<\/td><td class=\"r\">${f1(d.R.theta)}\u00b0<\/td><\/tr>\n        <tr><td class=\"lbl\">Bracing angle (\u03b3)<\/td><td class=\"l\">${f1(d.L.brace)}\u00b0<\/td><td class=\"r\">${f1(d.R.brace)}\u00b0<\/td><\/tr>\n        <tr><td class=\"lbl\">Rim wrap angle (\u03b1)<\/td><td class=\"l\">${f1(d.L.rimWrap)}\u00b0<\/td><td class=\"r\">${f1(d.R.rimWrap)}\u00b0<\/td><\/tr>\n        <tr><td class=\"lbl\">Flange exit angle (\u03b2)<\/td><td class=\"l\">${f1(d.L.flangeExit)}\u00b0<\/td><td class=\"r\">${f1(d.R.flangeExit)}\u00b0<\/td><\/tr>\n      <\/table>\n    <\/div>\n\n    <div class=\"rcard\">\n      <h3>Tension balance <span class=\"spec-sub\">${dished?'dished':'symmetric'}<\/span><\/h3>\n      <div class=\"bar-row\">\n        <div class=\"bar-top\"><span class=\"s\">NDS (${sideTag.L})<\/span><span class=\"v\">${f0(d.tenL)} kgf \u00b7 ${pct(d.tenL,d.maxTen)}%<\/span><\/div>\n        <div class=\"bar\"><i style=\"width:${pct(d.tenL,d.maxTen)}%\"><\/i><\/div>\n      <\/div>\n      <div class=\"bar-row\">\n        <div class=\"bar-top\"><span class=\"s\">DS (${sideTag.R})<\/span><span class=\"v\">${f0(d.tenR)} kgf \u00b7 ${pct(d.tenR,d.maxTen)}%<\/span><\/div>\n        <div class=\"bar r\"><i style=\"width:${pct(d.tenR,d.maxTen)}%\"><\/i><\/div>\n      <\/div>\n      <p class=\"note\" style=\"padding:0;margin:12px 0 0;font-size:11.5px\">${(()=>{ const hiL=d.tenL>=d.tenR, r=Math.round(Math.min(d.tenL,d.tenR)\/Math.max(d.tenL,d.tenR)*100);\n          return r>=95 ? 'Both sides reach about the same tension.'\n            : `The ${hiL?'NDS':'DS'} carries the higher tension: build it to the target, the ${hiL?'DS':'NDS'} follows at about ${r}%.${d.ratio!=='1:1'?` With ${d.ratio} lacing the side count matters too: the side with fewer spokes needs more tension per spoke to hold the rim centred.`:''}`; })()}${d.offsetOn?((Math.round((d.splitL||1)*100)===100&&Math.round((d.splitR||1)*100)===100)?' Leading and trailing spokes carry the same tension: the wheel balances without a difference between them.':` Leading\/trailing: trailing \u2248 ${Math.round((d.splitL||1)*100)}% of leading (NDS) \u00b7 ${Math.round((d.splitR||1)*100)}% (DS), the smallest difference that keeps the wheel balanced against wind-up.`):''}<\/p>\n      <p class=\"note\" style=\"padding:0;margin:8px 0 0;font-size:11.5px\">Allowed window: <b>${win}<\/b> \u00b7 ${lo.lbl} \u2248 <b>${f0(lo.v)} kgf<\/b> ${d.tenMin?(lo.v>=d.tenMin?'<span class=\"ok-chip\">OK<\/span>':'<span class=\"warn-chip\">LOW<\/span>'):''}${d.tenMax?(' \u00b7 '+hi.lbl+' '+f0(hi.v)+' kgf '+(hi.v<=d.tenMax?'<span class=\"ok-chip\">OK<\/span>':'<span class=\"warn-chip\">OVER<\/span>')):''}<\/p>\n      ${d.warns.map(w=>`<p class=\"ten-warn\">\u26a0 ${w}<\/p>`).join('')}\n    <\/div>\n\n    <div class=\"rcard\">\n      <h3>Estimated build weight<\/h3>\n      <div class=\"wt\">\n        <div class=\"wt-row\"><span class=\"lbl\"><b>Spokes<\/b> ${d.count} \u00d7 ${d.manualSpoke?'manual':escape(d.modelName.split(',')[0])}<\/span><span class=\"v\">${f0(d.spokeWt)} g<\/span><\/div>\n        <div class=\"wt-row\"><span class=\"lbl\"><b>Nipples<\/b> ${d.count} \u00d7 ${d.nip.mat==='alu'?'alloy':'brass'} ${d.nip.len} mm<\/span><span class=\"v\">${f0(d.nipWt)} g<\/span><\/div>\n        <div class=\"wt-row\"><span class=\"lbl\"><b>Hub<\/b><\/span><span class=\"v\">${d.hubW?f0(d.hubW)+' g':'\u2014'}<\/span><\/div>\n        <div class=\"wt-row\"><span class=\"lbl\"><b>Rim<\/b><\/span><span class=\"v\">${d.rimW?f0(d.rimW)+' g':'\u2014'}<\/span><\/div>\n        <div class=\"wt-row total\"><span>Total ${(!d.hubW||!d.rimW)?'(spokes + nipples'+(d.hubW?' + hub':'')+(d.rimW?' + rim':'')+')':''}<\/span><span class=\"v\">${f0(d.totalWt)} g<\/span><\/div>\n      <\/div>\n      <p class=\"note\" style=\"padding:0;margin:12px 0 0;font-size:11.5px\">Estimate. Per spoke: ${f1(d.perL)} g (NDS) \u00b7 ${f1(d.perR)} g (DS) \u00b7 per nipple \u2248 ${f2(d.nip.gEach)} g.<\/p>\n    <\/div>\n\n    <div class=\"rcard\">\n      <h3>Lacing per side <span class=\"spec-sub\">leading and trailing spokes<\/span><\/h3>\n      <div class=\"lacing-wrap\">\n        <div class=\"lace-col\"><div class=\"cap\">NDS \u00b7 ${d.nL} spokes \u00b7 ${crossLabel(d.cross)}<\/div>${laceSVG(d.nL,d.cross,'l',d.drill,d.pairDeg,(sp&&d.spLayout==='paired'&&d.cross>0)?0.5:0,tripletDelta(d.count,d.ratio,'L'),(sp&&(d.cross>0||d.crossR>0))?d.drillAngle:0)}<\/div>\n        <div class=\"lace-col\"><div class=\"cap\">DS \u00b7 ${d.nR} spokes \u00b7 ${crossLabel(d.crossR)}<\/div>${laceSVG(d.nR,d.crossR,'r',d.drill,d.pairDeg,(sp&&d.spLayout==='paired'&&d.crossR>0)?0.5:0,tripletDelta(d.count,d.ratio,'R'),(sp&&(d.cross>0||d.crossR>0))?d.drillAngle:0)}<\/div>\n      <\/div>\n      <div class=\"legend\">\n        <span><i style=\"background:${GC.Sa}\"><\/i>Sa \u00b7 NDS leading<\/span>\n        <span><i style=\"background:${GC.Sb}\"><\/i>Sb \u00b7 NDS trailing<\/span>\n        <span><i style=\"background:${GC.Sd}\"><\/i>Sd \u00b7 DS leading<\/span>\n        <span><i style=\"background:${GC.Sc}\"><\/i>Sc \u00b7 DS trailing<\/span>\n        <span><i style=\"background:#96ABC2\"><\/i>Rim \/ flange<\/span>\n      <\/div>\n    <\/div>\n\n    <div class=\"rcard span2 inputs-card\">\n      <h3>As entered <span class=\"spec-sub\">the inputs behind this sheet \u00b7 numbered as on the form<\/span><\/h3>\n      <div class=\"inp-grid\">\n        <div><div class=\"inp-h\">Hub<\/div>${hubRows}<\/div>\n        <div><div class=\"inp-h\">Rim<\/div>${rimRows}<\/div>\n        <div><div class=\"inp-h\">Lacing &amp; spokes<\/div>${laceRows}<\/div>\n      <\/div>\n    <\/div>\n\n    <div class=\"rcard span2 notes-card\">\n      <h3>Notes<\/h3>\n      <ul>${notes.map(n=>`<li>${n}<\/li>`).join('')}<\/ul>\n    <\/div>\n  <\/div>\n  `;\n  box.classList.add('show');\n  box.scrollIntoView({behavior:'smooth',block:'start'});\n}\n\nfunction escape(s){return String(s).replace(\/[&<>\"]\/g,c=>({'&':'&amp;','<':'&lt;','>':'&gt;','\"':'&quot;'}[c]));}\n\n\/* lacing diagram: one side, nSide spokes, k crosses, leading\/trailing alternate *\/\nfunction laceSVG(nSide,k,side,drill,dDeg,half,tri,gDeg){\n  const n=nSide, S=220, c=S\/2, rRim=92, rFl=34;\n  const dRad=drill==='paired' ? (dDeg||0)*Math.PI\/180 : 0;\n  half=half||0; tri=tri||0; const gRad=(gDeg||0)*Math.PI\/180;\n  let spokes='', rimHoles='', flHoles='';\n  const step=2*Math.PI\/n;\n  const dir = side==='l'?1:-1;\n  for(let j=0;j<n;j++){\n    const lead = (j%2===0);                             \/\/ alternate leading\/trailing\n    \/\/ paired straight-pull anchors: leading j and trailing j+1 share one anchor, half a hole back\n    const hubA=-Math.PI\/2 + (half ? j-j%2-0.5 : j)*step;\n    const aR=hubA + dir*(lead?1:-1)*((k+half)*step+tri) + dRad;   \/\/ triplet pairs splay by 90\/N; paired rim holes sit off even spacing\n    const aF=hubA - dir*gRad;                           \/\/ a straight-pull group offset turns the anchors\n    const fx=c+rFl*Math.cos(aF), fy=c+rFl*Math.sin(aF);\n    const rx=c+rRim*Math.cos(aR), ry=c+rRim*Math.sin(aR);\n    spokes+=`<line x1=\"${fx.toFixed(1)}\" y1=\"${fy.toFixed(1)}\" x2=\"${rx.toFixed(1)}\" y2=\"${ry.toFixed(1)}\" stroke=\"${k===0?(side==='l'?GC.Sa:GC.Sd):grpCol(side,lead?'leading':'trailing')}\" stroke-width=\"1.5\" stroke-opacity=\".92\"\/>`;\n    flHoles+=`<circle cx=\"${fx.toFixed(1)}\" cy=\"${fy.toFixed(1)}\" r=\"2\" fill=\"#fff\" stroke=\"#96ABC2\" stroke-width=\"1\"\/>`;\n    rimHoles+=`<circle cx=\"${rx.toFixed(1)}\" cy=\"${ry.toFixed(1)}\" r=\"2.2\" fill=\"#fff\" stroke=\"#647E9C\" stroke-width=\"1.2\"\/>`;\n  }\n  return `<svg viewBox=\"0 0 ${S} ${S}\" xmlns=\"http:\/\/www.w3.org\/2000\/svg\" role=\"img\" aria-label=\"${n} spokes ${crossLabel(k)} lacing\">\n    <circle cx=\"${c}\" cy=\"${c}\" r=\"${rRim}\" fill=\"none\" stroke=\"#C6D3DF\" stroke-width=\"6\"\/>\n    <circle cx=\"${c}\" cy=\"${c}\" r=\"${rFl}\" fill=\"#EDF2F6\" stroke=\"#C6D3DF\" stroke-width=\"2\"\/>\n    <circle cx=\"${c}\" cy=\"${c}\" r=\"3\" fill=\"#084490\"\/>\n    ${spokes}${rimHoles}${flHoles}\n  <\/svg>`;\n}\n\n\/* lateral cross-section: dish + offset to scale *\/\nfunction sectionSVG(d){\n  const W=320,H=210, R=d.erd\/2;\n  const rimY=20, axleY=H-26;\n  const radScale=(axleY-rimY)\/R;\n  const yOf=rho=>axleY-rho*radScale;\n  const g=d.rimZig||0;\n  const maxLat=Math.max(d.fcL,d.fcR,Math.abs(d.osb)+Math.abs(g))+12;\n  const latScale=(W\/2-26)\/maxLat;\n  const xOf=lat=>W\/2+lat*latScale;\n  const bedLat=-d.osb;                         \/\/ + osb => bed toward left\n  const seatL=bedLat-g, seatR=bedLat+g;        \/\/ alternating seats: each toward its flange\n  \/\/ points\n  const lf={x:xOf(-d.fcL),y:yOf(d.flL\/2)};     \/\/ left flange hole (raw position)\n  const rf={x:xOf(d.fcR),y:yOf(d.flR\/2)};      \/\/ right flange hole\n  const bedL={x:xOf(seatL),y:rimY};\n  const bedR={x:xOf(seatR),y:rimY};\n  const cx=xOf(0);\n  const fmt=p=>p.x.toFixed(1)+' '+p.y.toFixed(1);\n  const seatMarks = g!==0\n    ? `<circle cx=\"${bedL.x.toFixed(1)}\" cy=\"${rimY}\" r=\"3\" fill=\"#084490\"\/><circle cx=\"${bedR.x.toFixed(1)}\" cy=\"${rimY}\" r=\"3\" fill=\"#D9530A\"\/>`\n    : `<circle cx=\"${bedL.x.toFixed(1)}\" cy=\"${rimY}\" r=\"3\" fill=\"#E63329\"\/>`;\n  return `<svg class=\"section-svg\" viewBox=\"0 0 ${W} ${H}\" xmlns=\"http:\/\/www.w3.org\/2000\/svg\" role=\"img\" aria-label=\"wheel cross-section\">\n    <line x1=\"${cx}\" y1=\"${rimY-6}\" x2=\"${cx}\" y2=\"${axleY+6}\" stroke=\"#C6D3DF\" stroke-width=\"1\" stroke-dasharray=\"3 4\"\/>\n    <text x=\"${cx}\" y=\"${axleY+20}\" text-anchor=\"middle\" font-family=\"Inter,sans-serif\" font-size=\"9\" fill=\"#96ABC2\">centre<\/text>\n    <line x1=\"${xOf(-d.fcL)-2}\" y1=\"${axleY}\" x2=\"${xOf(d.fcR)+2}\" y2=\"${axleY}\" stroke=\"#084490\" stroke-width=\"4\" stroke-linecap=\"round\"\/>\n    <line x1=\"${xOf(-d.fcL)}\" y1=\"${axleY}\" x2=\"${lf.x}\" y2=\"${lf.y}\" stroke=\"#647E9C\" stroke-width=\"2\"\/>\n    <line x1=\"${xOf(d.fcR)}\" y1=\"${axleY}\" x2=\"${rf.x}\" y2=\"${rf.y}\" stroke=\"#647E9C\" stroke-width=\"2\"\/>\n    <rect x=\"${(xOf(bedLat)-26).toFixed(1)}\" y=\"${rimY-5}\" width=\"52\" height=\"7\" rx=\"2\" fill=\"#C6D3DF\"\/>\n    <line x1=\"${lf.x.toFixed(1)}\" y1=\"${lf.y.toFixed(1)}\" x2=\"${bedL.x.toFixed(1)}\" y2=\"${bedL.y.toFixed(1)}\" stroke=\"#084490\" stroke-width=\"2\"\/>\n    <line x1=\"${rf.x.toFixed(1)}\" y1=\"${rf.y.toFixed(1)}\" x2=\"${bedR.x.toFixed(1)}\" y2=\"${bedR.y.toFixed(1)}\" stroke=\"#D9530A\" stroke-width=\"2\"\/>\n    <circle cx=\"${lf.x.toFixed(1)}\" cy=\"${lf.y.toFixed(1)}\" r=\"3\" fill=\"#084490\"\/>\n    <circle cx=\"${rf.x.toFixed(1)}\" cy=\"${rf.y.toFixed(1)}\" r=\"3\" fill=\"#D9530A\"\/>\n    ${seatMarks}\n    <text x=\"${Math.max(16,Math.min(W-16,lf.x)).toFixed(1)}\" y=\"${axleY+20}\" text-anchor=\"middle\" font-family=\"Inter,sans-serif\" font-size=\"9\" fill=\"#084490\">NDS<\/text>\n    <text x=\"${Math.max(16,Math.min(W-16,rf.x)).toFixed(1)}\" y=\"${axleY+20}\" text-anchor=\"middle\" font-family=\"Inter,sans-serif\" font-size=\"9\" fill=\"#D9530A\">DS<\/text>\n  <\/svg>`;\n}\n\n\/* ---------- state, sharing, helpers ---------- *\/\nlet PF=null, PR=null;\nfunction setChoiceNear(host,group,v){\n  const btns=[...host.querySelectorAll(`[data-choice=\"${group}\"] button`)]; if(!btns.length) return false;\n  if(btns.some(b=>b.dataset.v===String(v))){ setChoice(host,group,v); return false; }\n  const t=parseFloat(v), nums=btns.map(b=>+b.dataset.v);\n  let pick=btns[0].dataset.v;\n  if(!isNaN(t) && nums.every(x=>!isNaN(x))) pick=String(nums.reduce((a,b)=>Math.abs(b-t)<Math.abs(a-t)?b:a));\n  setChoice(host,group,pick); return true;\n}\nfunction setChoice(host,group,v){\n  host.querySelectorAll(`[data-choice=\"${group}\"] button`).forEach(b=>b.setAttribute('aria-pressed', b.dataset.v===String(v)?'true':'false'));\n}\nfunction captureState(host){\n  const o={};\n  host.querySelectorAll('[data-f]').forEach(el=>{ if(el.dataset.f==='accept')return; o[el.dataset.f]=el.value; });\n  o.__hub=pressed(host,'[data-seg=\"hubType\"]');\n  o.__count=pressed(host,'[data-choice=\"count\"]');\n  o.__ratio=pressed(host,'[data-choice=\"ratio\"]');\n  o.__cross=pressed(host,'[data-choice=\"cross\"]');\n  o.__crossR=pressed(host,'[data-choice=\"crossR\"]');\n  o.__crossMode=crossMode(host);\n  o.__drill=pressed(host,'[data-choice=\"drill\"]');\n  o.__spLayout=pressed(host,'[data-choice=\"spLayout\"]');\n  o.__hubMode=hubMode(host);\n  o.__rimMode=rimMode(host);\n  o.__osbSide=osbSide(host);\n  o.__erdRef=erdRef(host);\n  o.__nipType=pressed(host,'[data-choice=\"nipType\"]')||'round';\n  o.__nipLen=pressed(host,'[data-choice=\"nipLen\"]');\n  o.__nipMat=pressed(host,'[data-choice=\"nipMat\"]')||'brass';\n  o.__tcs=pressed(host,'[data-seg=\"spokeType\"]')||'std';\n  return o;\n}\nfunction applyState(host,o){\n  if(!o)return;\n  Object.keys(o).forEach(k=>{ if(k.indexOf('__')===0)return; const el=host.querySelector(`[data-f=\"${k}\"]`); if(el&&el.type!=='checkbox') el.value=o[k]; });\n  if(o.__hub) applyHubType(host,o.__hub);\n  applyRimMode(host, o.__rimMode || 'spec');\n  setOsbSide(host, o.__osbSide || 'NDS');\n  setErdRef(host, o.__erdRef || 'std');\n  setChoice(host,'nipType',o.__nipType||'round'); refreshNipple(host);\n  if(o.__nipLen){ setChoice(host,'nipLen',o.__nipLen); refreshNipple(host); }\n  setChoice(host,'nipMat',o.__nipMat||'brass');\n  { const el=host.querySelector('[data-f=\"osb\"]'), v=num(host,'osb'); if(el && v<0){ el.value=String(-v); setOsbSide(host, osbSide(host)==='DS'?'NDS':'DS'); } }\n  applyHubMode(host, o.__hubMode || (String(o.hubWidth||'').trim() ? 'measure' : ((String(o.fcL||'').trim()||String(o.fcR||'').trim()) ? 'spec' : 'measure')));\n  if(o.__tcs) applySpokeType(host,o.__tcs);\n  if(o.grpSpread!==undefined && o.grpSpreadL===undefined && o.grpSpreadR===undefined){\n    const gs=host.querySelector('[data-f=\"grpSpreadL\"]'), gs2=host.querySelector('[data-f=\"grpSpreadR\"]');\n    if(gs) gs.value=o.grpSpread; if(gs2) gs2.value=o.grpSpread;\n  }\n  let adj=false;\n  if(o.__count) adj=setChoiceNear(host,'count',o.__count)||adj;\n  if(o.__ratio) adj=setChoiceNear(host,'ratio',o.__ratio)||adj;\n  if(o.__cross) adj=setChoiceNear(host,'cross',o.__cross)||adj;\n  adj=setChoiceNear(host,'crossR',o.__crossR||o.__cross||'3')||adj;\n  setCrossModeUI(host,o.__crossMode||'same');\n  if(o.__drill){ const dv=o.__drill==='offset'?'even':o.__drill; setChoice(host,'drill',dv); const da=host.querySelector('.drill-angle'); if(da) da.style.display=dv==='even'?'none':''; }\n  setChoice(host,'spLayout',o.__spLayout||'paired');\n  openAdvIfSet(host);\n  const snapped=refreshChoices(host)||adj;\n  toggleCustomSpoke(host);\n  updateLive(host);\n  return snapped;\n}\nfunction shareURL(){\n  const data={f:captureState(PF), r:captureState(PR), t:document.getElementById('tab-rear').getAttribute('aria-selected')==='true'?'r':'f'};\n  const enc=encodeURIComponent(JSON.stringify(data));\n  const base=location.href.split('#')[0];\n  const url=base+'#b='+enc;\n  try{ history.replaceState(null,'',url); }catch(e){}\n  return url;\n}\nasync function copyShare(){\n  const url=shareURL();\n  try{ await navigator.clipboard.writeText(url); toast('Share link copied \u2014 paste it to reopen this exact build'); }\n  catch(e){ window.prompt('Copy this link to reopen this build:',url); }\n}\nfunction copyFrontToRear(){\n  ['rimName','erd','rimInner','rimWall','osb','rimZig','rimW','bsd','rimWidth','rimH','model','customName','customDia','spokeW','tension','tenMax','tenMin','pairSpacing','washer','nipAdd'].forEach(f=>{\n    const a=PF.querySelector(`[data-f=\"${f}\"]`), b=PR.querySelector(`[data-f=\"${f}\"]`); if(a&&b) b.value=a.value;\n  });\n  applyRimMode(PR, rimMode(PF));\n  setErdRef(PR, erdRef(PF));\n  setChoice(PR,'nipType',pressed(PF,'[data-choice=\"nipType\"]')||'round'); refreshNipple(PR);\n  setChoice(PR,'nipLen',pressed(PF,'[data-choice=\"nipLen\"]')); refreshNipple(PR);\n  setChoice(PR,'nipMat',pressed(PF,'[data-choice=\"nipMat\"]')||'brass');\n  { const o=PR.querySelector('[data-f=\"osb\"]'); if(o) o.value=String(Math.abs(num(PF,'osb')||0)); setOsbSide(PR,'NDS'); }\n  setChoice(PR,'ratio',pressed(PF,'[data-choice=\"ratio\"]'));\n  setChoice(PR,'count',pressed(PF,'[data-choice=\"count\"]'));\n  setChoice(PR,'cross',pressed(PF,'[data-choice=\"cross\"]'));\n  setChoice(PR,'crossR',pressed(PF,'[data-choice=\"crossR\"]'));\n  setCrossModeUI(PR,crossMode(PF));\n  if(PF.dataset.crossAuto) PR.dataset.crossAuto='1'; else delete PR.dataset.crossAuto;\n  const mirrored = pressed(PF,'[data-choice=\"ratio\"]')==='2:1';\n  if(mirrored){ setChoice(PR,'ratio','1:2'); setChoice(PR,'cross',pressed(PF,'[data-choice=\"crossR\"]')); setChoice(PR,'crossR',pressed(PF,'[data-choice=\"cross\"]')); }\n  setChoice(PR,'drill',pressed(PF,'[data-choice=\"drill\"]')||'even');\n  const rda=PR.querySelector('.drill-angle');\n  if(rda) rda.style.display=(pressed(PR,'[data-choice=\"drill\"]')||'even')==='even'?'none':'';\n  applySpokeType(PR, pressed(PF,'[data-seg=\"spokeType\"]')||'std');\n  refreshChoices(PR);\n  toggleCustomSpoke(PR);\n  markStale(PR);\n  toast(mirrored ? 'Rim, lacing & spokes copied from front. The 2:1 lacing is mirrored to 1:2 for the rear: the extra spokes go to the drive side.' : 'Rim, lacing & spokes copied from front');\n}\nfunction toast(msg){\n  let t=document.getElementById('toast');\n  if(!t){ t=document.createElement('div'); t.id='toast'; HMSC_ROOT.appendChild(t); }\n  t.textContent=msg; t.classList.add('show');\n  clearTimeout(toast._t); toast._t=setTimeout(()=>t.classList.remove('show'),Math.max(2400,msg.length*55));\n}\n\n\/* ---------- example report & target advice template ---------- *\/\nconst EXAMPLE_CFG={\n  erd:'560', flL:'45', flR:'45', fcL:'35', fcR:'19', hubWidth:'142', endL:'36', endR:'52', hole:'2.6', __hubMode:'measure',\n  tension:'110', tenMax:'140', tenMin:'65',\n  __hub:'classic', __tcs:'std', __count:'24', __cross:'2', __ratio:'1:1', __drill:'even'\n};\nfunction buildExamplePanel(host){\n  const erd=560, flL=45, flR=45, fcL=35, fcR=19, hole=2.6, count=24, cross=2;\n  const tgt=110, tenMin=65, tenMax=140;\n  const mk=(fl,fc,n,toff)=>sideLength({flangeDia:fl,flangeToCenter:fc,erd,count,cross,hole,hubType:'classic',thetaOffset:toff||0,sideCount:n});\n  const bal=(L,R,nL,nR)=>{\n    const wL=nL*Math.sin(L.brace*Math.PI\/180), wR=nR*Math.sin(R.brace*Math.PI\/180);\n    const tenL=wL<=wR?tgt:tgt*wR\/wL, tenR=wL<=wR?tgt*wL\/wR:tgt;\n    return {tenL,tenR,pct:Math.round(Math.min(wL,wR)\/Math.max(wL,wR)*100)};\n  };\n  const L0=mk(flL,fcL,12), R0=mk(flR,fcR,12), b0=bal(L0,R0,12,12);\n  const Losb=mk(flL,fcL-2.5,12), Rosb=mk(flR,fcR+2.5,12), bOsb=bal(Losb,Rosb,12,12);\n  const b12=bal(mk(flL,fcL,8), mk(flR,fcR,16,tripletDelta(count,'1:2','R')), 8, 16);\n  const fr=x=>Math.round(x), f2x=x=>x.toFixed(2);\n  const lowOK=Math.min(b0.tenL,b0.tenR)>=tenMin;\n  host.innerHTML=`\n  <div class=\"rcard span2\" style=\"margin-bottom:16px\">\n    <h3>Example build report <span class=\"spec-sub\">design targets &rarr; build spec &rarr; production QC<\/span><\/h3>\n    <p class=\"note\" style=\"padding:0;margin:0\">This is what a complete wheel design hand-off looks like: the targets you set here are the numbers a production line builds to and a manufacturer report verifies per wheel. All figures below are computed live with the same formulas as the calculator.<\/p>\n  <\/div>\n  <div class=\"subgrid\">\n    <div class=\"rcard\">\n      <h3>Example wheel<\/h3>\n      <table class=\"geo\">\n        <tr><td class=\"lbl\">Wheel<\/td><td>Rear &middot; J-bend &middot; 24h &middot; 2-cross &middot; 1:1 (12\/12)<\/td><\/tr>\n        <tr><td class=\"lbl\">Rim<\/td><td>ERD ${erd} mm &middot; symmetric bed<\/td><\/tr>\n        <tr><td class=\"lbl\">Hub<\/td><td>flange &Oslash; ${flL}\/${flR} &middot; NDS ${fcL} mm &middot; DS ${fcR} mm to centre<\/td><\/tr>\n        <tr><td class=\"lbl\">Spoke<\/td><td>standard J-bend &middot; hole &Oslash; ${hole} mm<\/td><\/tr>\n        <tr><td class=\"lbl\">Targets<\/td><td>${tgt} kgf high side &middot; window ${tenMin}&ndash;${tenMax} kgf<\/td><\/tr>\n      <\/table>\n    <\/div>\n    <div class=\"rcard\">\n      <h3>Tension targets<\/h3>\n      <div class=\"bar-row\"><div class=\"bar-top\"><span class=\"s\">NDS<\/span><span class=\"v\">${fr(b0.tenL)} kgf<\/span><\/div>\n        <div class=\"bar\"><i style=\"width:${Math.round(b0.tenL\/tgt*100)}%\"><\/i><\/div><\/div>\n      <div class=\"bar-row\"><div class=\"bar-top\"><span class=\"s\">DS<\/span><span class=\"v\">${fr(b0.tenR)} kgf<\/span><\/div>\n        <div class=\"bar r\"><i style=\"width:${Math.round(b0.tenR\/tgt*100)}%\"><\/i><\/div><\/div>\n      <p class=\"note\" style=\"padding:0;margin:12px 0 0;font-size:11.5px\">Balance ${b0.pct}% &middot; low side ${fr(Math.min(b0.tenL,b0.tenR))} kgf ${lowOK?'<span class=\"ok-chip\">OK<\/span>':'<span class=\"warn-chip\">LOW<\/span>'} (min ${tenMin})<\/p>\n      ${lowOK?'':`<p class=\"ten-warn\">&#9888; The NDS sits below the ${tenMin} kgf minimum &mdash; exactly the situation the advice template below resolves.<\/p>`}\n    <\/div>\n    <div class=\"rcard span2 spec-card\">\n      <h3>Build specification <span class=\"spec-sub\">example<\/span><\/h3>\n      <table class=\"spec-table\">\n        <thead><tr><th>Group<\/th><th>Spokes<\/th><th>Length (order)<\/th><th>Length (exact)<\/th><th>Expected tension<\/th><th>Share<\/th><\/tr><\/thead>\n        <tbody>\n          <tr><td><b>1<\/b> &middot; NDS <span class=\"pcode\">Sa&middot;Sb<\/span><\/td><td>12&times;<\/td><td><b>${b0.tenL>=b0.tenR*0.995?Math.floor(L0.L+1e-6):fr(L0.L)} mm<\/b><\/td><td>${f2x(L0.L)} mm<\/td><td>${fr(b0.tenL)} kgf<\/td><td>${fr(b0.tenL\/tgt*100)}%<\/td><\/tr>\n          <tr><td><b>2<\/b> &middot; DS <span class=\"pcode\">Sc&middot;Sd<\/span><\/td><td>12&times;<\/td><td><b>${b0.tenR>=b0.tenL*0.995?Math.floor(R0.L+1e-6):fr(R0.L)} mm<\/b><\/td><td>${f2x(R0.L)} mm<\/td><td>${fr(b0.tenR)} kgf<\/td><td>${fr(b0.tenR\/tgt*100)}%<\/td><\/tr>\n        <\/tbody>\n      <\/table>\n      <div class=\"spec-seq\">\n        <div class=\"spec-seq-h\">Lacing order <span class=\"spec-sub\">one repeat of rim holes, continuing the same way round the rim<\/span><\/div>\n        ${groupSeqSVG(count,'1:1',cross,cross)}\n      <\/div>\n      <p class=\"section-note\">Bracing &gamma; ${L0.brace.toFixed(1)}&deg; NDS &middot; ${R0.brace.toFixed(1)}&deg; DS &middot; spoke angle &theta; ${L0.theta.toFixed(0)}&deg;. Expected tensions are static design values; per-spoke spread after stabilisation is a machine\/QC tolerance.<\/p>\n    <\/div>\n    <div class=\"rcard span2\">\n      <h3>Target advice template<\/h3>\n      <p class=\"note\" style=\"padding:0;margin:0 0 10px\">When the low side falls under the minimum, three levers fix it &mdash; each computed here with the same geometry:<\/p>\n      <table class=\"spec-table\">\n        <thead><tr><th>Advice<\/th><th>What changes<\/th><th>NDS tension<\/th><th>Balance<\/th><\/tr><\/thead>\n        <tbody>\n          <tr><td><b>A<\/b> &middot; Asymmetric (OSB) rim, 2.5 mm<\/td><td>bed shifts toward NDS<\/td><td>${fr(bOsb.tenL)} kgf<\/td><td>${bOsb.pct}%<\/td><\/tr>\n          <tr><td><b>B<\/b> &middot; Triplet lacing 1:2 (8\/16)<\/td><td>twice the spokes on DS<\/td><td>${fr(b12.tenL)} kgf<\/td><td>${b12.pct}%<\/td><\/tr>\n          <tr><td><b>C<\/b> &middot; Raise target within rim max<\/td><td>${tgt} &rarr; ${tenMax} kgf<\/td><td>${fr(Math.min(b0.tenL,b0.tenR)*tenMax\/tgt)} kgf<\/td><td>${b0.pct}%<\/td><\/tr>\n        <\/tbody>\n      <\/table>\n      <p class=\"section-note\">A and B improve the balance itself; C only lifts both sides within the rim limit. Combine A or B with a TCS spoke for accurate, torsion-free tension setting.<\/p>\n    <\/div>\n    <div class=\"rcard span2\">\n      <h3>Production QC template <span class=\"spec-sub\">what a manufacturer report verifies per wheel<\/span><\/h3>\n      <table class=\"geo\">\n        <tr><td class=\"lbl\">Lateral deviation<\/td><td>&lt; 0.25 mm<\/td><\/tr>\n        <tr><td class=\"lbl\">Height (radial) deviation<\/td><td>&lt; 0.3 mm<\/td><\/tr>\n        <tr><td class=\"lbl\">Out of centre (dish)<\/td><td>&lt; 0.3 mm<\/td><\/tr>\n        <tr><td class=\"lbl\">Per-spoke tension<\/td><td>all spokes inside the ${tenMin}&ndash;${tenMax} kgf window<\/td><\/tr>\n        <tr><td class=\"lbl\">Group averages<\/td><td>per-group average within tolerance of the expected tensions above<\/td><\/tr>\n        <tr><td class=\"lbl\">Stabilisation<\/td><td>pre-stressed before final truing, so tensions hold in service<\/td><\/tr>\n      <\/table>\n      <p class=\"section-note\">A production line records these per wheel, with per-spoke tensions and an ID. The design targets on this page are the input; the report is the proof.<\/p>\n    <\/div>\n    <div class=\"rcard span2\" style=\"text-align:center\">\n      <button type=\"button\" class=\"btn btn-primary\" data-act=\"load-example\">Load this example into the calculator &rarr;<\/button>\n      <p class=\"section-note\" style=\"margin-top:10px\">Fills the Rear wheel tab with these values so you can adjust and recalculate.<\/p>\n    <\/div>\n  <\/div>`;\n  host.querySelector('[data-act=\"load-example\"]').addEventListener('click',()=>{\n    clearPanel(PR,'rear');\n    applyState(PR, EXAMPLE_CFG);\n    document.getElementById('tab-rear').click();\n    PR.scrollIntoView({behavior:'smooth',block:'start'});\n  });\n}\n\n\n\/* ---------- lead capture modal ---------- *\/\nlet leadHost=null;\nfunction buildLeadModal(){\n  const w=document.createElement('div'); w.id='leadwrap';\n  w.innerHTML=`\n  <div class=\"ovl\" data-l=\"close\"><\/div>\n  <div class=\"leadcard\" role=\"dialog\" aria-modal=\"true\" aria-label=\"Download your build sheet\">\n    <button class=\"lclose\" data-l=\"close\" aria-label=\"Close\">&times;<\/button>\n    <div data-l=\"form\">\n      <h3>Download your build sheet<\/h3>\n      <p class=\"sub\">Leave your details and the printable build specification unlocks &mdash; including spoke lengths per group, tension targets and the QC window.<\/p>\n      <div class=\"lrow\">\n        <div class=\"lf\"><label>First name *<\/label><input data-l=\"firstname\" autocomplete=\"given-name\"><\/div>\n        <div class=\"lf\"><label>Last name *<\/label><input data-l=\"lastname\" autocomplete=\"family-name\"><\/div>\n        <div class=\"lf full\"><label>Business email *<\/label><input data-l=\"email\" type=\"email\" autocomplete=\"email\" inputmode=\"email\"><\/div>\n        <div class=\"lf full\"><label>Company<\/label><input data-l=\"company\" autocomplete=\"organization\"><\/div>\n        <div class=\"lf full\"><label>I am a&hellip;<\/label>\n          <select data-l=\"wheel_type\">\n            <option value=\"\">Select&hellip;<\/option>\n            <option>Wheel \/ bicycle manufacturer<\/option>\n            <option>Bike brand<\/option>\n            <option>Professional wheel builder<\/option>\n            <option>Distributor \/ agent<\/option>\n            <option>Hobby builder<\/option>\n            <option>Other<\/option>\n          <\/select>\n        <\/div>\n      <\/div>\n      <label class=\"consent\"><input type=\"checkbox\" data-l=\"consent\">\n        <span>Holland Mechanics may contact me about wheelbuilding solutions. See the <a href=\"${LEAD_GATE.privacyUrl}\" target=\"_blank\" rel=\"noopener\">privacy policy<\/a>. *<\/span><\/label>\n      <p class=\"lerr\" data-l=\"err\"><\/p>\n      <div class=\"lactions\">\n        <button class=\"btn btn-primary\" data-l=\"submit\">Unlock &amp; print &rarr;<\/button>\n      <\/div>\n    <\/div>\n    <div class=\"lok\" data-l=\"ok\">\n      <div class=\"big\">&#10003;<\/div>\n      <h3>Thank you!<\/h3>\n      <p class=\"sub\">Your build sheet is unlocked &mdash; the print dialog opens now. Save as PDF if you prefer a file.<\/p>\n    <\/div>\n  <\/div>`;\n  HMSC_ROOT.appendChild(w);\n  w.addEventListener('click',e=>{ if(e.target.closest('[data-l=\"close\"]')) w.classList.remove('show'); });\n  w.querySelector('[data-l=\"submit\"]').addEventListener('click',submitLead);\n  return w;\n}\nfunction openLeadModal(host){\n  leadHost=host;\n  const w=document.getElementById('leadwrap')||buildLeadModal();\n  w.querySelector('[data-l=\"form\"]').style.display='';\n  w.querySelector('[data-l=\"ok\"]').classList.remove('show');\n  w.classList.add('show');\n  setTimeout(()=>{ const f=w.querySelector('[data-l=\"firstname\"]'); if(f&&f.focus) f.focus(); },50);\n}\nfunction leadVal(w,k){ const el=w.querySelector(`[data-l=\"${k}\"]`); return el?(el.type==='checkbox'?el.checked:el.value.trim()):''; }\nasync function submitLead(){\n  const w=document.getElementById('leadwrap');\n  const err=w.querySelector('[data-l=\"err\"]');\n  const fn=leadVal(w,'firstname'), ln=leadVal(w,'lastname'), em=leadVal(w,'email'),\n        co=leadVal(w,'company'), wt=leadVal(w,'wheel_type'), ok=leadVal(w,'consent');\n  const problems=[];\n  if(!fn) problems.push('first name');\n  if(!ln) problems.push('last name');\n  if(!\/^[^@\\s]+@[^@\\s]+\\.[^@\\s]+$\/.test(em)) problems.push('valid email');\n  if(!ok) problems.push('consent');\n  if(problems.length){ err.textContent='Please provide: '+problems.join(', ')+'.'; err.classList.add('show'); return; }\n  err.classList.remove('show');\n  const btn=w.querySelector('[data-l=\"submit\"]'); btn.disabled=true; btn.textContent='Sending\\u2026';\n  \/\/ wheel context: last calculated config of the panel that triggered the gate (plus the other, if present)\n  const cfg=[LAST_CALC[leadHost?leadHost.id:''], ...Object.entries(LAST_CALC).filter(([k])=>!leadHost||k!==leadHost.id).map(([,v])=>v)]\n    .filter(Boolean).join('  ||  ').slice(0,950);\n  const fields=[\n    {name:'email',value:em},{name:'firstname',value:fn},{name:'lastname',value:ln},\n    {name:'company',value:co},{name:'wheel_type',value:wt},{name:'wheel_configuration',value:cfg}\n  ];\n  let hutk=null;\n  try{ const m=document.cookie.match(\/hubspotutk=([^;]+)\/); if(m) hutk=m[1]; }catch(e){}\n  const payload={fields,context:{pageUri:location.href,pageName:'HM Spoke Length & Wheel Configurator',...(hutk?{hutk}:{})}};\n  let sent=false;\n  try{\n    const r=await fetch(`https:\/\/api.hsforms.com\/submissions\/v3\/integration\/submit\/${LEAD_GATE.portalId}\/${LEAD_GATE.formGuid}`,{\n      method:'POST',headers:{'Content-Type':'application\/json'},body:JSON.stringify(payload)});\n    sent=r.ok;\n  }catch(e){ sent=false; }\n  btn.disabled=false; btn.textContent='Unlock & print \\u2192';\n  leadUnlocked=true;\n  try{ localStorage.setItem('hm_lead_unlocked','1'); }catch(e){}\n  if(!sent){\n    \/\/ do not block the user on a config\/network problem; unlock anyway\n    err.textContent='We could not reach our system, but your build sheet is unlocked anyway.';\n    err.classList.add('show');\n  }\n  w.querySelector('[data-l=\"form\"]').style.display='none';\n  w.querySelector('[data-l=\"ok\"]').classList.add('show');\n  setTimeout(()=>{ w.classList.remove('show'); window.print(); }, sent?900:1600);\n}\n\n\/* ---------- tab switching ---------- *\/\nconst tabs=[['tab-front','panel-front'],['tab-rear','panel-rear'],['tab-example','panel-example']];\ntabs.forEach(([t,p])=>{\n  document.getElementById(t).addEventListener('click',()=>{\n    tabs.forEach(([tt,pp])=>{\n      const sel=tt===t;\n      document.getElementById(tt).setAttribute('aria-selected',sel);\n      document.getElementById(pp).classList.toggle('active',sel);\n    });\n  });\n});\n\n\/* ---------- init ---------- *\/\nPF=document.getElementById('panel-front');\nPR=document.getElementById('panel-rear');\nbuildPanel('front', PF);\nbuildPanel('rear', PR);\nbuildExamplePanel(document.getElementById('panel-example'));\nwindow.addEventListener('hm3d-ready',()=>{ [PF,PR].forEach(p=>{ if(p) updateLive(p); }); });\nsetTimeout(()=>{ if(!window.HM3D) document.querySelectorAll('[data-w3msg]').forEach(el=>{ el.textContent='The 3D view could not load \u2014 it needs an internet connection for its 3D engine. Everything else works as normal.'; }); },15000);\ntry{\n  if(location.hash.indexOf('#b=')===0){\n    const d=JSON.parse(decodeURIComponent(location.hash.slice(3)));\n    const adj={f:!!applyState(PF,d.f), r:!!applyState(PR,d.r)};\n    \/\/ a wheel counts as loaded when the link carries any of its measurements\n    const MEAS=['erd','rimInner','rimWall','flL','flR','fcL','fcR','hubWidth','endL','endR'];\n    const has=o=>!!o && MEAS.some(k=>String(o[k]==null?'':o[k]).trim()!=='');\n    \/\/ open the tab the link was made on; older links: the rear tab when only the rear wheel is filled in\n    const open = (d.t==='r'||d.t==='f') ? d.t : (!has(d.f) && has(d.r) ? 'r' : 'f');\n    if(open==='r') document.getElementById('tab-rear').click();\n    [['f',PF],['r',PR]].forEach(([k,p])=>{ const b=p.querySelector('[data-banner]'); if(!b || !(has(d[k]) || k===open)) return;\n      if(adj[k]) b.textContent='Build loaded from a shared link \u2014 some options were adjusted to the nearest valid combination. Accept the disclaimer and press Calculate.';\n      b.classList.add('show'); });\n  }\n}catch(e){}\n\n})();\n<\/script>\n<script type=\"module\" data-no-optimize=\"1\" data-no-defer=\"1\" data-no-minify=\"1\" data-cfasync=\"false\" data-pagespeed-no-defer nowprocket>\nimport * as THREE from 'https:\/\/cdn.jsdelivr.net\/npm\/three@0.160.0\/+esm';\nimport { OrbitControls } from 'https:\/\/cdn.jsdelivr.net\/npm\/three@0.160.0\/examples\/jsm\/controls\/OrbitControls.js\/+esm';\nimport { RoomEnvironment } from 'https:\/\/cdn.jsdelivr.net\/npm\/three@0.160.0\/examples\/jsm\/environments\/RoomEnvironment.js\/+esm';\n\nconst HLC=0xD4237A;\nconst COL={L:0x084490,R:0xD9530A,Sa:0x084490,Sb:0x4F8FE6,Sd:0xD9530A,Sc:0xF5A23F,ok:0x119915,low:0xFA1414,high:0xE76C00};\nconst UP=new THREE.Vector3(0,1,0);\nconst UNIT=new THREE.CylinderGeometry(1,1,1,10,1,false).translate(0,0.5,0);   \/\/ grows along +Y from its base\nconst BALL=new THREE.SphereGeometry(1,12,8);\nconst VIEWS={iso:[0.62,0.3,0.72],front:[1,0.0001,0],side:[0,0.0001,1],hub:[0.7,0.34,0.62]};\nconst HLMAP={washer:['nipple'],nipAdd:['nipple'],rimInner:['rim','nipple'],rimWall:['rim','nipple'],hubWidth:['hub','flL','flR'],endL:['flL'],endR:['flR'],flL:['flL'],fcL:['flL'],offL:['flL'],grpSpreadL:['flL'],flR:['flR'],fcR:['flR'],offR:['flR'],grpSpreadR:['flR'],\n  hole:['flL','flR'],hubName:['hub','flL','flR'],hubW:['hub','flL','flR'],\n  erd:['rim','nipple'],osb:['rim','nipple'],rimZig:['nipple'],rimName:['rim'],rimW:['rim'],bsd:['rim'],rimWidth:['rim'],rimH:['rim'],pairSpacing:['nipple','spokes'],\n  grpAngle:['spokes'],model:['spokes'],customDia:['spokes'],customName:['spokes'],spokeW:['spokes'],tension:['spokes'],tenMin:['spokes'],tenMax:['spokes']};\nconst ORDER={Sa:0,'Sa\u00b7Sb':0,Sb:1,Sd:2,'Sc\u00b7Sd':2,Sc:3};\nconst ctxs=new Map();\n\nconst std=(color,metal,rough)=>new THREE.MeshStandardMaterial({color,metalness:metal,roughness:rough});\nconst P=(x,r,a)=>new THREE.Vector3(x,-r*Math.sin(a),r*Math.cos(a));\nfunction xCyl(r,x0,x1,mat,seg){\n  const g=new THREE.CylinderGeometry(r,r,Math.max(0.1,x1-x0),seg||40).rotateZ(-Math.PI\/2);\n  const m=new THREE.Mesh(g,mat); m.position.x=(x0+x1)\/2; return m;\n}\nfunction label(text,color){\n  const c=document.createElement('canvas'); c.width=128; c.height=64; const g=c.getContext('2d');\n  g.font='700 34px Inter, sans-serif'; g.fillStyle=color; g.textAlign='center'; g.textBaseline='middle'; g.fillText(text,64,34);\n  const t=new THREE.CanvasTexture(c); t.colorSpace=THREE.SRGBColorSpace;\n  const s=new THREE.Sprite(new THREE.SpriteMaterial({map:t,depthTest:false,transparent:true}));\n  s.scale.set(40,20,1); s.renderOrder=10; return s;\n}\n\nfunction rimSizes(d){\n  const R=d.M.R, H=Math.max(12,Math.min(110,d.o.rimDepth||22)), Wd=Math.max(14,Math.min(70,d.o.rimWidth||22));\n  const seat=R-(d.o.nipAdd!=null?d.o.nipAdd:2), inner=seat-4, top=inner+H, bead=top-5.5, tyreW=Math.max(25,Wd+6);\n  return {R,H,W:Wd,inner,top,bead,tyreW};\n}\n\/* rim section: box-like for shallow rims, U\/V-shaped for deep ones; nipple seats at R *\/\nfunction rimProfile(rs){\n  const {inner,top,bead,H}=rs, hw=rs.W\/2, deep=H>30;\n  const bw=hw*(deep?0.42:0.8), widen=inner+Math.min(H-7,Math.max(6,H*(deep?0.6:0.4)));\n  const half=[[inner,0],[inner+0.5,-bw*0.55],[inner+2,-bw]];\n  for(let i=1;i<=6;i++){ const f=i\/6; half.push([inner+2+(widen-inner-2)*f,-(bw+(hw-bw)*Math.sin(f*Math.PI\/2))]); }\n  half.push([top-1.2,-hw],[top,-hw+1.2],[top-0.6,-hw+2.6],[bead+0.4,-hw+3],[bead,-hw+3.4],[bead-2,-hw*0.45]);\n  return [...half,[bead-2.6,0],...half.slice(1).reverse().map(([r,a])=>[r,-a]),[inner,0]];\n}\n\/* ---- concept rim looks: procedural finishes and sidewall decals ---- *\/\nconst LOGO_IMG=new Image();\nLOGO_IMG.onload=()=>{ ctxs.forEach(c=>{ if(c.look.design==='logo'&&c.d) build(c); }); };\nif(typeof LOGO_B!=='undefined') LOGO_IMG.src=LOGO_B;\nconst FIN={};\nfunction finishTex(kind){\n  if(FIN[kind]) return FIN[kind];\n  const c=document.createElement('canvas'), g=c.getContext('2d');\n  if(kind==='3k'){\n    c.width=c.height=128; const q=32;\n    for(let y=0;y<4;y++) for(let x=0;x<4;x++){\n      const across=(x+y)%2===0, gr=across?g.createLinearGradient(0,y*q,0,y*q+q):g.createLinearGradient(x*q,0,x*q+q,0);\n      gr.addColorStop(0,'#121417'); gr.addColorStop(0.5,across?'#3B424B':'#2B3037'); gr.addColorStop(1,'#101215');\n      g.fillStyle=gr; g.fillRect(x*q,y*q,q,q);\n    }\n  } else {\n    c.width=256; c.height=64; g.fillStyle=kind==='ud'?'#191C21':'#303843'; g.fillRect(0,0,256,64);\n    for(let i=0;i<(kind==='ud'?220:320);i++){\n      g.strokeStyle=`rgba(255,255,255,${(Math.random()*(kind==='ud'?0.05:0.07)).toFixed(3)})`;\n      g.lineWidth=kind==='ud'?Math.random()*1.4+0.3:0.6; const y=Math.random()*64;\n      g.beginPath(); g.moveTo(0,y); g.lineTo(256,y+(kind==='ud'?Math.random()*2-1:0)); g.stroke();\n    }\n  }\n  const t=new THREE.CanvasTexture(c); t.colorSpace=THREE.SRGBColorSpace; t.wrapS=t.wrapT=THREE.RepeatWrapping;\n  return FIN[kind]=t;\n}\nfunction resample(pts,n){   \/\/ evenly spaced points along a polyline, so lathe UVs follow arc length\n  const seg=[]; let L=0;\n  for(let i=1;i<pts.length;i++){ const d=Math.hypot(pts[i][0]-pts[i-1][0],pts[i][1]-pts[i-1][1]); seg.push(d); L+=d; }\n  const out=[];\n  for(let k=0;k<n;k++){\n    let t=L*k\/(n-1), i=0; while(i<seg.length-1 && t>seg[i]){ t-=seg[i]; i++; }\n    const f=seg[i]?Math.min(1,t\/seg[i]):0;\n    out.push([pts[i][0]+(pts[i+1][0]-pts[i][0])*f, pts[i][1]+(pts[i+1][1]-pts[i][1])*f]);\n  }\n  return {pts:out,len:L};\n}\nfunction rimSidePts(rs){   \/\/ outer sidewall (NDS face), bottom corner to the top of the brake track\n  const {inner,top,H}=rs, hw=rs.W\/2, deep=H>30;\n  const bw=hw*(deep?0.42:0.8), widen=inner+Math.min(H-7,Math.max(6,H*(deep?0.6:0.4)));\n  const p=[[inner+2,-bw]];\n  for(let i=1;i<=6;i++){ const f=i\/6; p.push([inner+2+(widen-inner-2)*f,-(bw+(hw-bw)*Math.sin(f*Math.PI\/2))]); }\n  p.push([top-1.2,-hw]);\n  return p;\n}\nfunction applyFinish(ctx,rz,per){\n  const fin=ctx.look.finish==='auto'?(rz.H>=30?'ud':'alloy'):ctx.look.finish, m=ctx.mats.rim;\n  const tex=finishTex(fin).clone(); tex.needsUpdate=true;\n  const circ=2*Math.PI*rz.R;\n  if(fin==='3k') tex.repeat.set(circ\/8,per\/8); else if(fin==='ud') tex.repeat.set(circ\/90,per\/22); else tex.repeat.set(circ\/70,per\/18);\n  if(m.map) m.map.dispose();\n  m.map=tex; m.color.setHex(0xffffff);\n  m.metalness=fin==='alloy'?0.6:0.2; m.roughness=fin==='alloy'?0.36:0.42;\n  m.clearcoat=fin==='alloy'?0:0.9; m.clearcoatRoughness=0.1; m.needsUpdate=true;\n}\nfunction badgeCanvas(look,S){   \/\/ logo or text art; the canvas spans the full sidewall height S\n  const Hc=256, decH=(look.design==='logo'?0.74:0.56)*Hc, sw=decH*0.28;\n  let artW;\n  if(look.design==='logo') artW=decH*(LOGO_IMG.naturalWidth\/LOGO_IMG.naturalHeight);\n  else { const m=document.createElement('canvas').getContext('2d'); m.font='italic 800 100px Inter, sans-serif';\n    artW=sw*1.25+m.measureText((look.text||' ').toUpperCase()).width*decH*1.16\/100; }\n  const c=document.createElement('canvas'); c.width=Math.min(4096,Math.round(artW*1.12)); c.height=Hc;\n  const g=c.getContext('2d'), x0=(c.width-artW)\/2, y0=(Hc-decH)\/2;\n  if(look.design==='logo'){\n    g.drawImage(LOGO_IMG,x0,y0,artW,decH);\n    g.globalCompositeOperation='source-in'; g.fillStyle=look.color; g.fillRect(0,0,c.width,Hc);\n  } else {\n    g.fillStyle='#E63329'; g.beginPath();\n    g.moveTo(x0+sw*0.6,y0); g.lineTo(x0+sw,y0); g.lineTo(x0+sw*0.4,y0+decH); g.lineTo(x0,y0+decH); g.closePath(); g.fill();\n    g.font=`italic 800 ${Math.round(decH*1.16)}px Inter, sans-serif`; g.textBaseline='middle';\n    g.fillStyle=look.color; g.fillText((look.text||'').toUpperCase(),x0+sw*1.25,Hc\/2+decH*0.04);\n  }\n  return {canvas:c,wmm:S*c.width\/Hc};\n}\nfunction stripesCanvas(look,circ,S){\n  const Wc=4096, Hc=Math.max(48,Math.round(Wc*S\/circ)), c=document.createElement('canvas'); c.width=Wc; c.height=Hc;\n  const g=c.getContext('2d'); g.fillStyle=look.color;\n  g.fillRect(0,Hc*0.18,Wc,Math.max(2,Hc*0.05)); g.fillRect(0,Hc*0.77,Wc,Math.max(2,Hc*0.05));\n  const sets=8, per=Wc\/sets, bw=Hc*0.32, gap=Hc*0.16, sl=Hc*0.45;\n  for(let k=0;k<sets;k++) for(let b=0;b<3;b++){\n    const xx=k*per+per*0.4+b*(bw+gap); g.fillStyle=b===2?'#E63329':look.color; g.beginPath();\n    g.moveTo(xx+sl,Hc*0.18); g.lineTo(xx+sl+bw,Hc*0.18); g.lineTo(xx+bw,Hc*0.82); g.lineTo(xx,Hc*0.82); g.closePath(); g.fill();\n  }\n  return c;\n}\nfunction addDecals(ctx,rz){\n  const look=ctx.look; if(look.design==='none') return;\n  if(look.design==='logo' && !(LOGO_IMG.complete&&LOGO_IMG.naturalWidth)) return;\n  const side=resample(rimSidePts(rz),18), S=side.len, rMid=side.pts.reduce((s,p)=>s+p[0],0)\/side.pts.length;\n  const aniso=ctx.renderer.capabilities.getMaxAnisotropy();\n  const place=(canvas,phiStart,phiLen)=>{\n    for(const ds of [false,true]){\n      const t=new THREE.CanvasTexture(canvas); t.colorSpace=THREE.SRGBColorSpace; t.anisotropy=aniso;\n      if(ds){ t.wrapS=THREE.RepeatWrapping; t.repeat.x=-1; t.offset.x=1; }   \/\/ mirrored face: keep the art readable\n      const mat=new THREE.MeshStandardMaterial({map:t,transparent:true,depthWrite:false,polygonOffset:true,polygonOffsetFactor:-2,polygonOffsetUnits:-2,roughness:0.4,metalness:0.1,side:THREE.DoubleSide});\n      const pts=side.pts.map(([r,a])=>new THREE.Vector2(r,ds?-a+0.35:a-0.35));\n      const m=new THREE.Mesh(new THREE.LatheGeometry(pts,64,phiStart,phiLen),mat); m.rotation.z=-Math.PI\/2;\n      ctx.wheel.add(m); ctx.decalRes.push(t,mat);\n    }\n  };\n  if(look.design==='stripes'){ place(stripesCanvas(look,2*Math.PI*rMid,S),0,2*Math.PI); return; }\n  const {canvas,wmm}=badgeCanvas(look,S), phiLen=Math.min(1.4,wmm\/rMid);\n  for(const c of [3*Math.PI\/2,Math.PI\/2]) place(canvas,c-phiLen\/2,phiLen);\n}\nfunction syncLookUI(ctx){\n  const h=ctx.host, dz=ctx.look.design, ti=h.querySelector('[data-w3text]'), sw=h.querySelector('[data-w3swatches]');\n  if(ti) ti.style.display=dz==='text'?'':'none';\n  if(sw) sw.style.display=dz==='none'?'none':'';\n}\nfunction makeCtx(host){\n  const stage=host.querySelector('[data-w3stage]'); if(!stage) return null;\n  const msg=stage.querySelector('[data-w3msg]');\n  let renderer;\n  try{ renderer=new THREE.WebGLRenderer({antialias:true}); }\n  catch(e){ if(msg) msg.textContent='The 3D view needs WebGL, which is turned off in this browser.'; return null; }\n  renderer.setPixelRatio(Math.min(window.devicePixelRatio||1,2));\n  renderer.outputColorSpace=THREE.SRGBColorSpace;\n  renderer.toneMapping=THREE.ACESFilmicToneMapping;\n  renderer.domElement.setAttribute('aria-label','3D view of the wheel \u2014 drag to rotate, scroll to zoom');\n  stage.prepend(renderer.domElement);\n  if(msg) msg.style.display='none';\n  const scene=new THREE.Scene(); scene.background=new THREE.Color(0xF6F9FB);\n  scene.environment=new THREE.PMREMGenerator(renderer).fromScene(new RoomEnvironment(),0.04).texture;\n  scene.add(new THREE.HemisphereLight(0xffffff,0xC6D3DF,0.55));\n  const sun=new THREE.DirectionalLight(0xffffff,1.1); sun.position.set(500,900,700); scene.add(sun);\n  const camera=new THREE.PerspectiveCamera(30,1,1,20000);\n  const controls=new OrbitControls(camera,renderer.domElement);\n  controls.enableDamping=true; controls.dampingFactor=0.08; controls.minDistance=40; controls.maxDistance=8000;\n  const wheel=new THREE.Group(); scene.add(wheel);\n  const mats={rim:new THREE.MeshPhysicalMaterial({color:0x1F2833,metalness:0.55,roughness:0.4}),hub:std(0xC9D2DC,0.8,0.28),flL:std(0x084490,0.5,0.35),flR:std(0xD9530A,0.5,0.35),\n    axle:std(0x8D99A6,0.85,0.3),nipple:std(0xB8C2CC,0.9,0.25),tyre:std(0x1C1E21,0,0.85),free:std(0xA7B1BC,0.85,0.3),\n    hover:std(0xD4237A,0.4,0.4),hit:new THREE.MeshBasicMaterial({transparent:true,opacity:0,depthWrite:false,colorWrite:false})};\n  mats.rim.side=THREE.DoubleSide; mats.hover.emissive.setHex(HLC); mats.hover.emissiveIntensity=0.35;\n  const ctx={host,stage,renderer,scene,camera,controls,wheel,mats,spokeMats:{},spokes:[],color:'lead',spin:false,tyre:false,\n    lace:null,hl:null,visible:true,framed:false,view:'iso',d:null,fly:null,hover:null,\n    look:{finish:'auto',design:'none',text:'HM RACE 45',color:'#FFFFFF'},decalRes:[]};\n  ctxs.set(host.id,ctx);\n  new ResizeObserver(()=>resize(ctx)).observe(stage);\n  new IntersectionObserver(es=>{ ctx.visible=es[0].isIntersecting; },{threshold:0}).observe(stage);\n  wireUI(ctx); setupPick(ctx); resize(ctx);\n  const clock=new THREE.Clock();\n  (function loop(){\n    requestAnimationFrame(loop);\n    const dt=Math.min(clock.getDelta(),0.05);\n    if(!ctx.visible || !host.classList.contains('active') || !stage.clientWidth) return;\n    tick(ctx,dt); controls.update(); renderer.render(scene,camera);\n  })();\n  return ctx;\n}\nfunction resize(ctx){\n  const w=ctx.stage.clientWidth, h=ctx.stage.clientHeight; if(!w||!h) return;\n  ctx.renderer.setSize(w,h,false); ctx.camera.aspect=w\/h; ctx.camera.updateProjectionMatrix();\n  if(ctx.d && (!ctx.framed || Math.abs(ctx.camera.aspect\/(ctx.frameAspect||1)-1)>0.15)) frame(ctx,ctx.view,true);\n}\nfunction frame(ctx,view,instant){\n  const d=ctx.d; if(!d) return;\n  ctx.view=view;\n  const rs=rimSizes(d), cam=ctx.camera, R=(ctx.tyre?rs.bead+rs.tyreW*0.92:rs.top)+6;\n  ctx.frameAspect=cam.aspect;\n  const vf=cam.fov*Math.PI\/180, hf=2*Math.atan(Math.tan(vf\/2)*cam.aspect);\n  let dist=R\/Math.tan(Math.min(vf,hf)\/2)*1.05;\n  if(view==='hub') dist=Math.max(d.M.sides.L.fc,d.M.sides.R.fc,35)*4.4+90;\n  const to=new THREE.Vector3(...VIEWS[view]).normalize().multiplyScalar(dist), target=new THREE.Vector3();\n  if(instant){\n    cam.position.copy(to); ctx.controls.target.copy(target); ctx.controls.update();\n    if(ctx.stage.clientWidth) ctx.framed=true;\n    return;\n  }\n  ctx.fly={from:cam.position.clone(),to,t:0,tFrom:ctx.controls.target.clone(),tTo:target}; ctx.framed=true;\n}\nfunction clearWheel(ctx){\n  ctx.wheel.traverse(o=>{\n    if(o.geometry && o.geometry!==UNIT && o.geometry!==BALL) o.geometry.dispose();\n    if(o.isSprite){ o.material.map.dispose(); o.material.dispose(); }\n  });\n  ctx.wheel.clear(); ctx.spokes=[]; ctx.hover=null;\n  ctx.decalRes.forEach(x=>x.dispose()); ctx.decalRes=[];\n}\nfunction build(ctx){\n  const d=ctx.d, M=d.M, o=d.o, W=ctx.wheel, R=M.R; clearWheel(ctx);\n  const rz=rimSizes(d), RP=resample(rimProfile(rz),160);\n  applyFinish(ctx,rz,RP.len);\n  const rim=new THREE.Mesh(new THREE.LatheGeometry(RP.pts.map(([r,a])=>new THREE.Vector2(r,a)),180),ctx.mats.rim);\n  rim.rotation.z=-Math.PI\/2; W.add(rim);\n  addDecals(ctx,rz);\n  if(ctx.tyre) W.add(new THREE.Mesh(new THREE.TorusGeometry(rz.bead+rz.tyreW*0.42,rz.tyreW\/2,20,180).rotateY(Math.PI\/2),ctx.mats.tyre));\n  \/\/ hub, flanges, axle\n  const sp=o.hubType==='straight', xL=-M.sides.L.fc, xR=M.sides.R.fc, rear=d.key==='rear';\n  W.add(xCyl(Math.max(8,Math.min(M.sides.L.fl,M.sides.R.fl)\/2*0.5),xL,xR,ctx.mats.hub));\n  const ft=sp?6:3.2, sprL=Math.abs(M.sides.L.spr), sprR=Math.abs(M.sides.R.spr);\n  W.add(xCyl(M.sides.L.fl\/2+(sp?5:4),xL-(ft+sprL)\/2,xL+(ft+sprL)\/2,ctx.mats.flL,64));\n  W.add(xCyl(M.sides.R.fl\/2+(sp?5:4),xR-(ft+sprR)\/2,xR+(ft+sprR)\/2,ctx.mats.flR,64));\n  const endL=xL-(rear?28:16), endR=xR+(rear?50:16);\n  W.add(xCyl(5.5,endL,endR,ctx.mats.axle,24), xCyl(9.5,endL,endL+7,ctx.mats.axle,32), xCyl(9.5,endR-7,endR,ctx.mats.axle,32));\n  if(rear) W.add(xCyl(17,xR+5,xR+42,ctx.mats.free,48));\n  const lN=label('NDS','#084490'); lN.position.set(endL-30,0,0);\n  const lD=label('DS','#D9530A'); lD.position.set(endR+26,0,0); W.add(lN,lD);\n  \/\/ spokes, heads and nipples from the shared wheel model\n  const rs=Math.max(1.2,d.spokeDia||1.8)\/2;\n  M.spokes.forEach((s,i)=>{\n    const A=P(s.hub.x,s.hub.r,s.hub.a), B=P(s.rim.x,s.rim.r,s.rim.a), v=B.clone().sub(A), len=v.length(), u=v.clone().normalize();\n    const q=new THREE.Quaternion().setFromUnitVectors(UP,u);\n    const m=new THREE.Mesh(UNIT,ctx.mats.hit); m.position.copy(A); m.quaternion.copy(q); m.scale.set(rs,len,rs);\n    const hit=new THREE.Mesh(UNIT,ctx.mats.hit); hit.position.copy(A); hit.quaternion.copy(q); hit.scale.set(3.4,len,3.4); hit.userData.i=i;\n    const nI=o.nip||{type:'round',len:12,add:2,head:2.2}, nIn=nI.type==='internal', nA=isFinite(nI.add)?nI.add:2, nL=nI.len||12;\n    const nip=new THREE.Mesh(UNIT,ctx.mats.nipple); nip.position.copy(B).addScaledVector(u,-(nIn?nA:nA+nL-(nI.head||0))); nip.quaternion.copy(q); nip.scale.set(nIn?2.2:1.75,nL,nIn?2.2:1.75);\n    const head=new THREE.Mesh(BALL,ctx.mats.hit); head.position.copy(A); head.scale.setScalar(rs*1.9);\n    W.add(m,hit,nip,head);\n    ctx.spokes.push({s,m,hit,nip,head,len,base:null});\n  });\n  ctx.lace=null;\n  applyColors(ctx); applyHL(ctx);\n  if(!ctx.framed) frame(ctx,ctx.view,true);\n}\nfunction spokeTen(ctx,s){\n  const d=ctx.d, T=s.side==='L'?d.tenL:d.tenR, sp=s.side==='L'?d.spL:d.spR;\n  if(sp==null || s.role==='radial') return T;\n  const lead=2*T\/(1+sp); return s.lead?lead:sp*lead;\n}\nfunction spokeColor(ctx,s){\n  if(ctx.color==='lead') return s.role==='radial' ? COL[s.side] : COL[s.side==='L'?(s.lead?'Sa':'Sb'):(s.lead?'Sd':'Sc')];\n  if(ctx.color==='tension'){ const t=spokeTen(ctx,s), d=ctx.d; return t<d.tenMin?COL.low:(d.tenMax&&t>d.tenMax?COL.high:COL.ok); }\n  return COL[s.side];\n}\nconst matFor=(ctx,hex)=>ctx.spokeMats[hex]||(ctx.spokeMats[hex]=std(hex,0.6,0.32));\nfunction applyColors(ctx){\n  for(const x of ctx.spokes){\n    x.base=matFor(ctx,spokeColor(ctx,x.s));\n    if(ctx.hover!==x){ x.m.material=x.base; x.head.material=x.base; }\n  }\n  legend(ctx);\n}\nfunction applyHL(ctx){\n  const on=new Set(HLMAP[ctx.hl]||[]);\n  const set=(mat,flag)=>{ mat.emissive.setHex(flag?HLC:0x000000); mat.emissiveIntensity=flag?0.5:0; };\n  set(ctx.mats.rim,on.has('rim')); set(ctx.mats.hub,on.has('hub')); set(ctx.mats.flL,on.has('flL')); set(ctx.mats.flR,on.has('flR')); set(ctx.mats.nipple,on.has('nipple'));\n  for(const k in ctx.spokeMats) set(ctx.spokeMats[k],on.has('spokes'));\n}\nfunction legend(ctx){\n  const el=ctx.stage.querySelector('[data-w3legend]'); if(!el||!ctx.d) return;\n  const d=ctx.d, hex=c=>'#'+c.toString(16).padStart(6,'0'), it=(c,t)=>`<span><i style=\"background:${hex(c)}\"><\/i>${t}<\/span>`;\n  if(ctx.color==='lead'){ const rad=sd=>ctx.spokes.some(x=>x.s.side===sd&&x.s.role==='radial');\n    el.innerHTML=(rad('L')?it(COL.L,'NDS radial'):it(COL.Sa,'Sa NDS leading')+it(COL.Sb,'Sb NDS trailing'))+(rad('R')?it(COL.R,'DS radial'):it(COL.Sd,'Sd DS leading')+it(COL.Sc,'Sc DS trailing')); }\n  else if(ctx.color==='tension') el.innerHTML=it(COL.ok,`In window ${d.tenMin?Math.round(d.tenMin):'\u2014'}\u2013${d.tenMax?Math.round(d.tenMax):'\u2014'} kgf`)+(d.tenMin?it(COL.low,'Below min'):'')+(d.tenMax?it(COL.high,'Above max'):'');\n  else el.innerHTML=it(COL.L,`NDS \u00b7 ${d.M.nL} spokes`)+it(COL.R,`DS \u00b7 ${d.M.nR} spokes`);\n  const rs=rimSizes(d); el.innerHTML+=`<span style=\"opacity:.7\">rim ${Math.round(rs.W)} \u00d7 ${Math.round(rs.H)} mm<\/span>`;\n}\nfunction startLace(ctx){\n  if(!ctx.spokes.length) return;\n  const order=[...ctx.spokes].sort((a,b)=>(ORDER[a.s.code]-ORDER[b.s.code])||(a.s.j-b.s.j));\n  order.forEach(x=>{ x.m.visible=false; x.head.visible=false; x.nip.visible=false; });\n  ctx.lace={t:0,order,per:0.35,gap:Math.min(0.12,3.2\/order.length)};\n}\nfunction tick(ctx,dt){\n  if(ctx.spin) ctx.wheel.rotation.x+=dt*0.9;\n  if(ctx.fly){\n    const f=ctx.fly; f.t=Math.min(1,f.t+dt\/0.6); const e=1-Math.pow(1-f.t,3);\n    ctx.camera.position.lerpVectors(f.from,f.to,e); ctx.controls.target.lerpVectors(f.tFrom,f.tTo,e);\n    if(f.t>=1) ctx.fly=null;\n  }\n  if(ctx.lace){\n    const L=ctx.lace; L.t+=dt; let done=true;\n    L.order.forEach((x,k)=>{\n      const p=Math.max(0,Math.min(1,(L.t-k*L.gap)\/L.per)); if(p<1) done=false;\n      const sl=Math.max(0.001,x.len*(1-Math.pow(1-p,2)));\n      x.m.scale.y=sl; x.hit.scale.y=sl; x.m.visible=p>0; x.head.visible=p>0; x.nip.visible=p>=1;\n    });\n    if(done) ctx.lace=null;\n  }\n}\nfunction wireUI(ctx){\n  const h=ctx.host;\n  h.querySelectorAll('[data-w3seg] button').forEach(b=>b.addEventListener('click',()=>{\n    const grp=b.closest('[data-w3seg]'), kind=grp.dataset.w3seg;\n    grp.querySelectorAll('button').forEach(x=>x.setAttribute('aria-pressed',x===b?'true':'false'));\n    if(kind==='color'){ ctx.color=b.dataset.v; applyColors(ctx); applyHL(ctx); }\n    else if(kind==='finish'){ ctx.look.finish=b.dataset.v; if(ctx.d) build(ctx); }\n    else if(kind==='decal'){ ctx.look.design=b.dataset.v; syncLookUI(ctx); if(ctx.d) build(ctx); }\n    else frame(ctx,b.dataset.v,false);\n  }));\n  h.querySelectorAll('[data-w3color]').forEach(b=>b.addEventListener('click',()=>{\n    h.querySelectorAll('[data-w3color]').forEach(x=>x.setAttribute('aria-pressed',x===b?'true':'false'));\n    ctx.look.color=b.dataset.w3color; if(ctx.d) build(ctx);\n  }));\n  const ti=h.querySelector('[data-w3text]'); let tT=null;\n  if(ti) ti.addEventListener('input',()=>{ ctx.look.text=ti.value; clearTimeout(tT); tT=setTimeout(()=>{ if(ctx.d) build(ctx); },160); });\n  syncLookUI(ctx);\n  h.querySelectorAll('[data-w3act]').forEach(b=>b.addEventListener('click',()=>{\n    const a=b.dataset.w3act;\n    if(a==='spin'){ ctx.spin=!ctx.spin; b.setAttribute('aria-pressed',String(ctx.spin)); }\n    else if(a==='tyre'){ ctx.tyre=!ctx.tyre; b.setAttribute('aria-pressed',String(ctx.tyre)); if(ctx.d) build(ctx); }\n    else if(a==='lace') startLace(ctx);\n  }));\n}\nfunction setHover(ctx,x){\n  if(ctx.hover===x) return;\n  if(ctx.hover){ ctx.hover.m.material=ctx.hover.base; ctx.hover.head.material=ctx.hover.base; }\n  ctx.hover=x||null;\n  if(x){ x.m.material=ctx.mats.hover; x.head.material=ctx.mats.hover; }\n}\nfunction tipHTML(ctx,x){\n  const s=x.s, showLen=ctx.host.classList.contains('has-results')&&!ctx.host.classList.contains('stale');\n  return `<b>${s.sideName} ${s.role} \u00b7 ${s.code}<\/b><br>\u03b8 ${s.theta.toFixed(1)}\u00b0 \u00b7 \u03b3 ${s.brace.toFixed(1)}\u00b0 \u00b7 \u2248${Math.round(spokeTen(ctx,s))} kgf<br>`+\n    (showLen?`length ${s.len.toFixed(1)} mm`:'<span style=\"opacity:.75\">Length shown after Calculate<\/span>');\n}\nfunction setupPick(ctx){\n  const ray=new THREE.Raycaster(), ptr=new THREE.Vector2(), el=ctx.renderer.domElement, tip=ctx.stage.querySelector('[data-w3tip]');\n  const hide=()=>tip.classList.remove('show');\n  el.addEventListener('pointermove',e=>{\n    if(e.buttons||ctx.lace){ setHover(ctx,null); hide(); return; }\n    const r=el.getBoundingClientRect();\n    ptr.set((e.clientX-r.left)\/r.width*2-1,-(e.clientY-r.top)\/r.height*2+1);\n    ray.setFromCamera(ptr,ctx.camera);\n    const hits=ray.intersectObjects(ctx.spokes.map(x=>x.hit),false);\n    const x=hits.length?ctx.spokes[hits[0].object.userData.i]:null;\n    setHover(ctx,x);\n    if(!x){ hide(); return; }\n    tip.innerHTML=tipHTML(ctx,x); tip.classList.add('show');\n    tip.style.left=Math.max(8,Math.min(r.width-tip.offsetWidth-8,e.clientX-r.left+14))+'px';\n    tip.style.top=Math.max(8,Math.min(r.height-tip.offsetHeight-8,e.clientY-r.top+14))+'px';\n  });\n  el.addEventListener('pointerleave',()=>{ setHover(ctx,null); hide(); });\n}\nwindow.HM3D={\n  update(host,d){ const ctx=ctxs.get(host.id)||makeCtx(host); if(!ctx) return; ctx.d=d; build(ctx); },\n  highlight(host,f){ const ctx=ctxs.get(host.id); if(!ctx) return; ctx.hl=f; applyHL(ctx); },\n  _ctx(host){ return ctxs.get(host.id); }\n};\nwindow.dispatchEvent(new Event('hm3d-ready'));\n<\/script>\n<script data-no-optimize=\"1\" data-no-defer=\"1\" data-no-minify=\"1\" data-cfasync=\"false\" data-pagespeed-no-defer nowprocket>\n(()=>{ const r=document.getElementById(\"hmsc-app\"); if(!r||r.hasAttribute(\"data-boxed\")||r.dataset.hmscFull) return; r.dataset.hmscFull=\"1\";\n  const fit=()=>{ if(!r.isConnected) return; const w=document.documentElement.clientWidth, x=r.parentElement.getBoundingClientRect().left;\n    r.style.width=w+\"px\"; r.style.marginLeft=(-x)+\"px\"; };\n  fit(); addEventListener(\"resize\",fit); if(\"ResizeObserver\" in window) new ResizeObserver(fit).observe(r.parentElement); })();\n<\/script>\n<!--\/noptimize-->\n\t\t\t\t<\/div>\n\t\t\t\t\t<\/div>\n\t\t\t\t<\/div>\n\t\t\t\t<\/div>\n\t\t\t\t\t<\/div>\n\t\t\t\t<\/div>\n\t\t\t\t<\/div>\n\t\t","protected":false},"excerpt":{"rendered":"<p>Spoke Length &amp; Wheel Configurator Spoke length, build weight, tension balance and lacing geometry for classic and straight-pull hubs. Everything runs in your browser. Wheelbuilding tool \u00b7 front &amp; rear All values in mm \u00b7 dot (.) for decimals Front wheel Rear wheel Example report<\/p>","protected":false},"author":1,"featured_media":0,"parent":0,"menu_order":0,"comment_status":"closed","ping_status":"closed","template":"elementor_header_footer","meta":{"content-type":"","inline_featured_image":false,"footnotes":""},"class_list":["post-49057","page","type-page","status-publish","hentry"],"yoast_head":"<!-- This site is optimized with the Yoast SEO plugin v28.7 - https:\/\/yoast.com\/product\/yoast-seo-wordpress\/ -->\n<title>Wheel &amp; Spoke Calculator - Holland Mechanics<\/title>\n<meta name=\"description\" content=\"The Holland Mechanics Wheel &amp; Spoke calculator. Check advanced data needed to configure your wheel.\" \/>\n<meta name=\"robots\" content=\"index, follow, max-snippet:-1, max-image-preview:large, max-video-preview:-1\" \/>\n<link rel=\"canonical\" href=\"https:\/\/hollandmechanics.com\/zh\/wheel-spoke-calculator\/\" \/>\n<meta property=\"og:locale\" content=\"zh_CN\" \/>\n<meta property=\"og:type\" content=\"article\" \/>\n<meta property=\"og:title\" content=\"Wheel &amp; Spoke Calculator - Holland Mechanics\" \/>\n<meta property=\"og:description\" content=\"The Holland Mechanics Wheel &amp; Spoke calculator. Check advanced data needed to configure your wheel.\" \/>\n<meta property=\"og:url\" content=\"https:\/\/hollandmechanics.com\/zh\/wheel-spoke-calculator\/\" \/>\n<meta property=\"og:site_name\" content=\"Holland Mechanics\" \/>\n<meta property=\"article:modified_time\" content=\"2026-10-13T09:58:40+00:00\" \/>\n<meta name=\"twitter:card\" content=\"summary_large_image\" \/>\n<meta name=\"twitter:label1\" content=\"\u9884\u8ba1\u9605\u8bfb\u65f6\u95f4\" \/>\n\t<meta name=\"twitter:data1\" content=\"97 \u5206\" \/>\n<script type=\"application\/ld+json\" class=\"yoast-schema-graph\">{\"@context\":\"https:\\\/\\\/schema.org\",\"@graph\":[{\"@type\":\"WebPage\",\"@id\":\"https:\\\/\\\/hollandmechanics.com\\\/en\\\/wheel-spoke-calculator\\\/\",\"url\":\"https:\\\/\\\/hollandmechanics.com\\\/en\\\/wheel-spoke-calculator\\\/\",\"name\":\"Wheel &amp; Spoke Calculator - Holland Mechanics\",\"isPartOf\":{\"@id\":\"https:\\\/\\\/hollandmechanics.com\\\/#website\"},\"datePublished\":\"2026-06-10T13:57:33+00:00\",\"dateModified\":\"2026-10-13T09:58:40+00:00\",\"description\":\"The Holland Mechanics Wheel & Spoke calculator. Check advanced data needed to configure your wheel.\",\"breadcrumb\":{\"@id\":\"https:\\\/\\\/hollandmechanics.com\\\/en\\\/wheel-spoke-calculator\\\/#breadcrumb\"},\"inLanguage\":\"zh-Hans\",\"potentialAction\":[{\"@type\":\"ReadAction\",\"target\":[\"https:\\\/\\\/hollandmechanics.com\\\/en\\\/wheel-spoke-calculator\\\/\"]}]},{\"@type\":\"BreadcrumbList\",\"@id\":\"https:\\\/\\\/hollandmechanics.com\\\/en\\\/wheel-spoke-calculator\\\/#breadcrumb\",\"itemListElement\":[{\"@type\":\"ListItem\",\"position\":1,\"name\":\"Home\",\"item\":\"https:\\\/\\\/hollandmechanics.com\\\/\"},{\"@type\":\"ListItem\",\"position\":2,\"name\":\"Wheel &amp; Spoke Calculator\"}]},{\"@type\":\"WebSite\",\"@id\":\"https:\\\/\\\/hollandmechanics.com\\\/#website\",\"url\":\"https:\\\/\\\/hollandmechanics.com\\\/\",\"name\":\"Holland Mechanics\",\"description\":\"Go to homepage\",\"publisher\":{\"@id\":\"https:\\\/\\\/hollandmechanics.com\\\/#organization\"},\"potentialAction\":[{\"@type\":\"SearchAction\",\"target\":{\"@type\":\"EntryPoint\",\"urlTemplate\":\"https:\\\/\\\/hollandmechanics.com\\\/?s={search_term_string}\"},\"query-input\":{\"@type\":\"PropertyValueSpecification\",\"valueRequired\":true,\"valueName\":\"search_term_string\"}}],\"inLanguage\":\"zh-Hans\"},{\"@type\":\"Organization\",\"@id\":\"https:\\\/\\\/hollandmechanics.com\\\/#organization\",\"name\":\"Holland Mechanics\",\"url\":\"https:\\\/\\\/hollandmechanics.com\\\/\",\"logo\":{\"@type\":\"ImageObject\",\"inLanguage\":\"zh-Hans\",\"@id\":\"https:\\\/\\\/hollandmechanics.com\\\/#\\\/schema\\\/logo\\\/image\\\/\",\"url\":\"https:\\\/\\\/hollandmechanics.com\\\/wp-content\\\/uploads\\\/2023\\\/01\\\/cropped-HMLogo-1-e1672838939593.png\",\"contentUrl\":\"https:\\\/\\\/hollandmechanics.com\\\/wp-content\\\/uploads\\\/2023\\\/01\\\/cropped-HMLogo-1-e1672838939593.png\",\"width\":200,\"height\":70,\"caption\":\"Holland Mechanics\"},\"image\":{\"@id\":\"https:\\\/\\\/hollandmechanics.com\\\/#\\\/schema\\\/logo\\\/image\\\/\"},\"sameAs\":[\"https:\\\/\\\/www.linkedin.com\\\/company\\\/holland-mechanics-b.v.\\\/\"]}]}<\/script>\n<!-- \/ Yoast SEO plugin. -->","yoast_head_json":{"title":"Wheel &amp; Spoke Calculator - Holland Mechanics","description":"The Holland Mechanics Wheel & Spoke calculator. Check advanced data needed to configure your wheel.","robots":{"index":"index","follow":"follow","max-snippet":"max-snippet:-1","max-image-preview":"max-image-preview:large","max-video-preview":"max-video-preview:-1"},"canonical":"https:\/\/hollandmechanics.com\/zh\/wheel-spoke-calculator\/","og_locale":"zh_CN","og_type":"article","og_title":"Wheel &amp; Spoke Calculator - Holland Mechanics","og_description":"The Holland Mechanics Wheel & Spoke calculator. Check advanced data needed to configure your wheel.","og_url":"https:\/\/hollandmechanics.com\/zh\/wheel-spoke-calculator\/","og_site_name":"Holland Mechanics","article_modified_time":"2026-10-13T09:58:40+00:00","twitter_card":"summary_large_image","twitter_misc":{"\u9884\u8ba1\u9605\u8bfb\u65f6\u95f4":"97 \u5206"},"schema":{"@context":"https:\/\/schema.org","@graph":[{"@type":"WebPage","@id":"https:\/\/hollandmechanics.com\/en\/wheel-spoke-calculator\/","url":"https:\/\/hollandmechanics.com\/en\/wheel-spoke-calculator\/","name":"Wheel &amp; Spoke Calculator - Holland Mechanics","isPartOf":{"@id":"https:\/\/hollandmechanics.com\/#website"},"datePublished":"2026-06-10T13:57:33+00:00","dateModified":"2026-10-13T09:58:40+00:00","description":"The Holland Mechanics Wheel & Spoke calculator. Check advanced data needed to configure your wheel.","breadcrumb":{"@id":"https:\/\/hollandmechanics.com\/en\/wheel-spoke-calculator\/#breadcrumb"},"inLanguage":"zh-Hans","potentialAction":[{"@type":"ReadAction","target":["https:\/\/hollandmechanics.com\/en\/wheel-spoke-calculator\/"]}]},{"@type":"BreadcrumbList","@id":"https:\/\/hollandmechanics.com\/en\/wheel-spoke-calculator\/#breadcrumb","itemListElement":[{"@type":"ListItem","position":1,"name":"Home","item":"https:\/\/hollandmechanics.com\/"},{"@type":"ListItem","position":2,"name":"Wheel &amp; Spoke Calculator"}]},{"@type":"WebSite","@id":"https:\/\/hollandmechanics.com\/#website","url":"https:\/\/hollandmechanics.com\/","name":"Holland Mechanics","description":"Go to homepage","publisher":{"@id":"https:\/\/hollandmechanics.com\/#organization"},"potentialAction":[{"@type":"SearchAction","target":{"@type":"EntryPoint","urlTemplate":"https:\/\/hollandmechanics.com\/?s={search_term_string}"},"query-input":{"@type":"PropertyValueSpecification","valueRequired":true,"valueName":"search_term_string"}}],"inLanguage":"zh-Hans"},{"@type":"Organization","@id":"https:\/\/hollandmechanics.com\/#organization","name":"Holland Mechanics","url":"https:\/\/hollandmechanics.com\/","logo":{"@type":"ImageObject","inLanguage":"zh-Hans","@id":"https:\/\/hollandmechanics.com\/#\/schema\/logo\/image\/","url":"https:\/\/hollandmechanics.com\/wp-content\/uploads\/2023\/01\/cropped-HMLogo-1-e1672838939593.png","contentUrl":"https:\/\/hollandmechanics.com\/wp-content\/uploads\/2023\/01\/cropped-HMLogo-1-e1672838939593.png","width":200,"height":70,"caption":"Holland Mechanics"},"image":{"@id":"https:\/\/hollandmechanics.com\/#\/schema\/logo\/image\/"},"sameAs":["https:\/\/www.linkedin.com\/company\/holland-mechanics-b.v.\/"]}]}},"_links":{"self":[{"href":"https:\/\/hollandmechanics.com\/zh\/wp-json\/wp\/v2\/pages\/49057","targetHints":{"allow":["GET"]}}],"collection":[{"href":"https:\/\/hollandmechanics.com\/zh\/wp-json\/wp\/v2\/pages"}],"about":[{"href":"https:\/\/hollandmechanics.com\/zh\/wp-json\/wp\/v2\/types\/page"}],"author":[{"embeddable":true,"href":"https:\/\/hollandmechanics.com\/zh\/wp-json\/wp\/v2\/users\/1"}],"replies":[{"embeddable":true,"href":"https:\/\/hollandmechanics.com\/zh\/wp-json\/wp\/v2\/comments?post=49057"}],"version-history":[{"count":52,"href":"https:\/\/hollandmechanics.com\/zh\/wp-json\/wp\/v2\/pages\/49057\/revisions"}],"predecessor-version":[{"id":50489,"href":"https:\/\/hollandmechanics.com\/zh\/wp-json\/wp\/v2\/pages\/49057\/revisions\/50489"}],"wp:attachment":[{"href":"https:\/\/hollandmechanics.com\/zh\/wp-json\/wp\/v2\/media?parent=49057"}],"curies":[{"name":"wp","href":"https:\/\/api.w.org\/{rel}","templated":true}]}}