{"product_id":"elastomer-configurator-sim","title":"3DRap Elastomers Configurator","description":"\u003cbody\u003e\n\u003c!-- Cart bubble fix: keeps the badge perfectly circular and prevents header span stretching --\u003e\n\u003cstyle id=\"cart-bubble-fix\"\u003e\n\/* Limit height:100% to actual icons, not the cart badge *\/\n.header__icon .icon,\n.header__icon svg,\n.header__icon .icon \u003e * {\n  height: 100%;\n}\n.header__icon span:not(.cart-count-bubble):not([data-cart-count]):not([data-cart-count-bubble]) {\n  height: 100%;\n}\n\n\/* Enforce a perfect circle and use the CSS variables for size\/position *\/\n.header__icon .cart-count-bubble,\n.header__icon [data-cart-count],\n.header__icon [data-cart-count-bubble]{\n  position: absolute;\n  width: var(--cart-badge-size) !important;\n  height: var(--cart-badge-size) !important;\n  aspect-ratio: 1 \/ 1;\n  border-radius: 50%;\n  top: var(--cart-badge-top) !important;\n  right: var(--cart-badge-right) !important;\n  left: auto !important;\n  bottom: auto !important;\n  align-self: auto;\n  display: flex !important;\n  align-items: center !important;\n  justify-content: center !important;\n}\n\n\/* Inner number shouldn't stretch vertically *\/\n.header__icon .cart-count-bubble \u003e span[aria-hidden=\"true\"]{\n  height: auto !important;\n  line-height: 1;\n}\n\u003c\/style\u003e\n\n  \u003cmeta charset=\"UTF-8\"\u003e\n  \u003cmeta name=\"viewport\" content=\"width=1024\"\u003e\n  \u003ctitle\u003eConfiguratore Elastomero Dinamico\u003c\/title\u003e\n  \u003cscript src=\"https:\/\/cdn.jsdelivr.net\/npm\/chart.js\"\u003e\u003c\/script\u003e\n\n  \u003cstyle\u003e\n    body { margin:0; font-family:sans-serif; background:#1d1d1b; color:#fff; }\n    .container { position:relative; max-width:720px; margin:auto; aspect-ratio:9\/16; }\n    .background { position:absolute; inset:0; width:100%; height:100%; object-fit:contain; pointer-events:none; z-index:0; }\n    .overlay { position:absolute; inset:0; z-index:1; }\n\n    .select-container { position:absolute; top:69%; left:19.7%; display:flex; gap:4px; transform:translateY(-50%); }\n    .elastomer-slot { font-size:16px; font-weight:700; background:#fff; width:68px; text-align:center; padding:.3em; border-radius:5px; }\n    .elastomer-control { font-size:18px; padding:10px 18px; border-radius:6px; font-weight:700; background:#fff; color:#000; border:none; cursor:pointer; }\n\n    input[type=\"number\"] { font-size:18px; padding:.3em; width:65px; background:#fff; color:#000; border:1px solid #ccc; border-radius:5px; }\n    .input-group { position:absolute; display:flex; flex-direction:column; gap:5px; font-size:18px; align-items:center; text-align:center; }\n    .input-group label { margin-bottom:2px; }\n    #input-L1 { top:63%; left:83.5%; width:100px; }\n    #input-L2 { top:70%; left:72.5%; width:100px; }\n\n    .graph { position:absolute; left:7%; top:4%; width:82%; height:44%; }\n    .graph canvas { width:100% !important; height:100% !important; }\n\n    .button-group { position:absolute; bottom:11%; left:50%; transform:translateX(-50%); display:flex; gap:15px; }\n    .btn-reset,.btn-add,.btn-cart,.btn-download,.btn-choose,.btn-delete { border:none; border-radius:8px; font-weight:700; cursor:pointer; }\n    .btn-reset { background:#fff; color:#000; padding:12px; font-size:18px; width:210px; }\n    .btn-add { background:#d9ff00; color:#000; padding:12px; font-size:18px; width:210px; }\n    .btn-cart { background:#d9ff00; color:#000; padding:6px; font-size:18px; width:210px; }\n    .btn-download { background:#d9ff00; color:#000; padding:12px; font-size:18px; width:180px; }\n    .btn-choose { background:#d9ff00; color:#000; padding:6px; font-size:18px; width:100px; }\n    .btn-delete { background:#fff; color:#000; padding:6px 12px; font-size:18px; }\n\n    .saved-configs { position:absolute; top:89.5%; left:45%; transform:translateX(-65%); display:grid; grid-template-columns:1fr 1fr; gap:1px 34px; max-width:400px; z-index:10; }\n\n    #elastomerModal button,#elastomerModal input,#elastomerModal select,#elastomerModal textarea { font-size:18px; border-radius:5px; padding:12px; }\n    #elastomerModal { position:absolute; inset:0; background:rgba(0,0,0,.7); z-index:10; display:none; }\n    #elastomerModal\u003ediv { position:absolute; top:75%; left:50%; transform:translate(-50%,-50%); width:300px; min-height:220px; background:#2a2a28; padding:1em; border-radius:10px; color:#fff; font-size:18px; display:flex; flex-direction:column; justify-content:center; align-items:center; gap:12px; }\n\n    #holeFilter { font-size:18px; padding:.3em; background:#fff; color:#000; border:1px solid #ccc; border-radius:5px; margin-top:8px; }\n\n    .tooltip { position:relative; display:inline-block; cursor:help; }\n    .tooltip .tooltiptext { visibility:hidden; width:280px; background:#2e2e2c; color:#fff; text-align:justify; padding:8px; border-radius:6px; position:absolute; z-index:100; bottom:125%; left:50%; transform:translateX(-50%); opacity:0; transition:opacity .3s; font-size:18px; line-height:1.3; }\n    .tooltip:hover .tooltiptext { visibility:visible; opacity:1; }\n    .tooltip.vertical-icon { display:inline-flex; flex-direction:column; align-items:center; text-align:center; gap:2px; }\n    .tooltip.vertical-icon .info-icon { font-size:20px; color:#d9ff00; font-weight:bold; line-height:1; }\n\n    @media (max-width:1024px){\n      body { width:1024px; transform:scale(1.3); transform-origin:top left; overflow-x:auto; }\n      html { overflow-x:auto; }\n    }\n\n    \/* === Cart badge finale 3DRAP (centrato, senza 9+) === *\/\n    :root{\n      --cart-badge-bg:#d9ff00;\n      --cart-badge-size:22px;\n      --cart-badge-font:13px;\n      --cart-badge-top:-10px;\n      --cart-badge-right:-2px;\n    }\n    [data-cart-toggle],[data-cart-drawer-toggle],.header__icon--cart,.header__icon,.header__icon .icon,a[href*=\"\/cart\"]{ position:relative; }\n    .cart-count-bubble,.cart-count,[data-cart-count],[data-cart-count-bubble]{\n      position:absolute; top:var(--cart-badge-top); right:var(--cart-badge-right);\n      width:var(--cart-badge-size); height:var(--cart-badge-size);\n      min-width:var(--cart-badge-size); min-height:var(--cart-badge-size);\n      padding:0 !important; border:0; border-radius:50%; box-sizing:border-box;\n      display:flex !important; align-items:center !important; justify-content:center !important;\n      background:var(--cart-badge-bg); color:#000; font-weight:700; font-size:var(--cart-badge-font);\n      line-height:1; text-align:center; z-index:25;\n      overflow:hidden;\n    }\n    .cart-count-bubble \u003e span[aria-hidden=\"true\"], .cart-count \u003e span[aria-hidden=\"true\"], [data-cart-count] \u003e span[aria-hidden=\"true\"]{\n      display:flex; align-items:center; justify-content:center; height:100%;\n      transform:translateY(0px);\n    }\n    .cart-count-bubble .visually-hidden,.cart-count .visually-hidden,[data-cart-count] .visually-hidden{\n      position:absolute !important; width:1px; height:1px; margin:-1px; padding:0; border:0; clip:rect(0 0 0 0); overflow:hidden;\n    }\n    @media (max-width:990px){\n      :root{ --cart-badge-size:22px; --cart-badge-font:13px; --cart-badge-top:-10px; --cart-badge-right:-2px; }\n    }\n  \u003c\/style\u003e\n\n  \u003c!-- === GLOBAL: patch carrello (bolla + osservatori + fetch\/xhr) === --\u003e\n  \u003cscript\u003e\n  (function ensureCartRefresh(){\n    function getOrCreateBubbles(){\n      const bubbles = Array.from(document.querySelectorAll('.cart-count-bubble, .cart-count, [data-cart-count], [data-cart-count-bubble]'));\n      if (bubbles.length) return bubbles;\n\n      const icon =\n        document.querySelector('[data-cart-toggle], [data-cart-drawer-toggle], a[href*=\"\/cart\"], .header__icon--cart') ||\n        document.querySelector('cart-drawer-toggler') ||\n        document.querySelector('cart-drawer button, cart-drawer-toggle');\n      if (!icon) return [];\n\n      const bubble = document.createElement('span');\n      bubble.className = 'cart-count-bubble';\n      bubble.innerHTML = '\u003cspan aria-hidden=\"true\"\u003e0\u003c\/span\u003e\u003cspan class=\"visually-hidden\"\u003e0 items\u003c\/span\u003e';\n      bubble.style.display = 'none';\n      icon.appendChild(bubble);\n      return [bubble];\n    }\n\n    function applyCountToBubble(bubble, count){\n      const hidden = bubble.querySelector('span[aria-hidden=\"true\"]') || bubble.querySelector('[data-count]');\n      const vis    = bubble.querySelector('.visually-hidden') || bubble.querySelector('[data-a11y]');\n      const display = String(count);\n      if (hidden) hidden.textContent = display;\n      if (vis)    vis.textContent    = count === 1 ? '1 item' : `${count} items`;\n      bubble.style.display = count \u003e 0 ? '' : 'none';\n      bubble.classList?.remove('hidden');\n      bubble.setAttribute?.('data-cart-count', String(count));\n    }\n\n    window.updateThemeCartBubble = function(count){\n      const bubbles = getOrCreateBubbles();\n      bubbles.forEach(b =\u003e applyCountToBubble(b, count));\n    };\n\n    window.refreshCartUI = async function(openAfter = false){\n      try {\n        const cart  = await fetch('\/cart.js', { credentials:'same-origin' }).then(r =\u003e r.json());\n        const count = cart?.item_count ?? 0;\n        window.updateThemeCartBubble(count);\n        document.body.dispatchEvent(new CustomEvent('cart:updated', { detail: cart }));\n\n        if (openAfter) {\n          const drawer = document.querySelector('cart-drawer');\n          if (drawer \u0026\u0026 typeof drawer.open === 'function') drawer.open();\n          else {\n            const t = document.querySelector('[data-cart-drawer-toggle],[data-cart-toggle]');\n            if (t) t.click();\n          }\n        }\n      } catch(e){ console.warn('refreshCartUI failed', e); }\n    };\n\n    if (!window.__fetchPatchedForCart__) {\n      window.__fetchPatchedForCart__ = true;\n      const _fetch = window.fetch;\n      window.fetch = async function(...args){\n        const res = await _fetch.apply(this, args);\n        try {\n          const input = args[0];\n          const url = typeof input === 'string' ? input : (input \u0026\u0026 input.url) || '';\n          if (\/\\\/cart\\\/(add|change|update|clear)\\.js(\\?|$)\/.test(url)) {\n            setTimeout(() =\u003e window.refreshCartUI(false), 0);\n          }\n        } catch {}\n        return res;\n      };\n    }\n\n    if (!window.__xhrPatchedForCart__) {\n      window.__xhrPatchedForCart__ = true;\n      const _open = XMLHttpRequest.prototype.open;\n      const _send = XMLHttpRequest.prototype.send;\n      XMLHttpRequest.prototype.open = function(method, url){\n        this.__isCartReq = typeof url === 'string' \u0026\u0026 \/\\\/cart\\\/(add|change|update|clear)\\.js(\\?|$)\/.test(url);\n        return _open.apply(this, arguments);\n      };\n      XMLHttpRequest.prototype.send = function(){\n        if (this.__isCartReq) this.addEventListener('load', () =\u003e setTimeout(() =\u003e window.refreshCartUI(false), 0));\n        return _send.apply(this, arguments);\n      };\n    }\n\n    (function observeCartNotification(){\n      const parseCount = (root) =\u003e {\n        const btn = root?.querySelector?.('#cart-notification-button, .cart-notification__link, [data-cart-notification-button]');\n        if (!btn) return null;\n        const m = (btn.textContent || '').match(\/\\((\\d+)\\)\/);\n        return m ? parseInt(m[1], 10) : null;\n      };\n      const attach = (node) =\u003e {\n        const mo = new MutationObserver(() =\u003e {\n          const n = parseCount(node);\n          if (Number.isFinite(n)) window.updateThemeCartBubble(n);\n        });\n        mo.observe(node, { childList:true, subtree:true, characterData:true, attributes:true });\n        const first = parseCount(node);\n        if (Number.isFinite(first)) window.updateThemeCartBubble(first);\n      };\n      const tryAttach = () =\u003e {\n        document.querySelectorAll('#cart-notification, .cart-notification, [data-cart-notification]').forEach(n =\u003e {\n          if (!n.__cn_observed__) { n.__cn_observed__ = true; attach(n); }\n        });\n      };\n      tryAttach();\n      const moDoc = new MutationObserver(tryAttach);\n      moDoc.observe(document.documentElement, { childList:true, subtree:true });\n    })();\n\n    \/\/ click generici \"Add to cart\" -\u003e sync bolla\n    document.addEventListener('click', (ev) =\u003e {\n      const btn = ev.target.closest('[name=\"add\"], button[type=\"submit\"], [data-add-to-cart], .product-form__submit');\n      if (btn) setTimeout(() =\u003e window.refreshCartUI(false), 700);\n    }, true);\n\n    document.addEventListener('DOMContentLoaded', () =\u003e window.refreshCartUI(false));\n  })();\n  \u003c\/script\u003e\n\n  \u003cdiv class=\"container\"\u003e\n    \u003cimg class=\"background\" src=\"https:\/\/cdn.shopify.com\/s\/files\/1\/0899\/4816\/0342\/t\/6\/assets\/ConfiguratoreElastomeri_1El.png\" alt=\"config\"\u003e\n    \u003cdiv class=\"overlay\"\u003e\n      \u003cdiv id=\"input-L1\" class=\"input-group\"\u003e\n        \u003clabel for=\"L1\" class=\"tooltip vertical-icon\"\u003e\n          \u003cspan class=\"info-icon\"\u003eℹ️\u003c\/span\u003e\u003cspan\u003eL\u003csub\u003e1\u003c\/sub\u003e [mm]\u003c\/span\u003e\n          \u003cspan class=\"tooltiptext\"\u003e\u003cimg src=\"https:\/\/cdn.shopify.com\/s\/files\/1\/0899\/4816\/0342\/t\/6\/assets\/ConfiguratoreElastomeri_L1_Popup.jpg\" alt=\"L1\" style=\"width:100%;margin-bottom:8px\"\u003ePlease, define the distance between the pedal pivot point and the center of the pad\u003c\/span\u003e\n        \u003c\/label\u003e\n        \u003cinput type=\"number\" id=\"L1\" value=\"170\" min=\"1\" max=\"500\"\u003e\n      \u003c\/div\u003e\n\n      \u003cdiv id=\"input-L2\" class=\"input-group\"\u003e\n        \u003clabel for=\"L2\" class=\"tooltip vertical-icon\"\u003e\n          \u003cspan class=\"info-icon\"\u003eℹ️\u003c\/span\u003e\u003cspan\u003eL\u003csub\u003e2\u003c\/sub\u003e [mm]\u003c\/span\u003e\n          \u003cspan class=\"tooltiptext\"\u003e\u003cimg src=\"https:\/\/cdn.shopify.com\/s\/files\/1\/0899\/4816\/0342\/t\/6\/assets\/ConfiguratoreElastomeri_L2_Popup.jpg\" alt=\"L2\" style=\"width:100%;margin-bottom:8px\"\u003ePlease, define the distance from the pedal pivot to the elastomer axle pin\u003c\/span\u003e\n        \u003c\/label\u003e\n        \u003cinput type=\"number\" id=\"L2\" value=\"60\" min=\"1\" max=\"500\"\u003e\n      \u003c\/div\u003e\n\n      \u003cdiv id=\"ltotDisplay\" style=\"position:absolute; top:58%; left:35%; font-size:18px; background:none; padding:4px 8px; border:none; border-radius:5px; z-index:3; color:white;\"\u003e\n        \u003cspan class=\"tooltip vertical-icon\"\u003e\n          \u003cspan class=\"info-icon\"\u003eℹ️\u003c\/span\u003e\n          \u003cspan\u003e Total length [mm]: \u003cspan id=\"ltotValue\"\u003e0\u003c\/span\u003e \u003c\/span\u003e\n          \u003cspan class=\"tooltiptext\"\u003e\u003cimg src=\"https:\/\/cdn.shopify.com\/s\/files\/1\/0899\/4816\/0342\/t\/6\/assets\/ConfiguratoreElastomeri_Ltot_Popup.jpg\" alt=\"Ltot\" style=\"width:100%;margin-bottom:8px\"\u003ePay attention to the available length between the elastomer plates of your setup\u003c\/span\u003e\n        \u003c\/span\u003e\n        \u003cspan id=\"ltotWarningIcon\" style=\"display:none;color:red\"\u003e⚠️\u003c\/span\u003e\n      \u003c\/div\u003e\n\n      \u003cdiv id=\"constraintsBox\" style=\"position:absolute; top:51%; left:2%; z-index:2; font-size:18px; color:white; background:none; padding:0; border:none; border-radius:0; max-width:200px; height:auto;\"\u003e\n        \u003clabel for=\"holeFilter\" class=\"tooltip vertical-icon\"\u003e\n          \u003cspan class=\"info-icon\"\u003eℹ️\u003c\/span\u003e\u003cspan\u003eHole Diameter\u003c\/span\u003e\n          \u003cspan class=\"tooltiptext\"\u003e\u003cimg src=\"https:\/\/cdn.shopify.com\/s\/files\/1\/0899\/4816\/0342\/t\/6\/assets\/Configuratore_Elastomeri_HoleDIameter.jpg\" alt=\"Hole Diameter\" style=\"width:100%;margin-bottom:8px\"\u003ePlease, select the diameter of the elastomers internal hole according to the axis diameter where they will be placed\u003c\/span\u003e\n        \u003c\/label\u003e\n\n        \u003cselect id=\"holeFilter\"\u003e\n          \u003coption value=\"6\"\u003e6 mm\u003c\/option\u003e\n          \u003coption value=\"8\" selected\u003e8 mm\u003c\/option\u003e\n          \u003coption value=\"10\"\u003e10 mm\u003c\/option\u003e\n        \u003c\/select\u003e\n        \u003cbr\u003e\u003cbr\u003e\n      \u003c\/div\u003e\n\n      \u003cdiv class=\"select-container\" id=\"selectContainer\"\u003e\u003c\/div\u003e\n      \u003cdiv class=\"graph\"\u003e\u003ccanvas id=\"chart\"\u003e\u003c\/canvas\u003e\u003c\/div\u003e\n      \u003cdiv class=\"button-group\"\u003e\n        \u003cbutton class=\"btn-reset\" onclick=\"resetConfigurations()\"\u003eReset\u003c\/button\u003e\n        \u003cbutton class=\"btn-add tooltip\" onclick=\"saveConfiguration()\"\u003eAdd Configuration\n          \u003cspan class=\"tooltiptext\"\u003eSave the current configuration for later\u003c\/span\u003e\n        \u003c\/button\u003e\n        \u003cbutton class=\"btn-cart tooltip\" onclick=\"bulkAddToCart()\"\u003e🛒 Add Cart\u003c\/button\u003e\n      \u003c\/div\u003e\n\n      \u003cdiv class=\"saved-configs\" id=\"savedConfigsList\"\u003e\u003c\/div\u003e\n    \u003c\/div\u003e\n\n    \u003c!-- Modal --\u003e\n    \u003cdiv id=\"elastomerModal\"\u003e\n      \u003cdiv\u003e\n        \u003ch3\u003eElastomer Selection\u003c\/h3\u003e\n        \u003cimg id=\"elastomerPreview\" src=\"\" alt=\"Preview\" style=\"width:200%; max-height:200px; object-fit:contain; display:none; margin-bottom:1em;\"\u003e\n        \u003clabel\u003eCategory:\n          \u003cselect id=\"modalType\"\u003e\n            \u003coption\u003eLinear\u003c\/option\u003e\n            \u003coption\u003eProgressive\u003c\/option\u003e\n            \u003coption\u003eSpacer\u003c\/option\u003e\n          \u003c\/select\u003e\n        \u003c\/label\u003e\n        \u003cdiv id=\"stiffnessWrapper\"\u003e\n          \u003clabel\u003eStiffness:\n            \u003cselect id=\"modalStiffness\"\u003e\n              \u003coption\u003eSoft\u003c\/option\u003e\n              \u003coption\u003eMedium\u003c\/option\u003e\n              \u003coption\u003eHard\u003c\/option\u003e\n            \u003c\/select\u003e\n          \u003c\/label\u003e\n        \u003c\/div\u003e\n        \u003cdiv id=\"spacerCountWrapper\" style=\"display:none\"\u003e\n          \u003clabel\u003eNumber of Spacers (max 4):\n            \u003cinput type=\"number\" id=\"spacerCountInput\" min=\"1\" max=\"4\" value=\"1\"\u003e\n          \u003c\/label\u003e\n        \u003c\/div\u003e\n        \u003cdiv style=\"display:flex; gap:12px; margin-top:12px\"\u003e\n          \u003cbutton onclick=\"confirmElastomerSelection()\"\u003eConfirm\u003c\/button\u003e\n          \u003cbutton onclick=\"closeModal()\"\u003eDelete\u003c\/button\u003e\n        \u003c\/div\u003e\n      \u003c\/div\u003e\n    \u003c\/div\u003e\n\n    \u003cscript\u003e\n      \/* ===== Dati elastomeri ===== *\/\n      const elastomerData = {\n        El1:{SKU:\"ACC027_01AF\",Length:24,Type:\"Progressive\",Stiffness:\"Soft\",Mask:\"P-S\",ImageURL:\"https:\/\/cdn.shopify.com\/s\/files\/1\/0899\/4816\/0342\/t\/6\/assets\/Configuratore_Elastomeri_ACC027.jpg?v=1750670653\",F:[0,2,4,6,8,10,12,14,16,18,20,22,24,26,28,30,32,34,36,38,40,42,44,46,48,50,52,54,56,58,60,62,64,66,68,70,72,74,76,78,80,82,84,86,88,90,92,94,96,98,100],x:[0,1.24,1.7,2.28,3.02,3.98,4.99,5.82,6.45,6.95,7.36,7.72,8.03,8.32,8.57,8.8,9.02,9.22,9.41,9.59,9.76,9.92,10.08,10.22,10.37,10.5,10.63,10.76,10.88,11,11.12,11.23,11.34,11.44,11.55,11.65,11.75,11.84,11.94,12.03,12.12,12.21,12.29,12.38,12.46,12.54,12.62,12.7,12.78,12.85,12.93]},\n        El2:{SKU:\"ACC027_02AF\",Length:24,Type:\"Progressive\",Stiffness:\"Medium\",Mask:\"P-M\",ImageURL:\"https:\/\/cdn.shopify.com\/s\/files\/1\/0899\/4816\/0342\/t\/6\/assets\/Configuratore_Elastomeri_ACC027.jpg?v=1750670653\",F:[0,2,4,6,8,10,12,14,16,18,20,22,24,26,28,30,32,34,36,38,40,42,44,46,48,50,52,54,56,58,60,62,64,66,68,70,72,74,76,78,80,82,84,86,88,90,92,94,96,98,100],x:[0,1.09,1.43,1.81,2.23,2.7,3.23,3.8,4.37,4.93,5.44,5.9,6.31,6.68,7.01,7.3,7.58,7.83,8.07,8.29,8.49,8.69,8.87,9.04,9.21,9.37,9.52,9.67,9.81,9.95,10.08,10.21,10.33,10.45,10.56,10.68,10.79,10.89,11,11.1,11.2,11.3,11.39,11.49,11.58,11.67,11.75,11.84,11.92,12.01,12.09]},\n        El3:{SKU:\"ACC027_03AF\",Length:24,Type:\"Progressive\",Stiffness:\"Hard\",Mask:\"P-H\",ImageURL:\"https:\/\/cdn.shopify.com\/s\/files\/1\/0899\/4816\/0342\/t\/6\/assets\/Configuratore_Elastomeri_ACC027.jpg?v=1750670653\",F:[0,2,4,6,8,10,12,14,16,18,20,22,24,26,28,30,32,34,36,38,40,42,44,46,48,50,52,54,56,58,60,62,64,66,68,70,72,74,76,78,80,82,84,86,88,90,92,94,96,98,100],x:[0,1.28,1.6,1.94,2.31,2.71,3.13,3.58,4.03,4.47,4.9,5.3,5.68,6.02,6.34,6.64,6.91,7.17,7.41,7.64,7.85,8.05,8.24,8.42,8.59,8.76,8.92,9.07,9.22,9.36,9.5,9.63,9.76,9.88,10,10.12,10.23,10.34,10.45,10.56,10.66,10.76,10.86,10.95,11.05,11.14,11.23,11.32,11.41,11.49,11.58]},\n        El4:{SKU:\"ACC028_01AF\",Length:24,Type:\"Linear\",Stiffness:\"Soft\",Mask:\"L-S\",ImageURL:\"https:\/\/cdn.shopify.com\/s\/files\/1\/0899\/4816\/0342\/t\/6\/assets\/Configuratore_Elastomeri_ACC028.jpg?v=1750670667\",F:[0,2,4,6,8,10,12,14,16,18,20,22,24,26,28,30,32,34,36,38,40,42,44,46,48,50,52,54,56,58,60,62,64,66,68,70,72,74,76,78,80,82,84,86,88,90,92,94,96,98,100],x:[0,0.72,1.1,1.54,2.06,2.68,3.43,4.25,5.04,5.7,6.25,6.72,7.11,7.46,7.77,8.05,8.31,8.55,8.77,8.97,9.17,9.35,9.52,9.69,9.85,10,10.14,10.28,10.42,10.55,10.68,10.8,10.92,11.03,11.15,11.25,11.36,11.47,11.57,11.67,11.76,11.86,11.95,12.04,12.13,12.22,12.3,12.39,12.47,12.55,12.63]},\n        El5:{SKU:\"ACC028_02AF\",Length:24,Type:\"Linear\",Stiffness:\"Medium\",Mask:\"L-M\",ImageURL:\"https:\/\/cdn.shopify.com\/s\/files\/1\/0899\/4816\/0342\/t\/6\/assets\/Configuratore_Elastomeri_ACC028.jpg?v=1750670667\",F:[0,2,4,6,8,10,12,14,16,18,20,22,24,26,28,30,32,34,36,38,40,42,44,46,48,50,52,54,56,58,60,62,64,66,68,70,72,74,76,78,80,82,84,86,88,90,92,94,96,98,100],x:[0,0.83,1.12,1.43,1.77,2.13,2.54,2.97,3.43,3.92,4.41,4.89,5.34,5.76,6.15,6.51,6.83,7.13,7.41,7.67,7.91,8.13,8.35,8.55,8.74,8.92,9.09,9.26,9.42,9.57,9.72,9.86,10,10.13,10.26,10.39,10.51,10.63,10.74,10.85,10.96,11.07,11.17,11.27,11.37,11.47,11.56,11.66,11.75,11.84,11.93]},\n        El6:{SKU:\"ACC028_03AF\",Length:24,Type:\"Linear\",Stiffness:\"Hard\",Mask:\"L-H\",ImageURL:\"https:\/\/cdn.shopify.com\/s\/files\/1\/0899\/4816\/0342\/t\/6\/assets\/Configuratore_Elastomeri_ACC028.jpg?v=1750670667\",F:[0,2,4,6,8,10,12,14,16,18,20,22,24,26,28,30,32,34,36,38,40,42,44,46,48,50,52,54,56,58,60,62,64,66,68,70,72,74,76,78,80,82,84,86,88,90,92,94,96,98,100],x:[0,0.8,1.02,1.25,1.5,1.75,2.02,2.31,2.61,2.92,3.24,3.57,3.91,4.25,4.59,4.93,5.26,5.57,5.88,6.17,6.44,6.7,6.95,7.19,7.42,7.63,7.84,8.03,8.22,8.4,8.57,8.74,8.9,9.05,9.2,9.35,9.49,9.62,9.75,9.88,10.01,10.13,10.25,10.36,10.47,10.58,10.69,10.79,10.9,11,11.1]},\n      };\n\n      const accessoryMap = { 6:\"ACC029_01AP\", 8:\"ACC029_02AP\", 10:\"ACC029_03AP\" };\n      const spacerMap    = { 6:\"ACC030_01SP\", 8:\"ACC030_02SP\", 10:\"ACC030_03SP\" };\n\n      let _productData = null;\n      const PRODUCT_HANDLE_FALLBACK = 'elastomer-configurator-sim';\n\n      async function loadProductData() {\n        if (_productData) return _productData;\n        const handleFromUrl = (() =\u003e {\n          try { const m = location.pathname.match(\/\\\/products\\\/([^\\\/?#]+)\/); return m ? m[1] : null; } catch { return null; }\n        })();\n        const handle = handleFromUrl || PRODUCT_HANDLE_FALLBACK;\n        const res = await fetch(`\/products\/${handle}.js`, { credentials:'same-origin' });\n        if (!res.ok) throw new Error('Impossibile caricare il JSON del prodotto');\n        _productData = await res.json();\n        return _productData;\n      }\n\n      function matchHoleLabel(optionValue, holeMm) {\n        if (!optionValue) return false;\n        const norm = String(optionValue).replace(\/\\s+\/g,'').toLowerCase();\n        const candidates = [String(holeMm), `${holeMm}mm`, `${holeMm} mm`].map(s =\u003e s.replace(\/\\s+\/g,'').toLowerCase());\n        return candidates.includes(norm);\n      }\n\n      function findVariantIdByOptions(product, type, stiffness, holeMm) {\n        const variants = product.variants || [];\n        let v = variants.find(v =\u003e v.option1 === type \u0026\u0026 v.option2 === stiffness \u0026\u0026 matchHoleLabel(v.option3, holeMm));\n        if (v) return v.id;\n        v = variants.find(v =\u003e Array.isArray(v.options) \u0026\u0026 v.options[0] === type \u0026\u0026 v.options[1] === stiffness \u0026\u0026 matchHoleLabel(v.options[2], holeMm));\n        return v ? v.id : null;\n      }\n\n      function findElKeyBySKU(sku) {\n        return Object.keys(elastomerData).find(k =\u003e elastomerData[k].SKU === sku) || null;\n      }\n\n      async function resolveElastomerVariantIdBySKU(sku, holeMm) {\n        const pd = await loadProductData();\n        const elKey = findElKeyBySKU(sku);\n        if (!elKey) return null;\n        const e = elastomerData[elKey];\n        return findVariantIdByOptions(pd, e.Type, e.Stiffness, holeMm);\n      }\n      async function resolveHoleAdapterVariantId(holeMm) {\n        const pd = await loadProductData();\n        return findVariantIdByOptions(pd, 'HoleAdapter', 'Soft', holeMm);\n      }\n      async function resolveSpacerVariantId(holeMm) {\n        const pd = await loadProductData();\n        return findVariantIdByOptions(pd, 'Spacer', 'Soft', holeMm);\n      }\n\n      let chart;\n      let elastomerCount = 1;\n      let savedConfigs = [];\n      let elastomerSelections = Array(4).fill(null);\n      let activeModalIndex = 0;\n      let selectedConfigs = [];\n\n      function updateSelectors() {\n        const container = document.getElementById(\"selectContainer\");\n        container.innerHTML = \"\";\n        for (let i = 0; i \u003c elastomerCount; i++) {\n          const btn = document.createElement(\"button\");\n          btn.className = \"elastomer-slot\";\n\n          if (elastomerSelections[i]) {\n            const selection = elastomerSelections[i];\n            if (typeof selection === \"object\" \u0026\u0026 selection.spacer) {\n              btn.textContent = `Spacer x${selection.count}`;\n            } else {\n              const elData = elastomerData[selection];\n              btn.textContent = elData?.Mask || selection;\n            }\n          } else {\n            btn.className = \"elastomer-slot tooltip\";\n            btn.innerHTML = `Select\u003cspan class=\"tooltiptext\"\u003ePlease select the elastomer to compute the force\u003c\/span\u003e`;\n          }\n\n          btn.onclick = () =\u003e openModal(i);\n          container.appendChild(btn);\n        }\n\n        const subBtn = document.createElement(\"button\");\n        subBtn.className = \"tooltip elastomer-control\";\n        subBtn.innerHTML = '−\u003cspan class=\"tooltiptext\"\u003eRemove one elastomer\u003c\/span\u003e';\n        subBtn.onclick = () =\u003e {\n          if (elastomerCount \u003e 1) {\n            elastomerCount--;\n            elastomerSelections[elastomerCount] = null;\n            updateSelectors();\n            updateCurrent();\n          }\n        };\n\n        const addBtn = document.createElement(\"button\");\n        addBtn.className = \"tooltip elastomer-control\";\n        addBtn.innerHTML = '+\u003cspan class=\"tooltiptext\"\u003eAdd another elastomer\u003c\/span\u003e';\n        addBtn.onclick = () =\u003e {\n          if (elastomerCount \u003c 4) {\n            elastomerCount++;\n            updateSelectors();\n            updateCurrent();\n          }\n        };\n\n        container.appendChild(subBtn);\n        container.appendChild(addBtn);\n\n        const bgImg = document.querySelector(\".background\");\n        if (bgImg) bgImg.src = `https:\/\/cdn.shopify.com\/s\/files\/1\/0899\/4816\/0342\/t\/6\/assets\/ConfiguratoreElastomeri_${elastomerCount}El.png`;\n\n        updateLtot();\n      }\n\n      function openModal(index){ activeModalIndex = index; document.getElementById(\"elastomerModal\").style.display = \"block\"; refreshModalOptions(); }\n      function closeModal(){ document.getElementById(\"elastomerModal\").style.display = \"none\"; }\n\n      function refreshModalOptions() {\n        const type = document.getElementById(\"modalType\").value;\n        const stiffnessWrapper = document.getElementById(\"stiffnessWrapper\");\n        const spacerWrapper = document.getElementById(\"spacerCountWrapper\");\n\n        if (type === \"Spacer\") {\n          stiffnessWrapper.style.display = \"none\";\n          spacerWrapper.style.display = \"block\";\n          const previewImg = document.getElementById(\"elastomerPreview\");\n          previewImg.src = \"https:\/\/cdn.shopify.com\/s\/files\/1\/0899\/4816\/0342\/t\/6\/assets\/Configuratore_Elastomeri_ACC030.jpg?v=1750672880\";\n          previewImg.style.display = \"block\";\n          window._modalSelectedElastomer = \"SPACER\";\n        } else {\n          stiffnessWrapper.style.display = \"block\";\n          spacerWrapper.style.display = \"none\";\n          const stiffness = document.getElementById(\"modalStiffness\").value;\n          const filtered = Object.entries(elastomerData).filter(([, e]) =\u003e e.Type === type \u0026\u0026 e.Stiffness === stiffness);\n          const previewImg = document.getElementById(\"elastomerPreview\");\n          if (filtered.length \u003e 0) {\n            const selectedKey = filtered[0][0];\n            const imgUrl = elastomerData[selectedKey]?.ImageURL;\n            if (imgUrl) { previewImg.src = imgUrl; previewImg.style.display = \"block\"; }\n            else previewImg.style.display = \"none\";\n            window._modalSelectedElastomer = selectedKey;\n          } else {\n            previewImg.style.display = \"none\";\n            window._modalSelectedElastomer = null;\n          }\n        }\n      }\n      document.getElementById(\"modalType\").addEventListener(\"change\", refreshModalOptions);\n      document.getElementById(\"modalStiffness\").addEventListener(\"change\", refreshModalOptions);\n\n      function confirmElastomerSelection() {\n        const type = document.getElementById(\"modalType\").value;\n        if (type === \"Spacer\") {\n          let count = parseInt(document.getElementById(\"spacerCountInput\").value);\n          if (isNaN(count) || count \u003c 1) count = 1;\n          if (count \u003e 4) { alert(\"⚠️ You can add up to 4 spacers maximum.\"); count = 4; document.getElementById(\"spacerCountInput\").value = 4; }\n          elastomerSelections[activeModalIndex] = { spacer:true, count };\n        } else {\n          const selected = window._modalSelectedElastomer;\n          if (!selected) { alert(\"Elastomer not found in database.\"); return; }\n          elastomerSelections[activeModalIndex] = selected;\n        }\n        closeModal();\n        updateSelectors();\n        updateCurrent();\n        updateLtot();\n      }\n\n      function generateSeriesData(selected) {\n        const L1 = parseFloat(document.getElementById(\"L1\").value);\n        const L2 = parseFloat(document.getElementById(\"L2\").value);\n        const RL = L1 \/ L2;\n        const Fmax = 100 \/ RL;\n        const data = [];\n        for (let F = 0; F \u003c= Fmax; F += 0.5) {\n          const Fk = F * RL;\n          let xKtot = 0;\n          for (const elKey of selected) {\n            if (typeof elKey === \"object\" \u0026\u0026 elKey.spacer) continue;\n            const e = elastomerData[elKey]; if (!e) continue;\n            xKtot += interpolate(Fk, e.F, e.x);\n          }\n          data.push({ x: xKtot * RL, y: F });\n        }\n        return data;\n      }\n\n      function interpolate(x, xArray, yArray) {\n        if (x \u003c= xArray[0]) return yArray[0];\n        if (x \u003e= xArray[xArray.length - 1]) return yArray[yArray.length - 1];\n        for (let i=0;i\u003cxArray.length-1;i++) {\n          if (xArray[i] \u003c= x \u0026\u0026 x \u003c= xArray[i+1]) {\n            const x0=xArray[i], x1=xArray[i+1], y0=yArray[i], y1=yArray[i+1];\n            return y0 + ((y1-y0)*(x-x0))\/(x1-x0);\n          }\n        }\n        return 0;\n      }\n\n      function updateCurrent() {\n        const selected = elastomerSelections.slice(0, elastomerCount);\n        const allDefined = selected.length === elastomerCount \u0026\u0026 selected.every(Boolean);\n\n        const addButton = document.querySelector(\".btn-add\");\n        if (addButton) {\n          addButton.disabled = !allDefined;\n          addButton.style.opacity = allDefined ? \"1\" : \"0.5\";\n          addButton.style.pointerEvents = allDefined ? \"auto\" : \"none\";\n          addButton.title = allDefined ? \"\" : \"Please select all elastomers before adding a configuration.\";\n        }\n\n        if (!allDefined) {\n          const datasets = [...savedConfigs];\n          if (datasets.length === 0) {\n            datasets.unshift({ label:\"Please select all elastomers to generate the curve\", data:[], borderColor:\"gray\", borderWidth:1, pointRadius:0, pointHoverRadius:0 });\n          }\n          drawChart(datasets);\n          updateLtot();\n          return;\n        }\n\n        const data = generateSeriesData(selected);\n        drawChart([{ label:\"Current\", data, borderColor:\"deepskyblue\" }, ...savedConfigs]);\n\n        const hasRealElastomers = selected.some(el =\u003e typeof el !== \"object\" || !el.spacer);\n        if (!hasRealElastomers) {\n          drawChart([{ label:\"Only spacers selected – no force curve\", data:[], borderColor:\"gray\" }, ...savedConfigs]);\n          return;\n        }\n        updateLtot();\n      }\n\n      function saveConfiguration() {\n        const selected = elastomerSelections.slice(0, elastomerCount).filter(Boolean);\n        if (selected.length === 0) { alert(\"Please, select at least one elastomer to create a configuration.\"); return; }\n        if (savedConfigs.length \u003e= 4) { alert(\"Warning! A maximum of 4 elastomers can be placed.\"); return; }\n\n        const data = generateSeriesData(selected);\n        const id = Date.now();\n        const label = `Config_${savedConfigs.length + 1}`;\n        const color = `hsl(${(savedConfigs.length * 60) % 360}, 70%, 50%)`;\n        const skus = selected.map((key) =\u003e (typeof key === \"object\" \u0026\u0026 key.spacer) ? { spacer:true, count:key.count } : elastomerData[key].SKU);\n\n        savedConfigs.push({ label, data, borderColor: color, id, skus });\n\n        elastomerCount = 1;\n        elastomerSelections = Array(4).fill(null);\n        updateSelectors();\n        updateCurrent();\n        updateLtot();\n        renderSavedConfigs();\n      }\n\n      function resetConfigurations() {\n        savedConfigs = [];\n        selectedConfigs = [];\n        elastomerCount = 1;\n        elastomerSelections = Array(4).fill(null);\n        updateSelectors();\n        updateCurrent();\n        updateLtot();\n        renderSavedConfigs();\n        updateCartAndDownloadButtons();\n      }\n\n      function renderSavedConfigs() {\n        const container = document.getElementById(\"savedConfigsList\");\n        container.innerHTML = \"\";\n\n        savedConfigs.forEach((cfg) =\u003e {\n          const div = document.createElement(\"div\");\n          div.style.marginBottom = \"10px\";\n\n          const row = document.createElement(\"div\");\n          row.style.display = \"flex\";\n          row.style.alignItems = \"center\";\n          row.style.justifyContent = \"center\";\n          row.style.gap = \"10px\";\n\n          const label = document.createElement(\"span\");\n          label.textContent = `${cfg.label}`;\n          label.style.fontWeight = \"bold\";\n          label.style.color = cfg.borderColor;\n          label.className = \"config-label\";\n\n          const chooseBtn = document.createElement(\"button\");\n          chooseBtn.innerHTML = \"✅\u003cbr\u003eChoose\";\n          chooseBtn.className = \"btn-choose\";\n          chooseBtn.style.backgroundColor = \"#d9ff00\";\n          chooseBtn.style.color = \"black\";\n\n          const isAlreadySelected = selectedConfigs.some((sel) =\u003e sel.id === cfg.id);\n          if (isAlreadySelected) {\n            chooseBtn.disabled = true;\n            chooseBtn.style.opacity = \"0.5\";\n            chooseBtn.style.pointerEvents = \"none\";\n            chooseBtn.title = \"Already selected\";\n          }\n\n          chooseBtn.onclick = () =\u003e {\n            if (!isAlreadySelected) {\n              selectedConfigs.push(cfg);\n              chooseBtn.disabled = true;\n              chooseBtn.style.opacity = \"0.5\";\n              chooseBtn.style.pointerEvents = \"none\";\n              chooseBtn.title = \"Selected\";\n              updateCartAndDownloadButtons();\n            }\n          };\n\n          const delBtn = document.createElement(\"button\");\n          delBtn.className = \"btn-delete\";\n          delBtn.innerHTML = \"🗑️\u003cbr\u003eDelete\";\n          delBtn.onclick = () =\u003e {\n            savedConfigs = savedConfigs.filter((c) =\u003e c.id !== cfg.id);\n            selectedConfigs = selectedConfigs.filter((c) =\u003e c.id !== cfg.id);\n            updateCurrent();\n            renderSavedConfigs();\n            updateCartAndDownloadButtons();\n          };\n\n          row.appendChild(label);\n          row.appendChild(chooseBtn);\n          row.appendChild(delBtn);\n          div.appendChild(row);\n          container.appendChild(div);\n        });\n      }\n\n      \/* ====== Aggiunta al carrello: QUEUE + BATCH + RETRY ====== *\/\n      let __cartQueue = Promise.resolve();\n      function enqueueCartRequest(task){\n        __cartQueue = __cartQueue.then(() =\u003e task()).catch(() =\u003e task());\n        return __cartQueue;\n      }\n      function sleep(ms){ return new Promise(r=\u003esetTimeout(r,ms)); }\n      function parseRetryAfter(res){\n        const h = res.headers?.get?.('Retry-After');\n        const n = h ? parseFloat(h) : NaN;\n        return Number.isFinite(n) ? Math.max(0, n*1000) : null;\n      }\n      async function addItemsToCartBatch(items, { retries=6, baseDelay=1200 } = {}){\n        const byId = {};\n        for (const it of items){\n          const key = String(it.id);\n          if (!byId[key]) byId[key] = { id: it.id, quantity: 0 };\n          byId[key].quantity += Number(it.quantity || 1);\n        }\n        const payload = Object.values(byId);\n\n        return enqueueCartRequest(async () =\u003e {\n          let lastTxt = '';\n          for (let attempt=0; attempt\u003c=retries; attempt++){\n            const res = await fetch('\/cart\/add.js', {\n              method:'POST',\n              headers:{ 'Content-Type':'application\/json', 'Accept':'application\/json' },\n              body: JSON.stringify({ items: payload })\n            });\n\n            if (res.ok) return res.json();\n\n            lastTxt = await res.text().catch(()=\u003e '');\n            const shouldRetry = res.status === 429 || \/too[_ ]?many[_ ]?requests|throttled\/i.test(lastTxt);\n            if (shouldRetry \u0026\u0026 attempt \u003c retries){\n              const retryAfter = parseRetryAfter(res);\n              const jitter = Math.floor(Math.random()*250);\n              const delay = (retryAfter ?? (baseDelay * Math.pow(2, attempt))) + jitter;\n              await sleep(delay);\n              continue;\n            }\n            throw new Error(`Cart add failed: ${lastTxt || `HTTP ${res.status}`}`);\n          }\n          throw new Error('Cart add failed: throttled (too many attempts)');\n        });\n      }\n      async function addItemToCart(variantId, quantity, opts){\n        return addItemsToCartBatch([{ id: variantId, quantity }], opts);\n      }\n\n      function drawChart(datasets) {\n        const ctx = document.getElementById(\"chart\").getContext(\"2d\");\n        if (chart) chart.destroy();\n\n        if (!datasets || datasets.length === 0) {\n          datasets = [{ label:\"Please, add at least one elastomer\", data:[], borderColor:\"gray\", borderWidth:1, pointRadius:0, pointHoverRadius:0 }];\n        }\n\n        const L1 = parseFloat(document.getElementById(\"L1\").value);\n        const L2 = parseFloat(document.getElementById(\"L2\").value);\n        const RL = L1 \/ L2;\n        const Fmax = 100 \/ RL;\n\n        chart = new Chart(ctx, {\n          type:\"line\",\n          data:{ datasets },\n          options:{\n            responsive:true, maintainAspectRatio:false,\n            plugins:{ legend:{ display:true, labels:{ color:\"#fff\", font:{ size:18 }}} },\n            scales:{\n              x:{ type:\"linear\", min:0, title:{ display:true, text:\"Displacement\", color:\"#fff\", font:{ size:18 }}, grid:{ color:\"#3A3A38\" }, ticks:{ color:\"#252525\" } },\n              y:{ min:0, max:Fmax+5, title:{ display:true, text:\"Force\", color:\"#fff\", font:{ size:18 }}, grid:{ color:\"#3A3A38\" }, ticks:{ color:\"#252525\" } },\n            }\n          }\n        });\n      }\n\n      function downloadSummaryPNG() {\n        const scale = 2;\n        const canvas = document.createElement(\"canvas\");\n        canvas.width = 1000 * scale; canvas.height = 1800 * scale;\n        const ctx = canvas.getContext(\"2d\"); ctx.scale(scale, scale);\n\n        ctx.fillStyle = \"#000\"; ctx.fillRect(0,0,canvas.width,canvas.height);\n        ctx.fillStyle = \"#fff\"; ctx.font = \"32px sans-serif\";\n        const title = \"Elastomer Configurations Report\";\n        const textWidth = ctx.measureText(title).width;\n        ctx.fillText(title, (canvas.width\/scale - textWidth)\/2, 40);\n\n        const chartCanvas = document.getElementById(\"chart\");\n        ctx.drawImage(chartCanvas, 50, 80, 900, 900);\n\n        const tableX = 40, tableY = 1100, rowHeight = 42, colWidth = 180, maxCols = 5;\n        ctx.font = \"20px sans-serif\"; ctx.fillStyle = \"#fff\";\n        ctx.fillText(\"Configurations Table:\", tableX, tableY - 10);\n\n        const headers = [\"Config\",\"Elastomer 1\",\"Elastomer 2\",\"Elastomer 3\",\"Elastomer 4\"];\n        headers.forEach((text,i)=\u003e{\n          const x = tableX + i*colWidth;\n          ctx.fillStyle=\"#222\"; ctx.fillRect(x,tableY,colWidth,rowHeight);\n          ctx.strokeStyle=\"#666\"; ctx.strokeRect(x,tableY,colWidth,rowHeight);\n          ctx.fillStyle=\"#fff\"; ctx.fillText(text, x+10, tableY+26);\n        });\n\n        const sortedConfigs = [...selectedConfigs].sort((a,b)=\u003eparseInt(a.label.replace(\"Config_\",\"\")) - parseInt(b.label.replace(\"Config_\",\"\")));\n        sortedConfigs.forEach((cfg,rowIdx)=\u003e{\n          const y = tableY + (rowIdx+1)*rowHeight;\n          const types = cfg.skus.map((skuOrObj)=\u003e{\n            if (typeof skuOrObj === \"object\" \u0026\u0026 skuOrObj.spacer) return `Spacer x${skuOrObj.count}`;\n            const key = Object.keys(elastomerData).find(k =\u003e elastomerData[k].SKU === skuOrObj);\n            const e = elastomerData[key]; return e ? `${e.Type} (${e.Stiffness})` : \"-\";\n          });\n          for (let col=0; col\u003cmaxCols; col++){\n            const x = tableX + col*colWidth;\n            const content = col===0 ? cfg.label.replace(\"Config_\",\"\") : (types[col-1] || \"\");\n            ctx.fillStyle = rowIdx%2===0 ? \"#111\" : \"#000\";\n            ctx.fillRect(x,y,colWidth,rowHeight);\n            ctx.strokeStyle=\"#333\"; ctx.strokeRect(x,y,colWidth,rowHeight);\n            ctx.fillStyle=\"#fff\"; ctx.fillText(content, x+10, y+26);\n          }\n        });\n\n        const logo = new Image(); logo.crossOrigin = \"anonymous\";\n        logo.src = \"https:\/\/8g7gcbwbtmz8zbiu-89948160342.shopifypreview.com\/cdn\/shop\/files\/newlogo3drap_simracing_testo.png?v=1742814361\u0026width=180\";\n        logo.onload = ()=\u003e{\n          const logoWidth=360, logoHeight=120, x = canvas.width\/scale - logoWidth - 30, y = canvas.height\/scale - logoHeight - 30;\n          ctx.drawImage(logo,x,y,logoWidth,logoHeight);\n          const link = document.createElement(\"a\"); link.download = \"Elastomer_Report.png\"; link.href = canvas.toDataURL(\"image\/png\"); link.click();\n        };\n        logo.onerror = ()=\u003e alert(\"⚠️ Logo non caricato. Verifica il link.\");\n      }\n\n      function updateLtot() {\n        const selected = elastomerSelections.slice(0, elastomerCount);\n        const allDefined = selected.every(Boolean);\n        const ltotValue = document.getElementById(\"ltotValue\");\n        const warningIcon = document.getElementById(\"ltotWarningIcon\");\n        const ltotDisplay = document.getElementById(\"ltotDisplay\");\n\n        if (!allDefined) { ltotValue.textContent = \"\"; warningIcon.style.display=\"none\"; ltotDisplay.style.color=\"white\"; ltotDisplay.classList.remove(\"warned\"); return; }\n\n        const ltot = selected.reduce((sum, item)=\u003e{\n          if (typeof item === \"object\" \u0026\u0026 item.spacer) return sum + item.count * 5;\n          const el = elastomerData[item]; return sum + (el?.Length || 0);\n        }, 0);\n\n        ltotValue.textContent = ltot; warningIcon.style.display=\"none\"; ltotDisplay.style.color=\"white\"; ltotDisplay.classList.remove(\"warned\");\n      }\n\n      function updateCartAndDownloadButtons() {\n        const enabled = selectedConfigs.length \u003e 0;\n        [{ selector: \".btn-cart\", tooltip: \"Please choose at least one configuration to add to cart.\" },\n         { selector: \".btn-download\", tooltip: \"Please choose at least one configuration to download summary.\" }]\n        .forEach(({ selector, tooltip }) =\u003e {\n          const btn = document.querySelector(selector);\n          if (btn) { btn.disabled = !enabled; btn.style.opacity = enabled ? \"1\" : \"0.5\"; btn.style.pointerEvents = enabled ? \"auto\" : \"none\"; btn.title = enabled ? \"\" : tooltip; }\n        });\n      }\n\n      \/* ====== bulkAddToCart: una sola chiamata \/cart\/add.js ====== *\/\n      let __bulkRunning = false;\n      async function bulkAddToCart() {\n        if (__bulkRunning) return;\n        __bulkRunning = true;\n\n        if (selectedConfigs.length === 0) { alert(\"Please choose at least one configuration.\"); __bulkRunning = false; return; }\n\n        const selectedIds = new Set(selectedConfigs.map((cfg) =\u003e cfg.id));\n        savedConfigs = savedConfigs.filter((cfg) =\u003e selectedIds.has(cfg.id));\n        updateCurrent();\n        renderSavedConfigs();\n\n        const matrix = selectedConfigs.map((cfg) =\u003e {\n          const count = {};\n          for (const sku of cfg.skus) {\n            if (typeof sku === \"object\" \u0026\u0026 sku.spacer) continue;\n            count[sku] = (count[sku] || 0) + 1;\n          }\n          return count;\n        });\n\n        const finalSkuCounts = {};\n        for (const sku of new Set(matrix.flatMap((cfg) =\u003e Object.keys(cfg)))) {\n          finalSkuCounts[sku] = Math.max(...matrix.map((cfg) =\u003e cfg[sku] || 0));\n        }\n\n        const hole = parseInt(document.getElementById(\"holeFilter\").value, 10);\n\n        const totalElastomers = Object.values(finalSkuCounts).reduce((acc, qty) =\u003e acc + qty, 0);\n        const accessorySku = accessoryMap[hole];\n        const needsHoleAdapterQty = totalElastomers * 2;\n\n        const spacerPerPosition = [0, 0, 0, 0];\n        for (const cfg of selectedConfigs) {\n          cfg.skus.forEach((skuOrObj, index) =\u003e {\n            if (typeof skuOrObj === \"object\" \u0026\u0026 skuOrObj.spacer) {\n              spacerPerPosition[index] = Math.max(spacerPerPosition[index], skuOrObj.count);\n            }\n          });\n        }\n        const totalSpacers = spacerPerPosition.reduce((a,b)=\u003ea+b,0);\n        const spacerSku = spacerMap[hole];\n\n        const items = [];\n        for (const [sku, qty] of Object.entries(finalSkuCounts)) {\n          const variantId = await resolveElastomerVariantIdBySKU(sku, hole);\n          if (!variantId) { alert(`❌ Nessuna variante trovata per ${sku} (${hole} mm).`); __bulkRunning = false; return; }\n          items.push({ id: variantId, quantity: qty });\n        }\n        if (accessorySku \u0026\u0026 needsHoleAdapterQty \u003e 0) {\n          const holeAdapterId = await resolveHoleAdapterVariantId(hole);\n          if (!holeAdapterId) { alert(\"❌ Nessuna variante trovata per HoleAdapter.\"); __bulkRunning = false; return; }\n          items.push({ id: holeAdapterId, quantity: needsHoleAdapterQty });\n        }\n        if (spacerSku \u0026\u0026 totalSpacers \u003e 0) {\n          const spacerId = await resolveSpacerVariantId(hole);\n          if (!spacerId) { alert(\"❌ Nessuna variante trovata per Spacer.\"); __bulkRunning = false; return; }\n          items.push({ id: spacerId, quantity: totalSpacers });\n        }\n\n        const cartBtn = document.querySelector('.btn-cart');\n        if (cartBtn) { cartBtn.disabled = true; cartBtn.style.opacity = '0.5'; cartBtn.textContent = '🛒 Adding…'; }\n\n        try {\n          await addItemsToCartBatch(items, { retries: 6, baseDelay: 1200 });\n          await window.refreshCartUI(true);\n\n          setTimeout(() =\u003e {\n            const confirmDownload = confirm(\"Would you download the choosed configuration on your device?\");\n            if (confirmDownload) downloadSummaryPNG();\n            selectedConfigs = [];\n          }, 100);\n        } catch (err) {\n          console.error(err);\n          alert(\"❌ Error adding items to cart.\\n\" + (err?.message || \"\"));\n        } finally {\n          if (cartBtn) { cartBtn.disabled = false; cartBtn.style.opacity = '1'; cartBtn.textContent = '🛒 Add Cart'; }\n          __bulkRunning = false;\n        }\n      }\n\n      \/\/ Intercetta QUALSIASI submit di form \/cart\/add e usa la nostra API batch\n      function onCartAddSubmit(e) {\n        const form = e.target.closest('form[action=\"\/cart\/add\"]');\n        if (!form) return;\n        e.preventDefault();\n        e.stopPropagation();\n        try {\n          const fd = new FormData(form);\n          const id = fd.get('id');\n          const qty = parseInt(fd.get('quantity') || '1', 10) || 1;\n          if (!id) { alert('Variant ID mancante.'); return; }\n          addItemToCart(id, qty).then(() =\u003e window.refreshCartUI(true)).catch(err =\u003e {\n            console.error(err); alert('❌ Error adding item to cart.');\n          });\n        } catch (err) {\n          console.error(err); alert('❌ Error adding item to cart.');\n        }\n      }\n      document.addEventListener('submit', onCartAddSubmit, true);\n\n      \/\/ Init UI\n      updateSelectors();\n      updateCurrent();\n      updateCartAndDownloadButtons();\n\n      document.getElementById(\"L1\").addEventListener(\"input\", () =\u003e {\n        const input = document.getElementById(\"L1\");\n        let value = parseFloat(input.value);\n        if (isNaN(value)) value = 1;\n        if (value \u003c 1) { value = 1; alert(\"⚠️ Minimum L1 is 1 mm.\"); }\n        else if (value \u003e 500) { value = 500; alert(\"⚠️ Maximum L2 is 500 mm.\"); }\n        input.value = value; updateCurrent();\n      });\n\n      document.getElementById(\"L2\").addEventListener(\"input\", () =\u003e {\n        const input = document.getElementById(\"L2\");\n        let value = parseFloat(input.value);\n        if (isNaN(value)) value = 1;\n        if (value \u003c 1) { value = 1; alert(\"⚠️ Minimum L2 is 1 mm.\"); }\n        else if (value \u003e 500) { value = 500; alert(\"⚠️ Maximum L2 is 500 mm.\"); }\n        input.value = value; updateCurrent();\n      });\n\n      document.getElementById(\"holeFilter\").addEventListener(\"change\", updateSelectors);\n    \u003c\/script\u003e\n\n    \u003c!-- ============== PATCH PDP: intercetta Add to cart del tema e forza \/cart\/add.js ============== --\u003e\n    \u003cscript\u003e\n      function getSelectedValueByName(name) {\n        const sel = document.querySelector(`input[name=\"options[${name}]\"]:checked`);\n        if (sel \u0026\u0026 sel.value) return sel.value;\n\n        const btn = Array.from(document.querySelectorAll(`[data-options] button, .product-form__input button`))\n          .find(b =\u003e (b.getAttribute('name') === `options[${name}]` || b.closest(`[data-option-name=\"${name}\"]`)) \u0026\u0026 b.getAttribute('aria-pressed') === 'true');\n        if (btn) return (btn.value || btn.textContent || '').trim();\n\n        const badge = document.querySelector(`.product-form__input [data-option-name=\"${name}\"] .is-active, .product-form__input .is-selected[data-option-name=\"${name}\"]`);\n        if (badge) return (badge.getAttribute('data-value') || badge.textContent || '').trim();\n        return null;\n      }\n\n      function getCurrentPdpSelection() {\n        const type = getSelectedValueByName('Type') || getSelectedValueByName('type') || 'Linear';\n        const stiffness = getSelectedValueByName('Stiffness') || getSelectedValueByName('stiffness') || 'Soft';\n        const hole = getSelectedValueByName('HoleDiameter') || getSelectedValueByName('holediameter') || '8mm';\n        const holeMm = parseInt(String(hole).replace(\/\\D+\/g, ''), 10) || 8;\n        return { type, stiffness, holeMm };\n      }\n\n      function getCurrentQty() {\n        const qtyInput = document.querySelector('input[name=\"quantity\"]');\n        const v = parseInt(qtyInput \u0026\u0026 qtyInput.value, 10);\n        return Number.isFinite(v) \u0026\u0026 v \u003e 0 ? v : 1;\n      }\n\n      async function interceptPdpAddToCart(e) {\n        const btn = e.target.closest('button[type=\"submit\"][name=\"add\"]');\n        const form = btn \u0026\u0026 btn.closest('form[action=\"\/cart\/add\"]');\n        if (!form) return;\n\n        e.preventDefault();\n        e.stopPropagation();\n\n        try {\n          const { type, stiffness, holeMm } = getCurrentPdpSelection();\n          const pd = await loadProductData();\n          const variantId = findVariantIdByOptions(pd, type, stiffness, holeMm);\n          if (!variantId) { alert(`❌ Variante non trovata per ${type} \/ ${stiffness} \/ ${holeMm}mm`); return; }\n          const qty = getCurrentQty();\n\n          await addItemToCart(variantId, qty);\n          await window.refreshCartUI(true);\n\n        } catch (err) {\n          console.error(err);\n          alert('❌ Error adding item to cart.\\n' + (err?.message || ''));\n        }\n      }\n\n      function enablePdpIntercept() {\n        document.addEventListener('click', interceptPdpAddToCart, true);\n      }\n      if (document.readyState === 'loading') {\n        document.addEventListener('DOMContentLoaded', enablePdpIntercept);\n      } else {\n        enablePdpIntercept();\n      }\n    \u003c\/script\u003e\n  \u003c\/div\u003e\n\u003c\/body\u003e","brand":"3DRap Sim Racing","offers":[{"title":"Linear \/ Soft \/ 6mm","offer_id":50492217262422,"sku":"ACC028_01","price":19.9,"currency_code":"EUR","in_stock":true},{"title":"Linear \/ Soft \/ 8mm","offer_id":50653547135318,"sku":"ACC028_01","price":19.9,"currency_code":"EUR","in_stock":true},{"title":"Linear \/ Soft \/ 10mm","offer_id":50653547168086,"sku":"ACC028_01","price":19.9,"currency_code":"EUR","in_stock":true},{"title":"Linear \/ Medium \/ 6mm","offer_id":50492217295190,"sku":"ACC028_02","price":19.9,"currency_code":"EUR","in_stock":true},{"title":"Linear \/ Medium \/ 8mm","offer_id":50653547200854,"sku":"ACC028_02","price":19.9,"currency_code":"EUR","in_stock":true},{"title":"Linear \/ Medium \/ 10mm","offer_id":50653547233622,"sku":"ACC028_02","price":19.9,"currency_code":"EUR","in_stock":true},{"title":"Linear \/ Hard \/ 6mm","offer_id":50492217327958,"sku":"ACC028_03","price":19.9,"currency_code":"EUR","in_stock":true},{"title":"Linear \/ Hard \/ 8mm","offer_id":50653547266390,"sku":"ACC028_03","price":19.9,"currency_code":"EUR","in_stock":true},{"title":"Linear \/ Hard \/ 10mm","offer_id":50653547299158,"sku":"ACC028_03","price":19.9,"currency_code":"EUR","in_stock":true},{"title":"Progressive \/ Soft \/ 6mm","offer_id":50492217360726,"sku":"ACC027_01","price":19.9,"currency_code":"EUR","in_stock":true},{"title":"Progressive \/ Soft \/ 8mm","offer_id":50653547331926,"sku":"ACC027_01","price":19.9,"currency_code":"EUR","in_stock":true},{"title":"Progressive \/ Soft \/ 10mm","offer_id":50653547364694,"sku":"ACC027_01","price":19.9,"currency_code":"EUR","in_stock":true},{"title":"Progressive \/ Medium \/ 6mm","offer_id":50492217393494,"sku":"ACC027_02","price":19.9,"currency_code":"EUR","in_stock":true},{"title":"Progressive \/ Medium \/ 8mm","offer_id":50653547397462,"sku":"ACC027_02","price":19.9,"currency_code":"EUR","in_stock":true},{"title":"Progressive \/ Medium \/ 10mm","offer_id":50653547430230,"sku":"ACC027_02","price":19.9,"currency_code":"EUR","in_stock":true},{"title":"Progressive \/ Hard \/ 6mm","offer_id":50492217426262,"sku":"ACC027_03","price":19.9,"currency_code":"EUR","in_stock":true},{"title":"Progressive \/ Hard \/ 8mm","offer_id":50653547462998,"sku":"ACC027_03","price":19.9,"currency_code":"EUR","in_stock":true},{"title":"Progressive \/ Hard \/ 10mm","offer_id":50653547495766,"sku":"ACC027_03","price":19.9,"currency_code":"EUR","in_stock":true},{"title":"Spacer \/ Soft \/ 6mm","offer_id":50653538681174,"sku":"ACC030_01","price":1.0,"currency_code":"EUR","in_stock":true},{"title":"Spacer \/ Soft \/ 8mm","offer_id":50653547528534,"sku":"ACC030_02","price":1.0,"currency_code":"EUR","in_stock":true},{"title":"Spacer \/ Soft \/ 10mm","offer_id":50653547561302,"sku":"ACC030_03","price":1.0,"currency_code":"EUR","in_stock":true},{"title":"Spacer \/ Medium \/ 6mm","offer_id":50653538713942,"sku":"ACC030_01","price":1.0,"currency_code":"EUR","in_stock":true},{"title":"Spacer \/ Medium \/ 8mm","offer_id":50653547594070,"sku":"ACC030_02","price":1.0,"currency_code":"EUR","in_stock":true},{"title":"Spacer \/ Medium \/ 10mm","offer_id":50653547626838,"sku":"ACC030_03","price":1.0,"currency_code":"EUR","in_stock":true},{"title":"Spacer \/ Hard \/ 6mm","offer_id":50653538746710,"sku":"ACC030_01","price":1.0,"currency_code":"EUR","in_stock":true},{"title":"Spacer \/ Hard \/ 8mm","offer_id":50653547659606,"sku":"ACC030_02","price":1.0,"currency_code":"EUR","in_stock":true},{"title":"Spacer \/ Hard \/ 10mm","offer_id":50653547692374,"sku":"ACC030_03","price":1.0,"currency_code":"EUR","in_stock":true},{"title":"HoleAdapter \/ Soft \/ 6mm","offer_id":50653538779478,"sku":"ACC029_01","price":1.0,"currency_code":"EUR","in_stock":true},{"title":"HoleAdapter \/ Soft \/ 8mm","offer_id":50653547725142,"sku":"ACC029_02","price":1.0,"currency_code":"EUR","in_stock":true},{"title":"HoleAdapter \/ Soft \/ 10mm","offer_id":50653547757910,"sku":"ACC029_03","price":1.0,"currency_code":"EUR","in_stock":true},{"title":"HoleAdapter \/ Medium \/ 6mm","offer_id":50653538812246,"sku":"ACC029_01","price":1.0,"currency_code":"EUR","in_stock":true},{"title":"HoleAdapter \/ Medium \/ 8mm","offer_id":50653547790678,"sku":"ACC029_02","price":1.0,"currency_code":"EUR","in_stock":true},{"title":"HoleAdapter \/ Medium \/ 10mm","offer_id":50653547823446,"sku":"ACC029_03","price":1.0,"currency_code":"EUR","in_stock":true},{"title":"HoleAdapter \/ Hard \/ 6mm","offer_id":50653538845014,"sku":"ACC029_01","price":1.0,"currency_code":"EUR","in_stock":true},{"title":"HoleAdapter \/ Hard \/ 8mm","offer_id":50653547856214,"sku":"ACC029_02","price":1.0,"currency_code":"EUR","in_stock":true},{"title":"HoleAdapter \/ Hard \/ 10mm","offer_id":50653547888982,"sku":"ACC029_03","price":1.0,"currency_code":"EUR","in_stock":true}],"thumbnail_url":"\/\/cdn.shopify.com\/s\/files\/1\/0899\/4816\/0342\/files\/Elastomer_Configurator_3DRap_Custom_Sim_racing.jpg?v=1750775235","url":"https:\/\/3drap.com\/es\/products\/elastomer-configurator-sim","provider":"3DRap Sim Racing","version":"1.0","type":"link"}