{"product_id":"prototipo-configurador-modular-de-bano-visobath-compon-tu-conjunto-a-medida","title":"[PROTOTIPO] Configurador Modular de Baño VISOBATH | Compón tu Conjunto a Medida","description":"\n\u003c!-- DEPENDENCIAS 3D WEBGL: THREE.JS + ORBITCONTROLS + ROOM ENVIRONMENT --\u003e\n\u003cscript src=\"https:\/\/cdnjs.cloudflare.com\/ajax\/libs\/three.js\/r128\/three.min.js\"\u003e\u003c\/script\u003e\n\u003cscript src=\"https:\/\/cdn.jsdelivr.net\/npm\/three@0.128.0\/examples\/js\/controls\/OrbitControls.js\"\u003e\u003c\/script\u003e\n\u003cscript src=\"https:\/\/cdn.jsdelivr.net\/npm\/three@0.128.0\/examples\/js\/environments\/RoomEnvironment.js\"\u003e\u003c\/script\u003e\n\n\u003cdiv id=\"visobath-builder-root\" style=\"font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Helvetica, Arial, sans-serif; color: #1C1B1A; max-width: 1220px; margin: 0 auto; line-height: 1.5;\"\u003e\n\n  \u003c!-- CABECERA PRINCIPAL --\u003e\n  \u003cdiv style=\"background: #FAF8F5; border: 1px solid #E8E2D8; border-radius: 12px; padding: 22px 26px; margin-bottom: 22px;\"\u003e\n    \u003cdiv style=\"display: flex; flex-wrap: wrap; justify-content: space-between; align-items: center; gap: 15px;\"\u003e\n      \u003cdiv\u003e\n        \u003cdiv style=\"display: flex; align-items: center; gap: 8px; margin-bottom: 4px;\"\u003e\n          \u003cspan style=\"font-size: 10.5px; font-weight: 800; background: #9E7B56; color: #FFF; padding: 3px 9px; border-radius: 4px; text-transform: uppercase; letter-spacing: 1.2px;\"\u003eESTUDIO 3D OFICIAL VISOBATH\u003c\/span\u003e\n          \u003cspan style=\"font-size: 10.5px; font-weight: 700; background: #E8E2D8; color: #1C1B1A; padding: 3px 9px; border-radius: 4px; text-transform: uppercase;\"\u003eGEOMETRÍA ORGÁNICA CONTINUA\u003c\/span\u003e\n        \u003c\/div\u003e\n        \u003ch2 style=\"font-size: 26px; font-weight: 700; color: #1C1B1A; margin: 4px 0 2px 0; letter-spacing: -0.5px;\"\u003eConfigurador Modular de Baño Visobath\u003c\/h2\u003e\n        \u003cspan style=\"font-size: 13.5px; color: #666;\"\u003eSeno cerámico esculpido continuo sin aristas cuadradas, Roble Costa PBR, espejo cápsula LED y aplique de iluminación.\u003c\/span\u003e\n      \u003c\/div\u003e\n      \n      \u003c!-- PESTAÑAS DE VISUALIZACIÓN --\u003e\n      \u003cdiv style=\"display: flex; background: #EFECE6; padding: 4px; border-radius: 8px; border: 1px solid #DED7CD; gap: 4px;\"\u003e\n        \u003cbutton type=\"button\" id=\"tab-btn-3d\" onclick=\"switchViewMode('3d')\" style=\"background: #1C1B1A; color: #FFF; border: none; padding: 9px 16px; border-radius: 6px; font-size: 12.5px; font-weight: 700; cursor: pointer; transition: all 0.2s; display: flex; align-items: center; gap: 6px;\"\u003e\n          \u003cspan\u003e🔄\u003c\/span\u003e\n          \u003cspan\u003eVisor 3D 360° Real\u003c\/span\u003e\n        \u003c\/button\u003e\n        \u003cbutton type=\"button\" id=\"tab-btn-photo\" onclick=\"switchViewMode('photo')\" style=\"background: transparent; color: #555; border: none; padding: 9px 16px; border-radius: 6px; font-size: 12.5px; font-weight: 700; cursor: pointer; transition: all 0.2s; display: flex; align-items: center; gap: 6px;\"\u003e\n          \u003cspan\u003e✨\u003c\/span\u003e\n          \u003cspan\u003eRender de Estudio 8K\u003c\/span\u003e\n        \u003c\/button\u003e\n      \u003c\/div\u003e\n    \u003c\/div\u003e\n  \u003c\/div\u003e\n\n  \u003c!-- CONTENEDOR PRINCIPAL DE VISUALIZACIÓN --\u003e\n  \u003cdiv style=\"background: #FFFFFF; border: 1px solid #E8E2D8; border-radius: 12px; padding: 18px; margin-bottom: 30px; box-shadow: 0 10px 35px rgba(0,0,0,0.06);\"\u003e\n    \n    \u003c!-- BARRA DE ESTADO SUPERIOR --\u003e\n    \u003cdiv style=\"display: flex; justify-content: space-between; align-items: center; border-bottom: 1px solid #F0ECE5; padding-bottom: 12px; margin-bottom: 14px; flex-wrap: wrap; gap: 10px;\"\u003e\n      \u003cdiv style=\"display: flex; align-items: center; gap: 8px;\"\u003e\n        \u003cspan style=\"width: 10px; height: 10px; border-radius: 50%; background: #28A745; display: inline-block;\"\u003e\u003c\/span\u003e\n        \u003cspan id=\"cfg-mode-status-text\" style=\"font-size: 12px; font-weight: 700; text-transform: uppercase; letter-spacing: 0.8px; color: #1C1B1A;\"\u003eEscenario 3D 360° Activo (Seno Cerámico Suave \u0026amp; Roble PBR)\u003c\/span\u003e\n        \u003cspan style=\"font-size: 11px; background: #FAF8F5; border: 1px solid #E8E2D8; padding: 2px 8px; border-radius: 4px; color: #777;\"\u003eArrastra para rotar · Rueda para zoom\u003c\/span\u003e\n      \u003c\/div\u003e\n      \u003cdiv id=\"cfg-3d-status-pill\" style=\"font-size: 12.5px; font-weight: 700; color: #9E7B56; background: #FAF8F5; border: 1px solid #E8E2D8; padding: 4px 14px; border-radius: 20px;\"\u003e\n        Mueble BOX 80 cm · Roble Costa\n      \u003c\/div\u003e\n    \u003c\/div\u003e\n\n    \u003c!-- 1. MODO 3D INTERACTIVO 360° WEBGL --\u003e\n    \u003cdiv id=\"view-container-3d\" style=\"position: relative; width: 100%; height: 580px; background: #EAE6DF; border-radius: 10px; overflow: hidden; border: 1px solid #DFD9CF; display: block;\"\u003e\n      \u003ccanvas id=\"visobath-3d-canvas\" style=\"width: 100%; height: 100%; display: block; outline: none;\"\u003e\u003c\/canvas\u003e\n      \n      \u003c!-- CONTROLES FLOTANTES EN MODO 3D --\u003e\n      \u003cdiv style=\"position: absolute; top: 15px; left: 15px; display: flex; gap: 6px; flex-wrap: wrap;\"\u003e\n        \u003cbutton type=\"button\" onclick=\"reset3DCamera('perspectiva')\" style=\"background: rgba(28,27,26,0.88); color: #FFF; border: none; padding: 7px 13px; border-radius: 6px; font-size: 11.5px; font-weight: 700; cursor: pointer; backdrop-filter: blur(4px);\"\u003e\n          🔄 Vista Catálogo\n        \u003c\/button\u003e\n        \u003cbutton type=\"button\" onclick=\"reset3DCamera('frontal')\" style=\"background: rgba(255,255,255,0.92); color: #1C1B1A; border: 1px solid #DED7CD; padding: 7px 13px; border-radius: 6px; font-size: 11.5px; font-weight: 700; cursor: pointer; backdrop-filter: blur(4px);\"\u003e\n          👁️ Frente\n        \u003c\/button\u003e\n        \u003cbutton type=\"button\" onclick=\"reset3DCamera('detalle')\" style=\"background: rgba(255,255,255,0.92); color: #1C1B1A; border: 1px solid #DED7CD; padding: 7px 13px; border-radius: 6px; font-size: 11.5px; font-weight: 700; cursor: pointer; backdrop-filter: blur(4px);\"\u003e\n          🔍 Detalle Seno \u0026amp; Grifo\n        \u003c\/button\u003e\n        \u003cbutton type=\"button\" id=\"btn-toggle-drawers\" onclick=\"toggleDrawers()\" style=\"background: #9E7B56; color: #FFF; border: none; padding: 7px 15px; border-radius: 6px; font-size: 11.5px; font-weight: 700; cursor: pointer; box-shadow: 0 2px 8px rgba(0,0,0,0.18);\"\u003e\n          👐 Abrir Cajones\n        \u003c\/button\u003e\n      \u003c\/div\u003e\n\n      \u003cdiv style=\"position: absolute; bottom: 15px; left: 15px; background: rgba(28,27,26,0.85); backdrop-filter: blur(8px); color: #FFF; padding: 6px 14px; border-radius: 20px; font-size: 11px; pointer-events: none;\"\u003e\n        🖱️ Arrastra en 360° para rotar libremente · Rueda o pellizca para zoom\n      \u003c\/div\u003e\n\n      \u003cdiv style=\"position: absolute; bottom: 15px; right: 15px;\"\u003e\n        \u003cbutton type=\"button\" onclick=\"switchViewMode('photo')\" style=\"background: #1C1B1A; color: #FFF; border: none; padding: 8px 16px; border-radius: 20px; font-size: 12px; font-weight: 700; cursor: pointer; box-shadow: 0 4px 14px rgba(0,0,0,0.2); display: flex; align-items: center; gap: 6px;\"\u003e\n          \u003cspan\u003e✨\u003c\/span\u003e\n          \u003cspan\u003eVer Render de Estudio 8K\u003c\/span\u003e\n        \u003c\/button\u003e\n      \u003c\/div\u003e\n    \u003c\/div\u003e\n\n    \u003c!-- 2. MODO RENDER FOTORREALISTA DE ESTUDIO (8K) --\u003e\n    \u003cdiv id=\"view-container-photo\" style=\"position: relative; width: 100%; min-height: 580px; background: #FAF8F5; border-radius: 10px; overflow: hidden; border: 1px solid #E0DAD0; display: none; align-items: center; justify-content: center;\"\u003e\n      \u003cimg id=\"cfg-studio-main-img\" src=\"https:\/\/cdn.shopify.com\/s\/files\/1\/0728\/8596\/1049\/files\/studio_roble_80.jpg?v=1789514546\" alt=\"Visobath Mueble de Baño Configurado\" style=\"width: 100%; max-height: 620px; object-fit: contain; display: block; transition: opacity 0.3s ease;\"\u003e\n      \n      \u003cdiv style=\"position: absolute; top: 16px; left: 16px; background: rgba(28,27,26,0.85); backdrop-filter: blur(8px); color: #FFF; padding: 8px 16px; border-radius: 24px; font-size: 12px; pointer-events: none; display: flex; align-items: center; gap: 8px;\"\u003e\n        \u003cspan style=\"color: #D4AF37;\"\u003e★\u003c\/span\u003e\n        \u003cspan\u003eRender Fotorrealista de Fábrica Visobath\u003c\/span\u003e\n      \u003c\/div\u003e\n\n      \u003cdiv style=\"position: absolute; bottom: 16px; left: 16px; background: rgba(255,255,255,0.92); backdrop-filter: blur(8px); border: 1px solid #DED7CD; padding: 7px 16px; border-radius: 24px; font-size: 11.5px; font-weight: 700; color: #1C1B1A; display: flex; align-items: center; gap: 8px;\"\u003e\n        \u003cspan style=\"color: #9E7B56;\"\u003e👐\u003c\/span\u003e\n        \u003cspan\u003eCajón Superior Estrecho con Salvasifón + Cajón Inferior Amplio\u003c\/span\u003e\n      \u003c\/div\u003e\n\n      \u003cdiv style=\"position: absolute; bottom: 16px; right: 16px;\"\u003e\n        \u003cbutton type=\"button\" onclick=\"switchViewMode('3d')\" style=\"background: #9E7B56; color: #FFF; border: none; padding: 8px 16px; border-radius: 20px; font-size: 12px; font-weight: 700; cursor: pointer; box-shadow: 0 4px 14px rgba(0,0,0,0.2); display: flex; align-items: center; gap: 6px;\"\u003e\n          \u003cspan\u003e🔄\u003c\/span\u003e\n          \u003cspan\u003eExplorar en Visor 3D 360°\u003c\/span\u003e\n        \u003c\/button\u003e\n      \u003c\/div\u003e\n    \u003c\/div\u003e\n\n  \u003c\/div\u003e\n\n  \u003c!-- CONFIGURACIÓN PASO A PASO + HOJA DE PEDIDO --\u003e\n  \u003cdiv style=\"display: flex; flex-wrap: wrap; gap: 28px; align-items: flex-start;\"\u003e\n    \n    \u003c!-- COLUMNA IZQUIERDA: LOS 6 PASOS GUIADOS --\u003e\n    \u003cdiv style=\"flex: 1 1 650px; min-width: 320px;\"\u003e\n\n      \u003c!-- PASO 1: MUEBLE --\u003e\n      \u003cdiv style=\"background: #FFF; border: 1px solid #E8E2D8; border-radius: 10px; padding: 22px; margin-bottom: 22px;\"\u003e\n        \u003cdiv style=\"display: flex; align-items: center; gap: 10px; margin-bottom: 16px;\"\u003e\n          \u003cspan style=\"width: 28px; height: 28px; background: #9E7B56; color: #FFF; border-radius: 50%; display: flex; align-items: center; justify-content: center; font-size: 13px; font-weight: 700;\"\u003e1\u003c\/span\u003e\n          \u003cdiv\u003e\n            \u003ch3 style=\"font-size: 18px; font-weight: 700; color: #1C1B1A; margin: 0;\"\u003eMueble Principal: Colección, Ancho y Acabado\u003c\/h3\u003e\n            \u003cspan style=\"font-size: 12px; color: #666;\"\u003e2 cajones asimétricos: superior estrecho (20 cm) e inferior amplio (31 cm). Altura total oficial 54 cm.\u003c\/span\u003e\n          \u003c\/div\u003e\n        \u003c\/div\u003e\n\n        \u003c!-- Colección --\u003e\n        \u003clabel style=\"font-size: 11.5px; font-weight: 700; color: #666; text-transform: uppercase; display: block; margin-bottom: 8px;\"\u003e1.1 Colección Oficial\u003c\/label\u003e\n        \u003cdiv style=\"display: grid; grid-template-columns: repeat(auto-fit, minmax(150px, 1fr)); gap: 10px; margin-bottom: 18px;\"\u003e\n          \u003cbutton type=\"button\" class=\"cfg-btn cfg-model-btn active\" data-model=\"BOX\" onclick=\"setCfgModel('BOX')\" style=\"padding: 12px; border: 2px solid #9E7B56; background: #FAF8F5; border-radius: 6px; cursor: pointer; text-align: left;\"\u003e\n            \u003cstrong style=\"display: block; font-size: 14px; color: #1C1B1A;\"\u003eBOX\u003c\/strong\u003e\n            \u003cspan style=\"font-size: 11px; color: #777;\"\u003eTirador uñero negro integrado\u003c\/span\u003e\n          \u003c\/button\u003e\n          \u003cbutton type=\"button\" class=\"cfg-btn cfg-model-btn\" data-model=\"GRANADA\" onclick=\"setCfgModel('GRANADA')\" style=\"padding: 12px; border: 1px solid #DED7CD; background: #FFF; border-radius: 6px; cursor: pointer; text-align: left;\"\u003e\n            \u003cstrong style=\"display: block; font-size: 14px; color: #1C1B1A;\"\u003eGRANADA\u003c\/strong\u003e\n            \u003cspan style=\"font-size: 11px; color: #777;\"\u003eGola recta ingletada (+20€)\u003c\/span\u003e\n          \u003c\/button\u003e\n        \u003c\/div\u003e\n\n        \u003c!-- Ancho --\u003e\n        \u003clabel style=\"font-size: 11.5px; font-weight: 700; color: #666; text-transform: uppercase; display: block; margin-bottom: 8px;\"\u003e1.2 Ancho Modular\u003c\/label\u003e\n        \u003cdiv style=\"display: grid; grid-template-columns: repeat(4, 1fr); gap: 8px; margin-bottom: 18px;\"\u003e\n          \u003cbutton type=\"button\" class=\"cfg-btn cfg-size-btn\" data-size=\"60\" onclick=\"setCfgSize(60)\" style=\"padding: 10px 6px; border: 1px solid #DED7CD; background: #FFF; border-radius: 6px; font-size: 12.5px; font-weight: 700; cursor: pointer; text-align: center;\"\u003e60 cm\u003c\/button\u003e\n          \u003cbutton type=\"button\" class=\"cfg-btn cfg-size-btn active\" data-size=\"80\" onclick=\"setCfgSize(80)\" style=\"padding: 10px 6px; border: 2px solid #9E7B56; background: #FAF8F5; border-radius: 6px; font-size: 12.5px; font-weight: 700; cursor: pointer; text-align: center;\"\u003e80 cm\u003c\/button\u003e\n          \u003cbutton type=\"button\" class=\"cfg-btn cfg-size-btn\" data-size=\"100\" onclick=\"setCfgSize(100)\" style=\"padding: 10px 6px; border: 1px solid #DED7CD; background: #FFF; border-radius: 6px; font-size: 12.5px; font-weight: 700; cursor: pointer; text-align: center;\"\u003e100 cm\u003c\/button\u003e\n          \u003cbutton type=\"button\" class=\"cfg-btn cfg-size-btn\" data-size=\"120\" onclick=\"setCfgSize(120)\" style=\"padding: 10px 6px; border: 1px solid #DED7CD; background: #FFF; border-radius: 6px; font-size: 12.5px; font-weight: 700; cursor: pointer; text-align: center;\"\u003e120 cm (Doble)\u003c\/button\u003e\n        \u003c\/div\u003e\n\n        \u003c!-- Acabado --\u003e\n        \u003clabel style=\"font-size: 11.5px; font-weight: 700; color: #666; text-transform: uppercase; display: block; margin-bottom: 8px;\"\u003e1.3 Acabado de Carcasa y Cajones\u003c\/label\u003e\n        \u003cdiv style=\"display: grid; grid-template-columns: repeat(auto-fit, minmax(135px, 1fr)); gap: 8px;\"\u003e\n          \u003cbutton type=\"button\" class=\"cfg-btn cfg-finish-btn active\" data-finish=\"roble\" onclick=\"setCfgFinish('roble', 'Roble Costa')\" style=\"padding: 10px; border: 2px solid #9E7B56; background: #FAF8F5; border-radius: 6px; cursor: pointer; text-align: left; display: flex; align-items: center; gap: 8px;\"\u003e\n            \u003cspan style=\"width: 20px; height: 20px; border-radius: 4px; background: #C4A47E; border: 1px solid rgba(0,0,0,0.15); display: inline-block;\"\u003e\u003c\/span\u003e\n            \u003cdiv\u003e\n              \u003cstrong style=\"display: block; font-size: 12px;\"\u003eRoble Costa\u003c\/strong\u003e\n              \u003cspan style=\"font-size: 10px; color: #777;\"\u003eMadera natural\u003c\/span\u003e\n            \u003c\/div\u003e\n          \u003c\/button\u003e\n          \u003cbutton type=\"button\" class=\"cfg-btn cfg-finish-btn\" data-finish=\"blanco\" onclick=\"setCfgFinish('blanco', 'Blanco Ada')\" style=\"padding: 10px; border: 1px solid #DED7CD; background: #FFF; border-radius: 6px; cursor: pointer; text-align: left; display: flex; align-items: center; gap: 8px;\"\u003e\n            \u003cspan style=\"width: 20px; height: 20px; border-radius: 4px; background: #F8F7F4; border: 1px solid #DDD; display: inline-block;\"\u003e\u003c\/span\u003e\n            \u003cdiv\u003e\n              \u003cstrong style=\"display: block; font-size: 12px;\"\u003eBlanco Ada\u003c\/strong\u003e\n              \u003cspan style=\"font-size: 10px; color: #777;\"\u003eSeda mate\u003c\/span\u003e\n            \u003c\/div\u003e\n          \u003c\/button\u003e\n          \u003cbutton type=\"button\" class=\"cfg-btn cfg-finish-btn\" data-finish=\"ceniza\" onclick=\"setCfgFinish('ceniza', 'Ceniza')\" style=\"padding: 10px; border: 1px solid #DED7CD; background: #FFF; border-radius: 6px; cursor: pointer; text-align: left; display: flex; align-items: center; gap: 8px;\"\u003e\n            \u003cspan style=\"width: 20px; height: 20px; border-radius: 4px; background: #343434; border: 1px solid #222; display: inline-block;\"\u003e\u003c\/span\u003e\n            \u003cdiv\u003e\n              \u003cstrong style=\"display: block; font-size: 12px;\"\u003eCeniza\u003c\/strong\u003e\n              \u003cspan style=\"font-size: 10px; color: #777;\"\u003eGris carbón\u003c\/span\u003e\n            \u003c\/div\u003e\n          \u003c\/button\u003e\n          \u003cbutton type=\"button\" class=\"cfg-btn cfg-finish-btn\" data-finish=\"avio\" onclick=\"setCfgFinish('avio', 'Ávio')\" style=\"padding: 10px; border: 1px solid #DED7CD; background: #FFF; border-radius: 6px; cursor: pointer; text-align: left; display: flex; align-items: center; gap: 8px;\"\u003e\n            \u003cspan style=\"width: 20px; height: 20px; border-radius: 4px; background: #445562; border: 1px solid #333; display: inline-block;\"\u003e\u003c\/span\u003e\n            \u003cdiv\u003e\n              \u003cstrong style=\"display: block; font-size: 12px;\"\u003eÁvio\u003c\/strong\u003e\n              \u003cspan style=\"font-size: 10px; color: #777;\"\u003eAzul petróleo\u003c\/span\u003e\n            \u003c\/div\u003e\n          \u003c\/button\u003e\n        \u003c\/div\u003e\n      \u003c\/div\u003e\n\n      \u003c!-- PASO 2: LAVABO --\u003e\n      \u003cdiv style=\"background: #FFF; border: 1px solid #E8E2D8; border-radius: 10px; padding: 22px; margin-bottom: 22px;\"\u003e\n        \u003cdiv style=\"display: flex; align-items: center; gap: 10px; margin-bottom: 16px;\"\u003e\n          \u003cspan style=\"width: 28px; height: 28px; background: #9E7B56; color: #FFF; border-radius: 50%; display: flex; align-items: center; justify-content: center; font-size: 13px; font-weight: 700;\"\u003e2\u003c\/span\u003e\n          \u003cdiv\u003e\n            \u003ch3 style=\"font-size: 18px; font-weight: 700; color: #1C1B1A; margin: 0;\"\u003eLavabo y Encimera\u003c\/h3\u003e\n            \u003cspan style=\"font-size: 12px; color: #666;\"\u003eSeno cerámico integrado orgánico o tapa de madera con lavabo sobre encimera.\u003c\/span\u003e\n          \u003c\/div\u003e\n        \u003c\/div\u003e\n\n        \u003cdiv style=\"display: grid; grid-template-columns: repeat(auto-fit, minmax(240px, 1fr)); gap: 12px;\"\u003e\n          \u003cbutton type=\"button\" class=\"cfg-btn cfg-lavabo-btn active\" data-lavabo=\"flat\" onclick=\"setCfgLavabo('flat', 'FLAT Porcelana Integrada')\" style=\"padding: 14px; border: 2px solid #9E7B56; background: #FAF8F5; border-radius: 6px; cursor: pointer; text-align: left;\"\u003e\n            \u003cdiv style=\"display: flex; justify-content: space-between; align-items: center; margin-bottom: 4px;\"\u003e\n              \u003cstrong style=\"font-size: 14px; color: #1C1B1A;\"\u003eFLAT Cerámica Blanco\u003c\/strong\u003e\n              \u003cspan style=\"font-size: 11px; background: #E8E2D8; padding: 2px 7px; border-radius: 4px; font-weight: 700;\"\u003eIncluido\u003c\/span\u003e\n            \u003c\/div\u003e\n            \u003cspan style=\"font-size: 12px; color: #666; display: block;\"\u003eSeno rectangular continuo suave con válvula cromo y rebosadero.\u003c\/span\u003e\n          \u003c\/button\u003e\n\n          \u003cbutton type=\"button\" class=\"cfg-btn cfg-lavabo-btn\" data-lavabo=\"colors\" onclick=\"setCfgLavabo('colors', 'COLORS Bol Cerámico + Encimera')\" style=\"padding: 14px; border: 1px solid #DED7CD; background: #FFF; border-radius: 6px; cursor: pointer; text-align: left;\"\u003e\n            \u003cdiv style=\"display: flex; justify-content: space-between; align-items: center; margin-bottom: 4px;\"\u003e\n              \u003cstrong style=\"font-size: 14px; color: #1C1B1A;\"\u003eEncimera + Bol COLORS\u003c\/strong\u003e\n              \u003cspan style=\"font-size: 11px; color: #9E7B56; font-weight: 700;\"\u003e+160 €\u003c\/span\u003e\n            \u003c\/div\u003e\n            \u003cspan style=\"font-size: 12px; color: #666; display: block;\"\u003eTapa a juego con lavabo bol cilíndrico en Verde Salvia mate y grifo alto.\u003c\/span\u003e\n          \u003c\/button\u003e\n        \u003c\/div\u003e\n      \u003c\/div\u003e\n\n      \u003c!-- PASO 3: ESPEJO --\u003e\n      \u003cdiv style=\"background: #FFF; border: 1px solid #E8E2D8; border-radius: 10px; padding: 22px; margin-bottom: 22px;\"\u003e\n        \u003cdiv style=\"display: flex; align-items: center; gap: 10px; margin-bottom: 16px;\"\u003e\n          \u003cspan style=\"width: 28px; height: 28px; background: #9E7B56; color: #FFF; border-radius: 50%; display: flex; align-items: center; justify-content: center; font-size: 13px; font-weight: 700;\"\u003e3\u003c\/span\u003e\n          \u003cdiv\u003e\n            \u003ch3 style=\"font-size: 18px; font-weight: 700; color: #1C1B1A; margin: 0;\"\u003eEspejo de Baño\u003c\/h3\u003e\n            \u003cspan style=\"font-size: 12px; color: #666;\"\u003eEspejo cápsula horizontal con iluminación LED perimetral retroiluminada.\u003c\/span\u003e\n          \u003c\/div\u003e\n        \u003c\/div\u003e\n\n        \u003cdiv style=\"display: grid; grid-template-columns: repeat(auto-fit, minmax(180px, 1fr)); gap: 10px;\"\u003e\n          \u003cbutton type=\"button\" class=\"cfg-btn cfg-espejo-btn active\" data-espejo=\"ella\" onclick=\"setCfgEspejo('ella', 'ELLA Cápsula LED 80x50')\" style=\"padding: 12px; border: 2px solid #9E7B56; background: #FAF8F5; border-radius: 6px; cursor: pointer; text-align: left;\"\u003e\n            \u003cdiv style=\"display: flex; justify-content: space-between; align-items: center; margin-bottom: 3px;\"\u003e\n              \u003cstrong style=\"font-size: 13px; color: #1C1B1A;\"\u003eELLA Cápsula LED\u003c\/strong\u003e\n              \u003cspan style=\"font-size: 11px; color: #9E7B56; font-weight: 700;\"\u003e+135 €\u003c\/span\u003e\n            \u003c\/div\u003e\n            \u003cspan style=\"font-size: 11.5px; color: #666;\"\u003eOvalado horizontal con halo LED cálido perimetral.\u003c\/span\u003e\n          \u003c\/button\u003e\n\n          \u003cbutton type=\"button\" class=\"cfg-btn cfg-espejo-btn\" data-espejo=\"circular\" onclick=\"setCfgEspejo('circular', 'SEVILLA Redondo LED Ø80')\" style=\"padding: 12px; border: 1px solid #DED7CD; background: #FFF; border-radius: 6px; cursor: pointer; text-align: left;\"\u003e\n            \u003cdiv style=\"display: flex; justify-content: space-between; align-items: center; margin-bottom: 3px;\"\u003e\n              \u003cstrong style=\"font-size: 13px; color: #1C1B1A;\"\u003eSEVILLA Circular LED\u003c\/strong\u003e\n              \u003cspan style=\"font-size: 11px; color: #9E7B56; font-weight: 700;\"\u003e+115 €\u003c\/span\u003e\n            \u003c\/div\u003e\n            \u003cspan style=\"font-size: 11.5px; color: #666;\"\u003eEspejo circular Ø80 cm con retroiluminación.\u003c\/span\u003e\n          \u003c\/button\u003e\n\n          \u003cbutton type=\"button\" class=\"cfg-btn cfg-espejo-btn\" data-espejo=\"none\" onclick=\"setCfgEspejo('none', 'Sin Espejo')\" style=\"padding: 12px; border: 1px solid #DED7CD; background: #FFF; border-radius: 6px; cursor: pointer; text-align: left;\"\u003e\n            \u003cstrong style=\"font-size: 13px; color: #1C1B1A; display: block; margin-bottom: 3px;\"\u003eSin Espejo\u003c\/strong\u003e\n            \u003cspan style=\"font-size: 11.5px; color: #666;\"\u003eNo incluir espejo en el conjunto.\u003c\/span\u003e\n          \u003c\/button\u003e\n        \u003c\/div\u003e\n      \u003c\/div\u003e\n\n      \u003c!-- PASO 4: APLIQUE LUMINARIA --\u003e\n      \u003cdiv style=\"background: #FFF; border: 1px solid #E8E2D8; border-radius: 10px; padding: 22px; margin-bottom: 22px;\"\u003e\n        \u003cdiv style=\"display: flex; align-items: center; gap: 10px; margin-bottom: 16px;\"\u003e\n          \u003cspan style=\"width: 28px; height: 28px; background: #9E7B56; color: #FFF; border-radius: 50%; display: flex; align-items: center; justify-content: center; font-size: 13px; font-weight: 700;\"\u003e4\u003c\/span\u003e\n          \u003cdiv\u003e\n            \u003ch3 style=\"font-size: 18px; font-weight: 700; color: #1C1B1A; margin: 0;\"\u003eAplique Luminaria LED para Espejo\u003c\/h3\u003e\n            \u003cspan style=\"font-size: 12px; color: #666;\"\u003eAplique lineal negro mate con pinza superior montada sobre el espejo.\u003c\/span\u003e\n          \u003c\/div\u003e\n        \u003c\/div\u003e\n\n        \u003cdiv style=\"display: grid; grid-template-columns: repeat(auto-fit, minmax(200px, 1fr)); gap: 10px;\"\u003e\n          \u003cbutton type=\"button\" class=\"cfg-btn cfg-light-btn active\" data-light=\"black\" onclick=\"setCfgLight('black', 'BLACK Barra LED 30cm')\" style=\"padding: 12px; border: 2px solid #9E7B56; background: #FAF8F5; border-radius: 6px; cursor: pointer; text-align: left;\"\u003e\n            \u003cdiv style=\"display: flex; justify-content: space-between; align-items: center; margin-bottom: 3px;\"\u003e\n              \u003cstrong style=\"font-size: 13px; color: #1C1B1A;\"\u003eBLACK Barra LED 30cm\u003c\/strong\u003e\n              \u003cspan style=\"font-size: 11px; color: #9E7B56; font-weight: 700;\"\u003e+48 €\u003c\/span\u003e\n            \u003c\/div\u003e\n            \u003cspan style=\"font-size: 11.5px; color: #666;\"\u003ePerfil negro mate con luz cálida dirigida hacia el lavabo.\u003c\/span\u003e\n          \u003c\/button\u003e\n\n          \u003cbutton type=\"button\" class=\"cfg-btn cfg-light-btn\" data-light=\"chrome\" onclick=\"setCfgLight('chrome', 'CHROME Barra LED 30cm')\" style=\"padding: 12px; border: 1px solid #DED7CD; background: #FFF; border-radius: 6px; cursor: pointer; text-align: left;\"\u003e\n            \u003cdiv style=\"display: flex; justify-content: space-between; align-items: center; margin-bottom: 3px;\"\u003e\n              \u003cstrong style=\"font-size: 13px; color: #1C1B1A;\"\u003eCHROME Barra LED 30cm\u003c\/strong\u003e\n              \u003cspan style=\"font-size: 11px; color: #9E7B56; font-weight: 700;\"\u003e+45 €\u003c\/span\u003e\n            \u003c\/div\u003e\n            \u003cspan style=\"font-size: 11.5px; color: #666;\"\u003eCromo brillo con difusor opal antideslumbramiento.\u003c\/span\u003e\n          \u003c\/button\u003e\n\n          \u003cbutton type=\"button\" class=\"cfg-btn cfg-light-btn\" data-light=\"none\" onclick=\"setCfgLight('none', 'Sin Aplique')\" style=\"padding: 12px; border: 1px solid #DED7CD; background: #FFF; border-radius: 6px; cursor: pointer; text-align: left;\"\u003e\n            \u003cstrong style=\"font-size: 13px; color: #1C1B1A; display: block; margin-bottom: 3px;\"\u003eSin Aplique Adicional\u003c\/strong\u003e\n            \u003cspan style=\"font-size: 11.5px; color: #666;\"\u003eAprovechar retroiluminación del espejo.\u003c\/span\u003e\n          \u003c\/button\u003e\n        \u003c\/div\u003e\n      \u003c\/div\u003e\n\n      \u003c!-- PASO 5: MÓDULO COLUMNA AUXILIAR --\u003e\n      \u003cdiv style=\"background: #FFF; border: 1px solid #E8E2D8; border-radius: 10px; padding: 22px; margin-bottom: 22px;\"\u003e\n        \u003cdiv style=\"display: flex; align-items: center; gap: 10px; margin-bottom: 16px;\"\u003e\n          \u003cspan style=\"width: 28px; height: 28px; background: #9E7B56; color: #FFF; border-radius: 50%; display: flex; align-items: center; justify-content: center; font-size: 13px; font-weight: 700;\"\u003e5\u003c\/span\u003e\n          \u003cdiv\u003e\n            \u003ch3 style=\"font-size: 18px; font-weight: 700; color: #1C1B1A; margin: 0;\"\u003eColumna Auxiliar Suspendida\u003c\/h3\u003e\n            \u003cspan style=\"font-size: 12px; color: #666;\"\u003eColumna suspendida de 120 cm con 2 puertas reversibles a juego.\u003c\/span\u003e\n          \u003c\/div\u003e\n        \u003c\/div\u003e\n\n        \u003cdiv style=\"display: grid; grid-template-columns: repeat(auto-fit, minmax(220px, 1fr)); gap: 10px;\"\u003e\n          \u003cbutton type=\"button\" class=\"cfg-btn cfg-col-btn active\" data-col=\"none\" onclick=\"setCfgColumn('none', 'Sin Columna')\" style=\"padding: 12px; border: 2px solid #9E7B56; background: #FAF8F5; border-radius: 6px; cursor: pointer; text-align: left;\"\u003e\n            \u003cstrong style=\"font-size: 13px; color: #1C1B1A; display: block; margin-bottom: 3px;\"\u003eSin Columna Auxiliar\u003c\/strong\u003e\n            \u003cspan style=\"font-size: 11.5px; color: #666;\"\u003eSólo mueble principal y lavabo.\u003c\/span\u003e\n          \u003c\/button\u003e\n\n          \u003cbutton type=\"button\" class=\"cfg-btn cfg-col-btn\" data-col=\"columna\" onclick=\"setCfgColumn('columna', 'Columna Suspendida 120x35')\" style=\"padding: 12px; border: 1px solid #DED7CD; background: #FFF; border-radius: 6px; cursor: pointer; text-align: left;\"\u003e\n            \u003cdiv style=\"display: flex; justify-content: space-between; align-items: center; margin-bottom: 3px;\"\u003e\n              \u003cstrong style=\"font-size: 13px; color: #1C1B1A;\"\u003eColumna 120×35 cm\u003c\/strong\u003e\n              \u003cspan style=\"font-size: 11px; color: #9E7B56; font-weight: 700;\"\u003e+210 €\u003c\/span\u003e\n            \u003c\/div\u003e\n            \u003cspan style=\"font-size: 11.5px; color: #666;\"\u003eSuspendida con 2 puertas con baldas regulables.\u003c\/span\u003e\n          \u003c\/button\u003e\n        \u003c\/div\u003e\n      \u003c\/div\u003e\n\n    \u003c\/div\u003e\n\n    \u003c!-- COLUMNA DERECHA: RESUMEN DE PEDIDO TÉCNICO Y BOTÓN DE COMPRA --\u003e\n    \u003cdiv style=\"flex: 1 1 360px; min-width: 300px; position: sticky; top: 20px;\"\u003e\n      \u003cdiv style=\"background: #FAF8F5; border: 1px solid #E8E2D8; border-radius: 12px; padding: 24px; box-shadow: 0 4px 20px rgba(0,0,0,0.04);\"\u003e\n        \u003ch3 style=\"font-size: 18px; font-weight: 700; color: #1C1B1A; margin: 0 0 4px 0;\"\u003eResumen del Conjunto\u003c\/h3\u003e\n        \u003cspan style=\"font-size: 11.5px; color: #777; display: block; margin-bottom: 16px;\"\u003eConfiguración técnica según catálogo Visobath\u003c\/span\u003e\n\n        \u003cdiv style=\"border-top: 1px solid #E8E2D8; padding: 12px 0;\"\u003e\n          \u003cdiv style=\"display: flex; justify-content: space-between; font-size: 13px; margin-bottom: 6px;\"\u003e\n            \u003cspan style=\"color: #666;\"\u003eMueble Principal:\u003c\/span\u003e\n            \u003cstrong id=\"summary-mueble\" style=\"color: #1C1B1A; text-align: right;\"\u003eBOX 80 cm · Roble Costa\u003c\/strong\u003e\n          \u003c\/div\u003e\n          \u003cdiv style=\"display: flex; justify-content: space-between; font-size: 13px; margin-bottom: 6px;\"\u003e\n            \u003cspan style=\"color: #666;\"\u003eLavabo:\u003c\/span\u003e\n            \u003cstrong id=\"summary-lavabo\" style=\"color: #1C1B1A; text-align: right;\"\u003eFLAT Cerámica\u003c\/strong\u003e\n          \u003c\/div\u003e\n          \u003cdiv style=\"display: flex; justify-content: space-between; font-size: 13px; margin-bottom: 6px;\"\u003e\n            \u003cspan style=\"color: #666;\"\u003eEspejo:\u003c\/span\u003e\n            \u003cstrong id=\"summary-espejo\" style=\"color: #1C1B1A; text-align: right;\"\u003eELLA Cápsula LED\u003c\/strong\u003e\n          \u003c\/div\u003e\n          \u003cdiv style=\"display: flex; justify-content: space-between; font-size: 13px; margin-bottom: 6px;\"\u003e\n            \u003cspan style=\"color: #666;\"\u003eAplique:\u003c\/span\u003e\n            \u003cstrong id=\"summary-light\" style=\"color: #1C1B1A; text-align: right;\"\u003eBLACK Barra LED\u003c\/strong\u003e\n          \u003c\/div\u003e\n          \u003cdiv style=\"display: flex; justify-content: space-between; font-size: 13px; margin-bottom: 6px;\"\u003e\n            \u003cspan style=\"color: #666;\"\u003eColumna:\u003c\/span\u003e\n            \u003cstrong id=\"summary-col\" style=\"color: #1C1B1A; text-align: right;\"\u003eNo incluida\u003c\/strong\u003e\n          \u003c\/div\u003e\n        \u003c\/div\u003e\n\n        \u003cdiv style=\"background: #FFF; border: 1px dashed #DED7CD; border-radius: 8px; padding: 12px; margin: 12px 0;\"\u003e\n          \u003cspan style=\"font-size: 11px; font-weight: 700; color: #888; text-transform: uppercase; display: block; margin-bottom: 4px;\"\u003eReferencia Técnica de Fábrica:\u003c\/span\u003e\n          \u003ccode id=\"summary-ref\" style=\"font-size: 12.5px; font-weight: 700; color: #9E7B56;\"\u003eVB-BOX-080-ROB-FLAT-ELLA-BL-11224080\u003c\/code\u003e\n        \u003c\/div\u003e\n\n        \u003cdiv style=\"border-top: 2px solid #1C1B1A; padding-top: 14px; margin-top: 14px;\"\u003e\n          \u003cdiv style=\"display: flex; justify-content: space-between; align-items: baseline; margin-bottom: 16px;\"\u003e\n            \u003cspan style=\"font-size: 15px; font-weight: 700; color: #1C1B1A;\"\u003eTotal Configurado:\u003c\/span\u003e\n            \u003cdiv style=\"text-align: right;\"\u003e\n              \u003cspan id=\"summary-price\" style=\"font-size: 28px; font-weight: 800; color: #1C1B1A; letter-spacing: -0.5px;\"\u003e633 €\u003c\/span\u003e\n              \u003cspan style=\"font-size: 11px; color: #777; display: block;\"\u003eIVA y Envío Asegurado incluidos\u003c\/span\u003e\n            \u003c\/div\u003e\n          \u003c\/div\u003e\n\n          \u003cbutton type=\"button\" onclick=\"addToCartConfigured()\" style=\"width: 100%; background: #1C1B1A; color: #FFF; border: none; padding: 16px; border-radius: 8px; font-size: 15px; font-weight: 700; cursor: pointer; transition: background 0.2s; box-shadow: 0 4px 14px rgba(0,0,0,0.15);\"\u003e\n            Añadir Conjunto al Carrito\n          \u003c\/button\u003e\n          \n          \u003cdiv style=\"display: flex; align-items: center; justify-content: center; gap: 8px; margin-top: 12px; font-size: 11.5px; color: #666;\"\u003e\n            \u003cspan\u003e🛡️ Garantía Oficial Visobath 3 Años\u003c\/span\u003e\n            \u003cspan\u003e·\u003c\/span\u003e\n            \u003cspan\u003e🚚 Entrega Paletizada\u003c\/span\u003e\n          \u003c\/div\u003e\n        \u003c\/div\u003e\n\n      \u003c\/div\u003e\n    \u003c\/div\u003e\n\n  \u003c\/div\u003e\n\n  \u003c!-- CÓDIGO JAVASCRIPT DEL CONFIGURADOR Y MOTOR 3D --\u003e\n  \u003cscript\u003e\n    const cfgState = {\n      model: 'BOX',\n      size: 80,\n      finishKey: 'roble',\n      finishName: 'Roble Costa',\n      lavaboKey: 'flat',\n      lavaboName: 'FLAT Cerámica',\n      espejoKey: 'ella',\n      espejoName: 'ELLA Cápsula LED',\n      lightKey: 'black',\n      lightName: 'BLACK Barra LED',\n      colKey: 'none',\n      colName: 'No incluida'\n    };\n\n    const basePrices = {\n      BOX: { 60: 420, 80: 450, 100: 510, 120: 640 },\n      GRANADA: { 60: 440, 80: 470, 100: 530, 120: 660 }\n    };\n\n    const addonPrices = {\n      lavabo: { flat: 0, colors: 160 },\n      espejo: { ella: 135, circular: 115, none: 0 },\n      light: { black: 48, chrome: 45, none: 0 },\n      columna: { none: 0, columna: 210 }\n    };\n\n    function updateSummary() {\n      const base = basePrices[cfgState.model][cfgState.size];\n      const lav = addonPrices.lavabo[cfgState.lavaboKey];\n      const esp = addonPrices.espejo[cfgState.espejoKey];\n      const lig = addonPrices.light[cfgState.lightKey];\n      const col = addonPrices.columna[cfgState.colKey];\n      const total = base + lav + esp + lig + col;\n\n      document.getElementById('summary-mueble').innerText = cfgState.model + ' ' + cfgState.size + ' cm · ' + cfgState.finishName;\n      document.getElementById('summary-lavabo').innerText = cfgState.lavaboName;\n      document.getElementById('summary-espejo').innerText = cfgState.espejoName;\n      document.getElementById('summary-light').innerText = cfgState.lightName;\n      document.getElementById('summary-col').innerText = cfgState.colName;\n      document.getElementById('summary-price').innerText = total + ' €';\n\n      const ref = 'VB-' + cfgState.model + '-' + String(cfgState.size).padStart(3, '0') + '-' + \n                  cfgState.finishKey.substring(0, 3).toUpperCase() + '-' +\n                  cfgState.lavaboKey.toUpperCase() + '-' +\n                  cfgState.espejoKey.toUpperCase() + '-' +\n                  (cfgState.lightKey !== 'none' ? 'LGT-' : 'NOL-') +\n                  (cfgState.colKey !== 'none' ? 'COL' : 'NOC');\n      document.getElementById('summary-ref').innerText = ref;\n      document.getElementById('cfg-3d-status-pill').innerText = cfgState.model + ' ' + cfgState.size + ' cm · ' + cfgState.finishName;\n    }\n\n    function switchViewMode(mode) {\n      const c3d = document.getElementById('view-container-3d');\n      const cPhoto = document.getElementById('view-container-photo');\n      const btn3d = document.getElementById('tab-btn-3d');\n      const btnPhoto = document.getElementById('tab-btn-photo');\n\n      if (mode === '3d') {\n        c3d.style.display = 'block';\n        cPhoto.style.display = 'none';\n        btn3d.style.background = '#1C1B1A';\n        btn3d.style.color = '#FFF';\n        btnPhoto.style.background = 'transparent';\n        btnPhoto.style.color = '#555';\n        if (renderer \u0026\u0026 camera) {\n          onWindowResize();\n        }\n      } else {\n        c3d.style.display = 'none';\n        cPhoto.style.display = 'flex';\n        btnPhoto.style.background = '#1C1B1A';\n        btnPhoto.style.color = '#FFF';\n        btn3d.style.background = 'transparent';\n        btn3d.style.color = '#555';\n        updateStudioPhoto();\n      }\n    }\n\n    function updateStudioPhoto() {\n      const img = document.getElementById('cfg-studio-main-img');\n      if (!img) return;\n      let targetSrc = 'https:\/\/cdn.shopify.com\/s\/files\/1\/0728\/8596\/1049\/files\/studio_roble_80.jpg?v=1789514546';\n\n      if (cfgState.lavaboKey === 'colors') {\n        targetSrc = 'https:\/\/cdn.shopify.com\/s\/files\/1\/0728\/8596\/1049\/files\/studio_colors_countertop.jpg?v=1789514551';\n      } else if (cfgState.size === 120) {\n        targetSrc = 'https:\/\/cdn.shopify.com\/s\/files\/1\/0728\/8596\/1049\/files\/studio_120_double.jpg?v=1789514550';\n      } else if (cfgState.finishKey === 'blanco') {\n        targetSrc = 'https:\/\/cdn.shopify.com\/s\/files\/1\/0728\/8596\/1049\/files\/studio_blanco_80.jpg?v=1789514547';\n      } else if (cfgState.finishKey === 'ceniza') {\n        targetSrc = 'https:\/\/cdn.shopify.com\/s\/files\/1\/0728\/8596\/1049\/files\/studio_ceniza_80.jpg?v=1789514548';\n      } else {\n        targetSrc = 'https:\/\/cdn.shopify.com\/s\/files\/1\/0728\/8596\/1049\/files\/studio_roble_80.jpg?v=1789514546';\n      }\n\n      img.style.opacity = '0.3';\n      setTimeout(() =\u003e {\n        img.src = targetSrc;\n        img.onload = () =\u003e { img.style.opacity = '1'; };\n      }, 60);\n    }\n\n    \/\/ ==========================================\n    \/\/ MOTOR 3D THREE.JS DE ULTRA-ALTA DEFINICIÓN\n    \/\/ ==========================================\n    let scene, camera, renderer, controls;\n    let muebleMeshGroup, drawerGroup1, drawerGroup2;\n    let lavaboFlatGroup, lavaboColorsGroup;\n    let mirrorGroup, columnGroup;\n    let drawersOpen = false;\n\n    \/\/ GENERADOR DE TEXTURA INMEDIATA DE ALTA DEFINICIÓN (ROBLE COSTA NATURAL)\n    function createProceduralOakTexture(isHorizontal) {\n      const canvas = document.createElement('canvas');\n      canvas.width = 1024;\n      canvas.height = 512;\n      const ctx = canvas.getContext('2d');\n\n      const grad = isHorizontal \n        ? ctx.createLinearGradient(0, 0, 0, 512)\n        : ctx.createLinearGradient(0, 0, 1024, 0);\n      grad.addColorStop(0.0, '#C6A582');\n      grad.addColorStop(0.2, '#B89774');\n      grad.addColorStop(0.4, '#C9A986');\n      grad.addColorStop(0.7, '#B59471');\n      grad.addColorStop(1.0, '#C2A17E');\n      ctx.fillStyle = grad;\n      ctx.fillRect(0, 0, 1024, 512);\n\n      const numLines = 75;\n      for (let i = 0; i \u003c numLines; i++) {\n        ctx.beginPath();\n        const alpha = 0.05 + Math.random() * 0.09;\n        const isDark = Math.random() \u003e 0.32;\n        ctx.strokeStyle = isDark ? `rgba(92, 64, 40, ${alpha})` : `rgba(240, 222, 200, ${alpha * 0.9})`;\n        ctx.lineWidth = 1 + Math.random() * 3.5;\n\n        if (isHorizontal) {\n          const y = (i \/ numLines) * 512 + (Math.random() - 0.5) * 16;\n          ctx.moveTo(0, y);\n          for (let x = 0; x \u003c= 1024; x += 32) {\n            const wave = Math.sin(x * 0.007 + i * 0.55) * 5.5 + Math.cos(x * 0.016 + i) * 3;\n            ctx.lineTo(x, y + wave);\n          }\n        } else {\n          const x = (i \/ numLines) * 1024 + (Math.random() - 0.5) * 16;\n          ctx.moveTo(x, 0);\n          for (let y = 0; y \u003c= 512; y += 32) {\n            const wave = Math.sin(y * 0.007 + i * 0.55) * 5.5 + Math.cos(y * 0.016 + i) * 3;\n            ctx.lineTo(x + wave, y);\n          }\n        }\n        ctx.stroke();\n      }\n\n      ctx.fillStyle = 'rgba(75, 48, 28, 0.07)';\n      for (let p = 0; p \u003c 1600; p++) {\n        const px = Math.random() * 1024;\n        const py = Math.random() * 512;\n        if (isHorizontal) {\n          ctx.fillRect(px, py, 4 + Math.random() * 14, 1.2);\n        } else {\n          ctx.fillRect(px, py, 1.2, 4 + Math.random() * 14);\n        }\n      }\n\n      const tex = new THREE.CanvasTexture(canvas);\n      tex.wrapS = THREE.RepeatWrapping;\n      tex.wrapT = THREE.RepeatWrapping;\n      return tex;\n    }\n\n    function createFloorTexture() {\n      const canvas = document.createElement('canvas');\n      canvas.width = 512;\n      canvas.height = 512;\n      const ctx = canvas.getContext('2d');\n      ctx.fillStyle = '#DCD3C6';\n      ctx.fillRect(0, 0, 512, 512);\n\n      ctx.strokeStyle = 'rgba(135, 115, 95, 0.28)';\n      ctx.lineWidth = 2;\n      for (let y = 0; y \u003c= 512; y += 64) {\n        ctx.beginPath();\n        ctx.moveTo(0, y);\n        ctx.lineTo(512, y);\n        ctx.stroke();\n      }\n      const tex = new THREE.CanvasTexture(canvas);\n      tex.wrapS = THREE.RepeatWrapping;\n      tex.wrapT = THREE.RepeatWrapping;\n      tex.repeat.set(4, 4);\n      return tex;\n    }\n\n    function createWallTexture() {\n      const canvas = document.createElement('canvas');\n      canvas.width = 512;\n      canvas.height = 512;\n      const ctx = canvas.getContext('2d');\n      ctx.fillStyle = '#ECE7E1';\n      ctx.fillRect(0, 0, 512, 512);\n\n      ctx.fillStyle = 'rgba(0,0,0,0.015)';\n      for (let i = 0; i \u003c 2500; i++) {\n        ctx.fillRect(Math.random() * 512, Math.random() * 512, 2, 2);\n      }\n      const tex = new THREE.CanvasTexture(canvas);\n      tex.wrapS = THREE.RepeatWrapping;\n      tex.wrapT = THREE.RepeatWrapping;\n      tex.repeat.set(3, 3);\n      return tex;\n    }\n\n    let texOakHorizontal, texOakVertical, texFloor, texWall;\n    let ceramicGlazeMat, chromeShineMat, tapBlackMat, sageGreenBowlMat;\n\n    \/\/ CARGADOR PBR REAL\n    const texLoader = new THREE.TextureLoader();\n    texLoader.setCrossOrigin('anonymous');\n    let pbrRobleLoaded = false;\n    let pbrRobleTex = null;\n\n    texLoader.load('https:\/\/cdn.shopify.com\/s\/files\/1\/0728\/8596\/1049\/files\/roble_wood_pbr.jpg?v=1789513516', function(tex) {\n      tex.wrapS = THREE.RepeatWrapping;\n      tex.wrapT = THREE.RepeatWrapping;\n      tex.encoding = THREE.sRGBEncoding;\n      tex.needsUpdate = true;\n      pbrRobleTex = tex;\n      pbrRobleLoaded = true;\n      if (scene \u0026\u0026 cfgState.finishKey === 'roble') {\n        rebuildMueble3D();\n        if (cfgState.lavaboKey === 'colors') rebuildLavabo3D();\n      }\n    });\n\n    function initMaterials() {\n      texOakHorizontal = createProceduralOakTexture(true);\n      texOakVertical = createProceduralOakTexture(false);\n      texFloor = createFloorTexture();\n      texWall = createWallTexture();\n\n      \/\/ Cerámica blanca esmaltada suave y brillante\n      ceramicGlazeMat = new THREE.MeshPhysicalMaterial({\n        color: 0xFFFFFF,\n        roughness: 0.05,\n        metalness: 0.02,\n        clearcoat: 1.0,\n        clearcoatRoughness: 0.03,\n        reflectivity: 0.98\n      });\n\n      \/\/ Cromo espejo brillante\n      chromeShineMat = new THREE.MeshStandardMaterial({\n        color: 0xFFFFFF,\n        metalness: 0.98,\n        roughness: 0.04\n      });\n\n      \/\/ Negro mate satinado\n      tapBlackMat = new THREE.MeshStandardMaterial({\n        color: 0x1C1C1E,\n        roughness: 0.28,\n        metalness: 0.70\n      });\n\n      \/\/ Bol COLORS Verde Salvia mate\n      sageGreenBowlMat = new THREE.MeshStandardMaterial({\n        color: 0x5C6D5B,\n        roughness: 0.50,\n        metalness: 0.02\n      });\n    }\n\n    function initThreeJS() {\n      const container = document.getElementById('view-container-3d');\n      const canvas = document.getElementById('visobath-3d-canvas');\n      if (!container || !canvas) return;\n\n      const width = container.clientWidth || 800;\n      const height = container.clientHeight || 580;\n\n      scene = new THREE.Scene();\n      scene.background = new THREE.Color(0xEEEAE4);\n\n      \/\/ CÁMARA ELEVADA 3\/4 EXACTA A LA FOTOGRAFÍA OFICIAL\n      camera = new THREE.PerspectiveCamera(34, width \/ height, 0.1, 100);\n      camera.position.set(1.18, 1.38, 1.85);\n\n      renderer = new THREE.WebGLRenderer({ canvas: canvas, antialias: true, powerPreference: \"high-performance\" });\n      renderer.setSize(width, height);\n      renderer.setPixelRatio(Math.min(window.devicePixelRatio, 2));\n      renderer.shadowMap.enabled = true;\n      renderer.shadowMap.type = THREE.PCFSoftShadowMap;\n      renderer.toneMapping = THREE.ACESFilmicToneMapping;\n      renderer.toneMappingExposure = 1.02;\n      renderer.outputEncoding = THREE.sRGBEncoding;\n\n      if (typeof THREE.RoomEnvironment !== 'undefined') {\n        const pmremGenerator = new THREE.PMREMGenerator(renderer);\n        pmremGenerator.compileEquirectangularShader();\n        const roomEnv = new THREE.RoomEnvironment();\n        scene.environment = pmremGenerator.fromScene(roomEnv).texture;\n      }\n\n      initMaterials();\n\n      controls = new THREE.OrbitControls(camera, renderer.domElement);\n      controls.enableDamping = true;\n      controls.dampingFactor = 0.05;\n      controls.maxPolarAngle = Math.PI \/ 2 - 0.02;\n      controls.minDistance = 0.85;\n      controls.maxDistance = 4.2;\n      controls.target.set(0, 0.72, 0);\n      controls.update();\n\n      setupLighting();\n      setupArchitecturalEnvironment();\n      buildModularScene();\n\n      window.addEventListener('resize', onWindowResize);\n      animate();\n    }\n\n    function setupLighting() {\n      \/\/ Luz hemisférica con rebote cálido\n      const hemiLight = new THREE.HemisphereLight(0xFFFFFF, 0xDDD4C7, 0.70);\n      hemiLight.position.set(0, 10, 0);\n      scene.add(hemiLight);\n\n      \/\/ Luz principal cálida de estudio (Key light)\n      const dirLight = new THREE.DirectionalLight(0xFFFAF2, 1.10);\n      dirLight.position.set(2.2, 3.8, 2.5);\n      dirLight.castShadow = true;\n      dirLight.shadow.mapSize.width = 2048;\n      dirLight.shadow.mapSize.height = 2048;\n      dirLight.shadow.camera.near = 0.5;\n      dirLight.shadow.camera.far = 10;\n      dirLight.shadow.bias = -0.0003;\n      dirLight.shadow.radius = 1.8;\n      scene.add(dirLight);\n\n      \/\/ Luz de relleno lateral suave\n      const fillLight = new THREE.DirectionalLight(0xFFF3E4, 0.40);\n      fillLight.position.set(-2.5, 2.0, 1.4);\n      scene.add(fillLight);\n    }\n\n    function setupArchitecturalEnvironment() {\n      \/\/ Suelo de lamas de madera clara\n      const floorGeo = new THREE.PlaneGeometry(8, 8);\n      const floorMat = new THREE.MeshStandardMaterial({ map: texFloor, roughness: 0.52, metalness: 0.02 });\n      const floor = new THREE.Mesh(floorGeo, floorMat);\n      floor.rotation.x = -Math.PI \/ 2;\n      floor.receiveShadow = true;\n      scene.add(floor);\n\n      \/\/ Pared trasera de microcemento cálido situada justo tras el mueble\n      const wallGeo = new THREE.PlaneGeometry(8, 5);\n      const wallMat = new THREE.MeshStandardMaterial({ map: texWall, roughness: 0.70, metalness: 0.02 });\n      const wall = new THREE.Mesh(wallGeo, wallMat);\n      wall.position.set(0, 2.5, -0.238);\n      wall.receiveShadow = true;\n      scene.add(wall);\n\n      \/\/ Sombra de contacto arquitectónica bajo el mueble suspendido\n      const shadowGeo = new THREE.PlaneGeometry(1.6, 0.65);\n      const sCanvas = document.createElement('canvas');\n      sCanvas.width = 128;\n      sCanvas.height = 128;\n      const sCtx = sCanvas.getContext('2d');\n      const grad = sCtx.createRadialGradient(64, 64, 8, 64, 64, 60);\n      grad.addColorStop(0, 'rgba(0,0,0,0.38)');\n      grad.addColorStop(0.7, 'rgba(0,0,0,0.08)');\n      grad.addColorStop(1, 'rgba(0,0,0,0)');\n      sCtx.fillStyle = grad;\n      sCtx.fillRect(0, 0, 128, 128);\n      const shadowTex = new THREE.CanvasTexture(sCanvas);\n      const shadowMat = new THREE.MeshBasicMaterial({ map: shadowTex, transparent: true, opacity: 0.85, depthWrite: false });\n      const shadowPlane = new THREE.Mesh(shadowGeo, shadowMat);\n      shadowPlane.rotation.x = -Math.PI \/ 2;\n      shadowPlane.position.set(0, 0.005, 0.02);\n      scene.add(shadowPlane);\n    }\n\n    \/\/ MATERIAL DE MADERA \/ ACABADO\n    function getWoodMaterial(finishType, isHorizontal) {\n      if (finishType === 'roble') {\n        let mapTex = isHorizontal ? texOakHorizontal : texOakVertical;\n        if (pbrRobleLoaded \u0026\u0026 pbrRobleTex) {\n          if (isHorizontal) {\n            mapTex = pbrRobleTex;\n          } else {\n            const vTex = pbrRobleTex.clone();\n            vTex.rotation = Math.PI \/ 2;\n            vTex.center.set(0.5, 0.5);\n            mapTex = vTex;\n          }\n        }\n        return new THREE.MeshStandardMaterial({\n          map: mapTex,\n          color: 0xFFFFFF,\n          roughness: 0.38,\n          metalness: 0.02\n        });\n      } else if (finishType === 'blanco') {\n        return new THREE.MeshStandardMaterial({\n          color: 0xF7F6F3,\n          roughness: 0.28,\n          metalness: 0.02\n        });\n      } else if (finishType === 'ceniza') {\n        return new THREE.MeshStandardMaterial({\n          color: 0x343434,\n          roughness: 0.35,\n          metalness: 0.03\n        });\n      } else if (finishType === 'avio') {\n        return new THREE.MeshStandardMaterial({\n          color: 0x445562,\n          roughness: 0.32,\n          metalness: 0.03\n        });\n      }\n      return new THREE.MeshStandardMaterial({ map: texOakHorizontal });\n    }\n\n    function buildModularScene() {\n      muebleMeshGroup = new THREE.Group();\n      scene.add(muebleMeshGroup);\n\n      rebuildMueble3D();\n      rebuildLavabo3D();\n      rebuildMirror3D();\n      rebuildColumn3D();\n    }\n\n    \/\/ RECONSTRUCCIÓN CON PROPORCIONES ASIMÉTRICAS OFICIALES:\n    \/\/ CAJÓN SUPERIOR ESTRECHO (19.5 CM) + CAJÓN INFERIOR AMPLIO (30.5 CM)\n    function rebuildMueble3D() {\n      while (muebleMeshGroup.children.length \u003e 0) {\n        muebleMeshGroup.remove(muebleMeshGroup.children[0]);\n      }\n\n      const w = cfgState.size \/ 100;\n      const h = 0.54; \/\/ Altura total oficial 54 cm\n      const d = 0.46; \/\/ Fondo oficial 46 cm\n      const matSides = getWoodMaterial(cfgState.finishKey, false); \/\/ Veta vertical\n      const matDrawers = getWoodMaterial(cfgState.finishKey, true); \/\/ Veta horizontal\n\n      \/\/ CASCO EXTERIOR (Laterales de 16 mm + base)\n      const carcaseGroup = new THREE.Group();\n\n      const sideGeo = new THREE.BoxGeometry(0.016, h, d);\n      const sideL = new THREE.Mesh(sideGeo, matSides);\n      sideL.position.set(-w\/2 + 0.008, 0.52, 0);\n      sideL.castShadow = true;\n      sideL.receiveShadow = true;\n      carcaseGroup.add(sideL);\n\n      const sideR = new THREE.Mesh(sideGeo, matSides);\n      sideR.position.set(w\/2 - 0.008, 0.52, 0);\n      sideR.castShadow = true;\n      sideR.receiveShadow = true;\n      carcaseGroup.add(sideR);\n\n      const botGeo = new THREE.BoxGeometry(w - 0.032, 0.016, d);\n      const botMesh = new THREE.Mesh(botGeo, matDrawers);\n      botMesh.position.set(0, 0.52 - (h\/2) + 0.008, 0);\n      botMesh.castShadow = true;\n      botMesh.receiveShadow = true;\n      carcaseGroup.add(botMesh);\n\n      \/\/ Gola interior oscura\n      const golaMat = new THREE.MeshStandardMaterial({ color: 0x1E1D1C, roughness: 0.5 });\n      const golaMesh = new THREE.Mesh(new THREE.BoxGeometry(w - 0.032, 0.024, 0.04), golaMat);\n      golaMesh.position.set(0, 0.52 + 0.025, d\/2 - 0.02);\n      carcaseGroup.add(golaMesh);\n\n      muebleMeshGroup.add(carcaseGroup);\n\n      \/\/ CAJONES ASIMÉTRICOS\n      drawerGroup1 = new THREE.Group(); \/\/ Superior estrecho\n      drawerGroup2 = new THREE.Group(); \/\/ Inferior amplio\n\n      const topDrawerH = 0.195; \/\/ 19.5 cm -\u003e ESTRECHO (idéntico a catálogo)\n      const botDrawerH = 0.305; \/\/ 30.5 cm -\u003e AMPLIO\n      const drawerW = w - 0.006;\n\n      const drawerMetalMat = new THREE.MeshStandardMaterial({ color: 0x2A2A2B, metalness: 0.7, roughness: 0.3 });\n      const drawerInnerMat = new THREE.MeshStandardMaterial({ color: 0xDCD6CD, roughness: 0.5 });\n\n      function buildDrawerInterior(isTop, innerH) {\n        const box = new THREE.Group();\n        const innerW = w - 0.045;\n        const innerD = d - 0.05;\n\n        const base = new THREE.Mesh(new THREE.BoxGeometry(innerW, 0.012, innerD), drawerInnerMat);\n        base.position.set(0, -innerH\/2 + 0.006, -innerD\/2);\n        box.add(base);\n\n        const sl = new THREE.Mesh(new THREE.BoxGeometry(0.012, innerH, innerD), drawerMetalMat);\n        sl.position.set(-innerW\/2 + 0.006, 0, -innerD\/2);\n        box.add(sl);\n\n        const sr = new THREE.Mesh(new THREE.BoxGeometry(0.012, innerH, innerD), drawerMetalMat);\n        sr.position.set(innerW\/2 - 0.006, 0, -innerD\/2);\n        box.add(sr);\n\n        if (isTop) {\n          const uMesh = new THREE.Mesh(new THREE.BoxGeometry(0.18, innerH, 0.22), drawerMetalMat);\n          uMesh.position.set(0, 0, -innerD\/2 + 0.02);\n          box.add(uMesh);\n\n          const div1 = new THREE.Mesh(new THREE.BoxGeometry(0.008, innerH - 0.02, innerD - 0.04), drawerMetalMat);\n          div1.position.set(-innerW\/4, 0, -innerD\/2);\n          box.add(div1);\n\n          const div2 = new THREE.Mesh(new THREE.BoxGeometry(0.008, innerH - 0.02, innerD - 0.04), drawerMetalMat);\n          div2.position.set(innerW\/4, 0, -innerD\/2);\n          box.add(div2);\n        }\n        return box;\n      }\n\n      \/\/ 1. CAJÓN SUPERIOR ESTRECHO\n      const topY = 0.52 + (h\/2) - (topDrawerH\/2) - 0.006;\n      const front1 = new THREE.Mesh(new THREE.BoxGeometry(drawerW, topDrawerH, 0.019), matDrawers);\n      front1.castShadow = true;\n      front1.receiveShadow = true;\n      drawerGroup1.add(front1);\n      drawerGroup1.add(buildDrawerInterior(true, topDrawerH - 0.04));\n      drawerGroup1.position.set(0, topY, (d \/ 2) + 0.01);\n\n      const handleTop = new THREE.Mesh(new THREE.BoxGeometry(drawerW * 0.94, 0.014, 0.020), tapBlackMat);\n      handleTop.position.set(0, topDrawerH\/2 - 0.008, 0.016);\n      handleTop.castShadow = true;\n      drawerGroup1.add(handleTop);\n\n      \/\/ 2. CAJÓN INFERIOR AMPLIO\n      const botY = 0.52 - (h\/2) + (botDrawerH\/2) + 0.006;\n      const front2 = new THREE.Mesh(new THREE.BoxGeometry(drawerW, botDrawerH, 0.019), matDrawers);\n      front2.castShadow = true;\n      front2.receiveShadow = true;\n      drawerGroup2.add(front2);\n      drawerGroup2.add(buildDrawerInterior(false, botDrawerH - 0.04));\n      drawerGroup2.position.set(0, botY, (d \/ 2) + 0.01);\n\n      const handleBot = new THREE.Mesh(new THREE.BoxGeometry(drawerW * 0.94, 0.014, 0.020), tapBlackMat);\n      handleBot.position.set(0, botDrawerH\/2 - 0.008, 0.016);\n      handleBot.castShadow = true;\n      drawerGroup2.add(handleBot);\n\n      muebleMeshGroup.add(drawerGroup1);\n      muebleMeshGroup.add(drawerGroup2);\n    }\n\n    \/\/ =========================================================================\n    \/\/ GEOMETRÍA PARAMÉTRICA CONTINUA DEL SENO CERÁMICO (CERO FORMAS CUADRADAS)\n    \/\/ Se esculpe una sola malla continua con curvatura suave y esquinas redondeadas\n    \/\/ =========================================================================\n    function createSmoothSculptedBasin(w, d, isDouble) {\n      const bGrp = new THREE.Group();\n      const topY = 0.812;\n      const depth = 0.125; \/\/ Profundidad suave del seno\n      const bw = isDouble ? 0.42 : 0.48; \/\/ Ancho rectangular\n      const bd = 0.28; \/\/ Fondo rectangular\n      const cornerR = 0.055; \/\/ Radio de redondeo de esquinas\n      const slopeW = 0.058; \/\/ Ancho de transición curva suave\n\n      \/\/ Malla subdividida para el plano de la encimera y seno esculpido\n      const nx = isDouble ? 110 : 80;\n      const nz = 60;\n      const geo = new THREE.PlaneGeometry(w, d, nx, nz);\n      geo.rotateX(-Math.PI \/ 2); \/\/ Situar en plano XZ\n      const pos = geo.attributes.position;\n\n      function distToBasin(x, z, basinX) {\n        const dx = Math.max(0, Math.abs(x - basinX) - (bw\/2 - cornerR));\n        const dz = Math.max(0, Math.abs(z - 0.01) - (bd\/2 - cornerR));\n        return Math.sqrt(dx*dx + dz*dz) - cornerR;\n      }\n\n      for (let i = 0; i \u003c pos.count; i++) {\n        const x = pos.getX(i);\n        const z = pos.getZ(i);\n\n        let dist = 0;\n        if (isDouble) {\n          const d1 = distToBasin(x, z, -0.33);\n          const d2 = distToBasin(x, z, 0.33);\n          dist = Math.min(d1, d2);\n        } else {\n          dist = distToBasin(x, z, 0);\n        }\n\n        let y = topY;\n        if (dist \u003c= -slopeW) {\n          \/\/ Fondo del seno con ligera curvatura cóncava\n          const bottomR = dist + slopeW;\n          y = topY - depth + (bottomR * bottomR * 0.4);\n        } else if (dist \u003c 0) {\n          \/\/ Curva de caída suave Hermite cúbica (sin aristas duras)\n          const t = (dist - (-slopeW)) \/ slopeW;\n          const smoothT = t * t * (3.0 - 2.0 * t);\n          y = (topY - depth) + (smoothT * depth);\n        }\n        pos.setY(i, y);\n      }\n\n      pos.needsUpdate = true;\n      geo.computeVertexNormals();\n\n      const basinMesh = new THREE.Mesh(geo, ceramicGlazeMat);\n      basinMesh.castShadow = true;\n      basinMesh.receiveShadow = true;\n      bGrp.add(basinMesh);\n\n      \/\/ Faldones perimetrales de 20 mm para dar grosor a la encimera cerámica\n      const thickMat = ceramicGlazeMat;\n      const skirtH = 0.020;\n      const skirtY = topY - (skirtH \/ 2);\n\n      \/\/ Frontal\n      const frontSkirt = new THREE.Mesh(new THREE.BoxGeometry(w, skirtH, 0.014), thickMat);\n      frontSkirt.position.set(0, skirtY, d\/2 - 0.007);\n      frontSkirt.castShadow = true;\n      bGrp.add(frontSkirt);\n\n      \/\/ Trasero\n      const backSkirt = new THREE.Mesh(new THREE.BoxGeometry(w, skirtH, 0.014), thickMat);\n      backSkirt.position.set(0, skirtY, -d\/2 + 0.007);\n      bGrp.add(backSkirt);\n\n      \/\/ Lateral izquierdo\n      const leftSkirt = new THREE.Mesh(new THREE.BoxGeometry(0.014, skirtH, d), thickMat);\n      leftSkirt.position.set(-w\/2 + 0.007, skirtY, 0);\n      leftSkirt.castShadow = true;\n      bGrp.add(leftSkirt);\n\n      \/\/ Lateral derecho\n      const rightSkirt = new THREE.Mesh(new THREE.BoxGeometry(0.014, skirtH, d), thickMat);\n      rightSkirt.position.set(w\/2 - 0.007, skirtY, 0);\n      rightSkirt.castShadow = true;\n      bGrp.add(rightSkirt);\n\n      \/\/ VÁLVULA CLICK-CLACK CROMADA, REBOSADERO Y GRIFO MONOMANDO\n      function addBasinDetails(basinX) {\n        \/\/ Válvula Click-Clack en el fondo del seno\n        const drain = new THREE.Mesh(new THREE.CylinderGeometry(0.035, 0.035, 0.008, 32), chromeShineMat);\n        drain.position.set(basinX, topY - depth + 0.004, 0.01);\n        bGrp.add(drain);\n\n        \/\/ Rebosadero oblongo cromado en la pared trasera del seno\n        const ov = new THREE.Mesh(new THREE.BoxGeometry(0.045, 0.010, 0.004), chromeShineMat);\n        ov.position.set(basinX, topY - 0.038, 0.01 - (bd\/2) + 0.018);\n        bGrp.add(ov);\n\n        \/\/ GRIFO MONOMANDO CROMO ESPEJO (Idéntico a la foto)\n        const fGrp = new THREE.Group();\n\n        const fBase = new THREE.Mesh(new THREE.CylinderGeometry(0.022, 0.024, 0.014, 32), chromeShineMat);\n        fBase.position.set(basinX, topY + 0.007, -0.14);\n        fGrp.add(fBase);\n\n        const fBody = new THREE.Mesh(new THREE.CylinderGeometry(0.016, 0.016, 0.12, 32), chromeShineMat);\n        fBody.position.set(basinX, topY + 0.065, -0.14);\n        fBody.castShadow = true;\n        fGrp.add(fBody);\n\n        const fSpout = new THREE.Mesh(new THREE.BoxGeometry(0.014, 0.012, 0.10), chromeShineMat);\n        fSpout.position.set(basinX, topY + 0.115, -0.09);\n        fSpout.castShadow = true;\n        fGrp.add(fSpout);\n\n        const fNozzle = new THREE.Mesh(new THREE.CylinderGeometry(0.008, 0.008, 0.012, 16), chromeShineMat);\n        fNozzle.position.set(basinX, topY + 0.104, -0.045);\n        fGrp.add(fNozzle);\n\n        const fLever = new THREE.Mesh(new THREE.BoxGeometry(0.012, 0.006, 0.075), chromeShineMat);\n        fLever.position.set(basinX, topY + 0.13, -0.14);\n        fLever.rotation.x = 0.12;\n        fLever.castShadow = true;\n        fGrp.add(fLever);\n\n        bGrp.add(fGrp);\n      }\n\n      if (isDouble) {\n        addBasinDetails(-0.33);\n        addBasinDetails(0.33);\n      } else {\n        addBasinDetails(0);\n      }\n\n      return bGrp;\n    }\n\n    function rebuildLavabo3D() {\n      if (lavaboFlatGroup) scene.remove(lavaboFlatGroup);\n      if (lavaboColorsGroup) scene.remove(lavaboColorsGroup);\n\n      const w = (cfgState.size \/ 100) + 0.016;\n      const d = 0.465;\n\n      if (cfgState.lavaboKey === 'flat') {\n        const isDouble = (cfgState.size === 120);\n        lavaboFlatGroup = createSmoothSculptedBasin(w, d, isDouble);\n        scene.add(lavaboFlatGroup);\n\n      } else {\n        \/\/ LAVABO COLOURS: ENCIMERA EN MADERA + BOL CILÍNDRICO VERDE SALVIA MATE + GRIFO ALTO NEGRO\n        lavaboColorsGroup = new THREE.Group();\n\n        const tapaMat = getWoodMaterial(cfgState.finishKey, true);\n        const tapaGeo = new THREE.BoxGeometry(w, 0.024, d);\n        const tapa = new THREE.Mesh(tapaGeo, tapaMat);\n        tapa.position.set(0, 0.806, 0.005);\n        tapa.castShadow = true;\n        tapa.receiveShadow = true;\n        lavaboColorsGroup.add(tapa);\n\n        function buildColorsBowl(xCenter) {\n          const cGrp = new THREE.Group();\n          const bowlR = 0.18;\n          const bowlH = 0.13;\n\n          const bowlMesh = new THREE.Mesh(new THREE.CylinderGeometry(bowlR, bowlR * 0.96, bowlH, 48), sageGreenBowlMat);\n          bowlMesh.position.set(xCenter, 0.806 + 0.012 + (bowlH\/2), 0.01);\n          bowlMesh.castShadow = true;\n          bowlMesh.receiveShadow = true;\n          cGrp.add(bowlMesh);\n\n          const innerR = bowlR - 0.015;\n          const innerMesh = new THREE.Mesh(new THREE.CylinderGeometry(innerR, innerR * 0.95, bowlH - 0.01, 48), sageGreenBowlMat);\n          innerMesh.position.set(xCenter, 0.806 + 0.012 + (bowlH\/2) + 0.008, 0.01);\n          cGrp.add(innerMesh);\n\n          const drain = new THREE.Mesh(new THREE.CylinderGeometry(0.034, 0.034, 0.008, 24), sageGreenBowlMat);\n          drain.position.set(xCenter, 0.806 + 0.022, 0.01);\n          cGrp.add(drain);\n\n          \/\/ Grifo caño alto negro mate\n          const tapG = new THREE.Group();\n          const tBase = new THREE.Mesh(new THREE.CylinderGeometry(0.020, 0.022, 0.018, 20), tapBlackMat);\n          tBase.position.set(xCenter, 0.826, -0.15);\n          tapG.add(tBase);\n\n          const tCol = new THREE.Mesh(new THREE.CylinderGeometry(0.017, 0.017, 0.28, 20), tapBlackMat);\n          tCol.position.set(xCenter, 0.965, -0.15);\n          tCol.castShadow = true;\n          tapG.add(tCol);\n\n          const tSpout = new THREE.Mesh(new THREE.BoxGeometry(0.015, 0.014, 0.13), tapBlackMat);\n          tSpout.position.set(xCenter, 1.095, -0.095);\n          tSpout.castShadow = true;\n          tapG.add(tSpout);\n\n          const tNozzle = new THREE.Mesh(new THREE.CylinderGeometry(0.008, 0.008, 0.02, 16), tapBlackMat);\n          tNozzle.position.set(xCenter, 1.08, -0.04);\n          tapG.add(tNozzle);\n\n          const tLever = new THREE.Mesh(new THREE.CylinderGeometry(0.006, 0.006, 0.06, 16), tapBlackMat);\n          tLever.position.set(xCenter, 1.12, -0.15);\n          tLever.rotation.x = 0.25;\n          tLever.castShadow = true;\n          tapG.add(tLever);\n\n          cGrp.add(tapG);\n          return cGrp;\n        }\n\n        if (cfgState.size === 120) {\n          lavaboColorsGroup.add(buildColorsBowl(-0.33));\n          lavaboColorsGroup.add(buildColorsBowl(0.33));\n        } else {\n          lavaboColorsGroup.add(buildColorsBowl(0));\n        }\n\n        scene.add(lavaboColorsGroup);\n      }\n    }\n\n    \/\/ ESPEJO CÁPSULA EXACTO AL CATÁLOGO CON HALO LED CÁLIDO Y APLIQUE NEGRO\n    function rebuildMirror3D() {\n      if (mirrorGroup) scene.remove(mirrorGroup);\n      if (cfgState.espejoKey === 'none') return;\n\n      mirrorGroup = new THREE.Group();\n\n      const mirrorSurfaceMat = new THREE.MeshStandardMaterial({\n        color: 0xF0F6FA,\n        roughness: 0.04,\n        metalness: 0.85\n      });\n\n      const glowMat = new THREE.MeshBasicMaterial({\n        color: 0xFFF4DE,\n        transparent: true,\n        opacity: 0.96\n      });\n\n      if (cfgState.espejoKey === 'ella') {\n        const mw = 0.80;\n        const mh = 0.48;\n        const radius = mh \/ 2;\n\n        const capsuleShape = new THREE.Shape();\n        capsuleShape.moveTo(-mw\/2 + radius, -radius);\n        capsuleShape.lineTo(mw\/2 - radius, -radius);\n        capsuleShape.absarc(mw\/2 - radius, 0, radius, -Math.PI\/2, Math.PI\/2, false);\n        capsuleShape.lineTo(-mw\/2 + radius, radius);\n        capsuleShape.absarc(-mw\/2 + radius, 0, radius, Math.PI\/2, Math.PI * 1.5, false);\n\n        const mirrorMesh = new THREE.Mesh(new THREE.ShapeGeometry(capsuleShape, 48), mirrorSurfaceMat);\n        mirrorMesh.position.set(0, 1.40, -0.222);\n        mirrorGroup.add(mirrorMesh);\n\n        \/\/ Borde LED brillante perimetral (Efecto halo perimetral sobre pared)\n        const haloR = radius + 0.024;\n        const haloShape = new THREE.Shape();\n        haloShape.moveTo(-mw\/2 + radius, -haloR);\n        haloShape.lineTo(mw\/2 - radius, -haloR);\n        haloShape.absarc(mw\/2 - radius, 0, haloR, -Math.PI\/2, Math.PI\/2, false);\n        haloShape.lineTo(-mw\/2 + radius, haloR);\n        haloShape.absarc(-mw\/2 + radius, 0, haloR, Math.PI\/2, Math.PI * 1.5, false);\n\n        const haloMesh = new THREE.Mesh(new THREE.ShapeGeometry(haloShape, 48), glowMat);\n        haloMesh.position.set(0, 1.40, -0.230);\n        mirrorGroup.add(haloMesh);\n\n        const mirrorLight = new THREE.PointLight(0xFFE8C2, 1.4, 2.0);\n        mirrorLight.position.set(0, 1.40, -0.18);\n        mirrorGroup.add(mirrorLight);\n\n        \/\/ LUMINARIA LED NEGRA SLIM MONTADA EN EL CENTRO SUPERIOR DEL ESPEJO\n        if (cfgState.lightKey !== 'none') {\n          const lampMat = (cfgState.lightKey === 'chrome') ? chromeShineMat : tapBlackMat;\n          \n          const lBase = new THREE.Mesh(new THREE.BoxGeometry(0.08, 0.035, 0.04), lampMat);\n          lBase.position.set(0, 1.40 + radius + 0.015, -0.215);\n          mirrorGroup.add(lBase);\n\n          const lArm = new THREE.Mesh(new THREE.BoxGeometry(0.024, 0.018, 0.075), lampMat);\n          lArm.position.set(0, 1.40 + radius + 0.015, -0.175);\n          mirrorGroup.add(lArm);\n\n          const lBar = new THREE.Mesh(new THREE.BoxGeometry(0.38, 0.016, 0.026), lampMat);\n          lBar.position.set(0, 1.40 + radius + 0.015, -0.138);\n          mirrorGroup.add(lBar);\n\n          const lDiff = new THREE.Mesh(new THREE.BoxGeometry(0.36, 0.004, 0.022), new THREE.MeshBasicMaterial({ color: 0xFFFFFF }));\n          lDiff.position.set(0, 1.40 + radius + 0.006, -0.138);\n          mirrorGroup.add(lDiff);\n\n          const spotL = new THREE.PointLight(0xFFFAF0, 1.2, 1.6);\n          spotL.position.set(0, 1.40 + radius - 0.02, -0.12);\n          mirrorGroup.add(spotL);\n        }\n\n      } else if (cfgState.espejoKey === 'circular') {\n        const r = 0.40;\n        const circleGeo = new THREE.CircleGeometry(r, 48);\n        const mirrorMesh = new THREE.Mesh(circleGeo, mirrorSurfaceMat);\n        mirrorMesh.position.set(0, 1.40, -0.222);\n        mirrorGroup.add(mirrorMesh);\n\n        const haloGeo = new THREE.CircleGeometry(r + 0.024, 48);\n        const haloMesh = new THREE.Mesh(haloGeo, glowMat);\n        haloMesh.position.set(0, 1.40, -0.230);\n        mirrorGroup.add(haloMesh);\n\n        const mirrorLight = new THREE.PointLight(0xFFE8C2, 1.4, 2.0);\n        mirrorLight.position.set(0, 1.40, -0.18);\n        mirrorGroup.add(mirrorLight);\n\n        if (cfgState.lightKey !== 'none') {\n          const lampMat = (cfgState.lightKey === 'chrome') ? chromeShineMat : tapBlackMat;\n          const lBar = new THREE.Mesh(new THREE.BoxGeometry(0.32, 0.016, 0.026), lampMat);\n          lBar.position.set(0, 1.40 + r + 0.02, -0.18);\n          mirrorGroup.add(lBar);\n        }\n      }\n\n      scene.add(mirrorGroup);\n    }\n\n    \/\/ COLUMNA AUXILIAR A JUEGO\n    function rebuildColumn3D() {\n      if (columnGroup) scene.remove(columnGroup);\n      if (cfgState.colKey === 'none') return;\n\n      columnGroup = new THREE.Group();\n      const colW = 0.35;\n      const colH = 1.20;\n      const colD = 0.28;\n      const matSides = getWoodMaterial(cfgState.finishKey, false);\n      const matDoors = getWoodMaterial(cfgState.finishKey, true);\n\n      const colMesh = new THREE.Mesh(new THREE.BoxGeometry(colW, colH, colD), matSides);\n      colMesh.position.set(0, 1.05, 0);\n      colMesh.castShadow = true;\n      columnGroup.add(colMesh);\n\n      const doorTop = new THREE.Mesh(new THREE.BoxGeometry(colW - 0.008, (colH\/2) - 0.01, 0.018), matDoors);\n      doorTop.position.set(0, 1.05 + (colH\/4), colD\/2 + 0.009);\n      doorTop.castShadow = true;\n      columnGroup.add(doorTop);\n\n      const doorBot = new THREE.Mesh(new THREE.BoxGeometry(colW - 0.008, (colH\/2) - 0.01, 0.018), matDoors);\n      doorBot.position.set(0, 1.05 - (colH\/4), colD\/2 + 0.009);\n      doorBot.castShadow = true;\n      columnGroup.add(doorBot);\n\n      const offset = (cfgState.size \/ 200) + 0.26;\n      columnGroup.position.set(offset, 0, -0.09);\n      scene.add(columnGroup);\n    }\n\n    \/\/ APERTURA DE CAJONES\n    function toggleDrawers() {\n      drawersOpen = !drawersOpen;\n      const btn = document.getElementById('btn-toggle-drawers');\n      if (btn) {\n        btn.innerText = drawersOpen ? '🚪 Cerrar Cajones' : '👐 Abrir Cajones';\n        btn.style.background = drawersOpen ? '#1C1B1A' : '#9E7B56';\n      }\n\n      const targetZ1 = drawersOpen ? 0.34 : 0;\n      const targetZ2 = drawersOpen ? 0.26 : 0;\n      const d = 0.46;\n      const initialZ = (d \/ 2) + 0.01;\n\n      let step = 0;\n      const totalSteps = 24;\n      function animStep() {\n        step++;\n        const prog = step \/ totalSteps;\n        const ease = 1 - Math.pow(1 - prog, 3);\n        drawerGroup1.position.z = initialZ + (targetZ1 * ease);\n        drawerGroup2.position.z = initialZ + (targetZ2 * ease);\n        if (step \u003c totalSteps) {\n          requestAnimationFrame(animStep);\n        }\n      }\n      animStep();\n    }\n\n    \/\/ CONTROLADORES DE CÁMARA\n    function reset3DCamera(view) {\n      if (!camera || !controls) return;\n      if (view === 'perspectiva') {\n        camera.position.set(1.18, 1.38, 1.85);\n        controls.target.set(0, 0.72, 0);\n      } else if (view === 'frontal') {\n        camera.position.set(0, 0.95, 2.15);\n        controls.target.set(0, 0.95, 0);\n      } else if (view === 'detalle') {\n        camera.position.set(0.42, 1.12, 0.72);\n        controls.target.set(0, 0.81, 0);\n      }\n      controls.update();\n    }\n\n    function onWindowResize() {\n      const container = document.getElementById('view-container-3d');\n      if (!container || !renderer || !camera) return;\n      const width = container.clientWidth;\n      const height = container.clientHeight;\n      camera.aspect = width \/ height;\n      camera.updateProjectionMatrix();\n      renderer.setSize(width, height);\n    }\n\n    function animate() {\n      requestAnimationFrame(animate);\n      if (controls) controls.update();\n      if (renderer \u0026\u0026 scene \u0026\u0026 camera) {\n        renderer.render(scene, camera);\n      }\n    }\n\n    \/\/ SELECTORES DINÁMICOS\n    function setCfgModel(model) {\n      cfgState.model = model;\n      document.querySelectorAll('.cfg-model-btn').forEach(btn =\u003e {\n        if (btn.dataset.model === model) {\n          btn.style.border = '2px solid #9E7B56';\n          btn.style.background = '#FAF8F5';\n        } else {\n          btn.style.border = '1px solid #DED7CD';\n          btn.style.background = '#FFF';\n        }\n      });\n      updateSummary();\n      updateStudioPhoto();\n    }\n\n    function setCfgSize(size) {\n      cfgState.size = size;\n      document.querySelectorAll('.cfg-size-btn').forEach(btn =\u003e {\n        if (parseInt(btn.dataset.size) === size) {\n          btn.style.border = '2px solid #9E7B56';\n          btn.style.background = '#FAF8F5';\n        } else {\n          btn.style.border = '1px solid #DED7CD';\n          btn.style.background = '#FFF';\n        }\n      });\n      rebuildMueble3D();\n      rebuildLavabo3D();\n      rebuildColumn3D();\n      updateSummary();\n      updateStudioPhoto();\n    }\n\n    function setCfgFinish(key, name) {\n      cfgState.finishKey = key;\n      cfgState.finishName = name;\n      document.querySelectorAll('.cfg-finish-btn').forEach(btn =\u003e {\n        if (btn.dataset.finish === key) {\n          btn.style.border = '2px solid #9E7B56';\n          btn.style.background = '#FAF8F5';\n        } else {\n          btn.style.border = '1px solid #DED7CD';\n          btn.style.background = '#FFF';\n        }\n      });\n      rebuildMueble3D();\n      if (cfgState.lavaboKey === 'colors') rebuildLavabo3D();\n      rebuildColumn3D();\n      updateSummary();\n      updateStudioPhoto();\n    }\n\n    function setCfgLavabo(key, name) {\n      cfgState.lavaboKey = key;\n      cfgState.lavaboName = name;\n      document.querySelectorAll('.cfg-lavabo-btn').forEach(btn =\u003e {\n        if (btn.dataset.lavabo === key) {\n          btn.style.border = '2px solid #9E7B56';\n          btn.style.background = '#FAF8F5';\n        } else {\n          btn.style.border = '1px solid #DED7CD';\n          btn.style.background = '#FFF';\n        }\n      });\n      rebuildLavabo3D();\n      updateSummary();\n      updateStudioPhoto();\n    }\n\n    function setCfgEspejo(key, name) {\n      cfgState.espejoKey = key;\n      cfgState.espejoName = name;\n      document.querySelectorAll('.cfg-espejo-btn').forEach(btn =\u003e {\n        if (btn.dataset.espejo === key) {\n          btn.style.border = '2px solid #9E7B56';\n          btn.style.background = '#FAF8F5';\n        } else {\n          btn.style.border = '1px solid #DED7CD';\n          btn.style.background = '#FFF';\n        }\n      });\n      rebuildMirror3D();\n      updateSummary();\n    }\n\n    function setCfgLight(key, name) {\n      cfgState.lightKey = key;\n      cfgState.lightName = name;\n      document.querySelectorAll('.cfg-light-btn').forEach(btn =\u003e {\n        if (btn.dataset.light === key) {\n          btn.style.border = '2px solid #9E7B56';\n          btn.style.background = '#FAF8F5';\n        } else {\n          btn.style.border = '1px solid #DED7CD';\n          btn.style.background = '#FFF';\n        }\n      });\n      rebuildMirror3D();\n      updateSummary();\n    }\n\n    function setCfgColumn(key, name) {\n      cfgState.colKey = key;\n      cfgState.colName = name;\n      document.querySelectorAll('.cfg-col-btn').forEach(btn =\u003e {\n        if (btn.dataset.col === key) {\n          btn.style.border = '2px solid #9E7B56';\n          btn.style.background = '#FAF8F5';\n        } else {\n          btn.style.border = '1px solid #DED7CD';\n          btn.style.background = '#FFF';\n        }\n      });\n      rebuildColumn3D();\n      updateSummary();\n    }\n\n    function addToCartConfigured() {\n      const totalText = document.getElementById('summary-price').innerText;\n      const refText = document.getElementById('summary-ref').innerText;\n      const title = cfgState.model + ' ' + cfgState.size + 'cm (' + cfgState.finishName + ') + ' + \n                    cfgState.lavaboName + ' + ' + cfgState.espejoName + \n                    (cfgState.lightKey !== 'none' ? ' + ' + cfgState.lightName : '') + \n                    (cfgState.colKey !== 'none' ? ' + ' + cfgState.colName : '');\n      \n      const note = `Conjunto Configurado: ${title} | Ref: ${refText} | PVP: ${totalText}`;\n      alert('¡Configuración añadida con éxito!\\n\\n' + note + '\\n\\nRedirigiendo a la cesta...');\n      \n      fetch('\/cart\/add.js', {\n        method: 'POST',\n        headers: { 'Content-Type': 'application\/json' },\n        body: JSON.stringify({\n          items: [{\n            id: 48946777030969,\n            quantity: 1,\n            properties: {\n              'Configuración Completa': title,\n              'Referencia Técnica Visobath': refText,\n              'PVP Configurado': totalText\n            }\n          }]\n        })\n      }).then(() =\u003e {\n        window.location.href=\"\/it\/cart\";\n      }).catch(() =\u003e {\n        window.location.href=\"\/it\/cart\";\n      });\n    }\n\n    window.addEventListener('DOMContentLoaded', () =\u003e {\n      updateSummary();\n      setTimeout(() =\u003e {\n        initThreeJS();\n      }, 150);\n    });\n\n    if (document.readyState === 'complete' || document.readyState === 'interactive') {\n      updateSummary();\n      setTimeout(() =\u003e {\n        if (!scene) initThreeJS();\n      }, 150);\n    }\n  \u003c\/script\u003e\n\n\u003c\/div\u003e\n","brand":"Visobath","offers":[{"title":"Default Title","offer_id":59441308467545,"sku":"VIS-CONFIGURADOR-PROTOTIPO","price":425.0,"currency_code":"EUR","in_stock":true}],"thumbnail_url":"\/\/cdn.shopify.com\/s\/files\/1\/0728\/8596\/1049\/files\/visobath_3d_mounted_roble.jpg?v=1789512730","url":"https:\/\/getupdesign.es\/it\/products\/prototipo-configurador-modular-de-bano-visobath-compon-tu-conjunto-a-medida","provider":"Get Up \u0026 design interiorismo","version":"1.0","type":"link"}