{"id":25656,"date":"2026-05-20T17:31:46","date_gmt":"2026-05-20T15:31:46","guid":{"rendered":"https:\/\/immune.institute\/?page_id=25656"},"modified":"2026-05-20T17:31:47","modified_gmt":"2026-05-20T15:31:47","slug":"formulario-de-cualificacion","status":"publish","type":"page","link":"https:\/\/immune.institute\/en\/formulario-de-cualificacion\/","title":{"rendered":"Formulario de cualificaci\u00f3n"},"content":{"rendered":"\n<!--\n  ============================================================\n  FORMULARIO HUBSPOT MULTI-PASO CON ANIMACIONES Y VALIDACI\u00d3N\n  ============================================================\n  Este fragmento embebe un formulario de HubSpot (portal 6604339)\n  y a\u00f1ade tres capas de funcionalidad personalizada:\n    1. Spinner de carga mientras HubSpot inyecta el HTML del form\n    2. Animaciones GSAP en cada transici\u00f3n entre pasos\n    3. Validaci\u00f3n de campos obligatorios antes de avanzar al paso siguiente,\n       con limpieza de errores en tiempo real\n  ============================================================\n-->\n\n<!-- SDK de HubSpot Forms: renderiza el formulario en .hs-form-html -->\n<script\n  src=\"https:\/\/js.hsforms.net\/forms\/embed\/developer\/6604339.js\"\n  defer=\"\"\n><\/script>\n\n<!-- GSAP 3: librer\u00eda de animaciones usada para los fade-in de cada paso -->\n<script src=\"https:\/\/cdnjs.cloudflare.com\/ajax\/libs\/gsap\/3.12.5\/gsap.min.js\"><\/script>\n\n<!--\n  Spinner visible mientras el formulario carga.\n  Se oculta con GSAP una vez que HubSpot inyecta el primer paso.\n  aria-hidden=\"true\" lo excluye de lectores de pantalla.\n-->\n<div class=\"hs-form-loader\" aria-hidden=\"true\">\n  <span class=\"hs-form-loader__spinner\"><\/span>\n<\/div>\n\n<!--\n  Contenedor donde HubSpot inyecta el HTML del formulario.\n  - data-region: regi\u00f3n del servidor HubSpot (na1 = Norteam\u00e9rica)\n  - data-form-id: ID \u00fanico del formulario en HubSpot\n  - data-portal-id: ID del portal\/cuenta de HubSpot\n  Arranca con opacity:0 (ver CSS) y se revela mediante GSAP.\n-->\n<div\n  class=\"hs-form-html\"\n  data-region=\"na1\"\n  data-form-id=\"4d803d8f-a683-41b7-bef8-f2d01ba5a4f8\"\n  data-portal-id=\"6604339\"\n><\/div>\n\n<script>\n  document.addEventListener(\"DOMContentLoaded\", function () {\n    \/\/ Referencias a los dos elementos principales del DOM\n    const formContainer = document.querySelector(\".hs-form-html\");\n    const loader = document.querySelector(\".hs-form-loader\");\n\n    \/\/ Si el contenedor no existe en la p\u00e1gina, no hacemos nada\n    if (!formContainer) return;\n\n    \/\/ \u2500\u2500\u2500 GSAP: Animar filas del paso visible \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\n    \/**\n     * Anima con GSAP las filas (.hsfc-Row) del paso que acaba de hacerse visible.\n     * Cada fila entra desde abajo (y:28px) con un peque\u00f1o stagger entre ellas.\n     *\n     * @param {Element} step      - Elemento del paso a animar\n     * @param {boolean} skipFirst - Si es true, omite las dos primeras filas\n     *                              (usadas en el primer paso para no reanimar\n     *                              el t\u00edtulo\/subt\u00edtulo ya visible)\n     *\/\n    function animateStepIn(step, skipFirst) {\n      if (!step) return;\n      let targets = Array.from(step.querySelectorAll(\".hsfc-Row\"));\n      if (!targets.length) return;\n      if (skipFirst) targets = targets.slice(3); \/\/ Omitir t\u00edtulo y subt\u00edtulo del paso inicial\n\n      gsap.fromTo(\n        targets,\n        { opacity: 0, y: 40 }, \/\/ Estado inicial: invisible y desplazado hacia abajo\n        {\n          opacity: 1,\n          y: 0,\n          duration: 0.55,\n          ease: \"power3.out\",\n          stagger: 0.08, \/\/ 80ms de retardo entre cada fila\n          clearProps: \"transform,opacity\", \/\/ Limpia las propiedades inline al terminar\n        },\n      );\n    }\n\n    \/\/ \u2500\u2500\u2500 Observar cambios de visibilidad entre pasos \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\n    \/**\n     * Referencia al \u00faltimo paso visible para evitar reanimar el mismo paso\n     * si el MutationObserver dispara m\u00faltiples veces.\n     * @type {Element|null}\n     *\/\n    let lastVisibleStep = null;\n\n    \/**\n     * Observa cambios en el atributo `style` de cada paso.\n     * HubSpot muestra\/oculta pasos cambiando display:none, por lo que\n     * este observer detecta cu\u00e1ndo un paso pasa a estar visible y\n     * lanza la animaci\u00f3n de entrada.\n     *\/\n    const stepVisibilityObserver = new MutationObserver(function (mutations) {\n      mutations.forEach(function (mutation) {\n        const target = mutation.target;\n        if (target.getAttribute(\"data-hsfc-id\") === \"Step\") {\n          const isNowVisible = target.style.display !== \"none\";\n          \/\/ Solo animar si el paso se vuelve visible y es diferente al anterior\n          if (isNowVisible && target !== lastVisibleStep) {\n            lastVisibleStep = target;\n            animateStepIn(target);\n          }\n        }\n      });\n    });\n\n    \/**\n     * Registra en stepVisibilityObserver todos los pasos [data-hsfc-id=\"Step\"]\n     * que a\u00fan no han sido observados (marcados con data-gsap-watched=\"1\").\n     * Se llama cada vez que HubSpot inyecta nuevo contenido en el DOM.\n     *\/\n    function watchSteps() {\n      formContainer\n        .querySelectorAll('[data-hsfc-id=\"Step\"]')\n        .forEach(function (step) {\n          if (step.dataset.gsapWatched) return; \/\/ Ya observado, saltar\n          step.dataset.gsapWatched = \"1\";\n          stepVisibilityObserver.observe(step, {\n            attributes: true,\n            attributeFilter: [\"style\"], \/\/ Solo nos interesan cambios en el atributo style\n          });\n        });\n    }\n\n    \/\/ \u2500\u2500\u2500 Helpers \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\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\n\n    \/**\n     * Devuelve el paso actualmente visible (el que no tiene display:none).\n     * @returns {Element|undefined}\n     *\/\n    function getActiveStep() {\n      return Array.from(\n        formContainer.querySelectorAll('[data-hsfc-id=\"Step\"]'),\n      ).find(function (step) {\n        return step.style.display !== \"none\";\n      });\n    }\n\n    \/**\n     * Comprueba si un bot\u00f3n es el bot\u00f3n \"Siguiente\" del formulario.\n     * @param {HTMLButtonElement} btn\n     * @returns {boolean}\n     *\/\n    function isNextButton(btn) {\n      return btn.type === \"button\" && btn.textContent.trim() === \"Siguiente\";\n    }\n\n    \/**\n     * Muestra un mensaje de error personalizado dentro de un contenedor.\n     * No inserta el mensaje si ya existe uno previo (evita duplicados).\n     * @param {Element} container - Elemento donde se a\u00f1ade el error\n     * @param {string}  msg       - Texto del mensaje de error\n     *\/\n    function showError(container, msg) {\n      if (container.querySelector(\".custom-err-msg\")) return; \/\/ Ya existe, no duplicar\n      var el = document.createElement(\"div\");\n      el.className = \"custom-err-msg hsfc-ErrorAlert\";\n      el.textContent = msg;\n      container.appendChild(el);\n    }\n\n    \/**\n     * Elimina el mensaje de error personalizado de un contenedor, si existe.\n     * @param {Element} container\n     *\/\n    function removeError(container) {\n      var el = container.querySelector(\".custom-err-msg\");\n      if (el) el.remove();\n    }\n\n    \/\/ \u2500\u2500\u2500 Validaci\u00f3n \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\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\n    \/**\n     * Valida todos los campos obligatorios del paso actual antes de avanzar.\n     * Gestiona cuatro tipos de campo:\n     *   - Dropdowns (.hsfc-DropdownField)\n     *   - Grupos de checkboxes (.hsfc-CheckboxFieldGroup)\n     *   - Grupos de radio buttons (.hsfc-RadioFieldGroup)\n     *   - Inputs de texto, email y tel\u00e9fono (input[required])\n     *\n     * A\u00f1ade la clase .hs-input-error a los campos inv\u00e1lidos y muestra\n     * mensajes de error con showError(). Devuelve false si hay alg\u00fan error.\n     *\n     * @param {Element} step - Paso del formulario a validar\n     * @returns {boolean}    - true si todos los campos son v\u00e1lidos\n     *\/\n    function validateStep(step) {\n      if (!step) return true;\n      let allValid = true;\n\n      \/\/ \u2500\u2500 Dropdowns \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\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\n      \/\/ HubSpot usa un input visible (combobox) + un input hidden con el valor real.\n      \/\/ Validamos el input hidden porque es el que almacena la opci\u00f3n seleccionada.\n      step.querySelectorAll(\".hsfc-DropdownField\").forEach(function (field) {\n        const visibleInput = field.querySelector(\n          'input[aria-required=\"true\"][role=\"combobox\"]',\n        );\n        if (!visibleInput) return;\n        const hiddenInput = field.querySelector('input[type=\"hidden\"]');\n        const hasValue = hiddenInput && hiddenInput.value.trim() !== \"\";\n        if (!hasValue) {\n          allValid = false;\n          visibleInput.classList.add(\"hs-input-error\");\n          showError(field, \"Rellena este campo obligatorio.\");\n        } else {\n          visibleInput.classList.remove(\"hs-input-error\");\n          removeError(field);\n        }\n      });\n\n      \/\/ \u2500\u2500 Checkboxes m\u00faltiples \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\n      \/\/ Agrupa los checkboxes por su contenedor (.hsfc-CheckboxFieldGroup)\n      \/\/ y verifica que al menos uno est\u00e9 marcado en cada grupo.\n      const checkboxGroups = {};\n      step\n        .querySelectorAll(\n          '.hsfc-CheckboxFieldGroup input[type=\"checkbox\"][aria-required=\"true\"]',\n        )\n        .forEach(function (cb) {\n          const groupId = cb.closest(\".hsfc-CheckboxFieldGroup\")?.id;\n          if (!groupId) return;\n          if (!checkboxGroups[groupId])\n            checkboxGroups[groupId] = {\n              checked: false,\n              wrapper: cb.closest(\".hsfc-CheckboxFieldGroup\"),\n            };\n          if (cb.checked) checkboxGroups[groupId].checked = true;\n        });\n      Object.keys(checkboxGroups).forEach(function (gid) {\n        var g = checkboxGroups[gid];\n        if (!g.checked) {\n          allValid = false;\n          showError(g.wrapper, \"Selecciona al menos una opci\u00f3n.\");\n        } else {\n          removeError(g.wrapper);\n        }\n      });\n\n      \/\/ \u2500\u2500 Radio buttons \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\u2500\u2500\u2500\u2500\u2500\u2500\n      \/\/ Igual que los checkboxes: agrupa por contenedor y verifica\n      \/\/ que exactamente uno est\u00e9 seleccionado en cada grupo.\n      const radioGroups = {};\n      step\n        .querySelectorAll(\n          '.hsfc-RadioFieldGroup input[type=\"radio\"][aria-required=\"true\"]',\n        )\n        .forEach(function (r) {\n          const groupId = r.closest(\".hsfc-RadioFieldGroup\")?.id;\n          if (!groupId) return;\n          if (!radioGroups[groupId])\n            radioGroups[groupId] = {\n              checked: false,\n              wrapper: r.closest(\".hsfc-RadioFieldGroup\"),\n            };\n          if (r.checked) radioGroups[groupId].checked = true;\n        });\n      Object.keys(radioGroups).forEach(function (gid) {\n        var g = radioGroups[gid];\n        if (!g.checked) {\n          allValid = false;\n          showError(g.wrapper, \"Selecciona una opci\u00f3n.\");\n        } else {\n          removeError(g.wrapper);\n        }\n      });\n\n      \/\/ \u2500\u2500 Inputs de texto, email y tel\u00e9fono \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\n      \/\/ Excluimos inputs dentro de .hsfc-DropdownOptions para evitar\n      \/\/ validar el campo de b\u00fasqueda interno del dropdown.\n      step\n        .querySelectorAll('input[required]:not([type=\"hidden\"])')\n        .forEach(function (input) {\n          if (input.closest(\".hsfc-DropdownOptions\")) return;\n          var wrapper =\n            input.closest(\".hsfc-TextField\") ||\n            input.closest(\".hsfc-EmailField\") ||\n            input.closest(\".hsfc-PhoneField\");\n          if (!input.value || input.value.trim() === \"\") {\n            allValid = false;\n            input.classList.add(\"hs-input-error\");\n            if (wrapper) showError(wrapper, \"Rellena este campo obligatorio.\");\n          } else {\n            input.classList.remove(\"hs-input-error\");\n            if (wrapper) removeError(wrapper);\n          }\n        });\n\n      return allValid;\n    }\n\n    \/\/ \u2500\u2500\u2500 Limpieza de errores en tiempo real \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\n    \/**\n     * Adjunta listeners a todos los campos del paso actual para eliminar\n     * los mensajes de error en cuanto el usuario corrige el valor.\n     * Usa data-err-watch=\"1\" como flag para no registrar el mismo listener dos veces.\n     *\n     * Cubre tres casos:\n     *   - Dropdowns: observa el input hidden con MutationObserver + evento focus\n     *   - Checkboxes\/radios: evento change\n     *   - Inputs de texto: evento input\n     *\n     * @param {Element} step - Paso del formulario al que adjuntar los listeners\n     *\/\n    function attachLiveCleanup(step) {\n      \/\/ \u2500\u2500 Dropdowns \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\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\n      step.querySelectorAll(\".hsfc-DropdownField\").forEach(function (field) {\n        const hiddenInput = field.querySelector('input[type=\"hidden\"]');\n        const visibleInput = field.querySelector('input[role=\"combobox\"]');\n        if (!hiddenInput || !visibleInput || hiddenInput.dataset.errWatch)\n          return;\n        hiddenInput.dataset.errWatch = \"1\";\n\n        \/\/ MutationObserver: detecta cuando HubSpot actualiza el value del input hidden\n        new MutationObserver(function () {\n          if (hiddenInput.value.trim() !== \"\") {\n            visibleInput.classList.remove(\"hs-input-error\");\n            removeError(field);\n          }\n        }).observe(hiddenInput, {\n          attributes: true,\n          attributeFilter: [\"value\"],\n        });\n\n        \/\/ Fallback: comprueba el valor al hacer focus (algunos navegadores no disparan\n        \/\/ la mutaci\u00f3n inmediatamente)\n        visibleInput.addEventListener(\"focus\", function () {\n          setTimeout(function () {\n            if (hiddenInput.value.trim() !== \"\") {\n              visibleInput.classList.remove(\"hs-input-error\");\n              removeError(field);\n            }\n          }, 500);\n        });\n      });\n\n      \/\/ \u2500\u2500 Checkboxes y radio buttons \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\n      step\n        .querySelectorAll('input[type=\"checkbox\"], input[type=\"radio\"]')\n        .forEach(function (input) {\n          if (input.dataset.errWatch) return;\n          input.dataset.errWatch = \"1\";\n          input.addEventListener(\"change\", function () {\n            var wrapper =\n              input.closest(\".hsfc-CheckboxFieldGroup\") ||\n              input.closest(\".hsfc-RadioFieldGroup\");\n            if (wrapper) removeError(wrapper);\n          });\n        });\n\n      \/\/ \u2500\u2500 Inputs de texto, email y tel\u00e9fono \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\n      step\n        .querySelectorAll('input[required]:not([type=\"hidden\"])')\n        .forEach(function (input) {\n          if (input.dataset.errWatch) return;\n          input.dataset.errWatch = \"1\";\n          input.addEventListener(\"input\", function () {\n            if (input.value.trim() !== \"\") {\n              input.classList.remove(\"hs-input-error\");\n              var wrapper =\n                input.closest(\".hsfc-TextField\") ||\n                input.closest(\".hsfc-EmailField\") ||\n                input.closest(\".hsfc-PhoneField\");\n              if (wrapper) removeError(wrapper);\n            }\n          });\n        });\n    }\n\n    \/\/ \u2500\u2500\u2500 Observer principal \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\u2500\n    \/**\n     * Observa el contenedor del formulario para detectar cu\u00e1ndo HubSpot\n     * inyecta nuevos nodos (pasos, botones, etc.) y:\n     *   1. Registra los nuevos pasos en stepVisibilityObserver (watchSteps)\n     *   2. Intercepta el bot\u00f3n \"Siguiente\" para ejecutar la validaci\u00f3n\n     *      antes de que HubSpot procese el clic (fase de captura, useCapture=true)\n     *\/\n    new MutationObserver(function () {\n      watchSteps();\n\n      formContainer\n        .querySelectorAll('button[data-hsfc-id=\"Button\"]')\n        .forEach(function (btn) {\n          if (btn.dataset.validated || !isNextButton(btn)) return;\n          btn.dataset.validated = \"true\"; \/\/ Flag para no registrar el listener dos veces\n\n          \/\/ useCapture=true: se ejecuta en la fase de captura, antes que los\n          \/\/ handlers nativos de HubSpot, permitiendo bloquear el avance con\n          \/\/ e.stopImmediatePropagation() si la validaci\u00f3n falla\n          btn.addEventListener(\n            \"click\",\n            function (e) {\n              var step = getActiveStep();\n              if (!step) return;\n              attachLiveCleanup(step); \/\/ Registrar limpieza en tiempo real si a\u00fan no est\u00e1\n              if (!validateStep(step)) {\n                e.preventDefault();\n                e.stopImmediatePropagation(); \/\/ Impide otros handlers en el mismo elemento\n                e.stopPropagation(); \/\/ Impide que el evento burbujee a handlers delegados de HubSpot\n              }\n            },\n            true,\n          );\n        });\n    }).observe(formContainer, { childList: true, subtree: true });\n\n    \/\/ \u2500\u2500\u2500 Fade in inicial + animaci\u00f3n del primer paso \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\n    \/**\n     * Espera a que HubSpot inyecte el primer paso del formulario en el DOM.\n     * Una vez detectado:\n     *   1. Registra el paso en los observadores\n     *   2. Desconecta este observer (ya no es necesario)\n     *   3. Oculta el spinner con un fade-out (0.3s, con 0.8s de delay)\n     *   4. Revela el formulario con un fade-in (0.5s)\n     *   5. Anima las filas del primer paso (skipFirst=true para omitir\n     *      t\u00edtulo y subt\u00edtulo que ya est\u00e1n posicionados)\n     *\/\n    const initialStepObserver = new MutationObserver(function () {\n      const firstStep = getActiveStep();\n      if (!firstStep) return;\n\n      lastVisibleStep = firstStep;\n      watchSteps();\n      initialStepObserver.disconnect(); \/\/ Observer de un solo uso\n\n      \/\/ Secuencia de animaci\u00f3n: loader \u2192 formulario \u2192 filas del primer paso\n      gsap.to(loader, {\n        opacity: 0,\n        duration: 0.3,\n        ease: \"power2.in\",\n        delay: 0.8, \/\/ Peque\u00f1o delay para evitar un parpadeo si el form carga r\u00e1pido\n        onComplete: function () {\n          loader.style.display = \"none\"; \/\/ Sacar del flujo tras el fade-out\n          gsap.to(formContainer, {\n            opacity: 1,\n            duration: 0.5,\n            ease: \"power2.out\",\n            onComplete: function () {\n              animateStepIn(firstStep, true); \/\/ Animar filas del primer paso\n            },\n          });\n        },\n      });\n    });\n\n    initialStepObserver.observe(formContainer, {\n      childList: true,\n      subtree: true,\n    });\n  });\n<\/script>\n\n<style>\n  :root {\n    \/* Hereda el gap del bloque de WordPress para el espaciado entre columnas *\/\n    --hsf-row__horizontal-spacing: var(--wp--style--block-gap);\n  }\n\n  \/* \u2500\u2500 Loader \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\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500 *\/\n\n  \/* Ocupa toda la altura visible y centra el spinner mientras carga el form *\/\n  .hs-form-loader {\n    min-height: 90svh;\n    width: 100%;\n    display: flex;\n    align-items: center;\n    justify-content: center;\n    position: absolute; \/* Se superpone al formulario durante la carga *\/\n  }\n\n  \/* Spinner CSS puro: c\u00edrculo con borde superior transparente que gira *\/\n  .hs-form-loader__spinner {\n    width: 32px;\n    height: 32px;\n    border: 2px solid currentColor;\n    border-top-color: transparent; \/* Crea el efecto de arco incompleto *\/\n    border-radius: 50%;\n    opacity: 0.3;\n    animation: hsSpinner 0.75s linear infinite;\n  }\n\n  @keyframes hsSpinner {\n    to {\n      transform: rotate(360deg);\n    }\n  }\n\n  \/* \u2500\u2500 Formulario \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\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500 *\/\n\n  \/* Invisible al inicio; GSAP lo revela con opacity:1 tras ocultar el loader *\/\n  .hs-form-html {\n    margin-top: 0;\n    text-wrap: balance;\n    opacity: 0;\n  }\n\n  \/* El formulario, sus pasos y su contenido ocupan al menos el 75% de la viewport *\/\n  .hs-form-html,\n  .hsfc-Step,\n  .hsfc-Step__Content {\n    min-height: 90svh;\n  }\n\n  \/* Columna vertical para distribuir el contenido del paso *\/\n  .hsfc-Step__Content {\n    display: flex;\n    flex-direction: column;\n  }\n\n  \/* El primer elemento empuja el contenido hacia el centro vertical (margin-top:auto) *\/\n  .hsfc-Step__Content > :first-child {\n    margin-top: auto;\n  }\n\n  \/* El \u00faltimo elemento tambi\u00e9n usa margin-top:auto para distribuir el espacio *\/\n  .hsfc-Step__Content > :last-child {\n    margin-top: auto;\n  }\n\n  \/* Cada elemento hijo se centra horizontalmente y tiene un ancho m\u00e1ximo de 40rem *\/\n  .hsfc-Row {\n    margin-left: auto;\n    margin-right: auto;\n    width: 100%;\n    max-width: 40rem;\n  }\n\n  \/* Etiquetas de campo (sin input dentro): tama\u00f1o de fuente \"medium\" *\/\n  .hsfc-FieldLabel:not(:has(input)) {\n    --hsf-field-label__font-size: var(--wp--preset--font-size--medium);\n  }\n\n  [data-hsfc-id=\"Renderer\"] .hsfc-ProgressBar__Progress > div {\n    height: 4px;\n  }\n\n  \/* Normaliza los botones del formulario: peso normal y altura de l\u00ednea compacta *\/\n  [data-hsfc-id=\"Renderer\"] .hsfc-Button {\n    font-weight: normal;\n    line-height: 1;\n  }\n\n  \/* \u2500\u2500 Validaci\u00f3n \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\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500 *\/\n\n  \/*\n   * Oculta los errores nativos de HubSpot (.hsfc-ErrorAlert) cuando ya existe\n   * un error personalizado (.custom-err-msg) en el mismo contenedor padre.\n   * Esto evita que ambos mensajes aparezcan duplicados a la vez.\n   *\/\n  *:has(> .custom-err-msg) > .hsfc-ErrorAlert:not(.custom-err-msg) {\n    display: none;\n  }\n\n  \/* Borde rojo + sombra suave para campos que no superan la validaci\u00f3n *\/\n  .hs-input-error {\n    border: 2px solid rgba(229, 21, 32, 1) !important;\n    box-shadow: 0 0 4px rgba(229, 21, 32, 0.3) !important;\n  }\n\n  \/* Mensaje de error personalizado: hereda el espaciado vertical del tema *\/\n  .custom-err-msg {\n    margin-top: var(\n      --hsf-module__vertical-spacing,\n      var(--hsf-default-module__vertical-spacing)\n    );\n  }\n<\/style>\n\n","protected":false},"excerpt":{"rendered":"","protected":false},"author":1,"featured_media":0,"parent":0,"menu_order":0,"comment_status":"closed","ping_status":"closed","template":"simple","meta":{"_acf_changed":false,"ai_generated_summary":"","footnotes":""},"class_list":["post-25656","page","type-page","status-publish","hentry"],"acf":[],"_links":{"self":[{"href":"https:\/\/immune.institute\/en\/wp-json\/wp\/v2\/pages\/25656","targetHints":{"allow":["GET"]}}],"collection":[{"href":"https:\/\/immune.institute\/en\/wp-json\/wp\/v2\/pages"}],"about":[{"href":"https:\/\/immune.institute\/en\/wp-json\/wp\/v2\/types\/page"}],"author":[{"embeddable":true,"href":"https:\/\/immune.institute\/en\/wp-json\/wp\/v2\/users\/1"}],"replies":[{"embeddable":true,"href":"https:\/\/immune.institute\/en\/wp-json\/wp\/v2\/comments?post=25656"}],"version-history":[{"count":0,"href":"https:\/\/immune.institute\/en\/wp-json\/wp\/v2\/pages\/25656\/revisions"}],"wp:attachment":[{"href":"https:\/\/immune.institute\/en\/wp-json\/wp\/v2\/media?parent=25656"}],"curies":[{"name":"wp","href":"https:\/\/api.w.org\/{rel}","templated":true}]}}