{"id":533,"date":"2026-03-31T12:06:32","date_gmt":"2026-03-31T12:06:32","guid":{"rendered":"https:\/\/nueva.acustium.com\/?page_id=533"},"modified":"2026-06-16T11:32:17","modified_gmt":"2026-06-16T11:32:17","slug":"calculadora","status":"publish","type":"page","link":"https:\/\/acustium.es\/?page_id=533","title":{"rendered":"Calculadora"},"content":{"rendered":"\t\t<div data-elementor-type=\"wp-page\" data-elementor-id=\"533\" class=\"elementor elementor-533\">\n\t\t\t\t<div class=\"elementor-element elementor-element-2ae6440 e-flex e-con-boxed elementor-invisible e-con e-parent\" data-id=\"2ae6440\" data-element_type=\"container\" data-e-type=\"container\" data-settings=\"{&quot;animation&quot;:&quot;fadeIn&quot;,&quot;background_background&quot;:&quot;classic&quot;,&quot;position&quot;:&quot;fixed&quot;}\">\n\t\t\t\t\t<div class=\"e-con-inner\">\n\t\t<div class=\"elementor-element elementor-element-db52a02 e-con-full e-flex e-con e-child\" data-id=\"db52a02\" data-element_type=\"container\" data-e-type=\"container\">\n\t\t\t\t<div class=\"elementor-element elementor-element-f6d9db8 elementor-absolute elementor-invisible elementor-widget elementor-widget-image\" data-id=\"f6d9db8\" data-element_type=\"widget\" data-e-type=\"widget\" data-settings=\"{&quot;_animation&quot;:&quot;fadeInDown&quot;,&quot;_position&quot;:&quot;absolute&quot;}\" data-widget_type=\"image.default\">\n\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t<a href=\"https:\/\/acustium.es\">\n\t\t\t\t\t\t\t<img fetchpriority=\"high\" decoding=\"async\" width=\"496\" height=\"217\" src=\"https:\/\/acustium.es\/wp-content\/uploads\/2026\/05\/6b49573f-a8ce-480b-ad2a-097dbe2359f6.png\" class=\"attachment-full size-full wp-image-1251\" alt=\"\" srcset=\"https:\/\/acustium.es\/wp-content\/uploads\/2026\/05\/6b49573f-a8ce-480b-ad2a-097dbe2359f6.png 496w, https:\/\/acustium.es\/wp-content\/uploads\/2026\/05\/6b49573f-a8ce-480b-ad2a-097dbe2359f6-300x131.png 300w\" sizes=\"(max-width: 496px) 100vw, 496px\" \/>\t\t\t\t\t\t\t\t<\/a>\n\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t<\/div>\n\t\t\t\t<\/div>\n\t\t<div class=\"elementor-element elementor-element-df48667 e-con-full e-flex e-con e-child\" data-id=\"df48667\" data-element_type=\"container\" data-e-type=\"container\">\n\t\t\t\t<div class=\"elementor-element elementor-element-36caaee elementor-absolute elementor-invisible elementor-widget elementor-widget-ekit-nav-menu\" data-id=\"36caaee\" data-element_type=\"widget\" data-e-type=\"widget\" data-settings=\"{&quot;_animation&quot;:&quot;fadeInDown&quot;,&quot;_position&quot;:&quot;absolute&quot;}\" data-widget_type=\"ekit-nav-menu.default\">\n\t\t\t\t<div class=\"elementor-widget-container\">\n\t\t\t\t\t\t\t<nav class=\"ekit-wid-con ekit_menu_responsive_tablet\"\n\t\t\tdata-hamburger-icon=\"icon icon-menu-11\"\n\t\t\tdata-hamburger-icon-type=\"icon\"\n\t\t\tdata-responsive-breakpoint=\"1024\">\n\t\t\t            <button class=\"elementskit-menu-hamburger elementskit-menu-toggler\"  type=\"button\" aria-label=\"hamburger-icon\">\n                <i aria-hidden=\"true\" class=\"ekit-menu-icon icon icon-menu-11\"><\/i>            <\/button>\n            <div id=\"ekit-megamenu-menu-header\" class=\"elementskit-menu-container elementskit-menu-offcanvas-elements elementskit-navbar-nav-default ekit-nav-menu-one-page- ekit-nav-dropdown-hover\"><ul id=\"menu-menu-header\" class=\"elementskit-navbar-nav elementskit-menu-po-center submenu-click-on-icon\"><li id=\"menu-item-375\" class=\"menu-item menu-item-type-post_type menu-item-object-page menu-item-has-children menu-item-375 nav-item elementskit-dropdown-has relative_position elementskit-dropdown-menu-default_width elementskit-mobile-builder-content\" data-vertical-menu=750px><a href=\"https:\/\/acustium.es\/?page_id=253\" class=\"ekit-menu-nav-link ekit-menu-dropdown-toggle\">PRODUCTOS<i aria-hidden=\"true\" class=\"icon icon-down-arrow1 elementskit-submenu-indicator\"><\/i><\/a>\n<ul class=\"elementskit-dropdown elementskit-submenu-panel\">\n\t<li id=\"menu-item-772\" class=\"menu-item menu-item-type-post_type menu-item-object-page menu-item-772 nav-item elementskit-mobile-builder-content\" data-vertical-menu=750px><a href=\"https:\/\/acustium.es\/?page_id=734\" class=\" dropdown-item\">Arquitectura textil by PONGS<\/a>\t<li id=\"menu-item-546\" class=\"menu-item menu-item-type-post_type menu-item-object-page menu-item-546 nav-item elementskit-mobile-builder-content\" data-vertical-menu=750px><a href=\"https:\/\/acustium.es\/?page_id=535\" class=\" dropdown-item\">Fichas de producto<\/a>\t<li id=\"menu-item-545\" class=\"menu-item menu-item-type-post_type menu-item-object-page menu-item-545 nav-item elementskit-mobile-builder-content\" data-vertical-menu=750px><a href=\"https:\/\/acustium.es\/?page_id=537\" class=\" dropdown-item\">Normativa &#8211; Control de calidad<\/a>\t<li id=\"menu-item-547\" class=\"menu-item menu-item-type-post_type menu-item-object-page menu-item-547 nav-item elementskit-mobile-builder-content\" data-vertical-menu=750px><a href=\"https:\/\/acustium.es\/?page_id=533\" class=\" dropdown-item\">Calculadora<\/a><\/ul>\n<\/li>\n<li id=\"menu-item-858\" class=\"menu-item menu-item-type-post_type menu-item-object-page menu-item-858 nav-item elementskit-mobile-builder-content\" data-vertical-menu=750px><a href=\"https:\/\/acustium.es\/?page_id=850\" class=\"ekit-menu-nav-link\">SOLUCIONES<\/a><\/li>\n<li id=\"menu-item-531\" class=\"menu-item menu-item-type-post_type menu-item-object-page menu-item-531 nav-item elementskit-mobile-builder-content\" data-vertical-menu=750px><a href=\"https:\/\/acustium.es\/?page_id=270\" class=\"ekit-menu-nav-link\">SECTORES<\/a><\/li>\n<li id=\"menu-item-518\" class=\"menu-item menu-item-type-post_type menu-item-object-page menu-item-518 nav-item elementskit-mobile-builder-content\" data-vertical-menu=750px><a href=\"https:\/\/acustium.es\/?page_id=448\" class=\"ekit-menu-nav-link\">NOSOTROS<\/a><\/li>\n<li id=\"menu-item-532\" class=\"menu-item menu-item-type-post_type menu-item-object-page menu-item-532 nav-item elementskit-mobile-builder-content\" data-vertical-menu=750px><a href=\"https:\/\/acustium.es\/?page_id=288\" class=\"ekit-menu-nav-link\">PROYECTOS<\/a><\/li>\n<li id=\"menu-item-377\" class=\"menu-item menu-item-type-post_type menu-item-object-page menu-item-377 nav-item elementskit-mobile-builder-content\" data-vertical-menu=750px><a href=\"https:\/\/acustium.es\/?page_id=364\" class=\"ekit-menu-nav-link\">BLOG<\/a><\/li>\n<li id=\"menu-item-469\" class=\"menu-item menu-item-type-post_type menu-item-object-page menu-item-469 nav-item elementskit-mobile-builder-content\" data-vertical-menu=750px><a href=\"https:\/\/acustium.es\/?page_id=300\" class=\"ekit-menu-nav-link\">CONTACTO<\/a><\/li>\n<\/ul><div class=\"elementskit-nav-identity-panel\"><a class=\"elementskit-nav-logo\" href=\"https:\/\/acustium.es\" target=\"\" rel=\"\"><img src=\"https:\/\/nueva.acustium.com\/wp-content\/uploads\/2026\/03\/logo.png\" title=\"logo.png\" alt=\"logo.png\" decoding=\"async\" \/><\/a><button class=\"elementskit-menu-close elementskit-menu-toggler\" type=\"button\">X<\/button><\/div><\/div>\n\t\t\t<div class=\"elementskit-menu-overlay elementskit-menu-offcanvas-elements elementskit-menu-toggler ekit-nav-menu--overlay\"><\/div>        <\/nav>\n\t\t\t\t\t\t<\/div>\n\t\t\t\t<\/div>\n\t\t\t\t<\/div>\n\t\t<div class=\"elementor-element elementor-element-f8df9ed e-con-full elementor-hidden-desktop elementor-hidden-tablet elementor-hidden-mobile e-flex e-con e-child\" data-id=\"f8df9ed\" data-element_type=\"container\" data-e-type=\"container\">\n\t\t\t\t<div class=\"elementor-element elementor-element-b399182 elementor-absolute ekit-off-canvas-position-right elementor-invisible elementor-widget elementor-widget-elementskit-header-offcanvas\" data-id=\"b399182\" data-element_type=\"widget\" data-e-type=\"widget\" data-settings=\"{&quot;_position&quot;:&quot;absolute&quot;,&quot;_animation&quot;:&quot;fadeInDown&quot;}\" data-widget_type=\"elementskit-header-offcanvas.default\">\n\t\t\t\t\t<div class=\"ekit-wid-con\" >        <div class=\"ekit-offcanvas-toggle-wraper before\">\n            <a href=\"#\" class=\"ekit_navSidebar-button ekit_offcanvas-sidebar\" aria-label=\"offcanvas-menu\">\n                <i aria-hidden=\"true\" class=\"icon icon-options\"><\/i>            <\/a>\n        <\/div>\n        <!-- offset cart strart -->\n        <!-- sidebar cart item -->\n        <div class=\"ekit-sidebar-group info-group ekit-slide\" data-settings=\"{&quot;disable_bodyscroll&quot;:&quot;&quot;}\">\n            <div class=\"ekit-overlay ekit-bg-black\"><\/div>\n            <div class=\"ekit-sidebar-widget\">\n                <div class=\"ekit_sidebar-widget-container\">\n                    <div class=\"ekit_widget-heading before\">\n                        <a href=\"#\" class=\"ekit_close-side-widget\" aria-label=\"close-icon\">\n\n\t\t\t\t\t\t\t<i aria-hidden=\"true\" class=\"icon icon-cancel\"><\/i>\n                        <\/a>\n                    <\/div>\n                    <div class=\"ekit_sidebar-textwidget\">\n                        \n\t\t<div class=\"widgetarea_warper widgetarea_warper_editable\" data-elementskit-widgetarea-key=\"b399182\"  data-elementskit-widgetarea-index=\"99\">\n\t\t\t\n\t\t\t\n\t\t\t\t\t\t\t\t<div class=\"ekit-widget-area-container\">\n\t\t\t\t\tHaz clic en el bot\u00f3n editar contenido para editar\/a\u00f1adir el contenido.\t\t\t\t\t<\/div>\n\t\t\t\t\t\t<\/div>\n\t\t \n                    <\/div>\n                <\/div>\n            <\/div>\n        <\/div> <!-- END sidebar widget item -->\n        <!-- END offset cart strart -->\n        <\/div>\t\t\t\t<\/div>\n\t\t\t\t<\/div>\n\t\t\t\t\t<\/div>\n\t\t\t\t<\/div>\n\t\t<div class=\"elementor-element elementor-element-9cbf2a4 e-con e-atomic-element e-flexbox-base e-b564d58 \" data-id=\"9cbf2a4\" data-element_type=\"e-flexbox\" data-e-type=\"e-flexbox\" data-interaction-id=\"9cbf2a4\">\n    \t\t<div class=\"elementor-element elementor-element-702c5b3 elementor-widget elementor-widget-html\" data-id=\"702c5b3\" data-element_type=\"widget\" data-e-type=\"widget\" data-widget_type=\"html.default\">\n\t\t\t\t\t<!DOCTYPE html>\n<!-- saved from url=(0067)file:\/\/\/C:\/Users\/Usuario\/Downloads\/DISE%C3%91ANDO_EL_SONIDO_12.html -->\n<html lang=\"es\"><head><meta http-equiv=\"Content-Type\" content=\"text\/html; charset=UTF-8\">\n\n<meta name=\"viewport\" content=\"width=device-width, initial-scale=1\">\n<title>DISE\u00d1ANDO EL SONIDO \u00b7 CTE DB-HR<\/title>\n<style>\n:root{\n  --bg:#f4f6f8; --card:#ffffff; --ink:#1b2430; --muted:#6b7888; --line:#e3e8ee;\n  --brand:#0e7c7b; --brand-d:#0a5d5c; --accent:#f2a154;\n  --ok:#1e9e63; --ok-bg:#e7f6ee; --bad:#d94b46; --bad-bg:#fdecea; --warn:#b8860b; --warn-bg:#fbf3df;\n  --sans:'Segoe UI',system-ui,-apple-system,Roboto,sans-serif; --mono:ui-monospace,Menlo,Consolas,monospace;\n  --r:12px; --sh:0 1px 3px rgba(20,40,60,.08),0 6px 24px rgba(20,40,60,.06);\n}\n*{box-sizing:border-box}\nbody{margin:0;background:var(--bg);color:var(--ink);font-family:var(--sans);line-height:1.55}\na{color:var(--brand)}\n.topbar{display:flex;align-items:center;gap:1rem;background:var(--brand);color:#fff;padding:.8rem 1.4rem;position:sticky;top:0;z-index:50}\n.topbar .brand{font-weight:700;font-size:1.1rem;letter-spacing:-.01em;display:flex;align-items:center;gap:.6rem}\n.topbar .brand img{height:30px;border-radius:6px;background:#fff;padding:2px}\n.topbar nav{margin-left:auto;display:flex;gap:.4rem}\n.topbar nav button{background:rgba(255,255,255,.12);color:#fff;border:0;padding:.5rem .9rem;border-radius:8px;cursor:pointer;font:inherit;font-size:.9rem}\n.topbar nav button.active{background:#fff;color:var(--brand);font-weight:600}\n.wrap{max-width:1080px;margin:0 auto;padding:1.6rem 1.2rem 5rem}\n.card{background:var(--card);border:1px solid var(--line);border-radius:var(--r);box-shadow:var(--sh);padding:1.5rem 1.6rem;margin-bottom:1.2rem}\nh1{font-size:1.4rem;margin:.2rem 0 .3rem}\nh2{font-size:1.05rem;margin:.1rem 0 1rem}\nh3{font-size:.95rem;margin:1.2rem 0 .6rem;color:var(--brand-d)}\n.lead{color:var(--muted);margin:0 0 1.4rem;max-width:60ch}\nlabel{display:block;font-size:.82rem;font-weight:600;color:var(--ink);margin:0 0 .35rem}\n.req{color:var(--bad)}\ninput,select,textarea{width:100%;background:#fff;border:1px solid var(--line);color:var(--ink);border-radius:9px;padding:.6rem .7rem;font:inherit;font-size:.92rem}\ninput:focus,select:focus,textarea:focus{outline:none;border-color:var(--brand);box-shadow:0 0 0 3px rgba(14,124,123,.12)}\n.grid2{display:grid;grid-template-columns:1fr 1fr;gap:1rem}\n.grid3{display:grid;grid-template-columns:1fr 1fr 1fr;gap:1rem}\n@media(max-width:720px){.grid2,.grid3{grid-template-columns:1fr}}\n.fld{margin-bottom:1rem}\n.hint{font-size:.78rem;color:var(--muted);margin-top:.35rem}\n.btn{border:1px solid var(--line);background:#fff;color:var(--ink);padding:.6rem 1.1rem;border-radius:9px;font:inherit;font-size:.92rem;cursor:pointer;transition:.15s}\n.btn:hover{border-color:var(--brand)}\n.btn-primary{background:var(--brand);color:#fff;border-color:var(--brand);font-weight:600}\n.btn-primary:hover{background:var(--brand-d)}\n.btn-ghost{background:transparent;border:0;color:var(--muted)}\n.btn-sm{padding:.35rem .7rem;font-size:.82rem}\n.btn-danger{color:var(--bad);border-color:#f3c9c7}\n.actions{display:flex;justify-content:space-between;gap:1rem;margin-top:1.6rem}\n.stepper{display:flex;gap:.4rem;margin-bottom:1.4rem;flex-wrap:wrap}\n.stepper .st{flex:1;min-width:120px;padding:.55rem .7rem;border-radius:9px;background:#fff;border:1px solid var(--line);font-size:.82rem;color:var(--muted);display:flex;gap:.5rem;align-items:center}\n.stepper .st .n{width:22px;height:22px;border-radius:50%;background:var(--line);color:var(--muted);display:grid;place-items:center;font-weight:700;font-size:.78rem}\n.stepper .st.active{border-color:var(--brand);color:var(--brand-d);font-weight:600}\n.stepper .st.active .n{background:var(--brand);color:#fff}\n.stepper .st.done .n{background:var(--ok);color:#fff}\n.uploader{border:2px dashed var(--line);border-radius:11px;padding:1.6rem;text-align:center;color:var(--muted);cursor:pointer;background:#fbfcfd}\n.uploader:hover{border-color:var(--brand);color:var(--brand)}\n.thumbs{display:flex;flex-wrap:wrap;gap:.6rem;margin-top:.8rem}\n.thumb{position:relative;width:120px;height:90px;border-radius:9px;overflow:hidden;border:1px solid var(--line);background:#000}\n.thumb img{width:100%;height:100%;object-fit:cover}\n.thumb .x{position:absolute;top:3px;right:3px;background:rgba(0,0,0,.6);color:#fff;border:0;border-radius:50%;width:20px;height:20px;cursor:pointer;font-size:.8rem;line-height:1}\n.chip{display:inline-flex;align-items:center;gap:.4rem;background:#eef6f6;border:1px solid #cfe7e6;color:var(--brand-d);padding:.3rem .6rem;border-radius:999px;font-size:.8rem;margin:.2rem .3rem .2rem 0}\n.chip button{border:0;background:none;color:var(--brand-d);cursor:pointer;font-size:.9rem}\ntable{width:100%;border-collapse:collapse;font-size:.86rem}\nth,td{text-align:left;padding:.5rem .55rem;border-bottom:1px solid var(--line)}\nth{color:var(--muted);font-weight:600;font-size:.76rem;text-transform:uppercase;letter-spacing:.03em}\ntd.num,th.num{text-align:right;font-family:var(--mono)}\n.verdict{border-radius:11px;padding:1rem 1.1rem;font-weight:600;margin:1rem 0;border:1px solid}\n.v-ok{background:var(--ok-bg);border-color:#b6e2c8;color:var(--ok)}\n.v-bad{background:var(--bad-bg);border-color:#f3c9c7;color:var(--bad)}\n.v-na{background:var(--warn-bg);border-color:#ecd9a6;color:var(--warn)}\n.kpi{display:flex;gap:1.2rem;flex-wrap:wrap;margin:.4rem 0 1rem}\n.kpi .k{background:#fbfcfd;border:1px solid var(--line);border-radius:10px;padding:.7rem 1rem;min-width:120px}\n.kpi .k .v{font-size:1.5rem;font-family:var(--mono);font-weight:700}\n.kpi .k .l{font-size:.74rem;color:var(--muted)}\nsvg .grid-l{stroke:#e8edf2;stroke-width:1}\n.wallrow{display:grid;grid-template-columns:1.2fr .7fr 1.4fr auto;gap:.6rem;align-items:center;margin-bottom:.5rem}\n.tabs{display:flex;gap:.3rem;margin-bottom:1.2rem;border-bottom:1px solid var(--line)}\n.tabs button{background:none;border:0;padding:.6rem .9rem;cursor:pointer;color:var(--muted);font:inherit;border-bottom:2px solid transparent}\n.tabs button.active{color:var(--brand-d);border-color:var(--brand);font-weight:600}\n.lock{max-width:380px;margin:3rem auto;text-align:center}\n.muted{color:var(--muted)}\n.badge{font-size:.72rem;padding:.15rem .5rem;border-radius:999px;background:#eef2f6;color:var(--muted)}\n.badge.sent{background:var(--ok-bg);color:var(--ok)}\n.hide{display:none!important}\n.bar-track{background:#eef2f6;border-radius:6px;height:14px;overflow:hidden}\n.note-demo{font-size:.76rem;color:var(--warn);background:var(--warn-bg);border:1px solid #ecd9a6;border-radius:8px;padding:.5rem .7rem;margin:.6rem 0}\nfieldset{border:1px solid var(--line);border-radius:10px;padding:1rem;margin:0 0 1rem}\nlegend{font-size:.82rem;font-weight:600;color:var(--brand-d);padding:0 .4rem}\n<\/style>\n<\/head>\n<body>\n<div class=\"topbar\">\n  <div class=\"brand\" id=\"brandBox\"><span>\ud83d\udd0a DISE\u00d1ANDO EL SONIDO<\/span><\/div>\n  <nav>\n    <button id=\"navClient\" class=\"active\" onclick=\"showView(&#39;client&#39;)\">Cliente<\/button>\n    <button id=\"navInternal\" onclick=\"showView(&#39;internal&#39;)\">Zona interna \ud83d\udd12<\/button>\n  <\/nav>\n<\/div>\n\n<!-- ===================== VISTA CLIENTE (WIZARD) ===================== -->\n<div id=\"view-client\" class=\"wrap\">\n  <div class=\"stepper\" id=\"stepper\"><div class=\"st active\"><span class=\"n\">1<\/span>Tus datos<\/div><div class=\"st \"><span class=\"n\">2<\/span>Fotos y plano<\/div><div class=\"st \"><span class=\"n\">3<\/span>Tu estancia<\/div><div class=\"st \"><span class=\"n\">4<\/span>Resultado y propuesta<\/div><\/div>\n  <div id=\"steps\"><div class=\"card\">\n    <h1>Bienvenido\/a<\/h1>\n    <p class=\"lead\">Calcula en pocos minutos si tu sala cumple la normativa ac\u00fastica (CTE DB-HR) y recibe una propuesta para mejorarla. Empecemos con tus datos de contacto.<\/p>\n    <div class=\"grid2\">\n      <div class=\"fld\"><label>Nombre <span class=\"req\">*<\/span><\/label><input id=\"c_name\" value=\"\"><\/div>\n      <div class=\"fld\"><label>Apellidos <span class=\"req\">*<\/span><\/label><input id=\"c_surname\" value=\"\"><\/div>\n      <div class=\"fld\"><label>Correo electr\u00f3nico <span class=\"req\">*<\/span><\/label><input id=\"c_email\" type=\"email\" value=\"\"><\/div>\n      <div class=\"fld\"><label>C\u00f3digo postal <span class=\"req\">*<\/span><\/label><input id=\"c_cp\" inputmode=\"numeric\" maxlength=\"5\" value=\"\" oninput=\"cpInfo()\"><span class=\"hint\" id=\"cpFeedback\"><\/span><\/div>\n      <div class=\"fld\"><label>Tel\u00e9fono <span class=\"muted\">(opcional)<\/span><\/label><input id=\"c_phone\" value=\"\"><\/div>\n    <\/div>\n    <div class=\"actions\"><span><\/span>\n      <button class=\"btn btn-primary\" onclick=\"saveRegister()\">Continuar \u2192<\/button><\/div>\n  <\/div><\/div>\n<\/div>\n\n<!-- ===================== VISTA INTERNA ===================== -->\n<div id=\"view-internal\" class=\"wrap hide\"><\/div>\n\n<script>\n\/* ============================================================\n   ALMAC\u00c9N (localStorage) \u2014 guarda config y valoraciones.\n   ============================================================ *\/\nconst DB={\n  get(k,def){try{const v=localStorage.getItem(k);return v?JSON.parse(v):def;}catch(e){return def;}},\n  set(k,v){try{localStorage.setItem(k,JSON.stringify(v));}catch(e){alert('No se pudo guardar (almacenamiento lleno o bloqueado).');}},\n};\n\n\/* ============================================================\n   CAT\u00c1LOGO POR DEFECTO (editable en Configuraci\u00f3n)\n   \u03b1 por banda 125..4000 Hz. VALORES DE DEMOSTRACI\u00d3N.\n   ============================================================ *\/\n\/\/ \u2500\u2500 CAT\u00c1LOGO ACUSTIUM \u2014 datos reales de BBDD_MATERIALES_MASTER_v1 \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\n\/\/ Lista simplificada (42 materiales): uso en el calculador cliente.\n\/\/ Lista extensa (90 materiales): referencia t\u00e9cnica completa \/ zona interna.\nconst ACUSTIUM_SIMPLIFICADA=[\n  {id:\"s01\",name:\"Hormig\\u00f3n pulido \/ pintado \/ sellado\",kind:\"acabado\",app:\"suelo\",a:[0.01,0.01,0.02,0.02,0.02,0.02],aw:0.02,iso:\"Sin clase\",price:0},\n  {id:\"s02\",name:\"Hormig\\u00f3n rugoso \/ en masa sin acabado\",kind:\"acabado\",app:\"suelo\",a:[0.02,0.03,0.04,0.06,0.08,0.1],aw:0.06,iso:\"Sin clase\",price:0},\n  {id:\"s03\",name:\"Resina epoxi \/ poliuretano \/ microcemento\",kind:\"acabado\",app:\"suelo\",a:[0.01,0.01,0.02,0.02,0.02,0.02],aw:0.02,iso:\"Sin clase\",price:0},\n  {id:\"s04\",name:\"Cer\\u00e1mica \/ Porcel\\u00e1nico (baldosa)\",kind:\"acabado\",app:\"suelo\",a:[0.01,0.01,0.01,0.02,0.02,0.02],aw:0.02,iso:\"Sin clase\",price:0},\n  {id:\"s05\",name:\"M\\u00e1rmol \/ Granito \/ Piedra natural pulida\",kind:\"acabado\",app:\"suelo\",a:[0.01,0.01,0.01,0.01,0.02,0.02],aw:0.01,iso:\"Sin clase\",price:0},\n  {id:\"s06\",name:\"Parquet \/ Tarima pegada \/ Suelo laminado\",kind:\"acabado\",app:\"suelo\",a:[0.04,0.04,0.07,0.06,0.06,0.07],aw:0.06,iso:\"Sin clase\",price:0},\n  {id:\"s07\",name:\"Tarima sobre rastreles \/ Suelo t\\u00e9cnico elevado\",kind:\"acabado\",app:\"suelo\",a:[0.15,0.11,0.1,0.07,0.06,0.07],aw:0.08,iso:\"Sin clase\",price:0},\n  {id:\"s08\",name:\"Vin\\u00edlico \/ Linoleo \/ PVC \/ Caucho fino\",kind:\"acabado\",app:\"suelo\",a:[0.02,0.03,0.03,0.03,0.03,0.03],aw:0.03,iso:\"Sin clase\",price:0},\n  {id:\"s09\",name:\"Caucho \/ Goma gruesa\",kind:\"acabado\",app:\"suelo\",a:[0.04,0.04,0.08,0.12,0.08,0.1],aw:0.09,iso:\"Sin clase\",price:0},\n  {id:\"s10\",name:\"Moqueta fina \\u22646mm \/ Alfombra tipo bucle\",kind:\"acabado\",app:\"suelo\",a:[0.09,0.08,0.21,0.26,0.27,0.37],aw:0.25,iso:\"E\",price:0},\n  {id:\"s11\",name:\"Moqueta media 7-9mm sobre fieltro\",kind:\"acabado\",app:\"suelo\",a:[0.11,0.14,0.37,0.43,0.27,0.25],aw:0.36,iso:\"D\",price:0},\n  {id:\"s12\",name:\"Moqueta gruesa \\u226512mm sobre espuma\",kind:\"acabado\",app:\"suelo\",a:[0.12,0.1,0.2,0.3,0.64,0.93],aw:0.38,iso:\"D\",price:0},\n  {id:\"p01\",name:\"Hormig\\u00f3n visto (encofrado \/ rugoso)\",kind:\"acabado\",app:\"pared\",a:[0.01,0.02,0.04,0.06,0.08,0.10],aw:0.05,iso:\"Sin clase\",price:0},\n  {id:\"p19\",name:\"Hormig\\u00f3n liso \/ pulido \/ sellado\",kind:\"acabado\",app:\"pared\",a:[0.01,0.01,0.02,0.02,0.02,0.03],aw:0.02,iso:\"Sin clase\",price:0},\n  {id:\"p03\",name:\"Hormig\\u00f3n pintado\",kind:\"acabado\",app:\"pared\",a:[0.01,0.01,0.01,0.02,0.02,0.02],aw:0.02,iso:\"Sin clase\",price:0},\n  {id:\"p20\",name:\"Ladrillo cara vista (sin pintar)\",kind:\"acabado\",app:\"pared\",a:[0.03,0.03,0.03,0.04,0.05,0.07],aw:0.05,iso:\"Sin clase\",price:0},\n  {id:\"p21\",name:\"Ladrillo pintado\",kind:\"acabado\",app:\"pared\",a:[0.01,0.01,0.02,0.02,0.02,0.03],aw:0.02,iso:\"Sin clase\",price:0},\n  {id:\"p22\",name:\"Ladrillo \/ f\\u00e1brica enlucido con yeso (sin pintar)\",kind:\"acabado\",app:\"pared\",a:[0.02,0.03,0.04,0.05,0.05,0.05],aw:0.04,iso:\"Sin clase\",price:0},\n  {id:\"p02\",name:\"Enlucido de yeso pintado (pared interior est\\u00e1ndar)\",kind:\"acabado\",app:\"pared\",a:[0.01,0.02,0.02,0.03,0.04,0.04],aw:0.03,iso:\"Sin clase\",price:0},\n  {id:\"p23\",name:\"Bloque de hormig\\u00f3n visto (poroso, sin revestir)\",kind:\"acabado\",app:\"pared\",a:[0.36,0.44,0.31,0.29,0.39,0.25],aw:0.35,iso:\"D\",price:0},\n  {id:\"p04\",name:\"Alicatado \/ Porcel\\u00e1nico \/ M\\u00e1rmol \/ Vidrio cerrado\",kind:\"acabado\",app:\"pared\",a:[0.01,0.01,0.01,0.02,0.02,0.02],aw:0.02,iso:\"Sin clase\",price:0},\n  {id:\"p05\",name:\"Ventana \/ Mampara vidrio simple\",kind:\"acabado\",app:\"pared\",a:[0.035,0.04,0.027,0.03,0.02,0.02],aw:0.03,iso:\"Sin clase\",price:0},\n  {id:\"p06\",name:\"Ventana doble \/ triple acristalamiento\",kind:\"acabado\",app:\"pared\",a:[0.1,0.06,0.04,0.03,0.02,0.02],aw:0.03,iso:\"Sin clase\",price:0},\n  {id:\"p07\",name:\"Tabique PYL pintado (sin lana)\",kind:\"acabado\",app:\"pared\",a:[0.15,0.1,0.06,0.05,0.04,0.04],aw:0.05,iso:\"Sin clase\",price:0},\n  {id:\"p08\",name:\"Trasdosado PYL + lana mineral 40-50mm\",kind:\"acabado\",app:\"pared\",a:[0.35,0.25,0.2,0.15,0.1,0.08],aw:0.15,iso:\"E\",price:0},\n  {id:\"p09\",name:\"Panel PYL perforado absorbente (FON+)\",kind:\"acabado\",app:\"pared\",a:[0.3,0.4,0.55,0.6,0.55,0.45],aw:0.57,iso:\"D\",price:0},\n  {id:\"p10\",name:\"Revestimiento madera pegada \/ MDF liso\",kind:\"acabado\",app:\"pared\",a:[0.1,0.1,0.08,0.07,0.06,0.06],aw:0.07,iso:\"Sin clase\",price:0},\n  {id:\"p11\",name:\"Madera \/ contrachapado sobre c\\u00e1mara de aire\",kind:\"acabado\",app:\"pared\",a:[0.25,0.34,0.18,0.1,0.1,0.07],aw:0.13,iso:\"Sin clase\",price:0},\n  {id:\"p12\",name:\"Panel madera perforada + lana mineral\",kind:\"acabado\",app:\"pared\",a:[0.2,0.45,0.7,0.85,0.8,0.7],aw:0.78,iso:\"C\",price:0},\n  {id:\"p13\",name:\"Moqueta \/ Tela pegada en pared\",kind:\"acabado\",app:\"pared\",a:[0.09,0.08,0.21,0.27,0.27,0.37],aw:0.25,iso:\"E\",price:0},\n  {id:\"p14\",name:\"Tela fruncida \/ Revestimiento textil con c\\u00e1mara\",kind:\"acabado\",app:\"pared\",a:[0.35,0.25,0.4,0.54,0.52,0.0],aw:0.49,iso:\"D\",price:0},\n  {id:\"p15\",name:\"Cortina ligera (visillo, tela fina)\",kind:\"acabado\",app:\"pared\",a:[0.03,0.04,0.11,0.17,0.24,0.35],aw:0.17,iso:\"E\",price:0},\n  {id:\"p16\",name:\"Cortina pesada (terciopelo, tela gruesa fruncida)\",kind:\"acabado\",app:\"pared\",a:[0.14,0.35,0.55,0.72,0.7,0.0],aw:0.66,iso:\"C\",price:0},\n  {id:\"p17\",name:\"Panel absorbente mural (lana roca + tela)\",kind:\"acabado\",app:\"pared\",a:[0.35,0.62,0.88,0.92,0.78,0.84],aw:0.86,iso:\"B\",price:0},\n  {id:\"p18\",name:\"Puerta de madera (cerrada)\",kind:\"acabado\",app:\"pared\",a:[0.14,0.1,0.06,0.08,0.1,0.1],aw:0.08,iso:\"Sin clase\",price:0},\n  {id:\"t01\",name:\"Forjado hormig\\u00f3n visto\",kind:\"acabado\",app:\"techo\",a:[0.02,0.03,0.04,0.06,0.08,0.1],aw:0.06,iso:\"Sin clase\",price:0},\n  {id:\"t02\",name:\"Forjado pintado o enlucido\",kind:\"acabado\",app:\"techo\",a:[0.01,0.01,0.02,0.02,0.02,0.05],aw:0.02,iso:\"Sin clase\",price:0},\n  {id:\"t03\",name:\"Techo liso de escayola o PYL sin perforar\",kind:\"acabado\",app:\"techo\",a:[0.02,0.03,0.04,0.05,0.05,0.06],aw:0.05,iso:\"Sin clase\",price:0},\n  {id:\"t04\",name:\"Falso techo continuo PYL suspendido (sin lana)\",kind:\"acabado\",app:\"techo\",a:[0.2,0.15,0.1,0.08,0.06,0.05],aw:0.08,iso:\"Sin clase\",price:0},\n  {id:\"t05\",name:\"Falso techo continuo PYL + lana mineral 60mm\",kind:\"acabado\",app:\"techo\",a:[0.35,0.25,0.18,0.12,0.09,0.07],aw:0.13,iso:\"Sin clase\",price:0},\n  {id:\"t06\",name:\"Techo perforado PYL FON+ sin lana\",kind:\"acabado\",app:\"techo\",a:[0.4,0.7,0.8,0.7,0.65,0.6],aw:0.72,iso:\"C\",price:0},\n  {id:\"t07\",name:\"Techo perforado PYL FON+ + lana mineral 60mm\",kind:\"acabado\",app:\"techo\",a:[0.5,0.8,0.85,0.8,0.75,0.7],aw:0.8,iso:\"B\",price:0},\n  {id:\"t08\",name:\"Techo registrable fibra mineral est\\u00e1ndar\",kind:\"acabado\",app:\"techo\",a:[0.22,0.45,0.65,0.8,0.75,0.65],aw:0.73,iso:\"C\",price:0},\n  {id:\"t09\",name:\"Techo registrable fibra mineral \/ lana alta absorci\\u00f3n\",kind:\"acabado\",app:\"techo\",a:[0.3,0.55,0.75,0.9,0.85,0.75],aw:0.83,iso:\"B\",price:0},\n  {id:\"t10\",name:\"Techo registrable lana roca premium (Clase A)\",kind:\"acabado\",app:\"techo\",a:[0.5,0.7,0.85,0.95,0.9,0.8],aw:0.9,iso:\"A\",price:0},\n  {id:\"t11\",name:\"Techo desmontable madera perforada + lana\",kind:\"acabado\",app:\"techo\",a:[0.2,0.45,0.7,0.85,0.8,0.7],aw:0.78,iso:\"C\",price:0},\n  {id:\"t12\",name:\"Techo desmontable metal perforado + lana\",kind:\"acabado\",app:\"techo\",a:[0.3,0.55,0.8,0.9,0.85,0.75],aw:0.85,iso:\"B\",price:0}\n];\n\n\nconst ACUSTIUM_EXTENSA=[\n  {id:\"ext_1_s\",name:\"Hormig\\u00f3n en masa rugoso (sin acabado)\",kind:\"acabado\",app:\"suelo\",subcat:\"Hormig\\u00f3n\/Cemento\",a:[0.02,0.03,0.04,0.06,0.08,0.1],aw:0.06,iso:\"Sin clase\",price:0},\n  {id:\"ext_2_s\",name:\"Hormig\\u00f3n liso sin pintar\",kind:\"acabado\",app:\"suelo\",subcat:\"Hormig\\u00f3n\/Cemento\",a:[0.01,0.02,0.02,0.02,0.02,0.05],aw:0.02,iso:\"Sin clase\",price:0},\n  {id:\"ext_3_s\",name:\"Hormig\\u00f3n pulido sin tratar\",kind:\"acabado\",app:\"suelo\",subcat:\"Hormig\\u00f3n\/Cemento\",a:[0.01,0.01,0.01,0.02,0.02,0.02],aw:0.02,iso:\"Sin clase\",price:0},\n  {id:\"ext_4_s\",name:\"Hormig\\u00f3n pintado \/ sellado\",kind:\"acabado\",app:\"suelo\",subcat:\"Hormig\\u00f3n\/Cemento\",a:[0.01,0.01,0.02,0.02,0.02,0.02],aw:0.02,iso:\"Sin clase\",price:0},\n  {id:\"ext_5_s\",name:\"Hormig\\u00f3n impreso \/ estampado\",kind:\"acabado\",app:\"suelo\",subcat:\"Hormig\\u00f3n\/Cemento\",a:[0.01,0.01,0.02,0.02,0.02,0.03],aw:0.02,iso:\"Sin clase\",price:0},\n  {id:\"ext_6_s\",name:\"Resina epoxi continua 2-3mm\",kind:\"acabado\",app:\"suelo\",subcat:\"Resinas\",a:[0.01,0.01,0.02,0.02,0.02,0.02],aw:0.02,iso:\"Sin clase\",price:0},\n  {id:\"ext_7_s\",name:\"Resina poliuretano continua 3-4mm\",kind:\"acabado\",app:\"suelo\",subcat:\"Resinas\",a:[0.01,0.01,0.02,0.02,0.03,0.03],aw:0.02,iso:\"Sin clase\",price:0},\n  {id:\"ext_8_s\",name:\"Microcemento continuo 3-5mm\",kind:\"acabado\",app:\"suelo\",subcat:\"Resinas\",a:[0.01,0.01,0.02,0.02,0.02,0.02],aw:0.02,iso:\"Sin clase\",price:0},\n  {id:\"ext_9_s\",name:\"Pintura de suelo sobre hormig\\u00f3n\",kind:\"acabado\",app:\"suelo\",subcat:\"Resinas\",a:[0.01,0.01,0.01,0.02,0.02,0.02],aw:0.02,iso:\"Sin clase\",price:0},\n  {id:\"ext_10_s\",name:\"Baldosa cer\\u00e1mica esmaltada\",kind:\"acabado\",app:\"suelo\",subcat:\"Cer\\u00e1mico\/Porcel\\u00e1nico\",a:[0.01,0.01,0.01,0.02,0.02,0.02],aw:0.02,iso:\"Sin clase\",price:0},\n  {id:\"ext_11_s\",name:\"Porcel\\u00e1nico t\\u00e9cnico 60\\u00d760\",kind:\"acabado\",app:\"suelo\",subcat:\"Cer\\u00e1mico\/Porcel\\u00e1nico\",a:[0.01,0.01,0.01,0.01,0.02,0.02],aw:0.01,iso:\"Sin clase\",price:0},\n  {id:\"ext_12_s\",name:\"Porcel\\u00e1nico rectificado 60\\u00d7120\",kind:\"acabado\",app:\"suelo\",subcat:\"Cer\\u00e1mico\/Porcel\\u00e1nico\",a:[0.01,0.01,0.01,0.01,0.02,0.02],aw:0.01,iso:\"Sin clase\",price:0},\n  {id:\"ext_13_s\",name:\"Gres r\\u00fastico \/ antideslizante\",kind:\"acabado\",app:\"suelo\",subcat:\"Cer\\u00e1mico\/Porcel\\u00e1nico\",a:[0.01,0.01,0.02,0.02,0.02,0.03],aw:0.02,iso:\"Sin clase\",price:0},\n  {id:\"ext_14_s\",name:\"M\\u00e1rmol pulido sobre mortero\",kind:\"acabado\",app:\"suelo\",subcat:\"M\\u00e1rmol\/Piedra natural\",a:[0.01,0.01,0.01,0.01,0.02,0.02],aw:0.01,iso:\"Sin clase\",price:0},\n  {id:\"ext_15_s\",name:\"M\\u00e1rmol sin pulir (apomazado)\",kind:\"acabado\",app:\"suelo\",subcat:\"M\\u00e1rmol\/Piedra natural\",a:[0.01,0.01,0.02,0.02,0.03,0.03],aw:0.02,iso:\"Sin clase\",price:0},\n  {id:\"ext_16_s\",name:\"Caliza \/ travertino pulido\",kind:\"acabado\",app:\"suelo\",subcat:\"M\\u00e1rmol\/Piedra natural\",a:[0.01,0.01,0.01,0.02,0.02,0.02],aw:0.02,iso:\"Sin clase\",price:0},\n  {id:\"ext_17_s\",name:\"Granito pulido sobre mortero\",kind:\"acabado\",app:\"suelo\",subcat:\"M\\u00e1rmol\/Piedra natural\",a:[0.01,0.01,0.01,0.01,0.02,0.02],aw:0.01,iso:\"Sin clase\",price:0},\n  {id:\"ext_18_s\",name:\"Pizarra natural sobre mortero\",kind:\"acabado\",app:\"suelo\",subcat:\"M\\u00e1rmol\/Piedra natural\",a:[0.01,0.01,0.02,0.02,0.02,0.03],aw:0.02,iso:\"Sin clase\",price:0},\n  {id:\"ext_19_s\",name:\"Terrazo pulido in situ\",kind:\"acabado\",app:\"suelo\",subcat:\"Terrazo\",a:[0.01,0.01,0.01,0.02,0.02,0.02],aw:0.02,iso:\"Sin clase\",price:0},\n  {id:\"ext_20_s\",name:\"Baldosa de terrazo prefabricada\",kind:\"acabado\",app:\"suelo\",subcat:\"Terrazo\",a:[0.01,0.01,0.01,0.02,0.02,0.02],aw:0.02,iso:\"Sin clase\",price:0},\n  {id:\"ext_21_s\",name:\"Tarima maciza pegada sobre forjado\",kind:\"acabado\",app:\"suelo\",subcat:\"Madera pegada\",a:[0.04,0.04,0.07,0.06,0.06,0.07],aw:0.06,iso:\"Sin clase\",price:0},\n  {id:\"ext_22_s\",name:\"Tarima flotante laminada HDF\",kind:\"acabado\",app:\"suelo\",subcat:\"Madera pegada\",a:[0.04,0.04,0.06,0.06,0.07,0.07],aw:0.06,iso:\"Sin clase\",price:0},\n  {id:\"ext_23_s\",name:\"Parquet multicapa encolado\",kind:\"acabado\",app:\"suelo\",subcat:\"Madera pegada\",a:[0.05,0.04,0.07,0.06,0.06,0.07],aw:0.06,iso:\"Sin clase\",price:0},\n  {id:\"ext_24_s\",name:\"Tarima maciza sobre rastreles (c\\u00e1mara aire)\",kind:\"acabado\",app:\"suelo\",subcat:\"Madera entramado\",a:[0.15,0.11,0.1,0.07,0.06,0.07],aw:0.08,iso:\"Sin clase\",price:0},\n  {id:\"ext_25_s\",name:\"Parquet sobre listones de madera\",kind:\"acabado\",app:\"suelo\",subcat:\"Madera entramado\",a:[0.2,0.15,0.12,0.1,0.1,0.07],aw:0.11,iso:\"Sin clase\",price:0},\n  {id:\"ext_26_s\",name:\"Suelo t\\u00e9cnico elevado (panel 60\\u00d760)\",kind:\"acabado\",app:\"suelo\",subcat:\"Madera entramado\",a:[0.1,0.08,0.07,0.06,0.06,0.06],aw:0.06,iso:\"Sin clase\",price:0},\n  {id:\"ext_27_s\",name:\"Suelo vin\\u00edlico LVT pegado 2,5mm\",kind:\"acabado\",app:\"suelo\",subcat:\"Laminado\/Vin\\u00edlico\",a:[0.02,0.03,0.03,0.03,0.03,0.02],aw:0.03,iso:\"Sin clase\",price:0},\n  {id:\"ext_28_s\",name:\"Suelo laminado flotante 8mm\",kind:\"acabado\",app:\"suelo\",subcat:\"Laminado\/Vin\\u00edlico\",a:[0.02,0.03,0.05,0.05,0.06,0.06],aw:0.05,iso:\"Sin clase\",price:0},\n  {id:\"ext_29_s\",name:\"Linoleo natural 3,2mm\",kind:\"acabado\",app:\"suelo\",subcat:\"Laminado\/Vin\\u00edlico\",a:[0.02,0.025,0.03,0.035,0.04,0.04],aw:0.04,iso:\"Sin clase\",price:0},\n  {id:\"ext_30_s\",name:\"Caucho vulcanizado en rollo 4mm\",kind:\"acabado\",app:\"suelo\",subcat:\"Caucho\/Goma\",a:[0.04,0.04,0.08,0.12,0.08,0.1],aw:0.09,iso:\"Sin clase\",price:0},\n  {id:\"ext_31_s\",name:\"Pavimento goma espumosa sobre cemento\",kind:\"acabado\",app:\"suelo\",subcat:\"Caucho\/Goma\",a:[0.08,0.0,0.35,0.0,0.6,0.0],aw:0.47,iso:\"D\",price:0},\n  {id:\"ext_32_s\",name:\"Moqueta fina (\\u22646mm) sobre losa\",kind:\"acabado\",app:\"suelo\",subcat:\"Moqueta\",a:[0.09,0.08,0.21,0.26,0.27,0.37],aw:0.25,iso:\"E\",price:0},\n  {id:\"ext_33_s\",name:\"Moqueta media (7-9mm) sobre fieltro\",kind:\"acabado\",app:\"suelo\",subcat:\"Moqueta\",a:[0.11,0.14,0.37,0.43,0.27,0.25],aw:0.36,iso:\"D\",price:0},\n  {id:\"ext_34_s\",name:\"Moqueta gruesa (\\u226512mm) sobre espuma\",kind:\"acabado\",app:\"suelo\",subcat:\"Moqueta\",a:[0.12,0.1,0.2,0.3,0.64,0.93],aw:0.38,iso:\"D\",price:0},\n  {id:\"ext_35_s\",name:\"Moqueta en baldosa 50\\u00d750 sobre losa\",kind:\"acabado\",app:\"suelo\",subcat:\"Moqueta\",a:[0.09,0.08,0.21,0.26,0.27,0.37],aw:0.25,iso:\"E\",price:0},\n  {id:\"ext_36_s\",name:\"Suelo de corcho natural 4mm encolado\",kind:\"acabado\",app:\"suelo\",subcat:\"Corcho\/Bamb\\u00fa\",a:[0.08,0.02,0.08,0.19,0.21,0.22],aw:0.16,iso:\"E\",price:0},\n  {id:\"ext_37_s\",name:\"Alfombra de \\u00e1rea \/ tatami\",kind:\"acabado\",app:\"suelo\",subcat:\"Alfombras\/Textil\",a:[0.09,0.1,0.22,0.3,0.35,0.4],aw:0.29,iso:\"E\",price:0},\n  {id:\"ext_1_p\",name:\"Hormig\\u00f3n en masa sin acabado (rugoso)\",kind:\"acabado\",app:\"pared\",subcat:\"Hormig\\u00f3n\",a:[0.02,0.03,0.03,0.03,0.04,0.07],aw:0.03,iso:\"Sin clase\",price:0},\n  {id:\"ext_2_p\",name:\"Hormig\\u00f3n liso sin pintar\",kind:\"acabado\",app:\"pared\",subcat:\"Hormig\\u00f3n\",a:[0.01,0.01,0.02,0.02,0.02,0.05],aw:0.02,iso:\"Sin clase\",price:0},\n  {id:\"ext_3_p\",name:\"Hormig\\u00f3n pintado\",kind:\"acabado\",app:\"pared\",subcat:\"Hormig\\u00f3n\",a:[0.01,0.01,0.01,0.02,0.02,0.02],aw:0.02,iso:\"Sin clase\",price:0},\n  {id:\"ext_4_p\",name:\"Hormig\\u00f3n enlucido con yeso + pintado\",kind:\"acabado\",app:\"pared\",subcat:\"Hormig\\u00f3n enlucido\",a:[0.013,0.015,0.02,0.028,0.04,0.05],aw:0.03,iso:\"Sin clase\",price:0},\n  {id:\"ext_5_p\",name:\"Hormig\\u00f3n enfoscado + pintado\",kind:\"acabado\",app:\"pared\",subcat:\"Hormig\\u00f3n enlucido\",a:[0.01,0.01,0.02,0.02,0.02,0.02],aw:0.02,iso:\"Sin clase\",price:0},\n  {id:\"ext_6_p\",name:\"Hormig\\u00f3n enlucido cemento rugoso\",kind:\"acabado\",app:\"pared\",subcat:\"Hormig\\u00f3n enlucido\",a:[0.025,0.026,0.06,0.085,0.043,0.056],aw:0.06,iso:\"Sin clase\",price:0},\n  {id:\"ext_7_p\",name:\"Ladrillo cara vista sin enlucir\",kind:\"acabado\",app:\"pared\",subcat:\"Ladrillo\",a:[0.024,0.025,0.032,0.042,0.05,0.07],aw:0.04,iso:\"Sin clase\",price:0},\n  {id:\"ext_8_p\",name:\"Ladrillo hueco doble 7cm sin enlucir\",kind:\"acabado\",app:\"pared\",subcat:\"Ladrillo\",a:[0.03,0.03,0.04,0.05,0.05,0.07],aw:0.05,iso:\"Sin clase\",price:0},\n  {id:\"ext_9_p\",name:\"Ladrillo pintado\",kind:\"acabado\",app:\"pared\",subcat:\"Ladrillo\",a:[0.012,0.014,0.017,0.02,0.023,0.025],aw:0.02,iso:\"Sin clase\",price:0},\n  {id:\"ext_10_p\",name:\"Ladrillo enlucido con yeso + pintado\",kind:\"acabado\",app:\"pared\",subcat:\"Ladrillo enlucido\",a:[0.013,0.015,0.02,0.028,0.04,0.05],aw:0.03,iso:\"Sin clase\",price:0},\n  {id:\"ext_11_p\",name:\"Ladrillo enfoscado revoco cal-arena\",kind:\"acabado\",app:\"pared\",subcat:\"Ladrillo enlucido\",a:[0.04,0.05,0.06,0.08,0.04,0.06],aw:0.06,iso:\"Sin clase\",price:0},\n  {id:\"ext_12_p\",name:\"Bloque hormig\\u00f3n macizo sin acabado\",kind:\"acabado\",app:\"pared\",subcat:\"Bloque hormig\\u00f3n\",a:[0.3,0.45,0.3,0.25,0.4,0.25],aw:0.32,iso:\"D\",price:0},\n  {id:\"ext_13_p\",name:\"Bloque hormig\\u00f3n pintado\",kind:\"acabado\",app:\"pared\",subcat:\"Bloque hormig\\u00f3n\",a:[0.1,0.09,0.08,0.09,0.1,0.0],aw:0.09,iso:\"Sin clase\",price:0},\n  {id:\"ext_14_p\",name:\"PYL 12,5mm pintado (tabique est\\u00e1ndar)\",kind:\"acabado\",app:\"pared\",subcat:\"PYL \\u2013 Tabique\/Trasdosado\",a:[0.15,0.1,0.06,0.05,0.04,0.04],aw:0.05,iso:\"Sin clase\",price:0},\n  {id:\"ext_15_p\",name:\"PYL 12,5mm + c\\u00e1mara 48mm sin lana\",kind:\"acabado\",app:\"pared\",subcat:\"PYL \\u2013 Tabique\/Trasdosado\",a:[0.25,0.15,0.08,0.06,0.05,0.05],aw:0.06,iso:\"Sin clase\",price:0},\n  {id:\"ext_16_p\",name:\"PYL 12,5mm + lana mineral 40mm\",kind:\"acabado\",app:\"pared\",subcat:\"PYL \\u2013 Tabique\/Trasdosado\",a:[0.35,0.25,0.2,0.15,0.1,0.08],aw:0.15,iso:\"E\",price:0},\n  {id:\"ext_17_p\",name:\"PYL 2\\u00d712,5mm + lana mineral 50mm\",kind:\"acabado\",app:\"pared\",subcat:\"PYL \\u2013 Tabique\/Trasdosado\",a:[0.3,0.2,0.15,0.1,0.08,0.06],aw:0.11,iso:\"Sin clase\",price:0},\n  {id:\"ext_18_p\",name:\"PYL perforado FON+ (panel absorbente)\",kind:\"acabado\",app:\"pared\",subcat:\"PYL \\u2013 Tabique\/Trasdosado\",a:[0.3,0.4,0.55,0.6,0.55,0.45],aw:0.57,iso:\"D\",price:0},\n  {id:\"ext_19_p\",name:\"Tabique cer\\u00e1mico 7cm enlucido\",kind:\"acabado\",app:\"pared\",subcat:\"Tabiquer\\u00eda\/Mamparas\",a:[0.02,0.02,0.03,0.04,0.04,0.05],aw:0.04,iso:\"Sin clase\",price:0},\n  {id:\"ext_20_p\",name:\"Mampara vidrio simple 6mm\",kind:\"acabado\",app:\"pared\",subcat:\"Tabiquer\\u00eda\/Mamparas\",a:[0.035,0.04,0.027,0.03,0.02,0.02],aw:0.03,iso:\"Sin clase\",price:0},\n  {id:\"ext_21_p\",name:\"Mampara vidrio doble acristalamiento\",kind:\"acabado\",app:\"pared\",subcat:\"Tabiquer\\u00eda\/Mamparas\",a:[0.1,0.06,0.04,0.03,0.02,0.02],aw:0.03,iso:\"Sin clase\",price:0},\n  {id:\"ext_22_p\",name:\"Azulejo cer\\u00e1mico esmaltado\",kind:\"acabado\",app:\"pared\",subcat:\"Alicatado\",a:[0.01,0.01,0.01,0.02,0.02,0.02],aw:0.02,iso:\"Sin clase\",price:0},\n  {id:\"ext_23_p\",name:\"Porcel\\u00e1nico gran formato 60\\u00d7120\",kind:\"acabado\",app:\"pared\",subcat:\"Alicatado\",a:[0.01,0.01,0.01,0.01,0.02,0.02],aw:0.01,iso:\"Sin clase\",price:0},\n  {id:\"ext_24_p\",name:\"Pintura pl\\u00e1stica mate sobre yeso\",kind:\"acabado\",app:\"pared\",subcat:\"Pintura\/Enlucido\",a:[0.013,0.015,0.02,0.028,0.04,0.05],aw:0.03,iso:\"Sin clase\",price:0},\n  {id:\"ext_25_p\",name:\"Microcemento en pared 3mm\",kind:\"acabado\",app:\"pared\",subcat:\"Pintura\/Enlucido\",a:[0.01,0.01,0.02,0.02,0.02,0.02],aw:0.02,iso:\"Sin clase\",price:0},\n  {id:\"ext_26_p\",name:\"Estuco veneciano sobre yeso\",kind:\"acabado\",app:\"pared\",subcat:\"Pintura\/Enlucido\",a:[0.02,0.02,0.02,0.02,0.03,0.03],aw:0.02,iso:\"Sin clase\",price:0},\n  {id:\"ext_27_p\",name:\"Papel pintado vin\\u00edlico sobre yeso\",kind:\"acabado\",app:\"pared\",subcat:\"Empapelado\/Textil\",a:[0.02,0.02,0.03,0.04,0.04,0.05],aw:0.04,iso:\"Sin clase\",price:0},\n  {id:\"ext_28_p\",name:\"Tela \/ lino pegado en pared\",kind:\"acabado\",app:\"pared\",subcat:\"Empapelado\/Textil\",a:[0.05,0.08,0.12,0.22,0.32,0.0],aw:0.22,iso:\"E\",price:0},\n  {id:\"ext_29_p\",name:\"Tela fruncida a 2cm de pared\",kind:\"acabado\",app:\"pared\",subcat:\"Empapelado\/Textil\",a:[0.5,0.22,0.4,0.54,0.52,0.0],aw:0.49,iso:\"D\",price:0},\n  {id:\"ext_30_p\",name:\"Moqueta pegada en pared\",kind:\"acabado\",app:\"pared\",subcat:\"Moqueta en pared\",a:[0.09,0.08,0.21,0.27,0.27,0.37],aw:0.25,iso:\"E\",price:0},\n  {id:\"ext_31_p\",name:\"Revestimiento madera pegada \/ MDF liso\",kind:\"acabado\",app:\"pared\",subcat:\"Madera en pared\",a:[0.1,0.1,0.08,0.07,0.06,0.06],aw:0.07,iso:\"Sin clase\",price:0},\n  {id:\"ext_32_p\",name:\"Lambriz \/ madera sobre c\\u00e1mara 5cm\",kind:\"acabado\",app:\"pared\",subcat:\"Madera en pared\",a:[0.25,0.34,0.18,0.1,0.1,0.07],aw:0.13,iso:\"Sin clase\",price:0},\n  {id:\"ext_33_p\",name:\"Panel MDF perforado + lana mineral\",kind:\"acabado\",app:\"pared\",subcat:\"Madera en pared\",a:[0.2,0.45,0.7,0.85,0.8,0.7],aw:0.78,iso:\"C\",price:0},\n  {id:\"ext_34_p\",name:\"M\\u00e1rmol \/ granito pulido en pared\",kind:\"acabado\",app:\"pared\",subcat:\"Piedra natural\",a:[0.01,0.01,0.01,0.02,0.02,0.01],aw:0.02,iso:\"Sin clase\",price:0},\n  {id:\"ext_35_p\",name:\"Cortina ligera (visillo, tela fina)\",kind:\"acabado\",app:\"pared\",subcat:\"Cortinas\/Paneles\",a:[0.03,0.04,0.11,0.17,0.24,0.35],aw:0.17,iso:\"E\",price:0},\n  {id:\"ext_36_p\",name:\"Cortina pesada (terciopelo fruncido)\",kind:\"acabado\",app:\"pared\",subcat:\"Cortinas\/Paneles\",a:[0.14,0.35,0.55,0.72,0.7,0.0],aw:0.66,iso:\"C\",price:0},\n  {id:\"ext_37_p\",name:\"Panel absorbente mural lana de roca 40mm\",kind:\"acabado\",app:\"pared\",subcat:\"Cortinas\/Paneles\",a:[0.35,0.62,0.88,0.92,0.78,0.84],aw:0.86,iso:\"B\",price:0},\n  {id:\"ext_38_p\",name:\"Ventana vidrio simple 4mm\",kind:\"acabado\",app:\"pared\",subcat:\"Puertas\/Ventanas\",a:[0.035,0.04,0.027,0.03,0.02,0.02],aw:0.03,iso:\"Sin clase\",price:0},\n  {id:\"ext_39_p\",name:\"Ventana doble acristalamiento 4-16-4\",kind:\"acabado\",app:\"pared\",subcat:\"Puertas\/Ventanas\",a:[0.1,0.06,0.04,0.03,0.02,0.02],aw:0.03,iso:\"Sin clase\",price:0},\n  {id:\"ext_40_p\",name:\"Puerta de madera (cerrada)\",kind:\"acabado\",app:\"pared\",subcat:\"Puertas\/Ventanas\",a:[0.14,0.1,0.06,0.08,0.1,0.1],aw:0.08,iso:\"Sin clase\",price:0},\n  {id:\"ext_1_t\",name:\"Forjado hormig\\u00f3n visto sin acabado\",kind:\"acabado\",app:\"techo\",subcat:\"Forjado\/Hormig\\u00f3n\",a:[0.02,0.03,0.04,0.06,0.08,0.1],aw:0.06,iso:\"Sin clase\",price:0},\n  {id:\"ext_2_t\",name:\"Forjado pintado \/ enlucido\",kind:\"acabado\",app:\"techo\",subcat:\"Forjado\/Hormig\\u00f3n\",a:[0.01,0.01,0.02,0.02,0.02,0.05],aw:0.02,iso:\"Sin clase\",price:0},\n  {id:\"ext_3_t\",name:\"Techo escayola \/ PYL liso pintado\",kind:\"acabado\",app:\"techo\",subcat:\"Escayola\/PYL liso\",a:[0.02,0.03,0.04,0.05,0.05,0.06],aw:0.05,iso:\"Sin clase\",price:0},\n  {id:\"ext_4_t\",name:\"Techo continuo PYL 12,5mm suspendido sin lana\",kind:\"acabado\",app:\"techo\",subcat:\"PYL continuo\",a:[0.2,0.15,0.1,0.08,0.06,0.05],aw:0.08,iso:\"Sin clase\",price:0},\n  {id:\"ext_5_t\",name:\"Techo continuo PYL + lana mineral 60mm\",kind:\"acabado\",app:\"techo\",subcat:\"PYL + lana\",a:[0.35,0.25,0.18,0.12,0.09,0.07],aw:0.13,iso:\"Sin clase\",price:0},\n  {id:\"ext_6_t\",name:\"Techo continuo PYL doble + lana 60mm\",kind:\"acabado\",app:\"techo\",subcat:\"PYL + lana\",a:[0.3,0.2,0.15,0.1,0.08,0.06],aw:0.11,iso:\"Sin clase\",price:0},\n  {id:\"ext_7_t\",name:\"Techo FON+ continuo perforado sin lana\",kind:\"acabado\",app:\"techo\",subcat:\"FON+ continuo\",a:[0.4,0.7,0.8,0.7,0.65,0.6],aw:0.72,iso:\"C\",price:0},\n  {id:\"ext_8_t\",name:\"Techo FON+ continuo perforado + lana 60mm\",kind:\"acabado\",app:\"techo\",subcat:\"FON+ + lana\",a:[0.5,0.8,0.85,0.8,0.75,0.7],aw:0.8,iso:\"B\",price:0},\n  {id:\"ext_9_t\",name:\"Techo registrable fibra mineral 15mm (NRC 0,55)\",kind:\"acabado\",app:\"techo\",subcat:\"Registrable fibra mineral\",a:[0.22,0.45,0.65,0.8,0.75,0.65],aw:0.73,iso:\"C\",price:0},\n  {id:\"ext_10_t\",name:\"Techo registrable fibra mineral 20mm alta absorci\\u00f3n\",kind:\"acabado\",app:\"techo\",subcat:\"Registrable fibra mineral\",a:[0.3,0.55,0.75,0.9,0.85,0.75],aw:0.83,iso:\"B\",price:0},\n  {id:\"ext_11_t\",name:\"Techo registrable lana roca 40mm premium (Clase A)\",kind:\"acabado\",app:\"techo\",subcat:\"Registrable lana roca\",a:[0.55,0.8,0.95,1.0,0.95,0.9],aw:0.97,iso:\"A\",price:0},\n  {id:\"ext_12_t\",name:\"Techo desmontable metal perforado + lana 50mm\",kind:\"acabado\",app:\"techo\",subcat:\"Metal perforado\",a:[0.25,0.5,0.8,0.9,0.85,0.75],aw:0.85,iso:\"B\",price:0},\n  {id:\"ext_13_t\",name:\"Techo desmontable madera perforada MDF + lana\",kind:\"acabado\",app:\"techo\",subcat:\"Madera perforada\",a:[0.2,0.45,0.7,0.85,0.8,0.7],aw:0.78,iso:\"C\",price:0}\n];\n\n\n\/\/ El cat\u00e1logo activo del calculador usa la simplificada como base.\n\/\/ Los sistemas de tratamiento ac\u00fastico se a\u00f1aden desde la zona interna.\nconst DEFAULT_MATERIALS=ACUSTIUM_SIMPLIFICADA;\n\/\/ Categor\u00edas de soluci\u00f3n\/producto (definidas por la soluci\u00f3n comercial)\nconst SYSTEM_CATEGORIES=['Falso techo modular','Baffles','Islas','Panel mural','Arquitectura textil','Proyectado','Cuadros ac\u00fasticos','Otros'];\nconst DEFAULT_SYSTEMS=[\n  {id:'s_t10', name:'Falso techo modular lana roca premium (Clase A)', materialId:'t10', cat:'Falso techo modular', surf:'techo'},\n  {id:'s_t09', name:'Falso techo modular fibra mineral alta absorci\u00f3n (Clase B)', materialId:'t09', cat:'Falso techo modular', surf:'techo'},\n  {id:'s_t12', name:'Falso techo metal perforado + lana (Clase B)', materialId:'t12', cat:'Falso techo modular', surf:'techo'},\n  {id:'s_baffle', name:'Baffles ac\u00fasticos suspendidos', materialId:'t10', cat:'Baffles', surf:'techo'},\n  {id:'s_isla', name:'Islas ac\u00fasticas suspendidas', materialId:'t09', cat:'Islas', surf:'techo'},\n  {id:'s_p17', name:'Panel absorbente mural lana roca 40mm (Clase B)', materialId:'p17', cat:'Panel mural', surf:'pared'},\n  {id:'s_textil', name:'Arquitectura textil (tela tensada + absorbente)', materialId:'p16', cat:'Arquitectura textil', surf:'pared'},\n  {id:'s_proy', name:'Proyectado ac\u00fastico sobre soporte', materialId:'p12', cat:'Proyectado', surf:'pared'},\n  {id:'s_cuadro', name:'Cuadros ac\u00fasticos decorativos', materialId:'p17', cat:'Cuadros ac\u00fasticos', surf:'pared'},\n];\n\/\/ Objetos de ocupaci\u00f3n (\u00e1rea de absorci\u00f3n equivalente A en m\u00b2 por unidad, por banda 125-4000)\nconst OCC_ITEMS=[\n  {id:'persona', name:'Persona (sentada\/de pie)', a:[0.15,0.25,0.35,0.42,0.46,0.50]},\n  {id:'butaca_oc', name:'Butaca tapizada ocupada', a:[0.30,0.40,0.45,0.48,0.50,0.50]},\n  {id:'butaca_vac', name:'Butaca tapizada vac\u00eda', a:[0.20,0.28,0.31,0.35,0.37,0.38]},\n  {id:'silla', name:'Silla madera\/pl\u00e1stico', a:[0.02,0.02,0.02,0.04,0.04,0.03]},\n  {id:'mesa', name:'Mesa de trabajo', a:[0.01,0.01,0.02,0.02,0.02,0.02]},\n  {id:'armario', name:'Armario \/ estanter\u00eda', a:[0.10,0.12,0.15,0.18,0.20,0.20]},\n];\n\/\/ Mapeo tipo de elemento singular \u2192 material por defecto del cat\u00e1logo (el tipo define el material)\nconst ELEM_TYPE_MAT={\n  'Ventana simple':'p05','Ventana doble acristalamiento':'p06','Puerta de madera':'p18','Puerta met\u00e1lica':'p04',\n  'Cuadro \/ Panel decorativo':'p17','Cortina ligera':'p15','Cortina pesada (terciopelo)':'p16',\n  'Armario \/ Estanter\u00eda':'p10','Pizarra \/ Pantalla':'p04','Muebles tapizados':'p13','Otros':'p02',\n};\nfunction materials(){return DB.get('cfg.materials',DEFAULT_MATERIALS);}\nfunction systems(){return DB.get('cfg.systems',DEFAULT_SYSTEMS);}\nfunction matById(id){return materials().find(m=>m.id===id);}\n\/\/ Materiales aplicables a una superficie ('techo'|'suelo'|'pared'). app ausente = 'todas'.\nfunction matsFor(zone){return materials().filter(m=>{const a=m.app||'todas';return a==='todas'||a===zone;});}\nfunction cfg(k,def){return DB.get('cfg.'+k,def);}\n\n\/* ============================================================\n   MOTOR DE C\u00c1LCULO (portado del n\u00facleo DB-HR ya validado)\n   ============================================================ *\/\nconst BANDS=[125,250,500,1000,2000,4000];\nconst REVERB=[500,1000,2000];\nconst AIR_M={125:0.0001,250:0.0003,500:0.0006,1000:0.0010,2000:0.0019,4000:0.0058};\nconst K_SABINE=0.16; \/\/ DB-HR vigente\n\n\/\/ Tipos de estancia organizados por sector\nconst USE_GROUPS=[\n  {sector:'Hoteles',items:[\n    {id:'HOT_REST',label:'Restaurantes y comedores',dbr:'RESTAURANTE'},\n    {id:'HOT_REUN',label:'Salas de reuniones \/ eventos',dbr:'SALA_CONFERENCIAS'},\n    {id:'HOT_GYM',label:'Gimnasios internos',dbr:'ZONA_COMUN'},\n    {id:'HOT_SPA',label:'Zonas spa \/ wellness',dbr:'ZONA_COMUN'},\n    {id:'HOT_TERR',label:'Terrazas cerradas',dbr:'ZONA_COMUN'},\n  ]},\n  {sector:'Educaci\u00f3n',items:[\n    {id:'EDU_AULA',label:'Aulas',dbr:'AULA'},\n    {id:'EDU_COM',label:'Comedores',dbr:'COMEDOR'},\n    {id:'EDU_MULTI',label:'Salas multiuso',dbr:'SALA_CONFERENCIAS'},\n    {id:'EDU_DESC',label:'Zonas de descanso',dbr:'ZONA_COMUN'},\n    {id:'EDU_BIB',label:'Biblioteca',dbr:'AULA'},\n  ]},\n  {sector:'Salud',items:[\n    {id:'SAL_ESPERA',label:'Salas de espera \/ zonas de descanso',dbr:'ZONA_COMUN'},\n    {id:'SAL_HAB',label:'Habitaciones',dbr:'ZONA_COMUN'},\n    {id:'SAL_QUIRO',label:'Quir\u00f3fanos',dbr:'ZONA_COMUN'},\n    {id:'SAL_BOX',label:'Boxes de tratamiento',dbr:'ZONA_COMUN'},\n  ]},\n  {sector:'Instalaciones deportivas',items:[\n    {id:'DEP_ALTURA',label:'Techos de gran altura (cr\u00edtico)',dbr:'ZONA_COMUN'},\n    {id:'DEP_PISC',label:'Zona de piscinas',dbr:'ZONA_COMUN'},\n    {id:'DEP_HUM',label:'Techos altos con humedad',dbr:'ZONA_COMUN'},\n    {id:'DEP_ACT',label:'Salas de actividades dirigidas',dbr:'ZONA_COMUN'},\n    {id:'DEP_MAQ',label:'Zona de m\u00e1quinas',dbr:'ZONA_COMUN'},\n  ]},\n  {sector:'Comercio',items:[\n    {id:'COM_VENTAS',label:'Sala de ventas',dbr:'ZONA_COMUN'},\n    {id:'COM_VEHIC',label:'Exposici\u00f3n de veh\u00edculos',dbr:'ZONA_COMUN'},\n    {id:'COM_TIENDA',label:'Zonas comerciales \/ tiendas',dbr:'ZONA_COMUN'},\n  ]},\n  {sector:'Oficinas',items:[\n    {id:'OFI_OPEN',label:'Zonas de trabajo \/ open space',dbr:'ZONA_COMUN'},\n    {id:'OFI_REUN',label:'Salas de reuniones',dbr:'SALA_CONFERENCIAS'},\n    {id:'OFI_DESP',label:'Despachos',dbr:'AULA'},\n    {id:'OFI_CALL',label:'\u00c1rea de operadores (call centers)',dbr:'ZONA_COMUN'},\n  ]},\n  {sector:'Ocio \/ Cultura',items:[\n    {id:'OCI_EXPO',label:'Salas de exposici\u00f3n',dbr:'ZONA_COMUN'},\n    {id:'OCI_BIB',label:'Bibliotecas \/ salas de lectura',dbr:'AULA'},\n    {id:'OCI_AUD',label:'Auditorios',dbr:'SALA_CONFERENCIAS'},\n    {id:'OCI_ESC',label:'Escenario',dbr:'SALA_CONFERENCIAS'},\n    {id:'OCI_PARAM',label:'Techos y paramentos ac\u00fasticos espec\u00edficos',dbr:'SALA_CONFERENCIAS'},\n    {id:'OCI_GRAB',label:'Salas de grabaci\u00f3n',dbr:'SALA_CONFERENCIAS'},\n  ]},\n  {sector:'Industria',items:[\n    {id:'IND_NAVE',label:'Naves de producci\u00f3n',dbr:'ZONA_COMUN'},\n    {id:'IND_LINEA',label:'L\u00edneas de trabajo',dbr:'ZONA_COMUN'},\n  ]},\n  {sector:'Particulares',items:[\n    {id:'PAR_SALON',label:'Salones',dbr:'ZONA_COMUN'},\n    {id:'PAR_HOME',label:'Home office',dbr:'AULA'},\n    {id:'PAR_CINE',label:'Salas de cine en casa',dbr:'SALA_CONFERENCIAS'},\n    {id:'PAR_MUSICA',label:'Estudios de m\u00fasica',dbr:'SALA_CONFERENCIAS'},\n  ]},\n  {sector:'Otros',items:[\n    {id:'OTRO',label:'Personalizado (describir y clasificar con IA)',dbr:'ZONA_COMUN'},\n  ]},\n];\n\/\/ Mapeo tipo\u2192categor\u00eda DB-HR (se construye desde USE_GROUPS; los tipos IA se a\u00f1aden en runtime)\nconst ROOM_TO_DBR={};\nUSE_GROUPS.forEach(g=>g.items.forEach(it=>{ROOM_TO_DBR[it.id]=it.dbr||'ZONA_COMUN';}));\nfunction dbrUse(use){return ROOM_TO_DBR[use]||'ZONA_COMUN';}\n\/\/ Etiqueta amistosa de la categor\u00eda DB-HR (para el sector Otros \/ IA)\nfunction dbrLabel(c){return {AULA:'Aula \/ espacio de palabra (RT \u2264 0,7 s)',SALA_CONFERENCIAS:'Sala de conferencias (RT \u2264 0,7 s)',COMEDOR:'Comedor (RT \u2264 0,9 s)',RESTAURANTE:'Restaurante (RT \u2264 0,9 s)',ZONA_COMUN:'Zona com\u00fan \u2014 criterio por absorci\u00f3n (A \u2265 0,2\u00b7V)'}[c]||c;}\n\/\/ Heur\u00edstica sin IA para clasificar una descripci\u00f3n libre\nfunction heuristicDbr(n){n=(n||'').toLowerCase();\n  if(\/comedor|cocina\/.test(n))return 'COMEDOR';\n  if(\/restaurant|bar|cafeter|comida\/.test(n))return 'RESTAURANTE';\n  if(\/conferenc|auditor|teatro|sala de actos|reuni|cine|grabaci|m[u\u00fa]sic|escenario|concierto\/.test(n))return 'SALA_CONFERENCIAS';\n  if(\/aula|clase|estudio|despacho|consulta|lectura|biblioteca|home\\s*office|formaci\/.test(n))return 'AULA';\n  return 'ZONA_COMUN';}\n\/\/ Label de display para cualquier ID\nfunction useLabel(id){for(const g of USE_GROUPS)for(const it of g.items)if(it.id===id)return it.label;return id;}\n\/\/ Opciones agrupadas para el select\nfunction useGroupOptions(sel){return USE_GROUPS.map(g=>`<optgroup label=\"${g.sector}\">${g.items.map(it=>`<option value=\"${it.id}\" ${it.id===sel?'selected':''}>${it.label}<\/option>`).join('')}<\/optgroup>`).join('');}\n\n\/* \u2500\u2500 ISO 11654 \u2014 Clases de absorci\u00f3n ac\u00fastica \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500 *\/\nconst ISO_CLASSES=[\n  {clase:'A',label:'Muy alta absorci\u00f3n', rango:'\u03b1w \u2265 0,90', min:0.90,uso:'Estudios, salas conferencias, oficinas premium',color:'#00B050'},\n  {clase:'B',label:'Alta absorci\u00f3n',      rango:'0,80\u20130,89', min:0.80,uso:'Open space, coworking, trabajo concentrado',   color:'#70AD47'},\n  {clase:'C',label:'Absorci\u00f3n media-alta',rango:'0,60\u20130,79', min:0.60,uso:'Restaurantes, zonas informales, corredores',   color:'#FFC000'},\n  {clase:'D',label:'Absorci\u00f3n media',     rango:'0,30\u20130,59', min:0.30,uso:'Zonas secundarias, pasillos, aplicaciones mixtas',color:'#FF7F00'},\n  {clase:'E',label:'Baja absorci\u00f3n',      rango:'0,15\u20130,29', min:0.15,uso:'Complemento, no suficiente como tratamiento \u00fanico',color:'#FF0000'},\n  {clase:'\u2014',label:'Reflectante',         rango:'\u03b1w < 0,15', min:0,  uso:'Hormig\u00f3n, vidrio, cer\u00e1mica, piedra sin tratamiento',color:'#808080'},\n];\nfunction isoClass(aw){\n  if(aw==null||!isFinite(aw))return ISO_CLASSES[5];\n  return ISO_CLASSES.find(c=>aw>=c.min)||ISO_CLASSES[5];\n}\n\/\/ Renderiza un badge de clase ISO coloreado\nfunction isoBadge(aw){const c=isoClass(aw);return `<span style=\"background:${c.color};color:#fff;font-weight:700;padding:2px 7px;border-radius:999px;font-size:.78rem\">Clase ${c.clase}<\/span>`;}\n\/\/ Tabla de referencia ISO para incluir en documentos\nfunction isoTableHTML(){\n  return `<h3 style=\"color:#0e7c7b;margin-top:18px\">Clasificaci\u00f3n absorci\u00f3n ac\u00fastica \u2014 ISO 11654<\/h3>\n  <table style=\"border-collapse:collapse;font-size:12px;width:100%\">\n    <tr style=\"background:#eef6f6\"><th style=\"border:1px solid #ddd;padding:5px\">Clase<\/th><th style=\"border:1px solid #ddd;padding:5px\">Rango \u03b1w<\/th><th style=\"border:1px solid #ddd;padding:5px\">Descripci\u00f3n<\/th><th style=\"border:1px solid #ddd;padding:5px\">Id\u00f3neo para<\/th><\/tr>\n    ${ISO_CLASSES.map(c=>`<tr><td style=\"border:1px solid #ddd;padding:5px;text-align:center\"><b style=\"background:${c.color};color:#fff;padding:1px 8px;border-radius:4px\">${c.clase}<\/b><\/td><td style=\"border:1px solid #ddd;padding:5px;text-align:center\">${c.rango}<\/td><td style=\"border:1px solid #ddd;padding:5px\">${c.label}<\/td><td style=\"border:1px solid #ddd;padding:5px;color:#555\">${c.uso}<\/td><\/tr>`).join('')}\n  <\/table>\n  <p style=\"font-size:10px;color:#888\">\u03b1w estimado como media de bandas 500\/1000\/2000 Hz (aproximaci\u00f3n; valor exacto seg\u00fan UNE-EN ISO 11654 requiere ensayo en c\u00e1mara reverberante).<\/p>`;}\n\n\/\/ Tabla ISO 11654 con estilo de la app, resaltando estado actual y soluci\u00f3n propuesta\nfunction isoTableClient(currentAw,proposedAw){\n  const curC=isoClass(currentAw).clase, propC=isoClass(proposedAw).clase;\n  const rows=ISO_CLASSES.map(c=>{\n    const isCur=c.clase===curC, isProp=c.clase===propC;\n    const mark=isProp?'<span style=\"background:var(--brand,#0e7c7b);color:#fff;font-weight:700;padding:1px 8px;border-radius:999px;font-size:.66rem;margin-left:.4rem;white-space:nowrap\">Soluci\u00f3n propuesta<\/span>'\n      :(isCur?'<span style=\"background:var(--bad,#c0392b);color:#fff;font-weight:700;padding:1px 8px;border-radius:999px;font-size:.66rem;margin-left:.4rem;white-space:nowrap\">Tu sala ahora<\/span>':'');\n    const hl=isProp?'background:var(--brand-soft,#e4f0ef)':(isCur?'background:var(--bad-bg,#fbeae5)':'');\n    return `<tr style=\"${hl}\">\n      <td style=\"border:1px solid var(--line);padding:6px;text-align:center\"><b style=\"background:${c.color};color:#fff;padding:1px 8px;border-radius:5px\">${c.clase}<\/b><\/td>\n      <td style=\"border:1px solid var(--line);padding:6px;text-align:center;font-size:.8rem\">${c.rango}<\/td>\n      <td style=\"border:1px solid var(--line);padding:6px;font-size:.8rem\">${c.label}${mark}<\/td>\n      <td style=\"border:1px solid var(--line);padding:6px;font-size:.78rem;color:var(--muted)\">${c.uso}<\/td>\n    <\/tr>`;\n  }).join('');\n  return `<h3 style=\"font-size:.95rem;margin:1.4rem 0 .5rem\">Nivel de absorci\u00f3n ac\u00fastica (ISO 11654)<\/h3>\n    <p class=\"hint\" style=\"margin-bottom:.5rem\">Cuanto m\u00e1s alta es la clase (<b>A<\/b> es la mejor), m\u00e1s sonido \u201cabsorbe\u201d el material en vez de rebotarlo.<\/p>\n    <table style=\"border-collapse:collapse;width:100%\">\n      <tr style=\"background:#34516e;color:#fff\">\n        <th style=\"border:1px solid #2c4258;padding:6px;font-size:.76rem\">Clase<\/th>\n        <th style=\"border:1px solid #2c4258;padding:6px;font-size:.76rem\">Rango \u03b1w<\/th>\n        <th style=\"border:1px solid #2c4258;padding:6px;font-size:.76rem\">Absorci\u00f3n<\/th>\n        <th style=\"border:1px solid #2c4258;padding:6px;font-size:.76rem\">Id\u00f3neo para<\/th>\n      <\/tr>\n      ${rows}\n    <\/table>`;\n}\n\n\nfunction meanReverb(byBand){return REVERB.reduce((s,b)=>s+(byBand[b]||0),0)\/REVERB.length;}\n\nfunction resolverLimite(use,occ,V){\n  const u=dbrUse(use);\n  if(u==='ZONA_COMUN')return{absBased:true,absLimit:0.2*V,desc:'Zona com\u00fan: absorci\u00f3n A \u2265 0,2\u00b7V'};\n  if(u==='AULA'||u==='SALA_CONFERENCIAS'){\n    if(occ==='VACIO_CON_BUTACAS')return{absBased:false,rtLimit:0.5,desc:'Con butacas (sin ocupar): RT \u2264 0,5 s'};\n    return{absBased:false,rtLimit:0.7,desc:'Vac\u00eda: RT \u2264 0,7 s'};\n  }\n  if(u==='COMEDOR'||u==='RESTAURANTE')return{absBased:false,rtLimit:0.9,desc:'Comedor\/restaurante vac\u00edo: RT \u2264 0,9 s'};\n  return{absBased:false,rtLimit:null,desc:'Uso orientativo: sin l\u00edmite normativo directo'};\n}\n\n\/\/ surfaces: [{area, a:[6]}]  \u2192 calcula A y T por banda\nfunction calcular(V,use,occ,surfaces,opts={}){\n  const air=!!opts.air, method=opts.method||'SABINE_GENERAL', k=opts.k||K_SABINE;\n  const A={},T={};\n  BANDS.forEach((b,i)=>{\n    let a=0; surfaces.forEach(s=>a+=(s.a[i]||0)*s.area);\n    if(air)a+=4*(AIR_M[b]||0)*V;\n    A[b]=a;\n  });\n  if(method==='SIMPLIFIED'){const am=meanReverb(A);BANDS.forEach(b=>T[b]=am>0?k*V\/am:Infinity);}\n  else BANDS.forEach((b,i)=>T[b]=A[b]>0?k*V\/A[b]:Infinity);\n  const rtMean=meanReverb(T), eqAbs=meanReverb(A);\n  const lim=resolverLimite(use,occ,V);\n  let compliant=null,reason=lim.desc,deficitM2=0;\n  if(lim.absBased){compliant=eqAbs>=lim.absLimit;deficitM2=Math.max(0,lim.absLimit-eqAbs);\n    reason=compliant?`CUMPLE \u00b7 A=${f(eqAbs)} m\u00b2 \u2265 ${f(lim.absLimit)} m\u00b2`:`NO CUMPLE \u00b7 faltan ${f(deficitM2)} m\u00b2 de absorci\u00f3n`;\n  }else if(lim.rtLimit!=null){compliant=rtMean<=lim.rtLimit;\n    const Atarget=k*V\/lim.rtLimit; deficitM2=Math.max(0,Atarget-eqAbs);\n    reason=compliant?`CUMPLE \u00b7 RT=${f(rtMean)} s \u2264 ${f(lim.rtLimit)} s`:`NO CUMPLE \u00b7 RT=${f(rtMean)} s > ${f(lim.rtLimit)} s`;\n  }else reason='SIN VEREDICTO \u00b7 '+lim.desc;\n  return{A,T,rtMean,eqAbs,compliant,reason,absBased:lim.absBased,limit:lim,deficitM2};\n}\n\nconst f=x=>(x==null||!isFinite(x))?'\u2014':(Math.round(x*100)\/100).toLocaleString('es-ES');\nconst f3=x=>(x==null||!isFinite(x))?'\u2014':(Math.round(x*1000)\/1000).toLocaleString('es-ES');\nconst eur=x=>(Math.round(x*100)\/100).toLocaleString('es-ES',{minimumFractionDigits:2,maximumFractionDigits:2})+' \u20ac';\n\n\/* ============================================================\n   GEOMETR\u00cdA \u2014 pol\u00edgono (cualquier forma) por v\u00e9rtices en metros\n   ============================================================ *\/\nfunction shoelaceArea(pts){let a=0;for(let i=0;i<pts.length;i++){const j=(i+1)%pts.length;a+=pts[i].x*pts[j].y-pts[j].x*pts[i].y;}return Math.abs(a)\/2;}\nfunction perimeterEdges(pts){const e=[];for(let i=0;i<pts.length;i++){const j=(i+1)%pts.length;const len=Math.hypot(pts[j].x-pts[i].x,pts[j].y-pts[i].y);e.push({i,len});}return e;}\n\n\/* ============================================================\n   ESTADO DEL PROYECTO (cliente)\n   ============================================================ *\/\nlet S=blankProject();\nfunction blankProject(){return{\n  client:{name:'',surname:'',email:'',cp:'',phone:''},\n  photos:[], plan:null, detected:[],\n  room:{type:'EDU_AULA',sector:'Educaci\u00f3n',occ:'VACIO',occItems:[],shapeMode:'rect',\n    rectL:0,rectW:0,height:0,\n    poly:[], scale:1,\n    floorMat:'s04', ceilMat:'t03',\n    walls:[], elements:[]},\n  result:null, resultOcc:null, proposal:null, chosenSystem:null, observations:'',\n};}\n\n\/* ============================================================\n   NAVEGACI\u00d3N\n   ============================================================ *\/\nlet step=1;\nconst STEP_TITLES=['Tus datos','Fotos y plano','Tu estancia','Resultado y propuesta'];\nfunction showView(v){\n  document.getElementById('view-client').classList.toggle('hide',v!=='client');\n  document.getElementById('view-internal').classList.toggle('hide',v!=='internal');\n  document.getElementById('navClient').classList.toggle('active',v==='client');\n  document.getElementById('navInternal').classList.toggle('active',v==='internal');\n  if(v==='internal')renderInternal();\n}\nfunction renderStepper(){\n  document.getElementById('stepper').innerHTML=STEP_TITLES.map((t,i)=>{\n    const n=i+1,cls=n===step?'active':(n<step?'done':'');\n    return `<div class=\"st ${cls}\"><span class=\"n\">${n<step?'\u2713':n}<\/span>${t}<\/div>`;\n  }).join('');\n}\nfunction goStep(n){step=n;renderStepper();renderStep();window.scrollTo(0,0);}\nfunction renderStep(){\n  const c=document.getElementById('steps');\n  if(step===1)c.innerHTML=stepRegister();\n  else if(step===2)c.innerHTML=stepCapture(),bindCapture();\n  else if(step===3)c.innerHTML=stepRoom(),bindRoom();\n  else if(step===4)stepResult(c);\n}\n\n\/* ===== PASO 1: REGISTRO ===== *\/\n\/\/ C\u00f3digo postal espa\u00f1ol: 5 d\u00edgitos; los 2 primeros identifican la provincia (01\u201352).\nconst PROVINCES={'01':'\u00c1lava','02':'Albacete','03':'Alicante','04':'Almer\u00eda','05':'\u00c1vila','06':'Badajoz','07':'Illes Balears','08':'Barcelona','09':'Burgos','10':'C\u00e1ceres','11':'C\u00e1diz','12':'Castell\u00f3n','13':'Ciudad Real','14':'C\u00f3rdoba','15':'A Coru\u00f1a','16':'Cuenca','17':'Girona','18':'Granada','19':'Guadalajara','20':'Gipuzkoa','21':'Huelva','22':'Huesca','23':'Ja\u00e9n','24':'Le\u00f3n','25':'Lleida','26':'La Rioja','27':'Lugo','28':'Madrid','29':'M\u00e1laga','30':'Murcia','31':'Navarra','32':'Ourense','33':'Asturias','34':'Palencia','35':'Las Palmas','36':'Pontevedra','37':'Salamanca','38':'Santa Cruz de Tenerife','39':'Cantabria','40':'Segovia','41':'Sevilla','42':'Soria','43':'Tarragona','44':'Teruel','45':'Toledo','46':'Valencia','47':'Valladolid','48':'Bizkaia','49':'Zamora','50':'Zaragoza','51':'Ceuta','52':'Melilla'};\nfunction cpProvince(cp){return \/^\\d{5}$\/.test(cp)?(PROVINCES[cp.slice(0,2)]||null):null;}\nfunction cpInfo(){\n  const el=document.getElementById('cpFeedback');if(!el)return;\n  const cp=val('c_cp');\n  if(!cp){el.textContent='';return;}\n  const prov=cpProvince(cp);\n  if(prov){el.style.color='var(--ok)';el.textContent='\u2713 Provincia: '+prov;}\n  else{el.style.color='var(--bad)';el.textContent='C\u00f3digo postal no v\u00e1lido (5 d\u00edgitos, provincia 01\u201352).';}\n}\nfunction stepRegister(){\n  const c=S.client;\n  return `<div class=\"card\">\n    <h1>Bienvenido\/a<\/h1>\n    <p class=\"lead\">Calcula en pocos minutos si tu sala cumple la normativa ac\u00fastica (CTE DB-HR) y recibe una propuesta para mejorarla. Empecemos con tus datos de contacto.<\/p>\n    <div class=\"grid2\">\n      <div class=\"fld\"><label>Nombre <span class=\"req\">*<\/span><\/label><input id=\"c_name\" value=\"${esc(c.name)}\"><\/div>\n      <div class=\"fld\"><label>Apellidos <span class=\"req\">*<\/span><\/label><input id=\"c_surname\" value=\"${esc(c.surname)}\"><\/div>\n      <div class=\"fld\"><label>Correo electr\u00f3nico <span class=\"req\">*<\/span><\/label><input id=\"c_email\" type=\"email\" value=\"${esc(c.email)}\"><\/div>\n      <div class=\"fld\"><label>C\u00f3digo postal <span class=\"req\">*<\/span><\/label><input id=\"c_cp\" inputmode=\"numeric\" maxlength=\"5\" value=\"${esc(c.cp)}\" oninput=\"cpInfo()\"><span class=\"hint\" id=\"cpFeedback\"><\/span><\/div>\n      <div class=\"fld\"><label>Tel\u00e9fono <span class=\"muted\">(opcional)<\/span><\/label><input id=\"c_phone\" value=\"${esc(c.phone)}\"><\/div>\n    <\/div>\n    <div class=\"actions\"><span><\/span>\n      <button class=\"btn btn-primary\" onclick=\"saveRegister()\">Continuar \u2192<\/button><\/div>\n  <\/div>`;\n}\nfunction saveRegister(){\n  S.client={name:val('c_name'),surname:val('c_surname'),email:val('c_email'),cp:val('c_cp'),phone:val('c_phone')};\n  if(!S.client.name||!S.client.surname||!S.client.email||!S.client.cp){alert('Completa los campos obligatorios (*).');return;}\n  if(!\/^\\S+@\\S+\\.\\S+$\/.test(S.client.email)){alert('Revisa el correo electr\u00f3nico.');return;}\n  const prov=cpProvince(S.client.cp);\n  if(!prov){alert('El c\u00f3digo postal no es v\u00e1lido. Debe tener 5 d\u00edgitos y corresponder a una provincia espa\u00f1ola (01\u201352).');return;}\n  S.client.province=prov;\n  goStep(2);\n}\n\n\/* ===== PASO 2: FOTOS Y PLANO ===== *\/\nfunction stepCapture(){\n  const hasKey=!!cfg('claudeApiKey','');\n  return `<div class=\"card\">\n    <h1>Fotos y plano<\/h1>\n    <p class=\"lead\">Sube una o varias fotos de la sala y, si tienes, un plano. Los elementos se identifican autom\u00e1ticamente al subirlos. Este paso es opcional.<\/p>\n\n    <h3>\ud83d\udcf7 Fotos del entorno<\/h3>\n    <div class=\"uploader\" id=\"up_photos\">Pulsa o arrastra aqu\u00ed tus fotos<br><span class=\"hint\">JPG\/PNG \u00b7 varias permitidas<\/span><\/div>\n    <input id=\"in_photos\" type=\"file\" accept=\"image\/*\" multiple class=\"hide\">\n    <div class=\"thumbs\" id=\"th_photos\"><\/div>\n    <div class=\"note-demo\" style=\"${hasKey?'border-color:var(--ok);color:var(--ok);background:var(--ok-bg)':''}\">\n      ${hasKey?'\u2705 <b>Identificaci\u00f3n IA activa<\/b>: al subir una foto Claude analizar\u00e1 el recinto autom\u00e1ticamente.':'\ud83d\udd0e <b>Modo demo<\/b>: se asignan elementos t\u00edpicos al subir la foto. Configura tu <b>API key de Claude<\/b> en \u00abZona interna \u2192 Configuraci\u00f3n\u00bb para activar la visi\u00f3n real.'}\n    <\/div>\n    <div id=\"detList\" style=\"margin-top:.7rem\"><\/div>\n    <button class=\"btn btn-sm\" style=\"margin-top:.4rem\" onclick=\"redetect()\">\u21bb Volver a identificar<\/button>\n\n    <h3 style=\"margin-top:1.6rem\">\ud83d\udcd0 Plano (a mano o de proyecto)<\/h3>\n    <div class=\"uploader\" id=\"up_plan\">Pulsa o arrastra aqu\u00ed el plano<br><span class=\"hint\">imagen o foto del croquis<\/span><\/div>\n    <input id=\"in_plan\" type=\"file\" accept=\"image\/*\" class=\"hide\">\n    <div id=\"th_plan\" class=\"thumbs\"><\/div>\n    <div id=\"planStatus\" style=\"margin:.5rem 0\"><\/div>\n    <div id=\"multiRoomNote\"><\/div>\n    <div class=\"grid3\" id=\"planMeasures\" style=\"margin-top:.4rem\">\n      <div class=\"fld\"><label>Largo (m)<\/label><input id=\"p_l\" type=\"number\" step=\"0.01\" placeholder=\"ej. 8\"><\/div>\n      <div class=\"fld\"><label>Ancho (m)<\/label><input id=\"p_w\" type=\"number\" step=\"0.01\" placeholder=\"ej. 6\"><\/div>\n      <div class=\"fld\"><label>Altura (m)<\/label><input id=\"p_h\" type=\"number\" step=\"0.01\" placeholder=\"ej. 3\"><\/div>\n    <\/div>\n    <p class=\"hint\">Revisa siempre las medidas antes de continuar. ${hasKey?'Con IA activa se rellenan autom\u00e1ticamente si son legibles.':'Introd\u00facelas manualmente leyendo el plano.'}<\/p>\n\n    <div class=\"actions\">\n      <button class=\"btn\" onclick=\"goStep(1)\">\u2190 Atr\u00e1s<\/button>\n      <button class=\"btn btn-primary\" onclick=\"saveCapture()\">Continuar \u2192<\/button>\n    <\/div>\n  <\/div>`;\n}\nfunction bindCapture(){\n  hookUpload('up_photos','in_photos',true,addPhotos);\n  hookUpload('up_plan','in_plan',false,addPlan);\n  renderPhotos();renderPlan();renderDetected();\n}\nfunction hookUpload(zoneId,inputId,multi,cb){\n  const z=document.getElementById(zoneId),inp=document.getElementById(inputId);\n  if(!z)return;\n  z.onclick=()=>inp.click();\n  inp.onchange=e=>readFiles(e.target.files,cb);\n  z.ondragover=e=>{e.preventDefault();z.style.borderColor='var(--brand)';};\n  z.ondragleave=()=>z.style.borderColor='';\n  z.ondrop=e=>{e.preventDefault();z.style.borderColor='';readFiles(e.dataTransfer.files,cb);};\n}\nfunction readFiles(files,cb){[...files].forEach(file=>{const r=new FileReader();r.onload=()=>cb(r.result,file.name);r.readAsDataURL(file);});}\n\n\/* \u2500\u2500 Claude Vision API \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500 *\/\nasync function callClaudeVision(dataURL,prompt){\n  const apiKey=cfg('claudeApiKey','');\n  if(!apiKey)throw new Error('no_key');\n  const b64=dataURL.split(',')[1], mime=(dataURL.match(\/data:([^;]+)\/)||[])[1]||'image\/jpeg';\n  const res=await fetch('https:\/\/api.anthropic.com\/v1\/messages',{\n    method:'POST',\n    headers:{'Content-Type':'application\/json','x-api-key':apiKey,'anthropic-version':'2023-06-01','anthropic-dangerous-direct-browser-access':'true'},\n    body:JSON.stringify({model:'claude-haiku-4-5-20251001',max_tokens:500,\n      messages:[{role:'user',content:[\n        {type:'image',source:{type:'base64',media_type:mime,data:b64}},\n        {type:'text',text:prompt}\n      ]}]\n    })\n  });\n  if(!res.ok){const e=await res.json().catch(()=>({}));throw new Error(e.error?.message||'Error '+res.status);}\n  const d=await res.json();\n  return d.content[0].text;\n}\n\nconst DEMO_ELEMENTS=[\n  'Paredes de yeso \/ enlucido','Suelo de terrazo o pavimento duro',\n  'Techo de escayola liso','Ventanas de vidrio','Sin tratamiento ac\u00fastico visible'\n];\nconst DEMO_DIMS={largo:null,ancho:null,altura:null};\n\n\/* \u2500\u2500\u2500 PUNTO 1: Materiales no en simplificada \u2192 buscar en extensa \u2500\u2500\u2500 *\/\nfunction normTxt(s){return s.toLowerCase().normalize('NFD').replace(\/\\p{M}\/gu,'').replace(\/[^a-z0-9 ]\/g,' ');}\nfunction findMatInExtensa(label){\n  const q=normTxt(label);\n  \/\/ Busca primero en simplificada\n  const inSimple=ACUSTIUM_SIMPLIFICADA.find(m=>{\n    const n=normTxt(m.name);\n    return q.split(' ').filter(w=>w.length>3).some(w=>n.includes(w));\n  });\n  if(inSimple)return{mat:inSimple,source:'simplificada'};\n  \/\/ Si no, busca en extensa\n  const inExt=ACUSTIUM_EXTENSA.find(m=>{\n    const n=normTxt(m.name);\n    return q.split(' ').filter(w=>w.length>3).some(w=>n.includes(w));\n  });\n  if(inExt)return{mat:inExt,source:'extensa'};\n  return null;\n}\n\/\/ Llama al matcheo sobre los elementos detectados y guarda resultado en S.detectedMats\nfunction matchDetectedToMaterials(){\n  S.detectedMats=S.detected.map(d=>{\n    const found=findMatInExtensa(d.t);\n    return found?{...d,matId:found.mat.id,matName:found.mat.name,source:found.source}:{...d,matId:null,source:'desconocido'};\n  });\n}\n\nasync function addPhotos(d){\n  S.photos.push(d);renderPhotos();\n  const box=document.getElementById('detList');\n  if(box)box.innerHTML='<span class=\"hint\">\ud83d\udd04 Analizando imagen\u2026<\/span>';\n  try{\n    const txt=await callClaudeVision(d,\n      'Analiza esta foto del recinto. Lista los materiales y elementos ac\u00fasticamente relevantes que observas (tipo de suelo, revestimiento de paredes, techo, ventanas, puertas, tratamientos ac\u00fasticos existentes). Responde SOLO con una lista de elementos separados por punto y coma en espa\u00f1ol, sin explicaci\u00f3n. Ejemplo: Suelo de terrazo; Paredes de yeso liso; Techo de escayola; Ventanas grandes de vidrio; Sin tratamiento ac\u00fastico'\n    );\n    const items=txt.split(';').map(t=>t.trim()).filter(Boolean);\n    items.forEach(t=>{if(!S.detected.find(x=>x.t===t))S.detected.push({t,ok:true});});\n  }catch(e){\n    if(S.photos.length===1&&!S.detected.length)\n      DEMO_ELEMENTS.forEach(t=>S.detected.push({t,ok:true}));\n  }\n  matchDetectedToMaterials();\n  renderDetected();\n}\n\nasync function addPlan(d){\n  S.plan=d;renderPlan();\n  const box=document.getElementById('planStatus');\n  if(box)box.innerHTML='<span class=\"hint\">\ud83d\udd04 Leyendo medidas del plano\u2026<\/span>';\n  try{\n    const txt=await callClaudeVision(d,\n      'Lee las medidas num\u00e9ricas de este plano o croquis. Responde SOLO en JSON con este formato exacto: {\"largo\":null,\"ancho\":null,\"altura\":null,\"varias_estancias\":false}. Sustituye null por el n\u00famero que leas. Convierte cm a metros si es necesario. Pon varias_estancias:true si ves m\u00e1s de una sala en el plano. Solo el JSON, sin texto adicional.'\n    );\n    const clean=txt.replace(\/```[^`]*```\/g,'').replace(\/```\/g,'').trim();\n    const dims=JSON.parse(clean);\n    if(dims.largo>0){const e=document.getElementById('p_l');if(e)e.value=dims.largo;}\n    if(dims.ancho>0){const e=document.getElementById('p_w');if(e)e.value=dims.ancho;}\n    if(dims.altura>0){const e=document.getElementById('p_h');if(e)e.value=dims.altura;}\n    if(dims.varias_estancias){\n      S.planHasMultipleRooms=true;\n      const mr=document.getElementById('multiRoomNote');\n      if(mr)mr.innerHTML='<div class=\"note-demo\" style=\"border-color:var(--brand);color:var(--brand-d)\">\ud83d\udcd0 El plano parece tener <b>varias estancias<\/b>. Calcula una a la vez y usa \u00abCalcular otra estancia\u00bb al terminar cada una.<\/div>';\n    }\n    if(box)box.innerHTML='<span style=\"color:var(--ok)\">\u2713 Medidas extra\u00eddas. Rev\u00edsalas antes de continuar.<\/span>';\n  }catch(e){\n    if(box)box.innerHTML='<span class=\"hint\">Plano cargado. Introduce las medidas que ves en el plano en los campos de abajo.<\/span>';\n  }\n}\nfunction renderPhotos(){const el=document.getElementById('th_photos');if(!el)return;\n  el.innerHTML=S.photos.map((p,i)=>`<div class=\"thumb\"><img decoding=\"async\" src=\"${p}\"><button class=\"x\" onclick=\"S.photos.splice(${i},1);renderPhotos()\">\u00d7<\/button><\/div>`).join('');}\nfunction renderPlan(){const el=document.getElementById('th_plan');if(!el)return;\n  el.innerHTML=S.plan?`<div class=\"thumb\"><img decoding=\"async\" src=\"${S.plan}\"><button class=\"x\" onclick=\"S.plan=null;renderPlan()\">\u00d7<\/button><\/div>`:'';}\nfunction detectDemo(){\n  if(!S.photos.length){alert('Sube primero al menos una foto.');return;}\n  S.detected=[];DEMO_ELEMENTS.forEach(t=>S.detected.push({t,ok:true}));\n  matchDetectedToMaterials();renderDetected();\n}\nasync function redetect(){\n  if(!S.photos.length){alert('Sube primero al menos una foto.');return;}\n  S.detected=[];\n  const box=document.getElementById('detList');\n  if(box)box.innerHTML='<span class=\"hint\">\ud83d\udd04 Analizando\u2026<\/span>';\n  try{\n    const txt=await callClaudeVision(S.photos[S.photos.length-1],\n      'Lista los materiales y elementos ac\u00fasticamente relevantes de este recinto (suelo, paredes, techo, ventanas, tratamientos). Solo una lista separada por punto y coma en espa\u00f1ol, sin explicaci\u00f3n.'\n    );\n    txt.split(';').map(t=>t.trim()).filter(Boolean).forEach(t=>S.detected.push({t,ok:true}));\n  }catch(e){DEMO_ELEMENTS.forEach(t=>S.detected.push({t,ok:true}));}\n  matchDetectedToMaterials();renderDetected();\n}\nfunction renderDetected(){\n  const el=document.getElementById('detList');if(!el)return;\n  if(!S.detected.length){el.innerHTML='';return;}\n  const mats=S.detectedMats||S.detected.map(d=>({...d,source:'desconocido'}));\n  const hasExtensa=mats.some(m=>m.source==='extensa');\n  el.innerHTML='<b style=\"font-size:.85rem\">Elementos identificados (revisa y elimina lo que no aplique):<\/b><br>'+\n    mats.map((d,i)=>{\n      let badge='';\n      if(d.source==='extensa')badge=` <span style=\"font-size:.7rem;background:#eef6f6;border:1px solid #0e7c7b;color:#0e7c7b;padding:1px 5px;border-radius:4px\" title=\"En lista extensa: se a\u00f1adir\u00e1 al paso 3\">\u2192 extensa<\/span>`;\n      else if(d.source==='simplificada')badge=` <span style=\"font-size:.7rem;color:var(--muted)\">\u2713<\/span>`;\n      return `<span class=\"chip\">${esc(d.t)}${badge} <button onclick=\"S.detected.splice(${i},1);S.detectedMats&&S.detectedMats.splice(${i},1);renderDetected()\">\u00d7<\/button><\/span>`;\n    }).join('')+\n    (hasExtensa?'<p class=\"hint\" style=\"margin-top:.5rem\">Los marcados como <b>\u2192 extensa<\/b> no est\u00e1n en la lista habitual. Se a\u00f1adir\u00e1n como elementos en el siguiente paso para que completes sus medidas.<\/p>':'');\n}\n\n\/* \u2500\u2500\u2500 PUNTO 3: Validaci\u00f3n de datos faltantes antes de continuar \u2500\u2500\u2500\u2500 *\/\nfunction datosFaltantes(){\n  const faltan=[];\n  const l=parseFloat(val('p_l')),w=parseFloat(val('p_w'));\n  if(!(l>0)||!(w>0))faltan.push('Medidas de la sala (largo y ancho)');\n  if(!S.photos.length&&!S.plan)faltan.push('Fotos o plano del espacio (recomendado para mayor precisi\u00f3n)');\n  return faltan;\n}\n\n\/* \u2500\u2500\u2500 PUNTO 2: Varias estancias \u2192 traspasar materiales extensa y preguntar \u2500\u2500\u2500 *\/\nfunction saveCapture(){\n  const l=parseFloat(val('p_l')),w=parseFloat(val('p_w')),h=parseFloat(val('p_h'));\n  if(l>0)S.room.rectL=l; if(w>0)S.room.rectW=w; if(h>0)S.room.height=h;\n  \/\/ Punto 3: advertir si faltan datos cr\u00edticos\n  const faltan=datosFaltantes();\n  if(faltan.some(f=>f.includes('Medidas'))){\n    if(!confirm('\u26a0\ufe0f Faltan datos importantes:\\n\\n\u2022 '+faltan.join('\\n\u2022 ')+'\\n\\n\u00bfDeseas continuar de todas formas e introducir las medidas en el siguiente paso?'))return;\n  }else if(faltan.length&&!confirm('Falta: '+faltan.join(', ')+'. \u00bfContinuar igualmente?'))return;\n  \/\/ Reset al avanzar\n  S.room.walls=[];\n  S.room.elements=[];\n  \/\/ Punto 1: a\u00f1adir materiales de lista extensa como elementos pendientes de medida\n  if(S.detectedMats){\n    S.detectedMats.filter(d=>d.source==='extensa'&&d.matId).forEach(d=>{\n      S.room.elements.push({name:d.matName,area:0,mat:d.matId,wall:'libre',_fromDetect:true});\n    });\n  }\n  goStep(3);\n}\n\n\/* ===== PASO 3: ESTANCIA \u2014 soporte sector\/IA\/ocupaci\u00f3n ===== *\/\n\/\/ Opciones de tipo filtradas por sector\nfunction typeOptionsForSector(sector,sel){\n  const g=USE_GROUPS.find(x=>x.sector===sector)||USE_GROUPS[0];\n  let opts=g.items.map(it=>`<option value=\"${it.id}\" ${it.id===sel?'selected':''}>${it.label}<\/option>`).join('');\n  \/\/ tipos generados por IA\/usuario para este sector\n  (S.customTypes||[]).filter(t=>t.sector===sector).forEach(t=>{\n    opts+=`<option value=\"${t.id}\" ${t.id===sel?'selected':''}>${t.label} (personalizado)<\/option>`;\n  });\n  return opts;\n}\n\/\/ Etiqueta de un tipo personalizado\nfunction customLabelFor(id){const t=(S.customTypes||[]).find(x=>x.id===id);return t?t.label:'';}\n\/\/ El campo \"tipo de estancia\": desplegable normal, o texto libre+IA si sector = Otros\nfunction typeFieldHTML(sector,sel){\n  if(sector==='Otros'){\n    const lbl=customLabelFor(sel);\n    const cls=ROOM_TO_DBR[sel];\n    const res=cls&&sel.startsWith('custom_')?`\u2705 Clasificado como: <b>${esc(dbrLabel(cls))}<\/b>`:'Escribe el espacio y pulsa \u00abClasificar con IA\u00bb.';\n    return `<label>Tipo de estancia (personalizado)<\/label>\n      <input id=\"r_other_desc\" placeholder=\"Ej. sala de catas, estudio de yoga, escape room\u2026\" value=\"${esc(lbl)}\">\n      <button class=\"btn btn-sm\" type=\"button\" style=\"margin-top:.4rem\" onclick=\"clasificarOtros()\">\ud83e\udd16 Clasificar con IA<\/button>\n      <div id=\"otrosResult\" class=\"hint\" style=\"margin-top:.4rem\">${res}<\/div>`;\n  }\n  return `<label>Tipo de estancia<\/label>\n    <select id=\"r_type\">${typeOptionsForSector(sector,sel)}<\/select>\n    <button class=\"btn btn-sm\" type=\"button\" style=\"margin-top:.4rem\" onclick=\"generarEstanciaIA()\">\ud83d\udd0d \u00bfNo est\u00e1? Buscar \/ generar con IA<\/button>`;\n}\nfunction onSectorChange(sector){\n  S.room.sector=sector;\n  const g=USE_GROUPS.find(x=>x.sector===sector)||USE_GROUPS[0];\n  S.room.type=g.items[0].id;\n  const tf=document.getElementById('typeField');\n  if(tf)tf.innerHTML=typeFieldHTML(sector,S.room.type);\n}\n\/\/ Clasificar la descripci\u00f3n libre del sector \"Otros\" con IA (o heur\u00edstica)\nasync function clasificarOtros(){\n  const desc=((document.getElementById('r_other_desc')||{}).value||'').trim();\n  if(!desc){alert('Describe el espacio que quieres calcular.');return;}\n  const rEl=document.getElementById('otrosResult');if(rEl)rEl.textContent='Clasificando\u2026';\n  let dbr='ZONA_COMUN';\n  try{\n    const txt=await callClaudeText('Clasifica esta estancia en UNA de estas categor\u00edas DB-HR para c\u00e1lculo de reverberaci\u00f3n: AULA, SALA_CONFERENCIAS, COMEDOR, RESTAURANTE, ZONA_COMUN. Estancia: \"'+desc+'\". Responde SOLO con la categor\u00eda, una palabra.');\n    const c=txt.trim().toUpperCase().replace(\/[^A-Z_]\/g,'');\n    if(['AULA','SALA_CONFERENCIAS','COMEDOR','RESTAURANTE','ZONA_COMUN'].includes(c))dbr=c;\n  }catch(e){dbr=heuristicDbr(desc);}\n  const id='custom_'+Date.now();\n  S.customTypes=S.customTypes||[];\n  S.customTypes.push({id,label:desc,sector:'Otros'});\n  ROOM_TO_DBR[id]=dbr;\n  S.room.type=id;\n  if(rEl)rEl.innerHTML=`\u2705 Clasificado como: <b>${esc(dbrLabel(dbr))}<\/b>`;\n}\n\/\/ Generar\/clasificar una estancia nueva con IA (mapea a categor\u00eda DB-HR)\nasync function generarEstanciaIA(){\n  const nombre=prompt('Describe el tipo de estancia (ej. \"sala de catas\", \"estudio de grabaci\u00f3n\", \"gimnasio\"):');\n  if(!nombre)return;\n  let dbr='ZONA_COMUN';\n  try{\n    const txt=await callClaudeText('Clasifica esta estancia en UNA de estas categor\u00edas DB-HR para c\u00e1lculo de reverberaci\u00f3n: AULA, SALA_CONFERENCIAS, COMEDOR, RESTAURANTE, ZONA_COMUN. Estancia: \"'+nombre+'\". Responde SOLO con la categor\u00eda, una palabra.');\n    const c=txt.trim().toUpperCase().replace(\/[^A-Z_]\/g,'');\n    if(['AULA','SALA_CONFERENCIAS','COMEDOR','RESTAURANTE','ZONA_COMUN'].includes(c))dbr=c;\n  }catch(e){dbr=heuristicDbr(nombre);}\n  const id='custom_'+Date.now();\n  S.customTypes=S.customTypes||[];\n  S.customTypes.push({id,label:nombre,sector:S.room.sector});\n  ROOM_TO_DBR[id]=dbr;\n  S.room.type=id;\n  const sel=document.getElementById('r_type');\n  if(sel)sel.innerHTML=typeOptionsForSector(S.room.sector,id);\n  toast('Estancia \u00ab'+nombre+'\u00bb a\u00f1adida (DB-HR: '+dbr+')');\n}\n\/\/ Texto plano v\u00eda Claude (reutiliza la API key configurada)\nasync function callClaudeText(prompt){\n  const apiKey=cfg('claudeApiKey','');\n  if(!apiKey)throw new Error('no_key');\n  const res=await fetch('https:\/\/api.anthropic.com\/v1\/messages',{\n    method:'POST',\n    headers:{'Content-Type':'application\/json','x-api-key':apiKey,'anthropic-version':'2023-06-01','anthropic-dangerous-direct-browser-access':'true'},\n    body:JSON.stringify({model:'claude-haiku-4-5-20251001',max_tokens:50,messages:[{role:'user',content:prompt}]})\n  });\n  if(!res.ok)throw new Error('API '+res.status);\n  const d=await res.json();return d.content[0].text;\n}\n\/\/ Caja de mobiliario\/personas si ocupado\nfunction occBoxHTML(){\n  if(S.room.occ!=='OCUPADO')return'';\n  const items=S.room.occItems||[];\n  const rows=items.map((it,i)=>{\n    const cat=OCC_ITEMS.find(o=>o.id===it.id)||OCC_ITEMS[0];\n    return `<div style=\"display:grid;grid-template-columns:1.6fr .6fr auto;gap:.4rem;align-items:center;margin-bottom:.4rem\">\n      <select onchange=\"S.room.occItems[${i}].id=this.value\">${OCC_ITEMS.map(o=>`<option value=\"${o.id}\" ${o.id===it.id?'selected':''}>${o.name}<\/option>`).join('')}<\/select>\n      <input type=\"number\" min=\"0\" step=\"1\" value=\"${it.qty}\" onchange=\"S.room.occItems[${i}].qty=parseInt(this.value)||0\" title=\"Cantidad\">\n      <button class=\"btn btn-sm btn-danger\" onclick=\"S.room.occItems.splice(${i},1);refreshOccBox()\">\u2715<\/button>\n    <\/div>`;\n  }).join('');\n  return `<fieldset><legend>Mobiliario y personas (estimaci\u00f3n de ocupaci\u00f3n)<\/legend>\n    <p class=\"hint\">Estos elementos a\u00f1aden absorci\u00f3n a la estimaci\u00f3n ocupada. No alteran el veredicto DB-HR (calculado en vac\u00edo).<\/p>\n    <div style=\"display:grid;grid-template-columns:1.6fr .6fr auto;gap:.4rem;font-size:.72rem;color:var(--muted);margin-bottom:.3rem\"><span>Elemento<\/span><span>Cantidad<\/span><span><\/span><\/div>\n    ${rows||'<p class=\"hint\">Sin elementos a\u00f1adidos.<\/p>'}\n    <button class=\"btn btn-sm\" type=\"button\" onclick=\"S.room.occItems=S.room.occItems||[];S.room.occItems.push({id:'persona',qty:1});refreshOccBox()\">+ A\u00f1adir mobiliario\/personas<\/button>\n  <\/fieldset>`;\n}\nfunction refreshOccBox(){const el=document.getElementById('occBox');if(el)el.innerHTML=occBoxHTML();}\nfunction onOccChange(v){S.room.occ=v;refreshOccBox();}\n\/\/ Absorci\u00f3n equivalente total de la ocupaci\u00f3n, por banda\nfunction occupancyAbsorption(){\n  const A=[0,0,0,0,0,0];\n  (S.room.occItems||[]).forEach(it=>{\n    const cat=OCC_ITEMS.find(o=>o.id===it.id);if(!cat)return;\n    cat.a.forEach((v,i)=>A[i]+=v*(it.qty||0));\n  });\n  return A;\n}\n\n\/* ===== PASO 3: ESTANCIA ===== *\/\nfunction stepRoom(){\n  const r=S.room, mats=materials();\n  const opt=(arr,sel)=>arr.map(m=>`<option value=\"${m.id}\" ${m.id===sel?'selected':''}>${esc(m.name)}<\/option>`).join('');\n  return `<div class=\"card\">\n    <h1>Tu estancia<\/h1>\n    <p class=\"lead\">Indica el tipo de sala, sus medidas y de qu\u00e9 est\u00e1n hechas las superficies. No necesitas saber de ac\u00fastica: elige las opciones que mejor describan tu espacio.<\/p>\n\n    <div class=\"grid2\">\n      <div class=\"fld\"><label>Sector \/ Industria<\/label>\n        <select id=\"r_sector\" onchange=\"onSectorChange(this.value)\">${USE_GROUPS.map(g=>`<option ${g.sector===r.sector?'selected':''}>${g.sector}<\/option>`).join('')}<\/select><\/div>\n      <div class=\"fld\" id=\"typeField\">${typeFieldHTML(r.sector,r.type)}<\/div>\n    <\/div>\n    <div class=\"fld\"><label>Estado de ocupaci\u00f3n de referencia<\/label>\n      <select id=\"r_occ\" onchange=\"onOccChange(this.value)\">\n        <option value=\"VACIO\" ${r.occ==='VACIO'?'selected':''}>Vac\u00eda (referencia DB-HR)<\/option>\n        <option value=\"VACIO_CON_BUTACAS\" ${r.occ==='VACIO_CON_BUTACAS'?'selected':''}>Vac\u00eda con butacas (sin ocupar)<\/option>\n        <option value=\"OCUPADO\" ${r.occ==='OCUPADO'?'selected':''}>Ocupada (estimaci\u00f3n con mobiliario y personas)<\/option>\n      <\/select>\n      <p class=\"hint\">El veredicto de cumplimiento DB-HR se calcula siempre en vac\u00edo (seg\u00fan norma). La ocupaci\u00f3n a\u00f1ade una estimaci\u00f3n informativa adicional.<\/p>\n    <\/div>\n    <div id=\"occBox\">${occBoxHTML()}<\/div>\n\n    <fieldset><legend>Forma y medidas<\/legend>\n      <div class=\"fld\"><label>Forma de la planta<\/label>\n        <select id=\"r_shape\" onchange=\"S.room.shapeMode=this.value;bindRoom()\">\n          <option value=\"rect\" ${r.shapeMode==='rect'?'selected':''}>Rectangular (r\u00e1pido)<\/option>\n          <option value=\"poly\" ${r.shapeMode==='poly'?'selected':''}>Forma libre \/ multiforme (dibujar)<\/option>\n        <\/select><\/div>\n      <div id=\"rectBox\" class=\"${r.shapeMode==='rect'?'':'hide'}\">\n        <div class=\"grid3\">\n          <div class=\"fld\"><label>Largo (m)<\/label><input id=\"r_l\" type=\"number\" step=\"0.1\" value=\"${r.rectL}\" onchange=\"syncRectWalls()\"><\/div>\n          <div class=\"fld\"><label>Ancho (m)<\/label><input id=\"r_w\" type=\"number\" step=\"0.1\" value=\"${r.rectW}\" onchange=\"syncRectWalls()\"><\/div>\n          <div class=\"fld\"><label>Altura (m)<\/label><input id=\"r_h1\" type=\"number\" step=\"0.1\" value=\"${r.height}\" onchange=\"syncRectWalls()\"><\/div>\n        <\/div>\n      <\/div>\n      <div id=\"polyBox\" class=\"${r.shapeMode==='poly'?'':'hide'}\">\n        <p class=\"hint\">Haz clic en la cuadr\u00edcula para marcar las esquinas de la sala (en orden). Cada cuadro = <b><span id=\"scaleLbl\">${r.scale}<\/span> m<\/b> \u00b7 la rejilla cubre hasta ~28\u00d721 m (sube la escala para salas mayores). La medida en cm aparece al mover el cursor. Pulsa \u00abCerrar planta\u00bb al terminar.<\/p>\n        <div class=\"grid3\">\n          <div class=\"fld\"><label>Escala (m por cuadro)<\/label><input id=\"r_scale\" type=\"number\" step=\"0.1\" value=\"${r.scale}\" onchange=\"S.room.scale=parseFloat(this.value)||0.5;document.getElementById('scaleLbl').textContent=S.room.scale;drawPolyEditor()\"><\/div>\n          <div class=\"fld\"><label>Altura (m)<\/label><input id=\"r_h2\" type=\"number\" step=\"0.1\" value=\"${r.height}\"><\/div>\n          <div class=\"fld\" style=\"display:flex;align-items:end;gap:.4rem\">\n            <button class=\"btn btn-sm\" type=\"button\" onclick=\"S.room.poly=[];drawPolyEditor()\">Reiniciar<\/button>\n            <button class=\"btn btn-sm btn-primary\" type=\"button\" onclick=\"closePoly()\" style=\"background:var(--brand);color:#fff;font-weight:700\">\u2705 Validar plano<\/button>\n          <\/div>\n        <\/div>\n        <div id=\"polyWrap\" style=\"border:1px solid var(--line);border-radius:10px;background:#fbfcfd\"><\/div>\n        <p class=\"hint\" id=\"polyInfo\"><\/p>\n      <\/div>\n    <\/fieldset>\n\n    <fieldset><legend>Materiales de las superficies<\/legend>\n      <div class=\"grid2\">\n        <div class=\"fld\"><label>Suelo<\/label><select id=\"r_floor\">${opt(matsFor('suelo'),r.floorMat)}<\/select><\/div>\n        <div class=\"fld\"><label>Techo<\/label><select id=\"r_ceil\">${opt(matsFor('techo'),r.ceilMat)}<\/select>\n          <p class=\"hint\">Selecci\u00f3n propia de materiales de techo (configurable en Zona interna \u2192 Fichas t\u00e9cnicas, campo \u00abAplicaci\u00f3n\u00bb).<\/p><\/div>\n      <\/div>\n      <label>Paredes<\/label>\n      <div id=\"wallList\"><\/div>\n      <button class=\"btn btn-sm\" type=\"button\" onclick=\"genWalls();renderWalls()\">\u21bb Generar paredes desde la planta<\/button>\n    <\/fieldset>\n\n    <fieldset><legend>Elementos singulares (opcional)<\/legend>\n      <p class=\"hint\">A\u00f1ade ventanales, puertas, mobiliario u otros elementos con su superficie aproximada.<\/p>\n      <div id=\"elemList\"><\/div>\n      <button class=\"btn btn-sm\" type=\"button\" onclick=\"addElem()\">+ A\u00f1adir elemento<\/button>\n    <\/fieldset>\n\n    <div class=\"actions\">\n      <button class=\"btn\" onclick=\"goStep(2)\">\u2190 Atr\u00e1s<\/button>\n      <button class=\"btn btn-primary\" onclick=\"saveRoom()\">Calcular \u2192<\/button>\n    <\/div>\n  <\/div>`;\n}\nfunction bindRoom(){\n  document.getElementById('rectBox')?.classList.toggle('hide',S.room.shapeMode!=='rect');\n  document.getElementById('polyBox')?.classList.toggle('hide',S.room.shapeMode!=='poly');\n  if(S.room.shapeMode==='poly')drawPolyEditor();\n  if(!S.room.walls.length)genWalls();\n  renderWalls();renderElems();\n}\n\/* --- Editor de pol\u00edgono (SVG con cuadr\u00edcula clicable) --- *\/\nconst PED={w:560,h:420,cell:20};\nfunction drawPolyEditor(){\n  const wrap=document.getElementById('polyWrap');if(!wrap)return;\n  const {w,h,cell}=PED;\n  let lines='';\n  for(let x=0;x<=w;x+=cell)lines+=`<line class=\"grid-l\" x1=\"${x}\" y1=\"0\" x2=\"${x}\" y2=\"${h}\"\/>`;\n  for(let y=0;y<=h;y+=cell)lines+=`<line class=\"grid-l\" x1=\"0\" y1=\"${y}\" x2=\"${w}\" y2=\"${y}\"\/>`;\n  const pts=S.room.poly;\n  const poly=pts.length>1?`<polyline points=\"${pts.map(p=>p.px+','+p.py).join(' ')}\" fill=\"rgba(14,124,123,.10)\" stroke=\"var(--brand)\" stroke-width=\"2\"\/>`:'';\n  const verts=pts.map((p,i)=>`<circle cx=\"${p.px}\" cy=\"${p.py}\" r=\"5\" fill=\"var(--brand)\"\/><text x=\"${p.px+7}\" y=\"${p.py-7}\" font-size=\"11\" fill=\"var(--brand-d)\">${i+1}<\/text>`).join('');\n  \/\/ Etiqueta de cada lado ya trazado, en cm\n  const edgeLbls=pts.map((p,i)=>{\n    if(i===0)return'';\n    const a=pts[i-1];\n    const mx=(a.px+p.px)\/2,my=(a.py+p.py)\/2;\n    const cm=Math.round(Math.hypot(p.x-a.x,p.y-a.y)*100);\n    return `<rect x=\"${mx-20}\" y=\"${my-11}\" width=\"40\" height=\"15\" rx=\"3\" fill=\"#fff\" opacity=\"0.85\"\/><text x=\"${mx}\" y=\"${my}\" font-size=\"10\" text-anchor=\"middle\" fill=\"var(--brand-d)\">${cm} cm<\/text>`;\n  }).join('');\n  wrap.innerHTML=`<svg id=\"pedSvg\" viewBox=\"0 0 ${w} ${h}\" width=\"100%\" style=\"display:block;cursor:crosshair\">\n    ${lines}${poly}${edgeLbls}${verts}\n    <line id=\"pedPrev\" stroke=\"var(--accent)\" stroke-width=\"2\" stroke-dasharray=\"5 4\" style=\"display:none\"\/>\n    <circle id=\"pedPrevDot\" r=\"4\" fill=\"var(--accent)\" style=\"display:none\"\/>\n    <rect id=\"pedPrevBg\" rx=\"3\" fill=\"var(--accent)\" style=\"display:none\"\/>\n    <text id=\"pedPrevLbl\" font-size=\"11\" font-weight=\"700\" fill=\"#fff\" style=\"display:none\"><\/text>\n  <\/svg>`;\n  const svg=document.getElementById('pedSvg');\n  \/\/ Snap a CENT\u00cdMETRO (no a cuadro): convierte px\u2192metros, redondea a 0,01 m, vuelve a px.\n  const snap=ev=>{const r=svg.getBoundingClientRect();const sx=PED.w\/r.width,sy=PED.h\/r.height;\n    const mx=Math.round((ev.clientX-r.left)*sx\/cell*S.room.scale*100)\/100;\n    const my=Math.round((ev.clientY-r.top)*sy\/cell*S.room.scale*100)\/100;\n    return{x:mx,y:my,px:mx\/S.room.scale*cell,py:my\/S.room.scale*cell};};\n  \/\/ Per\u00edmetro ya trazado (m)\n  const perimSoFar=()=>{let p=0;for(let i=1;i<pts.length;i++)p+=Math.hypot(pts[i].x-pts[i-1].x,pts[i].y-pts[i-1].y);return p;};\n  svg.addEventListener('click',ev=>{\n    const p=snap(ev);\n    S.room.poly.push({px:p.px,py:p.py,x:p.x,y:p.y});\n    drawPolyEditor();updatePolyInfo();\n  });\n  \/\/ Medida en vivo en cm mientras se mueve el cursor (segmento actual + total acumulado)\n  svg.addEventListener('mousemove',ev=>{\n    if(!pts.length)return;\n    const p=snap(ev), last=pts[pts.length-1];\n    const segCm=Math.round(Math.hypot(p.x-last.x,p.y-last.y)*100);\n    const totalCm=Math.round((perimSoFar()+segCm\/100)*100);\n    const line=svg.querySelector('#pedPrev'),dot=svg.querySelector('#pedPrevDot');\n    const bg=svg.querySelector('#pedPrevBg'),lbl=svg.querySelector('#pedPrevLbl');\n    line.setAttribute('x1',last.px);line.setAttribute('y1',last.py);\n    line.setAttribute('x2',p.px);line.setAttribute('y2',p.py);line.style.display='block';\n    dot.setAttribute('cx',p.px);dot.setAttribute('cy',p.py);dot.style.display='block';\n    const txt=segCm+' cm';lbl.textContent=txt;\n    const bx=p.px+10,by=p.py-10,bw=txt.length*7+10;\n    bg.setAttribute('x',bx-4);bg.setAttribute('y',by-12);bg.setAttribute('width',bw);bg.setAttribute('height',16);bg.style.display='block';\n    lbl.setAttribute('x',bx);lbl.setAttribute('y',by);lbl.style.display='block';\n    const info=document.getElementById('polyInfo');\n    if(info)info.innerHTML=`Segmento: <b>${segCm} cm<\/b> \u00b7 per\u00edmetro acumulado: <b>${totalCm} cm<\/b> (${f(totalCm\/100)} m)`;\n  });\n  const hide=()=>{['#pedPrev','#pedPrevDot','#pedPrevBg','#pedPrevLbl'].forEach(s=>{const e=svg.querySelector(s);if(e)e.style.display='none';});updatePolyInfo();};\n  svg.addEventListener('mouseleave',hide);\n  updatePolyInfo();\n}\nfunction updatePolyInfo(){\n  const el=document.getElementById('polyInfo');if(!el)return;\n  const pts=S.room.poly;\n  if(pts.length<2){el.textContent='Marca las esquinas con el cursor (la medida en cm aparece al moverlo). M\u00edn. 3 esquinas.';return;}\n  let per=0;for(let i=1;i<pts.length;i++)per+=Math.hypot(pts[i].x-pts[i-1].x,pts[i].y-pts[i-1].y);\n  let txt=`Per\u00edmetro acumulado: <b>${Math.round(per*100)} cm<\/b> (${f(per)} m) \u00b7 esquinas: ${pts.length}`;\n  if(pts.length>=3)txt+=` \u00b7 Superficie \u2248 <b>${f(shoelaceArea(pts))} m\u00b2<\/b>`;\n  el.innerHTML=txt;\n}\nfunction closePoly(){\n  if(S.room.poly.length<3){alert('Marca al menos 3 esquinas.');return;}\n  S.room.height=parseFloat(val('r_h2'))||S.room.height;\n  genWalls();renderWalls();updatePolyInfo();\n  alert('Planta cerrada. Se han generado las paredes; as\u00edgnales material abajo.');\n}\n\/* --- Geometr\u00eda \u2192 paredes --- *\/\nfunction currentGeometry(){\n  const r=S.room;\n  if(r.shapeMode==='rect'){\n    const L=parseFloat(val('r_l'))||r.rectL, W=parseFloat(val('r_w'))||r.rectW, H=parseFloat(val('r_h1'))||r.height;\n    r.rectL=L;r.rectW=W;r.height=H;\n    return{area:L*W, edges:[{len:L},{len:W},{len:L},{len:W}], H, volume:L*W*H};\n  }else{\n    const pts=r.poly, H=parseFloat(val('r_h2'))||r.height; r.height=H;\n    if(pts.length<3)return{area:0,edges:[],H,volume:0};\n    const area=shoelaceArea(pts), edges=perimeterEdges(pts);\n    return{area,edges,H,volume:area*H};\n  }\n}\nfunction genWalls(){\n  const g=currentGeometry();\n  const prev=S.room.walls;\n  S.room.walls=g.edges.map((e,i)=>({name:'Pared '+(i+1),len:e.len,mat:(prev[i]?.mat)||'p02'}));\n}\n\/\/ En modo rectangular: al fijar largo\/ancho\/altura, rellena las paredes con esas longitudes.\nfunction syncRectWalls(){\n  if(S.room.shapeMode!=='rect')return;\n  genWalls();renderWalls();\n}\nfunction renderWalls(){\n  const el=document.getElementById('wallList');if(!el)return;\n  const mats=matsFor('pared');\n  const opt=sel=>mats.map(m=>`<option value=\"${m.id}\" ${m.id===sel?'selected':''}>${esc(m.name)}<\/option>`).join('');\n  el.innerHTML=`<div class=\"wallrow\" style=\"font-size:.74rem;color:var(--muted)\"><span>Pared<\/span><span>Longitud (m)<\/span><span>Material<\/span><span><\/span><\/div>`+\n    S.room.walls.map((w,i)=>`<div class=\"wallrow\">\n      <input value=\"${esc(w.name)}\" onchange=\"S.room.walls[${i}].name=this.value\">\n      <input type=\"number\" step=\"0.01\" value=\"${+w.len.toFixed(4)}\" onchange=\"S.room.walls[${i}].len=parseFloat(this.value)||0\">\n      <select onchange=\"S.room.walls[${i}].mat=this.value\">${opt(w.mat)}<\/select>\n      <button class=\"btn btn-sm btn-danger\" onclick=\"S.room.walls.splice(${i},1);renderWalls()\">\u2715<\/button>\n    <\/div>`).join('')||'<p class=\"hint\">Genera las paredes desde la planta.<\/p>';\n}\nconst ELEM_TYPES=['Ventana simple','Ventana doble acristalamiento','Puerta de madera','Puerta met\u00e1lica','Cuadro \/ Panel decorativo','Cortina ligera','Cortina pesada (terciopelo)','Armario \/ Estanter\u00eda','Pizarra \/ Pantalla','Muebles tapizados','Otros'];\nfunction addElem(){const t='Ventana simple';S.room.elements.push({name:t,area:2,mat:ELEM_TYPE_MAT[t],wall:'libre'});renderElems();}\nfunction wallOptions(sel){\n  const walls=S.room.walls.map((w,i)=>`<option value=\"pared_${i}\" ${sel==='pared_'+i?'selected':''}>Pared ${i+1} (${f(w.len)} m)<\/option>`).join('');\n  return `<option value=\"suelo\" ${sel==='suelo'?'selected':''}>Suelo<\/option><option value=\"techo\" ${sel==='techo'?'selected':''}>Techo<\/option>${walls}<option value=\"libre\" ${sel==='libre'?'selected':''}>Sin asignar<\/option>`;\n}\n\/\/ El TIPO define el material (ELEM_TYPE_MAT). El material es informativo, no editable directamente.\nfunction setElemType(i,type){S.room.elements[i].name=type;S.room.elements[i].mat=ELEM_TYPE_MAT[type]||'p02';renderElems();}\nfunction renderElems(){\n  const el=document.getElementById('elemList');if(!el)return;\n  const optType=sel=>ELEM_TYPES.map(t=>`<option value=\"${t}\" ${t===sel?'selected':''}>${esc(t)}<\/option>`).join('');\n  el.innerHTML=`<div style=\"display:grid;grid-template-columns:1.4fr .6fr 1.3fr 1fr auto;gap:.4rem;font-size:.72rem;color:var(--muted);margin-bottom:.3rem\"><span>Tipo (define el material)<\/span><span>\u00c1rea m\u00b2<\/span><span>Material asignado<\/span><span>Superficie<\/span><span><\/span><\/div>`+\n  S.room.elements.map((e,i)=>{\n    const m=matById(e.mat||ELEM_TYPE_MAT[e.name]||'p02');\n    return `<div style=\"display:grid;grid-template-columns:1.4fr .6fr 1.3fr 1fr auto;gap:.4rem;align-items:center;margin-bottom:.4rem\">\n    <select onchange=\"setElemType(${i},this.value)\">${optType(e.name)}<\/select>\n    <input type=\"number\" step=\"0.01\" value=\"${e.area}\" onchange=\"S.room.elements[${i}].area=parseFloat(this.value)||0\">\n    <span style=\"font-size:.78rem;color:var(--muted);padding:.2rem .4rem;background:#f3f6f6;border-radius:6px\">${m?esc(m.name):'\u2014'}<\/span>\n    <select onchange=\"S.room.elements[${i}].wall=this.value\">${wallOptions(e.wall||'libre')}<\/select>\n    <button class=\"btn btn-sm btn-danger\" onclick=\"S.room.elements.splice(${i},1);renderElems()\">\u2715<\/button>\n  <\/div>`;}).join('')||'<p class=\"hint\">Ning\u00fan elemento a\u00f1adido.<\/p>';\n}\nfunction buildSurfaces(){\n  const g=currentGeometry();\n  const surf=[];\n  const add=(area,matId)=>{const m=matById(matId);if(m&&area>0)surf.push({area,a:m.a,name:m.name});};\n  add(g.area,S.room.floorMat);\n  add(g.area,S.room.ceilMat);\n  S.room.walls.forEach(w=>add(w.len*g.H,w.mat));\n  S.room.elements.forEach(e=>add(e.area,e.mat));\n  return{surfaces:surf,volume:g.volume,area:g.area,H:g.H};\n}\nfunction saveRoom(){\n  S.room.type=document.getElementById('r_type')?val('r_type'):S.room.type;S.room.sector=val('r_sector');S.room.occ=val('r_occ');\n  S.room.floorMat=val('r_floor');S.room.ceilMat=val('r_ceil');\n  const g=buildSurfaces();\n  if(g.volume<=0){alert('Define las medidas de la sala (volumen mayor que 0).');return;}\n  if(!g.surfaces.length){alert('Faltan superficies. Genera las paredes y asigna materiales.');return;}\n  \/\/ Veredicto DB-HR: SIEMPRE en vac\u00edo seg\u00fan norma. OCUPADO usa VACIO como base de veredicto.\n  const occVerdict=S.room.occ==='VACIO_CON_BUTACAS'?'VACIO_CON_BUTACAS':'VACIO';\n  S.result=calcular(g.volume,S.room.type,occVerdict,g.surfaces,{k:K_SABINE});\n  S.geom=g;\n  \/\/ Estimaci\u00f3n informativa ocupada (no cambia el veredicto)\n  if(S.room.occ==='OCUPADO'){\n    const occA=occupancyAbsorption();\n    const occSurf=g.surfaces.concat([{area:1,a:occA,name:'Ocupaci\u00f3n (mobiliario y personas)'}]);\n    S.resultOcc=calcular(g.volume,S.room.type,occVerdict,occSurf,{k:K_SABINE});\n  }else S.resultOcc=null;\n  buildProposal();\n  computeImproved();\n  goStep(4);\n}\n\n\/* ===== PROPUESTA: sistema m\u00e1s ventajoso que cierra el d\u00e9ficit ===== *\/\n\/* Soluci\u00f3n de partida de alta absorci\u00f3n (Clase A, \u03b1w \u2248 0,95) *\/\nconst SOLUTION_A095={name:'Soluci\u00f3n ac\u00fastica de alta absorci\u00f3n (Clase A \u00b7 \u03b1w 0,95)', a:[0.50,0.85,0.95,1.00,1.00,0.95], aw:0.95};\nfunction computeImproved(){\n  const g=S.geom,res=S.result; if(!g||!res){S.improved=null;S.improvedArea=0;return;}\n  const amA=(SOLUTION_A095.a[2]+SOLUTION_A095.a[3]+SOLUTION_A095.a[4])\/3; \/\/ \u22480,983\n  const totalSurf=g.surfaces.reduce((s,x)=>s+x.area,0);\n  let areaTreat;\n  if(res.absBased){\n    const need=Math.max(0,(0.2*g.volume)-res.eqAbs);\n    areaTreat=need>0?need\/amA:g.area*0.5;\n  }else{\n    const Aneed=0.16*g.volume\/res.limit.rtLimit;   \/\/ absorci\u00f3n media necesaria\n    const Acur=0.16*g.volume\/res.rtMean;            \/\/ absorci\u00f3n media actual\n    const deficit=Aneed-Acur;\n    areaTreat=deficit>0?deficit\/amA:g.area*0.4;      \/\/ si ya cumple, tratar 40% del techo (mejora de confort)\n  }\n  areaTreat=Math.max(1,Math.min(areaTreat,totalSurf)); \/\/ f\u00edsicamente acotado\n  const surf=g.surfaces.concat([{area:areaTreat,a:SOLUTION_A095.a,name:SOLUTION_A095.name}]);\n  S.improved=calcular(g.volume,S.room.type,S.room.occ,surf,{k:K_SABINE});\n  S.improvedArea=areaTreat;\n}\n\nfunction buildProposal(){\n  const res=S.result;\n  if(!res||res.compliant!==false||res.deficitM2<=0){S.proposal=null;S.chosenSystem=null;return;}\n  const sys=systems().map(sy=>{\n    const m=matById(sy.materialId); if(!m)return null;\n    const am=meanReverb({500:m.a[2],1000:m.a[3],2000:m.a[4]});\n    if(am<=0)return null;\n    const areaNeeded=res.deficitM2\/am;\n    const cost=areaNeeded*(m.price||0);\n    return{sys:sy,mat:m,am,areaNeeded,cost};\n  }).filter(Boolean).sort((a,b)=>a.cost-b.cost);\n  if(!sys.length){S.proposal=null;return;}\n  S.proposal=sys;\n  S.chosenSystem=sys[0]; \/\/ por defecto, el m\u00e1s ventajoso (menor coste)\n  computeProposed();\n}\nfunction computeProposed(){\n  if(!S.chosenSystem){S.proposed=null;return;}\n  const ch=S.chosenSystem, g=S.geom;\n  \/\/ A\u00f1ade una superficie absorbente con el material elegido del \u00e1rea propuesta.\n  const surf=g.surfaces.concat([{area:ch.areaNeeded,a:ch.mat.a,name:ch.mat.name}]);\n  S.proposed=calcular(g.volume,S.room.type,S.room.occ,surf,{k:K_SABINE});\n}\n\n\/* ===== PASO 4: RESULTADO (Propuesta C \u2014 escala de confort) ===== *\/\nfunction stepResult(c){\n  const res=S.result, g=S.geom, imp=S.improved;\n  const isRT=!res.absBased;\n  \/\/ Objetivo en RT: si es por absorci\u00f3n (A\u22650,2\u00b7V) equivale a RT\u22640,8 s\n  const targetRT=isRT?res.limit.rtLimit:0.8;\n  const nowRT=res.rtMean, impRT=imp?imp.rtMean:null;\n  const nowOk=res.compliant===true;\n  const impOk=imp?(imp.rtMean<=targetRT):true;\n  const qNow=acousticParams(g.volume,nowRT,targetRT);\n  const qImp=imp?acousticParams(g.volume,impRT,targetRT):null;\n  \/\/ Escala: el objetivo queda hacia el centro; los marcadores siempre dentro\n  const maxScale=Math.max(nowRT*1.12, targetRT*2.2, (impRT||0)*1.12)||1;\n  const pos=v=>Math.max(6,Math.min(94,(v\/maxScale)*100));\n  const nowP=pos(nowRT), impP=imp?pos(impRT):0, limP=pos(targetRT);\n\n  let html=`<div class=\"card\">\n    <h1>Tu resultado<\/h1>\n    <p class=\"lead\">${useLabel(S.room.type)} \u00b7 ${f(g.volume)} m\u00b3 \u00b7 planta ${f(g.area)} m\u00b2<\/p>\n\n    <style>\n      .scaleWrap{margin:1.2rem 0 .6rem}\n      .scaleBar{position:relative;height:30px;border-radius:8px;background:linear-gradient(90deg,#1f8a52 0%,#7cc04a 30%,#FFC000 55%,#e0913f 75%,#c2452e 100%)}\n      .pin{position:absolute;transform:translateX(-50%);text-align:center;font-size:.74rem;font-weight:700;white-space:nowrap;z-index:2}\n      .pin .dot{display:block;width:0;height:0;margin:0 auto;border-left:7px solid transparent;border-right:7px solid transparent}\n      .pin.now{top:-34px;color:var(--bad)}\n      .pin.now .dot{margin-top:3px;border-top:9px solid var(--bad)}\n      .pin.aft{top:34px;color:var(--brand-d,var(--brand))}\n      .pin.aft .dot{margin-bottom:3px;border-bottom:9px solid var(--brand)}\n      .limmark{position:absolute;top:-5px;bottom:-5px;width:0;border-left:2px dashed var(--ink);z-index:1}\n      .limlbl{position:absolute;top:-22px;transform:translateX(-50%);font-size:.68rem;color:var(--ink);font-weight:700;white-space:nowrap}\n      .scaleLabels{display:flex;justify-content:space-between;font-size:.72rem;color:var(--muted);margin-top:.55rem}\n      .verdict2{display:flex;gap:.7rem;flex-wrap:wrap;margin-top:1.5rem}\n      .vchip{flex:1;min-width:160px;border-radius:13px;padding:.9rem 1.05rem;border:1.5px solid var(--line)}\n      .vchip .t{font-size:.7rem;text-transform:uppercase;letter-spacing:.05em;font-weight:700;color:var(--muted)}\n      .vchip .b{font-size:1.45rem;font-weight:800;margin-top:.15rem}\n      .vchip.now{background:var(--bad-bg)}.vchip.now.ok{background:var(--ok-bg)}\n      .vchip.now .b{color:var(--bad)}.vchip.now.ok .b{color:var(--ok)}\n      .vchip.aft{background:var(--brand-soft,#e4f0ef);border-color:var(--brand)}.vchip.aft .b{color:var(--brand-d,var(--brand))}\n      .qb{display:inline-block;padding:.12rem .5rem;border-radius:999px;color:#fff;font-weight:700;font-size:.74rem}\n      .reco{display:flex;gap:.8rem;align-items:flex-start;background:linear-gradient(160deg,var(--brand-soft,#e4f0ef),#fff);border:1.5px solid var(--brand);border-radius:14px;padding:1rem 1.1rem;margin-top:1.3rem}\n      .reco .ico{font-size:1.6rem;line-height:1}\n      .reco b{color:var(--brand-d,var(--brand))}\n    <\/style>\n\n    <div class=\"scaleWrap\">\n      <div class=\"scaleBar\">\n        <div class=\"pin now\" style=\"left:${nowP}%\">Ahora ${f3(nowRT)} s<span class=\"dot\"><\/span><\/div>\n        ${imp?`<div class=\"pin aft\" style=\"left:${impP}%\"><span class=\"dot\"><\/span>Con tratamiento ${f3(impRT)} s<\/div>`:''}\n        <div class=\"limmark\" style=\"left:${limP}%\"><\/div>\n        <div class=\"limlbl\" style=\"left:${limP}%; top:${imp?'64px':'-22px'}\">Objetivo ${f3(targetRT)} s<\/div>\n      <\/div>\n      <div class=\"scaleLabels\"><span>\u25c0 Confort (poco eco)<\/span><span>Reverberante (mucho eco) \u25b6<\/span><\/div>\n    <\/div>\n\n    <div class=\"verdict2\">\n      <div class=\"vchip now ${nowOk?'ok':''}\">\n        <div class=\"t\">Ahora<\/div>\n        <div class=\"b\">${nowOk?'\u2705 Cumple':'\u26a0\ufe0f No cumple'}<\/div>\n        <div class=\"hint\">RT ${f3(nowRT)} s ${qNow?`\u00b7 <span class=\"qb\" style=\"background:${qNow.qColor}\">${qNow.quality}<\/span>`:''}<\/div>\n      <\/div>\n      <div class=\"vchip aft\">\n        <div class=\"t\">Con tratamiento ac\u00fastico<\/div>\n        <div class=\"b\">${impOk?'\u2705 Confort \u00f3ptimo':'\u2705 Gran mejora'}<\/div>\n        <div class=\"hint\">RT ${imp?f3(impRT):'\u2014'} s ${qImp?`\u00b7 <span class=\"qb\" style=\"background:${qImp.qColor}\">${qImp.quality}<\/span>`:''}<\/div>\n      <\/div>\n    <\/div>\n\n    <div class=\"reco\">\n      <div class=\"ico\">\ud83c\udfaf<\/div>\n      <div>\n        <div style=\"font-weight:800;margin-bottom:.15rem\">Soluci\u00f3n recomendada<\/div>\n        <div>Tratamiento ac\u00fastico de <b>alta absorci\u00f3n (Clase A \u00b7 \u03b1w 0,95)<\/b>.<\/div>\n        <div class=\"hint\" style=\"margin-top:.3rem\">${imp?`Reduce la reverberaci\u00f3n de ${f3(nowRT)} s a ${f3(impRT)} s.`:''} El equipo comercial concretar\u00e1 el sistema (techo, paneles, baffles\u2026) y la propuesta en la oferta.<\/div>\n      <\/div>\n    <\/div>\n\n    <div style=\"margin-top:1.4rem\">\n      ${acousticParamsHTML(g.volume,nowRT,targetRT)}\n      ${paramBars(g.volume,nowRT,impRT,targetRT)}\n      ${isoTableClient(\n        (function(){const t=g.surfaces.reduce((s,x)=>s+x.area,0);return t>0?g.surfaces.reduce((s,x)=>s+x.area*((x.a[2]+x.a[3]+x.a[4])\/3),0)\/t:0;})(),\n        SOLUTION_A095.aw)}\n    <\/div>\n  <\/div>`;\n\n  \/\/ Observaciones (simple)\n  html+=`<div class=\"card\"><h2>\u00bfAlgo que a\u00f1adir?<\/h2>\n    <textarea id=\"obsField\" rows=\"2\" style=\"width:100%;border:1px solid var(--line);border-radius:10px;padding:.7rem;font:inherit;resize:vertical\" placeholder=\"Comentarios o necesidades especiales (est\u00e9tica, plazos, montaje\u2026)\" oninput=\"S.observations=this.value\">${esc(S.observations||'')}<\/textarea>\n  <\/div>`;\n\n  html+=`<div class=\"card\"><h2>Enviar al equipo comercial<\/h2>\n    <p class=\"muted\">Recibir\u00e1s una propuesta personalizada en tu correo (${esc(S.client.email)}).<\/p>\n    <div class=\"actions\">\n      <div style=\"display:flex;gap:.6rem;flex-wrap:wrap\">\n        <button class=\"btn\" onclick=\"goStep(3)\">\u2190 Ajustar datos<\/button>\n        ${S.planHasMultipleRooms?`<button class=\"btn\" onclick=\"S.room=blankProject().room;S.result=null;S.proposal=null;S.improved=null;goStep(3)\">\u2795 Otra estancia del plano<\/button>`:''}\n      <\/div>\n      <button class=\"btn btn-primary\" onclick=\"sendToCommercial()\">\ud83d\udce8 Enviar valoraci\u00f3n<\/button>\n    <\/div><\/div>`;\n  c.innerHTML=html;\n}\nfunction bandTable(r){\n  return `<table><thead><tr><th>Banda<\/th><th class=\"num\">Absorci\u00f3n A (m\u00b2)<\/th><th class=\"num\">Reverberaci\u00f3n T (s)<\/th><\/tr><\/thead><tbody>`+\n    BANDS.map((b,i)=>`<tr><td>${b} Hz<\/td><td class=\"num\">${f(r.A[b])}<\/td><td class=\"num\">${f3(r.T[b])}<\/td><\/tr>`).join('')+\n    `<\/tbody><\/table>`;\n}\n\n\/* Par\u00e1metros ac\u00fasticos adicionales \u2014 estimaciones campo difuso *\/\nfunction acousticParams(V,RT60,rtLimit){\n  if(!isFinite(RT60)||RT60<=0)return null;\n  const D50=Math.max(0,Math.min(1,1-Math.exp(-0.69\/RT60)));\n  const C50=10*Math.log10(D50\/Math.max(1-D50,1e-9));\n  const G=10*Math.log10(Math.max(1e-9,6.82*RT60\/V));\n  \/\/ %ALcons Peutz campo reverberante; STI informativo\n  const ALcons=Math.min(33,200*RT60*RT60\/V);\n  const STI=Math.max(0,Math.min(1,1-ALcons\/33));\n  \/\/ Calidad basada en margen respecto al l\u00edmite DB-HR; D50 como criterio secundario\n  let quality,qColor;\n  if(rtLimit!=null){\n    const ratio=RT60\/rtLimit;\n    if(ratio<=0.70){quality='Excelente';qColor='#00B050';}\n    else if(ratio<=0.90){quality='Bueno';qColor='#70AD47';}\n    else if(ratio<=1.00){quality='Aceptable';qColor='#FFC000';}\n    else{quality='Deficiente';qColor='#d94b46';}\n  }else{\n    if(D50>=0.50){quality='Excelente';qColor='#00B050';}\n    else if(D50>=0.35){quality='Bueno';qColor='#70AD47';}\n    else if(D50>=0.20){quality='Aceptable';qColor='#FFC000';}\n    else{quality='Deficiente';qColor='#d94b46';}\n  }\n  return{D50,C50,G,STI,ALcons,quality,qColor};\n}\n\/* Interpretaci\u00f3n en lenguaje claro de cada par\u00e1metro ac\u00fastico *\/\nfunction interpD50(d){const v=d*100;if(v<30)return['Baja','el habla se entiende con dificultad'];if(v<50)return['Media','definici\u00f3n aceptable de la palabra'];return['Buena','palabra clara y definida'];}\nfunction interpSTI(s){if(s<0.45)return['Mala','cuesta entender lo que se dice'];if(s<0.60)return['Aceptable','inteligibilidad justa'];if(s<0.75)return['Buena','se entiende bien'];return['Excelente','inteligibilidad \u00f3ptima'];}\nfunction interpC50(c){if(c<-2)return['Baja','poca claridad para la palabra'];if(c<2)return['Media','claridad intermedia'];return['Buena','sonido claro y definido'];}\nfunction interpG(g){if(g>4)return['Alta','el sonido se acumula y retumba'];if(g>0)return['Media','refuerzo moderado del sonido'];return['Baja','poco refuerzo del sonido'];}\n\/\/ \u00bfCu\u00e1ntas veces es mayor el RT actual frente al objetivo?\nfunction rtRatio(RT,target){return (target&&target>0)?RT\/target:null;}\nfunction rtRatioHTML(RT,target){\n  const r=rtRatio(RT,target);if(r==null)return'';\n  if(r<=1.0)return `<div class=\"ratio-ok\">\u2705 Tu reverberaci\u00f3n (${f3(RT)} s) est\u00e1 dentro del objetivo (${f3(target)} s).<\/div>`;\n  const v=(Math.round(r*10)\/10+'').replace('.',',');\n  return `<div class=\"ratio-bad\"><span class=\"big\">${v}\u00d7<\/span><div><b>Tu reverberaci\u00f3n es ${v} veces mayor de lo que deber\u00eda.<\/b><br><span class=\"hint\">Actual ${f3(RT)} s \u00b7 objetivo ${f3(target)} s. Cuanto m\u00e1s alto, m\u00e1s eco y peor se entiende la palabra.<\/span><\/div><\/div>`;\n}\nfunction acousticParamsHTML(V,RT60,rtLimit){\n  const p=acousticParams(V,RT60,rtLimit);if(!p)return'';\n  const d=interpD50(p.D50),s=interpSTI(p.STI),cc=interpC50(p.C50),gg=interpG(p.G);\n  return `<div style=\"margin-top:1rem\">\n    <style>\n      .pcap{font-size:.68rem;color:var(--muted);margin-top:.3rem;line-height:1.35}\n      .ptag{font-weight:700;color:var(--ink)}\n      .ratio-bad{display:flex;gap:.9rem;align-items:center;background:var(--bad-bg);border:1.5px solid #eecac3;border-radius:13px;padding:.9rem 1.1rem;margin:.2rem 0 1rem}\n      .ratio-bad .big{font-size:2.1rem;font-weight:800;color:var(--bad);line-height:1;flex-shrink:0}\n      .ratio-ok{background:var(--ok-bg);border:1.5px solid #b6e2c8;border-radius:13px;padding:.75rem 1rem;margin:.2rem 0 1rem;font-weight:600;color:var(--ok)}\n    <\/style>\n    <h3 style=\"font-size:.95rem;margin-bottom:.6rem\">Par\u00e1metros ac\u00fasticos \u2014 qu\u00e9 significan<\/h3>\n    ${rtRatioHTML(RT60,rtLimit)}\n    <div class=\"kpi\">\n      <div class=\"k\" style=\"border-left:3px solid ${p.qColor}\">\n        <div class=\"v\" style=\"color:${p.qColor}\">${p.quality}<\/div><div class=\"l\">Calidad sonora global<\/div>\n        <div class=\"pcap\">Valoraci\u00f3n general del confort ac\u00fastico de la sala.<\/div><\/div>\n      <div class=\"k\"><div class=\"v\">${Math.round(p.D50*100)}%<\/div><div class=\"l\">Definici\u00f3n (D50)<\/div>\n        <div class=\"pcap\"><span class=\"ptag\">${d[0]}<\/span> \u2014 ${d[1]}.<\/div><\/div>\n      <div class=\"k\"><div class=\"v\">${p.C50>=0?'+':''}${Math.round(p.C50*10)\/10} dB<\/div><div class=\"l\">Claridad (C50)<\/div>\n        <div class=\"pcap\"><span class=\"ptag\">${cc[0]}<\/span> \u2014 ${cc[1]}.<\/div><\/div>\n      <div class=\"k\"><div class=\"v\">${p.G>=0?'+':''}${Math.round(p.G*10)\/10} dB<\/div><div class=\"l\">Fuerza sonora (G)<\/div>\n        <div class=\"pcap\"><span class=\"ptag\">${gg[0]}<\/span> \u2014 ${gg[1]}.<\/div><\/div>\n      <div class=\"k\"><div class=\"v\">${Math.round(p.STI*100)\/100}<\/div><div class=\"l\">Inteligibilidad (STI)<\/div>\n        <div class=\"pcap\"><span class=\"ptag\">${s[0]}<\/span> \u2014 ${s[1]}.<\/div><\/div>\n    <\/div>\n    <p class=\"hint\">Estimaciones en campo difuso (Sabine + Peutz). Para resultados precisos se requiere medici\u00f3n in situ.<\/p>\n  <\/div>`;\n}\n\/* Gr\u00e1fico de barras: \u00edndice de calidad (0\u2013100) por par\u00e1metro \u2014 antes y despu\u00e9s *\/\nfunction paramBars(V,nowRT,impRT,target){\n  const p=acousticParams(V,nowRT,target);if(!p)return'';\n  const pi=impRT?acousticParams(V,impRT,target):null;\n  function metrics(pp){\n    const qScore={'Excelente':92,'Bueno':75,'Aceptable':55,'Deficiente':30}[pp.quality]||50;\n    const d50=pp.D50*100, sti=pp.STI*100, c50n=Math.max(0,Math.min(100,(pp.C50+5)\/10*100));\n    const colD=d50<30?'#c2452e':d50<50?'#FFC000':'#1f8a52';\n    const colS=sti<45?'#c2452e':sti<60?'#FFC000':sti<75?'#70AD47':'#1f8a52';\n    const colC=pp.C50<-2?'#c2452e':pp.C50<2?'#FFC000':'#1f8a52';\n    return [\n      ['Calidad global',qScore,pp.qColor,pp.quality],\n      ['Definici\u00f3n (D50)',d50,colD,Math.round(d50)+'%'],\n      ['Claridad (C50)',c50n,colC,(pp.C50>=0?'+':'')+(Math.round(pp.C50*10)\/10)+' dB'],\n      ['Inteligibilidad (STI)',sti,colS,(Math.round(pp.STI*100)\/100)],\n    ];\n  }\n  const mNow=metrics(p), mImp=pi?metrics(pi):null;\n  const bar=(val,col,lab,txt)=>`<div style=\"display:flex;align-items:center;gap:.5rem;margin:.18rem 0\">\n    <div style=\"width:64px;font-size:.7rem;color:var(--muted);flex-shrink:0\">${lab}<\/div>\n    <div style=\"flex:1;background:#ececec;border-radius:6px;height:18px;overflow:hidden\"><div style=\"width:${Math.max(3,Math.min(100,val))}%;height:100%;background:${col};border-radius:6px;transition:width .4s\"><\/div><\/div>\n    <div style=\"width:76px;text-align:right;font-size:.78rem;font-weight:700;color:${col}\">${txt}<\/div>\n  <\/div>`;\n  const groups=mNow.map((r,i)=>{\n    const ri=mImp?mImp[i]:null;\n    return `<div style=\"margin:.75rem 0;padding-bottom:.5rem;border-bottom:1px solid var(--line)\">\n      <div style=\"font-size:.82rem;font-weight:700;margin-bottom:.25rem\">${r[0]}<\/div>\n      ${bar(r[1],r[2],'Ahora',r[3])}\n      ${ri?bar(ri[1],ri[2],'Con trat.',ri[3]):''}\n    <\/div>`;\n  }).join('');\n  return `<h3 style=\"font-size:.95rem;margin:1.4rem 0 .6rem\">Calidad ac\u00fastica por par\u00e1metro${mImp?' \u2014 antes y despu\u00e9s':''}<\/h3>\n    ${groups}\n    <p class=\"hint\" style=\"margin-top:.6rem\">\u00cdndice de calidad para la palabra (0\u2013100): cuanto m\u00e1s larga y verde la barra, mejor.${mImp?' \u00abAhora\u00bb es el estado actual y \u00abCon trat.\u00bb con la soluci\u00f3n de alta absorci\u00f3n (Clase A \u00b7 \u03b1w 0,95).':''} La \u00abFuerza sonora (G)\u00bb se muestra en las tarjetas.<\/p>`;\n}\nfunction proposalCard(){\n  const ch=S.chosenSystem;\n  \/\/ Agrupar opciones por categor\u00eda de producto\n  const byCat={};\n  S.proposal.forEach((p,i)=>{const c=p.sys.cat||'Otros';(byCat[c]=byCat[c]||[]).push({p,i});});\n  const opts=SYSTEM_CATEGORIES.filter(c=>byCat[c]).map(c=>\n    `<optgroup label=\"${c}\">`+byCat[c].map(({p,i})=>{\n      const cls=isoClass(p.am);\n      return `<option value=\"${i}\" ${p===ch?'selected':''}>${esc(p.sys.name)} \u2014 Clase ${cls.clase} \u00b7 ${f(p.areaNeeded)} m\u00b2<\/option>`;\n    }).join('')+`<\/optgroup>`\n  ).join('');\n  const chCls=isoClass(ch.am);\n  return `<div class=\"card\">\n    <h2>Propuesta para cumplir el DB-HR<\/h2>\n    <p class=\"muted\">D\u00e9ficit de ${f(S.result.deficitM2)} m\u00b2 de absorci\u00f3n. Opci\u00f3n m\u00e1s ventajosa seleccionada por defecto:<\/p>\n    <div class=\"fld\" style=\"max-width:560px\"><label>Sistema \/ producto (cat\u00e1logo comercial)<\/label>\n      <select id=\"sysSel\" onchange=\"S.chosenSystem=S.proposal[this.value];computeProposed();stepResult(document.getElementById('steps'))\">${opts}<\/select>\n      <p class=\"hint\">Categor\u00eda: <b>${esc(ch.sys.cat||'\u2014')}<\/b> \u00b7 aplicaci\u00f3n: ${esc(ch.sys.surf==='pared'?'pared':'techo')}. Los productos se gestionan en Zona interna \u2192 Sistemas.<\/p><\/div>\n\n    <h3>Material propuesto<\/h3>\n    <table><thead><tr><th>Sistema \/ producto<\/th><th class=\"num\">\u03b1w estimado<\/th><th class=\"num\">Clase ISO<\/th><th class=\"num\">Superficie propuesta<\/th><\/tr><\/thead>\n    <tbody><tr>\n      <td>${esc(ch.sys.name)}<\/td>\n      <td class=\"num\">${f3(ch.am)}<\/td>\n      <td class=\"num\">${isoBadge(ch.am)}<\/td>\n      <td class=\"num\">${f(ch.areaNeeded)} m\u00b2<\/td>\n    <\/tr><\/tbody><\/table>\n    <p class=\"hint\">La valoraci\u00f3n econ\u00f3mica la elabora el equipo comercial seg\u00fan configuraci\u00f3n y condiciones.<\/p>\n    <p class=\"hint\" style=\"margin-top:.6rem\">${chCls.label} \u00b7 Id\u00f3neo para: ${chCls.uso}<\/p>\n\n    <h3>Estado actual vs. propuesto \u2014 por par\u00e1metro<\/h3>\n    <div id=\"paramCharts\"><\/div>\n\n    <h3>Esquema de disposici\u00f3n del material<\/h3>\n    <p class=\"hint\">Representaci\u00f3n a escala seg\u00fan el sistema elegido y las medidas de la sala.<\/p>\n    <div id=\"layout\"><\/div>\n\n    <h3>Vista 3D esquem\u00e1tica de la estancia<\/h3>\n    <div id=\"view3d\"><\/div>\n\n    <h3>Clasificaci\u00f3n ISO 11654 \u2014 referencia<\/h3>\n    <div style=\"overflow-x:auto\">${isoTableHTML()}<\/div>\n  <\/div>`;\n}\n\/* Gr\u00e1ficos por par\u00e1metro: RT por banda (curva actual vs propuesto) + barras G\/C50\/STI\/D50 *\/\nfunction drawParamCharts(){\n  const el=document.getElementById('paramCharts');if(!el||!S.proposed)return;\n  const cur=S.result,prop=S.proposed,lim=cur.limit.rtLimit,V=S.geom.volume;\n  \/\/ 1) Curva RT por banda (actual vs propuesto) \u2014 l\u00edneas\n  const w=560,h=220,pad=38;\n  const xb=i=>pad+i*((w-pad*2)\/(BANDS.length-1));\n  const maxT=Math.max(lim||0,...BANDS.map(b=>Math.max(cur.T[b]||0,prop.T[b]||0)))*1.15||1;\n  const yT=v=>h-pad-(Math.min(v,maxT)\/maxT)*(h-pad*2);\n  const lineCur=BANDS.map((b,i)=>`${xb(i)},${yT(cur.T[b]||0)}`).join(' ');\n  const lineProp=BANDS.map((b,i)=>`${xb(i)},${yT(prop.T[b]||0)}`).join(' ');\n  const limLine=lim!=null?`<line x1=\"${pad}\" y1=\"${yT(lim)}\" x2=\"${w-pad}\" y2=\"${yT(lim)}\" stroke=\"var(--accent)\" stroke-width=\"2\" stroke-dasharray=\"6 4\"\/><text x=\"${w-pad}\" y=\"${yT(lim)-5}\" font-size=\"11\" text-anchor=\"end\" fill=\"var(--accent)\">L\u00edmite ${f3(lim)} s<\/text>`:'';\n  const rtChart=`<svg viewBox=\"0 0 ${w} ${h}\" width=\"100%\" style=\"background:#fbfcfd;border:1px solid var(--line);border-radius:10px\">\n    <line x1=\"${pad}\" y1=\"${h-pad}\" x2=\"${w-pad}\" y2=\"${h-pad}\" stroke=\"var(--line)\"\/>\n    ${BANDS.map((b,i)=>`<text x=\"${xb(i)}\" y=\"${h-pad+15}\" font-size=\"10\" text-anchor=\"middle\" fill=\"var(--muted)\">${b}<\/text>`).join('')}\n    <polyline points=\"${lineCur}\" fill=\"none\" stroke=\"var(--bad)\" stroke-width=\"2.5\"\/>\n    <polyline points=\"${lineProp}\" fill=\"none\" stroke=\"var(--ok)\" stroke-width=\"2.5\"\/>\n    ${BANDS.map((b,i)=>`<circle cx=\"${xb(i)}\" cy=\"${yT(cur.T[b]||0)}\" r=\"3\" fill=\"var(--bad)\"\/><circle cx=\"${xb(i)}\" cy=\"${yT(prop.T[b]||0)}\" r=\"3\" fill=\"var(--ok)\"\/>`).join('')}\n    ${limLine}\n    <g font-size=\"11\"><rect x=\"${pad}\" y=\"6\" width=\"11\" height=\"11\" fill=\"var(--bad)\"\/><text x=\"${pad+16}\" y=\"15\" fill=\"var(--muted)\">Actual<\/text><rect x=\"${pad+70}\" y=\"6\" width=\"11\" height=\"11\" fill=\"var(--ok)\"\/><text x=\"${pad+86}\" y=\"15\" fill=\"var(--muted)\">Propuesto<\/text><\/g>\n  <\/svg>`;\n  \/\/ 2) Barras comparativas por par\u00e1metro ac\u00fastico (actual vs propuesto)\n  const pa=acousticParams(V,cur.rtMean,lim), pp=acousticParams(V,prop.rtMean,lim);\n  const params=[\n    {n:'RT60 (s)',a:cur.rtMean,p:prop.rtMean,inv:true,fmt:f3},\n    {n:'D50 Definici\u00f3n (%)',a:pa.D50*100,p:pp.D50*100,inv:false,fmt:v=>Math.round(v)},\n    {n:'C50 Claridad (dB)',a:pa.C50,p:pp.C50,inv:false,fmt:v=>Math.round(v*10)\/10,off:true},\n    {n:'STI Inteligibilidad',a:pa.STI,p:pp.STI,inv:false,fmt:v=>Math.round(v*100)\/100},\n    {n:'G Fuerza sonora (dB)',a:pa.G,p:pp.G,inv:true,fmt:v=>Math.round(v*10)\/10,off:true},\n  ];\n  const barRows=params.map(pr=>{\n    const lo=Math.min(pr.a,pr.p),hi=Math.max(pr.a,pr.p);\n    const span=(hi-lo)||1, base=pr.off?Math.min(lo,0):0, range=(hi-base)||1;\n    const wa=Math.max(2,Math.abs(pr.a-base)\/range*100), wp=Math.max(2,Math.abs(pr.p-base)\/range*100);\n    const better=pr.inv?(pr.p<pr.a):(pr.p>pr.a);\n    return `<div style=\"margin-bottom:.7rem\">\n      <div style=\"font-size:.8rem;font-weight:600;margin-bottom:.2rem\">${pr.n} ${better?'<span style=\"color:var(--ok)\">\u25b2 mejora<\/span>':''}<\/div>\n      <div style=\"display:flex;align-items:center;gap:.5rem;font-size:.75rem\"><span style=\"width:70px;color:var(--bad)\">Actual<\/span><div style=\"flex:1;background:#f0f0f0;border-radius:5px\"><div style=\"width:${wa}%;background:var(--bad);height:14px;border-radius:5px\"><\/div><\/div><span style=\"width:54px;text-align:right\">${pr.fmt(pr.a)}<\/span><\/div>\n      <div style=\"display:flex;align-items:center;gap:.5rem;font-size:.75rem;margin-top:.2rem\"><span style=\"width:70px;color:var(--ok)\">Propuesto<\/span><div style=\"flex:1;background:#f0f0f0;border-radius:5px\"><div style=\"width:${wp}%;background:var(--ok);height:14px;border-radius:5px\"><\/div><\/div><span style=\"width:54px;text-align:right\">${pr.fmt(pr.p)}<\/span><\/div>\n    <\/div>`;\n  }).join('');\n  el.innerHTML=`<h4 style=\"font-size:.9rem;margin:.4rem 0\">Curva de reverberaci\u00f3n por banda de octava<\/h4>${rtChart}\n    <h4 style=\"font-size:.9rem;margin:1rem 0 .4rem\">Comparativa por par\u00e1metro<\/h4>${barRows}\n    <p class=\"hint\">RT propuesto medio: <b>${f3(S.proposed.rtMean)} s<\/b> \u2014 ${S.proposed.compliant?'<span style=\"color:var(--ok)\">cumplir\u00eda el objetivo<\/span>':'<span style=\"color:var(--bad)\">a\u00fan por encima, considera m\u00e1s superficie<\/span>'}.<\/p>`;\n}\n\/* Esquema de disposici\u00f3n seg\u00fan el sistema (techo modular 60\u00d760, pared, etc.) \u2014 fiel a medidas *\/\nfunction drawLayout(){\n  const el=document.getElementById('layout');if(!el)return;\n  const r=S.room, ch=S.chosenSystem, surf=ch.sys.surf||'techo';\n  if(surf==='pared'){el.innerHTML=drawWallLayout();return;}\n  \/\/ Techo: planta con cuadr\u00edcula a escala\n  let pts;\n  if(r.shapeMode==='poly'&&r.poly.length>=3)pts=r.poly.map(p=>({x:p.x,y:p.y}));\n  else pts=[{x:0,y:0},{x:r.rectL||1,y:0},{x:r.rectL||1,y:r.rectW||1},{x:0,y:r.rectW||1}];\n  const xs=pts.map(p=>p.x),ys=pts.map(p=>p.y);\n  const minX=Math.min(...xs),minY=Math.min(...ys),maxX=Math.max(...xs),maxY=Math.max(...ys);\n  const W=460,H=320,pad=30,sc=Math.min((W-pad*2)\/(maxX-minX||1),(H-pad*2)\/(maxY-minY||1));\n  const tx=x=>pad+(x-minX)*sc, ty=y=>pad+(y-minY)*sc;\n  const poly=pts.map(p=>tx(p.x)+','+ty(p.y)).join(' ');\n  \/\/ Tama\u00f1o de m\u00f3dulo seg\u00fan sistema: modular 0.6\u00d70.6, baffles 1.2 l\u00ednea, islas 1.2\u00d71.2\n  const cat=ch.sys.cat||'';\n  let mod=0.6, gapF=0.06, label='';\n  if(\/modular\/i.test(cat)){mod=0.6;label='M\u00f3dulos 60\u00d760';}\n  else if(\/baffle\/i.test(cat)){mod=1.2;label='Baffles (l\u00edneas)';}\n  else if(\/isla\/i.test(cat)){mod=1.2;gapF=0.4;label='Islas suspendidas';}\n  else{mod=0.6;label=cat;}\n  let panels='';\n  if(\/baffle\/i.test(cat)){\n    \/\/ l\u00edneas paralelas\n    for(let x=minX+mod\/2;x<maxX;x+=mod){panels+=`<line x1=\"${tx(x)}\" y1=\"${ty(minY)+4}\" x2=\"${tx(x)}\" y2=\"${ty(maxY)-4}\" stroke=\"var(--brand)\" stroke-width=\"4\" opacity=\"0.5\"\/>`;}\n  }else{\n    for(let x=minX;x<maxX-mod*0.3;x+=mod)for(let y=minY;y<maxY-mod*0.3;y+=mod){\n      panels+=`<rect x=\"${tx(x)+1}\" y=\"${ty(y)+1}\" width=\"${mod*sc*(1-gapF)}\" height=\"${mod*sc*(1-gapF)}\" fill=\"var(--brand)\" opacity=\"0.4\" rx=\"2\"\/>`;}\n  }\n  el.innerHTML=`<svg viewBox=\"0 0 ${W} ${H}\" width=\"100%\" style=\"background:#fbfcfd;border:1px solid var(--line);border-radius:10px\">\n    <clipPath id=\"rc\"><polygon points=\"${poly}\"\/><\/clipPath>\n    <g clip-path=\"url(#rc)\">${panels}<\/g>\n    <polygon points=\"${poly}\" fill=\"none\" stroke=\"var(--brand)\" stroke-width=\"2\"\/>\n    <text x=\"${tx((minX+maxX)\/2)}\" y=\"${ty(maxY)+18}\" font-size=\"11\" text-anchor=\"middle\" fill=\"var(--muted)\">${f(maxX-minX)} m<\/text>\n    <text x=\"${tx(minX)-8}\" y=\"${ty((minY+maxY)\/2)}\" font-size=\"11\" text-anchor=\"middle\" fill=\"var(--muted)\" transform=\"rotate(-90 ${tx(minX)-8} ${ty((minY+maxY)\/2)})\">${f(maxY-minY)} m<\/text>\n    <text x=\"${W\/2}\" y=\"${H-6}\" font-size=\"11\" text-anchor=\"middle\" fill=\"var(--brand)\">${label} \u00b7 ${esc(ch.sys.name)} \u00b7 ${f(ch.areaNeeded)} m\u00b2<\/text>\n  <\/svg>`;\n}\n\/* Representaci\u00f3n de la pared donde se aloja el tratamiento (cuadros, textil, paneles) *\/\nfunction drawWallLayout(){\n  const r=S.room, ch=S.chosenSystem;\n  const Lw=(r.shapeMode==='rect'?r.rectL:Math.max(...(r.poly.length?r.poly.map(p=>p.x):[4])))||4;\n  const Hh=r.height||3;\n  const W=460,H=260,pad=34,sc=Math.min((W-pad*2)\/(Lw||1),(H-pad*2)\/(Hh||1));\n  const wpx=Lw*sc, hpx=Hh*sc, x0=(W-wpx)\/2, y0=(H-hpx)\/2;\n  \/\/ paneles repartidos en la pared cubriendo el \u00e1rea propuesta\n  const panelArea=ch.areaNeeded, panelW=1.0, panelH=1.2;\n  const nPanels=Math.max(1,Math.round(panelArea\/(panelW*panelH)));\n  const cols=Math.max(1,Math.floor(Lw\/(panelW+0.3))), rows=Math.ceil(nPanels\/cols);\n  let panels='';let n=0;\n  for(let row=0;row<rows&&n<nPanels;row++)for(let col=0;col<cols&&n<nPanels;col++,n++){\n    const px=x0+0.3*sc+col*(panelW+0.3)*sc, py=y0+0.3*sc+row*(panelH+0.3)*sc;\n    panels+=`<rect x=\"${px}\" y=\"${py}\" width=\"${panelW*sc}\" height=\"${panelH*sc}\" fill=\"var(--brand)\" opacity=\"0.45\" rx=\"3\"\/>`;\n  }\n  return `<svg viewBox=\"0 0 ${W} ${H}\" width=\"100%\" style=\"background:#fbfcfd;border:1px solid var(--line);border-radius:10px\">\n    <rect x=\"${x0}\" y=\"${y0}\" width=\"${wpx}\" height=\"${hpx}\" fill=\"#fff\" stroke=\"var(--brand)\" stroke-width=\"2\"\/>\n    ${panels}\n    <text x=\"${W\/2}\" y=\"${y0+hpx+18}\" font-size=\"11\" text-anchor=\"middle\" fill=\"var(--muted)\">Pared ${f(Lw)} \u00d7 ${f(Hh)} m<\/text>\n    <text x=\"${W\/2}\" y=\"${H-6}\" font-size=\"11\" text-anchor=\"middle\" fill=\"var(--brand)\">${esc(ch.sys.name)} \u00b7 ${nPanels} ud \u00b7 ${f(ch.areaNeeded)} m\u00b2<\/text>\n  <\/svg>`;\n}\n\/* Vista 3D esquem\u00e1tica (caja isom\u00e9trica con la superficie tratada resaltada) *\/\nfunction draw3D(){\n  const el=document.getElementById('view3d');if(!el)return;\n  const r=S.room, ch=S.chosenSystem, surf=ch.sys.surf||'techo';\n  const L=(r.shapeMode==='rect'?r.rectL:Math.max(...(r.poly.length?r.poly.map(p=>p.x):[4])))||4;\n  const Wd=(r.shapeMode==='rect'?r.rectW:Math.max(...(r.poly.length?r.poly.map(p=>p.y):[3])))||3;\n  const Ht=r.height||3;\n  \/\/ proyecci\u00f3n isom\u00e9trica simple\n  const W=460,H=320, s=Math.min(300\/(L+Wd), 150\/Ht, 40);\n  const ang=Math.PI\/6, cx=W\/2, cy=H*0.62;\n  const px=(x,y,z)=>({X:cx+(x-y)*Math.cos(ang)*s, Y:cy-(z*s)+(x+y)*Math.sin(ang)*s});\n  const A=px(0,0,0),B=px(L,0,0),C=px(L,Wd,0),D=px(0,Wd,0);\n  const A2=px(0,0,Ht),B2=px(L,0,Ht),C2=px(L,Wd,Ht),D2=px(0,Wd,Ht);\n  const pol=(...p)=>p.map(q=>q.X+','+q.Y).join(' ');\n  const treatTecho=surf==='techo'?`<polygon points=\"${pol(A2,B2,C2,D2)}\" fill=\"var(--brand)\" opacity=\"0.5\"\/>`:'';\n  const treatPared=surf==='pared'?`<polygon points=\"${pol(A,B,B2,A2)}\" fill=\"var(--brand)\" opacity=\"0.5\"\/>`:'';\n  el.innerHTML=`<svg viewBox=\"0 0 ${W} ${H}\" width=\"100%\" style=\"background:#fbfcfd;border:1px solid var(--line);border-radius:10px\">\n    <polygon points=\"${pol(A,B,C,D)}\" fill=\"#eef2f2\" stroke=\"var(--line)\"\/>\n    <polygon points=\"${pol(A,B,B2,A2)}\" fill=\"#f6f9f9\" stroke=\"var(--line)\"\/>\n    <polygon points=\"${pol(B,C,C2,B2)}\" fill=\"#eef2f2\" stroke=\"var(--line)\"\/>\n    ${treatPared}${treatTecho}\n    <polygon points=\"${pol(A2,B2,C2,D2)}\" fill=\"none\" stroke=\"var(--brand)\" stroke-width=\"1.5\" opacity=\"0.6\"\/>\n    <text x=\"${W\/2}\" y=\"${H-8}\" font-size=\"11\" text-anchor=\"middle\" fill=\"var(--muted)\">${f(L)} \u00d7 ${f(Wd)} \u00d7 ${f(Ht)} m \u00b7 tratamiento en ${surf==='pared'?'pared':'techo'} (${esc(ch.sys.cat||'')})<\/text>\n  <\/svg>`;\n}\n\n\/* ===== ENV\u00cdO A COMERCIAL ===== *\/\nfunction sendToCommercial(){\n  \/\/ Sistema elegido del cat\u00e1logo si existe; si no, la soluci\u00f3n de partida Clase A 0,95\n  const chosen=S.chosenSystem?{name:S.chosenSystem.sys.name,cat:S.chosenSystem.sys.cat,surf:S.chosenSystem.sys.surf,\n      mat:S.chosenSystem.mat.name,materialId:S.chosenSystem.mat.id,\n      price:S.chosenSystem.mat.price,area:S.chosenSystem.areaNeeded,cost:S.chosenSystem.cost,am:S.chosenSystem.am}\n    :{name:SOLUTION_A095.name,cat:'Falso techo modular',surf:'techo',mat:'Clase A (\u03b1w 0,95)',materialId:null,\n      price:0,area:S.improvedArea||0,cost:0,am:SOLUTION_A095.aw};\n  const v={\n    id:'V-'+Date.now(),\n    createdAt:new Date().toISOString(),\n    status:'pendiente',\n    client:S.client,\n    room:{type:S.room.type,sector:S.room.sector,occ:S.room.occ,...({volume:S.geom.volume,area:S.geom.area,H:S.geom.H})},\n    result:S.result,\n    resultOcc:S.resultOcc?{rtMean:S.resultOcc.rtMean}:null,\n    observations:S.observations||'',\n    chosenSystem:chosen,\n    improvedRt:S.improved?S.improved.rtMean:null,\n    improvedArea:S.improvedArea||0,\n    proposedRt:S.proposed?S.proposed.rtMean:(S.improved?S.improved.rtMean:null),\n    proposedCompliant:S.proposed?S.proposed.compliant:(S.improved?S.improved.compliant:null),\n    plan:S.plan, photo:S.photos[0]||null,\n  };\n  const list=DB.get('valuations',[]);list.unshift(v);DB.set('valuations',list);\n  document.getElementById('steps').innerHTML=`<div class=\"card\" style=\"text-align:center\">\n    <h1>\u2705 \u00a1Valoraci\u00f3n enviada!<\/h1>\n    <p class=\"lead\" style=\"margin:1rem auto\">Gracias, ${esc(S.client.name)}. Tu consulta (<b>${v.id}<\/b>) ha llegado al departamento comercial. En breve recibir\u00e1s noticias en <b>${esc(S.client.email)}<\/b>.<\/p>\n    <button class=\"btn btn-primary\" onclick=\"S=blankProject();goStep(1)\">Nueva consulta<\/button>\n  <\/div>`;\n  document.getElementById('stepper').innerHTML='';\n}\n\n\/* ============================================================\n   ZONA INTERNA (protegida) \u2014 comercial + configuraci\u00f3n\n   ============================================================ *\/\nfunction internalUsers(){return DB.get('internal.users',[]);}\nfunction internalPass(){return cfg('internalPass','comercial2025');}\nlet internalAuthed=false, internalTab='val';\n\nfunction renderInternal(){\n  const c=document.getElementById('view-internal');\n  if(!internalAuthed){c.innerHTML=lockScreen();return;}\n  c.innerHTML=`<div class=\"card\">\n    <div class=\"tabs\">\n      <button class=\"${internalTab==='val'?'active':''}\" onclick=\"internalTab='val';renderInternal()\">Valoraciones<\/button>\n      <button class=\"${internalTab==='cfg'?'active':''}\" onclick=\"internalTab='cfg';renderInternal()\">Configuraci\u00f3n<\/button>\n      <button class=\"btn-ghost\" style=\"margin-left:auto\" onclick=\"internalAuthed=false;renderInternal()\">Salir \ud83d\udd12<\/button>\n    <\/div>\n    <div id=\"internalBody\"><\/div>\n  <\/div>`;\n  if(internalTab==='val')renderValuations();else renderConfig();\n}\nfunction lockScreen(){\n  return `<div class=\"card lock\">\n    <h1>\ud83d\udd12 Zona interna<\/h1>\n    <p class=\"muted\">Acceso restringido al equipo comercial. No es para clientes.<\/p>\n    <div class=\"fld\"><label>Clave de acceso<\/label><input id=\"lk_pass\" type=\"password\" placeholder=\"clave\"><\/div>\n    <button class=\"btn btn-primary\" style=\"width:100%\" onclick=\"tryLogin()\">Entrar<\/button>\n    <p class=\"hint\" style=\"margin-top:1rem\">\u00bfNuevo? Reg\u00edstrate:<\/p>\n    <div class=\"grid2\">\n      <div class=\"fld\"><input id=\"rg_name\" placeholder=\"Nombre\"><\/div>\n      <div class=\"fld\"><input id=\"rg_email\" placeholder=\"Correo\"><\/div>\n    <\/div>\n    <div class=\"fld\"><input id=\"rg_pass\" type=\"password\" placeholder=\"Crea una contrase\u00f1a personal\"><\/div>\n    <button class=\"btn\" style=\"width:100%\" onclick=\"register()\">Registrarme<\/button>\n    <p class=\"hint\" style=\"margin-top:1rem\">Prototipo: el control de acceso es local y orientativo, no sustituye a autenticaci\u00f3n real (Entra ID en la versi\u00f3n servidor). Clave por defecto: <code>comercial2025<\/code>.<\/p>\n  <\/div>`;\n}\nfunction tryLogin(){\n  const p=val('lk_pass');\n  const u=internalUsers().find(x=>x.pass===p);\n  if(p===internalPass()||u){internalAuthed=true;renderInternal();}\n  else alert('Clave incorrecta.');\n}\nfunction register(){\n  const u={name:val('rg_name'),email:val('rg_email'),pass:val('rg_pass')};\n  if(!u.name||!u.email||!u.pass){alert('Completa nombre, correo y contrase\u00f1a.');return;}\n  const list=internalUsers();list.push(u);DB.set('internal.users',list);\n  alert('Registrado. Ya puedes entrar con tu contrase\u00f1a.');\n}\n\n\/* ----- VALORACIONES ----- *\/\nfunction renderValuations(){\n  const body=document.getElementById('internalBody');\n  const list=DB.get('valuations',[]);\n  if(!list.length){body.innerHTML='<p class=\"muted\">No hay valoraciones recibidas todav\u00eda. Las que env\u00eden los clientes aparecer\u00e1n aqu\u00ed.<\/p>';return;}\n  body.innerHTML=`<h2>Valoraciones recibidas (${list.length})<\/h2>\n  <table><thead><tr><th>Ref.<\/th><th>Cliente<\/th><th>Estancia<\/th><th class=\"num\">RT actual<\/th><th>Estado<\/th><th>Documentos<\/th><\/tr><\/thead><tbody>`+\n  list.map((v,i)=>`<tr>\n    <td><b>${v.id.replace('V-','#')}<\/b><br><span class=\"hint\">${new Date(v.createdAt).toLocaleDateString('es-ES')}<\/span><\/td>\n    <td>${esc(v.client.name)} ${esc(v.client.surname)}<br><span class=\"hint\">${esc(v.client.email)} \u00b7 ${esc(v.client.cp)}<\/span><\/td>\n    <td>${useLabel(v.room.type)}<br><span class=\"hint\">${f(v.room.volume)} m\u00b3<\/span><\/td>\n    <td class=\"num\">${v.result.absBased?f(v.result.eqAbs)+' m\u00b2':f3(v.result.rtMean)+' s'}<\/td>\n    <td>\n      <select onchange=\"setStatus(${i},this.value)\" style=\"font-size:.8rem;padding:.3rem;border-radius:7px;border:1px solid var(--line)\">\n        ${['pendiente','en_revision','oferta_enviada','seguimiento','cerrado_ganado','cerrado_perdido'].map(s=>`<option value=\"${s}\" ${v.status===s?'selected':''}>${s.replace(\/_\/g,' ')}<\/option>`).join('')}\n      <\/select>\n    <\/td>\n    <td>\n      <button class=\"btn btn-sm\" onclick=\"genReport(${i})\">\ud83d\udcc4 Informe<\/button>\n      <button class=\"btn btn-sm\" onclick=\"genOffer(${i})\">\ud83d\udcca Oferta<\/button>\n      <button class=\"btn btn-sm\" style=\"background:var(--brand);color:#fff;border-color:var(--brand)\" onclick=\"renderBudget(${i})\">\ud83d\udcb6 Presupuesto<\/button>\n      <button class=\"btn btn-sm\" onclick=\"genConditions(${i})\">\ud83d\udcdc Condiciones<\/button>\n      <button class=\"btn btn-sm\" onclick=\"genPDF(${i})\">\ud83d\udda8\ufe0f PDF<\/button>\n      <button class=\"btn btn-sm\" onclick=\"sendFollowUp(${i})\">\ud83d\udcec Seguimiento<\/button>\n      <button class=\"btn btn-sm btn-danger\" onclick=\"delVal(${i})\">\u2715<\/button>\n    <\/td><\/tr>`).join('')+`<\/tbody><\/table>`;\n}\nfunction delVal(i){const l=DB.get('valuations',[]);l.splice(i,1);DB.set('valuations',l);renderValuations();}\n\n\/* ============================================================\n   PRESUPUESTO INTERNO (integrado en Valoraciones)\n   Est\u00e9tica inspirada en acustium.es \u2014 premium, limpia, campos destacados\n   ============================================================ *\/\nlet budgetIdx=null;\nfunction bcfg(){return cfg('budget',{pMat:40,waste:8,pMont:18,montUnit:'m2',rend:18,ops:2,pkm:0.42,diet:40,margin:35,dto:0,iva:21});}\nfunction estimateKm(cp){\n  const p=parseInt(String(cp).slice(0,2));\n  const map={28:5,8:620,41:530,46:355,29:530,48:395,50:315,3:420,30:400,15:595,33:450,36:600,35:1700,38:1750,7:680,18:420,11:655,14:400,21:600,23:335,4:560,37:235,47:200,9:240,34:240,49:270,5:115,40:90,42:230,45:70,13:200,16:165,19:60,2:255,10:300,6:410,1:355,20:455,31:405,26:335,22:390,44:285,12:425,25:575,17:700,43:545,24:340,27:560,32:490,39:435,51:380,52:400};\n  return map[p]||300;\n}\nfunction renderBudget(i){\n  budgetIdx=i;\n  const v=DB.get('valuations',[])[i];const ch=v.chosenSystem;\n  const b=bcfg();\n  \/\/ autocargar\n  if(ch&&ch.price)b.pMat=ch.price;\n  if(v.client&&v.client.cp)b.km=estimateKm(v.client.cp); else b.km=b.km||0;\n  const area=ch?ch.area:0;\n  const body=document.getElementById('internalBody');\n  const fld=(id,lab,val,step,hi)=>`<div class=\"bfld ${hi?'bfld-hi':''}\"><label>${lab}<\/label><input id=\"${id}\" type=\"number\" step=\"${step}\" value=\"${val}\" oninput=\"calcBudget()\"><\/div>`;\n  const unitOpts=[['m2','\u20ac\/m\u00b2'],['ud','\u20ac\/ud'],['ml','\u20ac\/ml']].map(u=>`<option value=\"${u[0]}\" ${b.montUnit===u[0]?'selected':''}>${u[1]}<\/option>`).join('');\n  body.innerHTML=`\n  <style>\n    .budget-hero{background:linear-gradient(115deg,#0e1c1c 0%,#0e7c7b 130%);color:#fff;border-radius:18px;padding:1.6rem 1.8rem;margin-bottom:1.4rem;position:relative;overflow:hidden}\n    .budget-hero::after{content:'';position:absolute;right:-40px;top:-40px;width:200px;height:200px;border:1px solid rgba(255,255,255,.12);border-radius:50%}\n    .budget-hero::before{content:'';position:absolute;right:10px;bottom:-60px;width:160px;height:160px;border:1px solid rgba(255,255,255,.08);border-radius:50%}\n    .budget-hero .eyebrow{font-size:.7rem;letter-spacing:.22em;text-transform:uppercase;opacity:.75;font-weight:600}\n    .budget-hero h2{font-size:1.7rem;margin:.3rem 0 .1rem;color:#fff;font-weight:700;letter-spacing:-.01em}\n    .budget-hero p{opacity:.85;font-size:.88rem;margin:0}\n    .bsec{background:#fff;border:1px solid var(--line);border-radius:16px;padding:1.4rem 1.5rem;margin-bottom:1.3rem}\n    .bsec-h{display:flex;align-items:center;gap:.6rem;margin-bottom:1rem}\n    .bsec-h .num{width:30px;height:30px;border-radius:8px;background:var(--brand);color:#fff;display:flex;align-items:center;justify-content:center;font-weight:700;font-size:.9rem}\n    .bsec-h h3{margin:0;font-size:1.15rem}\n    .bgrid{display:grid;gap:.9rem}\n    .bgrid-3{grid-template-columns:repeat(3,1fr)}.bgrid-4{grid-template-columns:repeat(4,1fr)}.bgrid-2{grid-template-columns:1fr 1fr}\n    @media(max-width:680px){.bgrid-3,.bgrid-4,.bgrid-2{grid-template-columns:1fr 1fr}}\n    .bfld label{display:block;font-size:.7rem;font-weight:600;text-transform:uppercase;letter-spacing:.04em;color:var(--muted);margin-bottom:.25rem}\n    .bfld input,.bfld select{width:100%;font:inherit;padding:.5rem .65rem;border:1px solid var(--line);border-radius:9px}\n    .bfld-hi{background:linear-gradient(160deg,#eef7f6,#fff);border:1.5px solid var(--brand);border-radius:11px;padding:.6rem .7rem .7rem}\n    .bfld-hi label{color:var(--brand)}\n    .bfld-hi input{border-color:var(--brand);font-weight:700;font-size:1.05rem;background:#fff}\n    .bmont{display:grid;grid-template-columns:1.3fr .8fr;gap:.5rem;align-items:end}\n    .bkpi{display:grid;grid-template-columns:repeat(4,1fr);gap:.9rem;margin-bottom:.4rem}\n    @media(max-width:680px){.bkpi{grid-template-columns:1fr 1fr}}\n    .bk{border-radius:14px;padding:1rem 1.1rem;border:1px solid var(--line)}\n    .bk .v{font-size:1.6rem;font-weight:800;line-height:1;letter-spacing:-.02em}\n    .bk .l{font-size:.68rem;text-transform:uppercase;letter-spacing:.05em;color:var(--muted);margin-top:.35rem}\n    .bk-cost{background:#fbf3ef;border-color:#e7c9bd}.bk-cost .v{color:#b8543a}\n    .bk-margin{background:#f0f6ef;border-color:#cfe1c8}.bk-margin .v{color:#3f7d3a}\n    .bk-total{background:linear-gradient(155deg,#0e7c7b,#0a5a59);border-color:#0a5a59}.bk-total .v,.bk-total .l{color:#fff}.bk-total .l{opacity:.85}\n    .btable{width:100%;border-collapse:collapse;font-size:.88rem;margin-top:.4rem}\n    .btable th,.btable td{padding:.5rem .6rem;border-bottom:1px solid var(--line);text-align:left}\n    .btable th{font-size:.7rem;text-transform:uppercase;color:var(--muted)}\n    .btable td.n,.btable th.n{text-align:right;font-variant-numeric:tabular-nums}\n    .btable tr.sub td{background:#f3f7f6;font-weight:600}\n    .btable tr.tot td{font-weight:800;border-top:2px solid var(--ink)}\n    .bmargin-box{background:linear-gradient(160deg,#fffaf0,#fff);border:1.5px dashed var(--accent);border-radius:13px;padding:1rem 1.1rem;margin-top:1rem}\n    .bmargin-box .row{display:flex;justify-content:space-between;padding:.3rem 0;font-size:.92rem}\n    .bmargin-box .row.big{font-weight:800;font-size:1.05rem;border-top:1px solid var(--line);margin-top:.3rem;padding-top:.6rem}\n  <\/style>\n\n  <div style=\"display:flex;justify-content:space-between;align-items:center;margin-bottom:1rem;flex-wrap:wrap;gap:.6rem\">\n    <button class=\"btn btn-sm\" onclick=\"renderValuations()\">\u2190 Volver a valoraciones<\/button>\n    <span class=\"hint\">Ref. ${esc(v.id)} \u00b7 ${esc(v.client.name)} ${esc(v.client.surname)}<\/span>\n  <\/div>\n\n  <div class=\"budget-hero\">\n    <div class=\"eyebrow\">DISE\u00d1ANDO EL SONIDO \u00b7 Presupuesto interno<\/div>\n    <h2>${esc(ch?ch.name:'Tratamiento ac\u00fastico')}<\/h2>\n    <p>${esc(useLabel(v.room.type))} \u00b7 ${f(v.room.volume)} m\u00b3 \u00b7 superficie propuesta <b>${f(area)} m\u00b2<\/b> \u00b7 ${esc(ch?ch.cat||'':'')} ${v.client.cp?'\u00b7 CP '+esc(v.client.cp):''}<\/p>\n  <\/div>\n\n  <div class=\"bsec\">\n    <div class=\"bsec-h\"><span class=\"num\">1<\/span><h3>Par\u00e1metros de coste<\/h3><\/div>\n    <p class=\"hint\" style=\"margin-top:-.5rem;margin-bottom:1rem\">Campos principales destacados. El coste de mano de obra se calcula con el <b>precio de montaje<\/b>; el margen se refleja aparte.<\/p>\n    <div class=\"bgrid bgrid-3\">\n      ${fld('p_mat','Precio material\/sistema \u20ac\/m\u00b2',b.pMat,'0.01',true)}\n      <div class=\"bfld bfld-hi\"><label>Precio montaje<\/label>\n        <div class=\"bmont\"><input id=\"p_mont\" type=\"number\" step=\"0.01\" value=\"${b.pMont}\" oninput=\"calcBudget()\">\n        <select id=\"p_montunit\" onchange=\"onMontUnit()\">${unitOpts}<\/select><\/div><\/div>\n      ${fld('p_waste','Desperdicio material %',b.waste,'0.1')}\n    <\/div>\n    <div class=\"bgrid bgrid-3\" style=\"margin-top:.9rem\">\n      <div class=\"bfld\"><label>Cantidad de montaje (<span id=\"lblUnit\">${b.montUnit==='m2'?'m\u00b2':b.montUnit}<\/span>)<\/label><input id=\"p_qty\" type=\"number\" step=\"0.01\" value=\"${area}\" oninput=\"calcBudget()\"><\/div>\n      ${fld('p_rend','Rendimiento (uds\/jornada)',b.rend,'0.5')}\n      ${fld('p_ops','Operarios',b.ops,'1')}\n    <\/div>\n    <div class=\"bgrid bgrid-3\" style=\"margin-top:.9rem\">\n      ${fld('p_km','Distancia ida km',b.km||0,'1')}\n      ${fld('p_pkm','Precio km \u20ac\/km',b.pkm,'0.01')}\n      ${fld('p_diet','Dieta \u20ac\/d\u00eda\u00b7operario',b.diet,'1')}\n    <\/div>\n    <div class=\"bgrid bgrid-3\" style=\"margin-top:.9rem\">\n      ${fld('p_margin','Margen comercial %',b.margin,'0.5',true)}\n      ${fld('p_dto','Descuento cliente %',b.dto,'0.5')}\n      ${fld('p_iva','IVA \/ impuestos %',b.iva,'0.5')}\n    <\/div>\n  <\/div>\n\n  <div class=\"bsec\">\n    <div class=\"bsec-h\"><span class=\"num\">2<\/span><h3>Escandallo desglosado<\/h3><\/div>\n    <div id=\"b_escandallo\"><\/div>\n  <\/div>\n\n  <div class=\"bsec\">\n    <div class=\"bsec-h\"><span class=\"num\">3<\/span><h3>Resumen y precio cliente<\/h3><\/div>\n    <div id=\"b_resumen\"><\/div>\n    <div style=\"margin-top:1.1rem;display:flex;gap:.7rem;flex-wrap:wrap\">\n      <button class=\"btn btn-primary\" onclick=\"exportBudget(${i})\">\u2b07 Exportar presupuesto interno<\/button>\n      <button class=\"btn\" onclick=\"exportBudgetClient(${i})\">\u2b07 Exportar presupuesto cliente<\/button>\n    <\/div>\n  <\/div>`;\n  calcBudget();\n}\nfunction bv(id){const e=document.getElementById(id);return e?parseFloat(e.value)||0:0;}\nfunction onMontUnit(){const u=document.getElementById('p_montunit').value;const l=document.getElementById('lblUnit');if(l)l.textContent=u==='m2'?'m\u00b2':u;calcBudget();}\nfunction calcBudget(){\n  if(budgetIdx==null)return;\n  const v=DB.get('valuations',[])[budgetIdx];const ch=v.chosenSystem;\n  const area=ch?ch.area:0;\n  const pMat=bv('p_mat'),waste=bv('p_waste'),pMont=bv('p_mont'),qty=bv('p_qty'),rend=bv('p_rend'),ops=bv('p_ops');\n  const km=bv('p_km'),pkm=bv('p_pkm'),diet=bv('p_diet'),margin=bv('p_margin'),dto=bv('p_dto'),iva=bv('p_iva');\n  const montUnit=(document.getElementById('p_montunit')||{}).value||'m2';\n  const uLbl=montUnit==='m2'?'m\u00b2':montUnit;\n  \/\/ Material (siempre por m\u00b2 sobre la superficie de tratamiento)\n  const matArea=area*(1+waste\/100), costMat=matArea*pMat;\n  \/\/ Mano de obra = montaje (precio montaje \u00d7 cantidad). Las jornadas son el resultado.\n  const costMontaje=pMont*qty;\n  const jornadas=rend>0?qty\/rend:0, dias=Math.max(1,Math.ceil(jornadas));\n  const precioM2obra=area>0?costMontaje\/area:0;\n  \/\/ Desplazamiento y dietas (seg\u00fan d\u00edas de obra)\n  const costKm=km*2*pkm*dias, costDiet=diet*ops*dias, costViaje=costKm+costDiet;\n  \/\/ Coste de ejecuci\u00f3n (SIN margen)\n  const costeEjecucion=costMat+costMontaje+costViaje;\n  \/\/ Margen reflejado APARTE\n  const margenImporte=costeEjecucion*margin\/100;\n  const precioVenta=costeEjecucion+margenImporte;\n  \/\/ Precio cliente\n  const subtotal=precioVenta, descuento=subtotal*dto\/100, base=subtotal-descuento, impuestos=base*iva\/100, totalCliente=base+impuestos;\n\n  document.getElementById('b_escandallo').innerHTML=`<table class=\"btable\">\n    <thead><tr><th>Concepto<\/th><th class=\"n\">Cantidad<\/th><th class=\"n\">Precio unit.<\/th><th class=\"n\">Coste \u20ac<\/th><\/tr><\/thead><tbody>\n    <tr class=\"sub\"><td colspan=\"4\">Material<\/td><\/tr>\n    <tr><td>Superficie de tratamiento<\/td><td class=\"n\">${f(area)} m\u00b2<\/td><td class=\"n\">\u2014<\/td><td class=\"n\">\u2014<\/td><\/tr>\n    <tr><td>+ Desperdicio (${f(waste)}%)<\/td><td class=\"n\">${f(matArea)} m\u00b2<\/td><td class=\"n\">${f(pMat)} \u20ac\/m\u00b2<\/td><td class=\"n\">${f(costMat)}<\/td><\/tr>\n    <tr class=\"sub\"><td colspan=\"4\">Mano de obra (montaje)<\/td><\/tr>\n    <tr><td>Montaje<\/td><td class=\"n\">${f(qty)} ${uLbl}<\/td><td class=\"n\">${f(pMont)} \u20ac\/${uLbl}<\/td><td class=\"n\">${f(costMontaje)}<\/td><\/tr>\n    <tr><td><span class=\"muted\">Jornadas resultantes (rend. ${f(rend)} uds\/jornada)<\/span><\/td><td class=\"n\">${f(jornadas)} \u2192 ${dias} d\u00eda\/s<\/td><td class=\"n\">${f(precioM2obra)} \u20ac\/m\u00b2<\/td><td class=\"n\">\u2014<\/td><\/tr>\n    <tr class=\"sub\"><td colspan=\"4\">Desplazamiento y dietas<\/td><\/tr>\n    <tr><td>Kilometraje (${f(km)} km \u00d7 2 \u00d7 ${dias})<\/td><td class=\"n\">${f(km*2*dias)} km<\/td><td class=\"n\">${f(pkm)} \u20ac\/km<\/td><td class=\"n\">${f(costKm)}<\/td><\/tr>\n    <tr><td>Dietas (${f(ops)} op. \u00d7 ${dias} d\u00eda\/s)<\/td><td class=\"n\">${f(ops*dias)}<\/td><td class=\"n\">${f(diet)} \u20ac\/d\u00eda<\/td><td class=\"n\">${f(costDiet)}<\/td><\/tr>\n    <tr class=\"tot\"><td colspan=\"3\">COSTE DE EJECUCI\u00d3N (sin margen)<\/td><td class=\"n\">${f(costeEjecucion)} \u20ac<\/td><\/tr>\n    <\/tbody><\/table>\n    <div class=\"bmargin-box\">\n      <div class=\"row\"><span>Coste de ejecuci\u00f3n<\/span><span>${f(costeEjecucion)} \u20ac<\/span><\/div>\n      <div class=\"row\" style=\"color:#3f7d3a\"><span>+ Margen comercial (${f(margin)}%)<\/span><span>+ ${f(margenImporte)} \u20ac<\/span><\/div>\n      <div class=\"row big\"><span>Precio de venta (sin IVA)<\/span><span>${f(precioVenta)} \u20ac<\/span><\/div>\n    <\/div>`;\n  document.getElementById('b_resumen').innerHTML=`\n    <div class=\"bkpi\">\n      <div class=\"bk bk-cost\"><div class=\"v\">${f(costeEjecucion)} \u20ac<\/div><div class=\"l\">Coste ejecuci\u00f3n<\/div><\/div>\n      <div class=\"bk bk-margin\"><div class=\"v\">${f(margenImporte)} \u20ac<\/div><div class=\"l\">Margen (${f(margin)}%)<\/div><\/div>\n      <div class=\"bk bk-margin\"><div class=\"v\">${f(precioVenta)} \u20ac<\/div><div class=\"l\">Precio venta s\/IVA<\/div><\/div>\n      <div class=\"bk bk-total\"><div class=\"v\">${f(totalCliente)} \u20ac<\/div><div class=\"l\">Total cliente c\/IVA<\/div><\/div>\n    <\/div>\n    <table class=\"btable\">\n      <thead><tr><th>Presupuesto cliente<\/th><th class=\"n\">Uds (${uLbl})<\/th><th class=\"n\">Precio \u20ac\/${uLbl}<\/th><th class=\"n\">Importe \u20ac<\/th><\/tr><\/thead><tbody>\n      <tr><td>${esc(ch?ch.name:'Tratamiento ac\u00fastico')}<\/td><td class=\"n\">${f(qty)}<\/td><td class=\"n\">${f(qty>0?precioVenta\/qty:0)}<\/td><td class=\"n\">${f(precioVenta)}<\/td><\/tr>\n      <tr class=\"sub\"><td colspan=\"3\" class=\"n\">Subtotal<\/td><td class=\"n\">${f(subtotal)}<\/td><\/tr>\n      <tr><td colspan=\"3\" class=\"n\">Descuento ${f(dto)}%<\/td><td class=\"n\">-${f(descuento)}<\/td><\/tr>\n      <tr><td colspan=\"3\" class=\"n\">Base imponible<\/td><td class=\"n\">${f(base)}<\/td><\/tr>\n      <tr><td colspan=\"3\" class=\"n\">IVA ${f(iva)}%<\/td><td class=\"n\">${f(impuestos)}<\/td><\/tr>\n      <tr class=\"tot\"><td colspan=\"3\" class=\"n\">TOTAL CLIENTE<\/td><td class=\"n\">${f(totalCliente)} \u20ac<\/td><\/tr>\n    <\/tbody><\/table>`;\n  DB.set('cfg.budget',{pMat,waste,pMont,montUnit,rend,ops,km,pkm,diet,margin,dto,iva});\n  window._bcalc={area,qty,uLbl,matArea,pMat,costMat,pMont,costMontaje,jornadas,dias,km,pkm,diet,ops,costKm,costDiet,costViaje,costeEjecucion,margin,margenImporte,precioVenta,subtotal,descuento,dto,base,iva,impuestos,totalCliente};\n}\nfunction exportBudget(i){const v=DB.get('valuations',[])[i];const c=window._bcalc;if(!c)return;const nm=(v.client.name||'cliente').replace(\/\\s\/g,'_');\n  const h=`<html xmlns:x=\"urn:schemas-microsoft-com:office:excel\"><head><meta charset=\"utf-8\"><\/head><body><h2>Presupuesto interno \u2014 ${esc(v.client.name)} ${esc(v.client.surname)} (${esc(v.id)})<\/h2>\n  <table border=\"1\" cellspacing=\"0\" cellpadding=\"4\"><tr style=\"background:#0e7c7b;color:#fff\"><th>Concepto<\/th><th>Cantidad<\/th><th>Precio<\/th><th>Coste \u20ac<\/th><\/tr>\n  <tr><td>Material + desperdicio<\/td><td>${f(c.matArea)} m\u00b2<\/td><td>${f(c.pMat)} \u20ac\/m\u00b2<\/td><td>${f(c.costMat)}<\/td><\/tr>\n  <tr><td>Mano de obra (montaje)<\/td><td>${f(c.qty)} ${c.uLbl}<\/td><td>${f(c.pMont)} \u20ac\/${c.uLbl}<\/td><td>${f(c.costMontaje)}<\/td><\/tr>\n  <tr><td>Jornadas resultantes<\/td><td>${f(c.jornadas)} \u2192 ${c.dias} d\u00eda\/s<\/td><td><\/td><td><\/td><\/tr>\n  <tr><td>Kilometraje<\/td><td>${f(c.km*2*c.dias)} km<\/td><td>${f(c.pkm)}<\/td><td>${f(c.costKm)}<\/td><\/tr>\n  <tr><td>Dietas<\/td><td>${f(c.ops*c.dias)}<\/td><td>${f(c.diet)}<\/td><td>${f(c.costDiet)}<\/td><\/tr>\n  <tr><td colspan=\"3\"><b>COSTE DE EJECUCI\u00d3N (sin margen)<\/b><\/td><td><b>${f(c.costeEjecucion)}<\/b><\/td><\/tr>\n  <tr style=\"background:#f0f6ef\"><td colspan=\"3\">Margen comercial ${f(c.margin)}%<\/td><td>+ ${f(c.margenImporte)}<\/td><\/tr>\n  <tr><td colspan=\"3\"><b>PRECIO DE VENTA (sin IVA)<\/b><\/td><td><b>${f(c.precioVenta)}<\/b><\/td><\/tr>\n  <tr><td colspan=\"3\">Descuento ${f(c.dto)}%<\/td><td>-${f(c.descuento)}<\/td><\/tr>\n  <tr><td colspan=\"3\">IVA ${f(c.iva)}%<\/td><td>${f(c.impuestos)}<\/td><\/tr>\n  <tr><td colspan=\"3\"><b>TOTAL CLIENTE<\/b><\/td><td><b>${f(c.totalCliente)}<\/b><\/td><\/tr><\/table><\/body><\/html>`;\n  download(`Presupuesto_interno_${nm}.xls`,h,'application\/vnd.ms-excel');}\nfunction exportBudgetClient(i){const v=DB.get('valuations',[])[i];const c=window._bcalc;if(!c)return;const ch=v.chosenSystem;const nm=(v.client.name||'cliente').replace(\/\\s\/g,'_');\n  const h=`<html xmlns:x=\"urn:schemas-microsoft-com:office:excel\"><head><meta charset=\"utf-8\"><\/head><body><h2>Presupuesto \u2014 ${esc(v.client.name)} ${esc(v.client.surname)}<\/h2>\n  <table border=\"1\" cellspacing=\"0\" cellpadding=\"4\"><tr style=\"background:#0e7c7b;color:#fff\"><th>Descripci\u00f3n<\/th><th>Uds (${c.uLbl})<\/th><th>Precio \u20ac\/${c.uLbl}<\/th><th>Importe \u20ac<\/th><\/tr>\n  <tr><td>${esc(ch?ch.name:'Tratamiento')}<\/td><td>${f(c.qty)}<\/td><td>${f(c.qty>0?c.precioVenta\/c.qty:0)}<\/td><td>${f(c.precioVenta)}<\/td><\/tr>\n  <tr><td colspan=\"3\">Subtotal<\/td><td>${f(c.subtotal)}<\/td><\/tr>\n  <tr><td colspan=\"3\">Descuento ${f(c.dto)}%<\/td><td>-${f(c.descuento)}<\/td><\/tr>\n  <tr><td colspan=\"3\">Base<\/td><td>${f(c.base)}<\/td><\/tr>\n  <tr><td colspan=\"3\">IVA ${f(c.iva)}%<\/td><td>${f(c.impuestos)}<\/td><\/tr>\n  <tr><td colspan=\"3\"><b>TOTAL<\/b><\/td><td><b>${f(c.totalCliente)}<\/b><\/td><\/tr><\/table><\/body><\/html>`;\n  download(`Presupuesto_cliente_${nm}.xls`,h,'application\/vnd.ms-excel');}\n\n\/* ----- GENERACI\u00d3N DE DOCUMENTOS (cliente, offline) ----- *\/\nfunction acustiumLogo(h){\n  h=h||40; const userLogo=cfg('logo',null);\n  if(userLogo)return `<img decoding=\"async\" src=\"${userLogo}\" style=\"height:${h+12}px\">`;\n  return `<svg height=\"${h}\" viewBox=\"0 0 210 48\" xmlns=\"http:\/\/www.w3.org\/2000\/svg\" style=\"display:block\">\n    <circle cx=\"20\" cy=\"22\" r=\"13\" fill=\"none\" stroke=\"#1f7a78\" stroke-width=\"4.5\"\/>\n    <line x1=\"31\" y1=\"13\" x2=\"31\" y2=\"33\" stroke=\"#1f7a78\" stroke-width=\"4.5\"\/>\n    <text x=\"40\" y=\"29\" font-family=\"Arial,Helvetica,sans-serif\" font-size=\"23\" font-weight=\"800\" letter-spacing=\"1.5\"><tspan fill=\"#1f7a78\">a<\/tspan><tspan fill=\"#243b56\">CUSTIUM<\/tspan><\/text>\n    <text x=\"42\" y=\"42\" font-family=\"Arial,Helvetica,sans-serif\" font-size=\"8\" letter-spacing=\"3\" fill=\"#1f7a78\">DISE\u00d1ANDO EL SONIDO<\/text>\n  <\/svg>`;\n}\nfunction docHeader(){\n  const bg=cfg('bg',null);\n  const bgCss=bg?`background-image:url('${bg}');background-size:cover;`:'';\n  return `<div style=\"font-family:Arial,Helvetica,'Segoe UI',sans-serif;color:#243b56;${bgCss}padding:28px;max-width:820px;margin:0 auto\">\n    ${acustiumLogo(40)}`;\n}\nfunction valLine(v){\n  const r=v.room, res=v.result;\n  return `<table style=\"width:100%;border-collapse:collapse;font-size:13px\">\n    <tr><td style=\"padding:4px\"><b>Cliente<\/b><\/td><td>${esc(v.client.name)} ${esc(v.client.surname)} \u00b7 ${esc(v.client.email)} \u00b7 CP ${esc(v.client.cp)}${v.client.phone?' \u00b7 '+esc(v.client.phone):''}<\/td><\/tr>\n    <tr><td style=\"padding:4px\"><b>Estancia<\/b><\/td><td>${useLabel(r.type)} \u00b7 Volumen ${f(r.volume)} m\u00b3 \u00b7 Planta ${f(r.area)} m\u00b2 \u00b7 Altura ${f(r.H)} m<\/td><\/tr>\n    <tr><td style=\"padding:4px\"><b>Referencia<\/b><\/td><td>${v.id}<\/td><\/tr>\n  <\/table>`;\n}\nfunction rtRatioReport(RT,target){\n  if(!target)return'';\n  const r=RT\/target;\n  if(r<=1)return `<p style=\"color:#1e9e63;font-weight:bold\">La reverberaci\u00f3n (${f3(RT)} s) est\u00e1 dentro del objetivo (${f3(target)} s).<\/p>`;\n  const v=(Math.round(r*10)\/10+'').replace('.',',');\n  return `<p style=\"padding:10px;background:#fbeae5;border-left:4px solid #d94b46;font-size:14px\"><b>La reverberaci\u00f3n actual es ${v} veces mayor que el objetivo.<\/b><br>Actual ${f3(RT)} s frente a un objetivo de ${f3(target)} s. Cuanto mayor es este valor, m\u00e1s eco tiene la sala y peor se entiende la palabra.<\/p>`;\n}\nfunction paramsReport(V,RT,target){\n  const p=acousticParams(V,RT,target);if(!p)return'';\n  const d=interpD50(p.D50),s=interpSTI(p.STI),cc=interpC50(p.C50),gg=interpG(p.G);\n  const th='background:#34516e;color:#fff;border:1px solid #2c4258;padding:7px 9px;text-align:left;font-size:12px';\n  const td='border:1px solid #dfe6ec;padding:6px 9px;font-size:12px';\n  const row=(name,val,tag,desc)=>`<tr><td style=\"${td}\">${name}<\/td><td style=\"${td};text-align:right;font-weight:700\">${val}<\/td><td style=\"${td}\"><b>${tag}<\/b> \u2014 ${desc}<\/td><\/tr>`;\n  return `<table style=\"border-collapse:collapse;width:100%;margin-top:6px\">\n    <tr><th style=\"${th}\">Par\u00e1metro<\/th><th style=\"${th}\">Valor<\/th><th style=\"${th}\">Qu\u00e9 significa<\/th><\/tr>\n    <tr><td style=\"${td}\">Calidad sonora global<\/td><td style=\"${td};text-align:right;font-weight:700\">${p.quality}<\/td><td style=\"${td}\">Valoraci\u00f3n general del confort ac\u00fastico de la sala.<\/td><\/tr>\n    ${row('Definici\u00f3n (D50)',Math.round(p.D50*100)+'%',d[0],d[1])}\n    ${row('Claridad (C50)',(p.C50>=0?'+':'')+(Math.round(p.C50*10)\/10)+' dB',cc[0],cc[1])}\n    ${row('Fuerza sonora (G)',(p.G>=0?'+':'')+(Math.round(p.G*10)\/10)+' dB',gg[0],gg[1])}\n    ${row('Inteligibilidad (STI)',(Math.round(p.STI*100)\/100),s[0],s[1])}\n  <\/table>`;\n}\n\/* Gr\u00e1fico de barras horizontal para el informe (situaci\u00f3n actual vs con tratamiento) *\/\nfunction barChartReport(nowRT,impRT,target){\n  const W=620,rowH=48,top=28,pad=150;\n  const maxV=Math.max(nowRT,impRT||0,target)*1.18||1;\n  const sx=v=>pad+(v\/maxV)*(W-pad-40);\n  let y=top, rows='';\n  const bar=(v,col,lab,vt)=>{const x=sx(v);const r=`<text x=\"${pad-12}\" y=\"${y+21}\" text-anchor=\"end\" font-size=\"12\" fill=\"#243b56\" font-weight=\"700\">${lab}<\/text>\n    <rect x=\"${pad}\" y=\"${y+6}\" width=\"${Math.max(3,x-pad)}\" height=\"22\" fill=\"${col}\" rx=\"3\"\/>\n    <text x=\"${x+7}\" y=\"${y+22}\" font-size=\"12\" fill=\"#243b56\" font-weight=\"700\">${vt}<\/text>`;y+=rowH;return r;};\n  rows+=bar(nowRT,'#c0392b','Situaci\u00f3n actual',f3(nowRT)+' s \u2717');\n  if(impRT)rows+=bar(impRT,'#1f8a52','Con tratamiento',f3(impRT)+' s \u2713');\n  const tx=sx(target), H=y+10;\n  return `<svg width=\"100%\" viewBox=\"0 0 ${W} ${H}\" font-family=\"Arial,sans-serif\" style=\"margin:6px 0\">\n    <line x1=\"${tx}\" y1=\"14\" x2=\"${tx}\" y2=\"${y-6}\" stroke=\"#1f7a78\" stroke-width=\"2\" stroke-dasharray=\"5 4\"\/>\n    <text x=\"${tx}\" y=\"10\" text-anchor=\"middle\" font-size=\"10\" fill=\"#1f7a78\" font-weight=\"700\">Objetivo ${f3(target)} s<\/text>\n    ${rows}\n  <\/svg>`;\n}\nfunction reportHTML(v){\n  const res=v.result, msg=cfg('reportMsg',''), company=cfg('company','ACUSTIUM');\n  const cumple=res.compliant===true;\n  const tRT=(res.limit&&res.limit.rtLimit!=null)?res.limit.rtLimit:(res.absBased?0.8:null);\n  const nowRT=res.rtMean, impRT=(v.improvedRt!=null?v.improvedRt:(v.proposedRt!=null?v.proposedRt:null));\n  const meses=['enero','febrero','marzo','abril','mayo','junio','julio','agosto','septiembre','octubre','noviembre','diciembre'];\n  const dt=new Date(v.createdAt||Date.now()); const fecha=(meses[dt.getMonth()][0].toUpperCase()+meses[dt.getMonth()].slice(1))+' '+dt.getFullYear();\n  const prov=(v.client&&v.client.province)?v.client.province:(v.client&&v.client.cp?('CP '+v.client.cp):'\u2014');\n  const qNow=acousticParams(v.room.volume,nowRT,tRT);\n  const qImp=impRT?acousticParams(v.room.volume,impRT,tRT):null;\n  \/\/ estilos reutilizables\n  const H2=(t)=>`<h2 style=\"color:#243b56;border-bottom:2px solid #1f7a78;padding-bottom:5px;margin:24px 0 10px;font-size:16px;font-weight:800\">${t}<\/h2>`;\n  const TH='background:#34516e;color:#fff;border:1px solid #2c4258;padding:7px 9px;text-align:left;font-size:12px';\n  const TD='border:1px solid #dfe6ec;padding:6px 9px;font-size:12px';\n  const okCell=(ok)=>`<td style=\"${TD};background:${ok?'#1f8a52':'#c0392b'};color:#fff;font-weight:700;text-align:center\">${ok?'\u2713 S\u00cd':'\u2717 NO'}<\/td>`;\n  \/\/ banda\n  const filas=BANDS.map(b=>`<tr><td style=\"${TD}\">${b} Hz<\/td><td style=\"${TD};text-align:right\">${f(res.A[b])}<\/td><td style=\"${TD};text-align:right\">${f3(res.T[b])}<\/td><\/tr>`).join('');\n  \/\/ propuesta\n  let prop='';\n  if(v.chosenSystem){\n    const cls=isoClass(v.chosenSystem.am||0);\n    prop=`${H2('3. Propuesta de tratamiento')}\n    <table style=\"border-collapse:collapse;width:100%\">\n      <tr><th style=\"${TH}\">Par\u00e1metro<\/th><th style=\"${TH}\">Valor<\/th><\/tr>\n      <tr><td style=\"${TD}\">Sistema propuesto<\/td><td style=\"${TD};font-weight:700\">${esc(v.chosenSystem.name)}<\/td><\/tr>\n      <tr><td style=\"${TD}\">Material absorbente<\/td><td style=\"${TD}\">${esc(v.chosenSystem.mat||'Alta absorci\u00f3n (Clase A \u00b7 \u03b1w 0,95)')}<\/td><\/tr>\n      <tr><td style=\"${TD}\">Superficie a tratar (estimada)<\/td><td style=\"${TD};font-weight:700\">${f(v.chosenSystem.area||v.improvedArea||0)} m\u00b2<\/td><\/tr>\n      <tr><td style=\"${TD}\">\u03b1w \/ Clase ISO 11654<\/td><td style=\"${TD}\"><b>${f3(v.chosenSystem.am||0.95)}<\/b> \u00b7 <span style=\"background:${cls.color};color:#fff;padding:1px 8px;border-radius:4px;font-weight:700\">${cls.clase}<\/span> ${esc(cls.label)}<\/td><\/tr>\n      <tr><td style=\"${TD}\">Reverberaci\u00f3n estimada tras el tratamiento<\/td>${okCell(impRT!=null&&tRT!=null&&impRT<=tRT)}<\/tr>\n    <\/table>\n    <p style=\"font-size:12px;margin-top:8px\">Reverberaci\u00f3n media estimada tras el tratamiento: <b>${impRT!=null?f3(impRT)+' s':'\u2014'}<\/b>${tRT!=null&&impRT!=null?(impRT<=tRT?' \u2014 cumplir\u00eda el objetivo del CTE DB-HR.':' \u2014 reducci\u00f3n significativa.'):''}<\/p>\n    ${isoTableHTML()}`;\n  }\n  return `<div style=\"font-family:Arial,Helvetica,'Segoe UI',sans-serif;color:#243b56;max-width:820px;margin:0 auto;padding:30px\">\n    <style>@page{margin:1.6cm} table{page-break-inside:avoid}<\/style>\n\n    <!-- PORTADA -->\n    ${acustiumLogo(44)}\n    <h1 style=\"color:#243b56;font-size:30px;font-weight:800;line-height:1.12;margin:20px 0 4px\">DIAGN\u00d3STICO DE AC\u00daSTICA INTERIOR<\/h1>\n    <p style=\"color:#6b7785;font-size:13px;margin:0 0 14px\">An\u00e1lisis de la reverberaci\u00f3n \u00b7 Propuesta de acondicionamiento ac\u00fastico \u00b7 Cobertura CTE DB-HR<\/p>\n    <hr style=\"border:none;border-top:1.5px solid #cfdbe2;margin:12px 0 20px\">\n    <table style=\"border-collapse:collapse;width:100%\">\n      <tr><th style=\"${TH};width:30%\">Campo<\/th><th style=\"${TH}\">Informaci\u00f3n<\/th><\/tr>\n      <tr><td style=\"${TD};font-weight:700\">Proyecto<\/td><td style=\"${TD}\">Acondicionamiento ac\u00fastico \u2014 ${esc(useLabel(v.room.type))}<\/td><\/tr>\n      <tr><td style=\"${TD};font-weight:700\">Local<\/td><td style=\"${TD}\">${esc(prov)}<\/td><\/tr>\n      <tr><td style=\"${TD};font-weight:700\">Cliente<\/td><td style=\"${TD}\">${esc(v.client.name)} ${esc(v.client.surname)}<\/td><\/tr>\n      <tr><td style=\"${TD};font-weight:700\">Redacta<\/td><td style=\"${TD}\">${esc(company)}<\/td><\/tr>\n      <tr><td style=\"${TD};font-weight:700\">Fecha<\/td><td style=\"${TD}\">${fecha}<\/td><\/tr>\n      <tr><td style=\"${TD};font-weight:700\">Referencia<\/td><td style=\"${TD}\">${esc(v.id)}<\/td><\/tr>\n    <\/table>\n    <p style=\"color:#1f7a78;font-size:11px;margin-top:14px\">${esc(company)} \u00b7 Dise\u00f1ando el Sonido \u00b7 www.acustium.com<\/p>\n\n    <!-- 1. RESUMEN -->\n    ${H2('1. Resumen')}\n    <p style=\"font-size:13px;line-height:1.5\">Se ha analizado el acondicionamiento ac\u00fastico de <b>${esc(useLabel(v.room.type))}<\/b> (${f(v.room.volume)} m\u00b3, planta ${f(v.room.area)} m\u00b2). En su estado actual el recinto ${cumple?'cumple':'no cumple'} el objetivo del CTE DB-HR. A continuaci\u00f3n se compara la situaci\u00f3n actual con la soluci\u00f3n de acondicionamiento de alta absorci\u00f3n propuesta.<\/p>\n    <table style=\"border-collapse:collapse;width:100%;margin-top:6px\">\n      <tr><th style=\"${TH}\">Escenario<\/th><th style=\"${TH}\">T\u2086\u2080 estimado<\/th><th style=\"${TH}\">\u00bfCumple CTE${tRT!=null?' \u2264 '+f3(tRT)+' s':''}?<\/th><th style=\"${TH}\">Valoraci\u00f3n<\/th><\/tr>\n      <tr><td style=\"${TD};font-weight:700\">Situaci\u00f3n actual (sin tratar)<\/td><td style=\"${TD};text-align:right\">${f3(nowRT)} s<\/td>${okCell(cumple)}<td style=\"${TD}\">${qNow?qNow.quality:'\u2014'}<\/td><\/tr>\n      ${impRT!=null?`<tr><td style=\"${TD};font-weight:700\">Con tratamiento (Clase A \u00b7 \u03b1w 0,95)<\/td><td style=\"${TD};text-align:right\">${f3(impRT)} s<\/td>${okCell(tRT!=null&&impRT<=tRT)}<td style=\"${TD}\">${qImp?qImp.quality:'\u2014'}<\/td><\/tr>`:''}\n    <\/table>\n    ${barChartReport(nowRT,impRT,tRT||0.7)}\n\n    <!-- 2. SITUACI\u00d3N ACTUAL -->\n    ${H2('2. Situaci\u00f3n actual del espacio')}\n    <p style=\"font-size:13px;line-height:1.5\">Los c\u00e1lculos se han realizado con el recinto <b>vac\u00edo<\/b>, sin ocupaci\u00f3n ni mobiliario, que es la condici\u00f3n m\u00e1s desfavorable y coherente con el m\u00e9todo general del CTE DB-HR.<\/p>\n    ${rtRatioReport(nowRT,tRT)}\n    <p style=\"font-weight:700;color:${cumple?'#1f8a52':'#c0392b'};font-size:13px;margin:8px 0 4px\">${esc(res.reason)}<\/p>\n    <p style=\"font-size:12px;font-weight:700;margin:14px 0 4px;color:#243b56\">Reverberaci\u00f3n por banda de frecuencia<\/p>\n    <table style=\"border-collapse:collapse;width:100%\">\n      <tr><th style=\"${TH}\">Banda<\/th><th style=\"${TH}\">Absorci\u00f3n A (m\u00b2)<\/th><th style=\"${TH}\">T\u2086\u2080 (s)<\/th><\/tr>\n      ${filas}\n    <\/table>\n    <p style=\"font-size:12px;font-weight:700;margin:16px 0 4px;color:#243b56\">Par\u00e1metros ac\u00fasticos \u2014 qu\u00e9 significan<\/p>\n    ${paramsReport(v.room.volume,nowRT,tRT)}\n\n    ${prop}\n\n    <!-- SIGUIENTES PASOS -->\n    ${H2((v.chosenSystem?'4':'3')+'. Siguientes pasos')}\n    <table style=\"border-collapse:collapse;width:100%\">\n      <tr><th style=\"${TH};width:8%\">Paso<\/th><th style=\"${TH}\">Acci\u00f3n<\/th><\/tr>\n      <tr><td style=\"${TD};text-align:center;font-weight:700\">1<\/td><td style=\"${TD}\">Confirmar la superficie objetivo de tratamiento y su distribuci\u00f3n en el recinto.<\/td><\/tr>\n      <tr><td style=\"${TD};text-align:center;font-weight:700\">2<\/td><td style=\"${TD}\">Revisar las instalaciones existentes (luminarias, difusores, conductos) para definir el layout definitivo.<\/td><\/tr>\n      <tr><td style=\"${TD};text-align:center;font-weight:700\">3<\/td><td style=\"${TD}\">Confirmar el sistema y el acabado est\u00e9tico seg\u00fan preferencia del cliente.<\/td><\/tr>\n      <tr><td style=\"${TD};text-align:center;font-weight:700\">4<\/td><td style=\"${TD}\">Aceptaci\u00f3n de oferta: se elaborar\u00e1 oferta econ\u00f3mica detallada una vez confirmados los metros cuadrados definitivos.<\/td><\/tr>\n      <tr><td style=\"${TD};text-align:center;font-weight:700\">5<\/td><td style=\"${TD}\">Instalaci\u00f3n por equipo propio de ${esc(company)}, ajustada al sistema seleccionado.<\/td><\/tr>\n    <\/table>\n\n    ${msg?`<p style=\"margin-top:16px;padding:10px 12px;background:#eef6f6;border-left:3px solid #1f7a78;font-size:12px\">${esc(msg)}<\/p>`:''}\n\n    <p style=\"font-size:11px;color:#6b7785;margin-top:20px;line-height:1.5\"><b>Nota:<\/b> los valores de este informe son una estimaci\u00f3n de car\u00e1cter orientativo (m\u00e9todo de Sabine, T = 0,16\u00b7V\/A, sobre 500\/1000\/2000 Hz; clasificaci\u00f3n seg\u00fan ISO 11654 con \u03b1w estimado). Para un informe t\u00e9cnico con validez normativa, los par\u00e1metros deben obtenerse mediante medici\u00f3n ac\u00fastica real en el recinto con equipos calibrados seg\u00fan UNE-EN ISO 3382.<\/p>\n\n    <hr style=\"border:none;border-top:1px solid #cfdbe2;margin:18px 0 8px\">\n    <p style=\"text-align:center;font-size:10px;color:#9aa6b2\">Documento de diagn\u00f3stico \u2014 ${fecha} \u00b7 ${esc(company)} \u00b7 Dise\u00f1ando el Sonido \u00b7 www.acustium.com<\/p>\n  <\/div>`;\n}\nfunction download(filename,content,mime){\n  const blob=new Blob(['\\ufeff',content],{type:mime});\n  const a=document.createElement('a');a.href=URL.createObjectURL(blob);a.download=filename;a.click();\n  setTimeout(()=>URL.revokeObjectURL(a.href),2000);\n  markReviewed(filename);\n}\nfunction markReviewed(){}\nfunction genReport(i){const v=DB.get('valuations',[])[i];\n  download(`Informe_${v.id}.doc`,'<html><head><meta charset=\"utf-8\"><\/head><body>'+reportHTML(v)+'<\/body><\/html>','application\/msword');\n  setStatus(i,'revisada');}\nfunction genPDF(i){const v=DB.get('valuations',[])[i];\n  const w=window.open('','_blank');\n  w.document.write('<html><head><title>Informe '+v.id+'<\/title><\/head><body onload=\"window.print()\">'+reportHTML(v)+'<\/body><\/html>');\n  w.document.close();setStatus(i,'revisada');}\nfunction genOffer(i){const v=DB.get('valuations',[])[i];const ch=v.chosenSystem;\n  const logo=cfg('logo',null),cover=cfg('cover',null);\n  const dtoPct=cfg('offerDto',0), ivaPct=cfg('offerIva',21);\n  const lines=ch?[{d:ch.name+' \u2014 '+ch.mat,q:ch.area,p:ch.price,t:ch.area*(ch.price||0)}]:[];\n  const rows=lines.map(l=>`<tr><td>${esc(l.d)}<\/td><td style=\"text-align:right\">${f(l.q)}<\/td><td style=\"text-align:right\">${f(l.p)}<\/td><td style=\"text-align:right\">${f(l.t)}<\/td><\/tr>`).join('');\n  const subtotal=lines.reduce((s,l)=>s+l.t,0);\n  const dto=subtotal*dtoPct\/100, base=subtotal-dto, iva=base*ivaPct\/100, total=base+iva;\n  const html=`<html xmlns:x=\"urn:schemas-microsoft-com:office:excel\"><head><meta charset=\"utf-8\"><\/head><body>\n    ${cover?`<img decoding=\"async\" src=\"${cover}\" width=\"300\"><br>`:''}${logo?`<img decoding=\"async\" src=\"${logo}\" height=\"50\"><br>`:''}\n    <h2>Presupuesto ${v.id} \u2014 ${esc(v.client.name)} ${esc(v.client.surname)}<\/h2>\n    <table border=\"1\" cellspacing=\"0\" cellpadding=\"4\">\n      <tr style=\"background:#eef6f6\"><th>Descripci\u00f3n<\/th><th>Uds (m\u00b2)<\/th><th>Precio \u20ac\/ud<\/th><th>Importe \u20ac<\/th><\/tr>\n      ${rows}\n      <tr><td colspan=\"3\" style=\"text-align:right\"><b>Subtotal<\/b><\/td><td style=\"text-align:right\">${f(subtotal)}<\/td><\/tr>\n      <tr><td colspan=\"3\" style=\"text-align:right\">Descuento ${f(dtoPct)}%<\/td><td style=\"text-align:right\">-${f(dto)}<\/td><\/tr>\n      <tr><td colspan=\"3\" style=\"text-align:right\">Base imponible<\/td><td style=\"text-align:right\">${f(base)}<\/td><\/tr>\n      <tr><td colspan=\"3\" style=\"text-align:right\">IVA ${f(ivaPct)}%<\/td><td style=\"text-align:right\">${f(iva)}<\/td><\/tr>\n      <tr><td colspan=\"3\" style=\"text-align:right\"><b>TOTAL<\/b><\/td><td style=\"text-align:right\"><b>${f(total)}<\/b><\/td><\/tr>\n    <\/table>\n    <p style=\"font-size:11px;color:#666\">Descuento e impuestos configurables en Zona interna \u2192 Configuraci\u00f3n.<\/p>\n    <\/body><\/html>`;\n  download(`Presupuesto_${v.id}.xls`,html,'application\/vnd.ms-excel');setStatus(i,'oferta_enviada');}\nfunction genConditions(i){const v=DB.get('valuations',[])[i];\n  const tmpl=cfg('conditionsTmpl',DEFAULT_CONDITIONS);\n  const filled=tmpl.replace(\/\\{cliente\\}\/g,`${v.client.name} ${v.client.surname}`).replace(\/\\{ref\\}\/g,v.id).replace(\/\\{fecha\\}\/g,new Date().toLocaleDateString('es-ES'));\n  download(`Condiciones_${v.id}.doc`,docHeader()+'<h1 style=\"color:#0e7c7b\">Condiciones particulares de venta<\/h1>'+valLine(v)+'<div style=\"white-space:pre-wrap;font-size:13px;margin-top:12px\">'+esc(filled)+'<\/div><\/div>','application\/msword');}\nfunction sendFollowUp(i){\n  const v=DB.get('valuations',[])[i];if(!v)return;\n  const sub=encodeURIComponent('Seguimiento consulta ac\u00fastica \u2014 '+v.id);\n  const body=encodeURIComponent('Estimado\/a '+v.client.name+',\\n\\nNos ponemos en contacto en relaci\u00f3n a su consulta de acondicionamiento ac\u00fastico (ref. '+v.id+').\\n\\n[Escribe aqu\u00ed el mensaje de seguimiento]\\n\\nAtentamente,\\nEl equipo comercial');\n  window.open('mailto:'+v.client.email+'?subject='+sub+'&body='+body,'_blank');\n  setStatus(i,'seguimiento');\n}\nfunction setStatus(i,st){const l=DB.get('valuations',[]);if(l[i]){l[i].status=st;DB.set('valuations',l);}renderValuations();}\n\nconst DEFAULT_CONDITIONS=`1. Validez de la oferta: 30 d\u00edas desde la fecha {fecha}.\n2. Forma de pago: 50% a la aceptaci\u00f3n, 50% a la entrega.\n3. Plazo de ejecuci\u00f3n: a convenir seg\u00fan disponibilidad de material.\n4. Garant\u00eda: 2 a\u00f1os sobre materiales y montaje.\n5. La presente oferta (ref. {ref}) se emite para {cliente} y queda sujeta a revisi\u00f3n t\u00e9cnica final en obra.\n6. Los c\u00e1lculos ac\u00fasticos son estimativos seg\u00fan DB-HR; mediciones in situ pueden ajustar la soluci\u00f3n.`;\n\n\/* ----- CONFIGURACI\u00d3N ----- *\/\nfunction renderConfig(){\n  const body=document.getElementById('internalBody');\n  body.innerHTML=`<h2>Configuraci\u00f3n de la herramienta<\/h2>\n\n  <fieldset><legend>Im\u00e1genes y marca<\/legend>\n    ${imgField('Logo (cabecera de documentos)','logo')}\n    ${imgField('Imagen de portada (oferta)','cover')}\n    ${imgField('Dibujo de fondo (informes\/ofertas)','bg')}\n  <\/fieldset>\n\n  <fieldset><legend>Mensaje para informes y ofertas<\/legend>\n    <textarea id=\"cfg_msg\" rows=\"3\" placeholder=\"Texto opcional que aparecer\u00e1 en los informes...\">${esc(cfg('reportMsg',''))}<\/textarea>\n    <button class=\"btn btn-sm\" style=\"margin-top:.5rem\" onclick=\"DB.set('cfg.reportMsg',val('cfg_msg'));toast('Mensaje guardado')\">Guardar mensaje<\/button>\n  <\/fieldset>\n\n  <fieldset><legend>Plantilla de condiciones particulares<\/legend>\n    <textarea id=\"cfg_cond\" rows=\"6\">${esc(cfg('conditionsTmpl',DEFAULT_CONDITIONS))}<\/textarea>\n    <p class=\"hint\">Variables: {cliente}, {ref}, {fecha}<\/p>\n    <button class=\"btn btn-sm\" onclick=\"DB.set('cfg.conditionsTmpl',val('cfg_cond'));toast('Plantilla guardada')\">Guardar plantilla<\/button>\n  <\/fieldset>\n\n  <fieldset><legend>Documentaci\u00f3n de ejemplo (referencia)<\/legend>\n    <p class=\"hint\">Sube ejemplos de informe, oferta y condiciones para que el equipo los tenga a mano.<\/p>\n    ${docField('Ejemplo de informe','ex_report')}\n    ${docField('Ejemplo de oferta','ex_offer')}\n    ${docField('Ejemplo de condiciones','ex_cond')}\n  <\/fieldset>\n\n  <fieldset><legend>Fichas t\u00e9cnicas \/ materiales (para calcular el RT)<\/legend>\n    <p class=\"hint\"><b>Fuente:<\/b> ACUSTIUM BBDD Materiales Master v1 \u2014 <b>Lista simplificada<\/b> (42 materiales, usada en el calculador cliente) \u00b7 <b>Lista extensa<\/b> (90 materiales, referencia t\u00e9cnica). Coeficientes \u03b1 medidos seg\u00fan ISO 354.<\/p>\n    <div id=\"matTable\"><\/div>\n    <button class=\"btn btn-sm\" onclick=\"addMaterial()\">+ A\u00f1adir material<\/button>\n    <button class=\"btn btn-sm\" onclick=\"DB.set('cfg.materials',ACUSTIUM_SIMPLIFICADA);toast('Lista simplificada cargada (42 mat.)');renderConfig()\">\u21ba Cargar lista simplificada<\/button>\n    <button class=\"btn btn-sm\" onclick=\"DB.set('cfg.materials',ACUSTIUM_EXTENSA);toast('Lista extensa cargada (90 mat.)');renderConfig()\">\ud83d\udcda Cargar lista extensa (90 mat.)<\/button>\n    <span style=\"display:inline-block;width:1px;height:20px;background:var(--line);vertical-align:middle;margin:0 .4rem\"><\/span>\n    <button class=\"btn btn-sm\" onclick=\"document.getElementById('impMat').click()\">\ud83d\udce5 Importar archivo (CSV)<\/button>\n    <input id=\"impMat\" type=\"file\" accept=\".csv,.xlsx,.xls,.pdf,text\/csv\" class=\"hide\" onchange=\"importMaterials(event)\">\n    <button class=\"btn btn-sm\" onclick=\"downloadMatTemplate()\">\u2b07\ufe0f Descargar plantilla<\/button>\n    <p class=\"hint\">Formato CSV (separador <b>;<\/b>): <code>nombre;tipo;aplicacion;a125;a250;a500;a1000;a2000;a4000;precio<\/code>. tipo = acabado|sistema \u00b7 aplicacion = todas|techo|suelo|pared \u00b7 decimales con coma o punto. Si el nombre ya existe, se actualiza.<\/p>\n  <\/fieldset>\n\n  <fieldset><legend>Sistemas<\/legend>\n    <div id=\"sysTable\"><\/div>\n    <button class=\"btn btn-sm\" onclick=\"addSystem()\">+ A\u00f1adir sistema<\/button>\n    <span style=\"display:inline-block;width:1px;height:20px;background:var(--line);vertical-align:middle;margin:0 .4rem\"><\/span>\n    <button class=\"btn btn-sm\" onclick=\"document.getElementById('impSys').click()\">\ud83d\udce5 Importar archivo (CSV)<\/button>\n    <input id=\"impSys\" type=\"file\" accept=\".csv,text\/csv\" class=\"hide\" onchange=\"importSystems(event)\">\n    <button class=\"btn btn-sm\" onclick=\"downloadSysTemplate()\">\u2b07\ufe0f Descargar plantilla<\/button>\n    <p class=\"hint\">Formato CSV (separador <b>;<\/b>): <code>sistema;material;precio<\/code>. \u00abmaterial\u00bb = nombre exacto de una ficha t\u00e9cnica (si no existe, se crea vac\u00eda y deber\u00e1s completar sus \u03b1). \u00abprecio\u00bb actualiza el \u20ac\/m\u00b2 de ese material.<\/p>\n  <\/fieldset>\n\n  <fieldset><legend>Identificaci\u00f3n IA (fotos y planos)<\/legend>\n    <p class=\"hint\">Con una API key de Claude las fotos y planos se analizan autom\u00e1ticamente al subirlos (visi\u00f3n IA). Sin key, se usan elementos de demostraci\u00f3n.<\/p>\n    <div class=\"fld\" style=\"max-width:460px\"><label>Claude API Key<\/label>\n      <input id=\"cfg_apikey\" type=\"password\" value=\"${esc(cfg('claudeApiKey',''))}\" placeholder=\"sk-ant-\u2026\">\n      <span class=\"hint\">Se guarda solo en este navegador. Obt\u00e9n la tuya en <a href=\"https:\/\/console.anthropic.com\" target=\"_blank\">console.anthropic.com<\/a>.<\/span>\n    <\/div>\n    <button class=\"btn btn-sm\" onclick=\"DB.set('cfg.claudeApiKey',val('cfg_apikey'));toast('API key guardada')\">Guardar key<\/button>\n  <\/fieldset>\n\n  <fieldset><legend>Presupuesto cliente \u2014 descuento e impuestos<\/legend>\n    <div class=\"grid2\" style=\"max-width:460px\">\n      <div class=\"fld\"><label>Descuento por defecto (%)<\/label><input id=\"cfg_dto\" type=\"number\" step=\"0.1\" value=\"${cfg('offerDto',0)}\"><\/div>\n      <div class=\"fld\"><label>IVA \/ impuestos (%)<\/label><input id=\"cfg_iva\" type=\"number\" step=\"0.1\" value=\"${cfg('offerIva',21)}\"><\/div>\n    <\/div>\n    <button class=\"btn btn-sm\" onclick=\"DB.set('cfg.offerDto',parseFloat(val('cfg_dto'))||0);DB.set('cfg.offerIva',parseFloat(val('cfg_iva'))||0);toast('Guardado')\">Guardar<\/button>\n    <p class=\"hint\">El presupuesto del cliente mostrar\u00e1 Uds \u00b7 Precio \u00b7 Importe \u00b7 Descuento \u00b7 IVA \u00b7 Total.<\/p>\n  <\/fieldset>\n\n  <fieldset><legend>Clave de acceso interna<\/legend>\n    <div class=\"fld\" style=\"max-width:320px\"><input id=\"cfg_pass\" value=\"${esc(internalPass())}\"><\/div>\n    <button class=\"btn btn-sm\" onclick=\"DB.set('cfg.internalPass',val('cfg_pass'));toast('Clave actualizada')\">Guardar clave<\/button>\n  <\/fieldset>`;\n  renderMatTable();renderSysTable();\n}\nfunction imgField(label,key){\n  const v=cfg(key,null);\n  return `<div class=\"fld\"><label>${label}<\/label>\n    <div style=\"display:flex;gap:.8rem;align-items:center\">\n      ${v?`<img decoding=\"async\" src=\"${v}\" style=\"height:48px;border:1px solid var(--line);border-radius:6px\">`:'<span class=\"hint\">sin imagen<\/span>'}\n      <input type=\"file\" accept=\"image\/*\" onchange=\"cfgImg(event,'${key}')\">\n      ${v?`<button class=\"btn btn-sm btn-danger\" onclick=\"DB.set('cfg.${key}',null);renderConfig()\">Quitar<\/button>`:''}\n    <\/div><\/div>`;\n}\nfunction cfgImg(ev,key){const file=ev.target.files[0];if(!file)return;const r=new FileReader();r.onload=()=>{DB.set('cfg.'+key,r.result);renderConfig();};r.readAsDataURL(file);}\nfunction docField(label,key){\n  const v=cfg(key,null);\n  return `<div class=\"fld\"><label>${label}<\/label>\n    <div style=\"display:flex;gap:.8rem;align-items:center\">\n      ${v?`<a href=\"${v.data}\" download=\"${esc(v.name)}\">${esc(v.name)}<\/a>`:'<span class=\"hint\">sin archivo<\/span>'}\n      <input type=\"file\" onchange=\"cfgDoc(event,'${key}')\">\n      ${v?`<button class=\"btn btn-sm btn-danger\" onclick=\"DB.set('cfg.${key}',null);renderConfig()\">Quitar<\/button>`:''}\n    <\/div><\/div>`;\n}\nfunction cfgDoc(ev,key){const file=ev.target.files[0];if(!file)return;const r=new FileReader();r.onload=()=>{DB.set('cfg.'+key,{name:file.name,data:r.result});renderConfig();};r.readAsDataURL(file);}\nfunction renderMatTable(){\n  const el=document.getElementById('matTable');const mats=materials();\n  const appOpt=sel=>['todas','techo','suelo','pared'].map(z=>`<option value=\"${z}\" ${(sel||'todas')===z?'selected':''}>${z}<\/option>`).join('');\n  el.innerHTML=`<table><thead><tr><th>Nombre<\/th><th>Tipo<\/th><th>Aplicaci\u00f3n<\/th>${BANDS.map(b=>`<th class=\"num\">${b}<\/th>`).join('')}<th class=\"num\">\u20ac\/m\u00b2<\/th><th><\/th><\/tr><\/thead><tbody>`+\n   mats.map((m,i)=>`<tr>\n     <td><input value=\"${esc(m.name)}\" onchange=\"updMat(${i},'name',this.value)\" style=\"min-width:150px\"><\/td>\n     <td><select onchange=\"updMat(${i},'kind',this.value)\"><option value=\"acabado\" ${m.kind==='acabado'?'selected':''}>acabado<\/option><option value=\"sistema\" ${m.kind==='sistema'?'selected':''}>sistema<\/option><\/select><\/td>\n     <td><select onchange=\"updMat(${i},'app',this.value)\">${appOpt(m.app)}<\/select><\/td>\n     ${BANDS.map((b,j)=>`<td class=\"num\"><input type=\"number\" step=\"0.01\" value=\"${m.a[j]}\" onchange=\"updMatA(${i},${j},this.value)\" style=\"width:56px;text-align:right\"><\/td>`).join('')}\n     <td class=\"num\"><input type=\"number\" step=\"0.5\" value=\"${m.price}\" onchange=\"updMat(${i},'price',parseFloat(this.value)||0)\" style=\"width:62px;text-align:right\"><\/td>\n     <td><button class=\"btn btn-sm btn-danger\" onclick=\"delMat(${i})\">\u2715<\/button><\/td>\n   <\/tr>`).join('')+`<\/tbody><\/table>`;\n}\nfunction updMat(i,k,v){const m=materials();m[i][k]=v;DB.set('cfg.materials',m);}\nfunction updMatA(i,j,v){const m=materials();m[i].a[j]=parseFloat(v)||0;DB.set('cfg.materials',m);}\nfunction delMat(i){const m=materials();m.splice(i,1);DB.set('cfg.materials',m);renderMatTable();}\nfunction addMaterial(){const m=materials();m.push({id:'m'+Date.now(),name:'Nuevo material',kind:'sistema',app:'todas',a:[0,0,0,0,0,0],price:0});DB.set('cfg.materials',m);renderMatTable();}\n\n\/* ---- Importaci\u00f3n CSV (separador ; ; decimales con coma o punto) ---- *\/\nfunction parseCSV(text){\n  const sep=text.indexOf(';')>=0?';':',';\n  return text.replace(\/\\r\/g,'').split('\\n').map(l=>l.trim()).filter(l=>l.length)\n    .map(l=>l.split(sep).map(c=>c.trim().replace(\/^\"|\"$\/g,'')));\n}\nconst numES=v=>parseFloat(String(v).replace(',','.'))||0;\nconst slug=s=>String(s).toLowerCase().replace(\/[^a-z0-9]+\/g,'_').replace(\/^_|_$\/g,'')||('m'+Date.now());\n\/* \u2500\u2500 Carga din\u00e1mica de SheetJS (necesita internet la primera vez) \u2500\u2500 *\/\nfunction loadSheetJS(){\n  if(window.XLSX)return Promise.resolve(window.XLSX);\n  return new Promise((res,rej)=>{\n    const s=document.createElement('script');\n    s.src='https:\/\/cdnjs.cloudflare.com\/ajax\/libs\/xlsx\/0.18.5\/xlsx.full.min.js';\n    s.onload=()=>window.XLSX?res(window.XLSX):rej(new Error('No se carg\u00f3 XLSX'));\n    s.onerror=()=>rej(new Error('Sin conexi\u00f3n para cargar la librer\u00eda Excel.\\nConvierte el archivo a CSV (Archivo \u2192 Guardar como \u2192 CSV UTF-8) e importa ese.'));\n    document.head.appendChild(s);\n  });\n}\n\/* Procesa un array de arrays [cabecera, ...filas] igual para CSV y Excel *\/\nfunction processMaterialRows(rows){\n  if(rows.length<2)throw new Error('Archivo vac\u00edo o sin cabecera');\n  const head=rows[0].map(h=>String(h||'').toLowerCase().trim());\n  const idx=n=>head.indexOf(n);\n  if(idx('nombre')<0)throw new Error('Falta columna \"nombre\"');\n  const list=materials();let added=0,upd=0;\n  for(let i=1;i<rows.length;i++){\n    const c=rows[i];const nm=String(c[idx('nombre')]||'').trim();if(!nm)continue;\n    const mat={\n      name:nm,\n      kind:(String(c[idx('tipo')]||'sistema')).toLowerCase()==='acabado'?'acabado':'sistema',\n      app:(()=>{const a=(String(c[idx('aplicacion')]||'todas')).toLowerCase().trim();return['todas','techo','suelo','pared'].includes(a)?a:'todas';})(),\n      a:[numES(c[idx('a125')]),numES(c[idx('a250')]),numES(c[idx('a500')]),numES(c[idx('a1000')]),numES(c[idx('a2000')]),numES(c[idx('a4000')])],\n      price:numES(c[idx('precio')]),\n    };\n    const ex=list.find(m=>m.name.toLowerCase()===mat.name.toLowerCase());\n    if(ex){Object.assign(ex,mat);upd++;}else{list.push({id:slug(mat.name)+'_'+Date.now().toString().slice(-4),...mat});added++;}\n  }\n  DB.set('cfg.materials',list);renderConfig();\n  toast(`Importados: ${added} nuevos, ${upd} actualizados`);\n}\nasync function importMaterials(ev){\n  const file=ev.target.files[0];if(!file)return;\n  const ext=file.name.split('.').pop().toLowerCase();\n  \/\/ PDF: no se puede extraer tablas sin servidor\n  if(ext==='pdf'){\n    alert('Importaci\u00f3n PDF: no es posible extraer tablas directamente en el navegador sin servidor.\\n\\nSoluci\u00f3n: abre el PDF en Excel (Datos \u2192 Obtener datos \u2192 Desde PDF) o c\u00f3pialo a Excel\/CSV, guarda como CSV UTF-8 y vuelve a importar.');\n    ev.target.value='';return;\n  }\n  \/\/ Excel: .xlsx o .xls v\u00eda SheetJS (requiere internet la primera vez para cargar la librer\u00eda)\n  if(ext==='xlsx'||ext==='xls'){\n    try{\n      toast('Cargando librer\u00eda Excel\u2026');\n      const XLSX=await loadSheetJS();\n      const buf=await file.arrayBuffer();\n      const wb=XLSX.read(buf,{type:'array'});\n      const ws=wb.Sheets[wb.SheetNames[0]];\n      const rows=XLSX.utils.sheet_to_json(ws,{header:1,raw:false,defval:''});\n      processMaterialRows(rows);\n    }catch(e){alert('Error al leer el Excel:\\n'+e.message);}\n    ev.target.value='';return;\n  }\n  \/\/ CSV (por defecto)\n  const r=new FileReader();\n  r.onload=()=>{\n    try{processMaterialRows(parseCSV(r.result));}\n    catch(e){alert('Error en el CSV:\\n'+(e.message||'Revisa cabecera y separador \u00ab;\u00bb'));}\n    ev.target.value='';\n  };\n  r.readAsText(file,'utf-8');\n}\nfunction importSystems(ev){\n  const file=ev.target.files[0];if(!file)return;\n  const r=new FileReader();\n  r.onload=()=>{\n    try{\n      const rows=parseCSV(r.result); if(rows.length<2)throw 0;\n      const head=rows[0].map(h=>h.toLowerCase());\n      const idx=n=>head.indexOf(n);\n      const mats=materials(), sys=systems(); let n=0;\n      for(let i=1;i<rows.length;i++){\n        const c=rows[i]; const sName=c[idx('sistema')], mName=c[idx('material')];\n        if(!sName||!mName)continue;\n        let mat=mats.find(m=>m.name.toLowerCase()===mName.toLowerCase());\n        if(!mat){mat={id:slug(mName)+'_'+Date.now().toString().slice(-4),name:mName,kind:'sistema',app:'todas',a:[0,0,0,0,0,0],price:0};mats.push(mat);}\n        if(idx('precio')>=0&&c[idx('precio')]!=='')mat.price=numES(c[idx('precio')]);\n        const exS=sys.find(s=>s.name.toLowerCase()===sName.toLowerCase());\n        if(exS)exS.materialId=mat.id; else sys.push({id:slug(sName)+'_'+Date.now().toString().slice(-4),name:sName,materialId:mat.id});\n        n++;\n      }\n      DB.set('cfg.materials',mats);DB.set('cfg.systems',sys);renderConfig();\n      toast(`Sistemas importados: ${n}`);\n    }catch(e){alert('No se pudo leer el CSV. Revisa cabecera (sistema;material;precio) y separador \u00ab;\u00bb.');}\n    ev.target.value='';\n  };\n  r.readAsText(file,'utf-8');\n}\nfunction downloadMatTemplate(){\n  const csv=['nombre;tipo;aplicacion;a125;a250;a500;a1000;a2000;a4000;precio',\n    'Panel lana de roca 50 mm;sistema;todas;0,20;0,55;0,85;0,95;0,92;0,90;22',\n    'Techo metalico microperforado;sistema;techo;0,30;0,55;0,70;0,75;0,70;0,65;35',\n    'Moqueta;acabado;suelo;0,05;0,10;0,20;0,35;0,45;0,50;0'].join('\\n');\n  download('plantilla_materiales.csv',csv,'text\/csv;charset=utf-8');\n}\nfunction downloadSysTemplate(){\n  const csv=['sistema;material;precio',\n    'Sistema Confort;Panel lana de roca 50 mm;22',\n    'Sistema Economico;Panel lana de roca 40 mm;18',\n    'Sistema Techo Microperforado;Techo metalico microperforado;35'].join('\\n');\n  download('plantilla_sistemas.csv',csv,'text\/csv;charset=utf-8');\n}\nfunction renderSysTable(){\n  const el=document.getElementById('sysTable');const sys=systems(),mats=materials();\n  el.innerHTML=`<table><thead><tr><th>Nombre del sistema\/producto<\/th><th>Categor\u00eda<\/th><th>Aplicaci\u00f3n<\/th><th>Material asociado<\/th><th><\/th><\/tr><\/thead><tbody>`+\n   sys.map((s,i)=>`<tr>\n     <td><input value=\"${esc(s.name)}\" onchange=\"updSys(${i},'name',this.value)\" style=\"min-width:200px\"><\/td>\n     <td><select onchange=\"updSys(${i},'cat',this.value)\">${SYSTEM_CATEGORIES.map(c=>`<option ${c===(s.cat||'Otros')?'selected':''}>${c}<\/option>`).join('')}<\/select><\/td>\n     <td><select onchange=\"updSys(${i},'surf',this.value)\"><option value=\"techo\" ${(s.surf||'techo')==='techo'?'selected':''}>Techo<\/option><option value=\"pared\" ${s.surf==='pared'?'selected':''}>Pared<\/option><\/select><\/td>\n     <td><select onchange=\"updSys(${i},'materialId',this.value)\">${mats.map(m=>`<option value=\"${m.id}\" ${m.id===s.materialId?'selected':''}>${esc(m.name)}<\/option>`).join('')}<\/select><\/td>\n     <td><button class=\"btn btn-sm btn-danger\" onclick=\"delSys(${i})\">\u2715<\/button><\/td>\n   <\/tr>`).join('')+`<\/tbody><\/table>`;\n}\nfunction updSys(i,k,v){const s=systems();s[i][k]=v;DB.set('cfg.systems',s);}\nfunction delSys(i){const s=systems();s.splice(i,1);DB.set('cfg.systems',s);renderSysTable();}\nfunction addSystem(){const s=systems();const m=materials()[0];s.push({id:'s'+Date.now(),name:'Nuevo sistema',materialId:m.id,cat:'Otros',surf:'techo'});DB.set('cfg.systems',s);renderSysTable();}\n\n\/* ============================================================\n   UTILIDADES\n   ============================================================ *\/\nfunction val(id){const e=document.getElementById(id);return e?e.value.trim():'';}\nfunction esc(s){return String(s==null?'':s).replace(\/[&<>\"]\/g,c=>({'&':'&amp;','<':'&lt;','>':'&gt;','\"':'&quot;'}[c]));}\nfunction toast(m){const t=document.createElement('div');t.textContent=m;t.style.cssText='position:fixed;bottom:20px;left:50%;transform:translateX(-50%);background:#0e7c7b;color:#fff;padding:.6rem 1.1rem;border-radius:8px;z-index:99;box-shadow:var(--sh)';document.body.appendChild(t);setTimeout(()=>t.remove(),1800);}\nfunction applyBrand(){const logo=cfg('logo',null);if(logo)document.getElementById('brandBox').innerHTML=`<img decoding=\"async\" src=\"${logo}\"><span>DISE\u00d1ANDO EL SONIDO<\/span>`;}\n\n\/* INIT *\/\napplyBrand();renderStepper();renderStep();\n<\/script>\n\n\n<\/body><\/html>\t\t\t\t<\/div>\n\t\t\n<\/div>\n<div class=\"elementor-element elementor-element-db6bd4d e-flex e-con-boxed e-con e-parent\" data-id=\"db6bd4d\" data-element_type=\"container\" data-e-type=\"container\" data-settings=\"{&quot;background_background&quot;:&quot;classic&quot;}\">\n\t\t\t\t\t<div class=\"e-con-inner\">\n\t\t<div class=\"elementor-element elementor-element-22f6999 e-con-full e-flex e-con e-child\" data-id=\"22f6999\" data-element_type=\"container\" data-e-type=\"container\">\n\t\t\t\t<div class=\"elementor-element elementor-element-c5880ef elementor-invisible elementor-widget elementor-widget-elementskit-heading\" data-id=\"c5880ef\" data-element_type=\"widget\" data-e-type=\"widget\" data-settings=\"{&quot;_animation&quot;:&quot;fadeInUp&quot;}\" data-widget_type=\"elementskit-heading.default\">\n\t\t\t\t\t<div class=\"ekit-wid-con\" ><div class=\"ekit-heading elementskit-section-title-wraper text_left   ekit_heading_tablet-   ekit_heading_mobile-\"><h2 class=\"ekit-heading--title elementskit-section-title \">ACUSTIUM<\/h2><\/div><\/div>\t\t\t\t<\/div>\n\t\t\t\t<div class=\"elementor-element elementor-element-67cf438 elementor-align-left elementor-invisible elementor-widget elementor-widget-button\" data-id=\"67cf438\" data-element_type=\"widget\" data-e-type=\"widget\" data-settings=\"{&quot;_animation&quot;:&quot;fadeInUp&quot;}\" data-widget_type=\"button.default\">\n\t\t\t\t\t\t\t\t\t\t<a class=\"elementor-button elementor-button-link elementor-size-sm\" href=\"https:\/\/acustium.es\/?page_id=300\">\n\t\t\t\t\t\t<span class=\"elementor-button-content-wrapper\">\n\t\t\t\t\t\t<span class=\"elementor-button-icon\">\n\t\t\t\t<i aria-hidden=\"true\" class=\"icon icon-right-arrow\"><\/i>\t\t\t<\/span>\n\t\t\t\t\t\t\t\t\t<span class=\"elementor-button-text\">Contacta con nosotros<\/span>\n\t\t\t\t\t<\/span>\n\t\t\t\t\t<\/a>\n\t\t\t\t\t\t\t\t<\/div>\n\t\t\t\t<\/div>\n\t\t<div class=\"elementor-element elementor-element-af83362 e-con-full e-flex elementor-invisible e-con e-child\" data-id=\"af83362\" data-element_type=\"container\" data-e-type=\"container\" data-settings=\"{&quot;animation&quot;:&quot;fadeIn&quot;}\">\n\t\t\t\t<div class=\"elementor-element elementor-element-7f867d8 elementor-widget-mobile__width-inherit elementor-icon-list--layout-traditional elementor-list-item-link-full_width elementor-invisible elementor-widget elementor-widget-icon-list\" data-id=\"7f867d8\" data-element_type=\"widget\" data-e-type=\"widget\" data-settings=\"{&quot;_animation&quot;:&quot;fadeInUp&quot;}\" data-widget_type=\"icon-list.default\">\n\t\t\t\t\t\t\t<ul class=\"elementor-icon-list-items\">\n\t\t\t\t\t\t\t<li class=\"elementor-icon-list-item\">\n\t\t\t\t\t\t\t\t\t\t\t<a href=\"https:\/\/www.google.com\/maps\/place\/Refconsa+Servicios+Integrales+Aragoneses+S.L.\/@41.6745912,-0.9523676,17z\/data=!3m2!4b1!5s0xd596b8727fb4cf3:0xfb07d7e94651e598!4m6!3m5!1s0xd596eb9df6744bf:0x7d777fdbb893af35!8m2!3d41.6745912!4d-0.9497927!16s%2Fg%2F1z44bzm4t?entry=ttu&#038;g_ep=EgoyMDI2MDQyNi4wIKXMDSoASAFQAw%3D%3D\">\n\n\t\t\t\t\t\t\t\t\t\t\t<span class=\"elementor-icon-list-text\">Pol. Ind. El Portazgo Nave 20 -50011 Zaragoza<\/span>\n\t\t\t\t\t\t\t\t\t\t\t<\/a>\n\t\t\t\t\t\t\t\t\t<\/li>\n\t\t\t\t\t\t\t\t<li class=\"elementor-icon-list-item\">\n\t\t\t\t\t\t\t\t\t\t<span class=\"elementor-icon-list-text\">611983165<\/span>\n\t\t\t\t\t\t\t\t\t<\/li>\n\t\t\t\t\t\t\t\t<li class=\"elementor-icon-list-item\">\n\t\t\t\t\t\t\t\t\t\t\t<a href=\"mailto:Sheila@acustium.com\">\n\n\t\t\t\t\t\t\t\t\t\t\t<span class=\"elementor-icon-list-text\">Sheila@acustium.com<\/span>\n\t\t\t\t\t\t\t\t\t\t\t<\/a>\n\t\t\t\t\t\t\t\t\t<\/li>\n\t\t\t\t\t\t<\/ul>\n\t\t\t\t\t\t<\/div>\n\t\t\t\t<\/div>\n\t\t<div class=\"elementor-element elementor-element-5dc0aee e-con-full e-flex elementor-invisible e-con e-child\" data-id=\"5dc0aee\" data-element_type=\"container\" data-e-type=\"container\" data-settings=\"{&quot;animation&quot;:&quot;fadeIn&quot;}\">\n\t\t\t\t<div class=\"elementor-element elementor-element-1a33f17 elementor-invisible elementor-widget elementor-widget-heading\" data-id=\"1a33f17\" data-element_type=\"widget\" data-e-type=\"widget\" data-settings=\"{&quot;_animation&quot;:&quot;fadeInUp&quot;}\" data-widget_type=\"heading.default\">\n\t\t\t\t\t<h6 class=\"elementor-heading-title elementor-size-default\">Empresa<\/h6>\t\t\t\t<\/div>\n\t\t\t\t<div class=\"elementor-element elementor-element-ad05b21 elementor-widget-mobile__width-inherit elementor-icon-list--layout-traditional elementor-list-item-link-full_width elementor-invisible elementor-widget elementor-widget-icon-list\" data-id=\"ad05b21\" data-element_type=\"widget\" data-e-type=\"widget\" data-settings=\"{&quot;_animation&quot;:&quot;fadeInUp&quot;}\" data-widget_type=\"icon-list.default\">\n\t\t\t\t\t\t\t<ul class=\"elementor-icon-list-items\">\n\t\t\t\t\t\t\t<li class=\"elementor-icon-list-item\">\n\t\t\t\t\t\t\t\t\t\t\t<a href=\"https:\/\/nueva.acustium.com\/?page_id=448\">\n\n\t\t\t\t\t\t\t\t\t\t\t<span class=\"elementor-icon-list-text\">Nosotros<\/span>\n\t\t\t\t\t\t\t\t\t\t\t<\/a>\n\t\t\t\t\t\t\t\t\t<\/li>\n\t\t\t\t\t\t\t\t<li class=\"elementor-icon-list-item\">\n\t\t\t\t\t\t\t\t\t\t\t<a href=\"https:\/\/nueva.acustium.com\/?page_id=300\">\n\n\t\t\t\t\t\t\t\t\t\t\t<span class=\"elementor-icon-list-text\">Contacto<\/span>\n\t\t\t\t\t\t\t\t\t\t\t<\/a>\n\t\t\t\t\t\t\t\t\t<\/li>\n\t\t\t\t\t\t\t\t<li class=\"elementor-icon-list-item\">\n\t\t\t\t\t\t\t\t\t\t\t<a href=\"https:\/\/nueva.acustium.com\/?page_id=288\">\n\n\t\t\t\t\t\t\t\t\t\t\t<span class=\"elementor-icon-list-text\">Proyectos<\/span>\n\t\t\t\t\t\t\t\t\t\t\t<\/a>\n\t\t\t\t\t\t\t\t\t<\/li>\n\t\t\t\t\t\t<\/ul>\n\t\t\t\t\t\t<\/div>\n\t\t\t\t<\/div>\n\t\t<div class=\"elementor-element elementor-element-78ac31e e-con-full e-flex elementor-invisible e-con e-child\" data-id=\"78ac31e\" data-element_type=\"container\" data-e-type=\"container\" data-settings=\"{&quot;animation&quot;:&quot;fadeIn&quot;}\">\n\t\t\t\t<div class=\"elementor-element elementor-element-b80e24d elementor-invisible elementor-widget elementor-widget-heading\" data-id=\"b80e24d\" data-element_type=\"widget\" data-e-type=\"widget\" data-settings=\"{&quot;_animation&quot;:&quot;fadeInUp&quot;,&quot;_animation_delay&quot;:200}\" data-widget_type=\"heading.default\">\n\t\t\t\t\t<h6 class=\"elementor-heading-title elementor-size-default\">Servicios<\/h6>\t\t\t\t<\/div>\n\t\t\t\t<div class=\"elementor-element elementor-element-bc80b30 elementor-widget-mobile__width-inherit elementor-icon-list--layout-traditional elementor-list-item-link-full_width elementor-invisible elementor-widget elementor-widget-icon-list\" data-id=\"bc80b30\" data-element_type=\"widget\" data-e-type=\"widget\" data-settings=\"{&quot;_animation&quot;:&quot;fadeInUp&quot;,&quot;_animation_delay&quot;:200}\" data-widget_type=\"icon-list.default\">\n\t\t\t\t\t\t\t<ul class=\"elementor-icon-list-items\">\n\t\t\t\t\t\t\t<li class=\"elementor-icon-list-item\">\n\t\t\t\t\t\t\t\t\t\t\t<a href=\"https:\/\/nueva.acustium.com\/?page_id=253\">\n\n\t\t\t\t\t\t\t\t\t\t\t<span class=\"elementor-icon-list-text\">Productos<\/span>\n\t\t\t\t\t\t\t\t\t\t\t<\/a>\n\t\t\t\t\t\t\t\t\t<\/li>\n\t\t\t\t\t\t\t\t<li class=\"elementor-icon-list-item\">\n\t\t\t\t\t\t\t\t\t\t\t<a href=\"https:\/\/nueva.acustium.com\/?page_id=734\">\n\n\t\t\t\t\t\t\t\t\t\t\t<span class=\"elementor-icon-list-text\">by PONGS<\/span>\n\t\t\t\t\t\t\t\t\t\t\t<\/a>\n\t\t\t\t\t\t\t\t\t<\/li>\n\t\t\t\t\t\t\t\t<li class=\"elementor-icon-list-item\">\n\t\t\t\t\t\t\t\t\t\t\t<a href=\"https:\/\/nueva.acustium.com\/?page_id=850\">\n\n\t\t\t\t\t\t\t\t\t\t\t<span class=\"elementor-icon-list-text\">Soluciones<\/span>\n\t\t\t\t\t\t\t\t\t\t\t<\/a>\n\t\t\t\t\t\t\t\t\t<\/li>\n\t\t\t\t\t\t\t\t<li class=\"elementor-icon-list-item\">\n\t\t\t\t\t\t\t\t\t\t\t<a href=\"https:\/\/nueva.acustium.com\/?page_id=270\">\n\n\t\t\t\t\t\t\t\t\t\t\t<span class=\"elementor-icon-list-text\">Sectores<\/span>\n\t\t\t\t\t\t\t\t\t\t\t<\/a>\n\t\t\t\t\t\t\t\t\t<\/li>\n\t\t\t\t\t\t<\/ul>\n\t\t\t\t\t\t<\/div>\n\t\t\t\t<\/div>\n\t\t<div class=\"elementor-element elementor-element-abe12ba e-con-full e-flex elementor-invisible e-con e-child\" data-id=\"abe12ba\" data-element_type=\"container\" data-e-type=\"container\" data-settings=\"{&quot;animation&quot;:&quot;fadeIn&quot;}\">\n\t\t\t\t<div class=\"elementor-element elementor-element-d2d4cfa elementor-invisible elementor-widget elementor-widget-heading\" data-id=\"d2d4cfa\" data-element_type=\"widget\" data-e-type=\"widget\" data-settings=\"{&quot;_animation&quot;:&quot;fadeInUp&quot;,&quot;_animation_delay&quot;:400}\" data-widget_type=\"heading.default\">\n\t\t\t\t\t<h6 class=\"elementor-heading-title elementor-size-default\">Socials<\/h6>\t\t\t\t<\/div>\n\t\t\t\t<div class=\"elementor-element elementor-element-87bbdd4 elementor-widget-mobile__width-inherit elementor-icon-list--layout-traditional elementor-list-item-link-full_width elementor-invisible elementor-widget elementor-widget-icon-list\" data-id=\"87bbdd4\" data-element_type=\"widget\" data-e-type=\"widget\" data-settings=\"{&quot;_animation&quot;:&quot;fadeInUp&quot;,&quot;_animation_delay&quot;:400}\" data-widget_type=\"icon-list.default\">\n\t\t\t\t\t\t\t<ul class=\"elementor-icon-list-items\">\n\t\t\t\t\t\t\t<li class=\"elementor-icon-list-item\">\n\t\t\t\t\t\t\t\t\t\t\t<a href=\"#\">\n\n\t\t\t\t\t\t\t\t\t\t\t<span class=\"elementor-icon-list-text\">Instagram<\/span>\n\t\t\t\t\t\t\t\t\t\t\t<\/a>\n\t\t\t\t\t\t\t\t\t<\/li>\n\t\t\t\t\t\t\t\t<li class=\"elementor-icon-list-item\">\n\t\t\t\t\t\t\t\t\t\t\t<a href=\"#\">\n\n\t\t\t\t\t\t\t\t\t\t\t<span class=\"elementor-icon-list-text\">Facebook<\/span>\n\t\t\t\t\t\t\t\t\t\t\t<\/a>\n\t\t\t\t\t\t\t\t\t<\/li>\n\t\t\t\t\t\t\t\t<li class=\"elementor-icon-list-item\">\n\t\t\t\t\t\t\t\t\t\t\t<a href=\"#\">\n\n\t\t\t\t\t\t\t\t\t\t\t<span class=\"elementor-icon-list-text\">YouTube<\/span>\n\t\t\t\t\t\t\t\t\t\t\t<\/a>\n\t\t\t\t\t\t\t\t\t<\/li>\n\t\t\t\t\t\t\t\t<li class=\"elementor-icon-list-item\">\n\t\t\t\t\t\t\t\t\t\t\t<a href=\"#\">\n\n\t\t\t\t\t\t\t\t\t\t\t<span class=\"elementor-icon-list-text\">LinkedIn<\/span>\n\t\t\t\t\t\t\t\t\t\t\t<\/a>\n\t\t\t\t\t\t\t\t\t<\/li>\n\t\t\t\t\t\t<\/ul>\n\t\t\t\t\t\t<\/div>\n\t\t\t\t<\/div>\n\t\t\t\t\t<\/div>\n\t\t\t\t<\/div>\n\t\t\t\t<\/div>\n\t\t","protected":false},"excerpt":{"rendered":"<p>PRODUCTOS Arquitectura textil by PONGS Fichas de producto Normativa &#8211; Control de calidad Calculadora SOLUCIONES SECTORES NOSOTROS PROYECTOS BLOG CONTACTO X Haz clic en el bot\u00f3n editar contenido para editar\/a\u00f1adir el contenido. DISE\u00d1ANDO EL SONIDO \u00b7 CTE DB-HR &#x1f50a; DISE\u00d1ANDO EL SONIDO Cliente Zona interna &#x1f512; 1Tus datos2Fotos y plano3Tu estancia4Resultado y propuesta Bienvenido\/a Calcula [&hellip;]<\/p>\n","protected":false},"author":1,"featured_media":0,"parent":0,"menu_order":0,"comment_status":"closed","ping_status":"closed","template":"elementor_canvas","meta":{"footnotes":""},"class_list":["post-533","page","type-page","status-publish","hentry"],"yoast_head":"<!-- This site is optimized with the Yoast SEO plugin v27.7 - https:\/\/yoast.com\/product\/yoast-seo-wordpress\/ -->\r\n<title>Calculadora - ACUSTIUM<\/title>\r\n<meta name=\"robots\" content=\"index, follow, max-snippet:-1, max-image-preview:large, max-video-preview:-1\" \/>\r\n<link rel=\"canonical\" href=\"https:\/\/acustium.es\/?page_id=533\" \/>\r\n<meta property=\"og:locale\" content=\"es_ES\" \/>\r\n<meta property=\"og:type\" content=\"article\" \/>\r\n<meta property=\"og:title\" content=\"Calculadora - ACUSTIUM\" \/>\r\n<meta property=\"og:description\" content=\"PRODUCTOS Arquitectura textil by PONGS Fichas de producto Normativa &#8211; Control de calidad Calculadora SOLUCIONES SECTORES NOSOTROS PROYECTOS BLOG CONTACTO X Haz clic en el bot\u00f3n editar contenido para editar\/a\u00f1adir el contenido. DISE\u00d1ANDO EL SONIDO \u00b7 CTE DB-HR &#x1f50a; DISE\u00d1ANDO EL SONIDO Cliente Zona interna &#x1f512; 1Tus datos2Fotos y plano3Tu estancia4Resultado y propuesta Bienvenido\/a Calcula [&hellip;]\" \/>\r\n<meta property=\"og:url\" content=\"https:\/\/acustium.es\/?page_id=533\" \/>\r\n<meta property=\"og:site_name\" content=\"ACUSTIUM\" \/>\r\n<meta property=\"article:modified_time\" content=\"2026-06-16T11:32:17+00:00\" \/>\r\n<meta property=\"og:image\" content=\"https:\/\/acustium.es\/wp-content\/uploads\/2026\/05\/6b49573f-a8ce-480b-ad2a-097dbe2359f6.png\" \/>\r\n\t<meta property=\"og:image:width\" content=\"496\" \/>\r\n\t<meta property=\"og:image:height\" content=\"217\" \/>\r\n\t<meta property=\"og:image:type\" content=\"image\/png\" \/>\r\n<meta name=\"twitter:card\" content=\"summary_large_image\" \/>\r\n<meta name=\"twitter:label1\" content=\"Tiempo de lectura\" \/>\n\t<meta name=\"twitter:data1\" content=\"1 minuto\" \/>\r\n<script type=\"application\/ld+json\" class=\"yoast-schema-graph\">{\"@context\":\"https:\\\/\\\/schema.org\",\"@graph\":[{\"@type\":\"WebPage\",\"@id\":\"https:\\\/\\\/acustium.es\\\/?page_id=533\",\"url\":\"https:\\\/\\\/acustium.es\\\/?page_id=533\",\"name\":\"Calculadora - ACUSTIUM\",\"isPartOf\":{\"@id\":\"https:\\\/\\\/acustium.es\\\/#website\"},\"primaryImageOfPage\":{\"@id\":\"https:\\\/\\\/acustium.es\\\/?page_id=533#primaryimage\"},\"image\":{\"@id\":\"https:\\\/\\\/acustium.es\\\/?page_id=533#primaryimage\"},\"thumbnailUrl\":\"https:\\\/\\\/acustium.es\\\/wp-content\\\/uploads\\\/2026\\\/05\\\/6b49573f-a8ce-480b-ad2a-097dbe2359f6.png\",\"datePublished\":\"2026-03-31T12:06:32+00:00\",\"dateModified\":\"2026-06-16T11:32:17+00:00\",\"breadcrumb\":{\"@id\":\"https:\\\/\\\/acustium.es\\\/?page_id=533#breadcrumb\"},\"inLanguage\":\"es\",\"potentialAction\":[{\"@type\":\"ReadAction\",\"target\":[\"https:\\\/\\\/acustium.es\\\/?page_id=533\"]}]},{\"@type\":\"ImageObject\",\"inLanguage\":\"es\",\"@id\":\"https:\\\/\\\/acustium.es\\\/?page_id=533#primaryimage\",\"url\":\"https:\\\/\\\/acustium.es\\\/wp-content\\\/uploads\\\/2026\\\/05\\\/6b49573f-a8ce-480b-ad2a-097dbe2359f6.png\",\"contentUrl\":\"https:\\\/\\\/acustium.es\\\/wp-content\\\/uploads\\\/2026\\\/05\\\/6b49573f-a8ce-480b-ad2a-097dbe2359f6.png\",\"width\":496,\"height\":217},{\"@type\":\"BreadcrumbList\",\"@id\":\"https:\\\/\\\/acustium.es\\\/?page_id=533#breadcrumb\",\"itemListElement\":[{\"@type\":\"ListItem\",\"position\":1,\"name\":\"Portada\",\"item\":\"https:\\\/\\\/acustium.es\\\/\"},{\"@type\":\"ListItem\",\"position\":2,\"name\":\"Calculadora\"}]},{\"@type\":\"WebSite\",\"@id\":\"https:\\\/\\\/acustium.es\\\/#website\",\"url\":\"https:\\\/\\\/acustium.es\\\/\",\"name\":\"ACUSTIUM\",\"description\":\"ACUSTIUM\",\"potentialAction\":[{\"@type\":\"SearchAction\",\"target\":{\"@type\":\"EntryPoint\",\"urlTemplate\":\"https:\\\/\\\/acustium.es\\\/?s={search_term_string}\"},\"query-input\":{\"@type\":\"PropertyValueSpecification\",\"valueRequired\":true,\"valueName\":\"search_term_string\"}}],\"inLanguage\":\"es\"}]}<\/script>\r\n<!-- \/ Yoast SEO plugin. -->","yoast_head_json":{"title":"Calculadora - ACUSTIUM","robots":{"index":"index","follow":"follow","max-snippet":"max-snippet:-1","max-image-preview":"max-image-preview:large","max-video-preview":"max-video-preview:-1"},"canonical":"https:\/\/acustium.es\/?page_id=533","og_locale":"es_ES","og_type":"article","og_title":"Calculadora - ACUSTIUM","og_description":"PRODUCTOS Arquitectura textil by PONGS Fichas de producto Normativa &#8211; Control de calidad Calculadora SOLUCIONES SECTORES NOSOTROS PROYECTOS BLOG CONTACTO X Haz clic en el bot\u00f3n editar contenido para editar\/a\u00f1adir el contenido. DISE\u00d1ANDO EL SONIDO \u00b7 CTE DB-HR &#x1f50a; DISE\u00d1ANDO EL SONIDO Cliente Zona interna &#x1f512; 1Tus datos2Fotos y plano3Tu estancia4Resultado y propuesta Bienvenido\/a Calcula [&hellip;]","og_url":"https:\/\/acustium.es\/?page_id=533","og_site_name":"ACUSTIUM","article_modified_time":"2026-06-16T11:32:17+00:00","og_image":[{"width":496,"height":217,"url":"https:\/\/acustium.es\/wp-content\/uploads\/2026\/05\/6b49573f-a8ce-480b-ad2a-097dbe2359f6.png","type":"image\/png"}],"twitter_card":"summary_large_image","twitter_misc":{"Tiempo de lectura":"1 minuto"},"schema":{"@context":"https:\/\/schema.org","@graph":[{"@type":"WebPage","@id":"https:\/\/acustium.es\/?page_id=533","url":"https:\/\/acustium.es\/?page_id=533","name":"Calculadora - ACUSTIUM","isPartOf":{"@id":"https:\/\/acustium.es\/#website"},"primaryImageOfPage":{"@id":"https:\/\/acustium.es\/?page_id=533#primaryimage"},"image":{"@id":"https:\/\/acustium.es\/?page_id=533#primaryimage"},"thumbnailUrl":"https:\/\/acustium.es\/wp-content\/uploads\/2026\/05\/6b49573f-a8ce-480b-ad2a-097dbe2359f6.png","datePublished":"2026-03-31T12:06:32+00:00","dateModified":"2026-06-16T11:32:17+00:00","breadcrumb":{"@id":"https:\/\/acustium.es\/?page_id=533#breadcrumb"},"inLanguage":"es","potentialAction":[{"@type":"ReadAction","target":["https:\/\/acustium.es\/?page_id=533"]}]},{"@type":"ImageObject","inLanguage":"es","@id":"https:\/\/acustium.es\/?page_id=533#primaryimage","url":"https:\/\/acustium.es\/wp-content\/uploads\/2026\/05\/6b49573f-a8ce-480b-ad2a-097dbe2359f6.png","contentUrl":"https:\/\/acustium.es\/wp-content\/uploads\/2026\/05\/6b49573f-a8ce-480b-ad2a-097dbe2359f6.png","width":496,"height":217},{"@type":"BreadcrumbList","@id":"https:\/\/acustium.es\/?page_id=533#breadcrumb","itemListElement":[{"@type":"ListItem","position":1,"name":"Portada","item":"https:\/\/acustium.es\/"},{"@type":"ListItem","position":2,"name":"Calculadora"}]},{"@type":"WebSite","@id":"https:\/\/acustium.es\/#website","url":"https:\/\/acustium.es\/","name":"ACUSTIUM","description":"ACUSTIUM","potentialAction":[{"@type":"SearchAction","target":{"@type":"EntryPoint","urlTemplate":"https:\/\/acustium.es\/?s={search_term_string}"},"query-input":{"@type":"PropertyValueSpecification","valueRequired":true,"valueName":"search_term_string"}}],"inLanguage":"es"}]}},"_links":{"self":[{"href":"https:\/\/acustium.es\/index.php?rest_route=\/wp\/v2\/pages\/533","targetHints":{"allow":["GET"]}}],"collection":[{"href":"https:\/\/acustium.es\/index.php?rest_route=\/wp\/v2\/pages"}],"about":[{"href":"https:\/\/acustium.es\/index.php?rest_route=\/wp\/v2\/types\/page"}],"author":[{"embeddable":true,"href":"https:\/\/acustium.es\/index.php?rest_route=\/wp\/v2\/users\/1"}],"replies":[{"embeddable":true,"href":"https:\/\/acustium.es\/index.php?rest_route=%2Fwp%2Fv2%2Fcomments&post=533"}],"version-history":[{"count":14,"href":"https:\/\/acustium.es\/index.php?rest_route=\/wp\/v2\/pages\/533\/revisions"}],"predecessor-version":[{"id":1509,"href":"https:\/\/acustium.es\/index.php?rest_route=\/wp\/v2\/pages\/533\/revisions\/1509"}],"wp:attachment":[{"href":"https:\/\/acustium.es\/index.php?rest_route=%2Fwp%2Fv2%2Fmedia&parent=533"}],"curies":[{"name":"wp","href":"https:\/\/api.w.org\/{rel}","templated":true}]}}