{"title":"Productos personalizables","description":null,"products":[{"product_id":"elevadores-mangas-libros-estanterias-personalizado-a-medida","title":"Elevador para mangas y libros a medida","description":"\u003cp\u003eLas estanterías no siempre entienden de medidas estándar. Como coleccionistas que somos, sabemos la rabia que da comprar un elevador y ver que te sobran 4 centímetros a los lados, o que se queda corto y no te cabe ese manga que te falta.\u003c\/p\u003e\n\u003cp\u003eCon nuestros elevadores a medida, el elevador se adapta a tu estantería al centímetro, y no al revés, para que aproveches el espacio al 100 %.\u003c\/p\u003e\n\u003ch2\u003e💡 ¿Cómo funciona?\u003c\/h2\u003e\n\u003cp\u003eMide tu balda y dinos cuánto mide. Nosotros hacemos los números: te preparamos los módulos justos para cubrirla de lado a lado y te los enviamos con unas llaves para unirlos entre sí. Así el elevador queda como una sola pieza y no se separa cuando sacas o metes tomos.\u003c\/p\u003e\n\u003cdiv id=\"kmc\"\u003e\n\u003cstyle\u003e\n#kmc{--verde:#008a00;--verde-osc:#006f00;--gris:#666;--borde:#ccc;--linea:#eaeaea;--caja:#f9f9f9;\n  width:100%;max-width:560px;margin:30px auto;padding:0 10px;box-sizing:border-box;font-family:inherit;color:#111}\n#kmc *{box-sizing:border-box}\n#kmc .kmc-caja{background:#fff;padding:25px;border-radius:8px;border:1px solid var(--linea);box-shadow:0 4px 12px rgba(0,0,0,.05)}\n#kmc .kmc-titulo{margin:0 0 6px;text-align:center;font-family:Montserrat,sans-serif;font-weight:800;font-size:18px;text-transform:uppercase;letter-spacing:.5px;color:#111}\n#kmc .kmc-sub{margin:0 0 4px;text-align:center;font-size:13px;color:var(--gris)}\n#kmc .kmc-paso{margin:22px 0 0}\n#kmc .kmc-et{display:block;margin:0 0 8px;font-weight:600;font-size:14px;color:#333}\n#kmc .kmc-et small{font-weight:400;color:var(--gris);font-size:13px}\n#kmc .kmc-botones{display:flex;gap:12px;width:100%}\n#kmc .kmc-sel{flex:1;padding:12px;font-family:inherit;font-size:14px;font-weight:600;line-height:1.3;border:1px solid var(--borde);border-radius:4px;cursor:pointer;background:#fff;color:#333;text-align:center;transition:border-color .15s,background-color .15s,color .15s}\n#kmc .kmc-sel span{display:block;font-size:11px;font-weight:400;color:var(--gris);margin-top:3px}\n#kmc .kmc-sel:hover{border-color:#999}\n#kmc .kmc-sel[aria-pressed=\"true\"]{border:2px solid #000;padding:11px;color:#000}\n#kmc .kmc-sel[aria-pressed=\"true\"] span{color:#444}\n#kmc .kmc-sel.kmc-negro[aria-pressed=\"true\"]{background:#000;color:#fff}\n#kmc .kmc-sel.kmc-negro[aria-pressed=\"true\"] span{color:#bbb}\n#kmc .kmc-sel:focus-visible{outline:2px solid var(--verde);outline-offset:2px}\n#kmc .kmc-sel[data-color]::before{content:\"\";display:inline-block;width:12px;height:12px;border-radius:50%;vertical-align:-1px;margin-right:6px;border:1px solid #999}\n#kmc .kmc-sel[data-color=\"blanco\"]::before{background:#fff}\n#kmc .kmc-sel[data-color=\"negro\"]::before{background:#222}\n#kmc .kmc-nota{background:var(--caja);border-left:3px solid #000;padding:12px 15px;margin:12px 0 0;border-radius:0 4px 4px 0;font-size:13px;line-height:1.5;color:#444}\n#kmc .kmc-nota ul{margin:4px 0 0;padding-left:18px}\n#kmc .kmc-nota li{margin-bottom:3px}\n#kmc .kmc-nota button,#kmc .kmc-enlace{background:none;border:0;padding:0;color:#000;text-decoration:underline;font-family:inherit;font-size:13px;font-weight:600;line-height:1.5;cursor:pointer}\n#kmc .kmc-nota button:hover,#kmc .kmc-enlace:hover{color:#555}\n#kmc .kmc-nota .kmc-enlace{font-style:italic}\n#kmc .kmc-icono{display:block;width:84px;height:auto;margin:2px auto 8px;transition:filter .15s,opacity .15s}\n#kmc .kmc-sel[aria-pressed=\"false\"] .kmc-icono{filter:grayscale(1);opacity:.55}\n#kmc .kmc-tomo-dib{display:block;margin:0 auto 8px}\n#kmc .kmc-tomo svg{display:block;width:100%;max-width:160px;max-height:170px;height:auto;margin:0 auto}\n#kmc .kmc-campo{display:flex;align-items:stretch;border:1px solid var(--borde);border-radius:4px;overflow:hidden;background:#fff}\n#kmc .kmc-campo:focus-within{border-color:#000}\n#kmc .kmc-campo button{border:0;background:#fff;cursor:pointer;width:48px;font-size:20px;color:#000}\n#kmc .kmc-campo button:hover{background:var(--caja)}\n#kmc .kmc-campo input{flex:1;min-width:0;padding:12px;border:0;border-left:1px solid var(--linea);border-right:1px solid var(--linea);text-align:center;font-family:inherit;font-size:18px;font-weight:600;color:#000;background:#fff;-moz-appearance:textfield;-webkit-appearance:none;margin:0}\n#kmc .kmc-campo input::-webkit-outer-spin-button,#kmc .kmc-campo input::-webkit-inner-spin-button{-webkit-appearance:none;margin:0}\n#kmc .kmc-error{font-size:13px;color:#b00020;margin-top:8px}\n#kmc .kmc-resultado{margin-top:25px;padding-top:20px;border-top:1px solid var(--linea)}\n#kmc .kmc-dibujo{background:var(--caja);border-radius:4px;padding:10px 6px 4px}\n#kmc .kmc-dibujo svg{display:block;width:100%;height:auto}\n#kmc .kmc-leyenda{display:flex;gap:14px;flex-wrap:wrap;justify-content:center;font-size:12px;color:var(--gris);margin-top:8px}\n#kmc .kmc-leyenda span{display:inline-flex;gap:5px;align-items:center}\n#kmc .kmc-aviso{margin:6px 0 0;text-align:center;font-size:12px;color:#888}\n#kmc .kmc-lista{margin:0;padding-left:18px;font-size:15px;line-height:1.5}\n#kmc .kmc-lista li{padding:3px 0}\n#kmc .kmc-total{display:flex;justify-content:space-between;align-items:baseline;gap:10px;margin-top:20px;padding-top:16px;border-top:1px solid var(--linea)}\n#kmc .kmc-total span{font-weight:600;font-size:15px}\n#kmc .kmc-total b{font-size:24px;font-weight:700}\n#kmc .kmc-desglose{text-align:right;font-size:12px;color:var(--gris);margin-top:2px}\n#kmc .kmc-boton{display:block;width:100%;margin-top:14px;padding:15px;border:0;border-radius:4px;background:var(--verde);color:#fff;font-family:inherit;font-size:16px;font-weight:700;cursor:pointer;transition:background-color .15s}\n#kmc .kmc-boton:hover{background:var(--verde-osc)}\n#kmc .kmc-boton[disabled]{background:#9a9a9a;cursor:default}\n#kmc .kmc-msg{font-size:13px;margin-top:10px;color:#b00020}\n#kmc .kmc-carrito-dev{margin-top:12px;padding:10px;border:1px dashed #bbb;border-radius:4px;font:12px\/1.4 ui-monospace,Consolas,monospace;white-space:pre-wrap;color:#444;display:none}\n\/* ventana de \"Cómo medir\": al abrirla se mueve al final de la página, por eso no va dentro de #kmc *\/\n#kmc-guia{display:none;position:fixed;inset:0;z-index:2147483000;background:rgba(0,0,0,.5);align-items:center;justify-content:center;padding:16px;box-sizing:border-box;font-family:inherit;color:#111}\n#kmc-guia.kmc-abierto{display:flex}\n#kmc-guia *{box-sizing:border-box}\n#kmc-guia .kmc-modal-caja{position:relative;background:#fff;border-radius:8px;max-width:460px;width:100%;max-height:calc(100vh - 32px);overflow:auto;padding:24px 22px 20px;text-align:left}\n#kmc-guia h4{margin:0 32px 12px 0;font-family:Montserrat,sans-serif;font-weight:800;font-size:16px;text-transform:uppercase;letter-spacing:.5px;color:#111}\n#kmc-guia svg{display:block;width:100%;height:auto;background:#f9f9f9;border-radius:4px;margin-bottom:12px}\n#kmc-guia ol{margin:0;padding-left:20px;font-size:14px;line-height:1.5}\n#kmc-guia li{margin:6px 0}\n#kmc-guia .kmc-cerrar{position:absolute;top:10px;right:10px;width:36px;height:36px;border:0;background:none;font-size:26px;line-height:1;color:#666;cursor:pointer;padding:0}\n#kmc-guia .kmc-cerrar:hover{color:#000}\n#kmc-guia .kmc-boton{display:block;width:100%;margin-top:14px;padding:15px;border:0;border-radius:4px;background:#008a00;color:#fff;font-family:inherit;font-size:16px;font-weight:700;cursor:pointer}\n#kmc-guia .kmc-boton:hover{background:#006f00}\n@media (max-width:480px){\n  #kmc .kmc-caja{padding:20px 16px}\n  #kmc .kmc-botones{gap:8px}\n  #kmc .kmc-sel{padding:10px 8px}\n  #kmc .kmc-sel[aria-pressed=\"true\"]{padding:9px 7px}\n}\n\u003c\/style\u003e\n\n\u003cdiv class=\"kmc-caja\"\u003e\n  \u003ch3 class=\"kmc-titulo\"\u003eCreador de elevadores a medida\u003c\/h3\u003e\n  \u003cp class=\"kmc-sub\"\u003eMide tu balda y en un minuto verás exactamente qué piezas te llegan.\u003c\/p\u003e\n\n  \u003cdiv class=\"kmc-paso\"\u003e\n    \u003cspan class=\"kmc-et\"\u003e1. Elige el tamaño de tus mangas:\u003c\/span\u003e\n    \u003cdiv class=\"kmc-botones\" role=\"group\" aria-label=\"Tamaño de los mangas\"\u003e\n      \u003cbutton type=\"button\" class=\"kmc-sel kmc-tomo\" data-tipo=\"estandar\" aria-pressed=\"true\"\u003eTomo estándar\u003cspan\u003eOne Piece, Naruto…\u003cbr\u003e\u003cb\u003eElevador de 9 cm\u003c\/b\u003e\u003c\/span\u003e\u003c\/button\u003e\n      \u003cbutton type=\"button\" class=\"kmc-sel kmc-tomo\" data-tipo=\"grande\" aria-pressed=\"false\"\u003eTomo grande\u003cspan\u003eMaximum Berserk, Monster…\u003cbr\u003e\u003cb\u003eElevador de 4,5 cm\u003c\/b\u003e\u003c\/span\u003e\u003c\/button\u003e\n    \u003c\/div\u003e\n  \u003c\/div\u003e\n\n  \u003cdiv class=\"kmc-paso\"\u003e\n    \u003cspan class=\"kmc-et\"\u003e2. ¿Cómo es tu estantería?\u003c\/span\u003e\n    \u003cdiv class=\"kmc-botones\" role=\"group\" aria-label=\"Tipo de estantería\"\u003e\n      \u003cbutton type=\"button\" class=\"kmc-sel\" data-estanteria=\"cerrada\" aria-pressed=\"true\"\u003eCon paredes\u003cspan\u003eKALLAX, BILLY…\u003c\/span\u003e\u003c\/button\u003e\n      \u003cbutton type=\"button\" class=\"kmc-sel\" data-estanteria=\"abierta\" aria-pressed=\"false\"\u003eAbierta\u003cspan\u003eTe incluimos dos sujetalibros laterales (+\u003cspan data-precio-aletas style=\"display:inline;margin:0;font-size:inherit;color:inherit\"\u003e5\u003c\/span\u003e €)\u003c\/span\u003e\u003c\/button\u003e\n    \u003c\/div\u003e\n  \u003c\/div\u003e\n\n  \u003cdiv class=\"kmc-paso\"\u003e\n    \u003cspan class=\"kmc-et\"\u003e\u003cspan data-txt-medida\u003e3. Introduce el ancho de tu balda (en cm):\u003c\/span\u003e\u003c\/span\u003e\n    \u003cdiv class=\"kmc-campo\"\u003e\n      \u003cbutton type=\"button\" data-paso=\"-0.5\" aria-label=\"Medio centímetro menos\"\u003e−\u003c\/button\u003e\n      \u003cinput id=\"kmc-ancho\" type=\"number\" inputmode=\"decimal\" min=\"10\" max=\"300\" step=\"0.5\" value=\"76\" aria-label=\"Ancho en centímetros\"\u003e\n      \u003cbutton type=\"button\" data-paso=\"0.5\" aria-label=\"Medio centímetro más\"\u003e+\u003c\/button\u003e\n    \u003c\/div\u003e\n    \u003cdiv class=\"kmc-error\" data-error style=\"display:none\"\u003e \u003c\/div\u003e\n    \u003cdiv class=\"kmc-nota\"\u003e\n      \u003cstrong\u003e📏 Medidas más populares:\u003c\/strong\u003e\n      \u003cul\u003e\n        \u003cli\u003e\u003cbutton type=\"button\" data-medida=\"33\"\u003eKALLAX (33 cm interiores)\u003c\/button\u003e\u003c\/li\u003e\n        \u003cli\u003e\u003cbutton type=\"button\" data-medida=\"36\"\u003eBILLY de 40 cm (36 cm interiores)\u003c\/button\u003e\u003c\/li\u003e\n        \u003cli\u003e\u003cbutton type=\"button\" data-medida=\"76\"\u003eBILLY de 80 cm (76 cm interiores)\u003c\/button\u003e\u003c\/li\u003e\n      \u003c\/ul\u003e\n      \u003cdiv style=\"margin-top:6px\"\u003eAdmite medios centímetros (76,5).\u003c\/div\u003e\n      \u003cdiv style=\"margin-top:4px\"\u003e💡 \u003cbutton type=\"button\" class=\"kmc-enlace\" data-abrir-guia\u003e¿Cómo medir tu estantería?\u003c\/button\u003e\n\u003c\/div\u003e\n    \u003c\/div\u003e\n  \u003c\/div\u003e\n\n  \u003cdiv class=\"kmc-paso\"\u003e\n    \u003cspan class=\"kmc-et\"\u003e4. Elige un color:\u003c\/span\u003e\n    \u003cdiv class=\"kmc-botones\" role=\"group\" aria-label=\"Color\"\u003e\n      \u003cbutton type=\"button\" class=\"kmc-sel\" data-color=\"blanco\" aria-pressed=\"true\"\u003eBlanco\u003c\/button\u003e\n      \u003cbutton type=\"button\" class=\"kmc-sel kmc-negro\" data-color=\"negro\" aria-pressed=\"false\"\u003eNegro\u003c\/button\u003e\n    \u003c\/div\u003e\n  \u003c\/div\u003e\n\n  \u003cdiv class=\"kmc-resultado\" aria-live=\"polite\"\u003e\n    \u003cspan class=\"kmc-et\"\u003eAsí quedará tu balda:\u003c\/span\u003e\n    \u003cdiv class=\"kmc-dibujo\" data-dibujo\u003e \u003c\/div\u003e\n    \u003cdiv class=\"kmc-leyenda\"\u003e\u003cspan data-leyenda-llave\u003eLlave de unión (incluida)\u003c\/span\u003e\u003c\/div\u003e\n    \u003cp class=\"kmc-aviso\"\u003eDibujo a escala, visto de frente. Es orientativo: no todos los tomos miden lo mismo.\u003c\/p\u003e\n\n    \u003cdiv class=\"kmc-paso\"\u003e\n      \u003cspan class=\"kmc-et\"\u003eTe llegará:\u003c\/span\u003e\n      \u003cul class=\"kmc-lista\" data-lista\u003e\u003cli\u003e \u003c\/li\u003e\u003c\/ul\u003e\n    \u003c\/div\u003e\n\n    \u003cdiv class=\"kmc-nota\" data-capacidad\u003e \u003c\/div\u003e\n\n    \u003cdiv class=\"kmc-total\"\u003e\n\u003cspan\u003ePrecio total:\u003c\/span\u003e\u003cb data-precio\u003e0,00 €\u003c\/b\u003e\n\u003c\/div\u003e\n    \u003cdiv class=\"kmc-desglose\" data-desglose\u003e \u003c\/div\u003e\n    \u003cbutton type=\"button\" class=\"kmc-boton\" data-comprar\u003eAñadir al carrito\u003c\/button\u003e\n    \u003cdiv class=\"kmc-msg\" data-msg style=\"display:none\"\u003e \u003c\/div\u003e\n    \u003cdiv class=\"kmc-carrito-dev\" data-dev\u003e \u003c\/div\u003e\n  \u003c\/div\u003e\n\u003c\/div\u003e\n\n\u003cdiv id=\"kmc-guia\" class=\"kmc-modal\"\u003e\n  \u003cdiv class=\"kmc-modal-caja\" role=\"dialog\" aria-modal=\"true\" aria-labelledby=\"kmc-guia-titulo\"\u003e\n    \u003cbutton type=\"button\" class=\"kmc-cerrar\" data-cerrar-guia aria-label=\"Cerrar\"\u003e×\u003c\/button\u003e\n    \u003ch4 id=\"kmc-guia-titulo\"\u003eCómo medir tu estantería\u003c\/h4\u003e\n    \u003col\u003e\n      \u003cli\u003e\n\u003cb\u003eMide por dentro\u003c\/b\u003e, de pared a pared y justo encima de la balda. No uses la medida exterior del mueble.\u003c\/li\u003e\n      \u003cli\u003eMide \u003cb\u003edelante y al fondo\u003c\/b\u003e. Si no coinciden, quédate con la más pequeña.\u003c\/li\u003e\n      \u003cli\u003eSi algo sobresale de las paredes (soportes, tornillos, bisagras), mide desde ahí.\u003c\/li\u003e\n      \u003cli\u003e\n\u003cb\u003eNo restes nada.\u003c\/b\u003e Nosotros dejamos un pequeño margen para que entre sin forzar.\u003c\/li\u003e\n      \u003cli\u003eMira también el \u003cb\u003ealto libre\u003c\/b\u003e, de la balda a la de arriba: hacen falta unos 27,7 cm con tomos estándar y unos 26 cm con tomos grandes.\u003c\/li\u003e\n      \u003cli\u003e¿Estantería abierta? Pon el largo total que quieres que ocupe. Los sujetalibros laterales ya tienen en cuenta en esa medida.\u003c\/li\u003e\n    \u003c\/ol\u003e\n    \u003cbutton type=\"button\" class=\"kmc-boton\" data-cerrar-guia\u003e¡Entendido!\u003c\/button\u003e\n  \u003c\/div\u003e\n\u003c\/div\u003e\n\n\u003cscript\u003e\n(function () {\n  \"use strict\";\n  \/\/ ---------- AJUSTES (lo único que hay que tocar) ----------\n  var CONFIG = {\n    margenCerrada: 2,          \/\/ mm que se quitan para que entre sin forzar\n    grosorAleta: 3,            \/\/ mm por aleta\n    maxModulo: 250,            \/\/ mm, por el tamaño de las cajas\n    minCm: 10, maxCm: 300,\n    precioAletas: 5,           \/\/ € el par de sujetalibros (antes \"aletas\"; igual para los dos tipos)\n    llavesPorJunta: 2,         \/\/ una en la ranura de atrás y otra en la de delante\n    llavesExtra: 1,            \/\/ de repuesto\n    lagrimasPorModulo: 4\n  };\n  var TIPOS = {\n    estandar: { nombre: \"Tomo estándar\", alto: 90, tomoAlto: 182, tomoGrosor: 15, precioCm: 0.80,\n      variantes: { blanco: \"56747703927170\", negro: \"56675123233154\" },   \/\/ Por centímetro\n      aletas:    { blanco: \"57036415074690\", negro: \"57036415041922\" },   \/\/ Par de sujetalibros\n      colores: [\"#e8a33d\", \"#3f6fa8\", \"#c94b3c\", \"#4f8f5f\", \"#7b5aa6\", \"#e9c44a\", \"#2f7f86\", \"#d9708f\"] },\n    grande:   { nombre: \"Tomo grande\", alto: 45, tomoAlto: 210, tomoGrosor: 21, precioCm: 0.80,\n      variantes: { blanco: \"56747703959938\", negro: \"56747703894402\" },   \/\/ Por centímetro\n      aletas:    { blanco: \"57036415140226\", negro: \"57036415107458\" },   \/\/ Par de sujetalibros\n      colores: [\"#3f6fa8\", \"#c58b3a\", \"#a33b3b\", \"#6f8f5a\", \"#8a5b9e\", \"#d6b85a\", \"#2f7f86\", \"#c46a4a\"] }\n  };\n  \/\/ -----------------------------------------------------------\n\n  var raiz = document.getElementById(\"kmc\");\n  var $ = function (s) { return raiz.querySelector(s); };\n  var $$ = function (s) { return Array.prototype.slice.call(raiz.querySelectorAll(s)); };\n  var E = { tipo: \"estandar\", abierta: false, color: \"blanco\", ancho: 76 };\n  var input = $(\"#kmc-ancho\");\n\n  function num(x, dec) {\n    return x.toLocaleString(\"es-ES\", { minimumFractionDigits: 0, maximumFractionDigits: dec == null ? 2 : dec });\n  }\n  function eur(x) { return x.toLocaleString(\"es-ES\", { style: \"currency\", currency: \"EUR\" }); }\n  function cm(mm) { return num(Math.round(mm * 10) \/ 100, 2) + \" cm\"; }\n\n  \/\/ Reparto en medidas redondas: módulos de 25, 20, 15, 10 o 5 cm, el mayor posible,\n  \/\/ y uno de ajuste que se queda con el resto. El de ajuste no pasa del máximo.\n  function repartir(L) {\n    var max = CONFIG.maxModulo, nMin = Math.max(1, Math.ceil(L \/ max));\n    if (nMin === 1) return [L];\n    var redondos = [250, 200, 150, 100, 50];\n    \/\/ 1.º el mínimo de módulos con el de ajuste de al menos la mitad de los otros;\n    \/\/ 2.º el mínimo de módulos aunque el de ajuste sea pequeño (5 cm o más);\n    \/\/ 3.º un módulo más.\n    var pasadas = [[nMin, 0.5], [nMin, 0], [nMin + 1, 0.5]];\n    for (var p = 0; p \u003c pasadas.length; p++) {\n      var n = pasadas[p][0], fr = pasadas[p][1];\n      for (var j = 0; j \u003c redondos.length; j++) {\n        var s = redondos[j], r = L - (n - 1) * s;\n        if (s \u003c= max \u0026\u0026 r \u003c= max \u0026\u0026 r \u003e= Math.max(50, s * fr)) {\n          var out = []; for (var k = 0; k \u003c n - 1; k++) out.push(s); out.push(r); return out;\n        }\n      }\n    }\n    var sc = Math.floor(L \/ nMin \/ 10) * 10, lista = [];          \/\/ por si acaso: centímetros enteros\n    for (var q = 0; q \u003c nMin - 1; q++) lista.push(sc);\n    lista.push(L - sc * (nMin - 1)); return lista;\n  }\n  function textoModulos(mods) {\n    var grupos = [];\n    mods.forEach(function (w) { var g = grupos.filter(function (x) { return x.w === w; })[0]; if (g) g.n++; else grupos.push({ w: w, n: 1 }); });\n    return grupos.map(function (g) { return g.n + \" de \" + cm(g.w); }).join(\" y \");\n  }\n\n  function calcular() {\n    var t = TIPOS[E.tipo];\n    var anchoMm = Math.round(E.ancho * 10);\n    var descuento = E.abierta ? 2 * CONFIG.grosorAleta : CONFIG.margenCerrada;\n    var modulosMm = anchoMm - descuento;\n    \/\/ Al cliente se le enseñan medidas redondas sobre lo que ha pedido (76,5 -\u003e 3 de 20 y 1 de 16,5).\n    \/\/ El margen o las aletas se le quitan solo al módulo de ajuste (el último): 2 o 6 mm que no se notan.\n    var mods = repartir(anchoMm), n = mods.length;\n    var reales = mods.slice(); reales[n - 1] -= descuento;\n    var cmCobrados = Math.ceil(E.ancho - 1e-9);\n    var precio = cmCobrados * t.precioCm + (E.abierta ? CONFIG.precioAletas : 0);\n    return { t: t, anchoMm: anchoMm, modulosMm: modulosMm, mods: mods, reales: reales, n: n, cmCobrados: cmCobrados, precio: precio,\n      arriba: Math.floor(modulosMm \/ t.tomoGrosor), abajo: Math.floor(anchoMm \/ t.tomoGrosor),\n      llaves: n \u003e 1 ? CONFIG.llavesPorJunta * (n - 1) + CONFIG.llavesExtra : 0 };\n  }\n\n  \/\/ ---------- dibujo de frente, a escala ----------\n  function dibujo(r) {\n    var t = r.t, H = t.alto, TH = t.tomoAlto;\n    var pared = 18, suelo = 18, aleta = CONFIG.grosorAleta, alAleta = 122;\n    var W = r.anchoMm;\n    var x0 = E.abierta ? aleta : (W - r.modulosMm) \/ 2;          \/\/ inicio del primer módulo\n    var techo = H + TH + 28;\n    var mL = E.abierta ? 40 : pared + 12, mR = mL;\n    var vbW = W + mL + mR, vbH = techo + suelo + 46;\n    var Y = function (z) { return techo - z; };                    \/\/ z hacia arriba\n    var anchoPantalla = $(\"[data-dibujo]\").clientWidth || 520;\n    var esc = vbW \/ Math.max(280, anchoPantalla);                  \/\/ mm por píxel en pantalla: el texto se lee igual en móvil\n    var fs = Math.max(11, 13 * esc);\n    var blanco = E.color === \"blanco\";\n    var cuerpo = blanco ? \"#ffffff\" : \"#1a1a1a\", borde = blanco ? \"#999\" : \"#000\", texto = blanco ? \"#111\" : \"#fff\";\n    var s = [];\n    s.push('\u003csvg viewBox=\"' + (-mL) + ' 0 ' + vbW + ' ' + vbH + '\" role=\"img\" aria-label=\"Dibujo a escala del elevador\"\u003e');\n    s.push('\u003crect x=\"' + (-mL) + '\" y=\"' + Y(0) + '\" width=\"' + vbW + '\" height=\"' + suelo + '\" fill=\"#d8bb93\"\/\u003e');\n    if (!E.abierta) {\n      s.push('\u003crect x=\"' + (-pared) + '\" y=\"0\" width=\"' + pared + '\" height=\"' + (techo + suelo) + '\" fill=\"#c9a77c\"\/\u003e');\n      s.push('\u003crect x=\"' + W + '\" y=\"0\" width=\"' + pared + '\" height=\"' + (techo + suelo) + '\" fill=\"#c9a77c\"\/\u003e');\n    }\n    var xi = x0;\n    for (var i = 0; i \u003c r.arriba; i++) {\n      var c = t.colores[Math.floor(i \/ 5) % t.colores.length];   \/\/ series de 5 tomos; colores vecinos bien distintos\n      s.push('\u003crect x=\"' + xi.toFixed(1) + '\" y=\"' + Y(H + TH) + '\" width=\"' + t.tomoGrosor + '\" height=\"' + TH + '\" fill=\"' + c + '\" stroke=\"#fff\" stroke-width=\"' + Math.max(0.6, esc * 0.6) + '\"\/\u003e');\n      s.push('\u003crect x=\"' + (xi + 2).toFixed(1) + '\" y=\"' + Y(H + TH - 14) + '\" width=\"' + (t.tomoGrosor - 4) + '\" height=\"10\" fill=\"rgba(255,255,255,.55)\"\/\u003e');\n      xi += t.tomoGrosor;\n    }\n    var x = x0;\n    for (var m = 0; m \u003c r.n; m++) {\n      var wm = r.reales[m];                     \/\/ se dibuja la medida real, se rotula la redonda\n      if (m \u003e 0) x += r.reales[m - 1];\n      s.push('\u003crect x=\"' + x.toFixed(2) + '\" y=\"' + Y(H) + '\" width=\"' + wm + '\" height=\"' + H + '\" fill=\"' + cuerpo + '\" stroke=\"' + borde + '\" stroke-width=\"' + Math.max(1, esc) + '\"\/\u003e');\n      if (wm \/ esc \u003e 46) {\n        s.push('\u003ctext x=\"' + (x + wm \/ 2).toFixed(1) + '\" y=\"' + (Y(H \/ 2) + fs * 0.35) + '\" text-anchor=\"middle\" font-size=\"' + fs + '\" font-weight=\"600\" fill=\"' + texto + '\"\u003e' + cm(r.mods[m]) + '\u003c\/text\u003e');\n      }\n      if (m \u003e 0) {   \/\/ llave en la junta\n        var kw = Math.max(9, 9 * esc), kh = Math.max(7, 7 * esc), ky = Y(H) + kh * 0.9;\n        s.push('\u003cpath d=\"M' + (x - kw \/ 2) + ' ' + ky + ' L' + x + ' ' + (ky + kh * 0.35) + ' L' + (x + kw \/ 2) + ' ' + ky + ' L' + (x + kw \/ 2) + ' ' + (ky + kh) + ' L' + x + ' ' + (ky + kh * 0.65) + ' L' + (x - kw \/ 2) + ' ' + (ky + kh) + 'Z\" fill=\"' + (blanco ? \"#000\" : \"#e8a33d\") + '\"\/\u003e');\n      }\n    }\n    if (E.abierta) {\n      var aw = Math.max(aleta, 3 * esc);\n      s.push('\u003crect x=\"' + (x0 - aw) + '\" y=\"' + Y(alAleta) + '\" width=\"' + aw + '\" height=\"' + alAleta + '\" rx=\"1\" fill=\"' + cuerpo + '\" stroke=\"' + borde + '\" stroke-width=\"' + Math.max(1, esc) + '\"\/\u003e');\n      s.push('\u003crect x=\"' + (x0 + r.modulosMm) + '\" y=\"' + Y(alAleta) + '\" width=\"' + aw + '\" height=\"' + alAleta + '\" rx=\"1\" fill=\"' + cuerpo + '\" stroke=\"' + borde + '\" stroke-width=\"' + Math.max(1, esc) + '\"\/\u003e');\n    }\n    var yc = Y(0) + suelo + 18;\n    s.push('\u003cg stroke=\"#666\" stroke-width=\"' + Math.max(1, esc) + '\"\u003e\u003cline x1=\"0\" y1=\"' + yc + '\" x2=\"' + W + '\" y2=\"' + yc + '\"\/\u003e\u003cline x1=\"0\" y1=\"' + (yc - 7) + '\" x2=\"0\" y2=\"' + (yc + 7) + '\"\/\u003e\u003cline x1=\"' + W + '\" y1=\"' + (yc - 7) + '\" x2=\"' + W + '\" y2=\"' + (yc + 7) + '\"\/\u003e\u003c\/g\u003e');\n    s.push('\u003crect x=\"' + (W \/ 2 - fs * 3.2) + '\" y=\"' + (yc - fs * 0.8) + '\" width=\"' + fs * 6.4 + '\" height=\"' + fs * 1.6 + '\" fill=\"#f9f9f9\"\/\u003e');\n    s.push('\u003ctext x=\"' + W \/ 2 + '\" y=\"' + (yc + fs * 0.35) + '\" text-anchor=\"middle\" font-size=\"' + fs + '\" fill=\"#111\" font-weight=\"700\"\u003e' + num(E.ancho, 1) + ' cm\u003c\/text\u003e');\n    s.push('\u003c\/svg\u003e');\n    return s.join(\"\");\n  }\n\n  \/\/ ---------- tomos dentro de cada botón: misma escala en los dos (1 unidad = 1 mm) ----------\n  var TOMOS_DIBUJO = {\n    estandar: { alto: 180, colores: [\"#e8a33d\", \"#c94b3c\", \"#3f6fa8\"] },\n    grande:   { alto: 210, colores: [\"#2f2f35\", \"#a33b3b\", \"#6b6b70\"] }\n  };\n  function tomosSvg(tipo) {\n    var d = TOMOS_DIBUJO[tipo], g = TIPOS[tipo].tomoGrosor, on = E.tipo === tipo;\n    var suelo = 214, cx = 92, n = 3, w = g * n, x0 = cx - w \/ 2, y = suelo - d.alto;\n    var tc = on ? \"#000\" : \"#999\";\n    var s = '\u003csvg viewBox=\"0 0 160 240\" aria-hidden=\"true\"\u003e';\n    s += '\u003cline x1=\"4\" y1=\"' + (suelo - 180) + '\" x2=\"156\" y2=\"' + (suelo - 180) + '\" stroke=\"#ccc\" stroke-width=\"1\" stroke-dasharray=\"4 4\"\/\u003e';\n    s += '\u003cline x1=\"4\" y1=\"' + suelo + '\" x2=\"156\" y2=\"' + suelo + '\" stroke=\"#bbb\" stroke-width=\"2\"\/\u003e';\n    for (var i = 0; i \u003c n; i++) {\n      var xi = x0 + i * g;\n      s += '\u003crect x=\"' + xi + '\" y=\"' + y + '\" width=\"' + g + '\" height=\"' + d.alto + '\" rx=\"1.5\" fill=\"' + (on ? d.colores[i] : \"#d6d6d6\") + '\" stroke=\"#fff\" stroke-width=\"1\"\/\u003e';\n      s += '\u003crect x=\"' + (xi + 3) + '\" y=\"' + (y + 10) + '\" width=\"' + (g - 6) + '\" height=\"9\" fill=\"#fff\" opacity=\".55\"\/\u003e';\n    }\n    s += '\u003ctext x=\"' + (x0 - 7) + '\" y=\"' + (y + d.alto \/ 2) + '\" text-anchor=\"end\" font-size=\"18\" font-weight=\"700\" fill=\"' + tc + '\"\u003e' + num(d.alto \/ 10) + ' cm\u003c\/text\u003e';\n    s += '\u003ctext x=\"' + (x0 - 7) + '\" y=\"' + (y + d.alto \/ 2 + 16) + '\" text-anchor=\"end\" font-size=\"13\" fill=\"' + tc + '\"\u003ede alto\u003c\/text\u003e';\n    s += '\u003ctext x=\"' + cx + '\" y=\"' + (suelo + 20) + '\" text-anchor=\"middle\" font-size=\"13\" fill=\"' + tc + '\"\u003e' + num(g \/ 10, 1) + ' cm de grosor\u003c\/text\u003e';\n    return s + '\u003c\/svg\u003e';\n  }\n\n  function marcar(sel, attr, val) { $$(sel).forEach(function (b) { b.setAttribute(\"aria-pressed\", String(b.getAttribute(attr) === val)); }); }\n  function reiniciarBoton() { var b = $(\"[data-comprar]\"); b.textContent = \"Añadir al carrito\"; b.onclick = comprar; $(\"[data-dev]\").style.display = \"none\"; }\n  function elegirTipo(tipo) { E.tipo = tipo; marcar(\"[data-tipo]\", \"data-tipo\", E.tipo); reiniciarBoton(); pintar(); }\n\n  function pintar() {\n    var err = $(\"[data-error]\");\n    var v = parseFloat(String(input.value).replace(\",\", \".\"));\n    var ok = isFinite(v) \u0026\u0026 v \u003e= CONFIG.minCm \u0026\u0026 v \u003c= CONFIG.maxCm;\n    $(\"[data-txt-medida]\").textContent = E.abierta ? \"3. ¿Cuánto quieres que mida en total? (en cm):\" : \"3. Introduce el ancho de tu balda (en cm):\";\n    $$(\"[data-tomos]\").forEach(function (el) { el.innerHTML = tomosSvg(el.getAttribute(\"data-tomos\")); });\n    if (!ok) {\n      err.style.display = \"\";\n      err.textContent = !isFinite(v) ? \"Escribe el ancho en centímetros.\" :\n        v \u003c CONFIG.minCm ? \"El mínimo son \" + CONFIG.minCm + \" cm.\" :\n        \"Para más de \" + CONFIG.maxCm + \" cm escríbenos y te lo preparamos.\";\n      $(\"[data-comprar]\").disabled = true;\n      return;\n    }\n    err.style.display = \"none\";\n    E.ancho = Math.round(v * 2) \/ 2;\n    var r = calcular();\n    $(\"[data-comprar]\").disabled = false;\n    $(\"[data-dibujo]\").innerHTML = dibujo(r);\n    var iguales = r.mods.every(function (w) { return w === r.mods[0]; });\n    var li = [];\n    li.push(\"\u003cli\u003e\" + (r.n === 1 ? \"1 módulo de \u003cb\u003e\" + cm(r.mods[0]) + \"\u003c\/b\u003e\" :\n      iguales ? r.n + \" módulos de \u003cb\u003e\" + cm(r.mods[0]) + \"\u003c\/b\u003e\" :\n      r.n + \" módulos: \u003cb\u003e\" + textoModulos(r.mods) + \"\u003c\/b\u003e\") + \"\u003c\/li\u003e\");\n    if (E.abierta) li.push(\"\u003cli\u003e2 sujetalibros, uno a cada lado\u003c\/li\u003e\");\n    if (r.llaves) li.push(\"\u003cli\u003e\" + r.llaves + \" llaves para unir los módulos (\" + (CONFIG.llavesExtra === 1 ? \"una\" : CONFIG.llavesExtra) + \" de repuesto)\u003c\/li\u003e\");\n    li.push(\"\u003cli\u003e\" + r.n * CONFIG.lagrimasPorModulo + \" lágrimas de silicona antideslizante ya puestas\u003c\/li\u003e\");\n    li.push(\"\u003cli\u003eTodo en color \" + (E.color === \"blanco\" ? \"blanco\" : \"negro\") + \"\u003c\/li\u003e\");\n    li.push(\"\u003cli\u003e¡Y un regalito de nuestra parte! 🎁\u003c\/li\u003e\");\n    $(\"[data-lista]\").innerHTML = li.join(\"\");\n    $(\"[data-capacidad]\").innerHTML = \"📚 \u003cstrong\u003eTe caben unos \" + (r.arriba + r.abajo) + \" tomos:\u003c\/strong\u003e \" + r.arriba +\n      \" encima del elevador y \" + r.abajo + \" delante. ¡El doble de espacio en la misma balda!\";\n    $(\"[data-precio]\").textContent = eur(r.precio);\n    $(\"[data-desglose]\").textContent = r.cmCobrados + \" cm × \" + eur(r.t.precioCm) + (E.abierta ? \" + sujetalibros \" + eur(CONFIG.precioAletas) : \"\");\n    $(\"[data-precio-aletas]\").textContent = num(CONFIG.precioAletas);\n    $(\"[data-msg]\").style.display = \"none\";\n    return r;\n  }\n\n  function carrito(r) {\n    var props = {\n      \"Ancho\": num(E.ancho, 1) + \" cm\",\n      \"Estantería\": E.abierta ? \"Abierta (con sujetalibros)\" : \"Con paredes\",\n      \"Módulos\": textoModulos(r.mods),\n      \"_Reparto real (mm)\": r.reales.join(\" + \")\n    };\n    var items = [{ id: Number(r.t.variantes[E.color]), quantity: r.cmCobrados, properties: props }];\n    if (E.abierta) items.push({ id: r.t.aletas[E.color], quantity: 1, properties: { \"Para\": num(E.ancho, 1) + \" cm\" } });\n    return { items: items };\n  }\n\n  function comprar() {\n    var r = pintar(); if (!r) return;\n    var datos = carrito(r);\n    var enTienda = \/kamimarks\\.com$\/.test(location.hostname) || (window.Shopify \u0026\u0026 window.Shopify.shop);\n    var dev = $(\"[data-dev]\"), msg = $(\"[data-msg]\"), btn = $(\"[data-comprar]\");\n    var sinNumero = datos.items.filter(function (it) { return !\/^\\d+$\/.test(String(it.id)); });\n    if (sinNumero.length) {   \/\/ seguro: faltan los números de variante (por ejemplo, los de los sujetalibros)\n      msg.style.display = \"\";\n      msg.textContent = \"⚠️ Ahora mismo no podemos añadir esta opción desde aquí. Escríbenos y te lo preparamos.\";\n      if (window.console) console.warn(\"Calculadora: faltan números de variante\", sinNumero);\n      if (enTienda) return;\n    }\n    if (!enTienda) {   \/\/ boceto: enseña lo que se mandaría\n      dev.style.display = \"block\";\n      dev.textContent = \"Boceto: esto es lo que se enviaría al carrito de Shopify\\n\" + JSON.stringify(datos, null, 2);\n      return;\n    }\n    btn.disabled = true; btn.textContent = \"Añadiendo…\";\n    fetch(\"\/cart\/add.js\", { method: \"POST\", headers: { \"Content-Type\": \"application\/json\" }, body: JSON.stringify(datos) })\n      .then(function (res) { return res.json(); })\n      .then(function (d) {\n        btn.disabled = false;\n        if (d \u0026\u0026 d.status) { btn.textContent = \"Añadir al carrito\"; msg.style.display = \"\"; msg.textContent = \"⚠️ \" + (d.description || \"No se ha podido añadir. Inténtalo de nuevo.\"); }\n        else { btn.textContent = \"✅ Listo, pulsa aquí para ver el carrito\"; btn.onclick = function () { location.href = \"\/cart\"; }; }\n      })\n      .catch(function () { btn.disabled = false; btn.textContent = \"Añadir al carrito\"; msg.style.display = \"\"; msg.textContent = \"⚠️ Ha habido un problema de conexión. Inténtalo de nuevo.\"; });\n  }\n\n  $$(\"[data-tipo]\").forEach(function (b) { b.addEventListener(\"click\", function () { elegirTipo(b.dataset.tipo); }); });\n  $$(\"[data-estanteria]\").forEach(function (b) { b.addEventListener(\"click\", function () { E.abierta = b.dataset.estanteria === \"abierta\"; marcar(\"[data-estanteria]\", \"data-estanteria\", b.dataset.estanteria); reiniciarBoton(); pintar(); }); });\n  $$(\"[data-color]\").forEach(function (b) { b.addEventListener(\"click\", function () { E.color = b.dataset.color; marcar(\"[data-color]\", \"data-color\", E.color); reiniciarBoton(); pintar(); }); });\n  $$(\"[data-medida]\").forEach(function (b) { b.addEventListener(\"click\", function () { input.value = b.dataset.medida; reiniciarBoton(); pintar(); }); });\n  $$(\"[data-paso]\").forEach(function (b) { b.addEventListener(\"click\", function () {\n    var v = parseFloat(String(input.value).replace(\",\", \".\")) || CONFIG.minCm;\n    input.value = Math.min(CONFIG.maxCm, Math.max(CONFIG.minCm, Math.round((v + parseFloat(b.dataset.paso)) * 2) \/ 2));\n    reiniciarBoton(); pintar(); }); });\n  input.addEventListener(\"input\", function () { reiniciarBoton(); pintar(); });\n  $(\"[data-comprar]\").onclick = comprar;\n  var tRes; window.addEventListener(\"resize\", function () { clearTimeout(tRes); tRes = setTimeout(pintar, 150); });\n\n  \/\/ ---------- dibujos fijos: se meten desde aquí porque el editor de Shopify puede limpiar los SVG escritos en el HTML ----------\n  function libros(x0, n) {\n    var cols = [\"#e8a33d\", \"#3f6fa8\", \"#c94b3c\", \"#4f8f5f\", \"#e9c44a\", \"#7b5aa6\"], s = \"\";\n    for (var i = 0; i \u003c n; i++) s += '\u003crect x=\"' + (x0 + 6 * i) + '\" y=\"8\" width=\"6\" height=\"22\" fill=\"' + cols[i % cols.length] + '\"\/\u003e';\n    return s;\n  }\n  var ICONOS = {\n    cerrada: '\u003csvg class=\"kmc-icono\" viewBox=\"0 0 80 56\" aria-hidden=\"true\"\u003e\u003crect x=\"2\" y=\"2\" width=\"8\" height=\"52\" fill=\"#c9a77c\"\/\u003e\u003crect x=\"70\" y=\"2\" width=\"8\" height=\"52\" fill=\"#c9a77c\"\/\u003e' +\n      '\u003crect x=\"2\" y=\"46\" width=\"76\" height=\"8\" fill=\"#d8bb93\"\/\u003e\u003crect x=\"10\" y=\"30\" width=\"60\" height=\"16\" fill=\"#fff\" stroke=\"#999\" stroke-width=\"1.2\"\/\u003e' + libros(12, 9) + '\u003c\/svg\u003e',\n    abierta: '\u003csvg class=\"kmc-icono\" viewBox=\"0 0 80 56\" aria-hidden=\"true\"\u003e\u003crect x=\"2\" y=\"46\" width=\"76\" height=\"8\" fill=\"#d8bb93\"\/\u003e' +\n      '\u003crect x=\"14\" y=\"30\" width=\"52\" height=\"16\" fill=\"#fff\" stroke=\"#999\" stroke-width=\"1.2\"\/\u003e\u003crect x=\"10\" y=\"10\" width=\"4\" height=\"36\" fill=\"#222\"\/\u003e\u003crect x=\"66\" y=\"10\" width=\"4\" height=\"36\" fill=\"#222\"\/\u003e' +\n      libros(16, 8) + '\u003c\/svg\u003e'\n  };\n  var GUIA_DIBUJO = '\u003csvg viewBox=\"0 0 360 190\" aria-hidden=\"true\"\u003e\u003crect x=\"20\" y=\"10\" width=\"16\" height=\"170\" fill=\"#c9a77c\"\/\u003e\u003crect x=\"324\" y=\"10\" width=\"16\" height=\"170\" fill=\"#c9a77c\"\/\u003e' +\n    '\u003crect x=\"20\" y=\"150\" width=\"320\" height=\"16\" fill=\"#d8bb93\"\/\u003e\u003crect x=\"20\" y=\"10\" width=\"320\" height=\"12\" fill=\"#d8bb93\"\/\u003e' +\n    '\u003cg stroke=\"#008a00\" stroke-width=\"3\" fill=\"#008a00\"\u003e\u003cline x1=\"40\" y1=\"138\" x2=\"320\" y2=\"138\"\/\u003e\u003cpath d=\"M38 138 l12 -7 v14z\" stroke=\"none\"\/\u003e\u003cpath d=\"M322 138 l-12 -7 v14z\" stroke=\"none\"\/\u003e\u003c\/g\u003e' +\n    '\u003crect x=\"132\" y=\"122\" width=\"96\" height=\"30\" rx=\"4\" fill=\"#fff\" stroke=\"#008a00\" stroke-width=\"1.5\"\/\u003e\u003ctext x=\"180\" y=\"142\" text-anchor=\"middle\" font-size=\"15\" font-weight=\"700\" fill=\"#008a00\"\u003eAncho\u003c\/text\u003e' +\n    '\u003cg stroke=\"#666\" stroke-width=\"2\" fill=\"#666\"\u003e\u003cline x1=\"300\" y1=\"26\" x2=\"300\" y2=\"146\"\/\u003e\u003cpath d=\"M300 24 l-6 10 h12z\" stroke=\"none\"\/\u003e\u003cpath d=\"M300 148 l-6 -10 h12z\" stroke=\"none\"\/\u003e\u003c\/g\u003e' +\n    '\u003ctext x=\"292\" y=\"90\" text-anchor=\"end\" font-size=\"13\" fill=\"#666\"\u003eAlto libre\u003c\/text\u003e\u003c\/svg\u003e';\n  $$(\"[data-estanteria]\").forEach(function (b) { b.insertAdjacentHTML(\"afterbegin\", ICONOS[b.dataset.estanteria]); });\n  $$(\"[data-tipo]\").forEach(function (b) { b.insertAdjacentHTML(\"afterbegin\", '\u003cspan class=\"kmc-tomo-dib\" data-tomos=\"' + b.dataset.tipo + '\"\u003e\u003c\/span\u003e'); });\n  $(\"[data-leyenda-llave]\").insertAdjacentHTML(\"afterbegin\", '\u003csvg width=\"16\" height=\"10\" aria-hidden=\"true\"\u003e\u003cpath d=\"M1 1 L8 4 L15 1 L15 9 L8 6 L1 9Z\" fill=\"#000\"\/\u003e\u003c\/svg\u003e');\n\n  \/\/ ---------- ventana de \"Cómo medir\" ----------\n  \/\/ Se mueve al final de la página: si algún contenedor del tema tiene transformaciones, una ventana\n  \/\/ \"fija\" dentro de él no taparía toda la pantalla.\n  var guia = document.getElementById(\"kmc-guia\"), antesDeGuia = null;\n  guia.querySelector(\"ol\").insertAdjacentHTML(\"beforebegin\", GUIA_DIBUJO);\n  document.body.appendChild(guia);\n  function guiaAbierta() { return guia.classList.contains(\"kmc-abierto\"); }\n  function abrirGuia() { antesDeGuia = document.activeElement; guia.classList.add(\"kmc-abierto\"); guia.querySelector(\".kmc-cerrar\").focus(); }\n  function cerrarGuia() { guia.classList.remove(\"kmc-abierto\"); if (antesDeGuia) antesDeGuia.focus(); }\n  $(\"[data-abrir-guia]\").addEventListener(\"click\", abrirGuia);\n  Array.prototype.forEach.call(guia.querySelectorAll(\"[data-cerrar-guia]\"), function (b) { b.addEventListener(\"click\", cerrarGuia); });\n  guia.addEventListener(\"click\", function (e) { if (e.target === guia) cerrarGuia(); });\n  document.addEventListener(\"keydown\", function (e) { if (e.key === \"Escape\" \u0026\u0026 guiaAbierta()) cerrarGuia(); });\n\n  pintar();\n})();\n\u003c\/script\u003e\n\u003c\/div\u003e\n\u003ch2\u003e🔗 ¿Cómo se monta?\u003c\/h2\u003e\n\u003col\u003e\n\u003cli\u003eColoca los módulos en la balda, uno al lado del otro, con el hueco curvo mirando hacia ti.\u003c\/li\u003e\n\u003cli\u003eMete cada llave desde arriba en la ranura que queda entre dos módulos y empújala hasta abajo. Cada unión lleva dos llaves: una atrás y otra delante.\u003c\/li\u003e\n\u003cli\u003eSi has pedido sujetalibros, se ponen igual: deslízalos desde arriba por las ranuras de los extremos.\u003c\/li\u003e\n\u003cli\u003eLas ranuras ofrecen resistencia para mantenerlo todo bien junto, así que aprieta suavemente, pero con contudencia.\u003c\/li\u003e\n\u003c\/ol\u003e\n\u003cp\u003e¡Y listo!\u003c\/p\u003e\n\u003ch2\u003e📏 Medidas\u003c\/h2\u003e\n\u003cul\u003e\n\u003cli\u003e\n\u003cstrong\u003eElevador para tomo estándar:\u003c\/strong\u003e 9 cm de alto y 9 cm de fondo. Para tomos B6 de unos 18 cm, como One Piece, Naruto, Gachiakuta o Sakamoto Days. Necesitas unos 27,7 cm de alto libre en la balda.\u003c\/li\u003e\n\u003cli\u003e\n\u003cstrong\u003eElevador para tomo grande:\u003c\/strong\u003e 4,5 cm de alto y 9 cm de fondo. Para tomos kanzenban de unos 21 cm, como Maximum Berserk, Solo Leveling o Monster. Necesitas unos 26 cm de alto libre.\u003c\/li\u003e\n\u003cli\u003e\n\u003cstrong\u003eAncho:\u003c\/strong\u003e el que necesites.\u003c\/li\u003e\n\u003cli\u003e\n\u003cstrong\u003eSujetalibros:\u003c\/strong\u003e 12,2 cm de alto, para estanterías sin paredes a los lados.\u003c\/li\u003e\n\u003c\/ul\u003e\n\u003ch2\u003e📖 ¿Qué incluye el paquete?\u003c\/h2\u003e\n\u003cp\u003eTodos los módulos que te indica la calculadora, las llaves para unirlos (y una de repuesto), los sujetalibros si los has pedido y un regalito de nuestra parte. Cada módulo lleva en la base cuatro lágrimas de silicona antideslizante ya puestas, para que no se mueva ni patine cada vez que sacas un libro.\u003c\/p\u003e\n\u003ch2\u003e📕 ¿Por qué un elevador a medida?\u003c\/h2\u003e\n\u003cp\u003ePorque cada casa, estantería y balda es un mundo. Con un elevador de medida fija casi siempre sobra o falta algo; con este no. Y si un día cambias de estantería o tu colección crece, puedes pedirnos más módulos y unirlos a los que ya tienes.\u003c\/p\u003e","brand":"KamiMarks","offers":[{"title":"Manga estándar (One Piece o Naruto) \/ Por centímetro \/ Negro","offer_id":56675123233154,"sku":null,"price":0.8,"currency_code":"EUR","in_stock":true},{"title":"Manga estándar (One Piece o Naruto) \/ Por centímetro \/ Blanco","offer_id":56747703927170,"sku":null,"price":0.8,"currency_code":"EUR","in_stock":true},{"title":"Manga estándar (One Piece o Naruto) \/ Par de sujetalibros \/ Negro","offer_id":57036415041922,"sku":null,"price":5.0,"currency_code":"EUR","in_stock":true},{"title":"Manga estándar (One Piece o Naruto) \/ Par de sujetalibros \/ Blanco","offer_id":57036415074690,"sku":null,"price":5.0,"currency_code":"EUR","in_stock":true},{"title":"Manga grande (Solo Leveling o Monster) \/ Por centímetro \/ Negro","offer_id":56747703894402,"sku":null,"price":0.8,"currency_code":"EUR","in_stock":true},{"title":"Manga grande (Solo Leveling o Monster) \/ Por centímetro \/ Blanco","offer_id":56747703959938,"sku":null,"price":0.8,"currency_code":"EUR","in_stock":true},{"title":"Manga grande (Solo Leveling o Monster) \/ Par de sujetalibros \/ Negro","offer_id":57036415107458,"sku":null,"price":5.0,"currency_code":"EUR","in_stock":true},{"title":"Manga grande (Solo Leveling o Monster) \/ Par de sujetalibros \/ Blanco","offer_id":57036415140226,"sku":null,"price":5.0,"currency_code":"EUR","in_stock":true}],"thumbnail_url":"\/\/cdn.shopify.com\/s\/files\/1\/0937\/6363\/3538\/files\/IMG_20260705_181804.jpg?v=1783271901"},{"product_id":"exhibidor-para-manga-libros-personalizado-a-medida","title":"Exhibidor para manga y libros personalizado","description":"\u003cp\u003eTodas las estanterías se ven igual, con un lomo detrás de otro. ¿Y si pudieras lucir la portada de tu libro favorito? Una novela, un cómic, un artbook o ese manga especial del que quieres presumir...\u003c\/p\u003e\n\u003cp\u003eCon nuestro exhibidor a medida pones un libro delante, con la portada a la vista, y guardas varios más detrás. Tú nos dices cuánto mide tu libro y nosotros lo hacemos a su medida.\u003c\/p\u003e\n\u003ch2\u003e💡 ¿Cómo funciona?\u003c\/h2\u003e\n\u003cp\u003eElige el tipo de libro en el configurador. Si es un manga \u003ca href=\"\/products\/exhibidor-para-manga-mostrar-portada-estanteria\"\u003etankobon\u003c\/a\u003e, \u003ca href=\"\/products\/exhibidor-para-manga-seinen-mostrar-portada-libro-estanteria\"\u003eB6\u003c\/a\u003e o \u003ca href=\"\/products\/exhibidor-para-manga-xl-kanzenban-mostrar-portada-estanteria\"\u003ekanzenban\u003c\/a\u003e, tienes nuestros modelos de siempre. Para cualquier otro libro, elige \"A medida\" y escribe su ancho y su grosor: el exhibidor a medida cuesta 15 euros, sea del tamaño que sea. Puedes girar el dibujo para verlo por todos los lados e incluso probar con la foto de tu portada.\u003c\/p\u003e\n\u003cdiv id=\"kmx\"\u003e\n\u003cstyle\u003e\n#kmx{--verde:#008a00;--verde-osc:#006f00;--gris:#666;--borde:#ccc;--linea:#eaeaea;--caja:#f6f5f2;\n  width:100%;max-width:560px;margin:30px auto;padding:0 10px;box-sizing:border-box;font-family:inherit;color:#111}\n#kmx *{box-sizing:border-box}\n#kmx .kmx-caja{background:#fff;padding:22px;border-radius:8px;border:1px solid var(--linea);box-shadow:0 4px 12px rgba(0,0,0,.05)}\n#kmx .kmx-titulo{margin:0 0 4px;text-align:center;font-family:Montserrat,sans-serif;font-weight:800;font-size:18px;text-transform:uppercase;letter-spacing:.5px;color:#111}\n#kmx .kmx-sub{margin:0 0 14px;text-align:center;font-size:13px;color:var(--gris)}\n#kmx .kmx-visor{position:relative;width:100%;aspect-ratio:4\/3;background:radial-gradient(ellipse at 50% 30%,#f7f6f3 0%,#e6e3dc 85%);border-radius:6px;overflow:hidden;user-select:none;-webkit-user-select:none}\n#kmx .kmx-visor canvas{display:block;width:100%;height:100%;cursor:grab;touch-action:pan-y}\n#kmx .kmx-visor canvas:active{cursor:grabbing}\n#kmx .kmx-cargando{position:absolute;inset:0;display:flex;align-items:center;justify-content:center;padding:20px;text-align:center;font-size:13px;color:var(--gris)}\n#kmx .kmx-cota{position:absolute;left:0;top:0;transform:translate(-50%,-50%);padding:2px 8px;border-radius:20px;background:#fff;border:1px solid #222;font-size:12px;font-weight:700;line-height:1.5;white-space:nowrap;pointer-events:none;color:#111;box-shadow:0 1px 3px rgba(0,0,0,.12);display:none}\n#kmx .kmx-vistas{display:flex;border:1px solid var(--borde);border-radius:20px;overflow:hidden}\n#kmx .kmx-vistas button{border:0;border-left:1px solid var(--linea);background:#fff;padding:6px 10px;font-family:inherit;font-size:13px;font-weight:600;color:#333;cursor:pointer}\n#kmx .kmx-vistas button:first-child{border-left:0}\n#kmx .kmx-vistas button:hover{background:var(--caja);color:#000}\n#kmx .kmx-pista{position:absolute;right:10px;top:8px;font-size:11px;color:#999;pointer-events:none}\n#kmx .kmx-opciones{display:flex;gap:8px;flex-wrap:wrap;margin-top:10px;justify-content:center}\n#kmx .kmx-chip{display:inline-flex;align-items:center;gap:6px;border:1px solid var(--borde);background:#fff;border-radius:20px;padding:6px 12px;font-family:inherit;font-size:13px;font-weight:600;color:#333;cursor:pointer}\n#kmx .kmx-chip[aria-pressed=\"true\"]{border-color:#000;color:#000;background:var(--caja)}\n#kmx .kmx-chip[aria-pressed=\"true\"]::before{content:\"✓\";font-size:12px}\n#kmx .kmx-chip input{display:none}\n#kmx .kmx-paso{margin:24px 0 0}\n#kmx .kmx-et{display:block;margin:0 0 8px;font-weight:600;font-size:14px;color:#333}\n#kmx .kmx-et small{font-weight:400;color:var(--gris);font-size:13px}\n#kmx .kmx-botones{display:flex;gap:8px;width:100%;flex-wrap:wrap}\n#kmx .kmx-sel{flex:1 1 0;min-width:110px;padding:10px 8px;font-family:inherit;font-size:14px;font-weight:600;line-height:1.3;border:1px solid var(--borde);border-radius:4px;cursor:pointer;background:#fff;color:#333;text-align:center}\n#kmx .kmx-sel span{display:block;font-size:11px;font-weight:400;color:var(--gris);margin-top:3px}\n#kmx .kmx-sel em{display:block;font-style:normal;font-size:13px;font-weight:700;color:#000;margin-top:4px}\n#kmx .kmx-sel:hover{border-color:#999}\n#kmx .kmx-sel[aria-pressed=\"true\"]{border:2px solid #000;padding:9px 7px;color:#000}\n#kmx .kmx-sel.kmx-negro[aria-pressed=\"true\"]{background:#000;color:#fff}\n#kmx .kmx-sel.kmx-negro[aria-pressed=\"true\"] span{color:#bbb}\n#kmx .kmx-sel:focus-visible,#kmx .kmx-chip:focus-visible{outline:2px solid var(--verde);outline-offset:2px}\n#kmx .kmx-sel[data-color]::before{content:\"\";display:inline-block;width:12px;height:12px;border-radius:50%;vertical-align:-1px;margin-right:6px;border:1px solid #999}\n#kmx .kmx-sel[data-color=\"blanco\"]::before{background:#fff}\n#kmx .kmx-sel[data-color=\"negro\"]::before{background:#222}\n#kmx .kmx-medida{margin-top:16px}\n#kmx .kmx-fila{display:flex;align-items:center;justify-content:space-between;gap:12px}\n#kmx .kmx-fila label{font-size:14px;font-weight:600;color:#333}\n#kmx .kmx-campo{display:flex;align-items:stretch;border:1px solid var(--borde);border-radius:4px;overflow:hidden;background:#fff;flex:0 0 auto}\n#kmx .kmx-campo:focus-within{border-color:#000}\n#kmx .kmx-campo button{border:0;background:#fff;cursor:pointer;width:36px;font-size:18px;color:#000;font-family:inherit}\n#kmx .kmx-campo button:hover{background:var(--caja)}\n#kmx .kmx-campo input{width:58px;padding:6px 2px;border:0;border-left:1px solid var(--linea);text-align:right;font-family:inherit;font-size:16px;font-weight:700;color:#000;background:#fff;margin:0}\n#kmx .kmx-campo span{display:flex;align-items:center;padding:0 8px 0 4px;font-size:13px;color:var(--gris);border-right:1px solid var(--linea)}\n#kmx input[type=range]{width:100%;margin:10px 0 0;accent-color:#000;height:24px}\n#kmx .kmx-ayuda{font-size:12px;color:var(--gris);margin-top:2px}\n#kmx .kmx-nota{background:var(--caja);border-left:3px solid #000;padding:10px 14px;margin:14px 0 0;border-radius:0 4px 4px 0;font-size:13px;line-height:1.5;color:#444}\n#kmx .kmx-subir{display:flex;align-items:center;gap:10px;margin-top:10px;font-size:13px;color:#444}\n#kmx .kmx-subir label{border:1px solid #000;border-radius:4px;padding:7px 12px;font-weight:600;cursor:pointer;background:#fff;white-space:nowrap}\n#kmx .kmx-subir label:hover{background:var(--caja)}\n#kmx .kmx-subir input{position:absolute;width:1px;height:1px;opacity:0}\n#kmx .kmx-subir button{background:none;border:0;padding:0;font-family:inherit;font-size:13px;text-decoration:underline;cursor:pointer;color:#000}\n#kmx .kmx-resultado{margin-top:24px;padding-top:18px;border-top:1px solid var(--linea)}\n#kmx .kmx-medidas{display:grid;grid-template-columns:repeat(3,1fr);gap:8px;text-align:center}\n#kmx .kmx-medidas div{background:var(--caja);border-radius:4px;padding:8px 4px}\n#kmx .kmx-medidas b{display:block;font-size:18px;font-weight:700;color:#000}\n#kmx .kmx-medidas span{font-size:12px;color:var(--gris)}\n#kmx .kmx-total{display:flex;justify-content:space-between;align-items:baseline;gap:10px;margin-top:18px;padding-top:14px;border-top:1px solid var(--linea)}\n#kmx .kmx-total span{font-weight:600;font-size:15px}\n#kmx .kmx-total b{font-size:24px;font-weight:700}\n#kmx .kmx-boton{display:block;width:100%;margin-top:12px;padding:15px;border:0;border-radius:4px;background:var(--verde);color:#fff;font-family:inherit;font-size:16px;font-weight:700;cursor:pointer}\n#kmx .kmx-boton:hover{background:var(--verde-osc)}\n#kmx .kmx-boton[disabled]{background:#9a9a9a;cursor:default}\n#kmx .kmx-msg{font-size:13px;margin-top:10px;color:#b00020}\n#kmx .kmx-aviso{margin:14px 0 0;text-align:center;font-size:12px;line-height:1.5;color:#888}\n#kmx .kmx-carrito-dev{margin-top:12px;padding:10px;border:1px dashed #bbb;border-radius:4px;font:12px\/1.4 ui-monospace,Consolas,monospace;white-space:pre-wrap;color:#444;display:none}\n@media (max-width:420px){#kmx .kmx-caja{padding:16px}#kmx .kmx-vistas button{padding:6px 8px}#kmx .kmx-pista{display:none}}\n\u003c\/style\u003e\n\u003cdiv class=\"kmx-caja\"\u003e\n  \u003ch3 class=\"kmx-titulo\"\u003eDiseña tu exhibidor\u003c\/h3\u003e\n  \u003cp class=\"kmx-sub\"\u003eIntroduce las medidas y mira cómo cambia. Arrástralo para girarlo.\u003c\/p\u003e\n\n  \u003cdiv class=\"kmx-visor\" data-visor\u003e\n    \u003ccanvas aria-label=\"Vista 3D del exhibidor\"\u003e\u003c\/canvas\u003e\n    \u003cdiv class=\"kmx-cargando\" data-cargando\u003eCargando la vista 3D…\u003c\/div\u003e\n    \u003cdiv class=\"kmx-cota\" data-cota=\"ancho\"\u003e\u003c\/div\u003e\n    \u003cdiv class=\"kmx-cota\" data-cota=\"alto\"\u003e\u003c\/div\u003e\n    \u003cdiv class=\"kmx-cota\" data-cota=\"fondo\"\u003e\u003c\/div\u003e\n    \u003cdiv class=\"kmx-pista\"\u003e↔ arrastra para girar\u003c\/div\u003e\n  \u003c\/div\u003e\n  \u003cdiv class=\"kmx-opciones\"\u003e\n    \u003cdiv class=\"kmx-vistas\"\u003e\n      \u003cbutton type=\"button\" data-vista=\"frente\"\u003eFrente\u003c\/button\u003e\n      \u003cbutton type=\"button\" data-vista=\"lado\"\u003eLado\u003c\/button\u003e\n      \u003cbutton type=\"button\" data-vista=\"detras\"\u003eDetrás\u003c\/button\u003e\n      \u003cbutton type=\"button\" data-vista=\"tres\"\u003e3\/4\u003c\/button\u003e\n    \u003c\/div\u003e\n    \u003cbutton type=\"button\" class=\"kmx-chip\" data-chip=\"tomos\" aria-pressed=\"true\"\u003eCon libros\u003c\/button\u003e\n    \u003cbutton type=\"button\" class=\"kmx-chip\" data-chip=\"cotas\" aria-pressed=\"true\"\u003eMedidas\u003c\/button\u003e\n  \u003c\/div\u003e\n\n  \u003cdiv class=\"kmx-paso\"\u003e\n    \u003cspan class=\"kmx-et\"\u003e1. ¿Qué libro vas a poner delante?\u003c\/span\u003e\n    \u003cdiv class=\"kmx-botones\"\u003e\n      \u003cbutton type=\"button\" class=\"kmx-sel\" data-preset=\"tankobon\" aria-pressed=\"true\"\u003eTankobon\u003cspan\u003eOne Piece, Naruto\u003c\/span\u003e\u003cem data-precio-preset=\"tankobon\"\u003e\u003c\/em\u003e\u003c\/button\u003e\n      \u003cbutton type=\"button\" class=\"kmx-sel\" data-preset=\"b6\" aria-pressed=\"false\"\u003eB6\u003cspan\u003eBerserk, Centuria\u003c\/span\u003e\u003cem data-precio-preset=\"b6\"\u003e\u003c\/em\u003e\u003c\/button\u003e\n      \u003cbutton type=\"button\" class=\"kmx-sel\" data-preset=\"kanzenban\" aria-pressed=\"false\"\u003eKanzenban\u003cspan\u003eSolo Leveling\u003c\/span\u003e\u003cem data-precio-preset=\"kanzenban\"\u003e\u003c\/em\u003e\u003c\/button\u003e\n      \u003cbutton type=\"button\" class=\"kmx-sel\" data-preset=\"medida\" aria-pressed=\"false\"\u003eA medida\u003cspan\u003eOtro libro\u003c\/span\u003e\u003cem data-precio-preset=\"medida\"\u003e\u003c\/em\u003e\u003c\/button\u003e\n    \u003c\/div\u003e\n    \u003cdiv data-medidas\u003e\u003c\/div\u003e\n    \u003cdiv class=\"kmx-nota\"\u003eMide el libro más grande que vayas a poner delante, con la sobrecubierta si la tiene. En los exhibidores a medida añadimos 1 mm de holgura al ancho y al grosor para que entre y salga sin forzarlo. Todos miden 17 cm de alto.\u003c\/div\u003e\n    \u003cdiv class=\"kmx-subir\"\u003e\n      \u003clabel for=\"kmx-foto\"\u003e📷 Prueba con tu portada\u003c\/label\u003e\n      \u003cinput type=\"file\" id=\"kmx-foto\" accept=\"image\/*\"\u003e\n      \u003cspan data-foto-txt\u003eLa foto solo se usa para la vista previa: no se envía a ningún sitio.\u003c\/span\u003e\n    \u003c\/div\u003e\n  \u003c\/div\u003e\n\n  \u003cdiv class=\"kmx-paso\"\u003e\n    \u003cspan class=\"kmx-et\"\u003e2. Elige el color\u003c\/span\u003e\n    \u003cdiv class=\"kmx-botones\"\u003e\n      \u003cbutton type=\"button\" class=\"kmx-sel\" data-color=\"blanco\" aria-pressed=\"true\"\u003eBlanco\u003c\/button\u003e\n      \u003cbutton type=\"button\" class=\"kmx-sel kmx-negro\" data-color=\"negro\" aria-pressed=\"false\"\u003eNegro\u003c\/button\u003e\n    \u003c\/div\u003e\n  \u003c\/div\u003e\n\n  \u003cdiv class=\"kmx-resultado\"\u003e\n    \u003cspan class=\"kmx-et\"\u003eTu exhibidor medirá\u003c\/span\u003e\n    \u003cdiv class=\"kmx-medidas\"\u003e\n      \u003cdiv\u003e\n\u003cb data-r=\"ancho\"\u003e\u003c\/b\u003e\u003cspan\u003ede ancho\u003c\/span\u003e\n\u003c\/div\u003e\n      \u003cdiv\u003e\n\u003cb data-r=\"alto\"\u003e\u003c\/b\u003e\u003cspan\u003ede alto\u003c\/span\u003e\n\u003c\/div\u003e\n      \u003cdiv\u003e\n\u003cb data-r=\"fondo\"\u003e\u003c\/b\u003e\u003cspan\u003ede fondo\u003c\/span\u003e\n\u003c\/div\u003e\n    \u003c\/div\u003e\n    \u003cdiv class=\"kmx-nota\" data-r=\"balda\"\u003e\u003c\/div\u003e\n    \u003cdiv class=\"kmx-total\"\u003e\n\u003cspan\u003eTotal\u003c\/span\u003e\u003cb data-r=\"precio\"\u003e\u003c\/b\u003e\n\u003c\/div\u003e\n    \u003cbutton type=\"button\" class=\"kmx-boton\" data-comprar\u003eAñadir al carrito\u003c\/button\u003e\n    \u003cdiv class=\"kmx-msg\" data-msg style=\"display:none\"\u003e\u003c\/div\u003e\n    \u003cdiv class=\"kmx-carrito-dev\" data-dev\u003e\u003c\/div\u003e\n    \u003cp class=\"kmx-aviso\"\u003eLa vista previa es aproximada y solo tiene fines ilustrativos. El exhibidor se fabrica con las medidas que nos indiques.\u003c\/p\u003e\n  \u003c\/div\u003e\n\u003c\/div\u003e\n\u003c\/div\u003e\n\u003cscript\u003e\n(function () {\n  var raiz = document.getElementById(\"kmx\");\n  if (!raiz) return;\n\n  \/* ---------- AJUSTES (cambiar aquí) ---------- *\/\n  \/\/ Exhibidor a medida (producto nuevo, falta crearlo en Shopify)\n  var MEDIDA = { precio: 15, variantes: { blanco: 57036599689602, negro: 57036599722370 } };\n  \/\/ Modelos de la tienda. Wi y P: hueco del bolsillo; Wo: ancho exterior (mm, medidos en sus STL)\n  var MODELOS = {\n    tankobon: { nombre: \"Exhibidor para manga\", precio: 12, ancho: 118, alto: 177, grosor: 15, Wi: 119, P: 16, Wo: 123.2, atras: 9,\n      variantes: { blanco: 56496374022530, negro: 56496374055298 } },\n    b6: { nombre: \"Exhibidor para B6\", precio: 12, ancho: 131, alto: 182, grosor: 15, Wi: 132.8, P: 16, Wo: 137, atras: 9,\n      variantes: { blanco: 56655521186178, negro: 56655521218946 } },\n    kanzenban: { nombre: \"Exhibidor para manga XL\", precio: 14, ancho: 152, alto: 210, grosor: 26, Wi: 153.1, P: 27.2, Wo: 156.5, atras: 6,\n      variantes: { blanco: 56655522890114, negro: 56655522922882 } }\n  };\n  var ALTO = 170, FONDO_TRASERO = 111;   \/\/ iguales en todos los exhibidores\n  var LIM = { ancho: [80, 220], grosor: [5, 60] };   \/\/ en mm\n  var ALTO_VISTA = 230;   \/\/ alto con el que se dibuja un libro a medida en la vista previa (novela normal); no cambia la pieza\n  var GROSOR_MEDIO = 0.8; \/\/ para contar los tomos de detrás en los de a medida: un tomo normal mide el 80 % del más gordo\n  \/* -------------------------------------------- *\/\n\n  var E = { ancho: 118, alto: 177, grosor: 15, color: \"blanco\", tomos: true, cotas: true, portada: null };\n  var $ = function (s) { return raiz.querySelector(s); };\n  var $$ = function (s) { return Array.prototype.slice.call(raiz.querySelectorAll(s)); };\n  function cm(mm, dec) { var v = Math.round(mm) \/ 10; return v.toLocaleString(\"es-ES\", { minimumFractionDigits: dec === 0 ? 0 : (v % 1 ? 1 : 0), maximumFractionDigits: 1 }); }\n  function eur(x) { return x.toLocaleString(\"es-ES\", { style: \"currency\", currency: \"EUR\" }); }\n  function lim(k, v) { return Math.min(LIM[k][1], Math.max(LIM[k][0], Math.round(v))); }\n\n  \/\/ El modelo de la tienda que corresponde al tomo (el alto no cuenta: la pieza mide siempre 17 cm)\n  function presetActual() {\n    if (E.medida) return \"medida\";   \/\/ ha pulsado \"A medida\" y aún no ha tocado las medidas\n    for (var p in MODELOS) { var q = MODELOS[p]; if (q.ancho === E.ancho \u0026\u0026 q.grosor === E.grosor) return p; }\n    return \"medida\";\n  }\n  function modelo() { return MODELOS[presetActual()] || null; }\n  function precio() { var m = modelo(); return m ? m.precio : MEDIDA.precio; }\n\n  \/\/ Medidas de la pieza (mm). A medida: como el estándar, con 1 mm de holgura y laterales de 2,1 mm\n  function geo() {\n    var m = modelo(), Wi = m ? m.Wi : E.ancho + 1, P = m ? m.P : E.grosor + 1;\n    return { Wi: Wi, Wo: m ? m.Wo : Wi + 4.2, P: P, Lb: FONDO_TRASERO, H: ALTO, D: FONDO_TRASERO + 4 + P };\n  }\n  \/\/ Los tomos de detrás van de pie, en fila a lo ancho, con el lomo hacia atrás\n  function tomosAtras() { var m = modelo(); return m ? m.atras : Math.max(1, Math.floor((E.ancho + 1) \/ Math.max(5, E.grosor * GROSOR_MEDIO))); }\n\n  \/* ---------- controles de medidas ---------- *\/\n  var CAMPOS = [\n    { k: \"ancho\", et: \"Ancho del libro\", sitio: \"[data-medidas]\" },\n    { k: \"grosor\", et: \"Grosor del libro\", sitio: \"[data-medidas]\" }\n  ];\n  CAMPOS.forEach(function (c) {\n    var d = document.createElement(\"div\");\n    d.className = \"kmx-medida\";\n    d.innerHTML = '\u003cdiv class=\"kmx-fila\"\u003e\u003clabel for=\"kmx-' + c.k + '\"\u003e' + c.et + '\u003c\/label\u003e' +\n      '\u003cdiv class=\"kmx-campo\"\u003e\u003cbutton type=\"button\" data-menos aria-label=\"Quitar 1 mm\"\u003e−\u003c\/button\u003e' +\n      '\u003cinput id=\"kmx-' + c.k + '\" type=\"text\" inputmode=\"decimal\" autocomplete=\"off\"\u003e\u003cspan\u003ecm\u003c\/span\u003e' +\n      '\u003cbutton type=\"button\" data-mas aria-label=\"Añadir 1 mm\"\u003e+\u003c\/button\u003e\u003c\/div\u003e\u003c\/div\u003e' +\n      '\u003cinput type=\"range\" min=\"' + LIM[c.k][0] + '\" max=\"' + LIM[c.k][1] + '\" step=\"1\" aria-label=\"' + c.et + '\"\u003e' +\n      \"\";\n    $(c.sitio).appendChild(d);\n    var caja = d.querySelector('input[type=text]'), barra = d.querySelector('input[type=range]');\n    c.caja = caja; c.barra = barra;\n    function poner(v) { E.medida = false; E[c.k] = lim(c.k, v); cambio(c.k); }\n    barra.addEventListener(\"input\", function () { poner(+barra.value); });\n    d.querySelector(\"[data-menos]\").addEventListener(\"click\", function () { poner(E[c.k] - 1); });\n    d.querySelector(\"[data-mas]\").addEventListener(\"click\", function () { poner(E[c.k] + 1); });\n    caja.addEventListener(\"input\", function () {\n      var v = parseFloat(caja.value.replace(\",\", \".\")) * 10;\n      if (v \u003e= LIM[c.k][0] \u0026\u0026 v \u003c= LIM[c.k][1] \u0026\u0026 Math.round(v) !== E[c.k]) { E.medida = false; E[c.k] = Math.round(v); cambio(c.k, true); }\n    });\n    caja.addEventListener(\"blur\", function () {\n      var v = parseFloat(caja.value.replace(\",\", \".\")) * 10;\n      if (!isNaN(v) \u0026\u0026 lim(c.k, v) !== E[c.k]) { E.medida = false; E[c.k] = lim(c.k, v); }\n      cambio(c.k);\n    });\n  });\n\n  function marcar(sel, attr, val) { $$(sel).forEach(function (b) { b.setAttribute(\"aria-pressed\", String(b.getAttribute(attr) === val)); }); }\n  $$(\"[data-precio-preset]\").forEach(function (el) { var m = MODELOS[el.dataset.precioPreset]; el.textContent = eur(m ? m.precio : MEDIDA.precio); });\n\n  function cambio(k, escribiendo) {\n    CAMPOS.forEach(function (c) {\n      c.barra.value = E[c.k];\n      if (!(escribiendo \u0026\u0026 c.k === k)) c.caja.value = cm(E[c.k], 1).replace(\/^(\\d+)$\/, \"$1,0\");\n    });\n    marcar(\"[data-preset]\", \"data-preset\", presetActual());\n    var g = geo(), m = modelo(), n = tomosAtras();\n    E.alto = m ? m.alto : ALTO_VISTA;   \/\/ alto del libro, solo para la vista previa\n    $('[data-r=\"ancho\"]').textContent = cm(g.Wo) + \" cm\";\n    $('[data-r=\"alto\"]').textContent = cm(g.H) + \" cm\";\n    $('[data-r=\"fondo\"]').textContent = cm(g.D) + \" cm\";\n    $('[data-r=\"balda\"]').textContent = (m ? \"Es nuestro \" + m.nombre + \". \" : \"\") +\n      \"Detrás caben unos \" + n + \" libros como el de delante. El libro asoma por encima según lo alto que sea: comprueba que cabe en tu estantería.\";\n    $('[data-r=\"precio\"]').textContent = eur(precio());\n    reiniciarBoton();\n    interno(g);\n    if (vista) vista.rehacer();\n  }\n\n  $$(\"[data-preset]\").forEach(function (b) {\n    b.addEventListener(\"click\", function () {\n      var p = MODELOS[b.dataset.preset];\n      if (p) { E.medida = false; E.ancho = p.ancho; E.grosor = p.grosor; }\n      else { E.medida = true; }   \/\/ a medida: parte de las medidas del modelo anterior\n      cambio();\n    });\n  });\n  $$(\"[data-color]\").forEach(function (b) {\n    b.addEventListener(\"click\", function () { E.color = b.dataset.color; marcar(\"[data-color]\", \"data-color\", E.color); reiniciarBoton(); interno(geo()); if (vista) vista.rehacer(); });\n  });\n  $$(\"[data-chip]\").forEach(function (b) {\n    b.addEventListener(\"click\", function () {\n      var k = b.dataset.chip; E[k] = !E[k]; b.setAttribute(\"aria-pressed\", String(E[k]));\n      if (vista) vista.rehacer();\n    });\n  });\n  $(\"#kmx-foto\").addEventListener(\"change\", function (ev) {\n    var f = ev.target.files \u0026\u0026 ev.target.files[0];\n    if (!f) return;\n    var lector = new FileReader();\n    lector.onload = function () {\n      var im = new Image();\n      im.onload = function () {\n        E.portada = im;\n        $(\"[data-foto-txt]\").innerHTML = 'Tu portada está en la vista previa. \u003cbutton type=\"button\" data-quitar-foto\u003eQuitarla\u003c\/button\u003e';\n        $(\"[data-quitar-foto]\").addEventListener(\"click\", function () { E.portada = null; $(\"#kmx-foto\").value = \"\"; $(\"[data-foto-txt]\").textContent = \"La foto solo se usa para la vista previa: no se envía a ningún sitio.\"; if (vista) vista.portada(); });\n        if (!E.tomos) { E.tomos = true; $('[data-chip=\"tomos\"]').setAttribute(\"aria-pressed\", \"true\"); if (vista) vista.rehacer(); }\n        if (vista) { vista.portada(); vista.ir(\"frente\"); }\n      };\n      im.src = lector.result;\n    };\n    lector.readAsDataURL(f);\n  });\n\n  \/* ---------- carrito ---------- *\/\n  function datosCarrito() {\n    var g = geo(), m = modelo();\n    if (m) return { items: [{ id: m.variantes[E.color], quantity: 1 }] };   \/\/ modelo de siempre: su propio producto\n    return {\n      items: [{\n        id: MEDIDA.variantes[E.color], quantity: 1, properties: {\n          \"Libro delantero\": cm(E.ancho) + \" × \" + cm(E.grosor) + \" cm (ancho × grosor)\",\n          \"Medidas del exhibidor\": cm(g.Wo) + \" × \" + cm(g.H) + \" × \" + cm(g.D) + \" cm\",\n          \"_Hueco del bolsillo (mm)\": g.Wi + \" × \" + g.P\n        }\n      }]\n    };\n  }\n  function reiniciarBoton() { var b = $(\"[data-comprar]\"); b.disabled = false; b.textContent = \"Añadir al carrito\"; b.onclick = comprar; $(\"[data-dev]\").style.display = \"none\"; $(\"[data-msg]\").style.display = \"none\"; }\n  function comprar() {\n    var btn = $(\"[data-comprar]\"), msg = $(\"[data-msg]\"), datos = datosCarrito();\n    var enTienda = \/kamimarks\\.com$\/.test(location.hostname) || (window.Shopify \u0026\u0026 window.Shopify.shop);\n    if (!enTienda || !\/^\\d+$\/.test(String(datos.items[0].id))) {\n      var dev = $(\"[data-dev]\");\n      dev.style.display = \"block\";\n      dev.textContent = \"Boceto: en la tienda esto se manda a \/cart\/add.js\\n\\n\" + JSON.stringify(datos, null, 2);\n      return;\n    }\n    btn.disabled = true; btn.textContent = \"Añadiendo…\";\n    fetch(\"\/cart\/add.js\", { method: \"POST\", headers: { \"Content-Type\": \"application\/json\" }, body: JSON.stringify(datos) })\n      .then(function (r) { return r.json(); })\n      .then(function (d) {\n        btn.disabled = false;\n        if (d.status \u0026\u0026 d.status \u003e= 400) { btn.textContent = \"Añadir al carrito\"; msg.style.display = \"\"; msg.textContent = \"⚠️ \" + (d.description || \"No se ha podido añadir.\"); }\n        else { btn.textContent = \"✅ Listo, pulsa aquí para ver el carrito\"; btn.onclick = function () { location.href = \"\/cart\"; }; }\n      })\n      .catch(function () { btn.disabled = false; btn.textContent = \"Añadir al carrito\"; msg.style.display = \"\"; msg.textContent = \"⚠️ Ha habido un problema de conexión. Inténtalo de nuevo.\"; });\n  }\n\n  \/* ---------- panel interno (solo existe en el boceto) ---------- *\/\n  function volumenCm3(g) {\n    \/\/ modelo de paredes calibrado con el STL estándar con la pared lisa (97,8 cm³)\n    var v = g.Wo * g.D + g.Wi * g.P + 4.4 * (4 + g.P) * (g.H - 1) + 4.4 * (g.Lb * 49 + 28) +\n      16 * (g.H - 1) + (g.Wi - 8) * 10 + g.Wi * 2 * (g.H - 1);\n    return v * 0.968 \/ 1000;\n  }\n  function interno(g) {\n    var el = document.getElementById(\"kmx-interno\");\n    if (!el) return;\n    var m = modelo(), gramos = volumenCm3(g) * 115 \/ 97.8, d = datosCarrito().items[0];\n    el.innerHTML = \"\u003ctr\u003e\u003ctd\u003eProducto\u003c\/td\u003e\u003ctd\u003e\" + (m ? m.nombre : \"Exhibidor a medida\") + \"\u003c\/td\u003e\u003c\/tr\u003e\" +\n      \"\u003ctr\u003e\u003ctd\u003eVariante que va al carrito\u003c\/td\u003e\u003ctd\u003e\" + d.id + \"\u003c\/td\u003e\u003c\/tr\u003e\" +\n      \"\u003ctr\u003e\u003ctd\u003eHueco del bolsillo (ancho × grosor)\u003c\/td\u003e\u003ctd\u003e\" + g.Wi + \" × \" + g.P + \" mm\u003c\/td\u003e\u003c\/tr\u003e\" +\n      \"\u003ctr\u003e\u003ctd\u003eMaterial estimado (pared lisa)\u003c\/td\u003e\u003ctd\u003e\" + Math.round(gramos) + \" g (\" + (gramos \u003e= 115 ? \"+\" : \"\") + Math.round((gramos \/ 115 - 1) * 100) + \" % frente al estándar)\u003c\/td\u003e\u003c\/tr\u003e\";\n  }\n\n  \/* ---------- vista 3D (three.js) ---------- *\/\n  var vista = null;\n  function cargarThree(cb) {\n    if (window.THREE) return cb();\n    var s = document.createElement(\"script\");\n    s.src = \"https:\/\/cdnjs.cloudflare.com\/ajax\/libs\/three.js\/r128\/three.min.js\";\n    s.onload = cb;\n    s.onerror = function () { $(\"[data-cargando]\").textContent = \"No se ha podido cargar la vista 3D, pero puedes configurarlo igual.\"; };\n    document.head.appendChild(s);\n  }\n\n  function crearVista() {\n    var T = window.THREE, visor = $(\"[data-visor]\"), canvas = visor.querySelector(\"canvas\");\n    var ren;\n    try { ren = new T.WebGLRenderer({ canvas: canvas, antialias: true, alpha: true }); }\n    catch (e) { $(\"[data-cargando]\").textContent = \"Tu navegador no puede mostrar la vista 3D, pero puedes configurarlo igual.\"; return null; }\n    $(\"[data-cargando]\").style.display = \"none\";\n    ren.setPixelRatio(Math.min(2, window.devicePixelRatio || 1));\n    ren.outputEncoding = T.sRGBEncoding;\n    ren.shadowMap.enabled = true;\n    ren.shadowMap.type = T.PCFSoftShadowMap;\n\n    var esc = new T.Scene();\n    var cam = new T.PerspectiveCamera(28, 4 \/ 3, 5, 6000);\n    esc.add(cam);\n    esc.add(new T.HemisphereLight(0xffffff, 0xbdb6aa, 0.5));\n    var foco = new T.DirectionalLight(0xffffff, 0.85);\n    foco.position.set(260, 420, 120);              \/\/ relativo a la cámara: la luz gira con ella\n    foco.castShadow = true;\n    foco.shadow.mapSize.set(1024, 1024);\n    var sc = foco.shadow.camera; sc.left = -320; sc.right = 320; sc.top = 320; sc.bottom = -320; sc.near = 50; sc.far = 2500;\n    foco.shadow.bias = -0.0008;\n    cam.add(foco);\n    var centro = new T.Object3D(); esc.add(centro); foco.target = centro;\n    var relleno = new T.DirectionalLight(0xffffff, 0.22); relleno.position.set(-300, 100, 50); cam.add(relleno);\n\n    var suelo = new T.Mesh(new T.PlaneGeometry(3000, 3000), new T.ShadowMaterial({ opacity: 0.16 }));\n    suelo.rotation.x = -Math.PI \/ 2; suelo.receiveShadow = true; esc.add(suelo);\n    var rejilla = new T.GridHelper(600, 60, 0xd8d3ca, 0xe6e2da);   \/\/ cuadros de 1 cm para dar escala\n    rejilla.position.y = 0.05; esc.add(rejilla);\n\n    var pieza = new T.Group(), libros = new T.Group(), cotas = new T.Group();\n    esc.add(pieza); esc.add(libros); esc.add(cotas);\n\n    var lienzo = document.createElement(\"canvas\");\n    var texPortada = new T.CanvasTexture(lienzo); texPortada.encoding = T.sRGBEncoding; texPortada.anisotropy = 4;\n\n    function limpiar(gr) {\n      while (gr.children.length) {\n        var o = gr.children.pop();\n        o.traverse(function (x) { if (x.geometry) x.geometry.dispose(); });   \/\/ los materiales se reutilizan\n      }\n    }\n    var matPieza = new T.MeshStandardMaterial({ color: 0xf3f2ef, roughness: 0.62, metalness: 0 });\n    var matBorde = new T.LineBasicMaterial({ color: 0x6a6a6a, transparent: true, opacity: 0.6 });\n    function conBordes(m) {\n      m.castShadow = true;\n      m.add(new T.LineSegments(new T.EdgesGeometry(m.geometry, 25), matBorde));\n      pieza.add(m); return m;\n    }\n    function caja(w, h, d, x, y, z) {\n      var m = new T.Mesh(new T.BoxGeometry(w, h, d), matPieza);\n      m.position.set(x, y + h \/ 2, z); return conBordes(m);\n    }\n    function hacerPieza(g) {\n      limpiar(pieza);\n      matPieza.color.setHex(E.color === \"negro\" ? 0x2b2b2b : 0xf3f2ef);\n      matBorde.color.setHex(E.color === \"negro\" ? 0x777777 : 0x6a6a6a);\n      var z0 = -g.D \/ 2, zDiv = z0 + g.Lb, zFr = g.D \/ 2;\n      caja(g.Wo, 1, g.D, 0, 0, 0);                                   \/\/ base\n      caja(g.Wi, 1, g.P, 0, 1, zDiv + 2 + g.P \/ 2);                   \/\/ suelo del bolsillo\n      caja(4, g.H, 2, -g.Wi \/ 2 + 2, 0, zFr - 1);                     \/\/ pestañas que sujetan la portada\n      caja(4, g.H, 2, g.Wi \/ 2 - 2, 0, zFr - 1);\n      caja(g.Wi - 8, 7, 2, 0, 0, zFr - 1);                            \/\/ reborde de abajo\n      caja(g.Wi, g.H - 1, 2, 0, 1, zDiv + 1);                         \/\/ pared de la portada, lisa\n      \/\/ laterales: altos delante, bajan con un chaflán a 5 cm detrás\n      var pl = new T.Shape(), pts = [[z0, 0], [zFr, 0], [zFr, g.H], [zDiv, g.H], [zDiv, 57.5], [zDiv - 7.5, 50], [z0, 50]];\n      pts.forEach(function (p, i) { if (i) pl.lineTo(-p[0], p[1]); else pl.moveTo(-p[0], p[1]); });\n      pl.closePath();\n      var tw = (g.Wo - g.Wi) \/ 2;\n      [g.Wi \/ 2, -g.Wi \/ 2 - tw].forEach(function (x) {\n        var m = new T.Mesh(new T.ExtrudeGeometry(pl, { depth: tw, bevelEnabled: false }), matPieza);\n        m.rotation.y = Math.PI \/ 2; m.position.x = x; conBordes(m);\n      });\n    }\n\n    var COLORES = [0x2f5d8a, 0xc8553d, 0x3f7d58, 0xe0a526, 0x6b4c8a, 0x1f1f1f, 0xb83b5e, 0x4a90a4, 0x8a6d3b];\n    var matPapel = new T.MeshStandardMaterial({ color: 0xf1ead8, roughness: 0.9 });\n    var matPortada = new T.MeshStandardMaterial({ map: texPortada, roughness: 0.55 });\n    var lomo = new T.MeshStandardMaterial({ color: 0xe9e4da, roughness: 0.6 });\n    \/\/ colores de los libros de detrás: se barajan una vez por visita y luego no cambian\n    var pal = COLORES.slice();\n    for (var q = pal.length - 1; q \u003e 0; q--) { var r = Math.floor(Math.random() * (q + 1)), tmpc = pal[q]; pal[q] = pal[r]; pal[r] = tmpc; }\n    var matColores = pal.map(function (c) { return new T.MeshStandardMaterial({ color: c, roughness: 0.6 }); });\n    function hacerLibros(g) {\n      limpiar(libros);\n      if (!E.tomos) return;\n      var z0 = -g.D \/ 2, zDiv = z0 + g.Lb;\n      var frente = new T.Mesh(new T.BoxGeometry(E.ancho, E.alto, E.grosor),\n        [lomo, matPapel, matPapel, matPapel, matPortada, lomo]);\n      frente.position.set(0, 2 + E.alto \/ 2, zDiv + 2 + 0.5 + E.grosor \/ 2);\n      frente.castShadow = true; libros.add(frente);\n      \/\/ detrás: de pie, perpendiculares al de delante, en fila a lo ancho y con el lomo hacia atrás\n      var n = tomosAtras(), t = (g.Wi - 1) \/ n - 0.6;\n      for (var i = 0; i \u003c n; i++) {\n        \/\/ Los colores se cuentan desde los extremos: los dos libros de fuera (los que más se ven de lado)\n        \/\/ conservan siempre su color aunque cambie cuántos caben; los nuevos aparecen por dentro.\n        var k = n - 1 - i;\n        var ci = k === 0 ? 0 : (i === 0 ? 1 : 2 + (k - 1) % (matColores.length - 2));\n        var c = matColores[ci];\n        var h = E.alto - (k % 3) * 1.5;\n        var b = new T.Mesh(new T.BoxGeometry(t, h, E.ancho), [c, c, matPapel, matPapel, matPapel, c]);\n        b.position.set(-g.Wi \/ 2 + 0.5 + t \/ 2 + i * (t + 0.6), 1 + h \/ 2, zDiv - 0.5 - E.ancho \/ 2);\n        b.castShadow = true; libros.add(b);\n      }\n    }\n\n    function dibujarPortada() {\n      var w = 512, h = Math.round(512 * E.alto \/ E.ancho), c = lienzo.getContext(\"2d\");\n      lienzo.width = w; lienzo.height = h;\n      if (E.portada) {\n        var im = E.portada, k = Math.max(w \/ im.width, h \/ im.height);\n        c.drawImage(im, (w - im.width * k) \/ 2, (h - im.height * k) \/ 2, im.width * k, im.height * k);\n      } else {\n        var gr = c.createLinearGradient(0, 0, 0, h); gr.addColorStop(0, \"#ffb347\"); gr.addColorStop(1, \"#f0506e\");\n        c.fillStyle = gr; c.fillRect(0, 0, w, h);\n        c.save(); c.translate(w \/ 2, h * 0.6); c.fillStyle = \"rgba(255,255,255,.16)\";\n        for (var i = 0; i \u003c 18; i++) { c.rotate(Math.PI \/ 9); c.beginPath(); c.moveTo(0, 0); c.lineTo(-40, -h); c.lineTo(40, -h); c.closePath(); c.fill(); }\n        c.restore();\n        c.beginPath(); c.arc(w \/ 2, h * 0.6, w * 0.24, 0, 7); c.fillStyle = \"#fff4d6\"; c.fill();\n        c.fillStyle = \"#111\"; c.fillRect(0, 0, w, h * 0.2);\n        c.fillStyle = \"#fff\"; c.textAlign = \"center\"; c.textBaseline = \"middle\";\n        c.font = \"900 \" + Math.round(w * 0.13) + \"px Montserrat, Arial, sans-serif\";\n        c.fillText(\"TU LIBRO\", w \/ 2, h * 0.1);\n        c.beginPath(); c.arc(w * 0.86, h * 0.27, w * 0.07, 0, 7); c.fillStyle = \"#111\"; c.fill();\n        c.fillStyle = \"#fff\"; c.font = \"800 \" + Math.round(w * 0.07) + \"px Montserrat, Arial, sans-serif\"; c.fillText(\"1\", w * 0.86, h * 0.272);\n        c.fillStyle = \"rgba(0,0,0,.55)\"; c.font = \"600 \" + Math.round(w * 0.045) + \"px Geist, Arial, sans-serif\";\n        c.fillText(\"Prueba con la foto de tu portada\", w \/ 2, h * 0.93);\n      }\n      texPortada.needsUpdate = true;\n    }\n\n    \/\/ líneas de cota y sus etiquetas\n    var matCota = new T.LineBasicMaterial({ color: 0x222222 });\n    var etiquetas = {}; $$(\"[data-cota]\").forEach(function (el) { etiquetas[el.dataset.cota] = { el: el, p: new T.Vector3() }; });\n    var ladoCotas = null;\n    function hacerCotas(g, sx, sz) {\n      limpiar(cotas);\n      [\"ancho\", \"alto\", \"fondo\"].forEach(function (k) { etiquetas[k].el.style.display = E.cotas ? \"block\" : \"none\"; });\n      if (!E.cotas) return;\n      var p = [], o = 14, e = 20, xs = sx * g.Wo \/ 2, zs = sz * g.D \/ 2, xl = xs + sx * o, zl = zs + sz * o;\n      function seg(a, b) { p.push(new T.Vector3(a[0], a[1], a[2]), new T.Vector3(b[0], b[1], b[2])); }\n      seg([-g.Wo \/ 2, 0.3, zl], [g.Wo \/ 2, 0.3, zl]);                          \/\/ ancho\n      seg([-g.Wo \/ 2, 0.3, zs], [-g.Wo \/ 2, 0.3, zs + sz * e]); seg([g.Wo \/ 2, 0.3, zs], [g.Wo \/ 2, 0.3, zs + sz * e]);\n      seg([xl, 0, zs], [xl, g.H, zs]);                                          \/\/ alto\n      seg([xs, g.H, zs], [xs + sx * e, g.H, zs]); seg([xs, 0.3, zs], [xs + sx * e, 0.3, zs]);\n      seg([xl, 0.3, -g.D \/ 2], [xl, 0.3, g.D \/ 2]);                            \/\/ fondo\n      seg([xs, 0.3, -zs], [xs + sx * e, 0.3, -zs]);\n      cotas.add(new T.LineSegments(new T.BufferGeometry().setFromPoints(p), matCota));\n      etiquetas.ancho.p.set(0, 0, zl);\n      etiquetas.alto.p.set(xl, g.H \/ 2, zs);\n      etiquetas.fondo.p.set(xl, 0, 0);\n      etiquetas.ancho.el.textContent = cm(g.Wo) + \" cm\";\n      etiquetas.alto.el.textContent = cm(g.H) + \" cm\";\n      etiquetas.fondo.el.textContent = cm(g.D) + \" cm\";\n    }\n\n    \/\/ cámara en órbita\n    var o = { yaw: 0.62, pitch: 0.3, dist: 900, cy: 90, yawT: 0.62, pitchT: 0.3, distT: 900, cyT: 90 };\n    var VISTAS = { tres: [0.62, 0.3], frente: [0, 0.12], lado: [Math.PI \/ 2, 0.12], detras: [Math.PI - 0.55, 0.42] };\n    function encuadrar(g) {\n      var alto = Math.max(g.H, E.tomos ? E.alto + 2 : 0);\n      var r = 0.5 * Math.sqrt(g.Wo * g.Wo + alto * alto + g.D * g.D) + (E.cotas ? 22 : 8);\n      o.distT = 0.86 * r \/ Math.sin(cam.fov * Math.PI \/ 360);\n      o.cyT = alto \/ 2;\n    }\n    function rehacer() {\n      var g = geo();\n      hacerPieza(g); hacerLibros(g); dibujarPortada(); ladoCotas = null; encuadrar(g);\n    }\n    function ir(nombre) {\n      var v = VISTAS[nombre], d = v[0] - o.yawT;\n      d = Math.atan2(Math.sin(d), Math.cos(d));\n      o.yawT += d; o.pitchT = v[1];\n    }\n    $$(\"[data-vista]\").forEach(function (b) { b.addEventListener(\"click\", function () { ir(b.dataset.vista); }); });\n\n    var arr = null;\n    canvas.addEventListener(\"pointerdown\", function (e) { arr = { x: e.clientX, y: e.clientY, yaw: o.yawT, pitch: o.pitchT }; try { canvas.setPointerCapture(e.pointerId); } catch (_) {} });\n    canvas.addEventListener(\"pointermove\", function (e) {\n      if (!arr) return;\n      o.yawT = arr.yaw - (e.clientX - arr.x) * 0.01;\n      o.pitchT = Math.min(1.2, Math.max(0.03, arr.pitch + (e.clientY - arr.y) * 0.006));\n    });\n    function soltar() { arr = null; }\n    canvas.addEventListener(\"pointerup\", soltar); canvas.addEventListener(\"pointercancel\", soltar);\n\n    function tamano() {\n      var w = visor.clientWidth, h = visor.clientHeight;\n      if (!w || !h) return;\n      ren.setSize(w, h, false); cam.aspect = w \/ h; cam.updateProjectionMatrix();\n    }\n    window.addEventListener(\"resize\", tamano);\n\n    var visible = true;\n    if (\"IntersectionObserver\" in window) new IntersectionObserver(function (es) { visible = es[0].isIntersecting; }).observe(visor);\n\n    var tmp = new T.Vector3();\n    function cuadro() {\n      requestAnimationFrame(cuadro);\n      if (!visible) return;\n      var k = 0.14;\n      o.yaw += (o.yawT - o.yaw) * k; o.pitch += (o.pitchT - o.pitch) * k;\n      o.dist += (o.distT - o.dist) * k; o.cy += (o.cyT - o.cy) * k;\n      var g = geo();\n      var sx = Math.sin(o.yaw) \u003e= 0 ? 1 : -1, sz = Math.cos(o.yaw) \u003e= 0 ? 1 : -1;   \/\/ cotas en el lado que se ve\n      var lado = sx + \",\" + sz + \",\" + E.cotas;\n      if (lado !== ladoCotas) { ladoCotas = lado; hacerCotas(g, sx, sz); }\n      centro.position.set(0, o.cy, 0);\n      cam.position.set(o.dist * Math.sin(o.yaw) * Math.cos(o.pitch), o.cy + o.dist * Math.sin(o.pitch), o.dist * Math.cos(o.yaw) * Math.cos(o.pitch));\n      cam.lookAt(0, o.cy, 0);\n      ren.render(esc, cam);\n      if (E.cotas) {\n        var w = visor.clientWidth, h = visor.clientHeight;\n        for (var n in etiquetas) {\n          tmp.copy(etiquetas[n].p).project(cam);\n          etiquetas[n].el.style.left = ((tmp.x + 1) \/ 2 * w) + \"px\";\n          etiquetas[n].el.style.top = ((1 - tmp.y) \/ 2 * h) + \"px\";\n        }\n      }\n    }\n    tamano(); rehacer();\n    o.dist = o.distT; o.cy = o.cyT;\n    cuadro();\n    return { rehacer: rehacer, portada: dibujarPortada, ir: ir };\n  }\n\n  cambio();\n  if (document.readyState === \"loading\") document.addEventListener(\"DOMContentLoaded\", function () { interno(geo()); });\n  cargarThree(function () { vista = crearVista(); });\n})();\n\u003c\/script\u003e\n\u003ch2\u003e\u0026gt;📐 ¿Cómo mido mi libro?\u003c\/h2\u003e\n\u003cul\u003e\n\u003cli\u003e\n\u003cstrong\u003eAncho:\u003c\/strong\u003e mide la portada de lado a lado. Si el libro tiene sobrecubierta, mídelo con ella puesta.\u003c\/li\u003e\n\u003cli\u003e\n\u003cstrong\u003eGrosor:\u003c\/strong\u003e mide el lomo. Si vas a ir cambiando de libro, mide el más gordo.\u003c\/li\u003e\n\u003cli\u003e\n\u003cstrong\u003eAlto:\u003c\/strong\u003e no hace falta. El exhibidor mide siempre 17 cm de alto y el libro asoma por arriba.\u003c\/li\u003e\n\u003c\/ul\u003e\n\u003cp\u003eNosotros añadimos 1 mm de holgura al ancho y al grosor para que el libro entre y salga sin forzarlo.\u003c\/p\u003e\n\u003ch2\u003e📏 Medidas\u003c\/h2\u003e\n\u003cul\u003e\n\u003cli\u003e\n\u003cstrong\u003eAlto:\u003c\/strong\u003e 17 cm.\u003c\/li\u003e\n\u003cli\u003e\n\u003cstrong\u003eAncho:\u003c\/strong\u003e el de tu libro más medio centímetro.\u003c\/li\u003e\n\u003cli\u003e\n\u003cstrong\u003eFondo:\u003c\/strong\u003e unos 11 cm para los libros de detrás más el hueco del libro de delante.\u003c\/li\u003e\n\u003cli\u003e\n\u003cstrong\u003eDetrás:\u003c\/strong\u003e los libros van de pie, uno al lado del otro, como en una estantería.\u003c\/li\u003e\n\u003c\/ul\u003e\n\u003cp\u003eEl configurador te dice las medidas exactas de tu exhibidor y cuántos libros caben detrás. Comprueba que entra en tu balda, contando lo que asoma el libro por arriba.\u003c\/p\u003e\n\u003ch2\u003e📖 ¿Qué incluye el paquete?\u003c\/h2\u003e\n\u003cp\u003eTu exhibidor, hecho a la medida de tu libro, y un regalito de nuestra parte. Puedes colocar tantos exhibidores juntos como quieras para crear tu propia composición: están pensados para que el hueco entre uno y otro sea mínimo.\u003c\/p\u003e\n\u003ch2\u003e🎨 Colores\u003c\/h2\u003e\n\u003cp\u003eBlanco o negro. Si quieres otro color, escríbenos y lo vemos.\u003c\/p\u003e\n\u003ch2\u003e📕 ¿Por qué un exhibidor a medida?\u003c\/h2\u003e\n\u003cp\u003ePorque no todos los libros tienen las medidas de un manga. Con un exhibidor a medida, tu libro favorito queda bien sujeto y con la portada a la vista, sin holguras ni apreturas.\u003c\/p\u003e","brand":"KamiMarks","offers":[{"title":"Blanco","offer_id":57036599689602,"sku":null,"price":15.0,"currency_code":"EUR","in_stock":true},{"title":"Negro","offer_id":57036599722370,"sku":null,"price":15.0,"currency_code":"EUR","in_stock":true}],"thumbnail_url":"\/\/cdn.shopify.com\/s\/files\/1\/0937\/6363\/3538\/files\/exhibidor-manga-tomos_2.jpg?v=1774088136"}],"url":"https:\/\/kamimarks.com\/collections\/productos-personalizables.oembed","provider":"KamiMarks","version":"1.0","type":"link"}